[object Object]

← back to Wallco Ai

feat(cactus+murals): 48 cactus-11ft + 32 tree-murals + 6 cactus-pine scenics + product-page mode-aware UI

3ddc65b47d49112427475e0553a7757bd02d7e99 · 2026-05-21 18:40:03 -0700 · Steve Abrams

Generation scripts:
- generate-cactus-11ft-pass2.js — 50 more 11ft cactus murals (Pass 4) covering
  saguaro variants, joshua, boojum, ocotillo, organ-pipe, cardon, agave,
  cholla, barrel, totem-pole, queen-of-the-night × 10 Tucson colorways.
  48/50 succeeded; ~$0.14 on Replicate.
- generate-cactus-36x132.js — narrow 36×132 accent-wall variant (scripted
  but superseded by the tree-mural / cactus-pine-scenic models).
- generate-tree-murals.js — 32 tree-mural Pass 1 designs at 12ft × 11ft (4
  panels × 36in × 132in), 8 species × 4 colorways. Photoreal landing per
  SDXL's anti-prompt blindness; Steve approved publish-as-is.
- generate-cactus-pine-scenic.js — 6 wraparound L→R scenic murals at 60ft ×
  11ft (20 panels × 36in × 132in) in the style of Zuber / Gracie /
  de Gournay. Two passes: Pass 1 style-first dropped cactus; Pass 2
  cactus-first dropped pine. Pass 3 Gemini image-edit fixed both.
- gemini-inject-pine-pass3.js — calls gemini-2.5-flash-image-edit on the
  6 Pass-2 scenics to inject Monterey pine onto the indicated edge while
  preserving cactus content + pushing toward flat hand-painted-silk
  aesthetic. 6/6 ok in 56s; ~$0.24.
- gemini-redo-39057.js — re-edit one scenic (#39046 → #39062) whose pine
  had landed in a separated-panel feel rather than integrated.

Product page (server.js):
- mural-stepper apply() now toggles full mode-aware spec rows: shows
  Panel-size / hides Repeat + Roll-length in MURAL mode (level 0),
  inverse in WALLPAPER mode (levels 1-4). Sold-by label swaps between
  "Per panel (36" × 11 ft)" and "Per roll (27 ft)".
- Wall-size calculator branches on mode: MURAL math (panels =
  ceil(wall_w_in/roll_w_in) × ceil(wall_h_ft/11), $12-22/sq-ft estimate);
  WALLPAPER math (drop_length = wall_h + repeat, drops_per_roll =
  floor(27/drop_length), rolls = ceil(drops/drops_per_roll), $5-10/sq-ft).
  Eliminates the "sold per yard" framing across the entire page.
- Room-setting tile preview rewired to real wall × roll × repeat math:
  tile_w_pct = (roll_width_in / wall_width_in) × zoom, tile_h_pct =
  (repeat_in / wall_height_in) × zoom. The 'count' (level 2/4/8/16)
  becomes a ZOOM factor rather than abstract density. apply() re-fires
  when wall-w/wall-h inputs change, when the roll-width tab is clicked,
  and when the repeat slider moves. Customer sees a literal scale
  preview of how the pattern lands on their actual wall.
- Admin grid: pattern-num DIG pill moved from top:left → top:right so it
  no longer covers the bulk-select checkbox. decision-badge auto-shifts
  to top:36px when both visible.

Fixed along the way: backtick-in-template-literal trap that 502'd prod
for ~90s (memory feedback_backticks_in_template_literal_comments) —
comment contained `count` inside the outer server-side template literal.

Files touched

Diff

commit 3ddc65b47d49112427475e0553a7757bd02d7e99
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu May 21 18:40:03 2026 -0700

    feat(cactus+murals): 48 cactus-11ft + 32 tree-murals + 6 cactus-pine scenics + product-page mode-aware UI
    
    Generation scripts:
    - generate-cactus-11ft-pass2.js — 50 more 11ft cactus murals (Pass 4) covering
      saguaro variants, joshua, boojum, ocotillo, organ-pipe, cardon, agave,
      cholla, barrel, totem-pole, queen-of-the-night × 10 Tucson colorways.
      48/50 succeeded; ~$0.14 on Replicate.
    - generate-cactus-36x132.js — narrow 36×132 accent-wall variant (scripted
      but superseded by the tree-mural / cactus-pine-scenic models).
    - generate-tree-murals.js — 32 tree-mural Pass 1 designs at 12ft × 11ft (4
      panels × 36in × 132in), 8 species × 4 colorways. Photoreal landing per
      SDXL's anti-prompt blindness; Steve approved publish-as-is.
    - generate-cactus-pine-scenic.js — 6 wraparound L→R scenic murals at 60ft ×
      11ft (20 panels × 36in × 132in) in the style of Zuber / Gracie /
      de Gournay. Two passes: Pass 1 style-first dropped cactus; Pass 2
      cactus-first dropped pine. Pass 3 Gemini image-edit fixed both.
    - gemini-inject-pine-pass3.js — calls gemini-2.5-flash-image-edit on the
      6 Pass-2 scenics to inject Monterey pine onto the indicated edge while
      preserving cactus content + pushing toward flat hand-painted-silk
      aesthetic. 6/6 ok in 56s; ~$0.24.
    - gemini-redo-39057.js — re-edit one scenic (#39046 → #39062) whose pine
      had landed in a separated-panel feel rather than integrated.
    
    Product page (server.js):
    - mural-stepper apply() now toggles full mode-aware spec rows: shows
      Panel-size / hides Repeat + Roll-length in MURAL mode (level 0),
      inverse in WALLPAPER mode (levels 1-4). Sold-by label swaps between
      "Per panel (36" × 11 ft)" and "Per roll (27 ft)".
    - Wall-size calculator branches on mode: MURAL math (panels =
      ceil(wall_w_in/roll_w_in) × ceil(wall_h_ft/11), $12-22/sq-ft estimate);
      WALLPAPER math (drop_length = wall_h + repeat, drops_per_roll =
      floor(27/drop_length), rolls = ceil(drops/drops_per_roll), $5-10/sq-ft).
      Eliminates the "sold per yard" framing across the entire page.
    - Room-setting tile preview rewired to real wall × roll × repeat math:
      tile_w_pct = (roll_width_in / wall_width_in) × zoom, tile_h_pct =
      (repeat_in / wall_height_in) × zoom. The 'count' (level 2/4/8/16)
      becomes a ZOOM factor rather than abstract density. apply() re-fires
      when wall-w/wall-h inputs change, when the roll-width tab is clicked,
      and when the repeat slider moves. Customer sees a literal scale
      preview of how the pattern lands on their actual wall.
    - Admin grid: pattern-num DIG pill moved from top:left → top:right so it
      no longer covers the bulk-select checkbox. decision-badge auto-shifts
      to top:36px when both visible.
    
    Fixed along the way: backtick-in-template-literal trap that 502'd prod
    for ~90s (memory feedback_backticks_in_template_literal_comments) —
    comment contained `count` inside the outer server-side template literal.
---
 scripts/gemini-inject-pine-pass3.js    | 207 ++++++++++++++++++++++++++++++
 scripts/gemini-redo-39057.js           | 153 +++++++++++++++++++++++
 scripts/generate-cactus-11ft-pass2.js  | 185 +++++++++++++++++++++++++++
 scripts/generate-cactus-36x132.js      | 146 ++++++++++++++++++++++
 scripts/generate-cactus-pine-scenic.js | 221 +++++++++++++++++++++++++++++++++
 scripts/generate-tree-murals.js        | 167 +++++++++++++++++++++++++
 server.js                              | 190 ++++++++++++++++++++++------
 7 files changed, 1231 insertions(+), 38 deletions(-)

diff --git a/scripts/gemini-inject-pine-pass3.js b/scripts/gemini-inject-pine-pass3.js
new file mode 100644
index 0000000..7b29b33
--- /dev/null
+++ b/scripts/gemini-inject-pine-pass3.js
@@ -0,0 +1,207 @@
+#!/usr/bin/env node
+/**
+ * Pass 3 — Gemini image-edit on the 6 cactus-pine-scenic Pass-2 renders
+ * (39041-39046) to: (a) inject Monterey pines on the indicated edge while
+ * preserving the existing cactus content + composition, and (b) push toward
+ * the flat hand-painted Gracie / de Gournay / Zuber 2-tone aesthetic that
+ * SDXL refused to honor.
+ *
+ * Per memory `feedback_sdxl_antiprompts_unreliable`: "Switch to
+ * gemini-2.5-flash-image-edit or FLUX for flat 2-tone designer work".
+ *
+ * Outputs land as NEW rows in spoon_all_designs (category =
+ * cactus-pine-scenic, kind = mural_panel, panels=20, 720"×132"). Original
+ * Pass-2 rows stay published as Sonoran scenics; admin can pick which set
+ * to keep.
+ *
+ * Cost: 6 × ~$0.04 = ~$0.24 on Gemini image-edit.
+ */
+
+'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');
+fs.mkdirSync(OUT_DIR, { recursive: true });
+
+const DB = process.env.WALLCO_DB || 'dw_unified';
+const PSQL = process.platform === 'linux'
+  ? `sudo -n -u postgres psql ${DB} -At -q`
+  : `psql ${DB} -At -q`;
+function psql(sql) { return execSync(PSQL, { input: sql, encoding: 'utf8' }).trim(); }
+function esc(v) { if (v == null) return 'NULL'; return "'" + String(v).replace(/'/g, "''") + "'"; }
+
+// Per-source edit prompt. Each one names which edge to add pines on (matches
+// the original Pass-2 composition spec), names the colorway, and asks Gemini
+// to PUSH the existing photoreal render toward flat hand-painted silk.
+const SHARED_PUSH = (
+  'TRANSFORM THE STYLE — convert this image from photorealistic to a HAND-PAINTED-SILK SCENIC ' +
+  'wallcovering in the style of Zuber & Cie, Gracie, and de Gournay: ' +
+  'flat solid color regions (no gradient, no atmospheric perspective, no half-tones, no crosshatching), ' +
+  'hard razor-sharp silhouette edges, single-layer screenprint register, 2-tone palette only. ' +
+  'The sky / background area should be one completely flat solid color from edge to edge — ' +
+  'remove any sky gradient, clouds, atmospheric haze, or color variation in the sky. ' +
+  'Distant mountains should be a single flat silhouette band, no atmospheric fading. ' +
+  'Every cactus, plant, and tree should be one flat color silhouette — no internal shading, no highlights, no fur texture. ' +
+  'Keep ALL the existing cactus content (saguaros, ocotillo, prickly pear, agave, cholla) — do not remove or replace them. ' +
+  'No watermark, no text, no border.'
+);
+
+const PASS_2_IDS = [39041, 39042, 39043, 39044, 39045, 39046];
+
+const EDITS = {
+  39041: {
+    edge:   'right',
+    palette:'soft dawn coral cream ground (#E8C5A8) + deep cypress brown silhouette (#3A2A1C)',
+    pines:  'Three tall windswept Monterey pines (Pinus radiata) silhouetted on a granite coastal bluff at the FAR RIGHT EDGE of the canvas. Their canopies lean inland (leftward), trunks visible from the lower third up. Pure flat cypress-brown silhouette against the dawn-coral sky.',
+  },
+  39042: {
+    edge:   'left',
+    palette:'Sonoran twilight indigo blue ground (#1E3550) + tortilla-bone cream silhouette (#F2EADB)',
+    pines:  'Three tall Monterey pines on a low coastal bluff at the FAR LEFT EDGE of the canvas. Trunks and canopies silhouetted in flat cream against the indigo sky. Canopies windswept leaning rightward toward the desert.',
+  },
+  39043: {
+    edge:   'right',
+    palette:'sage-mint spring meadow ground (#A8B79A) + cypress charcoal silhouette (#3A2A1C)',
+    pines:  'Two Monterey pines on a low Carmel-style bluff at the FAR RIGHT EDGE. Trunks rising from the lower edge up to about 80 percent of canvas height, canopies rounded, leaning leftward. Flat cypress-charcoal silhouette against the sage-mint sky.',
+  },
+  39044: {
+    edge:   'right',
+    palette:'burnt-umber autumn saddle ground (#7A4A28) + tortilla-bone cream silhouette (#F2EADB)',
+    pines:  'A small Monterey pine windbreak of three pines on a coastal bluff at the FAR RIGHT EDGE. Tall canopies leaning inland. Flat tortilla-bone cream silhouette against the burnt-umber sky.',
+  },
+  39045: {
+    edge:   'right',
+    palette:'Pacific-mist pewter grey ground (#9CA09B) + dark cypress charcoal silhouette (#3A2A1C)',
+    pines:  'Three tall Monterey pines emerging from low fog on a coastal bluff at the FAR RIGHT EDGE. Lower trunks partly obscured by the fog band, upper canopies dark and silhouetted against the pewter sky. Flat cypress charcoal silhouette.',
+  },
+  39046: {
+    edge:   'right',
+    palette:'sun-baked-clay desert coral ground (#B85440) + tortilla-bone cream silhouette (#F2EADB)',
+    pines:  'Two Monterey pines on a Carmel-style coastal bluff at the FAR RIGHT EDGE. Canopies rounded windswept leaning inland (leftward). Flat tortilla-bone cream silhouette against the desert-coral sky.',
+  },
+};
+
+async function editOne(id) {
+  // Pull the source row + image path
+  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 = ${id}`);
+  if (!row) throw new Error(`design ${id} not found`);
+  const [_id, srcPath, srcPrompt, srcSeed, w, h, panels, dom, pal, motifs, tags] = row.split('|');
+  if (!fs.existsSync(srcPath)) throw new Error(`source PNG missing: ${srcPath}`);
+
+  const edit = EDITS[id];
+  if (!edit) throw new Error(`no edit recipe for id ${id}`);
+
+  const imageB64 = fs.readFileSync(srcPath).toString('base64');
+  const fullPrompt = (
+    `Add to this Sonoran cactus desert scenic mural: ` +
+    `${edit.pines} ` +
+    `The pines should occupy the ${edit.edge}most 15-20 percent of the canvas width. ` +
+    `Palette: ${edit.palette}. ` +
+    SHARED_PUSH
+  );
+
+  const body = {
+    contents: [{
+      parts: [
+        { inline_data: { mime_type: 'image/png', data: imageB64 } },
+        { text: fullPrompt },
+      ],
+    }],
+    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) throw new Error(`HTTP ${r.status}: ${await r.text()}`);
+  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: 'cactus-pine-scenic-pass3-inject-pine', model: 'gemini-2.5-flash-image' });
+  } catch { /* non-fatal */ }
+
+  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;
+    throw new Error(`no image (${j.candidates?.[0]?.finishReason || 'unknown'}): ${(text || '').slice(0, 200)}`);
+  }
+
+  const newSeed = crypto.randomInt(1, 2 ** 31 - 1);
+  const filename = `cactus_pine_pass3_${Date.now()}_${newSeed}.png`;
+  const outPath = path.join(OUT_DIR, filename);
+  fs.writeFileSync(outPath, Buffer.from(data, 'base64'));
+  const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
+
+  // Re-extract dominant + palette from the new image
+  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 = dom;
+  try { palette = JSON.parse(py.stdout.trim()); dominant = palette[0]?.hex || dom; } catch {}
+
+  // INSERT new row, leaving the source unchanged.
+  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(`Pass 3 (pine injection from #${id}): ` + fullPrompt.slice(0, 1500))},
+   ${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}' WHERE id = ${newId}`);
+  console.log(`  ✓ from #${id} → #${newId} · ${filename} · ${elapsed}s`);
+  return newId;
+}
+
+(async function main() {
+  console.log(`Pass 3 — Gemini inject pine + push to flat aesthetic on ${PASS_2_IDS.length} scenics\n`);
+  const created = [];
+  const failures = [];
+  for (const id of PASS_2_IDS) {
+    process.stdout.write(`[${PASS_2_IDS.indexOf(id) + 1}/${PASS_2_IDS.length}] editing #${id} → `);
+    try {
+      const newId = await editOne(id);
+      created.push(newId);
+    } catch (e) {
+      console.log(`FAIL ${e.message}`);
+      failures.push({ id, err: e.message });
+    }
+  }
+  console.log(`\nDone — ${created.length} ok, ${failures.length} fail`);
+  console.log('New IDs: ' + JSON.stringify(created));
+  if (failures.length) console.log('Failures: ' + JSON.stringify(failures, null, 2));
+})();
diff --git a/scripts/gemini-redo-39057.js b/scripts/gemini-redo-39057.js
new file mode 100644
index 0000000..b5a28a4
--- /dev/null
+++ b/scripts/gemini-redo-39057.js
@@ -0,0 +1,153 @@
+#!/usr/bin/env node
+/**
+ * One-off — redo cactus-pine-scenic id 39057. Its Pass-3 pine landed in a
+ * separated-panel feel rather than integrated into the continuous landscape.
+ *
+ * Source = 39046 (the Pass-2 cactus-only desert-coral scenic).
+ * Strategy: same Gemini image-edit call as Pass-3, but with a tighter prompt
+ * that forces the pine to belong to the same desert landscape — same ground
+ * plane, same horizon line, same lighting, same palette — as a natural
+ * continuation of the cactus scene on the right side.
+ *
+ * Output:
+ *   - new row in spoon_all_designs (kept as cactus-pine-scenic)
+ *   - 39057 left in DB but is_published flipped to FALSE
+ */
+
+'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  = 39046;  // the Pass-2 cactus scenic in desert-coral palette
+
+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 EDIT_PROMPT = (
+  'Add Monterey pine trees (Pinus radiata) to this Sonoran desert cactus scenic so they are ' +
+  'FULLY INTEGRATED into the same continuous landscape — NOT a separate panel, NOT a stuck-on ' +
+  'box, NOT a side strip. ' +
+  '\n\nIntegration requirements: ' +
+  'Place 2 to 3 tall windswept Monterey pines on the right-hand third of the canvas, ROOTED ' +
+  'into the same desert floor as the saguaros, with the same ground line, the same horizon, ' +
+  'and the same flat sky tone overhead. The pines should look like they grew naturally on ' +
+  'the same bluff as the cactus — coastal-desert ecotone where Carmel-style headland meets ' +
+  'Sonoran scrub. Trunks visible from the lower third of the canvas, canopies windswept ' +
+  'leaning slightly leftward (inland). ' +
+  '\n\nKeep ALL the existing cactus content (saguaros, ocotillo, prickly pear, agave, cholla, ' +
+  'mountains, ground texture, sky). Do NOT crop, do NOT add a vertical divider line, do NOT ' +
+  'create a separate inset panel. The composition must read as one continuous 60-foot mural. ' +
+  '\n\nStyle: PUSH the entire image toward a flat hand-painted-silk scenic wallcovering in ' +
+  'the manner of Zuber & Cie, Gracie, de Gournay. Flat solid color regions, hard razor-sharp ' +
+  'silhouette edges, single-layer screenprint register, 2-3 tone palette (sun-baked desert ' +
+  'coral ground #B85440 + tortilla-bone cream silhouette #F2EADB + soft accent). Remove any ' +
+  'photoreal shading, atmospheric perspective, sky gradient, or half-tone — the sky should ' +
+  'be ONE flat solid coral color edge to edge. ' +
+  '\n\nPalette: desert coral ground (#B85440), tortilla-bone cream highlights (#F2EADB), dark ' +
+  'shadow silhouette accent (#3A2520). ' +
+  '\n\nNo watermark, no text, no border, no signature.'
+);
+
+(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 body = {
+    contents: [{
+      parts: [
+        { inline_data: { mime_type: 'image/png', data: imageB64 } },
+        { text: EDIT_PROMPT },
+      ],
+    }],
+    generationConfig: { responseModalities: ['IMAGE'] },
+  };
+  const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent?key=${KEY}`;
+
+  console.log(`Gemini image-edit: source #${SRC_ID} → new row (integrated pine, desert-coral)`);
+  const t0 = Date.now();
+  const r = await fetch(url, {
+    method:  'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body:    JSON.stringify(body),
+  });
+  if (!r.ok) {
+    console.error(`HTTP ${r.status}: ${await r.text()}`);
+    process.exit(1);
+  }
+  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: 'redo-39057-integrated-pine', model: 'gemini-2.5-flash-image' });
+  } catch { /* non-fatal */ }
+  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.error(`no image (${j.candidates?.[0]?.finishReason || 'unknown'}): ${(text || '').slice(0, 200)}`);
+    process.exit(1);
+  }
+
+  const newSeed = crypto.randomInt(1, 2 ** 31 - 1);
+  const filename = `cactus_pine_pass3b_${Date.now()}_${newSeed}.png`;
+  const outPath = path.join(OUT_DIR, filename);
+  fs.writeFileSync(outPath, Buffer.from(data, 'base64'));
+  console.log(`  wrote ${filename} (${((Date.now() - t0) / 1000).toFixed(1)}s)`);
+
+  // Re-extract palette
+  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 = dom;
+  try { palette = JSON.parse(py.stdout.trim()); dominant = palette[0]?.hex || dom; } 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(`Pass 3b (redo of 39057, integrated pine): ` + EDIT_PROMPT.slice(0, 1500))},
+   ${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}`);
+
+  // Unpublish the original 39057
+  psql(`UPDATE spoon_all_designs SET is_published = FALSE WHERE id = 39057`);
+
+  console.log(`  → new id #${newId} (replaces 39057, original unpublished)`);
+  console.log(`  local path: ${outPath}`);
+})();
diff --git a/scripts/generate-cactus-11ft-pass2.js b/scripts/generate-cactus-11ft-pass2.js
new file mode 100644
index 0000000..2941645
--- /dev/null
+++ b/scripts/generate-cactus-11ft-pass2.js
@@ -0,0 +1,185 @@
+#!/usr/bin/env node
+// Pass 4 — 50 more 11ft-tall single-cactus / tall-desert-tree murals.
+//
+// Brief (Steve, 2026-05-21): "do 50 more of the cactus tall trees" — same
+// look as Pass 3 (10 designs, IDs 38842-38851) which Steve approved as
+// "they look like luxury hospitality murals". Pass 3 came back as
+// photoreal desert scenes with one big cactus front-and-center; we keep
+// that vibe and just expand the species + colorway matrix.
+//
+// What's new:
+//   - 25 distinct cactus / tall-desert-tree species/arrangements (Pass 3 had 10).
+//   - 10 Tucson colorways carried over from Pass 1's palette.
+//   - 50 unique (species × colorway) pairs sampled non-repeating from the
+//     250-cell matrix.
+//   - Same SDXL 832×1216 portrait, same 90"×132" (7.5 × 11 ft) print spec,
+//     same single-panel mural kind.
+//
+// Settlement: still single isolated subject on solid ground → A1+A2+A3 fail
+// by construction, cactus not in Part B. Gate passes.
+//
+// Cost: 50 × ~$0.003 on Replicate SDXL ≈ $0.15. Mac1 ComfyUI bypassed
+// because the night-builder/drunk-animals crons keep its queue jammed.
+//
+// To run:  cd ~/Projects/wallco-ai && node scripts/generate-cactus-11ft-pass2.js
+
+const { spawnSync } = require('child_process');
+const path = require('path');
+const ROOT = path.join(__dirname, '..');
+
+// 25 distinct "tall desert tree" arrangements. Each `desc` is consumed
+// verbatim by the prompt builder below.
+const ARRANGEMENTS = [
+  // ── saguaros — the canonical Sonoran giant
+  { slug: 'saguaro-classic-two-arm',      species: 'saguaro',           desc: 'A classic two-arm candelabra saguaro, trunk thick at the base and tapering slightly toward the crown, ribs clearly visible, two symmetric arms curving up from each side at 40% trunk height, arm tips reaching about 75% of total trunk height. Reads as a 30-foot mature saguaro.' },
+  { slug: 'saguaro-six-arm-elder',        species: 'saguaro',           desc: 'A massive elder saguaro with six arms in dense candelabra arrangement — three on the left at staggered 25/50/75% heights, three on the right at 35/60/80%. Trunk very thick at base, ribs cracked from age, slightly weathered silhouette.' },
+  { slug: 'saguaro-bare-column',          species: 'saguaro',           desc: 'A single young mature saguaro with NO arms — pure vertical ribbed column, tapered crown, very slender silhouette. Minimalist and monumental.' },
+  { slug: 'saguaro-asymmetric-long-arm',  species: 'saguaro',           desc: 'A tall slender saguaro with ONE long arm curving up the right side starting at 30% trunk height and reaching near the top, plus ONE stubby short arm on the left at 20% height. Asymmetric, graceful, slightly windswept.' },
+  { slug: 'saguaro-three-arm-balanced',   species: 'saguaro',           desc: 'A saguaro with THREE arms — two main arms curving up symmetrically from each side at 45% trunk height, plus a third smaller arm budding off the upper-right main arm at 70% of its length. Three-fingered candelabra.' },
+  { slug: 'saguaro-old-leaning',          species: 'saguaro',           desc: 'A very old saguaro leaning slightly to one side, trunk weathered with woody scars, four arms in irregular positions — two curving up dramatically, one bent partway up like an elbow, one short stub near the base. Reads as an ancient specimen.' },
+  { slug: 'cardon-giant-mexican',         species: 'cardon',            desc: 'A Mexican cardon cactus — taller and thicker than a saguaro, with MULTIPLE branching arms from low on the trunk creating a candelabra of 8-12 vertical stems all reaching upward. Looks like a giant tuning fork. Native to Baja.' },
+
+  // ── joshua trees / yucca relatives
+  { slug: 'joshua-tree-classic',          species: 'joshua tree',       desc: 'A single mature Joshua tree (Yucca brevifolia) — gnarled trunk with multiple thick branches splitting in twisted candelabra pattern, each branch tipped with a dense rosette of spiky narrow leaves. Reads as a Mojave Desert icon.' },
+  { slug: 'joshua-tree-tall-young',       species: 'joshua tree',       desc: 'A tall young Joshua tree with a single straight trunk and only two or three branches near the top, each topped with a leaf rosette. More vertical than the classic gnarled form.' },
+  { slug: 'joshua-tree-elder-multi',      species: 'joshua tree',       desc: 'An ancient Joshua tree with TEN or more branches twisting in every direction from a massive central trunk, each branch ending in a leaf-tuft. Reads as 300 years old, monumental presence.' },
+
+  // ── boojum / cirio
+  { slug: 'boojum-tree-tapered',          species: 'boojum tree',       desc: 'A single boojum tree (Fouquieria columnaris) — a tall slightly tapered column rising from a thick base to a narrow top, the whole trunk covered in stubby horizontal branches like a giant inverted carrot. Native to Baja California.' },
+
+  // ── ocotillo
+  { slug: 'ocotillo-fan',                 species: 'ocotillo',          desc: 'A single ocotillo plant — 15-20 long whip-like stems radiating up and outward from one base point, each stem covered in tiny rounded bumps (spine/leaf nodes). Looks like a frozen firework.' },
+  { slug: 'ocotillo-in-bloom',            species: 'ocotillo',          desc: 'A single ocotillo in full red bloom — long whip stems radiating up, each tipped with a brilliant red-orange flame-shaped flower cluster against the silhouette.' },
+
+  // ── organ pipe
+  { slug: 'organ-pipe-cluster',           species: 'organ-pipe',        desc: 'A dense organ-pipe cactus cluster — 15-20 parallel vertical stems rising from a shared base, varying in height, all roughly the same diameter, looks like a pipe organ. Tightest stems in the center, slightly shorter ones fanning out.' },
+  { slug: 'organ-pipe-three-stems',       species: 'organ-pipe',        desc: 'A young organ-pipe cactus with only THREE main stems rising side-by-side from the shared base — middle stem tallest, two flanking stems about 80% as tall. Minimalist trio.' },
+  { slug: 'mexican-fence-post-row',       species: 'mexican fence post',desc: 'A row of mexican-fence-post cacti — six tall slender pillar stems standing parallel in a line, each ribbed, all the same diameter, varying slightly in height. Reads as a living architectural fence.' },
+
+  // ── prickly pear (Opuntia)
+  { slug: 'prickly-pear-tower',           species: 'prickly-pear',      desc: 'A large prickly-pear cactus tower — oval paddle pads stacked vertically in a zigzag, growing wider and taller toward the top. Each pad covered in subtle spine clusters. Single isolated plant.' },
+  { slug: 'prickly-pear-with-fruit',      species: 'prickly-pear',      desc: 'A prickly-pear tower with red ripe fruits crowning the top pads — pads stacked zigzag, fruits clustered in groups of two and three along the edges of the upper paddle pads.' },
+
+  // ── agave
+  { slug: 'agave-bloom-stalk',            species: 'agave',             desc: 'A monumental agave americana with a tall flowering stalk — wide spiky leaves radiating from a tight ground-level rosette, a 20-foot bloom stalk shooting straight up the center, branched flower spikes at the top. The bloom stalk reaches the 11-foot scale.' },
+  { slug: 'agave-rosette-only',           species: 'agave',             desc: 'A massive agave americana rosette without bloom stalk — wide spiky leaves radiating in all directions from a tight center, each leaf curving slightly, longest leaves nearly horizontal. Architectural, monumental.' },
+
+  // ── cholla
+  { slug: 'cholla-branching',             species: 'cholla',            desc: 'A jumping-cholla / teddy-bear-cholla cactus — trunk-like lower stem, branching into multiple chubby cylindrical segments, each segment covered in dense fuzzy spines rendered as a soft halo around the silhouette. Candelabra-from-below shape.' },
+  { slug: 'cholla-walking-stick',         species: 'cholla',            desc: 'A walking-stick cholla — single tall slender main trunk with thin lateral branches sticking out horizontally at intervals like a coat rack, every branch covered in fine spines (halo texture).' },
+
+  // ── barrel
+  { slug: 'barrel-cactus-hero',           species: 'barrel cactus',     desc: 'A single dramatic golden-barrel cactus — perfectly round, very large, ribbed in deep vertical pleats, with a halo of golden spines around the crown. Centered at the bottom of the canvas occupying the lower half.' },
+
+  // ── totem pole
+  { slug: 'totem-pole-cactus',            species: 'totem pole cactus', desc: 'A totem-pole cactus (Lophocereus schottii) — tall single column with smooth bumpy unsymmetrical surface like a tribal totem pole, no spines, slightly twisted form. Reads as a Baja-California specimen.' },
+
+  // ── queen of the night
+  { slug: 'queen-of-night-night-blooming',species: 'queen of the night',desc: 'A queen-of-the-night cactus (Peniocereus greggii) — tall lanky stems looking dead and thorny by day, but with one ENORMOUS white blooming flower opened at the top. Single bloom, very dramatic against silhouette.' },
+];
+
+// 10 Tucson colorways. (ground = background paper, figure = ink color)
+const COLORWAYS = [
+  { slug: 'mesquite-on-bone',    ground_hex: '#E8DCC4', ground_name: 'warm chalky cream paper',          figure_hex: '#2A2520', figure_name: 'deep mesquite-charcoal ink' },
+  { slug: 'catalina-dusk',       ground_hex: '#7A4A52', ground_name: 'Catalina Mountain twilight mauve', figure_hex: '#E8DCC4', figure_name: 'tortilla-bone cream' },
+  { slug: 'adobe-sundown',       ground_hex: '#A8623A', ground_name: 'Spanish Colonial adobe terracotta',figure_hex: '#E8DCC4', figure_name: 'tortilla-bone cream' },
+  { slug: 'caliche-limestone',   ground_hex: '#B0A68C', ground_name: 'warm caliche-limestone taupe',     figure_hex: '#2A2520', figure_name: 'mesquite charcoal' },
+  { slug: 'saguaro-patina',      ground_hex: '#345E51', ground_name: 'aged saguaro skin green-grey',     figure_hex: '#E8DCC4', figure_name: 'tortilla-bone cream' },
+  { slug: 'cochineal-oxblood',   ground_hex: '#6B2A22', ground_name: 'cochineal deep red',               figure_hex: '#E8DCC4', figure_name: 'tortilla-bone cream' },
+  { slug: 'indigo-atelier',      ground_hex: '#1E3550', ground_name: 'natural indigo deep blue',         figure_hex: '#E8DCC4', figure_name: 'tortilla-bone cream' },
+  { slug: 'tortilla-bone',       ground_hex: '#E8DCC4', ground_name: 'chalky stucco cream',              figure_hex: '#2A2520', figure_name: 'mesquite charcoal' },
+  { slug: 'sonoran-espresso',    ground_hex: '#3A2820', ground_name: 'aged mesquite-barrel espresso',    figure_hex: '#E8DCC4', figure_name: 'tortilla-bone cream' },
+  { slug: 'prickly-pear-plum',   ground_hex: '#824050', ground_name: 'prickly-pear plum magenta',        figure_hex: '#E8DCC4', figure_name: 'tortilla-bone cream' },
+];
+
+// Build 50 (arrangement, colorway) pairs. Walk arrangements 2× through the
+// 10-colorway list, offset by 5 on the second pass so we never repeat a pair.
+const PAIRS = [];
+for (let pass = 0; pass < 2; pass++) {
+  for (let i = 0; i < ARRANGEMENTS.length; i++) {
+    const cwIdx = (i + pass * 5) % COLORWAYS.length;
+    PAIRS.push({ arrangement: ARRANGEMENTS[i], colorway: COLORWAYS[cwIdx] });
+    if (PAIRS.length >= 50) break;
+  }
+  if (PAIRS.length >= 50) break;
+}
+
+function buildPositive(a, cw) {
+  return (
+    `A single monumental ${a.species} rendered as a designer botanical mural. ` +
+    `${a.desc} ` +
+    `The ${a.species} fills 90 to 95 percent of the vertical canvas height, dead-center horizontally with even ` +
+    `${cw.ground_name} background on both sides. ` +
+    `Composition is one isolated specimen on a solid background — NOT a wallpaper repeat, NOT a tile, ` +
+    `NOT seamless, NOT tessellated, NOT a pattern. ` +
+    `Hand-painted gouache silhouette aesthetic. ` +
+    `Entire plant is rendered in the same single ink tone (${cw.figure_name}, hex ${cw.figure_hex}) ` +
+    `— minimal internal color variation, no gradients, no highlights. ` +
+    `Background is a solid uniform ${cw.ground_name} (hex ${cw.ground_hex}). ` +
+    `Tall narrow portrait orientation. Scaled to read at 11 feet tall on a wall. ` +
+    `Designer-grade archival illustration quality, reads as a luxury Spanish Colonial / Pueblo Revival mural, ` +
+    `Omni Tucson National Resort hospitality aesthetic.`
+  );
+}
+
+const NEGATIVE =
+  'No multiple plants spread across the canvas, no tile repeat, no seamless pattern, no tessellation, no wallpaper repeat. ' +
+  'No banana, no banana leaves, no banana pods, no palm, no palm fronds, no monstera, no philodendron, no tropical foliage. ' +
+  'No grapes, no birds, no butterflies. ' +
+  'No room mockup, no furniture, no people. ' +
+  'No watermark, no signature, no text, no logos, no border, no frame.';
+
+const PRINT_W_IN = 90;   // 7.5 ft
+const PRINT_H_IN = 132;  // 11 ft
+
+const renderEnv = {
+  ...process.env,
+  COMFY_WIDTH:     '832',
+  COMFY_HEIGHT:    '1216',
+  REPLICATE_WIDTH: '832',
+  REPLICATE_HEIGHT:'1216',
+  // Replicate — Mac1 ComfyUI is jammed by other crons, cost is negligible.
+  GEN_BACKEND: 'replicate',
+};
+
+console.log(`Pass 4 — Generating ${PAIRS.length} 11ft × 7.5ft single-cactus murals via Replicate SDXL\n`);
+
+let success = 0, fail = 0;
+const ids = [];
+const startedAt = Date.now();
+
+for (let i = 0; i < PAIRS.length; i++) {
+  const { arrangement: a, colorway: cw } = PAIRS[i];
+  const prompt = buildPositive(a, cw) + ' ' + NEGATIVE;
+  const tag = `${a.slug} · ${cw.slug}`;
+
+  process.stdout.write(`[${(i+1).toString().padStart(2,' ')}/${PAIRS.length}] ${tag} `);
+
+  const r = spawnSync('node', [
+    path.join(__dirname, 'generate_designs.js'),
+    '--n', '1', '--kind', 'mural',
+    '--width',  String(PRINT_W_IN),
+    '--height', String(PRINT_H_IN),
+    '--panels', '1',
+    '--category', 'cactus-11ft-mural',
+    '--prompts', prompt,
+  ], { cwd: ROOT, encoding: 'utf8', env: renderEnv });
+
+  const m = (r.stdout || '').match(/#(\d+)\s*·/);
+  if (m) {
+    success++;
+    ids.push(parseInt(m[1], 10));
+    process.stdout.write(`OK (id=${m[1]})\n`);
+  } else {
+    fail++;
+    const reason = ((r.stdout || '') + '\n' + (r.stderr || ''))
+      .split('\n').filter((l) => /✗|Error|FAILED|timed out|undefined/.test(l)).slice(-1).join(' | ')
+      || (r.stderr || r.stdout || '').slice(-160);
+    process.stdout.write(`FAIL ${reason}\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-cactus-36x132.js b/scripts/generate-cactus-36x132.js
new file mode 100644
index 0000000..10f4e23
--- /dev/null
+++ b/scripts/generate-cactus-36x132.js
@@ -0,0 +1,146 @@
+#!/usr/bin/env node
+// Pass 5 — 30 single-cactus accent-wall murals at 36" wide × 132" tall (3 ft × 11 ft).
+//
+// Brief (Steve, 2026-05-21): "36" wide x 11 feet tall" — much narrower than
+// Pass 4's 90×132 (7.5 × 11 ft). This is an accent-column mural; the wall
+// area is narrow, so the artwork has to be a SINGLE TALL SLENDER subject
+// that reads cleanly when the image is 3 ft wide on the wall.
+//
+// SDXL's narrowest standard portrait is 640×1536 (aspect 0.417), still wider
+// than the 36/132 = 0.273 target. We render at 640×1536 (safe + standard)
+// and store print spec 36×132; the wallco-ai mural display crops the sides
+// to the wall width. We restrict the species list to plants that look right
+// in a tall narrow column.
+//
+// Species filter (only tall-slender forms):
+//   saguaro bare column, saguaro single-arm tall, organ-pipe single stem,
+//   ocotillo fan, ocotillo in bloom, boojum, totem-pole, mexican-fence-post
+//   pillar, joshua tree tall young, queen-of-the-night, cardon slender
+//
+// Cost: 30 × ~$0.003 = ~$0.09 on Replicate SDXL.
+//
+// To run:  cd ~/Projects/wallco-ai && node scripts/generate-cactus-36x132.js
+
+const { spawnSync } = require('child_process');
+const path = require('path');
+const ROOT = path.join(__dirname, '..');
+
+// 11 tall-slender arrangements that suit a 36"-wide canvas
+const ARRANGEMENTS = [
+  { slug: 'saguaro-bare-column-tall',     species: 'saguaro',           desc: 'A single young mature saguaro with NO arms — pure vertical ribbed column, slightly tapered crown, very slender silhouette. Reads as a tall narrow pillar 30 feet high.' },
+  { slug: 'saguaro-single-arm-slender',   species: 'saguaro',           desc: 'A tall slender saguaro with ONE arm curving up the right side from 35% trunk height, reaching nearly the crown. Very narrow vertical silhouette.' },
+  { slug: 'organ-pipe-single-stem',       species: 'organ-pipe',        desc: 'A single very tall organ-pipe cactus stem rising straight up from the ground, ribbed, narrow, slightly thicker at base than at the top. ONE stem only, no neighbors.' },
+  { slug: 'organ-pipe-three-tight',       species: 'organ-pipe',        desc: 'THREE organ-pipe stems standing tightly bunched in a narrow vertical cluster — center stem tallest, two flanking stems slightly shorter, all parallel, ribbed.' },
+  { slug: 'ocotillo-narrow-fan',          species: 'ocotillo',          desc: 'An ocotillo with whip stems radiating UPWARD nearly vertically (not horizontally) — a tight narrow upward bouquet of 12-15 slender stems. Looks like a tall fountain frozen mid-spray.' },
+  { slug: 'ocotillo-bloom-vertical',      species: 'ocotillo',          desc: 'An ocotillo in full red bloom, stems pointing straight up in a narrow tight cluster, each tip crowned with a vivid red-orange flame-shaped flower against a clean ground.' },
+  { slug: 'boojum-tree-tall',             species: 'boojum tree',       desc: 'A tall boojum tree (Fouquieria columnaris) — single tapered column rising from a thick base to a narrow top, the trunk covered in stubby horizontal branches. Looks like a giant inverted carrot 25 feet tall.' },
+  { slug: 'totem-pole-cactus-narrow',     species: 'totem pole cactus', desc: 'A single tall totem-pole cactus (Lophocereus schottii) — narrow vertical column with smooth bumpy unsymmetrical surface, no spines, slightly twisted form. Very slender silhouette.' },
+  { slug: 'mexican-fence-post-pillar',    species: 'mexican fence post',desc: 'A SINGLE mexican-fence-post cactus pillar standing alone — tall slender ribbed column, all the same diameter top to bottom, slightly tapered crown. No neighbors.' },
+  { slug: 'joshua-tree-tall-narrow',      species: 'joshua tree',       desc: 'A tall young Joshua tree with a single straight trunk and only two short branches near the very top, each ending in a small spiky leaf rosette. Narrow vertical form (not the gnarled wide form).' },
+  { slug: 'queen-of-night-tall',          species: 'queen of the night',desc: 'A queen-of-the-night cactus with tall lanky thorny stems rising in a tight narrow upward cluster, ONE enormous white blooming flower at the top of the tallest stem.' },
+];
+
+const COLORWAYS = [
+  { slug: 'mesquite-on-bone',  ground_hex: '#E8DCC4', ground_name: 'warm chalky cream paper',          figure_hex: '#2A2520', figure_name: 'deep mesquite-charcoal ink' },
+  { slug: 'catalina-dusk',     ground_hex: '#7A4A52', ground_name: 'Catalina Mountain twilight mauve', figure_hex: '#E8DCC4', figure_name: 'tortilla-bone cream' },
+  { slug: 'adobe-sundown',     ground_hex: '#A8623A', ground_name: 'Spanish Colonial adobe terracotta',figure_hex: '#E8DCC4', figure_name: 'tortilla-bone cream' },
+  { slug: 'caliche-limestone', ground_hex: '#B0A68C', ground_name: 'warm caliche-limestone taupe',     figure_hex: '#2A2520', figure_name: 'mesquite charcoal' },
+  { slug: 'saguaro-patina',    ground_hex: '#345E51', ground_name: 'aged saguaro skin green-grey',     figure_hex: '#E8DCC4', figure_name: 'tortilla-bone cream' },
+  { slug: 'cochineal-oxblood', ground_hex: '#6B2A22', ground_name: 'cochineal deep red',               figure_hex: '#E8DCC4', figure_name: 'tortilla-bone cream' },
+  { slug: 'indigo-atelier',    ground_hex: '#1E3550', ground_name: 'natural indigo deep blue',         figure_hex: '#E8DCC4', figure_name: 'tortilla-bone cream' },
+  { slug: 'tortilla-bone',     ground_hex: '#E8DCC4', ground_name: 'chalky stucco cream',              figure_hex: '#2A2520', figure_name: 'mesquite charcoal' },
+  { slug: 'sonoran-espresso',  ground_hex: '#3A2820', ground_name: 'aged mesquite-barrel espresso',    figure_hex: '#E8DCC4', figure_name: 'tortilla-bone cream' },
+  { slug: 'prickly-pear-plum', ground_hex: '#824050', ground_name: 'prickly-pear plum magenta',        figure_hex: '#E8DCC4', figure_name: 'tortilla-bone cream' },
+];
+
+// 30 unique pairs from the 11×10 = 110-cell matrix.
+// Walk arrangements 3× with rotating colorway offset so we cover all 11 species
+// AND most colorways with no exact repeats.
+const PAIRS = [];
+for (let pass = 0; pass < 3; pass++) {
+  for (let i = 0; i < ARRANGEMENTS.length; i++) {
+    const cwIdx = (i + pass * 3) % COLORWAYS.length;
+    PAIRS.push({ arrangement: ARRANGEMENTS[i], colorway: COLORWAYS[cwIdx] });
+    if (PAIRS.length >= 30) break;
+  }
+  if (PAIRS.length >= 30) break;
+}
+
+function buildPositive(a, cw) {
+  return (
+    `A single monumental ${a.species} rendered as a designer botanical mural for a NARROW accent wall. ` +
+    `${a.desc} ` +
+    `The ${a.species} fills 95 percent of the vertical canvas height, dead-center, with just a sliver of ` +
+    `${cw.ground_name} background on each side. ` +
+    `EXTREMELY tall narrow portrait orientation, like a vertical banner or a narrow column. ` +
+    `Composition is one isolated specimen on a solid ${cw.ground_name} background — NOT a tile, ` +
+    `NOT seamless, NOT a wallpaper repeat. ` +
+    `Hand-painted gouache silhouette aesthetic. ` +
+    `Plant in single ink tone (${cw.figure_name}, hex ${cw.figure_hex}). ` +
+    `Background solid uniform ${cw.ground_name} (hex ${cw.ground_hex}). ` +
+    `Print spec: 36 inches wide × 132 inches tall (3 ft × 11 ft) accent-wall mural. ` +
+    `Designer-grade archival illustration quality, luxury Tucson hospitality aesthetic.`
+  );
+}
+
+const NEGATIVE =
+  'No multiple plants, no tile repeat, no seamless pattern, no tessellation, no wallpaper repeat. ' +
+  'No banana, no palm, no monstera, no tropical foliage. No grapes, no birds, no butterflies. ' +
+  'No room mockup, no furniture, no people. No watermark, no signature, no text, no logos, no border, no frame. ' +
+  'NOT square, NOT landscape, NOT wide. Must be tall narrow vertical portrait.';
+
+const PRINT_W_IN = 36;   // 3 ft
+const PRINT_H_IN = 132;  // 11 ft
+
+const renderEnv = {
+  ...process.env,
+  // SDXL narrowest standard portrait — 640×1536, aspect 0.417 (target 0.273).
+  // Wider than ideal but standard + safe. Wallco-ai mural display crops to wall.
+  COMFY_WIDTH:     '640',
+  COMFY_HEIGHT:    '1536',
+  REPLICATE_WIDTH: '640',
+  REPLICATE_HEIGHT:'1536',
+  GEN_BACKEND:     'replicate',
+};
+
+console.log(`Pass 5 — Generating ${PAIRS.length} narrow 36×132 in (3×11 ft) accent-wall cactus murals via Replicate SDXL 640×1536\n`);
+
+let success = 0, fail = 0;
+const ids = [];
+const startedAt = Date.now();
+
+for (let i = 0; i < PAIRS.length; i++) {
+  const { arrangement: a, colorway: cw } = PAIRS[i];
+  const prompt = buildPositive(a, cw) + ' ' + NEGATIVE;
+  const tag = `${a.slug} · ${cw.slug}`;
+
+  process.stdout.write(`[${(i+1).toString().padStart(2,' ')}/${PAIRS.length}] ${tag} `);
+
+  const r = spawnSync('node', [
+    path.join(__dirname, 'generate_designs.js'),
+    '--n', '1', '--kind', 'mural',
+    '--width',  String(PRINT_W_IN),
+    '--height', String(PRINT_H_IN),
+    '--panels', '1',
+    '--category', 'cactus-accent-mural',
+    '--prompts', prompt,
+  ], { cwd: ROOT, encoding: 'utf8', env: renderEnv });
+
+  const m = (r.stdout || '').match(/#(\d+)\s*·/);
+  if (m) {
+    success++;
+    ids.push(parseInt(m[1], 10));
+    process.stdout.write(`OK (id=${m[1]})\n`);
+  } else {
+    fail++;
+    const reason = ((r.stdout || '') + '\n' + (r.stderr || ''))
+      .split('\n').filter((l) => /✗|Error|FAILED|timed out|undefined/.test(l)).slice(-1).join(' | ')
+      || (r.stderr || r.stdout || '').slice(-160);
+    process.stdout.write(`FAIL ${reason}\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-cactus-pine-scenic.js b/scripts/generate-cactus-pine-scenic.js
new file mode 100644
index 0000000..dd3b9cf
--- /dev/null
+++ b/scripts/generate-cactus-pine-scenic.js
@@ -0,0 +1,221 @@
+#!/usr/bin/env node
+// Cactus × Monterey-Pine Scenic Murals — Steve directive 2026-05-21:
+//
+//   "make 20 panels that attach to left and right. as cactus and monterey
+//    Pine. Each panel 36" × 11ft tall like zuber, gracie, degournay.
+//    Create 6 of these"
+//
+// Interpretation:
+//   - 6 distinct scenic compositions ("Create 6 of these")
+//   - Each scenic is a single continuous landscape that spans 20 panels
+//     attached left-to-right (a 60ft × 11ft wraparound install)
+//   - Each panel = 36" wide × 132" tall
+//   - Full composite per scenic: 720" wide × 132" tall (60 ft × 11 ft)
+//   - Aesthetic anchor: Zuber & Cie, Gracie, de Gournay — the three living
+//     houses doing hand-painted scenic wallpaper murals at this scale.
+//     Flat 2-3 tone, single-layer hand-blocked silhouette, NO gradients.
+//   - Subject: cactus + Monterey pine combined (Sonoran-meets-California-coast)
+//
+// Render plan:
+//   - SDXL widest landscape = 1536×640 (2.4:1). True target is 720/132 =
+//     5.45:1 — we render at the widest standard and store the metadata at
+//     720×132. At print time the customer's wide-format printer upscales
+//     and slices into 20 panels of 36" each.
+//   - kind = 'mural_panel', width_in=720, height_in=132, panels=20,
+//     category = 'cactus-pine-scenic'
+//
+// Cost: 6 × ~$0.003 = ~$0.018. Wall: 1-2 min.
+
+const { spawnSync } = require('child_process');
+const path = require('path');
+const ROOT = path.join(__dirname, '..');
+
+// 6 distinct scenic compositions — Pass 2.
+// CACTUS-FIRST framing (Pass 1 had pine-first and SDXL ignored the cactus
+// completely). All descs lead with Sonoran cactus content; Monterey pine is
+// a secondary cluster on one edge.
+const SCENICS = [
+  {
+    slug: 'cactus-pine-coast-dawn',
+    name: 'Cactus & Pine Coast at Dawn',
+    palette_name: 'dawn-coral',
+    ground: '#E8C5A8',
+    ground_tone: 'Sonoran dawn-coral cream',
+    figure: '#3A2A1C',
+    figure_tone: 'deep cypress brown',
+    desc:
+      'A continuous 60-foot wide Sonoran cactus desert scenic reading left-to-right as ONE continuous landscape. ' +
+      'CACTUS DOMINATES the canvas across the LEFT 80% of the width: 10 mature saguaro cactus of varied heights and arm geometries ' +
+      '(some with 2 arms, some 4, one massive 6-arm elder near center-left), 6 ocotillo plants with whip stems radiating upward, ' +
+      'a continuous row of prickly-pear paddle clusters at the base across the foreground, scattered barrel cactus, and a stand of ' +
+      'organ-pipe cactus mid-canvas. ' +
+      'On the FAR RIGHT EDGE only (the rightmost 20% of the canvas): a small stand of three Monterey pines (Pinus radiata) silhouetted ' +
+      'on a granite coastal bluff, their windswept canopies leaning back toward the desert. ' +
+      'A thin band of Catalina-style mountains runs across the upper portion at horizon level (flat solid silhouette band, NO gradient). ' +
+      'The sky above the horizon is solid uniform dawn-coral ground tone — NO sunrise gradient, NO clouds. ' +
+      'Saguaros, ocotillo, prickly pear, agave, cholla are the heroes of this composition.',
+  },
+  {
+    slug: 'cactus-pine-twilight-blue',
+    name: 'Cactus & Pine at Twilight',
+    palette_name: 'twilight-indigo',
+    ground: '#1E3550',
+    ground_tone: 'Sonoran twilight indigo blue',
+    figure: '#F2EADB',
+    figure_tone: 'tortilla-bone cream',
+    desc:
+      'A continuous 60-foot wide moonlit Sonoran cactus scenic. CACTUS DOMINATES the LEFT 75%: 12 saguaro cactus at varying heights ' +
+      'including one massive 12-arm elder center-left, scattered ocotillo and barrel cactus and cholla, a row of agave americana with ' +
+      'bloom stalks at the base, organ-pipe cactus clusters. ' +
+      'FAR LEFT EDGE only (leftmost 15% of canvas): a small cluster of Monterey pines silhouetted as a coastal bluff. ' +
+      'A pale crescent moon sits in the upper-right third. Distant low mountains as a flat horizon band. ' +
+      'Sky: uniform indigo ground tone, NO gradient, NO stars. Hand-blocked silkscreen register. ' +
+      'Saguaros, ocotillo, agave, cholla, organ-pipe are the heroes of this composition.',
+  },
+  {
+    slug: 'cactus-pine-sage-meadow',
+    name: 'Sage Meadow with Pine & Cactus',
+    palette_name: 'sage-mint-spring',
+    ground: '#A8B79A',
+    ground_tone: 'sage-mint spring meadow',
+    figure: '#3A2A1C',
+    figure_tone: 'cypress charcoal',
+    desc:
+      'A continuous 60-foot wide Sonoran spring scenic. CACTUS DOMINATES the LEFT 75%: 10 saguaros at varying heights, 5 ocotillos in red ' +
+      'bloom rendered as cypress-charcoal silhouette, prickly-pear paddles with fruits clustered on top, agave americana with tall bloom ' +
+      'stalks reaching skyward, scattered cholla and barrel cactus. ' +
+      'FAR RIGHT 20%: a small stand of two Monterey pines on a low Carmel-style bluff. ' +
+      'A distant low mountain band runs across the upper portion. Sky: solid sage-mint ground tone, NO gradient. ' +
+      'Saguaros, ocotillo, prickly pear, agave are the heroes.',
+  },
+  {
+    slug: 'cactus-pine-burnt-umber-fall',
+    name: 'Cactus & Pine in Autumn',
+    palette_name: 'autumn-saddle',
+    ground: '#7A4A28',
+    ground_tone: 'burnt-umber autumn saddle',
+    figure: '#F2EADB',
+    figure_tone: 'tortilla-bone cream',
+    desc:
+      'A continuous 60-foot wide Sonoran autumn scenic. CACTUS DOMINATES the LEFT 80%: 12 saguaros of varying ages and heights, ' +
+      'mexican-fence-post cactus pillars in the mid-ground, agave americana with bloom stalks reaching the canvas top, a row of cholla ' +
+      'silhouettes across the base, scattered prickly-pear and ocotillo. ' +
+      'FAR RIGHT EDGE only (20% of canvas): a small Monterey pine windbreak — 3 pines with windswept canopies on a coastal bluff. ' +
+      'Mountain horizon band runs across the upper portion. Sky: solid burnt-umber ground tone, NO gradient. ' +
+      'Saguaros, agave, mexican fence post, cholla are the heroes.',
+  },
+  {
+    slug: 'cactus-pine-pacific-mist',
+    name: 'Pacific Mist with Joshua and Pine',
+    palette_name: 'winter-fog',
+    ground: '#9CA09B',
+    ground_tone: 'Pacific-mist pewter grey',
+    figure: '#3A2A1C',
+    figure_tone: 'dark cypress charcoal',
+    desc:
+      'A continuous 60-foot wide Mojave high-desert scenic. CACTUS / DESERT TREES DOMINATE the LEFT 80%: 8 Joshua trees of varying ages ' +
+      'and heights with gnarled branching trunks and leaf rosettes, scattered Mojave yucca with tall white bloom stalks, one boojum tree ' +
+      'silhouette mid-canvas, scattered Mojave saguaros (yes saguaros mixed in), prickly pear and cholla at the base. ' +
+      'FAR RIGHT EDGE only (rightmost 20%): 3 tall Monterey pines emerging from fog on a coastal bluff. ' +
+      'Distant snow-capped Sierra band at the horizon. Sky: solid Pacific-mist pewter ground tone, NO gradient. ' +
+      'Joshua trees, yucca, boojum, saguaro are the heroes.',
+  },
+  {
+    slug: 'cactus-pine-desert-coral',
+    name: 'Desert Coral Cactus and Pine',
+    palette_name: 'sun-baked-clay',
+    ground: '#B85440',
+    ground_tone: 'sun-baked-clay desert coral',
+    figure: '#F2EADB',
+    figure_tone: 'tortilla-bone cream',
+    desc:
+      'A continuous 60-foot wide Sonoran high-noon scenic in saturated coral palette. CACTUS DOMINATES the LEFT 80%: 14 saguaros of ' +
+      'varying heights and arm geometries (the absolute hero plant of this mural), scattered ocotillo with whip stems, barrel cactus, ' +
+      'cholla, prickly-pear, one tall organ-pipe cluster center-right. ' +
+      'FAR RIGHT EDGE only (rightmost 15-20%): a small stand of 2 Monterey pines on a Carmel-style coastal bluff. ' +
+      'Distant low rust-colored mountain band at horizon. Sky: solid desert-coral ground tone, NO gradient. ' +
+      'Saguaros, ocotillo, organ-pipe are the heroes.',
+  },
+];
+
+function composeScenicPrompt(s) {
+  // Cactus-first prompt — Pass 1 led with "Zuber/Gracie/de Gournay style" and
+  // SDXL went full Chinoiserie pine and dropped the cactus. Now cactus content
+  // leads the prompt; style anchor moves to the back as a styling note.
+  return (
+    `${s.desc} ` +
+    `Spec: 60 feet wide × 11 feet tall accent install — 20 panels × 36" wide × 132" tall, joined edge-to-edge L-to-R into one continuous scene. ` +
+    `Composition continues smoothly across the entire canvas so adjacent panel pairs seam visually. ` +
+    `2-TONE PALETTE only: ground = ${s.ground_tone} (${s.ground}). Figure (all silhouettes including saguaro trunks, cactus arms, pine canopies, mountains) = ${s.figure_tone} (${s.figure}). ` +
+    `Styling: hand-painted-silk scenic register in the manner of Zuber & Cie, Gracie, de Gournay — single-layer hand-blocked silkscreen aesthetic, ` +
+    `flat solid color regions only. Hard razor-sharp silhouette edges. Every figure region is one flat color tone. NO gradient, ` +
+    `NO sky gradient, NO atmospheric perspective, NO depth haze, NO half-tones, NO crosshatching, NO photoreal, NOT botanical plate, NOT engraving. ` +
+    `Reference: Sonoran hand-painted scenic wallcovering with desert cactus as the hero, hand-blocked register. ` +
+    `Settlement: NO bananas, NO palm fronds, NO monstera, NO grapes, NO birds, NO butterflies. Pine = Monterey pine only (Pinus radiata).`
+  );
+}
+
+const NEGATIVE =
+  'gradient, ombre, sunset gradient, sky gradient, sunrise gradient, dawn gradient, dusk gradient, ' +
+  'atmospheric depth, atmospheric perspective, haze, fog gradient, layered background fade, ' +
+  'photoreal, photorealism, photograph, painterly oil, oil painting, watercolor, ' +
+  'half-tone, halftone, crosshatching, dot screen, ben-day, fur texture, ' +
+  'three colors, four colors, multi-color, multicolor, rainbow, ' +
+  'palm, banana, monstera, philodendron, grapes, birds, butterflies, ' +
+  'room mockup, perspective, people, furniture, watermark, signature, text, logos, border, frame, ' +
+  'photo, raster, jpeg, photograph, depth-of-field, bokeh, glow, lens flare.';
+
+const PRINT_W_IN = 720;  // 60 ft
+const PRINT_H_IN = 132;  // 11 ft
+
+const renderEnv = {
+  ...process.env,
+  // SDXL widest standard landscape — 1536×640 (aspect 2.4:1). True target
+  // is 5.45:1; the printer upscales + crops at order time.
+  COMFY_WIDTH:     '1536',
+  COMFY_HEIGHT:    '640',
+  REPLICATE_WIDTH: '1536',
+  REPLICATE_HEIGHT:'640',
+  GEN_BACKEND:     'replicate',
+};
+
+console.log(`Cactus × Monterey-Pine Scenic — Generating ${SCENICS.length} 60ft × 11ft wraparound murals via Replicate SDXL 1536×640\n`);
+
+let success = 0, fail = 0;
+const ids = [];
+const startedAt = Date.now();
+
+for (let i = 0; i < SCENICS.length; i++) {
+  const s = SCENICS[i];
+  const prompt = composeScenicPrompt(s) + ' ' + NEGATIVE;
+
+  process.stdout.write(`[${(i+1).toString().padStart(2,' ')}/${SCENICS.length}] ${s.slug} `);
+
+  const r = spawnSync('node', [
+    path.join(__dirname, 'generate_designs.js'),
+    '--n', '1', '--kind', 'mural_panel',
+    '--width',  String(PRINT_W_IN),
+    '--height', String(PRINT_H_IN),
+    '--panels', '20',
+    '--category', 'cactus-pine-scenic',
+    '--prompts', prompt,
+  ], { cwd: ROOT, encoding: 'utf8', env: renderEnv });
+
+  const m = (r.stdout || '').match(/#(\d+)\s*·/);
+  if (m) {
+    success++;
+    ids.push(parseInt(m[1], 10));
+    process.stdout.write(`OK (id=${m[1]})\n`);
+  } else {
+    fail++;
+    const reason = ((r.stdout || '') + '\n' + (r.stderr || ''))
+      .split('\n').filter((l) => /✗|Error|FAILED|timed out|undefined/.test(l)).slice(-1).join(' | ')
+      || (r.stderr || r.stdout || '').slice(-160);
+    process.stdout.write(`FAIL ${reason}\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-tree-murals.js b/scripts/generate-tree-murals.js
new file mode 100644
index 0000000..89ac8ea
--- /dev/null
+++ b/scripts/generate-tree-murals.js
@@ -0,0 +1,167 @@
+#!/usr/bin/env node
+// Tree-Mural Pass 1 — 32 scenic tree murals at 144" × 132" (12 ft × 11 ft).
+//
+// Brief (Steve, 2026-05-21):
+//   "Keep building tree murals that start at 36" × 11 feet per panel and
+//    show on a full 11 feet tall by 12 feet wide at 150 DPI. Review the
+//    murals created of monterey with these specs."
+//
+// Model: identical to the Monterey track in night-builder.js (mural_panel
+// kind, full-room metadata, hand-painted-silk-scenic 2-tone aesthetic), but
+// 11 ft tall instead of 9 ft, with tall-tree subjects instead of Monterey
+// landmarks.
+//
+// Panel math:
+//   - Composite mural: 144" wide × 132" tall (12 ft × 11 ft)
+//   - At 150 DPI: 21,600 × 19,800 px composite (upscale from SDXL via
+//     Topaz/Real-ESRGAN at order time, per the Monterey workflow)
+//   - Sliced into 4 panels × 36" wide × 132" tall at print
+//
+// Aesthetic anchor — copied verbatim from composeMontereyMuralPrompt:
+//   2-tone hand-painted-silk scenic register, single-layer stencil silhouette,
+//   panoramic vertical panel, ground tone fills the sky area (no sky gradient),
+//   composition for FULL room.
+//
+// Settlement: scenic landscapes without bananas/grapes/birds/butterflies.
+// All subjects are tall TREES (saguaro counts as a tree per Sonoran convention,
+// per Steve's "cactus tall trees" framing). No Part B elements.
+//
+// Cost: 32 × ~$0.003 = ~$0.10 on Replicate SDXL.
+// Wall: ~5-6 min.
+
+const { spawnSync } = require('child_process');
+const path = require('path');
+const ROOT = path.join(__dirname, '..');
+
+// 8 tall-tree scenic subjects
+const TREE_SUBJECTS = [
+  {
+    slug: 'saguaro-forest-dusk',
+    desc: 'A Sonoran Desert saguaro forest at dusk — 5 to 8 mature saguaros at varying distances and heights silhouetted against a flat solid ground-tone sky, arms reaching in all directions, foreground rolling sand dunes with creosote bush silhouettes, distant low Catalina Mountains as a flat band at the horizon. Hand-painted scenic register.',
+  },
+  {
+    slug: 'joshua-tree-grove',
+    desc: 'A Mojave Desert Joshua tree grove — 6 Joshua trees of varying ages and heights standing in a sparse arrangement, gnarled trunks with multiple branching arms each topped with leaf rosettes, foreground brittlebush and yucca silhouettes, distant low desert mountains at the horizon. The tallest Joshua tree front-center reaches near the top of the canvas.',
+  },
+  {
+    slug: 'cypress-windbreak-coast',
+    desc: 'A Monterey-style coastal cypress windbreak — a row of 4 to 6 windswept Monterey cypress trees on a granite headland, dramatic canopies sculpted by ocean wind to lean inland, the Pacific opening behind in flat layered bands of solid ground tone (no gradient), foreground rocky bluff with succulent silhouettes.',
+  },
+  {
+    slug: 'coast-redwood-cathedral',
+    desc: 'A coast-redwood cathedral interior view — 5 to 8 towering redwood trunks rising as straight vertical columns into the canopy, the trunks varying in diameter (foreground thickest), foreground forest floor with sword-fern and oxalis silhouettes, the canopy a flat band of solid ground tone at the top. Cathedral of trees, vertical emphasis.',
+  },
+  {
+    slug: 'eucalyptus-allee',
+    desc: 'A California eucalyptus allée — two rows of tall slender eucalyptus trees flanking a dirt path that recedes into the distance using simple flat scale (no perspective gradient), peeling bark trunks rendered as silhouette with a thin highlight stripe per trunk, foreground roadside grass silhouettes, distant low coastal hills at the horizon.',
+  },
+  {
+    slug: 'oak-savanna-rolling-hills',
+    desc: 'A California oak savanna — 5 to 8 mature valley oaks scattered across rolling golden hills, each oak with a wide spreading canopy and gnarled trunk, the hills rendered as 3 to 4 flat overlapping bands of ground tone (foreground darkest, distant lightest), no sky gradient, a band of distant Sierra foothills at the horizon.',
+  },
+  {
+    slug: 'cactus-forest-mixed',
+    desc: 'A mixed Sonoran cactus forest — 4 saguaros, 3 organ-pipe clusters, and 5 ocotillo plants scattered across the canvas at varying scales, foreground prickly-pear and barrel-cactus silhouettes, distant low desert mesa rendered as a flat band at the horizon. Tall verticals throughout.',
+  },
+  {
+    slug: 'aspen-grove-vertical',
+    desc: 'A Colorado aspen grove — 8 to 12 vertical white aspen trunks rising as straight slender columns, each trunk with a thin highlight stripe to read as bark, foreground forest floor with grass silhouettes, the leafy canopy rendered as a flat band of ground tone at the top of the canvas. Strong vertical rhythm.',
+  },
+];
+
+// Same Monterey 4-season colorway palette + 1 extra desert-coral
+const 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' },
+  { season: 'all',    name: 'Desert Coral',   ground: '#B85440', figure: '#F2EADB', tone: 'sun-baked-clay' },
+];
+
+// 8 subjects × 4 colorway picks each = 32 unique pairs, non-repeating.
+const PAIRS = [];
+for (let i = 0; i < TREE_SUBJECTS.length; i++) {
+  for (let j = 0; j < 4; j++) {
+    const cw = COLORWAYS[(i + j) % COLORWAYS.length];
+    PAIRS.push({ subject: TREE_SUBJECTS[i], colorway: cw });
+  }
+}
+
+function composeTreeMuralPrompt(s, cw) {
+  return (
+    `Tall-tree scenic mural — ${s.desc} ` +
+    `${cw.season.charAt(0).toUpperCase() + cw.season.slice(1)} colorway: ${cw.name}. ` +
+    `Ground (sky and background fill): solid ${cw.tone} (${cw.ground}). ` +
+    `Figure (silhouette/trunks/canopy): ${cw.figure} single-ink. ` +
+    `Composition: panoramic FULL-ROOM mural panel, motifs anchored center-bottom, sky/atmosphere in the upper half of the canvas STAYS SOLID GROUND TONE (NOT a sky gradient, NOT clouds). Generous breathing room. ` +
+    `Spec: 12 feet wide × 11 feet tall accent mural for an interior wall. Composer should fill the full wall — at print time the image is sliced into 4 panels × 36 inches wide × 132 inches tall (11 ft). Generate at room scale, NOT panel scale. ` +
+    `2-COLOR PALETTE — strictly two tones: ground + figure. NO highlight color, NO shadow color. A thin paler highlight stripe per trunk is allowed (for bark texture) as long as it stays the figure color at 50-70% opacity register. ` +
+    `Reference: hand-painted-silk scenic panel, hand-painted silk register, Zuber & Cie hand-blocked scenic, NOT Haeckel, NOT Audubon, NOT botanical-plate. Single-layer stencil silhouette only. Hard clean razor-sharp edges. No half-tones, no gradient, no watercolor, no crosshatch. ` +
+    `Settlement compliance — no bananas, no banana leaves, no banana pods, no palm fronds, no monstera, no philodendron, no grapes, no birds, no butterflies. Tall trees only.`
+  );
+}
+
+const NEGATIVE =
+  'gradient, ombre, fade, faded, ghosted, ghost layer, atmospheric perspective, depth, ' +
+  'sky gradient, sunset gradient, cloud gradient, layered background, halftone, half-tone, ' +
+  'chromatic gradient, watercolor wash, airbrush, crosshatching, ben-day dots, dot screen, ' +
+  'fur texture, photoreal, Haeckel, Audubon, botanical plate, engraving, woodcut, ' +
+  'palm, banana, monstera, philodendron, grapes, birds, butterflies, ' +
+  'room mockup, perspective, people, furniture, watermark, signature, text, logos, border, frame.';
+
+const PRINT_W_IN = 144;  // 12 ft
+const PRINT_H_IN = 132;  // 11 ft
+
+const renderEnv = {
+  ...process.env,
+  // Square SDXL render — matches the Monterey track which actually ships
+  // 1024×1024 PNGs despite metadata saying 1536×1152. Aspect 144/132 = 1.091
+  // is close enough to square (1.0) that a square render upscales cleanly.
+  COMFY_WIDTH:     '1024',
+  COMFY_HEIGHT:    '1024',
+  REPLICATE_WIDTH: '1024',
+  REPLICATE_HEIGHT:'1024',
+  GEN_BACKEND:     'replicate',
+};
+
+console.log(`Tree-Mural Pass 1 — Generating ${PAIRS.length} 12×11 ft tree murals via Replicate SDXL (1024×1024)\n`);
+
+let success = 0, fail = 0;
+const ids = [];
+const startedAt = Date.now();
+
+for (let i = 0; i < PAIRS.length; i++) {
+  const { subject: s, colorway: cw } = PAIRS[i];
+  const prompt = composeTreeMuralPrompt(s, cw) + ' ' + NEGATIVE;
+  const tag = `${s.slug} · ${cw.season}`;
+
+  process.stdout.write(`[${(i+1).toString().padStart(2,' ')}/${PAIRS.length}] ${tag} `);
+
+  const r = spawnSync('node', [
+    path.join(__dirname, 'generate_designs.js'),
+    '--n', '1', '--kind', 'mural_panel',
+    '--width',  String(PRINT_W_IN),
+    '--height', String(PRINT_H_IN),
+    '--panels', '4',  // 4 panels × 36" = 144" wide
+    '--category', 'tree-mural',
+    '--prompts', prompt,
+  ], { cwd: ROOT, encoding: 'utf8', env: renderEnv });
+
+  const m = (r.stdout || '').match(/#(\d+)\s*·/);
+  if (m) {
+    success++;
+    ids.push(parseInt(m[1], 10));
+    process.stdout.write(`OK (id=${m[1]})\n`);
+  } else {
+    fail++;
+    const reason = ((r.stdout || '') + '\n' + (r.stderr || ''))
+      .split('\n').filter((l) => /✗|Error|FAILED|timed out|undefined/.test(l)).slice(-1).join(' | ')
+      || (r.stderr || r.stdout || '').slice(-160);
+    process.stdout.write(`FAIL ${reason}\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/server.js b/server.js
index 49ce562..2e8deea 100644
--- a/server.js
+++ b/server.js
@@ -5058,9 +5058,14 @@ ${_isAdmin ? `
     max-height: 300px;
     pointer-events: auto;
   }
-  /* Pattern # pill — visible only on hover, top-left of the image. */
+  /* Pattern # pill — visible only on hover. Moved to TOP-RIGHT to stop
+     blocking the bulk-select checkbox in the upper-left (Steve, 2026-05-21).
+     The decision-badge also lives at top:right:8px but is display:none
+     unless data-decision is set; when both are visible the badge sits
+     below the pill via the chained .review-card[data-decision] selector
+     in the same stylesheet block. */
   .design-card.review-card .pattern-num {
-    position: absolute; top: 8px; left: 8px; z-index: 3;
+    position: absolute; top: 8px; right: 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;
@@ -5069,6 +5074,11 @@ ${_isAdmin ? `
     transition: opacity .18s ease, transform .18s ease;
     pointer-events: none;
   }
+  /* When a decision is set, push the decision-badge down so the pattern-num
+     stays at the upper-right corner unobscured. */
+  .design-card.review-card[data-decision]:not([data-decision=""]) .decision-badge {
+    top: 36px;
+  }
   .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 */
@@ -7671,6 +7681,25 @@ ${htmlHeader('/designs')}
               };
               probe.src = ${JSON.stringify(design.image_url || '')};
             })();
+            // Read current wall / roll / repeat from the live controls so the
+            // tile preview reflects the customer's actual install dimensions
+            // — not abstract 2x/4x/8x density. Steve directive 2026-05-21:
+            // "we must use client width, height of wall plus the width,
+            //  repeat, length of roll in not sold per yard with all users."
+            function readWallSpec() {
+              var wallWFt = parseFloat((document.getElementById('wc-w') || {}).value);
+              var wallHFt = parseFloat((document.getElementById('wc-h') || {}).value);
+              if (!isFinite(wallWFt) || wallWFt <= 0) wallWFt = 10;  // sane default
+              if (!isFinite(wallHFt) || wallHFt <= 0) wallHFt = 9;
+              var rollW = 24;
+              try { var s = JSON.parse(localStorage.getItem('wallco.material') || 'null'); if (s && parseInt(s.w,10)) rollW = parseInt(s.w,10); } catch(e){}
+              var repeatIn = 24;
+              try {
+                var sl = document.getElementById('scale-slider');
+                if (sl) { var v = parseInt(sl.value, 10); if (isFinite(v) && v > 0) repeatIn = v; }
+              } catch(e){}
+              return { wallWFt: wallWFt, wallHFt: wallHFt, wallWIn: wallWFt * 12, wallHIn: wallHFt * 12, rollW: rollW, repeatIn: repeatIn };
+            }
             function apply() {
               const { count, label } = LEVELS[level];
               dots.forEach((d,i) => d.classList.toggle('active', i===level));
@@ -7685,8 +7714,20 @@ ${htmlHeader('/designs')}
                 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) + '%';
+                // Real-scale tiling: ONE tile in the viewport represents one
+                // roll-width by one repeat-height of the customer's wall.
+                // tile_w_pct = (roll_width_in / wall_width_in)  × zoom
+                // tile_h_pct = (repeat_in    / wall_height_in) × zoom
+                // The 'count' level value (2/4/8/16) becomes a ZOOM factor so
+                // users can step closer/farther — but the SHAPE of each tile
+                // (its aspect ratio + position in the wall grid) is always
+                // anchored to the real wall dimensions.
+                var sp = readWallSpec();
+                var zoom = count / 4;  // count=2 → 0.5× (zoomed out), 4 → 1×, 8 → 2×, 16 → 4×
+                var tileWPct = Math.max(2, (sp.rollW    / sp.wallWIn) * 100 * zoom);
+                var tileHPct = Math.max(2, (sp.repeatIn / sp.wallHIn) * 100 * zoom);
+                vp.style.backgroundRepeat   = 'repeat';
+                vp.style.backgroundSize     = tileWPct.toFixed(2) + '% ' + tileHPct.toFixed(2) + '%';
                 vp.style.backgroundPosition = '0% 0%';
               }
               // Ghosting button (admin-only) — hide in MURAL mode. Ghosting is
@@ -7704,18 +7745,39 @@ ${htmlHeader('/designs')}
               // slider in MURAL mode is misleading. Hide the whole control row.
               const scaleControls = document.getElementById('scale-controls');
               if (scaleControls) scaleControls.style.display = (level === 0) ? 'none' : 'flex';
+              // Spec rows + sold-by + hint — Mural mode hides repeat/roll
+              // rows and shows panel-size, switches "Per roll" → "Per panel".
+              // Wallpaper mode does the inverse.
+              const isMural = (level === 0);
+              const repeatRows = document.querySelectorAll('.repeat-only');
+              const muralRows  = document.querySelectorAll('.mural-only');
+              repeatRows.forEach(r => { r.style.display = isMural ? 'none' : ''; });
+              muralRows.forEach(r  => { r.style.display = isMural ? '' : 'none'; });
+              const soldBy = document.getElementById('spec-soldby');
+              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)';
             }
             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.
+            // Pattern-scale slider adjusts the vertical repeat in WALLPAPER
+            // modes (levels 1-4). In MURAL mode (level 0) the slider is
+            // hidden anyway, so this listener is a no-op there.
             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();
+              if (level === 0) return;
+              apply();  // re-render with new repeat against same wall spec
+            });
+            // Live-update the tile preview when the wall calculator inputs
+            // change or the user picks a different roll width. Same trigger
+            // for everything: re-run apply() against the new wall spec.
+            ['wc-w','wc-h'].forEach(function(id){
+              var el = document.getElementById(id);
+              if (!el) return;
+              el.addEventListener('input',  function(){ if (level !== 0) apply(); });
+              el.addEventListener('change', function(){ if (level !== 0) apply(); });
+            });
+            document.querySelectorAll('.w-tab').forEach(function(t){
+              t.addEventListener('click', function(){ if (level !== 0) setTimeout(apply, 0); });
             });
 
             // ── Mural mode interactions: drag-to-pan, wheel-to-zoom, dblclick reset.
@@ -7782,7 +7844,7 @@ ${htmlHeader('/designs')}
           </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 id="scale-width-hint">${design.kind === 'mural_panel' ? '36" panel × 11 ft, sold per panel' : '24" wide × 27 ft, sold per roll'}</span>
             <span>Larger repeat →</span>
           </div>
 
@@ -8193,7 +8255,12 @@ ${htmlHeader('/designs')}
             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';
+            // Mode-aware hint: per-roll (wallpaper) vs per-panel (mural).
+            // Falls back to per-roll if mural-stepper isn't on the page.
+            var isMural = !!(document.querySelector('.mural-dot.active') && document.querySelector('.mural-dot.active').dataset.level === '0');
+            if (hint) hint.textContent = isMural
+              ? (rollW + '" panel × 11 ft, sold per panel')
+              : (rollW + '" wide × 27 ft, sold per roll');
             paint();
           }
 
@@ -8401,13 +8468,18 @@ ${htmlHeader('/designs')}
         <div id="material-readout" style="margin-top:10px;font:12px var(--sans);color:var(--ink-soft)"></div>
       </div>
 
-      <div class="detail-spec-panel">
+      <!-- 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="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">Sold by</span><span class="spec-val" id="spec-soldby">Per roll (27&nbsp;ft)</span></div>
+        <div class="spec-row repeat-only" id="spec-repeat-row"><span class="spec-key">Repeat</span><span class="spec-val" id="spec-repeat">24" (adjust on left)</span></div>
+        <div class="spec-row repeat-only" id="spec-roll-length-row"><span class="spec-key">Roll length</span><span class="spec-val" id="spec-roll-length">27&nbsp;ft (≈9&nbsp;yd)</span></div>
+        <div class="spec-row mural-only" id="spec-panel-row" style="display:none"><span class="spec-key">Panel size</span><span class="spec-val" id="spec-panel">36" × 11&nbsp;ft (132")</span></div>
+        <div class="spec-row"><span class="spec-key">Kind</span><span class="spec-val">${design.kind === 'seamless_tile' ? 'Seamless tile' : (design.kind === 'mural_panel' ? 'Mural panel' : 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>
@@ -8580,29 +8652,71 @@ ${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();
-          // Read user's chosen panel width (24/27/36) from the existing material picker if present.
+          // 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.
           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 rollWidthIn = savedWidth;
           var wallSqFt = w * h;
-          var panelCoverageFt2 = (panelWidthInches/12) * panelLengthFt; // each panel covers this many sq-ft
-          // Match-waste factor: Curated-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, Curated-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>';
+          var wallWidthIn  = w * 12;
+          var wallHeightFt = h;
+          var box$ = box; var html = '';
+
+          if (isMural) {
+            // ── MURAL math — panels are 36" wide × 132" (11 ft) tall by spec.
+            // Customer chose roll width via the picker (24/36/52); we treat
+            // that as the PANEL width. Each panel is 11 ft tall full-bleed.
+            var panelHFt = 11;
+            var panelWidthIn = rollWidthIn;
+            var panelsAcross = Math.ceil(wallWidthIn / panelWidthIn);
+            var rowsTall     = Math.ceil(wallHeightFt / panelHFt);
+            var panels       = panelsAcross * rowsTall;
+            var totalLfFt    = +((panels * panelHFt)).toFixed(1);
+            // 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);
+            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>' +
+              '</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>';
+          } else {
+            // ── WALLPAPER WITH REPEAT math.
+            // Per-roll spec: 27 ft length × roll-width (chosen 24/36/52).
+            // Match-waste = max(15%, repeat/2 per drop). Repeat read from
+            // #scale-readout when present (e.g. "24\" repeat"), default 24.
+            var repeatIn = 24;
+            try {
+              var rt = document.getElementById('scale-readout');
+              if (rt) {
+                var mm = (rt.textContent || '').match(/(\d+)/);
+                if (mm) repeatIn = parseInt(mm[1], 10);
+              }
+            } catch(e){}
+            var rollLengthFt = 27;       // typical residential roll
+            var dropLengthFt = h + (repeatIn / 12); // wall height + 1 repeat waste per drop
+            var dropsPerRoll = Math.floor(rollLengthFt / dropLengthFt);
+            var dropsNeeded  = Math.ceil(wallWidthIn / rollWidthIn);
+            var rolls = (dropsPerRoll > 0) ? Math.ceil(dropsNeeded / dropsPerRoll) : Math.ceil(dropsNeeded);
+            var totalLinearFt = rolls * rollLengthFt;
+            // 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);
+            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>' +
+              '</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>';
+          }
+          box$.style.display='block';
+          box$.innerHTML = html;
         }
         btn.addEventListener('click', compute);
         wEl.addEventListener('keyup', function(e){ if (e.key==='Enter') compute(); });

← b4d4d16 feat(cactus): 10 11ft × 7.5ft single-cactus murals + hide Pa  ·  back to Wallco Ai  ·  feat(scenic+ux): 6 Gracie-language colorways + 3 reviewer-re 5782b51 →