[object Object]

← back to Wallco Ai

feat(scenic+ux): 6 Gracie-language colorways + 3 reviewer-redo murals + tiles-readout + sub-brand proposal

5782b5100bdd087959cac3538c4d4fdcb70f0d81 · 2026-05-21 19:28:47 -0700 · Steve Abrams

Three batches landed this turn, all from the 2026-05-21 reviewer panel
recommendations (interior-designer + ui-ux-designer + debate-team-fast):

Designs (Gemini image-edit from #39055):
- 39101-39106 — 6 colorway variants of the burnt-umber Pass-3 winner
  (celadon, oxblood, ivory-on-black, indigo, sand-on-bone, eucalyptus).
  Preserves #39055's flat hand-painted-silk Zuber/Gracie/de Gournay
  silhouette composition; only recolors. The interior-designer panel:
  "the brand's defensible product is this aesthetic; multiply it."
- 39107 — desert-coral redo of #39062 (the weakest of the original 6,
  uncanny photoreal-flat hybrid). Re-rendered from #39055 with coral as
  the single ink. Old #39062 unpublished.
- 39108 — B&W agave (#38848) recomposed off-center with a second sepia
  ink tone. Centered-radial was the cardinal mural sin per the reviewer.
  Old #38848 unpublished.
- 39109 — saguaro dusk (#38908) with cobalt-orange dusk gradient
  swapped for a single oyster-pink wash. Reviewer panel: "stock postcard
  palette → curated quiet twilight." Old #38908 unpublished.

UI (server.js):
- New #tiles-readout block above the room-setting #scale-viewport:
  "Your 10 × 9 ft wall will hold 5 tiles wide × 9 repeats tall
  (5 drops at 24" roll · 24" repeat)". Hidden in mural mode; live-updates
  from readWallSpec() on every wall/roll/repeat change. Fixes the
  ui-ux-designer panel finding that at default zoom the viewport shows
  a magnified excerpt, not the full wall — customer can't visually count
  tiles.

Sub-brand proposal:
- docs/SUB-BRAND-PROPOSAL-SCENIC-VS-STUDIO.md drafts the wallco/Scenic
  (90 photoreal hospitality murals) + wallco/Studio (7+ Gracie register)
  split with names TBD, trade-pricing question open, PG migration plan
  spec'd. Memory entry in ~/.claude flags it as awaiting Steve approval
  before any catalog rename ships.

Scripts:
- scripts/gemini-39055-colorways.js — single-source 6-colorway renderer
- scripts/gemini-rerender-weak3.js  — targeted weakest-of-batch redos

Memory patches: feedback_wallco_product_page_mode_aware.md gained 5 new
"Known holes" called out by the ui-ux-designer panel review (scale-readout
regex stale-read, dropsPerRoll silent fallback on tall ceilings, count vs
data-level ambiguity, savedWidth localStorage vs active-tab stale-read,
and the missing tiles-readout — now shipped).

Cost: 9 × ~$0.04 ≈ $0.36 on Gemini image-edit. Wall ~90s total.

Files touched

Diff

commit 5782b5100bdd087959cac3538c4d4fdcb70f0d81
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu May 21 19:28:47 2026 -0700

    feat(scenic+ux): 6 Gracie-language colorways + 3 reviewer-redo murals + tiles-readout + sub-brand proposal
    
    Three batches landed this turn, all from the 2026-05-21 reviewer panel
    recommendations (interior-designer + ui-ux-designer + debate-team-fast):
    
    Designs (Gemini image-edit from #39055):
    - 39101-39106 — 6 colorway variants of the burnt-umber Pass-3 winner
      (celadon, oxblood, ivory-on-black, indigo, sand-on-bone, eucalyptus).
      Preserves #39055's flat hand-painted-silk Zuber/Gracie/de Gournay
      silhouette composition; only recolors. The interior-designer panel:
      "the brand's defensible product is this aesthetic; multiply it."
    - 39107 — desert-coral redo of #39062 (the weakest of the original 6,
      uncanny photoreal-flat hybrid). Re-rendered from #39055 with coral as
      the single ink. Old #39062 unpublished.
    - 39108 — B&W agave (#38848) recomposed off-center with a second sepia
      ink tone. Centered-radial was the cardinal mural sin per the reviewer.
      Old #38848 unpublished.
    - 39109 — saguaro dusk (#38908) with cobalt-orange dusk gradient
      swapped for a single oyster-pink wash. Reviewer panel: "stock postcard
      palette → curated quiet twilight." Old #38908 unpublished.
    
    UI (server.js):
    - New #tiles-readout block above the room-setting #scale-viewport:
      "Your 10 × 9 ft wall will hold 5 tiles wide × 9 repeats tall
      (5 drops at 24" roll · 24" repeat)". Hidden in mural mode; live-updates
      from readWallSpec() on every wall/roll/repeat change. Fixes the
      ui-ux-designer panel finding that at default zoom the viewport shows
      a magnified excerpt, not the full wall — customer can't visually count
      tiles.
    
    Sub-brand proposal:
    - docs/SUB-BRAND-PROPOSAL-SCENIC-VS-STUDIO.md drafts the wallco/Scenic
      (90 photoreal hospitality murals) + wallco/Studio (7+ Gracie register)
      split with names TBD, trade-pricing question open, PG migration plan
      spec'd. Memory entry in ~/.claude flags it as awaiting Steve approval
      before any catalog rename ships.
    
    Scripts:
    - scripts/gemini-39055-colorways.js — single-source 6-colorway renderer
    - scripts/gemini-rerender-weak3.js  — targeted weakest-of-batch redos
    
    Memory patches: feedback_wallco_product_page_mode_aware.md gained 5 new
    "Known holes" called out by the ui-ux-designer panel review (scale-readout
    regex stale-read, dropsPerRoll silent fallback on tall ceilings, count vs
    data-level ambiguity, savedWidth localStorage vs active-tab stale-read,
    and the missing tiles-readout — now shipped).
    
    Cost: 9 × ~$0.04 ≈ $0.36 on Gemini image-edit. Wall ~90s total.
---
 docs/SUB-BRAND-PROPOSAL-SCENIC-VS-STUDIO.md | 119 ++++++++++++++++++++
 scripts/gemini-39055-colorways.js           | 140 ++++++++++++++++++++++++
 scripts/gemini-rerender-weak3.js            | 163 ++++++++++++++++++++++++++++
 server.js                                   |  30 +++++
 4 files changed, 452 insertions(+)

diff --git a/docs/SUB-BRAND-PROPOSAL-SCENIC-VS-STUDIO.md b/docs/SUB-BRAND-PROPOSAL-SCENIC-VS-STUDIO.md
new file mode 100644
index 0000000..254b05e
--- /dev/null
+++ b/docs/SUB-BRAND-PROPOSAL-SCENIC-VS-STUDIO.md
@@ -0,0 +1,119 @@
+# wallco.ai — sub-brand proposal: Scenic + Studio
+
+Drafted 2026-05-21 by the panel review (interior-designer + ui-ux-designer + debate-team-fast).
+Status: **proposal — awaiting Steve approval before any catalog rename.**
+
+---
+
+## The problem
+
+The wallco.ai catalog now has **97 mural-class designs** in three internal categories:
+
+| Category | Count | Aesthetic | Generated by |
+|---|---|---|---|
+| `cactus-11ft-mural` | 58 | Photoreal Sonoran landscape | SDXL alone |
+| `tree-mural` | 32 | Photoreal scenic landscape | SDXL alone |
+| `cactus-pine-scenic` | 7 (Pass-3 + redo) | Flat hand-painted-silk (Gracie/de Gournay register) | SDXL → Gemini image-edit |
+
+The original wallco.ai pitch was "AI-generated luxury wallcovering" with an implicit aesthetic anchor on **flat hand-painted scenic register** (Zuber / Gracie / de Gournay). The Pass-3 batch nailed that aesthetic; the other 90 designs landed as **photoreal landscape murals** that LOOK luxurious but are functionally a different product class.
+
+The interior-designer panel was clear: **photoreal at 11ft is a commodity** (Phillip Jeffries, Calico, Rebel Walls all do it better). **Flat hand-painted at sub-$1,500/panel is a moat** (nobody under Gracie/de Gournay is shipping it).
+
+Three reads of the situation, per the debate-team:
+- Catalog diversity (different aesthetics for different rooms)
+- Brand confusion (undermines the hand-painted pitch)
+- Stealth strategy shift (photoreal is where the model is strongest)
+
+Conclusion: **all three reads are simultaneously true.** The honest fix is to NAME the two product lines so customers and trade designers can tell them apart.
+
+---
+
+## The proposal
+
+### wallco / **Scenic**
+
+The photoreal hospitality mural line.
+
+- **Subject:** Photoreal landscape — cactus, redwood, joshua tree, cypress, oak, eucalyptus, agave
+- **Aesthetic:** Photoreal SDXL, atmospheric depth, sunset gradients allowed
+- **Form factor:**
+  - 7.5 ft × 11 ft single-panel (Sonoran single-subject hero) — *was `cactus-11ft-mural`*
+  - 12 ft × 11 ft 4-panel full-room scenic — *was `tree-mural`*
+- **Target install:** Boutique hospitality lobbies, hotel dining rooms, corridor accent walls, residential dining/family-room accents
+- **Buyer:** Interior designer specifying a "view-through-a-window" trompe-l'oeil mural; hospitality procurement
+- **Price tier:** $12-22/sq-ft installed list ($150-250/panel residential)
+- **Competitive frame:** Phillip Jeffries Indigo, Calico Wallpaper "Wonderland," Rebel Walls — but at a fraction of the price
+- **Count today:** 90 designs
+- **Generator pipeline:** SDXL only
+
+### wallco / **Studio**
+
+The flat hand-painted scenic line. *The moat.*
+
+- **Subject:** Flat silhouette scenic — cactus, Monterey pine, agave, ocotillo, mountains, sky
+- **Aesthetic:** Single-layer hand-blocked silkscreen, 2-tone flat register, hard razor-sharp silhouette edges, no gradient, no atmospheric perspective
+- **Form factor:**
+  - 60 ft × 11 ft wraparound (20 panels × 36" × 132") — *was `cactus-pine-scenic`*
+  - 12 ft × 11 ft 4-panel single-wall scenic *(future, when we render some)*
+  - 11 ft single-panel accent *(future)*
+- **Target install:** Luxury hospitality (Four Seasons, Auberge, Aman), high-end residential dining rooms, corporate boardrooms, design-forward retail
+- **Buyer:** Senior interior designer; Designers Today reader; specifier on a $1M+ project
+- **Price tier:** $40-80/sq-ft installed list ($800-1,200/panel)
+- **Competitive frame:** Zuber & Cie, Gracie, de Gournay — at 60-70% of their price point
+- **Count today:** 7 designs (the Pass-3 cactus-pine-scenic batch + the 6 colorway variants being rendered right now)
+- **Generator pipeline:** SDXL → `gemini-2.5-flash-image-edit` 2-pass (the recipe in `feedback_wallco_gemini_edit_recovers_sdxl_aesthetic.md`)
+
+---
+
+## What needs to change
+
+### 1. PG categories
+Rename `category` column values:
+- `cactus-11ft-mural` → `scenic-cactus-11ft`
+- `tree-mural` → `scenic-tree-fullroom`
+- `cactus-pine-scenic` → `studio-cactus-pine-scenic`
+
+OR add a new `product_line` column to `spoon_all_designs` populated as `Scenic` | `Studio` | NULL. Less invasive — old category strings stay valid for any external references.
+
+### 2. Catalog URL structure
+- `/scenic` → photoreal hospitality landing page
+- `/studio` → flat hand-painted hospitality landing page
+- `/designs` → unified browse, both lines, with a sub-brand chip filter
+- Per-design URL stays `/design/:id` (already canonical); add a `data-product-line` attribute the rendered HTML so the page chrome can theme accordingly
+
+### 3. Page copy
+Lead headline on `/scenic`: *"Photoreal hospitality murals — Phillip Jeffries quality, AI-scale catalog, sub-$250 per panel."*
+Lead headline on `/studio`: *"Hand-painted-silk scenic wallcovering in the Gracie / de Gournay register — at 60% of their price point."*
+
+### 4. Trade gates
+Studio products gate pricing behind "Contact for trade pricing" (the Gracie/de Gournay convention). Scenic products show the public price.
+
+### 5. Image-edit pipeline as default for Studio
+Per the debate-team consensus on Q1 — `--two-pass` flag, mandatory for Studio batches, opt-out only with explicit reason. Cost ($0.04/design) is priced into the Studio tier.
+
+---
+
+## What does NOT need to change
+
+- Existing customer-facing URLs (`/design/:id`) stay live
+- Existing 97 designs stay published — no deletion
+- The mural-stepper + room-setting preview stay as-is (already mode-aware)
+- The SDXL Pass-1 + Pass-2 history rows stay in DB as unpublished — internal record of how we got here
+
+---
+
+## Open questions for Steve
+
+1. Are the names right? Steve has veto on "Scenic" and "Studio" — alternatives include:
+   - **wallco / Hospitality** + **wallco / Atelier**
+   - **wallco / Vista** + **wallco / Plaster**
+   - **wallco / Wide** + **wallco / Flat**
+2. Trade-pricing gate on Studio — yes / no?
+3. Should the existing 90 Scenic designs get a "Hospitality" sub-tag (Aman / Auberge / Four Seasons style packs), or is the single category enough?
+4. Next-batch target: 12 more Studio designs at #39055 quality level? Or scale Scenic first?
+
+---
+
+## Memory entry summary
+
+This proposal is paired with a memory entry at `~/.claude/projects/-Users-stevestudio2/memory/project_wallco_scenic_studio_subbrand.md` (creating now) that records the proposal as the canonical brand structure pending approval. Once Steve approves the names + the rename plan, that memory becomes the rule and the catalog UI follows.
diff --git a/scripts/gemini-39055-colorways.js b/scripts/gemini-39055-colorways.js
new file mode 100644
index 0000000..0de7185
--- /dev/null
+++ b/scripts/gemini-39055-colorways.js
@@ -0,0 +1,140 @@
+#!/usr/bin/env node
+/**
+ * 6 colorway variants of the #39055 burnt-umber Gracie-language cactus-pine
+ * scenic. Source = the Pass-3 winning aesthetic; output = same composition,
+ * same flat hand-painted silhouette, NEW ink+ground palettes.
+ *
+ * Per the interior-designer panel review (2026-05-21): "#39055 is the
+ * single most spec-able image of the six; the brand should multiply this
+ * exact aesthetic across 6 colorways."
+ *
+ * Cost: 6 × ~$0.04 = ~$0.24. Wall: ~1 min.
+ */
+'use strict';
+require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') });
+const fs = require('fs');
+const path = require('path');
+const crypto = require('crypto');
+const { execSync, spawnSync } = require('child_process');
+
+const KEY = process.env.GEMINI_API_KEY;
+if (!KEY) { console.error('GEMINI_API_KEY missing'); process.exit(1); }
+const OUT_DIR = path.join(__dirname, '..', 'data', 'generated');
+const SRC_ID = 39055;
+
+const DB = 'dw_unified';
+function psql(sql) { return execSync(`psql ${DB} -At -q`, { input: sql, encoding: 'utf8' }).trim(); }
+function esc(v) { if (v == null) return 'NULL'; return "'" + String(v).replace(/'/g, "''") + "'"; }
+
+const COLORWAYS = [
+  { slug: 'celadon',          ground: '#A8B8A8', ground_name: 'pale celadon green-grey jade',     figure: '#F2EADB', figure_name: 'tortilla-bone cream' },
+  { slug: 'oxblood',          ground: '#6B2A22', ground_name: 'deep oxblood cochineal red',        figure: '#F2EADB', figure_name: 'tortilla-bone cream' },
+  { slug: 'ivory-on-black',   ground: '#1A1A1A', ground_name: 'matte charcoal-black',              figure: '#F0E8D8', figure_name: 'warm ivory cream' },
+  { slug: 'indigo',           ground: '#1E3550', ground_name: 'natural indigo-dye deep blue',      figure: '#F2EADB', figure_name: 'tortilla-bone cream' },
+  { slug: 'sand-on-bone',     ground: '#E8DCC4', ground_name: 'warm chalky bone-cream paper',      figure: '#A8895C', figure_name: 'soft sand-taupe ink' },
+  { slug: 'eucalyptus',       ground: '#7A8B7E', ground_name: 'sage-eucalyptus green-grey',        figure: '#F2EADB', figure_name: 'tortilla-bone cream' },
+];
+
+(async function main() {
+  const row = psql(`SELECT id, local_path, prompt, seed, width_in, height_in, panels, dominant_hex, palette, motifs, tags
+                      FROM spoon_all_designs WHERE id = ${SRC_ID}`);
+  if (!row) { console.error(`source #${SRC_ID} not found`); process.exit(1); }
+  const [_id, srcPath, _srcPrompt, _seed, w, h, panels, _dom, _pal, motifs, tags] = row.split('|');
+  if (!fs.existsSync(srcPath)) { console.error(`source PNG missing: ${srcPath}`); process.exit(1); }
+
+  const imageB64 = fs.readFileSync(srcPath).toString('base64');
+  const newIds = [];
+
+  console.log(`6 Gracie-language colorways via Gemini image-edit from #${SRC_ID}\n`);
+
+  for (let i = 0; i < COLORWAYS.length; i++) {
+    const cw = COLORWAYS[i];
+    const editPrompt = (
+      `Recolor this hand-painted cactus and Monterey pine scenic mural to a NEW colorway, ` +
+      `preserving the EXACT composition, silhouettes, layout, and flat hand-painted-silk register. ` +
+      `Do NOT change positions of saguaros, pines, mountains, or ground line. Do NOT re-introduce ` +
+      `photoreal shading, gradients, or atmospheric perspective — keep the flat 2-tone Zuber/Gracie/ ` +
+      `de Gournay aesthetic intact. ` +
+      `\n\nNew palette: ` +
+      `Ground (sky, mountain band, ground plane all share the same flat ground tone): ${cw.ground_name} (hex ${cw.ground}). ` +
+      `Figure (all silhouettes — cactus, pine trunks, pine canopies, mountain edges): ${cw.figure_name} (hex ${cw.figure}). ` +
+      `\n\nFlat solid color regions only. Hard razor-sharp silhouette edges. Single-layer screenprint register. ` +
+      `Hand-painted-silk scenic in the style of Zuber & Cie, Gracie, and de Gournay. ` +
+      `No watermark, no text, no border, no signature.`
+    );
+
+    process.stdout.write(`[${i+1}/6] ${cw.slug} → `);
+    const t0 = Date.now();
+    const body = {
+      contents: [{
+        parts: [
+          { inline_data: { mime_type: 'image/png', data: imageB64 } },
+          { text: editPrompt },
+        ],
+      }],
+      generationConfig: { responseModalities: ['IMAGE'] },
+    };
+    const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent?key=${KEY}`;
+    const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
+    if (!r.ok) { console.log(`HTTP ${r.status}`); continue; }
+    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: '39055-colorway-' + cw.slug, model: 'gemini-2.5-flash-image' });
+    } catch {}
+    const part = j.candidates?.[0]?.content?.parts?.find(p => p.inline_data || p.inlineData);
+    const data = part?.inline_data?.data || part?.inlineData?.data;
+    if (!data) {
+      const text = j.candidates?.[0]?.content?.parts?.find(p => p.text)?.text;
+      console.log(`no image (${j.candidates?.[0]?.finishReason || 'unknown'}): ${(text || '').slice(0, 120)}`);
+      continue;
+    }
+
+    const newSeed = crypto.randomInt(1, 2 ** 31 - 1);
+    const filename = `gracie_${cw.slug}_${Date.now()}_${newSeed}.png`;
+    const outPath = path.join(OUT_DIR, filename);
+    fs.writeFileSync(outPath, Buffer.from(data, 'base64'));
+
+    const py = spawnSync('python3', ['-c', `
+from PIL import Image
+from collections import Counter
+import json, sys
+img = Image.open(sys.argv[1]).convert('RGB')
+img.thumbnail((300, 300))
+pal = img.quantize(colors=5, method=Image.Quantize.MEDIANCUT).convert('RGB')
+pixels = list(pal.getdata())
+cnt = Counter(pixels)
+total = sum(cnt.values())
+palette = [{'hex':'#{:02x}{:02x}{:02x}'.format(*rgb), 'pct':round(n/total*100,2)} for rgb,n in cnt.most_common(5)]
+print(json.dumps(palette))
+`, outPath], { encoding: 'utf8' });
+    let palette = []; let dominant = cw.ground;
+    try { palette = JSON.parse(py.stdout.trim()); dominant = palette[0]?.hex || cw.ground; } catch {}
+
+    const ins = psql(`
+INSERT INTO spoon_all_designs
+  (kind, brand, width_in, height_in, panels, generator, prompt, seed,
+   image_url, local_path, dominant_hex, palette, motifs, tags, category, is_published)
+VALUES
+  ('mural_panel', 'wallco.ai', ${w}, ${h}, ${panels || 20},
+   'gemini-2.5-flash-image-edit',
+   ${esc(`Gracie-language colorway ${cw.slug} from #${SRC_ID}: ` + editPrompt.slice(0, 1200))},
+   ${newSeed},
+   '/designs/img/by-id/__NEW__',
+   ${esc(outPath)},
+   ${esc(dominant)},
+   ${esc(JSON.stringify(palette))}::jsonb,
+   ${motifs || 'NULL'},
+   ${tags || 'NULL'},
+   'cactus-pine-scenic',
+   TRUE)
+RETURNING id`);
+    const newId = parseInt(ins, 10);
+    psql(`UPDATE spoon_all_designs SET image_url = '/designs/img/by-id/${newId}', is_published = TRUE WHERE id = ${newId}`);
+    newIds.push(newId);
+    console.log(`#${newId} (${((Date.now() - t0)/1000).toFixed(1)}s)`);
+  }
+
+  console.log(`\nDone — ${newIds.length}/${COLORWAYS.length} ok`);
+  console.log('New IDs: ' + JSON.stringify(newIds));
+})();
diff --git a/scripts/gemini-rerender-weak3.js b/scripts/gemini-rerender-weak3.js
new file mode 100644
index 0000000..24e48db
--- /dev/null
+++ b/scripts/gemini-rerender-weak3.js
@@ -0,0 +1,163 @@
+#!/usr/bin/env node
+/**
+ * Re-render the 3 weakest cactus-pine + cactus-11ft + tree-mural designs per
+ * the 2026-05-21 interior-designer panel review:
+ *
+ *   #39062 desert-coral redo (2.2/5) → redo in flat Gracie-language with
+ *     coral as the SINGLE ink, source = #39055 (winning aesthetic), not the
+ *     photoreal Pass-2 source
+ *   #38848 B&W agave centered radial (2.5/5) → asymmetric crop, second ink
+ *     tone (sepia or indigo wash) so it can sit in a room with color
+ *   #38908 saguaro forest dusk (2.8/5) → desaturate sky 30%, replace cobalt-
+ *     orange dusk with single-tone wash (ash-rose / oyster-pink)
+ *
+ * Existing rows go unpublished; new rows replace them in the same category.
+ *
+ * Cost: 3 × ~$0.04 = ~$0.12.
+ */
+'use strict';
+require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') });
+const fs = require('fs');
+const path = require('path');
+const crypto = require('crypto');
+const { execSync, spawnSync } = require('child_process');
+
+const KEY = process.env.GEMINI_API_KEY;
+if (!KEY) { console.error('GEMINI_API_KEY missing'); process.exit(1); }
+const OUT_DIR = path.join(__dirname, '..', 'data', 'generated');
+const DB = 'dw_unified';
+function psql(sql) { return execSync(`psql ${DB} -At -q`, { input: sql, encoding: 'utf8' }).trim(); }
+function esc(v) { if (v == null) return 'NULL'; return "'" + String(v).replace(/'/g, "''") + "'"; }
+
+const JOBS = [
+  {
+    src_id: 39055,   // use the WINNING aesthetic as the base, not the failed #39046
+    replaces: 39062,
+    label: 'desert-coral flat silhouette',
+    edit_prompt: (
+      'Recolor this hand-painted cactus and Monterey pine scenic to a desert-coral colorway. ' +
+      'Preserve the EXACT composition, silhouettes, layout, and flat hand-painted-silk register. ' +
+      'Do NOT re-introduce photoreal shading, gradients, or atmospheric perspective. ' +
+      'Ground (sky, mountains, ground plane all share the same flat tone): sun-baked desert-coral (#B85440). ' +
+      'Figure (all silhouettes — cactus, pine trunks, pine canopies, mountain edges): tortilla-bone cream (#F2EADB). ' +
+      'Single-layer hand-blocked silkscreen, hard razor-sharp edges, Zuber/Gracie/de Gournay register. ' +
+      'No watermark, no text, no border.'
+    ),
+  },
+  {
+    src_id: 38848,
+    replaces: 38848,
+    label: 'B&W agave off-center + warm sepia second ink',
+    edit_prompt: (
+      'Re-compose this monochrome agave specimen into an OFF-CENTER vignette: anchor the leaf cluster ' +
+      'to the right third of the canvas, leave the left two-thirds as flat negative sky space. The plant ' +
+      'should no longer be radially symmetric or dead-center — it should feel like a botanical-screen ' +
+      'composition seen from the side. ' +
+      'Add a SECOND ink tone: a warm sepia-brown wash (#7A4A28) for the inner leaf detail, on top of ' +
+      'the deep black silhouette already present. The flat background stays bone-cream (#F2EADB). ' +
+      'Keep the hand-painted screenprint register. Hard edges. No gradient sky. ' +
+      'Reads as a Zuber-style botanical panel, not a museum poster. No watermark, no text.'
+    ),
+  },
+  {
+    src_id: 38908,
+    replaces: 38908,
+    label: 'saguaro dusk in oyster-pink single-tone wash',
+    edit_prompt: (
+      'Recolor this dusk saguaro forest mural. REMOVE the cobalt-blue-to-hot-orange dusk gradient sky ' +
+      'entirely and replace with a single flat tone of warm oyster-pink (#D4A89A) covering the entire ' +
+      'sky area edge to edge — no gradient, no clouds, no sun glow. ' +
+      'Distant mountains become a single flat silhouette band in deep wine-mauve (#5C3540). ' +
+      'Cactus and foreground scrub in a deeper sepia-charcoal (#3A2A1C). ' +
+      'Drop overall saturation 30 percent. Push toward flat hand-painted-silk register — single ' +
+      'screen-print pass per layer, hard edges, no atmospheric perspective. ' +
+      'Reads as a quiet Sonoran-twilight scenic, not a postcard dusk. No watermark, no text.'
+    ),
+  },
+];
+
+(async function main() {
+  console.log(`Re-rendering ${JOBS.length} weakest murals via Gemini image-edit\n`);
+  const newIds = [];
+
+  for (const job of JOBS) {
+    const row = psql(`SELECT id, local_path, width_in, height_in, panels, palette, motifs, tags, category
+                        FROM spoon_all_designs WHERE id = ${job.src_id}`);
+    if (!row) { console.log(`SKIP src #${job.src_id} missing`); continue; }
+    const [_id, srcPath, w, h, panels, _pal, motifs, tags, category] = row.split('|');
+    if (!fs.existsSync(srcPath)) { console.log(`SKIP src PNG ${srcPath} missing`); continue; }
+
+    process.stdout.write(`[${job.label}] src=#${job.src_id} → `);
+    const imageB64 = fs.readFileSync(srcPath).toString('base64');
+    const body = {
+      contents: [{ parts: [
+        { inline_data: { mime_type: 'image/png', data: imageB64 } },
+        { text: job.edit_prompt },
+      ]}],
+      generationConfig: { responseModalities: ['IMAGE'] },
+    };
+    const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent?key=${KEY}`;
+    const t0 = Date.now();
+    const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
+    if (!r.ok) { console.log(`HTTP ${r.status}`); continue; }
+    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: 'rerender-weak-' + job.replaces, model: 'gemini-2.5-flash-image' });
+    } catch {}
+    const part = j.candidates?.[0]?.content?.parts?.find(p => p.inline_data || p.inlineData);
+    const data = part?.inline_data?.data || part?.inlineData?.data;
+    if (!data) {
+      const text = j.candidates?.[0]?.content?.parts?.find(p => p.text)?.text;
+      console.log(`no image (${j.candidates?.[0]?.finishReason || 'unknown'}): ${(text || '').slice(0, 120)}`);
+      continue;
+    }
+
+    const newSeed = crypto.randomInt(1, 2 ** 31 - 1);
+    const filename = `rerender_${job.replaces}_${Date.now()}_${newSeed}.png`;
+    const outPath = path.join(OUT_DIR, filename);
+    fs.writeFileSync(outPath, Buffer.from(data, 'base64'));
+
+    const py = spawnSync('python3', ['-c', `
+from PIL import Image
+from collections import Counter
+import json, sys
+img = Image.open(sys.argv[1]).convert('RGB')
+img.thumbnail((300, 300))
+pal = img.quantize(colors=5, method=Image.Quantize.MEDIANCUT).convert('RGB')
+pixels = list(pal.getdata())
+cnt = Counter(pixels)
+total = sum(cnt.values())
+palette = [{'hex':'#{:02x}{:02x}{:02x}'.format(*rgb), 'pct':round(n/total*100,2)} for rgb,n in cnt.most_common(5)]
+print(json.dumps(palette))
+`, outPath], { encoding: 'utf8' });
+    let palette = []; let dominant = null;
+    try { palette = JSON.parse(py.stdout.trim()); dominant = palette[0]?.hex; } catch {}
+
+    const ins = psql(`
+INSERT INTO spoon_all_designs
+  (kind, brand, width_in, height_in, panels, generator, prompt, seed,
+   image_url, local_path, dominant_hex, palette, motifs, tags, category, is_published)
+VALUES
+  ('mural_panel', 'wallco.ai', ${w}, ${h}, ${panels || 1},
+   'gemini-2.5-flash-image-edit',
+   ${esc('Re-render of #' + job.replaces + ' (' + job.label + '): ' + job.edit_prompt.slice(0, 1200))},
+   ${newSeed},
+   '/designs/img/by-id/__NEW__',
+   ${esc(outPath)},
+   ${esc(dominant)},
+   ${esc(JSON.stringify(palette))}::jsonb,
+   ${motifs || 'NULL'},
+   ${tags || 'NULL'},
+   ${esc(category)},
+   TRUE)
+RETURNING id`);
+    const newId = parseInt(ins, 10);
+    psql(`UPDATE spoon_all_designs SET image_url = '/designs/img/by-id/${newId}', is_published = TRUE WHERE id = ${newId}`);
+    psql(`UPDATE spoon_all_designs SET is_published = FALSE WHERE id = ${job.replaces}`);
+    newIds.push(newId);
+    console.log(`#${newId} (${((Date.now() - t0)/1000).toFixed(1)}s)  · old #${job.replaces} unpublished`);
+  }
+
+  console.log(`\nDone — ${newIds.length}/${JOBS.length} ok. New IDs: ${JSON.stringify(newIds)}`);
+})();
diff --git a/server.js b/server.js
index 2e8deea..04f8cc3 100644
--- a/server.js
+++ b/server.js
@@ -7605,6 +7605,13 @@ ${htmlHeader('/designs')}
             #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>
+          <!-- N×M tiles callout — single-line readout of how many roll-widths
+               and pattern-repeats the customer's wall will hold. Updates live
+               from readWallSpec() on every wall/roll/repeat change. Hidden in
+               mural mode (level=0) where the concept doesn't apply. -->
+          <div id="tiles-readout" style="display:none;font:12px var(--sans);color:var(--ink-soft,#555);margin:8px 2px 6px;text-align:center;letter-spacing:.02em">
+            Your <strong id="tr-wall">10 × 9 ft</strong> wall will hold <strong id="tr-grid">5 tiles wide × 9 repeats tall</strong> <span style="color:var(--ink-faint,#999)">(5 drops at <span id="tr-roll">24"</span> roll · <span id="tr-repeat">24"</span> repeat)</span>
+          </div>
           <div id="scale-viewport" style="width:100%;aspect-ratio:20/11;border:1px solid var(--line);border-radius:8px;overflow:hidden;background-image:url('${design.image_url}');background-repeat:no-repeat;background-size:cover;background-position:50% 50%;position:relative;user-select:none;-webkit-user-select:none">
             <div class="mural-hint" id="mural-hint" style="display:none">
               <span>20 ft × 11 ft mural · drag to position · scroll to zoom · double-click to reset</span>
@@ -7757,6 +7764,29 @@ ${htmlHeader('/designs')}
               if (soldBy) soldBy.innerHTML = isMural ? 'Per panel (36"&nbsp;×&nbsp;11&nbsp;ft)' : 'Per roll (27&nbsp;ft)';
               const hint = document.getElementById('scale-width-hint');
               if (hint) hint.textContent = isMural ? '36" panel × 11 ft, sold per panel' : '24" wide, sold per roll (27 ft)';
+              // N×M tiles textual readout (UX-designer panel review 2026-05-21):
+              // at zoom != 0.5× the viewport shows a magnified excerpt, not the
+              // full wall — customer cannot count tiles visually. This text
+              // spells it out: "Your 10×9 ft wall will hold 5 tiles × 9 repeats".
+              const trBlock = document.getElementById('tiles-readout');
+              if (trBlock) {
+                if (isMural) {
+                  trBlock.style.display = 'none';
+                } else {
+                  trBlock.style.display = '';
+                  var sp2 = readWallSpec();
+                  var tilesX   = Math.ceil(sp2.wallWIn / sp2.rollW);
+                  var repeatsY = Math.ceil(sp2.wallHIn / sp2.repeatIn);
+                  var trWall   = document.getElementById('tr-wall');
+                  var trGrid   = document.getElementById('tr-grid');
+                  var trRoll   = document.getElementById('tr-roll');
+                  var trRep    = document.getElementById('tr-repeat');
+                  if (trWall) trWall.textContent = sp2.wallWFt + ' × ' + sp2.wallHFt + ' ft';
+                  if (trGrid) trGrid.textContent = tilesX + ' tiles wide × ' + repeatsY + ' repeats tall';
+                  if (trRoll) trRoll.textContent = sp2.rollW + '"';
+                  if (trRep)  trRep.textContent  = sp2.repeatIn + '"';
+                }
+              }
             }
             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(); }));

← 3ddc65b feat(cactus+murals): 48 cactus-11ft + 32 tree-murals + 6 cac  ·  back to Wallco Ai  ·  regen 10 BLEED_GHOST_REGEN lips via SDXL→Gemini 2-pass — new 38532bd →