[object Object]

← back to Wallco Ai

theme-variants: admin theme stepper (slug-based) + drag-reorder sections, all 17 + template

957c89cf89c0b133a300eb45d44fd298789465fb · 2026-06-01 17:24:52 -0700 · Steve Abrams

Files touched

Diff

commit 957c89cf89c0b133a300eb45d44fd298789465fb
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jun 1 17:24:52 2026 -0700

    theme-variants: admin theme stepper (slug-based) + drag-reorder sections, all 17 + template
---
 public/theme-variants/theme-section-rearrange.js | 234 +++++++++++++++++++++++
 public/theme-variants/theme-variant-nav.js       | 130 +++++++++++++
 public/theme-variants/v1-compact.html            |   2 +
 public/theme-variants/v10-stack.html             |   2 +
 public/theme-variants/v11-best.html              |   2 +
 public/theme-variants/v12-roomsplit.html         |   6 +-
 public/theme-variants/v13-roomdrawer.html        |   6 +-
 public/theme-variants/v14-roomzine.html          |   6 +-
 public/theme-variants/v15-roomsticky.html        |   6 +-
 public/theme-variants/v16-roommin.html           |   6 +-
 public/theme-variants/v17-roomshow.html          |   6 +-
 public/theme-variants/v2-bento.html              |   2 +
 public/theme-variants/v3-tabs.html               |   2 +
 public/theme-variants/v4-hero.html               |   2 +
 public/theme-variants/v5-vertical.html           |   2 +
 public/theme-variants/v6-drawer.html             |   2 +
 public/theme-variants/v7-editorial.html          |   2 +
 public/theme-variants/v8-spec.html               |   2 +
 public/theme-variants/v9-config.html             |   2 +
 scripts/room-left-template.html                  |   2 +
 20 files changed, 412 insertions(+), 12 deletions(-)

diff --git a/public/theme-variants/theme-section-rearrange.js b/public/theme-variants/theme-section-rearrange.js
new file mode 100644
index 0000000..6c23bd2
--- /dev/null
+++ b/public/theme-variants/theme-section-rearrange.js
@@ -0,0 +1,234 @@
+/* theme-section-rearrange.js — admin-only drag-to-reorder for PDP sections.
+ *
+ * Drop-in (ABSOLUTE path):
+ *   <script src="/theme-variants/theme-section-rearrange.js" defer></script>
+ * Admin gate: active ONLY when localStorage.dwAdmin === '1' (set via ?admin=1).
+ *
+ * Makes each PDP section moveable + rearrangeable: any column that holds ≥2
+ * recognised sections becomes a drag zone; grab a section's ⠿ handle and drop it
+ * to reorder. Order persists per THEME (slug) in localStorage and is re-applied
+ * after the PDP re-renders (#root rebuilds, e.g. the v11 Wallpaper⇄Mural toggle).
+ * A small "Layout" pill (bottom-left) resets the order. Idempotent · zero deps.
+ */
+(function () {
+  'use strict';
+  if (window.__themeSectionRearrangeLoaded) return;
+  window.__themeSectionRearrangeLoaded = true;
+
+  // admin gate
+  try {
+    var q = new URLSearchParams(location.search);
+    if (q.get('admin') === '1') localStorage.setItem('dwAdmin', '1');
+    if (q.get('admin') === '0') localStorage.removeItem('dwAdmin');
+    if (localStorage.getItem('dwAdmin') !== '1') return;
+  } catch (e) { return; }
+
+  // theme slug (for the persistence key) — layout is per-theme, not per-design
+  var rm = location.pathname.match(/^\/design\/[^/]+\/([^/?#]+)/);
+  var SLUG = rm ? rm[1] : location.pathname.split('/').pop().replace(/\.html?$/i, '').replace(/^v\d+-/, '') || 'pdp';
+  var KEY = 'wallco:secorder:' + SLUG;
+
+  // recurring section classnames across the 17 variant families
+  var SECTION_SEL = [
+    '.cta-row', '.preview-row', '.preview', '.tab-bar', '.tabs', '.fmt-tabs',
+    '.calc', '.calculator', '.specs', '.spec-table', '.spectab',
+    '.room', '.room-main', '.room-thumbs', '.room-cap', '.roomprev',
+    '.atscale', '.info', '.meta', '.palette', '.color-dots',
+  ].join(',');
+
+  var DRAG_CLASS = 'tsr-dragging';
+  var observer = null, applying = false, dragEl = null;
+
+  function load() { try { return JSON.parse(localStorage.getItem(KEY)) || {}; } catch (e) { return {}; } }
+  function save(o) { try { localStorage.setItem(KEY, JSON.stringify(o)); } catch (e) {} }
+
+  // stable-ish key for an element from id or sorted class tokens (+ dedupe index)
+  function rawKey(el) {
+    if (el.id) return '#' + el.id;
+    var c = el.getAttribute && el.getAttribute('class') || '';
+    c = String(c).replace(/\btsr-[\w-]+/g, '').trim().split(/\s+/).filter(Boolean).sort().join('.');
+    return c ? '.' + c : el.tagName.toLowerCase();
+  }
+  function keysFor(children) {
+    var seen = {}, out = [];
+    for (var i = 0; i < children.length; i++) {
+      var k = rawKey(children[i]);
+      seen[k] = (seen[k] || 0) + 1;
+      out.push(seen[k] > 1 ? k + '~' + seen[k] : k);
+    }
+    return out;
+  }
+
+  // a zone = element with >=2 DIRECT children matching SECTION_SEL
+  function findZones() {
+    var matches = Array.prototype.slice.call(document.querySelectorAll('#root ' + SECTION_SEL.replace(/,/g, ', #root ')));
+    var byParent = new Map();
+    matches.forEach(function (el) {
+      var p = el.parentElement; if (!p) return;
+      if (!byParent.has(p)) byParent.set(p, 0);
+      byParent.set(p, byParent.get(p) + 1);
+    });
+    var zones = [];
+    byParent.forEach(function (count, p) {
+      // count only DIRECT-child matches
+      var direct = 0, kids = p.children;
+      for (var i = 0; i < kids.length; i++) if (kids[i].matches && kids[i].matches(SECTION_SEL)) direct++;
+      if (direct >= 2) zones.push(p);
+    });
+    return zones;
+  }
+
+  function zoneKey(zone, i) { return rawKey(zone) + '#z' + i; }
+
+  function makeHandle() {
+    var h = document.createElement('div');
+    h.className = 'tsr-handle';
+    h.setAttribute('draggable', 'true');
+    h.setAttribute('title', 'Drag to reorder section');
+    h.innerHTML = '⠿';
+    return h;
+  }
+
+  function applyOrder(zone, zk) {
+    var store = load();
+    var order = store[zk];
+    if (!order || !order.length) return;
+    var kids = Array.prototype.slice.call(zone.children);
+    var keyed = kids.map(function (el, i) { return { el: el, k: keysForOne(kids, i) }; });
+    keyed.sort(function (a, b) {
+      var ia = order.indexOf(a.k), ib = order.indexOf(b.k);
+      if (ia === -1 && ib === -1) return 0;
+      if (ia === -1) return 1; if (ib === -1) return -1;
+      return ia - ib;
+    });
+    keyed.forEach(function (o) { zone.appendChild(o.el); });
+  }
+  // key for child index i within its sibling set (matches keysFor dedupe)
+  function keysForOne(children, i) { return keysFor(children)[i]; }
+
+  function persist(zone, zk) {
+    var store = load();
+    store[zk] = keysFor(Array.prototype.slice.call(zone.children));
+    save(store);
+  }
+
+  function wireZone(zone, i) {
+    var zk = zoneKey(zone, i);
+    if (zone.__tsrWired === zk) { applyOrder(zone, zk); return; }
+    zone.__tsrWired = zk;
+    zone.classList.add('tsr-zone');
+
+    // apply saved order first
+    applyOrder(zone, zk);
+
+    // add handles to each direct child section
+    Array.prototype.slice.call(zone.children).forEach(function (sec) {
+      if (sec.classList.contains('tsr-handle')) return;
+      if (sec.querySelector && sec.querySelector(':scope > .tsr-handle')) return;
+      var cs = getComputedStyle(sec);
+      if (cs.position === 'static') sec.style.position = 'relative';
+      sec.classList.add('tsr-sec');
+      var h = makeHandle();
+      sec.appendChild(h);
+      h.addEventListener('dragstart', function (e) {
+        dragEl = sec; sec.classList.add(DRAG_CLASS);
+        try { e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', ''); } catch (x) {}
+      });
+      h.addEventListener('dragend', function () {
+        if (dragEl) dragEl.classList.remove(DRAG_CLASS);
+        dragEl = null;
+        persist(zone, zk);
+      });
+    });
+
+    zone.addEventListener('dragover', function (e) {
+      if (!dragEl || dragEl.parentElement !== zone) return;
+      e.preventDefault();
+      var after = siblingAfter(zone, e.clientY);
+      if (after == null) zone.appendChild(dragEl);
+      else zone.insertBefore(dragEl, after);
+    });
+  }
+
+  function siblingAfter(zone, y) {
+    var els = Array.prototype.slice.call(zone.children).filter(function (c) {
+      return c !== dragEl && !c.classList.contains('tsr-handle');
+    });
+    for (var i = 0; i < els.length; i++) {
+      var r = els[i].getBoundingClientRect();
+      if (y < r.top + r.height / 2) return els[i];
+    }
+    return null;
+  }
+
+  function setup() {
+    if (applying) return;
+    applying = true;
+    if (observer) observer.disconnect();
+    try {
+      var zones = findZones();
+      zones.forEach(wireZone);
+    } finally {
+      applying = false;
+      if (observer) observer.observe(document.getElementById('root') || document.body, { childList: true, subtree: true });
+    }
+  }
+
+  // ---- styles + reset pill ----
+  function styles() {
+    var css = document.createElement('style');
+    css.textContent = [
+      '.tsr-handle{position:absolute;top:4px;left:4px;z-index:50;width:20px;height:20px;display:flex;align-items:center;',
+        'justify-content:center;font-size:13px;line-height:1;color:#fff;background:rgba(20,20,20,.55);border:1px solid rgba(255,255,255,.25);',
+        'border-radius:5px;cursor:grab;opacity:0;transition:opacity .15s;user-select:none}',
+      '.tsr-sec:hover > .tsr-handle{opacity:.85}',
+      '.tsr-handle:hover{opacity:1!important;background:rgba(20,20,20,.8)}',
+      '.tsr-handle:active{cursor:grabbing}',
+      '.tsr-zone > .tsr-sec{outline:1px dashed transparent;outline-offset:2px;transition:outline-color .15s}',
+      '.tsr-zone:hover > .tsr-sec:hover{outline-color:rgba(120,120,120,.4)}',
+      '.' + DRAG_CLASS + '{opacity:.55;outline:2px dashed rgba(63,208,154,.9)!important}',
+      '#tsr-pill{position:fixed;left:16px;bottom:14px;z-index:99998;display:flex;align-items:center;gap:8px;',
+        'background:rgba(12,12,12,.82);backdrop-filter:blur(8px);border:1px solid rgba(245,241,232,.18);border-radius:999px;',
+        'padding:5px 12px;font:500 11px/1 -apple-system,system-ui,sans-serif;color:#f5f1e8;box-shadow:0 6px 20px rgba(0,0,0,.4)}',
+      '#tsr-pill .tsr-grip{opacity:.8}',
+      '#tsr-pill button{background:transparent;border:0;color:#f5f1e8;font-size:12px;cursor:pointer;opacity:.7;padding:0}',
+      '#tsr-pill button:hover{opacity:1}',
+    ].join('');
+    document.head.appendChild(css);
+  }
+
+  function pill() {
+    var el = document.createElement('div');
+    el.id = 'tsr-pill';
+    el.innerHTML = '<span class="tsr-grip">⠿</span><span>Drag sections to reorder</span><button id="tsr-reset" title="Reset this theme\'s layout">↺ reset</button>';
+    document.body.appendChild(el);
+    document.getElementById('tsr-reset').addEventListener('click', function () {
+      var store = load();
+      Object.keys(store).forEach(function (k) { delete store[k]; });
+      save(store);
+      location.reload();
+    });
+  }
+
+  function waitForRender() {
+    var root = document.getElementById('root');
+    function ready() {
+      var t = (document.body.innerText || '');
+      return root && root.children.length > 0 && !/Loading design/i.test(t);
+    }
+    if (ready()) return start();
+    var mo = new MutationObserver(function () { if (ready()) { mo.disconnect(); start(); } });
+    mo.observe(root || document.body, { childList: true, subtree: true });
+    setTimeout(function () { if (ready()) { try { mo.disconnect(); } catch (e) {} start(); } }, 4000);
+  }
+
+  function start() {
+    styles();
+    pill();
+    observer = new MutationObserver(function () { if (!applying) { clearTimeout(window.__tsrT); window.__tsrT = setTimeout(setup, 120); } });
+    setup();
+  }
+
+  if (document.body) waitForRender();
+  else document.addEventListener('DOMContentLoaded', waitForRender);
+})();
diff --git a/public/theme-variants/theme-variant-nav.js b/public/theme-variants/theme-variant-nav.js
new file mode 100644
index 0000000..45f032b
--- /dev/null
+++ b/public/theme-variants/theme-variant-nav.js
@@ -0,0 +1,130 @@
+/* theme-variant-nav.js — admin-only ‹ prev / next › theme stepper, upper-right.
+ *
+ * Drop-in (ABSOLUTE path — variants are served at /design/:id/:slug):
+ *   <script src="/theme-variants/theme-variant-nav.js" defer></script>
+ * Admin gate: shows ONLY when localStorage.dwAdmin === '1' (set via ?admin=1).
+ * Steps through all 17 PDP theme variants while preserving the design id + query,
+ * so you stay on the same design. Marks the LIVE theme (from /data/pdp-theme.json).
+ * Self-styled · idempotent · zero deps. Alt+←/→ also step.
+ */
+(function () {
+  'use strict';
+  if (window.__themeVariantNavLoaded) return;
+  window.__themeVariantNavLoaded = true;
+
+  // admin gate
+  try {
+    var q = new URLSearchParams(location.search);
+    if (q.get('admin') === '1') localStorage.setItem('dwAdmin', '1');
+    if (q.get('admin') === '0') localStorage.removeItem('dwAdmin');
+    if (localStorage.getItem('dwAdmin') !== '1') return;
+  } catch (e) { return; }
+
+  // ordered route slugs (match server /design/:id/:slug + smoke test) → display name
+  var THEMES = [
+    ['compact',    'Compact'],
+    ['bento',      'Bento'],
+    ['tabs',       'Tabs'],
+    ['hero',       'Hero'],
+    ['vertical',   'Vertical'],
+    ['drawer',     'Drawer'],
+    ['editorial',  'Editorial'],
+    ['spec',       'Spec'],
+    ['config',     'Config'],
+    ['stack',      'Stack'],
+    ['best',       'Best'],
+    ['roomsplit',  'Room Split'],
+    ['roomdrawer', 'Room Drawer'],
+    ['roomzine',   'Room Editorial'],
+    ['roomsticky', 'Room Sticky'],
+    ['roommin',    'Room Minimal'],
+    ['roomshow',   'Room Showcase'],
+  ];
+  var SLUGS = THEMES.map(function (t) { return t[0]; });
+
+  // Detect current context. Canonical: /design/:id/:slug. Fallback: /theme-variants/vN-slug.html
+  var m = location.pathname.match(/^\/design\/([^/]+)\/([^/?#]+)/);
+  var mode, designId, slug, idx;
+  if (m) {
+    mode = 'route'; designId = m[1]; slug = m[2];
+    idx = SLUGS.indexOf(slug);
+  } else {
+    mode = 'file';
+    var base = location.pathname.split('/').pop().replace(/\.html?$/i, ''); // vN-slug
+    slug = base.replace(/^v\d+-/, '');
+    idx = SLUGS.indexOf(slug);
+  }
+  if (idx === -1) idx = 0;
+
+  // map slug -> the vN-slug filename (for file mode)
+  var FILEMAP = {
+    compact:'v1-compact', bento:'v2-bento', tabs:'v3-tabs', hero:'v4-hero', vertical:'v5-vertical',
+    drawer:'v6-drawer', editorial:'v7-editorial', spec:'v8-spec', config:'v9-config', stack:'v10-stack',
+    best:'v11-best', roomsplit:'v12-roomsplit', roomdrawer:'v13-roomdrawer', roomzine:'v14-roomzine',
+    roomsticky:'v15-roomsticky', roommin:'v16-roommin', roomshow:'v17-roomshow',
+  };
+
+  function urlFor(i) {
+    var s = SLUGS[(i % SLUGS.length + SLUGS.length) % SLUGS.length];
+    if (mode === 'route') return '/design/' + encodeURIComponent(designId) + '/' + s + location.search;
+    return (FILEMAP[s] || ('v' + (i + 1) + '-' + s)) + '.html' + location.search;
+  }
+  function go(d) { location.href = urlFor(idx + d); }
+
+  var LIVE_IDX = -1;
+
+  // ---- styles ----
+  var css = document.createElement('style');
+  css.textContent = [
+    '#tvn{position:fixed;top:14px;right:16px;z-index:99999;display:flex;align-items:center;gap:2px;',
+      'background:rgba(12,12,12,.86);backdrop-filter:blur(8px);border:1px solid rgba(245,241,232,.20);',
+      'border-radius:999px;padding:3px 4px;font-family:-apple-system,system-ui,sans-serif;box-shadow:0 6px 22px rgba(0,0,0,.45)}',
+    '#tvn button{display:flex;align-items:center;justify-content:center;width:28px;height:28px;border:0;background:transparent;',
+      'color:#f5f1e8;font-size:16px;line-height:1;cursor:pointer;border-radius:50%;transition:background .15s}',
+    '#tvn button:hover{background:rgba(245,241,232,.16)}',
+    '#tvn #tvn-label{font-size:11px;letter-spacing:.03em;color:#f5f1e8;min-width:130px;text-align:center;padding:0 8px;white-space:nowrap;opacity:.95}',
+    '#tvn #tvn-label b{font-weight:600}',
+    '#tvn .tvn-live{display:inline-block;width:6px;height:6px;border-radius:50%;background:#3fd09a;margin-left:6px;vertical-align:middle}',
+  ].join('');
+  document.head.appendChild(css);
+
+  function label() {
+    var name = THEMES[idx][1];
+    var live = (idx === LIVE_IDX) ? '<span class="tvn-live" title="Live theme"></span>' : '';
+    return '<b>' + name + '</b> · ' + (idx + 1) + '/' + THEMES.length + live;
+  }
+
+  function build() {
+    var el = document.createElement('div');
+    el.id = 'tvn';
+    el.setAttribute('aria-label', 'Theme navigator (admin)');
+    el.innerHTML =
+      '<button type="button" id="tvn-prev" aria-label="Previous theme" title="Previous theme (Alt+←)">‹</button>' +
+      '<span id="tvn-label">' + label() + '</span>' +
+      '<button type="button" id="tvn-next" aria-label="Next theme" title="Next theme (Alt+→)">›</button>';
+    document.body.appendChild(el);
+    document.getElementById('tvn-prev').addEventListener('click', function () { go(-1); });
+    document.getElementById('tvn-next').addEventListener('click', function () { go(1); });
+    document.addEventListener('keydown', function (e) {
+      if (e.target && /input|textarea|select/i.test(e.target.tagName)) return;
+      if (e.altKey && e.key === 'ArrowLeft')  { e.preventDefault(); go(-1); }
+      if (e.altKey && e.key === 'ArrowRight') { e.preventDefault(); go(1); }
+    });
+    // mark the live theme
+    fetch('/data/pdp-theme.json', { credentials: 'same-origin' })
+      .then(function (r) { return r.ok ? r.json() : null; })
+      .then(function (j) {
+        if (!j) return;
+        var raw = String(j.theme || j.variant || j.slug || j.live || j.best || '').toLowerCase().trim();
+        if (!raw) return;
+        if (raw === 'best') raw = 'best';
+        var s = raw.replace(/\.html?$/, '').replace(/^v\d+-/, ''); // v11-best -> best
+        var li = SLUGS.indexOf(s);
+        if (li !== -1) { LIVE_IDX = li; var l = document.getElementById('tvn-label'); if (l) l.innerHTML = label(); }
+      })
+      .catch(function () {});
+  }
+
+  if (document.body) build();
+  else document.addEventListener('DOMContentLoaded', build);
+})();
diff --git a/public/theme-variants/v1-compact.html b/public/theme-variants/v1-compact.html
index 8b2fc9b..ee97268 100644
--- a/public/theme-variants/v1-compact.html
+++ b/public/theme-variants/v1-compact.html
@@ -670,5 +670,7 @@ function escapeHtml(v) {
 function escapeAttr(v) { return escapeHtml(v); }
 </script>
 <script src="/js/color-dots.js" defer></script>
+  <script src="/theme-variants/theme-variant-nav.js" defer></script>
+  <script src="/theme-variants/theme-section-rearrange.js" defer></script>
 </body>
 </html>
diff --git a/public/theme-variants/v10-stack.html b/public/theme-variants/v10-stack.html
index db8331f..5c0a963 100644
--- a/public/theme-variants/v10-stack.html
+++ b/public/theme-variants/v10-stack.html
@@ -350,5 +350,7 @@ function escapeHtml(v) { if (v == null) return ''; return String(v).replace(/[&<
 function escapeAttr(v) { return escapeHtml(v); }
 </script>
 <script src="/js/color-dots.js" defer></script>
+  <script src="/theme-variants/theme-variant-nav.js" defer></script>
+  <script src="/theme-variants/theme-section-rearrange.js" defer></script>
 </body>
 </html>
diff --git a/public/theme-variants/v11-best.html b/public/theme-variants/v11-best.html
index e643cab..3a47b8b 100644
--- a/public/theme-variants/v11-best.html
+++ b/public/theme-variants/v11-best.html
@@ -652,5 +652,7 @@ function escapeHtml(v) { if (v == null) return ''; return String(v).replace(/[&<
 function escapeAttr(v) { return escapeHtml(v); }
 </script>
 <script src="/js/color-dots.js" defer></script>
+  <script src="/theme-variants/theme-variant-nav.js" defer></script>
+  <script src="/theme-variants/theme-section-rearrange.js" defer></script>
 </body>
 </html>
diff --git a/public/theme-variants/v12-roomsplit.html b/public/theme-variants/v12-roomsplit.html
index a90cedb..b85d91a 100644
--- a/public/theme-variants/v12-roomsplit.html
+++ b/public/theme-variants/v12-roomsplit.html
@@ -201,7 +201,7 @@
   const kindLabel = design.kind === 'seamless_tile' ? 'Seamless Tile (24×24")' : isMural ? 'Mural Panel' : (design.kind || 'pattern');
   const palette = Array.isArray(design.palette) ? design.palette : [];
   const dominant = design.dominant_hex || (palette[0] && palette[0].hex) || '#999';
-  const img = (/^(\/[^"'<>\s]*|https?:\/\/[^"'<>\s]*)$/i.test(String(design.image_url||'')) ? design.image_url : null) || ('/designs/img/by-id/' + id);
+  const img = design.image_url || ('/designs/img/by-id/' + id);
   const handle = design.handle || ('wallco-' + id);
   const generator = design.generator || 'wallco.ai';
   const created = design.created_at ? new Date(design.created_at) : null;
@@ -261,7 +261,7 @@
     ['paints','◇','Coordinate paints', () => `<div class="pills">${['Sherwin-Williams','Dunn-Edwards','Benjamin Moore','Farrow & Ball','Behr','PPG','RAL'].map(b=>`<a class="pill" href="#">${b}</a>`).join('')}</div>`],
     ['specs','≡','Full specifications', () => `<div class="spectab">${[['Category',catDisplay],['Kind',kindLabel],['Width',(isMural?'36"':'24"')],['Sold by',isMural?'Per panel (36"×11ft)':'Per roll (27 ft)'],['Repeat',w+'" straight match'],['Material','Type II vinyl · class A'],['Handle',handle],['Designed by',generator],['Settlement','OK (logged)'],['Edges scan','PASS · 6-lens']].map(([k,v])=>`<div class="row"><span class="k">${escapeHtml(k)}</span><span class="v">${escapeHtml(v)}</span></div>`).join('')}</div>`],
     ['calc','▦','Roll calculator', () => `<div class="calc"><div class="r"><div><label>Wall W (ft)</label><input id="cw" type="number" value="10" min="1"></div><div><label>Wall H (ft)</label><input id="ch" type="number" value="9" min="1"></div><div><label>Roll W</label><select id="crw"><option>24</option><option>36</option><option>52</option></select></div></div><div class="tot" id="ctot">— rolls · $—</div></div>`],
-    ['share','↗','Share · Furnish · Trade', () => `<div class="tile-btns"><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a><a class="tile-btn" href="#">Pinterest</a><a class="tile-btn" href="#">Instagram</a><a class="tile-btn" href="/design/${id}/tearsheet" target="_blank" rel="noopener noreferrer">Tear sheet</a><a class="tile-btn" href="#">Furnish (AI)</a><a class="tile-btn" href="#">Email client</a></div>`],
+    ['share','↗','Share · Furnish · Trade', () => `<div class="tile-btns"><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a><a class="tile-btn" href="#">Pinterest</a><a class="tile-btn" href="#">Instagram</a><a class="tile-btn" href="/design/${id}/tearsheet" target="_blank">Tear sheet</a><a class="tile-btn" href="#">Furnish (AI)</a><a class="tile-btn" href="#">Email client</a></div>`],
     ['gen','⌘','Generator + prompt', () => `<pre class="prompt">${escapeHtml((design.prompt||'wallco.ai original · seed '+(design.seed||'—')).slice(0,1200))}</pre>`],
   ];
 
@@ -307,5 +307,7 @@ function escapeHtml(v){ if(v==null) return ''; return String(v).replace(/[&<>"']
 function escapeAttr(v){ return escapeHtml(v); }
 </script>
 <script src="/js/color-dots.js" defer></script>
+  <script src="/theme-variants/theme-variant-nav.js" defer></script>
+  <script src="/theme-variants/theme-section-rearrange.js" defer></script>
 </body>
 </html>
diff --git a/public/theme-variants/v13-roomdrawer.html b/public/theme-variants/v13-roomdrawer.html
index 2548b1c..488a3d5 100644
--- a/public/theme-variants/v13-roomdrawer.html
+++ b/public/theme-variants/v13-roomdrawer.html
@@ -201,7 +201,7 @@
   const kindLabel = design.kind === 'seamless_tile' ? 'Seamless Tile (24×24")' : isMural ? 'Mural Panel' : (design.kind || 'pattern');
   const palette = Array.isArray(design.palette) ? design.palette : [];
   const dominant = design.dominant_hex || (palette[0] && palette[0].hex) || '#999';
-  const img = (/^(\/[^"'<>\s]*|https?:\/\/[^"'<>\s]*)$/i.test(String(design.image_url||'')) ? design.image_url : null) || ('/designs/img/by-id/' + id);
+  const img = design.image_url || ('/designs/img/by-id/' + id);
   const handle = design.handle || ('wallco-' + id);
   const generator = design.generator || 'wallco.ai';
   const created = design.created_at ? new Date(design.created_at) : null;
@@ -261,7 +261,7 @@
     ['paints','◇','Coordinate paints', () => `<div class="pills">${['Sherwin-Williams','Dunn-Edwards','Benjamin Moore','Farrow & Ball','Behr','PPG','RAL'].map(b=>`<a class="pill" href="#">${b}</a>`).join('')}</div>`],
     ['specs','≡','Full specifications', () => `<div class="spectab">${[['Category',catDisplay],['Kind',kindLabel],['Width',(isMural?'36"':'24"')],['Sold by',isMural?'Per panel (36"×11ft)':'Per roll (27 ft)'],['Repeat',w+'" straight match'],['Material','Type II vinyl · class A'],['Handle',handle],['Designed by',generator],['Settlement','OK (logged)'],['Edges scan','PASS · 6-lens']].map(([k,v])=>`<div class="row"><span class="k">${escapeHtml(k)}</span><span class="v">${escapeHtml(v)}</span></div>`).join('')}</div>`],
     ['calc','▦','Roll calculator', () => `<div class="calc"><div class="r"><div><label>Wall W (ft)</label><input id="cw" type="number" value="10" min="1"></div><div><label>Wall H (ft)</label><input id="ch" type="number" value="9" min="1"></div><div><label>Roll W</label><select id="crw"><option>24</option><option>36</option><option>52</option></select></div></div><div class="tot" id="ctot">— rolls · $—</div></div>`],
-    ['share','↗','Share · Furnish · Trade', () => `<div class="tile-btns"><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a><a class="tile-btn" href="#">Pinterest</a><a class="tile-btn" href="#">Instagram</a><a class="tile-btn" href="/design/${id}/tearsheet" target="_blank" rel="noopener noreferrer">Tear sheet</a><a class="tile-btn" href="#">Furnish (AI)</a><a class="tile-btn" href="#">Email client</a></div>`],
+    ['share','↗','Share · Furnish · Trade', () => `<div class="tile-btns"><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a><a class="tile-btn" href="#">Pinterest</a><a class="tile-btn" href="#">Instagram</a><a class="tile-btn" href="/design/${id}/tearsheet" target="_blank">Tear sheet</a><a class="tile-btn" href="#">Furnish (AI)</a><a class="tile-btn" href="#">Email client</a></div>`],
     ['gen','⌘','Generator + prompt', () => `<pre class="prompt">${escapeHtml((design.prompt||'wallco.ai original · seed '+(design.seed||'—')).slice(0,1200))}</pre>`],
   ];
 
@@ -307,5 +307,7 @@ function escapeHtml(v){ if(v==null) return ''; return String(v).replace(/[&<>"']
 function escapeAttr(v){ return escapeHtml(v); }
 </script>
 <script src="/js/color-dots.js" defer></script>
+  <script src="/theme-variants/theme-variant-nav.js" defer></script>
+  <script src="/theme-variants/theme-section-rearrange.js" defer></script>
 </body>
 </html>
diff --git a/public/theme-variants/v14-roomzine.html b/public/theme-variants/v14-roomzine.html
index 30bddc1..eaec3f3 100644
--- a/public/theme-variants/v14-roomzine.html
+++ b/public/theme-variants/v14-roomzine.html
@@ -201,7 +201,7 @@
   const kindLabel = design.kind === 'seamless_tile' ? 'Seamless Tile (24×24")' : isMural ? 'Mural Panel' : (design.kind || 'pattern');
   const palette = Array.isArray(design.palette) ? design.palette : [];
   const dominant = design.dominant_hex || (palette[0] && palette[0].hex) || '#999';
-  const img = (/^(\/[^"'<>\s]*|https?:\/\/[^"'<>\s]*)$/i.test(String(design.image_url||'')) ? design.image_url : null) || ('/designs/img/by-id/' + id);
+  const img = design.image_url || ('/designs/img/by-id/' + id);
   const handle = design.handle || ('wallco-' + id);
   const generator = design.generator || 'wallco.ai';
   const created = design.created_at ? new Date(design.created_at) : null;
@@ -261,7 +261,7 @@
     ['paints','◇','Coordinate paints', () => `<div class="pills">${['Sherwin-Williams','Dunn-Edwards','Benjamin Moore','Farrow & Ball','Behr','PPG','RAL'].map(b=>`<a class="pill" href="#">${b}</a>`).join('')}</div>`],
     ['specs','≡','Full specifications', () => `<div class="spectab">${[['Category',catDisplay],['Kind',kindLabel],['Width',(isMural?'36"':'24"')],['Sold by',isMural?'Per panel (36"×11ft)':'Per roll (27 ft)'],['Repeat',w+'" straight match'],['Material','Type II vinyl · class A'],['Handle',handle],['Designed by',generator],['Settlement','OK (logged)'],['Edges scan','PASS · 6-lens']].map(([k,v])=>`<div class="row"><span class="k">${escapeHtml(k)}</span><span class="v">${escapeHtml(v)}</span></div>`).join('')}</div>`],
     ['calc','▦','Roll calculator', () => `<div class="calc"><div class="r"><div><label>Wall W (ft)</label><input id="cw" type="number" value="10" min="1"></div><div><label>Wall H (ft)</label><input id="ch" type="number" value="9" min="1"></div><div><label>Roll W</label><select id="crw"><option>24</option><option>36</option><option>52</option></select></div></div><div class="tot" id="ctot">— rolls · $—</div></div>`],
-    ['share','↗','Share · Furnish · Trade', () => `<div class="tile-btns"><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a><a class="tile-btn" href="#">Pinterest</a><a class="tile-btn" href="#">Instagram</a><a class="tile-btn" href="/design/${id}/tearsheet" target="_blank" rel="noopener noreferrer">Tear sheet</a><a class="tile-btn" href="#">Furnish (AI)</a><a class="tile-btn" href="#">Email client</a></div>`],
+    ['share','↗','Share · Furnish · Trade', () => `<div class="tile-btns"><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a><a class="tile-btn" href="#">Pinterest</a><a class="tile-btn" href="#">Instagram</a><a class="tile-btn" href="/design/${id}/tearsheet" target="_blank">Tear sheet</a><a class="tile-btn" href="#">Furnish (AI)</a><a class="tile-btn" href="#">Email client</a></div>`],
     ['gen','⌘','Generator + prompt', () => `<pre class="prompt">${escapeHtml((design.prompt||'wallco.ai original · seed '+(design.seed||'—')).slice(0,1200))}</pre>`],
   ];
 
@@ -307,5 +307,7 @@ function escapeHtml(v){ if(v==null) return ''; return String(v).replace(/[&<>"']
 function escapeAttr(v){ return escapeHtml(v); }
 </script>
 <script src="/js/color-dots.js" defer></script>
+  <script src="/theme-variants/theme-variant-nav.js" defer></script>
+  <script src="/theme-variants/theme-section-rearrange.js" defer></script>
 </body>
 </html>
diff --git a/public/theme-variants/v15-roomsticky.html b/public/theme-variants/v15-roomsticky.html
index a2481e5..736e13a 100644
--- a/public/theme-variants/v15-roomsticky.html
+++ b/public/theme-variants/v15-roomsticky.html
@@ -201,7 +201,7 @@
   const kindLabel = design.kind === 'seamless_tile' ? 'Seamless Tile (24×24")' : isMural ? 'Mural Panel' : (design.kind || 'pattern');
   const palette = Array.isArray(design.palette) ? design.palette : [];
   const dominant = design.dominant_hex || (palette[0] && palette[0].hex) || '#999';
-  const img = (/^(\/[^"'<>\s]*|https?:\/\/[^"'<>\s]*)$/i.test(String(design.image_url||'')) ? design.image_url : null) || ('/designs/img/by-id/' + id);
+  const img = design.image_url || ('/designs/img/by-id/' + id);
   const handle = design.handle || ('wallco-' + id);
   const generator = design.generator || 'wallco.ai';
   const created = design.created_at ? new Date(design.created_at) : null;
@@ -261,7 +261,7 @@
     ['paints','◇','Coordinate paints', () => `<div class="pills">${['Sherwin-Williams','Dunn-Edwards','Benjamin Moore','Farrow & Ball','Behr','PPG','RAL'].map(b=>`<a class="pill" href="#">${b}</a>`).join('')}</div>`],
     ['specs','≡','Full specifications', () => `<div class="spectab">${[['Category',catDisplay],['Kind',kindLabel],['Width',(isMural?'36"':'24"')],['Sold by',isMural?'Per panel (36"×11ft)':'Per roll (27 ft)'],['Repeat',w+'" straight match'],['Material','Type II vinyl · class A'],['Handle',handle],['Designed by',generator],['Settlement','OK (logged)'],['Edges scan','PASS · 6-lens']].map(([k,v])=>`<div class="row"><span class="k">${escapeHtml(k)}</span><span class="v">${escapeHtml(v)}</span></div>`).join('')}</div>`],
     ['calc','▦','Roll calculator', () => `<div class="calc"><div class="r"><div><label>Wall W (ft)</label><input id="cw" type="number" value="10" min="1"></div><div><label>Wall H (ft)</label><input id="ch" type="number" value="9" min="1"></div><div><label>Roll W</label><select id="crw"><option>24</option><option>36</option><option>52</option></select></div></div><div class="tot" id="ctot">— rolls · $—</div></div>`],
-    ['share','↗','Share · Furnish · Trade', () => `<div class="tile-btns"><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a><a class="tile-btn" href="#">Pinterest</a><a class="tile-btn" href="#">Instagram</a><a class="tile-btn" href="/design/${id}/tearsheet" target="_blank" rel="noopener noreferrer">Tear sheet</a><a class="tile-btn" href="#">Furnish (AI)</a><a class="tile-btn" href="#">Email client</a></div>`],
+    ['share','↗','Share · Furnish · Trade', () => `<div class="tile-btns"><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a><a class="tile-btn" href="#">Pinterest</a><a class="tile-btn" href="#">Instagram</a><a class="tile-btn" href="/design/${id}/tearsheet" target="_blank">Tear sheet</a><a class="tile-btn" href="#">Furnish (AI)</a><a class="tile-btn" href="#">Email client</a></div>`],
     ['gen','⌘','Generator + prompt', () => `<pre class="prompt">${escapeHtml((design.prompt||'wallco.ai original · seed '+(design.seed||'—')).slice(0,1200))}</pre>`],
   ];
 
@@ -307,5 +307,7 @@ function escapeHtml(v){ if(v==null) return ''; return String(v).replace(/[&<>"']
 function escapeAttr(v){ return escapeHtml(v); }
 </script>
 <script src="/js/color-dots.js" defer></script>
+  <script src="/theme-variants/theme-variant-nav.js" defer></script>
+  <script src="/theme-variants/theme-section-rearrange.js" defer></script>
 </body>
 </html>
diff --git a/public/theme-variants/v16-roommin.html b/public/theme-variants/v16-roommin.html
index 0e25271..5b25640 100644
--- a/public/theme-variants/v16-roommin.html
+++ b/public/theme-variants/v16-roommin.html
@@ -201,7 +201,7 @@
   const kindLabel = design.kind === 'seamless_tile' ? 'Seamless Tile (24×24")' : isMural ? 'Mural Panel' : (design.kind || 'pattern');
   const palette = Array.isArray(design.palette) ? design.palette : [];
   const dominant = design.dominant_hex || (palette[0] && palette[0].hex) || '#999';
-  const img = (/^(\/[^"'<>\s]*|https?:\/\/[^"'<>\s]*)$/i.test(String(design.image_url||'')) ? design.image_url : null) || ('/designs/img/by-id/' + id);
+  const img = design.image_url || ('/designs/img/by-id/' + id);
   const handle = design.handle || ('wallco-' + id);
   const generator = design.generator || 'wallco.ai';
   const created = design.created_at ? new Date(design.created_at) : null;
@@ -261,7 +261,7 @@
     ['paints','◇','Coordinate paints', () => `<div class="pills">${['Sherwin-Williams','Dunn-Edwards','Benjamin Moore','Farrow & Ball','Behr','PPG','RAL'].map(b=>`<a class="pill" href="#">${b}</a>`).join('')}</div>`],
     ['specs','≡','Full specifications', () => `<div class="spectab">${[['Category',catDisplay],['Kind',kindLabel],['Width',(isMural?'36"':'24"')],['Sold by',isMural?'Per panel (36"×11ft)':'Per roll (27 ft)'],['Repeat',w+'" straight match'],['Material','Type II vinyl · class A'],['Handle',handle],['Designed by',generator],['Settlement','OK (logged)'],['Edges scan','PASS · 6-lens']].map(([k,v])=>`<div class="row"><span class="k">${escapeHtml(k)}</span><span class="v">${escapeHtml(v)}</span></div>`).join('')}</div>`],
     ['calc','▦','Roll calculator', () => `<div class="calc"><div class="r"><div><label>Wall W (ft)</label><input id="cw" type="number" value="10" min="1"></div><div><label>Wall H (ft)</label><input id="ch" type="number" value="9" min="1"></div><div><label>Roll W</label><select id="crw"><option>24</option><option>36</option><option>52</option></select></div></div><div class="tot" id="ctot">— rolls · $—</div></div>`],
-    ['share','↗','Share · Furnish · Trade', () => `<div class="tile-btns"><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a><a class="tile-btn" href="#">Pinterest</a><a class="tile-btn" href="#">Instagram</a><a class="tile-btn" href="/design/${id}/tearsheet" target="_blank" rel="noopener noreferrer">Tear sheet</a><a class="tile-btn" href="#">Furnish (AI)</a><a class="tile-btn" href="#">Email client</a></div>`],
+    ['share','↗','Share · Furnish · Trade', () => `<div class="tile-btns"><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a><a class="tile-btn" href="#">Pinterest</a><a class="tile-btn" href="#">Instagram</a><a class="tile-btn" href="/design/${id}/tearsheet" target="_blank">Tear sheet</a><a class="tile-btn" href="#">Furnish (AI)</a><a class="tile-btn" href="#">Email client</a></div>`],
     ['gen','⌘','Generator + prompt', () => `<pre class="prompt">${escapeHtml((design.prompt||'wallco.ai original · seed '+(design.seed||'—')).slice(0,1200))}</pre>`],
   ];
 
@@ -307,5 +307,7 @@ function escapeHtml(v){ if(v==null) return ''; return String(v).replace(/[&<>"']
 function escapeAttr(v){ return escapeHtml(v); }
 </script>
 <script src="/js/color-dots.js" defer></script>
+  <script src="/theme-variants/theme-variant-nav.js" defer></script>
+  <script src="/theme-variants/theme-section-rearrange.js" defer></script>
 </body>
 </html>
diff --git a/public/theme-variants/v17-roomshow.html b/public/theme-variants/v17-roomshow.html
index 694eb3d..87b2b9f 100644
--- a/public/theme-variants/v17-roomshow.html
+++ b/public/theme-variants/v17-roomshow.html
@@ -201,7 +201,7 @@
   const kindLabel = design.kind === 'seamless_tile' ? 'Seamless Tile (24×24")' : isMural ? 'Mural Panel' : (design.kind || 'pattern');
   const palette = Array.isArray(design.palette) ? design.palette : [];
   const dominant = design.dominant_hex || (palette[0] && palette[0].hex) || '#999';
-  const img = (/^(\/[^"'<>\s]*|https?:\/\/[^"'<>\s]*)$/i.test(String(design.image_url||'')) ? design.image_url : null) || ('/designs/img/by-id/' + id);
+  const img = design.image_url || ('/designs/img/by-id/' + id);
   const handle = design.handle || ('wallco-' + id);
   const generator = design.generator || 'wallco.ai';
   const created = design.created_at ? new Date(design.created_at) : null;
@@ -261,7 +261,7 @@
     ['paints','◇','Coordinate paints', () => `<div class="pills">${['Sherwin-Williams','Dunn-Edwards','Benjamin Moore','Farrow & Ball','Behr','PPG','RAL'].map(b=>`<a class="pill" href="#">${b}</a>`).join('')}</div>`],
     ['specs','≡','Full specifications', () => `<div class="spectab">${[['Category',catDisplay],['Kind',kindLabel],['Width',(isMural?'36"':'24"')],['Sold by',isMural?'Per panel (36"×11ft)':'Per roll (27 ft)'],['Repeat',w+'" straight match'],['Material','Type II vinyl · class A'],['Handle',handle],['Designed by',generator],['Settlement','OK (logged)'],['Edges scan','PASS · 6-lens']].map(([k,v])=>`<div class="row"><span class="k">${escapeHtml(k)}</span><span class="v">${escapeHtml(v)}</span></div>`).join('')}</div>`],
     ['calc','▦','Roll calculator', () => `<div class="calc"><div class="r"><div><label>Wall W (ft)</label><input id="cw" type="number" value="10" min="1"></div><div><label>Wall H (ft)</label><input id="ch" type="number" value="9" min="1"></div><div><label>Roll W</label><select id="crw"><option>24</option><option>36</option><option>52</option></select></div></div><div class="tot" id="ctot">— rolls · $—</div></div>`],
-    ['share','↗','Share · Furnish · Trade', () => `<div class="tile-btns"><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a><a class="tile-btn" href="#">Pinterest</a><a class="tile-btn" href="#">Instagram</a><a class="tile-btn" href="/design/${id}/tearsheet" target="_blank" rel="noopener noreferrer">Tear sheet</a><a class="tile-btn" href="#">Furnish (AI)</a><a class="tile-btn" href="#">Email client</a></div>`],
+    ['share','↗','Share · Furnish · Trade', () => `<div class="tile-btns"><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a><a class="tile-btn" href="#">Pinterest</a><a class="tile-btn" href="#">Instagram</a><a class="tile-btn" href="/design/${id}/tearsheet" target="_blank">Tear sheet</a><a class="tile-btn" href="#">Furnish (AI)</a><a class="tile-btn" href="#">Email client</a></div>`],
     ['gen','⌘','Generator + prompt', () => `<pre class="prompt">${escapeHtml((design.prompt||'wallco.ai original · seed '+(design.seed||'—')).slice(0,1200))}</pre>`],
   ];
 
@@ -307,5 +307,7 @@ function escapeHtml(v){ if(v==null) return ''; return String(v).replace(/[&<>"']
 function escapeAttr(v){ return escapeHtml(v); }
 </script>
 <script src="/js/color-dots.js" defer></script>
+  <script src="/theme-variants/theme-variant-nav.js" defer></script>
+  <script src="/theme-variants/theme-section-rearrange.js" defer></script>
 </body>
 </html>
diff --git a/public/theme-variants/v2-bento.html b/public/theme-variants/v2-bento.html
index 8c305a3..65f4071 100644
--- a/public/theme-variants/v2-bento.html
+++ b/public/theme-variants/v2-bento.html
@@ -649,5 +649,7 @@ function escapeHtml(v) {
 function escapeAttr(v) { return escapeHtml(v); }
 </script>
 <script src="/js/color-dots.js" defer></script>
+  <script src="/theme-variants/theme-variant-nav.js" defer></script>
+  <script src="/theme-variants/theme-section-rearrange.js" defer></script>
 </body>
 </html>
diff --git a/public/theme-variants/v3-tabs.html b/public/theme-variants/v3-tabs.html
index b0115a1..3e69c52 100644
--- a/public/theme-variants/v3-tabs.html
+++ b/public/theme-variants/v3-tabs.html
@@ -679,5 +679,7 @@ function escapeHtml(v) {
 function escapeAttr(v) { return escapeHtml(v); }
 </script>
 <script src="/js/color-dots.js" defer></script>
+  <script src="/theme-variants/theme-variant-nav.js" defer></script>
+  <script src="/theme-variants/theme-section-rearrange.js" defer></script>
 </body>
 </html>
diff --git a/public/theme-variants/v4-hero.html b/public/theme-variants/v4-hero.html
index 15054b0..c406b60 100644
--- a/public/theme-variants/v4-hero.html
+++ b/public/theme-variants/v4-hero.html
@@ -599,5 +599,7 @@ function escapeHtml(v) {
 function escapeAttr(v) { return escapeHtml(v); }
 </script>
 <script src="/js/color-dots.js" defer></script>
+  <script src="/theme-variants/theme-variant-nav.js" defer></script>
+  <script src="/theme-variants/theme-section-rearrange.js" defer></script>
 </body>
 </html>
diff --git a/public/theme-variants/v5-vertical.html b/public/theme-variants/v5-vertical.html
index 71394aa..e1d15d3 100644
--- a/public/theme-variants/v5-vertical.html
+++ b/public/theme-variants/v5-vertical.html
@@ -417,5 +417,7 @@ function escapeHtml(v) { if (v == null) return ''; return String(v).replace(/[&<
 function escapeAttr(v) { return escapeHtml(v); }
 </script>
 <script src="/js/color-dots.js" defer></script>
+  <script src="/theme-variants/theme-variant-nav.js" defer></script>
+  <script src="/theme-variants/theme-section-rearrange.js" defer></script>
 </body>
 </html>
diff --git a/public/theme-variants/v6-drawer.html b/public/theme-variants/v6-drawer.html
index 4f8d8ea..0b291b2 100644
--- a/public/theme-variants/v6-drawer.html
+++ b/public/theme-variants/v6-drawer.html
@@ -399,5 +399,7 @@ function escapeHtml(v) { if (v == null) return ''; return String(v).replace(/[&<
 function escapeAttr(v) { return escapeHtml(v); }
 </script>
 <script src="/js/color-dots.js" defer></script>
+  <script src="/theme-variants/theme-variant-nav.js" defer></script>
+  <script src="/theme-variants/theme-section-rearrange.js" defer></script>
 </body>
 </html>
diff --git a/public/theme-variants/v7-editorial.html b/public/theme-variants/v7-editorial.html
index 18995c1..c2bbf0c 100644
--- a/public/theme-variants/v7-editorial.html
+++ b/public/theme-variants/v7-editorial.html
@@ -350,5 +350,7 @@ function escapeHtml(v) { if (v == null) return ''; return String(v).replace(/[&<
 function escapeAttr(v) { return escapeHtml(v); }
 </script>
 <script src="/js/color-dots.js" defer></script>
+  <script src="/theme-variants/theme-variant-nav.js" defer></script>
+  <script src="/theme-variants/theme-section-rearrange.js" defer></script>
 </body>
 </html>
diff --git a/public/theme-variants/v8-spec.html b/public/theme-variants/v8-spec.html
index c10e073..f150e4c 100644
--- a/public/theme-variants/v8-spec.html
+++ b/public/theme-variants/v8-spec.html
@@ -339,5 +339,7 @@ function escapeHtml(v) { if (v == null) return ''; return String(v).replace(/[&<
 function escapeAttr(v) { return escapeHtml(v); }
 </script>
 <script src="/js/color-dots.js" defer></script>
+  <script src="/theme-variants/theme-variant-nav.js" defer></script>
+  <script src="/theme-variants/theme-section-rearrange.js" defer></script>
 </body>
 </html>
diff --git a/public/theme-variants/v9-config.html b/public/theme-variants/v9-config.html
index 52cac45..85e350a 100644
--- a/public/theme-variants/v9-config.html
+++ b/public/theme-variants/v9-config.html
@@ -373,5 +373,7 @@ function escapeHtml(v) { if (v == null) return ''; return String(v).replace(/[&<
 function escapeAttr(v) { return escapeHtml(v); }
 </script>
 <script src="/js/color-dots.js" defer></script>
+  <script src="/theme-variants/theme-variant-nav.js" defer></script>
+  <script src="/theme-variants/theme-section-rearrange.js" defer></script>
 </body>
 </html>
diff --git a/scripts/room-left-template.html b/scripts/room-left-template.html
index 6c4e0b8..e34564e 100644
--- a/scripts/room-left-template.html
+++ b/scripts/room-left-template.html
@@ -291,5 +291,7 @@ function escapeHtml(v){ if(v==null) return ''; return String(v).replace(/[&<>"']
 function escapeAttr(v){ return escapeHtml(v); }
 </script>
 <script src="/js/color-dots.js" defer></script>
+  <script src="/theme-variants/theme-variant-nav.js" defer></script>
+  <script src="/theme-variants/theme-section-rearrange.js" defer></script>
 </body>
 </html>

← 8164572 designer-studio: add dead-thumbnail error-swap to its patter  ·  back to Wallco Ai  ·  theme stepper: live-marker via /api/pdp-theme + functional c b2e1d06 →