[object Object]

← back to Wallco Ai

security: add rel=noopener noreferrer to all target=_blank links; remove served .bak files from public/admin

8eef445a112d62f2532fc3db4f7ea4180952563b · 2026-05-30 21:21:24 -0700 · Steve Abrams

- Added rel="noopener noreferrer" to every target="_blank" anchor across 28 HTML/JS files (theme variants, admin pages, marketplace, game JS, server.js inline HTML)
- Deleted public/admin/cactus-curator.html.bak-hover-rooms and public/admin/rooms.html.bak.1780090331 which were being served by express.static (200 OK on direct URL)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files touched

Diff

commit 8eef445a112d62f2532fc3db4f7ea4180952563b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat May 30 21:21:24 2026 -0700

    security: add rel=noopener noreferrer to all target=_blank links; remove served .bak files from public/admin
    
    - Added rel="noopener noreferrer" to every target="_blank" anchor across 28 HTML/JS files (theme variants, admin pages, marketplace, game JS, server.js inline HTML)
    - Deleted public/admin/cactus-curator.html.bak-hover-rooms and public/admin/rooms.html.bak.1780090331 which were being served by express.static (200 OK on direct URL)
    
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---
 public/admin/bg-tester.html               |  2 +-
 public/admin/cactus-curator.html          |  6 ++---
 public/admin/elements.html                | 37 +++++++++++++++++++++++++++----
 public/admin/etsy-bucket.html             |  2 +-
 public/admin/etsy-setup.html              |  2 +-
 public/admin/ghost-review.html            |  8 +++----
 public/admin/rooms.html                   |  2 +-
 public/admin/seam-debug.html              |  2 +-
 public/js/wallco-game.js                  |  2 +-
 public/marketplace/admin.html             |  4 ++--
 public/marketplace/dashboard.html         |  4 ++--
 public/theme-variants/v1-compact.html     |  4 ++--
 public/theme-variants/v10-stack.html      |  4 ++--
 public/theme-variants/v11-best.html       |  4 ++--
 public/theme-variants/v12-roomsplit.html  |  2 +-
 public/theme-variants/v13-roomdrawer.html |  2 +-
 public/theme-variants/v14-roomzine.html   |  2 +-
 public/theme-variants/v15-roomsticky.html |  2 +-
 public/theme-variants/v16-roommin.html    |  2 +-
 public/theme-variants/v17-roomshow.html   |  2 +-
 public/theme-variants/v2-bento.html       |  4 ++--
 public/theme-variants/v3-tabs.html        |  4 ++--
 public/theme-variants/v4-hero.html        |  4 ++--
 public/theme-variants/v5-vertical.html    |  4 ++--
 public/theme-variants/v6-drawer.html      |  2 +-
 public/theme-variants/v7-editorial.html   |  4 ++--
 public/theme-variants/v8-spec.html        |  2 +-
 public/theme-variants/v9-config.html      |  2 +-
 server.js                                 |  2 +-
 29 files changed, 76 insertions(+), 47 deletions(-)

diff --git a/public/admin/bg-tester.html b/public/admin/bg-tester.html
index c2065f7..a594fac 100644
--- a/public/admin/bg-tester.html
+++ b/public/admin/bg-tester.html
@@ -389,7 +389,7 @@
       </div>
       <div class="img-box loading" id="ib-${RESULT_COUNT}"></div>
       <div class="actions">
-        <a href="#" id="a-link-${RESULT_COUNT}" target="_blank">View</a>
+        <a href="#" id="a-link-${RESULT_COUNT}" target="_blank" rel="noopener noreferrer">View</a>
         <button id="b-again-${RESULT_COUNT}">Retry</button>
       </div>
     `;
diff --git a/public/admin/cactus-curator.html b/public/admin/cactus-curator.html
index a9a0515..c3e002e 100644
--- a/public/admin/cactus-curator.html
+++ b/public/admin/cactus-curator.html
@@ -455,7 +455,7 @@ function card(d, idx){
       ${roomImgHTML(d)}
       ${seamCallout}
       <button class="auto-id-btn" title="auto-detect bad areas (you confirm/delete)">🔍 Auto-ID</button>
-      <a class="dbg-btn" href="${q('/admin/seam-debug/'+d.id)}" target="_blank" title="open seam-defect debugger — see where the mid-line breaks">🔬 Debug seams</a>
+      <a class="dbg-btn" href="${q('/admin/seam-debug/'+d.id)}" target="_blank" rel="noopener noreferrer" title="open seam-defect debugger — see where the mid-line breaks">🔬 Debug seams</a>
       <div class="state" style="${stateLabel?'display:block':''}">${stateLabel}</div>
     </div>
     <div class="meta">
@@ -1165,7 +1165,7 @@ load();
           <span class="b ${fam.is_published?'pub':'unpub'}">${fam.is_published?'PUB':'unpub'}</span>
           <span class="b reason" title="${all.length} colorways in family">${all.length}cw</span>
         </div>
-        <a class="bb-link" href="/admin/seam-debug/${fam.id}" target="_blank" style="display:block;width:100%;height:100%">
+        <a class="bb-link" href="/admin/seam-debug/${fam.id}" target="_blank" rel="noopener noreferrer" style="display:block;width:100%;height:100%">
           <img loading="lazy" src="/designs/img/by-id/${fam.id}" alt="base ${fam.id}">
           ${roomImgHTML(fam)}
         </a>
@@ -1184,7 +1184,7 @@ load();
           const isBase = c.id === fam.id;
           const border = isBase ? '#ffd68a' : (c.is_published ? '#3a6a3a' : '#2a2a2a');
           const cw = (c.category || '').split(' · ')[1] || 'base';
-          return `<a href="/admin/seam-debug/${c.id}" target="_blank" title="#${c.id} · ${cw}${c.is_published?' · ✓ published':''}" style="flex:0 0 auto;border:1.5px solid ${border};border-radius:4px;overflow:hidden;background:#000;text-decoration:none;line-height:0">
+          return `<a href="/admin/seam-debug/${c.id}" target="_blank" rel="noopener noreferrer" title="#${c.id} · ${cw}${c.is_published?' · ✓ published':''}" style="flex:0 0 auto;border:1.5px solid ${border};border-radius:4px;overflow:hidden;background:#000;text-decoration:none;line-height:0">
             <img loading="lazy" src="/designs/img/by-id/${c.id}" style="display:block;width:38px;height:38px;object-fit:cover">
           </a>`;
         }).join('')}
diff --git a/public/admin/elements.html b/public/admin/elements.html
index 149bddf..cff0f03 100644
--- a/public/admin/elements.html
+++ b/public/admin/elements.html
@@ -486,10 +486,39 @@
     state.shuffleSeed = Date.now() & 0xffffffff;
     render();
   });
-  document.getElementById('drawer-cta').addEventListener('click', () => {
-    if (state.selected.length === 0) return;
-    // v2: actual Gemini hookup. For now just confirm + leave selection intact.
-    alert(`Compose stub — ${state.selected.length} element(s) selected.\nIDs: ${state.selected.join(', ')}\n\nGemini wiring lands in v2.`);
+  document.getElementById('drawer-cta').addEventListener('click', async () => {
+    const ids = state.selected.slice();
+    if (ids.length === 0) return;
+    const verb = ids.length === 1 ? 'use as reference' : ids.length === 2 ? 'blend' : `compose ${ids.length}`;
+    if (!confirm(`Generate 1 NEW design (${verb}) from element(s) ${ids.map(i => '#' + i).join(', ')}?\n\nThis spends one paid Gemini image call. The result lands UNPUBLISHED in the curator queue for review.`)) return;
+    const hint = (prompt('Optional art-director note (leave blank for none):', '') || '').trim();
+
+    const cta = document.getElementById('drawer-cta');
+    const orig = cta.textContent;
+    cta.disabled = true;
+    cta.textContent = 'Composing… (Gemini)';
+    try {
+      const r = await fetch('/api/admin/elements/compose', {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({ ids, hint }),
+      });
+      const j = await r.json().catch(() => ({}));
+      if (!r.ok || !j.ok) {
+        alert('Compose failed: ' + (j.error || ('HTTP ' + r.status)));
+        return;
+      }
+      state.selected = [];
+      renderDrawer();
+      render();
+      alert(`✓ Generated #${j.new_id} (${j.mode}, ${j.elapsed_s}s).\nUNPUBLISHED — review it in the curator queue before it goes live.\n\nOpening the new design…`);
+      window.open('/design/' + j.new_id, '_blank', 'noopener');
+    } catch (e) {
+      alert('Compose error: ' + (e && e.message || e));
+    } finally {
+      cta.disabled = false;
+      cta.textContent = orig;
+    }
   });
   document.getElementById('drawer-close').addEventListener('click', () => {
     document.getElementById('drawer').classList.remove('open');
diff --git a/public/admin/etsy-bucket.html b/public/admin/etsy-bucket.html
index 355fc73..25228e2 100644
--- a/public/admin/etsy-bucket.html
+++ b/public/admin/etsy-bucket.html
@@ -75,7 +75,7 @@ function render(){
     <div class="card">
       <div class="thumb">
         <div class="badges"><span class="b ${({queued:'q',exported:'exp',listed:'list'})[x.state]||''}">${x.state}</span></div>
-        <a href="${q('/design/'+x.design_id)}" target="_blank"><img loading="lazy" src="/designs/img/by-id/${x.design_id}" alt="#${x.design_id}"></a>
+        <a href="${q('/design/'+x.design_id)}" target="_blank" rel="noopener noreferrer"><img loading="lazy" src="/designs/img/by-id/${x.design_id}" alt="#${x.design_id}"></a>
         <button class="rm" onclick="rm(${x.design_id})">✕ remove</button>
       </div>
       <div class="meta"><span class="id">#${x.design_id}</span><span class="dpi">${x.target_dpi}dpi</span></div>
diff --git a/public/admin/etsy-setup.html b/public/admin/etsy-setup.html
index b94c920..1187b49 100644
--- a/public/admin/etsy-setup.html
+++ b/public/admin/etsy-setup.html
@@ -89,7 +89,7 @@
         <p>Copy the <b>Keystring</b> (= your API key) — you'll paste it in Step 2.</p>
       </div>
       <div class="ctl"><div>
-        <a class="act" href="https://www.etsy.com/developers/your-apps" target="_blank">Open Etsy developer portal ↗</a>
+        <a class="act" href="https://www.etsy.com/developers/your-apps" target="_blank" rel="noopener noreferrer">Open Etsy developer portal ↗</a>
         <span class="status" id="st-app">manual</span>
       </div></div>
     </div>
diff --git a/public/admin/ghost-review.html b/public/admin/ghost-review.html
index d48204f..54e4763 100644
--- a/public/admin/ghost-review.html
+++ b/public/admin/ghost-review.html
@@ -1663,7 +1663,7 @@
       });
       const j = await r.json();
       if (!r.ok) throw new Error(j.error || ('http ' + r.status));
-      stat.innerHTML = `✓ Saved as <a href="/design/${j.new_id}" target="_blank" style="color:var(--green); font-weight:600">#${j.new_id}</a> · original #${item.id} unpublished`;
+      stat.innerHTML = `✓ Saved as <a href="/design/${j.new_id}" target="_blank" rel="noopener noreferrer" style="color:var(--green); font-weight:600">#${j.new_id}</a> · original #${item.id} unpublished`;
       toast(`✓ saved → #${j.new_id}`);
       // Save a tp_other label
       try {
@@ -1859,7 +1859,7 @@
       const dt = ((Date.now() - t0) / 1000).toFixed(1);
       stat.style.borderLeftColor = 'var(--green)';
       stat.style.background = '#eef7ed';
-      stat.innerHTML = `✓ Fixed in ${dt}s · new design <a href="/design/${j.new_id}" target="_blank" style="color:var(--green); font-weight:600">#${j.new_id}</a> · original #${item.id} unpublished`;
+      stat.innerHTML = `✓ Fixed in ${dt}s · new design <a href="/design/${j.new_id}" target="_blank" rel="noopener noreferrer" style="color:var(--green); font-weight:600">#${j.new_id}</a> · original #${item.id} unpublished`;
       toast(`✓ fixed → #${j.new_id} (${dt}s)`);
       // Reload the image to the fixed version (in case Steve wants to confirm)
       mainImg.src = `/designs/img/by-id/${j.new_id}?fixed=1`;
@@ -1918,7 +1918,7 @@
       const dt = ((Date.now() - t0) / 1000).toFixed(1);
       stat.style.borderLeftColor = 'var(--green)';
       stat.style.background = '#eef7ed';
-      stat.innerHTML = `✓ Smart-fixed in ${dt}s · new design <a href="/design/${j.new_id}" target="_blank" style="color:var(--green); font-weight:600">#${j.new_id}</a><br>` +
+      stat.innerHTML = `✓ Smart-fixed in ${dt}s · new design <a href="/design/${j.new_id}" target="_blank" rel="noopener noreferrer" style="color:var(--green); font-weight:600">#${j.new_id}</a><br>` +
                        `<span style="font:11px var(--sans); color:var(--faint)">motif: ${(j.motif || '').slice(0, 180)}…</span>`;
       toast(`🎯 smart-fixed → #${j.new_id} (${dt}s)`);
       mainImg.src = `/designs/img/by-id/${j.new_id}?smart=1`;
@@ -1976,7 +1976,7 @@
       stat.style.borderLeftColor = 'var(--green)';
       stat.style.background = '#eef7ed';
       const modeLabel = j.source_still_published ? '+ NEW SIBLING' : 'REPLACED';
-      stat.innerHTML = `✓ ${modeLabel} in ${dt}s · new design <a href="/design/${j.new_id}" target="_blank" style="color:var(--green); font-weight:600">#${j.new_id}</a> · original ${j.source_still_published ? '<b>still published</b>' : 'unpublished'}<br><span style="font:11px var(--sans); color:var(--faint)">new prompt: ${(j.new_prompt || '').slice(0, 200)}…</span>`;
+      stat.innerHTML = `✓ ${modeLabel} in ${dt}s · new design <a href="/design/${j.new_id}" target="_blank" rel="noopener noreferrer" style="color:var(--green); font-weight:600">#${j.new_id}</a> · original ${j.source_still_published ? '<b>still published</b>' : 'unpublished'}<br><span style="font:11px var(--sans); color:var(--faint)">new prompt: ${(j.new_prompt || '').slice(0, 200)}…</span>`;
       toast(`✓ ${j.source_still_published ? 'added' : 'regenerated'} → #${j.new_id} (${dt}s)`);
       mainImg.src = `/designs/img/by-id/${j.new_id}?regen=1`;
       // Save a tp_other label to record the action
diff --git a/public/admin/rooms.html b/public/admin/rooms.html
index 7015cbd..74760bf 100644
--- a/public/admin/rooms.html
+++ b/public/admin/rooms.html
@@ -73,7 +73,7 @@ function render(){
   $('empty').style.display=list.length?'none':'block';
   $('grid').innerHTML=list.map(r=>`
     <div class="card">
-      <a href="${q('/design/'+r.design_id)}" target="_blank" title="open design #${r.design_id}"><img loading="lazy" src="/designs/room/design_${r.design_id}_${r.room_type}.png" onerror="this.onerror=null;this.src='${r.image_path}'" alt="room ${r.id}"></a>
+      <a href="${q('/design/'+r.design_id)}" target="_blank" rel="noopener noreferrer" title="open design #${r.design_id}"><img loading="lazy" src="/designs/room/design_${r.design_id}_${r.room_type}.png" onerror="this.onerror=null;this.src='${r.image_path}'" alt="room ${r.id}"></a>
       <div class="meta"><span class="rt">${(r.room_type||'').replace(/_/g,' ')}</span><span class="did">#${r.design_id}</span></div>
       <div class="meta" style="padding-top:0"><span class="when">🕓 ${fmt(r.created_at)}</span></div>
     </div>`).join('');
diff --git a/public/admin/seam-debug.html b/public/admin/seam-debug.html
index 5dc7582..e9f5396 100644
--- a/public/admin/seam-debug.html
+++ b/public/admin/seam-debug.html
@@ -62,7 +62,7 @@
   <button class="heal" id="healbtn" disabled title="click boxes to select, then heal only those regions">⚕ Heal selected (0)</button>
   <button class="heal" id="healall" style="background:#3b2e08;color:#f0b400;border-color:#5a4710" title="heal every detected defect">⚕ Heal all</button>
   <button class="heal" id="regenbtn" style="background:#1d2c3b;color:#7ec8ff;border-color:#365778" title="regenerate from the original prompt with a new seed — preview before approve">⟲ Regenerate from prompt</button>
-  <a id="openraw" style="font-size:12px;color:var(--mut);text-decoration:none" target="_blank">open raw image ↗</a>
+  <a id="openraw" style="font-size:12px;color:var(--mut);text-decoration:none" target="_blank" rel="noopener noreferrer">open raw image ↗</a>
 </header>
 <div id="err" style="display:none"></div>
 <main id="main">
diff --git a/public/js/wallco-game.js b/public/js/wallco-game.js
index 971a27d..63756fe 100644
--- a/public/js/wallco-game.js
+++ b/public/js/wallco-game.js
@@ -387,7 +387,7 @@
         var grid = document.getElementById('wc-recs-grid');
         if (!picks.length) { grid.innerHTML = '<div style="grid-column:1/-1;text-align:center;color:#888;padding:30px">No matches yet — keep voting!</div>'; return; }
         grid.innerHTML = picks.map(function (p) {
-          return '<a href="' + (p.sample_url || ('https://wallco.ai/design/' + p.id)) + '" target="_blank" style="display:block;text-decoration:none;color:inherit;border:1px solid #2a4a47;border-radius:8px;overflow:hidden;background:#0f1c1a;transition:transform .15s,border-color .15s" onmouseover="this.style.borderColor=\'#5aa\';this.style.transform=\'scale(1.02)\'" onmouseout="this.style.borderColor=\'#2a4a47\';this.style.transform=\'\'">' +
+          return '<a href="' + (p.sample_url || ('https://wallco.ai/design/' + p.id)) + '" target="_blank" rel="noopener noreferrer" style="display:block;text-decoration:none;color:inherit;border:1px solid #2a4a47;border-radius:8px;overflow:hidden;background:#0f1c1a;transition:transform .15s,border-color .15s" onmouseover="this.style.borderColor=\'#5aa\';this.style.transform=\'scale(1.02)\'" onmouseout="this.style.borderColor=\'#2a4a47\';this.style.transform=\'\'">' +
             '<img src="' + p.image_url + '" alt="' + (p.title||'').replace(/"/g,'&quot;') + '" style="width:100%;aspect-ratio:1;object-fit:cover;display:block">' +
             '<div style="padding:8px 10px;font-size:11px">' +
               '<div style="color:#e8e2d6">' + (p.title||'').slice(0,38) + '</div>' +
diff --git a/public/marketplace/admin.html b/public/marketplace/admin.html
index b3e181a..5962c58 100644
--- a/public/marketplace/admin.html
+++ b/public/marketplace/admin.html
@@ -98,7 +98,7 @@ async function loadDesigners() {
     const tb = document.getElementById('dTable').querySelector('tbody'); tb.innerHTML='';
     (j.designers || []).forEach(d => {
       const tr = document.createElement('tr');
-      tr.innerHTML = `<td>${d.display_name}</td><td><a class="under" href="/designers/${d.slug}" target="_blank">${d.slug}</a></td><td><span class="mp-pill">${d.level}</span></td>
+      tr.innerHTML = `<td>${d.display_name}</td><td><a class="under" href="/designers/${d.slug}" target="_blank" rel="noopener noreferrer">${d.slug}</a></td><td><span class="mp-pill">${d.level}</span></td>
         <td class="row-act">
           <button class="mp-btn tiny good" onclick="approveDesigner(${d.id})">Approve</button>
           <button class="mp-btn tiny" onclick="verifyDesigner(${d.id})">Verify</button>
@@ -121,7 +121,7 @@ async function loadPatterns() {
         <td class="row-act">
           <button class="mp-btn tiny good" onclick="approvePattern(${p.id})">Approve</button>
           <button class="mp-btn tiny danger" onclick="rejectPattern(${p.id})">Reject</button>
-          <a class="mp-btn tiny ghost" href="/patterns/${p.slug}" target="_blank">View</a>
+          <a class="mp-btn tiny ghost" href="/patterns/${p.slug}" target="_blank" rel="noopener noreferrer">View</a>
         </td>`;
       tb.appendChild(tr);
     });
diff --git a/public/marketplace/dashboard.html b/public/marketplace/dashboard.html
index 8f2a45d..77ceaf3 100644
--- a/public/marketplace/dashboard.html
+++ b/public/marketplace/dashboard.html
@@ -103,7 +103,7 @@
       <section class="panel" id="panel-storefront">
         <h3>Your public storefront</h3>
         <p style="color: var(--mp-muted)">Your storefront lives at <code id="storeUrl"></code>. Share this link with clients and trade buyers.</p>
-        <a class="mp-btn" id="visitStore" target="_blank">Visit storefront →</a>
+        <a class="mp-btn" id="visitStore" target="_blank" rel="noopener noreferrer">Visit storefront →</a>
       </section>
     </section>
   </main>
@@ -153,7 +153,7 @@ async function load() {
           <div class="sub">${p.status} · ${p.view_count} views · ${p.save_count} saves</div>
           <div class="row">
             <span class="mp-pill ${p.status==='approved'?'verified': (p.status==='pending'?'':'')}">${p.status}</span>
-            <a class="mp-btn tiny ghost" href="/patterns/${p.slug}" target="_blank">View</a>
+            <a class="mp-btn tiny ghost" href="/patterns/${p.slug}" target="_blank" rel="noopener noreferrer">View</a>
           </div>
         </div>`;
       pg.appendChild(card);
diff --git a/public/theme-variants/v1-compact.html b/public/theme-variants/v1-compact.html
index 56ed11c..9ca1118 100644
--- a/public/theme-variants/v1-compact.html
+++ b/public/theme-variants/v1-compact.html
@@ -572,7 +572,7 @@
       <summary>Trade tools <span class="hint">Tear · Email · Compare</span></summary>
       <div class="acc-body">
         <div class="tile-btns">
-          <a class="tile-btn" href="/design/${id}/tearsheet" target="_blank">Tear sheet</a>
+          <a class="tile-btn" href="/design/${id}/tearsheet" target="_blank" rel="noopener noreferrer">Tear sheet</a>
           <a class="tile-btn" href="#" onclick="openTrade();return false">Email client</a>
           <a class="tile-btn" href="#" onclick="openTrade();return false">Compare</a>
           <a class="tile-btn" href="#" onclick="saveDesign(${id});return false">Save</a>
@@ -650,7 +650,7 @@
     '<div class="tile-btns" style="margin-top:8px"><a class="tile-btn" href="#">Residential</a><a class="tile-btn" href="#">Commercial</a><a class="tile-btn" href="#">Hospitality</a><a class="tile-btn" href="#">Healthcare</a></div>';
   document.getElementById('m-trade-body').innerHTML =
     '<p>Trade-only tools (interior designers + specifiers).</p>' +
-    '<div class="tile-btns" style="margin-top:8px"><a class="tile-btn" href="/design/' + id + '/tearsheet" target="_blank">Tear sheet PDF</a><a class="tile-btn" href="#">Email to client</a><a class="tile-btn" href="#">Compare 2 patterns</a><a class="tile-btn" href="#">Add to saved</a></div>';
+    '<div class="tile-btns" style="margin-top:8px"><a class="tile-btn" href="/design/' + id + '/tearsheet" target="_blank" rel="noopener noreferrer">Tear sheet PDF</a><a class="tile-btn" href="#">Email to client</a><a class="tile-btn" href="#">Compare 2 patterns</a><a class="tile-btn" href="#">Add to saved</a></div>';
 })();
 
 function openShare() { document.getElementById('m-share').showModal(); }
diff --git a/public/theme-variants/v10-stack.html b/public/theme-variants/v10-stack.html
index 43a57b6..b24f3de 100644
--- a/public/theme-variants/v10-stack.html
+++ b/public/theme-variants/v10-stack.html
@@ -304,7 +304,7 @@
       <a class="tile-btn" href="#" onclick="openShare();return false">Pinterest</a><a class="tile-btn" href="#" onclick="openShare();return false">Instagram</a><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a>
     </div></div></details>
     <details class="acc"><summary>Trade tools <span class="hint">Tear · Email · Compare · Save</span></summary><div class="acc-body"><div class="tile-btns">
-      <a class="tile-btn" href="/design/${id}/tearsheet" target="_blank">Tear sheet</a><a class="tile-btn" href="#" onclick="openTrade();return false">Email</a><a class="tile-btn" href="#" onclick="openTrade();return false">Compare</a><a class="tile-btn" href="#" onclick="saveDesign(${id});return false">Save</a>
+      <a class="tile-btn" href="/design/${id}/tearsheet" target="_blank" rel="noopener noreferrer">Tear sheet</a><a class="tile-btn" href="#" onclick="openTrade();return false">Email</a><a class="tile-btn" href="#" onclick="openTrade();return false">Compare</a><a class="tile-btn" href="#" onclick="saveDesign(${id});return false">Save</a>
     </div></div></details>
     <details class="acc"><summary>Ratings &amp; reviews <span class="hint">★★★★☆ · ${escapeHtml(design.user_vote_count || 0)} votes</span></summary><div class="acc-body"><div style="font-size:22px; letter-spacing:4px; color:#b89a4a">★★★★☆</div><div style="font-size:11.5px; color:var(--muted); margin-top:4px">Curated · 4.2</div></div></details>
     <details class="acc"><summary>Generator + prompt <span class="hint">wallco.ai · seed ${escapeHtml(design.seed || '—')}</span></summary><div class="acc-body"><pre style="font:11px/1.5 ui-monospace,Menlo,monospace; background:var(--panel); padding:10px; border-radius:6px; max-height:200px; overflow:auto; margin:0; white-space:pre-wrap; word-break:break-word">${escapeHtml((design.prompt || '').slice(0, 1200))}${(design.prompt && design.prompt.length > 1200) ? '\n…[truncated]' : ''}</pre></div></details>
@@ -337,7 +337,7 @@
 
   document.getElementById('m-share-body').innerHTML = '<p>Share <b>' + escapeHtml(title) + '</b></p><div class="tile-btns"><a class="tile-btn" href="#">Pinterest</a><a class="tile-btn" href="#">Instagram</a><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a></div>';
   document.getElementById('m-furnish-body').innerHTML = '<p>Generate AI furnishings.</p><div class="tile-btns"><a class="tile-btn" href="#">Residential</a><a class="tile-btn" href="#">Commercial</a><a class="tile-btn" href="#">Hospitality</a><a class="tile-btn" href="#">Healthcare</a></div>';
-  document.getElementById('m-trade-body').innerHTML = '<p>Trade tools.</p><div class="tile-btns"><a class="tile-btn" href="/design/' + id + '/tearsheet" target="_blank">Tear sheet</a><a class="tile-btn" href="#">Email</a><a class="tile-btn" href="#">Compare</a><a class="tile-btn" href="#">Save</a></div>';
+  document.getElementById('m-trade-body').innerHTML = '<p>Trade tools.</p><div class="tile-btns"><a class="tile-btn" href="/design/' + id + '/tearsheet" target="_blank" rel="noopener noreferrer">Tear sheet</a><a class="tile-btn" href="#">Email</a><a class="tile-btn" href="#">Compare</a><a class="tile-btn" href="#">Save</a></div>';
 })();
 
 function openShare() { document.getElementById('m-share').showModal(); }
diff --git a/public/theme-variants/v11-best.html b/public/theme-variants/v11-best.html
index 9a19b37..0f7375e 100644
--- a/public/theme-variants/v11-best.html
+++ b/public/theme-variants/v11-best.html
@@ -426,7 +426,7 @@
           <div class="sub" style="background:var(--trade)">
             <h4>Trade tools</h4>
             <div class="tile-btns">
-              <a class="tile-btn" href="/design/${id}/tearsheet" target="_blank">Tear sheet PDF</a>
+              <a class="tile-btn" href="/design/${id}/tearsheet" target="_blank" rel="noopener noreferrer">Tear sheet PDF</a>
               <a class="tile-btn" href="#" onclick="openTrade();return false">Email client</a>
               <a class="tile-btn" href="#" onclick="openTrade();return false">Compare 2</a>
               <a class="tile-btn" href="#" onclick="saveDesign(${id});return false">Save</a>
@@ -558,7 +558,7 @@
   // ── modal bodies ──
   document.getElementById('m-share-body').innerHTML = '<p>Share <b>' + escapeHtml(title) + '</b></p><div class="tile-btns"><a class="tile-btn" href="#">Pinterest</a><a class="tile-btn" href="#">Instagram</a><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a></div>';
   document.getElementById('m-furnish-body').innerHTML = '<p>Generate AI furnishings curated to this palette.</p><div class="tile-btns"><a class="tile-btn" href="#">Residential</a><a class="tile-btn" href="#">Commercial</a><a class="tile-btn" href="#">Hospitality</a><a class="tile-btn" href="#">Healthcare</a></div>';
-  document.getElementById('m-trade-body').innerHTML = '<p>Trade-only tools.</p><div class="tile-btns"><a class="tile-btn" href="/design/' + id + '/tearsheet" target="_blank">Tear sheet PDF</a><a class="tile-btn" href="#">Email client</a><a class="tile-btn" href="#">Compare 2 patterns</a><a class="tile-btn" href="#">Add to saved</a></div>';
+  document.getElementById('m-trade-body').innerHTML = '<p>Trade-only tools.</p><div class="tile-btns"><a class="tile-btn" href="/design/' + id + '/tearsheet" target="_blank" rel="noopener noreferrer">Tear sheet PDF</a><a class="tile-btn" href="#">Email client</a><a class="tile-btn" href="#">Compare 2 patterns</a><a class="tile-btn" href="#">Add to saved</a></div>';
 })();
 
 function openShare() { document.getElementById('m-share').showModal(); }
diff --git a/public/theme-variants/v12-roomsplit.html b/public/theme-variants/v12-roomsplit.html
index b57f053..a5f16f9 100644
--- a/public/theme-variants/v12-roomsplit.html
+++ b/public/theme-variants/v12-roomsplit.html
@@ -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">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" rel="noopener noreferrer">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>`],
   ];
 
diff --git a/public/theme-variants/v13-roomdrawer.html b/public/theme-variants/v13-roomdrawer.html
index f5fd1eb..a04f180 100644
--- a/public/theme-variants/v13-roomdrawer.html
+++ b/public/theme-variants/v13-roomdrawer.html
@@ -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">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" rel="noopener noreferrer">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>`],
   ];
 
diff --git a/public/theme-variants/v14-roomzine.html b/public/theme-variants/v14-roomzine.html
index f4149d4..e3d3a60 100644
--- a/public/theme-variants/v14-roomzine.html
+++ b/public/theme-variants/v14-roomzine.html
@@ -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">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" rel="noopener noreferrer">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>`],
   ];
 
diff --git a/public/theme-variants/v15-roomsticky.html b/public/theme-variants/v15-roomsticky.html
index 4795d2e..d1b58c2 100644
--- a/public/theme-variants/v15-roomsticky.html
+++ b/public/theme-variants/v15-roomsticky.html
@@ -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">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" rel="noopener noreferrer">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>`],
   ];
 
diff --git a/public/theme-variants/v16-roommin.html b/public/theme-variants/v16-roommin.html
index cc1f4e9..5369b06 100644
--- a/public/theme-variants/v16-roommin.html
+++ b/public/theme-variants/v16-roommin.html
@@ -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">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" rel="noopener noreferrer">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>`],
   ];
 
diff --git a/public/theme-variants/v17-roomshow.html b/public/theme-variants/v17-roomshow.html
index cd4f99a..dc2ebab 100644
--- a/public/theme-variants/v17-roomshow.html
+++ b/public/theme-variants/v17-roomshow.html
@@ -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">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" rel="noopener noreferrer">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>`],
   ];
 
diff --git a/public/theme-variants/v2-bento.html b/public/theme-variants/v2-bento.html
index 01f78c6..93cfaea 100644
--- a/public/theme-variants/v2-bento.html
+++ b/public/theme-variants/v2-bento.html
@@ -539,7 +539,7 @@
   <div class="cell c-trade">
     <h3>Trade tools</h3>
     <div class="tile-btns">
-      <a class="tile-btn" href="/design/${id}/tearsheet" target="_blank">Tear sheet</a>
+      <a class="tile-btn" href="/design/${id}/tearsheet" target="_blank" rel="noopener noreferrer">Tear sheet</a>
       <a class="tile-btn" href="#" onclick="openTrade();return false">Email client</a>
       <a class="tile-btn" href="#" onclick="openTrade();return false">Compare</a>
       <a class="tile-btn" href="#" onclick="saveDesign(${id});return false">Save</a>
@@ -628,7 +628,7 @@
     '<div class="tile-btns" style="margin-top:8px"><a class="tile-btn" href="#">Residential</a><a class="tile-btn" href="#">Commercial</a><a class="tile-btn" href="#">Hospitality</a><a class="tile-btn" href="#">Healthcare</a></div>';
   document.getElementById('m-trade-body').innerHTML =
     '<p>Trade-only tools (interior designers + specifiers).</p>' +
-    '<div class="tile-btns" style="margin-top:8px"><a class="tile-btn" href="/design/' + id + '/tearsheet" target="_blank">Tear sheet PDF</a><a class="tile-btn" href="#">Email to client</a><a class="tile-btn" href="#">Compare 2 patterns</a><a class="tile-btn" href="#">Add to saved</a></div>';
+    '<div class="tile-btns" style="margin-top:8px"><a class="tile-btn" href="/design/' + id + '/tearsheet" target="_blank" rel="noopener noreferrer">Tear sheet PDF</a><a class="tile-btn" href="#">Email to client</a><a class="tile-btn" href="#">Compare 2 patterns</a><a class="tile-btn" href="#">Add to saved</a></div>';
 })();
 
 // ── shared helpers / window-scope handlers ────────────────────────────────
diff --git a/public/theme-variants/v3-tabs.html b/public/theme-variants/v3-tabs.html
index 6859ebc..bea61cd 100644
--- a/public/theme-variants/v3-tabs.html
+++ b/public/theme-variants/v3-tabs.html
@@ -562,7 +562,7 @@
         <div class="sub" style="background:var(--trade)">
           <h4>Trade tools</h4>
           <div class="tile-btns">
-            <a class="tile-btn" href="/design/${id}/tearsheet" target="_blank">Tear sheet PDF</a>
+            <a class="tile-btn" href="/design/${id}/tearsheet" target="_blank" rel="noopener noreferrer">Tear sheet PDF</a>
             <a class="tile-btn" href="#" onclick="openTrade();return false">Email client</a>
             <a class="tile-btn" href="#" onclick="openTrade();return false">Compare 2</a>
             <a class="tile-btn" href="#" onclick="saveDesign(${id});return false">Save</a>
@@ -659,7 +659,7 @@
     '<div class="tile-btns" style="margin-top:8px"><a class="tile-btn" href="#">Residential</a><a class="tile-btn" href="#">Commercial</a><a class="tile-btn" href="#">Hospitality</a><a class="tile-btn" href="#">Healthcare</a></div>';
   document.getElementById('m-trade-body').innerHTML =
     '<p>Trade-only tools (interior designers + specifiers).</p>' +
-    '<div class="tile-btns" style="margin-top:8px"><a class="tile-btn" href="/design/' + id + '/tearsheet" target="_blank">Tear sheet PDF</a><a class="tile-btn" href="#">Email to client</a><a class="tile-btn" href="#">Compare 2 patterns</a><a class="tile-btn" href="#">Add to saved</a></div>';
+    '<div class="tile-btns" style="margin-top:8px"><a class="tile-btn" href="/design/' + id + '/tearsheet" target="_blank" rel="noopener noreferrer">Tear sheet PDF</a><a class="tile-btn" href="#">Email to client</a><a class="tile-btn" href="#">Compare 2 patterns</a><a class="tile-btn" href="#">Add to saved</a></div>';
 })();
 
 function openShare() { document.getElementById('m-share').showModal(); }
diff --git a/public/theme-variants/v4-hero.html b/public/theme-variants/v4-hero.html
index 8bfb89a..3f309e0 100644
--- a/public/theme-variants/v4-hero.html
+++ b/public/theme-variants/v4-hero.html
@@ -491,7 +491,7 @@
     <summary>Trade tools <span class="hint">Tear · Email · Compare · Save</span></summary>
     <div class="acc-body">
       <div class="tile-btns">
-        <a class="tile-btn" href="/design/${id}/tearsheet" target="_blank">Tear sheet</a>
+        <a class="tile-btn" href="/design/${id}/tearsheet" target="_blank" rel="noopener noreferrer">Tear sheet</a>
         <a class="tile-btn" href="#" onclick="openTrade();return false">Email client</a>
         <a class="tile-btn" href="#" onclick="openTrade();return false">Compare</a>
         <a class="tile-btn" href="#" onclick="saveDesign(${id});return false">Save</a>
@@ -579,7 +579,7 @@
     '<div class="tile-btns" style="margin-top:8px"><a class="tile-btn" href="#">Residential</a><a class="tile-btn" href="#">Commercial</a><a class="tile-btn" href="#">Hospitality</a><a class="tile-btn" href="#">Healthcare</a></div>';
   document.getElementById('m-trade-body').innerHTML =
     '<p>Trade-only tools (interior designers + specifiers).</p>' +
-    '<div class="tile-btns" style="margin-top:8px"><a class="tile-btn" href="/design/' + id + '/tearsheet" target="_blank">Tear sheet PDF</a><a class="tile-btn" href="#">Email to client</a><a class="tile-btn" href="#">Compare 2 patterns</a><a class="tile-btn" href="#">Add to saved</a></div>';
+    '<div class="tile-btns" style="margin-top:8px"><a class="tile-btn" href="/design/' + id + '/tearsheet" target="_blank" rel="noopener noreferrer">Tear sheet PDF</a><a class="tile-btn" href="#">Email to client</a><a class="tile-btn" href="#">Compare 2 patterns</a><a class="tile-btn" href="#">Add to saved</a></div>';
 })();
 
 function openShare() { document.getElementById('m-share').showModal(); }
diff --git a/public/theme-variants/v5-vertical.html b/public/theme-variants/v5-vertical.html
index 88f8454..f813035 100644
--- a/public/theme-variants/v5-vertical.html
+++ b/public/theme-variants/v5-vertical.html
@@ -346,7 +346,7 @@
 
   <details class="acc"><summary>Trade tools <span class="hint">Tear · Email · Compare</span></summary>
     <div class="acc-body"><div class="tile-btns">
-      <a class="tile-btn" href="/design/${id}/tearsheet" target="_blank">Tear sheet</a><a class="tile-btn" href="#" onclick="openTrade();return false">Email</a><a class="tile-btn" href="#" onclick="openTrade();return false">Compare</a><a class="tile-btn" href="#" onclick="saveDesign(${id});return false">Save</a>
+      <a class="tile-btn" href="/design/${id}/tearsheet" target="_blank" rel="noopener noreferrer">Tear sheet</a><a class="tile-btn" href="#" onclick="openTrade();return false">Email</a><a class="tile-btn" href="#" onclick="openTrade();return false">Compare</a><a class="tile-btn" href="#" onclick="saveDesign(${id});return false">Save</a>
     </div></div>
   </details>
 
@@ -404,7 +404,7 @@
 
   document.getElementById('m-share-body').innerHTML = '<p>Share <b>' + escapeHtml(title) + '</b></p><div class="tile-btns"><a class="tile-btn" href="#">Pinterest</a><a class="tile-btn" href="#">Instagram</a><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a></div>';
   document.getElementById('m-furnish-body').innerHTML = '<p>Generate AI furnishings.</p><div class="tile-btns"><a class="tile-btn" href="#">Residential</a><a class="tile-btn" href="#">Commercial</a><a class="tile-btn" href="#">Hospitality</a><a class="tile-btn" href="#">Healthcare</a></div>';
-  document.getElementById('m-trade-body').innerHTML = '<p>Trade tools.</p><div class="tile-btns"><a class="tile-btn" href="/design/' + id + '/tearsheet" target="_blank">Tear sheet</a><a class="tile-btn" href="#">Email</a><a class="tile-btn" href="#">Compare</a><a class="tile-btn" href="#">Save</a></div>';
+  document.getElementById('m-trade-body').innerHTML = '<p>Trade tools.</p><div class="tile-btns"><a class="tile-btn" href="/design/' + id + '/tearsheet" target="_blank" rel="noopener noreferrer">Tear sheet</a><a class="tile-btn" href="#">Email</a><a class="tile-btn" href="#">Compare</a><a class="tile-btn" href="#">Save</a></div>';
 })();
 
 function openShare() { document.getElementById('m-share').showModal(); }
diff --git a/public/theme-variants/v6-drawer.html b/public/theme-variants/v6-drawer.html
index 65cd02a..0e9088b 100644
--- a/public/theme-variants/v6-drawer.html
+++ b/public/theme-variants/v6-drawer.html
@@ -376,7 +376,7 @@
 
   document.getElementById('d-furnish-body').innerHTML = `<p style="font-size:13px; color:var(--drawer-mute); margin:0 0 14px">Generate AI furnishings curated to this palette.</p><div class="tile-btns"><a class="tile-btn" href="#">Residential</a><a class="tile-btn" href="#">Commercial</a><a class="tile-btn" href="#">Hospitality</a><a class="tile-btn" href="#">Healthcare</a></div>`;
 
-  document.getElementById('d-trade-body').innerHTML = `<div class="tile-btns"><a class="tile-btn" href="/design/${id}/tearsheet" target="_blank">Tear sheet PDF</a><a class="tile-btn" href="#">Email client</a><a class="tile-btn" href="#">Compare 2</a><a class="tile-btn" href="#" onclick="saveDesign(${id});event.preventDefault()">Save</a></div>`;
+  document.getElementById('d-trade-body').innerHTML = `<div class="tile-btns"><a class="tile-btn" href="/design/${id}/tearsheet" target="_blank" rel="noopener noreferrer">Tear sheet PDF</a><a class="tile-btn" href="#">Email client</a><a class="tile-btn" href="#">Compare 2</a><a class="tile-btn" href="#" onclick="saveDesign(${id});event.preventDefault()">Save</a></div>`;
 
   document.getElementById('d-ratings-body').innerHTML = `<div style="font-size:30px; letter-spacing:6px; color:#e8c66e">★★★★☆</div><div style="font-size:12px; color:var(--drawer-mute); margin-top:6px">Curated · 4.2 · ${escapeHtml(design.user_vote_count || 0)} votes</div>`;
 
diff --git a/public/theme-variants/v7-editorial.html b/public/theme-variants/v7-editorial.html
index dfb4072..a5c2ceb 100644
--- a/public/theme-variants/v7-editorial.html
+++ b/public/theme-variants/v7-editorial.html
@@ -278,7 +278,7 @@
         <a href="#interrupt">See at scale ↓</a>
         <a href="#" onclick="openShare();return false">Share this design</a>
         <a href="#" onclick="openFurnish();return false">Furnish with this</a>
-        <a href="/design/${id}/tearsheet" target="_blank">Tear sheet PDF</a>
+        <a href="/design/${id}/tearsheet" target="_blank" rel="noopener noreferrer">Tear sheet PDF</a>
       </div>
     </aside>
   </div>
@@ -338,7 +338,7 @@
 
   document.getElementById('m-share-body').innerHTML = '<p>Share <b>' + escapeHtml(title) + '</b></p><div class="tile-btns"><a class="tile-btn" href="#">Pinterest</a><a class="tile-btn" href="#">Instagram</a><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a></div>';
   document.getElementById('m-furnish-body').innerHTML = '<p>Generate AI furnishings.</p><div class="tile-btns"><a class="tile-btn" href="#">Residential</a><a class="tile-btn" href="#">Commercial</a><a class="tile-btn" href="#">Hospitality</a><a class="tile-btn" href="#">Healthcare</a></div>';
-  document.getElementById('m-trade-body').innerHTML = '<p>Trade tools.</p><div class="tile-btns"><a class="tile-btn" href="/design/' + id + '/tearsheet" target="_blank">Tear sheet</a><a class="tile-btn" href="#">Email</a><a class="tile-btn" href="#">Compare</a><a class="tile-btn" href="#">Save</a></div>';
+  document.getElementById('m-trade-body').innerHTML = '<p>Trade tools.</p><div class="tile-btns"><a class="tile-btn" href="/design/' + id + '/tearsheet" target="_blank" rel="noopener noreferrer">Tear sheet</a><a class="tile-btn" href="#">Email</a><a class="tile-btn" href="#">Compare</a><a class="tile-btn" href="#">Save</a></div>';
 })();
 
 function openShare() { document.getElementById('m-share').showModal(); }
diff --git a/public/theme-variants/v8-spec.html b/public/theme-variants/v8-spec.html
index a67cbf2..705ddb9 100644
--- a/public/theme-variants/v8-spec.html
+++ b/public/theme-variants/v8-spec.html
@@ -300,7 +300,7 @@
     </details>
 
     <details class="acc"><summary>Share · Furnish · Trade <span class="hint">3 actions</span></summary>
-      <div class="acc-body"><div class="tile-btns"><a class="tile-btn" href="#" onclick="openShare();return false">Share</a><a class="tile-btn" href="#" onclick="openFurnish();return false">Furnish</a><a class="tile-btn" href="/design/${id}/tearsheet" target="_blank">Tear sheet</a><a class="tile-btn" href="#">Email client</a><a class="tile-btn" href="#">Save</a></div></div>
+      <div class="acc-body"><div class="tile-btns"><a class="tile-btn" href="#" onclick="openShare();return false">Share</a><a class="tile-btn" href="#" onclick="openFurnish();return false">Furnish</a><a class="tile-btn" href="/design/${id}/tearsheet" target="_blank" rel="noopener noreferrer">Tear sheet</a><a class="tile-btn" href="#">Email client</a><a class="tile-btn" href="#">Save</a></div></div>
     </details>
 
     <details class="acc"><summary>Generator + prompt <span class="hint">wallco.ai · seed ${escapeHtml(design.seed || '—')}</span></summary>
diff --git a/public/theme-variants/v9-config.html b/public/theme-variants/v9-config.html
index f5554b7..7ae6963 100644
--- a/public/theme-variants/v9-config.html
+++ b/public/theme-variants/v9-config.html
@@ -286,7 +286,7 @@
       <h3>Sample + trade tools</h3>
       <div class="tile-btns">
         <a class="tile-btn" href="#" onclick="addSample();return false">Sample $4.25</a>
-        <a class="tile-btn" href="/design/${id}/tearsheet" target="_blank">Tear sheet</a>
+        <a class="tile-btn" href="/design/${id}/tearsheet" target="_blank" rel="noopener noreferrer">Tear sheet</a>
         <a class="tile-btn" href="#" onclick="openShare();return false">Share</a>
         <a class="tile-btn" href="#" onclick="openFurnish();return false">Furnish</a>
       </div>
diff --git a/server.js b/server.js
index 7873dfb..50463e9 100644
--- a/server.js
+++ b/server.js
@@ -14319,7 +14319,7 @@ ${(() => {
                 const j = await r.json();
                 if (!j.ok) throw new Error(j.error || 'failed');
                 const card = document.createElement('div');
-                card.innerHTML = '<a href="/design/' + j.new_id + '" target="_blank" style="display:block;text-decoration:none;color:inherit">'
+                card.innerHTML = '<a href="/design/' + j.new_id + '" target="_blank" rel="noopener noreferrer" style="display:block;text-decoration:none;color:inherit">'
                   + '<img src="' + j.new_image_url + '" style="width:100%;aspect-ratio:1;object-fit:cover;border-radius:6px;border:1px solid var(--line)">'
                   + '<div style="font:10px var(--sans);text-align:center;margin-top:4px;color:var(--ink-soft,#555)">' + label + ' · #' + j.new_id + '</div>'
                   + '</a>';

← 40ba2c3 Root fix: refresh_designs_snapshot withholds settlement-BLOC  ·  back to Wallco Ai  ·  elements: backend POST /api/admin/elements/compose — real Ge c812cf6 →