← back to Dw Photo Capture

public/kid-simple/v5-swipe-deck.html

491 lines

<!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>