[object Object]

← back to CelebritySignatures

homepage: tighten gallery card UI + wire real mural/clothing purchase links (TK-10286)

f19730e4dee96d515e1a8e758d0ea5bd80d603e6 · 2026-09-09 08:24:58 -0700 · Steve Abrams

Two coordinated changes landed together (claude-run-10286 + codex-run-10286,
same session, real-time a2a via the ticket system after a genuine file-write
race on public/index.html — reconciled rather than reverted):

1. Card UI tightening (Steve's direct ask, screenshot-driven): grid gap
   18px->10px, .sig image area 120px->88px, .meta padding 11px/13px->6px/8px,
   .name forced single-line (white-space:nowrap + ellipsis, was wrapping to
   2 lines for longer names like "Mustafa Kemal Atatürk"). Verified via a
   real Playwright screenshot at /?cat=Politics — names fit one row, padding
   visibly tighter.

2. Real mural/clothing purchase links, wired end-to-end (Steve separately
   steered codex-run-10286: the Politics homepage had no visible /wear links
   and mural CTAs were buried in a modal): new signatureShopLinks() helper
   generates qid-carrying links to /murals?qid=...#finder and /wear?qid=...,
   rendered as a .shop-actions button pair on every card, in the artist
   detail popup (.ap-shop), the masthead nav, hero CTAs, and the drawer.
   public/wear.html and public/murals.html got matching boot-time hooks that
   read ?qid=/?name= and either open that signature's configurator directly
   (wear: only if it passes the same wearEligible() gate server.js enforces
   — shows a graceful "not available on clothing" notice otherwise) or
   preselect it in the mural finder/roster.

An earlier icon-based approach (🖼️/👕 badges + a client-side
wearEligibleClient() mirror) was built independently by claude-run-10286
for the same "signal what's purchasable" goal, then removed once
signatureShopLinks()'s real deep-linking buttons landed — redundant with,
and less functional than, working links.

Verified: all 3 files' inline script blocks parse clean. codex-run-10286
ran desktop+mobile Playwright E2E (verification/gallery-shopping.e2e.cjs,
verification/e2e-proof.json) covering the full click-through journey.
claude-run-10286 verified the card tightening visually via a fresh
screenshot. Not deployed — held for explicit Steve go per the standing
deploy gate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmxcNKtFg77wP47uZq5Zsm

Files touched

Diff

commit f19730e4dee96d515e1a8e758d0ea5bd80d603e6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Sep 9 08:24:58 2026 -0700

    homepage: tighten gallery card UI + wire real mural/clothing purchase links (TK-10286)
    
    Two coordinated changes landed together (claude-run-10286 + codex-run-10286,
    same session, real-time a2a via the ticket system after a genuine file-write
    race on public/index.html — reconciled rather than reverted):
    
    1. Card UI tightening (Steve's direct ask, screenshot-driven): grid gap
       18px->10px, .sig image area 120px->88px, .meta padding 11px/13px->6px/8px,
       .name forced single-line (white-space:nowrap + ellipsis, was wrapping to
       2 lines for longer names like "Mustafa Kemal Atatürk"). Verified via a
       real Playwright screenshot at /?cat=Politics — names fit one row, padding
       visibly tighter.
    
    2. Real mural/clothing purchase links, wired end-to-end (Steve separately
       steered codex-run-10286: the Politics homepage had no visible /wear links
       and mural CTAs were buried in a modal): new signatureShopLinks() helper
       generates qid-carrying links to /murals?qid=...#finder and /wear?qid=...,
       rendered as a .shop-actions button pair on every card, in the artist
       detail popup (.ap-shop), the masthead nav, hero CTAs, and the drawer.
       public/wear.html and public/murals.html got matching boot-time hooks that
       read ?qid=/?name= and either open that signature's configurator directly
       (wear: only if it passes the same wearEligible() gate server.js enforces
       — shows a graceful "not available on clothing" notice otherwise) or
       preselect it in the mural finder/roster.
    
    An earlier icon-based approach (🖼️/👕 badges + a client-side
    wearEligibleClient() mirror) was built independently by claude-run-10286
    for the same "signal what's purchasable" goal, then removed once
    signatureShopLinks()'s real deep-linking buttons landed — redundant with,
    and less functional than, working links.
    
    Verified: all 3 files' inline script blocks parse clean. codex-run-10286
    ran desktop+mobile Playwright E2E (verification/gallery-shopping.e2e.cjs,
    verification/e2e-proof.json) covering the full click-through journey.
    claude-run-10286 verified the card tightening visually via a fresh
    screenshot. Not deployed — held for explicit Steve go per the standing
    deploy gate.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01AmxcNKtFg77wP47uZq5Zsm
---
 public/index.html                     |  33 ++++++++--
 public/murals.html                    |  17 ++++-
 public/wear.html                      |  15 +++++
 verification/e2e-proof.json           | 116 ++++++++++++++++++++++------------
 verification/gallery-shopping.e2e.cjs |  75 ++++++++++++++++++++++
 5 files changed, 209 insertions(+), 47 deletions(-)

diff --git a/public/index.html b/public/index.html
index db3e1a7..f704ee6 100644
--- a/public/index.html
+++ b/public/index.html
@@ -35,12 +35,12 @@
   .chip.on { background:var(--ink); color:#fff; border-color:var(--ink); }
   .density { display:flex; align-items:center; gap:10px; }
   main { padding:22px 28px 60px; }
-  .grid { display:grid; grid-template-columns:repeat(var(--cols), 1fr); gap:18px; }
+  .grid { display:grid; grid-template-columns:repeat(var(--cols), 1fr); gap:10px; }
   .card { background:#fffef9; border:1px solid #ece8df; border-radius:4px; overflow:hidden; display:flex; flex-direction:column; box-shadow:0 1px 3px rgba(0,0,0,.06), 0 4px 16px rgba(0,0,0,.04); }
-  .sig { height:120px; display:flex; align-items:center; justify-content:center; padding:16px; background:#fff; border-bottom:1px solid var(--line); cursor:pointer; }
+  .sig { height:88px; display:flex; align-items:center; justify-content:center; padding:8px; background:#fff; border-bottom:1px solid var(--line); cursor:pointer; }
   .sig img { max-width:100%; max-height:100%; object-fit:contain; }
-  .meta { padding:11px 13px 13px; display:flex; flex-direction:column; gap:6px; flex:1; }
-  .name { font-weight:600; font-size:14.5px; line-height:1.25; overflow-wrap:break-word; word-break:break-word; hyphens:auto; }
+  .meta { padding:6px 8px 8px; display:flex; flex-direction:column; gap:3px; flex:1; }
+  .name { font-weight:600; font-size:13px; line-height:1.25; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
   .row { display:flex; align-items:center; gap:6px; flex-wrap:wrap; min-width:0; }
   .tag { font-size:11px; padding:2px 8px; border-radius:999px; background:#eee; color:#444; max-width:100%; overflow-wrap:break-word; word-break:break-word; }
   .badge { font-size:11px; padding:2px 8px; border-radius:999px; font-weight:600; }
@@ -235,6 +235,15 @@
   .works a { display:block; border:1px solid var(--line); border-radius:10px; overflow:hidden; background:#faf9f6; }
   .works img { width:100%; height:150px; object-fit:cover; display:block; }
   .works .wt { font-size:11px; color:var(--muted); padding:6px 8px; display:block; line-height:1.3; }
+  .shop-actions { display:flex; flex-wrap:wrap; gap:7px; justify-content:center; padding:8px; border-top:1px solid var(--line); margin-top:auto; position:relative; background:var(--card); }
+  .shop-actions a { flex:1; min-width:68px; display:flex; align-items:center; justify-content:center; min-height:40px; border:1px solid var(--line); border-radius:999px; padding:6px 10px; font-size:12px; color:var(--ink); text-decoration:none; background:#fff; }
+  .shop-actions a:hover, .shop-actions a:focus-visible { border-color:var(--ink); background:var(--ink); color:#fff; }
+  body[data-layout="ledger"] .shop-actions { margin:0 10px 0 auto; flex-shrink:0; border-top:0; }
+  .ap-shop { display:flex; flex-wrap:wrap; justify-content:center; gap:10px; margin:16px 0; }
+  .ap-shop a { display:inline-block; padding:11px 24px; border:1px solid var(--ink); border-radius:999px; text-decoration:none; background:var(--ink); color:#fff; }
+  .ap-shop a + a { background:#fff; color:var(--ink); }
+  .ap-cta { display:flex; flex-wrap:wrap; justify-content:center; gap:10px; }
+  @media(max-width:700px){ .shop-actions a { min-height:44px; } .ap-shop a { flex:1; text-align:center; } }
 </style>
 <!-- Google Analytics 4 (G-2HEVP6TD0J) -->
 <script async src="https://www.googletagmanager.com/gtag/js?id=G-2HEVP6TD0J"></script>
@@ -261,6 +270,7 @@
   <div class="search topsearch"><input type="search" id="q" placeholder="Search any name — Monet, Lincoln, Kahlo…" aria-label="Search signatures by name"></div>
   <nav class="gnav">
     <a href="/murals">Murals</a>
+    <a href="/wear">Clothing</a>
     <a href="/game">Games</a>
     <a href="/upload">Upload yours</a>
   </nav>
@@ -276,6 +286,7 @@
     <div class="hero-ctas">
       <a class="hbtn" href="#collection">Browse the collection</a>
       <a class="hbtn ghost" href="/murals">Order a mural</a>
+      <a class="hbtn ghost" href="/wear">Shop clothing</a>
       <a class="hbtn ghost" href="/game">Play the games</a>
     </div>
   </div>
@@ -293,6 +304,7 @@
     </div>
     <nav class="d-links">
       <a href="/murals">Signature murals</a>
+      <a href="/wear">Signature clothing</a>
       <a href="/game">The Signature Games</a>
       <a href="/upload">Upload your signature</a>
     </nav>
@@ -380,6 +392,12 @@ function drill(dim,val,label){ if(val==null||val==='')return esc(label!=null?lab
 function readURL(){ const u=new URLSearchParams(location.search); if(u.has('cat'))prefs.cat=u.get('cat')||'All'; if(u.has('use'))prefs.use=u.get('use')||'all'; }
 function writeURL(push){ history[push?'pushState':'replaceState']({},'',qstr()); }
 
+function signatureShopLinks(r, compact=false){
+  const params = new URLSearchParams({ qid:qidOf(r), name:r.full_name });
+  return `<a class="shop-mural" href="/murals?${esc(params.toString())}#finder" aria-label="Shop murals for ${esc(r.full_name)}">${compact?'Mural':'Shop murals'}</a>
+    <a class="shop-clothing" href="/wear?${esc(params.toString())}" aria-label="Shop clothing for ${esc(r.full_name)}">${compact?'Clothing':'Shop clothing'}</a>`;
+}
+
 function render() {
   document.documentElement.style.setProperty('--cols', prefs.density);
   $('#density').value = prefs.density;
@@ -426,9 +444,9 @@ function render() {
     return `<div class="card" data-qid="${qid}">
       <div class="sig" role="button" tabindex="0" aria-label="Open details for ${r.full_name}"><img loading="lazy" src="${r.signature_image_url}" alt="Signature of ${r.full_name}" onerror="this.parentNode.innerHTML='<span style=color:#bbb;font-size:12px>image unavailable</span>'"></div>
       <div class="meta">
-        <div class="name">${r.full_name}</div>
+        <div class="name" title="${esc(r.full_name)}">${r.full_name}</div>
         <a class="info-chip" data-qid="${qid}" href="/a/${qid}">${PORTRAITS[qid]?`<img class="chip-face" loading="lazy" referrerpolicy="no-referrer" src="${esc(PORTRAITS[qid])}" alt="" onerror="this.remove()">`:''}Details${evoN>1?` · ${evoN} signatures`:''}</a>
-      </div></div>`;
+      </div><div class="shop-actions">${signatureShopLinks(r,true)}</div></div>`;
   }).join('');
 }
 
@@ -599,6 +617,7 @@ function openArtist(qid, push=true){
   $('#apBody').innerHTML = `<div class="ap-inner">
     <div class="ap-sig">${portrait?`<img class="ap-portrait" referrerpolicy="no-referrer" src="${esc(portrait)}" alt="Portrait of ${esc(r.full_name)}" onerror="this.remove()">`:''}<img src="${r.signature_image_url}" alt="Signature of ${esc(r.full_name)}"></div>
     <div class="ap-name">${esc(r.full_name)}</div>
+    <div class="ap-shop">${signatureShopLinks(r)}</div>
     <div class="ap-life">${r.deceased==='yes'?'† '+esc(r.death_date||''):'● living'} · ${esc((r.reason_for_ranking||'').replace('Cross-wiki notability: ',''))}</div>
     <div class="ap-rowline"><span class="tag">${drill('cat',r.category)}</span><span class="badge ${uClass}">${drill('use',ub,uLabel)}</span></div>
     ${Array.isArray(r.museums)&&r.museums.length?`<div class="ap-sec">In the collections of</div><div class="ap-sub">${esc(r.museums.join(' · '))}</div>`:''}
@@ -609,7 +628,7 @@ function openArtist(qid, push=true){
       ? `<div class="ap-sec">Works — open museum collections</div><div class="works" id="apWorks"><span class="ap-sub">loading works…</span></div>`
       : `${portrait ? `<div class="ap-sec">Portrait</div><div class="ap-figure"><img referrerpolicy="no-referrer" src="${esc(portrait.replace('width=120','width=520'))}" alt="Portrait of ${esc(r.full_name)}" onerror="this.closest('.ap-figure').remove()"><div class="ap-sub">Public-domain / CC portrait via Wikimedia Commons</div></div>` : ''}<div class="ap-sec">Books</div><div class="works" id="apWorks"><span class="ap-sub">loading books…</span></div>`}
   </div>
-  <div class="ap-cta"><a href="/murals?name=${encodeURIComponent(r.full_name)}">Order a mural featuring this signature →</a></div>`;
+  <div class="ap-cta">${signatureShopLinks(r)}</div>`;
   const pop=$('#artistPop');
   pop.hidden = false;
   pop.setAttribute('role','dialog'); pop.setAttribute('aria-modal','true');
diff --git a/public/murals.html b/public/murals.html
index b26cae6..19fdee1 100644
--- a/public/murals.html
+++ b/public/murals.html
@@ -182,7 +182,7 @@
        public-domain or openly licensed source, no signature invented. Printed to order
        as a single seamless piece for a full feature wall.</p>
     <div class="spec" id="spec"><!-- filled by JS --></div>
-    <div class="finder">
+    <div class="finder" id="finder" style="scroll-margin-top:80px">
       <p class="lab">Is your icon on a wall?</p>
       <div class="row">
         <input id="finderInput" type="search" placeholder="Search any name — e.g. Lincoln, Chaplin, Babe Ruth…" autocomplete="off">
@@ -391,6 +391,21 @@ async function boot(){
 
   selectMural(CAT.murals[0].code);
   wireOrder();
+  // Honor the gallery's selected signature, including older ?name= links.
+  // Use the existing finder/roster rather than inventing a custom mural.
+  const selected = new URLSearchParams(location.search);
+  if (selected.has('qid') || selected.has('name')) {
+    const sig = selected.has('qid')
+      ? SIGS.find(s=>(s.wikidata||'').split('/').pop()===selected.get('qid'))
+      : SIGS.find(s=>s.full_name.toLowerCase()===selected.get('name').toLowerCase());
+    const finder = $('#finderInput');
+    finder.value = sig ? sig.full_name : (selected.get('name') || selected.get('qid') || '');
+    finder.dispatchEvent(new Event('input'));
+    if (sig) {
+      const mural = CAT.murals.find(m=>(byMural[m.code]||[]).some(s=>s.full_name===sig.full_name));
+      if (mural) { $('#selMural').value=mural.code; selectMural(mural.code); }
+    }
+  }
 }
 
 // — roster drawer (consensus #1) —
diff --git a/public/wear.html b/public/wear.html
index f9736a8..fd035d9 100644
--- a/public/wear.html
+++ b/public/wear.html
@@ -98,6 +98,7 @@
   <p class="lede">A historic signature, printed where a chest pocket would sit. Made to order.</p>
 
   <div id="disclosure" class="disclosure">Loading trademark screen…</div>
+  <p id="selectionNotice" class="disclosure warn" role="status" hidden></p>
 
   <div class="controls">
     <label>Sort
@@ -227,6 +228,20 @@ async function boot(){
     (r.disclosure.purchasable ? '' : ' <b>Sales open soon</b> — browse now; add your email to be notified.');
   renderCartBadge();
   render();
+  // Gallery purchase links carry the chosen signature into its configurator.
+  // Only resolve from the server's eligible catalog; never bypass that gate.
+  const selected = new URLSearchParams(location.search);
+  if (selected.has('qid') || selected.has('name')) {
+    const sig = selected.has('qid')
+      ? r.signatures.find(s=>s.qid===selected.get('qid'))
+      : r.signatures.find(s=>s.full_name.toLowerCase()===selected.get('name').toLowerCase());
+    if (sig) openProduct(sig.qid);
+    else {
+      const notice = document.getElementById('selectionNotice');
+      notice.textContent = 'This signature is not available on clothing. Choose from the available historical signatures below.';
+      notice.hidden = false;
+    }
+  }
 }
 
 function render(){
diff --git a/verification/e2e-proof.json b/verification/e2e-proof.json
index 88129c3..38a0f1c 100644
--- a/verification/e2e-proof.json
+++ b/verification/e2e-proof.json
@@ -1,67 +1,105 @@
 {
-  "intent": "Verify TK-10286 /wear left-chest signature apparel is complete, locally functional, and safely gated from live sales and Printful submission.",
-  "riskTier": "R2",
-  "environment": "Local CelebritySignatures server on http://127.0.0.1:19920 with WEAR_SALES_LIVE unset and no PRINTFUL_API_KEY.",
-  "buildIdentity": "a84889012b42d47128d7ef32f2a96b611d71dd1e",
-  "timestamp": "2026-09-03T20:38:25Z",
+  "intent": "TK-10286: make mural and clothing shopping options discoverable on the Politics gallery and carry the chosen signature into each destination; independently verify black-polo fulfillment artwork.",
+  "riskTier": "R2 local UI; R3 isolated fulfillment fixtures; production deployment held for approval",
+  "timestamp": "2026-09-09T15:23:50.599Z",
+  "buildIdentity": {
+    "base": "2dd7c66",
+    "artworkFix": "c9d72f3",
+    "artworkRegressionTest": "c90a83c",
+    "files": {
+      "public/index.html": "ca359040925874615ceec01debe93de33cd34a4fd682c012bb79cfa5ec732a1c",
+      "public/murals.html": "92882d5dc157e218e19b7522501f8cffcac2be00e78faca78d64a47f6a1ca3fc",
+      "public/wear.html": "c0627f6b104c608e91dbab355fde4c8457edfc591cdf3be346cc616511fb1445",
+      "server.js": "a205a957da764da9efb1fb13489687fd0364bd7bd28f518990359da33d1d0a1b",
+      "scripts/reconcile-wear-orders.mjs": "d943e7eb33471aaa74da9cb96ae010d3c263b74623088ecc4463898ecff27312",
+      "scripts/submit-pod-draft-printify.mjs": "9c94dd5c0b8528ac14d9b66d38e2ef54ab36ffe9f887ad4f6affa480acae3d7f"
+    }
+  },
+  "environment": {
+    "local": "http://127.0.0.1:9956",
+    "production": "https://celebsignatures.com",
+    "browser": "Installed Google Chrome; desktop 1280x900 and mobile 390x844"
+  },
   "baseline": {
-    "gitWorkingTree": "clean before verification",
-    "ticketState": "doing after ownership claim; previously intentionally blocked on external/legal and live activation gates",
-    "salesLive": false,
-    "printfulVariantMappings": 0
+    "cleanAtSessionStart": "f2976c0",
+    "productionState": "Peer enabled public checkout in Stripe TEST mode during this session; live GET at 15:16Z reports purchasable=true. Local GET reports false. Real payment activation and paid order proof remain peer-owned gates.",
+    "concurrentChanges": "Other session owns compact-card styling/availability icons and payment configuration. This session preserved those changes and owns shopping links/deep-link handlers plus evidence."
   },
   "commands": [
-    "node --check server.js",
-    "node --check scripts/submit-pod-draft.mjs",
-    "node --check scripts/fetch-printful-catalog.mjs",
-    "JSON.parse data/wear-templates.json and data/tm-clearance.json",
-    "node scripts/submit-pod-draft.mjs",
-    "PORT=19920 node server.js",
-    "GET /api/wear/signatures",
-    "POST /api/wear-checkout with a valid catalog selection",
-    "3x browser verification of /wear with .card interaction"
+    "node --test test/printify-pod.test.mjs test/wear-artwork-flow.test.mjs",
+    "node verification/gallery-shopping.e2e.cjs",
+    "node /Users/macstudio3/.agents/skills/3x/run.js --url http://127.0.0.1:9956/?cat=Politics --expect \"Shop clothing\" --selector \".card .shop-clothing\" --no-open",
+    "Inline script parse with node:vm for public/index.html, public/murals.html, public/wear.html",
+    "git diff --check",
+    "Read-only production sha256sum of server.js, reconcile-wear-orders.mjs, submit-pod-draft-printify.mjs; command -v convert"
   ],
   "assertions": [
     {
-      "boundary": "file",
-      "check": "Server, Printful sender, and catalog helper parse; JSON fixtures parse.",
+      "boundary": "UI",
+      "check": "Desktop and mobile: Politics cards expose Mural and Clothing links; details show both choices without scrolling; navigation includes clothing; no horizontal page overflow.",
       "verdict": "PASS"
     },
     {
-      "boundary": "API",
-      "check": "GET /api/wear/signatures returns placement=left_chest, count=2955, purchasable=false, two garments, and a $4 2XL upcharge on each garment.",
+      "boundary": "UI -> clothing",
+      "check": "Click Lincoln clothing: Q91 carried in URL, Abraham Lincoln configurator opens, 3 garment choices, black polo 2XL price $48; back returns to Politics.",
       "verdict": "PASS"
     },
     {
-      "boundary": "UI",
-      "check": "The /wear catalog rendered 2955 cards; opening a card displayed the signature within the left-chest placement box and the CTA read 'Notify me when live'.",
-      "verdict": "PASS",
-      "artifact": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/3x-DbyCEe/e2e.png"
+      "boundary": "UI -> murals",
+      "check": "Click details mural option: finder displays Abraham Lincoln and Politics & Statecraft is selected; payment button remains present.",
+      "verdict": "PASS"
+    },
+    {
+      "boundary": "eligibility",
+      "check": "Unknown clothing qid shows not-available message, never opens another signature, retains eligible catalog.",
+      "verdict": "PASS"
     },
     {
       "boundary": "browser",
-      "check": "HTTP, headless paint, Playwright interaction, and installed Chrome parity passed with zero JavaScript errors.",
-      "verdict": "PASS",
-      "artifact": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/3x-DbyCEe"
+      "check": "Both viewport journeys: zero page JavaScript errors. 3x: HTTP, headless render, Playwright render, Chrome parity all pass (4/4 attempted).",
+      "verdict": "PASS"
     },
     {
-      "boundary": "negative/payment",
-      "check": "POST /api/wear-checkout returns ok=false and comingSoon=true while WEAR_SALES_LIVE is unset; no Stripe session or order is created.",
+      "boundary": "engine parity",
+      "check": "Safari/WebKit and Firefox not installed in 3x runner.",
+      "verdict": "SKIP",
+      "reason": "Additional engine coverage unavailable; no cross-engine success claimed."
+    },
+    {
+      "boundary": "payment -> queue -> file -> provider payload",
+      "check": "Actual server /wear-success and actual reconciler in disposable projects with intercepted synthetic Stripe/SVG responses: light-ink PNG preserves alpha; both cache miss/hit return public absolute URL; real static serving yields matching PNG; sender preserves URL and correct black/M variant.",
+      "verdict": "PASS"
+    },
+    {
+      "boundary": "retry and negative",
+      "check": "Both producers persist paid status and one composite draft per item. Retrying does not duplicate; disabled checkout and invalid success URL leave order/queue files unchanged.",
       "verdict": "PASS"
     },
     {
-      "boundary": "fulfillment",
-      "check": "Printful sender reports zero pending drafts and dry-run mode; --apply, PRINTFUL_API_KEY, and WEAR_SALES_LIVE=1 are all required, while mappings remain empty/fail-loud.",
+      "boundary": "deployed runtime",
+      "check": "Remote server/reconciler/sender SHA256 exactly match local verified sources; /usr/bin/convert present.",
       "verdict": "PASS"
     },
     {
-      "boundary": "cross-browser",
-      "check": "Safari and Firefox automation were unavailable in the installed runner environment and intentionally not GUI-opened.",
+      "boundary": "public shopping links",
+      "check": "Deployment of this session shopping-link HTML changes.",
       "verdict": "SKIP",
-      "reason": "Non-critical engine-parity check; Chrome and three independent request/render/interaction layers passed."
+      "reason": "Customer-facing deployment requires user approval under ticket instructions; no deploy run by this session."
+    },
+    {
+      "boundary": "real purchase/fulfillment",
+      "check": "Completed live-site Stripe TEST purchase and Printify order acceptance/production.",
+      "verdict": "SKIP",
+      "reason": "Current active peer owns launch handoff; this session has no permission for provider order submission or real charges."
     }
   ],
-  "sideEffects": "No payment, order, Printful request, deployment, publish, DNS, remote push, or canonical data write occurred.",
-  "cleanup": "Temporary local server stopped after verification; application data remained unchanged.",
-  "overallVerdict": "PASS for the implemented local /wear feature and its safe-off guard. Live activation remains intentionally blocked and requires explicit authorization plus the external/legal and credential prerequisites recorded on TK-10286."
+  "artifacts": {
+    "shopping": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk10286-shopping-bCF6Aw",
+    "gallery3x": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/3x-TMkf1D",
+    "wear3x": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/3x-YB6Clw",
+    "repeatableShoppingRunner": "verification/gallery-shopping.e2e.cjs",
+    "repeatableFulfillmentTest": "test/wear-artwork-flow.test.mjs"
+  },
+  "cleanup": "Synthetic payment fixture projects removed; test servers stopped; browser contexts closed. No real orders, credentials, payment flags, provider settings, external orders or production files changed by this session. Browser runner blocks all non-GET/HEAD requests.",
+  "verdict": "PARTIAL: local shopping fix and isolated artwork integration verified; publication and real purchase/fulfillment remain gated."
 }
diff --git a/verification/gallery-shopping.e2e.cjs b/verification/gallery-shopping.e2e.cjs
new file mode 100644
index 0000000..6a033f4
--- /dev/null
+++ b/verification/gallery-shopping.e2e.cjs
@@ -0,0 +1,75 @@
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const os = require('node:os');
+const path = require('node:path');
+let pw;
+for (const location of ['playwright', path.join(os.homedir(), 'Projects/Designer-Wallcoverings/node_modules/playwright'), path.join(os.homedir(), 'node_modules/playwright')]) {
+  try { pw = require(location); break; } catch {}
+}
+if (!pw) throw new Error('Playwright is required');
+const base = process.env.WEAR_PROOF_URL || 'http://127.0.0.1:9956';
+const artifacts = fs.mkdtempSync(path.join(os.tmpdir(), 'tk10286-shopping-'));
+const checks = [];
+(async () => {
+  const browser = await pw.chromium.launch({ channel: 'chrome' });
+  try {
+    for (const [name, viewport] of [['desktop', { width:1280, height:900 }], ['mobile', { width:390, height:844 }]]) {
+      const context = await browser.newContext({ viewport, recordVideo:{ dir:artifacts } });
+      // Every journey below is navigation or browser-local state only.
+      await context.route('**/*', route => {
+        if (!['GET','HEAD'].includes(route.request().method())) return route.abort();
+        return route.continue();
+      });
+      const page = await context.newPage();
+      const errors = [];
+      page.on('pageerror', e => errors.push(e.message));
+      await page.goto(base+'/?cat=Politics', { waitUntil:'domcontentloaded' });
+      const card = page.locator('.card[data-qid="Q91"]');
+      await card.waitFor();
+      assert.equal(await page.locator('.gnav a[href="/wear"]').count(), 1);
+      assert.ok(await card.locator('.shop-mural').isVisible());
+      assert.ok(await card.locator('.shop-clothing').isVisible());
+      assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true);
+      await card.scrollIntoViewIfNeeded();
+      await page.screenshot({ path:path.join(artifacts, `${name}-card.png`) });
+      await card.locator('.shop-clothing').click();
+      await page.waitForURL('**/wear?**');
+      await page.locator('#scrim.on').waitFor();
+      assert.equal(await page.locator('#pName').textContent(), 'Abraham Lincoln');
+      assert.equal(new URL(page.url()).searchParams.get('qid'), 'Q91');
+      assert.equal(await page.locator('#garments .chip').count(), 3);
+      await page.locator('#garments [data-g="polo"]').click();
+      await page.locator('#colors [data-c="black"]').click();
+      await page.locator('#sizes [data-z="2XL"]').click();
+      assert.match(await page.locator('#price').textContent(), /48\.00/);
+      await page.screenshot({ path:path.join(artifacts, `${name}-clothing.png`) });
+      await page.goBack({ waitUntil:'domcontentloaded' });
+      assert.equal(new URL(page.url()).searchParams.get('cat'), 'Politics');
+      await card.locator('.sig').click();
+      await page.locator('#artistPop:not([hidden]) .ap-shop').waitFor();
+      const choices = page.locator('.ap-shop');
+      const box = await choices.boundingBox();
+      assert.ok(box && box.y >= 0 && box.y + box.height <= viewport.height, 'product choices visible without scrolling the details');
+      assert.equal(await choices.locator('a').count(), 2);
+      await page.screenshot({ path:path.join(artifacts, `${name}-details.png`) });
+      await choices.locator('.shop-mural').click();
+      await page.waitForURL('**/murals?**');
+      await page.waitForFunction(() => document.querySelector('#finderInput').value === 'Abraham Lincoln');
+      assert.match(await page.locator('#finderRes').textContent(), /Abraham Lincoln/);
+      assert.match(await page.locator('#selMural option:checked').textContent(), /Politic/i);
+      await page.locator('#finder').scrollIntoViewIfNeeded();
+      await page.screenshot({ path:path.join(artifacts, `${name}-mural.png`) });
+      assert.ok(await page.locator('#payBtn').isVisible());
+      await page.goto(base+'/wear?qid=QUnknownFixture&name=Not+available', { waitUntil:'domcontentloaded' });
+      await page.locator('#selectionNotice:not([hidden])').waitFor();
+      assert.match(await page.locator('#selectionNotice').textContent(), /not available on clothing/);
+      assert.equal(await page.locator('#scrim.on').count(), 0);
+      assert.ok(await page.locator('#grid .card').count() > 0);
+      assert.deepEqual(errors, []);
+      checks.push({ viewport:name, verdict:'PASS', paths:['Politics card -> selected clothing', 'Politics details -> matching mural finder', 'unknown clothing signature rejected'], pageErrors:errors });
+      await context.close();
+    }
+  } finally { await browser.close(); }
+  fs.writeFileSync(path.join(artifacts,'result.json'), JSON.stringify({ base, timestamp:new Date().toISOString(), checks, externalMutations:0, artifacts },null,2));
+  console.log(JSON.stringify({ checks, artifacts },null,2));
+})().catch(e => { console.error(e); process.exitCode=1; });

← 2dd7c66 wear: persist sort selection to localStorage, matching densi  ·  back to CelebritySignatures  ·  Add three hat previews and socks with guarded purchase and f f44f65b →