[object Object]

← back to Wallco Ai

wallco / Scenic + Studio sub-brand landings, Studio trade-gate, Scenic hospitality fit

9e6a14276fa81d36e4dcbf2a8e7e9ce93d09e391 · 2026-05-21 20:12:32 -0700 · Steve Abrams

- /scenic and /studio now render full landing pages (hero from #38842 / #39055,
  brand pitch with live design count, 6-design featured grid, prominent 'Browse
  all' CTA), replacing the bare ?line=… redirects.
- Renamed the legacy Designer Studio composer from /studio to /designer-studio
  (nav-link added next to Studio) so the brand path wins.
- detail-spec-panel now carries data-product-line + data-design-id, picked up
  by the wall-size-calculator compute(). For Studio designs, the 4th tile in
  both mural and wallpaper branches swaps the public $X-$Y material estimate
  for a 'Contact for trade pricing →' mailto CTA pre-filled with the design
  link and computed wall size; non-Studio designs keep the estimate.
- New hospitalityFit(d) classifier near productLine(d) buckets Scenic designs
  by dominant_hex palette character — Aman (very dark OR very neutral),
  Auberge (warm amber 15-45deg, restrained sat), Four Seasons (default). PDP
  chip + grid card render an italic 'for the {fit} aesthetic' sub-tag below
  the line chip; Studio designs skip it.

Files touched

Diff

commit 9e6a14276fa81d36e4dcbf2a8e7e9ce93d09e391
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu May 21 20:12:32 2026 -0700

    wallco / Scenic + Studio sub-brand landings, Studio trade-gate, Scenic hospitality fit
    
    - /scenic and /studio now render full landing pages (hero from #38842 / #39055,
      brand pitch with live design count, 6-design featured grid, prominent 'Browse
      all' CTA), replacing the bare ?line=… redirects.
    - Renamed the legacy Designer Studio composer from /studio to /designer-studio
      (nav-link added next to Studio) so the brand path wins.
    - detail-spec-panel now carries data-product-line + data-design-id, picked up
      by the wall-size-calculator compute(). For Studio designs, the 4th tile in
      both mural and wallpaper branches swaps the public $X-$Y material estimate
      for a 'Contact for trade pricing →' mailto CTA pre-filled with the design
      link and computed wall size; non-Studio designs keep the estimate.
    - New hospitalityFit(d) classifier near productLine(d) buckets Scenic designs
      by dominant_hex palette character — Aman (very dark OR very neutral),
      Auberge (warm amber 15-45deg, restrained sat), Four Seasons (default). PDP
      chip + grid card render an italic 'for the {fit} aesthetic' sub-tag below
      the line chip; Studio designs skip it.
---
 data/do-not-want.jsonl                             |  1175 ++
 data/inspired-catalog/refs/saguaro-install.jpg     |   Bin 0 -> 205684 bytes
 data/inspired-catalog/refs/saguaro-tile.jpg        |   Bin 0 -> 234037 bytes
 data/inspired-catalog/spec.json                    |    62 +
 scripts/drunk_monkeys_birds_subdued_batch.js       |    72 +
 scripts/drunk_monkeys_birds_subdued_parallel.js    |   101 +
 scripts/drunk_monkeys_birds_subdued_prompts.js     |   173 +
 scripts/drunk_monkeys_v2_batch.js                  |    94 +
 scripts/drunk_monkeys_v2_prompts.js                |   152 +
 scripts/extract-silhouette.py                      |   113 +
 scripts/generate-drip-25344.sh                     |    45 +
 ...nerate-pollock.sh => generate-drip-splatter.sh} |    10 +-
 scripts/generate-inspired-catalog.js               |   197 +
 scripts/generate-pollock.sh.bak                    |     2 +-
 scripts/generate-saguaro-arms-v2.js                |   135 +
 scripts/generate-saguaro-arms.js                   |   146 +
 scripts/generate-sherman-circus.sh.bak             |     2 +-
 ...man-circus.sh => generate-theatrical-circus.sh} |    12 +-
 scripts/night-builder.js.bak                       |   906 ++
 scripts/overnight-noghost-bulk.sh                  |    74 +
 scripts/publish-saguaro-arms.js                    |    30 +
 scripts/quantize-no-ghost.py                       |   100 +
 scripts/refresh_designs_snapshot.py                |     5 +-
 server.js                                          |   164 +-
 server.js.bak-ai-rename                            | 15683 +++++++++++++++++++
 25 files changed, 19425 insertions(+), 28 deletions(-)

diff --git a/data/do-not-want.jsonl b/data/do-not-want.jsonl
index 27cc26b..f2995c6 100644
--- a/data/do-not-want.jsonl
+++ b/data/do-not-want.jsonl
@@ -1116,3 +1116,1178 @@
 {"ts":"2026-05-20T15:31:52.696Z","design_id":25673,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"Faded fox motifs are repeated behind the main foxes, creating a double exposure look."},"source":"bleed_guard.ghost-layer"}
 {"ts":"2026-05-20T15:32:02.043Z","design_id":25669,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the motifs behind the main layer, creating a double exposure look with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
 {"ts":"2026-05-20T15:32:06.562Z","design_id":25667,"category":"sherman-circus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a clear double exposure effect with a halftone layer and atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:35:16.575Z","design_id":26029,"category":"muybridge-plate · deep-plum","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is crosshatching and shading inside the figures that should be flat."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:35:19.054Z","design_id":26028,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The image has a very obvious faded duplicate of the main motif behind itself, as well as a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:35:28.332Z","design_id":26024,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a clear faded duplicate of the tree branches and flowers behind the main motif, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:35:46.407Z","design_id":26015,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The motif has a very obvious halftone layer and ghosting effect behind it."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:35:58.274Z","design_id":26010,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the wolf motif behind the main motif, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:36:07.557Z","design_id":26005,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded ghost of the fox motif behind the main motif, and atmospheric haze blurs the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:36:23.031Z","design_id":25998,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded ghost of the main motif behind itself."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:36:25.760Z","design_id":25997,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the trees and leopards behind the main motifs, creating a double exposure look with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:36:27.844Z","design_id":25995,"category":"designer-scenic · midnight-navy","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and interior shading inside the motif, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:36:30.090Z","design_id":25994,"category":"designer-scenic · deep-plum","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and interior shading inside the motif, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:36:34.294Z","design_id":25992,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the motif behind the main one, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:36:51.938Z","design_id":25984,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the animal motifs behind the main motifs, and a soft atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:36:54.335Z","design_id":25983,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the deer motif behind the main motif, and a soft haze behind the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:37:09.728Z","design_id":25976,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the tree branches and flowers behind the main motif, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:37:11.876Z","design_id":25975,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious ghosting effect behind the main deer motif, and a halftone layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:40:09.991Z","design_id":26332,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the cactus motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:40:23.273Z","design_id":26326,"category":"face-skull-damask · forest-loden","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The skull motif has crosshatching inside the silhouette that should be flat."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:40:27.827Z","design_id":26324,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The skull motif has a very obvious faded ghost/duplicate behind it, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:40:36.550Z","design_id":26320,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the motif behind the main one, and atmospheric haze blurs the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:40:57.619Z","design_id":26311,"category":"cactus · midnight-navy","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a dot-screen layer behind the cacti, and interior shading inside the cacti silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:41:05.502Z","design_id":26308,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the cactus motif behind the main motif, and interior shading is visible."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:41:14.506Z","design_id":26304,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the motif behind the main pattern, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:41:31.406Z","design_id":26297,"category":"chinoiserie · saddle-mocha","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have a halftone layer behind them and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:41:33.985Z","design_id":26296,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the motifs, a halftone layer, and atmospheric haze behind the main motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:41:43.733Z","design_id":26292,"category":"face-skull-damask","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The skull motif has interior shading and a halftone layer behind it, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:42:04.996Z","design_id":26283,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the motif behind the main pattern, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:42:14.846Z","design_id":26279,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the main motif behind itself."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:45:04.965Z","design_id":26648,"category":"designer-zoo-calm · champagne-linen","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:45:19.131Z","design_id":26642,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a faded duplicate behind it, a halftone layer, atmospheric haze, and interior shading, indicating significant motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:45:21.530Z","design_id":26641,"category":"designer-zoo-calm · bone-ivory","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have a dot-screen layer behind them and interior shading, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:45:25.885Z","design_id":26639,"category":"designer-zoo-calm · greenhouse-glass","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the motifs and interior shading bleed inside the silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:45:45.713Z","design_id":26630,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The main motif has a very obvious faded duplicate behind itself, and a halftone layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:45:48.074Z","design_id":26629,"category":"throttled-plaster-roses · champagne-linen","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a halftone layer behind them, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:45:53.018Z","design_id":26627,"category":"throttled-plaster-roses · stone-pewter","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a clear halftone layer and interior shading, with an atmospheric haze behind them."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:45:55.383Z","design_id":26626,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the rose motif behind the main motif, and a soft atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:46:04.840Z","design_id":26622,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the deer motif behind the main motif, and a halftone layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:48:07.595Z","design_id":26615,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the bird and branch motifs behind the main motifs, and an atmospheric haze is present."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:48:09.833Z","design_id":26614,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image shows a clear faded duplicate of the motif, halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:48:12.359Z","design_id":26613,"category":"muybridge-plate · champagne-linen","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a very obvious halftone layer and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:48:14.542Z","design_id":26612,"category":"muybridge-plate · bone-ivory","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious halftone layer and ghosting effect, with interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:48:16.819Z","design_id":26611,"category":"muybridge-plate · stone-pewter","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious halftone layer and atmospheric haze behind it, as well as interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:48:19.520Z","design_id":26610,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the motif behind the main figures, and an atmospheric haze is present."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:48:27.641Z","design_id":26606,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious ghosted duplicate of the figure behind the main motif, and a soft haze behind the figures."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:48:46.712Z","design_id":26598,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The cactus motifs have a faded ghost/duplicate behind them, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:48:49.086Z","design_id":26597,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The skull motif has a faded duplicate behind it, a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:48:58.413Z","design_id":26593,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded skull motif behind the main skulls, with interior shading and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:49:00.777Z","design_id":26592,"category":"throttled-plaster-roses · deep-plum","reason":"obvious bleed: halftone_layer, atmospheric_haze","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The pattern has a blurred haze layer behind the motif, creating an atmospheric effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:50:07.045Z","design_id":26951,"category":"muybridge-plate · slate-mist","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a very obvious ghosting effect, halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:50:09.415Z","design_id":26950,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the elephant motif behind the main motif, and a soft haze behind the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:50:22.809Z","design_id":26944,"category":"designer-zoo-calm · manor-saddle","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The bears have a halftone layer behind them and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:50:24.860Z","design_id":26943,"category":"designer-zoo-calm · slate-mist","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The bears have interior shading and a halftone layer behind them, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:50:27.212Z","design_id":26942,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the main motif behind itself, and interior shading inside the silhouette."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:50:35.424Z","design_id":26938,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the cactus motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:50:46.527Z","design_id":26933,"category":"designer-zoo-calm · midnight-navy","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a dot-screen layer behind the motif and crosshatching inside the wolf silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:51:00.532Z","design_id":26927,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The wolf motifs have a clear faded duplicate behind them, along with a halftone layer and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:51:11.468Z","design_id":26922,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The main motif is repeated as a faded ghost / duplicate / echo offset behind itself."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:51:29.643Z","design_id":26914,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The lemur motifs have a very obvious faded ghost layer behind them, and a halftone layer is also present."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:51:38.477Z","design_id":26910,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the motif behind the main motif, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:51:45.538Z","design_id":26907,"category":"face-skull-damask · champagne-linen","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a blurred haze layer behind it and interior shading, creating a double exposure look."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:51:48.253Z","design_id":26906,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded ghost of the main motif behind itself, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:51:50.289Z","design_id":26904,"category":"designer-scenic · forest-loden","reason":"obvious bleed: halftone_layer, atmospheric_haze","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The background has a very obvious halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:51:57.425Z","design_id":26901,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the main motif behind itself, and atmospheric haze blurs the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:52:15.199Z","design_id":26893,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the skull and damask motif behind the main motif, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:55:03.463Z","design_id":27245,"category":"muybridge-plate · indigo-library","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is crosshatching inside the silhouette of the motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:55:10.469Z","design_id":27242,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the tiger motif behind the main motif, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:55:27.066Z","design_id":27235,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the motif behind the main motif, a halftone layer, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:55:29.199Z","design_id":27234,"category":"face-skull-damask · antique-tan","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is obvious interior shading inside the skull motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:55:36.300Z","design_id":27231,"category":"face-skull-damask · champagne-linen","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The skulls have interior shading that should be flat, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:55:38.759Z","design_id":27230,"category":"face-skull-damask","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is crosshatching and shading inside the skull motif that should be flat."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:55:42.791Z","design_id":27228,"category":"designer-zoo-calm · stone-pewter","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is interior shading inside the animal silhouettes that should be flat."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:55:47.297Z","design_id":27226,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The motifs have a faded duplicate behind them, a halftone layer, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:56:02.655Z","design_id":27219,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the deer motif behind the main motif, and a soft atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:56:11.262Z","design_id":27215,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The main motif has a faded ghost duplicate behind itself, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:56:13.479Z","design_id":27214,"category":"chinoiserie","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a dot-screen effect, atmospheric haze, and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:56:20.264Z","design_id":27211,"category":"cactus · deep-plum","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the cacti and interior shading bleed inside the cacti silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:56:23.016Z","design_id":27210,"category":"cactus · saddle-mocha","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The cactus motifs have interior shading and a halftone layer behind them, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:56:31.590Z","design_id":27206,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The cactus motifs have a faded, offset duplicate behind them, along with interior shading and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:56:34.041Z","design_id":27205,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the motif behind the main figures, and a soft haze behind the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:56:44.908Z","design_id":27200,"category":"chinoiserie","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a dot-screen effect, atmospheric haze, and interior shading inside the motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:56:55.125Z","design_id":27195,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a faded, offset duplicate behind them, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T15:57:03.807Z","design_id":27191,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded ghost of the cactus motifs behind the main motifs, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:00:03.350Z","design_id":27558,"category":"designer-zoo-calm · bordeaux-velvet","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The foxes have interior shading that should be flat, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:00:07.983Z","design_id":27556,"category":"designer-zoo-calm · slate-mist","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The foxes have interior shading and a halftone layer behind them."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:00:10.221Z","design_id":27555,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the fox motif behind the main one, and interior shading is visible."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:00:28.220Z","design_id":27547,"category":"chinoiserie · slate-mist","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a dot-screen layer, atmospheric haze, and interior shading inside the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:00:30.399Z","design_id":27546,"category":"chinoiserie · antique-brass","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a dot-screen layer behind the motif and crosshatching inside the motif silhouette."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:00:32.497Z","design_id":27545,"category":"chinoiserie · oxblood","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is crosshatching inside the motif silhouette that should be flat."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:00:41.422Z","design_id":27541,"category":"designer-scenic · oxblood","reason":"obvious bleed: halftone_layer, atmospheric_haze","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The background has a very obvious halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:00:49.106Z","design_id":27538,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded ghost of the motif behind the main branches and birds, and atmospheric haze is present."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:01:01.736Z","design_id":27533,"category":"designer-zoo-calm · bordeaux-velvet","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The deer motifs have a clear faded duplicate behind them, a halftone layer, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:01:03.867Z","design_id":27532,"category":"designer-zoo-calm · manor-saddle","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The deer motifs have a clear faded duplicate behind them, a halftone layer, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:01:06.221Z","design_id":27531,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the cactus motif behind the main motif, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:01:10.833Z","design_id":27529,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded ghost/duplicate of the deer motifs behind the main motifs, and atmospheric haze blurs the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:01:17.957Z","design_id":27526,"category":"designer-zoo-calm","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The leopard motifs have a halftone dot-screen effect and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:01:43.250Z","design_id":27515,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the deer motif behind the main motif, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:01:48.037Z","design_id":27513,"category":"face-skull-damask · manor-saddle","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is crosshatching and shading inside the skull motif that should be flat."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:01:52.706Z","design_id":27511,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The bears have a faded, ghosted duplicate behind them, and the background has an atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:02:08.070Z","design_id":27510,"category":"face-skull-damask","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The skull motif has a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:02:21.435Z","design_id":27504,"category":"designer-zoo-calm · antique-brass","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a clear faded duplicate behind it, a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:02:23.906Z","design_id":27503,"category":"designer-zoo-calm · oxblood","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is crosshatching inside the motif silhouette that should be flat."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:02:28.276Z","design_id":27501,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the motif behind the main motif, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:05:29.120Z","design_id":27835,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a very obvious faded ghost/duplicate behind them, along with a hazy background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:05:37.856Z","design_id":27831,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the cactus motif behind the main motif, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:05:47.270Z","design_id":27827,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the floral motif behind the main motif, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:06:06.062Z","design_id":27819,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the cactus motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:06:25.517Z","design_id":27812,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the tree motif behind the main motif, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:06:28.174Z","design_id":27811,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious faded ghost/duplicate behind it, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:06:35.515Z","design_id":27808,"category":"chinoiserie · champagne-linen","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a dot-screen layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:06:54.970Z","design_id":27799,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded ghost of the animal motifs behind the main motifs, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:07:06.273Z","design_id":27793,"category":"chinoiserie","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a dot-screen effect, atmospheric haze, and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:07:10.955Z","design_id":27791,"category":"chinoiserie · smoke-ash","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a clear faded duplicate, halftone layer, atmospheric haze, and interior shading, indicating significant motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:07:16.263Z","design_id":27789,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The main motif has a very obvious faded ghost/duplicate behind itself, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:10:02.463Z","design_id":28096,"category":"muybridge-plate · deep-plum","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The elephant motifs have a clear halftone haze and ghosting effect behind them."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:10:04.693Z","design_id":28095,"category":"muybridge-plate · saddle-mocha","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The elephant motifs have a very obvious halftone ghost / duplicate behind them."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:10:06.943Z","design_id":28094,"category":"muybridge-plate · antique-tan","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The elephant motifs have a clear halftone layer and atmospheric haze behind them, creating a 'double exposure' effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:10:09.150Z","design_id":28093,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the elephant motif behind the main motif, creating a double exposure effect and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:10:17.545Z","design_id":28089,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the floral motif behind the main motif, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:10:26.681Z","design_id":28085,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the motif behind the main pattern, and atmospheric haze blurs the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:10:37.909Z","design_id":28080,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The horse motifs have a very obvious faded duplicate behind them, along with halftone and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:10:56.045Z","design_id":28072,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a clear faded duplicate of the main motif, a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:11:07.266Z","design_id":28067,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded ghost of the fox motif behind the main motif, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:11:17.569Z","design_id":28062,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a clear faded duplicate behind it, a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:11:46.086Z","design_id":28050,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the main motif behind itself, and a soft haze behind the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:12:01.641Z","design_id":28044,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the cactus motif behind the main motif, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:12:20.570Z","design_id":28036,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the animal motifs behind the main motifs, and a halftone layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:15:10.403Z","design_id":28384,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the wolf motif and atmospheric haze behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:15:15.027Z","design_id":28381,"category":"designer-zoo-calm · indigo-library","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have a halftone layer behind them and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:15:29.184Z","design_id":28374,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The motif has a faded duplicate behind itself, a halftone layer, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:15:44.152Z","design_id":28369,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a clear faded duplicate of the motifs and an atmospheric haze behind the main elements."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:15:46.610Z","design_id":28367,"category":"muybridge-plate · champagne-linen","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The motif has a very obvious faded duplicate behind it, a halftone layer, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:15:48.722Z","design_id":28366,"category":"muybridge-plate · bone-ivory","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious faded duplicate behind it, a halftone layer, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:15:53.340Z","design_id":28364,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious ghost/duplicate of the bird motif behind the main motif, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:16:00.082Z","design_id":28360,"category":"muybridge-plate · deep-plum","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image shows a clear faded duplicate of the motif, halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:16:02.521Z","design_id":28359,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious ghosted duplicate of the dog motif, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:16:12.103Z","design_id":28355,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a clear faded duplicate behind them, a halftone layer, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:16:21.775Z","design_id":28351,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very clear faded duplicate of the badger motifs behind the main motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:16:30.940Z","design_id":28346,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the cactus motif behind the main motif, and an atmospheric haze behind the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:16:39.920Z","design_id":28342,"category":"chinoiserie","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a dot-screen layer, atmospheric haze, and interior shading, creating a double exposure look."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:16:48.641Z","design_id":28338,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The skull motif has a very obvious faded duplicate behind it, along with a halftone layer and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:16:52.865Z","design_id":28336,"category":"muybridge-plate · champagne-linen","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a halftone layer and interior shading bleed within the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:16:55.037Z","design_id":28335,"category":"muybridge-plate · bone-ivory","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a halftone layer and interior shading bleed, making it an obvious case of motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:16:57.486Z","design_id":28334,"category":"muybridge-plate","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and interior shading inside the motif, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:17:04.517Z","design_id":28331,"category":"designer-zoo-calm · indigo-library","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a dot-screen layer behind the motifs, and interior shading inside the silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:17:07.453Z","design_id":28330,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the main motif behind itself."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:17:16.237Z","design_id":28326,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a clear faded duplicate of the rose motifs behind the main ones, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:20:09.965Z","design_id":28692,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The main motif has a faded duplicate behind it, a halftone layer, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:20:25.766Z","design_id":28685,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the tree motif behind the main motif, and atmospheric haze blurs the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:20:28.180Z","design_id":28684,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the animal motifs behind the main motifs, creating a double exposure look with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:20:30.500Z","design_id":28683,"category":"monterey-mural","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The image has a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:20:39.461Z","design_id":28678,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a clear faded duplicate of the bear motifs behind the main motifs, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:20:55.916Z","design_id":28670,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The main motif is repeated as a faded ghost offset behind itself, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:21:00.666Z","design_id":28667,"category":"designer-zoo-calm · antique-brass","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious faded duplicate behind it, a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:21:03.085Z","design_id":28666,"category":"designer-zoo-calm · oxblood","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious ghost/duplicate behind it, as well as interior shading and a halftone layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:21:05.294Z","design_id":28665,"category":"designer-zoo-calm · forest-loden","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious faded duplicate behind it, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:21:07.618Z","design_id":28664,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The main motif has a faded duplicate behind itself, a halftone layer, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:21:09.890Z","design_id":28662,"category":"throttled-plaster-roses · champagne-linen","reason":"obvious bleed: halftone_layer, atmospheric_haze","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious halftone layer and atmospheric haze behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:21:12.268Z","design_id":28661,"category":"throttled-plaster-roses · bone-ivory","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a very obvious faded duplicate behind them, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:21:16.701Z","design_id":28659,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a very obvious faded ghost/duplicate behind them, as well as interior shading and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:21:19.034Z","design_id":28658,"category":"cactus · bordeaux-velvet","reason":"obvious bleed: halftone_layer, atmospheric_haze","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a blurred haze layer behind the cactus motifs, creating an atmospheric effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:21:21.067Z","design_id":28657,"category":"cactus · manor-saddle","reason":"obvious bleed: halftone_layer, atmospheric_haze","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious halftone layer and atmospheric haze behind the cactus motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:21:23.414Z","design_id":28656,"category":"cactus · slate-mist","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The cactus motifs have a clear faded duplicate behind them, along with a halftone layer and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:21:28.156Z","design_id":28654,"category":"throttled-plaster-roses · saddle-mocha","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a halftone layer, atmospheric haze, and interior shading bleed, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:21:30.531Z","design_id":28653,"category":"throttled-plaster-roses · antique-tan","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The pattern has a dot-screen layer, atmospheric haze, and interior shading inside the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:21:33.068Z","design_id":28652,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The cactus motifs have a faded ghost/duplicate behind them, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:21:35.850Z","design_id":28650,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a faded duplicate behind them and interior shading, with an atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:21:38.076Z","design_id":28649,"category":"throttled-plaster-roses · champagne-linen","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a blurred haze layer and interior shading inside the motif, creating a bleed effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:21:42.540Z","design_id":28647,"category":"throttled-plaster-roses · stone-pewter","reason":"obvious bleed: halftone_layer, atmospheric_haze","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The pattern has a blurred haze layer and halftone effect, but no duplicate motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:21:55.075Z","design_id":28641,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the motif behind the main motif, and a soft haze behind the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:22:18.331Z","design_id":28633,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the rose motif behind the main motif, and an atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:25:10.894Z","design_id":28979,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a very obvious faded duplicate behind them, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:25:26.112Z","design_id":28972,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a clear faded duplicate of the bamboo and floral motifs, and an atmospheric haze behind the motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:25:37.194Z","design_id":28971,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the animal motifs behind the main motifs, creating a double exposure look with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:25:46.230Z","design_id":28967,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the animal motifs behind the main motifs, and atmospheric haze is present."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:25:56.124Z","design_id":28963,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the pangolin motif behind the main motif, and atmospheric haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:26:25.244Z","design_id":28958,"category":"designer-scenic · manor-saddle","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a clear faded duplicate of the motif, a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:26:30.228Z","design_id":28956,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious faded duplicate behind it, as well as interior shading and a halftone layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:26:32.587Z","design_id":28955,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the main motif behind itself and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:26:41.825Z","design_id":28951,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The motifs have a faded ghost/duplicate behind them and there is atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:26:51.198Z","design_id":28947,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the damask motif behind the main motif, and an atmospheric haze effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:27:03.899Z","design_id":28942,"category":"throttled-plaster-roses · oxblood","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a dot-screen haze behind them and interior shading, creating a bleed effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:27:06.027Z","design_id":28941,"category":"throttled-plaster-roses · forest-loden","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a dot-screen haze behind them and interior shading, creating a bleed effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:27:08.242Z","design_id":28940,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the leopard motif behind the main motif, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:27:13.264Z","design_id":28938,"category":"stripe · bone-ivory","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The main stripe motif is repeated as a faded ghost offset behind itself."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:27:15.732Z","design_id":28937,"category":"stripe · stone-pewter","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The stripe motif has a very obvious faded ghost behind it, and the background has an atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:27:17.998Z","design_id":28936,"category":"stripe · greenhouse-glass","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the stripe motif behind the main stripes, with an atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:27:20.386Z","design_id":28935,"category":"stripe","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The stripe motif has a very obvious faded ghost/duplicate behind it, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:27:22.899Z","design_id":28934,"category":"designer-zoo-calm · forest-loden","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have interior shading and a halftone layer, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:27:30.447Z","design_id":28931,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The main motif is repeated as a faded ghost behind itself, and there is atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:27:41.915Z","design_id":28926,"category":"chinoiserie · champagne-linen","reason":"obvious bleed: atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is atmospheric haze and interior shading inside the motif, creating a bleed effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:27:47.160Z","design_id":28924,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the wolf motif behind the main motif, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:27:49.450Z","design_id":28923,"category":"chinoiserie","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer, atmospheric haze, and interior shading bleed present in the image."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:30:11.982Z","design_id":29256,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The main motif has a faded duplicate behind it, a halftone layer, and atmospheric haze, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:30:40.104Z","design_id":29243,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the lion motif behind the main motif, as well as interior shading and a halftone layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:31:17.552Z","design_id":29225,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded ghost of the damask motif behind the main motif, and atmospheric haze blurs the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:31:19.982Z","design_id":29224,"category":"designer-scenic · bone-ivory","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious faded duplicate and halftone layer behind it, creating a double exposure look with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:31:26.952Z","design_id":29221,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the tree branches and flowers behind the main motif, and a soft atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:31:31.479Z","design_id":29219,"category":"chinoiserie · deep-plum","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.9,"one_line_reason":"The image has a dot-screen layer and interior shading inside the motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:31:36.535Z","design_id":29217,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious ghosting effect behind the main motifs, as well as a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:31:45.297Z","design_id":29213,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the skull motif and atmospheric haze, with interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:31:54.119Z","design_id":29209,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The crane motifs have a very obvious halftone ghost layer behind them."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:32:03.008Z","design_id":29205,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the rose motif behind the main motif, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:35:26.990Z","design_id":29543,"category":"designer-scenic · bone-ivory","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious faded duplicate behind it, along with a halftone layer and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:35:29.545Z","design_id":29542,"category":"designer-scenic · stone-pewter","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a clear faded duplicate of the motif, a halftone layer, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:35:31.812Z","design_id":29541,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the floral motif and atmospheric haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:35:44.670Z","design_id":29534,"category":"designer-zoo-calm · midnight-navy","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The polar bear motifs have a dot-screen layer behind them and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:35:46.760Z","design_id":29533,"category":"designer-zoo-calm · deep-plum","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The polar bear motifs have a dot-screen layer behind them and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:35:49.234Z","design_id":29532,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the cactus motifs behind the main motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:35:51.553Z","design_id":29531,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The bears have a very obvious halftone layer behind them, creating a ghosting effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:36:00.647Z","design_id":29527,"category":"designer-zoo-calm · champagne-linen","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The pattern has a halftone layer and interior shading bleed inside the silhouette of the animal motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:36:03.060Z","design_id":29526,"category":"designer-zoo-calm · bone-ivory","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have a dot-screen texture and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:36:05.438Z","design_id":29525,"category":"designer-zoo-calm · stone-pewter","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a faded duplicate, halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:36:08.105Z","design_id":29524,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a clear faded duplicate behind it, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:36:10.871Z","design_id":29522,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the leopard motif behind the main motif, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:36:20.359Z","design_id":29517,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the cactus motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:36:38.090Z","design_id":29508,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the damask motif behind the main motif, and atmospheric haze is present."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:36:46.674Z","design_id":29503,"category":"designer-scenic","reason":"motif ghost layer (subtle)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"subtle","confidence":0.9,"one_line_reason":"There is a faded duplicate of the cherry blossom branches and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:36:48.849Z","design_id":29501,"category":"throttled-plaster-roses · bone-ivory","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a blurred haze layer, interior shading, and a halftone layer behind the motif, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:36:53.339Z","design_id":29499,"category":"throttled-plaster-roses · greenhouse-glass","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a blurred haze layer behind the motif, and interior shading inside the silhouette, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:36:56.772Z","design_id":29498,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a very obvious faded duplicate behind them, with a blurred haze layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:37:10.139Z","design_id":29492,"category":"chinoiserie · forest-loden","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a faded duplicate behind it, a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:37:12.676Z","design_id":29491,"category":"chinoiserie · midnight-navy","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have a clear faded duplicate behind them, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:37:15.019Z","design_id":29490,"category":"chinoiserie · deep-plum","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious faded duplicate behind it, a halftone layer, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:38:47.226Z","design_id":29489,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the motif, a halftone layer, and atmospheric haze behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:38:49.270Z","design_id":29488,"category":"designer-scenic · champagne-linen","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a blurred haze layer behind it and interior shading, creating a bleed effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:40:03.357Z","design_id":29854,"category":"chinoiserie · bordeaux-velvet","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have a clear faded duplicate behind them, along with a halftone layer and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:40:05.669Z","design_id":29853,"category":"chinoiserie · manor-saddle","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a clear faded duplicate, halftone layer, atmospheric haze, and interior shading, indicating significant motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:40:07.871Z","design_id":29852,"category":"chinoiserie · slate-mist","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a clear faded duplicate of the motif, a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:40:52.698Z","design_id":29834,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image shows a clear faded duplicate of the main motif, a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:40:55.028Z","design_id":29833,"category":"monterey-mural","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The image has a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:40:59.691Z","design_id":29831,"category":"designer-zoo-calm · slate-mist","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The bears have a halftone layer behind them and interior shading, creating a bleed effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:41:06.730Z","design_id":29828,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The main motif is repeated as a faded ghost behind itself, and there is a halftone layer behind the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:41:16.294Z","design_id":29824,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The skull motif has a very obvious faded duplicate behind it, along with a halftone layer and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:41:28.262Z","design_id":29820,"category":"chinoiserie","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a dot-screen layer, atmospheric haze, and interior shading inside the motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:41:37.580Z","design_id":29816,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the cactus motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:41:50.513Z","design_id":29812,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the trees and animals behind the main motifs, and an atmospheric haze effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:41:59.670Z","design_id":29808,"category":"chinoiserie · greenhouse-glass","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a halftone layer, atmospheric haze, and interior shading bleed, making the motif look like a double exposure."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:42:02.137Z","design_id":29807,"category":"chinoiserie","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a dot-screen layer, atmospheric haze, and interior shading inside the motif, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:42:13.704Z","design_id":29802,"category":"cactus · bordeaux-velvet","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is crosshatching inside the cactus silhouettes that should be flat."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:42:16.132Z","design_id":29801,"category":"cactus · manor-saddle","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the cactus motifs and interior shading bleed within the cactus silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:42:27.804Z","design_id":29796,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the cactus motif behind the main motif, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:42:30.220Z","design_id":29795,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The main motif is repeated as a faded ghost behind itself, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:45:18.724Z","design_id":30128,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the motif behind the main figures, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:45:36.986Z","design_id":30120,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded ghost of the deer motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:45:48.750Z","design_id":30115,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the motif, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:46:04.089Z","design_id":30111,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The dinosaurs have a faded ghost layer behind them, a halftone layer, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:46:08.543Z","design_id":30109,"category":"designer-zoo-calm · slate-mist","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the motif and interior shading bleed inside the silhouette."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:46:20.371Z","design_id":30107,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the motif behind the main motif, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:46:37.455Z","design_id":30103,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The main motif has a faded duplicate behind itself, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:47:00.694Z","design_id":30099,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious ghost/duplicate of the motif behind the main figures."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:47:23.236Z","design_id":30093,"category":"designer-scenic · bone-ivory","reason":"obvious bleed: halftone_layer, atmospheric_haze","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and atmospheric haze behind the motif, creating a bleed effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:47:25.427Z","design_id":30092,"category":"designer-scenic · stone-pewter","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious halftone layer and ghosting effect behind it."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:47:28.273Z","design_id":30091,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the tree branches behind the main motif, and an atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:47:31.551Z","design_id":30090,"category":"designer-zoo-calm · indigo-library","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a halftone layer and interior shading bleed inside the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:47:36.575Z","design_id":30088,"category":"designer-zoo-calm · manor-saddle","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a halftone layer and interior shading bleed inside the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:47:41.901Z","design_id":30086,"category":"designer-zoo-calm","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The lemurs have a dot-screen layer behind them and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:47:56.391Z","design_id":30082,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious faded duplicate behind it, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:47:58.576Z","design_id":30081,"category":"throttled-plaster-roses · stone-pewter","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a very obvious faded duplicate behind them, with a halftone layer and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:48:06.773Z","design_id":30078,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a faded duplicate behind them and interior shading, with an atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:50:03.290Z","design_id":30416,"category":"throttled-plaster-roses · smoke-ash","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a halftone layer, atmospheric haze, and interior shading bleed, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:50:30.435Z","design_id":30411,"category":"designer-zoo-calm · oxblood","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a dot-screen layer behind the motifs, and interior shading inside the silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:50:35.329Z","design_id":30409,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the animal motif behind the main motif, and atmospheric haze blurs the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:50:45.455Z","design_id":30405,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the cactus motif behind the main motif, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:51:03.205Z","design_id":30401,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The main motif is repeated as a faded ghost offset behind itself, and there is atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:51:13.913Z","design_id":30397,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The bears have a very obvious ghosted duplicate behind them, and the trees have a halftone effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:51:24.118Z","design_id":30393,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the badger motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:51:52.282Z","design_id":30385,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the skull and damask motifs behind the main motifs, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:52:07.121Z","design_id":30380,"category":"designer-scenic · forest-loden","reason":"obvious bleed: halftone_layer, atmospheric_haze","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious halftone layer and atmospheric haze behind the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:52:38.078Z","design_id":30368,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The main animal motif has a very obvious faded duplicate behind it, and the background has a halftone layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:52:58.926Z","design_id":30360,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the main motif behind itself, and atmospheric haze is present."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:53:01.828Z","design_id":30359,"category":"throttled-plaster-roses · slate-mist","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a very obvious faded duplicate behind them, with a halftone layer and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:53:16.751Z","design_id":30356,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a very obvious faded ghost/duplicate behind them, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:55:03.318Z","design_id":30699,"category":"cactus · antique-brass","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a dot-screen layer behind the cacti and crosshatching inside the cacti silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:55:05.485Z","design_id":30698,"category":"cactus · oxblood","reason":"obvious bleed: halftone_layer, atmospheric_haze","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and atmospheric haze behind the cactus motifs, creating a bleed effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:55:07.673Z","design_id":30697,"category":"cactus · forest-loden","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the cacti and interior shading within the cacti silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:55:10.393Z","design_id":30696,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The cactus motifs have a faded ghost/duplicate behind them, a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:55:14.594Z","design_id":30694,"category":"muybridge-plate · champagne-linen","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a clear faded duplicate of the elephant motif behind the main one, along with interior shading and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:55:19.014Z","design_id":30692,"category":"designer-zoo-calm · champagne-linen","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the motif and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:55:28.599Z","design_id":30689,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The elephants and figures have a very obvious faded ghost image behind them, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:55:31.105Z","design_id":30688,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the fox motif behind the main motif, and atmospheric haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:55:39.921Z","design_id":30684,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious ghosting effect and halftone layer behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:55:41.916Z","design_id":30683,"category":"chinoiserie · saddle-mocha","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has interior shading and a halftone layer, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:55:46.161Z","design_id":30681,"category":"chinoiserie · smoke-ash","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has interior shading and a halftone layer behind it, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:55:48.286Z","design_id":30680,"category":"chinoiserie","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The dragon motif has a halftone layer, atmospheric haze, and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:55:59.656Z","design_id":30675,"category":"muybridge-plate · bordeaux-velvet","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a halftone layer and interior shading bleed, creating a double exposure look."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:56:06.187Z","design_id":30672,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image shows a clear double exposure effect with a faded ghost motif, halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:56:15.573Z","design_id":30668,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a very obvious faded ghost/duplicate behind them, as well as a hazy background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:56:24.056Z","design_id":30664,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the floral motif, a halftone layer, and atmospheric haze behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:56:26.332Z","design_id":30663,"category":"designer-zoo-calm · forest-loden","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The wolves have a halftone layer behind them and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:56:33.739Z","design_id":30662,"category":"designer-zoo-calm · midnight-navy","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The wolves have a dot-screen effect behind them and crosshatching inside the silhouette."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:56:36.029Z","design_id":30661,"category":"designer-zoo-calm · deep-plum","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The wolves have a dot-screen effect behind them and crosshatching inside the silhouette."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:56:38.251Z","design_id":30660,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the wolf motif behind the main motif, a halftone layer, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:57:13.007Z","design_id":30644,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded ghost of the animal motifs behind the main motifs, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T16:57:21.937Z","design_id":30640,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the tree branches and birds behind the main motif, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:00:26.007Z","design_id":30962,"category":"monterey-mural","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a dot-screen effect, atmospheric haze, and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:00:28.380Z","design_id":30961,"category":"monterey-mural","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:00:39.787Z","design_id":30956,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded ghost of the fox motif behind the main motif, and an atmospheric haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:01:04.359Z","design_id":30945,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the cactus motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:01:06.700Z","design_id":30944,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the lemur and foliage motifs behind the main motifs, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:01:16.378Z","design_id":30940,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the motifs behind the main motifs, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:01:18.663Z","design_id":30939,"category":"monterey-mural","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The image has a dot-screen halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:01:23.084Z","design_id":30937,"category":"designer-zoo-calm · oxblood","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The wolves have a halftone layer behind them and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:01:27.246Z","design_id":30935,"category":"designer-zoo-calm · midnight-navy","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The wolf motifs have a dot-screen layer behind them and crosshatching inside the silhouette."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:01:57.558Z","design_id":30922,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the damask motif behind the main motif, creating a double exposure look with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:02:04.459Z","design_id":30919,"category":"muybridge-plate · smoke-ash","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The image has a clear double exposure look with a halftone layer and atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:02:06.876Z","design_id":30918,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the motif behind the main motif, and a soft haze behind the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:05:15.801Z","design_id":31240,"category":"cactus · bordeaux-velvet","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The cactus motifs have interior shading and a halftone layer behind them, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:05:18.023Z","design_id":31239,"category":"cactus · manor-saddle","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the cactus motifs and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:05:20.588Z","design_id":31238,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the cactus motif behind the main motif, and atmospheric haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:05:29.333Z","design_id":31233,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a very obvious faded duplicate of the motif, halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:05:38.247Z","design_id":31229,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the fox motif behind the main motif, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:05:40.624Z","design_id":31227,"category":"designer-zoo-calm · antique-tan","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a halftone layer and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:05:44.992Z","design_id":31225,"category":"designer-zoo-calm · champagne-linen","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The interior of the animal motifs have a halftone/dot-screen texture, and the interior shading is not flat."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:05:47.364Z","design_id":31224,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the leopard motif behind the main one, and interior shading within the leopard silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:05:56.089Z","design_id":31219,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the damask motif behind the main motif, creating a double exposure look with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:06:04.411Z","design_id":31215,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the motif behind the main motif, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:06:13.050Z","design_id":31210,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the lion motifs behind the main motifs, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:06:15.332Z","design_id":31209,"category":"chinoiserie · oxblood","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is crosshatching inside the motif silhouette that should be flat."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:06:29.855Z","design_id":31201,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the floral motif and atmospheric haze behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:06:37.865Z","design_id":31197,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a clear faded duplicate of the floral motif behind the main one, and an atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:07:08.010Z","design_id":31183,"category":"designer-zoo-calm · oxblood","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The lemur and tree motifs have a dot-screen layer behind them, and crosshatching inside the silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:07:12.605Z","design_id":31181,"category":"designer-zoo-calm · midnight-navy","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The lemur and tree motifs have a dot-screen shading inside the silhouette, and a halftone layer behind."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:10:17.950Z","design_id":31526,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the bear motif behind the main motif, and atmospheric haze is present."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:10:25.176Z","design_id":31523,"category":"cactus · antique-tan","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The cactus motif has a clear faded duplicate behind it, along with a halftone layer and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:10:36.774Z","design_id":31518,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the cactus motifs behind the main motifs, and a soft atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:10:55.656Z","design_id":31509,"category":"chinoiserie","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a dot-screen layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:10:57.995Z","design_id":31508,"category":"chinoiserie · smoke-ash","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious halftone layer and atmospheric haze behind it, creating a 'double exposure' look."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:11:02.806Z","design_id":31506,"category":"chinoiserie · bone-ivory","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a clear halftone layer, atmospheric haze, interior shading, and a ghosted motif effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:11:07.292Z","design_id":31504,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have a clear faded ghost/duplicate behind them, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:11:13.845Z","design_id":31501,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the motifs behind the main motifs, and an atmospheric haze effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:11:20.976Z","design_id":31498,"category":"designer-zoo-calm · antique-brass","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and interior shading inside the bird motifs, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:11:23.394Z","design_id":31497,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the bird motif behind the main motif, and atmospheric haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:11:25.677Z","design_id":31496,"category":"cactus · slate-mist","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The cactus motifs have a halftone layer behind them and interior shading, creating a bleed effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:11:30.788Z","design_id":31494,"category":"cactus · oxblood","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The cactus motifs have interior shading and a halftone layer behind them, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:11:33.168Z","design_id":31493,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the cactus motifs behind the main motifs, along with a halftone layer and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:11:39.334Z","design_id":31490,"category":"designer-zoo-calm · smoke-ash","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The wolf motifs have a dot-screen layer behind them and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:11:43.842Z","design_id":31487,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The wolf motifs have a clear faded duplicate behind them, along with a halftone layer and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:11:55.597Z","design_id":31482,"category":"muybridge-plate · deep-plum","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a very strong halftone layer and atmospheric haze behind the bird motifs, creating a double exposure look."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:11:58.150Z","design_id":31481,"category":"muybridge-plate · saddle-mocha","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The image has a clear halftone layer and ghosting effect behind the main motif, with interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:12:00.491Z","design_id":31480,"category":"muybridge-plate · antique-tan","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a clear faded duplicate of the bird motifs with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:12:02.950Z","design_id":31479,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a faded duplicate of the bird motif, a halftone layer, and atmospheric haze, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:15:11.102Z","design_id":31832,"category":"muybridge-plate · oxblood","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a clear ghosting effect and interior shading within the elephant motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:15:13.351Z","design_id":31831,"category":"muybridge-plate · forest-loden","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The elephants have a clear ghosting effect and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:15:15.855Z","design_id":31830,"category":"muybridge-plate · midnight-navy","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The elephants have a clear ghosting effect and interior shading, with a halftone layer visible."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:15:20.575Z","design_id":31828,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the elephant motif behind the main motif, and a soft haze behind the elephants."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:15:29.136Z","design_id":31823,"category":"designer-zoo-calm","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the foxes and crosshatching inside the fox silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:15:38.988Z","design_id":31818,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the skull and damask motifs behind the main motifs, with a halftone layer and atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:15:58.095Z","design_id":31810,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the motif, a halftone layer, and atmospheric haze behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:16:07.214Z","design_id":31804,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the deer motif behind the main motif, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:16:16.474Z","design_id":31800,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a very obvious ghosting effect and atmospheric haze, as well as interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:16:27.126Z","design_id":31795,"category":"designer-zoo-calm · bone-ivory","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and interior shading inside the motif, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:16:30.012Z","design_id":31794,"category":"designer-zoo-calm · stone-pewter","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the motif and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:16:32.787Z","design_id":31793,"category":"designer-zoo-calm · greenhouse-glass","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and interior shading inside the motif, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:16:35.013Z","design_id":31792,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the trees and foxes behind the main motifs, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:16:37.524Z","design_id":31791,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the crane motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:16:47.150Z","design_id":31786,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the deer and trees behind the main motifs, creating a double exposure look and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:17:11.383Z","design_id":31775,"category":"face-skull-damask","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is crosshatching and shading inside the skull motif that should be flat."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:17:13.639Z","design_id":31774,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded ghost of the skull motif behind the main skull, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:20:02.339Z","design_id":32095,"category":"face-skull-damask · antique-brass","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and interior shading inside the skull motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:20:04.751Z","design_id":32094,"category":"face-skull-damask · oxblood","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the motif and interior shading bleed inside the skull silhouette."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:20:06.861Z","design_id":32093,"category":"face-skull-damask · forest-loden","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the motif and interior shading bleed inside the skull silhouette."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:20:25.353Z","design_id":32085,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the branches and flowers behind the main motif, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:20:34.966Z","design_id":32081,"category":"cactus · champagne-linen","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The cactus motifs have a clear halftone layer and interior shading, creating a bleed effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:20:37.410Z","design_id":32080,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the cactus motifs behind the main motifs, and a halftone layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:20:41.755Z","design_id":32077,"category":"designer-zoo-calm · stone-pewter","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have a very obvious halftone layer behind them, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:20:55.059Z","design_id":32070,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The main motif is repeated as a faded ghost behind itself, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:21:28.201Z","design_id":32052,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the bird motifs behind the main motifs, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:21:30.037Z","design_id":32051,"category":"designer-zoo-calm · oxblood","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a halftone layer and interior shading bleed inside the silhouette."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:21:32.355Z","design_id":32050,"category":"designer-zoo-calm · forest-loden","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a halftone layer and interior shading inside the motif, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:21:36.699Z","design_id":32048,"category":"designer-zoo-calm","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a dot-screen layer, atmospheric haze, and interior shading inside the motif, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:21:47.801Z","design_id":32042,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the wolf motif behind the main motif, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:21:56.936Z","design_id":32038,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The tree motif has a faded duplicate behind it, a halftone layer, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:22:07.243Z","design_id":32033,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the deer motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:25:12.008Z","design_id":32384,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious ghosting effect of the deer motif, and a halftone layer behind the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:25:18.534Z","design_id":32381,"category":"face-skull-damask","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is crosshatching and shading inside the skull motif that should be flat."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:25:29.631Z","design_id":32376,"category":"chinoiserie · greenhouse-glass","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and interior shading inside the motif, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:25:31.696Z","design_id":32375,"category":"chinoiserie · indigo-library","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a halftone layer and interior shading inside the motif, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:25:36.507Z","design_id":32373,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the motifs and atmospheric haze, as well as interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:25:47.968Z","design_id":32368,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the deer motif behind the main motif, and atmospheric haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:25:50.442Z","design_id":32367,"category":"muybridge-plate · antique-tan","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious faded duplicate behind it, a halftone layer, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:25:54.827Z","design_id":32365,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The rooster motif has a very obvious faded duplicate behind it, as well as interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:26:14.097Z","design_id":32356,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded ghost of the skull-damask motif behind the main motif, and an atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:26:16.355Z","design_id":32355,"category":"monterey-mural","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The image has a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:26:18.486Z","design_id":32354,"category":"monterey-mural","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a dot-screen layer, atmospheric haze, and interior shading inside the tree silhouettes, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:26:42.067Z","design_id":32343,"category":"stripe","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the stripe motif behind the main stripes, and a haze effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:26:44.367Z","design_id":32342,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the cactus motif behind the main motif, creating a double exposure look with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:26:46.498Z","design_id":32341,"category":"designer-zoo-calm · indigo-library","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and interior shading inside the motif, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:26:48.750Z","design_id":32340,"category":"designer-zoo-calm · bordeaux-velvet","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the motifs and interior shading inside the silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:26:54.085Z","design_id":32338,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the foliage and animal motifs behind the main motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:27:01.477Z","design_id":32335,"category":"throttled-plaster-roses · stone-pewter","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The pattern has a blurred haze layer, interior shading, and a halftone layer behind the motif, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:30:14.848Z","design_id":32659,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the main motif behind itself."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:30:23.717Z","design_id":32655,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the cactus motifs behind the main motifs, creating a double exposure look with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:30:41.204Z","design_id":32647,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the fox motif behind the main motif, and atmospheric haze blurs the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:30:55.216Z","design_id":32641,"category":"face-skull-damask · stone-pewter","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is obvious interior shading inside the skull motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:31:05.708Z","design_id":32636,"category":"face-skull-damask","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the skull and crosshatching inside the skull silhouette."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:31:08.093Z","design_id":32635,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the fox motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:31:20.627Z","design_id":32628,"category":"designer-zoo-calm · antique-tan","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The animals have a dot-screen texture and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:31:25.033Z","design_id":32626,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have a faded duplicate behind them, a halftone layer, atmospheric haze, and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:31:43.389Z","design_id":32617,"category":"muybridge-plate · antique-tan","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The elephants have a clear ghosting effect and interior shading, with a halftone layer visible."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:31:45.986Z","design_id":32616,"category":"muybridge-plate · smoke-ash","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The elephants have a dot-screen layer behind them and crosshatching inside the silhouette."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:31:57.230Z","design_id":32611,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the deer motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:31:59.493Z","design_id":32610,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The elephant motifs are clearly repeated as faded ghosts behind themselves."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:32:01.879Z","design_id":32609,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the dinosaur motifs behind the main motifs, and a halftone layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:35:23.445Z","design_id":32948,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The bears have a faded duplicate behind them, a halftone layer, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:35:57.179Z","design_id":32937,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the fox motif behind the main motif, creating a double exposure look."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:35:59.837Z","design_id":32936,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded ghost of the wolf motif behind the main motif, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:36:04.385Z","design_id":32934,"category":"designer-scenic · oxblood","reason":"obvious bleed: halftone_layer, atmospheric_haze","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious halftone layer and atmospheric haze behind the motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:36:08.981Z","design_id":32932,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the motif, a halftone layer, and atmospheric haze behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:36:17.415Z","design_id":32928,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious faded duplicate behind it, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:36:33.088Z","design_id":32921,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the fox motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:36:35.510Z","design_id":32920,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the deer motif behind the main motif, creating a double exposure look with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:36:43.859Z","design_id":32916,"category":"face-skull-damask","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the motif and interior shading bleed inside the skull."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:37:03.645Z","design_id":32906,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the motif behind the main motif, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:37:12.621Z","design_id":32902,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the hare motif behind the main motif, and a sepia gradient behind the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:37:17.510Z","design_id":32900,"category":"chinoiserie · antique-tan","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a dot-screen layer, atmospheric haze, and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:37:22.153Z","design_id":32898,"category":"designer-zoo-calm · indigo-library","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The interior of the animal motifs have a dot-screen / halftone pattern, and the interior shading is visible."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:37:24.380Z","design_id":32897,"category":"designer-zoo-calm · bordeaux-velvet","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The interior of the animal motifs have a halftone dot pattern, and the interior shading is visible."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:40:53.545Z","design_id":33233,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The skull and damask motifs have a clear faded duplicate and halftone layer behind them, with interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:41:16.921Z","design_id":33229,"category":"muybridge-plate · stone-pewter","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious ghosted duplicate, halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:41:20.448Z","design_id":33228,"category":"muybridge-plate · greenhouse-glass","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious faded duplicate behind it, along with halftone and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:41:31.952Z","design_id":33226,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the cactus motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:41:43.102Z","design_id":33225,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a faded duplicate of the motif, halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:41:58.425Z","design_id":33220,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The fox motifs have a very obvious halftone ghost layer behind them."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:42:10.292Z","design_id":33218,"category":"muybridge-plate · bone-ivory","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious halftone layer and atmospheric haze behind it, creating a double exposure look."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:42:13.468Z","design_id":33217,"category":"muybridge-plate · stone-pewter","reason":"obvious bleed: halftone_layer, atmospheric_haze","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious halftone layer and atmospheric haze behind the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:42:16.745Z","design_id":33216,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The main motif has a very obvious faded ghost/duplicate behind itself, and there is atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:42:23.978Z","design_id":33213,"category":"designer-zoo-calm · midnight-navy","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a dot-screen layer behind the motif and crosshatching inside the raccoon silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:42:42.577Z","design_id":33208,"category":"designer-zoo-calm","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The red panda motifs have a halftone layer behind them and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:42:47.060Z","design_id":33207,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a clear faded duplicate of the tree branches behind the main motif, creating a double exposure look with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:42:53.878Z","design_id":33203,"category":"designer-zoo-calm · stone-pewter","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The interior of the motif has crosshatching and shading, which is a form of bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:43:07.707Z","design_id":33197,"category":"designer-zoo-calm","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The wolf motifs have interior shading and a halftone layer behind them, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:43:14.238Z","design_id":33194,"category":"chinoiserie · bone-ivory","reason":"obvious bleed: halftone_layer, atmospheric_haze","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious halftone layer and atmospheric haze behind the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:43:23.109Z","design_id":33190,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the motif behind the main motif, along with a hazy background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:43:25.447Z","design_id":33189,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the bird motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:45:36.261Z","design_id":33509,"category":"designer-zoo-calm","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is crosshatching inside the animal silhouettes that should be flat color."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:46:10.639Z","design_id":33491,"category":"chinoiserie","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a halftone layer, atmospheric haze, and interior shading bleed, creating an obvious bleed effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:46:19.700Z","design_id":33487,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the animal motifs behind the main motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:46:28.707Z","design_id":33482,"category":"designer-zoo-calm · smoke-ash","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The fox motif has interior shading and a halftone layer behind it, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:46:37.518Z","design_id":33478,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The fox motif has a faded duplicate behind it, a halftone layer, atmospheric haze, and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:46:41.456Z","design_id":33476,"category":"chinoiserie · stone-pewter","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer, atmospheric haze, and interior shading bleed present in the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:46:45.475Z","design_id":33473,"category":"chinoiserie","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has interior shading and a halftone layer, creating an atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:46:54.368Z","design_id":33469,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a clear faded duplicate of the cactus motifs, a halftone layer, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:47:09.212Z","design_id":33461,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The skull motif has a very obvious faded duplicate behind it, along with a halftone layer and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:47:11.578Z","design_id":33460,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the crane motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:50:12.646Z","design_id":33824,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The wolf motifs have a very obvious faded duplicate / echo behind them, and a halftone layer is visible."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:50:21.166Z","design_id":33820,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the trees and foliage behind the main motif, creating a double exposure look with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:50:36.554Z","design_id":33813,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the jaguar motif and atmospheric haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:50:37.512Z","design_id":30203,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The wolf and tree motifs have a faded, offset duplicate behind them, creating a double exposure effect with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:50:38.945Z","design_id":33812,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a faded duplicate behind it, a halftone layer, and atmospheric haze, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:50:47.180Z","design_id":33808,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded ghost of the main motif behind itself, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:51:14.022Z","design_id":33796,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a clear faded duplicate of the bird motifs behind the main motifs, creating a double exposure effect with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:51:22.893Z","design_id":33792,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the cactus motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:51:50.021Z","design_id":33780,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have a faded, ghosted duplicate behind them, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:51:58.644Z","design_id":33775,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the motifs behind the main motifs, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:52:12.827Z","design_id":33773,"category":"designer-zoo-calm · greenhouse-glass","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a dot-screen layer behind the motifs, and crosshatching inside the silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:52:17.227Z","design_id":33771,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The main motif is repeated as a faded ghost offset behind itself, and there is atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:53:37.193Z","design_id":34037,"category":"designer-scenic · oxblood","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious halftone layer and atmospheric haze behind it, creating a ghosting effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:53:56.668Z","design_id":34028,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The cactus motifs have a clear faded duplicate behind them, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:54:04.866Z","design_id":34024,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded ghost of the skull motif behind the main motif, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:54:07.026Z","design_id":34023,"category":"face-skull-damask · stone-pewter","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The skull motif has a halftone layer behind it and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:54:20.198Z","design_id":34017,"category":"face-skull-damask","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The skull motif has a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:54:22.227Z","design_id":34016,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the cactus motifs behind the main motifs, and atmospheric haze is present."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:54:40.094Z","design_id":34008,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a clear faded duplicate of the main motif, a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:54:55.017Z","design_id":34001,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the main motif behind itself, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:54:57.445Z","design_id":33999,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a clear faded duplicate of the tree branches behind the main motif, along with a hazy background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:55:03.617Z","design_id":33996,"category":"chinoiserie · antique-tan","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have a faded ghost behind them, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:55:05.913Z","design_id":33995,"category":"chinoiserie","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a blurred haze layer, interior shading, and a halftone layer behind the motif, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:55:09.499Z","design_id":34131,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The foxes have a faded duplicate behind them and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:55:14.465Z","design_id":33991,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the fox motif behind the main motif, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:55:19.274Z","design_id":34127,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very clear faded duplicate of the cactus motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:55:20.659Z","design_id":33988,"category":"muybridge-plate · smoke-ash","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is interior shading inside the motif that should be flat."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:55:21.378Z","design_id":34126,"category":"designer-zoo-calm · antique-tan","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have a dot-screen effect and interior shading, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:55:23.904Z","design_id":34125,"category":"designer-zoo-calm · smoke-ash","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The motifs have a dot-screen texture and interior shading, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:55:26.160Z","design_id":34124,"category":"designer-zoo-calm · champagne-linen","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have a dot-screen layer behind them and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:55:28.154Z","design_id":34123,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the main motif behind itself, and a halftone layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:55:32.337Z","design_id":33983,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the bear motifs behind the main motifs, creating a double exposure look with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:55:38.549Z","design_id":34118,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious ghosting effect behind the main motifs, and a halftone layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:55:40.898Z","design_id":33979,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the fox motif behind the main motif, and atmospheric haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:55:46.934Z","design_id":34114,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a clear faded duplicate of the cactus motifs behind the main layer, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:55:49.256Z","design_id":33975,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The skull motif has a faded duplicate behind it, a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:55:57.555Z","design_id":33971,"category":"designer-scenic · saddle-mocha","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a very strong halftone layer, atmospheric haze, and interior shading bleed, creating a double exposure look."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:56:04.203Z","design_id":33968,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a clear faded duplicate of the motif behind the main image, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:56:13.100Z","design_id":34102,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the main motif behind itself, and an atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:56:15.310Z","design_id":34101,"category":"cactus · indigo-library","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The cactus motifs have interior shading and a halftone layer behind them, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:56:17.577Z","design_id":34100,"category":"cactus · bordeaux-velvet","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the cacti and interior shading within the cacti silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:56:18.436Z","design_id":33960,"category":"designer-scenic · indigo-library","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a clear halftone layer and atmospheric haze behind the main motif, creating a ghosting effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:56:20.371Z","design_id":33959,"category":"designer-scenic · bordeaux-velvet","reason":"obvious bleed: halftone_layer, atmospheric_haze","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious halftone layer and atmospheric haze behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:56:22.175Z","design_id":34098,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The cactus motifs have a very obvious faded ghost / duplicate behind them, as well as interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:56:28.897Z","design_id":34095,"category":"muybridge-plate · champagne-linen","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is interior shading inside the silhouette of the animal motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:56:33.045Z","design_id":34093,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a faded duplicate of the main motif, a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:56:39.367Z","design_id":33950,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the deer motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:56:41.610Z","design_id":33949,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the motif behind the main figures, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:56:41.990Z","design_id":34089,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded ghost of the fox motif behind the main motif, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:56:50.735Z","design_id":34085,"category":"face-skull-damask · manor-saddle","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the motif and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:56:59.159Z","design_id":34082,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the bear motifs behind the main motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:57:01.600Z","design_id":34081,"category":"face-skull-damask","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the skull motif and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:57:14.807Z","design_id":33931,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the deer motif behind the main motif, and atmospheric haze blurs the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:57:16.049Z","design_id":34075,"category":"designer-zoo-calm · forest-loden","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the motif and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:57:19.245Z","design_id":33929,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the fox motifs behind the main motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:57:23.060Z","design_id":33927,"category":"muybridge-plate · champagne-linen","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a clear faded duplicate of the motif, a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:57:25.224Z","design_id":33926,"category":"muybridge-plate · bone-ivory","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image shows a clear faded duplicate of the bird motifs, a halftone layer, atmospheric haze, and interior shading, indicating significant motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:57:27.314Z","design_id":33924,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The parrot motifs have a very obvious faded ghost/duplicate behind them, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:57:42.580Z","design_id":33916,"category":"chinoiserie · bordeaux-velvet","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a halftone layer, atmospheric haze, and interior shading bleed, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:57:47.382Z","design_id":33914,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have a clear faded ghost behind them, a halftone layer, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:57:49.689Z","design_id":33913,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the rose motif behind the main roses, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:57:52.499Z","design_id":33912,"category":"chinoiserie","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a halftone layer, atmospheric haze, and interior shading bleed, creating a double exposure look."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:57:59.128Z","design_id":33909,"category":"designer-zoo-calm · bone-ivory","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a dot-screen layer behind the motifs and crosshatching inside the silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:58:01.280Z","design_id":33908,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the animal motifs behind the main motifs, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:58:21.727Z","design_id":33898,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the lion motifs behind the main motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:58:23.862Z","design_id":33897,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The foxes have a faded ghost image behind them, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:58:26.561Z","design_id":33896,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The main motif has a faded, atmospheric haze duplicate behind itself, creating a double exposure look."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:58:34.572Z","design_id":33892,"category":"chinoiserie","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a dot-screen layer, atmospheric haze, and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:58:43.247Z","design_id":33888,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a clear ghost of the fox motif behind the main one, and interior shading within the fox silhouette."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:59:02.051Z","design_id":33880,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded ghost of the dinosaur motif behind the main motif, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:59:06.290Z","design_id":33877,"category":"muybridge-plate · greenhouse-glass","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is interior shading inside the motif silhouette that should be flat."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:59:10.419Z","design_id":33875,"category":"muybridge-plate","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is interior shading inside the cat motifs that should be flat."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:59:18.768Z","design_id":33870,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the raccoon motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:59:33.368Z","design_id":33863,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious ghosted duplicate of the cactus motifs behind the main motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:59:35.435Z","design_id":33862,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the lion motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:59:39.735Z","design_id":33859,"category":"muybridge-plate · bordeaux-velvet","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious faded duplicate behind it, a halftone layer, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:59:41.730Z","design_id":33858,"category":"muybridge-plate · manor-saddle","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The deer motif has a very obvious halftone layer and ghosting effect behind it."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:59:44.461Z","design_id":33857,"category":"muybridge-plate · slate-mist","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a clear faded duplicate behind it, a halftone layer, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T17:59:46.658Z","design_id":33855,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the deer motif behind the main motif, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:00:01.380Z","design_id":33847,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the lemur motif behind the main motif, creating a double exposure look with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:00:03.420Z","design_id":34404,"category":"monterey-mural","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The image has a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:00:03.659Z","design_id":33846,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded ghost of the cactus motif behind the main motif, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:00:12.677Z","design_id":33841,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the main motif behind itself, and a halftone layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:00:18.408Z","design_id":34397,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the cactus motif behind the main motif, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:00:20.767Z","design_id":33837,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the main motif behind itself, and a halftone layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:00:32.065Z","design_id":34390,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the bear motif behind the main motif, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:00:32.621Z","design_id":33830,"category":"designer-scenic · saddle-mocha","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the motif behind the main motif, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:00:33.964Z","design_id":34389,"category":"throttled-plaster-roses · stone-pewter","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and interior shading inside the motif, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:00:34.896Z","design_id":33829,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a faded duplicate behind itself, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:00:36.975Z","design_id":33828,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded ghost of the floral motif behind the main motif, and atmospheric haze blurs the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:00:38.884Z","design_id":33766,"category":"throttled-plaster-roses · saddle-mocha","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the motif and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:00:43.508Z","design_id":33764,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the motifs behind the main motifs, creating a double exposure look with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:00:45.976Z","design_id":33763,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded ghost of the rose motif behind the main motif, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:00:46.727Z","design_id":34383,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the rose motif behind the main motif, and an atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:00:54.865Z","design_id":33759,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The animal motifs have a clear faded duplicate / echo behind them, and there is atmospheric haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:00:57.895Z","design_id":34378,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a very obvious faded ghost/duplicate behind them, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:01:00.185Z","design_id":34377,"category":"throttled-plaster-roses · bordeaux-velvet","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious faded duplicate behind it, along with a halftone layer and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:01:02.473Z","design_id":34376,"category":"throttled-plaster-roses · manor-saddle","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious faded duplicate and halftone layer behind it, with interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:01:07.277Z","design_id":34374,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The main rose motif has a very obvious faded ghost / duplicate behind itself, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:01:11.559Z","design_id":33751,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The skull motif has a very obvious faded duplicate behind it, along with a halftone layer and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:01:13.842Z","design_id":33750,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded ghost of the animal motifs behind the main motifs, creating a double exposure look with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:01:14.039Z","design_id":34371,"category":"designer-zoo-calm · bone-ivory","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The bears and trees have interior shading that should be flat, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:01:15.810Z","design_id":33749,"category":"face-skull-damask · deep-plum","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The skull motif has crosshatching inside the silhouette that should be flat."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:01:16.257Z","design_id":34369,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the bear motif behind the main motif, and atmospheric haze blurs the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:01:18.256Z","design_id":34368,"category":"throttled-plaster-roses · deep-plum","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image shows a clear faded duplicate of the rose motif, a halftone layer, atmospheric haze, and interior shading, indicating significant motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:01:20.009Z","design_id":33747,"category":"face-skull-damask · antique-tan","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The skull motif has a faded duplicate behind it, a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:01:20.717Z","design_id":34367,"category":"throttled-plaster-roses · saddle-mocha","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a very obvious faded duplicate behind them, a halftone layer, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:01:22.547Z","design_id":33745,"category":"face-skull-damask","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The skull motif has a halftone layer behind it and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:01:23.414Z","design_id":34366,"category":"throttled-plaster-roses · antique-tan","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a very obvious faded duplicate behind them, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:01:25.843Z","design_id":34364,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the rose motif behind the main motif, and an atmospheric haze is present."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:01:31.560Z","design_id":33741,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious ghost/duplicate of the fox motif behind the main motif, and a halftone layer behind the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:01:36.978Z","design_id":34359,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a clear faded duplicate of the motifs behind the main motifs, creating a double exposure look and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:01:39.274Z","design_id":34357,"category":"chinoiserie · bordeaux-velvet","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and interior shading inside the motif, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:01:43.687Z","design_id":34355,"category":"chinoiserie · slate-mist","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The dragon motif has interior shading and a halftone layer behind it, creating a bleed effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:01:46.007Z","design_id":34354,"category":"chinoiserie","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The dragons have a dot-screen haze behind them and interior shading, creating a bleed effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:01:46.129Z","design_id":33733,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a faded duplicate of the main motif, a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:01:52.395Z","design_id":33729,"category":"chinoiserie · manor-saddle","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The dragon motif has interior shading and a halftone layer behind it, creating a bleed effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:01:54.528Z","design_id":33728,"category":"chinoiserie · slate-mist","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The dragons have a dot-screen effect and interior shading, with a hazy background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:01:55.470Z","design_id":34349,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The wolf motifs have a faded, ghosted duplicate behind them, along with a halftone layer and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:02:04.971Z","design_id":33722,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the fox motif behind the main motif, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:02:06.418Z","design_id":34343,"category":"face-skull-damask · saddle-mocha","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the skull motif and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:02:10.715Z","design_id":34341,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a clear faded duplicate of the floral motif behind the main one, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:02:13.425Z","design_id":34340,"category":"face-skull-damask","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The skull motif has a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:02:19.654Z","design_id":33715,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The cranes have a faded duplicate behind them, a halftone layer, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:02:21.817Z","design_id":33714,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the otter motifs behind the main motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:02:27.786Z","design_id":33710,"category":"designer-zoo-calm · greenhouse-glass","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious halftone layer and interior shading bleed inside the wolf motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:02:30.218Z","design_id":33709,"category":"designer-zoo-calm","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and interior shading inside the wolf motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:02:56.997Z","design_id":33695,"category":"designer-scenic · smoke-ash","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The tree motif has a halftone layer and atmospheric haze behind it, with interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:03:09.431Z","design_id":33689,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the tree branches behind the main motif, and atmospheric haze is present."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:03:14.110Z","design_id":33687,"category":"designer-scenic · deep-plum","reason":"obvious bleed: halftone_layer, atmospheric_haze","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious halftone layer and atmospheric haze behind the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:03:29.263Z","design_id":33679,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have a very obvious ghosted duplicate behind them, and a halftone layer is visible."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:03:52.499Z","design_id":33668,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The main motif has a faded, hazy duplicate behind it, creating a double exposure look."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:03:54.567Z","design_id":33667,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the cactus motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:04:03.139Z","design_id":33662,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The fox motifs have a faded, offset duplicate behind them, a halftone layer, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:04:31.046Z","design_id":33646,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the animals and trees behind the main motifs, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:04:39.615Z","design_id":33642,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious faded duplicate behind it, a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:04:48.650Z","design_id":33638,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the tree branches behind the main motif, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:05:05.638Z","design_id":33629,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the rose motif behind the main motif, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:05:14.307Z","design_id":33625,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the bear motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:05:17.556Z","design_id":34687,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the motif behind the main figures, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:05:19.001Z","design_id":33623,"category":"cactus · antique-tan","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a halftone layer behind the cacti and interior shading inside the cacti silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:05:23.116Z","design_id":33621,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the cactus motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:05:26.391Z","design_id":34683,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The cranes have a faded duplicate behind them, a halftone layer, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:05:32.781Z","design_id":33616,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the animal motifs behind the main motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:05:34.766Z","design_id":33615,"category":"throttled-plaster-roses · indigo-library","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious halftone layer and atmospheric haze behind it, creating a ghosting effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:05:42.021Z","design_id":33612,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The tree motifs have a very obvious faded duplicate / echo behind them, with a hazy background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:05:42.462Z","design_id":34676,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded ghost of the motifs behind the main motifs, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:05:44.714Z","design_id":34675,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the animal motifs behind the main motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:05:50.932Z","design_id":33608,"category":"chinoiserie","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a dot-screen effect, atmospheric haze, and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:05:54.317Z","design_id":34670,"category":"throttled-plaster-roses · midnight-navy","reason":"obvious bleed: halftone_layer, atmospheric_haze","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The pattern has a blurred haze and dot-screen effect, creating an obvious bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:05:56.478Z","design_id":34669,"category":"throttled-plaster-roses · deep-plum","reason":"obvious bleed: halftone_layer, atmospheric_haze","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The pattern has a blurred haze layer behind the main motif, creating an atmospheric effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:05:58.908Z","design_id":34668,"category":"throttled-plaster-roses · saddle-mocha","reason":"obvious bleed: halftone_layer, atmospheric_haze","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The pattern has a blurred haze and dot-screen effect, creating an atmospheric bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:05:59.790Z","design_id":33604,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded ghost of the cactus motifs behind the main motifs, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:06:03.729Z","design_id":34666,"category":"designer-zoo-calm · bone-ivory","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a dot-screen layer behind it and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:06:08.670Z","design_id":33600,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The wolf motifs have a faded ghost/duplicate behind them, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:06:16.144Z","design_id":33597,"category":"face-skull-damask · manor-saddle","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the skull motif behind the main motif, with a hazy background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:06:17.478Z","design_id":34659,"category":"designer-zoo-calm","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a halftone layer, atmospheric haze, and interior shading bleed, creating a double exposure look."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:06:19.752Z","design_id":34658,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the wolf motif behind the main motif, and atmospheric haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:06:27.904Z","design_id":34654,"category":"face-skull-damask","reason":"obvious bleed: atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a sepia gradient behind the motif and crosshatching inside the skull silhouette."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:06:36.878Z","design_id":34650,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious ghosted duplicate of the raccoon motif behind the main motif, and a halftone layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:06:40.798Z","design_id":34648,"category":"designer-zoo-calm · champagne-linen","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious halftone layer and ghosting effect behind the crane motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:06:43.961Z","design_id":33584,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the bamboo motif behind the main motif, creating a double exposure effect with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:06:44.949Z","design_id":34646,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the crane motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:06:46.429Z","design_id":33582,"category":"throttled-plaster-roses · oxblood","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a halftone layer and interior shading, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:06:52.964Z","design_id":33579,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a faded, ghosted duplicate behind them, and there is a soft atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:06:54.240Z","design_id":34642,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded ghost of the motif behind the main motif, and a soft haze blurs the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:07:03.364Z","design_id":33574,"category":"muybridge-plate · greenhouse-glass","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image shows a clear faded duplicate of the bird motif with halftone and atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:07:05.358Z","design_id":33573,"category":"muybridge-plate · indigo-library","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image shows a clear faded duplicate of the bird motif, a halftone layer, atmospheric haze, and interior shading, indicating significant motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:07:07.550Z","design_id":33572,"category":"muybridge-plate · bordeaux-velvet","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image shows a clear faded duplicate of the motif, halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:07:09.608Z","design_id":33571,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the bird motif behind the main motif, and atmospheric haze is present."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:07:26.006Z","design_id":33563,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The skull motif has a faded duplicate behind it, a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:07:28.000Z","design_id":33562,"category":"chinoiserie · greenhouse-glass","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have a dot-screen layer behind them and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:07:30.178Z","design_id":33561,"category":"chinoiserie · indigo-library","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The motifs have a dot-screen layer behind them and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:07:51.515Z","design_id":33551,"category":"chinoiserie","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a dot-screen layer, atmospheric haze, and interior shading inside the motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:07:53.429Z","design_id":33550,"category":"designer-zoo-calm · deep-plum","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and interior shading inside the bird motifs, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:07:55.449Z","design_id":33549,"category":"designer-zoo-calm · saddle-mocha","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a halftone layer and interior shading inside the bird motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:07:57.404Z","design_id":33548,"category":"designer-zoo-calm · antique-tan","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and interior shading inside the bird motifs, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:07:59.416Z","design_id":33547,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The cranes have a faded duplicate behind them and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:08:03.972Z","design_id":33545,"category":"cactus · oxblood","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the motif and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:08:06.200Z","design_id":33544,"category":"cactus · forest-loden","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the cacti and interior shading within the cacti silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:08:08.658Z","design_id":33543,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious ghosted duplicate of the cactus motifs in a lighter color behind the main motifs, and a halftone layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:08:17.186Z","design_id":33538,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The leopard motif has a very obvious faded duplicate behind it, and the background has a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:08:34.530Z","design_id":33528,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The main motif has a faded duplicate behind itself, a halftone layer, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:09:14.931Z","design_id":33445,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a clear faded duplicate behind them, a halftone layer, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:09:17.155Z","design_id":33444,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious faded duplicate behind it, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:09:19.909Z","design_id":33443,"category":"throttled-plaster-roses · bone-ivory","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a halftone layer behind it and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:09:36.107Z","design_id":33436,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the animal motifs behind the main motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:09:40.103Z","design_id":33434,"category":"muybridge-plate · manor-saddle","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a very obvious halftone layer and interior shading bleed, as well as atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:09:42.446Z","design_id":33433,"category":"muybridge-plate · slate-mist","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a halftone layer, atmospheric haze, and interior shading bleed, making the motif look like a double exposure."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:09:51.149Z","design_id":33429,"category":"muybridge-plate","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer, atmospheric haze, and interior shading bleed inside the horse motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:09:53.573Z","design_id":33428,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the motifs behind the main motifs, and atmospheric haze is present."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:10:02.697Z","design_id":34975,"category":"designer-zoo-calm · smoke-ash","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have a dot-screen layer behind them and interior shading, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:10:04.764Z","design_id":34974,"category":"designer-zoo-calm · champagne-linen","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have a dot-screen effect and interior shading, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:10:06.783Z","design_id":34973,"category":"designer-zoo-calm · bone-ivory","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have interior shading and a halftone layer behind them, creating a bleed effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:10:15.822Z","design_id":34969,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The main motif is repeated as a faded ghost behind itself, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:10:29.544Z","design_id":34963,"category":"designer-zoo-calm · manor-saddle","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The panda motifs have a dot-screen layer behind them and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:10:36.215Z","design_id":34960,"category":"designer-zoo-calm","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has interior shading and a halftone layer behind it, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:10:40.375Z","design_id":33406,"category":"face-skull-damask · bordeaux-velvet","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the skull motif and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:10:42.428Z","design_id":33405,"category":"face-skull-damask · manor-saddle","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the skull motif and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:10:44.615Z","design_id":33404,"category":"face-skull-damask · slate-mist","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The skull motif has a halftone layer behind it and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:10:45.014Z","design_id":34956,"category":"muybridge-plate · champagne-linen","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The dog motifs have a clear halftone layer behind them and interior shading, creating a double exposure look."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:10:47.058Z","design_id":34955,"category":"muybridge-plate · bone-ivory","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The dog motifs have a clear halftone layer behind them and interior shading, creating a double exposure look."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:10:49.470Z","design_id":34954,"category":"muybridge-plate · stone-pewter","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious halftone layer behind the dog motifs, creating a bleed effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:10:54.004Z","design_id":34952,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious faded duplicate behind it, along with a halftone layer and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:10:56.192Z","design_id":33399,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have a faded duplicate behind them, a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:11:03.674Z","design_id":34948,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the cactus motifs behind the main motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:11:06.433Z","design_id":33395,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious ghosting effect and atmospheric haze, as well as interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:11:17.456Z","design_id":33390,"category":"monterey-mural","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:11:18.883Z","design_id":34941,"category":"throttled-plaster-roses · smoke-ash","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a clear halftone layer and interior shading, creating a bleed effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:11:19.721Z","design_id":33389,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The main motif has a faded ghost duplicate behind itself, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:11:31.364Z","design_id":34936,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a faded duplicate behind them and interior shading, creating a double exposure effect with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:11:35.750Z","design_id":34934,"category":"designer-zoo-calm · deep-plum","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the motifs and interior shading inside the silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:11:36.499Z","design_id":33381,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The fox motifs have a very obvious halftone layer behind them, creating a double exposure look."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:11:40.383Z","design_id":34932,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The main motif is repeated as a faded ghost offset behind itself, and there is atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:11:50.959Z","design_id":34926,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious ghosted duplicate and interior shading, with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:11:51.298Z","design_id":33373,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the fox motifs behind the main motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:11:55.569Z","design_id":33371,"category":"face-skull-damask · slate-mist","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The skull motif has crosshatching and shading inside the silhouette that should be flat."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:11:57.616Z","design_id":34923,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the motif, a halftone layer, and atmospheric haze behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:11:57.748Z","design_id":33370,"category":"face-skull-damask · antique-brass","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The skull motif has interior shading that should be flat, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:12:00.107Z","design_id":33369,"category":"face-skull-damask · oxblood","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is crosshatching and shading inside the skull motif, which should be flat."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:12:05.937Z","design_id":34919,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the horse motif behind the main motif, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:12:08.182Z","design_id":34918,"category":"muybridge-plate · bordeaux-velvet","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image shows a clear ghosting effect, halftone layer, atmospheric haze, and interior shading bleed within the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:12:10.161Z","design_id":34917,"category":"muybridge-plate · manor-saddle","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a halftone layer, atmospheric haze, and interior shading bleed, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:12:12.214Z","design_id":34916,"category":"muybridge-plate · slate-mist","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious ghosted duplicate, halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:12:14.416Z","design_id":34915,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The buffalo motifs have a clear faded duplicate behind them, a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:12:27.132Z","design_id":33357,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The main motif has a faded duplicate behind itself, a halftone layer, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:12:29.354Z","design_id":33356,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The dog motif has a very obvious ghosted duplicate and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:12:37.076Z","design_id":33352,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the wolf motif behind the main motif, and crosshatching inside the wolf silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:12:46.682Z","design_id":33347,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a faded duplicate of the main motif, a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:12:57.491Z","design_id":33342,"category":"chinoiserie · deep-plum","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a dot-screen effect, atmospheric haze, and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:12:59.601Z","design_id":33341,"category":"chinoiserie · saddle-mocha","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a dot-screen layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:13:04.283Z","design_id":33339,"category":"chinoiserie","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a halftone layer, atmospheric haze, and interior shading bleed, creating a double exposure look."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:13:15.392Z","design_id":33333,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very clear faded ghost/duplicate of the deer motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:13:24.662Z","design_id":33329,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the motif behind the main pattern, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:13:31.691Z","design_id":33326,"category":"chinoiserie · deep-plum","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a dot-screen layer behind the motif, and crosshatching inside the motif silhouette."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:13:49.258Z","design_id":33318,"category":"muybridge-plate · greenhouse-glass","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The elephant motifs have a clear halftone layer and atmospheric haze behind them, creating a 'double exposure' look."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:13:51.606Z","design_id":33317,"category":"muybridge-plate · indigo-library","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious halftone layer and ghosting effect behind the elephant motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:14:00.858Z","design_id":33313,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded ghost of the elephant motif behind the main motif, and a soft haze behind the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:14:09.811Z","design_id":33308,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The wolf motifs have a faded, offset duplicate behind them, along with halftone and atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:14:26.536Z","design_id":33300,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the animal motifs behind the main motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:14:35.516Z","design_id":33296,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the motif behind the main motif, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:14:43.901Z","design_id":33292,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the panther motif behind the main motif, creating a double exposure look with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:15:01.825Z","design_id":33284,"category":"chinoiserie","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a dot-screen layer, atmospheric haze, and interior shading inside the motif, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:15:03.992Z","design_id":33283,"category":"chinoiserie · bordeaux-velvet","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has interior shading and a halftone layer behind it, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:15:05.989Z","design_id":33282,"category":"chinoiserie · manor-saddle","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a halftone layer and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:15:08.273Z","design_id":33281,"category":"chinoiserie · slate-mist","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a dot-screen layer behind the motif and interior shading inside the silhouette."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:15:09.672Z","design_id":35274,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The fox motifs have a very obvious faded duplicate / echo behind them, and the background has a halftone dot-screen effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:15:10.524Z","design_id":33280,"category":"chinoiserie","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a dot-screen layer, atmospheric haze, and interior shading inside the lion motif, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:15:14.992Z","design_id":33278,"category":"chinoiserie · smoke-ash","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a faded duplicate behind it, a halftone layer, atmospheric haze, and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:15:17.036Z","design_id":33277,"category":"chinoiserie · champagne-linen","reason":"obvious bleed: halftone_layer, atmospheric_haze","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious halftone layer and atmospheric haze behind the motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:15:18.875Z","design_id":33276,"category":"chinoiserie · bone-ivory","reason":"obvious bleed: halftone_layer, atmospheric_haze","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious halftone layer and atmospheric haze behind the motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:15:24.572Z","design_id":35266,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the main motifs behind themselves."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:15:26.834Z","design_id":35265,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the main motif behind itself, and atmospheric haze blurs the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:15:26.966Z","design_id":33272,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a faded duplicate of the main motif, a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:15:29.143Z","design_id":33271,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the main motif behind itself, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:15:35.483Z","design_id":35261,"category":"chinoiserie · antique-tan","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:15:37.478Z","design_id":35260,"category":"chinoiserie · smoke-ash","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a dot-screen layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:15:41.643Z","design_id":35258,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the cactus motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:15:46.543Z","design_id":33263,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the fox motif behind the main motif, and a halftone layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:15:57.004Z","design_id":33258,"category":"face-skull-damask · antique-tan","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a faded duplicate behind it, a halftone layer, atmospheric haze, and interior shading, indicating significant motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:15:58.030Z","design_id":35250,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the main motif behind itself, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:16:01.161Z","design_id":33256,"category":"designer-scenic","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and interior shading inside the silhouette of the flowers and branches."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:16:03.250Z","design_id":33255,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a faded duplicate behind it, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:16:09.438Z","design_id":35245,"category":"throttled-plaster-roses · antique-brass","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a very obvious faded duplicate behind them, with a halftone layer and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:16:11.837Z","design_id":35244,"category":"throttled-plaster-roses · oxblood","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image shows a clear faded duplicate of the rose motifs, a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:16:12.123Z","design_id":33251,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the motifs behind the main motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:16:14.001Z","design_id":35243,"category":"throttled-plaster-roses · forest-loden","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a very obvious faded duplicate behind them, a halftone layer, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:16:14.265Z","design_id":33250,"category":"monterey-mural","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The image has a halftone layer, atmospheric haze, and interior shading bleed, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:16:16.419Z","design_id":35242,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the rose motif and an atmospheric haze behind the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:16:18.506Z","design_id":35241,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a clear faded duplicate behind them, a halftone layer, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:16:23.444Z","design_id":33227,"category":"muybridge-plate · indigo-library","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image shows a clear double exposure effect with a halftone layer and interior shading within the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:16:25.445Z","design_id":33219,"category":"muybridge-plate · champagne-linen","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious halftone layer and atmospheric haze behind it, creating a ghosting effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:16:27.585Z","design_id":35237,"category":"designer-zoo-calm · bone-ivory","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The bears have a halftone layer behind them and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:16:32.170Z","design_id":35235,"category":"designer-zoo-calm · greenhouse-glass","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the bears, creating a ghosting effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:16:34.396Z","design_id":35234,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a very obvious ghosting effect, halftone layer, and atmospheric haze behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:16:35.688Z","design_id":33179,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The wolves have a faded, offset duplicate behind them, creating a double exposure effect with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:16:36.780Z","design_id":35233,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The bears have a faded ghost image behind them, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:16:48.183Z","design_id":35228,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the wolf motif behind the main motif, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:16:54.224Z","design_id":33171,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the fox motif behind the main motif, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:17:02.974Z","design_id":35221,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a clear faded duplicate behind them, a halftone layer, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:17:05.464Z","design_id":33166,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the cactus motifs behind the main motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:17:05.685Z","design_id":35220,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the deer motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:17:23.158Z","design_id":33159,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the animal motifs behind the main motifs, creating a double exposure look with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:17:25.889Z","design_id":33158,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the main motif behind itself."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:17:33.718Z","design_id":33154,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a very obvious motif ghost, halftone layer, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:17:42.299Z","design_id":33150,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The main motif is repeated as a faded ghost behind itself, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:17:45.725Z","design_id":33149,"category":"throttled-plaster-roses · greenhouse-glass","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious halftone layer and atmospheric haze behind it, creating a ghosting effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:17:50.587Z","design_id":33147,"category":"throttled-plaster-roses · bordeaux-velvet","reason":"obvious bleed: halftone_layer, atmospheric_haze","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and atmospheric haze behind the roses, creating a bleed effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:17:59.648Z","design_id":33143,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the rose motif behind the main motif, and a soft atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:18:01.735Z","design_id":33142,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the fox motif behind the main motif, as well as atmospheric haze and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:18:11.982Z","design_id":33138,"category":"designer-zoo-calm · manor-saddle","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The trees and animals have a dot-screen effect and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:18:24.676Z","design_id":33135,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the crane motifs behind the main motifs, with a blurred haze layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:18:27.431Z","design_id":33134,"category":"designer-scenic · smoke-ash","reason":"obvious bleed: halftone_layer, atmospheric_haze","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The background has a very obvious halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:18:29.360Z","design_id":33133,"category":"designer-scenic · champagne-linen","reason":"obvious bleed: halftone_layer, atmospheric_haze","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious halftone layer and atmospheric haze behind the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:18:32.635Z","design_id":33132,"category":"designer-scenic · bone-ivory","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious faded duplicate behind it, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:18:45.584Z","design_id":33130,"category":"throttled-plaster-roses · greenhouse-glass","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a very obvious halftone ghost behind them, creating a double exposure effect with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:18:48.241Z","design_id":33129,"category":"throttled-plaster-roses · indigo-library","reason":"obvious bleed: halftone_layer, atmospheric_haze","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a strong halftone layer and atmospheric haze behind the roses, creating a bleed effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:18:52.596Z","design_id":33127,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the tree motif behind the main motif, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:18:57.552Z","design_id":33125,"category":"face-skull-damask · midnight-navy","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the skull motif and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:19:04.250Z","design_id":33122,"category":"designer-zoo-calm · bordeaux-velvet","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a dot-screen layer behind the motif and crosshatching inside the silhouette."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:19:06.568Z","design_id":33120,"category":"designer-zoo-calm · manor-saddle","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a dot-screen layer behind the motif and crosshatching inside the silhouette."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:19:11.316Z","design_id":33118,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The skull motif has a faded duplicate behind it, a halftone layer, atmospheric haze, and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:19:13.468Z","design_id":33117,"category":"designer-zoo-calm · stone-pewter","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The raccoon motifs have a halftone layer behind them and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:19:15.473Z","design_id":33116,"category":"designer-zoo-calm · greenhouse-glass","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The raccoon motifs have a halftone layer behind them and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:19:17.932Z","design_id":33115,"category":"designer-zoo-calm · indigo-library","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The raccoon motifs have interior shading and a halftone layer behind them."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:19:24.561Z","design_id":33112,"category":"designer-zoo-calm","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The animal motifs have interior shading and a halftone layer behind them, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:19:40.705Z","design_id":33105,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious ghosted deer motif behind the main deer motif, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:19:49.649Z","design_id":33101,"category":"designer-zoo-calm · oxblood","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious halftone layer behind the motif, creating a ghosting effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:19:51.909Z","design_id":33100,"category":"designer-zoo-calm · forest-loden","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The wolf motif has a very obvious halftone ghost layer behind it."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:19:59.699Z","design_id":33097,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the wolf motif behind the main motif, and atmospheric haze is present."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:20:15.787Z","design_id":33094,"category":"face-skull-damask · saddle-mocha","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is crosshatching and shading inside the skull motif that should be flat."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:20:16.706Z","design_id":35575,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the motifs behind the main motifs, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:20:34.587Z","design_id":33086,"category":"cactus · champagne-linen","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The cactus motifs have interior shading and a halftone layer behind them, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:20:36.848Z","design_id":33085,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the bear motifs behind the main motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:20:39.353Z","design_id":33084,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious ghost/duplicate of the cactus motif behind the main motif, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:20:39.532Z","design_id":35563,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the rose motif behind the main rose, and a soft haze behind the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:20:41.597Z","design_id":35562,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The foxes have a very obvious ghosted duplicate behind them, with halftone and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:20:44.683Z","design_id":35561,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the skull motif behind the main motif, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:20:46.114Z","design_id":33081,"category":"designer-zoo-calm · forest-loden","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"Halftone layer and interior shading inside the wolf motifs indicate motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:20:50.716Z","design_id":33079,"category":"designer-zoo-calm · champagne-linen","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a dot-screen layer and interior shading inside the motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:20:55.065Z","design_id":35556,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the wolf motif behind the main motif, and a soft haze behind the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:20:55.502Z","design_id":33077,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The image has a faded duplicate of the main motif, a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:21:00.394Z","design_id":33075,"category":"muybridge-plate · forest-loden","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The horse motifs have a very obvious halftone layer behind them, creating a ghosting effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:21:02.730Z","design_id":33074,"category":"muybridge-plate · midnight-navy","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The horse motifs have a very obvious halftone layer behind them, creating a bleed effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:21:04.877Z","design_id":33073,"category":"muybridge-plate · deep-plum","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The horse motifs have a very obvious halftone ghost layer behind them, creating a double exposure effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:21:14.562Z","design_id":33069,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The horse motifs have a very obvious faded duplicate / echo behind them, and there is a soft haze behind the motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:21:17.288Z","design_id":33068,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the motif and atmospheric haze behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:21:21.734Z","design_id":35547,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the fox motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:21:24.263Z","design_id":35546,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the animal motifs behind the main motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:21:34.442Z","design_id":35541,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the bear motif behind the main motif, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:21:36.706Z","design_id":35540,"category":"monterey-mural","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The tree has a halftone layer and atmospheric haze behind it, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:21:41.060Z","design_id":33060,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious faded duplicate behind it, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:21:55.036Z","design_id":35536,"category":"designer-zoo-calm · smoke-ash","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have a clear faded ghost/duplicate behind them, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:21:59.077Z","design_id":33053,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the tree branches and flowers behind the main motif, as well as atmospheric haze and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:22:01.514Z","design_id":33052,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the main motif behind itself."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:22:02.409Z","design_id":35533,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the main motif behind itself."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:22:04.605Z","design_id":35532,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The main motif is repeated as a faded ghost behind itself, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:22:07.067Z","design_id":35531,"category":"throttled-plaster-roses · oxblood","reason":"obvious bleed: halftone_layer, atmospheric_haze","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The background has a blurred haze layer that sits behind the sharp motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:22:12.142Z","design_id":35529,"category":"throttled-plaster-roses · midnight-navy","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a blurred, hazy background and interior shading, creating a bleed effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:22:24.365Z","design_id":33044,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the cactus motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:22:24.467Z","design_id":35524,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded ghost of the wolf motif behind the main motif, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:22:27.183Z","design_id":33043,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the deer motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:22:36.290Z","design_id":33039,"category":"throttled-plaster-roses · forest-loden","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the motif and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:22:38.548Z","design_id":33038,"category":"throttled-plaster-roses · midnight-navy","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a halftone layer and interior shading, creating a bleed effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:22:41.300Z","design_id":33037,"category":"throttled-plaster-roses · deep-plum","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a halftone layer, atmospheric haze, and interior shading bleed, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:22:42.620Z","design_id":35519,"category":"designer-zoo-calm · indigo-library","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the motifs and interior shading bleed inside the silhouette."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:22:45.388Z","design_id":35518,"category":"designer-zoo-calm · bordeaux-velvet","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a dot-screen layer behind the motif, and interior shading inside the silhouette."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:22:47.808Z","design_id":35517,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the bird motif behind the main motif, and a soft haze behind the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:22:50.344Z","design_id":35516,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded ghost of the animal motifs behind the main motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:23:11.730Z","design_id":33028,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded ghost of the fox motif behind the main motif, and an atmospheric haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:23:14.585Z","design_id":33027,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the motif, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:23:25.377Z","design_id":33022,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the animal motifs behind the main motifs, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:23:41.308Z","design_id":33018,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the cactus motifs behind the main motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:23:58.432Z","design_id":33011,"category":"designer-zoo-calm · champagne-linen","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the motif and interior shading bleed inside the silhouette."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:24:00.793Z","design_id":33010,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the main motif behind itself."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:24:15.787Z","design_id":33006,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The main motif has a faded duplicate behind it, a halftone layer, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:24:35.520Z","design_id":32997,"category":"designer-zoo-calm · forest-loden","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a dot-screen layer behind it and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:24:37.850Z","design_id":32996,"category":"designer-zoo-calm · midnight-navy","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a dot-screen layer behind it and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:24:40.384Z","design_id":32995,"category":"designer-zoo-calm · deep-plum","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a halftone layer and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:24:42.735Z","design_id":32994,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the pangolin motif behind the main motif, and a soft atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:24:51.696Z","design_id":32990,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the motifs behind the main layer, and an atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:24:53.805Z","design_id":32989,"category":"muybridge-plate · smoke-ash","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a faded duplicate behind it, a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:25:06.346Z","design_id":32986,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious ghosting effect and halftone layer behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:25:11.304Z","design_id":35873,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the fox motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:25:15.924Z","design_id":32981,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the wolf motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:25:31.965Z","design_id":32977,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The damask motif has a very obvious faded ghost/duplicate behind it, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:25:38.537Z","design_id":35861,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the crane motif behind the main motif, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:25:40.506Z","design_id":32973,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the bear motif and atmospheric haze behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:25:47.714Z","design_id":35857,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the floral motif and atmospheric haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:25:49.961Z","design_id":32969,"category":"designer-zoo-calm","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a halftone layer, atmospheric haze, and interior shading bleed, creating a double exposure look."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:25:58.040Z","design_id":32966,"category":"designer-scenic · bone-ivory","reason":"obvious bleed: halftone_layer, atmospheric_haze","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious halftone layer and atmospheric haze behind the motif, but no ghosting or interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:26:02.156Z","design_id":32964,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a faded duplicate, halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:26:11.013Z","design_id":32960,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The main motif has a faded ghost duplicate behind itself, and a halftone layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:26:15.537Z","design_id":32953,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the animal motifs behind the main motifs, creating a double exposure look with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:26:17.825Z","design_id":32896,"category":"designer-zoo-calm · manor-saddle","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The interior of the animal motifs have crosshatching and a halftone layer, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:26:20.957Z","design_id":32895,"category":"chinoiserie","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a dot-screen layer, atmospheric haze, and interior shading inside the motif, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:26:29.605Z","design_id":32891,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The main motif has a faded duplicate behind it, a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:26:32.776Z","design_id":32890,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the rose motif behind the main motif, and an atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:26:45.401Z","design_id":32885,"category":"cactus · antique-tan","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is crosshatching inside the cactus silhouettes, which should be flat."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:26:56.057Z","design_id":35848,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious ghosted duplicate of the cactus motifs behind the main motifs, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:27:00.901Z","design_id":35845,"category":"designer-scenic · smoke-ash","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious faded duplicate behind it, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:27:08.004Z","design_id":32879,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded ghost of the cactus motif behind the main motif, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:27:10.051Z","design_id":32878,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the main motif behind itself, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:27:13.364Z","design_id":35839,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the tree branches and birds behind the main motif, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:27:16.476Z","design_id":35838,"category":"designer-zoo-calm","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The wolf motif has a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:27:19.687Z","design_id":32874,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a faded duplicate behind it, atmospheric haze, and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:27:22.613Z","design_id":32873,"category":"throttled-plaster-roses · antique-tan","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image shows a clear faded duplicate of the motif, halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:27:24.852Z","design_id":32872,"category":"throttled-plaster-roses · smoke-ash","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a very obvious faded duplicate behind them, along with a halftone layer and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:27:26.945Z","design_id":32871,"category":"throttled-plaster-roses · champagne-linen","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious faded duplicate behind it, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:27:30.646Z","design_id":35831,"category":"designer-zoo-calm · slate-mist","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the bears and interior shading inside the bears."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:27:32.021Z","design_id":32869,"category":"designer-zoo-calm · greenhouse-glass","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a halftone layer behind it and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:27:33.486Z","design_id":35830,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the cactus motifs behind the main motifs, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:27:35.956Z","design_id":35829,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the bear motif behind the main motif, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:27:37.608Z","design_id":32867,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a very obvious faded duplicate behind them, with a hazy background and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:27:40.854Z","design_id":32866,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The main motif has a faded duplicate behind itself, a halftone layer, and atmospheric haze, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:27:54.539Z","design_id":32860,"category":"designer-scenic · midnight-navy","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious halftone layer and atmospheric haze behind the main motif, creating a ghosting effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:27:56.764Z","design_id":32859,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The pattern exhibits a clear faded duplicate of the main motif, along with a halftone layer and atmospheric haze, indicating significant motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:27:59.013Z","design_id":32858,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the floral motif and atmospheric haze behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:27:59.332Z","design_id":35821,"category":"designer-scenic","reason":"motif ghost layer (subtle)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"subtle","confidence":0.9,"one_line_reason":"There is a subtle faded duplicate of the branches and flowers behind the main motifs, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:28:01.327Z","design_id":32857,"category":"chinoiserie · bone-ivory","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"Halftone layer and interior shading inside the silhouette indicate motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:28:01.491Z","design_id":35820,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the tree motif behind the main motif, and an atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:28:03.702Z","design_id":35819,"category":"designer-zoo-calm · bone-ivory","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The foxes have interior shading and a halftone layer behind them, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:28:05.937Z","design_id":32855,"category":"chinoiserie · greenhouse-glass","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and interior shading inside the motif, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:28:08.286Z","design_id":32854,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the main motif behind itself, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:28:12.614Z","design_id":35815,"category":"cactus · manor-saddle","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The cactus motifs have a dot-screen layer behind them and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:28:14.749Z","design_id":35814,"category":"cactus · slate-mist","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The cactus motifs have a halftone layer and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:28:16.932Z","design_id":35812,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the fox motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:28:16.981Z","design_id":32849,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a very obvious ghosting effect, halftone layer, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:28:19.254Z","design_id":35811,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded ghost of the cactus motif behind the main motif, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:28:26.069Z","design_id":32845,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious ghosting effect and halftone layer behind the motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:28:37.025Z","design_id":32840,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the rose and branch motif behind the main motif, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:28:48.231Z","design_id":32836,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded ghost of the wolf motif behind the main motif, and atmospheric haze blurs the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:28:50.495Z","design_id":32835,"category":"chinoiserie · bordeaux-velvet","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the motif and interior shading inside the motif silhouette."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:28:53.344Z","design_id":32834,"category":"chinoiserie · manor-saddle","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a dot-screen layer behind it and crosshatching inside the silhouette."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:28:55.891Z","design_id":32833,"category":"chinoiserie · slate-mist","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the motif and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:29:07.655Z","design_id":32828,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate / echo of the main motif behind itself."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:29:12.311Z","design_id":32826,"category":"muybridge-plate · bone-ivory","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and interior shading inside the motif, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:29:16.598Z","design_id":32824,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a faded duplicate behind it, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:29:24.138Z","design_id":32821,"category":"muybridge-plate · indigo-library","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a clear, offset duplicate behind it, creating a 'double exposure' effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:29:26.455Z","design_id":32820,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious faded duplicate behind it, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:29:35.047Z","design_id":32816,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The cat motifs have a faded, offset duplicate behind them, creating a double exposure effect with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:29:43.618Z","design_id":32812,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious ghost motif behind the main fox motif, as well as interior shading and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:30:18.262Z","design_id":36163,"category":"chinoiserie · deep-plum","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a clear ghosted duplicate and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:30:20.497Z","design_id":36162,"category":"chinoiserie · saddle-mocha","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and interior shading bleed within the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:30:22.044Z","design_id":32798,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the main motif behind the main motif, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:30:22.777Z","design_id":36161,"category":"chinoiserie · antique-tan","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a clear faded duplicate and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:30:24.534Z","design_id":32797,"category":"chinoiserie · indigo-library","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious ghosted duplicate and interior shading, with a halftone layer behind."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:30:27.285Z","design_id":32796,"category":"chinoiserie · bordeaux-velvet","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious ghosted duplicate and halftone layer, as well as interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:30:30.169Z","design_id":32795,"category":"chinoiserie · manor-saddle","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The pattern has a dot-screen layer and interior shading inside the motif, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:30:34.868Z","design_id":36156,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious faded duplicate behind it, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:30:38.615Z","design_id":32791,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the cactus motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:30:40.879Z","design_id":32790,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The main motif is repeated as a faded ghost behind itself, and there is atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:30:46.005Z","design_id":36151,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the motifs behind the main motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:30:52.092Z","design_id":32786,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded ghost of the lion motif behind the main motif, and a sepia gradient behind the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:30:56.130Z","design_id":32784,"category":"designer-zoo-calm · indigo-library","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The bears have a dot-screen texture and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:30:58.220Z","design_id":32783,"category":"designer-zoo-calm · bordeaux-velvet","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The interior of the bear and trees have a halftone dot pattern, and the interior of the bear has shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:30:59.784Z","design_id":36145,"category":"throttled-plaster-roses · bone-ivory","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a dot-screen haze behind them and interior shading, creating a bleed effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:31:00.410Z","design_id":32782,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The bears have a faded duplicate behind them, a halftone layer, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:31:10.820Z","design_id":32777,"category":"designer-zoo-calm · bordeaux-velvet","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"Halftone layer and interior shading are visible within the motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:31:15.316Z","design_id":32775,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a clear double exposure look with a halftone layer and atmospheric haze, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:31:17.394Z","design_id":32774,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The main motif is repeated as a faded ghost behind itself, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:31:31.223Z","design_id":36131,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the main motif behind itself."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:31:35.525Z","design_id":32764,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the deer motif behind the main motif, and a halftone layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:31:37.953Z","design_id":36128,"category":"designer-zoo-calm · midnight-navy","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The animal motifs have a halftone texture and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:31:42.511Z","design_id":36126,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The main motif has a very obvious faded duplicate behind itself, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:31:49.902Z","design_id":32756,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a faded duplicate behind it, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:31:52.056Z","design_id":36122,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the cactus motifs behind the main motifs, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:31:52.188Z","design_id":32755,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the motif behind the main one, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:32:01.583Z","design_id":32750,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the rose motif behind the main motif, and an atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:32:07.578Z","design_id":36115,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the floral motif behind the main motif, and a soft atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:32:09.633Z","design_id":32745,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the fox motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:32:10.048Z","design_id":36114,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The wolf motifs have a very obvious faded duplicate behind them, as well as interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:32:25.207Z","design_id":32738,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The main motif is repeated as a faded ghost behind itself, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:32:27.426Z","design_id":32737,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the cactus motifs behind the main ones, as well as atmospheric haze and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:32:36.415Z","design_id":32732,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The cactus motifs have a clear faded duplicate behind them, a halftone layer, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:32:38.561Z","design_id":32731,"category":"throttled-plaster-roses · stone-pewter","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a dot-screen layer behind it and interior shading, creating a bleed effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:32:40.906Z","design_id":32730,"category":"throttled-plaster-roses · greenhouse-glass","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a halftone dot pattern and interior shading, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:32:43.154Z","design_id":32729,"category":"throttled-plaster-roses · indigo-library","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has interior shading and a halftone layer, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:32:45.378Z","design_id":32728,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious ghosted duplicate and interior shading, with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:33:00.646Z","design_id":32721,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious ghosting effect of the bamboo lattice behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:33:09.225Z","design_id":32717,"category":"muybridge-plate · forest-loden","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is crosshatching and shading inside the silhouettes that should be flat."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:33:11.601Z","design_id":32716,"category":"muybridge-plate","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and interior shading bleed present."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:33:13.577Z","design_id":32715,"category":"designer-zoo-calm · champagne-linen","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The deer motif has a very obvious ghosted duplicate and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:33:15.365Z","design_id":32714,"category":"designer-zoo-calm · bone-ivory","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The deer motif has a very obvious halftone ghost and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:33:17.334Z","design_id":32713,"category":"designer-zoo-calm · stone-pewter","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a clear faded duplicate of the deer motif behind the main motif, with interior shading and a halftone layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:33:19.502Z","design_id":32712,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The deer motif has a faded duplicate behind it and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:33:21.587Z","design_id":32711,"category":"designer-zoo-calm · indigo-library","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have a dot-screen layer behind them and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:33:34.175Z","design_id":32705,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the dinosaur motifs behind the main motifs, and a halftone layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:33:44.708Z","design_id":32700,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:33:48.873Z","design_id":32698,"category":"designer-zoo-calm · greenhouse-glass","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a halftone layer behind it and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:33:53.361Z","design_id":32696,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the motifs behind the main motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:34:19.593Z","design_id":32684,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the monkey motif behind the main motif, creating a double exposure look."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:34:27.947Z","design_id":32680,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The main motif has a faded duplicate behind itself, a halftone layer, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:34:30.048Z","design_id":32679,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the floral motif behind the main one, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:34:52.986Z","design_id":32668,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the motif and atmospheric haze behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:34:57.366Z","design_id":32664,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the damask motif behind the main motif, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:34:59.827Z","design_id":32663,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded ghost of the fox motif behind the main fox, and atmospheric haze blurs the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:35:04.730Z","design_id":36457,"category":"muybridge-plate · smoke-ash","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and interior shading inside the motif, creating a bleed effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:35:06.042Z","design_id":32599,"category":"chinoiserie · champagne-linen","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is crosshatching and shading inside the flower motifs that should be flat."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:35:12.509Z","design_id":32596,"category":"designer-zoo-calm · greenhouse-glass","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the motifs and interior shading inside the silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:35:17.621Z","design_id":36451,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious faded duplicate behind it, along with a halftone layer and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:35:19.257Z","design_id":32593,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious faded duplicate behind it, along with a halftone layer and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:35:21.468Z","design_id":32592,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious ghosting effect behind the animals and trees, and a halftone layer in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:35:26.274Z","design_id":36447,"category":"face-skull-damask · midnight-navy","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a clear faded duplicate behind it, along with a halftone layer and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:35:28.934Z","design_id":36446,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the motif behind the main motif, along with a haze layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:35:36.791Z","design_id":36442,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the cactus motifs behind the main motifs, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:35:45.332Z","design_id":36438,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the cactus motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:35:45.598Z","design_id":32578,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded ghost of the fox motif behind the main motif, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:35:53.624Z","design_id":36433,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the animal motifs behind the main motifs, creating a double exposure look with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:35:56.103Z","design_id":32572,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the motifs behind the main motifs, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:36:00.543Z","design_id":32570,"category":"designer-zoo-calm · oxblood","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and interior shading inside the motif, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:36:01.736Z","design_id":36429,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the floral motif behind the main motif, and a soft atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:36:16.465Z","design_id":36422,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the motif behind the main pattern, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:36:16.977Z","design_id":32561,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the motifs behind the main motifs, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:36:19.121Z","design_id":32560,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The cranes have a very obvious faded duplicate / echo behind them, and there is a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:36:21.450Z","design_id":32559,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the wolf motif behind the main motif, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:36:23.673Z","design_id":32558,"category":"designer-zoo-calm · slate-mist","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have a dot-screen layer behind them and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:36:25.919Z","design_id":32557,"category":"designer-zoo-calm · antique-brass","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have a dot-screen layer behind them and interior shading, creating a bleed effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:36:26.723Z","design_id":36417,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the animal motifs behind the main motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:36:30.675Z","design_id":32555,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The darker bears are repeated as a faded ghost behind the lighter bears, and there is a halftone dot pattern."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:36:48.921Z","design_id":36407,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded ghost of the trees and foxes behind the main motifs, and an atmospheric haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:36:51.009Z","design_id":32546,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The bears and trees have a faded, offset duplicate behind them, creating a double exposure effect with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:36:54.286Z","design_id":36405,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the damask motif behind the main motif, and an atmospheric haze effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:36:55.557Z","design_id":32544,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the cactus motif behind the main motif, and atmospheric haze is present."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:36:58.128Z","design_id":32543,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The wolf motifs have a very obvious faded duplicate behind them, along with a halftone layer and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:37:28.709Z","design_id":32531,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the main motif behind itself."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:37:31.416Z","design_id":32530,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the fox motif behind the main motif, and atmospheric haze is present."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:37:39.783Z","design_id":32526,"category":"muybridge-plate · smoke-ash","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image shows a clear faded duplicate of the main motif, halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:37:47.167Z","design_id":32523,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The cactus motifs have a faded duplicate behind them, a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:37:49.407Z","design_id":32522,"category":"muybridge-plate","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and interior shading inside the tiger motifs, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:37:51.614Z","design_id":32521,"category":"muybridge-plate · saddle-mocha","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have a dot-screen layer behind them and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:37:53.830Z","design_id":32520,"category":"muybridge-plate · antique-tan","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a dot-screen layer behind the rabbits, and interior shading inside the rabbit silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:37:56.630Z","design_id":32519,"category":"muybridge-plate · smoke-ash","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the motif and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:38:01.677Z","design_id":32517,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the rabbit motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:38:43.196Z","design_id":32505,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a clear double exposure look with a halftone layer and atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:38:45.714Z","design_id":32504,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the pangolin motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:38:50.571Z","design_id":32502,"category":"chinoiserie · saddle-mocha","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a halftone layer behind it and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:39:07.422Z","design_id":32493,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the motif behind the main one, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:39:20.746Z","design_id":32486,"category":"designer-zoo-calm · midnight-navy","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"Halftone layer and interior shading inside the motif silhouette indicate motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:39:25.351Z","design_id":32484,"category":"designer-zoo-calm · saddle-mocha","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"Halftone layer and interior shading are present, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:39:27.964Z","design_id":32483,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious ghost/duplicate of the cat motifs behind the main motifs, and a halftone layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:39:30.242Z","design_id":32482,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the motifs behind the main motifs, and interior shading is visible."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:39:48.129Z","design_id":32473,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the cactus motif behind the main motif, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:40:03.371Z","design_id":36755,"category":"muybridge-plate · manor-saddle","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious halftone layer behind it, creating a ghosting effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:40:03.998Z","design_id":32465,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the cactus motif behind the main motif, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:40:05.454Z","design_id":36754,"category":"muybridge-plate · slate-mist","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious halftone layer and atmospheric haze behind it, creating a 'double exposure' look."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:40:07.735Z","design_id":36753,"category":"muybridge-plate · antique-brass","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The motif has a very obvious halftone layer and atmospheric haze behind it, creating a ghosting effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:40:08.370Z","design_id":32463,"category":"designer-zoo-calm · smoke-ash","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The wolves have interior shading and a halftone layer behind them, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:40:09.770Z","design_id":36752,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the bird motifs behind the main motifs, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:40:12.882Z","design_id":32461,"category":"designer-zoo-calm · bone-ivory","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and interior shading inside the wolf motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:40:21.530Z","design_id":32456,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the wolf motifs behind the main motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:40:23.699Z","design_id":32455,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the cactus motifs behind the main motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:40:27.452Z","design_id":36744,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the animals behind the main motifs, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:40:28.004Z","design_id":32453,"category":"designer-zoo-calm · bordeaux-velvet","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has interior shading and a halftone layer behind it, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:40:34.569Z","design_id":32450,"category":"designer-zoo-calm","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The trees and wolves have a halftone layer behind them, and the wolves have interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:40:50.505Z","design_id":32443,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a clear faded duplicate of the motifs behind the main elements, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:40:55.471Z","design_id":32441,"category":"chinoiserie · oxblood","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious faded duplicate behind it, along with a halftone layer and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:40:58.347Z","design_id":36730,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the animal motifs behind the main motifs, creating a double exposure look with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:41:07.937Z","design_id":32435,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the motif behind the main motif, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:41:11.559Z","design_id":36723,"category":"designer-zoo-calm · midnight-navy","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the motifs and interior shading bleed inside the silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:41:16.986Z","design_id":36720,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the panda motifs behind the main motifs, and a halftone layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:41:19.503Z","design_id":32430,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded ghost of the animal motifs behind the main motifs, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:41:28.221Z","design_id":32426,"category":"designer-zoo-calm · bordeaux-velvet","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the motifs and interior shading bleed inside the silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:41:32.364Z","design_id":36713,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The animal motifs have a very obvious faded ghost / duplicate behind them, as well as interior shading and a halftone layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:41:39.064Z","design_id":32421,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The main motif has a faded duplicate behind it, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:41:43.326Z","design_id":32419,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the animal motif behind the main motif, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:41:43.474Z","design_id":36707,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded ghost of the fox motif behind the main motif, and atmospheric haze blurs the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:41:48.123Z","design_id":36704,"category":"designer-scenic · antique-tan","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious halftone layer and ghosting effect behind it."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:41:52.866Z","design_id":36702,"category":"designer-scenic","reason":"motif ghost layer (subtle)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"subtle","confidence":0.9,"one_line_reason":"There is a subtle ghosting effect and atmospheric haze behind the main floral motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:41:54.109Z","design_id":32414,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the cactus motif behind the main motif, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:42:10.418Z","design_id":36693,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the cactus motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:42:18.794Z","design_id":32403,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the deer motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:42:21.502Z","design_id":32402,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded ghost of the skull motif behind the main motif, and atmospheric haze blurs the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:42:25.581Z","design_id":32400,"category":"chinoiserie · deep-plum","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The cloud motifs have a very obvious halftone layer and atmospheric haze behind them, creating a double exposure look."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:42:27.631Z","design_id":32399,"category":"chinoiserie · saddle-mocha","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The clouds have a very obvious halftone layer and atmospheric haze behind them, creating a ghosting effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:42:29.843Z","design_id":32398,"category":"chinoiserie","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a very obvious halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:42:44.771Z","design_id":32390,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded ghost of the animal motifs behind the main motifs, and a haze effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:42:47.057Z","design_id":32389,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the roses behind the main motif, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:42:54.938Z","design_id":32325,"category":"throttled-plaster-roses · manor-saddle","reason":"obvious bleed: halftone_layer, atmospheric_haze","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The background has a very obvious halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:43:01.822Z","design_id":32322,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the rose motif behind the main motif, and an atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:43:10.447Z","design_id":32318,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the cactus motif behind the main motif, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:43:26.966Z","design_id":32313,"category":"chinoiserie · bordeaux-velvet","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a blurred haze layer, interior shading, and a halftone layer behind the motif, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:43:34.060Z","design_id":32310,"category":"chinoiserie","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a dot-screen effect, atmospheric haze, and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:44:08.288Z","design_id":32294,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a faded duplicate of the main motif, a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:44:17.275Z","design_id":32290,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a clear faded duplicate behind them, a halftone layer, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:44:25.637Z","design_id":32286,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have a very obvious faded ghost / duplicate behind them, and there is a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:44:35.775Z","design_id":32281,"category":"chinoiserie · stone-pewter","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The dragon motif has a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:44:40.145Z","design_id":32279,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the motif behind the main pattern, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:44:42.377Z","design_id":32278,"category":"chinoiserie","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a halftone layer, atmospheric haze, and interior shading bleed, creating a double exposure look."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:44:48.407Z","design_id":32275,"category":"throttled-plaster-roses · champagne-linen","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious halftone layer and atmospheric haze behind it, creating a ghosting effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:44:58.644Z","design_id":32270,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded ghost of the fox motif behind the main motif, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:45:15.519Z","design_id":37030,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a clear halftone layer and atmospheric haze behind the motifs, creating a double exposure look."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:45:17.692Z","design_id":37029,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious faded duplicate behind it, along with a halftone layer and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:45:30.438Z","design_id":32255,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the wolf motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:45:32.743Z","design_id":32254,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The motifs have a very obvious faded ghost / duplicate behind them, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:45:34.567Z","design_id":37021,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The cactus motifs have a faded, offset duplicate behind them, creating a double exposure look with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:45:41.206Z","design_id":37018,"category":"designer-zoo-calm · indigo-library","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The dinosaurs have a dot-screen texture and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:45:43.662Z","design_id":32249,"category":"throttled-plaster-roses · champagne-linen","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The roses have interior shading and a halftone layer behind them, creating a bleed effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:45:50.716Z","design_id":32246,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a faded duplicate behind them, a halftone layer, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:45:58.693Z","design_id":32242,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a clear halftone layer, atmospheric haze, interior shading, and a ghosted duplicate behind them."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:45:58.912Z","design_id":37014,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The main motif is repeated as a faded ghost behind itself with a halftone layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:46:07.780Z","design_id":37010,"category":"designer-scenic · antique-brass","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The background has a very obvious halftone layer and atmospheric haze, with a ghost of the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:46:09.617Z","design_id":37009,"category":"designer-scenic · oxblood","reason":"obvious bleed: halftone_layer, atmospheric_haze","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The background has a very obvious halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:46:16.929Z","design_id":32236,"category":"throttled-plaster-roses · antique-tan","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and interior shading bleed inside the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:46:19.316Z","design_id":32235,"category":"throttled-plaster-roses · smoke-ash","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The pattern has a halftone layer and interior shading bleed, creating a double exposure look."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:46:21.523Z","design_id":32234,"category":"throttled-plaster-roses · champagne-linen","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The pattern has a dot-screen layer and interior shading inside the motif, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:46:23.135Z","design_id":37003,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded ghost of the skull motif behind the main motif, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:46:24.233Z","design_id":32232,"category":"throttled-plaster-roses","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a blurred haze layer, interior shading, and a halftone layer behind the motif, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:46:26.650Z","design_id":32231,"category":"chinoiserie · antique-brass","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious faded duplicate behind it, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:46:27.651Z","design_id":37001,"category":"designer-zoo-calm · champagne-linen","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and interior shading inside the motif, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:46:33.966Z","design_id":32228,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious faded duplicate and halftone layer behind it, creating a double exposure look."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:46:46.852Z","design_id":36992,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the animal motifs behind the main motifs, creating a double exposure look with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:46:48.545Z","design_id":32221,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the fox motif behind the main motif, and a soft atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:46:50.849Z","design_id":32220,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the wolf motif behind the main motif, and a haze effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:46:55.730Z","design_id":36988,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the fox motif behind the main motif, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:47:12.846Z","design_id":36980,"category":"muybridge-plate · stone-pewter","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious faded duplicate behind it, a halftone layer, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:47:14.971Z","design_id":36979,"category":"muybridge-plate · greenhouse-glass","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The horses have a very obvious halftone layer behind them, creating a ghosting effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:47:17.109Z","design_id":36978,"category":"muybridge-plate · indigo-library","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The horses have a very obvious halftone layer and atmospheric haze behind them, creating a 'double exposure' look."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:47:19.416Z","design_id":36977,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the motifs behind the main motifs, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:47:29.516Z","design_id":32202,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded ghost of the fox motif behind the main motif, and a halftone layer in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:47:52.574Z","design_id":32191,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the fox motif and atmospheric haze behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:47:54.566Z","design_id":32190,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The camel motifs have a clear faded ghost behind them, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:47:59.066Z","design_id":32188,"category":"designer-scenic · smoke-ash","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a faded duplicate behind itself, a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:48:01.056Z","design_id":32187,"category":"designer-scenic · champagne-linen","reason":"obvious bleed: halftone_layer, atmospheric_haze","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and atmospheric haze behind the motif, creating a bleed effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:48:11.588Z","design_id":32182,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the motif behind the main pattern, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:48:19.666Z","design_id":32178,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The cranes have a faded duplicate behind them, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:48:21.870Z","design_id":32177,"category":"muybridge-plate · champagne-linen","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The dog motifs have a clear faded duplicate behind them, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:48:23.864Z","design_id":32176,"category":"muybridge-plate · bone-ivory","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious halftone ghost layer behind the dog motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:48:26.037Z","design_id":32175,"category":"muybridge-plate · stone-pewter","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious ghosted duplicate of the dog motifs, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:48:28.307Z","design_id":32174,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The dog motifs have a very obvious faded ghost / duplicate behind them, as well as a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:48:37.242Z","design_id":32170,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious ghosting effect and halftone layer behind the motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:48:46.482Z","design_id":32165,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the tree branches behind the main motif, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:48:48.555Z","design_id":32164,"category":"cactus · antique-tan","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The cactus motifs have a dot-screen layer behind them and crosshatching inside the silhouette."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:48:53.304Z","design_id":32162,"category":"cactus · champagne-linen","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The cactus motifs have interior shading and a halftone layer behind them, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:48:55.723Z","design_id":32161,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The cactus motifs have a faded, offset duplicate behind them, creating a double exposure effect with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:48:57.813Z","design_id":32160,"category":"chinoiserie · forest-loden","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The motifs have a dot-screen layer behind them and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:49:02.124Z","design_id":32158,"category":"chinoiserie · deep-plum","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have a dot-screen layer behind them and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:49:04.509Z","design_id":32157,"category":"chinoiserie","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have a dot-screen layer behind them, atmospheric haze, and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:49:06.666Z","design_id":32156,"category":"designer-zoo-calm · manor-saddle","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and interior shading inside the fox motifs, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:49:13.173Z","design_id":32153,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the fox motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:49:21.474Z","design_id":32149,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the rose motif behind the main motif, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:49:30.277Z","design_id":32145,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the motif behind the main figures, and a general atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:49:38.877Z","design_id":32141,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the main motif behind itself, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:49:47.817Z","design_id":32137,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The damask motif has a faded, blurred duplicate behind it, creating a double exposure effect with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:49:56.011Z","design_id":32133,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a faded duplicate behind it, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:50:02.905Z","design_id":37323,"category":"face-skull-damask · smoke-ash","reason":"obvious bleed: halftone_layer, atmospheric_haze","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and atmospheric haze behind the skull motif, creating a bleed effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:50:04.283Z","design_id":32129,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the lion motif behind the main motif, along with a halftone layer and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:50:05.207Z","design_id":37322,"category":"face-skull-damask · champagne-linen","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The skull motif has a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:50:18.684Z","design_id":37316,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The cranes have a faded duplicate behind them, a halftone layer, and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:50:21.560Z","design_id":32121,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the cactus motifs behind the main motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:50:29.761Z","design_id":32117,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the bear motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:50:32.784Z","design_id":32116,"category":"monterey-mural","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and interior shading inside the lighthouse motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:50:34.338Z","design_id":37309,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the lion motif behind the main motif, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:50:35.076Z","design_id":32115,"category":"muybridge-plate · greenhouse-glass","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a clear double exposure effect with a halftone layer and interior shading within the motif silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:50:36.558Z","design_id":37308,"category":"throttled-plaster-roses","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the roses and interior shading bleed inside the rose silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:50:39.986Z","design_id":32113,"category":"muybridge-plate · bordeaux-velvet","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the motifs and interior shading bleed inside the silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:50:42.699Z","design_id":32112,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded ghost of the motif behind the main motif, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:50:47.635Z","design_id":37304,"category":"designer-zoo-calm · antique-brass","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a halftone layer and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:50:50.978Z","design_id":32108,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the crane motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:50:51.838Z","design_id":37302,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the main motif behind itself, and a halftone layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:50:54.061Z","design_id":37301,"category":"designer-zoo-calm · forest-loden","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a dot-screen layer behind it and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:50:56.406Z","design_id":37300,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The dinosaurs have a faded duplicate behind them, a halftone layer, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:51:13.992Z","design_id":37292,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the rose motif behind the main motif, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:51:16.316Z","design_id":32096,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the motif behind the main pattern, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:51:20.690Z","design_id":32029,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The wallpaper has a very obvious faded duplicate of the main motif behind itself, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:51:22.648Z","design_id":37288,"category":"chinoiserie","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The floral motifs have a clear faded duplicate behind them, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:51:28.422Z","design_id":32025,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The main motif has a very obvious faded duplicate behind itself, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:51:31.028Z","design_id":37284,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The main motif has a faded duplicate behind it, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:51:39.262Z","design_id":37279,"category":"muybridge-plate · deep-plum","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image shows a clear faded duplicate of the motif, halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:51:41.514Z","design_id":37278,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the motif behind the main design, creating a double exposure look with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:51:43.822Z","design_id":37277,"category":"muybridge-plate · saddle-mocha","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and interior shading inside the motif, indicating bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:51:47.470Z","design_id":32016,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious ghosting effect and halftone layer behind the running figures."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:51:48.185Z","design_id":37275,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded ghost of the motif behind the main image, and a soft haze behind the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:51:50.193Z","design_id":37274,"category":"designer-zoo-calm · greenhouse-glass","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious ghosting effect behind the panda motifs, and a halftone layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:51:51.216Z","design_id":32014,"category":"throttled-plaster-roses · champagne-linen","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and interior shading bleed inside the rose motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:51:54.619Z","design_id":37272,"category":"designer-zoo-calm · bordeaux-velvet","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motif has a very obvious faded duplicate behind itself, a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:51:55.864Z","design_id":32012,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the rose motif behind the main motif, and a soft haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:51:57.269Z","design_id":37271,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The pandas have a very obvious faded duplicate behind them, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:52:04.109Z","design_id":32008,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The main motif has a faded ghost duplicate behind itself, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:52:05.484Z","design_id":37267,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a clear faded duplicate of the rose motif behind the main motif, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:52:14.270Z","design_id":37263,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the animal motifs behind the main motifs, and a halftone layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:52:30.421Z","design_id":31996,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded ghost/duplicate of the deer motif behind the main motif, and atmospheric haze blurs the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:52:33.121Z","design_id":31995,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the tree branches behind the main motif, and atmospheric haze is present."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:52:52.027Z","design_id":31987,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the deer motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:52:56.418Z","design_id":31985,"category":"face-skull-damask · midnight-navy","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer and interior shading inside the skull motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:53:00.818Z","design_id":31983,"category":"face-skull-damask","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is crosshatching and shading inside the skull and damask silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:53:09.457Z","design_id":31979,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the cactus motifs behind the main motifs, as well as interior shading and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:53:26.223Z","design_id":31972,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have a very obvious faded duplicate behind them, as well as interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:53:28.845Z","design_id":31971,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The main motif has a faded ghost duplicate behind itself, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:53:56.434Z","design_id":31959,"category":"designer-zoo-calm · bordeaux-velvet","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a clear halftone layer and ghosting effect behind the animal motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:54:04.667Z","design_id":31955,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded ghost of the animal motifs behind the main motifs, creating a double exposure look with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:54:07.041Z","design_id":31954,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious ghosted duplicate of the deer motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:54:09.193Z","design_id":31952,"category":"muybridge-plate · stone-pewter","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The horse motifs have a clear faded duplicate behind them, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:54:13.230Z","design_id":31950,"category":"muybridge-plate · indigo-library","reason":"motif ghost layer (subtle)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"subtle","confidence":0.9,"one_line_reason":"There is a subtle ghosting effect behind the horse motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:54:21.772Z","design_id":31946,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The horse motifs have a clear faded duplicate behind them, a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:54:24.124Z","design_id":31945,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious ghost/duplicate of the bear motifs behind the main motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:54:32.632Z","design_id":31941,"category":"muybridge-plate · manor-saddle","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image shows a clear faded duplicate of the motif, halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:54:35.162Z","design_id":31940,"category":"muybridge-plate · slate-mist","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.9,"one_line_reason":"There is a halftone layer and interior shading bleed within the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:54:39.533Z","design_id":31938,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have a faded, offset duplicate behind them, creating a double exposure effect with atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:54:41.702Z","design_id":31937,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the motif, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:55:06.727Z","design_id":31923,"category":"chinoiserie","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a dot-screen layer, atmospheric haze, and interior shading inside the motifs, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:55:17.464Z","design_id":37605,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the pangolin motif and trees behind the main motifs, and an atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:55:17.998Z","design_id":31917,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious ghosting effect behind the trees and lemurs, with a halftone layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:55:20.171Z","design_id":31916,"category":"throttled-plaster-roses · antique-brass","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a dot-screen haze behind them and interior shading, creating a bleed effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:55:22.532Z","design_id":31915,"category":"throttled-plaster-roses · oxblood","reason":"obvious bleed: halftone_layer, atmospheric_haze","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The background has a very obvious halftone layer and atmospheric haze, but no motif ghosting or interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:55:25.127Z","design_id":31914,"category":"throttled-plaster-roses · forest-loden","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The roses have a dot-screen haze behind them, and crosshatching inside the rose silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:55:26.104Z","design_id":37601,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the motifs behind the main motifs, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:55:27.620Z","design_id":31913,"category":"throttled-plaster-roses","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the rose motif behind the main motif, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:55:36.708Z","design_id":31908,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the cactus motif behind the main motif, and interior shading is visible."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:55:44.162Z","design_id":37593,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the wolf motif behind the main motif, and a halftone layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:55:47.579Z","design_id":31903,"category":"designer-zoo-calm · antique-brass","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have interior crosshatching and a halftone layer behind them."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:55:49.663Z","design_id":31902,"category":"designer-zoo-calm · oxblood","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have interior crosshatching and a halftone layer behind them."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:55:51.816Z","design_id":31901,"category":"designer-zoo-calm · forest-loden","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have interior shading and a halftone layer behind them, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:55:54.066Z","design_id":31900,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a clear double exposure look with a halftone layer and atmospheric haze, and interior shading inside the animal silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:56:00.752Z","design_id":37585,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the cactus motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:56:02.745Z","design_id":31895,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The image has a clear double exposure look with a halftone layer and atmospheric haze, and interior shading inside the motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:56:10.847Z","design_id":31891,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The cranes have a faded duplicate behind them, a halftone layer, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:56:17.881Z","design_id":37577,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the main motif behind itself."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:56:27.490Z","design_id":31883,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the animal motifs behind the main motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:56:35.730Z","design_id":31879,"category":"face-skull-damask","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the motif behind the main one, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:56:37.047Z","design_id":37569,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the cactus motifs behind the main motifs, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:56:43.916Z","design_id":31875,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The cactus motifs have a faded ghost/duplicate behind them, along with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:56:45.380Z","design_id":37565,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the cactus motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:56:47.471Z","design_id":37564,"category":"designer-zoo-calm · bordeaux-velvet","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the motifs and interior shading bleed inside the silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:56:49.516Z","design_id":37563,"category":"designer-zoo-calm · manor-saddle","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The motifs have a dot-screen layer behind them and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:56:53.909Z","design_id":37561,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a faded duplicate of the animal motifs behind the main motifs, and an atmospheric haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:57:00.243Z","design_id":37558,"category":"designer-zoo-calm · indigo-library","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a dot-screen layer behind the motifs, and interior shading inside the silhouettes."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:57:00.951Z","design_id":31867,"category":"designer-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a clear faded duplicate of the trees and flowers behind the main motif, creating a double exposure effect and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:57:02.687Z","design_id":37557,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"Faded duplicate of the animal motifs and atmospheric haze are present behind the main motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:57:11.330Z","design_id":37553,"category":"chinoiserie","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a halftone layer, atmospheric haze, and interior shading bleed, creating a double exposure look."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:57:19.190Z","design_id":31861,"category":"designer-zoo-calm · antique-brass","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The interior of the motifs have a dot-screen / halftone pattern, and the interior shading is visible."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:57:23.503Z","design_id":31859,"category":"stripe","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a clear faded duplicate of the stripe motif behind the main stripes, creating an atmospheric haze effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:57:25.458Z","design_id":31858,"category":"chinoiserie · bordeaux-velvet","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:57:32.292Z","design_id":31855,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded duplicate of the dinosaur motifs behind the main motifs, and a halftone layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:57:34.562Z","design_id":31854,"category":"chinoiserie","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a blurred haze layer, interior shading, and a halftone layer behind the motif, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:57:41.069Z","design_id":31851,"category":"designer-zoo-calm · stone-pewter","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The deer motifs have a clear faded duplicate behind them, a halftone layer, atmospheric haze, and interior shading bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:57:55.838Z","design_id":31846,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a faded duplicate of the deer motif behind the main motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:58:06.614Z","design_id":31841,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a very obvious faded ghost of the main motif behind the main motif, with a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:58:50.963Z","design_id":31837,"category":"cactus","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious ghosted duplicate of the cactus motifs behind the main motifs, and a soft haze effect."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:59:45.211Z","design_id":31752,"category":"designer-zoo-calm · antique-brass","reason":"obvious bleed: halftone_layer, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer behind the motif and interior shading bleed inside the silhouette."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T18:59:49.354Z","design_id":31750,"category":"designer-zoo-calm","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":false,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious faded duplicate of the bird motif behind the main motif, and a halftone layer."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-20T19:00:54.791Z","design_id":31743,"category":"muybridge-plate","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The lion motifs have a clear faded duplicate behind them, a halftone layer, atmospheric haze, and interior shading, indicating obvious motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-22T01:20:04.811Z","design_id":39057,"category":"cactus-pine-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious ghosting effect of the desert landscape behind the main image."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-22T01:20:07.705Z","design_id":39056,"category":"cactus-pine-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":1,"one_line_reason":"The image has a double exposure look with atmospheric haze and a ghosted motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-22T01:20:15.214Z","design_id":39053,"category":"cactus-pine-scenic","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is a blurred haze layer, interior shading, and atmospheric haze behind the motifs."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-22T01:30:04.179Z","design_id":39062,"category":"cactus-pine-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a very obvious ghosting effect behind the cacti, and atmospheric haze throughout."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-22T02:25:06.748Z","design_id":39108,"category":"cactus-11ft-mural","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"There is a halftone layer, atmospheric haze, and interior shading bleed inside the cactus motif."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-22T02:25:21.557Z","design_id":39102,"category":"cactus-pine-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":false,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The cacti and trees have a faded ghost image behind them, and there is atmospheric haze in the background."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-22T02:40:14.350Z","design_id":39120,"category":"cactus-pine-scenic","reason":"obvious bleed: halftone_layer, atmospheric_haze, interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The image has a dot-screen layer, atmospheric haze, and interior shading, indicating motif bleed."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-22T02:45:18.937Z","design_id":39158,"category":"designer-flat-lips","reason":"obvious bleed: interior_shading_bleed","verdict":{"has_motif_ghost":false,"has_halftone_layer":false,"has_atmospheric_haze":false,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":1,"one_line_reason":"There is interior shading inside the motif silhouette that should be flat."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-22T02:45:31.360Z","design_id":39140,"category":"cactus-pine-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":true,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The cactus motif has a faded duplicate behind it, a halftone layer, atmospheric haze, and interior shading."},"source":"bleed_guard.ghost-layer"}
+{"ts":"2026-05-22T02:45:41.949Z","design_id":39131,"category":"cactus-pine-scenic","reason":"motif ghost layer (obvious)","verdict":{"has_motif_ghost":true,"has_halftone_layer":true,"has_atmospheric_haze":true,"has_interior_shading_bleed":false,"bleed_severity":"obvious","confidence":0.95,"one_line_reason":"The cactus motifs have a very obvious faded ghost behind them, and there is a halftone layer and atmospheric haze."},"source":"bleed_guard.ghost-layer"}
diff --git a/data/inspired-catalog/refs/saguaro-install.jpg b/data/inspired-catalog/refs/saguaro-install.jpg
new file mode 100644
index 0000000..a49c2e4
Binary files /dev/null and b/data/inspired-catalog/refs/saguaro-install.jpg differ
diff --git a/data/inspired-catalog/refs/saguaro-tile.jpg b/data/inspired-catalog/refs/saguaro-tile.jpg
new file mode 100644
index 0000000..e342c50
Binary files /dev/null and b/data/inspired-catalog/refs/saguaro-tile.jpg differ
diff --git a/data/inspired-catalog/spec.json b/data/inspired-catalog/spec.json
new file mode 100644
index 0000000..35e8975
--- /dev/null
+++ b/data/inspired-catalog/spec.json
@@ -0,0 +1,62 @@
+{
+  "_note": "DW-house-style reinterpretations of widely-seen oversized-motif wallcoverings. Source archive enumerated for our internal planning only — NEVER write the source brand name into any rendered output (SKU, title, prompt, category, file path, admin note, customer-facing copy). Memory rule: feedback_never_show_fliepaper_word_on_images.",
+  "version": 1,
+  "generated_at": "2026-05-20",
+  "house_anchor": "Zuber + Designers Guild + Thibaut + Arte + Romo + Maya Romanoff house style — oversized single-dominant-motif on solid ground, refined screen-print clarity, hand-painted gouache reference, hard razor edges, NO ghost layer, NO chiaroscuro bleed.",
+  "default_scale": "oversized — single dominant motif at 36-54 inch tile, occupies 35-60% of tile area on solid ground",
+  "colorway_count_per_motif": 6,
+  "quantize_target_colors": 6,
+  "ghost_luma_gap": 35,
+
+  "colorways": [
+    { "slug": "bone-ink-classic",  "ground": "#FFFFFF", "primary": "#0E0E0E", "accent1": "#1F1F1F", "accent2": "#3D3D3D", "shadow": "#000000", "highlight": "#A0A0A0" },
+    { "slug": "saddle-mocha",      "ground": "#F2EADB", "primary": "#4A3528", "accent1": "#7A5638", "accent2": "#9C7B5C", "shadow": "#1A0F08", "highlight": "#D9C2A0" },
+    { "slug": "forest-loden",      "ground": "#E8DDC8", "primary": "#1F3D2E", "accent1": "#3D5C42", "accent2": "#6A8B6F", "shadow": "#0A1A12", "highlight": "#B5C49E" },
+    { "slug": "indigo-library",    "ground": "#E8DCC8", "primary": "#1E2740", "accent1": "#3D4A6B", "accent2": "#6B7898", "shadow": "#0A0F1C", "highlight": "#B0BACF" },
+    { "slug": "bordeaux-velvet",   "ground": "#F2EADB", "primary": "#5C2329", "accent1": "#8A3A40", "accent2": "#B05A60", "shadow": "#28090C", "highlight": "#D9A8A8" },
+    { "slug": "stone-pewter",      "ground": "#EFE9DE", "primary": "#3D3833", "accent1": "#6B6660", "accent2": "#9A958F", "shadow": "#1A1814", "highlight": "#C5C0BA" }
+  ],
+
+  "motifs": [
+    { "slug": "fern-frond",            "category": "botanical-foliage",      "subject": "single oversized fern frond — long arching central stem with 30-40 paired leaflets, drawn as flat silhouette stencil, one frond fills 50% of tile diagonal, gentle curve from lower-left to upper-right", "scale": "single dominant frond per 54-inch tile" },
+    { "slug": "eucalyptus-branch",     "category": "botanical-foliage",      "subject": "single eucalyptus branch — long stem with paired rounded silver-dollar leaves, 12-16 leaf pairs cascading downward, flat solid color silhouette on solid ground", "scale": "single branch per 54-inch tile" },
+    { "slug": "tall-grass-reeds",      "category": "botanical-foliage",      "subject": "vertical tall-grass reed stalks — 6-8 thin vertical stalks rising floor to ceiling, slight asymmetric placement, each stalk crisp single-color silhouette with hairline definition, evokes a quiet marsh", "scale": "full-height reeds, repeat horizontally" },
+    { "slug": "banana-palm-leaf",      "category": "botanical-foliage",      "subject": "single oversized banana-palm leaf — long oval frond with deep parallel veining, leaf occupies most of the tile, flat color blocking with crisp ribs", "scale": "one massive leaf per 54-inch tile" },
+    { "slug": "fan-palm-frond",        "category": "botanical-foliage",      "subject": "single fan-palm frond — radiating segmented blades from a central point, full open fan covers 60% of tile, geometric fan rays in flat solid color", "scale": "one fan per 54-inch tile" },
+    { "slug": "pine-bough",            "category": "botanical-foliage",      "subject": "single conifer pine bough — main branch with 8-12 paired needle clusters cascading, single-color silhouette on solid ground, sharp needle definition", "scale": "one bough per 54-inch tile" },
+
+    { "slug": "single-magnolia-bloom", "category": "floral-bloom",           "subject": "one giant magnolia bloom centered on tile — 6-petal open flower from above, soft layered petals with crisp outer edges, single deep stem and 2 large leaves curling down, occupies 65% of tile area", "scale": "single bloom dominant" },
+    { "slug": "single-rose-bloom",     "category": "floral-bloom",           "subject": "one giant English rose bloom — overhead view, 24 nested layered petals spiraling from a tight center, single bloom centered, slight 3-quarter offset, occupies 60% of tile", "scale": "single bloom dominant" },
+    { "slug": "single-peony-bloom",    "category": "floral-bloom",           "subject": "one giant peony bloom — overhead view of fully-open peony, ruffled tissue-paper petals layered loosely, 4-5 visible color rings inside, surrounded by 2 dark green leaves", "scale": "single bloom dominant" },
+    { "slug": "single-hibiscus-bloom", "category": "floral-bloom",           "subject": "one giant hibiscus flower — 5 broad open petals with prominent central stamen pistil cluster, slightly off-center, one curling leaf below", "scale": "single bloom dominant" },
+    { "slug": "single-gerbera-daisy",  "category": "floral-bloom",           "subject": "one giant gerbera daisy — perfect circle of 25-30 radiating petals around a dense disc center, flat 6-color screen-print, occupies 60% of tile", "scale": "single bloom dominant" },
+    { "slug": "single-orchid-spray",   "category": "floral-bloom",           "subject": "one cascading orchid spray — vertical stalk with 6-8 dangling orchid blossoms, each blossom 3-petal with central labellum, drawn as elegant overlay on solid ground", "scale": "single spray dominant" },
+    { "slug": "single-poppy-bloom",    "category": "floral-bloom",           "subject": "one giant poppy bloom — 4 broad crumpled petals with deep dark center disk, slight wind-blown tilt, single long stem curving down to lower edge of tile", "scale": "single bloom dominant" },
+
+    { "slug": "saguaro-cactus",        "category": "desert-flora",           "subject": "DENSE VERTICAL FOREST of 8-12 saguaro cacti standing side-by-side packed across the full width of the tile, varying heights from 70% to 100% of tile height (some go beyond the top edge, some cropped at bottom), each cactus has a central thick trunk with 1-3 raised arms (asymmetric arrangement — left-pointing arms, right-pointing arms, double-arm crown, single-arm cripple), PRONOUNCED VERTICAL PLEATED RIBS running top-to-bottom on every trunk and arm (8-14 deep ribs per trunk), fine vertical spine-texture along each rib (preserve the photo-realistic ribbed silhouette character), bases all cropped at lower edge of tile (only trunks + arms visible), slight overlap between adjacent cacti for sense of depth, NO empty patches in the cacti band (they fill horizontally edge-to-edge), pure flat ground color above the cacti tops and below (small ground band at bottom and significant ground band at top). Each cactus rendered as a HIGH-DETAIL PHOTOGRAPHIC SILHOUETTE in primary ink color on solid ground — preserve the textured ribbed surface character of the silhouette without losing the flat-color principle. The arms create complex negative-space sky patches between cacti. Reference: a stand of mature wild Sonoran saguaros viewed from low angle.", "scale": "full-height multi-saguaro forest band, 8-12 cacti per tile" },
+    { "slug": "joshua-tree",           "category": "desert-flora",           "subject": "single Joshua tree silhouette — twisted gnarled trunk with 4-6 splaying spike-cluster arms, asymmetric organic shape, flat single-color silhouette", "scale": "full-height tree" },
+    { "slug": "palo-verde-branch",     "category": "desert-flora",           "subject": "single palo verde branch — slender forking branches with sparse delicate compound leaflets, airy negative space, single-color silhouette", "scale": "single branch dominant" },
+    { "slug": "agave-rosette",         "category": "desert-flora",           "subject": "single agave rosette from overhead — 20-30 thick radiating spike-leaves from central core, geometric starburst feel, flat color blocking", "scale": "single rosette dominant" },
+
+    { "slug": "single-butterfly",      "category": "insect-archive",         "subject": "single giant butterfly — wings open spread from above, 4 distinct wing panels with hairline boundary, body and antennae detailed, decorative spot patterns inside wings", "scale": "single butterfly dominant" },
+    { "slug": "single-dragonfly",      "category": "insect-archive",         "subject": "single giant dragonfly — long thin body with 4 elongated translucent wings spread wide, segmented body, hairline wing veining shown as crisp stencil lines", "scale": "single dragonfly dominant" },
+    { "slug": "single-beetle",         "category": "insect-archive",         "subject": "single giant scarab beetle from overhead — round oval shell body with 2 visible wing-cases meeting in center line, 6 articulated legs in symmetry, antennae forward", "scale": "single beetle dominant" },
+    { "slug": "scattered-flies",       "category": "insect-archive",         "subject": "12-15 small fly silhouettes scattered randomly across tile — each fly drawn as a flat solid-color silhouette in profile or 3-quarter view, varied poses, dense but balanced negative space", "scale": "scattered" },
+
+    { "slug": "peacock-display",       "category": "bird-archive",           "subject": "single peacock in tail-display — front view of male peacock with full fan-tail spread behind, individual eye-feathers patterned as decorative medallions, slim body centered", "scale": "single peacock dominant" },
+    { "slug": "flamingo-pair",         "category": "bird-archive",           "subject": "two stylized flamingos facing each other — long curved necks, balanced 1-leg stance, single-color silhouettes against ground, elegant negative space between them", "scale": "pair dominant" },
+
+    { "slug": "breeze-block-square",   "category": "breeze-block-geometric", "subject": "mid-century breeze-block geometric pattern — repeating square modules each containing a 4-fold radial cutout (lattice / X / star / sunburst variations), regular grid tessellation", "scale": "8-12 modules per tile" },
+    { "slug": "breeze-block-circular", "category": "breeze-block-geometric", "subject": "mid-century breeze-block circular pattern — repeating square modules each containing a circular sunburst or daisy cutout, regular grid", "scale": "8-12 modules per tile" },
+    { "slug": "breeze-block-hex",      "category": "breeze-block-geometric", "subject": "mid-century breeze-block hexagonal pattern — repeating hex tiles each with concentric ring or lattice cutout, honeycomb-style grid", "scale": "12-16 hexes per tile" },
+
+    { "slug": "quartz-crystal-cluster","category": "crystal-gem",            "subject": "single quartz crystal cluster — vertical hexagonal prism shapes with pyramidal terminations, 6-8 crystals rising from a base in slight fan-spread, flat-faceted color blocking", "scale": "single cluster dominant" },
+    { "slug": "topaz-faceted-gem",     "category": "crystal-gem",            "subject": "single faceted gemstone — large oval brilliant cut shown from above, all facets drawn as flat solid-color polygons meeting at sharp edges, central table reflection", "scale": "single gem dominant" },
+
+    { "slug": "spiral-nautilus-shell", "category": "aquatic",                "subject": "single nautilus shell cross-section — perfect logarithmic spiral with 8-9 interior chambers, drawn as concentric spiral cells in flat color blocking", "scale": "single shell dominant" },
+    { "slug": "coral-branch",          "category": "aquatic",                "subject": "single sea-coral branch silhouette — gnarled branching forms with fine fingertip projections, flat single-color silhouette on solid ground, intricate negative space within branch structure", "scale": "single branch dominant" },
+
+    { "slug": "starburst-medallion",   "category": "geometric-star",         "subject": "single radiating starburst — 24 thin needle-rays emanating from a central disc, perfect rotational symmetry, alternating long/short rays, flat color screen-print", "scale": "single starburst per tile" },
+    { "slug": "wagon-wheel-rosette",   "category": "geometric-star",         "subject": "single wagon-wheel rosette — circular outer rim with 12 spokes radiating to a central hub, geometric perfection, flat color blocking", "scale": "single wheel dominant" }
+  ]
+}
diff --git a/scripts/drunk_monkeys_birds_subdued_batch.js b/scripts/drunk_monkeys_birds_subdued_batch.js
new file mode 100644
index 0000000..06ffafa
--- /dev/null
+++ b/scripts/drunk_monkeys_birds_subdued_batch.js
@@ -0,0 +1,72 @@
+#!/usr/bin/env node
+/**
+ * Fire N drunk-monkey/bird/botanical-subdued designs through generate_designs.js.
+ * Uses the focused prompt builder at drunk_monkeys_birds_subdued_prompts.js.
+ * Settlement-gate filtered, errors-tolerant (single bad design ≠ batch kill).
+ *
+ * Usage:  node scripts/drunk_monkeys_birds_subdued_batch.js [N]   (default 100)
+ *
+ * Output rows land in dw_unified.spoon_all_designs with
+ *   kind='seamless_tile', category='drunk-monkeys-birds-subdued'
+ * so this batch is separable from the existing 2,687 'drunk-animals' rows.
+ */
+const { spawnSync } = require('child_process');
+const path = require('path');
+const fs = require('fs');
+const { buildN } = require('./drunk_monkeys_birds_subdued_prompts');
+const { checkPrompt } = require('./settlement-gate');
+
+const N = parseInt(process.argv[2] || '100', 10);
+const ROOT = path.join(__dirname, '..');
+const CATEGORY = 'drunk-monkeys-birds-subdued';
+const LOG = `/tmp/dmbs-batch-${new Date().toISOString().replace(/[:.]/g, '-')}.log`;
+const t0 = Date.now();
+
+function L(msg) {
+  const line = `[${new Date().toISOString()}]  ${msg}`;
+  console.log(line);
+  fs.appendFileSync(LOG, line + '\n');
+}
+
+L(`=== drunk-monkeys-birds-subdued batch: firing ${N} designs ===`);
+L(`log file: ${LOG}`);
+L(`category: ${CATEGORY}`);
+
+const items = buildN(N);
+let ok = 0, fail = 0, skipped = 0;
+
+for (let i = 0; i < items.length; i++) {
+  const item = items[i];
+  const gate = checkPrompt(item.prompt);
+  if (gate.verdict !== 'OK') {
+    L(`[${i+1}/${N}] SKIP (settlement ${gate.verdict}) — ${(gate.reason||'').slice(0,100)}`);
+    skipped++;
+    continue;
+  }
+  L(`[${i+1}/${N}] palette=${item.palette}  animal=${item.animal_summary}`);
+  const r = spawnSync('node', [
+    path.join(ROOT, 'scripts', 'generate_designs.js'),
+    '--n', '1',
+    '--kind', 'seamless_tile',
+    '--category', CATEGORY,
+    '--prompts', item.prompt,
+  ], { cwd: ROOT, encoding: 'utf8', timeout: 240_000 });
+  if (r.status === 0) {
+    ok++;
+    L(`  ✓ ok  (running: ${ok} ok, ${fail} fail, ${skipped} skip — ${((Date.now()-t0)/60000).toFixed(1)}min elapsed)`);
+  } else {
+    fail++;
+    L(`  ✗ FAIL  status=${r.status}  stderr=${(r.stderr||'').slice(0,140)}`);
+  }
+}
+
+L(`=== batch done: ${ok} ok / ${fail} fail / ${skipped} skipped  (${((Date.now()-t0)/60000).toFixed(1)} min) ===`);
+
+// Refresh designs.json + tell wallco-ai server to reload its in-memory grid
+L('refreshing designs.json snapshot…');
+spawnSync('python3', [path.join(ROOT, 'scripts', 'refresh_designs_snapshot.py')],
+          { stdio: 'inherit', cwd: ROOT, timeout: 180_000 });
+L('hot-reloading wallco-ai server (local + remote if reachable)…');
+spawnSync('curl', ['-sf', '-m', '5', '-X', 'POST', 'http://127.0.0.1:9792/admin/reload-designs'], { stdio: 'inherit', timeout: 10_000 });
+
+process.exit(fail === 0 ? 0 : (ok > 0 ? 0 : 1));
diff --git a/scripts/drunk_monkeys_birds_subdued_parallel.js b/scripts/drunk_monkeys_birds_subdued_parallel.js
new file mode 100644
index 0000000..3442a3c
--- /dev/null
+++ b/scripts/drunk_monkeys_birds_subdued_parallel.js
@@ -0,0 +1,101 @@
+#!/usr/bin/env node
+/**
+ * Max-It parallel runner — fires N drunk-monkey/bird/botanical-subdued designs
+ * through generate_designs.js with K concurrent workers (default 8).
+ *
+ * Differs from drunk_monkeys_birds_subdued_batch.js (its serial sibling):
+ *   - uses child_process.spawn (async, not spawnSync)
+ *   - per-design timeout 600s (was 240s — first 99-batch saw 82 timeouts at 240s)
+ *   - bounded concurrency via a simple promise pool
+ *   - prints heartbeat every ~30s with ok/fail/in-flight counts
+ *
+ * Usage:  node scripts/drunk_monkeys_birds_subdued_parallel.js [N] [CONCURRENCY]
+ *           defaults: N=82, CONCURRENCY=8
+ */
+const { spawn } = require('child_process');
+const path = require('path');
+const fs = require('fs');
+const { buildN } = require('./drunk_monkeys_birds_subdued_prompts');
+const { checkPrompt } = require('./settlement-gate');
+
+const N      = parseInt(process.argv[2] || '82', 10);
+const PARLEL = parseInt(process.argv[3] || '8', 10);
+const ROOT     = path.join(__dirname, '..');
+const CATEGORY = 'drunk-monkeys-birds-subdued';
+const LOG = `/tmp/dmbs/parallel-${new Date().toISOString().replace(/[:.]/g, '-')}.log`;
+fs.mkdirSync(path.dirname(LOG), { recursive: true });
+const t0 = Date.now();
+function L(msg) {
+  const line = `[${new Date().toISOString()}]  ${msg}`;
+  console.log(line);
+  fs.appendFileSync(LOG, line + '\n');
+}
+
+L(`=== drunk-monkeys-birds-subdued PARALLEL: ${N} designs × ${PARLEL} workers ===`);
+L(`log file: ${LOG}`);
+
+// Build + filter prompts upfront
+const items = buildN(N).filter((it, i) => {
+  const g = checkPrompt(it.prompt);
+  if (g.verdict !== 'OK') {
+    L(`[skip ${i+1}] settlement ${g.verdict} — ${(g.reason||'').slice(0,80)}`);
+    return false;
+  }
+  return true;
+});
+L(`prompts: ${items.length} (${N - items.length} skipped by settlement-gate)`);
+
+let ok = 0, fail = 0, idx = 0;
+const inflight = new Map();
+
+function runOne(item, i) {
+  return new Promise((resolve) => {
+    const child = spawn('node', [
+      path.join(ROOT, 'scripts', 'generate_designs.js'),
+      '--n', '1',
+      '--kind', 'seamless_tile',
+      '--category', CATEGORY,
+      '--prompts', item.prompt,
+    ], { cwd: ROOT });
+    let stderr = '';
+    child.stderr.on('data', d => { stderr += d.toString(); });
+    const killTimer = setTimeout(() => { try { child.kill('SIGKILL'); } catch (e){} }, 600_000);
+    child.on('close', (code) => {
+      clearTimeout(killTimer);
+      if (code === 0) {
+        ok++;
+        L(`  ✓ [${i+1}] ok  palette=${item.palette}  animal=${item.animal_summary.slice(0,50)}  (${ok}+${fail}=${ok+fail}/${items.length}  ${((Date.now()-t0)/60000).toFixed(1)}min)`);
+      } else {
+        fail++;
+        L(`  ✗ [${i+1}] FAIL code=${code}  stderr=${stderr.slice(0,160).replace(/\n/g,' ')}`);
+      }
+      resolve();
+    });
+  });
+}
+
+const HEART_MS = 30_000;
+const heart = setInterval(() => {
+  L(`  ❤ heartbeat — done=${ok+fail}/${items.length}  ok=${ok}  fail=${fail}  inflight=${inflight.size}  elapsed=${((Date.now()-t0)/60000).toFixed(1)}min`);
+}, HEART_MS);
+
+(async () => {
+  // Bounded-concurrency promise pool
+  const workers = Array.from({ length: PARLEL }, async () => {
+    while (idx < items.length) {
+      const myIdx = idx++;
+      const item = items[myIdx];
+      inflight.set(myIdx, item);
+      await runOne(item, myIdx);
+      inflight.delete(myIdx);
+    }
+  });
+  await Promise.all(workers);
+  clearInterval(heart);
+  L(`=== batch done: ${ok} ok / ${fail} fail of ${items.length} (${((Date.now()-t0)/60000).toFixed(1)} min) ===`);
+  // Refresh designs.json + hot-reload
+  L('refreshing designs.json snapshot…');
+  spawn('python3', [path.join(ROOT, 'scripts', 'refresh_designs_snapshot.py')],
+        { cwd: ROOT, stdio: 'inherit' });
+  process.exit(fail === 0 ? 0 : (ok > 0 ? 0 : 1));
+})();
diff --git a/scripts/drunk_monkeys_birds_subdued_prompts.js b/scripts/drunk_monkeys_birds_subdued_prompts.js
new file mode 100644
index 0000000..22591d4
--- /dev/null
+++ b/scripts/drunk_monkeys_birds_subdued_prompts.js
@@ -0,0 +1,173 @@
+#!/usr/bin/env node
+/**
+ * Focused prompt builder for Steve's 2026-05-20 ask — "100 new drunk-animal
+ * wallpapers using std colors. monkeys, birds, botanical subdued."
+ *
+ * Differs from the existing drunk_animal_prompts.js (which leans Warhol-pop
+ * with electric pinks + cobalt) in three ways:
+ *   1. Animals weighted to MONKEYS + BIRDS only (the existing builder has
+ *      frogs/elephants/giraffes/sloths/etc. mixed in)
+ *   2. Botanical subjects are SUBDUED — heritage roses, dusted hydrangeas,
+ *      dried lavender, neutrals — NOT saturated cottage-garden bombs
+ *   3. Palettes pulled from professional-palette-suffix.txt (the empirical
+ *      23,817-product reference baked into TONE_ON_TONE_SUFFIX) — earth tones,
+ *      sage/cream/charcoal/parchment/dusty rose/ochre/slate, no neon
+ *
+ * Settlement-gate compatibility — no directional foliage, no banana/grape/
+ * palm/butterfly, birds are PERMITTED because Part A trio isn't satisfied
+ * (botanical is subdued/symmetric, not directional).
+ */
+
+const { TONE_ON_TONE_SUFFIX } = require('./fashion_palettes');
+
+const MONKEYS = [
+  'wide-eyed capuchin monkey',
+  'long-tailed spider monkey, sloshed and woozy',
+  'sleepy golden lion tamarin, glassy-eyed',
+  'tipsy squirrel monkey, mid-stumble',
+  'plastered black-and-white colobus monkey',
+  'wobbly red howler monkey, mouth slack',
+  'sloshed mandrill with drooping eyelids',
+  'jolly white-faced saki monkey',
+  'drunken proboscis monkey, swaying',
+  'inebriated japanese macaque, blushing pink under the eyes',
+  'crooked-grin marmoset, holding a tiny coupe glass',
+  'glassy-eyed gibbon swinging slow-motion',
+  'long-armed orangutan with a cocktail',
+  'cross-eyed pygmy marmoset perched on a glass rim',
+];
+
+const BIRDS = [
+  'tipsy scarlet macaw with feathers ruffled',
+  'sloshed great horned owl, one eye half-closed',
+  'drunken kingfisher with cocktail in beak',
+  'plastered toucan with crooked beak grin',
+  'woozy hummingbird hovering off-balance',
+  'inebriated peacock, fanned feathers drooping',
+  'wobbly pelican mid-spit-take',
+  'glassy-eyed great blue heron tipping a glass',
+  'drunk barn owl, talons clutching a bottle',
+  'sloshed flamingo, one leg up, wine sloshing',
+  'tipsy cockatoo mid-screech, crest askew',
+  'drunken cedar waxwing pair sharing a coupe',
+  'plastered house finch hiccupping',
+  'wobbly long-eared owl mid-blink',
+  'inebriated mourning dove, eyes lidded',
+];
+
+// 80% animal-with-drink, 20% pure pair / trio scenes — gives the collection
+// some variety beyond solo-portrait shots.
+const TABLEAUX = [
+  'cradling a long-stemmed wine glass, deep burgundy sloshing over the rim',
+  'mid-toast with a coupe of pale rosé, head tilted',
+  'holding an amber whiskey tumbler in two paws, two melting ice cubes',
+  'gripping a frosted clear vodka glass aloft, condensation beads',
+  'one wing/paw on a stout pint, cream head, eyelids drooping',
+  'mid-pour from a cobalt-blue bottle into a shot glass, eyes crossed',
+  'cradling a gold-labeled cognac snifter, swirling deliberately',
+  'martini glass in mid-spin, two olives, vodka splashing',
+  'champagne flute tipped over, golden bubbles spilling, surprised face',
+  'pale-gold pilsner glass in one paw, amber bottle in the other',
+  'pair sharing a coupe, beaks/paws touching the rim simultaneously',
+  'trio mid-cheers, three glasses meeting at the center',
+  'collapsed against a flower stem, empty glass tipped beside',
+  'asleep curled around a bottle, mouth open, contented',
+];
+
+// SUBDUED botanical-subjects. Soft, heritage, controlled — no directional
+// variation, no jungle-tropical, no banana/palm/butterfly. Mix of native
+// US wildflowers + heritage English garden.
+const BOTANICALS = [
+  'pale heirloom roses in soft cream and dusty pink, scattered evenly',
+  'antique David Austin roses, butter-yellow and burgundy, controlled spacing',
+  'lacecap hydrangeas in dusted sage and parchment cream',
+  'mophead hydrangeas, faded periwinkle and dove-gray',
+  'soft pencil-sketched ranunculus on a parchment ground',
+  'dried lavender wands, muted purple-gray on bone',
+  'wisteria racemes, mauve-on-cream, gentle drooping',
+  'soft tea-stained peonies, blush and cream, sparse spacing',
+  'wild dogwood blossoms in chalky white on a misty ground',
+  'native trillium and Solomon\'s seal, soft sage on parchment',
+  'columbine and bleeding heart, soft pink and cream, watercolor-faded',
+  'magnolia branches with parchment-white blooms on a slate ground',
+  'softly muted camellias, antique blush on a putty ground',
+  'wildflower meadow rendered in chalky watercolor — sage, oat, dusty rose',
+  'flowering quince in soft coral on a cream ground',
+  'bone-white anemones scattered on a sage-gray field',
+];
+
+// Composition cues that DO NOT trigger Part A (no directional variation,
+// no edge-to-edge tropical foliage, no multiple ink colors against bg)
+const COMPOSITIONS = [
+  'half-drop repeat, generous open ground, motifs evenly spaced',
+  'block repeat, single-color background, motifs symmetric',
+  'soft scattered placement with breathing room between motifs',
+  'centered medallion repeat, symmetric arrangement',
+  'tone-on-tone vignettes, motif and ground sharing the same family',
+  'damask-style symmetric repeat, motif inside controlled medallion',
+  'stripe-and-floral, vertical bands separating motifs',
+  'grid placement, motifs locked in invisible 9-square grid',
+];
+
+// 8 std color palettes — earth/heritage tones pulled from the empirical
+// professional reference. Each palette is 3-4 hexes with a one-line voice.
+const PALETTES = [
+  { name: 'sage on cream',       hex: ['#A8B89E', '#F5F0E1', '#3A3530'], voice: 'soft eucalyptus sage, cream ground, deep umber accent' },
+  { name: 'dusty rose & bone',   hex: ['#C9A39A', '#F5F3EE', '#6B382C'], voice: 'antique dusty rose, bone ground, warm cocoa accent' },
+  { name: 'charcoal & parchment', hex: ['#363636', '#F2E8D9', '#A47C5A'], voice: 'deep charcoal, parchment cream, heritage tan accent' },
+  { name: 'oyster & slate',      hex: ['#D3D3D3', '#808080', '#36454F', '#F0F0F0'], voice: 'oyster gray, blued slate, cream ground, ink charcoal' },
+  { name: 'ochre & oat',         hex: ['#C9A87C', '#F0E68C', '#4A4A4A', '#F5F0E1'], voice: 'aged ochre, oat field, walnut umber, cream' },
+  { name: 'cocoa & ivory',       hex: ['#6B3827', '#FFFFFF', '#D2B48C'], voice: 'dark cocoa, pure ivory, fawn middle' },
+  { name: 'mineral teal & cream', hex: ['#008080', '#F5F0E1', '#363636'], voice: 'aged mineral teal, cream ground, charcoal punctuation' },
+  { name: 'putty & blush',       hex: ['#C9BFA7', '#FFE4E1', '#3A3530'], voice: 'warm putty, soft antique blush, deep umber' },
+];
+
+function pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; }
+
+function buildOne(i) {
+  // Bias 55% monkey, 35% bird, 10% mixed pair (one of each)
+  let animal;
+  const r = Math.random();
+  if (r < 0.55) animal = pick(MONKEYS);
+  else if (r < 0.90) animal = pick(BIRDS);
+  else animal = `${pick(MONKEYS)} sharing a drink with a ${pick(BIRDS).replace(/^tipsy |^sloshed |^drunken |^plastered |^woozy |^inebriated |^wobbly |^glassy-eyed |^drunk /, '')}`;
+
+  const tableau = pick(TABLEAUX);
+  const botanical = pick(BOTANICALS);
+  const composition = pick(COMPOSITIONS);
+  const palette = pick(PALETTES);
+
+  // Build the prompt — clearly SUBDUED, clearly REPEAT-FRIENDLY, clearly
+  // settlement-safe (no directional/tropical foliage, controlled multicolor).
+  const positive =
+    `Editorial wallpaper design — ${animal}, ${tableau}, set on a subdued botanical ground of ${botanical}. ` +
+    `Composition: ${composition}. ` +
+    `Palette: ${palette.voice}; palette hex anchors: ${palette.hex.join(', ')}. ` +
+    `Style: hand-painted gouache illustration on heritage textured paper, ` +
+    `restrained luxury wallcovering, museum-archival quality, fine brushwork, ` +
+    `tone-on-tone restraint, no neon, no electric saturation, no rainbow. ` +
+    TONE_ON_TONE_SUFFIX;
+
+  return {
+    prompt: positive,
+    palette: palette.name,
+    palette_hex: palette.hex,
+    animal_summary: animal.slice(0, 64),
+  };
+}
+
+function buildN(n) {
+  const out = [];
+  for (let i = 0; i < n; i++) out.push(buildOne(i));
+  return out;
+}
+
+module.exports = { buildN, buildOne, MONKEYS, BIRDS, BOTANICALS, PALETTES };
+
+if (require.main === module) {
+  const n = parseInt(process.argv[2] || '5', 10);
+  buildN(n).forEach((p, i) => {
+    console.log(`\n#${i+1}  palette=${p.palette}  animal=${p.animal_summary}`);
+    console.log('  ' + p.prompt.slice(0, 200) + '…');
+  });
+}
diff --git a/scripts/drunk_monkeys_v2_batch.js b/scripts/drunk_monkeys_v2_batch.js
new file mode 100644
index 0000000..606fc39
--- /dev/null
+++ b/scripts/drunk_monkeys_v2_batch.js
@@ -0,0 +1,94 @@
+#!/usr/bin/env node
+/**
+ * v2 batch — uses drunk_monkeys_v2_prompts (subject-first, short, explicit
+ * cocktail props in prompt body) + the strengthened solid-colors prompt
+ * suffix wired into generate_designs.js (2026-05-21 directive).
+ *
+ * Differs from v1 (drunk_monkeys_birds_subdued_parallel.js):
+ *   - Imports v2 prompt builder
+ *   - Category = 'drunk-monkeys-v2' (separable from the 'drunk-monkeys-birds-subdued' v1 batch)
+ *   - Same 6-worker parallel pool, 600s per-design timeout
+ *
+ * Usage:  node scripts/drunk_monkeys_v2_batch.js [N] [CONCURRENCY]
+ *           defaults: N=100, CONCURRENCY=6
+ */
+const { spawn } = require('child_process');
+const path = require('path');
+const fs = require('fs');
+const { buildN } = require('./drunk_monkeys_v2_prompts');
+const { checkPrompt } = require('./settlement-gate');
+
+const N      = parseInt(process.argv[2] || '100', 10);
+const PARLEL = parseInt(process.argv[3] || '6', 10);
+const ROOT     = path.join(__dirname, '..');
+const CATEGORY = 'drunk-monkeys-v2';
+const LOG = `/tmp/dmbs/v2-${new Date().toISOString().replace(/[:.]/g, '-')}.log`;
+fs.mkdirSync(path.dirname(LOG), { recursive: true });
+const t0 = Date.now();
+function L(msg) {
+  const line = `[${new Date().toISOString()}]  ${msg}`;
+  console.log(line);
+  fs.appendFileSync(LOG, line + '\n');
+}
+
+L(`=== drunk-monkeys-v2: ${N} designs × ${PARLEL} workers ===`);
+L(`log file: ${LOG}`);
+
+const items = buildN(N).filter((it, i) => {
+  const g = checkPrompt(it.prompt);
+  if (g.verdict !== 'OK') { L(`[skip ${i+1}] settlement ${g.verdict}`); return false; }
+  return true;
+});
+L(`prompts: ${items.length} (${N - items.length} settlement-skipped)`);
+
+let ok = 0, fail = 0, idx = 0;
+const inflight = new Map();
+
+function runOne(item, i) {
+  return new Promise((resolve) => {
+    const child = spawn('node', [
+      path.join(ROOT, 'scripts', 'generate_designs.js'),
+      '--n', '1',
+      '--kind', 'seamless_tile',
+      '--category', CATEGORY,
+      '--prompts', item.prompt,
+    ], { cwd: ROOT });
+    let stderr = '';
+    child.stderr.on('data', d => { stderr += d.toString(); });
+    const killTimer = setTimeout(() => { try { child.kill('SIGKILL'); } catch (e){} }, 600_000);
+    child.on('close', (code) => {
+      clearTimeout(killTimer);
+      if (code === 0) {
+        ok++;
+        L(`  ✓ [${i+1}] palette=${item.palette}  animal=${item.animal_summary}  (${ok}+${fail}=${ok+fail}/${items.length}  ${((Date.now()-t0)/60000).toFixed(1)}min)`);
+      } else {
+        fail++;
+        L(`  ✗ [${i+1}] FAIL code=${code}  ${stderr.slice(0,140).replace(/\n/g,' ')}`);
+      }
+      resolve();
+    });
+  });
+}
+
+const heart = setInterval(() => {
+  L(`  ❤ heartbeat — done=${ok+fail}/${items.length}  ok=${ok}  fail=${fail}  inflight=${inflight.size}  elapsed=${((Date.now()-t0)/60000).toFixed(1)}min`);
+}, 30_000);
+
+(async () => {
+  const workers = Array.from({ length: PARLEL }, async () => {
+    while (idx < items.length) {
+      const myIdx = idx++;
+      const item = items[myIdx];
+      inflight.set(myIdx, item);
+      await runOne(item, myIdx);
+      inflight.delete(myIdx);
+    }
+  });
+  await Promise.all(workers);
+  clearInterval(heart);
+  L(`=== batch done: ${ok} ok / ${fail} fail of ${items.length} (${((Date.now()-t0)/60000).toFixed(1)} min) ===`);
+  L('refreshing designs.json snapshot…');
+  spawn('python3', [path.join(ROOT, 'scripts', 'refresh_designs_snapshot.py')],
+        { cwd: ROOT, stdio: 'inherit' });
+  process.exit(fail === 0 ? 0 : (ok > 0 ? 0 : 1));
+})();
diff --git a/scripts/drunk_monkeys_v2_prompts.js b/scripts/drunk_monkeys_v2_prompts.js
new file mode 100644
index 0000000..63cb6eb
--- /dev/null
+++ b/scripts/drunk_monkeys_v2_prompts.js
@@ -0,0 +1,152 @@
+#!/usr/bin/env node
+/**
+ * v2 prompt builder — Steve flagged v1 output as "horrible" because the model
+ * was averaging out the long structured prompt toward generic
+ * birds-on-a-botanical-ground. v2 fixes:
+ *
+ *   1. LEAD with the subject — "Drunk capuchin monkey clutching a wine glass"
+ *      goes FIRST. SDXL/ComfyUI weights early tokens heaviest.
+ *   2. SHORT prompts — drop the 1.3KB TONE_ON_TONE_SUFFIX. Use only the
+ *      palette voice + 1 composition cue.
+ *   3. Explicit cocktail props in BOTH the subject + a repeat at mid-prompt
+ *      so the diffusion model doesn't ignore it.
+ *   4. Stronger negative prompt — bans no-animals, generic-wallpaper-pattern,
+ *      monstera/banana/palm (settlement-safe), and the failure modes from v1.
+ *
+ * Settlement-gate compatibility — birds (Part B) only block when combined
+ * with full Part A. v2 prompts have no directional foliage and no
+ * edge-to-edge banana/palm; safe.
+ */
+
+const MONKEYS = [
+  'wide-eyed capuchin monkey',
+  'long-tailed spider monkey',
+  'sleepy golden lion tamarin',
+  'tipsy squirrel monkey',
+  'wobbly red howler monkey',
+  'sloshed mandrill',
+  'jolly white-faced saki monkey',
+  'drunken proboscis monkey',
+  'inebriated japanese macaque',
+  'crooked-grin marmoset',
+  'glassy-eyed gibbon',
+  'long-armed orangutan',
+  'cross-eyed pygmy marmoset',
+  'plastered black-and-white colobus monkey',
+];
+
+const BIRDS = [
+  'tipsy scarlet macaw',
+  'sloshed great horned owl',
+  'drunken kingfisher',
+  'plastered toucan',
+  'woozy hummingbird',
+  'inebriated peacock',
+  'wobbly pelican',
+  'drunk barn owl',
+  'sloshed flamingo',
+  'tipsy cockatoo',
+  'plastered house finch',
+  'wobbly long-eared owl',
+];
+
+// Lead-line action — what the animal IS doing in the moment (drink-in-hand).
+// Each one mentions the glass/bottle TYPE explicitly so the diffusion model
+// has a concrete object to render alongside the animal.
+const ACTIONS = [
+  'clutching a deep-burgundy wine glass, head tilted back, eyelids drooping',
+  'cradling an amber whiskey tumbler in two paws, two ice cubes melting',
+  'mid-toast with a coupe of pale rosé, foam spilling over the rim',
+  'pouring a clear vodka bottle into a tiny shot glass, eyes crossed',
+  'collapsed onto a pillow, empty wine glass tipped on its side beside it',
+  'mid-spit-take, dark stout spraying from a pint glass',
+  'asleep curled around a frosted decanter, mouth hanging open',
+  'gripping a champagne flute, golden bubbles overflowing, surprised look',
+  'mid-sip from a martini glass with two olives skewered',
+  'hugging a tall pilsner glass the size of its head, eyes glazed',
+  'two-pawed grip on a cognac snifter, swirling sloppily',
+  'one paw on a cocktail with a paper umbrella, the other paw raised in cheers',
+];
+
+// SUBDUED botanical *backgrounds* — kept minimal and explicitly behind-the-animal
+// so the model doesn't promote the flowers to the foreground.
+const BACKDROPS = [
+  'soft cream peonies in the background, blurred and recessive',
+  'antique cream roses behind, painted small and faint',
+  'lacecap hydrangeas in dusted sage behind the figure',
+  'dried lavender on parchment behind, suggested not dominant',
+  'misty wildflower meadow in the background, watercolor wash',
+  'soft pencil-sketched ranunculus on a parchment ground behind',
+  'magnolia branches in chalky white behind, faint',
+  'plain parchment ground with a few scattered cream blooms behind',
+  'softly muted camellias on a putty ground behind',
+  'sage-on-cream tone-on-tone botanical wash behind the figure',
+];
+
+const COMPOSITIONS = [
+  'half-drop repeat pattern',
+  'block repeat pattern',
+  'scattered placement with generous open ground',
+  'centered symmetric repeat',
+  'damask medallion repeat',
+];
+
+const PALETTES = [
+  { name: 'sage-on-cream',       voice: 'soft sage and cream palette, warm cocoa accent' },
+  { name: 'dusty-rose-bone',     voice: 'antique dusty rose on bone ground, cocoa accent' },
+  { name: 'charcoal-parchment',  voice: 'deep charcoal on parchment, heritage tan accent' },
+  { name: 'oyster-slate',        voice: 'oyster gray on slate, cream ground' },
+  { name: 'ochre-oat',           voice: 'aged ochre on oat field, walnut umber accent' },
+  { name: 'cocoa-ivory',         voice: 'dark cocoa on ivory, fawn middle tone' },
+  { name: 'mineral-teal-cream',  voice: 'aged mineral teal on cream, charcoal accent' },
+  { name: 'putty-blush',         voice: 'warm putty with antique blush accent' },
+];
+
+function pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; }
+
+function buildOne(i) {
+  // 60% monkey-led, 30% bird-led, 10% pair (monkey + bird)
+  let subject;
+  const r = Math.random();
+  if (r < 0.60) subject = pick(MONKEYS);
+  else if (r < 0.90) subject = pick(BIRDS);
+  else subject = `${pick(MONKEYS)} and a ${pick(BIRDS)} together`;
+
+  const action = pick(ACTIONS);
+  const backdrop = pick(BACKDROPS);
+  const composition = pick(COMPOSITIONS);
+  const palette = pick(PALETTES);
+
+  // SUBJECT FIRST — that's the key v2 fix. ~150-200 char prompt total.
+  const positive =
+    `${subject}, ${action}. ` +
+    `${backdrop}. ` +
+    `Hand-painted gouache wallpaper illustration, ${composition}, ${palette.voice}, ` +
+    `restrained tone-on-tone, museum-archival print quality. ` +
+    `Make sure the animal AND its drinking glass/bottle are clearly visible and prominent.`;
+
+  return {
+    prompt: positive,
+    palette: palette.name,
+    palette_hex: null,
+    animal_summary: subject.slice(0, 60),
+  };
+}
+
+function buildN(n) {
+  const out = [];
+  for (let i = 0; i < n; i++) out.push(buildOne(i));
+  return out;
+}
+
+const NEGATIVE = 'no animal, animal missing, only flowers, only botanical, only birds in a field, generic wallpaper pattern, generic floral, abstract, no drink, no glass, no bottle, monstera, palm leaves, banana leaves, jungle, butterflies, grapes, bananas, neon, fluorescent, vivid saturated, rainbow, electric pink, photoreal photograph, low quality, blurry, smudgy face, deformed limbs';
+
+module.exports = { buildN, buildOne, MONKEYS, BIRDS, ACTIONS, PALETTES, NEGATIVE };
+
+if (require.main === module) {
+  const n = parseInt(process.argv[2] || '5', 10);
+  buildN(n).forEach((p, i) => {
+    console.log(`\n#${i+1}  palette=${p.palette}  animal=${p.animal_summary}`);
+    console.log('  ' + p.prompt);
+  });
+}
diff --git a/scripts/extract-silhouette.py b/scripts/extract-silhouette.py
new file mode 100644
index 0000000..f9c0bd3
--- /dev/null
+++ b/scripts/extract-silhouette.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python3
+"""Extract a clean binary silhouette from a photographic source, then
+recolor the foreground+background to a designer colorway.
+
+Use case: SDXL generates photographic images of subjects (saguaros,
+joshua trees, palms, etc.) when asked for graphic wallpaper. This script
+post-processes that photo into a true 2-tone (or 3-tone) graphic by
+Otsu-thresholding to a binary mask, then tinting.
+
+Mirrors the fliepaper approach: photographer captures the subject, then
+the design is reduced to a stencil silhouette.
+
+Usage:
+  GROUND=#FFFFFF FG=#0E0E0E python3 extract-silhouette.py <input.png>
+
+Env:
+  GROUND          background hex (default #FFFFFF)
+  FG              foreground hex (default #0E0E0E)
+  INVERT          if set to 1, invert mask (use if subject ends up as background)
+  EDGE_BIAS       Otsu offset, -50..+50 (default 0). Positive = more pixels become FG.
+
+Behavior:
+  - Backs up to <input>.original.png if not already present.
+  - Converts to grayscale, applies Otsu threshold.
+  - Auto-detects whether subject is dark-on-light or light-on-dark by
+    checking which class is at the image corners (corners ≈ background).
+  - Recolors: background → GROUND, foreground → FG.
+  - Saves in-place at <input>.
+"""
+import os
+import sys
+from PIL import Image
+import numpy as np
+
+if len(sys.argv) != 2:
+    print("usage: extract-silhouette.py <input.png>", file=sys.stderr)
+    sys.exit(1)
+
+src = sys.argv[1]
+if not os.path.exists(src):
+    print(f"file not found: {src}", file=sys.stderr)
+    sys.exit(2)
+
+
+def parse_hex(h):
+    h = h.lstrip('#')
+    return (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))
+
+
+GROUND    = parse_hex(os.environ.get('GROUND', '#FFFFFF'))
+FG        = parse_hex(os.environ.get('FG',     '#0E0E0E'))
+INVERT    = os.environ.get('INVERT', '0') == '1'
+EDGE_BIAS = int(os.environ.get('EDGE_BIAS', '0'))
+
+backup = src.replace('.png', '.original.png')
+if not os.path.exists(backup):
+    with open(src, 'rb') as fi, open(backup, 'wb') as fo:
+        fo.write(fi.read())
+
+im = Image.open(src).convert('RGB')
+gray = np.array(im.convert('L'), dtype=np.int32)
+
+
+def otsu(g):
+    """Standard Otsu threshold on a uint8 grayscale array."""
+    hist, _ = np.histogram(g, bins=256, range=(0, 255))
+    total = g.size
+    sum_total = (np.arange(256) * hist).sum()
+    sumB, wB, max_var, thresh = 0.0, 0, 0.0, 127
+    for t in range(256):
+        wB += int(hist[t])
+        if wB == 0:
+            continue
+        wF = total - wB
+        if wF == 0:
+            break
+        sumB += t * int(hist[t])
+        mB = sumB / wB
+        mF = (sum_total - sumB) / wF
+        v = wB * wF * (mB - mF) ** 2
+        if v > max_var:
+            max_var, thresh = v, t
+    return thresh
+
+
+thresh = otsu(gray) + EDGE_BIAS
+mask = gray < thresh  # True = darker pixels = candidate foreground
+
+# Decide which class is actually foreground by checking corner pixels —
+# corners almost always = background in well-composed subject photos.
+H, W = gray.shape
+corner_avg = (int(gray[0, 0]) + int(gray[0, W - 1]) +
+              int(gray[H - 1, 0]) + int(gray[H - 1, W - 1])) / 4
+# If corners are dark, the dark-pixel class IS the background — flip mask.
+corners_are_dark = corner_avg < thresh
+fg_mask = mask  # dark = fg, light = bg
+if corners_are_dark:
+    fg_mask = ~mask
+
+if INVERT:
+    fg_mask = ~fg_mask
+
+out = np.zeros((H, W, 3), dtype=np.uint8)
+out[~fg_mask] = GROUND
+out[fg_mask]  = FG
+
+Image.fromarray(out, 'RGB').save(src, optimize=True)
+
+fg_count = int(fg_mask.sum())
+total = fg_mask.size
+print(f"OK silhouette {src} thresh={thresh} fg={100*fg_count/total:.1f}% "
+      f"ground=#{GROUND[0]:02X}{GROUND[1]:02X}{GROUND[2]:02X} "
+      f"fg=#{FG[0]:02X}{FG[1]:02X}{FG[2]:02X}")
diff --git a/scripts/generate-drip-25344.sh b/scripts/generate-drip-25344.sh
new file mode 100755
index 0000000..f79f1f5
--- /dev/null
+++ b/scripts/generate-drip-25344.sh
@@ -0,0 +1,45 @@
+#!/usr/bin/env bash
+# 12 painterly drip-splatter designs tuned to design #25344's formula:
+#   - cream/warm-white ground (the "canvas")
+#   - DENSE dark drip-web (charcoal/black/oxblood/midnight as primary)
+#   - warm-gold/saddle/grey accent (the secondary "echo" splatter)
+#   - all colors hue-synced
+# Goal: painterly density, gestural movement, no thin/sparse.
+
+cd "$(dirname "$0")/.."
+PSQL="/opt/homebrew/opt/postgresql@14/bin/psql"
+
+# 12 colorways tuned to the 25344 density formula — each is a charcoal-cream
+# base with a different warm-gold/secondary accent and a deep shadow.
+declare -a CWS=(
+  "Charcoal Cream Bronze|#F2EADB|#3D3833|#A78863|#0F0D0B"
+  "Ivory Ink Sienna|#F2EADB|#1A1814|#A85040|#0A0908"
+  "Bone Onyx Gold|#F2EADB|#0B0B0B|#C9A14B|#040404"
+  "Cream Soot Camel|#EDE4D3|#2A2520|#9B8050|#0F0D0B"
+  "Linen Sumi Saddle|#E8DDC8|#1A1410|#7A4A28|#0A0805"
+  "Ivory Iron Brass|#F2EADB|#3D3833|#9B8050|#1A1814"
+  "Bone Coal Russet|#F2EADB|#2A1810|#7A2828|#0F0908"
+  "Cream Tar Wheat|#EDE4D3|#1A1410|#D8C8AA|#0A0805"
+  "Warm Ivory Black Olive|#F2EADB|#0F0D0B|#7A6248|#040404"
+  "Champagne Onyx Pewter|#D7C9A8|#1A1814|#8A857B|#0A0908"
+  "Bone Ink Antique-Brass|#F2EADB|#1F2740|#9B8050|#0B0F1C"
+  "Cream Anthracite Saddle|#F2EADB|#2D2C2B|#5C4033|#0F0E0D"
+)
+
+for i in 1 2 3 4 5 6 7 8 9 10 11 12; do
+  IFS='|' read -r NAME GROUND FIGURE ACCENT SHADOW <<< "${CWS[$((i-1))]}"
+  PROMPT="Drip-splatter wallpaper — ${NAME} colorway. Ground: solid ${GROUND}. Primary dark drip-web ink: ${FIGURE} (the dominant gestural element — long thrown lines, dripped pools, swirling skeins, splatter clusters). Secondary echo splatter: ${ACCENT} (smaller punctuation drips + dripped pools beneath the primary web). Tertiary shadow: ${SHADOW} (deepest pools under primary). EXACTLY 4 COLORS — ground + primary + accent + shadow, all in the same hue family (hue-sync). Composition: DENSE PAINTERLY DRIP-WEB, gestural thrown lines crossing the canvas in tangled overlapping arcs, not sparse — at least 45-55% of the canvas is covered with drip elements. The marks are FLAT FILLED SHAPES (not painted brush strokes) — each splatter, drip-line, dripped pool, and skein is one uniform solid ink color with hard razor-sharp edges. NO half-tones, NO chromatic gradient, NO color bleed between layers. The 4 colors are layered in distinct flat zones — primary-drip-line, accent-splatter, shadow-pool, ground — never blended. Reference: gestural abstract-expressionist drip painting rendered as a 4-color screen-print serigraph; refined high-end wallcovering finish, hand-painted gouache. Square aspect ratio, seamless tile repeat, no edge artifacts."
+  echo "=== drip $i/12: $NAME ==="
+  RESULT=$(GEN_BACKEND=replicate node scripts/generate_designs.js --n 1 --kind seamless_tile --width 54 --height 54 --category drip-splatter --prompts "$PROMPT" 2>&1 | tail -3)
+  ID=$(echo "$RESULT" | grep -oE "#[0-9]+" | head -1 | tr -d '#')
+  if [ -n "$ID" ]; then
+    LP=$("$PSQL" dw_unified -At -c "SELECT local_path FROM spoon_all_designs WHERE id=$ID;")
+    if [ -f "$LP" ]; then
+      QUANT_COLORS=4 python3 scripts/quantize-2tone.py "$LP" 2>&1 | grep -v Deprecation | tail -1
+      "$PSQL" dw_unified -c "UPDATE spoon_all_designs SET is_published=TRUE WHERE id=$ID;" >/dev/null 2>&1
+      echo "  published id=$ID"
+    fi
+  fi
+  sleep 5
+done
+echo "=== done — 12 new painterly drips published ==="
diff --git a/scripts/generate-pollock.sh b/scripts/generate-drip-splatter.sh
similarity index 53%
rename from scripts/generate-pollock.sh
rename to scripts/generate-drip-splatter.sh
index 622d6cc..cd8e572 100755
--- a/scripts/generate-pollock.sh
+++ b/scripts/generate-drip-splatter.sh
@@ -1,7 +1,7 @@
 #!/usr/bin/env bash
-# One-shot batch: 12 Jackson Pollock-style drip/splatter wallpaper designs
+# One-shot batch: 12 drip-splatter drip-splatter master-style drip/splatter wallpaper designs
 # in Steve's muted Designer-Wallcoverings palette.
-# Inspired by Pollock "Number 17A" ($200M private 2015) and tapestry-scale
+# Inspired by drip-splatter master "Number 17A" ($200M private 2015) and tapestry-scale
 # drip canvases. NOT a copy — emulate the gestural drip-splatter language
 # in our color vocabulary.
 # 12 base designs × 3 colorway variants = 48 total.
@@ -28,9 +28,9 @@ declare -a CWS=(
 
 for i in 1 2 3 4 5 6 7 8 9 10 11 12; do
   IFS='|' read -r NAME GROUND FIGURE ACCENT SHADOW <<< "${CWS[$((i-1))]}"
-  PROMPT="Jackson Pollock drip-splatter abstract wallpaper — ${NAME} colorway. Ground: solid ${GROUND}. Primary drip ink: ${FIGURE}. Secondary accent splatter: ${ACCENT}. Tertiary depth splatter: ${SHADOW}. EXACTLY 4 COLORS — ground + figure + accent + shadow, all in the same hue family (hue-sync). Composition: chaotic gestural drip painting — long thrown lines of ink, splatter clusters, dripped pools, swirling skeins. The marks are FLAT FILLED SHAPES (not painted brush strokes) — each splatter, drip, and pool is one uniform solid ink color with hard edges. NO half-tones, NO chromatic gradient, NO color bleed between layers. The 4 colors are layered in distinct flat zones — drip-line, accent-splatter, shadow-pool, ground — never blended. References: Pollock 'Number 17A' \$200M drip painting, but rendered as a 4-color screen-print serigraph (Andy Warhol pop-art treatment of Pollock's drip language), Maya Romanoff handcrafted luxe wallcovering. Generous breathing room — at least 60% of the canvas shows clean ground tone between drip clusters. Square aspect ratio, seamless tile repeat, no edge artifacts."
+  PROMPT="drip-splatter drip-splatter master drip-splatter abstract wallpaper — ${NAME} colorway. Ground: solid ${GROUND}. Primary drip ink: ${FIGURE}. Secondary accent splatter: ${ACCENT}. Tertiary depth splatter: ${SHADOW}. EXACTLY 4 COLORS — ground + figure + accent + shadow, all in the same hue family (hue-sync). Composition: chaotic gestural drip painting — long thrown lines of ink, splatter clusters, dripped pools, swirling skeins. The marks are FLAT FILLED SHAPES (not painted brush strokes) — each splatter, drip, and pool is one uniform solid ink color with hard edges. NO half-tones, NO chromatic gradient, NO color bleed between layers. The 4 colors are layered in distinct flat zones — drip-line, accent-splatter, shadow-pool, ground — never blended. References: drip-splatter master 'Number 17A' \$200M drip painting, but rendered as a 4-color screen-print serigraph (Andy Warhol pop-art treatment of drip-splatter master's drip language), Maya Romanoff handcrafted luxe wallcovering. Generous breathing room — at least 60% of the canvas shows clean ground tone between drip clusters. Square aspect ratio, seamless tile repeat, no edge artifacts."
   echo "=== generating $i/12: $NAME ==="
-  RESULT=$(GEN_BACKEND=replicate node scripts/generate_designs.js --n 1 --kind seamless_tile --width 54 --height 54 --category pollock-splatter --prompts "$PROMPT" 2>&1 | tail -3)
+  RESULT=$(GEN_BACKEND=replicate node scripts/generate_designs.js --n 1 --kind seamless_tile --width 54 --height 54 --category drip-splatter --prompts "$PROMPT" 2>&1 | tail -3)
   ID=$(echo "$RESULT" | grep -oE "#[0-9]+" | head -1 | tr -d '#')
   if [ -n "$ID" ]; then
     LP=$("$PSQL" dw_unified -At -c "SELECT local_path FROM spoon_all_designs WHERE id=$ID;")
@@ -43,4 +43,4 @@ for i in 1 2 3 4 5 6 7 8 9 10 11 12; do
   fi
 sleep 5
 done
-echo "=== done — all 12 Pollock-splatter designs published ==="
+echo "=== done — all 12 drip-splatter master-splatter designs published ==="
diff --git a/scripts/generate-inspired-catalog.js b/scripts/generate-inspired-catalog.js
new file mode 100644
index 0000000..7c27781
--- /dev/null
+++ b/scripts/generate-inspired-catalog.js
@@ -0,0 +1,197 @@
+#!/usr/bin/env node
+/**
+ * DW-house-style reinterpretation generator.
+ *
+ * Reads data/inspired-catalog/spec.json, iterates motifs × colorways,
+ * generates via Replicate SDXL with anti-ghost prompting, post-quantizes
+ * with quantize-no-ghost.py @ N=6 + GHOST_LUMA_GAP=35, inserts into
+ * spoon_all_designs with generic category + descriptive title.
+ *
+ * NEVER mentions the source archive brand in any rendered output. Memory
+ * rule: feedback_never_show_fliepaper_word_on_images.
+ *
+ * Usage:
+ *   node scripts/generate-inspired-catalog.js              # all motifs
+ *   node scripts/generate-inspired-catalog.js --motif fern-frond
+ *   node scripts/generate-inspired-catalog.js --limit 2    # first 2 motifs
+ *   node scripts/generate-inspired-catalog.js --dry-run    # print prompts only
+ */
+const fs = require('fs');
+const path = require('path');
+const { execSync, spawnSync } = require('child_process');
+
+const SPEC_PATH = path.join(__dirname, '..', 'data', 'inspired-catalog', 'spec.json');
+const spec = JSON.parse(fs.readFileSync(SPEC_PATH, 'utf8'));
+
+const args = process.argv.slice(2);
+const argv = {
+  motif:   args.includes('--motif')  ? args[args.indexOf('--motif')  + 1] : null,
+  limit:   args.includes('--limit')  ? parseInt(args[args.indexOf('--limit')  + 1], 10) : null,
+  dryRun:  args.includes('--dry-run'),
+  cwLimit: args.includes('--cw-limit') ? parseInt(args[args.indexOf('--cw-limit') + 1], 10) : null,
+};
+
+const motifs = argv.motif
+  ? spec.motifs.filter(m => m.slug === argv.motif)
+  : (argv.limit ? spec.motifs.slice(0, argv.limit) : spec.motifs);
+
+const colorways = argv.cwLimit ? spec.colorways.slice(0, argv.cwLimit) : spec.colorways;
+
+if (motifs.length === 0) {
+  console.error(`no motif matched --motif=${argv.motif}`);
+  process.exit(1);
+}
+
+console.log(`=== inspired-catalog generator ===`);
+console.log(`motifs:    ${motifs.length} (${motifs.map(m => m.slug).join(', ')})`);
+console.log(`colorways: ${colorways.length} per motif`);
+console.log(`total:     ${motifs.length * colorways.length} generations`);
+console.log(`backend:   replicate`);
+console.log(``);
+
+function composePrompt(motif, cw) {
+  // CRITICAL: SDXL has strong real-world color priors (saguaros are green,
+  // roses are red, etc.) — without explicit override they hallucinate the
+  // real-world color regardless of colorway hex. The "graphic-print-only"
+  // clause forces it to read the motif as a graphic silhouette tinted to
+  // the colorway, NOT a photograph of the subject.
+  const groundName    = describeColor(cw.ground);
+  const primaryName   = describeColor(cw.primary);
+  return `${motif.subject}. ` +
+    `STRICTLY A GRAPHIC INK SILHOUETTE PRINT — this is a screen-printed wallpaper, NOT a photograph, NOT a botanical illustration. ` +
+    `IGNORE the real-world coloration of the subject entirely. The motif is rendered ENTIRELY in ${primaryName} (${cw.primary}) ink on a SOLID FLAT ${groundName} (${cw.ground}) ground — no green, no real-world subject color, no naturalistic palette. ` +
+    `The subject is tinted only to: ground ${cw.ground} (${groundName}), primary ${cw.primary} (${primaryName}), accent ${cw.accent1}, deep shadow ${cw.shadow}, highlight ${cw.highlight}. ` +
+    `${spec.house_anchor} ${motif.scale}. ` +
+    `Exactly 2-4 well-separated flat ink colors — no in-between mid-tones, no ghost-layer behind the figure, no atmospheric fog, no chromatic gradient. Each motif region is one solid flat color with hard razor edges. ` +
+    `Reference: hand-blocked scenic wallcovering meets refined modern designer screen-print. Single-pass silkscreen. NO half-tones, NO bleed between layers, NO faded secondary echo of the motif behind itself, NO chiaroscuro shading inside the figure. ` +
+    `The colors are all in the same hue family — hue-synced from light highlight through ground through accents to deepest shadow. ` +
+    `Square aspect ratio, seamless tile repeat (edges meet cleanly), flat-paper rendering, no watermark, no signature, no text, no logos. ` +
+    `ABSOLUTELY NO photographic embossed relief, NOT a 3D render, NOT a sculpture photo — flat 2D graphic only. Refined elegant high-end designer wallcovering finish.`;
+}
+
+function describeColor(hex) {
+  // Quick perceptual color naming so the model gets a word-anchor that
+  // matches the hex (it weights words much more than hex codes).
+  const r = parseInt(hex.substr(1, 2), 16);
+  const g = parseInt(hex.substr(3, 2), 16);
+  const b = parseInt(hex.substr(5, 2), 16);
+  const luma = 0.299 * r + 0.587 * g + 0.114 * b;
+  if (luma > 230 && Math.abs(r - g) < 12 && Math.abs(g - b) < 12) return 'pure white';
+  if (luma > 220) return 'warm cream';
+  if (luma < 20)  return 'pure black ink';
+  if (luma < 50)  return 'deep ink black';
+  // Hue-tinted
+  if (r > g && r > b && r - Math.min(g, b) > 30) {
+    if (r > 120 && g < 80) return 'oxblood bordeaux';
+    return 'warm saddle brown';
+  }
+  if (g > r && g > b) return 'deep loden green';
+  if (b > r && b > g) return 'deep indigo navy';
+  return 'warm stone pewter';
+}
+
+const PSQL = '/opt/homebrew/opt/postgresql@14/bin/psql';
+let okCount = 0;
+let failCount = 0;
+const startTime = Date.now();
+
+for (const motif of motifs) {
+  for (const cw of colorways) {
+    const tag = `${motif.slug} · ${cw.slug}`;
+    const prompt = composePrompt(motif, cw);
+
+    if (argv.dryRun) {
+      console.log(`--- ${tag} ---`);
+      console.log(prompt);
+      console.log(``);
+      continue;
+    }
+
+    console.log(`=== ${tag} ===`);
+    const r = spawnSync('node', [
+      path.join(__dirname, 'generate_designs.js'),
+      '--n', '1',
+      '--kind', 'seamless_tile',
+      '--width', '54',
+      '--height', '54',
+      '--category', motif.category,
+      '--prompts', prompt,
+    ], { cwd: path.join(__dirname, '..'), encoding: 'utf8', env: { ...process.env, GEN_BACKEND: 'replicate' } });
+
+    if (r.status !== 0) {
+      console.log(`  ✗ generation failed`);
+      failCount += 1;
+      continue;
+    }
+
+    const idMatch = (r.stdout || '').match(/✓\s*#(\d+)/);
+    const id = idMatch ? parseInt(idMatch[1], 10) : null;
+    if (!id) {
+      console.log(`  ✗ no id parsed`);
+      failCount += 1;
+      continue;
+    }
+
+    // Look up local_path + retitle with descriptive (no-source) title
+    let lp;
+    try {
+      lp = execSync(`${PSQL} dw_unified -At -c "SELECT local_path FROM spoon_all_designs WHERE id=${id};"`, { encoding: 'utf8' }).trim();
+    } catch (e) {
+      console.log(`  ✗ psql lookup failed`);
+      failCount += 1;
+      continue;
+    }
+
+    if (!lp || !fs.existsSync(lp)) {
+      console.log(`  ✗ file not found at ${lp}`);
+      failCount += 1;
+      continue;
+    }
+
+    // Two post-process paths:
+    //   - SILHOUETTE_STYLE motifs: SDXL generates a photo, we threshold
+    //     to a true 2-tone silhouette and tint to the colorway. This is
+    //     how fliepaper does it (photo → silhouette extraction).
+    //   - All other motifs: quantize-no-ghost @ 6 colors with merge.
+    const SILHOUETTE_STYLE = new Set([
+      'saguaro-cactus', 'joshua-tree', 'palo-verde-branch', 'agave-rosette',
+      'scattered-flies', 'flamingo-pair', 'coral-branch', 'tall-grass-reeds',
+      'fan-palm-frond', 'pine-bough', 'banana-palm-leaf', 'eucalyptus-branch',
+      'fern-frond',
+    ]);
+    let q;
+    if (SILHOUETTE_STYLE.has(motif.slug)) {
+      q = spawnSync('python3', [
+        path.join(__dirname, 'extract-silhouette.py'), lp
+      ], {
+        encoding: 'utf8',
+        env: { ...process.env, GROUND: cw.ground, FG: cw.primary },
+      });
+    } else {
+      q = spawnSync('python3', [
+        path.join(__dirname, 'quantize-no-ghost.py'), lp
+      ], {
+        encoding: 'utf8',
+        env: { ...process.env, QUANT_COLORS: '6', GHOST_LUMA_GAP: '35' },
+      });
+    }
+    const quantSummary = (q.stdout || '').trim().split('\n').pop();
+
+    // Update title + tags (descriptive, never source-named)
+    const titleSlug = motif.slug.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
+    const cwTitle = cw.slug.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
+    const finalTitle = `${titleSlug} — ${cwTitle}`;
+    try {
+      execSync(`${PSQL} dw_unified -c "UPDATE spoon_all_designs SET is_published=TRUE, designer='Designer Wallcoverings' WHERE id=${id};" >/dev/null 2>&1`);
+    } catch (_) {}
+
+    okCount += 1;
+    console.log(`  ✓ #${id} · ${finalTitle} · ${quantSummary || 'quantized'}`);
+  }
+}
+
+const elapsed = Math.round((Date.now() - startTime) / 1000);
+console.log(``);
+console.log(`=== done in ${elapsed}s ===`);
+console.log(`ok:   ${okCount}`);
+console.log(`fail: ${failCount}`);
diff --git a/scripts/generate-pollock.sh.bak b/scripts/generate-pollock.sh.bak
index 92cbe9a..622d6cc 100755
--- a/scripts/generate-pollock.sh.bak
+++ b/scripts/generate-pollock.sh.bak
@@ -30,7 +30,7 @@ for i in 1 2 3 4 5 6 7 8 9 10 11 12; do
   IFS='|' read -r NAME GROUND FIGURE ACCENT SHADOW <<< "${CWS[$((i-1))]}"
   PROMPT="Jackson Pollock drip-splatter abstract wallpaper — ${NAME} colorway. Ground: solid ${GROUND}. Primary drip ink: ${FIGURE}. Secondary accent splatter: ${ACCENT}. Tertiary depth splatter: ${SHADOW}. EXACTLY 4 COLORS — ground + figure + accent + shadow, all in the same hue family (hue-sync). Composition: chaotic gestural drip painting — long thrown lines of ink, splatter clusters, dripped pools, swirling skeins. The marks are FLAT FILLED SHAPES (not painted brush strokes) — each splatter, drip, and pool is one uniform solid ink color with hard edges. NO half-tones, NO chromatic gradient, NO color bleed between layers. The 4 colors are layered in distinct flat zones — drip-line, accent-splatter, shadow-pool, ground — never blended. References: Pollock 'Number 17A' \$200M drip painting, but rendered as a 4-color screen-print serigraph (Andy Warhol pop-art treatment of Pollock's drip language), Maya Romanoff handcrafted luxe wallcovering. Generous breathing room — at least 60% of the canvas shows clean ground tone between drip clusters. Square aspect ratio, seamless tile repeat, no edge artifacts."
   echo "=== generating $i/12: $NAME ==="
-  RESULT=$(GEN_BACKEND=comfy COMFY_URL=http://192.168.1.133:8188 node scripts/generate_designs.js --n 1 --kind seamless_tile --width 54 --height 54 --category pollock-splatter --prompts "$PROMPT" 2>&1 | tail -3)
+  RESULT=$(GEN_BACKEND=replicate node scripts/generate_designs.js --n 1 --kind seamless_tile --width 54 --height 54 --category pollock-splatter --prompts "$PROMPT" 2>&1 | tail -3)
   ID=$(echo "$RESULT" | grep -oE "#[0-9]+" | head -1 | tr -d '#')
   if [ -n "$ID" ]; then
     LP=$("$PSQL" dw_unified -At -c "SELECT local_path FROM spoon_all_designs WHERE id=$ID;")
diff --git a/scripts/generate-saguaro-arms-v2.js b/scripts/generate-saguaro-arms-v2.js
new file mode 100644
index 0000000..83c0dde
--- /dev/null
+++ b/scripts/generate-saguaro-arms-v2.js
@@ -0,0 +1,135 @@
+#!/usr/bin/env node
+// Saguaro Arms — Pass 2.
+// One colorway (Mesquite on Bone — the working one from Pass 1).
+// Eight distinct arm geometries.
+// Tighter single-subject prompting: framed as ISOLATED BOTANICAL SPECIMEN
+// STUDY, not "wallpaper" or "mural" (those words pull the model toward
+// tile-repeats and desert-scene compositions).
+
+const { spawnSync } = require('child_process');
+const path = require('path');
+
+const ROOT = path.join(__dirname, '..');
+
+const CW = {
+  slug: 'mesquite-on-bone',
+  name: 'Mesquite on Bone',
+  ground_hex: '#E8DCC4',
+  ground_name: 'warm chalky cream paper',
+  figure_hex: '#2A2520',
+  figure_name: 'deep mesquite-charcoal ink',
+};
+
+const ARM_ARRANGEMENTS = [
+  {
+    slug: 'two-arm-symmetric',
+    desc:
+      'TWO symmetric raised arms, one curving up from each side of the trunk at the same trunk height (about 40 percent from the base), arm tips terminating at the same vertical level. Perfectly balanced classic candelabra silhouette.',
+  },
+  {
+    slug: 'two-arm-asymmetric',
+    desc:
+      'TWO arms in off-balance arrangement — a short stub arm low on the LEFT side at about 25 percent trunk height, and a long full arm on the RIGHT side curving up high, its tip reaching nearly the top of the trunk. Very asymmetric.',
+  },
+  {
+    slug: 'three-arm-candelabra',
+    desc:
+      'THREE arms — two main arms curving symmetrically up from each side of the trunk at about 45 percent trunk height, PLUS a smaller third arm budding from the upper-right main arm. Three-fingered candelabra.',
+  },
+  {
+    slug: 'four-arm-pair',
+    desc:
+      'FOUR arms arranged as two pairs — two arms on the LEFT side (one low at 30 percent, one high at 65 percent) and two arms on the RIGHT side (one at 30 percent, one at 65 percent). Stacked pair geometry, very organized.',
+  },
+  {
+    slug: 'five-arm-scattered',
+    desc:
+      'FIVE arms scattered at uneven trunk heights, none matching — one short arm low LEFT, one mid arm RIGHT, one tall arm upper LEFT, one tall arm near-top RIGHT, and one tiny stub arm budding from the upper-left arm itself. Wild and uneven.',
+  },
+  {
+    slug: 'six-arm-dense',
+    desc:
+      'SIX arms in a dense candelabra arrangement — three arms on the LEFT side at staggered heights (25, 50, 75 percent up) and three arms on the RIGHT side at staggered heights (35, 60, 80 percent up). Saguaro looks like a tree of life, very busy silhouette.',
+  },
+  {
+    slug: 'single-tall-bare',
+    desc:
+      'NO ARMS AT ALL — just a single tall slender column with a slightly tapered top. A young mature saguaro that has not yet branched. Pure vertical silhouette, nothing else.',
+  },
+  {
+    slug: 'three-saguaro-cluster',
+    desc:
+      'NOT one saguaro — THREE saguaros explicitly arranged as a family cluster, all three sharing a single isolated paper background. ONE tall parent at full height in the center with two symmetric arms, ONE shorter saguaro at 60 percent of parent height on the LEFT with one arm on its outer side, ONE shorter saguaro at 55 percent of parent height on the RIGHT with one arm on its outer side. The three saguaros stand side-by-side as a family of three.',
+  },
+];
+
+const POSITIVE =
+  'ISOLATED BOTANICAL SPECIMEN STUDY — one Saguaro cactus (Carnegiea gigantea) mounted as a museum specimen on a single sheet of paper. ' +
+  'The cactus is the ONLY object visible on the page. It is rendered as a bold flat solid silhouette in deep mesquite-charcoal ink (#2A2520) on a warm chalky cream paper ground (#E8DCC4). ' +
+  'No internal coloring variation in the silhouette — pure flat single-tone ink, no gradient, no shading, no spines drawn, no shadows, no highlights. ' +
+  'Subtle parallel hairlines down the trunk imply the iconic vertical ribbing. ' +
+  'The saguaro fills about 80 percent of the vertical height of the page, centered horizontally with even cream paper on both sides. ' +
+  'Tall narrow portrait paper orientation. ' +
+  'Aesthetic: high-end editorial screen-print, archival paper, fliepaper.com register, Cole & Son botanical study, Hermes Saguaro National Park poster.';
+
+const NEGATIVE =
+  'NOT a wallpaper pattern. NOT a seamless tile. NOT a tessellation. NOT a repeating motif. ' +
+  'NOT a desert scene. NOT a landscape. ' +
+  'NO mountains. NO sun. NO clouds. NO sky. NO ground line. NO horizon. NO sand. NO rocks. NO grass. NO other plants on the page. NO surrounding cacti unless I explicitly said so. ' +
+  'NO photoreal cactus. NO 3D rendering. NO drop shadow. NO room mockup. NO frame around the image. NO mat. NO label. NO caption. ' +
+  'NO multi-color rainbow. NO fluorescent neon. NO day-glo. NO chromatic gradient. NO color variation inside the cactus silhouette. ' +
+  'NO banana, NO banana leaves, NO banana pods, NO palm, NO palm fronds, NO monstera, NO philodendron, NO tropical foliage, NO jungle. ' +
+  'NO bananas, NO grapes, NO birds, NO butterflies, NO insects, NO snakes, NO any animals. ' +
+  'NO watermark, NO signature, NO text, NO logos, NO ZARNCCO, NO numbers, NO captions of any kind anywhere on the image.';
+
+console.log('Pass 2 — Generating ' + ARM_ARRANGEMENTS.length + ' Mesquite-on-Bone saguaro specimens (ComfyUI 768×1344 portrait)\n');
+
+let success = 0, fail = 0;
+const ids = [];
+const startedAt = Date.now();
+
+const renderEnv = {
+  ...process.env,
+  COMFY_WIDTH: '768',
+  COMFY_HEIGHT: '1344',
+  REPLICATE_WIDTH: '768',
+  REPLICATE_HEIGHT: '1344',
+};
+
+for (let i = 0; i < ARM_ARRANGEMENTS.length; i++) {
+  const arm = ARM_ARRANGEMENTS[i];
+  const prompt =
+    `Saguaro botanical specimen study — ${arm.slug} arm geometry. ` +
+    `Arm geometry — ${arm.desc} ` +
+    POSITIVE + ' ' +
+    NEGATIVE;
+
+  console.log('[' + (i+1) + '/' + ARM_ARRANGEMENTS.length + '] ' + arm.slug);
+
+  const r = spawnSync('node', [
+    path.join(__dirname, 'generate_designs.js'),
+    '--n', '1',
+    '--kind', 'mural',
+    '--width', '51',
+    '--height', '96',
+    '--panels', '1',
+    '--category', 'saguaro-arms',
+    '--prompts', prompt,
+  ], { cwd: ROOT, encoding: 'utf8', env: renderEnv });
+
+  if (r.status === 0) {
+    success++;
+    const m = (r.stdout || '').match(/#(\d+)/);
+    if (m) ids.push(parseInt(m[1], 10));
+    process.stdout.write('  OK' + (m ? ' (id=' + m[1] + ')' : '') + '\n');
+  } else {
+    fail++;
+    process.stdout.write('  FAIL ' + (r.stderr || r.stdout || '').slice(0, 240) + '\n');
+  }
+}
+
+const elapsed = ((Date.now() - startedAt) / 1000).toFixed(1);
+console.log('\nDone in ' + elapsed + 's — ' + success + ' ok, ' + fail + ' fail');
+console.log('Inserted ids: ' + JSON.stringify(ids));
+
+if (success === 0) process.exit(2);
diff --git a/scripts/generate-saguaro-arms.js b/scripts/generate-saguaro-arms.js
new file mode 100755
index 0000000..97af691
--- /dev/null
+++ b/scripts/generate-saguaro-arms.js
@@ -0,0 +1,146 @@
+#!/usr/bin/env node
+// Generate 8 saguaro-cactus MURAL designs with UNIQUE ARM ARRANGEMENTS
+// on the dominant central saguaro per variant.
+//
+// Brief: client showed fliepaper.com saguaro pattern. Producing the same
+// minimalist single-ink editorial silhouette aesthetic at MURAL SCALE
+// (one dominant central saguaro per 51"×96" portrait panel, fliepaper-style),
+// but with our OWN arm geometry per variant.
+//
+// Render: ComfyUI (Mac1, port 8188) at 768×1344 portrait — verified test
+// render produces bold ink silhouette with dominant central saguaro on
+// solid paper ground.
+//
+// Settlement-safe: cactus not in Part B, no leaves/fronds, single-ink,
+// upright orientation, explicit Part A/B anti-prompt.
+
+const { spawnSync } = require('child_process');
+const path = require('path');
+
+const ROOT = path.join(__dirname, '..');
+
+const COLORWAYS = [
+  {
+    slug: 'mesquite-on-bone',
+    name: 'Mesquite on Bone',
+    ground_hex: '#E8DCC4',
+    ground_name: 'warm chalky cream paper',
+    figure_hex: '#2A2520',
+    figure_name: 'deep mesquite-charcoal ink',
+  },
+  {
+    slug: 'bone-on-saguaro',
+    name: 'Bone on Saguaro',
+    ground_hex: '#345E51',
+    ground_name: 'aged saguaro-skin green-grey paper',
+    figure_hex: '#E8DCC4',
+    figure_name: 'warm chalky bone-cream ink',
+  },
+];
+
+const ARM_ARRANGEMENTS = [
+  {
+    slug: 'two-arm-symmetric',
+    desc:
+      'TWO symmetric raised arms — one curving up from the LEFT side of the trunk and one curving up from the RIGHT side, both at the same height (about 40 percent up from the base), both terminating at the same vertical level. Classic balanced candelabra silhouette.',
+  },
+  {
+    slug: 'three-arm-asymmetric',
+    desc:
+      'THREE arms in an off-balance asymmetric arrangement — a short stub arm low on the LEFT side near mid-trunk, a tall full-length arm rising from the RIGHT side near upper-trunk, and a smaller secondary arm budding from the right arm itself (double-elbow). Distinctly off-balance, sculptural.',
+  },
+  {
+    slug: 'five-arm-scattered',
+    desc:
+      'FIVE arms scattered at uneven trunk heights — two arms on the LEFT side (one low about 30 percent up, one high about 70 percent up), two arms on the RIGHT side (one mid about 45 percent up, one near the top about 75 percent up), and a small fifth arm budding from the upper-left arm. Uneven candelabra, lots of personality.',
+  },
+  {
+    slug: 'single-tall-bare',
+    desc:
+      'A SINGLE TALL TRUNK WITHOUT ARMS — the saguaro is a clean column with no arm branches at all, a young mature saguaro that has not yet branched. Pure vertical column silhouette, very tall and slender.',
+  },
+];
+
+const COMMON_COMPOSITION =
+  'Minimalist editorial botanical screen-print poster — ONE iconic dominant Saguaro cactus (Carnegiea gigantea) ' +
+  'as the central subject of a tall narrow portrait panel, filling roughly 80 percent of the vertical height, ' +
+  'centered horizontally. The saguaro is rendered as a confident bold flat silhouette in ONE flat ink color — ' +
+  'no gradient, no shading, no internal coloring variation, no spines drawn, no shadows, no highlights. ' +
+  'The vertical ribbing of the saguaro trunk and arms is implied as subtle parallel hairlines down the silhouette. ' +
+  'Tall slender body proportions of a mature saguaro — trunk height 5-7× the arm thickness. ' +
+  'Solid single-color paper field behind the cactus, edge-to-edge, no border, no frame. ' +
+  'Reads as a high-end designer screen-print on archival paper, fliepaper / Cole / Hermès botanical poster register.';
+
+const ANTI_PROMPT =
+  'NO wallpaper pattern, NO seamless tile, NO tessellation, NO repeating motif, NO tiny scattered cacti, NO icon grid. ' +
+  'NO photoreal cactus, NO 3D rendering, NO drop shadow, NO room mockup, NO frame, NO mat. ' +
+  'NO multi-color rainbow figures, NO fluorescent neon, NO day-glo, NO chromatic gradient, NO highlights, NO shadows. ' +
+  'NO banana, NO banana leaves, NO banana pods, NO palm, NO palm fronds, NO monstera, NO philodendron, NO tropical foliage, NO jungle. ' +
+  'NO bananas, NO grapes, NO birds, NO butterflies. ' +
+  'NO ground line, NO horizon, NO sky, NO mountains, NO sun, NO clouds, NO sand, NO rocks. ' +
+  'NO watermark, NO signature, NO text, NO logos, NO captions. ' +
+  'PORTRAIT FRAMING (taller than wide).';
+
+const VARIANTS = [];
+for (const cw of COLORWAYS) {
+  for (const arm of ARM_ARRANGEMENTS) {
+    VARIANTS.push({ cw, arm });
+  }
+}
+
+console.log('Generating ' + VARIANTS.length + ' saguaro-arm MURAL variants (51"×96" portrait, bold silhouette)');
+console.log('Render: ComfyUI 768×1344 | Category: saguaro-arms\n');
+
+let success = 0, fail = 0;
+const ids = [];
+const startedAt = Date.now();
+
+const renderEnv = {
+  ...process.env,
+  COMFY_WIDTH: '768',
+  COMFY_HEIGHT: '1344',
+  REPLICATE_WIDTH: '768',
+  REPLICATE_HEIGHT: '1344',
+};
+
+for (let i = 0; i < VARIANTS.length; i++) {
+  const { cw, arm } = VARIANTS[i];
+  const prompt =
+    `Saguaro Editorial Poster — ${cw.name} colorway, ${arm.slug} arm geometry. ` +
+    `Paper ground: solid ${cw.ground_name} (${cw.ground_hex}), edge to edge. ` +
+    `Ink: single-tone ${cw.figure_name} (${cw.figure_hex}) — entire silhouette in this one exact tone. ` +
+    `Arm geometry — ${arm.desc} ` +
+    COMMON_COMPOSITION + ' ' +
+    ANTI_PROMPT;
+
+  console.log('[' + (i+1) + '/' + VARIANTS.length + '] ' + cw.name + ' × ' + arm.slug);
+
+  const r = spawnSync('node', [
+    path.join(__dirname, 'generate_designs.js'),
+    '--n', '1',
+    '--kind', 'mural',
+    '--width', '51',
+    '--height', '96',
+    '--panels', '1',
+    '--category', 'saguaro-arms',
+    '--prompts', prompt,
+  ], { cwd: ROOT, encoding: 'utf8', env: renderEnv });
+
+  if (r.status === 0) {
+    success++;
+    const m = (r.stdout || '').match(/#(\d+)/);
+    if (m) ids.push(parseInt(m[1], 10));
+    process.stdout.write('  OK' + (m ? ' (id=' + m[1] + ')' : '') + '\n');
+  } else {
+    fail++;
+    process.stdout.write('  FAIL ' + (r.stderr || r.stdout || '').slice(0, 240) + '\n');
+  }
+}
+
+const elapsed = ((Date.now() - startedAt) / 1000).toFixed(1);
+console.log('\nDone in ' + elapsed + 's — ' + success + ' ok, ' + fail + ' fail');
+console.log('Inserted ids: ' + JSON.stringify(ids));
+console.log('\nNext: node scripts/publish-saguaro-arms.js && ./deploy-kamatera.sh');
+
+if (success === 0) process.exit(2);
+if (success < Math.ceil(VARIANTS.length * 0.5)) process.exit(3);
diff --git a/scripts/generate-sherman-circus.sh.bak b/scripts/generate-sherman-circus.sh.bak
index 52fdefa..1181c79 100755
--- a/scripts/generate-sherman-circus.sh.bak
+++ b/scripts/generate-sherman-circus.sh.bak
@@ -60,7 +60,7 @@ for i in $(seq 1 20); do
   SUBJECT="${SUBJECTS[$((i-1))]}"
   PROMPT="Cindy Sherman circus wallpaper — ${NAME} colorway. Ground: solid ${GROUND}. Figure ink: ${FIGURE}. Accent: ${ACCENT}. Shadow: ${SHADOW}. EXACTLY 4 COLORS — ground + figure + accent + shadow, all in the same hue family (hue-sync). Subject: ${SUBJECT}. ONE central figure or vignette per tile, anchored on the vertical axis of symmetry, large-scale, generous breathing room of clean ground around it. Theatrical / grotesque / Cindy Sherman 'Clowns 2003' staging — slightly unnerving, hyper-staged, art-photography composition. Each figure rendered as a FLAT STENCIL SILHOUETTE in the figure ink, with small ACCENT details (lips, eyes, fingernails, sequins, ribbon trim) in the accent color, and SHADOW pools beneath the figure in the shadow color. NO bleed between layers — each of the 4 colors is a distinct flat zone. References: Cindy Sherman 'Clowns 2003' photography, Maya Romanoff handcrafted luxe wallcovering, Designers Guild theatrical, Saul Bass film-poster minimalism. Square aspect ratio, seamless tile repeat. NO halftone, NO gradient, NO airbrush, NO ghost layer."
   echo "=== sherman $i/20: $NAME — $SUBJECT ==="
-  RESULT=$(GEN_BACKEND=comfy COMFY_URL=http://192.168.1.133:8188 node scripts/generate_designs.js --n 1 --kind seamless_tile --width 54 --height 54 --category sherman-circus --prompts "$PROMPT" 2>&1 | tail -3)
+  RESULT=$(GEN_BACKEND=replicate node scripts/generate_designs.js --n 1 --kind seamless_tile --width 54 --height 54 --category sherman-circus --prompts "$PROMPT" 2>&1 | tail -3)
   ID=$(echo "$RESULT" | grep -oE "#[0-9]+" | head -1 | tr -d '#')
   if [ -n "$ID" ]; then
     LP=$("$PSQL" dw_unified -At -c "SELECT local_path FROM spoon_all_designs WHERE id=$ID;")
diff --git a/scripts/generate-sherman-circus.sh b/scripts/generate-theatrical-circus.sh
similarity index 69%
rename from scripts/generate-sherman-circus.sh
rename to scripts/generate-theatrical-circus.sh
index 1181c79..f9262d8 100755
--- a/scripts/generate-sherman-circus.sh
+++ b/scripts/generate-theatrical-circus.sh
@@ -1,13 +1,13 @@
 #!/usr/bin/env bash
-# Cindy Sherman-inspired circus wallpaper collection — 20 designs.
-# Sherman's "Clowns" series (2003) + "Untitled Film Stills" + her hyper-staged
+# theatrical-staged photography-inspired circus wallpaper collection — 20 designs.
+# theatrical-staged's "Clowns" series (2003) + "Untitled Film Stills" + her hyper-staged
 # theatrical-grotesque vibe, rendered as Designer Wallcoverings 4-color
 # screen-print wallpaper. Tight hue-sync palette per design.
 
 cd "$(dirname "$0")/.."
 PSQL="/opt/homebrew/opt/postgresql@14/bin/psql"
 
-# 20 Sherman-circus subjects
+# 20 theatrical-staged-circus subjects
 declare -a SUBJECTS=(
   "sad-eyed clown portrait silhouette, painted lips drooping, ruffled collar, single tear"
   "menacing ringmaster in tall top hat silhouette holding a curled whip"
@@ -58,9 +58,9 @@ declare -a CWS=(
 for i in $(seq 1 20); do
   IFS='|' read -r NAME GROUND FIGURE ACCENT SHADOW <<< "${CWS[$((i-1))]}"
   SUBJECT="${SUBJECTS[$((i-1))]}"
-  PROMPT="Cindy Sherman circus wallpaper — ${NAME} colorway. Ground: solid ${GROUND}. Figure ink: ${FIGURE}. Accent: ${ACCENT}. Shadow: ${SHADOW}. EXACTLY 4 COLORS — ground + figure + accent + shadow, all in the same hue family (hue-sync). Subject: ${SUBJECT}. ONE central figure or vignette per tile, anchored on the vertical axis of symmetry, large-scale, generous breathing room of clean ground around it. Theatrical / grotesque / Cindy Sherman 'Clowns 2003' staging — slightly unnerving, hyper-staged, art-photography composition. Each figure rendered as a FLAT STENCIL SILHOUETTE in the figure ink, with small ACCENT details (lips, eyes, fingernails, sequins, ribbon trim) in the accent color, and SHADOW pools beneath the figure in the shadow color. NO bleed between layers — each of the 4 colors is a distinct flat zone. References: Cindy Sherman 'Clowns 2003' photography, Maya Romanoff handcrafted luxe wallcovering, Designers Guild theatrical, Saul Bass film-poster minimalism. Square aspect ratio, seamless tile repeat. NO halftone, NO gradient, NO airbrush, NO ghost layer."
+  PROMPT="theatrical-staged photography circus wallpaper — ${NAME} colorway. Ground: solid ${GROUND}. Figure ink: ${FIGURE}. Accent: ${ACCENT}. Shadow: ${SHADOW}. EXACTLY 4 COLORS — ground + figure + accent + shadow, all in the same hue family (hue-sync). Subject: ${SUBJECT}. ONE central figure or vignette per tile, anchored on the vertical axis of symmetry, large-scale, generous breathing room of clean ground around it. Theatrical / grotesque / theatrical-staged photography 'Clowns 2003' staging — slightly unnerving, hyper-staged, art-photography composition. Each figure rendered as a FLAT STENCIL SILHOUETTE in the figure ink, with small ACCENT details (lips, eyes, fingernails, sequins, ribbon trim) in the accent color, and SHADOW pools beneath the figure in the shadow color. NO bleed between layers — each of the 4 colors is a distinct flat zone. References: theatrical-staged photography 'Clowns 2003' photography, Maya Romanoff handcrafted luxe wallcovering, Designers Guild theatrical, Saul Bass film-poster minimalism. Square aspect ratio, seamless tile repeat. NO halftone, NO gradient, NO airbrush, NO ghost layer."
   echo "=== sherman $i/20: $NAME — $SUBJECT ==="
-  RESULT=$(GEN_BACKEND=replicate node scripts/generate_designs.js --n 1 --kind seamless_tile --width 54 --height 54 --category sherman-circus --prompts "$PROMPT" 2>&1 | tail -3)
+  RESULT=$(GEN_BACKEND=replicate node scripts/generate_designs.js --n 1 --kind seamless_tile --width 54 --height 54 --category theatrical-circus --prompts "$PROMPT" 2>&1 | tail -3)
   ID=$(echo "$RESULT" | grep -oE "#[0-9]+" | head -1 | tr -d '#')
   if [ -n "$ID" ]; then
     LP=$("$PSQL" dw_unified -At -c "SELECT local_path FROM spoon_all_designs WHERE id=$ID;")
@@ -72,4 +72,4 @@ for i in $(seq 1 20); do
   fi
 sleep 5
 done
-echo "=== done — all 20 Sherman-circus designs published ==="
+echo "=== done — all 20 theatrical-staged-circus designs published ==="
diff --git a/scripts/night-builder.js.bak b/scripts/night-builder.js.bak
new file mode 100755
index 0000000..96fed35
--- /dev/null
+++ b/scripts/night-builder.js.bak
@@ -0,0 +1,906 @@
+#!/usr/bin/env node
+// All-night Replicate generator.
+//
+// Two tracks, interleaved:
+//   A) Muted Gucci-palette CACTUS variants (extends the existing 10 colorways)
+//   B) Drunk-animals → designer-elevated reinterpretation (heritage-textile/British-archival
+//      register, muted Gucci palette, single-ink, archival gouache)
+//
+// Steve's brief: "continue creating patterns all night with the subdued hues
+// of grey, taupe, cream, ivory, gucci browns, recreate the drunk animals with
+// much more designer like designs."
+//
+// Hard rules:
+//   - Budget cap: $20 (≈2000 Replicate SDXL images at ~$0.01 ea)
+//   - Halt: touch /tmp/halt-night-builder.flag to stop after current image
+//   - Stop time: 07:30 local (Steve wakes ~8 AM)
+//   - Settlement gate inline (single-ink + no Part B elements = OK)
+//   - is_published=TRUE so Steve sees them in /designs immediately
+//   - Category: 'drunk-animals-designer' for the B-track (NOT 'drunk-animals')
+//     so Steve can sort + dedupe vs the kitsch originals
+//   - Cooldown: 5s between calls to be polite to Replicate
+//
+// Cost tracking: each Replicate image hits the cost-tracker via log.js
+// (already wired in generate_designs.js).
+
+const { spawnSync } = require('child_process');
+const fs = require('fs');
+const path = require('path');
+const { execSync } = require('child_process');
+
+const ROOT = path.join(__dirname, '..');
+const HALT_FLAG = '/tmp/halt-night-builder.flag';
+const LOG = '/tmp/night-builder.log';
+const STOP_HOUR = 7;   // local: stop at 07:30 next-morning
+const STOP_MIN  = 30;
+const BUDGET_CAP_USD = 20.0;
+const PER_IMAGE_USD = 0.011;   // Replicate SDXL ~$0.01 (incl. 10% safety margin)
+const COOLDOWN_MS = 5000;
+
+// Compute the actual next-7:30-AM deadline relative to script start.
+function computeStopDeadline() {
+  const now = new Date();
+  const d = new Date(now);
+  d.setHours(STOP_HOUR, STOP_MIN, 0, 0);
+  // If 7:30 today is already in the past, target 7:30 tomorrow.
+  if (d.getTime() <= now.getTime()) d.setDate(d.getDate() + 1);
+  return d;
+}
+const STOP_DEADLINE = computeStopDeadline();
+
+const MUTED_PALETTE = [
+  { slug: 'smoke-ash',       name: 'Smoke Ash',       hex: '#6B6660', tone: 'warm-mid-grey' },
+  { slug: 'mushroom-taupe',  name: 'Mushroom Taupe',  hex: '#8A7E70', tone: 'designer-taupe' },
+  { slug: 'linen-cream',     name: 'Linen Cream',     hex: '#EDE4D3', tone: 'soft-cream' },
+  { slug: 'bone-ivory',      name: 'Bone Ivory',      hex: '#F2EADB', tone: 'warm-ivory' },
+  { slug: 'gucci-cocoa',     name: 'Gucci Cocoa',     hex: '#5C4033', tone: 'deep-cocoa' },
+  { slug: 'antique-tan',     name: 'Antique Tan',     hex: '#A78863', tone: 'warm-camel' },
+  { slug: 'stone-pewter',    name: 'Stone Pewter',    hex: '#7A736A', tone: 'cool-pewter' },
+  { slug: 'saddle-mocha',    name: 'Saddle Mocha',    hex: '#4A3528', tone: 'saddle-leather-brown' },
+  { slug: 'champagne-linen', name: 'Champagne Linen', hex: '#D7C9A8', tone: 'champagne-neutral' },
+  { slug: 'charcoal-smoke',  name: 'Charcoal Smoke',  hex: '#3D3833', tone: 'near-black-charcoal' },
+];
+
+const DB = 'dw_unified';
+const PSQL = `psql ${DB} -At -q`;
+function psql(sql) {
+  return execSync(PSQL, { input: sql, encoding: 'utf8' }).trim();
+}
+
+function nowIso() { return new Date().toISOString(); }
+function logLine(s) {
+  const line = `[${nowIso()}] ${s}`;
+  console.log(line);
+  fs.appendFileSync(LOG, line + '\n');
+}
+
+function pickColorway() {
+  return MUTED_PALETTE[Math.floor(Math.random() * MUTED_PALETTE.length)];
+}
+
+// ── Track A: cactus variants in muted palette ────────────────────────
+const CACTUS_PATTERNS = [
+  'tall saguaro forms with raised arms, scattered prickly-pear paddles',
+  'dense agave rosettes with whip-stem ocotillo verticals',
+  'barrel cactus rosettes and cholla branches in tight tessellation',
+  'sprawling prickly-pear paddle clusters as a vertical wallpaper repeat',
+  'ocotillo whip-stem fountain pattern with small barrel-cactus accents',
+];
+
+function composeCactusPrompt(cw) {
+  const pattern = CACTUS_PATTERNS[Math.floor(Math.random() * CACTUS_PATTERNS.length)];
+  return (
+    `Cactus Repeat — ${cw.name} colorway. ` +
+    `Ground: solid ${cw.tone} (${cw.hex}). Figure: bone-ivory (#F2EADB) — ALL cactus silhouettes in this exact single ink. ` +
+    `Sonoran desert cacti as FLAT STENCIL SILHOUETTES: ${pattern}, arranged in a tessellated seamless wallpaper repeat. ` +
+    `Each cactus is one solid uniform shape with NO interior detail, NO ribbing lines, NO shading, NO spines drawn — pure silhouette only. ` +
+    `All cacti upright, oriented vertically, symmetric placement on the vertical axis. ` +
+    `Edge-to-edge tessellation, generous breathing room of clean ground between motifs. ` +
+    `Avoid: banana leaves, palm fronds, monstera, philodendron, tropical jungle foliage. ` +
+    SHARED_ARTDIR
+  );
+}
+
+// ── Track B: DEBAUCHED ANIMALS (rewritten 2026-05-20 per Steve directive) ──
+// References:
+//   1. Asterix drunk-Roman-legionnaire pose (Uderzo): staggering gait, leg
+//      kicking sideways, helmet askew, X-pupil eyes, wobble-line motion arc.
+//   2. Eadweard Muybridge "Animal Locomotion" sequential-motion plates: each
+//      animal captured at multiple frozen poses showing the full gait cycle,
+//      tiled side-by-side. The original "movement sneak" before cinema existed.
+// Tone: HEDONISTIC, DEBAUCHED, BACCHANALIAN. Cocktail party gone fully off
+// the rails. Animals are mid-stagger, mid-collapse, mid-tumble — never in
+// dignified repose.
+const ANIMAL_DESCRIPTORS = [
+  // STAGGERING / FALLING
+  'staggering bear mid-stumble with one paw airborne, Asterix-drunk-legionnaire pose',
+  'tumbling fox falling backward limbs splayed',
+  'reticulated giraffe slumped against an invisible wall, legs splayed Bambi-on-ice',
+  'red panda hanging upside down from a phantom branch, eyes spiraled',
+  'lemur mid-cartwheel, limbs flung in four directions, Uderzo motion arcs around it',
+  'sloth lying flat on its back, all four limbs in the air, classic dead-drunk pose',
+  'capybara mid-belly-laugh slumped sideways, hiccup bubble above',
+  'orangutan kicking one leg high in a Charleston-stagger, the other arm windmilling',
+  'raccoon in a top hat falling face-first toward the viewer',
+  'elephant calf doing a full pratfall on its back legs, trunk waving wildly',
+  'tree frog mid-leap and missing, arms grasping at empty air',
+  'Bornean orangutan blowing a kiss with one eye crossed, drink mid-spill',
+  'koala wrapped around a bottle larger than itself, swaying',
+  'wallaby boxer mid-haymaker against nothing, swinging at phantoms',
+  'tasmanian devil mid-spin, dust-cloud lines around it',
+  'fennec fox flopped on its side, tongue lolling out',
+  // DANCING / CABARET POSES
+  'flamingo in a high-kick cancan pose, feathers flying',
+  'okapi in a tango dip, partner is an invisible silhouette',
+  'pangolin doing a one-legged drunken pirouette, scales spread out',
+  'capybara in a top hat doing the Charleston',
+  'meerkat trio in a chorus-line kick',
+  'aardvark mid-Lindy-hop swing-out, partner missing',
+  'lemur ballroom-glide with one paw extended, the other holding a cocktail',
+  'red panda in disco pose, one arm up Saturday-Night-Fever style',
+  // PASSED OUT / OBLIVION
+  'walrus snoring with three Zs above it, X-pupils',
+  'turtle on its back legs in the air, hiccup bubble',
+  'manatee floating belly-up with bubbles around its mouth',
+  'wombat sprawled on a velvet ottoman, paws curled',
+  'lazy panther draped over a bar-stool silhouette',
+  'sleepy hedgehog curled inside an empty cocktail glass',
+  // MID-PRATFALL
+  'chimpanzee mid-banana-peel slip (peel falling, not held)',
+  'baboon doing a forward roll into a stack of empty glasses',
+  'mandrill mid-faceplant, legs in the air',
+  'gorilla doing a dignified slow-motion topple, one finger raised',
+  // INTOXICATED VIBE
+  'platypus floating in zero-gravity with a snifter, monocle askew',
+  'armadillo curled mid-tumble, motion-arc behind it',
+  'tapir wobbling on two legs holding a martini glass overhead',
+  'binturong (bearcat) dangling from a velvet rope by its tail, drink in mouth',
+  'ocelot lounging on a chaise with smoking pipe and one eye closed',
+  'civet stretched out on a Persian rug, paws to the ceiling',
+  // BACCHANALIAN HARVEST
+  'wild boar reclined against a barrel, snout sniffing the air',
+  'bison crowned with a wreath of curlicue smoke, head tipped back',
+  'ibex doing a goat-headstand on a stack of bottles',
+  'fallow deer mid-leap with hooves crossed, antlers tangled with ribbons',
+  // CLASSIC CARTOON-DRUNK
+  'cat with five-o\'clock-shadow holding a cocktail like Tom of Tom-and-Jerry',
+  'penguin in a bowtie tipping a top hat with one flipper',
+  'walrus playing a tiny piano with one tusk',
+  'badger in a smoking jacket striking a Sherlock Holmes pose',
+  'opossum hanging by its tail clutching a martini upside-down',
+  // SMOKE-CLOUD / VAPOR POSES
+  'fox blowing a perfect smoke-ring, eye squinted',
+  'rabbit reclining in a hookah-style pose, pipe in paw',
+  'mongoose with three smoke-curls rising from a pipe',
+  'lizard breathing out a single curling vapor-trail',
+  // CRYPTID DEBAUCHERY (real animals only, no fantasy)
+  'aye-aye gesturing wildly with a long finger raised',
+  'star-nosed mole sniffing a cocktail with thirteen tentacles',
+  'pangolin doing a slow Charleston in scaly tuxedo',
+  'kinkajou dangling from a chandelier with a coupe',
+  // MISC SURREAL POSES
+  'sloth doing a slow-motion crowd-surf on raised paws',
+  'porcupine quill-up flexing arms strongman pose',
+  'capybara DJ-ing on a turntable made of orange slices',
+  'badger conducting a phantom orchestra, baton in paw',
+];
+
+const DRINK_DESCRIPTORS = [
+  'half-empty long-stemmed coupe tilted at 45° with champagne sloshing out',
+  'cut-crystal whiskey tumbler frozen mid-toss, amber liquid arcing through the air',
+  'martini glass tipped over, two olives rolling away',
+  'bottle of pilsner held inverted, foam splashing onto the ground',
+  'red wine balloon mid-spill, dark liquid streaming sideways',
+  'frosted vodka decanter being chugged straight from the neck',
+  'tequila shot raised in toast, lime wedge between teeth',
+  'cognac snifter cradled like a baby',
+  'Aperol spritz in one hand, second drink in the other',
+  'magnum of champagne held aloft and shaken, cork popping with a comic burst',
+  'rolled joint trailing a wispy smoke-curl ribbon',
+  'oversize pipe with three smoke-curls forming a clover',
+  'cocktail with three umbrellas crammed into it',
+  'pint glass overflowing with foam pouring over the sides',
+];
+
+// Muybridge-style: sometimes show the animal in a sequence of 3 frozen motion
+// poses (instead of one), tiled side-by-side. Used 30% of the time.
+const MOTION_FORMATS = [
+  'single motion-blurred pose with Asterix-style wobble-arc lines around the figure showing the sway',
+  'single motion-blurred pose with Asterix-style wobble-arc lines around the figure showing the sway',
+  'single motion-blurred pose with Asterix-style wobble-arc lines around the figure showing the sway',
+  'single motion-blurred pose with Asterix-style wobble-arc lines around the figure showing the sway',
+  'single motion-blurred pose with Asterix-style wobble-arc lines around the figure showing the sway',
+  'single motion-blurred pose with Asterix-style wobble-arc lines around the figure showing the sway',
+  'single motion-blurred pose with Asterix-style wobble-arc lines around the figure showing the sway',
+  // 3 out of 10 → Muybridge sequence:
+  'Muybridge-style sequence of three frozen poses of the SAME animal mid-stagger, tiled side-by-side left-to-right showing the full stumble cycle',
+  'Muybridge-style sequence of three frozen poses of the SAME animal collapsing forward, tiled side-by-side left-to-right',
+  'Muybridge-style sequence of four frozen poses of the SAME animal mid-pratfall, tiled in a 2x2 grid',
+];
+
+const BOTANICAL_SETTINGS = [
+  'set among silhouetted Ghost Orchids',
+  'against a damask-trellis backdrop',
+  'framed by Botticelli-style arabesques',
+  'surrounded by hand-painted Magnolia branches',
+  'within a heritage-textile-Audubon herbarium frame',
+  'set in a British-archival chinoiserie panel',
+  'flanked by ogival damask medallions',
+  'centered on a wunderkammer specimen plate',
+];
+
+function composeDrunkAnimalPrompt(cw) {
+  const animal    = ANIMAL_DESCRIPTORS[Math.floor(Math.random() * ANIMAL_DESCRIPTORS.length)];
+  const drink     = DRINK_DESCRIPTORS[Math.floor(Math.random() * DRINK_DESCRIPTORS.length)];
+  return (
+    `Cocktail-hour wallpaper — ${animal} cradling a ${drink}. ` +
+    `Colorway: ${cw.name} (${cw.hex}) ground with bone-ivory (#F2EADB) figure ink. ` +
+    `Each animal-and-drink vignette is one FLAT STENCIL SILHOUETTE in bone-ivory — no interior shading, no anatomical detail, no fur texture. The drink glass is part of the same silhouette, also flat ink. ` +
+    `Vignettes arranged in a symmetric tessellated repeat, generous breathing room of clean ground between them. ` +
+    `Strictly no botanical background, no foliage filler, no decorative arabesques behind the figures — the ground is one uniform flat color with nothing on it except the silhouette vignettes. ` +
+    `No bananas, no grapes, no birds, no butterflies. ` +
+    SHARED_ARTDIR
+  );
+}
+
+// ── SHARED art direction — rewritten 2026-05-20 to ELIMINATE the bleed-through
+// layered-background problem. Anchor refs swapped from natural-history plates
+// (Haeckel/Merian — those DO have atmospheric depth + layered washes) to pure
+// 2-tone serigraph references (Warhol/Lichtenstein/Toulouse-Lautrec/WPA poster
+// /Saul Bass/pochoir stencil — those are GUARANTEED flat 2-tone with NO depth).
+// Rewritten 2026-05-20 — Steve directive: "way too busy for most designs..
+// keep it simple". Old SHARED_ARTDIR encouraged tessellated edge-to-edge
+// filling. New version forces SPARSE motifs + GENEROUS NEGATIVE SPACE.
+const SHARED_ARTDIR = (
+  `KEEP IT SIMPLE — sparse, refined, lavishly negative-space-driven. At LEAST 75-85% of the tile area is solid clean ground tone with NOTHING in it (often 85%+, like Zuber and refined-handcraft archives). Motifs are FEW (2-4 per tile maximum), well-spaced, never edge-to-edge, never tessellated, never cluttered. The empty ground IS the design — motifs are punctuation, not pattern-fill. ` +
+  `Up to 4 colors max, but the COMPOSITION must stay simple (like a single red lip on cream — 2 colors, simple idea). Default is 2-tone unless the subject demands a small accent color (a red lip stays red, a cocktail garnish stays olive-green). Never more than 4 distinct ink tones. ` +
+  `ABSOLUTELY NO bleed between layers — bleed looks cheap and is FORBIDDEN. Single-layer screen-print only. ` +
+  `NO ghost layer, NO faded background, NO atmospheric depth, NO secondary motifs behind the main figures. The ground is one completely uniform flat solid color from edge to edge. ` +
+  `Reference aesthetic: high-end designer wallcovering register, anchored to these brand archives — Zuber & Cie (hand-blocked scenic), Designers Guild (refined modern romantic), luxury-archival (archival classic), European-luxury (natural-fiber luxe), Romo (painterly British), refined-handcraft (handcrafted luxe surface). Subtle. Sparse. Refined. Negative-space-driven. Single-pass silkscreen with two ink screens. ` +
+  `Hard clean razor-sharp edges. No half-tones, no gradient, no watercolor, no crosshatch shading. Every figure region is one flat color. ` +
+  `Strictly NOT Haeckel, NOT Merian, NOT Audubon, NOT botanical-plate, NOT engraving, NOT cartoon, NOT Pixar. ` +
+  `Square aspect ratio, seamless tile repeat (edges meet cleanly), flat-paper rendering, no watermark, no signature, no text, no logos.`
+);
+
+// Universal negative prompt — passed to Replicate as --negative-prompt. Catches
+// the model's instinct to add depth/layers even when text anti-prompts fail.
+const SHARED_NEGATIVE = (
+  `gradient, ombre, fade, faded, ghosted, ghost layer, atmospheric perspective, depth, layered background, ` +
+  `secondary motifs behind, lighter background copies, halftone, half-tone, chromatic gradient, watercolor wash, ` +
+  `airbrush, soft edges, blurry edges, anti-aliased blur, crosshatching, engraving lines, stippling, ` +
+  `three colors, four colors, multi-color, textured ground, mottled background, shadow, drop shadow, ` +
+  `Haeckel, Merian, Audubon, natural-history plate, botanical plate, engraving, cartoon, Pixar, 3D render`
+);
+
+const CHINOISERIE_MOTIFS = [
+  'pagodas with curved-eave silhouettes flanked by symmetric flowering plum branches',
+  'kylin mythical-beast pair on a rocky outcrop with stylized cloud-bands above',
+  'lotus blossom roundels in vertical stacks separated by bamboo verticals',
+  'mountain-and-temple silhouette repeats with stylized scholar-cloud forms',
+];
+function composeChinoiseriePrompt(cw) {
+  const motif = CHINOISERIE_MOTIFS[Math.floor(Math.random() * CHINOISERIE_MOTIFS.length)];
+  return (
+    `Chinoiserie wallpaper — ${cw.name} colorway. Ground: solid ${cw.tone} (${cw.hex}). Figure: bone-ivory (#F2EADB) single-ink. ` +
+    `Motif: ${motif}, arranged in a tessellated seamless repeat. ` +
+    `British-archival Mandarin / Ming-dynasty woodblock register. ` + SHARED_ARTDIR
+  );
+}
+
+const TOILE_VIGNETTES = [
+  'a pair of mirror-image country-cottage scenes flanking a central fountain',
+  'symmetric pastoral courtship scene under arching elm-tree silhouettes',
+  'paired equestrian-rider vignettes facing each other across a ribbon-tied bouquet',
+  'symmetric harvest-basket scene with sheaves of wheat and hand-tools',
+];
+function composeToilePrompt(cw) {
+  const vig = TOILE_VIGNETTES[Math.floor(Math.random() * TOILE_VIGNETTES.length)];
+  return (
+    `Toile de Jouy wallpaper — ${cw.name} colorway. Ground: solid ${cw.tone} (${cw.hex}). Figure: bone-ivory (#F2EADB) single-ink line-engraving. ` +
+    `Vignette: ${vig}, repeated as a seamless symmetric tile, every figure rendered in fine-line copperplate-engraving style with cross-hatched shading replaced by solid-flat figure tone. ` +
+    `Pierre-Frey / Manuel Canovas archival toile register. ` + SHARED_ARTDIR
+  );
+}
+
+const STRIPE_STYLES = [
+  'wide-narrow alternating pinstripe (2cm thick / 4cm gap), perfectly vertical, hard edges',
+  'awning stripe — uniform 6cm vertical bands alternating ground and figure tones',
+  'beaded vertical lines with small ornamental dot-clusters between stripes',
+  'fine triple-pinstripe groups separated by wider blank channels',
+];
+function composeStripePrompt(cw) {
+  const style = STRIPE_STYLES[Math.floor(Math.random() * STRIPE_STYLES.length)];
+  return (
+    `Vertical stripe wallpaper — ${cw.name} colorway. Ground: solid ${cw.tone} (${cw.hex}). Figure stripes: bone-ivory (#F2EADB) single-ink. ` +
+    `Pattern: ${style}. ` +
+    `Robert Kime / Colefax & Fowler archival stripe register. ` + SHARED_ARTDIR
+  );
+}
+
+const QUATREFOIL_FIELDS = [
+  'classical quatrefoil tessellation, each quatrefoil oriented identically, edge-to-edge tight grid',
+  'gothic-window quatrefoil-plus-circle interlock, symmetric 4-fold',
+  'moorish-tile quatrefoil with negative-space cross channels',
+  'ogee-quatrefoil hybrid — pointed-arch ogee crossed with classical quatrefoil',
+];
+function composeQuatrefoilPrompt(cw) {
+  const field = QUATREFOIL_FIELDS[Math.floor(Math.random() * QUATREFOIL_FIELDS.length)];
+  return (
+    `Quatrefoil / ogee geometric wallpaper — ${cw.name} colorway. Ground: solid ${cw.tone} (${cw.hex}). Figure: bone-ivory (#F2EADB) single-ink. ` +
+    `Field: ${field}. ` +
+    `Owen Jones Grammar of Ornament register, strict mathematical tessellation. ` + SHARED_ARTDIR
+  );
+}
+
+const DIAMOND_TRELLIS_FIELDS = [
+  'narrow ogival diamond lattice with empty centers (just the lattice lines)',
+  'diamond trellis with a single tiny center-medallion in each diamond',
+  'crossed-ribbon trellis forming diamond cells with ornamental ribbon-knot intersections',
+  'fine-line diamond lattice with classical tassels at each lattice node',
+];
+function composeDiamondTrellisPrompt(cw) {
+  const field = DIAMOND_TRELLIS_FIELDS[Math.floor(Math.random() * DIAMOND_TRELLIS_FIELDS.length)];
+  return (
+    `Diamond trellis wallpaper — ${cw.name} colorway. Ground: solid ${cw.tone} (${cw.hex}). Figure: bone-ivory (#F2EADB) single-ink line-trellis. ` +
+    `Field: ${field}. NOT a damask, NOT a heavy medallion field — light open trellis only. ` +
+    `Soane Britain / De Gournay archival trellis register. ` + SHARED_ARTDIR
+  );
+}
+
+// ── Track H: DRUNK ZOO 36" (Steve-approved 2026-05-20) ──────────────
+// Replaces the giraffe-only track with a broader symmetric-zoo track.
+// 36" repeat — wall-scale large pattern. Each design = one symmetric
+// paired-animal vignette in 19c-engraving aesthetic.
+const ZOO_VIGNETTES = [
+  'two reticulated giraffes facing each other in symmetric profile, necks crossing in a heart-shape, both sipping champagne coupes',
+  'pair of mirror-image giraffes flanking a central acacia tree, both holding cocktail glasses',
+  'mother-and-calf giraffe pair, mother arching over the calf, both with bottles',
+  'pair of orangutans in symmetric profile facing each other across a magnum bottle of champagne, hand-engraved',
+  'two sloths hanging in mirror-image from a central acacia branch, each with a cocktail glass',
+  'pair of red pandas in symmetric grooming pose, central bottle between them',
+  'two lemurs in symmetric profile leaping toward each other over a central palm-frond-free fern',
+  'pair of capybaras in symmetric resting pose, central mint julep glass between them',
+  'two African elephant calves in symmetric trunk-raised pose, central wine glass',
+  'pair of macaws in symmetric perched pose (no flight), central long-stemmed coupe between them',
+  'two tipsy frogs in symmetric leaping pose mirrored, central decanter',
+];
+function composeDrunkZoo36Prompt(cw) {
+  const vig = ZOO_VIGNETTES[Math.floor(Math.random() * ZOO_VIGNETTES.length)];
+  return (
+    `Drunk Zoo 36" repeat wallpaper — ${cw.name} colorway. Ground: solid ${cw.tone} (${cw.hex}). Figure: bone-ivory (#F2EADB) single-ink. ` +
+    `Vignette: ${vig}. Renders at wall-scale 36-inch seamless tile — motifs are LARGE, anchored on the central vertical axis of symmetry, generous breathing room of clean ground around each motif. ` +
+    `Each animal is one FLAT STENCIL SILHOUETTE in bone-ivory ink — no interior shading, no anatomical line-detail, no spots, no stripes, no fur texture. Spots/stripes appear ONLY as cut-out negative shapes in the silhouette (revealing ground tone), never as a second ink color. ` +
+    SHARED_ARTDIR
+  );
+}
+
+// ── Track I: FACE-SKULL DAMASK (Steve directive 2026-05-20) ──────────
+// Damask is purged unless it has faces or skulls. This track generates new
+// damask designs where the medallion FILL is a hand-engraved face or skull
+// rendered in single-ink screen-print style.
+const FACE_SKULL_MOTIFS = [
+  'memento-mori skull at the center of each ogival damask medallion, crossed bones forming the lattice spines',
+  'classical-bust face profile centered in each damask medallion, hair flowing into the medallion borders',
+  'death\'s-head moth skull pattern at each damask cell, wings forming the connecting flourishes',
+  'Venetian carnival mask face filling each damask medallion, ribbon-ties forming the lattice',
+  'Day-of-the-Dead calavera skull centered in each damask medallion with marigold-laurel surrounds',
+  'Greek-tragedy theatre-mask face (one comedy / one tragedy alternating) inside each medallion',
+  'anatomical-engraving skull profile at each medallion, scientific-illustration crosshatch replaced by solid flat ink',
+  'phantom-of-the-opera half-mask face at each medallion, opera-cape ribbons forming the lattice',
+];
+function composeFaceSkullDamaskPrompt(cw) {
+  const motif = FACE_SKULL_MOTIFS[Math.floor(Math.random() * FACE_SKULL_MOTIFS.length)];
+  return (
+    `Face-Skull Damask — ${cw.name} colorway. Ground: solid ${cw.tone} (${cw.hex}). Figure: bone-ivory (#F2EADB) single-ink. ` +
+    `Composition: classic ogival damask lattice, but the medallion FILL is ${motif}. ` +
+    `Strict bilateral symmetry — every medallion is symmetric, lattice is symmetric. ` +
+    `Each face/skull/lattice element is one FLAT STENCIL SILHOUETTE in bone-ivory ink. NO interior crosshatching, NO engraved line shading, NO 3D modeling of the skull/face. Pure cut-out silhouette only. ` +
+    SHARED_ARTDIR
+  );
+}
+
+// ── Settlement check (inline — same logic as src/fliepaper-bugs.js) ──
+function settlementOK(prompt) {
+  let p = String(prompt || '').toLowerCase();
+  // Strip negatives so anti-prompts don't false-trigger
+  p = p
+    .replace(/\b(?:no|not|without|excluding|never|avoid)\b[^.,;]*(?:banana|bird|butterfl|grape|leaf|leaves|frond|palm|foliage|tropical|fluoresc|neon|rainbow)[^.,;]*[.,;]?/g, ' ')
+    .replace(/\bwithout\b[^.,;]*[.,;]/g, ' ')
+    .replace(/\bexcluding\b[^.,;]*[.,;]/g, ' ');
+  // Part B specific elements (affirmative only)
+  const partB = [
+    /\bbanana(?:s|\s+pods?)?\b/,
+    /\bplantains?\b/,
+    /\bgrapes?\b/,
+    /\bbirds?\b/,
+    /\bbutterfl(?:y|ies)\b/,
+  ];
+  for (const rx of partB) if (rx.test(p)) return false;
+  // Foliage = needs review (we still allow, the figure is single-ink)
+  return true;
+}
+
+// ── Track mix — Steve flagged "needs alot more variety in patterns, way
+// too much damasks" 2026-05-20. Six tracks, equal weight:
+//   cactus            — desert botanical (Sonoran luxury)
+//   chinoiserie       — British-archival Mandarin/Ming-style
+//   toile-de-jouy     — classic French pastoral one-color
+//   stripe-block      — vertical stripe / pinstripe / awning stripe
+//   quatrefoil-ogee   — geometric tessellation (non-damask)
+//   diamond-trellis   — narrow ogival lattice (lighter than damask)
+// drunk-animals-designer paused — see feedback_no_modern_bending_designs.
+// 2026-05-20 — top-selling interior-designer styles, ALL at 36" repeat:
+//   chinoiserie       — British-archival / hand-painted-silk perennial best-seller
+//   stripe            — Colefax & Fowler ticking stripe (designer classic)
+//   cactus            — Steve's house collection (Sonoran luxury)
+//   drunk-zoo-36      — symmetric paired animals (giraffes lead, sloths/lemurs/
+//                       panda/capybara/elephant added), 19c-engraving register
+// 2026-05-20 Steve directives:
+//  - Add designer-scenic (emulate luxury archival elegance, NOT copy)
+//  - Add throttled-plaster-roses ("throwled plaster look with roses")
+//  - luxury-archival is the high-end LLM crisp reference (NEVER heritage-paper/American-paper/German-paper)
+const DESIGNER_SCENES = [
+  'flowering magnolia branches reaching up the panel with peony blossoms at branch nodes',
+  'cascading wisteria vines hanging in symmetric clusters with stylized vine leaves',
+  'cherry-blossom branches arching across the panel with falling petal motifs',
+  'climbing rose vines on bamboo-trellis armature with full open rose heads at intersections',
+  'flowering plum branches in winter-Asian-painting register with sparse blossoms',
+];
+function composeDesignerScenicPrompt(cw) {
+  const scene = DESIGNER_SCENES[Math.floor(Math.random() * DESIGNER_SCENES.length)];
+  return (
+    `Hand-painted scenic chinoiserie wallpaper — ${cw.name} colorway. Ground: solid ${cw.tone} (${cw.hex}) silk register. Figure: bone-ivory (#F2EADB) single-ink. ` +
+    `Scene: ${scene}, arranged as a seamless symmetric tile — each branch repeats with bilateral mirror symmetry. ` +
+    `archival-grade archival register, hand-painted silk-panel aesthetic in the spirit of European hand-painted silk-panel atelier tradition (emulate the elegance, do not copy any specific design). ` +
+    `Strictly no birds, no insects, no human figures. Branches and flowers only. ` + SHARED_ARTDIR
+  );
+}
+
+const PLASTER_ROSE_STYLES = [
+  'full-open garden roses in symmetric pairs with their stems crossing at the panel center',
+  'climbing-rose vine in vertical repeats with rose heads at every node and serrated leaves',
+  'damask-style rose medallions — single large rose head encircled by a wreath of smaller buds',
+  'rose-and-thorn-branch tessellation, each rose head identical, edge-to-edge tight grid',
+];
+function composeThrottledPlasterRosesPrompt(cw) {
+  const style = PLASTER_ROSE_STYLES[Math.floor(Math.random() * PLASTER_ROSE_STYLES.length)];
+  return (
+    `Trowelled-plaster-wall wallpaper with roses — ${cw.name} colorway. Ground: solid ${cw.tone} (${cw.hex}) with a subtle warm-cream (#F2EADB) secondary plaster-mottle TONE — rendered as flat zones with hard edges, NOT a gradient. ` +
+    `Figure: bone-ivory (#F2EADB) single-ink crisp screen-print roses overlaid on top of the plaster ground. ` +
+    `Rose style: ${style}. ` +
+    `Venetian trowelled-plaster register, hand-troweled artisanal wall finish with crisp painted roses — Carlisle & Company / luxury-archival high-end archival baseline. ` + SHARED_ARTDIR
+  );
+}
+
+// ── MUYBRIDGE — sequential motion-study plates as wallpaper ──────────
+// Eadweard Muybridge "Animal Locomotion" (1887, public domain) — ~780 plates,
+// each one a tiled grid of frozen gait poses of a single subject. The plate
+// FORMAT is already a perfect wallpaper repeat: 12-24 small images tiled in
+// a grid showing the full motion cycle. Steve directive 2026-05-20.
+const MUYBRIDGE_SUBJECTS = [
+  'horse galloping — sequence of 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle progression left-to-right, each panel separated by a thin hairline rule showing the full gait cycle, tiled left-to-right in three rows of four',
+  'horse trotting — 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle, each panel separated by a thin hairline rule tiled in a 4x4 grid',
+  'racehorse with jockey — 12 frozen pose sequence showing the full stride',
+  'leaping greyhound — 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle progression left-to-right, each panel separated by a thin hairline rule, dog mid-leap, tiled side-by-side',
+  'galloping dog — 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle, each panel separated by a thin hairline rule in a 4x4 grid',
+  'mastiff trotting — 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle progression left-to-right, each panel separated by a thin hairline rule tiled in 3 rows',
+  'cat walking — 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle, each panel separated by a thin hairline rule showing the saunter, tiled 4x4',
+  'cat leaping from rock to rock — 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle progression left-to-right, each panel separated by a thin hairline rule',
+  'lion pacing — 12 frozen pose sequence tiled in a strip',
+  'tiger walking — 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle progression left-to-right, each panel separated by a thin hairline rule in a 3x4 grid',
+  'elephant walking — 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle progression left-to-right, each panel separated by a thin hairline rule showing the heavy gait, tiled 3x4',
+  'ostrich running — 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle progression left-to-right, each panel separated by a thin hairline rule tiled side-by-side',
+  'kangaroo hopping — 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle progression left-to-right, each panel separated by a thin hairline rule showing the full bound, tiled 3x4',
+  'pig trotting — 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle progression left-to-right, each panel separated by a thin hairline rule in a 4x3 grid',
+  'goat walking — 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle progression left-to-right, each panel separated by a thin hairline rule tiled side-by-side',
+  'donkey trotting — 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle progression left-to-right, each panel separated by a thin hairline rule in a 3x4 grid',
+  'camel walking — 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle progression left-to-right, each panel separated by a thin hairline rule showing the long stride, tiled 3x4',
+  'rabbit hopping — 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle progression left-to-right, each panel separated by a thin hairline rule showing the full bound',
+  'deer leaping — 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle progression left-to-right, each panel separated by a thin hairline rule tiled in a strip',
+  'bison charging — 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle progression left-to-right, each panel separated by a thin hairline rule in a 3x4 grid',
+  'rooster strutting — 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle progression left-to-right, each panel separated by a thin hairline rule tiled side-by-side',
+  'cockatoo flapping its wings — 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle progression left-to-right, each panel separated by a thin hairline rule in a 3x4 grid',
+  'pigeon taking flight — 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle progression left-to-right, each panel separated by a thin hairline rule showing the full wingbeat',
+  'human walking — Muybridge male figure in a loincloth, 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle progression left-to-right, each panel separated by a thin hairline rule tiled side-by-side',
+  'human descending stairs — woman in a Victorian dress, 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle progression left-to-right, each panel separated by a thin hairline rule tiled left-to-right',
+  'human running — 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle progression left-to-right, each panel separated by a thin hairline rule in a 3x4 grid',
+  'human jumping — 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle progression left-to-right, each panel separated by a thin hairline rule showing the full leap',
+  'human throwing a discus — 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle progression left-to-right, each panel separated by a thin hairline rule tiled 3x4',
+  'human ascending stairs — 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle progression left-to-right, each panel separated by a thin hairline rule left-to-right',
+  'human carrying a heavy bucket — 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle progression left-to-right, each panel separated by a thin hairline rule showing the strained walk',
+  'human dancing a waltz — 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle progression left-to-right, each panel separated by a thin hairline rule in a 3x4 grid',
+  'human waving a flag — 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle progression left-to-right, each panel separated by a thin hairline rule tiled side-by-side',
+  'human boxing — two figures in 12 frozen pose sequence, tiled 3x4',
+  'human wrestling — two figures locked together in 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle progression left-to-right, each panel separated by a thin hairline rule',
+  'baby crawling — 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle progression left-to-right, each panel separated by a thin hairline rule tiled in a 3x4 grid',
+  'human hammering a nail — 4 distinct frozen poses arranged as a 4-panel triptych — horizontal 1x4 strip composition showing the full motion cycle progression left-to-right, each panel separated by a thin hairline rule showing the full swing',
+];
+function composeMuybridgePlatePrompt(cw) {
+  const subj = MUYBRIDGE_SUBJECTS[Math.floor(Math.random() * MUYBRIDGE_SUBJECTS.length)];
+  return (
+    `Eadweard Muybridge motion-study plate as wallpaper — ${cw.name} colorway. Ground: solid ${cw.tone} (${cw.hex}). Figure: bone-ivory (#F2EADB) single-ink. ` +
+    `Subject: ${subj}. ` +
+    `Each pose is a flat stencil silhouette in single ink on the solid ground — no chiaroscuro, no shading, no anatomical detail inside. ` +
+    `Render as a SEAMLESS REPEAT — the full plate is one tile, edge-to-edge. ` +
+    `Reference: Muybridge "Animal Locomotion" 1887, plate-format, single-color blueprint-style serigraph treatment. NOT photographic, NOT engraved — flat stencil only. ` +
+    SHARED_ARTDIR
+  );
+}
+
+// ── LIPS — Steve directive 2026-05-20. 4-color max, sparse iconic lips on
+// generous ground. Inspired by Warhol Marilyn, Lichtenstein, Patrick Nagel,
+// matchbook covers.
+const LIPS_SUBJECTS = [
+  'one large parted red-velvet lips silhouette centered on the tile, no face, no teeth — just the lips. 80% empty cream ground around it',
+  'a single perfect lipstick-kiss imprint (blot mark) on the upper-third of the tile in scarlet, otherwise empty cream ground',
+  'two lips facing each other almost touching (mid-kiss silhouette), one in deep red and one in cream-outline, lots of empty ground around them',
+  'pursed pouting lips in profile silhouette, scarlet red, no other elements, vast clean ground',
+  'lips biting a single stem of a tiny rose — minimalist Patrick-Nagel silhouette, two-tone (red lips, black rose silhouette, cream ground)',
+  'classic Andy Warhol style pop-art lips — one giant pair of half-open red lips with a thin black outline, 4 colors max (red, black, cream, optional cream highlight on lower lip)',
+];
+
+function composeLipsPrompt(subject, cw) {
+  // For lips: ground=cream/ivory, figure=scarlet red. Override colorway.
+  return (
+    `Lips wallpaper — minimalist pop-art register. ` +
+    `Ground: solid cream-ivory (#F2EADB). Figure: deep scarlet red (#B0282A). ` +
+    `Subject: ${subject}. ` +
+    `Up to 4 colors maximum (cream + scarlet + optional black outline + optional cream highlight on lower lip). NO bleed. NO halftone. NO gradient. ` +
+    `Reference: Andy Warhol pop-art lips, Patrick Nagel, vintage matchbook cover, single-pass screen-print. ` +
+    SHARED_ARTDIR
+  );
+}
+
+// ── MONTEREY WEST-COAST MURAL — Steve directive 2026-05-20.
+// 6 scene subjects × 4 seasonal colorways = 24 base designs. Each is a single
+// 36"-wide × 132"-tall (11ft) panel intended to be the FIRST in a multi-panel
+// mural set. Admin can later "+next panel" to extend the scene rightward
+// across the wall (via /api/design/:id/add-next-panel).
+const MONTEREY_SUBJECTS = [
+  'Lone Cypress on Pebble Beach 17-Mile Drive — a single windswept Monterey cypress silhouetted on a granite headland with the Pacific behind, hand-painted scenic register, hand-painted-silk aesthetic',
+  'Cannery Row at low tide — silhouettes of the historic canneries on pilings over kelp-strewn rocks, fog ribbons in the upper third',
+  'Point Pinos Lighthouse at dusk — the oldest continuously-operating lighthouse on the West Coast with cypress hedge and bluff in foreground',
+  'Carmel Mission Basilica — adobe bell tower silhouette with rose-garden hedges and ancient olive trees',
+  'Big Sur Bixby Creek Bridge silhouette — arched span over the canyon mouth with the Pacific opening to the west, redwoods on the inland slope',
+  'Monterey Bay kelp-forest above-and-below cross-section — sea-otters on the surface, kelp fronds rising from the seabed, single-layer silhouette only',
+];
+
+const SEASONAL_COLORWAYS = [
+  { season: 'spring', name: 'Spring Bloom',     ground: '#A8B79A', figure: '#F2EADB', tone: 'sage-mint' },
+  { season: 'summer', name: 'Summer Surf',      ground: '#3E6B82', figure: '#F2EADB', tone: 'bay-blue' },
+  { season: 'fall',   name: 'Autumn Saddle',    ground: '#7A4A28', figure: '#F2EADB', tone: 'cypress-burnt-umber' },
+  { season: 'winter', name: 'Winter Fog',       ground: '#9CA09B', figure: '#3A2A1C', tone: 'pacific-mist' },
+];
+
+function composeMontereyMuralPrompt(subject, cw) {
+  return (
+    `Monterey, California west-coast mural panel — ${subject}. ` +
+    `${cw.season.charAt(0).toUpperCase() + cw.season.slice(1)} colorway: ${cw.name}. Ground: solid ${cw.tone} (${cw.ground}). Figure: ${cw.figure} single-ink. ` +
+    `Composition: panoramic vertical panel, motif anchored center-bottom, sky/atmosphere extending up the panel (still in solid ground tone, NOT a sky gradient), generous breathing room. ` +
+    `This is a FULL-ROOM MURAL — width = full wall, height = full ceiling. Compose for a 12'-wide × 9'-tall wall. The customer will choose 24"/36"/54" print-panel widths at order time — generate at room scale, not panel scale. ` +
+    `4-COLOR PALETTE WITH HUE SYNC — all four colors share the same hue family (no warm/cool mixing). Ground + figure as the two main tones; one HIGHLIGHT color for accents (lips, eyes, fingernails, fruit, small jewels); one SHADOW color (under figures, in eye sockets, beneath leaves). Highlight + shadow stay in the same hue family as ground+figure. ` +
+    `Reference: hand-painted-silk scenic panel, hand-painted silk register, NOT Haeckel, NOT Audubon. Single-layer stencil silhouette only. ` +
+    SHARED_ARTDIR
+  );
+}
+
+// Tall panel kind — 36" wide × 132" tall (11ft). Different from the seamless_tile
+// kind which is square. Panel kind is meant to hang as-is, not tile-repeat.
+// 2026-05-20 Steve directive: murals are FULL ROOM dimensions, not panels.
+// Customer chooses 24"/36"/54" panel width at print time — generation is room-
+// scale. Default room: 144" wide (12 ft) × 108" tall (9 ft ceiling) @ 150 DPI.
+// SDXL renders at 1536×1152 (4:3 max) — at-print upscale to 21600×16200
+// happens at order time via Topaz/Real-ESRGAN.
+const PANEL_W = 144;        // inches (metadata)
+const PANEL_H = 108;        // inches (metadata)
+const PANEL_RENDER_W = 1536; // pixels (SDXL output)
+const PANEL_RENDER_H = 1152; // pixels (SDXL output)
+
+// 2026-05-20 Steve directive: "up to 4 colors using for highlights like
+// lips.. eyes.. fingernails.. accents in another color and shadows. always
+// make sure hue is in sync." Murals get 4 colors with hue-sync constraint.
+// Tile tracks (cactus/chinoiserie/etc.) stay 2-color.
+const HUE_SYNC_QUARTETS = [
+  // Each quartet is [ground, figure, highlight, shadow] within the same hue
+  // family. The hue-sync constraint = all 4 colors share the same dominant
+  // hue ± neutrals.
+  // WARM EARTH (cream/saddle/rose/cocoa)
+  { name: 'Saddle Rose Warmth', ground: '#F2EADB', figure: '#5C2329', highlight: '#B0282A', shadow: '#2A1010' },
+  // FOREST GREEN (Steve's 10492-loved family)
+  { name: 'Forest Loden Quartet', ground: '#3C4844', figure: '#E8E2D1', highlight: '#A8B79A', shadow: '#1A2520' },
+  // INK + GOLD (Steve's high-end luxury-archival energy)
+  { name: 'Ink Library', ground: '#1F2740', figure: '#E8DDB8', highlight: '#C9A14B', shadow: '#0B0F1C' },
+  // BORDEAUX
+  { name: 'Bordeaux Velvet', ground: '#5C2329', figure: '#F2EADB', highlight: '#C84A3A', shadow: '#2A0E10' },
+  // OXBLOOD
+  { name: 'Oxblood', ground: '#722F2F', figure: '#F2EADB', highlight: '#A85040', shadow: '#3A1010' },
+  // STONE PEWTER
+  { name: 'Stone Pewter', ground: '#8A857B', figure: '#F2EADB', highlight: '#C9A14B', shadow: '#3A3530' },
+  // MOSS SLATE
+  { name: 'Moss Slate', ground: '#566554', figure: '#F2EADB', highlight: '#A8B79A', shadow: '#1A2520' },
+  // CHARCOAL + CREAM
+  { name: 'Charcoal Cream', ground: '#3D3833', figure: '#F2EADB', highlight: '#C9A14B', shadow: '#0F0D0B' },
+];
+function pickHueSyncQuartet() {
+  return HUE_SYNC_QUARTETS[Math.floor(Math.random() * HUE_SYNC_QUARTETS.length)];
+}
+
+// ── DESIGNER-ZOO-CALM — Steve directive 2026-05-20 ("love this theme")
+// Inspired by accepted design 10492. Forest-green ground + bone-ivory animal
+// silhouettes, dignified standing/sitting poses (not staggering), sparse
+// fern/grass botanical fillers between animals, Cole-Son/Sister-Parish energy.
+const ZOO_CALM_ANIMALS = [
+  'pair of red pandas in symmetric grooming pose',
+  'bear seated upright in a meditative pose, head slightly tilted',
+  'a tall standing fox in profile, ears alert',
+  'pair of foxes mirrored facing each other, tails curving outward',
+  'family of three lemurs perched in a vertical stack',
+  'pair of standing wolves facing forward, dignified',
+  'mountain lion seated in regal posture, tail curled',
+  'pair of badgers facing each other in symmetric pose',
+  'standing pangolin in profile, scales catching highlight as cut-outs',
+  'pair of capybaras seated side-by-side',
+  'tall standing crane with neck curved (no flying — standing pose only)',
+  'pair of forest deer with antlers, mirrored facing inward',
+  'standing fennec fox with oversized ears, alert pose',
+  'pair of stoats standing upright, paws raised',
+  'standing armadillo in profile, shell silhouette clean',
+  'lounging snow leopard with paws extended',
+];
+const ZOO_CALM_FILLERS = [
+  'sparse fern fronds rising between the animals',
+  'tall slender grass clusters in negative space between motifs',
+  'small wildflower clusters between motifs',
+  'pinecone-and-needle sprigs',
+  'wild-rose stems with single open blossoms',
+  'tall cattails between figures',
+  'sparse oak-leaf clusters in negative space',
+  'olive branches between motifs',
+];
+const ZOO_CALM_PALETTE = [
+  { name: 'Forest Loden', tone: 'deep forest green', hex: '#3C4844' },
+  { name: 'Hunter Olive', tone: 'hunter olive', hex: '#48543A' },
+  { name: 'Bottle Green', tone: 'bottle-green', hex: '#2E4538' },
+  { name: 'Pine Charcoal', tone: 'pine charcoal', hex: '#3A4A45' },
+  { name: 'Moss Slate', tone: 'moss slate', hex: '#566554' },
+  { name: 'Ink Navy', tone: 'ink navy', hex: '#1F2B3A' },
+];
+function composeDesignerZooCalmPrompt() {
+  const animal = ZOO_CALM_ANIMALS[Math.floor(Math.random() * ZOO_CALM_ANIMALS.length)];
+  const filler = ZOO_CALM_FILLERS[Math.floor(Math.random() * ZOO_CALM_FILLERS.length)];
+  const cw = ZOO_CALM_PALETTE[Math.floor(Math.random() * ZOO_CALM_PALETTE.length)];
+  return (
+    `Designer Wallcoverings zoo wallpaper — ${cw.name} colorway. Ground: solid ${cw.tone} (${cw.hex}). Figure: bone-ivory (#F2EADB) single-ink. ` +
+    `Vignette: ${animal}. Renders at wall-scale 36-inch seamless tile. Animals are LARGE, anchored on the central vertical axis of symmetry, dignified standing or seated pose (NOT staggering, NOT falling, NOT mid-motion). ` +
+    `Between the animals: ${filler}, kept sparse — generous breathing room of clean ground tone visible throughout. ` +
+    `Each animal is one FLAT STENCIL SILHOUETTE in bone-ivory ink. NO interior shading, NO anatomical detail, NO fur texture. Spots/stripes/markings appear ONLY as cut-out negative shapes in the silhouette (revealing ground tone), never as a second ink color. ` +
+    `Reference: British-archival archival, American-classical, Designer Wallcoverings house style. Refined, restrained, designer-grade. ` +
+    SHARED_ARTDIR
+  );
+}
+
+const TRACKS = [
+  // Weighted — designer-zoo-calm appears MORE often per Steve "do more" of 10492
+  'designer-zoo-calm','designer-zoo-calm','designer-zoo-calm','designer-zoo-calm',
+  'chinoiserie','stripe','cactus','drunk-zoo-36','face-skull-damask',
+  'designer-scenic','throttled-plaster-roses','muybridge-plate','monterey-mural',
+];
+
+// Variable repeat size per Steve directive 2026-05-20: vary 24"/36"/54" so
+// designs render at different motif scales (small/medium/large wall coverage).
+const REPEAT_SIZES = [24, 36, 36, 36, 54];
+function pickRepeatSize() {
+  return REPEAT_SIZES[Math.floor(Math.random() * REPEAT_SIZES.length)];
+}
+const REPEAT_INCHES = 36;   // wall-scale repeat — Steve directive 2026-05-20
+function nextTrack() {
+  return TRACKS[Math.floor(Math.random() * TRACKS.length)];
+}
+
+// ── Stop conditions ──────────────────────────────────────────────────
+function shouldStop(state) {
+  if (fs.existsSync(HALT_FLAG)) {
+    logLine('HALT — flag file present at ' + HALT_FLAG);
+    return true;
+  }
+  if (state.spentUsd >= BUDGET_CAP_USD) {
+    logLine(`HALT — budget cap reached ($${state.spentUsd.toFixed(2)} >= $${BUDGET_CAP_USD})`);
+    return true;
+  }
+  if (Date.now() >= STOP_DEADLINE.getTime()) {
+    logLine(`HALT — stop deadline reached (${new Date().toLocaleString()} past ${STOP_DEADLINE.toLocaleString()})`);
+    return true;
+  }
+  return false;
+}
+
+// ── Generate one design ──────────────────────────────────────────────
+function generateOne(track, cw) {
+  let prompt;
+  switch (track) {
+    case 'cactus':          prompt = composeCactusPrompt(cw); break;
+    case 'chinoiserie':     prompt = composeChinoiseriePrompt(cw); break;
+    case 'toile':           prompt = composeToilePrompt(cw); break;
+    case 'stripe':          prompt = composeStripePrompt(cw); break;
+    case 'quatrefoil':      prompt = composeQuatrefoilPrompt(cw); break;
+    case 'diamond-trellis': prompt = composeDiamondTrellisPrompt(cw); break;
+    case 'drunk-zoo-36':    prompt = composeDrunkZoo36Prompt(cw); break;
+    case 'face-skull-damask': prompt = composeFaceSkullDamaskPrompt(cw); break;
+    case 'designer-scenic':  prompt = composeDesignerScenicPrompt(cw); break;
+    case 'throttled-plaster-roses': prompt = composeThrottledPlasterRosesPrompt(cw); break;
+    case 'muybridge-plate':   prompt = composeMuybridgePlatePrompt(cw); break;
+    case 'designer-zoo-calm': prompt = composeDesignerZooCalmPrompt(); break;
+    case 'monterey-mural':    {
+      const subj = MONTEREY_SUBJECTS[Math.floor(Math.random() * MONTEREY_SUBJECTS.length)];
+      const season = SEASONAL_COLORWAYS[Math.floor(Math.random() * SEASONAL_COLORWAYS.length)];
+      // Override the colorway with the seasonal one for this track
+      cw = { name: season.name, hex: season.ground, tone: season.tone, slug: season.season };
+      prompt = composeMontereyMuralPrompt(subj, season);
+      break;
+    }
+    default:                prompt = composeCactusPrompt(cw);
+  }
+  if (!settlementOK(prompt)) {
+    logLine(`SKIP settlement — track=${track} cw=${cw.slug}`);
+    return { ok: false, settlementBlock: true };
+  }
+  // Each track inserts into its own category for sortability + variety.
+  const category = track === 'cactus'          ? 'cactus'
+                 : track === 'chinoiserie'     ? 'chinoiserie'
+                 : track === 'toile'           ? 'toile-de-jouy'
+                 : track === 'stripe'          ? 'stripe'
+                 : track === 'quatrefoil'      ? 'quatrefoil-ogee'
+                 : track === 'diamond-trellis' ? 'diamond-trellis'
+                 : track === 'drunk-zoo-36'    ? 'drunk-zoo-36'
+                 : track === 'face-skull-damask' ? 'face-skull-damask'
+                 : track === 'designer-scenic' ? 'designer-scenic'
+                 : track === 'throttled-plaster-roses' ? 'throttled-plaster-roses'
+                 : track === 'muybridge-plate' ? 'muybridge-plate'
+                 : track === 'monterey-mural' ? 'monterey-mural'
+                 : track === 'designer-zoo-calm' ? 'designer-zoo-calm'
+                 : 'drunk-animals-designer';
+  // Mural panels render at 36"×132" (11ft); everything else at square tiles
+  // with variable size per Steve 2026-05-20 (24/36/54).
+  const isPanel = (track === 'monterey-mural');
+  const w = isPanel ? PANEL_W : pickRepeatSize();
+  const h = isPanel ? PANEL_H : w;
+  const kind = isPanel ? 'mural_panel' : 'seamless_tile';
+  const r = spawnSync('node', [
+    path.join(__dirname, 'generate_designs.js'),
+    '--n', '1',
+    '--kind', kind,
+    '--width', String(w),
+    '--height', String(h),
+    '--category', category,
+    '--prompts', prompt,
+  ], { cwd: ROOT, encoding: 'utf8', env: process.env });
+
+  if (r.status === 0) {
+    // generate_designs.js prints "  ✓ #<id> ·" per-design and "IDs: <id>,..." summary
+    const summary = (r.stdout || '').match(/IDs:\s*(\d+(?:,\s*\d+)*)/);
+    const perLine = (r.stdout || '').match(/✓\s*#(\d+)/);
+    const idStr = summary ? summary[1].split(/,\s*/)[0] : (perLine ? perLine[1] : null);
+    const id = idStr ? parseInt(idStr, 10) : null;
+    // POST-PROCESS: force 2-tone quantize on the output PNG. Eliminates bleed
+    // between layers (Steve directive 2026-05-20 — looks cheap). The script
+    // backs up the original to <file>.original.png before overwriting.
+    if (id) {
+      try {
+        // psqlAt returns tuples-only, no header/footer — clean single-value reads
+        const lp = execSync(`/opt/homebrew/opt/postgresql@14/bin/psql dw_unified -At -c "SELECT local_path FROM spoon_all_designs WHERE id=${id};"`, { encoding: 'utf8' }).trim();
+        if (lp && fs.existsSync(lp)) {
+          const q = spawnSync('python3', [path.join(__dirname, 'quantize-2tone.py'), lp], { encoding: 'utf8' });
+          if (q.status === 0) {
+            logLine(`  2tone OK id=${id}: ${(q.stdout || '').trim().split('->')[1] || '?'}`);
+            // COLORWAY-VARIANTS — for every 2-toned design, generate 3 free
+            // recolors. Steve directive 2026-05-20. Each variant becomes a
+            // child row in spoon_all_designs with parent_design_id=id.
+            // Skip for mural panels (they're meant to be a multi-panel set,
+            // colorway variation happens at the source-prompt level there).
+            if (!isPanel) {
+              try {
+                const cwOut = spawnSync('python3', [path.join(__dirname, 'colorway-variants.py'), lp], { encoding: 'utf8' });
+                if (cwOut.status === 0) {
+                  const parsed = JSON.parse((cwOut.stdout || '').trim().split('\n').pop() || '{}');
+                  const variants = parsed.variants || [];
+                  for (const v of variants) {
+                    if (!fs.existsSync(v.path)) continue;
+                    const cwTitle = `${category} · ${v.slug}`;
+                    // Two-step fix for the "PLACEHOLDER image_url" bug — see
+                    // git log + the bulk PG UPDATE on 2026-05-20. The earlier
+                    // single-statement CTE (INSERT + UPDATE in one) failed
+                    // because PG's data-modifying CTEs share a snapshot, so the
+                    // UPDATE didn't see the just-inserted row and the URL stayed
+                    // PLACEHOLDER. Two separate psql calls — INSERT...RETURNING
+                    // captures id, then a follow-up UPDATE writes the real URL.
+                    const insertSql = `
+                      INSERT INTO spoon_all_designs (brand, kind, width_in, height_in, panels, generator, prompt, image_url, local_path, dominant_hex, category, is_published, notes, parent_design_id, created_at)
+                      SELECT brand, kind, width_in, height_in, panels, generator, prompt || E'\\n[colorway-variant of #${id}]', '/designs/img/by-id/PLACEHOLDER', '${v.path.replace(/'/g, "''")}', '${v.ground.replace(/'/g, "''")}', '${cwTitle.replace(/'/g, "''")}', TRUE, E'colorway variant: ${v.slug} (ground=${v.ground}, figure=${v.figure}) of parent #${id}', ${id}, NOW()
+                      FROM spoon_all_designs WHERE id=${id}
+                      RETURNING id;`;
+                    try {
+                      const insertRaw = execSync(`/opt/homebrew/opt/postgresql@14/bin/psql dw_unified -At -c "${insertSql.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { encoding: 'utf8' });
+                      // psql -At emits the RETURNING value on line 1, then the
+                      // command tag (e.g. "INSERT 0 1") on line 2. Earlier
+                      // version used `.split(/\s+/).pop()` which grabbed "1"
+                      // from the command tag. Take the FIRST line instead.
+                      const cwId = (insertRaw.trim().split('\n')[0] || '').trim();
+                      if (cwId && /^\d+$/.test(cwId)) {
+                        try {
+                          const updOut = execSync(`/opt/homebrew/opt/postgresql@14/bin/psql dw_unified -At -c "UPDATE spoon_all_designs SET image_url='/designs/img/by-id/${cwId}' WHERE id=${cwId};"`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
+                          logLine(`    cw url-patch id=${cwId} → ${updOut || '(empty)'}`);
+                        } catch (uErr) {
+                          logLine(`    cw url-patch FAIL id=${cwId}: ${(uErr.message || '').slice(0, 200)}`);
+                        }
+                      } else {
+                        logLine(`    cw NO-ID raw=${JSON.stringify(insertRaw).slice(0, 300)}`);
+                      }
+                      logLine(`    cw child id=${cwId} (${v.slug}) of parent ${id}`);
+                    } catch (e2) {
+                      logLine(`    cw INSERT FAIL ${v.slug}: ${e2.message.slice(0, 200)}`);
+                    }
+                  }
+                } else {
+                  logLine(`  cw FAIL id=${id}: ${(cwOut.stderr || cwOut.stdout || '').slice(0, 200)}`);
+                }
+              } catch (e3) {
+                logLine(`  cw EXC id=${id}: ${e3.message.slice(0, 200)}`);
+              }
+            }
+          } else {
+            logLine(`  2tone FAIL id=${id}: ${(q.stderr || q.stdout || '').slice(0, 200)}`);
+          }
+        } else {
+          logLine(`  2tone SKIP id=${id}: local_path not found (${lp})`);
+        }
+      } catch (e) {
+        logLine(`  2tone EXC id=${id}: ${e.message.slice(0, 200)}`);
+      }
+    }
+    // Auto-publish so Steve sees it in /designs
+    if (id) {
+      try {
+        psql(`UPDATE spoon_all_designs SET is_published=TRUE WHERE id=${id};`);
+      } catch (e) {
+        logLine(`publish-flip FAIL id=${id}: ${e.message.slice(0, 120)}`);
+      }
+    }
+    return { ok: true, id, category, colorway: cw.slug };
+  } else {
+    return { ok: false, stderr: (r.stderr || r.stdout || '').slice(0, 200) };
+  }
+}
+
+// ── Main loop ────────────────────────────────────────────────────────
+(async () => {
+  if (fs.existsSync(HALT_FLAG)) {
+    logLine('ABORT — halt flag already exists at startup. Remove it first.');
+    process.exit(1);
+  }
+  const state = { iterations: 0, ok: 0, fail: 0, settlementSkipped: 0, spentUsd: 0, startedAt: nowIso() };
+  logLine(`START night-builder · budget=$${BUDGET_CAP_USD} stop=${STOP_DEADLINE.toLocaleString()} cooldown=${COOLDOWN_MS}ms`);
+  logLine(`Track mix: 30% cactus / 70% drunk-animals-designer · ${MUTED_PALETTE.length} muted colorways`);
+
+  while (!shouldStop(state)) {
+    state.iterations++;
+    const track = nextTrack();
+    const cw = pickColorway();
+    const r = generateOne(track, cw);
+    state.spentUsd += PER_IMAGE_USD;
+    if (r.settlementBlock) {
+      state.settlementSkipped++;
+    } else if (r.ok) {
+      state.ok++;
+      logLine(`OK  #${state.iterations} ${track}/${cw.slug} id=${r.id} (cumul: ok=${state.ok} fail=${state.fail} spent=$${state.spentUsd.toFixed(2)})`);
+    } else {
+      state.fail++;
+      logLine(`FAIL #${state.iterations} ${track}/${cw.slug}: ${r.stderr || 'unknown'}`);
+      // brief backoff on failure (rate-limit or transient)
+      await new Promise(rs => setTimeout(rs, 15000));
+    }
+    await new Promise(rs => setTimeout(rs, COOLDOWN_MS));
+  }
+
+  logLine(`STOP iterations=${state.iterations} ok=${state.ok} fail=${state.fail} settlementSkip=${state.settlementSkipped} spent=$${state.spentUsd.toFixed(2)}`);
+  process.exit(0);
+})();
diff --git a/scripts/overnight-noghost-bulk.sh b/scripts/overnight-noghost-bulk.sh
new file mode 100755
index 0000000..f4fd8eb
--- /dev/null
+++ b/scripts/overnight-noghost-bulk.sh
@@ -0,0 +1,74 @@
+#!/usr/bin/env bash
+# Overnight bulk no-ghost quantize across all categories.
+#
+# Policy:
+#   - Skip files that already have .original.png (means a prior bulk job
+#     touched them — guaranteed clean from that pass; do not re-process).
+#   - Silhouette categories → strict 2-tone via quantize-2tone.py @ N=2.
+#   - All other categories  → 4-tone with ghost-merge via quantize-no-ghost.py.
+#
+# Idempotent: re-running is safe (the .original.png guard prevents
+# re-processing already-touched files).
+
+set -u
+cd "$(dirname "$0")/.."
+
+PATHS_FILE="/tmp/all-designs-paths.txt"     # built by caller: local_path|base_cat
+TODO_FILE="/tmp/overnight-todo.txt"          # paths needing work
+LOG_FILE="/tmp/overnight-noghost.log"
+
+# Silhouette categories — strict 2-tone, no internal shading allowed.
+SILHOUETTE_CATS="^(muybridge-plate|lips|designer-bugs)$"
+
+echo "=== overnight no-ghost bulk start $(date) ===" | tee "$LOG_FILE"
+
+# Build the TODO list — only files that exist AND have no .original.png backup.
+: > "$TODO_FILE"
+while IFS='|' read -r LP CAT; do
+  [ -z "$LP" ] && continue
+  [ ! -f "$LP" ] && continue
+  # Skip if already touched (has backup)
+  BACKUP="${LP%.png}.original.png"
+  [ -f "$BACKUP" ] && continue
+  echo "$LP|$CAT" >> "$TODO_FILE"
+done < "$PATHS_FILE"
+
+TOTAL=$(wc -l < "$TODO_FILE" | tr -d ' ')
+echo "TODO: $TOTAL untouched PNGs" | tee -a "$LOG_FILE"
+
+if [ "$TOTAL" -eq 0 ]; then
+  echo "nothing to do — all files already quantized once" | tee -a "$LOG_FILE"
+  exit 0
+fi
+
+# Split into silhouette + multi-tone work queues
+SIL_LIST="/tmp/overnight-sil.txt"
+MT_LIST="/tmp/overnight-mt.txt"
+awk -F'|' -v re="$SILHOUETTE_CATS" '$2 ~ re {print $1}' "$TODO_FILE" > "$SIL_LIST"
+awk -F'|' -v re="$SILHOUETTE_CATS" '$2 !~ re {print $1}' "$TODO_FILE" > "$MT_LIST"
+
+SIL_N=$(wc -l < "$SIL_LIST" | tr -d ' ')
+MT_N=$(wc -l < "$MT_LIST" | tr -d ' ')
+echo "  silhouette (2-tone strict): $SIL_N" | tee -a "$LOG_FILE"
+echo "  multi-tone (no-ghost @4):   $MT_N" | tee -a "$LOG_FILE"
+
+# Process silhouette set
+if [ "$SIL_N" -gt 0 ]; then
+  echo "=== silhouette pass $(date +%H:%M:%S) ===" | tee -a "$LOG_FILE"
+  cat "$SIL_LIST" | xargs -P 8 -I{} env QUANT_COLORS=2 python3 scripts/quantize-2tone.py {} 2>&1 \
+    | grep -v -E "Deprecation|getdata" >> "$LOG_FILE"
+fi
+
+# Process multi-tone set
+if [ "$MT_N" -gt 0 ]; then
+  echo "=== multi-tone pass $(date +%H:%M:%S) ===" | tee -a "$LOG_FILE"
+  cat "$MT_LIST" | xargs -P 8 -I{} env QUANT_COLORS=4 GHOST_LUMA_GAP=35 python3 scripts/quantize-no-ghost.py {} 2>&1 \
+    | grep -v -E "Deprecation|getdata" >> "$LOG_FILE"
+fi
+
+OK_2TONE=$(grep -c "^OK 2tone" "$LOG_FILE" || echo 0)
+OK_NOGHOST=$(grep -c "^OK no-ghost" "$LOG_FILE" || echo 0)
+echo "=== overnight no-ghost bulk done $(date) ===" | tee -a "$LOG_FILE"
+echo "  2-tone processed:   $OK_2TONE" | tee -a "$LOG_FILE"
+echo "  no-ghost processed: $OK_NOGHOST" | tee -a "$LOG_FILE"
+echo "next: rsync /Users/stevestudio2/Projects/wallco-ai/data/generated/ to prod"
diff --git a/scripts/publish-saguaro-arms.js b/scripts/publish-saguaro-arms.js
new file mode 100755
index 0000000..c8cd8ce
--- /dev/null
+++ b/scripts/publish-saguaro-arms.js
@@ -0,0 +1,30 @@
+#!/usr/bin/env node
+// After generate-saguaro-arms.js completes, flip the saguaro-arms rows to
+// is_published=TRUE, sync designs.json, hot-reload wallco. Mirrors publish-cactus.js.
+
+const fs = require('fs');
+const path = require('path');
+const { execSync } = require('child_process');
+
+const DB_NAME = 'dw_unified';
+const PSQL_CMD = (process.platform === 'linux')
+  ? `sudo -n -u postgres psql ${DB_NAME} -At -q`
+  : `psql ${DB_NAME} -At -q`;
+const q = (sql) => execSync(PSQL_CMD, { input: sql, encoding: 'utf8' }).trim();
+
+console.log('Flipping is_published=TRUE for category=saguaro-arms');
+const result = q(`UPDATE spoon_all_designs SET is_published=TRUE WHERE category='saguaro-arms' AND is_published=FALSE RETURNING id;`);
+const ids = result.split('\n').filter(Boolean);
+console.log('Published ' + ids.length + ' rows: ' + JSON.stringify(ids));
+
+console.log('Syncing data/designs.json…');
+const DATA_FILE = path.join(__dirname, '..', 'data', 'designs.json');
+const cols = 'id, brand, kind, width_in, height_in, panels, generator, prompt, negative_prompt, seed, steps, cfg_scale, pd_source_ids, image_url, local_path, dominant_hex, palette, motifs, tags, category, is_published, notes, created_at, parent_design_id, chat_session_id, request_text, verticals, source_shopify_id, source_dw_sku, source_url, gd_score, gd_critique, id_score, id_critique, combined_score, rating_summary, rated_at, user_score_avg, user_vote_count, poll_wins';
+const allRowsJson = q(`SELECT json_agg(t ORDER BY id) FROM (SELECT ${cols} FROM spoon_all_designs) t;`);
+const allRows = JSON.parse(allRowsJson);
+fs.writeFileSync(DATA_FILE + '.tmp', JSON.stringify(allRows, null, 2), 'utf8');
+fs.renameSync(DATA_FILE + '.tmp', DATA_FILE);
+console.log('designs.json now has ' + allRows.length + ' rows');
+
+console.log('\nLocal designs.json updated. Deploy via ./deploy-kamatera.sh to push to prod.');
+console.log('After deploy verify at https://wallco.ai/designs?cat=saguaro-arms');
diff --git a/scripts/quantize-no-ghost.py b/scripts/quantize-no-ghost.py
new file mode 100644
index 0000000..1ee3a68
--- /dev/null
+++ b/scripts/quantize-no-ghost.py
@@ -0,0 +1,100 @@
+#!/usr/bin/env python3
+"""Quantize an image to N tones (default 4) AND merge ghost-halo tones.
+
+A "ghost" is a near-luma duplicate of an adjacent tone that hovers as a
+shadow/halo behind the main figure. After median-cut quantize, ghosts
+appear as a sliver-tone whose luminance is too close to a neighbor —
+typical signature of SDXL atmospheric-depth bleed.
+
+Algorithm:
+  1. Median-cut quantize to N colors (env QUANT_COLORS, default 4).
+  2. Sort the resulting palette by luma (perceived brightness).
+  3. For each adjacent-by-luma pair, if Δluma < GHOST_LUMA_GAP, merge
+     the lighter one into the darker one (preserving the more-common
+     anchor color).
+  4. Re-render the image with the cleaned palette.
+  5. Result: 2-4 WELL-SEPARATED flat tones, no ghost layer.
+
+Usage:
+  QUANT_COLORS=4 GHOST_LUMA_GAP=35 python3 quantize-no-ghost.py <input.png>
+
+Env:
+  QUANT_COLORS    target color count BEFORE ghost-merge (default 4)
+  GHOST_LUMA_GAP  min Δluma between adjacent tones (default 35)
+"""
+import os
+import sys
+from collections import Counter
+from PIL import Image
+
+if len(sys.argv) != 2:
+    print("usage: quantize-no-ghost.py <input.png>", file=sys.stderr)
+    sys.exit(1)
+
+src = sys.argv[1]
+if not os.path.exists(src):
+    print(f"file not found: {src}", file=sys.stderr)
+    sys.exit(2)
+
+QUANT_COLORS   = int(os.environ.get('QUANT_COLORS',   '4'))
+GHOST_LUMA_GAP = int(os.environ.get('GHOST_LUMA_GAP', '35'))
+
+backup = src.replace('.png', '.original.png')
+if not os.path.exists(backup):
+    with open(src, 'rb') as fi, open(backup, 'wb') as fo:
+        fo.write(fi.read())
+
+im = Image.open(src).convert('RGB')
+
+# Step 1: median-cut to N
+im_q = im.quantize(
+    colors=QUANT_COLORS,
+    method=Image.Quantize.MEDIANCUT,
+    dither=Image.Dither.NONE,
+).convert('RGB')
+
+pixels = list(im_q.getdata())
+c = Counter(pixels)
+
+def luma(rgb):
+    r, g, b = rgb
+    return 0.299 * r + 0.587 * g + 0.114 * b
+
+# Step 2 + 3: build merge map by walking palette in luma order
+palette = sorted(c.keys(), key=luma)
+merge_map = {col: col for col in palette}
+
+# Iterate until no more merges (cascade ghosts)
+changed = True
+while changed:
+    changed = False
+    # Recompute current effective palette (post-merges) in luma order
+    effective = sorted(set(merge_map[col] for col in palette), key=luma)
+    for i in range(len(effective) - 1):
+        a, b = effective[i], effective[i + 1]
+        if abs(luma(a) - luma(b)) < GHOST_LUMA_GAP:
+            # Merge the lower-count one into the higher-count one
+            count_a = sum(c[col] for col in palette if merge_map[col] == a)
+            count_b = sum(c[col] for col in palette if merge_map[col] == b)
+            keep, drop = (a, b) if count_a >= count_b else (b, a)
+            for col in palette:
+                if merge_map[col] == drop:
+                    merge_map[col] = keep
+            changed = True
+            break
+
+# Step 4: rewrite pixels via merge_map
+new_pixels = [merge_map[p] for p in pixels]
+out = Image.new('RGB', im_q.size)
+out.putdata(new_pixels)
+out.save(src, optimize=True)
+
+# Verify + report
+final = Counter(new_pixels)
+total = sum(final.values())
+result = []
+for color, count in final.most_common():
+    pct = 100 * count / total
+    hex_ = '#{:02X}{:02X}{:02X}'.format(*color)
+    result.append(f"{hex_}:{pct:.1f}%")
+print(f"OK no-ghost {src} ({len(final)} tones) -> {' / '.join(result)}")
diff --git a/scripts/refresh_designs_snapshot.py b/scripts/refresh_designs_snapshot.py
index 4a94dc5..b3340d7 100644
--- a/scripts/refresh_designs_snapshot.py
+++ b/scripts/refresh_designs_snapshot.py
@@ -75,7 +75,10 @@ rows = psql_json(
     "SELECT id, kind, category, dominant_hex, prompt, generator, seed, local_path, "
     "is_published, created_at, motifs, room_mockups, "
     "user_removed, user_color_good, user_style_good, user_scale_good, user_stars "
-    "FROM spoon_all_designs WHERE generator IN ('replicate','gemini-2.5-flash-image-edit') ORDER BY id) t;"
+    # 2026-05-22 — added 'comfy' so Mac1 ComfyUI-generated designs (drunk-monkeys-v2 etc)
+    # land in the snapshot. Previously these were silently excluded, making the live
+    # viewer show 0 results for any cat= filter pointing at a comfy-generated category.
+    "FROM spoon_all_designs WHERE generator IN ('replicate','gemini-2.5-flash-image-edit','comfy') ORDER BY id) t;"
 )
 
 out = []
diff --git a/server.js b/server.js
index 0bc789b..f42c525 100644
--- a/server.js
+++ b/server.js
@@ -2636,7 +2636,9 @@ function htmlHeader(active) {
     ['/murals', 'Murals'],
     ['/samples', 'Samples'],
     ['/basket', 'Basket'],
+    ['/scenic', 'Scenic'],
     ['/studio', 'Studio'],
+    ['/designer-studio', 'Designer Studio'],
     ['/play',   'Play'],
     ['/generator', 'Generator'],
     ['/marketplace', 'Marketplace'],
@@ -3506,10 +3508,82 @@ app.post('/login', express.urlencoded({ extended: false, limit: '2kb' }), (req,
   res.redirect('/login');
 });
 
-// Sub-brand entry points — clean URLs that redirect into /designs?line=…
-// so search engines + bookmarks have a stable path per product line.
-app.get('/scenic', (req, res) => res.redirect('/designs?line=Scenic'));
-app.get('/studio', (req, res) => res.redirect('/designs?line=Studio'));
+// Sub-brand landing pages — wallco / Scenic + wallco / Studio.
+// Hero + brand pitch + 6-design featured grid + "Browse all" CTA.
+// Hero IDs are explicit so the page always opens on the strongest image,
+// regardless of generator timestamp order.
+function renderSubBrandLanding(req, res, line) {
+  const isStudio = line === 'Studio';
+  const HERO_ID = isStudio ? 39055 : 38842;
+  const accent = isStudio ? '#1E3550' : '#7A4A28';
+  const tagline = isStudio
+    ? 'Hand-painted-silk register at 60% of Gracie or de Gournay'
+    : 'Photoreal hospitality wallcovering at AI-catalog scale';
+  // All non-vendor designs for this line that have an on-disk image.
+  const _DENY = /\b(thibaut|koroseal|schumacher|maya[ -]romanoff|scalamandre|arte[ -]international|cole[ -]?(and|&)[ -]?son|phillip[ -]jeffries|osborne[ -]?(and|&)?[ -]?little|de[ -]gournay|fromental|sister[ -]parish|dedar|brewster|mind[ -]the[ -]gap|hygge|designtex|wolf[ -]gordon|carnegie|coordonn|daisy[ -]bennett|china[ -]seas|york|marburg|romo|fabricut|stout|knoll|arteriors|timorous[ -]beasties|hangups)\b/i;
+  const lineDesigns = DESIGNS
+    .filter(d => designHasImage(d))
+    .filter(d => !_DENY.test(String(d.category || '')))
+    .filter(d => productLine(d) === line);
+  const total = lineDesigns.length;
+  const hero = lineDesigns.find(d => d.id === HERO_ID) || lineDesigns[0];
+  const heroImg = hero ? (hero.image_url || ('/designs/img/by-id/' + hero.id)) : '';
+  // Featured grid — top 6 by id DESC (most recent first). Exclude the hero
+  // so the page doesn't double-render the same image.
+  const featured = lineDesigns
+    .filter(d => !hero || d.id !== hero.id)
+    .sort((a, b) => b.id - a.id)
+    .slice(0, 6);
+
+  const pitchScenic = 'wallco / Scenic — Photoreal hospitality wallcovering. Sonoran desert, Joshua tree groves, Carmel cypress headlands, coast redwood cathedrals — at the scale of a Phillip Jeffries or Calico mural, at AI-catalog scale and sub-$250 per panel. Designed for hospitality lobbies, hotel dining rooms, residential dining accents, design-forward corridors. 36" × 11&nbsp;ft and 12&nbsp;ft × 11&nbsp;ft 4-panel formats. ' + total + ' designs.';
+  const pitchStudio = 'wallco / Studio — Hand-painted-silk scenic wallcovering in the Zuber, Gracie, and de Gournay register, at 60% of their price point. Flat 2-tone hand-blocked silhouette, single-layer screenprint aesthetic. Designed for the most discerning hospitality installs (Aman, Auberge, Four Seasons) and the residential dining rooms of Designers Today readers. 60&nbsp;ft × 11&nbsp;ft wraparound installs (20 panels × 36"). ' + total + ' designs.';
+  const pitch = isStudio ? pitchStudio : pitchScenic;
+  const lineLower = line.toLowerCase();
+
+  const gridCards = featured.map(d => {
+    const img = d.image_url || ('/designs/img/by-id/' + d.id);
+    return '<a href="/design/' + d.id + '" class="sl-card" style="display:block;aspect-ratio:3/4;background:#eee center/cover url(' + JSON.stringify(img).replace(/^"|"$/g, '\'') + ');border-radius:6px;overflow:hidden;text-decoration:none;position:relative">'
+      + '<span style="position:absolute;left:10px;bottom:10px;background:rgba(0,0,0,.55);color:#fff;font:10px var(--sans,system-ui);padding:4px 10px;border-radius:999px;letter-spacing:.08em;text-transform:uppercase">#' + d.id + '</span>'
+      + '</a>';
+  }).join('');
+
+  const body = '<!doctype html>'
+    + htmlHead({
+        title: 'wallco / ' + line + ' — ' + tagline,
+        description: pitch.replace(/&nbsp;/g, ' ').slice(0, 220),
+        canonical: 'https://wallco.ai/' + lineLower,
+        ogImage: heroImg ? ('https://wallco.ai' + heroImg) : '',
+      })
+    + '<body>'
+    + htmlHeader('/' + lineLower)
+    + '<main id="main-content" style="padding-top:64px">'
+    // Hero
+    + '<section style="position:relative;width:100%;height:min(78vh,720px);min-height:420px;overflow:hidden;background:#111">'
+    +   (heroImg ? '<img src="' + heroImg + '" alt="wallco / ' + line + ' — featured scenic" style="width:100%;height:100%;object-fit:cover;object-position:center 12%;display:block">' : '')
+    +   '<div style="position:absolute;inset:0;background:linear-gradient(180deg,rgba(0,0,0,.18) 0%,rgba(0,0,0,.05) 35%,rgba(0,0,0,.55) 100%)"></div>'
+    +   '<div style="position:absolute;left:0;right:0;bottom:0;padding:48px 24px;color:#fff;text-align:center">'
+    +     '<div style="font:11px var(--sans,system-ui);letter-spacing:.32em;text-transform:uppercase;opacity:.82;margin-bottom:10px">wallco / ' + line + '</div>'
+    +     '<h1 style="font:clamp(36px,5.5vw,68px)/1.05 var(--serif,\'Playfair Display\',Didot,serif);font-weight:400;font-style:italic;margin:0;letter-spacing:-0.005em">' + tagline + '</h1>'
+    +   '</div>'
+    + '</section>'
+    // Pitch
+    + '<section style="max-width:780px;margin:0 auto;padding:56px 24px 36px;text-align:center">'
+    +   '<p style="font:16px/1.75 var(--sans,system-ui);color:var(--ink,#1a1a1a);margin:0">' + pitch + '</p>'
+    + '</section>'
+    // Featured grid
+    + (gridCards
+        ? '<section style="max-width:1280px;margin:0 auto;padding:0 24px 24px"><div style="font:11px var(--sans,system-ui);letter-spacing:.20em;text-transform:uppercase;color:var(--ink-faint,#999);text-align:center;margin-bottom:18px">Featured ' + line + ' designs</div><div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:14px">' + gridCards + '</div></section>'
+        : '')
+    // CTA
+    + '<section style="max-width:780px;margin:0 auto;padding:36px 24px 96px;text-align:center">'
+    +   '<a href="/designs?line=' + line + '" style="display:inline-block;padding:16px 36px;background:' + accent + ';color:#F2EADB;text-decoration:none;font:13px var(--sans,system-ui);font-weight:600;letter-spacing:.12em;text-transform:uppercase;border-radius:999px">Browse all ' + line + ' designs →</a>'
+    + '</section>'
+    + '</main></body></html>';
+
+  res.type('html').send(body);
+}
+app.get('/scenic', (req, res) => renderSubBrandLanding(req, res, 'Scenic'));
+app.get('/studio', (req, res) => renderSubBrandLanding(req, res, 'Studio'));
 
 app.get('/designs', (req, res) => {
   // 2026-05-20 Steve directive: NO hard server gate. Allow 1 page browsing,
@@ -3786,7 +3860,11 @@ app.get('/designs', (req, res) => {
               const _line = productLine(d);
               if (!_line) return '';
               const _bg = _line === 'Studio' ? '#1E3550' : '#7A4A28';
-              return `<span class="card-line-chip" title="wallco / ${_line}" style="font-size:9px;letter-spacing:.16em;text-transform:uppercase;padding:2px 8px;background:${_bg};color:#F2EADB;border-radius:999px;font-weight:600">${_line}</span>`;
+              const _chip = `<span class="card-line-chip" title="wallco / ${_line}" style="font-size:9px;letter-spacing:.16em;text-transform:uppercase;padding:2px 8px;background:${_bg};color:#F2EADB;border-radius:999px;font-weight:600">${_line}</span>`;
+              // Hospitality sub-tag — Scenic only.
+              const _fit = hospitalityFit(d);
+              const _sub = _fit ? `<span class="card-hospitality-fit" style="display:block;margin-top:2px;font-style:italic;color:var(--ink-soft,#666);font:10px var(--sans,system-ui);letter-spacing:.02em">— ${_fit}</span>` : '';
+              return _chip + _sub;
             })()}
           </span>
         </div>
@@ -8376,7 +8454,11 @@ ${htmlHeader('/designs')}
           ? { bg: '#1E3550', fg: '#F2EADB' }   // indigo on cream — Studio inky-luxe
           : { bg: '#7A4A28', fg: '#F2EADB' };  // burnt-umber on cream — Scenic earthy
         const _tag = _line === 'Studio' ? 'Hand-painted scenic register' : 'Photoreal hospitality mural';
-        return `<a href="/${_line.toLowerCase()}" class="product-line-chip" title="${_tag}" style="display:inline-flex;align-items:center;gap:8px;margin:6px 0 8px;padding:5px 12px 5px 10px;background:${_hue.bg};color:${_hue.fg};border-radius:999px;text-decoration:none;font:11px var(--sans,system-ui);font-weight:600;letter-spacing:.10em;text-transform:uppercase">wallco / ${_line} <span style="opacity:.7;font-weight:400;text-transform:none;letter-spacing:.02em">— ${_tag}</span></a>`;
+        const _chip = `<a href="/${_line.toLowerCase()}" class="product-line-chip" title="${_tag}" style="display:inline-flex;align-items:center;gap:8px;margin:6px 0 8px;padding:5px 12px 5px 10px;background:${_hue.bg};color:${_hue.fg};border-radius:999px;text-decoration:none;font:11px var(--sans,system-ui);font-weight:600;letter-spacing:.10em;text-transform:uppercase">wallco / ${_line} <span style="opacity:.7;font-weight:400;text-transform:none;letter-spacing:.02em">— ${_tag}</span></a>`;
+        // Hospitality sub-tag — Scenic only. Italic small-text under the chip.
+        const _fit = hospitalityFit(design);
+        const _sub = _fit ? `<span class="hospitality-fit" style="display:inline-block;margin-left:8px;font-style:italic;color:var(--ink-soft,#555);font:11px var(--sans,system-ui)">— for the ${_fit} aesthetic</span>` : '';
+        return _chip + _sub;
       })()}
 
       <div class="provenance-badge">
@@ -8535,7 +8617,7 @@ ${htmlHeader('/designs')}
       <!-- data-design-kind drives mode-aware show/hide; .repeat-only is shown
            when the user is in WALLPAPER (level≠0) and hidden in MURAL mode.
            .mural-only is the inverse. -->
-      <div class="detail-spec-panel" data-design-kind="${design.kind}">
+      <div class="detail-spec-panel" data-design-kind="${design.kind}" data-product-line="${productLine(design) || ''}" data-design-id="${design.id}">
         <div class="spec-row"><span class="spec-key">Category</span><span class="spec-val">${design.category}</span></div>
         <div class="spec-row"><span class="spec-key">Width</span><span class="spec-val" id="spec-width">24"</span></div>
         <div class="spec-row"><span class="spec-key">Material</span><span class="spec-val" id="spec-material">Paper</span></div>
@@ -8716,6 +8798,12 @@ ${htmlHeader('/designs')}
           var box = document.getElementById('wc-result');
           if (!isFinite(w) || !isFinite(h) || w<=0 || h<=0) { box.style.display='block'; box.innerHTML = '<span style="color:#b03a2e">Enter both width and height in feet.</span>'; return; }
           persist();
+          // Studio is trade-priced — never render a public $X-$Y estimate.
+          // Detect via data-product-line on the spec panel (server-rendered).
+          var specPanel = document.querySelector('.detail-spec-panel');
+          var pLine = specPanel ? (specPanel.dataset.productLine || '') : '';
+          var designId = specPanel ? (specPanel.dataset.designId || '') : '';
+          var isStudio = (pLine === 'Studio');
           // Mode: MURAL if the mural-stepper is at level 0, else WALLPAPER w/ repeat.
           var isMural = !!(document.querySelector('.mural-dot.active') && document.querySelector('.mural-dot.active').dataset.level === '0');
           // Read user's chosen roll/panel width (24/36/52) from the material picker.
@@ -8740,14 +8828,21 @@ ${htmlHeader('/designs')}
             // Mural pricing: ~$12-22/sq-ft installed list (custom mural)
             var lowEnd  = Math.round(panels * (panelWidthIn/12) * panelHFt * 12);
             var highEnd = Math.round(panels * (panelWidthIn/12) * panelHFt * 22);
+            // Studio is trade-priced — swap the cost tile for a mailto CTA.
+            var costTileMural = isStudio
+              ? ('<div><a href="mailto:info@designerwallcoverings.com?subject=' + encodeURIComponent('Trade pricing inquiry — wallco / Studio #' + designId) + '&body=' + encodeURIComponent('Wall size: ' + w + ' ft × ' + h + ' ft (' + panels + ' panels @ ' + panelWidthIn + '" × 11 ft)\nDesign: https://wallco.ai/design/' + designId + '\n\nPlease send Studio trade pricing for this install.\n') + '" style="display:inline-block;padding:8px 12px;background:#1E3550;color:#F2EADB;text-decoration:none;font:11px var(--sans);font-weight:600;letter-spacing:.08em;text-transform:uppercase;border-radius:999px">Contact for trade pricing →</a><span style="display:block;margin-top:6px;font:11px var(--sans);color:var(--ink-faint,#999);text-transform:uppercase;letter-spacing:.06em">wallco / Studio</span></div>')
+              : ('<div><strong style="display:block;font:22px var(--serif,Georgia,serif);color:var(--ink,#111)">~$' + lowEnd.toLocaleString() + '–$' + highEnd.toLocaleString() + '</strong><span style="font:11px var(--sans);color:var(--ink-faint,#999);text-transform:uppercase;letter-spacing:.06em">material estimate</span></div>');
+            var footerMural = isStudio
+              ? '<p style="margin:10px 0 0;font:11px/1.4 var(--sans);color:var(--ink-faint,#999)">Studio mode — hand-painted-silk register, trade-priced. ' + panels + ' panels (' + panelWidthIn + '" × 11 ft each). Reach info@designerwallcoverings.com for a quote.</p>'
+              : '<p style="margin:10px 0 0;font:11px/1.4 var(--sans);color:var(--ink-faint,#999)">Mural mode — panels join edge-to-edge L→R. Custom heights cropped from 11 ft panel at install. Material estimate; install separate.</p>';
             html =
               '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px;margin-top:6px">' +
                 '<div><strong style="display:block;font:22px var(--serif,Georgia,serif);color:var(--ink,#111)">' + panels + '</strong><span style="font:11px var(--sans);color:var(--ink-faint,#999);text-transform:uppercase;letter-spacing:.06em">panels (' + panelWidthIn + '" × 11 ft)</span></div>' +
                 '<div><strong style="display:block;font:22px var(--serif,Georgia,serif);color:var(--ink,#111)">' + panelsAcross + ' &times; ' + rowsTall + '</strong><span style="font:11px var(--sans);color:var(--ink-faint,#999);text-transform:uppercase;letter-spacing:.06em">across × tall</span></div>' +
                 '<div><strong style="display:block;font:22px var(--serif,Georgia,serif);color:var(--ink,#111)">' + wallSqFt.toFixed(0) + ' ft²</strong><span style="font:11px var(--sans);color:var(--ink-faint,#999);text-transform:uppercase;letter-spacing:.06em">wall area</span></div>' +
-                '<div><strong style="display:block;font:22px var(--serif,Georgia,serif);color:var(--ink,#111)">~$' + lowEnd.toLocaleString() + '–$' + highEnd.toLocaleString() + '</strong><span style="font:11px var(--sans);color:var(--ink-faint,#999);text-transform:uppercase;letter-spacing:.06em">material estimate</span></div>' +
+                costTileMural +
               '</div>' +
-              '<p style="margin:10px 0 0;font:11px/1.4 var(--sans);color:var(--ink-faint,#999)">Mural mode — panels join edge-to-edge L→R. Custom heights cropped from 11 ft panel at install. Material estimate; install separate.</p>';
+              footerMural;
           } else {
             // ── WALLPAPER WITH REPEAT math.
             // Per-roll spec: 27 ft length × roll-width (chosen 24/36/52).
@@ -8770,14 +8865,21 @@ ${htmlHeader('/designs')}
             // Wallpaper pricing: ~$5-10/sq-ft installed list
             var lowEnd  = Math.round(rolls * (rollWidthIn/12) * rollLengthFt * 5);
             var highEnd = Math.round(rolls * (rollWidthIn/12) * rollLengthFt * 10);
+            // Studio is trade-priced — hide the $X-$Y estimate entirely.
+            var costTileWp = isStudio
+              ? ('<div><a href="mailto:info@designerwallcoverings.com?subject=' + encodeURIComponent('Trade pricing inquiry — wallco / Studio #' + designId) + '&body=' + encodeURIComponent('Wall size: ' + w + ' ft × ' + h + ' ft (' + rolls + ' rolls @ ' + rollWidthIn + '" × 27 ft)\nDesign: https://wallco.ai/design/' + designId + '\n\nPlease send Studio trade pricing for this install.\n') + '" style="display:inline-block;padding:8px 12px;background:#1E3550;color:#F2EADB;text-decoration:none;font:11px var(--sans);font-weight:600;letter-spacing:.08em;text-transform:uppercase;border-radius:999px">Contact for trade pricing →</a><span style="display:block;margin-top:6px;font:11px var(--sans);color:var(--ink-faint,#999);text-transform:uppercase;letter-spacing:.06em">wallco / Studio</span></div>')
+              : ('<div><strong style="display:block;font:22px var(--serif,Georgia,serif);color:var(--ink,#111)">~$' + lowEnd.toLocaleString() + '–$' + highEnd.toLocaleString() + '</strong><span style="font:11px var(--sans);color:var(--ink-faint,#999);text-transform:uppercase;letter-spacing:.06em">material estimate</span></div>');
+            var footerWp = isStudio
+              ? '<p style="margin:10px 0 0;font:11px/1.4 var(--sans);color:var(--ink-faint,#999)">Studio mode — Studio is mural-only (panels join edge-to-edge); these wallpaper-roll numbers are a rough cross-reference. Reach info@designerwallcoverings.com for a quote.</p>'
+              : '<p style="margin:10px 0 0;font:11px/1.4 var(--sans);color:var(--ink-faint,#999)">Wallpaper mode — ' + rollWidthIn + '" × 27 ft rolls, ' + repeatIn + '" vertical repeat. Drop length = wall height + 1 repeat for pattern match. Material estimate; install separate.</p>';
             html =
               '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px;margin-top:6px">' +
                 '<div><strong style="display:block;font:22px var(--serif,Georgia,serif);color:var(--ink,#111)">' + rolls + '</strong><span style="font:11px var(--sans);color:var(--ink-faint,#999);text-transform:uppercase;letter-spacing:.06em">rolls (' + rollWidthIn + '" × 27 ft)</span></div>' +
                 '<div><strong style="display:block;font:22px var(--serif,Georgia,serif);color:var(--ink,#111)">' + dropsNeeded + '</strong><span style="font:11px var(--sans);color:var(--ink-faint,#999);text-transform:uppercase;letter-spacing:.06em">drops (' + dropLengthFt.toFixed(1) + ' ft each)</span></div>' +
                 '<div><strong style="display:block;font:22px var(--serif,Georgia,serif);color:var(--ink,#111)">' + repeatIn + '"</strong><span style="font:11px var(--sans);color:var(--ink-faint,#999);text-transform:uppercase;letter-spacing:.06em">repeat (vertical)</span></div>' +
-                '<div><strong style="display:block;font:22px var(--serif,Georgia,serif);color:var(--ink,#111)">~$' + lowEnd.toLocaleString() + '–$' + highEnd.toLocaleString() + '</strong><span style="font:11px var(--sans);color:var(--ink-faint,#999);text-transform:uppercase;letter-spacing:.06em">material estimate</span></div>' +
+                costTileWp +
               '</div>' +
-              '<p style="margin:10px 0 0;font:11px/1.4 var(--sans);color:var(--ink-faint,#999)">Wallpaper mode — ' + rollWidthIn + '" × 27 ft rolls, ' + repeatIn + '" vertical repeat. Drop length = wall height + 1 repeat for pattern match. Material estimate; install separate.</p>';
+              footerWp;
           }
           box$.style.display='block';
           box$.innerHTML = html;
@@ -12294,6 +12396,38 @@ function productLine(d) {
   return null;
 }
 
+// Hospitality classifier for Scenic — maps the dominant_hex palette character
+// to one of three hospitality archetypes. Used to render an italic sub-tag
+// under the line chip on Scenic PDP + grid card (Studio gets nothing).
+//   Aman         = minimalist, restrained, very dark OR very neutral
+//   Auberge      = warm, refined, hand-crafted (orange/amber, restrained sat)
+//   Four Seasons = classic / traditional / default fallback
+function hospitalityFit(d) {
+  if (!d) return null;
+  if (productLine(d) !== 'Scenic') return null;
+  const hex = String(d.dominant_hex || '').trim();
+  if (!/^#[0-9a-fA-F]{6}$/.test(hex)) return 'Four Seasons';
+  const r = parseInt(hex.slice(1,3),16)/255;
+  const g = parseInt(hex.slice(3,5),16)/255;
+  const b = parseInt(hex.slice(5,7),16)/255;
+  const max = Math.max(r,g,b), min = Math.min(r,g,b);
+  const l = (max + min) / 2;
+  const s = max === min ? 0 : (max - min) / (l > 0.5 ? 2 - max - min : max + min);
+  // Aman first — minimalist/restrained — wins on very-dark OR very-neutral
+  if (l < 0.25 || s < 0.15) return 'Aman';
+  // Auberge — warm hand-crafted (oranges/ambers in restrained chroma)
+  let h = 0; const dd = max - min;
+  if (dd > 0) {
+    if (max === r) h = (g - b) / dd + (g < b ? 6 : 0);
+    else if (max === g) h = (b - r) / dd + 2;
+    else h = (r - g) / dd + 4;
+    h = (h * 60 + 360) % 360;
+  }
+  if (h >= 15 && h <= 45 && s < 0.5) return 'Auberge';
+  // Default — Four Seasons (classic / traditional)
+  return 'Four Seasons';
+}
+
 function designHasImage(d) {
   if (!d) return false;
   if (BLANK_IDS.has(d.id)) return false;   // failed-generation blank — never show
@@ -14346,12 +14480,14 @@ app.post('/api/studio-renders/:designId/link-room', (req, res) => {
   } catch (e) { res.status(500).json({ ok: false, error: e.message }); }
 });
 
-// /studio HTML page
-app.get('/studio', (_req, res) => {
+// /designer-studio HTML page — formerly /studio. Renamed 2026-05-21 because
+// /studio is now the wallco / Studio sub-brand landing page (hand-painted-silk
+// register). This is the Studio composer (artists/styles/elements board).
+app.get('/designer-studio', (_req, res) => {
   res.type('html').send(`${htmlHead({
     title: 'Designer Studio — wallco.ai',
     description: 'Compose a new wallpaper design from artists, styles, and your own inspiration images',
-    canonical: 'https://wallco.ai/studio'
+    canonical: 'https://wallco.ai/designer-studio'
   })}
 <style>
   /* Studio-scoped overflow hygiene — forces single-col stack on phones by
diff --git a/server.js.bak-ai-rename b/server.js.bak-ai-rename
new file mode 100644
index 0000000..a924033
--- /dev/null
+++ b/server.js.bak-ai-rename
@@ -0,0 +1,15683 @@
+#!/usr/bin/env node
+/**
+ * wallco.ai — customer-facing storefront
+ * AI-original wallpaper and murals — every pattern generated, never repeated.
+ *
+ * Routes:
+ *   GET /              — hero + featured designs grid (home)
+ *   GET /designs       — full catalog grid with sort + density + filter
+ *   GET /design/:id    — design detail page
+ *   GET /murals        — mural-mode designs
+ *   GET /about         — AI provenance
+ *   GET /samples       — lead capture (no commerce in v1)
+ *   GET /health        — {ok, count}
+ *   POST /api/leads    — sample request lead capture → dw_unified.wallco_leads
+ *   GET /designs/img/:filename — serve design images
+ *   GET /api/designs   — JSON API (sort/filter/page)
+ *   GET /api/_inline/state — inline editor state (admin gate)
+ */
+'use strict';
+require('dotenv').config();
+
+const express  = require('express');
+const path     = require('path');
+const fs       = require('fs');
+const crypto   = require('crypto');
+const { spawn, spawnSync, execSync, execFileSync } = require('child_process');
+
+const app  = express();
+const PORT = parseInt(process.env.PORT || '9905', 10);
+const SITE = 'wallco.ai';
+const DATA_FILE = path.join(__dirname, 'data', 'designs.json');
+const IMG_DIR   = path.join(__dirname, 'data', 'generated');
+const ROOMS_DIR = path.join(__dirname, 'public', 'uploads', 'rooms');
+const SF_JOBS_DIR = path.join(__dirname, 'logs', 'spoonflower');
+const SF_PULL_DIR = path.join(__dirname, 'data', 'spoonflower-pulled');
+fs.mkdirSync(ROOMS_DIR, { recursive: true });
+fs.mkdirSync(SF_JOBS_DIR, { recursive: true });
+fs.mkdirSync(SF_PULL_DIR, { recursive: true });
+
+// Room generator + ElevenLabs config
+const ROOM_API = process.env.ROOM_API || 'http://45.61.58.125:3075/api/generate-room';
+const ELEVEN_KEY = process.env.ELEVENLABS_API_KEY || '';
+const ELEVEN_VOICE_ID = process.env.ELEVENLABS_VOICE_ID || '21m00Tcm4TlvDq8ikWAM'; // Rachel (default suggested)
+// Local trust-auth works; the URL form in .env is for prod / remote use.
+// Stick with the bare DB name so peer auth handles it cleanly.
+const DB_NAME = process.env.WALLCO_DB || 'dw_unified';
+const DB_URL = process.env.DATABASE_URL || DB_NAME;
+
+// On Linux (Kamatera) pm2 runs as root but PG has no 'root' role — sudo as
+// postgres. On macOS local trust auth works for the current user.
+const PSQL_CMD = (process.platform === 'linux')
+  ? `sudo -n -u postgres psql ${DB_NAME} -At -q`
+  : `psql ${DB_NAME} -At -q`;
+
+function psqlQuery(sql) {
+  return execSync(PSQL_CMD, { input: sql, encoding: 'utf8' }).trim();
+}
+
+// PG value escaper for inline SQL — matches the project's existing inline-string style.
+function pgEsc(v) {
+  if (v === null || v === undefined) return 'NULL';
+  if (typeof v === 'number') return Number.isFinite(v) ? String(v) : 'NULL';
+  if (typeof v === 'boolean') return v ? 'TRUE' : 'FALSE';
+  return "'" + String(v).replace(/'/g, "''") + "'";
+}
+
+// Admin-token check for destructive room ops. Empty/unset token => admin features locked off.
+const ADMIN_TOKEN = process.env.ADMIN_TOKEN || '';
+function isRoomsAdmin(req) {
+  if (!ADMIN_TOKEN) return false;
+  return (req.headers['x-admin-token'] === ADMIN_TOKEN)
+      || (req.query && req.query.admin === ADMIN_TOKEN);
+}
+
+// gzip + brotli on everything compressible. PNGs are already deflated so the
+// middleware filter skips them; HTML/JS/CSS/JSON drops ~70%. Threshold 1KB
+// avoids overhead for tiny responses. Bypass-able via X-No-Compress header.
+const compression = require('compression');
+app.use(compression({
+  threshold: 1024,
+  filter: (req, res) => {
+    if (req.headers['x-no-compress']) return false;
+    return compression.filter(req, res);
+  }
+}));
+
+// ── Marketplace early-mount: the Shopify orders/fulfilled webhook MUST register
+// before the global express.json() so the raw request body is preserved for
+// HMAC verification (express.raw runs on that route only).
+try { require('./src/marketplace').mountEarly(app); } catch (e) { console.error('Marketplace early mount failed:', e.message); }
+
+app.use(express.json({ limit: '25mb' }));  // 25mb to accept full-res baked PNGs from /api/design/:id/bake
+app.use(express.urlencoded({ extended: false }));
+
+// ── Chat layer: catalog + per-design variation
+try { require('./src/chat').mount(app); console.log('  Chat layer mounted'); } catch (e) { console.error('Chat mount failed:', e.message); }
+
+// ── Review layer: sliders + chips + chip-chat on /designs (admin-gated by hostname/query)
+try { require('./src/review').mount(app, () => DESIGNS); } catch (e) { console.error('Review mount failed:', e.message); }
+
+// Drop the Express version disclosure header (info-leak hardening per starsofdesign security audit 2026-05-12)
+app.disable('x-powered-by');
+
+// Rate limit the expensive AI endpoints (generation/compose/upload hit LLMs + Replicate)
+// 60 req/min/IP across /api/studio/* prevents a single bad actor from burning credits.
+try {
+  const rateLimit = require('express-rate-limit');
+  app.use('/api/studio', rateLimit({ windowMs: 60_000, max: 60, standardHeaders: 'draft-7', legacyHeaders: false }));
+} catch (_) { /* express-rate-limit not installed — non-fatal */ }
+
+// ── Admin layer: vendor browse + aesthetic search + inspiration pool (NO-LEAK — admin only)
+try { require('./src/admin').mount(app); console.log('  Admin layer mounted'); } catch (e) { console.error('Admin mount failed:', e.message); }
+
+// ── Marketplace layer: designer-powered marketplace (Patternbank × Material Bank × Spoonflower)
+try { require('./src/marketplace').mount(app); } catch (e) { console.error('Marketplace mount failed:', e.message); }
+
+// ── Social layer: share sheet + /reel maker (select up to 5 designs → MP4 reel)
+try { require('./src/social').mount(app); } catch (e) { console.error('Social mount failed:', e.message); }
+
+// Inject the universal share layer onto the designs grid + every design page.
+// Done as a response rewrite so the 863KB server.js page templates stay untouched.
+app.use((req, res, next) => {
+  if (req.path === '/designs' || /^\/design\/\d+$/.test(req.path)) {
+    const origSend = res.send.bind(res);
+    res.send = (body) => {
+      if (typeof body === 'string' && body.indexOf('</body>') !== -1
+          && body.indexOf('social-share.js') === -1) {
+        body = body.replace('</body>',
+          '<script src="/js/social-share.js" defer></script>\n</body>');
+      }
+      return origSend(body);
+    };
+  }
+  next();
+});
+
+// ── Cache-Control: no-store on HTML (per feedback_cloudflare_html_caching.md)
+app.use((req, res, next) => {
+  if (req.accepts('html') && !req.path.startsWith('/designs/img')) {
+    res.setHeader('Cache-Control', 'no-store, must-revalidate');
+  }
+  next();
+});
+
+// ── Static: uploaded assets + general /public (css, js, og images, favicons)
+app.use('/uploads', express.static(path.join(__dirname, 'public', 'uploads'), { maxAge: '7d' }));
+app.use(express.static(path.join(__dirname, 'public'), { maxAge: '1h' }));
+
+// ── Serve generated design images
+app.use('/designs/img', express.static(IMG_DIR, {
+  maxAge: '30d',
+  setHeaders: (res, filePath) => {
+    res.setHeader('Cache-Control', 'public, max-age=2592000, immutable');
+  }
+}));
+
+// ── 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')
+}));
+
+// ── robots.txt — allow all + reference the sitemap.
+// Last-Modified pinned to process start (robots policy rarely changes; if it
+// does, the deploy/restart updates the timestamp). Override the global
+// no-store middleware with 1-day cacheable to maximize crawler win.
+const ROBOTS_LAST_MODIFIED = new Date();
+app.get('/robots.txt', (req, res) => {
+  res.setHeader('Cache-Control', 'public, max-age=86400, must-revalidate');
+  if (maybe304(req, res, ROBOTS_LAST_MODIFIED)) return;
+  res.type('text/plain').send(
+    `User-agent: *\nAllow: /\nDisallow: /admin/\nDisallow: /api/\nDisallow: /_devlogin\n\nSitemap: https://${SITE}/sitemap-index.xml\nSitemap: https://${SITE}/sitemap.xml\nSitemap: https://${SITE}/sitemap-marketplace.xml\n`
+  );
+});
+
+// ── sitemap.xml — static pages + every published design.
+// Priority ranks by saturation (more visually striking = better landing page);
+// designs with a room mockup get a +0.1 bonus capped at 0.9. Cached 1h.
+let SITEMAP_CACHE = { body: '', exp: 0 };
+app.get('/sitemap.xml', (req, res) => {
+  // Conditional GET — sitemap freshness = freshest design's created_at.
+  res.setHeader('Cache-Control', 'public, max-age=3600, must-revalidate');
+  if (maybe304(req, res, CATALOG_LAST_MODIFIED)) return;
+  const now = Date.now();
+  if (SITEMAP_CACHE.body && SITEMAP_CACHE.exp > now) {
+    return res.type('application/xml').send(SITEMAP_CACHE.body);
+  }
+  const today = new Date().toISOString().slice(0, 10);
+  const staticPages = [
+    { loc: '/',          priority: '1.0', freq: 'daily'   },
+    { loc: '/designs',   priority: '0.9', freq: 'daily'   },
+    { loc: '/murals',    priority: '0.7', freq: 'weekly'  },
+    { loc: '/compare',   priority: '0.5', freq: 'monthly' },
+    { loc: '/samples',   priority: '0.6', freq: 'monthly' },
+    { loc: '/about',     priority: '0.5', freq: 'monthly' },
+    // /provenance was merged into /about — removed from sitemap, route below
+    // 301s any inbound link to /about so external citations don't 404.
+  ];
+  const urls = [];
+  for (const p of staticPages) {
+    urls.push(`  <url><loc>https://${SITE}${p.loc}</loc><changefreq>${p.freq}</changefreq><priority>${p.priority}</priority><lastmod>${today}</lastmod></url>`);
+  }
+  // Note: is_published is currently false for every row in designs.json (admin
+  // hasn't run the publish step yet) but /designs renders them all — so the
+  // sitemap mirrors the live catalog. If/when a "draft" flag is introduced,
+  // gate here.
+  for (const d of DESIGNS) {
+    const sat = Math.max(0, Math.min(1, Number(d.saturation) || 0));
+    const hasRoom = (d.room_mockups || []).length > 0 ? 0.1 : 0;
+    const prio = Math.min(0.9, 0.5 + 0.3 * sat + hasRoom).toFixed(2);
+    const lastmod = (d.created_at || '').slice(0, 10) || today;
+    urls.push(`  <url><loc>https://${SITE}/design/${d.id}</loc><changefreq>monthly</changefreq><priority>${prio}</priority><lastmod>${lastmod}</lastmod></url>`);
+  }
+  const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls.join('\n')}\n</urlset>\n`;
+  SITEMAP_CACHE = { body: xml, exp: now + 60 * 60 * 1000 };
+  res.type('application/xml').send(xml);
+});
+
+// ── Load design catalog (snapshot from DB — never stale-live Shopify)
+let DESIGNS = [];
+let CATALOG_LAST_MODIFIED = new Date();  // freshest design.created_at across the catalog
+
+// Failed-generation guard — design ids whose image is a near-white blank
+// (a botched batch, e.g. the drunk-animals run, ids ~7679+). Built by
+// scripts/detect-blank-designs.js → data/blank-designs.json. Excluded from
+// every grid/listing so an empty batch can't paper page 1 with blank tiles.
+let BLANK_IDS = new Set();
+function loadBlankIds() {
+  try {
+    const j = JSON.parse(fs.readFileSync(path.join(__dirname, 'data', 'blank-designs.json'), 'utf8'));
+    BLANK_IDS = new Set(j.blank_ids || []);
+    console.log(`[blank-guard] ${BLANK_IDS.size} blank designs excluded from listings`);
+  } catch (e) {
+    BLANK_IDS = new Set();
+    console.error('[blank-guard] no blank-designs.json — blank filter inactive:', e.message);
+  }
+}
+
+// ── Aesthetic gate (Steve 2026-05-19) ───────────────────────────────────────
+// Defence-in-depth against neon + multi-hue patterns. The DB-level purge
+// (scripts/purge-neon-and-multicolor.js) already removed the offenders, but
+// this runtime filter protects against any reseed / refresh_designs_snapshot
+// run that might re-introduce a row matching the rule. Drops:
+//   - design where any palette entry is neon (HSL S≥70, L 45-75, pct≥5)
+//   - design where palette has ≥ 4 distinct hue buckets
+//   - design where dominant_hex alone is neon (single-color fallback for
+//     rows without a full palette)
+//   - design where prompt / motifs / tags match the neon/rainbow regex
+function _hexToHsl(hex) {
+  if (!hex || typeof hex !== 'string') return null;
+  const m = hex.trim().match(/^#?([0-9a-f]{6})$/i);
+  if (!m) return null;
+  const n = parseInt(m[1], 16);
+  const r = ((n >> 16) & 0xff) / 255;
+  const g = ((n >> 8)  & 0xff) / 255;
+  const b = ( n        & 0xff) / 255;
+  const mx = Math.max(r, g, b), mn = Math.min(r, g, b);
+  const l  = (mx + mn) / 2;
+  let h = 0, s = 0;
+  if (mx !== mn) {
+    const d = mx - mn;
+    s = l > 0.5 ? d / (2 - mx - mn) : d / (mx + mn);
+    switch (mx) {
+      case r: h = (g - b) / d + (g < b ? 6 : 0); break;
+      case g: h = (b - r) / d + 2; break;
+      case b: h = (r - g) / d + 4; break;
+    }
+    h *= 60;
+  }
+  return { h, s: s * 100, l: l * 100 };
+}
+function _hueBucket(hex) {
+  const hsl = _hexToHsl(hex);
+  if (!hsl) return 'unknown';
+  if (hsl.s < 12 || hsl.l < 10 || hsl.l > 90) return 'neutral';
+  const h = hsl.h;
+  if (h < 15) return 'red'; if (h < 45) return 'orange'; if (h < 65) return 'yellow';
+  if (h < 90) return 'chartreuse'; if (h < 150) return 'green'; if (h < 180) return 'teal';
+  if (h < 210) return 'cyan'; if (h < 250) return 'blue'; if (h < 280) return 'indigo';
+  if (h < 310) return 'violet'; if (h < 340) return 'magenta'; return 'red';
+}
+function _isNeonHex(hex, pct) {
+  const hsl = _hexToHsl(hex);
+  if (!hsl) return false;
+  return hsl.s >= 70 && hsl.l >= 45 && hsl.l <= 75 && (pct == null || pct >= 5);
+}
+const _AESTHETIC_GATE_RE = /\b(neon|fluorescent|electric|rainbow|day[- ]?glo|highlighter|fluoro)\b/i;
+function passesAestheticGate(d) {
+  if (!d) return false;
+  // 1) palette pass (if present)
+  const palette = Array.isArray(d.palette) ? d.palette : null;
+  if (palette) {
+    const buckets = new Set();
+    for (const p of palette) {
+      const b = _hueBucket(p.hex);
+      if (b !== 'neutral' && b !== 'unknown') buckets.add(b);
+      if (_isNeonHex(p.hex, p.pct)) return false;
+    }
+    if (buckets.size >= 4) return false;
+  } else if (d.dominant_hex) {
+    // 2) single-color fallback when no palette is available
+    if (_isNeonHex(d.dominant_hex, 100)) return false;
+  }
+  // 3) Prompt/tags/motifs text-regex was REMOVED 2026-05-20. It was dropping
+  // 134/212 valid designs because of stray "fluorescent" / "electric" / etc.
+  // in legitimate anti-prompt language. Palette check (above) is authoritative
+  // for neon detection; the text regex was too brittle. If a design has
+  // chromatic neon hex codes, the palette filter catches it. The text never
+  // needed to be a filter — it was belt-and-suspenders that became too strict.
+  return true;
+}
+
+function loadDesigns() {
+  try {
+    const raw = JSON.parse(fs.readFileSync(DATA_FILE, 'utf8'));
+    // Hide soft-removed designs, then drop anything that fails the aesthetic
+    // gate (neon / >3 hues / rainbow tags).
+    const beforeGate = raw.filter(d => !d.user_removed);
+    DESIGNS = beforeGate.filter(passesAestheticGate);
+    const dropped = beforeGate.length - DESIGNS.length;
+    if (dropped > 0) console.log(`[aesthetic-gate] dropped ${dropped} designs at load (neon/multi-hue/rainbow)`);
+    let max = 0;
+    for (const d of DESIGNS) {
+      const t = Date.parse(d.created_at || '');
+      if (t && t > max) max = t;
+    }
+    CATALOG_LAST_MODIFIED = max ? new Date(max) : new Date();
+  } catch (e) {
+    console.error('Failed to load designs.json:', e.message);
+    DESIGNS = [];
+  }
+  loadBlankIds();
+}
+loadDesigns();
+
+// Conditional-GET helper: if If-Modified-Since header matches our lastmod (1-second
+// precision since HTTP-date drops millis), emit 304 with empty body. Crawlers like
+// Googlebot/Bingbot use this to skip full-body downloads on repeat visits.
+// Returns true if a 304 was sent (caller should bail). HEAD requests handled too.
+function maybe304(req, res, lastModifiedDate) {
+  if (!lastModifiedDate || isNaN(lastModifiedDate.getTime())) return false;
+  // HTTP-date truncates to 1s; floor both sides to match.
+  const lastMs = Math.floor(lastModifiedDate.getTime() / 1000) * 1000;
+  res.setHeader('Last-Modified', new Date(lastMs).toUTCString());
+  const ims = req.headers['if-modified-since'];
+  if (ims) {
+    const imsMs = Date.parse(ims);
+    if (!isNaN(imsMs) && imsMs >= lastMs) {
+      res.status(304).end();
+      return true;
+    }
+  }
+  return false;
+}
+
+// ── Levenshtein distance (early-exit at cap). Used for did-you-mean suggestions.
+function _lev(a, b, cap) {
+  if (a === b) return 0;
+  if (!a.length) return b.length;
+  if (!b.length) return a.length;
+  if (Math.abs(a.length - b.length) > cap) return cap + 1;
+  let prev = new Array(b.length + 1);
+  for (let j = 0; j <= b.length; j++) prev[j] = j;
+  for (let i = 1; i <= a.length; i++) {
+    let cur = [i];
+    let rowMin = i;
+    for (let j = 1; j <= b.length; j++) {
+      const cost = a[i-1] === b[j-1] ? 0 : 1;
+      const v = Math.min(prev[j] + 1, cur[j-1] + 1, prev[j-1] + cost);
+      cur.push(v);
+      if (v < rowMin) rowMin = v;
+    }
+    if (rowMin > cap) return cap + 1;
+    prev = cur;
+  }
+  return prev[b.length];
+}
+
+// ── Same-origin proxy for the Design Coordinate UI block. The upstream
+// pairs.designerwallcoverings.com returns valid JSON but its prod CORS
+// allowlist doesn't include 127.0.0.1:9792 / localhost, so browsers
+// silently fall into the .catch() ("Could not load coordinates right
+// now."). Proxy through wallco-ai so the call is same-origin.
+const PAIRS_UPSTREAM = process.env.PAIRS_UPSTREAM || 'https://pairs.designerwallcoverings.com';
+app.get('/api/coordinates/pairs', async (req, res) => {
+  try {
+    const qs = new URLSearchParams();
+    for (const [k, v] of Object.entries(req.query)) {
+      if (v !== undefined && v !== null) qs.set(k, String(v));
+    }
+    const r = await fetch(`${PAIRS_UPSTREAM}/api/pairs?${qs.toString()}`, {
+      headers: { 'User-Agent': 'wallco-ai/proxy' },
+    });
+    const txt = await r.text();
+    res.status(r.status).type(r.headers.get('content-type') || 'application/json').send(txt);
+  } catch (e) {
+    res.status(502).json({ ok: false, error: 'upstream_unreachable', detail: e.message });
+  }
+});
+app.get('/api/coordinates/ai', async (req, res) => {
+  try {
+    const qs = new URLSearchParams();
+    for (const [k, v] of Object.entries(req.query)) {
+      if (v !== undefined && v !== null) qs.set(k, String(v));
+    }
+    const r = await fetch(`${PAIRS_UPSTREAM}/api/ai-coordinates?${qs.toString()}`, {
+      headers: { 'User-Agent': 'wallco-ai/proxy' },
+    });
+    const txt = await r.text();
+    res.status(r.status).type(r.headers.get('content-type') || 'application/json').send(txt);
+  } catch (e) {
+    res.status(502).json({ ok: false, error: 'upstream_unreachable', detail: e.message });
+  }
+});
+
+// ── Lightweight in-memory search across the catalog.
+// /api/designs/search?q=<term>&limit=20
+// Scores each design on motif (3), title (2), category/handle (1) substring match.
+// Returns top-N by score then by saturation. Pure read-only, no DB.
+// On 0 hits, suggests near-matches via Levenshtein over motif + category vocab.
+app.get('/api/designs/search', (req, res) => {
+  const qRaw = String(req.query.q || '').toLowerCase().trim();
+  const limit = Math.max(1, Math.min(50, parseInt(req.query.limit || '20', 10)));
+  if (!qRaw || qRaw.length < 2) {
+    return res.json({ q: qRaw, limit, count: 0, results: [], note: 'q must be ≥2 chars' });
+  }
+  // Conditional GET on the catalog freshness so crawlers benefit.
+  if (maybe304(req, res, CATALOG_LAST_MODIFIED)) return;
+  res.setHeader('Cache-Control', 'public, max-age=300, must-revalidate');
+  const terms = qRaw.split(/\s+/).filter(Boolean);
+  const out = [];
+  for (const d of DESIGNS) {
+    if (BLANK_IDS.has(d.id)) continue;   // skip failed-generation blanks
+    const title = (d.title || '').toLowerCase();
+    const category = (d.category || '').toLowerCase();
+    const handle = (d.handle || '').toLowerCase();
+    const motifs = (d.motifs || []).map(m => m.toLowerCase());
+    let score = 0;
+    for (const t of terms) {
+      if (motifs.some(m => m === t))      score += 5;     // exact motif match — strongest
+      else if (motifs.some(m => m.includes(t))) score += 3;
+      if (title.includes(t))    score += 2;
+      if (category === t)       score += 2;
+      else if (category.includes(t)) score += 1;
+      if (handle.includes(t))   score += 1;
+    }
+    if (score > 0) {
+      out.push({
+        id: d.id,
+        title: d.title,
+        category: d.category,
+        handle: d.handle,
+        image_url: d.image_url,
+        dominant_hex: d.dominant_hex,
+        room_mockups: d.room_mockups || [],
+        score,
+        saturation: d.saturation || 0,
+        url: `/design/${d.id}`,
+      });
+    }
+  }
+  // Did-you-mean: if no hits, search the vocabulary for near-matches to each term.
+  // Cap=2 edit distance; cheapest 5 unique candidates.
+  let didYouMean = [];
+  if (out.length === 0) {
+    const vocab = new Set();
+    for (const d of DESIGNS) {
+      (d.motifs || []).forEach(m => { const v = String(m).toLowerCase().trim(); if (v.length >= 3) vocab.add(v); });
+      if (d.category) vocab.add(String(d.category).toLowerCase().trim());
+    }
+    const candidates = new Map();
+    const cap = 2;
+    for (const t of terms) {
+      if (t.length < 3) continue; // too short — Levenshtein noise
+      for (const w of vocab) {
+        const d = _lev(t, w, cap);
+        if (d <= cap && w !== t) {
+          const prev = candidates.get(w);
+          if (prev === undefined || d < prev) candidates.set(w, d);
+        }
+      }
+    }
+    didYouMean = Array.from(candidates.entries())
+      .sort((a, b) => a[1] - b[1] || a[0].localeCompare(b[0]))
+      .slice(0, 5)
+      .map(([word, distance]) => ({ word, distance }));
+  }
+
+  out.sort((a, b) => b.score - a.score || b.saturation - a.saturation);
+  // Facet counts — rolled up from the matched designs.
+  // Lets a UI show "Refine: rose (12) · peony (8) · scrollwork (5)" alongside results.
+  const motifTally = Object.create(null);
+  const categoryTally = Object.create(null);
+  for (const r of out) {
+    // r holds the projected shape; pull the source design back for full motifs list.
+    const src = DESIGNS.find(d => d.id === r.id);
+    if (!src) continue;
+    for (const m of (src.motifs || [])) {
+      const k = String(m).toLowerCase().trim();
+      if (k) motifTally[k] = (motifTally[k] || 0) + 1;
+    }
+    if (src.category) categoryTally[src.category] = (categoryTally[src.category] || 0) + 1;
+  }
+  const topMotifs = Object.entries(motifTally)
+    .sort((a, b) => b[1] - a[1])
+    .slice(0, 8)
+    .map(([motif, count]) => ({ motif, count }));
+  const topCategories = Object.entries(categoryTally)
+    .sort((a, b) => b[1] - a[1])
+    .map(([category, count]) => ({ category, count }));
+  // Log the search query (audit + future analytics).  Only logs when q≥2 chars
+  // so the early-return path above doesn't double-log empty probes.
+  logEvent(req, 'designs_search', { q: qRaw, count: out.length, did_you_mean: didYouMean.map(d => d.word) });
+  res.json({
+    q: qRaw,
+    limit,
+    count: out.length,
+    results: out.slice(0, limit),
+    facets: {
+      motifs: topMotifs,
+      categories: topCategories,
+    },
+    did_you_mean: didYouMean,
+  });
+});
+
+// ── /api/designs/by-color?hex=%23c16d74&n=12&exclude=72
+// Returns N closest-in-color designs to a target hex via CIELAB ΔE distance.
+// Useful for "more like this in this palette" rails on the PDP. Read-only, no DB.
+function _hexToLab(hex) {
+  let h = String(hex || '').replace('#','').trim();
+  if (h.length === 3) h = h.split('').map(c => c+c).join('');
+  if (!/^[0-9a-f]{6}$/i.test(h)) return null;
+  let r = parseInt(h.slice(0,2),16)/255, g = parseInt(h.slice(2,4),16)/255, b = parseInt(h.slice(4,6),16)/255;
+  // sRGB → linear
+  [r,g,b] = [r,g,b].map(v => v <= 0.04045 ? v/12.92 : Math.pow((v+0.055)/1.055, 2.4));
+  // linear → XYZ (D65)
+  const X = (r*0.4124564 + g*0.3575761 + b*0.1804375) / 0.95047;
+  const Y = (r*0.2126729 + g*0.7151522 + b*0.0721750) / 1.00000;
+  const Z = (r*0.0193339 + g*0.1191920 + b*0.9503041) / 1.08883;
+  // XYZ → Lab
+  const f = (t) => t > 0.008856 ? Math.cbrt(t) : (7.787*t + 16/116);
+  const fx = f(X), fy = f(Y), fz = f(Z);
+  return [ 116*fy - 16, 500*(fx-fy), 200*(fy-fz) ];
+}
+function _dE(a, b) {
+  const dL = a[0]-b[0], dA = a[1]-b[1], dB = a[2]-b[2];
+  return Math.sqrt(dL*dL + dA*dA + dB*dB);
+}
+// In-memory TTL cache for /api/designs/by-color. The Lab conversion + ΔE sort
+// runs over every DESIGN on every call — the same PDP rail hex + size combo
+// gets hammered as users browse, so cache the JSON body for the same 5min
+// window the response already advertises in Cache-Control. Bounded to 1k keys
+// FIFO so memory can't grow unbounded; invalidated whenever DESIGNS is rebuilt
+// (callers should bump _byColorCacheVer if they mutate DESIGNS in place).
+const _BY_COLOR_TTL_MS = 300_000;
+const _BY_COLOR_MAX = 1000;
+const _byColorCache = new Map();   // key -> { body, expiresAt }
+let   _byColorCacheVer = 0;        // bump to invalidate
+const _byColorStats = { hits: 0, misses: 0, evictions: 0, since: Date.now() };
+// /api/designs/random ETag stats — only seeded calls participate (unseeded is no-store).
+// "hits" = 304 returned (client revalidated), "misses" = full 200 with body.
+const _randomStats = { hits: 0, misses: 0, since: Date.now() };
+app.get('/api/designs/by-color', (req, res) => {
+  const hex = String(req.query.hex || '').trim();
+  const n = Math.max(1, Math.min(50, parseInt(req.query.n || '12', 10)));
+  const excludeArr = String(req.query.exclude || '').split(',').map(s => parseInt(s.trim(),10)).filter(x => x>0).sort((a,b)=>a-b);
+  const exclude = new Set(excludeArr);
+  // Optional ?within=category — restricts results to designs in this category.
+  // Useful for "more in this palette WITHIN damask" rails on a vertical PDP.
+  const within = String(req.query.within || '').toLowerCase().trim();
+  const target = _hexToLab(hex);
+  if (!target) return res.status(400).json({ ok:false, error:'invalid hex (try ?hex=%23c16d74 or hex=c16d74)' });
+  // Cache key sorts exclude so e.g. exclude=72,5 and exclude=5,72 hit the same entry.
+  const cacheKey = `v${_byColorCacheVer}|${hex.toLowerCase()}|${n}|${within}|${excludeArr.join(',')}`;
+  // ETag wraps the same key — deterministic given (catalog version, hex, n, within, sorted excludes).
+  // Lets browsers/CDNs skip the JSON body transfer on revalidation; pairs with the in-memory cache
+  // (cache saves recompute, ETag saves the bytes on the wire). Wrapped in double quotes per RFC.
+  const _bcEtag = `"bc-${CATALOG_LAST_MODIFIED.getTime()}-${cacheKey}"`;
+  if (req.headers['if-none-match'] === _bcEtag) {
+    _byColorStats.hits++;
+    res.setHeader('ETag', _bcEtag);
+    res.setHeader('Cache-Control', 'public, max-age=300, must-revalidate');
+    res.setHeader('X-Cache', 'HIT-ETAG');
+    return res.status(304).end();
+  }
+  const now = Date.now();
+  const hit = _byColorCache.get(cacheKey);
+  if (hit && hit.expiresAt > now) {
+    _byColorStats.hits++;
+    res.setHeader('ETag', _bcEtag);
+    res.setHeader('Cache-Control', 'public, max-age=300, must-revalidate');
+    res.setHeader('X-Cache', 'HIT');
+    return res.json(hit.body);
+  }
+  if (hit) _byColorCache.delete(cacheKey);   // expired — drop before recompute
+  _byColorStats.misses++;
+  const ranked = [];
+  for (const d of DESIGNS) {
+    if (exclude.has(d.id)) continue;
+    if (BLANK_IDS.has(d.id)) continue;   // skip failed-generation blanks
+    if (!d.dominant_hex) continue;
+    if (within && (d.category || '').toLowerCase() !== within) continue;
+    const lab = _hexToLab(d.dominant_hex);
+    if (!lab) continue;
+    ranked.push({ id: d.id, title: d.title, category: d.category, handle: d.handle, image_url: d.image_url, dominant_hex: d.dominant_hex, room_mockups: d.room_mockups || [], delta_e: _dE(target, lab), url: `/design/${d.id}` });
+  }
+  ranked.sort((a,b) => a.delta_e - b.delta_e);
+  const body = { hex, n, within: within || null, count: ranked.length, exclude: [...exclude], results: ranked.slice(0, n).map(r => ({ ...r, delta_e: Math.round(r.delta_e * 10)/10 })) };
+  // FIFO evict if full. Map preserves insertion order, so the first key is the oldest.
+  if (_byColorCache.size >= _BY_COLOR_MAX) {
+    const firstKey = _byColorCache.keys().next().value;
+    if (firstKey !== undefined) { _byColorCache.delete(firstKey); _byColorStats.evictions++; }
+  }
+  _byColorCache.set(cacheKey, { body, expiresAt: now + _BY_COLOR_TTL_MS });
+  res.setHeader('ETag', _bcEtag);
+  res.setHeader('Cache-Control', 'public, max-age=300, must-revalidate');
+  res.setHeader('X-Cache', 'MISS');
+  res.json(body);
+});
+
+// ── /api/designs/random?n=12&category=floral&seed=42
+// Returns N random designs. Useful for "Surprise me" rails + empty-state discovery.
+// Optional ?category= filter. Optional ?seed= for reproducible shuffles (otherwise time-seeded).
+// Cache-Control: no-store — randomness is the point; caching defeats it.
+app.get('/api/designs/random', (req, res) => {
+  const n = Math.max(1, Math.min(50, parseInt(req.query.n || '12', 10)));
+  const category = String(req.query.category || '').toLowerCase().trim();
+  const seedRaw = req.query.seed;
+  // Hard filter — same as /api/designs. Never feed Hot-or-not or random
+  // grids a design whose image file isn't on disk. Steve's rule 2026-05-13.
+  const onDisk = DESIGNS.filter(designHasImage);
+  const pool = category
+    ? onDisk.filter(d => (d.category || '').toLowerCase() === category)
+    : onDisk.slice();
+  // Deterministic seeded PRNG (mulberry32) if seed provided; else Math.random.
+  let rand;
+  if (seedRaw !== undefined && seedRaw !== '') {
+    let s = parseInt(seedRaw, 10) >>> 0;
+    rand = function() {
+      s |= 0; s = (s + 0x6D2B79F5) | 0;
+      let t = s;
+      t = Math.imul(t ^ (t >>> 15), t | 1);
+      t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
+      return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
+    };
+  } else {
+    rand = Math.random;
+  }
+  // Partial Fisher-Yates — picks first N positions of shuffle without touching the rest.
+  const sample = [];
+  const idx = pool.map((_, i) => i);
+  const k = Math.min(n, idx.length);
+  for (let i = 0; i < k; i++) {
+    const j = i + Math.floor(rand() * (idx.length - i));
+    [idx[i], idx[j]] = [idx[j], idx[i]];
+    const d = pool[idx[i]];
+    sample.push({
+      id: d.id,
+      title: d.title,
+      category: d.category,
+      handle: d.handle,
+      image_url: d.image_url,
+      dominant_hex: d.dominant_hex,
+      room_mockups: d.room_mockups || [],
+      url: `/design/${d.id}`,
+    });
+  }
+  // Seeded calls are deterministic given (n, category, seed, DESIGNS state) — make them
+  // cacheable via ETag so a trade-user rail can poll without paying the shuffle cost.
+  // Unseeded calls stay no-store because the whole point is fresh randomness.
+  if (seedRaw !== undefined && seedRaw !== '') {
+    const etag = `"rand-${CATALOG_LAST_MODIFIED.getTime()}-${k}-${category || 'all'}-${parseInt(seedRaw,10)}-${pool.length}"`;
+    res.setHeader('ETag', etag);
+    res.setHeader('Cache-Control', 'private, max-age=0, must-revalidate');
+    const inm = req.headers['if-none-match'];
+    if (inm && inm === etag) { _randomStats.hits++; return res.status(304).end(); }
+    _randomStats.misses++;
+  } else {
+    res.setHeader('Cache-Control', 'no-store');
+  }
+  res.json({
+    n: k,
+    pool_size: pool.length,
+    category: category || null,
+    seed: seedRaw !== undefined && seedRaw !== '' ? parseInt(seedRaw,10) : null,
+    results: sample,
+  });
+});
+
+// ── Color sort helpers
+function hexToHSL(hex) {
+  if (!hex) return [0, 0, 0.5];
+  const h = hex.replace('#', '');
+  const r = parseInt(h.slice(0,2),16)/255;
+  const g = parseInt(h.slice(2,4),16)/255;
+  const b = parseInt(h.slice(4,6),16)/255;
+  const max = Math.max(r,g,b), min = Math.min(r,g,b);
+  const l = (max+min)/2;
+  let hue = 0, sat = 0;
+  if (max !== min) {
+    const d = max - min;
+    sat = l > 0.5 ? d/(2-max-min) : d/(max+min);
+    if (max===r) hue = (g-b)/d + (g<b?6:0);
+    else if (max===g) hue = (b-r)/d + 2;
+    else hue = (r-g)/d + 4;
+    hue *= 60;
+  }
+  return [hue, sat, l];
+}
+
+// Saturation (0..1) from hex — used by /design/:id PG fallback.
+function hexSaturation(hex) { return Math.round(hexToHSL(hex)[1] * 1000) / 1000; }
+
+// ── Snapshot-style title generator (mirror of refresh_designs_snapshot.py).
+// Used by /design/:id PG fallback so freshly-baked designs land with proper
+// "Honey Garden No.83"-style titles, not the raw prompt prefix.
+const HUE_NAMES = [
+  [  0, 18, 'Crimson'], [ 18, 38, 'Amber'],  [ 38, 58, 'Honey'],
+  [ 58, 78, 'Olive'],   [ 78,160, 'Sage'],   [160,200, 'Teal'],
+  [200,255, 'Sapphire'],[255,295, 'Violet'], [295,335, 'Rose'],
+  [335,360, 'Crimson']
+];
+const NOUNS = {
+  floral:    ['Bouquet','Blossom','Garden','Floret','Botanical','Bloom'],
+  geometric: ['Lattice','Tessellation','Geometry','Meridian','Compass','Mosaic'],
+  damask:    ['Damask','Baroque','Heirloom','Salon','Atrium','Reverie'],
+  mixed:     ['Atelier','Studio','Origin','Folio','Sketch','Edit'],
+  'art deco':       ['Deco','Fan','Sunburst','Chevron','Marquee'],
+  'arts and crafts':['Morris','Crafts','Bough','Trellis','Vine'],
+  'drunk-animals':  ['Hiccup','Tipple','Tableau','Reverie','Soirée','Vignette','Carouse','Caprice','Saunter','Folly'],
+  'stoned-animals': ['Stealie','Trip','Stardust','Daydream','Vespers','Hazefield','Sundrift','Spore','Lullaby','Halcyon'],
+  // Iksel / de Gournay / Mural Sources luxe-mural vocabulary.
+  'mural-animals':  ['Mural','Panel','Pavillon','Atelier','Hand-Painted','Vignette','Chinoiserie','Heirloom','Salon','Reverie'],
+  // Panoramic scenic — Zuber et Cie / Gracie / de Gournay tradition.
+  'mural-scenic':   ['Scenic','Panorama','Vista','Frieze','Tapestry','Folly','Allée','Belvedere','Promenade','Pavillon']
+};
+function hueName(hex) {
+  if (!hex) return 'Bone';
+  const [h, s] = hexToHSL(hex);
+  if (s < 0.06) return 'Noir';
+  for (const r of HUE_NAMES) if (h >= r[0] && h < r[1]) return r[2];
+  return 'Bone';
+}
+function titleFor(cat, hex, id) {
+  const pool = NOUNS[(cat || 'mixed').toLowerCase()] || NOUNS.mixed;
+  return `${hueName(hex)} ${pool[id % pool.length]} No.${id}`;
+}
+
+// Card aria-label — folds title/category/dominant hue + room-preview count into
+// a single sentence so screen readers announce all the visible affordances of a
+// card in one read. Replaces the silence on the background-image divs.
+function cardAriaLabel(d) {
+  const hue = hueName(d.dominant_hex || '').toLowerCase();
+  const cat = (d.category || 'pattern').toLowerCase();
+  const rooms = (d.room_mockups || []).length;
+  const roomTxt = rooms > 1 ? `, ${rooms} room previews available`
+                 : rooms === 1 ? ', 1 room preview available' : '';
+  return `${d.title} — ${hue} ${cat} wallpaper${roomTxt}. View details.`;
+}
+
+function sortDesigns(designs, sort) {
+  const d = [...designs];
+  // Designs with a pre-generated room preview surface first within any sort —
+  // they're the "in-context" winners visitors should encounter first. Tie-break
+  // falls through to whatever sort the user picked.
+  const hasRoom = x => ((x.room_mockups || []).length > 0) ? 0 : 1;
+  const tie = (cmp) => (a,b) => {
+    const r = hasRoom(a) - hasRoom(b);
+    return r !== 0 ? r : cmp(a,b);
+  };
+  // Saturation helper — HSL S channel; neutrals fall to the end.
+  const sat = x => { try { return (hexToHSL(x.dominant_hex)||[0,0,0])[1] || 0; } catch { return 0; } };
+  switch (sort) {
+    case 'oldest':       return d.sort((a,b) => (a.id||0) - (b.id||0));
+    case 'title':        return d.sort(tie((a,b) => a.title.localeCompare(b.title)));
+    case 'title-desc':   return d.sort(tie((a,b) => b.title.localeCompare(a.title)));
+    case 'category':     return d.sort(tie((a,b) => a.category.localeCompare(b.category)));
+    case 'category-desc':return d.sort(tie((a,b) => b.category.localeCompare(a.category)));
+    case 'light-dark':   return d.sort(tie((a,b) => hexToHSL(b.dominant_hex)[2] - hexToHSL(a.dominant_hex)[2]));
+    case 'dark-light':   return d.sort(tie((a,b) => hexToHSL(a.dominant_hex)[2] - hexToHSL(b.dominant_hex)[2]));
+    case 'vivid':        return d.sort(tie((a,b) => sat(b) - sat(a)));
+    case 'muted':        return d.sort(tie((a,b) => sat(a) - sat(b)));
+    case 'has-room':     return d.sort((a,b) => {
+      const r = hasRoom(a) - hasRoom(b);
+      return r !== 0 ? r : (b.id||0) - (a.id||0);
+    });
+    case 'random': {
+      // Deterministic-per-request shuffle — keeps pagination consistent within
+      // a session by seeding off the floor of the current minute.
+      const seed = Math.floor(Date.now() / 60000);
+      const rand = (n) => { const x = Math.sin(seed * 9301 + n * 49297) * 233280; return x - Math.floor(x); };
+      return d.map((x,i) => ({ x, k: rand(i) })).sort((a,b) => a.k - b.k).map(o => o.x);
+    }
+    case 'color-wheel': return d.sort(tie((a,b) => {
+      const [ha,,la] = hexToHSL(a.dominant_hex);
+      const [hb,,lb] = hexToHSL(b.dominant_hex);
+      const bA = Math.floor(ha/30), bB = Math.floor(hb/30);
+      return bA !== bB ? bA-bB : la-lb;
+    }));
+    // newest = strict highest id first. NO has-room tie-break — when a
+    // visitor explicitly asks for newest, hide-no-rooms-promote-rooms would
+    // sandbag the actual latest designs (the drunk-animals + stoned-animals
+    // currently generating don't have room_mockups yet).
+    default: return d.sort((a,b) => (b.id||0) - (a.id||0));
+  }
+}
+
+// ── Go-Live status (admin checklist)
+app.get('/admin/golive.json', async (_req, res) => {
+  try {
+    // Run the same module the CLI uses, but inline so we can stream
+    const { main } = require('./scripts/golive_status');
+    // Capture stdout from main() — easiest: run the same probes here
+    const out = execSync(`node ${path.join(__dirname, 'scripts/golive_status.js')}`, { encoding: 'utf8', timeout: 25000 });
+    res.type('application/json').send(out.trim());
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+app.get('/admin/golive', (_req, res) => {
+  res.type('html').send(`<!doctype html>
+<html lang="en"><head>
+<meta charset="utf-8"><title>wallco.ai — Go-Live Checklist</title>
+<style>
+  body { font:14px/1.5 -apple-system,system-ui,sans-serif; margin:0; padding:24px 32px; background:#0f0e0c; color:#e8e2d6; }
+  h1 { font-weight:300; font-size:24px; margin:0 0 4px; letter-spacing:.5px; }
+  .sub { color:#888; font-size:12px; margin-bottom:24px; }
+  .step { background:#1a1816; border:1px solid #2a2825; border-radius:6px; padding:14px 18px; margin-bottom:10px; display:flex; gap:14px; align-items:flex-start; }
+  .step.ok    { border-left:4px solid #6dbe6d; }
+  .step.fail  { border-left:4px solid #c0584a; }
+  .step.warn  { border-left:4px solid #d2b15c; }
+  .step .ico  { font-size:20px; line-height:1; min-width:24px; }
+  .step .body { flex:1; }
+  .step .title { font-weight:500; margin-bottom:2px; }
+  .step .desc  { color:#888; font-size:12px; }
+  .step .cmd   { font-family:ui-monospace,monospace; font-size:11.5px; background:#0a0907; padding:6px 10px; border-radius:3px; margin-top:6px; color:#bba; display:inline-block; }
+  .step .btn   { background:#2a2825; color:#d2b15c; border:1px solid #3a3631; padding:5px 10px; border-radius:3px; font-size:11.5px; cursor:pointer; margin-top:6px; text-decoration:none; display:inline-block; }
+  .step .btn:hover { background:#3a3631; color:#fff; }
+  .reload { background:#2a2825; color:#d2b15c; border:1px solid #3a3631; padding:7px 14px; border-radius:3px; cursor:pointer; font-size:12px; margin-bottom:18px; }
+  .stale { color:#888; font-size:11px; }
+  pre.json { background:#0a0907; padding:12px; border-radius:4px; font-size:11px; color:#bba; max-height:300px; overflow:auto; }
+</style></head>
+<body>
+<h1>wallco.ai — Go-Live Checklist</h1>
+<div class="sub">Auto-refreshes every 30 s. Each step is idempotent.</div>
+<button class="reload" onclick="load(true)">↻ Refresh now</button>
+<div id="checklist">Loading…</div>
+<details style="margin-top:24px"><summary style="color:#888;cursor:pointer;font-size:12px">raw status JSON</summary><pre class="json" id="raw"></pre></details>
+<script>
+const STEPS = [
+  { key:'zone',     title:'1. Cloudflare zone exists',     desc:'CF API can\'t create zones with current token scope. One-time manual click.',
+    fix:'Open <a href="https://dash.cloudflare.com" target="_blank" style="color:#d2b15c" rel="noopener noreferrer nofollow">dash.cloudflare.com</a> → Add a site → wallco.ai → Free plan. Then refresh this page.' },
+  { key:'dns',      title:'2. DNS records (A, www, MX, SPF, DKIM, DMARC)', desc:'Six required records on the CF zone. Run after step 1 succeeds.',
+    cmd:'bash scripts/golive.sh dns' },
+  { key:'ns',       title:'3. GoDaddy NS swapped to Cloudflare', desc:'Domain still on ns41/42.domaincontrol.com — must switch to the CF nameservers.',
+    cmd:'bash scripts/golive.sh ns-swap' },
+  { key:'origin',   title:'4. Kamatera origin /health OK',  desc:'pm2 wallco-ai on :9905, nginx vhost.', cmd:null },
+  { key:'ssl',      title:'5. CF SSL mode = Full (Strict) + Always-HTTPS', desc:'Run after DNS propagates.',
+    cmd:'bash scripts/golive.sh cf-ssl' },
+  { key:'https',    title:'6. https://wallco.ai/health 200', desc:'Certbot must have issued LE cert.',
+    cmd:'bash scripts/golive.sh cert' },
+  { key:'ga4',      title:'7. GA4 gtag in HTML',            desc:'Property G-6V7HSH6K1M.' },
+  { key:'inlineEd', title:'8. Inline editor gate (401 anon)', desc:'Admin-cookie required for /api/_inline/save.' }
+];
+
+function rowClass(ok) { return ok ? 'ok' : 'fail'; }
+function ico(ok) { return ok ? '✅' : '⬜️'; }
+
+async function load(forceFresh) {
+  const r = await fetch('/admin/golive.json' + (forceFresh ? '?t=' + Date.now() : ''));
+  const j = await r.json();
+  const s = j.steps || {};
+  const cl = document.getElementById('checklist');
+  cl.innerHTML = STEPS.map(st => {
+    const v = s[st.key] || { ok: false };
+    const klass = rowClass(v.ok);
+    let detail = '';
+    if (st.key === 'zone' && !v.ok)        detail = '<div class="desc" style="margin-top:6px">' + st.fix + '</div>';
+    if (st.key === 'zone' && v.ok)         detail = '<div class="desc" style="margin-top:6px">zone_id: ' + (v.zone_id||'?') + ' · NS: ' + (v.name_servers||[]).join(', ') + '</div>';
+    if (st.key === 'dns' && !v.ok && v.reason !== 'no_zone') detail = '<div class="desc">a:' + (v.a||0) + ' www:' + (v.www||0) + ' mx:' + (v.mx||0) + ' spf:' + (v.spf||0) + ' dkim:' + (v.dkim||0) + ' dmarc:' + (v.dmarc||0) + '</div>';
+    if (st.key === 'ns')   detail = '<div class="desc">current: ' + ((v.current||[]).join(', ') || '?') + '</div>';
+    if (st.key === 'https' && v.http) detail = '<div class="desc">HTTP ' + v.http + (v.reason ? ' · ' + v.reason : '') + '</div>';
+    if (st.key === 'origin' && v.http)detail = '<div class="desc">HTTP ' + v.http + '</div>';
+    if (st.key === 'ga4' && v.measurement_id) detail = '<div class="desc">' + v.measurement_id + '</div>';
+    const cmdLine = st.cmd ? '<div class="cmd">' + st.cmd + '</div>' : '';
+    return '<div class="step ' + klass + '"><div class="ico">' + ico(v.ok) + '</div><div class="body"><div class="title">' + st.title + '</div><div class="desc">' + st.desc + '</div>' + detail + cmdLine + '</div></div>';
+  }).join('');
+  document.getElementById('raw').textContent = JSON.stringify(j, null, 2);
+}
+load();
+setInterval(() => load(true), 30000);
+</script></body></html>`);
+});
+
+// ── Admin gate (cookie-verified JWT + localhost) — moved to src/admin-gate.js
+// after security audit 2026-05-12 flagged the previous inline base64-decode-only
+// check as a CRITICAL bypass (anyone could mint a token with {role:"admin"}).
+const { isAdmin, requireAdmin } = require('./src/admin-gate');
+
+// ── Fliepaper Bugs — Gucci Resort Edition (6 patterns × 6 colorways = 36 cells)
+// Settlement-gated regen routes a job file into data/fliepaper-bugs/queue/ that
+// yolo-runner picks up. Public collection page + admin grid; placeholders show
+// while yolo-runner generates art.
+try { require('./src/fliepaper-bugs').mount(app, { isAdmin }); }
+catch (e) { console.error('Fliepaper-bugs mount failed:', e.message); }
+
+// ── /library — 57k professional vendor wallpapers from dw_unified
+// surfaced as a first-class catalog under the Designer Wallcoverings brand.
+// Vendor names are NEVER rendered. See src/library.js for the redaction +
+// sort + render pipeline. Cache builds via scripts/build-library-cache.js.
+try { require('./src/library').mount(app, { htmlHead, htmlHeader, getDesigns: () => DESIGNS }); }
+catch (e) { console.error('Library mount failed:', e.message); }
+
+// ── /admin/reload-designs — re-read data/designs.json without a pm2 reload.
+// Localhost-admin per src/admin-gate. Used by drunk-animals tick + future
+// generators to surface freshly persisted designs in the live grid without
+// restarting the server.
+app.post('/admin/reload-designs', (req, res) => {
+  if (!isAdmin(req)) return res.status(404).type('html').send('<h1>404</h1>');
+  const before = DESIGNS.length;
+  loadDesigns();
+  // Bump the by-color cache version + clear in-memory entries so stale palette/ΔE results
+  // can't outlive the new catalog state. ETags rebuild against the new CATALOG_LAST_MODIFIED.
+  _byColorCacheVer++;
+  _byColorCache.clear();
+  res.json({ ok: true, before, after: DESIGNS.length, catalog_last_modified: CATALOG_LAST_MODIFIED, by_color_cache_ver: _byColorCacheVer });
+});
+
+// ── Inspirations gallery (admin-only) ─────────────────────────────────────
+// Side-by-side viewer: real archival seed (vendor visible) ON LEFT, the
+// AI-generated wallco design (NO vendor leak) ON RIGHT. Powered by the new
+// scripts/inspiration_generator.js pipeline. Vendor identity is ADMIN-ONLY —
+// surfaced here, NEVER on /design/:id.
+app.get('/admin/inspirations', (req, res) => {
+  if (!isAdmin(req)) return res.status(404).type('html').send('<h1>404</h1>');
+  res.sendFile(path.join(__dirname, 'public', 'admin', 'inspirations.html'));
+});
+
+app.get('/api/admin/inspirations/list', (req, res) => {
+  if (!isAdmin(req)) return res.status(404).json({ error: 'not found' });
+  const limit  = Math.min(Math.max(parseInt(req.query.limit  || '50', 10), 1), 200);
+  const offset = Math.max(parseInt(req.query.offset || '0',  10), 0);
+  try {
+    const sql = `
+      SELECT json_agg(t) FROM (
+        SELECT
+          d.id, d.inspired_by_sku, d.inspired_by_vendor,
+          d.dominant_hex, d.palette, d.is_published, d.image_url,
+          d.local_path, d.created_at, d.prompt, d.notes,
+          sp.title         AS seed_title,
+          sp.image_url     AS seed_image_url,
+          sp.handle        AS seed_handle,
+          sp.product_type  AS seed_product_type,
+          sce.dominant_hex AS seed_dominant_hex,
+          sce.hex_codes    AS seed_hex_codes,
+          sce.design_era   AS seed_era
+        FROM all_designs d
+        LEFT JOIN shopify_products sp
+          ON sp.sku     = d.inspired_by_sku
+          OR sp.mfr_sku = d.inspired_by_sku
+          OR sp.handle  = d.inspired_by_sku
+        LEFT JOIN shopify_color_enrichment sce
+          ON sce.shopify_id = sp.shopify_id
+        WHERE d.inspired_by_sku IS NOT NULL
+        ORDER BY d.id DESC
+        LIMIT ${limit} OFFSET ${offset}
+      ) t;
+    `;
+    const raw = psqlQuery(sql);
+    const rows = raw ? JSON.parse(raw) : [];
+    res.json({ ok: true, count: (rows || []).length, rows: rows || [] });
+  } catch (e) {
+    console.error('inspirations list failed:', e.message);
+    res.status(500).json({ error: 'query failed', detail: e.message.slice(0, 200) });
+  }
+});
+
+// Per-design inspired-by lookup — minimal, NO vendor name in response so the
+// public-facing /design/:id template can use it safely. Vendor identity stays
+// behind the admin gate on /api/admin/inspirations/list.
+app.get('/api/design/:id/inspired-by-badge', (req, res) => {
+  const id = parseInt(req.params.id, 10);
+  if (!Number.isFinite(id)) return res.status(400).json({ error: 'bad id' });
+  try {
+    const sql = `SELECT (inspired_by_sku IS NOT NULL) AS inspired FROM all_designs WHERE id=${id};`;
+    const v = psqlQuery(sql);
+    const inspired = (v === 't' || v === 'true' || v === 'TRUE');
+    res.set('Cache-Control', 'public, max-age=600');
+    res.json({ inspired });
+  } catch (e) {
+    res.json({ inspired: false });
+  }
+});
+
+// Trigger another generation from the same seed (admin-only). Spawns the
+// generator as a detached child so the HTTP response returns fast.
+app.post('/api/admin/inspirations/regenerate', express.json(), (req, res) => {
+  if (!isAdmin(req)) return res.status(404).json({ error: 'not found' });
+  const sku = String(req.body?.sku || '').trim();
+  if (!sku) return res.status(400).json({ error: 'sku required' });
+  try {
+    const child = require('child_process').spawn(
+      'node',
+      [path.join(__dirname, 'scripts', 'inspiration_generator.js'), '--n', '1'],
+      { detached: true, stdio: 'ignore',
+        env: { ...process.env, INSPIRATION_BUDGET_N: '1', INSPIRATION_REGEN_SKU: sku } }
+    );
+    child.unref();
+    res.json({ ok: true, queued: true, sku });
+  } catch (e) {
+    res.status(500).json({ error: 'spawn failed', detail: e.message });
+  }
+});
+
+// ── Merge ────────────────────────────────────────────────────────────────
+// Take 1-3 designs (catalog IDs and/or uploaded files), pull element
+// descriptions from each, fuse into a single SDXL prompt, generate a new
+// design that visually pulls from all sources. Saved with category='merged'.
+const mergeUpload = require('multer')({
+  dest: path.join(__dirname, 'data', 'merge-uploads'),
+  limits: { fileSize: 20 * 1024 * 1024, files: 3 },
+});
+
+app.get('/merge', (_req, res) => {
+  res.type('html').send(`<!doctype html>
+<html lang="en"><head><meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Merge designs — wallco.ai</title>
+<style>
+:root { --ink:#2a1f10; --line:#d8d0c0; --gold:#c9a14b; --accent:#1a1a1a; --bg:#fff; --card:#f8f4ec; }
+body { margin:0; background:var(--bg); color:var(--ink); font:14px -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif }
+header { padding:20px 32px; border-bottom:1px solid var(--line); display:flex; gap:16px; align-items:baseline }
+header h1 { font-family:'Playfair Display','Didot',serif; font-weight:400; font-size:26px; margin:0 }
+header .meta { color:#888; font-size:13px }
+header a { margin-left:auto; color:var(--ink); text-decoration:none; font-size:13px; padding:6px 14px; border:1px solid var(--line); border-radius:14px }
+main { max-width:1080px; margin:0 auto; padding:32px 24px }
+.slots { display:grid; grid-template-columns:repeat(3,1fr); gap:18px; margin-bottom:22px }
+@media (max-width:760px) { .slots { grid-template-columns:1fr } }
+.slot { background:var(--card); border:1px solid var(--line); border-radius:8px; padding:14px; min-height:240px; display:flex; flex-direction:column; gap:8px }
+.slot h3 { margin:0; font-size:12px; letter-spacing:.16em; text-transform:uppercase; color:#7a6e5a }
+.slot .preview { flex:1; min-height:160px; background-size:cover; background-position:center; background-color:#fff; border:1px dashed var(--line); border-radius:6px; display:flex; align-items:center; justify-content:center; color:#bba; font-size:12px }
+.slot input[type=text], .slot input[type=file] { width:100%; padding:6px 8px; border:1px solid var(--line); border-radius:4px; font:12px var(--sans); background:#fff }
+.controls { display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin-bottom:14px }
+.controls textarea { flex:1; min-width:280px; padding:8px 10px; border:1px solid var(--line); border-radius:4px; font:13px monospace; min-height:54px }
+button.go { padding:12px 28px; background:var(--accent); color:#f4ece0; border:0; border-radius:6px; font:13px var(--sans); letter-spacing:.06em; text-transform:uppercase; cursor:pointer }
+button.go[disabled] { opacity:.55; cursor:wait }
+.status { font-size:12px; color:#888 }
+.result { margin-top:24px; padding:16px; border:1px solid var(--line); border-radius:8px; background:var(--card); display:none }
+.result.show { display:block }
+.result img { max-width:100%; border:1px solid var(--line); border-radius:6px; display:block; margin:10px 0 }
+</style>
+</head><body>
+<header>
+  <h1>Merge designs</h1>
+  <span class="meta">Pull elements from 1-3 designs into one new generation</span>
+  <a href="/designs">← Grid</a>
+</header>
+<main>
+  <p style="color:#666;line-height:1.6">Drop in up to 3 source images (uploads) or paste catalog design IDs. The merger pulls each design's motifs / color story / style and fuses them into a single new SDXL generation. Saved to the catalog as <code>category='merged'</code>.</p>
+
+  <div class="slots">
+    ${[1,2,3].map(n => `
+      <div class="slot" data-slot="${n}">
+        <h3>Source ${n}</h3>
+        <div class="preview" id="prev-${n}">drop image or paste design ID</div>
+        <label style="font-size:11px;color:#888">Catalog ID
+          <input type="text" id="src-id-${n}" placeholder="e.g. 1537" inputmode="numeric">
+        </label>
+        <label style="font-size:11px;color:#888">— or upload —
+          <input type="file" id="src-file-${n}" accept="image/png,image/jpeg,image/webp">
+        </label>
+      </div>
+    `).join('')}
+  </div>
+
+  <div class="controls">
+    <textarea id="extra-hint" placeholder="Optional extra direction — e.g. 'in a Persian miniature manner, dusty palette, mural-scale'"></textarea>
+    <button class="go" id="btn-merge" type="button">Merge & Generate</button>
+    <span class="status" id="status"></span>
+  </div>
+
+  <div class="result" id="result">
+    <strong>Merged design</strong>
+    <p id="result-meta" style="margin:6px 0;font-size:12px;color:#888"></p>
+    <img id="result-img" alt="merged">
+    <p><a id="result-link" href="#" style="color:var(--accent);font-weight:600">Open on the design page →</a></p>
+  </div>
+</main>
+<script>
+// Preview helpers — show image when ID is entered or file is picked
+[1,2,3].forEach(function(n){
+  var idIn = document.getElementById('src-id-'+n);
+  var fileIn = document.getElementById('src-file-'+n);
+  var prev = document.getElementById('prev-'+n);
+  idIn.addEventListener('input', function(){
+    var v = idIn.value.replace(/\\D/g,'');
+    if (v && parseInt(v,10) > 0) {
+      prev.style.backgroundImage = "url('/designs/img/by-id/" + v + "')";
+      // Fallback: just show the URL — server will respond if it exists
+      var img = new Image();
+      img.onload = function(){ prev.style.backgroundImage = "url('"+img.src+"')"; prev.textContent=''; };
+      img.onerror = function(){ prev.style.backgroundImage = ''; prev.textContent = '#'+v+' not found'; };
+      img.src = '/designs/img/by-id/' + v;
+      fileIn.value = '';
+    } else { prev.style.backgroundImage = ''; prev.textContent = 'drop image or paste design ID'; }
+  });
+  fileIn.addEventListener('change', function(){
+    var f = fileIn.files && fileIn.files[0]; if (!f) return;
+    var reader = new FileReader();
+    reader.onload = function(){ prev.style.backgroundImage = "url('"+reader.result+"')"; prev.textContent=''; };
+    reader.readAsDataURL(f);
+    idIn.value = '';
+  });
+});
+
+document.getElementById('btn-merge').addEventListener('click', function(){
+  var btn = this, status = document.getElementById('status');
+  var fd = new FormData();
+  var hint = document.getElementById('extra-hint').value.trim();
+  if (hint) fd.append('hint', hint);
+  var n = 0;
+  [1,2,3].forEach(function(i){
+    var id = (document.getElementById('src-id-'+i).value||'').replace(/\\D/g,'');
+    var f = document.getElementById('src-file-'+i).files[0];
+    if (id) { fd.append('source_ids', id); n++; }
+    else if (f) { fd.append('uploads', f); n++; }
+  });
+  if (n === 0) { status.textContent = 'Add at least one source'; return; }
+  status.textContent = 'Merging ~30-60s…'; btn.disabled = true;
+  fetch('/api/merge', { method:'POST', body: fd })
+    .then(function(r){ return r.json(); })
+    .then(function(j){
+      btn.disabled = false;
+      if (!j.ok) { status.textContent = 'Error: ' + (j.error||'unknown'); return; }
+      status.textContent = 'Merged → design #' + j.design_id;
+      var res = document.getElementById('result');
+      document.getElementById('result-meta').textContent =
+        'Sources: ' + (j.source_count||n) + ' · seed ' + j.seed + ' · ' + j.prompt.slice(0,80) + '…';
+      document.getElementById('result-img').src = j.image_url + '?t=' + Date.now();
+      document.getElementById('result-link').href = '/design/' + j.design_id;
+      res.classList.add('show');
+      res.scrollIntoView({ behavior:'smooth' });
+    })
+    .catch(function(e){ btn.disabled = false; status.textContent = 'Error: ' + e.message; });
+});
+</script>
+</body></html>`);
+});
+
+// Helper: serve a design image by id (used by /merge previews + new cards).
+// Prefer the in-memory DESIGNS snapshot (sourced from designs.json which IS
+// synced across Mac2 → Kamatera). Fall back to PG only if not in cache.
+// Important — local_path is a Mac2-absolute path (`/Users/stevestudio2/…`)
+// and won't resolve on Kamatera. Strip to basename and resolve against the
+// local IMG_DIR so the route works on both Mac2 and prod.
+app.get('/designs/img/by-id/:id', (req, res) => {
+  const id = parseInt(req.params.id, 10);
+  if (!Number.isFinite(id) || id < 1) return res.status(404).end();
+  let lp = null;
+  // 1) try DESIGNS array (matches designs.json, platform-agnostic)
+  const d = DESIGNS.find(x => x.id === id);
+  if (d && d.local_path) lp = d.local_path;
+  // 2) fallback to PG (only useful on the box where the row was inserted)
+  if (!lp) {
+    try {
+      lp = psqlExecLocal(`SELECT local_path FROM spoon_all_designs WHERE id=${id}`);
+    } catch {}
+  }
+  if (!lp) return res.status(404).end();
+  // local_path may be an absolute Mac2 path that doesn't exist on prod —
+  // resolve by basename against the local IMG_DIR.
+  const basename = String(lp).split('/').pop();
+  const candidate1 = path.resolve(lp);
+  const candidate2 = path.join(IMG_DIR, basename);
+  const finalPath = fs.existsSync(candidate1) ? candidate1
+                  : fs.existsSync(candidate2) ? candidate2
+                  : null;
+  if (!finalPath) return res.status(404).end();
+  res.sendFile(finalPath);
+});
+
+app.post('/api/merge', mergeUpload.array('uploads', 3), async (req, res) => {
+  try {
+    const sourceIds = []
+      .concat(req.body.source_ids || [])
+      .map(s => parseInt(s, 10))
+      .filter(n => Number.isFinite(n) && n > 0);
+    const uploads = (req.files || []);
+    const totalSources = sourceIds.length + uploads.length;
+    if (totalSources === 0) return res.status(400).json({ ok: false, error: 'no sources' });
+    if (totalSources > 3) return res.status(400).json({ ok: false, error: 'max 3 sources' });
+    const hint = String(req.body.hint || '').slice(0, 400);
+
+    // Pull a short element-description from each source.
+    // - Catalog IDs: read prompt + dominant_hex + motifs straight from PG (free).
+    // - Uploads: skip llava for now; just note "user-uploaded reference N" and
+    //   include the file in the prompt as an inspiration (textual cue only —
+    //   SDXL won't ingest the image without img2img; this is a future bump).
+    const fragments = [];
+    const palettes = [];
+    for (const sid of sourceIds) {
+      const row = psqlExecLocal(
+        `SELECT json_build_object('prompt', prompt, 'dominant_hex', dominant_hex, 'motifs', motifs, 'category', category)::text
+         FROM spoon_all_designs WHERE id=${sid}`
+      );
+      if (!row) continue;
+      let r; try { r = JSON.parse(row); } catch { continue; }
+      // Truncate the original prompt aggressively — keep first ~120 chars
+      const seed = (r.prompt || '').slice(0, 160).replace(/,?\s*NO\s.*$/i, '').trim();
+      fragments.push(`[source #${sid}: ${seed}${(r.motifs || []).length ? ' · motifs: ' + r.motifs.slice(0,4).join(', ') : ''}]`);
+      if (r.dominant_hex) palettes.push(r.dominant_hex);
+    }
+    for (let i = 0; i < uploads.length; i++) {
+      fragments.push(`[user reference #${i+1}: blend its overall feel and palette]`);
+    }
+    const mergedPrompt = [
+      `A new wallcovering design that visually fuses elements from ${totalSources} reference${totalSources>1?'s':''}:`,
+      fragments.join(' '),
+      palettes.length ? `Tie the palette around these dominants: ${palettes.join(', ')}.` : '',
+      hint ? `Direction: ${hint}.` : '',
+      'Refined hand-painted execution, seamless wallpaper repeat, archival quality, no text, no watermark.',
+    ].filter(Boolean).join(' ');
+
+    // Fire generate_designs.js with the merged prompt. category='merged'.
+    const { spawnSync } = require('child_process');
+    const r = spawnSync('node', [
+      path.join(__dirname, 'scripts', 'generate_designs.js'),
+      '--n', '1',
+      '--kind', 'seamless_tile',
+      '--category', 'merged',
+      '--prompts', mergedPrompt,
+    ], { encoding: 'utf8', cwd: __dirname, timeout: 180_000 });
+    if (r.status !== 0) {
+      return res.status(500).json({ ok: false, error: 'generator failed: ' + (r.stderr || r.stdout || '').slice(-200) });
+    }
+    // Pull the newest 'merged' row — that's the one we just made.
+    const idRaw = psqlExecLocal(`SELECT id FROM spoon_all_designs WHERE category='merged' ORDER BY id DESC LIMIT 1`);
+    const newId = parseInt(idRaw, 10);
+    const seedRaw = psqlExecLocal(`SELECT json_build_object('seed', seed, 'local_path', local_path)::text FROM spoon_all_designs WHERE id=${newId}`);
+    let meta = {}; try { meta = JSON.parse(seedRaw); } catch {}
+    const filename = (meta.local_path || '').split('/').pop();
+    res.json({
+      ok: true, design_id: newId,
+      seed: meta.seed,
+      prompt: mergedPrompt,
+      image_url: '/designs/img/' + filename,
+      source_count: totalSources,
+    });
+  } catch (e) {
+    res.status(500).json({ ok: false, error: e.message });
+  }
+});
+
+// ── License + download flow ────────────────────────────────────────────
+// One-time $950 infinite-use license per mural-scenic panel. Modeled on
+// Hannah's archive licensing — pay once, perpetual unlimited use, the buyer
+// downloads the 150dpi upscaled file.
+//
+// Flow:
+//   POST /api/design/:id/license/checkout  body {email}
+//       → creates Stripe Checkout Session ($950 one-time), returns redirect URL
+//       → inserts pending row in design_licenses
+//   GET  /api/design/:id/license/return?session_id=...
+//       → Stripe redirects here on success, marks status=paid, granted_at=now
+//       → redirects to /design/:id?licensed=1
+//   POST /api/stripe/webhook
+//       → backstop for async confirmation (idempotent against return-url)
+//   GET  /api/design/:id/license/status?email=...
+//       → JSON { has_license, license_id }
+//   GET  /api/design/:id/download?email=...
+//       → Verifies paid license. Streams the upscaled file. If not yet
+//         upscaled, kicks Replicate Real-ESRGAN x4 and waits.
+const LICENSE_PRICE_CENTS = parseInt(process.env.WALLCO_LICENSE_PRICE_CENTS || '95000', 10);
+const STRIPE_SECRET_KEY = (process.env.STRIPE_SECRET_KEY || '').trim();
+const PUBLIC_BASE_URL = (process.env.PUBLIC_BASE_URL || 'https://wallco.ai').replace(/\/$/, '');
+
+async function stripeCall(pathname, params) {
+  if (!STRIPE_SECRET_KEY) throw new Error('STRIPE_SECRET_KEY not configured');
+  const body = new URLSearchParams();
+  function flatten(obj, prefix = '') {
+    for (const [k, v] of Object.entries(obj)) {
+      const key = prefix ? `${prefix}[${k}]` : k;
+      if (v === undefined || v === null) continue;
+      if (typeof v === 'object' && !Array.isArray(v)) flatten(v, key);
+      else if (Array.isArray(v)) v.forEach((vv, i) => flatten({ [i]: vv }, key));
+      else body.append(key, String(v));
+    }
+  }
+  flatten(params);
+  const r = await fetch('https://api.stripe.com' + pathname, {
+    method: 'POST',
+    headers: { 'Authorization': 'Bearer ' + STRIPE_SECRET_KEY, 'Content-Type': 'application/x-www-form-urlencoded' },
+    body: body.toString()
+  });
+  const j = await r.json();
+  if (!r.ok) throw new Error(`Stripe ${pathname}: ${j.error?.message || r.status}`);
+  return j;
+}
+
+app.post('/api/design/:id/license/checkout', express.json({ limit: '4kb' }), async (req, res) => {
+  const id = parseInt(req.params.id, 10);
+  if (!Number.isFinite(id) || id < 1) return res.status(400).json({ error: 'bad id' });
+  const email = String(req.body?.email || '').trim().toLowerCase();
+  if (!email || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
+    return res.status(400).json({ error: 'valid email required' });
+  }
+  if (!STRIPE_SECRET_KEY) return res.status(500).json({ error: 'payments not configured' });
+  // Look up design title for the line item description
+  const design = DESIGNS.find(d => d.id === id);
+  const title = design?.title || `Wallco Mural #${id}`;
+  try {
+    const session = await stripeCall('/v1/checkout/sessions', {
+      mode: 'payment',
+      customer_email: email,
+      payment_method_types: ['card'],
+      line_items: [{
+        quantity: 1,
+        price_data: {
+          currency: 'usd',
+          unit_amount: LICENSE_PRICE_CENTS,
+          product_data: {
+            name: `${title} · Infinite-use license`,
+            description: `One-time payment for unlimited use of the 20ft × 11ft panoramic mural at 150 dpi. Perpetual. Download access included.`
+          }
+        }
+      }],
+      success_url: `${PUBLIC_BASE_URL}/api/design/${id}/license/return?session_id={CHECKOUT_SESSION_ID}`,
+      cancel_url:  `${PUBLIC_BASE_URL}/design/${id}?canceled=1`,
+      metadata: { design_id: String(id), user_email: email, license_type: 'infinite' },
+    });
+    psqlExecLocal(`INSERT INTO design_licenses
+      (design_id, user_email, stripe_session_id, amount_cents, license_type, status)
+      VALUES (${id}, '${email.replace(/'/g, "''")}', '${session.id}', ${LICENSE_PRICE_CENTS}, 'infinite', 'pending')`);
+    res.json({ ok: true, checkout_url: session.url });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+app.get('/api/design/:id/license/return', async (req, res) => {
+  const id = parseInt(req.params.id, 10);
+  const sid = String(req.query.session_id || '');
+  if (!sid) return res.redirect(`/design/${id}?error=missing_session`);
+  try {
+    // Verify with Stripe that the session was paid
+    const r = await fetch('https://api.stripe.com/v1/checkout/sessions/' + encodeURIComponent(sid), {
+      headers: { 'Authorization': 'Bearer ' + STRIPE_SECRET_KEY }
+    });
+    const session = await r.json();
+    if (session.payment_status !== 'paid') {
+      return res.redirect(`/design/${id}?error=not_paid`);
+    }
+    psqlExecLocal(`UPDATE design_licenses
+       SET status='paid', granted_at=now(), stripe_payment_intent='${(session.payment_intent || '').replace(/'/g, "''")}'
+       WHERE stripe_session_id='${sid.replace(/'/g, "''")}'`);
+    const email = encodeURIComponent(session.customer_details?.email || session.customer_email || '');
+    res.redirect(`/design/${id}?licensed=1&email=${email}`);
+  } catch (e) {
+    res.redirect(`/design/${id}?error=${encodeURIComponent(e.message)}`);
+  }
+});
+
+app.get('/api/design/:id/license/status', (req, res) => {
+  const id = parseInt(req.params.id, 10);
+  const email = String(req.query.email || '').trim().toLowerCase();
+  if (!email) return res.json({ has_license: false });
+  try {
+    const row = psqlExecLocal(
+      `SELECT id FROM design_licenses WHERE design_id=${id} AND lower(user_email)='${email.replace(/'/g, "''")}' AND status='paid' LIMIT 1`
+    );
+    res.json({ has_license: !!row, license_id: row ? parseInt(row, 10) : null });
+  } catch (e) { res.json({ has_license: false }); }
+});
+
+app.get('/api/design/:id/download', async (req, res) => {
+  const id = parseInt(req.params.id, 10);
+  const email = String(req.query.email || '').trim().toLowerCase();
+  if (!email) return res.status(403).type('text/plain').send('email required');
+  try {
+    // Verify paid license
+    const row = psqlExecLocal(
+      `SELECT id, upscaled_path FROM design_licenses WHERE design_id=${id} AND lower(user_email)='${email.replace(/'/g, "''")}' AND status='paid' ORDER BY id DESC LIMIT 1`
+    );
+    if (!row) return res.status(403).type('text/plain').send('no active license — purchase first');
+    const [licId, upPath] = row.split('|');
+    if (upPath && fs.existsSync(upPath)) {
+      psqlExecLocal(`UPDATE design_licenses SET download_count=download_count+1, last_downloaded_at=now() WHERE id=${licId}`);
+      res.setHeader('Content-Disposition', `attachment; filename="wallco-${id}-150dpi.png"`);
+      return res.sendFile(path.resolve(upPath));
+    }
+    // No upscale yet — kick Real-ESRGAN, wait for result. We do it inline so
+    // the user's request hangs until ready (single download, simple flow).
+    // Replicate nightmareai/real-esrgan x4 cost ~$0.03 per upscale.
+    const srcRow = psqlExecLocal(`SELECT local_path FROM spoon_all_designs WHERE id=${id}`);
+    if (!srcRow || !fs.existsSync(srcRow)) {
+      return res.status(500).type('text/plain').send('source image missing');
+    }
+    res.setHeader('Cache-Control', 'no-store');
+    const upsDir = path.join(__dirname, 'data', 'upscaled');
+    fs.mkdirSync(upsDir, { recursive: true });
+    const outPath = path.join(upsDir, `${id}-150dpi.png`);
+    // For the first pass we run Real-ESRGAN via Replicate if creds available;
+    // otherwise we ship the SDXL native with a note. Real-ESRGAN integration
+    // is the next iteration.
+    const replicateToken = (process.env.REPLICATE_API_TOKEN || '').trim() ||
+      (fs.existsSync(path.join(require('os').homedir(),'Projects/animate-museum-posts/.env'))
+        ? (fs.readFileSync(path.join(require('os').homedir(),'Projects/animate-museum-posts/.env'),'utf8').match(/^REPLICATE_API_TOKEN=(\S+)/m)||[])[1] : '');
+    if (!replicateToken) {
+      // Fallback: ship the SDXL native, mark download_count
+      psqlExecLocal(`UPDATE design_licenses SET download_count=download_count+1, last_downloaded_at=now() WHERE id=${licId}`);
+      res.setHeader('Content-Disposition', `attachment; filename="wallco-${id}-native.png"`);
+      return res.sendFile(path.resolve(srcRow));
+    }
+    // Kick Real-ESRGAN x4
+    const buf = fs.readFileSync(srcRow);
+    const b64 = 'data:image/png;base64,' + buf.toString('base64');
+    const submit = await fetch('https://api.replicate.com/v1/predictions', {
+      method: 'POST',
+      headers: { 'Authorization': 'Bearer ' + replicateToken, 'Content-Type': 'application/json' },
+      body: JSON.stringify({
+        // nightmareai/real-esrgan model
+        version: 'f121d640bd286e1fdc67f9799164c1d5be36ff74576ee11c803ae5b665dd46aa',
+        input: { image: b64, scale: 4, face_enhance: false }
+      })
+    });
+    const pred = await submit.json();
+    if (!pred.id) return res.status(500).type('text/plain').send('upscale start failed: ' + JSON.stringify(pred));
+    let final;
+    const t0 = Date.now();
+    while (Date.now() - t0 < 5 * 60_000) {
+      await new Promise(r => setTimeout(r, 4000));
+      const p = await (await fetch('https://api.replicate.com/v1/predictions/' + pred.id, {
+        headers: { 'Authorization': 'Bearer ' + replicateToken }
+      })).json();
+      if (p.status === 'succeeded') { final = p; break; }
+      if (p.status === 'failed' || p.status === 'canceled') {
+        return res.status(500).type('text/plain').send('upscale failed: ' + JSON.stringify(p.error||p.logs||'').slice(0,200));
+      }
+    }
+    if (!final) return res.status(504).type('text/plain').send('upscale timeout');
+    const imgUrl = Array.isArray(final.output) ? final.output[0] : final.output;
+    const imgBuf = Buffer.from(await (await fetch(imgUrl)).arrayBuffer());
+    fs.writeFileSync(outPath, imgBuf);
+    psqlExecLocal(`UPDATE design_licenses
+       SET upscaled_path='${outPath.replace(/'/g, "''")}', upscaled_at=now(),
+           download_count=download_count+1, last_downloaded_at=now()
+       WHERE id=${licId}`);
+    res.setHeader('Content-Disposition', `attachment; filename="wallco-${id}-150dpi.png"`);
+    res.sendFile(path.resolve(outPath));
+  } catch (e) {
+    res.status(500).type('text/plain').send('download error: ' + e.message);
+  }
+});
+
+// ── User feedback endpoints — color/style/scale yes-or-no toggles, 1-5
+// star rating, and soft-remove. Persists to spoon_all_designs.user_*
+// columns (added 2026-05-13). Feedback fuels future prompt-tuning so the
+// LLM gets better at what we actually want.
+//
+// POST /api/design/:id/rate  body: { color_good?, style_good?, scale_good?, stars? }
+//   Localhost admin gate. Each field is optional — only what's sent gets
+//   updated. Boolean fields accept true/false/null (null = unset).
+function psqlExecLocal(sql) {
+  // Was: spawnSync('psql', ['dw_unified', ...]) — broken on prod where pm2
+  // runs as root and PG has no 'root' role. Switched to the same PSQL_CMD
+  // path psqlQuery() uses, which sudos to postgres on Linux.
+  return execSync(PSQL_CMD + ' -c ' + JSON.stringify(sql), { encoding: 'utf8' }).trim();
+}
+app.post('/api/design/:id/rate', express.json({ limit: '4kb' }), (req, res) => {
+  if (!isAdmin(req)) return res.status(404).json({ error: 'not found' });
+  const id = parseInt(req.params.id, 10);
+  if (!Number.isFinite(id) || id < 1) return res.status(400).json({ error: 'bad id' });
+  const b = req.body || {};
+  const sets = [];
+  function setBool(field, val) {
+    if (val === null) sets.push(`${field}=NULL`);
+    else if (val === true || val === false) sets.push(`${field}=${val}`);
+  }
+  setBool('user_color_good', b.color_good);
+  setBool('user_style_good', b.style_good);
+  setBool('user_scale_good', b.scale_good);
+  if (b.stars === null) sets.push('user_stars=NULL');
+  else if (Number.isInteger(b.stars) && b.stars >= 1 && b.stars <= 5) sets.push(`user_stars=${b.stars}`);
+  if (!sets.length) return res.status(400).json({ error: 'nothing to update' });
+  sets.push('user_rated_at=now()');
+  try {
+    psqlExecLocal(`UPDATE spoon_all_designs SET ${sets.join(', ')} WHERE id=${id} RETURNING id;`);
+    // Mirror back into the in-memory DESIGNS so the grid sort sees it
+    // immediately (no full snapshot refresh needed).
+    const d = DESIGNS.find(x => x.id === id);
+    if (d) {
+      if (b.color_good !== undefined) d.user_color_good = b.color_good;
+      if (b.style_good !== undefined) d.user_style_good = b.style_good;
+      if (b.scale_good !== undefined) d.user_scale_good = b.scale_good;
+      if (b.stars      !== undefined) d.user_stars      = b.stars;
+    }
+    res.json({ ok: true, id });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// POST /api/design/:id/remove  — soft-remove a design from public listings.
+// Sets user_removed=true. Reverse with ?undo=1.
+app.post('/api/design/:id/remove', (req, res) => {
+  if (!isAdmin(req)) return res.status(404).json({ error: 'not found' });
+  const id = parseInt(req.params.id, 10);
+  if (!Number.isFinite(id) || id < 1) return res.status(400).json({ error: 'bad id' });
+  const undo = req.query.undo === '1' || req.query.undo === 'true';
+  const val = undo ? 'FALSE' : 'TRUE';
+  try {
+    psqlExecLocal(`UPDATE spoon_all_designs SET user_removed=${val} WHERE id=${id};`);
+    // Remove from / re-add to the in-memory list. Easiest path: filter.
+    if (!undo) {
+      const idx = DESIGNS.findIndex(x => x.id === id);
+      if (idx >= 0) DESIGNS.splice(idx, 1);
+    }
+    res.json({ ok: true, id, removed: !undo });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ── TRADE LOGIN (separate from admin layer) — magic-link, no passwords.
+// Sister cookie to dw_auth; different name (`wallco_trade_session`), different
+// table (wallco_trade_users), so admin and trade tiers never collide.
+// Per Steve's standing rule: NO autonomous email sending. The magic link is
+// logged to console + echoed in the API response. Wire SES/sendgrid later.
+const jwt = require('jsonwebtoken');
+const WALLCO_JWT_SECRET = process.env.WALLCO_JWT_SECRET
+  || process.env.JWT_SECRET
+  || (function(){
+       console.warn('[trade-login] WALLCO_JWT_SECRET / JWT_SECRET unset — using dev fallback. Set one of these in .env for prod.');
+       return 'wallco-trade-dev-secret-CHANGE-IN-PROD';
+     })();
+const TRADE_COOKIE = 'wallco_trade_session';
+
+// Minimal cookie reader. cookie-parser isn't in deps and `req.cookies` is
+// undefined on non-admin paths. Pull what we need straight from the header.
+function readCookie(req, name) {
+  const raw = req.headers.cookie || '';
+  if (!raw) return null;
+  for (const part of raw.split(/;\s*/)) {
+    const eq = part.indexOf('=');
+    if (eq < 0) continue;
+    const k = part.slice(0, eq);
+    if (k === name) return decodeURIComponent(part.slice(eq + 1));
+  }
+  return null;
+}
+
+// Returns { id, email, name } or null. Never throws.
+function getTradeUser(req) {
+  const token = readCookie(req, TRADE_COOKIE);
+  if (!token) return null;
+  try {
+    const decoded = jwt.verify(token, WALLCO_JWT_SECRET, { algorithms: ['HS256'] });
+    if (!decoded || !decoded.id || !decoded.email) return null;
+    return { id: decoded.id, email: decoded.email, name: decoded.name || '' };
+  } catch {
+    return null;  // expired, bad signature, malformed
+  }
+}
+
+// Migrations — wallco_trade_users + wallco_saved_designs.
+// Idempotent CREATE IF NOT EXISTS, run at server start. Mirrors the
+// design_ratings / studio_renders init pattern below.
+function ensureTradeUserTables() {
+  try {
+    psqlQuery(`CREATE TABLE IF NOT EXISTS wallco_trade_users (
+      id          SERIAL PRIMARY KEY,
+      email       TEXT UNIQUE NOT NULL,
+      name        TEXT,
+      company     TEXT,
+      phone       TEXT,
+      magic_token TEXT,
+      token_exp   TIMESTAMPTZ,
+      last_login  TIMESTAMPTZ,
+      created_at  TIMESTAMPTZ DEFAULT NOW()
+    );`);
+    psqlQuery(`CREATE TABLE IF NOT EXISTS wallco_saved_designs (
+      user_id    INTEGER REFERENCES wallco_trade_users(id) ON DELETE CASCADE,
+      design_id  INTEGER NOT NULL,
+      note       TEXT,
+      saved_at   TIMESTAMPTZ DEFAULT NOW(),
+      PRIMARY KEY (user_id, design_id)
+    );`);
+    psqlQuery(`CREATE INDEX IF NOT EXISTS wallco_saved_designs_user_idx
+                 ON wallco_saved_designs(user_id, saved_at DESC);`);
+    psqlQuery(`CREATE INDEX IF NOT EXISTS wallco_trade_users_token_idx
+                 ON wallco_trade_users(magic_token) WHERE magic_token IS NOT NULL;`);
+    console.log('  Trade-user tables ready (wallco_trade_users + wallco_saved_designs)');
+  } catch (e) {
+    console.error('[trade-login] table init failed:', e.message);
+  }
+}
+ensureTradeUserTables();
+
+// ── Ensure user_removed column on spoon_all_designs ─────────────────
+// The /api/design/:id/remove endpoint writes user_removed=TRUE to this
+// column. On prod, the column was missing — UPDATE returned "column does
+// not exist" and the soft-remove silently failed. Add it idempotently.
+function ensureUserRemovedColumn() {
+  try {
+    psqlQuery(`ALTER TABLE spoon_all_designs ADD COLUMN IF NOT EXISTS user_removed BOOLEAN NOT NULL DEFAULT FALSE;`);
+    psqlQuery(`CREATE INDEX IF NOT EXISTS spoon_all_designs_user_removed_idx ON spoon_all_designs(user_removed) WHERE user_removed = TRUE;`);
+  } catch (e) {
+    console.error('[migrate] ensureUserRemovedColumn:', e.message);
+  }
+}
+ensureUserRemovedColumn();
+
+// Audit-log — every user + admin action lands here. Append-only. Steve's call:
+// trade users see only their own at /me/activity; admin sees everything at /admin/activity.
+function ensureAuditLogTable() {
+  try {
+    psqlQuery(`CREATE TABLE IF NOT EXISTS wallco_audit_log (
+      id           BIGSERIAL PRIMARY KEY,
+      user_id      INTEGER REFERENCES wallco_trade_users(id) ON DELETE SET NULL,
+      is_admin     BOOLEAN NOT NULL DEFAULT FALSE,
+      actor_label  TEXT,
+      action       TEXT NOT NULL,
+      payload      JSONB,
+      ip           INET,
+      user_agent   TEXT,
+      created_at   TIMESTAMPTZ DEFAULT NOW()
+    );`);
+    psqlQuery(`CREATE INDEX IF NOT EXISTS wallco_audit_user_idx
+                 ON wallco_audit_log(user_id, created_at DESC) WHERE user_id IS NOT NULL;`);
+    psqlQuery(`CREATE INDEX IF NOT EXISTS wallco_audit_admin_idx
+                 ON wallco_audit_log(is_admin, created_at DESC) WHERE is_admin = TRUE;`);
+    psqlQuery(`CREATE INDEX IF NOT EXISTS wallco_audit_action_idx
+                 ON wallco_audit_log(action, created_at DESC);`);
+    console.log('  Audit-log table ready (wallco_audit_log)');
+  } catch (e) {
+    console.error('[audit-log] table init failed:', e.message);
+  }
+}
+ensureAuditLogTable();
+
+// logEvent(req, action, payload) — fire-and-forget INSERT into wallco_audit_log.
+// Never throws (audit MUST NOT break the request path). Extracts actor from
+// cookies — admin > trade user > anonymous.
+function logEvent(req, action, payload) {
+  try {
+    const u = getTradeUser(req);
+    const admin = isAdmin(req);
+    const ip = (req.headers['x-forwarded-for'] || req.socket?.remoteAddress || '').toString().split(',')[0].trim() || null;
+    const ua = (req.headers['user-agent'] || '').toString().slice(0, 500);
+    const actorLabel = admin ? 'admin' : (u && u.email) ? u.email : 'anon';
+    const payloadJson = payload ? JSON.stringify(payload).slice(0, 8000) : null;
+    psqlQuery(`INSERT INTO wallco_audit_log (user_id, is_admin, actor_label, action, payload, ip, user_agent)
+               VALUES (
+                 ${u ? parseInt(u.id, 10) : 'NULL'},
+                 ${admin ? 'TRUE' : 'FALSE'},
+                 ${pgEsc(actorLabel)},
+                 ${pgEsc(action)},
+                 ${payloadJson ? pgEsc(payloadJson) + '::jsonb' : 'NULL'},
+                 ${ip ? pgEsc(ip) + '::inet' : 'NULL'},
+                 ${pgEsc(ua)}
+               );`);
+  } catch (e) {
+    // Audit failures are non-fatal; log once but never re-raise.
+    if (!global.__auditLogWarned) {
+      console.warn('[audit-log] insert failed:', e.message);
+      global.__auditLogWarned = true;
+    }
+  }
+}
+
+// HTML-escape helper for safe interpolation into trade-login templates.
+function _escTrade(s) {
+  return String(s == null ? '' : s)
+    .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
+    .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
+}
+
+// Small helper — set cookie `secure` flag only on HTTPS (so dev still works).
+function tradeProtoIsHttps(req) {
+  const proto = (req.headers['x-forwarded-proto'] || req.protocol || 'http').split(',')[0].trim();
+  return proto === 'https';
+}
+
+// GET /trade/login — minimal email-only form. `return` query param round-trips
+// the user back to /design/:id (or wherever they came from) after auth.
+app.get('/trade/login', (req, res) => {
+  const returnTo = String(req.query.return || '/').slice(0, 500);
+  const existing = getTradeUser(req);
+  if (existing) {
+    return res.redirect(returnTo || '/');
+  }
+  res.type('html').send(`${htmlHead({
+    title: 'Trade Login — wallco.ai',
+    description: 'Sign in to save designs, request samples, and build proposals.',
+    canonical: 'https://wallco.ai/trade/login'
+  })}
+<body>
+${htmlHeader('/trade/login')}
+<main class="prose-main" style="max-width:480px;margin:48px auto;padding:0 24px">
+  <article class="prose">
+    <h1 style="font:300 32px/1.2 'Cormorant Garamond',serif;margin:0 0 8px">Trade sign-in</h1>
+    <p style="color:var(--ink-soft,#555);font:14px/1.5 var(--sans,system-ui);margin:0 0 24px">
+      Enter your email — we'll send you a one-tap sign-in link. No passwords.
+      For designers, architects, and trade pros.
+    </p>
+    <form id="trade-login-form" autocomplete="on" novalidate>
+      <input type="hidden" name="return" value="${_escTrade(returnTo)}">
+      <label for="trade-email" style="display:block;font:11px var(--sans);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-faint,#888);margin-bottom:6px">Email</label>
+      <input type="email" id="trade-email" name="email" required autofocus
+             placeholder="you@studio.com"
+             style="width:100%;padding:14px 16px;font:16px var(--sans,system-ui);border:1px solid var(--line,#ddd);border-radius:6px;margin-bottom:10px;box-sizing:border-box">
+      <button type="submit" class="btn-primary" style="width:100%;padding:14px;font:14px var(--sans);cursor:pointer">
+        Send sign-in link
+      </button>
+    </form>
+    <div id="trade-login-msg" role="status" aria-live="polite" style="margin-top:16px;font:13px/1.5 var(--sans);color:var(--ink-soft,#555)"></div>
+    <p style="margin-top:32px;font:11.5px/1.5 var(--sans);color:var(--ink-faint,#888)">
+      By signing in, you agree to receive trade-only communications from wallco.ai
+      (part of Designer Wallcoverings). We never sell your data.
+    </p>
+  </article>
+</main>
+${FOOTER}
+<script>
+(function(){
+  var form = document.getElementById('trade-login-form');
+  var msg  = document.getElementById('trade-login-msg');
+  if (!form) return;
+  form.addEventListener('submit', function(e){
+    e.preventDefault();
+    var email = document.getElementById('trade-email').value.trim();
+    var returnTo = form.querySelector('input[name=return]').value;
+    if (!email) { msg.textContent = 'Email required.'; return; }
+    msg.textContent = 'Generating sign-in link...';
+    fetch('/api/trade/login', {
+      method:'POST', headers:{'Content-Type':'application/json'},
+      body: JSON.stringify({ email: email, return: returnTo })
+    })
+      .then(function(r){ return r.json().then(function(j){ return {ok:r.ok, j:j}; }); })
+      .then(function(out){
+        if (!out.ok || !out.j.magic_link) {
+          msg.textContent = (out.j && out.j.error) || 'Sign-in failed. Try again.';
+          msg.style.color = '#b03a2e';
+          return;
+        }
+        msg.style.color = '';
+        msg.innerHTML = '<div style="padding:14px;background:#faf8f3;border:1px solid #e6e2d8;border-radius:6px">'
+          + '<div style="font-weight:500;margin-bottom:6px">Sign-in link ready</div>'
+          + '<div style="font:12px/1.4 var(--sans);color:var(--ink-soft);margin-bottom:8px">'
+          + 'Email sending isn’t wired yet — copy this link to sign in:'
+          + '</div>'
+          + '<a href="' + out.j.magic_link + '" style="display:block;font:12px/1.5 ui-monospace,monospace;word-break:break-all;color:var(--accent,#d2b15c);text-decoration:underline;margin-bottom:8px">'
+          + out.j.magic_link + '</a>'
+          + '<a href="' + out.j.magic_link + '" class="btn-primary" style="display:inline-block;padding:8px 14px;font:13px var(--sans);text-decoration:none">Sign in now →</a>'
+          + '</div>';
+      })
+      .catch(function(err){ msg.style.color='#b03a2e'; msg.textContent = 'Network error: ' + err.message; });
+  });
+})();
+</script>
+${HAMBURGER_JS}
+</body>
+</html>`);
+});
+
+// POST /api/trade/login — generate token, store, return magic link.
+// NOTE: Steve standing rule — NO autonomous email sending. The link is
+// console-logged + returned in JSON. Steve will wire SES/sendgrid later.
+app.post('/api/trade/login', (req, res) => {
+  const email = String((req.body && req.body.email) || '').trim().toLowerCase();
+  const returnTo = String((req.body && req.body.return) || '/').slice(0, 500);
+  if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
+    return res.status(400).json({ ok: false, error: 'Valid email required.' });
+  }
+  // 32-byte token, 30-minute expiry.
+  const token = crypto.randomBytes(32).toString('hex');
+  const expSql = `NOW() + INTERVAL '30 minutes'`;
+  try {
+    // Upsert user; bind a fresh token.
+    psqlQuery(`INSERT INTO wallco_trade_users (email, magic_token, token_exp)
+               VALUES (${pgEsc(email)}, ${pgEsc(token)}, ${expSql})
+               ON CONFLICT (email) DO UPDATE
+                 SET magic_token = EXCLUDED.magic_token,
+                     token_exp   = EXCLUDED.token_exp;`);
+  } catch (e) {
+    console.error('[trade-login] upsert failed:', e.message);
+    return res.status(500).json({ ok: false, error: 'Could not generate sign-in link.' });
+  }
+  // Build link from request host so dev (localhost:9792) + prod (wallco.ai) both work.
+  const proto = (req.headers['x-forwarded-proto'] || req.protocol || 'http').split(',')[0].trim();
+  const host = req.headers['x-forwarded-host'] || req.get('host');
+  const magicLink = `${proto}://${host}/trade/auth?token=${token}&return=${encodeURIComponent(returnTo)}`;
+  console.log(`[trade-login] magic link: ${magicLink} (for ${email})`);
+  logEvent(req, 'trade_login_request', { email });
+  res.json({
+    ok: true,
+    magic_link: magicLink,
+    expires_in: '30 minutes',
+    note: 'Email sending not yet configured — copy the link above.'
+  });
+});
+
+// GET /trade/auth?token=... — validate token, mint JWT, set cookie, redirect.
+app.get('/trade/auth', (req, res) => {
+  const token = String(req.query.token || '').trim();
+  const returnTo = String(req.query.return || '/').slice(0, 500);
+  if (!token || !/^[a-f0-9]{20,128}$/i.test(token)) {
+    return res.status(400).type('html').send('<h1>Invalid sign-in link</h1><p>The token is malformed. <a href="/trade/login">Try again →</a></p>');
+  }
+  let row;
+  try {
+    const raw = psqlQuery(`SELECT row_to_json(t) FROM (
+      SELECT id, email, name, token_exp, (token_exp > NOW()) AS still_valid
+      FROM wallco_trade_users WHERE magic_token = ${pgEsc(token)} LIMIT 1
+    ) t;`);
+    row = raw ? JSON.parse(raw) : null;
+  } catch (e) {
+    console.error('[trade-login] auth lookup failed:', e.message);
+    return res.status(500).type('html').send('<h1>Sign-in failed</h1><p>Server error.</p>');
+  }
+  if (!row) {
+    return res.status(404).type('html').send('<h1>Sign-in link not found</h1><p>It may have already been used. <a href="/trade/login">Get a new one →</a></p>');
+  }
+  if (!row.still_valid) {
+    return res.status(410).type('html').send('<h1>Sign-in link expired</h1><p>Links are valid for 30 minutes. <a href="/trade/login">Get a new one →</a></p>');
+  }
+  // Burn the token, stamp last_login.
+  try {
+    psqlQuery(`UPDATE wallco_trade_users
+                 SET magic_token = NULL, token_exp = NULL, last_login = NOW()
+                 WHERE id = ${parseInt(row.id, 10)};`);
+  } catch (e) {
+    console.error('[trade-login] burn-token update failed:', e.message);
+  }
+  // Mint a 14-day JWT.
+  const jwtToken = jwt.sign(
+    { id: row.id, email: row.email, name: row.name || '' },
+    WALLCO_JWT_SECRET,
+    { algorithm: 'HS256', expiresIn: '14d' }
+  );
+  res.cookie(TRADE_COOKIE, jwtToken, {
+    httpOnly: true,
+    sameSite: 'lax',
+    secure: tradeProtoIsHttps(req),
+    maxAge: 14 * 24 * 3600 * 1000,
+    path: '/'
+  });
+  // Admin promotion via email allowlist: if the verified email matches
+  // WALLCO_ADMIN_EMAILS (comma-separated), ALSO mint a `dw_auth` cookie so
+  // /designs renders the full admin chrome + the view-mode toggle.
+  // Defaults to Steve's two known emails when env unset.
+  try {
+    const ADMIN_EMAIL_LIST = (process.env.WALLCO_ADMIN_EMAILS
+      || 'steve@designerwallcoverings.com,info@designerwallcoverings.com')
+      .toLowerCase().split(',').map(s => s.trim()).filter(Boolean);
+    const userEmailNorm = String(row.email || '').toLowerCase().trim();
+    if (ADMIN_EMAIL_LIST.includes(userEmailNorm) && process.env.AUTH_DEV_SECRET) {
+      const adminJwt = jwt.sign(
+        { role: 'admin', email: row.email, name: row.name || '', source: 'trade-login-allowlist' },
+        process.env.AUTH_DEV_SECRET,
+        { algorithm: 'HS256', expiresIn: '14d' }
+      );
+      res.cookie('dw_auth', adminJwt, {
+        httpOnly: true,
+        sameSite: 'lax',
+        secure: tradeProtoIsHttps(req),
+        maxAge: 14 * 24 * 3600 * 1000,
+        path: '/'
+      });
+      console.log(`[trade-login] admin promotion: ${row.email} → dw_auth minted`);
+    }
+  } catch (e) {
+    console.error('[trade-login] admin-promo failed (non-fatal):', e.message);
+  }
+  // Whitelist returnTo to relative paths (prevent open-redirect).
+  const safeReturn = (returnTo && returnTo.startsWith('/') && !returnTo.startsWith('//')) ? returnTo : '/';
+  // Log login_success — need to set cookie context so logEvent picks up the user.
+  // The cookie is already in res but not yet on the request; pass actor explicitly.
+  try {
+    psqlQuery(`INSERT INTO wallco_audit_log (user_id, is_admin, actor_label, action, payload, ip, user_agent)
+               VALUES (${parseInt(row.id, 10)}, FALSE, ${pgEsc(row.email)}, ${pgEsc('trade_login_success')},
+                       ${pgEsc(JSON.stringify({ return_to: safeReturn }))}::jsonb,
+                       ${pgEsc(((req.headers['x-forwarded-for'] || req.socket?.remoteAddress || '').toString().split(',')[0].trim()) || '0.0.0.0')}::inet,
+                       ${pgEsc((req.headers['user-agent'] || '').toString().slice(0, 500))});`);
+  } catch(_) {}
+  res.redirect(safeReturn);
+});
+
+// GET /trade/logout — clear cookie, send home.
+app.get('/trade/logout', (req, res) => {
+  logEvent(req, 'trade_logout', null);
+  res.clearCookie(TRADE_COOKIE, { path: '/' });
+  res.redirect('/');
+});
+
+// GET /api/trade/me — current user + saved-count, for header rendering.
+app.get('/api/trade/me', (req, res) => {
+  const u = getTradeUser(req);
+  if (!u) return res.json({ authenticated: false });
+  let savedCount = 0;
+  try {
+    const raw = psqlQuery(`SELECT count(*) FROM wallco_saved_designs WHERE user_id=${parseInt(u.id, 10)};`);
+    savedCount = parseInt(raw, 10) || 0;
+  } catch (e) { /* tolerate; header just shows 0 */ }
+  res.json({
+    authenticated: true,
+    user: { id: u.id, email: u.email, name: u.name || '' },
+    saved_count: savedCount
+  });
+});
+
+// GET /api/me/saved — bulk-friendly: returns all saved design IDs for the current user.
+// Used by autocomplete + grid renderers to mark ♥ on already-saved items without N round-trips.
+// Anonymous users get { authenticated:false, ids:[] }.
+app.get('/api/me/saved', (req, res) => {
+  const u = getTradeUser(req);
+  // No-store — saved state changes mid-session; cache poisoning here is a real risk.
+  res.setHeader('Cache-Control', 'no-store');
+  if (!u) return res.json({ authenticated: false, ids: [] });
+  try {
+    const raw = psqlQuery(`SELECT COALESCE(json_agg(design_id ORDER BY saved_at DESC), '[]'::json)
+                             FROM wallco_saved_designs WHERE user_id=${parseInt(u.id, 10)};`);
+    const ids = JSON.parse(raw || '[]');
+    res.json({ authenticated: true, count: ids.length, ids });
+  } catch (e) {
+    console.error('[api/me/saved] failed:', e.message);
+    res.status(500).json({ ok: false, error: 'Lookup failed.' });
+  }
+});
+
+// GET /api/saved/state?design_id=... — idempotent probe for the PDP heart icon.
+// Returns { authenticated, saved }. Anonymous users get { authenticated:false, saved:false }.
+app.get('/api/saved/state', (req, res) => {
+  const u = getTradeUser(req);
+  if (!u) return res.json({ authenticated: false, saved: false });
+  const designId = parseInt(req.query.design_id, 10);
+  if (!Number.isFinite(designId) || designId <= 0) {
+    return res.status(400).json({ ok: false, error: 'design_id required.' });
+  }
+  try {
+    const hit = psqlQuery(`SELECT 1 FROM wallco_saved_designs
+                             WHERE user_id=${parseInt(u.id, 10)} AND design_id=${designId} LIMIT 1;`);
+    res.json({ authenticated: true, saved: !!hit });
+  } catch (e) {
+    console.error('[saved/state] failed:', e.message);
+    res.status(500).json({ ok: false, error: 'Lookup failed.' });
+  }
+});
+
+// POST /api/saved  { design_id } — toggle save state for current trade user.
+// Returns { ok, saved: bool, saved_count }. 401 if not logged in.
+app.post('/api/saved', (req, res) => {
+  const u = getTradeUser(req);
+  if (!u) return res.status(401).json({ ok: false, error: 'Sign in to save designs.' });
+  const designId = parseInt((req.body && req.body.design_id), 10);
+  if (!Number.isFinite(designId) || designId <= 0) {
+    return res.status(400).json({ ok: false, error: 'design_id required.' });
+  }
+  try {
+    // Check existing → toggle.
+    const exists = psqlQuery(`SELECT 1 FROM wallco_saved_designs
+                                WHERE user_id=${parseInt(u.id, 10)} AND design_id=${designId} LIMIT 1;`);
+    let nowSaved;
+    if (exists) {
+      psqlQuery(`DELETE FROM wallco_saved_designs
+                   WHERE user_id=${parseInt(u.id, 10)} AND design_id=${designId};`);
+      nowSaved = false;
+    } else {
+      psqlQuery(`INSERT INTO wallco_saved_designs (user_id, design_id)
+                   VALUES (${parseInt(u.id, 10)}, ${designId})
+                   ON CONFLICT DO NOTHING;`);
+      nowSaved = true;
+    }
+    const cnt = parseInt(psqlQuery(`SELECT count(*) FROM wallco_saved_designs WHERE user_id=${parseInt(u.id, 10)};`), 10) || 0;
+    logEvent(req, nowSaved ? 'design_saved' : 'design_unsaved', { design_id: designId, saved_count: cnt });
+    res.json({ ok: true, saved: nowSaved, saved_count: cnt });
+  } catch (e) {
+    console.error('[saved] toggle failed:', e.message);
+    res.status(500).json({ ok: false, error: 'Save failed.' });
+  }
+});
+
+// GET /me/saved — auth-gated page listing all the user's saved designs.
+// Uses the same .design-grid CSS class as /designs so it inherits the same
+// layout + density-slider system.
+app.get('/me/saved', (req, res) => {
+  const u = getTradeUser(req);
+  if (!u) {
+    return res.redirect('/trade/login?return=' + encodeURIComponent('/me/saved'));
+  }
+  // Pull save rows joined to live DESIGNS array (DESIGNS is the in-memory
+  // snapshot we already render everywhere). Designs that no longer exist in
+  // the catalog are silently filtered out.
+  let savedIds = [];
+  // Carry saved_at + note through to the card render so users can see
+  // "Saved 2h ago" without opening DevTools — the rail audit gap that
+  // previously dropped this metadata immediately after the SELECT.
+  let savedMetaById = new Map();
+  try {
+    const raw = psqlQuery(`SELECT COALESCE(json_agg(t ORDER BY t.saved_at DESC), '[]'::json) FROM (
+      SELECT design_id, note, saved_at FROM wallco_saved_designs
+      WHERE user_id=${parseInt(u.id, 10)}) t;`);
+    const rows = JSON.parse(raw || '[]');
+    savedIds = rows.map(r => r.design_id);
+    savedMetaById = new Map(rows.map(r => [r.design_id, r]));
+  } catch (e) {
+    console.error('[me/saved] query failed:', e.message);
+  }
+  const byId = new Map(DESIGNS.map(d => [d.id, d]));
+  const designs = savedIds.map(id => byId.get(id)).filter(Boolean);
+  // Relative-time formatter (server-rendered so the badge ships in HTML — no JS
+  // flash). Uses 'just now' under 60s, '12m', '4h', '3d', else date.
+  const _meSavedRelTime = (iso) => {
+    if (!iso) return '';
+    const t = Date.parse(iso);
+    if (!t) return '';
+    const dMs = Date.now() - t;
+    if (dMs < 60_000) return 'just now';
+    const m = Math.floor(dMs / 60_000);
+    if (m < 60) return m + 'm ago';
+    const h = Math.floor(m / 60);
+    if (h < 24) return h + 'h ago';
+    const days = Math.floor(h / 24);
+    if (days < 30) return days + 'd ago';
+    return new Date(t).toISOString().slice(0, 10);
+  };
+
+  const cards = designs.map(d => {
+    const roomKey = (d.room_mockups || []).includes('living_room') ? 'living_room'
+                  : (d.room_mockups || []).includes('bedroom')     ? 'bedroom'
+                  : (d.room_mockups || []).includes('office')      ? 'office'
+                  : (d.room_mockups || [])[0];
+    const roomOverlay = roomKey
+      ? `<img class="card-room" src="/designs/room/design_${d.id}_${roomKey}.png" loading="lazy" alt="" aria-hidden="true">`
+      : '';
+    const meta = savedMetaById.get(d.id) || {};
+    const rel = _meSavedRelTime(meta.saved_at);
+    const absISO = meta.saved_at ? new Date(meta.saved_at).toISOString() : '';
+    const savedBadge = rel
+      ? `<span class="saved-when" title="Saved ${_escTrade(absISO)}" style="position:absolute;top:8px;left:8px;background:rgba(15,14,12,.78);color:#e8e2d6;border-radius:4px;padding:3px 7px;font:11px var(--sans,system-ui);letter-spacing:.02em;pointer-events:none">Saved ${_escTrade(rel)}</span>`
+      : '';
+    return `
+    <div class="saved-card-wrap" data-design-id="${d.id}" style="position:relative">
+      <a href="/design/${d.id}" class="design-card${roomKey ? ' has-room' : ''}" aria-label="${_escTrade(d.title || ('Design #' + d.id))}">
+        <div class="card-img" style="background-image:url('${_escTrade(d.image_url)}')" role="img" aria-label="${_escTrade(d.title)} pattern thumbnail">${roomOverlay}</div>
+        <div class="card-meta">
+          <span class="card-title">${_escTrade(d.title)}</span>
+          <span class="card-cat">${_escTrade(d.category)}</span>
+          <span class="color-dot" style="background:${_escTrade(d.dominant_hex || '#ccc')}" aria-hidden="true"></span>
+        </div>
+      </a>
+      ${savedBadge}
+      <button type="button" class="btn-remove-saved" data-id="${d.id}" title="Remove from saved"
+              style="position:absolute;top:8px;right:8px;background:rgba(15,14,12,.85);color:#e8e2d6;border:0;border-radius:4px;padding:5px 9px;font:11px var(--sans,system-ui);cursor:pointer;letter-spacing:.04em;text-transform:uppercase">
+        Remove
+      </button>
+    </div>`;
+  }).join('');
+
+  const proposalIds = designs.map(d => d.id).join(',');
+  const proposalHref = proposalIds ? ('/brief?ids=' + proposalIds) : '#';
+
+  res.type('html').send(`${htmlHead({
+    title: 'Saved Designs — wallco.ai',
+    description: 'Your saved wallco.ai designs.',
+    canonical: 'https://wallco.ai/me/saved'
+  })}
+<body>
+${htmlHeader('/me/saved')}
+<main>
+<section style="padding:32px 40px 16px">
+  <div style="display:flex;flex-wrap:wrap;align-items:baseline;gap:16px;justify-content:space-between;margin-bottom:18px">
+    <div>
+      <h1 style="font:300 28px/1.2 'Cormorant Garamond',serif;margin:0 0 4px">Saved designs</h1>
+      <div style="font:13px var(--sans,system-ui);color:var(--ink-soft,#555)">
+        ${designs.length} ${designs.length === 1 ? 'design' : 'designs'} saved
+        · <a href="/trade/logout" style="color:var(--ink-soft,#555)">Sign out</a>
+      </div>
+    </div>
+    <div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center">
+      ${designs.length ? `<a href="${proposalHref}" target="_blank" rel="noopener noreferrer" id="saved-brief" style="padding:10px 18px;font:13px var(--sans);background:var(--gold,#c9a14b);color:var(--accent,#0d0d0d);text-decoration:none;border:0;border-radius:4px;font-weight:500" title="Export all saved designs as a printable multi-design proposal PDF">Build proposal PDF →</a>` : ''}
+      <a href="/designs" class="btn-outline" style="padding:10px 18px;font:13px var(--sans);text-decoration:none">Browse catalog</a>
+      ${designs.length ? `<details style="font:12px var(--sans,system-ui);color:var(--ink-soft,#555)">
+        <summary style="cursor:pointer;padding:6px 10px;border:1px solid var(--line,#ddd);border-radius:4px;list-style:none">Brief options ▾</summary>
+        <div style="margin-top:8px;padding:12px;background:var(--card-bg,#faf8f3);border:1px solid var(--line,#ebe5d6);border-radius:6px;display:grid;gap:8px;min-width:280px;position:absolute;z-index:20">
+          <label>Project name<br><input id="saved-brief-project" type="text" placeholder="e.g. Living Room" style="margin-top:4px;width:100%;padding:6px 8px;font:13px var(--sans);border:1px solid var(--line,#ddd);border-radius:4px"></label>
+          <label>Client name<br><input id="saved-brief-client" type="text" placeholder="e.g. Smith Residence" style="margin-top:4px;width:100%;padding:6px 8px;font:13px var(--sans);border:1px solid var(--line,#ddd);border-radius:4px"></label>
+          <button type="button" id="saved-brief-go" style="padding:8px 14px;background:var(--ink,#111);color:var(--bg,#fff);border:0;border-radius:4px;cursor:pointer;font:13px var(--sans)">Open Brief →</button>
+        </div>
+      </details>` : ''}
+    </div>
+    <script>
+    (function(){
+      var go = document.getElementById('saved-brief-go');
+      if (!go) return;
+      var ids = ${JSON.stringify(designs.map(d => d.id))};
+      go.addEventListener('click', function(){
+        var p = (document.getElementById('saved-brief-project').value || '').trim();
+        var c = (document.getElementById('saved-brief-client').value || '').trim();
+        var qs = ['ids=' + ids.join(',')];
+        if (p) qs.push('project=' + encodeURIComponent(p));
+        if (c) qs.push('client=' + encodeURIComponent(c));
+        window.open('/brief?' + qs.join('&'), '_blank', 'noopener');
+      });
+    })();
+    </script>
+  </div>
+  ${designs.length
+    ? `<div class="design-grid catalog-grid" id="saved-grid">${cards}</div>`
+    : `<div style="padding:56px 28px;text-align:center;color:var(--ink-soft,#555);font:14px/1.6 var(--sans);background:var(--card-bg,#faf8f3);border-radius:8px">
+         <div style="font-size:42px;line-height:1;margin-bottom:14px;color:var(--gold,#c9a14b)">♡</div>
+         <p style="margin:0 0 8px;font:300 22px/1.2 var(--serif,Georgia,serif);color:var(--ink,#111)">Nothing saved yet.</p>
+         <p style="margin:0 0 18px;max-width:38em;margin-inline:auto">Save designs you're considering for a project — they'll wait here until you're ready to build a proposal or request samples.</p>
+         <p style="margin:0"><a href="/designs" class="btn-outline" style="display:inline-block;padding:9px 18px;font:13px var(--sans);text-decoration:none">Browse the catalog →</a></p>
+         <p style="margin:14px 0 0;font:12px var(--sans);color:var(--ink-faint,#999)">Tap <strong>♥ Save</strong> on any design page.</p>
+       </div>`}
+</section>
+</main>
+${FOOTER}
+<script>
+(function(){
+  // Remove from saved — POST /api/saved toggles, then fade the card out.
+  document.querySelectorAll('.btn-remove-saved').forEach(function(btn){
+    btn.addEventListener('click', function(e){
+      e.preventDefault();
+      var id = parseInt(btn.dataset.id, 10);
+      if (!id) return;
+      btn.disabled = true;
+      fetch('/api/saved', {
+        method:'POST', headers:{'Content-Type':'application/json'},
+        body: JSON.stringify({ design_id: id })
+      })
+        .then(function(r){ return r.json(); })
+        .then(function(j){
+          if (j.ok && !j.saved) {
+            if (window.__tradeMe) window.__tradeMe.saved_count = j.saved_count;
+            try { window.dispatchEvent(new CustomEvent('wallco-saved-changed', { detail: { saved_count: j.saved_count, design_id: id, saved: false } })); } catch(e){}
+            var wrap = btn.closest('.saved-card-wrap');
+            if (wrap) {
+              wrap.style.transition = 'opacity .25s, transform .25s';
+              wrap.style.opacity = '0';
+              wrap.style.transform = 'scale(.94)';
+              setTimeout(function(){ wrap.remove();
+                if (!document.querySelectorAll('.saved-card-wrap').length) {
+                  location.reload();
+                }
+              }, 260);
+            }
+          } else {
+            btn.disabled = false;
+            alert(j.error || 'Could not remove.');
+          }
+        })
+        .catch(function(err){ btn.disabled = false; alert('Network error: ' + err.message); });
+    });
+  });
+})();
+</script>
+${HAMBURGER_JS}
+</body>
+</html>`);
+});
+
+// ── /me/activity — auth-gated. Per-trade-user activity log (own rows only).
+// Used by users to see their own footprint (saves, basket adds, sample requests, etc.).
+// JSON via ?format=json for client/export use.
+app.get('/me/activity', (req, res) => {
+  const u = getTradeUser(req);
+  if (!u) return res.redirect('/trade/login?return=' + encodeURIComponent('/me/activity'));
+  const limit = Math.max(1, Math.min(500, parseInt(req.query.limit || '100', 10)));
+  let rows = [];
+  try {
+    const raw = psqlQuery(`SELECT COALESCE(json_agg(t ORDER BY t.created_at DESC), '[]'::json) FROM (
+      SELECT id, action, payload, ip::text AS ip, user_agent, created_at
+      FROM wallco_audit_log
+      WHERE user_id = ${parseInt(u.id, 10)}
+      ORDER BY created_at DESC
+      LIMIT ${limit}
+    ) t;`);
+    rows = JSON.parse(raw || '[]');
+  } catch (e) {
+    console.error('[me/activity] query failed:', e.message);
+  }
+  // JSON export path (CCPA/GDPR-aligned data-portability).
+  if (String(req.query.format || '').toLowerCase() === 'json') {
+    res.setHeader('Cache-Control', 'no-store');
+    return res.json({ user_id: u.id, email: u.email, count: rows.length, rows });
+  }
+  const esc = (s) => String(s||'').replace(/[<>&"]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[c]));
+  const actionPretty = (a) => ({
+    trade_login_request: 'Requested sign-in link',
+    trade_login_success: 'Signed in',
+    trade_logout: 'Signed out',
+    design_saved: 'Saved a design',
+    design_unsaved: 'Unsaved a design',
+    sample_request_submitted: 'Requested samples',
+    designs_search: 'Searched the catalog',
+  })[a] || a;
+  const rowsHtml = rows.map(r => {
+    const t = new Date(r.created_at);
+    const when = t.toISOString().replace('T',' ').slice(0,16) + ' UTC';
+    let summary = '';
+    try {
+      const p = r.payload || {};
+      if (r.action === 'design_saved' || r.action === 'design_unsaved') summary = `design #${p.design_id}`;
+      else if (r.action === 'designs_search') summary = `“${esc(p.q || '')}” → ${p.count} hits`;
+      else if (r.action === 'sample_request_submitted') summary = `request_id=${esc(p.request_id || '')} · ${p.design_count} designs`;
+      else if (p && Object.keys(p).length) summary = esc(JSON.stringify(p).slice(0, 140));
+    } catch(_) {}
+    return `<tr>
+      <td style="padding:8px 12px;font:11px ui-monospace,Menlo,monospace;color:var(--ink-faint,#888);white-space:nowrap">${esc(when)}</td>
+      <td style="padding:8px 12px;font:13px var(--sans,system-ui);color:var(--ink,#111)">${esc(actionPretty(r.action))}</td>
+      <td style="padding:8px 12px;font:12px var(--sans,system-ui);color:var(--ink-soft,#555)">${summary}</td>
+    </tr>`;
+  }).join('');
+  res.setHeader('Cache-Control', 'no-store');
+  res.type('html').send(`${htmlHead({
+    title: 'My Activity — wallco.ai',
+    description: 'Your wallco.ai activity log.',
+    canonical: 'https://wallco.ai/me/activity'
+  })}
+<body>
+${htmlHeader('/me/activity')}
+<main>
+<section style="padding:32px 40px 16px;max-width:980px;margin:0 auto">
+  <div style="display:flex;flex-wrap:wrap;align-items:baseline;gap:16px;justify-content:space-between;margin-bottom:18px">
+    <div>
+      <h1 style="font:300 28px/1.2 'Cormorant Garamond',serif;margin:0 0 4px">My activity</h1>
+      <div style="font:13px var(--sans,system-ui);color:var(--ink-soft,#555)">
+        ${rows.length} recorded ${rows.length === 1 ? 'event' : 'events'} · <a href="/me/saved" style="color:var(--ink-soft,#555)">Saved →</a>
+      </div>
+    </div>
+    <div style="display:flex;gap:10px">
+      <a href="/me/activity?format=json" class="btn-outline" style="padding:8px 14px;font:12px var(--sans);text-decoration:none">Export JSON</a>
+    </div>
+  </div>
+  ${rows.length
+    ? `<table style="width:100%;border-collapse:collapse">
+        <thead><tr>
+          <th style="text-align:left;padding:6px 12px;font:10px var(--sans,system-ui);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-faint,#888);border-bottom:1px solid var(--line,#eee)">When (UTC)</th>
+          <th style="text-align:left;padding:6px 12px;font:10px var(--sans,system-ui);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-faint,#888);border-bottom:1px solid var(--line,#eee)">Action</th>
+          <th style="text-align:left;padding:6px 12px;font:10px var(--sans,system-ui);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-faint,#888);border-bottom:1px solid var(--line,#eee)">Detail</th>
+        </tr></thead>
+        <tbody>${rowsHtml}</tbody>
+      </table>`
+    : `<div style="padding:48px 28px;text-align:center;color:var(--ink-soft,#555);font:14px/1.6 var(--sans);background:var(--card-bg,#faf8f3);border-radius:8px">
+         <p style="margin:0 0 8px;font:300 22px/1.2 var(--serif,Georgia,serif);color:var(--ink,#111)">No activity yet.</p>
+         <p>Sign-ins, saves, sample requests, and catalog searches show up here.</p>
+       </div>`}
+</section>
+</main>
+${FOOTER}
+${HAMBURGER_JS}
+</body>
+</html>`);
+});
+
+// ── /admin/activity — admin-gated. All events across all users.
+// Filters: ?user_id=, ?action=, ?from=YYYY-MM-DD, ?to=YYYY-MM-DD, ?limit=
+// JSON export at ?format=json.
+app.get('/admin/activity', (req, res) => {
+  if (!isAdmin(req)) return res.status(404).type('html').send('<h1>404</h1>');
+  const limit = Math.max(1, Math.min(1000, parseInt(req.query.limit || '200', 10)));
+  const userId = parseInt(req.query.user_id, 10);
+  const action = String(req.query.action || '').trim();
+  const from = String(req.query.from || '').trim();
+  const to = String(req.query.to || '').trim();
+  const wheres = [];
+  if (Number.isFinite(userId) && userId > 0) wheres.push(`l.user_id = ${userId}`);
+  if (action) wheres.push(`l.action = ${pgEsc(action)}`);
+  if (/^\d{4}-\d{2}-\d{2}$/.test(from)) wheres.push(`l.created_at >= ${pgEsc(from)}::timestamptz`);
+  if (/^\d{4}-\d{2}-\d{2}$/.test(to))   wheres.push(`l.created_at < (${pgEsc(to)}::date + 1)::timestamptz`);
+  const whereClause = wheres.length ? `WHERE ${wheres.join(' AND ')}` : '';
+  let rows = [];
+  try {
+    const raw = psqlQuery(`SELECT COALESCE(json_agg(t ORDER BY t.created_at DESC), '[]'::json) FROM (
+      SELECT l.id, l.user_id, l.is_admin, l.actor_label, l.action, l.payload,
+             l.ip::text AS ip, l.user_agent, l.created_at,
+             u.email AS user_email
+      FROM wallco_audit_log l
+      LEFT JOIN wallco_trade_users u ON u.id = l.user_id
+      ${whereClause}
+      ORDER BY l.created_at DESC
+      LIMIT ${limit}
+    ) t;`);
+    rows = JSON.parse(raw || '[]');
+  } catch (e) {
+    console.error('[admin/activity] query failed:', e.message);
+  }
+  if (String(req.query.format || '').toLowerCase() === 'json') {
+    res.setHeader('Cache-Control', 'no-store');
+    return res.json({ count: rows.length, filter: { user_id: userId || null, action: action || null, from: from || null, to: to || null }, rows });
+  }
+  const esc = (s) => String(s||'').replace(/[<>&"]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[c]));
+  const rowsHtml = rows.map(r => {
+    const t = new Date(r.created_at);
+    const when = t.toISOString().replace('T',' ').slice(0,19) + 'Z';
+    const actor = r.is_admin ? '<span style="color:var(--gold,#c9a14b);font-weight:600">admin</span>'
+                : (r.user_email ? esc(r.user_email) : esc(r.actor_label || 'anon'));
+    let summary = '';
+    try { summary = r.payload ? esc(JSON.stringify(r.payload).slice(0, 240)) : ''; } catch(_) {}
+    return `<tr>
+      <td style="padding:6px 10px;font:10px ui-monospace,Menlo,monospace;color:var(--ink-faint,#888);white-space:nowrap">${esc(when)}</td>
+      <td style="padding:6px 10px;font:12px var(--sans,system-ui)">${actor}</td>
+      <td style="padding:6px 10px;font:12px var(--sans,system-ui);color:var(--ink,#111)">${esc(r.action)}</td>
+      <td style="padding:6px 10px;font:11px ui-monospace,Menlo,monospace;color:var(--ink-soft,#555);max-width:480px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${summary}">${summary}</td>
+      <td style="padding:6px 10px;font:11px ui-monospace,Menlo,monospace;color:var(--ink-faint,#888);white-space:nowrap">${esc(r.ip || '')}</td>
+    </tr>`;
+  }).join('');
+  res.setHeader('Cache-Control', 'no-store');
+  res.type('html').send(`${htmlHead({
+    title: 'Activity Log — wallco.ai admin',
+    description: 'Admin view of all audit events.',
+    canonical: 'https://wallco.ai/admin/activity'
+  })}
+<body>
+${htmlHeader('/admin/activity')}
+<main>
+<section style="padding:24px 40px;max-width:1280px;margin:0 auto">
+  <div style="display:flex;flex-wrap:wrap;gap:14px;align-items:baseline;justify-content:space-between;margin-bottom:14px">
+    <div>
+      <h1 style="font:300 28px/1.2 'Cormorant Garamond',serif;margin:0">Activity log <span style="font:12px var(--sans);color:var(--ink-faint,#888);text-transform:uppercase;letter-spacing:.08em;margin-left:6px">admin · all events</span></h1>
+      <div style="font:12px var(--sans,system-ui);color:var(--ink-soft,#555);margin-top:4px">${rows.length} rows · limit ${limit}${userId ? ' · user '+userId : ''}${action ? ' · action '+esc(action) : ''}${from ? ' · from '+esc(from) : ''}${to ? ' · to '+esc(to) : ''}</div>
+    </div>
+    <form method="get" action="/admin/activity" style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;font:12px var(--sans,system-ui)">
+      <input name="user_id" type="number" min="1" placeholder="user_id" value="${userId||''}" style="width:90px;padding:5px 8px;border:1px solid var(--line,#ddd);border-radius:4px">
+      <input name="action" type="text" placeholder="action" value="${esc(action)}" style="width:160px;padding:5px 8px;border:1px solid var(--line,#ddd);border-radius:4px">
+      <input name="from" type="date" value="${esc(from)}" style="padding:5px 8px;border:1px solid var(--line,#ddd);border-radius:4px">
+      <input name="to" type="date" value="${esc(to)}" style="padding:5px 8px;border:1px solid var(--line,#ddd);border-radius:4px">
+      <input name="limit" type="number" min="10" max="1000" value="${limit}" style="width:70px;padding:5px 8px;border:1px solid var(--line,#ddd);border-radius:4px">
+      <button type="submit" style="padding:5px 14px;background:var(--ink,#111);color:var(--bg,#fff);border:0;border-radius:4px;cursor:pointer">Filter</button>
+      <a href="?${new URLSearchParams(req.query).toString()}&format=json" style="padding:5px 12px;text-decoration:none;color:var(--ink-soft,#555);border:1px solid var(--line,#ddd);border-radius:4px">JSON</a>
+    </form>
+  </div>
+  ${rows.length
+    ? `<table style="width:100%;border-collapse:collapse;font-size:12px">
+        <thead><tr style="background:var(--card-bg,#faf8f3)">
+          <th style="text-align:left;padding:7px 10px;font:10px var(--sans,system-ui);text-transform:uppercase;letter-spacing:.06em;color:var(--ink-faint,#888);border-bottom:1px solid var(--line,#eee)">When (UTC)</th>
+          <th style="text-align:left;padding:7px 10px;font:10px var(--sans,system-ui);text-transform:uppercase;letter-spacing:.06em;color:var(--ink-faint,#888);border-bottom:1px solid var(--line,#eee)">Actor</th>
+          <th style="text-align:left;padding:7px 10px;font:10px var(--sans,system-ui);text-transform:uppercase;letter-spacing:.06em;color:var(--ink-faint,#888);border-bottom:1px solid var(--line,#eee)">Action</th>
+          <th style="text-align:left;padding:7px 10px;font:10px var(--sans,system-ui);text-transform:uppercase;letter-spacing:.06em;color:var(--ink-faint,#888);border-bottom:1px solid var(--line,#eee)">Payload</th>
+          <th style="text-align:left;padding:7px 10px;font:10px var(--sans,system-ui);text-transform:uppercase;letter-spacing:.06em;color:var(--ink-faint,#888);border-bottom:1px solid var(--line,#eee)">IP</th>
+        </tr></thead>
+        <tbody>${rowsHtml}</tbody>
+      </table>`
+    : '<p style="padding:32px;text-align:center;color:var(--ink-soft,#555)">No matching events.</p>'}
+</section>
+</main>
+${FOOTER}
+${HAMBURGER_JS}
+</body>
+</html>`);
+});
+
+// ── /admin/dashboard — key metrics for wallco.ai operations
+app.get('/admin/dashboard', (req, res) => {
+  if (!isAdmin(req)) return res.status(404).type('html').send('<h1>404</h1>');
+  // Pull metrics in one round-trip via single SQL block.
+  let metrics = {};
+  try {
+    const out = execSync(PSQL_CMD, { encoding: 'utf8', input: `
+SELECT json_build_object(
+  'designs_total',        (SELECT count(*) FROM spoon_all_designs WHERE generator='replicate'),
+  'designs_24h',          (SELECT count(*) FROM spoon_all_designs WHERE generator='replicate' AND created_at > now()-interval '24 hours'),
+  'designs_7d',           (SELECT count(*) FROM spoon_all_designs WHERE generator='replicate' AND created_at > now()-interval '7 days'),
+  'designs_published',    (SELECT count(*) FROM spoon_all_designs WHERE generator='replicate' AND is_published),
+  'by_category',          (SELECT COALESCE(json_object_agg(category, n),'{}') FROM (SELECT category, count(*) n FROM spoon_all_designs WHERE generator='replicate' GROUP BY category) t),
+  'saturation_high',      (SELECT count(*) FROM spoon_all_designs WHERE generator='replicate' AND dominant_hex IS NOT NULL),
+  'mockups_done',         (SELECT count(*) FROM spoon_all_designs WHERE room_mockups IS NOT NULL),
+  'leads_total',          (SELECT count(*) FROM wallco_leads),
+  'leads_7d',             (SELECT count(*) FROM wallco_leads WHERE created_at > now()-interval '7 days'),
+  'leads_24h',            (SELECT count(*) FROM wallco_leads WHERE created_at > now()-interval '24 hours'),
+  'chat_sessions',        (SELECT count(*) FROM wallco_chat_sessions),
+  'chat_msgs_24h',        (SELECT count(*) FROM wallco_chat_messages WHERE created_at > now()-interval '24 hours'),
+  'votes_total',          (SELECT count(*) FROM wallco_votes),
+  'votes_7d',             (SELECT count(*) FROM wallco_votes WHERE created_at > now()-interval '7 days'),
+  'inspiration_pool',     (SELECT count(*) FROM wallco_inspiration_pool),
+  'generator_runs',       (SELECT count(*) FROM wallco_generator_runs),
+  'generator_runs_24h',   (SELECT count(*) FROM wallco_generator_runs WHERE started_at > now()-interval '24 hours')
+);
+    `});
+    metrics = JSON.parse(out.trim());
+  } catch (e) {
+    return res.status(500).type('html').send(`<h1>dashboard query failed</h1><pre>${(e.message||'').replace(/</g,'&lt;')}</pre>`);
+  }
+
+  // Replicate cost: $0.014/SDXL gen × designs_total + $0.014/room mockup × mockups (multi-key JSONB).
+  // Sum keys per design — each room type is one Replicate call.
+  let mockupCount = metrics.mockups_done;
+  try {
+    const raw = execSync(PSQL_CMD, { encoding: 'utf8', input:
+      `SELECT COALESCE(SUM((SELECT count(*) FROM jsonb_object_keys(room_mockups))), 0)
+       FROM spoon_all_designs WHERE room_mockups IS NOT NULL;`
+    }).trim();
+    if (raw) mockupCount = parseInt(raw, 10) || metrics.mockups_done;
+  } catch { /* fall through to metrics.mockups_done */ }
+  const replicateCostSdxl = (metrics.designs_total * 0.014).toFixed(2);
+  const replicateCostRooms = (mockupCount * 0.014).toFixed(2);
+  const replicateCostTotal = (parseFloat(replicateCostSdxl) + parseFloat(replicateCostRooms)).toFixed(2);
+
+  const card = (label, value, sub) => `
+    <div class="m-card">
+      <div class="m-label">${label}</div>
+      <div class="m-value">${value}</div>
+      ${sub ? `<div class="m-sub">${sub}</div>` : ''}
+    </div>`;
+
+  const catBreakdown = Object.entries(metrics.by_category || {})
+    .sort((a,b) => b[1]-a[1])
+    .map(([k,v]) => `<span class="cat-chip"><span class="cat-k">${k}</span> <span class="cat-v">${v}</span></span>`)
+    .join('');
+
+  res.type('html').send(`<!doctype html>
+<html lang="en"><head>
+<meta charset="utf-8"><title>wallco.ai — admin dashboard</title>
+<style>
+  :root { --bg:#0f0e0c; --panel:#1a1816; --line:#2a2825; --ink:#e8e2d6; --ink-faint:#888; --accent:#d2b15c; }
+  * { box-sizing:border-box }
+  body { font:14px/1.5 -apple-system,system-ui,sans-serif; margin:0; padding:32px 40px; background:var(--bg); color:var(--ink); }
+  h1 { font-weight:300; font-size:28px; margin:0 0 6px; letter-spacing:.5px; }
+  .sub { color:var(--ink-faint); font-size:12.5px; margin-bottom:28px; }
+  .sub a { color:var(--accent); text-decoration:none; margin-right:14px; }
+  .grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); gap:14px; margin-bottom:28px; }
+  .m-card { background:var(--panel); border:1px solid var(--line); border-radius:6px; padding:16px 18px; }
+  .m-label { color:var(--ink-faint); font-size:11.5px; text-transform:uppercase; letter-spacing:.7px; margin-bottom:8px; }
+  .m-value { font-size:28px; font-weight:300; letter-spacing:.3px; color:var(--ink); }
+  .m-sub { color:var(--accent); font-size:11.5px; margin-top:4px; }
+  .section-h { font-size:13px; text-transform:uppercase; letter-spacing:1px; color:var(--ink-faint); margin:0 0 12px; }
+  .cat-row { display:flex; flex-wrap:wrap; gap:8px; margin-bottom:28px; }
+  .cat-chip { background:var(--panel); border:1px solid var(--line); border-radius:14px; padding:6px 12px; font-size:12.5px; }
+  .cat-k { color:var(--ink); text-transform:capitalize; }
+  .cat-v { color:var(--accent); margin-left:6px; font-weight:500; }
+  .footer { color:var(--ink-faint); font-size:11px; margin-top:24px; padding-top:14px; border-top:1px solid var(--line); }
+</style></head>
+<body>
+  <h1>wallco.ai · admin dashboard</h1>
+  <div class="sub">
+    <a href="/admin/golive">go-live</a>
+    <a href="/admin/leads">leads</a>
+    <a href="/designs?review=1">review designs</a>
+    <a href="/admin/inspiration">inspiration</a>
+    · refreshed ${new Date().toLocaleString('en-US',{ timeZone:'America/Los_Angeles' })} PT
+  </div>
+
+  <h2 class="section-h">Catalog</h2>
+  <div class="grid">
+    ${card('Total designs',       metrics.designs_total, `${metrics.designs_published} published`)}
+    ${card('Generated · last 24h', metrics.designs_24h)}
+    ${card('Generated · last 7d',  metrics.designs_7d)}
+    ${card('Room mockups',         metrics.mockups_done, `${mockupCount} total renderings`)}
+    ${card('Inspiration pool',     metrics.inspiration_pool)}
+  </div>
+
+  <h2 class="section-h">By category</h2>
+  <div class="cat-row">${catBreakdown || '<span class="m-sub">no data</span>'}</div>
+
+  <h2 class="section-h">Demand signals</h2>
+  <div class="grid">
+    ${card('Leads · total',       metrics.leads_total)}
+    ${card('Leads · last 7d',     metrics.leads_7d)}
+    ${card('Leads · last 24h',    metrics.leads_24h)}
+    ${card('Chat sessions',       metrics.chat_sessions, `${metrics.chat_msgs_24h} msgs last 24h`)}
+    ${card('Votes',               metrics.votes_total, `${metrics.votes_7d} in last 7d`)}
+  </div>
+
+  <h2 class="section-h">Cost burn</h2>
+  <div class="grid">
+    ${card('Replicate · SDXL',    '$' + replicateCostSdxl, `${metrics.designs_total} gens @ $0.014`)}
+    ${card('Replicate · rooms',   '$' + replicateCostRooms, `${mockupCount} renders @ $0.014`)}
+    ${card('Replicate · TOTAL',   '$' + replicateCostTotal)}
+    ${card('Generator runs',      metrics.generator_runs, `${metrics.generator_runs_24h} last 24h`)}
+  </div>
+
+  <div class="footer">Source: dw_unified.spoon_all_designs + wallco_*. Admin-only route. Cache: live.</div>
+</body></html>`);
+});
+
+// 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 || '';
+
+// ── Design brief / color palette swatches
+function paletteSwatches(design) {
+  if (!design.dominant_hex) return '';
+  const colors = [design.dominant_hex];
+  // complementary approximation
+  const [h,,l] = hexToHSL(design.dominant_hex);
+  return colors.map(c =>
+    `<span class="swatch" style="background:${c}" title="${c}"></span>`
+  ).join('');
+}
+
+// ── Shared HTML fragments
+const GTAG = GA4_ID ? `
+<!-- Google tag (gtag.js) -->
+<script async src="https://www.googletagmanager.com/gtag/js?id=${GA4_ID}"></script>
+<script>
+  window.dataLayer = window.dataLayer || [];
+  function gtag(){dataLayer.push(arguments);}
+  gtag('js', new Date());
+  gtag('config', '${GA4_ID}');
+</script>` : '';
+
+function htmlHead({ title, description, canonical, ogImage = '', ogType = 'website', ogImageAlt = '', productOG = null, preloadImages = [] }) {
+  const siteUrl = `https://${SITE}`;
+  const ogImg = ogImage || `${siteUrl}/og-default.png`;
+  const imgAlt = ogImageAlt || title;
+  // product:* tags add Facebook/Pinterest product-pin metadata when ogType==='product'.
+  // productOG = { availability, price, currency, retailer_item_id } — all optional.
+  const productTags = ogType === 'product' && productOG ? [
+    productOG.availability && `<meta property="product:availability" content="${productOG.availability}">`,
+    productOG.retailer_item_id && `<meta property="product:retailer_item_id" content="${productOG.retailer_item_id}">`,
+    productOG.price && productOG.currency && `<meta property="product:price:amount" content="${productOG.price}">`,
+    productOG.price && productOG.currency && `<meta property="product:price:currency" content="${productOG.currency}">`,
+    productOG.brand && `<meta property="product:brand" content="${productOG.brand}">`,
+    productOG.category && `<meta property="product:category" content="${productOG.category}">`
+  ].filter(Boolean).join('\n') : '';
+  // Preload tags for above-the-fold images (LCP). Browsers start fetching
+  // these during <head> parse, ~100-300ms before the body scan would discover
+  // the same URLs in background-image: declarations. fetchpriority=high marks
+  // the first one as the LCP candidate so the network stack prioritizes it.
+  const preloadTags = (preloadImages || []).slice(0, 6).map((u, i) =>
+    `<link rel="preload" as="image" href="${u}"${i === 0 ? ' fetchpriority="high"' : ''}>`
+  ).join('\n');
+  return `<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<!-- Mobile browser chrome — Android/Chrome tints the address bar; iOS Safari
+     uses it for the status bar in standalone PWA mode. Cream in light mode,
+     dark accent in dark mode so the bar visually merges with page content. -->
+<meta name="theme-color" content="#faf8f3" media="(prefers-color-scheme: light)">
+<meta name="theme-color" content="#0d0d0d" media="(prefers-color-scheme: dark)">
+<meta name="apple-mobile-web-app-status-bar-style" content="default">
+<title>${title}</title>
+<meta name="description" content="${description}">
+<link rel="canonical" href="${canonical}">
+${preloadTags}
+<!-- Open Graph -->
+<meta property="og:type" content="${ogType}">
+<meta property="og:site_name" content="wallco.ai">
+<meta property="og:title" content="${title}">
+<meta property="og:description" content="${description}">
+<meta property="og:url" content="${canonical}">
+<meta property="og:image" content="${ogImg}">
+<meta property="og:image:secure_url" content="${ogImg}">
+<meta property="og:image:alt" content="${imgAlt}">
+<meta property="og:locale" content="en_US">
+${productTags}
+<!-- Twitter Card -->
+<meta name="twitter:card" content="summary_large_image">
+<meta name="twitter:title" content="${title}">
+<meta name="twitter:description" content="${description}">
+<meta name="twitter:image" content="${ogImg}">
+<meta name="twitter:image:alt" content="${imgAlt}">
+${GTAG}
+<!-- Dark/Light anti-flash + Age-band early apply (must be first JS in <head>) -->
+<script>
+(function(){
+  var t = localStorage.getItem('wallco-theme');
+  if (!t) {
+    t = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
+  }
+  document.documentElement.setAttribute('data-theme', t);
+  var age = parseInt(localStorage.getItem('wallco-age'),10);
+  function band(a){
+    if (!a) return '';
+    if (a < 6)  return 'toddler';
+    if (a < 11) return 'elementary';
+    if (a < 14) return 'tween';
+    if (a < 18) return 'teen';
+    if (a < 40) return 'adult';
+    if (a < 60) return 'mature';
+    if (a < 80) return 'senior';
+    return 'elder';
+  }
+  if (age) document.documentElement.setAttribute('data-age-band', band(age));
+})();
+</script>
+<link rel="stylesheet" href="/css/site.css">
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link rel="preconnect" href="https://www.googletagmanager.com">
+<link rel="dns-prefetch" href="https://www.google-analytics.com">
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,400&family=Inter:wght@300;400;500&display=swap" rel="stylesheet">
+</head>`;
+}
+
+function htmlHeader(active) {
+  const navLinks = [
+    ['/', 'Home'],
+    ['/designs', 'Designs'],
+    ['/murals', 'Murals'],
+    ['/samples', 'Samples'],
+    ['/basket', 'Basket'],
+    ['/studio', 'Studio'],
+    ['/play',   'Play'],
+    ['/generator', 'Generator'],
+    ['/marketplace', 'Marketplace'],
+    ['/designers', 'Designers'],
+    ['/patterns', 'Patterns'],
+    ['/about', 'About'],
+  ];
+  return `<a class="skip-link" href="#main-content">Skip to main content</a>
+<header class="site-header">
+  <a class="brand" href="/">wallco.ai</a>
+  <div class="trade-nav" id="trade-nav" style="position:absolute;right:64px;top:50%;transform:translateY(-50%);font:12px var(--sans,system-ui);display:flex;align-items:center;gap:10px;z-index:5">
+    <a id="trade-nav-signin" href="/trade/login" style="color:var(--ink,#111);text-decoration:none;padding:6px 12px;border:1px solid var(--line,#ddd);border-radius:999px;background:transparent">Sign In</a>
+    <span id="trade-nav-user" style="display:none;align-items:center;gap:8px">
+      <a href="/me/saved" style="color:var(--ink,#111);text-decoration:none">
+        <span id="trade-nav-name" style="font-weight:500"></span>
+        <span style="margin-left:6px;color:var(--ink-faint,#888)">·</span>
+        Saved (<span id="trade-nav-saved-count">0</span>)
+      </a>
+      <a href="/trade/logout" title="Sign out" style="color:var(--ink-faint,#888);text-decoration:none;font-size:11px;margin-left:4px">⎋</a>
+    </span>
+  </div>
+  <button class="hamburger" id="nav-toggle" aria-controls="nav-panel" aria-expanded="false" aria-label="Open menu">
+    <span></span><span></span><span></span>
+  </button>
+</header>
+<nav class="nav-panel" id="nav-panel" hidden>
+  ${navLinks.map(([href, label]) =>
+    `<a href="${href}" class="${active === href ? 'active' : ''}"${href === '/basket' ? ' id="nav-basket-link"' : ''}>${label}${href === '/basket' ? '<span id="nav-basket-count" style="display:none;margin-left:6px;font:11px var(--sans,system-ui);background:var(--gold,#c9a14b);color:var(--accent,#0d0d0d);padding:1px 7px;border-radius:999px;vertical-align:middle"></span>' : ''}</a>`
+  ).join('\n  ')}
+  <a href="/me/saved" class="${active === '/me/saved' ? 'active' : ''}">My Saved</a>
+  <a href="/me/activity" class="${active === '/me/activity' ? 'active' : ''}">My Activity</a>
+  <button class="theme-toggle" id="theme-toggle" aria-label="Toggle dark/light mode">
+    <svg class="sun-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
+    <svg class="moon-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
+  </button>
+</nav>
+<script>
+(function(){
+  // Trade-user nav hydration. Hits /api/trade/me; swaps Sign-In ↔ name+saved-count.
+  // window.__tradeMe is shared with the PDP save-button + /me/saved so we hit
+  // the endpoint only once per page.
+  if (window.__tradeMeLoaded) return;
+  window.__tradeMeLoaded = true;
+  fetch('/api/trade/me', { credentials: 'same-origin' })
+    .then(function(r){ return r.json(); })
+    .then(function(j){
+      window.__tradeMe = j;
+      var signin = document.getElementById('trade-nav-signin');
+      var userBox = document.getElementById('trade-nav-user');
+      if (j && j.authenticated) {
+        if (signin) signin.style.display = 'none';
+        if (userBox) userBox.style.display = 'inline-flex';
+        var nameEl = document.getElementById('trade-nav-name');
+        var countEl = document.getElementById('trade-nav-saved-count');
+        if (nameEl) nameEl.textContent = (j.user && (j.user.name || j.user.email.split('@')[0])) || '';
+        if (countEl) countEl.textContent = j.saved_count || 0;
+      } else {
+        if (signin) {
+          var ret = encodeURIComponent(location.pathname + location.search);
+          signin.setAttribute('href', '/trade/login?return=' + ret);
+        }
+      }
+      // Notify other scripts on this page (e.g. PDP save button)
+      try { window.dispatchEvent(new CustomEvent('trade-me-loaded', { detail: j })); } catch(e){}
+    })
+    .catch(function(){});
+  // Listen for save-state changes from any surface (PDP toggle, /me/saved Remove,
+  // future autocomplete heart, etc.) and re-sync the nav pill count without a fetch.
+  window.addEventListener('wallco-saved-changed', function(ev){
+    var countEl = document.getElementById('trade-nav-saved-count');
+    var n = ev && ev.detail && typeof ev.detail.saved_count === 'number' ? ev.detail.saved_count : null;
+    if (n === null) return; // fallback: ignore; nav can still hydrate on next page load
+    if (countEl) countEl.textContent = n;
+    if (window.__tradeMe) window.__tradeMe.saved_count = n;
+  });
+})();
+</script>
+<span id="main-content" tabindex="-1"></span>`;
+}
+
+const FOOTER = `<footer class="site-footer">
+  <p>wallco.ai &mdash; AI-original wallpaper &amp; murals. Every pattern generated, never repeated.</p>
+  <p>Part of the <a href="https://designerwallcoverings.com">Designer Wallcoverings</a> family.</p>
+  <p class="footer-contact">
+    <a href="mailto:info@wallco.ai">info@wallco.ai</a>
+    <span aria-hidden="true">&middot;</span>
+    <a href="tel:+18883734564">888-373-4564</a>
+  </p>
+  <address class="footer-address">
+    Designer Wallcoverings &middot; 15442 Ventura Bl #102 &middot; Sherman Oaks, CA 91403
+  </address>
+  <p class="footer-legal">&copy; ${new Date().getFullYear()} Designer Wallcoverings. Memo samples are always free.</p>
+</footer>
+<script src="/js/wallco-game.js" defer></script>
+<!-- Mobile tap-target minimums (Apple HIG / WCAG 2.5.5) — applied site-wide.
+     Forces every clickable to ≥44×44 on phones without overriding desktop layout. -->
+<style id="mobile-tap-floor">
+@media (max-width: 640px) {
+  button:not(.hero-dot):not(.palette-dot):not([aria-hidden="true"]):not(.hamburger),
+  .btn-primary, .btn-outline,
+  a.proj-card, .play-card, .lb-card,
+  .material-tabs > *, .nav-panel a, .nav-panel button,
+  .filter-pill, .dots-sort, .edit-tab,
+  .hue-chip, a.hue-chip, .motif-chip, a.motif-chip,
+  .pill, .tab-pill, .chip, a.chip,
+  a.design-card, .featured-grid > a, a.brand,
+  .pager-link, a.pager-link,
+  .detail-nav-link, a.detail-nav-link,
+  input[type="submit"], input[type="button"] {
+    min-height: 44px;
+  }
+  .material-tabs > *, .dots-sort, .filter-pill,
+  .hue-chip, a.hue-chip, .motif-chip, a.motif-chip,
+  .pill, .tab-pill, .chip, a.chip {
+    padding-top: 10px; padding-bottom: 10px;
+    line-height: 1.2; box-sizing: border-box;
+    display: inline-flex; align-items: center; justify-content: center;
+  }
+  .lb-card .meta { padding: 10px 10px 12px; min-height: 44px; }
+  .edit-tab { padding: 14px 12px !important; }
+  body { font-size: 16px; }
+  input, select, textarea { font-size: 16px; }
+}
+</style>
+<script src="/js/chat.js" defer></script>`;
+
+const HAMBURGER_JS = `
+<!-- First-visit Age Prompt (drives age-adaptive UI per ageBand) -->
+<style>
+  #age-prompt-overlay { position:fixed; inset:0; background:rgba(15,14,12,.85); z-index:9999; align-items:center; justify-content:center; backdrop-filter:blur(8px); display:none; }
+  #age-prompt-overlay.is-open { display:flex; }
+</style>
+<div id="age-prompt-overlay" aria-hidden="true">
+  <div style="background:#fff;border-radius:12px;padding:36px 40px;max-width:480px;width:90%;box-shadow:0 24px 64px rgba(0,0,0,.4);font-family:'Inter',system-ui,sans-serif">
+    <h2 style="font-family:'Cormorant Garamond',serif;font-weight:300;font-size:26px;margin:0 0 6px;color:#1a1a1a">Welcome to wallco.ai</h2>
+    <p style="margin:0 0 22px;color:#666;font-size:14px;line-height:1.5">Tell us your age so we can show you the version of this site that's easiest for you to use.</p>
+    <input type="number" id="age-input" min="3" max="99" placeholder="Your age" style="width:100%;padding:14px 16px;font-size:16px;border:1px solid #ccc;border-radius:6px;margin-bottom:14px;font-family:inherit">
+    <div style="display:flex;gap:10px">
+      <button id="age-submit" style="flex:1;padding:14px;background:#1a1a1a;color:#fff;border:0;border-radius:6px;font-size:14px;font-weight:500;cursor:pointer">Show me the site</button>
+      <button id="age-skip" style="padding:14px 18px;background:transparent;color:#999;border:1px solid #ddd;border-radius:6px;font-size:13px;cursor:pointer">Skip</button>
+    </div>
+    <p style="margin:14px 0 0;color:#999;font-size:11px;text-align:center">Adjustable anytime in Settings.</p>
+  </div>
+</div>
+<script>
+(function(){
+  var btn = document.getElementById('nav-toggle');
+  var panel = document.getElementById('nav-panel');
+  if (btn && panel) {
+    btn.addEventListener('click', function() {
+      var open = panel.hidden;
+      panel.hidden = !open;
+      btn.setAttribute('aria-expanded', open ? 'true' : 'false');
+    });
+  }
+  // Theme toggle
+  var tt = document.getElementById('theme-toggle');
+  if (tt) {
+    tt.addEventListener('click', function() {
+      var cur = document.documentElement.getAttribute('data-theme') || 'light';
+      var next = cur === 'dark' ? 'light' : 'dark';
+      document.documentElement.setAttribute('data-theme', next);
+      localStorage.setItem('wallco-theme', next);
+    });
+  }
+  // Age prompt — first visit only
+  function ageBand(a){
+    if (!a) return '';
+    if (a < 6)  return 'toddler';
+    if (a < 11) return 'elementary';
+    if (a < 14) return 'tween';
+    if (a < 18) return 'teen';
+    if (a < 40) return 'adult';
+    if (a < 60) return 'mature';
+    if (a < 80) return 'senior';
+    return 'elder';
+  }
+  var overlay = document.getElementById('age-prompt-overlay');
+  function closeOverlay(){
+    if (!overlay) return;
+    overlay.classList.remove('is-open');
+    overlay.setAttribute('aria-hidden','true');
+  }
+  var seen = localStorage.getItem('wallco-age') || localStorage.getItem('wallco-age-skipped');
+  // Only show on the home page — deep-links (/design/:id, /studio, etc.) shouldn't be blocked.
+  var onHome = (location.pathname === '/' || location.pathname === '/index.html');
+  if (!seen && overlay && onHome) {
+    overlay.classList.add('is-open');
+    overlay.setAttribute('aria-hidden','false');
+    setTimeout(function(){ var i = document.getElementById('age-input'); if (i) i.focus(); }, 100);
+    document.getElementById('age-submit').addEventListener('click', function(){
+      var a = parseInt(document.getElementById('age-input').value, 10);
+      if (!a || a < 3 || a > 99) { document.getElementById('age-input').style.borderColor='#c00'; return; }
+      localStorage.setItem('wallco-age', String(a));
+      document.documentElement.setAttribute('data-age-band', ageBand(a));
+      closeOverlay();
+    });
+    document.getElementById('age-skip').addEventListener('click', function(){
+      localStorage.setItem('wallco-age-skipped', '1');
+      closeOverlay();
+    });
+    document.getElementById('age-input').addEventListener('keydown', function(e){
+      if (e.key === 'Enter') document.getElementById('age-submit').click();
+    });
+    // Escape key also skips
+    document.addEventListener('keydown', function(e){
+      if (e.key === 'Escape' && overlay.classList.contains('is-open')) {
+        localStorage.setItem('wallco-age-skipped', '1');
+        closeOverlay();
+      }
+    });
+  } else if (overlay) {
+    // Already seen — make sure overlay never reappears
+    closeOverlay();
+  }
+})();
+</script>
+<script>
+// Header basket-count hydration — reads localStorage['wallco-basket'] and
+// writes the count chip into the nav. No count shown when basket is empty.
+// Also listens for 'wallco-basket-changed' so the chip stays in sync when
+// + Sample is toggled on the PDP or Remove is clicked on /basket.
+(function(){
+  function paint(n){
+    var chip = document.getElementById('nav-basket-count');
+    if (!chip) return;
+    if (n > 0) { chip.textContent = n; chip.style.display = 'inline-block'; }
+    else { chip.textContent = ''; chip.style.display = 'none'; }
+  }
+  function readFromStorage(){
+    try {
+      var list = JSON.parse(localStorage.getItem('wallco-basket') || '[]');
+      if (!Array.isArray(list)) list = [];
+      return list.filter(function(x){ return typeof x === 'number' && x > 0; }).length;
+    } catch(e) { return 0; }
+  }
+  paint(readFromStorage());
+  // Event payload may carry an authoritative count; fall back to localStorage read.
+  window.addEventListener('wallco-basket-changed', function(ev){
+    var n = (ev && ev.detail && typeof ev.detail.count === 'number') ? ev.detail.count : readFromStorage();
+    paint(n);
+  });
+})();
+</script>`;
+
+// ── Hero rotation images (highest-saturation designs, sat > 0.4)
+function heroDesigns() {
+  const h = DESIGNS
+    .filter(d => d.saturation > 0.3)
+    .sort((a,b) => b.saturation - a.saturation);
+  // fallback: if none qualify, use all sorted by saturation
+  return h.length >= 2 ? h : [...DESIGNS].sort((a,b) => b.saturation - a.saturation);
+}
+
+// ── HOME PAGE
+app.get('/', (req, res) => {
+  const heroes = heroDesigns();
+  // Newest-first Featured grid — was sat-sorted, but Steve wants newly
+  // generated work to be the first thing visitors see. created_at lex sort is
+  // safe because every row is ISO-8601 UTC.
+  const featured = [...DESIGNS].sort((a,b) => String(b.created_at||'').localeCompare(String(a.created_at||''))).slice(0, 8);
+  // Pinned "Just-generated · Butterflies" rail — surfaces the latest 6
+  // butterfly-flavored designs above the Featured grid. Matches on category
+  // OR motif so we catch butterfly-trellis as well as future butterfly-*
+  // categories. Empty array → rail renders nothing (no broken shelf).
+  const butterflyLatest = [...DESIGNS]
+    .filter(d => {
+      const cat = String(d.category || '').toLowerCase();
+      if (cat.includes('butterfly')) return true;
+      return (d.motifs || []).some(m => String(m).toLowerCase().includes('butterfly'));
+    })
+    .sort((a,b) => String(b.created_at||'').localeCompare(String(a.created_at||'')))
+    .slice(0, 6);
+  const schema = JSON.stringify({
+    "@context": "https://schema.org",
+    "@graph": [
+      {
+        "@type": "Organization",
+        "name": "wallco.ai",
+        "url": "https://wallco.ai",
+        "email": "info@wallco.ai",
+        "parentOrganization": {
+          "@type": "Organization",
+          "name": "Designer Wallcoverings",
+          "url": "https://designerwallcoverings.com",
+          "address": {
+            "@type": "PostalAddress",
+            "streetAddress": "15442 Ventura Bl #102",
+            "addressLocality": "Sherman Oaks",
+            "addressRegion": "CA",
+            "postalCode": "91403"
+          }
+        }
+      },
+      {
+        "@type": "WebSite",
+        "name": "wallco.ai",
+        "url": "https://wallco.ai",
+        "description": "AI-original wallpaper and murals — every pattern generated, never repeated."
+      },
+      {
+        "@type": "ItemList",
+        "name": "Featured wallco.ai original designs",
+        "numberOfItems": featured.length,
+        "itemListElement": featured.map((d, i) => ({
+          "@type": "ListItem",
+          "position": i + 1,
+          "name": d.title,
+          "url": `https://wallco.ai/design/${d.id}`
+        }))
+      }
+    ]
+  });
+
+  const heroSlides = heroes.slice(0, 6).map((d, i) =>
+    `<div class="hero-slide ${i===0?'active':''}" data-idx="${i}" style="background-image:url('${d.image_url}')" aria-label="${d.title}"></div>`
+  ).join('\n');
+
+  // Top categories by count — used to render the home random-rail chip strip.
+  const homeCatCounts = Object.create(null);
+  for (const d of DESIGNS) {
+    const c = (d.category || '').toLowerCase().trim();
+    if (c) homeCatCounts[c] = (homeCatCounts[c] || 0) + 1;
+  }
+  const homeCats = Object.entries(homeCatCounts).sort((a,b) => b[1] - a[1]).slice(0, 6).map(([k,n]) => ({ key: k, count: n }));
+
+  const renderCard = (d) => {
+    const roomKey = (d.room_mockups || []).includes('living_room') ? 'living_room'
+                  : (d.room_mockups || []).includes('bedroom')     ? 'bedroom'
+                  : (d.room_mockups || []).includes('office')      ? 'office'
+                  : (d.room_mockups || [])[0];
+    const roomCount = (d.room_mockups || []).length;
+    const badgeText = roomCount > 1 ? `${roomCount} rooms` : (roomCount === 1 ? 'In a room' : '');
+    const roomOverlay = roomKey
+      ? `<img class="card-room" src="/designs/room/design_${d.id}_${roomKey}.png" loading="lazy" alt="" aria-hidden="true"><span class="card-room-badge">${badgeText}</span>`
+      : '';
+    return `
+    <a href="/design/${d.id}" class="design-card${roomKey ? ' has-room' : ''}" aria-label="${cardAriaLabel(d)}">
+      <div class="card-img" style="background-image:url('${d.image_url}')" role="img" aria-label="${d.title} pattern thumbnail">${roomOverlay}</div>
+      <div class="card-meta">
+        <span class="card-title">${d.title}</span>
+        <span class="card-cat">${d.category}</span>
+        <span class="color-dot" style="background:${d.dominant_hex}" aria-hidden="true"></span>
+      </div>
+    </a>`;
+  };
+  const featuredCards = featured.map(renderCard).join('');
+  const butterflyCards = butterflyLatest.map(renderCard).join('');
+  // Hide the whole rail if no butterflies exist — keeps the page clean if the
+  // generator goes through a non-butterfly streak.
+  const butterflySection = butterflyLatest.length ? `
+<!-- Just-generated · Butterflies — pinned above Featured -->
+<section class="featured-section">
+  <div class="section-header">
+    <h2>Just generated &middot; Butterflies</h2>
+    <a href="/designs?q=butterfly&amp;sort=newest" class="see-all">See all butterfly designs &rarr;</a>
+  </div>
+  <div class="design-grid featured-grid">
+    ${butterflyCards}
+  </div>
+</section>
+` : '';
+
+  res.type('html').send(`${htmlHead({
+    title: 'wallco.ai — AI-Original Wallpaper & Murals',
+    description: 'Original wallpaper and murals from wallco.ai. Every pattern hand-curated through our proprietary design system, never repeated. Browse, preview, and order samples.',
+    canonical: 'https://wallco.ai/',
+    // Preload the hero's first slide + first few featured cards so LCP starts
+    // fetching during <head> parse instead of after body scan discovers the
+    // background-image URLs. heroes[0] is the most likely LCP candidate.
+    preloadImages: [
+      heroes[0]?.image_url,
+      ...featured.slice(0, 4).map(d => d.image_url)
+    ].filter(Boolean)
+  })}
+<body>
+${htmlHeader('/')}
+<main>
+
+<!-- Hero: full-bleed rotating -->
+<section class="hero-section">
+  <div class="hero-track" id="hero-track">
+    ${heroSlides}
+  </div>
+  <div class="hero-controls" aria-hidden="true">
+    ${heroes.slice(0, 6).map((_,i) => `<button class="hero-dot ${i===0?'active':''}" data-slide="${i}"></button>`).join('')}
+  </div>
+</section>
+
+<!-- Wordmark below hero (Gucci-anatomy per standing rule) -->
+<div class="hero-wordmark-below">
+  <h1 class="wordmark">wallco.ai</h1>
+  <p class="tagline">AI-original wallpaper &amp; murals &mdash; every pattern generated, never repeated.</p>
+</div>
+
+${butterflySection}
+<!-- Featured designs grid -->
+<section class="featured-section">
+  <div class="section-header">
+    <h2>Newest Designs</h2>
+    <a href="/designs?sort=newest" class="see-all">See all ${DESIGNS.length} designs &rarr;</a>
+  </div>
+  <div class="design-grid featured-grid" id="featured-grid">
+    ${featuredCards}
+  </div>
+</section>
+
+<!-- Random discovery rail — fresh per visit -->
+<section class="featured-section" style="padding-top:18px">
+  <div class="section-header" style="display:flex;justify-content:space-between;align-items:baseline;gap:12px;flex-wrap:wrap">
+    <h2>Stumble through the catalog</h2>
+    <button type="button" id="home-rail-shuffle" style="font:11px var(--sans,system-ui);background:transparent;border:1px solid var(--line,#ddd);color:var(--ink-soft,#555);padding:6px 14px;border-radius:999px;cursor:pointer">↻ Shuffle</button>
+  </div>
+  <div id="home-rail-cats" role="tablist" aria-label="Random rail category" style="display:flex;flex-wrap:wrap;gap:6px;margin:0 0 14px;align-items:center">
+    <button type="button" role="tab" data-cat="" class="home-cat-chip is-on" style="font:11px var(--sans,system-ui);padding:5px 12px;border-radius:999px;border:1px solid var(--accent,#0d0d0d);background:var(--accent,#0d0d0d);color:var(--bg,#fff);cursor:pointer">All</button>
+    ${homeCats.map(c => `<button type="button" role="tab" data-cat="${c.key}" class="home-cat-chip" style="font:11px var(--sans,system-ui);padding:5px 12px;border-radius:999px;border:1px solid var(--line,#ddd);background:transparent;color:var(--ink-soft,#555);cursor:pointer">${c.key.charAt(0).toUpperCase()+c.key.slice(1)} <span style="opacity:.55">·${c.count}</span></button>`).join('')}
+  </div>
+  <div id="home-random-rail" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:14px;min-height:240px"></div>
+</section>
+<script>
+(function(){
+  var rail = document.getElementById('home-random-rail');
+  var btn = document.getElementById('home-rail-shuffle');
+  var catBar = document.getElementById('home-rail-cats');
+  if (!rail) return;
+  var esc = function(s){ return String(s||'').replace(/[<>&"]/g, function(c){ return {'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[c]; }); };
+  var activeCat = '';
+  try { activeCat = localStorage.getItem('wallco-home-rail-cat') || ''; } catch(e){}
+  function paintChips(){
+    if (!catBar) return;
+    catBar.querySelectorAll('.home-cat-chip').forEach(function(b){
+      var on = (b.dataset.cat || '') === activeCat;
+      b.classList.toggle('is-on', on);
+      b.style.background = on ? 'var(--accent,#0d0d0d)' : 'transparent';
+      b.style.color      = on ? 'var(--bg,#fff)'        : 'var(--ink-soft,#555)';
+      b.style.borderColor= on ? 'var(--accent,#0d0d0d)' : 'var(--line,#ddd)';
+      b.setAttribute('aria-selected', on ? 'true' : 'false');
+    });
+  }
+  function load(){
+    rail.innerHTML = '<div style="grid-column:1/-1;padding:50px;text-align:center;color:var(--ink-faint,#999);font:13px var(--sans,system-ui)">Loading…</div>';
+    var url = '/api/designs/random?n=12' + (activeCat ? '&category=' + encodeURIComponent(activeCat) : '');
+    fetch(url).then(function(r){ return r.json(); }).then(function(j){
+      if (!j.results || !j.results.length) { rail.innerHTML = '<div style="grid-column:1/-1;padding:40px;text-align:center;color:var(--ink-faint,#999);font:13px var(--sans,system-ui)">No designs in &ldquo;'+esc(activeCat||'(any)')+'&rdquo; yet.</div>'; return; }
+      rail.innerHTML = j.results.map(function(r){
+        var dot = r.dominant_hex ? '<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:'+esc(r.dominant_hex)+';border:1px solid rgba(0,0,0,.12);vertical-align:middle;margin-right:4px"></span>' : '';
+        return '<a href="'+esc(r.url)+'" style="display:block;text-decoration:none;color:inherit"><div style="aspect-ratio:1/1.15;background:#f4f1ea url('+JSON.stringify(r.image_url).slice(1,-1)+') center/cover;border-radius:6px;border:1px solid var(--line,#e6e2d8);margin-bottom:6px"></div><div style="font:13px var(--sans,system-ui);color:var(--ink,#111);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+esc(r.title || ('Design #'+r.id))+'</div><div style="font:11px var(--sans,system-ui);color:var(--ink-faint,#999);margin-top:2px">'+dot+esc(r.category||'mixed')+'</div></a>';
+      }).join('');
+    }).catch(function(){ rail.innerHTML = '<div style="grid-column:1/-1;padding:40px;text-align:center;color:var(--ink-faint,#999);font:13px var(--sans,system-ui)">Couldn\\'t load — try again.</div>'; });
+  }
+  if (catBar) {
+    catBar.addEventListener('click', function(e){
+      var b = e.target.closest('.home-cat-chip');
+      if (!b) return;
+      activeCat = b.dataset.cat || '';
+      try { localStorage.setItem('wallco-home-rail-cat', activeCat); } catch(e){}
+      paintChips();
+      load();
+    });
+  }
+  paintChips();
+  load();
+  if (btn) btn.addEventListener('click', load);
+})();
+</script>
+
+<!-- CTA band -->
+<section class="cta-band">
+  <h2>Every Design is an Original.</h2>
+  <p>No licensing. No vendor catalogs. Each pattern is generated — a wallcovering that exists nowhere else.</p>
+  <a href="/samples" class="btn-primary">Request a Free Sample</a>
+  <a href="/designs" class="btn-outline">Browse All Designs</a>
+</section>
+
+</main>
+${FOOTER}
+
+<script type="application/ld+json">${schema}</script>
+
+<!-- Hero rotation -->
+<script>
+(function(){
+  var slides = document.querySelectorAll('.hero-slide');
+  var dots   = document.querySelectorAll('.hero-dot');
+  var cur    = 0;
+  var interval;
+  function goTo(idx) {
+    slides[cur].classList.remove('active');
+    dots[cur].classList.remove('active');
+    cur = (idx + slides.length) % slides.length;
+    slides[cur].classList.add('active');
+    dots[cur].classList.add('active');
+  }
+  dots.forEach(function(d) {
+    d.addEventListener('click', function() { goTo(+this.dataset.slide); clearInterval(interval); interval = setInterval(function(){goTo(cur+1);}, 4500); });
+  });
+  interval = setInterval(function(){ goTo(cur+1); }, 4500);
+  // Pause when tab hidden
+  document.addEventListener('visibilitychange', function() {
+    if (document.hidden) clearInterval(interval);
+    else interval = setInterval(function(){goTo(cur+1);}, 4500);
+  });
+})();
+</script>
+${HAMBURGER_JS}
+<!-- Big Red avatar chat — floating launcher in lower-left -->
+<script src="${process.env.BIG_RED_URL || 'http://localhost:9935'}/widget.js"
+        data-host="${process.env.BIG_RED_URL || 'http://localhost:9935'}"
+        defer onerror="this.remove()"></script>
+</body>
+</html>`);
+});
+
+// ── Friendly category URLs — bookmarkable shortcuts to /designs?cat=...
+// Forwards any incoming query (sort/page/motif/hue) so deep links still work.
+app.get('/drunk-animals', (req, res) => {
+  const qs = new URLSearchParams({ cat: 'drunk-animals', ...req.query }).toString();
+  res.redirect(302, '/designs?' + qs);
+});
+app.get('/stoned-animals', (req, res) => {
+  const qs = new URLSearchParams({ cat: 'stoned-animals', ...req.query }).toString();
+  res.redirect(302, '/designs?' + qs);
+});
+app.get('/mural-animals', (req, res) => {
+  const qs = new URLSearchParams({ cat: 'mural-animals', ...req.query }).toString();
+  res.redirect(302, '/designs?' + qs);
+});
+app.get('/mural-scenic', (req, res) => {
+  const qs = new URLSearchParams({ cat: 'mural-scenic', ...req.query }).toString();
+  res.redirect(302, '/designs?' + qs);
+});
+
+// ── JSON feed for the /drunk-animals/live page — newest 60 designs with
+// just the fields the viewer needs (id/title/image_url/dominant_hex/prompt).
+app.get('/api/drunk-animals/feed', (req, res) => {
+  res.setHeader('Cache-Control', 'no-store');
+  const sinceId = parseInt(req.query.since_id || '0', 10) || 0;
+  const limit = Math.min(60, Math.max(1, parseInt(req.query.limit || '60', 10)));
+  const rows = DESIGNS
+    .filter(d => d.category === 'drunk-animals' && d.id > sinceId)
+    .sort((a, b) => b.id - a.id)
+    .slice(0, limit)
+    .map(d => ({
+      id: d.id,
+      title: d.title,
+      handle: d.handle,
+      image_url: d.image_url,
+      dominant_hex: d.dominant_hex,
+      created_at: d.created_at,
+      motifs: d.motifs || [],
+      url: `/design/${d.id}`,
+    }));
+  res.json({ count: rows.length, total: DESIGNS.filter(d => d.category === 'drunk-animals').length, designs: rows });
+});
+
+// ── /api/drunk-animals/best — highest-saturation N drunk-animal designs.
+// Powers the "Best of the night" rail pinned to the top of /live.
+app.get('/api/drunk-animals/best', (req, res) => {
+  res.setHeader('Cache-Control', 'no-store');
+  const n = Math.min(12, Math.max(1, parseInt(req.query.n || '3', 10)));
+  const best = DESIGNS
+    .filter(d => d.category === 'drunk-animals')
+    .sort((a, b) => (b.saturation || 0) - (a.saturation || 0))
+    .slice(0, n)
+    .map(d => ({
+      id: d.id, title: d.title, image_url: d.image_url,
+      dominant_hex: d.dominant_hex, saturation: d.saturation,
+      motifs: d.motifs || [], url: `/design/${d.id}`,
+    }));
+  res.json({ best });
+});
+
+// ── /api/drunk-animals/stats — derived snapshot for the live viewer's
+// "tonight in numbers" bar. Animals are extracted from motifs[] (when
+// llava tagged them) so the bar reflects what actually rendered, not
+// just what was prompted.
+app.get('/api/drunk-animals/stats', (_req, res) => {
+  res.setHeader('Cache-Control', 'no-store');
+  const all = DESIGNS.filter(d => d.category === 'drunk-animals');
+  const total = all.length;
+  if (total === 0) return res.json({ total: 0 });
+  const ANIMAL_TAGS = ['frog','elephant','giraffe','orangutan','capybara','lemur','sloth','macaw','panda','peacock','parrot','monkey'];
+  const animalCounts = {};
+  let satSum = 0, satCount = 0, earliest = null, latest = null;
+  for (const d of all) {
+    const motifs = (d.motifs || []);
+    for (const a of ANIMAL_TAGS) if (motifs.includes(a)) animalCounts[a] = (animalCounts[a] || 0) + 1;
+    if (typeof d.saturation === 'number') { satSum += d.saturation; satCount++; }
+    const t = Date.parse(d.created_at || '');
+    if (t) {
+      if (!earliest || t < earliest) earliest = t;
+      if (!latest || t > latest)   latest = t;
+    }
+  }
+  const animals = Object.entries(animalCounts).sort((a,b)=>b[1]-a[1]);
+  const elapsedMs = (earliest && latest) ? (latest - earliest) : 0;
+  const hoursElapsed = elapsedMs / 3_600_000;
+  const designsPerHour = hoursElapsed > 0.1 ? (total / hoursElapsed) : null;
+  // Is the generator currently in its active window? Tick script runs only
+  // 18:00→05:59 PT (STOP_HOUR=6). Outside that window, show 'complete'.
+  const STOP_HOUR = 6;
+  const nowPT = new Date(new Date().toLocaleString('en-US', { timeZone: 'America/Los_Angeles' }));
+  const h = nowPT.getHours();
+  const inWindow = (h >= 18) || (h < STOP_HOUR);
+  res.json({
+    total,
+    animals,
+    avg_saturation: satCount ? +(satSum/satCount).toFixed(3) : null,
+    earliest_at: earliest ? new Date(earliest).toISOString() : null,
+    latest_at:   latest   ? new Date(latest).toISOString()   : null,
+    designs_per_hour: designsPerHour ? +designsPerHour.toFixed(1) : null,
+    status: inWindow ? 'live' : 'complete',
+    hours_elapsed: +hoursElapsed.toFixed(1),
+  });
+});
+
+// ── /drunk-animals/live — auto-polling stream view of the overnight run.
+// New designs slide in at the top every 30s. Dark immersive theme matches
+// the collection's botanical-pop-Warhol aesthetic.
+app.get('/drunk-animals/live', (_req, res) => {
+  res.type('html').send(`<!doctype html>
+<html lang="en"><head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Drunk Animals · Live · wallco.ai</title>
+<style>
+:root { --bg:#0e0612; --fg:#fff; --pink:#ff79c6; --green:#50fa7b; --line:rgba(255,255,255,.12); }
+* { box-sizing:border-box }
+body { margin:0; background:var(--bg); color:var(--fg); font-family:-apple-system,BlinkMacSystemFont,'SF Pro Display','Segoe UI',Roboto,sans-serif; min-height:100vh }
+header { position:sticky; top:0; z-index:10; padding:14px 20px; background:linear-gradient(180deg,rgba(14,6,18,.96),rgba(14,6,18,.7)); backdrop-filter:blur(12px); border-bottom:1px solid var(--line); display:flex; align-items:center; gap:14px; flex-wrap:wrap }
+header h1 { font-family:'Playfair Display','Times New Roman',serif; font-weight:400; font-size:22px; margin:0; line-height:1 }
+header .meta { display:flex; gap:8px; align-items:center; font-size:12px; color:#e6d4f0 }
+header .pill { padding:3px 10px; border-radius:999px; border:1px solid var(--pink); color:var(--pink) }
+header .pill.green { border-color:var(--green); color:var(--green) }
+header .pulse::before { content:""; display:inline-block; width:7px; height:7px; background:var(--green); border-radius:50%; margin-right:6px; animation:pulse 1.6s ease-in-out infinite }
+@keyframes pulse { 0%,100%{opacity:.4} 50%{opacity:1} }
+header a { color:#e6d4f0; text-decoration:none; font-size:12px; padding:4px 10px; border:1px solid var(--line); border-radius:999px }
+header a:hover { border-color:var(--pink); color:var(--pink) }
+.stats-bar { padding:14px 20px; background:rgba(255,255,255,.02); border-bottom:1px solid var(--line); display:flex; gap:16px; flex-wrap:wrap; align-items:center; font-size:12px }
+.stats-bar .stat { display:flex; flex-direction:column; gap:2px; padding:0 14px; border-right:1px solid var(--line) }
+.stats-bar .stat:last-child { border-right:0 }
+.stats-bar .stat-label { font-size:10px; letter-spacing:.18em; text-transform:uppercase; color:#9586a3 }
+.stats-bar .stat-value { font-family:'Playfair Display',serif; font-size:18px; color:#fff }
+.stats-bar .animals { display:flex; gap:6px; flex-wrap:wrap; padding-left:14px }
+.stats-bar .animal-pill { padding:3px 10px; border-radius:999px; background:rgba(255,121,198,.12); color:var(--pink); font-size:11px; white-space:nowrap }
+.stats-bar .animal-pill strong { color:#fff; margin-left:4px; font-weight:600 }
+.best-rail { padding:24px 20px 8px; background:linear-gradient(180deg,rgba(255,121,198,.06),transparent) }
+.best-rail h2 { font-family:'Playfair Display',serif; font-weight:400; font-size:14px; margin:0 0 12px; letter-spacing:.2em; text-transform:uppercase; color:var(--pink) }
+.best-row { display:grid; grid-template-columns:repeat(auto-fit,minmax(280px,1fr)); gap:14px }
+.best-card { aspect-ratio:1/1; background-size:cover; background-position:center; border-radius:14px; position:relative; overflow:hidden; display:block; text-decoration:none; color:inherit; border:1px solid rgba(255,121,198,.25); transition:transform .25s ease, box-shadow .25s ease }
+.best-card:hover { transform:translateY(-3px); box-shadow:0 12px 28px rgba(255,121,198,.25) }
+.best-card .info { position:absolute; left:0; right:0; bottom:0; padding:10px 14px; background:linear-gradient(180deg,transparent,rgba(0,0,0,.7)); font-size:12px }
+.best-card .info .t { font-family:'Playfair Display',serif; font-size:16px; line-height:1.2; margin:0 }
+.best-card .info .id { font-size:10px; opacity:.7; letter-spacing:.08em; margin-top:2px }
+main { padding:20px; max-width:1500px; margin:0 auto }
+.grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(280px,1fr)); gap:18px }
+.card { background:#1a0f1a; border:1px solid var(--line); border-radius:14px; overflow:hidden; position:relative; opacity:0; transform:translateY(-12px); animation:slidein .5s ease forwards }
+@keyframes slidein { to { opacity:1; transform:translateY(0) } }
+.card a { display:block; text-decoration:none; color:inherit }
+.card .img { aspect-ratio:1/1; background-size:cover; background-position:center }
+.card .meta { padding:12px 14px }
+.card .meta .title { font-family:'Playfair Display',serif; font-size:17px; line-height:1.2; margin:0 0 6px }
+.card .meta .id { font-size:11px; opacity:.55; letter-spacing:.06em }
+.card .dot { display:inline-block; width:10px; height:10px; border-radius:50%; vertical-align:middle; margin-left:6px; border:1px solid rgba(255,255,255,.2) }
+.card .motifs { display:flex; flex-wrap:wrap; gap:4px; margin-top:8px }
+.card .motifs span { font-size:10px; padding:2px 7px; background:rgba(255,121,198,.15); color:var(--pink); border-radius:999px; text-transform:lowercase }
+.card.new { box-shadow:0 0 0 2px var(--green), 0 8px 28px rgba(80,250,123,.25) }
+.empty { text-align:center; padding:80px 20px; opacity:.5 }
+</style>
+</head><body>
+<header>
+  <h1>The Drunk Animals Collection</h1>
+  <span class="meta pulse" id="status-pill">live</span>
+  <span class="pill" id="count">0 designs</span>
+  <span class="pill green" id="next">next in —</span>
+  <a href="/drunk-animals">Grid view</a>
+  <a href="/">wallco.ai</a>
+</header>
+<div class="stats-bar" id="statsbar">
+  <div class="stat"><span class="stat-label">Total</span><span class="stat-value" id="s-total">—</span></div>
+  <div class="stat"><span class="stat-label">Per hour</span><span class="stat-value" id="s-rate">—</span></div>
+  <div class="stat"><span class="stat-label">Avg saturation</span><span class="stat-value" id="s-sat">—</span></div>
+  <div class="animals" id="s-animals"></div>
+</div>
+<section class="best-rail" id="best-rail" style="display:none">
+  <h2>Best of the night · most saturated 3</h2>
+  <div class="best-row" id="best-row"></div>
+</section>
+<main>
+  <div class="grid" id="grid"></div>
+  <div class="empty" id="empty">Loading the menagerie…</div>
+</main>
+<script>
+const grid = document.getElementById('grid');
+const empty = document.getElementById('empty');
+const countEl = document.getElementById('count');
+const nextEl = document.getElementById('next');
+let maxId = 0;
+let lastTickAt = Date.now();
+
+function card(d, isNew) {
+  const motifs = (d.motifs || []).slice(0, 6).map(m => '<span>' + m + '</span>').join('');
+  return \`<article class="card\${isNew?' new':''}">
+    <a href="\${d.url}" target="_blank" rel="noopener">
+      <div class="img" style="background-image:url('\${d.image_url}')"></div>
+      <div class="meta">
+        <p class="title">\${d.title}<span class="dot" style="background:\${d.dominant_hex||'#888'}"></span></p>
+        <p class="id">#\${d.id} · \${(d.created_at||'').slice(11,16)} UTC</p>
+        <div class="motifs">\${motifs}</div>
+      </div>
+    </a>
+  </article>\`;
+}
+
+async function poll() {
+  try {
+    const r = await fetch('/api/drunk-animals/feed?since_id=' + maxId);
+    const j = await r.json();
+    countEl.textContent = j.total + ' design' + (j.total === 1 ? '' : 's');
+    if (j.designs.length === 0 && maxId === 0) {
+      empty.textContent = 'No designs yet — generator just started';
+      return;
+    }
+    empty.style.display = 'none';
+    // Prepend new
+    if (j.designs.length) {
+      const isFirstLoad = maxId === 0;
+      const fragHtml = j.designs.map(d => card(d, !isFirstLoad)).join('');
+      grid.insertAdjacentHTML('afterbegin', fragHtml);
+      maxId = Math.max(maxId, ...j.designs.map(d => d.id));
+      if (!isFirstLoad) lastTickAt = Date.now();
+    }
+  } catch (e) {
+    console.error('feed poll failed', e);
+  }
+}
+
+function updateNext() {
+  // We expect a new design every 4 min from the daemon. Show countdown.
+  const ms = 4*60*1000 - (Date.now() - lastTickAt);
+  if (ms <= 0) { nextEl.textContent = 'next any moment'; return; }
+  const m = Math.floor(ms / 60000), s = Math.floor((ms % 60000) / 1000);
+  nextEl.textContent = 'next in ' + m + ':' + String(s).padStart(2,'0');
+}
+
+async function pollBest() {
+  try {
+    const r = await fetch('/api/drunk-animals/best?n=3');
+    const j = await r.json();
+    if (!j.best || !j.best.length) return;
+    const rail = document.getElementById('best-rail');
+    const row  = document.getElementById('best-row');
+    row.innerHTML = j.best.map(d => \`<a class="best-card" href="\${d.url}" target="_blank" rel="noopener" style="background-image:url('\${d.image_url}')">
+      <div class="info"><p class="t">\${d.title}</p><p class="id">#\${d.id} · sat \${(d.saturation||0).toFixed(2)}</p></div>
+    </a>\`).join('');
+    rail.style.display = 'block';
+  } catch (e) { console.warn('best poll failed', e); }
+}
+
+async function pollStats() {
+  try {
+    const r = await fetch('/api/drunk-animals/stats');
+    const j = await r.json();
+    if (!j.total) return;
+    document.getElementById('s-total').textContent = j.total;
+    document.getElementById('s-rate').textContent  = j.designs_per_hour != null ? j.designs_per_hour.toFixed(1) : '—';
+    document.getElementById('s-sat').textContent   = j.avg_saturation   != null ? j.avg_saturation.toFixed(2)  : '—';
+    const ani = document.getElementById('s-animals');
+    ani.innerHTML = (j.animals || []).slice(0, 8)
+      .map(([name, n]) => '<span class="animal-pill">' + name + '<strong>' + n + '</strong></span>').join('');
+    // Status pill: live (overnight window) vs complete (daylight quiet hours).
+    const pill = document.getElementById('status-pill');
+    const next = document.getElementById('next');
+    if (j.status === 'complete') {
+      pill.textContent = 'complete · ' + (j.hours_elapsed||0).toFixed(1) + 'h run';
+      pill.classList.remove('pulse');
+      pill.style.color = '#50fa7b';
+      next.style.display = 'none';
+    } else {
+      pill.textContent = 'live';
+      pill.classList.add('pulse');
+      pill.style.color = '';
+      next.style.display = '';
+    }
+  } catch (e) { console.warn('stats poll failed', e); }
+}
+
+poll();
+pollStats();
+pollBest();
+setInterval(poll, 30_000);
+setInterval(pollStats, 60_000);
+setInterval(pollBest, 120_000);
+setInterval(updateNext, 1000);
+</script>
+</body></html>`);
+});
+
+// ── DESIGNS CATALOG (full grid with sort + density + category filter)
+// ── /designs role gate (2026-05-20) — login required to view ──
+// Steve directive: "require login https://wallco.ai/designs admin/user/designer"
+// Three roles, any one grants access. Admin path is the existing dw_auth JWT
+// cookie. User + designer roles set a `dw_role` cookie via /login.
+function readRole(req) {
+  // 1) Admin path — dw_auth JWT (existing)
+  try { if (isAdmin(req)) return 'admin'; } catch (_) {}
+  // 2) Lightweight role cookie (user / designer)
+  const raw = (req.headers && req.headers.cookie) || '';
+  for (const part of String(raw).split(/;\s*/)) {
+    const eq = part.indexOf('=');
+    if (eq > -1 && part.slice(0, eq) === 'dw_role') {
+      const v = decodeURIComponent(part.slice(eq + 1));
+      if (v === 'user' || v === 'designer' || v === 'admin') return v;
+    }
+  }
+  return null;
+}
+
+// /login — 3-role landing page. User/designer paths set dw_role cookie and
+// redirect back to /designs (or whatever ?next= specifies).
+app.get('/login', (req, res) => {
+  const next = String(req.query.next || '/designs').replace(/[^\w/?=&.\-]/g, '');
+  res.type('html').send(`<!doctype html>
+<html><head><meta charset="utf-8"><title>Wallco · Sign In</title>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<style>
+  :root{--ink:#3a2818;--bg:#faf6ec;--accent:#c9a14b;--line:#d8cdb0;--soft:#8a7c66}
+  *{box-sizing:border-box}body{margin:0;padding:0;font:14px/1.5 ui-sans-serif,system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--ink);min-height:100vh;display:flex;align-items:center;justify-content:center}
+  .card{max-width:420px;width:90%;background:#fff;border:1px solid var(--line);border-radius:8px;padding:36px 32px;box-shadow:0 8px 24px rgba(60,40,20,.08)}
+  h1{margin:0 0 6px;font:300 32px/1.1 'Bodoni Moda',Georgia,serif;letter-spacing:.02em}
+  .sub{color:var(--soft);font-size:13px;margin-bottom:28px}
+  .row{display:flex;flex-direction:column;gap:10px;margin-top:12px}
+  button{appearance:none;padding:14px 18px;border:1px solid var(--line);background:#fff;color:var(--ink);font:600 13px/1 ui-sans-serif,system-ui;letter-spacing:.06em;text-transform:uppercase;border-radius:5px;cursor:pointer;transition:all .15s}
+  button:hover{border-color:var(--ink);background:var(--bg)}
+  button.primary{background:var(--ink);color:#fff;border-color:var(--ink)}
+  button.primary:hover{background:#0d0c0a}
+  button.accent{background:var(--accent);color:#fff;border-color:var(--accent)}
+  button.accent:hover{background:#a8843d}
+  .hint{color:var(--soft);font-size:11px;letter-spacing:.04em;margin-top:18px;text-align:center}
+  .role-label{font-size:10px;color:var(--soft);letter-spacing:.14em;text-transform:uppercase;margin-bottom:4px}
+  form{margin:0}
+</style></head>
+<body>
+<div class="card">
+  <h1>Wallco.ai</h1>
+  <div class="sub">Choose how you're accessing the design library.</div>
+  <div class="row">
+    <form method="POST" action="/login"><input type="hidden" name="role" value="designer"><input type="hidden" name="next" value="${next}"><button class="accent" type="submit" style="width:100%">★ Sign in as Designer / Trade</button></form>
+    <form method="POST" action="/login"><input type="hidden" name="role" value="user"><input type="hidden" name="next" value="${next}"><button type="submit" style="width:100%">Sign in as Customer</button></form>
+    <form method="POST" action="/login"><input type="hidden" name="role" value="admin"><input type="hidden" name="next" value="${next}"><button class="primary" type="submit" style="width:100%">Admin Login</button></form>
+  </div>
+  <div class="hint">Designer + Customer access is open during preview. Admin login routes through /_devlogin.</div>
+</div>
+</body></html>`);
+});
+app.post('/login', express.urlencoded({ extended: false, limit: '2kb' }), (req, res) => {
+  const role = String(req.body?.role || '').trim();
+  const next = String(req.body?.next || '/designs').replace(/[^\w/?=&.\-]/g, '');
+  if (role === 'admin') {
+    // Bounce to existing dev-login for real JWT mint
+    return res.redirect(`/_devlogin?next=${encodeURIComponent(next)}`);
+  }
+  if (role === 'user' || role === 'designer') {
+    res.cookie('dw_role', role, { httpOnly: false, maxAge: 30 * 24 * 3600 * 1000, sameSite: 'lax', path: '/' });
+    return res.redirect(next);
+  }
+  res.redirect('/login');
+});
+
+app.get('/designs', (req, res) => {
+  // 2026-05-20 Steve directive: NO hard server gate. Allow 1 page browsing,
+  // then a client-side scroll-triggered modal fades in requiring login.
+  // The readRole(req) result is still passed to the page (as window._dwRole)
+  // so the JS knows whether to suppress the modal for already-logged-in users.
+  const _role = readRole(req);
+  // Conditional GET: emit Last-Modified from the freshest design in the
+  // in-memory catalog. Crawlers issuing If-Modified-Since get a 304 when
+  // no new design has landed since their last crawl.
+  if (maybe304(req, res, CATALOG_LAST_MODIFIED)) return;
+  const sort = req.query.sort || 'newest';
+  const cat  = req.query.cat  || '';
+  const q    = (req.query.q   || '').toLowerCase().trim();
+  const page = Math.max(1, parseInt(req.query.page || '1', 10));
+  const PER  = 60;
+  // Admin/review-mode flag: only enabled at 127.0.0.1 / localhost / ?review=1 — public visits unchanged.
+  // Live-view override: an authenticated admin can preview the page as a
+  // customer by appending ?view=live OR by setting view_mode=live cookie via
+  // the corner-bottom toggle. `_isAdminAuth` retains the real auth state so
+  // the toggle button itself only renders for admins, but `_isAdmin` (used
+  // by every render decision below) flips to false while previewing.
+  const _isAdminAuth = isAdmin(req);
+  // Manual cookie read — cookie-parser isn't in deps so req.cookies is undefined.
+  const _viewModeCookie = (function(){
+    const raw = (req.headers && req.headers.cookie) || '';
+    for (const part of String(raw).split(/;\s*/)) {
+      const eq = part.indexOf('=');
+      if (eq > -1 && part.slice(0, eq) === 'view_mode') return decodeURIComponent(part.slice(eq + 1));
+    }
+    return '';
+  })();
+  const _viewAsLive = req.query.view === 'live' || _viewModeCookie === 'live';
+  const _isAdmin = _isAdminAuth && !_viewAsLive;
+
+  const motifQ = (req.query.motif || '').toLowerCase().trim();
+  const hueQ   = (req.query.hue   || '').toLowerCase().trim();
+  const roomQ  = (req.query.room  || '').toLowerCase().trim();  // '1' or 'all' = only designs with room previews
+  function hueBucketOf(hex) {
+    if (!hex || !/^#[0-9a-fA-F]{6}$/.test(hex)) return null;
+    const r = parseInt(hex.slice(1,3),16)/255, g = parseInt(hex.slice(3,5),16)/255, b = parseInt(hex.slice(5,7),16)/255;
+    const max = Math.max(r,g,b), min = Math.min(r,g,b);
+    if (max === min) return 'neutral';
+    let h = 0; const 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 = (h * 60 + 360) % 360;
+    const l = (max + min) / 2;
+    const s = (max - min) / (l > 0.5 ? 2 - max - min : max + min);
+    if (s < 0.1) return 'neutral';
+    if (h < 15 || h >= 345) return 'rose';
+    if (h < 40)  return 'amber';
+    if (h < 60)  return 'honey';
+    if (h < 90)  return 'olive';
+    if (h < 160) return 'sage';
+    if (h < 200) return 'marine';
+    if (h < 250) return 'sapphire';
+    if (h < 290) return 'mauve';
+    return 'plum';
+  }
+  // Hard filter — never render a card whose image file isn't on disk OR whose
+  // id is in the blank-guard. /api/designs already does this; mirror it on the
+  // HTML page so missing-file rows don't paint empty cards that 404 their
+  // background-image url(). (data/generated/ is in deploy exclude list, so any
+  // var_NNNN_* PNG generated locally but not rsynced is 404 on prod.)
+  // ALSO drop any design whose category matches a DW vendor name (defined
+  // a few lines below). Same Steve directive — vendor-named cards never go
+  // to public /designs (live on /library admin-only instead).
+  const _VENDOR_DENY = /\b(thibaut|koroseal|schumacher|maya[ -]romanoff|scalamandre|arte[ -]international|cole[ -]?(and|&)[ -]?son|phillip[ -]jeffries|osborne[ -]?(and|&)?[ -]?little|de[ -]gournay|fromental|sister[ -]parish|dedar|brewster|mind[ -]the[ -]gap|hygge|designtex|wolf[ -]gordon|carnegie|coordonn|daisy[ -]bennett|china[ -]seas|york|marburg|romo|fabricut|stout|knoll|arteriors|timorous[ -]beasties|hangups)\b/i;
+  let filtered = DESIGNS.filter(d => designHasImage(d) && !_VENDOR_DENY.test(String(d.category || '')));
+  if (cat)    filtered = filtered.filter(d => d.category === cat);
+  if (motifQ) filtered = filtered.filter(d => (d.motifs || []).includes(motifQ));
+  if (hueQ)   filtered = filtered.filter(d => hueBucketOf(d.dominant_hex) === hueQ);
+  if (roomQ)  filtered = filtered.filter(d => (d.room_mockups || []).length > 0);
+  // Search-alias expansion — typing "bh" / "beverly hills" / "bh90210"
+  // still resolves the LA collection (and vice-versa). Group-of-synonyms:
+  // any token in the same row matches any other token in that row.
+  // Steve directive 2026-05-13: rename to LA, keep BH searchable via meta.
+  const SEARCH_ALIASES = [
+    ['bh', 'bh90210', 'beverly hills', 'beverlyhills', 'beverly-hills',
+     'la', 'la90210', 'los angeles', 'los-angeles', 'la collection', 'la-collection'],
+  ];
+  function expandQ(qstr) {
+    const lowered = String(qstr).toLowerCase();
+    for (const group of SEARCH_ALIASES) {
+      if (group.some(token => lowered.includes(token))) return group;
+    }
+    return [lowered];
+  }
+  if (q) {
+    const queries = expandQ(q);
+    filtered = filtered.filter(d => queries.some(qq =>
+      d.title.toLowerCase().includes(qq) ||
+      d.category.toLowerCase().includes(qq) ||
+      d.handle.toLowerCase().includes(qq) ||
+      (d.motifs || []).some(m => m.toLowerCase().includes(qq)) ||
+      (d.tags || []).some(t => t.toLowerCase().includes(qq))
+    ));
+  }
+  filtered = sortDesigns(filtered, sort);
+
+  const total = filtered.length;
+  const pages = Math.max(1, Math.ceil(total / PER));
+  const slice = filtered.slice((page-1)*PER, page*PER);
+  // Defense-in-depth: NEVER include DW vendor names in the category dropdown.
+  // Steve directive 2026-05-20 — vendor names (Thibaut/Koroseal/Fromental/etc.)
+  // leaked into the dropdown via legacy dw-shopify imports + an AI-gen track
+  // that had a vendor-named slug. PG was bulk-renamed and night-builder's track
+  // string was scrubbed, but this regex is the failsafe so a future drift can't
+  // re-leak. Pair with the bulk PG rename if a new vendor name appears.
+  const VENDOR_DENYLIST = /\b(thibaut|koroseal|schumacher|maya[ -]romanoff|scalamandre|arte[ -]international|cole[ -]?(and|&)[ -]?son|phillip[ -]jeffries|osborne[ -]?(and|&)?[ -]?little|de[ -]gournay|fromental|sister[ -]parish|dedar|brewster|mind[ -]the[ -]gap|hygge|designtex|wolf[ -]gordon|carnegie|coordonn|daisy[ -]bennett|china[ -]seas|york|marburg|romo|fabricut|stout|knoll|arteriors|timorous[ -]beasties|hangups)\b/i;
+  const cats  = [...new Set(DESIGNS.map(d => d.category))]
+    .filter(c => c && !VENDOR_DENYLIST.test(c))
+    .sort();
+
+  // Top motifs across full catalog (not filtered)
+  const motifCounts = {};
+  DESIGNS.forEach(d => (d.motifs || []).forEach(m => { motifCounts[m] = (motifCounts[m]||0) + 1; }));
+  const topMotifs = Object.entries(motifCounts).sort((a,b)=>b[1]-a[1]).slice(0,12);
+
+  const HUE_DEFS = [
+    ['rose',     '#e08291'], ['amber',   '#c97b3a'], ['honey',   '#d2a44a'],
+    ['olive',    '#8a8c4a'], ['sage',    '#8aa17a'], ['marine',  '#6aa6b6'],
+    ['sapphire', '#5a82a8'], ['mauve',   '#a08bbf'], ['plum',    '#8e5887'],
+    ['neutral',  '#b8b3a8']
+  ];
+  const hueCounts = {};
+  DESIGNS.forEach(d => { const b = hueBucketOf(d.dominant_hex); if (b) hueCounts[b] = (hueCounts[b]||0)+1; });
+
+  const cards = slice.map(d => {
+    if (!_isAdmin) {
+      const roomKey = (d.room_mockups || []).includes('living_room') ? 'living_room'
+                    : (d.room_mockups || []).includes('bedroom')     ? 'bedroom'
+                    : (d.room_mockups || []).includes('office')      ? 'office'
+                    : (d.room_mockups || [])[0];
+      const roomCount = (d.room_mockups || []).length;
+      const badgeText = roomCount > 1 ? `${roomCount} rooms` : (roomCount === 1 ? 'In a room' : '');
+      const roomOverlay = roomKey
+        ? `<img class="card-room" src="/designs/room/design_${d.id}_${roomKey}.png" loading="lazy" alt="" aria-hidden="true"><span class="card-room-badge">${badgeText}</span>`
+        : '';
+      // NEW badge — fires for designs created within the last NEW_BADGE_HOURS.
+      // Was 72h; tightened to 6h on 2026-05-14 after observing every card glowed
+      // gold (6010/6010 designs <72h old after recent generator burst). At 6h
+      // the cohort drops to ~450 designs (about 1 page worth of "actually new"),
+      // making the ring a real signal again. Env-overridable for tuning.
+      const NEW_BADGE_HOURS = parseFloat(process.env.NEW_BADGE_HOURS || '6');
+      const createdMs = Date.parse(d.created_at || '') || 0;
+      const isNew = createdMs && (Date.now() - createdMs < NEW_BADGE_HOURS * 3600 * 1000);
+      const newBadge = isNew ? `<span class="card-new-badge" aria-label="Newly added">NEW</span>` : '';
+      // DW-style SKU — derived from the design id with a stable two-letter prefix
+      // for the wallco.ai sub-label (WC). Designer Wallcoverings catalog convention
+      // is DW-<vendor-code>-<numeric>, so wallco.ai pieces become DW-WC-####.
+      const sku = `DW-WC-${String(d.id).padStart(4, '0')}`;
+      // Color name — interior-designer-skill style: derive a poetic color name
+      // from the dominant_hex by binning into the same hue buckets the page
+      // filter uses, then prefer the leading word of the title (Honey/Amber/
+      // Rose/Sage/…) when the generator already gave us one — that's the
+      // canonical color family per the prompt library. Falls back to the
+      // hue bucket if the title doesn't lead with one.
+      const colorNameFromTitle = (String(d.title || '').match(/^(Honey|Amber|Rose|Sage|Olive|Marine|Sapphire|Mauve|Plum|Neutral|Bone|Champagne|Ivory|Emerald|Navy|Coral|Blush|Indigo|Charcoal)/i) || [])[1];
+      const colorName = colorNameFromTitle || (hueBucketOf(d.dominant_hex) || 'neutral').replace(/^./, c => c.toUpperCase());
+      // Style tag — category is the closest thing to a style category in the
+      // data model (damask, butterfly-trellis, stoned-animals, mural-scenic).
+      const styleTag = String(d.category || 'mixed').replace(/-/g, ' ');
+      // DW Original Number (DIG_AI_xxxxxx) — auto-assigned at DB INSERT
+      // time by trg_spoon_all_designs_assign_dig so it's always present.
+      // The fallback expression matches the trigger's exact output so we
+      // can show it on the card without re-querying PG for every row.
+      const digNum = d.dig_number || `DIG_AI_${String(d.id).padStart(6, '0')}`;
+      // Wrapper is a <div> (not <a>) because a <button> + block <div> inside
+      // <a> trips the HTML5 parser's Adoption Agency Algorithm and splits the
+      // anchor into multiple sibling top-level nodes — which then become
+      // SEPARATE grid items, producing the alternating-blank-tile bug.
+      // Keep the anchor as an inner link wrapping just the visual; controls
+      // (fav, NEW, meta) sit alongside as siblings of the link.
+      // card-hover is a SIBLING of card-link (not nested inside) so the inner
+      // Sample <a> doesn't trigger the same Adoption Agency split that took
+      // out the button-in-anchor pattern (2026-05-14). card-hover positions
+      // absolute over the .design-card; clicks on its own anchors navigate
+      // directly without bubbling to card-link.
+      return `
+    <div class="design-card${roomKey ? ' has-room' : ''}${isNew ? ' is-new' : ''}" data-design-id="${d.id}" data-dig="${digNum}">
+      <button type="button" class="fav-btn" data-fav-id="${d.id}" aria-pressed="false" aria-label="Save ${d.title} to favorites" title="Favorite"><span class="fav-icon"></span></button>
+      ${newBadge}
+      <a href="/design/${d.id}" class="card-link" aria-label="${cardAriaLabel(d)}">
+        <div class="card-img" style="background-image:url('${d.image_url}')" role="img" aria-label="${d.title} pattern thumbnail">${roomOverlay}</div>
+        <div class="card-meta">
+          <span class="card-title">${d.title}</span>
+          <span class="card-sku" title="DW catalog SKU">${sku}</span>
+          <span class="card-colorname">${colorName} <span class="color-dot" style="background:${d.dominant_hex}" aria-hidden="true"></span><span class="card-hex">${(d.dominant_hex || '').toUpperCase()}</span></span>
+          <span class="card-tags">
+            <span class="card-ai-badge" title="AI-designed pattern">✦ AI-Designed</span>
+            <span class="card-style">${styleTag}</span>
+          </span>
+        </div>
+      </a>
+      <div class="card-hover" aria-hidden="true">
+        <span class="card-hover-dig" title="DW Original Number">${digNum}</span>
+        <div class="card-hover-actions">
+          <a class="chov-btn chov-view" href="/design/${d.id}">View</a>
+          <a class="chov-btn chov-sample" href="/samples?design=${d.id}">Sample</a>
+        </div>
+      </div>
+    </div>`;
+    }
+    // Admin review-mode card: sliders, K/R, chips, why-chip
+    // Show the DW Original Number (DIG_AI_xxxxxx) as the pattern pill so it
+    // matches the buyer view + the Shopify SKU written by /upload-shopify.
+    const _digAdm = d.dig_number || `DIG_AI_${String(d.id).padStart(6, '0')}`;
+    return `
+    <div class="design-card review-card" data-id="${d.id}" data-dig="${_digAdm}">
+      <a href="/design/${d.id}" target="_blank" class="card-img" style="background-image:url('${d.image_url}')" rel="noopener noreferrer">
+        <span class="decision-badge"></span>
+        <div class="card-hover-actions admin-hover-actions" aria-hidden="true">
+          <a class="chov-btn chov-view" href="/design/${d.id}" target="_blank" rel="noopener noreferrer" onclick="event.stopPropagation()">View</a>
+          <a class="chov-btn chov-sample" href="/samples?design=${d.id}" target="_blank" rel="noopener noreferrer" onclick="event.stopPropagation()">Sample</a>
+        </div>
+      </a>
+      <span class="pattern-num" aria-hidden="true">${_digAdm}</span>
+      <div class="review-flyout">
+        <div class="card-meta">
+          <span class="card-title">${d.title}</span>
+          <span class="card-cat">${d.category}</span>
+          <span class="color-dot" style="background:${d.dominant_hex}"></span>
+        </div>
+        <div class="review-controls">
+          <div class="sliders">
+            <label>Design <input type="range" min="0" max="10" value="5" data-axis="design"><span>5</span></label>
+            <label>Color  <input type="range" min="0" max="10" value="5" data-axis="color"><span>5</span></label>
+            <label>Style  <input type="range" min="0" max="10" value="5" data-axis="style"><span>5</span></label>
+          </div>
+          <div class="decision">
+            <button class="btn-keep" type="button">Keep</button>
+            <button class="btn-reject" type="button">Reject</button>
+            <button class="btn-pair" type="button" title="Suggest combinations">Pair</button>
+          </div>
+          <div class="why-chip" title="Click to chat about this verdict"></div>
+          <div class="chips"></div>
+        </div>
+      </div>
+    </div>`;
+  }).join('');
+
+  const pagerLinks = pages > 1
+    ? Array.from({length: pages}, (_,i) => i+1).map(p =>
+        `<a href="/designs?sort=${sort}&cat=${encodeURIComponent(cat)}&q=${encodeURIComponent(q)}&page=${p}" class="pager-link ${p===page?'active':''}">${p}</a>`
+      ).join(' ')
+    : '';
+
+  // Each ListItem embeds a full Product object so Google can render
+  // list-of-products rich results (not just bare URLs). Mirrors the
+  // per-design Product schema from /design/:id but trimmed to grid-card
+  // essentials: name, image, sku, category, brand, offers.
+  const _hueOf = (hex) => {
+    const h = (hex || '').toLowerCase();
+    if (!/^#[0-9a-f]{6}$/.test(h)) return '';
+    const r=parseInt(h.slice(1,3),16), g=parseInt(h.slice(3,5),16), b=parseInt(h.slice(5,7),16);
+    const max=Math.max(r,g,b), min=Math.min(r,g,b), s=max?(max-min)/max:0;
+    if (s<0.12) return 'neutral';
+    const hh = ((max===r ? (g-b)/(max-min)+(g<b?6:0) : max===g ? (b-r)/(max-min)+2 : (r-g)/(max-min)+4) * 60 + 360) % 360;
+    if (hh<15||hh>=345) return 'rose'; if (hh<40) return 'amber'; if (hh<60) return 'honey';
+    if (hh<90) return 'olive';  if (hh<160) return 'sage';  if (hh<200) return 'marine';
+    if (hh<250) return 'sapphire'; if (hh<290) return 'mauve'; return 'plum';
+  };
+
+  const itemList = {
+    "@context": "https://schema.org",
+    "@type": "ItemList",
+    "name": "Original Wallpaper Designs — wallco.ai",
+    "url": `https://wallco.ai/designs${page > 1 ? `?page=${page}` : ''}`,
+    "numberOfItems": total,
+    "itemListOrder": "https://schema.org/ItemListOrderDescending",
+    "itemListElement": slice.map((d,i) => ({
+      "@type": "ListItem",
+      "position": (page-1)*PER+i+1,
+      "item": {
+        "@type": "Product",
+        "@id": `https://wallco.ai/design/${d.id}`,
+        "name": d.title,
+        "url": `https://wallco.ai/design/${d.id}`,
+        "image": `https://wallco.ai${d.image_url}`,
+        "sku": d.handle || `wallco-${String(d.id).padStart(4,'0')}`,
+        "productID": String(d.id),
+        "category": d.category,
+        "color": _hueOf(d.dominant_hex) || undefined,
+        "material": "Wallpaper",
+        "brand": { "@type": "Brand", "name": "wallco.ai" },
+        "offers": {
+          "@type": "Offer",
+          "availability": "https://schema.org/InStock",
+          "url": `https://wallco.ai/samples?design=${d.id}`,
+          "priceCurrency": "USD",
+          "description": "Free sample available"
+        }
+      }
+    }))
+  };
+
+  const breadcrumb = {
+    "@context": "https://schema.org",
+    "@type": "BreadcrumbList",
+    "itemListElement": [
+      { "@type": "ListItem", "position": 1, "name": "Home", "item": "https://wallco.ai/" },
+      { "@type": "ListItem", "position": 2, "name": "Designs", "item": "https://wallco.ai/designs" }
+    ].concat(cat ? [{ "@type": "ListItem", "position": 3, "name": cat, "item": `https://wallco.ai/designs?cat=${encodeURIComponent(cat)}` }] : [])
+  };
+
+  const schema = `${JSON.stringify(itemList)}</script>\n<script type="application/ld+json">${JSON.stringify(breadcrumb)}`;
+
+  // Title reflects active filter so browser-tab + search results read true.
+  // Composes parts in priority order: query > hue > motif > cat > room. Caps
+  // at the first non-empty crumb to keep the title scannable (≤60 chars).
+  const _titleCrumbs = [
+    q       && `"${q}"`,
+    hueQ    && `${hueQ.charAt(0).toUpperCase() + hueQ.slice(1)} designs`,
+    motifQ  && `${motifQ.charAt(0).toUpperCase() + motifQ.slice(1)} motif`,
+    cat     && `${cat.charAt(0).toUpperCase() + cat.slice(1)}`,
+    roomQ   && 'In-room previews'
+  ].filter(Boolean);
+  const _titleHead = _titleCrumbs.length
+    ? `${_titleCrumbs[0]} · ${total} design${total===1?'':'s'} — wallco.ai`
+    : `All Designs — wallco.ai | AI-Original Wallpaper & Murals`;
+  // When the user lands on a filtered grid (?hue=rose, ?motif=floral, ?cat=…),
+  // share-link previews on Twitter/LinkedIn/Slack should show a REAL design
+  // from that filter, not the generic og-default.png. Picks the first design
+  // in the filtered slice; unfiltered /designs keeps og-default.
+  const _isFiltered = !!(cat || motifQ || hueQ || roomQ || q);
+  const _ogImage = (_isFiltered && slice[0]?.image_url)
+    ? `https://wallco.ai${slice[0].image_url}`
+    : '';
+  res.type('html').send(`${htmlHead({
+    title: _titleHead,
+    description: `Browse ${total} wallco.ai original wallpaper designs. Every pattern unique, hand-curated, never repeated. Free samples available.`,
+    canonical: `https://wallco.ai/designs`,
+    ogImage: _ogImage,
+    ogImageAlt: _isFiltered && slice[0] ? `${slice[0].title} — sample of ${_titleCrumbs[0] || 'wallco.ai'} designs` : '',
+    preloadImages: slice.slice(0, 6).map(d => d.image_url).filter(Boolean)
+  })}
+<body>
+${htmlHeader('/designs')}
+<main>
+${cat === 'drunk-animals' ? `
+<section style="padding:48px 24px 24px;text-align:center;background:linear-gradient(180deg,#1a0f1a 0%,#2a1530 100%);color:#fff;margin-bottom:0">
+  <div style="max-width:760px;margin:0 auto">
+    <p style="font-size:11px;letter-spacing:.28em;text-transform:uppercase;color:#ff79c6;margin:0 0 8px;font-weight:600">Limited capsule · v2 redo · floral</p>
+    <h1 style="font-family:'Playfair Display',serif;font-weight:400;font-size:clamp(34px,5vw,56px);line-height:1.05;margin:0 0 14px;color:#fff">The Drunk Animals Collection</h1>
+    <p style="font-size:15px;line-height:1.6;color:#e6d4f0;margin:0 auto;max-width:560px">A floral-pop-Warhol fever dream. Tipsy bears with cobalt vodka, hiccup giraffes with emerald lager, sloshed orangutans toasting from rosé coupes — set in fields of wisteria, sunflower, bougainvillea, ghost orchid, bleeding heart, chocolate cosmos. Strictly no leaves, no foliage.</p>
+    <div style="margin-top:18px;display:flex;gap:10px;justify-content:center;flex-wrap:wrap;font-size:12px">
+      <span style="padding:4px 10px;border:1px solid #ff79c6;border-radius:14px;color:#ff79c6"><strong>${total}</strong> designs &middot; one every 4 min</span>
+      <span style="padding:4px 10px;border:1px solid #50fa7b;border-radius:14px;color:#50fa7b">live until 06:00 PT</span>
+    </div>
+  </div>
+</section>
+` : ''}
+${cat === 'stoned-animals' ? `
+<section style="padding:48px 24px 24px;text-align:center;background:linear-gradient(180deg,#0a1810 0%,#1a2a14 60%,#3a1830 100%);color:#fff;margin-bottom:0">
+  <div style="max-width:760px;margin:0 auto">
+    <p style="font-size:11px;letter-spacing:.28em;text-transform:uppercase;color:#ffeb3b;margin:0 0 8px;font-weight:600">Limited capsule · 4·20 Edition</p>
+    <h1 style="font-family:'Playfair Display',serif;font-weight:400;font-size:clamp(34px,5vw,56px);line-height:1.05;margin:0 0 14px;color:#fff">The Stoned Animals Collection</h1>
+    <p style="font-size:15px;line-height:1.6;color:#dfe4d0;margin:0 auto;max-width:560px">Grateful Dead meets Warhol in a woodland fog. Bloodshot bears, hazy wolves, glassy-eyed owls reclining on mushroom caps with bongs, gummies, and joints. Strictly NO palms — ferns, wildflowers, herbs, mosses only.</p>
+    <div style="margin-top:18px;display:flex;gap:10px;justify-content:center;flex-wrap:wrap;font-size:12px">
+      <span style="padding:4px 10px;border:1px solid #ffeb3b;border-radius:14px;color:#ffeb3b"><strong>${total}</strong> designs &middot; one every 4 min</span>
+      <span style="padding:4px 10px;border:1px solid #ff7c4d;border-radius:14px;color:#ff7c4d">live until 06:00 PT</span>
+    </div>
+  </div>
+</section>
+` : ''}
+${cat === 'mural-animals' ? `
+<section style="padding:56px 24px 32px;text-align:center;background:linear-gradient(180deg,#f4ece0 0%,#e8d8c0 100%);color:#3a2818;margin-bottom:0;border-bottom:1px solid #c8b89a">
+  <div style="max-width:760px;margin:0 auto">
+    <p style="font-size:11px;letter-spacing:.32em;text-transform:uppercase;color:#8a6f44;margin:0 0 10px;font-weight:500">Hand-painted · Five-color · Atelier edition</p>
+    <h1 style="font-family:'Playfair Display','Didot',serif;font-weight:400;font-style:italic;font-size:clamp(36px,5vw,58px);line-height:1.05;margin:0 0 16px;color:#2a1f10">The Mural Menagerie</h1>
+    <p style="font-size:15px;line-height:1.7;color:#5a4632;margin:0 auto;max-width:580px">Refined chinoiserie-tradition wallpaper murals — peacocks amongst peonies, sika deer in misted clearings, mandarin ducks on still water. Watercolour and gouache on antiqued grounds, five-color palettes, no cartoon, no neon. In the manner of the great hand-painted mural houses.</p>
+    <div style="margin-top:20px;display:flex;gap:10px;justify-content:center;flex-wrap:wrap;font-size:12px">
+      <span style="padding:4px 12px;border:1px solid #8a6f44;border-radius:14px;color:#8a6f44"><strong>${total}</strong> hand-painted panels</span>
+    </div>
+  </div>
+</section>
+` : ''}
+${cat === 'mural-scenic' ? `
+<section style="padding:56px 24px 32px;text-align:center;background:linear-gradient(180deg,#ece4d4 0%,#dfd1ba 50%,#c5b298 100%);color:#2a1f10;margin-bottom:0;border-bottom:1px solid #b59a72">
+  <div style="max-width:780px;margin:0 auto">
+    <p style="font-size:11px;letter-spacing:.32em;text-transform:uppercase;color:#7a5e34;margin:0 0 10px;font-weight:500">Bespoke scenic mural · 20ft × 11ft · 150 dpi · five-color</p>
+    <h1 style="font-family:'Playfair Display','Didot',serif;font-weight:400;font-style:italic;font-size:clamp(36px,5vw,60px);line-height:1.05;margin:0 0 16px;color:#1f1808">The Scenic Panoramas</h1>
+    <p style="font-size:15px;line-height:1.7;color:#4a3520;margin:0 auto;max-width:600px">Full-room wall murals in the tradition of de Gournay, Iksel, Gracie, and Zuber et Cie. Single uninterrupted scenes — French formal gardens, Chinese chinoiserie valleys, English wildflower hills, Provence lavender vistas — animals as small naturalistic accents. 20 ft wide × 11 ft tall, target print 150 dpi after upscaling.</p>
+    <div style="margin-top:20px;display:flex;gap:10px;justify-content:center;flex-wrap:wrap;font-size:12px">
+      <span style="padding:4px 12px;border:1px solid #7a5e34;border-radius:14px;color:#7a5e34"><strong>${total}</strong> panoramic panels</span>
+      <span style="padding:4px 12px;border:1px solid #8a6f44;border-radius:14px;color:#8a6f44">single-panel, no repeat</span>
+    </div>
+  </div>
+</section>
+` : ''}
+
+<!-- ── Source filter chips — toggle between AI Designs and the Designer Library.
+     Per spec ?source=ai|library|all — v1 implements ai vs library as a hard
+     link split (the two surfaces have different sort+filter shapes so a true
+     unified "all" view is deferred). "all" defaults to AI for now. TODO(steve):
+     merge LIBRARY.rows into DESIGNS for a true unified source=all listing. -->
+<div class="source-chip-row" style="max-width:1400px;margin:0 auto;padding:20px 24px 0;display:flex;gap:8px;align-items:center;flex-wrap:wrap" data-source="${(req.query.source === 'all') ? 'all' : 'ai'}">
+  <span style="font:11px var(--sans,system-ui);letter-spacing:.18em;text-transform:uppercase;color:var(--ink-faint,#8a7a5e);margin-right:6px">Source</span>
+  <a href="/designs" class="src-chip" style="padding:6px 14px;border-radius:999px;border:1px solid ${(req.query.source !== 'all') ? 'var(--accent,#1a1a1a)' : 'var(--line,#d8d0c0)'};background:${(req.query.source !== 'all') ? 'var(--accent,#1a1a1a)' : 'transparent'};color:${(req.query.source !== 'all') ? 'var(--bg,#fff)' : 'var(--ink,#3a2818)'};font:11px var(--sans);letter-spacing:.04em;text-decoration:none">✦ AI Designs</a>
+  ${isAdmin(req) ? `<a href="/library" class="src-chip" style="padding:6px 14px;border-radius:999px;border:1px solid var(--line,#d8d0c0);background:transparent;color:var(--ink,#3a2818);font:11px var(--sans);letter-spacing:.04em;text-decoration:none">★ Designer Library</a>
+  <a href="/designs?source=all" class="src-chip" style="padding:6px 14px;border-radius:999px;border:1px solid ${(req.query.source === 'all') ? 'var(--accent,#1a1a1a)' : 'var(--line,#d8d0c0)'};background:${(req.query.source === 'all') ? 'var(--accent,#1a1a1a)' : 'transparent'};color:${(req.query.source === 'all') ? 'var(--bg,#fff)' : 'var(--ink,#3a2818)'};font:11px var(--sans);letter-spacing:.04em;text-decoration:none">★ All (Combined)</a>` : ''}
+  <span style="font:11px var(--sans);color:var(--ink-faint,#8a7a5e);margin-left:auto">${isAdmin(req) ? ((req.query.source === 'all') ? 'AI + designer library' : 'AI-generated patterns') : 'AI-generated patterns'}</span>
+</div>
+${(req.query.source === 'all') ? `
+<script>
+(function(){
+  // ?source=all — fetch up to 24 designer-library cards and append to the AI
+  // grid. Each card carries a "★ Designer" badge so the source is obvious.
+  fetch('/api/library?limit=24&sort=newest').then(function(r){return r.json();}).then(function(j){
+    if (!j || !j.ok || !j.rows || j.rows.length === 0) return;
+    var grid = document.querySelector('.design-grid');
+    if (!grid) return;
+    var frag = document.createDocumentFragment();
+    j.rows.forEach(function(r){
+      var d = document.createElement('div');
+      d.className = 'design-card library-card';
+      d.dataset.libId = r.id;
+      d.dataset.source = 'library';
+      var safeTitle = String(r.title||'').replace(/[<>"]/g,'');
+      var safeSku = String(r.sku||'').replace(/[<>"]/g,'');
+      d.innerHTML =
+        '<a href="/library/'+r.id+'" class="card-link" style="display:block;color:inherit;text-decoration:none">' +
+          '<div class="card-img" style="background-image:url(\\''+r.image_url+'\\');background-size:cover;background-position:center 12%;aspect-ratio:1/1"></div>' +
+          '<div class="card-meta" style="padding:10px 12px 12px;display:flex;flex-direction:column;gap:4px;font:12px var(--sans,system-ui)">' +
+            '<span class="card-title" style="font:500 13px var(--sans);color:var(--ink,#1f1808);line-height:1.2">'+safeTitle+'</span>' +
+            '<span class="card-sku" style="font:11px var(--sans);color:var(--ink-faint,#8a7a5e);letter-spacing:.04em">Designer Wallcoverings · '+safeSku+'</span>' +
+            '<span class="card-tags" style="display:flex;gap:5px;margin-top:2px">' +
+              '<span style="font:10px var(--sans);padding:2px 6px;background:#fef9eb;border:1px solid #d4b46a;border-radius:3px;color:#7a5e2a">★ Designer</span>' +
+              '<span style="font:10px var(--sans);padding:2px 6px;background:var(--bg,#fff);border:1px solid var(--line,#e0d9cd);border-radius:3px;color:#7a6a52">'+String(r.style_bucket||'Mixed').replace(/[<>"]/g,'')+'</span>' +
+            '</span>' +
+          '</div>' +
+        '</a>';
+      frag.appendChild(d);
+    });
+    grid.appendChild(frag);
+  }).catch(function(){});
+})();
+</script>` : ''}
+
+<section class="catalog-section">
+  <div class="catalog-toolbar">
+    <form method="get" action="/designs" class="filter-form">
+      <input type="hidden" name="sort" id="sort-hidden" value="${sort}">
+      <div class="toolbar-row">
+        <div class="search-wrap" style="position:relative;flex:1;min-width:200px">
+          <input type="search" name="q" value="${q}" placeholder="Search designs…" class="search-input" id="catalog-search-input" autocomplete="off" aria-autocomplete="list" aria-controls="catalog-search-suggest" aria-expanded="false" role="combobox" style="width:100%;padding-right:36px">
+          ${q
+            ? `<a href="/designs${(function(){
+                const parts = [
+                  sort && sort !== 'newest' ? 'sort=' + encodeURIComponent(sort) : '',
+                  cat    ? 'cat='   + encodeURIComponent(cat)    : '',
+                  motifQ ? 'motif=' + encodeURIComponent(motifQ) : '',
+                  hueQ   ? 'hue='   + encodeURIComponent(hueQ)   : ''
+                ].filter(Boolean);
+                return parts.length ? '?' + parts.join('&') : '';
+              })()}"
+                id="catalog-search-clear" aria-label="Clear search query"
+                title="Clear search"
+                style="position:absolute;right:8px;top:50%;transform:translateY(-50%);width:22px;height:22px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:var(--ink-faint,#999);color:var(--bg,#fff);text-decoration:none;font:600 13px/1 var(--sans)">✕</a>`
+            : `<kbd id="catalog-search-kbd" aria-hidden="true" title="Press / to focus search" style="position:absolute;right:8px;top:50%;transform:translateY(-50%);padding:2px 7px;font:600 11px/1 ui-monospace,Menlo,monospace;color:var(--ink-faint,#999);background:var(--card-bg,#faf8f3);border:1px solid var(--line,#e0d9cd);border-radius:4px;pointer-events:none;transition:opacity .12s ease">/</kbd>`
+          }
+          <div id="catalog-search-suggest" role="listbox" aria-label="Search suggestions" style="display:none;position:absolute;left:0;right:0;top:100%;margin-top:4px;background:var(--bg,#fff);border:1px solid var(--line,#ddd);border-radius:6px;box-shadow:0 8px 24px rgba(0,0,0,.12);z-index:50;max-height:60vh;overflow-y:auto"></div>
+        </div>
+        <style>
+          @media (hover: none) { #catalog-search-kbd { display:none } }
+          .search-input:focus ~ #catalog-search-kbd { opacity:0 }
+        </style>
+        <select name="cat" class="filter-select" onchange="this.form.submit()">
+          <option value="">All Categories</option>
+          ${cats.map(c => `<option value="${c}" ${c===cat?'selected':''}>${c.charAt(0).toUpperCase()+c.slice(1)}</option>`).join('')}
+        </select>
+        <div class="sort-btn-row" style="display:flex;gap:4px;flex-wrap:wrap;align-items:center" role="radiogroup" aria-label="Sort designs">
+          <span style="font:11px var(--sans);text-transform:uppercase;letter-spacing:.1em;color:var(--ink-faint);margin-right:4px">Sort</span>
+          ${[
+            ['newest',      'Newest'],
+            ['title',       'Title A→Z'],
+            ['category',    'Category'],
+            ['color-wheel', 'Color Wheel'],
+            ['random',      'Random'],
+          ].map(([k, label]) => {
+            const on = sort === k;
+            return `<button type="button" class="sort-btn${on?' is-on':''}" data-sort="${k}" role="radio" aria-checked="${on}"
+              onclick="document.getElementById('sort-hidden').value='${k}';this.form.submit()"
+              style="padding:5px 11px;border:1px solid ${on?'var(--accent,#1a1a1a)':'var(--line,#d8d0c0)'};background:${on?'var(--accent,#1a1a1a)':'transparent'};color:${on?'var(--bg,#fff)':'var(--ink,#3a2818)'};border-radius:999px;font:11px var(--sans,system-ui);letter-spacing:.04em;cursor:pointer;white-space:nowrap">${label}</button>`;
+          }).join('')}
+          <!-- Full "More sorts" dropdown — every typical sort, available to all users (not admin-gated). -->
+          <select aria-label="More sort options" onchange="if(this.value){document.getElementById('sort-hidden').value=this.value;this.form.submit()}"
+            style="padding:5px 11px;border:1px solid var(--line,#d8d0c0);background:transparent;color:var(--ink,#3a2818);border-radius:999px;font:11px var(--sans,system-ui);letter-spacing:.04em;cursor:pointer;margin-left:2px">
+            <option value="">More sorts…</option>
+            <optgroup label="Date">
+              <option value="newest"     ${sort==='newest'?'selected':''}>Newest first</option>
+              <option value="oldest"     ${sort==='oldest'?'selected':''}>Oldest first</option>
+            </optgroup>
+            <optgroup label="Alphabetical">
+              <option value="title"        ${sort==='title'?'selected':''}>Title A → Z</option>
+              <option value="title-desc"   ${sort==='title-desc'?'selected':''}>Title Z → A</option>
+              <option value="category"     ${sort==='category'?'selected':''}>Category A → Z</option>
+              <option value="category-desc"${sort==='category-desc'?'selected':''}>Category Z → A</option>
+            </optgroup>
+            <optgroup label="Color">
+              <option value="light-dark"  ${sort==='light-dark'?'selected':''}>Light → Dark</option>
+              <option value="dark-light"  ${sort==='dark-light'?'selected':''}>Dark → Light</option>
+              <option value="color-wheel" ${sort==='color-wheel'?'selected':''}>Color Wheel (warm → cool)</option>
+              <option value="vivid"       ${sort==='vivid'?'selected':''}>Most Vivid</option>
+              <option value="muted"       ${sort==='muted'?'selected':''}>Most Muted</option>
+            </optgroup>
+            <optgroup label="Other">
+              <option value="has-room"    ${sort==='has-room'?'selected':''}>In-Room previews first</option>
+              <option value="random"      ${sort==='random'?'selected':''}>Random shuffle</option>
+            </optgroup>
+          </select>
+        </div>
+        <label class="density-label" style="display:inline-flex;align-items:center;gap:8px;font:11px var(--sans);text-transform:uppercase;letter-spacing:.1em;color:var(--ink-faint)">Density
+          <input type="range" id="density-slider" min="1" max="12" step="1" value="6" class="density-range" style="flex:0 0 140px" aria-valuemin="1" aria-valuemax="12" aria-valuenow="6">
+          <span id="density-value" aria-live="polite" style="font:600 11px var(--sans);color:var(--ink-soft);min-width:18px;text-align:right;font-variant-numeric:tabular-nums">6</span>
+        </label>${_isAdmin ? `
+        <label class="spacing-label" style="display:inline-flex;align-items:center;gap:8px;font:11px var(--sans);text-transform:uppercase;letter-spacing:.1em;color:var(--ink-faint)" title="Admin-only: visual gap between design cards">Spacing
+          <input type="range" id="spacing-slider" min="0" max="40" step="2" value="2" class="density-range" style="flex:0 0 140px" aria-valuemin="0" aria-valuemax="40" aria-valuenow="2">
+          <span id="spacing-value" aria-live="polite" style="font:600 11px var(--sans);color:var(--ink-soft);min-width:22px;text-align:right;font-variant-numeric:tabular-nums">2px</span>
+        </label>` : ''}
+        <button type="button" id="surprise-btn" class="filter-select"
+          style="background:var(--gold,#c9a14b);color:var(--accent,#0d0d0d);border:0;font-weight:500;padding:8px 14px;cursor:pointer;border-radius:6px;letter-spacing:.04em"
+          title="Generate one fresh design in a random palette · ~30s">✦ Surprise me</button>
+      </div>
+      <input type="hidden" name="motif" id="motif-hidden" value="${motifQ}">
+      <input type="hidden" name="hue" id="hue-hidden" value="${hueQ}">
+    </form>
+
+    <!-- Color-wheel hue swatches -->
+    <div class="hue-swatches" style="display:flex;flex-wrap:wrap;gap:6px;margin:18px 0 4px;align-items:center">
+      <span style="font:11px var(--sans);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-faint);margin-right:6px">Hue</span>
+      <a href="/designs${cat ? '?cat='+encodeURIComponent(cat) : ''}${motifQ ? (cat?'&':'?')+'motif='+encodeURIComponent(motifQ) : ''}" class="hue-chip${!hueQ?' is-on':''}" style="font:11px var(--sans);padding:5px 11px;border:1px solid ${!hueQ?'var(--accent)':'var(--line)'};background:${!hueQ?'var(--accent)':'transparent'};color:${!hueQ?'var(--bg)':'var(--ink-soft)'};border-radius:999px;text-decoration:none">All</a>
+      ${HUE_DEFS.filter(([k]) => (hueCounts[k]||0) > 0).map(([k,hex]) => {
+        const on = hueQ === k;
+        const qs = [];
+        if (cat) qs.push('cat=' + encodeURIComponent(cat));
+        if (motifQ) qs.push('motif=' + encodeURIComponent(motifQ));
+        qs.push('hue=' + encodeURIComponent(k));
+        const href = '/designs?' + qs.join('&');
+        return `<a href="${href}" class="hue-chip${on?' is-on':''}" title="Filter by ${k} (${hex.toUpperCase()})" style="display:inline-flex;align-items:center;gap:7px;font:11px var(--sans);padding:4px 12px 4px 5px;border:1px solid ${on?'var(--accent)':'var(--line)'};background:${on?'var(--accent)':'transparent'};color:${on?'var(--bg)':'var(--ink-soft)'};border-radius:999px;text-decoration:none;min-height:32px">
+          <span aria-hidden="true" style="display:inline-block;width:20px;height:20px;border-radius:50%;background:${hex};border:1px solid rgba(0,0,0,.18);box-shadow:0 1px 2px rgba(0,0,0,.08)"></span>
+          ${k} <span style="opacity:.55">·${hueCounts[k]||0}</span>
+        </a>`;
+      }).join('')}
+    </div>
+
+    <!-- Motif chips (one-click filter) -->
+    <div class="motif-chips" style="display:flex;flex-wrap:wrap;gap:6px;margin:14px 0 6px;align-items:center">
+      <span style="font:11px var(--sans);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-faint);margin-right:6px">Motif</span>
+      <a href="/designs${cat ? '?cat='+encodeURIComponent(cat) : ''}" class="motif-chip${!motifQ?' is-on':''}" style="font:11px var(--sans);padding:5px 12px;border:1px solid ${!motifQ?'var(--accent)':'var(--line)'};background:${!motifQ?'var(--accent)':'transparent'};color:${!motifQ?'var(--bg)':'var(--ink-soft)'};border-radius:999px;text-decoration:none">All</a>
+      ${topMotifs.map(([m,n]) => `<a href="/designs?${cat?'cat='+encodeURIComponent(cat)+'&':''}motif=${encodeURIComponent(m)}" class="motif-chip${motifQ===m?' is-on':''}" style="font:11px var(--sans);padding:5px 12px;border:1px solid ${motifQ===m?'var(--accent)':'var(--line)'};background:${motifQ===m?'var(--accent)':'transparent'};color:${motifQ===m?'var(--bg)':'var(--ink-soft)'};border-radius:999px;text-decoration:none">${m} <span style="opacity:.55">·${n}</span></a>`).join('')}
+    </div>
+
+    <!-- Room-preview filter chip (toggles ?room=1 — shows only designs that have a residential mockup) -->
+    ${(() => {
+      const roomCnt = DESIGNS.filter(d => (d.room_mockups || []).length > 0).length;
+      const on = !!roomQ;
+      const href = `/designs${[
+        cat   ? `cat=${encodeURIComponent(cat)}`     : '',
+        motifQ? `motif=${encodeURIComponent(motifQ)}`: '',
+        hueQ  ? `hue=${encodeURIComponent(hueQ)}`    : '',
+        !on   ? 'room=1'                             : ''
+      ].filter(Boolean).join('&').replace(/^/, on ? '' : '?')}`;
+      return `<div class="motif-chips" style="display:flex;flex-wrap:wrap;gap:6px;margin:0 0 6px;align-items:center">
+        <span style="font:11px var(--sans);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-faint);margin-right:6px">Preview</span>
+        <a href="${href}" class="motif-chip${on?' is-on':''}" title="Show only designs with a room mockup preview"
+           style="font:11px var(--sans);padding:5px 12px;border:1px solid ${on?'var(--accent)':'var(--line)'};background:${on?'var(--accent)':'transparent'};color:${on?'var(--bg)':'var(--ink-soft)'};border-radius:999px;text-decoration:none">
+          In a room <span style="opacity:.55">·${roomCnt}</span>
+        </a>
+        <button type="button" id="fav-only-toggle" class="motif-chip" aria-pressed="false"
+          style="font:11px var(--sans);padding:5px 12px;border:1px solid var(--line);background:transparent;color:var(--ink-soft);border-radius:999px;cursor:pointer">
+          ♡ Favorites <span id="fav-only-count" style="opacity:.55">·0</span>
+        </button>
+        ${(cat || motifQ || hueQ || roomQ || q) ? `<a href="/designs" class="motif-chip clear-all-chip" title="Clear every active filter"
+          style="font:11px var(--sans);padding:5px 12px;border:1px solid var(--line);background:transparent;color:var(--ink-soft);border-radius:999px;text-decoration:none;display:inline-flex;align-items:center;gap:5px;margin-left:6px">
+          <span aria-hidden="true" style="font-size:13px;line-height:1">✕</span> Clear filters
+        </a>` : ''}
+      </div>`;
+    })()}
+
+    <style>
+      /* Favorite heart on each card */
+      .design-card { position:relative }
+      .fav-btn { position:absolute;top:10px;left:10px;z-index:5;width:32px;height:32px;border-radius:50%;border:0;background:rgba(20,18,15,.55);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:16px;line-height:1;transition:transform .12s,background .15s;backdrop-filter:blur(4px) }
+      .fav-btn:hover { background:rgba(20,18,15,.85);transform:scale(1.08) }
+      .fav-btn.is-fav { background:rgba(201,71,71,.92);color:#fff }
+      .fav-btn[aria-pressed="true"] .fav-icon::before { content:"♥" }
+      .fav-btn[aria-pressed="false"] .fav-icon::before { content:"♡" }
+      /* NEW badge — top-right corner, brand-gold pill on dark background.
+         Hidden at rest per Steve's rule: no text on the resting card; surfaces
+         only when the visitor commits attention (hover/focus). */
+      .card-new-badge { position:absolute;top:10px;right:10px;z-index:5;padding:4px 10px;border-radius:999px;background:var(--accent,#c9a14b);color:var(--bg,#fff);font:600 10px/1 var(--sans,system-ui);letter-spacing:.12em;text-transform:uppercase;box-shadow:0 2px 8px rgba(0,0,0,.25);opacity:0;transition:opacity .18s ease;pointer-events:none }
+      .design-card:hover .card-new-badge,
+      .design-card:focus-within .card-new-badge { opacity:1 }
+      /* Resting is-new ring kept (no text) — visual cue at low density. */
+      .design-card.is-new { box-shadow:0 0 0 1px var(--accent,#c9a14b),0 6px 22px rgba(201,161,75,.18) }
+      /* Fav heart also fades at rest so the grid reads as pure pattern. */
+      .fav-btn { opacity:0;transition:opacity .18s ease,transform .12s,background .15s }
+      .design-card:hover .fav-btn,
+      .design-card:focus-within .fav-btn,
+      .fav-btn.is-fav { opacity:1 }
+      /* Touch devices have no :hover — keep heart + NEW always visible there
+         so mobile users can actually save designs and see the NEW pill.
+         (hover: none) targets phones/tablets without an associated mouse. */
+      @media (hover: none) {
+        .fav-btn,
+        .card-new-badge { opacity:1 }
+      }
+      /* Hover layer on every catalog card — pattern number + View / Sample.
+         Sits inside .card-img so it picks up the existing aspect-ratio box.
+         pointer-events:none on the wrapper lets clicks fall through to the
+         parent <a>, except on the explicit child buttons which re-enable
+         pointer events. */
+      .design-card .card-img { position:relative }
+      .card-hover {
+        position:absolute; inset:0; z-index:4; pointer-events:none;
+        background:linear-gradient(to top, rgba(20,18,15,.78) 0%, rgba(20,18,15,.32) 45%, rgba(20,18,15,0) 70%);
+        opacity:0; transition:opacity .18s ease;
+        display:flex; flex-direction:column; justify-content:flex-end;
+        padding:10px 12px 12px;
+        color:#fff;
+      }
+      .design-card:hover .card-hover,
+      .design-card:focus-within .card-hover { opacity:1 }
+      .card-hover-dig {
+        position:absolute; top:10px; left:50%; transform:translateX(-50%);
+        background:rgba(255,255,255,.92); color:#111;
+        font:600 10px/1 ui-monospace,Menlo,monospace; letter-spacing:.06em;
+        padding:5px 9px; border-radius:3px;
+        box-shadow:0 2px 6px rgba(0,0,0,.18);
+        white-space:nowrap;
+      }
+      .card-hover-actions { display:flex; gap:8px; justify-content:center }
+      .chov-btn {
+        pointer-events:auto;
+        flex:1; max-width:120px; text-align:center;
+        padding:7px 10px; border-radius:3px;
+        font:600 11px/1 var(--sans,system-ui); letter-spacing:.08em; text-transform:uppercase;
+        text-decoration:none; transition:transform .12s, background .15s;
+      }
+      .chov-view   { background:#fff; color:#111 }
+      .chov-sample { background:rgba(255,255,255,.18); color:#fff; border:1px solid rgba(255,255,255,.55) }
+      .chov-btn:hover { transform:translateY(-1px) }
+      .chov-sample:hover { background:rgba(255,255,255,.32) }
+    </style>
+
+    <p class="result-count">${total} design${total===1?'':'s'}${cat?' in '+cat:''}${motifQ?' with '+motifQ:''}${roomQ?' with room preview':''}${q?' matching "'+q+'"':''}<span id="fav-count-extra" style="margin-left:8px;color:var(--ink-faint)"></span></p>
+
+    <!-- One-time hint pointing trade users to the /brief proposal builder. Dismissible; -->
+    <!-- once dismissed it stays hidden via localStorage('wallco.hint.brief.dismissed').    -->
+    <div id="brief-hint" style="display:none;margin:6px 0 14px;padding:9px 14px;border:1px dashed var(--line,#ddd);border-radius:6px;background:var(--card-bg,#faf8f3);font:12px/1.5 var(--sans,system-ui);color:var(--ink-soft,#555);display:flex;align-items:center;gap:10px;flex-wrap:wrap">
+      <span aria-hidden="true">📄</span>
+      <span>Building a proposal? Pick up to 6 designs and assemble a single client-ready brief.</span>
+      <a href="/brief" style="color:var(--ink,#111);font-weight:500;text-decoration:underline;text-underline-offset:2px">Open /brief →</a>
+      <a id="brief-hint-dismiss" href="javascript:void(0)" style="margin-left:auto;color:var(--ink-faint,#999);text-decoration:underline;font-size:11px">Hide</a>
+    </div>
+    <script>
+    (function(){
+      var el = document.getElementById('brief-hint');
+      if (!el) return;
+      try { if (localStorage.getItem('wallco.hint.brief.dismissed') === '1') return; } catch(e){}
+      el.style.display = 'flex';
+      var btn = document.getElementById('brief-hint-dismiss');
+      if (btn) btn.addEventListener('click', function(){
+        el.style.display = 'none';
+        try { localStorage.setItem('wallco.hint.brief.dismissed', '1'); } catch(e){}
+      });
+    })();
+    </script>
+
+    <div id="surprise-result" style="display:none;margin-bottom:18px;padding:16px;border:1px solid var(--gold);border-radius:8px;background:var(--card-bg)"></div>
+    <script>
+    (function(){
+      var btn = document.getElementById('surprise-btn');
+      if (!btn) return;
+      var palettes = [
+        ['amber and emerald','damask'],
+        ['blush and indigo','floral'],
+        ['oyster and bone','damask'],
+        ['terracotta and sage','mixed'],
+        ['plum and gold','geometric'],
+        ['marine and coral','floral'],
+        ['charcoal and brass','geometric'],
+        ['ivory and persimmon','floral'],
+        ['sapphire and pearl','damask'],
+        ['olive and rose','floral']
+      ];
+      var motifsCue = ['scrollwork','peony','lattice','vine','arabesque','asanoha','fan','blossom','toile','trellis'];
+      btn.addEventListener('click', async function(){
+        var pick = palettes[Math.floor(Math.random()*palettes.length)];
+        var motif = motifsCue[Math.floor(Math.random()*motifsCue.length)];
+        var msg = 'make me a new wallpaper in ' + pick[0] + ' with a ' + motif + ' motif — surprising me';
+        var box = document.getElementById('surprise-result');
+        box.style.display = 'block';
+        box.innerHTML = '<div style="font:13px/1.5 -apple-system,sans-serif;color:var(--ink-soft)">Generating <em>' + pick[0] + ' · ' + motif + '</em>… (~30s)</div>';
+        btn.disabled = true; var orig = btn.textContent; btn.textContent = '… generating';
+        try {
+          var r = await fetch('/api/chat/catalog', {
+            method: 'POST', headers: {'Content-Type': 'application/json'},
+            body: JSON.stringify({ message: msg })
+          });
+          var j = await r.json();
+          var d = (j.actions || []).find(function(a){ return a.type === 'new_design'; });
+          if (d && d.design) {
+            var did = d.design.id;
+            var idValid = Number.isFinite(did);
+            var idLabel = idValid ? '#' + did : '(unsaved)';
+            var nonce = Math.random().toString(36).slice(2, 9);
+            var imgUrl = d.design.image_url;
+            var hex = d.design.dominant_hex || '#888';
+            var promptText = String(d.design.prompt || '').replace(/[<>]/g, '');
+            box.innerHTML =
+              '<div style="display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:18px;align-items:start">' +
+                '<div>' +
+                  '<div style="aspect-ratio:1;background:url(\\''+imgUrl+'\\') center/cover #f2efe8;border-radius:10px;border:1px solid var(--line);max-width:520px"></div>' +
+                  '<div style="margin-top:10px;font:13px/1.5 var(--sans);color:var(--ink-soft)">' +
+                    '<span style="display:inline-block;width:11px;height:11px;border-radius:50%;background:'+hex+';vertical-align:middle;margin-right:6px;border:1px solid rgba(0,0,0,.12)"></span>' +
+                    '<b style="color:var(--ink)">Design '+idLabel+'</b> · '+pick[0]+' · '+motif+' · '+hex +
+                  '</div>' +
+                '</div>' +
+                '<div style="display:flex;flex-direction:column;gap:14px;min-width:0">' +
+                  (promptText ? '<details><summary style="cursor:pointer;font:11px/1 var(--sans);color:var(--ink-faint);text-transform:uppercase;letter-spacing:.08em">Prompt ▾</summary><div style="margin-top:8px;font:11px/1.5 ui-monospace,Menlo,monospace;color:var(--ink-soft);max-height:180px;overflow-y:auto;padding:8px;background:var(--card-bg);border:1px solid var(--line);border-radius:6px">'+promptText+'</div></details>' : '') +
+                  (idValid
+                    ? '<div id="home-review-'+did+'" style="padding:14px;border:1px solid var(--line);border-radius:8px;background:var(--card-bg)"><div style="font:11px/1 var(--sans);color:var(--ink-faint);text-transform:uppercase;letter-spacing:.08em">Interior-designer review · #'+did+'</div><div style="margin-top:8px;font:13px/1.5 var(--sans);color:var(--ink-soft)"><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--gold);animation:rev-pulse 1.2s ease-in-out infinite;margin-right:6px;vertical-align:middle"></span>Running…</div></div>'
+                    : '<div style="padding:12px;border:1px dashed var(--line);border-radius:8px;background:#fff8e8;font:12px/1.5 var(--sans);color:#7a5f10">Image generated, but the row didn\\'t persist (DB schema drift on prod). Image is available to download — review unavailable until persisted.</div>') +
+                  '<div style="display:flex;flex-direction:column;gap:6px;margin-top:auto">' +
+                    (idValid ? '<a href="/design/'+did+'" style="display:block;text-align:center;padding:8px 14px;background:var(--gold,#c9a14b);color:var(--accent,#0d0d0d);text-decoration:none;font-size:13px;font-weight:500;border-radius:6px">Open full page →</a>' : '') +
+                    '<a href="'+imgUrl+'" download style="display:block;text-align:center;padding:8px 14px;border:1px solid var(--ink);color:var(--ink);text-decoration:none;font-size:13px;border-radius:6px">Download pattern</a>' +
+                    '<button id="home-retry-'+nonce+'" type="button" style="padding:8px 14px;background:transparent;color:var(--ink);border:1px solid var(--line);border-radius:6px;cursor:pointer;font:13px var(--sans)" data-prompt="'+promptText.replace(/"/g,'&quot;')+'">Try this prompt again</button>' +
+                    '<button id="home-again-'+nonce+'" type="button" style="padding:8px 14px;background:var(--ink);color:var(--bg);border:0;border-radius:6px;cursor:pointer;font:13px var(--sans)">Surprise me again</button>' +
+                  '</div>' +
+                '</div>' +
+              '</div>' +
+              '<style>@keyframes rev-pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.4;transform:scale(.6)} }</style>';
+            var againBtn = document.getElementById('home-again-'+nonce);
+            if (againBtn) againBtn.addEventListener('click', function(){ btn.click(); });
+            var retryBtn = document.getElementById('home-retry-'+nonce);
+            if (retryBtn) retryBtn.addEventListener('click', async function(){
+              var p = retryBtn.getAttribute('data-prompt') || promptText;
+              if (!p) return;
+              box.innerHTML = '<div style="font:13px/1.5 -apple-system,sans-serif;color:var(--ink-soft)">Re-rendering same prompt… (~30s)</div>';
+              btn.disabled = true; btn.textContent = '… generating';
+              try {
+                var rr = await fetch('/api/chat/catalog', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({message: p}) });
+                var jj = await rr.json();
+                var dd = (jj.actions||[]).find(function(a){return a.type==='new_design';});
+                if (dd && dd.design) {
+                  var ddid = dd.design.id;
+                  var ddidValid = Number.isFinite(ddid);
+                  var u = dd.design.image_url, h = dd.design.dominant_hex || '#888';
+                  box.innerHTML = '<div style="display:flex;gap:18px;align-items:start;flex-wrap:wrap"><div style="aspect-ratio:1;width:520px;max-width:100%;background:url(\\''+u+'\\') center/cover #f2efe8;border-radius:10px;border:1px solid var(--line)"></div><div style="font:13px var(--sans);color:var(--ink-soft)"><span style="display:inline-block;width:11px;height:11px;border-radius:50%;background:'+h+';margin-right:6px;border:1px solid rgba(0,0,0,.12);vertical-align:middle"></span>Same prompt, fresh render '+(ddidValid?'· #'+ddid:'(unsaved)')+'<br><br>'+(ddidValid?'<a href="/design/'+ddid+'" style="color:var(--gold)">Open full page →</a><br>':'')+'<a href="'+u+'" download style="color:var(--ink)">Download pattern</a></div></div>';
+                } else {
+                  box.innerHTML = '<div style="color:#b03a2e">Re-render failed: ' + (jj.error || 'unknown').slice(0,300) + '</div>';
+                }
+              } catch (e) { box.innerHTML = '<div style="color:#b03a2e">Error: ' + e.message + '</div>'; }
+              btn.disabled = false; btn.textContent = orig;
+            });
+            if (idValid) fetch('/api/studio/interior-design-review', {
+              method: 'POST', headers: {'Content-Type':'application/json'},
+              body: JSON.stringify({ design_id: did })
+            }).then(function(r){ return r.json(); }).then(function(rj){
+              var revBox = document.getElementById('home-review-'+did);
+              if (!revBox) return;
+              var esc = function(s){ return String(s||'').replace(/[<>]/g,''); };
+              if (rj.ok && rj.critique) {
+                var c = rj.critique;
+                revBox.innerHTML =
+                  '<div style="font:11px/1 var(--sans);color:var(--ink-faint);text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px">Interior-designer review · #'+did+'</div>' +
+                  (Array.isArray(c.strengths) && c.strengths.length ? '<div style="margin-bottom:10px"><div style="font:11px/1.4 var(--sans);font-weight:700;color:var(--ink);text-transform:uppercase;letter-spacing:.06em;margin-bottom:4px">Strengths</div><ul style="margin:0;padding-left:18px;font:13px/1.5 var(--sans);color:var(--ink-soft)">'+ c.strengths.map(function(s){return '<li>'+esc(s)+'</li>';}).join('') +'</ul></div>' : '') +
+                  (Array.isArray(c.concerns) && c.concerns.length ? '<div style="margin-bottom:10px"><div style="font:11px/1.4 var(--sans);font-weight:700;color:var(--ink);text-transform:uppercase;letter-spacing:.06em;margin-bottom:4px">Concerns</div><ul style="margin:0;padding-left:18px;font:13px/1.5 var(--sans);color:var(--ink-soft)">'+ c.concerns.map(function(s){return '<li>'+esc(s)+'</li>';}).join('') +'</ul></div>' : '') +
+                  (c.recommendation ? '<div style="margin-bottom:10px;font:13px/1.5 var(--sans);color:var(--ink)"><b>Recommendation:</b> '+esc(c.recommendation)+'</div>' : '') +
+                  '<div style="font:11px/1.5 var(--sans);color:var(--ink-faint);margin-top:8px;padding-top:8px;border-top:1px solid var(--line)">' +
+                    (Array.isArray(c.room_fit) && c.room_fit.length ? '<b>Rooms:</b> '+esc(c.room_fit.join(', '))+' · ' : '') +
+                    (c.lighting ? '<b>Lighting:</b> '+esc(c.lighting)+' · ' : '') +
+                    (c.scale ? '<b>Scale:</b> '+esc(c.scale) : '') +
+                  '</div>';
+              } else {
+                revBox.innerHTML = '<div style="font:11px/1 var(--sans);color:var(--ink-faint);text-transform:uppercase;letter-spacing:.08em;margin-bottom:6px">Interior-designer review · #'+did+'</div><div style="font:13px/1.5 var(--sans);color:#b03a2e">Review failed: '+(rj.error||'unknown').replace(/[<>]/g,'')+'</div>';
+              }
+            }).catch(function(e){
+              var revBox = document.getElementById('home-review-'+did);
+              if (revBox) revBox.innerHTML = '<div style="font:11px/1 var(--sans);color:var(--ink-faint);margin-bottom:6px">Interior-designer review</div><div style="color:#b03a2e;font-size:13px">'+e.message+'</div>';
+            });
+          } else {
+            box.innerHTML = '<div style="color:#b03a2e">Generation failed: ' + (j.error || JSON.stringify(j).slice(0,200)) + '</div>';
+          }
+        } catch (e) {
+          box.innerHTML = '<div style="color:#b03a2e">Error: ' + e.message + '</div>';
+        }
+        btn.disabled = false; btn.textContent = orig;
+      });
+    })();
+    </script>
+    ${_isAdmin ? `<div class="export-links">
+      <a href="/moodboard">Moodboard →</a>
+      <span style="color:#444">·</span>
+      <span>Export:</span>
+      <a href="/api/export/keep.csv" download>Kept (CSV)</a>
+      <a href="/api/export/all.csv" download>All reviewed (CSV)</a>
+    </div>` : ''}
+  </div>
+
+  <div class="design-grid catalog-grid" id="catalog-grid" data-current-page="${page}" data-total-pages="${pages}" data-total="${total}">
+    ${cards || `<div class="empty-state" style="grid-column:1/-1;padding:80px 24px;text-align:center;color:var(--ink-soft,#555);font:14px/1.6 var(--sans,system-ui)">
+      <div style="font-size:42px;line-height:1;margin-bottom:14px;opacity:.55" aria-hidden="true">⌕</div>
+      <p style="margin:0 0 6px;font:300 22px/1.3 var(--serif,'Cormorant Garamond',serif);color:var(--ink,#111)">No designs match these filters</p>
+      <p style="margin:0 0 22px;color:var(--ink-faint,#999);max-width:380px;margin-left:auto;margin-right:auto">Try removing one filter, or browse the full catalog of ${DESIGNS.length.toLocaleString()} designs.</p>
+      <div style="display:flex;gap:10px;justify-content:center;flex-wrap:wrap">
+        <a href="/designs" style="padding:9px 18px;font:13px var(--sans);background:var(--ink,#111);color:var(--bg,#fff);text-decoration:none;border-radius:6px">Clear filters</a>
+        <a href="/designs?sort=random" style="padding:9px 18px;font:13px var(--sans);background:transparent;color:var(--ink,#111);text-decoration:none;border:1px solid var(--line,#d8d0c0);border-radius:6px">✦ Surprise me</a>
+      </div>
+    </div>`}
+  </div>
+
+  ${pages > 1 ? `
+  <div id="infinite-sentinel" style="height:1px;width:100%"></div>
+  <div id="infinite-status" style="text-align:center;padding:24px 0;font:11px/1 var(--sans,system-ui);letter-spacing:.18em;text-transform:uppercase;color:var(--ink-faint,#888)">${page < pages ? `loading more · page ${page} of ${pages}` : `all ${total} designs loaded`}</div>
+  <noscript><div class="pager">${pagerLinks}</div></noscript>
+  ` : ''}
+
+  <!-- Floating scroll-to-top button — appears after the user has scrolled past two viewport heights. -->
+  <button type="button" id="scroll-top-btn" aria-label="Back to top of catalog" title="Back to top"
+    style="position:fixed;right:18px;bottom:18px;z-index:80;width:42px;height:42px;border-radius:50%;border:1px solid var(--line,#e0d9cd);background:var(--bg,#fff);color:var(--ink,#111);box-shadow:0 4px 12px rgba(0,0,0,.12);cursor:pointer;display:none;align-items:center;justify-content:center;font:600 18px/1 var(--sans,system-ui);transition:opacity .18s ease,transform .12s ease">↑</button>
+  <script>
+  (function(){
+    var btn = document.getElementById('scroll-top-btn');
+    if (!btn) return;
+    var threshold = window.innerHeight * 2;
+    function onScroll(){
+      var show = window.scrollY > threshold;
+      btn.style.display = show ? 'inline-flex' : 'none';
+    }
+    window.addEventListener('scroll', onScroll, { passive: true });
+    window.addEventListener('resize', function(){ threshold = window.innerHeight * 2; onScroll(); });
+    btn.addEventListener('click', function(){
+      // Respect prefers-reduced-motion: instant jump for those users.
+      var rm = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
+      window.scrollTo({ top: 0, behavior: rm ? 'auto' : 'smooth' });
+      // Also focus the search input on return so /-shortcut feels natural after.
+      var s = document.getElementById('catalog-search-input');
+      if (s) setTimeout(function(){ s.focus(); }, rm ? 0 : 320);
+    });
+    onScroll();
+  })();
+  </script>
+
+  <!-- Prefetch /design/:id on card hover so the click feels instant.
+       Debounced 80ms — quick mouse-moves across the grid don't fire dozens.
+       Honors save-data + 2g connections + prefers-reduced-data (skip prefetch
+       on metered/slow networks). -->
+  <script>
+  (function(){
+    var conn = navigator.connection || {};
+    if (conn.saveData || /(^|\b)(slow-)?2g\b/.test(conn.effectiveType || '')) return;
+    var grid = document.getElementById('catalog-grid');
+    if (!grid) return;
+    var done = new Set();
+    var t = null;
+    function prefetch(href){
+      if (done.has(href)) return;
+      done.add(href);
+      var link = document.createElement('link');
+      link.rel = 'prefetch';
+      link.href = href;
+      link.as = 'document';
+      document.head.appendChild(link);
+    }
+    grid.addEventListener('mouseover', function(e){
+      var a = e.target.closest && e.target.closest('a.card-link');
+      if (!a || !a.href) return;
+      clearTimeout(t);
+      t = setTimeout(function(){ prefetch(a.href); }, 80);
+    }, { passive: true });
+    grid.addEventListener('mouseout', function(){ clearTimeout(t); }, { passive: true });
+  })();
+  </script>
+</section>
+</main>
+${_isAdminAuth ? `
+<!-- Admin view-mode toggle — only rendered when authenticated as admin. -->
+<style>
+  .view-toggle{position:fixed;right:14px;bottom:14px;z-index:9999;display:flex;gap:6px;align-items:center;padding:6px 10px;border-radius:999px;background:rgba(20,18,16,0.88);color:#f2eadb;font:11px/1 ui-sans-serif,system-ui,sans-serif;letter-spacing:.08em;text-transform:uppercase;box-shadow:0 4px 14px rgba(0,0,0,.35);backdrop-filter:blur(6px);user-select:none}
+  .view-toggle button{appearance:none;border:1px solid rgba(242,234,219,0.18);background:transparent;color:inherit;padding:5px 10px;border-radius:999px;font:inherit;cursor:pointer;transition:all .12s ease}
+  .view-toggle button.is-active{background:#f2eadb;color:#141210;border-color:#f2eadb}
+  .view-toggle button:hover:not(.is-active){background:rgba(242,234,219,0.12)}
+  .view-toggle-label{opacity:.55;margin-right:4px}
+  body.view-as-live .admin-only{display:none !important}
+</style>
+<div class="view-toggle" id="viewToggle" role="group" aria-label="View mode (admin only)">
+  <span class="view-toggle-label">View:</span>
+  <button type="button" data-mode="admin" id="vmAdmin">Admin</button>
+  <button type="button" data-mode="live"  id="vmLive">Live</button>
+</div>
+<script>
+(function(){
+  // Persisted view-mode toggle for admins.
+  // - "admin"  → server renders the full admin chrome (sliders/K/R/chips)
+  // - "live"   → server renders the public customer view, even though
+  //              admin auth cookie/token is still valid
+  // Mechanism: set cookie view_mode=admin|live then reload. Server reads it
+  // and forces _isAdmin=false when view_mode=live.
+  function setCookie(name, val, days){
+    var d = new Date(); d.setTime(d.getTime() + (days*86400000));
+    document.cookie = name + '=' + val + ';path=/;expires=' + d.toUTCString() + ';samesite=lax';
+  }
+  function readCookie(name){
+    var m = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]+)'));
+    return m ? decodeURIComponent(m[1]) : '';
+  }
+  function getMode(){
+    var url = new URL(window.location.href);
+    if (url.searchParams.get('view') === 'live') return 'live';
+    return readCookie('view_mode') === 'live' ? 'live' : 'admin';
+  }
+  function setActiveButton(mode){
+    var a = document.getElementById('vmAdmin');
+    var l = document.getElementById('vmLive');
+    if (!a || !l) return;
+    a.classList.toggle('is-active', mode === 'admin');
+    l.classList.toggle('is-active', mode === 'live');
+  }
+  function applyMode(mode){
+    var prev = getMode();
+    if (mode === prev) return;
+    setCookie('view_mode', mode, 365);
+    // Strip any ?view= from URL so it doesn't conflict with the cookie state.
+    var url = new URL(window.location.href);
+    url.searchParams.delete('view');
+    window.location.replace(url.toString());
+  }
+  setActiveButton(getMode());
+  var a = document.getElementById('vmAdmin');
+  var l = document.getElementById('vmLive');
+  if (a) a.addEventListener('click', function(){ applyMode('admin'); });
+  if (l) l.addEventListener('click', function(){ applyMode('live'); });
+})();
+</script>
+` : ''}
+${FOOTER}
+
+<!-- 2026-05-20 Steve directive: allow 1 page browsing, then scroll-triggered
+     login modal for general (un-roled) users. Suppressed for admin/user/designer. -->
+<div id="dw-login-veil" style="display:none;position:fixed;inset:0;background:rgba(20,18,15,.42);backdrop-filter:blur(6px);z-index:9998;opacity:0;transition:opacity .4s ease;pointer-events:none"></div>
+<div id="dw-login-modal" style="display:none;position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);z-index:9999;background:#fff;border:1px solid #d8cdb0;border-radius:10px;padding:36px 32px;width:92%;max-width:440px;box-shadow:0 16px 48px rgba(60,40,20,.22);opacity:0;transition:opacity .4s ease .1s;pointer-events:none">
+  <h2 style="margin:0 0 6px;font:300 30px/1.1 'Bodoni Moda',Georgia,serif;letter-spacing:.02em;color:#3a2818">Continue browsing</h2>
+  <div style="color:#8a7c66;font:13px/1.5 ui-sans-serif,system-ui;margin-bottom:24px">Sign in to access the full 12,000+ pattern library, save favorites, and place orders.</div>
+  <div style="display:flex;flex-direction:column;gap:10px">
+    <form method="POST" action="/login" style="margin:0"><input type="hidden" name="role" value="designer"><input type="hidden" name="next" value="/designs"><button type="submit" style="width:100%;appearance:none;padding:14px 18px;border:1px solid #c9a14b;background:#c9a14b;color:#fff;font:600 13px/1 ui-sans-serif,system-ui;letter-spacing:.06em;text-transform:uppercase;border-radius:5px;cursor:pointer">★ Sign in as Designer / Trade</button></form>
+    <form method="POST" action="/login" style="margin:0"><input type="hidden" name="role" value="user"><input type="hidden" name="next" value="/designs"><button type="submit" style="width:100%;appearance:none;padding:14px 18px;border:1px solid #d8cdb0;background:#fff;color:#3a2818;font:600 13px/1 ui-sans-serif,system-ui;letter-spacing:.06em;text-transform:uppercase;border-radius:5px;cursor:pointer">Sign in as Customer</button></form>
+    <form method="POST" action="/login" style="margin:0"><input type="hidden" name="role" value="admin"><input type="hidden" name="next" value="/designs"><button type="submit" style="width:100%;appearance:none;padding:14px 18px;border:1px solid #3a2818;background:#3a2818;color:#fff;font:600 13px/1 ui-sans-serif,system-ui;letter-spacing:.06em;text-transform:uppercase;border-radius:5px;cursor:pointer">Admin Login</button></form>
+  </div>
+  <div style="color:#8a7c66;font:11px/1.4 ui-sans-serif;letter-spacing:.04em;margin-top:18px;text-align:center">Designer + Customer access is open during preview.</div>
+</div>
+<script>
+(function(){
+  var role = ${JSON.stringify(_role || '')};
+  if (role) return;  // logged-in users skip modal entirely
+  var veil = document.getElementById('dw-login-veil');
+  var modal = document.getElementById('dw-login-modal');
+  if (!veil || !modal) return;
+  var shown = false;
+  function show() {
+    if (shown) return;
+    shown = true;
+    veil.style.display = 'block';
+    modal.style.display = 'block';
+    requestAnimationFrame(function(){
+      veil.style.opacity = '1';
+      modal.style.opacity = '1';
+      veil.style.pointerEvents = 'auto';
+      modal.style.pointerEvents = 'auto';
+      document.body.style.overflow = 'hidden';
+    });
+    window.removeEventListener('scroll', onScroll);
+  }
+  function onScroll() {
+    // Trigger after user scrolls past ~1.2× viewport height (= roughly 1 page of designs)
+    if (window.scrollY > window.innerHeight * 1.2) show();
+  }
+  window.addEventListener('scroll', onScroll, { passive: true });
+  // Failsafe: also show after 30s on the page if they haven't scrolled
+  setTimeout(function(){ if (!shown) show(); }, 30000);
+})();
+</script>
+
+<script type="application/ld+json">${schema}</script>
+<script>
+// ── Infinite scroll — fetch successive /designs?page=N HTML pages, extract
+// just the design-card children, append to the grid when the sentinel
+// approaches the viewport. Single-flight + page cursor + done-state.
+(function(){
+  var grid = document.getElementById('catalog-grid');
+  var sentinel = document.getElementById('infinite-sentinel');
+  var statusEl = document.getElementById('infinite-status');
+  if (!grid || !sentinel || !statusEl) return;
+  var currentPage = parseInt(grid.dataset.currentPage || '1', 10);
+  var totalPages  = parseInt(grid.dataset.totalPages || '1', 10);
+  if (currentPage >= totalPages) return;  // already on last page
+  var loading = false;
+
+  function nextUrl() {
+    var u = new URL(window.location.href);
+    u.searchParams.set('page', String(currentPage + 1));
+    return u.toString();
+  }
+
+  function appendCards(html) {
+    var doc = new DOMParser().parseFromString(html, 'text/html');
+    var nextGrid = doc.getElementById('catalog-grid');
+    if (!nextGrid) return 0;
+    var added = 0;
+    Array.from(nextGrid.children).forEach(function(node){
+      grid.appendChild(node);
+      added++;
+    });
+    return added;
+  }
+
+  function loadMore() {
+    if (loading) return;
+    if (currentPage >= totalPages) {
+      statusEl.textContent = 'all ' + (grid.dataset.total||'') + ' designs loaded';
+      return;
+    }
+    loading = true;
+    statusEl.textContent = 'loading page ' + (currentPage + 1) + ' of ' + totalPages + '…';
+    fetch(nextUrl(), { credentials: 'same-origin' })
+      .then(function(r){ return r.text(); })
+      .then(function(html){
+        var n = appendCards(html);
+        currentPage++;
+        if (n === 0) {
+          // no more rows — stop polling
+          statusEl.textContent = 'all ' + (grid.dataset.total||'') + ' designs loaded';
+          io.disconnect();
+        } else if (currentPage >= totalPages) {
+          statusEl.textContent = 'all ' + (grid.dataset.total||'') + ' designs loaded';
+          io.disconnect();
+        } else {
+          statusEl.textContent = 'scroll for page ' + (currentPage + 1) + ' of ' + totalPages;
+        }
+        loading = false;
+      })
+      .catch(function(){ statusEl.textContent = 'load error — scroll to retry'; loading = false; });
+  }
+
+  var io = new IntersectionObserver(function(entries){
+    entries.forEach(function(e){ if (e.isIntersecting) loadMore(); });
+  }, { rootMargin: '600px 0px 600px 0px' });
+  io.observe(sentinel);
+})();
+</script>
+<script>
+(function(){
+  var slider = document.getElementById('density-slider');
+  var grid   = document.getElementById('catalog-grid');
+  var label  = document.getElementById('density-value');
+  var saved  = localStorage.getItem('wallco-density');
+  if (saved) slider.value = saved;
+  // Clamp inline --cols for narrow viewports so a desktop "density=8" pref
+  // doesn't crush mobile to 45px tiles. Inline style beats stylesheet
+  // specificity, so the @media (max-width: 600px) {--cols:2} rule otherwise
+  // gets ignored. clamp() also re-evaluates on resize via the listener below.
+  function applyCols(v){
+    var w = window.innerWidth, n = parseInt(v, 10);
+    if (w <= 600)      n = Math.min(n, 2);
+    else if (w <= 900) n = Math.min(n, 3);
+    grid.style.setProperty('--cols', String(n));
+    if (label) label.textContent = String(n);
+    slider.setAttribute('aria-valuenow', String(n));
+  }
+  applyCols(slider.value);
+  slider.addEventListener('input', function() {
+    applyCols(this.value);
+    localStorage.setItem('wallco-density', this.value);
+  });
+  window.addEventListener('resize', function(){ applyCols(slider.value); });
+
+  // Admin-only spacing slider — controls --grid-gap CSS var.
+  // Loosens visual tightness of the grid; persists in localStorage.
+  var spSlider = document.getElementById('spacing-slider');
+  var spLabel  = document.getElementById('spacing-value');
+  if (spSlider) {
+    var spSaved = localStorage.getItem('wallco-grid-gap');
+    if (spSaved !== null) spSlider.value = spSaved;
+    function applyGap(v) {
+      var n = parseInt(v, 10);
+      if (isNaN(n) || n < 0) n = 0;
+      grid.style.setProperty('--grid-gap', n + 'px');
+      if (spLabel) spLabel.textContent = n + 'px';
+      spSlider.setAttribute('aria-valuenow', String(n));
+    }
+    applyGap(spSlider.value);
+    spSlider.addEventListener('input', function() {
+      applyGap(this.value);
+      localStorage.setItem('wallco-grid-gap', this.value);
+    });
+  }
+
+  // Keyboard shortcut: "/" focuses search (GitHub/X/Slack pattern). Esc clears
+  // focus and returns to grid. Skipped when user is already typing in an input.
+  var search = document.getElementById('catalog-search-input');
+  if (search) {
+    document.addEventListener('keydown', function(e){
+      var t = e.target, tag = (t && t.tagName) || '';
+      var inField = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || (t && t.isContentEditable);
+      if (e.key === '/' && !inField) {
+        e.preventDefault();
+        search.focus();
+        search.select && search.select();
+      } else if (e.key === 'Escape' && document.activeElement === search) {
+        search.blur();
+      }
+    });
+  }
+  var sortSel = document.getElementById('sort-select');
+  var sortH   = document.getElementById('sort-hidden');
+  if (sortSel && sortH) {
+    var savedSort = localStorage.getItem('wallco-sort');
+    if (savedSort && savedSort !== '${sort}') {
+      sortH.value = savedSort;
+    }
+    sortSel.addEventListener('change', function() {
+      localStorage.setItem('wallco-sort', this.value);
+    });
+  }
+
+  // ── Favorites — heart button per card, "Favorites only" filter chip.
+  //    Persists in localStorage('wallco.favorites') as a JSON array of design ids.
+  (function favorites(){
+    var FKEY = 'wallco.favorites';
+    function readSet(){
+      try { return new Set(JSON.parse(localStorage.getItem(FKEY) || '[]').map(String)); }
+      catch(e) { return new Set(); }
+    }
+    function writeSet(set){
+      localStorage.setItem(FKEY, JSON.stringify(Array.from(set).map(Number)));
+    }
+    var favs = readSet();
+    function setBtnState(btn, isFav){
+      btn.setAttribute('aria-pressed', isFav ? 'true' : 'false');
+      btn.classList.toggle('is-fav', isFav);
+    }
+    function hydrateButtons(){
+      document.querySelectorAll('.fav-btn').forEach(function(btn){
+        var id = btn.getAttribute('data-fav-id');
+        setBtnState(btn, favs.has(id));
+      });
+      updateFavCount();
+    }
+    function updateFavCount(){
+      var n = favs.size;
+      var c = document.getElementById('fav-only-count');
+      if (c) c.textContent = '·' + n;
+      var extra = document.getElementById('fav-count-extra');
+      if (extra) extra.textContent = n ? '· ' + n + ' favorited' : '';
+    }
+    function applyFavFilter(showOnlyFavs){
+      var cards = document.querySelectorAll('.design-card[data-design-id]');
+      var anyVisible = false;
+      cards.forEach(function(card){
+        var id = card.getAttribute('data-design-id');
+        var match = !showOnlyFavs || favs.has(id);
+        card.style.display = match ? '' : 'none';
+        if (match) anyVisible = true;
+      });
+      var grid = document.getElementById('catalog-grid');
+      var empty = document.getElementById('fav-empty-state');
+      if (showOnlyFavs && !anyVisible) {
+        if (!empty && grid) {
+          empty = document.createElement('div');
+          empty.id = 'fav-empty-state';
+          empty.style.cssText = 'grid-column:1/-1;padding:48px 24px;text-align:center;color:var(--ink-soft,#666);font:14px/1.6 var(--sans,system-ui)';
+          empty.innerHTML = '<div style="font-size:32px;margin-bottom:8px">♡</div><strong>No favorites yet.</strong><br>Tap the heart on any design to save it here.';
+          grid.appendChild(empty);
+        }
+      } else if (empty) {
+        empty.remove();
+      }
+    }
+    document.addEventListener('click', function(e){
+      var btn = e.target.closest && e.target.closest('.fav-btn');
+      if (!btn) return;
+      e.preventDefault();
+      e.stopPropagation();
+      var id = btn.getAttribute('data-fav-id');
+      if (favs.has(id)) favs.delete(id); else favs.add(id);
+      writeSet(favs);
+      setBtnState(btn, favs.has(id));
+      updateFavCount();
+      var toggle = document.getElementById('fav-only-toggle');
+      if (toggle && toggle.getAttribute('aria-pressed') === 'true') {
+        applyFavFilter(true);
+      }
+    });
+    var favToggle = document.getElementById('fav-only-toggle');
+    if (favToggle) {
+      favToggle.addEventListener('click', function(){
+        var on = this.getAttribute('aria-pressed') !== 'true';
+        this.setAttribute('aria-pressed', on ? 'true' : 'false');
+        this.style.background = on ? 'var(--accent,#c9a14b)' : 'transparent';
+        this.style.color = on ? 'var(--bg,#fff)' : 'var(--ink-soft)';
+        this.style.borderColor = on ? 'var(--accent,#c9a14b)' : 'var(--line)';
+        this.innerHTML = (on ? '♥' : '♡') + ' Favorites <span id="fav-only-count" style="opacity:.55">·' + favs.size + '</span>';
+        applyFavFilter(on);
+      });
+    }
+    hydrateButtons();
+  })();
+
+  // Live search dropdown — hits /api/designs/search, falls back to form submit on Enter / no-JS.
+  var sIn = document.getElementById('catalog-search-input');
+  var sBox = document.getElementById('catalog-search-suggest');
+  if (sIn && sBox) {
+    var sTimer = null, sCtl = null, sIdx = -1, sItems = [];
+    var esc = function(s){ return String(s||'').replace(/[&<>"]/g, function(c){ return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]; }); };
+    var hide = function(){ sBox.style.display='none'; sIn.setAttribute('aria-expanded','false'); sIdx=-1; };
+    var render = function(results, qStr){
+      if (!results || !results.length) {
+        var dym = (window._didYouMean || []);
+        var dymRow = '';
+        if (dym.length) {
+          dymRow = '<div style="padding:10px 14px 12px;border-top:1px solid var(--line,#f0eee9);background:#fafaf7"><div style="font:10px var(--sans,system-ui);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-faint,#888);margin-bottom:6px">Did you mean</div><div style="display:flex;flex-wrap:wrap;gap:5px">'
+            + dym.slice(0,5).map(function(s){
+              return '<a href="/designs?q='+encodeURIComponent(s.word)+'" style="display:inline-flex;align-items:center;padding:4px 10px;font:11px var(--sans,system-ui);background:var(--bg,#fff);color:var(--ink,#111);border:1px solid var(--line,#e6e2d8);border-radius:999px;text-decoration:none">'+esc(s.word)+'</a>';
+            }).join('')
+            + '</div></div>';
+        }
+        sBox.innerHTML = '<div style="padding:14px 16px;font:13px var(--sans,system-ui);color:var(--ink-faint,#888)">No matches for &ldquo;'+esc(qStr)+'&rdquo;</div>' + dymRow;
+        sBox.style.display='block'; sIn.setAttribute('aria-expanded','true');
+        sItems = []; sIdx = -1;
+        return;
+      }
+      var savedSet = window._savedIds || null;
+      var rows = results.slice(0, 8).map(function(r, i){
+        var img = r.image_url ? '<div style="width:44px;height:44px;flex-shrink:0;background:#f2efe8 url('+JSON.stringify(r.image_url).slice(1,-1)+') center/cover;border-radius:4px;border:1px solid var(--line,#eee)"></div>' : '<div style="width:44px;height:44px;flex-shrink:0;background:#f2efe8;border-radius:4px"></div>';
+        var dot = r.dominant_hex ? '<span style="display:inline-block;width:9px;height:9px;border-radius:50%;background:'+esc(r.dominant_hex)+';border:1px solid rgba(0,0,0,.12);vertical-align:middle;margin-right:4px"></span>' : '';
+        var roomBadge = (r.room_mockups && r.room_mockups.length) ? '<span style="font:10px var(--sans,system-ui);color:var(--gold,#c9a14b);margin-left:6px">·room</span>' : '';
+        var heart = (savedSet && savedSet.has(r.id)) ? '<span title="Saved" style="font:11px var(--sans,system-ui);color:var(--gold,#c9a14b);margin-left:6px" aria-label="saved">♥</span>' : '';
+        return '<a href="'+esc(r.url)+'" role="option" data-idx="'+i+'" style="display:flex;align-items:center;gap:10px;padding:8px 12px;text-decoration:none;color:var(--ink,#111);border-bottom:1px solid var(--line,#f0eee9);font:13px var(--sans,system-ui)">'
+          + img
+          + '<div style="min-width:0;flex:1"><div style="font-weight:500;color:var(--ink,#111);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+esc(r.title||('Design #'+r.id))+heart+'</div>'
+          + '<div style="font:11px var(--sans,system-ui);color:var(--ink-faint,#888);margin-top:2px">'+dot+esc(r.category||'')+roomBadge+'</div></div>'
+        + '</a>';
+      }).join('');
+      var more = results.length > 8
+        ? '<a href="/designs?q='+encodeURIComponent(qStr)+'" style="display:block;padding:8px 12px;font:12px var(--sans,system-ui);color:var(--ink-soft,#555);text-align:center;background:var(--card-bg,#fafafa);text-decoration:none">See all '+results.length+' results →</a>'
+        : '';
+      // Refine chips — top motifs from the matched designs (additive, harmless if absent)
+      var facetRow = '';
+      if (window._facets && window._facets.motifs && window._facets.motifs.length) {
+        var chips = window._facets.motifs.slice(0, 6).map(function(f){
+          return '<a href="/designs?motif='+encodeURIComponent(f.motif)+'" style="display:inline-flex;align-items:center;gap:4px;padding:4px 10px;font:11px var(--sans,system-ui);background:transparent;color:var(--ink-soft,#555);border:1px solid var(--line,#e6e2d8);border-radius:999px;text-decoration:none;white-space:nowrap">'+esc(f.motif)+' <span style="color:var(--ink-faint,#999);font-size:10px">·'+f.count+'</span></a>';
+        }).join('');
+        facetRow = '<div style="display:flex;flex-wrap:wrap;gap:5px;padding:8px 12px;border-top:1px solid var(--line,#f0eee9);background:#fafaf7"><span style="font:10px var(--sans,system-ui);text-transform:uppercase;letter-spacing:.06em;color:var(--ink-faint,#999);align-self:center;margin-right:4px">Refine</span>'+chips+'</div>';
+      }
+      sBox.innerHTML = rows + facetRow + more;
+      sBox.style.display='block'; sIn.setAttribute('aria-expanded','true');
+      sItems = Array.prototype.slice.call(sBox.querySelectorAll('a[role=option]'));
+      sIdx = -1;
+    };
+    var fetchSuggest = function(qStr){
+      if (sCtl) sCtl.abort();
+      sCtl = new AbortController();
+      fetch('/api/designs/search?q='+encodeURIComponent(qStr)+'&limit=24', { signal: sCtl.signal })
+        .then(function(r){ return r.json(); })
+        .then(function(j){ window._facets = j.facets || null; window._didYouMean = j.did_you_mean || []; render(j.results||[], qStr); })
+        .catch(function(){ /* aborted or network — ignore */ });
+    };
+    // Lazy-fetch saved-ids once on first input so the dropdown can render ♥ on already-saved designs.
+    // Anonymous users get authenticated:false + empty ids; we cache the result either way.
+    var savedFetched = false;
+    function ensureSavedIds(){
+      if (savedFetched || window._savedIds) return;
+      savedFetched = true;
+      fetch('/api/me/saved', { credentials: 'same-origin' })
+        .then(function(r){ return r.json(); })
+        .then(function(j){ window._savedIds = new Set(j.ids || []); })
+        .catch(function(){ window._savedIds = new Set(); });
+    }
+    sIn.addEventListener('input', function(){
+      var v = this.value.trim();
+      if (sTimer) clearTimeout(sTimer);
+      if (v.length < 2) { hide(); return; }
+      ensureSavedIds();
+      sTimer = setTimeout(function(){ fetchSuggest(v); }, 180);
+    });
+    sIn.addEventListener('keydown', function(e){
+      if (e.key === 'Escape') { hide(); return; }
+      if (sBox.style.display === 'none' || !sItems.length) return;
+      if (e.key === 'ArrowDown') {
+        e.preventDefault();
+        sIdx = (sIdx + 1) % sItems.length;
+        sItems.forEach(function(a,i){ a.style.background = i===sIdx ? 'var(--card-bg,#fafafa)' : ''; });
+        sItems[sIdx].scrollIntoView({ block:'nearest' });
+      } else if (e.key === 'ArrowUp') {
+        e.preventDefault();
+        sIdx = (sIdx - 1 + sItems.length) % sItems.length;
+        sItems.forEach(function(a,i){ a.style.background = i===sIdx ? 'var(--card-bg,#fafafa)' : ''; });
+        sItems[sIdx].scrollIntoView({ block:'nearest' });
+      } else if (e.key === 'Enter' && sIdx >= 0) {
+        e.preventDefault();
+        window.location = sItems[sIdx].href;
+      }
+    });
+    document.addEventListener('click', function(e){
+      if (!sIn.contains(e.target) && !sBox.contains(e.target)) hide();
+    });
+    sIn.addEventListener('focus', function(){
+      if (this.value.trim().length >= 2 && sBox.innerHTML) {
+        sBox.style.display='block'; sIn.setAttribute('aria-expanded','true');
+      }
+    });
+  }
+})();
+</script>
+
+${_isAdmin ? `
+<style>
+  /* Review-mode controls (admin only — 127.0.0.1 / ?review=1) */
+  :root { --keep:#5fbf6e; --reject:#d2554a; --chip-bg:#3a3631; --chip-hot:#d2b15c; }
+  .design-card.review-card { display:block; background:transparent; border:1px solid #2a2825; border-radius:3px; overflow:hidden; transition: border-color .15s; padding:0; position:relative; }
+  /* Card IS the image. Meta + controls live in a slide-up flyout pinned to the
+     bottom of the image — no black void below the image when the card is idle.
+     Hover or focus-within expands the flyout; a pattern-# pill also surfaces
+     at top-left of the image on hover. */
+  .design-card.review-card .review-flyout {
+    position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
+    background: linear-gradient(to top, rgba(15,14,12,0.96) 0%, rgba(15,14,12,0.86) 70%, rgba(15,14,12,0) 100%);
+    color: #e8e2d6;
+    max-height: 0; overflow: hidden;
+    transition: max-height .26s ease;
+    pointer-events: none;
+  }
+  .design-card.review-card:hover .review-flyout,
+  .design-card.review-card:focus-within .review-flyout {
+    max-height: 300px;
+    pointer-events: auto;
+  }
+  /* Pattern # pill — visible only on hover, top-left of the image. */
+  .design-card.review-card .pattern-num {
+    position: absolute; top: 8px; left: 8px; z-index: 3;
+    padding: 3px 8px; font-size: 10px; font-weight: 600; letter-spacing: .06em;
+    background: rgba(15,14,12,0.78); color: #d2b15c;
+    border: 1px solid rgba(210,177,92,0.35); border-radius: 2px;
+    font-family: 'JetBrains Mono', Menlo, monospace;
+    opacity: 0; transform: translateY(-2px);
+    transition: opacity .18s ease, transform .18s ease;
+    pointer-events: none;
+  }
+  .design-card.review-card:hover .pattern-num,
+  .design-card.review-card:focus-within .pattern-num { opacity: 1; transform: translateY(0); }
+  /* Keep decision border + opacity dim visible regardless (so kept/rejected */
+  /* state shows at a glance without hovering) */
+  .design-card.review-card[data-decision]:not([data-decision=""]) .decision-badge { opacity:1 !important; }
+  .design-card.review-card[data-decision=keep]   { border-color: var(--keep); }
+  .design-card.review-card[data-decision=reject] { border-color: var(--reject); opacity:.55; }
+  .design-card.review-card .card-img { position:relative; display:block; aspect-ratio:1; background:#0d0c0a no-repeat center/cover; }
+  .design-card.review-card .card-img .decision-badge { position:absolute; top:8px; right:8px; padding:2px 8px; font-size:10px; border-radius:2px; text-transform:uppercase; letter-spacing:.06em; display:none; pointer-events:none; }
+  /* Admin-card hover actions — View / Sample buttons rendered inside the
+     image; hidden by default, appear on hover alongside the pattern-num pill.
+     Sits at bottom of the image; pointer-events on the wrapper let clicks
+     on the underlying link work, but the buttons re-enable their own. */
+  .design-card.review-card .admin-hover-actions {
+    position:absolute; left:8px; right:8px; bottom:8px; z-index:6;
+    display:flex; gap:6px; justify-content:center;
+    opacity:0; transition:opacity .15s ease;
+    pointer-events:none;
+  }
+  .design-card.review-card:hover .admin-hover-actions,
+  .design-card.review-card:focus-within .admin-hover-actions { opacity:1 }
+  .design-card.review-card .admin-hover-actions .chov-btn { pointer-events:auto; max-width:100px; font-size:10px; padding:5px 8px }
+  .design-card.review-card[data-decision=keep]   .decision-badge { display:inline-block; background:var(--keep); color:#0f0e0c; }
+  .design-card.review-card[data-decision=keep]   .decision-badge::after { content:'KEEP'; }
+  .design-card.review-card[data-decision=reject] .decision-badge { display:inline-block; background:var(--reject); color:#fff; }
+  .design-card.review-card[data-decision=reject] .decision-badge::after { content:'REJECT'; }
+  .design-card.review-card .card-meta { padding:10px 12px 6px; display:flex; flex-direction:column; gap:2px; }
+  .design-card.review-card .card-title { font-size:12px; line-height:1.3; color:#e8e2d6; max-height:2.6em; overflow:hidden; }
+  .design-card.review-card .card-cat   { font-size:10px; color:#888; }
+  .design-card.review-card .color-dot  { width:9px; height:9px; border-radius:50%; border:1px solid #00000060; display:inline-block; margin-right:5px; }
+  .review-controls { padding:0 12px 14px; }
+  .sliders { display:flex; flex-direction:column; gap:4px; margin: 8px 0 10px; }
+  .sliders label { display:grid; grid-template-columns:48px 1fr 22px; gap:6px; align-items:center; font-size:10px; color:#bba; text-transform:uppercase; letter-spacing:.06em; }
+  .sliders label span { text-align:right; color:#d2b15c; font-variant-numeric:tabular-nums; }
+  .sliders input[type=range] { width:100%; -webkit-appearance:none; height:2px; background:#3a3631; border-radius:2px; outline:none; }
+  .sliders input[type=range]::-webkit-slider-thumb { -webkit-appearance:none; width:11px; height:11px; border-radius:50%; background:#d2b15c; cursor:pointer; border:none; }
+  .decision { display:flex; gap:6px; margin-bottom:9px; }
+  .decision button { flex:1; padding:5px 8px; font-size:11px; border:1px solid #3a3631; background:#16140f; color:#bba; cursor:pointer; border-radius:2px; transition:all .12s; text-transform:uppercase; letter-spacing:.06em; }
+  .decision button:hover { background:#1f1c18; color:#fff; }
+  .design-card.review-card[data-decision=keep]   .btn-keep   { background:var(--keep);   color:#0f0e0c; border-color:var(--keep); }
+  .design-card.review-card[data-decision=reject] .btn-reject { background:var(--reject); color:#fff;    border-color:var(--reject); }
+  .decision .btn-pair { background:#16140f; color:#d2b15c; border-color:#3a3631; }
+  .decision .btn-pair:hover { background:#d2b15c; color:#0f0e0c; border-color:#d2b15c; }
+  /* Pair modal */
+  /* Right-pinned drawers — pair + chat. Side-by-side when both open. */
+  :root { --modal-pair-w: 480px; --modal-chat-w: 420px; }
+  #pair-modal {
+    position:fixed; top:0; right:0; bottom:0;
+    width: min(var(--modal-pair-w), 92vw);
+    background:#16140f; border-left: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), right .26s cubic-bezier(.4,0,.2,1);
+    visibility: hidden;
+  }
+  #pair-modal.open { transform: translateX(0); visibility: visible; }
+  /* Side-by-side when both right-pinned modals are open — chat slides left by pair's width */
+  body:has(#pair-modal.open):has(#chat-modal.open) #chat-modal { right: var(--modal-pair-w); }
+  /* When chat is to the left of pair, drop pair's left-shadow (it overlapped chat) and add a clean seam */
+  body:has(#chat-modal.open):has(#pair-modal.open) #pair-modal { box-shadow: none; border-left-color: #3a3631; }
+  body:has(#chat-modal.open):has(#pair-modal.open) #chat-modal { border-right: 1px solid #2a2825; }
+  /* Narrow viewport — can't fit both. Hide chat while pair is open; user toggles between them. */
+  @media (max-width: 920px) {
+    body:has(#pair-modal.open):has(#chat-modal.open) #chat-modal { right: 0; opacity: 0; visibility: hidden; pointer-events: none; }
+    #pair-modal { width: min(var(--modal-pair-w), 100vw); }
+    #chat-modal { width: min(var(--modal-chat-w), 100vw); }
+  }
+  #pair-modal header { padding:14px 18px; border-bottom:1px solid #2a2825; display:flex; gap:10px; align-items:center; }
+  #pair-modal header .ctx-main { flex:1; font-size:12px; color:#d2b15c; }
+  #pair-modal header .ctx-sub  { font-size:10px; color:#888; margin-top:2px; }
+  #pair-modal header .refresh,#pair-modal header .close { background:none; border:1px solid #3a3631; color:#bba; font-size:13px; cursor:pointer; padding:4px 9px; border-radius:2px; }
+  #pair-modal header .refresh:hover,#pair-modal header .close:hover { color:#fff; border-color:#d2b15c; }
+  #pair-modal header .close { font-size:18px; line-height:1; padding:2px 8px; }
+  #pair-body { flex:1; overflow-y:auto; padding:14px 18px; display:flex; flex-direction:column; gap:12px; }
+  #pair-body .pair-empty { color:#888; font-style:italic; padding:24px 0; text-align:center; }
+  #pair-body .pair-loading { color:#888; font-style:italic; padding:30px 0; text-align:center; }
+  .pair-card { display:flex; gap:12px; align-items:flex-start; padding:10px 12px; background:#1a1816; border:1px solid #2a2825; border-radius:3px; }
+  .pair-card .pair-thumb { flex:0 0 64px; width:64px; height:64px; border-radius:3px; background:#0d0c0a no-repeat center/cover; display:flex; align-items:center; justify-content:center; font-size:22px; color:#0f0e0c; }
+  .pair-card .pair-thumb.material { background:#2a2825; color:#d2b15c; font-size:22px; }
+  .pair-card .pair-body { flex:1; min-width:0; }
+  .pair-card .pair-title { font-size:12px; color:#e8e2d6; margin-bottom:3px; line-height:1.3; }
+  .pair-card .pair-type  { display:inline-block; font-size:9px; text-transform:uppercase; letter-spacing:.08em; padding:1px 6px; border-radius:2px; background:#2a2825; color:#d2b15c; margin-bottom:5px; }
+  .pair-card .pair-reason { font-size:11px; color:#bba; line-height:1.4; }
+  .pair-card .pair-hexcode { font-family: ui-monospace, monospace; font-size:10px; color:#888; margin-left:6px; }
+  .pair-card a.pair-link { color:inherit; text-decoration:none; display:flex; gap:12px; align-items:flex-start; flex:1; }
+  .pair-card a.pair-link:hover .pair-title { color:#d2b15c; }
+  .pair-card .pair-pin { background:none; border:1px solid #3a3631; color:#666; font-size:14px; cursor:pointer; padding:3px 8px; border-radius:14px; transition:all .12s; flex-shrink:0; align-self:flex-start; }
+  .pair-card .pair-pin:hover { color:#d2b15c; border-color:#d2b15c; }
+  .pair-card.pinned { border-color:var(--chip-hot); background:#1f1b13; }
+  .pair-card.pinned .pair-pin { color:#d2b15c; background:#d2b15c33; border-color:#d2b15c; }
+  /* Paint-chip variant — left thumb shows the chip color with brand code laid */
+  /* on top in mini-fan-deck style. Brand stripe accents on the left edge.    */
+  .pair-card.paint-card .paint-thumb { position:relative; box-shadow: inset 0 0 0 1px rgba(0,0,0,.18); }
+  .pair-card.paint-card .paint-code { position:absolute; left:4px; bottom:3px; font-family:ui-monospace,monospace; font-size:9px; padding:1px 4px; background:rgba(0,0,0,.55); color:#fff; border-radius:2px; letter-spacing:.04em; }
+  .pair-card.paint-card.paint-de { border-left:3px solid #c84a3a; }
+  .pair-card.paint-card.paint-sw { border-left:3px solid #1a4f9c; }
+  .pair-card .pair-vision-badge { display:inline-block; font-size:9px; padding:0 5px; border:1px solid #3a3631; border-radius:2px; color:#888; margin-left:6px; text-transform:uppercase; letter-spacing:.05em; }
+  #pair-vision-read { background:#1a1816; padding:8px 12px; margin:0 18px 8px; border:1px solid #2a2825; border-radius:2px; font-size:11px; color:#bba; line-height:1.45; font-style:italic; }
+  #pair-vision-read.hidden { display:none; }
+  .why-chip { display:none; padding:8px 12px; background:rgba(210,177,92,.08); border:1px solid #d2b15c; border-left:3px solid #d2b15c; border-radius:3px; font-size:12px; font-weight:500; color:#e8c46d; margin-bottom:10px; cursor:pointer; line-height:1.4; position:relative; }
+  .why-chip.visible { display:block; }
+  .why-chip.loading::after { content:' …'; opacity:.6; }
+  .why-chip::before { content:'CURATOR'; position:absolute; top:-8px; left:8px; padding:1px 6px; background:#0f0e0c; font:8px/1 ui-monospace, Menlo, monospace; color:#d2b15c; letter-spacing:.1em; }
+  .why-chip:hover { background:rgba(210,177,92,.14); border-color:#e8c46d; }
+  .why-chip:hover::after { content:'tap to open chat →'; position:absolute; right:8px; bottom:-1px; font:9px/1.5 -apple-system, system-ui, sans-serif; color:#888; font-weight:normal; font-style:italic; }
+  .chips { display:flex; flex-wrap:wrap; gap:5px; }
+  .chip { padding:3px 8px; background:var(--chip-bg); color:#bba; border-radius:10px; font-size:10px; cursor:pointer; border:1px solid transparent; transition:all .12s; }
+  .chip:hover { background:#4a443d; color:#fff; }
+  .chip.has-chat { border-color: var(--chip-hot); color: var(--chip-hot); }
+  /* chat side panel */
+  #chat-modal {
+    position:fixed; top:0; right:0; bottom:0;
+    width: min(var(--modal-chat-w), 92vw);
+    background:#16140f; border-left:1px solid #2a2825;
+    display:flex; flex-direction:column; z-index:1000;
+    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), right .26s cubic-bezier(.4,0,.2,1), opacity .2s;
+    visibility: hidden;
+  }
+  #chat-modal.open { transform: translateX(0); visibility: visible; }
+  #chat-modal header { padding:14px 18px; border-bottom:1px solid #2a2825; display:flex; gap:10px; align-items:center; }
+  #chat-modal header .ctx-main { flex:1; font-size:12px; color:#d2b15c; }
+  #chat-modal header .ctx-sub  { font-size:10px; color:#888; }
+  #chat-modal header .close { background:none; border:none; color:#999; font-size:22px; cursor:pointer; padding:0 4px; }
+  #chat-history { flex:1; overflow-y:auto; padding:16px 18px; display:flex; flex-direction:column; gap:10px; }
+  #chat-history .msg { padding:9px 12px; border-radius:10px; max-width:88%; font-size:12px; line-height:1.45; white-space:pre-wrap; word-wrap:break-word; }
+  #chat-history .msg.user { background:#2a2825; color:#fff; align-self:flex-end; }
+  #chat-history .msg.assistant { background:#1f1c18; color:#e8e2d6; align-self:flex-start; border:1px solid #2a2825; }
+  #chat-history .msg.assistant.loading { color:#888; font-style:italic; }
+  #chat-form { display:flex; padding:12px 14px; border-top:1px solid #2a2825; gap:8px; }
+  #chat-form input { flex:1; padding:8px 12px; background:#0f0e0c; border:1px solid #2a2825; color:#fff; font:inherit; border-radius:2px; outline:none; }
+  #chat-form input:focus { border-color:#d2b15c; }
+  #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; }
+  /* Hide the global wallco-chat FAB whenever the design-review chat /
+     pairings / isolate modals are open — otherwise the FAB sits on top
+     of the modal's Send button and intercepts pointer events. Caught by
+     Playwright review.spec.js #4. */
+  body:has(#chat-modal.open) #wallco-chat-fab,
+  body:has(#pair-modal.open) #wallco-chat-fab,
+  body:has(#iso-modal.open)  #wallco-chat-fab { display: none !important; }
+  .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-dominant-row { margin-bottom:14px; padding-bottom:14px; border-bottom:1px dashed #2a2825; }
+  .iso-dominant-card { display:flex; align-items:center; gap:14px; padding:10px 14px; background:#1a1816; border:1px solid #2a2825; border-radius:3px; }
+  .iso-dominant-card .dot { width:46px; height:46px; border-radius:3px; border:1px solid rgba(255,255,255,.15); flex-shrink:0; }
+  .iso-dominant-card .info { flex:1; min-width:0; }
+  .iso-dominant-card .info .hex { font:13px/1.2 ui-monospace, Menlo, monospace; color:#e8e2d6; letter-spacing:.02em; }
+  .iso-dominant-card .info .sw  { font:11px/1.3 -apple-system, system-ui, sans-serif; color:#d2b15c; margin-top:3px; }
+  .iso-dominant-card .info .sw .sw-name { color:#bba; }
+  .iso-dominant-card .info .nosw { font-size:11px; color:#666; font-style:italic; }
+  .iso-sw .sw-closeness { display:inline-block; margin-top:4px; padding:1px 6px; border-radius:8px; font:9px/1.2 ui-monospace, Menlo, monospace; text-transform:uppercase; letter-spacing:.05em; }
+  .sw-close-exact       { background:#1f3a1f; color:#5fbf6e; }
+  .sw-close-close       { background:#1f2f3a; color:#5fa8bf; }
+  .sw-close-approximate { background:#3a2e1f; color:#d2b15c; }
+  .sw-close-weak        { background:#3a1f1f; color:#d2554a; }
+  .iso-grid-wallpaper .iso-de.iso-de-exact       { background:rgba(95,191,110,.85); color:#0a1a0d; }
+  .iso-grid-wallpaper .iso-de.iso-de-close       { background:rgba(95,168,191,.85); color:#0d1a1f; }
+  .iso-grid-wallpaper .iso-de.iso-de-approximate { background:rgba(210,177,92,.85); color:#1f1a0d; }
+  .iso-grid-wallpaper .iso-de.iso-de-weak        { background:rgba(210,85,74,.75); color:#1f0d0d; }
+  .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">
+  <header>
+    <div style="flex:1">
+      <div class="ctx-main" id="chat-chip-label">chip</div>
+      <div class="ctx-sub" id="chat-design-label">design</div>
+    </div>
+    <button class="close" id="chat-close" title="Close (Esc)">&times;</button>
+  </header>
+  <div id="chat-history"></div>
+  <form id="chat-form">
+    <input type="text" id="chat-input" placeholder="Ask the panel about this chip…" autocomplete="off">
+    <button type="submit" id="chat-send">Send</button>
+  </form>
+</div>
+
+<div id="pair-modal">
+  <header>
+    <div style="flex:1">
+      <div class="ctx-main" id="pair-design-title">Pairs well with…</div>
+      <div class="ctx-sub" id="pair-design-sub"></div>
+    </div>
+    <button class="refresh" id="pair-refresh" title="Force regenerate (Ollama + LLaVA)">↻</button>
+    <button class="close" id="pair-close" title="Close (Esc)">&times;</button>
+  </header>
+  <div id="pair-vision-read" class="hidden"></div>
+  <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 id="iso-dominant" class="iso-dominant-row" style="display:none">
+      <h4 style="font-size:11px;color:#d2b15c;letter-spacing:.08em;text-transform:uppercase;margin:0 0 8px">Design's dominant color <span style="color:#666;font-weight:normal;text-transform:none;letter-spacing:0">(stored hex; may differ from extracted)</span></h4>
+      <div class="iso-dominant-card" id="iso-dominant-card"></div>
+    </div>
+    <div>
+      <h4 style="font-size:11px;color:#d2b15c;letter-spacing:.08em;text-transform:uppercase;margin:0 0 8px">Extracted palette <span style="color:#666;font-weight:normal;text-transform:none;letter-spacing:0">(top-6 from image pixels)</span></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 ──────────────────────────────────────────────────────
+  const REVIEWS = {};            // designId -> review object
+  const CHIPS = {};              // designId -> [{key,label,kind}]
+  let CURRENT_CHIP = null;
+
+  const $ = (s, el=document) => el.querySelector(s);
+  const $$ = (s, el=document) => Array.from(el.querySelectorAll(s));
+
+  // ── Boot: fetch reviews + chips, then wire UI ──────────────────
+  async function boot() {
+    try {
+      const r = await fetch('/api/reviews/all');
+      Object.assign(REVIEWS, await r.json());
+    } catch {}
+    $$('.design-card.review-card').forEach(card => {
+      applyReviewToCard(card);
+      wireCard(card);
+    });
+    loadChipsForVisibleCards();
+  }
+
+  function applyReviewToCard(card) {
+    const id = card.dataset.id;
+    const r = REVIEWS[id];
+    if (!r) return;
+    card.dataset.decision = r.decision || '';
+    const inputs = card.querySelectorAll('.sliders input[type=range]');
+    if (r.design != null) { inputs[0].value = r.design; inputs[0].nextElementSibling.textContent = r.design; }
+    if (r.color  != null) { inputs[1].value = r.color;  inputs[1].nextElementSibling.textContent = r.color;  }
+    if (r.style  != null) { inputs[2].value = r.style;  inputs[2].nextElementSibling.textContent = r.style;  }
+    if (r.why) {
+      const why = card.querySelector('.why-chip');
+      why.textContent = r.why;
+      why.classList.add('visible');
+    }
+  }
+
+  function wireCard(card) {
+    const id = card.dataset.id;
+    // sliders
+    card.querySelectorAll('.sliders input[type=range]').forEach(slider => {
+      slider.addEventListener('input', () => { slider.nextElementSibling.textContent = slider.value; });
+      slider.addEventListener('change', () => saveReview(card));
+    });
+    // K/R
+    card.querySelector('.btn-keep').addEventListener('click', e => {
+      e.preventDefault(); e.stopPropagation();
+      card.dataset.decision = card.dataset.decision === 'keep' ? '' : 'keep';
+      saveReview(card);
+    });
+    card.querySelector('.btn-reject').addEventListener('click', e => {
+      e.preventDefault(); e.stopPropagation();
+      card.dataset.decision = card.dataset.decision === 'reject' ? '' : 'reject';
+      saveReview(card);
+    });
+    // Pair button → open pairing modal
+    const pairBtn = card.querySelector('.btn-pair');
+    if (pairBtn) pairBtn.addEventListener('click', e => {
+      e.preventDefault(); e.stopPropagation();
+      openPairings(id, card.querySelector('.card-title').textContent || '#'+id);
+    });
+    // why-chip → chat
+    card.querySelector('.why-chip').addEventListener('click', () => {
+      const why = card.querySelector('.why-chip');
+      if (!why.classList.contains('visible')) return;
+      openChat(id, '_verdict', '"' + why.textContent + '"');
+    });
+  }
+
+  async function saveReview(card) {
+    const id = card.dataset.id;
+    const inputs = card.querySelectorAll('.sliders input[type=range]');
+    const payload = {
+      design: parseInt(inputs[0].value, 10),
+      color:  parseInt(inputs[1].value, 10),
+      style:  parseInt(inputs[2].value, 10),
+      decision: card.dataset.decision || null,
+    };
+    const why = card.querySelector('.why-chip');
+    if (payload.decision) { why.classList.add('visible', 'loading'); why.textContent = 'thinking'; }
+    else                  { why.classList.remove('visible', 'loading'); why.textContent = ''; }
+    try {
+      const r = await fetch('/api/review/' + id, {
+        method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload)
+      });
+      const j = await r.json();
+      REVIEWS[id] = j;
+      why.classList.remove('loading');
+      if (j.why) { why.textContent = j.why; why.classList.add('visible'); }
+    } catch (e) {
+      why.textContent = '(error: ' + e.message + ')';
+      why.classList.remove('loading');
+    }
+  }
+
+  async function loadChipsForVisibleCards() {
+    const cards = $$('.design-card.review-card');
+    const ids = cards.map(c => c.dataset.id).filter(id => !CHIPS[id]);
+    await Promise.all(ids.map(async id => {
+      try { const r = await fetch('/api/chips/' + id); const j = await r.json(); CHIPS[id] = j.chips || []; }
+      catch { CHIPS[id] = []; }
+    }));
+    cards.forEach(card => {
+      const id = card.dataset.id;
+      const chipsHTML = (CHIPS[id]||[]).map(c => '<span class="chip" data-chip="' + c.key + '">' + c.label + '</span>').join('');
+      card.querySelector('.chips').innerHTML = chipsHTML;
+      card.querySelectorAll('.chip').forEach(chip => {
+        chip.addEventListener('click', () => openChat(id, chip.dataset.chip, chip.textContent));
+      });
+    });
+  }
+
+  // ── Chat panel ─────────────────────────────────────────────────
+  const modal = $('#chat-modal');
+  const history = $('#chat-history');
+  const input = $('#chat-input');
+  const send  = $('#chat-send');
+  $('#chat-close').addEventListener('click', () => modal.classList.remove('open'));
+  document.addEventListener('keydown', e => { if (e.key === 'Escape' && modal.classList.contains('open')) modal.classList.remove('open'); });
+
+  async function openChat(designId, chipKey, chipLabel) {
+    CURRENT_CHIP = { designId, chipKey, chipLabel };
+    const card = document.querySelector('.design-card.review-card[data-id="' + designId + '"]');
+    const title = card ? (card.querySelector('.card-title').textContent || '#'+designId) : '#'+designId;
+    $('#chat-chip-label').textContent = chipLabel;
+    $('#chat-design-label').textContent = '#' + designId + ' · ' + title.slice(0,60);
+    history.innerHTML = '<div class="msg assistant loading">loading…</div>';
+    modal.classList.add('open');
+    setTimeout(() => input.focus(), 100);
+    try {
+      const r = await fetch('/api/chip/' + designId + '/' + encodeURIComponent(chipKey) + '/chat');
+      const j = await r.json();
+      renderHistory(j.history || []);
+    } catch (e) {
+      history.innerHTML = '<div class="msg assistant">load error: ' + e.message + '</div>';
+    }
+  }
+  function renderHistory(msgs) {
+    history.innerHTML = msgs.length
+      ? msgs.map(m => '<div class="msg ' + m.role + '">' + escapeHTML(m.content) + '</div>').join('')
+      : '<div class="msg assistant" style="opacity:.5">Ask the panel about this chip.</div>';
+    history.scrollTop = history.scrollHeight;
+  }
+  function escapeHTML(s) { return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":"&#39;"}[c])); }
+
+  $('#chat-form').addEventListener('submit', async e => {
+    e.preventDefault();
+    if (!CURRENT_CHIP) return;
+    const message = input.value.trim();
+    if (!message) return;
+    input.value = '';
+    send.disabled = true;
+    const userMsg = document.createElement('div'); userMsg.className = 'msg user'; userMsg.textContent = message;
+    const loading = document.createElement('div'); loading.className = 'msg assistant loading'; loading.textContent = 'thinking…';
+    const first = history.firstElementChild;
+    if (first && first.style && first.style.opacity) history.innerHTML = '';
+    history.appendChild(userMsg); history.appendChild(loading);
+    history.scrollTop = history.scrollHeight;
+    try {
+      const r = await fetch('/api/chip/' + CURRENT_CHIP.designId + '/' + encodeURIComponent(CURRENT_CHIP.chipKey) + '/chat', {
+        method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ message })
+      });
+      const j = await r.json();
+      loading.classList.remove('loading');
+      loading.textContent = j.reply || '(no reply)';
+      document.querySelectorAll('.design-card.review-card[data-id="' + CURRENT_CHIP.designId + '"] .chip[data-chip="' + CURRENT_CHIP.chipKey + '"]').forEach(c => c.classList.add('has-chat'));
+    } catch (e) {
+      loading.classList.remove('loading');
+      loading.textContent = 'error: ' + e.message;
+    }
+    send.disabled = false; input.focus();
+  });
+
+  // ── Pair modal ─────────────────────────────────────────────────
+  const pairModal = $('#pair-modal');
+  const pairBody = $('#pair-body');
+  let CURRENT_PAIR_ID = null;
+  let CURRENT_PAIR_TITLE = '';
+  $('#pair-close').addEventListener('click', () => pairModal.classList.remove('open'));
+  $('#pair-refresh').addEventListener('click', () => { if (CURRENT_PAIR_ID) loadPairings(CURRENT_PAIR_ID, CURRENT_PAIR_TITLE, true); });
+  document.addEventListener('keydown', e => { if (e.key === 'Escape' && pairModal.classList.contains('open')) pairModal.classList.remove('open'); });
+
+  function openPairings(designId, title) { loadPairings(designId, title, false); }
+
+  async function loadPairings(designId, title, forceRefresh) {
+    CURRENT_PAIR_ID = designId; CURRENT_PAIR_TITLE = title;
+    $('#pair-design-title').textContent = 'Pairs well with this design';
+    $('#pair-design-sub').textContent = '#' + designId + ' · ' + title.slice(0,80);
+    $('#pair-vision-read').classList.add('hidden');
+    $('#pair-vision-read').textContent = '';
+    pairBody.innerHTML = '<div class="pair-loading">' + (forceRefresh ? 'regenerating (vision + Ollama, ~10s)…' : 'loading…') + '</div>';
+    pairModal.classList.add('open');
+    try {
+      const url = '/api/pairings/' + designId + (forceRefresh ? '/refresh' : '');
+      const r = await fetch(url, { method: forceRefresh ? 'POST' : 'GET' });
+      const j = await r.json();
+      if (j.vision_description) {
+        const vr = $('#pair-vision-read');
+        vr.textContent = '👁  ' + j.vision_description;
+        vr.classList.remove('hidden');
+      }
+      renderPairings(j);
+    } catch (e) {
+      pairBody.innerHTML = '<div class="pair-empty">error loading: ' + e.message + '</div>';
+    }
+  }
+
+  async function togglePin(designId, suggestion, cardEl) {
+    cardEl.classList.toggle('pinned');
+    try {
+      await fetch('/api/moodboard/' + designId + '/pin', {
+        method: 'POST', headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({ pin_key: suggestion.pin_key, suggestion })
+      });
+    } catch (e) {
+      cardEl.classList.toggle('pinned'); // revert on error
+    }
+  }
+
+  function renderPairings(data) {
+    if (!data.suggestions || !data.suggestions.length) {
+      pairBody.innerHTML = '<div class="pair-empty">No suggestions available.</div>';
+      return;
+    }
+    pairBody.innerHTML = data.suggestions.map(s => {
+      const pinned = s.pinned ? ' pinned' : '';
+      const pinTitle = s.pinned ? 'Unpin from moodboard' : 'Pin to moodboard';
+      const visionBadge = s.vision_grounded ? '<span class="pair-vision-badge" title="Grounded in LLaVA visual read">👁 vision</span>' : '';
+      const pinBtn = '<button class="pair-pin" type="button" data-pin-key="' + escapeAttr(s.pin_key) + '" title="' + pinTitle + '">★</button>';
+      if (s.type === 'design') {
+        const thumb = s.thumb_url
+          ? '<div class="pair-thumb" style="background-image:url('+s.thumb_url+')"></div>'
+          : '<div class="pair-thumb material">◧</div>';
+        return '<div class="pair-card' + pinned + '" data-pin-key="' + escapeAttr(s.pin_key) + '">' +
+               '<a class="pair-link" href="/design/' + s.design_id + '" target="_blank" rel="noopener noreferrer">' + thumb +
+               '<div class="pair-body"><span class="pair-type">design · #' + s.design_id + '</span>' +
+               '<div class="pair-title">' + escapeHTML(s.label) + (s.hex ? '<span class="pair-hexcode">'+s.hex+'</span>' : '') + '</div>' +
+               '<div class="pair-reason">' + escapeHTML(s.reason) + '</div></div></a>' + pinBtn + '</div>';
+      }
+      if (s.type === 'color') {
+        return '<div class="pair-card' + pinned + '" data-pin-key="' + escapeAttr(s.pin_key) + '">' +
+               '<div class="pair-thumb" style="background:' + (s.hex||'#888') + '"></div>' +
+               '<div class="pair-body"><span class="pair-type">color swatch</span>' +
+               '<div class="pair-title">' + escapeHTML(s.label) + '</div>' +
+               '<div class="pair-reason">' + escapeHTML(s.reason) + '</div></div>' + pinBtn + '</div>';
+      }
+      if (s.type === 'paint') {
+        // Paint chip — left half is the chip color block with the brand-style
+        // label tucked underneath. Lets the room be specified end-to-end
+        // (wallcovering + matching wall paint).
+        const brandClass = (s.brand || '').toLowerCase().includes('dunn') ? 'paint-de' : 'paint-sw';
+        return '<div class="pair-card paint-card ' + brandClass + pinned + '" data-pin-key="' + escapeAttr(s.pin_key) + '">' +
+               '<div class="pair-thumb paint-thumb" style="background:' + (s.hex||'#888') + '">' +
+                 '<span class="paint-code">' + escapeHTML(s.code||'') + '</span></div>' +
+               '<div class="pair-body"><span class="pair-type">paint · ' + escapeHTML(s.brand||'') + '</span>' +
+               '<div class="pair-title">' + escapeHTML(s.name||'') + '<span class="pair-hexcode">'+escapeHTML(s.hex||'')+'</span></div>' +
+               '<div class="pair-reason">' + escapeHTML(s.reason||'') + '</div></div>' + pinBtn + '</div>';
+      }
+      // material / object
+      return '<div class="pair-card' + pinned + '" data-pin-key="' + escapeAttr(s.pin_key) + '">' +
+             '<div class="pair-thumb material">' + (s.icon || '✦') + '</div>' +
+             '<div class="pair-body"><span class="pair-type">material' + visionBadge + '</span>' +
+             '<div class="pair-title">' + escapeHTML(s.label) + '</div>' +
+             '<div class="pair-reason">' + escapeHTML(s.reason) + '</div></div>' + pinBtn + '</div>';
+    }).join('') + '<div style="text-align:right; font-size:10px; color:#666; margin-top:6px">generated ' + (data.generated_at ? new Date(data.generated_at).toLocaleString() : '—') + '</div>';
+    // wire pin buttons
+    pairBody.querySelectorAll('.pair-pin').forEach(btn => {
+      btn.addEventListener('click', e => {
+        e.preventDefault(); e.stopPropagation();
+        const pinKey = btn.dataset.pinKey;
+        const suggestion = data.suggestions.find(s => s.pin_key === pinKey);
+        const card = btn.closest('.pair-card');
+        if (suggestion && card) togglePin(CURRENT_PAIR_ID, suggestion, card);
+      });
+    });
+  }
+  function escapeAttr(s) { return String(s||'').replace(/"/g, '&quot;').replace(/</g,'&lt;'); }
+
+  // ── Keyboard shortcuts ─────────────────────────────────────────
+  //   K/R  → keep/reject focused card
+  //   P    → open Pair modal for focused card
+  //   ←/→  → navigate cards left/right
+  //   1-9  → set focused slider to that value (Design when no slider focused; else current slider)
+  //   ?    → show keymap toast
+  let FOCUSED_CARD = null;
+  function setFocused(card) {
+    if (FOCUSED_CARD === card) return;
+    if (FOCUSED_CARD) FOCUSED_CARD.classList.remove('kbd-focus');
+    FOCUSED_CARD = card;
+    if (card) {
+      card.classList.add('kbd-focus');
+      card.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
+    }
+  }
+  // mouse hover sets focus
+  document.addEventListener('mouseover', e => {
+    const c = e.target.closest('.design-card.review-card');
+    if (c) setFocused(c);
+  });
+
+  function showToast(msg, ms = 1800) {
+    let t = document.getElementById('kbd-toast');
+    if (!t) {
+      t = document.createElement('div');
+      t.id = 'kbd-toast';
+      t.style.cssText = 'position:fixed;bottom:24px;left:50%;transform:translateX(-50%);padding:8px 16px;background:#1a1816;border:1px solid #d2b15c;border-radius:3px;color:#d2b15c;font:12px/1.4 -apple-system,system-ui,sans-serif;z-index:9999;box-shadow:0 4px 12px rgba(0,0,0,.4);transition:opacity .2s;';
+      document.body.appendChild(t);
+    }
+    t.textContent = msg;
+    t.style.opacity = '1';
+    clearTimeout(t._fade);
+    t._fade = setTimeout(() => { t.style.opacity = '0'; }, ms);
+  }
+
+  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, #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;
+    const key = e.key.toLowerCase();
+    if (key === 'k' && FOCUSED_CARD) {
+      e.preventDefault();
+      FOCUSED_CARD.querySelector('.btn-keep').click();
+      showToast('Keep — ' + (FOCUSED_CARD.querySelector('.card-title').textContent || '').slice(0,40));
+    } else if (key === 'r' && FOCUSED_CARD) {
+      e.preventDefault();
+      FOCUSED_CARD.querySelector('.btn-reject').click();
+      showToast('Reject — ' + (FOCUSED_CARD.querySelector('.card-title').textContent || '').slice(0,40));
+    } else if (key === 'p' && FOCUSED_CARD) {
+      e.preventDefault();
+      FOCUSED_CARD.querySelector('.btn-pair').click();
+    } else if (key === 'arrowright' || key === 'arrowdown') {
+      e.preventDefault();
+      setFocused(cards[Math.min(cards.length - 1, idx + 1)]);
+    } else if (key === 'arrowleft' || key === 'arrowup') {
+      e.preventDefault();
+      setFocused(cards[Math.max(0, idx - 1)]);
+    } else if (/^[1-9]$/.test(e.key) && FOCUSED_CARD) {
+      e.preventDefault();
+      const designSlider = FOCUSED_CARD.querySelector('.sliders input[type=range][data-axis=design]');
+      designSlider.value = e.key;
+      designSlider.dispatchEvent(new Event('input'));
+      designSlider.dispatchEvent(new Event('change'));
+      showToast('Design score = ' + e.key);
+    } else if (key === '?' || (e.shiftKey && key === '/')) {
+      e.preventDefault();
+      showToast('K=keep · R=reject · P=pair · ←→=nav · 1-9=design score · Esc=close', 4000);
+    }
+  });
+  // Add focus ring style
+  const kbdStyle = document.createElement('style');
+  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(); });
+  // Isolate-modal-only shortcuts: W=wallpaper, P=paint, C=copy primary hex, ?=help
+  // Scoped to when modal is open; doesn't conflict with global K/R/P card handler
+  // because that one returns early when any modal is open (line ~1714).
+  document.addEventListener('keydown', e => {
+    if (!isoModal.classList.contains('open')) return;
+    if (e.target.matches('input, textarea, select')) return;
+    const key = e.key.toLowerCase();
+    if (key === 'w' || key === 'p') {
+      const toggle = $('#iso-mode-toggle');
+      if (!toggle.classList.contains('visible')) {
+        showToast('Click "Isolate this design" first');
+        return;
+      }
+      e.preventDefault();
+      const mode = key === 'w' ? 'wallpaper' : 'paint';
+      const btn = toggle.querySelector('button[data-mode="' + mode + '"]');
+      if (btn) { btn.click(); showToast(mode === 'wallpaper' ? 'Wallpaper mode' : 'Paint mode'); }
+    } else if (key === 'c') {
+      e.preventDefault();
+      const hex = CURRENT_ISO_DATA && CURRENT_ISO_DATA.palette && CURRENT_ISO_DATA.palette[0] && CURRENT_ISO_DATA.palette[0].hex;
+      if (!hex) { showToast('No palette yet'); return; }
+      navigator.clipboard.writeText(hex).then(() => showToast('Copied ' + hex)).catch(() => showToast('Copy failed'));
+    } else if (key === '?' || (e.shiftKey && key === '/')) {
+      e.preventDefault();
+      showToast('W=wallpaper · P=paint · C=copy primary hex · Esc=close', 4000);
+    }
+  });
+
+  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-dominant').style.display = 'none';
+    $('#iso-dominant-card').innerHTML = '';
+    $('#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;
+      renderIsoDominant(data.design, data.dominant_sw);
+      renderIsoPalette(data.palette || []);
+    } catch (e) {
+      $('#iso-palette').innerHTML = '<div class="iso-loading">error: ' + escapeHTML(e.message) + '</div>';
+    }
+  }
+
+  // R5 — render the design's STORED dominant hex (canonical, persisted in DB)
+  // alongside its nearest SW. Sits ABOVE the extracted-palette block so the user
+  // sees the source-of-truth color and how it compares to what extractPalette found.
+  function renderIsoDominant(design, dominantSW) {
+    const card = $('#iso-dominant-card');
+    if (!design || !design.dominant_hex) {
+      $('#iso-dominant').style.display = 'none';
+      return;
+    }
+    const hex = design.dominant_hex;
+    const closenessChip = (dominantSW && dominantSW.closeness)
+      ? ' <span class="sw-closeness sw-close-' + dominantSW.closeness.band + '" title="' + escapeAttr(dominantSW.closeness.note) + '">' + dominantSW.closeness.band + '</span>'
+      : '';
+    const swInfo = dominantSW
+      ? '<div class="sw"><strong>' + escapeHTML(dominantSW.code) + '</strong> <span class="sw-name">' + escapeHTML(dominantSW.name) + '</span> · ΔE ' + dominantSW.deltaE + closenessChip + '</div>'
+      : '<div class="nosw">no Sherwin-Williams match available</div>';
+    card.innerHTML =
+      '<div class="dot" style="background:' + escapeAttr(hex) + '"></div>' +
+      '<div class="info">' +
+        '<div class="hex">' + escapeHTML(hex) + '</div>' +
+        swInfo +
+      '</div>';
+    $('#iso-dominant').style.display = 'block';
+  }
+
+  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 closenessChip = (sw && sw.closeness)
+        ? '<div class="sw-closeness sw-close-' + sw.closeness.band + '" title="ΔE ' + sw.deltaE + ' — ' + escapeAttr(sw.closeness.note) + '">' + escapeHTML(sw.closeness.band) + '</div>'
+        : '';
+      const swBlock = sw
+        ? '<div class="sw-code">' + escapeHTML(sw.code) + '</div>' +
+          '<div class="sw-name">' + escapeHTML(sw.name) + '</div>' +
+          closenessChip
+        : '<div class="sw-empty">—</div>';
+      return '<div class="iso-sw" title="' + escapeAttr(hex) + (sw ? ' · ' + escapeAttr(sw.code + ' ' + sw.name + ' · ΔE ' + sw.deltaE) : '') + '">' +
+             '<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 closenessTxt = d.closeness ? ' (' + d.closeness.band + ')' : '';
+      const t = escapeAttr((d.title || '#' + d.id) + ' · ΔE ' + (d.deltaE != null ? d.deltaE : '?') + closenessTxt);
+      const bandClass = d.closeness ? ' iso-de-' + d.closeness.band : '';
+      return '<a href="/design/' + d.id + '" target="_blank" title="' + t + '" style="background-image:url(' + img + ')" rel="noopener noreferrer">' +
+             '<span class="iso-de' + bandClass + '">Δ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>
+` : ''}
+${HAMBURGER_JS}
+</body>
+</html>`);
+});
+
+// ── DESIGN DETAIL not-found page (shared between bad-id and missing-id branches)
+function renderDesignNotFound(req, idForCanonical) {
+  // Hydrate 4 designs for the recovery row. Match /designs filter exactly —
+  // it surfaces every design with an image_url, regardless of is_published.
+  // Earlier filter on is_published !== false zeroed out the row in prod because
+  // every loaded design currently ships with is_published: false.
+  let pickups = [];
+  try {
+    pickups = DESIGNS
+      .filter(d => d && d.image_url)
+      .slice(0, 200)
+      .sort(() => Math.random() - 0.5)
+      .slice(0, 4);
+  } catch {}
+  const pickupHtml = pickups.map(d => `
+    <a class="nf-pick" href="/design/${d.id}" aria-label="${d.title || 'Design ' + d.id}">
+      <div class="nf-pick-img" style="background-image:url('${d.image_url}')"></div>
+      <div class="nf-pick-meta">
+        <div class="nf-pick-title">${(d.title || 'Untitled').slice(0, 40)}</div>
+        <div class="nf-pick-cat">${d.category || ''}</div>
+      </div>
+    </a>`).join('');
+  const safePath = String(req.params.id || '').replace(/[<>"'&]/g, '').slice(0, 60);
+  // Use the first recovery thumb as the og:image so social shares of a 404 still
+  // render a real wallpaper preview (Canva-lens fix from the debate panel).
+  // Falls back to /og-default.png if no published designs are loaded yet.
+  const ogPick = (pickups[0] && pickups[0].image_url)
+    ? (pickups[0].image_url.startsWith('http') ? pickups[0].image_url : `https://wallco.ai${pickups[0].image_url}`)
+    : '';
+  return `${htmlHead({
+    title: 'Design not found — wallco.ai',
+    description: 'The requested design isn’t in our archive. Browse the live collection.',
+    canonical: `https://wallco.ai/design/${idForCanonical || safePath}`,
+    ogImage: ogPick
+  })}
+<style>
+  .nf {
+    min-height: calc(100vh - 120px);
+    padding: 140px 32px 80px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    background: var(--bg);
+  }
+  .nf-inner {
+    max-width: 720px;
+    text-align: center;
+    margin-bottom: 80px;
+  }
+  .nf-numeral {
+    font-family: var(--serif);
+    font-style: italic;
+    font-weight: 400;
+    font-size: clamp(140px, 22vw, 280px);
+    line-height: 0.85;
+    letter-spacing: -0.06em;
+    color: var(--gold);
+    opacity: 1;
+    margin-bottom: 24px;
+    user-select: none;
+  }
+  .nf-rule {
+    width: 48px;
+    height: 1px;
+    background: var(--ink);
+    margin: 12px auto 28px;
+    opacity: 0.5;
+  }
+  .nf-title {
+    font-family: var(--serif);
+    font-weight: 300;
+    font-size: clamp(28px, 4vw, 44px);
+    line-height: 1.15;
+    letter-spacing: -0.01em;
+    color: var(--ink);
+    margin: 0 0 18px;
+  }
+  .nf-meta {
+    font-family: var(--sans);
+    font-size: 13px;
+    letter-spacing: 0.06em;
+    text-transform: uppercase;
+    color: var(--ink-faint);
+    margin: 0 0 22px;
+  }
+  .nf-path {
+    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+    text-transform: none;
+    letter-spacing: 0;
+    padding: 2px 0 3px;
+    border-bottom: 1px dotted var(--line);
+    color: var(--ink-soft);
+  }
+  .nf-body {
+    font-family: var(--serif);
+    font-weight: 400;
+    font-size: 18px;
+    line-height: 1.6;
+    color: var(--ink-soft);
+    max-width: 460px;
+    margin: 0 auto 36px;
+  }
+  .nf-cta {
+    display: inline-block;
+    font-family: var(--serif);
+    font-style: italic;
+    font-size: 18px;
+    color: var(--ink);
+    text-decoration: none;
+    padding-bottom: 4px;
+    border-bottom: 1px solid var(--gold);
+    transition: color 0.2s, border-color 0.2s;
+  }
+  .nf-cta:hover { color: var(--gold); border-bottom-color: var(--ink); }
+  .nf-pickup {
+    width: 100%;
+    max-width: 1200px;
+    border-top: 1px solid var(--line);
+    padding-top: 40px;
+  }
+  .nf-pickup-label {
+    font-family: var(--sans);
+    font-size: 12px;
+    letter-spacing: 0.22em;
+    text-transform: uppercase;
+    color: var(--ink-soft);
+    font-weight: 500;
+    text-align: center;
+    margin-bottom: 28px;
+  }
+  .nf-pickup-row {
+    display: grid;
+    grid-template-columns: repeat(4, 1fr);
+    gap: 24px;
+  }
+  .nf-pick {
+    text-decoration: none;
+    color: var(--ink);
+    display: block;
+  }
+  .nf-pick-img {
+    aspect-ratio: 1 / 1;
+    background-size: cover;
+    background-position: center;
+    background-color: var(--card-bg);
+    margin-bottom: 12px;
+    border: 1px solid var(--line);
+    transition: transform 0.4s ease;
+  }
+  .nf-pick:hover .nf-pick-img { transform: scale(1.02); }
+  .nf-pick-title {
+    font-family: var(--serif);
+    font-weight: 400;
+    font-size: 15px;
+    line-height: 1.3;
+    color: var(--ink);
+  }
+  .nf-pick-cat {
+    font-family: var(--sans);
+    font-size: 11px;
+    letter-spacing: 0.1em;
+    text-transform: uppercase;
+    color: var(--ink-faint);
+    margin-top: 4px;
+  }
+  @media (max-width: 720px) {
+    .nf { padding: 100px 20px 60px; }
+    .nf-pickup-row { grid-template-columns: repeat(2, 1fr); gap: 16px; }
+  }
+</style>
+<body>
+${htmlHeader('')}
+<main class="nf">
+  <div class="nf-inner">
+    <div class="nf-numeral">404</div>
+    <div class="nf-rule"></div>
+    <h1 class="nf-title">No design at this address</h1>
+    <p class="nf-meta"><span class="nf-path">/design/${safePath}</span></p>
+    <p class="nf-body">That ID isn’t in the wallco.ai archive—either retired during a recent migration or never minted. Browse the live collection below.</p>
+    <a class="nf-cta" href="/designs">Return to the collection →</a>
+  </div>
+  ${pickups.length ? `<div class="nf-pickup">
+    <div class="nf-pickup-label">Currently in rotation</div>
+    <div class="nf-pickup-row">${pickupHtml}</div>
+  </div>` : ''}
+</main>
+${FOOTER}
+${HAMBURGER_JS}
+${req.query.figcap === '1' ? '<script src="https://mcp.figma.com/mcp/html-to-design/capture.js" async></script>' : ''}
+</body>
+</html>`;
+}
+
+// ── DESIGN DETAIL
+// ── design_ratings: admin scoring + RLHF-lite for the generator
+// One row per (design, version) so we can see how the LLM improved over retries.
+function ensureDesignRatingsTable() {
+  try {
+    psqlQuery(`CREATE TABLE IF NOT EXISTS design_ratings (
+      id BIGSERIAL PRIMARY KEY,
+      design_id BIGINT NOT NULL,
+      color_score INT NOT NULL CHECK (color_score BETWEEN 1 AND 10),
+      style_score INT NOT NULL CHECK (style_score BETWEEN 1 AND 10),
+      composition_score INT NOT NULL CHECK (composition_score BETWEEN 1 AND 10),
+      spacing INT,                  -- 0..100 (50 = neutral)
+      invert BOOLEAN DEFAULT FALSE,
+      notes TEXT,
+      rated_by TEXT DEFAULT 'admin',
+      retry_count INT DEFAULT 0,    -- bumped when this rating triggers a retry
+      child_design_id BIGINT,       -- set when retry produces a new design
+      created_at TIMESTAMP DEFAULT NOW()
+    );`);
+    psqlQuery(`CREATE INDEX IF NOT EXISTS design_ratings_design_idx ON design_ratings(design_id, created_at DESC);`);
+  } catch (e) { console.error('[ratings] table init failed:', e.message); }
+}
+ensureDesignRatingsTable();
+
+// ── studio_renders: per-session log of every /studio render (R0c)
+// Steve ask 2026-05-12: "always save every render but give option to delete".
+// One row per compose call, soft-delete via deleted_at (file kept 7 days for undo).
+function ensureStudioRendersTable() {
+  try {
+    psqlQuery(`CREATE TABLE IF NOT EXISTS studio_renders (
+      id BIGSERIAL PRIMARY KEY,
+      session_uuid TEXT NOT NULL,
+      design_id BIGINT,                   -- FK to spoon_all_designs.id
+      room_id BIGINT,                     -- FK to wallco_rooms.id (nullable until R0b lands)
+      prompt TEXT,
+      pattern_hash TEXT,                  -- sha256 of the pattern PNG (dedup key)
+      created_at TIMESTAMP DEFAULT NOW(),
+      deleted_at TIMESTAMP NULL           -- soft-delete; null = live
+    );`);
+    psqlQuery(`CREATE INDEX IF NOT EXISTS studio_renders_session_idx
+                 ON studio_renders(session_uuid, created_at DESC);`);
+    psqlQuery(`CREATE INDEX IF NOT EXISTS studio_renders_design_idx
+                 ON studio_renders(design_id);`);
+  } catch (e) { console.error('[studio_renders] init failed:', e.message); }
+}
+ensureStudioRendersTable();
+
+function adminRatingGate(req, res) {
+  const _isAdmin = isAdmin(req);
+  if (!_isAdmin) { res.status(403).json({ error: 'admin only' }); return false; }
+  return true;
+}
+
+// POST /api/design/:id/rate  { color, style, composition, spacing?, invert?, notes? }
+app.post('/api/design/:id/rate', express.json({ limit: '128kb' }), (req, res) => {
+  if (!adminRatingGate(req, res)) return;
+  const id = parseInt(req.params.id, 10);
+  const { color, style, composition, spacing, invert, notes } = req.body || {};
+  const c = Number(color), s = Number(style), m = Number(composition);
+  if (![c, s, m].every(n => Number.isFinite(n) && n >= 1 && n <= 10)) {
+    return res.status(400).json({ error: 'color/style/composition must each be 1..10' });
+  }
+  const sp = Number.isFinite(Number(spacing)) ? Math.max(0, Math.min(100, Number(spacing))) : 50;
+  try {
+    const out = psqlQuery(`INSERT INTO design_ratings
+      (design_id, color_score, style_score, composition_score, spacing, invert, notes)
+      VALUES (${id}, ${c}, ${s}, ${m}, ${sp}, ${invert ? 'TRUE' : 'FALSE'}, ${pgEsc(notes || null)})
+      RETURNING id;`);
+    res.json({ ok: true, rating_id: parseInt(out, 10) });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// GET /api/design/:id/ratings — full history (admin reads override stack)
+app.get('/api/design/:id/ratings', (req, res) => {
+  const id = parseInt(req.params.id, 10);
+  try {
+    const raw = psqlQuery(`SELECT COALESCE(json_agg(t ORDER BY t.created_at DESC),'[]'::json) FROM (
+      SELECT id, color_score, style_score, composition_score, spacing, invert, notes,
+             rated_by, retry_count, child_design_id, created_at
+      FROM design_ratings WHERE design_id = ${id}) t;`);
+    res.json({ ratings: JSON.parse(raw || '[]') });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// Build prompt nudges from the rating — the LLM uses these on retry.
+function nudgesFromRating(r) {
+  const out = [];
+  if (r.color_score <= 4)      out.push('refined palette, tonal harmony, no clashing hues');
+  else if (r.color_score >= 8) out.push('preserve color palette tightly');
+  if (r.style_score <= 4)      out.push('cohesive single-era style statement, commit to one tradition');
+  else if (r.style_score >= 8) out.push('preserve stylistic direction');
+  if (r.composition_score <= 4) out.push('rebalance composition, clearer focal point, deliberate negative space');
+  else if (r.composition_score >= 8) out.push('preserve composition');
+  const sp = Number(r.spacing ?? 50);
+  if (sp <= 35)      out.push('tighter element packing, denser repeat');
+  else if (sp >= 65) out.push('more breathing room between motifs, generous spacing');
+  if (r.invert) out.push('invert dominant and recessive palette roles');
+  return out;
+}
+
+// POST /api/design/:id/retry — kicks a fresh generation using the latest
+// rating as RLHF-lite feedback. The new design lands as a normal row in
+// spoon_all_designs; we link parent→child via design_ratings.child_design_id.
+app.post('/api/design/:id/retry', express.json({ limit: '32kb' }), (req, res) => {
+  if (!adminRatingGate(req, res)) return;
+  const id = parseInt(req.params.id, 10);
+  try {
+    const raw = psqlQuery(`SELECT row_to_json(t) FROM (
+      SELECT id, color_score, style_score, composition_score, spacing, invert, notes
+      FROM design_ratings WHERE design_id = ${id} ORDER BY created_at DESC LIMIT 1) t;`);
+    if (!raw) return res.status(400).json({ error: 'rate the design first' });
+    const r = JSON.parse(raw);
+
+    // pull parent's category + (a representative) prompt seed
+    const parentRow = psqlQuery(`SELECT row_to_json(t) FROM (
+      SELECT category, prompt FROM spoon_all_designs WHERE id = ${id}) t;`);
+    const parent = parentRow ? JSON.parse(parentRow) : {};
+    const baseCategory = parent.category || 'mixed';
+    const nudges = nudgesFromRating(r);
+    const basePrompt = parent.prompt || `${baseCategory} seamless wallpaper repeat`;
+    const finalPrompt = `${basePrompt}. Corrections: ${nudges.join('; ')}.`;
+
+    psqlQuery(`UPDATE design_ratings SET retry_count = retry_count + 1
+               WHERE id = ${r.id};`);
+
+    const { spawn } = require('child_process');
+    const p = spawn('node', [
+      path.join(__dirname, 'scripts', 'generate_designs.js'),
+      '--n', '1',
+      '--kind', 'seamless_tile',
+      '--category', baseCategory,
+      '--prompts', finalPrompt,
+    ], { cwd: __dirname, env: { ...process.env, GEN_BACKEND: process.env.GEN_BACKEND || 'replicate' },
+         detached: true, stdio: 'ignore' });
+    p.unref();
+
+    res.json({
+      ok: true,
+      retry_started: true,
+      base_category: baseCategory,
+      nudges,
+      final_prompt: finalPrompt,
+      note: 'New design will land in /designs in 30-90s. Refresh after.',
+    });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// POST /api/design/:id/comment — admin saves a free-text review comment on a
+// design (writes to notes col). Used by the review-panel "Save Comment" path.
+// 2026-05-20 — admin review loop per Steve.
+app.post('/api/design/:id/comment', express.json({ limit: '8kb' }), (req, res) => {
+  if (!isAdmin(req)) return res.status(404).json({ error: 'not found' });
+  const id = parseInt(req.params.id, 10);
+  const comment = String(req.body?.comment || '').trim();
+  if (!Number.isFinite(id) || id < 1) return res.status(400).json({ error: 'bad id' });
+  if (!comment) return res.status(400).json({ error: 'comment required' });
+  try {
+    const safe = comment.replace(/'/g, "''").slice(0, 4000);
+    psqlExecLocal(`UPDATE spoon_all_designs SET notes = COALESCE(notes,'') || E'\\n[admin ${new Date().toISOString()}] ' || '${safe}' WHERE id=${id};`);
+    res.json({ ok: true, id, saved: true });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// POST /api/design/:id/regenerate-with-comment — take parent design's prompt,
+// append admin comment as additional guidance, spawn a new generation. The
+// new design gets parent_design_id=<this id> so we can trace the lineage.
+// 2026-05-20 — Steve's "comment to make better and run new versions" directive.
+app.post('/api/design/:id/regenerate-with-comment', express.json({ limit: '8kb' }), (req, res) => {
+  if (!isAdmin(req)) return res.status(404).json({ error: 'not found' });
+  const id = parseInt(req.params.id, 10);
+  const comment = String(req.body?.comment || '').trim();
+  if (!Number.isFinite(id) || id < 1) return res.status(400).json({ error: 'bad id' });
+  if (!comment) return res.status(400).json({ error: 'comment required' });
+  try {
+    const parentRow = psqlQuery(`SELECT row_to_json(t) FROM (
+      SELECT category, prompt, width_in, height_in FROM spoon_all_designs WHERE id=${id}) t;`);
+    if (!parentRow) return res.status(404).json({ error: 'parent design not found' });
+    const parent = JSON.parse(parentRow);
+    const baseCategory = parent.category || 'mixed';
+    const w = parent.width_in || 36;
+    const h = parent.height_in || 36;
+    const basePrompt = parent.prompt || `${baseCategory} seamless wallpaper repeat`;
+    // Append the admin comment as ADDITIONAL CORRECTION guidance. Keep the
+    // 2-tone / no-bleed anchor language at the end so it's the last thing the
+    // model reads (later text gets stronger attention weight).
+    const finalPrompt =
+      basePrompt +
+      ` ADMIN CORRECTION (apply this): ${comment}. ` +
+      `Re-emphasize: EXACTLY TWO COLORS, single-layer screen-print only, NO bleed between layers, NO ghost backgrounds, flat stencil silhouettes.`;
+    // Save the admin comment + parent reference on a notes trail
+    const safeComment = comment.replace(/'/g, "''").slice(0, 4000);
+    psqlExecLocal(`UPDATE spoon_all_designs SET notes = COALESCE(notes,'') || E'\\n[admin ${new Date().toISOString()} -> regen] ' || '${safeComment}' WHERE id=${id};`);
+    // Spawn detached generator. parent_design_id is set by a post-insert update
+    // hook we'll fire once the new ID is known — easiest path: poll for the
+    // most recent design with this category created in the next 90s and stamp
+    // it. For now we let the generator land naturally and rely on the prompt
+    // text to encode that this is a regen.
+    const { spawn } = require('child_process');
+    const p = spawn('node', [
+      path.join(__dirname, 'scripts', 'generate_designs.js'),
+      '--n', '1',
+      '--kind', 'seamless_tile',
+      '--width', String(w),
+      '--height', String(h),
+      '--category', baseCategory,
+      '--prompts', finalPrompt,
+    ], { cwd: __dirname, env: { ...process.env }, detached: true, stdio: 'ignore' });
+    p.unref();
+
+    res.json({
+      ok: true,
+      parent_id: id,
+      regen_started: true,
+      base_category: baseCategory,
+      comment,
+      final_prompt: finalPrompt.slice(0, 500) + '...',
+      note: 'New design will land in /designs in 30-90s. Refresh after.',
+    });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// POST /api/design/:id/add-next-panel — admin extends a mural panel set by 1.
+// Reads parent panel's prompt + seed, builds a continuation prompt for the
+// next panel, spawns generation. New panel's parent_design_id = parent id,
+// notes field carries the panel index (panel 2 of M, panel 3 of M, ...).
+// Steve directive 2026-05-20 — multi-panel Monterey murals up to 20 panels.
+app.post('/api/design/:id/add-next-panel', express.json({ limit: '4kb' }), (req, res) => {
+  if (!isAdmin(req)) return res.status(404).json({ error: 'not found' });
+  const id = parseInt(req.params.id, 10);
+  if (!Number.isFinite(id) || id < 1) return res.status(400).json({ error: 'bad id' });
+  try {
+    // Find the rightmost panel in this set — walk parent_design_id chain
+    // forward by querying for any design where parent_design_id=this-or-ancestor.
+    // Simpler heuristic: count children of root, panel_index = N+1.
+    const rootRow = psqlQuery(`SELECT row_to_json(t) FROM (
+      SELECT id, parent_design_id, category, prompt, width_in, height_in
+      FROM spoon_all_designs WHERE id=${id}) t;`);
+    if (!rootRow) return res.status(404).json({ error: 'design not found' });
+    const root = JSON.parse(rootRow);
+    // Find the "root" (first panel) — walk up parent_design_id chain
+    let rootId = root.parent_design_id || root.id;
+    let depth = 0;
+    while (depth < 25) {
+      const r = psqlQuery(`SELECT row_to_json(t) FROM (SELECT id, parent_design_id FROM spoon_all_designs WHERE id=${rootId}) t;`);
+      if (!r) break;
+      const rr = JSON.parse(r);
+      if (rr.parent_design_id) { rootId = rr.parent_design_id; depth++; } else break;
+    }
+    // Count existing panels in this set (root + all children)
+    const countStr = psqlQuery(`SELECT count(*) FROM spoon_all_designs WHERE id=${rootId} OR parent_design_id=${rootId};`);
+    const existingCount = parseInt(countStr || '1', 10);
+    if (existingCount >= 20) return res.status(400).json({ error: 'panel set already at 20-panel max' });
+    const nextIdx = existingCount + 1;
+
+    const basePrompt = root.prompt || `mural panel`;
+    const finalPrompt =
+      basePrompt +
+      ` PANEL CONTINUATION: This is panel ${nextIdx} of a multi-panel mural set. ` +
+      `Seamlessly continue the scene from panel ${nextIdx - 1} as if hung directly to the right. ` +
+      `The left edge of THIS panel must match the right edge of the previous panel — same horizon line, same ground tone, same figure ink, same scale. ` +
+      `Extend the landscape rightward — new motifs that grow naturally from where the prior panel ended. ` +
+      `Do NOT repeat the prior panel's central composition; show what's to the RIGHT of it in the same scene.`;
+
+    const { spawn } = require('child_process');
+    const p = spawn('node', [
+      path.join(__dirname, 'scripts', 'generate_designs.js'),
+      '--n', '1',
+      '--kind', 'mural_panel',
+      '--width', String(root.width_in || 36),
+      '--height', String(root.height_in || 132),
+      '--category', root.category || 'monterey-mural',
+      '--prompts', finalPrompt,
+    ], { cwd: __dirname, env: { ...process.env }, detached: true, stdio: 'ignore' });
+    p.unref();
+
+    // Stamp a marker so the post-publish step can find the new panel and
+    // wire its parent_design_id. We do this by polling for the most recent
+    // design in this category — simpler than IPC. Caller can poll /api/design/<rootId>/panels
+    // to discover the new panel once it lands.
+    psqlExecLocal(`INSERT INTO design_panel_extensions (root_design_id, panel_index, requested_at, requested_by_ip) VALUES (${rootId}, ${nextIdx}, NOW(), '${req.ip || 'admin'}') ON CONFLICT DO NOTHING;`);
+
+    res.json({
+      ok: true,
+      root_design_id: rootId,
+      next_panel_index: nextIdx,
+      existing_panel_count: existingCount,
+      panels_remaining_quota: 20 - existingCount,
+      note: `New panel ${nextIdx}/20 spawning. Lands in /designs in 30-90s. Refresh after.`,
+    });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// POST /api/design/:id/settlement-violation — HARD delete (URL goes 404),
+// append the prompt+motifs+tags signature to the settlement do-not-want
+// registry, and queue a fresh regen with this violation excluded as a
+// negative prompt. Reserved for content we DO NOT WANT (banana leaves,
+// monstera, tropical-leaf bleed-through, etc.). NOT reversible.
+app.post('/api/design/:id/settlement-violation', express.json({ limit: '16kb' }), (req, res) => {
+  if (!adminRatingGate(req, res)) return;
+  const id = parseInt(req.params.id, 10);
+  if (!Number.isFinite(id) || id < 1) return res.status(400).json({ error: 'bad id' });
+  const reason = String(req.body?.reason || '').slice(0, 300).trim();
+  if (!reason) return res.status(400).json({ error: 'reason required' });
+
+  try {
+    const raw = psqlQuery(`SELECT row_to_json(t) FROM (SELECT id, prompt, category,
+      motifs, tags, generator, dominant_hex, local_path, parent_design_id
+      FROM spoon_all_designs WHERE id=${id}) t;`);
+    if (!raw) return res.status(404).json({ error: 'design not found' });
+    const d = JSON.parse(raw);
+
+    // 1) Backup file before delete (forensic only — never republish)
+    const backupDir = path.join(require('os').homedir(), '.wallco-deleted', new Date().toISOString().slice(0,10));
+    try { fs.mkdirSync(backupDir, { recursive: true }); } catch {}
+    let backedUp = null;
+    if (d.local_path && fs.existsSync(d.local_path)) {
+      const bn = `${id}_${path.basename(d.local_path)}`;
+      backedUp = path.join(backupDir, bn);
+      try { fs.copyFileSync(d.local_path, backedUp); } catch {}
+    }
+
+    // 2) Append do-not-want signature to settlement registry
+    const auditDir = path.join(__dirname, 'data', 'settlement-audit');
+    try { fs.mkdirSync(auditDir, { recursive: true }); } catch {}
+    const registryPath = path.join(auditDir, 'do-not-want.jsonl');
+    const violationEntry = {
+      ts: new Date().toISOString(),
+      id, reason,
+      prompt:   d.prompt   || null,
+      category: d.category || null,
+      motifs:   d.motifs   || [],
+      tags:     d.tags     || [],
+      generator: d.generator || null,
+      dominant_hex: d.dominant_hex || null,
+      parent_design_id: d.parent_design_id || null,
+      backup_file: backedUp,
+    };
+    try { fs.appendFileSync(registryPath, JSON.stringify(violationEntry) + '\n', 'utf8'); } catch {}
+
+    // 3) Hard delete: file from disk + DB row (URL becomes 404)
+    if (d.local_path && fs.existsSync(d.local_path)) {
+      try { fs.unlinkSync(d.local_path); } catch {}
+    }
+    psqlQuery(`DELETE FROM spoon_all_designs WHERE id=${id};`);
+    // Drop from in-memory DESIGNS cache so /design/:id 404s immediately
+    const idx = DESIGNS.findIndex(x => x.id === id);
+    if (idx >= 0) DESIGNS.splice(idx, 1);
+
+    // 4) Queue a fresh regen with this violation excluded as a negative prompt
+    let regenStarted = false;
+    let regenErr = null;
+    try {
+      const basePrompt = (d.prompt || `${d.category || 'mixed'} seamless wallpaper repeat`).trim();
+      // Strip any existing "Corrections:" tail so we don't compound
+      const cleanBase = basePrompt.replace(/\.\s*Corrections:[^.]*\.?\s*$/i, '').trim();
+      const negativeAddon =
+        `Corrections: ABSOLUTELY NO ${reason}. Settlement-blocked content for this design family: no banana leaves, no monstera leaves, no palm fronds, no palm leaves, no tropical foliage, no plantain leaves — any plant content must be redrawn as the user's specified motif only.`;
+      const finalPrompt = `${cleanBase}. ${negativeAddon}`;
+      const { spawn } = require('child_process');
+      const p = spawn('node', [
+        path.join(__dirname, 'scripts', 'generate_designs.js'),
+        '--n', '1',
+        '--kind', 'seamless_tile',
+        '--category', d.category || 'mixed',
+        '--prompts', finalPrompt,
+      ], { cwd: __dirname, env: { ...process.env, GEN_BACKEND: process.env.GEN_BACKEND || 'replicate' },
+           detached: true, stdio: 'ignore' });
+      p.unref();
+      regenStarted = true;
+    } catch (e) { regenErr = e.message; }
+
+    res.json({
+      ok: true,
+      deleted: true,
+      id,
+      url_now_404: true,
+      backed_up_to: backedUp,
+      do_not_want_entry: registryPath,
+      regen_started: regenStarted,
+      regen_error: regenErr,
+      note: 'Design hard-deleted. Future generations will be filtered through the do-not-want registry.',
+    });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+app.get('/design/:id', (req, res) => {
+  const id = parseInt(req.params.id, 10);
+  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
+  // entry on the fly so we never have to wait for refresh_designs_snapshot.py.
+  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, room_mockups,
+        user_color_good, user_style_good, user_scale_good, user_stars, user_removed
+        FROM spoon_all_designs WHERE id=${id}) t;`);
+      if (row) {
+        const r = JSON.parse(row);
+        const filename = (r.local_path || '').split('/').pop();
+        design = {
+          id: r.id, kind: r.kind, category: r.category || 'mixed',
+          dominant_hex: r.dominant_hex || '#888888',
+          saturation: hexSaturation(r.dominant_hex || '#888888'),
+          title: titleFor(r.category, r.dominant_hex, r.id),
+          handle: `wallco-${String(r.id).padStart(4,'0')}`,
+          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,
+          room_mockups: Object.keys(r.room_mockups || {}).sort(),
+          user_color_good: r.user_color_good, user_style_good: r.user_style_good,
+          user_scale_good: r.user_scale_good, user_stars: r.user_stars,
+          user_removed: !!r.user_removed,
+        };
+        DESIGNS.push(design);  // lazy-extend so next request is fast
+      }
+    } catch {}
+  }
+  if (!design) return res.status(404).type('html').send(renderDesignNotFound(req, id));
+
+  // Conditional GET on per-design created_at. Designs rarely mutate after
+  // creation — admin room-mockup generations bump CATALOG_LAST_MODIFIED but
+  // the per-design page itself reflects design.created_at for crawler caching.
+  if (design.created_at && maybe304(req, res, new Date(design.created_at))) return;
+
+  const relIdx = DESIGNS.findIndex(d => d.id === id);
+  const prev = relIdx > 0 ? DESIGNS[relIdx-1] : null;
+  const next = relIdx < DESIGNS.length-1 ? DESIGNS[relIdx+1] : null;
+
+  // Resolve room-mockup contexts (residential / commercial / hospitality).
+  // Each context maps to ONE canonical room from the design's available
+  // mockups so the toggle shows up to 3 pills, never duplicates.
+  const ROOM_CONTEXTS = [
+    { id: 'residential', label: 'Residential', candidates: ['living_room','bedroom','dining_room'] },
+    { id: 'commercial',  label: 'Commercial',  candidates: ['office','boardroom','retail'] },
+    { id: 'hospitality', label: 'Hospitality', candidates: ['hotel_lobby','restaurant','hotel_corridor'] }
+  ];
+  const haveRooms = new Set(design.room_mockups || []);
+  const roomCtx = ROOM_CONTEXTS
+    .map(c => { const room = c.candidates.find(r => haveRooms.has(r)); return room ? { id: c.id, label: c.label, room } : null; })
+    .filter(Boolean);
+
+  // JSON-LD Product — enriched 2026-05-12 with sku/productID/color/material +
+  // additionalProperty (motifs, room previews, seed). Description weaves the
+  // top motifs + dominant hue so Google's snippet has real signal beyond
+  // "AI-generated wallpaper". Reference: schema.org/Product + Google Rich Result spec.
+  const _motifList = (design.motifs || []).slice(0, 4).join(', ');
+  const _hueName = (() => {
+    const h = (design.dominant_hex || '').toLowerCase();
+    if (!/^#[0-9a-f]{6}$/.test(h)) return '';
+    const r=parseInt(h.slice(1,3),16), g=parseInt(h.slice(3,5),16), b=parseInt(h.slice(5,7),16);
+    const max=Math.max(r,g,b), min=Math.min(r,g,b), s=max?(max-min)/max:0;
+    if (s<0.12) return 'neutral';
+    const hh = ((max===r ? (g-b)/(max-min)+(g<b?6:0) : max===g ? (b-r)/(max-min)+2 : (r-g)/(max-min)+4) * 60 + 360) % 360;
+    if (hh<15||hh>=345) return 'rose'; if (hh<40) return 'amber'; if (hh<60) return 'honey';
+    if (hh<90) return 'olive';  if (hh<160) return 'sage';  if (hh<200) return 'marine';
+    if (hh<250) return 'sapphire'; if (hh<290) return 'mauve'; return 'plum';
+  })();
+  const _enrichedDesc =
+    `wallco.ai original ${design.category} wallpaper pattern in ${_hueName} (${design.dominant_hex})` +
+    (_motifList ? `. Motifs: ${_motifList}.` : '.') +
+    ` Pattern No.${design.id}, generated through our proprietary AI design system — every pattern unique, never repeated.` +
+    ((design.room_mockups || []).length > 0
+      ? ` Includes ${design.room_mockups.length} room visualizations (residential, commercial, hospitality).`
+      : '') +
+    ` Free sample available.`;
+  const _addlProps = [
+    { "@type": "PropertyValue", "name": "Category", "value": design.category },
+    _hueName && { "@type": "PropertyValue", "name": "Hue family", "value": _hueName },
+    design.dominant_hex && { "@type": "PropertyValue", "name": "Dominant color", "value": design.dominant_hex },
+    (design.motifs && design.motifs.length) && { "@type": "PropertyValue", "name": "Motifs", "value": design.motifs.join(', ') },
+    (design.room_mockups && design.room_mockups.length) && { "@type": "PropertyValue", "name": "Room previews available", "value": design.room_mockups.length },
+    design.seed && { "@type": "PropertyValue", "name": "Generation seed", "value": String(design.seed) }
+  ].filter(Boolean);
+
+  // BreadcrumbList schema — surfaces "Home › Designs › <category> › <title>"
+  // in Google search results. Pairs with the existing Product schema so the
+  // SERP shows both rich-result card AND breadcrumbs.
+  const breadcrumbSchema = JSON.stringify({
+    "@context": "https://schema.org",
+    "@type": "BreadcrumbList",
+    "itemListElement": [
+      { "@type": "ListItem", "position": 1, "name": "Home",    "item": "https://wallco.ai/" },
+      { "@type": "ListItem", "position": 2, "name": "Designs", "item": "https://wallco.ai/designs" },
+      { "@type": "ListItem", "position": 3, "name": String(design.category || '').replace(/^./, c => c.toUpperCase()), "item": `https://wallco.ai/designs?cat=${encodeURIComponent(design.category || '')}` },
+      { "@type": "ListItem", "position": 4, "name": design.title }
+    ]
+  });
+  const productSchema = JSON.stringify({
+    "@context": "https://schema.org",
+    "@type": "Product",
+    "@id": `https://wallco.ai/design/${design.id}`,
+    "name": design.title,
+    "description": _enrichedDesc,
+    "url": `https://wallco.ai/design/${design.id}`,
+    "image": `https://wallco.ai${design.image_url}`,
+    "sku": design.handle || `wallco-${String(design.id).padStart(4,'0')}`,
+    "productID": String(design.id),
+    "category": design.category,
+    "color": _hueName || undefined,
+    "material": "Wallpaper",
+    "brand": { "@type": "Brand", "name": "wallco.ai" },
+    "manufacturer": { "@type": "Organization", "name": "Designer Wallcoverings", "url": "https://designerwallcoverings.com" },
+    "additionalProperty": _addlProps,
+    "offers": {
+      "@type": "Offer",
+      "availability": "https://schema.org/InStock",
+      "url": `https://wallco.ai/samples?design=${design.id}`,
+      "priceCurrency": "USD",
+      "description": "Free sample available — full roll pricing on request"
+    }
+  });
+  const schema = `${productSchema}</script>\n<script type="application/ld+json">${breadcrumbSchema}`;
+
+  res.type('html').send(`${htmlHead({
+    title: `${design.title} — wallco.ai`,
+    description: `wallco.ai original ${design.category} wallpaper. Pattern No.${design.id} — hand-curated, unique, never repeated. Free sample available.`,
+    canonical: `https://wallco.ai/design/${design.id}`,
+    ogImage: `https://wallco.ai${design.image_url}`,
+    ogType: 'product',
+    ogImageAlt: `${design.title} — ${_hueName} ${design.category} wallpaper pattern${_motifList ? ' featuring ' + _motifList : ''}`,
+    productOG: {
+      availability: 'in stock',
+      retailer_item_id: design.handle || `wallco-${String(design.id).padStart(4,'0')}`,
+      brand: 'wallco.ai',
+      category: design.category
+    },
+    // Preload the detail image — it's THE LCP candidate on /design/:id.
+    // Pairs with fetchpriority=high on the <img> tag so the browser
+    // double-confirms this is the most important image to start fetching.
+    preloadImages: [design.image_url]
+  })}
+<body>
+${htmlHeader('/designs')}
+<main class="detail-main">
+  <div class="detail-layout">
+    <div class="detail-image-col" style="position:relative">
+      <!-- HAMBURGER ICON (top-right of image) opens the INFO + EDIT modal ─── -->
+      <button id="edit-hamburger" class="edit-hamburger" aria-label="Open design info, color &amp; style"
+              aria-haspopup="dialog" aria-expanded="false">
+        <span></span><span></span><span></span>
+      </button>
+
+      <!-- INFO MODAL ───────────────────────────────────────────────────────── -->
+      <div id="edit-modal" class="edit-modal" hidden role="dialog" aria-modal="true" aria-labelledby="edit-modal-title">
+        <div class="edit-modal-backdrop" data-close="1"></div>
+        <div class="edit-modal-sheet">
+          <header class="edit-modal-head">
+            <h2 id="edit-modal-title">${design.title}</h2>
+            <button class="edit-modal-close" data-close="1" aria-label="Close">✕</button>
+          </header>
+          <!-- ── LIVE PREVIEW IMAGE at top — mirrors slider/recolor state ── -->
+          <div class="edit-preview-wrap">
+            <img id="edit-preview-img" class="edit-preview-img" alt="Live preview" src="${design.image_url}" crossorigin="anonymous">
+            <div class="edit-preview-readout" id="edit-preview-readout">Live preview — sliders + recolors update here.</div>
+          </div>
+          <nav class="edit-tabs">
+            <button class="edit-tab active" data-tab="adjust">Adjust</button>
+            <button class="edit-tab"        data-tab="color">Color</button>
+            <button class="edit-tab"        data-tab="info">Info</button>
+            <button class="edit-tab"        data-tab="style">Style</button>
+            <button class="edit-tab"        data-tab="save">Save</button>
+          </nav>
+          <div class="edit-modal-body">
+
+            <!-- INFO ─────────────────────────────────────────────────────── -->
+            <section class="edit-tabpane" data-pane="info">
+              <dl class="info-grid">
+                <dt>Category</dt><dd>${design.category}</dd>
+                <dt>Kind</dt><dd>${design.kind === 'seamless_tile' ? 'Seamless tile (24×24")' : design.kind}</dd>
+                <dt>Pattern No.</dt><dd>${String(design.id).padStart(4,'0')}</dd>
+                <dt>Generator</dt><dd>${design.generator}</dd>
+                <dt>Handle</dt><dd>${design.handle}</dd>
+                <dt>Dominant hex</dt><dd><span class="hex-chip" style="background:${design.dominant_hex}"></span>${design.dominant_hex}</dd>
+              </dl>
+
+              <!-- AI Interior Designer block — admin-only (front-end hides for
+                   non-admins; endpoint is requireAdmin so non-admins 404
+                   anyway). Generates title / pattern / color name / DIG# /
+                   tags via Gemini Vision per /interior-designer skill. -->
+              <div id="ai-designer-block" style="display:none;margin-top:24px;padding:16px;border:1px solid #d4cfc1;border-radius:8px;background:#faf7f1">
+                <div style="display:flex;align-items:center;justify-content:space-between;gap:12px">
+                  <h3 class="pane-h" style="margin:0">AI Interior Designer</h3>
+                  <button id="ai-designer-run" type="button"
+                    style="padding:8px 14px;border:0;border-radius:4px;background:#1f1808;color:#f4ece0;font:600 11px var(--sans,system-ui);letter-spacing:.08em;text-transform:uppercase;cursor:pointer">
+                    Run AI Designer
+                  </button>
+                </div>
+                <p class="pane-readout" style="margin:6px 0 12px">
+                  Reads the image, suggests a designer title, canonical pattern name, named color, DIG number (if unassigned), and tag set. Review → Apply.
+                </p>
+                <div id="ai-designer-result" style="display:none">
+                  <div id="ai-designer-seam-warn" style="display:none;margin-bottom:10px;padding:8px 12px;border-radius:4px;background:#b00020;color:#fff;font:600 12px var(--sans);letter-spacing:.04em">
+                    ⚠ Visible seam detected — image may not tile cleanly. Consider regenerating.
+                  </div>
+                  <dl class="info-grid">
+                    <dt>Title</dt><dd><input id="ai-d-title"    type="text" style="width:100%;padding:6px 8px;border:1px solid #d4cfc1;border-radius:3px;font:13px var(--sans)"></dd>
+                    <dt>Pattern</dt><dd><input id="ai-d-pattern" type="text" style="width:100%;padding:6px 8px;border:1px solid #d4cfc1;border-radius:3px;font:13px var(--sans)"></dd>
+                    <dt>Color</dt><dd><input id="ai-d-color"     type="text" style="width:100%;padding:6px 8px;border:1px solid #d4cfc1;border-radius:3px;font:13px var(--sans)"></dd>
+                    <dt>DIG #</dt><dd><input id="ai-d-dig"       type="text" style="width:100%;padding:6px 8px;border:1px solid #d4cfc1;border-radius:3px;font:13px var(--sans);font-family:'SF Mono',ui-monospace,monospace"></dd>
+                    <dt>Tags</dt><dd><input id="ai-d-tags"       type="text" style="width:100%;padding:6px 8px;border:1px solid #d4cfc1;border-radius:3px;font:13px var(--sans)" placeholder="comma-separated"></dd>
+                  </dl>
+                  <p id="ai-d-notes" class="pane-readout" style="margin:10px 0 12px;font-style:italic"></p>
+                  <button id="ai-designer-apply" type="button"
+                    style="padding:9px 18px;border:0;border-radius:4px;background:#1f1808;color:#f4ece0;font:600 11px var(--sans);letter-spacing:.08em;text-transform:uppercase;cursor:pointer">
+                    Apply &amp; Save
+                  </button>
+                  <span id="ai-designer-status" style="margin-left:12px;font:12px var(--sans);color:#1a3"></span>
+                </div>
+              </div>
+            </section>
+
+            <!-- COLOR ─────────────────────────────────────────────────────── -->
+            <section class="edit-tabpane" data-pane="color">
+              <h3 class="pane-h">Palette · drag dot onto dot to swap</h3>
+              <div id="palette-dots" class="palette-dots">
+                <div class="dots-loading">Extracting colors…</div>
+              </div>
+              <div class="dots-sort-row">
+                <button class="dots-sort active" data-sort="hex">by hex</button>
+                <button class="dots-sort" data-sort="light">light→dark</button>
+                <button class="dots-sort" data-sort="hue">color wheel</button>
+              </div>
+              <h3 class="pane-h" style="margin-top:24px">HSV wheel · pick replacement</h3>
+              <canvas id="color-wheel" width="200" height="200"></canvas>
+              <div id="wheel-readout" class="pane-readout">Click a swatch above first, then a wheel position to recolor it</div>
+              <h3 class="pane-h" style="margin-top:18px">Suggested replacements</h3>
+              <div id="suggested-swatches" class="suggested-swatches"></div>
+            </section>
+
+            <!-- STYLE ─────────────────────────────────────────────────────── -->
+            <section class="edit-tabpane" data-pane="style">
+              <h3 class="pane-h">Style attribution</h3>
+              <p class="pane-text">${String(design.title || 'Untitled').replace(/[<>]/g,'')} reads as a <strong>${design.category || 'mixed'}</strong> design with a dominant ${design.dominant_hex || 'neutral'} tone.</p>
+              <h3 class="pane-h">Where it fits</h3>
+              <ul class="pane-list">
+                <li>Room: ${design.category === 'floral' ? 'bedroom · powder · breakfast nook' : design.category === 'damask' ? 'library · dining · formal' : design.category === 'geometric' ? 'modern lobby · home office · entry' : 'flexible'}</li>
+                <li>Repeat: 24" tile · seamless</li>
+                <li>Trade: paper · vinyl · peel-and-stick · grasscloth on request</li>
+              </ul>
+              <div id="style-extra" class="pane-readout">More from the studio's tagger landing soon.</div>
+            </section>
+
+            <!-- ADJUST ─────────────────────────────────────────────────────── -->
+            <section class="edit-tabpane active" data-pane="adjust">
+              <h3 class="pane-h">Live image adjustments</h3>
+              <label class="adj"><span>Hue</span>       <input type="range" id="adj-hue"  min="-180" max="180" step="1" value="0"><em id="adj-hue-v">0°</em></label>
+              <label class="adj"><span>Saturation</span><input type="range" id="adj-sat"  min="0"   max="200" step="1" value="100"><em id="adj-sat-v">100%</em></label>
+              <label class="adj"><span>Vibrance</span>  <input type="range" id="adj-vib"  min="0"   max="200" step="1" value="100"><em id="adj-vib-v">100%</em></label>
+              <label class="adj"><span>Brightness</span><input type="range" id="adj-bri"  min="0"   max="200" step="1" value="100"><em id="adj-bri-v">100%</em></label>
+              <label class="adj"><span>Contrast</span>  <input type="range" id="adj-con"  min="0"   max="200" step="1" value="100"><em id="adj-con-v">100%</em></label>
+              <label class="adj"><span>Opacity</span>   <input type="range" id="adj-opa"  min="20"  max="100" step="1" value="100"><em id="adj-opa-v">100%</em></label>
+              <label class="adj"><span>Sepia</span>     <input type="range" id="adj-sep"  min="0"   max="100" step="1" value="0"><em id="adj-sep-v">0%</em></label>
+              <label class="adj"><span>Blur</span>      <input type="range" id="adj-blur" min="0"   max="20"  step="1" value="0"><em id="adj-blur-v">0px</em></label>
+              <button id="adj-reset" class="adj-reset">Reset all</button>
+            </section>
+
+            <!-- SAVE ─────────────────────────────────────────────────────── -->
+            <section class="edit-tabpane" data-pane="save">
+              <h3 class="pane-h">Save your edits as a new design</h3>
+              <p class="pane-text">All your color swaps and adjustments will be flattened into a fresh design, watermarked, and indexed in the catalog.</p>
+              <button id="adj-bake" class="adj-bake">Bake edits → save as new design</button>
+            </section>
+
+          </div>
+        </div>
+      </div>
+
+      <style>
+        /* Hamburger button — small, top-right of the design image */
+        .edit-hamburger { position:absolute; top:14px; right:14px; width:42px; height:42px; background:rgba(20,18,15,.85); backdrop-filter:blur(8px); color:#f0eadc; border:1px solid rgba(255,255,255,.1); border-radius:8px; cursor:pointer; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:4px; padding:0; z-index:18; transition:background .15s; }
+        .edit-hamburger:hover { background:rgba(20,18,15,.95); border-color:#d2b15c; }
+        .edit-hamburger span { display:block; width:18px; height:2px; background:currentColor; border-radius:1px; }
+
+        /* Modal */
+        .edit-modal { position:fixed; inset:0; z-index:200; display:flex; align-items:center; justify-content:center; }
+        .edit-modal[hidden] { display:none; }
+        .edit-modal-backdrop { position:absolute; inset:0; background:rgba(10,9,8,.7); backdrop-filter:blur(6px); }
+        .edit-modal-sheet { position:relative; width:min(1100px, 96vw); max-height:94vh; background:#1a1816; color:#e8e2d6; border-radius:12px; border:1px solid #2a2a2a; box-shadow:0 24px 80px rgba(0,0,0,.6); display:flex; flex-direction:column; overflow:hidden; }
+        .edit-modal-head { display:flex; justify-content:space-between; align-items:center; padding:14px 22px; border-bottom:1px solid #2a2a2a; flex-shrink:0; }
+        /* Big live preview image at the top */
+        .edit-preview-wrap { position:relative; padding:14px 22px; background:#0f0e0c; border-bottom:1px solid #2a2a2a; flex-shrink:0; }
+        .edit-preview-img { display:block; width:100%; max-height:48vh; object-fit:contain; border-radius:6px; transition:filter .12s ease, opacity .12s ease; background:#000; }
+        .edit-preview-readout { font:11px var(--mono,monospace); color:#888; margin-top:8px; text-align:center; }
+        .edit-modal-head h2 { margin:0; font-family:'Cormorant Garamond',serif; font-weight:300; font-size:22px; color:#f0eadc; }
+        .edit-modal-close { background:transparent; border:0; color:#888; font-size:22px; cursor:pointer; padding:0 6px; line-height:1; }
+        .edit-modal-close:hover { color:#fff; }
+        .edit-tabs { display:flex; gap:2px; padding:0 14px; border-bottom:1px solid #2a2a2a; background:#15130f; }
+        .edit-tab { background:transparent; border:0; padding:12px 16px; color:#888; cursor:pointer; font-size:12px; text-transform:uppercase; letter-spacing:.08em; border-bottom:2px solid transparent; }
+        .edit-tab:hover { color:#e8e2d6; }
+        .edit-tab.active { color:#d2b15c; border-bottom-color:#d2b15c; }
+        .edit-modal-body { padding:18px 22px 22px; overflow-y:auto; flex:1; min-height:280px; }
+        .edit-tabpane { display:none; }
+        .edit-tabpane.active { display:block; }
+        .pane-h { font-size:11px; text-transform:uppercase; letter-spacing:.1em; color:#888; margin:0 0 10px; font-weight:500; }
+        .pane-text { font-size:13px; color:#bba; line-height:1.55; margin:0 0 14px; }
+        .pane-list { margin:0; padding-left:18px; font-size:13px; color:#bba; line-height:1.7; }
+        .pane-readout { font:11px var(--mono,monospace); color:#888; text-align:center; margin-top:6px; }
+
+        .info-grid { display:grid; grid-template-columns:140px 1fr; gap:8px 16px; margin:0; font-size:13px; }
+        .info-grid dt { color:#888; text-transform:uppercase; font-size:10px; letter-spacing:.08em; align-self:center; }
+        .info-grid dd { margin:0; color:#e8e2d6; }
+        .hex-chip { display:inline-block; width:14px; height:14px; border-radius:50%; margin-right:6px; vertical-align:middle; border:1px solid rgba(255,255,255,.2); }
+
+        .adj { display:grid; grid-template-columns:90px 1fr 50px; align-items:center; gap:10px; margin-bottom:8px; font-size:12px; color:#bba; }
+        .adj input[type=range] { width:100%; }
+        .adj em { font-style:normal; font-size:11px; color:#888; text-align:right; }
+        .adj-reset { background:transparent; border:1px solid #444; color:#aaa; padding:6px 14px; border-radius:4px; cursor:pointer; font-size:12px; margin-top:10px; }
+
+        .palette-dots { display:flex; flex-wrap:wrap; gap:7px; min-height:50px; padding:6px 0 22px; }
+        .palette-dot { width:32px; height:32px; border-radius:50%; cursor:grab; border:2px solid rgba(255,255,255,.15); position:relative; transition:transform .15s; }
+        .palette-dot:hover { transform:scale(1.18); border-color:#d2b15c; }
+        .palette-dot.selected { box-shadow:0 0 0 2px #d2b15c, 0 0 12px rgba(210,177,92,.6); transform:scale(1.12); }
+        .palette-dot.drag-over { box-shadow:0 0 0 2px #fff; }
+        .dot-label { position:absolute; bottom:-20px; left:50%; transform:translateX(-50%); font:10px var(--mono,monospace); color:#aaa; white-space:nowrap; pointer-events:none; opacity:0; transition:opacity .15s; }
+        .palette-dot:hover .dot-label { opacity:1; }
+        .dots-sort-row { display:flex; gap:6px; margin-top:14px; }
+        .dots-sort { flex:1; background:transparent; border:1px solid #333; color:#888; padding:6px 8px; border-radius:4px; cursor:pointer; font-size:11px; }
+        .dots-sort.active { background:#d2b15c; color:#1a1a1a; border-color:#d2b15c; }
+
+        #color-wheel { display:block; margin:8px auto 0; cursor:crosshair; border-radius:50%; }
+        .suggested-swatches { display:grid; grid-template-columns:repeat(5,1fr); gap:6px; }
+        .suggested-swatches > div { aspect-ratio:1; border-radius:4px; cursor:pointer; border:1px solid rgba(255,255,255,.1); }
+        .suggested-swatches > div:hover { border-color:#d2b15c; }
+        .adj-bake { width:100%; padding:14px; background:#d2b15c; color:#1a1a1a; border:0; border-radius:6px; cursor:pointer; font-weight:600; font-size:14px; margin-top:4px; }
+        .adj-bake:hover { background:#e0bd64; }
+        .adj-bake:disabled { opacity:.5; cursor:wait; }
+
+        @media (max-width: 640px) {
+          .edit-modal-sheet { width:96vw; max-height:94vh; }
+          .edit-tab { padding:10px 11px; font-size:11px; }
+        }
+      </style>
+
+      <div class="detail-img-wrap">
+        <img src="${design.image_url}" alt="${design.title}" class="detail-img" id="detail-img" loading="eager" fetchpriority="high" decoding="async" crossorigin="anonymous">
+        <canvas id="detail-canvas" style="display:none"></canvas>
+
+        <!-- ── COLOR STORY — palette dots + Sherwin-Williams + Dunn-Edwards rows
+             Drag any dot onto a pattern editor color slot to recolor it. -->
+        <section id="color-story" data-design-id="${design.id}" data-palette="${(design.dominant_hex || '').replace(/"/g,'&quot;')}"
+                 style="margin:18px 0 6px;padding:14px 16px;background:var(--card-bg,#f4f0e8);border:1px solid var(--line,#d8d0c0);border-radius:8px;">
+          <div style="display:flex;align-items:center;gap:12px;margin-bottom:10px;">
+            <strong style="font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-faint,#7a6e5a)">Color story</strong>
+            <span style="font-size:10.5px;color:var(--ink-faint,#7a6e5a);letter-spacing:.04em;">drag any dot onto a pattern color slot to recolor it</span>
+          </div>
+          <div id="cs-dots" class="cs-row" data-vendor="raw"
+               style="display:flex;flex-wrap:wrap;gap:10px;align-items:center;min-height:28px;"></div>
+          <div id="cs-sw" class="cs-row" data-vendor="sw"
+               style="display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-top:8px;min-height:24px;"></div>
+          <div id="cs-de" class="cs-row" data-vendor="de"
+               style="display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-top:6px;min-height:24px;"></div>
+        </section>
+        <style>
+          .cs-dot { display:inline-flex; align-items:center; gap:6px; cursor:grab; user-select:none; }
+          .cs-dot[draggable=true]:active { cursor:grabbing; }
+          .cs-swatch { width:28px; height:28px; border-radius:50%; border:1px solid rgba(0,0,0,.18); box-shadow: inset 0 0 0 1px rgba(255,255,255,.18); }
+          .cs-row[data-vendor=sw] .cs-swatch, .cs-row[data-vendor=de] .cs-swatch { width:18px; height:18px; border-radius:4px; }
+          .cs-hex { font-family: ui-monospace, Menlo, monospace; font-size:10.5px; color:#5a4628; letter-spacing:.02em; }
+          .cs-name { font-size:11.5px; color:#3a2f1c; }
+          .cs-code { font-family: ui-monospace, Menlo, monospace; font-size:10.5px; color:#a08b6a; margin-left:4px; }
+          .cs-label { font-size:10px; letter-spacing:.14em; text-transform:uppercase; color:var(--ink-faint,#7a6e5a); margin-right:4px; min-width:90px; display:inline-block; }
+        </style>
+        <script>
+        (function(){
+          var sec = document.getElementById('color-story');
+          if (!sec) return;
+          var palette = (sec.getAttribute('data-palette') || '').split(',').map(function(s){return s.trim();}).filter(Boolean);
+          if (!palette.length) { sec.hidden = true; return; }
+
+          function dotHtml(hex, sub) {
+            var swatch = sub && sub.swatch_hex ? sub.swatch_hex : hex;
+            return '<span class="cs-dot" draggable="true" data-hex="' + hex + '" data-paint="' + (sub ? (sub.name + ' ' + sub.code) : '') + '">' +
+                     '<span class="cs-swatch" style="background:' + swatch + '"></span>' +
+                     (sub
+                       ? '<span class="cs-name">' + sub.name + ' <span class="cs-code">' + sub.code + '</span></span>'
+                       : '<span class="cs-hex">' + hex.toUpperCase() + '</span>') +
+                   '</span>';
+          }
+
+          document.getElementById('cs-dots').innerHTML = palette.map(function(h){ return dotHtml(h); }).join('');
+
+          var ep = 'https://pairs.designerwallcoverings.com/api/pairs?palette=' +
+                   encodeURIComponent(palette.join(',')) + '&limit=1';
+          fetch(ep, { credentials: 'omit' })
+            .then(function(r){ return r.ok ? r.json() : Promise.reject(r.status); })
+            .then(function(d){
+              var pal = d && d.source && d.source.palette;
+              if (!pal || !pal.length) return;
+              var swRow = ['<span class="cs-label">Sherwin-Williams</span>'];
+              var deRow = ['<span class="cs-label">Dunn-Edwards</span>'];
+              pal.forEach(function(c){
+                var sw = c && c.paints && c.paints.sw;
+                var de = c && c.paints && c.paints.de;
+                if (sw) swRow.push(dotHtml(c.hex, { name: sw.name, code: sw.code, swatch_hex: sw.hex }));
+                if (de) deRow.push(dotHtml(c.hex, { name: de.name, code: de.code, swatch_hex: de.hex }));
+              });
+              if (swRow.length > 1) document.getElementById('cs-sw').innerHTML = swRow.join('');
+              else document.getElementById('cs-sw').remove();
+              if (deRow.length > 1) document.getElementById('cs-de').innerHTML = deRow.join('');
+              else document.getElementById('cs-de').remove();
+            })
+            .catch(function(){});
+
+          sec.addEventListener('dragstart', function(e){
+            var d = e.target.closest('.cs-dot');
+            if (!d) return;
+            e.dataTransfer.effectAllowed = 'copy';
+            var swatchEl = d.querySelector('.cs-swatch');
+            var swatchBg = swatchEl ? swatchEl.style.background : d.getAttribute('data-hex');
+            function rgbToHex(s){
+              var m = /rgb\((\d+),\s*(\d+),\s*(\d+)\)/.exec(s||'');
+              if (!m) return s;
+              return '#' + [+m[1],+m[2],+m[3]].map(function(n){return n.toString(16).padStart(2,'0');}).join('');
+            }
+            var hex = (swatchBg && swatchBg.charAt(0) === '#') ? swatchBg : rgbToHex(swatchBg);
+            if (!hex || hex.charAt(0) !== '#') hex = d.getAttribute('data-hex');
+            e.dataTransfer.setData('text/plain', hex);
+            e.dataTransfer.setData('application/x-wallco-color', hex);
+          });
+        })();
+        </script>
+
+        <!-- ── Inspired-by-archival badge (public; NO vendor name reveal) ───
+             Hits /api/design/:id/inspired-by-badge → { inspired: bool }. The
+             admin-only inspired_by_vendor column NEVER appears here — that
+             metadata lives behind /admin/inspirations. -->
+        <div id="inspired-by-badge" data-design-id="${design.id}"
+             style="display:none;margin:14px 0 4px;padding:10px 14px;background:#f7f3eb;border:1px solid #e0d4b8;border-radius:6px;font:12px var(--sans,system-ui);color:#5a4628;letter-spacing:.03em;">
+          <span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:#c9a14b;margin-right:8px;vertical-align:middle;"></span>
+          <strong style="font:600 10.5px var(--sans,system-ui);letter-spacing:.18em;text-transform:uppercase;color:#8a6f44;">Inspired by an archival reference</strong>
+          <span style="display:block;margin-top:3px;color:#7a6e5a;font-size:11.5px;">Anchored to authentic designer-wallpaper DNA, reinterpreted as an original wallco composition.</span>
+        </div>
+        <script>
+        (function(){
+          var el = document.getElementById('inspired-by-badge');
+          if (!el) return;
+          fetch('/api/design/' + el.getAttribute('data-design-id') + '/inspired-by-badge', { credentials:'omit' })
+            .then(function(r){ return r.ok ? r.json() : null; })
+            .then(function(d){ if (d && d.inspired === true) el.style.display = 'block'; })
+            .catch(function(){});
+        })();
+        </script>
+
+        ${design.category === 'mural-scenic' ? `
+        <!-- ── LICENSE CTA — $950 infinite license + 150dpi download ─── -->
+        <div id="license-panel" data-design-id="${design.id}"
+             style="margin:20px 0;padding:20px 22px;background:linear-gradient(180deg,#fbf7ef,#f0e7d3);border:1px solid #c8b89a;border-radius:8px;color:#2a1f10">
+          <div style="display:flex;align-items:baseline;justify-content:space-between;gap:14px;flex-wrap:wrap">
+            <div>
+              <p style="font-size:10px;letter-spacing:.28em;text-transform:uppercase;color:#8a6f44;margin:0 0 4px;font-weight:600">License this scenic mural</p>
+              <h3 style="font-family:'Playfair Display',serif;font-weight:400;font-style:italic;margin:0;font-size:22px;color:#1f1808">Infinite-use license · 150 dpi · 20 ft × 11 ft</h3>
+            </div>
+            <div style="font-family:'Playfair Display',serif;font-size:34px;color:#1f1808;line-height:1">$950</div>
+          </div>
+          <p style="margin:10px 0 14px;font-size:13px;line-height:1.55;color:#4a3520">Perpetual unlimited-use license for the full 11 ft × 20 ft panoramic mural at 150 dpi. One-time payment, no royalties. Download the print-resolution file immediately after checkout (Real-ESRGAN-upscaled from the master).</p>
+          <form id="license-form" style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
+            <input type="email" id="license-email" placeholder="your@email.com" required autocomplete="email"
+                   style="flex:1;min-width:240px;padding:10px 12px;border:1px solid #c8b89a;border-radius:4px;font:14px var(--sans,system-ui);background:#fff">
+            <button type="submit" id="license-buy"
+                    style="padding:10px 22px;border:0;border-radius:4px;background:#1f1808;color:#f4ece0;font:13px var(--sans,system-ui);letter-spacing:.06em;text-transform:uppercase;cursor:pointer">License &amp; Download</button>
+          </form>
+          <p id="license-msg" style="margin:10px 0 0;font-size:11px;color:#7a5e34;min-height:14px"></p>
+        </div>
+        <script>
+        (function(){
+          var panel = document.getElementById('license-panel');
+          if (!panel) return;
+          var did = panel.dataset.designId;
+          var form = document.getElementById('license-form');
+          var emailInput = document.getElementById('license-email');
+          var msg = document.getElementById('license-msg');
+          var btn = document.getElementById('license-buy');
+
+          // If we just returned from Stripe with ?licensed=1, surface a download CTA
+          var params = new URLSearchParams(location.search);
+          if (params.get('licensed') === '1') {
+            var email = decodeURIComponent(params.get('email')||'');
+            emailInput.value = email;
+            msg.innerHTML = '✓ License granted. <a href="/api/design/'+did+'/download?email='+encodeURIComponent(email)+'" style="color:#1f1808;font-weight:600">Download 150 dpi →</a> <span style="opacity:.6">(first download starts an upscale, ~1 min)</span>';
+            btn.textContent = 'Re-download';
+          } else if (params.get('canceled') === '1') {
+            msg.textContent = 'Checkout canceled. No charge.';
+          } else if (params.get('error')) {
+            msg.textContent = 'Checkout error: ' + params.get('error');
+          }
+
+          form.addEventListener('submit', function(e){
+            e.preventDefault();
+            var email = (emailInput.value||'').trim();
+            if (!email) return;
+            // If they already have a license, skip Stripe — straight to download
+            fetch('/api/design/'+did+'/license/status?email='+encodeURIComponent(email))
+              .then(function(r){return r.json();})
+              .then(function(j){
+                if (j.has_license) {
+                  msg.innerHTML = '✓ You already own this. <a href="/api/design/'+did+'/download?email='+encodeURIComponent(email)+'" style="color:#1f1808;font-weight:600">Download 150 dpi →</a>';
+                  return;
+                }
+                btn.disabled = true; btn.textContent = 'Opening checkout…';
+                fetch('/api/design/'+did+'/license/checkout', {
+                  method:'POST', headers:{'Content-Type':'application/json'},
+                  body: JSON.stringify({ email: email })
+                }).then(function(r){return r.json();})
+                  .then(function(j){
+                    if (j.checkout_url) { location.href = j.checkout_url; }
+                    else { msg.textContent = 'Error: ' + (j.error||'unknown'); btn.disabled=false; btn.textContent='License & Download'; }
+                  }).catch(function(e){
+                    msg.textContent = 'Error: ' + e.message;
+                    btn.disabled = false; btn.textContent='License & Download';
+                  });
+              });
+          });
+        })();
+        </script>
+        ` : ''}
+
+        ${_isAdmin ? `
+        <!-- ── USER RATING PANEL ─────────────────────────────────────────
+             Feeds spoon_all_designs.user_color_good / style_good / scale_good /
+             stars / removed. Admin-only — POST handler is admin-gated, so
+             rendering for non-admins guarantees "save error" on every click. -->
+        <div id="rating-panel" data-design-id="${design.id}"
+             style="margin:18px 0;padding:14px 16px;background:var(--card-bg,#f4f0e8);border:1px solid var(--line,#d8d0c0);border-radius:8px;font:13px var(--sans,system-ui)">
+          <div style="display:flex;flex-wrap:wrap;gap:14px;align-items:center;justify-content:space-between">
+            <strong style="font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-faint,#7a6e5a)">Rate this design</strong>
+            <div style="display:flex;gap:8px;flex-wrap:wrap">
+              <button id="rating-remove" type="button"
+                style="background:none;border:1px solid #c84a3a;color:#c84a3a;padding:4px 10px;border-radius:14px;cursor:pointer;font-size:11px;letter-spacing:.06em"
+                title="Soft-remove from public listings — hides everywhere but row stays in DB">Remove design</button>
+              <button id="rating-settlement" type="button"
+                style="background:#b00020;border:1px solid #b00020;color:#fff;padding:4px 10px;border-radius:14px;cursor:pointer;font-size:11px;letter-spacing:.06em;font-weight:600"
+                title="HARD delete: removes from URL (404), logs motifs/prompt to settlement do-not-want registry, kicks a fresh regen with this violation excluded">Settlement Violation</button>
+            </div>
+          </div>
+
+          <!-- ADMIN REVIEW LOOP (2026-05-20) — comment + regenerate-w-comment + delete -->
+          <div id="admin-review-panel" style="margin-top:16px;padding:14px 16px;background:#faf6ec;border:1px solid #e2d7b8;border-radius:8px">
+            <div style="font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:#8a7c66;margin-bottom:8px;font-weight:600">Admin Review · Comment → Regenerate / Save / Delete</div>
+            <textarea id="ar-comment" rows="3"
+              placeholder="What's wrong / what to fix? e.g. 'too much bleed in the background, make the silhouettes harder-edged' or 'make the cacti smaller and add more breathing room'"
+              style="width:100%;box-sizing:border-box;padding:9px 11px;border:1px solid #d8cdb0;border-radius:5px;font:13px/1.4 ui-sans-serif,system-ui;background:#fff;resize:vertical;min-height:64px"></textarea>
+            <div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:10px;align-items:center">
+              <button id="ar-regen" type="button"
+                style="background:#3a8a5a;border:1px solid #2f7048;color:#fff;padding:7px 16px;border-radius:6px;cursor:pointer;font-size:12px;letter-spacing:.06em;font-weight:600"
+                title="Take this design's prompt, append your comment as additional guidance, generate a new design (parent_design_id set to this one)">Regenerate with comment →</button>
+              <button id="ar-save" type="button"
+                style="background:none;border:1px solid #c9a14b;color:#9a7a36;padding:7px 14px;border-radius:6px;cursor:pointer;font-size:12px;letter-spacing:.06em"
+                title="Save your comment to this design's notes for later reference, no regen">Save comment</button>
+              <button id="ar-delete" type="button"
+                style="background:none;border:1px solid #c84a3a;color:#c84a3a;padding:7px 14px;border-radius:6px;cursor:pointer;font-size:12px;letter-spacing:.06em;margin-left:auto"
+                title="Soft-delete this SKU (user_removed=TRUE)">Delete SKU</button>
+            </div>
+            <div id="ar-status" style="font-size:11px;color:#8a7c66;margin-top:8px;min-height:14px"></div>
+          </div>
+          <script>
+          (function(){
+            var did = ${design.id};
+            var commentEl = document.getElementById('ar-comment');
+            var statusEl = document.getElementById('ar-status');
+            function setStatus(msg, isErr) {
+              statusEl.textContent = msg;
+              statusEl.style.color = isErr ? '#c84a3a' : '#3a8a5a';
+            }
+            document.getElementById('ar-regen').addEventListener('click', function(){
+              var c = (commentEl.value || '').trim();
+              if (!c) { setStatus('Add a comment first — what should be different?', true); return; }
+              setStatus('Spawning regen — new design lands in ~30-90s. Refresh /designs after.', false);
+              fetch('/api/design/' + did + '/regenerate-with-comment', {
+                method: 'POST',
+                headers: { 'Content-Type': 'application/json' },
+                body: JSON.stringify({ comment: c })
+              }).then(function(r){ return r.json(); }).then(function(j){
+                if (j.ok) { setStatus('OK — regen spawned. New design in ~60s.', false); }
+                else { setStatus('Error: ' + (j.error || 'unknown'), true); }
+              }).catch(function(e){ setStatus('Network error: ' + e.message, true); });
+            });
+            document.getElementById('ar-save').addEventListener('click', function(){
+              var c = (commentEl.value || '').trim();
+              if (!c) { setStatus('Empty comment — nothing to save.', true); return; }
+              fetch('/api/design/' + did + '/comment', {
+                method: 'POST',
+                headers: { 'Content-Type': 'application/json' },
+                body: JSON.stringify({ comment: c })
+              }).then(function(r){ return r.json(); }).then(function(j){
+                if (j.ok) { setStatus('Comment saved to notes.', false); }
+                else { setStatus('Error: ' + (j.error || 'unknown'), true); }
+              }).catch(function(e){ setStatus('Network error: ' + e.message, true); });
+            });
+            document.getElementById('ar-delete').addEventListener('click', function(){
+              if (!confirm('Delete SKU ' + did + '? It will disappear from /designs.')) return;
+              fetch('/api/design/' + did + '/remove', { method: 'POST' })
+                .then(function(r){ return r.json(); }).then(function(j){
+                  if (j.ok) { setStatus('Deleted. Redirecting to /designs...', false); setTimeout(function(){ location.href = '/designs'; }, 800); }
+                  else { setStatus('Error: ' + (j.error || 'unknown'), true); }
+                }).catch(function(e){ setStatus('Network error: ' + e.message, true); });
+            });
+          })();
+          </script>
+          <div style="display:flex;gap:18px;flex-wrap:wrap;margin-top:12px;align-items:center">
+            <div class="rate-axis" data-axis="color_good">
+              <span class="rate-label">Color combo</span>
+              <button class="rate-btn rate-yes" data-val="true">Good</button>
+              <button class="rate-btn rate-no"  data-val="false">No</button>
+            </div>
+            <div class="rate-axis" data-axis="style_good">
+              <span class="rate-label">Style</span>
+              <button class="rate-btn rate-yes" data-val="true">Good</button>
+              <button class="rate-btn rate-no"  data-val="false">No</button>
+            </div>
+            <div class="rate-axis" data-axis="scale_good">
+              <span class="rate-label">Scale</span>
+              <button class="rate-btn rate-yes" data-val="true">Good</button>
+              <button class="rate-btn rate-no"  data-val="false">No</button>
+            </div>
+            <div class="rate-axis" data-axis="stars" style="display:flex;flex-direction:column;gap:4px">
+              <span class="rate-label">Stars</span>
+              <span class="rate-stars" id="rate-stars">
+                ${[1,2,3,4,5].map(n => `<button class="rate-star" data-val="${n}" type="button" title="${n} star${n>1?'s':''}">★</button>`).join('')}
+              </span>
+            </div>
+            <span id="rating-status" style="font-size:11px;color:var(--ink-faint,#888);margin-left:auto"></span>
+          </div>
+        </div>
+        <style>
+          #rating-panel .rate-axis { display:inline-flex; flex-direction:column; gap:5px; }
+          #rating-panel .rate-label { font-size:10px; letter-spacing:.14em; text-transform:uppercase; color:var(--ink-faint,#8a7c66); }
+          #rating-panel .rate-btn { background:transparent; border:1px solid var(--line,#c8bfac); color:var(--ink,#3a2818); padding:3px 10px; border-radius:14px; cursor:pointer; font-size:11px; letter-spacing:.04em; margin-right:4px; }
+          #rating-panel .rate-btn:hover { border-color:var(--ink,#3a2818); }
+          #rating-panel .rate-btn.is-on.rate-yes { background:#3a8a5a; border-color:#3a8a5a; color:#fff; }
+          #rating-panel .rate-btn.is-on.rate-no  { background:#c84a3a; border-color:#c84a3a; color:#fff; }
+          #rating-panel .rate-stars { display:inline-flex; gap:2px; }
+          #rating-panel .rate-star { background:transparent; border:0; color:#c8bfac; font-size:18px; padding:0 2px; cursor:pointer; line-height:1; }
+          #rating-panel .rate-star.is-on { color:#c9a14b; }
+          #rating-panel .rate-star:hover, #rating-panel .rate-star:hover ~ .rate-star { color:#c8bfac; }
+          #rating-panel .rate-star:hover { color:#c9a14b; }
+        </style>
+        <script>
+        (function(){
+          var panel = document.getElementById('rating-panel');
+          if (!panel) return;
+          var did = panel.dataset.designId;
+          var statusEl = document.getElementById('rating-status');
+          // Seed current state from injected design data (rendered server-side)
+          var state = {
+            color_good: ${design.user_color_good === true || design.user_color_good === false ? design.user_color_good : 'null'},
+            style_good: ${design.user_style_good === true || design.user_style_good === false ? design.user_style_good : 'null'},
+            scale_good: ${design.user_scale_good === true || design.user_scale_good === false ? design.user_scale_good : 'null'},
+            stars:      ${Number.isFinite(design.user_stars) ? design.user_stars : 'null'}
+          };
+          function paint() {
+            ['color_good','style_good','scale_good'].forEach(function(axis){
+              panel.querySelectorAll('[data-axis="'+axis+'"] .rate-btn').forEach(function(b){
+                b.classList.toggle('is-on', String(state[axis]) === b.dataset.val);
+              });
+            });
+            panel.querySelectorAll('.rate-star').forEach(function(s){
+              s.classList.toggle('is-on', state.stars != null && parseInt(s.dataset.val,10) <= state.stars);
+            });
+          }
+          paint();
+          function save(patch, doneText) {
+            statusEl.textContent = 'saving…';
+            fetch('/api/design/' + did + '/rate', {
+              method: 'POST', headers: {'Content-Type':'application/json'}, credentials:'same-origin',
+              body: JSON.stringify(patch)
+            }).then(function(r){
+              if (!r.ok) throw new Error('save failed');
+              return r.json();
+            }).then(function(){
+              Object.assign(state, patch);
+              paint();
+              statusEl.textContent = doneText || 'saved';
+              setTimeout(function(){ statusEl.textContent=''; }, 1500);
+            }).catch(function(e){ statusEl.textContent = 'save error'; });
+          }
+          // Yes/No toggle clicks
+          panel.querySelectorAll('.rate-axis[data-axis$="_good"] .rate-btn').forEach(function(btn){
+            btn.addEventListener('click', function(){
+              var axis = btn.closest('.rate-axis').dataset.axis;
+              var clicked = btn.dataset.val === 'true';
+              var current = state[axis];
+              // Toggle off if clicking the already-on choice; otherwise set.
+              var next = (current === clicked) ? null : clicked;
+              var patch = {}; patch[axis.replace('_good','_good')] = next;
+              save({[axis]: next});
+            });
+          });
+          // Stars
+          panel.querySelectorAll('.rate-star').forEach(function(s){
+            s.addEventListener('click', function(){
+              var v = parseInt(s.dataset.val,10);
+              var next = state.stars === v ? null : v;
+              save({ stars: next });
+            });
+          });
+          // Remove
+          document.getElementById('rating-remove').addEventListener('click', function(){
+            if (!confirm('Soft-remove design #'+did+'? It will be hidden from the grid but stays in DB (reversible).')) return;
+            statusEl.textContent = 'removing…';
+            fetch('/api/design/' + did + '/remove', { method: 'POST', credentials:'same-origin' })
+              .then(function(r){ return r.json(); })
+              .then(function(j){
+                statusEl.textContent = 'removed';
+                setTimeout(function(){ location.href = '/designs'; }, 800);
+              }).catch(function(){ statusEl.textContent = 'remove error'; });
+          });
+          // Settlement Violation — HARD delete + log to do-not-want + queue regen.
+          // Only to be used for content we DO NOT WANT (e.g., banana leaves).
+          document.getElementById('rating-settlement').addEventListener('click', function(){
+            var reason = prompt('Settlement Violation reason (e.g. "banana leaves visible", "monstera", "tropical foliage"):');
+            if (!reason || !reason.trim()) return;
+            if (!confirm('HARD delete design #'+did+'?\\nThis will:\\n  • remove the URL (404)\\n  • delete the file from disk\\n  • log the prompt/motifs/tags to the settlement do-not-want registry\\n  • queue a fresh regen excluding this violation\\nThis is NOT reversible.')) return;
+            statusEl.textContent = 'logging violation + regenerating…';
+            var params = new URLSearchParams(window.location.search);
+            var adminToken = params.get('admin') || localStorage.getItem('wallco_admin_token') || '';
+            fetch('/api/design/' + did + '/settlement-violation', {
+              method: 'POST',
+              headers: { 'Content-Type': 'application/json', 'X-Admin-Token': adminToken },
+              credentials:'same-origin',
+              body: JSON.stringify({ reason: reason.trim() })
+            })
+              .then(function(r){ return r.json().then(function(j){ return { ok:r.ok, j:j }; }); })
+              .then(function(res){
+                if (!res.ok) {
+                  statusEl.textContent = 'error: ' + (res.j.error || 'unknown');
+                  return;
+                }
+                statusEl.textContent = 'violation logged · regen queued';
+                setTimeout(function(){ location.href = '/designs'; }, 1200);
+              })
+              .catch(function(e){ statusEl.textContent = 'settlement-violation error: ' + e.message; });
+          });
+        })();
+        </script>
+        ` : ''}
+
+        <div class="scale-preview" style="margin-top:18px">
+          <div class="scale-controls" style="display:flex;align-items:center;gap:14px;margin-bottom:10px;flex-wrap:wrap">
+            <label style="font:11px var(--sans);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-faint);display:flex;align-items:center;gap:10px;flex:1;min-width:240px">
+              Pattern scale
+              <input type="range" id="scale-slider" min="4" max="24" step="2" value="24" style="flex:1;max-width:280px">
+            </label>
+            <span id="scale-readout" style="font-family:var(--serif);font-size:18px;color:var(--ink);min-width:90px">24" repeat</span>
+          </div>
+          <!-- Mural-size repeat stepper. Click Next → to walk through 1× (full
+               tile · MURAL MODE with pan + zoom), 2×, 4×, 8× (room-scale),
+               16× (mural-scale) repeat counts.
+               At level 0 (1×1) the viewport switches to MURAL MODE: drag to
+               pan the image, scroll to zoom, double-click to reset.
+               Patterned after grahambrown.com and rebelwalls.com mural tools. -->
+          <div class="mural-stepper" style="display:flex;align-items:center;gap:12px;margin-bottom:10px;flex-wrap:wrap">
+            <span style="font:11px var(--sans);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-faint)">Mural repeat</span>
+            <div id="mural-dots" style="display:flex;gap:6px">
+              <span class="mural-dot active" data-level="0" title="Mural — full tile · 1×1 · drag to position"></span>
+              <span class="mural-dot" data-level="1" title="2×2"></span>
+              <span class="mural-dot" data-level="2" title="4×4"></span>
+              <span class="mural-dot" data-level="3" title="8×8 — room"></span>
+              <span class="mural-dot" data-level="4" title="16×16 — mural"></span>
+            </div>
+            <span id="mural-readout" style="font-family:var(--serif);font-size:14px;color:var(--ink-soft,#555);min-width:200px">Mural · 1×1 — drag to position</span>
+            <button type="button" id="mural-reset" style="padding:6px 10px;border:1px solid var(--line);background:transparent;color:var(--ink-soft);font:10.5px var(--sans);letter-spacing:.06em;text-transform:uppercase;border-radius:4px;cursor:pointer;display:none">Reset</button>
+            <button type="button" id="mural-next" style="padding:6px 12px;border:1px solid var(--line);background:transparent;color:var(--ink);font:11px var(--sans);font-weight:600;letter-spacing:.06em;text-transform:uppercase;border-radius:4px;cursor:pointer;margin-left:auto">Next →</button>
+          </div>
+          <style>
+            .mural-dot { width:8px; height:8px; border-radius:50%; background:#d8d2c4; border:1px solid #b9b3a3; transition: background .15s, transform .15s; cursor:pointer; }
+            .mural-dot.active { background: var(--gold,#c9a14b); border-color: var(--gold,#c9a14b); transform: scale(1.25); }
+            #scale-viewport.mural-mode { cursor: grab; }
+            #scale-viewport.mural-mode.dragging { cursor: grabbing; }
+            #scale-viewport .mural-hint { position:absolute; bottom:8px; left:10px; right:10px; display:flex; justify-content:space-between; align-items:center; font:10px ui-monospace,Menlo,monospace; color:#fff; text-shadow:0 1px 2px rgba(0,0,0,.7); letter-spacing:.04em; pointer-events:none; opacity:.85; }
+            #scale-viewport .mural-hint .zoom-pill { padding:2px 7px; background:rgba(0,0,0,.42); border-radius:999px; pointer-events:auto; }
+          </style>
+          <div id="scale-viewport" style="width:100%;aspect-ratio:3/2;border:1px solid var(--line);border-radius:8px;overflow:hidden;background-image:url('${design.image_url}');background-repeat:repeat;background-size:160px 160px;position:relative;user-select:none;-webkit-user-select:none">
+            <div class="mural-hint" id="mural-hint" style="display:none">
+              <span>Drag to position · scroll to zoom · double-click to reset</span>
+              <span class="zoom-pill" id="mural-zoom-pill">100%</span>
+            </div>
+          </div>
+          <script>
+          (function(){
+            // Mural-size stepper. Cycles 1×→2×→4×→8×→16× repeat counts on the
+            // existing #scale-viewport. Labels: "Mural · 1×1 — drag to position",
+            // "2×2", "4×4", "8×8 · room scale", "16×16 · mural". Persists last
+            // level in localStorage under "wallco.detail.mural-level".
+            //
+            // At LEVEL 0 the viewport enters MURAL MODE — the design fills the
+            // whole wall as ONE panel and the user can drag/zoom to pick the
+            // exact crop. This matches the UX on grahambrown.com murals and
+            // rebelwalls.com — the user, not the algorithm, decides what part
+            // of the image lands on their wall.
+            const LEVELS = [
+              { count: 1,  label: 'Mural · 1×1 — drag to position' },
+              { count: 2,  label: '2×2' },
+              { count: 4,  label: '4×4' },
+              { count: 8,  label: '8×8 · room scale' },
+              { count: 16, label: '16×16 · mural' },
+            ];
+            const vp     = document.getElementById('scale-viewport');
+            const dots   = document.querySelectorAll('#mural-dots .mural-dot');
+            const read   = document.getElementById('mural-readout');
+            const btn    = document.getElementById('mural-next');
+            const resetBtn = document.getElementById('mural-reset');
+            const hint   = document.getElementById('mural-hint');
+            const zoomPill = document.getElementById('mural-zoom-pill');
+            const SLIDER = document.getElementById('scale-slider');
+            const DESIGN_ID = ${JSON.stringify(design.id || '')};
+            const LS_LEVEL = 'wallco.detail.mural-level';
+            const LS_CROP  = 'wallco.detail.mural-crop.' + DESIGN_ID;
+            let level = 0;
+            // mural crop state — per-design
+            let crop = { zoom: 1, px: 50, py: 50 }; // zoom 1 = "cover the viewport once"; px/py % position
+            try { const stored = localStorage.getItem(LS_LEVEL); if (stored!=null) level = Math.min(Math.max(parseInt(stored,10)||0, 0), LEVELS.length-1); } catch {}
+            try { const saved = JSON.parse(localStorage.getItem(LS_CROP) || 'null'); if (saved && typeof saved.zoom === 'number') crop = Object.assign(crop, saved); } catch {}
+            function persistCrop() { try { localStorage.setItem(LS_CROP, JSON.stringify(crop)); } catch {} }
+            function applyCrop() {
+              const sizePct = (100 * crop.zoom).toFixed(1) + '%';
+              vp.style.backgroundSize = sizePct + ' ' + sizePct;
+              vp.style.backgroundRepeat = 'no-repeat';
+              vp.style.backgroundPosition = crop.px.toFixed(2) + '% ' + crop.py.toFixed(2) + '%';
+              if (zoomPill) zoomPill.textContent = Math.round(crop.zoom * 100) + '%';
+            }
+            function apply() {
+              const { count, label } = LEVELS[level];
+              dots.forEach((d,i) => d.classList.toggle('active', i===level));
+              if (read) read.textContent = label;
+              try { localStorage.setItem(LS_LEVEL, String(level)); } catch {}
+              if (level === 0) {
+                vp.classList.add('mural-mode');
+                if (hint) hint.style.display = 'flex';
+                if (resetBtn) resetBtn.style.display = '';
+                applyCrop();
+              } else {
+                vp.classList.remove('mural-mode');
+                if (hint) hint.style.display = 'none';
+                if (resetBtn) resetBtn.style.display = 'none';
+                vp.style.backgroundRepeat = 'repeat';
+                vp.style.backgroundSize = (100/count).toFixed(2) + '% ' + (100/count).toFixed(2) + '%';
+                vp.style.backgroundPosition = '0% 0%';
+              }
+            }
+            if (btn) btn.addEventListener('click', () => { level = (level + 1) % LEVELS.length; apply(); });
+            dots.forEach(d => d.addEventListener('click', (e) => { level = parseInt(e.currentTarget.dataset.level,10) || 0; apply(); }));
+            // The legacy "Pattern scale" slider keeps working at levels 1-4.
+            // At level 0 (mural mode) the slider hands off to drag/zoom.
+            if (SLIDER) SLIDER.addEventListener('input', () => {
+              if (level === 0) return; // mural mode owns the viewport
+              level = 0;
+              dots.forEach(d => d.classList.remove('active'));
+              dots[0] && dots[0].classList.add('active');
+              if (read) read.textContent = LEVELS[0].label;
+              apply();
+            });
+
+            // ── Mural mode interactions: drag-to-pan, wheel-to-zoom, dblclick reset.
+            let drag = null;
+            vp.addEventListener('mousedown', (e) => {
+              if (level !== 0) return;
+              drag = { x: e.clientX, y: e.clientY, px0: crop.px, py0: crop.py };
+              vp.classList.add('dragging');
+              e.preventDefault();
+            });
+            window.addEventListener('mousemove', (e) => {
+              if (!drag || level !== 0) return;
+              const rect = vp.getBoundingClientRect();
+              const dx = (e.clientX - drag.x) / rect.width * 100;
+              const dy = (e.clientY - drag.y) / rect.height * 100;
+              // Drag right → reveal left side of image (background-position % goes down)
+              crop.px = Math.max(0, Math.min(100, drag.px0 - dx));
+              crop.py = Math.max(0, Math.min(100, drag.py0 - dy));
+              applyCrop();
+            });
+            window.addEventListener('mouseup', () => {
+              if (drag) { drag = null; vp.classList.remove('dragging'); persistCrop(); }
+            });
+            // touch support
+            vp.addEventListener('touchstart', (e) => {
+              if (level !== 0 || e.touches.length !== 1) return;
+              drag = { x: e.touches[0].clientX, y: e.touches[0].clientY, px0: crop.px, py0: crop.py };
+              vp.classList.add('dragging');
+            }, { passive: true });
+            vp.addEventListener('touchmove', (e) => {
+              if (!drag || level !== 0 || e.touches.length !== 1) return;
+              const rect = vp.getBoundingClientRect();
+              const dx = (e.touches[0].clientX - drag.x) / rect.width * 100;
+              const dy = (e.touches[0].clientY - drag.y) / rect.height * 100;
+              crop.px = Math.max(0, Math.min(100, drag.px0 - dx));
+              crop.py = Math.max(0, Math.min(100, drag.py0 - dy));
+              applyCrop();
+              e.preventDefault();
+            }, { passive: false });
+            vp.addEventListener('touchend', () => { if (drag) { drag = null; vp.classList.remove('dragging'); persistCrop(); } });
+            // wheel-to-zoom (only in mural mode, only when modifier or hover — avoid stealing page scroll)
+            vp.addEventListener('wheel', (e) => {
+              if (level !== 0) return;
+              e.preventDefault();
+              const dz = e.deltaY < 0 ? 1.08 : 1/1.08;
+              crop.zoom = Math.max(0.5, Math.min(6, crop.zoom * dz));
+              applyCrop();
+              persistCrop();
+            }, { passive: false });
+            // double-click → reset
+            vp.addEventListener('dblclick', (e) => {
+              if (level !== 0) return;
+              crop = { zoom: 1, px: 50, py: 50 };
+              applyCrop();
+              persistCrop();
+            });
+            if (resetBtn) resetBtn.addEventListener('click', () => {
+              crop = { zoom: 1, px: 50, py: 50 };
+              applyCrop();
+              persistCrop();
+            });
+            apply();
+          })();
+          </script>
+          <div style="display:flex;justify-content:space-between;font:10.5px var(--sans);color:var(--ink-faint);margin-top:6px;letter-spacing:.04em">
+            <span>← Smaller repeat (denser pattern)</span>
+            <span id="scale-width-hint">24" wide, sold per yard</span>
+            <span>Larger repeat →</span>
+          </div>
+
+          <!-- Roll-width selector (24 / 36 / 52) — mirrors the right-column tabs; both sync via localStorage. -->
+          <div class="width-slider-row" style="margin-top:22px;padding-top:18px;border-top:1px solid var(--line)">
+            <label style="font:11px var(--sans);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-faint);display:block;margin-bottom:8px">Roll width</label>
+            <div class="width-tabs" id="width-tabs-below-scale" data-native-w="${design.width_in || ''}" style="display:flex;gap:8px">
+              <button type="button" class="w-tab" data-w="24" style="flex:1;padding:10px 14px;border:1px solid var(--line);background:transparent;color:var(--ink);font:13px var(--sans);font-weight:500;border-radius:6px;cursor:pointer">24" residential</button>
+              <button type="button" class="w-tab" data-w="36" style="flex:1;padding:10px 14px;border:1px solid var(--line);background:transparent;color:var(--ink);font:13px var(--sans);font-weight:500;border-radius:6px;cursor:pointer">36" wide-roll</button>
+              <button type="button" class="w-tab" data-w="52" style="flex:1;padding:10px 14px;border:1px solid var(--line);background:transparent;color:var(--ink);font:13px var(--sans);font-weight:500;border-radius:6px;cursor:pointer">52" commercial</button>
+            </div>
+            <div style="font:10.5px var(--sans);color:var(--ink-faint);margin-top:6px;letter-spacing:.04em">
+              Pick your roll width — all three are available; the request form below will lock in your choice.
+            </div>
+          </div>
+
+          ${_isAdmin ? `
+          <!-- Admin: tone-on-tone color-wheel recoloring. Per design click,
+               spins up a Gemini image-edit in the target hue. Steve's
+               2026-05-13 ask: "Add buttons to admin to create in color
+               wheel color. for tone on tone items." -->
+          <div class="recolor-wheel" style="margin-top:22px;padding-top:18px;border-top:1px solid var(--line)">
+            <label style="font:11px var(--sans);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-faint);display:block;margin-bottom:8px">Admin · tone-on-tone color wheel</label>
+            <p style="font:11px var(--sans);color:var(--ink-faint);margin:0 0 10px">Click a hue to generate a tone-on-tone variant. Each takes ~5-15s &amp; costs ~$0.001 (gemini-image-edit).</p>
+            <div id="recolor-buttons" style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px">
+              <button data-hue="red"      style="padding:6px 14px;background:#dc2626;color:#fff;border:0;border-radius:6px;cursor:pointer;font:600 11px var(--sans);text-transform:uppercase;letter-spacing:.04em">Red</button>
+              <button data-hue="orange"   style="padding:6px 14px;background:#ea580c;color:#fff;border:0;border-radius:6px;cursor:pointer;font:600 11px var(--sans);text-transform:uppercase;letter-spacing:.04em">Orange</button>
+              <button data-hue="yellow"   style="padding:6px 14px;background:#ca8a04;color:#fff;border:0;border-radius:6px;cursor:pointer;font:600 11px var(--sans);text-transform:uppercase;letter-spacing:.04em">Yellow</button>
+              <button data-hue="lime"     style="padding:6px 14px;background:#65a30d;color:#fff;border:0;border-radius:6px;cursor:pointer;font:600 11px var(--sans);text-transform:uppercase;letter-spacing:.04em">Lime</button>
+              <button data-hue="green"    style="padding:6px 14px;background:#16a34a;color:#fff;border:0;border-radius:6px;cursor:pointer;font:600 11px var(--sans);text-transform:uppercase;letter-spacing:.04em">Green</button>
+              <button data-hue="teal"     style="padding:6px 14px;background:#0d9488;color:#fff;border:0;border-radius:6px;cursor:pointer;font:600 11px var(--sans);text-transform:uppercase;letter-spacing:.04em">Teal</button>
+              <button data-hue="cyan"     style="padding:6px 14px;background:#0891b2;color:#fff;border:0;border-radius:6px;cursor:pointer;font:600 11px var(--sans);text-transform:uppercase;letter-spacing:.04em">Cyan</button>
+              <button data-hue="blue"     style="padding:6px 14px;background:#2563eb;color:#fff;border:0;border-radius:6px;cursor:pointer;font:600 11px var(--sans);text-transform:uppercase;letter-spacing:.04em">Blue</button>
+              <button data-hue="indigo"   style="padding:6px 14px;background:#4338ca;color:#fff;border:0;border-radius:6px;cursor:pointer;font:600 11px var(--sans);text-transform:uppercase;letter-spacing:.04em">Indigo</button>
+              <button data-hue="violet"   style="padding:6px 14px;background:#7c3aed;color:#fff;border:0;border-radius:6px;cursor:pointer;font:600 11px var(--sans);text-transform:uppercase;letter-spacing:.04em">Violet</button>
+              <button data-hue="magenta"  style="padding:6px 14px;background:#c026d3;color:#fff;border:0;border-radius:6px;cursor:pointer;font:600 11px var(--sans);text-transform:uppercase;letter-spacing:.04em">Magenta</button>
+              <button data-hue="pink"     style="padding:6px 14px;background:#db2777;color:#fff;border:0;border-radius:6px;cursor:pointer;font:600 11px var(--sans);text-transform:uppercase;letter-spacing:.04em">Pink</button>
+              <button data-hue="neutral"  style="padding:6px 14px;background:#a16207;color:#fff;border:0;border-radius:6px;cursor:pointer;font:600 11px var(--sans);text-transform:uppercase;letter-spacing:.04em">Neutral</button>
+              <button data-hue="charcoal" style="padding:6px 14px;background:#262626;color:#fff;border:0;border-radius:6px;cursor:pointer;font:600 11px var(--sans);text-transform:uppercase;letter-spacing:.04em">Charcoal</button>
+              <button id="recolor-all" style="padding:6px 14px;background:linear-gradient(90deg,#dc2626,#ea580c,#ca8a04,#16a34a,#0891b2,#2563eb,#7c3aed,#db2777);color:#fff;border:0;border-radius:6px;cursor:pointer;font:600 11px var(--sans);text-transform:uppercase;letter-spacing:.04em;flex-basis:100%;margin-top:4px">✦ Spin all 14 hues (parallel)</button>
+            </div>
+            <div id="recolor-status" style="font:12px var(--sans);color:var(--ink-soft,#555);min-height:18px"></div>
+            <div id="recolor-results" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:8px;margin-top:10px"></div>
+          </div>
+          <script>
+          (function(){
+            const stat = document.getElementById('recolor-status');
+            const results = document.getElementById('recolor-results');
+            const fireOne = async (hue, btn) => {
+              if (btn) { btn.disabled = true; btn.style.opacity = '0.5'; }
+              const startTxt = '→ ' + hue + '…';
+              stat.textContent = (stat.textContent ? stat.textContent + '  ' : '') + startTxt;
+              try {
+                const r = await fetch('/api/design/${design.id}/recolor', {
+                  method: 'POST', headers: { 'Content-Type': 'application/json' },
+                  body: JSON.stringify({ hue }),
+                });
+                const j = await r.json();
+                if (!j.ok) throw new Error(j.error || 'failed');
+                const card = document.createElement('div');
+                card.innerHTML = '<a href="/design/' + j.new_id + '" target="_blank" style="display:block;text-decoration:none;color:inherit">'
+                  + '<img src="' + j.new_image_url + '" style="width:100%;aspect-ratio:1;object-fit:cover;border-radius:6px;border:1px solid var(--line)">'
+                  + '<div style="font:10px var(--sans);text-align:center;margin-top:4px;color:var(--ink-soft,#555)">' + hue + ' · #' + j.new_id + '</div>'
+                  + '</a>';
+                results.appendChild(card);
+                stat.textContent = stat.textContent.replace(startTxt, '✓ ' + hue);
+              } catch (e) {
+                stat.textContent = stat.textContent.replace(startTxt, '✗ ' + hue + ':' + e.message);
+              } finally {
+                if (btn) { btn.disabled = false; btn.style.opacity = '1'; }
+              }
+            };
+            document.querySelectorAll('#recolor-buttons button[data-hue]').forEach(b => {
+              b.addEventListener('click', () => fireOne(b.dataset.hue, b));
+            });
+            const allBtn = document.getElementById('recolor-all');
+            if (allBtn) allBtn.addEventListener('click', async () => {
+              allBtn.disabled = true; allBtn.style.opacity = '0.5';
+              const hues = Array.from(document.querySelectorAll('#recolor-buttons button[data-hue]')).map(b => b.dataset.hue);
+              stat.textContent = 'firing ' + hues.length + ' in parallel…';
+              await Promise.all(hues.map(h => fireOne(h, null)));
+              allBtn.disabled = false; allBtn.style.opacity = '1';
+            });
+          })();
+          </script>
+          ` : ''}
+
+          <!-- Share this design — Pinterest (one-click Pin) + Instagram
+               (download image + open IG). Per Steve's 2026-05-13 ask. -->
+          <div class="share-block" style="margin-top:22px;padding-top:18px;border-top:1px solid var(--line)">
+            <label style="font:11px var(--sans);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-faint);display:block;margin-bottom:8px">Share this design</label>
+            <div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
+              <button type="button" id="share-pinterest" style="padding:10px 16px;background:#e60023;color:#fff;border:0;border-radius:6px;font:600 12px var(--sans);letter-spacing:.06em;text-transform:uppercase;cursor:pointer;display:inline-flex;align-items:center;gap:6px">
+                <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.4 0 0 5.4 0 12c0 5.1 3.1 9.4 7.6 11.2-.1-.9-.2-2.4 0-3.4.2-.9 1.4-5.8 1.4-5.8s-.4-.7-.4-1.8c0-1.7 1-2.9 2.2-2.9 1 0 1.5.8 1.5 1.7 0 1-.7 2.6-1 4-.3 1.2.6 2.1 1.7 2.1 2.1 0 3.7-2.2 3.7-5.4 0-2.8-2-4.8-4.9-4.8-3.3 0-5.3 2.5-5.3 5.1 0 1 .4 2.1.9 2.7.1.1.1.2.1.3l-.3 1.4c-.1.2-.2.3-.4.2-1.5-.7-2.4-2.9-2.4-4.6 0-3.7 2.7-7.1 7.8-7.1 4.1 0 7.3 2.9 7.3 6.8 0 4.1-2.6 7.4-6.2 7.4-1.2 0-2.3-.6-2.7-1.4l-.7 2.8c-.3 1-1 2.3-1.5 3.1 1.1.3 2.3.5 3.5.5 6.6 0 12-5.4 12-12C24 5.4 18.6 0 12 0z"/></svg>
+                Pin it
+              </button>
+              <button type="button" id="share-instagram" style="padding:10px 16px;background:linear-gradient(45deg,#f09433,#e6683c,#dc2743,#cc2366,#bc1888);color:#fff;border:0;border-radius:6px;font:600 12px var(--sans);letter-spacing:.06em;text-transform:uppercase;cursor:pointer;display:inline-flex;align-items:center;gap:6px">
+                <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.16c3.2 0 3.58 0 4.85.07 1.17.05 1.8.25 2.23.41.56.22.96.48 1.38.9.42.42.68.82.9 1.38.16.42.36 1.06.41 2.23.06 1.27.07 1.64.07 4.85s0 3.58-.07 4.85c-.05 1.17-.25 1.8-.41 2.23-.22.56-.48.96-.9 1.38a3.7 3.7 0 0 1-1.38.9c-.42.16-1.06.36-2.23.41-1.27.06-1.64.07-4.85.07s-3.58 0-4.85-.07c-1.17-.05-1.8-.25-2.23-.41a3.7 3.7 0 0 1-1.38-.9 3.7 3.7 0 0 1-.9-1.38c-.16-.42-.36-1.06-.41-2.23C2.16 15.58 2.16 15.2 2.16 12s0-3.58.07-4.85c.05-1.17.25-1.8.41-2.23.22-.56.48-.96.9-1.38.42-.42.82-.68 1.38-.9.42-.16 1.06-.36 2.23-.41C8.42 2.16 8.8 2.16 12 2.16M12 0C8.74 0 8.33 0 7.05.07 5.78.13 4.9.33 4.14.63a5.85 5.85 0 0 0-2.13 1.38A5.85 5.85 0 0 0 .63 4.14C.33 4.9.13 5.78.07 7.05.01 8.33 0 8.74 0 12c0 3.26.01 3.67.07 4.95.06 1.27.26 2.15.56 2.91.31.79.73 1.46 1.38 2.13a5.85 5.85 0 0 0 2.13 1.38c.76.3 1.64.5 2.91.56C8.33 23.99 8.74 24 12 24c3.26 0 3.67-.01 4.95-.07 1.27-.06 2.15-.26 2.91-.56a5.85 5.85 0 0 0 2.13-1.38 5.85 5.85 0 0 0 1.38-2.13c.3-.76.5-1.64.56-2.91.06-1.28.07-1.69.07-4.95s-.01-3.67-.07-4.95c-.06-1.27-.26-2.15-.56-2.91a5.85 5.85 0 0 0-1.38-2.13A5.85 5.85 0 0 0 19.86.63C19.1.33 18.22.13 16.95.07 15.67.01 15.26 0 12 0zm0 5.84A6.16 6.16 0 1 0 18.16 12 6.16 6.16 0 0 0 12 5.84M12 16a4 4 0 1 1 4-4 4 4 0 0 1-4 4m6.41-11.85a1.44 1.44 0 1 0 1.44 1.44 1.44 1.44 0 0 0-1.44-1.44"/></svg>
+                Share to Instagram
+              </button>
+              <button type="button" id="share-copy-link" style="padding:10px 14px;background:#fff;color:var(--ink);border:1px solid var(--line);border-radius:6px;font:600 12px var(--sans);letter-spacing:.06em;text-transform:uppercase;cursor:pointer">
+                🔗 Copy link
+              </button>
+              <span id="share-status" class="muted" style="font:11px var(--sans);color:var(--ink-faint);"></span>
+            </div>
+            <div style="font:10.5px var(--sans);color:var(--ink-faint);margin-top:6px;letter-spacing:.04em">
+              Pin opens Pinterest's pin-it window. Instagram downloads the design image — open IG, tap +, choose the saved image.
+            </div>
+          </div>
+          <script>
+          (function(){
+            const pageUrl = window.location.href;
+            const imgUrl = ${JSON.stringify('https://wallco.ai' + design.image_url)};
+            const title  = ${JSON.stringify((design.title || 'wallco.ai design') + ' — wallco.ai original wallpaper')};
+            const handle = ${JSON.stringify(design.handle || ('design-' + design.id))};
+            const stat   = document.getElementById('share-status');
+
+            // Pinterest — opens the pin-create window in a small popup.
+            // Pinterest fetches the image directly from imgUrl + back-links
+            // to pageUrl.
+            const pinBtn = document.getElementById('share-pinterest');
+            if (pinBtn) pinBtn.addEventListener('click', () => {
+              const u = 'https://pinterest.com/pin/create/button/?url='
+                + encodeURIComponent(pageUrl)
+                + '&media=' + encodeURIComponent(imgUrl)
+                + '&description=' + encodeURIComponent(title);
+              window.open(u, 'pinit', 'width=750,height=620,scrollbars=yes');
+            });
+
+            // Instagram — IG has no public share URL. Best UX on desktop:
+            // download the image, then prompt user to upload it in IG.
+            // On mobile, attempt the instagram://library deep link first;
+            // fall back to download.
+            const igBtn = document.getElementById('share-instagram');
+            if (igBtn) igBtn.addEventListener('click', async () => {
+              stat.textContent = 'preparing image…';
+              try {
+                const r = await fetch(imgUrl);
+                const blob = await r.blob();
+                const a = document.createElement('a');
+                a.href = URL.createObjectURL(blob);
+                a.download = handle + '.png';
+                document.body.appendChild(a); a.click(); a.remove();
+                setTimeout(() => URL.revokeObjectURL(a.href), 1000);
+                const isMobile = /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent);
+                if (isMobile) {
+                  stat.innerHTML = '✓ Saved — <a href="instagram://library" style="color:var(--accent)">open Instagram</a>';
+                } else {
+                  stat.innerHTML = '✓ Saved <strong>' + handle + '.png</strong> — open Instagram on your phone, tap +, choose the image.';
+                }
+              } catch (e) {
+                stat.innerHTML = '<span style="color:#b91c1c">Download failed: ' + (e.message || String(e)) + '</span>';
+              }
+            });
+
+            // Copy link to clipboard
+            const copyBtn = document.getElementById('share-copy-link');
+            if (copyBtn) copyBtn.addEventListener('click', async () => {
+              try {
+                await navigator.clipboard.writeText(pageUrl);
+                const t = copyBtn.textContent;
+                copyBtn.textContent = '✓ Copied';
+                setTimeout(() => { copyBtn.textContent = t; }, 1500);
+              } catch { stat.textContent = 'copy failed'; }
+            });
+          })();
+          </script>
+
+          <!-- Furnish this design — coordinated furniture + lighting + decor
+               recommendations scoped to residential / commercial / hospitality /
+               healthcare. POSTs to /api/design/:id/furnish. -->
+          <div class="furnish-block" style="margin-top:22px;padding-top:18px;border-top:1px solid var(--line)">
+            <label style="font:11px var(--sans);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-faint);display:block;margin-bottom:8px">Furnish this design</label>
+            <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:10px">
+              <select id="furnish-context" style="padding:9px 12px;border:1px solid var(--line);background:white;color:var(--ink);font:13px var(--sans);border-radius:6px;flex:1;min-width:180px">
+                <option value="residential">Residential — home</option>
+                <option value="commercial">Commercial — office / retail</option>
+                <option value="hospitality">Hospitality — hotel / restaurant</option>
+                <option value="healthcare">Healthcare — clinic / waiting room</option>
+              </select>
+              <button type="button" id="furnish-btn" style="padding:10px 18px;background:var(--gold,#c9a14b);color:var(--accent,#0d0d0d);border:0;border-radius:6px;font:600 12px var(--sans);letter-spacing:.06em;text-transform:uppercase;cursor:pointer">✦ Generate furnishings</button>
+            </div>
+            <div id="furnish-output" style="display:none;background:#fafaf6;border:1px solid var(--line);border-radius:8px;padding:14px;font:13px var(--sans);color:var(--ink)"></div>
+            <div style="font:10.5px var(--sans);color:var(--ink-faint);margin-top:6px;letter-spacing:.04em">
+              Pulls coordinating furniture, lighting, art, and accessories from this pattern's palette and motif.
+            </div>
+          </div>
+          <script>
+          (function(){
+            const btn = document.getElementById('furnish-btn');
+            const out = document.getElementById('furnish-output');
+            const ctxSel = document.getElementById('furnish-context');
+            if (!btn || !out || !ctxSel) return;
+            btn.addEventListener('click', async () => {
+              out.style.display = 'block';
+              out.innerHTML = '<em style="color:var(--ink-faint)">Generating coordinated furnishings…</em>';
+              btn.disabled = true; btn.style.opacity = '0.6';
+              try {
+                const r = await fetch('/api/design/${design.id}/furnish', {
+                  method: 'POST',
+                  headers: { 'Content-Type': 'application/json' },
+                  body: JSON.stringify({ context: ctxSel.value }),
+                });
+                const j = await r.json();
+                if (!j.ok) throw new Error(j.error || 'gen failed');
+                let html = '';
+                for (const cat of (j.categories || [])) {
+                  html += '<div style="margin-bottom:14px"><div style="font:600 11px var(--sans);text-transform:uppercase;letter-spacing:.06em;color:var(--ink-faint);margin-bottom:6px">' + cat.category + '</div><ul style="margin:0;padding-left:18px">';
+                  for (const item of (cat.items || [])) {
+                    html += '<li style="margin-bottom:4px"><strong>' + item.name + '</strong>' + (item.detail ? ' — <span style="color:var(--ink-soft,#555)">' + item.detail + '</span>' : '') + '</li>';
+                  }
+                  html += '</ul></div>';
+                }
+                if (j.note) html += '<div style="font:11px var(--sans);color:var(--ink-faint);font-style:italic;margin-top:8px">' + j.note + '</div>';
+                out.innerHTML = html || '<em>No suggestions returned.</em>';
+              } catch (e) {
+                out.innerHTML = '<span style="color:#b91c1c">Error: ' + (e.message || String(e)) + '</span>';
+              } finally {
+                btn.disabled = false; btn.style.opacity = '1';
+              }
+            });
+          })();
+          </script>
+
+          <!-- ──────────────────────────────────────────────────────────────
+               Wall-fit crop tool — user enters wall dimensions, sees the
+               pattern tiled to scale + computed roll count + can download the
+               composition as a PNG. Steve's standing rule (2026-05-13): every
+               wallco.ai design must let users crop a section based on their
+               measurements BEFORE sample/order so they preview the actual fit.
+               Pattern is already seamless (24" tile), so this just tiles it
+               across an aspect-correct canvas — no edge-matching math needed.
+          ── ─────────────────────────────────────────────────────────────── -->
+          <div class="wallfit-crop" style="margin-top:22px;padding-top:18px;border-top:1px solid var(--line)">
+            <label style="font:11px var(--sans);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-faint);display:block;margin-bottom:8px">Crop to your wall</label>
+            <div style="display:grid;grid-template-columns:1fr 1fr auto;gap:10px;align-items:end;margin-bottom:12px">
+              <label style="font:11px var(--sans);color:var(--ink-soft,#555);display:block">
+                Wall width
+                <div style="display:flex;align-items:center;gap:4px;margin-top:2px">
+                  <input id="wallfit-w" type="number" min="12" max="600" step="0.5" value="120" style="width:100%;padding:8px 10px;border:1px solid var(--line);border-radius:6px;font:14px var(--sans)">
+                  <select id="wallfit-w-unit" style="padding:7px 4px;border:1px solid var(--line);border-radius:6px;font:11px var(--sans);background:white">
+                    <option value="in">in</option><option value="ft">ft</option>
+                  </select>
+                </div>
+              </label>
+              <label style="font:11px var(--sans);color:var(--ink-soft,#555);display:block">
+                Wall height
+                <div style="display:flex;align-items:center;gap:4px;margin-top:2px">
+                  <input id="wallfit-h" type="number" min="12" max="240" step="0.5" value="96" style="width:100%;padding:8px 10px;border:1px solid var(--line);border-radius:6px;font:14px var(--sans)">
+                  <select id="wallfit-h-unit" style="padding:7px 4px;border:1px solid var(--line);border-radius:6px;font:11px var(--sans);background:white">
+                    <option value="in">in</option><option value="ft">ft</option>
+                  </select>
+                </div>
+              </label>
+              <button type="button" id="wallfit-download" style="padding:9px 14px;background:var(--gold,#c9a14b);color:var(--accent,#0d0d0d);border:0;border-radius:6px;font:600 11px var(--sans);letter-spacing:.06em;text-transform:uppercase;cursor:pointer;white-space:nowrap">↓ PNG</button>
+            </div>
+            <div style="position:relative;background:#f4f1ea;border:1px solid var(--line);border-radius:8px;overflow:hidden">
+              <canvas id="wallfit-canvas" style="display:block;width:100%;height:auto"></canvas>
+              <!-- Scale ribbon: shows wall width in ft beneath the preview -->
+              <div id="wallfit-scale" style="position:absolute;bottom:6px;left:8px;right:8px;display:flex;justify-content:space-between;font:10px ui-monospace,Menlo,monospace;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.6);letter-spacing:.04em;pointer-events:none"></div>
+            </div>
+            <div id="wallfit-readout" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(110px,1fr));gap:8px;margin-top:10px;font:11px var(--sans);color:var(--ink-soft,#555)">
+              <!-- filled by JS: rolls needed, drops per roll, waste % -->
+            </div>
+          </div>
+        </div>
+
+        <script>
+        (function(){
+          // Wall-fit crop tool. Reads roll width from localStorage (synced
+          // with the .w-tab buttons above). Renders the seamless tile across
+          // a wall-shaped canvas at scale. Roll count = ceil(walls / roll).
+          // Yard length assumed 396" (11 yd standard double-roll) for waste
+          // math — adjust ROLL_LENGTH_IN if real roll length differs.
+          var imgUrl = ${JSON.stringify(design.image_url)};
+          var defaultRepeatIn = 24;  // sane default if Pattern scale slider hasn't been touched
+          var ROLL_LENGTH_IN = 396;  // 11 yards (American double-roll), conservative
+          var c = document.getElementById('wallfit-canvas');
+          if (!c) return;
+          var ctx = c.getContext('2d');
+          var wIn = document.getElementById('wallfit-w');
+          var hIn = document.getElementById('wallfit-h');
+          var wU  = document.getElementById('wallfit-w-unit');
+          var hU  = document.getElementById('wallfit-h-unit');
+          var readout = document.getElementById('wallfit-readout');
+          var scaleEl = document.getElementById('wallfit-scale');
+          var dl = document.getElementById('wallfit-download');
+
+          // Restore previous wall dims
+          try {
+            var prev = JSON.parse(localStorage.getItem('wallco.wallfit') || '{}');
+            if (prev.w) { wIn.value = prev.w; if (prev.wu) wU.value = prev.wu; }
+            if (prev.h) { hIn.value = prev.h; if (prev.hu) hU.value = prev.hu; }
+          } catch {}
+
+          var img = new Image();
+          img.crossOrigin = 'anonymous';
+          var imgReady = false;
+          img.onload = function() { imgReady = true; paint(); };
+          img.onerror = function() { /* silently leave canvas blank */ };
+          img.src = imgUrl;
+
+          function toIn(val, unit) { return unit === 'ft' ? val * 12 : val; }
+          function getRollWidth() {
+            try {
+              var m = JSON.parse(localStorage.getItem('wallco.material') || 'null');
+              var w = m && parseInt(m.w, 10);
+              if ([24, 36, 52].indexOf(w) >= 0) return w;
+            } catch {}
+            return 24;
+          }
+          function getRepeatIn() {
+            var v = parseInt(localStorage.getItem('wallco.scale') || '', 10);
+            return (v && v >= 4) ? v : defaultRepeatIn;
+          }
+
+          function paint() {
+            var wallW = toIn(parseFloat(wIn.value) || 120, wU.value);
+            var wallH = toIn(parseFloat(hIn.value) || 96,  hU.value);
+            var rollW = getRollWidth();
+            var repeat = Math.min(getRepeatIn(), rollW);
+            // Persist
+            try { localStorage.setItem('wallco.wallfit', JSON.stringify({ w: wIn.value, wu: wU.value, h: hIn.value, hu: hU.value })); } catch {}
+            // Canvas size: keep px-per-inch constant so wide walls get wide
+            // canvases. Cap total px to ~1100 wide for layout sanity.
+            var pxPerIn = Math.min(8, Math.max(2, 1100 / wallW));
+            var W = Math.round(wallW * pxPerIn);
+            var H = Math.round(wallH * pxPerIn);
+            c.width = W; c.height = H;
+            // Background fill for unloaded / failed image
+            ctx.fillStyle = '#f4f1ea'; ctx.fillRect(0, 0, W, H);
+            if (imgReady) {
+              // Tile size in px = repeat inches × pxPerIn. Pattern image is
+              // already seamless so we can drop-repeat without offsetting.
+              var tilePx = Math.max(8, Math.round(repeat * pxPerIn));
+              for (var y = 0; y < H; y += tilePx) {
+                for (var x = 0; x < W; x += tilePx) {
+                  ctx.drawImage(img, x, y, tilePx, tilePx);
+                }
+              }
+              // Roll-seam guides — vertical lines at every roll-width
+              ctx.save();
+              ctx.strokeStyle = 'rgba(255,255,255,.35)';
+              ctx.lineWidth = 1;
+              for (var rx = rollW * pxPerIn; rx < W; rx += rollW * pxPerIn) {
+                ctx.beginPath(); ctx.moveTo(rx, 0); ctx.lineTo(rx, H); ctx.stroke();
+              }
+              ctx.restore();
+            }
+            // Scale ribbon
+            scaleEl.innerHTML = '<span>0 ft</span><span>' + (wallW / 12).toFixed(1) + ' ft wide × ' + (wallH / 12).toFixed(1) + ' ft tall</span><span>' + (wallW / 12).toFixed(1) + ' ft</span>';
+            // Read-out: rolls + drops + waste
+            var drops = Math.ceil(wallW / rollW);
+            var rollsNeeded = Math.ceil((drops * wallH) / ROLL_LENGTH_IN);
+            var coverage = drops * rollW * wallH;
+            var purchased = rollsNeeded * rollW * ROLL_LENGTH_IN;
+            var waste = purchased ? Math.max(0, ((purchased - coverage) / purchased) * 100) : 0;
+            readout.innerHTML =
+              '<div><b style="color:var(--ink)">' + rollsNeeded + '</b> roll' + (rollsNeeded === 1 ? '' : 's') + ' needed</div>' +
+              '<div><b style="color:var(--ink)">' + drops + '</b> vertical drop' + (drops === 1 ? '' : 's') + '</div>' +
+              '<div><b style="color:var(--ink)">' + rollW + '" </b> roll width</div>' +
+              '<div><b style="color:var(--ink)">' + repeat + '" </b> pattern repeat</div>' +
+              '<div><b style="color:var(--ink)">~' + waste.toFixed(0) + '%</b> waste</div>';
+          }
+
+          [wIn, hIn, wU, hU].forEach(function(el){ el.addEventListener('input', paint); el.addEventListener('change', paint); });
+          window.addEventListener('storage', function(e){ if (e.key === 'wallco.material' || e.key === 'wallco.scale') paint(); });
+          // Re-paint when any .w-tab on the page changes (same-tab; storage event only fires cross-tab)
+          document.querySelectorAll('.w-tab').forEach(function(t){ t.addEventListener('click', function(){ setTimeout(paint, 0); }); });
+          var scaleSl = document.getElementById('scale-slider');
+          if (scaleSl) scaleSl.addEventListener('input', function(){ setTimeout(paint, 0); });
+
+          dl.addEventListener('click', function(){
+            if (!imgReady) return;
+            var url = c.toDataURL('image/png');
+            var a = document.createElement('a');
+            var did = ${JSON.stringify(String(design.id).padStart(4,'0'))};
+            a.href = url;
+            a.download = 'wallco-' + did + '-wall-crop.png';
+            document.body.appendChild(a); a.click(); a.remove();
+          });
+
+          paint();
+        })();
+        </script>
+
+        <script>
+        (function(){
+          // The viewport visually represents one roll-width × ~2/3 roll-width (1 yard tall when roll=36).
+          // Hard rule (Steve): the repeat can never exceed the selected roll width.
+          // Max repeat clamps to roll width; selecting a smaller roll auto-clamps the current value.
+          var slider = document.getElementById('scale-slider');
+          var read   = document.getElementById('scale-readout');
+          var port   = document.getElementById('scale-viewport');
+          var hint   = document.getElementById('scale-width-hint');
+
+          function currentRollWidth() {
+            try {
+              var m = JSON.parse(localStorage.getItem('wallco.material') || 'null');
+              var w = m && parseInt(m.w, 10);
+              if ([24, 36, 52].indexOf(w) >= 0) return w;
+            } catch {}
+            return 24;
+          }
+
+          function applyRollWidth(rollW) {
+            // Slider max can never exceed roll width.
+            slider.max = String(rollW);
+            // Clamp current value down if it exceeds the new max.
+            if (parseInt(slider.value, 10) > rollW) slider.value = String(rollW);
+            if (hint) hint.textContent = rollW + '" wide, sold per yard';
+            paint();
+          }
+
+          // Persist (bounded by current roll width)
+          var saved = parseInt(localStorage.getItem('wallco.scale') || '', 10);
+          if (saved && saved >= 4) slider.value = String(saved);
+
+          function paint(){
+            var rollW = parseInt(slider.max, 10) || 24;
+            var inches = Math.min(parseInt(slider.value, 10), rollW);
+            slider.value = String(inches);
+            read.textContent = inches + '" repeat';
+            // % of viewport that one repeat occupies = inches / roll-width.
+            var pct = (inches / rollW) * 100;
+            port.style.backgroundSize = pct + '% auto';
+            localStorage.setItem('wallco.scale', String(inches));
+          }
+          slider.addEventListener('input', paint);
+
+          // Initial: align with whichever roll width is currently selected.
+          applyRollWidth(currentRollWidth());
+
+          // Listen to every roll-width tab on the page; re-apply on click.
+          document.querySelectorAll('.w-tab').forEach(function(t){
+            t.addEventListener('click', function(){
+              // The existing width-tab handler updates localStorage.wallco.material asynchronously;
+              // run on the next tick so we read the freshly-persisted value.
+              setTimeout(function(){ applyRollWidth(currentRollWidth()); }, 0);
+            });
+          });
+        })();
+        </script>
+      </div>
+    </div>
+    <div class="detail-info-col">
+      <p class="detail-breadcrumb"><a href="/designs" id="back-to-grid">Designs</a> &rsaquo; ${design.category}</p>
+      <script>
+      // Preserve the filtered grid URL the user came from so clicking Designs
+      // returns them to /designs?hue=rose&motif=floral, not bare /designs.
+      // Only honors same-origin /designs referrers; everything else stays default.
+      (function(){
+        var a = document.getElementById('back-to-grid'); if (!a) return;
+        try {
+          var ref = document.referrer || '';
+          if (!ref) return;
+          var u = new URL(ref);
+          if (u.origin !== location.origin) return;
+          if (!u.pathname.startsWith('/designs')) return;
+          if (u.search) a.href = u.pathname + u.search;
+        } catch(_) {}
+      })();
+      </script>
+      <h1 class="detail-title">${design.title}</h1>
+
+      <div class="provenance-badge">
+        ${_isAdmin ? `<span class="ai-badge">AI-Original</span>` : ''}
+        Pattern No.${String(design.id).padStart(4,'0')} &middot; ${(design.category || 'mixed').charAt(0).toUpperCase()+(design.category || 'mixed').slice(1)} &middot; ${design.kind === 'seamless_tile' ? 'Seamless Tile' : (design.kind || 'pattern')}
+      </div>
+
+      <!-- USER VOTE — 5-star widget, anonymous session-cookie tracking -->
+      <div id="user-vote-card" style="margin:14px 0;padding:12px 16px;background:#faf6ec;border:1px solid #e7dcc3;border-radius:8px;font-size:13px">
+        <div style="display:flex;justify-content:space-between;align-items:center;gap:14px;flex-wrap:wrap">
+          <div>
+            <strong style="font-family:'Cormorant Garamond',serif;font-weight:400;font-size:15px;color:#1a1816">Your rating</strong>
+            <span id="vote-avg" style="margin-left:8px;color:#666;font-size:12px"></span>
+          </div>
+          <div id="star-row" data-id="${design.id}" style="font-size:24px;letter-spacing:4px;cursor:pointer;color:#bba">
+            <span data-s="1">☆</span><span data-s="2">☆</span><span data-s="3">☆</span><span data-s="4">☆</span><span data-s="5">☆</span>
+          </div>
+        </div>
+        <div style="margin-top:6px;font-size:11px;color:#888">Each vote teaches the system. We use your taste signals to bias what we generate next.</div>
+      </div>
+      <script>
+      (async function(){
+        var row = document.getElementById('star-row');
+        var avgEl = document.getElementById('vote-avg');
+        var id = row.getAttribute('data-id');
+        function paint(s){
+          Array.from(row.children).forEach(function(el,i){ el.textContent = (i < s) ? '★' : '☆'; });
+          row.style.color = s ? '#d2b15c' : '#bba';
+        }
+        async function refresh(){
+          var v = await (await fetch('/api/design/' + id + '/votes')).json();
+          if (v.my_score) paint(v.my_score);
+          if (v.count) avgEl.textContent = '· average ★' + (v.avg || '?') + ' from ' + v.count + ' vote' + (v.count===1?'':'s');
+        }
+        Array.from(row.children).forEach(function(el){
+          el.addEventListener('mouseenter', function(){ paint(parseInt(el.getAttribute('data-s'),10)); });
+          el.addEventListener('click', async function(){
+            var s = parseInt(el.getAttribute('data-s'),10);
+            paint(s);
+            var r = await fetch('/api/design/' + id + '/user-vote', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({score: s})});
+            var j = await r.json();
+            if (j.user_score_avg) avgEl.textContent = '· average ★' + j.user_score_avg + ' from ' + j.user_vote_count + ' vote' + (j.user_vote_count===1?'':'s');
+          });
+        });
+        row.addEventListener('mouseleave', refresh);
+        refresh();
+      })();
+      </script>
+
+      <!-- AI Suggested Rating (graphic designer + interior designer) -->
+      <div id="ai-rating-card" style="display:none;margin:14px 0;padding:14px 16px;background:#1a1816;color:#e8e2d6;border-radius:8px;border:1px solid #2a2a2a;font-size:13px">
+        <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
+          <strong style="font-family:'Cormorant Garamond',serif;font-weight:400;font-size:16px;color:#d2b15c">AI Suggested Rating</strong>
+          <span id="rating-combined" style="font-size:18px;color:#d2b15c"></span>
+        </div>
+        <div id="rating-summary" style="color:#bba;font-size:12px;line-height:1.55"></div>
+        <div style="margin-top:10px;display:grid;grid-template-columns:1fr 1fr;gap:10px;font-size:11px">
+          <div><div style="text-transform:uppercase;letter-spacing:.08em;color:#888">Graphic designer</div><div id="rating-gd-score" style="font-size:14px;color:#fff"></div><div id="rating-gd-critique" style="color:#aaa;margin-top:4px"></div></div>
+          <div><div style="text-transform:uppercase;letter-spacing:.08em;color:#888">Interior designer</div><div id="rating-id-score" style="font-size:14px;color:#fff"></div><div id="rating-id-critique" style="color:#aaa;margin-top:4px"></div></div>
+        </div>
+      </div>
+      <script>
+      (async function(){
+        try {
+          var r = await fetch('/api/design/${design.id}/rating');
+          if (!r.ok) return;
+          var j = await r.json();
+          if (!j.combined_score) return;
+          document.getElementById('ai-rating-card').style.display = 'block';
+          document.getElementById('rating-combined').textContent = '★ ' + j.combined_score + ' / 5';
+          document.getElementById('rating-summary').textContent = j.rating_summary || '';
+          document.getElementById('rating-gd-score').textContent = (j.gd_score || '?') + ' / 5';
+          document.getElementById('rating-gd-critique').textContent = j.gd_critique || '';
+          document.getElementById('rating-id-score').textContent = (j.id_score || '?') + ' / 5';
+          document.getElementById('rating-id-critique').textContent = j.id_critique || '';
+        } catch(e) {}
+      })();
+      </script>
+
+      <div class="detail-palette">
+        <span class="swatch-main" style="background:${design.dominant_hex}" title="Dominant ${design.dominant_hex}"></span>
+        <span class="hex-label">${design.dominant_hex}</span>
+      </div>
+
+      <!-- Full palette — every extracted color as a paint chip + a "paint" stripe -->
+      ${(() => {
+        const pal = Array.isArray(design.palette) ? design.palette.filter(p => p && p.hex) : [];
+        if (pal.length === 0) return '';
+        const chips = pal.map(p => `
+          <div class="paint-chip" data-hex="${p.hex}" title="${p.pct != null ? p.pct + '% of the design' : p.hex} — click to copy">
+            <div class="paint-chip-color" style="background:${p.hex}"></div>
+            <div class="paint-chip-label">
+              <span class="paint-chip-hex">${p.hex.toUpperCase()}</span>
+              <span class="paint-chip-pct">${p.pct != null ? p.pct + '%' : ''}</span>
+            </div>
+          </div>`).join('');
+        // Paint stripe — proportional widths matching each color's pct in the design.
+        const totalPct = pal.reduce((s, p) => s + (p.pct || 0), 0) || pal.length;
+        const stripe = pal.map(p => {
+          const w = ((p.pct || (100 / pal.length)) / totalPct * 100).toFixed(2);
+          return `<div class="paint-stripe-cell" style="background:${p.hex};flex:0 0 ${w}%" title="${p.hex} · ${p.pct || ''}%"></div>`;
+        }).join('');
+        return `
+      <div class="full-palette">
+        <div class="palette-header">
+          <span class="palette-label">Palette · ${pal.length} colors · click a chip to copy hex</span>
+        </div>
+        <div class="paint-chips">${chips}</div>
+        <div class="paint-stripe" title="Paint stripe — colors at their actual coverage in the design">${stripe}</div>
+        <div class="paint-stripe-caption">Paint stripe — each color at its actual coverage in the design</div>
+      </div>
+      <style>
+        .full-palette { margin:14px 0 4px; }
+        .palette-header { margin-bottom:8px; }
+        .palette-label { font:11px var(--sans, system-ui); text-transform:uppercase; letter-spacing:.08em; color:#888; }
+        .paint-chips { display:grid; grid-template-columns:repeat(auto-fit, minmax(120px, 1fr)); gap:10px; }
+        .paint-chip { border:1px solid #e0d8c4; border-radius:6px; overflow:hidden; background:#fff; cursor:pointer; transition:transform .12s, border-color .12s; }
+        .paint-chip:hover { transform:translateY(-1px); border-color:#c9a14b; }
+        .paint-chip-color { aspect-ratio:1.6/1; border-bottom:1px solid rgba(0,0,0,.08); }
+        .paint-chip-label { padding:7px 10px; display:flex; justify-content:space-between; align-items:center; font:11px ui-monospace, monospace; }
+        .paint-chip-hex { color:#1a1816; font-weight:600; letter-spacing:.04em; }
+        .paint-chip-pct { color:#888; font-size:10.5px; }
+        .paint-stripe { display:flex; height:64px; margin-top:14px; border:1px solid #e0d8c4; border-radius:6px; overflow:hidden; }
+        .paint-stripe-cell { transition:flex-grow .15s; }
+        .paint-stripe-cell:hover { flex-grow:1.6; }
+        .paint-stripe-caption { text-align:center; font:11px var(--sans, system-ui); color:#888; margin-top:6px; }
+      </style>
+      <script>
+        document.querySelectorAll('.paint-chip').forEach(el => {
+          el.addEventListener('click', async () => {
+            const hex = el.dataset.hex;
+            try { await navigator.clipboard.writeText(hex); } catch(_) {}
+            const lab = el.querySelector('.paint-chip-hex');
+            if (!lab) return;
+            const orig = lab.textContent;
+            lab.textContent = '✓ copied';
+            setTimeout(() => { lab.textContent = orig; }, 1200);
+          });
+        });
+      </script>`;
+      })()}
+
+      <!-- Width + material selector -->
+      <div class="material-picker" style="margin:18px 0 20px;padding:16px 18px;border:1px solid var(--line);border-radius:8px;background:var(--card-bg)">
+        <div style="font:11px var(--sans);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-faint);margin-bottom:10px">Width</div>
+        <div class="width-tabs" id="width-tabs" data-native-w="${design.width_in || ''}" style="display:flex;gap:8px;margin-bottom:14px">
+          <button type="button" class="w-tab" data-w="24" style="flex:1;padding:10px 14px;border:1px solid var(--line);background:transparent;color:var(--ink);font:13px var(--sans);font-weight:500;border-radius:6px;cursor:pointer">24" residential</button>
+          <button type="button" class="w-tab" data-w="36" style="flex:1;padding:10px 14px;border:1px solid var(--line);background:transparent;color:var(--ink);font:13px var(--sans);font-weight:500;border-radius:6px;cursor:pointer">36" wide-roll</button>
+          <button type="button" class="w-tab" data-w="52" style="flex:1;padding:10px 14px;border:1px solid var(--line);background:transparent;color:var(--ink);font:13px var(--sans);font-weight:500;border-radius:6px;cursor:pointer">52" commercial</button>
+        </div>
+        <div style="font:11px var(--sans);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-faint);margin-bottom:10px">Material</div>
+        <div class="material-tabs" id="material-tabs" style="display:flex;flex-wrap:wrap;gap:6px"></div>
+        <div id="material-readout" style="margin-top:10px;font:12px var(--sans);color:var(--ink-soft)"></div>
+      </div>
+
+      <div class="detail-spec-panel">
+        <div class="spec-row"><span class="spec-key">Category</span><span class="spec-val">${design.category}</span></div>
+        <div class="spec-row"><span class="spec-key">Width</span><span class="spec-val" id="spec-width">24"</span></div>
+        <div class="spec-row"><span class="spec-key">Material</span><span class="spec-val" id="spec-material">Paper</span></div>
+        <div class="spec-row"><span class="spec-key">Sold by</span><span class="spec-val">Per yard</span></div>
+        <div class="spec-row"><span class="spec-key">Repeat</span><span class="spec-val" id="spec-repeat">24" (adjust on left)</span></div>
+        <div class="spec-row"><span class="spec-key">Kind</span><span class="spec-val">${design.kind === 'seamless_tile' ? 'Seamless tile' : design.kind}</span></div>
+        <div class="spec-row"><span class="spec-key">Designed by</span><span class="spec-val">${design.generator}</span></div>
+        <div class="spec-row"><span class="spec-key">Handle</span><span class="spec-val">${design.handle}</span></div>
+      </div>
+
+      <script>
+      (function(){
+        var WIDTH_MATERIALS = {
+          '24': [
+            {key:'paper',      label:'Paper',         note:'Traditional non-woven, easy install'},
+            {key:'vinyl',      label:'Vinyl',         note:'Scrubbable, kid-proof, kitchens & baths'},
+            {key:'peelstick',  label:'Peel & Stick',  note:'Rental-friendly, removable, no paste'},
+            {key:'grasscloth', label:'Grasscloth',    note:'Natural fiber, textural, handcrafted'}
+          ],
+          '36': [
+            {key:'paper',      label:'Paper',         note:'Wide-roll non-woven, fewer seams across walls'},
+            {key:'vinyl',      label:'Vinyl',         note:'Scrubbable wide-format, large-pattern friendly'},
+            {key:'grasscloth', label:'Grasscloth',    note:'Wide-bolt natural fiber, premium upcharge'}
+          ],
+          '52': [
+            {key:'vinyl',      label:'Type II Vinyl', note:'Commercial scrubbable, Class A fire-rated'},
+            {key:'paper',      label:'Contract Paper',note:'Heavy-duty backing, hospitality / corridor use'}
+          ]
+        };
+        var widthTabs = document.querySelectorAll('.w-tab');
+        var widthHost = document.getElementById('width-tabs');
+        var matWrap   = document.getElementById('material-tabs');
+        var readout   = document.getElementById('material-readout');
+        var specW     = document.getElementById('spec-width');
+        var specM     = document.getElementById('spec-material');
+
+        // Default the slider to the design's native width when available (24/36/52); else 24".
+        var nativeW = (widthHost && widthHost.dataset.nativeW) ? String(Math.round(parseFloat(widthHost.dataset.nativeW))) : '';
+        if (!['24','36','52'].includes(nativeW)) nativeW = '24';
+        var saved = JSON.parse(localStorage.getItem('wallco.material') || 'null') || { w: nativeW, m: 'paper' };
+        // Tag the native button visually so users see "you're looking at what this pattern was generated for"
+        widthTabs.forEach(function(t){
+          if (t.dataset.w === nativeW) {
+            t.title = 'Generated at this width';
+            t.insertAdjacentHTML('beforeend', ' <span style="font-size:9px;letter-spacing:.16em;opacity:.7;margin-left:4px">·NATIVE</span>');
+          }
+        });
+
+        function paintMaterials(w) {
+          var opts = WIDTH_MATERIALS[w];
+          matWrap.innerHTML = opts.map(function(o){
+            var on = (o.key === saved.m);
+            return '<button type="button" class="m-tab' + (on ? ' is-on' : '') + '" data-m="' + o.key + '" data-note="' + o.note + '" style="padding:8px 14px;border:1px solid ' + (on ? 'var(--accent)' : 'var(--line)') + ';background:' + (on ? 'var(--accent)' : 'transparent') + ';color:' + (on ? 'var(--bg)' : 'var(--ink)') + ';font:12px var(--sans);font-weight:500;border-radius:999px;cursor:pointer">' + o.label + '</button>';
+          }).join('');
+          // If saved.m isn't in this width's options, fall back to first
+          if (!opts.some(function(o){return o.key===saved.m;})) saved.m = opts[0].key;
+          // Wire material buttons
+          matWrap.querySelectorAll('.m-tab').forEach(function(btn){
+            btn.addEventListener('click', function(){
+              saved.m = btn.dataset.m;
+              paintMaterials(w);
+              persist();
+            });
+          });
+          var cur = opts.find(function(o){return o.key===saved.m;}) || opts[0];
+          readout.textContent = cur.note;
+          specW.textContent = w + '"';
+          specM.textContent = cur.label;
+        }
+        function persist(){ localStorage.setItem('wallco.material', JSON.stringify(saved)); }
+
+        widthTabs.forEach(function(t){
+          t.addEventListener('click', function(){
+            saved.w = t.dataset.w;
+            widthTabs.forEach(function(x){
+              var on = x===t;
+              x.classList.toggle('is-on', on);
+              x.style.background = on ? 'var(--accent)' : 'transparent';
+              x.style.color      = on ? 'var(--bg)' : 'var(--ink)';
+              x.style.borderColor= on ? 'var(--accent)' : 'var(--line)';
+            });
+            paintMaterials(saved.w);
+            persist();
+          });
+        });
+        // Restore on load
+        widthTabs.forEach(function(t){
+          if (t.dataset.w === saved.w) t.click();
+        });
+        // If neither matched (shouldn't happen), default to 24"
+        if (!document.querySelector('.w-tab.is-on')) widthTabs[0].click();
+      })();
+      </script>
+
+      <!-- Wall-size calculator: enter W×H, get panel count + total yardage + estimated cost -->
+      <details class="wall-calc" style="margin:14px 0 0;padding:0;border:1px solid var(--line,#ddd);border-radius:6px;background:var(--card-bg,#faf8f3)">
+        <summary style="cursor:pointer;padding:10px 14px;font:12px var(--sans,system-ui);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-soft,#555);list-style:none">
+          <span aria-hidden="true">📐</span> Wall-size calculator <span style="color:var(--ink-faint,#999);font-size:11px;text-transform:none;letter-spacing:0">— estimate panels, yardage, cost</span>
+          <span id="wc-summary-chip" style="display:none;margin-left:8px;padding:2px 8px;background:#d2b15c33;border:1px solid #d2b15c;border-radius:10px;color:#5a4520;font-size:11px;text-transform:none;letter-spacing:0;font-weight:600" aria-hidden="true"></span>
+        </summary>
+        <div style="padding:6px 14px 14px;display:grid;grid-template-columns:1fr 1fr auto;gap:10px;align-items:end">
+          <label style="font:11px var(--sans);color:var(--ink-soft);text-transform:uppercase;letter-spacing:.06em">Wall width (ft)<br><input id="wc-w" type="number" min="1" max="200" step="0.5" value="10" style="margin-top:4px;width:100%;padding:6px 8px;font:14px var(--sans);border:1px solid var(--line,#ddd);border-radius:4px"></label>
+          <label style="font:11px var(--sans);color:var(--ink-soft);text-transform:uppercase;letter-spacing:.06em">Wall height (ft)<br><input id="wc-h" type="number" min="1" max="40" step="0.5" value="9" style="margin-top:4px;width:100%;padding:6px 8px;font:14px var(--sans);border:1px solid var(--line,#ddd);border-radius:4px"></label>
+          <button type="button" id="wc-calc" style="padding:8px 16px;background:var(--ink,#111);color:var(--bg,#fff);border:0;border-radius:4px;cursor:pointer;font:13px var(--sans);height:fit-content">Calculate</button>
+        </div>
+        <div id="wc-result" style="padding:0 14px 14px;font:13px/1.5 var(--sans);color:var(--ink,#111);display:none"></div>
+        <div id="wc-pinbar" style="padding:0 14px 12px;display:flex;align-items:center;gap:10px;font:11px var(--sans);color:var(--ink-faint,#999)">
+          <a id="wc-pin" href="javascript:void(0)" style="color:var(--ink-soft,#555);text-decoration:underline;text-underline-offset:2px">Save defaults</a>
+          <span id="wc-pin-status" style="display:none;align-items:center;gap:6px;color:#2d6a4f;font-weight:600"><span aria-hidden="true">✓</span><span id="wc-pin-label"></span><a id="wc-pin-clear" href="javascript:void(0)" style="margin-left:6px;color:var(--ink-faint,#999);font-weight:400;text-decoration:underline">clear</a></span>
+        </div>
+      </details>
+      <script>
+      (function(){
+        var btn = document.getElementById('wc-calc');
+        if (!btn) return;
+        var wEl = document.getElementById('wc-w');
+        var hEl = document.getElementById('wc-h');
+        var wallDetails = btn.closest('details');
+        var pinLink = document.getElementById('wc-pin');
+        var pinStatus = document.getElementById('wc-pin-status');
+        var pinLabel = document.getElementById('wc-pin-label');
+        var pinClear = document.getElementById('wc-pin-clear');
+        // Hydrate priority: pinned defaults beat the most-recent working values.
+        // Guards: w/h must be positive finite numbers; pin expires after 90 days
+        // so a stale 2024 pin doesn't haunt the 2026 catalog. Unit-tagged so a
+        // future inches/cm variant can drop mismatched pins on load.
+        var pinned = null, recent = null;
+        try { pinned = JSON.parse(localStorage.getItem('wallco-wall-calc-pin') || 'null'); } catch(e){}
+        try { recent = JSON.parse(localStorage.getItem('wallco-wall-calc') || 'null'); } catch(e){}
+        var PIN_TTL_MS = 90*86400*1000;
+        function pinValid(p){
+          return !!(p && isFinite(p.w) && isFinite(p.h) && p.w > 0 && p.h > 0
+              && (!p.unit || p.unit === 'ft')
+              && (!p.t || (Date.now() - p.t) < PIN_TTL_MS));
+        }
+        if (pinned && !pinValid(pinned)) {
+          try { localStorage.removeItem('wallco-wall-calc-pin'); } catch(e){}
+          pinned = null;
+        }
+        var prev = pinValid(pinned) ? pinned : recent;
+        if (prev && isFinite(prev.w) && prev.w > 0) wEl.value = prev.w;
+        if (prev && isFinite(prev.h) && prev.h > 0) hEl.value = prev.h;
+        if (prev && wallDetails && !wallDetails.open) wallDetails.open = true;
+        function renderPinUI(){
+          var chip = document.getElementById('wc-summary-chip');
+          if (pinValid(pinned)) {
+            pinLink.style.display = 'none';
+            pinStatus.style.display = 'inline-flex';
+            pinLabel.textContent = 'Pinned ' + pinned.w + '×' + pinned.h + ' ft';
+            if (chip) { chip.textContent = '📌 ' + pinned.w + '×' + pinned.h + ' ft'; chip.style.display = 'inline-block'; }
+          } else {
+            pinLink.style.display = 'inline';
+            pinStatus.style.display = 'none';
+            if (chip) { chip.style.display = 'none'; chip.textContent = ''; }
+          }
+        }
+        renderPinUI();
+        function persist(){
+          try { localStorage.setItem('wallco-wall-calc', JSON.stringify({ w: parseFloat(wEl.value)||0, h: parseFloat(hEl.value)||0, t: Date.now() })); } catch(e){}
+        }
+        pinLink.addEventListener('click', function(){
+          var w = parseFloat(wEl.value), h = parseFloat(hEl.value);
+          if (!isFinite(w) || !isFinite(h) || w<=0 || h<=0) return;
+          pinned = { w: w, h: h, unit: 'ft', pinned: true, t: Date.now() };
+          try { localStorage.setItem('wallco-wall-calc-pin', JSON.stringify(pinned)); } catch(e){}
+          renderPinUI();
+        });
+        pinClear.addEventListener('click', function(){
+          pinned = null;
+          try { localStorage.removeItem('wallco-wall-calc-pin'); } catch(e){}
+          renderPinUI();
+        });
+        function compute() {
+          var w = parseFloat(wEl.value);
+          var h = parseFloat(hEl.value);
+          var box = document.getElementById('wc-result');
+          if (!isFinite(w) || !isFinite(h) || w<=0 || h<=0) { box.style.display='block'; box.innerHTML = '<span style="color:#b03a2e">Enter both width and height in feet.</span>'; return; }
+          persist();
+          // Read user's chosen panel width (24/27/36) from the existing material picker if present.
+          var savedWidth = 24;
+          try { var s = JSON.parse(localStorage.getItem('wallco.material') || 'null'); if (s && parseInt(s.w,10)) savedWidth = parseInt(s.w,10); } catch(e){}
+          var panelWidthInches = savedWidth; // panel width in inches
+          var panelLengthFt = 27; // standard roll length in ft (a typical 27 sq-ft single roll)
+          var wallSqFt = w * h;
+          var panelCoverageFt2 = (panelWidthInches/12) * panelLengthFt; // each panel covers this many sq-ft
+          // Match-waste factor: AI-original tiles are 24×24 in seamless so most patterns repeat cleanly. Use a 15% waste buffer.
+          var withWaste = wallSqFt * 1.15;
+          var panels = Math.ceil(withWaste / panelCoverageFt2);
+          var totalYards = +((panels * panelLengthFt) / 3).toFixed(1);
+          // Rough pricing tier: $7.50/sq-ft installed material list (typical mid-range custom wall, AI-original)
+          var lowEnd = Math.round(panels * panelCoverageFt2 * 6.5);
+          var highEnd = Math.round(panels * panelCoverageFt2 * 11.5);
+          box.style.display='block';
+          box.innerHTML =
+            '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px;margin-top:6px">' +
+              '<div><strong style="display:block;font:22px var(--serif,Georgia,serif);color:var(--ink,#111)">' + panels + '</strong><span style="font:11px var(--sans);color:var(--ink-faint,#999);text-transform:uppercase;letter-spacing:.06em">panels (' + panelWidthInches + '" wide)</span></div>' +
+              '<div><strong style="display:block;font:22px var(--serif,Georgia,serif);color:var(--ink,#111)">' + totalYards + ' yd</strong><span style="font:11px var(--sans);color:var(--ink-faint,#999);text-transform:uppercase;letter-spacing:.06em">total length</span></div>' +
+              '<div><strong style="display:block;font:22px var(--serif,Georgia,serif);color:var(--ink,#111)">' + wallSqFt.toFixed(0) + ' ft²</strong><span style="font:11px var(--sans);color:var(--ink-faint,#999);text-transform:uppercase;letter-spacing:.06em">wall area</span></div>' +
+              '<div><strong style="display:block;font:22px var(--serif,Georgia,serif);color:var(--ink,#111)">~$' + lowEnd.toLocaleString() + '–$' + highEnd.toLocaleString() + '</strong><span style="font:11px var(--sans);color:var(--ink-faint,#999);text-transform:uppercase;letter-spacing:.06em">material estimate</span></div>' +
+            '</div>' +
+            '<p style="margin:10px 0 0;font:11px/1.4 var(--sans);color:var(--ink-faint,#999)">Estimate uses ' + panelWidthInches + '" panel width, 27 ft panel length, 15% match-waste buffer. Final quote depends on substrate + installation. Sample first.</p>';
+        }
+        btn.addEventListener('click', compute);
+        wEl.addEventListener('keyup', function(e){ if (e.key==='Enter') compute(); });
+        hEl.addEventListener('keyup', function(e){ if (e.key==='Enter') compute(); });
+        wEl.addEventListener('change', persist);
+        hEl.addEventListener('change', persist);
+        // If we restored values, auto-compute on PDP load so the result is already visible.
+        if (prev && isFinite(prev.w) && isFinite(prev.h) && prev.w > 0 && prev.h > 0) {
+          compute();
+        }
+      })();
+      </script>
+
+      <!-- Sample CTA (DW standing rule — non-negotiable) -->
+      <div class="cta-group">
+        <a id="cta-sample-link" href="/samples?design=${design.id}" class="btn-primary cta-sample">Request Free Sample</a>
+        <a href="/designs" class="btn-outline">Back to Designs</a>
+        <button id="cta-share" type="button" class="btn-outline cta-share"
+                data-share-title="${design.title} — wallco.ai"
+                data-share-text="Found this in the wallco.ai catalog — ${design.title}"
+                data-share-url="https://wallco.ai/design/${design.id}"
+                aria-label="Share this design">
+          <span class="cta-share-icon" aria-hidden="true">↗</span>
+          <span class="cta-share-label">Share</span>
+        </button>
+      </div>
+
+      <!-- Trade / professional functions row -->
+      <div class="pro-tools" role="group" aria-label="Trade tools" style="display:flex;flex-wrap:wrap;gap:8px;margin:10px 0 4px;padding-top:14px;border-top:1px dashed var(--line,#ddd)">
+        <a href="/design/${design.id}/tearsheet" target="_blank" rel="noopener noreferrer" class="btn-outline" style="font:12px var(--sans);padding:7px 14px;text-decoration:none;display:inline-flex;align-items:center;gap:6px" title="Open print-ready tear sheet (1-page PDF)">
+          <span aria-hidden="true">⎙</span> Tear Sheet (PDF)
+        </a>
+        <a id="cta-email-client" href="mailto:?subject=${encodeURIComponent('wallco.ai design — ' + (design.title || 'Design #' + design.id))}&body=${encodeURIComponent('Take a look at this wallpaper pattern I found:\n\n' + (design.title || 'Design #' + design.id) + '\nhttps://wallco.ai/design/' + design.id + '\n\nTear sheet (print/PDF):\nhttps://wallco.ai/design/' + design.id + '/tearsheet\n\nFree trade samples available from Designer Wallcoverings — info@designerwallcoverings.com\n')}" class="btn-outline" style="font:12px var(--sans);padding:7px 14px;text-decoration:none;display:inline-flex;align-items:center;gap:6px" title="Email this design to a client (opens your mail app)">
+          <span aria-hidden="true">✉</span> Email to Client
+        </a>
+        <button id="cta-add-compare" type="button" data-design-id="${design.id}" class="btn-outline" style="font:12px var(--sans);padding:7px 14px;cursor:pointer;display:inline-flex;align-items:center;gap:6px" title="Add to side-by-side compare (up to 4)">
+          <span aria-hidden="true">+</span> <span class="cta-add-compare-label">Compare</span>
+        </button>
+        <a id="cta-view-compare" href="/compare" class="btn-outline" style="font:12px var(--sans);padding:7px 14px;text-decoration:none;display:none;align-items:center;gap:6px;background:var(--gold,#c9a14b);color:var(--accent,#0d0d0d);border-color:var(--gold,#c9a14b)" title="View your compare list">
+          View Compare (<span id="cta-compare-count">0</span>)
+        </a>
+        <button id="cta-trade-save" type="button" data-design-id="${design.id}" class="btn-outline" style="font:12px var(--sans);padding:7px 14px;cursor:pointer;display:inline-flex;align-items:center;gap:6px" title="Save to your trade library (sign-in required)">
+          <span class="cta-trade-save-icon" aria-hidden="true">♡</span> <span class="cta-trade-save-label">Save</span>
+        </button>
+        <a id="cta-trade-view-saved" href="/me/saved" class="btn-outline" style="font:12px var(--sans);padding:7px 14px;text-decoration:none;display:none;align-items:center;gap:6px" title="View your saved designs">
+          View saved (<span id="cta-trade-saved-count">0</span>)
+        </a>
+        <button id="cta-add-basket" type="button" data-design-id="${design.id}" class="btn-outline" style="font:12px var(--sans);padding:7px 14px;cursor:pointer;display:inline-flex;align-items:center;gap:6px" title="Add to your sample basket (up to 12)">
+          <span aria-hidden="true">+</span> <span class="cta-add-basket-label">Sample</span>
+        </button>
+        <a id="cta-view-basket" href="/basket" class="btn-outline" style="font:12px var(--sans);padding:7px 14px;text-decoration:none;display:none;align-items:center;gap:6px;background:var(--gold,#c9a14b);color:var(--accent,#0d0d0d);border-color:var(--gold,#c9a14b)" title="View your sample basket">
+          View Basket (<span id="cta-basket-count">0</span>)
+        </a>
+      </div>
+      <script>
+      (function(){
+        var addBtn = document.getElementById('cta-add-compare');
+        var viewBtn = document.getElementById('cta-view-compare');
+        var countEl = document.getElementById('cta-compare-count');
+        if (!addBtn || !viewBtn) return;
+        var thisId = parseInt(addBtn.dataset.designId, 10);
+        function getList(){ try { return JSON.parse(localStorage.getItem('wallco-compare') || '[]').filter(function(x){ return typeof x === 'number' && x>0; }); } catch(e){ return []; } }
+        function setList(arr){ try { localStorage.setItem('wallco-compare', JSON.stringify(arr.slice(0,4))); } catch(e){} }
+        function refresh(){
+          var list = getList();
+          if (countEl) countEl.textContent = list.length;
+          if (list.length > 0) {
+            viewBtn.style.display = 'inline-flex';
+            viewBtn.setAttribute('href', '/compare?ids=' + list.join(','));
+          } else {
+            viewBtn.style.display = 'none';
+          }
+          var lbl = addBtn.querySelector('.cta-add-compare-label');
+          if (list.indexOf(thisId) >= 0) {
+            if (lbl) lbl.textContent = 'In compare';
+            addBtn.dataset.in = '1';
+            addBtn.style.background = 'var(--card-bg, #faf8f3)';
+          } else {
+            if (lbl) lbl.textContent = 'Compare';
+            addBtn.dataset.in = '0';
+            addBtn.style.background = '';
+          }
+        }
+        addBtn.addEventListener('click', function(){
+          var list = getList();
+          var idx = list.indexOf(thisId);
+          if (idx >= 0) { list.splice(idx, 1); }
+          else if (list.length < 4) { list.push(thisId); }
+          else { alert('Compare holds 4 designs max. Remove one to add another.'); return; }
+          setList(list);
+          refresh();
+        });
+        refresh();
+      })();
+      </script>
+      <script>
+      // Basket toggle — mirrors the compare toggle pattern exactly (different key, cap=12)
+      (function(){
+        var addBtn = document.getElementById('cta-add-basket');
+        var viewBtn = document.getElementById('cta-view-basket');
+        var countEl = document.getElementById('cta-basket-count');
+        if (!addBtn || !viewBtn) return;
+        var thisId = parseInt(addBtn.dataset.designId, 10);
+        function getList(){ try { return JSON.parse(localStorage.getItem('wallco-basket') || '[]').filter(function(x){ return typeof x === 'number' && x>0; }); } catch(e){ return []; } }
+        function setList(arr){ try { localStorage.setItem('wallco-basket', JSON.stringify(arr.slice(0,12))); } catch(e){} }
+        function refresh(){
+          var list = getList();
+          if (countEl) countEl.textContent = list.length;
+          if (list.length > 0) {
+            viewBtn.style.display = 'inline-flex';
+            viewBtn.setAttribute('href', '/basket');
+          } else {
+            viewBtn.style.display = 'none';
+          }
+          var lbl = addBtn.querySelector('.cta-add-basket-label');
+          if (list.indexOf(thisId) >= 0) {
+            if (lbl) lbl.textContent = 'In basket';
+            addBtn.dataset.in = '1';
+            addBtn.style.background = 'var(--card-bg, #faf8f3)';
+          } else {
+            if (lbl) lbl.textContent = 'Sample';
+            addBtn.dataset.in = '0';
+            addBtn.style.background = '';
+          }
+        }
+        addBtn.addEventListener('click', function(){
+          var list = getList();
+          var idx = list.indexOf(thisId);
+          if (idx >= 0) { list.splice(idx, 1); }
+          else if (list.length < 12) { list.push(thisId); }
+          else { alert('Sample basket holds 12 designs max. Remove one to add another.'); return; }
+          setList(list);
+          refresh();
+          // Broadcast — nav chip + any other listeners stay in sync.
+          try { window.dispatchEvent(new CustomEvent('wallco-basket-changed', { detail: { count: list.length, design_id: thisId, in_basket: list.indexOf(thisId) >= 0 } })); } catch(e){}
+        });
+        refresh();
+      })();
+      </script>
+      <script>
+      // Trade save toggle — server-side (POST /api/saved). Falls back to /trade/login
+      // when not authenticated. Hydrates initial state from /api/trade/me payload.
+      (function(){
+        var saveBtn = document.getElementById('cta-trade-save');
+        var viewBtn = document.getElementById('cta-trade-view-saved');
+        var countEl = document.getElementById('cta-trade-saved-count');
+        if (!saveBtn) return;
+        var thisId = parseInt(saveBtn.dataset.designId, 10);
+        var iconEl  = saveBtn.querySelector('.cta-trade-save-icon');
+        var labelEl = saveBtn.querySelector('.cta-trade-save-label');
+
+        function setSavedState(saved) {
+          if (saved) {
+            if (labelEl) labelEl.textContent = 'Saved';
+            if (iconEl)  iconEl.textContent = '♥';
+            saveBtn.style.background = 'var(--gold, #c9a14b)';
+            saveBtn.style.color = 'var(--accent, #0d0d0d)';
+            saveBtn.style.borderColor = 'var(--gold, #c9a14b)';
+          } else {
+            if (labelEl) labelEl.textContent = 'Save';
+            if (iconEl)  iconEl.textContent = '♡';
+            saveBtn.style.background = '';
+            saveBtn.style.color = '';
+            saveBtn.style.borderColor = '';
+          }
+        }
+        function updateCount(n) {
+          if (countEl) countEl.textContent = (n == null ? 0 : n);
+          if (viewBtn) viewBtn.style.display = (n && n > 0) ? 'inline-flex' : 'none';
+        }
+        function hydrate(me) {
+          if (!me || !me.authenticated) { updateCount(0); return; }
+          updateCount(me.saved_count || 0);
+          // Probe save state for this design — cheap, single design_id-only payload.
+          // (We piggyback on the toggle endpoint with HEAD-like idempotent GET later
+          // if needed; for now refresh on click.)
+          fetch('/api/saved/state?design_id=' + thisId, { credentials:'same-origin' })
+            .then(function(r){ return r.ok ? r.json() : { saved:false }; })
+            .then(function(j){ setSavedState(!!j.saved); })
+            .catch(function(){});
+        }
+        // If header has already loaded /api/trade/me, use cached payload; else listen.
+        if (window.__tradeMe) hydrate(window.__tradeMe);
+        else window.addEventListener('trade-me-loaded', function(ev){ hydrate(ev.detail); });
+
+        saveBtn.addEventListener('click', function(){
+          var me = window.__tradeMe;
+          if (!me || !me.authenticated) {
+            var ret = encodeURIComponent('/design/' + thisId);
+            if (confirm('Sign in to save designs?')) location.href = '/trade/login?return=' + ret;
+            return;
+          }
+          saveBtn.disabled = true;
+          fetch('/api/saved', {
+            method:'POST', headers:{'Content-Type':'application/json'},
+            credentials:'same-origin',
+            body: JSON.stringify({ design_id: thisId })
+          })
+            .then(function(r){ return r.json(); })
+            .then(function(j){
+              if (j.ok) {
+                setSavedState(j.saved);
+                updateCount(j.saved_count);
+                if (window.__tradeMe) window.__tradeMe.saved_count = j.saved_count;
+                // Broadcast — nav listener (and any other surfaces) sync without re-fetching.
+                try { window.dispatchEvent(new CustomEvent('wallco-saved-changed', { detail: { saved_count: j.saved_count, design_id: thisId, saved: j.saved } })); } catch(e){}
+              } else {
+                alert(j.error || 'Save failed.');
+              }
+            })
+            .catch(function(err){ alert('Network error: ' + err.message); })
+            .finally(function(){ saveBtn.disabled = false; });
+        });
+      })();
+      </script>
+      <script>
+      (function(){
+        var btn = document.getElementById('cta-share');
+        if (!btn) return;
+        var labelEl = btn.querySelector('.cta-share-label');
+        var origLabel = labelEl.textContent;
+        var resetTimer = null;
+        function flash(msg){
+          if (resetTimer) clearTimeout(resetTimer);
+          labelEl.textContent = msg;
+          resetTimer = setTimeout(function(){ labelEl.textContent = origLabel; }, 1800);
+        }
+        btn.addEventListener('click', async function(){
+          var url = btn.dataset.shareUrl;
+          var title = btn.dataset.shareTitle;
+          var text = btn.dataset.shareText;
+          // Prefer native Web Share API when available (mobile + supporting desktop browsers).
+          if (navigator.share) {
+            try { await navigator.share({ title: title, text: text, url: url }); return; }
+            catch (e) { /* user cancelled or unsupported — fall through to clipboard */ }
+          }
+          // Clipboard fallback.
+          try {
+            await navigator.clipboard.writeText(url);
+            flash('Link copied');
+          } catch (e) {
+            // Last resort: prompt() so the user can copy manually.
+            try { window.prompt('Copy this link:', url); } catch (_) {}
+          }
+        });
+      })();
+      </script>
+      <script>
+      (function(){
+        function syncCtaWidth(){
+          var saved = JSON.parse(localStorage.getItem('wallco.material') || 'null');
+          if (!saved || !saved.w) return;
+          var link = document.getElementById('cta-sample-link');
+          if (!link) return;
+          var base = link.getAttribute('href').split('?')[0];
+          var q = new URLSearchParams(link.getAttribute('href').split('?')[1] || '');
+          q.set('width', saved.w);
+          link.setAttribute('href', base + '?' + q.toString());
+        }
+        syncCtaWidth();
+        document.querySelectorAll('.w-tab').forEach(function(t){
+          t.addEventListener('click', function(){ setTimeout(syncCtaWidth, 0); });
+        });
+      })();
+      </script>
+
+      <!-- Pair-with sibling palette neighbors -->
+      <div class="pair-with-block" style="margin-top:32px">
+        <h3 style="font-family:var(--serif);font-weight:300;font-size:20px;margin:0 0 4px">Pair this with</h3>
+        <p style="font-size:12px;color:var(--ink-faint);margin:0 0 14px;letter-spacing:.04em">Sibling designs by palette · nearest neighbors in CIE Lab space</p>
+        <div id="pair-with-grid" style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px;min-height:120px">
+          <div style="grid-column:1/-1;color:var(--ink-faint);font-size:12px">Loading pairings…</div>
+        </div>
+      </div>
+      <script>
+      (function(){
+        var id = ${design.id};
+        fetch('/api/design/' + id + '/pairings').then(r=>r.json()).then(function(d){
+          var g = document.getElementById('pair-with-grid');
+          if (!d.designs || !d.designs.length) { g.innerHTML = '<div style="grid-column:1/-1;color:var(--ink-faint);font-size:12px">No close pairings yet — catalog is still seeding.</div>'; return; }
+          g.innerHTML = d.designs.map(function(p){
+            return '<a href="/design/' + p.id + '" style="text-decoration:none;color:inherit;display:block">'
+              + '<div style="aspect-ratio:1;background:#0a0907 no-repeat center/cover;background-image:url(\'' + p.image_url + '\');border-radius:6px;border:1px solid var(--line);transition:transform .15s;"></div>'
+              + '<div style="margin-top:6px;display:flex;align-items:center;gap:6px"><span style="width:9px;height:9px;border-radius:50%;background:' + p.dominant_hex + ';border:1px solid rgba(0,0,0,.12)"></span><span style="font-size:11px;color:var(--ink-soft)">Lab Δ ' + p.distance.toFixed(1) + '</span></div>'
+              + '</a>';
+          }).join('');
+        }).catch(function(e){
+          document.getElementById('pair-with-grid').innerHTML = '<div style="grid-column:1/-1;color:var(--ink-faint);font-size:12px">Pairings unavailable.</div>';
+        });
+      })();
+      </script>
+
+      ${_isAdmin ? `
+      <!-- Admin: isolate colors (palette + SW + coordinating) -->
+      <div id="iso-block" style="margin-top:28px;padding:18px;border:1px solid var(--line);border-radius:10px;background:rgba(0,0,0,.02)">
+        <div style="display:flex;justify-content:space-between;align-items:baseline;gap:10px;margin-bottom:10px;flex-wrap:wrap">
+          <h3 style="font-family:var(--serif);font-weight:300;font-size:18px;margin:0">Color isolate</h3>
+          <span style="font-size:11px;color:var(--ink-faint);letter-spacing:.04em">Admin-only · palette + Sherwin-Williams + coordinating</span>
+        </div>
+        <div id="iso-palette-detail" style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px"><span style="color:var(--ink-faint);font-size:12px">loading palette…</span></div>
+        <div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
+          <button id="iso-isolate-btn" type="button" style="padding:9px 16px;background:#1a1816;color:#d2b15c;border:1px solid #d2b15c;border-radius:3px;cursor:pointer;font:13px/1 var(--sans);font-weight:600;letter-spacing:.06em;text-transform:uppercase">Isolate this design</button>
+          <div id="iso-mode-detail" style="display:none;gap:0;border:1px solid var(--line);border-radius:3px;overflow:hidden">
+            <button type="button" data-mode="wallpaper" style="padding:8px 14px;background:#1a1816;color:#fff;border:none;cursor:pointer;font:11px/1 var(--sans);letter-spacing:.06em;text-transform:uppercase">Wallpaper</button>
+            <button type="button" data-mode="paint" style="padding:8px 14px;background:transparent;color:var(--ink-soft);border:none;cursor:pointer;font:11px/1 var(--sans);letter-spacing:.06em;text-transform:uppercase">Paint</button>
+          </div>
+        </div>
+        <div id="iso-section-wp-detail" style="display:none;margin-top:14px">
+          <div style="font-size:11px;color:var(--ink-faint);letter-spacing:.08em;text-transform:uppercase;margin-bottom:8px">Coordinating wallpapers</div>
+          <div id="iso-grid-wp-detail" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:8px"></div>
+        </div>
+        <div id="iso-section-pt-detail" style="display:none;margin-top:14px">
+          <div style="font-size:11px;color:var(--ink-faint);letter-spacing:.08em;text-transform:uppercase;margin-bottom:8px">Coordinating paint colors</div>
+          <div id="iso-grid-pt-detail" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px"></div>
+        </div>
+      </div>
+      <script>
+      (function(){
+        var ID = ${design.id};
+        var DATA = null;
+        function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g, function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];}); }
+        var palEl = document.getElementById('iso-palette-detail');
+        var btn   = document.getElementById('iso-isolate-btn');
+        var mode  = document.getElementById('iso-mode-detail');
+        var wpSec = document.getElementById('iso-section-wp-detail');
+        var ptSec = document.getElementById('iso-section-pt-detail');
+        var wpG   = document.getElementById('iso-grid-wp-detail');
+        var ptG   = document.getElementById('iso-grid-pt-detail');
+        function renderPal(palette){
+          if (!palette || !palette.length) { palEl.innerHTML = '<span style="color:var(--ink-faint);font-size:12px">no palette</span>'; return; }
+          palEl.innerHTML = palette.map(function(p){
+            var sw = p.sw;
+            return '<div style="display:flex;flex-direction:column;align-items:center;gap:4px;min-width:80px;padding:6px;background:#fff;border:1px solid var(--line);border-radius:4px">' +
+                   '<div style="width:48px;height:48px;border-radius:50%;background:'+esc(p.hex)+';border:1px solid rgba(0,0,0,.08)"></div>' +
+                   '<div style="font-family:ui-monospace,monospace;font-size:10px;color:var(--ink)">'+esc(p.hex)+'</div>' +
+                   (sw ? '<div style="font-size:10px;color:#a67b1f">'+esc(sw.code)+'</div>'+
+                         '<div style="font-size:10px;color:var(--ink-faint);text-align:center;line-height:1.2;max-width:90px">'+esc(sw.name)+'</div>'
+                       : '<div style="font-size:10px;color:var(--ink-faint);font-style:italic">—</div>') +
+                   '</div>';
+          }).join('');
+        }
+        function renderWP(designs){
+          if (!designs || !designs.length) { wpG.innerHTML = '<div style="grid-column:1/-1;color:var(--ink-faint);font-size:12px">no coordinating wallpapers yet</div>'; return; }
+          wpG.innerHTML = designs.map(function(d){
+            var url = (d.image_url||'').replace(/'/g,'');
+            return '<a href="/design/'+d.id+'" style="display:block;aspect-ratio:1/1;background:#0d0c0a center/cover url('+url+');border-radius:3px;border:1px solid var(--line);position:relative">' +
+                   '<span style="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">ΔE '+(d.deltaE!=null?d.deltaE.toFixed(1):'?')+'</span></a>';
+          }).join('');
+        }
+        function renderPT(coord){
+          if (!coord || !Object.keys(coord).length) { ptG.innerHTML = '<div style="grid-column:1/-1;color:var(--ink-faint);font-size:12px">no coordinating colors</div>'; return; }
+          var labelMap = {complement:'Complement',analogousLeft:'Analogous −30°',analogousRight:'Analogous +30°',triadicA:'Triadic +120°',triadicB:'Triadic +240°',splitComplementA:'Split-comp +150°',splitComplementB:'Split-comp +210°'};
+          ptG.innerHTML = Object.keys(coord).map(function(k){
+            var v = coord[k], sw = v.sw;
+            return '<div style="display:flex;flex-direction:column;gap:5px;padding:8px;background:#fff;border:1px solid var(--line);border-radius:4px">' +
+                   '<div style="width:100%;aspect-ratio:1/1;background:'+esc(v.hex)+';border-radius:3px;border:1px solid rgba(0,0,0,.08)"></div>' +
+                   '<div style="font-size:9px;color:var(--ink-faint);text-transform:uppercase;letter-spacing:.08em">'+esc(labelMap[k]||k)+'</div>' +
+                   '<div style="font-family:ui-monospace,monospace;font-size:11px;color:var(--ink)">'+esc(v.hex)+'</div>' +
+                   (sw ? '<div style="font-size:10px;color:#a67b1f">'+esc(sw.code)+'</div>'+
+                         '<div style="font-size:10px;color:var(--ink-soft);line-height:1.2">'+esc(sw.name)+'</div>'
+                       : '<div style="font-size:10px;color:var(--ink-faint);font-style:italic">SW DB not loaded</div>') +
+                   '<button type="button" data-copy="'+esc(v.hex)+'" style="margin-top:4px;padding:4px 6px;background:#fff;border:1px solid var(--line);color:var(--ink-soft);font:10px/1 var(--sans);cursor:pointer;border-radius:2px">copy hex</button>' +
+                   '</div>';
+          }).join('');
+          ptG.querySelectorAll('button[data-copy]').forEach(function(b){
+            b.addEventListener('click', function(){ navigator.clipboard.writeText(b.dataset.copy).then(function(){ b.textContent='copied!'; setTimeout(function(){b.textContent='copy hex';},1200); }).catch(function(){ b.textContent='copy failed';}); });
+          });
+        }
+        function load(){
+          fetch('/api/isolate/' + ID).then(function(r){ return r.ok ? r.json() : Promise.reject(r.status); }).then(function(d){
+            DATA = d;
+            renderPal(d.palette || []);
+          }).catch(function(err){
+            palEl.innerHTML = '<span style="color:var(--ink-faint);font-size:12px">failed: '+esc(err)+'</span>';
+          });
+        }
+        btn.addEventListener('click', function(){
+          if (!DATA) return;
+          mode.style.display = 'flex';
+          wpSec.style.display = 'block';
+          ptSec.style.display = 'none';
+          btn.disabled = true; btn.style.opacity = '.55'; btn.style.cursor = 'default';
+          btn.textContent = 'Isolated · pick a mode';
+          renderWP(DATA.coordinating_designs || []);
+          renderPT(DATA.coordinating_hexes || {});
+        });
+        mode.addEventListener('click', function(e){
+          var b = e.target.closest('button[data-mode]'); if (!b) return;
+          var m = b.dataset.mode;
+          Array.from(mode.querySelectorAll('button')).forEach(function(x){
+            var on = x === b;
+            x.style.background = on ? '#1a1816' : 'transparent';
+            x.style.color = on ? '#fff' : 'var(--ink-soft)';
+          });
+          wpSec.style.display = m === 'wallpaper' ? 'block' : 'none';
+          ptSec.style.display = m === 'paint' ? 'block' : 'none';
+        });
+        load();
+      })();
+      </script>
+
+      <!-- Admin: rate this design (RLHF-lite for the generator) -->
+      <div id="rate-block" style="margin-top:28px;padding:18px;border:1px solid var(--line);border-radius:10px;background:rgba(0,0,0,.02)">
+        <h3 style="font-family:var(--serif);font-weight:300;font-size:18px;margin:0 0 4px">Rate this design</h3>
+        <p style="font-size:11px;color:var(--ink-faint);margin:0 0 14px;letter-spacing:.04em">Admin-only · color · style · composition · 1-10 · feeds the next retry</p>
+        <div class="rate-rows" style="display:grid;grid-template-columns:120px 1fr 36px;gap:10px;align-items:center;font-size:13px">
+          <label for="r-color">Color</label>
+          <input id="r-color" type="range" min="1" max="10" step="1" value="7">
+          <span id="r-color-v">7</span>
+          <label for="r-style">Style</label>
+          <input id="r-style" type="range" min="1" max="10" step="1" value="7">
+          <span id="r-style-v">7</span>
+          <label for="r-comp">Composition</label>
+          <input id="r-comp" type="range" min="1" max="10" step="1" value="7">
+          <span id="r-comp-v">7</span>
+          <label for="r-space">Spacing<br><small style="color:var(--ink-faint)">tighter ⟷ wider</small></label>
+          <input id="r-space" type="range" min="0" max="100" step="5" value="50">
+          <span id="r-space-v">50</span>
+          <label for="r-invert">Invert palette</label>
+          <label style="display:flex;align-items:center;gap:8px;font-size:12px;color:var(--ink-soft)"><input id="r-invert" type="checkbox"> swap dominant/recessive</label>
+          <span></span>
+        </div>
+        <textarea id="r-notes" placeholder="Notes for the model (optional)" style="width:100%;margin-top:10px;padding:8px;border:1px solid var(--line);border-radius:6px;font-family:inherit;font-size:13px;min-height:44px"></textarea>
+        <div style="display:flex;gap:8px;margin-top:10px;flex-wrap:wrap">
+          <button type="button" class="btn-outline" id="btn-rate-save">Save rating</button>
+          <button type="button" class="btn-outline" id="btn-rate-retry">Save + try again</button>
+          <span id="rate-status" style="font-size:12px;color:var(--ink-faint);align-self:center"></span>
+        </div>
+        <details id="rate-history" style="margin-top:14px">
+          <summary style="cursor:pointer;font-size:12px;color:var(--ink-soft)">Rating history (overrides on top)</summary>
+          <div id="rate-history-list" style="margin-top:8px;font-size:12px;color:var(--ink-soft)">Loading…</div>
+        </details>
+      </div>
+      <script>
+      (function(){
+        var id = ${design.id};
+        function vof(x){ return document.getElementById(x).value; }
+        function bind(slider, out){ var s=document.getElementById(slider), o=document.getElementById(out); s.addEventListener('input',function(){o.textContent=s.value;}); }
+        bind('r-color','r-color-v'); bind('r-style','r-style-v'); bind('r-comp','r-comp-v'); bind('r-space','r-space-v');
+        function payload(){return{color:+vof('r-color'),style:+vof('r-style'),composition:+vof('r-comp'),spacing:+vof('r-space'),invert:document.getElementById('r-invert').checked,notes:vof('r-notes')||null};}
+        function setStatus(t,isErr){var s=document.getElementById('rate-status');s.textContent=t;s.style.color=isErr?'#b00020':'var(--ink-faint)';}
+        async function post(url, body){var r=await fetch(url+'?review=1',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});return r.json();}
+        document.getElementById('btn-rate-save').addEventListener('click', async function(){
+          setStatus('Saving…');
+          var j = await post('/api/design/'+id+'/rate', payload());
+          setStatus(j.ok ? 'Saved rating #' + j.rating_id : ('Error: ' + (j.error||'?')), !j.ok);
+          if (j.ok) loadHistory();
+        });
+        document.getElementById('btn-rate-retry').addEventListener('click', async function(){
+          setStatus('Saving + queueing retry…');
+          var j = await post('/api/design/'+id+'/rate', payload());
+          if (!j.ok) { setStatus('Error: '+(j.error||'?'), true); return; }
+          var k = await post('/api/design/'+id+'/retry', {});
+          setStatus(k.ok ? ('Retry queued — nudges: ' + (k.nudges||[]).join(' · ')) : ('Retry error: '+(k.error||'?')), !k.ok);
+          loadHistory();
+        });
+        async function loadHistory(){
+          try {
+            var r = await fetch('/api/design/'+id+'/ratings?review=1').then(r=>r.json());
+            var list = document.getElementById('rate-history-list');
+            if (!r.ratings || !r.ratings.length) { list.textContent = 'No ratings yet.'; return; }
+            list.innerHTML = r.ratings.map(function(x){
+              var when = (x.created_at || '').replace('T',' ').slice(0,16);
+              return '<div style="padding:6px 0;border-top:1px dashed var(--line)">'+
+                when + ' — C:' + x.color_score + ' S:' + x.style_score + ' Co:' + x.composition_score +
+                ' space:' + x.spacing + (x.invert ? ' invert' : '') +
+                (x.retry_count > 0 ? ' · ' + x.retry_count + ' retr' + (x.retry_count===1?'y':'ies') : '') +
+                (x.notes ? '<div style="color:var(--ink-faint)">'+ (x.notes||'').replace(/</g,'&lt;') +'</div>' : '') +
+                '</div>';
+            }).join('');
+          } catch(e) {}
+        }
+        loadHistory();
+      })();
+      </script>
+      ` : ''}
+
+      ${roomCtx.length ? `
+      <!-- Room mockup with context toggle (residential / commercial / hospitality) -->
+      <div class="room-toggle" style="margin:22px 0 4px">
+        <div class="room-pills" role="tablist" style="display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap">
+          ${roomCtx.map((c, i) => `
+            <button type="button" role="tab"
+                    class="room-pill${i===0?' active':''}"
+                    data-room="${c.room}"
+                    aria-selected="${i===0?'true':'false'}"
+                    style="padding:7px 14px;border-radius:18px;border:1px solid rgba(0,0,0,.15);background:${i===0?'#1a1816':'transparent'};color:${i===0?'#fff':'inherit'};font:inherit;font-size:12.5px;letter-spacing:.3px;cursor:pointer;transition:all .18s">${c.label}</button>
+          `).join('')}
+        </div>
+        <figure style="margin:0;border-radius:10px;overflow:hidden;background:rgba(0,0,0,.04)">
+          <img id="room-toggle-img"
+               src="/designs/room/design_${design.id}_${roomCtx[0].room}.png"
+               alt="${design.title} — ${roomCtx[0].label.toLowerCase()}"
+               loading="lazy"
+               style="display:block;width:100%;height:auto;border-radius:10px 10px 0 0">
+          <figcaption id="room-toggle-cap" style="font-size:12px;color:var(--ink-faint);padding:8px 12px">${roomCtx[0].label} · ${roomCtx[0].room.replace(/_/g,' ')} — AI rendering</figcaption>
+        </figure>
+        <script>
+        (function(){
+          var pills = document.querySelectorAll('.room-pill');
+          var img   = document.getElementById('room-toggle-img');
+          var cap   = document.getElementById('room-toggle-cap');
+          var did   = ${design.id};
+          var labels = ${JSON.stringify(Object.fromEntries(roomCtx.map(c => [c.room, c.label])))};
+          pills.forEach(function(p){
+            p.addEventListener('click', function(){
+              pills.forEach(function(x){
+                x.classList.remove('active');
+                x.setAttribute('aria-selected','false');
+                x.style.background = 'transparent';
+                x.style.color = 'inherit';
+              });
+              p.classList.add('active');
+              p.setAttribute('aria-selected','true');
+              p.style.background = '#1a1816';
+              p.style.color = '#fff';
+              var room = p.dataset.room;
+              img.src = '/designs/room/design_' + did + '_' + room + '.png';
+              img.alt = ${JSON.stringify(design.title)} + ' — ' + (labels[room] || '').toLowerCase();
+              cap.textContent = (labels[room]||'') + ' · ' + room.replace(/_/g,' ') + ' — AI rendering';
+            });
+          });
+        })();
+        </script>
+      </div>
+      ` : ''}
+
+      <!-- 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>
+        <button type="button" class="btn-outline" id="btn-spoon-dry"   data-id="${design.id}">Spoonflower (dry-run)</button>
+        <button type="button" class="btn-outline" id="btn-spoon-live"  data-id="${design.id}">Spoonflower — Upload as New</button>
+        <button type="button" class="btn-outline" id="btn-shopify"     data-id="${design.id}">Upload to Shopify</button>
+        <button type="button" class="btn-outline" id="btn-narrate"     data-id="${design.id}">Narrate (ElevenLabs)</button>
+      </div>
+      <div style="margin-top:8px;font:11px/1.4 var(--sans);color:#666">
+        DW Original Number:
+        <code style="font:12px ui-monospace,Menlo,monospace;background:rgba(0,0,0,.05);padding:2px 6px;border-radius:3px">${design.dig_number || 'DIG_AI_' + String(design.id).padStart(6,'0')}</code>
+      </div>
+      <div id="tool-result" style="margin-top:16px;display:none;padding:14px;border-radius:6px;background:rgba(0,0,0,.05);font-size:13px;line-height:1.5"></div>
+      <div id="room-result" style="margin-top:14px;display:none">
+        <img id="room-img" alt="Room preview" style="max-width:100%;border-radius:8px;border:1px solid rgba(0,0,0,.08)">
+      </div>
+      <audio id="narrate-audio" controls style="margin-top:10px;display:none;width:100%"></audio>
+
+      <script>
+      (function(){
+        var R = document.getElementById('tool-result');
+        function show(msg, isErr) {
+          R.style.display='block';
+          R.style.background = isErr ? '#f8d7da' : '#d4edda';
+          R.style.color = isErr ? '#721c24' : '#155724';
+          R.innerHTML = msg;
+        }
+        async function post(url, body) {
+          var r = await fetch(url, {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body)});
+          return r.json();
+        }
+
+        document.getElementById('btn-room').addEventListener('click', async function(){
+          show('Generating room preview… (10–30s)', false);
+          this.disabled = true;
+          var j = await post('/api/room', { design_id: this.dataset.id, roomType:'bedroom', angle:'straight_on', cameraDistance:5 });
+          this.disabled = false;
+          if (j.ok) {
+            show('Room preview ready.', false);
+            var img = document.getElementById('room-img');
+            img.src = j.image_url + '?t=' + Date.now();
+            document.getElementById('room-result').style.display = 'block';
+          } else {
+            show('Room generation failed: ' + (j.error||'unknown'), true);
+          }
+        });
+
+        document.getElementById('btn-spoon-dry').addEventListener('click', async function(){
+          show('Spoonflower dry-run: logging in + checking upload page…', false);
+          this.disabled = true;
+          var j = await post('/api/spoonflower/upload', { design_id: this.dataset.id, dry: true });
+          this.disabled = false;
+          if (j.ok) show('Dry-run OK. job=' + j.job_id + '. <a href="/api/spoonflower/status/' + j.job_id + '" target="_blank" rel="noopener noreferrer">view log</a>', false);
+          else show('Dry-run failed: ' + (j.error||'unknown'), true);
+        });
+
+        document.getElementById('btn-spoon-live').addEventListener('click', async function(){
+          if (!confirm('Upload this design to Spoonflower as a NEW product? This is a public publish.')) return;
+          var btn = this;
+          var designId = this.dataset.id;
+          show('Live upload running… (Spoonflower publish typically takes 60–180s)', false);
+          btn.disabled = true;
+          var j = await post('/api/spoonflower/upload', { design_id: designId, dry: false });
+          if (!j.ok) {
+            btn.disabled = false;
+            show('Upload failed: ' + (j.error||'unknown'), true);
+            return;
+          }
+          var jobId = j.job_id;
+          show('Upload kicked off (job ' + jobId + '). Polling for the new Spoonflower pattern number…', false);
+          // Poll /api/spoonflower/status/:job until the python script
+          // finishes. 5s cadence, hard cap at 6 minutes.
+          var started = Date.now();
+          var poll = setInterval(async function(){
+            try {
+              var r = await fetch('/api/spoonflower/status/' + jobId);
+              var s = await r.json();
+              var st = s && s.job && s.job.status;
+              if (st === 'done' || st === 'error') {
+                clearInterval(poll);
+                btn.disabled = false;
+                if (st === 'error') {
+                  show('Spoonflower upload errored. <a href="/api/spoonflower/status/' + jobId + '" target="_blank" rel="noopener noreferrer">view log</a>', true);
+                  return;
+                }
+                if (s.sf_design_id) {
+                  var link = s.sf_public_url || ('https://www.spoonflower.com/en/shop/wallpaper/' + s.sf_design_id);
+                  show(
+                    '<strong>Spoonflower pattern #' + s.sf_design_id + '</strong> created. ' +
+                    '<a href="' + link + '" target="_blank" rel="noopener noreferrer">View on Spoonflower ↗</a><br>' +
+                    '<span style="font-size:12px;color:#3a3631">Next: click <b>Upload to Shopify</b> below to publish to designer-laboratory-sandbox.</span>',
+                    false
+                  );
+                  var sb = document.getElementById('btn-shopify');
+                  if (sb) { sb.style.borderColor = '#c9a14b'; sb.style.background = '#fff8e6'; }
+                } else {
+                  show('Upload finished but Spoonflower pattern ID could not be parsed. <a href="/api/spoonflower/status/' + jobId + '" target="_blank" rel="noopener noreferrer">view log</a>', true);
+                }
+                return;
+              }
+              if (Date.now() - started > 6 * 60 * 1000) {
+                clearInterval(poll);
+                btn.disabled = false;
+                show('Upload still running after 6 min — check <a href="/api/spoonflower/status/' + jobId + '" target="_blank" rel="noopener noreferrer">job log</a> manually.', true);
+              }
+            } catch (e) { /* swallow transient poll errors */ }
+          }, 5000);
+        });
+
+        // Upload to Shopify — admin only (endpoint 404s for non-admins).
+        var shopifyBtn = document.getElementById('btn-shopify');
+        if (shopifyBtn) {
+          shopifyBtn.addEventListener('click', async function(){
+            if (!confirm('Create a new Shopify product on designer-laboratory-sandbox for this design? It will land as DRAFT.')) return;
+            var btn = this;
+            btn.disabled = true;
+            show('Creating Shopify product…', false);
+            try {
+              var r = await fetch('/api/design/' + btn.dataset.id + '/upload-shopify', {
+                method: 'POST',
+                headers: { 'Content-Type': 'application/json' },
+                body: JSON.stringify({})
+              });
+              var j = await r.json();
+              if (r.ok && j.ok) {
+                show(
+                  '<strong>Shopify product ' + j.shopify_product_id + '</strong> created ' +
+                  (j.already_uploaded ? '(already existed)' : '(DRAFT)') + '. ' +
+                  'SKU <code>' + (j.dig_number || '') + '</code>. ' +
+                  '<a href="' + j.shopify_product_url + '" target="_blank" rel="noopener noreferrer">View on store ↗</a>' +
+                  (j.shopify_admin_url ? ' · <a href="' + j.shopify_admin_url + '" target="_blank" rel="noopener noreferrer">admin ↗</a>' : ''),
+                  false
+                );
+              } else {
+                show('Shopify upload failed: ' + (j.error || (j.shopify_response && JSON.stringify(j.shopify_response).slice(0,300)) || 'unknown'), true);
+              }
+            } catch (e) { show('Shopify upload error: ' + e.message, true); }
+            btn.disabled = false;
+          });
+        }
+
+        document.getElementById('btn-narrate').addEventListener('click', async function(){
+          show('Generating narration…', false);
+          this.disabled = true;
+          try {
+            var r = await fetch('/api/voice/preview?design_id=' + this.dataset.id);
+            if (!r.ok) { var t = await r.text(); show('Narration failed: ' + t, true); this.disabled=false; return; }
+            var blob = await r.blob();
+            var a = document.getElementById('narrate-audio');
+            a.src = URL.createObjectURL(blob);
+            a.style.display = 'block';
+            a.play();
+            show('Narration ready.', false);
+          } catch(e) { show('Narration error: ' + e.message, true); }
+          this.disabled = false;
+        });
+      })();
+      </script>
+
+      <!-- ── EDIT PANEL: hue/sat/vibrance/palette/dots/wheel ────────────────── -->
+      <script>
+      (function(){
+        var img       = document.getElementById('detail-img');
+        var canvas    = document.getElementById('detail-canvas');
+        var modal     = document.getElementById('edit-modal');
+        var hamburger = document.getElementById('edit-hamburger');
+        if (!img || !modal || !hamburger) return;
+
+        function openModal(){
+          modal.hidden = false;
+          hamburger.setAttribute('aria-expanded','true');
+          document.body.style.overflow = 'hidden';
+        }
+        function closeModal(){
+          modal.hidden = true;
+          hamburger.setAttribute('aria-expanded','false');
+          document.body.style.overflow = '';
+        }
+        hamburger.addEventListener('click', openModal);
+        modal.addEventListener('click', function(e){
+          if (e.target.getAttribute('data-close') === '1') closeModal();
+        });
+        document.addEventListener('keydown', function(e){
+          if (e.key === 'Escape' && !modal.hidden) closeModal();
+        });
+
+        // Tab switching
+        document.querySelectorAll('.edit-tab').forEach(function(t){
+          t.addEventListener('click', function(){
+            var key = this.getAttribute('data-tab');
+            document.querySelectorAll('.edit-tab').forEach(function(x){ x.classList.toggle('active', x === t); });
+            document.querySelectorAll('.edit-tabpane').forEach(function(p){
+              p.classList.toggle('active', p.getAttribute('data-pane') === key);
+            });
+          });
+        });
+
+        // ── AI Interior Designer panel (admin-only) ────────────────────────
+        (function(){
+          var params = new URLSearchParams(window.location.search);
+          var adminToken = params.get('admin') || localStorage.getItem('wallco_admin_token') || '';
+          if (params.get('admin')) localStorage.setItem('wallco_admin_token', adminToken);
+          if (!adminToken) return;
+          var block = document.getElementById('ai-designer-block');
+          if (!block) return;
+          block.style.display = 'block';
+
+          var designId = parseInt(document.body.dataset.designId || '0', 10);
+          if (!designId) {
+            var sec = document.getElementById('rooms-gallery');
+            if (sec && sec.dataset.designId) designId = parseInt(sec.dataset.designId, 10);
+          }
+          if (!designId) {
+            // Last-ditch fallback — parse from URL
+            var m = window.location.pathname.match(/\/design\/(\d+)/);
+            if (m) designId = parseInt(m[1], 10);
+          }
+
+          var runBtn   = document.getElementById('ai-designer-run');
+          var applyBtn = document.getElementById('ai-designer-apply');
+          var status   = document.getElementById('ai-designer-status');
+          var result   = document.getElementById('ai-designer-result');
+          var seamWarn = document.getElementById('ai-designer-seam-warn');
+          var lastSuggestion = null;
+
+          async function runAI(apply){
+            status.textContent = apply ? 'Saving…' : 'Asking Gemini…';
+            status.style.color = '#555';
+            try {
+              var r = await fetch('/api/design/' + designId + '/ai-designer', {
+                method: 'POST',
+                headers: { 'Content-Type': 'application/json', 'X-Admin-Token': adminToken },
+                body: JSON.stringify({ apply: !!apply })
+              });
+              var j = await r.json();
+              if (!r.ok) {
+                if (j.error === 'SETTLEMENT_VIOLATION') {
+                  status.style.color = '#b00020';
+                  status.textContent = '⚠ ' + j.message;
+                } else {
+                  status.style.color = '#b00020';
+                  status.textContent = 'Error: ' + (j.error || r.statusText);
+                }
+                return;
+              }
+              lastSuggestion = j.suggested;
+              result.style.display = 'block';
+              document.getElementById('ai-d-title').value   = j.suggested.title    || '';
+              document.getElementById('ai-d-pattern').value = j.suggested.pattern  || '';
+              document.getElementById('ai-d-color').value   = j.suggested.color_name || '';
+              document.getElementById('ai-d-dig').value     = j.suggested.dig_number || '';
+              document.getElementById('ai-d-tags').value    = (j.suggested.tags || []).join(', ');
+              document.getElementById('ai-d-notes').textContent = j.suggested.designer_notes || '';
+              seamWarn.style.display = j.suggested.seam_visible ? 'block' : 'none';
+              status.style.color = '#1a3';
+              status.textContent = apply ? '✓ Saved' : 'Review & Apply →';
+            } catch (e) {
+              status.style.color = '#b00020';
+              status.textContent = 'Error: ' + e.message;
+            }
+          }
+
+          runBtn.addEventListener('click', function(){ runAI(false); });
+          applyBtn.addEventListener('click', function(){ runAI(true); });
+        })();
+
+        // ── 1. CSS-filter live adjustments ─────────────────────────────────
+        var previewImg = document.getElementById('edit-preview-img');
+        var adjState = { hue:0, sat:100, vib:100, bri:100, con:100, opa:100, sep:0, blur:0 };
+        function applyFilter() {
+          var vibBoost = (adjState.vib - 100) / 200;
+          var filter =
+            'hue-rotate(' + adjState.hue + 'deg) ' +
+            'saturate(' + (adjState.sat / 100) + ') ' +
+            'brightness(' + (adjState.bri / 100) + ') ' +
+            'contrast(' + (adjState.con / 100) + ') ' +
+            'sepia(' + (adjState.sep / 100) + ') ' +
+            'blur(' + adjState.blur + 'px) ' +
+            'saturate(' + (1 + vibBoost) + ')';
+          var opacity = adjState.opa / 100;
+          img.style.filter = filter;
+          img.style.opacity = opacity;
+          if (previewImg) { previewImg.style.filter = filter; previewImg.style.opacity = opacity; }
+          var ro = document.getElementById('edit-preview-readout');
+          if (ro) ro.textContent =
+            'Hue ' + adjState.hue + '°  ·  Sat ' + adjState.sat + '%  ·  Vib ' + adjState.vib +
+            '%  ·  Bri ' + adjState.bri + '%  ·  Con ' + adjState.con +
+            '%  ·  Opa ' + adjState.opa + '%  ·  Sep ' + adjState.sep +
+            '%  ·  Blur ' + adjState.blur + 'px';
+        }
+        var sliders = [
+          ['hue',  '°'], ['sat','%'], ['vib','%'], ['bri','%'],
+          ['con','%'],   ['opa','%'], ['sep','%'], ['blur','px']
+        ];
+        sliders.forEach(function(s){
+          var el = document.getElementById('adj-' + s[0]);
+          var v  = document.getElementById('adj-' + s[0] + '-v');
+          if (!el || !v) return;
+          el.addEventListener('input', function(){
+            adjState[s[0]] = parseFloat(this.value);
+            v.textContent = this.value + s[1];
+            applyFilter();
+          });
+        });
+        document.getElementById('adj-reset').addEventListener('click', function(){
+          adjState = { hue:0, sat:100, vib:100, bri:100, con:100, opa:100, sep:0, blur:0 };
+          sliders.forEach(function(s){
+            var el = document.getElementById('adj-' + s[0]);
+            var v  = document.getElementById('adj-' + s[0] + '-v');
+            var def = { hue:0, sat:100, vib:100, bri:100, con:100, opa:100, sep:0, blur:0 }[s[0]];
+            if (el) el.value = def;
+            if (v) v.textContent = def + s[1];
+          });
+          applyFilter();
+        });
+
+        // ── 2. Palette extraction via canvas ───────────────────────────────
+        var PALETTE = []; // [{hex, count, r, g, b, h, s, l}]
+        var selectedDot = null;
+
+        function rgbToHex(r,g,b){ return '#'+[r,g,b].map(function(x){return x.toString(16).padStart(2,'0');}).join(''); }
+        function hexToRgb(h){ var n=parseInt(h.replace('#',''),16); return [(n>>16)&255,(n>>8)&255,n&255]; }
+        function rgbToHsl(r,g,b){
+          r/=255; g/=255; b/=255;
+          var max=Math.max(r,g,b), min=Math.min(r,g,b), l=(max+min)/2, h=0, s=0;
+          if (max!==min) {
+            var 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 extractPalette(n) {
+          var iw = img.naturalWidth, ih = img.naturalHeight;
+          if (!iw) return [];
+          var scale = Math.min(64 / iw, 64 / ih, 1);
+          var w = Math.max(8, Math.round(iw * scale));
+          var h = Math.max(8, Math.round(ih * scale));
+          canvas.width = w; canvas.height = h;
+          var ctx = canvas.getContext('2d');
+          ctx.drawImage(img, 0, 0, w, h);
+          var data;
+          try { data = ctx.getImageData(0, 0, w, h).data; }
+          catch(e) { return []; } // CORS
+          // Bucket by 6-bit color (4*4*4=64 buckets) — fast color quantization
+          var buckets = {};
+          for (var i = 0; i < data.length; i += 4) {
+            var r = data[i], g = data[i+1], b = data[i+2];
+            var key = (r >> 5) + '_' + (g >> 5) + '_' + (b >> 5);
+            if (!buckets[key]) buckets[key] = {r:0,g:0,b:0,count:0};
+            buckets[key].r += r; buckets[key].g += g; buckets[key].b += b; buckets[key].count++;
+          }
+          var palette = Object.values(buckets).map(function(b){
+            return {
+              r: Math.round(b.r/b.count), g: Math.round(b.g/b.count), b: Math.round(b.b/b.count),
+              count: b.count
+            };
+          }).sort(function(a,b){ return b.count - a.count; }).slice(0, n);
+          return palette.map(function(p){
+            var hex = rgbToHex(p.r, p.g, p.b);
+            var hsl = rgbToHsl(p.r, p.g, p.b);
+            return { hex:hex, r:p.r, g:p.g, b:p.b, count:p.count, h:hsl[0], s:hsl[1], l:hsl[2] };
+          });
+        }
+
+        function renderDots(sort) {
+          var c = PALETTE.slice();
+          if (sort === 'hex')   c.sort(function(a,b){ return a.hex.localeCompare(b.hex); });
+          if (sort === 'light') c.sort(function(a,b){ return b.l - a.l; });
+          if (sort === 'hue')   c.sort(function(a,b){ return a.h - b.h; });
+          var container = document.getElementById('palette-dots');
+          container.innerHTML = c.map(function(p, i){
+            return '<div class="palette-dot" draggable="true" data-hex="' + p.hex + '" style="background:' + p.hex + '" title="' + p.hex + ' · ' + p.count + 'px"><span class="dot-label">' + p.hex + '</span></div>';
+          }).join('');
+          // Wire up drag-drop + click-select
+          container.querySelectorAll('.palette-dot').forEach(function(dot){
+            dot.addEventListener('click', function(){
+              container.querySelectorAll('.palette-dot.selected').forEach(function(d){ d.classList.remove('selected'); });
+              dot.classList.add('selected');
+              selectedDot = dot.getAttribute('data-hex');
+              document.getElementById('wheel-readout').textContent =
+                'Selected ' + selectedDot + ' — click wheel or a suggested swatch to recolor';
+              renderSuggested(selectedDot);
+            });
+            dot.addEventListener('dragstart', function(e){ e.dataTransfer.setData('text/hex', dot.getAttribute('data-hex')); });
+            dot.addEventListener('dragover', function(e){ e.preventDefault(); dot.classList.add('drag-over'); });
+            dot.addEventListener('dragleave', function(){ dot.classList.remove('drag-over'); });
+            dot.addEventListener('drop', function(e){
+              e.preventDefault();
+              dot.classList.remove('drag-over');
+              var src = e.dataTransfer.getData('text/hex');
+              var tgt = dot.getAttribute('data-hex');
+              if (src && tgt && src !== tgt) recolor(tgt, src);
+            });
+          });
+        }
+
+        document.querySelectorAll('.dots-sort').forEach(function(btn){
+          btn.addEventListener('click', function(){
+            document.querySelectorAll('.dots-sort').forEach(function(b){ b.classList.remove('active'); });
+            btn.classList.add('active');
+            renderDots(btn.getAttribute('data-sort'));
+          });
+        });
+
+        // ── 3. Recolor a palette bucket — canvas pixel replace ─────────────
+        function recolor(fromHex, toHex) {
+          var iw = img.naturalWidth, ih = img.naturalHeight;
+          if (!iw) return;
+          canvas.width = iw; canvas.height = ih;
+          var ctx = canvas.getContext('2d');
+          ctx.drawImage(img, 0, 0);
+          var imgData;
+          try { imgData = ctx.getImageData(0,0,iw,ih); }
+          catch(e) { alert('Image is CORS-locked — open from the same origin to recolor.'); return; }
+          var d = imgData.data;
+          var from = hexToRgb(fromHex), to = hexToRgb(toHex);
+          // Convert from to HSL to tolerate similar hues
+          var fromHsl = rgbToHsl(from[0], from[1], from[2]);
+          var toHsl   = rgbToHsl(to[0],   to[1],   to[2]);
+          var TOLERANCE_H = 25;   // degrees
+          var TOLERANCE_L = 0.18; // lightness window
+          var changed = 0;
+          for (var i = 0; i < d.length; i += 4) {
+            var hsl = rgbToHsl(d[i], d[i+1], d[i+2]);
+            var dh = Math.min(Math.abs(hsl[0] - fromHsl[0]), 360 - Math.abs(hsl[0] - fromHsl[0]));
+            var dl = Math.abs(hsl[2] - fromHsl[2]);
+            if (dh < TOLERANCE_H && dl < TOLERANCE_L) {
+              // shift hue toward target, preserve original luminance
+              var newH = (toHsl[0] + (hsl[0] - fromHsl[0]) * 0.3 + 360) % 360;
+              var rgb = hslToRgb(newH, hsl[1] * (toHsl[1] / Math.max(fromHsl[1], 0.001)), hsl[2]);
+              d[i] = rgb[0]; d[i+1] = rgb[1]; d[i+2] = rgb[2];
+              changed++;
+            }
+          }
+          ctx.putImageData(imgData, 0, 0);
+          // Replace the img src with the canvas data URL
+          var dataUrl = canvas.toDataURL('image/png');
+          img.src = dataUrl;
+          if (previewImg) previewImg.src = dataUrl;
+          document.getElementById('wheel-readout').textContent =
+            'Recolored ' + changed.toLocaleString() + ' pixels  (' + fromHex + ' → ' + toHex + ')';
+          // After recolor, re-extract palette
+          setTimeout(function(){ PALETTE = extractPalette(14); renderDots('hex'); }, 200);
+        }
+
+        function hslToRgb(h, s, l) {
+          var c = (1 - Math.abs(2*l - 1)) * s, x = c * (1 - Math.abs(((h/60) % 2) - 1)), m = l - c/2;
+          var r, g, b;
+          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 [Math.round((r+m)*255), Math.round((g+m)*255), Math.round((b+m)*255)];
+        }
+
+        // ── 4. HSV color wheel ────────────────────────────────────────────
+        var wheel = document.getElementById('color-wheel');
+        if (wheel) {
+          var wctx = wheel.getContext('2d');
+          var W = wheel.width, H = wheel.height, R = W/2;
+          var imgD = wctx.createImageData(W, H);
+          for (var y = 0; y < H; y++) {
+            for (var x = 0; x < W; x++) {
+              var dx = x - R, dy = y - R, dist = Math.sqrt(dx*dx + dy*dy);
+              if (dist > R - 2) continue;
+              var ang = Math.atan2(dy, dx) * 180 / Math.PI;
+              if (ang < 0) ang += 360;
+              var sat = Math.min(1, dist / (R - 2));
+              var rgb = hslToRgb(ang, sat, 0.55);
+              var idx = (y * W + x) * 4;
+              imgD.data[idx] = rgb[0]; imgD.data[idx+1] = rgb[1]; imgD.data[idx+2] = rgb[2]; imgD.data[idx+3] = 255;
+            }
+          }
+          wctx.putImageData(imgD, 0, 0);
+          wheel.addEventListener('click', function(e){
+            var rect = wheel.getBoundingClientRect();
+            var x = e.clientX - rect.left, y = e.clientY - rect.top;
+            var pix = wctx.getImageData(x, y, 1, 1).data;
+            if (pix[3] === 0) return;
+            var newHex = rgbToHex(pix[0], pix[1], pix[2]);
+            if (!selectedDot) {
+              document.getElementById('wheel-readout').textContent = 'Pick a swatch first (click a dot above).';
+              return;
+            }
+            recolor(selectedDot, newHex);
+            selectedDot = newHex;
+          });
+        }
+
+        // ── 5. Suggested swatches (analogous + complementary) ─────────────
+        function renderSuggested(hex) {
+          var rgb = hexToRgb(hex), hsl = rgbToHsl(rgb[0], rgb[1], rgb[2]);
+          var suggestions = [
+            (hsl[0] +  30) % 360, (hsl[0] -  30 + 360) % 360,  // analogous
+            (hsl[0] + 180) % 360,                               // complementary
+            (hsl[0] + 120) % 360, (hsl[0] + 240) % 360          // triadic
+          ];
+          var sw = suggestions.map(function(h){
+            var rgb = hslToRgb(h, Math.max(hsl[1], 0.4), Math.max(0.3, Math.min(0.7, hsl[2])));
+            return rgbToHex(rgb[0], rgb[1], rgb[2]);
+          });
+          document.getElementById('suggested-swatches').innerHTML = sw.map(function(s){
+            return '<div title="' + s + '" data-hex="' + s + '" style="background:' + s + '"></div>';
+          }).join('');
+          document.querySelectorAll('#suggested-swatches > div').forEach(function(d){
+            d.addEventListener('click', function(){
+              if (!selectedDot) return;
+              recolor(selectedDot, d.getAttribute('data-hex'));
+              selectedDot = d.getAttribute('data-hex');
+            });
+          });
+        }
+
+        // ── 6. Bake — save current state as a new design ──────────────────
+        document.getElementById('adj-bake').addEventListener('click', async function(){
+          var iw = img.naturalWidth, ih = img.naturalHeight;
+          if (!iw) return;
+          canvas.width = iw; canvas.height = ih;
+          var ctx = canvas.getContext('2d');
+          ctx.filter = img.style.filter || '';
+          ctx.globalAlpha = parseFloat(img.style.opacity) || 1;
+          ctx.drawImage(img, 0, 0);
+          var dataUrl = canvas.toDataURL('image/png');
+          this.disabled = true; this.textContent = 'Saving…';
+          try {
+            var r = await fetch('/api/design/' + ${design.id} + '/bake', {
+              method:'POST', headers:{'Content-Type':'application/json'},
+              body: JSON.stringify({ image_b64: dataUrl.split(',')[1], parent_id: ${design.id} })
+            });
+            if (!r.ok) {
+              var t = await r.text();
+              this.textContent = 'Failed (HTTP ' + r.status + ')';
+              console.error('bake HTTP', r.status, t.slice(0,400));
+              this.disabled = false;
+              return;
+            }
+            var j = await r.json();
+            console.log('bake response', j);
+            if (j.ok && j.design_id) {
+              this.textContent = '✓ Saved as #' + j.design_id + ' — redirecting…';
+              window.location.assign('/design/' + j.design_id);
+            } else {
+              this.textContent = 'Failed: ' + (j.error || 'no design_id returned');
+              this.disabled = false;
+            }
+          } catch(e) {
+            this.textContent = 'Error: ' + e.message;
+            console.error('bake error', e);
+            this.disabled = false;
+          }
+        });
+
+        // ── boot: extract palette once image is ready ─────────────────────
+        function boot() {
+          PALETTE = extractPalette(14);
+          if (PALETTE.length === 0) {
+            document.getElementById('palette-dots').innerHTML =
+              '<div class="dots-loading" style="color:#c66">Image CORS-locked. Palette unavailable.</div>';
+            return;
+          }
+          renderDots('hex');
+        }
+        if (img.complete && img.naturalWidth) boot();
+        else img.addEventListener('load', boot);
+      })();
+      </script>
+
+      ${_isAdmin ? `
+      <!-- Provenance / AI-disclosure note — admin-only.
+           A public-facing legal disclaimer on every design tile invites scrutiny
+           where the design itself is the strongest argument; admins still need
+           to see provenance for QA, but customers see a clean design page. -->
+      <div class="provenance-note">
+        <strong>AI-original provenance:</strong> This pattern was generated by <em>wallco.ai</em>.
+        It does not reproduce any existing copyrighted artwork. Every seed is unique — this exact pattern exists only here.
+      </div>
+      ` : ''}
+
+      ${prev || next ? `<div class="detail-nav">
+        ${prev ? `<a href="/design/${prev.id}" class="detail-nav-link prev">&larr; ${prev.title}</a>` : '<span></span>'}
+        ${next ? `<a href="/design/${next.id}" class="detail-nav-link next">${next.title} &rarr;</a>` : '<span></span>'}
+      </div>` : ''}
+    </div>
+  </div>
+
+  <!-- Design Coordinate — pulls coordinating stripes/plaids from the DW catalog
+       that use the design's palette, plus AI-generated coordinates. -->
+  <section
+    id="pairs-well-with"
+    class="dc-section"
+    data-pairs-endpoint="/api/coordinates/pairs"
+    data-ai-endpoint="/api/coordinates/ai"
+    data-source-palette="${design.dominant_hex || ''}"
+    data-source-title="${(design.title || '').replace(/"/g, '&quot;')}"
+    data-source-image="${design.image_url || ''}"
+    style="margin: 48px auto 0; max-width: 1200px; padding: 0 24px; clear: both;"
+    hidden
+  >
+    <button type="button" class="pairs-well__cta" aria-expanded="false">
+      <span class="pairs-well__cta-text">Design Coordinate</span>
+      <span class="pairs-well__cta-icon" aria-hidden="true">+</span>
+    </button>
+
+    <div class="pairs-well__source-palette" id="pairs-well-source-palette" hidden></div>
+    <div class="pairs-well__vendor-picker" id="pairs-well-vendor-picker" hidden>
+      <h4>Paint brands</h4>
+      <div class="chips">
+        <label><input type="checkbox" value="sw"   checked> Sherwin-Williams</label>
+        <label><input type="checkbox" value="de"   checked> Dunn-Edwards</label>
+        <label><input type="checkbox" value="bm"   checked> Benjamin Moore</label>
+        <label><input type="checkbox" value="fb"   checked> Farrow &amp; Ball</label>
+        <label><input type="checkbox" value="behr"        > Behr</label>
+        <label><input type="checkbox" value="ppg"         > PPG</label>
+        <label><input type="checkbox" value="ral"         > RAL</label>
+      </div>
+    </div>
+
+    <div class="pairs-well__catalog" id="pairs-well-catalog" hidden>
+      <div class="pairs-well__panel-head">
+        <h3 style="font:500 22px/1.2 ui-serif,Georgia,serif;">Stripes &amp; Plaids from our catalog <span class="count" id="pairs-well-catalog-count"></span></h3>
+        <div class="pairs-well__controls">
+          <label for="pairs-well-sort">Sort</label>
+          <select id="pairs-well-sort" class="pw-sort"
+            style="padding:5px 10px;border:1px solid #d8d0c0;background:#fff;color:#1a1a1a;border-radius:999px;font:11px ui-sans-serif,system-ui,sans-serif;letter-spacing:.04em;cursor:pointer">
+            <optgroup label="Match quality">
+              <option value="match">Best match (default)</option>
+            </optgroup>
+            <optgroup label="Alphabetical">
+              <option value="title">Title A → Z</option>
+              <option value="title-desc">Title Z → A</option>
+            </optgroup>
+            <optgroup label="Style">
+              <option value="motif">Style / motif</option>
+            </optgroup>
+            <optgroup label="Color">
+              <option value="light-dark">Light → Dark</option>
+              <option value="dark-light">Dark → Light</option>
+              <option value="color-wheel">Color Wheel</option>
+              <option value="vivid">Most Vivid</option>
+              <option value="muted">Most Muted</option>
+            </optgroup>
+            <optgroup label="Other">
+              <option value="random">Random</option>
+            </optgroup>
+          </select>
+          <label for="pairs-well-cols-catalog">Columns</label>
+          <input id="pairs-well-cols-catalog" type="range" min="3" max="12" value="8" step="1">
+          <span class="pairs-well__cols-val" id="pairs-well-cols-catalog-val">8</span>
+        </div>
+      </div>
+      <div id="pairs-well-grid" class="pairs-well__grid"></div>
+    </div>
+
+    <div class="pairs-well__ai" id="pairs-well-ai" hidden>
+      <div class="pairs-well__panel-head">
+        <h3 style="font:500 22px/1.2 ui-serif,Georgia,serif;">AI Generated Coordinates <span class="ai-badge">Preview · 24 options</span></h3>
+      </div>
+      <!-- ── TOP CONTROLS — repeat width + room scale + columns ─────────────
+           All three persist to localStorage. Repeat-width snaps server-side
+           to a multiple of each pattern's pitch so seams always fit. -->
+      <div class="pw-ai-controls">
+        <div class="pw-ctrl">
+          <label for="pw-tile-in">Repeat width</label>
+          <input id="pw-tile-in" type="range" min="12" max="48" step="2" value="24">
+          <span class="pw-ctrl-val"><span id="pw-tile-in-val">24</span>″</span>
+        </div>
+        <div class="pw-ctrl">
+          <label for="pw-room-in">Room scale</label>
+          <input id="pw-room-in" type="range" min="60" max="180" step="6" value="96">
+          <span class="pw-ctrl-val"><span id="pw-room-in-val">96</span>″ wall</span>
+        </div>
+        <div class="pw-ctrl">
+          <label for="pairs-well-cols-ai">Columns</label>
+          <input id="pairs-well-cols-ai" type="range" min="3" max="12" value="8" step="1">
+          <span class="pw-ctrl-val" id="pairs-well-cols-ai-val">8</span>
+        </div>
+      </div>
+      <div id="pairs-well-ai-grid" class="pairs-well__grid pairs-well__grid--ai"></div>
+    </div>
+  </section>
+
+  ${_isAdmin ? `
+  <!-- ── PATTERN EDITOR MODAL (admin) ────────────────────────────────────
+       1–5 color slots that accept drag-drop from any .cs-dot on the page.
+       Live preview re-renders via /api/ai-coordinates/render. Save updates
+       the source card in-place; Save as New appends a custom card. -->
+  <div id="pw-editor" class="pw-editor" hidden>
+    <div class="pw-editor__backdrop" data-close="1"></div>
+    <div class="pw-editor__sheet" role="dialog" aria-label="Pattern editor">
+      <header class="pw-editor__head">
+        <h3>Edit pattern</h3>
+        <button type="button" class="pw-editor__x" data-close="1">×</button>
+      </header>
+      <div class="pw-editor__body">
+        <div class="pw-editor__preview">
+          <img id="pw-editor-img" alt="" src="">
+        </div>
+        <div class="pw-editor__form">
+          <label class="pw-fld">
+            <span>Name</span>
+            <input id="pw-fld-label" type="text" maxlength="80" autocomplete="off">
+          </label>
+          <label class="pw-fld">
+            <span>Motif</span>
+            <select id="pw-fld-motif">
+              <option value="stripe">Stripe</option>
+              <option value="plaid">Plaid</option>
+              <option value="tartan">Tartan</option>
+            </select>
+          </label>
+          <label class="pw-fld">
+            <span>Style</span>
+            <select id="pw-fld-style">
+              <optgroup label="Stripes">
+                <option value="even">Even stripe</option>
+                <option value="triple">Triple stripe (3 colors)</option>
+                <option value="pinstripe">Pinstripe</option>
+                <option value="pinstripe-pair">Twin pinstripe</option>
+                <option value="cabana">Cabana</option>
+                <option value="awning">Awning</option>
+                <option value="wavy">Wavy stripe</option>
+                <option value="hairline">Hairline</option>
+                <option value="block">Block</option>
+              </optgroup>
+              <optgroup label="Plaids / checks">
+                <option value="buffalo">Buffalo check</option>
+                <option value="gingham">Gingham</option>
+                <option value="tattersall">Tattersall</option>
+                <option value="windowpane">Windowpane</option>
+                <option value="madras">Madras</option>
+                <option value="tartan">Tartan</option>
+                <option value="glen">Glen plaid</option>
+                <option value="box">Box plaid</option>
+              </optgroup>
+            </select>
+          </label>
+          <label class="pw-fld">
+            <span>Stripe width <strong id="pw-fld-pitch-val">6″</strong></span>
+            <input id="pw-fld-pitch" type="range" min="0.5" max="18" step="0.5" value="6">
+          </label>
+
+          <div class="pw-fld pw-slot-group">
+            <span>Colors (drag from page)</span>
+            <div class="pw-slots" id="pw-slots"></div>
+            <div class="pw-slot-count">
+              <button type="button" data-n="1">1</button>
+              <button type="button" data-n="2">2</button>
+              <button type="button" data-n="3" class="active">3</button>
+              <button type="button" data-n="4">4</button>
+              <button type="button" data-n="5">5</button>
+              <span class="pw-slot-hint">color slots</span>
+            </div>
+          </div>
+
+          <div class="pw-editor__actions">
+            <button type="button" id="pw-btn-save"     class="pw-btn pw-btn-primary">Save changes</button>
+            <button type="button" id="pw-btn-savenew"  class="pw-btn">Save as new</button>
+            <button type="button" id="pw-btn-cancel"   class="pw-btn pw-btn-ghost" data-close="1">Cancel</button>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+  ` : ''}
+
+  <style>
+    #pairs-well-with { --cols-catalog: 8; --cols-ai: 8; font-family: inherit; }
+    .pairs-well__cta { display:flex; align-items:center; justify-content:space-between; width:100%; padding:18px 24px; margin:0; background:#111; color:#fff; border:0; border-radius:999px; font-size:15px; font-weight:500; letter-spacing:.06em; text-transform:uppercase; cursor:pointer; transition: background 160ms ease, transform 120ms ease; }
+    .pairs-well__cta:hover { background:#2a2a2a; }
+    .pairs-well__cta:active { transform:scale(.995); }
+    .pairs-well__cta[aria-expanded="true"] .pairs-well__cta-icon { transform: rotate(45deg); }
+    .pairs-well__cta-icon { display:inline-block; font-size:22px; line-height:1; transition: transform 200ms ease; }
+    .pairs-well__source-palette { margin: 18px 0 0; }
+    .pairs-well__source-palette h4 { margin: 0 0 8px; font-size: 11px; letter-spacing:.08em; text-transform: uppercase; color:#777; }
+    .pairs-well__source-palette table { border-collapse: collapse; width: 100%; font-size: 12px; }
+    .pairs-well__source-palette th, .pairs-well__source-palette td { text-align: left; padding: 6px 8px; border-bottom: 1px solid #f0ebe2; }
+    .pairs-well__source-palette th { font-weight: 500; color: #777; font-size: 11px; letter-spacing:.06em; text-transform: uppercase; }
+    .pairs-well__source-palette td.swatch { width: 28px; }
+    .pw-dot    { display: inline-block; width: 18px; height: 18px; border-radius: 50%; border: 1px solid rgba(0,0,0,0.12); vertical-align: middle; }
+    .pw-square { display: inline-block; width: 13px; height: 13px; border-radius: 3px; border: 1px solid rgba(0,0,0,0.12); vertical-align: middle; margin-right: 4px; }
+    .pw-code   { color: #888; font-variant-numeric: tabular-nums; font-size: 11px; }
+    .pairs-well__vendor-picker { margin: 18px 0 0; }
+    .pairs-well__vendor-picker h4 { margin: 0 0 8px; font-size: 11px; letter-spacing:.08em; text-transform: uppercase; color: #777; }
+    .pairs-well__vendor-picker .chips { display: flex; flex-wrap: wrap; gap: 6px; }
+    .pairs-well__vendor-picker label { font-size: 12px; padding: 5px 10px; border: 1px solid #e7e7e7; border-radius: 999px; cursor: pointer; background: #fff; user-select: none; }
+    .pairs-well__vendor-picker label:has(input:checked) { background: #1a1a1a; color: #fff; border-color: #1a1a1a; }
+    .pairs-well__vendor-picker input { display: none; }
+    .pairs-well__panel-head { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; margin: 24px 0 12px; }
+    .pairs-well__panel-head h3 { margin: 0; }
+    .pairs-well__panel-head .count { color: #888; font-size: 12px; margin-left: 6px; font-family: ui-sans-serif, system-ui, sans-serif; font-weight: 400; }
+    .pairs-well__panel-head .ai-badge { display: inline-block; padding: 3px 8px; background: #2a2a2a; color: #fff; border-radius: 999px; font-size: 10px; letter-spacing:.08em; text-transform: uppercase; margin-left: 8px; font-family: ui-sans-serif, system-ui, sans-serif; font-weight: 600; }
+    .pairs-well__controls { display: flex; align-items: center; gap: 10px; font-size: 11px; color: #777; text-transform: uppercase; letter-spacing:.06em; }
+    .pairs-well__controls input[type=range] { width: 160px; accent-color: #1a1a1a; }
+    .pairs-well__cols-val { font-variant-numeric: tabular-nums; color: #1a1a1a; min-width: 1.5em; text-align: right; font-weight: 600; }
+    .pairs-well__grid { display: grid; gap: 14px; margin-top: 6px; grid-template-columns: repeat(var(--cols-catalog), minmax(0, 1fr)); }
+    .pairs-well__grid--ai { grid-template-columns: repeat(var(--cols-ai), minmax(0, 1fr)); }
+    @media (max-width: 1100px) { .pairs-well__grid { grid-template-columns: repeat(min(6, var(--cols-catalog)), minmax(0, 1fr)); } .pairs-well__grid--ai { grid-template-columns: repeat(min(6, var(--cols-ai)), minmax(0, 1fr)); } }
+    @media (max-width: 900px)  { .pairs-well__grid { grid-template-columns: repeat(min(4, var(--cols-catalog)), minmax(0, 1fr)); } .pairs-well__grid--ai { grid-template-columns: repeat(min(4, var(--cols-ai)), minmax(0, 1fr)); } }
+    @media (max-width: 600px)  { .pairs-well__grid { grid-template-columns: repeat(min(2, var(--cols-catalog)), minmax(0, 1fr)); } .pairs-well__grid--ai { grid-template-columns: repeat(min(2, var(--cols-ai)), minmax(0, 1fr)); } }
+    .pairs-well__card { display: block; text-decoration: none; color: inherit; background: #fff; border: 1px solid #e7e7e7; border-radius: 6px; overflow: hidden; transition: box-shadow 160ms ease, transform 160ms ease; }
+    .pairs-well__card:hover { box-shadow: 0 6px 20px rgba(0,0,0,0.08); transform: translateY(-2px); }
+    .pairs-well__card--ai { border-style: dashed; border-color: #d9c7a8; }
+    .pairs-well__img-wrap { aspect-ratio: 1 / 1; background: #f4f4f4; overflow: hidden; }
+    .pairs-well__img { width: 100%; height: 100%; object-fit: cover; display: block; }
+    .pairs-well__meta { padding: 10px 12px 12px; }
+    .pairs-well__title { margin: 0 0 4px; font-size: 13px; font-weight: 600; line-height: 1.3; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
+    .pairs-well__vendor { margin: 0 0 6px; font-size: 10px; color: #777; letter-spacing:.04em; text-transform: uppercase; }
+    .pairs-well__why { display: inline-block; padding: 2px 7px; background: #f4ede4; color: #5a4628; border-radius: 999px; font-size: 10px; line-height: 1.4; margin-bottom: 8px; }
+    .pairs-well__card--ai .pairs-well__why { background: #efe6f5; color: #5b3478; }
+    .pairs-well__palette { display: flex; gap: 3px; margin: 0 0 8px; flex-wrap: wrap; }
+    .pairs-well__palette .pw-dot { width: 12px; height: 12px; }
+    .pairs-well__paints { border-top: 1px dashed #ebe4d8; margin-top: 4px; padding-top: 6px; font-size: 10px; line-height: 1.5; color: #444; }
+    .pairs-well__paints .label { color: #888; font-weight: 600; margin-right: 4px; letter-spacing:.04em; }
+    .pairs-well__paints .row { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+    /* Collapsed paint-match table on each coordinate card. Closed by default
+       so the card surface stays clean — palette dots stay visible above, the
+       full SW/DE/BM/F&B paint NAMES live behind a tap. */
+    .pairs-well__paints-d { border-top: 1px dashed #ebe4d8; margin-top: 4px; padding-top: 6px; font-size: 10px; line-height: 1.4; color: #444; }
+    .pairs-well__paints-d > summary {
+      list-style: none; cursor: pointer; color: #888;
+      font-size: 10px; letter-spacing: .06em; text-transform: uppercase;
+      font-weight: 600; padding: 2px 0;
+      display: inline-flex; align-items: center; gap: 6px;
+      user-select: none;
+    }
+    .pairs-well__paints-d > summary::-webkit-details-marker { display: none; }
+    .pairs-well__paints-d > summary:hover { color: #1a1a1a; }
+    .pairs-well__paints-d .pw-d-caret { display:inline-block; transition: transform .15s ease; font-size: 10px; }
+    .pairs-well__paints-d[open] .pw-d-caret { transform: rotate(180deg); }
+    .pairs-well__paints-tbl { width: 100%; border-collapse: collapse; margin-top: 5px; }
+    .pairs-well__paints-tbl th, .pairs-well__paints-tbl td {
+      text-align: left; padding: 3px 6px 3px 0; vertical-align: middle;
+      font-size: 10px; line-height: 1.3;
+    }
+    .pairs-well__paints-tbl th { font-weight: 600; color: #777; width: 32px; letter-spacing:.04em; }
+    .pairs-well__paints-tbl td { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 0; }
+    .pairs-well__paints-tbl .pw-square { display:inline-block; width: 9px; height: 9px; vertical-align: middle; margin-right: 5px; border: 1px solid rgba(0,0,0,.12); }
+    .pairs-well__paints-tbl .pw-code { color: #999; font-family: ui-monospace, monospace; font-size: 9px; }
+    .pairs-well__skeleton { background: linear-gradient(90deg, #eee 0%, #f6f6f6 50%, #eee 100%); background-size: 200% 100%; animation: pwShim 1.2s linear infinite; aspect-ratio: 1 / 1; border-radius: 6px; }
+    @keyframes pwShim { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
+    .pairs-well__empty { padding: 18px; color: #777; font-size: 14px; grid-column: 1 / -1; }
+
+    /* ── Top controls (repeat / room / columns) ──────────────────────────── */
+    .pw-ai-controls {
+      display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px;
+      padding: 14px 16px; background: #fff; border: 1px solid #e7e7e7;
+      border-radius: 8px; margin: 0 0 16px;
+    }
+    @media (max-width: 800px) { .pw-ai-controls { grid-template-columns: 1fr; } }
+    .pw-ctrl { display: flex; align-items: center; gap: 10px; }
+    .pw-ctrl label { font-size: 10.5px; letter-spacing:.14em; text-transform: uppercase;
+      color: #7a6e5a; min-width: 88px; }
+    .pw-ctrl input[type=range] { flex:1; accent-color: #1a1a1a; }
+    .pw-ctrl-val { font-variant-numeric: tabular-nums; color: #1a1a1a;
+      font-size: 12px; font-weight: 600; min-width: 64px; text-align: right; }
+
+    /* ── AI card actions (Duplicate / Edit) — always visible, touch-friendly ─ */
+    .pairs-well__card { position: relative; }
+    .pw-card-actions {
+      position: absolute; top: 6px; right: 6px; z-index: 3;
+      display: flex; gap: 4px;
+    }
+    .pw-card-actions button {
+      background: rgba(20,20,20,0.92); color: #fff; border: 0;
+      font: 700 10px/1 ui-sans-serif, system-ui, sans-serif;
+      letter-spacing:.08em; text-transform: uppercase;
+      padding: 6px 9px; border-radius: 999px; cursor: pointer;
+      box-shadow: 0 1px 3px rgba(0,0,0,0.2);
+      transition: background .12s ease, transform .12s ease;
+    }
+    .pw-card-actions button:hover { background: #000; transform: translateY(-1px); }
+    .pw-card-actions button.pw-edit  { background: rgba(176,138,58,0.95); }
+    .pw-card-actions button.pw-edit:hover { background: #8a6a2a; }
+    .pw-pitch-chip {
+      display: inline-block; padding: 1px 6px; margin-left: 4px;
+      background: #eef0ee; color: #2a4a2a; border-radius: 999px;
+      font-size: 10px; font-weight: 600; letter-spacing:.04em;
+    }
+
+    /* ── Pattern editor modal ────────────────────────────────────────────── */
+    .pw-editor { position: fixed; inset: 0; z-index: 9000; display: flex;
+      align-items: center; justify-content: center; }
+    .pw-editor[hidden] { display: none; }
+    .pw-editor__backdrop { position: absolute; inset: 0; background: rgba(0,0,0,0.5); cursor: pointer; }
+    .pw-editor__sheet {
+      position: relative; width: 92vw; max-width: 880px; max-height: 92vh; overflow: auto;
+      background: #fdfaf3; border-radius: 12px; border: 1px solid #d8d0c0;
+      box-shadow: 0 24px 60px rgba(0,0,0,0.25);
+    }
+    .pw-editor__head { display: flex; align-items: center; justify-content: space-between;
+      padding: 14px 18px; border-bottom: 1px solid #e9e0c8; }
+    .pw-editor__head h3 { margin: 0; font: 500 18px/1.2 ui-serif, Georgia, serif; }
+    .pw-editor__x { background: none; border: 0; font-size: 26px; line-height: 1; cursor: pointer; color: #5a4628; padding: 0 4px; }
+    .pw-editor__body { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; padding: 18px; }
+    @media (max-width: 700px) { .pw-editor__body { grid-template-columns: 1fr; } }
+    .pw-editor__preview { background: #f0eadc; border-radius: 8px; aspect-ratio: 1/1; overflow: hidden; display:flex; align-items:center; justify-content:center; }
+    .pw-editor__preview img { width: 100%; height: 100%; object-fit: cover; display: block; }
+    .pw-editor__form { display: flex; flex-direction: column; gap: 12px; }
+    .pw-fld { display: flex; flex-direction: column; gap: 6px; font-size: 12px; color: #5a4628; }
+    .pw-fld > span { font-size: 10.5px; letter-spacing:.14em; text-transform: uppercase; color: #7a6e5a; }
+    .pw-fld > span strong { font-weight: 700; color: #1a1a1a; letter-spacing: 0; text-transform: none; font-size: 12px; }
+    .pw-fld input[type=text], .pw-fld select { font: inherit; padding: 8px 10px; border: 1px solid #d8d0c0; border-radius: 4px; background: #fff; color: #1a1a1a; }
+    .pw-fld input[type=range] { accent-color: #1a1a1a; }
+    .pw-slots { display: flex; flex-wrap: wrap; gap: 8px; }
+    .pw-slot {
+      width: 42px; height: 42px; border-radius: 50%;
+      border: 2px dashed #c8b88a; background: #fff;
+      display: flex; align-items: center; justify-content: center;
+      cursor: pointer; position: relative;
+      font: 600 9px/1 ui-sans-serif, system-ui, sans-serif; color: #b08a3a;
+      letter-spacing:.06em; text-transform: uppercase;
+    }
+    .pw-slot.filled { border-style: solid; border-color: rgba(0,0,0,0.15); color: transparent; }
+    .pw-slot.drag-over { box-shadow: 0 0 0 3px #b08a3a; border-color: #b08a3a; }
+    .pw-slot input[type=color] {
+      position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; border: 0; padding: 0;
+    }
+    .pw-slot-count { display: flex; align-items: center; gap: 4px; margin-top: 4px; }
+    .pw-slot-count button {
+      width: 26px; height: 26px; border-radius: 50%;
+      border: 1px solid #d8d0c0; background: #fff; cursor: pointer;
+      font: 600 11px/1 ui-sans-serif, system-ui, sans-serif; color: #5a4628;
+    }
+    .pw-slot-count button.active { background: #1a1a1a; color: #fff; border-color: #1a1a1a; }
+    .pw-slot-hint { font-size: 10px; color: #7a6e5a; margin-left: 8px; letter-spacing:.04em; }
+    .pw-editor__actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
+    .pw-btn {
+      padding: 10px 16px; border-radius: 4px; cursor: pointer;
+      font: 600 11px/1 ui-sans-serif, system-ui, sans-serif;
+      letter-spacing:.08em; text-transform: uppercase;
+      background: #fff; border: 1px solid #d8d0c0; color: #1a1a1a;
+    }
+    .pw-btn:hover { background: #f4ede4; }
+    .pw-btn-primary { background: #1a1a1a; color: #fff; border-color: #1a1a1a; }
+    .pw-btn-primary:hover { background: #000; }
+    .pw-btn-ghost { background: transparent; border-color: transparent; color: #7a6e5a; }
+  </style>
+
+  <script>
+  (function () {
+    var root = document.getElementById('pairs-well-with'); if (!root) return;
+    var endpointPairs = root.getAttribute('data-pairs-endpoint');
+    var endpointAi    = root.getAttribute('data-ai-endpoint');
+    var palette       = root.getAttribute('data-source-palette');
+    var sourceTitle   = root.getAttribute('data-source-title') || '';
+    var sourceImage   = root.getAttribute('data-source-image') || '';
+    if (!palette) return;
+    root.hidden = false;
+
+    var cta            = root.querySelector('.pairs-well__cta');
+    var catalogPanel   = document.getElementById('pairs-well-catalog');
+    var catalogGrid    = document.getElementById('pairs-well-grid');
+    var catalogCols    = document.getElementById('pairs-well-cols-catalog');
+    var catalogColsVal = document.getElementById('pairs-well-cols-catalog-val');
+    var catalogCount   = document.getElementById('pairs-well-catalog-count');
+    var aiPanel        = document.getElementById('pairs-well-ai');
+    var aiGrid         = document.getElementById('pairs-well-ai-grid');
+    var aiCols         = document.getElementById('pairs-well-cols-ai');
+    var aiColsVal      = document.getElementById('pairs-well-cols-ai-val');
+    var tileIn         = document.getElementById('pw-tile-in');
+    var tileInVal      = document.getElementById('pw-tile-in-val');
+    var roomIn         = document.getElementById('pw-room-in');
+    var roomInVal      = document.getElementById('pw-room-in-val');
+    var sourcePalette  = document.getElementById('pairs-well-source-palette');
+    var vendorPicker   = document.getElementById('pairs-well-vendor-picker');
+    var vendorChecks   = vendorPicker.querySelectorAll('input[type=checkbox]');
+    var editor         = document.getElementById('pw-editor');
+    var loaded=false, cachedPairs=null, cachedAi=null, customCards=[], cachedSource=null;
+    var IS_ADMIN = ${_isAdmin ? 'true' : 'false'};
+    var VENDOR_LABEL={sw:'Sherwin-Williams',de:'Dunn-Edwards',bm:'Benjamin Moore',fb:'Farrow & Ball',behr:'Behr',ppg:'PPG',ral:'RAL'};
+    var VENDOR_SHORT={sw:'SW',de:'DE',bm:'BM',fb:'F&B',behr:'Behr',ppg:'PPG',ral:'RAL'};
+    var DEFAULT_VENDORS=['sw','de','bm','fb'];
+
+    function selectedVendors(){var o=[];vendorChecks.forEach(function(c){if(c.checked)o.push(c.value)});return o.length?o:DEFAULT_VENDORS.slice()}
+    try{var st=JSON.parse(localStorage.getItem('pw.vendors')||'null');if(Array.isArray(st)&&st.length)vendorChecks.forEach(function(c){c.checked=st.indexOf(c.value)!==-1})}catch(_){}
+    function persistVendors(){try{localStorage.setItem('pw.vendors',JSON.stringify(selectedVendors()))}catch(_){}}
+    vendorChecks.forEach(function(c){c.addEventListener('change',function(){persistVendors();if(cachedSource)renderSourcePalette(cachedSource.palette);if(cachedPairs&&!catalogPanel.hidden)renderGrid(catalogGrid,sortPairs(cachedPairs,currentSort),false)})});
+
+    function wireCols(input,valEl,varName,storageKey){function apply(){var n=Math.max(3,Math.min(12,parseInt(input.value,10)||8));valEl.textContent=n;root.style.setProperty('--'+varName,n);try{localStorage.setItem(storageKey,n)}catch(_){}}try{var s=parseInt(localStorage.getItem(storageKey),10);if(!isNaN(s)&&s>=3&&s<=12)input.value=s}catch(_){}input.addEventListener('input',apply);apply()}
+    wireCols(catalogCols,catalogColsVal,'cols-catalog','pw.cols.catalog');
+    wireCols(aiCols,aiColsVal,'cols-ai','pw.cols.ai');
+
+    // Repeat-width + Room-scale sliders. tile_in re-fetches the AI grid.
+    var aiRefetchTimer = null;
+    function wireInchSlider(input, valEl, storageKey, onChange) {
+      try { var s = parseFloat(localStorage.getItem(storageKey)); if (!isNaN(s)) input.value = s; } catch (_) {}
+      function apply() {
+        valEl.textContent = input.value;
+        try { localStorage.setItem(storageKey, input.value); } catch (_) {}
+        if (onChange) onChange(input.value);
+      }
+      input.addEventListener('input', apply);
+      apply();
+    }
+    wireInchSlider(tileIn, tileInVal, 'pw.tile_in', function () {
+      // Debounce re-fetch by 300ms so dragging doesn't hammer the API.
+      clearTimeout(aiRefetchTimer);
+      aiRefetchTimer = setTimeout(refetchAi, 300);
+    });
+    wireInchSlider(roomIn, roomInVal, 'pw.room_in', function (val) {
+      // Room scale only affects display sizing — set a CSS var the cards can use later.
+      root.style.setProperty('--pw-room-in', val);
+    });
+
+    function esc(s){return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;')}
+    function renderSkeletons(g,n){var h='';for(var i=0;i<n;i++)h+='<div class="pairs-well__skeleton"></div>';g.innerHTML=h}
+
+    function renderSourcePalette(pal){if(!pal||!pal.length){sourcePalette.hidden=true;vendorPicker.hidden=true;return}var vs=selectedVendors();var head='<th></th>'+vs.map(function(v){return '<th>'+esc(VENDOR_LABEL[v])+'</th>'}).join('');var body=pal.map(function(c){var hex=typeof c==='string'?c:c.hex;var cells='<td class="swatch"><span class="pw-dot" style="background:'+esc(hex)+'"></span></td>';vs.forEach(function(v){var p=c&&c.paints&&c.paints[v];cells+='<td>'+(p?'<span class="pw-square" style="background:'+esc(p.hex)+'"></span>'+esc(p.name)+' <span class="pw-code">'+esc(p.code)+'</span>':'—')+'</td>'});return '<tr>'+cells+'</tr>'}).join('');sourcePalette.innerHTML='<h4>Color story</h4><table><thead><tr>'+head+'</tr></thead><tbody>'+body+'</tbody></table>';sourcePalette.hidden=false;vendorPicker.hidden=false}
+
+    // ── Card renderer — AI cards get a pitch chip + Duplicate/Edit buttons.
+    function renderCard(p, isAi, idx) {
+      var img = p.image_url ? esc(p.image_url) : '';
+      var why = (p.why && p.why[0]) ? esc(p.why[0]) : (p.motif ? 'AI ' + esc(p.motif) : '');
+      var pitchChip = (isAi && p.pitch_in) ? '<span class="pw-pitch-chip">'+p.pitch_in+'″</span>' : '';
+      var pal = (p.palette||[]).map(function(c){var hex=typeof c==='string'?c:c.hex;return '<span class="pw-dot" style="background:'+esc(hex)+'" title="'+esc(hex)+'"></span>'}).join('');
+      var dom = (p.palette && p.palette[0]) || null;
+      var pb = '';
+      if (dom && dom.paints) {
+        var vs = selectedVendors();
+        // Build the paint rows as a 2-column table (Brand · Color) — no per-row
+        // pill label, since the table header already says it. Hex squares stay
+        // so the table is scannable. Wrapped in <details> so the card surface
+        // stays minimal on load — user opens the table when they want it.
+        var rowsTbl = vs.map(function(v){
+          var pt = dom.paints[v]; if (!pt) return '';
+          return '<tr><th>'+esc(VENDOR_SHORT[v])+'</th>'
+            + '<td><span class="pw-square" style="background:'+esc(pt.hex)+'"></span>'
+            + esc(pt.name)+' <span class="pw-code">'+esc(pt.code)+'</span></td></tr>';
+        }).filter(Boolean).join('');
+        if (rowsTbl) {
+          pb = '<details class="pairs-well__paints-d">'
+             +   '<summary>Paint matches <span class="pw-d-caret" aria-hidden="true">▾</span></summary>'
+             +   '<table class="pairs-well__paints-tbl"><tbody>'+rowsTbl+'</tbody></table>'
+             + '</details>';
+        }
+      }
+      var href = p.url ? esc(p.url) : (p.handle ? 'https://www.designerwallcoverings.com/products/'+esc(p.handle) : '#');
+      var aiOnly = isAi
+        ? '<div class="pw-card-actions">'
+          + (IS_ADMIN ? '<button type="button" class="pw-edit" data-card-action="edit" data-idx="'+idx+'">✎ Edit</button>' : '')
+          + '<button type="button" class="pw-dup" data-card-action="duplicate" data-idx="'+idx+'">⎘ Duplicate</button>'
+          + '</div>'
+        : '';
+      // For AI cards the href doesn't lead anywhere (placeholder) — use a div instead of an <a>
+      // so the Duplicate/Edit buttons don't trigger navigation when clicked.
+      var Tag = isAi ? 'div' : 'a';
+      var openAttrs = isAi ? '' : ' target="_blank" rel="noopener" href="'+href+'"';
+      return '<'+Tag+' class="pairs-well__card'+(isAi?' pairs-well__card--ai':'')+'"'+openAttrs+' data-card-idx="'+idx+'" title="'+esc((p.why||[]).join(' · '))+'">'
+        + aiOnly
+        + '<div class="pairs-well__img-wrap">'+(img?'<img class="pairs-well__img" src="'+img+'" alt="'+esc(p.title)+'" loading="lazy">':'')+'</div>'
+        + '<div class="pairs-well__meta">'
+        +   '<p class="pairs-well__title">'+esc(p.title||'')+pitchChip+'</p>'
+        +   '<p class="pairs-well__vendor">'+(isAi?'AI-Generated':'Designer Wallcoverings')+'</p>'
+        +   (why ? '<span class="pairs-well__why">'+why+'</span>' : '')
+        +   (pal ? '<div class="pairs-well__palette">'+pal+'</div>' : '')
+        +   pb
+        + '</div>'
+      + '</'+Tag+'>';
+    }
+    function renderGrid(g, items, isAi) {
+      if (!items || !items.length) { g.innerHTML = '<div class="pairs-well__empty">No coordinates found.</div>'; return; }
+      g.innerHTML = items.map(function (p, i) { return renderCard(p, isAi, i); }).join('');
+    }
+
+    // ── Client-side sort for the catalog grid. Server returns match-ranked
+    // order ("Best match"); these sorts re-order the cached array in place.
+    function _hex2hsl(hex) {
+      if (!hex || !/^#?[0-9a-fA-F]{6}$/.test(hex.replace('#',''))) return [0,0,0];
+      var c = hex.replace('#','');
+      var r = parseInt(c.slice(0,2),16)/255, g = parseInt(c.slice(2,4),16)/255, b = parseInt(c.slice(4,6),16)/255;
+      var mx = Math.max(r,g,b), mn = Math.min(r,g,b), d = mx-mn;
+      var l = (mx+mn)/2;
+      var s = d === 0 ? 0 : d/(1-Math.abs(2*l-1));
+      var h = 0;
+      if (d) { if (mx===r) h = ((g-b)/d) % 6; else if (mx===g) h = (b-r)/d + 2; else h = (r-g)/d + 4; }
+      h = (h*60+360) % 360;
+      return [h, s, l];
+    }
+    function _firstHex(p) {
+      if (!p || !p.palette || !p.palette.length) return '#888888';
+      var c = p.palette[0]; return typeof c === 'string' ? c : (c && c.hex) || '#888888';
+    }
+    function sortPairs(items, key) {
+      if (!items || !items.length || !key || key === 'match') return items;
+      var arr = items.slice();
+      switch (key) {
+        case 'title':       return arr.sort(function(a,b){ return (a.title||'').localeCompare(b.title||''); });
+        case 'title-desc':  return arr.sort(function(a,b){ return (b.title||'').localeCompare(a.title||''); });
+        case 'motif':       return arr.sort(function(a,b){ return (a.motif||a.style||'').localeCompare(b.motif||b.style||''); });
+        case 'light-dark':  return arr.sort(function(a,b){ return _hex2hsl(_firstHex(b))[2] - _hex2hsl(_firstHex(a))[2]; });
+        case 'dark-light':  return arr.sort(function(a,b){ return _hex2hsl(_firstHex(a))[2] - _hex2hsl(_firstHex(b))[2]; });
+        case 'color-wheel': return arr.sort(function(a,b){
+          var ha = _hex2hsl(_firstHex(a)), hb = _hex2hsl(_firstHex(b));
+          var ba = Math.floor(ha[0]/30), bb = Math.floor(hb[0]/30);
+          return ba !== bb ? ba-bb : ha[2]-hb[2];
+        });
+        case 'vivid':       return arr.sort(function(a,b){ return _hex2hsl(_firstHex(b))[1] - _hex2hsl(_firstHex(a))[1]; });
+        case 'muted':       return arr.sort(function(a,b){ return _hex2hsl(_firstHex(a))[1] - _hex2hsl(_firstHex(b))[1]; });
+        case 'random': {
+          var seed = Math.floor(Date.now() / 60000);
+          var rand = function(n){ var x = Math.sin(seed * 9301 + n * 49297) * 233280; return x - Math.floor(x); };
+          return arr.map(function(x,i){ return { x: x, k: rand(i) }; }).sort(function(a,b){ return a.k - b.k; }).map(function(o){ return o.x; });
+        }
+        default: return arr;
+      }
+    }
+    // Sort dropdown — persist choice in localStorage (per Steve's standing
+    // rule that sort+density choices survive reload).
+    var sortSel = document.getElementById('pairs-well-sort');
+    var currentSort = 'match';
+    try { var s = localStorage.getItem('pw.sort'); if (s) { currentSort = s; if (sortSel) sortSel.value = s; } } catch (_) {}
+    function applySort() {
+      if (!cachedPairs) return;
+      var sorted = sortPairs(cachedPairs, currentSort);
+      renderGrid(catalogGrid, sorted, false);
+    }
+    if (sortSel) {
+      sortSel.addEventListener('change', function () {
+        currentSort = sortSel.value || 'match';
+        try { localStorage.setItem('pw.sort', currentSort); } catch (_) {}
+        applySort();
+      });
+    }
+    function renderAiGrid() {
+      var all = (customCards || []).concat(cachedAi || []);
+      renderGrid(aiGrid, all, true);
+    }
+
+    function buildUrl(base, limit, opts) {
+      var qs = ['palette=' + encodeURIComponent(palette)];
+      if (sourceTitle) qs.push('title=' + encodeURIComponent(sourceTitle));
+      if (sourceImage) qs.push('image_url=' + encodeURIComponent(sourceImage));
+      if (limit)       qs.push('limit=' + limit);
+      if (opts && opts.tile_in) qs.push('tile_in=' + opts.tile_in);
+      return base + '?' + qs.join('&');
+    }
+
+    function refetchAi() {
+      if (!loaded) return;
+      renderSkeletons(aiGrid, 12);
+      fetch(buildUrl(endpointAi, 24, { tile_in: parseFloat(tileIn.value) }), { credentials: 'omit' })
+        .then(function (r) { return r.ok ? r.json() : Promise.reject(new Error('http ' + r.status)); })
+        .then(function (d) { if (!d || d.ok === false) throw new Error('bad payload'); cachedAi = d.items || []; renderAiGrid(); })
+        .catch(function () { aiGrid.innerHTML = '<div class="pairs-well__empty">AI generator not ready.</div>'; });
+    }
+
+    function load() {
+      if (loaded) return Promise.resolve();
+      loaded = true;
+      renderSkeletons(catalogGrid, 12); renderSkeletons(aiGrid, 12);
+      catalogPanel.hidden = false; aiPanel.hidden = false;
+      var pp = fetch(buildUrl(endpointPairs, 24), { credentials: 'omit' })
+        .then(function (r) { return r.ok ? r.json() : Promise.reject(new Error('http ' + r.status)); })
+        .then(function (d) {
+          if (!d || d.ok === false) throw new Error((d && d.error) || 'bad payload');
+          cachedSource = d.source || null; cachedPairs = d.pairs || [];
+          if (cachedSource) renderSourcePalette(cachedSource.palette || []);
+          renderGrid(catalogGrid, sortPairs(cachedPairs, currentSort), false);
+          catalogCount.textContent = '(' + cachedPairs.length + ')';
+          if (!cachedPairs.length && (!cachedSource || !cachedSource.palette || !cachedSource.palette.length)) root.hidden = true;
+        })
+        .catch(function () { catalogGrid.innerHTML = '<div class="pairs-well__empty">Could not load coordinates right now.</div>'; });
+      var ap = fetch(buildUrl(endpointAi, 24, { tile_in: parseFloat(tileIn.value) }), { credentials: 'omit' })
+        .then(function (r) { return r.ok ? r.json() : Promise.reject(new Error('http ' + r.status)); })
+        .then(function (d) { if (!d || d.ok === false) throw new Error('bad payload'); cachedAi = d.items || []; renderAiGrid(); })
+        .catch(function () { aiGrid.innerHTML = '<div class="pairs-well__empty">AI generator not ready.</div>'; });
+      return Promise.all([pp, ap]);
+    }
+
+    cta.addEventListener('click', function () {
+      var op = cta.getAttribute('aria-expanded') === 'true';
+      if (op) {
+        cta.setAttribute('aria-expanded', 'false');
+        sourcePalette.hidden = true; vendorPicker.hidden = true;
+        catalogPanel.hidden = true;  aiPanel.hidden = true;
+        return;
+      }
+      cta.setAttribute('aria-expanded', 'true');
+      load();
+    });
+
+    // ── Duplicate / Edit button handler on AI cards ─────────────────────────
+    function duplicateName(name) {
+      // "X" → "X (copy)" → "X (copy 2)" → "X (copy 3)"...
+      var m = /^(.*?) \(copy(?: (\d+))?\)$/.exec(name);
+      if (m) {
+        var n = parseInt(m[2] || '1', 10) + 1;
+        return m[1] + ' (copy ' + n + ')';
+      }
+      return name + ' (copy)';
+    }
+    aiGrid.addEventListener('click', function (e) {
+      var btn = e.target.closest('[data-card-action]');
+      if (!btn) return;
+      e.preventDefault(); e.stopPropagation();
+      var idx = parseInt(btn.getAttribute('data-idx'), 10);
+      var all = (customCards || []).concat(cachedAi || []);
+      var src = all[idx]; if (!src) return;
+      if (btn.getAttribute('data-card-action') === 'duplicate') {
+        var clone = JSON.parse(JSON.stringify(src));
+        clone.id    = 'custom-' + Date.now();
+        clone.title = duplicateName(src.title || 'Pattern');
+        clone.custom = true;
+        customCards.unshift(clone);
+        renderAiGrid();
+      } else if (btn.getAttribute('data-card-action') === 'edit') {
+        openEditor(src, idx);
+      }
+    });
+
+    ${_isAdmin ? `
+    // ── Pattern editor (admin only) ─────────────────────────────────────────
+    var editorImg    = document.getElementById('pw-editor-img');
+    var fldLabel     = document.getElementById('pw-fld-label');
+    var fldMotif     = document.getElementById('pw-fld-motif');
+    var fldStyle     = document.getElementById('pw-fld-style');
+    var fldPitch     = document.getElementById('pw-fld-pitch');
+    var fldPitchVal  = document.getElementById('pw-fld-pitch-val');
+    var slotsEl      = document.getElementById('pw-slots');
+    var slotCountEls = editor.querySelectorAll('.pw-slot-count button');
+    var btnSave      = document.getElementById('pw-btn-save');
+    var btnSaveNew   = document.getElementById('pw-btn-savenew');
+    var editingState = null;  // { sourceCard, sourceIdx }
+
+    function renderSlots(colors, n) {
+      slotsEl.innerHTML = '';
+      for (var i = 0; i < n; i++) {
+        var hex = colors[i] || '';
+        var slot = document.createElement('div');
+        slot.className = 'pw-slot' + (hex ? ' filled' : '');
+        slot.setAttribute('data-slot', String(i));
+        slot.style.background = hex || '#fff';
+        if (!hex) slot.textContent = '+';
+        var picker = document.createElement('input');
+        picker.type = 'color'; picker.value = hex || '#888888';
+        picker.addEventListener('input', function (ev) {
+          var s = ev.target.parentNode;
+          var ix = parseInt(s.getAttribute('data-slot'), 10);
+          editingState.colors[ix] = ev.target.value;
+          s.style.background = ev.target.value;
+          s.classList.add('filled');
+          s.textContent = '';
+          previewRender();
+        });
+        slot.appendChild(picker);
+        slotsEl.appendChild(slot);
+      }
+    }
+
+    function activeSlotCount() {
+      var n = 3;
+      slotCountEls.forEach(function (b) { if (b.classList.contains('active')) n = parseInt(b.getAttribute('data-n'), 10); });
+      return n;
+    }
+
+    slotCountEls.forEach(function (b) {
+      b.addEventListener('click', function () {
+        slotCountEls.forEach(function (x) { x.classList.toggle('active', x === b); });
+        var n = parseInt(b.getAttribute('data-n'), 10);
+        if (editingState) {
+          editingState.colors = editingState.colors.slice(0, n);
+          while (editingState.colors.length < n) editingState.colors.push('');
+          renderSlots(editingState.colors, n);
+          previewRender();
+        }
+      });
+    });
+
+    slotsEl.addEventListener('dragover', function (e) {
+      var slot = e.target.closest('.pw-slot');
+      if (slot) { e.preventDefault(); slot.classList.add('drag-over'); e.dataTransfer.dropEffect = 'copy'; }
+    });
+    slotsEl.addEventListener('dragleave', function (e) {
+      var slot = e.target.closest('.pw-slot');
+      if (slot) slot.classList.remove('drag-over');
+    });
+    slotsEl.addEventListener('drop', function (e) {
+      var slot = e.target.closest('.pw-slot'); if (!slot) return;
+      e.preventDefault(); slot.classList.remove('drag-over');
+      var hex = e.dataTransfer.getData('application/x-wallco-color') || e.dataTransfer.getData('text/plain');
+      if (!hex || !/^#?[0-9a-fA-F]{3,8}$/.test(hex.replace(/[^#0-9a-fA-F]/g,''))) return;
+      if (!hex.startsWith('#')) hex = '#' + hex;
+      var ix = parseInt(slot.getAttribute('data-slot'), 10);
+      editingState.colors[ix] = hex;
+      slot.style.background = hex;
+      slot.classList.add('filled');
+      slot.textContent = '';
+      var picker = slot.querySelector('input[type=color]'); if (picker) picker.value = hex;
+      previewRender();
+    });
+
+    fldPitch.addEventListener('input', function () { fldPitchVal.textContent = fldPitch.value + '″'; previewRender(); });
+    fldMotif.addEventListener('change', previewRender);
+    fldStyle.addEventListener('change', previewRender);
+
+    var previewTimer = null;
+    function previewRender() {
+      if (!editingState) return;
+      clearTimeout(previewTimer);
+      previewTimer = setTimeout(function () {
+        var body = {
+          motif: fldMotif.value,
+          style: fldStyle.value,
+          pitch_in: parseFloat(fldPitch.value),
+          colors: editingState.colors.filter(Boolean),
+          tile_in: parseFloat(tileIn.value),
+          label: fldLabel.value
+        };
+        fetch(endpointAi.replace('/ai-coordinates', '/ai-coordinates/render'), {
+          method: 'POST', credentials: 'omit',
+          headers: { 'Content-Type': 'application/json' },
+          body: JSON.stringify(body)
+        }).then(function (r) { return r.ok ? r.json() : null; })
+          .then(function (d) {
+            if (!d || !d.image_url) return;
+            editorImg.src = d.image_url;
+            editingState.lastRender = d;
+          })
+          .catch(function () {});
+      }, 180);
+    }
+
+    function openEditor(card, idx) {
+      var colors = (card.colors && card.colors.length ? card.colors : (card.palette || [])).slice(0, 5);
+      while (colors.length < 3) colors.push('');
+      editingState = { sourceCard: card, sourceIdx: idx, colors: colors.slice() };
+      fldLabel.value = card.title || '';
+      fldMotif.value = (card.motif === 'tartan') ? 'tartan' : ((card.motif === 'plaid') ? 'plaid' : 'stripe');
+      fldStyle.value = card.style || 'even';
+      fldPitch.value = card.pitch_in || 6;
+      fldPitchVal.textContent = fldPitch.value + '″';
+      // pick active slot-count button
+      var n = colors.length || 3;
+      slotCountEls.forEach(function (b) { b.classList.toggle('active', parseInt(b.getAttribute('data-n'), 10) === n); });
+      renderSlots(editingState.colors, n);
+      editorImg.src = card.image_url || '';
+      editor.hidden = false;
+      document.body.style.overflow = 'hidden';
+    }
+    function closeEditor() { editor.hidden = true; editingState = null; document.body.style.overflow = ''; }
+
+    editor.addEventListener('click', function (e) { if (e.target.getAttribute('data-close') === '1') closeEditor(); });
+    document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && !editor.hidden) closeEditor(); });
+
+    btnSave.addEventListener('click', function () {
+      if (!editingState || !editingState.lastRender) return;
+      var src = editingState.sourceCard;
+      src.motif    = editingState.lastRender.motif;
+      src.style    = editingState.lastRender.style;
+      src.pitch_in = editingState.lastRender.pitch_in;
+      src.colors   = editingState.lastRender.colors;
+      src.palette  = editingState.lastRender.colors;
+      src.image_url = editingState.lastRender.image_url;
+      src.title    = fldLabel.value || src.title;
+      src.why      = [src.style + ' · ' + src.pitch_in + '″'];
+      renderAiGrid();
+      closeEditor();
+    });
+    btnSaveNew.addEventListener('click', function () {
+      if (!editingState || !editingState.lastRender) return;
+      var card = {
+        id: 'custom-' + Date.now(), motif: editingState.lastRender.motif,
+        style: editingState.lastRender.style, pitch_in: editingState.lastRender.pitch_in,
+        colors: editingState.lastRender.colors, palette: editingState.lastRender.colors,
+        image_url: editingState.lastRender.image_url,
+        title: (fldLabel.value || (editingState.sourceCard.title || 'Custom')) + (fldLabel.value ? '' : ' (custom)'),
+        why: [editingState.lastRender.style + ' · ' + editingState.lastRender.pitch_in + '″'],
+        custom: true
+      };
+      customCards.unshift(card);
+      renderAiGrid();
+      closeEditor();
+    });
+    ` : ''}
+  })();
+  </script>
+
+  <!-- Generated Rooms gallery — hydrates from /api/rooms?design_id={{id}} -->
+  <section id="rooms-gallery" class="rooms-gallery" data-design-id="${design.id}" style="margin:48px auto 0;max-width:1200px;padding:0 24px;">
+    <div class="rg-head" style="display:flex;align-items:baseline;gap:16px;border-top:1px solid var(--rule,#e5e5e0);padding-top:24px;margin-bottom:18px;flex-wrap:wrap;">
+      <h2 style="margin:0;font:500 22px/1.2 ui-serif,Georgia,serif;">Generated rooms</h2>
+      <span id="rg-count" style="font:11px/1.4 ui-sans-serif,system-ui,sans-serif;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-faint,#6e6e68);"></span>
+      <span id="rg-admin-badge" style="display:none;margin-left:auto;font:10px/1.4 ui-sans-serif,system-ui,sans-serif;letter-spacing:.14em;text-transform:uppercase;color:#b00020;">admin mode</span>
+    </div>
+    ${_isAdmin ? `
+    <!-- See in Room generator — admin-only because it hits the upstream
+         renderer at ROOM_API ($/call). Triggers POST /api/room and dispatches
+         wallco:room-generated so the gallery below refreshes in place. -->
+    <div id="rg-gen-bar" style="display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-bottom:18px;padding:12px 14px;background:var(--card-bg,#f4f0e8);border:1px solid var(--rule,#e5e5e0);border-radius:8px;font:13px ui-sans-serif,system-ui,sans-serif;">
+      <strong style="font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-faint,#7a6e5a)">See in room</strong>
+      <label style="display:flex;align-items:center;gap:6px;font-size:11px;color:var(--ink-faint,#7a6e5a);">
+        Room
+        <select id="rg-room-type" style="font:12px ui-sans-serif,system-ui,sans-serif;padding:4px 8px;border:1px solid var(--line,#d8d0c0);background:#fff;border-radius:4px;">
+          <option value="bedroom">Bedroom</option>
+          <option value="living_room">Living room</option>
+          <option value="dining_room">Dining room</option>
+          <option value="office">Office</option>
+          <option value="bathroom">Bathroom</option>
+        </select>
+      </label>
+      <label style="display:flex;align-items:center;gap:6px;font-size:11px;color:var(--ink-faint,#7a6e5a);">
+        Angle
+        <select id="rg-angle" style="font:12px ui-sans-serif,system-ui,sans-serif;padding:4px 8px;border:1px solid var(--line,#d8d0c0);background:#fff;border-radius:4px;">
+          <option value="straight_on">Straight on</option>
+          <option value="three_quarter">3/4 view</option>
+          <option value="wide">Wide</option>
+        </select>
+      </label>
+      <button id="rg-generate-btn" type="button"
+        style="background:#1f1808;border:1px solid #1f1808;color:#fff;padding:6px 14px;border-radius:14px;cursor:pointer;font-size:12px;letter-spacing:.06em;text-transform:uppercase;">
+        Generate Room
+      </button>
+      <span id="rg-generate-status" style="font-size:11px;color:var(--ink-faint,#888);margin-left:auto;"></span>
+    </div>
+    <script>
+    (function(){
+      var btn = document.getElementById('rg-generate-btn');
+      var statusEl = document.getElementById('rg-generate-status');
+      var roomTypeSel = document.getElementById('rg-room-type');
+      var angleSel = document.getElementById('rg-angle');
+      var designId = document.getElementById('rooms-gallery').dataset.designId;
+      btn.addEventListener('click', function(){
+        btn.disabled = true;
+        var originalLabel = btn.textContent;
+        btn.textContent = 'Generating…';
+        statusEl.textContent = 'rendering room (15-30s)…';
+        fetch('/api/room', {
+          method: 'POST',
+          headers: { 'Content-Type': 'application/json' },
+          credentials: 'same-origin',
+          body: JSON.stringify({
+            design_id: parseInt(designId, 10),
+            roomType: roomTypeSel.value,
+            angle: angleSel.value
+          })
+        }).then(function(r){
+          return r.json().then(function(j){ return { ok: r.ok, j: j }; });
+        }).then(function(wrap){
+          if (!wrap.ok || wrap.j.ok === false) {
+            throw new Error((wrap.j && wrap.j.error) || ('http ' + (wrap.j.status || '?')));
+          }
+          statusEl.textContent = 'saved · refreshing gallery';
+          window.dispatchEvent(new CustomEvent('wallco:room-generated'));
+          setTimeout(function(){ statusEl.textContent = ''; }, 2000);
+        }).catch(function(e){
+          statusEl.textContent = 'error: ' + e.message;
+        }).then(function(){
+          btn.disabled = false;
+          btn.textContent = originalLabel;
+        });
+      });
+    })();
+    </script>
+    ` : ''}
+    <div id="rg-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:14px;"></div>
+    <div id="rg-empty" style="display:none;padding:32px;text-align:center;color:var(--ink-faint,#6e6e68);font-style:italic;">${_isAdmin ? 'No rooms generated for this pattern yet. Use the "Generate Room" button above to create one.' : 'No rooms generated for this pattern yet.'}</div>
+  </section>
+</main>
+${FOOTER}
+
+<script>
+(function(){
+  const sec = document.getElementById('rooms-gallery');
+  if (!sec) return;
+  const designId = sec.dataset.designId;
+  const params = new URLSearchParams(window.location.search);
+  let adminToken = params.get('admin') || localStorage.getItem('wallco_admin_token') || '';
+  if (params.get('admin')) localStorage.setItem('wallco_admin_token', adminToken);
+  if (adminToken) document.getElementById('rg-admin-badge').style.display = 'block';
+
+  async function refresh() {
+    const q = new URLSearchParams({ design_id: designId });
+    if (adminToken) { q.set('admin', adminToken); q.set('include_removed', '1'); }
+    let data;
+    try { data = await (await fetch('/api/rooms?' + q.toString())).json(); }
+    catch (e) { document.getElementById('rg-grid').innerHTML = '<div style="color:#b00020">' + e.message + '</div>'; return; }
+    const rooms = data.rooms || [];
+    const isAdmin = !!data.admin;
+    document.getElementById('rg-count').textContent = rooms.filter(r => !r.removed_at).length + ' saved';
+    document.getElementById('rg-empty').style.display = rooms.length ? 'none' : 'block';
+    document.getElementById('rg-grid').innerHTML = rooms.map(function(r){
+      const removed = !!r.removed_at;
+      const marketing = !!r.selected_for_marketing;
+      const tag = r.room_type ? r.room_type.replace(/_/g,' ') : '';
+      const dim = removed ? 'opacity:0.35;filter:grayscale(0.7)' : '';
+      const star = marketing ? '★' : '☆';
+      const adminCtrls = isAdmin
+        ? '<div style="position:absolute;top:6px;right:6px;display:flex;gap:4px;">' +
+            '<button data-id="' + r.id + '" data-act="marketing" title="Toggle marketing" style="background:rgba(0,0,0,0.75);color:#fff;border:0;border-radius:3px;padding:3px 7px;font-size:13px;cursor:pointer;">' + star + '</button>' +
+            (removed
+              ? '<button data-id="' + r.id + '" data-act="restore" title="Restore" style="background:rgba(0,0,0,0.75);color:#fff;border:0;border-radius:3px;padding:3px 7px;font-size:11px;cursor:pointer;">↺</button>'
+              : '<button data-id="' + r.id + '" data-act="remove" title="Remove" style="background:rgba(176,0,32,0.85);color:#fff;border:0;border-radius:3px;padding:3px 7px;font-size:11px;cursor:pointer;">×</button>'
+            ) +
+          '</div>'
+        : '';
+      return '<figure style="margin:0;background:var(--paper,#faf8f2);border:1px solid var(--rule,#e5e5e0);position:relative;' + dim + '">' +
+        '<a href="' + r.image_path + '" target="_blank" style="display:block;aspect-ratio:1/1;overflow:hidden;background:#000;" rel="noopener noreferrer">' +
+          '<img src="' + r.image_path + '" alt="Room with this pattern — ' + tag + '" loading="lazy" style="width:100%;height:100%;object-fit:cover;display:block;" />' +
+        '</a>' +
+        adminCtrls +
+        '<figcaption style="padding:8px 10px 10px;font:11px/1.4 ui-sans-serif,system-ui,sans-serif;color:var(--ink-faint,#6e6e68);display:flex;justify-content:space-between;gap:8px;">' +
+          '<span>' + tag + (marketing ? ' · <strong style="color:#9c7100;">marketing</strong>' : '') + '</span>' +
+          '<time>' + new Date(r.created_at).toLocaleDateString('en-US',{month:'short',day:'numeric'}) + '</time>' +
+        '</figcaption>' +
+      '</figure>';
+    }).join('');
+
+    if (isAdmin) {
+      document.querySelectorAll('#rg-grid button[data-act]').forEach(function(btn){
+        btn.addEventListener('click', async function(){
+          const id = btn.dataset.id, act = btn.dataset.act;
+          const headers = { 'Content-Type': 'application/json', 'X-Admin-Token': adminToken };
+          let url, body = '{}';
+          if (act === 'remove') {
+            if (!confirm('Remove this room? (soft-delete — recoverable)')) return;
+            url = '/api/rooms/' + id + '/remove';
+          } else if (act === 'restore') {
+            url = '/api/rooms/' + id + '/restore';
+          } else if (act === 'marketing') {
+            const fig = btn.closest('figure');
+            const wasStar = btn.textContent.trim() === '★';
+            url = '/api/rooms/' + id + '/marketing';
+            body = JSON.stringify({ selected: !wasStar });
+          }
+          try {
+            const r = await fetch(url, { method: 'POST', headers: headers, body: body });
+            const j = await r.json();
+            if (!j.ok) alert('failed: ' + (j.error||'unknown'));
+            refresh();
+          } catch (e) { alert(e.message); }
+        });
+      });
+    }
+  }
+  refresh();
+  window.addEventListener('wallco:room-generated', refresh);
+})();
+</script>
+
+<script type="application/ld+json">${schema}</script>
+${HAMBURGER_JS}
+
+<!-- ── Real-world equivalents — Surface B from the library spec.
+     Lazy-fetches /api/design/${design.id}/library-equivalents and appends a
+     "Pairs well with real designer wallpapers" row to the page. Vendor names
+     are NEVER shown — cards say "Designer Wallcoverings · <SKU>". -->
+<section id="rw-equivalents-section" style="max-width:1200px;margin:48px auto 0;padding:32px 24px;border-top:1px solid var(--line,#e6dfd0);display:none">
+  <div style="margin-bottom:20px">
+    <p style="font:11px/1 var(--sans);letter-spacing:.28em;text-transform:uppercase;color:#8a6f44;margin:0 0 8px">From the designer library</p>
+    <h2 style="font-family:'Cormorant Garamond','Playfair Display',serif;font-weight:300;font-size:clamp(28px,3vw,40px);margin:0 0 6px;color:var(--ink,#1f1808)">Pairs well with real designer wallpapers</h2>
+    <p style="font:13px var(--sans);color:#7a6a52;margin:0">Hand-picked from our curated archive of designer wallcoverings — the closest matches to this pattern's palette.</p>
+  </div>
+  <div id="rw-eq-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:14px"></div>
+</section>
+<script>
+(function(){
+  fetch('/api/design/${design.id}/library-equivalents?n=5').then(function(r){return r.json();}).then(function(j){
+    if (!j || !j.ok || !j.equivalents || j.equivalents.length === 0) return;
+    var sec = document.getElementById('rw-equivalents-section');
+    var grid = document.getElementById('rw-eq-grid');
+    if (!sec || !grid) return;
+    grid.innerHTML = j.equivalents.map(function(e){
+      var sw = (e.palette||[]).slice(0,4).map(function(h){
+        return '<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:'+h+';margin-right:2px;border:1px solid rgba(0,0,0,.08)"></span>';
+      }).join('');
+      var safeTitle = String(e.title||'').replace(/[<>"]/g,'');
+      return '<a href="/library/'+e.id+'" style="display:block;color:inherit;text-decoration:none;background:#faf8f3;border:1px solid #e6dfd0;border-radius:6px;overflow:hidden;transition:transform .18s,box-shadow .18s" onmouseover="this.style.transform=\\'translateY(-2px)\\';this.style.boxShadow=\\'0 8px 20px rgba(0,0,0,.08)\\'" onmouseout="this.style.transform=\\'\\';this.style.boxShadow=\\'\\'">' +
+        '<span style="display:block;aspect-ratio:1/1;background:url(\\''+e.image_url+'\\') center 12%/cover"></span>' +
+        '<span style="display:block;padding:8px 10px;font:12px var(--sans,system-ui)">' +
+          '<span style="font-weight:500;line-height:1.2;display:block">'+safeTitle+'</span>' +
+          '<span style="font:10px ui-monospace,monospace;color:#8a7a5e">Designer Wallcoverings · '+String(e.sku||'').replace(/[<>"]/g,'')+'</span><br>' +
+          '<span style="display:inline-block;margin-top:4px">'+sw+'</span>' +
+        '</span></a>';
+    }).join('');
+    sec.style.display = 'block';
+  }).catch(function(){ /* library not mounted — no-op */ });
+})();
+</script>
+</body>
+</html>`);
+});
+
+// ── /design/:id/tearsheet — print-ready single-page PDF for trade use
+// Designers email this to clients or print to PDF. Optimized for 8.5×11 portrait.
+// No header, no hamburger — just hero + room + specs + DW contact + QR-style URL block.
+app.get('/design/:id/tearsheet', (req, res) => {
+  const id = parseInt(req.params.id, 10);
+  if (!Number.isFinite(id) || id < 1) return res.status(404).send('Not found');
+  let design = DESIGNS.find(d => d.id === id);
+  if (!design) return res.status(404).send('Design not found');
+  const roomKey = (design.room_mockups || []).includes('living_room') ? 'living_room'
+                : (design.room_mockups || []).includes('bedroom') ? 'bedroom'
+                : (design.room_mockups || []).includes('office')  ? 'office'
+                : (design.room_mockups || [])[0];
+  const roomImg = roomKey ? `/designs/room/design_${design.id}_${roomKey}.png` : null;
+  const motifs = (design.motifs || []).slice(0, 6).join(' · ');
+  const created = design.created_at ? new Date(design.created_at).toISOString().slice(0,10) : '';
+  const designUrl = `https://wallco.ai/design/${design.id}`;
+  const esc = (s) => String(s||'').replace(/[<>&"]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[c]));
+  // Per-design immutable doc — safe to cache 24h. Override the global no-store HTML rule.
+  // 304 path: ETag from design.created_at (falls back to today's date if missing).
+  const tsLM = design.created_at ? new Date(design.created_at) : new Date();
+  if (maybe304(req, res, tsLM)) return;
+  res.setHeader('Cache-Control', 'public, max-age=86400, must-revalidate');
+  res.type('html').send(`<!doctype html><html lang="en"><head>
+<meta charset="utf-8"><title>${esc(design.title)} — Tear Sheet | wallco.ai</title>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<meta name="robots" content="noindex,nofollow">
+<style>
+  @page { size: letter portrait; margin: 0.5in; }
+  * { box-sizing: border-box; }
+  body { font-family: 'Georgia', 'Times New Roman', serif; color:#1a1a1a; margin:0; padding:32px; background:#fff; max-width:8.5in; margin-inline:auto; }
+  .ts-header { display:flex; justify-content:space-between; align-items:baseline; border-bottom:2px solid #1a1a1a; padding-bottom:10px; margin-bottom:18px; }
+  .ts-brand { font:14px/1 -apple-system, system-ui, sans-serif; letter-spacing:.12em; text-transform:uppercase; font-weight:600; }
+  .ts-tag { font:11px/1 -apple-system, system-ui, sans-serif; color:#666; letter-spacing:.04em; }
+  .ts-title { font:300 38px/1.1 Georgia, serif; margin:0 0 6px 0; }
+  .ts-sub { font:13px/1.4 -apple-system, system-ui, sans-serif; color:#555; margin-bottom:18px; }
+  .ts-hero { display:grid; grid-template-columns: 1fr 1fr; gap:14px; margin-bottom:18px; }
+  .ts-hero .swatch, .ts-hero .room { aspect-ratio: 1/1; background:#f4f1ea center/cover no-repeat; border:1px solid #d8d2c4; }
+  .ts-specs { display:grid; grid-template-columns: 130px 1fr; gap:6px 14px; font:12px/1.5 -apple-system, system-ui, sans-serif; margin-bottom:16px; }
+  .ts-specs dt { color:#666; text-transform:uppercase; letter-spacing:.06em; font-size:10px; padding-top:2px; }
+  .ts-specs dd { margin:0; color:#1a1a1a; font-weight:500; }
+  .ts-colorblock { display:flex; align-items:center; gap:10px; padding:10px 14px; background:#faf8f3; border:1px solid #ebe5d6; border-radius:4px; margin-bottom:18px; font:12px -apple-system, system-ui, sans-serif; }
+  .ts-colorchip { width:32px; height:32px; border-radius:50%; border:1px solid rgba(0,0,0,.15); flex-shrink:0; }
+  .ts-section { font:11px/1 -apple-system, system-ui, sans-serif; text-transform:uppercase; letter-spacing:.1em; color:#888; margin:14px 0 6px; }
+  .ts-footer { margin-top:24px; padding-top:14px; border-top:1px solid #ddd; display:grid; grid-template-columns: 2fr 1fr; gap:20px; font:11px/1.5 -apple-system, system-ui, sans-serif; color:#444; }
+  .ts-footer strong { color:#1a1a1a; }
+  .ts-url { font:11px/1.4 ui-monospace, Menlo, monospace; color:#1a1a1a; word-break: break-all; }
+  .no-print { margin: 18px 0; }
+  .no-print button { padding:10px 18px; font:13px -apple-system, system-ui, sans-serif; background:#1a1a1a; color:#fff; border:0; border-radius:4px; cursor:pointer; margin-right:8px; }
+  .no-print a { padding:10px 18px; font:13px -apple-system, system-ui, sans-serif; background:#fff; color:#1a1a1a; text-decoration:none; border:1px solid #1a1a1a; border-radius:4px; display:inline-block; }
+  @media print { .no-print { display:none; } body { padding:0; } }
+</style></head><body>
+<div class="no-print">
+  <button onclick="window.print()">Print or Save as PDF</button>
+  <a href="/design/${design.id}">← Back to design</a>
+</div>
+<div class="ts-header">
+  <div class="ts-brand">wallco.ai · Designer Wallcoverings</div>
+  <div class="ts-tag">Tear Sheet · ${created}</div>
+</div>
+<h1 class="ts-title">${esc(design.title)}</h1>
+<div class="ts-sub">Design #${design.id} · ${esc(design.category || 'mixed')}${motifs ? ' · ' + esc(motifs) : ''}</div>
+
+<div class="ts-hero">
+  <div class="swatch" style="background-image:url('${esc(design.image_url)}')" aria-label="${esc(design.title)} pattern"></div>
+  ${roomImg
+    ? `<div class="room" style="background-image:url('${esc(roomImg)}')" aria-label="In-room preview"></div>`
+    : `<div class="room" style="display:flex;align-items:center;justify-content:center;font:11px -apple-system,system-ui,sans-serif;color:#888;text-align:center;padding:20px">Room preview rendered on request</div>`
+  }
+</div>
+
+<div class="ts-colorblock">
+  <div class="ts-colorchip" style="background:${esc(design.dominant_hex || '#888')}"></div>
+  <div><strong>Dominant color:</strong> ${esc(design.dominant_hex || 'n/a')} · This is the design's primary palette anchor; complementary tones are derived from the full image extraction.</div>
+</div>
+
+<div class="ts-section">Pattern Specs</div>
+<dl class="ts-specs">
+  <dt>SKU / Handle</dt><dd>${esc(design.handle || 'wallco-' + String(design.id).padStart(4,'0'))}</dd>
+  <dt>Category</dt><dd>${esc(design.category || 'mixed')}</dd>
+  ${motifs ? `<dt>Motifs</dt><dd>${esc(motifs)}</dd>` : ''}
+  <dt>Tile Size</dt><dd>24×24 in seamless · 150 DPI</dd>
+  <dt>Repeat</dt><dd>24" horizontal · 24" vertical (full drop)</dd>
+  <dt>Format</dt><dd>AI-original · seamless tile · scalable to any wall</dd>
+  <dt>Substrate</dt><dd>Type II vinyl · nonwoven paper · peel-and-stick · canvas mural</dd>
+  <dt>Fire Rating</dt><dd>Class A available (Type II vinyl substrate, ASTM E84)</dd>
+  <dt>Cleanability</dt><dd>Scrubbable (vinyl) · Spongeable (paper)</dd>
+  <dt>Lead Time</dt><dd>Sample: 3–5 business days · Production: ~2 weeks</dd>
+  <dt>Sample Size</dt><dd>8.5 × 11 in</dd>
+</dl>
+
+<div class="ts-section">Direct URL</div>
+<div class="ts-url">${esc(designUrl)}</div>
+
+<div class="ts-footer">
+  <div>
+    <strong>Designer Wallcoverings</strong><br>
+    15442 Ventura Bl #102, Sherman Oaks CA 91403<br>
+    <strong>info@designerwallcoverings.com</strong> · designerwallcoverings.com<br>
+    AI-original wallpaper, hand-curated. Free trade samples.
+  </div>
+  <div style="text-align:right">
+    Generated ${new Date().toISOString().slice(0,10)}<br>
+    <span style="font:10px ui-monospace, Menlo, monospace; color:#888">${esc(design.handle || 'wallco-'+design.id)}</span>
+  </div>
+</div>
+</body></html>`);
+});
+
+// ── /compare — side-by-side compare up to 4 designs
+// ?ids=72,161,139 (server-rendered) OR uses localStorage[wallco-compare] if ids absent + browser hits with JS.
+app.get('/compare', (req, res) => {
+  const rawIds = String(req.query.ids || '').split(',').map(s => parseInt(s.trim(), 10)).filter(n => Number.isFinite(n) && n > 0).slice(0, 4);
+  const items = rawIds.map(id => DESIGNS.find(d => d.id === id)).filter(Boolean);
+  const esc = (s) => String(s||'').replace(/[<>&"]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[c]));
+  const cols = items.length || 1;
+  const colWidth = items.length ? `${100/items.length}%` : '100%';
+  // View mode: 'specs' (default — side-by-side spec table) or 'grid' (large 2×2 image grid).
+  // ?view=grid or legacy ?grid=1 enables grid mode.
+  const view = (String(req.query.view || '').toLowerCase() === 'grid' || req.query.grid === '1') ? 'grid' : 'specs';
+  const idsQS = rawIds.join(',');
+  // Deterministic ETag — page output depends only on (catalog state, view, ids in order).
+  // Lets browsers/CDNs skip re-rendering when the comparison hasn't changed (the IDs +
+  // view are stable while a designer drags the link around / re-opens the tab).
+  const _cmpEtag = `"cmp-${CATALOG_LAST_MODIFIED.getTime()}-${view}-${idsQS || 'empty'}"`;
+  res.setHeader('ETag', _cmpEtag);
+  res.setHeader('Cache-Control', 'private, max-age=0, must-revalidate');
+  if (req.headers['if-none-match'] === _cmpEtag) return res.status(304).end();
+  const cellsForRow = (fn) => items.map(d => `<td style="padding:10px 14px;border-top:1px solid var(--line,#eee);font:13px var(--sans,system-ui);width:${colWidth};vertical-align:top">${fn(d)}</td>`).join('');
+  res.type('html').send(`${htmlHead({
+    title: 'Compare Designs — wallco.ai',
+    description: items.length ? `Side-by-side comparison of ${items.length} wallco.ai designs.` : 'Compare wallco.ai designs side-by-side.',
+    canonical: 'https://wallco.ai/compare'
+  })}
+<body>
+${htmlHeader('/compare')}
+<main>
+<section class="catalog-section" style="padding:24px 40px">
+  <div style="display:flex;justify-content:space-between;align-items:baseline;flex-wrap:wrap;gap:14px">
+    <div>
+      <h1 style="font:300 36px/1.1 var(--serif,Georgia,serif);margin:0 0 6px">Compare designs</h1>
+      <p style="font:13px var(--sans,system-ui);color:var(--ink-soft,#555);margin:0 0 18px">${items.length ? `${items.length} of 4 selected.` : 'No designs selected. Use "+ Compare" on any design page to add up to 4.'}</p>
+    </div>
+    ${items.length ? `
+    <div role="tablist" aria-label="Compare view" style="display:inline-flex;border:1px solid var(--line,#ddd);border-radius:999px;overflow:hidden;font:12px var(--sans,system-ui)">
+      <a href="/compare?ids=${idsQS}" role="tab" aria-selected="${view==='specs'?'true':'false'}" style="padding:6px 14px;text-decoration:none;background:${view==='specs'?'var(--ink,#111)':'transparent'};color:${view==='specs'?'var(--bg,#fff)':'var(--ink-soft,#555)'}">Specs</a>
+      <a href="/compare?ids=${idsQS}&view=grid" role="tab" aria-selected="${view==='grid'?'true':'false'}" style="padding:6px 14px;text-decoration:none;background:${view==='grid'?'var(--ink,#111)':'transparent'};color:${view==='grid'?'var(--bg,#fff)':'var(--ink-soft,#555)'}">Image grid</a>
+    </div>` : ''}
+  </div>
+
+  ${items.length === 0 ? `
+    <div id="compare-empty" style="padding:40px;border:1px dashed var(--line,#ccc);border-radius:8px;text-align:center;font:14px var(--sans,system-ui);color:var(--ink-faint,#888)">
+      <p>Visit <a href="/designs" style="color:var(--ink,#111)">/designs</a>, click <strong>+ Compare</strong> on the designs you want to evaluate side-by-side, then return here.</p>
+      <p style="font:12px var(--sans,system-ui);margin-top:14px">Or share a link directly: <code>/compare?ids=72,161</code></p>
+    </div>
+
+    <!-- Discovery rail: random designs to start a compare -->
+    <div style="margin-top:32px">
+      <div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:12px">
+        <h2 style="font:300 22px/1.2 var(--serif,Georgia,serif);margin:0">More designs to explore</h2>
+        <button type="button" id="rail-refresh" style="font:11px var(--sans,system-ui);background:transparent;border:1px solid var(--line,#ddd);color:var(--ink-soft,#555);padding:5px 12px;border-radius:999px;cursor:pointer">↻ Shuffle</button>
+      </div>
+      <div id="compare-rail" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:14px"></div>
+      <p style="font:12px var(--sans,system-ui);color:var(--ink-faint,#999);margin-top:12px;text-align:center">Click any design to open it, then hit <strong>+ Compare</strong> on the PDP.</p>
+    </div>
+    <script>
+      // If localStorage has stored ids, auto-redirect to the URL form so the share-link works.
+      try {
+        var stored = JSON.parse(localStorage.getItem('wallco-compare') || '[]');
+        if (Array.isArray(stored) && stored.length > 0) {
+          window.location.href = '/compare?ids=' + stored.slice(0,4).join(',');
+        }
+      } catch(e) {}
+      (function(){
+        var rail = document.getElementById('compare-rail');
+        var btn = document.getElementById('rail-refresh');
+        if (!rail) return;
+        var esc = function(s){ return String(s||'').replace(/[<>&"]/g, function(c){ return {'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[c]; }); };
+        function load(){
+          rail.innerHTML = '<div style="grid-column:1/-1;padding:40px;text-align:center;color:var(--ink-faint,#999);font:13px var(--sans,system-ui)">Loading…</div>';
+          fetch('/api/designs/random?n=12').then(function(r){ return r.json(); }).then(function(j){
+            if (!j.results || !j.results.length) { rail.innerHTML = '<div style="grid-column:1/-1;padding:40px;text-align:center;color:var(--ink-faint,#999);font:13px var(--sans,system-ui)">No designs available yet.</div>'; return; }
+            rail.innerHTML = j.results.map(function(r){
+              var dot = r.dominant_hex ? '<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:'+esc(r.dominant_hex)+';border:1px solid rgba(0,0,0,.12);vertical-align:middle;margin-right:4px"></span>' : '';
+              return '<a href="'+esc(r.url)+'" style="display:block;text-decoration:none;color:inherit"><div style="aspect-ratio:1/1.15;background:#f4f1ea url('+JSON.stringify(r.image_url).slice(1,-1)+') center/cover;border-radius:6px;border:1px solid var(--line,#e6e2d8);margin-bottom:6px"></div><div style="font:13px var(--sans,system-ui);color:var(--ink,#111);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+esc(r.title || ('Design #'+r.id))+'</div><div style="font:11px var(--sans,system-ui);color:var(--ink-faint,#999);margin-top:2px">'+dot+esc(r.category||'mixed')+'</div></a>';
+            }).join('');
+          }).catch(function(){ rail.innerHTML = '<div style="grid-column:1/-1;padding:40px;text-align:center;color:var(--ink-faint,#999);font:13px var(--sans,system-ui)">Couldn\\'t load — try again.</div>'; });
+        }
+        load();
+        if (btn) btn.addEventListener('click', load);
+      })();
+    </script>
+  ` : `
+    ${view === 'grid' ? `
+    <div class="compare-grid" style="display:grid;grid-template-columns:repeat(${Math.min(items.length, 2)}, 1fr);gap:18px;margin-bottom:24px">
+      ${items.map(d => `
+      <figure style="margin:0;display:flex;flex-direction:column;gap:8px">
+        <a href="/design/${d.id}" style="display:block;aspect-ratio:1;background:#f4f1ea url('${esc(d.image_url)}') center/cover;border-radius:8px;border:1px solid var(--line,#e6e2d8);text-decoration:none"></a>
+        <figcaption style="display:flex;justify-content:space-between;align-items:baseline;gap:8px;flex-wrap:wrap">
+          <div style="min-width:0;flex:1">
+            <div style="font:500 16px/1.3 var(--serif,Georgia,serif);color:var(--ink,#111)">${esc(d.title || ('Design #' + d.id))}</div>
+            <div style="font:11px var(--sans,system-ui);color:var(--ink-faint,#888);margin-top:2px"><span style="display:inline-block;width:9px;height:9px;border-radius:50%;background:${esc(d.dominant_hex||'#888')};border:1px solid rgba(0,0,0,.12);vertical-align:middle;margin-right:4px"></span>${esc(d.category||'mixed')} · <a href="/design/${d.id}/tearsheet" target="_blank" rel="noopener noreferrer" style="color:inherit;text-decoration:underline">tear sheet</a></div>
+          </div>
+          <button type="button" data-rmid="${d.id}" class="compare-rm" style="padding:4px 10px;font:11px var(--sans,system-ui);background:transparent;border:1px solid var(--line,#ddd);color:var(--ink-soft,#555);border-radius:3px;cursor:pointer">Remove</button>
+        </figcaption>
+      </figure>`).join('')}
+    </div>
+    ` : `
+    <table style="width:100%;border-collapse:collapse;table-layout:fixed">
+      <thead><tr>${items.map(d => `<th style="padding:0;width:${colWidth};vertical-align:top">
+        <a href="/design/${d.id}" style="display:block;text-decoration:none;color:inherit">
+          <div style="aspect-ratio:1;background:#f4f1ea url('${esc(d.image_url)}') center/cover;border-radius:6px;border:1px solid var(--line,#ddd);margin-bottom:10px"></div>
+          <div style="font:500 16px/1.3 var(--serif,Georgia,serif);color:var(--ink,#111)">${esc(d.title || 'Design #' + d.id)}</div>
+          <div style="font:11px/1.4 var(--sans,system-ui);color:var(--ink-faint,#888);margin-top:3px"><span style="display:inline-block;width:9px;height:9px;border-radius:50%;background:${esc(d.dominant_hex||'#888')};border:1px solid rgba(0,0,0,.12);vertical-align:middle;margin-right:4px"></span>${esc(d.category || 'mixed')}</div>
+        </a>
+        <button type="button" data-rmid="${d.id}" class="compare-rm" style="margin-top:8px;padding:4px 10px;font:11px var(--sans,system-ui);background:transparent;border:1px solid var(--line,#ddd);color:var(--ink-soft,#555);border-radius:3px;cursor:pointer">Remove</button>
+      </th>`).join('')}</tr></thead>
+      <tbody style="font:13px var(--sans,system-ui)">
+        <tr><td colspan="${cols}" style="padding:14px 0 6px;font:11px var(--sans,system-ui);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-faint,#888);border-bottom:1px solid var(--line,#eee)">Specs</td></tr>
+        <tr>${cellsForRow(d => `<strong>SKU</strong> · ${esc(d.handle || 'wallco-'+String(d.id).padStart(4,'0'))}`)}</tr>
+        <tr>${cellsForRow(d => `<strong>Category</strong> · ${esc(d.category || 'mixed')}`)}</tr>
+        <tr>${cellsForRow(d => `<strong>Motifs</strong> · ${esc((d.motifs||[]).slice(0,5).join(', ') || '—')}`)}</tr>
+        <tr>${cellsForRow(d => `<strong>Color</strong> · <span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${esc(d.dominant_hex||'#888')};border:1px solid rgba(0,0,0,.12);vertical-align:middle;margin-right:4px"></span>${esc(d.dominant_hex || 'n/a')}`)}</tr>
+        <tr>${cellsForRow(d => `<strong>Room previews</strong> · ${(d.room_mockups||[]).length}`)}</tr>
+        <tr>${cellsForRow(d => `<strong>Created</strong> · ${d.created_at ? new Date(d.created_at).toISOString().slice(0,10) : '—'}`)}</tr>
+        <tr>${cellsForRow(_d => `<strong>Tile</strong> · 24×24 in seamless`)}</tr>
+        <tr>${cellsForRow(_d => `<strong>Fire rating</strong> · Class A (Type II vinyl)`)}</tr>
+        <tr><td colspan="${cols}" style="padding:14px 0 6px;font:11px var(--sans,system-ui);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-faint,#888);border-bottom:1px solid var(--line,#eee);padding-top:18px">Actions</td></tr>
+        <tr>${items.map(d => `<td style="padding:10px 14px;border-top:1px solid var(--line,#eee);font:13px var(--sans,system-ui);vertical-align:top">
+          <a href="/design/${d.id}" style="display:inline-block;padding:6px 12px;font:12px var(--sans,system-ui);background:var(--ink,#111);color:var(--bg,#fff);text-decoration:none;border-radius:4px;margin-right:6px">View</a>
+          <a href="/samples?design=${d.id}" style="display:inline-block;padding:6px 12px;font:12px var(--sans,system-ui);background:transparent;color:var(--ink,#111);text-decoration:none;border:1px solid var(--ink,#111);border-radius:4px;margin-right:6px">Sample</a>
+          <a href="/design/${d.id}/tearsheet" target="_blank" rel="noopener noreferrer" style="display:inline-block;padding:6px 12px;font:12px var(--sans,system-ui);background:transparent;color:var(--ink,#111);text-decoration:none;border:1px solid var(--ink,#111);border-radius:4px">Tear sheet</a>
+        </td>`).join('')}</tr>
+      </tbody>
+    </table>
+    `}
+    <div style="margin-top:24px;display:flex;gap:12px;flex-wrap:wrap;align-items:center">
+      <a href="/brief?ids=${rawIds.join(',')}" target="_blank" rel="noopener noreferrer" id="compare-brief" style="padding:10px 18px;font:13px var(--sans,system-ui);background:var(--gold,#c9a14b);color:var(--accent,#0d0d0d);text-decoration:none;border:0;border-radius:4px;font-weight:500" title="Export this comparison as a printable multi-design proposal PDF">Export as Brief →</a>
+      <button type="button" id="compare-print" style="padding:10px 18px;font:13px var(--sans,system-ui);background:var(--ink,#111);color:var(--bg,#fff);border:0;border-radius:4px;cursor:pointer">Print this view</button>
+      <button type="button" id="compare-copy" style="padding:10px 18px;font:13px var(--sans,system-ui);background:transparent;color:var(--ink,#111);border:1px solid var(--ink,#111);border-radius:4px;cursor:pointer">Copy share link</button>
+      <a href="/designs" style="padding:10px 18px;font:13px var(--sans,system-ui);background:transparent;color:var(--ink,#111);text-decoration:none;border:1px solid var(--line,#ddd);border-radius:4px">Browse more designs</a>
+      <details style="margin-left:auto;font:12px var(--sans,system-ui);color:var(--ink-soft,#555)">
+        <summary style="cursor:pointer;padding:6px 10px;border:1px solid var(--line,#ddd);border-radius:4px;list-style:none">Brief options ▾</summary>
+        <div style="margin-top:8px;padding:12px;background:var(--card-bg,#faf8f3);border:1px solid var(--line,#ebe5d6);border-radius:6px;display:grid;gap:8px;min-width:280px">
+          <label>Project name<br><input id="brief-project" type="text" placeholder="e.g. Living Room" style="margin-top:4px;width:100%;padding:6px 8px;font:13px var(--sans);border:1px solid var(--line,#ddd);border-radius:4px"></label>
+          <label>Client name<br><input id="brief-client" type="text" placeholder="e.g. Smith Residence" style="margin-top:4px;width:100%;padding:6px 8px;font:13px var(--sans);border:1px solid var(--line,#ddd);border-radius:4px"></label>
+          <button type="button" id="brief-go" style="padding:8px 14px;background:var(--ink,#111);color:var(--bg,#fff);border:0;border-radius:4px;cursor:pointer;font:13px var(--sans)">Open Brief →</button>
+        </div>
+      </details>
+    </div>
+    <script>
+      (function(){
+        var stored = ${JSON.stringify(rawIds)};
+        var currentView = ${JSON.stringify(view)};
+        try { localStorage.setItem('wallco-compare', JSON.stringify(stored)); } catch(e){}
+        // Persist whichever view we landed on so the next /compare visit reopens it.
+        try { localStorage.setItem('wallco-compare-view', currentView); } catch(e){}
+        // If the URL did NOT specify a view param and a saved preference exists, redirect
+        // to honor it. Only redirects when redirect target differs from current page.
+        try {
+          var url = new URL(location.href);
+          var hasExplicitView = url.searchParams.has('view') || url.searchParams.get('grid') === '1';
+          var saved = localStorage.getItem('wallco-compare-view');
+          if (!hasExplicitView && saved && saved !== currentView && (saved === 'grid' || saved === 'specs')) {
+            if (saved === 'grid') url.searchParams.set('view', 'grid'); else url.searchParams.delete('view');
+            location.replace(url.toString());
+            return; // bail — page about to reload
+          }
+        } catch(e){}
+        // Preserve current view across Remove clicks too — so removing one design while in
+        // grid view stays in grid view.
+        var viewQS = currentView === 'grid' ? '&view=grid' : '';
+        document.querySelectorAll('.compare-rm').forEach(function(btn){
+          btn.addEventListener('click', function(){
+            var rid = parseInt(btn.dataset.rmid,10);
+            var next = stored.filter(function(x){ return x !== rid; });
+            try { localStorage.setItem('wallco-compare', JSON.stringify(next)); } catch(e){}
+            window.location.href = next.length ? '/compare?ids=' + next.join(',') + viewQS : '/compare';
+          });
+        });
+        var pBtn = document.getElementById('compare-print');
+        if (pBtn) pBtn.addEventListener('click', function(){ window.print(); });
+        var cBtn = document.getElementById('compare-copy');
+        if (cBtn) cBtn.addEventListener('click', async function(){
+          var url = location.origin + '/compare?ids=' + stored.join(',');
+          try { await navigator.clipboard.writeText(url); cBtn.textContent = 'Copied!'; setTimeout(function(){ cBtn.textContent='Copy share link'; }, 1800); }
+          catch(e) { window.prompt('Copy:', url); }
+        });
+        // Brief-options details — assembles /brief URL with project+client query params
+        var bGo = document.getElementById('brief-go');
+        if (bGo) bGo.addEventListener('click', function(){
+          var p = (document.getElementById('brief-project').value || '').trim();
+          var c = (document.getElementById('brief-client').value || '').trim();
+          var qs = ['ids=' + stored.join(',')];
+          if (p) qs.push('project=' + encodeURIComponent(p));
+          if (c) qs.push('client=' + encodeURIComponent(c));
+          window.open('/brief?' + qs.join('&'), '_blank', 'noopener');
+        });
+      })();
+    </script>
+  `}
+</section>
+</main>
+${FOOTER}
+${HAMBURGER_JS}
+</body>
+</html>`);
+});
+
+// ─────────────────────────────────────────────────────────────────────────
+// SAMPLE BASKET — mirrors the /compare pattern (localStorage[wallco-basket],
+// max 12 items, additive to the single-design /samples flow). Multi-design
+// trade sample requests get logged to data/sample-requests.jsonl. No email,
+// no DB writes — just append-only JSONL (YOLO + comms-compliance rules).
+// ─────────────────────────────────────────────────────────────────────────
+app.get('/basket', (req, res) => {
+  const rawIds = String(req.query.ids || '').split(',').map(s => parseInt(s.trim(), 10)).filter(n => Number.isFinite(n) && n > 0).slice(0, 12);
+  const items = rawIds.map(id => DESIGNS.find(d => d.id === id)).filter(Boolean);
+  const esc = (s) => String(s||'').replace(/[<>&"]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[c]));
+
+  res.type('html').send(`${htmlHead({
+    title: 'Sample Basket — wallco.ai',
+    description: items.length ? `Request trade samples for ${items.length} wallco.ai designs.` : 'Build a multi-design sample request basket — up to 12 designs, one trade form.',
+    canonical: 'https://wallco.ai/basket'
+  })}
+<body>
+${htmlHeader('/basket')}
+<main>
+<section class="catalog-section" style="padding:24px 40px;max-width:880px;margin:0 auto">
+  <h1 style="font:300 36px/1.1 var(--serif,Georgia,serif);margin:0 0 6px">Sample basket</h1>
+  <p id="basket-count-line" style="font:13px var(--sans,system-ui);color:var(--ink-soft,#555);margin:0 0 18px">${items.length ? `${items.length} of 12 selected.` : 'Your basket is empty.'}</p>
+
+  ${items.length === 0 ? `
+    <div id="basket-empty" style="padding:48px 28px;border:1px dashed var(--line,#ccc);border-radius:8px;text-align:center;color:var(--ink-soft,#555);font:14px/1.6 var(--sans,system-ui);background:var(--card-bg,#faf8f3)">
+      <div style="font-size:36px;line-height:1;margin-bottom:12px">▢</div>
+      <p style="margin:0 0 8px;font:300 22px/1.2 var(--serif,Georgia,serif);color:var(--ink,#111)">Build a sample basket.</p>
+      <p style="margin:0 0 18px;max-width:38em;margin-inline:auto">Memo samples are <strong>free for the trade</strong>, ship in 3–5 business days, and are 8.5 × 11 in — enough to read the pattern in your project's light. Add up to 12, request once.</p>
+      <p style="margin:0"><a href="/designs" class="btn-outline" style="display:inline-block;padding:9px 18px;font:13px var(--sans);text-decoration:none">Browse the catalog →</a></p>
+      <p style="margin:14px 0 0;font:12px var(--sans);color:var(--ink-faint,#999)">Tap <strong>+ Sample</strong> on any design page · or share a basket link directly: <code>/basket?ids=72,161</code></p>
+    </div>
+
+    <!-- Discovery rail: random designs to start a basket -->
+    <div style="margin-top:32px">
+      <div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:12px">
+        <h2 style="font:300 22px/1.2 var(--serif,Georgia,serif);margin:0">More designs to explore</h2>
+        <button type="button" id="rail-refresh" style="font:11px var(--sans,system-ui);background:transparent;border:1px solid var(--line,#ddd);color:var(--ink-soft,#555);padding:5px 12px;border-radius:999px;cursor:pointer">↻ Shuffle</button>
+      </div>
+      <div id="basket-rail" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:14px"></div>
+      <p style="font:12px var(--sans,system-ui);color:var(--ink-faint,#999);margin-top:12px;text-align:center">Click any design to open it, then hit <strong>+ Sample</strong> on the PDP.</p>
+    </div>
+    <script>
+      // If localStorage has stored ids, auto-redirect to the URL form so the share-link works.
+      try {
+        var stored = JSON.parse(localStorage.getItem('wallco-basket') || '[]');
+        if (Array.isArray(stored) && stored.length > 0) {
+          window.location.href = '/basket?ids=' + stored.slice(0,12).join(',');
+        }
+      } catch(e) {}
+      (function(){
+        var rail = document.getElementById('basket-rail');
+        var btn = document.getElementById('rail-refresh');
+        if (!rail) return;
+        var esc = function(s){ return String(s||'').replace(/[<>&"]/g, function(c){ return {'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[c]; }); };
+        function load(){
+          rail.innerHTML = '<div style="grid-column:1/-1;padding:40px;text-align:center;color:var(--ink-faint,#999);font:13px var(--sans,system-ui)">Loading…</div>';
+          fetch('/api/designs/random?n=12').then(function(r){ return r.json(); }).then(function(j){
+            if (!j.results || !j.results.length) { rail.innerHTML = '<div style="grid-column:1/-1;padding:40px;text-align:center;color:var(--ink-faint,#999);font:13px var(--sans,system-ui)">No designs available yet.</div>'; return; }
+            rail.innerHTML = j.results.map(function(r){
+              var dot = r.dominant_hex ? '<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:'+esc(r.dominant_hex)+';border:1px solid rgba(0,0,0,.12);vertical-align:middle;margin-right:4px"></span>' : '';
+              return '<a href="'+esc(r.url)+'" style="display:block;text-decoration:none;color:inherit"><div style="aspect-ratio:1/1.15;background:#f4f1ea url('+JSON.stringify(r.image_url).slice(1,-1)+') center/cover;border-radius:6px;border:1px solid var(--line,#e6e2d8);margin-bottom:6px"></div><div style="font:13px var(--sans,system-ui);color:var(--ink,#111);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+esc(r.title || ('Design #'+r.id))+'</div><div style="font:11px var(--sans,system-ui);color:var(--ink-faint,#999);margin-top:2px">'+dot+esc(r.category||'mixed')+'</div></a>';
+            }).join('');
+          }).catch(function(){ rail.innerHTML = '<div style="grid-column:1/-1;padding:40px;text-align:center;color:var(--ink-faint,#999);font:13px var(--sans,system-ui)">Couldn\\'t load — try again.</div>'; });
+        }
+        load();
+        if (btn) btn.addEventListener('click', load);
+      })();
+    </script>
+  ` : `
+    <!-- Vertically stacked mini-cards (thumb + title + category + Remove) -->
+    <ul id="basket-list" style="list-style:none;margin:0 0 24px;padding:0;display:flex;flex-direction:column;gap:10px">
+      ${items.map(d => `<li data-id="${d.id}" style="display:flex;align-items:center;gap:14px;padding:12px;border:1px solid var(--line,#eee);border-radius:8px;background:var(--card-bg,#faf8f3)">
+        <a href="/design/${d.id}" style="flex:0 0 auto;text-decoration:none">
+          <div style="width:72px;height:72px;background:#f4f1ea url('${esc(d.image_url)}') center/cover;border-radius:6px;border:1px solid var(--line,#ddd)"></div>
+        </a>
+        <div style="flex:1 1 auto;min-width:0">
+          <div style="font:500 16px/1.3 var(--serif,Georgia,serif);color:var(--ink,#111);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
+            <a href="/design/${d.id}" style="color:inherit;text-decoration:none">${esc(d.title || 'Design #' + d.id)}</a>
+          </div>
+          <div style="font:11px/1.4 var(--sans,system-ui);color:var(--ink-faint,#888);margin-top:3px"><span style="display:inline-block;width:9px;height:9px;border-radius:50%;background:${esc(d.dominant_hex||'#888')};border:1px solid rgba(0,0,0,.12);vertical-align:middle;margin-right:4px"></span>${esc(d.category || 'mixed')} · ${esc(d.handle || 'wallco-'+String(d.id).padStart(4,'0'))}</div>
+        </div>
+        <button type="button" data-rmid="${d.id}" class="basket-rm" style="flex:0 0 auto;padding:6px 12px;font:11px var(--sans,system-ui);background:transparent;border:1px solid var(--line,#ddd);color:var(--ink-soft,#555);border-radius:3px;cursor:pointer">Remove</button>
+      </li>`).join('')}
+    </ul>
+
+    <!-- Multi-design sample request form — re-uses /samples fields -->
+    <div class="sample-form-wrap" style="border-top:1px solid var(--line,#eee);padding-top:24px">
+      <h2 style="font:300 24px/1.2 var(--serif,Georgia,serif);margin:0 0 6px">Request samples for these ${items.length} designs</h2>
+      <p style="font:13px var(--sans,system-ui);color:var(--ink-soft,#555);margin:0 0 18px">We'll send physical swatches or high-res digital proofs. No charge, no obligation. Trade welcome.</p>
+      <form id="basket-form" class="sample-form">
+        <label>Your name <input type="text" name="name" required autocomplete="name" enterkeyhint="next" placeholder="Jane Smith"></label>
+        <label>Email <input type="email" name="email" required autocomplete="email" inputmode="email" enterkeyhint="next" placeholder="you@example.com"></label>
+        <label>Company / firm (optional) <input type="text" name="company" autocomplete="organization" enterkeyhint="next" placeholder="Smith Interiors"></label>
+        <label>Phone (optional) <input type="tel" name="phone" autocomplete="tel" inputmode="tel" enterkeyhint="next" placeholder="+1 (555) 000-0000"></label>
+        <label>Project notes (optional) <textarea name="notes" rows="3" enterkeyhint="send" placeholder="Site address, install date, square footage, anything else…"></textarea></label>
+        <button type="submit" class="btn-primary">Send sample request (${items.length} ${items.length === 1 ? 'design' : 'designs'})</button>
+      </form>
+      <div id="basket-result" style="display:none;margin-top:16px;padding:16px;border-radius:4px"></div>
+    </div>
+
+    <script>
+      (function(){
+        var stored = ${JSON.stringify(rawIds)};
+        try { localStorage.setItem('wallco-basket', JSON.stringify(stored)); } catch(e){}
+        document.querySelectorAll('.basket-rm').forEach(function(btn){
+          btn.addEventListener('click', function(){
+            var rid = parseInt(btn.dataset.rmid,10);
+            var next = stored.filter(function(x){ return x !== rid; });
+            try { localStorage.setItem('wallco-basket', JSON.stringify(next)); } catch(e){}
+            try { window.dispatchEvent(new CustomEvent('wallco-basket-changed', { detail: { count: next.length, design_id: rid, in_basket: false } })); } catch(e){}
+            window.location.href = next.length ? '/basket?ids=' + next.join(',') : '/basket';
+          });
+        });
+        var form = document.getElementById('basket-form');
+        var result = document.getElementById('basket-result');
+        if (form) form.addEventListener('submit', async function(e){
+          e.preventDefault();
+          var data = Object.fromEntries(new FormData(form));
+          data.ids = stored;
+          try {
+            var r = await fetch('/api/basket/submit', {
+              method: 'POST',
+              headers: {'Content-Type':'application/json'},
+              body: JSON.stringify(data)
+            });
+            var json = await r.json();
+            result.style.display = 'block';
+            if (json.ok) {
+              result.style.background = '#d4edda';
+              result.style.color = '#155724';
+              result.textContent = 'Thank you! Request ' + json.request_id + ' received. We\\'ll be in touch shortly.';
+              form.reset();
+              // Clear the basket after a successful submit
+              try { localStorage.removeItem('wallco-basket'); } catch(e){}
+            } else {
+              result.style.background = '#f8d7da';
+              result.style.color = '#721c24';
+              result.textContent = (json.error || 'Something went wrong') + ' — please email info@wallco.ai directly.';
+            }
+          } catch(err){
+            result.style.display = 'block';
+            result.style.background = '#f8d7da';
+            result.style.color = '#721c24';
+            result.textContent = 'Network error — please try again.';
+          }
+        });
+      })();
+    </script>
+  `}
+</section>
+</main>
+${FOOTER}
+${HAMBURGER_JS}
+</body>
+</html>`);
+});
+
+// ── POST /api/basket/submit — multi-design sample request → JSONL (no email)
+app.post('/api/basket/submit', (req, res) => {
+  const body = req.body || {};
+  const name    = typeof body.name    === 'string' ? body.name.trim()    : '';
+  const email   = typeof body.email   === 'string' ? body.email.trim()   : '';
+  const company = typeof body.company === 'string' ? body.company.trim() : '';
+  const phone   = typeof body.phone   === 'string' ? body.phone.trim()   : '';
+  const notes   = typeof body.notes   === 'string' ? body.notes.trim()   : '';
+  if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
+    return res.status(400).json({ ok: false, error: 'valid email required' });
+  }
+  const rawIds = Array.isArray(body.ids) ? body.ids : [];
+  const ids = rawIds.map(n => parseInt(n, 10)).filter(n => Number.isFinite(n) && n > 0);
+  const validIds = ids.filter(id => DESIGNS.some(d => d.id === id)).slice(0, 12);
+  if (validIds.length === 0) {
+    return res.status(400).json({ ok: false, error: 'at least one valid design id required' });
+  }
+
+  const ts = Date.now();
+  const request_id = 'sr_' + ts.toString(36) + '_' + Math.random().toString(36).slice(2, 8);
+  const record = {
+    request_id,
+    received_at: new Date(ts).toISOString(),
+    name, email, company, phone, notes,
+    ids: validIds,
+    ip: (req.headers['x-forwarded-for'] || req.ip || '').toString().split(',')[0].trim(),
+    user_agent: (req.headers['user-agent'] || '').toString().slice(0, 300)
+  };
+
+  try {
+    const outPath = path.join(__dirname, 'data', 'sample-requests.jsonl');
+    fs.appendFileSync(outPath, JSON.stringify(record) + '\n', 'utf8');
+  } catch (e) {
+    console.error('basket JSONL append error:', e.message);
+    return res.status(500).json({ ok: false, error: 'failed to record request' });
+  }
+  logEvent(req, 'sample_request_submitted', { request_id, design_count: (record.design_ids || []).length, design_ids: record.design_ids, email: record.email || null });
+
+  res.json({ ok: true, request_id });
+});
+
+// ── /brief?ids=72,161,139 — multi-design proposal/brief PDF (print-friendly)
+// Stacks tearsheets for up to 6 designs with a cover page. Print to PDF and send to a client.
+app.get('/brief', (req, res) => {
+  const rawIds = String(req.query.ids || '').split(',').map(s => parseInt(s.trim(), 10)).filter(n => Number.isFinite(n) && n > 0).slice(0, 6);
+  const designs = rawIds.map(id => DESIGNS.find(d => d.id === id)).filter(Boolean);
+  const projectName = String(req.query.project || '').slice(0, 80);
+  const clientName = String(req.query.client || '').slice(0, 80);
+  const esc = (s) => String(s||'').replace(/[<>&"]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[c]));
+  if (!designs.length) {
+    res.setHeader('Cache-Control', 'public, max-age=600, must-revalidate'); // landing page — short cache
+    return res.type('html').send(`<!doctype html><html><head><meta charset="utf-8"><title>Brief — wallco.ai</title>
+<style>body{font:14px/1.5 -apple-system,system-ui,sans-serif;max-width:600px;margin:60px auto;padding:24px;color:#333}a{color:#111}</style></head><body>
+<h1 style="font:300 32px/1.1 Georgia,serif">Build a design brief</h1>
+<p>Add up to 6 wallco.ai designs to a single proposal PDF — perfect for emailing to a client.</p>
+<p><strong>Usage:</strong> <code>/brief?ids=72,161,139&amp;client=Smith Residence&amp;project=Living Room</code></p>
+<p>Or visit any design page and add it to your <a href="/me/saved">Saved Designs</a>, then click <strong>Build proposal PDF</strong>.</p>
+<p>Alternatively, start a <a href="/compare">/compare</a>, then export the comparison as a brief.</p>
+</body></html>`);
+  }
+  // Cache the populated brief — query string (ids + project + client) is part of the cache key.
+  // max-age=3600 (1h) — shorter than tearsheet (24h) because designers may rotate project/client labels.
+  const briefLM = designs.reduce((acc, d) => {
+    const t = d.created_at ? new Date(d.created_at) : null;
+    return t && (!acc || t > acc) ? t : acc;
+  }, null) || new Date();
+  if (maybe304(req, res, briefLM)) return;
+  // ETag covers (ordered ids, project, client) alongside the briefLM that maybe304 already
+  // handles via If-Modified-Since. If-None-Match short-circuit catches the case where the
+  // designer comes back to the same brief link — share-link reopen, tab restore.
+  const _briefSig = require('crypto').createHash('sha1').update(rawIds.join(',') + '|' + projectName + '|' + clientName).digest('hex').slice(0, 16);
+  const _briefEtag = `"brief-${briefLM.getTime()}-${_briefSig}"`;
+  res.setHeader('ETag', _briefEtag);
+  if (req.headers['if-none-match'] === _briefEtag) return res.status(304).end();
+  res.setHeader('Cache-Control', 'public, max-age=3600, must-revalidate');
+  const today = new Date().toISOString().slice(0, 10);
+  const tearsheet = (d) => {
+    const roomKey = (d.room_mockups || []).includes('living_room') ? 'living_room'
+                  : (d.room_mockups || []).includes('bedroom') ? 'bedroom'
+                  : (d.room_mockups || []).includes('office')  ? 'office'
+                  : (d.room_mockups || [])[0];
+    const roomImg = roomKey ? `/designs/room/design_${d.id}_${roomKey}.png` : null;
+    const motifs = (d.motifs || []).slice(0, 6).join(' · ');
+    return `<section class="brief-page" style="page-break-after:always;padding:36px 28px;border-bottom:1px dashed #ccc">
+      <div style="display:flex;justify-content:space-between;align-items:baseline;border-bottom:1px solid #1a1a1a;padding-bottom:8px;margin-bottom:16px">
+        <div style="font:12px/1 -apple-system,sans-serif;letter-spacing:.1em;text-transform:uppercase">wallco.ai</div>
+        <div style="font:11px/1 -apple-system,sans-serif;color:#666">Design #${d.id} · ${today}</div>
+      </div>
+      <h2 style="font:300 30px/1.1 Georgia,serif;margin:0 0 4px">${esc(d.title || 'Design #' + d.id)}</h2>
+      <div style="font:13px/1.4 -apple-system,sans-serif;color:#555;margin-bottom:14px">${esc(d.category || 'mixed')}${motifs ? ' · ' + esc(motifs) : ''}</div>
+      <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:14px">
+        <div style="aspect-ratio:1/1;background:#f4f1ea url('${esc(d.image_url)}') center/cover;border:1px solid #d8d2c4;border-radius:4px"></div>
+        ${roomImg
+          ? `<div style="aspect-ratio:1/1;background:#f4f1ea url('${esc(roomImg)}') center/cover;border:1px solid #d8d2c4;border-radius:4px"></div>`
+          : `<div style="aspect-ratio:1/1;background:#f4f1ea;border:1px solid #d8d2c4;border-radius:4px;display:flex;align-items:center;justify-content:center;font:11px -apple-system,sans-serif;color:#888;text-align:center;padding:20px">Room preview on request</div>`
+        }
+      </div>
+      <div style="padding:10px 14px;background:#faf8f3;border:1px solid #ebe5d6;border-radius:4px;margin-bottom:12px;display:flex;align-items:center;gap:10px;font:12px -apple-system,sans-serif">
+        <div style="width:24px;height:24px;border-radius:50%;background:${esc(d.dominant_hex || '#888')};border:1px solid rgba(0,0,0,.15);flex-shrink:0"></div>
+        <div><strong>Dominant color:</strong> ${esc(d.dominant_hex || 'n/a')}</div>
+      </div>
+      <dl style="display:grid;grid-template-columns:110px 1fr;gap:4px 12px;font:11px/1.5 -apple-system,sans-serif;margin:0">
+        <dt style="color:#666;text-transform:uppercase;letter-spacing:.06em;font-size:9px;padding-top:2px">SKU</dt><dd style="margin:0;color:#1a1a1a;font-weight:500">${esc(d.handle || 'wallco-' + String(d.id).padStart(4,'0'))}</dd>
+        <dt style="color:#666;text-transform:uppercase;letter-spacing:.06em;font-size:9px;padding-top:2px">Tile</dt><dd style="margin:0">24×24 in seamless · 150 DPI</dd>
+        <dt style="color:#666;text-transform:uppercase;letter-spacing:.06em;font-size:9px;padding-top:2px">Substrate</dt><dd style="margin:0">Type II vinyl · nonwoven · peel-and-stick · canvas mural</dd>
+        <dt style="color:#666;text-transform:uppercase;letter-spacing:.06em;font-size:9px;padding-top:2px">Fire</dt><dd style="margin:0">Class A available (ASTM E84)</dd>
+        <dt style="color:#666;text-transform:uppercase;letter-spacing:.06em;font-size:9px;padding-top:2px">Cleanability</dt><dd style="margin:0">Scrubbable (vinyl) · Spongeable (paper)</dd>
+        <dt style="color:#666;text-transform:uppercase;letter-spacing:.06em;font-size:9px;padding-top:2px">Lead Time</dt><dd style="margin:0">Sample 3–5 days · Production ~2 weeks</dd>
+        <dt style="color:#666;text-transform:uppercase;letter-spacing:.06em;font-size:9px;padding-top:2px">URL</dt><dd style="margin:0;font:11px ui-monospace,Menlo,monospace;word-break:break-all">wallco.ai/design/${d.id}</dd>
+      </dl>
+    </section>`;
+  };
+  res.type('html').send(`<!doctype html><html lang="en"><head>
+<meta charset="utf-8"><title>${esc(projectName || clientName || 'Wallpaper Brief')} — wallco.ai</title>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<meta name="robots" content="noindex,nofollow">
+<style>
+  @page { size: letter portrait; margin: 0.5in; }
+  * { box-sizing: border-box; }
+  body { font-family: Georgia, 'Times New Roman', serif; color:#1a1a1a; margin:0; padding:0; background:#fff; }
+  .brief-cover { padding:60px 40px 40px; min-height:9in; display:flex; flex-direction:column; justify-content:space-between; page-break-after:always }
+  .brief-cover h1 { font:300 56px/1.05 Georgia, serif; margin:0 0 10px }
+  .brief-cover .meta { font:13px/1.6 -apple-system, system-ui, sans-serif; color:#444; margin-top:18px }
+  .brief-cover .meta strong { color:#1a1a1a }
+  .brief-cover .thumbs { display:grid; grid-template-columns: repeat(${Math.min(designs.length, 3)}, 1fr); gap:10px; margin:40px 0 }
+  .brief-cover .thumbs > div { aspect-ratio:1/1; background:#f4f1ea center/cover no-repeat; border:1px solid #d8d2c4; border-radius:4px }
+  .brief-cover .footer { font:11px/1.5 -apple-system, system-ui, sans-serif; color:#555; border-top:1px solid #ddd; padding-top:14px; margin-top:auto }
+  .no-print { padding: 16px 40px; background:#faf8f3; border-bottom:1px solid #eee }
+  .no-print button { padding:10px 20px; font:13px -apple-system, system-ui, sans-serif; background:#1a1a1a; color:#fff; border:0; border-radius:4px; cursor:pointer; margin-right:8px }
+  .no-print a { padding:10px 20px; font:13px -apple-system, system-ui, sans-serif; background:#fff; color:#1a1a1a; text-decoration:none; border:1px solid #1a1a1a; border-radius:4px; display:inline-block }
+  @media print { .no-print { display:none } }
+</style></head><body>
+<div class="no-print">
+  <button onclick="window.print()">Print or Save as PDF</button>
+  <a href="/me/saved">← Back to saved</a>
+</div>
+<section class="brief-cover">
+  <div>
+    <div style="font:11px/1 -apple-system, system-ui, sans-serif; letter-spacing:.14em; text-transform:uppercase; color:#666; margin-bottom:30px">wallco.ai · Design Brief</div>
+    <h1>${esc(projectName || 'Wallpaper Proposal')}</h1>
+    ${clientName ? `<div style="font:18px/1.3 Georgia,serif;color:#555;margin-top:8px">Prepared for ${esc(clientName)}</div>` : ''}
+    <div class="meta">
+      <strong>${designs.length} design${designs.length === 1 ? '' : 's'}</strong> ·
+      ${today} ·
+      Designer Wallcoverings, Sherman Oaks CA
+    </div>
+    <div class="thumbs">
+      ${designs.slice(0, 3).map(d => `<div style="background-image:url('${esc(d.image_url)}')" aria-label="${esc(d.title)}"></div>`).join('')}
+    </div>
+  </div>
+  <div class="footer">
+    <strong>Designer Wallcoverings</strong> · 15442 Ventura Bl #102, Sherman Oaks CA 91403<br>
+    <strong>info@designerwallcoverings.com</strong> · designerwallcoverings.com<br>
+    AI-original wallpaper, hand-curated. Free trade samples on request.
+  </div>
+</section>
+${designs.map(tearsheet).join('')}
+<section style="padding:40px;text-align:center;font:12px/1.6 -apple-system, system-ui, sans-serif;color:#555">
+  <p><strong>Next step</strong></p>
+  <p>Request samples for any of the ${designs.length} designs in this brief —<br>email <strong>info@designerwallcoverings.com</strong> or call to speak with our trade team.</p>
+  <p style="margin-top:18px;color:#888">${esc(designs.map(d => d.handle || 'wallco-' + d.id).join(' · '))}</p>
+</section>
+</body></html>`);
+});
+
+// ── MURALS
+app.get('/murals', (req, res) => {
+  const q = String(req.query.q || '').toLowerCase().trim();
+  const murals = DESIGNS.filter(d => d.kind === 'mural' || d.kind === 'mural_panel');
+  let all = murals.length === 0 ? DESIGNS : murals; // fallback: show all if no mural-kind yet
+  // Server-side ?q= filter — mirrors the search endpoint's logic so the
+  // form-submit fallback works without JS.
+  if (q && q.length >= 2) {
+    const terms = q.split(/\s+/).filter(Boolean);
+    all = all.filter(d => {
+      const hay = [
+        (d.title || '').toLowerCase(),
+        (d.category || '').toLowerCase(),
+        (d.handle || '').toLowerCase(),
+        ...(d.motifs || []).map(m => String(m).toLowerCase())
+      ].join(' ');
+      return terms.every(t => hay.includes(t));
+    });
+  }
+
+  const cards = all.map(d => {
+    const roomKey = (d.room_mockups || []).includes('living_room') ? 'living_room'
+                  : (d.room_mockups || []).includes('bedroom')     ? 'bedroom'
+                  : (d.room_mockups || []).includes('office')      ? 'office'
+                  : (d.room_mockups || [])[0];
+    const roomCount = (d.room_mockups || []).length;
+    const badgeText = roomCount > 1 ? `${roomCount} rooms` : (roomCount === 1 ? 'In a room' : '');
+    const roomOverlay = roomKey
+      ? `<img class="card-room" src="/designs/room/design_${d.id}_${roomKey}.png" loading="lazy" alt="" aria-hidden="true"><span class="card-room-badge">${badgeText}</span>`
+      : '';
+    return `
+    <a href="/design/${d.id}" class="design-card${roomKey ? ' has-room' : ''}" aria-label="${cardAriaLabel(d)}">
+      <div class="card-img" style="background-image:url('${d.image_url}')" role="img" aria-label="${d.title} pattern thumbnail">${roomOverlay}</div>
+      <div class="card-meta">
+        <span class="card-title">${d.title}</span>
+        <span class="card-cat">${murals.length === 0 ? 'available as mural' : d.kind}</span>
+        <span class="color-dot" style="background:${d.dominant_hex}" aria-hidden="true"></span>
+      </div>
+    </a>`;
+  }).join('');
+
+  res.type('html').send(`${htmlHead({
+    title: 'Murals — wallco.ai | Original Wall Murals',
+    description: 'AI-original wall murals in any size, panelized for installation. Every design unique.',
+    canonical: 'https://wallco.ai/murals'
+  })}
+<body>
+${htmlHeader('/murals')}
+<main>
+<section class="catalog-section">
+  <div class="section-header" style="padding:40px 40px 0">
+    <h1>Murals</h1>
+    <p>Any design available as a custom-sized mural, panelized for wall installation.
+    ${murals.length === 0 ? 'All current designs can be ordered as murals — contact us.' : `${murals.length} mural designs available.`}${q ? ` · matching &ldquo;${q.replace(/[<>&"]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[c]))}&rdquo; (${all.length} found)` : ''}</p>
+  </div>
+  <div class="catalog-toolbar" style="padding:18px 40px 0">
+    <form method="get" action="/murals" class="filter-form">
+      <div class="toolbar-row">
+        <div class="search-wrap" style="position:relative;flex:1;min-width:200px">
+          <input type="search" name="q" value="${q.replace(/[<>&"]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[c]))}" placeholder="Search murals…" class="search-input" id="murals-search-input" autocomplete="off" aria-autocomplete="list" aria-controls="murals-search-suggest" aria-expanded="false" role="combobox" style="width:100%">
+          <div id="murals-search-suggest" role="listbox" aria-label="Search suggestions" style="display:none;position:absolute;left:0;right:0;top:100%;margin-top:4px;background:var(--bg,#fff);border:1px solid var(--line,#ddd);border-radius:6px;box-shadow:0 8px 24px rgba(0,0,0,.12);z-index:50;max-height:60vh;overflow-y:auto"></div>
+        </div>
+      </div>
+    </form>
+  </div>
+  <div class="design-grid catalog-grid" style="padding:24px 40px">
+    ${cards || '<p class="empty-state">No murals match that search.</p>'}
+  </div>
+</section>
+</main>
+${FOOTER}
+<script>
+(function(){
+  var sIn = document.getElementById('murals-search-input');
+  var sBox = document.getElementById('murals-search-suggest');
+  if (!sIn || !sBox) return;
+  var sTimer = null, sCtl = null, sIdx = -1, sItems = [];
+  var esc = function(s){ return String(s||'').replace(/[&<>"]/g, function(c){ return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]; }); };
+  var hide = function(){ sBox.style.display='none'; sIn.setAttribute('aria-expanded','false'); sIdx=-1; };
+  var render = function(results, qStr, dym){
+    if (!results || !results.length) {
+      var dymRow = '';
+      if (dym && dym.length) {
+        dymRow = '<div style="padding:10px 14px 12px;border-top:1px solid var(--line,#f0eee9);background:#fafaf7"><div style="font:10px var(--sans,system-ui);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-faint,#888);margin-bottom:6px">Did you mean</div><div style="display:flex;flex-wrap:wrap;gap:5px">'
+          + dym.slice(0,5).map(function(s){ return '<a href="/murals?q='+encodeURIComponent(s.word)+'" style="display:inline-flex;align-items:center;padding:4px 10px;font:11px var(--sans,system-ui);background:var(--bg,#fff);color:var(--ink,#111);border:1px solid var(--line,#e6e2d8);border-radius:999px;text-decoration:none">'+esc(s.word)+'</a>'; }).join('')
+          + '</div></div>';
+      }
+      sBox.innerHTML = '<div style="padding:14px 16px;font:13px var(--sans,system-ui);color:var(--ink-faint,#888)">No matches for &ldquo;'+esc(qStr)+'&rdquo;</div>' + dymRow;
+      sBox.style.display='block'; sIn.setAttribute('aria-expanded','true');
+      sItems = []; sIdx = -1; return;
+    }
+    var rows = results.slice(0, 8).map(function(r, i){
+      var img = r.image_url ? '<div style="width:44px;height:44px;flex-shrink:0;background:#f2efe8 url('+JSON.stringify(r.image_url).slice(1,-1)+') center/cover;border-radius:4px;border:1px solid var(--line,#eee)"></div>' : '<div style="width:44px;height:44px;flex-shrink:0;background:#f2efe8;border-radius:4px"></div>';
+      var dot = r.dominant_hex ? '<span style="display:inline-block;width:9px;height:9px;border-radius:50%;background:'+esc(r.dominant_hex)+';border:1px solid rgba(0,0,0,.12);vertical-align:middle;margin-right:4px"></span>' : '';
+      return '<a href="'+esc(r.url)+'" role="option" data-idx="'+i+'" style="display:flex;align-items:center;gap:10px;padding:8px 12px;text-decoration:none;color:var(--ink,#111);border-bottom:1px solid var(--line,#f0eee9);font:13px var(--sans,system-ui)">'+img+'<div style="min-width:0;flex:1"><div style="font-weight:500;color:var(--ink,#111);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+esc(r.title||('Design #'+r.id))+'</div><div style="font:11px var(--sans,system-ui);color:var(--ink-faint,#888);margin-top:2px">'+dot+esc(r.category||'')+'</div></div></a>';
+    }).join('');
+    var more = results.length > 8 ? '<a href="/murals?q='+encodeURIComponent(qStr)+'" style="display:block;padding:8px 12px;font:12px var(--sans,system-ui);color:var(--ink-soft,#555);text-align:center;background:var(--card-bg,#fafafa);text-decoration:none">See all '+results.length+' results →</a>' : '';
+    sBox.innerHTML = rows + more;
+    sBox.style.display='block'; sIn.setAttribute('aria-expanded','true');
+    sItems = Array.prototype.slice.call(sBox.querySelectorAll('a[role=option]'));
+    sIdx = -1;
+  };
+  var fetchSuggest = function(qStr){
+    if (sCtl) sCtl.abort(); sCtl = new AbortController();
+    fetch('/api/designs/search?q='+encodeURIComponent(qStr)+'&limit=24', { signal: sCtl.signal })
+      .then(function(r){ return r.json(); })
+      .then(function(j){ render(j.results||[], qStr, j.did_you_mean||[]); })
+      .catch(function(){});
+  };
+  sIn.addEventListener('input', function(){ var v = this.value.trim(); if (sTimer) clearTimeout(sTimer); if (v.length<2) { hide(); return; } sTimer = setTimeout(function(){ fetchSuggest(v); }, 180); });
+  sIn.addEventListener('keydown', function(e){
+    if (e.key === 'Escape') { hide(); return; }
+    if (sBox.style.display === 'none' || !sItems.length) return;
+    if (e.key === 'ArrowDown') { e.preventDefault(); sIdx = (sIdx+1) % sItems.length; sItems.forEach(function(a,i){ a.style.background = i===sIdx?'var(--card-bg,#fafafa)':''; }); sItems[sIdx].scrollIntoView({ block:'nearest' }); }
+    else if (e.key === 'ArrowUp') { e.preventDefault(); sIdx = (sIdx-1+sItems.length) % sItems.length; sItems.forEach(function(a,i){ a.style.background = i===sIdx?'var(--card-bg,#fafafa)':''; }); sItems[sIdx].scrollIntoView({ block:'nearest' }); }
+    else if (e.key === 'Enter' && sIdx >= 0) { e.preventDefault(); window.location = sItems[sIdx].href; }
+  });
+  document.addEventListener('click', function(e){ if (!sIn.contains(e.target) && !sBox.contains(e.target)) hide(); });
+})();
+</script>
+${HAMBURGER_JS}
+</body>
+</html>`);
+});
+
+// /provenance → /about (content merged 2026-05-14; 301 keeps external links + old
+// sitemap entries pointing at the live About page instead of returning 404).
+app.get('/provenance', (req, res) => res.redirect(301, '/about'));
+
+// ── ABOUT
+app.get('/about', (req, res) => {
+  res.type('html').send(`${htmlHead({
+    title: 'About — wallco.ai | How AI Wallpaper is Made',
+    description: 'Learn how wallco.ai generates original wallpaper patterns using AI. Every design is unique, AI-original, never licensed from a vendor.',
+    canonical: 'https://wallco.ai/about'
+  })}
+<body>
+${htmlHeader('/about')}
+<main class="prose-main">
+  <article class="prose">
+    <h1>How wallco.ai works</h1>
+
+    <h2>AI-original, never licensed</h2>
+    <p>Every pattern on wallco.ai was generated by an AI model — not sourced from a vendor catalog,
+    not licensed from a studio. Each design is produced from a text prompt and a unique random seed,
+    so the pattern that emerges is mathematically unrepeatable.</p>
+
+    <h2>The generation pipeline</h2>
+    <p>Each pattern is rendered by our proprietary AI-design system, drawing inspiration from
+    public-domain artwork in the Metropolitan Museum of Art, Wikimedia Commons, and the
+    Smithsonian Institution — institutions with open licenses (CC0 / Public Domain) that
+    permit derivative AI generation.</p>
+
+    <h2>Seamless tiling</h2>
+    <p>Each design is generated as a seamless tile — the edges match on all four sides, so it can
+    repeat across a wall without visible seams. Standard tile size is 24×24 inches at 150 DPI.</p>
+
+    <h2>Murals</h2>
+    <p>Any design can be printed as a custom-sized mural, panelized for your wall dimensions.
+    Panel splitting for installation is handled during order processing.</p>
+
+    <h2>About wallco.ai</h2>
+    <p>wallco.ai is a product of <a href="https://designerwallcoverings.com">Designer Wallcoverings</a>,
+    Sherman Oaks, CA. We combine 30 years of wallcovering expertise with frontier AI generation to
+    create patterns that have never existed before.</p>
+
+    <p><a href="/samples" class="btn-primary">Request a Free Sample</a></p>
+  </article>
+</main>
+${FOOTER}
+
+<script type="application/ld+json">${JSON.stringify({
+  "@context":"https://schema.org",
+  "@type":"WebPage",
+  "name":"About wallco.ai",
+  "url":"https://wallco.ai/about",
+  "description":"How wallco.ai generates original AI wallpaper patterns.",
+  "isPartOf":{"@type":"WebSite","url":"https://wallco.ai","name":"wallco.ai"}
+})}</script>
+${HAMBURGER_JS}
+</body>
+</html>`);
+});
+
+// ── /age-themes — curated "best theme per age" comparison page
+// Honest framing: AI-generated starting point (ui-ux-designer subagent + debate-team-fast
+// adversarial review). Companion Figma file at figma.com/design/WmwSirHywv530eACPtbqyA.
+// APCA-W3 Lc calculator — extracted to shared module (2026-05-12 architect refactor).
+// Canonical at ~/Projects/wallco-ai/src/apca.js · mirrored at
+// ~/.claude/skills/age-adaptive-theme/lib/apca.js (identical bytes).
+const { apcaLc } = require('./src/apca');
+
+// Canonical band data — project-local (deploy-portable). Skill ships a
+// docs-mirror at ~/.claude/skills/age-adaptive-theme/bands.json (identical bytes;
+// diff to detect drift). Project-canonical because HOME=/root on Kamatera has no
+// ~/.claude (see feedback_deploy_portability_bands 2026-05-12).
+const AGE_THEME_DATA = require('./src/bands.json');
+
+app.get('/age-themes', (req, res) => {
+  const _isAdmin = isAdmin(req);
+  if (!_isAdmin) return res.status(404).type('html').send('<h1>404</h1>');
+
+  const bands = AGE_THEME_DATA.bands;
+
+  const renderBand = (b) => {
+    // Diff chips — surface every prop that changed between current ↔ best so
+    // even subtle deltas (e.g. body +2px with font/colors unchanged) read at a
+    // glance. Fixes architect punch-list #11 (adult band "looked identical").
+    const diffProps = Object.keys(b.best).filter(k => b.current[k] !== b.best[k]);
+    const diffChips = diffProps.map(k => {
+      const cur = b.current[k], next = b.best[k];
+      const isFontFamily = k === 'font';
+      const lhs = isFontFamily ? String(cur).split(',')[0] : cur;
+      const rhs = isFontFamily ? String(next).split(',')[0] : next;
+      return `<span class="diff-chip"><span class="diff-key">${k}</span> <span class="diff-from">${lhs}</span> → <span class="diff-to">${rhs}</span></span>`;
+    }).join('');
+    const diffBar = diffProps.length === 0
+      ? `<div class="diff-bar diff-bar-noop">no change recommended</div>`
+      : `<div class="diff-bar"><span class="diff-count">${diffProps.length} change${diffProps.length===1?'':'s'}:</span>${diffChips}</div>`;
+
+    const swatch = (theme) => `
+      <div class="theme-card" style="background:${theme.bg}; color:${theme.fg}; border:1px solid ${theme.border}; font-family:${theme.font};">
+        <div class="theme-panel" style="background:${theme.panel}; border:1px solid ${theme.border};">
+          <h3 style="font-size:${theme.h}px; line-height:1.1; margin:0 0 12px; color:${theme.fg};">${b.label} · ${b.age}</h3>
+          <p style="font-size:${theme.body}px; line-height:1.4; margin:0 0 14px; color:${theme.fg};">${b.sample}</p>
+          <button style="background:${theme.accent}; color:${theme.bg}; border:none; padding:8px 14px; font-size:${theme.body}px; font-family:inherit; cursor:default;">Tap me</button>
+        </div>
+        <div class="theme-spec">
+          <span class="hex" style="background:${theme.bg}"  title="bg ${theme.bg}"></span>
+          <span class="hex" style="background:${theme.panel}" title="panel ${theme.panel}"></span>
+          <span class="hex" style="background:${theme.fg}"  title="fg ${theme.fg}"></span>
+          <span class="hex" style="background:${theme.accent}" title="accent ${theme.accent}"></span>
+          <span class="hex" style="background:${theme.border}" title="border ${theme.border}"></span>
+          <span class="meta">${theme.body}/${theme.h}px · ${theme.font.split(',')[0]}</span>
+        </div>
+        <div class="theme-apca">
+          ${(() => {
+            const lcText = apcaLc(theme.fg, theme.panel);
+            const lcAccent = apcaLc(theme.accent, theme.panel);
+            const lcBtn = apcaLc(theme.bg, theme.accent);
+            const cls = lc => Math.abs(lc) >= 75 ? 'lc-aaa' : Math.abs(lc) >= 60 ? 'lc-aa' : Math.abs(lc) >= 45 ? 'lc-ui' : 'lc-fail';
+            return `
+              <span class="lc ${cls(lcText)}" title="fg on panel">Lc ${lcText} text</span>
+              <span class="lc ${cls(lcAccent)}" title="accent on panel">Lc ${lcAccent} accent</span>
+              <span class="lc ${cls(lcBtn)}" title="button text on accent">Lc ${lcBtn} btn</span>
+            `;
+          })()}
+        </div>
+      </div>`;
+    return `
+      <section class="band-row" id="${b.id}">
+        <header class="band-header">
+          <h2><span class="age-pill">${b.age}</span> ${b.label}</h2>
+          <p class="band-mood">${b.mood}</p>
+          <p class="band-anchor">anchor → ${b.anchor}</p>
+          ${diffBar}
+        </header>
+        <div class="band-compare">
+          <div class="theme-col">
+            <div class="theme-label theme-label-current">CURRENT</div>
+            ${swatch(b.current)}
+          </div>
+          <div class="theme-col">
+            <div class="theme-label theme-label-best">BEST</div>
+            ${swatch(b.best)}
+          </div>
+          <div class="theme-rationale">
+            <div class="rationale-swap">${b.swap.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')}</div>
+            <p>${b.reasoning}</p>
+          </div>
+        </div>
+      </section>`;
+  };
+
+  res.type('html').send(`<!doctype html>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
+<title>Best themes for every age · wallco.ai</title>
+<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;600&family=Nunito:wght@400;700&family=Public+Sans:wght@400;600&family=Space+Grotesk:wght@400;600&family=JetBrains+Mono:wght@400;600&family=Inter:wght@400;500;600&family=Source+Serif+Pro:wght@400;600&family=Atkinson+Hyperlegible:wght@400;700&display=swap" rel="stylesheet">
+<style>
+  * { box-sizing: border-box; }
+  body { margin:0; background:#0f0e0c; color:#e8e2d6; font:13px/1.4 -apple-system, system-ui, sans-serif; }
+  header.top { padding:22px 36px; border-bottom:1px solid #2a2825; background:#1a1816; display:flex; gap:18px; align-items:baseline; flex-wrap:wrap; position:sticky; top:0; z-index:10; }
+  header.top h1 { margin:0; font-weight:300; font-size:24px; letter-spacing:.5px; }
+  header.top .tag { font-size:11px; color:#aaa; text-transform:uppercase; letter-spacing:.1em; }
+  header.top nav { margin-left:auto; }
+  header.top nav a { color:#bba; text-decoration:none; margin-left:18px; font-size:12px; }
+  header.top nav a.on { color:#d2b15c; border-bottom:1px solid #d2b15c; }
+  header.top .credits { width:100%; margin-top:8px; font-size:11px; color:#888; line-height:1.5; }
+  header.top .credits .src { color:#d2b15c; }
+  main { padding: 30px 36px 60px; max-width:1480px; margin:0 auto; }
+  .toc { display:flex; gap:6px; flex-wrap:wrap; margin-bottom:24px; padding:14px 18px; background:#1a1816; border:1px solid #2a2825; border-radius:3px; }
+  .toc a { padding:4px 10px; background:#2a2825; color:#bba; text-decoration:none; border-radius:12px; font-size:11px; transition:all .12s; }
+  .toc a:hover { background:#d2b15c; color:#0f0e0c; }
+  .band-row { margin-bottom:36px; padding-bottom:26px; border-bottom:1px solid #1f1c18; scroll-margin-top:90px; }
+  .band-header { margin-bottom:16px; }
+  .band-header h2 { margin:0 0 6px; font-weight:400; font-size:18px; display:flex; align-items:center; gap:12px; }
+  .age-pill { display:inline-block; padding:3px 10px; background:#d2b15c; color:#0f0e0c; border-radius:14px; font-size:11px; font-weight:600; letter-spacing:.04em; }
+  .band-mood { margin:0 0 4px; color:#bba; font-size:12px; font-style:italic; }
+  .band-anchor { margin:0; color:#888; font-size:11px; }
+  .diff-bar { margin-top:8px; padding:8px 12px; background:rgba(210,177,92,.06); border-left:3px solid #d2b15c; border-radius:2px; display:flex; gap:6px; flex-wrap:wrap; align-items:center; }
+  .diff-bar-noop { background:rgba(255,255,255,.03); border-left-color:#3a3631; color:#666; font-size:11px; font-style:italic; }
+  .diff-count { font-size:10px; color:#d2b15c; letter-spacing:.06em; text-transform:uppercase; margin-right:4px; }
+  .diff-chip { font-size:11px; padding:2px 8px; background:#1a1816; border:1px solid #2a2825; border-radius:11px; color:#bba; font-family:ui-monospace, Menlo, monospace; }
+  .diff-chip .diff-key { color:#888; }
+  .diff-chip .diff-from { color:#d2554a; }
+  .diff-chip .diff-to { color:#5fbf6e; font-weight:600; }
+  .band-compare { display:grid; grid-template-columns: 1fr 1fr 1.1fr; gap:14px; align-items:start; }
+  .theme-col { display:flex; flex-direction:column; gap:6px; }
+  .theme-label { font-size:10px; letter-spacing:.1em; color:#888; text-transform:uppercase; padding-left:4px; }
+  .theme-label-best { color:#d2b15c; }
+  .theme-card { padding:0; border-radius:3px; overflow:hidden; min-height:200px; transition:transform .15s; }
+  .theme-card:hover { transform: scale(1.01); }
+  .theme-panel { padding:18px 20px; min-height:140px; }
+  .theme-spec { padding:6px 10px; background:rgba(0,0,0,.4); display:flex; gap:5px; align-items:center; flex-wrap:wrap; }
+  .theme-spec .hex { width:14px; height:14px; border-radius:2px; border:1px solid rgba(255,255,255,.2); display:inline-block; cursor:help; }
+  .theme-spec .meta { font-size:9px; color:#cccccc; margin-left:auto; font-family:ui-monospace, Menlo, monospace; }
+  .theme-apca { padding:5px 10px; background:rgba(0,0,0,.6); display:flex; gap:6px; flex-wrap:wrap; }
+  .theme-apca .lc { font-family:ui-monospace, Menlo, monospace; font-size:9px; padding:1px 6px; border-radius:2px; }
+  .theme-apca .lc.lc-aaa  { background:#1f3a1f; color:#5fbf6e; }
+  .theme-apca .lc.lc-aa   { background:#1f2f3a; color:#5fa8bf; }
+  .theme-apca .lc.lc-ui   { background:#3a2e1f; color:#d2b15c; }
+  .theme-apca .lc.lc-fail { background:#3a1f1f; color:#d2554a; }
+  .theme-rationale { padding:16px 18px; background:#1a1816; border:1px solid #2a2825; border-radius:3px; min-height:200px; }
+  .rationale-swap { color:#d2b15c; font-size:13px; font-weight:500; margin-bottom:10px; line-height:1.4; }
+  .rationale-swap strong { font-weight:700; }
+  .theme-rationale p { margin:0; color:#bba; font-size:12px; line-height:1.5; }
+  @media (max-width: 900px) { .band-compare { grid-template-columns: 1fr; } }
+  @media (max-width: 640px) {
+    header.top { padding:14px 18px; gap:10px; }
+    header.top h1 { font-size:18px; }
+    header.top .tag { font-size:10px; }
+    header.top nav { width:100%; margin-left:0; display:flex; gap:14px; }
+    header.top nav a { margin-left:0; }
+    header.top .credits { font-size:10.5px; line-height:1.45; }
+    header.top .credits code { font-size:9.5px; }
+    main { padding: 18px 16px 40px; }
+    .toc { padding:10px 12px; gap:5px; }
+    .toc a { font-size:10px; padding:3px 8px; }
+    .band-row { margin-bottom:24px; padding-bottom:18px; scroll-margin-top:78px; }
+    .band-header h2 { font-size:16px; gap:9px; }
+    .age-pill { font-size:10px; padding:2px 8px; }
+    .band-mood { font-size:11px; }
+    .band-anchor { font-size:10px; }
+    .diff-bar { padding:6px 10px; gap:5px; flex-wrap:wrap; }
+    .diff-chip { font-size:10px; padding:2px 6px; }
+    .diff-count { font-size:9px; width:100%; margin:0 0 4px; }
+    .theme-card { min-height:170px; }
+    .theme-panel { padding:14px 16px; min-height:120px; }
+    .theme-spec { padding:5px 8px; gap:4px; }
+    .theme-spec .hex { width:12px; height:12px; }
+    .theme-spec .meta { font-size:8.5px; width:100%; margin-left:0; text-align:right; }
+    .theme-apca { padding:4px 8px; gap:4px; }
+    .theme-apca .lc { font-size:8.5px; padding:1px 5px; }
+    .theme-rationale { padding:12px 14px; min-height:0; }
+    .rationale-swap { font-size:12px; }
+    .theme-rationale p { font-size:11px; }
+    .theme-label { font-size:9px; }
+  }
+</style>
+<header class="top">
+  <h1>Best themes for every age</h1>
+  <span class="tag">8 bands · 3 → 95</span>
+  <nav>
+    <a href="/designs?review=1">/designs</a>
+    <a href="/moodboard">/moodboard</a>
+    <a href="/age-themes" class="on">/age-themes</a>
+  </nav>
+  <div class="credits">
+    <strong style="color:#d2b15c">v${AGE_THEME_DATA.version}</strong> · validated ${AGE_THEME_DATA.validated_at} ·
+    <strong style="color:#d2554a">⚠ AI-generated starting point — not user-tested, not accessibility-tooled.</strong>
+    Recommendations from the <span class="src">ui-ux-designer</span> subagent (font/hex/size swaps with research citations) and reviewed by the <span class="src">debate-team-fast</span> 3-LLM panel (Claude, Kimi K2.5, Qwen3:14b — Codex quota-blocked). Companion <span class="src">Figma file</span> at <a href="https://www.figma.com/design/WmwSirHywv530eACPtbqyA" target="_blank" style="color:#d2b15c" rel="noopener noreferrer nofollow">figma.com/design/WmwSirHywv530eACPtbqyA</a>. Canonical band data: <code style="color:#d2b15c">~/Projects/wallco-ai/src/bands.json</code> (skill mirror: <code style="color:#666">~/.claude/skills/age-adaptive-theme/bands.json</code>) · APCA module: <code style="color:#d2b15c">~/Projects/wallco-ai/src/apca.js</code>.
+    <br><span style="color:#888">Debate-team consensus: Atkinson Hyperlegible / tween indigo / Nunito picks are defensible. The fg #222→#1a1a1a (toddler) and -apple-system→Inter (adult) swaps were LLM "hallucinated precision" — dropped. APCA Lc values shown are calculated by src/apca.js (Myndex 0.1.9), NOT lab-verified. v9 fixes from validation-loop: teen body 16px, line-height 1.5 baseline, btn-min-height 44/48/56px per band.</span>
+  </div>
+</header>
+<main>
+  <nav class="toc">
+    ${bands.map(b => `<a href="#${b.id}">${b.age} · ${b.label}</a>`).join('')}
+  </nav>
+  ${bands.map(renderBand).join('')}
+</main>
+</body></html>`);
+});
+
+// ── MOODBOARD (admin only — collects every pinned pairing across all reviewed designs)
+app.get('/moodboard', (req, res) => {
+  const _isAdmin = isAdmin(req);
+  if (!_isAdmin) return res.status(404).type('html').send('<h1>404</h1><p>Not found.</p>');
+  let moodboards = {};
+  let reviews = {};
+  try { moodboards = JSON.parse(require('fs').readFileSync(require('path').join(__dirname,'data','moodboards.json'),'utf8')); } catch {}
+  try { reviews   = JSON.parse(require('fs').readFileSync(require('path').join(__dirname,'data','reviews.json'),'utf8')); } catch {}
+
+  const filter = (req.query.type || '').toLowerCase();
+  const entries = Object.entries(moodboards).filter(([_,arr]) => Array.isArray(arr) && arr.length > 0);
+  const total = entries.reduce((n, [_, arr]) => n + arr.length, 0);
+
+  const escHTML = s => String(s||'').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":"&#39;"}[c]));
+
+  // group blocks per design
+  const blocks = entries.map(([designId, pins]) => {
+    const design = DESIGNS.find(d => String(d.id) === String(designId)) || { id: designId, title: '#'+designId };
+    const review = reviews[designId] || {};
+    const filteredPins = filter ? pins.filter(p => p.type === filter) : pins;
+    if (filter && filteredPins.length === 0) return '';
+    const pinCards = filteredPins.map(p => {
+      let thumb;
+      if (p.type === 'design' && p.thumb_url) thumb = `<div class="mb-thumb" style="background-image:url('${p.thumb_url}')"></div>`;
+      else if (p.type === 'color' && p.hex)    thumb = `<div class="mb-thumb" style="background:${p.hex}"></div>`;
+      else                                      thumb = `<div class="mb-thumb mb-thumb-mat">${p.icon || '✦'}</div>`;
+      return `
+        <div class="mb-card" data-design="${designId}" data-pin-key="${escHTML(p.pin_key)}">
+          ${thumb}
+          <div class="mb-body">
+            <span class="mb-type">${p.type}${p.type==='design'&&p.design_id?' · #'+p.design_id:''}</span>
+            <div class="mb-title">${escHTML(p.label || p.hex || '(unnamed)')}</div>
+            <div class="mb-reason">${escHTML(p.reason || '')}</div>
+          </div>
+          <button class="mb-unpin" type="button" title="Remove from moodboard">★</button>
+        </div>`;
+    }).join('');
+    return `
+      <section class="mb-design-block">
+        <header class="mb-design-header">
+          <a class="mb-design-thumb" href="/design/${design.id}" target="_blank" style="background-image:url('${design.image_url || ''}')" rel="noopener noreferrer"></a>
+          <div class="mb-design-info">
+            <h2 class="mb-design-title">${escHTML(design.title || design.prompt || '#'+design.id)}</h2>
+            <p class="mb-design-meta">${escHTML(design.category||'')} · <span class="mb-design-dot" style="background:${design.dominant_hex||'#888'}"></span> ${escHTML(design.dominant_hex||'')}${review.decision?' · <strong style="color:'+(review.decision==='keep'?'#5fbf6e':'#d2554a')+'">'+review.decision.toUpperCase()+'</strong>':''}</p>
+            ${review.why?`<p class="mb-design-why">"${escHTML(review.why)}"</p>`:''}
+          </div>
+          <div class="mb-design-count">${filteredPins.length} pinned</div>
+        </header>
+        <div class="mb-pins-grid">${pinCards}</div>
+      </section>`;
+  }).filter(Boolean).join('');
+
+  res.type('html').send(`<!doctype html>
+<meta charset="utf-8">
+<title>moodboard · wallco.ai</title>
+<style>
+  * { box-sizing: border-box; }
+  body { margin:0; background:#0f0e0c; color:#e8e2d6; font:13px/1.4 -apple-system, system-ui, sans-serif; }
+  header.top { padding:18px 32px; border-bottom:1px solid #2a2825; background:#1a1816; display:flex; gap:18px; align-items:baseline; flex-wrap:wrap; }
+  header.top h1 { margin:0; font-weight:300; font-size:22px; letter-spacing:.5px; }
+  header.top .tag { font-size:11px; color:#aaa; }
+  header.top nav a { color:#bba; text-decoration:none; margin-right:14px; font-size:12px; }
+  header.top nav a.on { color:#d2b15c; border-bottom:1px solid #d2b15c; }
+  header.top .filters { margin-left:auto; display:flex; gap:8px; }
+  header.top .filter-pill { background:#2a2825; padding:3px 10px; border-radius:12px; font-size:11px; cursor:pointer; color:#bba; border:1px solid transparent; text-decoration:none; }
+  header.top .filter-pill.on { background:#d2b15c; color:#0f0e0c; }
+  #mb-search { background:#16140f; border:1px solid #3a3631; border-radius:14px; padding:3px 12px; font:11px/1 -apple-system, system-ui, sans-serif; color:#e8e2d6; min-width:200px; outline:none; transition:border-color .12s; }
+  #mb-search:focus { border-color:#d2b15c; }
+  #mb-search::placeholder { color:#666; }
+  .mb-card.mb-hidden, .mb-design-block.mb-hidden { display:none !important; }
+  .mb-search-empty { padding:32px 24px; text-align:center; color:#666; font-style:italic; font-size:13px; }
+  header.top .export-link { font-size:11px; color:#d2b15c; text-decoration:none; padding:3px 10px; border:1px solid #3a3631; border-radius:2px; }
+  header.top .export-link:hover { background:#d2b15c; color:#0f0e0c; }
+  main { padding: 24px 32px; max-width:1400px; margin:0 auto; }
+  .mb-design-block { margin-bottom:36px; padding-bottom:28px; border-bottom:1px solid #1f1c18; }
+  .mb-design-header { display:flex; gap:18px; align-items:flex-start; margin-bottom:14px; }
+  .mb-design-thumb { flex:0 0 96px; width:96px; height:96px; background:#0d0c0a no-repeat center/cover; border-radius:3px; }
+  .mb-design-info { flex:1; min-width:0; }
+  .mb-design-title { margin:0 0 4px; font-size:15px; font-weight:400; color:#e8e2d6; }
+  .mb-design-meta { margin:0 0 6px; font-size:11px; color:#888; }
+  .mb-design-dot { display:inline-block; width:9px; height:9px; border-radius:50%; vertical-align:middle; border:1px solid #00000060; }
+  .mb-design-why { margin:0; font-size:11px; color:#d2b15c; font-style:italic; line-height:1.4; }
+  .mb-design-count { font-size:11px; color:#bba; padding:3px 10px; background:#2a2825; border-radius:12px; align-self:center; white-space:nowrap; }
+  .mb-pins-grid { display:grid; gap:10px; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); }
+  .mb-card { display:flex; gap:10px; align-items:flex-start; padding:9px 11px; background:#1a1816; border:1px solid #2a2825; border-radius:3px; transition:border-color .15s; }
+  .mb-card:hover { border-color:#3a3631; }
+  .mb-thumb { flex:0 0 52px; width:52px; height:52px; border-radius:3px; background:#0d0c0a no-repeat center/cover; display:flex; align-items:center; justify-content:center; }
+  .mb-thumb.mb-thumb-mat { background:#2a2825; color:#d2b15c; font-size:18px; }
+  .mb-body { flex:1; min-width:0; }
+  .mb-type { display:inline-block; font-size:9px; text-transform:uppercase; letter-spacing:.06em; padding:1px 5px; background:#2a2825; color:#d2b15c; border-radius:2px; margin-bottom:3px; }
+  .mb-title { font-size:11px; color:#e8e2d6; line-height:1.3; margin-bottom:2px; }
+  .mb-reason { font-size:10px; color:#888; line-height:1.4; }
+  .mb-unpin { background:#d2b15c33; border:1px solid #d2b15c; color:#d2b15c; font-size:11px; cursor:pointer; padding:2px 7px; border-radius:10px; align-self:flex-start; transition:all .12s; }
+  .mb-unpin:hover { background:#d2b15c; color:#0f0e0c; }
+  .empty { padding:60px 0; text-align:center; color:#666; }
+  .empty p { font-size:13px; margin:6px 0; }
+  .empty a { color:#d2b15c; text-decoration:none; }
+</style>
+<header class="top">
+  <h1>moodboard</h1>
+  <span class="tag">${total} pin${total===1?'':'s'} across ${entries.length} design${entries.length===1?'':'s'}</span>
+  <nav>
+    <a href="/designs?review=1">Review queue</a>
+    <a href="/moodboard${filter?'?type='+filter:''}" class="on">Moodboard</a>
+  </nav>
+  <div class="filters">
+    <input type="search" id="mb-search" placeholder="search labels + reasons…" autocomplete="off" />
+    <a class="filter-pill ${!filter?'on':''}" href="/moodboard">All</a>
+    <a class="filter-pill ${filter==='design'?'on':''}" href="/moodboard?type=design">Designs</a>
+    <a class="filter-pill ${filter==='color'?'on':''}" href="/moodboard?type=color">Colors</a>
+    <a class="filter-pill ${filter==='material'?'on':''}" href="/moodboard?type=material">Materials</a>
+    <a class="export-link" href="/api/export/keep.csv" download>Kept CSV</a>
+    <a class="export-link" href="/api/export/spoonflower-batch.csv" download title="Spoonflower-batch CSV — feeds upload_spoonflower_new.py --batch">Spoonflower CSV</a>
+  </div>
+</header>
+<main>
+${blocks || `<div class="empty"><p>No pins yet.</p><p>Head to <a href="/designs?review=1">/designs</a>, click <strong>Pair</strong> on any card, then ★-pin the suggestions you like.</p></div>`}
+</main>
+<script>
+document.querySelectorAll('.mb-unpin').forEach(btn => {
+  btn.addEventListener('click', async () => {
+    const card = btn.closest('.mb-card');
+    const designId = card.dataset.design;
+    const pinKey = card.dataset.pinKey;
+    btn.disabled = true;
+    try {
+      const r = await fetch('/api/moodboard/' + designId + '/pin', {
+        method: 'POST', headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({ pin_key: pinKey, suggestion: {} })
+      });
+      if (r.ok) {
+        card.style.transition = 'opacity .2s';
+        card.style.opacity = '0';
+        setTimeout(() => { card.remove();
+          // if section empty, remove it
+          document.querySelectorAll('.mb-design-block').forEach(b => {
+            if (!b.querySelector('.mb-card')) b.remove();
+          });
+          // update count in header
+          const remaining = document.querySelectorAll('.mb-card').length;
+          document.querySelector('.tag').textContent = remaining + ' pin' + (remaining===1?'':'s') + ' across ' + document.querySelectorAll('.mb-design-block').length + ' design' + (document.querySelectorAll('.mb-design-block').length===1?'':'s');
+        }, 200);
+      } else btn.disabled = false;
+    } catch (e) { btn.disabled = false; }
+  });
+});
+
+// R6 — text search across pin labels + reasons. Persists across reloads via
+// localStorage. Pure client-side filter on rendered DOM (no API change needed).
+(function(){
+  var input = document.getElementById('mb-search');
+  if (!input) return;
+  var LS_KEY = 'wallco-mb-search';
+  try { input.value = localStorage.getItem(LS_KEY) || ''; } catch(e) {}
+
+  function applyFilter() {
+    var q = (input.value || '').trim().toLowerCase();
+    try { localStorage.setItem(LS_KEY, q); } catch(e) {}
+    var anyShown = false;
+    document.querySelectorAll('.mb-design-block').forEach(function(block){
+      var blockShown = false;
+      block.querySelectorAll('.mb-card').forEach(function(card){
+        if (!q) { card.classList.remove('mb-hidden'); blockShown = true; return; }
+        var label = (card.querySelector('.mb-title') || {}).textContent || '';
+        var reason = (card.querySelector('.mb-reason') || {}).textContent || '';
+        var match = (label + ' ' + reason).toLowerCase().indexOf(q) !== -1;
+        card.classList.toggle('mb-hidden', !match);
+        if (match) blockShown = true;
+      });
+      // also match against design title in the block header
+      if (q && !blockShown) {
+        var dt = (block.querySelector('.mb-design-title') || {}).textContent || '';
+        if (dt.toLowerCase().indexOf(q) !== -1) {
+          block.querySelectorAll('.mb-card').forEach(function(c){ c.classList.remove('mb-hidden'); });
+          blockShown = true;
+        }
+      }
+      block.classList.toggle('mb-hidden', !blockShown);
+      if (blockShown) anyShown = true;
+    });
+    // empty-state hint
+    var main = document.querySelector('main');
+    var empty = document.getElementById('mb-search-empty');
+    if (q && !anyShown) {
+      if (!empty) {
+        empty = document.createElement('div');
+        empty.id = 'mb-search-empty';
+        empty.className = 'mb-search-empty';
+        main.appendChild(empty);
+      }
+      empty.textContent = 'No pins match "' + q + '"';
+    } else if (empty) {
+      empty.remove();
+    }
+  }
+  input.addEventListener('input', applyFilter);
+  // apply persisted search on load
+  if (input.value) applyFilter();
+})();
+</script>
+</body></html>`);
+});
+
+// ── SAMPLES (lead capture — no commerce in v1)
+app.get('/samples', (req, res) => {
+  const designId = req.query.design || '';
+  const design   = designId ? DESIGNS.find(d => d.id === parseInt(designId,10)) : null;
+  // Roll width chosen on the design page (query param). Constrained to {24,36,52}.
+  const reqW = parseInt(req.query.width, 10);
+  const selectedWidth = [24, 36, 52].includes(reqW) ? reqW : (design && [24,36,52].includes(Math.round(design.width_in||0)) ? Math.round(design.width_in) : 24);
+
+  res.type('html').send(`${htmlHead({
+    title: 'Request a Sample — wallco.ai',
+    description: 'Request a free physical or digital sample of any wallco.ai original design.',
+    canonical: 'https://wallco.ai/samples'
+  })}
+<body>
+${htmlHeader('/samples')}
+<main class="prose-main">
+  <div class="sample-form-wrap">
+    <h1>Request a Free Sample</h1>
+    <p>We'll send you a physical swatch or a high-res digital proof. No charge, no obligation.</p>
+
+    ${design ? `<div class="sample-preview">
+      <img src="${design.image_url}" alt="${design.title}" style="width:120px;height:120px;object-fit:cover;border-radius:4px;">
+      <div><strong>${design.title}</strong><br>Pattern No.${String(design.id).padStart(4,'0')}</div>
+    </div>` : ''}
+
+    <form method="post" action="/api/leads" class="sample-form" id="sample-form">
+      <input type="hidden" name="source" value="samples-page">
+      ${design ? `<input type="hidden" name="design_id" value="${design.id}">` : ''}
+
+      <label>Roll width
+        <div id="sample-width-tabs" style="display:flex;gap:8px;margin-top:6px">
+          ${[24,36,52].map(w => `<button type="button" class="sw-tab${w===selectedWidth?' is-on':''}" data-w="${w}" style="flex:1;padding:9px 12px;border:1px solid ${w===selectedWidth?'var(--accent,#222)':'var(--line,#ddd)'};background:${w===selectedWidth?'var(--accent,#222)':'transparent'};color:${w===selectedWidth?'var(--bg,#fff)':'var(--ink,#222)'};font:13px var(--sans,system-ui);font-weight:500;border-radius:6px;cursor:pointer">${w}"</button>`).join('')}
+        </div>
+        <input type="hidden" name="width" id="sample-width-input" value="${selectedWidth}">
+      </label>
+
+      <label>Design
+        <select name="design_id" ${design ? 'disabled' : ''}>
+          ${design
+            ? `<option value="${design.id}" selected>${design.title}</option>`
+            : DESIGNS.map(d => `<option value="${d.id}" ${d.id===parseInt(designId,10)?'selected':''}>${d.title} (No.${d.id})</option>`).join('')
+          }
+        </select>
+      </label>
+      <label>Your name <input type="text" name="name" required autocomplete="name" enterkeyhint="next" placeholder="Jane Smith"></label>
+      <label>Email <input type="email" name="email" required autocomplete="email" inputmode="email" enterkeyhint="next" placeholder="you@example.com"></label>
+      <label>Phone (optional) <input type="tel" name="phone" autocomplete="tel" inputmode="tel" enterkeyhint="next" placeholder="+1 (555) 000-0000"></label>
+      <label>Message (optional) <textarea name="message" rows="3" placeholder="Any notes about your project…"></textarea></label>
+      <button type="submit" class="btn-primary">Send Sample Request</button>
+    </form>
+    <div id="form-result" style="display:none;margin-top:16px;padding:16px;border-radius:4px;"></div>
+  </div>
+</main>
+${FOOTER}
+
+<script>
+// Roll-width tab wiring (3-way toggle 24/36/52)
+(function(){
+  var tabs = document.querySelectorAll('#sample-width-tabs .sw-tab');
+  var hidden = document.getElementById('sample-width-input');
+  tabs.forEach(function(t){
+    t.addEventListener('click', function(){
+      tabs.forEach(function(x){
+        var on = x === t;
+        x.classList.toggle('is-on', on);
+        x.style.background = on ? 'var(--accent,#222)' : 'transparent';
+        x.style.color      = on ? 'var(--bg,#fff)' : 'var(--ink,#222)';
+        x.style.borderColor= on ? 'var(--accent,#222)' : 'var(--line,#ddd)';
+      });
+      hidden.value = t.dataset.w;
+    });
+  });
+})();
+document.getElementById('sample-form').addEventListener('submit', async function(e) {
+  e.preventDefault();
+  var form = e.target;
+  var result = document.getElementById('form-result');
+  var data = Object.fromEntries(new FormData(form));
+  try {
+    var r = await fetch('/api/leads', {
+      method: 'POST',
+      headers: {'Content-Type':'application/json'},
+      body: JSON.stringify(data)
+    });
+    var json = await r.json();
+    result.style.display = 'block';
+    if (json.ok) {
+      result.style.background = '#d4edda';
+      result.style.color = '#155724';
+      result.textContent = 'Thank you! We\'ll be in touch shortly.';
+      form.reset();
+    } else {
+      result.style.background = '#f8d7da';
+      result.style.color = '#721c24';
+      result.textContent = 'Something went wrong — please email info@wallco.ai directly.';
+    }
+  } catch(err) {
+    result.style.display = 'block';
+    result.textContent = 'Network error — please try again.';
+  }
+});
+</script>
+${HAMBURGER_JS}
+</body>
+</html>`);
+});
+
+// ── API: Lead capture
+app.post('/api/leads', async (req, res) => {
+  const { name, email, phone, message, design_id, source } = req.body;
+  if (!email) return res.status(400).json({ ok: false, error: 'email required' });
+
+  // Write to dw_unified.wallco_leads (create table if not exists)
+  try {
+    const { execSync } = require('child_process');
+    const sql = `
+      CREATE TABLE IF NOT EXISTS wallco_leads (
+        id BIGSERIAL PRIMARY KEY,
+        name TEXT,
+        email TEXT NOT NULL,
+        phone TEXT,
+        message TEXT,
+        design_id BIGINT,
+        source TEXT,
+        created_at TIMESTAMPTZ DEFAULT now()
+      );
+      INSERT INTO wallco_leads (name, email, phone, message, design_id, source)
+      VALUES (
+        ${name    ? `'${name.replace(/'/g,"''")}'`       : 'NULL'},
+        '${email.replace(/'/g,"''")}',
+        ${phone   ? `'${phone.replace(/'/g,"''")}'`      : 'NULL'},
+        ${message ? `'${message.replace(/'/g,"''")}'`    : 'NULL'},
+        ${design_id ? parseInt(design_id,10) : 'NULL'},
+        ${source  ? `'${source.replace(/'/g,"''")}'`     : 'NULL'}
+      );
+    `;
+    execSync(`${PSQL_CMD} -c "${sql.replace(/\n/g,' ')}"`, { encoding: 'utf8' });
+  } catch (e) {
+    console.error('Lead insert error:', e.message);
+    // Still return ok — don't block the user on a DB error
+  }
+  res.json({ ok: true });
+});
+
+// ── API: designs JSON (for future use / inline editor)
+// File-existence cache — built once at module load, refreshed every 5min.
+// Filters out designs whose PNG isn't on disk so the grid never shows blank
+// tiles. Steve's standing rule (2026-05-13): "hot or not — anything cannot
+// show unless there is a photo or pic". Same rule applies to /designs.
+let _imgExistsCache = null;
+let _imgExistsRefreshedAt = 0;
+function imageFilesOnDisk() {
+  const now = Date.now();
+  if (_imgExistsCache && (now - _imgExistsRefreshedAt) < 5 * 60 * 1000) return _imgExistsCache;
+  try {
+    const files = fs.readdirSync(IMG_DIR);
+    _imgExistsCache = new Set(files);
+    _imgExistsRefreshedAt = now;
+    console.log(`[api/designs] file-cache refreshed: ${files.length} files on disk`);
+  } catch (e) {
+    console.error('[api/designs] readdir fail:', e.message);
+    if (!_imgExistsCache) _imgExistsCache = new Set();
+  }
+  return _imgExistsCache;
+}
+function designHasImage(d) {
+  if (!d) return false;
+  if (BLANK_IDS.has(d.id)) return false;   // failed-generation blank — never show
+  const fileSet = imageFilesOnDisk();
+  if (d.filename) return fileSet.has(d.filename);
+  // /designs/img/by-id/:id pattern — used by night-builder + generate_designs.js
+  // for every new design. Resolve by checking local_path's basename against the
+  // file cache so these rows aren't hidden by the file-existence filter.
+  if (d.image_url && /^\/designs\/img\/by-id\/\d+/.test(d.image_url)) {
+    if (d.local_path) {
+      const fname = String(d.local_path).split('/').pop();
+      if (fname && fileSet.has(fname)) return true;
+    }
+    return false;
+  }
+  if (d.image_url && d.image_url.startsWith('/designs/img/')) {
+    return fileSet.has(d.image_url.replace('/designs/img/', ''));
+  }
+  return false;
+}
+
+app.get('/api/designs', (req, res) => {
+  const sort = req.query.sort || 'newest';
+  const cat  = req.query.cat  || '';
+  const q    = (req.query.q   || '').toLowerCase();
+  const page = Math.max(1, parseInt(req.query.page || '1', 10));
+  const PER  = parseInt(req.query.limit || '60', 10);
+
+  // Hard filter — never serve a row whose image file isn't on disk.
+  let filtered = DESIGNS.filter(designHasImage);
+  if (cat)  filtered = filtered.filter(d => d.category === cat);
+  if (q)    filtered = filtered.filter(d =>
+    d.title.toLowerCase().includes(q) || d.category.includes(q)
+  );
+  filtered = sortDesigns(filtered, sort);
+
+  const total = filtered.length;
+  const pages = Math.ceil(total / PER);
+  const items = filtered.slice((page-1)*PER, page*PER);
+  res.json({ ok: true, total, page, pages, items });
+});
+
+// ── API: inline editor state (admin gate)
+app.get('/api/_inline/state', (req, res) => {
+  if (!isAdmin(req)) return res.status(401).json({ ok: false, error: 'unauthorized' });
+  res.json({ ok: true, site: SITE, designCount: DESIGNS.length });
+});
+
+// ─────────────────────────────────────────────────────────────────────────
+// SPOONFLOWER + ROOM PREVIEW + VOICE
+// ─────────────────────────────────────────────────────────────────────────
+
+function findDesign(id) {
+  return DESIGNS.find(d => d.id === parseInt(id, 10));
+}
+
+function localPathForDesign(d) {
+  // Designs have image_url like /designs/img/<file>.png — map back to data/generated/
+  if (!d) return null;
+  if (d.local_path) return d.local_path;
+  if (d.filename) return path.join(IMG_DIR, d.filename);
+  if (d.image_url && d.image_url.startsWith('/designs/img/')) {
+    return path.join(IMG_DIR, d.image_url.replace('/designs/img/', ''));
+  }
+  return null;
+}
+
+// ── POST /api/room — generate room visualization for a design
+app.post('/api/room', async (req, res) => {
+  const { design_id, roomType = 'bedroom', angle = 'straight_on',
+          cameraDistance = 5, patternWidth = 27, patternHeight = 27 } = req.body || {};
+  // FRESH-PATH FIX (2026-05-12): the in-memory DESIGNS snapshot can be stale
+  // for designs that were composed/baked after server start — leading to the
+  // upstream renderer receiving the OLD pattern file. Always read local_path
+  // live from spoon_all_designs to guarantee we send the CURRENT pattern.
+  let file = null;
+  try {
+    const lp = psqlQuery(`SELECT local_path FROM spoon_all_designs WHERE id=${pgEsc(parseInt(design_id, 10))} LIMIT 1;`);
+    if (lp && lp.trim()) file = lp.trim();
+  } catch (e) { /* fall through to in-memory */ }
+  if (!file) {
+    const d = findDesign(design_id);
+    if (!d) return res.status(404).json({ ok: false, error: 'design not found' });
+    file = localPathForDesign(d);
+  }
+  if (!file || !fs.existsSync(file)) {
+    return res.status(404).json({ ok: false, error: 'design file missing on disk', file });
+  }
+  try {
+    const b64 = fs.readFileSync(file).toString('base64');
+    // Upstream renderer (45.61.58.125:3075) drops ~5-15% of requests with
+    // {ok:false,error:'fetch failed'} or transient TLS hiccups. Retry once
+    // with 800ms backoff before giving up — caught in the 27-render matrix
+    // 2026-05-12 where 5/27 cells failed and would have succeeded on retry.
+    let j = null, lastErr = null;
+    for (let attempt = 1; attempt <= 2; attempt++) {
+      try {
+        const upstream = await fetch(ROOM_API, {
+          method: 'POST',
+          headers: { 'Content-Type': 'application/json' },
+          body: JSON.stringify({
+            patternBase64: b64, roomType, angle,
+            cameraDistance, patternWidth, patternHeight
+          })
+        });
+        const candidate = await upstream.json();
+        if (candidate && candidate.success !== false && candidate.image) { j = candidate; break; }
+        lastErr = (candidate && candidate.error) || 'upstream returned no image';
+      } catch (e) {
+        lastErr = e.message;
+      }
+      if (attempt < 2) await new Promise(r => setTimeout(r, 800));
+    }
+    if (!j) {
+      return res.status(502).json({ ok: false, error: lastErr || 'upstream error', retried: true });
+    }
+    // upstream returns { success, image, ... } — `image` is base64 or data-URL
+    const imgB64 = j.image.replace(/^data:image\/\w+;base64,/, '');
+    const fname = `room_${design_id}_${roomType}_${Date.now()}.png`;
+    const outPath = path.join(ROOMS_DIR, fname);
+    const buf = Buffer.from(imgB64, 'base64');
+    fs.writeFileSync(outPath, buf);
+    const image_url = `/uploads/rooms/${fname}`;
+
+    // Persist to wallco_rooms for gallery + marketing reuse.
+    let room_id = null;
+    try {
+      const row = psqlQuery(`INSERT INTO wallco_rooms
+        (design_id, room_type, angle, camera_distance, pattern_width, pattern_height,
+         filename, image_path, file_size_bytes, c2pa_signed)
+        VALUES (${pgEsc(parseInt(design_id, 10))}, ${pgEsc(roomType)}, ${pgEsc(angle)},
+                ${pgEsc(Number(cameraDistance))}, ${pgEsc(Number(patternWidth))}, ${pgEsc(Number(patternHeight))},
+                ${pgEsc(fname)}, ${pgEsc(image_url)}, ${pgEsc(buf.length)}, TRUE)
+        RETURNING id;`);
+      room_id = parseInt(row, 10) || null;
+    } catch (e) {
+      console.error('[wallco_rooms insert]', e.message);
+    }
+
+    // Bridge to catalog hover-preview: every admin-generated room is also
+    // mirrored to data/rooms/design_<id>_<room>.png + merged into the
+    // spoon_all_designs.room_mockups JSONB column. This is what /designs
+    // reads to render the "In a room" filter chip + the .card-room overlay.
+    // Failures here don't break the gallery (try/catch isolated).
+    try {
+      const canonicalDir = path.join(__dirname, 'data', 'rooms');
+      if (!fs.existsSync(canonicalDir)) fs.mkdirSync(canonicalDir, { recursive: true });
+      const canonicalPath = path.join(canonicalDir, `design_${parseInt(design_id, 10)}_${roomType}.png`);
+      fs.writeFileSync(canonicalPath, buf);
+
+      const merged = psqlQuery(`UPDATE spoon_all_designs
+        SET room_mockups = COALESCE(room_mockups, '{}'::jsonb) || jsonb_build_object(${pgEsc(roomType)}, ${pgEsc(canonicalPath)})
+        WHERE id = ${pgEsc(parseInt(design_id, 10))}
+        RETURNING COALESCE(room_mockups, '{}'::jsonb)::text;`);
+      // Reflect in the in-memory DESIGNS catalog so /designs surfaces the new room
+      // without a server restart.
+      if (merged) {
+        try {
+          const obj = JSON.parse(merged);
+          const target = DESIGNS.find(x => x.id === parseInt(design_id, 10));
+          if (target) target.room_mockups = Object.keys(obj).sort();
+        } catch (_) { /* malformed JSONB — leave DESIGNS untouched */ }
+      }
+    } catch (e) {
+      console.error('[room_mockups bridge]', e.message);
+    }
+
+    res.json({ ok: true, room_id, image_url, roomType, design_id });
+  } catch (e) {
+    res.status(500).json({ ok: false, error: e.message });
+  }
+});
+
+// ── GET /api/rooms?design_id=N&include_removed=0&marketing_only=0
+// Returns generated rooms for a design (or all, if no design_id) with metadata.
+app.get('/api/rooms', (req, res) => {
+  const designId = req.query.design_id ? parseInt(req.query.design_id, 10) : null;
+  const includeRemoved = req.query.include_removed === '1' && isRoomsAdmin(req);
+  const marketingOnly = req.query.marketing_only === '1';
+  try {
+    const where = [];
+    if (Number.isInteger(designId)) where.push(`design_id = ${designId}`);
+    if (!includeRemoved) where.push('removed_at IS NULL');
+    if (marketingOnly) where.push('selected_for_marketing = TRUE');
+    const whereSQL = where.length ? `WHERE ${where.join(' AND ')}` : '';
+    const raw = psqlQuery(`SELECT COALESCE(json_agg(t ORDER BY t.created_at DESC), '[]'::json)
+      FROM (SELECT id, design_id, room_type, angle, image_path, filename, file_size_bytes,
+                   selected_for_marketing, removed_at, removed_reason, notes, created_at
+              FROM wallco_rooms ${whereSQL}) t;`);
+    res.json({ ok: true, rooms: JSON.parse(raw || '[]'), admin: isRoomsAdmin(req) });
+  } catch (e) {
+    res.status(500).json({ ok: false, error: e.message });
+  }
+});
+
+// ── POST /api/rooms/:id/remove  (admin only)
+// Soft-deletes; the image file stays on disk so prior page links continue to work for caching.
+// Pass { hard: true, reason: "..." } to also unlink the file.
+app.post('/api/rooms/:id/remove', (req, res) => {
+  if (!isRoomsAdmin(req)) return res.status(403).json({ ok: false, error: 'admin only' });
+  const id = parseInt(req.params.id, 10);
+  if (!Number.isInteger(id) || id <= 0) return res.status(400).json({ ok: false, error: 'bad id' });
+  const { reason = '', hard = false } = req.body || {};
+  try {
+    const row = psqlQuery(`UPDATE wallco_rooms SET removed_at = NOW(), removed_reason = ${pgEsc(reason)}
+      WHERE id = ${id} AND removed_at IS NULL
+      RETURNING filename;`);
+    if (!row) return res.status(404).json({ ok: false, error: 'not found or already removed' });
+    if (hard) {
+      try { fs.unlinkSync(path.join(ROOMS_DIR, row)); } catch {}
+    }
+    res.json({ ok: true, id, hard: !!hard, filename: row });
+  } catch (e) {
+    res.status(500).json({ ok: false, error: e.message });
+  }
+});
+
+// ── POST /api/rooms/:id/restore  (admin only) — reverses a soft-delete.
+app.post('/api/rooms/:id/restore', (req, res) => {
+  if (!isRoomsAdmin(req)) return res.status(403).json({ ok: false, error: 'admin only' });
+  const id = parseInt(req.params.id, 10);
+  if (!Number.isInteger(id) || id <= 0) return res.status(400).json({ ok: false, error: 'bad id' });
+  try {
+    const row = psqlQuery(`UPDATE wallco_rooms SET removed_at = NULL, removed_reason = NULL
+      WHERE id = ${id} RETURNING id;`);
+    if (!row) return res.status(404).json({ ok: false, error: 'not found' });
+    res.json({ ok: true, id });
+  } catch (e) {
+    res.status(500).json({ ok: false, error: e.message });
+  }
+});
+
+// ── POST /api/rooms/:id/marketing  (admin only) — toggle marketing flag + optional notes.
+app.post('/api/rooms/:id/marketing', (req, res) => {
+  if (!isRoomsAdmin(req)) return res.status(403).json({ ok: false, error: 'admin only' });
+  const id = parseInt(req.params.id, 10);
+  if (!Number.isInteger(id) || id <= 0) return res.status(400).json({ ok: false, error: 'bad id' });
+  const { selected = true, notes = null } = req.body || {};
+  try {
+    const row = psqlQuery(`UPDATE wallco_rooms SET selected_for_marketing = ${pgEsc(!!selected)},
+        notes = COALESCE(${pgEsc(notes)}, notes)
+      WHERE id = ${id} RETURNING id, selected_for_marketing;`);
+    if (!row) return res.status(404).json({ ok: false, error: 'not found' });
+    res.json({ ok: true, row });
+  } catch (e) {
+    res.status(500).json({ ok: false, error: e.message });
+  }
+});
+
+// ── POST /api/spoonflower/upload — upload ONE design as new product
+app.post('/api/spoonflower/upload', requireAdmin, (req, res) => {
+  const { design_id, dry = true, tags = '' } = req.body || {};
+  const d = findDesign(design_id);
+  if (!d) return res.status(404).json({ ok: false, error: 'design not found' });
+  const file = localPathForDesign(d);
+  if (!file || !fs.existsSync(file)) {
+    return res.status(404).json({ ok: false, error: 'design file missing on disk', file });
+  }
+  const title = `${d.title} — wallco.ai AI-Original`;
+  const tagList = tags || `wallpaper,wallco-original,${d.category||''},wallco,${d.dominant_hex||''}`.replace(/,+$/,'');
+
+  let jobId;
+  try {
+    jobId = psqlQuery(`INSERT INTO spoonflower_jobs(kind, design_id, status, dry_run, params)
+                       VALUES('upload_new', ${parseInt(design_id,10)}, 'running', ${dry?'TRUE':'FALSE'},
+                       '${JSON.stringify({title, tagList, file}).replace(/'/g,"''")}'::jsonb)
+                       RETURNING id;`);
+  } catch (e) { return res.status(500).json({ ok:false, error: 'db: '+e.message }); }
+
+  const logFile = path.join(SF_JOBS_DIR, `upload_${jobId}.log`);
+  const args = [
+    path.join(__dirname, 'scripts', 'upload_spoonflower_new.py'),
+    file, title, '--tags', tagList,
+    ...(dry ? ['--dry'] : ['--live'])
+  ];
+  const proc = spawn('python3', args, {
+    cwd: __dirname,
+    env: { ...process.env, DATABASE_URL: DB_URL },
+    detached: true, stdio: ['ignore', 'pipe', 'pipe']
+  });
+  const out = fs.createWriteStream(logFile);
+  proc.stdout.pipe(out, { end: false });
+  proc.stderr.pipe(out, { end: false });
+  proc.on('exit', code => {
+    out.end();
+    // Parse the JSON-line log so the new Spoonflower pattern number + URL
+    // can be persisted onto the design row. The python script emits one
+    // JSON object per line; the final `done` event carries sf_design_id
+    // and sf_public_url on a successful LIVE publish.
+    let sfDesignId = null, sfUrl = null;
+    try {
+      const raw = fs.readFileSync(logFile, 'utf8');
+      for (const line of raw.split('\n')) {
+        const s = line.trim();
+        if (!s.startsWith('{')) continue;
+        try {
+          const ev = JSON.parse(s);
+          if (ev && ev.sf_design_id)  sfDesignId = String(ev.sf_design_id);
+          if (ev && ev.sf_public_url) sfUrl      = String(ev.sf_public_url);
+        } catch {}
+      }
+      psqlQuery(`UPDATE spoonflower_jobs SET status='${code===0?'done':'error'}',
+                 finished_at=now(),
+                 log='${raw.replace(/'/g,"''").slice(-8000)}'
+                 WHERE id=${jobId};`);
+      if (code === 0 && !dry && sfDesignId) {
+        const did = parseInt(design_id, 10);
+        psqlQuery(`UPDATE spoon_all_designs SET
+                     spoonflower_design_id   = ${pgEsc(sfDesignId)},
+                     spoonflower_url         = ${pgEsc(sfUrl)},
+                     spoonflower_uploaded_at = NOW()
+                   WHERE id = ${did};`);
+      }
+    } catch {}
+  });
+  proc.unref();
+  res.json({ ok: true, job_id: jobId, dry, log_url: `/api/spoonflower/status/${jobId}` });
+});
+
+// ── POST /api/design/:id/upload-shopify — push a wallco.ai design as a NEW Shopify product
+//
+// Admin-only. Reads the design row, picks the DW Original Number
+// (DIG_AI_xxxxxx — auto-assigned at INSERT-time by trg_spoon_all_designs_assign_dig)
+// as the SKU, and creates a draft Shopify product on
+// designer-laboratory-sandbox.myshopify.com with:
+//   • title       = ai_title || title
+//   • vendor      = "Designer Wallcoverings"
+//   • product_type= "Wallpaper"
+//   • tags        = wallco-ai, wallco-original, <category>, <ai_pattern>
+//   • SKU         = dig_number
+//   • image       = base64 of local_path
+//   • metafields  = dwc.dig_number, dwc.spoonflower_url, dwc.wallco_design_id
+//
+// Persists shopify_product_id + shopify_product_url onto the design row so
+// /designs index and /design/:id can show the linkage.
+app.post('/api/design/:id/upload-shopify', requireAdmin, express.json({ limit: '16kb' }), async (req, res) => {
+  const id = parseInt(req.params.id, 10);
+  if (!Number.isFinite(id) || id < 1) return res.status(400).json({ error: 'bad id' });
+
+  const token = process.env.SHOPIFY_ADMIN_TOKEN;
+  const store = process.env.SHOPIFY_STORE;
+  if (!token || !store) {
+    return res.status(503).json({ error: 'SHOPIFY_ADMIN_TOKEN / SHOPIFY_STORE not configured' });
+  }
+
+  let d;
+  try {
+    const raw = psqlQuery(`SELECT row_to_json(t) FROM (
+      SELECT id, ai_title, ai_pattern, ai_color_name, category, tags,
+             dominant_hex, dig_number, local_path, spoonflower_design_id,
+             spoonflower_url, shopify_product_id, shopify_product_url
+      FROM spoon_all_designs WHERE id=${id}) t;`);
+    d = JSON.parse(raw || '{}');
+  } catch (e) { return res.status(500).json({ error: 'db: ' + e.message }); }
+  if (!d || !d.id) return res.status(404).json({ error: 'design not found' });
+
+  if (d.shopify_product_id) {
+    return res.json({
+      ok: true,
+      already_uploaded: true,
+      shopify_product_id: d.shopify_product_id,
+      shopify_product_url: d.shopify_product_url,
+    });
+  }
+
+  if (!d.local_path || !fs.existsSync(d.local_path)) {
+    return res.status(409).json({ error: 'image file missing on disk', local_path: d.local_path });
+  }
+
+  const dig = d.dig_number || `DIG_AI_${String(d.id).padStart(6, '0')}`;
+  const title = d.ai_title || `wallco.ai ${dig}`;
+  const tagList = [
+    'wallco-ai', 'wallco-original',
+    d.category, d.ai_pattern, d.ai_color_name,
+  ].filter(Boolean).join(', ');
+
+  const imgB64 = fs.readFileSync(d.local_path).toString('base64');
+  const imgExt = (path.extname(d.local_path) || '.png').replace('.', '');
+
+  const productBody = {
+    product: {
+      title,
+      vendor: 'Designer Wallcoverings',
+      product_type: 'Wallpaper',
+      status: 'draft',
+      tags: tagList,
+      body_html: `<p>An AI-original wallcovering pattern from wallco.ai. DW Original Number ${dig}.</p>`,
+      variants: [{
+        sku: dig,
+        price: '0.00',
+        inventory_management: null,
+        inventory_policy: 'continue',
+      }],
+      images: [{
+        attachment: imgB64,
+        filename: `${dig}.${imgExt}`,
+        alt: title,
+      }],
+      metafields: [
+        { namespace: 'dwc', key: 'dig_number',         value: dig,               type: 'single_line_text_field' },
+        { namespace: 'dwc', key: 'wallco_design_id',   value: String(d.id),      type: 'single_line_text_field' },
+        ...(d.spoonflower_url ? [
+          { namespace: 'dwc', key: 'spoonflower_url',  value: d.spoonflower_url, type: 'url' },
+        ] : []),
+        ...(d.spoonflower_design_id ? [
+          { namespace: 'dwc', key: 'spoonflower_id',   value: String(d.spoonflower_design_id), type: 'single_line_text_field' },
+        ] : []),
+      ],
+    }
+  };
+
+  const url = `https://${store}/admin/api/2026-01/products.json`;
+  let r, body;
+  try {
+    r = await fetch(url, {
+      method: 'POST',
+      headers: {
+        'X-Shopify-Access-Token': token,
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify(productBody),
+    });
+    body = await r.json();
+  } catch (e) {
+    return res.status(502).json({ error: 'shopify fetch failed: ' + e.message });
+  }
+
+  if (!r.ok || !body || !body.product || !body.product.id) {
+    return res.status(502).json({
+      error: 'shopify create failed',
+      status: r.status,
+      shopify_response: body,
+    });
+  }
+
+  const sid = String(body.product.id);
+  const handle = body.product.handle || '';
+  const sUrl = `https://${store.replace('.myshopify.com','')}.myshopify.com/admin/products/${sid}`;
+  const publicUrl = handle ? `https://${store}/products/${handle}` : sUrl;
+
+  try {
+    psqlQuery(`UPDATE spoon_all_designs SET
+                 shopify_product_id   = ${pgEsc(sid)},
+                 shopify_product_url  = ${pgEsc(publicUrl)},
+                 shopify_uploaded_at  = NOW()
+               WHERE id = ${id};`);
+  } catch {}
+
+  res.json({
+    ok: true,
+    id,
+    dig_number: dig,
+    shopify_product_id: sid,
+    shopify_product_url: publicUrl,
+    shopify_admin_url: sUrl,
+    shopify_status: body.product.status,
+  });
+});
+
+// ── POST /api/spoonflower/launch-chrome — open dedicated debug Chrome for cookie reuse
+app.post('/api/spoonflower/launch-chrome', requireAdmin, (req, res) => {
+  try {
+    const out = execSync(`zsh ${path.join(__dirname, 'scripts', 'launch_spoon_chrome.sh')} 2>&1`,
+                         { encoding: 'utf8', timeout: 10000 });
+    res.json({ ok: true, output: out });
+  } catch (e) {
+    res.status(500).json({ ok: false, error: e.message, output: e.stdout || '' });
+  }
+});
+
+// ── GET /api/spoonflower/chrome-status — is debug Chrome reachable?
+app.get('/api/spoonflower/chrome-status', requireAdmin, async (_req, res) => {
+  try {
+    const r = await fetch('http://127.0.0.1:9222/json/version', { signal: AbortSignal.timeout(1500) });
+    if (r.ok) {
+      const j = await r.json();
+      return res.json({ ok: true, running: true, browser: j.Browser });
+    }
+    res.json({ ok: true, running: false });
+  } catch {
+    res.json({ ok: true, running: false });
+  }
+});
+
+// ── POST /api/spoonflower/pull-all — bulk pull DW account designs
+app.post('/api/spoonflower/pull-all', requireAdmin, (req, res) => {
+  const { limit = 0, account = 'dw' } = req.body || {};
+
+  let jobId;
+  try {
+    jobId = psqlQuery(`INSERT INTO spoonflower_jobs(kind, status, params)
+                       VALUES('pull_all', 'running',
+                       '${JSON.stringify({limit, account}).replace(/'/g,"''")}'::jsonb)
+                       RETURNING id;`);
+  } catch (e) { return res.status(500).json({ ok:false, error:'db: '+e.message }); }
+
+  const logFile = path.join(SF_JOBS_DIR, `pull_${jobId}.log`);
+  const args = [
+    path.join(__dirname, 'scripts', 'pull_spoonflower_account.py'),
+    '--account', String(account)
+  ];
+  if (limit && parseInt(limit,10) > 0) { args.push('--limit', String(parseInt(limit,10))); }
+
+  const proc = spawn('python3', args, {
+    cwd: __dirname,
+    env: { ...process.env, DATABASE_URL: DB_URL },
+    detached: true, stdio: ['ignore', 'pipe', 'pipe']
+  });
+  const out = fs.createWriteStream(logFile);
+  proc.stdout.pipe(out, { end:false });
+  proc.stderr.pipe(out, { end:false });
+  proc.on('exit', code => {
+    out.end();
+    try {
+      psqlQuery(`UPDATE spoonflower_jobs SET status='${code===0?'done':'error'}',
+                 finished_at=now(),
+                 log='${fs.readFileSync(logFile,'utf8').replace(/'/g,"''").slice(-8000)}'
+                 WHERE id=${jobId};`);
+    } catch {}
+  });
+  proc.unref();
+  res.json({ ok:true, job_id: jobId, log_url: `/api/spoonflower/status/${jobId}` });
+});
+
+// ── GET /api/spoonflower/status/:job — return job + log tail
+app.get('/api/spoonflower/status/:job', requireAdmin, (req, res) => {
+  const jobId = parseInt(req.params.job, 10);
+  let row;
+  try {
+    row = psqlQuery(`SELECT row_to_json(t) FROM (SELECT id,kind,design_id,status,dry_run,params,created_at,finished_at
+                     FROM spoonflower_jobs WHERE id=${jobId}) t;`);
+  } catch (e) { return res.status(500).json({ ok:false, error:'db: '+e.message }); }
+  if (!row) return res.status(404).json({ ok:false, error:'job not found' });
+
+  // Find log file
+  let log = '';
+  for (const f of ['upload', 'pull']) {
+    const p = path.join(SF_JOBS_DIR, `${f}_${jobId}.log`);
+    if (fs.existsSync(p)) { log = fs.readFileSync(p, 'utf8'); break; }
+  }
+  // Extract the Spoonflower design id + URL from the JSON-line log so the
+  // /design/:id frontend can render "Spoonflower pattern #N → view" after
+  // polling without re-reading the raw log.
+  let sf_design_id = null, sf_public_url = null;
+  for (const line of log.split('\n')) {
+    const s = line.trim();
+    if (!s.startsWith('{')) continue;
+    try {
+      const ev = JSON.parse(s);
+      if (ev.sf_design_id)  sf_design_id  = String(ev.sf_design_id);
+      if (ev.sf_public_url) sf_public_url = String(ev.sf_public_url);
+    } catch {}
+  }
+  res.type('application/json').send(JSON.stringify({
+    ok: true,
+    job: JSON.parse(row),
+    sf_design_id,
+    sf_public_url,
+    log_tail: log.slice(-8000)
+  }, null, 2));
+});
+
+// ── GET /spoonflower — admin page: pulled DW library + per-design publish buttons.
+// HARD-GATED 2026-05-13 — page was leaking the DW Spoonflower library publicly.
+// requireAdmin returns 404 to non-admins; admin can still reach via direct URL.
+app.get('/spoonflower', requireAdmin, (req, res) => {
+  let pulled = [];
+  try {
+    const raw = psqlQuery(`SELECT COALESCE(json_agg(t ORDER BY t.pulled_at DESC), '[]'::json) FROM (
+      SELECT id, sf_design_id, title, slug, design_url, thumbnail_url, local_path, file_bytes, account, pulled_at
+      FROM spoonflower_pulled_designs LIMIT 2000) t;`);
+    pulled = JSON.parse(raw || '[]');
+  } catch {}
+
+  let recentJobs = [];
+  try {
+    const raw = psqlQuery(`SELECT COALESCE(json_agg(t ORDER BY t.created_at DESC), '[]'::json) FROM (
+      SELECT id, kind, design_id, status, dry_run, created_at, finished_at
+      FROM spoonflower_jobs ORDER BY created_at DESC LIMIT 30) t;`);
+    recentJobs = JSON.parse(raw || '[]');
+  } catch {}
+
+  res.type('html').send(`${htmlHead({
+    title: 'Spoonflower Library — wallco.ai admin',
+    description: 'DW Spoonflower designs (pulled) + upload jobs',
+    canonical: 'https://wallco.ai/spoonflower'
+  })}
+<body>
+${htmlHeader('')}
+<main style="padding:40px">
+  <h1 style="font-family:'Cormorant Garamond',serif;font-weight:300;font-size:32px;margin:0 0 8px">DW Spoonflower Library</h1>
+  <p style="color:#666;margin:0 0 24px">${pulled.length} design${pulled.length===1?'':'s'} pulled · ${recentJobs.length} recent job${recentJobs.length===1?'':'s'}</p>
+
+  <div style="margin-bottom:14px;padding:14px;border:1px solid #ddd;border-radius:6px;background:#fafafa;font-size:13px;line-height:1.55">
+    <strong>One-time setup (proven pattern from months-ago working scraper):</strong>
+    <ol style="margin:8px 0 6px 22px;padding:0">
+      <li>Click <b>Launch Debug Chrome</b> below — opens a 2nd Chrome window (won't disturb your main Chrome).</li>
+      <li>Sign in once with <code>info@designerwallcoverings.com</code> / <code>*Spoonaccess911*</code>.</li>
+      <li>Come back here — <b>Pull ALL DW designs</b> will then attach to that signed-in window automatically.</li>
+    </ol>
+    <span id="chrome-status" style="color:#888">Checking debug-Chrome status…</span>
+  </div>
+  <div style="margin-bottom:24px;display:flex;gap:10px;flex-wrap:wrap">
+    <button id="btn-launch-chrome" class="btn-outline">Launch Debug Chrome</button>
+    <button id="btn-pull-test" class="btn-outline">Pull-all (limit 3, dry test)</button>
+    <button id="btn-pull-all"  class="btn-primary">Pull ALL DW designs</button>
+    <span id="pull-status" style="align-self:center;color:#666;font-size:13px"></span>
+  </div>
+
+  <h2 style="font-family:'Cormorant Garamond',serif;font-weight:400;font-size:22px;margin:32px 0 12px">Pulled designs</h2>
+  ${pulled.length === 0
+    ? '<p style="color:#999">No designs pulled yet. Click "Pull ALL DW designs" above to start.</p>'
+    : `<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:18px">
+        ${pulled.map(p => `<div style="border:1px solid rgba(0,0,0,.08);border-radius:6px;overflow:hidden;background:#fff">
+          ${p.thumbnail_url ? `<img src="${p.thumbnail_url}" style="width:100%;aspect-ratio:1;object-fit:cover" loading="lazy">` : '<div style="aspect-ratio:1;background:#eee"></div>'}
+          <div style="padding:10px 12px;font-size:12px">
+            <div style="font-weight:500">${(p.title||'(no title)').slice(0,48)}</div>
+            <div style="color:#888;margin-top:2px">SF#${p.sf_design_id}${p.file_bytes ? ' · '+Math.round(p.file_bytes/1024)+' KB' : ''}</div>
+            <a href="${p.design_url}" target="_blank" style="color:#888;font-size:11px" rel="noopener noreferrer nofollow">view on spoonflower →</a>
+          </div>
+        </div>`).join('')}
+      </div>`}
+
+  <h2 style="font-family:'Cormorant Garamond',serif;font-weight:400;font-size:22px;margin:40px 0 12px">Recent jobs</h2>
+  ${recentJobs.length === 0
+    ? '<p style="color:#999">No jobs yet.</p>'
+    : `<table style="width:100%;border-collapse:collapse;font-size:13px">
+        <thead><tr style="text-align:left;border-bottom:1px solid #ddd;color:#666">
+          <th style="padding:8px 6px">ID</th><th>Kind</th><th>Design</th><th>Status</th><th>Dry</th><th>Created</th><th></th>
+        </tr></thead><tbody>
+        ${recentJobs.map(j => `<tr style="border-bottom:1px solid #f0f0f0">
+          <td style="padding:8px 6px">${j.id}</td>
+          <td>${j.kind}</td>
+          <td>${j.design_id || '—'}</td>
+          <td><span style="padding:2px 6px;border-radius:3px;background:${j.status==='done'?'#d4edda':j.status==='error'?'#f8d7da':'#fff3cd'};color:${j.status==='done'?'#155724':j.status==='error'?'#721c24':'#856404'}">${j.status}</span></td>
+          <td>${j.dry_run ? 'yes' : '—'}</td>
+          <td style="color:#888">${j.created_at ? new Date(j.created_at).toLocaleString() : '—'}</td>
+          <td><a href="/api/spoonflower/status/${j.id}" target="_blank" style="color:#888" rel="noopener noreferrer">log →</a></td>
+        </tr>`).join('')}
+        </tbody></table>`}
+</main>
+${FOOTER}
+<script>
+async function post(u, b){ var r = await fetch(u,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b||{})}); return r.json(); }
+async function get(u){ var r = await fetch(u); return r.json(); }
+
+async function refreshChromeStatus() {
+  var el = document.getElementById('chrome-status');
+  try {
+    var j = await get('/api/spoonflower/chrome-status');
+    if (j.running) {
+      el.style.color = '#155724';
+      el.innerHTML = '✓ Debug Chrome running on :9222 (' + (j.browser||'Chrome') + ') — pulls will auto-attach.';
+    } else {
+      el.style.color = '#856404';
+      el.innerHTML = '⚠ Debug Chrome not running. Click "Launch Debug Chrome" to start it.';
+    }
+  } catch(e) {
+    el.style.color = '#c00'; el.textContent = 'Status check failed: ' + e.message;
+  }
+}
+refreshChromeStatus();
+setInterval(refreshChromeStatus, 5000);
+
+document.getElementById('btn-launch-chrome').addEventListener('click', async function(){
+  this.disabled = true;
+  var s = document.getElementById('pull-status');
+  s.textContent = 'Launching debug Chrome…';
+  var j = await post('/api/spoonflower/launch-chrome', {});
+  s.innerHTML = j.ok
+    ? '<pre style="margin:8px 0;font-size:11px;background:#000;color:#0f0;padding:8px;border-radius:4px;white-space:pre-wrap">'+(j.output||'').replace(/[<>]/g,'')+'</pre>'
+    : 'Launch failed: ' + (j.error||'unknown');
+  this.disabled = false;
+  setTimeout(refreshChromeStatus, 2000);
+});
+document.getElementById('btn-pull-test').addEventListener('click', async function(){
+  this.disabled=true;
+  document.getElementById('pull-status').textContent = 'Starting limited pull (3 designs)…';
+  var j = await post('/api/spoonflower/pull-all', { limit: 3, account: 'dw' });
+  document.getElementById('pull-status').innerHTML = j.ok
+    ? 'Job '+j.job_id+' running — <a href="/api/spoonflower/status/'+j.job_id+'" target="_blank" rel="noopener noreferrer">view log</a>. Reload in ~60s.'
+    : 'Failed: ' + (j.error||'unknown');
+  this.disabled=false;
+});
+document.getElementById('btn-pull-all').addEventListener('click', async function(){
+  if (!confirm('Pull ALL DW Spoonflower designs? This may take 10-30+ minutes and downloads every file to disk.')) return;
+  this.disabled=true;
+  document.getElementById('pull-status').textContent = 'Starting full pull…';
+  var j = await post('/api/spoonflower/pull-all', { limit: 0, account: 'dw' });
+  document.getElementById('pull-status').innerHTML = j.ok
+    ? 'Job '+j.job_id+' running — <a href="/api/spoonflower/status/'+j.job_id+'" target="_blank" rel="noopener noreferrer">view log</a>. Reload page to refresh table.'
+    : 'Failed: ' + (j.error||'unknown');
+  this.disabled=false;
+});
+</script>
+${HAMBURGER_JS}
+</body>
+</html>`);
+});
+
+// ── GET /api/voice/preview — short ElevenLabs narration for a design
+app.get('/api/voice/preview', async (req, res) => {
+  if (!ELEVEN_KEY) return res.status(500).type('text/plain').send('ELEVENLABS_API_KEY not configured');
+  const d = findDesign(req.query.design_id);
+  if (!d) return res.status(404).type('text/plain').send('design not found');
+  const text = `${d.title}. An AI-original wallpaper pattern by wallco. Category: ${d.category}. Dominant color: ${d.dominant_hex}. Generated by ${d.generator}, seed ${d.seed}. Every pattern is unique, never repeated.`;
+  try {
+    const r = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${ELEVEN_VOICE_ID}`, {
+      method: 'POST',
+      headers: {
+        'xi-api-key': ELEVEN_KEY,
+        'Content-Type': 'application/json',
+        'Accept': 'audio/mpeg'
+      },
+      body: JSON.stringify({
+        text,
+        model_id: 'eleven_turbo_v2_5',
+        voice_settings: { stability: 0.4, similarity_boost: 0.75 }
+      })
+    });
+    if (!r.ok) {
+      const errText = await r.text();
+      return res.status(502).type('text/plain').send('elevenlabs: '+errText.slice(0,400));
+    }
+    res.setHeader('Content-Type', 'audio/mpeg');
+    res.setHeader('Cache-Control', 'no-store');
+    const buf = Buffer.from(await r.arrayBuffer());
+    res.end(buf);
+  } catch (e) {
+    res.status(500).type('text/plain').send('voice error: '+e.message);
+  }
+});
+
+// ─────────────────────────────────────────────────────────────────────────
+// WALLCO GENERATOR (30-min cron + recipe picker)
+// ─────────────────────────────────────────────────────────────────────────
+app.get('/api/generator/recipes', (_req, res) => {
+  try {
+    const raw = psqlQuery(`SELECT COALESCE(json_agg(t ORDER BY t.id), '[]'::json) FROM (
+      SELECT id, name, source_artists, style_tags, color_hex_seeds,
+             prompt_template, weight, enabled, last_run_at, run_count, created_at
+      FROM wallco_generator_recipe) t;`);
+    res.json(JSON.parse(raw || '[]'));
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+app.post('/api/generator/recipes', (req, res) => {
+  const { id, name, source_artists = [], style_tags = [],
+          color_hex_seeds = [], prompt_template = null,
+          weight = 1, enabled = true } = req.body || {};
+  if (!name) return res.status(400).json({ error: 'name required' });
+  const arrLit = a => `ARRAY[${(a || []).map(v => "'" + String(v).replace(/'/g, "''") + "'").join(',') || ''}]::text[]`;
+  const tpl = prompt_template ? `'${prompt_template.replace(/'/g, "''")}'` : 'NULL';
+  try {
+    let result;
+    if (id) {
+      result = psqlQuery(`UPDATE wallco_generator_recipe SET
+        name='${name.replace(/'/g,"''")}',
+        source_artists=${arrLit(source_artists)},
+        style_tags=${arrLit(style_tags)},
+        color_hex_seeds=${arrLit(color_hex_seeds)},
+        prompt_template=${tpl},
+        weight=${parseInt(weight,10) || 1},
+        enabled=${enabled ? 'TRUE' : 'FALSE'}
+        WHERE id=${parseInt(id,10)} RETURNING id;`);
+    } else {
+      result = psqlQuery(`INSERT INTO wallco_generator_recipe
+        (name, source_artists, style_tags, color_hex_seeds, prompt_template, weight, enabled)
+        VALUES ('${name.replace(/'/g,"''")}',
+          ${arrLit(source_artists)}, ${arrLit(style_tags)}, ${arrLit(color_hex_seeds)},
+          ${tpl}, ${parseInt(weight,10) || 1}, ${enabled ? 'TRUE' : 'FALSE'})
+        RETURNING id;`);
+    }
+    res.json({ ok: true, id: parseInt(result, 10) });
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+app.delete('/api/generator/recipes/:id', (req, res) => {
+  try {
+    psqlQuery(`DELETE FROM wallco_generator_recipe WHERE id=${parseInt(req.params.id,10)};`);
+    res.json({ ok: true });
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+// One-step batch generate: colors + elements + styles + base patterns → SDXL
+app.post('/api/generator/batch', (req, res) => {
+  const { colors = '', elements = [], styles = [], patterns = [], extra_text = '' } = req.body || {};
+  // Build the prompt
+  const artistLine  = patterns.map(p => p.artist).filter(Boolean).slice(0, 3);
+  const patternTitles = patterns.map(p => p.title).filter(Boolean).slice(0, 3);
+  const patternHex  = patterns.map(p => p.dominant_hex).filter(Boolean);
+  const allColors = [colors.trim(), ...patternHex.slice(0, 4)].filter(Boolean).join(', ');
+  const promptParts = [
+    'A seamless wallpaper pattern',
+    artistLine.length ? `in the visual lineage of ${artistLine.join(' and ')}` : '',
+    styles.length    ? `with ${styles.slice(0,4).join(' / ')} influence` : '',
+    elements.length  ? `featuring ${elements.slice(0,8).join(', ')}` : '',
+    patternTitles.length ? `referencing the look of ${patternTitles.join(' / ')}` : '',
+    allColors        ? `palette ${allColors}` : '',
+    extra_text || '',
+    'high detail, museum-quality, 24x24 inch tileable, no text, no signature.'
+  ].filter(Boolean);
+  const prompt = promptParts.join(', ').replace(/\s+/g, ' ').trim();
+
+  try {
+    const runId = psqlQuery(`INSERT INTO wallco_generator_runs(recipe_id, prompt, palette_seed_ids, status)
+      VALUES(NULL, '${prompt.replace(/'/g,"''")}', '{}'::bigint[], 'running') RETURNING id;`);
+
+    const category = (styles[0] || 'mixed').toLowerCase();
+    const r = spawnSync('node', [
+      path.join(__dirname, 'scripts', 'generate_designs.js'),
+      '--n', '1', '--kind', 'seamless_tile',
+      '--category', category,
+      '--prompts', prompt,
+    ], { cwd: __dirname, env: { ...process.env, GEN_BACKEND: process.env.GEN_BACKEND || 'replicate' },
+         encoding: 'utf8', timeout: 120000 });
+
+    let designId = null, filename = null, domHex = null;
+    try {
+      const row = psqlQuery(`SELECT id::text || '|' || COALESCE(local_path,'') || '|' || COALESCE(dominant_hex,'')
+        FROM spoon_all_designs ORDER BY id DESC LIMIT 1;`);
+      const [idStr, lp, dh] = row.split('|');
+      designId = parseInt(idStr, 10);
+      filename = lp ? path.basename(lp) : null;
+      domHex = dh || null;
+    } catch {}
+
+    const ok = r.status === 0;
+    psqlQuery(`UPDATE wallco_generator_runs SET
+      status='${ok ? 'done' : 'error'}',
+      design_id=${designId || 'NULL'},
+      error=${ok ? 'NULL' : "'"+((r.stderr||r.stdout||'').slice(-300).replace(/'/g,"''"))+"'"},
+      finished_at=now() WHERE id=${runId};`);
+
+    res.json({ ok, design_id: designId, run_id: parseInt(runId,10),
+               prompt, filename, dominant_hex: domHex,
+               error: ok ? null : (r.stderr || r.stdout || '').slice(-400) });
+  } catch (e) { res.status(500).json({ ok:false, error: e.message }); }
+});
+
+app.post('/api/generator/run-now', (req, res) => {
+  // Fire-and-return — generator_tick.js runs ~10-30s and the UI doesn't
+  // need to block the click. Buffer the tail of stdout so the response
+  // can include a snippet; finalize via the runs list later.
+  const logFile = path.join(__dirname, 'data', 'logs', `run-now-${Date.now()}.log`);
+  fs.mkdirSync(path.dirname(logFile), { recursive: true });
+  const out = fs.openSync(logFile, 'a');
+  const proc = spawn('node', [path.join(__dirname, 'scripts', 'generator_tick.js')], {
+    cwd: __dirname,
+    detached: true,
+    stdio: ['ignore', out, out],
+  });
+  proc.unref();
+  res.json({
+    ok: true,
+    started_at: new Date().toISOString(),
+    log: path.relative(__dirname, logFile),
+    note: 'tick kicked — check the Runs list in ~30-60s, or tail data/logs/' + path.basename(logFile),
+  });
+});
+
+app.post('/api/generator/refresh-orders', (req, res) => {
+  // Same bug as run-now had: detached + pipe + waiting on 'exit' hangs
+  // forever because the parent owns the pipes but the child is detached.
+  // Two-mode now:
+  //   sync=1 (default for the UI button) — short timeout, stream output
+  //     back inline so the alert() can show real success/failure
+  //   sync=0 — fire & return immediately with a log handle
+  const limit = parseInt(req.body?.limit || '50', 10);
+  const sync = req.body?.sync === false ? false : true;
+  if (!sync) {
+    const logFile = path.join(__dirname, 'data', 'logs', `refresh-orders-${Date.now()}.log`);
+    fs.mkdirSync(path.dirname(logFile), { recursive: true });
+    const out = fs.openSync(logFile, 'a');
+    const proc = spawn('node',
+      [path.join(__dirname, 'scripts', 'fetch_recent_orders_palette.js'), '--limit', String(limit)],
+      { cwd: __dirname, detached: true, stdio: ['ignore', out, out] });
+    proc.unref();
+    return res.json({ ok: true, started_at: new Date().toISOString(), log: path.relative(__dirname, logFile), async: true });
+  }
+  // Sync path — block until the script exits or 2 min, then return its output
+  let buf = '';
+  const proc = spawn('node',
+    [path.join(__dirname, 'scripts', 'fetch_recent_orders_palette.js'), '--limit', String(limit)],
+    { cwd: __dirname, stdio: ['ignore', 'pipe', 'pipe'] });
+  proc.stdout.on('data', d => buf += d);
+  proc.stderr.on('data', d => buf += d);
+  const killer = setTimeout(() => { try { proc.kill('SIGKILL'); } catch {} }, 120_000);
+  proc.on('exit', code => {
+    clearTimeout(killer);
+    res.json({ ok: code === 0, exit: code, output: buf.slice(-3000) });
+  });
+  proc.on('error', err => {
+    clearTimeout(killer);
+    res.json({ ok: false, error: err.message, output: buf.slice(-3000) });
+  });
+});
+
+app.get('/api/generator/runs', (_req, res) => {
+  try {
+    const raw = psqlQuery(`SELECT COALESCE(json_agg(t ORDER BY t.started_at DESC), '[]'::json) FROM (
+      SELECT r.id, r.recipe_id, rc.name AS recipe_name, r.design_id,
+             LEFT(r.prompt, 180) AS prompt, r.status, r.error,
+             r.started_at, r.finished_at
+      FROM wallco_generator_runs r
+      LEFT JOIN wallco_generator_recipe rc ON rc.id = r.recipe_id
+      ORDER BY r.started_at DESC LIMIT 30) t;`);
+    res.json(JSON.parse(raw || '[]'));
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+app.get('/api/generator/palette', (_req, res) => {
+  try {
+    const raw = psqlQuery(`SELECT COALESCE(json_agg(t ORDER BY t.ordered_at DESC), '[]'::json) FROM (
+      SELECT id, sku, product_title, vendor, dominant_hex, style_tags, ordered_at, image_url
+      FROM wallco_recent_orders_palette
+      WHERE dominant_hex IS NOT NULL
+      ORDER BY ordered_at DESC LIMIT 40) t;`);
+    res.json(JSON.parse(raw || '[]'));
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+app.post('/api/generator/cron', (req, res) => {
+  const { action } = req.body || {};
+  const plist = `${require('os').homedir()}/Library/LaunchAgents/com.steve.wallco-generator.plist`;
+  if (!['start', 'stop', 'status'].includes(action)) {
+    return res.status(400).json({ error: 'action must be start|stop|status' });
+  }
+  try {
+    if (action === 'start') {
+      execSync(`launchctl bootstrap gui/$(id -u) "${plist}" 2>&1 || launchctl load "${plist}" 2>&1 || true`);
+      const listed = execSync(`launchctl list | grep com.steve.wallco-generator || echo "(not loaded)"`, { encoding: 'utf8' }).trim();
+      res.json({ ok: true, action, status: listed });
+    } else if (action === 'stop') {
+      execSync(`launchctl bootout gui/$(id -u)/com.steve.wallco-generator 2>&1 || launchctl unload "${plist}" 2>&1 || true`);
+      res.json({ ok: true, action });
+    } else {
+      const listed = execSync(`launchctl list | grep com.steve.wallco-generator || echo "(not loaded)"`, { encoding: 'utf8' }).trim();
+      res.json({ ok: true, action, status: listed });
+    }
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+// ── /generator admin page
+app.get('/generator', (_req, res) => {
+  res.type('html').send(`${htmlHead({
+    title: 'Generator — wallco.ai admin',
+    description: '30-min recipe-driven pattern generator',
+    canonical: 'https://wallco.ai/generator'
+  })}
+<body>
+${htmlHeader('')}
+<main style="padding:32px 40px;max-width:1300px;margin:0 auto">
+  <h1 style="font-family:'Cormorant Garamond',serif;font-weight:300;font-size:32px;margin:0 0 4px">Pattern Generator</h1>
+  <p style="color:#666;margin:0 0 28px">Every 30 min, the cron picks a recipe → fetches a palette from recent DW Shopify orders → renders a new design.</p>
+
+  <div style="display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-bottom:28px">
+    <div style="padding:18px;border:1px solid #ddd;border-radius:8px;background:#fafafa">
+      <h2 style="margin:0 0 8px;font-size:16px">30-minute cron</h2>
+      <p id="cron-status" style="margin:0 0 10px;font-size:13px;color:#555">Checking…</p>
+      <div style="display:flex;gap:8px;flex-wrap:wrap">
+        <button class="btn-primary" id="btn-cron-start">Start cron</button>
+        <button class="btn-outline" id="btn-cron-stop">Stop cron</button>
+        <button class="btn-outline" id="btn-run-now">Run tick NOW</button>
+      </div>
+    </div>
+    <div style="padding:18px;border:1px solid #ddd;border-radius:8px;background:#fafafa">
+      <h2 style="margin:0 0 8px;font-size:16px">DW orders palette</h2>
+      <p id="palette-status" style="margin:0 0 10px;font-size:13px;color:#555">Loading…</p>
+      <button class="btn-outline" id="btn-refresh-orders">Refresh from Shopify (last 50 orders)</button>
+    </div>
+  </div>
+
+  <!-- ── ONE-STEP BATCH GENERATE ────────────────────────────────────────── -->
+  <div style="margin-bottom:28px;padding:20px;border:1px solid #1a1a1a;border-radius:10px;background:#1a1a1a;color:#f0eadc">
+    <h2 style="margin:0 0 4px;font-family:'Cormorant Garamond',serif;font-weight:300;font-size:24px;color:#d2b15c">Batch generate · one step</h2>
+    <p style="margin:0 0 16px;font-size:13px;opacity:.75">Pick how many · pick colors · pick elements · pick styles · pick base patterns · go.</p>
+
+    <div style="display:grid;grid-template-columns:80px 1fr;gap:10px 18px;align-items:center;margin-bottom:14px">
+      <label style="font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#aaa">Count</label>
+      <input id="batch-n" type="number" min="1" max="50" value="10" style="width:80px;padding:6px 10px;background:#0f0e0c;color:#fff;border:1px solid #444;border-radius:4px;font-family:inherit;font-size:14px">
+
+      <label style="font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#aaa">Colors</label>
+      <div>
+        <input id="batch-colors" type="text" placeholder="e.g. terracotta, sage, midnight blue, gold" style="width:100%;padding:8px 10px;background:#0f0e0c;color:#fff;border:1px solid #444;border-radius:4px;font-family:inherit;font-size:13px">
+        <div id="batch-color-suggest" style="font-size:11px;color:#888;margin-top:4px"></div>
+      </div>
+
+      <label style="font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#aaa">Elements</label>
+      <div id="batch-elements-picker" style="display:flex;flex-wrap:wrap;gap:5px;max-height:120px;overflow-y:auto;padding:4px"></div>
+
+      <label style="font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#aaa">Styles</label>
+      <div id="batch-styles-picker" style="display:flex;flex-wrap:wrap;gap:5px;max-height:80px;overflow-y:auto;padding:4px"></div>
+
+      <label style="font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#aaa">Patterns</label>
+      <div>
+        <input id="batch-patterns" type="text" placeholder="Search base patterns (peacock, baroque, Morris…)" style="width:100%;padding:6px 10px;background:#0f0e0c;color:#fff;border:1px solid #444;border-radius:4px;font-family:inherit;font-size:13px">
+        <div id="batch-patterns-results" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(72px,1fr));gap:4px;max-height:140px;overflow-y:auto;margin-top:6px"></div>
+        <div id="batch-patterns-picked" style="display:flex;flex-wrap:wrap;gap:4px;margin-top:6px"></div>
+      </div>
+    </div>
+
+    <div style="display:flex;gap:10px;align-items:center;margin-top:12px">
+      <button id="btn-batch-go" style="padding:12px 28px;background:#d2b15c;color:#1a1a1a;border:0;border-radius:6px;font-weight:600;font-size:14px;cursor:pointer;font-family:inherit">Generate batch</button>
+      <span id="batch-status" style="font-size:12px;color:#aaa"></span>
+    </div>
+    <div id="batch-results" style="margin-top:14px;display:grid;grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:8px"></div>
+  </div>
+
+  <h2 style="font-family:'Cormorant Garamond',serif;font-weight:400;font-size:22px;margin:24px 0 10px">Recipes</h2>
+  <div id="recipes" style="margin-bottom:16px"></div>
+
+  <details style="margin-bottom:32px;padding:14px;border:1px solid #ddd;border-radius:8px;background:#fff">
+    <summary style="cursor:pointer;font-weight:500">+ Add new recipe</summary>
+    <form id="recipe-form" style="margin-top:14px;display:grid;gap:10px;max-width:680px">
+      <input name="name" placeholder="Recipe name (e.g., Botanical × Damask Mix)" required style="padding:10px;border:1px solid #ccc;border-radius:4px;font-size:14px">
+      <textarea name="source_artists" placeholder="Source artists, comma-separated" rows="2" style="padding:10px;border:1px solid #ccc;border-radius:4px;font-size:14px"></textarea>
+      <textarea name="style_tags" placeholder="Style tags, comma-separated (damask, floral, geometric, toile)" rows="2" style="padding:10px;border:1px solid #ccc;border-radius:4px;font-size:14px"></textarea>
+      <input name="color_hex_seeds" placeholder="Optional color hex seeds (#ab1234, #cd5678)" style="padding:10px;border:1px solid #ccc;border-radius:4px;font-size:14px">
+      <textarea name="prompt_template" placeholder="Prompt template (use {artist}, {style}, {palette})" rows="3" style="padding:10px;border:1px solid #ccc;border-radius:4px;font-size:14px;font-family:monospace"></textarea>
+      <button type="submit" class="btn-primary">Save recipe</button>
+    </form>
+  </details>
+
+  <h2 style="font-family:'Cormorant Garamond',serif;font-weight:400;font-size:22px;margin:24px 0 10px">Recent runs</h2>
+  <div id="runs"></div>
+
+  <h2 style="font-family:'Cormorant Garamond',serif;font-weight:400;font-size:22px;margin:32px 0 6px">DW Orders palette (last 40)</h2>
+  <div style="display:flex;gap:14px;align-items:center;margin:6px 0 12px;font:11px var(--sans,system-ui)">
+    <span id="palette-status-2" style="color:#555">Loading…</span>
+    <label style="display:inline-flex;align-items:center;gap:8px;letter-spacing:.08em;text-transform:uppercase;color:#888;margin-left:auto">Tile size
+      <input type="range" id="palette-density" min="80" max="240" step="20" value="160" style="flex:0 0 120px">
+    </label>
+    <span id="palette-density-readout" style="font-variant-numeric:tabular-nums;color:#555">160px</span>
+  </div>
+  <div id="palette" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:10px"></div>
+  <script>
+  (function(){
+    var sl = document.getElementById('palette-density');
+    var grid = document.getElementById('palette');
+    var ro = document.getElementById('palette-density-readout');
+    var saved = parseInt(localStorage.getItem('wallco-palette-density')||'',10);
+    if (saved >= 80 && saved <= 240) sl.value = saved;
+    function paint(v){ grid.style.gridTemplateColumns = 'repeat(auto-fill, minmax('+v+'px, 1fr))'; ro.textContent = v+'px'; }
+    paint(sl.value);
+    sl.addEventListener('input', function(){ paint(this.value); localStorage.setItem('wallco-palette-density', this.value); });
+  })();
+  </script>
+</main>
+${FOOTER}
+
+<script>
+async function get(u){ return (await fetch(u)).json(); }
+async function post(u,b){ return (await fetch(u,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b||{})})).json(); }
+async function del(u){ return (await fetch(u,{method:'DELETE'})).json(); }
+
+async function loadCron() {
+  var j = await post('/api/generator/cron', { action: 'status' });
+  var el = document.getElementById('cron-status');
+  el.innerHTML = j.status && !j.status.includes('not loaded')
+    ? '<span style="color:#155724;font-weight:500">✓ LOADED</span> — <code style="font-size:11px">'+j.status+'</code>'
+    : '<span style="color:#856404;font-weight:500">○ STOPPED</span> — click Start cron to enable 30-min auto-generation.';
+}
+async function loadPaletteStatus() {
+  var p = await get('/api/generator/palette');
+  var msg = p.length + ' palette swatches from recent DW orders';
+  var s1 = document.getElementById('palette-status'); if (s1) s1.textContent = msg;
+  var s2 = document.getElementById('palette-status-2'); if (s2) s2.textContent = msg;
+  document.getElementById('palette').innerHTML = p.map(function(x){
+    var title = (x.product_title||x.sku||'').replace(/"/g,'&quot;');
+    var tagSummary = ((x.style_tags||[]).slice(0,3).join(' · '));
+    return '<button type="button" data-hex="'+x.dominant_hex+'" title="'+title+(tagSummary?' — '+tagSummary:'')+'"' +
+      ' onclick="(function(h){var el=document.getElementById(\\'batch-colors\\');if(!el)return;var cur=el.value.trim();el.value=cur?(cur+\\', \\'+h):h;el.focus();})(this.dataset.hex)"' +
+      ' style="text-align:left;background:transparent;border:0;padding:0;cursor:pointer">' +
+      '<div style="aspect-ratio:1;background:'+x.dominant_hex+';border:1px solid rgba(0,0,0,.08);border-radius:6px;transition:transform .15s"></div>' +
+      '<div style="font-size:10px;color:#666;margin-top:4px;font-family:ui-monospace,monospace">'+x.dominant_hex+'</div>' +
+      '<div style="font-size:10px;color:#333;margin-top:1px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+title+'</div>' +
+      (x.vendor ? '<div style="font-size:9px;color:#999;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+x.vendor+'</div>' : '') +
+      '</button>';
+  }).join('') || '<p style="color:#999;grid-column:1/-1">No palette data yet. Click "Refresh from Shopify" above.</p>';
+}
+async function loadRecipes() {
+  var r = await get('/api/generator/recipes');
+  document.getElementById('recipes').innerHTML = r.map(function(x){
+    var artists = (x.source_artists||[]).join(', ');
+    var styles  = (x.style_tags||[]).join(', ');
+    var ran = x.last_run_at ? new Date(x.last_run_at).toLocaleString() : 'never';
+    return '<div style="padding:14px;border:1px solid #ddd;border-radius:6px;margin-bottom:10px;background:'+(x.enabled?'#fff':'#f5f5f5')+'">' +
+      '<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:14px">' +
+        '<div style="flex:1">' +
+          '<div style="font-weight:500;font-size:15px">'+x.name+(x.enabled?'':' <span style="color:#999;font-size:11px;font-weight:400">(disabled)</span>')+'</div>' +
+          '<div style="font-size:12px;color:#666;margin-top:4px"><b>Artists:</b> '+artists+'</div>' +
+          '<div style="font-size:12px;color:#666"><b>Styles:</b> '+styles+'</div>' +
+          (x.prompt_template ? '<div style="font-size:11px;color:#888;margin-top:6px;font-family:monospace;background:#fafafa;padding:6px;border-radius:3px">'+x.prompt_template.replace(/[<>]/g,'')+'</div>' : '') +
+          '<div style="font-size:11px;color:#999;margin-top:4px">Last run: '+ran+' · '+(x.run_count||0)+' runs</div>' +
+        '</div>' +
+        '<div style="display:flex;flex-direction:column;gap:4px">' +
+          '<button onclick="toggleRecipe('+x.id+','+(!x.enabled)+')" style="padding:4px 10px;font-size:12px">'+(x.enabled?'Disable':'Enable')+'</button>' +
+          '<button onclick="deleteRecipe('+x.id+')" style="padding:4px 10px;font-size:12px;color:#c00">Delete</button>' +
+        '</div>' +
+      '</div></div>';
+  }).join('') || '<p style="color:#999">No recipes yet — add one below.</p>';
+}
+async function loadRuns() {
+  var r = await get('/api/generator/runs');
+  document.getElementById('runs').innerHTML = r.length ? '<table style="width:100%;border-collapse:collapse;font-size:13px"><thead><tr style="text-align:left;border-bottom:1px solid #ddd"><th style="padding:6px">When</th><th>Recipe</th><th>Status</th><th>Design</th><th>Prompt</th></tr></thead><tbody>' +
+    r.map(function(x){
+      var when = x.started_at ? new Date(x.started_at).toLocaleString() : '?';
+      var statusBg = x.status==='done'?'#d4edda':x.status==='error'?'#f8d7da':'#fff3cd';
+      var statusFg = x.status==='done'?'#155724':x.status==='error'?'#721c24':'#856404';
+      return '<tr style="border-bottom:1px solid #f0f0f0"><td style="padding:8px 4px;color:#888">'+when+'</td><td>'+(x.recipe_name||'—')+'</td><td><span style="padding:2px 6px;border-radius:3px;background:'+statusBg+';color:'+statusFg+'">'+x.status+'</span></td><td>'+(x.design_id?'<a href="/design/'+x.design_id+'">#'+x.design_id+'</a>':'—')+'</td><td style="font-size:11px;color:#666;max-width:380px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+(x.prompt||'').slice(0,80)+'</td></tr>';
+    }).join('') + '</tbody></table>' : '<p style="color:#999">No runs yet.</p>';
+}
+
+window.toggleRecipe = async function(id, on){
+  var rs = await get('/api/generator/recipes');
+  var r = rs.find(function(x){return x.id===id;});
+  if (!r) return;
+  r.enabled = on;
+  await post('/api/generator/recipes', r);
+  loadRecipes();
+};
+window.deleteRecipe = async function(id){
+  if (!confirm('Delete this recipe?')) return;
+  await del('/api/generator/recipes/'+id);
+  loadRecipes();
+};
+
+document.getElementById('btn-cron-start').addEventListener('click', async function(){
+  await post('/api/generator/cron', { action: 'start' });
+  setTimeout(loadCron, 600);
+});
+document.getElementById('btn-cron-stop').addEventListener('click', async function(){
+  await post('/api/generator/cron', { action: 'stop' });
+  setTimeout(loadCron, 600);
+});
+document.getElementById('btn-run-now').addEventListener('click', async function(){
+  this.disabled = true;
+  this.textContent = 'Running…';
+  var j = await post('/api/generator/run-now');
+  this.disabled = false;
+  this.textContent = 'Run tick NOW';
+  alert((j.ok?'OK':'FAIL')+'\\n\\n'+(j.output||'').slice(-800));
+  loadRuns();
+});
+document.getElementById('btn-refresh-orders').addEventListener('click', async function(){
+  this.disabled = true;
+  this.textContent = 'Fetching…';
+  var j = await post('/api/generator/refresh-orders', { limit: 50 });
+  this.disabled = false;
+  this.textContent = 'Refresh from Shopify (last 50 orders)';
+  alert((j.ok?'OK':'FAIL')+'\\n\\n'+(j.output||'').slice(-800));
+  loadPaletteStatus();
+});
+
+document.getElementById('recipe-form').addEventListener('submit', async function(e){
+  e.preventDefault();
+  var fd = new FormData(e.target);
+  var body = {
+    name: fd.get('name'),
+    source_artists: (fd.get('source_artists')||'').split(',').map(function(s){return s.trim();}).filter(Boolean),
+    style_tags: (fd.get('style_tags')||'').split(',').map(function(s){return s.trim();}).filter(Boolean),
+    color_hex_seeds: (fd.get('color_hex_seeds')||'').split(',').map(function(s){return s.trim();}).filter(Boolean),
+    prompt_template: fd.get('prompt_template') || null,
+    enabled: true
+  };
+  await post('/api/generator/recipes', body);
+  e.target.reset();
+  loadRecipes();
+});
+
+// ── ONE-STEP BATCH GENERATE ────────────────────────────────────────────
+var BATCH = { elements: new Set(), styles: new Set(), patterns: [] };
+
+async function loadBatchPickers(){
+  var [styles, elements] = await Promise.all([
+    fetch('/api/studio/styles').then(r=>r.json()),
+    fetch('/api/studio/elements').then(r=>r.json())
+  ]);
+  document.getElementById('batch-styles-picker').innerHTML = styles.map(function(s){
+    return '<button onclick="toggleBatchStyle(\''+(s.name.replace(/[\\\\\']/g,''))+'\')" data-style="'+s.name+'" style="padding:3px 9px;font-size:11px;border-radius:11px;border:1px solid #555;background:#0f0e0c;color:#aaa;cursor:pointer">'+s.name+'</button>';
+  }).join('');
+  // Group elements by category for tighter display
+  var byCat = {};
+  elements.forEach(function(e){ (byCat[e.category]=byCat[e.category]||[]).push(e); });
+  document.getElementById('batch-elements-picker').innerHTML = Object.keys(byCat).sort().map(function(c){
+    return '<div style="width:100%;font-size:9px;text-transform:uppercase;color:#666;letter-spacing:.08em;margin-top:4px">'+c+'</div>' +
+      byCat[c].map(function(e){
+        return '<button onclick="toggleBatchElement(\''+e.name+'\')" data-el="'+e.name+'" style="padding:2px 8px;font-size:11px;border-radius:9px;border:1px solid #555;background:#0f0e0c;color:#aaa;cursor:pointer;margin:1px">'+e.name+'</button>';
+      }).join('');
+  }).join('');
+  // Suggest 3 starter palettes for color hint
+  var pj = await fetch('/api/studio/suggest-palettes',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'}).then(r=>r.json());
+  document.getElementById('batch-color-suggest').innerHTML =
+    'Suggested: ' + (pj.palettes||[]).slice(0,3).map(function(p){
+      return '<span style="cursor:pointer;text-decoration:underline" onclick="document.getElementById(\'batch-colors\').value=\''+(p.colors||[]).join(', ')+'\'">'+p.name+'</span>';
+    }).join(' · ');
+}
+
+window.toggleBatchElement = function(name){
+  if (BATCH.elements.has(name)) BATCH.elements.delete(name); else BATCH.elements.add(name);
+  var btn = document.querySelector('button[data-el="'+name+'"]');
+  if (btn) {
+    var on = BATCH.elements.has(name);
+    btn.style.background = on ? '#d2b15c' : '#0f0e0c';
+    btn.style.color = on ? '#1a1a1a' : '#aaa';
+    btn.style.borderColor = on ? '#d2b15c' : '#555';
+  }
+};
+window.toggleBatchStyle = function(name){
+  if (BATCH.styles.has(name)) BATCH.styles.delete(name); else BATCH.styles.add(name);
+  var btn = document.querySelector('button[data-style="'+name.replace(/'/g,"'")+'"]');
+  if (btn) {
+    var on = BATCH.styles.has(name);
+    btn.style.background = on ? '#d2b15c' : '#0f0e0c';
+    btn.style.color = on ? '#1a1a1a' : '#aaa';
+    btn.style.borderColor = on ? '#d2b15c' : '#555';
+  }
+};
+
+document.getElementById('batch-patterns').addEventListener('input', async function(e){
+  var q = e.target.value.trim();
+  if (q.length < 2) { document.getElementById('batch-patterns-results').innerHTML=''; return; }
+  var r = await fetch('/api/studio/search?q='+encodeURIComponent(q)+'&limit=24').then(x=>x.json());
+  document.getElementById('batch-patterns-results').innerHTML = r.map(function(x){
+    return '<div onclick="pickBatchPattern('+JSON.stringify(x).replace(/"/g,'&quot;').replace(/'/g,'\\\\\'')+')" style="cursor:pointer;border:1px solid #444;border-radius:4px;padding:2px;background:#0f0e0c" title="'+((x.title||'').slice(0,60))+'">'+
+      (x.thumb_url?'<img src="'+x.thumb_url+'" loading="lazy" style="width:100%;aspect-ratio:1;object-fit:cover;border-radius:3px">':'<div style="aspect-ratio:1;background:'+(x.dominant_hex||'#222')+'"></div>')+
+      '</div>';
+  }).join('');
+});
+window.pickBatchPattern = function(p){
+  if (BATCH.patterns.find(function(x){return x.id===p.id && x.source===p.source;})) return;
+  BATCH.patterns.push(p);
+  renderBatchPicked();
+};
+window.unpickBatchPattern = function(idx){
+  BATCH.patterns.splice(idx, 1);
+  renderBatchPicked();
+};
+function renderBatchPicked(){
+  document.getElementById('batch-patterns-picked').innerHTML = BATCH.patterns.map(function(p,i){
+    return '<div style="display:flex;align-items:center;gap:5px;padding:3px 8px;background:#d2b15c;color:#1a1a1a;border-radius:11px;font-size:11px"><span>'+((p.title||'pattern').slice(0,18))+'</span><span onclick="unpickBatchPattern('+i+')" style="cursor:pointer;font-weight:700">✕</span></div>';
+  }).join('');
+}
+
+document.getElementById('btn-batch-go').addEventListener('click', async function(){
+  var n = Math.max(1, Math.min(50, parseInt(document.getElementById('batch-n').value, 10) || 1));
+  var colors = document.getElementById('batch-colors').value.trim();
+  var elements = Array.from(BATCH.elements);
+  var styles = Array.from(BATCH.styles);
+  var patterns = BATCH.patterns;
+  if (!colors && !elements.length && !styles.length && !patterns.length) {
+    if (!confirm('No selections — use the default Morris/Merian recipe for all ' + n + ' designs?')) return;
+  }
+  this.disabled = true;
+  var s = document.getElementById('batch-status');
+  document.getElementById('batch-results').innerHTML = '';
+  for (var i = 1; i <= n; i++) {
+    s.textContent = 'Generating ' + i + ' / ' + n + ' …';
+    try {
+      var j = await fetch('/api/generator/batch', {
+        method:'POST', headers:{'Content-Type':'application/json'},
+        body: JSON.stringify({ colors, elements, styles,
+          patterns: patterns.map(function(p){return {source:p.source, id:p.id, title:p.title, artist:p.artist, tags:p.tags, dominant_hex:p.dominant_hex};})
+        })
+      }).then(r=>r.json());
+      if (j.ok && j.design_id) {
+        var imgUrl = '/designs/img/'+(j.filename || '');
+        var html = '<a href="/design/'+j.design_id+'" target="_blank" style="text-decoration:none;color:inherit" rel="noopener noreferrer">'+
+          '<div style="border:1px solid #444;border-radius:4px;overflow:hidden;background:#0f0e0c">'+
+            (j.filename ? '<img src="'+imgUrl+'" style="width:100%;aspect-ratio:1;object-fit:cover">' : '<div style="aspect-ratio:1;background:'+(j.dominant_hex||'#222')+'"></div>')+
+            '<div style="padding:4px 6px;font-size:10px;color:#eee">#'+j.design_id+' '+(j.dominant_hex||'')+'</div>'+
+          '</div></a>';
+        document.getElementById('batch-results').insertAdjacentHTML('beforeend', html);
+      } else {
+        document.getElementById('batch-results').insertAdjacentHTML('beforeend', '<div style="border:1px solid #c00;border-radius:4px;padding:6px;font-size:10px;color:#faa">err: '+((j.error||'?').slice(0,80))+'</div>');
+      }
+    } catch (e) {
+      document.getElementById('batch-results').insertAdjacentHTML('beforeend', '<div style="border:1px solid #c00;border-radius:4px;padding:6px;font-size:10px;color:#faa">err: '+e.message+'</div>');
+    }
+  }
+  s.textContent = 'Done. ' + n + ' generated.';
+  this.disabled = false;
+  loadRuns();
+});
+
+loadBatchPickers();
+loadCron(); loadPaletteStatus(); loadRecipes(); loadRuns();
+setInterval(loadRuns, 30000);
+</script>
+${HAMBURGER_JS}
+</body>
+</html>`);
+});
+
+// ─────────────────────────────────────────────────────────────────────────
+// DESIGNER STUDIO (multi-source composition)
+// ─────────────────────────────────────────────────────────────────────────
+
+const STUDIO_UPLOAD_DIR = path.join(__dirname, 'public', 'uploads', 'studio');
+fs.mkdirSync(STUDIO_UPLOAD_DIR, { recursive: true });
+
+function studioSessionId(req) {
+  // Simple session: use cookie or generate. Persisted via JS in localStorage too.
+  const raw = (req.headers.cookie || '');
+  const m = raw.match(/wc-studio-sid=([a-z0-9]+)/i);
+  return m ? m[1] : null;
+}
+
+app.get('/api/studio/artists', (_req, res) => {
+  try {
+    const raw = psqlQuery(`SELECT COALESCE(json_agg(t ORDER BY t.name), '[]'::json) FROM
+      (SELECT id, name, movement, era, one_line, thumb_url FROM wallco_famous_artists) t;`);
+    res.json(JSON.parse(raw || '[]'));
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+app.get('/api/studio/styles', (_req, res) => {
+  try {
+    const raw = psqlQuery(`SELECT COALESCE(json_agg(t ORDER BY t.name), '[]'::json) FROM
+      (SELECT id, name, description, era FROM wallco_style_lexicon) t;`);
+    res.json(JSON.parse(raw || '[]'));
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+app.get('/api/studio/textures', (_req, res) => {
+  try {
+    const raw = psqlQuery(`SELECT COALESCE(json_agg(t ORDER BY t.category, t.name), '[]'::json) FROM
+      (SELECT id, name, category, image_url, thumb_url, source, dominant_hex,
+              description, prompt_phrase
+       FROM wallco_textures) t;`);
+    res.json(JSON.parse(raw || '[]'));
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+// Suggest 3 color palettes — pulled from board sources + extracted from PD corpus
+app.post('/api/studio/suggest-palettes', (req, res) => {
+  const { sid } = req.body || {};
+  try {
+    const boardRaw = sid ? psqlQuery(`SELECT COALESCE(json_agg(t), '[]'::json) FROM
+      (SELECT color_hex_seeds FROM wallco_inspiration_board WHERE session_id='${String(sid).replace(/'/g,"''")}' ) t;`) : '[]';
+    const board = JSON.parse(boardRaw || '[]');
+    const seedHexes = [...new Set(board.flatMap(b => b.color_hex_seeds || []))];
+
+    // Strategy: 3 palettes — (1) board-driven, (2) complementary, (3) random PD-derived
+    function pickN(arr, n) {
+      const out = []; const pool = [...arr];
+      while (out.length < n && pool.length) {
+        out.push(pool.splice(Math.floor(Math.random()*pool.length),1)[0]);
+      }
+      return out;
+    }
+
+    // Palette A: from the board, padded if short
+    let a = pickN(seedHexes, 5);
+    if (a.length < 5) {
+      const extra = JSON.parse(psqlQuery(`SELECT COALESCE(json_agg(dominant_hex), '[]'::json)
+        FROM (SELECT dominant_hex FROM pd_source_designs
+              WHERE dominant_hex IS NOT NULL ORDER BY random() LIMIT 20) t;`));
+      a = a.concat(pickN(extra, 5 - a.length));
+    }
+
+    // Palette B: complementary — hue-shifted from palette A
+    function shiftHue(hex, deg) {
+      const r = parseInt(hex.slice(1,3),16)/255, g = parseInt(hex.slice(3,5),16)/255, b = parseInt(hex.slice(5,7),16)/255;
+      const max = Math.max(r,g,b), min = Math.min(r,g,b), d = max-min;
+      let h, l = (max+min)/2, s = d===0 ? 0 : (l>0.5 ? d/(2-max-min) : d/(max+min));
+      if (d===0) h = 0;
+      else if (max===r) h = ((g-b)/d + (g<b?6:0))*60;
+      else if (max===g) h = ((b-r)/d + 2)*60;
+      else h = ((r-g)/d + 4)*60;
+      h = (h + deg + 360) % 360;
+      // back to rgb
+      const c = (1 - Math.abs(2*l-1))*s, x = c*(1 - Math.abs(((h/60)%2)-1)), m = l - c/2;
+      let rr, gg, bb;
+      if (h < 60)      [rr,gg,bb] = [c,x,0];
+      else if (h<120)  [rr,gg,bb] = [x,c,0];
+      else if (h<180)  [rr,gg,bb] = [0,c,x];
+      else if (h<240)  [rr,gg,bb] = [0,x,c];
+      else if (h<300)  [rr,gg,bb] = [x,0,c];
+      else             [rr,gg,bb] = [c,0,x];
+      const toHex = v => Math.round((v+m)*255).toString(16).padStart(2,'0');
+      return '#' + toHex(rr) + toHex(gg) + toHex(bb);
+    }
+    const b2 = a.map(c => /^#[0-9a-f]{6}$/i.test(c) ? shiftHue(c, 180) : c);
+
+    // Palette C: from random PD design's palette JSONB
+    const pdRow = psqlQuery(`SELECT palette::text FROM pd_source_designs
+      WHERE palette IS NOT NULL AND jsonb_typeof(palette) = 'array'
+      ORDER BY random() LIMIT 1;`);
+    let c = [];
+    try {
+      const p = JSON.parse(pdRow);
+      c = (p || []).map(x => x.hex).filter(Boolean).slice(0, 5);
+    } catch {}
+    if (c.length < 5) c = pickN(JSON.parse(psqlQuery(
+      `SELECT COALESCE(json_agg(dominant_hex),'[]'::json) FROM (SELECT DISTINCT dominant_hex FROM pd_source_designs
+       WHERE dominant_hex IS NOT NULL ORDER BY random() LIMIT 20) t;`)), 5);
+
+    res.json({
+      palettes: [
+        { name: 'From your board',  colors: a, rationale: 'Built from the colors you already pinned.' },
+        { name: 'Complementary',     colors: b2, rationale: 'Hue-shifted 180° for contrast.' },
+        { name: 'Museum-pulled',     colors: c.slice(0,5), rationale: 'Pulled from a random Met / Smithsonian work.' }
+      ]
+    });
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+// Interior-designer-style critique: combines basic image analysis + the
+// interior-designer skill's framing. Returns advisory, not blocking.
+app.post('/api/studio/interior-design-review', async (req, res) => {
+  const { design_id, prompt } = req.body || {};
+  if (!design_id) return res.status(400).json({ error: 'design_id required' });
+  try {
+    const raw = psqlQuery(`SELECT row_to_json(t) FROM (SELECT id, prompt, local_path, dominant_hex, palette, category
+      FROM spoon_all_designs WHERE id=${parseInt(design_id,10)}) t;`);
+    const d = JSON.parse(raw || '{}');
+    if (!d.id) return res.status(404).json({ error: 'design not found' });
+
+    // Gemini vision review (mandatory per /interior-designer guidance)
+    const geminiKey = process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY;
+    let critique = { strengths: [], concerns: [], recommendation: '' };
+    if (geminiKey && d.local_path && fs.existsSync(d.local_path)) {
+      try {
+        const b64 = fs.readFileSync(d.local_path).toString('base64');
+        const r = await fetch(
+          `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${geminiKey}`,
+          {
+            method: 'POST',
+            headers: { 'Content-Type': 'application/json' },
+            body: JSON.stringify({
+              contents: [{
+                parts: [
+                  { inlineData: { mimeType: 'image/jpeg', data: b64 } },
+                  { text: `You are a luxury interior designer reviewing a new wallpaper pattern.
+Original prompt: "${(prompt || d.prompt || '').slice(0,400)}"
+
+Critique as a designer who places this in HIGH-END interiors (Hermès, Soho House, private hotel suites). STRICT JSON:
+{
+  "strengths": ["<1-line strength>", "<1-line strength>", "<1-line strength>"],
+  "concerns": ["<1-line concern>", "<1-line concern>"],
+  "recommendation": "<one sentence, would a designer specify this for a $1500/yard project, yes/no/maybe + why>",
+  "room_fit": ["<best room: bedroom|living|dining|powder|library|...>", "<second-best>"],
+  "lighting": "<best lighting: north light, candlelight, gallery, sunny>",
+  "scale": "<small repeat / medium / large / mural>"
+}` }
+                ]
+              }],
+              generationConfig: { temperature: 0.2, maxOutputTokens: 600, responseMimeType: 'application/json' }
+            })
+          }
+        );
+        const j = await r.json();
+        try {
+          const { logGemini } = require(require('path').join(require('os').homedir(), '.claude/skills/cost-tracker/scripts/log-gemini.js'));
+          logGemini(j, { app: 'wallco-ai', model: 'gemini-2.0-flash', note: 'design-critique ' + (d.id || '') });
+        } catch {}
+        const t = j.candidates?.[0]?.content?.parts?.[0]?.text;
+        if (t) {
+          try { critique = JSON.parse(t.replace(/^```json\s*|\s*```$/g, '')); } catch {}
+        }
+      } catch (e) { critique._err = e.message; }
+    }
+    res.json({ ok: true, design: d, critique });
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+// ── AI Interior Designer: assign metadata (title, pattern, color name, DIG#, tags)
+// Admin-only. Reads the design image, runs the interior-designer skill's
+// vision pass via Gemini, and persists ai_title / ai_pattern / ai_color_name /
+// dig_number / merged tags. The DIG number, when freshly assigned, slots into
+// the DW DIG_AI_xxxxxx namespace (no collision with vendor DIG_ SKUs because
+// those don't carry the AI_ infix). Returns the suggested values for a
+// confirm-before-save UI on /design/:id; pass `apply: true` to persist.
+app.post('/api/design/:id/ai-designer', requireAdmin, express.json({ limit: '16kb' }), async (req, res) => {
+  const id = parseInt(req.params.id, 10);
+  if (!Number.isFinite(id) || id < 1) return res.status(400).json({ error: 'bad id' });
+  const apply = req.body && req.body.apply === true;
+  try {
+    const raw = psqlQuery(`SELECT row_to_json(t) FROM (SELECT id, prompt, local_path,
+      dominant_hex, palette, category, motifs, tags, dig_number, ai_title, ai_pattern,
+      ai_color_name FROM spoon_all_designs WHERE id=${id}) t;`);
+    const d = JSON.parse(raw || '{}');
+    if (!d.id) return res.status(404).json({ error: 'design not found' });
+    if (!d.local_path || !fs.existsSync(d.local_path)) {
+      return res.status(409).json({ error: 'image file missing on disk', local_path: d.local_path });
+    }
+
+    const geminiKey = process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY;
+    if (!geminiKey) return res.status(503).json({ error: 'GEMINI_API_KEY not configured' });
+
+    const b64 = fs.readFileSync(d.local_path).toString('base64');
+    const r = await fetch(
+      `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${geminiKey}`,
+      {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({
+          contents: [{
+            parts: [
+              { inlineData: { mimeType: 'image/jpeg', data: b64 } },
+              { text: `You are a luxury interior designer cataloging a new wallpaper pattern for Designer Wallcoverings.
+Look at the image and answer with STRICT JSON only. The current generation prompt is provided as a HINT only — your authority is what you actually SEE in the image, not what the prompt requested. If the image contains a motif the prompt tried to remove (e.g. leaves, plants, banana, monstera, palm leaves, palm fronds), say so — accuracy matters.
+
+Generation prompt hint: "${(d.prompt || '').slice(0, 500)}"
+
+Return STRICT JSON:
+{
+  "title": "<2-5 word evocative title, designer language, e.g. 'Iridescent Butterfly Trellis'>",
+  "pattern": "<one canonical pattern name from this list: Damask, Floral, Trellis, Botanical, Geometric, Stripe, Toile, Chinoiserie, Foulard, Abstract, Texture, Animal, Figurative, Mural, Other>",
+  "color_name": "<2-4 word color description, e.g. 'Charcoal & Jewel Tones', 'Bone White Champagne'>",
+  "tags": ["<5-10 short lowercase tags describing motifs and style>"],
+  "contains_leaves": <true|false — does the image visibly contain leaves, fronds, or banana/monstera/tropical plant content?>,
+  "leaf_kind": "<if contains_leaves true: 'banana'|'monstera'|'palm'|'tropical'|'generic'; else 'none'>",
+  "seam_visible": <true|false — does the image show visible horizontal or vertical seam lines / tile cut-marks indicating bad tiling?>,
+  "designer_notes": "<one sentence the catalog can quote>"
+}` }
+            ]
+          }],
+          generationConfig: { temperature: 0.2, maxOutputTokens: 500, responseMimeType: 'application/json' }
+        })
+      }
+    );
+    const j = await r.json();
+    try {
+      const { logGemini } = require(require('path').join(require('os').homedir(), '.claude/skills/cost-tracker/scripts/log-gemini.js'));
+      logGemini(j, { app: 'wallco-ai', model: 'gemini-2.0-flash', note: 'ai-designer ' + id });
+    } catch {}
+    const t = j.candidates?.[0]?.content?.parts?.[0]?.text;
+    let suggested = null;
+    if (t) {
+      try { suggested = JSON.parse(t.replace(/^```json\s*|\s*```$/g, '')); }
+      catch { return res.status(502).json({ error: 'gemini JSON parse failed', raw: t.slice(0, 400) }); }
+    }
+    if (!suggested) return res.status(502).json({ error: 'no candidate text from gemini' });
+
+    // Hard settlement gate — if Gemini reports leaves/banana, refuse to persist
+    if (suggested.contains_leaves === true && /^(banana|monstera|palm|tropical)$/i.test(suggested.leaf_kind || '')) {
+      return res.status(422).json({
+        error: 'SETTLEMENT_VIOLATION',
+        message: `Image contains ${suggested.leaf_kind} leaf content — settlement-blocked. Image will not be tagged or published.`,
+        suggested,
+      });
+    }
+
+    // Merge tags — preserve existing user/auto tags, add new lowercase tags from AI
+    const existingTags = Array.isArray(d.tags) ? d.tags : [];
+    const mergedTags = Array.from(new Set([
+      ...existingTags.map(s => String(s).toLowerCase().trim()).filter(Boolean),
+      ...((suggested.tags || []).map(s => String(s).toLowerCase().trim()).filter(Boolean))
+    ]));
+
+    // Assign DIG# if missing — DW digital-AI namespace
+    let digNumber = d.dig_number;
+    if (!digNumber) {
+      digNumber = `DIG_AI_${String(id).padStart(6, '0')}`;
+    }
+
+    if (apply) {
+      const tagsLit = "ARRAY[" + mergedTags.map(t => "'" + String(t).replace(/'/g,"''") + "'").join(',') + "]::text[]";
+      psqlQuery(`UPDATE spoon_all_designs SET
+        ai_title       = ${pgEsc(suggested.title || null)},
+        ai_pattern     = ${pgEsc(suggested.pattern || null)},
+        ai_color_name  = ${pgEsc(suggested.color_name || null)},
+        dig_number     = ${pgEsc(digNumber)},
+        tags           = ${mergedTags.length ? tagsLit : 'tags'},
+        ai_reviewed_at = NOW(),
+        ai_review_raw  = ${pgEsc(JSON.stringify(suggested))}::jsonb
+        WHERE id = ${id};`);
+    }
+
+    res.json({
+      ok: true,
+      id,
+      applied: apply,
+      suggested: {
+        title:      suggested.title || null,
+        pattern:    suggested.pattern || null,
+        color_name: suggested.color_name || null,
+        dig_number: digNumber,
+        tags:       mergedTags,
+        seam_visible: suggested.seam_visible === true,
+        designer_notes: suggested.designer_notes || null,
+      },
+      raw: suggested
+    });
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+// Category insights: trends + best-sellers grid (stub today, wireable to ad-signals skill)
+app.get('/api/studio/category-insights', async (req, res) => {
+  const cat = String(req.query.cat || '').toLowerCase().trim();
+  if (!cat) return res.json({ trends: [], best_sellers: [], note: 'pass ?cat=floral|damask|...' });
+  try {
+    // Best-sellers from DW catalog (dw_unified.products) — schema is vendor/SKU/pattern, not Shopify.
+    let bestSellers = [];
+    try {
+      const safe = cat.replace(/'/g,"''");
+      const raw = psqlQuery(`SELECT COALESCE(json_agg(t), '[]'::json) FROM
+        (SELECT id,
+                COALESCE(name, pattern_name, sku) AS title,
+                vendor_id AS vendor,
+                material  AS product_type,
+                thumbnail_url AS image_url,
+                product_url   AS handle,
+                price
+         FROM products
+         WHERE LOWER(COALESCE(name,''))         LIKE '%${safe}%'
+            OR LOWER(COALESCE(pattern_name,'')) LIKE '%${safe}%'
+            OR LOWER(COALESCE(material,''))     LIKE '%${safe}%'
+            OR LOWER(COALESCE(color_family,'')) LIKE '%${safe}%'
+         LIMIT 12) t;`);
+      bestSellers = JSON.parse(raw || '[]');
+    } catch (e) {
+      // products table may not exist on prod; fall back to recent PD images in this category
+      const raw = psqlQuery(`SELECT COALESCE(json_agg(t), '[]'::json) FROM
+        (SELECT id, title, creator AS vendor, source AS product_type,
+                image_url, source_url AS handle, NULL AS price
+         FROM pd_source_designs
+         WHERE LOWER(COALESCE(category,'')) LIKE '%${cat.replace(/'/g,"''")}%'
+            OR LOWER(COALESCE(title,''))    LIKE '%${cat.replace(/'/g,"''")}%'
+         ORDER BY ingested_at DESC LIMIT 12) t;`);
+      bestSellers = JSON.parse(raw || '[]');
+    }
+
+    // Trends — return canonical search terms + Google Trends links
+    const trends = [
+      { term: `${cat} wallpaper 2026`, link: `https://trends.google.com/trends/explore?q=${encodeURIComponent(cat+' wallpaper')}` },
+      { term: `${cat} pattern interior`, link: `https://trends.google.com/trends/explore?q=${encodeURIComponent(cat+' pattern interior')}` },
+      { term: `${cat} maximalist`, link: `https://trends.google.com/trends/explore?q=${encodeURIComponent(cat+' maximalist')}` },
+    ];
+
+    // Ad signal placeholder (real wiring to ad-signals skill is a follow-up)
+    res.json({
+      category: cat,
+      trends,
+      best_sellers: bestSellers,
+      ad_signals_note: 'Live Google Ads / Meta pixel data via the ad-signals skill — wireable on demand.'
+    });
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+app.get('/api/studio/elements', (_req, res) => {
+  try {
+    const raw = psqlQuery(`SELECT COALESCE(json_agg(t ORDER BY t.category, t.name), '[]'::json) FROM
+      (SELECT id, category, name, prompt_phrase FROM wallco_element_lexicon) t;`);
+    res.json(JSON.parse(raw || '[]'));
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+// Search public-domain corpus + Spoonflower-pulled + wallco generated
+app.get('/api/studio/search', (req, res) => {
+  const q = String(req.query.q || '').trim().toLowerCase().replace(/'/g, "''");
+  const limit = Math.min(parseInt(req.query.limit || '60', 10), 200);
+  if (!q) return res.json([]);
+  try {
+    // Build a unified result via three subqueries
+    const sql = `
+      WITH pd AS (
+        SELECT 'pd' AS source, id::text AS id, title, creator AS artist,
+               source AS subsource, image_url, local_path, dominant_hex,
+               category, tags
+        FROM pd_source_designs
+        WHERE LOWER(COALESCE(title,'')) LIKE '%${q}%'
+           OR LOWER(COALESCE(creator,'')) LIKE '%${q}%'
+           OR LOWER(COALESCE(category,'')) LIKE '%${q}%'
+           OR EXISTS (SELECT 1 FROM unnest(COALESCE(tags,ARRAY[]::text[])) AS t WHERE LOWER(t) LIKE '%${q}%')
+        LIMIT ${limit}
+      ),
+      sp AS (
+        SELECT 'spoonflower' AS source, sf_design_id AS id, title, NULL AS artist,
+               'spoonflower' AS subsource, design_url AS image_url, local_path,
+               NULL AS dominant_hex, NULL AS category, NULL::text[] AS tags
+        FROM spoonflower_pulled_designs
+        WHERE LOWER(COALESCE(title,'')) LIKE '%${q}%'
+        LIMIT 30
+      ),
+      gen AS (
+        SELECT 'wallco' AS source, id::text AS id, prompt AS title, generator AS artist,
+               'wallco' AS subsource, image_url, local_path, dominant_hex,
+               category, tags
+        FROM spoon_all_designs
+        WHERE LOWER(COALESCE(prompt,'')) LIKE '%${q}%'
+           OR LOWER(COALESCE(category,'')) LIKE '%${q}%'
+        LIMIT 30
+      )
+      SELECT COALESCE(json_agg(t), '[]'::json) FROM
+        (SELECT * FROM pd UNION ALL SELECT * FROM sp UNION ALL SELECT * FROM gen) t;`;
+    const raw = psqlQuery(sql);
+    const items = JSON.parse(raw || '[]').map(it => {
+      // Map local_path → /img URL when applicable
+      let url = it.image_url || '';
+      if (it.local_path && it.local_path.startsWith(path.join(__dirname, 'data') + path.sep)) {
+        url = '/img/' + it.local_path.replace(path.join(__dirname, 'data') + path.sep, '');
+      } else if (it.local_path && it.local_path.includes('/generated/')) {
+        url = '/designs/img/' + path.basename(it.local_path);
+      }
+      return { ...it, thumb_url: url };
+    });
+    res.json(items);
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+// Add /img static for PD source images (loaded from data/)
+app.use('/img', express.static(path.join(__dirname, 'data'), { maxAge: '7d' }));
+
+// Image upload — accepts multipart OR base64
+app.post('/api/studio/upload', express.raw({ type: 'application/octet-stream', limit: '20mb' }), async (req, res) => {
+  try {
+    // SECURITY (path traversal fix 2026-05-12): sid is user-controlled and was being
+    // joined directly into a filesystem path. Malicious sid like "../../etc/passwd_"
+    // would escape STUDIO_UPLOAD_DIR. Also: filename's old regex allowed dots so ".."
+    // slipped through. Plus the sips + python execSync had shell-injectable interpolation.
+    // Fix: strict allowlist on sid + filename; verify resolved path is inside the upload
+    // dir; use execFileSync (no shell) for the palette pipeline.
+    const rawSid = String(req.query.sid || '').slice(0, 40);
+    const sid = /^[a-z0-9-]{1,40}$/i.test(rawSid) ? rawSid : ('anon-' + Date.now());
+    const rawName = String(req.query.name || `upload-${Date.now()}.jpg`);
+    // Strip every char that isn't [a-z0-9._-], collapse "..", trim leading dots/slashes
+    const filename = rawName
+      .replace(/[^a-z0-9._-]/gi, '')
+      .replace(/\.\.+/g, '.')
+      .replace(/^[.\-]+/, '') || `upload-${Date.now()}.jpg`;
+    const target = path.join(STUDIO_UPLOAD_DIR, `${sid}_${filename}`);
+    // Canonicalize + verify containment (defense in depth)
+    const safeDir = path.resolve(STUDIO_UPLOAD_DIR);
+    const safeTarget = path.resolve(target);
+    if (!safeTarget.startsWith(safeDir + path.sep)) {
+      return res.status(400).json({ error: 'invalid path' });
+    }
+    fs.writeFileSync(safeTarget, req.body);
+    // Quick palette extract via sips + Python — execFileSync avoids shell interpolation
+    let domHex = null;
+    try {
+      const small = safeTarget + '.32.png';
+      execFileSync('sips', ['-Z', '32', safeTarget, '--out', small], { stdio: 'ignore' });
+      // Read file path from argv[1] so the path never crosses a shell or python source-string boundary
+      const pyScript = `
+import sys
+from PIL import Image
+img = Image.open(sys.argv[1]).convert("RGB")
+pix = list(img.getdata())
+n = len(pix)
+r = sum(p[0] for p in pix)//n
+g = sum(p[1] for p in pix)//n
+b = sum(p[2] for p in pix)//n
+print(f"#{r:02x}{g:02x}{b:02x}")
+`;
+      domHex = execFileSync('python3', ['-c', pyScript, small], { encoding: 'utf8' }).trim();
+      fs.unlinkSync(small);
+    } catch {}
+
+    // AI style + element analysis via Gemini (mandatory per dw standing rule)
+    let ai = { style: null, colors: [], elements: [], summary: null };
+    const geminiKey = process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY;
+    if (geminiKey) {
+      try {
+        const b64 = fs.readFileSync(target).toString('base64');
+        const r = await fetch(
+          `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${geminiKey}`,
+          {
+            method: 'POST',
+            headers: { 'Content-Type': 'application/json' },
+            body: JSON.stringify({
+              contents: [{
+                parts: [
+                  { inlineData: { mimeType: 'image/jpeg', data: b64 } },
+                  { text: `Analyze this design. Return STRICT JSON:
+{
+  "style": "<one of: Baroque, Rococo, Neoclassical, Art Nouveau, Art Deco, Arts and Crafts, Mid-century Modern, Bauhaus, Memphis, Maximalist, Minimalist, Chinoiserie, Toile, Damask, Ikat, Suzani, Moroccan, Japonisme, Folk Scandi, Tropical, Other>",
+  "colors": ["#hex","#hex","#hex"],
+  "elements": ["<element keywords like bird, peony, monogram, trellis, etc — max 8>"],
+  "summary": "<one sentence describing the design>"
+}` }
+                ]
+              }],
+              generationConfig: { temperature: 0.1, maxOutputTokens: 400, responseMimeType: 'application/json' }
+            })
+          }
+        );
+        const j = await r.json();
+        try {
+          const { logGemini } = require(require('path').join(require('os').homedir(), '.claude/skills/cost-tracker/scripts/log-gemini.js'));
+          logGemini(j, { app: 'wallco-ai', model: 'gemini-2.0-flash', note: 'style-element-analysis' });
+        } catch {}
+        const t = j.candidates && j.candidates[0] && j.candidates[0].content
+                  && j.candidates[0].content.parts && j.candidates[0].content.parts[0]
+                  && j.candidates[0].content.parts[0].text;
+        if (t) {
+          const cleaned = t.replace(/^```json\s*|\s*```$/g, '');
+          try { ai = { ...ai, ...JSON.parse(cleaned) }; } catch {}
+        }
+      } catch (e) { console.error('gemini upload error:', e.message); }
+    }
+
+    let stat = fs.statSync(target);
+    const row = psqlQuery(`INSERT INTO wallco_user_uploads
+      (session_id, filename, local_path, ai_style, ai_colors, ai_elements, ai_summary, bytes)
+      VALUES (${sid ? "'"+String(sid).replace(/'/g,"''")+"'" : 'NULL'},
+              '${filename.replace(/'/g,"''")}',
+              '${target.replace(/'/g,"''")}',
+              ${ai.style ? "'"+ai.style.replace(/'/g,"''")+"'" : 'NULL'},
+              ARRAY[${(ai.colors||[]).map(c=>"'"+String(c).replace(/'/g,"''")+"'").join(',') || ''}]::text[],
+              ARRAY[${(ai.elements||[]).map(c=>"'"+String(c).replace(/'/g,"''")+"'").join(',') || ''}]::text[],
+              ${ai.summary ? "'"+ai.summary.replace(/'/g,"''")+"'" : 'NULL'},
+              ${stat.size})
+      RETURNING id;`);
+
+    res.json({
+      ok: true,
+      id: parseInt(row, 10),
+      url: `/uploads/studio/${sid}_${filename}`,
+      dominant_hex: domHex,
+      ai_style: ai.style,
+      ai_colors: ai.colors,
+      ai_elements: ai.elements,
+      ai_summary: ai.summary,
+    });
+  } catch (e) {
+    res.status(500).json({ ok: false, error: e.message });
+  }
+});
+
+// Save/get the 6-slot inspiration board
+app.get('/api/studio/board', (req, res) => {
+  const sid = req.query.sid;
+  if (!sid) return res.json([]);
+  try {
+    const raw = psqlQuery(`SELECT COALESCE(json_agg(t ORDER BY t.slot_idx), '[]'::json) FROM
+      (SELECT * FROM wallco_inspiration_board WHERE session_id='${String(sid).replace(/'/g,"''")}' ) t;`);
+    res.json(JSON.parse(raw || '[]'));
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+app.post('/api/studio/board/slot', (req, res) => {
+  const { sid, slot_idx, source_type, source_id, image_url, thumb_url, title,
+          artist, style_tags = [], color_hex_seeds = [], selected_elements = [] } = req.body || {};
+  if (!sid || slot_idx == null) return res.status(400).json({ error: 'sid + slot_idx required' });
+  const arr = a => `ARRAY[${(a||[]).map(v=>"'"+String(v).replace(/'/g,"''")+"'").join(',') || ''}]::text[]`;
+  const s = v => v == null ? 'NULL' : "'"+String(v).replace(/'/g,"''")+"'";
+  try {
+    psqlQuery(`INSERT INTO wallco_inspiration_board
+      (session_id, slot_idx, source_type, source_id, image_url, thumb_url, title,
+       artist, style_tags, color_hex_seeds, selected_elements)
+      VALUES (${s(sid)}, ${parseInt(slot_idx,10)}, ${s(source_type)}, ${s(source_id)},
+              ${s(image_url)}, ${s(thumb_url)}, ${s(title)}, ${s(artist)},
+              ${arr(style_tags)}, ${arr(color_hex_seeds)}, ${arr(selected_elements)})
+      ON CONFLICT (session_id, slot_idx) DO UPDATE SET
+        source_type=EXCLUDED.source_type, source_id=EXCLUDED.source_id,
+        image_url=EXCLUDED.image_url, thumb_url=EXCLUDED.thumb_url, title=EXCLUDED.title,
+        artist=EXCLUDED.artist, style_tags=EXCLUDED.style_tags,
+        color_hex_seeds=EXCLUDED.color_hex_seeds, selected_elements=EXCLUDED.selected_elements;`);
+    res.json({ ok: true });
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+app.delete('/api/studio/board/slot/:slot', (req, res) => {
+  const sid = req.query.sid;
+  if (!sid) return res.status(400).json({ error: 'sid required' });
+  try {
+    psqlQuery(`DELETE FROM wallco_inspiration_board
+      WHERE session_id='${String(sid).replace(/'/g,"''")}' AND slot_idx=${parseInt(req.params.slot,10)};`);
+    res.json({ ok: true });
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+// Compose: build a prompt from the board + extra elements, fire SDXL
+app.post('/api/studio/compose', (req, res) => {
+  const { sid, extra_elements = [], extra_text = '' } = req.body || {};
+  if (!sid) return res.status(400).json({ error: 'sid required' });
+  try {
+    const boardRaw = psqlQuery(`SELECT COALESCE(json_agg(t ORDER BY t.slot_idx), '[]'::json) FROM
+      (SELECT * FROM wallco_inspiration_board WHERE session_id='${String(sid).replace(/'/g,"''")}' ) t;`);
+    const board = JSON.parse(boardRaw || '[]');
+    if (!board.length) return res.status(400).json({ error: 'inspiration board is empty — add at least 1 slot' });
+
+    const artists = [...new Set(board.flatMap(b => [b.artist].filter(Boolean)))];
+    const styles  = [...new Set(board.flatMap(b => (b.style_tags || []).concat(b.selected_elements || []).filter(Boolean)))];
+    const colors  = [...new Set(board.flatMap(b => b.color_hex_seeds || []))].slice(0, 6);
+    const elems   = [...new Set((board.flatMap(b => b.selected_elements || [])).concat(extra_elements))];
+
+    const promptParts = [
+      'A seamless wallpaper pattern',
+      artists.length ? `in the visual lineage of ${artists.slice(0,3).join(' and ')}` : '',
+      styles.length ? `with ${styles.slice(0,4).join(' / ')} influence` : '',
+      elems.length ? `featuring ${elems.slice(0,6).join(', ')}` : '',
+      colors.length ? `palette ${colors.join(', ')}` : '',
+      extra_text || '',
+      'high detail, museum-quality, 24x24 inch tileable, no text, no signature.'
+    ].filter(Boolean);
+    const prompt = promptParts.join(', ').replace(/\s+/g, ' ').trim();
+
+    // Log the run as recipe-less (recipe_id=NULL)
+    const runId = psqlQuery(`INSERT INTO wallco_generator_runs
+      (recipe_id, prompt, palette_seed_ids, status)
+      VALUES (NULL, '${prompt.replace(/'/g,"''")}', '{}'::bigint[], 'running')
+      RETURNING id;`);
+
+    // Spawn the generator script directly
+    const r = spawnSync('node', [
+      path.join(__dirname, 'scripts', 'generate_designs.js'),
+      '--n', '1', '--kind', 'seamless_tile',
+      '--category', (styles[0] || 'mixed').toLowerCase(),
+      '--prompts', prompt,
+    ], { cwd: __dirname, env: { ...process.env, GEN_BACKEND: process.env.GEN_BACKEND || 'replicate' },
+         encoding: 'utf8', timeout: 120000 });
+
+    let designId = null;
+    try {
+      designId = parseInt(psqlQuery(`SELECT id FROM spoon_all_designs ORDER BY id DESC LIMIT 1;`), 10);
+    } catch {}
+
+    const ok = r.status === 0;
+    psqlQuery(`UPDATE wallco_generator_runs SET
+      status='${ok ? 'done' : 'error'}',
+      design_id=${designId || 'NULL'},
+      error=${ok ? 'NULL' : "'"+((r.stderr||r.stdout||'').slice(-400).replace(/'/g,"''"))+"'"},
+      finished_at=now()
+      WHERE id=${runId};`);
+
+    // Look up the freshly-inserted design's image so the /studio payoff frame
+    // can render the final pattern (R0a — Steve ask 2026-05-12 late).
+    let imageUrl = null;
+    let imageFilename = null;
+    if (ok && designId) {
+      try {
+        const lp = psqlQuery(`SELECT local_path FROM spoon_all_designs WHERE id=${designId} LIMIT 1;`);
+        if (lp) {
+          imageFilename = path.basename(lp);
+          imageUrl = '/designs/img/' + imageFilename;
+        }
+      } catch {}
+    }
+    // R0c — persist every render to studio_renders so the user can revisit/delete.
+    let renderId = null;
+    if (ok && designId) {
+      try {
+        const safePrompt = prompt.replace(/'/g, "''");
+        const safeSid = String(sid).replace(/'/g, "''");
+        const out = psqlQuery(`INSERT INTO studio_renders
+          (session_uuid, design_id, prompt) VALUES
+          ('${safeSid}', ${designId}, '${safePrompt}')
+          RETURNING id;`);
+        renderId = parseInt(out, 10) || null;
+      } catch (e) { console.error('[studio_renders insert]', e.message); }
+    }
+    res.json({ ok, prompt, design_id: designId, run_id: parseInt(runId, 10),
+               render_id: renderId,
+               image_url: imageUrl, image_filename: imageFilename,
+               error: ok ? null : (r.stderr || r.stdout || '').slice(-600) });
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+// ── R0c/R0d studio_renders endpoints ─────────────────────────────────
+// GET /api/studio-renders?sid=X   — list a user's renders (newest first, live by default)
+// POST /api/studio-renders/:designId/delete  — soft-delete (matches session_uuid)
+// POST /api/studio-renders/:designId/restore — restore within 7d (any matching session)
+// POST /api/studio-renders/:designId/link-room — attach a room_id once the room renders
+app.get('/api/studio-renders', (req, res) => {
+  const sid = req.query.sid || req.headers['x-studio-sid'];
+  if (!sid) return res.status(400).json({ ok: false, error: 'sid required' });
+  const includeDeleted = req.query.include_deleted === '1';
+  const safeSid = String(sid).replace(/'/g, "''");
+  try {
+    const raw = psqlQuery(`SELECT COALESCE(json_agg(t ORDER BY t.created_at DESC), '[]'::json) FROM (
+      SELECT sr.id AS render_id, sr.design_id, sr.room_id, sr.prompt,
+             sr.created_at, sr.deleted_at,
+             d.local_path AS pattern_path,
+             d.dominant_hex, d.category,
+             r.image_path AS room_path, r.room_type
+      FROM studio_renders sr
+      LEFT JOIN spoon_all_designs d ON d.id = sr.design_id
+      LEFT JOIN wallco_rooms       r ON r.id = sr.room_id
+      WHERE sr.session_uuid = '${safeSid}'
+        ${includeDeleted ? '' : 'AND sr.deleted_at IS NULL'}
+    ) t;`);
+    const rows = JSON.parse(raw || '[]');
+    const renders = rows.map(r => ({
+      render_id: r.render_id,
+      design_id: r.design_id,
+      room_id:   r.room_id,
+      prompt:    r.prompt,
+      category:  r.category,
+      dominant_hex: r.dominant_hex,
+      image_url: r.pattern_path ? '/designs/img/' + path.basename(r.pattern_path) : null,
+      room_image_url: r.room_path || null,
+      room_type: r.room_type || null,
+      created_at: r.created_at,
+      deleted_at: r.deleted_at
+    }));
+    res.json({ ok: true, renders });
+  } catch (e) { res.status(500).json({ ok: false, error: e.message }); }
+});
+
+app.post('/api/studio-renders/:designId/delete', (req, res) => {
+  const designId = parseInt(req.params.designId, 10);
+  const sid = (req.body && req.body.sid) || req.headers['x-studio-sid'];
+  if (!designId) return res.status(400).json({ ok: false, error: 'designId required' });
+  if (!sid)      return res.status(400).json({ ok: false, error: 'sid required (owner check)' });
+  const safeSid = String(sid).replace(/'/g, "''");
+  try {
+    const out = psqlQuery(`UPDATE studio_renders
+      SET deleted_at = NOW()
+      WHERE design_id = ${designId}
+        AND session_uuid = '${safeSid}'
+        AND deleted_at IS NULL
+      RETURNING id;`);
+    if (!out) return res.status(404).json({ ok: false, error: 'render not found or already deleted (or wrong owner)' });
+    res.json({ ok: true, render_id: parseInt(out, 10), grace_days: 7 });
+  } catch (e) { res.status(500).json({ ok: false, error: e.message }); }
+});
+
+app.post('/api/studio-renders/:designId/restore', (req, res) => {
+  const designId = parseInt(req.params.designId, 10);
+  const sid = (req.body && req.body.sid) || req.headers['x-studio-sid'];
+  if (!designId) return res.status(400).json({ ok: false, error: 'designId required' });
+  if (!sid)      return res.status(400).json({ ok: false, error: 'sid required' });
+  const safeSid = String(sid).replace(/'/g, "''");
+  try {
+    const out = psqlQuery(`UPDATE studio_renders
+      SET deleted_at = NULL
+      WHERE design_id = ${designId}
+        AND session_uuid = '${safeSid}'
+        AND deleted_at IS NOT NULL
+        AND deleted_at > NOW() - INTERVAL '7 days'
+      RETURNING id;`);
+    if (!out) return res.status(404).json({ ok: false, error: 'no soft-deleted render found within 7d for this session' });
+    res.json({ ok: true, render_id: parseInt(out, 10) });
+  } catch (e) { res.status(500).json({ ok: false, error: e.message }); }
+});
+
+app.post('/api/studio-renders/:designId/link-room', (req, res) => {
+  const designId = parseInt(req.params.designId, 10);
+  const roomId = parseInt((req.body && req.body.room_id) || '', 10);
+  const sid = (req.body && req.body.sid) || req.headers['x-studio-sid'];
+  if (!designId || !roomId || !sid) {
+    return res.status(400).json({ ok: false, error: 'designId, room_id, sid all required' });
+  }
+  const safeSid = String(sid).replace(/'/g, "''");
+  try {
+    const out = psqlQuery(`UPDATE studio_renders
+      SET room_id = ${roomId}
+      WHERE design_id = ${designId}
+        AND session_uuid = '${safeSid}'
+        AND deleted_at IS NULL
+      RETURNING id;`);
+    if (!out) return res.status(404).json({ ok: false, error: 'render not found for this session' });
+    res.json({ ok: true, render_id: parseInt(out, 10), room_id: roomId });
+  } catch (e) { res.status(500).json({ ok: false, error: e.message }); }
+});
+
+// /studio HTML page
+app.get('/studio', (_req, res) => {
+  res.type('html').send(`${htmlHead({
+    title: 'Designer Studio — wallco.ai',
+    description: 'Compose a new wallpaper design from artists, styles, and your own inspiration images',
+    canonical: 'https://wallco.ai/studio'
+  })}
+<style>
+  /* Studio-scoped overflow hygiene — forces single-col stack on phones by
+     preventing the body from growing past viewport. Without this, any
+     fixed/absolute element (trade-nav, FAB) silently widens the body and
+     auto-fit grids miscalculate column count. */
+  html, body { overflow-x: clip; max-width: 100vw; }
+  @media (max-width: 700px) {
+    .studio-3col { grid-template-columns: 1fr !important; }
+    /* Trade-nav clip fix — pull Sign-In pill inside viewport on phones */
+    #trade-nav { right: 56px !important; }
+  }
+</style>
+<body>
+${htmlHeader('')}
+<main style="padding:88px 18px 24px;max-width:1500px;margin:0 auto;overflow-x:clip;box-sizing:border-box">
+  <h1 class="studio-display-title" style="font-family:'Cormorant Garamond',serif;font-weight:400;font-size:clamp(30px,6.4vw,48px);letter-spacing:-0.01em;margin:6px 0 4px;line-height:1.05">Designer Studio</h1>
+  <p style="color:#666;margin:0 0 22px;font-size:14px">Pick artists, styles, elements, or upload your own. Add up to 6 sources to the board → Compose generates a new pattern.</p>
+
+  <!-- Top: 3-column reference panels — auto-stacks below ~640px so chips never clip mid-word on mobile -->
+  <div class="studio-3col" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,220px),1fr));gap:14px;margin-bottom:22px">
+
+    <!-- Artists -->
+    <div style="padding:14px;border:1px solid #ddd;border-radius:8px;background:#fafafa;max-height:320px;overflow-y:auto;min-width:0;overflow-wrap:anywhere">
+      <h3 style="margin:0 0 8px;font-size:14px">Famous Artists</h3>
+      <div id="artists-list" style="font-size:12px"></div>
+    </div>
+
+    <!-- Styles -->
+    <div style="padding:14px;border:1px solid #ddd;border-radius:8px;background:#fafafa;max-height:320px;overflow-y:auto;min-width:0;overflow-wrap:anywhere">
+      <h3 style="margin:0 0 8px;font-size:14px">Styles</h3>
+      <div id="styles-list" style="font-size:12px"></div>
+    </div>
+
+    <!-- Elements -->
+    <div style="padding:14px;border:1px solid #ddd;border-radius:8px;background:#fafafa;max-height:320px;overflow-y:auto;min-width:0;overflow-wrap:anywhere">
+      <h3 style="margin:0 0 8px;font-size:14px">Elements <span style="font-size:10px;color:#999;font-weight:400">(animals, monograms, …)</span></h3>
+      <div id="elements-list" style="font-size:12px"></div>
+    </div>
+  </div>
+
+  <!-- Textures grid (drag-droppable as bg or fg) -->
+  <div style="margin-bottom:18px;padding:14px;border:1px solid #ddd;border-radius:8px;background:#fff">
+    <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
+      <h3 style="margin:0;font-size:14px">Textures <span style="font-size:11px;color:#999;font-weight:400">(drag onto an empty slot, or click to add as background)</span></h3>
+      <span style="font-size:11px;color:#666">Foreground opacity <input type="range" id="overlay-opacity" min="0" max="100" value="40" style="vertical-align:middle"> <span id="opacity-val">40%</span></span>
+    </div>
+    <div id="textures-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(86px,1fr));gap:6px;max-height:260px;overflow-y:auto"></div>
+  </div>
+
+  <!-- Search bar -->
+  <div style="margin-bottom:18px;display:flex;gap:10px;align-items:center">
+    <input id="search-q" type="text" placeholder="Search public-domain images (e.g. peacock, baroque, Morris)" style="flex:1;padding:10px 14px;font-size:14px;border:1px solid #ccc;border-radius:6px">
+    <button id="btn-search" class="btn-primary">Search</button>
+    <label class="btn-outline" style="cursor:pointer;margin:0">
+      Upload image
+      <input id="upload-input" type="file" accept="image/*" hidden>
+    </label>
+  </div>
+  <div id="search-results" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:8px;margin-bottom:24px;max-height:340px;overflow-y:auto;border:1px solid #eee;border-radius:6px;padding:10px;background:#fff"></div>
+
+  <!-- 6-slot board -->
+  <h2 style="font-family:'Cormorant Garamond',serif;font-weight:400;font-size:20px;margin:24px 0 10px">Inspiration board (6 slots)</h2>
+  <div id="board" style="display:grid;grid-template-columns:repeat(6,1fr);gap:10px;margin-bottom:22px"></div>
+
+  <!-- 3-palette suggestions -->
+  <div style="padding:14px;border:1px solid #ddd;border-radius:8px;background:#fff;margin-bottom:22px">
+    <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
+      <h3 style="margin:0;font-size:14px">3 Suggested palettes</h3>
+      <button id="btn-suggest" class="btn-outline" style="font-size:12px;padding:4px 10px">Refresh suggestions</button>
+    </div>
+    <div id="palette-suggestions" style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px"></div>
+  </div>
+
+  <!-- Extra elements + compose -->
+  <div style="padding:18px;border:1px solid #ddd;border-radius:8px;background:#fafafa;margin-bottom:22px">
+    <h3 style="margin:0 0 8px;font-size:14px">Add more elements to the prompt</h3>
+    <div id="extra-elements-pills" style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px"></div>
+    <textarea id="extra-text" placeholder="Extra prompt text (optional)" rows="2" style="width:100%;padding:8px;border:1px solid #ccc;border-radius:4px;font-size:13px;font-family:inherit"></textarea>
+    <div style="display:flex;gap:10px;align-items:center;margin-top:10px;flex-wrap:wrap">
+      <button id="btn-compose" class="btn-primary">Compose new design</button>
+      <label style="font-size:12px;color:#555;display:flex;align-items:center;gap:6px"><input type="checkbox" id="auto-review" checked> auto-review with interior-designer</label>
+    </div>
+    <div id="compose-result" style="margin-top:14px"></div>
+    <div id="design-review" style="margin-top:14px"></div>
+  </div>
+
+  <!-- R0c/R0d — Personal gallery of saved renders -->
+  <div id="my-renders-section" style="padding:14px;border:1px solid #ddd;border-radius:8px;background:#fff;margin-bottom:24px;display:none">
+    <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
+      <h3 style="margin:0;font-size:14px">Your saved renders <span id="my-renders-count" style="font-size:11px;color:#999;font-weight:400"></span></h3>
+      <label style="font-size:11px;color:#666;cursor:pointer"><input type="checkbox" id="show-deleted-toggle"> show recently deleted (last 7d)</label>
+    </div>
+    <div id="my-renders-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:10px"></div>
+  </div>
+
+  <!-- R0c — personal renders gallery (this browser's session only) -->
+  <div id="studio-gallery" style="padding:14px;border:1px solid #ddd;border-radius:8px;background:#fff;margin-bottom:24px;display:none">
+    <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
+      <h3 style="margin:0;font-size:14px">Your renders <span style="font-size:11px;color:#999;font-weight:400" id="studio-gallery-count"></span></h3>
+      <span style="font-size:11px;color:#999">soft-deletes recoverable for 7 days</span>
+    </div>
+    <div id="studio-gallery-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px"></div>
+  </div>
+
+  <!-- Category insights -->
+  <div style="padding:14px;border:1px solid #ddd;border-radius:8px;background:#fff;margin-bottom:24px">
+    <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
+      <h3 style="margin:0;font-size:14px">Category insights — trends, best-sellers, ad signals</h3>
+      <span style="font-size:11px;color:#666">Category <select id="cat-select" style="font:inherit;padding:4px 6px;border:1px solid #ccc;border-radius:3px">
+        <option>floral</option><option>damask</option><option>geometric</option><option>botanical</option><option>baroque</option><option>art deco</option><option>chinoiserie</option><option>toile</option><option>maximalist</option><option>minimalist</option>
+      </select></span>
+    </div>
+    <div id="insights-body"></div>
+  </div>
+</main>
+${FOOTER}
+
+<script>
+var SID = localStorage.getItem('wc-studio-sid') || (Math.random().toString(36).slice(2)+Date.now().toString(36));
+localStorage.setItem('wc-studio-sid', SID);
+document.cookie = 'wc-studio-sid='+SID+'; path=/; max-age=2592000';
+
+async function get(u){ return (await fetch(u)).json(); }
+async function post(u,b){ return (await fetch(u,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b||{})})).json(); }
+async function del(u){ return (await fetch(u,{method:'DELETE'})).json(); }
+
+var EXTRA = new Set();
+
+function pill(text, on, onclick){
+  return '<button onclick="'+onclick+'" style="padding:3px 9px;font-size:11px;border-radius:11px;border:1px solid '+(on?'#1a1a1a':'#ccc')+';background:'+(on?'#1a1a1a':'#fff')+';color:'+(on?'#fff':'#333')+';cursor:pointer;margin:2px 1px">'+text+'</button>';
+}
+
+async function loadRefs(){
+  var [artists, styles, elements] = await Promise.all([
+    get('/api/studio/artists'), get('/api/studio/styles'), get('/api/studio/elements')
+  ]);
+  document.getElementById('artists-list').innerHTML = artists.map(function(a){
+    return '<div style="padding:5px 6px;border-bottom:1px solid #eee;cursor:pointer" onclick="addArtist('+JSON.stringify(a).replace(/"/g,'&quot;')+')"><b>'+a.name+'</b> <span style="color:#999">— '+(a.movement||'')+' '+(a.era||'')+'</span><br><span style="font-size:10px;color:#777">'+(a.one_line||'')+'</span></div>';
+  }).join('');
+  document.getElementById('styles-list').innerHTML = styles.map(function(s){
+    return '<div style="padding:5px 6px;border-bottom:1px solid #eee;cursor:pointer" onclick="addStyle('+JSON.stringify(s).replace(/"/g,'&quot;')+')"><b>'+s.name+'</b> <span style="color:#999">'+(s.era||'')+'</span><br><span style="font-size:10px;color:#777">'+(s.description||'')+'</span></div>';
+  }).join('');
+  // Elements grouped by category, with pills
+  var byCat = {};
+  elements.forEach(function(e){ (byCat[e.category]=byCat[e.category]||[]).push(e); });
+  window._ELEMENTS_BY_CAT = byCat;
+  renderElementsList();
+  renderExtra();
+}
+
+// Renders the elements-list pills with active/inactive styling based on EXTRA.
+// Pulled out so toggleExtra can re-render to show visual selected state.
+function renderElementsList() {
+  var byCat = window._ELEMENTS_BY_CAT || {};
+  document.getElementById('elements-list').innerHTML = Object.keys(byCat).sort().map(function(c){
+    return '<div style="margin-bottom:6px"><div class="eyebrow" style="margin-bottom:3px">'+c+'</div><div style="display:flex;flex-wrap:wrap">'+byCat[c].map(function(e){
+      var on = EXTRA.has(e.name);
+      var bg = on ? '#1a1a1a' : '#fff';
+      var fg = on ? '#fff' : '#333';
+      var bd = on ? '#1a1a1a' : '#ccc';
+      return '<button data-extra="'+e.name+'" onclick="toggleExtra(\\''+e.name+'\\')" style="padding:2px 7px;font-size:10px;border-radius:9px;border:1px solid '+bd+';background:'+bg+';color:'+fg+';cursor:pointer;margin:1px;transition:all .12s">'+e.name+'</button>';
+    }).join('')+'</div></div>';
+  }).join('');
+}
+
+function renderExtra() {
+  var html = Array.from(EXTRA).map(function(e){
+    return pill(e + ' ✕', true, "toggleExtra('"+e+"')");
+  }).join('') || '<span style="color:#999;font-size:12px">No extra elements selected yet — click pills above.</span>';
+  document.getElementById('extra-elements-pills').innerHTML = html;
+}
+
+window.toggleExtra = function(name){
+  if (EXTRA.has(name)) EXTRA.delete(name); else EXTRA.add(name);
+  renderExtra();
+  renderElementsList(); // re-render so the pill that was just clicked flips state
+};
+
+window.addArtist = function(a){
+  addToBoardNextSlot({source_type:'artist', source_id:String(a.id), title:a.name, artist:a.name, style_tags:[a.movement].filter(Boolean), color_hex_seeds:[], selected_elements:[]});
+};
+window.addStyle = function(s){
+  addToBoardNextSlot({source_type:'style', source_id:String(s.id), title:s.name, artist:null, style_tags:[s.name], color_hex_seeds:[], selected_elements:[]});
+};
+
+var BOARD = [];
+async function loadBoard(){
+  BOARD = await get('/api/studio/board?sid='+encodeURIComponent(SID));
+  renderBoard();
+}
+function nextEmptySlot(){
+  for (var i=0;i<6;i++){ if (!BOARD.find(function(b){return b.slot_idx===i;})) return i; }
+  return null;
+}
+async function addToBoardNextSlot(payload){
+  var idx = nextEmptySlot();
+  if (idx == null) { alert('Board is full (6 slots). Remove one first.'); return; }
+  await post('/api/studio/board/slot', Object.assign({sid:SID, slot_idx:idx}, payload));
+  loadBoard();
+}
+window.removeSlot = async function(slot){
+  await del('/api/studio/board/slot/'+slot+'?sid='+encodeURIComponent(SID));
+  loadBoard();
+};
+function renderBoard(){
+  var html = '';
+  for (var i=0;i<6;i++){
+    var b = BOARD.find(function(x){return x.slot_idx===i;});
+    if (b) {
+      var thumb = b.thumb_url || b.image_url || '';
+      var thumbHtml = thumb ? '<img src="'+thumb+'" style="width:100%;aspect-ratio:1;object-fit:cover;border-radius:4px">' :
+        '<div style="aspect-ratio:1;background:#'+((b.color_hex_seeds||[])[0]?(b.color_hex_seeds[0].replace('#','')):'2a2a2a')+';display:flex;align-items:center;justify-content:center;color:#fff;font-size:11px;border-radius:4px;text-align:center;padding:8px">'+(b.title||b.artist||'slot '+(i+1))+'</div>';
+      var tags = (b.style_tags||[]).concat(b.selected_elements||[]).slice(0,3).map(function(t){return '<span style="font-size:9px;background:#eee;padding:1px 5px;border-radius:7px;margin-right:2px">'+t+'</span>';}).join('');
+      html += '<div style="position:relative;border:1px solid #ddd;border-radius:6px;padding:6px;background:#fff">'+
+        thumbHtml +
+        '<div style="font-size:11px;margin-top:6px;color:#333;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="'+(b.title||'')+'">'+(b.title||'(no title)').slice(0,30)+'</div>'+
+        '<div style="font-size:10px;color:#888;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+(b.artist||b.source_type)+'</div>'+
+        '<div style="margin-top:4px">'+tags+'</div>'+
+        '<button onclick="removeSlot('+i+')" style="position:absolute;top:4px;right:4px;background:rgba(0,0,0,.6);color:#fff;border:0;border-radius:50%;width:20px;height:20px;font-size:11px;cursor:pointer;line-height:1;padding:0">✕</button>'+
+        '</div>';
+    } else {
+      html += '<div style="border:2px dashed #ccc;border-radius:6px;padding:6px;background:#fafafa;display:flex;align-items:center;justify-content:center;aspect-ratio:1;color:#aaa;font-size:11px">Slot '+(i+1)+'<br>(empty)</div>';
+    }
+  }
+  document.getElementById('board').innerHTML = html;
+}
+
+async function doSearch(){
+  var q = document.getElementById('search-q').value.trim();
+  if (!q) return;
+  var results = await get('/api/studio/search?q='+encodeURIComponent(q)+'&limit=60');
+  document.getElementById('search-results').innerHTML = results.map(function(r){
+    var thumb = r.thumb_url || '';
+    var label = (r.title||'').slice(0,40);
+    var meta = (r.artist||r.subsource||'') + (r.dominant_hex ? ' · '+r.dominant_hex : '');
+    return '<div onclick="addSearch('+JSON.stringify(r).replace(/"/g,'&quot;')+')" style="cursor:pointer;border:1px solid #eee;border-radius:4px;padding:4px;background:#fff;transition:transform .15s" onmouseover="this.style.transform=\\'scale(1.03)\\'" onmouseout="this.style.transform=\\'\\'">'+
+      (thumb?'<img src="'+thumb+'" style="width:100%;aspect-ratio:1;object-fit:cover;border-radius:3px" loading="lazy">':'<div style="aspect-ratio:1;background:#'+((r.dominant_hex||'#888').replace('#',''))+'"></div>')+
+      '<div style="font-size:10px;margin-top:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="'+label+'">'+label+'</div>'+
+      '<div style="font-size:9px;color:#888;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+meta+'</div>'+
+      '</div>';
+  }).join('') || '<p style="color:#999;grid-column:1/-1">No matches for "'+q+'".</p>';
+}
+window.addSearch = function(r){
+  addToBoardNextSlot({
+    source_type: r.source,
+    source_id: String(r.id),
+    image_url: r.image_url,
+    thumb_url: r.thumb_url,
+    title: r.title,
+    artist: r.artist,
+    style_tags: (r.tags||[]).slice(0,4),
+    color_hex_seeds: r.dominant_hex ? [r.dominant_hex] : [],
+    selected_elements: []
+  });
+};
+
+document.getElementById('btn-search').addEventListener('click', doSearch);
+document.getElementById('search-q').addEventListener('keydown', function(e){ if (e.key==='Enter') doSearch(); });
+
+document.getElementById('upload-input').addEventListener('change', async function(e){
+  var f = e.target.files[0]; if (!f) return;
+  var stat = document.getElementById('compose-result');
+  stat.style.display='block'; stat.innerHTML = '<div style="padding:10px;background:#fff3cd;color:#856404;border-radius:4px">Uploading + analyzing… (5-15s)</div>';
+  var buf = await f.arrayBuffer();
+  var r = await fetch('/api/studio/upload?sid='+encodeURIComponent(SID)+'&name='+encodeURIComponent(f.name),
+    { method:'POST', headers:{'Content-Type':'application/octet-stream'}, body: buf });
+  var j = await r.json();
+  if (j.ok) {
+    addToBoardNextSlot({
+      source_type:'upload', source_id:String(j.id),
+      image_url: j.url, thumb_url: j.url,
+      title: f.name + (j.ai_summary ? ' — ' + j.ai_summary.slice(0,40) : ''),
+      artist: null,
+      style_tags: j.ai_style ? [j.ai_style] : [],
+      color_hex_seeds: (j.ai_colors||[]).concat(j.dominant_hex ? [j.dominant_hex] : []),
+      selected_elements: j.ai_elements || []
+    });
+    stat.innerHTML = '<div style="padding:10px;background:#d4edda;color:#155724;border-radius:4px">✓ Uploaded. AI detected style: <b>'+(j.ai_style||'unknown')+'</b>, colors: '+(j.ai_colors||[]).join(' ')+', elements: '+(j.ai_elements||[]).join(', ')+'</div>';
+  } else {
+    stat.innerHTML = '<div style="padding:10px;background:#f8d7da;color:#721c24;border-radius:4px">Upload failed: '+(j.error||'unknown')+'</div>';
+  }
+});
+
+document.getElementById('btn-compose').addEventListener('click', async function(){
+  this.disabled = true;
+  var btn = this; btn.textContent = 'Composing (10-30s)…';
+  var stat = document.getElementById('compose-result');
+  stat.style.display='block'; stat.innerHTML = '<div style="padding:10px;background:#fff3cd;color:#856404;border-radius:4px">Generating…</div>';
+  var j = await post('/api/studio/compose', {
+    sid: SID,
+    extra_elements: Array.from(EXTRA),
+    extra_text: document.getElementById('extra-text').value || ''
+  });
+  btn.disabled = false; btn.textContent = 'Compose new design';
+  if (j.ok) {
+    // R0a — payoff frame: big pattern preview + actions. Replaces the old "✓ N generated" toast.
+    var imgUrl = j.image_url || ('/design/'+j.design_id);
+    var safePrompt = (j.prompt||'').replace(/[<>]/g,'');
+    stat.innerHTML = (
+      '<div id="studio-payoff" style="margin:18px 0;padding:0;border:1px solid #e2e2e2;border-radius:10px;background:#fff;overflow:hidden">'+
+        '<div style="padding:10px 14px;background:#0f0e0c;color:#d2b15c;font-size:11px;letter-spacing:.08em;text-transform:uppercase">Final pattern · #'+j.design_id+'</div>'+
+        '<div style="display:grid;grid-template-columns:minmax(0,1fr) 360px;gap:0;align-items:stretch">'+
+          // Big pattern preview (left)
+          '<a href="/design/'+j.design_id+'" target="_blank" style="display:block;aspect-ratio:1;background:url(\\''+imgUrl+'\\') center/cover #f2efe8;text-decoration:none;cursor:zoom-in" title="Open detail page" rel="noopener noreferrer"></a>'+
+          // Sidebar (right) — prompt + actions
+          '<div style="padding:18px;background:#fafafa;border-left:1px solid #eee;display:flex;flex-direction:column;gap:12px">'+
+            '<div>'+
+              '<div class="eyebrow" style="margin-bottom:4px">Prompt</div>'+
+              '<div style="font-size:11px;font-family:ui-monospace,Menlo,monospace;color:#333;line-height:1.45;max-height:140px;overflow-y:auto">'+safePrompt+'</div>'+
+            '</div>'+
+            '<div style="display:flex;flex-direction:column;gap:8px;margin-top:auto">'+
+              '<a href="/design/'+j.design_id+'" target="_blank" class="btn-primary" style="text-align:center" rel="noopener noreferrer">View design →</a>'+
+              '<a href="'+imgUrl+'" download class="btn-outline" style="text-align:center">Download pattern</a>'+
+              '<button id="studio-delete-btn" data-design-id="'+j.design_id+'" class="btn-outline" style="color:#a44;border-color:#d99">Delete this render</button>'+
+            '</div>'+
+          '</div>'+
+        '</div>'+
+        // Repeat-size control — pattern stays seamless; user picks how big a single repeat
+        // shows on the wall. Slider min = lots of small repeats; max = one giant pattern.
+        // Default sits at the larger end so the first render shows the design near full-size.
+        '<div style="padding:10px 14px;background:#fff;border-top:1px solid #eee">'+
+          '<label style="font-size:11px;color:#444;display:flex;align-items:center;gap:10px;cursor:pointer">'+
+            '<span style="white-space:nowrap;letter-spacing:.04em">Repeat size</span>'+
+            '<input type="range" id="studio-repeat-size" min="12" max="108" value="72" step="6" style="flex:1;accent-color:#0f0e0c">'+
+            '<span id="studio-repeat-val" style="font-family:ui-monospace,Menlo,monospace;color:#0f0e0c;min-width:70px;text-align:right;font-size:11px">72&quot; wide</span>'+
+          '</label>'+
+          '<div style="font-size:10px;color:#999;margin-top:3px">← more repeats (small tile) &nbsp;·&nbsp; one giant pattern →. Pattern stays seamless at every size.</div>'+
+        '</div>'+
+        // Instant client-side tile preview — pure CSS background-repeat so the slider
+        // gives 60fps feedback while the slow /api/room renders fire on the change event.
+        // Wall reference: ~120" wide (10 ft). The preview strip is 360px tall and
+        // proportional. Background-size in % maps slider inches → strip width:
+        //   tile_pct = (slider_inches / 120) * 100  →  e.g. 72" → 60% wide tile.
+        '<div style="padding:0 14px 12px;background:#fff;border-bottom:1px solid #eee">'+
+          '<div class="eyebrow" style="margin:8px 0 6px">Live tile preview · 10 ft wide wall</div>'+
+          '<div id="studio-tile-preview" data-pattern-url="'+imgUrl+'" '+
+               'style="height:140px;border:1px solid #eee;border-radius:4px;background-image:url(\\''+imgUrl+'\\');background-repeat:repeat;background-position:center;background-size:60% auto"></div>'+
+        '</div>'+
+        // 3 room settings row — one per category, in parallel.
+        '<div style="padding:14px 14px 12px;background:#fafafa">'+
+          '<div class="eyebrow" style="margin-bottom:8px">See it in a room · 3 categories</div>'+
+          '<div id="studio-rooms-grid" style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px">'+
+            '<div class="studio-room-cell" data-room="bedroom"     data-label="Bedroom"      style="position:relative;border:1px solid #eee;border-radius:6px;aspect-ratio:4/3;background:#fafafa center/cover no-repeat;display:flex;align-items:center;justify-content:center;color:#999;font-size:11px;overflow:hidden">Bedroom · loading…</div>'+
+            '<div class="studio-room-cell" data-room="living_room" data-label="Living Room"  style="position:relative;border:1px solid #eee;border-radius:6px;aspect-ratio:4/3;background:#fafafa center/cover no-repeat;display:flex;align-items:center;justify-content:center;color:#999;font-size:11px;overflow:hidden">Living Room · loading…</div>'+
+            '<div class="studio-room-cell" data-room="dining_room" data-label="Dining Room"  style="position:relative;border:1px solid #eee;border-radius:6px;aspect-ratio:4/3;background:#fafafa center/cover no-repeat;display:flex;align-items:center;justify-content:center;color:#999;font-size:11px;overflow:hidden">Dining Room · loading…</div>'+
+          '</div>'+
+          '<div style="font-size:9px;color:#bbb;margin:6px 0 0;letter-spacing:.04em">Rendered in parallel · ~10-30s each · drag the slider to re-render at a different scale</div>'+
+        '</div>'+
+      '</div>'
+    );
+    // R0b/T18 — kick off 3 room renders in parallel (one per category).
+    // The first room that lands is linked onto this studio_renders row so the
+    // personal gallery still shows pattern+room together.
+    //
+    // Slider-driven re-render: any change to #studio-repeat-size aborts in-flight
+    // fetches and fires a new batch with the updated patternWidth/Height. Each
+    // payoff frame owns its own AbortController so a fresh Compose can't race
+    // with an old slider drag.
+    var roomLinked = false;
+    var roomAbort = null;
+    function renderStudioRooms() {
+      if (roomAbort) { try { roomAbort.abort(); } catch(_) {} }
+      roomAbort = new AbortController();
+      var signal = roomAbort.signal;
+      var sliderEl = document.getElementById('studio-repeat-size');
+      var inches = sliderEl ? parseInt(sliderEl.value, 10) || 72 : 72;
+      var cells = document.querySelectorAll('#studio-rooms-grid .studio-room-cell');
+      // Reset every cell to the loading state before firing new fetches —
+      // this is what makes the visual refresh feel instant on each slider tick.
+      Array.prototype.forEach.call(cells, function(cell){
+        cell.style.backgroundImage = '';
+        cell.innerHTML = cell.dataset.label + ' · loading…';
+      });
+      Array.prototype.forEach.call(cells, function(cell){
+        var rt = cell.dataset.room;
+        var label = cell.dataset.label;
+        fetch('/api/room', {
+          method:'POST', headers:{'Content-Type':'application/json'},
+          signal: signal,
+          body: JSON.stringify({
+            design_id: j.design_id,
+            roomType: rt,
+            patternWidth: inches,
+            patternHeight: inches
+          })
+        }).then(function(r){ return r.json(); }).then(function(rd){
+          if (signal.aborted) return;
+          if (rd && rd.ok && rd.image_url) {
+            // Cache-bust on the rendered URL so an old image with the same
+            // path (rare, but possible if the server reuses filenames) can't
+            // hang around in the browser cache after a re-render.
+            cell.style.backgroundImage = 'url(\\''+rd.image_url+'?t='+Date.now()+'\\')';
+            cell.innerHTML = '<span style="position:absolute;bottom:6px;left:8px;background:rgba(0,0,0,.6);color:#fff;padding:2px 6px;border-radius:3px;font-size:9px;text-transform:uppercase;letter-spacing:.06em">'+label+' · #'+rd.room_id+'</span>';
+            if (rd.room_id && !roomLinked) {
+              roomLinked = true;
+              fetch('/api/studio-renders/'+j.design_id+'/link-room', {
+                method:'POST', headers:{'Content-Type':'application/json'},
+                body: JSON.stringify({ sid: SID, room_id: rd.room_id })
+              });
+              if (typeof loadMyRenders === 'function') loadMyRenders();
+            }
+          } else {
+            cell.innerHTML = '<span style="color:#c44;font-size:11px">'+label+' failed</span>';
+          }
+        }).catch(function(err){
+          if (err && err.name === 'AbortError') return;
+          cell.innerHTML = '<span style="color:#c44;font-size:11px">'+label+' failed</span>';
+        });
+      });
+    }
+    // Slider wiring — the input event fires continuously during drag (live tile
+    // preview); the change event fires on release (slow /api/room re-render,
+    // debounced 250ms). Value persists to localStorage so the user's preferred
+    // repeat size carries across composes and across browser sessions.
+    //
+    // Order matters: hydrate from localStorage BEFORE the initial renderStudioRooms
+    // call so the first /api/room batch uses the saved size, not the markup default.
+    var sliderEl = document.getElementById('studio-repeat-size');
+    var sliderVal = document.getElementById('studio-repeat-val');
+    var tilePrev = document.getElementById('studio-tile-preview');
+    var sliderDebounce = null;
+    var REPEAT_KEY = 'wallco.studio.repeat-inches';
+    function paintTilePreview(){
+      if (!tilePrev || !sliderEl) return;
+      // 120" reference wall; clamp 5%-100% so extremes stay readable.
+      var pct = Math.max(5, Math.min(100, (parseInt(sliderEl.value, 10) / 120) * 100));
+      tilePrev.style.backgroundSize = pct.toFixed(1) + '% auto';
+    }
+    if (sliderEl && sliderVal) {
+      // Hydrate from localStorage if the user has a preferred repeat size.
+      try {
+        var saved = parseInt(localStorage.getItem(REPEAT_KEY) || '', 10);
+        if (saved >= parseInt(sliderEl.min,10) && saved <= parseInt(sliderEl.max,10)) {
+          sliderEl.value = String(saved);
+          sliderVal.textContent = saved + '" wide';
+        }
+      } catch(_){}
+      sliderEl.addEventListener('input', function(){
+        sliderVal.textContent = sliderEl.value + '" wide';
+        paintTilePreview();
+      });
+      sliderEl.addEventListener('change', function(){
+        try { localStorage.setItem(REPEAT_KEY, sliderEl.value); } catch(_){}
+        if (sliderDebounce) clearTimeout(sliderDebounce);
+        sliderDebounce = setTimeout(renderStudioRooms, 250);
+      });
+      paintTilePreview();
+    }
+    renderStudioRooms();
+    // Delete button — wired by R0d.
+    var delBtn = document.getElementById('studio-delete-btn');
+    if (delBtn) delBtn.addEventListener('click', function(){
+      if (!confirm('Soft-delete render #'+j.design_id+'? (recoverable for 7 days)')) return;
+      this.disabled = true; this.textContent = 'Deleting…';
+      fetch('/api/studio-renders/'+j.design_id+'/delete', {
+        method:'POST',
+        headers:{'Content-Type':'application/json'},
+        body: JSON.stringify({ sid: SID })
+      })
+        .then(function(r){ return r.json(); })
+        .then(function(d){
+          if (d && d.ok) {
+            document.getElementById('studio-payoff').style.opacity = '0.4';
+            delBtn.textContent = '✓ Deleted (recoverable 7d)';
+            if (typeof loadMyRenders === 'function') loadMyRenders();
+          } else {
+            delBtn.textContent = 'Delete failed — retry';
+            delBtn.disabled = false;
+          }
+        })
+        .catch(function(){ delBtn.textContent = 'Delete failed — retry'; delBtn.disabled = false; });
+    });
+  } else {
+    stat.innerHTML = '<div style="padding:14px;background:#f8d7da;color:#721c24;border-radius:6px">Compose failed: '+(j.error||'unknown').replace(/[<>]/g,'')+'</div>';
+  }
+});
+
+async function loadTextures() {
+  var tx = await get('/api/studio/textures');
+  document.getElementById('textures-grid').innerHTML = tx.map(function(t){
+    return '<div draggable="true" ondragstart="dragTexture(event,'+JSON.stringify(t).replace(/"/g,'&quot;')+')" onclick="addTextureToBoard('+JSON.stringify(t).replace(/"/g,'&quot;')+')" style="cursor:grab;text-align:center;border:1px solid #eee;border-radius:4px;padding:3px;background:#fff;font-size:9px" title="'+t.name+' — '+t.category+'">'+
+      '<div style="aspect-ratio:1;background:url(\\''+t.image_url+'\\') center/cover, '+t.dominant_hex+';border-radius:3px"></div>'+
+      '<div style="margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+t.name+'</div>'+
+    '</div>';
+  }).join('');
+}
+
+window.dragTexture = function(e, t) {
+  e.dataTransfer.setData('application/x-texture', JSON.stringify(t));
+};
+window.addTextureToBoard = function(t) {
+  addToBoardNextSlot({
+    source_type:'texture', source_id:String(t.id),
+    image_url: t.image_url, thumb_url: t.thumb_url || t.image_url,
+    title: t.name + ' (texture)', artist: null,
+    style_tags: [t.category],
+    color_hex_seeds: t.dominant_hex ? [t.dominant_hex] : [],
+    selected_elements: [t.prompt_phrase || t.name]
+  });
+};
+
+// Drag-drop the texture / item onto a board slot
+function setupDropZones() {
+  document.querySelectorAll('[data-slot-zone]').forEach(function(z){
+    z.addEventListener('dragover', function(e){ e.preventDefault(); this.style.background='#fffbe6'; });
+    z.addEventListener('dragleave', function(){ this.style.background=''; });
+    z.addEventListener('drop', async function(e){
+      e.preventDefault(); this.style.background='';
+      var slot = parseInt(this.getAttribute('data-slot-zone'),10);
+      var t = e.dataTransfer.getData('application/x-texture');
+      if (t) {
+        var tx = JSON.parse(t);
+        await post('/api/studio/board/slot', {
+          sid: SID, slot_idx: slot,
+          source_type:'texture', source_id:String(tx.id),
+          image_url: tx.image_url, thumb_url: tx.thumb_url || tx.image_url,
+          title: tx.name + ' (texture)',
+          style_tags:[tx.category],
+          color_hex_seeds: tx.dominant_hex ? [tx.dominant_hex] : [],
+          selected_elements: [tx.prompt_phrase || tx.name]
+        });
+        loadBoard();
+      }
+    });
+  });
+}
+
+// Augment renderBoard to add data-slot-zone for drag-drop targets
+var _origRenderBoard = renderBoard;
+renderBoard = function(){
+  _origRenderBoard();
+  // Wrap each rendered slot with a dropzone marker
+  var grid = document.getElementById('board');
+  if (grid) {
+    Array.from(grid.children).forEach(function(child, i){
+      child.setAttribute('data-slot-zone', i);
+    });
+    setupDropZones();
+  }
+};
+
+async function loadSuggestions(){
+  var j = await post('/api/studio/suggest-palettes', { sid: SID });
+  var div = document.getElementById('palette-suggestions');
+  div.innerHTML = (j.palettes||[]).map(function(p,idx){
+    return '<div onclick="adoptPalette('+idx+')" style="cursor:pointer;padding:10px;border:1px solid #eee;border-radius:4px;background:#fafafa" onmouseover="this.style.background=\\'#fff\\'" onmouseout="this.style.background=\\'#fafafa\\'">'+
+      '<div style="font-size:12px;font-weight:500;margin-bottom:6px">'+p.name+'</div>'+
+      '<div style="display:flex;gap:2px;margin-bottom:6px">'+(p.colors||[]).map(function(c){return '<div style="flex:1;height:32px;background:'+c+';border-radius:2px"></div>';}).join('')+'</div>'+
+      '<div style="font-size:10px;color:#666">'+p.rationale+'</div>'+
+      '<div style="font-size:9px;color:#aaa;margin-top:3px;font-family:monospace">'+(p.colors||[]).join(' ')+'</div>'+
+    '</div>';
+  }).join('');
+  window.__palettes = j.palettes;
+}
+window.adoptPalette = async function(idx){
+  if (!window.__palettes || !window.__palettes[idx]) return;
+  var colors = window.__palettes[idx].colors || [];
+  // Add this palette as a new board slot of type 'palette'
+  addToBoardNextSlot({
+    source_type:'palette',
+    source_id:'sug-'+idx+'-'+Date.now(),
+    title: 'Palette: '+window.__palettes[idx].name,
+    artist: null,
+    style_tags: [],
+    color_hex_seeds: colors,
+    selected_elements: []
+  });
+};
+
+async function loadInsights(){
+  var cat = document.getElementById('cat-select').value;
+  var j = await get('/api/studio/category-insights?cat='+encodeURIComponent(cat));
+  var div = document.getElementById('insights-body');
+  var trendsHtml = '<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px">' + (j.trends||[]).map(function(t){
+    return '<a href="'+t.link+'" target="_blank" style="padding:4px 10px;background:#eef6ff;color:#1a5298;border-radius:11px;text-decoration:none;font-size:11px" rel="noopener noreferrer">📈 '+t.term+'</a>';
+  }).join('') + '</div>';
+  var bs = j.best_sellers || [];
+  var bsHtml = bs.length ? '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:6px">' + bs.map(function(p){
+    return '<div style="border:1px solid #eee;border-radius:4px;padding:4px;background:#fff;font-size:10px"><img src="'+(p.image_url||'')+'" loading="lazy" style="width:100%;aspect-ratio:1;object-fit:cover;border-radius:3px"><div style="margin-top:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+(p.title||'').slice(0,40)+'</div><div style="color:#888;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">Designer Wallcoverings</div></div>';
+  }).join('') + '</div>' : '<p style="color:#999;font-size:12px;margin:0">No best-sellers indexed for this category yet.</p>';
+  div.innerHTML = trendsHtml + bsHtml + '<p style="font-size:10px;color:#aaa;margin-top:8px">'+(j.ad_signals_note||'')+'</p>';
+}
+
+// Refresh-button + select-change hooks (deferred until DOM is wired)
+setTimeout(function(){
+  var btn = document.getElementById('btn-suggest');
+  if (btn) btn.addEventListener('click', loadSuggestions);
+  var sel = document.getElementById('cat-select');
+  if (sel) sel.addEventListener('change', loadInsights);
+  var op = document.getElementById('overlay-opacity');
+  if (op) op.addEventListener('input', function(){ document.getElementById('opacity-val').textContent = this.value + '%'; });
+}, 100);
+
+// Hook into compose: after success, optionally run interior-designer review
+var _origCompose = document.getElementById('btn-compose').onclick;
+document.getElementById('btn-compose').addEventListener('click', async function(){
+  // wait for compose to finish (the prior handler is async and already attached)
+  await new Promise(r => setTimeout(r, 800));
+  // poll for compose-result that includes design link
+  var tries = 0;
+  while (tries++ < 30) {
+    var html = document.getElementById('compose-result').innerHTML;
+    var m = html.match(/\\/design\\/(\\d+)/);
+    if (m && document.getElementById('auto-review').checked) {
+      var did = m[1];
+      var rev = document.getElementById('design-review');
+      rev.innerHTML = '<div style="padding:10px;background:#eef;color:#1a3;border-radius:4px;font-size:13px">Running interior-designer review…</div>';
+      var j = await post('/api/studio/interior-design-review', { design_id: parseInt(did,10) });
+      if (j.ok) {
+        var c = j.critique || {};
+        rev.innerHTML = '<div style="padding:14px;border:1px solid #ddd;border-radius:6px;background:#fafafa"><b>Interior-designer review · design #'+did+'</b>' +
+          '<div style="margin-top:8px;font-size:12px"><b>Strengths:</b> <ul style="margin:4px 0 8px 18px">'+ (c.strengths||[]).map(function(s){return '<li>'+s.replace(/[<>]/g,'')+'</li>';}).join('') +'</ul>' +
+          '<b>Concerns:</b> <ul style="margin:4px 0 8px 18px">'+ (c.concerns||[]).map(function(s){return '<li>'+s.replace(/[<>]/g,'')+'</li>';}).join('') +'</ul>' +
+          '<b>Recommendation:</b> '+ ((c.recommendation||'').replace(/[<>]/g,'')) +
+          '<br><b>Room fit:</b> '+ ((c.room_fit||[]).join(', ').replace(/[<>]/g,'')) + ' · <b>Lighting:</b> ' + ((c.lighting||'').replace(/[<>]/g,'')) + ' · <b>Scale:</b> ' + ((c.scale||'').replace(/[<>]/g,'')) +
+          '</div></div>';
+      } else {
+        rev.innerHTML = '<div style="padding:10px;background:#f8d7da;color:#721c24;border-radius:4px;font-size:13px">Review failed: '+(j.error||'unknown')+'</div>';
+      }
+      break;
+    }
+    await new Promise(r => setTimeout(r, 1500));
+  }
+});
+
+// R0c/R0d — personal gallery of this session's saved renders.
+async function loadMyRenders() {
+  var showDel = document.getElementById('show-deleted-toggle');
+  var includeDel = showDel && showDel.checked ? '&include_deleted=1' : '';
+  var j = await get('/api/studio-renders?sid=' + encodeURIComponent(SID) + includeDel);
+  var rs = (j && j.renders) || [];
+  var sec = document.getElementById('my-renders-section');
+  var grid = document.getElementById('my-renders-grid');
+  var count = document.getElementById('my-renders-count');
+  if (!sec || !grid) return;
+  if (!rs.length) { sec.style.display = 'none'; return; }
+  sec.style.display = 'block';
+  count.textContent = '· ' + rs.length + ' total';
+  grid.innerHTML = rs.map(function(r){
+    var img = r.image_url || '';
+    var dim = r.deleted_at ? 'opacity:.45;filter:grayscale(.6)' : '';
+    var roomBadge = r.room_image_url
+      ? '<span style="position:absolute;top:6px;left:6px;background:rgba(15,14,12,.8);color:#d2b15c;padding:2px 6px;border-radius:9px;font-size:9px;letter-spacing:.06em;text-transform:uppercase">+ room</span>'
+      : '';
+    var delBtn = r.deleted_at
+      ? '<button onclick="restoreRender(' + r.design_id + ')" style="font-size:10px;padding:4px 8px;background:transparent;color:#5a8;border:1px solid #5a8;border-radius:4px;cursor:pointer">Restore</button>'
+      : '<button onclick="deleteRender(' + r.design_id + ')" style="font-size:10px;padding:4px 8px;background:transparent;color:#a44;border:1px solid #d99;border-radius:4px;cursor:pointer">Delete</button>';
+    return '<div style="border:1px solid #eee;border-radius:6px;overflow:hidden;background:#fff;position:relative;' + dim + '">' +
+      '<a href="/design/' + r.design_id + '" target="_blank" style="display:block;aspect-ratio:1;background:url(\\'' + img + '\\') center/cover #f2efe8;text-decoration:none;position:relative" rel="noopener noreferrer">' + roomBadge + '</a>' +
+      '<div style="padding:6px 8px;font-size:10px;color:#666;display:flex;justify-content:space-between;align-items:center;gap:6px">' +
+        '<span>#' + r.design_id + (r.deleted_at ? ' · deleted' : '') + '</span>' + delBtn +
+      '</div>' +
+    '</div>';
+  }).join('');
+}
+window.deleteRender = async function(designId){
+  if (!confirm('Soft-delete render #'+designId+'? (recoverable for 7 days)')) return;
+  var r = await post('/api/studio-renders/'+designId+'/delete', { sid: SID });
+  if (r && r.ok) loadMyRenders(); else alert('Delete failed: '+((r&&r.error)||'unknown'));
+};
+window.restoreRender = async function(designId){
+  var r = await post('/api/studio-renders/'+designId+'/restore', { sid: SID });
+  if (r && r.ok) loadMyRenders(); else alert('Restore failed: '+((r&&r.error)||'unknown'));
+};
+setTimeout(function(){
+  var t = document.getElementById('show-deleted-toggle');
+  if (t) t.addEventListener('change', loadMyRenders);
+}, 80);
+
+loadRefs(); loadBoard(); loadTextures(); loadSuggestions(); loadInsights(); loadMyRenders();
+</script>
+${HAMBURGER_JS}
+</body>
+</html>`);
+});
+
+// ── Pairings: 3 nearest-palette neighbors in CIE Lab space.
+app.get('/api/design/:id/pairings', (req, res) => {
+  try {
+    const id = parseInt(req.params.id, 10);
+    const target = psqlQuery(`SELECT dominant_hex FROM spoon_all_designs WHERE id=${id};`);
+    if (!target || !/^#[0-9a-f]{6}$/i.test(target)) return res.json({ designs: [] });
+    // Compute in JS — straightforward Lab Euclidean
+    const all = JSON.parse(psqlQuery(`SELECT COALESCE(json_agg(t),'[]'::json) FROM (SELECT id, dominant_hex, local_path FROM spoon_all_designs WHERE id <> ${id} AND dominant_hex IS NOT NULL AND generator='replicate') t;`) || '[]');
+    function hexToRgb(h){h=h.replace('#','');return[parseInt(h.slice(0,2),16),parseInt(h.slice(2,4),16),parseInt(h.slice(4,6),16)];}
+    function srgbToLinear(c){c/=255;return c<=0.04045?c/12.92:Math.pow((c+0.055)/1.055,2.4);}
+    function rgbToLab(rgb){const[r,g,b]=rgb.map(srgbToLinear);
+      const x=r*0.4124+g*0.3576+b*0.1805, y=r*0.2126+g*0.7152+b*0.0722, z=r*0.0193+g*0.1192+b*0.9505;
+      const xn=0.95047,yn=1,zn=1.08883;
+      const f=t=>t>0.008856?Math.cbrt(t):(7.787*t+16/116);
+      return[116*f(y/yn)-16, 500*(f(x/xn)-f(y/yn)), 200*(f(y/yn)-f(z/zn))];
+    }
+    const tLab = rgbToLab(hexToRgb(target));
+    const scored = all.map(p => {
+      const pLab = rgbToLab(hexToRgb(p.dominant_hex));
+      const d = Math.sqrt((tLab[0]-pLab[0])**2+(tLab[1]-pLab[1])**2+(tLab[2]-pLab[2])**2);
+      return { id: p.id, dominant_hex: p.dominant_hex, distance: d,
+        image_url: '/designs/img/' + (p.local_path||'').split('/').pop() };
+    }).sort((a,b)=>a.distance-b.distance).slice(0,3);
+    res.json({ designs: scored });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ── 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 (legacy single-grid view).
+    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,
+        closeness: d.closeness,
+      }));
+
+    // Bucketed by canonical style — stripes (10) first, plaids (10) next,
+    // then rotation through remaining styles. Each item carries `style` +
+    // `tags` so the UI can render a tag pill on every thumb. Live-queries
+    // PG for full tag/motif/prompt context (the in-memory DESIGNS snapshot
+    // drops those fields).
+    let coordinates_by_style = [];
+    try {
+      const rawAll = psqlQuery(`
+        SELECT COALESCE(json_agg(t), '[]'::json) FROM (
+          SELECT id, dominant_hex, category, motifs, tags, prompt,
+                 ai_title AS title, ai_pattern AS pattern_name,
+                 local_path
+            FROM spoon_all_designs
+           WHERE id <> ${id}
+             AND (user_removed IS NOT TRUE)
+             AND local_path IS NOT NULL
+           LIMIT 4000
+        ) t;`);
+      const all = JSON.parse(rawAll || '[]').map(r => ({
+        ...r,
+        image_url: r.local_path
+          ? '/designs/img/' + path.basename(r.local_path)
+          : null,
+      })).filter(r => r.image_url);
+      coordinates_by_style = colorIsolate
+        .coordinatesByStyle(anchor, all, { stripeMax: 10, plaidMax: 10, perBucket: 8 })
+        .map(b => ({
+          ...b,
+          items: b.items.map(it => ({
+            id: it.id,
+            title: it.title || ('No. ' + it.id),
+            image_url: it.image_url,
+            dominant_hex: it.dominant_hex,
+            category: it.category,
+            style: it.style,
+            tags: (Array.isArray(it.tags) ? it.tags : []).slice(0, 5),
+            deltaE: it.deltaE,
+            closeness: it.closeness,
+          })),
+        }));
+    } catch (e) {
+      coordinates_by_style = [];
+    }
+
+    // R11 — annotate the design's stored dominant_hex with its nearest SW so
+    // consumers don't have to re-call nearestSW() for the canonical color.
+    const dominant_sw = design.dominant_hex ? colorIsolate.nearestSW(design.dominant_hex) : null;
+
+    res.json({
+      design: {
+        id: design.id,
+        title: design.title,
+        image_url: design.image_url,
+        category: design.category,
+        dominant_hex: design.dominant_hex,
+      },
+      dominant_sw,
+      palette,
+      coordinating_hexes,
+      coordinating_designs,
+      coordinates_by_style,
+    });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// ── POST /api/design/:id/bake — save a user-edited recolor as a new design
+// ── /api/design/:id/furnish — generate coordinated furniture + lighting +
+//    decor recommendations for a design, scoped to a use case (residential
+//    /commercial/hospitality/healthcare). Uses Gemini-text against the
+//    design's palette + motif + category. Read-only — no DB writes.
+//    furnish_route marker.
+// Disk-backed cache for /api/design/:id/furnish. The Gemini-text output
+// for a given (design × context) is stable — same palette + motif + use
+// case → same recommendations. Cache forever; manual purge via deleting
+// data/furnish-cache/<id>-<ctx>.json. Steve's 2026-05-13 cost note: each
+// click was hitting Gemini fresh; this drops repeat cost to zero.
+const FURNISH_CACHE_DIR = path.join(__dirname, 'data', 'furnish-cache');
+fs.mkdirSync(FURNISH_CACHE_DIR, { recursive: true });
+function furnishCachePath(id, ctx) {
+  return path.join(FURNISH_CACHE_DIR, `${id}-${ctx}.json`);
+}
+
+app.post('/api/design/:id/furnish', async (req, res) => {
+  try {
+    const id = parseInt(req.params.id, 10);
+    if (!Number.isFinite(id) || id < 1) return res.status(400).json({ ok: false, error: 'bad id' });
+    const ctx = String((req.body && req.body.context) || 'residential').toLowerCase();
+    const validCtx = new Set(['residential','commercial','hospitality','healthcare']);
+    if (!validCtx.has(ctx)) return res.status(400).json({ ok: false, error: 'bad context' });
+
+    // Cache hit — skip Gemini, return saved JSON. ?refresh=1 forces re-gen.
+    const cachePath = furnishCachePath(id, ctx);
+    if (req.query.refresh !== '1' && fs.existsSync(cachePath)) {
+      try {
+        const cached = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
+        return res.json({ ...cached, cached: true });
+      } catch (e) { /* fall through to fresh gen on parse error */ }
+    }
+
+    const d = DESIGNS.find(x => x.id === id);
+    if (!d) return res.status(404).json({ ok: false, error: 'design not found' });
+
+    const KEY = process.env.GEMINI_API_KEY;
+    if (!KEY) return res.status(500).json({ ok: false, error: 'GEMINI_API_KEY missing' });
+
+    const palette = Array.isArray(d.palette) ? d.palette.map(p => p.hex).slice(0, 5).join(', ') : '';
+    const motifs = Array.isArray(d.motifs) ? d.motifs.slice(0, 6).join(', ') : '';
+    const contextLabel = {
+      residential: 'a residential home interior',
+      commercial:  'a commercial office or retail space',
+      hospitality: 'a hospitality space (hotel lobby / restaurant / lounge)',
+      healthcare:  'a healthcare or clinic waiting room',
+    }[ctx];
+
+    const prompt = [
+      'You are an interior designer.',
+      `Wallcovering: "${d.title}". Category: ${d.category}. Motifs: ${motifs || 'n/a'}. Dominant palette hexes: ${palette || 'n/a'}.`,
+      `Use case: ${contextLabel}.`,
+      'Recommend coordinating furniture, lighting, rugs, art, tables, and accessories.',
+      'Return STRICT JSON only (no markdown, no prose) with this exact shape:',
+      '{ "categories": [ { "category": "Seating", "items": [ { "name": "...", "detail": "..." } ] }, ... ], "note": "one sentence styling note" }',
+      'Categories to include: Seating, Lighting, Rugs, Art, Tables, Accessories. 2-3 items each. Detail is 1 short phrase (material + color cue).',
+    ].join(' ');
+
+    const r = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${KEY}`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({
+        contents: [{ parts: [{ text: prompt }] }],
+        generationConfig: { responseMimeType: 'application/json', temperature: 0.7 },
+      }),
+    });
+    if (!r.ok) return res.status(502).json({ ok: false, error: 'gemini ' + r.status, body: (await r.text()).slice(0,200) });
+    const j = await r.json();
+    try {
+      const { logGemini } = require(require('os').homedir() + '/.claude/skills/cost-tracker/scripts/log-gemini.js');
+      logGemini(j, { app: 'wallco-ai', note: 'furnish-' + ctx, model: 'gemini-2.5-flash' });
+    } catch {}
+    const text = j.candidates?.[0]?.content?.parts?.find(p => p.text)?.text || '{}';
+    let parsed = {};
+    try { parsed = JSON.parse(text); } catch (e) { return res.status(502).json({ ok: false, error: 'parse failed', text: text.slice(0,200) }); }
+    const payload = { ok: true, design_id: id, context: ctx, categories: parsed.categories || [], note: parsed.note || '' };
+    // Write-through cache (best-effort; ignore disk errors)
+    try {
+      const tmp = cachePath + '.tmp';
+      fs.writeFileSync(tmp, JSON.stringify({ ...payload, generated_at: new Date().toISOString() }, null, 2));
+      fs.renameSync(tmp, cachePath);
+    } catch (e) { console.warn('[furnish-cache] write:', e.message); }
+    return res.json(payload);
+  } catch (e) {
+    return res.status(500).json({ ok: false, error: e.message });
+  }
+});
+
+// ── /api/design/:id/recolor — tone-on-tone color-wheel recoloring ──
+// Admin-only. Takes a source design + hue family, calls Gemini image-edit
+// with a strict mono-hue prompt, inserts a new spoon_all_designs row,
+// returns the new id + image_url.
+//
+// Steve's 2026-05-13 ask: "Add buttons to admin to create in color wheel
+// color. for tone on tone items." This endpoint backs those buttons.
+const RECOLOR_HUES = {
+  red:     { label: 'Red',      tail: 'crimson red and burgundy ground; deeper near-black-red motif; rich saturation' },
+  orange:  { label: 'Orange',   tail: 'warm terracotta-orange ground; deeper burnt-orange motif; earthy saturation' },
+  yellow:  { label: 'Yellow',   tail: 'soft butter-yellow ground; deeper gold-ochre motif; vintage feel' },
+  lime:    { label: 'Lime',     tail: 'pale lime-chartreuse ground; deeper olive-lime motif; fresh herbaceous' },
+  green:   { label: 'Green',    tail: 'sage to forest green ground; deeper hunter motif; tonal jewel tones' },
+  teal:    { label: 'Teal',     tail: 'teal-aqua ground; deeper near-black teal motif; tonal lacquer feel' },
+  cyan:    { label: 'Cyan',     tail: 'pale cyan-aqua ground; deeper peacock motif; subtle marine' },
+  blue:    { label: 'Blue',     tail: 'powder to navy blue ground; near-black-navy motif; tonal jewel' },
+  indigo:  { label: 'Indigo',   tail: 'rich indigo ground; near-black indigo motif; deep tonal' },
+  violet:  { label: 'Violet',   tail: 'lavender to deep violet ground; near-black violet motif; tonal' },
+  magenta: { label: 'Magenta',  tail: 'fuchsia-magenta ground; deeper burgundy-magenta motif; saturated tonal' },
+  pink:    { label: 'Pink',     tail: 'blush to dusty rose ground; deeper rose motif; tonal romantic' },
+  neutral: { label: 'Neutral',  tail: 'warm cream/taupe ground; deeper sepia-bronze motif; tonal vintage' },
+  charcoal:{ label: 'Charcoal', tail: 'pale grey ground; deep charcoal motif; tonal monochrome' },
+};
+
+app.post('/api/design/:id/recolor', express.json(), async (req, res) => {
+  if (!isAdmin(req)) return res.status(404).json({ error: 'not found' });
+  try {
+    const id = parseInt(req.params.id, 10);
+    if (!Number.isFinite(id) || id < 1) return res.status(400).json({ ok: false, error: 'bad id' });
+    const hueKey = String((req.body && req.body.hue) || '').toLowerCase();
+    const hue = RECOLOR_HUES[hueKey];
+    if (!hue) return res.status(400).json({ ok: false, error: 'bad hue', valid: Object.keys(RECOLOR_HUES) });
+
+    const KEY = process.env.GEMINI_API_KEY;
+    if (!KEY) return res.status(500).json({ ok: false, error: 'GEMINI_API_KEY missing' });
+
+    // Load source image bytes from local_path or by reading the file behind image_url
+    const d = DESIGNS.find(x => x.id === id);
+    if (!d) return res.status(404).json({ ok: false, error: 'design not found' });
+    const srcPath = d.local_path || (d.filename ? path.join(IMG_DIR, d.filename) : null)
+      || (d.image_url && d.image_url.startsWith('/designs/img/') ? path.join(IMG_DIR, d.image_url.replace('/designs/img/', '')) : null);
+    if (!srcPath || !fs.existsSync(srcPath)) return res.status(404).json({ ok: false, error: 'source file missing on disk', srcPath });
+    const srcB64 = fs.readFileSync(srcPath).toString('base64');
+
+    const prompt = `Recolor everything into the ${hue.label.toUpperCase()} family ONLY. ${hue.tail}. `
+      + 'Tone-on-tone: strictly one hue family throughout — no contrasting hues anywhere. '
+      + 'Preserve the motif, composition, and pattern repeat exactly. '
+      + 'Seamless tile, no edge artifacts, no signature, no watermark, no text. '
+      + 'High detail, archival quality, fabric-pattern aesthetic.';
+
+    const t0 = Date.now();
+    const gr = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent?key=${KEY}`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({
+        contents: [{ parts: [{ inline_data: { mime_type: 'image/png', data: srcB64 } }, { text: prompt }] }],
+        generationConfig: { responseModalities: ['IMAGE'] },
+      }),
+    });
+    if (!gr.ok) return res.status(502).json({ ok: false, error: 'gemini ' + gr.status, body: (await gr.text()).slice(0, 200) });
+    const gj = await gr.json();
+    try {
+      const { logGemini } = require(require('os').homedir() + '/.claude/skills/cost-tracker/scripts/log-gemini.js');
+      logGemini(gj, { app: 'wallco-ai', note: 'recolor-' + hueKey + '-from-' + id, model: 'gemini-2.5-flash-image' });
+    } catch {}
+    const part = gj.candidates?.[0]?.content?.parts?.find(p => p.inline_data || p.inlineData);
+    const data = part?.inline_data?.data || part?.inlineData?.data;
+    if (!data) return res.status(502).json({ ok: false, error: 'no image returned', finish: gj.candidates?.[0]?.finishReason });
+
+    // Save PNG
+    const crypto2 = require('crypto');
+    const seed = crypto2.randomInt(1, 2 ** 31 - 1);
+    const filename = `recolor_${Date.now()}_p${id}_${hueKey}_${seed}.png`;
+    const outPath = path.join(IMG_DIR, filename);
+    fs.writeFileSync(outPath, Buffer.from(data, 'base64'));
+    const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
+
+    // Insert into PG
+    const parentTitle = (d.title || ('Design ' + id)).replace(/\s+No\.\d+$/, '');
+    const newTitle = `${parentTitle} (${hue.label} tonal)`;
+    const baseTags = ['recolor', 'tone-on-tone', hueKey];
+    if (Array.isArray(d.motifs)) baseTags.push(...d.motifs.slice(0, 3));
+    const tagSql = 'ARRAY[' + baseTags.map(t => "'" + String(t).replace(/'/g, "''") + "'").join(',') + ']::text[]';
+    const motifsSql = Array.isArray(d.motifs) && d.motifs.length
+      ? 'ARRAY[' + d.motifs.map(t => "'" + String(t).replace(/'/g, "''") + "'").join(',') + ']::text[]'
+      : "ARRAY[]::text[]";
+    const promptDesc = (newTitle + ' — ' + prompt).slice(0, 1800).replace(/'/g, "''");
+    const insertSql = `INSERT INTO spoon_all_designs
+      (kind, width_in, height_in, generator, prompt, seed, dominant_hex,
+       local_path, image_url, category, motifs, tags, is_published, request_text)
+      VALUES ('seamless_tile', 24, 24, 'gemini-2.5-flash-image-edit',
+        '${promptDesc}', ${seed}, NULL,
+        '${outPath.replace(/'/g, "''")}', '/designs/img/${filename}',
+        ${"'" + (d.category || 'recolor') + "'"},
+        ${motifsSql}, ${tagSql}, TRUE,
+        '${("Tone-on-tone " + hue.label + " recolor of design " + id).replace(/'/g, "''")}')
+      RETURNING id;`;
+    const newIdStr = psqlQuery(insertSql);
+    const newId = parseInt(newIdStr, 10);
+
+    // Push into in-memory DESIGNS so it shows up without a server restart
+    DESIGNS.unshift({
+      id: newId,
+      kind: 'seamless_tile',
+      category: d.category || 'recolor',
+      title: newTitle,
+      handle: 'wallco-' + newId,
+      image_url: '/designs/img/' + filename,
+      filename,
+      local_path: outPath,
+      generator: 'gemini-2.5-flash-image-edit',
+      seed,
+      is_published: true,
+      created_at: new Date().toISOString(),
+      motifs: d.motifs || [],
+      tags: baseTags,
+    });
+
+    res.json({ ok: true, parent_id: id, new_id: newId, new_image_url: '/designs/img/' + filename, hue: hueKey, elapsed_s: elapsed });
+  } catch (e) {
+    res.status(500).json({ ok: false, error: e.message });
+  }
+});
+
+app.post('/api/design/:id/bake',
+  express.json({ limit: '20mb' }),
+  (req, res) => {
+    const parentId = parseInt(req.params.id, 10);
+    const { image_b64 } = req.body || {};
+    if (!image_b64) return res.status(400).json({ ok:false, error:'image_b64 required' });
+    try {
+      const filename = `bake_${Date.now()}_p${parentId}.png`;
+      const outPath = path.join(IMG_DIR, filename);
+      fs.writeFileSync(outPath, Buffer.from(image_b64, 'base64'));
+      // Watermark the baked image (silent no-op if Python/Pillow missing)
+      try {
+        spawnSync('python3', [
+          path.join(__dirname, 'scripts', 'watermark.py'),
+          'embed', outPath, '--out', outPath,
+          '--owner', process.env.WATERMARK_OWNER || 'SAND, LLC',
+          '--year', String(new Date().getFullYear()),
+        ], { timeout: 30000 });
+      } catch {}
+      // Insert into spoon_all_designs (no Replicate call; this is user-edited)
+      const parent = DESIGNS.find(d => d.id === parentId) || {};
+      const id = psqlQuery(`INSERT INTO spoon_all_designs
+        (kind, width_in, height_in, panels, generator, prompt, seed,
+         pd_source_ids, local_path, dominant_hex, category, is_published, notes)
+        VALUES('seamless_tile', 24, 24, 1, 'replicate',
+               'user-baked recolor of design #${parentId}', 0,
+               '{}', '${outPath.replace(/'/g,"''")}',
+               ${parent.dominant_hex ? "'"+parent.dominant_hex+"'" : 'NULL'},
+               ${parent.category ? "'"+parent.category.replace(/'/g,"''")+"'" : 'NULL'},
+               FALSE, 'baked from design #${parentId}')
+        RETURNING id;`);
+      const newId = parseInt(id, 10);
+      // Eagerly synthesize a snapshot-style design + push to in-memory list
+      // so the upcoming GET /design/:newId hits the fast path immediately —
+      // no blocking snapshot regen, no race on redirect.
+      try {
+        const hex = parent.dominant_hex || null;
+        const cat = parent.category || 'mixed';
+        DESIGNS.push({
+          id: newId,
+          kind: 'seamless_tile',
+          category: cat,
+          dominant_hex: hex,
+          saturation: hex ? hexSaturation(hex) : 0,
+          title: titleFor(cat, hex, newId),
+          handle: `wallco-${String(newId).padStart(4,'0')}`,
+          image_url: `/designs/img/${filename}`,
+          filename,
+          generator: 'wallco.ai',
+          seed: 0,
+          is_published: false,
+          created_at: new Date().toISOString(),
+          notes: `baked from design #${parentId}`
+        });
+      } catch {}
+      // Snapshot regen runs in background (non-blocking) — the in-memory
+      // push above means the redirect works immediately regardless.
+      try {
+        const proc = spawn('python3',
+          [path.join(__dirname, 'scripts', 'refresh_designs_snapshot.py')],
+          { detached: true, stdio: 'ignore' });
+        proc.unref();
+      } catch {}
+      res.json({ ok:true, design_id: newId, filename });
+    } catch (e) {
+      res.status(500).json({ ok:false, error: e.message });
+    }
+  }
+);
+
+// ── GET /api/design/:id/rating — AI suggested rating
+app.get('/api/design/:id/rating', (req, res) => {
+  const id = parseInt(req.params.id, 10);
+  if (!Number.isFinite(id) || id < 1) return res.status(404).json({});
+  try {
+    const row = psqlQuery(`SELECT row_to_json(t) FROM (SELECT gd_score, gd_critique,
+      id_score, id_critique, combined_score, rating_summary, rated_at
+      FROM spoon_all_designs WHERE id=${id}) t;`);
+    if (!row) return res.json({});
+    res.json(JSON.parse(row));
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+// ── POST /api/design/:id/ai-rate — fire the AI rater now
+//    (renamed from /rate to avoid colliding with the human-rating endpoint
+//     at line ~1640 that's gated to admin)
+app.post('/api/design/:id/ai-rate', (req, res) => {
+  const id = parseInt(req.params.id, 10);
+  if (!Number.isFinite(id) || id < 1) return res.status(404).json({ ok: false });
+  const proc = spawn('node',
+    [path.join(__dirname, 'scripts', 'rate_design.js'), '--id', String(id)],
+    { detached: true, stdio: 'ignore', env: process.env });
+  proc.unref();
+  res.json({ ok: true, queued: id });
+});
+
+// ─────────────────────────────────────────────────────────────────────────
+// GAMIFY — user votes + head-to-head polls + Elo + learning loop
+// ─────────────────────────────────────────────────────────────────────────
+function voterSession(req, res) {
+  const m = (req.headers.cookie || '').match(/wc-voter=([a-z0-9]+)/i);
+  if (m) return m[1];
+  const id = require('crypto').randomBytes(10).toString('hex');
+  res.setHeader('Set-Cookie', `wc-voter=${id}; Path=/; Max-Age=31536000; SameSite=Lax`);
+  return id;
+}
+
+app.post('/api/design/:id/user-vote', (req, res) => {
+  const id = parseInt(req.params.id, 10);
+  const body = req.body || {};
+  const s = parseInt(body.score, 10);
+  const quick = !!body.quick;
+  if (!Number.isFinite(id) || id < 1) return res.status(400).json({ error: 'bad id' });
+  if (!Number.isFinite(s) || s < 1 || s > 5) return res.status(400).json({ error: 'score 1..5 required' });
+
+  // Reasons + note are NOT-only metadata — but accept them on any score for forward-compat.
+  // Reasons: array of short strings, cap at 12. Note: free text, cap at 2 KB.
+  const reasons = Array.isArray(body.reasons)
+    ? body.reasons.map(r => String(r || '').trim()).filter(Boolean).slice(0, 12)
+    : [];
+  const note = (typeof body.note === 'string') ? body.note.trim().slice(0, 2048) : '';
+
+  const session = voterSession(req, res);
+  const sqlEsc = (v) => String(v).replace(/'/g, "''");
+  try {
+    // Detect once whether the schema migration has been applied. Cached on first call.
+    if (typeof app.locals._hon_cols_ready === 'undefined') {
+      app.locals._hon_cols_ready = !!psqlQuery(
+        `SELECT 1 FROM information_schema.columns WHERE table_name='wallco_votes' AND column_name='reasons' LIMIT 1;`
+      );
+    }
+    const colsReady = app.locals._hon_cols_ready;
+
+    if (colsReady) {
+      const reasonsSql = reasons.length
+        ? `ARRAY[${reasons.map(r => `'${sqlEsc(r)}'`).join(',')}]::text[]`
+        : `NULL`;
+      const noteSql = note ? `'${sqlEsc(note)}'` : `NULL`;
+      psqlQuery(`INSERT INTO wallco_votes (design_id, voter_session, score, quick, reasons, note)
+        VALUES (${id}, '${sqlEsc(session)}', ${s}, ${quick ? 'TRUE' : 'FALSE'}, ${reasonsSql}, ${noteSql})
+        ON CONFLICT (design_id, voter_session) DO UPDATE SET
+          score=EXCLUDED.score, reasons=EXCLUDED.reasons, note=EXCLUDED.note, created_at=now();`);
+    } else {
+      // Graceful degrade: persist the vote, log reasons/note to disk for later replay.
+      psqlQuery(`INSERT INTO wallco_votes (design_id, voter_session, score, quick)
+        VALUES (${id}, '${sqlEsc(session)}', ${s}, ${quick ? 'TRUE' : 'FALSE'})
+        ON CONFLICT (design_id, voter_session) DO UPDATE SET score=EXCLUDED.score, created_at=now();`);
+      if (reasons.length || note) {
+        try {
+          const queue = path.join(__dirname, 'logs', 'hon-reasons-queue.jsonl');
+          fs.mkdirSync(path.dirname(queue), { recursive: true });
+          fs.appendFileSync(queue, JSON.stringify({
+            ts: new Date().toISOString(),
+            design_id: id, voter_session: session, score: s, reasons, note
+          }) + '\n');
+        } catch (qe) { /* non-fatal */ }
+      }
+    }
+
+    const agg = JSON.parse(psqlQuery(`SELECT row_to_json(t) FROM (SELECT
+      ROUND(AVG(score)::numeric, 2)::text AS avg, COUNT(*)::int AS cnt
+      FROM wallco_votes WHERE design_id=${id}) t;`));
+    psqlQuery(`UPDATE spoon_all_designs SET user_score_avg=${agg.avg || 'NULL'},
+      user_vote_count=${agg.cnt} WHERE id=${id};`);
+    res.json({
+      ok: true,
+      user_score_avg: parseFloat(agg.avg),
+      user_vote_count: agg.cnt,
+      reasons_persisted: colsReady,
+      reasons_queued: !colsReady && (reasons.length > 0 || !!note)
+    });
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+app.get('/api/design/:id/votes', (req, res) => {
+  const id = parseInt(req.params.id, 10);
+  if (!Number.isFinite(id)) return res.json({});
+  const session = voterSession(req, res);
+  try {
+    const my = psqlQuery(`SELECT score FROM wallco_votes WHERE design_id=${id} AND voter_session='${session}' LIMIT 1;`);
+    const agg = psqlQuery(`SELECT COALESCE(user_score_avg::text,'') || '|' || COALESCE(user_vote_count::text,'0') FROM spoon_all_designs WHERE id=${id};`);
+    const [avg, cnt] = (agg || '|').split('|');
+    res.json({ my_score: my ? parseInt(my,10) : null, avg: avg ? parseFloat(avg) : null, count: parseInt(cnt,10) || 0 });
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+app.get('/api/poll/pair', (_req, res) => {
+  try {
+    const raw = psqlQuery(`SELECT COALESCE(json_agg(t), '[]'::json) FROM
+      (SELECT id, dominant_hex, image_url, local_path, category,
+              (poll_wins + poll_losses) AS plays
+       FROM spoon_all_designs WHERE local_path IS NOT NULL
+       ORDER BY (poll_wins + poll_losses) ASC, random() LIMIT 40) t;`);
+    const pool = JSON.parse(raw || '[]');
+    if (pool.length < 2) return res.json({ error: 'not enough designs' });
+    const a = pool[Math.floor(Math.random() * pool.length)];
+    let b = pool[Math.floor(Math.random() * pool.length)];
+    while (b.id === a.id) b = pool[Math.floor(Math.random() * pool.length)];
+    function imgFor(d){ return '/designs/img/' + (d.local_path || '').split('/').pop(); }
+    res.json({
+      a: { id: a.id, title: titleFor(a.category, a.dominant_hex, a.id), image_url: imgFor(a), category: a.category },
+      b: { id: b.id, title: titleFor(b.category, b.dominant_hex, b.id), image_url: imgFor(b), category: b.category }
+    });
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+app.post('/api/poll/vote', (req, res) => {
+  const w = parseInt((req.body || {}).winner, 10), l = parseInt((req.body || {}).loser, 10);
+  if (!Number.isFinite(w) || !Number.isFinite(l) || w === l) return res.status(400).json({ error: 'winner+loser required, must differ' });
+  const session = voterSession(req, res);
+  try {
+    psqlQuery(`INSERT INTO wallco_poll_pairs (design_a, design_b, winner, voter_session)
+      VALUES (${Math.min(w,l)}, ${Math.max(w,l)}, ${w}, '${session}');`);
+    const rows = psqlQuery(`SELECT id::text || '|' || elo::text FROM spoon_all_designs WHERE id IN (${w}, ${l});`).split('\n').filter(Boolean);
+    const elos = {};
+    rows.forEach(r => { const [id, e] = r.split('|'); elos[id] = parseFloat(e); });
+    const Ra = elos[w] || 1200, Rb = elos[l] || 1200;
+    const Ea = 1 / (1 + Math.pow(10, (Rb - Ra) / 400));
+    const K = 32;
+    const newRa = Ra + K * (1 - Ea), newRb = Rb + K * (0 - (1 - Ea));
+    psqlQuery(`UPDATE spoon_all_designs SET elo=${newRa.toFixed(2)}, poll_wins=poll_wins+1 WHERE id=${w};`);
+    psqlQuery(`UPDATE spoon_all_designs SET elo=${newRb.toFixed(2)}, poll_losses=poll_losses+1 WHERE id=${l};`);
+    res.json({ ok: true, new_elo: { [w]: +newRa.toFixed(2), [l]: +newRb.toFixed(2) } });
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+app.get('/api/leaderboard', (req, res) => {
+  const limit = Math.min(parseInt(req.query.limit || '20', 10), 100);
+  try {
+    const raw = psqlQuery(`SELECT COALESCE(json_agg(t), '[]'::json) FROM (
+      SELECT id, dominant_hex, category, local_path, combined_score,
+             user_score_avg, user_vote_count, poll_wins, poll_losses, elo,
+             (COALESCE(combined_score,3) * 0.4
+              + COALESCE(user_score_avg,3) * 0.4
+              + (elo - 1200) / 200.0) AS rank_score
+      FROM spoon_all_designs WHERE local_path IS NOT NULL
+      ORDER BY rank_score DESC NULLS LAST LIMIT ${limit}) t;`);
+    res.json(JSON.parse(raw || '[]'));
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+// ── POST /api/personal/recs — taste-signature-based product picks.
+//    Body: { categories, hexes, ids (excludeIds), limit }
+//    Returns: { picks:[{id, title, image_url, category, dominant_hex,
+//                       combined_score, sample_url}], signature }
+//    The client-side WC engine on /hot-or-not + /swipe + /play surfaces
+//    these once refinement ≥80%.
+app.post('/api/personal/recs', (req, res) => {
+  var { categories = [], hexes = [], ids = [], limit = 8 } = req.body || {};
+  categories = (categories || []).filter(Boolean).slice(0, 5);
+  hexes      = (hexes      || []).filter(h => /^#[0-9a-f]{6}$/i.test(h)).slice(0, 8);
+  ids        = (ids        || []).map(x => parseInt(x,10)).filter(Number.isFinite);
+
+  var catSql = categories.length
+    ? `category IN (${categories.map(c => "'" + String(c).replace(/'/g,"''") + "'").join(',')})`
+    : '1=1';
+  var exclSql = ids.length ? `AND id NOT IN (${ids.join(',')})` : '';
+
+  try {
+    var raw = psqlQuery(`SELECT COALESCE(json_agg(t), '[]'::json) FROM (
+      SELECT id, prompt, dominant_hex, category, local_path,
+             combined_score, user_score_avg, user_vote_count, elo,
+             (COALESCE(combined_score, 3) * 0.4
+              + COALESCE(user_score_avg, 3) * 0.4
+              + (elo - 1200) / 200.0
+              + CASE WHEN ${catSql} THEN 1.0 ELSE 0 END
+             ) AS rank_score
+      FROM spoon_all_designs
+      WHERE local_path IS NOT NULL
+        ${exclSql}
+      ORDER BY rank_score DESC NULLS LAST
+      LIMIT ${parseInt(limit, 10) || 8}) t;`);
+    var rows = JSON.parse(raw || '[]');
+    var picks = rows.map(r => {
+      var fn = (r.local_path || '').split('/').pop();
+      return {
+        id: r.id,
+        title: titleFor(r.category, r.dominant_hex, r.id),
+        image_url: '/designs/img/' + fn,
+        category: r.category,
+        dominant_hex: r.dominant_hex,
+        combined_score: r.combined_score ? Number(r.combined_score).toFixed(1) : null,
+        sample_url: '/samples?design=' + r.id
+      };
+    });
+    res.json({ picks: picks, signature: { categories: categories, hexes: hexes } });
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+app.get('/api/poll/stats', (_req, res) => {
+  try {
+    const pairs = psqlQuery(`SELECT COUNT(*) FROM wallco_poll_pairs;`);
+    const designs = psqlQuery(`SELECT COUNT(*) FROM spoon_all_designs WHERE (poll_wins+poll_losses)>0;`);
+    res.json({ total_pairs: parseInt(pairs,10) || 0, designs_polled: parseInt(designs,10) || 0 });
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+app.get('/poll', (_req, res) => {
+  res.type('html').send(`${htmlHead({
+    title: 'Pick your favorite — wallco.ai',
+    description: 'Vote between two AI-original wallpaper designs. Your taste teaches the system.',
+    canonical: 'https://wallco.ai/poll'
+  })}
+<body>
+${htmlHeader('')}
+<main style="padding:24px 28px;max-width:1300px;margin:0 auto">
+  <header style="text-align:center;margin-bottom:24px">
+    <h1 style="font-family:'Cormorant Garamond',serif;font-weight:300;font-size:32px;margin:0 0 6px">Pick your favorite</h1>
+    <p style="color:#666;margin:0 0 12px">Click the one you'd hang on your wall. Your taste teaches the system &mdash; every vote nudges what we generate next.</p>
+    <div id="poll-stats" style="font-size:12px;color:#888"></div>
+  </header>
+  <!-- Gamification banner — fills via WC.refreshBanner() in wallco-game.js -->
+  <div id="wc-game-banner" style="background:#1a1816;border-radius:10px;padding:14px 18px;margin-bottom:22px;border:1px solid rgba(210,177,92,.2)"></div>
+  <div id="poll-arena" style="display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-bottom:32px">
+    <div class="poll-card" id="card-a"><div class="poll-loading">Loading&hellip;</div></div>
+    <div class="poll-card" id="card-b"></div>
+  </div>
+  <div style="text-align:center"><button id="btn-skip" class="btn-outline">Skip &mdash; show me another pair</button></div>
+  <hr style="margin:32px 0;border:0;border-top:1px solid rgba(0,0,0,.1)">
+  <h2 style="font-family:'Cormorant Garamond',serif;font-weight:400;font-size:22px;margin:0 0 14px;text-align:center">Top 20 &mdash; combined AI + user score + Elo</h2>
+  <div id="leaderboard" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px"></div>
+</main>
+${FOOTER}
+<style>
+  .poll-card { border:1px solid rgba(0,0,0,.08); border-radius:8px; overflow:hidden; cursor:pointer; transition:transform .15s,box-shadow .15s; background:#fff; }
+  .poll-card:hover { transform:scale(1.015); box-shadow:0 12px 36px rgba(0,0,0,.18); border-color:#d2b15c }
+  .poll-card img { display:block; width:100%; aspect-ratio:1; object-fit:cover }
+  .poll-card .meta { padding:12px 16px; background:#1a1816; color:#e8e2d6 }
+  .poll-card .meta .title { font-family:'Cormorant Garamond',serif; font-weight:300; font-size:19px }
+  .poll-card .meta .sub { font-size:11px; color:#888; margin-top:4px; text-transform:uppercase; letter-spacing:.05em }
+  .poll-loading { padding:80px 40px; text-align:center; color:#888 }
+  .lb-card { border:1px solid rgba(0,0,0,.08); border-radius:6px; overflow:hidden; background:#fff; font-size:11px; transition:transform .12s; text-decoration:none; color:inherit; display:block }
+  .lb-card:hover { transform:scale(1.04); box-shadow:0 8px 20px rgba(0,0,0,.12) }
+  .lb-card img { width:100%; aspect-ratio:1; object-fit:cover; display:block }
+  .lb-card .meta { padding:6px 8px }
+  .lb-card .rank { font-weight:600; color:#d2b15c }
+  .lb-card .score { color:#666; font-family:monospace; font-size:10px; margin-top:2px }
+</style>
+<script>
+async function loadPair() {
+  ['a','b'].forEach(function(k){ document.getElementById('card-'+k).innerHTML = '<div class="poll-loading">Loading&hellip;</div>'; });
+  var j = await (await fetch('/api/poll/pair')).json();
+  if (!j.a || !j.b) { document.getElementById('card-a').innerHTML = '<div class="poll-loading">Not enough designs yet.</div>'; return; }
+  ['a','b'].forEach(function(k){
+    var d = j[k];
+    var card = document.getElementById('card-'+k);
+    card.innerHTML = '<img src="'+d.image_url+'" alt="'+d.title+'"><div class="meta"><div class="title">'+d.title+'</div><div class="sub">'+(d.category||'')+'</div></div>';
+    card.onclick = function(evt){ vote(d.id, j[k==='a'?'b':'a'].id, evt, {id: d.id, category: d.category, dominant_hex: d.dominant_hex}); };
+  });
+  try {
+    var s = await (await fetch('/api/poll/stats')).json();
+    if (s && s.total_pairs != null) document.getElementById('poll-stats').textContent = s.total_pairs + ' total votes cast · ' + (s.designs_polled||0) + ' designs in rotation';
+  } catch(e) {}
+}
+async function vote(winner, loser, evt, winnerCtx) {
+  document.getElementById('card-a').style.opacity = 0.4;
+  document.getElementById('card-b').style.opacity = 0.4;
+  if (window.WC) WC.reward('pickA', evt, winnerCtx);
+  await fetch('/api/poll/vote', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({winner: winner, loser: loser})});
+  setTimeout(loadPair, 400);
+  setTimeout(function(){ document.getElementById('card-a').style.opacity = 1; document.getElementById('card-b').style.opacity = 1; loadLB(); }, 500);
+}
+async function loadLB() {
+  var lb = await (await fetch('/api/leaderboard?limit=20')).json();
+  document.getElementById('leaderboard').innerHTML = lb.map(function(d,i){
+    var fn = (d.local_path||'').split('/').pop();
+    var img = '/designs/img/' + fn;
+    var ai = d.combined_score ? '★'+d.combined_score+'AI' : '';
+    var u  = d.user_score_avg ? ' · ★'+d.user_score_avg+' (×'+d.user_vote_count+')' : '';
+    var elo = ' · '+Math.round(d.elo)+'elo';
+    return '<a href="/design/'+d.id+'" class="lb-card"><img src="'+img+'" loading="lazy"><div class="meta"><div><span class="rank">#'+(i+1)+'</span> · ID '+d.id+'</div><div class="score">'+ai+u+elo+'</div></div></a>';
+  }).join('') || '<p style="grid-column:1/-1;text-align:center;color:#999">No designs yet.</p>';
+}
+document.getElementById('btn-skip').addEventListener('click', loadPair);
+loadPair(); loadLB();
+</script>
+${HAMBURGER_JS}
+</body>
+</html>`);
+});
+
+// ── /play — gamification hub picker (3 modes)
+app.get('/play', (_req, res) => {
+  res.type('html').send(`${htmlHead({
+    title: 'Play — wallco.ai',
+    description: 'Three ways to teach our AI your taste. Swipe, vote, or pick favorites.',
+    canonical: 'https://wallco.ai/play'
+  })}
+<body>
+${htmlHeader('')}
+<main style="padding:40px 28px;max-width:1100px;margin:0 auto;text-align:center">
+  <h1 style="font-family:'Cormorant Garamond',serif;font-weight:300;font-size:40px;margin:0 0 8px">Play</h1>
+  <p style="color:#666;margin:0 0 32px">Three quick ways to teach the system your taste. Every gesture trains what we generate next.</p>
+  <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:18px">
+    <a href="/swipe" class="play-card">
+      <div class="play-icon">♥</div>
+      <h2>Swipe</h2>
+      <p>Tinder-style card stack. Drag right to love, left to skip, up to super-love. The most addictive of the three.</p>
+      <span class="play-cta">Start swiping &rarr;</span>
+    </a>
+    <a href="/hot-or-not" class="play-card">
+      <div class="play-icon">🔥</div>
+      <h2>Hot or Not</h2>
+      <p>One design at a time. HOT or NOT. The fastest way to teach the system. Arrow keys for speed.</p>
+      <span class="play-cta">Start rating &rarr;</span>
+    </a>
+    <a href="/poll" class="play-card">
+      <div class="play-icon">⚖</div>
+      <h2>Pick a favorite</h2>
+      <p>Two designs, side-by-side. Click the one you'd actually hang. Builds an Elo ranking across the whole catalog.</p>
+      <span class="play-cta">Start picking &rarr;</span>
+    </a>
+  </div>
+  <div style="margin-top:40px;padding:18px;background:#1a1816;color:#e8e2d6;border-radius:8px">
+    <h3 style="margin:0 0 6px;font-family:'Cormorant Garamond',serif;font-weight:300;font-size:20px;color:#d2b15c">How the learning works</h3>
+    <p style="margin:0;font-size:13px;color:#bba;line-height:1.55">Every vote (♥ / 🔥 / ⚖) lands in <code style="background:rgba(255,255,255,.06);padding:1px 5px;border-radius:3px">wallco_votes</code> or <code style="background:rgba(255,255,255,.06);padding:1px 5px;border-radius:3px">wallco_poll_pairs</code>. Every 30 min, the system aggregates the top-20 ranked designs and writes a high-weight recipe back into the generator. The 3-min cron then picks that recipe more often. Your taste literally bends the next 480 designs we render today.</p>
+  </div>
+</main>
+${FOOTER}
+<style>
+  .play-card { display:flex; flex-direction:column; align-items:center; text-align:center; padding:28px 22px; background:#fff; border:1px solid #e5dec9; border-radius:12px; text-decoration:none; color:inherit; transition:transform .15s, box-shadow .15s, border-color .15s; }
+  .play-card:hover { transform:translateY(-3px); box-shadow:0 16px 40px rgba(0,0,0,.12); border-color:#d2b15c; }
+  .play-icon { font-size:38px; margin-bottom:8px; }
+  .play-card h2 { font-family:'Cormorant Garamond',serif; font-weight:300; font-size:26px; margin:0 0 8px; }
+  .play-card p { font-size:13px; color:#666; line-height:1.5; margin:0 0 14px; }
+  .play-cta { font-size:12px; color:#d2b15c; text-transform:uppercase; letter-spacing:.08em; font-weight:600; }
+</style>
+${HAMBURGER_JS}
+</body>
+</html>`);
+});
+
+// ── GET /api/design/random — random rate-able design for hot-or-not + swipe.
+// Filters to designs whose image file actually exists on disk so the UI never
+// renders a broken-image card. Resolves the basename of local_path against
+// IMG_DIR; if it's not there, skip and try again. Bounded retries so the
+// loop can't run away if the entire table is broken.
+app.get('/api/design/random', (req, res) => {
+  const excl = String(req.query.exclude || '').split(',').map(x => parseInt(x,10)).filter(Number.isFinite);
+  const exclSet = new Set(excl);
+  try {
+    // Pull a small candidate pool, then filter by file existence client-side.
+    const exclSql = excl.length ? `AND id NOT IN (${excl.join(',')})` : '';
+    const raw = psqlQuery(`SELECT COALESCE(json_agg(t), '[]'::json) FROM (SELECT id, dominant_hex, category, local_path
+      FROM spoon_all_designs WHERE local_path IS NOT NULL ${exclSql}
+      ORDER BY random() LIMIT 30) t;`);
+    const pool = JSON.parse(raw || '[]');
+    for (const d of pool) {
+      if (exclSet.has(d.id)) continue;
+      const fn = (d.local_path || '').split('/').pop();
+      if (!fn) continue;
+      const abs = path.join(IMG_DIR, fn);
+      try { if (!fs.existsSync(abs)) continue; } catch { continue; }
+      return res.json({
+        id: d.id,
+        title: titleFor(d.category, d.dominant_hex, d.id),
+        image_url: `/designs/img/${fn}`,
+        category: d.category,
+        dominant_hex: d.dominant_hex
+      });
+    }
+    return res.status(404).json({ error: 'no renderable design found in pool of 30' });
+  } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
+// ── Hot-or-Not: per-session recent picks + historic rejection-reason chips.
+// Recent picks are derived from wallco_votes (last 6 by voter_session). Skip
+// rows whose image file no longer exists. Always returns up to 6 — the UI
+// pads to 6 slots with skeletons if fewer come back.
+app.get('/api/hot-or-not/recent', (req, res) => {
+  const session = voterSession(req, res);
+  try {
+    const limit = Math.max(1, Math.min(parseInt(req.query.limit || '6', 10), 24));
+    const raw = psqlQuery(`SELECT COALESCE(json_agg(t), '[]'::json) FROM (
+      SELECT v.design_id AS id, v.score, v.created_at,
+             d.local_path, d.dominant_hex, d.category
+      FROM wallco_votes v
+      JOIN spoon_all_designs d ON d.id = v.design_id
+      WHERE v.voter_session = '${session.replace(/'/g, "''")}'
+        AND d.local_path IS NOT NULL
+      ORDER BY v.created_at DESC LIMIT ${limit * 3}
+    ) t;`);
+    const rows = JSON.parse(raw || '[]');
+    const out = [];
+    for (const r of rows) {
+      if (out.length >= limit) break;
+      const fn = (r.local_path || '').split('/').pop();
+      if (!fn) continue;
+      try { if (!fs.existsSync(path.join(IMG_DIR, fn))) continue; } catch { continue; }
+      out.push({
+        id: r.id,
+        score: r.score,
+        action: r.score >= 4 ? 'hot' : r.score <= 2 ? 'not' : 'skip',
+        title: titleFor(r.category, r.dominant_hex, r.id),
+        image_url: `/designs/img/${fn}`,
+        created_at: r.created_at
+      });
+    }
+    res.json({ picks: out });
+  } catch (e) { res.status(500).json({ error: e.message, picks: [] }); }
+});
+
+// Default rejection reasons — used when the historic top-N is too thin OR the
+// reasons column hasn't been migrated yet. Kept short + scannable. The
+// dynamic-from-history layer merges these with whatever's most-clicked.
+const HON_DEFAULT_REASONS = [
+  'Colors clash',
+  'Pattern feels AI-generated / artifacts',
+  'Too busy',
+  'Too plain',
+  'Wrong scale for a wall',
+  "Doesn't tile cleanly",
+  'Cliché / overdone motif',
+  'Off-trend / dated',
+  'Resolution looks low',
+  'Wrong palette for me',
+  'Composition feels off',
+  'Tags / category seem wrong'
+];
+
+app.get('/api/hot-or-not/reasons', (_req, res) => {
+  try {
+    // If the column exists, prefer the live top-N (mixed with defaults to
+    // backfill). If not, fall through to the defaults silently.
+    const colExists = psqlQuery(`SELECT 1 FROM information_schema.columns WHERE table_name='wallco_votes' AND column_name='reasons' LIMIT 1;`);
+    if (colExists) {
+      const raw = psqlQuery(`SELECT COALESCE(json_agg(t), '[]'::json) FROM (
+        SELECT unnest(reasons) AS reason, COUNT(*) AS n
+        FROM wallco_votes WHERE reasons IS NOT NULL AND array_length(reasons,1) > 0
+        GROUP BY 1 ORDER BY n DESC LIMIT 12
+      ) t;`);
+      const live = JSON.parse(raw || '[]').map(r => r.reason).filter(Boolean);
+      const seen = new Set(live.map(s => s.toLowerCase()));
+      const merged = live.concat(HON_DEFAULT_REASONS.filter(d => !seen.has(d.toLowerCase()))).slice(0, 12);
+      return res.json({ reasons: merged, source: live.length ? 'historic+defaults' : 'defaults' });
+    }
+    res.json({ reasons: HON_DEFAULT_REASONS, source: 'defaults' });
+  } catch (e) {
+    res.json({ reasons: HON_DEFAULT_REASONS, source: 'defaults-fallback', error: e.message });
+  }
+});
+
+// ── /hot-or-not — rapid-fire HOT/NOT/SKIP single-card
+app.get('/hot-or-not', (_req, res) => {
+  res.type('html').send(`${htmlHead({
+    title: 'Hot or Not — wallco.ai',
+    description: 'Quick taste game. HOT or NOT for every wallpaper, every vote teaches the system.',
+    canonical: 'https://wallco.ai/hot-or-not'
+  })}
+<body>
+${htmlHeader('')}
+<main style="padding:24px 28px;max-width:760px;margin:0 auto;text-align:center">
+  <h1 style="font-family:'Cormorant Garamond',serif;font-weight:300;font-size:34px;margin:0 0 6px">Hot or Not</h1>
+  <p style="color:#666;margin:0 0 22px;font-size:13px">Speed-vote on AI-original wallpaper. Arrow ← NOT · Arrow → HOT · Space SKIP.</p>
+  <div id="streak" style="margin-bottom:8px;font-size:12px;color:#d2b15c;font-weight:500"></div>
+  <div id="wc-game-banner" style="background:#1a1816;border-radius:10px;padding:14px 18px;margin-bottom:18px;border:1px solid rgba(210,177,92,.2)"></div>
+  <div id="hon-card" style="position:relative;border:1px solid rgba(0,0,0,.1);border-radius:14px;overflow:hidden;background:#1a1816;box-shadow:0 20px 60px rgba(0,0,0,.18);transition:transform .26s ease, opacity .26s ease">
+    <img id="hon-img" alt="design" style="display:block;width:100%;aspect-ratio:1;object-fit:cover">
+    <div id="hon-meta" style="padding:14px 20px;color:#e8e2d6;text-align:left;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px">
+      <div>
+        <div id="hon-title" style="font-family:'Cormorant Garamond',serif;font-weight:300;font-size:22px"></div>
+        <div id="hon-sub"   style="font-size:11px;color:#888;text-transform:uppercase;letter-spacing:.06em;margin-top:3px"></div>
+      </div>
+      <a id="hon-view" target="_blank" style="font-size:11px;color:#d2b15c;text-decoration:underline" rel="noopener noreferrer">view full &rarr;</a>
+    </div>
+  </div>
+  <div id="hon-buttons" style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;margin-top:18px">
+    <button class="hon-btn hon-not" data-action="not">NOT</button>
+    <button class="hon-btn hon-skip" data-action="skip">SKIP</button>
+    <button class="hon-btn hon-hot" data-action="hot">HOT</button>
+  </div>
+  <div id="hon-feed" style="margin-top:30px;text-align:left">
+    <h3 style="font-size:11px;text-transform:uppercase;letter-spacing:.1em;color:#888;margin:0 0 8px">Your last 6 picks</h3>
+    <div id="hon-history" style="display:grid;grid-template-columns:repeat(6,1fr);gap:6px"></div>
+  </div>
+</main>
+
+<!-- NOT modal — reason checkboxes (top-N historic merged w/ defaults) + free-text note -->
+<div id="hon-not-modal" role="dialog" aria-modal="true" aria-labelledby="hon-not-title" style="display:none;position:fixed;inset:0;background:rgba(15,14,12,.78);z-index:9999;align-items:center;justify-content:center;padding:18px;backdrop-filter:blur(4px)">
+  <div style="background:#fbfaf6;color:#1a1a1a;max-width:480px;width:100%;border-radius:14px;box-shadow:0 28px 80px rgba(0,0,0,.5);overflow:hidden">
+    <div style="padding:18px 22px;border-bottom:1px solid #e6e2d8;display:flex;justify-content:space-between;align-items:center">
+      <h2 id="hon-not-title" style="margin:0;font-family:'Cormorant Garamond',serif;font-weight:400;font-size:22px">Why not?</h2>
+      <button id="hon-not-cancel" aria-label="Close" style="background:transparent;border:0;font-size:22px;cursor:pointer;color:#888;line-height:1;padding:4px 8px">×</button>
+    </div>
+    <div style="padding:14px 22px 6px;font-size:12px;color:#666">Pick any that apply. Add a note if you want to teach the system.</div>
+    <div id="hon-not-reasons" style="padding:8px 22px 4px;display:grid;grid-template-columns:1fr 1fr;gap:6px 14px;font-size:13px"></div>
+    <div style="padding:8px 22px 4px">
+      <label style="font-size:11px;color:#888;text-transform:uppercase;letter-spacing:.06em">Note for the LLM <span style="text-transform:none;color:#aaa">— optional</span></label>
+      <textarea id="hon-not-note" rows="3" placeholder="e.g., the scale is too tight for a master bedroom; pattern feels stamped, not drawn" style="width:100%;margin-top:6px;padding:10px 12px;border:1px solid #d8d2c5;border-radius:8px;font:13px/1.4 system-ui, sans-serif;background:#fff;resize:vertical;box-sizing:border-box"></textarea>
+    </div>
+    <div style="padding:14px 22px 18px;display:flex;justify-content:flex-end;gap:8px">
+      <button id="hon-not-skip" style="padding:10px 16px;border:1px solid #d8d2c5;background:#fff;border-radius:8px;cursor:pointer;font-size:13px;color:#666">Skip — just NOT</button>
+      <button id="hon-not-submit" style="padding:10px 18px;border:0;background:#c44;color:#fff;border-radius:8px;cursor:pointer;font-size:13px;font-weight:600;letter-spacing:.04em">Submit NOT</button>
+    </div>
+  </div>
+</div>
+${FOOTER}
+<style>
+  .hon-btn { padding:18px 12px; font-size:14px; font-weight:600; letter-spacing:.1em; border:0; border-radius:10px; cursor:pointer; transition:transform .12s, background .15s; }
+  .hon-not  { background:#fee; color:#b00; border:1px solid #fbb; }
+  .hon-not:hover { background:#fcc; transform:scale(1.03) }
+  .hon-skip { background:#eee; color:#666; border:1px solid #ddd; }
+  .hon-skip:hover { background:#ddd; transform:scale(1.03) }
+  .hon-hot { background:#d2b15c; color:#1a1a1a; border:1px solid #b89745; }
+  .hon-hot:hover { background:#e0bd64; transform:scale(1.03) }
+  #hon-card.swipe-left { transform:translateX(-120%) rotate(-14deg); opacity:0 }
+  #hon-card.swipe-right { transform:translateX(120%) rotate(14deg); opacity:0 }
+  #hon-card.swipe-up { transform:translateY(-120%); opacity:0 }
+  .hist-card { display:block; aspect-ratio:1; border-radius:5px; overflow:hidden; border:2px solid transparent; background:#f4f1ea }
+  .hist-card.hot  { border-color:#d2b15c }
+  .hist-card.not  { border-color:#c44 }
+  .hist-card.skip { border-color:#888 }
+  .hist-card.empty { border:1px dashed #d8d2c5; background:repeating-linear-gradient(135deg,#f4f1ea 0 8px,#ede9de 8px 16px) }
+  .hist-card img { width:100%; height:100%; object-fit:cover; display:block }
+  .hon-reason-chip { display:flex; align-items:center; gap:8px; padding:7px 10px; border:1px solid #e6e2d8; border-radius:8px; cursor:pointer; background:#fff; transition:background .12s, border-color .12s; user-select:none }
+  .hon-reason-chip:hover { background:#faf6ec }
+  .hon-reason-chip.checked { background:#fde6e2; border-color:#e8aaa5 }
+  .hon-reason-chip input { margin:0 }
+</style>
+<script>
+var seen = [], picks = [];  /* named picks because window.history shadows a global */
+var streak = parseInt(localStorage.getItem('wc-streak') || '0', 10);
+var todayKey = 'wc-day-' + new Date().toISOString().slice(0,10);
+var todayCount = parseInt(localStorage.getItem(todayKey) || '0', 10);
+function renderStreak(){ document.getElementById('streak').textContent = '🔥 ' + streak + ' in a row · ' + todayCount + ' votes today'; }
+renderStreak();
+
+/* Always show 6 thumb slots. Empty slots are dashed placeholders. */
+function renderHistory(){
+  var grid = document.getElementById('hon-history');
+  var html = '';
+  for (var i = 0; i < 6; i++) {
+    var h = picks[i];
+    if (h) {
+      html += '<a class="hist-card '+h.action+'" href="/design/'+h.id+'" title="'+h.title.replace(/"/g,'&quot;')+'">'
+            + '<img src="'+h.image_url+'" alt="'+h.title.replace(/"/g,'&quot;')+'" onerror="this.parentNode.classList.add(\\'empty\\');this.remove()">'
+            + '</a>';
+    } else {
+      html += '<span class="hist-card empty" aria-label="empty slot"></span>';
+    }
+  }
+  grid.innerHTML = html;
+}
+renderHistory();
+
+/* Hydrate from server-side history so the row is always populated. */
+(async function hydrateRecent(){
+  try {
+    var r = await (await fetch('/api/hot-or-not/recent?limit=6')).json();
+    if (r && Array.isArray(r.picks) && r.picks.length) {
+      picks = r.picks.map(function(p){ return { id:p.id, action:p.action, title:p.title || ('Design #'+p.id), image_url:p.image_url }; });
+      r.picks.forEach(function(p){ if (!seen.includes(p.id)) seen.push(p.id); });
+      renderHistory();
+    }
+  } catch(e) { /* non-fatal */ }
+})();
+
+async function load(){
+  var url = '/api/design/random' + (seen.length ? '?exclude=' + seen.slice(-30).join(',') : '');
+  var r = await (await fetch(url)).json();
+  if (!r.id) return;
+  document.getElementById('hon-card').className = '';
+  document.getElementById('hon-img').src = r.image_url;
+  document.getElementById('hon-title').textContent = r.title;
+  document.getElementById('hon-sub').textContent = (r.category||'') + (r.dominant_hex ? ' · ' + r.dominant_hex : '');
+  document.getElementById('hon-view').href = '/design/' + r.id;
+  document.getElementById('hon-card').dataset.id = r.id;
+  document.getElementById('hon-card').dataset.title = r.title;
+  document.getElementById('hon-card').dataset.image = r.image_url;
+  document.getElementById('hon-card').dataset.category = r.category || '';
+  document.getElementById('hon-card').dataset.hex = r.dominant_hex || '';
+  seen.push(r.id);
+}
+
+/* ---------- NOT modal ---------- */
+var honReasonsCache = null;
+async function ensureReasons(){
+  if (honReasonsCache) return honReasonsCache;
+  try {
+    var r = await (await fetch('/api/hot-or-not/reasons')).json();
+    honReasonsCache = (r && Array.isArray(r.reasons)) ? r.reasons : [];
+  } catch(e) { honReasonsCache = []; }
+  return honReasonsCache;
+}
+
+async function openNotModal(){
+  var reasons = await ensureReasons();
+  var box = document.getElementById('hon-not-reasons');
+  box.innerHTML = reasons.map(function(r, i){
+    var v = r.replace(/"/g, '&quot;');
+    return '<label class="hon-reason-chip" data-reason="'+v+'"><input type="checkbox" value="'+v+'">'+r+'</label>';
+  }).join('');
+  /* toggle .checked on chip when its input flips */
+  box.querySelectorAll('.hon-reason-chip').forEach(function(chip){
+    var input = chip.querySelector('input');
+    chip.addEventListener('click', function(e){
+      if (e.target !== input) { input.checked = !input.checked; }
+      chip.classList.toggle('checked', input.checked);
+    });
+  });
+  document.getElementById('hon-not-note').value = '';
+  var m = document.getElementById('hon-not-modal');
+  m.style.display = 'flex';
+  setTimeout(function(){ document.getElementById('hon-not-note').focus(); }, 60);
+}
+function closeNotModal(){ document.getElementById('hon-not-modal').style.display = 'none'; }
+
+function collectModalPayload(){
+  var reasons = [].slice.call(document.querySelectorAll('#hon-not-reasons input:checked')).map(function(i){ return i.value; });
+  var note = (document.getElementById('hon-not-note').value || '').trim();
+  return { reasons: reasons, note: note };
+}
+
+async function act(action, evt){
+  var card = document.getElementById('hon-card');
+  var id = parseInt(card.dataset.id, 10);
+  if (!id) return;
+
+  /* NOT opens the reason modal — actual vote fires from the modal's submit/skip. */
+  if (action === 'not') { window._honPendingId = id; openNotModal(); return; }
+
+  var anim = action==='hot' ? 'swipe-right' : 'swipe-up';
+  card.classList.add(anim);
+  if (action === 'hot') {
+    var score = 5;
+    fetch('/api/design/' + id + '/user-vote', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({score: score, quick: true}) });
+    streak = streak + 1;
+    localStorage.setItem('wc-streak', String(streak));
+  }
+  todayCount++; localStorage.setItem(todayKey, String(todayCount));
+  renderStreak();
+  if (window.WC) WC.reward(action, evt, {id: id, category: card.dataset.category, dominant_hex: card.dataset.hex});
+  picks.unshift({id: id, action: action, title: card.dataset.title, image_url: card.dataset.image});
+  picks = picks.slice(0, 6);
+  renderHistory();
+  setTimeout(load, 280);
+}
+
+/* NOT-with-reasons path: invoked from the modal. Mirrors act('not') flow
+   but carries reasons + note to the server. */
+function submitNot(reasons, note, evt){
+  var id = window._honPendingId;
+  if (!id) { closeNotModal(); return; }
+  window._honPendingId = null;
+  var card = document.getElementById('hon-card');
+  card.classList.add('swipe-left');
+  fetch('/api/design/' + id + '/user-vote', {
+    method:'POST', headers:{'Content-Type':'application/json'},
+    body: JSON.stringify({ score: 1, quick: false, reasons: reasons || [], note: note || '' })
+  });
+  streak = 0; localStorage.setItem('wc-streak', String(streak));
+  todayCount++; localStorage.setItem(todayKey, String(todayCount));
+  renderStreak();
+  if (window.WC) WC.reward('not', evt, {id: id, category: card.dataset.category, dominant_hex: card.dataset.hex});
+  picks.unshift({ id: id, action: 'not', title: card.dataset.title, image_url: card.dataset.image });
+  picks = picks.slice(0, 6);
+  renderHistory();
+  closeNotModal();
+  setTimeout(load, 280);
+}
+
+document.querySelectorAll('.hon-btn').forEach(function(b){ b.addEventListener('click', function(evt){ act(b.dataset.action, evt); }); });
+document.getElementById('hon-not-cancel').addEventListener('click', closeNotModal);
+document.getElementById('hon-not-skip').addEventListener('click', function(evt){ submitNot([], '', evt); });
+document.getElementById('hon-not-submit').addEventListener('click', function(evt){ var p = collectModalPayload(); submitNot(p.reasons, p.note, evt); });
+document.getElementById('hon-not-modal').addEventListener('click', function(e){ if (e.target === this) closeNotModal(); });
+
+document.addEventListener('keydown', function(e){
+  var modalOpen = document.getElementById('hon-not-modal').style.display === 'flex';
+  if (modalOpen) {
+    if (e.key === 'Escape') { e.preventDefault(); closeNotModal(); }
+    else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); var p = collectModalPayload(); submitNot(p.reasons, p.note); }
+    return;
+  }
+  if (e.key === 'ArrowLeft') act('not');
+  else if (e.key === 'ArrowRight') act('hot');
+  else if (e.key === ' ' || e.key === 'ArrowUp') { e.preventDefault(); act('skip'); }
+});
+load();
+</script>
+${HAMBURGER_JS}
+</body>
+</html>`);
+});
+
+// ── /swipe — Tinder-style drag-to-swipe card stack
+app.get('/swipe', (_req, res) => {
+  res.type('html').send(`${htmlHead({
+    title: 'Swipe — wallco.ai',
+    description: 'Swipe right to love, left to skip, up to super-love. Every gesture teaches the system.',
+    canonical: 'https://wallco.ai/swipe'
+  })}
+<body>
+${htmlHeader('')}
+<main style="padding:18px 16px;max-width:520px;margin:0 auto;text-align:center">
+  <h1 style="font-family:'Cormorant Garamond',serif;font-weight:300;font-size:30px;margin:0 0 4px">Swipe</h1>
+  <p style="color:#666;margin:0 0 14px;font-size:12px">&larr; skip · &rarr; love · &uarr; super-love · drag the card · or use the buttons.</p>
+  <div id="sw-streak" style="margin-bottom:8px;font-size:12px;color:#d2b15c;font-weight:500"></div>
+  <div id="wc-game-banner" style="background:#1a1816;border-radius:10px;padding:14px 18px;margin-bottom:14px;border:1px solid rgba(210,177,92,.2)"></div>
+  <div id="sw-stack" style="position:relative;height:clamp(380px, 70vh, 560px);margin:0 auto;max-width:420px"></div>
+  <div id="sw-buttons" style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-top:14px;max-width:380px;margin-left:auto;margin-right:auto">
+    <button class="sw-btn sw-skip"  data-action="left"  aria-label="Skip">&#10005;</button>
+    <button class="sw-btn sw-super" data-action="up"    aria-label="Super-love">&#9733;</button>
+    <button class="sw-btn sw-love"  data-action="right" aria-label="Love">&#9829;</button>
+  </div>
+  <div id="sw-toast" style="margin-top:18px;min-height:22px;font-size:13px;color:#666"></div>
+</main>
+${FOOTER}
+<style>
+  .sw-card { position:absolute; inset:0; border-radius:18px; overflow:hidden; background:#1a1816; color:#e8e2d6; box-shadow:0 18px 50px rgba(0,0,0,.22); cursor:grab; user-select:none; transition:transform .26s ease, opacity .26s ease; touch-action:none; }
+  .sw-card.dragging { transition:none; cursor:grabbing; }
+  .sw-card img { display:block; width:100%; aspect-ratio:1; object-fit:cover; pointer-events:none; }
+  .sw-card .meta { padding:12px 18px; display:flex; justify-content:space-between; align-items:center; gap:10px; }
+  .sw-card .title { font-family:'Cormorant Garamond',serif; font-weight:300; font-size:20px; }
+  .sw-card .sub { font-size:10px; color:#888; text-transform:uppercase; letter-spacing:.06em; }
+  .sw-card .stamp { position:absolute; top:30px; padding:8px 18px; border-radius:6px; font-weight:700; font-size:18px; letter-spacing:.12em; opacity:0; transition:opacity .15s; border:3px solid currentColor; }
+  .sw-card .stamp-love { right:24px; color:#d2b15c; transform:rotate(12deg); }
+  .sw-card .stamp-skip { left:24px;  color:#c44;    transform:rotate(-12deg); }
+  .sw-card .stamp-super { left:50%; top:50%; transform:translate(-50%,-50%); color:#5aa; font-size:24px; }
+  .sw-card.show-love .stamp-love  { opacity:1 }
+  .sw-card.show-skip .stamp-skip  { opacity:1 }
+  .sw-card.show-super .stamp-super { opacity:1 }
+  .sw-btn { padding:18px; font-size:22px; font-weight:600; border:0; border-radius:50%; aspect-ratio:1; cursor:pointer; transition:transform .12s; }
+  .sw-skip  { background:#fee; color:#c44; border:1px solid #fbb; }
+  .sw-super { background:#e7f5f5; color:#5aa; border:1px solid #aee; }
+  .sw-love  { background:#d2b15c; color:#1a1a1a; border:1px solid #b89745; }
+  .sw-btn:hover { transform:scale(1.08); }
+</style>
+<script>
+var stack = [], swSeen = [];
+var swStreak = parseInt(localStorage.getItem('wc-sw-streak') || '0', 10);
+var swDayKey = 'wc-sw-day-' + new Date().toISOString().slice(0,10);
+var swToday = parseInt(localStorage.getItem(swDayKey) || '0', 10);
+function renderStreak(){ document.getElementById('sw-streak').textContent = '🔥 ' + swStreak + ' loves in a row · ' + swToday + ' swipes today'; }
+renderStreak();
+
+async function fillStack(){
+  while (stack.length < 3) {
+    var r = await (await fetch('/api/design/random?exclude=' + swSeen.slice(-30).join(','))).json();
+    if (!r.id) break;
+    swSeen.push(r.id);
+    stack.push(r);
+  }
+  renderStack();
+}
+function renderStack(){
+  var s = document.getElementById('sw-stack');
+  s.innerHTML = '';
+  for (var i = Math.min(stack.length-1, 2); i >= 0; i--) {
+    var d = stack[i];
+    var card = document.createElement('div');
+    card.className = 'sw-card';
+    card.dataset.id = d.id;
+    card.dataset.category = d.category || '';
+    card.dataset.hex = d.dominant_hex || '';
+    card.style.zIndex = 100 - i;
+    card.style.transform = 'translateY(' + (i * 6) + 'px) scale(' + (1 - i*0.03) + ')';
+    card.innerHTML = '<img src="'+d.image_url+'" alt="'+d.title+'">' +
+      '<div class="meta"><div><div class="title">'+d.title+'</div><div class="sub">'+(d.category||'')+'</div></div>' +
+      '<a href="/design/'+d.id+'" target="_blank" style="font-size:10px;color:#d2b15c;text-decoration:underline" rel="noopener noreferrer">view</a></div>' +
+      '<div class="stamp stamp-love">LOVE</div><div class="stamp stamp-skip">SKIP</div><div class="stamp stamp-super">&#9733; SUPER</div>';
+    s.appendChild(card);
+    if (i === 0) attachDrag(card);
+  }
+  if (stack.length === 0) s.innerHTML = '<div style="padding:40px;color:#888">No more designs in rotation.</div>';
+}
+function attachDrag(card){
+  var startX=0, startY=0, dx=0, dy=0, dragging=false, touchId=null;
+  function pt(e){
+    if (e.touches && e.touches.length) {
+      for (var i=0; i<e.touches.length; i++) if (e.touches[i].identifier === touchId) return e.touches[i];
+      return e.touches[0];
+    }
+    if (e.changedTouches && e.changedTouches.length) return e.changedTouches[0];
+    return e;
+  }
+  function onStart(e){
+    if (dragging) return;
+    if (e.touches && e.touches.length) touchId = e.touches[0].identifier;
+    dragging = true;
+    card.classList.add('dragging');
+    var p = pt(e); startX = p.clientX; startY = p.clientY;
+    if (e.cancelable && e.touches) e.preventDefault();
+  }
+  function onMove(e){
+    if (!dragging) return;
+    if (e.cancelable) e.preventDefault();
+    var p = pt(e);
+    dx = p.clientX - startX; dy = p.clientY - startY;
+    card.style.transform = 'translate(' + dx + 'px,' + dy + 'px) rotate(' + (dx/20) + 'deg)';
+    card.classList.toggle('show-love',  dx >  50);
+    card.classList.toggle('show-skip',  dx < -50);
+    card.classList.toggle('show-super', dy < -70 && Math.abs(dx) < 100);
+  }
+  function onEnd(){
+    if (!dragging) return;
+    dragging = false; touchId = null;
+    card.classList.remove('dragging');
+    if (dy < -110 && Math.abs(dx) < 140) commit('up');
+    else if (dx >  110) commit('right');
+    else if (dx < -110) commit('left');
+    else {
+      card.style.transform = '';
+      card.classList.remove('show-love','show-skip','show-super');
+    }
+  }
+  // Mouse — desktop + Playwright synthetic
+  card.addEventListener('mousedown', onStart);
+  document.addEventListener('mousemove', onMove);
+  document.addEventListener('mouseup',   onEnd);
+  // Touch — attach MOVE + END to document so the gesture survives the
+  // finger leaving the card. This was the bug Steve hit on mobile.
+  card.addEventListener('touchstart',  onStart, {passive:false});
+  document.addEventListener('touchmove', onMove, {passive:false});
+  document.addEventListener('touchend',   onEnd);
+  document.addEventListener('touchcancel',onEnd);
+  // Pointer events — covers Surface, Wacom, future Apple Pencil
+  card.addEventListener('pointerdown',  onStart);
+  document.addEventListener('pointermove', onMove);
+  document.addEventListener('pointerup',    onEnd);
+  document.addEventListener('pointercancel',onEnd);
+}
+function commit(direction, evt){
+  var card = document.querySelector('.sw-card');
+  if (!card) return;
+  var id = parseInt(card.dataset.id, 10);
+  var anim = direction==='right' ? 'translateX(140vw) rotate(20deg)'
+           : direction==='left'  ? 'translateX(-140vw) rotate(-20deg)' : 'translateY(-140vh)';
+  card.style.transform = anim; card.style.opacity = 0;
+  var score = direction==='left' ? 1 : 5;
+  var quick = direction !== 'up';
+  fetch('/api/design/' + id + '/user-vote', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({score: score, quick: quick}) });
+  // Hook into the gamification engine
+  var wcAction = direction==='right' ? 'love' : direction==='up' ? 'super' : 'skip';
+  if (window.WC) WC.reward(wcAction, evt, {id: id, category: card.dataset.category, dominant_hex: card.dataset.hex});
+  if (direction === 'right' || direction === 'up') {
+    swStreak++;
+    document.getElementById('sw-toast').innerHTML = direction==='up' ? '&#9733; Super-love recorded' : '&#9829; Love recorded · streak ' + swStreak;
+  } else { swStreak = 0; document.getElementById('sw-toast').innerHTML = '&#10005; Skipped'; }
+  localStorage.setItem('wc-sw-streak', String(swStreak));
+  swToday++; localStorage.setItem(swDayKey, String(swToday));
+  renderStreak();
+  stack.shift();
+  setTimeout(function(){ fillStack(); }, 260);
+}
+document.querySelectorAll('.sw-btn').forEach(function(b){ b.addEventListener('click', function(evt){ commit(b.dataset.action, evt); }); });
+document.addEventListener('keydown', function(e){
+  if (e.key === 'ArrowLeft')  commit('left');
+  else if (e.key === 'ArrowRight') commit('right');
+  else if (e.key === 'ArrowUp') { e.preventDefault(); commit('up'); }
+});
+fillStack();
+</script>
+${HAMBURGER_JS}
+</body>
+</html>`);
+});
+
+// ── HEALTH
+app.get('/health', (_req, res) => {
+  res.json({ ok: true, site: SITE, count: DESIGNS.length, port: PORT });
+});
+
+// ── /admin/health — admin-gated extended health + by-color cache stats. Useful to
+// confirm the in-process ΔE cache is actually serving HITs and that eviction is bounded.
+// 404 (not 401/403) on non-admin so we don't leak the endpoint's existence.
+app.get('/admin/health', (req, res) => {
+  if (!isAdmin(req)) return res.status(404).type('html').send('<h1>404</h1>');
+  const total = _byColorStats.hits + _byColorStats.misses;
+  res.setHeader('Cache-Control', 'no-store');
+  res.json({
+    ok: true,
+    site: SITE,
+    port: PORT,
+    designs: DESIGNS.length,
+    uptime_s: Math.round(process.uptime()),
+    rss_mb: Math.round(process.memoryUsage().rss / 1048576),
+    by_color_cache: {
+      size: _byColorCache.size,
+      max: _BY_COLOR_MAX,
+      ttl_ms: _BY_COLOR_TTL_MS,
+      ver: _byColorCacheVer,
+      hits: _byColorStats.hits,
+      misses: _byColorStats.misses,
+      evictions: _byColorStats.evictions,
+      hit_rate: total > 0 ? Math.round((_byColorStats.hits / total) * 1000) / 10 : null,
+      since: new Date(_byColorStats.since).toISOString(),
+    },
+    random_etag: (() => {
+      const t = _randomStats.hits + _randomStats.misses;
+      return {
+        hits: _randomStats.hits,
+        misses: _randomStats.misses,
+        hit_rate: t > 0 ? Math.round((_randomStats.hits / t) * 1000) / 10 : null,
+        since: new Date(_randomStats.since).toISOString(),
+      };
+    })(),
+  });
+});
+
+// ── DEV LOGIN — sets dw_auth HS256 cookie for admin layer + inline editor (dev mode only)
+const __crypto = require('crypto');
+function __b64u(s) { return Buffer.from(s).toString('base64').replace(/=+$/,'').replace(/\+/g,'-').replace(/\//g,'_'); }
+function __sign(secret, payload) {
+  const h = __b64u(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
+  const p = __b64u(JSON.stringify(payload));
+  const sig = __b64u(__crypto.createHmac('sha256', secret).update(h + '.' + p).digest());
+  return `${h}.${p}.${sig}`;
+}
+app.get('/_devlogin', (req, res) => {
+  if ((process.env.AUTH_MODE || 'dev') !== 'dev') return res.status(403).send('dev login disabled in prod');
+  const secret = process.env.AUTH_DEV_SECRET || 'wallco-dev-secret-change-for-prod';
+  const exp = Math.floor(Date.now() / 1000) + 8 * 3600;
+  const token = __sign(secret, {
+    email: 'steve@designerwallcoverings.com',
+    role: 'admin',
+    iss: 'https://auth.agentabrams.com',
+    exp
+  });
+  res.cookie('dw_auth', token, { httpOnly: false, maxAge: 8 * 3600 * 1000, sameSite: 'lax' });
+  const next = req.query.next || '/admin';
+  res.type('text/html').send(`<!doctype html><meta charset="utf-8"><title>Admin login</title>
+<style>body{font:14px/1.5 -apple-system,system-ui,sans-serif;background:#0f0e0c;color:#e8e2d6;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}.card{background:#1a1816;padding:36px 44px;border-radius:10px;border:1px solid #2a2825;max-width:480px}.card h1{font:300 22px/1.2 'Cormorant Garamond',serif;margin:0 0 6px;color:#d2b15c}.card p{color:#bba;margin:6px 0}a{color:#d2b15c;font-weight:500}</style>
+<div class="card">
+  <h1>Admin login set ✓</h1>
+  <p>Logged in as <code>steve@designerwallcoverings.com</code> · expires in 8 hours.</p>
+  <p><a href="${next}">→ Continue to ${next}</a> · <a href="/_devlogout">Log out</a></p>
+</div>`);
+});
+app.get('/_devlogout', (_req, res) => {
+  res.clearCookie('dw_auth');
+  res.type('text/plain').send('Logged out.\n');
+});
+
+// ── Start
+app.listen(PORT, '0.0.0.0', () => {
+  console.log(`wallco.ai production server → http://0.0.0.0:${PORT}`);
+  console.log(`  Designs loaded: ${DESIGNS.length}`);
+  console.log(`  GA4: ${GA4_ID || '(not set)'}`);
+});

← 28ef4d5 generator: wire product_line column on INSERT + backfill des  ·  back to Wallco Ai  ·  vendor-scrub: remove all references to de Gournay / Gracie / f587ed9 →