← back to Dw Photo Capture
Front screen: one-at-a-time swipe theme picker (Figma v1-v25 + Photoshop Kid-Simple v7-v16); capture app moves to /app (TK-12343)
24bc0c33fa80044da81fa68aead0db83b67d171d · 2026-09-26 10:58:06 -0700 · Steve Abrams
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SC2DKkJkSNenmFz2Eqnvpk
Files touched
M public/captures.htmlM public/desk.htmlA public/pick.htmlM public/simple-probooth.htmlA public/themes/v07-traffic-light.htmlA public/themes/v08-big-emoji.htmlA public/themes/v09-checklist.htmlA public/themes/v1-magic-camera.htmlA public/themes/v10-scan-first.htmlA public/themes/v11-talk-to-it.htmlA public/themes/v12-bottom-tabs.htmlA public/themes/v13-photo-wall.htmlA public/themes/v14-sample-ticket.htmlA public/themes/v15-giant-words.htmlA public/themes/v16-sticky-notes.htmlA public/themes/v17-game-pad.htmlA public/themes/v18-trail-map.htmlA public/themes/v19-left-or-right.htmlA public/themes/v2-four-tiles.htmlA public/themes/v20-luxe-gold.htmlA public/themes/v21-beat-the-clock.htmlA public/themes/v22-buddy-pup.htmlA public/themes/v23-code-keypad.htmlA public/themes/v24-split-screen.htmlA public/themes/v25-paper-stack.htmlA public/themes/v3-text-buddy.htmlA public/themes/v4-level-up.htmlA public/themes/v5-swipe-deck.htmlA public/themes/v6-one-thing.htmlM public/vendor-photos.htmlM server.js
Diff
commit 24bc0c33fa80044da81fa68aead0db83b67d171d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Sep 26 10:58:06 2026 -0700
Front screen: one-at-a-time swipe theme picker (Figma v1-v25 + Photoshop Kid-Simple v7-v16); capture app moves to /app (TK-12343)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SC2DKkJkSNenmFz2Eqnvpk
---
public/captures.html | 2 +-
public/desk.html | 2 +-
public/pick.html | 223 +++++++++++++++++++++
public/simple-probooth.html | 2 +-
public/themes/v07-traffic-light.html | 148 ++++++++++++++
public/themes/v08-big-emoji.html | 133 +++++++++++++
public/themes/v09-checklist.html | 125 ++++++++++++
public/themes/v1-magic-camera.html | 360 ++++++++++++++++++++++++++++++++++
public/themes/v10-scan-first.html | 140 +++++++++++++
public/themes/v11-talk-to-it.html | 138 +++++++++++++
public/themes/v12-bottom-tabs.html | 194 ++++++++++++++++++
public/themes/v13-photo-wall.html | 201 +++++++++++++++++++
public/themes/v14-sample-ticket.html | 173 ++++++++++++++++
public/themes/v15-giant-words.html | 159 +++++++++++++++
public/themes/v16-sticky-notes.html | 160 +++++++++++++++
public/themes/v17-game-pad.html | 192 ++++++++++++++++++
public/themes/v18-trail-map.html | 163 +++++++++++++++
public/themes/v19-left-or-right.html | 160 +++++++++++++++
public/themes/v2-four-tiles.html | 348 ++++++++++++++++++++++++++++++++
public/themes/v20-luxe-gold.html | 158 +++++++++++++++
public/themes/v21-beat-the-clock.html | 210 ++++++++++++++++++++
public/themes/v22-buddy-pup.html | 153 +++++++++++++++
public/themes/v23-code-keypad.html | 172 ++++++++++++++++
public/themes/v24-split-screen.html | 162 +++++++++++++++
public/themes/v25-paper-stack.html | 158 +++++++++++++++
public/themes/v3-text-buddy.html | 322 ++++++++++++++++++++++++++++++
public/themes/v4-level-up.html | 282 ++++++++++++++++++++++++++
public/themes/v5-swipe-deck.html | 309 +++++++++++++++++++++++++++++
public/themes/v6-one-thing.html | 246 +++++++++++++++++++++++
public/vendor-photos.html | 2 +-
server.js | 22 ++-
31 files changed, 5213 insertions(+), 6 deletions(-)
diff --git a/public/captures.html b/public/captures.html
index fb273a5..073f6dc 100644
--- a/public/captures.html
+++ b/public/captures.html
@@ -39,7 +39,7 @@
</style></head>
<body>
<header>
- <h1>Capture History <span id="count" style="color:var(--muted);font-weight:400;font-size:14px"></span><a href="/">← capture</a><a href="/vendor-photos">vendor photos</a></h1>
+ <h1>Capture History <span id="count" style="color:var(--muted);font-weight:400;font-size:14px"></span><a href="/app">← capture</a><a href="/vendor-photos">vendor photos</a></h1>
<div class="controls">
<input type="search" id="q" placeholder="Search SKU, vendor, source">
<select id="sort" aria-label="Sort">
diff --git a/public/desk.html b/public/desk.html
index 3495e6f..5de3a58 100644
--- a/public/desk.html
+++ b/public/desk.html
@@ -121,7 +121,7 @@
<div class="stat" id="stat"></div>
<div class="camlinkbox">
📱 On the phone, open <code id="camUrl">/cam</code> (this same host) and tap <b>Start Camera</b>. Keep it open + the phone awake.
- <br><a class="biglink" id="camLink" href="/cam" target="_blank">Open /cam here ↗</a> · <a class="biglink" href="/" target="_blank">Full capture app ↗</a>
+ <br><a class="biglink" id="camLink" href="/cam" target="_blank">Open /cam here ↗</a> · <a class="biglink" href="/app" target="_blank">Full capture app ↗</a>
</div>
</section>
</main>
diff --git a/public/pick.html b/public/pick.html
new file mode 100644
index 0000000..444ca70
--- /dev/null
+++ b/public/pick.html
@@ -0,0 +1,223 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
+<meta name="theme-color" content="#0f0e0c">
+<link rel="manifest" href="/manifest.webmanifest">
+<link rel="apple-touch-icon" href="/apple-touch-icon.png">
+<title>DW Photo · Pick a theme</title>
+<style>
+/* Front screen (TK-12343): swipe through theme versions one at a time, pick one, or go to the app. */
+:root{--bg:#0f0e0c;--panel:#1b1a17;--line:#34312b;--ink:#f1ede4;--muted:#aaa395;--accent:#d49a5c;--accent-ink:#1a1206;--fig:#8fb4ff;--ps:#6fd1c4;--good:#7fd19b}
+*{box-sizing:border-box}
+html,body{margin:0;height:100%;background:var(--bg);color:var(--ink);font:16px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;-webkit-font-smoothing:antialiased}
+body{display:flex;flex-direction:column;height:100dvh;overflow:hidden}
+button,a.btn{font:inherit;min-height:44px;border-radius:12px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:6px;text-decoration:none}
+button:focus-visible,a:focus-visible{outline:3px solid var(--accent);outline-offset:2px}
+header{display:flex;align-items:center;gap:10px;padding:max(10px,env(safe-area-inset-top)) 16px 8px}
+.brand{font-weight:700;letter-spacing:.02em;flex:1;min-width:0}
+.brand small{display:block;font-weight:400;color:var(--muted);font-size:13px}
+.go{background:var(--accent);color:var(--accent-ink);border:0;padding:0 16px;font-weight:700;white-space:nowrap}
+.filters{display:flex;gap:8px;padding:0 16px 8px}.filters button{flex:0 0 auto}
+.filters button{background:var(--panel);color:var(--ink);border:1px solid var(--line);padding:0 14px;font-size:14px;white-space:nowrap}
+.filters button[aria-pressed=true]{border-color:var(--accent);background:#2a2118}
+.mine{margin-left:auto;align-self:center;font-size:13px;color:var(--good);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;flex:1 1 auto;text-align:right}
+.track{flex:1;display:flex;overflow-x:auto;overflow-y:hidden;scroll-snap-type:x mandatory;scrollbar-width:none;overscroll-behavior-x:contain}
+.track::-webkit-scrollbar{display:none}
+.slide{flex:0 0 100%;scroll-snap-align:center;scroll-snap-stop:always;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;padding:4px 16px;min-width:0}
+.phone{position:relative;border:4px solid #3a372f;border-radius:30px;overflow:hidden;background:#000;flex:0 0 auto}
+.phone .scaler{position:absolute;top:0;left:0;width:390px;height:780px;transform-origin:0 0}
+.phone iframe{width:390px;height:780px;border:0;display:block;pointer-events:none}
+.phone.live iframe{pointer-events:auto}
+.phone .ph{position:absolute;inset:0;display:grid;place-items:center;color:var(--muted);font-size:14px}
+.meta{text-align:center;max-width:420px}
+.src{display:inline-block;font-size:12px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;padding:2px 8px;border-radius:999px;border:1px solid currentColor}
+.src.figma{color:var(--fig)} .src.photoshop{color:var(--ps)}
+.meta h2{margin:6px 0 2px;font-size:20px}
+.meta p{margin:0;color:var(--muted);font-size:14px}
+.picked{color:var(--good);font-weight:700;font-size:14px;margin-top:4px}
+footer{display:flex;align-items:center;gap:8px;padding:8px 16px max(12px,env(safe-area-inset-bottom))}
+.nav{background:var(--panel);color:var(--ink);border:1px solid var(--line);width:46px;font-size:22px;flex:0 0 auto}
+.nav:disabled{opacity:.35;cursor:default}
+.count{flex:0 0 auto;min-width:52px;text-align:center;color:var(--muted);font-variant-numeric:tabular-nums;font-size:14px}
+.try{background:var(--panel);color:var(--ink);border:1px solid var(--line);padding:0 12px;white-space:nowrap}
+.try[aria-pressed=true]{border-color:var(--fig)}
+.pick{flex:1;background:var(--ink);color:var(--bg);border:0;font-weight:700;padding:0 10px;white-space:nowrap}
+.pick.done{background:var(--good);color:#06240f}
+.hint{position:fixed;left:50%;top:140px;transform:translateX(-50%);background:#000c;color:#fff;padding:8px 14px;border-radius:999px;font-size:13px;pointer-events:none;transition:opacity .6s}
+@media (max-width:420px){.brand small{display:none}.try{padding:0 10px}}
+</style>
+</head>
+<body>
+<header>
+ <div class="brand">DW Photo<small>Pick a theme — swipe to see more</small></div>
+ <a class="btn go" href="/app" id="goApp">Go to the app →</a>
+</header>
+<div class="filters" role="group" aria-label="Show themes from">
+ <button data-f="all" aria-pressed="true">All</button>
+ <button data-f="figma" aria-pressed="false">Figma</button>
+ <button data-f="photoshop" aria-pressed="false">Photoshop</button>
+ <span class="mine" id="mine" aria-live="polite"></span>
+</div>
+<main class="track" id="track" aria-roledescription="carousel" aria-label="Theme versions"></main>
+<footer>
+ <button class="nav" id="prev" aria-label="Previous theme">‹</button>
+ <span class="count" id="count" aria-live="polite"></span>
+ <button class="nav" id="next" aria-label="Next theme">›</button>
+ <button class="try" id="try" aria-pressed="false" title="Let taps go into the mockup">Try it</button>
+ <button class="pick" id="pick">Pick this one</button>
+</footer>
+<div class="hint" id="hint">Swipe left or right · or use the ‹ › buttons</div>
+<script>
+(function(){
+ // Figma set (TK-12343, /themes/) and Photoshop Kid-Simple set (TK-12344, /kid-simple/).
+ var FIG = [
+ ['v1','Magic Camera','v1-magic-camera.html','One giant shutter button; the app guesses which job you meant and asks you to confirm.'],
+ ['v2','Four Big Tiles','v2-four-tiles.html','Four big coloured tiles on the home screen, one per job, like app icons.'],
+ ['v3','Text Buddy','v3-text-buddy.html','The whole app is a text chat with “Sam”, who asks one thing at a time.'],
+ ['v4','Level Up','v4-level-up.html','Each job is a short game path with progress dots and confetti at the end.'],
+ ['v5','Swipe Deck','v5-swipe-deck.html','Every step is a card: tap ✓ for yes or ✗ to redo.'],
+ ['v6','One Thing At A Time','v6-one-thing.html','No menus: one big question and one to three big buttons per screen.'],
+ ['v7','Traffic Light','v07-traffic-light.html','Green, yellow, red: every screen tells you plainly whether you are good to go.'],
+ ['v8','Big Emoji','v08-big-emoji.html','Giant emoji buttons do the talking, so there is almost nothing to read.'],
+ ['v9','Checklist','v09-checklist.html','A to-do list that ticks itself off as you take the photo and pick the brand.'],
+ ['v10','Scan First','v10-scan-first.html','Open straight to the scanner; the app finds the item, you just confirm.'],
+ ['v11','Talk To It','v11-talk-to-it.html','Hold a button and say what you need; pick from what it heard.'],
+ ['v12','Bottom Tabs','v12-bottom-tabs.html','Four tabs along the bottom — New, Fix, Find, Sample — like any phone app.'],
+ ['v13','Photo Wall','v13-photo-wall.html','Your recent photos as a wall; tap one to retake or swap it.'],
+ ['v14','Sample Ticket','v14-sample-ticket.html','A sample arriving prints a ticket and gets stamped “received”.'],
+ ['v15','Giant Words','v15-giant-words.html','Huge one-word buttons — FIND, NEW, FIX — readable from arm’s length.'],
+ ['v16','Sticky Notes','v16-sticky-notes.html','A corkboard of sticky notes; snap, jot, and pin each item.'],
+ ['v17','Game Pad','v17-game-pad.html','A game-controller layout: press start, aim, choose, level clear.'],
+ ['v18','Trail Map','v18-trail-map.html','Pick a trail and walk the stops on a map to finish the job.'],
+ ['v19','Left or Right','v19-left-or-right.html','Every question is a two-way choice: left or right, yes or no.'],
+ ['v20','Luxe Gold','v20-luxe-gold.html','A quiet, gold-and-serif showroom look for grown-up staff.'],
+ ['v21','Beat the Clock','v21-beat-the-clock.html','A friendly timer turns each job into a quick challenge.'],
+ ['v22','Buddy Pup','v22-buddy-pup.html','A puppy fetches the photo, sniff-checks it, and earns a treat.'],
+ ['v23','Code Keypad','v23-code-keypad.html','Type the item code on a big phone-style keypad and match it.'],
+ ['v24','Split Screen','v24-split-screen.html','The camera is always on top; the buttons live on the bottom half.'],
+ ['v25','Paper Stack','v25-paper-stack.html','One paper sheet per step, stacked; flip to the next sheet to move on.']
+ ];
+ var PS = [
+ ['v7','Scan & Go','v7-scan-and-go.html','Point at a tag or box; a match goes to Update or Sample, no match to Add New Item.'],
+ ['v8','Mode Dial','v8-mode-dial.html','Camera home with a swipeable NEW / UPDATE / FIND / SAMPLE strip.'],
+ ['v9','Story Mode','v9-story-mode.html','Instagram-style stories: tap right to continue, left to go back.'],
+ ['v10','Today’s Inbox','v10-todays-inbox.html','A to-do list of what’s waiting, plus a camera button for Add New Item.'],
+ ['v11','Bottom Sheet','v11-bottom-sheet.html','Full-screen camera with a Maps-style pull-up sheet of the four jobs.'],
+ ['v12','Hold to Talk','v12-hold-to-talk.html','Press and hold the mic and say what you’re doing; big buttons as fallback.'],
+ ['v13','Sticker Book','v13-sticker-book.html','Take a photo, then drag the polaroid onto one of four job stickers.'],
+ ['v14','Ticket Printer','v14-ticket-printer.html','Four arcade keys; each finished job prints a little paper ticket.'],
+ ['v15','Thumb Zone','v15-thumb-zone.html','Shoot first; every control lives in the bottom third, in thumb reach.'],
+ ['v16','Widget Stack','v16-widget-stack.html','iPhone-style widgets that open in place and count down as you work.']
+ ];
+ var ALL = FIG.map(function(r){ return {key:'figma-'+r[0], src:'figma', id:r[0], name:r[1], url:'/themes/'+r[2], idea:r[3]}; })
+ .concat(PS.map(function(r){ return {key:'photoshop-'+r[0], src:'photoshop', id:r[0], name:r[1], url:'/kid-simple/'+r[2], idea:r[3]}; }));
+
+ var KEY = 'dw-photo-theme', FKEY = 'dw-photo-theme-filter';
+ function get(k){ try { return localStorage.getItem(k); } catch(e){ return null; } }
+ function set(k, v){ try { localStorage.setItem(k, v); } catch(e){} }
+ var pick = null; try { pick = JSON.parse(get(KEY) || 'null'); } catch(e){ pick = null; }
+
+ var $ = function(id){ return document.getElementById(id); };
+ var track = $('track'), list = [], cur = 0, filter = get(FKEY) || 'all', live = false;
+
+ function srcLabel(s){ return s === 'figma' ? 'Figma' : 'Photoshop'; }
+
+ function build(){
+ list = ALL.filter(function(t){ return filter === 'all' || t.src === filter; });
+ track.innerHTML = '';
+ list.forEach(function(t, i){
+ var s = document.createElement('section');
+ s.className = 'slide'; s.setAttribute('role','group'); s.setAttribute('aria-roledescription','slide');
+ s.setAttribute('aria-label', (i+1) + ' of ' + list.length + ': ' + srcLabel(t.src) + ' ' + t.id + ' ' + t.name);
+ s.innerHTML = '<div class="phone"><div class="scaler"></div><div class="ph">Loading…</div></div>' +
+ '<div class="meta"><span class="src ' + t.src + '">' + srcLabel(t.src) + ' · ' + t.id + '</span>' +
+ '<h2></h2><p></p><div class="picked" hidden>★ Your pick</div></div>';
+ s.querySelector('h2').textContent = t.name;
+ s.querySelector('p').textContent = t.idea;
+ track.appendChild(s);
+ });
+ document.querySelectorAll('.filters button').forEach(function(b){ b.setAttribute('aria-pressed', String(b.dataset.f === filter)); });
+ size();
+ var start = 0, want = (location.hash || '').slice(1) || (pick && pick.key);
+ list.forEach(function(t, i){ if (t.key === want) start = i; });
+ go(start, true);
+ }
+
+ // Fit a 390×780 mockup inside the space left for the phone, then scale it down.
+ function size(){
+ var h = Math.min(track.clientHeight - 120, 780), w = Math.min(h / 2, window.innerWidth - 40);
+ h = w * 2; var k = w / 390;
+ document.querySelectorAll('.phone').forEach(function(p){
+ p.style.width = (w + 8) + 'px'; p.style.height = (h + 8) + 'px';
+ p.querySelector('.scaler').style.transform = 'scale(' + k + ')';
+ });
+ }
+
+ // Keep only the current slide and its neighbours loaded (35 live mockups at once is too heavy for a phone).
+ function load(){
+ Array.prototype.forEach.call(track.children, function(s, i){
+ var sc = s.querySelector('.scaler'), f = sc.querySelector('iframe');
+ if (Math.abs(i - cur) <= 1) {
+ if (!f) { f = document.createElement('iframe'); f.title = list[i].name + ' mockup'; f.src = list[i].url;
+ f.onload = function(){ var ph = s.querySelector('.ph'); if (ph) ph.remove(); }; sc.appendChild(f); }
+ } else if (f) { f.remove(); if (!s.querySelector('.ph')) { var ph = document.createElement('div'); ph.className = 'ph'; ph.textContent = 'Loading…'; s.querySelector('.phone').appendChild(ph); } }
+ });
+ }
+
+ function go(i, instant){
+ cur = Math.max(0, Math.min(list.length - 1, i));
+ track.scrollTo({ left: cur * track.clientWidth, behavior: instant ? 'auto' : 'smooth' });
+ update();
+ }
+
+ function update(){
+ var t = list[cur]; if (!t) return;
+ $('count').textContent = (cur + 1) + ' / ' + list.length;
+ $('prev').disabled = cur === 0; $('next').disabled = cur === list.length - 1;
+ var isPick = !!(pick && pick.key === t.key);
+ $('pick').textContent = isPick ? '★ Picked' : 'Pick this one';
+ $('pick').classList.toggle('done', isPick);
+ Array.prototype.forEach.call(track.children, function(s, i){ s.querySelector('.picked').hidden = !(pick && list[i].key === pick.key); });
+ $('mine').textContent = pick ? 'Your pick: ' + srcLabel(pick.src) + ' ' + pick.id + ' ' + pick.name : '';
+ setLive(false);
+ history.replaceState(null, '', '#' + t.key);
+ load();
+ }
+
+ function setLive(on){
+ live = on; $('try').setAttribute('aria-pressed', String(on)); $('try').textContent = on ? 'Done trying' : 'Try it';
+ Array.prototype.forEach.call(track.children, function(s, i){ s.querySelector('.phone').classList.toggle('live', on && i === cur); });
+ }
+
+ var st;
+ track.addEventListener('scroll', function(){
+ clearTimeout(st);
+ st = setTimeout(function(){ var i = Math.round(track.scrollLeft / track.clientWidth); if (i !== cur) { cur = i; update(); } }, 90);
+ }, { passive: true });
+
+ $('prev').onclick = function(){ go(cur - 1); };
+ $('next').onclick = function(){ go(cur + 1); };
+ $('try').onclick = function(){ setLive(!live); };
+ $('pick').onclick = function(){
+ var t = list[cur];
+ pick = { key: t.key, src: t.src, id: t.id, name: t.name, url: t.url, at: new Date().toISOString() };
+ set(KEY, JSON.stringify(pick)); update();
+ };
+ document.querySelectorAll('.filters button').forEach(function(b){
+ b.onclick = function(){ filter = b.dataset.f; set(FKEY, filter); history.replaceState(null, '', location.pathname); build(); };
+ });
+ document.addEventListener('keydown', function(e){
+ if (e.target.closest && e.target.closest('input,textarea')) return;
+ if (e.key === 'ArrowRight') { e.preventDefault(); go(cur + 1); }
+ else if (e.key === 'ArrowLeft') { e.preventDefault(); go(cur - 1); }
+ });
+ window.addEventListener('resize', function(){ size(); go(cur, true); });
+ setTimeout(function(){ $('hint').style.opacity = 0; }, 3500);
+
+ build();
+})();
+</script>
+</body>
+</html>
diff --git a/public/simple-probooth.html b/public/simple-probooth.html
index dfebc56..842ab77 100644
--- a/public/simple-probooth.html
+++ b/public/simple-probooth.html
@@ -518,7 +518,7 @@ $('#nextBtn').addEventListener('click',()=>{
$('#saved').hidden=true; $('#live').hidden=false;
if(!live) startCam();
});
-$('#homeBtn').addEventListener('click',()=>{ location.href='/'; });
+$('#homeBtn').addEventListener('click',()=>{ location.href='/app'; });
/* iOS backgrounding pauses the stream — re-assert on return */
document.addEventListener('visibilitychange',()=>{
diff --git a/public/themes/v07-traffic-light.html b/public/themes/v07-traffic-light.html
new file mode 100644
index 0000000..7b0009c
--- /dev/null
+++ b/public/themes/v07-traffic-light.html
@@ -0,0 +1,148 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>v07 — Traffic Light</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --white:#ffffff;
+ --ink:#1e1e1e;
+ --muted:#595959; /* Figma #6b6b6b @80% → darkened for 4.5:1 */
+ --green-bg:#2e9e5b; /* screen background (dark text on it) */
+ --green:#1e7b46; /* Figma #2e9e5b darkened so white text passes */
+ --green-ink:#0b0b0b; /* small text on the green screens (Figma grey fails) */
+ --yellow:#e7b416;
+ --blue:#2f6fd6;
+ --orange:#b4561a; /* Figma #e8762c darkened so white text passes */
+ --grey:#cfcfcf;
+ }
+ *{box-sizing:border-box;}
+ html,body{margin:0;padding:0;background:#0a0a0a;font-family:'Inter',system-ui,sans-serif;-webkit-font-smoothing:antialiased;}
+ button{font-family:inherit;-webkit-tap-highlight-color:transparent;}
+ .stage{
+ width:390px;height:780px;margin:24px auto;position:relative;
+ border-radius:36px;overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.5);border:8px solid #000;
+ display:flex;flex-direction:column;background:var(--white);
+ }
+ .screen{flex:1;min-height:0;display:flex;flex-direction:column;gap:14px;padding:14px 20px 22px;overflow-y:auto;background:var(--white);color:var(--ink);}
+ .screen[hidden]{display:none;}
+ .screen.on-green{background:var(--green-bg);}
+ .topbar{display:flex;align-items:center;justify-content:space-between;min-height:44px;gap:8px;flex-shrink:0;}
+ .step{margin:0;font-size:13px;font-weight:600;color:var(--muted);}
+ .on-green .step{color:var(--green-ink);}
+ .home{min-width:44px;min-height:44px;padding:0 14px;border-radius:14px;border:2px solid #d4d4d4;background:#fff;color:var(--ink);font-size:15px;font-weight:600;cursor:pointer;}
+ .on-green .home{border-color:#fff;}
+ h1{margin:0;text-align:center;font-weight:700;line-height:1.15;color:var(--ink);flex-shrink:0;}
+ .h-34{font-size:34px;} .h-30{font-size:30px;} .h-44{font-size:44px;}
+ .sub{margin:0;text-align:center;font-size:18px;line-height:1.35;color:var(--muted);flex-shrink:0;}
+ .on-green .sub{color:var(--green-ink);font-size:20px;}
+ .spacer{flex:1 1 0;min-height:0;}
+ .btn{display:flex;align-items:center;justify-content:center;gap:10px;width:100%;flex-shrink:0;border:0;border-radius:20px;padding:0 16px;font-size:22px;font-weight:700;line-height:1.2;cursor:pointer;}
+ .btn-96{min-height:96px;} .btn-72{min-height:72px;}
+ .c-green{background:var(--green);color:#fff;}
+ .c-yellow{background:var(--yellow);color:var(--ink);}
+ .c-blue{background:var(--blue);color:#fff;}
+ .c-orange{background:var(--orange);color:#fff;}
+ .c-dark{background:var(--ink);color:#fff;}
+ .c-white{background:#fff;color:var(--green);}
+ .camera{flex:1 1 auto;min-height:220px;background:#1c1c1e;border-radius:24px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:18px;font-weight:600;position:relative;overflow:hidden;}
+ .camera::before{content:"";position:absolute;inset:18%;border:2px dashed rgba(255,255,255,.35);border-radius:16px;}
+ .shutter-row{display:flex;justify-content:center;flex-shrink:0;}
+ .shutter{width:96px;height:96px;border-radius:50%;border:0;background:#fff;cursor:pointer;box-shadow:0 0 0 6px rgba(255,255,255,.35);}
+ .grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;flex-shrink:0;}
+ .tile{min-height:120px;border:0;border-radius:20px;background:var(--green);color:#fff;font-size:20px;font-weight:700;cursor:pointer;padding:8px;}
+ .tile.long{font-size:17px;}
+ .tile.other{background:var(--grey);color:var(--ink);}
+ .tile[aria-pressed="true"]{box-shadow:inset 0 0 0 5px var(--ink);}
+ .tile.other[aria-pressed="true"]{box-shadow:inset 0 0 0 5px var(--green);}
+ .big-emoji{margin:0;text-align:center;font-size:96px;line-height:1;flex-shrink:0;}
+ button:focus-visible{outline:3px solid #000;outline-offset:3px;box-shadow:0 0 0 6px #fff;}
+ .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;}
+ @media (max-width:405.98px){
+ .stage{width:100%;height:100dvh;min-height:640px;margin:0;border:0;border-radius:0;box-shadow:none;}
+ }
+</style>
+</head>
+<body>
+<main class="stage" aria-label="Traffic Light photo capture mockup">
+
+ <!-- 1 · Home -->
+ <section class="screen is-active" data-screen="1" aria-labelledby="t1">
+ <div class="topbar"><p class="step">1 / 4 · Home</p></div>
+ <h1 id="t1" class="h-34">Tap a color</h1>
+ <p class="sub">Green = new · Yellow = fix · Blue = find · Orange = sample</p>
+ <div class="spacer"></div>
+ <button class="btn btn-96 c-green" data-go="2"><span aria-hidden="true">🟢</span> New Item</button>
+ <button class="btn btn-96 c-yellow" data-go="2"><span aria-hidden="true">🟡</span> Fix an Item</button>
+ <button class="btn btn-96 c-blue" data-go="2"><span aria-hidden="true">🔵</span> Find an Item</button>
+ <button class="btn btn-96 c-orange" data-go="2"><span aria-hidden="true">🟠</span> Sample Came In</button>
+ </section>
+
+ <!-- 2 · Snap -->
+ <section class="screen on-green" data-screen="2" aria-labelledby="t2" hidden>
+ <div class="topbar"><p class="step">2 / 4 · Snap</p><button class="home" data-go="1">⌂ Home</button></div>
+ <h1 id="t2" class="h-30">Snap the sample</h1>
+ <div class="camera" role="img" aria-label="Camera viewfinder (pretend)"><span>📷 point at the sample</span></div>
+ <div class="shutter-row"><button class="shutter" data-go="3" aria-label="Take photo"></button></div>
+ </section>
+
+ <!-- 3 · Pick brand -->
+ <section class="screen" data-screen="3" aria-labelledby="t3" hidden>
+ <div class="topbar"><p class="step">3 / 4 · Pick brand</p><button class="home" data-go="1">⌂ Home</button></div>
+ <h1 id="t3" class="h-34">Which brand?</h1>
+ <div class="grid" role="group" aria-label="Brands">
+ <button class="tile" aria-pressed="true">Thibaut</button>
+ <button class="tile" aria-pressed="false">York</button>
+ <button class="tile" aria-pressed="false">Kravet</button>
+ <button class="tile long" aria-pressed="false">Schumacher</button>
+ <button class="tile" aria-pressed="false">Brewster</button>
+ <button class="tile other" aria-pressed="false">Other…</button>
+ </div>
+ <div class="spacer"></div>
+ <button class="btn btn-72 c-dark" data-go="4">Save it ✓</button>
+ </section>
+
+ <!-- 4 · Done -->
+ <section class="screen on-green" data-screen="4" aria-labelledby="t4" hidden>
+ <div class="topbar"><p class="step">4 / 4 · Done</p><button class="home" data-go="1">⌂ Home</button></div>
+ <div class="spacer"></div>
+ <p class="big-emoji" aria-hidden="true">✅</p>
+ <h1 id="t4" class="h-44">Saved!</h1>
+ <p class="sub">Nice. It’s in the drafts.</p>
+ <div class="spacer"></div>
+ <button class="btn btn-72 c-white" data-go="1">Do another</button>
+ </section>
+
+</main>
+<script>
+(function(){
+ var screens = Array.prototype.slice.call(document.querySelectorAll('[data-screen]'));
+ function go(n){
+ n = String(n);
+ screens.forEach(function(s){
+ var on = s.getAttribute('data-screen') === n;
+ s.classList.toggle('is-active', on);
+ s.hidden = !on;
+ });
+ var cur = document.querySelector('[data-screen="' + n + '"]');
+ if (cur){ cur.scrollTop = 0; var f = cur.querySelector('button'); if (f) f.focus({preventScroll:true}); }
+ }
+ document.addEventListener('click', function(e){
+ var t = e.target.closest('.tile');
+ if (t){
+ t.parentNode.querySelectorAll('.tile').forEach(function(x){ x.setAttribute('aria-pressed', x === t ? 'true' : 'false'); });
+ return;
+ }
+ var b = e.target.closest('[data-go]');
+ if (b) go(b.getAttribute('data-go'));
+ });
+ window.__go = go;
+})();
+</script>
+<script>window.__theme = {id:'v07', name:'Traffic Light', screens:4};</script>
+</body>
+</html>
diff --git a/public/themes/v08-big-emoji.html b/public/themes/v08-big-emoji.html
new file mode 100644
index 0000000..ee501db
--- /dev/null
+++ b/public/themes/v08-big-emoji.html
@@ -0,0 +1,133 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>v08 — Big Emoji</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --bg:#111111;
+ --ink:#111111;
+ --muted:#9a9a9a;
+ --yellow:#ffd23f;
+ --cyan:#5ce1e6;
+ --pink:#ff7ab6;
+ --lime:#8be07a;
+ --red:#ff6b6b;
+ --photo:#b9a27a;
+ --cam:#1c1c1e;
+ }
+ *{box-sizing:border-box;}
+ html,body{margin:0;padding:0;background:#0a0a0a;font-family:'Inter',system-ui,sans-serif;-webkit-font-smoothing:antialiased;}
+ button{font-family:inherit;-webkit-tap-highlight-color:transparent;}
+ .stage{
+ width:390px;height:780px;margin:24px auto;position:relative;
+ border-radius:36px;overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.5);border:8px solid #000;
+ display:flex;flex-direction:column;background:var(--bg);
+ }
+ .screen{flex:1;min-height:0;display:flex;flex-direction:column;gap:14px;padding:14px 20px 22px;overflow-y:auto;background:var(--bg);color:#fff;}
+ .screen[hidden]{display:none;}
+ .screen.on-yellow{background:var(--yellow);color:var(--ink);}
+ .topbar{display:flex;align-items:center;justify-content:space-between;min-height:44px;gap:8px;flex-shrink:0;}
+ .step{margin:0;font-size:13px;font-weight:600;color:var(--muted);}
+ .on-yellow .step{color:#3d3200;} /* Figma grey fails on yellow → dark olive */
+ .home{min-width:44px;min-height:44px;padding:0 14px;border-radius:14px;border:2px solid #3a3a3a;background:#1f1f1f;color:#fff;font-size:15px;font-weight:600;cursor:pointer;}
+ .on-yellow .home{background:#111;border-color:#111;color:var(--yellow);}
+ h1{margin:0;text-align:center;font-weight:700;line-height:1.1;flex-shrink:0;}
+ .h-34{font-size:34px;} .h-40{font-size:40px;} .h-emoji{font-size:64px;line-height:1;}
+ .h-party{font-size:120px;line-height:1;}
+ .spacer{flex:1 1 0;min-height:0;}
+ .grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;flex-shrink:0;}
+ .tile{border:0;border-radius:20px;color:var(--ink);font-weight:700;cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;padding:8px 4px;}
+ .tile-big{min-height:230px;font-size:44px;line-height:1.15;}
+ .tile-big .lbl{font-size:40px;}
+ .tile-big .lbl.long{font-size:34px;}
+ .tile-mid{min-height:120px;font-size:56px;line-height:1;}
+ .t-yellow{background:var(--yellow);} .t-cyan{background:var(--cyan);} .t-pink{background:var(--pink);}
+ .t-lime{background:var(--lime);} .t-red{background:var(--red);}
+ .camera{flex:1 1 auto;min-height:220px;background:var(--cam);border-radius:24px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:18px;font-weight:600;position:relative;overflow:hidden;}
+ .camera::before{content:"";position:absolute;inset:18%;border:2px dashed rgba(255,255,255,.3);border-radius:16px;}
+ .photo{flex:1 1 auto;min-height:200px;border-radius:24px;display:flex;align-items:center;justify-content:center;font-size:18px;font-weight:600;color:var(--ink);
+ background:repeating-linear-gradient(90deg,rgba(0,0,0,.05) 0 3px,transparent 3px 9px),var(--photo);}
+ .shutter-row{display:flex;justify-content:center;flex-shrink:0;}
+ .shutter{width:96px;height:96px;border-radius:50%;background:var(--yellow);border:6px solid #fff;cursor:pointer;}
+ .btn{display:flex;align-items:center;justify-content:center;gap:10px;width:100%;flex-shrink:0;border:0;border-radius:20px;padding:0 16px;min-height:96px;font-size:22px;font-weight:700;cursor:pointer;background:#111;color:var(--yellow);}
+ button:focus-visible{outline:3px solid #000;outline-offset:3px;box-shadow:0 0 0 6px #fff;}
+ .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;}
+ @media (max-width:405.98px){
+ .stage{width:100%;height:100dvh;min-height:640px;margin:0;border:0;border-radius:0;box-shadow:none;}
+ .tile-big{min-height:190px;}
+ }
+</style>
+</head>
+<body>
+<main class="stage" aria-label="Big Emoji photo capture mockup">
+
+ <!-- 1 · Home -->
+ <section class="screen is-active" data-screen="1" aria-labelledby="t1">
+ <div class="topbar"><p class="step">1 / 4 · Home</p></div>
+ <h1 id="t1" class="h-34">What’s up?</h1>
+ <div class="spacer"></div>
+ <div class="grid">
+ <button class="tile tile-big t-yellow" data-go="2"><span aria-hidden="true">➕</span><span class="lbl">New</span></button>
+ <button class="tile tile-big t-cyan" data-go="2"><span aria-hidden="true">🔄</span><span class="lbl">Fix</span></button>
+ <button class="tile tile-big t-pink" data-go="2"><span aria-hidden="true">🔍</span><span class="lbl">Find</span></button>
+ <button class="tile tile-big t-lime" data-go="2"><span aria-hidden="true">📦</span><span class="lbl long">Sample</span></button>
+ </div>
+ </section>
+
+ <!-- 2 · Snap -->
+ <section class="screen" data-screen="2" aria-labelledby="t2" hidden>
+ <div class="topbar"><p class="step">2 / 4 · Snap</p><button class="home" data-go="1">⌂ Home</button></div>
+ <h1 id="t2" class="h-emoji"><span aria-hidden="true">📸</span><span class="sr-only">Snap a photo</span></h1>
+ <div class="camera" role="img" aria-label="Camera viewfinder (pretend)"><span>point it here 👇</span></div>
+ <div class="shutter-row"><button class="shutter" data-go="3" aria-label="Take photo"></button></div>
+ </section>
+
+ <!-- 3 · Looks right? -->
+ <section class="screen" data-screen="3" aria-labelledby="t3" hidden>
+ <div class="topbar"><p class="step">3 / 4 · Looks right?</p><button class="home" data-go="1">⌂ Home</button></div>
+ <h1 id="t3" class="h-40">This one?</h1>
+ <div class="photo" role="img" aria-label="Your photo (pretend)">your photo</div>
+ <div class="grid">
+ <button class="tile tile-mid t-lime" data-go="4" aria-label="Yes, looks right">👍</button>
+ <button class="tile tile-mid t-red" data-go="2" aria-label="No, retake photo">👎</button>
+ </div>
+ </section>
+
+ <!-- 4 · Done -->
+ <section class="screen on-yellow" data-screen="4" aria-labelledby="t4" hidden>
+ <div class="topbar"><p class="step">4 / 4 · Done</p><button class="home" data-go="1">⌂ Home</button></div>
+ <div class="spacer"></div>
+ <h1 id="t4" class="h-party"><span aria-hidden="true">🎉</span><span class="sr-only">Done — saved!</span></h1>
+ <div class="spacer"></div>
+ <button class="btn" data-go="1" aria-label="Add again"><span aria-hidden="true">➕</span> again</button>
+ </section>
+
+</main>
+<script>
+(function(){
+ var screens = Array.prototype.slice.call(document.querySelectorAll('[data-screen]'));
+ function go(n){
+ n = String(n);
+ screens.forEach(function(s){
+ var on = s.getAttribute('data-screen') === n;
+ s.classList.toggle('is-active', on);
+ s.hidden = !on;
+ });
+ var cur = document.querySelector('[data-screen="' + n + '"]');
+ if (cur){ cur.scrollTop = 0; var f = cur.querySelector('button'); if (f) f.focus({preventScroll:true}); }
+ }
+ document.addEventListener('click', function(e){
+ var b = e.target.closest('[data-go]');
+ if (b) go(b.getAttribute('data-go'));
+ });
+ window.__go = go;
+})();
+</script>
+<script>window.__theme = {id:'v08', name:'Big Emoji', screens:4};</script>
+</body>
+</html>
diff --git a/public/themes/v09-checklist.html b/public/themes/v09-checklist.html
new file mode 100644
index 0000000..f3b8f71
--- /dev/null
+++ b/public/themes/v09-checklist.html
@@ -0,0 +1,125 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>v09 — Checklist</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --bg:#f6f4ef;
+ --ink:#1f2a24;
+ --muted:#565d56; /* Figma #6f766f @80% → darkened for 4.5:1 */
+ --done:#dde8df;
+ --green:#3e6b4f;
+ --card:#ffffff;
+ }
+ *{box-sizing:border-box;}
+ html,body{margin:0;padding:0;background:#0a0a0a;font-family:'Inter',system-ui,sans-serif;-webkit-font-smoothing:antialiased;}
+ button{font-family:inherit;-webkit-tap-highlight-color:transparent;}
+ .stage{
+ width:390px;height:780px;margin:24px auto;position:relative;
+ border-radius:36px;overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.5);border:8px solid #000;
+ display:flex;flex-direction:column;background:var(--bg);
+ }
+ .screen{flex:1;min-height:0;display:flex;flex-direction:column;gap:14px;padding:14px 20px 22px;overflow-y:auto;background:var(--bg);color:var(--ink);}
+ .screen[hidden]{display:none;}
+ .topbar{display:flex;align-items:center;justify-content:space-between;min-height:44px;gap:8px;flex-shrink:0;}
+ .step{margin:0;font-size:13px;font-weight:600;color:var(--muted);}
+ .home{min-width:44px;min-height:44px;padding:0 14px;border-radius:14px;border:2px solid #d9d4c8;background:#fff;color:var(--ink);font-size:15px;font-weight:600;cursor:pointer;}
+ h1{margin:0;text-align:center;font-weight:700;line-height:1.15;flex-shrink:0;}
+ .h-36{font-size:36px;} .h-28{font-size:28px;} .h-30{font-size:30px;}
+ .sub{margin:0;text-align:center;font-size:18px;color:var(--muted);flex-shrink:0;}
+ .spacer{flex:1 1 0;min-height:0;}
+ .btn{display:flex;align-items:center;justify-content:center;gap:12px;width:100%;flex-shrink:0;border:0;border-radius:20px;padding:0 16px;font-size:22px;font-weight:700;line-height:1.2;cursor:pointer;background:var(--card);color:var(--ink);box-shadow:0 1px 0 rgba(0,0,0,.04);}
+ .btn-80{min-height:80px;} .btn-72{min-height:72px;}
+ .btn.soft{color:#5f665f;}
+ .btn.primary{background:var(--green);color:#fff;}
+ .box{display:inline-block;width:20px;height:20px;border:2px solid var(--ink);border-radius:3px;flex-shrink:0;}
+ .checked{display:flex;align-items:center;justify-content:center;gap:10px;min-height:56px;border-radius:20px;background:var(--done);font-size:22px;font-weight:700;flex-shrink:0;margin:0;}
+ .tick{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:5px;background:#5b5b5b;color:#fff;font-size:16px;line-height:1;}
+ .camera{flex:1 1 auto;min-height:200px;background:#1c1c1e;border-radius:24px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:18px;font-weight:600;position:relative;overflow:hidden;}
+ .camera::before{content:"";position:absolute;inset:18%;border:2px dashed rgba(255,255,255,.3);border-radius:16px;}
+ .shutter-row{display:flex;justify-content:center;flex-shrink:0;}
+ .shutter{width:96px;height:96px;border-radius:50%;background:var(--green);border:6px solid #fff;cursor:pointer;}
+ button:focus-visible{outline:3px solid #000;outline-offset:3px;box-shadow:0 0 0 6px #fff;}
+ @media (max-width:405.98px){
+ .stage{width:100%;height:100dvh;min-height:640px;margin:0;border:0;border-radius:0;box-shadow:none;}
+ }
+</style>
+</head>
+<body>
+<main class="stage" aria-label="Checklist photo capture mockup">
+
+ <!-- 1 · Home -->
+ <section class="screen is-active" data-screen="1" aria-labelledby="t1">
+ <div class="topbar"><p class="step">1 / 4 · Home</p></div>
+ <h1 id="t1" class="h-36">Today’s list</h1>
+ <p class="sub">Tap one to start</p>
+ <button class="btn btn-80" data-go="2" data-task="Add a new item"><span class="box" aria-hidden="true"></span>Add a new item</button>
+ <button class="btn btn-80" data-go="2" data-task="Update an item"><span class="box" aria-hidden="true"></span>Update an item</button>
+ <button class="btn btn-80" data-go="2" data-task="Find an item"><span class="box" aria-hidden="true"></span>Find an item</button>
+ <button class="btn btn-80" data-go="2" data-task="Check in a sample"><span class="box" aria-hidden="true"></span>Check in a sample</button>
+ </section>
+
+ <!-- 2 · Step 1 · Photo -->
+ <section class="screen" data-screen="2" aria-labelledby="t2" hidden>
+ <div class="topbar"><p class="step">2 / 4 · Step 1 · Photo</p><button class="home" data-go="1">⌂ Home</button></div>
+ <p class="checked"><span class="tick" aria-hidden="true">✓</span><span id="taskName">Add a new item</span></p>
+ <h1 id="t2" class="h-28">Step 1: take a photo</h1>
+ <div class="camera" role="img" aria-label="Camera viewfinder (pretend)"><span>📷 point at the sample</span></div>
+ <div class="shutter-row"><button class="shutter" data-go="3" aria-label="Take photo"></button></div>
+ </section>
+
+ <!-- 3 · Step 2 · Brand -->
+ <section class="screen" data-screen="3" aria-labelledby="t3" hidden>
+ <div class="topbar"><p class="step">3 / 4 · Step 2 · Brand</p><button class="home" data-go="1">⌂ Home</button></div>
+ <p class="checked"><span class="tick" aria-hidden="true">✓</span>Photo taken</p>
+ <h1 id="t3" class="h-28">Step 2: pick the brand</h1>
+ <button class="btn btn-72" data-go="4">Thibaut</button>
+ <button class="btn btn-72" data-go="4">York</button>
+ <button class="btn btn-72" data-go="4">Kravet</button>
+ <button class="btn btn-72 soft" data-go="4">Something else…</button>
+ </section>
+
+ <!-- 4 · All done -->
+ <section class="screen" data-screen="4" aria-labelledby="t4" hidden>
+ <div class="topbar"><p class="step">4 / 4 · All done</p><button class="home" data-go="1">⌂ Home</button></div>
+ <p class="checked"><span class="tick" aria-hidden="true">✓</span>Photo taken</p>
+ <p class="checked"><span class="tick" aria-hidden="true">✓</span>Brand picked</p>
+ <p class="checked"><span class="tick" aria-hidden="true">✓</span>Saved as draft</p>
+ <div class="spacer"></div>
+ <h1 id="t4" class="h-30">All checked off ✅</h1>
+ <div class="spacer"></div>
+ <button class="btn btn-72 primary" data-go="1">Back to my list</button>
+ </section>
+
+</main>
+<script>
+(function(){
+ var screens = Array.prototype.slice.call(document.querySelectorAll('[data-screen]'));
+ function go(n){
+ n = String(n);
+ screens.forEach(function(s){
+ var on = s.getAttribute('data-screen') === n;
+ s.classList.toggle('is-active', on);
+ s.hidden = !on;
+ });
+ var cur = document.querySelector('[data-screen="' + n + '"]');
+ if (cur){ cur.scrollTop = 0; var f = cur.querySelector('button'); if (f) f.focus({preventScroll:true}); }
+ }
+ document.addEventListener('click', function(e){
+ var b = e.target.closest('[data-go]');
+ if (!b) return;
+ var task = b.getAttribute('data-task');
+ if (task) document.getElementById('taskName').textContent = task;
+ go(b.getAttribute('data-go'));
+ });
+ window.__go = go;
+})();
+</script>
+<script>window.__theme = {id:'v09', name:'Checklist', screens:4};</script>
+</body>
+</html>
diff --git a/public/themes/v1-magic-camera.html b/public/themes/v1-magic-camera.html
new file mode 100644
index 0000000..b76590e
--- /dev/null
+++ b/public/themes/v1-magic-camera.html
@@ -0,0 +1,360 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>v1 — Magic Camera</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link href="https://fonts.googleapis.com/css2?family=Fraunces:wght@500;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --charcoal:#1C1C1E;
+ --gold:#C9A24B;
+ --cream:#FDFBF7;
+ --green:#3FA66B;
+ }
+ *{box-sizing:border-box;}
+ html,body{margin:0;padding:0;background:#0a0a0a;font-family:'Inter',sans-serif;}
+ .stage{
+ width:390px;height:780px;margin:24px auto;position:relative;
+ background:var(--charcoal);border-radius:36px;overflow:hidden;
+ box-shadow:0 20px 60px rgba(0,0,0,.5);border:8px solid #000;
+ }
+ .screen{
+ position:absolute;inset:0;display:none;flex-direction:column;
+ background:var(--charcoal);color:var(--cream);
+ }
+ .screen.active{display:flex;}
+ .light{background:var(--cream);color:var(--charcoal);}
+ h1,h2,h3{font-family:'Fraunces',serif;margin:0;}
+ .dw-mark{
+ position:absolute;top:18px;left:18px;font-family:'Fraunces',serif;
+ font-weight:700;font-size:15px;letter-spacing:1px;color:var(--cream);opacity:.85;z-index:5;
+ }
+ .hint{
+ position:absolute;top:56px;left:0;right:0;text-align:center;
+ font-size:17px;color:var(--cream);opacity:.9;z-index:5;
+ }
+ .viewfinder{
+ position:absolute;inset:0;
+ background:
+ repeating-linear-gradient(135deg, #232326 0 40px, #1c1c1e 40px 80px);
+ }
+ .shutter-wrap{
+ position:absolute;bottom:48px;left:0;right:0;display:flex;justify-content:center;z-index:5;
+ }
+ .shutter{
+ width:96px;height:96px;border-radius:50%;background:var(--gold);
+ border:6px solid var(--cream);cursor:pointer;font-size:40px;
+ display:flex;align-items:center;justify-content:center;
+ box-shadow:0 8px 24px rgba(0,0,0,.4);
+ }
+ .shutter:focus-visible, button:focus-visible, .card-btn:focus-visible, .brand-tile:focus-visible{
+ outline:3px solid var(--gold);outline-offset:2px;
+ }
+ .btn:disabled{opacity:.4;cursor:not-allowed;}
+ .demo-caption{font-size:13px;opacity:.55;margin-top:-10px;}
+ .looking-wrap{align-items:center;justify-content:center;gap:18px;}
+ .looking-wrap .spinner{
+ width:64px;height:64px;border-radius:50%;
+ border:6px solid rgba(255,255,255,.15);border-top-color:var(--gold);
+ animation:spin 1s linear infinite;
+ }
+ @keyframes spin{to{transform:rotate(360deg);}}
+ .looking-wrap p{font-size:19px;}
+ .content{padding:28px 24px;display:flex;flex-direction:column;gap:18px;flex:1;overflow-y:auto;}
+ .swatch{
+ width:100%;height:220px;border-radius:18px;margin-top:4px;
+ background:
+ repeating-linear-gradient(45deg,#8FAE8B 0 22px,#B9CBB2 22px 44px),
+ linear-gradient(135deg,#8FAE8B,#6f8d6c);
+ background-blend-mode:multiply;
+ }
+ .swatch.blue{background:
+ repeating-linear-gradient(45deg,#7C93B0 0 22px,#B3C3D6 22px 44px),
+ linear-gradient(135deg,#7C93B0,#5f7692);
+ background-blend-mode:multiply;}
+ .item-title{font-size:22px;font-weight:700;}
+ .item-sub{font-size:15px;opacity:.75;}
+ .btn{
+ min-height:56px;border-radius:16px;border:none;font-family:'Inter',sans-serif;
+ font-size:18px;font-weight:600;cursor:pointer;padding:0 20px;width:100%;
+ }
+ .btn-primary{background:var(--gold);color:#1C1C1E;}
+ .btn-secondary{background:transparent;color:var(--cream);border:2px solid rgba(253,251,247,.4);}
+ .btn-light-primary{background:var(--gold);color:#1C1C1E;}
+ .btn-light-secondary{background:#ece7dd;color:var(--charcoal);border:2px solid #ddd4c2;}
+ .btn-success{background:var(--green);color:#fff;}
+ .brand-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;overflow-y:auto;padding-right:2px;}
+ .brand-tile{
+ background:#2a2a2d;border:2px solid #3a3a3d;border-radius:14px;
+ min-height:72px;display:flex;align-items:center;justify-content:center;
+ font-size:17px;font-weight:600;color:var(--cream);cursor:pointer;text-align:center;padding:8px;
+ }
+ .brand-tile:hover{border-color:var(--gold);}
+ .brand-tile.selected{border-color:var(--gold);background:#3a331f;box-shadow:inset 0 0 0 2px var(--gold);}
+ .center-flex{align-items:center;justify-content:center;text-align:center;}
+ .big-emoji{font-size:64px;}
+ .pick-list{display:flex;flex-direction:column;gap:14px;margin-top:8px;}
+ .home-hdr{text-align:center;font-size:17px;margin-top:4px;}
+ .ux-home{position:absolute;top:14px;right:14px;z-index:20;min-height:44px;min-width:44px;padding:0 14px;border-radius:12px;
+ border:2px solid var(--cream);background:rgba(28,28,30,.85);color:var(--cream);font:600 15px Inter,sans-serif;cursor:pointer}
+ .ux-home[hidden]{display:none}
+ .ux-cancel{margin-top:18px;min-height:48px;padding:0 22px;border-radius:12px;border:2px solid var(--cream);background:transparent;color:var(--cream);font:600 16px Inter,sans-serif;cursor:pointer}
+</style>
+<style id="ux-baseline">/* TK-12343 UX baseline: full-bleed on real phones, visible focus everywhere */
+@media (max-width:405px){ html,body{margin:0;padding:0} body{overflow-x:hidden} .stage,.phone{width:100% !important;max-width:100%;margin:0 !important;border:0 !important;border-radius:0 !important;box-shadow:none !important;height:100dvh;min-height:640px} }
+button:focus-visible,a:focus-visible,[tabindex]:focus-visible,input:focus-visible{outline:3px solid #9C7A33 !important;outline-offset:2px}
+</style>
+</head>
+<body>
+<div class="stage" id="stage">
+ <button class="ux-home" id="uxHome" type="button" onclick="goHome()" hidden>⌂ Home</button>
+
+ <!-- HOME -->
+ <div class="screen active" id="home">
+ <div class="dw-mark">DW</div>
+ <div class="hint">Point at anything and tap 📸</div>
+ <div class="viewfinder"></div>
+ <div class="shutter-wrap">
+ <button class="shutter" id="shutterBtn" aria-label="Shoot photo">📸</button>
+ </div>
+ </div>
+
+ <!-- LOOKING -->
+ <div class="screen looking-wrap" id="looking">
+ <div class="spinner"></div>
+ <p>Looking…</p>
+ <button class="ux-cancel" type="button" onclick="goHome()">Cancel</button>
+ </div>
+
+ <!-- GUESS: ADD NEW ITEM -->
+ <div class="screen" id="guessAdd">
+ <div class="content">
+ <h2>Looks like a New Item</h2>
+ <p class="demo-caption">Demo: cycles through jobs — real version would ask</p>
+ <div class="swatch"></div>
+ <button class="btn btn-primary" onclick="show('addBrand')">Yes, that's right</button>
+ <button class="btn btn-secondary" onclick="show('pickAnother')">No, pick another</button>
+ </div>
+ </div>
+
+ <!-- ADD: BRAND GRID -->
+ <div class="screen" id="addBrand">
+ <div class="content">
+ <h3>Which brand?</h3>
+ <div class="brand-grid">
+ <div class="brand-tile" tabindex="0" role="button" onclick="selectBrand(this,'York')" onkeydown="onBrandKey(event,this,'York')">York</div>
+ <div class="brand-tile" tabindex="0" role="button" onclick="selectBrand(this,'Brewster')" onkeydown="onBrandKey(event,this,'Brewster')">Brewster</div>
+ <div class="brand-tile" tabindex="0" role="button" onclick="selectBrand(this,'Kravet')" onkeydown="onBrandKey(event,this,'Kravet')">Kravet</div>
+ <div class="brand-tile" tabindex="0" role="button" onclick="selectBrand(this,'Cole & Son')" onkeydown="onBrandKey(event,this,'Cole & Son')">Cole & Son</div>
+ <div class="brand-tile" tabindex="0" role="button" onclick="selectBrand(this,'Thibaut')" onkeydown="onBrandKey(event,this,'Thibaut')">Thibaut</div>
+ <div class="brand-tile" tabindex="0" role="button" onclick="selectBrand(this,'Schumacher')" onkeydown="onBrandKey(event,this,'Schumacher')">Schumacher</div>
+ <div class="brand-tile" tabindex="0" role="button" onclick="selectBrand(this,'Mulberry')" onkeydown="onBrandKey(event,this,'Mulberry')">Mulberry</div>
+ <div class="brand-tile" tabindex="0" role="button" onclick="selectBrand(this,'Other / Unsure')" onkeydown="onBrandKey(event,this,'Other / Unsure')">Other / Unsure</div>
+ </div>
+ <button class="btn btn-primary" id="saveDraftBtn" disabled onclick="saveDraft()">Save as Draft</button>
+ </div>
+ </div>
+
+ <!-- ADD: SAVED -->
+ <div class="screen light center-flex" id="addSaved">
+ <div class="content center-flex">
+ <div class="big-emoji">🎉</div>
+ <h2>Saved!</h2>
+ <div class="item-sub">Added as <strong id="savedBrandName"></strong></div>
+ <button class="btn btn-light-primary" onclick="goHome()">Take Another</button>
+ </div>
+ </div>
+
+ <!-- GUESS: UPDATE -->
+ <div class="screen" id="guessUpdate">
+ <div class="content">
+ <h2>Found a Match</h2>
+ <p class="demo-caption">Demo: cycles through jobs — real version would ask</p>
+ <div class="swatch blue"></div>
+ <div class="item-title">Palm Damask — Sage</div>
+ <div class="item-sub">code DW-10423</div>
+ <button class="btn btn-primary" onclick="show('updateCam2')">Yes, that's right</button>
+ <button class="btn btn-secondary" onclick="show('pickAnother')">Not this item</button>
+ </div>
+ </div>
+
+ <!-- UPDATE: TAKE NEW PHOTO -->
+ <div class="screen" id="updateCam2">
+ <div class="dw-mark">DW</div>
+ <div class="hint">Take the new photo</div>
+ <div class="viewfinder"></div>
+ <div class="shutter-wrap">
+ <button class="shutter" onclick="show('updateDone')" aria-label="Shoot new photo">📸</button>
+ </div>
+ </div>
+
+ <!-- UPDATE: DONE -->
+ <div class="screen light center-flex" id="updateDone">
+ <div class="content center-flex">
+ <div class="big-emoji">✅</div>
+ <h2>Updated!</h2>
+ <button class="btn btn-light-primary" onclick="goHome()">Take Another</button>
+ </div>
+ </div>
+
+ <!-- GUESS: FIND -->
+ <div class="screen" id="guessFind">
+ <div class="content">
+ <h2>Best Match</h2>
+ <p class="demo-caption">Demo: cycles through jobs — real version would ask</p>
+ <div class="swatch"></div>
+ <div class="item-title">Palm Damask — Sage</div>
+ <div class="item-sub">code DW-10423</div>
+ <button class="btn btn-primary" onclick="show('findDone')">Yes, that's right</button>
+ <button class="btn btn-secondary" onclick="show('findFallback')">Not this item</button>
+ </div>
+ </div>
+
+ <!-- FIND: FALLBACK SEARCH -->
+ <div class="screen" id="findFallback">
+ <div class="content">
+ <h3>Search another way</h3>
+ <div class="pick-list">
+ <button class="btn btn-secondary" onclick="show('findResult')">📷 Scan</button>
+ <button class="btn btn-secondary" onclick="show('findResult')">🎤 Voice</button>
+ <button class="btn btn-secondary" onclick="show('findResult')">⌨️ Type</button>
+ <button class="btn btn-secondary" onclick="show('findResult')">🗂️ Browse</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- FIND: RESULT -->
+ <div class="screen" id="findResult">
+ <div class="content">
+ <h2>Here it is</h2>
+ <div class="swatch blue"></div>
+ <div class="item-title">Palm Damask — Sage</div>
+ <div class="item-sub">code DW-10423</div>
+ <button class="btn btn-primary" onclick="show('findDone')">Yes, that's right</button>
+ <button class="btn btn-secondary" onclick="show('findFallback')">Not this item</button>
+ </div>
+ </div>
+
+ <!-- FIND: DONE -->
+ <div class="screen light center-flex" id="findDone">
+ <div class="content center-flex">
+ <div class="big-emoji">🔍</div>
+ <h2>Found it!</h2>
+ <button class="btn btn-light-primary" onclick="goHome()">Take Another</button>
+ </div>
+ </div>
+
+ <!-- GUESS: SAMPLE -->
+ <div class="screen" id="guessSample">
+ <div class="content">
+ <h2>Sample Matched</h2>
+ <p class="demo-caption">Demo: cycles through jobs — real version would ask</p>
+ <div class="swatch"></div>
+ <div class="item-title">Palm Damask — Sage</div>
+ <div class="item-sub">code DW-10423</div>
+ <button class="btn btn-primary" onclick="show('samplePrinting')">Mark It Received</button>
+ <button class="btn btn-secondary" onclick="show('pickAnother')">Not this item</button>
+ </div>
+ </div>
+
+ <!-- SAMPLE: PRINTING -->
+ <div class="screen looking-wrap" id="samplePrinting">
+ <div class="spinner"></div>
+ <p>Printing your sticker…</p>
+ <button class="ux-cancel" type="button" onclick="goHome()">Cancel</button>
+ </div>
+
+ <!-- SAMPLE: DONE -->
+ <div class="screen light center-flex" id="sampleDone">
+ <div class="content center-flex">
+ <div class="big-emoji">✅</div>
+ <h2>Done!</h2>
+ <button class="btn btn-light-primary" onclick="goHome()">Take Another</button>
+ </div>
+ </div>
+
+ <!-- PICK ANOTHER (No, pick another fallback chooser) -->
+ <div class="screen" id="pickAnother">
+ <div class="content">
+ <h3>Which job is this?</h3>
+ <div class="pick-list">
+ <button class="btn btn-secondary" onclick="show('addBrand')">Add New Item</button>
+ <button class="btn btn-secondary" onclick="show('updateCam2')">Update an Item</button>
+ <button class="btn btn-secondary" onclick="show('findFallback')">Find an Item</button>
+ <button class="btn btn-secondary" onclick="show('guessSample')">A Sample Came In</button>
+ </div>
+ </div>
+ </div>
+
+</div>
+
+<script>
+ let jobIndex = 0;
+ const jobGuesses = ['guessAdd','guessUpdate','guessFind','guessSample'];
+
+ let selectedBrand = null;
+
+ function show(id){
+ document.querySelectorAll('.screen').forEach(s=>s.classList.remove('active'));
+ document.getElementById(id).classList.add('active');
+ document.getElementById('uxHome').hidden = (id === 'home');
+ if(id === 'addBrand'){
+ selectedBrand = null;
+ document.querySelectorAll('#addBrand .brand-tile').forEach(t=>t.classList.remove('selected'));
+ document.getElementById('saveDraftBtn').disabled = true;
+ }
+ }
+
+ function selectBrand(el, name){
+ document.querySelectorAll('#addBrand .brand-tile').forEach(t=>t.classList.remove('selected'));
+ el.classList.add('selected');
+ selectedBrand = name;
+ document.getElementById('saveDraftBtn').disabled = false;
+ }
+
+ function onBrandKey(evt, el, name){
+ if(evt.key === 'Enter' || evt.key === ' '){
+ evt.preventDefault();
+ selectBrand(el, name);
+ }
+ }
+
+ function saveDraft(){
+ if(!selectedBrand) return;
+ document.getElementById('savedBrandName').textContent = selectedBrand;
+ show('addSaved');
+ }
+
+ let pendingTimer = null;
+ function goHome(){
+ if (pendingTimer) { clearTimeout(pendingTimer); pendingTimer = null; }
+ show('home');
+ }
+
+ document.getElementById('shutterBtn').addEventListener('click', function(){
+ show('looking');
+ pendingTimer = setTimeout(function(){
+ pendingTimer = null;
+ const target = jobGuesses[jobIndex % jobGuesses.length];
+ jobIndex++;
+ show(target);
+ }, 900);
+ });
+
+ // sample "Mark It Received" leads through printing -> done automatically
+ document.addEventListener('DOMContentLoaded', function(){
+ const printingScreen = document.getElementById('samplePrinting');
+ });
+ // auto-advance printing screen to done
+ const origShow = show;
+ show = function(id){
+ origShow(id);
+ if(id === 'samplePrinting'){
+ pendingTimer = setTimeout(function(){ pendingTimer = null; origShow('sampleDone'); document.getElementById('uxHome').hidden = false; }, 1100);
+ }
+ };
+</script>
+</body>
+</html>
diff --git a/public/themes/v10-scan-first.html b/public/themes/v10-scan-first.html
new file mode 100644
index 0000000..d6d27cc
--- /dev/null
+++ b/public/themes/v10-scan-first.html
@@ -0,0 +1,140 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>v10 — Scan First</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --bg:#0e1b2c;
+ --panel:#1e3350;
+ --muted:#8fa3bf;
+ --card-sub:#aebdd1; /* Figma #777 on navy fails → lightened */
+ --teal:#29c4a9;
+ --swatch:#b9a27a;
+ }
+ *{box-sizing:border-box;}
+ html,body{margin:0;padding:0;background:#0a0a0a;font-family:'Inter',system-ui,sans-serif;-webkit-font-smoothing:antialiased;}
+ button{font-family:inherit;-webkit-tap-highlight-color:transparent;}
+ .stage{
+ width:390px;height:780px;margin:24px auto;position:relative;
+ border-radius:36px;overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.5);border:8px solid #000;
+ display:flex;flex-direction:column;background:var(--bg);
+ }
+ .screen{flex:1;min-height:0;display:flex;flex-direction:column;gap:14px;padding:14px 20px 22px;overflow-y:auto;background:var(--bg);color:#fff;}
+ .screen[hidden]{display:none;}
+ .screen.on-teal{background:var(--teal);color:var(--bg);}
+ .topbar{display:flex;align-items:center;justify-content:space-between;min-height:44px;gap:8px;flex-shrink:0;}
+ .step{margin:0;font-size:13px;font-weight:600;color:var(--muted);}
+ .on-teal .step{color:var(--bg);}
+ .home{min-width:44px;min-height:44px;padding:0 14px;border-radius:14px;border:2px solid #34507a;background:var(--panel);color:#fff;font-size:15px;font-weight:600;cursor:pointer;}
+ .on-teal .home{background:var(--bg);border-color:var(--bg);}
+ h1{margin:0;text-align:center;font-weight:700;line-height:1.15;flex-shrink:0;}
+ .h-30{font-size:30px;} .h-36{font-size:36px;} .h-34{font-size:34px;}
+ .sub{margin:0;text-align:center;font-size:18px;color:var(--muted);flex-shrink:0;}
+ .on-teal .sub{color:var(--bg);font-size:20px;}
+ .spacer{flex:1 1 0;min-height:0;}
+ .scanner{flex:1 1 auto;min-height:220px;width:100%;background:#000;border:0;border-radius:24px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;color:#fff;font-size:18px;font-weight:600;cursor:pointer;position:relative;overflow:hidden;}
+ .scanner .frame{position:absolute;inset:26% 14%;border:3px solid rgba(255,255,255,.55);border-radius:14px;}
+ .scanner .laser{position:absolute;left:14%;right:14%;top:50%;height:2px;background:var(--teal);box-shadow:0 0 12px var(--teal);animation:sweep 2.2s ease-in-out infinite alternate;}
+ @keyframes sweep{from{top:30%}to{top:70%}}
+ @media (prefers-reduced-motion:reduce){.scanner .laser{animation:none;}}
+ .scanner .hint{position:relative;font-size:14px;font-weight:600;color:#cfd8e4;}
+ .scanner .lbl{position:relative;}
+ .card{display:flex;align-items:center;gap:14px;padding:14px;border-radius:20px;background:var(--panel);flex-shrink:0;}
+ .swatch{width:84px;height:84px;border-radius:14px;flex-shrink:0;background:repeating-linear-gradient(90deg,rgba(0,0,0,.06) 0 3px,transparent 3px 8px),var(--swatch);}
+ .card .name{margin:0 0 4px;font-size:20px;font-weight:700;color:#fff;}
+ .card .meta{margin:0;font-size:15px;color:var(--card-sub);}
+ .btn{display:flex;align-items:center;justify-content:center;gap:10px;width:100%;flex-shrink:0;border:0;border-radius:20px;padding:0 16px;font-size:22px;font-weight:700;line-height:1.2;cursor:pointer;background:var(--panel);color:#fff;}
+ .btn-96{min-height:96px;} .btn-80{min-height:80px;} .btn-72{min-height:72px;}
+ .btn.teal{background:var(--teal);color:var(--bg);} /* white on teal fails → navy text */
+ .btn.navy{background:var(--bg);color:#fff;}
+ .note{margin:0;text-align:center;font-size:16px;color:var(--card-sub);min-height:20px;flex-shrink:0;}
+ .big-emoji{margin:0;text-align:center;font-size:96px;line-height:1;flex-shrink:0;}
+ button:focus-visible{outline:3px solid #000;outline-offset:3px;box-shadow:0 0 0 6px #fff;}
+ @media (max-width:405.98px){
+ .stage{width:100%;height:100dvh;min-height:640px;margin:0;border:0;border-radius:0;box-shadow:none;}
+ }
+</style>
+</head>
+<body>
+<main class="stage" aria-label="Scan First photo capture mockup">
+
+ <!-- 1 · Scan -->
+ <section class="screen is-active" data-screen="1" aria-labelledby="t1">
+ <div class="topbar"><p class="step">1 / 4 · Scan</p></div>
+ <h1 id="t1" class="h-30">Scan any label</h1>
+ <button class="scanner" data-go="2" aria-label="Scan label (pretend): line up the barcode">
+ <span class="frame" aria-hidden="true"></span><span class="laser" aria-hidden="true"></span>
+ <span class="lbl">▢ line up the barcode</span>
+ <span class="hint">tap to scan</span>
+ </button>
+ <button class="btn btn-72" data-go="2">No label? Take a photo</button>
+ </section>
+
+ <!-- 2 · Found it -->
+ <section class="screen" data-screen="2" aria-labelledby="t2" hidden>
+ <div class="topbar"><p class="step">2 / 4 · Found it</p><button class="home" data-go="1">⌂ Home</button></div>
+ <h1 id="t2" class="h-36">Found it!</h1>
+ <div class="card"><div class="swatch" role="img" aria-label="Sand grasscloth swatch"></div><div><p class="name">Grasscloth — Sand</p><p class="meta">Thibaut · code T41020</p></div></div>
+ <p class="sub">What do you want to do?</p>
+ <p class="note" id="lookNote" aria-live="polite"></p>
+ <div class="spacer"></div>
+ <button class="btn btn-80 teal" data-go="3"><span aria-hidden="true">📦</span> Sample came in</button>
+ <button class="btn btn-80" data-go="1"><span aria-hidden="true">📸</span> New photo</button>
+ <button class="btn btn-80" id="justLook"><span aria-hidden="true">👀</span> Just look</button>
+ </section>
+
+ <!-- 3 · Confirm -->
+ <section class="screen" data-screen="3" aria-labelledby="t3" hidden>
+ <div class="topbar"><p class="step">3 / 4 · Confirm</p><button class="home" data-go="1">⌂ Home</button></div>
+ <h1 id="t3" class="h-30">Mark it received?</h1>
+ <div class="card"><div class="swatch" role="img" aria-label="Sand grasscloth swatch"></div><div><p class="name">Grasscloth — Sand</p><p class="meta">Thibaut · code T41020</p></div></div>
+ <div class="spacer"></div>
+ <button class="btn btn-96 teal" data-go="4">Yes, received</button>
+ <button class="btn btn-72" data-go="1">Oops, wrong one</button>
+ </section>
+
+ <!-- 4 · Done -->
+ <section class="screen on-teal" data-screen="4" aria-labelledby="t4" hidden>
+ <div class="topbar"><p class="step">4 / 4 · Done</p><button class="home" data-go="1">⌂ Home</button></div>
+ <div class="spacer"></div>
+ <p class="big-emoji" aria-hidden="true">🖨️</p>
+ <h1 id="t4" class="h-34">Sticker printing…</h1>
+ <p class="sub">Stick it on the box.</p>
+ <div class="spacer"></div>
+ <button class="btn btn-72 navy" data-go="1">Scan the next one</button>
+ </section>
+
+</main>
+<script>
+(function(){
+ var screens = Array.prototype.slice.call(document.querySelectorAll('[data-screen]'));
+ var note = document.getElementById('lookNote');
+ function go(n){
+ n = String(n);
+ screens.forEach(function(s){
+ var on = s.getAttribute('data-screen') === n;
+ s.classList.toggle('is-active', on);
+ s.hidden = !on;
+ });
+ note.textContent = '';
+ var cur = document.querySelector('[data-screen="' + n + '"]');
+ if (cur){ cur.scrollTop = 0; var f = cur.querySelector('button'); if (f) f.focus({preventScroll:true}); }
+ }
+ document.getElementById('justLook').addEventListener('click', function(){
+ note.textContent = 'In stock · 3 rolls · Aisle 4';
+ });
+ document.addEventListener('click', function(e){
+ var b = e.target.closest('[data-go]');
+ if (b) go(b.getAttribute('data-go'));
+ });
+ window.__go = go;
+})();
+</script>
+<script>window.__theme = {id:'v10', name:'Scan First', screens:4};</script>
+</body>
+</html>
diff --git a/public/themes/v11-talk-to-it.html b/public/themes/v11-talk-to-it.html
new file mode 100644
index 0000000..1700bc6
--- /dev/null
+++ b/public/themes/v11-talk-to-it.html
@@ -0,0 +1,138 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>v11 — Talk To It</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --bg:#f3eeff;
+ --ink:#2a1b4d;
+ --muted:#5e5180; /* Figma #7a6d99 @80% → darkened for 4.5:1 */
+ --purple:#6b4eff;
+ --card-sub:#5f5f5f; /* Figma #777 → darkened for 4.5:1 */
+ --ocean:#3c6e9e; --sky:#8fb6da; --navy:#1f3a5f;
+ }
+ *{box-sizing:border-box;}
+ html,body{margin:0;padding:0;background:#0a0a0a;font-family:'Inter',system-ui,sans-serif;-webkit-font-smoothing:antialiased;}
+ button{font-family:inherit;-webkit-tap-highlight-color:transparent;}
+ .stage{
+ width:390px;height:780px;margin:24px auto;position:relative;
+ border-radius:36px;overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.5);border:8px solid #000;
+ display:flex;flex-direction:column;background:var(--bg);
+ }
+ .screen{flex:1;min-height:0;display:flex;flex-direction:column;gap:14px;padding:14px 20px 22px;overflow-y:auto;background:var(--bg);color:var(--ink);}
+ .screen[hidden]{display:none;}
+ .screen.on-purple{background:var(--purple);color:#fff;}
+ .topbar{display:flex;align-items:center;justify-content:space-between;min-height:44px;gap:8px;flex-shrink:0;}
+ .step{margin:0;font-size:13px;font-weight:600;color:var(--muted);}
+ .on-purple .step{color:#fff;}
+ .home{min-width:44px;min-height:44px;padding:0 14px;border-radius:14px;border:2px solid #ddd3fb;background:#fff;color:var(--ink);font-size:15px;font-weight:600;cursor:pointer;}
+ .on-purple .home{border-color:#fff;}
+ h1{margin:0;text-align:center;font-weight:700;line-height:1.15;flex-shrink:0;}
+ .h-40{font-size:40px;} .h-34{font-size:34px;} .h-28{font-size:28px;}
+ .sub{margin:0;text-align:center;font-size:18px;color:var(--muted);flex-shrink:0;}
+ .spacer{flex:1 1 0;min-height:0;}
+ .talk{width:100%;aspect-ratio:1/1;max-height:342px;align-self:center;border:0;border-radius:20px;background:var(--purple);color:#fff;font-size:34px;font-weight:700;line-height:1.2;cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;flex-shrink:0;}
+ .wave{display:flex;justify-content:center;align-items:center;gap:6px;height:64px;flex-shrink:0;}
+ .wave span{width:8px;border-radius:4px;background:#fff;animation:bar 1s ease-in-out infinite;}
+ .wave span:nth-child(odd){animation-delay:-.4s;} .wave span:nth-child(3n){animation-delay:-.7s;}
+ @keyframes bar{0%,100%{height:14px}50%{height:56px}}
+ @media (prefers-reduced-motion:reduce){.wave span{animation:none;height:36px;}}
+ .quote{margin:0;text-align:center;font-size:26px;font-weight:700;line-height:1.25;color:#fff;flex-shrink:0;}
+ .item{display:flex;align-items:center;gap:14px;width:100%;padding:14px;border:0;border-radius:20px;background:#fff;cursor:pointer;text-align:left;flex-shrink:0;}
+ .item .sw{width:84px;height:84px;border-radius:14px;flex-shrink:0;}
+ .item .name{display:block;font-size:20px;font-weight:700;color:#222;}
+ .item .meta{display:block;margin-top:4px;font-size:15px;color:var(--card-sub);}
+ .hero{flex:1 1 auto;min-height:180px;border-radius:24px;background:repeating-linear-gradient(90deg,rgba(255,255,255,.07) 0 3px,transparent 3px 9px),var(--ocean);display:flex;align-items:center;justify-content:center;color:#fff;font-size:18px;font-weight:600;}
+ .btn{display:flex;align-items:center;justify-content:center;gap:10px;width:100%;flex-shrink:0;border:0;border-radius:20px;padding:0 16px;font-size:22px;font-weight:700;line-height:1.2;cursor:pointer;background:#fff;color:var(--ink);}
+ .btn-60{min-height:60px;} .btn-72{min-height:72px;} .btn-80{min-height:80px;}
+ .btn.primary{background:var(--purple);color:#fff;}
+ .btn.on-purple-btn{color:var(--purple);}
+ .status{margin:0;text-align:center;font-size:16px;font-weight:600;color:var(--ink);min-height:20px;flex-shrink:0;}
+ button:focus-visible{outline:3px solid #000;outline-offset:3px;box-shadow:0 0 0 6px #fff;}
+ .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;}
+ @media (max-width:405.98px){
+ .stage{width:100%;height:100dvh;min-height:640px;margin:0;border:0;border-radius:0;box-shadow:none;}
+ }
+</style>
+</head>
+<body>
+<main class="stage" aria-label="Talk To It photo capture mockup">
+
+ <!-- 1 · Hold to talk -->
+ <section class="screen is-active" data-screen="1" aria-labelledby="t1">
+ <div class="topbar"><p class="step">1 / 4 · Hold to talk</p></div>
+ <h1 id="t1" class="h-40">Just say it.</h1>
+ <p class="sub">“Find the blue Thibaut grasscloth”</p>
+ <div class="spacer"></div>
+ <button class="talk" data-go="2"><span aria-hidden="true">🎤</span><span>Hold to talk</span></button>
+ <div class="spacer"></div>
+ <button class="btn btn-60" data-go="3"><span aria-hidden="true">⌨️</span> Type instead</button>
+ </section>
+
+ <!-- 2 · Listening -->
+ <section class="screen on-purple" data-screen="2" aria-labelledby="t2" hidden>
+ <div class="topbar"><p class="step">2 / 4 · Listening</p><button class="home" data-go="1">⌂ Home</button></div>
+ <div class="spacer"></div>
+ <h1 id="t2" class="sr-only">Listening…</h1>
+ <div class="wave" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
+ <p class="quote">“find blue thibaut grasscloth”</p>
+ <div class="spacer"></div>
+ <button class="btn btn-80 on-purple-btn" data-go="3">Done talking</button>
+ </section>
+
+ <!-- 3 · Pick one -->
+ <section class="screen" data-screen="3" aria-labelledby="t3" hidden>
+ <div class="topbar"><p class="step">3 / 4 · Pick one</p><button class="home" data-go="1">⌂ Home</button></div>
+ <h1 id="t3" class="h-34">I found 3</h1>
+ <button class="item" data-go="4"><span class="sw" style="background:var(--ocean)" aria-hidden="true"></span><span><span class="name">Grasscloth — Ocean</span><span class="meta">Thibaut</span></span></button>
+ <button class="item" data-go="4"><span class="sw" style="background:var(--sky)" aria-hidden="true"></span><span><span class="name">Grasscloth — Sky</span><span class="meta">Thibaut</span></span></button>
+ <button class="item" data-go="4"><span class="sw" style="background:var(--navy)" aria-hidden="true"></span><span><span class="name">Grasscloth — Navy</span><span class="meta">Thibaut</span></span></button>
+ <div class="spacer"></div>
+ <button class="btn btn-72" data-go="2"><span aria-hidden="true">🎤</span> Say it again</button>
+ </section>
+
+ <!-- 4 · Item -->
+ <section class="screen" data-screen="4" aria-labelledby="t4" hidden>
+ <div class="topbar"><p class="step">4 / 4 · Item</p><button class="home" data-go="1">⌂ Home</button></div>
+ <div class="hero" role="img" aria-label="Grasscloth — Ocean swatch">Grasscloth — Ocean</div>
+ <h1 id="t4" class="h-28">Grasscloth — Ocean</h1>
+ <p class="sub">Thibaut · code T41020 · in stock</p>
+ <p class="status" id="status" aria-live="polite"></p>
+ <button class="btn btn-72 primary" id="updPhoto"><span aria-hidden="true">📸</span> Update the photo</button>
+ <button class="btn btn-72" data-go="1"><span aria-hidden="true">🎤</span> Find another</button>
+ </section>
+
+</main>
+<script>
+(function(){
+ var screens = Array.prototype.slice.call(document.querySelectorAll('[data-screen]'));
+ var status = document.getElementById('status');
+ function go(n){
+ n = String(n);
+ screens.forEach(function(s){
+ var on = s.getAttribute('data-screen') === n;
+ s.classList.toggle('is-active', on);
+ s.hidden = !on;
+ });
+ status.textContent = '';
+ var cur = document.querySelector('[data-screen="' + n + '"]');
+ if (cur){ cur.scrollTop = 0; var f = cur.querySelector('button'); if (f) f.focus({preventScroll:true}); }
+ }
+ document.getElementById('updPhoto').addEventListener('click', function(){
+ status.textContent = '📸 New photo saved (pretend) ✓';
+ });
+ document.addEventListener('click', function(e){
+ var b = e.target.closest('[data-go]');
+ if (b) go(b.getAttribute('data-go'));
+ });
+ window.__go = go;
+})();
+</script>
+<script>window.__theme = {id:'v11', name:'Talk To It', screens:4};</script>
+</body>
+</html>
diff --git a/public/themes/v12-bottom-tabs.html b/public/themes/v12-bottom-tabs.html
new file mode 100644
index 0000000..d94f2ce
--- /dev/null
+++ b/public/themes/v12-bottom-tabs.html
@@ -0,0 +1,194 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>v12 — Bottom Tabs</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --bg:#eef2f6;
+ --ink:#16202b;
+ --muted:#56616f; /* Figma #667383 @80% → darkened for 4.5:1 */
+ --red:#c8432e; /* Figma #e0533d → darkened so white text passes 4.5:1 */
+ --card:#ffffff;
+ --tab-ink:#555555;
+ --focus:#16202b;
+ }
+ *{box-sizing:border-box;}
+ html,body{margin:0;padding:0;background:#0a0a0a;font-family:'Inter',system-ui,sans-serif;}
+ .stage{
+ width:390px;height:780px;margin:24px auto;position:relative;
+ background:var(--bg);border-radius:36px;overflow:hidden;border:8px solid #000;
+ box-shadow:0 20px 60px rgba(0,0,0,.5);color:var(--ink);
+ display:flex;flex-direction:column;
+ }
+ @media (max-width:405.98px){
+ .stage{width:100%;margin:0;border:0;border-radius:0;height:100vh;height:100dvh;min-height:640px;}
+ }
+ .screens{flex:1;position:relative;min-height:0;}
+ .screen{
+ position:absolute;inset:0;display:none;flex-direction:column;gap:14px;
+ padding:22px 20px 12px;overflow-y:auto;
+ }
+ .screen.is-active{display:flex;}
+ .step{margin:0;font-size:13px;font-weight:600;color:var(--muted);}
+ h1{margin:0;font-size:32px;font-weight:700;text-align:center;color:var(--ink);}
+ h1:focus{outline:none;}
+ button{font:inherit;cursor:pointer;border:0;min-height:44px;min-width:44px;color:inherit;}
+ button:focus-visible{outline:3px solid var(--focus);outline-offset:3px;}
+
+ .camera{
+ flex:1;min-height:200px;background:#1c1c1e;border-radius:24px;
+ display:flex;align-items:center;justify-content:center;color:#fff;
+ font-size:18px;font-weight:600;position:relative;overflow:hidden;
+ }
+ .camera::after{
+ content:"";position:absolute;inset:18%;border:2px dashed rgba(255,255,255,.35);border-radius:14px;
+ }
+ .camera.flash{animation:flash .35s ease-out;}
+ @keyframes flash{0%{background:#fff;}100%{background:#1c1c1e;}}
+ .shutter-row{display:flex;justify-content:center;flex-shrink:0;}
+ .shutter{
+ width:96px;height:96px;border-radius:50%;background:#fff;padding:8px;
+ box-shadow:0 2px 8px rgba(0,0,0,.08);
+ }
+ .shutter span{display:block;width:100%;height:100%;border-radius:50%;background:var(--red);}
+ .big{
+ width:100%;min-height:76px;border-radius:20px;font-size:22px;font-weight:700;
+ padding:0 16px;flex-shrink:0;
+ }
+ .big.primary{background:var(--red);color:#fff;}
+ .big.plain{background:#fff;color:var(--ink);}
+ .search{min-height:60px;background:#fff;color:#56616f;}
+ .section-label{margin:0;font-size:18px;color:var(--muted);text-align:center;}
+ .card{
+ width:100%;display:flex;gap:14px;align-items:center;background:var(--card);
+ border-radius:20px;padding:14px;text-align:left;flex-shrink:0;
+ }
+ .sw{width:84px;height:84px;border-radius:14px;flex-shrink:0;}
+ .card .t{font-size:20px;font-weight:700;color:#222;display:block;}
+ .card .s{font-size:15px;color:#6b6b6b;display:block;margin-top:4px;} /* Figma #777 → #6b6b6b for 4.5:1 */
+ .spacer{flex:1;}
+
+ .tabbar{
+ flex-shrink:0;margin:0 20px 16px;background:#fff;border-radius:24px;padding:8px;
+ display:flex;gap:6px;
+ }
+ .tab{
+ flex:1;min-height:64px;border-radius:18px;background:transparent;color:var(--tab-ink);
+ font-size:14px;font-weight:700;display:flex;flex-direction:column;align-items:center;justify-content:center;line-height:1.25;
+ }
+ .tab[aria-current="page"]{background:var(--red);color:#fff;}
+ .tab .ico{font-size:15px;}
+
+ .toast{
+ position:absolute;left:50%;bottom:120px;transform:translateX(-50%) translateY(10px);
+ background:#16202b;color:#fff;padding:12px 18px;border-radius:14px;font-size:15px;font-weight:600;
+ opacity:0;visibility:hidden;pointer-events:none;transition:opacity .2s,transform .2s,visibility 0s .2s;max-width:86%;text-align:center;
+ }
+ .toast.show{opacity:1;visibility:visible;transform:translateX(-50%) translateY(0);transition:opacity .2s,transform .2s,visibility 0s;}
+ @media (prefers-reduced-motion:reduce){ .toast,.toast.show{transition:none} }
+</style>
+</head>
+<body>
+<main class="stage" id="app">
+ <div class="screens">
+
+ <section class="screen is-active" data-screen="1" aria-labelledby="h1-1">
+ <p class="step">1 / 4 · New tab</p>
+ <h1 id="h1-1">New item</h1>
+ <div class="camera" id="cam1" role="img" aria-label="Camera viewfinder">📷 point at the sample</div>
+ <div class="shutter-row">
+ <button class="shutter" aria-label="Take photo" data-flash="cam1" data-toast="📸 Saved as a draft — nice!"><span></span></button>
+ </div>
+ </section>
+
+ <section class="screen" data-screen="2" aria-labelledby="h1-2">
+ <p class="step">2 / 4 · Fix tab</p>
+ <h1 id="h1-2">Fix an item</h1>
+ <button class="big primary" data-toast="📷 Scanning the label…">📷 Scan the label</button>
+ <button class="big plain" data-toast="⌨️ Keyboard would open here">⌨️ Type the code</button>
+ <p class="section-label">Recently fixed</p>
+ <div class="card">
+ <div class="sw" style="background:#c9a24b"></div>
+ <div><span class="t">Damask — Gold</span><span class="s">York · 2 min ago</span></div>
+ </div>
+ </section>
+
+ <section class="screen" data-screen="3" aria-labelledby="h1-3">
+ <p class="step">3 / 4 · Find tab</p>
+ <h1 id="h1-3">Find</h1>
+ <button class="big search" data-toast="🔍 Search would open here">🔍 Search…</button>
+ <button class="card" data-toast="Linen — Oat · Kravet">
+ <span class="sw" style="background:#d8cbb3"></span>
+ <span><span class="t">Linen — Oat</span><span class="s">Kravet</span></span>
+ </button>
+ <button class="card" data-toast="Stripe — Navy · Schumacher">
+ <span class="sw" style="background:#1f3a5f"></span>
+ <span><span class="t">Stripe — Navy</span><span class="s">Schumacher</span></span>
+ </button>
+ <button class="card" data-toast="Floral — Rose · Thibaut">
+ <span class="sw" style="background:#d98c9a"></span>
+ <span><span class="t">Floral — Rose</span><span class="s">Thibaut</span></span>
+ </button>
+ </section>
+
+ <section class="screen" data-screen="4" aria-labelledby="h1-4">
+ <p class="step">4 / 4 · Sample tab</p>
+ <h1 id="h1-4">Sample came in</h1>
+ <div class="camera" style="flex:0 0 auto;height:260px" role="img" aria-label="Camera viewfinder">▢ scan the box label</div>
+ <div class="card">
+ <div class="sw" style="background:#d8cbb3"></div>
+ <div><span class="t">Linen — Oat</span><span class="s">Kravet · waiting 3 days</span></div>
+ </div>
+ <button class="big primary" style="min-height:72px" data-toast="✓ Received — printing sticker 🖨️">Mark received ✓</button>
+ </section>
+
+ </div>
+
+ <nav class="tabbar" aria-label="Sections">
+ <button class="tab" data-go="1" aria-current="page"><span class="ico" aria-hidden="true">➕</span>New</button>
+ <button class="tab" data-go="2"><span class="ico" aria-hidden="true">🔄</span>Fix</button>
+ <button class="tab" data-go="3"><span class="ico" aria-hidden="true">🔍</span>Find</button>
+ <button class="tab" data-go="4"><span class="ico" aria-hidden="true">📦</span>Sample</button>
+ </nav>
+
+ <div class="toast" id="toast" role="status" aria-live="polite"></div>
+</main>
+
+<script>
+ const screens = Array.from(document.querySelectorAll('[data-screen]'));
+ const tabs = Array.from(document.querySelectorAll('.tab'));
+ const toastEl = document.getElementById('toast');
+ let toastTimer = null;
+
+ function go(n){
+ n = String(n);
+ screens.forEach(s => s.classList.toggle('is-active', s.dataset.screen === n));
+ tabs.forEach(t => {
+ if (t.dataset.go === n) t.setAttribute('aria-current','page');
+ else t.removeAttribute('aria-current');
+ });
+ }
+ function toast(msg){
+ toastEl.textContent = msg;
+ toastEl.classList.add('show');
+ clearTimeout(toastTimer);
+ toastTimer = setTimeout(() => toastEl.classList.remove('show'), 1600);
+ }
+ document.addEventListener('click', (e) => {
+ const b = e.target.closest('button');
+ if (!b) return;
+ if (b.dataset.flash){
+ const cam = document.getElementById(b.dataset.flash);
+ cam.classList.remove('flash'); void cam.offsetWidth; cam.classList.add('flash');
+ }
+ if (b.dataset.toast) toast(b.dataset.toast);
+ if (b.dataset.go) go(b.dataset.go);
+ });
+</script>
+<script>window.__theme = {id:'v12', name:'Bottom Tabs', screens:4};</script>
+</body>
+</html>
diff --git a/public/themes/v13-photo-wall.html b/public/themes/v13-photo-wall.html
new file mode 100644
index 0000000..e0813a0
--- /dev/null
+++ b/public/themes/v13-photo-wall.html
@@ -0,0 +1,201 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>v13 — Photo Wall</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --ink:#222222;
+ --muted:#6b6b6b; /* Figma #8a8a8a → darkened for 4.5:1 on white */
+ --soft:#f2f2f2;
+ --focus:#222222;
+ }
+ *{box-sizing:border-box;}
+ html,body{margin:0;padding:0;background:#0a0a0a;font-family:'Inter',system-ui,sans-serif;}
+ .stage{
+ width:390px;height:780px;margin:24px auto;position:relative;display:block;
+ background:#fff;border-radius:36px;overflow:hidden;border:8px solid #000;
+ box-shadow:0 20px 60px rgba(0,0,0,.5);color:var(--ink);
+ }
+ @media (max-width:405.98px){
+ .stage{width:100%;margin:0;border:0;border-radius:0;height:100vh;height:100dvh;min-height:640px;}
+ }
+ .screen{
+ position:absolute;inset:0;display:none;flex-direction:column;gap:14px;
+ padding:18px 20px 18px;overflow-y:auto;background:#fff;
+ }
+ .screen.is-active{display:flex;}
+ .screen.dark{background:#111;--focus:#ffffff;}
+ .top{display:flex;align-items:center;justify-content:space-between;gap:10px;min-height:44px;flex-shrink:0;}
+ .step{margin:0;font-size:13px;font-weight:600;color:var(--muted);}
+ .dark .step{color:#8a8a8a;} /* 5.5:1 on #111 */
+ .home{
+ background:var(--soft);color:var(--ink);border-radius:14px;padding:0 14px;
+ font-size:15px;font-weight:700;
+ }
+ .dark .home{background:#2a2a2a;color:#fff;}
+ h1{margin:0;font-size:32px;font-weight:700;text-align:center;}
+ h1:focus{outline:none;}
+ .sub{margin:0;font-size:18px;color:var(--muted);text-align:center;}
+ button{font:inherit;cursor:pointer;border:0;min-height:44px;min-width:44px;color:inherit;}
+ button:focus-visible{outline:3px solid var(--focus);outline-offset:3px;}
+
+ .grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;flex-shrink:0;}
+ .tile{
+ height:120px;border-radius:20px;display:flex;align-items:center;justify-content:center;
+ color:#fff;font-size:48px;font-weight:700;padding:0;
+ }
+ .tile.add{background:#222;}
+ .tile.done{font-size:40px;}
+ .spacer{flex:1;}
+ .big{
+ width:100%;min-height:72px;border-radius:20px;font-size:22px;font-weight:700;padding:0 16px;flex-shrink:0;
+ }
+ .big.dark{background:#222;color:#fff;}
+ .big.soft{background:var(--soft);color:var(--ink);}
+ .photo{
+ flex:1;min-height:220px;border-radius:24px;display:flex;align-items:center;justify-content:center;
+ color:#fff;font-size:18px;font-weight:600;
+ }
+ .pill{background:rgba(17,17,17,.72);padding:10px 16px;border-radius:999px;}
+ .item-name{margin:0;font-size:26px;font-weight:700;text-align:center;}
+ .camera{
+ flex:1;min-height:220px;background:#2a2a2a;border-radius:24px;display:flex;align-items:center;justify-content:center;
+ color:#fff;font-size:18px;font-weight:600;position:relative;
+ }
+ .camera::after{content:"";position:absolute;inset:18%;border:2px dashed rgba(255,255,255,.3);border-radius:14px;}
+ .dark h1{color:#f2f2f2;font-size:30px;} /* Figma #222 on #111 was invisible */
+ .shutter-row{display:flex;justify-content:center;flex-shrink:0;}
+ .shutter{width:96px;height:96px;border-radius:50%;background:#fff;}
+ .toast{
+ position:absolute;left:50%;bottom:110px;transform:translateX(-50%) translateY(10px);
+ background:#222;color:#fff;padding:12px 18px;border-radius:14px;font-size:15px;font-weight:600;
+ opacity:0;visibility:hidden;pointer-events:none;transition:opacity .2s,transform .2s,visibility 0s .2s;max-width:86%;text-align:center;z-index:5;
+ }
+ .toast.show{opacity:1;visibility:visible;transform:translateX(-50%) translateY(0);transition:opacity .2s,transform .2s,visibility 0s;}
+ @media (prefers-reduced-motion:reduce){ .toast,.toast.show{transition:none} }
+</style>
+</head>
+<body>
+<main class="stage" id="app">
+
+ <section class="screen is-active" data-screen="1" aria-labelledby="h1-1">
+ <div class="top"><p class="step">1 / 4 · Wall</p></div>
+ <h1 id="h1-1">My photos</h1>
+ <p class="sub">Tap + to add · tap a photo to fix it</p>
+ <div class="grid" id="wall"></div>
+ <div class="spacer"></div>
+ <button class="big soft" data-toast="🔍 Search would open here">🔍 Find something</button>
+ </section>
+
+ <section class="screen" data-screen="2" aria-labelledby="h1-2">
+ <div class="top">
+ <p class="step">2 / 4 · Tapped a photo</p>
+ <button class="home" data-go="1">⌂ My wall</button>
+ </div>
+ <div class="photo" id="photo" role="img" aria-label="Current photo"><span class="pill">📷 point at the sample</span></div>
+ <h1 id="h1-2" class="item-name">Grasscloth — Ocean</h1>
+ <p class="sub" id="meta">Thibaut · added yesterday</p>
+ <button class="big dark" data-go="3">📸 Retake this photo</button>
+ <button class="big soft" data-toast="📦 Marked as received">📦 Sample came in</button>
+ </section>
+
+ <section class="screen dark" data-screen="3" aria-labelledby="h1-3">
+ <div class="top">
+ <p class="step">3 / 4 · Retake</p>
+ <button class="home" data-go="1">⌂ My wall</button>
+ </div>
+ <h1 id="h1-3">Retake it</h1>
+ <div class="camera" role="img" aria-label="Camera viewfinder">📷 point at the sample</div>
+ <div class="shutter-row"><button class="shutter" aria-label="Take photo" data-go="4"></button></div>
+ </section>
+
+ <section class="screen" data-screen="4" aria-labelledby="h1-4">
+ <div class="top"><p class="step">4 / 4 · Swapped</p></div>
+ <h1 id="h1-4" style="font-size:30px">New photo in ✨</h1>
+ <div class="grid" id="wall2" role="img" aria-label="Your photo wall with the new photo marked"></div>
+ <div class="spacer"></div>
+ <button class="big dark" data-go="1">Back to my wall</button>
+ </section>
+
+ <div class="toast" id="toast" role="status" aria-live="polite"></div>
+</main>
+
+<script>
+ const items = [
+ {c:'#3c6e9e', n:'Grasscloth — Ocean', m:'Thibaut · added yesterday'},
+ {c:'#d98c9a', n:'Floral — Rose', m:'Thibaut · added 2 days ago'},
+ {c:'#8fae8b', n:'Palm — Sage', m:'York · added Monday'},
+ {c:'#d8cbb3', n:'Linen — Oat', m:'Kravet · added Monday'},
+ {c:'#1f3a5f', n:'Stripe — Navy', m:'Schumacher · last week'},
+ {c:'#b9a27a', n:'Sisal — Wheat', m:'Phillip Jeffries · last week'},
+ {c:'#e0533d', n:'Damask — Coral', m:'York · last week'},
+ {c:'#7c93b0', n:'Ikat — Denim', m:'Kravet · 2 weeks ago'},
+ ];
+ let picked = 0;
+ const wall = document.getElementById('wall');
+ const wall2 = document.getElementById('wall2');
+
+ function buildWall(){
+ wall.innerHTML = '';
+ const add = document.createElement('button');
+ add.className = 'tile add'; add.textContent = '+'; add.setAttribute('aria-label','Add a new photo');
+ add.dataset.toast = '📸 Camera opens for a new item';
+ wall.appendChild(add);
+ items.forEach((it, i) => {
+ const b = document.createElement('button');
+ b.className = 'tile'; b.style.background = it.c;
+ b.setAttribute('aria-label', 'Fix photo: ' + it.n);
+ b.dataset.pick = i; b.dataset.go = '2';
+ wall.appendChild(b);
+ });
+ }
+ function buildWall2(){
+ wall2.innerHTML = '<div class="tile add" aria-hidden="true">+</div>';
+ items.forEach((it, i) => {
+ const d = document.createElement('div');
+ d.className = 'tile' + (i === picked ? ' done' : '');
+ d.style.background = i === picked ? '#2e9e5b' : it.c;
+ d.textContent = i === picked ? '✓' : '';
+ d.setAttribute('aria-hidden','true');
+ wall2.appendChild(d);
+ });
+ }
+
+ const screens = Array.from(document.querySelectorAll('[data-screen]'));
+ const toastEl = document.getElementById('toast');
+ let toastTimer = null;
+ function go(n){
+ n = String(n);
+ if (n === '4') buildWall2();
+ screens.forEach(s => s.classList.toggle('is-active', s.dataset.screen === n));
+ const h = document.getElementById('h1-' + n);
+ if (h && document.activeElement && document.activeElement !== document.body){
+ h.setAttribute('tabindex','-1'); h.focus({preventScroll:true});
+ }
+ }
+ function toast(msg){
+ toastEl.textContent = msg; toastEl.classList.add('show');
+ clearTimeout(toastTimer); toastTimer = setTimeout(() => toastEl.classList.remove('show'), 1600);
+ }
+ document.addEventListener('click', (e) => {
+ const b = e.target.closest('button'); if (!b) return;
+ if (b.dataset.pick !== undefined){
+ picked = Number(b.dataset.pick);
+ const it = items[picked];
+ document.getElementById('photo').style.background = it.c;
+ document.getElementById('h1-2').textContent = it.n;
+ document.getElementById('meta').textContent = it.m;
+ }
+ if (b.dataset.toast) toast(b.dataset.toast);
+ if (b.dataset.go) go(b.dataset.go);
+ });
+ buildWall();
+ document.getElementById('photo').style.background = items[0].c;
+</script>
+<script>window.__theme = {id:'v13', name:'Photo Wall', screens:4};</script>
+</body>
+</html>
diff --git a/public/themes/v14-sample-ticket.html b/public/themes/v14-sample-ticket.html
new file mode 100644
index 0000000..ff771ea
--- /dev/null
+++ b/public/themes/v14-sample-ticket.html
@@ -0,0 +1,173 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>v14 — Sample Ticket</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --paper:#f4e9d8;
+ --ink:#2b2b2b;
+ --muted:#6e604c; /* Figma #8a7b66 → darkened for 4.5:1 on paper */
+ --rust:#b8452f;
+ --focus:#2b2b2b;
+ }
+ *{box-sizing:border-box;}
+ html,body{margin:0;padding:0;background:#0a0a0a;font-family:'Inter',system-ui,sans-serif;}
+ .stage{
+ width:390px;height:780px;margin:24px auto;position:relative;display:block;
+ background:var(--paper);border-radius:36px;overflow:hidden;border:8px solid #000;
+ box-shadow:0 20px 60px rgba(0,0,0,.5);color:var(--ink);
+ }
+ @media (max-width:405.98px){
+ .stage{width:100%;margin:0;border:0;border-radius:0;height:100vh;height:100dvh;min-height:640px;}
+ }
+ .screen{
+ position:absolute;inset:0;display:none;flex-direction:column;gap:14px;
+ padding:18px 20px 18px;overflow-y:auto;background:var(--paper);
+ }
+ .screen.is-active{display:flex;}
+ .screen.dark{background:#2b2b2b;--focus:#f4e9d8;}
+ .top{display:flex;align-items:center;justify-content:space-between;gap:10px;min-height:44px;flex-shrink:0;}
+ .step{margin:0;font-size:13px;font-weight:600;color:var(--muted);}
+ .dark .step{color:#b3a48d;}
+ .home{
+ background:#fff;color:var(--ink);border-radius:14px;padding:0 14px;font-size:15px;font-weight:700;
+ border:2px solid var(--ink);
+ }
+ .dark .home{background:transparent;color:#f4e9d8;border-color:#f4e9d8;}
+ h1{margin:0;font-size:32px;font-weight:700;text-align:center;}
+ h1:focus{outline:none;}
+ .sub{margin:0;font-size:18px;color:var(--muted);text-align:center;}
+ button{font:inherit;cursor:pointer;border:0;min-height:44px;min-width:44px;color:inherit;}
+ button:focus-visible{outline:3px solid var(--focus);outline-offset:3px;}
+
+ .camera{
+ flex:1;min-height:220px;background:#1c1c1e;border-radius:24px;display:flex;align-items:center;justify-content:center;
+ color:#fff;font-size:18px;font-weight:600;position:relative;
+ }
+ .camera::after{content:"";position:absolute;left:14%;right:14%;top:32%;bottom:32%;border:2px dashed rgba(255,255,255,.4);border-radius:8px;}
+ .shutter-row{display:flex;justify-content:center;flex-shrink:0;}
+ .shutter{width:96px;height:96px;border-radius:50%;background:#fff;padding:8px;box-shadow:0 2px 8px rgba(0,0,0,.08);}
+ .shutter span{display:block;width:100%;height:100%;border-radius:50%;background:var(--rust);}
+
+ .ticket{
+ background:#fff;border:2px dashed var(--ink);border-radius:6px;padding:20px;
+ display:flex;flex-direction:column;gap:10px;font-size:17px;flex-shrink:0;margin:0;
+ }
+ .ticket .row{display:flex;justify-content:space-between;gap:12px;}
+ .ticket dt{color:#6b6b6b;} /* Figma #777 → #6b6b6b for 4.5:1 */
+ .ticket dd{margin:0;font-weight:700;text-align:right;}
+ .spacer{flex:1;}
+ .big{width:100%;min-height:72px;border-radius:20px;font-size:22px;font-weight:700;padding:0 16px;flex-shrink:0;}
+ .big.primary{background:var(--rust);color:#fff;min-height:80px;}
+ .big.plain{background:#fff;color:var(--ink);}
+ .big.ghost{background:transparent;color:#f4e9d8;border:2px solid #f4e9d8;min-height:64px;}
+
+ .printer{margin:0;font-size:96px;text-align:center;line-height:1.1;}
+ .printer.go{animation:bob 1s ease-in-out infinite;}
+ @keyframes bob{50%{transform:translateY(-6px);}}
+ .dark h1{color:#f4e9d8;} /* Figma #2b2b2b on #2b2b2b was invisible */
+ .dark .sub{color:#b3a48d;font-size:20px;} /* Figma #8a7b66 → lightened for 4.5:1 on dark */
+ .stamped{font-size:44px;}
+ .toast{
+ position:absolute;left:50%;bottom:110px;transform:translateX(-50%) translateY(10px);
+ background:#2b2b2b;color:#fff;padding:12px 18px;border-radius:14px;font-size:15px;font-weight:600;
+ opacity:0;visibility:hidden;pointer-events:none;transition:opacity .2s,transform .2s,visibility 0s .2s;max-width:86%;text-align:center;z-index:5;
+ }
+ .toast.show{opacity:1;visibility:visible;transform:translateX(-50%) translateY(0);transition:opacity .2s,transform .2s,visibility 0s;}
+ @media (prefers-reduced-motion:reduce){ .toast,.toast.show{transition:none} }
+</style>
+</head>
+<body>
+<main class="stage" id="app">
+
+ <section class="screen is-active" data-screen="1" aria-labelledby="h1-1">
+ <div class="top"><p class="step">1 / 4 · Box arrived</p></div>
+ <h1 id="h1-1">📦 A box came in?</h1>
+ <p class="sub">Point at the label on the box</p>
+ <div class="camera" role="img" aria-label="Camera viewfinder">▢ box label here</div>
+ <div class="shutter-row"><button class="shutter" aria-label="Scan the box label" data-go="2"><span></span></button></div>
+ </section>
+
+ <section class="screen" data-screen="2" aria-labelledby="h1-2">
+ <div class="top">
+ <p class="step">2 / 4 · Ticket</p>
+ <button class="home" data-go="1">⌂ Home</button>
+ </div>
+ <h1 id="h1-2" style="font-size:30px">Here’s your ticket</h1>
+ <dl class="ticket">
+ <div class="row"><dt>Item</dt><dd>Linen — Oat</dd></div>
+ <div class="row"><dt>Brand</dt><dd>Kravet</dd></div>
+ <div class="row"><dt>Code</dt><dd>K-4410</dd></div>
+ <div class="row"><dt>Ordered</dt><dd>Sep 22</dd></div>
+ <div class="row"><dt>For</dt><dd>Stevie’s client</dd></div>
+ </dl>
+ <div class="spacer"></div>
+ <button class="big primary" data-go="3">Looks right ✓</button>
+ <button class="big plain" data-go="1" data-toast="No problem — scan it again">Something’s off</button>
+ </section>
+
+ <section class="screen dark" data-screen="3" aria-labelledby="h1-3">
+ <div class="top">
+ <p class="step">3 / 4 · Printing</p>
+ <button class="home" data-go="1">⌂ Home</button>
+ </div>
+ <div class="spacer"></div>
+ <p class="printer go" aria-hidden="true">🖨️</p>
+ <h1 id="h1-3">Printing sticker…</h1>
+ <p class="sub">Grab it from the printer</p>
+ <div class="spacer"></div>
+ <button class="big ghost" data-go="4">Got the sticker →</button>
+ </section>
+
+ <section class="screen" data-screen="4" aria-labelledby="h1-4">
+ <div class="top">
+ <p class="step">4 / 4 · Stamped</p>
+ <button class="home" data-go="1">⌂ Home</button>
+ </div>
+ <div class="spacer"></div>
+ <dl class="ticket">
+ <div class="row"><dt>Item</dt><dd>Linen — Oat</dd></div>
+ <div class="row"><dt>Status</dt><dd>RECEIVED ✓</dd></div>
+ <div class="row"><dt>Time</dt><dd id="stamp-time">2:14 PM</dd></div>
+ </dl>
+ <h1 id="h1-4" class="stamped">Stamped!</h1>
+ <div class="spacer"></div>
+ <button class="big primary" style="min-height:72px" data-go="1">Next box</button>
+ </section>
+
+ <div class="toast" id="toast" role="status" aria-live="polite"></div>
+</main>
+
+<script>
+ const screens = Array.from(document.querySelectorAll('[data-screen]'));
+ const toastEl = document.getElementById('toast');
+ let toastTimer = null;
+ function go(n){
+ n = String(n);
+ if (n === '4'){
+ document.getElementById('stamp-time').textContent =
+ new Date().toLocaleTimeString(undefined, {hour:'numeric', minute:'2-digit'});
+ }
+ screens.forEach(s => s.classList.toggle('is-active', s.dataset.screen === n));
+ const h = document.getElementById('h1-' + n);
+ if (h && document.activeElement && document.activeElement !== document.body){
+ h.setAttribute('tabindex','-1'); h.focus({preventScroll:true});
+ }
+ }
+ function toast(msg){
+ toastEl.textContent = msg; toastEl.classList.add('show');
+ clearTimeout(toastTimer); toastTimer = setTimeout(() => toastEl.classList.remove('show'), 1600);
+ }
+ document.addEventListener('click', (e) => {
+ const b = e.target.closest('button'); if (!b) return;
+ if (b.dataset.toast) toast(b.dataset.toast);
+ if (b.dataset.go) go(b.dataset.go);
+ });
+</script>
+<script>window.__theme = {id:'v14', name:'Sample Ticket', screens:4};</script>
+</body>
+</html>
diff --git a/public/themes/v15-giant-words.html b/public/themes/v15-giant-words.html
new file mode 100644
index 0000000..9a9c4e9
--- /dev/null
+++ b/public/themes/v15-giant-words.html
@@ -0,0 +1,159 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>v15 — Giant Words</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --yellow:#f5e663;
+ --grey:#9a9a9a;
+ --focus:#ffffff;
+ }
+ *{box-sizing:border-box;}
+ html,body{margin:0;padding:0;background:#0a0a0a;font-family:'Inter',system-ui,sans-serif;}
+ .stage{
+ width:390px;height:780px;margin:24px auto;position:relative;display:block;
+ background:#000;border-radius:36px;overflow:hidden;border:8px solid #000;
+ box-shadow:0 20px 60px rgba(0,0,0,.5),0 0 0 1px #222;color:#fff;
+ }
+ @media (max-width:405.98px){
+ .stage{width:100%;margin:0;border:0;border-radius:0;box-shadow:none;height:100vh;height:100dvh;min-height:640px;}
+ }
+ .screen{
+ position:absolute;inset:0;display:none;flex-direction:column;gap:14px;
+ padding:18px 20px 18px;overflow-y:auto;background:#000;
+ }
+ .screen.is-active{display:flex;}
+ .screen.yellow{background:var(--yellow);color:#000;--focus:#000000;}
+ .top{display:flex;align-items:center;justify-content:space-between;gap:10px;min-height:44px;flex-shrink:0;}
+ .step{margin:0;font-size:13px;font-weight:600;color:var(--grey);}
+ .yellow .step{color:#555;} /* Figma #9a9a9a on yellow → #555 for 4.5:1 */
+ .home{
+ background:#1f1f1f;color:#fff;border-radius:14px;padding:0 16px;font-size:15px;font-weight:800;letter-spacing:.04em;
+ }
+ .yellow .home{background:#000;color:var(--yellow);}
+ h1,.giant{margin:0;font-size:clamp(56px,18vw,72px);font-weight:800;line-height:1;letter-spacing:-.02em;}
+ h1:focus{outline:none;}
+ button{font:inherit;cursor:pointer;border:0;min-height:44px;min-width:44px;color:inherit;}
+ button:focus-visible{outline:3px solid var(--focus);outline-offset:3px;}
+
+ .words{display:flex;flex-direction:column;gap:6px;}
+ .word{
+ background:transparent;color:#fff;text-align:left;padding:4px 0;
+ font-size:clamp(56px,18vw,72px);font-weight:800;line-height:1.05;letter-spacing:-.02em;
+ border-radius:10px;width:100%;
+ }
+ .word:hover{color:var(--yellow);}
+ .word .arrow{font-size:.45em;vertical-align:middle;opacity:0;margin-left:8px;transition:opacity .15s;}
+ .word:hover .arrow,.word:focus-visible .arrow{opacity:1;}
+ .hint{margin:0;font-size:18px;color:var(--grey);text-align:center;}
+ .spacer{flex:1;}
+ .big{width:100%;min-height:110px;border-radius:20px;font-size:22px;font-weight:800;padding:0 16px;flex-shrink:0;letter-spacing:.02em;}
+ .big.y{background:var(--yellow);color:#000;}
+ .big.w{background:#fff;color:#000;}
+ .big.g{background:#333;color:#fff;}
+ .big.k{background:#000;color:var(--yellow);min-height:96px;}
+ .camera{
+ flex:1;min-height:220px;background:#222;border-radius:24px;display:flex;align-items:center;justify-content:center;
+ color:#fff;font-size:18px;font-weight:600;position:relative;
+ }
+ .camera::after{content:"";position:absolute;inset:20%;border:3px solid rgba(245,230,99,.55);border-radius:14px;}
+ .shutter-row{display:flex;justify-content:center;flex-shrink:0;}
+ .shutter{width:96px;height:96px;border-radius:50%;background:var(--yellow);border:6px solid #fff;}
+ .swatch{
+ height:240px;flex-shrink:0;border-radius:24px;background:#b9a27a;display:flex;align-items:center;justify-content:center;
+ }
+ .pill{background:rgba(0,0,0,.72);color:#fff;padding:10px 16px;border-radius:999px;font-size:18px;font-weight:600;}
+ .got{font-size:clamp(64px,20vw,80px);color:#000;} /* Figma white on yellow → black for contrast */
+ .meta{margin:0;font-size:24px;font-weight:800;color:#000;}
+ .toast{
+ position:absolute;left:50%;bottom:120px;transform:translateX(-50%) translateY(10px);
+ background:#fff;color:#000;padding:12px 18px;border-radius:14px;font-size:16px;font-weight:800;
+ opacity:0;visibility:hidden;pointer-events:none;transition:opacity .2s,transform .2s,visibility 0s .2s;max-width:86%;text-align:center;z-index:5;
+ }
+ .toast.show{opacity:1;visibility:visible;transform:translateX(-50%) translateY(0);transition:opacity .2s,transform .2s,visibility 0s;}
+ @media (prefers-reduced-motion:reduce){ .toast,.toast.show{transition:none} }
+</style>
+</head>
+<body>
+<main class="stage" id="app">
+
+ <section class="screen is-active" data-screen="1" aria-labelledby="h1-1">
+ <div class="top"><p class="step">1 / 4 · Home</p></div>
+ <h1 id="h1-1" class="sr-only" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;">What do you want to do?</h1>
+ <div class="words">
+ <button class="word" data-go="3" data-mode="NEW">NEW<span class="arrow" aria-hidden="true">→</span></button>
+ <button class="word" data-go="3" data-mode="FIX">FIX<span class="arrow" aria-hidden="true">→</span></button>
+ <button class="word" data-go="2">FIND<span class="arrow" aria-hidden="true">→</span></button>
+ <button class="word" data-go="3" data-mode="SAMPLE">SAMPLE<span class="arrow" aria-hidden="true">→</span></button>
+ </div>
+ <div class="spacer"></div>
+ <p class="hint">Tap a word.</p>
+ </section>
+
+ <section class="screen" data-screen="2" aria-labelledby="h1-2">
+ <div class="top">
+ <p class="step">2 / 4 · FIND</p>
+ <button class="home" data-go="1">⌂ HOME</button>
+ </div>
+ <h1 id="h1-2">FIND</h1>
+ <div class="spacer"></div>
+ <button class="big y" data-go="3" data-mode="FIND">📷 SCAN</button>
+ <button class="big w" data-toast="🎤 Listening…">🎤 SAY IT</button>
+ <button class="big g" data-toast="⌨️ Keyboard would open">⌨️ TYPE IT</button>
+ </section>
+
+ <section class="screen" data-screen="3" aria-labelledby="h1-3">
+ <div class="top">
+ <p class="step" id="step3">3 / 4 · Scan</p>
+ <button class="home" data-go="1">⌂ HOME</button>
+ </div>
+ <h1 id="h1-3">POINT.</h1>
+ <div class="camera" role="img" aria-label="Camera viewfinder">▢</div>
+ <div class="shutter-row"><button class="shutter" aria-label="Take photo" data-go="4"></button></div>
+ </section>
+
+ <section class="screen yellow" data-screen="4" aria-labelledby="h1-4">
+ <div class="top">
+ <p class="step">4 / 4 · Got it</p>
+ <button class="home" data-go="1">⌂ HOME</button>
+ </div>
+ <h1 id="h1-4" class="got">GOT IT.</h1>
+ <div class="swatch" role="img" aria-label="Linen — Oat swatch"><span class="pill">Linen — Oat</span></div>
+ <p class="meta">Linen — Oat · Kravet</p>
+ <div class="spacer"></div>
+ <button class="big k" data-toast="Opening Linen — Oat…">OPEN IT</button>
+ </section>
+
+ <div class="toast" id="toast" role="status" aria-live="polite"></div>
+</main>
+
+<script>
+ const screens = Array.from(document.querySelectorAll('[data-screen]'));
+ const toastEl = document.getElementById('toast');
+ let toastTimer = null;
+ function go(n){
+ n = String(n);
+ screens.forEach(s => s.classList.toggle('is-active', s.dataset.screen === n));
+ const h = document.getElementById('h1-' + n);
+ if (h && document.activeElement && document.activeElement !== document.body){
+ h.setAttribute('tabindex','-1'); h.focus({preventScroll:true});
+ }
+ }
+ function toast(msg){
+ toastEl.textContent = msg; toastEl.classList.add('show');
+ clearTimeout(toastTimer); toastTimer = setTimeout(() => toastEl.classList.remove('show'), 1600);
+ }
+ document.addEventListener('click', (e) => {
+ const b = e.target.closest('button'); if (!b) return;
+ if (b.dataset.mode) document.getElementById('step3').textContent = '3 / 4 · Scan · ' + b.dataset.mode;
+ if (b.dataset.toast) toast(b.dataset.toast);
+ if (b.dataset.go) go(b.dataset.go);
+ });
+</script>
+<script>window.__theme = {id:'v15', name:'Giant Words', screens:4};</script>
+</body>
+</html>
diff --git a/public/themes/v16-sticky-notes.html b/public/themes/v16-sticky-notes.html
new file mode 100644
index 0000000..783718b
--- /dev/null
+++ b/public/themes/v16-sticky-notes.html
@@ -0,0 +1,160 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>v16 — Sticky Notes</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --board:#ede6da;
+ --ink:#2b2b2b;
+ --muted:#5f574b; /* Figma #7d7466 @80% → darkened for 4.5:1 */
+ --yellow:#ffe66d;
+ --aqua:#a0e7e5;
+ --pink:#ffaebc;
+ --mint:#b4f8c8;
+ --focus:#2b2b2b;
+ }
+ *{box-sizing:border-box;}
+ html,body{margin:0;padding:0;background:#0a0a0a;font-family:'Inter',system-ui,sans-serif;}
+ .stage{
+ width:390px;height:780px;margin:24px auto;position:relative;display:block;
+ background:var(--board);border-radius:36px;overflow:hidden;border:8px solid #000;
+ box-shadow:0 20px 60px rgba(0,0,0,.5);color:var(--ink);
+ }
+ @media (max-width:405.98px){
+ .stage{width:100%;margin:0;border:0;border-radius:0;height:100vh;height:100dvh;min-height:640px;}
+ }
+ .screen{
+ position:absolute;inset:0;display:none;flex-direction:column;gap:14px;
+ padding:18px 20px 18px;overflow-y:auto;overflow-x:hidden;background:var(--board);
+ }
+ .screen.is-active{display:flex;}
+ .screen.note-bg{background:var(--yellow);}
+ .top{display:flex;align-items:center;justify-content:space-between;gap:10px;min-height:44px;flex-shrink:0;}
+ .step{margin:0;font-size:13px;font-weight:600;color:var(--muted);}
+ .home{background:#fff;color:var(--ink);border-radius:4px;padding:0 14px;font-size:15px;font-weight:700;box-shadow:0 2px 0 rgba(0,0,0,.12);}
+ h1{margin:0;font-size:32px;font-weight:700;text-align:center;}
+ h1.left{text-align:left;font-size:28px;}
+ h1:focus{outline:none;}
+ button{font:inherit;cursor:pointer;border:0;min-height:44px;min-width:44px;color:inherit;}
+ button:focus-visible{outline:3px solid var(--focus);outline-offset:3px;}
+
+ .board{display:grid;grid-template-columns:1fr 1fr;gap:16px 12px;padding:6px 4px;flex-shrink:0;}
+ .note{
+ height:170px;border-radius:4px;display:flex;align-items:center;justify-content:center;text-align:center;
+ font-size:24px;font-weight:700;color:var(--ink);line-height:1.2;padding:8px;
+ box-shadow:0 6px 12px rgba(60,40,10,.14),0 1px 0 rgba(0,0,0,.06);
+ transition:transform .15s;position:relative;
+ }
+ .note::before{ /* tape strip */
+ content:"";position:absolute;top:-8px;left:50%;width:54px;height:16px;transform:translateX(-50%) rotate(-2deg);
+ background:rgba(255,255,255,.55);border-radius:2px;
+ }
+ .note:hover{transform:rotate(0deg) scale(1.03) !important;}
+ .r3{transform:rotate(3deg);} .rm2{transform:rotate(-2deg);} .r2{transform:rotate(2deg);}
+ .y{background:var(--yellow);} .a{background:var(--aqua);} .p{background:var(--pink);} .m{background:var(--mint);}
+
+ .camera{
+ flex:1;min-height:220px;background:#1c1c1e;border-radius:6px;display:flex;align-items:center;justify-content:center;
+ color:#fff;font-size:18px;font-weight:600;position:relative;
+ }
+ .camera::after{content:"";position:absolute;inset:18%;border:2px dashed rgba(255,255,255,.35);border-radius:4px;}
+ .shutter-row{display:flex;justify-content:center;flex-shrink:0;}
+ .shutter{width:96px;height:96px;border-radius:50%;background:#2b2b2b;border:6px solid #fff;}
+ .choice{
+ width:100%;min-height:72px;border-radius:4px;background:#fff;color:var(--ink);font-size:22px;font-weight:700;flex-shrink:0;
+ box-shadow:0 2px 0 rgba(0,0,0,.08);
+ }
+ .choice[aria-pressed="true"]{background:#2b2b2b;color:#fff;}
+ .spacer{flex:1;}
+ .big{width:100%;min-height:72px;border-radius:4px;background:#2b2b2b;color:#fff;font-size:22px;font-weight:700;padding:0 16px;flex-shrink:0;}
+ .pinned-note{font-size:18px;height:150px;}
+ .add-note{background:#fff;font-size:48px;height:150px;}
+ .toast{
+ position:absolute;left:50%;bottom:110px;transform:translateX(-50%) translateY(10px);
+ background:#2b2b2b;color:#fff;padding:12px 18px;border-radius:4px;font-size:15px;font-weight:600;
+ opacity:0;visibility:hidden;pointer-events:none;transition:opacity .2s,transform .2s,visibility 0s .2s;max-width:86%;text-align:center;z-index:5;
+ }
+ .toast.show{opacity:1;visibility:visible;transform:translateX(-50%) translateY(0);transition:opacity .2s,transform .2s,visibility 0s;}
+ @media (prefers-reduced-motion:reduce){ .toast,.toast.show{transition:none} }
+</style>
+</head>
+<body>
+<main class="stage" id="app">
+
+ <section class="screen is-active" data-screen="1" aria-labelledby="h1-1">
+ <div class="top"><p class="step">1 / 4 · Board</p></div>
+ <h1 id="h1-1">Pick a note</h1>
+ <div class="board">
+ <button class="note y r3" data-go="2" data-job="✏️ New item">New<br>item ✏️</button>
+ <button class="note a rm2" data-go="2" data-job="🔧 Fix an item">Fix an<br>item 🔧</button>
+ <button class="note p rm2" data-go="2" data-job="🔍 Find it">Find<br>it 🔍</button>
+ <button class="note m r2" data-go="2" data-job="📦 Sample in">Sample<br>in 📦</button>
+ </div>
+ </section>
+
+ <section class="screen note-bg" data-screen="2" aria-labelledby="h1-2">
+ <div class="top">
+ <p class="step">2 / 4 · Snap</p>
+ <button class="home" data-go="1">⌂ Board</button>
+ </div>
+ <h1 id="h1-2" class="left">✏️ New item</h1>
+ <div class="camera" role="img" aria-label="Camera viewfinder">📷 point at the sample</div>
+ <div class="shutter-row"><button class="shutter" aria-label="Take photo" data-go="3"></button></div>
+ </section>
+
+ <section class="screen note-bg" data-screen="3" aria-labelledby="h1-3">
+ <div class="top">
+ <p class="step">3 / 4 · Write it</p>
+ <button class="home" data-go="1">⌂ Board</button>
+ </div>
+ <h1 id="h1-3" class="left">Jot the brand</h1>
+ <button class="choice" data-brand="Thibaut" aria-pressed="false">Thibaut</button>
+ <button class="choice" data-brand="York" aria-pressed="false">York</button>
+ <button class="choice" data-brand="Kravet" aria-pressed="false">Kravet</button>
+ <div class="spacer"></div>
+ <button class="big" data-go="4">Stick it on the board 📌</button>
+ </section>
+
+ <section class="screen" data-screen="4" aria-labelledby="h1-4">
+ <div class="top"><p class="step">4 / 4 · Pinned</p></div>
+ <h1 id="h1-4" style="font-size:36px">📌 Pinned!</h1>
+ <div class="board">
+ <div class="note y r3 pinned-note" id="pinned">Linen — Oat<br>Kravet</div>
+ <button class="note add-note" data-go="2" aria-label="Add another note">+</button>
+ </div>
+ <div class="spacer"></div>
+ <button class="big" data-go="1">Back to the board</button>
+ </section>
+
+ <div class="toast" id="toast" role="status" aria-live="polite"></div>
+</main>
+
+<script>
+ const screens = Array.from(document.querySelectorAll('[data-screen]'));
+ let brand = 'Kravet';
+ function go(n){
+ n = String(n);
+ if (n === '4') document.getElementById('pinned').innerHTML = 'Linen — Oat<br>' + brand;
+ screens.forEach(s => s.classList.toggle('is-active', s.dataset.screen === n));
+ const h = document.getElementById('h1-' + n);
+ if (h && document.activeElement && document.activeElement !== document.body){
+ h.setAttribute('tabindex','-1'); h.focus({preventScroll:true});
+ }
+ }
+ document.addEventListener('click', (e) => {
+ const b = e.target.closest('button'); if (!b) return;
+ if (b.dataset.job) document.getElementById('h1-2').textContent = b.dataset.job;
+ if (b.dataset.brand){
+ brand = b.dataset.brand;
+ document.querySelectorAll('.choice').forEach(c => c.setAttribute('aria-pressed', String(c === b)));
+ }
+ if (b.dataset.go) go(b.dataset.go);
+ });
+</script>
+<script>window.__theme = {id:'v16', name:'Sticky Notes', screens:4};</script>
+</body>
+</html>
diff --git a/public/themes/v17-game-pad.html b/public/themes/v17-game-pad.html
new file mode 100644
index 0000000..640f4c2
--- /dev/null
+++ b/public/themes/v17-game-pad.html
@@ -0,0 +1,192 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>v17 — Game Pad</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --bg:#1b1f3b;
+ --deep:#0f1226;
+ --panel:#2a2f5a;
+ --muted:#8e94c7;
+ --a:#ff4f7b;
+ --b:#ffc23f;
+ --x:#3fb8ff;
+ --y:#5be37d;
+ --ink:#1b1f3b;
+ --focus:#ffc23f;
+ }
+ *{box-sizing:border-box;}
+ html,body{margin:0;padding:0;background:#0a0a0a;font-family:'Inter',system-ui,sans-serif;}
+ .stage{
+ width:390px;height:780px;margin:24px auto;position:relative;
+ background:var(--bg);border-radius:36px;overflow:hidden;
+ box-shadow:0 20px 60px rgba(0,0,0,.5);border:8px solid #000;color:#fff;
+ display:flex;flex-direction:column;
+ }
+ @media (max-width:405.98px){
+ .stage{width:100%;height:100dvh;min-height:640px;margin:0;border:0;border-radius:0;}
+ }
+ .screen{display:none;flex:1;min-height:0;flex-direction:column;gap:16px;padding:18px 24px 28px;overflow-y:auto;overflow-x:hidden;}
+ .screen.is-active{display:flex;}
+ .screen[data-screen="4"]{background:var(--a);}
+ .topbar{display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:44px;flex-shrink:0;}
+ .step{margin:0;font-size:13px;font-weight:600;color:var(--muted);}
+ .screen[data-screen="4"] .step{color:var(--ink);}
+ h1{margin:0;font-weight:700;text-align:center;color:#fff;line-height:1.15;}
+ h1:focus{outline:none;}
+ .h-30{font-size:30px;} .h-28{font-size:28px;} .h-26{font-size:26px;}
+ button{font:inherit;cursor:pointer;border:0;-webkit-tap-highlight-color:transparent;}
+ button:focus-visible{outline:3px solid var(--focus);outline-offset:3px;}
+ .screen[data-screen="4"] button:focus-visible{outline-color:#fff;}
+ .home{
+ min-width:44px;min-height:44px;padding:0 14px;border-radius:14px;
+ background:var(--panel);color:#fff;font-size:15px;font-weight:700;
+ }
+ .screen[data-screen="4"] .home{background:var(--ink);}
+ .camera{
+ background:var(--deep);border-radius:24px;display:flex;align-items:center;justify-content:center;
+ flex-direction:column;gap:8px;color:#fff;font-size:18px;font-weight:600;position:relative;overflow:hidden;
+ flex:1 1 auto;min-height:140px;
+ }
+ .camera.short{max-height:260px;}
+ .reticle{width:22px;height:22px;position:relative;}
+ .reticle::before,.reticle::after{content:"";position:absolute;background:#fff;}
+ .reticle::before{left:50%;top:0;width:1.5px;height:100%;transform:translateX(-50%);}
+ .reticle::after{top:50%;left:0;height:1.5px;width:100%;transform:translateY(-50%);}
+ .reticle span{position:absolute;inset:4px;border:1.5px solid #fff;border-radius:50%;}
+ .flash{position:absolute;inset:0;background:#fff;opacity:0;pointer-events:none;}
+ .flash.go{animation:flash .35s ease-out;}
+ @keyframes flash{0%{opacity:.8}100%{opacity:0}}
+ .pad{display:grid;grid-template-columns:1fr 1fr;gap:12px;flex-shrink:0;}
+ .pad button{
+ aspect-ratio:1/1;width:100%;max-width:153px;justify-self:center;border-radius:50%;
+ display:flex;flex-direction:column;align-items:center;justify-content:center;
+ font-size:26px;font-weight:700;line-height:1.15;
+ box-shadow:0 6px 0 rgba(0,0,0,.28);transition:transform .08s ease,box-shadow .08s ease;
+ }
+ .pad button:active{transform:translateY(4px);box-shadow:0 2px 0 rgba(0,0,0,.28);}
+ .pad .ta{background:var(--a);color:#fff;}
+ .pad .tb{background:var(--b);color:var(--ink);}
+ .pad .tx{background:var(--x);color:var(--ink);}
+ .pad .ty{background:var(--y);color:var(--ink);font-size:24px;}
+ .spacer{flex:1 1 0;min-height:0;}
+ .shutter-row{display:flex;justify-content:center;flex-shrink:0;}
+ .shutter{
+ width:96px;height:96px;border-radius:50%;background:var(--a);
+ box-shadow:0 0 0 6px #fff inset, 0 0 0 0 transparent;border:0;
+ }
+ .shutter:active{transform:scale(.96);}
+ .choices{display:flex;flex-direction:column;gap:16px;flex-shrink:0;}
+ .choice{
+ min-height:72px;border-radius:20px;background:var(--panel);color:#fff;font-size:22px;font-weight:700;
+ display:flex;align-items:center;justify-content:center;gap:12px;padding:0 16px;
+ border:3px solid transparent;
+ }
+ .choice .cursor{width:0;height:0;border-top:10px solid transparent;border-bottom:10px solid transparent;border-left:16px solid #fff;visibility:hidden;}
+ .choice[aria-pressed="true"]{border-color:var(--b);}
+ .choice[aria-pressed="true"] .cursor{visibility:visible;}
+ .primary{
+ min-height:80px;border-radius:20px;font-size:24px;font-weight:700;width:100%;flex-shrink:0;
+ }
+ .primary.pink{background:var(--a);color:#fff;}
+ .primary.dark{background:var(--ink);color:#fff;}
+ .clear{font-size:56px;font-weight:700;line-height:1.1;}
+ .stars{margin:0;font-size:40px;text-align:center;letter-spacing:6px;}
+ .stars span{display:inline-block;}
+ .is-active .stars span{animation:pop .5s cubic-bezier(.2,1.6,.4,1) both;}
+ .is-active .stars span:nth-child(2){animation-delay:.12s;}
+ .is-active .stars span:nth-child(3){animation-delay:.24s;}
+ @keyframes pop{0%{transform:scale(0)}100%{transform:scale(1)}}
+ .sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;color:var(--ink);}
+ @media (prefers-reduced-motion:reduce){
+ *,*::before,*::after{animation:none !important;transition:none !important;}
+ }
+</style>
+</head>
+<body>
+<main class="stage" id="app">
+
+ <!-- 1 / 4 · Press start -->
+ <section class="screen is-active" data-screen="1" aria-labelledby="s1-h">
+ <div class="topbar"><p class="step">1 / 4 · Press start</p></div>
+ <h1 id="s1-h" class="h-30" tabindex="-1">PRESS A JOB</h1>
+ <div class="camera short" aria-hidden="true">🕹️ ready, player 1</div>
+ <div class="pad" role="group" aria-label="Pick a job">
+ <button class="ta" data-go="2" aria-label="A: New item">A<br>NEW</button>
+ <button class="tb" data-go="2" aria-label="B: Fix an item">B<br>FIX</button>
+ <button class="tx" data-go="2" aria-label="X: Find an item">X<br>FIND</button>
+ <button class="ty" data-go="2" aria-label="Y: Sample arrived">Y<br>SAMPLE</button>
+ </div>
+ </section>
+
+ <!-- 2 / 4 · Aim -->
+ <section class="screen" data-screen="2" aria-labelledby="s2-h">
+ <div class="topbar"><p class="step">2 / 4 · Aim</p><button class="home" data-go="1" aria-label="Home">⌂ Home</button></div>
+ <h1 id="s2-h" class="h-28" tabindex="-1">AIM + PRESS A</h1>
+ <div class="camera" aria-label="Camera viewfinder (pretend)" role="img">
+ <div class="reticle"><span></span></div>
+ <div class="flash" id="flash"></div>
+ </div>
+ <div class="shutter-row"><button class="shutter" id="shoot" aria-label="Press A to take photo"></button></div>
+ </section>
+
+ <!-- 3 / 4 · Choose -->
+ <section class="screen" data-screen="3" aria-labelledby="s3-h">
+ <div class="topbar"><p class="step">3 / 4 · Choose</p><button class="home" data-go="1" aria-label="Home">⌂ Home</button></div>
+ <h1 id="s3-h" class="h-26" tabindex="-1">PICK YOUR BRAND</h1>
+ <div class="choices" role="group" aria-label="Brand">
+ <button class="choice" aria-pressed="true"><span class="cursor" aria-hidden="true"></span>Thibaut</button>
+ <button class="choice" aria-pressed="false"><span class="cursor" aria-hidden="true"></span>York</button>
+ <button class="choice" aria-pressed="false"><span class="cursor" aria-hidden="true"></span>Kravet</button>
+ </div>
+ <div class="spacer"></div>
+ <button class="primary pink" data-go="4">A = SELECT</button>
+ </section>
+
+ <!-- 4 / 4 · Level clear -->
+ <section class="screen" data-screen="4" aria-labelledby="s4-h">
+ <div class="topbar"><p class="step">4 / 4 · Level clear</p><button class="home" data-go="1" aria-label="Home">⌂ Home</button></div>
+ <div class="spacer"></div>
+ <h1 id="s4-h" class="clear" tabindex="-1">LEVEL<br>CLEAR!</h1>
+ <p class="stars" aria-label="Three stars"><span>⭐</span> <span>⭐</span> <span>⭐</span></p>
+ <p class="sr" id="saved-note" aria-live="polite"></p>
+ <div class="spacer"></div>
+ <button class="primary dark" data-go="1">PLAY AGAIN</button>
+ </section>
+
+</main>
+
+<script>
+ (function(){
+ const screens = Array.from(document.querySelectorAll('.screen'));
+ function go(n){
+ screens.forEach(s => s.classList.toggle('is-active', s.dataset.screen === String(n)));
+ const active = document.querySelector('.screen.is-active');
+ const h = active && active.querySelector('h1');
+ if (h) h.focus({preventScroll:true});
+ if (String(n) === '4') document.getElementById('saved-note').textContent = 'Saved as a draft.';
+ }
+ document.addEventListener('click', e => {
+ const b = e.target.closest('[data-go]');
+ if (b) go(b.dataset.go);
+ });
+ // brand choice (single select)
+ document.querySelectorAll('.choice').forEach(c => c.addEventListener('click', () => {
+ document.querySelectorAll('.choice').forEach(o => o.setAttribute('aria-pressed', o === c ? 'true' : 'false'));
+ }));
+ // shutter: flash then advance
+ document.getElementById('shoot').addEventListener('click', () => {
+ const f = document.getElementById('flash');
+ f.classList.remove('go'); void f.offsetWidth; f.classList.add('go');
+ go(3);
+ });
+ })();
+</script>
+<script>window.__theme = {id:'v17', name:'Game Pad', screens:4};</script>
+</body>
+</html>
diff --git a/public/themes/v18-trail-map.html b/public/themes/v18-trail-map.html
new file mode 100644
index 0000000..17845e6
--- /dev/null
+++ b/public/themes/v18-trail-map.html
@@ -0,0 +1,163 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>v18 — Trail Map</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --bg:#f7f1e3;
+ --ink:#2f3b2f;
+ --muted:#6b6553;
+ --line:#d9d2c3;
+ --forest:#4f7c4a;
+ --amber:#a0621a;
+ --lake:#4a7a9c;
+ --plum:#8a5a8c;
+ --focus:#2f3b2f;
+ }
+ *{box-sizing:border-box;}
+ html,body{margin:0;padding:0;background:#0a0a0a;font-family:'Inter',system-ui,sans-serif;}
+ .stage{
+ width:390px;height:780px;margin:24px auto;position:relative;
+ background:var(--bg);border-radius:36px;overflow:hidden;
+ box-shadow:0 20px 60px rgba(0,0,0,.5);border:8px solid #000;color:var(--ink);
+ display:flex;flex-direction:column;
+ }
+ @media (max-width:405.98px){
+ .stage{width:100%;height:100dvh;min-height:640px;margin:0;border:0;border-radius:0;}
+ }
+ .screen{display:none;flex:1;min-height:0;flex-direction:column;gap:16px;padding:18px 24px 28px;overflow-y:auto;overflow-x:hidden;}
+ .screen.is-active{display:flex;}
+ .topbar{display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:44px;flex-shrink:0;}
+ .step{margin:0;font-size:13px;font-weight:600;color:var(--muted);}
+ h1{margin:0;font-weight:700;text-align:center;line-height:1.15;color:var(--ink);}
+ h1:focus{outline:none;}
+ .h-34{font-size:34px;} .h-32{font-size:32px;}
+ .h-trail{font-size:20px;text-align:left;}
+ button{font:inherit;cursor:pointer;border:0;-webkit-tap-highlight-color:transparent;}
+ button:focus-visible{outline:3px solid var(--focus);outline-offset:3px;}
+ .home{
+ min-width:44px;min-height:44px;padding:0 14px;border-radius:14px;
+ background:#fff;color:var(--ink);font-size:15px;font-weight:700;border:2px solid var(--line);
+ }
+ .trails{display:flex;flex-direction:column;gap:16px;}
+ .trail-btn{
+ min-height:84px;border-radius:20px;color:#fff;font-size:22px;font-weight:700;
+ display:flex;align-items:center;justify-content:center;gap:10px;padding:0 16px;width:100%;
+ }
+ .t-new{background:var(--forest);} .t-fix{background:var(--amber);} .t-find{background:var(--lake);} .t-sample{background:var(--plum);}
+
+ /* trail stepper */
+ .trail{list-style:none;margin:0;padding:0;flex-shrink:0;}
+ .trail li{display:flex;align-items:center;gap:16px;position:relative;font-size:20px;color:var(--muted);}
+ .trail li + li{margin-top:36px;}
+ .trail li + li::before{content:"";position:absolute;left:20px;top:-36px;width:4px;height:36px;background:var(--line);}
+ .trail li.done + li::before{background:var(--forest);}
+ .dot{width:44px;height:44px;border-radius:50%;background:var(--line);flex-shrink:0;}
+ .trail li.done{color:var(--ink);}
+ .trail li.done .dot{background:var(--forest);}
+ .trail li.now{color:var(--ink);font-weight:700;}
+ .trail li.now .dot{background:#fff;box-shadow:inset 0 0 0 5px var(--forest);}
+ .compact .trail li + li{margin-top:20px;}
+ .compact .trail li + li::before{top:-20px;height:20px;}
+
+ .camera{
+ background:#1c1c1e;border-radius:24px;display:flex;align-items:center;justify-content:center;
+ color:#fff;font-size:18px;font-weight:600;flex:1 1 auto;min-height:150px;max-height:240px;position:relative;overflow:hidden;
+ }
+ .camera::before{content:"";position:absolute;inset:18px;border:2px dashed rgba(255,255,255,.28);border-radius:14px;}
+ .spacer{flex:1 1 0;min-height:0;}
+ .shutter-row{display:flex;justify-content:center;flex-shrink:0;}
+ .shutter{width:96px;height:96px;border-radius:50%;background:var(--forest);box-shadow:inset 0 0 0 6px #fff;}
+ .shutter:active{transform:scale(.96);}
+ .brand{
+ min-height:72px;border-radius:20px;background:#fff;color:var(--ink);font-size:22px;font-weight:700;width:100%;flex-shrink:0;
+ }
+ .primary{min-height:72px;border-radius:20px;background:var(--forest);color:#fff;font-size:22px;font-weight:700;width:100%;flex-shrink:0;}
+ .made{font-size:32px;}
+ @media (prefers-reduced-motion:reduce){
+ *,*::before,*::after{animation:none !important;transition:none !important;}
+ }
+</style>
+</head>
+<body>
+<main class="stage" id="app">
+
+ <!-- 1 / 4 · Pick a trail -->
+ <section class="screen is-active" data-screen="1" aria-labelledby="s1-h">
+ <div class="topbar"><p class="step">1 / 4 · Pick a trail</p></div>
+ <h1 id="s1-h" class="h-34" tabindex="-1">Pick a trail</h1>
+ <div class="trails">
+ <button class="trail-btn t-new" data-go="2"><span aria-hidden="true">🌲</span> New item trail</button>
+ <button class="trail-btn t-fix" data-go="2"><span aria-hidden="true">🔧</span> Fix-it trail</button>
+ <button class="trail-btn t-find" data-go="2"><span aria-hidden="true">🔍</span> Find trail</button>
+ <button class="trail-btn t-sample" data-go="2"><span aria-hidden="true">📦</span> Sample trail</button>
+ </div>
+ </section>
+
+ <!-- 2 / 4 · Stop 1 -->
+ <section class="screen compact" data-screen="2" aria-labelledby="s2-h">
+ <div class="topbar"><p class="step">2 / 4 · Stop 1</p><button class="home" data-go="1" aria-label="Home">⌂ Home</button></div>
+ <ol class="trail" aria-label="Trail progress">
+ <li class="now" aria-current="step"><span class="dot" aria-hidden="true"></span><h1 id="s2-h" class="h-trail" tabindex="-1">Snap the photo</h1></li>
+ <li><span class="dot" aria-hidden="true"></span>Pick the brand</li>
+ <li><span class="dot" aria-hidden="true"></span>Save it</li>
+ <li><span class="dot" aria-hidden="true"></span>Done! 🏁</li>
+ </ol>
+ <div class="camera" role="img" aria-label="Camera viewfinder (pretend)">📷 point at the sample</div>
+ <div class="spacer"></div>
+ <div class="shutter-row"><button class="shutter" data-go="3" aria-label="Take photo"></button></div>
+ </section>
+
+ <!-- 3 / 4 · Stop 2 -->
+ <section class="screen" data-screen="3" aria-labelledby="s3-h">
+ <div class="topbar"><p class="step">3 / 4 · Stop 2</p><button class="home" data-go="1" aria-label="Home">⌂ Home</button></div>
+ <ol class="trail" aria-label="Trail progress">
+ <li class="done"><span class="dot" aria-hidden="true"></span>Snap the photo</li>
+ <li class="now" aria-current="step"><span class="dot" aria-hidden="true"></span><h1 id="s3-h" class="h-trail" tabindex="-1">Pick the brand</h1></li>
+ <li><span class="dot" aria-hidden="true"></span>Save it</li>
+ <li><span class="dot" aria-hidden="true"></span>Done! 🏁</li>
+ </ol>
+ <div class="spacer"></div>
+ <button class="brand" data-go="4">Thibaut</button>
+ <button class="brand" data-go="4">York</button>
+ <button class="brand" data-go="4">Kravet</button>
+ </section>
+
+ <!-- 4 / 4 · Finish -->
+ <section class="screen" data-screen="4" aria-labelledby="s4-h">
+ <div class="topbar"><p class="step">4 / 4 · Finish</p><button class="home" data-go="1" aria-label="Home">⌂ Home</button></div>
+ <ol class="trail" aria-label="Trail progress">
+ <li class="done"><span class="dot" aria-hidden="true"></span>Snap the photo</li>
+ <li class="done"><span class="dot" aria-hidden="true"></span>Pick the brand</li>
+ <li class="done"><span class="dot" aria-hidden="true"></span>Save it</li>
+ <li class="done" aria-current="step"><span class="dot" aria-hidden="true"></span>Done! 🏁</li>
+ </ol>
+ <div class="spacer"></div>
+ <h1 id="s4-h" class="made" tabindex="-1">You made it 🏁</h1>
+ <button class="primary" data-go="1">New trail</button>
+ </section>
+
+</main>
+
+<script>
+ (function(){
+ const screens = Array.from(document.querySelectorAll('.screen'));
+ function go(n){
+ screens.forEach(s => s.classList.toggle('is-active', s.dataset.screen === String(n)));
+ const h = document.querySelector('.screen.is-active h1');
+ if (h) h.focus({preventScroll:true});
+ }
+ document.addEventListener('click', e => {
+ const b = e.target.closest('[data-go]');
+ if (b) go(b.dataset.go);
+ });
+ })();
+</script>
+<script>window.__theme = {id:'v18', name:'Trail Map', screens:4};</script>
+</body>
+</html>
diff --git a/public/themes/v19-left-or-right.html b/public/themes/v19-left-or-right.html
new file mode 100644
index 0000000..d5e4b64
--- /dev/null
+++ b/public/themes/v19-left-or-right.html
@@ -0,0 +1,160 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>v19 — Left or Right</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --bg:#ffffff;
+ --ink:#1a1a1a;
+ --muted:#6b6b6b;
+ --left:#ededed;
+ --keep:#2e9e5b;
+ --photo:#b9a27a;
+ --focus:#2e6bd6;
+ }
+ *{box-sizing:border-box;}
+ html,body{margin:0;padding:0;background:#0a0a0a;font-family:'Inter',system-ui,sans-serif;}
+ .stage{
+ width:390px;height:780px;margin:24px auto;position:relative;
+ background:var(--bg);border-radius:36px;overflow:hidden;
+ box-shadow:0 20px 60px rgba(0,0,0,.5);border:8px solid #000;color:var(--ink);
+ display:flex;flex-direction:column;
+ }
+ @media (max-width:405.98px){
+ .stage{width:100%;height:100dvh;min-height:640px;margin:0;border:0;border-radius:0;}
+ }
+ .screen{display:none;flex:1;min-height:0;flex-direction:column;gap:16px;padding:18px 24px 28px;overflow-y:auto;overflow-x:hidden;}
+ .screen.is-active{display:flex;}
+ .topbar{display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:44px;flex-shrink:0;}
+ .step{margin:0;font-size:13px;font-weight:600;color:var(--muted);}
+ h1{margin:0;font-weight:700;text-align:center;line-height:1.2;color:var(--ink);}
+ h1:focus{outline:none;}
+ .h-30{font-size:30px;} .h-32{font-size:32px;}
+ button{font:inherit;cursor:pointer;border:0;-webkit-tap-highlight-color:transparent;}
+ button:focus-visible{outline:3px solid var(--focus);outline-offset:3px;}
+ .home{
+ min-width:44px;min-height:44px;padding:0 14px;border-radius:14px;
+ background:var(--left);color:var(--ink);font-size:15px;font-weight:700;
+ }
+ .spacer{flex:1 1 0;min-height:0;}
+ .split{display:grid;grid-template-columns:1fr 1fr;gap:12px;flex-shrink:0;}
+ .side{
+ height:360px;max-height:52dvh;min-height:220px;border-radius:20px;font-weight:700;line-height:1.2;
+ display:flex;flex-direction:column;align-items:center;justify-content:center;gap:28px;text-align:center;
+ transition:transform .12s ease;
+ }
+ .side:active{transform:scale(.98);}
+ .side .arrow{font-size:30px;}
+ .side .word{font-size:34px;}
+ .side .word.sm{font-size:26px;}
+ .side.l{background:var(--left);color:var(--ink);}
+ .side.r{background:var(--ink);color:#fff;}
+ .side.r:focus-visible{outline-color:var(--focus);}
+ .hint{margin:0;text-align:center;font-size:15px;color:var(--muted);min-height:20px;}
+ .camera{
+ background:#1c1c1e;border-radius:24px;display:flex;align-items:center;justify-content:center;
+ color:#fff;font-size:18px;font-weight:600;flex:1 1 auto;min-height:220px;max-height:470px;position:relative;overflow:hidden;
+ }
+ .camera::before,.camera::after{content:"";position:absolute;width:40px;height:40px;border:3px solid rgba(255,255,255,.55);}
+ .camera::before{top:20px;left:20px;border-right:0;border-bottom:0;border-radius:10px 0 0 0;}
+ .camera::after{bottom:20px;right:20px;border-left:0;border-top:0;border-radius:0 0 10px 0;}
+ .shutter-row{display:flex;justify-content:center;flex-shrink:0;}
+ .shutter{width:96px;height:96px;border-radius:50%;background:var(--ink);box-shadow:0 0 0 4px #fff inset;}
+ .shutter:active{transform:scale(.96);}
+ .photo{
+ background:var(--photo);border-radius:24px;height:380px;max-height:46dvh;min-height:200px;flex-shrink:0;
+ display:flex;align-items:center;justify-content:center;color:#2b2415;font-size:18px;font-weight:600;
+ background-image:repeating-linear-gradient(135deg,rgba(255,255,255,.08) 0 14px,rgba(0,0,0,.04) 14px 28px);
+ }
+ .choice{
+ min-height:120px;border-radius:20px;font-size:24px;font-weight:700;
+ display:flex;align-items:center;justify-content:center;gap:8px;
+ }
+ .choice.l{background:var(--left);color:var(--ink);}
+ .choice.r{background:var(--keep);color:#fff;}
+ .sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;}
+ @media (prefers-reduced-motion:reduce){
+ *,*::before,*::after{animation:none !important;transition:none !important;}
+ }
+</style>
+</head>
+<body>
+<main class="stage" id="app">
+
+ <!-- 1 / 4 · Start -->
+ <section class="screen is-active" data-screen="1" aria-labelledby="s1-h">
+ <div class="topbar"><p class="step">1 / 4 · Start</p></div>
+ <h1 id="s1-h" class="h-30" tabindex="-1">Got a sample in your hand?</h1>
+ <div class="spacer"></div>
+ <div class="split">
+ <button class="side l" id="no-btn" aria-label="No"><span class="arrow" aria-hidden="true">⬅️</span><span class="word">NO</span></button>
+ <button class="side r" data-go="2" aria-label="Yes"><span class="arrow" aria-hidden="true">➡️</span><span class="word">YES</span></button>
+ </div>
+ <p class="hint" id="no-hint" aria-live="polite"></p>
+ <div class="spacer"></div>
+ </section>
+
+ <!-- 2 / 4 · Is it new? -->
+ <section class="screen" data-screen="2" aria-labelledby="s2-h">
+ <div class="topbar"><p class="step">2 / 4 · Is it new?</p><button class="home" data-go="1" aria-label="Home">⌂ Home</button></div>
+ <h1 id="s2-h" class="h-32" tabindex="-1">Is it new to us?</h1>
+ <div class="spacer"></div>
+ <div class="split">
+ <button class="side l" data-go="3" aria-label="Already have it"><span class="arrow" aria-hidden="true">⬅️</span><span class="word sm">Already<br>have it</span></button>
+ <button class="side r" data-go="3" aria-label="Brand new"><span class="arrow" aria-hidden="true">➡️</span><span class="word sm">Brand<br>new</span></button>
+ </div>
+ <div class="spacer"></div>
+ </section>
+
+ <!-- 3 / 4 · Snap -->
+ <section class="screen" data-screen="3" aria-labelledby="s3-h">
+ <div class="topbar"><p class="step">3 / 4 · Snap</p><button class="home" data-go="1" aria-label="Home">⌂ Home</button></div>
+ <h1 id="s3-h" class="h-32" tabindex="-1">Snap it</h1>
+ <div class="camera" role="img" aria-label="Camera viewfinder (pretend)">📷 point at the sample</div>
+ <div class="shutter-row"><button class="shutter" data-go="4" aria-label="Take photo"></button></div>
+ </section>
+
+ <!-- 4 / 4 · Keep? -->
+ <section class="screen" data-screen="4" aria-labelledby="s4-h">
+ <div class="topbar"><p class="step">4 / 4 · Keep?</p><button class="home" data-go="1" aria-label="Home">⌂ Home</button></div>
+ <div class="photo" role="img" aria-label="Your photo (pretend)">your photo</div>
+ <h1 id="s4-h" class="h-30" tabindex="-1">Keep this one?</h1>
+ <div class="spacer"></div>
+ <div class="split">
+ <button class="choice l" data-go="3" aria-label="Redo"><span aria-hidden="true">⬅️</span> Redo</button>
+ <button class="choice r" id="keep-btn" aria-label="Keep"><span>Keep</span> <span aria-hidden="true">➡️</span></button>
+ </div>
+ </section>
+
+</main>
+
+<script>
+ (function(){
+ const screens = Array.from(document.querySelectorAll('.screen'));
+ function go(n){
+ screens.forEach(s => s.classList.toggle('is-active', s.dataset.screen === String(n)));
+ const h = document.querySelector('.screen.is-active h1');
+ if (h) h.focus({preventScroll:true});
+ if (String(n) === '1') document.getElementById('no-hint').textContent = '';
+ }
+ document.addEventListener('click', e => {
+ const b = e.target.closest('[data-go]');
+ if (b) go(b.dataset.go);
+ });
+ document.getElementById('no-btn').addEventListener('click', () => {
+ document.getElementById('no-hint').textContent = 'No worries — grab one and tap YES.';
+ });
+ document.getElementById('keep-btn').addEventListener('click', () => {
+ go(1);
+ document.getElementById('no-hint').textContent = 'Kept! Ready for the next one.';
+ });
+ })();
+</script>
+<script>window.__theme = {id:'v19', name:'Left or Right', screens:4};</script>
+</body>
+</html>
diff --git a/public/themes/v2-four-tiles.html b/public/themes/v2-four-tiles.html
new file mode 100644
index 0000000..9c5cf0b
--- /dev/null
+++ b/public/themes/v2-four-tiles.html
@@ -0,0 +1,348 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>v2 — Four Big Tiles</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link href="https://fonts.googleapis.com/css2?family=Fraunces:wght@500;600;700&family=DM+Sans:wght@400;500;700&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --bg:#F7F3EC;
+ --add:#8FAE8B;
+ --update:#7C93B0;
+ --find:#C97B5B;
+ --sample:#D9A441;
+ --text:#2B2B2B;
+ }
+ *{box-sizing:border-box;}
+ html,body{margin:0;padding:0;background:#0a0a0a;font-family:'DM Sans',sans-serif;}
+ .stage{
+ width:390px;height:780px;margin:24px auto;position:relative;
+ background:var(--bg);border-radius:36px;overflow:hidden;
+ box-shadow:0 20px 60px rgba(0,0,0,.5);border:8px solid #000;color:var(--text);
+ }
+ .screen{position:absolute;inset:0;display:none;flex-direction:column;background:var(--bg);}
+ .screen.active{display:flex;}
+ h1,h2,h3{font-family:'Fraunces',serif;margin:0;color:var(--text);}
+ .pad{padding:28px 22px;display:flex;flex-direction:column;gap:16px;flex:1;overflow-y:auto;}
+ .home-title{text-align:center;font-size:24px;margin:32px 0 8px;}
+ .tile-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;padding:16px 20px;flex:1;}
+ .tile{
+ border-radius:20px;min-height:140px;display:flex;flex-direction:column;align-items:center;
+ justify-content:center;gap:10px;color:#1B1B1B;cursor:pointer;font-size:18px;font-weight:700;
+ text-align:center;padding:12px;border:none;
+ }
+ .tile:focus-visible, button:focus-visible, .thumb-card:focus-visible, .brand-tile:focus-visible{outline:3px solid #2B2B2B;outline-offset:3px;}
+ .btn:disabled{opacity:.4;cursor:not-allowed;}
+ .home-fab{
+ position:absolute;top:14px;right:14px;z-index:6;min-width:56px;min-height:56px;border-radius:16px;
+ background:#fff;border:2px solid #ddd4c2;font-size:22px;cursor:pointer;color:var(--text);
+ }
+ .camera-screen .home-fab{background:rgba(255,255,255,.9);}
+ .tile .icon{font-size:44px;}
+ .tile.add{background:var(--add);}
+ .tile.update{background:var(--update);}
+ .tile.find{background:var(--find);}
+ .tile.sample{background:var(--sample);}
+
+ .camera-screen{background:#1c1c1e;color:#fff;position:relative;}
+ .camera-screen .hint{position:absolute;top:56px;left:50%;transform:translateX(-50%);width:max-content;max-width:calc(100% - 32px);text-align:center;font-size:17px;z-index:2;color:#fff;background:rgba(10,10,12,.82);padding:6px 14px;border-radius:999px;}
+ .viewfinder{position:absolute;inset:0;background:repeating-linear-gradient(135deg,#2a2a2c 0 40px,#1c1c1e 40px 80px);}
+ .shutter-wrap{position:absolute;bottom:48px;left:0;right:0;display:flex;justify-content:center;z-index:2;}
+ .shutter{
+ width:88px;height:88px;border-radius:50%;background:#fff;border:6px solid var(--sample);
+ font-size:36px;cursor:pointer;display:flex;align-items:center;justify-content:center;
+ }
+
+ .btn{min-height:56px;border-radius:16px;border:none;font-family:'DM Sans',sans-serif;
+ font-size:18px;font-weight:700;cursor:pointer;padding:0 20px;width:100%;}
+ .btn-primary{background:var(--add);color:#1B1B1B;}
+ .btn-update{background:var(--update);color:#1B1B1B;}
+ .btn-find{background:var(--find);color:#1B1B1B;}
+ .btn-sample{background:var(--sample);color:#1B1B1B;}
+ .btn-secondary{background:#fff;color:var(--text);border:2px solid #ddd4c2;}
+
+ .search-bar{
+ background:#fff;border-radius:14px;padding:14px 16px;font-size:16px;color:#6B6258;
+ border:2px solid #e6dfd2;
+ }
+ .brand-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;overflow-y:auto;}
+ .brand-tile{
+ background:#fff;border:2px solid #e6dfd2;border-radius:14px;min-height:72px;
+ display:flex;align-items:center;justify-content:center;font-size:17px;font-weight:600;
+ cursor:pointer;text-align:center;padding:8px;
+ }
+ .brand-tile:hover{border-color:var(--add);}
+ .brand-tile.selected{border-color:var(--add);background:#eef5ec;box-shadow:inset 0 0 0 2px var(--add);}
+
+ .card{background:#fff;border-radius:18px;padding:20px;box-shadow:0 4px 16px rgba(0,0,0,.08);}
+ .swatch{
+ width:100%;height:180px;border-radius:14px;margin-bottom:12px;
+ background:repeating-linear-gradient(45deg,#8FAE8B 0 22px,#B9CBB2 22px 44px),linear-gradient(135deg,#8FAE8B,#6f8d6c);
+ background-blend-mode:multiply;
+ }
+ .swatch.blue{background:repeating-linear-gradient(45deg,#7C93B0 0 22px,#B3C3D6 22px 44px),linear-gradient(135deg,#7C93B0,#5f7692);background-blend-mode:multiply;}
+ .swatch.small{height:120px;}
+ .item-title{font-size:20px;font-weight:700;}
+ .item-sub{font-size:14px;color:#6B6258;}
+
+ .center-flex{align-items:center;justify-content:center;text-align:center;}
+ .big-emoji{font-size:60px;}
+
+ .thumb-list{display:flex;flex-direction:column;gap:12px;overflow-y:auto;}
+ .thumb-card{
+ display:flex;gap:12px;background:#fff;border-radius:14px;padding:10px;border:2px solid #e6dfd2;
+ cursor:pointer;align-items:center;
+ }
+ .thumb-sw{width:64px;height:64px;border-radius:10px;flex-shrink:0;
+ background:repeating-linear-gradient(45deg,#C97B5B 0 14px,#e0a184 14px 28px);}
+ .thumb-text .item-title{font-size:16px;}
+</style>
+<style id="ux-baseline">/* TK-12343 UX baseline: full-bleed on real phones, visible focus everywhere */
+@media (max-width:405px){ html,body{margin:0;padding:0} body{overflow-x:hidden} .stage,.phone{width:100% !important;max-width:100%;margin:0 !important;border:0 !important;border-radius:0 !important;box-shadow:none !important;height:100dvh;min-height:640px} }
+button:focus-visible,a:focus-visible,[tabindex]:focus-visible,input:focus-visible{outline:3px solid #9C7A33 !important;outline-offset:2px}
+</style>
+</head>
+<body>
+<div class="stage">
+
+ <!-- HOME -->
+ <div class="screen active" id="home">
+ <div class="home-title">What are we doing?</div>
+ <div class="tile-grid">
+ <button class="tile add" onclick="show('addCamera')"><span class="icon">🆕</span>Add New Item</button>
+ <button class="tile update" onclick="show('updateCamera1')"><span class="icon">🔄</span>Update an Item</button>
+ <button class="tile find" onclick="show('findMethods')"><span class="icon">🔍</span>Find an Item</button>
+ <button class="tile sample" onclick="show('sampleCamera')"><span class="icon">📦</span>Sample Arrived</button>
+ </div>
+ </div>
+
+ <!-- ADD: CAMERA -->
+ <div class="screen camera-screen" id="addCamera">
+ <button class="home-fab" onclick="goHome()" aria-label="Home">⌂</button>
+ <div class="hint">Take a photo of the sample</div>
+ <div class="viewfinder"></div>
+ <div class="shutter-wrap"><button class="shutter" onclick="show('addBrand')" aria-label="Shoot photo">📸</button></div>
+ </div>
+
+ <!-- ADD: BRAND GRID -->
+ <div class="screen" id="addBrand">
+ <button class="home-fab" onclick="goHome()" aria-label="Home">⌂</button>
+ <div class="pad">
+ <div class="search-bar">🔎 Search brands…</div>
+ <h3>Which brand?</h3>
+ <div class="brand-grid">
+ <div class="brand-tile" tabindex="0" role="button" onclick="selectBrand(this,'York')" onkeydown="onBrandKey(event,this,'York')">York</div>
+ <div class="brand-tile" tabindex="0" role="button" onclick="selectBrand(this,'Brewster')" onkeydown="onBrandKey(event,this,'Brewster')">Brewster</div>
+ <div class="brand-tile" tabindex="0" role="button" onclick="selectBrand(this,'Kravet')" onkeydown="onBrandKey(event,this,'Kravet')">Kravet</div>
+ <div class="brand-tile" tabindex="0" role="button" onclick="selectBrand(this,'Cole & Son')" onkeydown="onBrandKey(event,this,'Cole & Son')">Cole & Son</div>
+ <div class="brand-tile" tabindex="0" role="button" onclick="selectBrand(this,'Thibaut')" onkeydown="onBrandKey(event,this,'Thibaut')">Thibaut</div>
+ <div class="brand-tile" tabindex="0" role="button" onclick="selectBrand(this,'Mulberry')" onkeydown="onBrandKey(event,this,'Mulberry')">Mulberry</div>
+ </div>
+ <button class="btn btn-primary" id="saveDraftBtn" disabled onclick="saveDraft()">Save as Draft</button>
+ </div>
+ </div>
+
+ <!-- ADD: SAVED -->
+ <div class="screen center-flex" id="addSaved">
+ <button class="home-fab" onclick="goHome()" aria-label="Home">⌂</button>
+ <div class="pad center-flex">
+ <div class="big-emoji">✅</div>
+ <h2>Saved!</h2>
+ <div class="item-sub">Added as <strong id="savedBrandName"></strong></div>
+ <button class="btn btn-primary" onclick="show('addCamera')">Add Another Photo</button>
+ <button class="btn btn-secondary" onclick="goHome()">Done</button>
+ </div>
+ </div>
+
+ <!-- UPDATE: CAMERA 1 -->
+ <div class="screen camera-screen" id="updateCamera1">
+ <button class="home-fab" onclick="goHome()" aria-label="Home">⌂</button>
+ <div class="hint">Take a photo of the label or pattern</div>
+ <div class="viewfinder"></div>
+ <div class="shutter-wrap"><button class="shutter" onclick="show('updateMatch')" aria-label="Shoot photo">📸</button></div>
+ </div>
+
+ <!-- UPDATE: MATCH CARD -->
+ <div class="screen" id="updateMatch">
+ <button class="home-fab" onclick="goHome()" aria-label="Home">⌂</button>
+ <div class="pad">
+ <div class="card">
+ <div class="swatch blue"></div>
+ <div class="item-title">Palm Damask — Sage</div>
+ <div class="item-sub">code DW-10423</div>
+ </div>
+ <button class="btn btn-update" onclick="show('updateCamera2')">Yes, this one</button>
+ <button class="btn btn-secondary" onclick="show('updateTyping')">Search by typing instead</button>
+ </div>
+ </div>
+
+ <!-- UPDATE: TYPE SEARCH FALLBACK -->
+ <div class="screen" id="updateTyping">
+ <button class="home-fab" onclick="goHome()" aria-label="Home">⌂</button>
+ <div class="pad">
+ <h3>Type the item name or code</h3>
+ <div class="search-bar">Palm Damask…</div>
+ <button class="btn btn-update" onclick="show('updateCamera2')">Found It</button>
+ </div>
+ </div>
+
+ <!-- UPDATE: CAMERA 2 -->
+ <div class="screen camera-screen" id="updateCamera2">
+ <button class="home-fab" onclick="goHome()" aria-label="Home">⌂</button>
+ <div class="hint">Take the new photo</div>
+ <div class="viewfinder"></div>
+ <div class="shutter-wrap"><button class="shutter" onclick="show('updateSaved')" aria-label="Shoot new photo">📸</button></div>
+ </div>
+
+ <!-- UPDATE: SAVED -->
+ <div class="screen center-flex" id="updateSaved">
+ <button class="home-fab" onclick="goHome()" aria-label="Home">⌂</button>
+ <div class="pad center-flex">
+ <div class="big-emoji">✅</div>
+ <h2>Saved!</h2>
+ <button class="btn btn-secondary" onclick="goHome()">Done</button>
+ </div>
+ </div>
+
+ <!-- FIND: METHODS -->
+ <div class="screen" id="findMethods">
+ <button class="home-fab" onclick="goHome()" aria-label="Home">⌂</button>
+ <div class="pad">
+ <h3>How do you want to find it?</h3>
+ <button class="btn btn-find" onclick="show('findResults')">📷 Scan the Label</button>
+ <button class="btn btn-find" onclick="show('findResults')">🎤 Say the Name</button>
+ <button class="btn btn-find" onclick="show('findResults')">⌨️ Type It</button>
+ </div>
+ </div>
+
+ <!-- FIND: RESULTS -->
+ <div class="screen" id="findResults">
+ <button class="home-fab" onclick="goHome()" aria-label="Home">⌂</button>
+ <div class="pad">
+ <h3>Results</h3>
+ <div class="thumb-list">
+ <div class="thumb-card" onclick="show('findDetail')">
+ <div class="thumb-sw"></div>
+ <div class="thumb-text">
+ <div class="item-title">Palm Damask — Sage</div>
+ <div class="item-sub">code DW-10423</div>
+ </div>
+ </div>
+ <div class="thumb-card" onclick="show('findDetail')">
+ <div class="thumb-sw" style="background:repeating-linear-gradient(45deg,#D9A441 0 14px,#eec076 14px 28px);"></div>
+ <div class="thumb-text">
+ <div class="item-title">Meadow Stripe — Gold</div>
+ <div class="item-sub">code DW-20115</div>
+ </div>
+ </div>
+ <div class="thumb-card" onclick="show('findDetail')">
+ <div class="thumb-sw" style="background:repeating-linear-gradient(45deg,#7C93B0 0 14px,#a9bcd3 14px 28px);"></div>
+ <div class="thumb-text">
+ <div class="item-title">Coastal Weave — Blue</div>
+ <div class="item-sub">code DW-30871</div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- FIND: DETAIL -->
+ <div class="screen" id="findDetail">
+ <button class="home-fab" onclick="goHome()" aria-label="Home">⌂</button>
+ <div class="pad">
+ <div class="card">
+ <div class="swatch"></div>
+ <div class="item-title">Palm Damask — Sage</div>
+ <div class="item-sub">code DW-10423</div>
+ </div>
+ <button class="btn btn-update" onclick="show('updateCamera2')">Update This</button>
+ <button class="btn btn-secondary" onclick="show('findMethods')">Back to Search</button>
+ </div>
+ </div>
+
+ <!-- SAMPLE: CAMERA -->
+ <div class="screen camera-screen" id="sampleCamera">
+ <button class="home-fab" onclick="goHome()" aria-label="Home">⌂</button>
+ <div class="hint">Scan the label on the box</div>
+ <div class="viewfinder"></div>
+ <div class="shutter-wrap"><button class="shutter" onclick="show('sampleMatch')" aria-label="Scan box">📸</button></div>
+ </div>
+
+ <!-- SAMPLE: MATCH -->
+ <div class="screen" id="sampleMatch">
+ <button class="home-fab" onclick="goHome()" aria-label="Home">⌂</button>
+ <div class="pad">
+ <div class="card">
+ <div class="swatch small"></div>
+ <div class="item-title">Palm Damask — Sage</div>
+ <div class="item-sub">code DW-10423</div>
+ </div>
+ <button class="btn btn-sample" onclick="show('samplePrinting')">Yes, Mark Received</button>
+ </div>
+ </div>
+
+ <!-- SAMPLE: PRINTING -->
+ <div class="screen center-flex camera-screen" id="samplePrinting">
+ <button class="home-fab" onclick="goHome()" aria-label="Home">⌂</button>
+ <div class="pad center-flex">
+ <div class="big-emoji">🖨️</div>
+ <p style="font-size:18px;">Printing sticker…</p>
+ </div>
+ </div>
+
+ <!-- SAMPLE: DONE -->
+ <div class="screen center-flex" id="sampleDone">
+ <button class="home-fab" onclick="goHome()" aria-label="Home">⌂</button>
+ <div class="pad center-flex">
+ <div class="big-emoji">✅</div>
+ <h2>Done!</h2>
+ <button class="btn btn-secondary" onclick="goHome()">Done</button>
+ </div>
+ </div>
+
+</div>
+
+<script>
+ let selectedBrand = null;
+
+ let printTimer = null;
+ function show(id){
+ if (printTimer && id !== 'sampleDone') { clearTimeout(printTimer); printTimer = null; }
+ document.querySelectorAll('.screen').forEach(s=>s.classList.remove('active'));
+ document.getElementById(id).classList.add('active');
+ if(id === 'addBrand'){
+ selectedBrand = null;
+ document.querySelectorAll('#addBrand .brand-tile').forEach(t=>t.classList.remove('selected'));
+ document.getElementById('saveDraftBtn').disabled = true;
+ }
+ if(id === 'samplePrinting'){
+ printTimer = setTimeout(function(){ printTimer = null; show('sampleDone'); }, 1100);
+ }
+ }
+
+ function selectBrand(el, name){
+ document.querySelectorAll('#addBrand .brand-tile').forEach(t=>t.classList.remove('selected'));
+ el.classList.add('selected');
+ selectedBrand = name;
+ document.getElementById('saveDraftBtn').disabled = false;
+ }
+
+ function onBrandKey(evt, el, name){
+ if(evt.key === 'Enter' || evt.key === ' '){
+ evt.preventDefault();
+ selectBrand(el, name);
+ }
+ }
+
+ function saveDraft(){
+ if(!selectedBrand) return;
+ document.getElementById('savedBrandName').textContent = selectedBrand;
+ show('addSaved');
+ }
+
+ function goHome(){ show('home'); }
+</script>
+</body>
+</html>
diff --git a/public/themes/v20-luxe-gold.html b/public/themes/v20-luxe-gold.html
new file mode 100644
index 0000000..27561e1
--- /dev/null
+++ b/public/themes/v20-luxe-gold.html
@@ -0,0 +1,158 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>v20 — Luxe Gold</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --cream:#fbf8f2;
+ --onyx:#1e1b16;
+ --onyx-2:#2b2720;
+ --gold:#b8914a; /* gold on dark surfaces */
+ --gold-ink:#8a6a2c; /* gold text on cream (darkened for 4.5:1) */
+ --muted-light:#6f6554;
+ --muted-dark:#9a8f7e;
+ --card-sub:#707070;
+ }
+ *{box-sizing:border-box;}
+ html,body{margin:0;padding:0;background:#0a0a0a;font-family:'Inter',system-ui,sans-serif;}
+ .stage{
+ width:390px;height:780px;margin:24px auto;position:relative;
+ background:var(--cream);border-radius:36px;overflow:hidden;
+ box-shadow:0 20px 60px rgba(0,0,0,.5);border:8px solid #000;color:var(--onyx);
+ display:flex;flex-direction:column;
+ }
+ @media (max-width:405.98px){
+ .stage{width:100%;height:100dvh;min-height:640px;margin:0;border:0;border-radius:0;}
+ }
+ .screen{display:none;flex:1;min-height:0;flex-direction:column;gap:16px;padding:18px 24px 28px;overflow-y:auto;overflow-x:hidden;}
+ .screen.is-active{display:flex;}
+ .screen.dark{background:var(--onyx);color:var(--cream);}
+ .topbar{display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:44px;flex-shrink:0;}
+ .step{margin:0;font-size:13px;font-weight:600;color:var(--muted-light);}
+ .dark .step{color:var(--muted-dark);}
+ h1{margin:0;font-weight:400;text-align:center;line-height:1.2;}
+ h1:focus{outline:none;}
+ .h-34{font-size:34px;} .h-32{font-size:32px;} .h-26{font-size:26px;}
+ .wordmark{margin:0;text-align:center;font-size:22px;font-weight:700;color:var(--gold-ink);letter-spacing:.04em;line-height:1.25;}
+ button{font:inherit;cursor:pointer;-webkit-tap-highlight-color:transparent;}
+ button:focus-visible{outline:2px solid var(--onyx);outline-offset:3px;}
+ .dark button:focus-visible{outline-color:var(--gold);}
+ .home{
+ min-width:44px;min-height:44px;padding:0 14px;border-radius:2px;
+ background:transparent;color:var(--onyx);border:1.5px solid var(--onyx);font-size:13px;font-weight:600;letter-spacing:.08em;
+ }
+ .dark .home{color:var(--cream);border-color:var(--muted-dark);}
+ .spacer{flex:1 1 0;min-height:0;}
+ .btn{
+ min-height:72px;width:100%;border-radius:2px;font-size:20px;font-weight:600;letter-spacing:.8px;flex-shrink:0;
+ border:1.5px solid var(--onyx);
+ }
+ .btn.solid{background:var(--onyx);color:var(--cream);}
+ .btn.line{background:var(--cream);color:var(--onyx);}
+ .btn.gold{background:var(--gold);color:var(--onyx);border-color:var(--gold);}
+ .dark .btn.gold:focus-visible{outline-color:var(--cream);}
+ .gap12{height:12px;flex-shrink:0;}
+ .camera{
+ background:var(--onyx-2);border-radius:2px;flex:1 1 auto;min-height:220px;max-height:500px;position:relative;
+ }
+ .camera i{position:absolute;width:28px;height:28px;border-color:var(--gold);border-style:solid;border-width:0;}
+ .camera i:nth-child(1){top:16px;left:16px;border-top-width:1.5px;border-left-width:1.5px;}
+ .camera i:nth-child(2){top:16px;right:16px;border-top-width:1.5px;border-right-width:1.5px;}
+ .camera i:nth-child(3){bottom:16px;left:16px;border-bottom-width:1.5px;border-left-width:1.5px;}
+ .camera i:nth-child(4){bottom:16px;right:16px;border-bottom-width:1.5px;border-right-width:1.5px;}
+ .shutter-row{display:flex;justify-content:center;flex-shrink:0;}
+ .shutter{width:96px;height:96px;border-radius:50%;background:var(--gold);border:0;box-shadow:inset 0 0 0 6px #fff;}
+ .shutter:active{transform:scale(.96);}
+ .cards{display:flex;flex-direction:column;gap:16px;flex-shrink:0;}
+ .card{
+ background:#fff;border:1.5px solid transparent;border-radius:2px;padding:14px;display:flex;gap:14px;align-items:center;
+ text-align:left;width:100%;min-height:112px;color:#222;
+ }
+ .card[aria-pressed="true"]{border-color:var(--gold-ink);box-shadow:0 0 0 1px var(--gold-ink) inset;}
+ .card .sw{width:84px;height:84px;border-radius:2px;flex-shrink:0;}
+ .card .t{display:block;font-size:20px;font-weight:700;color:#222;}
+ .card .s{display:block;font-size:15px;color:var(--card-sub);margin-top:4px;}
+ .star{margin:0;text-align:center;font-size:64px;line-height:1;color:var(--gold);}
+ .is-active .star{animation:glint 1.2s ease-out both;}
+ @keyframes glint{0%{opacity:0;transform:scale(.6) rotate(-20deg)}100%{opacity:1;transform:none}}
+ @media (prefers-reduced-motion:reduce){
+ *,*::before,*::after{animation:none !important;transition:none !important;}
+ }
+</style>
+</head>
+<body>
+<main class="stage" id="app">
+
+ <!-- 1 / 4 · Welcome -->
+ <section class="screen is-active" data-screen="1" aria-labelledby="s1-h">
+ <div class="topbar"><p class="step">1 / 4 · Welcome</p></div>
+ <p class="wordmark">DESIGNER<br>WALLCOVERINGS</p>
+ <div class="spacer"></div>
+ <h1 id="s1-h" class="h-34" tabindex="-1">What shall we do?</h1>
+ <div class="gap12"></div>
+ <button class="btn solid" data-go="2">ADD A NEW ITEM</button>
+ <button class="btn line" data-go="2">UPDATE AN ITEM</button>
+ <button class="btn line" data-go="2">FIND AN ITEM</button>
+ <button class="btn line" data-go="2">A SAMPLE ARRIVED</button>
+ </section>
+
+ <!-- 2 / 4 · Capture -->
+ <section class="screen dark" data-screen="2" aria-labelledby="s2-h">
+ <div class="topbar"><p class="step">2 / 4 · Capture</p><button class="home" data-go="1" aria-label="Home">HOME</button></div>
+ <h1 id="s2-h" class="h-26" tabindex="-1">Capture the sample</h1>
+ <div class="camera" role="img" aria-label="Camera viewfinder (pretend)"><i></i><i></i><i></i><i></i></div>
+ <div class="shutter-row"><button class="shutter" data-go="3" aria-label="Capture photo"></button></div>
+ </section>
+
+ <!-- 3 / 4 · Maison -->
+ <section class="screen" data-screen="3" aria-labelledby="s3-h">
+ <div class="topbar"><p class="step">3 / 4 · Maison</p><button class="home" data-go="1" aria-label="Home">HOME</button></div>
+ <h1 id="s3-h" class="h-32" tabindex="-1">Which maison?</h1>
+ <div class="cards" role="group" aria-label="Maison">
+ <button class="card" aria-pressed="true"><span class="sw" style="background:#8fae8b" aria-hidden="true"></span><span><span class="t">Thibaut</span><span class="s">American heritage</span></span></button>
+ <button class="card" aria-pressed="false"><span class="sw" style="background:#c97b5b" aria-hidden="true"></span><span><span class="t">York</span><span class="s">Since 1895</span></span></button>
+ <button class="card" aria-pressed="false"><span class="sw" style="background:#7c93b0" aria-hidden="true"></span><span><span class="t">Kravet</span><span class="s">Couture</span></span></button>
+ </div>
+ <div class="spacer"></div>
+ <button class="btn solid" data-go="4">SAVE AS DRAFT</button>
+ </section>
+
+ <!-- 4 / 4 · Saved -->
+ <section class="screen dark" data-screen="4" aria-labelledby="s4-h">
+ <div class="topbar"><p class="step">4 / 4 · Saved</p><button class="home" data-go="1" aria-label="Home">HOME</button></div>
+ <div class="spacer"></div>
+ <p class="star" aria-hidden="true">✦</p>
+ <h1 id="s4-h" class="h-32" tabindex="-1">Beautifully saved.</h1>
+ <div class="spacer"></div>
+ <button class="btn gold" data-go="1">ADD ANOTHER</button>
+ </section>
+
+</main>
+
+<script>
+ (function(){
+ const screens = Array.from(document.querySelectorAll('.screen'));
+ function go(n, focus){
+ screens.forEach(s => s.classList.toggle('is-active', s.dataset.screen === String(n)));
+ if (focus !== false){
+ const h = document.querySelector('.screen.is-active h1');
+ if (h) h.focus({preventScroll:true});
+ }
+ }
+ document.addEventListener('click', e => {
+ const b = e.target.closest('[data-go]');
+ if (b) go(b.dataset.go);
+ });
+ document.querySelectorAll('.card').forEach(c => c.addEventListener('click', () => {
+ document.querySelectorAll('.card').forEach(o => o.setAttribute('aria-pressed', o === c ? 'true' : 'false'));
+ }));
+ })();
+</script>
+<script>window.__theme = {id:'v20', name:'Luxe Gold', screens:4};</script>
+</body>
+</html>
diff --git a/public/themes/v21-beat-the-clock.html b/public/themes/v21-beat-the-clock.html
new file mode 100644
index 0000000..3d3f54d
--- /dev/null
+++ b/public/themes/v21-beat-the-clock.html
@@ -0,0 +1,210 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>v21 — Beat the Clock</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --bg:#141414;
+ --cam:#1c1c1e;
+ --track:#3a3a3a;
+ --orange:#ff6a00; /* decorative fills (bar, shutter) */
+ --orange-deep:#f06000; /* surfaces carrying white text (3:1 at >=24px) */
+ --amber:#ffc23f;
+ --green:#2e9e5b;
+ --muted:#9a9a9a;
+ --btn-dark:#333333;
+ --focus:#ffc23f;
+ }
+ *{box-sizing:border-box;}
+ html,body{margin:0;padding:0;background:#0a0a0a;font-family:'Inter',system-ui,sans-serif;}
+ .stage{
+ width:390px;height:780px;margin:24px auto;position:relative;
+ background:var(--bg);border-radius:36px;overflow:hidden;
+ box-shadow:0 20px 60px rgba(0,0,0,.5);border:8px solid #000;color:#fff;
+ display:flex;flex-direction:column;
+ }
+ @media (max-width:405.98px){
+ .stage{width:100%;height:100dvh;min-height:640px;margin:0;border:0;border-radius:0;}
+ }
+ .screen{display:none;flex:1;min-height:0;flex-direction:column;gap:16px;padding:18px 24px 28px;overflow-y:auto;overflow-x:hidden;}
+ .screen.is-active{display:flex;}
+ .screen[data-screen="4"]{background:var(--orange-deep);}
+ .topbar{display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:44px;flex-shrink:0;}
+ .topbar .right{display:flex;gap:8px;}
+ .step{margin:0;font-size:13px;font-weight:600;color:var(--muted);}
+ .screen[data-screen="4"] .step{color:var(--bg);}
+ h1{margin:0;font-weight:700;text-align:center;line-height:1.2;color:#fff;}
+ h1:focus{outline:none;}
+ .h-34{font-size:34px;} .h-26{font-size:26px;}
+ .sub{margin:0;text-align:center;font-size:17px;color:var(--muted);line-height:1.4;}
+ button{font:inherit;cursor:pointer;border:0;-webkit-tap-highlight-color:transparent;}
+ button:focus-visible{outline:3px solid var(--focus);outline-offset:3px;}
+ .screen[data-screen="4"] button:focus-visible{outline-color:#fff;}
+ .chip{
+ min-width:44px;min-height:44px;padding:0 12px;border-radius:14px;
+ background:var(--btn-dark);color:#fff;font-size:14px;font-weight:700;
+ }
+ .screen[data-screen="4"] .chip{background:var(--bg);}
+ .spacer{flex:1 1 0;min-height:0;}
+ .go{
+ width:100%;height:190px;border-radius:999px;background:var(--orange-deep);color:#fff;
+ font-size:28px;font-weight:700;letter-spacing:.5px;flex-shrink:0;
+ box-shadow:0 10px 0 #a84300;transition:transform .08s ease,box-shadow .08s ease;
+ }
+ .go:active{transform:translateY(6px);box-shadow:0 4px 0 #a84300;}
+ .bar{height:16px;border-radius:8px;background:var(--track);overflow:hidden;flex-shrink:0;position:relative;}
+ .bar > span{position:absolute;left:0;top:0;bottom:0;width:100%;border-radius:8px;background:var(--orange);transition:width 1s linear;}
+ .bar.amber > span{background:var(--amber);}
+ .timer{font-variant-numeric:tabular-nums;}
+ .camera{
+ background:var(--cam);border-radius:24px;display:flex;align-items:center;justify-content:center;
+ flex:1 1 auto;min-height:200px;max-height:470px;position:relative;
+ }
+ .target{width:14px;height:14px;border:2px solid #fff;border-radius:2px;}
+ .shutter-row{display:flex;justify-content:center;flex-shrink:0;}
+ .shutter{width:96px;height:96px;border-radius:50%;background:var(--orange);box-shadow:inset 0 0 0 6px #fff;}
+ .shutter:active{transform:scale(.96);}
+ .item{
+ background:#fff;border-radius:20px;padding:14px;display:flex;gap:14px;align-items:center;color:#1a1a1a;flex-shrink:0;
+ }
+ .item .sw{width:84px;height:84px;border-radius:14px;background:#d9ccb4;flex-shrink:0;}
+ .item .t{margin:0;font-size:20px;font-weight:700;}
+ .item .s{margin:4px 0 0;font-size:15px;color:#6b6b6b;}
+ .yes{min-height:104px;border-radius:20px;background:var(--green);color:#fff;font-size:24px;font-weight:700;letter-spacing:.8px;width:100%;flex-shrink:0;}
+ .nope{min-height:72px;border-radius:20px;background:var(--btn-dark);color:#fff;font-size:20px;font-weight:700;letter-spacing:.8px;width:100%;flex-shrink:0;}
+ .big{font-size:120px;line-height:1;font-variant-numeric:tabular-nums;}
+ .best{margin:0;text-align:center;font-size:34px;font-weight:700;color:#fff;}
+ .next{min-height:84px;border-radius:20px;background:var(--bg);color:#fff;font-size:20px;font-weight:600;letter-spacing:.8px;width:100%;flex-shrink:0;}
+ .timeup{margin:0;text-align:center;font-size:15px;font-weight:600;color:var(--amber);min-height:18px;}
+ @media (prefers-reduced-motion:reduce){
+ *,*::before,*::after{animation:none !important;transition:none !important;}
+ }
+ .confirm{margin:10px auto 0;text-align:center;font-size:18px;font-weight:700;color:#fff;background:#6b2600;padding:8px 16px;border-radius:999px;width:max-content;max-width:100%;}
+</style>
+</head>
+<body>
+<main class="stage" id="app">
+
+ <!-- 1 / 4 · Go -->
+ <section class="screen is-active" data-screen="1" aria-labelledby="s1-h">
+ <div class="topbar"><p class="step">1 / 4 · Go</p></div>
+ <h1 id="s1-h" class="h-34" tabindex="-1"><span aria-hidden="true">⏱️</span> 30 seconds.</h1>
+ <p class="sub">Check in a sample before time runs out.</p>
+ <div class="spacer"></div>
+ <button class="go" id="go-btn" aria-label="Go! Start the 30 second clock">GO!</button>
+ <div class="spacer"></div>
+ </section>
+
+ <!-- 2 / 4 · Scan -->
+ <section class="screen" data-screen="2" aria-labelledby="s2-h">
+ <div class="topbar">
+ <p class="step">2 / 4 · Scan</p>
+ <div class="right">
+ <button class="chip pause" aria-pressed="false">❚❚ Pause</button>
+ <button class="chip" data-home aria-label="Home">⌂ Home</button>
+ </div>
+ </div>
+ <div class="bar" aria-hidden="true"><span></span></div>
+ <h1 id="s2-h" class="h-26" tabindex="-1"><span class="timer" role="timer" aria-live="off">0:30</span> · scan the box</h1>
+ <p class="timeup" aria-live="polite"></p>
+ <div class="camera" role="img" aria-label="Camera viewfinder (pretend)"><div class="target"></div></div>
+ <div class="shutter-row"><button class="shutter" data-go="3" aria-label="Scan the box"></button></div>
+ </section>
+
+ <!-- 3 / 4 · Confirm -->
+ <section class="screen" data-screen="3" aria-labelledby="s3-h">
+ <div class="topbar">
+ <p class="step">3 / 4 · Confirm</p>
+ <div class="right">
+ <button class="chip pause" aria-pressed="false">❚❚ Pause</button>
+ <button class="chip" data-home aria-label="Home">⌂ Home</button>
+ </div>
+ </div>
+ <div class="bar amber" aria-hidden="true"><span></span></div>
+ <h1 id="s3-h" class="h-26" tabindex="-1"><span class="timer" role="timer" aria-live="off">0:30</span> · is this it?</h1>
+ <p class="timeup" aria-live="polite"></p>
+ <div class="item">
+ <div class="sw" aria-hidden="true"></div>
+ <div><p class="t">Linen — Oat</p><p class="s">Kravet · K-4410</p></div>
+ </div>
+ <div class="spacer"></div>
+ <button class="yes" id="yes-btn">YES ✓</button>
+ <button class="nope" data-go="2">NOPE</button>
+ </section>
+
+ <!-- 4 / 4 · Result -->
+ <section class="screen" data-screen="4" aria-labelledby="s4-h">
+ <div class="topbar"><p class="step">4 / 4 · Result</p><button class="chip" data-home aria-label="Home">⌂ Home</button></div>
+ <div class="spacer"></div>
+ <h1 id="s4-h" class="big" tabindex="-1"><span id="elapsed">19</span>s</h1>
+ <p class="best" id="best-line">New best! 🔥</p>
+ <p class="confirm">✓ Sample marked received</p>
+ <div class="spacer"></div>
+ <button class="next" data-home>NEXT BOX</button>
+ </section>
+
+</main>
+
+<script>
+ (function(){
+ const TOTAL = 30;
+ let remaining = TOTAL, tick = null, paused = false, best = Infinity;
+ const screens = Array.from(document.querySelectorAll('.screen'));
+
+ function fmt(s){ return '0:' + String(Math.max(0, s)).padStart(2, '0'); }
+ function render(){
+ document.querySelectorAll('.timer').forEach(t => t.textContent = fmt(remaining));
+ document.querySelectorAll('.bar > span').forEach(b => b.style.width = (remaining / TOTAL * 100) + '%');
+ const msg = remaining <= 0 ? "Time's up — finish when you're ready." : '';
+ document.querySelectorAll('.timeup').forEach(p => { if (p.textContent !== msg) p.textContent = msg; });
+ document.querySelectorAll('.pause').forEach(b => {
+ b.setAttribute('aria-pressed', paused ? 'true' : 'false');
+ b.textContent = paused ? '▶ Resume' : '❚❚ Pause';
+ b.disabled = remaining <= 0;
+ });
+ }
+ function stop(){ if (tick){ clearInterval(tick); tick = null; } }
+ function start(){
+ stop(); remaining = TOTAL; paused = false; render();
+ tick = setInterval(() => {
+ if (paused) return;
+ remaining -= 1;
+ if (remaining <= 0){ remaining = 0; stop(); }
+ render();
+ }, 1000);
+ }
+ // NOTE: the clock never navigates on its own; only the user's buttons move between screens.
+ function go(n){
+ screens.forEach(s => s.classList.toggle('is-active', s.dataset.screen === String(n)));
+ const h = document.querySelector('.screen.is-active h1');
+ if (h) h.focus({preventScroll:true});
+ }
+ function home(){ stop(); remaining = TOTAL; paused = false; render(); go(1); }
+
+ document.addEventListener('click', e => {
+ if (e.target.closest('[data-home]')) { home(); return; }
+ const b = e.target.closest('[data-go]');
+ if (b) go(b.dataset.go);
+ const p = e.target.closest('.pause');
+ if (p){ paused = !paused; render(); }
+ });
+ document.getElementById('go-btn').addEventListener('click', () => { start(); go(2); });
+ document.getElementById('yes-btn').addEventListener('click', () => {
+ stop();
+ const used = TOTAL - remaining;
+ document.getElementById('elapsed').textContent = used;
+ document.getElementById('best-line').textContent = used <= best ? 'New best! 🔥' : 'Nice run! 👏';
+ best = Math.min(best, used);
+ go(4);
+ });
+ render();
+ })();
+</script>
+<script>window.__theme = {id:'v21', name:'Beat the Clock', screens:4};</script>
+</body>
+</html>
diff --git a/public/themes/v22-buddy-pup.html b/public/themes/v22-buddy-pup.html
new file mode 100644
index 0000000..5003a1f
--- /dev/null
+++ b/public/themes/v22-buddy-pup.html
@@ -0,0 +1,153 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>v22 — Buddy Pup</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --cream:#fff1dc;
+ --orange:#e07a2f; /* Figma accent — used for backgrounds / shutter */
+ --orange-ink:#b3561a; /* darkened for white-text contrast (4.9:1) */
+ --brown:#3b2a1a;
+ --muted:#6e5a42; /* Figma #9a8266 darkened for 4.5:1 */
+ }
+ *{box-sizing:border-box;}
+ html,body{margin:0;padding:0;background:#0a0a0a;font-family:'Inter',system-ui,sans-serif;}
+ .stage{
+ width:390px;height:780px;margin:24px auto;position:relative;
+ background:var(--cream);border-radius:36px;overflow:hidden;
+ box-shadow:0 20px 60px rgba(0,0,0,.5);border:8px solid #000;color:var(--brown);
+ }
+ @media (max-width:405.98px){
+ .stage{width:100%;border-radius:0;border:0;margin:0;height:100vh;height:100dvh;min-height:640px;}
+ }
+ .screen{
+ display:none;position:absolute;inset:0;flex-direction:column;gap:16px;
+ padding:20px 24px 24px;overflow-y:auto;background:var(--cream);
+ }
+ .screen.is-active{display:flex;}
+ .screen.treat{background:var(--orange);}
+ .topbar{display:flex;align-items:center;justify-content:space-between;min-height:44px;flex-shrink:0;}
+ .step{font-size:13px;font-weight:600;color:var(--muted);margin:0;}
+ .treat .step{color:var(--brown);}
+ button{font-family:inherit;cursor:pointer;border:0;}
+ button:focus-visible{outline:3px solid #1a1a1a;outline-offset:3px;box-shadow:0 0 0 6px #fff;}
+ .home{
+ min-width:44px;min-height:44px;padding:0 14px;border-radius:14px;background:#fff;
+ color:var(--brown);font-size:15px;font-weight:700;
+ }
+ .buddy{display:flex;gap:12px;align-items:center;flex-shrink:0;}
+ .buddy .pup{font-size:48px;line-height:1;}
+ .buddy.big .pup{font-size:72px;}
+ .bubble{flex:1;min-width:0;background:#fff;border-radius:20px;padding:14px;}
+ .bubble h1{margin:0;font-size:20px;font-weight:600;line-height:1.25;color:var(--brown);}
+ h1:focus{outline:none;}
+ .spacer{flex:1 0 0;min-height:0;}
+ .btn{
+ width:100%;min-height:80px;border-radius:20px;padding:10px 16px;flex-shrink:0;
+ font-size:22px;font-weight:700;background:#fff;color:var(--brown);
+ display:flex;align-items:center;justify-content:center;gap:10px;text-align:center;
+ }
+ .btn.primary{background:var(--orange-ink);color:#fff;}
+ .btn.h84{min-height:84px;}
+ .btn.h72{min-height:72px;}
+ .camera{
+ flex:1 1 440px;min-height:220px;max-height:440px;background:#1c1c1e;border-radius:24px;
+ display:flex;align-items:center;justify-content:center;color:#fff;font-size:18px;font-weight:600;
+ position:relative;overflow:hidden;
+ }
+ .camera::before,.camera::after{content:"";position:absolute;width:44px;height:44px;border:3px solid rgba(255,255,255,.55);}
+ .camera::before{top:18px;left:18px;border-right:0;border-bottom:0;border-radius:10px 0 0 0;}
+ .camera::after{bottom:18px;right:18px;border-left:0;border-top:0;border-radius:0 0 10px 0;}
+ .shutter-row{display:flex;justify-content:center;flex-shrink:0;}
+ .shutter{
+ width:96px;height:96px;border-radius:50%;background:var(--orange);
+ border:6px solid #fff;box-shadow:0 4px 14px rgba(0,0,0,.12);
+ }
+ .card{display:flex;gap:14px;align-items:center;background:#fff;border-radius:20px;padding:14px;flex-shrink:0;}
+ .card .sw{width:84px;height:84px;border-radius:14px;background:#c9a24b;flex-shrink:0;
+ background-image:repeating-radial-gradient(circle at 30% 30%,rgba(255,255,255,.18) 0 6px,transparent 6px 14px);}
+ .card .t{font-size:20px;font-weight:700;color:#222;margin:0 0 4px;}
+ .card .s{font-size:15px;color:#6b6b6b;margin:0;}
+ .treat-art{font-size:96px;line-height:1.1;text-align:center;margin:0;}
+ .treat h1{font-size:32px;font-weight:700;text-align:center;margin:0;color:var(--brown);}
+ .btn.onorange{color:var(--orange-ink);}
+ .sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;}
+</style>
+</head>
+<body>
+<main class="stage" aria-label="Buddy Pup photo capture mockup">
+
+ <!-- 1 / 4 · Hi! -->
+ <section class="screen is-active" data-screen="1" aria-label="Step 1 of 4: Hi">
+ <div class="topbar"><p class="step">1 / 4 · Hi!</p></div>
+ <div class="buddy big">
+ <span class="pup" aria-hidden="true">🐶</span>
+ <div class="bubble"><h1>Hi! I’m Biscuit. What are we doing?</h1></div>
+ </div>
+ <div class="spacer"></div>
+ <button class="btn primary" data-go="2"><span aria-hidden="true">🆕</span> New item</button>
+ <button class="btn" data-go="2"><span aria-hidden="true">🔧</span> Fix an item</button>
+ <button class="btn" data-go="2"><span aria-hidden="true">🔍</span> Find something</button>
+ <button class="btn" data-go="2"><span aria-hidden="true">📦</span> A sample came</button>
+ </section>
+
+ <!-- 2 / 4 · Fetch a photo -->
+ <section class="screen" data-screen="2" aria-label="Step 2 of 4: Fetch a photo">
+ <div class="topbar"><p class="step">2 / 4 · Fetch a photo</p><button class="home" data-go="1">⌂ Home</button></div>
+ <div class="buddy">
+ <span class="pup" aria-hidden="true">🐶</span>
+ <div class="bubble"><h1>Show me the sample! <span aria-hidden="true">📸</span></h1></div>
+ </div>
+ <div class="camera" role="img" aria-label="Camera viewfinder (pretend)"><span><span aria-hidden="true">📷</span> point at the sample</span></div>
+ <div class="shutter-row"><button class="shutter" data-go="3" aria-label="Take photo"></button></div>
+ </section>
+
+ <!-- 3 / 4 · Sniff check -->
+ <section class="screen" data-screen="3" aria-label="Step 3 of 4: Sniff check">
+ <div class="topbar"><p class="step">3 / 4 · Sniff check</p><button class="home" data-go="1">⌂ Home</button></div>
+ <div class="buddy">
+ <span class="pup" aria-hidden="true">🐶</span>
+ <div class="bubble"><h1>Sniff sniff… is it this one?</h1></div>
+ </div>
+ <div class="card">
+ <div class="sw" aria-hidden="true"></div>
+ <div><p class="t">Damask — Gold</p><p class="s">York</p></div>
+ </div>
+ <div class="spacer"></div>
+ <button class="btn primary h84" data-go="4">Yes, good boy!</button>
+ <button class="btn h72" data-go="2">Nope, try again</button>
+ </section>
+
+ <!-- 4 / 4 · Treat -->
+ <section class="screen treat" data-screen="4" aria-label="Step 4 of 4: Treat">
+ <div class="topbar"><p class="step">4 / 4 · Treat</p><button class="home" data-go="1">⌂ Home</button></div>
+ <div class="spacer"></div>
+ <p class="treat-art" aria-hidden="true">🐶🦴</p>
+ <h1>Updated! Treat time.</h1>
+ <div class="spacer"></div>
+ <button class="btn h72 onorange" data-go="1">Do another</button>
+ </section>
+
+</main>
+<script>
+ (function(){
+ var screens = Array.prototype.slice.call(document.querySelectorAll('[data-screen]'));
+ function go(n){
+ screens.forEach(function(s){ s.classList.toggle('is-active', s.getAttribute('data-screen') === String(n)); });
+ var h = document.querySelector('[data-screen="' + n + '"] h1');
+ if (h){ h.setAttribute('tabindex','-1'); h.focus({preventScroll:true}); }
+ }
+ document.addEventListener('click', function(e){
+ var b = e.target.closest('[data-go]');
+ if (b) go(b.getAttribute('data-go'));
+ });
+ })();
+</script>
+<script>window.__theme = {id:'v22', name:'Buddy Pup', screens:4};</script>
+</body>
+</html>
diff --git a/public/themes/v23-code-keypad.html b/public/themes/v23-code-keypad.html
new file mode 100644
index 0000000..2619e0f
--- /dev/null
+++ b/public/themes/v23-code-keypad.html
@@ -0,0 +1,172 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>v23 — Code Keypad</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --bg:#e9edf3;
+ --blue:#2f6fd6;
+ --ink:#1e2330;
+ --muted:#555c68; /* Figma #6d7686 darkened for 4.5:1 on #e9edf3 */
+ }
+ *{box-sizing:border-box;}
+ html,body{margin:0;padding:0;background:#0a0a0a;font-family:'Inter',system-ui,sans-serif;}
+ .stage{
+ width:390px;height:780px;margin:24px auto;position:relative;
+ background:var(--bg);border-radius:36px;overflow:hidden;
+ box-shadow:0 20px 60px rgba(0,0,0,.5);border:8px solid #000;color:var(--ink);
+ }
+ @media (max-width:405.98px){
+ .stage{width:100%;border-radius:0;border:0;margin:0;height:100vh;height:100dvh;min-height:640px;}
+ }
+ .screen{
+ display:none;position:absolute;inset:0;flex-direction:column;gap:16px;
+ padding:20px 24px 24px;overflow-y:auto;background:var(--bg);
+ }
+ .screen.is-active{display:flex;}
+ .screen.done{background:var(--blue);color:#fff;}
+ .topbar{display:flex;align-items:center;justify-content:space-between;min-height:44px;flex-shrink:0;}
+ .step{font-size:13px;font-weight:600;color:var(--muted);margin:0;}
+ .done .step{color:#fff;}
+ button{font-family:inherit;cursor:pointer;border:0;}
+ button:focus-visible{outline:3px solid var(--ink);outline-offset:3px;box-shadow:0 0 0 6px #fff;}
+ .done button:focus-visible{outline-color:#fff;box-shadow:0 0 0 6px var(--ink);}
+ .home{
+ min-width:44px;min-height:44px;padding:0 14px;border-radius:14px;background:#fff;
+ color:var(--ink);font-size:15px;font-weight:700;
+ }
+ h1:focus{outline:none;}
+ .title{font-size:34px;font-weight:700;text-align:center;margin:0;}
+ .sub{font-size:18px;color:var(--muted);text-align:center;margin:0;}
+ .spacer{flex:1 0 0;min-height:0;}
+ .btn{
+ width:100%;min-height:96px;border-radius:20px;padding:10px 16px;flex-shrink:0;
+ font-size:22px;font-weight:700;background:#fff;color:var(--ink);
+ display:flex;align-items:center;justify-content:center;gap:10px;text-align:center;
+ }
+ .btn.primary{background:var(--blue);color:#fff;}
+ .btn.h80{min-height:80px;}
+ .btn.h72{min-height:72px;}
+ .btn.onblue{color:var(--blue);}
+ .display{
+ background:#fff;border-radius:14px;min-height:80px;display:flex;align-items:center;justify-content:center;
+ font-size:22px;font-weight:700;letter-spacing:.35em;flex-shrink:0;padding-left:.35em;
+ font-variant-numeric:tabular-nums;
+ }
+ .display .caret{animation:blink 1s steps(1) infinite;}
+ @keyframes blink{50%{opacity:0;}}
+ @media (prefers-reduced-motion:reduce){.display .caret{animation:none;}}
+ .grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;flex-shrink:0;}
+ .key{
+ min-height:88px;border-radius:20px;background:#fff;color:var(--ink);
+ font-size:32px;font-weight:700;display:flex;align-items:center;justify-content:center;
+ }
+ .key:active{transform:scale(.97);}
+ .key.ok{background:var(--blue);color:#fff;}
+ .code{font-size:40px;font-weight:700;text-align:center;margin:0;letter-spacing:.02em;}
+ .card{display:flex;gap:14px;align-items:center;background:#fff;border-radius:20px;padding:14px;flex-shrink:0;}
+ .card .sw{width:84px;height:84px;border-radius:14px;background:#d8cbb3;flex-shrink:0;
+ background-image:repeating-linear-gradient(90deg,rgba(120,100,60,.14) 0 2px,transparent 2px 5px);}
+ .card .t{font-size:20px;font-weight:700;color:#222;margin:0 0 4px;}
+ .card .s{font-size:15px;color:#6b6b6b;margin:0;}
+ .check{font-size:120px;font-weight:700;line-height:1;text-align:center;margin:0;color:#fff;}
+ .done h1{font-size:40px;font-weight:700;text-align:center;margin:0;color:#fff;}
+ .sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;}
+</style>
+</head>
+<body>
+<main class="stage" aria-label="Code Keypad photo capture mockup">
+
+ <!-- 1 / 4 · Home -->
+ <section class="screen is-active" data-screen="1" aria-label="Step 1 of 4: Home">
+ <div class="topbar"><p class="step">1 / 4 · Home</p></div>
+ <h1 class="title">Got a code?</h1>
+ <p class="sub">It’s on the back of the sample</p>
+ <div class="spacer"></div>
+ <button class="btn primary" data-go="2"><span aria-hidden="true">🔢</span> Type the code</button>
+ <button class="btn" data-go="3"><span aria-hidden="true">📷</span> No code — take a photo</button>
+ <div class="spacer"></div>
+ </section>
+
+ <!-- 2 / 4 · Keypad -->
+ <section class="screen" data-screen="2" aria-label="Step 2 of 4: Keypad">
+ <div class="topbar"><p class="step">2 / 4 · Keypad</p><button class="home" data-go="1">⌂ Home</button></div>
+ <h1 class="sr-only">Type the code</h1>
+ <div class="display" aria-live="polite"><span class="sr-only">Code entered: </span><span id="codeText">T410</span><span class="caret" aria-hidden="true">_</span></div>
+ <div class="spacer"></div>
+ <div class="grid">
+ <button class="key" data-key="1">1</button>
+ <button class="key" data-key="2">2</button>
+ <button class="key" data-key="3">3</button>
+ <button class="key" data-key="4">4</button>
+ <button class="key" data-key="5">5</button>
+ <button class="key" data-key="6">6</button>
+ <button class="key" data-key="7">7</button>
+ <button class="key" data-key="8">8</button>
+ <button class="key" data-key="9">9</button>
+ <button class="key" data-key="del" aria-label="Delete last digit">⌫</button>
+ <button class="key" data-key="0">0</button>
+ <button class="key ok" data-go="3" aria-label="Done — look up code">✓</button>
+ </div>
+ </section>
+
+ <!-- 3 / 4 · Match -->
+ <section class="screen" data-screen="3" aria-label="Step 3 of 4: Match">
+ <div class="topbar"><p class="step">3 / 4 · Match</p><button class="home" data-go="1">⌂ Home</button></div>
+ <h1 class="code" id="matchCode">T41020</h1>
+ <div class="card">
+ <div class="sw" aria-hidden="true"></div>
+ <div><p class="t">Grasscloth — Sand</p><p class="s">Thibaut</p></div>
+ </div>
+ <div class="spacer"></div>
+ <button class="btn primary h80" data-go="4"><span aria-hidden="true">📦</span> Mark sample received</button>
+ <button class="btn h80" data-go="4"><span aria-hidden="true">📸</span> Update photo</button>
+ </section>
+
+ <!-- 4 / 4 · Done -->
+ <section class="screen done" data-screen="4" aria-label="Step 4 of 4: Done">
+ <div class="topbar"><p class="step">4 / 4 · Done</p><button class="home" data-go="1">⌂ Home</button></div>
+ <div class="spacer"></div>
+ <p class="check" aria-hidden="true">✓</p>
+ <h1>Received!</h1>
+ <div class="spacer"></div>
+ <button class="btn h72 onblue" data-go="1">Next code</button>
+ </section>
+
+</main>
+<script>
+ (function(){
+ var screens = Array.prototype.slice.call(document.querySelectorAll('[data-screen]'));
+ var codeEl = document.getElementById('codeText');
+ var matchEl = document.getElementById('matchCode');
+ function go(n){
+ screens.forEach(function(s){ s.classList.toggle('is-active', s.getAttribute('data-screen') === String(n)); });
+ var h = document.querySelector('[data-screen="' + n + '"] h1');
+ if (h){ h.setAttribute('tabindex','-1'); h.focus({preventScroll:true}); }
+ }
+ document.addEventListener('click', function(e){
+ var k = e.target.closest('[data-key]');
+ if (k){
+ var v = k.getAttribute('data-key'), t = codeEl.textContent;
+ if (v === 'del') t = t.slice(0, -1);
+ else if (t.length < 8) t += v;
+ codeEl.textContent = t;
+ return;
+ }
+ var b = e.target.closest('[data-go]');
+ if (!b) return;
+ var n = b.getAttribute('data-go');
+ if (n === '3' && b.classList.contains('ok')) matchEl.textContent = codeEl.textContent.length >= 4 ? codeEl.textContent : 'T41020';
+ if (n === '1') codeEl.textContent = 'T410';
+ go(n);
+ });
+ })();
+</script>
+<script>window.__theme = {id:'v23', name:'Code Keypad', screens:4};</script>
+</body>
+</html>
diff --git a/public/themes/v24-split-screen.html b/public/themes/v24-split-screen.html
new file mode 100644
index 0000000..fe0b5e0
--- /dev/null
+++ b/public/themes/v24-split-screen.html
@@ -0,0 +1,162 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>v24 — Split Screen</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --bg:#f5f5f2;
+ --ink:#1d1d1d;
+ --teal:#0f766e;
+ --muted:#666666; /* Figma #808080 darkened for 4.5:1 */
+ --tan:#b9a27a;
+ }
+ *{box-sizing:border-box;}
+ html,body{margin:0;padding:0;background:#0a0a0a;font-family:'Inter',system-ui,sans-serif;}
+ .stage{
+ width:390px;height:780px;margin:24px auto;position:relative;
+ background:var(--bg);border-radius:36px;overflow:hidden;
+ box-shadow:0 20px 60px rgba(0,0,0,.5);border:8px solid #000;color:var(--ink);
+ }
+ @media (max-width:405.98px){
+ .stage{width:100%;border-radius:0;border:0;margin:0;height:100vh;height:100dvh;min-height:640px;}
+ }
+ .screen{
+ display:none;position:absolute;inset:0;flex-direction:column;gap:16px;
+ padding:20px 24px 24px;overflow-y:auto;background:var(--bg);
+ }
+ .screen.is-active{display:flex;}
+ .topbar{display:flex;align-items:center;justify-content:space-between;min-height:44px;flex-shrink:0;}
+ .step{font-size:13px;font-weight:600;color:var(--muted);margin:0;}
+ button{font-family:inherit;cursor:pointer;border:0;}
+ button:focus-visible{outline:3px solid var(--teal);outline-offset:3px;box-shadow:0 0 0 6px #fff;}
+ .home{
+ min-width:44px;min-height:44px;padding:0 14px;border-radius:14px;background:#fff;
+ color:var(--ink);font-size:15px;font-weight:700;border:2px solid #e2e2dc;
+ }
+ h1:focus{outline:none;}
+ /* camera: the "always on top" half of the split */
+ .camera{
+ flex:1 1 400px;min-height:200px;max-height:400px;background:#1c1c1e;border-radius:24px;
+ display:flex;align-items:center;justify-content:center;color:#fff;font-size:18px;font-weight:600;
+ position:relative;overflow:hidden;text-align:center;padding:0 16px;
+ }
+ .camera.shot{background:var(--tan);color:var(--ink);} /* dark text on tan: 6.8:1 (Figma had white, 2.5:1) */
+ .camera.live::after{
+ content:"";position:absolute;inset:22%;border:2px dashed rgba(255,255,255,.45);border-radius:14px;
+ }
+ .camera .rec{position:absolute;top:14px;left:16px;font-size:13px;font-weight:700;color:#fff;
+ background:#000;padding:4px 10px;border-radius:999px;}
+ .camera .rec::before{content:"";display:inline-block;width:8px;height:8px;border-radius:50%;background:#ff453a;margin-right:6px;vertical-align:1px;}
+ .prompt{font-size:24px;font-weight:700;margin:0;flex-shrink:0;}
+ .prompt.saved{font-size:28px;color:var(--teal);}
+ .row{display:flex;gap:12px;flex-shrink:0;}
+ .btn{
+ flex:1 1 0;min-width:0;min-height:80px;border-radius:20px;padding:10px 12px;
+ font-size:22px;font-weight:700;background:var(--ink);color:#fff;
+ display:flex;align-items:center;justify-content:center;text-align:center;
+ }
+ .btn.teal{background:var(--teal);}
+ .btn.light{background:#fff;color:var(--ink);min-height:72px;}
+ .btn.light.other{color:var(--muted);}
+ .btn.tall{min-height:96px;}
+ .spacer{flex:1 0 0;min-height:0;}
+ .sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;}
+</style>
+</head>
+<body>
+<main class="stage" aria-label="Split Screen photo capture mockup">
+
+ <!-- 1 / 4 · Always camera -->
+ <section class="screen is-active" data-screen="1" aria-label="Step 1 of 4: Always camera">
+ <div class="topbar"><p class="step">1 / 4 · Always camera</p></div>
+ <div class="camera live" role="img" aria-label="Live camera viewfinder (pretend)">
+ <span class="rec" aria-hidden="true">LIVE</span>
+ <span><span aria-hidden="true">📷</span> camera is always on top</span>
+ </div>
+ <h1 class="prompt">Point, then pick:</h1>
+ <div class="row">
+ <button class="btn teal" data-go="2" data-mode="new item">New</button>
+ <button class="btn" data-go="2" data-mode="fix">Fix</button>
+ </div>
+ <div class="row">
+ <button class="btn" data-go="2" data-mode="find">Find</button>
+ <button class="btn" data-go="2" data-mode="sample">Sample</button>
+ </div>
+ </section>
+
+ <!-- 2 / 4 · Picked New -->
+ <section class="screen" data-screen="2" aria-label="Step 2 of 4: Picked">
+ <div class="topbar"><p class="step">2 / 4 · Picked <span data-slot="modeTitle">New</span></p><button class="home" data-go="1">⌂ Home</button></div>
+ <div class="camera shot" role="img" aria-label="Photo taken (pretend)"><span>✓ photo taken</span></div>
+ <h1 class="prompt">Brand?</h1>
+ <div class="row">
+ <button class="btn light" data-go="3" data-brand="Thibaut">Thibaut</button>
+ <button class="btn light" data-go="3" data-brand="York">York</button>
+ </div>
+ <div class="row">
+ <button class="btn light" data-go="3" data-brand="Kravet">Kravet</button>
+ <button class="btn light other" data-go="3" data-brand="Other">Other</button>
+ </div>
+ </section>
+
+ <!-- 3 / 4 · Check -->
+ <section class="screen" data-screen="3" aria-label="Step 3 of 4: Check">
+ <div class="topbar"><p class="step">3 / 4 · Check</p><button class="home" data-go="1">⌂ Home</button></div>
+ <div class="camera shot" role="img" aria-label="Photo preview (pretend)"><span><span data-slot="brand">Thibaut</span> · <span data-slot="mode">new item</span></span></div>
+ <h1 class="prompt">Looks good?</h1>
+ <div class="spacer"></div>
+ <div class="row">
+ <button class="btn light tall" data-go="2">Redo</button>
+ <button class="btn teal tall" data-go="4">Save ✓</button>
+ </div>
+ </section>
+
+ <!-- 4 / 4 · Saved -->
+ <section class="screen" data-screen="4" aria-label="Step 4 of 4: Saved">
+ <div class="topbar"><p class="step">4 / 4 · Saved</p><button class="home" data-go="1">⌂ Home</button></div>
+ <div class="camera live" role="img" aria-label="Live camera viewfinder (pretend)">
+ <span class="rec" aria-hidden="true">LIVE</span>
+ <span><span aria-hidden="true">📷</span> ready for the next one</span>
+ </div>
+ <h1 class="prompt saved">Saved ✓ Next!</h1>
+ <div class="row">
+ <button class="btn teal" data-go="2" data-mode="new item">New</button>
+ <button class="btn" data-go="2" data-mode="fix">Fix</button>
+ </div>
+ <div class="row">
+ <button class="btn" data-go="2" data-mode="find">Find</button>
+ <button class="btn" data-go="2" data-mode="sample">Sample</button>
+ </div>
+ </section>
+
+</main>
+<script>
+ (function(){
+ var screens = Array.prototype.slice.call(document.querySelectorAll('[data-screen]'));
+ var modeTitles = {'new item':'New', fix:'Fix', find:'Find', sample:'Sample'};
+ function setSlot(name, val){
+ Array.prototype.forEach.call(document.querySelectorAll('[data-slot="' + name + '"]'), function(el){ el.textContent = val; });
+ }
+ function go(n){
+ screens.forEach(function(s){ s.classList.toggle('is-active', s.getAttribute('data-screen') === String(n)); });
+ var h = document.querySelector('[data-screen="' + n + '"] h1');
+ if (h){ h.setAttribute('tabindex','-1'); h.focus({preventScroll:true}); }
+ }
+ document.addEventListener('click', function(e){
+ var b = e.target.closest('[data-go]');
+ if (!b) return;
+ var m = b.getAttribute('data-mode'), br = b.getAttribute('data-brand');
+ if (m){ setSlot('mode', m); setSlot('modeTitle', modeTitles[m] || 'New'); }
+ if (br){ setSlot('brand', br); }
+ go(b.getAttribute('data-go'));
+ });
+ })();
+</script>
+<script>window.__theme = {id:'v24', name:'Split Screen', screens:4};</script>
+</body>
+</html>
diff --git a/public/themes/v25-paper-stack.html b/public/themes/v25-paper-stack.html
new file mode 100644
index 0000000..3049574
--- /dev/null
+++ b/public/themes/v25-paper-stack.html
@@ -0,0 +1,158 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>v25 — Paper Stack</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --bg:#d9dee4;
+ --ink:#23272e;
+ --soft:#eef0f3;
+ --muted:#4d5561; /* Figma #6b7380 darkened for 4.5:1 on #d9dee4 */
+ --muted-paper:#6b7380; /* Figma value, already 4.8:1 on white paper */
+ }
+ *{box-sizing:border-box;}
+ html,body{margin:0;padding:0;background:#0a0a0a;font-family:'Inter',system-ui,sans-serif;}
+ .stage{
+ width:390px;height:780px;margin:24px auto;position:relative;
+ background:var(--bg);border-radius:36px;overflow:hidden;
+ box-shadow:0 20px 60px rgba(0,0,0,.5);border:8px solid #000;color:var(--ink);
+ }
+ @media (max-width:405.98px){
+ .stage{width:100%;border-radius:0;border:0;margin:0;height:100vh;height:100dvh;min-height:640px;}
+ }
+ .screen{
+ display:none;position:absolute;inset:0;flex-direction:column;gap:16px;
+ padding:20px 24px 30px;overflow-y:auto;background:var(--bg);
+ }
+ .screen.is-active{display:flex;}
+ .topbar{display:flex;align-items:center;justify-content:space-between;min-height:44px;flex-shrink:0;}
+ .step{font-size:13px;font-weight:600;color:var(--muted);margin:0;}
+ button{font-family:inherit;cursor:pointer;border:0;}
+ button:focus-visible{outline:3px solid #1f5fbf;outline-offset:3px;box-shadow:0 0 0 6px #fff;}
+ .home{
+ min-width:44px;min-height:44px;padding:0 14px;border-radius:14px;background:#fff;
+ color:var(--ink);font-size:15px;font-weight:700;box-shadow:0 2px 6px rgba(0,0,0,.08);
+ }
+ h1:focus{outline:none;}
+ /* the paper sheet, with the rest of the stack peeking out underneath */
+ .pile{flex:1 1 0;min-height:0;position:relative;display:flex;}
+ .pile::before,.pile::after{
+ content:"";position:absolute;left:10px;right:10px;bottom:-6px;height:30px;
+ background:#fff;border-radius:10px;opacity:.75;box-shadow:0 4px 10px rgba(0,0,0,.08);z-index:0;
+ }
+ .pile::after{left:20px;right:20px;bottom:-12px;opacity:.5;}
+ .paper{
+ position:relative;z-index:1;flex:1;min-height:0;background:#fff;border-radius:10px;
+ box-shadow:0 8px 20px rgba(0,0,0,.14);padding:24px;display:flex;flex-direction:column;gap:14px;
+ overflow-y:auto;animation:slidein .28s ease-out;
+ }
+ @keyframes slidein{from{transform:translateY(14px) rotate(-.6deg);}to{transform:none;}}
+ @media (prefers-reduced-motion:reduce){.paper{animation:none;}}
+ .paper h1{font-size:30px;font-weight:700;margin:0;color:var(--ink);}
+ .paper .sub{font-size:18px;color:var(--muted-paper);margin:0;}
+ .spacer{flex:1 0 0;min-height:0;}
+ .btn{
+ width:100%;min-height:72px;border-radius:20px;padding:10px 16px;flex-shrink:0;
+ font-size:22px;font-weight:700;background:var(--soft);color:var(--ink);
+ display:flex;align-items:center;justify-content:center;gap:10px;text-align:center;
+ }
+ .btn.primary{background:var(--ink);color:#fff;}
+ .btn.h84{min-height:84px;}
+ .card{display:flex;gap:14px;align-items:center;background:#f4f5f7;border-radius:20px;padding:14px;flex-shrink:0;}
+ .card .sw{width:84px;height:84px;border-radius:14px;flex-shrink:0;}
+ .sw.navy{background:#1f3a5f repeating-linear-gradient(90deg,rgba(255,255,255,.14) 0 6px,transparent 6px 16px);}
+ .sw.cream{background:#e9dfc8 repeating-linear-gradient(90deg,rgba(150,120,70,.16) 0 6px,transparent 6px 16px);}
+ .card .t{font-size:20px;font-weight:700;color:#222;margin:0 0 4px;}
+ .card .s{font-size:15px;color:#5f5f5f;margin:0;}
+ .hero-swatch{
+ flex:1 1 300px;min-height:120px;max-height:300px;border-radius:24px;
+ background:#1f3a5f repeating-linear-gradient(90deg,rgba(255,255,255,.14) 0 14px,transparent 14px 36px);
+ }
+ .sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;}
+ .found{margin:0 0 4px;font-size:14px;font-weight:700;color:#1f6b43;letter-spacing:.02em;}
+</style>
+</head>
+<body>
+<main class="stage" aria-label="Paper Stack photo capture mockup">
+
+ <!-- 1 / 4 · Top sheet -->
+ <section class="screen is-active" data-screen="1" aria-label="Step 1 of 4: Top sheet">
+ <div class="topbar"><p class="step">1 / 4 · Top sheet</p></div>
+ <div class="pile"><div class="paper">
+ <h1>What’s the job?</h1>
+ <div class="spacer"></div>
+ <button class="btn primary" data-go="2">Add a new item</button>
+ <button class="btn" data-go="2">Update an item</button>
+ <button class="btn" data-go="2">Find an item</button>
+ <button class="btn" data-go="2">A sample came in</button>
+ </div></div>
+ </section>
+
+ <!-- 2 / 4 · Sheet 2 -->
+ <section class="screen" data-screen="2" aria-label="Step 2 of 4: Find it">
+ <div class="topbar"><p class="step">2 / 4 · Sheet 2</p><button class="home" data-go="1">⌂ Home</button></div>
+ <div class="pile"><div class="paper">
+ <h1>Find it</h1>
+ <p class="sub">Pick how</p>
+ <div class="spacer"></div>
+ <button class="btn primary h84" data-go="3"><span aria-hidden="true">📷</span> Scan label</button>
+ <button class="btn h84" data-go="3"><span aria-hidden="true">🎤</span> Say it</button>
+ <button class="btn h84" data-go="3"><span aria-hidden="true">⌨️</span> Type it</button>
+ </div></div>
+ </section>
+
+ <!-- 3 / 4 · Sheet 3 -->
+ <section class="screen" data-screen="3" aria-label="Step 3 of 4: Results">
+ <div class="topbar"><p class="step">3 / 4 · Sheet 3</p><button class="home" data-go="1">⌂ Home</button></div>
+ <div class="pile"><div class="paper">
+ <h1>Results</h1>
+ <div class="card">
+ <div class="sw navy" aria-hidden="true"></div>
+ <div><p class="t">Stripe — Navy</p><p class="s">Schumacher</p></div>
+ </div>
+ <div class="card">
+ <div class="sw cream" aria-hidden="true"></div>
+ <div><p class="t">Stripe — Cream</p><p class="s">Schumacher</p></div>
+ </div>
+ <div class="spacer"></div>
+ <button class="btn primary" data-go="4">Open the first one</button>
+ </div></div>
+ </section>
+
+ <!-- 4 / 4 · Sheet 4 -->
+ <section class="screen" data-screen="4" aria-label="Step 4 of 4: Item">
+ <div class="topbar"><p class="step">4 / 4 · Sheet 4</p><button class="home" data-go="1">⌂ Home</button></div>
+ <div class="pile"><div class="paper">
+ <p class="found">✓ Found it</p>
+ <h1>Stripe — Navy</h1>
+ <div class="hero-swatch" role="img" aria-label="Stripe Navy swatch (pretend photo)"></div>
+ <p class="sub">Schumacher · in stock</p>
+ <div class="spacer"></div>
+ <button class="btn primary" data-go="1">Update the photo</button>
+ <button class="btn" data-go="1">Start over</button>
+ </div></div>
+ </section>
+
+</main>
+<script>
+ (function(){
+ var screens = Array.prototype.slice.call(document.querySelectorAll('[data-screen]'));
+ function go(n){
+ screens.forEach(function(s){ s.classList.toggle('is-active', s.getAttribute('data-screen') === String(n)); });
+ var h = document.querySelector('[data-screen="' + n + '"] h1');
+ if (h){ h.setAttribute('tabindex','-1'); h.focus({preventScroll:true}); }
+ }
+ document.addEventListener('click', function(e){
+ var b = e.target.closest('[data-go]');
+ if (b) go(b.getAttribute('data-go'));
+ });
+ })();
+</script>
+<script>window.__theme = {id:'v25', name:'Paper Stack', screens:4};</script>
+</body>
+</html>
diff --git a/public/themes/v3-text-buddy.html b/public/themes/v3-text-buddy.html
new file mode 100644
index 0000000..14aa2bd
--- /dev/null
+++ b/public/themes/v3-text-buddy.html
@@ -0,0 +1,322 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>v3 — Text Buddy</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link href="https://fonts.googleapis.com/css2?family=Fraunces:wght@600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --paper:#EFEAE3;
+ --gold:#C9A24B;
+ --sage:#DCE8DA;
+ --text:#2B2B2B;
+ }
+ *{box-sizing:border-box;}
+ html,body{margin:0;padding:0;background:#0a0a0a;font-family:'Inter',sans-serif;}
+ .stage{
+ width:390px;height:780px;margin:24px auto;position:relative;
+ background:var(--paper);border-radius:36px;overflow:hidden;
+ box-shadow:0 20px 60px rgba(0,0,0,.5);border:8px solid #000;color:var(--text);
+ display:flex;flex-direction:column;
+ }
+ header{
+ display:flex;align-items:center;justify-content:space-between;
+ padding:18px 20px 14px;border-bottom:1px solid #ded6c8;background:var(--paper);flex-shrink:0;
+ }
+ header .wordmark{font-family:'Fraunces',serif;font-weight:700;font-size:18px;}
+ header .sam-label{font-size:13px;color:#6B6258;margin-top:2px;}
+ header .home-btn{
+ background:#fff;border:2px solid #ded6c8;border-radius:12px;min-height:44px;padding:0 14px;
+ font-size:14px;font-weight:600;cursor:pointer;color:var(--text);
+ }
+ header .home-btn:focus-visible{outline:3px solid var(--gold);outline-offset:2px;}
+ #thread{
+ flex:1;overflow-y:auto;padding:18px 16px 24px;display:flex;flex-direction:column;gap:12px;
+ }
+ .bubble{
+ max-width:78%;padding:14px 16px;border-radius:16px;font-size:17px;line-height:1.4;
+ }
+ .sam{
+ align-self:flex-start;background:#fff;border-left:5px solid var(--gold);
+ border-radius:4px 16px 16px 16px;
+ }
+ .user{
+ align-self:flex-end;background:var(--sage);border-radius:16px 4px 16px 16px;
+ }
+ .chips{display:flex;flex-wrap:wrap;gap:10px;align-self:flex-start;max-width:92%;}
+ .chip{
+ min-height:56px;padding:0 18px;border-radius:20px;background:#fff;border:2px solid var(--gold);
+ color:var(--text);font-size:17px;font-weight:600;cursor:pointer;
+ }
+ .chip:focus-visible{outline:3px solid var(--gold);outline-offset:2px;}
+ .cam-bubble{
+ font:inherit;color:inherit;
+ align-self:flex-start;background:#fff;border:2px dashed var(--gold);border-radius:16px;
+ padding:18px;display:flex;flex-direction:column;align-items:center;gap:8px;cursor:pointer;
+ min-width:180px;
+ }
+ .cam-bubble .cam-icon{font-size:40px;}
+ .cam-bubble .cam-label{font-size:15px;color:#6B6258;}
+ .swatch-bubble{
+ align-self:flex-end;width:180px;height:130px;border-radius:14px;
+ background:repeating-linear-gradient(45deg,#8FAE8B 0 20px,#B9CBB2 20px 40px),linear-gradient(135deg,#8FAE8B,#6f8d6c);
+ background-blend-mode:multiply;
+ }
+ .result-card{
+ font:inherit;color:inherit;text-align:left;
+ align-self:flex-start;display:flex;gap:10px;background:#fff;border-radius:14px;padding:10px;
+ border:2px solid #ded6c8;cursor:pointer;max-width:88%;align-items:center;
+ }
+ .result-card .thumb{width:56px;height:56px;border-radius:10px;flex-shrink:0;
+ background:repeating-linear-gradient(45deg,#C97B5B 0 12px,#e0a184 12px 24px);}
+ .result-card .rc-title{font-size:16px;font-weight:700;}
+ .result-card .rc-sub{font-size:13px;color:#6B6258;}
+ .full-item{
+ align-self:flex-start;background:#fff;border-radius:16px;padding:14px;border:2px solid #ded6c8;max-width:88%;
+ }
+ .full-item .swatch{width:100%;height:150px;border-radius:12px;margin-bottom:10px;
+ background:repeating-linear-gradient(45deg,#8FAE8B 0 20px,#B9CBB2 20px 40px),linear-gradient(135deg,#8FAE8B,#6f8d6c);
+ background-blend-mode:multiply;}
+ .full-item .rc-title{font-size:18px;font-weight:700;}
+ .full-item .rc-sub{font-size:14px;color:#6B6258;}
+ .search-mock{
+ align-self:flex-start;background:#fff;border-radius:14px;padding:14px 16px;font-size:16px;
+ color:#6B6258;border:2px solid #ded6c8;min-width:220px;
+ }
+ .text-input{
+ width:100%;border:none;outline:none;font-size:17px;font-family:'Inter',sans-serif;
+ background:transparent;color:var(--text);
+ }
+ .text-input:focus-visible{outline:3px solid var(--gold);outline-offset:2px;border-radius:4px;}
+ .spinner-inline{display:inline-block;width:16px;height:16px;border-radius:50%;
+ border:3px solid rgba(0,0,0,.12);border-top-color:var(--gold);animation:spin 1s linear infinite;margin-right:8px;vertical-align:-3px;}
+ @keyframes spin{to{transform:rotate(360deg);}}
+</style>
+<style id="ux-baseline">/* TK-12343 UX baseline: full-bleed on real phones, visible focus everywhere */
+@media (max-width:405px){ html,body{margin:0;padding:0} body{overflow-x:hidden} .stage,.phone{width:100% !important;max-width:100%;margin:0 !important;border:0 !important;border-radius:0 !important;box-shadow:none !important;height:100dvh;min-height:640px} }
+button:focus-visible,a:focus-visible,[tabindex]:focus-visible,input:focus-visible{outline:3px solid #9C7A33 !important;outline-offset:2px}
+</style>
+</head>
+<body>
+<div class="stage">
+ <header>
+ <div>
+ <div class="wordmark">DW</div>
+ <div class="sam-label">chatting with Sam</div>
+ </div>
+ <button class="home-btn" onclick="startHome()">⌂ New chat</button>
+ </header>
+ <div id="thread"></div>
+</div>
+
+<script>
+ const thread = document.getElementById('thread');
+
+ function scrollBottom(){ thread.scrollTop = thread.scrollHeight; }
+
+ function addSam(text){
+ const el = document.createElement('div');
+ el.className = 'bubble sam';
+ el.innerHTML = text;
+ thread.appendChild(el);
+ scrollBottom();
+ return el;
+ }
+ function addUser(text){
+ const el = document.createElement('div');
+ el.className = 'bubble user';
+ el.innerHTML = text;
+ thread.appendChild(el);
+ scrollBottom();
+ return el;
+ }
+ function addChips(options){
+ const wrap = document.createElement('div');
+ wrap.className = 'chips';
+ options.forEach(opt => {
+ const b = document.createElement('button');
+ b.className = 'chip';
+ b.textContent = opt.label;
+ b.onclick = () => { wrap.remove(); opt.action(); };
+ wrap.appendChild(b);
+ });
+ thread.appendChild(wrap);
+ scrollBottom();
+ return wrap;
+ }
+ function addCameraBubble(onShot){
+ function showCameraStep(){
+ const el = document.createElement('button');
+ el.type = 'button';
+ el.className = 'cam-bubble';
+ el.setAttribute('aria-label', 'Take photo');
+ el.innerHTML = '<div class="cam-icon">📸</div><div class="cam-label">Tap to shoot</div>';
+ el.onclick = () => {
+ el.remove();
+ const photo = addUser('<div class="swatch-bubble"></div>');
+ const looking = addSam('<span class="spinner-inline"></span>Looking…');
+ setTimeout(() => {
+ looking.remove();
+ addSam("Here's what I got — good?");
+ addChips([
+ {label:'Looks good', action: () => { onShot(); }},
+ {label:'Retake', action: () => { photo.remove(); showCameraStep(); }},
+ ]);
+ }, 900);
+ };
+ thread.appendChild(el);
+ scrollBottom();
+ }
+ showCameraStep();
+ }
+
+ function startHome(){
+ thread.innerHTML = '';
+ addSam('Hey! What are we doing today?');
+ addChips([
+ {label:'New Item', action: flowAddStart},
+ {label:'Update', action: flowUpdateStart},
+ {label:'Find', action: flowFindStart},
+ {label:'Sample In', action: flowSampleStart},
+ ]);
+ }
+
+ // ---------- ADD NEW ITEM ----------
+ function flowAddStart(){
+ addUser('New Item');
+ addSam('Cool, snap a photo of it 📸');
+ addCameraBubble(() => {
+ addSam('Which brand?');
+ addChips([
+ {label:'York', action: () => addAddSaved()},
+ {label:'Brewster', action: () => addAddSaved()},
+ {label:'Kravet', action: () => addAddSaved()},
+ {label:'Thibaut', action: () => addAddSaved()},
+ ]);
+ });
+ }
+ function addAddSaved(){
+ addSam('Saved it as a draft! 🎉');
+ addChips([
+ {label:'Yes, another', action: () => { addUser('Yes, another'); flowAddStart(); }},
+ {label:'Nope, done', action: () => { addUser('Nope, done'); addSam("Great, see you next time! 👋 Tap '⌂ New chat' any time."); }},
+ ]);
+ }
+
+ // ---------- UPDATE AN ITEM ----------
+ function flowUpdateStart(){
+ addUser('Update');
+ addSam('Show me the label or pattern');
+ addCameraBubble(() => {
+ addSam("Found it — Palm Damask — Sage, code DW-10423. That the one?");
+ addChips([
+ {label:"Yep, that's it", action: () => {
+ addUser("Yep, that's it");
+ addSam('Send me the new photo');
+ addCameraBubble(() => {
+ addSam('Updated! ✅');
+ });
+ }},
+ {label:'Nope, let me search', action: () => {
+ addUser('Nope, let me search');
+ addSam('No problem — type the name or code:');
+ const el = document.createElement('div');
+ el.className = 'search-mock';
+ el.innerHTML = '<input type="text" class="text-input" enterkeyhint="search" placeholder="Palm Damask…" aria-label="Type the item name or code">';
+ thread.appendChild(el);
+ scrollBottom();
+ const searchInput = el.querySelector('input');
+ const sendIt = () => {
+ const val = searchInput.value.trim() || 'Palm Damask';
+ addUser(val);
+ addSam("Great — Palm Damask — Sage, code DW-10423. That the one?");
+ addChips([
+ {label:"Yep, that's it", action: () => {
+ addUser("Yep, that's it");
+ addSam('Send me the new photo');
+ addCameraBubble(() => { addSam('Updated! ✅'); });
+ }},
+ ]);
+ };
+ searchInput.addEventListener('keydown', (e) => { if(e.key === 'Enter'){ e.preventDefault(); sendIt(); } });
+ addChips([{label:'Send', action: sendIt}]);
+ }},
+ ]);
+ });
+ }
+
+ // ---------- FIND AN ITEM ----------
+ function flowFindStart(){
+ addUser('Find');
+ findAskMethod();
+ }
+ function findAskMethod(){
+ addSam('How do you want to search? 📷 label 🎤 voice ⌨️ type');
+ addChips([
+ {label:'📷 label', action: () => { addUser('📷 label'); findShowResults(); }},
+ {label:'🎤 voice', action: () => { addUser('🎤 voice'); findShowResults(); }},
+ {label:'⌨️ type', action: () => { addUser('⌨️ type'); findShowResults(); }},
+ ]);
+ }
+ function findShowResults(){
+ addSam("Here's what I found:");
+ const items = [
+ {name:'Palm Damask — Sage', code:'DW-10423'},
+ {name:'Meadow Stripe — Gold', code:'DW-20115'},
+ {name:'Coastal Weave — Blue', code:'DW-30871'},
+ ];
+ items.forEach(it => {
+ const card = document.createElement('button');
+ card.type = 'button';
+ card.className = 'result-card';
+ card.innerHTML = '<div class="thumb"></div><div><div class="rc-title">'+it.name+'</div><div class="rc-sub">code '+it.code+'</div></div>';
+ card.onclick = () => {
+ document.querySelectorAll('.result-card').forEach(c => c.remove());
+ addUser(it.name);
+ findShowDetail(it);
+ };
+ thread.appendChild(card);
+ });
+ scrollBottom();
+ }
+ function findShowDetail(it){
+ addSam('Here it is —');
+ const el = document.createElement('div');
+ el.className = 'full-item';
+ el.innerHTML = '<div class="swatch"></div><div class="rc-title">'+it.name+'</div><div class="rc-sub">code '+it.code+'</div>';
+ thread.appendChild(el);
+ scrollBottom();
+ addChips([
+ {label:'Update This', action: () => {
+ addUser('Update This');
+ addSam('Send me the new photo');
+ addCameraBubble(() => { addSam('Updated! ✅'); });
+ }},
+ {label:'Search Again', action: () => { addUser('Search Again'); findAskMethod(); }},
+ ]);
+ }
+
+ // ---------- SAMPLE IN ----------
+ function flowSampleStart(){
+ addUser('Sample In');
+ addSam('Scan the box label');
+ addCameraBubble(() => {
+ addSam('Found it — mark it received?');
+ addChips([
+ {label:'Yes', action: () => {
+ addUser('Yes');
+ const printing = addSam('<span class="spinner-inline"></span>Printing your sticker now 🖨️');
+ setTimeout(() => {
+ printing.innerHTML = 'All set! ✅';
+ }, 1200);
+ }},
+ ]);
+ });
+ }
+
+ startHome();
+</script>
+</body>
+</html>
diff --git a/public/themes/v4-level-up.html b/public/themes/v4-level-up.html
new file mode 100644
index 0000000..12eb2e7
--- /dev/null
+++ b/public/themes/v4-level-up.html
@@ -0,0 +1,282 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+<title>v4 — Level Up</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,700&family=Nunito:wght@500;700;800;900&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --plum:#4A2E4E; --gold:#E8B84B; --cream:#F5EFE6; --green:#59B37A;
+ --ink:#2B2B2B;
+ }
+ *{box-sizing:border-box;}
+ html,body{margin:0;padding:0;background:#ddd;font-family:'Nunito',sans-serif;color:var(--ink);}
+ body{display:flex;align-items:flex-start;justify-content:center;min-height:100vh;padding:20px 0;}
+ .phone{
+ width:390px; min-height:780px; background:var(--cream);
+ border-radius:36px; overflow:hidden; box-shadow:0 24px 60px rgba(0,0,0,.28);
+ display:flex; flex-direction:column; position:relative;
+ }
+ header{
+ background:var(--plum); color:#fff; padding:18px 20px 16px;
+ display:flex; align-items:center; justify-content:space-between; min-height:56px;
+ }
+ header .mark{font-family:'Fraunces',serif; font-weight:700; font-size:18px; letter-spacing:.5px;}
+ header .homebtn{
+ background:transparent;border:none;color:#fff;font-size:22px;cursor:pointer;
+ min-width:44px;min-height:44px;line-height:1;border-radius:12px;
+ }
+ header .homebtn:hover{background:rgba(255,255,255,.12);}
+ .screen{flex:1; display:flex; flex-direction:column; gap:16px; padding:22px 20px 28px; overflow-y:auto;}
+ h1.title{font-family:'Fraunces',serif; font-weight:700; font-size:26px; margin:0 0 2px; color:var(--plum);}
+ p.sub{margin:0; font-size:17px; color:#4a4a4a;}
+ .dots{display:flex; gap:8px; margin:4px 0 6px;}
+ .dots span{width:16px;height:16px;border-radius:50%;background:#e2d9c9;border:2px solid #d8cdb8;}
+ .dots span.filled{background:var(--gold); border-color:var(--gold);}
+ button{font-family:'Nunito',sans-serif; cursor:pointer;}
+ .btn{
+ min-height:56px; width:100%; border-radius:18px; font-size:18px; font-weight:800;
+ border:none; display:flex; align-items:center; justify-content:center; gap:8px; padding:0 16px;
+ }
+ .btn-primary{background:var(--gold); color:#3a2a06;}
+ .btn-primary:active{transform:scale(.98);}
+ .btn-secondary{background:#fff; color:var(--plum); border:2px solid var(--plum);}
+ .btn-ghost{background:transparent; color:var(--plum); text-decoration:underline; min-height:44px;}
+ .btn:focus-visible, a:focus-visible, .brandcard:focus-visible, .resultcard:focus-visible{
+ outline:3px solid #2B6CB0; outline-offset:2px;
+ }
+ .card{background:#fff; border-radius:20px; padding:18px; box-shadow:0 6px 18px rgba(0,0,0,.08);}
+ .levelcard{background:#fff; border-radius:20px; padding:18px; display:flex; flex-direction:column; gap:10px; box-shadow:0 6px 18px rgba(0,0,0,.08);}
+ .levelcard h2{margin:0; font-family:'Fraunces',serif; font-size:20px; color:var(--plum);}
+ .levelcard .startrow{display:flex; align-items:center; justify-content:space-between; gap:12px; margin-top:8px;}
+ .camview{
+ flex:1; min-height:260px; background:#1c1c1e; border-radius:20px; position:relative;
+ display:flex; align-items:center; justify-content:center; color:#eee; overflow:hidden;
+ }
+ .camview .hint{position:absolute; top:14px; left:0; right:0; text-align:center; font-size:15px; color:#f0e8d8;}
+ .camview .status{font-size:20px; font-weight:700;}
+ .shutter{
+ position:absolute; bottom:20px; left:50%; transform:translateX(-50%);
+ width:76px; height:76px; border-radius:50%; background:var(--gold); border:5px solid #fff;
+ font-size:30px; display:flex; align-items:center; justify-content:center;
+ }
+ .swatch{width:100%; height:180px; border-radius:16px;}
+ .sw1{background:repeating-linear-gradient(45deg,#8FAE8B 0 18px,#7c9c78 18px 36px), radial-gradient(circle at 30% 30%, #a9c7a4, transparent 60%);}
+ .sw2{background:repeating-linear-gradient(-45deg,#7C93B0 0 16px,#617a99 16px 32px);}
+ .sw3{background:repeating-linear-gradient(90deg,#D9A441 0 14px,#c98f2c 14px 28px), radial-gradient(circle at 70% 40%, #e9c06b, transparent 55%);}
+ .brandgrid{display:grid; grid-template-columns:1fr 1fr; gap:12px;}
+ .brandcard{
+ min-height:64px; border-radius:16px; background:#fff; border:2px solid #e3d9c6;
+ font-size:17px; font-weight:700; color:var(--plum); display:flex; align-items:center; justify-content:center; text-align:center; padding:8px;
+ }
+ .brandcard.selected{border-color:var(--gold); background:#fff7e6;}
+ .itemname{font-size:19px; font-weight:800; margin:0 0 2px;}
+ .itemmeta{font-size:15px; color:#666; margin:0;}
+ .resultlist{display:flex; flex-direction:column; gap:10px;}
+ .resultcard{
+ display:flex; gap:12px; align-items:center; background:#fff; border-radius:16px; padding:10px;
+ border:2px solid #eee; min-height:72px; text-align:left; width:100%;
+ }
+ .resultcard .thumb{width:52px; height:52px; border-radius:10px; flex:none;}
+ .confetti-banner{
+ background:#2F7D55; color:#fff; border-radius:18px; padding:18px; text-align:center;
+ font-family:'Fraunces',serif; font-size:22px; font-weight:700;
+ }
+ .row{display:flex; gap:12px;}
+ .row .btn{flex:1;}
+ .homegrid{display:flex; flex-direction:column; gap:16px;}
+</style>
+<style id="ux-baseline">/* TK-12343 UX baseline: full-bleed on real phones, visible focus everywhere */
+@media (max-width:405px){ html,body{margin:0;padding:0} body{overflow-x:hidden} .stage,.phone{width:100% !important;max-width:100%;margin:0 !important;border:0 !important;border-radius:0 !important;box-shadow:none !important;height:100dvh;min-height:640px} }
+button:focus-visible,a:focus-visible,[tabindex]:focus-visible,input:focus-visible{outline:3px solid #9C7A33 !important;outline-offset:2px}
+</style>
+</head>
+<body>
+<div class="phone">
+ <header>
+ <span class="mark">DW · Photo</span>
+ <button class="homebtn" aria-label="Home" onclick="go('home')" id="homeBtnTop">🏠</button>
+ </header>
+ <div class="screen" id="screen" role="main"></div>
+</div>
+
+<script>
+const JOBS = [
+ {id:'new', title:'Add New Item', emoji:'🆕', levels:3},
+ {id:'update', title:'Update an Item', emoji:'🔄', levels:3},
+ {id:'find', title:'Find an Item', emoji:'🔍', levels:2},
+ {id:'sample', title:'Sample Arrived', emoji:'📦', levels:2},
+];
+const BRANDS = ['York Wallcoverings','Brewster Home','Kravet','Cole & Son','Thibaut','Ronald Redding'];
+const MOCK_ITEMS = [
+ {name:'Palm Damask', color:'Sage', code:'DW-10423', sw:'sw1'},
+ {name:'Trellis Weave', color:'Indigo', code:'DW-88214', sw:'sw2'},
+ {name:'Linen Bloom', color:'Blush', code:'DW-55071', sw:'sw3'},
+];
+
+const state = {screen:'home', job:null, brand:null, item:null};
+
+function go(screen){ state.screen = screen; render(); }
+function startJob(id){ state.job = id; state.brand=null; state.item=null; go(id+'-1'); }
+function jobTitle(){ return JOBS.find(j=>j.id===state.job).title; }
+function jobLevels(){ return JOBS.find(j=>j.id===state.job).levels; }
+
+function dots(filled, total){
+ total = total || jobLevels();
+ let out = '<div class="dots">';
+ for(let i=1;i<=total;i++) out += `<span class="${i<=filled?'filled':''}"></span>`;
+ out += '</div>';
+ return out;
+}
+
+function levelHeader(n, name){
+ return `<h1 class="title">${jobTitle()}</h1>${dots(n)}<p class="sub"><strong>Level ${n} of ${jobLevels()}</strong> — ${name}</p>`;
+}
+
+function cameraScreen(hint, onShoot){
+ return `
+ <div class="camview">
+ <div class="hint">${hint}</div>
+ <div class="status" id="camStatus">Tap the shutter</div>
+ <button class="shutter" aria-label="Snap It" onclick="${onShoot}">📸</button>
+ </div>`;
+}
+function shoot(cb){
+ const st = document.getElementById('camStatus');
+ const btn = document.querySelector('.shutter');
+ if(btn) btn.disabled = true;
+ if(st) st.textContent = 'Snapping…';
+ const from = state.screen;
+ setTimeout(()=>{
+ if (state.screen !== from) return;
+ const item = MOCK_ITEMS[Math.floor(Math.random()*MOCK_ITEMS.length)];
+ state.item = item;
+ cb();
+ }, 700);
+}
+
+function render(){
+ const s = document.getElementById('screen');
+ let html = '';
+ document.getElementById('homeBtnTop').style.display = state.screen==='home' ? 'none' : 'inline-flex';
+
+ if(state.screen==='home'){
+ html += `<h1 class="title">Pick a Job</h1><p class="sub">Every job is a quick level path.</p>
+ <div class="homegrid">`;
+ JOBS.forEach(j=>{
+ html += `<div class="levelcard">
+ <h2>${j.emoji} ${j.title}</h2>
+ <p class="sub">Level 1 of ${j.levels}</p>
+ <div class="startrow">
+ ${dots(0, j.levels)}
+ <button class="btn btn-primary" style="width:auto;min-width:110px;" onclick="startJob('${j.id}')">Start</button>
+ </div>
+ </div>`;
+ });
+ html += `</div>`;
+ }
+
+ else if(state.screen==='new-1'){
+ html += levelHeader(1,'Snap It');
+ html += cameraScreen('Take a photo of the sample', "shoot(()=>go('new-2'))");
+ }
+ else if(state.screen==='new-2'){
+ html += levelHeader(2,'Pick the Brand');
+ html += `<div class="brandgrid">`;
+ BRANDS.forEach(b=>{
+ html += `<button class="brandcard" onclick="state.brand='${b}'; go('new-3')">${b}</button>`;
+ });
+ html += `</div>`;
+ }
+ else if(state.screen==='new-3'){
+ html += levelHeader(3,'Save It');
+ html += `<div class="card">
+ <div class="swatch ${state.item.sw}"></div>
+ <p class="itemname">${state.item.name} — ${state.item.color}</p>
+ <p class="itemmeta">Brand: ${state.brand} · code ${state.item.code}</p>
+ </div>`;
+ html += `<button class="btn btn-primary" onclick="go('new-done')">Save as Draft ⭐</button>`;
+ }
+ else if(state.screen==='new-done'){
+ html += `<div class="confetti-banner">🎉 +10 points! Nice work</div>`;
+ html += `<div class="card"><p class="itemname">${state.item.name} — ${state.item.color}</p><p class="itemmeta">Saved as a draft · ${state.brand}</p></div>`;
+ html += `<div class="row"><button class="btn btn-secondary" onclick="startJob('new')">Do Another</button><button class="btn btn-primary" onclick="go('home')">Home</button></div>`;
+ }
+
+ else if(state.screen==='update-1'){
+ html += levelHeader(1,'Find It');
+ html += `<p class="sub">Scan the label, or search by typing.</p>`;
+ html += cameraScreen('Show me the label or pattern', "shoot(()=>go('update-2'))");
+ }
+ else if(state.screen==='update-2'){
+ html += levelHeader(2,'Confirm It');
+ html += `<div class="card">
+ <div class="swatch ${state.item.sw}"></div>
+ <p class="itemname">${state.item.name} — ${state.item.color}</p>
+ <p class="itemmeta">code ${state.item.code}</p>
+ </div>`;
+ html += `<div class="row"><button class="btn btn-secondary" onclick="go('update-1')">No, search again</button><button class="btn btn-primary" onclick="go('update-3')">Yes, that's it</button></div>`;
+ }
+ else if(state.screen==='update-3'){
+ html += levelHeader(3,'Snap the New Photo');
+ html += cameraScreen('Take the new photo', "shoot(()=>go('update-done'))");
+ }
+ else if(state.screen==='update-done'){
+ html += `<div class="confetti-banner">✅ Updated!</div>`;
+ html += `<div class="card"><div class="swatch ${state.item.sw}"></div><p class="itemname">${state.item.name} — ${state.item.color}</p><p class="itemmeta">code ${state.item.code}</p></div>`;
+ html += `<div class="row"><button class="btn btn-secondary" onclick="startJob('update')">Do Another</button><button class="btn btn-primary" onclick="go('home')">Home</button></div>`;
+ }
+
+ else if(state.screen==='find-1'){
+ html += levelHeader(1,'Search');
+ html += `<div class="row" style="flex-direction:column;">
+ <button class="btn btn-secondary" onclick="go('find-2')">📷 Scan</button>
+ <button class="btn btn-secondary" onclick="go('find-2')">🎤 Voice</button>
+ <button class="btn btn-secondary" onclick="go('find-2')">⌨️ Type</button>
+ </div>`;
+ }
+ else if(state.screen==='find-2'){
+ html += levelHeader(2,'Pick It');
+ html += `<div class="resultlist">`;
+ MOCK_ITEMS.forEach((it,i)=>{
+ html += `<button class="resultcard" onclick="state.item=MOCK_ITEMS[${i}]; go('find-done')">
+ <div class="thumb ${it.sw}"></div>
+ <div><p class="itemname" style="font-size:17px;">${it.name} — ${it.color}</p><p class="itemmeta">code ${it.code}</p></div>
+ </button>`;
+ });
+ html += `</div>`;
+ }
+ else if(state.screen==='find-done'){
+ html += `<h1 class="title">Found It</h1>`;
+ html += `<div class="card"><div class="swatch ${state.item.sw}"></div><p class="itemname">${state.item.name} — ${state.item.color}</p><p class="itemmeta">code ${state.item.code}</p></div>`;
+ html += `<div class="row"><button class="btn btn-secondary" onclick="startJob('find')">Do Another</button><button class="btn btn-primary" onclick="go('home')">Home</button></div>`;
+ }
+
+ else if(state.screen==='sample-1'){
+ html += levelHeader(1,'Scan the Box');
+ html += cameraScreen('Scan the label on the box', "shoot(()=>go('sample-2'))");
+ }
+ else if(state.screen==='sample-2'){
+ html += levelHeader(2,'Confirm & Print');
+ html += `<div class="card"><div class="swatch ${state.item.sw}"></div><p class="itemname">${state.item.name} — ${state.item.color}</p><p class="itemmeta">code ${state.item.code}</p></div>`;
+ html += `<button class="btn btn-primary" id="markReceivedBtn" onclick="markReceived()">Mark It Received</button>`;
+ }
+ else if(state.screen==='sample-done'){
+ html += `<div class="confetti-banner">🖨️ Sticker printed! +10 points</div>`;
+ html += `<div class="row"><button class="btn btn-secondary" onclick="startJob('sample')">Do Another</button><button class="btn btn-primary" onclick="go('home')">Home</button></div>`;
+ }
+
+ s.innerHTML = html;
+}
+function markReceived(){
+ const btn = document.getElementById('markReceivedBtn');
+ if(btn){ btn.disabled = true; btn.textContent = 'Printing…'; }
+ const from = state.screen; setTimeout(()=>{ if (state.screen === from) go('sample-done'); }, 700);
+}
+
+render();
+</script>
+</body>
+</html>
diff --git a/public/themes/v5-swipe-deck.html b/public/themes/v5-swipe-deck.html
new file mode 100644
index 0000000..80d33f6
--- /dev/null
+++ b/public/themes/v5-swipe-deck.html
@@ -0,0 +1,309 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+<title>v5 — Swipe Deck</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,700&family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
+<style>
+ :root{ --blush:#F4EDE7; --green:#3FA66B; --red:#C0503F; --gold:#C9A24B; --ink:#2B2B2B; }
+ *{box-sizing:border-box; touch-action:pan-y;}
+ html,body{margin:0;padding:0;background:#ddd;font-family:'Inter',sans-serif;color:var(--ink);}
+ body{display:flex; align-items:flex-start; justify-content:center; min-height:100vh; padding:20px 0;}
+ .phone{
+ width:390px; min-height:780px; background:var(--blush); border-radius:36px; overflow:hidden;
+ box-shadow:0 24px 60px rgba(0,0,0,.28); display:flex; flex-direction:column; position:relative;
+ }
+ header{ padding:16px 20px 10px; text-align:center; }
+ header .mark{ font-family:'Fraunces',serif; font-weight:700; font-size:18px; color:#7A5C1F; }
+ header{ position:relative; }
+ header .homebtn{ position:absolute; right:14px; top:8px; min-width:44px; min-height:44px; padding:0 12px; border-radius:12px;
+ border:2px solid #d9cfc4; background:#fff; color:var(--ink); font:700 14px Inter,sans-serif; cursor:pointer; }
+ header .homebtn[hidden]{ display:none; }
+ header .homebtn:focus-visible{ outline:3px solid #2B6CB0; outline-offset:2px; }
+ .screen-area{ flex:1; position:relative; display:flex; align-items:center; justify-content:center; padding:14px 18px; overflow:hidden; }
+ .inner{ width:100%; display:flex; flex-direction:column; gap:14px; }
+ h1.title{ font-family:'Fraunces',serif; font-weight:700; font-size:24px; margin:0 0 4px; text-align:center; }
+ p.sub{ margin:0; font-size:17px; color:#555; text-align:center; }
+ .card{
+ background:#fff; border-radius:22px; padding:20px; box-shadow:0 10px 28px rgba(0,0,0,.14);
+ display:flex; flex-direction:column; gap:12px; user-select:none;
+ }
+ .card.swiper{ cursor:grab; will-change:transform, opacity; }
+ .card.swiper:active{ cursor:grabbing; }
+ .itemname{ font-size:20px; font-weight:800; margin:0; }
+ .itemmeta{ font-size:15px; color:#666; margin:0; }
+ .swatch{ width:100%; height:170px; border-radius:16px; }
+ .sw1{background:repeating-linear-gradient(45deg,#8FAE8B 0 18px,#7c9c78 18px 36px);}
+ .sw2{background:repeating-linear-gradient(-45deg,#7C93B0 0 16px,#617a99 16px 32px);}
+ .sw3{background:repeating-linear-gradient(90deg,#D9A441 0 14px,#c98f2c 14px 28px);}
+ .camview{
+ height:280px; background:#1c1c1e; border-radius:20px; position:relative;
+ display:flex; align-items:center; justify-content:center; color:#eee;
+ }
+ .camview .hint{position:absolute; top:14px; left:0; right:0; text-align:center; font-size:15px; color:#f0e8d8;}
+ .camview .status{ font-size:19px; font-weight:700; }
+ .shutter{
+ position:absolute; bottom:18px; left:50%; transform:translateX(-50%);
+ width:74px; height:74px; border-radius:50%; background:var(--gold); border:5px solid #fff; font-size:28px;
+ display:flex; align-items:center; justify-content:center;
+ }
+ button{ font-family:'Inter',sans-serif; cursor:pointer; }
+ .btn{ min-height:56px; width:100%; border-radius:18px; font-size:18px; font-weight:700; border:none;
+ display:flex; align-items:center; justify-content:center; gap:8px; padding:0 16px; }
+ .btn-primary{ background:var(--gold); color:#3a2a06; }
+ .btn-secondary{ background:#fff; color:var(--ink); border:2px solid #ddd; }
+ .btn:focus-visible, .brandcard:focus-visible, .tabbtn:focus-visible{ outline:3px solid #2B6CB0; outline-offset:2px; }
+ .row{ display:flex; gap:12px; }
+ .row .btn{ flex:1; }
+ .swipebtns{ display:flex; justify-content:center; gap:36px; margin-top:6px; }
+ .circlewrap{ display:flex; flex-direction:column; align-items:center; gap:6px; }
+ .circle{
+ width:64px; height:64px; border-radius:50%; border:none; font-size:26px; font-weight:800;
+ display:flex; align-items:center; justify-content:center; box-shadow:0 6px 14px rgba(0,0,0,.15);
+ }
+ .circle.reject{ background:#fff; color:var(--red); border:3px solid var(--red); }
+ .circle.accept{ background:#fff; color:var(--green); border:3px solid var(--green); }
+ .circle-label{ font-size:14px; font-weight:700; color:#555; }
+ .brandgrid{ display:grid; grid-template-columns:1fr 1fr; gap:10px; }
+ .brandcard{ min-height:60px; border-radius:14px; background:#fff; border:2px solid #e6dccb; font-size:16px; font-weight:700; }
+ .resultlabel{ position:absolute; top:10px; left:10px; background:rgba(0,0,0,.55); color:#fff; font-size:13px; padding:4px 10px; border-radius:999px; }
+ .bottombar{
+ display:flex; justify-content:space-around; align-items:center; padding:10px 6px 16px;
+ background:#fff; border-top:1px solid #eee;
+ }
+ .tabbtn{
+ background:none; border:none; display:flex; flex-direction:column; align-items:center; gap:4px;
+ min-width:64px; min-height:56px; font-size:12px; font-weight:700; color:#5c5c5c;
+ }
+ .tabbtn .ic{ font-size:24px; }
+ .tabbtn.active{ color:#7A5C1F; }
+</style>
+<style id="ux-baseline">/* TK-12343 UX baseline: full-bleed on real phones, visible focus everywhere */
+@media (max-width:405px){ html,body{margin:0;padding:0} body{overflow-x:hidden} .stage,.phone{width:100% !important;max-width:100%;margin:0 !important;border:0 !important;border-radius:0 !important;box-shadow:none !important;height:100dvh;min-height:640px} }
+button:focus-visible,a:focus-visible,[tabindex]:focus-visible,input:focus-visible{outline:3px solid #9C7A33 !important;outline-offset:2px}
+</style>
+</head>
+<body>
+<div class="phone">
+ <header><span class="mark">DW · Photo</span><button class="homebtn" id="homeBtnTop" type="button" onclick="state.job=null; go('home')" hidden>⌂ Home</button></header>
+ <div class="screen-area" id="screen"></div>
+ <div class="bottombar">
+ <button class="tabbtn" onclick="startJob('new')" id="tab-new"><span class="ic">🆕</span>New</button>
+ <button class="tabbtn" onclick="startJob('update')" id="tab-update"><span class="ic">🔄</span>Update</button>
+ <button class="tabbtn" onclick="startJob('find')" id="tab-find"><span class="ic">🔍</span>Find</button>
+ <button class="tabbtn" onclick="startJob('sample')" id="tab-sample"><span class="ic">📦</span>Sample</button>
+ </div>
+</div>
+
+<script>
+const MOCK_ITEMS = [
+ {name:'Palm Damask', color:'Sage', code:'DW-10423', sw:'sw1'},
+ {name:'Trellis Weave', color:'Indigo', code:'DW-88214', sw:'sw2'},
+ {name:'Linen Bloom', color:'Blush', code:'DW-55071', sw:'sw3'},
+];
+const BRANDS = ['York Wallcoverings','Brewster Home','Kravet','Cole & Son','Thibaut','Ronald Redding'];
+
+const state = { screen:'home', job:null, item:null, brand:null, resultIndex:0 };
+let currentSwiper = null;
+
+function go(screen){ state.screen = screen; render(); }
+function startJob(job){ state.job = job; state.item=null; state.brand=null; state.resultIndex=0;
+ const map = {new:'new-cam', update:'update-scan', find:'find-method', sample:'sample-scan'};
+ go(map[job]);
+}
+function randomItem(){ return MOCK_ITEMS[Math.floor(Math.random()*MOCK_ITEMS.length)]; }
+
+function shoot(cb){
+ const st = document.getElementById('camStatus');
+ const btn = document.querySelector('.shutter');
+ if(btn) btn.disabled = true;
+ if(st) st.textContent = 'Snapping…';
+ const from = state.screen; setTimeout(()=>{ if (state.screen !== from) return; state.item = randomItem(); cb(); }, 700);
+}
+
+function swipeButtons(rejectAria, acceptAria){
+ acceptAria = acceptAria || 'Yes';
+ return `<div class="swipebtns">
+ <div class="circlewrap">
+ <button class="circle reject" aria-label="${rejectAria}" onclick="currentSwiper.swipeLeft()">✗</button>
+ <span class="circle-label">Redo</span>
+ </div>
+ <div class="circlewrap">
+ <button class="circle accept" aria-label="${acceptAria}" onclick="currentSwiper.swipeRight()">✓</button>
+ <span class="circle-label">Yes</span>
+ </div>
+ </div>`;
+}
+function cameraCard(hint, onShoot){
+ return `<div class="camview">
+ <div class="hint">${hint}</div>
+ <div class="status" id="camStatus">Tap the shutter</div>
+ <button class="shutter" aria-label="Snap It" onclick="${onShoot}">📸</button>
+ </div>`;
+}
+
+// generic pointer-drag swipe; buttons call swipeRight()/swipeLeft() on the returned handle
+function attachSwipe(el, onRight, onLeft){
+ let startX=0, dragging=false, dx=0;
+ function down(e){ dragging=true; startX=e.clientX; el.style.transition='none'; el.setPointerCapture && el.setPointerCapture(e.pointerId); }
+ function move(e){ if(!dragging) return; dx = e.clientX-startX;
+ el.style.transform = `translateX(${dx}px) rotate(${dx/18}deg)`;
+ el.style.opacity = 1 - Math.min(Math.abs(dx)/450,.5);
+ }
+ function up(){ if(!dragging) return; dragging=false;
+ el.style.transition='transform .25s ease, opacity .25s ease';
+ if(dx>90) flyOut(1,onRight); else if(dx<-90) flyOut(-1,onLeft); else { el.style.transform=''; el.style.opacity=1; }
+ dx=0;
+ }
+ function flyOut(dir,cb){
+ el.style.transform = `translateX(${dir*520}px) rotate(${dir*28}deg)`;
+ el.style.opacity=0;
+ setTimeout(()=>{ if(cb) cb(); }, 200);
+ }
+ el.addEventListener('pointerdown', down);
+ el.addEventListener('pointermove', move);
+ el.addEventListener('pointerup', up);
+ el.addEventListener('pointercancel', up);
+ const handle = { swipeRight:()=>flyOut(1,onRight), swipeLeft:()=>flyOut(-1,onLeft) };
+ return handle;
+}
+
+function render(){
+ const s = document.getElementById('screen');
+ document.getElementById('homeBtnTop').hidden = (state.screen === 'home');
+ ['new','update','find','sample'].forEach(j=>{
+ document.getElementById('tab-'+j).classList.toggle('active', state.job===j);
+ });
+ let html = '<div class="inner">';
+
+ if(state.screen==='home'){
+ html += `<div class="card" style="text-align:center; align-items:center;">
+ <h1 class="title">Ready when you are</h1>
+ <p class="sub">Pick a job below to get started.</p>
+ </div>`;
+ }
+
+ else if(state.screen==='new-cam'){
+ html += `<h1 class="title">Add New Item</h1>` + cameraCard('Take a photo of the sample', "shoot(()=>go('new-review'))");
+ }
+ else if(state.screen==='new-review'){
+ html += `<p class="sub">Swipe or tap to decide</p>`;
+ html += `<div class="card swiper" id="swipeCard">
+ <div class="swatch ${state.item.sw}"></div>
+ <p class="itemname">${state.item.name} — ${state.item.color}</p>
+ <p class="itemmeta">code ${state.item.code}</p>
+ </div>`;
+ html += swipeButtons('Retake', 'Looks Good');
+ }
+ else if(state.screen==='new-brand'){
+ html += `<h1 class="title">Which Brand?</h1><div class="brandgrid">`;
+ BRANDS.forEach(b=>{ html += `<button class="brandcard" onclick="state.brand='${b}'; go('new-savecheck')">${b}</button>`; });
+ html += `</div>`;
+ }
+ else if(state.screen==='new-savecheck'){
+ html += `<div class="card">
+ <h1 class="title" style="font-size:20px;">Save this?</h1>
+ <div class="swatch ${state.item.sw}"></div>
+ <p class="itemname">${state.item.name} — ${state.item.color}</p>
+ <p class="itemmeta">${state.brand} · code ${state.item.code}</p>
+ </div>`;
+ html += `<button class="btn btn-primary" onclick="go('new-done')">Save as Draft</button>`;
+ }
+ else if(state.screen==='new-done'){
+ html += `<div class="card" style="text-align:center;"><h1 class="title">Saved! 🎉</h1><div class="swatch ${state.item.sw}"></div><p class="itemname">${state.item.name} — ${state.item.color}</p></div>`;
+ html += `<div class="row"><button class="btn btn-secondary" onclick="startJob('new')">Take Another</button><button class="btn btn-primary" onclick="go('home')">Home</button></div>`;
+ }
+
+ else if(state.screen==='update-scan'){
+ html += `<h1 class="title">Update an Item</h1>` + cameraCard('Show me the label or pattern', "shoot(()=>go('update-match'))");
+ }
+ else if(state.screen==='update-match'){
+ html += `<p class="sub">Is this it?</p>`;
+ html += `<div class="card swiper" id="swipeCard">
+ <div class="swatch ${state.item.sw}"></div>
+ <p class="itemname">${state.item.name} — ${state.item.color}</p>
+ <p class="itemmeta">code ${state.item.code}</p>
+ </div>`;
+ html += swipeButtons('Not it', "Yes, that's it");
+ }
+ else if(state.screen==='update-newphoto'){
+ html += `<h1 class="title">New Photo</h1>` + cameraCard('Take the new photo', "shoot(()=>go('update-review'))");
+ }
+ else if(state.screen==='update-review'){
+ html += `<p class="sub">Swipe right to confirm</p>`;
+ html += `<div class="card swiper" id="swipeCard">
+ <div class="swatch ${state.item.sw}"></div>
+ <p class="itemname">${state.item.name} — ${state.item.color}</p>
+ <p class="itemmeta">code ${state.item.code}</p>
+ </div>`;
+ html += swipeButtons('Retake');
+ }
+ else if(state.screen==='update-done'){
+ html += `<div class="card" style="text-align:center;"><h1 class="title">Updated! ✅</h1><div class="swatch ${state.item.sw}"></div><p class="itemname">${state.item.name} — ${state.item.color}</p></div>`;
+ html += `<div class="row"><button class="btn btn-secondary" onclick="startJob('update')">Take Another</button><button class="btn btn-primary" onclick="go('home')">Home</button></div>`;
+ }
+
+ else if(state.screen==='find-method'){
+ html += `<h1 class="title">Find an Item</h1>`;
+ html += `<button class="btn btn-secondary" onclick="go('find-stack')">Scan the Label</button>`;
+ html += `<button class="btn btn-secondary" onclick="go('find-stack')">Say the Name</button>`;
+ html += `<button class="btn btn-secondary" onclick="go('find-stack')">Type It</button>`;
+ }
+ else if(state.screen==='find-stack'){
+ const it = MOCK_ITEMS[state.resultIndex % MOCK_ITEMS.length];
+ html += `<p class="sub">Swipe ✗ to skip, ✓ or tap to pick</p>`;
+ html += `<div class="card swiper" id="swipeCard" onclick="pickResult()" style="position:relative;">
+ <span class="resultlabel">${(state.resultIndex%MOCK_ITEMS.length)+1} of ${MOCK_ITEMS.length}</span>
+ <div class="swatch ${it.sw}"></div>
+ <p class="itemname">${it.name} — ${it.color}</p>
+ <p class="itemmeta">code ${it.code}</p>
+ </div>`;
+ html += swipeButtons('Skip', 'Pick this one');
+ }
+ else if(state.screen==='find-detail'){
+ html += `<div class="card" style="text-align:center;"><h1 class="title" style="font-size:20px;">${state.item.name}</h1><div class="swatch ${state.item.sw}"></div><p class="itemmeta">${state.item.color} · code ${state.item.code}</p></div>`;
+ html += `<div class="row"><button class="btn btn-secondary" onclick="go('find-stack')">Back</button><button class="btn btn-primary" onclick="go('update-newphoto')">Update This</button></div>`;
+ }
+
+ else if(state.screen==='sample-scan'){
+ html += `<h1 class="title">Sample Arrived</h1>` + cameraCard('Scan the label on the box', "shoot(()=>go('sample-match'))");
+ }
+ else if(state.screen==='sample-match'){
+ html += `<p class="sub">Mark Received?</p>`;
+ html += `<div class="card swiper" id="swipeCard">
+ <div class="swatch ${state.item.sw}"></div>
+ <p class="itemname">${state.item.name} — ${state.item.color}</p>
+ <p class="itemmeta">code ${state.item.code}</p>
+ </div>`;
+ html += swipeButtons('Not it, rescan', 'Mark Received');
+ }
+ else if(state.screen==='sample-printing'){
+ html += `<div class="card" style="text-align:center;"><h1 class="title" style="font-size:20px;">Printing sticker…</h1><p class="sub">🖨️ One sec…</p></div>`;
+ }
+ else if(state.screen==='sample-done'){
+ html += `<div class="card" style="text-align:center;"><h1 class="title">Done! ✅</h1><p class="sub">Sticker printed and item received.</p></div>`;
+ html += `<div class="row"><button class="btn btn-secondary" onclick="startJob('sample')">Take Another</button><button class="btn btn-primary" onclick="go('home')">Home</button></div>`;
+ }
+
+ html += '</div>';
+ s.innerHTML = html;
+
+ const swipeEl = document.getElementById('swipeCard');
+ if(swipeEl){
+ if(state.screen==='new-review'){ currentSwiper = attachSwipe(swipeEl, ()=>go('new-brand'), ()=>go('new-cam')); }
+ else if(state.screen==='update-match'){ currentSwiper = attachSwipe(swipeEl, ()=>go('update-newphoto'), ()=>go('update-scan')); }
+ else if(state.screen==='update-review'){ currentSwiper = attachSwipe(swipeEl, ()=>go('update-done'), ()=>go('update-newphoto')); }
+ else if(state.screen==='find-stack'){ currentSwiper = attachSwipe(swipeEl, pickResult, skipResult); }
+ else if(state.screen==='sample-match'){ currentSwiper = attachSwipe(swipeEl, markReceived, ()=>go('sample-scan')); }
+ }
+}
+function pickResult(){ state.item = MOCK_ITEMS[state.resultIndex % MOCK_ITEMS.length]; go('find-detail'); }
+function skipResult(){ state.resultIndex++; go('find-stack'); }
+function markReceived(){ go('sample-printing'); setTimeout(()=>{ if (state.screen === 'sample-printing') go('sample-done'); }, 900); }
+
+render();
+</script>
+</body>
+</html>
diff --git a/public/themes/v6-one-thing.html b/public/themes/v6-one-thing.html
new file mode 100644
index 0000000..a205c36
--- /dev/null
+++ b/public/themes/v6-one-thing.html
@@ -0,0 +1,246 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+<title>v6 — One Thing At A Time</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,700&family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
+<style>
+ :root{ --bg:#FBF9F6; --ink:#232323; --gold:#C9A24B; --taupe:#6B6258; --green:#3FA66B; }
+ *{box-sizing:border-box;}
+ html,body{margin:0;padding:0;background:#ddd;font-family:'Inter',sans-serif;color:var(--ink);}
+ body{display:flex; align-items:flex-start; justify-content:center; min-height:100vh; padding:20px 0;}
+ .phone{
+ width:390px; min-height:780px; background:var(--bg); border-radius:36px; overflow:hidden;
+ box-shadow:0 24px 60px rgba(0,0,0,.28); display:flex; flex-direction:column; position:relative;
+ }
+ .screen{ flex:1; display:flex; flex-direction:column; padding:36px 26px 30px; overflow-y:auto; }
+ .question{
+ font-family:'Fraunces',serif; font-weight:600; font-size:34px; line-height:1.18;
+ text-align:center; margin:6px 0 30px; color:var(--ink);
+ }
+ .answers{ flex:1; display:flex; flex-direction:column; gap:16px; }
+ button{ font-family:'Inter',sans-serif; cursor:pointer; }
+ .bigbtn{
+ width:100%; min-height:64px; border-radius:18px; font-size:19px; font-weight:700;
+ background:#fff; border:2px solid #e7e1d6; color:var(--ink); padding:12px 18px;
+ }
+ .bigbtn-primary{ background:var(--gold); border:2px solid var(--gold); color:#3a2a06; }
+ .bigbtn-secondary{ background:#fff; border:2px solid var(--taupe); color:var(--taupe); }
+ .bigbtn:focus-visible, .linktext:focus-visible{ outline:3px solid #2B6CB0; outline-offset:2px; }
+ .linktext{
+ background:none; border:none; color:var(--taupe); font-size:17px; text-decoration:underline;
+ text-align:center; min-height:44px; width:100%;
+ }
+ .brandlist{ display:flex; flex-direction:column; gap:12px; overflow-y:auto; max-height:520px; }
+ .swatch{ width:100%; height:170px; border-radius:18px; margin-bottom:8px; }
+ .swatch.small{ height:120px; }
+ .sw1{background:repeating-linear-gradient(45deg,#8FAE8B 0 18px,#7c9c78 18px 36px);}
+ .sw2{background:repeating-linear-gradient(-45deg,#7C93B0 0 16px,#617a99 16px 32px);}
+ .sw3{background:repeating-linear-gradient(90deg,#D9A441 0 14px,#c98f2c 14px 28px);}
+ .itemname{ font-size:20px; font-weight:800; margin:0; text-align:center; }
+ .itemmeta{ font-size:16px; color:#666; margin:4px 0 0; text-align:center; }
+ .camwrap{ flex:1; display:flex; flex-direction:column; padding:0; margin:-36px -26px -30px; }
+ .camview{
+ flex:1; background:#1c1c1e; position:relative; display:flex; align-items:center; justify-content:center;
+ color:#fff; min-height:520px;
+ }
+ .camview .hint{
+ position:absolute; top:40px; left:20px; right:20px; text-align:center;
+ font-family:'Fraunces',serif; font-size:28px; font-weight:600; color:#fdfbf7;
+ }
+ .camview .status{ font-size:19px; font-weight:700; color:#f0e8d8; }
+ .shutter{
+ position:absolute; bottom:34px; left:50%; transform:translateX(-50%);
+ width:90px; height:90px; border-radius:50%; background:var(--gold); border:6px solid #fff;
+ font-size:34px; display:flex; align-items:center; justify-content:center;
+ }
+ .center{ display:flex; flex-direction:column; align-items:center; justify-content:center; flex:1; gap:10px; }
+ .homefab{
+ position:absolute; top:14px; left:14px; z-index:20; min-width:56px; min-height:56px;
+ border-radius:16px; background:rgba(255,255,255,.94); border:2px solid #e7e1d6; color:var(--ink);
+ font-size:15px; font-weight:700; display:none; align-items:center; justify-content:center; gap:4px;
+ padding:0 12px; box-shadow:0 4px 10px rgba(0,0,0,.12);
+ }
+ .homefab:focus-visible{ outline:3px solid #2B6CB0; outline-offset:2px; }
+</style>
+<style id="ux-baseline">/* TK-12343 UX baseline: full-bleed on real phones, visible focus everywhere */
+@media (max-width:405px){ html,body{margin:0;padding:0} body{overflow-x:hidden} .stage,.phone{width:100% !important;max-width:100%;margin:0 !important;border:0 !important;border-radius:0 !important;box-shadow:none !important;height:100dvh;min-height:640px} }
+button:focus-visible,a:focus-visible,[tabindex]:focus-visible,input:focus-visible{outline:3px solid #9C7A33 !important;outline-offset:2px}
+</style>
+</head>
+<body>
+<div class="phone">
+ <button class="homefab" id="homeFab" aria-label="Home" onclick="go('home')">⌂ Home</button>
+ <div class="screen" id="screen" role="main"></div>
+</div>
+
+<script>
+const MOCK_ITEMS = [
+ {name:'Palm Damask', color:'Sage', code:'DW-10423', sw:'sw1'},
+ {name:'Trellis Weave', color:'Indigo', code:'DW-88214', sw:'sw2'},
+ {name:'Linen Bloom', color:'Blush', code:'DW-55071', sw:'sw3'},
+];
+const BRANDS = ['York Wallcoverings','Brewster Home','Kravet','Cole & Son','Thibaut','Ronald Redding'];
+
+const state = { screen:'home', job:null, item:null, brand:null, resultIndex:0 };
+
+function go(screen){ state.screen = screen; render(); }
+function startJob(job){
+ state.job = job; state.item=null; state.brand=null; state.resultIndex=0;
+ const map = {new:'new-cam', update:'update-cam', find:'find-method', sample:'sample-cam'};
+ go(map[job]);
+}
+function randomItem(){ return MOCK_ITEMS[Math.floor(Math.random()*MOCK_ITEMS.length)]; }
+
+function cameraScreen(hint, onShoot){
+ return `<div class="camwrap"><div class="camview">
+ <div class="hint">${hint}</div>
+ <div class="status" id="camStatus">Tap the shutter</div>
+ <button class="shutter" aria-label="Take photo" onclick="${onShoot}">📸</button>
+ </div></div>`;
+}
+function shoot(cb){
+ const st = document.getElementById('camStatus');
+ const btn = document.querySelector('.shutter');
+ if(btn) btn.disabled = true;
+ if(st) st.textContent = 'Snapping…';
+ const from = state.screen; setTimeout(()=>{ if (state.screen !== from) return; state.item = randomItem(); cb(); }, 700);
+}
+
+function render(){
+ const s = document.getElementById('screen');
+ const fab = document.getElementById('homeFab');
+ fab.style.display = state.screen==='home' ? 'none' : 'flex';
+ let html = '';
+
+ if(state.screen==='home'){
+ html += `<h1 class="question">What do you need to do?</h1>
+ <div class="answers">
+ <button class="bigbtn" onclick="startJob('new')">Add a New Item</button>
+ <button class="bigbtn" onclick="startJob('update')">Update an Item</button>
+ <button class="bigbtn" onclick="startJob('find')">Find an Item</button>
+ <button class="bigbtn" onclick="startJob('sample')">A Sample Came In</button>
+ </div>`;
+ }
+
+ else if(state.screen==='new-cam'){
+ html += cameraScreen('Take a photo of it.', "shoot(()=>go('new-brand'))");
+ }
+ else if(state.screen==='new-brand'){
+ html += `<h1 class="question">Which brand is it?</h1><div class="brandlist">`;
+ BRANDS.forEach(b=>{ html += `<button class="bigbtn" onclick="state.brand='${b}'; go('new-save')">${b}</button>`; });
+ html += `</div>`;
+ }
+ else if(state.screen==='new-save'){
+ html += `<h1 class="question">Save it?</h1>
+ <div class="swatch small ${state.item.sw}"></div>
+ <p class="itemmeta">${state.brand} · code ${state.item.code}</p>
+ <div class="answers" style="margin-top:20px;">
+ <button class="bigbtn bigbtn-primary" onclick="go('new-done')">Save as Draft</button>
+ <button class="linktext" onclick="go('new-cam')">Retake Photo</button>
+ </div>`;
+ }
+ else if(state.screen==='new-done'){
+ html += `<h1 class="question">Saved! 🎉</h1>
+ <div class="answers">
+ <button class="bigbtn bigbtn-primary" onclick="startJob('new')">Add Another</button>
+ <button class="linktext" onclick="go('home')">Home</button>
+ </div>`;
+ }
+
+ else if(state.screen==='update-cam'){
+ html += cameraScreen('Scan the label or pattern.', "shoot(()=>go('update-match'))");
+ }
+ else if(state.screen==='update-match'){
+ html += `<h1 class="question">Is this it?</h1>
+ <div class="swatch ${state.item.sw}"></div>
+ <p class="itemname">${state.item.name} — ${state.item.color}</p>
+ <p class="itemmeta">code ${state.item.code}</p>
+ <div class="answers" style="margin-top:20px;">
+ <button class="bigbtn bigbtn-primary" onclick="go('update-cam2')">Yes</button>
+ <button class="bigbtn bigbtn-secondary" onclick="go('update-cam')">No, Let Me Search</button>
+ </div>`;
+ }
+ else if(state.screen==='update-cam2'){
+ html += cameraScreen('Take the new photo.', "shoot(()=>go('update-done'))");
+ }
+ else if(state.screen==='update-done'){
+ html += `<div class="center"><h1 class="question">Updated! ✅</h1></div>
+ <button class="bigbtn bigbtn-primary" onclick="go('home')">Back Home</button>`;
+ }
+
+ else if(state.screen==='find-method'){
+ html += `<h1 class="question">How do you want to find it?</h1>
+ <div class="answers">
+ <button class="bigbtn" onclick="go('find-result')">Scan</button>
+ <button class="bigbtn" onclick="go('find-result')">Talk</button>
+ <button class="bigbtn" onclick="go('find-result')">Type</button>
+ </div>`;
+ }
+ else if(state.screen==='find-result'){
+ const it = MOCK_ITEMS[state.resultIndex];
+ html += `<h1 class="question">Which one is it?</h1>
+ <div class="swatch ${it.sw}"></div>
+ <p class="itemname">${it.name} — ${it.color}</p>
+ <p class="itemmeta">code ${it.code}</p>
+ <div class="answers" style="margin-top:20px;">
+ <button class="bigbtn bigbtn-primary" onclick="pickResult()">Yes, This One</button>
+ <button class="bigbtn bigbtn-secondary" onclick="nextResult()">Next Result</button>
+ </div>`;
+ }
+ else if(state.screen==='find-exhausted'){
+ html += `<h1 class="question">That's all — try another way?</h1>
+ <div class="answers">
+ <button class="bigbtn" onclick="state.resultIndex=0; go('find-result')">Scan</button>
+ <button class="bigbtn" onclick="state.resultIndex=0; go('find-result')">Talk</button>
+ <button class="bigbtn" onclick="state.resultIndex=0; go('find-result')">Type</button>
+ </div>`;
+ }
+ else if(state.screen==='find-detail'){
+ html += `<div class="center">
+ <h1 class="question" style="font-size:26px; margin-bottom:14px;">${state.item.name}</h1>
+ <div class="swatch ${state.item.sw}" style="width:260px;"></div>
+ <p class="itemmeta">${state.item.color} · code ${state.item.code}</p>
+ </div>
+ <div class="answers">
+ <button class="bigbtn bigbtn-primary" onclick="go('update-cam2')">Update This</button>
+ <button class="bigbtn bigbtn-secondary" onclick="go('find-method')">Search Again</button>
+ <button class="linktext" onclick="go('home')">Home</button>
+ </div>`;
+ }
+
+ else if(state.screen==='sample-cam'){
+ html += cameraScreen('Scan the box label.', "shoot(()=>go('sample-match'))");
+ }
+ else if(state.screen==='sample-match'){
+ html += `<h1 class="question">Mark it received?</h1>
+ <div class="swatch ${state.item.sw}"></div>
+ <p class="itemname">${state.item.name} — ${state.item.color}</p>
+ <p class="itemmeta">code ${state.item.code}</p>
+ <div class="answers" style="margin-top:20px;">
+ <button class="bigbtn bigbtn-primary" onclick="go('sample-printing')">Yes, Mark Received</button>
+ </div>`;
+ }
+ else if(state.screen==='sample-printing'){
+ html += `<div class="center"><h1 class="question">Printing your sticker…</h1><p class="itemmeta">🖨️ One sec…</p></div>`;
+ setTimeout(()=>{ if(state.screen==='sample-printing') go('sample-done'); }, 1100);
+ }
+ else if(state.screen==='sample-done'){
+ html += `<div class="center"><h1 class="question">Done! ✅</h1></div>
+ <button class="bigbtn bigbtn-primary" onclick="go('home')">Back Home</button>`;
+ }
+
+ s.innerHTML = html;
+}
+function pickResult(){ state.item = MOCK_ITEMS[state.resultIndex]; go('find-detail'); }
+function nextResult(){
+ if(state.resultIndex < MOCK_ITEMS.length-1){ state.resultIndex++; go('find-result'); }
+ else { go('find-exhausted'); }
+}
+
+render();
+</script>
+</body>
+</html>
diff --git a/public/vendor-photos.html b/public/vendor-photos.html
index 7b79687..3cf5fb9 100644
--- a/public/vendor-photos.html
+++ b/public/vendor-photos.html
@@ -25,7 +25,7 @@
</style></head>
<body>
<header>
- <h1>Vendor Photos <span id="count" style="color:var(--muted);font-weight:400;font-size:14px"></span><a href="/">← capture</a></h1>
+ <h1>Vendor Photos <span id="count" style="color:var(--muted);font-weight:400;font-size:14px"></span><a href="/app">← capture</a></h1>
<div class="controls">
<input type="search" id="q" placeholder="Search SKU or vendor">
<select id="sort">
diff --git a/server.js b/server.js
index eff859b..f09f0e8 100644
--- a/server.js
+++ b/server.js
@@ -1846,10 +1846,28 @@ const appHandler = (req, res) => {
<h1>DW SKU Photos — Self-Check</h1>
<div class="big">${ok ? '✅ All systems go' : '❌ Issues found'} · ${buildLabel()}</div>
<table>${rows}</table>
- <p class="d">${new Date().toLocaleString()} · <a href="/selfcheck">re-run</a> · <a href="/">← app</a></p>`);
+ <p class="d">${new Date().toLocaleString()} · <a href="/selfcheck">re-run</a> · <a href="/app">← app</a></p>`);
+ }
+
+ // Front screen (TK-12343, Steve 2026-09-26): one-at-a-time swipe theme picker. The capture
+ // app itself moved to /app (and /index.html); the picker's "Go to the app" button leads there.
+ if (u.pathname === '/') {
+ const f = path.join(ROOT, 'public/pick.html');
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store, must-revalidate' });
+ return res.end(fs.readFileSync(f));
+ }
+ // Figma theme mockups v1–v25 for the picker. Bounded: fixed dir, basename-only, .html only.
+ if (_p.startsWith('/themes/')) {
+ const name = _p.slice('/themes/'.length);
+ if (name.includes('/') || name.includes('..') || !/^v[0-9]+-[a-z0-9-]+\.html$/.test(name)) return send(res, 404, { err: 'not found' });
+ const TDIR = path.join(ROOT, 'public/themes');
+ const fp = path.join(TDIR, name);
+ if (!fp.startsWith(TDIR + path.sep) || !fs.existsSync(fp)) return send(res, 404, { err: 'not found' });
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
+ return res.end(fs.readFileSync(fp));
}
- if (u.pathname === '/' || u.pathname === '/index.html') {
+ if (u.pathname === '/app' || u.pathname === '/index.html') {
// no-store: the UI evolves fast and is read fresh per request — never let a
// browser serve a stale cached page (that hid the photo editor after a deploy).
let html = fs.readFileSync(path.join(ROOT, 'public/index.html'), 'utf8').replace(/__VER__/g, buildLabel());
← 6e2c3f2 Onload chooser: Adobe Spectrum themes (Light/Dark/Darkest/Au
·
back to Dw Photo Capture
·
Swipe picker: one trackpad swipe/wheel flick = one theme (Sa 28a1391 →