← back to Dw Photo Capture
Add /kid-simple/ review page: Kid-Simple mockups v1-v16 + Photoshop board, behind basic-auth (TK-12344)
4ace5d45c2e66843caa898f552453912fb384840 · 2026-09-26 10:43:25 -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
A public/kid-simple/index.htmlA public/kid-simple/renders/kid-simple-v7-v16-board.jpgA public/kid-simple/renders/v10-todays-inbox.pngA public/kid-simple/renders/v11-bottom-sheet.pngA public/kid-simple/renders/v12-hold-to-talk.pngA public/kid-simple/renders/v13-sticker-book.pngA public/kid-simple/renders/v14-ticket-printer.pngA public/kid-simple/renders/v15-thumb-zone.pngA public/kid-simple/renders/v16-widget-stack.pngA public/kid-simple/renders/v7-scan-and-go.pngA public/kid-simple/renders/v8-mode-dial.pngA public/kid-simple/renders/v9-story-mode.pngA public/kid-simple/v1-magic-camera.htmlA public/kid-simple/v10-todays-inbox.htmlA public/kid-simple/v11-bottom-sheet.htmlA public/kid-simple/v12-hold-to-talk.htmlA public/kid-simple/v13-sticker-book.htmlA public/kid-simple/v14-ticket-printer.htmlA public/kid-simple/v15-thumb-zone.htmlA public/kid-simple/v16-widget-stack.htmlA public/kid-simple/v2-four-tiles.htmlA public/kid-simple/v3-text-buddy.htmlA public/kid-simple/v4-level-up.htmlA public/kid-simple/v5-swipe-deck.htmlA public/kid-simple/v6-one-thing.htmlA public/kid-simple/v7-scan-and-go.htmlA public/kid-simple/v8-mode-dial.htmlA public/kid-simple/v9-story-mode.htmlM server.js
Diff
commit 4ace5d45c2e66843caa898f552453912fb384840
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Sep 26 10:43:25 2026 -0700
Add /kid-simple/ review page: Kid-Simple mockups v1-v16 + Photoshop board, behind basic-auth (TK-12344)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SC2DKkJkSNenmFz2Eqnvpk
---
public/kid-simple/index.html | 187 +++++++
.../kid-simple/renders/kid-simple-v7-v16-board.jpg | Bin 0 -> 1501686 bytes
public/kid-simple/renders/v10-todays-inbox.png | Bin 0 -> 115608 bytes
public/kid-simple/renders/v11-bottom-sheet.png | Bin 0 -> 301466 bytes
public/kid-simple/renders/v12-hold-to-talk.png | Bin 0 -> 555623 bytes
public/kid-simple/renders/v13-sticker-book.png | Bin 0 -> 201594 bytes
public/kid-simple/renders/v14-ticket-printer.png | Bin 0 -> 93527 bytes
public/kid-simple/renders/v15-thumb-zone.png | Bin 0 -> 198786 bytes
public/kid-simple/renders/v16-widget-stack.png | Bin 0 -> 106260 bytes
public/kid-simple/renders/v7-scan-and-go.png | Bin 0 -> 307129 bytes
public/kid-simple/renders/v8-mode-dial.png | Bin 0 -> 274678 bytes
public/kid-simple/renders/v9-story-mode.png | Bin 0 -> 107408 bytes
public/kid-simple/v1-magic-camera.html | 550 +++++++++++++++++++
public/kid-simple/v10-todays-inbox.html | 396 ++++++++++++++
public/kid-simple/v11-bottom-sheet.html | 397 ++++++++++++++
public/kid-simple/v12-hold-to-talk.html | 411 ++++++++++++++
public/kid-simple/v13-sticker-book.html | 409 ++++++++++++++
public/kid-simple/v14-ticket-printer.html | 344 ++++++++++++
public/kid-simple/v15-thumb-zone.html | 493 +++++++++++++++++
public/kid-simple/v16-widget-stack.html | 364 +++++++++++++
public/kid-simple/v2-four-tiles.html | 588 +++++++++++++++++++++
public/kid-simple/v3-text-buddy.html | 551 +++++++++++++++++++
public/kid-simple/v4-level-up.html | 444 ++++++++++++++++
public/kid-simple/v5-swipe-deck.html | 490 +++++++++++++++++
public/kid-simple/v6-one-thing.html | 460 ++++++++++++++++
public/kid-simple/v7-scan-and-go.html | 324 ++++++++++++
public/kid-simple/v8-mode-dial.html | 354 +++++++++++++
public/kid-simple/v9-story-mode.html | 386 ++++++++++++++
server.js | 18 +
29 files changed, 7166 insertions(+)
diff --git a/public/kid-simple/index.html b/public/kid-simple/index.html
new file mode 100644
index 0000000..b51a76c
--- /dev/null
+++ b/public/kid-simple/index.html
@@ -0,0 +1,187 @@
+<title>Photo Capture, Kid-Simple</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Newsreader:opsz,wght@6..72,500;6..72,600&family=Public+Sans:wght@400;600;700&display=swap">
+<style>
+:root{
+ --ground:#EDEFEC; --panel:#FFFFFF; --ink:#1E2320; --muted:#5E6A64;
+ --line:#D5DAD5; --gold:#9C7A33; --gold-soft:#F1E7D2; --sage:#4E7A64;
+ --good:#2F7D55; --warn:#A8651C; --bad:#A83A2E;
+ color-scheme:light;
+}
+@media (prefers-color-scheme: dark){
+ :root:not([data-theme="light"]){
+ --ground:#161A18; --panel:#1F2522; --ink:#E9ECE9; --muted:#9AA6A0;
+ --line:#333B37; --gold:#D6B26A; --gold-soft:#3A3120; --sage:#8DB8A2;
+ --good:#6CC495; --warn:#E0A35A; --bad:#E37E71; color-scheme:dark;
+ }
+}
+:root[data-theme="dark"]{
+ --ground:#161A18; --panel:#1F2522; --ink:#E9ECE9; --muted:#9AA6A0;
+ --line:#333B37; --gold:#D6B26A; --gold-soft:#3A3120; --sage:#8DB8A2;
+ --good:#6CC495; --warn:#E0A35A; --bad:#E37E71; color-scheme:dark;
+}
+body{background:var(--ground);color:var(--ink);font:16px/1.55 "Public Sans",system-ui,sans-serif;padding-inline:16px;padding-block:32px 64px}
+.wrap{max-width:1320px;margin:0 auto;display:flex;flex-direction:column;gap:32px}
+header{display:flex;flex-direction:column;gap:10px;max-width:68ch}
+.eyebrow{font-size:12px;letter-spacing:.12em;text-transform:uppercase;color:var(--gold);font-weight:700}
+h1{font:600 clamp(30px,5vw,46px)/1.1 "Newsreader",Georgia,serif;margin:0;text-wrap:balance}
+h2{font:600 24px/1.2 "Newsreader",Georgia,serif;margin:0;text-wrap:balance}
+p{margin:0}
+.lede{color:var(--muted)}
+.jobs{display:flex;flex-wrap:wrap;gap:8px}
+.chip{border:1px solid var(--line);background:var(--panel);border-radius:999px;padding:4px 12px;font-size:14px}
+.verdict{background:var(--panel);border:1px solid var(--line);border-left:4px solid var(--gold);padding:18px 20px;display:flex;flex-direction:column;gap:6px;max-width:80ch}
+.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,400px),1fr));gap:40px 28px}
+.card{display:flex;flex-direction:column;gap:12px}
+.head{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
+.tag{font-size:13px;color:var(--muted);font-variant-numeric:tabular-nums}
+.score{font:700 14px/1 "Public Sans",sans-serif;padding:6px 10px;border-radius:999px;background:var(--gold-soft);color:var(--ink);white-space:nowrap}
+.score.top{background:var(--good);color:var(--panel)}
+.phone{width:100%;max-width:398px;aspect-ratio:390/780;border:4px solid var(--ink);border-radius:34px;overflow:hidden;background:#000}
+.phone iframe{width:100%;height:100%;border:0;display:block}
+.idea{color:var(--muted)}
+dl{display:grid;grid-template-columns:auto 1fr;gap:4px 12px;margin:0;font-size:14px}
+dt{color:var(--muted)}
+dd{margin:0}
+.taps{font-variant-numeric:tabular-nums}
+.flaw{color:var(--bad)}
+a{color:var(--sage);font-weight:600}
+a:focus-visible{outline:2px solid var(--gold);outline-offset:3px}
+footer{color:var(--muted);font-size:14px;max-width:80ch}
+</style>
+
+<div class="wrap">
+ <header>
+ <div class="eyebrow">photo.designerwallcoverings.com · redesign study</div>
+ <h1>Sixteen ways to make photo capture simple enough for a 13-year-old</h1>
+ <p class="lede">Today's app puts about 40 controls on one screen. Each version below keeps only the four real jobs, uses words a teenager knows, and makes the camera the hero. Every phone is a live, clickable mockup with pretend data — tap through it.</p>
+ <div class="jobs"><span class="chip">Add New Item</span><span class="chip">Update an Item</span><span class="chip">Find an Item</span><span class="chip">A Sample Came In</span></div>
+ </header>
+
+ <section class="verdict">
+ <h2>Round 2 pick: v4 Level Up</h2>
+ <p>After a second UX pass (Home + Back everywhere, a "good photo?" check after every shot, Undo after saving, a "no match" path, bigger text and targets), a separate reviewer re-tested all six. v4 came out cleanest: its Back/Home bar is always on screen, so no screen can strand you. Every defect the reviewer found in round 2 has been fixed in the mockups below. Tip: each one has a "Demo: show 'no match'" switch so you can see the failure path.</p>
+ </section>
+
+ <div class="grid">
+ <article class="card">
+ <div class="head"><h2>v4 · Level Up</h2><span class="score top">#1</span></div>
+ <div class="phone"><iframe src="v4-level-up.html" title="v4 Level Up mockup" loading="lazy"></iframe></div>
+ <p class="idea">Each job is a short game path — 2 or 3 levels, progress dots, confetti at the end.</p>
+ <dl><dt>Round 2 fixed</dt><dd>A timer could yank you back to "done" after you tapped Back; Undo after Update landed on the wrong screen</dd><dt>Watch for</dt><dd class="flaw">Confetti and levels may feel like a toy to older staff</dd></dl>
+ <a href="v4-level-up.html">Open full screen</a>
+ </article>
+
+ <article class="card">
+ <div class="head"><h2>v3 · Text Buddy</h2><span class="score">#2</span></div>
+ <div class="phone"><iframe src="v3-text-buddy.html" title="v3 Text Buddy mockup" loading="lazy"></iframe></div>
+ <p class="idea">The whole app is a text chat with "Sam," who asks one thing at a time with tap-to-reply chips.</p>
+ <dl><dt>Round 2 fixed</dt><dd>"Undo last" could replay a stale photo; some captions were under 17px</dd><dt>Watch for</dt><dd class="flaw">The undo-by-replay design is the most complex to build for real</dd></dl>
+ <a href="v3-text-buddy.html">Open full screen</a>
+ </article>
+
+ <article class="card">
+ <div class="head"><h2>v6 · One Thing At A Time</h2><span class="score">#3</span></div>
+ <div class="phone"><iframe src="v6-one-thing.html" title="v6 One Thing At A Time mockup" loading="lazy"></iframe></div>
+ <p class="idea">No menus at all: one big question and one to three full-width buttons per screen.</p>
+ <dl><dt>Round 2 fixed</dt><dd>Wrong job name, three different "again" buttons (now all "Do Another"), low-contrast secondary buttons</dd><dt>Watch for</dt><dd class="flaw">Most screens per job — slowest for a practiced user</dd></dl>
+ <a href="v6-one-thing.html">Open full screen</a>
+ </article>
+
+ <article class="card">
+ <div class="head"><h2>v1 · Magic Camera</h2><span class="score">#4</span></div>
+ <div class="phone"><iframe src="v1-magic-camera.html" title="v1 Magic Camera mockup" loading="lazy"></iframe></div>
+ <p class="idea">One giant shutter button first; after the shot it asks "What's this photo for?" with the four jobs.</p>
+ <dl><dt>Round 2 fixed</dt><dd>Undo after Save wiped the brand you picked; loading screens had no way out</dd><dt>Watch for</dt><dd class="flaw">Photo-first means Find by code still starts with the camera</dd></dl>
+ <a href="v1-magic-camera.html">Open full screen</a>
+ </article>
+
+ <article class="card">
+ <div class="head"><h2>v2 · Four Big Tiles</h2><span class="score">#5</span></div>
+ <div class="phone"><iframe src="v2-four-tiles.html" title="v2 Four Big Tiles mockup" loading="lazy"></iframe></div>
+ <p class="idea">A home screen of four big coloured tiles, one per job, like phone app icons.</p>
+ <dl><dt>Round 2 fixed</dt><dd>"Sample Arrived" renamed; grey subtitles failed contrast; three demo switches disagreed with each other; same Undo bug as v1</dd><dt>Watch for</dt><dd class="flaw">Plain — nothing guides you once you're inside a job</dd></dl>
+ <a href="v2-four-tiles.html">Open full screen</a>
+ </article>
+
+ <article class="card">
+ <div class="head"><h2>v5 · Swipe Deck</h2><span class="score">#6</span></div>
+ <div class="phone"><iframe src="v5-swipe-deck.html" title="v5 Swipe Deck mockup" loading="lazy"></iframe></div>
+ <p class="idea">Every step is a card: swipe right or tap ✓ for yes, swipe left or tap ✗ to redo.</p>
+ <dl><dt>Round 2 fixed</dt><dd>Crashed on "switch tab, then Back"; swipe hint was wrong on the Find screen</dd><dt>Watch for</dt><dd class="flaw">Swiping is hard one-handed while holding a sample</dd></dl>
+ <a href="v5-swipe-deck.html">Open full screen</a>
+ </article>
+ </div>
+
+
+ <section class="verdict" id="round3">
+ <h2>Round 3: ten more directions, v7–v16</h2>
+ <p>Built in parallel this morning (TK-12344), reviewed by a separate critic, fixed, then laid out as one Photoshop board. Not ranked yet.</p>
+ <p><a href="renders/kid-simple-v7-v16-board.jpg">Open the Photoshop board (JPG)</a></p>
+ </section>
+ <a href="renders/kid-simple-v7-v16-board.jpg"><img src="renders/kid-simple-v7-v16-board.jpg" alt="Photoshop board showing Kid-Simple versions 7 through 16 side by side" style="width:100%;height:auto;border:1px solid var(--line);border-radius:8px"></a>
+ <div class="grid">
+ <article class="card">
+ <div class="head"><h2>v7 · Scan & Go</h2><span class="tag">round 3</span></div>
+ <div class="phone"><iframe src="v7-scan-and-go.html" title="v7 Scan & Go mockup" loading="lazy"></iframe></div>
+ <p class="idea">A scanner viewfinder home: point at a tag or box; a match goes to Update or Sample, no match goes to Add New Item.</p>
+ <a href="v7-scan-and-go.html">Open full screen</a> · <a href="renders/v7-scan-and-go.png">Photoshop render</a>
+ </article>
+ <article class="card">
+ <div class="head"><h2>v8 · Mode Dial</h2><span class="tag">round 3</span></div>
+ <div class="phone"><iframe src="v8-mode-dial.html" title="v8 Mode Dial mockup" loading="lazy"></iframe></div>
+ <p class="idea">Camera home with a swipeable NEW / UPDATE / FIND / SAMPLE strip; the shutter's colour and label change with the mode.</p>
+ <a href="v8-mode-dial.html">Open full screen</a> · <a href="renders/v8-mode-dial.png">Photoshop render</a>
+ </article>
+ <article class="card">
+ <div class="head"><h2>v9 · Story Mode</h2><span class="tag">round 3</span></div>
+ <div class="phone"><iframe src="v9-story-mode.html" title="v9 Story Mode mockup" loading="lazy"></iframe></div>
+ <p class="idea">Instagram-style stories: pick a job ring, then tap right to continue and left to go back.</p>
+ <a href="v9-story-mode.html">Open full screen</a> · <a href="renders/v9-story-mode.png">Photoshop render</a>
+ </article>
+ <article class="card">
+ <div class="head"><h2>v10 · Today's Inbox</h2><span class="tag">round 3</span></div>
+ <div class="phone"><iframe src="v10-todays-inbox.html" title="v10 Today's Inbox mockup" loading="lazy"></iframe></div>
+ <p class="idea">Home is a to-do list of what's waiting (samples in, photos needed), plus a camera button for Add New Item.</p>
+ <a href="v10-todays-inbox.html">Open full screen</a> · <a href="renders/v10-todays-inbox.png">Photoshop render</a>
+ </article>
+ <article class="card">
+ <div class="head"><h2>v11 · Bottom Sheet</h2><span class="tag">round 3</span></div>
+ <div class="phone"><iframe src="v11-bottom-sheet.html" title="v11 Bottom Sheet mockup" loading="lazy"></iframe></div>
+ <p class="idea">Full-screen camera with a Maps-style pull-up sheet holding the four jobs.</p>
+ <a href="v11-bottom-sheet.html">Open full screen</a> · <a href="renders/v11-bottom-sheet.png">Photoshop render</a>
+ </article>
+ <article class="card">
+ <div class="head"><h2>v12 · Hold to Talk</h2><span class="tag">round 3</span></div>
+ <div class="phone"><iframe src="v12-hold-to-talk.html" title="v12 Hold to Talk mockup" loading="lazy"></iframe></div>
+ <p class="idea">Press and hold the mic and say what you're doing; big tap buttons are the fallback.</p>
+ <a href="v12-hold-to-talk.html">Open full screen</a> · <a href="renders/v12-hold-to-talk.png">Photoshop render</a>
+ </article>
+ <article class="card">
+ <div class="head"><h2>v13 · Sticker Book</h2><span class="tag">round 3</span></div>
+ <div class="phone"><iframe src="v13-sticker-book.html" title="v13 Sticker Book mockup" loading="lazy"></iframe></div>
+ <p class="idea">Scrapbook look: take a photo, then drag the polaroid onto one of four job stickers (or just tap a sticker).</p>
+ <a href="v13-sticker-book.html">Open full screen</a> · <a href="renders/v13-sticker-book.png">Photoshop render</a>
+ </article>
+ <article class="card">
+ <div class="head"><h2>v14 · Ticket Printer</h2><span class="tag">round 3</span></div>
+ <div class="phone"><iframe src="v14-ticket-printer.html" title="v14 Ticket Printer mockup" loading="lazy"></iframe></div>
+ <p class="idea">Four arcade keys; each finished job prints a little thermal-paper ticket.</p>
+ <a href="v14-ticket-printer.html">Open full screen</a> · <a href="renders/v14-ticket-printer.png">Photoshop render</a>
+ </article>
+ <article class="card">
+ <div class="head"><h2>v15 · Thumb Zone</h2><span class="tag">round 3</span></div>
+ <div class="phone"><iframe src="v15-thumb-zone.html" title="v15 Thumb Zone mockup" loading="lazy"></iframe></div>
+ <p class="idea">Shoot first; every control lives in the bottom third where your thumb reaches; hold-to-confirm rings.</p>
+ <a href="v15-thumb-zone.html">Open full screen</a> · <a href="renders/v15-thumb-zone.png">Photoshop render</a>
+ </article>
+ <article class="card">
+ <div class="head"><h2>v16 · Widget Stack</h2><span class="tag">round 3</span></div>
+ <div class="phone"><iframe src="v16-widget-stack.html" title="v16 Widget Stack mockup" loading="lazy"></iframe></div>
+ <p class="idea">iPhone-style widgets (Samples Waiting, Need Updates, Last Added, Find) that open in place and count down as you work.</p>
+ <a href="v16-widget-stack.html">Open full screen</a> · <a href="renders/v16-widget-stack.png">Photoshop render</a>
+ </article>
+ </div>
+
+ <footer>Mockups only — nothing here talks to the real catalog, camera, or FileMaker. Ranking is the independent reviewer's round-2 order for a 13-year-old.</footer>
+</div>
diff --git a/public/kid-simple/renders/kid-simple-v7-v16-board.jpg b/public/kid-simple/renders/kid-simple-v7-v16-board.jpg
new file mode 100644
index 0000000..6687aea
Binary files /dev/null and b/public/kid-simple/renders/kid-simple-v7-v16-board.jpg differ
diff --git a/public/kid-simple/renders/v10-todays-inbox.png b/public/kid-simple/renders/v10-todays-inbox.png
new file mode 100644
index 0000000..e1e4741
Binary files /dev/null and b/public/kid-simple/renders/v10-todays-inbox.png differ
diff --git a/public/kid-simple/renders/v11-bottom-sheet.png b/public/kid-simple/renders/v11-bottom-sheet.png
new file mode 100644
index 0000000..1899ecb
Binary files /dev/null and b/public/kid-simple/renders/v11-bottom-sheet.png differ
diff --git a/public/kid-simple/renders/v12-hold-to-talk.png b/public/kid-simple/renders/v12-hold-to-talk.png
new file mode 100644
index 0000000..f9b1aeb
Binary files /dev/null and b/public/kid-simple/renders/v12-hold-to-talk.png differ
diff --git a/public/kid-simple/renders/v13-sticker-book.png b/public/kid-simple/renders/v13-sticker-book.png
new file mode 100644
index 0000000..f948227
Binary files /dev/null and b/public/kid-simple/renders/v13-sticker-book.png differ
diff --git a/public/kid-simple/renders/v14-ticket-printer.png b/public/kid-simple/renders/v14-ticket-printer.png
new file mode 100644
index 0000000..b4b12cc
Binary files /dev/null and b/public/kid-simple/renders/v14-ticket-printer.png differ
diff --git a/public/kid-simple/renders/v15-thumb-zone.png b/public/kid-simple/renders/v15-thumb-zone.png
new file mode 100644
index 0000000..68e158c
Binary files /dev/null and b/public/kid-simple/renders/v15-thumb-zone.png differ
diff --git a/public/kid-simple/renders/v16-widget-stack.png b/public/kid-simple/renders/v16-widget-stack.png
new file mode 100644
index 0000000..6f0987c
Binary files /dev/null and b/public/kid-simple/renders/v16-widget-stack.png differ
diff --git a/public/kid-simple/renders/v7-scan-and-go.png b/public/kid-simple/renders/v7-scan-and-go.png
new file mode 100644
index 0000000..62747c1
Binary files /dev/null and b/public/kid-simple/renders/v7-scan-and-go.png differ
diff --git a/public/kid-simple/renders/v8-mode-dial.png b/public/kid-simple/renders/v8-mode-dial.png
new file mode 100644
index 0000000..93654dd
Binary files /dev/null and b/public/kid-simple/renders/v8-mode-dial.png differ
diff --git a/public/kid-simple/renders/v9-story-mode.png b/public/kid-simple/renders/v9-story-mode.png
new file mode 100644
index 0000000..09d0bbb
Binary files /dev/null and b/public/kid-simple/renders/v9-story-mode.png differ
diff --git a/public/kid-simple/v1-magic-camera.html b/public/kid-simple/v1-magic-camera.html
new file mode 100644
index 0000000..4eb4317
--- /dev/null
+++ b/public/kid-simple/v1-magic-camera.html
@@ -0,0 +1,550 @@
+<!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;
+ }
+ .demo-toggle{
+ position:absolute;top:86px;left:0;right:0;text-align:center;z-index:5;
+ font-size:14px;color:var(--cream);opacity:.7;display:flex;align-items:center;justify-content:center;gap:8px;
+ }
+ .demo-toggle input{width:18px;height:18px;}
+ .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:active, .btn:active, .brand-tile:active, .nav-btn:active, .chip:active{transform:scale(.97);}
+ .shutter:focus-visible, button:focus-visible, .card-btn:focus-visible, .brand-tile:focus-visible, input:focus-visible{
+ outline:3px solid var(--gold);outline-offset:2px;
+ }
+ .btn:disabled{opacity:.4;cursor:not-allowed;}
+ .demo-caption{font-size:14px;opacity:.6;margin-top:-8px;}
+ .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;}
+ /* topbar: always-there way out */
+ .topbar{
+ display:flex;align-items:center;justify-content:space-between;gap:10px;
+ padding:14px 16px;flex-shrink:0;z-index:6;
+ }
+ .nav-btn{
+ min-height:56px;min-width:56px;border-radius:14px;border:2px solid rgba(253,251,247,.4);
+ background:transparent;color:var(--cream);font-family:'Inter',sans-serif;font-size:17px;
+ font-weight:600;cursor:pointer;padding:0 16px;display:flex;align-items:center;gap:6px;
+ }
+ .light .nav-btn{border-color:#ddd4c2;color:var(--charcoal);}
+ .step-indicator{padding:0 24px;font-size:17px;opacity:.7;flex-shrink:0;}
+ .content{padding:8px 24px 24px;display:flex;flex-direction:column;gap:16px;flex:1;overflow-y:auto;}
+ .actions{margin-top:auto;display:flex;flex-direction:column-reverse;gap:12px;padding-top:16px;}
+ .swatch{
+ width:100%;height:200px;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;}
+ .thumb{width:96px;height:96px;border-radius:14px;flex-shrink:0;
+ background:repeating-linear-gradient(45deg,#8FAE8B 0 16px,#B9CBB2 16px 32px),linear-gradient(135deg,#8FAE8B,#6f8d6c);
+ background-blend-mode:multiply;}
+ .item-title{font-size:22px;font-weight:700;}
+ .item-sub{font-size:17px;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;}
+ .btn-link{background:none;border:none;color:var(--cream);text-decoration:underline;font-size:17px;min-height:44px;cursor:pointer;font-family:'Inter',sans-serif;}
+ .light .btn-link{color:var(--charcoal);}
+ .search-bar{background:#2a2a2d;border:2px solid #3a3a3d;border-radius:14px;padding:0 14px;display:flex;align-items:center;}
+ .search-bar input{background:none;border:none;color:var(--cream);font-size:17px;padding:14px 8px;width:100%;font-family:'Inter',sans-serif;}
+ .search-bar input::placeholder{color:rgba(253,251,247,.5);}
+ .section-label{font-size:14px;opacity:.6;text-transform:uppercase;letter-spacing:.5px;margin-top:4px;}
+ .brand-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;}
+ .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);}
+ .brand-tile.hidden{display:none;}
+ .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;}
+ .done-row{display:flex;gap:14px;align-items:center;}
+ .done-facts{font-size:16px;line-height:1.5;}
+ .done-facts strong{font-size:18px;}
+ .toast{
+ position:absolute;left:16px;right:16px;bottom:24px;background:#2a2a2d;color:var(--cream);
+ border-radius:14px;padding:14px 18px;display:none;align-items:center;justify-content:space-between;
+ gap:12px;box-shadow:0 8px 24px rgba(0,0,0,.5);z-index:20;font-size:17px;border:2px solid var(--gold);
+ }
+ .toast.show{display:flex;}
+ .toast button{background:none;border:none;color:var(--gold);font-weight:700;font-size:17px;cursor:pointer;min-height:56px;min-width:56px;padding:0 6px;}
+ @media (prefers-reduced-motion: reduce){
+ .looking-wrap .spinner{animation:none;border-top-color:var(--gold);}
+ *{transition:none !important;}
+ }
+</style>
+</head>
+<body>
+<div class="stage" id="stage">
+
+ <!-- HOME -->
+ <div class="screen active" id="home">
+ <div class="dw-mark">DW</div>
+ <div class="hint">Point at anything and tap 📸</div>
+ <label class="demo-toggle"><input type="checkbox" id="demoToggle" onchange="demoNoMatch=this.checked"> Demo: show "no match"</label>
+ <div class="viewfinder"></div>
+ <div class="shutter-wrap">
+ <button class="shutter" id="shutterBtn" aria-label="Shoot photo">📸</button>
+ </div>
+ </div>
+
+ <!-- PHOTO CHECK (generic, reused after every shot) -->
+ <div class="screen center-flex" id="photoCheck">
+ <div class="topbar">
+ <button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button>
+ <button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button>
+ </div>
+ <div class="content center-flex">
+ <div class="thumb" id="photoCheckThumb" style="width:220px;height:220px;"></div>
+ <p style="font-size:18px;" aria-live="polite">Looks good?</p>
+ <div class="actions">
+ <button class="btn btn-primary" onclick="photoLooksGood()">Looks good</button>
+ <button class="btn btn-secondary" onclick="goBack()">Retake</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- LOOKING -->
+ <div class="screen looking-wrap" id="looking">
+ <div class="topbar" style="position:absolute;top:0;left:0;right:0;">
+ <button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button>
+ <button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button>
+ </div>
+ <div class="spinner"></div>
+ <p aria-live="polite">Looking for matches…</p>
+ </div>
+
+ <!-- WHAT'S THIS FOR -->
+ <div class="screen" id="whatFor">
+ <div class="topbar">
+ <button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button>
+ <button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button>
+ </div>
+ <div class="content">
+ <h2>What's this photo for?</h2>
+ <div class="pick-list">
+ <button class="btn btn-primary" onclick="chooseJob('add')">Add New Item</button>
+ <button class="btn btn-primary" onclick="chooseJob('update')">Update an Item</button>
+ <button class="btn btn-primary" onclick="chooseJob('find')">Find an Item</button>
+ <button class="btn btn-primary" onclick="chooseJob('sample')">A Sample Came In</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- ADD: BRAND GRID -->
+ <div class="screen" id="addBrand">
+ <div class="topbar">
+ <button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button>
+ <button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button>
+ </div>
+ <div class="content">
+ <h3>Which brand?</h3>
+ <div class="search-bar"><input type="text" id="brandSearch" placeholder="Search brands…" oninput="filterBrands()" aria-label="Search brands"></div>
+ <div class="section-label">Recent</div>
+ <div class="brand-grid" id="recentBrands">
+ <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,'Kravet')" onkeydown="onBrandKey(event,this,'Kravet')">Kravet</div>
+ <div class="brand-tile" tabindex="0" role="button" onclick="selectBrand(this,'Thibaut')" onkeydown="onBrandKey(event,this,'Thibaut')">Thibaut</div>
+ </div>
+ <div class="section-label">All Brands</div>
+ <div class="brand-grid" id="allBrands">
+ <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,'Cole & Son')" onkeydown="onBrandKey(event,this,'Cole & Son')">Cole & Son</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>
+ <div class="actions">
+ <button class="btn btn-primary" id="saveDraftBtn" disabled onclick="saveDraft()">Save as Draft</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- GENERIC DONE SCREEN -->
+ <div class="screen light" id="doneScreen">
+ <div class="topbar">
+ <button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button>
+ <button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button>
+ </div>
+ <div class="content">
+ <div class="done-row">
+ <div class="thumb" id="doneThumb"></div>
+ <div class="done-facts">
+ <strong id="doneTitle"></strong><br>
+ code <span id="doneCode"></span> · <span id="doneBrand"></span><br>
+ <span id="doneAction"></span>
+ </div>
+ </div>
+ <div class="actions">
+ <button class="btn btn-light-primary" onclick="goHome()">Do Another</button>
+ <button class="btn btn-light-secondary" onclick="goHome()">Home</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- UPDATE: MATCH -->
+ <div class="screen" id="updateMatch">
+ <div class="topbar">
+ <button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button>
+ <button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button>
+ </div>
+ <div class="step-indicator">Step 1 of 2</div>
+ <div class="content">
+ <h2>Found a Match</h2>
+ <div class="swatch blue"></div>
+ <div class="item-title">Palm Damask — Sage</div>
+ <div class="item-sub">code DW-10423 · York</div>
+ <div class="actions">
+ <button class="btn btn-primary" onclick="go('updateCam2')">Yes, that's right</button>
+ <button class="btn btn-secondary" onclick="showNoMatch('update')">Not this item</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- UPDATE: TAKE NEW PHOTO -->
+ <div class="screen" id="updateCam2">
+ <div class="topbar">
+ <button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button>
+ <button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button>
+ </div>
+ <div class="step-indicator">Step 2 of 2</div>
+ <div class="hint" style="position:static;margin:12px 0;">Take the new photo</div>
+ <div class="viewfinder" style="position:relative;flex:1;"></div>
+ <div class="shutter-wrap" style="position:absolute;bottom:48px;left:0;right:0;">
+ <button class="shutter" onclick="showPhotoCheck('UPDATE_SAVE')" aria-label="Shoot new photo">📸</button>
+ </div>
+ </div>
+
+ <!-- GENERIC NO MATCH -->
+ <div class="screen" id="noMatch">
+ <div class="topbar">
+ <button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button>
+ <button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button>
+ </div>
+ <div class="content center-flex">
+ <div class="big-emoji">🤔</div>
+ <h2 aria-live="polite">No match found</h2>
+ <div class="actions">
+ <button class="btn btn-primary" onclick="noMatchTryAgain()">Try again</button>
+ <button class="btn btn-secondary" onclick="go('typeCode')">Type the code instead</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- GENERIC TYPE CODE -->
+ <div class="screen" id="typeCode">
+ <div class="topbar">
+ <button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button>
+ <button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button>
+ </div>
+ <div class="content">
+ <h3>Type the item's code</h3>
+ <div class="search-bar"><input type="text" id="codeInput" placeholder="e.g. DW-10423" aria-label="Type the item code"></div>
+ <div class="actions">
+ <button class="btn btn-primary" onclick="codeSubmit()">Find It</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- FIND: MATCH -->
+ <div class="screen" id="findMatch">
+ <div class="topbar">
+ <button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button>
+ <button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button>
+ </div>
+ <div class="content">
+ <h2>Best Match</h2>
+ <div class="swatch"></div>
+ <div class="item-title">Palm Damask — Sage</div>
+ <div class="item-sub">code DW-10423 · York</div>
+ <div class="actions">
+ <button class="btn btn-primary" onclick="finishFind()">Yes, that's right</button>
+ <button class="btn btn-secondary" onclick="showNoMatch('find')">Not this item</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- SAMPLE: MATCH -->
+ <div class="screen" id="sampleMatch">
+ <div class="topbar">
+ <button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button>
+ <button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button>
+ </div>
+ <div class="content">
+ <h2>Sample Matched</h2>
+ <div class="swatch"></div>
+ <div class="item-title">Palm Damask — Sage</div>
+ <div class="item-sub">code DW-10423 · York</div>
+ <div class="actions">
+ <button class="btn btn-primary" onclick="markReceived()">Mark It Received</button>
+ <button class="btn btn-secondary" onclick="showNoMatch('sample')">Not this item</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- SAMPLE: PRINTING -->
+ <div class="screen looking-wrap" id="samplePrinting">
+ <div class="topbar" style="position:absolute;top:0;left:0;right:0;">
+ <button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button>
+ <button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button>
+ </div>
+ <div class="spinner"></div>
+ <p aria-live="polite">Printing your sticker…</p>
+ </div>
+
+</div>
+
+<!-- TOAST (undo) -->
+<div class="toast" id="toast" role="status" aria-live="polite">
+ <span id="toastText">Saved</span>
+ <button onclick="undoAction()">Undo</button>
+</div>
+
+<script>
+ let historyStack = ['home'];
+ let selectedBrand = null;
+ let currentJob = null;
+ let demoNoMatch = false;
+ let photoCheckNext = null;
+ let noMatchJob = null;
+ let toastTimer = null;
+ let undoTarget = null;
+
+ function render(id){
+ document.querySelectorAll('.screen').forEach(s=>s.classList.remove('active'));
+ document.getElementById(id).classList.add('active');
+ }
+
+ function go(id){
+ historyStack.push(id);
+ render(id);
+ onEnter(id);
+ }
+
+ function goBack(){
+ if(historyStack.length > 1){
+ historyStack.pop();
+ render(historyStack[historyStack.length - 1]);
+ } else {
+ goHome();
+ }
+ }
+
+ function goHome(){
+ historyStack = ['home'];
+ render('home');
+ }
+
+ function onEnter(id){
+ if(id === 'addBrand'){
+ selectedBrand = null;
+ document.querySelectorAll('#addBrand .brand-tile').forEach(t=>t.classList.remove('selected'));
+ document.getElementById('saveDraftBtn').disabled = true;
+ document.getElementById('brandSearch').value = '';
+ filterBrands();
+ }
+ }
+
+ function showPhotoCheck(nextId){
+ photoCheckNext = nextId;
+ go('photoCheck');
+ }
+
+ function photoLooksGood(){
+ // remove photoCheck from the history stack so Back skips it cleanly
+ historyStack.pop();
+ if(photoCheckNext === 'HOME_SHOT'){ afterHomeShot(); return; }
+ if(photoCheckNext === 'UPDATE_SAVE'){ updateSave(); return; }
+ go(photoCheckNext);
+ }
+
+ function chooseJob(job){
+ currentJob = job;
+ if(job === 'add'){ go('addBrand'); }
+ else if(job === 'update'){ demoNoMatch ? showNoMatch('update') : go('updateMatch'); }
+ else if(job === 'find'){ demoNoMatch ? showNoMatch('find') : go('findMatch'); }
+ else if(job === 'sample'){ demoNoMatch ? showNoMatch('sample') : go('sampleMatch'); }
+ }
+
+ function showNoMatch(job){
+ noMatchJob = job;
+ go('noMatch');
+ }
+
+ function noMatchTryAgain(){
+ // back out to the camera to reshoot
+ historyStack = ['home'];
+ render('home');
+ }
+
+ function codeSubmit(){
+ if(noMatchJob === 'update'){ go('updateCam2'); }
+ else if(noMatchJob === 'find'){ finishFind(); }
+ else if(noMatchJob === 'sample'){ markReceived(); }
+ else { goHome(); }
+ }
+
+ 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 filterBrands(){
+ const q = document.getElementById('brandSearch').value.trim().toLowerCase();
+ document.querySelectorAll('#allBrands .brand-tile').forEach(t=>{
+ t.classList.toggle('hidden', q.length > 0 && !t.textContent.toLowerCase().includes(q));
+ });
+ }
+
+ function randomCode(){ return 'DW-' + Math.floor(10000 + Math.random() * 89999); }
+
+ function showDone(opts){
+ document.getElementById('doneThumb').className = 'thumb' + (opts.blue ? ' blue' : '');
+ document.getElementById('doneTitle').textContent = opts.name;
+ document.getElementById('doneCode').textContent = opts.code;
+ document.getElementById('doneBrand').textContent = opts.brand;
+ document.getElementById('doneAction').textContent = opts.action;
+ historyStack = ['home', 'doneScreen'];
+ render('doneScreen');
+ }
+
+ function saveDraft(){
+ if(!selectedBrand) return;
+ showToast('Saved · Undo', 'addBrand');
+ showDone({name:'New Item', code: randomCode(), brand: selectedBrand, action:'Saved as a draft'});
+ }
+
+ function updateSave(){
+ showToast('Updated · Undo', 'updateMatch');
+ showDone({name:'Palm Damask — Sage', code:'DW-10423', brand:'York', action:'Updated the photo', blue:true});
+ }
+
+ function finishFind(){
+ showDone({name:'Palm Damask — Sage', code:'DW-10423', brand:'York', action:'Found it'});
+ }
+
+ function markReceived(){
+ go('samplePrinting');
+ var fromScreen = 'samplePrinting';
+ setTimeout(function(){
+ if(historyStack[historyStack.length - 1] !== fromScreen) return;
+ showToast('Saved · Undo', 'sampleMatch');
+ showDone({name:'Palm Damask — Sage', code:'DW-10423', brand:'York', action:'Marked as received'});
+ }, 1100);
+ }
+
+ function showToast(text, backTarget){
+ document.getElementById('toastText').textContent = text;
+ undoTarget = backTarget;
+ const t = document.getElementById('toast');
+ t.classList.add('show');
+ clearTimeout(toastTimer);
+ toastTimer = setTimeout(function(){ t.classList.remove('show'); }, 5000);
+ }
+
+ function undoAction(){
+ document.getElementById('toast').classList.remove('show');
+ clearTimeout(toastTimer);
+ if(undoTarget){
+ historyStack = ['home', undoTarget];
+ render(undoTarget);
+ // NOTE: do NOT call onEnter() here — undo must restore the confirm
+ // screen with the prior selection intact, not reset it (onEnter('addBrand')
+ // clears selectedBrand for a fresh forward-navigation entry only).
+ }
+ }
+
+ document.getElementById('shutterBtn').addEventListener('click', function(){
+ showPhotoCheck('HOME_SHOT');
+ });
+
+ // after the very first shot on Home, go looking then ask what it's for
+ function afterHomeShot(){
+ historyStack.push('looking');
+ render('looking');
+ var fromScreen = 'looking';
+ setTimeout(function(){
+ if(historyStack[historyStack.length - 1] !== fromScreen) return;
+ historyStack.pop();
+ go('whatFor');
+ }, 700);
+ }
+</script>
+</body>
+</html>
diff --git a/public/kid-simple/v10-todays-inbox.html b/public/kid-simple/v10-todays-inbox.html
new file mode 100644
index 0000000..20f7c4a
--- /dev/null
+++ b/public/kid-simple/v10-todays-inbox.html
@@ -0,0 +1,396 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+<title>v10 — Today's Inbox</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link href="https://fonts.googleapis.com/css2?family=Newsreader:opsz,wght@6..72,500;6..72,600;6..72,700&family=Manrope:wght@500;600;700;800&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --bg:#F3EEE4; --card:#FFFFFF; --ink:#24211C; --muted:#8A8171;
+ --brass:#A9772F; --brass-ink:#3A2A0E;
+ --terracotta:#B85C38; --terracotta-bg:#F3E1D6;
+ --teal:#4F7A73; --teal-bg:#DEEAE7;
+ --plum:#6B4E71; --plum-bg:#E9E0EB;
+ --green:#4C8C6B; --green-bg:#DCEEE3;
+ --line:#E7DFCF;
+ }
+ *{box-sizing:border-box;}
+ html,body{margin:0;padding:0;background:#ccc;font-family:'Manrope',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:26px 22px 30px; overflow-y:auto; }
+ button{ font-family:'Manrope',sans-serif; cursor:pointer; }
+
+ /* ---- Home / Inbox ---- */
+ .inbox-header{ margin-bottom:18px; }
+ .inbox-eyebrow{ font-size:14px; font-weight:700; letter-spacing:.06em; text-transform:uppercase; color:var(--muted); margin:0 0 2px; }
+ .inbox-title{ font-family:'Newsreader',serif; font-weight:600; font-size:34px; line-height:1.1; margin:0; color:var(--ink); }
+ .inbox-sub{ font-size:15px; color:var(--muted); margin:8px 0 0; }
+
+ .rowlist{ display:flex; flex-direction:column; gap:12px; margin-top:20px; }
+ .row{
+ display:flex; align-items:center; gap:14px; width:100%; min-height:76px;
+ background:var(--card); border:1px solid var(--line); border-radius:20px; padding:12px 16px;
+ text-align:left; box-shadow:0 2px 6px rgba(36,33,28,.05);
+ }
+ .row:focus-visible, .bigbtn:focus-visible, .linktext:focus-visible, .fab:focus-visible, .brandrow:focus-visible{
+ outline:3px solid #2B6CB0; outline-offset:2px;
+ }
+ .badge{
+ flex:0 0 auto; width:52px; height:52px; border-radius:16px; display:flex; align-items:center;
+ justify-content:center; font-size:24px;
+ }
+ .badge.terracotta{ background:var(--terracotta-bg); }
+ .badge.teal{ background:var(--teal-bg); }
+ .badge.plum{ background:var(--plum-bg); }
+ .row-text{ flex:1; min-width:0; }
+ .row-title{ font-size:17px; font-weight:800; margin:0; color:var(--ink); }
+ .row-sub{ font-size:14px; color:var(--muted); margin:2px 0 0; }
+ .chev{ font-size:20px; color:var(--muted); flex:0 0 auto; }
+
+ .caughtup{
+ display:flex; align-items:center; gap:14px; width:100%; min-height:76px;
+ background:var(--green-bg); border:1px solid transparent; border-radius:20px; padding:12px 16px;
+ }
+ .caughtup .badge{ background:#fff; }
+ .caughtup .row-title{ color:#2C5C42; }
+ .caughtup .row-sub{ color:#4C8C6B; }
+
+ .fab{
+ position:absolute; right:20px; bottom:26px; z-index:20;
+ width:68px; height:68px; border-radius:50%; background:var(--brass); border:4px solid #fff;
+ color:#fff; font-size:26px; display:flex; align-items:center; justify-content:center;
+ box-shadow:0 10px 22px rgba(169,119,47,.45);
+ }
+ .fab-label{
+ position:absolute; right:20px; bottom:98px; z-index:20; background:var(--ink); color:#fff;
+ font-size:12px; font-weight:700; padding:6px 10px; border-radius:10px; opacity:.88;
+ }
+
+ /* ---- Shared question / answer patterns ---- */
+ .topbar{ display:flex; align-items:center; gap:10px; margin-bottom:10px; min-height:56px; }
+ .backbtn{
+ background:none; border:none; color:var(--muted); font-size:15px; font-weight:700;
+ min-height:56px; padding:6px 4px; display:flex; align-items:center; gap:4px;
+ }
+ .question{
+ font-family:'Newsreader',serif; font-weight:600; font-size:28px; line-height:1.2;
+ margin:6px 0 20px; color:var(--ink);
+ }
+ .answers{ flex:1; display:flex; flex-direction:column; gap:14px; }
+ .bigbtn{
+ width:100%; min-height:64px; border-radius:18px; font-size:18px; font-weight:800;
+ background:#fff; border:2px solid var(--line); color:var(--ink); padding:12px 18px;
+ }
+ .bigbtn-primary{ background:var(--brass); border:2px solid var(--brass); color:#fff; }
+ .bigbtn-secondary{ background:#fff; border:2px solid var(--muted); color:var(--muted); }
+ .linktext{
+ background:none; border:none; color:var(--muted); font-size:16px; text-decoration:underline;
+ text-align:center; min-height:56px; width:100%; font-weight:700;
+ }
+
+ /* ---- Queue lists (samples / photos) ---- */
+ .queuelist{ display:flex; flex-direction:column; gap:12px; }
+ .queuecard{
+ display:flex; align-items:center; gap:14px; width:100%; min-height:76px;
+ background:#fff; border:1px solid var(--line); border-radius:20px; padding:12px 14px; text-align:left;
+ }
+ .swatch{ flex:0 0 auto; width:52px; height:52px; border-radius:14px; }
+ .sw1{background:repeating-linear-gradient(45deg,#8FAE8B 0 10px,#6f8f6b 10px 20px);}
+ .sw2{background:repeating-linear-gradient(-45deg,#B85C38 0 10px,#96482a 10px 20px);}
+ .sw3{background:radial-gradient(circle at 30% 30%, #A9772F 0 8px, #E9D9BD 9px 20px);}
+ .sw4{background:repeating-linear-gradient(90deg,#6B4E71 0 9px,#54394f 9px 18px);}
+ .sw5{background:radial-gradient(circle at 60% 40%, #4F7A73 0 9px, #DCEEE3 10px 22px);}
+ .qc-text{ flex:1; min-width:0; }
+ .qc-title{ font-size:16px; font-weight:800; margin:0; }
+ .qc-sub{ font-size:13px; color:var(--muted); margin:2px 0 0; }
+
+ .itemname{ font-size:21px; font-weight:800; margin:14px 0 0; text-align:center; }
+ .itemmeta{ font-size:15px; color:var(--muted); margin:4px 0 0; text-align:center; }
+ .swatch.big{ width:100%; height:170px; border-radius:20px; margin:0 auto; }
+
+ /* ---- Camera ---- */
+ .camwrap{ flex:1; display:flex; flex-direction:column; padding:0; margin:-26px -22px -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:44px; left:20px; right:20px; text-align:center;
+ font-family:'Newsreader',serif; font-size:25px; font-weight:600; color:#fdfbf7;
+ }
+ .camview .status{ font-size:17px; font-weight:700; color:#e9dfca; }
+ .shutter{
+ position:absolute; bottom:34px; left:50%; transform:translateX(-50%);
+ width:88px; height:88px; border-radius:50%; background:var(--brass); border:6px solid #fff;
+ font-size:32px; display:flex; align-items:center; justify-content:center;
+ }
+ .camback{
+ position:absolute; top:16px; left:16px; z-index:5; min-width:56px; min-height:56px;
+ background:rgba(255,255,255,.16); border:none; border-radius:12px; color:#fff; font-size:14px; font-weight:700;
+ }
+
+ .center{ display:flex; flex-direction:column; align-items:center; justify-content:center; flex:1; gap:10px; }
+
+ /* ---- Brand list ---- */
+ .brandlist{ display:flex; flex-direction:column; gap:12px; overflow-y:auto; }
+ .brandrow{
+ width:100%; min-height:60px; border-radius:16px; background:#fff; border:2px solid var(--line);
+ font-size:17px; font-weight:700; color:var(--ink); padding:10px 16px; text-align:left;
+ }
+</style>
+</head>
+<body>
+<div class="phone">
+ <div class="screen" id="screen" role="main"></div>
+</div>
+
+<script>
+const BRANDS = ['York Wallcoverings','Brewster Home','Kravet','Cole & Son','Thibaut','Ronald Redding'];
+
+let samplesWaiting = [
+ {name:'Trellis Weave', color:'Indigo', code:'DW-88214', sw:'sw4'},
+ {name:'Palm Damask', color:'Sage', code:'DW-10423', sw:'sw1'},
+ {name:'Harbor Stripe', color:'Rust', code:'DW-71190', sw:'sw2'},
+];
+let photosNeeded = [
+ {name:'Linen Bloom', color:'Blush', code:'DW-55071', sw:'sw3'},
+ {name:'Meadow Grid', color:'Moss', code:'DW-33982', sw:'sw5'},
+];
+const FIND_ITEMS = [
+ {name:'Trellis Weave', color:'Indigo', code:'DW-88214', sw:'sw4'},
+ {name:'Palm Damask', color:'Sage', code:'DW-10423', sw:'sw1'},
+ {name:'Linen Bloom', color:'Blush', code:'DW-55071', sw:'sw3'},
+];
+
+const state = { screen:'home', brand:null, newItemSw:'sw1', sampleIdx:0, photoIdx:0, findIdx:0, activeItem:null };
+
+function go(screen){ state.screen = screen; render(); }
+
+function cameraScreen(hint, onShoot, backTo){
+ return `<div class="camwrap"><div class="camview">
+ ${backTo ? `<button class="camback" onclick="go('${backTo}')">‹ Inbox</button>` : ''}
+ <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…';
+ setTimeout(()=>{ cb(); }, 650);
+}
+
+function render(){
+ const s = document.getElementById('screen');
+ let html = '';
+
+ // ---------------- HOME ----------------
+ if(state.screen==='home'){
+ const today = new Date();
+ const dateStr = today.toLocaleDateString(undefined,{weekday:'long', month:'long', day:'numeric'});
+ html += `<div class="inbox-header">
+ <p class="inbox-eyebrow">DW · Today's Inbox</p>
+ <h1 class="inbox-title">${dateStr}</h1>
+ <p class="inbox-sub">Here's what's waiting on you.</p>
+ </div>
+ <div class="rowlist">`;
+
+ if(samplesWaiting.length){
+ html += `<button class="row" onclick="go('sample-queue')">
+ <div class="badge terracotta">📦</div>
+ <div class="row-text">
+ <p class="row-title">${samplesWaiting.length} sample${samplesWaiting.length>1?'s':''} came in</p>
+ <p class="row-sub">Tap to check ${samplesWaiting.length>1?'them':'it'} in</p>
+ </div>
+ <div class="chev">›</div>
+ </button>`;
+ }
+ if(photosNeeded.length){
+ html += `<button class="row" onclick="go('update-queue')">
+ <div class="badge teal">🔄</div>
+ <div class="row-text">
+ <p class="row-title">${photosNeeded.length} item${photosNeeded.length>1?'s':''} need${photosNeeded.length>1?'':'s'} new photos</p>
+ <p class="row-sub">Tap to update ${photosNeeded.length>1?'them':'it'}</p>
+ </div>
+ <div class="chev">›</div>
+ </button>`;
+ }
+ if(!samplesWaiting.length && !photosNeeded.length){
+ html += `<div class="caughtup">
+ <div class="badge">🎉</div>
+ <div class="row-text">
+ <p class="row-title">All caught up!</p>
+ <p class="row-sub">Nothing waiting right now</p>
+ </div>
+ </div>`;
+ }
+ html += `<button class="row" onclick="state.findIdx=0; go('find-method')">
+ <div class="badge plum">🔍</div>
+ <div class="row-text">
+ <p class="row-title">Find an item</p>
+ <p class="row-sub">Scan, talk, or type</p>
+ </div>
+ <div class="chev">›</div>
+ </button>`;
+ html += `</div>`;
+ html += `<div class="fab-label">Add new</div>
+ <button class="fab" aria-label="Add a new item" onclick="go('new-cam')">📸</button>`;
+ }
+
+ // ---------------- ADD NEW ITEM (via FAB) ----------------
+ else if(state.screen==='new-cam'){
+ html += cameraScreen('Take a photo of it.', "shoot(()=>go('new-brand'))", 'home');
+ }
+ else if(state.screen==='new-brand'){
+ html += `<h1 class="question">Which brand is it?</h1><div class="brandlist">`;
+ BRANDS.forEach(b=>{ html += `<button class="brandrow" 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 big ${state.newItemSw}"></div>
+ <p class="itemmeta">${state.brand}</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 += `<div class="center"><h1 class="question">Saved! 🎉</h1></div>
+ <div class="answers">
+ <button class="bigbtn bigbtn-primary" onclick="go('new-cam')">Add Another</button>
+ <button class="linktext" onclick="go('home')">Back to Inbox</button>
+ </div>`;
+ }
+
+ // ---------------- SAMPLE QUEUE (row 1) ----------------
+ else if(state.screen==='sample-queue'){
+ html += `<div class="topbar"><button class="backbtn" onclick="go('home')">‹ Inbox</button></div>
+ <h1 class="question">Samples waiting (${samplesWaiting.length})</h1>
+ <div class="queuelist">`;
+ samplesWaiting.forEach((it,i)=>{
+ html += `<button class="queuecard" onclick="state.sampleIdx=${i}; go('sample-confirm')">
+ <div class="swatch ${it.sw}"></div>
+ <div class="qc-text"><p class="qc-title">${it.name} — ${it.color}</p><p class="qc-sub">code ${it.code}</p></div>
+ <div class="chev">›</div>
+ </button>`;
+ });
+ html += `</div>`;
+ }
+ else if(state.screen==='sample-confirm'){
+ const it = samplesWaiting[state.sampleIdx];
+ html += `<div class="topbar"><button class="backbtn" onclick="go('sample-queue')">‹ Back</button></div>
+ <h1 class="question">Mark it received?</h1>
+ <div class="swatch big ${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="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'){
+ samplesWaiting.splice(state.sampleIdx,1);
+ if(state.sampleIdx>=samplesWaiting.length) state.sampleIdx = 0;
+ go('sample-done');
+ } }, 1000);
+ }
+ else if(state.screen==='sample-done'){
+ html += `<div class="center"><h1 class="question">Received! ✅</h1></div>
+ <div class="answers">
+ ${samplesWaiting.length ? `<button class="bigbtn bigbtn-primary" onclick="go('sample-queue')">Next Sample</button>` : ''}
+ <button class="linktext" onclick="go('home')">Back to Inbox</button>
+ </div>`;
+ }
+
+ // ---------------- UPDATE QUEUE (row 2) ----------------
+ else if(state.screen==='update-queue'){
+ html += `<div class="topbar"><button class="backbtn" onclick="go('home')">‹ Inbox</button></div>
+ <h1 class="question">Items needing photos (${photosNeeded.length})</h1>
+ <div class="queuelist">`;
+ photosNeeded.forEach((it,i)=>{
+ html += `<button class="queuecard" onclick="state.photoIdx=${i}; go('update-cam')">
+ <div class="swatch ${it.sw}"></div>
+ <div class="qc-text"><p class="qc-title">${it.name} — ${it.color}</p><p class="qc-sub">code ${it.code}</p></div>
+ <div class="chev">›</div>
+ </button>`;
+ });
+ html += `</div>`;
+ }
+ else if(state.screen==='update-cam'){
+ const it = photosNeeded[state.photoIdx];
+ html += cameraScreen(`Take a new photo of ${it.name}.`, "shoot(()=>go('update-done'))", 'update-queue');
+ }
+ else if(state.screen==='update-done'){
+ html += `<div class="center"><h1 class="question">Updated! ✅</h1></div>`;
+ photosNeeded.splice(state.photoIdx,1);
+ if(state.photoIdx>=photosNeeded.length) state.photoIdx = 0;
+ html += `<div class="answers">
+ ${photosNeeded.length ? `<button class="bigbtn bigbtn-primary" onclick="go('update-queue')">Next Item</button>` : ''}
+ <button class="linktext" onclick="go('home')">Back to Inbox</button>
+ </div>`;
+ }
+
+ // ---------------- FIND AN ITEM (row 3) ----------------
+ else if(state.screen==='find-method'){
+ html += `<div class="topbar"><button class="backbtn" onclick="go('home')">‹ Inbox</button></div>
+ <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 = FIND_ITEMS[state.findIdx];
+ html += `<div class="topbar"><button class="backbtn" onclick="go('find-method')">‹ Back</button></div>
+ <h1 class="question">Which one is it?</h1>
+ <div class="swatch big ${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-detail'){
+ html += `<div class="center">
+ <h1 class="question" style="font-size:24px; margin-bottom:12px;">${state.activeItem.name}</h1>
+ <div class="swatch big ${state.activeItem.sw}" style="max-width:240px;"></div>
+ <p class="itemmeta">${state.activeItem.color} · code ${state.activeItem.code}</p>
+ </div>
+ <div class="answers">
+ <button class="bigbtn bigbtn-primary" onclick="go('update-cam-fromfind')">Update This</button>
+ <button class="bigbtn bigbtn-secondary" onclick="state.findIdx=0; go('find-method')">Search Again</button>
+ <button class="linktext" onclick="go('home')">Back to Inbox</button>
+ </div>`;
+ }
+ else if(state.screen==='update-cam-fromfind'){
+ html += cameraScreen(`Take the new photo of ${state.activeItem.name}.`, "shoot(()=>go('find-updated'))", 'find-detail');
+ }
+ else if(state.screen==='find-updated'){
+ html += `<div class="center"><h1 class="question">Updated! ✅</h1></div>
+ <button class="bigbtn bigbtn-primary" onclick="go('home')">Back to Inbox</button>`;
+ }
+
+ s.innerHTML = html;
+}
+function pickResult(){ state.activeItem = FIND_ITEMS[state.findIdx]; go('find-detail'); }
+function nextResult(){
+ if(state.findIdx < FIND_ITEMS.length-1){ state.findIdx++; go('find-result'); }
+ else { state.findIdx = 0; go('find-result'); }
+}
+
+render();
+</script>
+</body>
+</html>
diff --git a/public/kid-simple/v11-bottom-sheet.html b/public/kid-simple/v11-bottom-sheet.html
new file mode 100644
index 0000000..cf649b5
--- /dev/null
+++ b/public/kid-simple/v11-bottom-sheet.html
@@ -0,0 +1,397 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+<title>v11 — Bottom Sheet</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=Newsreader:opsz,wght@6..72,500;6..72,600;6..72,700&family=Manrope:wght@500;600;700;800&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --cream:#FFF8ED; --ink:#241E17; --copper:#B5652D; --copper-ink:#3A200D;
+ --olive:#6B7660; --forest:#4F8B6B; --line:#EAE0CE; --muted:#8A8074;
+ --camdark:#15130F;
+ }
+ *{box-sizing:border-box;}
+ html,body{margin:0;padding:0;background:#ddd;font-family:'Manrope',sans-serif;color:var(--ink);}
+ body{display:flex; align-items:flex-start; justify-content:center; min-height:100vh; padding:20px 0;}
+ .phone{
+ width:390px; height:780px; background:var(--camdark); border-radius:36px; overflow:hidden;
+ box-shadow:0 24px 60px rgba(0,0,0,.32); position:relative; isolation:isolate;
+ }
+
+ /* ---------- camera backdrop (always full-bleed, hero) ---------- */
+ .camera-bg{
+ position:absolute; inset:0;
+ background:
+ radial-gradient(120% 90% at 50% 8%, #2a251d 0%, #15130f 55%, #0e0c09 100%);
+ }
+ .cam-grid{ position:absolute; inset:0; opacity:.16;
+ background-image:
+ linear-gradient(rgba(255,255,255,.5) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(255,255,255,.5) 1px, transparent 1px);
+ background-size:33.33% 33.33%;
+ }
+ .cam-corner{ position:absolute; width:34px; height:34px; border-color:#F3E7CC; opacity:.85; }
+ .cam-corner.tl{ top:56px; left:20px; border-top:3px solid; border-left:3px solid; border-radius:6px 0 0 0;}
+ .cam-corner.tr{ top:56px; right:20px; border-top:3px solid; border-right:3px solid; border-radius:0 6px 0 0;}
+ .mark{
+ position:absolute; top:18px; left:20px; color:#F3E7CC; font-family:'Manrope',sans-serif;
+ font-weight:800; font-size:13px; letter-spacing:.14em; opacity:.92;
+ }
+ .live-pill{
+ position:absolute; top:18px; right:20px; display:flex; align-items:center; gap:6px;
+ background:rgba(0,0,0,.35); border:1px solid rgba(255,255,255,.25); border-radius:20px;
+ padding:5px 12px 5px 9px; color:#F3E7CC; font-size:12px; font-weight:700; letter-spacing:.06em;
+ }
+ .live-dot{ width:8px; height:8px; border-radius:50%; background:#E2543A; box-shadow:0 0 0 3px rgba(226,84,58,.28); }
+ .cam-hint{
+ position:absolute; left:24px; right:24px; top:110px; text-align:center;
+ font-family:'Newsreader',serif; font-weight:600; font-size:25px; line-height:1.28;
+ color:#FBF3E3; text-shadow:0 2px 14px rgba(0,0,0,.5);
+ }
+ .cam-status{
+ position:absolute; left:0; right:0; text-align:center; color:#D8CBB0; font-size:14px; font-weight:600;
+ letter-spacing:.03em;
+ }
+
+ /* shutter floats above the sheet, only present on "shoot" steps */
+ .shutter{
+ position:absolute; left:50%; transform:translateX(-50%);
+ width:78px; height:78px; border-radius:50%; background:var(--copper);
+ border:5px solid #FBF3E3; display:flex; align-items:center; justify-content:center;
+ font-size:28px; box-shadow:0 8px 22px rgba(0,0,0,.4); z-index:5;
+ }
+ .shutter:focus-visible{ outline:3px solid #2B6CB0; outline-offset:3px; }
+ .shutter[disabled]{ opacity:.55; }
+
+ /* ---------- bottom sheet ---------- */
+ .sheet{
+ position:absolute; left:0; right:0; bottom:0; z-index:10;
+ background:var(--cream); border-radius:26px 26px 0 0;
+ box-shadow:0 -12px 30px rgba(0,0,0,.28);
+ display:flex; flex-direction:column;
+ transition:height .32s cubic-bezier(.22,.9,.32,1);
+ height:340px; touch-action:none;
+ }
+ .sheet.full{ height:640px; }
+ .handle-wrap{
+ display:flex; flex-direction:column; align-items:center; padding:10px 0 4px; cursor:grab; flex:0 0 auto;
+ }
+ .handle-bar{ width:44px; height:5px; border-radius:4px; background:#DDD1B8; }
+ .sheet-body{ flex:1; min-height:0; overflow-y:auto; padding:6px 24px 26px; }
+
+ .home-fab{
+ position:absolute; top:14px; left:14px; z-index:20; min-width:56px; min-height:56px;
+ border-radius:16px; background:rgba(20,17,12,.55); border:1px solid rgba(255,255,255,.25); color:#FBF3E3;
+ font-size:14px; font-weight:700; display:none; align-items:center; justify-content:center; gap:4px;
+ padding:0 12px; backdrop-filter: blur(2px);
+ }
+ .home-fab:focus-visible{ outline:3px solid #7EC0E8; outline-offset:2px; }
+
+ h1.q{
+ font-family:'Newsreader',serif; font-weight:600; font-size:26px; line-height:1.2;
+ margin:8px 0 18px; color:var(--ink); text-align:left;
+ }
+ h1.q.center{ text-align:center; }
+ .sub{ font-size:14px; color:var(--muted); margin:-12px 0 16px; font-weight:600; }
+
+ .rows{ display:flex; flex-direction:column; gap:12px; }
+ button{ font-family:'Manrope',sans-serif; cursor:pointer; }
+
+ .row-btn{
+ width:100%; min-height:64px; border-radius:18px; border:2px solid var(--line); background:#fff;
+ display:flex; align-items:center; gap:14px; padding:10px 16px; text-align:left;
+ font-size:17px; font-weight:700; color:var(--ink);
+ }
+ .row-btn .ic{ font-size:26px; width:36px; text-align:center; flex:0 0 auto; }
+ .row-btn:focus-visible{ outline:3px solid #2B6CB0; outline-offset:2px; }
+
+ .bigbtn{
+ width:100%; min-height:60px; border-radius:16px; font-size:17px; font-weight:800;
+ background:#fff; border:2px solid var(--line); color:var(--ink); padding:10px 16px;
+ }
+ .bigbtn.primary{ background:var(--copper); border-color:var(--copper); color:#FFF6E9; }
+ .bigbtn.secondary{ background:#fff; border-color:var(--olive); color:var(--olive); }
+ .bigbtn:focus-visible{ outline:3px solid #2B6CB0; outline-offset:2px; }
+ .linktext{
+ background:none; border:none; color:var(--muted); font-size:15px; text-decoration:underline;
+ text-align:center; min-height:56px; width:100%; font-weight:600;
+ }
+
+ .brandlist{ display:flex; flex-direction:column; gap:10px; }
+ .brandlist .row-btn{ font-size:16px; min-height:58px; }
+
+ .swatch{ width:100%; height:150px; border-radius:16px; margin-bottom:12px; }
+ .swatch.sm{ width:120px; height:120px; margin:0 auto 14px; }
+ .sw1{background:repeating-linear-gradient(45deg,#B5652D 0 18px,#96501f 18px 36px);}
+ .sw2{background:repeating-linear-gradient(-45deg,#6B7660 0 16px,#565f4d 16px 32px);}
+ .sw3{background:repeating-linear-gradient(90deg,#D9A96A 0 14px,#c48f4a 14px 28px);}
+ .itemname{ font-size:19px; font-weight:800; margin:0; text-align:center; }
+ .itemmeta{ font-size:14px; color:var(--muted); margin:4px 0 0; text-align:center; font-weight:600; }
+
+ .center-wrap{ display:flex; flex-direction:column; align-items:center; justify-content:center; min-height:160px; gap:6px; }
+ .done-emoji{ font-size:44px; }
+</style>
+</head>
+<body>
+<div class="phone" id="phone">
+ <div class="camera-bg">
+ <div class="cam-grid"></div>
+ <div class="mark">DW · CAPTURE</div>
+ <div class="live-pill"><span class="live-dot"></span>LIVE</div>
+ <div class="cam-corner tl"></div>
+ <div class="cam-corner tr"></div>
+ <div class="cam-hint" id="camHint"></div>
+ <div class="cam-status" id="camStatus" style="bottom:378px;"></div>
+ <button class="shutter" id="shutterBtn" style="bottom:364px;" aria-label="Take photo" onclick="handleShoot()">📸</button>
+ </div>
+
+ <button class="home-fab" id="homeFab" aria-label="Home" onclick="go('home')">⌂ Home</button>
+
+ <div class="sheet" id="sheet">
+ <div class="handle-wrap" id="handleWrap">
+ <div class="handle-bar"></div>
+ </div>
+ <div class="sheet-body" id="sheetBody" role="main"></div>
+ </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 };
+
+// screen -> {sheet: 'peek'|'full', cam:{hint,status,shutter:bool}}
+function meta(screen){
+ const M = {
+ 'home': {sheet:'peek', cam:{hint:'', status:'', shutter:false}},
+ 'new-cam': {sheet:'peek', cam:{hint:'Take a photo of it', status:'Tap the shutter', shutter:true}},
+ 'new-brand': {sheet:'full', cam:{hint:'', status:'', shutter:false}},
+ 'new-review': {sheet:'full', cam:{hint:'', status:'', shutter:false}},
+ 'new-done': {sheet:'full', cam:{hint:'', status:'', shutter:false}},
+ 'update-cam': {sheet:'peek', cam:{hint:'Scan the label or pattern', status:'Tap the shutter', shutter:true}},
+ 'update-match':{sheet:'full', cam:{hint:'', status:'', shutter:false}},
+ 'update-cam2': {sheet:'peek', cam:{hint:'Take the new photo', status:'Tap the shutter', shutter:true}},
+ 'update-done': {sheet:'full', cam:{hint:'', status:'', shutter:false}},
+ 'find-method': {sheet:'full', cam:{hint:'', status:'', shutter:false}},
+ 'find-result': {sheet:'full', cam:{hint:'', status:'', shutter:false}},
+ 'find-exhausted':{sheet:'full', cam:{hint:'', status:'', shutter:false}},
+ 'find-detail': {sheet:'full', cam:{hint:'', status:'', shutter:false}},
+ 'sample-cam': {sheet:'peek', cam:{hint:'Scan the box label', status:'Tap the shutter', shutter:true}},
+ 'sample-match':{sheet:'full', cam:{hint:'', status:'', shutter:false}},
+ 'sample-printing':{sheet:'full', cam:{hint:'', status:'', shutter:false}},
+ 'sample-done': {sheet:'full', cam:{hint:'', status:'', shutter:false}},
+ };
+ return M[screen];
+}
+
+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)]; }
+
+let shootTarget = null;
+function armShoot(nextScreen){ shootTarget = nextScreen; }
+function handleShoot(){
+ if(!shootTarget) return;
+ const btn = document.getElementById('shutterBtn');
+ const st = document.getElementById('camStatus');
+ btn.disabled = true;
+ st.textContent = 'Snapping…';
+ const dest = shootTarget;
+ setTimeout(()=>{ state.item = randomItem(); go(dest); }, 650);
+}
+
+function bodyHTML(){
+ const s = state.screen;
+
+ if(s==='home'){
+ return `<h1 class="q">What are we doing?</h1>
+ <p class="sub">Pick one to get started</p>
+ <div class="rows">
+ <button class="row-btn" onclick="startJob('new')"><span class="ic">🆕</span>Add New Item</button>
+ <button class="row-btn" onclick="startJob('update')"><span class="ic">🔄</span>Update an Item</button>
+ <button class="row-btn" onclick="startJob('find')"><span class="ic">🔍</span>Find an Item</button>
+ <button class="row-btn" onclick="startJob('sample')"><span class="ic">📦</span>A Sample Came In</button>
+ </div>`;
+ }
+
+ if(s==='new-cam' || s==='update-cam' || s==='update-cam2' || s==='sample-cam'){
+ return `<p class="sub" style="margin-top:8px;">Point the camera above, then tap the shutter</p>`;
+ }
+
+ if(s==='new-brand'){
+ return `<h1 class="q">Which brand is it?</h1><div class="brandlist">` +
+ BRANDS.map(b=>`<button class="row-btn" onclick="state.brand='${b}'; go('new-review')">${b}</button>`).join('') +
+ `</div>`;
+ }
+ if(s==='new-review'){
+ return `<h1 class="q center">Save it?</h1>
+ <div class="swatch sm ${state.item.sw}"></div>
+ <p class="itemmeta" style="margin-bottom:18px;">${state.brand} · code ${state.item.code}</p>
+ <div class="rows">
+ <button class="bigbtn primary" onclick="go('new-done')">Save as Draft</button>
+ <button class="linktext" onclick="go('new-cam')">Retake Photo</button>
+ </div>`;
+ }
+ if(s==='new-done'){
+ return `<div class="center-wrap"><div class="done-emoji">🎉</div><h1 class="q center">Saved!</h1></div>
+ <div class="rows">
+ <button class="bigbtn primary" onclick="startJob('new')">Add Another</button>
+ <button class="linktext" onclick="go('home')">Back Home</button>
+ </div>`;
+ }
+
+ if(s==='update-match'){
+ return `<h1 class="q center">Is this it?</h1>
+ <div class="swatch sm ${state.item.sw}"></div>
+ <p class="itemname">${state.item.name} — ${state.item.color}</p>
+ <p class="itemmeta" style="margin-bottom:16px;">code ${state.item.code}</p>
+ <div class="rows">
+ <button class="bigbtn primary" onclick="go('update-cam2')">Yes</button>
+ <button class="bigbtn secondary" onclick="go('update-cam')">No, Let Me Search</button>
+ </div>`;
+ }
+ if(s==='update-done'){
+ return `<div class="center-wrap"><div class="done-emoji">✅</div><h1 class="q center">Updated!</h1></div>
+ <div class="rows"><button class="bigbtn primary" onclick="go('home')">Back Home</button></div>`;
+ }
+
+ if(s==='find-method'){
+ return `<h1 class="q">How do you want to find it?</h1>
+ <div class="rows">
+ <button class="row-btn" onclick="go('find-result')"><span class="ic">📷</span>Scan the Label</button>
+ <button class="row-btn" onclick="go('find-result')"><span class="ic">🎤</span>Say the Name</button>
+ <button class="row-btn" onclick="go('find-result')"><span class="ic">⌨️</span>Type It</button>
+ </div>`;
+ }
+ if(s==='find-result'){
+ const it = MOCK_ITEMS[state.resultIndex];
+ return `<h1 class="q center">Which one is it?</h1>
+ <div class="swatch sm ${it.sw}"></div>
+ <p class="itemname">${it.name} — ${it.color}</p>
+ <p class="itemmeta" style="margin-bottom:16px;">code ${it.code}</p>
+ <div class="rows">
+ <button class="bigbtn primary" onclick="pickResult()">Yes, This One</button>
+ <button class="bigbtn secondary" onclick="nextResult()">Next Result</button>
+ </div>`;
+ }
+ if(s==='find-exhausted'){
+ return `<h1 class="q">That's all — try another way?</h1>
+ <div class="rows">
+ <button class="row-btn" onclick="state.resultIndex=0; go('find-result')"><span class="ic">📷</span>Scan the Label</button>
+ <button class="row-btn" onclick="state.resultIndex=0; go('find-result')"><span class="ic">🎤</span>Say the Name</button>
+ <button class="row-btn" onclick="state.resultIndex=0; go('find-result')"><span class="ic">⌨️</span>Type It</button>
+ </div>`;
+ }
+ if(s==='find-detail'){
+ return `<div class="center-wrap">
+ <h1 class="q center" style="font-size:22px; margin-bottom:12px;">${state.item.name}</h1>
+ <div class="swatch sm ${state.item.sw}"></div>
+ <p class="itemmeta" style="margin-bottom:16px;">${state.item.color} · code ${state.item.code}</p>
+ </div>
+ <div class="rows">
+ <button class="bigbtn primary" onclick="go('update-cam2')">Update This</button>
+ <button class="bigbtn secondary" onclick="go('find-method')">Search Again</button>
+ <button class="linktext" onclick="go('home')">Home</button>
+ </div>`;
+ }
+
+ if(s==='sample-match'){
+ return `<h1 class="q center">Mark it received?</h1>
+ <div class="swatch sm ${state.item.sw}"></div>
+ <p class="itemname">${state.item.name} — ${state.item.color}</p>
+ <p class="itemmeta" style="margin-bottom:16px;">code ${state.item.code}</p>
+ <div class="rows"><button class="bigbtn primary" onclick="go('sample-printing')">Yes, Mark Received</button></div>`;
+ }
+ if(s==='sample-printing'){
+ setTimeout(()=>{ if(state.screen==='sample-printing') go('sample-done'); }, 1000);
+ return `<div class="center-wrap"><div class="done-emoji">🖨️</div><h1 class="q center">Printing your sticker…</h1></div>`;
+ }
+ if(s==='sample-done'){
+ return `<div class="center-wrap"><div class="done-emoji">✅</div><h1 class="q center">Done!</h1></div>
+ <div class="rows"><button class="bigbtn primary" onclick="go('home')">Back Home</button></div>`;
+ }
+ return '';
+}
+
+function render(){
+ const m = meta(state.screen);
+ const sheet = document.getElementById('sheet');
+ const fab = document.getElementById('homeFab');
+ const camHint = document.getElementById('camHint');
+ const camStatus = document.getElementById('camStatus');
+ const shutterBtn = document.getElementById('shutterBtn');
+
+ sheet.classList.toggle('full', m.sheet === 'full');
+ fab.style.display = state.screen === 'home' ? 'none' : 'flex';
+
+ camHint.textContent = m.cam.hint;
+ camStatus.textContent = m.cam.status;
+ if(m.cam.shutter){
+ shutterBtn.style.display = 'flex';
+ shutterBtn.disabled = false;
+ const nextMap = {'new-cam':'new-brand', 'update-cam':'update-match', 'update-cam2':'update-done', 'sample-cam':'sample-match'};
+ armShoot(nextMap[state.screen]);
+ } else {
+ shutterBtn.style.display = 'none';
+ shootTarget = null;
+ }
+
+ document.getElementById('sheetBody').innerHTML = bodyHTML();
+}
+
+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'); }
+}
+
+/* ---- draggable handle: free-drag between peek(340) and full(640), snaps on release ---- */
+(function(){
+ const sheet = document.getElementById('sheet');
+ const handle = document.getElementById('handleWrap');
+ const PEEK = 340, FULL = 640;
+ let dragging = false, startY = 0, startH = PEEK;
+
+ function clamp(v){ return Math.max(PEEK-40, Math.min(FULL+20, v)); }
+
+ handle.addEventListener('pointerdown', (e)=>{
+ dragging = true;
+ startY = e.clientY;
+ startH = sheet.getBoundingClientRect().height;
+ sheet.style.transition = 'none';
+ handle.setPointerCapture(e.pointerId);
+ });
+ handle.addEventListener('pointermove', (e)=>{
+ if(!dragging) return;
+ const dy = startY - e.clientY;
+ sheet.style.height = clamp(startH + dy) + 'px';
+ });
+ function endDrag(){
+ if(!dragging) return;
+ dragging = false;
+ sheet.style.transition = '';
+ const h = sheet.getBoundingClientRect().height;
+ const snapFull = h > (PEEK+FULL)/2;
+ sheet.style.height = '';
+ sheet.classList.toggle('full', snapFull);
+ }
+ handle.addEventListener('pointerup', endDrag);
+ handle.addEventListener('pointercancel', endDrag);
+})();
+
+render();
+</script>
+</body>
+</html>
diff --git a/public/kid-simple/v12-hold-to-talk.html b/public/kid-simple/v12-hold-to-talk.html
new file mode 100644
index 0000000..58565a2
--- /dev/null
+++ b/public/kid-simple/v12-hold-to-talk.html
@@ -0,0 +1,411 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+<title>v12 — Hold-to-Talk</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=Cormorant+Garamond:wght@500;600;700&family=Manrope:wght@500;600;700;800&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --bg:#F7EFE4; --ink:#2A2118; --panel:#FFFFFF; --line:#E7DCC9;
+ --accent:#B5502E; --accent-dark:#8C3A1F; --teal:#2F5D57; --teal-dark:#20403C;
+ --amber:#CE8B3C; --success:#4F8F6D; --cam:#181F1D; --muted:#8A7F6E;
+ }
+ *{box-sizing:border-box; -webkit-tap-highlight-color:transparent;}
+ html,body{margin:0;padding:0;background:#ddd;font-family:'Manrope',sans-serif;color:var(--ink);}
+ body{display:flex; align-items:flex-start; justify-content:center; min-height:100vh; padding:20px 0;}
+ .phone{
+ width:390px; 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;
+ user-select:none;
+ }
+ .screen{ flex:1; display:flex; flex-direction:column; overflow-y:auto; position:relative; }
+ h1,h2{ font-family:'Cormorant Garamond', serif; margin:0; }
+ button{ font-family:'Manrope',sans-serif; cursor:pointer; }
+
+ /* ---- Home ---- */
+ .homeWrap{ flex:1; display:flex; flex-direction:column; background:var(--cam); color:#F7EFE4; position:relative; }
+ .homeWrap .grain{
+ position:absolute; inset:0;
+ background:
+ radial-gradient(circle at 20% 15%, rgba(47,93,87,.55), transparent 55%),
+ radial-gradient(circle at 85% 80%, rgba(181,80,46,.35), transparent 55%);
+ }
+ .dwmark{ position:relative; z-index:2; padding:22px 24px 0; font-size:12px; letter-spacing:.22em; text-transform:uppercase; color:#CBB79A; font-weight:700; }
+ .captionWrap{ position:relative; z-index:2; padding:18px 22px 0; flex:0 0 auto; }
+ .captionPill{
+ background:rgba(255,255,255,.06); border:1px solid rgba(247,239,228,.18); border-radius:20px;
+ padding:16px 18px; min-height:64px; display:flex; align-items:center; gap:12px; backdrop-filter:blur(2px);
+ }
+ .captionDot{ width:10px; height:10px; border-radius:50%; background:var(--muted); flex:0 0 auto; transition:background .2s; }
+ .captionDot.live{ background:var(--accent); box-shadow:0 0 0 0 rgba(181,80,46,.6); animation:pulseDot 1s infinite; }
+ @keyframes pulseDot{ 0%{box-shadow:0 0 0 0 rgba(181,80,46,.55);} 70%{box-shadow:0 0 0 9px rgba(181,80,46,0);} 100%{box-shadow:0 0 0 0 rgba(181,80,46,0);} }
+ .captionText{ font-size:17px; font-weight:600; line-height:1.35; color:#F7EFE4; }
+ .captionText.idle{ color:#CBB79A; font-weight:500; font-size:16px; }
+
+ .holdArea{ position:relative; z-index:2; flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:18px; padding:10px 24px; }
+ .holdHint{ font-family:'Cormorant Garamond',serif; font-size:26px; font-weight:600; text-align:center; color:#F7EFE4; margin:0 0 4px; }
+ .holdSub{ font-size:14px; color:#CBB79A; text-align:center; margin-top:-10px; }
+ .micBtnWrap{ position:relative; width:168px; height:168px; display:flex; align-items:center; justify-content:center; }
+ .ring{ position:absolute; inset:0; border-radius:50%; border:2px solid rgba(181,80,46,.0); transition:all .25s; }
+ .micBtnWrap.listening .ring{ border-color:rgba(181,80,46,.55); animation:ringGrow 1.1s ease-out infinite; }
+ @keyframes ringGrow{ 0%{ transform:scale(.82); opacity:.9;} 100%{ transform:scale(1.28); opacity:0;} }
+ .micBtn{
+ width:132px; height:132px; border-radius:50%; background:var(--accent); border:6px solid #F7EFE4;
+ display:flex; align-items:center; justify-content:center; font-size:52px; color:#fff;
+ box-shadow:0 10px 26px rgba(0,0,0,.4); transition:transform .12s, background .2s;
+ }
+ .micBtnWrap.listening .micBtn{ background:var(--accent-dark); transform:scale(1.06); }
+ .micBtn:focus-visible{ outline:3px solid #F2C879; outline-offset:4px; }
+
+ .fallbackWrap{ position:relative; z-index:2; padding:10px 20px 22px; }
+ .fallbackLabel{ text-align:center; font-size:13px; color:#8F8272; margin:0 0 10px; letter-spacing:.04em; }
+ .chipGrid{ display:grid; grid-template-columns:1fr 1fr; gap:10px; }
+ .chip{
+ min-height:58px; border-radius:16px; background:rgba(255,255,255,.07); border:1.5px solid rgba(247,239,228,.22);
+ color:#F7EFE4; font-size:14.5px; font-weight:700; padding:8px 10px; text-align:center; line-height:1.25;
+ }
+ .chip:active{ background:rgba(255,255,255,.16); }
+
+ /* ---- generic content screens ---- */
+ .pad{ flex:1; display:flex; flex-direction:column; padding:30px 24px 26px; background:var(--bg); }
+ .kicker{ font-size:12.5px; font-weight:700; letter-spacing:.14em; text-transform:uppercase; color:var(--teal); margin:0 0 8px; }
+ .question{ font-family:'Cormorant Garamond',serif; font-weight:600; font-size:30px; line-height:1.2; margin:0 0 22px; color:var(--ink); }
+
+ .bigbtn{
+ width:100%; min-height:60px; border-radius:16px; font-size:18px; font-weight:700;
+ background:var(--panel); border:2px solid var(--line); color:var(--ink); padding:12px 18px; margin-bottom:12px;
+ }
+ .bigbtn-primary{ background:var(--accent); border:2px solid var(--accent); color:#fff; }
+ .bigbtn-secondary{ background:#fff; border:2px solid var(--muted); color:var(--muted); }
+ .bigbtn:focus-visible{ outline:3px solid #2B6CB0; outline-offset:2px; }
+ .linktext{
+ background:none; border:none; color:var(--muted); font-size:16px; text-decoration:underline;
+ text-align:center; min-height:48px; width:100%;
+ }
+
+ .brandlist{ display:flex; flex-direction:column; gap:12px; overflow-y:auto; }
+ .brandcard{
+ width:100%; min-height:64px; border-radius:16px; background:#fff; border:2px solid var(--line);
+ display:flex; align-items:center; gap:14px; padding:10px 14px; font-size:18px; font-weight:700; color:var(--ink);
+ }
+ .swatch{ width:44px; height:44px; border-radius:12px; flex:0 0 auto; }
+ .sw1{background:repeating-linear-gradient(45deg,#8FAE8B 0 9px,#6f9169 9px 18px);}
+ .sw2{background:repeating-linear-gradient(-45deg,#7C93B0 0 8px,#5d7897 8px 16px);}
+ .sw3{background:repeating-linear-gradient(90deg,#CE8B3C 0 7px,#b0722a 7px 14px);}
+ .sw4{background:repeating-linear-gradient(135deg,#B5502E 0 8px,#8c3a1f 8px 16px);}
+
+ .itemcard{ background:#fff; border:2px solid var(--line); border-radius:20px; padding:20px; display:flex; flex-direction:column; align-items:center; gap:10px; margin-bottom:20px; }
+ .itemcard .swatch.big{ width:100%; height:150px; border-radius:16px; }
+ .itemname{ font-size:21px; font-weight:800; margin:2px 0 0; text-align:center; }
+ .itemmeta{ font-size:15px; color:var(--muted); margin:0; text-align:center; }
+
+ /* camera screens */
+ .camwrap{ flex:1; display:flex; flex-direction:column; position:relative; background:var(--cam); }
+ .camview{ flex:1; position:relative; display:flex; align-items:center; justify-content:center; color:#fff; }
+ .camview .hint{
+ position:absolute; top:44px; left:20px; right:20px; text-align:center;
+ font-family:'Cormorant Garamond',serif; font-size:27px; font-weight:600; color:#F7EFE4;
+ }
+ .camview .flashmsg{ font-size:16px; font-weight:600; color:#CBB79A; }
+ .shutter{
+ position:absolute; bottom:36px; left:50%; transform:translateX(-50%);
+ width:88px; height:88px; border-radius:50%; background:var(--accent); border:6px solid #fff;
+ font-size:32px; display:flex; align-items:center; justify-content:center; color:#fff;
+ }
+ .shutter:focus-visible{ outline:3px solid #F2C879; outline-offset:4px; }
+
+ .doneWrap{ flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:16px; padding:30px; background:var(--bg); text-align:center; }
+ .doneBadge{ width:96px; height:96px; border-radius:50%; background:var(--success); display:flex; align-items:center; justify-content:center; font-size:44px; color:#fff; }
+ .doneTitle{ font-family:'Cormorant Garamond',serif; font-size:30px; font-weight:700; margin:0; }
+
+ .homefab{
+ position:absolute; top:16px; left:16px; z-index:30; min-width:56px; min-height:56px;
+ border-radius:16px; background:rgba(255,255,255,.94); border:2px solid var(--line); color:var(--ink);
+ font-size:14px; 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,.16);
+ }
+ .homefab.onDark{ background:rgba(0,0,0,.4); border-color:rgba(255,255,255,.3); color:#fff; }
+ .homefab:focus-visible{ outline:3px solid #2B6CB0; outline-offset:2px; }
+ .btnrow{ display:flex; gap:10px; width:100%; }
+ .btnrow .bigbtn{ flex:1; }
+</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 BRANDS = [
+ {name:'Thibaut', sw:'sw1'},
+ {name:'Kravet', sw:'sw2'},
+ {name:'Cole & Son', sw:'sw3'},
+ {name:'Groundworks', sw:'sw4'},
+];
+const MATCH_ITEM = {name:'Palm Damask', color:'Sage', code:'item 10423', sw:'sw1'};
+const RESULTS = [
+ {name:'Palm Damask', color:'Sage', code:'item 10423', sw:'sw1'},
+ {name:'Trellis Weave', color:'Indigo', code:'item 88214', sw:'sw2'},
+ {name:'Linen Bloom', color:'Blush', code:'item 55071', sw:'sw3'},
+];
+
+const PHRASES = [
+ { steps:['Mm...', 'I have a...', 'I have a new...', '"Add a new item"'], go:'addCam' },
+ { steps:['Hmm...', 'I need to...', 'I need to update...', '"Update an item"'], go:'updScan' },
+ { steps:['Umm...', 'Where is...', 'Where is that...', '"Find an item"'], go:'findMethod' },
+ { steps:['So...', 'A box just...', 'A box just came...', '"A sample came in"'], go:'sampScan' },
+];
+let phraseIdx = 0;
+let holding = false, capStep = 0, capTimer = null, finishTimer = null;
+
+function esc(s){ return s; }
+
+const screen = document.getElementById('screen');
+const homeFab = document.getElementById('homeFab');
+
+function go(name, data){
+ clearHold();
+ homeFab.style.display = (name === 'home') ? 'none' : 'flex';
+ const dark = ['addCam','updScan','updCam2','sampScan'].includes(name);
+ homeFab.classList.toggle('onDark', dark);
+ screen.innerHTML = RENDER[name](data || {});
+ screen.scrollTop = 0;
+}
+
+function clearHold(){
+ holding = false; capStep = 0;
+ if(capTimer){ clearInterval(capTimer); capTimer = null; }
+ if(finishTimer){ clearTimeout(finishTimer); finishTimer = null; }
+}
+
+function startHold(){
+ if(holding) return;
+ holding = true; capStep = 0;
+ const wrap = document.getElementById('micWrap');
+ const dot = document.getElementById('capDot');
+ const txt = document.getElementById('capText');
+ if(wrap) wrap.classList.add('listening');
+ if(dot) dot.classList.add('live');
+ const phrase = PHRASES[phraseIdx];
+ txt.textContent = phrase.steps[0];
+ txt.classList.remove('idle');
+ capTimer = setInterval(()=>{
+ capStep++;
+ if(capStep >= phrase.steps.length){
+ clearInterval(capTimer); capTimer = null;
+ finishTimer = setTimeout(()=>{
+ const nextRoute = phrase.go;
+ phraseIdx = (phraseIdx + 1) % PHRASES.length;
+ clearHold();
+ go(nextRoute);
+ }, 480);
+ return;
+ }
+ txt.textContent = phrase.steps[capStep];
+ }, 380);
+}
+
+function endHold(){
+ if(!holding) return;
+ const finishedFully = capTimer === null && finishTimer !== null;
+ if(finishedFully){ return; } // let the auto-navigate finish
+ clearHold();
+ const wrap = document.getElementById('micWrap');
+ const dot = document.getElementById('capDot');
+ const txt = document.getElementById('capText');
+ if(wrap) wrap.classList.remove('listening');
+ if(dot) dot.classList.remove('live');
+ if(txt){
+ txt.textContent = "Didn't quite catch that — try again, or tap a job below";
+ txt.classList.remove('idle');
+ setTimeout(()=>{
+ if(document.getElementById('capText') === txt){
+ txt.textContent = 'Hold the button & tell me what you need';
+ txt.classList.add('idle');
+ }
+ }, 1700);
+ }
+}
+
+const RENDER = {
+ home: () => `
+ <div class="homeWrap">
+ <div class="grain"></div>
+ <div class="dwmark">Designer Wallcoverings</div>
+ <div class="captionWrap">
+ <div class="captionPill">
+ <div class="captionDot" id="capDot"></div>
+ <div class="captionText idle" id="capText">Hold the button & tell me what you need</div>
+ </div>
+ </div>
+ <div class="holdArea">
+ <p class="holdHint">Press & hold, then say it</p>
+ <div class="micBtnWrap" id="micWrap">
+ <div class="ring"></div>
+ <button class="micBtn" id="micBtn" aria-label="Hold and talk"
+ onpointerdown="startHold()" onpointerup="endHold()" onpointerleave="endHold()" onpointercancel="endHold()">🎙️</button>
+ </div>
+ <p class="holdSub">"new item" · "update" · "find" · "sample came in"</p>
+ </div>
+ <div class="fallbackWrap">
+ <p class="fallbackLabel">OR TAP INSTEAD</p>
+ <div class="chipGrid">
+ <button class="chip" onclick="go('addCam')">Add a New Item</button>
+ <button class="chip" onclick="go('updScan')">Update an Item</button>
+ <button class="chip" onclick="go('findMethod')">Find an Item</button>
+ <button class="chip" onclick="go('sampScan')">A Sample Came In</button>
+ </div>
+ </div>
+ </div>
+ `,
+
+ addCam: () => `
+ <div class="camwrap">
+ <div class="camview">
+ <div class="hint">Take a photo of it.</div>
+ <div class="flashmsg">Point the camera at the sample</div>
+ <button class="shutter" aria-label="Shoot photo" onclick="go('addBrand')">📸</button>
+ </div>
+ </div>
+ `,
+ addBrand: () => `
+ <div class="pad">
+ <p class="kicker">Add a new item</p>
+ <h2 class="question">Which brand is it?</h2>
+ <div class="brandlist">
+ ${BRANDS.map(b => `<button class="brandcard" onclick="go('addSave')"><span class="swatch ${b.sw}"></span>${b.name}</button>`).join('')}
+ </div>
+ </div>
+ `,
+ addSave: () => `
+ <div class="pad">
+ <p class="kicker">Add a new item</p>
+ <h2 class="question">Save it?</h2>
+ <div class="itemcard">
+ <div class="swatch big sw1"></div>
+ <p class="itemname">New Sample</p>
+ <p class="itemmeta">Thibaut · Draft</p>
+ </div>
+ <button class="bigbtn bigbtn-primary" onclick="go('addDone')">Save as Draft</button>
+ <button class="linktext" onclick="go('addCam')">Retake Photo</button>
+ </div>
+ `,
+ addDone: () => `
+ <div class="doneWrap">
+ <div class="doneBadge">🎉</div>
+ <h2 class="doneTitle">Saved!</h2>
+ <div class="btnrow">
+ <button class="bigbtn bigbtn-secondary" onclick="go('addCam')">Add Another</button>
+ <button class="bigbtn bigbtn-primary" onclick="go('home')">Home</button>
+ </div>
+ </div>
+ `,
+
+ updScan: () => `
+ <div class="camwrap">
+ <div class="camview">
+ <div class="hint">Show me the label or pattern.</div>
+ <div class="flashmsg">Scan it in the frame</div>
+ <button class="shutter" aria-label="Shoot photo" onclick="go('updMatch')">📸</button>
+ </div>
+ </div>
+ `,
+ updMatch: () => `
+ <div class="pad">
+ <p class="kicker">Update an item</p>
+ <h2 class="question">Found it — is this the one?</h2>
+ <div class="itemcard">
+ <div class="swatch big ${MATCH_ITEM.sw}"></div>
+ <p class="itemname">${MATCH_ITEM.name}</p>
+ <p class="itemmeta">${MATCH_ITEM.color} · ${MATCH_ITEM.code}</p>
+ </div>
+ <button class="bigbtn bigbtn-primary" onclick="go('updCam2')">Yes</button>
+ <button class="bigbtn bigbtn-secondary" onclick="go('findMethod')">No, Let Me Search</button>
+ </div>
+ `,
+ updCam2: () => `
+ <div class="camwrap">
+ <div class="camview">
+ <div class="hint">Take the new photo.</div>
+ <div class="flashmsg">This replaces the old one</div>
+ <button class="shutter" aria-label="Shoot photo" onclick="go('updDone')">📸</button>
+ </div>
+ </div>
+ `,
+ updDone: () => `
+ <div class="doneWrap">
+ <div class="doneBadge">✅</div>
+ <h2 class="doneTitle">Updated!</h2>
+ <button class="bigbtn bigbtn-primary" onclick="go('home')">Home</button>
+ </div>
+ `,
+
+ findMethod: () => `
+ <div class="pad">
+ <p class="kicker">Find an item</p>
+ <h2 class="question">How do you want to find it?</h2>
+ <button class="bigbtn" onclick="go('findResults')">📷 Scan the Label</button>
+ <button class="bigbtn" onclick="go('findResults')">🎤 Say the Name</button>
+ <button class="bigbtn" onclick="go('findResults')">⌨️ Type It</button>
+ </div>
+ `,
+ findResults: () => `
+ <div class="pad">
+ <p class="kicker">Find an item</p>
+ <h2 class="question">Which one is it?</h2>
+ <div class="brandlist">
+ ${RESULTS.map(r => `<button class="brandcard" onclick="go('findDetail')"><span class="swatch ${r.sw}"></span>${r.name}</button>`).join('')}
+ </div>
+ </div>
+ `,
+ findDetail: () => `
+ <div class="pad">
+ <p class="kicker">Find an item</p>
+ <div class="itemcard">
+ <div class="swatch big ${MATCH_ITEM.sw}"></div>
+ <p class="itemname">${MATCH_ITEM.name}</p>
+ <p class="itemmeta">${MATCH_ITEM.color} · ${MATCH_ITEM.code}</p>
+ </div>
+ <button class="bigbtn bigbtn-primary" onclick="go('updCam2')">Update This</button>
+ <button class="bigbtn bigbtn-secondary" onclick="go('findMethod')">Search Again</button>
+ </div>
+ `,
+
+ sampScan: () => `
+ <div class="camwrap">
+ <div class="camview">
+ <div class="hint">Scan the box label.</div>
+ <div class="flashmsg">Line it up in the frame</div>
+ <button class="shutter" aria-label="Shoot photo" onclick="go('sampMatch')">📸</button>
+ </div>
+ </div>
+ `,
+ sampMatch: () => `
+ <div class="pad">
+ <p class="kicker">A sample came in</p>
+ <h2 class="question">Mark it received?</h2>
+ <div class="itemcard">
+ <div class="swatch big ${MATCH_ITEM.sw}"></div>
+ <p class="itemname">${MATCH_ITEM.name}</p>
+ <p class="itemmeta">${MATCH_ITEM.color} · ${MATCH_ITEM.code}</p>
+ </div>
+ <button class="bigbtn bigbtn-primary" onclick="go('sampDone')">Yes, Mark Received</button>
+ </div>
+ `,
+ sampDone: () => `
+ <div class="doneWrap">
+ <div class="doneBadge">✅</div>
+ <h2 class="doneTitle">Printing your sticker...<br>Done!</h2>
+ <button class="bigbtn bigbtn-primary" onclick="go('home')">Home</button>
+ </div>
+ `,
+};
+
+go('home');
+</script>
+</body>
+</html>
diff --git a/public/kid-simple/v13-sticker-book.html b/public/kid-simple/v13-sticker-book.html
new file mode 100644
index 0000000..2d23b8f
--- /dev/null
+++ b/public/kid-simple/v13-sticker-book.html
@@ -0,0 +1,409 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+<title>v13 — Sticker Book</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=Poppins:wght@400;500;600;700;800&family=Caveat:wght@600;700&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --bg:#EFE3CF; /* kraft paper */
+ --paper:#FBF6EC; /* cream card */
+ --ink:#2E2620; /* deep coffee */
+ --muted:#8C7C68; /* taupe */
+ --gold:#A8763A; /* brass CTA */
+ --teal:#5F9A91; /* New Item sticker */
+ --rose:#C97B6B; /* Update sticker */
+ --honey:#B98D3E; /* Find sticker */
+ --olive:#86A466; /* Sample sticker */
+ --green:#4C9A6A; /* success */
+ --cam:#2E2620; /* camera bg */
+ }
+ *{box-sizing:border-box;}
+ html,body{margin:0;padding:0;background:#ccc;font-family:'Poppins',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);
+ background-image:
+ radial-gradient(rgba(255,255,255,.35) 1px, transparent 1px);
+ background-size:14px 14px;
+ 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:26px 22px 26px; overflow-y:auto; position:relative; }
+
+ /* ---- home ---- */
+ .wordmark{ font-family:'DM Serif Display', serif; font-size:13px; letter-spacing:.14em; color:var(--muted); text-transform:uppercase; margin:0 0 6px; }
+ .title{ font-family:'DM Serif Display', serif; font-size:32px; margin:0 0 4px; color:var(--ink); }
+ .subtitle{ font-family:'Caveat', cursive; font-weight:700; font-size:20px; color:var(--gold); margin:0 0 14px; }
+
+ .booth{
+ background:var(--cam); border-radius:22px; padding:18px 16px 54px; position:relative;
+ display:flex; flex-direction:column; align-items:center; margin-bottom:34px;
+ }
+ .lens{
+ width:74px; height:74px; border-radius:50%; background:radial-gradient(circle at 35% 32%, #6b6660, #17130f 70%);
+ border:5px solid #46403a; margin-bottom:10px;
+ }
+ .boothtext{ color:#F3ECDD; font-size:14px; font-weight:500; text-align:center; margin:0 0 2px; }
+ .boothhint{ color:#c9bfae; font-size:12px; text-align:center; margin:0; }
+ .shutter{
+ position:absolute; left:50%; bottom:-30px; transform:translateX(-50%);
+ width:72px; height:72px; border-radius:50%; background:var(--gold); border:5px solid var(--paper);
+ font-size:28px; display:flex; align-items:center; justify-content:center; box-shadow:0 8px 16px rgba(0,0,0,.3);
+ cursor:pointer;
+ }
+ .shutter:focus-visible{ outline:3px solid #2B6CB0; outline-offset:3px; }
+
+ .dropzone{ min-height:98px; display:flex; align-items:center; justify-content:center; margin-bottom:10px; }
+ .dropzone .placeholder{
+ font-family:'Caveat', cursive; font-size:18px; color:var(--muted); text-align:center; padding:0 20px;
+ border:2px dashed #cfc2ab; border-radius:14px; width:100%; min-height:70px; display:flex; align-items:center; justify-content:center;
+ }
+ .polaroid{
+ width:104px; background:#fff; padding:8px 8px 16px; border-radius:5px; box-shadow:0 10px 18px rgba(0,0,0,.28);
+ transform:rotate(-6deg); touch-action:none; user-select:none; cursor:grab; position:relative; z-index:5;
+ }
+ .polaroid.dragging{ cursor:grabbing; transform:rotate(0deg) scale(1.06); }
+ .polaroid .shot{ width:100%; height:82px; border-radius:2px; }
+ .polaroid .cap{ font-family:'Caveat', cursive; font-weight:700; font-size:14px; text-align:center; color:#3a3a3a; margin-top:5px; }
+ .ps1{background:repeating-linear-gradient(45deg,#8FAE8B 0 12px,#7c9c78 12px 24px);}
+ .ps2{background:repeating-linear-gradient(-45deg,#7C93B0 0 11px,#617a99 11px 22px);}
+ .ps3{background:repeating-linear-gradient(90deg,#D9A441 0 10px,#c98f2c 10px 20px);}
+
+ .stickerlabel{ font-size:13px; font-weight:700; color:var(--muted); text-transform:uppercase; letter-spacing:.08em; margin:0 0 10px; text-align:center; }
+ .stickers{ display:grid; grid-template-columns:1fr 1fr; gap:16px; }
+ .sticker{
+ aspect-ratio:1/1; border-radius:50%; display:flex; flex-direction:column; align-items:center; justify-content:center;
+ gap:4px; color:#fff; font-family:'Poppins',sans-serif; font-weight:700; font-size:14px; text-align:center;
+ border:4px dashed rgba(255,255,255,.55); box-shadow:0 10px 18px rgba(0,0,0,.18), inset 0 0 0 6px rgba(255,255,255,.12);
+ cursor:pointer; position:relative; min-height:56px; min-width:56px; padding:8px; transition:transform .15s, box-shadow .15s;
+ -webkit-tap-highlight-color:transparent;
+ }
+ .sticker .emoji{ font-size:32px; }
+ .sticker:nth-child(1){ background:var(--teal); transform:rotate(-4deg); }
+ .sticker:nth-child(2){ background:var(--rose); transform:rotate(3deg); }
+ .sticker:nth-child(3){ background:var(--honey); transform:rotate(3deg); }
+ .sticker:nth-child(4){ background:var(--olive); transform:rotate(-3deg); }
+ .sticker.dropready{ transform:scale(1.1) rotate(0deg) !important; box-shadow:0 0 0 6px rgba(255,255,255,.55), 0 16px 24px rgba(0,0,0,.3); }
+ .sticker:focus-visible{ outline:3px solid #2B6CB0; outline-offset:4px; }
+
+ /* ---- shared flow screens ---- */
+ .question{ font-family:'DM Serif Display', serif; font-size:28px; line-height:1.2; text-align:center; margin:6px 0 24px; color:var(--ink); }
+ .answers{ flex:1; display:flex; flex-direction:column; gap:14px; }
+ button{ font-family:'Poppins',sans-serif; cursor:pointer; }
+ .bigbtn{
+ width:100%; min-height:60px; border-radius:16px; font-size:17px; font-weight:700;
+ background:var(--paper); border:2px solid #e2d6c0; color:var(--ink); padding:10px 16px;
+ }
+ .bigbtn-primary{ background:var(--gold); border:2px solid var(--gold); color:#fff; }
+ .bigbtn-secondary{ background:var(--paper); border:2px solid var(--muted); color:var(--muted); }
+ .bigbtn:focus-visible, .linktext:focus-visible{ outline:3px solid #2B6CB0; outline-offset:2px; }
+ .linktext{
+ background:none; border:none; color:var(--muted); font-size:16px; text-decoration:underline;
+ text-align:center; min-height:56px; width:100%;
+ }
+ .brandlist{ display:flex; flex-direction:column; gap:12px; overflow-y:auto; }
+ .swatch{ width:100%; height:150px; border-radius:16px; margin-bottom:8px; }
+ .swatch.small{ height:110px; }
+ .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:19px; font-weight:800; margin:0; text-align:center; }
+ .itemmeta{ font-size:15px; color:var(--muted); margin:4px 0 0; text-align:center; }
+
+ .camwrap{ flex:1; display:flex; flex-direction:column; padding:0; margin:-26px -22px -26px; }
+ .camview{
+ flex:1; background:var(--cam); position:relative; display:flex; align-items:center; justify-content:center;
+ color:#fff; min-height:480px;
+ }
+ .camview .hint{
+ position:absolute; top:36px; left:20px; right:20px; text-align:center;
+ font-family:'DM Serif Display', serif; font-size:25px; color:#F3ECDD;
+ }
+ .camview .status{ font-size:16px; font-weight:600; color:#d9cdb8; }
+ .camview .shutter2{
+ position:absolute; bottom:32px; left:50%; transform:translateX(-50%);
+ width:82px; height:82px; border-radius:50%; background:var(--gold); border:6px solid #fff;
+ font-size:30px; display:flex; align-items:center; justify-content:center; box-shadow:0 8px 16px rgba(0,0,0,.3);
+ }
+ .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 #e2d6c0; color:var(--ink);
+ font-size:14px; 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,.15);
+ }
+ .homefab:focus-visible{ outline:3px solid #2B6CB0; 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 POLA_SWATCH = ['ps1','ps2','ps3'];
+
+const state = { screen:'home', job:null, item:null, brand:null, resultIndex:0, photoTaken:false, polaSwatch:'ps1' };
+
+function go(screen){ state.screen = screen; render(); }
+function randomItem(){ return MOCK_ITEMS[Math.floor(Math.random()*MOCK_ITEMS.length)]; }
+
+function shootHome(){
+ state.photoTaken = true;
+ state.polaSwatch = POLA_SWATCH[Math.floor(Math.random()*POLA_SWATCH.length)];
+ render();
+}
+
+function startJobFromSticker(job, hasPhoto){
+ if(hasPhoto){ state.item = randomItem(); }
+ state.photoTaken = false;
+ state.job = job; state.brand = null; state.resultIndex = 0;
+ const jumpMap = { new:'new-brand', update:'update-match', find:'find-result', sample:'sample-match' };
+ const startMap = { new:'new-cam', update:'update-cam', find:'find-method', sample:'sample-cam' };
+ go(hasPhoto ? jumpMap[job] : startMap[job]);
+}
+function stickerTap(job){
+ const hadPhoto = state.photoTaken;
+ startJobFromSticker(job, hadPhoto);
+}
+
+/* ---- drag the polaroid onto a sticker ---- */
+let dragEl = null, dragStartX = 0, dragStartY = 0, dragElLeft = 0, dragElTop = 0;
+function onPolaroidPointerDown(e){
+ dragEl = e.currentTarget;
+ const rect = dragEl.getBoundingClientRect();
+ dragElLeft = rect.left; dragElTop = rect.top;
+ dragStartX = e.clientX; dragStartY = e.clientY;
+ dragEl.setPointerCapture(e.pointerId);
+ dragEl.classList.add('dragging');
+ dragEl.style.position = 'fixed';
+ dragEl.style.left = dragElLeft + 'px';
+ dragEl.style.top = dragElTop + 'px';
+ dragEl.style.margin = '0';
+ window.addEventListener('pointermove', onPolaroidMove);
+ window.addEventListener('pointerup', onPolaroidUp);
+}
+function stickerUnderPoint(x, y){
+ const el = document.elementFromPoint(x, y);
+ return el && el.closest ? el.closest('.sticker') : null;
+}
+function onPolaroidMove(e){
+ if(!dragEl) return;
+ const dx = e.clientX - dragStartX;
+ const dy = e.clientY - dragStartY;
+ dragEl.style.left = (dragElLeft + dx) + 'px';
+ dragEl.style.top = (dragElTop + dy) + 'px';
+ document.querySelectorAll('.sticker').forEach(s=>s.classList.remove('dropready'));
+ const st = stickerUnderPoint(e.clientX, e.clientY);
+ if(st) st.classList.add('dropready');
+}
+function onPolaroidUp(e){
+ window.removeEventListener('pointermove', onPolaroidMove);
+ window.removeEventListener('pointerup', onPolaroidUp);
+ const st = stickerUnderPoint(e.clientX, e.clientY);
+ document.querySelectorAll('.sticker').forEach(s=>s.classList.remove('dropready'));
+ dragEl = null;
+ if(st){
+ startJobFromSticker(st.dataset.job, true);
+ } else {
+ render(); // snap back to its resting spot
+ }
+}
+
+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="shutter2" aria-label="Take photo" onclick="${onShoot}">📸</button>
+ </div></div>`;
+}
+function shoot(cb){
+ const st = document.getElementById('camStatus');
+ const btn = document.querySelector('.shutter2');
+ if(btn) btn.disabled = true;
+ if(st) st.textContent = 'Snapping…';
+ setTimeout(()=>{ state.item = randomItem(); cb(); }, 650);
+}
+
+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 += `
+ <p class="wordmark">Designer Wallcoverings</p>
+ <h1 class="title">📖 Sticker Book</h1>
+ <p class="subtitle">${state.photoTaken ? 'Drag your photo onto a sticker!' : 'Snap a photo, then stick it on a job.'}</p>
+
+ <div class="booth">
+ <div class="lens"></div>
+ <p class="boothtext">Point at anything</p>
+ <p class="boothhint">then tap the button below</p>
+ <button class="shutter" aria-label="Take a photo" onclick="shootHome()">📸</button>
+ </div>
+
+ <div class="dropzone">
+ ${state.photoTaken
+ ? `<div class="polaroid ${state.polaSwatch === 'pola' ? '' : ''}" onpointerdown="onPolaroidPointerDown(event)">
+ <div class="shot ${state.polaSwatch}"></div>
+ <div class="cap">your photo!</div>
+ </div>`
+ : `<div class="placeholder">your photo shows up here ✂️</div>`}
+ </div>
+
+ <p class="stickerlabel">or just tap a sticker</p>
+ <div class="stickers">
+ <div class="sticker" data-job="new" tabindex="0" role="button" aria-label="Add a New Item"
+ onclick="stickerTap('new')" onkeydown="if(event.key==='Enter')stickerTap('new')">
+ <span class="emoji">🆕</span><span>New Item</span>
+ </div>
+ <div class="sticker" data-job="update" tabindex="0" role="button" aria-label="Update an Item"
+ onclick="stickerTap('update')" onkeydown="if(event.key==='Enter')stickerTap('update')">
+ <span class="emoji">🔄</span><span>Update</span>
+ </div>
+ <div class="sticker" data-job="find" tabindex="0" role="button" aria-label="Find an Item"
+ onclick="stickerTap('find')" onkeydown="if(event.key==='Enter')stickerTap('find')">
+ <span class="emoji">🔍</span><span>Find It</span>
+ </div>
+ <div class="sticker" data-job="sample" tabindex="0" role="button" aria-label="A Sample Came In"
+ onclick="stickerTap('sample')" onkeydown="if(event.key==='Enter')stickerTap('sample')">
+ <span class="emoji">📦</span><span>Sample In</span>
+ </div>
+ </div>`;
+ }
+
+ /* ---- Add New Item ---- */
+ 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:18px;">
+ <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 += `<div class="center"><h1 class="question">Saved! 🎉</h1></div>
+ <div class="answers">
+ <button class="bigbtn bigbtn-primary" onclick="go('new-cam')">Add Another</button>
+ <button class="linktext" onclick="go('home')">Home</button>
+ </div>`;
+ }
+
+ /* ---- Update an Item ---- */
+ 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:18px;">
+ <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>`;
+ }
+
+ /* ---- Find an Item ---- */
+ 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 the Label</button>
+ <button class="bigbtn" onclick="go('find-result')">🎤 Say the Name</button>
+ <button class="bigbtn" onclick="go('find-result')">⌨️ Type It</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:18px;">
+ <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 the Label</button>
+ <button class="bigbtn" onclick="state.resultIndex=0; go('find-result')">🎤 Say the Name</button>
+ <button class="bigbtn" onclick="state.resultIndex=0; go('find-result')">⌨️ Type It</button>
+ </div>`;
+ }
+ else if(state.screen==='find-detail'){
+ html += `<div class="center">
+ <h1 class="question" style="font-size:24px; margin-bottom:12px;">${state.item.name}</h1>
+ <div class="swatch ${state.item.sw}" style="width:240px;"></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>`;
+ }
+
+ /* ---- A Sample Came In ---- */
+ 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:18px;">
+ <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'); }, 1000);
+ }
+ 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/kid-simple/v14-ticket-printer.html b/public/kid-simple/v14-ticket-printer.html
new file mode 100644
index 0000000..354f756
--- /dev/null
+++ b/public/kid-simple/v14-ticket-printer.html
@@ -0,0 +1,344 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+<title>v14 — Ticket Printer</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=Fredoka:wght@400;500;600;700&family=Courier+Prime:wght@400;700&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --bg:#F6F1E7; --ink:#241F1C; --paper:#FFFDF6; --muted:#8A7F70;
+ --chassis:#23343B; --slot:#0F1A1E; --lamp:#7FD858;
+ --new:#B5502D; --update:#3C6E71; --find:#B08D3E; --sample:#6B4E71;
+ }
+ *{box-sizing:border-box;}
+ html,body{margin:0;padding:0;background:#ddd;font-family:'Fredoka',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;position:relative;overflow-y:auto;}
+ button{font-family:'Fredoka',sans-serif;cursor:pointer;border:none;}
+ button:focus-visible, .linktext:focus-visible{outline:3px solid #2B6CB0;outline-offset:2px;}
+
+ /* ---- home fab ---- */
+ .homefab{
+ position:absolute;top:14px;left:14px;z-index:30;min-width:56px;min-height:56px;
+ border-radius:16px;background:rgba(255,255,255,.94);border:2px solid #e7e1d0;color:var(--ink);
+ font-size:14px;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,.18);
+ }
+
+ /* ---- printer chassis (home) ---- */
+ .printer-top{background:var(--chassis);padding:24px 22px 20px;border-radius:0 0 30px 30px;position:relative;}
+ .chassis-row{display:flex;align-items:center;justify-content:space-between;}
+ .dw-mark{color:var(--paper);font-weight:700;font-size:13px;letter-spacing:2.5px;opacity:.8;}
+ .lamp{width:12px;height:12px;border-radius:50%;background:var(--lamp);box-shadow:0 0 9px var(--lamp);}
+ .chassis-title{color:var(--paper);font-weight:700;font-size:26px;text-align:center;margin:12px 0 18px;letter-spacing:.3px;}
+ .slot{background:var(--slot);height:24px;border-radius:6px;margin:0 14px;position:relative;box-shadow:inset 0 3px 7px rgba(0,0,0,.6);}
+ .paper-peek{position:absolute;left:50%;transform:translateX(-50%);top:-3px;width:64%;height:8px;background:var(--paper);border-radius:2px 2px 0 0;opacity:.4;}
+ .readout{color:var(--paper);opacity:.72;text-align:center;font-size:13px;margin:14px 0 0;font-family:'Courier Prime',monospace;letter-spacing:.3px;}
+
+ /* ---- keypad (home) ---- */
+ .keypad{padding:22px;display:grid;grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;gap:16px;flex:1;}
+ .key{
+ border-radius:24px;color:#fff;display:flex;flex-direction:column;align-items:center;justify-content:center;
+ gap:10px;font-weight:700;font-size:17px;text-align:center;
+ box-shadow:0 6px 0 rgba(0,0,0,.2), 0 12px 20px rgba(0,0,0,.14);
+ }
+ .key:active{transform:translateY(5px);box-shadow:0 1px 0 rgba(0,0,0,.2);}
+ .key .key-icon{font-size:40px;line-height:1;}
+ .key-new{background:var(--new);}
+ .key-update{background:var(--update);}
+ .key-find{background:var(--find);color:#2B2109;}
+ .key-sample{background:var(--sample);}
+ .keypad-hint{grid-column:1/3;text-align:center;color:var(--muted);font-size:13px;font-family:'Courier Prime',monospace;align-self:end;}
+
+ /* ---- generic padded question screen ---- */
+ .pad{padding:32px 24px 28px;display:flex;flex-direction:column;flex:1;}
+ .q{font-weight:600;font-size:25px;line-height:1.28;text-align:center;margin:4px 0 24px;}
+ .stack{display:flex;flex-direction:column;gap:14px;}
+ .rowbtn{
+ width:100%;min-height:64px;border-radius:18px;font-size:18px;font-weight:700;
+ background:#fff;border:2px solid #e9e1d0;color:var(--ink);padding:12px 16px;
+ }
+ .accent-new{background:var(--new)!important;color:#fff!important;border-color:var(--new)!important;}
+ .accent-update{background:var(--update)!important;color:#fff!important;border-color:var(--update)!important;}
+ .accent-find{background:var(--find)!important;color:#2B2109!important;border-color:var(--find)!important;}
+ .accent-sample{background:var(--sample)!important;color:#fff!important;border-color:var(--sample)!important;}
+ .linktext{
+ background:none;border:none;color:var(--muted);font-size:16px;text-decoration:underline;
+ text-align:center;min-height:56px;width:100%;font-family:'Fredoka',sans-serif;
+ }
+ .brandlist{display:flex;flex-direction:column;gap:12px;overflow-y:auto;}
+
+ /* ---- swatch (mock item art) ---- */
+ .swatch{width:100%;height:110px;border-radius:14px;margin-bottom:10px;}
+ .sw1{background:repeating-linear-gradient(45deg,#B5502D 0 16px,#8f3f22 16px 32px);}
+ .sw2{background:repeating-linear-gradient(-45deg,#3C6E71 0 16px,#2c5254 16px 32px);}
+ .sw3{background:repeating-linear-gradient(90deg,#B08D3E 0 14px,#8f712c 14px 28px);}
+ .itemname{font-size:19px;font-weight:700;margin:0;text-align:center;}
+ .itemmeta{font-size:15px;color:#6d6459;margin:4px 0 0;text-align:center;font-family:'Courier Prime',monospace;}
+
+ /* ---- camera ---- */
+ .camwrap{flex:1;display:flex;flex-direction:column;}
+ .camview{flex:1;background:#12181B;position:relative;display:flex;align-items:center;justify-content:center;color:#fff;min-height:520px;}
+ .camview .tag{position:absolute;top:18px;left:18px;padding:7px 14px;border-radius:20px;font-weight:700;font-size:12.5px;letter-spacing:.4px;color:#fff;}
+ .camview .hint{position:absolute;top:66px;left:20px;right:20px;text-align:center;font-weight:600;font-size:25px;color:var(--paper);}
+ .camview .status{font-family:'Courier Prime',monospace;font-size:14px;color:#cfc6b5;}
+ .shutter{
+ position:absolute;bottom:32px;left:50%;transform:translateX(-50%);
+ width:92px;height:92px;border-radius:50%;background:var(--paper);border:6px solid rgba(255,255,255,.15);
+ font-size:34px;display:flex;align-items:center;justify-content:center;box-shadow:0 8px 18px rgba(0,0,0,.45);
+ }
+
+ /* ---- ticket dock + ticket paper ---- */
+ .ticket-dock{background:var(--chassis);padding:20px 22px 30px;position:relative;border-radius:0 0 30px 30px;}
+ .ticket-dock .slot{margin:0 10px;}
+ .ticket-dock .paper-peek.out{opacity:1;height:34px;top:-3px;transition:height .5s ease, opacity .5s ease;}
+ .dockmsg{color:var(--paper);text-align:center;font-family:'Courier Prime',monospace;font-size:13.5px;margin:14px 0 0;opacity:.85;}
+ .ticket{
+ background:var(--paper);margin:-14px 22px 0;padding:24px 20px 20px;position:relative;
+ box-shadow:0 12px 24px rgba(0,0,0,.16);
+ clip-path:polygon(0% 0%,100% 0%,100% 90%,95% 100%,90% 90%,85% 100%,80% 90%,75% 100%,70% 90%,65% 100%,60% 90%,55% 100%,50% 90%,45% 100%,40% 90%,35% 100%,30% 90%,25% 100%,20% 90%,15% 100%,10% 90%,5% 100%,0% 90%);
+ }
+ .ticket-mono{font-family:'Courier Prime',monospace;font-size:13.5px;line-height:1.85;color:#2b2620;white-space:pre-wrap;text-align:center;}
+ .ticket-head{font-weight:700;font-size:15px;letter-spacing:.5px;}
+ .ticket-cta{padding:26px 24px 24px;display:flex;flex-direction:column;gap:14px;}
+ .center{display:flex;flex-direction:column;align-items:center;justify-content:center;flex:1;gap:10px;}
+</style>
+</head>
+<body>
+<div class="phone">
+ <button class="homefab" id="homeFab" aria-label="Home" onclick="go('home')">⌂ Home</button>
+ <div 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 TAG = {new:{cls:'accent-new', label:'🆕 NEW ITEM'}, update:{cls:'accent-update', label:'🔄 UPDATE'},
+ find:{cls:'accent-find', label:'🔍 FIND'}, sample:{cls:'accent-sample', label:'📦 SAMPLE IN'}};
+
+const state = { screen:'home', job:null, item:null, brand:null, resultIndex:0, printed:false };
+
+function go(screen){
+ state.screen = screen;
+ if(screen.endsWith('-ticket')){
+ state.printed = false;
+ render();
+ setTimeout(()=>{ if(state.screen===screen){ state.printed = true; render(); } }, 800);
+ return;
+ }
+ 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-cam', sample:'sample-cam'};
+ go(map[job]);
+}
+function randomItem(){ return MOCK_ITEMS[Math.floor(Math.random()*MOCK_ITEMS.length)]; }
+function stamp(){
+ return new Date().toLocaleString(undefined,{month:'2-digit',day:'2-digit',hour:'numeric',minute:'2-digit'});
+}
+
+function cameraScreen(hint, onShootExpr){
+ const t = TAG[state.job];
+ return `<div class="camwrap"><div class="camview">
+ <div class="tag ${t.cls}">${t.label}</div>
+ <div class="hint">${hint}</div>
+ <div class="status" id="camStatus">Tap the shutter</div>
+ <button class="shutter" aria-label="Take photo" onclick="${onShootExpr}">📸</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…';
+ setTimeout(()=>{ state.item = randomItem(); cb(); }, 700);
+}
+
+function ticketDock(msg){
+ return `<div class="ticket-dock">
+ <div class="chassis-row"><span class="dw-mark">DW</span><span class="lamp"></span></div>
+ <div class="slot"><div class="paper-peek ${state.printed?'out':''}"></div></div>
+ <p class="dockmsg">${state.printed ? 'Ticket printed! 🖨️' : msg}</p>
+ </div>`;
+}
+
+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 += `<div class="printer-top">
+ <div class="chassis-row"><span class="dw-mark">DW</span><span class="lamp" aria-hidden="true"></span></div>
+ <div class="chassis-title">TICKET PRINTER</div>
+ <div class="slot"><div class="paper-peek"></div></div>
+ <p class="readout">Ready — pick a job to print a ticket.</p>
+ </div>
+ <div class="keypad">
+ <button class="key key-new" onclick="startJob('new')"><span class="key-icon">🆕</span><span>New Item</span></button>
+ <button class="key key-update" onclick="startJob('update')"><span class="key-icon">🔄</span><span>Update</span></button>
+ <button class="key key-find" onclick="startJob('find')"><span class="key-icon">🔍</span><span>Find</span></button>
+ <button class="key key-sample" onclick="startJob('sample')"><span class="key-icon">📦</span><span>Sample In</span></button>
+ <p class="keypad-hint">every job prints a ticket</p>
+ </div>`;
+ }
+
+ /* ---------- NEW ITEM ---------- */
+ else if(state.screen==='new-cam'){
+ html += cameraScreen('Take a photo of it.', "shoot(()=>go('new-brand'))");
+ }
+ else if(state.screen==='new-brand'){
+ html += `<div class="pad"><h1 class="q">Which brand is it?</h1><div class="brandlist">`;
+ BRANDS.forEach(b=>{ html += `<button class="rowbtn" onclick="state.brand='${b}'; go('new-ticket')">${b}</button>`; });
+ html += `</div></div>`;
+ }
+ else if(state.screen==='new-ticket'){
+ html += ticketDock('Printing your ticket…');
+ if(state.printed){
+ html += `<div class="ticket">
+ <div class="swatch ${state.item.sw}"></div>
+ <p class="ticket-mono"><span class="ticket-head">★ NEW ITEM ADDED ★</span>
+BRAND: ${state.brand}
+CODE: ${state.item.code}
+DATE: ${stamp()}
+
+ thanks — nice work!</p>
+ </div>
+ <div class="ticket-cta">
+ <button class="rowbtn accent-new" onclick="startJob('new')">Print Another</button>
+ <button class="linktext" onclick="go('home')">Home</button>
+ </div>`;
+ }
+ }
+
+ /* ---------- UPDATE ---------- */
+ else if(state.screen==='update-cam'){
+ html += cameraScreen('Scan the label or pattern.', "shoot(()=>go('update-match'))");
+ }
+ else if(state.screen==='update-match'){
+ html += `<div class="pad"><h1 class="q">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="stack" style="margin-top:22px;">
+ <button class="rowbtn accent-update" onclick="go('update-cam2')">Yes, That's It</button>
+ <button class="rowbtn" onclick="go('update-cam')">No, Scan Again</button>
+ </div></div>`;
+ }
+ else if(state.screen==='update-cam2'){
+ html += cameraScreen('Take the new photo.', "shoot(()=>go('update-ticket'))");
+ }
+ else if(state.screen==='update-ticket'){
+ html += ticketDock('Printing your ticket…');
+ if(state.printed){
+ html += `<div class="ticket">
+ <div class="swatch ${state.item.sw}"></div>
+ <p class="ticket-mono"><span class="ticket-head">★ ITEM UPDATED ★</span>
+ITEM: ${state.item.name}
+CODE: ${state.item.code}
+DATE: ${stamp()}
+
+ all set!</p>
+ </div>
+ <div class="ticket-cta">
+ <button class="rowbtn accent-update" onclick="go('home')">Back Home</button>
+ </div>`;
+ }
+ }
+
+ /* ---------- FIND ---------- */
+ else if(state.screen==='find-cam'){
+ html += cameraScreen('Scan it to find the item.', "shoot(()=>{ state.resultIndex=0; go('find-result'); })");
+ }
+ else if(state.screen==='find-result'){
+ const it = MOCK_ITEMS[state.resultIndex];
+ html += `<div class="pad"><h1 class="q">Is this the one?</h1>
+ <div class="swatch ${it.sw}"></div>
+ <p class="itemname">${it.name} — ${it.color}</p>
+ <p class="itemmeta">code ${it.code}</p>
+ <div class="stack" style="margin-top:22px;">
+ <button class="rowbtn accent-find" onclick="pickResult()">Yes, This One</button>
+ <button class="rowbtn" onclick="nextResult()">Next Result</button>
+ </div></div>`;
+ }
+ else if(state.screen==='find-exhausted'){
+ html += `<div class="pad center"><h1 class="q">That's everything. Try scanning again?</h1>
+ <button class="rowbtn accent-find" onclick="go('find-cam')">Scan Again</button>
+ </div>`;
+ }
+ else if(state.screen==='find-ticket'){
+ html += ticketDock('Printing your ticket…');
+ if(state.printed){
+ html += `<div class="ticket">
+ <div class="swatch ${state.item.sw}"></div>
+ <p class="ticket-mono"><span class="ticket-head">★ HERE'S YOUR ITEM ★</span>
+ITEM: ${state.item.name}
+CODE: ${state.item.code}
+DATE: ${stamp()}
+
+ found it!</p>
+ </div>
+ <div class="ticket-cta">
+ <button class="rowbtn accent-update" onclick="go('update-cam2')">Update This</button>
+ <button class="rowbtn" onclick="go('find-cam')">Search Again</button>
+ <button class="linktext" onclick="go('home')">Home</button>
+ </div>`;
+ }
+ }
+
+ /* ---------- SAMPLE ---------- */
+ else if(state.screen==='sample-cam'){
+ html += cameraScreen('Scan the box label.', "shoot(()=>go('sample-match'))");
+ }
+ else if(state.screen==='sample-match'){
+ html += `<div class="pad"><h1 class="q">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="stack" style="margin-top:22px;">
+ <button class="rowbtn accent-sample" onclick="go('sample-ticket')">Yes, Mark Received</button>
+ </div></div>`;
+ }
+ else if(state.screen==='sample-ticket'){
+ html += ticketDock('Printing your ticket…');
+ if(state.printed){
+ html += `<div class="ticket">
+ <div class="swatch ${state.item.sw}"></div>
+ <p class="ticket-mono"><span class="ticket-head">★ SAMPLE RECEIVED ★</span>
+ITEM: ${state.item.name}
+CODE: ${state.item.code}
+DATE: ${stamp()}
+
+ sticker printed! ✅</p>
+ </div>
+ <div class="ticket-cta">
+ <button class="rowbtn accent-sample" onclick="go('home')">Back Home</button>
+ </div>`;
+ }
+ }
+
+ s.innerHTML = html;
+}
+function pickResult(){ state.item = MOCK_ITEMS[state.resultIndex]; go('find-ticket'); }
+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/kid-simple/v15-thumb-zone.html b/public/kid-simple/v15-thumb-zone.html
new file mode 100644
index 0000000..f25313c
--- /dev/null
+++ b/public/kid-simple/v15-thumb-zone.html
@@ -0,0 +1,493 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+<title>v15 — Thumb Zone (Shoot-First / One-Thumb Confirm)</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=Spectral:wght@500;600;700&family=Manrope:wght@400;600;700;800&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --stage:#07070A;
+ --panel:#18120E;
+ --panel-edge:#241A13;
+ --line:#3A2C20;
+ --ink:#F6EFE4;
+ --muted:#A9998A;
+ --accent:#FF8A42;
+ --accent-ink:#2B1206;
+ --success:#5FCE8B;
+ }
+ *{box-sizing:border-box;}
+ html,body{margin:0;padding:0;background:#000;font-family:'Manrope',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; height:780px; background:var(--stage); border-radius:36px;
+ overflow:hidden; box-shadow:0 24px 60px rgba(0,0,0,.5); display:flex; flex-direction:column;
+ position:relative;
+ }
+
+ /* ---------- STAGE (top two-thirds: camera is ALWAYS live, in every state) ---------- */
+ .stage{
+ flex:1; min-height:0; background:radial-gradient(120% 90% at 50% 0%, #101015 0%, #07070A 60%);
+ position:relative; display:flex; flex-direction:column; overflow:hidden;
+ }
+ .stage-topbar{
+ display:flex; align-items:center; justify-content:space-between; padding:16px 20px 0;
+ font-family:'Manrope'; font-size:12px; font-weight:700; letter-spacing:.08em; color:var(--muted);
+ }
+ .stage-topbar .dw{ color:var(--accent); font-weight:800; }
+ .live-dot{ display:inline-flex; align-items:center; gap:6px; }
+ .live-dot span.dot{ width:7px; height:7px; border-radius:50%; background:var(--accent); box-shadow:0 0 8px var(--accent); animation:pulse 1.6s ease-in-out infinite; }
+ @keyframes pulse{ 0%,100%{opacity:1;} 50%{opacity:.35;} }
+ .stage-body{ flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; padding:14px 26px; gap:14px; text-align:center; }
+ .stage-hint{ font-family:'Spectral',serif; font-weight:600; font-size:23px; line-height:1.3; color:var(--ink); max-width:300px; }
+ .stage-status{ font-family:'Manrope'; font-size:14px; font-weight:700; color:var(--muted); letter-spacing:.03em; }
+
+ .viewfinder{ width:230px; height:230px; position:relative; display:flex; align-items:center; justify-content:center; }
+ .corner{ position:absolute; width:34px; height:34px; border-color:var(--accent); }
+ .corner.tl{ top:0; left:0; border-top:3px solid; border-left:3px solid; border-radius:10px 0 0 0; }
+ .corner.tr{ top:0; right:0; border-top:3px solid; border-right:3px solid; border-radius:0 10px 0 0; }
+ .corner.bl{ bottom:0; left:0; border-bottom:3px solid; border-left:3px solid; border-radius:0 0 0 10px; }
+ .corner.br{ bottom:0; right:0; border-bottom:3px solid; border-right:3px solid; border-radius:0 0 10px 0; }
+ .viewfinder .glyph{ font-size:44px; opacity:.9; }
+
+ .idle-orb{
+ width:150px; height:150px; border-radius:50%;
+ background:radial-gradient(circle at 35% 30%, rgba(255,138,66,.35), rgba(255,138,66,0) 65%);
+ border:2px solid var(--line); display:flex; align-items:center; justify-content:center; font-size:46px;
+ }
+
+ .swatch-card{
+ width:190px; height:190px; border-radius:24px; box-shadow:0 14px 30px rgba(0,0,0,.5);
+ display:flex; align-items:flex-end; justify-content:center; position:relative; overflow:hidden;
+ }
+ .sw1{background:repeating-linear-gradient(45deg,#8FAE8B 0 18px,#5f7a5c 18px 36px);}
+ .sw2{background:repeating-linear-gradient(-45deg,#7C93B0 0 16px,#4c637f 16px 32px);}
+ .sw3{background:repeating-linear-gradient(90deg,#D9A441 0 14px,#a9711f 14px 28px);}
+ .item-caption{ margin-top:2px; }
+ .item-name{ font-family:'Spectral',serif; font-weight:700; font-size:20px; margin:0; color:var(--ink); }
+ .item-meta{ font-family:'Manrope'; font-size:13px; color:var(--muted); margin:4px 0 0; }
+
+ .big-emoji{ font-size:60px; }
+
+ /* toast, floats over the still-live stage */
+ .toast{
+ position:absolute; left:50%; bottom:14px; transform:translateX(-50%);
+ background:rgba(20,14,10,.94); border:2px solid var(--accent); border-radius:999px;
+ padding:10px 20px; font-weight:800; font-size:14px; display:flex; align-items:center; gap:8px;
+ box-shadow:0 10px 24px rgba(0,0,0,.4); white-space:nowrap;
+ }
+
+ /* ---------- THUMB CONTROLLER (bottom third: the ONE persistent panel) ---------- */
+ .panel{
+ background:linear-gradient(180deg, var(--panel-edge), var(--panel));
+ border-top:1px solid var(--line);
+ border-radius:26px 26px 0 0;
+ padding:16px 20px 24px;
+ display:flex; flex-direction:column; gap:12px;
+ min-height:298px;
+ box-shadow:0 -18px 40px rgba(0,0,0,.35);
+ position:relative;
+ }
+ .panel-head{ display:flex; align-items:center; gap:12px; }
+ .home-btn{
+ flex:0 0 auto; width:56px; height:56px; border-radius:16px; background:#241A13;
+ border:2px solid var(--line); color:var(--muted); font-size:20px; display:flex;
+ align-items:center; justify-content:center; cursor:pointer;
+ }
+ .home-btn:focus-visible{ outline:3px solid #6FB8FF; outline-offset:2px; }
+ .panel-title{ font-family:'Spectral',serif; font-weight:600; font-size:21px; line-height:1.25; color:var(--ink); flex:1; }
+
+ .panel-body{ flex:1; display:flex; flex-direction:column; gap:12px; justify-content:center; }
+
+ button{ font-family:'Manrope',sans-serif; cursor:pointer; }
+ .pbtn{
+ width:100%; min-height:60px; border-radius:16px; font-size:17px; font-weight:800;
+ background:#241A13; border:2px solid var(--line); color:var(--ink); padding:10px 16px;
+ }
+ .pbtn-primary{ background:var(--accent); border-color:var(--accent); color:var(--accent-ink); }
+ .pbtn-secondary{ background:transparent; border-color:var(--line); color:var(--muted); }
+ .pbtn:focus-visible, .home-btn:focus-visible{ outline:3px solid #6FB8FF; outline-offset:2px; }
+
+ .chip-row{ display:flex; gap:10px; overflow-x:auto; padding:4px 2px 8px; scroll-snap-type:x proximity; }
+ .chip{
+ flex:0 0 auto; scroll-snap-align:start; min-height:56px; padding:0 20px; border-radius:999px;
+ background:#241A13; border:2px solid var(--line); color:var(--ink); font-size:15px; font-weight:700;
+ display:flex; align-items:center; white-space:nowrap;
+ }
+ .chip:focus-visible{ outline:3px solid #6FB8FF; outline-offset:2px; }
+
+ .shutter-wrap{ display:flex; flex-direction:column; align-items:center; justify-content:center; gap:10px; flex:1; }
+ .shutter{
+ width:84px; height:84px; border-radius:50%; background:var(--accent); border:5px solid #4A2C16;
+ font-size:30px; display:flex; align-items:center; justify-content:center; box-shadow:0 0 0 6px rgba(255,138,66,.16);
+ }
+ .shutter-caption{ font-size:13px; font-weight:700; color:var(--muted); letter-spacing:.03em; }
+
+ .linktext{
+ background:none; border:none; color:var(--muted); font-size:15px; text-decoration:underline;
+ text-align:center; min-height:56px; min-width:56px; width:100%;
+ }
+ .linktext:focus-visible{ outline:3px solid #6FB8FF; outline-offset:2px; }
+
+ /* pull-up find drawer tab, lives on the LIVE screen */
+ .drawer-tab{
+ width:100%; min-height:56px; border-radius:16px; background:transparent;
+ border:2px dashed var(--line); color:var(--muted); font-weight:800; font-size:15px;
+ display:flex; align-items:center; justify-content:center; gap:8px;
+ }
+ .drawer-tab .chev{ color:var(--accent); }
+
+ /* the vertical 3-option thumb picker (post-shot job wheel) */
+ .picker-hint{ text-align:center; font-size:12px; font-weight:800; letter-spacing:.08em; color:var(--muted); text-transform:uppercase; }
+ .picker-stack{ display:flex; flex-direction:column; gap:10px; touch-action:pan-y; }
+ .picker-card{
+ min-height:64px; width:100%; border-radius:16px; background:#241A13; border:2px solid var(--line);
+ display:flex; align-items:center; gap:14px; padding:0 18px; font-weight:800; font-size:16px; color:var(--ink);
+ text-align:left;
+ }
+ .picker-card .ic{ font-size:26px; }
+ .picker-card.likely{ border-color:var(--accent); background:linear-gradient(135deg, rgba(255,138,66,.20), #241A13); }
+ .picker-card.likely::after{ content:'MOST LIKELY'; margin-left:auto; font-size:10px; letter-spacing:.06em; color:var(--accent); font-weight:800; }
+ .picker-card:focus-visible{ outline:3px solid #6FB8FF; outline-offset:2px; }
+
+ /* hold-to-confirm ring, replaces every yes/no screen */
+ .hold-wrap{ display:flex; flex-direction:column; align-items:center; justify-content:center; gap:10px; flex:1; }
+ .hold-ring{
+ --pct:0%;
+ width:104px; height:104px; border-radius:50%; padding:6px; border:none;
+ background:conic-gradient(var(--accent) var(--pct), #2A1F17 var(--pct));
+ display:flex; align-items:center; justify-content:center; transition:box-shadow .15s;
+ }
+ .hold-ring.holding{ box-shadow:0 0 0 8px rgba(255,138,66,.14); }
+ .hold-ring-inner{
+ width:100%; height:100%; border-radius:50%; background:#18120E; border:2px solid var(--line);
+ display:flex; align-items:center; justify-content:center; text-align:center; padding:6px;
+ font-size:13px; font-weight:800; color:var(--ink); line-height:1.2;
+ }
+ .hold-caption{ font-size:13px; font-weight:700; color:var(--muted); letter-spacing:.02em; text-align:center; }
+
+ /* mini persistent shutter FAB — sits on the stage/panel seam in every non-idle state */
+ .mini-fab{
+ position:absolute; top:-28px; right:22px; width:64px; height:64px; border-radius:50%;
+ background:var(--accent); border:4px solid #4A2C16; color:var(--accent-ink); font-size:24px;
+ display:flex; align-items:center; justify-content:center; box-shadow:0 8px 18px rgba(0,0,0,.4); z-index:5;
+ }
+ .mini-fab:focus-visible{ outline:3px solid #6FB8FF; outline-offset:2px; }
+</style>
+</head>
+<body>
+<div class="phone">
+ <div class="stage" id="stage"></div>
+ <div class="panel" id="panel"></div>
+</div>
+
+<script>
+/* ---------------------------------------------------------------------
+ NEW MODEL: "shoot first, one-thumb confirm"
+ - Camera is ALWAYS live on the stage, in every state.
+ - The bottom panel is a single persistent thumb controller.
+ - Tap the shutter (big one on the idle screen, or the mini FAB seam
+ button everywhere else) from ANY state to take a shot.
+ - After a shot taken from idle, the panel morphs into a vertical
+ 3-option thumb PICKER (swipe up/down or tap) offering the most
+ likely job first, instead of a menu screen.
+ - Every yes/no decision is a HOLD-TO-CONFIRM ring (0.6s) instead of
+ a Yes/No screen.
+ - Find is a pull-up search drawer from the panel, not its own flow.
+--------------------------------------------------------------------- */
+
+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 JOB_META = {
+ new: {ic:'🆕', label:'Add New Item'},
+ update: {ic:'🔄', label:'Update an Item'},
+ sample: {ic:'📦', label:'Sample Came In'},
+};
+
+const state = {
+ screen:'live', // live | picker | new-brand | new-confirm |
+ // update-confirm | update-shoot | update-save |
+ // find-drawer | find-result | find-detail |
+ // sample-confirm | sample-printing
+ job:null, item:null, brand:null, findIndex:0,
+ pickerOrder:['new','update','sample'],
+ toast:null, toastTimer:null,
+};
+
+function go(screen){ state.screen = screen; render(); }
+
+function randomItem(){ return MOCK_ITEMS[Math.floor(Math.random()*MOCK_ITEMS.length)]; }
+
+/* the ONE shoot action — captures pretend data */
+function shoot(cb){ state.item = randomItem(); cb && cb(); }
+
+/* tapping the shutter from idle (or the persistent mini-FAB anywhere else)
+ always leads to the picker: shoot first, decide the job second */
+function mainShoot(){
+ shoot(()=>{
+ const likely = state.job || 'new';
+ state.pickerOrder = ['new','update','sample'];
+ state.pickerOrder.sort((a,b)=> (a===likely?-1:0) - (b===likely?-1:0));
+ go('picker');
+ });
+}
+
+function choosePicker(job){
+ state.job = job;
+ if(job==='new') go('new-brand');
+ else if(job==='update') go('update-confirm');
+ else if(job==='sample') go('sample-confirm');
+}
+function swipePicker(dir){
+ const o = state.pickerOrder;
+ if(dir>0) o.push(o.shift()); else o.unshift(o.pop());
+ render();
+}
+
+function chooseBrand(b){ state.brand = b; go('new-confirm'); }
+function completeNew(){ state.toast = {emoji:'🎉', text:'Saved!'}; state.job=null; go('live'); }
+
+function confirmUpdateMatch(){ go('update-shoot'); }
+function shootUpdate(){ shoot(()=> go('update-save')); }
+function completeUpdateSave(){ state.toast = {emoji:'✅', text:'Updated!'}; state.job=null; go('live'); }
+
+function openFindDrawer(){ go('find-drawer'); }
+function startFind(){ state.findIndex = 0; go('find-result'); }
+function nextFindResult(){ state.findIndex = (state.findIndex+1) % MOCK_ITEMS.length; render(); }
+function confirmFindMatch(){ go('find-detail'); }
+function updateFromFind(){ go('update-shoot'); }
+
+function confirmSampleReceived(){
+ go('sample-printing');
+ setTimeout(()=>{ state.toast = {emoji:'✅', text:'Received!'}; state.job=null; go('live'); }, 1100);
+}
+
+/* ---------- stage fragments (camera is always "live" behind everything) ---------- */
+function stageTopbar(){
+ return `<div class="stage-topbar"><span class="dw">DW</span><span class="live-dot"><span class="dot"></span>LIVE</span></div>`;
+}
+function stageLive(){
+ return `${stageTopbar()}
+ <div class="stage-body">
+ <div class="idle-orb">📷</div>
+ <div class="stage-hint">Ready when you are.</div>
+ </div>`;
+}
+function stageViewfinder(hint){
+ return `${stageTopbar()}
+ <div class="stage-body">
+ <div class="stage-hint">${hint}</div>
+ <div class="viewfinder">
+ <div class="corner tl"></div><div class="corner tr"></div>
+ <div class="corner bl"></div><div class="corner br"></div>
+ <span class="glyph">🖼️</span>
+ </div>
+ </div>`;
+}
+function stageCaptured(caption){
+ const it = state.item || MOCK_ITEMS[0];
+ return `${stageTopbar()}
+ <div class="stage-body">
+ <div class="swatch-card ${it.sw}"></div>
+ <div class="item-caption">
+ <p class="item-name">${it.name} — ${it.color}</p>
+ <p class="item-meta">${caption}</p>
+ </div>
+ </div>`;
+}
+function stageMessage(emoji, text, sub){
+ return `${stageTopbar()}
+ <div class="stage-body">
+ <div class="big-emoji">${emoji}</div>
+ <div class="stage-hint">${text}</div>
+ ${sub ? `<div class="stage-status">${sub}</div>` : ''}
+ </div>`;
+}
+function toastHTML(){
+ return state.toast ? `<div class="toast">${state.toast.emoji} ${state.toast.text}</div>` : '';
+}
+
+/* ---------- panel fragments ---------- */
+function homeBtn(){ return `<button class="home-btn" aria-label="Home" onclick="go('live')">⌂</button>`; }
+function panelHead(title){
+ return `<div class="panel-head">${state.screen!=='live' ? homeBtn() : ''}<div class="panel-title">${title}</div></div>`;
+}
+function miniFabHTML(){
+ return `<button class="mini-fab" aria-label="Take a new photo" onclick="mainShoot()">📸</button>`;
+}
+function holdRingHTML(id, label, sub){
+ return `<div class="hold-wrap">
+ <button type="button" class="hold-ring" id="${id}"><span class="hold-ring-inner">${label}</span></button>
+ <div class="hold-caption">${sub}</div>
+ </div>`;
+}
+function pickerCardHTML(job, isLikely){
+ const m = JOB_META[job];
+ return `<button class="picker-card ${isLikely?'likely':''}" onclick="choosePicker('${job}')">
+ <span class="ic">${m.ic}</span><span>${m.label}</span>
+ </button>`;
+}
+
+/* ---------- hold-to-confirm mechanics ---------- */
+function attachHold(btnId, duration, onComplete){
+ const btn = document.getElementById(btnId);
+ if(!btn) return;
+ let raf=null, startTs=null, done=false;
+ function frame(ts){
+ if(startTs===null) startTs = ts;
+ const pct = Math.min(1, (ts-startTs)/duration);
+ btn.style.setProperty('--pct', (pct*100)+'%');
+ if(pct>=1 && !done){ done=true; setTimeout(onComplete, 100); }
+ else if(!done){ raf = requestAnimationFrame(frame); }
+ }
+ function start(e){ e.preventDefault(); if(done) return; startTs=null; btn.classList.add('holding'); raf=requestAnimationFrame(frame); }
+ function cancel(){ if(done) return; if(raf) cancelAnimationFrame(raf); startTs=null; btn.classList.remove('holding'); btn.style.setProperty('--pct','0%'); }
+ btn.addEventListener('pointerdown', start);
+ btn.addEventListener('pointerup', cancel);
+ btn.addEventListener('pointerleave', cancel);
+ btn.addEventListener('pointercancel', cancel);
+}
+function wirePickerSwipe(){
+ const el = document.querySelector('.picker-stack');
+ if(!el) return;
+ let startY = null;
+ el.addEventListener('touchstart', e=>{ startY = e.touches[0].clientY; }, {passive:true});
+ el.addEventListener('touchend', e=>{
+ if(startY===null) return;
+ const dy = e.changedTouches[0].clientY - startY;
+ if(Math.abs(dy) > 24) swipePicker(dy<0 ? 1 : -1);
+ startY = null;
+ }, {passive:true});
+}
+function wireInteractive(){
+ if(state.screen==='picker') wirePickerSwipe();
+ if(state.screen==='new-confirm') attachHold('holdNew', 600, completeNew);
+ if(state.screen==='update-confirm') attachHold('holdMatch', 600, confirmUpdateMatch);
+ if(state.screen==='update-save') attachHold('holdUpdateSave', 600, completeUpdateSave);
+ if(state.screen==='find-result') attachHold('holdFind', 600, confirmFindMatch);
+ if(state.screen==='sample-confirm') attachHold('holdSample', 600, confirmSampleReceived);
+}
+
+/* ---------- render ---------- */
+function render(){
+ const stage = document.getElementById('stage');
+ const panel = document.getElementById('panel');
+ let s = '', p = '';
+
+ if(state.screen==='live'){
+ s = stageLive();
+ p = `${panelHead('Point & shoot')}
+ <div class="panel-body">
+ <div class="shutter-wrap">
+ <button class="shutter" aria-label="Take photo" onclick="mainShoot()">📸</button>
+ <div class="shutter-caption">Tap the shutter — we'll ask what it's for</div>
+ </div>
+ <button class="drawer-tab" onclick="openFindDrawer()">🔎 Find an item <span class="chev">▲</span></button>
+ </div>`;
+ }
+
+ else if(state.screen==='picker'){
+ s = stageCaptured('What was that for?');
+ p = `${panelHead("Got it — what's this for?")}
+ <div class="panel-body">
+ <div class="picker-hint">↕ swipe or tap to pick</div>
+ <div class="picker-stack">${state.pickerOrder.map((j,i)=>pickerCardHTML(j, i===0)).join('')}</div>
+ </div>`;
+ }
+
+ else if(state.screen==='new-brand'){
+ s = stageCaptured('New item spotted');
+ p = `${miniFabHTML()}${panelHead('Which brand is it?')}
+ <div class="panel-body">
+ <div class="chip-row">${BRANDS.map(b=>`<button class="chip" onclick="chooseBrand('${b}')">${b}</button>`).join('')}</div>
+ </div>`;
+ }
+ else if(state.screen==='new-confirm'){
+ s = stageCaptured(state.brand || 'Ready to save');
+ p = `${miniFabHTML()}${panelHead('Save this item?')}
+ <div class="panel-body">${holdRingHTML('holdNew', 'HOLD<br>TO SAVE', 'Press and hold 0.6s to confirm')}</div>`;
+ }
+
+ else if(state.screen==='update-confirm'){
+ s = stageCaptured('Is this the item?');
+ p = `${miniFabHTML()}${panelHead('Confirm the match')}
+ <div class="panel-body">
+ ${holdRingHTML('holdMatch', "HOLD —<br>YES", 'Hold if this is the right item')}
+ <button class="linktext" onclick="mainShoot()">Not this — reshoot</button>
+ </div>`;
+ }
+ else if(state.screen==='update-shoot'){
+ s = stageViewfinder('Point at the replacement, then tap shutter.');
+ p = `${panelHead('Shoot the new photo')}
+ <div class="panel-body">
+ <div class="shutter-wrap">
+ <button class="shutter" aria-label="Take photo" onclick="shootUpdate()">📸</button>
+ <div class="shutter-caption">Tap the shutter to capture</div>
+ </div>
+ </div>`;
+ }
+ else if(state.screen==='update-save'){
+ s = stageCaptured('Ready to save the update');
+ p = `${miniFabHTML()}${panelHead('Save the update?')}
+ <div class="panel-body">${holdRingHTML('holdUpdateSave', 'HOLD<br>TO SAVE', 'Press and hold 0.6s to confirm')}</div>`;
+ }
+
+ else if(state.screen==='find-drawer'){
+ s = stageLive();
+ p = `${panelHead('Find an item')}
+ <div class="panel-body">
+ <button class="pbtn" onclick="startFind()">📷 Scan the Label</button>
+ <button class="pbtn" onclick="startFind()">🎤 Say the Name</button>
+ <button class="pbtn" onclick="startFind()">⌨️ Type It</button>
+ </div>`;
+ }
+ else if(state.screen==='find-result'){
+ state.item = MOCK_ITEMS[state.findIndex];
+ s = stageCaptured('Is this the one?');
+ p = `${miniFabHTML()}${panelHead('Confirm the item')}
+ <div class="panel-body">
+ ${holdRingHTML('holdFind', "HOLD —<br>IT'S THIS", 'Hold to confirm this match')}
+ <button class="linktext" onclick="nextFindResult()">Show Next Result</button>
+ </div>`;
+ }
+ else if(state.screen==='find-detail'){
+ s = stageCaptured('Confirmed match');
+ p = `${panelHead(state.item.name)}
+ <div class="panel-body">
+ <button class="pbtn pbtn-primary" onclick="updateFromFind()">Update This</button>
+ <button class="pbtn pbtn-secondary" onclick="go('find-drawer')">Search Again</button>
+ </div>`;
+ }
+
+ else if(state.screen==='sample-confirm'){
+ s = stageCaptured('Mark this sample received?');
+ p = `${miniFabHTML()}${panelHead('Sample received?')}
+ <div class="panel-body">${holdRingHTML('holdSample', 'HOLD TO<br>CONFIRM', 'Hold to mark it received')}</div>`;
+ }
+ else if(state.screen==='sample-printing'){
+ s = stageMessage('🖨️', 'Printing your sticker…', 'One sec…');
+ p = `${panelHead('Printing')}<div class="panel-body"></div>`;
+ }
+
+ if(state.toast) s += toastHTML();
+
+ stage.innerHTML = s;
+ panel.innerHTML = p;
+ wireInteractive();
+
+ if(state.toast && !state.toastTimer){
+ state.toastTimer = setTimeout(()=>{ state.toast=null; state.toastTimer=null; render(); }, 1800);
+ }
+}
+
+render();
+</script>
+</body>
+</html>
diff --git a/public/kid-simple/v16-widget-stack.html b/public/kid-simple/v16-widget-stack.html
new file mode 100644
index 0000000..9f055a7
--- /dev/null
+++ b/public/kid-simple/v16-widget-stack.html
@@ -0,0 +1,364 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+<title>v16 — Widget 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=Cormorant+Garamond:wght@500;600;700&family=Manrope:wght@400;500;700;800&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --bg:#F4EEE4; --card:#FFFFFF; --ink:#2B241D; --muted:#8A7F6F; --line:#E7DFD1;
+ --rust:#BD5B34; --teal:#2F6259; --plum:#5B3A5E; --gold:#C79A46; --green:#3F8F5F;
+ }
+ *{box-sizing:border-box;}
+ html,body{margin:0;padding:0;background:#ddd;font-family:'Manrope',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:22px 20px 26px; overflow-y:auto; }
+ button{ font-family:'Manrope',sans-serif; cursor:pointer; }
+ h1,h2{ margin:0; }
+ .question{
+ font-family:'Cormorant Garamond',serif; font-weight:700; font-size:26px; line-height:1.18;
+ text-align:center; margin:0 0 14px; color:var(--ink);
+ }
+ .rows{ display:flex; flex-direction:column; gap:12px; margin-top:14px; }
+ .bigbtn{
+ width:100%; min-height:56px; border-radius:18px; font-size:17px; font-weight:700;
+ background:#fff; border:2px solid var(--line); color:var(--ink); padding:12px 18px;
+ }
+ .bigbtn-primary{ background:var(--accent,var(--gold)); border:2px solid var(--accent,var(--gold)); color:#fff; }
+ .bigbtn-secondary{ background:#fff; border:2px solid var(--muted); color:var(--muted); }
+ .bigbtn:focus-visible, .linktext:focus-visible, .widget:focus-visible, .navchip:focus-visible{ outline:3px solid #2B6CB0; outline-offset:2px; }
+ .linktext{
+ background:none; border:none; color:var(--muted); font-size:15.5px; text-decoration:underline;
+ text-align:center; min-height:56px; width:100%;
+ }
+
+ /* ---------- Home: widget stack (collapsed) ---------- */
+ .topbar{ display:flex; align-items:baseline; justify-content:space-between; padding:6px 2px 18px; }
+ .dw{ font-family:'Cormorant Garamond',serif; font-weight:700; font-size:20px; letter-spacing:.02em; }
+ .today{ font-size:12px; color:var(--muted); font-weight:600; }
+ .widgetstack{ display:flex; flex-direction:column; gap:14px; }
+ .widget{
+ background:var(--card); border-radius:26px; padding:18px 18px 16px; box-shadow:0 8px 22px rgba(43,36,29,.08);
+ border:1px solid var(--line); text-align:left; transition:transform .12s ease; -webkit-tap-highlight-color:transparent;
+ }
+ .widget:active{ transform:scale(.98); }
+ .widget-top{ display:flex; align-items:center; justify-content:space-between; margin-bottom:8px; }
+ .chip{
+ width:44px; height:44px; border-radius:14px; background:var(--accent); color:#fff; font-size:21px;
+ display:flex; align-items:center; justify-content:center; flex:none;
+ }
+ .widget-num{ font-family:'Cormorant Garamond',serif; font-weight:700; font-size:38px; color:var(--ink); }
+ .widget-thumb{ width:44px; height:44px; border-radius:12px; flex:none; }
+ .widget-label{ font-size:16px; font-weight:800; margin:2px 0 2px; }
+ .widget-sub{ font-size:13.5px; color:var(--muted); margin-bottom:14px; }
+ .widget-btn{
+ width:100%; min-height:56px; border-radius:14px; border:none; background:var(--accent); color:#fff;
+ font-size:15.5px; font-weight:800; letter-spacing:.01em;
+ }
+
+ .sw1{background:repeating-linear-gradient(45deg,#BD5B34 0 16px,#a04b29 16px 32px);}
+ .sw2{background:repeating-linear-gradient(-45deg,#2F6259 0 14px,#254f47 14px 28px);}
+ .sw3{background:repeating-linear-gradient(90deg,#5B3A5E 0 14px,#48304a 14px 28px);}
+ .swatch{ width:100%; height:120px; border-radius:16px; margin-bottom:10px; }
+ .swatch.small{ height:90px; }
+
+ .itemname{ font-size:18px; font-weight:800; margin:0; text-align:center; }
+ .itemmeta{ font-size:14.5px; color:var(--muted); margin:4px 0 0; text-align:center; }
+
+ /* ---------- Expand-in-place: chip row for the OTHER widgets ---------- */
+ .chiprow{ display:flex; gap:10px; margin-bottom:14px; }
+ .navchip{
+ flex:1; min-height:56px; min-width:56px; border-radius:16px; background:#fff;
+ border:2px solid var(--line); border-top:4px solid var(--accent,var(--gold));
+ display:flex; flex-direction:column; align-items:center; justify-content:center; gap:2px;
+ font-size:11px; font-weight:800; color:var(--ink); padding:6px 4px;
+ }
+ .navchip .navicon{ font-size:19px; line-height:1; }
+
+ /* ---------- The expanded widget card — job happens IN HERE ---------- */
+ .expandedcard{
+ background:var(--card); border-radius:26px; padding:18px 18px 20px; box-shadow:0 12px 30px rgba(43,36,29,.14);
+ border:2px solid var(--accent,var(--gold)); flex:1; display:flex; flex-direction:column;
+ }
+ .expandedcard .widget-top{ margin-bottom:6px; }
+ .dots{ display:flex; gap:6px; margin-left:auto; align-items:center; }
+ .dot{ width:8px; height:8px; border-radius:50%; background:var(--line); }
+ .dot.active{ background:var(--accent,var(--gold)); }
+ .stepbody{ margin-top:8px; display:flex; flex-direction:column; flex:1; }
+ .miniCenter{ display:flex; flex-direction:column; align-items:center; justify-content:center; flex:1; gap:8px; text-align:center; }
+
+ /* inline camera viewport, lives INSIDE the card (not full-bleed) */
+ .camview{
+ background:#1c1c1e; position:relative; display:flex; align-items:center; justify-content:center;
+ color:#fff; min-height:200px; border-radius:20px; overflow:hidden; margin-bottom:4px;
+ }
+ .camview .hint{
+ position:absolute; top:18px; left:14px; right:14px; text-align:center;
+ font-family:'Cormorant Garamond',serif; font-size:20px; font-weight:700; color:#fdfbf7;
+ }
+ .camview .status{ font-size:15px; font-weight:700; color:#f0e8d8; }
+ .shutter{
+ position:absolute; bottom:16px; left:50%; transform:translateX(-50%);
+ width:72px; height:72px; border-radius:50%; background:var(--accent,var(--gold)); border:5px solid #fff;
+ font-size:26px; display:flex; align-items:center; justify-content:center;
+ }
+ .altrow{ display:flex; gap:10px; margin-top:12px; }
+ .altrow .linktext{ flex:1; border:2px solid var(--line); border-radius:14px; font-size:13.5px; text-decoration:none; font-weight:700; }
+ .brandrows{ display:flex; flex-direction:column; gap:10px; max-height:280px; overflow-y:auto; padding-right:2px; }
+</style>
+</head>
+<body>
+<div class="phone">
+ <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 ACCENT = { new:'#C79A46', update:'#2F6259', find:'#5B3A5E', sample:'#BD5B34', home:'#8A7F6F' };
+const JOB_META = {
+ sample:{ icon:'📦', short:'Samples', title:'Log a Sample' },
+ update:{ icon:'🔄', short:'Updates', title:'Update an Item' },
+ new: { icon:'🆕', short:'New', title:'Add New Item' },
+ find: { icon:'🔍', short:'Find', title:'Find an Item' },
+};
+// Card-local step lists, per job — each job is a stack of IN-CARD steps, not full screens.
+const STEPS = {
+ sample:['cam','confirm','printing','done'],
+ update:['cam','confirm','cam2','done'],
+ new: ['cam','brand','save','done'],
+ find: ['cam','result','detail'],
+};
+
+let counts = { sample:3, update:2 };
+let lastAdded = { ...MOCK_ITEMS[0] };
+
+// state.expanded = which widget card is currently open in place (or null = home stack).
+// state.step = the CARD-LOCAL step within that widget. Nothing here is a full screen.
+const state = { expanded:null, step:null, item:null, brand:null, resultIndex:0, exhausted:false };
+
+function accentFor(job){ return ACCENT[job] || '#C79A46'; }
+function randomItem(){ return MOCK_ITEMS[Math.floor(Math.random()*MOCK_ITEMS.length)]; }
+
+function openWidget(job){
+ state.expanded = job; state.item = null; state.brand = null; state.resultIndex = 0; state.exhausted = false;
+ state.step = 'cam'; // every job's first in-card step is always the camera
+ render();
+}
+function switchWidget(job){ openWidget(job); }
+function closeWidget(){ state.expanded = null; state.step = null; render(); }
+function go(step){ state.step = step; render(); }
+
+function shootInline(cb){
+ const st = document.getElementById('camStatus');
+ const btn = document.querySelector('.shutter');
+ if(btn) btn.disabled = true;
+ if(st) st.textContent = 'Snapping…';
+ setTimeout(()=>{ state.item = state.item || randomItem(); cb(); }, 700);
+}
+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="Take photo" onclick="${onShoot}">📸</button>
+ </div>`;
+}
+function dots(job){
+ const steps = STEPS[job]; const idx = steps.indexOf(state.step);
+ return `<div class="dots">${steps.map((s,i)=>`<span class="dot${i===idx?' active':''}"></span>`).join('')}</div>`;
+}
+
+/* ---------- job bodies: the job happens INSIDE the expanded card ---------- */
+function sampleBody(){
+ if(state.step==='cam') return cameraCard('Scan the box label.', "shootInline(()=>go('confirm'))");
+ if(state.step==='confirm') return `
+ <p class="itemname">${state.item.name} — ${state.item.color}</p>
+ <div class="swatch small ${state.item.sw}"></div>
+ <p class="itemmeta">code ${state.item.code}</p>
+ <div class="rows"><button class="bigbtn bigbtn-primary" onclick="go('printing')">Mark Received</button></div>`;
+ if(state.step==='printing') return `<div class="miniCenter"><p class="itemmeta">🖨️ Printing your sticker…</p></div>`;
+ if(state.step==='done'){
+ if(counts.sample>0) return `
+ <div class="miniCenter"><p class="itemname">Logged! ✅</p><p class="itemmeta">Samples Waiting: ${counts.sample}</p></div>
+ <div class="rows">
+ <button class="bigbtn bigbtn-primary" onclick="openWidget('sample')">Next sample (${counts.sample} left) →</button>
+ <button class="linktext" onclick="closeWidget()">Close</button>
+ </div>`;
+ return `
+ <div class="miniCenter"><p class="itemname">All caught up! 🎉</p><p class="itemmeta">No samples waiting.</p></div>
+ <div class="rows"><button class="bigbtn bigbtn-primary" onclick="closeWidget()">Close</button></div>`;
+ }
+}
+function finishSample(){ counts.sample = Math.max(0, counts.sample-1); state.step='done'; render(); }
+
+function updateBody(){
+ if(state.step==='cam') return cameraCard('Scan the label or pattern.', "shootInline(()=>go('confirm'))");
+ if(state.step==='confirm') return `
+ <p class="itemname">${state.item.name} — ${state.item.color}</p>
+ <div class="swatch small ${state.item.sw}"></div>
+ <p class="itemmeta">code ${state.item.code}</p>
+ <div class="rows">
+ <button class="bigbtn bigbtn-primary" onclick="go('cam2')">Yes, That's It</button>
+ <button class="bigbtn bigbtn-secondary" onclick="state.item=null; go('cam')">No, Scan Again</button>
+ </div>`;
+ if(state.step==='cam2') return cameraCard('Take the new photo.', "shootInline(()=>finishUpdate())");
+ if(state.step==='done') return `
+ <div class="miniCenter"><p class="itemname">Updated! ✅</p><p class="itemmeta">Need Updates: ${counts.update} left</p></div>
+ <div class="rows"><button class="bigbtn bigbtn-primary" onclick="closeWidget()">Close</button></div>`;
+}
+function finishUpdate(){ counts.update = Math.max(0, counts.update-1); state.step='done'; render(); }
+
+function newBody(){
+ if(state.step==='cam') return cameraCard('Take a photo of it.', "shootInline(()=>go('brand'))");
+ if(state.step==='brand') return `
+ <p class="question" style="font-size:20px;">Which brand?</p>
+ <div class="brandrows">${BRANDS.map(b=>`<button class="bigbtn" onclick="state.brand='${b}'; go('save')">${b}</button>`).join('')}</div>`;
+ if(state.step==='save') return `
+ <div class="swatch small ${state.item.sw}"></div>
+ <p class="itemmeta">${state.brand} · code ${state.item.code}</p>
+ <div class="rows">
+ <button class="bigbtn bigbtn-primary" onclick="finishNew()">Save as Draft</button>
+ <button class="linktext" onclick="go('cam')">Retake Photo</button>
+ </div>`;
+ if(state.step==='done') return `
+ <div class="miniCenter"><p class="itemname">Saved! 🎉</p><p class="itemmeta">${lastAdded.name} · ${lastAdded.color}</p></div>
+ <div class="rows">
+ <button class="bigbtn bigbtn-primary" onclick="openWidget('new')">Add Another</button>
+ <button class="linktext" onclick="closeWidget()">Close</button>
+ </div>`;
+}
+function finishNew(){ lastAdded = { ...state.item, brand: state.brand }; state.step='done'; render(); }
+
+function findBody(){
+ if(state.step==='cam') return `
+ ${cameraCard('Scan the label to find it.', "shootInline(()=>go('result'))")}
+ <div class="altrow">
+ <button class="linktext" onclick="state.item=randomItem(); go('result')">🎤 Say the Name</button>
+ <button class="linktext" onclick="state.item=randomItem(); go('result')">⌨️ Type It</button>
+ </div>`;
+ if(state.step==='result'){
+ if(state.exhausted) return `
+ <div class="miniCenter"><p class="itemname">That's everything.</p><p class="itemmeta">Try another way?</p></div>
+ <div class="altrow" style="margin-top:6px;">
+ <button class="linktext" onclick="state.exhausted=false; state.resultIndex=0; go('cam')">📷 Scan</button>
+ <button class="linktext" onclick="state.exhausted=false; state.resultIndex=0; state.item=randomItem(); go('result')">🎤 Say</button>
+ <button class="linktext" onclick="state.exhausted=false; state.resultIndex=0; state.item=randomItem(); go('result')">⌨️ Type</button>
+ </div>`;
+ const it = MOCK_ITEMS[state.resultIndex];
+ return `
+ <p class="itemname">${it.name} — ${it.color}</p>
+ <div class="swatch small ${it.sw}"></div>
+ <p class="itemmeta">code ${it.code}</p>
+ <div class="rows">
+ <button class="bigbtn bigbtn-primary" onclick="pickResult()">Yes, This One</button>
+ <button class="bigbtn bigbtn-secondary" onclick="nextResult()">Next Result</button>
+ </div>`;
+ }
+ if(state.step==='detail') return `
+ <div class="swatch small ${state.item.sw}"></div>
+ <p class="itemname">${state.item.name}</p>
+ <p class="itemmeta">${state.item.color} · code ${state.item.code}</p>
+ <div class="rows">
+ <button class="bigbtn bigbtn-primary" onclick="openWidgetForUpdateFromFind(state.item)">Update This</button>
+ <button class="bigbtn bigbtn-secondary" onclick="state.resultIndex=0; state.exhausted=false; go('cam')">Search Again</button>
+ <button class="linktext" onclick="closeWidget()">Close</button>
+ </div>`;
+}
+function pickResult(){ state.item = MOCK_ITEMS[state.resultIndex]; go('detail'); }
+function nextResult(){
+ if(state.resultIndex < MOCK_ITEMS.length-1){ state.resultIndex++; render(); }
+ else { state.exhausted = true; render(); }
+}
+// Cross-job jump: Find already knows the item, so Update skips straight to its "new photo" step.
+function openWidgetForUpdateFromFind(item){
+ state.expanded='update'; state.item=item; state.brand=null; state.resultIndex=0; state.exhausted=false;
+ state.step='cam2'; render();
+}
+
+function stepBody(){
+ if(state.expanded==='sample') return sampleBody();
+ if(state.expanded==='update') return updateBody();
+ if(state.expanded==='new') return newBody();
+ if(state.expanded==='find') return findBody();
+ return '';
+}
+
+function chipRowHtml(){
+ const jobs = ['sample','update','new','find'].filter(j=>j!==state.expanded);
+ return `<div class="chiprow">
+ <button class="navchip" style="--accent:${accentFor('home')}" onclick="closeWidget()"><span class="navicon">⌂</span><span>Home</span></button>
+ ${jobs.map(j=>`<button class="navchip" style="--accent:${accentFor(j)}" onclick="switchWidget('${j}')"><span class="navicon">${JOB_META[j].icon}</span><span>${JOB_META[j].short}</span></button>`).join('')}
+ </div>`;
+}
+function expandedCardHtml(){
+ return `<div class="expandedcard" style="--accent:${accentFor(state.expanded)}">
+ <div class="widget-top"><div class="chip">${JOB_META[state.expanded].icon}</div>${dots(state.expanded)}</div>
+ <div class="widget-label">${JOB_META[state.expanded].title}</div>
+ <div class="stepbody">${stepBody()}</div>
+ </div>`;
+}
+
+function homeHtml(){
+ return `
+ <div class="topbar"><span class="dw">Designer Wallcoverings</span><span class="today">Today · Updated now</span></div>
+ <div class="widgetstack">
+ <div class="widget widget-lg" style="--accent:${ACCENT.sample}" tabindex="0" role="button" onclick="openWidget('sample')">
+ <div class="widget-top"><div class="chip">📦</div><div class="widget-num">${counts.sample}</div></div>
+ <div class="widget-label">Samples Waiting</div>
+ <div class="widget-sub">Ready to scan & log</div>
+ <button class="widget-btn" onclick="event.stopPropagation(); openWidget('sample')">Log a Sample →</button>
+ </div>
+
+ <div class="widget widget-md" style="--accent:${ACCENT.update}" tabindex="0" role="button" onclick="openWidget('update')">
+ <div class="widget-top"><div class="chip">🔄</div><div class="widget-num">${counts.update}</div></div>
+ <div class="widget-label">Need Updates</div>
+ <div class="widget-sub">New photos needed</div>
+ <button class="widget-btn" onclick="event.stopPropagation(); openWidget('update')">Update an Item →</button>
+ </div>
+
+ <div class="widget widget-md" style="--accent:${ACCENT.new}" tabindex="0" role="button" onclick="openWidget('new')">
+ <div class="widget-top"><div class="chip">🆕</div><div class="widget-thumb ${lastAdded.sw}"></div></div>
+ <div class="widget-label">Last Added</div>
+ <div class="widget-sub">${lastAdded.name} · ${lastAdded.color}</div>
+ <button class="widget-btn" onclick="event.stopPropagation(); openWidget('new')">Add New Item →</button>
+ </div>
+
+ <div class="widget widget-sm" style="--accent:${ACCENT.find}" tabindex="0" role="button" onclick="openWidget('find')">
+ <div class="widget-top"><div class="chip">🔍</div></div>
+ <div class="widget-label">Find an Item</div>
+ <div class="widget-sub">Search everything you've added</div>
+ <button class="widget-btn" onclick="event.stopPropagation(); openWidget('find')">Find an Item →</button>
+ </div>
+ </div>`;
+}
+
+function render(){
+ const s = document.getElementById('screen');
+ s.style.setProperty('--accent', accentFor(state.expanded || 'home'));
+ let html = '';
+ if(!state.expanded){
+ html = homeHtml();
+ } else {
+ html = chipRowHtml() + expandedCardHtml();
+ }
+ s.innerHTML = html;
+ if(state.expanded==='sample' && state.step==='printing'){
+ setTimeout(()=>{ if(state.expanded==='sample' && state.step==='printing'){ finishSample(); } }, 900);
+ }
+}
+
+render();
+</script>
+</body>
+</html>
diff --git a/public/kid-simple/v2-four-tiles.html b/public/kid-simple/v2-four-tiles.html
new file mode 100644
index 0000000..3ad12c1
--- /dev/null
+++ b/public/kid-simple/v2-four-tiles.html
@@ -0,0 +1,588 @@
+<!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:#9C6B1F;
+ --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:8px 22px 22px;display:flex;flex-direction:column;gap:16px;flex:1;overflow-y:auto;}
+ .home-title{text-align:center;font-size:24px;margin:24px 0 0;}
+ .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:#fff;cursor:pointer;font-size:18px;font-weight:700;
+ text-align:center;padding:12px;border:none;
+ }
+ .tile:active, .btn:active, .thumb-card:active, .brand-tile:active, .nav-btn:active{transform:scale(.97);}
+ .tile:focus-visible, button:focus-visible, .thumb-card:focus-visible, .brand-tile:focus-visible, input:focus-visible{outline:3px solid #2B2B2B;outline-offset:3px;}
+ .btn:disabled{opacity:.4;cursor:not-allowed;}
+ .topbar{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:14px 16px;flex-shrink:0;z-index:6;}
+ .nav-btn{
+ min-height:56px;min-width:56px;border-radius:14px;border:2px solid #ddd4c2;background:#fff;
+ font-size:17px;font-weight:600;cursor:pointer;color:var(--text);padding:0 16px;
+ display:flex;align-items:center;gap:6px;font-family:'DM Sans',sans-serif;
+ }
+ .camera-screen .nav-btn{background:rgba(255,255,255,.92);}
+ .step-indicator{padding:0 22px;font-size:16px;opacity:.7;flex-shrink:0;}
+ .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:80px;left:0;right:0;text-align:center;font-size:17px;z-index:2;}
+ .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;
+ }
+ .demo-toggle{position:absolute;bottom:150px;left:0;right:0;text-align:center;z-index:2;
+ font-size:14px;color:#fff;opacity:.75;display:flex;align-items:center;justify-content:center;gap:8px;}
+ .demo-toggle input{width:18px;height:18px;}
+
+ .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:#fff;}
+ .btn-update{background:var(--update);color:#fff;}
+ .btn-find{background:var(--find);color:#fff;}
+ .btn-sample{background:var(--sample);color:#fff;}
+ .btn-secondary{background:#fff;color:var(--text);border:2px solid #ddd4c2;}
+
+ .actions{margin-top:auto;display:flex;flex-direction:column-reverse;gap:12px;padding-top:16px;}
+ .search-bar{background:#fff;border-radius:14px;padding:0 16px;border:2px solid #e6dfd2;display:flex;align-items:center;}
+ .search-bar input{border:none;background:none;font-size:17px;color:var(--text);padding:14px 6px;width:100%;font-family:'DM Sans',sans-serif;}
+ .search-bar input::placeholder{color:#6B6156;}
+ .section-label{font-size:14px;opacity:.65;text-transform:uppercase;letter-spacing:.5px;}
+ .brand-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;}
+ .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);}
+ .brand-tile.hidden{display:none;}
+
+ .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;}
+ .thumb{width:96px;height:96px;border-radius:12px;flex-shrink:0;
+ background:repeating-linear-gradient(45deg,#8FAE8B 0 16px,#B9CBB2 16px 32px),linear-gradient(135deg,#8FAE8B,#6f8d6c);
+ background-blend-mode:multiply;}
+ .item-title{font-size:20px;font-weight:700;}
+ .item-sub{font-size:17px;color:#6B6156;}
+
+ .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;min-height:56px;
+ }
+ .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;}
+ .done-row{display:flex;gap:14px;align-items:center;}
+ .done-facts{font-size:17px;line-height:1.5;}
+ .done-facts strong{font-size:18px;}
+ .toast{
+ position:absolute;left:16px;right:16px;bottom:24px;background:#2B2B2B;color:#fff;
+ border-radius:14px;padding:14px 18px;display:none;align-items:center;justify-content:space-between;
+ gap:12px;box-shadow:0 8px 24px rgba(0,0,0,.35);z-index:20;font-size:17px;border:2px solid var(--sample);
+ }
+ .toast.show{display:flex;}
+ .toast button{background:none;border:none;color:#F4D48A;font-weight:700;font-size:17px;cursor:pointer;min-height:56px;min-width:56px;padding:0 6px;}
+ @media (prefers-reduced-motion: reduce){
+ *{transition:none !important;}
+ }
+</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="go('addCamera')"><span class="icon">🆕</span>Add New Item</button>
+ <button class="tile update" onclick="go('updateCamera1')"><span class="icon">🔄</span>Update an Item</button>
+ <button class="tile find" onclick="go('findMethods')"><span class="icon">🔍</span>Find an Item</button>
+ <button class="tile sample" onclick="go('sampleCamera')"><span class="icon">📦</span>A Sample Came In</button>
+ </div>
+ </div>
+
+ <!-- ADD: CAMERA -->
+ <div class="screen camera-screen" id="addCamera">
+ <div class="topbar"><button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button><button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button></div>
+ <div class="hint">Take a photo of the sample</div>
+ <div class="viewfinder"></div>
+ <div class="shutter-wrap"><button class="shutter" onclick="showPhotoCheck('addBrand')" aria-label="Shoot photo">📸</button></div>
+ </div>
+
+ <!-- PHOTO CHECK (generic) -->
+ <div class="screen center-flex" id="photoCheck">
+ <div class="topbar"><button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button><button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button></div>
+ <div class="pad center-flex">
+ <div class="thumb" id="photoCheckThumb" style="width:200px;height:200px;"></div>
+ <p style="font-size:18px;" aria-live="polite">Looks good?</p>
+ <div class="actions">
+ <button class="btn btn-primary" onclick="photoLooksGood()">Looks good</button>
+ <button class="btn btn-secondary" onclick="goBack()">Retake</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- ADD: BRAND GRID -->
+ <div class="screen" id="addBrand">
+ <div class="topbar"><button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button><button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button></div>
+ <div class="pad">
+ <h3>Which brand?</h3>
+ <div class="search-bar"><input type="text" id="brandSearch" placeholder="Search brands…" oninput="filterBrands()" aria-label="Search brands"></div>
+ <div class="section-label">Recent</div>
+ <div class="brand-grid" id="recentBrands">
+ <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,'Kravet')" onkeydown="onBrandKey(event,this,'Kravet')">Kravet</div>
+ <div class="brand-tile" tabindex="0" role="button" onclick="selectBrand(this,'Thibaut')" onkeydown="onBrandKey(event,this,'Thibaut')">Thibaut</div>
+ </div>
+ <div class="section-label">All Brands</div>
+ <div class="brand-grid" id="allBrands">
+ <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,'Cole & Son')" onkeydown="onBrandKey(event,this,'Cole & Son')">Cole & Son</div>
+ <div class="brand-tile" tabindex="0" role="button" onclick="selectBrand(this,'Mulberry')" onkeydown="onBrandKey(event,this,'Mulberry')">Mulberry</div>
+ </div>
+ <div class="actions">
+ <button class="btn btn-primary" id="saveDraftBtn" disabled onclick="saveDraft()">Save as Draft</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- GENERIC DONE -->
+ <div class="screen center-flex" id="doneScreen">
+ <div class="topbar"><button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button><button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button></div>
+ <div class="pad">
+ <div class="done-row">
+ <div class="thumb" id="doneThumb"></div>
+ <div class="done-facts">
+ <strong id="doneTitle"></strong><br>
+ code <span id="doneCode"></span> · <span id="doneBrand"></span><br>
+ <span id="doneAction"></span>
+ </div>
+ </div>
+ <div class="actions">
+ <button class="btn btn-primary" id="doneRepeatBtn" onclick="goHome()">Do Another</button>
+ <button class="btn btn-secondary" onclick="goHome()">Home</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- UPDATE: CAMERA 1 -->
+ <div class="screen camera-screen" id="updateCamera1">
+ <div class="topbar"><button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button><button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button></div>
+ <div class="step-indicator" style="color:#fff;opacity:.75;">Step 1 of 2</div>
+ <div class="hint">Take a photo of the label or pattern</div>
+ <label class="demo-toggle"><input type="checkbox" id="demoToggle" onchange="setDemoToggle(this.checked)"> Demo: show "no match"</label>
+ <div class="viewfinder"></div>
+ <div class="shutter-wrap"><button class="shutter" onclick="showPhotoCheck('afterUpdateShot')" aria-label="Shoot photo">📸</button></div>
+ </div>
+
+ <!-- UPDATE: MATCH CARD -->
+ <div class="screen" id="updateMatch">
+ <div class="topbar"><button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button><button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button></div>
+ <div class="step-indicator">Step 1 of 2</div>
+ <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 · York</div>
+ </div>
+ <div class="actions">
+ <button class="btn btn-update" onclick="go('updateCamera2')">Yes, this one</button>
+ <button class="btn btn-secondary" onclick="go('updateTyping')">Search by typing instead</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- UPDATE: TYPE SEARCH FALLBACK -->
+ <div class="screen" id="updateTyping">
+ <div class="topbar"><button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button><button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button></div>
+ <div class="pad">
+ <h2>Type the item name or code</h2>
+ <div class="search-bar"><input type="text" id="updateTypeInput" placeholder="Palm Damask…" aria-label="Type the item name or code"></div>
+ <div class="actions">
+ <button class="btn btn-update" onclick="go('updateCamera2')">Found It</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- UPDATE: NO MATCH -->
+ <div class="screen center-flex" id="updateNoMatch">
+ <div class="topbar"><button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button><button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button></div>
+ <div class="pad center-flex">
+ <div class="big-emoji">🤔</div>
+ <h2 aria-live="polite">No match found</h2>
+ <div class="actions">
+ <button class="btn btn-update" onclick="go('updateCamera1')">Try again</button>
+ <button class="btn btn-secondary" onclick="go('updateTyping')">Type the code instead</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- UPDATE: CAMERA 2 -->
+ <div class="screen camera-screen" id="updateCamera2">
+ <div class="topbar"><button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button><button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button></div>
+ <div class="step-indicator" style="color:#fff;opacity:.75;">Step 2 of 2</div>
+ <div class="hint">Take the new photo</div>
+ <div class="viewfinder"></div>
+ <div class="shutter-wrap"><button class="shutter" onclick="showPhotoCheck('updateSave')" aria-label="Shoot new photo">📸</button></div>
+ </div>
+
+ <!-- FIND: METHODS -->
+ <div class="screen" id="findMethods">
+ <div class="topbar"><button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button><button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button></div>
+ <div class="pad">
+ <h3>How do you want to find it?</h3>
+ <button class="btn btn-find" onclick="findScan()">📷 Scan the Label</button>
+ <button class="btn btn-find" onclick="go('findResults')">🎤 Say the Name</button>
+ <button class="btn btn-find" onclick="go('findResults')">⌨️ Type It</button>
+ <label style="font-size:14px;opacity:.7;display:flex;align-items:center;gap:8px;margin-top:8px;">
+ <input type="checkbox" id="demoToggleFind" onchange="setDemoToggle(this.checked)" style="width:18px;height:18px;"> Demo: show "no match"
+ </label>
+ </div>
+ </div>
+
+ <!-- FIND: SCAN CAMERA -->
+ <div class="screen camera-screen" id="findCamera">
+ <div class="topbar"><button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button><button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button></div>
+ <div class="hint">Scan the label</div>
+ <div class="viewfinder"></div>
+ <div class="shutter-wrap"><button class="shutter" onclick="showPhotoCheck('findResults')" aria-label="Scan label">📸</button></div>
+ </div>
+
+ <!-- FIND: RESULTS -->
+ <div class="screen" id="findResults">
+ <div class="topbar"><button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button><button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button></div>
+ <div class="pad">
+ <h3 aria-live="polite">Results</h3>
+ <div class="thumb-list" id="findResultsList"></div>
+ <div id="findNoResults" style="display:none;" class="center-flex">
+ <div class="big-emoji">🤔</div>
+ <h2>No match found</h2>
+ <div class="actions">
+ <button class="btn btn-find" onclick="go('findMethods')">Try again</button>
+ <button class="btn btn-secondary" onclick="go('findTyping')">Type the code instead</button>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- FIND: TYPE CODE (no-match fallback) -->
+ <div class="screen" id="findTyping">
+ <div class="topbar"><button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button><button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button></div>
+ <div class="pad">
+ <h3>Type the item's code</h3>
+ <div class="search-bar"><input type="text" placeholder="e.g. DW-10423" aria-label="Type the item code"></div>
+ <div class="actions">
+ <button class="btn btn-find" onclick="go('findDetail')">Find It</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- FIND: DETAIL -->
+ <div class="screen" id="findDetail">
+ <div class="topbar"><button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button><button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button></div>
+ <div class="pad">
+ <div class="card">
+ <div class="swatch"></div>
+ <div class="item-title" id="findDetailTitle">Palm Damask — Sage</div>
+ <div class="item-sub" id="findDetailSub">code DW-10423 · York</div>
+ </div>
+ <div class="actions">
+ <button class="btn btn-update" onclick="go('updateCamera2')">Update This</button>
+ <button class="btn btn-secondary" onclick="go('findMethods')">Back to Search</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- SAMPLE: CAMERA -->
+ <div class="screen camera-screen" id="sampleCamera">
+ <div class="topbar"><button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button><button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button></div>
+ <div class="hint">Scan the label on the box</div>
+ <label class="demo-toggle"><input type="checkbox" id="demoToggleSample" onchange="setDemoToggle(this.checked)"> Demo: show "no match"</label>
+ <div class="viewfinder"></div>
+ <div class="shutter-wrap"><button class="shutter" onclick="showPhotoCheck('afterSampleShot')" aria-label="Scan box">📸</button></div>
+ </div>
+
+ <!-- SAMPLE: MATCH -->
+ <div class="screen" id="sampleMatch">
+ <div class="topbar"><button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button><button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button></div>
+ <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 · York</div>
+ </div>
+ <div class="actions">
+ <button class="btn btn-sample" onclick="markReceived()">Yes, Mark Received</button>
+ <button class="btn btn-secondary" onclick="go('sampleNoMatch')">Not this item</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- SAMPLE: NO MATCH -->
+ <div class="screen center-flex" id="sampleNoMatch">
+ <div class="topbar"><button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button><button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button></div>
+ <div class="pad center-flex">
+ <div class="big-emoji">🤔</div>
+ <h2 aria-live="polite">No match found</h2>
+ <div class="actions">
+ <button class="btn btn-sample" onclick="go('sampleCamera')">Try again</button>
+ <button class="btn btn-secondary" onclick="go('sampleTyping')">Type the code instead</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- SAMPLE: TYPE CODE -->
+ <div class="screen" id="sampleTyping">
+ <div class="topbar"><button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button><button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button></div>
+ <div class="pad">
+ <h3>Type the item's code</h3>
+ <div class="search-bar"><input type="text" placeholder="e.g. DW-10423" aria-label="Type the item code"></div>
+ <div class="actions">
+ <button class="btn btn-sample" onclick="markReceived()">Find It</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- SAMPLE: PRINTING -->
+ <div class="screen center-flex camera-screen" id="samplePrinting">
+ <div class="topbar"><button class="nav-btn" onclick="goBack()" aria-label="Back">‹ Back</button><button class="nav-btn" onclick="goHome()" aria-label="Home">⌂ Home</button></div>
+ <div class="pad center-flex">
+ <div class="big-emoji">🖨️</div>
+ <p style="font-size:18px;" aria-live="polite">Printing sticker…</p>
+ </div>
+ </div>
+
+</div>
+
+<!-- TOAST (undo) -->
+<div class="toast" id="toast" role="status" aria-live="polite">
+ <span id="toastText">Saved</span>
+ <button onclick="undoAction()">Undo</button>
+</div>
+
+<script>
+ let historyStack = ['home'];
+ let selectedBrand = null;
+ let demoNoMatch = false;
+ let photoCheckNext = null;
+ let toastTimer = null;
+ let undoTarget = null;
+ let findResultCounter = 0;
+
+ function render(id){
+ document.querySelectorAll('.screen').forEach(s=>s.classList.remove('active'));
+ document.getElementById(id).classList.add('active');
+ }
+
+ function go(id){
+ historyStack.push(id);
+ render(id);
+ onEnter(id);
+ }
+
+ function goBack(){
+ if(historyStack.length > 1){
+ historyStack.pop();
+ render(historyStack[historyStack.length - 1]);
+ } else {
+ goHome();
+ }
+ }
+
+ function goHome(){ historyStack = ['home']; render('home'); }
+
+ function onEnter(id){
+ if(id === 'addBrand'){
+ selectedBrand = null;
+ document.querySelectorAll('#addBrand .brand-tile').forEach(t=>t.classList.remove('selected'));
+ document.getElementById('saveDraftBtn').disabled = true;
+ document.getElementById('brandSearch').value = '';
+ filterBrands();
+ }
+ if(id === 'findResults'){ renderFindResults(); }
+ if(id === 'updateCamera1' || id === 'findMethods' || id === 'sampleCamera'){ syncDemoToggles(); }
+ }
+
+ // the three demo "no match" checkboxes all reflect ONE shared boolean
+ function setDemoToggle(checked){
+ demoNoMatch = checked;
+ syncDemoToggles();
+ }
+ function syncDemoToggles(){
+ ['demoToggle','demoToggleFind','demoToggleSample'].forEach(function(id){
+ const el = document.getElementById(id);
+ if(el) el.checked = demoNoMatch;
+ });
+ }
+
+ function showPhotoCheck(nextId){
+ photoCheckNext = nextId;
+ go('photoCheck');
+ }
+
+ function photoLooksGood(){
+ historyStack.pop(); // drop photoCheck
+ if(photoCheckNext === 'afterUpdateShot'){ afterUpdateShot(); return; }
+ if(photoCheckNext === 'afterSampleShot'){ afterSampleShot(); return; }
+ if(photoCheckNext === 'updateSave'){ updateSave(); return; }
+ if(photoCheckNext === 'findResults'){ go('findResults'); return; }
+ go(photoCheckNext);
+ }
+
+ function afterUpdateShot(){
+ demoNoMatch ? go('updateNoMatch') : go('updateMatch');
+ }
+ function afterSampleShot(){
+ demoNoMatch ? go('sampleNoMatch') : go('sampleMatch');
+ }
+
+ function findScan(){ go('findCamera'); }
+
+ function renderFindResults(){
+ const list = document.getElementById('findResultsList');
+ const empty = document.getElementById('findNoResults');
+ findResultCounter++;
+ const miss = demoNoMatch;
+ if(miss){
+ list.style.display = 'none';
+ empty.style.display = 'flex';
+ return;
+ }
+ list.style.display = 'flex';
+ empty.style.display = 'none';
+ const items = [
+ {name:'Palm Damask — Sage', code:'DW-10423', brand:'York', bg:'repeating-linear-gradient(45deg,#C97B5B 0 14px,#e0a184 14px 28px)'},
+ {name:'Meadow Stripe — Gold', code:'DW-20115', brand:'Kravet', bg:'repeating-linear-gradient(45deg,#B8842A 0 14px,#eec076 14px 28px)'},
+ {name:'Coastal Weave — Blue', code:'DW-30871', brand:'Thibaut', bg:'repeating-linear-gradient(45deg,#7C93B0 0 14px,#a9bcd3 14px 28px)'},
+ ];
+ list.innerHTML = '';
+ items.forEach(it=>{
+ const card = document.createElement('div');
+ card.className = 'thumb-card';
+ card.tabIndex = 0;
+ card.setAttribute('role','button');
+ card.innerHTML = '<div class="thumb-sw" style="background:'+it.bg+';"></div><div class="thumb-text"><div class="item-title">'+it.name+'</div><div class="item-sub">code '+it.code+'</div></div>';
+ card.onclick = () => {
+ document.getElementById('findDetailTitle').textContent = it.name;
+ document.getElementById('findDetailSub').textContent = 'code ' + it.code + ' · ' + it.brand;
+ go('findDetail');
+ };
+ list.appendChild(card);
+ });
+ }
+
+ 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 filterBrands(){
+ const q = document.getElementById('brandSearch').value.trim().toLowerCase();
+ document.querySelectorAll('#allBrands .brand-tile').forEach(t=>{
+ t.classList.toggle('hidden', q.length > 0 && !t.textContent.toLowerCase().includes(q));
+ });
+ }
+
+ function randomCode(){ return 'DW-' + Math.floor(10000 + Math.random() * 89999); }
+
+ function showDone(opts){
+ document.getElementById('doneThumb').className = 'thumb' + (opts.blue ? ' blue' : '');
+ document.getElementById('doneTitle').textContent = opts.name;
+ document.getElementById('doneCode').textContent = opts.code;
+ document.getElementById('doneBrand').textContent = opts.brand;
+ document.getElementById('doneAction').textContent = opts.action;
+ historyStack = ['home', 'doneScreen'];
+ render('doneScreen');
+ }
+
+ function saveDraft(){
+ if(!selectedBrand) return;
+ showToast('Saved · Undo', 'addBrand');
+ showDone({name:'New Item', code: randomCode(), brand: selectedBrand, action:'Saved as a draft'});
+ }
+
+ function updateSave(){
+ showToast('Saved · Undo', 'updateMatch');
+ showDone({name:'Palm Damask — Sage', code:'DW-10423', brand:'York', action:'Updated the photo', blue:true});
+ }
+
+ function markReceived(){
+ go('samplePrinting');
+ var fromScreen = 'samplePrinting';
+ setTimeout(function(){
+ if(historyStack[historyStack.length - 1] !== fromScreen) return;
+ showToast('Saved · Undo', 'sampleMatch');
+ showDone({name:'Palm Damask — Sage', code:'DW-10423', brand:'York', action:'Marked as received'});
+ }, 1100);
+ }
+
+ function showToast(text, backTarget){
+ document.getElementById('toastText').textContent = text;
+ undoTarget = backTarget;
+ const t = document.getElementById('toast');
+ t.classList.add('show');
+ clearTimeout(toastTimer);
+ toastTimer = setTimeout(function(){ t.classList.remove('show'); }, 5000);
+ }
+
+ function undoAction(){
+ document.getElementById('toast').classList.remove('show');
+ clearTimeout(toastTimer);
+ if(undoTarget){
+ historyStack = ['home', undoTarget];
+ render(undoTarget);
+ // NOTE: do NOT call onEnter() here — undo must restore the confirm
+ // screen with the prior selection intact, not reset it (onEnter('addBrand')
+ // clears selectedBrand for a fresh forward-navigation entry only).
+ }
+ }
+</script>
+</body>
+</html>
diff --git a/public/kid-simple/v3-text-buddy.html b/public/kid-simple/v3-text-buddy.html
new file mode 100644
index 0000000..95d45af
--- /dev/null
+++ b/public/kid-simple/v3-text-buddy.html
@@ -0,0 +1,551 @@
+<!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;
+ --muted:#6B6255;
+ }
+ *{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;flex-direction:column;gap:8px;
+ padding:14px 16px 10px;border-bottom:1px solid #ded6c8;background:var(--paper);flex-shrink:0;
+ }
+ .header-top{display:flex;align-items:center;justify-content:space-between;}
+ header .wordmark{font-family:'Fraunces',serif;font-weight:700;font-size:18px;}
+ header .sam-label{font-size:14px;color:var(--muted);margin-top:2px;}
+ .header-actions{display:flex;gap:10px;}
+ .nav-btn{
+ flex:1;min-height:56px;background:#fff;border:2px solid #ded6c8;border-radius:14px;padding:0 10px;
+ font-size:17px;font-weight:600;cursor:pointer;color:var(--text);font-family:'Inter',sans-serif;
+ }
+ .nav-btn:disabled{opacity:.4;cursor:not-allowed;}
+ .nav-btn:focus-visible, .chip:focus-visible, .thumb-card:focus-visible, .result-card:focus-visible, input:focus-visible{outline:3px solid var(--gold);outline-offset:2px;}
+ .nav-btn:active, .chip:active, .result-card:active, .cam-bubble:active{transform:scale(.97);}
+ .demo-toggle-inline{font-size:14px;color:var(--muted);display:flex;align-items:center;gap:8px;}
+ .demo-toggle-inline input{width:18px;height:18px;}
+ #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;
+ }
+ .step-note{align-self:flex-start;font-size:14px;color:var(--muted);padding:0 4px;}
+ .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;
+ }
+ .cam-bubble{
+ 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;min-height:56px;
+ }
+ .cam-bubble .cam-icon{font-size:40px;}
+ .cam-bubble .cam-label{font-size:17px;color:var(--muted);}
+ .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{
+ 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;min-height:56px;
+ }
+ .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:17px;color:var(--muted);}
+ .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 .swatch.blue{background:repeating-linear-gradient(45deg,#7C93B0 0 20px,#B3C3D6 20px 40px),linear-gradient(135deg,#7C93B0,#5f7692);background-blend-mode:multiply;}
+ .full-item .rc-title{font-size:18px;font-weight:700;}
+ .full-item .rc-sub{font-size:17px;color:var(--muted);}
+ .search-mock{
+ align-self:flex-start;background:#fff;border-radius:14px;padding:14px 16px;font-size:17px;
+ color:var(--muted);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);min-height:24px;
+ }
+ .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);}}
+ .toast{
+ position:absolute;left:16px;right:16px;bottom:16px;background:#2B2B2B;color:#fff;
+ border-radius:14px;padding:14px 18px;display:none;align-items:center;justify-content:space-between;
+ gap:12px;box-shadow:0 8px 24px rgba(0,0,0,.35);z-index:20;font-size:17px;border:2px solid var(--gold);
+ }
+ .toast.show{display:flex;}
+ .toast button{background:none;border:none;color:#F4D48A;font-weight:700;font-size:17px;cursor:pointer;min-height:56px;min-width:56px;padding:0 6px;}
+ @media (prefers-reduced-motion: reduce){
+ .spinner-inline{animation:none;border-top-color:var(--gold);}
+ *{transition:none !important;}
+ }
+</style>
+</head>
+<body>
+<div class="stage">
+ <header>
+ <div class="header-top">
+ <div>
+ <div class="wordmark">DW</div>
+ <div class="sam-label">chatting with Sam</div>
+ </div>
+ </div>
+ <div class="header-actions">
+ <button class="nav-btn" id="undoBtn" onclick="undoLast()" aria-label="Undo last">‹ Undo last</button>
+ <button class="nav-btn" onclick="resetChat()" aria-label="New chat">⌂ New chat</button>
+ </div>
+ <label class="demo-toggle-inline"><input type="checkbox" id="demoToggle" onchange="demoNoMatch=this.checked"> Demo: show "no match"</label>
+ </header>
+ <div id="thread" aria-live="polite"></div>
+</div>
+
+<!-- TOAST (undo) -->
+<div class="toast" id="toast" role="status" aria-live="polite">
+ <span id="toastText">Saved</span>
+ <button onclick="toastUndo()">Undo</button>
+</div>
+
+<script>
+ const thread = document.getElementById('thread');
+ let demoNoMatch = false;
+ let stepStack = [];
+ let toastTimer = null;
+
+ function scrollBottom(){ thread.scrollTop = thread.scrollHeight; }
+
+ function setUndoEnabled(){
+ document.getElementById('undoBtn').disabled = stepStack.length === 0;
+ }
+
+ // Anything "awaiting a tap" (chips, camera prompt, brand picker, result list,
+ // type-a-code box) is tagged .pending-ui. pushStep always clears it first, so
+ // both a real click AND an undo-replay land on a clean, non-duplicated screen.
+ function clearPendingUI(){
+ document.querySelectorAll('#thread .pending-ui').forEach(el => el.remove());
+ }
+
+ function pushStep(fn){
+ clearPendingUI();
+ fn();
+ stepStack.push(fn);
+ setUndoEnabled();
+ }
+
+ function undoLast(){
+ if(stepStack.length === 0) return;
+ hideToast();
+ stepStack.pop();
+ const keep = stepStack;
+ stepStack = [];
+ thread.innerHTML = '';
+ startHome();
+ keep.forEach(pushStep);
+ setUndoEnabled();
+ }
+
+ function toastUndo(){ undoLast(); }
+
+ function resetChat(){
+ hideToast();
+ stepStack = [];
+ thread.innerHTML = '';
+ startHome();
+ setUndoEnabled();
+ }
+
+ 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 addStepNote(text){
+ const el = document.createElement('div');
+ el.className = 'step-note';
+ el.textContent = text;
+ thread.appendChild(el);
+ scrollBottom();
+ }
+ function addChips(options){
+ const wrap = document.createElement('div');
+ wrap.className = 'chips pending-ui';
+ options.forEach(opt => {
+ const b = document.createElement('button');
+ b.className = 'chip';
+ b.textContent = opt.label;
+ b.onclick = () => pushStep(opt.action);
+ wrap.appendChild(b);
+ });
+ thread.appendChild(wrap);
+ scrollBottom();
+ return wrap;
+ }
+ function addCameraBubble(onShot){
+ function showCameraStep(){
+ const el = document.createElement('div');
+ el.className = 'cam-bubble pending-ui';
+ el.setAttribute('role','button');
+ el.tabIndex = 0;
+ el.innerHTML = '<div class="cam-icon">📸</div><div class="cam-label">Tap to shoot</div>';
+ // shoot is a recorded step (pushStep, not a bare onclick) so tapping the
+ // shutter has its own undo point. Its "Looks good"/"Retake" actions never
+ // hold onto a closed-over DOM node — they re-query the live tree by the
+ // '.shot-pending' marker at click time, so a replay (which rebuilds the
+ // #thread DOM from scratch) never touches a node from a destroyed tree.
+ const shoot = () => {
+ const photo = addUser('<div class="swatch-bubble"></div>');
+ photo.classList.add('shot-pending');
+ const looking = addSam('<span class="spinner-inline"></span>Looking…');
+ setTimeout(() => {
+ looking.remove();
+ addSam("Here's what I got — good?");
+ addChips([
+ {label:'Looks good', action: () => {
+ const p = thread.querySelector('.shot-pending');
+ if(p) p.classList.remove('shot-pending');
+ onShot();
+ }},
+ {label:'Retake', action: () => {
+ const p = thread.querySelector('.shot-pending');
+ if(p) p.remove();
+ showCameraStep();
+ }},
+ ]);
+ }, 900);
+ };
+ el.onclick = () => pushStep(shoot);
+ el.onkeydown = (e) => { if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); pushStep(shoot); } };
+ thread.appendChild(el);
+ scrollBottom();
+ }
+ showCameraStep();
+ }
+ function addDoneCard(opts){
+ const el = document.createElement('div');
+ el.className = 'full-item';
+ el.innerHTML = '<div class="swatch'+(opts.blue ? ' blue' : '')+'"></div>'+
+ '<div class="rc-title">'+opts.name+'</div>'+
+ '<div class="rc-sub">code '+opts.code+' · '+opts.brand+'</div>'+
+ '<div class="rc-sub" style="margin-top:6px;">'+opts.action+'</div>';
+ thread.appendChild(el);
+ scrollBottom();
+ }
+ function randomCode(){ return 'DW-' + Math.floor(10000 + Math.random() * 89999); }
+
+ function showToast(text){
+ document.getElementById('toastText').textContent = text;
+ const t = document.getElementById('toast');
+ t.classList.add('show');
+ clearTimeout(toastTimer);
+ toastTimer = setTimeout(hideToast, 5000);
+ }
+ function hideToast(){
+ document.getElementById('toast').classList.remove('show');
+ clearTimeout(toastTimer);
+ }
+
+ function startHome(){
+ thread.innerHTML = '';
+ addSam('Hey! What are we doing today?');
+ addChips([
+ {label:'Add New Item', action: flowAddStart},
+ {label:'Update an Item', action: flowUpdateStart},
+ {label:'Find an Item', action: flowFindStart},
+ {label:'A Sample Came In', action: flowSampleStart},
+ ]);
+ }
+
+ // ---------- ADD NEW ITEM ----------
+ function flowAddStart(){
+ addUser('Add New Item');
+ addSam('Cool, snap a photo of it 📸');
+ addCameraBubble(() => {
+ addSam('Which brand?');
+ addBrandPicker((brand) => addAddSaved(brand));
+ });
+ }
+ function addBrandPicker(onPick){
+ const recent = ['York', 'Kravet', 'Thibaut'];
+ const all = ['Brewster', 'Cole & Son', 'Mulberry', 'Other / Unsure'];
+ const wrap = document.createElement('div');
+ wrap.className = 'pending-ui';
+ wrap.style.cssText = 'align-self:flex-start;width:100%;display:flex;flex-direction:column;gap:10px;';
+ wrap.innerHTML =
+ '<div class="search-mock" style="width:100%;"><input type="text" class="text-input" id="brandSearchInput" placeholder="Search brands…" aria-label="Search brands"></div>'+
+ '<div class="step-note">Recent</div>'+
+ '<div class="chips" id="recentChipsRow"></div>'+
+ '<div class="step-note">All Brands</div>'+
+ '<div class="chips" id="allChipsRow"></div>';
+ thread.appendChild(wrap);
+ const recentRow = wrap.querySelector('#recentChipsRow');
+ const allRow = wrap.querySelector('#allChipsRow');
+ function addBrandChip(row, name){
+ const b = document.createElement('button');
+ b.className = 'chip';
+ b.textContent = name;
+ b.onclick = () => pushStep(() => { addUser(name); onPick(name); });
+ row.appendChild(b);
+ }
+ recent.forEach(n => addBrandChip(recentRow, n));
+ all.forEach(n => addBrandChip(allRow, n));
+ scrollBottom();
+ wrap.querySelector('#brandSearchInput').addEventListener('input', function(){
+ const q = this.value.trim().toLowerCase();
+ Array.from(allRow.children).forEach(b => {
+ b.style.display = (q.length === 0 || b.textContent.toLowerCase().includes(q)) ? '' : 'none';
+ });
+ });
+ }
+ function addAddSaved(brand){
+ showToast('Saved · Undo');
+ addSam('Saved it as a draft! 🎉');
+ addDoneCard({name:'New Item', code: randomCode(), brand: brand, action:'Saved as a draft'});
+ addChips([
+ {label:'Do Another', action: () => { addUser('Do Another'); flowAddStart(); }},
+ {label:'Home', action: () => { addUser('Home'); resetChat(); }},
+ ]);
+ }
+
+ // ---------- UPDATE AN ITEM ----------
+ function flowUpdateStart(){
+ addUser('Update an Item');
+ addStepNote('Step 1 of 2');
+ addSam('Show me the label or pattern');
+ addCameraBubble(() => {
+ if(demoNoMatch){
+ showNoMatch(
+ () => { addUser('Try again'); flowUpdateStart(); },
+ () => { addUser('Type the code instead'); updateTypeSearch(); }
+ );
+ return;
+ }
+ updateConfirm();
+ });
+ }
+ function updateConfirm(){
+ addSam("Found it — Palm Damask — Sage, code DW-10423. That the one?");
+ addChips([
+ {label:"Yep, that's it", action: () => {
+ addUser("Yep, that's it");
+ updateNewPhoto();
+ }},
+ {label:'Nope, let me search', action: () => {
+ addUser('Nope, let me search');
+ updateTypeSearch();
+ }},
+ ]);
+ }
+ function updateTypeSearch(){
+ addSam('No problem — type the name or code:');
+ const el = document.createElement('div');
+ el.className = 'search-mock pending-ui';
+ 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';
+ pushStep(() => {
+ 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"); updateNewPhoto(); }}]);
+ });
+ };
+ searchInput.addEventListener('keydown', (e) => { if(e.key === 'Enter'){ e.preventDefault(); sendIt(); } });
+ addChips([{label:'Send', action: sendIt}]);
+ }
+ function updateNewPhoto(){
+ addStepNote('Step 2 of 2');
+ addSam('Send me the new photo');
+ addCameraBubble(() => {
+ showToast('Saved · Undo');
+ addSam('Updated! ✅');
+ addDoneCard({name:'Palm Damask — Sage', code:'DW-10423', brand:'York', action:'Updated the photo', blue:true});
+ addChips([
+ {label:'Do Another', action: () => { addUser('Do Another'); flowUpdateStart(); }},
+ {label:'Home', action: () => { addUser('Home'); resetChat(); }},
+ ]);
+ });
+ }
+
+ // ---------- FIND AN ITEM ----------
+ function flowFindStart(){
+ addUser('Find an Item');
+ 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(){
+ if(demoNoMatch){
+ showNoMatch(
+ () => { addUser('Try again'); findAskMethod(); },
+ () => { addUser('Type the code instead'); findTypeSearch(); }
+ );
+ return;
+ }
+ addSam("Here's what I found:");
+ const items = [
+ {name:'Palm Damask — Sage', code:'DW-10423', brand:'York'},
+ {name:'Meadow Stripe — Gold', code:'DW-20115', brand:'Kravet'},
+ {name:'Coastal Weave — Blue', code:'DW-30871', brand:'Thibaut'},
+ ];
+ const listWrap = document.createElement('div');
+ listWrap.className = 'pending-ui';
+ listWrap.style.cssText = 'display:flex;flex-direction:column;gap:10px;align-self:flex-start;width:100%;';
+ items.forEach(it => {
+ const card = document.createElement('div');
+ card.className = 'result-card';
+ card.tabIndex = 0;
+ card.setAttribute('role','button');
+ card.innerHTML = '<div class="thumb"></div><div><div class="rc-title">'+it.name+'</div><div class="rc-sub">code '+it.code+'</div></div>';
+ const pick = () => pushStep(() => { addUser(it.name); findShowDetail(it); });
+ card.onclick = pick;
+ card.onkeydown = (e) => { if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); pick(); } };
+ listWrap.appendChild(card);
+ });
+ thread.appendChild(listWrap);
+ scrollBottom();
+ }
+ function findTypeSearch(){
+ addSam('Type the item name or code:');
+ const el = document.createElement('div');
+ el.className = 'search-mock pending-ui';
+ el.innerHTML = '<input type="text" class="text-input" enterkeyhint="search" placeholder="DW-10423" aria-label="Type the item code">';
+ thread.appendChild(el);
+ scrollBottom();
+ const searchInput = el.querySelector('input');
+ const sendIt = () => {
+ const val = searchInput.value.trim() || 'DW-10423';
+ pushStep(() => {
+ addUser(val);
+ findShowDetail({name:'Palm Damask — Sage', code:'DW-10423', brand:'York'});
+ });
+ };
+ searchInput.addEventListener('keydown', (e) => { if(e.key === 'Enter'){ e.preventDefault(); sendIt(); } });
+ addChips([{label:'Send', action: sendIt}]);
+ }
+ function findShowDetail(it){
+ addSam('Found it —');
+ addDoneCard({name:it.name, code:it.code, brand:it.brand, action:'Found it'});
+ addChips([
+ {label:'Update This', action: () => {
+ addUser('Update This');
+ updateNewPhoto();
+ }},
+ {label:'Search Again', action: () => { addUser('Search Again'); findAskMethod(); }},
+ ]);
+ }
+
+ // ---------- SAMPLE IN ----------
+ function flowSampleStart(){
+ addUser('A Sample Came In');
+ addSam('Scan the box label');
+ addCameraBubble(() => {
+ if(demoNoMatch){
+ showNoMatch(
+ () => { addUser('Try again'); flowSampleStart(); },
+ () => { addUser('Type the code instead'); sampleTypeSearch(); }
+ );
+ return;
+ }
+ sampleConfirm();
+ });
+ }
+ function sampleConfirm(){
+ addSam('Found it — mark it received?');
+ addChips([
+ {label:'Yes', action: () => { addUser('Yes'); sampleFinish(); }},
+ ]);
+ }
+ function sampleTypeSearch(){
+ addSam('Type the item name or code:');
+ const el = document.createElement('div');
+ el.className = 'search-mock pending-ui';
+ el.innerHTML = '<input type="text" class="text-input" enterkeyhint="search" placeholder="DW-10423" aria-label="Type the item code">';
+ thread.appendChild(el);
+ scrollBottom();
+ const searchInput = el.querySelector('input');
+ const sendIt = () => {
+ const val = searchInput.value.trim() || 'DW-10423';
+ pushStep(() => {
+ addUser(val);
+ sampleConfirm();
+ });
+ };
+ searchInput.addEventListener('keydown', (e) => { if(e.key === 'Enter'){ e.preventDefault(); sendIt(); } });
+ addChips([{label:'Send', action: sendIt}]);
+ }
+ function sampleFinish(){
+ const printing = addSam('<span class="spinner-inline"></span>Printing your sticker now 🖨️');
+ setTimeout(() => {
+ printing.remove();
+ showToast('Saved · Undo');
+ addSam('All set! ✅');
+ addDoneCard({name:'Palm Damask — Sage', code:'DW-10423', brand:'York', action:'Marked as received'});
+ addChips([
+ {label:'Do Another', action: () => { addUser('Do Another'); flowSampleStart(); }},
+ {label:'Home', action: () => { addUser('Home'); resetChat(); }},
+ ]);
+ }, 1200);
+ }
+
+ // ---------- SHARED NO-MATCH ----------
+ function showNoMatch(tryAgainFn, typeItFn){
+ addSam("Hmm, I couldn't find a match.");
+ addChips([
+ {label:'Try again', action: tryAgainFn},
+ {label:'Type the code instead', action: typeItFn},
+ ]);
+ }
+
+ startHome();
+ setUndoEnabled();
+</script>
+</body>
+</html>
diff --git a/public/kid-simple/v4-level-up.html b/public/kid-simple/v4-level-up.html
new file mode 100644
index 0000000..1813a6d
--- /dev/null
+++ b/public/kid-simple/v4-level-up.html
@@ -0,0 +1,444 @@
+<!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:12px 14px; gap:8px;
+ 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;}
+ .navbtn{
+ background:rgba(255,255,255,.14);border:none;color:#fff;font-size:16px;font-weight:800;cursor:pointer;
+ min-width:56px;min-height:56px;line-height:1;border-radius:14px; padding:0 14px;
+ font-family:'Nunito',sans-serif; display:inline-flex; align-items:center; justify-content:center; gap:4px;
+ }
+ .navbtn:hover{background:rgba(255,255,255,.24);}
+ .navbtn:active{transform:scale(.97);}
+ .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:active{transform:scale(.97);}
+ .btn-primary{background:var(--gold); color:#3a2a06;}
+ .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,
+ .navbtn:focus-visible, .demotoggle:focus-visible, .codeinput:focus-visible, .searchbox:focus-visible,
+ .toastundo: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;
+ }
+ .shutter:active{transform:translateX(-50%) scale(.97);}
+ .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:active{transform:scale(.97);}
+ .brandcard.selected{border-color:var(--gold); background:#fff7e6;}
+ .sectionlabel{margin:0; font-size:14px; font-weight:800; letter-spacing:.04em; text-transform:uppercase; color:#8a7a5a;}
+ .searchbox{
+ width:100%; min-height:56px; border-radius:16px; border:2px solid #e3d9c6; padding:0 16px;
+ font-size:17px; font-family:'Nunito',sans-serif; background:#fff; color:var(--ink);
+ }
+ .codeinput{
+ width:100%; min-height:56px; border-radius:16px; border:2px solid #e3d9c6; padding:0 16px;
+ font-size:17px; font-family:'Nunito',sans-serif; background:#fff; color:var(--ink);
+ }
+ .demotoggle{
+ background:none; border:2px dashed #cbbfa0; color:#4a4a4a; font-size:17px; font-weight:700;
+ border-radius:12px; padding:8px 12px; min-height:56px; align-self:flex-start;
+ }
+ .demotoggle:active{transform:scale(.97);}
+ .itemname{font-size:19px; font-weight:800; margin:0 0 2px;}
+ .itemmeta{font-size:17px; color:#5a5a5a; 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:active{transform:scale(.97);}
+ .resultcard .thumb{width:52px; height:52px; border-radius:10px; flex:none;}
+ .confetti-banner{
+ background:var(--green); 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;}
+ .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;}
+ .toast{
+ position:absolute; left:16px; right:16px; bottom:16px; background:#232323; color:#fff;
+ border-radius:14px; padding:14px 18px; display:flex; align-items:center; justify-content:space-between;
+ gap:12px; font-size:16px; font-weight:700; opacity:0; transform:translateY(12px); pointer-events:none;
+ transition:opacity .25s ease, transform .25s ease; z-index:30;
+ }
+ .toast.show{opacity:1; transform:translateY(0); pointer-events:auto;}
+ .toastundo{background:none; border:none; color:var(--gold); font-weight:800; font-size:16px; min-height:56px; padding:0 6px;}
+ @media (prefers-reduced-motion: reduce){
+ *{transition:none !important; animation:none !important;}
+ }
+</style>
+</head>
+<body>
+<div class="phone">
+ <header>
+ <button class="navbtn" aria-label="Back" onclick="goBack()" id="backBtn">‹ Back</button>
+ <span class="mark">DW · Photo</span>
+ <button class="navbtn" aria-label="Home" onclick="go('home')" id="homeBtnTop">⌂ Home</button>
+ </header>
+ <div class="screen" id="screen" role="main"></div>
+ <div class="toast" id="toast" role="status"></div>
+ <div class="sr-only" id="live" aria-live="polite"></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:'A Sample Came In', emoji:'📦', levels:2},
+];
+const BRANDS = ['York Wallcoverings','Brewster Home','Kravet','Cole & Son','Thibaut','Ronald Redding'];
+const RECENT_BRANDS = BRANDS.slice(0,3);
+const MOCK_ITEMS = [
+ {name:'Palm Damask', color:'Sage', code:'DW-10423', sw:'sw1', brand:'York Wallcoverings'},
+ {name:'Trellis Weave', color:'Indigo', code:'DW-88214', sw:'sw2', brand:'Cole & Son'},
+ {name:'Linen Bloom', color:'Blush', code:'DW-55071', sw:'sw3', brand:'Thibaut'},
+];
+const DONE_TOAST = {'new-done':'Saved','update-done':'Updated','sample-done':'Marked Received'};
+
+const state = {screen:'home', job:null, brand:null, item:null, demoNoMatch:false, checkNext:null, checkRetake:null, confirmScreen:null};
+let historyStack = [];
+let toastTimer = null;
+
+function announce(text){ const el=document.getElementById('live'); if(el) el.textContent = text; }
+
+function go(screen){
+ if(screen==='home'){ historyStack = []; }
+ else if(state.screen && state.screen!==screen){
+ if(String(screen).endsWith('-done')) state.confirmScreen = state.screen;
+ historyStack.push(state.screen);
+ }
+ state.screen = screen;
+ render();
+}
+function goReplace(screen){ state.screen = screen; render(); }
+function goBack(){
+ if(historyStack.length){ state.screen = historyStack.pop(); render(); }
+ else go('home');
+}
+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){
+ const 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 levelFromScreen(scr){ const m = /-(\d+)$/.exec(scr||''); return m ? parseInt(m[1],10) : null; }
+
+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, loadingLabel){
+ const fromScreen = state.screen;
+ const st = document.getElementById('camStatus');
+ const btn = document.querySelector('.shutter');
+ if(btn) btn.disabled = true;
+ if(st) st.textContent = 'Snapping…';
+ announce('Snapping photo…');
+ setTimeout(()=>{
+ if(state.screen!==fromScreen) return; // user navigated away mid-shot
+ const item = MOCK_ITEMS[Math.floor(Math.random()*MOCK_ITEMS.length)];
+ state.item = item;
+ if(loadingLabel){
+ if(st) st.textContent = loadingLabel;
+ announce(loadingLabel);
+ setTimeout(()=>{ if(state.screen===fromScreen) cb(); }, 500);
+ } else cb();
+ }, 700);
+}
+function shootThen(nextScreen, retakeScreen){
+ shoot(()=>{ state.checkNext=nextScreen; state.checkRetake=retakeScreen; go('photocheck'); });
+}
+function shootOrMiss(nextScreen, retakeScreen, missScreen, loadingLabel){
+ shoot(()=>{
+ if(state.demoNoMatch){ go(missScreen); return; }
+ state.checkNext=nextScreen; state.checkRetake=retakeScreen; go('photocheck');
+ }, loadingLabel);
+}
+function finishCheck(){
+ const next = state.checkNext;
+ go(next);
+ if(next==='update-done') state.confirmScreen = 'update-3';
+ if(DONE_TOAST[next]) showToast(DONE_TOAST[next]);
+}
+function toggleDemo(){ state.demoNoMatch = !state.demoNoMatch; render(); }
+function demoToggleHtml(){
+ return `<button class="demotoggle" aria-pressed="${state.demoNoMatch}" onclick="toggleDemo()">Demo: show "no match" ${state.demoNoMatch?'✅ On':'⬜ Off'}</button>`;
+}
+function tryFind(){
+ go('find-loading');
+ announce('Looking for matches…');
+ setTimeout(()=>{ if(state.screen==='find-loading') goReplace(state.demoNoMatch ? 'find-nomatch' : 'find-2'); }, 600);
+}
+function filterBrands(q){
+ q = q.trim().toLowerCase();
+ const recentWrap = document.getElementById('brandRecent');
+ if(recentWrap) recentWrap.style.display = q ? 'none' : '';
+ const full = document.getElementById('brandFullGrid');
+ if(!full) return;
+ full.querySelectorAll('.brandcard').forEach(btn=>{
+ btn.style.display = btn.textContent.toLowerCase().includes(q) ? '' : 'none';
+ });
+}
+function showToast(msg){
+ const t = document.getElementById('toast');
+ t.innerHTML = `${msg} <button class="toastundo" onclick="undoLast()">Undo</button>`;
+ t.classList.add('show');
+ announce(msg + '. Undo available for 5 seconds.');
+ clearTimeout(toastTimer);
+ toastTimer = setTimeout(()=>{ t.classList.remove('show'); }, 5000);
+}
+function undoLast(){
+ const t = document.getElementById('toast');
+ t.classList.remove('show');
+ clearTimeout(toastTimer);
+ if(state.confirmScreen) go(state.confirmScreen);
+}
+function saveDraft(){ go('new-done'); showToast('Saved'); }
+function markReceived(){
+ const fromScreen = state.screen;
+ const btn = document.getElementById('markReceivedBtn');
+ if(btn){ btn.disabled = true; btn.textContent = 'Printing sticker…'; }
+ announce('Printing sticker…');
+ setTimeout(()=>{
+ if(state.screen!==fromScreen) return; // user navigated away mid-print
+ go('sample-done'); showToast('Marked Received');
+ }, 700);
+}
+
+function brandGridHtml(nextScreen){
+ let html = `<input type="text" class="searchbox" id="brandSearch" placeholder="Search brands…" aria-label="Search brands" oninput="filterBrands(this.value)">`;
+ html += `<div id="brandRecent"><p class="sectionlabel">Recent</p><div class="brandgrid">`;
+ RECENT_BRANDS.forEach(b=>{ html += `<button class="brandcard" onclick="state.brand='${b}'; go('${nextScreen}')">${b}</button>`; });
+ html += `</div></div>`;
+ html += `<p class="sectionlabel">All Brands</p><div class="brandgrid" id="brandFullGrid">`;
+ BRANDS.forEach(b=>{ html += `<button class="brandcard" onclick="state.brand='${b}'; go('${nextScreen}')">${b}</button>`; });
+ html += `</div>`;
+ return html;
+}
+
+function render(){
+ const s = document.getElementById('screen');
+ let html = '';
+ const onHome = state.screen==='home';
+ document.getElementById('homeBtnTop').style.display = onHome ? 'none' : 'inline-flex';
+ document.getElementById('backBtn').style.display = onHome ? '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)}
+ <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==='photocheck'){
+ const lvl = levelFromScreen(state.checkRetake);
+ html += lvl ? levelHeader(lvl, 'Photo Check') : `<h1 class="title">Photo Check</h1>`;
+ html += `<div class="card"><div class="swatch ${state.item.sw}"></div></div>`;
+ html += `<p class="sub">Does this look right?</p>`;
+ html += `<div class="row"><button class="btn btn-secondary" onclick="go(state.checkRetake)">Retake</button><button class="btn btn-primary" onclick="finishCheck()">Looks good</button></div>`;
+ }
+
+ else if(state.screen==='new-1'){
+ html += levelHeader(1,'Snap It');
+ html += cameraScreen('Take a photo of the sample', "shootThen('new-2','new-1')");
+ }
+ else if(state.screen==='new-2'){
+ html += levelHeader(2,'Pick the Brand');
+ html += brandGridHtml('new-3');
+ }
+ 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="saveDraft()">Save as Draft ⭐</button>`;
+ }
+ else if(state.screen==='new-done'){
+ html += `<div class="confetti-banner">🎉 +10 points! Nice work</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">Brand: ${state.brand} · Code: ${state.item.code}</p><p class="itemmeta">Saved as a new item draft.</p></div>`;
+ html += `<div class="row"><button class="btn btn-primary" onclick="startJob('new')">Do Another</button><button class="btn btn-secondary" 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', "shootOrMiss('update-2','update-1','update-nomatch','Looking for matches…')");
+ html += demoToggleHtml();
+ }
+ else if(state.screen==='update-nomatch'){
+ html += `<h1 class="title">No Match Found</h1><p class="sub">We couldn't find a matching item.</p>`;
+ html += `<div class="row"><button class="btn btn-secondary" onclick="go('update-1')">Try again</button><button class="btn btn-primary" onclick="go('update-typecode')">Type the code instead</button></div>`;
+ }
+ else if(state.screen==='update-typecode'){
+ html += `<h1 class="title">Type the Code</h1>`;
+ html += `<input type="text" class="codeinput" placeholder="e.g. DW-10423" aria-label="Item code">`;
+ html += `<button class="btn btn-primary" onclick="state.item=MOCK_ITEMS[0]; go('update-2')">Find</button>`;
+ }
+ 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', "shootThen('update-done','update-3')");
+ }
+ 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">Brand: ${state.item.brand} · Code: ${state.item.code}</p><p class="itemmeta">Photo updated.</p></div>`;
+ html += `<div class="row"><button class="btn btn-primary" onclick="startJob('update')">Do Another</button><button class="btn btn-secondary" 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="tryFind()">📷 Scan</button>
+ <button class="btn btn-secondary" onclick="tryFind()">🎤 Voice</button>
+ <button class="btn btn-secondary" onclick="tryFind()">⌨️ Type</button>
+ </div>`;
+ html += demoToggleHtml();
+ }
+ else if(state.screen==='find-loading'){
+ html += `<div class="card" style="text-align:center;"><p class="sub">Looking for matches…</p></div>`;
+ }
+ else if(state.screen==='find-nomatch'){
+ html += `<h1 class="title">No Match Found</h1><p class="sub">We couldn't find a matching item.</p>`;
+ html += `<div class="row"><button class="btn btn-secondary" onclick="go('find-1')">Try again</button><button class="btn btn-primary" onclick="go('find-typecode')">Type the code instead</button></div>`;
+ }
+ else if(state.screen==='find-typecode'){
+ html += `<h1 class="title">Type the Code</h1>`;
+ html += `<input type="text" class="codeinput" placeholder="e.g. DW-10423" aria-label="Item code">`;
+ html += `<button class="btn btn-primary" onclick="state.item=MOCK_ITEMS[0]; go('find-done')">Find</button>`;
+ }
+ 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">Brand: ${state.item.brand} · Code: ${state.item.code}</p></div>`;
+ html += `<div class="row"><button class="btn btn-primary" onclick="startJob('find')">Do Another</button><button class="btn btn-secondary" 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', "shootOrMiss('sample-2','sample-1','sample-nomatch','Looking for matches…')");
+ html += demoToggleHtml();
+ }
+ else if(state.screen==='sample-nomatch'){
+ html += `<h1 class="title">No Match Found</h1><p class="sub">We couldn't find a matching item.</p>`;
+ html += `<div class="row"><button class="btn btn-secondary" onclick="go('sample-1')">Try again</button><button class="btn btn-primary" onclick="go('sample-typecode')">Type the code instead</button></div>`;
+ }
+ else if(state.screen==='sample-typecode'){
+ html += `<h1 class="title">Type the Code</h1>`;
+ html += `<input type="text" class="codeinput" placeholder="e.g. DW-10423" aria-label="Item code">`;
+ html += `<button class="btn btn-primary" onclick="state.item=MOCK_ITEMS[0]; go('sample-2')">Find</button>`;
+ }
+ 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="card"><div class="swatch ${state.item.sw}"></div><p class="itemname">${state.item.name} — ${state.item.color}</p><p class="itemmeta">Brand: ${state.item.brand} · Code: ${state.item.code}</p><p class="itemmeta">Marked received.</p></div>`;
+ html += `<div class="row"><button class="btn btn-primary" onclick="startJob('sample')">Do Another</button><button class="btn btn-secondary" onclick="go('home')">Home</button></div>`;
+ }
+
+ s.innerHTML = html;
+}
+
+render();
+</script>
+</body>
+</html>
diff --git a/public/kid-simple/v5-swipe-deck.html b/public/kid-simple/v5-swipe-deck.html
new file mode 100644
index 0000000..90273fe
--- /dev/null
+++ b/public/kid-simple/v5-swipe-deck.html
@@ -0,0 +1,490 @@
+<!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; --gold-text:#7A5C22; --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:10px 14px; display:flex; align-items:center; justify-content:space-between; gap:8px; }
+ header .mark{ font-family:'Fraunces',serif; font-weight:700; font-size:18px; color:var(--gold-text); }
+ .navbtn{
+ background:#fff; border:2px solid #e6dccb; color:var(--ink); font-size:15px; font-weight:800; cursor:pointer;
+ min-width:56px; min-height:56px; border-radius:14px; padding:0 14px; font-family:'Inter',sans-serif;
+ display:inline-flex; align-items:center; justify-content:center; gap:4px;
+ }
+ .navbtn:active{transform:scale(.97);}
+ .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; }
+ .steplabel{ text-align:center; font-size:14px; font-weight:800; letter-spacing:.03em; text-transform:uppercase; color:#a08a5a; margin:0; }
+ .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:17px; color:#555; 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;
+ }
+ .shutter:active{transform:translateX(-50%) scale(.97);}
+ 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:active{transform:scale(.97);}
+ .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, .navbtn:focus-visible,
+ .demotoggle:focus-visible, .codeinput:focus-visible, .searchbox:focus-visible, .toastundo:focus-visible,
+ .circle: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:active{transform:scale(.97);}
+ .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; }
+ .brandcard:active{transform:scale(.97);}
+ .sectionlabel{ margin:0; font-size:14px; font-weight:800; letter-spacing:.04em; text-transform:uppercase; color:#8a7a5a; }
+ .searchbox, .codeinput{
+ width:100%; min-height:56px; border-radius:16px; border:2px solid #e6dccb; padding:0 16px;
+ font-size:17px; font-family:'Inter',sans-serif; background:#fff; color:var(--ink);
+ }
+ .demotoggle{
+ background:none; border:2px dashed #cbbfa0; color:#4a4a4a; font-size:17px; font-weight:700;
+ border-radius:12px; padding:8px 12px; min-height:56px; align-self:center;
+ }
+ .demotoggle:active{transform:scale(.97);}
+ .resultlabel{ position:absolute; top:10px; left:10px; background:rgba(0,0,0,.55); color:#fff; font-size:13px; padding:4px 10px; border-radius:999px; }
+ .swipehint{ background:#232323; color:#fff; border-radius:16px; padding:16px; text-align:center; display:flex; flex-direction:column; gap:12px; font-size:15px; font-weight:600; }
+ .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:#7a7a7a;
+ }
+ .tabbtn:active{transform:scale(.97);}
+ .tabbtn .ic{ font-size:24px; }
+ .tabbtn.active{ color:var(--gold-text); }
+ .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;}
+ .toast{
+ position:absolute; left:16px; right:16px; bottom:96px; background:#232323; color:#fff;
+ border-radius:14px; padding:14px 18px; display:flex; align-items:center; justify-content:space-between;
+ gap:12px; font-size:16px; font-weight:700; opacity:0; transform:translateY(12px); pointer-events:none;
+ transition:opacity .25s ease, transform .25s ease; z-index:30;
+ }
+ .toast.show{opacity:1; transform:translateY(0); pointer-events:auto;}
+ .toastundo{background:none; border:none; color:var(--gold); font-weight:800; font-size:16px; min-height:56px; padding:0 6px;}
+ @media (prefers-reduced-motion: reduce){
+ *{transition:none !important; animation:none !important;}
+ }
+</style>
+</head>
+<body>
+<div class="phone">
+ <header>
+ <button class="navbtn" aria-label="Back" onclick="goBack()" id="backBtn">‹ Back</button>
+ <span class="mark">DW · Photo</span>
+ <button class="navbtn" aria-label="Home" onclick="go('home')" id="homeBtnTop">⌂ Home</button>
+ </header>
+ <div class="screen-area" id="screen"></div>
+ <div class="toast" id="toast" role="status"></div>
+ <div class="sr-only" id="live" aria-live="polite"></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', brand:'York Wallcoverings'},
+ {name:'Trellis Weave', color:'Indigo', code:'DW-88214', sw:'sw2', brand:'Cole & Son'},
+ {name:'Linen Bloom', color:'Blush', code:'DW-55071', sw:'sw3', brand:'Thibaut'},
+];
+const BRANDS = ['York Wallcoverings','Brewster Home','Kravet','Cole & Son','Thibaut','Ronald Redding'];
+const RECENT_BRANDS = BRANDS.slice(0,3);
+const STEP_MAP = {
+ 'new-cam':[1,4], 'new-review':[2,4], 'new-brand':[3,4], 'new-savecheck':[4,4],
+ 'update-scan':[1,4], 'update-match':[2,4], 'update-newphoto':[3,4], 'update-review':[4,4],
+ 'find-method':[1,3], 'find-stack':[2,3], 'find-detail':[3,3],
+ 'sample-scan':[1,3], 'sample-match':[2,3], 'sample-printing':[3,3],
+};
+// screens that render around state.item — if item is missing (e.g. a job switch
+// mid-flow nulled it while an old screen was still in historyStack), render()
+// must not throw; it routes home instead.
+const ITEM_SCREENS = ['new-review','new-savecheck','new-done','update-match','update-review','update-done','find-detail','sample-match','sample-done'];
+
+const state = { screen:'home', job:null, item:null, brand:null, resultIndex:0, demoNoMatch:false, confirmScreen:null };
+let currentSwiper = null;
+let historyStack = [];
+let toastTimer = null;
+let hintShown = false;
+
+function announce(text){ const el=document.getElementById('live'); if(el) el.textContent = text; }
+function go(screen){
+ if(screen==='home'){ historyStack = []; }
+ else if(state.screen && state.screen!==screen){ historyStack.push(state.screen); }
+ state.screen = screen; render();
+}
+function goReplace(screen){ state.screen = screen; render(); }
+function goBack(){
+ if(historyStack.length){ state.screen = historyStack.pop(); render(); }
+ else go('home');
+}
+function startJob(job){
+ state.job = job; state.item=null; state.brand=null; state.resultIndex=0;
+ // switching jobs (incl. via the always-visible bottom tab bar) is a fresh
+ // start — any screens left in historyStack belong to the old job and may
+ // reference state (item/brand) that just got cleared. Clear the stack AND
+ // use goReplace (not go) so the old screen never gets re-pushed as a back
+ // target — Back from a freshly-started job always lands on Home.
+ historyStack = [];
+ const map = {new:'new-cam', update:'update-scan', find:'find-method', sample:'sample-scan'};
+ goReplace(map[job]);
+}
+function randomItem(){ return MOCK_ITEMS[Math.floor(Math.random()*MOCK_ITEMS.length)]; }
+function stepLabel(){ const m = STEP_MAP[state.screen]; return m ? `<p class="steplabel">Card ${m[0]} of ${m[1]}</p>` : ''; }
+function toggleDemo(){ state.demoNoMatch = !state.demoNoMatch; render(); }
+function demoToggleHtml(){
+ return `<button class="demotoggle" aria-pressed="${state.demoNoMatch}" onclick="toggleDemo()">Demo: show "no match" ${state.demoNoMatch?'✅ On':'⬜ Off'}</button>`;
+}
+function maybeHint(text){
+ if(hintShown) return '';
+ hintShown = true;
+ text = text || 'Swipe right = yes, left = redo<br>(or tap the buttons)';
+ return `<div class="swipehint" role="note"><p>${text}</p><button class="btn btn-primary" style="width:auto;min-width:120px;align-self:center;" onclick="this.closest('.swipehint').remove()">Got it</button></div>`;
+}
+
+function shoot(cb, loadingLabel){
+ const fromScreen = state.screen;
+ const st = document.getElementById('camStatus');
+ const btn = document.querySelector('.shutter');
+ if(btn) btn.disabled = true;
+ if(st) st.textContent = 'Snapping…';
+ announce('Snapping photo…');
+ setTimeout(()=>{
+ if(state.screen!==fromScreen) return; // user navigated away mid-shot
+ state.item = randomItem();
+ if(loadingLabel){ if(st) st.textContent = loadingLabel; announce(loadingLabel); setTimeout(()=>{ if(state.screen===fromScreen) cb(); }, 500); }
+ else cb();
+ }, 700);
+}
+function afterUpdateScan(){ state.demoNoMatch ? go('update-nomatch') : go('update-match'); }
+function afterSampleScan(){ state.demoNoMatch ? go('sample-nomatch') : go('sample-match'); }
+function tryFindMethod(){
+ go('find-loading');
+ announce('Looking for matches…');
+ setTimeout(()=>{ if(state.screen==='find-loading') goReplace(state.demoNoMatch ? 'find-nomatch' : 'find-stack'); }, 600);
+}
+
+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>`;
+}
+function brandGridHtml(nextScreen){
+ let html = `<input type="text" class="searchbox" placeholder="Search brands…" aria-label="Search brands" oninput="filterBrands(this.value)">`;
+ html += `<div id="brandRecent"><p class="sectionlabel">Recent</p><div class="brandgrid">`;
+ RECENT_BRANDS.forEach(b=>{ html += `<button class="brandcard" onclick="state.brand='${b}'; go('${nextScreen}')">${b}</button>`; });
+ html += `</div></div>`;
+ html += `<p class="sectionlabel">All Brands</p><div class="brandgrid" id="brandFullGrid">`;
+ BRANDS.forEach(b=>{ html += `<button class="brandcard" onclick="state.brand='${b}'; go('${nextScreen}')">${b}</button>`; });
+ html += `</div>`;
+ return html;
+}
+function filterBrands(q){
+ q = q.trim().toLowerCase();
+ const recentWrap = document.getElementById('brandRecent');
+ if(recentWrap) recentWrap.style.display = q ? 'none' : '';
+ const full = document.getElementById('brandFullGrid');
+ if(!full) return;
+ full.querySelectorAll('.brandcard').forEach(btn=>{
+ btn.style.display = btn.textContent.toLowerCase().includes(q) ? '' : 'none';
+ });
+}
+function showToast(msg){
+ const t = document.getElementById('toast');
+ t.innerHTML = `${msg} <button class="toastundo" onclick="undoLast()">Undo</button>`;
+ t.classList.add('show');
+ announce(msg + '. Undo available for 5 seconds.');
+ clearTimeout(toastTimer);
+ toastTimer = setTimeout(()=>{ t.classList.remove('show'); }, 5000);
+}
+function undoLast(){
+ const t = document.getElementById('toast');
+ t.classList.remove('show');
+ clearTimeout(toastTimer);
+ if(state.confirmScreen) go(state.confirmScreen);
+}
+function saveDraft(){ state.confirmScreen='new-savecheck'; go('new-done'); showToast('Saved'); }
+function finishUpdate(){ state.confirmScreen='update-review'; go('update-done'); showToast('Updated'); }
+
+// 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(){
+ // defensive: never let a screen render around missing item/brand state —
+ // route home instead of throwing (e.g. tab-switch mid-flow, or any other
+ // path that leaves a stale screen pointing at cleared state).
+ if(ITEM_SCREENS.includes(state.screen) && !state.item){
+ state.screen = 'home';
+ historyStack = [];
+ }
+ const s = document.getElementById('screen');
+ const onHome = state.screen==='home';
+ document.getElementById('backBtn').style.display = onHome ? 'none' : 'inline-flex';
+ document.getElementById('homeBtnTop').style.display = onHome ? 'none' : 'inline-flex';
+ ['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 += stepLabel() + `<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 += stepLabel();
+ html += maybeHint();
+ 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 += stepLabel() + `<h1 class="title">Which Brand?</h1>` + brandGridHtml('new-savecheck');
+ }
+ else if(state.screen==='new-savecheck'){
+ html += stepLabel();
+ 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="saveDraft()">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><p class="itemmeta">Brand: ${state.brand} · Code: ${state.item.code}</p><p class="itemmeta">Saved as a draft.</p></div>`;
+ html += `<div class="row"><button class="btn btn-primary" onclick="startJob('new')">Do Another</button><button class="btn btn-secondary" onclick="go('home')">Home</button></div>`;
+ }
+
+ else if(state.screen==='update-scan'){
+ html += stepLabel() + `<h1 class="title">Update an Item</h1>` + cameraCard('Show me the label or pattern', "shoot(afterUpdateScan,'Looking for matches…')");
+ html += demoToggleHtml();
+ }
+ else if(state.screen==='update-nomatch'){
+ html += `<div class="card" style="text-align:center;"><h1 class="title">No Match Found</h1><p class="sub">We couldn't find a matching item.</p></div>`;
+ html += `<div class="row"><button class="btn btn-secondary" onclick="go('update-scan')">Try again</button><button class="btn btn-primary" onclick="go('update-typecode')">Type the code instead</button></div>`;
+ }
+ else if(state.screen==='update-typecode'){
+ html += `<div class="card"><h1 class="title" style="font-size:20px;">Type the Code</h1><input type="text" class="codeinput" placeholder="e.g. DW-10423" aria-label="Item code"></div>`;
+ html += `<button class="btn btn-primary" onclick="state.item=MOCK_ITEMS[0]; go('update-match')">Find</button>`;
+ }
+ else if(state.screen==='update-match'){
+ html += stepLabel();
+ html += maybeHint();
+ 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 += stepLabel() + `<h1 class="title">New Photo</h1>` + cameraCard('Take the new photo', "shoot(()=>go('update-review'))");
+ }
+ else if(state.screen==='update-review'){
+ html += stepLabel();
+ html += maybeHint();
+ 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', 'Looks Good');
+ }
+ 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><p class="itemmeta">Brand: ${state.item.brand} · Code: ${state.item.code}</p><p class="itemmeta">Photo updated.</p></div>`;
+ html += `<div class="row"><button class="btn btn-primary" onclick="startJob('update')">Do Another</button><button class="btn btn-secondary" onclick="go('home')">Home</button></div>`;
+ }
+
+ else if(state.screen==='find-method'){
+ html += stepLabel() + `<h1 class="title">Find an Item</h1>`;
+ html += `<button class="btn btn-secondary" onclick="tryFindMethod()">Scan the Label</button>`;
+ html += `<button class="btn btn-secondary" onclick="tryFindMethod()">Say the Name</button>`;
+ html += `<button class="btn btn-secondary" onclick="tryFindMethod()">Type It</button>`;
+ html += demoToggleHtml();
+ }
+ else if(state.screen==='find-loading'){
+ html += `<div class="card" style="text-align:center;"><p class="sub">Looking for matches…</p></div>`;
+ }
+ else if(state.screen==='find-nomatch'){
+ html += `<div class="card" style="text-align:center;"><h1 class="title">No Match Found</h1><p class="sub">We couldn't find a matching item.</p></div>`;
+ html += `<div class="row"><button class="btn btn-secondary" onclick="go('find-method')">Try again</button><button class="btn btn-primary" onclick="go('find-typecode')">Type the code instead</button></div>`;
+ }
+ else if(state.screen==='find-typecode'){
+ html += `<div class="card"><h1 class="title" style="font-size:20px;">Type the Code</h1><input type="text" class="codeinput" placeholder="e.g. DW-10423" aria-label="Item code"></div>`;
+ html += `<button class="btn btn-primary" onclick="state.item=MOCK_ITEMS[0]; go('find-detail')">Find</button>`;
+ }
+ else if(state.screen==='find-stack'){
+ const it = MOCK_ITEMS[state.resultIndex % MOCK_ITEMS.length];
+ html += stepLabel();
+ html += maybeHint('Swipe right = pick this one, left = skip');
+ 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 += stepLabel();
+ 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} · ${state.item.brand}</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 += stepLabel() + `<h1 class="title">A Sample Came In</h1>` + cameraCard('Scan the label on the box', "shoot(afterSampleScan,'Looking for matches…')");
+ html += demoToggleHtml();
+ }
+ else if(state.screen==='sample-nomatch'){
+ html += `<div class="card" style="text-align:center;"><h1 class="title">No Match Found</h1><p class="sub">We couldn't find a matching item.</p></div>`;
+ html += `<div class="row"><button class="btn btn-secondary" onclick="go('sample-scan')">Try again</button><button class="btn btn-primary" onclick="go('sample-typecode')">Type the code instead</button></div>`;
+ }
+ else if(state.screen==='sample-typecode'){
+ html += `<div class="card"><h1 class="title" style="font-size:20px;">Type the Code</h1><input type="text" class="codeinput" placeholder="e.g. DW-10423" aria-label="Item code"></div>`;
+ html += `<button class="btn btn-primary" onclick="state.item=MOCK_ITEMS[0]; go('sample-match')">Find</button>`;
+ }
+ else if(state.screen==='sample-match'){
+ html += stepLabel();
+ html += maybeHint();
+ 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 += stepLabel();
+ 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><div class="swatch ${state.item.sw}"></div><p class="itemname">${state.item.name} — ${state.item.color}</p><p class="itemmeta">Brand: ${state.item.brand} · Code: ${state.item.code}</p><p class="itemmeta">Sticker printed and item received.</p></div>`;
+ html += `<div class="row"><button class="btn btn-primary" onclick="startJob('sample')">Do Another</button><button class="btn btn-secondary" 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, finishUpdate, ()=>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(){
+ state.confirmScreen = 'sample-match';
+ go('sample-printing');
+ announce('Printing sticker…');
+ setTimeout(()=>{ if(state.screen==='sample-printing'){ go('sample-done'); showToast('Marked Received'); } }, 900);
+}
+
+render();
+</script>
+</body>
+</html>
diff --git a/public/kid-simple/v6-one-thing.html b/public/kid-simple/v6-one-thing.html
new file mode 100644
index 0000000..6d04237
--- /dev/null
+++ b/public/kid-simple/v6-one-thing.html
@@ -0,0 +1,460 @@
+<!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:#8A8074; --taupe-text:#6B6156; --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; }
+ .stepline{ text-align:center; font-size:14px; font-weight:800; letter-spacing:.03em; text-transform:uppercase; color:var(--taupe-text); margin:0 0 8px; }
+ .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:active{transform:scale(.97);}
+ .bigbtn-primary{ background:var(--gold); border:2px solid var(--gold); color:#3a2a06; }
+ .bigbtn-secondary{ background:#fff; border:2px solid var(--taupe); color:var(--taupe-text); }
+ .bigbtn:focus-visible, .linktext:focus-visible, .navfab:focus-visible, .demotoggle:focus-visible,
+ .codeinput:focus-visible, .searchbox:focus-visible, .toastundo:focus-visible{ outline:3px solid #2B6CB0; outline-offset:2px; }
+ .linktext{
+ background:none; border:none; color:var(--taupe-text); 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:420px; }
+ .searchbox, .codeinput{
+ width:100%; min-height:56px; border-radius:16px; border:2px solid #e7e1d6; padding:0 16px;
+ font-size:17px; font-family:'Inter',sans-serif; background:#fff; color:var(--ink); margin-bottom:6px;
+ }
+ .sectionlabel{ margin:14px 0 0; font-size:14px; font-weight:800; letter-spacing:.04em; text-transform:uppercase; color:var(--taupe-text); }
+ .demotoggle{
+ background:none; border:2px dashed #cbbfa0; color:#4a4a4a; font-size:17px; font-weight:700;
+ border-radius:12px; padding:8px 12px; min-height:56px; align-self:center; margin-top:10px;
+ }
+ .demotoggle:active{transform:scale(.97);}
+ .demotoggle-dark{
+ position:absolute; top:96px; left:20px; right:20px; text-align:center;
+ background:rgba(255,255,255,.14); border:2px dashed rgba(255,255,255,.45); color:#f5efe6;
+ font-size:17px; font-weight:700; border-radius:12px; padding:8px 12px; min-height:56px;
+ }
+ .demotoggle-dark:active{transform:scale(.97);}
+ .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:17px; 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;
+ }
+ .shutter:active{transform:translateX(-50%) scale(.97);}
+ .center{ display:flex; flex-direction:column; align-items:center; justify-content:center; flex:1; gap:10px; }
+ .navcluster{
+ position:absolute; top:14px; left:14px; z-index:20; display:none; gap:8px;
+ }
+ .navfab{
+ 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:flex; align-items:center; justify-content:center; gap:4px;
+ padding:0 12px; box-shadow:0 4px 10px rgba(0,0,0,.12);
+ }
+ .navfab:active{transform:scale(.97);}
+ .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;}
+ .toast{
+ position:absolute; left:16px; right:16px; bottom:16px; background:#232323; color:#fff;
+ border-radius:14px; padding:14px 18px; display:flex; align-items:center; justify-content:space-between;
+ gap:12px; font-size:16px; font-weight:700; opacity:0; transform:translateY(12px); pointer-events:none;
+ transition:opacity .25s ease, transform .25s ease; z-index:30;
+ }
+ .toast.show{opacity:1; transform:translateY(0); pointer-events:auto;}
+ .toastundo{background:none; border:none; color:var(--gold); font-weight:800; font-size:16px; min-height:56px; padding:0 6px;}
+ @media (prefers-reduced-motion: reduce){
+ *{transition:none !important; animation:none !important;}
+ }
+</style>
+</head>
+<body>
+<div class="phone">
+ <div class="navcluster" id="navCluster">
+ <button class="navfab" aria-label="Back" onclick="goBack()">‹ Back</button>
+ <button class="navfab" aria-label="Home" onclick="go('home')">⌂ Home</button>
+ </div>
+ <div class="screen" id="screen" role="main"></div>
+ <div class="toast" id="toast" role="status"></div>
+ <div class="sr-only" id="live" aria-live="polite"></div>
+</div>
+
+<script>
+const MOCK_ITEMS = [
+ {name:'Palm Damask', color:'Sage', code:'DW-10423', sw:'sw1', brand:'York Wallcoverings'},
+ {name:'Trellis Weave', color:'Indigo', code:'DW-88214', sw:'sw2', brand:'Cole & Son'},
+ {name:'Linen Bloom', color:'Blush', code:'DW-55071', sw:'sw3', brand:'Thibaut'},
+];
+const BRANDS = ['York Wallcoverings','Brewster Home','Kravet','Cole & Son','Thibaut','Ronald Redding'];
+const RECENT_BRANDS = BRANDS.slice(0,3);
+const DONE_TOAST = {'new-done':'Saved','update-done':'Updated','sample-done':'Marked Received'};
+const STEP_MAP = {
+ 'new-brand':[2,3], 'new-save':[3,3],
+ 'update-match':[2,3],
+ 'find-method':[1,3], 'find-result':[2,3], 'find-detail':[3,3],
+ 'sample-match':[2,2],
+};
+const PHOTOCHECK_STEP = { 'new-cam':[1,3], 'update-cam2':[3,3] };
+
+const state = { screen:'home', job:null, item:null, brand:null, resultIndex:0, demoNoMatch:false, checkNext:null, checkRetake:null, confirmScreen:null };
+let historyStack = [];
+let toastTimer = null;
+
+function announce(text){ const el=document.getElementById('live'); if(el) el.textContent = text; }
+function go(screen){
+ if(screen==='home'){ historyStack = []; }
+ else if(state.screen && state.screen!==screen){ historyStack.push(state.screen); }
+ state.screen = screen;
+ render();
+}
+function goReplace(screen){ state.screen = screen; render(); }
+function goBack(){
+ if(historyStack.length){ state.screen = historyStack.pop(); render(); }
+ else go('home');
+}
+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 stepLine(){ const m = STEP_MAP[state.screen]; return m ? `<p class="stepline">Step ${m[0]} of ${m[1]}</p>` : ''; }
+function toggleDemo(){ state.demoNoMatch = !state.demoNoMatch; render(); }
+function demoToggleHtml(dark){
+ const cls = dark ? 'demotoggle-dark' : 'demotoggle';
+ return `<button class="${cls}" aria-pressed="${state.demoNoMatch}" onclick="toggleDemo()">Demo: show "no match" ${state.demoNoMatch?'✅ On':'⬜ Off'}</button>`;
+}
+
+function cameraScreen(hint, onShoot, extra){
+ return `<div class="camwrap"><div class="camview">
+ <div class="hint">${hint}</div>
+ <div class="status" id="camStatus">Tap the shutter</div>
+ ${extra||''}
+ <button class="shutter" aria-label="Take photo" onclick="${onShoot}">📸</button>
+ </div></div>`;
+}
+function shoot(cb, loadingLabel){
+ const fromScreen = state.screen;
+ const st = document.getElementById('camStatus');
+ const btn = document.querySelector('.shutter');
+ if(btn) btn.disabled = true;
+ if(st) st.textContent = 'Snapping…';
+ announce('Snapping photo…');
+ setTimeout(()=>{
+ if(state.screen!==fromScreen) return; // user navigated away mid-shot
+ state.item = randomItem();
+ if(loadingLabel){ if(st) st.textContent = loadingLabel; announce(loadingLabel); setTimeout(()=>{ if(state.screen===fromScreen) cb(); }, 500); }
+ else cb();
+ }, 700);
+}
+function shootThen(nextScreen, retakeScreen){
+ shoot(()=>{ state.checkNext=nextScreen; state.checkRetake=retakeScreen; go('photocheck'); });
+}
+function shootOrMiss(nextScreen, retakeScreen, missScreen, loadingLabel){
+ shoot(()=>{
+ if(state.demoNoMatch){ go(missScreen); return; }
+ go(nextScreen);
+ }, loadingLabel);
+}
+function performAction(next, toastMsg){
+ state.confirmScreen = state.screen;
+ go(next);
+ if(toastMsg) showToast(toastMsg);
+}
+function finishCheck(){ performAction(state.checkNext, DONE_TOAST[state.checkNext]); }
+function markReceived(){
+ state.confirmScreen = state.screen;
+ go('sample-printing');
+ announce('Printing sticker…');
+}
+function tryFind(){
+ go('find-loading');
+ announce('Looking for matches…');
+ setTimeout(()=>{ if(state.screen==='find-loading') goReplace(state.demoNoMatch ? 'find-nomatch' : 'find-result'); }, 600);
+}
+function filterBrands(q){
+ q = q.trim().toLowerCase();
+ const recentWrap = document.getElementById('brandRecent');
+ if(recentWrap) recentWrap.style.display = q ? 'none' : '';
+ const full = document.getElementById('brandFullList');
+ if(!full) return;
+ full.querySelectorAll('button').forEach(btn=>{
+ btn.style.display = btn.textContent.toLowerCase().includes(q) ? '' : 'none';
+ });
+}
+function showToast(msg){
+ const t = document.getElementById('toast');
+ t.innerHTML = `${msg} <button class="toastundo" onclick="undoLast()">Undo</button>`;
+ t.classList.add('show');
+ announce(msg + '. Undo available for 5 seconds.');
+ clearTimeout(toastTimer);
+ toastTimer = setTimeout(()=>{ t.classList.remove('show'); }, 5000);
+}
+function undoLast(){
+ const t = document.getElementById('toast');
+ t.classList.remove('show');
+ clearTimeout(toastTimer);
+ if(state.confirmScreen) go(state.confirmScreen);
+}
+function brandListHtml(nextScreen){
+ let html = `<input type="text" class="searchbox" placeholder="Search brands…" aria-label="Search brands" oninput="filterBrands(this.value)">`;
+ html += `<div id="brandRecent"><p class="sectionlabel">Recent</p><div class="brandlist">`;
+ RECENT_BRANDS.forEach(b=>{ html += `<button class="bigbtn" onclick="state.brand='${b}'; go('${nextScreen}')">${b}</button>`; });
+ html += `</div></div>`;
+ html += `<p class="sectionlabel">All Brands</p><div class="brandlist" id="brandFullList">`;
+ BRANDS.forEach(b=>{ html += `<button class="bigbtn" onclick="state.brand='${b}'; go('${nextScreen}')">${b}</button>`; });
+ html += `</div>`;
+ return html;
+}
+
+function render(){
+ const s = document.getElementById('screen');
+ const nav = document.getElementById('navCluster');
+ nav.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 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==='photocheck'){
+ const st = PHOTOCHECK_STEP[state.checkRetake];
+ html += st ? `<p class="stepline">Step ${st[0]} of ${st[1]}</p>` : '';
+ html += `<h1 class="question">Does this look right?</h1>
+ <div class="swatch ${state.item.sw}"></div>
+ <div class="answers" style="margin-top:20px;">
+ <button class="linktext" onclick="go(state.checkRetake)">Retake Photo</button>
+ <button class="bigbtn bigbtn-primary" onclick="finishCheck()">Looks Good</button>
+ </div>`;
+ }
+
+ else if(state.screen==='new-cam'){
+ html += cameraScreen('Take a photo of it.', "shootThen('new-brand','new-cam')");
+ }
+ else if(state.screen==='new-brand'){
+ html += stepLine() + `<h1 class="question">Which brand is it?</h1>` + brandListHtml('new-save');
+ }
+ else if(state.screen==='new-save'){
+ html += stepLine();
+ 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="linktext" onclick="go('new-cam')">Retake Photo</button>
+ <button class="bigbtn bigbtn-primary" onclick="performAction('new-done','Saved')">Save as Draft</button>
+ </div>`;
+ }
+ else if(state.screen==='new-done'){
+ html += `<div class="center">
+ <h1 class="question">Saved! 🎉</h1>
+ <div class="swatch small ${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>
+ <div class="answers">
+ <button class="bigbtn bigbtn-primary" onclick="startJob('new')">Do Another</button>
+ <button class="linktext" onclick="go('home')">Home</button>
+ </div>`;
+ }
+
+ else if(state.screen==='update-cam'){
+ html += cameraScreen('Scan the label or pattern.', "shootOrMiss('update-match','update-cam','update-nomatch','Looking for matches…')", demoToggleHtml(true));
+ }
+ else if(state.screen==='update-nomatch'){
+ html += `<h1 class="question">No match found.</h1>
+ <div class="answers">
+ <button class="bigbtn" onclick="go('update-cam')">Try again</button>
+ <button class="bigbtn bigbtn-primary" onclick="go('update-typecode')">Type the code instead</button>
+ </div>`;
+ }
+ else if(state.screen==='update-typecode'){
+ html += `<h1 class="question">Type the code.</h1>
+ <input type="text" class="codeinput" placeholder="e.g. DW-10423" aria-label="Item code">
+ <div class="answers" style="margin-top:20px;">
+ <button class="bigbtn bigbtn-primary" onclick="state.item=MOCK_ITEMS[0]; go('update-match')">Find</button>
+ </div>`;
+ }
+ else if(state.screen==='update-match'){
+ html += stepLine();
+ 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-secondary" onclick="go('update-cam')">No, Let Me Search</button>
+ <button class="bigbtn bigbtn-primary" onclick="go('update-cam2')">Yes</button>
+ </div>`;
+ }
+ else if(state.screen==='update-cam2'){
+ html += cameraScreen('Take the new photo.', "shootThen('update-done','update-cam2')");
+ }
+ else if(state.screen==='update-done'){
+ html += `<div class="center">
+ <h1 class="question">Updated! ✅</h1>
+ <div class="swatch small ${state.item.sw}"></div>
+ <p class="itemname">${state.item.name} — ${state.item.color}</p>
+ <p class="itemmeta">Brand: ${state.item.brand} · Code: ${state.item.code}</p>
+ </div>
+ <div class="answers">
+ <button class="bigbtn bigbtn-primary" onclick="startJob('update')">Do Another</button>
+ <button class="linktext" onclick="go('home')">Home</button>
+ </div>`;
+ }
+
+ else if(state.screen==='find-method'){
+ html += stepLine();
+ html += `<h1 class="question">How do you want to find it?</h1>
+ <div class="answers">
+ <button class="bigbtn" onclick="tryFind()">Scan</button>
+ <button class="bigbtn" onclick="tryFind()">Talk</button>
+ <button class="bigbtn" onclick="tryFind()">Type</button>
+ </div>`;
+ html += demoToggleHtml(false);
+ }
+ else if(state.screen==='find-loading'){
+ html += `<div class="center"><h1 class="question">Looking for matches…</h1></div>`;
+ }
+ else if(state.screen==='find-nomatch'){
+ html += `<h1 class="question">No match found.</h1>
+ <div class="answers">
+ <button class="bigbtn" onclick="go('find-method')">Try again</button>
+ <button class="bigbtn bigbtn-primary" onclick="go('find-typecode')">Type the code instead</button>
+ </div>`;
+ }
+ else if(state.screen==='find-typecode'){
+ html += `<h1 class="question">Type the code.</h1>
+ <input type="text" class="codeinput" placeholder="e.g. DW-10423" aria-label="Item code">
+ <div class="answers" style="margin-top:20px;">
+ <button class="bigbtn bigbtn-primary" onclick="state.item=MOCK_ITEMS[0]; go('find-detail')">Find</button>
+ </div>`;
+ }
+ else if(state.screen==='find-result'){
+ const it = MOCK_ITEMS[state.resultIndex];
+ html += stepLine();
+ 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-secondary" onclick="nextResult()">Next Result</button>
+ <button class="bigbtn bigbtn-primary" onclick="pickResult()">Yes, This One</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 += stepLine();
+ 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} · ${state.item.brand}</p>
+ </div>
+ <div class="answers">
+ <button class="linktext" onclick="go('home')">Home</button>
+ <button class="bigbtn bigbtn-secondary" onclick="go('find-method')">Search Again</button>
+ <button class="bigbtn bigbtn-primary" onclick="go('update-cam2')">Update This</button>
+ </div>`;
+ }
+
+ else if(state.screen==='sample-cam'){
+ html += cameraScreen('Scan the box label.', "shootOrMiss('sample-match','sample-cam','sample-nomatch','Looking for matches…')", demoToggleHtml(true));
+ }
+ else if(state.screen==='sample-nomatch'){
+ html += `<h1 class="question">No match found.</h1>
+ <div class="answers">
+ <button class="bigbtn" onclick="go('sample-cam')">Try again</button>
+ <button class="bigbtn bigbtn-primary" onclick="go('sample-typecode')">Type the code instead</button>
+ </div>`;
+ }
+ else if(state.screen==='sample-typecode'){
+ html += `<h1 class="question">Type the code.</h1>
+ <input type="text" class="codeinput" placeholder="e.g. DW-10423" aria-label="Item code">
+ <div class="answers" style="margin-top:20px;">
+ <button class="bigbtn bigbtn-primary" onclick="state.item=MOCK_ITEMS[0]; go('sample-match')">Find</button>
+ </div>`;
+ }
+ else if(state.screen==='sample-match'){
+ html += stepLine();
+ 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-secondary" onclick="go('sample-cam')">No, Scan Again</button>
+ <button class="bigbtn bigbtn-primary" onclick="markReceived()">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'); showToast('Marked Received'); } }, 1100);
+ }
+ else if(state.screen==='sample-done'){
+ html += `<div class="center">
+ <h1 class="question">Done! ✅</h1>
+ <div class="swatch small ${state.item.sw}"></div>
+ <p class="itemname">${state.item.name} — ${state.item.color}</p>
+ <p class="itemmeta">Brand: ${state.item.brand} · Code: ${state.item.code}</p>
+ <p class="itemmeta">Sticker printed and item received.</p>
+ </div>
+ <div class="answers">
+ <button class="bigbtn bigbtn-primary" onclick="startJob('sample')">Do Another</button>
+ <button class="linktext" onclick="go('home')">Home</button>
+ </div>`;
+ }
+
+ 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/kid-simple/v7-scan-and-go.html b/public/kid-simple/v7-scan-and-go.html
new file mode 100644
index 0000000..333ee91
--- /dev/null
+++ b/public/kid-simple/v7-scan-and-go.html
@@ -0,0 +1,324 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+<title>v7 — Scan & Go</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=Spectral:wght@500;600;700&family=Manrope:wght@500;600;700;800&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --teal:#0F3D3E; --teal-2:#164C4D; --copper:#C7784F; --copper-dark:#9B5A38;
+ --parchment:#F6EFE4; --ink:#2E2A26; --muted:#8A7F6E; --sage:#5C8F6B;
+ --line:#E7DDCB;
+ }
+ *{box-sizing:border-box;}
+ html,body{margin:0;padding:0;background:#d9d3c8;font-family:'Manrope',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(--parchment); border-radius:36px; overflow:hidden;
+ box-shadow:0 24px 60px rgba(0,0,0,.30); display:flex; flex-direction:column; position:relative;
+ }
+ .screen{ flex:1; display:flex; flex-direction:column; }
+ h1,h2{ font-family:'Spectral',serif; margin:0; }
+ button{ font-family:'Manrope',sans-serif; cursor:pointer; }
+
+ /* ---- viewfinder (dark scan screens) ---- */
+ .viewfinder{
+ flex:1; min-height:780px; background:radial-gradient(120% 90% at 50% 0%, #1a5152 0%, var(--teal) 55%, #0A2C2D 100%);
+ display:flex; flex-direction:column; align-items:center; color:#F6EFE4; position:relative; overflow:hidden;
+ padding:44px 24px 30px;
+ }
+ .dw-mark{ position:absolute; top:16px; left:20px; font-size:12px; letter-spacing:2px; color:rgba(246,239,228,.55); font-weight:700; }
+ .hint-top{
+ font-family:'Spectral',serif; font-weight:600; font-size:22px; line-height:1.3; text-align:center;
+ margin:14px 0 22px; color:#FBF7F0;
+ }
+ .scan-frame{
+ width:220px; height:220px; position:relative; margin:6px auto 20px; flex-shrink:0;
+ }
+ .scan-frame .corner{ position:absolute; width:36px; height:36px; border-color:var(--copper); border-style:solid; }
+ .corner.tl{ top:0; left:0; border-width:5px 0 0 5px; border-top-left-radius:10px; }
+ .corner.tr{ top:0; right:0; border-width:5px 5px 0 0; border-top-right-radius:10px; }
+ .corner.bl{ bottom:0; left:0; border-width:0 0 5px 5px; border-bottom-left-radius:10px; }
+ .corner.br{ bottom:0; right:0; border-width:0 5px 5px 0; border-bottom-right-radius:10px; }
+ .scan-frame .tag-icon{ position:absolute; inset:0; display:flex; align-items:center; justify-content:center; font-size:64px; opacity:.9; }
+ .scanline{
+ position:absolute; left:8px; right:8px; height:3px; background:linear-gradient(90deg, transparent, #FFD9B0, transparent);
+ box-shadow:0 0 12px 2px rgba(255,178,110,.7); animation:sweep 1.8s ease-in-out infinite;
+ }
+ @keyframes sweep{ 0%{top:6px;} 50%{top:206px;} 100%{top:6px;} }
+ .scan-status{ min-height:22px; font-size:15px; font-weight:700; color:#FFD9B0; text-align:center; margin-bottom:14px; }
+
+ .toggle-row{ display:flex; gap:10px; width:100%; margin-bottom:18px; }
+ .toggle-btn{
+ flex:1; min-height:56px; border-radius:16px; border:2px solid rgba(246,239,228,.28); background:rgba(246,239,228,.08);
+ color:#F6EFE4; font-size:15px; font-weight:700; display:flex; align-items:center; justify-content:center; gap:6px;
+ }
+ .toggle-btn.active{ background:var(--copper); border-color:var(--copper); color:#2A1608; }
+ .toggle-btn:focus-visible{ outline:3px solid #6fd3d6; outline-offset:2px; }
+
+ .scanbtn{
+ width:100%; min-height:68px; border-radius:22px; background:var(--copper); border:none; color:#2A1608;
+ font-size:20px; font-weight:800; letter-spacing:.3px; box-shadow:0 10px 24px rgba(199,120,79,.35);
+ }
+ .scanbtn:disabled{ opacity:.7; }
+ .scanbtn:focus-visible{ outline:3px solid #6fd3d6; outline-offset:2px; }
+
+ .vf-link{
+ margin-top:16px; background:none; border:none; color:#D9CFC0; font-size:16px; text-decoration:underline;
+ min-height:56px; width:100%;
+ }
+ .vf-link:focus-visible{ outline:3px solid #6fd3d6; outline-offset:2px; }
+
+ .shutter-wrap{ flex:1; display:flex; align-items:flex-end; justify-content:center; padding-bottom:26px; width:100%; }
+ .shutter{
+ width:92px; height:92px; border-radius:50%; background:var(--copper); border:6px solid #F6EFE4;
+ font-size:36px; display:flex; align-items:center; justify-content:center; box-shadow:0 10px 26px rgba(0,0,0,.35);
+ }
+ .shutter:focus-visible{ outline:3px solid #6fd3d6; outline-offset:2px; }
+
+ /* ---- card / content screens (warm parchment) ---- */
+ .card-screen{ flex:1; display:flex; flex-direction:column; padding:34px 26px 28px; background:var(--parchment); }
+ .topbar{ display:flex; align-items:center; justify-content:space-between; margin-bottom:6px; }
+ .homefab{
+ min-width:52px; min-height:52px; border-radius:14px; background:#fff; border:2px solid var(--line);
+ color:var(--ink); font-size:14px; font-weight:700; display:flex; align-items:center; justify-content:center; gap:4px;
+ padding:0 12px;
+ }
+ .homefab:focus-visible{ outline:3px solid #2B6CB0; outline-offset:2px; }
+ .kicker{ font-size:13px; font-weight:800; letter-spacing:1.5px; text-transform:uppercase; color:var(--copper-dark); margin:2px 0 8px; }
+ .headline{ font-size:28px; font-weight:600; line-height:1.22; color:var(--ink); margin:0 0 22px; }
+
+ .item-card{
+ background:#fff; border:2px solid var(--line); border-radius:22px; padding:20px; display:flex; flex-direction:column;
+ align-items:center; gap:10px; margin-bottom:22px;
+ }
+ .swatch{ width:100%; height:150px; border-radius:16px; }
+ .sw1{background:repeating-linear-gradient(45deg,#5C8F6B 0 18px,#4c7a5a 18px 36px);}
+ .sw2{background:repeating-linear-gradient(-45deg,#7C93B0 0 16px,#617a99 16px 32px);}
+ .sw3{background:repeating-linear-gradient(90deg,#C7784F 0 14px,#9B5A38 14px 28px);}
+ .itemname{ font-size:20px; font-weight:800; margin:0; text-align:center; }
+ .itemmeta{ font-size:15px; color:var(--muted); margin:0; text-align:center; }
+
+ .stack{ display:flex; flex-direction:column; gap:14px; }
+ .btn{
+ width:100%; min-height:60px; border-radius:18px; font-size:18px; font-weight:800; padding:10px 16px;
+ border:2px solid var(--line); background:#fff; color:var(--ink);
+ }
+ .btn-primary{ background:var(--copper); border-color:var(--copper); color:#2A1608; }
+ .btn-secondary{ background:var(--teal); border-color:var(--teal); color:#F6EFE4; }
+ .btn:focus-visible{ outline:3px solid #2B6CB0; outline-offset:2px; }
+ .link{
+ background:none; border:none; color:var(--muted); font-size:16px; text-decoration:underline;
+ min-height:56px; width:100%; margin-top:6px;
+ }
+ .link:focus-visible{ outline:3px solid #2B6CB0; outline-offset:2px; }
+
+ .suggest-row{
+ display:flex; align-items:center; gap:14px; background:#fff; border:2px solid var(--line); border-radius:18px;
+ padding:12px 14px; min-height:64px; width:100%; text-align:left; margin-bottom:12px;
+ }
+ .suggest-row .sw-mini{ width:52px; height:52px; border-radius:12px; flex-shrink:0; }
+ .suggest-row .txt{ display:flex; flex-direction:column; gap:2px; }
+ .suggest-row .txt b{ font-size:16px; }
+ .suggest-row .txt span{ font-size:13px; color:var(--muted); }
+
+ .brandlist{ display:flex; flex-direction:column; gap:12px; overflow-y:auto; }
+ .brand-row{
+ width:100%; min-height:60px; border-radius:16px; border:2px solid var(--line); background:#fff; color:var(--ink);
+ font-size:17px; font-weight:700; text-align:left; padding:0 18px;
+ }
+
+ .center{ display:flex; flex-direction:column; align-items:center; justify-content:center; flex:1; gap:14px; text-align:center; }
+ .bigcheck{ font-size:56px; }
+</style>
+</head>
+<body>
+<div class="phone">
+ <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', simMode:'known', item:null };
+
+function go(screen){ state.screen = screen; render(); }
+function randomItem(){ return MOCK_ITEMS[Math.floor(Math.random()*MOCK_ITEMS.length)]; }
+function homeFab(){ return `<div class="topbar"><button class="homefab" onclick="go('home')">⌂ Home</button><span></span></div>`; }
+
+function doScan(){
+ const btn = document.getElementById('scanBtn');
+ const st = document.getElementById('scanStatus');
+ if(btn) btn.disabled = true;
+ if(st) st.textContent = 'Scanning…';
+ setTimeout(()=>{
+ if(state.simMode==='known'){ state.item = randomItem(); go('scan-match'); }
+ else { go('scan-nomatch'); }
+ }, 850);
+}
+
+function render(){
+ const s = document.getElementById('screen');
+ let html = '';
+
+ if(state.screen==='home'){
+ html += `<div class="viewfinder">
+ <div class="dw-mark">DESIGNER WALLCOVERINGS</div>
+ <div class="hint-top">Point your camera<br>at a tag or box</div>
+ <div class="scan-frame">
+ <div class="corner tl"></div><div class="corner tr"></div>
+ <div class="corner bl"></div><div class="corner br"></div>
+ <div class="tag-icon">${state.simMode==='known' ? '🏷️' : '🆕'}</div>
+ <div class="scanline"></div>
+ </div>
+ <div class="scan-status" id="scanStatus"> </div>
+ <div class="toggle-row">
+ <button class="toggle-btn ${state.simMode==='known'?'active':''}" onclick="state.simMode='known'; render();">🏷️ Known Tag</button>
+ <button class="toggle-btn ${state.simMode!=='known'?'active':''}" onclick="state.simMode='new'; render();">🆕 New Item</button>
+ </div>
+ <button class="scanbtn" id="scanBtn" onclick="doScan()">Tap to Scan</button>
+ <button class="vf-link" onclick="go('find-search')">Or type a code instead</button>
+ </div>`;
+ }
+
+ else if(state.screen==='scan-match'){
+ html += `<div class="card-screen">${homeFab()}
+ <div class="kicker">Found It</div>
+ <h1 class="headline">What do you want to do?</h1>
+ <div class="item-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>
+ <div class="stack">
+ <button class="btn btn-primary" onclick="go('update-camera')">Update It</button>
+ <button class="btn btn-secondary" onclick="go('sample-printing')">Sample Just Arrived</button>
+ <button class="link" onclick="go('home')">Not this one — scan again</button>
+ </div>
+ </div>`;
+ }
+
+ else if(state.screen==='scan-nomatch'){
+ html += `<div class="card-screen">${homeFab()}
+ <div class="center" style="flex:none; margin:30px 0 26px;">
+ <div class="bigcheck">🔎</div>
+ <h1 class="headline" style="margin-bottom:0;">Hmm, we don't<br>recognize this one.</h1>
+ </div>
+ <div class="stack">
+ <button class="btn btn-primary" onclick="go('add-brand')">Add It as New</button>
+ <button class="link" onclick="go('home')">Scan Again</button>
+ </div>
+ </div>`;
+ }
+
+ else if(state.screen==='add-camera'){
+ html += cameraScreen('Take a photo of it.', "add-brand");
+ }
+ else if(state.screen==='add-brand'){
+ html += `<div class="card-screen">${homeFab()}
+ <div class="kicker">Almost Done</div>
+ <h1 class="headline">Which brand is it?</h1>
+ <div class="brandlist">${BRANDS.map(b=>`<button class="brand-row" onclick="go('add-done')">${b}</button>`).join('')}</div>
+ </div>`;
+ }
+ else if(state.screen==='add-done'){
+ html += `<div class="card-screen"><div class="center">
+ <div class="bigcheck">🎉</div>
+ <h1 class="headline" style="margin-bottom:0;">Saved as a new item!</h1>
+ <div class="stack" style="width:100%; margin-top:18px;">
+ <button class="btn btn-primary" onclick="go('home')">Scan Something Else</button>
+ </div>
+ </div></div>`;
+ }
+
+ else if(state.screen==='update-camera'){
+ html += cameraScreen('Take the new photo.', "update-done");
+ }
+ else if(state.screen==='update-done'){
+ html += `<div class="card-screen"><div class="center">
+ <div class="bigcheck">✅</div>
+ <h1 class="headline" style="margin-bottom:0;">Updated!</h1>
+ <div class="stack" style="width:100%; margin-top:18px;">
+ <button class="btn btn-primary" onclick="go('home')">Scan Something Else</button>
+ </div>
+ </div></div>`;
+ }
+
+ else if(state.screen==='sample-printing'){
+ html += `<div class="card-screen"><div class="center">
+ <div class="bigcheck">🖨️</div>
+ <h1 class="headline" style="margin-bottom:0;">Printing your sticker…</h1>
+ </div></div>`;
+ setTimeout(()=>{ if(state.screen==='sample-printing') go('sample-done'); }, 1100);
+ }
+ else if(state.screen==='sample-done'){
+ html += `<div class="card-screen"><div class="center">
+ <div class="bigcheck">✅</div>
+ <h1 class="headline" style="margin-bottom:0;">Done! Sample marked received.</h1>
+ <div class="stack" style="width:100%; margin-top:18px;">
+ <button class="btn btn-primary" onclick="go('home')">Scan Something Else</button>
+ </div>
+ </div></div>`;
+ }
+
+ else if(state.screen==='find-search'){
+ html += `<div class="card-screen">${homeFab()}
+ <div class="kicker">Find An Item</div>
+ <h1 class="headline">Which one are you<br>looking for?</h1>
+ ${MOCK_ITEMS.map(it=>`
+ <button class="suggest-row" onclick='findPick("${it.code}")'>
+ <div class="sw-mini ${it.sw}"></div>
+ <div class="txt"><b>${it.name}</b><span>${it.color} · code ${it.code}</span></div>
+ </button>`).join('')}
+ <button class="vf-link" style="color:var(--muted); margin-top:4px;" onclick="go('home')">Scan instead</button>
+ </div>`;
+ }
+ else if(state.screen==='find-detail'){
+ html += `<div class="card-screen">${homeFab()}
+ <div class="kicker">Here It Is</div>
+ <div class="item-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>
+ <div class="stack">
+ <button class="btn btn-primary" onclick="go('update-camera')">Update This</button>
+ <button class="btn btn-secondary" onclick="go('find-search')">Search Again</button>
+ </div>
+ </div>`;
+ }
+
+ s.innerHTML = html;
+}
+
+function cameraScreen(hint, nextScreen){
+ return `<div class="viewfinder" style="padding-top:56px;">
+ <div class="dw-mark">DESIGNER WALLCOVERINGS</div>
+ <div class="hint-top">${hint}</div>
+ <div class="shutter-wrap">
+ <button class="shutter" aria-label="Take photo" onclick="shoot('${nextScreen}')">📸</button>
+ </div>
+ </div>`;
+}
+function shoot(nextScreen){
+ const btn = document.querySelector('.shutter');
+ if(btn){ btn.disabled = true; btn.style.opacity = .6; }
+ setTimeout(()=> go(nextScreen), 650);
+}
+function findPick(code){
+ state.item = MOCK_ITEMS.find(i=>i.code===code);
+ go('find-detail');
+}
+
+render();
+</script>
+</body>
+</html>
diff --git a/public/kid-simple/v8-mode-dial.html b/public/kid-simple/v8-mode-dial.html
new file mode 100644
index 0000000..773b843
--- /dev/null
+++ b/public/kid-simple/v8-mode-dial.html
@@ -0,0 +1,354 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+<title>v8 — Mode Dial</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;9..144,800&family=Manrope:wght@400;600;700;800&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --navy:#141B2E; --navy2:#1E2740; --cream:#F6EFE4; --ink:#241C14; --muted:#8B8172;
+ --copper:#C97B3F; --copper-ink:#3A200C;
+ --teal:#4F8A8B; --teal-ink:#0D2626;
+ --plum:#8B5A8F; --plum-ink:#2B0F2C;
+ --mustard:#D4A24C; --mustard-ink:#3A2707;
+ --green:#4C9A6A;
+ }
+ *{box-sizing:border-box;}
+ html,body{margin:0;padding:0;background:#ddd;font-family:'Manrope',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;
+ }
+ button{ font-family:'Manrope',sans-serif; cursor:pointer; }
+ .screen{ flex:1; display:flex; flex-direction:column; position:relative; }
+
+ /* ---------- HOME: camera + mode dial ---------- */
+ .cam{
+ flex:1; background:
+ radial-gradient(120px 120px at 78% 22%, rgba(201,123,63,.28), transparent 60%),
+ radial-gradient(160px 160px at 15% 68%, rgba(79,138,139,.22), transparent 60%),
+ radial-gradient(220px 220px at 60% 85%, rgba(139,90,143,.20), transparent 60%),
+ linear-gradient(180deg, var(--navy) 0%, var(--navy2) 100%);
+ position:relative; display:flex; flex-direction:column; color:var(--cream); min-height:780px;
+ }
+ .dwmark{ position:absolute; top:18px; left:20px; font-family:'Fraunces',serif; font-weight:700; font-size:14px;
+ letter-spacing:.14em; color:rgba(246,239,228,.72); }
+ .reticle{
+ position:absolute; top:50%; left:50%; transform:translate(-50%,-58%);
+ width:200px; height:200px; border:2px solid rgba(246,239,228,.28); border-radius:28px;
+ }
+ .reticle::before,.reticle::after{ content:''; position:absolute; width:22px; height:22px; border-color:rgba(246,239,228,.65); }
+ .reticle::before{ top:-2px; left:-2px; border-top:3px solid; border-left:3px solid; border-radius:6px 0 0 0; }
+ .reticle::after{ bottom:-2px; right:-2px; border-bottom:3px solid; border-right:3px solid; border-radius:0 0 6px 0; }
+
+ .hint{
+ position:absolute; top:52px; left:20px; right:20px; text-align:center;
+ font-family:'Fraunces',serif; font-weight:600; font-size:21px; color:var(--cream);
+ }
+ .hint .sub{ display:block; margin-top:4px; font-family:'Manrope',sans-serif; font-weight:600; font-size:14px; color:rgba(246,239,228,.68); }
+
+ .dialdock{ position:absolute; left:0; right:0; bottom:0; padding:0 0 30px; display:flex; flex-direction:column; align-items:center; gap:16px; }
+ .modestrip{
+ display:flex; gap:10px; overflow-x:auto; scroll-snap-type:x proximity; padding:4px 20px;
+ max-width:390px; -webkit-overflow-scrolling:touch;
+ }
+ .modestrip::-webkit-scrollbar{ display:none; }
+ .modepill{
+ scroll-snap-align:center; flex:0 0 auto; min-height:56px; padding:0 18px; border-radius:18px;
+ background:rgba(255,255,255,.10); border:2px solid rgba(255,255,255,.16); color:rgba(246,239,228,.62);
+ font-weight:800; font-size:13px; letter-spacing:.10em; display:flex; align-items:center; gap:8px;
+ }
+ .modepill .ic{ font-size:18px; }
+ .modepill.active{ background:rgba(255,255,255,.16); color:var(--cream); }
+ .modepill.active[data-m="new"]{ border-color:var(--copper); box-shadow:0 0 0 1px var(--copper) inset; }
+ .modepill.active[data-m="update"]{ border-color:var(--teal); box-shadow:0 0 0 1px var(--teal) inset; }
+ .modepill.active[data-m="find"]{ border-color:var(--plum); box-shadow:0 0 0 1px var(--plum) inset; }
+ .modepill.active[data-m="sample"]{ border-color:var(--mustard); box-shadow:0 0 0 1px var(--mustard) inset; }
+ .modepill:focus-visible{ outline:3px solid #fff; outline-offset:2px; }
+
+ .shutterwrap{ display:flex; flex-direction:column; align-items:center; gap:10px; }
+ .shutterlabel{ font-weight:800; font-size:15px; color:var(--cream); }
+ .shutter{
+ width:96px; height:96px; border-radius:50%; border:6px solid rgba(255,255,255,.9);
+ display:flex; align-items:center; justify-content:center; font-size:36px; color:#fff;
+ transition:transform .08s ease;
+ }
+ .shutter:active{ transform:scale(.94); }
+ .shutter:focus-visible{ outline:3px solid #fff; outline-offset:4px; }
+ .flash{ position:absolute; inset:0; background:#fff; opacity:0; pointer-events:none; }
+ .flash.go{ animation:flashfx .5s ease; }
+ @keyframes flashfx{ 0%{opacity:0;} 12%{opacity:.9;} 100%{opacity:0;} }
+
+ /* ---------- generic light screens ---------- */
+ .pad{ padding:60px 26px 30px; flex:1; display:flex; flex-direction:column; }
+ .backbar{
+ position:absolute; top:14px; left:14px; z-index:5; min-width:56px; min-height:56px;
+ border-radius:14px; background:#fff; border:2px solid #EBE3D6; color:var(--ink);
+ font-size:14px; font-weight:800; display:flex; align-items:center; justify-content:center; gap:4px;
+ padding:0 14px;
+ }
+ .backbar:focus-visible{ outline:3px solid #2B6CB0; outline-offset:2px; }
+ .question{ font-family:'Fraunces',serif; font-weight:700; font-size:28px; line-height:1.2; margin:6px 0 22px; }
+ .answers{ display:flex; flex-direction:column; gap:14px; margin-top:auto; }
+ .bigbtn{
+ width:100%; min-height:60px; border-radius:18px; font-size:18px; font-weight:800;
+ background:#fff; border:2px solid #EBE3D6; color:var(--ink); padding:12px 18px;
+ }
+ .bigbtn-secondary{ background:#fff; color:var(--muted); border:2px solid #E3DACB; }
+ .bigbtn:focus-visible{ outline:3px solid #2B6CB0; outline-offset:2px; }
+ .linktext{ background:none; border:none; color:var(--muted); font-size:16px; text-decoration:underline; min-height:56px; }
+
+ .brandlist{ display:flex; flex-direction:column; gap:12px; overflow-y:auto; }
+
+ .card{ display:flex; flex-direction:column; align-items:center; text-align:center; gap:6px; margin:8px 0 24px; }
+ .swatch{ width:100%; max-width:260px; height:170px; border-radius:18px; }
+ .sw1{background:repeating-linear-gradient(45deg,#C9A24B 0 16px,#b98a2e 16px 32px);}
+ .sw2{background:repeating-linear-gradient(-45deg,#7C93B0 0 16px,#5f7a99 16px 32px);}
+ .sw3{background:repeating-linear-gradient(90deg,#8FAE8B 0 14px,#729070 14px 28px);}
+ .sw4{background:repeating-linear-gradient(135deg,#C0785F 0 15px,#a85f47 15px 30px);}
+ .itemname{ font-size:19px; font-weight:800; margin:2px 0 0; }
+ .itemmeta{ font-size:15px; color:var(--muted); margin:2px 0 0; }
+
+ .center{ display:flex; flex-direction:column; align-items:center; justify-content:center; flex:1; gap:8px; text-align:center; }
+</style>
+</head>
+<body>
+<div class="phone">
+ <div class="screen" id="screen" role="main"></div>
+</div>
+
+<script>
+const MODES = {
+ new: { label:'NEW', icon:'🆕', color:'var(--copper)', ink:'var(--copper-ink)', shutterIcon:'📸', hint:'Point at anything new.', sub:'Tap to add a new item' },
+ update: { label:'UPDATE', icon:'🔄', color:'var(--teal)', ink:'var(--teal-ink)', shutterIcon:'📷', hint:'Scan the label or pattern.', sub:'Tap to update an item' },
+ find: { label:'FIND', icon:'🔍', color:'var(--plum)', ink:'var(--plum-ink)', shutterIcon:'🔎', hint:'Scan to find a match.', sub:'Tap to find an item' },
+ sample: { label:'SAMPLE', icon:'📦', color:'var(--mustard)', ink:'var(--mustard-ink)', shutterIcon:'📦', hint:'Scan the box label.', sub:'Tap to log a sample' },
+};
+const MODE_ORDER = ['new','update','find','sample'];
+
+const MOCK_ITEMS = [
+ { name:'Harbor Stripe', color:'Indigo', code:'item 4471', sw:'sw2' },
+ { name:'Fern & Fable', color:'Moss', code:'item 2093', sw:'sw3' },
+ { name:'Ember Weave', color:'Rust', code:'item 6650', sw:'sw4' },
+];
+const BRANDS = ['York Wallcoverings','Brewster Home','Kravet','Cole & Son','Thibaut','Ronald Redding'];
+
+const state = { screen:'home', mode:'new', job:null, item:null, brand:null, resultIndex:0, shooting:false };
+
+function go(screen){ state.screen = screen; render(); }
+function setMode(m){ state.mode = m; render(); }
+
+function accent(job){
+ return (MODES[job] || MODES.new).color;
+}
+function accentInk(job){
+ return (MODES[job] || MODES.new).ink;
+}
+
+function shootFromHome(){
+ if(state.shooting) return;
+ state.shooting = true;
+ state.job = state.mode;
+ render();
+ setTimeout(()=>{
+ state.shooting = false;
+ state.item = randomItem();
+ if(state.mode==='new') go('new-brand');
+ else if(state.mode==='update') go('update-match');
+ else if(state.mode==='find'){ state.resultIndex = 0; go('find-result'); }
+ else if(state.mode==='sample') go('sample-match');
+ }, 650);
+}
+function shootPlain(cb){
+ const st = document.getElementById('camStatus2');
+ if(st) st.textContent = 'Snapping…';
+ setTimeout(cb, 650);
+}
+function randomItem(){ return MOCK_ITEMS[Math.floor(Math.random()*MOCK_ITEMS.length)]; }
+
+function backBtn(target){
+ return `<button class="backbar" onclick="go('${target}')">‹ Camera</button>`;
+}
+
+function plainCamera(hint, onShoot, backTarget){
+ return `<div class="cam" style="min-height:auto; flex:1;">
+ ${backBtn(backTarget)}
+ <div class="hint" style="top:70px;">${hint}</div>
+ <div class="dialdock" style="position:static; margin-top:auto; padding-bottom:40px;">
+ <div class="shutterwrap">
+ <span class="shutterlabel" id="camStatus2">Tap to shoot</span>
+ <button class="shutter" style="background:${accent(state.job)};" aria-label="Take photo" onclick="${onShoot}">📸</button>
+ </div>
+ </div>
+ </div>`;
+}
+
+function render(){
+ const s = document.getElementById('screen');
+ let html = '';
+
+ if(state.screen==='home'){
+ const m = MODES[state.mode];
+ let pills = '';
+ MODE_ORDER.forEach(key=>{
+ const mm = MODES[key];
+ pills += `<button class="modepill ${key===state.mode?'active':''}" data-m="${key}" onclick="setMode('${key}')" aria-pressed="${key===state.mode}">
+ <span class="ic">${mm.icon}</span>${mm.label}</button>`;
+ });
+ html = `<div class="cam">
+ <div class="flash" id="flash"></div>
+ <div class="dwmark">DW · PHOTO</div>
+ <div class="reticle" aria-hidden="true"></div>
+ <div class="hint">${m.hint}<span class="sub">${m.sub}</span></div>
+ <div class="dialdock">
+ <div class="modestrip" role="tablist" aria-label="Choose what you're doing">${pills}</div>
+ <div class="shutterwrap">
+ <span class="shutterlabel">${state.shooting ? 'Snapping…' : m.label + ' MODE'}</span>
+ <button class="shutter" style="background:${m.color};" aria-label="Shoot for ${m.label}" onclick="shootFromHome()" ${state.shooting?'disabled':''}>${m.shutterIcon}</button>
+ </div>
+ </div>
+ </div>`;
+ s.innerHTML = html;
+ if(state.shooting){
+ const f = document.getElementById('flash');
+ if(f){ f.classList.remove('go'); void f.offsetWidth; f.classList.add('go'); }
+ }
+ return;
+ }
+
+ // ---- NEW ----
+ if(state.screen==='new-brand'){
+ html = `<div class="pad">${backBtn('home')}
+ <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></div>`;
+ }
+ else if(state.screen==='new-save'){
+ html = `<div class="pad">${backBtn('new-brand')}
+ <h1 class="question">Save it?</h1>
+ <div class="card">
+ <div class="swatch ${state.item.sw}"></div>
+ <p class="itemmeta">${state.brand} · ${state.item.code}</p>
+ </div>
+ <div class="answers">
+ <button class="bigbtn" style="background:${accent('new')}; border-color:${accent('new')}; color:#fff;" onclick="go('new-done')">Save as Draft</button>
+ <button class="linktext" onclick="go('home')">Retake Photo</button>
+ </div></div>`;
+ }
+ else if(state.screen==='new-done'){
+ html = `<div class="pad"><div class="center">
+ <h1 class="question">Saved! 🎉</h1>
+ </div>
+ <div class="answers">
+ <button class="bigbtn" style="background:${accent('new')}; border-color:${accent('new')}; color:#fff;" onclick="setMode('new'); go('home')">Add Another</button>
+ <button class="linktext" onclick="go('home')">Home</button>
+ </div></div>`;
+ }
+
+ // ---- UPDATE ----
+ else if(state.screen==='update-match'){
+ html = `<div class="pad">${backBtn('home')}
+ <h1 class="question">Is this it?</h1>
+ <div class="card">
+ <div class="swatch ${state.item.sw}"></div>
+ <p class="itemname">${state.item.name} — ${state.item.color}</p>
+ <p class="itemmeta">${state.item.code}</p>
+ </div>
+ <div class="answers">
+ <button class="bigbtn" style="background:${accent('update')}; border-color:${accent('update')}; color:#fff;" onclick="go('update-cam2')">Yes, That's It</button>
+ <button class="bigbtn bigbtn-secondary" onclick="go('home')">No, Let Me Search</button>
+ </div></div>`;
+ }
+ else if(state.screen==='update-cam2'){
+ html = plainCamera('Take the new photo.', "shootPlain(()=>go('update-done'))", 'update-match');
+ }
+ else if(state.screen==='update-done'){
+ html = `<div class="pad"><div class="center">
+ <h1 class="question">Updated! ✅</h1>
+ </div>
+ <div class="answers">
+ <button class="bigbtn" style="background:${accent('update')}; border-color:${accent('update')}; color:#fff;" onclick="go('home')">Home</button>
+ </div></div>`;
+ }
+
+ // ---- FIND ----
+ else if(state.screen==='find-result'){
+ const it = MOCK_ITEMS[state.resultIndex];
+ html = `<div class="pad">${backBtn('home')}
+ <h1 class="question">Is this the one?</h1>
+ <div class="card">
+ <div class="swatch ${it.sw}"></div>
+ <p class="itemname">${it.name} — ${it.color}</p>
+ <p class="itemmeta">${it.code}</p>
+ </div>
+ <div class="answers">
+ <button class="bigbtn" style="background:${accent('find')}; border-color:${accent('find')}; color:#fff;" onclick="pickResult()">Yes, This One</button>
+ <button class="bigbtn bigbtn-secondary" onclick="nextResult()">Next Result</button>
+ </div></div>`;
+ }
+ else if(state.screen==='find-exhausted'){
+ html = `<div class="pad">${backBtn('home')}
+ <h1 class="question">That's all we found.</h1>
+ <div class="answers">
+ <button class="bigbtn" style="background:${accent('find')}; border-color:${accent('find')}; color:#fff;" onclick="state.resultIndex=0; go('find-result')">Try Again</button>
+ <button class="linktext" onclick="go('home')">Home</button>
+ </div></div>`;
+ }
+ else if(state.screen==='find-detail'){
+ html = `<div class="pad"><div class="center">
+ <h1 class="question" style="font-size:24px;">${state.item.name}</h1>
+ <div class="swatch ${state.item.sw}" style="max-width:220px;"></div>
+ <p class="itemmeta">${state.item.color} · ${state.item.code}</p>
+ </div>
+ <div class="answers">
+ <button class="bigbtn" style="background:${accent('update')}; border-color:${accent('update')}; color:#fff;" onclick="setMode('update'); go('update-cam2')">Update This</button>
+ <button class="bigbtn bigbtn-secondary" onclick="setMode('find'); go('home')">Search Again</button>
+ </div></div>`;
+ }
+
+ // ---- SAMPLE ----
+ else if(state.screen==='sample-match'){
+ html = `<div class="pad">${backBtn('home')}
+ <h1 class="question">Mark it received?</h1>
+ <div class="card">
+ <div class="swatch ${state.item.sw}"></div>
+ <p class="itemname">${state.item.name} — ${state.item.color}</p>
+ <p class="itemmeta">${state.item.code}</p>
+ </div>
+ <div class="answers">
+ <button class="bigbtn" style="background:${accent('sample')}; border-color:${accent('sample')}; color:#fff;" onclick="go('sample-printing')">Yes, Mark Received</button>
+ </div></div>`;
+ }
+ else if(state.screen==='sample-printing'){
+ html = `<div class="pad"><div class="center">
+ <h1 class="question">Printing your sticker…</h1>
+ <p class="itemmeta">🖨️ One sec…</p>
+ </div></div>`;
+ setTimeout(()=>{ if(state.screen==='sample-printing') go('sample-done'); }, 1000);
+ }
+ else if(state.screen==='sample-done'){
+ html = `<div class="pad"><div class="center">
+ <h1 class="question">Done! ✅</h1>
+ </div>
+ <div class="answers">
+ <button class="bigbtn" style="background:${accent('sample')}; border-color:${accent('sample')}; color:#fff;" onclick="go('home')">Home</button>
+ </div></div>`;
+ }
+
+ 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/kid-simple/v9-story-mode.html b/public/kid-simple/v9-story-mode.html
new file mode 100644
index 0000000..4f13389
--- /dev/null
+++ b/public/kid-simple/v9-story-mode.html
@@ -0,0 +1,386 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+<title>v9 — Story Mode</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=Playfair+Display:wght@600;700;800&family=Manrope:wght@500;600;700;800&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --parchment:#F6F1EA; --ink:#2A2320;
+ --canvas:#201A1D; --cream:#FBF4EC; --muted:rgba(251,244,236,.58);
+ --accent:#E0764A; --accent-ink:#3A1F12;
+ --success:#6FA98A; --track:rgba(255,255,255,.22);
+ --ring-new:#E0764A; --ring-update:#3E7C7C; --ring-find:#C97A96; --ring-sample:#8A9B5E;
+ }
+ *{box-sizing:border-box; -webkit-tap-highlight-color:transparent;}
+ html,body{margin:0;padding:0;background:#ddd;font-family:'Manrope',sans-serif;color:var(--ink);}
+ body{display:flex; align-items:flex-start; justify-content:center; min-height:100vh; padding:20px 0;}
+ .phone{
+ width:390px; height:780px; background:var(--parchment); border-radius:36px; overflow:hidden;
+ box-shadow:0 24px 60px rgba(0,0,0,.28); position:relative; display:flex; flex-direction:column;
+ }
+ .screen{ flex:1; position:relative; overflow:hidden; }
+ h1,h2{ font-family:'Playfair Display',serif; margin:0; }
+ button{ font-family:'Manrope',sans-serif; cursor:pointer; }
+
+ /* ---------- HOME (light chrome, story selector) ---------- */
+ .home{ height:100%; display:flex; flex-direction:column; padding:34px 26px 30px; }
+ .home-mark{ font-family:'Playfair Display',serif; font-weight:800; font-size:15px; letter-spacing:.14em; color:#8A7F6E; }
+ .home-sub{ font-size:12px; letter-spacing:.16em; text-transform:uppercase; color:#B5A896; margin-top:2px; }
+ .home-headline{ font-size:30px; font-weight:700; line-height:1.2; margin:26px 0 8px; color:var(--ink); }
+ .home-hint{ font-size:15px; color:#8A7F6E; margin:0 0 26px; }
+ .storygrid{ display:grid; grid-template-columns:1fr 1fr; gap:20px; flex:1; }
+ .storytile{
+ background:none; border:none; padding:0; display:flex; flex-direction:column; align-items:center;
+ justify-content:flex-start; gap:12px; min-height:130px;
+ }
+ .ring{
+ width:92px; height:92px; border-radius:50%; padding:4px; display:flex; align-items:center; justify-content:center;
+ }
+ .ring-inner{
+ width:100%; height:100%; border-radius:50%; background:var(--canvas); display:flex; align-items:center;
+ justify-content:center; font-size:34px;
+ }
+ .r-new{ background:conic-gradient(from -40deg, var(--ring-new), #F4C79A, var(--ring-new)); }
+ .r-update{ background:conic-gradient(from -40deg, var(--ring-update), #A9D3CF, var(--ring-update)); }
+ .r-find{ background:conic-gradient(from -40deg, var(--ring-find), #EFC7D4, var(--ring-find)); }
+ .r-sample{ background:conic-gradient(from -40deg, var(--ring-sample), #D9E2B8, var(--ring-sample)); }
+ .storytile .label{ font-size:14.5px; font-weight:700; color:var(--ink); text-align:center; line-height:1.25; }
+ .storytile:focus-visible .ring-inner{ outline:3px solid #2B6CB0; outline-offset:2px; }
+
+ /* ---------- STORY (dark canvas) ---------- */
+ .story{ position:absolute; inset:0; background:var(--canvas); display:flex; flex-direction:column; color:var(--cream); }
+ .story.cam{ background:radial-gradient(120% 90% at 50% 0%, #2c2428 0%, #171214 75%); }
+ .progressrow{ display:flex; gap:6px; padding:16px 16px 0; z-index:30; }
+ .seg{ flex:1; height:4px; border-radius:3px; background:var(--track); overflow:hidden; }
+ .seg i{ display:block; height:100%; width:0%; background:var(--accent); border-radius:3px; }
+ .seg.done i{ width:100%; }
+ .seg.now i{ width:100%; }
+ .storytop{ display:flex; align-items:center; justify-content:space-between; padding:10px 10px 0 16px; z-index:30; }
+ .storytop .who{ font-size:12.5px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; color:var(--muted); }
+ .exitbtn{
+ width:56px; height:56px; border-radius:50%; background:rgba(0,0,0,.28); border:none; color:var(--cream);
+ font-size:20px; display:flex; align-items:center; justify-content:center;
+ }
+ .exitbtn:focus-visible{ outline:3px solid #7EB6E8; outline-offset:2px; }
+
+ .tapzone{ position:absolute; top:64px; bottom:150px; width:17%; background:none; border:none; z-index:5; padding:0; }
+ .tapzone.left{ left:0; }
+ .tapzone.right{ right:0; }
+ .tapzone .chev{ position:absolute; top:50%; transform:translateY(-50%); font-size:22px; color:rgba(255,255,255,.30); }
+ .tapzone.left .chev{ left:8px; }
+ .tapzone.right .chev{ right:8px; }
+
+ .framebody{ flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; padding:10px 30px; text-align:center; position:relative; z-index:8; gap:10px; }
+ .framebody .cue{ font-size:12.5px; font-weight:700; letter-spacing:.14em; text-transform:uppercase; color:var(--accent); margin-bottom:2px; }
+ .framebody h2{ font-size:27px; font-weight:700; line-height:1.24; color:var(--cream); text-shadow:0 2px 14px rgba(0,0,0,.35); }
+ .framebody .sub{ font-size:15px; color:var(--muted); margin-top:2px; }
+
+ .frameactions{ position:relative; z-index:10; padding:14px 22px 26px; display:flex; flex-direction:column; gap:12px; }
+ .bigbtn{
+ width:100%; min-height:58px; border-radius:18px; font-size:17.5px; font-weight:700;
+ border:2px solid rgba(255,255,255,.18); background:rgba(255,255,255,.08); color:var(--cream); padding:10px 16px;
+ }
+ .bigbtn-primary{ background:var(--accent); border-color:var(--accent); color:var(--accent-ink); }
+ .bigbtn-ghost{ background:none; border:2px solid rgba(255,255,255,.3); color:var(--muted); }
+ .bigbtn:focus-visible{ outline:3px solid #7EB6E8; outline-offset:2px; }
+
+ .shutterwrap{ position:relative; z-index:10; display:flex; justify-content:center; padding-bottom:30px; }
+ .shutter{
+ width:88px; height:88px; border-radius:50%; background:var(--accent); border:6px solid rgba(255,255,255,.85);
+ font-size:32px; display:flex; align-items:center; justify-content:center; color:var(--accent-ink);
+ }
+ .shutter:disabled{ opacity:.55; }
+ .shutter:focus-visible{ outline:3px solid #7EB6E8; outline-offset:4px; }
+
+ .brandlist{ width:100%; display:flex; flex-direction:column; gap:12px; overflow-y:auto; max-height:390px; padding:2px 2px 4px; }
+ .brandbtn{
+ width:100%; min-height:58px; border-radius:16px; background:rgba(255,255,255,.07); border:2px solid rgba(255,255,255,.16);
+ color:var(--cream); font-size:16.5px; font-weight:700; padding:10px 18px; text-align:left;
+ }
+ .brandbtn:focus-visible{ outline:3px solid #7EB6E8; outline-offset:2px; }
+
+ .swatch{ width:180px; height:130px; border-radius:16px; margin:4px auto 2px; box-shadow:0 10px 26px rgba(0,0,0,.35); }
+ .sw1{background:repeating-linear-gradient(45deg,#E0764A 0 16px,#c25f37 16px 32px);}
+ .sw2{background:repeating-linear-gradient(-45deg,#3E7C7C 0 14px,#2f6161 14px 28px);}
+ .sw3{background:repeating-linear-gradient(90deg,#C97A96 0 14px,#b05f7c 14px 28px);}
+ .itemname{ font-size:19px; font-weight:800; margin:0; color:var(--cream); }
+ .itemmeta{ font-size:14.5px; color:var(--muted); margin:2px 0 0; }
+
+ .doneflash{ font-size:52px; }
+</style>
+</head>
+<body>
+<div class="phone">
+ <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', item:null, brand:null, resultIndex:0 };
+
+function randomItem(){ return MOCK_ITEMS[Math.floor(Math.random()*MOCK_ITEMS.length)]; }
+function go(screen){ state.screen = screen; render(); }
+function startJob(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]);
+}
+
+/* progress-bar position tables: [index(1-based), total] per screen key */
+const POS = {
+ 'new-cam':[1,3], 'new-brand':[2,3], 'new-review':[3,3],
+ 'update-cam':[1,3], 'update-match':[2,3], 'update-cam2':[3,3],
+ 'find-method':[1,3], 'find-result':[2,3], 'find-detail':[3,3],
+ 'sample-cam':[1,3], 'sample-match':[2,3], 'sample-printing':[3,3],
+};
+
+function progressRow(){
+ const p = POS[state.screen];
+ if(!p) return '';
+ const [idx,total] = p;
+ let segs = '';
+ for(let i=1;i<=total;i++){
+ segs += `<div class="seg ${i<idx?'done':(i===idx?'now':'')}"><i></i></div>`;
+ }
+ return `<div class="progressrow">${segs}</div>`;
+}
+
+function tapZones(leftFn, rightFn){
+ const l = leftFn ? `<button class="tapzone left" aria-label="Back" onclick="${leftFn}"><span class="chev">‹</span></button>` : '';
+ const r = rightFn ? `<button class="tapzone right" aria-label="Continue" onclick="${rightFn}"><span class="chev">›</span></button>` : '';
+ return l+r;
+}
+
+function storyShell(canvasClass, innerHtml, opts){
+ opts = opts || {};
+ const exit = `<button class="exitbtn" aria-label="Exit story" onclick="go('home')">✕</button>`;
+ return `<div class="story ${canvasClass||''}">
+ ${progressRow()}
+ <div class="storytop"><span class="who">${opts.who||''}</span>${exit}</div>
+ ${tapZones(opts.tapLeft, opts.tapRight)}
+ ${innerHtml}
+ </div>`;
+}
+
+function shoot(cb){
+ const st = document.getElementById('camStatus');
+ const btn = document.querySelector('.shutter');
+ if(btn) btn.disabled = true;
+ if(st) st.textContent = 'Snapping…';
+ setTimeout(()=>{ state.item = randomItem(); cb(); }, 650);
+}
+
+function cameraFrame(who, cue, question, hint, onShoot, tapLeft){
+ return storyShell('cam', `
+ <div class="framebody">
+ <div class="cue">${cue}</div>
+ <h2>${question}</h2>
+ <p class="sub" id="camStatus">${hint}</p>
+ </div>
+ <div class="shutterwrap"><button class="shutter" aria-label="Take photo" onclick="${onShoot}">📸</button></div>
+ `, {who, tapLeft, tapRight:onShoot});
+}
+
+function render(){
+ const s = document.getElementById('screen');
+ let html = '';
+
+ if(state.screen==='home'){
+ html = `<div class="home">
+ <div class="home-mark">DW</div>
+ <div class="home-sub">Story Mode</div>
+ <h1 class="home-headline">Pick your story</h1>
+ <p class="home-hint">Tap one to start — swipe through, tap ‹ › to move around.</p>
+ <div class="storygrid">
+ <button class="storytile" onclick="startJob('new')">
+ <div class="ring r-new"><div class="ring-inner">🆕</div></div>
+ <div class="label">Add New Item</div>
+ </button>
+ <button class="storytile" onclick="startJob('update')">
+ <div class="ring r-update"><div class="ring-inner">🔄</div></div>
+ <div class="label">Update an Item</div>
+ </button>
+ <button class="storytile" onclick="startJob('find')">
+ <div class="ring r-find"><div class="ring-inner">🔍</div></div>
+ <div class="label">Find an Item</div>
+ </button>
+ <button class="storytile" onclick="startJob('sample')">
+ <div class="ring r-sample"><div class="ring-inner">📦</div></div>
+ <div class="label">A Sample Came In</div>
+ </button>
+ </div>
+ </div>`;
+ }
+
+ /* ---------------- ADD NEW ITEM ---------------- */
+ else if(state.screen==='new-cam'){
+ html = cameraFrame('Add New Item','Frame 1','Take a photo of it','Tap the shutter (or tap the right edge)', "shoot(()=>go('new-brand'))");
+ }
+ else if(state.screen==='new-brand'){
+ let list = '';
+ BRANDS.forEach(b=>{ list += `<button class="brandbtn" onclick="state.brand='${b}'; go('new-review')">${b}</button>`; });
+ html = storyShell('', `
+ <div class="framebody" style="justify-content:flex-start; padding-top:20px;">
+ <div class="cue">Frame 2</div>
+ <h2>Which brand is it?</h2>
+ <div class="brandlist">${list}</div>
+ </div>
+ `, {who:'Add New Item', tapLeft:"go('new-cam')"});
+ }
+ else if(state.screen==='new-review'){
+ html = storyShell('', `
+ <div class="framebody">
+ <div class="cue">Frame 3</div>
+ <h2>Save it?</h2>
+ <div class="swatch ${state.item.sw}"></div>
+ <p class="itemmeta">${state.brand} · code ${state.item.code}</p>
+ </div>
+ <div class="frameactions">
+ <button class="bigbtn bigbtn-primary" onclick="go('new-done')">Save as Draft</button>
+ <button class="bigbtn bigbtn-ghost" onclick="go('new-cam')">Retake Photo</button>
+ </div>
+ `, {who:'Add New Item', tapLeft:"go('new-brand')", tapRight:"go('new-done')"});
+ }
+ else if(state.screen==='new-done'){
+ html = doneFrame('Add New Item', '🎉', 'Saved!', 'Add Another', "startJob('new')");
+ }
+
+ /* ---------------- UPDATE AN ITEM ---------------- */
+ else if(state.screen==='update-cam'){
+ html = cameraFrame('Update an Item','Frame 1','Scan the label or pattern','Tap the shutter (or tap the right edge)', "shoot(()=>go('update-match'))");
+ }
+ else if(state.screen==='update-match'){
+ html = storyShell('', `
+ <div class="framebody">
+ <div class="cue">Frame 2</div>
+ <h2>Is this it?</h2>
+ <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>
+ <div class="frameactions">
+ <button class="bigbtn bigbtn-primary" onclick="go('update-cam2')">Yes</button>
+ <button class="bigbtn bigbtn-ghost" onclick="go('update-cam')">No, Let Me Search</button>
+ </div>
+ `, {who:'Update an Item', tapLeft:"go('update-cam')", tapRight:"go('update-cam2')"});
+ }
+ else if(state.screen==='update-cam2'){
+ html = cameraFrame('Update an Item','Frame 3','Take the new photo','Tap the shutter (or tap the right edge)', "shoot(()=>go('update-done'))", "go('update-match')");
+ }
+ else if(state.screen==='update-done'){
+ html = doneFrame('Update an Item', '✅', 'Updated!', 'Back Home', "go('home')");
+ }
+
+ /* ---------------- FIND AN ITEM ---------------- */
+ else if(state.screen==='find-method'){
+ html = storyShell('', `
+ <div class="framebody">
+ <div class="cue">Frame 1</div>
+ <h2>How do you want to find it?</h2>
+ </div>
+ <div class="frameactions">
+ <button class="bigbtn" onclick="go('find-result')">📷 Scan the Label</button>
+ <button class="bigbtn" onclick="go('find-result')">🎤 Say the Name</button>
+ <button class="bigbtn" onclick="go('find-result')">⌨️ Type It</button>
+ </div>
+ `, {who:'Find an Item', tapLeft:"go('home')"});
+ }
+ else if(state.screen==='find-result'){
+ const it = MOCK_ITEMS[state.resultIndex];
+ html = storyShell('', `
+ <div class="framebody">
+ <div class="cue">Frame 2</div>
+ <h2>Is this the one?</h2>
+ <div class="swatch ${it.sw}"></div>
+ <p class="itemname">${it.name} — ${it.color}</p>
+ <p class="itemmeta">code ${it.code}</p>
+ </div>
+ <div class="frameactions">
+ <button class="bigbtn bigbtn-primary" onclick="pickResult()">Yes, This One</button>
+ <button class="bigbtn bigbtn-ghost" onclick="nextResult()">Next Result</button>
+ </div>
+ `, {who:'Find an Item', tapLeft:"go('find-method')", tapRight:"nextResult()"});
+ }
+ else if(state.screen==='find-detail'){
+ html = storyShell('', `
+ <div class="framebody">
+ <div class="cue">Frame 3</div>
+ <h2>${state.item.name}</h2>
+ <div class="swatch ${state.item.sw}"></div>
+ <p class="itemmeta">${state.item.color} · code ${state.item.code}</p>
+ </div>
+ <div class="frameactions">
+ <button class="bigbtn bigbtn-primary" onclick="go('update-cam2')">Update This</button>
+ <button class="bigbtn bigbtn-ghost" onclick="go('find-method')">Search Again</button>
+ </div>
+ `, {who:'Find an Item', tapLeft:"go('find-result')"});
+ }
+
+ /* ---------------- A SAMPLE CAME IN ---------------- */
+ else if(state.screen==='sample-cam'){
+ html = cameraFrame('A Sample Came In','Frame 1','Scan the box label','Tap the shutter (or tap the right edge)', "shoot(()=>go('sample-match'))");
+ }
+ else if(state.screen==='sample-match'){
+ html = storyShell('', `
+ <div class="framebody">
+ <div class="cue">Frame 2</div>
+ <h2>Mark it received?</h2>
+ <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>
+ <div class="frameactions">
+ <button class="bigbtn bigbtn-primary" onclick="go('sample-printing')">Yes, Mark Received</button>
+ </div>
+ `, {who:'A Sample Came In', tapLeft:"go('sample-cam')", tapRight:"go('sample-printing')"});
+ }
+ else if(state.screen==='sample-printing'){
+ html = storyShell('', `
+ <div class="framebody">
+ <div class="cue">Frame 3</div>
+ <h2>Printing your sticker…</h2>
+ <p class="sub">🖨️ One sec…</p>
+ </div>
+ `, {who:'A Sample Came In'});
+ setTimeout(()=>{ if(state.screen==='sample-printing') go('sample-done'); }, 1000);
+ }
+ else if(state.screen==='sample-done'){
+ html = doneFrame('A Sample Came In', '✅', 'Done!', 'Back Home', "go('home')");
+ }
+
+ s.innerHTML = html;
+}
+
+function doneFrame(who, emoji, title, btnLabel, btnAction){
+ return storyShell('', `
+ <div class="framebody">
+ <div class="doneflash">${emoji}</div>
+ <h2>${title}</h2>
+ </div>
+ <div class="frameactions">
+ <button class="bigbtn bigbtn-primary" onclick="${btnAction}">${btnLabel}</button>
+ <button class="bigbtn bigbtn-ghost" onclick="go('home')">Home</button>
+ </div>
+ `, {who});
+}
+
+function pickResult(){ state.item = MOCK_ITEMS[state.resultIndex]; go('find-detail'); }
+function nextResult(){
+ state.resultIndex = (state.resultIndex + 1) % MOCK_ITEMS.length;
+ go('find-result');
+}
+
+render();
+</script>
+</body>
+</html>
diff --git a/server.js b/server.js
index 4abf2cb..eff859b 100644
--- a/server.js
+++ b/server.js
@@ -2431,6 +2431,24 @@ load();
return res.end(fs.readFileSync(fp));
}
+ // ── Kid-Simple redesign review page (TK-12344): static mockups v1–v16 + the Photoshop board.
+ // Same bounded posture as /js/: fixed dir, basename-only (+ one renders/ subdir), extension
+ // allowlist, path-traversal guard, behind the app basic-auth. Mockups only — no API calls.
+ if (_p === '/kid-simple') { res.writeHead(301, { Location: '/kid-simple/' }); return res.end(); }
+ if (_p.startsWith('/kid-simple/')) {
+ let name = _p.slice('/kid-simple/'.length) || 'index.html';
+ const m = /^(renders\/)?([A-Za-z0-9._-]+\.(html|png|jpg))$/.exec(name);
+ if (!m || name.includes('..')) return send(res, 404, { err: 'not found' });
+ const KDIR = path.join(ROOT, 'public/kid-simple');
+ const fp = path.join(KDIR, name);
+ if (!fp.startsWith(KDIR + path.sep) || !fs.existsSync(fp) || !fs.statSync(fp).isFile()) {
+ return send(res, 404, { err: 'not found' });
+ }
+ const type = { html: 'text/html; charset=utf-8', png: 'image/png', jpg: 'image/jpeg' }[m[3]];
+ res.writeHead(200, { 'Content-Type': type, 'Cache-Control': m[3] === 'html' ? 'no-store' : 'max-age=3600' });
+ return res.end(fs.readFileSync(fp));
+ }
+
// App icons + PWA manifest — make "Add to Home Screen" a real app (clean DW icon, fullscreen).
if (u.pathname === '/icon-180.png' || u.pathname === '/icon-192.png' || u.pathname === '/icon-512.png' || u.pathname === '/apple-touch-icon.png') {
const f = path.join(ROOT, 'public', u.pathname === '/apple-touch-icon.png' ? 'icon-180.png' : path.basename(u.pathname));
← ac13489 Untrack runtime build counter data/build.json and gitignore
·
back to Dw Photo Capture
·
Onload chooser: Adobe Spectrum themes (Light/Dark/Darkest/Au 6e2c3f2 →