← back to Dw Photo Capture

public/index.html

663 lines

<!doctype html>
<html lang="en">
<head>
<script>
// Credential-safe fetch guard (fleet drop-in) — if this page is opened with
// credentials in the URL (a saved bookmark / Chrome-remembered basic-auth:
// https://user:pass@host/…), the browser poisons document.baseURI, so any
// relative or root-relative fetch('/api/…') throws "Request cannot be constructed
// from a URL that includes credentials" and callers silently fall to an empty
// state. Resolve every non-absolute request URL against the credential-free
// location instead of baseURI. Placed first so it wraps window.fetch before any
// app script runs. Ref: creds-in-url-fetch-guard-fleet-pattern.
(function () {
  var _fetch = window.fetch.bind(window);
  var cleanBase = function () { return location.origin + location.pathname; };
  window.fetch = function (input, init) {
    try {
      if (typeof input === 'string' && !/^[a-z]+:\/\//i.test(input) && input.indexOf('//') !== 0) {
        input = new URL(input, cleanBase()).href;
      } else if (input instanceof Request && !/^[a-z]+:\/\//i.test(input.url)) {
        input = new Request(new URL(input.url, cleanBase()).href, input);
      }
    } catch (_) { /* fall through to native */ }
    return _fetch(input, init);
  };
})();
</script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, maximum-scale=1, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="DW Photo">
<meta name="mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#0f0e0c">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="192x192" href="/icon-192.png">
<link rel="icon" type="image/png" sizes="512x512" href="/icon-512.png">
<link rel="manifest" href="/manifest.webmanifest">
<title>Mobile DW SKU Photos</title>
<style>
  :root{ --ui-scale:1.35; /* "make all larger" — single dial for global UI size (1 = original). */
         --cols: 1; --bg:#0f0e0c; --card:#1b1916; --ink:#f3efe7; --muted:#9a9184; --line:#2e2a25;
         --gold:#c8a24a; --green:#3fa06a; --red:#c0563f; }
  /* Uniform magnification of the whole editor (fonts, controls, thumbs) — scrolls, so no clipping. */
  html{ zoom: var(--ui-scale); }
  *{box-sizing:border-box}
  body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.4 -apple-system,BlinkMacSystemFont,"SF Pro Text",Segoe UI,Roboto,sans-serif;-webkit-text-size-adjust:100%}
  header{position:sticky;top:0;z-index:10;background:rgba(15,14,12,.96);backdrop-filter:blur(8px);border-bottom:1px solid var(--line);padding:max(10px,env(safe-area-inset-top)) 14px 10px}
  h1{font:600 17px/1.1 "SF Pro Display",sans-serif;letter-spacing:.04em;margin:0 0 8px;text-transform:uppercase}
  h1 span{color:var(--gold)}
  h1 .ver{font:600 10px/1 ui-monospace,Menlo,monospace;text-decoration:none;color:#1b1407;background:var(--green);border-radius:99px;padding:3px 7px;letter-spacing:.04em;vertical-align:middle;text-transform:none;font-style:normal;margin-left:6px}
  .bar{height:8px;border-radius:99px;background:#2a261f;overflow:hidden;margin:6px 0}
  .bar i{display:block;height:100%;background:linear-gradient(90deg,var(--gold),#e7c878);width:0;transition:width .3s}
  .meta{display:flex;justify-content:space-between;font-size:13px;color:var(--muted)}
  .controls{display:flex;gap:8px;align-items:center;margin-top:10px;flex-wrap:wrap}
  input[type=search],select{background:#16140f;border:1px solid var(--line);color:var(--ink);border-radius:10px;padding:9px 11px;font-size:15px}
  input[type=search]{flex:1;min-width:140px}
  .scanbtn{position:relative;overflow:hidden;background:#16140f;border:1px solid var(--gold);color:var(--gold);border-radius:12px;padding:14px 18px;font-size:16px;font-weight:700;min-height:54px;display:inline-flex;align-items:center;justify-content:center;gap:6px;white-space:nowrap;cursor:pointer}
  .scanbtn input{position:absolute;inset:0;opacity:0;font-size:120px;cursor:pointer}
  .chips{display:flex;flex-wrap:wrap;gap:6px}
  .chip{padding:7px 13px;border-radius:99px;border:1px solid var(--line);background:#16140f;color:var(--muted);font-size:13px;cursor:pointer}
  .chip.on{background:var(--gold);color:#1b1407;border-color:var(--gold);font-weight:600}
  .recog{grid-column:1/-1;background:var(--card);border:1px solid var(--gold);border-radius:14px;padding:12px 14px;margin-bottom:4px}
  .recog .rg-d{font-size:14px;color:var(--ink);margin-bottom:8px}
  .recog .rg-d b{color:var(--gold)}
  .recog .rg-tags{display:flex;flex-wrap:wrap;gap:6px}
  .recog .rg-tags span{font-size:12px;padding:4px 9px;border-radius:99px;background:#16140f;border:1px solid var(--line);color:var(--muted)}
  .recog .rg-tags span.sw{padding-left:7px;display:inline-flex;align-items:center;gap:5px}
  .recog .rg-tags i{width:11px;height:11px;border-radius:99px;display:inline-block;border:1px solid rgba(255,255,255,.25)}
  .recog .rg-code{color:var(--gold);font-weight:600;cursor:pointer}
  .dens{display:flex;align-items:center;gap:6px;color:var(--muted);font-size:12px}
  input[type=range]{accent-color:var(--gold)}
  main{padding:14px;display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:12px;max-width:1200px;margin:0 auto}
  .card{background:var(--card);border:1px solid var(--line);border-radius:16px;overflow:hidden;display:flex;flex-direction:column}
  .card.done{border-color:var(--green)}
  .thumb{aspect-ratio:4/3;background:#0c0b09 center/cover no-repeat;display:flex;align-items:center;justify-content:center;color:#4a443b;font-size:34px;position:relative}
  .thumb.tap{cursor:pointer} .thumb.tap:hover{filter:brightness(1.08)}
  .badge{position:absolute;top:8px;right:8px;font-size:11px;padding:3px 8px;border-radius:99px;font-weight:700;letter-spacing:.03em}
  .fav-btn{position:absolute;top:6px;left:6px;width:32px;height:32px;border:none;border-radius:50%;background:rgba(8,7,6,.55);color:#f3efe7;font-size:18px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(2px)}
  .fav-btn.on{color:var(--gold)}
  .fav-btn:active{transform:scale(.88)}
  .b-need{background:#3a2a14;color:#e7c878} .b-done{background:var(--green);color:#fff} .b-err{background:var(--red);color:#fff}
  /* Discontinued SKU → red card with live successor link */
  .disco-card{grid-column:1/-1;background:#2a1512;border:1px solid #c0392b;border-left:5px solid #c0392b;border-radius:16px;padding:16px 18px;display:flex;flex-direction:column;gap:10px}
  .disco-card .dhead{font:800 16px/1.3 "SF Pro Display",sans-serif;color:#e8776a;display:flex;align-items:center;gap:8px}
  .disco-card .dsku{font:600 12px/1 ui-monospace,Menlo,monospace;color:#e8a79e;letter-spacing:.03em}
  .disco-card .dstatus{font-size:10.5px;font-weight:700;letter-spacing:.05em;color:#fff;background:#c0392b;border-radius:99px;padding:2px 9px;text-transform:uppercase}
  .disco-succ{background:#16140f;border:1px solid #3a5a44;border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:6px}
  .disco-succ .slabel{font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:#7fbf9a;font-weight:700}
  .disco-succ .stitle{font:600 15px/1.3 "SF Pro Display",sans-serif;color:var(--ink)}
  .disco-succ .ssku{font:600 12px/1 ui-monospace,Menlo,monospace;color:var(--green)}
  .disco-succ a{color:var(--green);text-decoration:none;font-weight:700}
  .disco-succ .go{align-self:flex-start;margin-top:4px;background:var(--green);color:#fff;border:none;border-radius:10px;padding:8px 15px;font-size:13.5px;font-weight:700;cursor:pointer;text-decoration:none;display:inline-block}
  .disco-nosucc{font-size:13px;color:var(--muted)}
  .b-live{background:#1f7a48;color:#fff;box-shadow:0 0 0 2px rgba(63,160,106,.4)}
  .b-new{background:#2a2350;color:#cbb8ff;box-shadow:0 0 0 1px #5a4aa0}
  .body{padding:11px 12px 13px;display:flex;flex-direction:column;gap:6px;flex:1}
  .ttl{font:600 15px/1.25 "SF Pro Display",sans-serif}
  .model{display:inline-flex;align-items:center;gap:6px;background:#22201a;border:1px solid var(--line);border-radius:8px;padding:5px 9px;align-self:flex-start;cursor:pointer}
  .model small{font-size:9.5px;letter-spacing:.12em;color:var(--muted);text-transform:uppercase}
  .model b{font:800 16px/1 ui-monospace,Menlo,monospace;color:var(--ink);letter-spacing:.03em}
  .sku{font:600 12px/1 ui-monospace,Menlo,monospace;color:var(--gold);letter-spacing:.02em}
  .sub{font-size:11.5px;color:var(--muted);display:flex;gap:8px;flex-wrap:wrap;align-items:center}
  .cols{display:flex;gap:5px;flex-wrap:wrap}
  .colchip{font-size:10px;color:#bcd;background:#16201c;border:1px solid #2a3a33;border-radius:99px;padding:2px 7px;white-space:nowrap}
  .price{color:var(--green);font-weight:700}
  .price.no{color:var(--muted)}
  .when{font-size:11px;color:var(--muted)}
  /* "make this the only image" — destructive opt-in, only shown when an existing image would be wiped */
  .onlyimg{display:flex;align-items:center;gap:7px;font-size:11.5px;color:var(--muted);margin-top:2px;cursor:pointer;user-select:none}
  .onlyimg input{accent-color:var(--red);width:15px;height:15px;flex:0 0 auto}
  .onlyimg.armed{color:var(--red);font-weight:600}
  .actions{display:flex;gap:8px;margin-top:auto;padding-top:6px}
  .shoot{flex:1;position:relative;overflow:hidden;background:var(--gold);color:#1b1407;border:none;border-radius:12px;padding:13px;font-weight:700;font-size:15px;text-align:center;cursor:pointer}
  .shoot input{position:absolute;inset:0;opacity:0;font-size:200px;cursor:pointer}
  .skip{background:#16140f;border:1px solid var(--line);color:var(--muted);border-radius:12px;padding:13px 14px;font-size:14px;cursor:pointer}
  .enh{background:#16140f;border:1px solid var(--gold);color:var(--gold);border-radius:12px;padding:13px 14px;font-size:14px;font-weight:600;cursor:pointer;white-space:nowrap}
  .gal{background:#16140f;border:1px solid var(--line);color:var(--ink);border-radius:12px;padding:13px 12px;font-size:14px;font-weight:600;cursor:pointer;white-space:nowrap}
  .card.done .shoot{background:#16140f;color:var(--gold);border:1px solid var(--gold)}
  .empty{grid-column:1/-1;text-align:center;color:var(--muted);padding:40px 10px}

  /* ── onload chooser (TK-12346): Adobe Spectrum themes (Light · Dark · Darkest) + accent, full UX ──
     Tokens are scoped to #homeScreen so the rest of the app is untouched. Theme/accent persist in localStorage. */
  #homeScreen{--h-bg:#080808;--h-surface:#1a1a1a;--h-surface-2:#232323;--h-border:#2c2c2c;--h-border-hi:#4a4a4a;
    --h-ink:#e8e8e8;--h-muted:#9a9a9a;--h-accent:#2680eb;--h-accent-ink:#fff;--h-accent-soft:rgba(38,128,235,.16);--h-focus:#5aa9fa;
    position:fixed;inset:0;z-index:200;background:var(--h-bg);color:var(--h-ink);overflow-y:auto;-webkit-overflow-scrolling:touch;
    font-family:"Adobe Clean","Source Sans 3","SF Pro Text",-apple-system,system-ui,sans-serif;transition:background .2s,color .2s}
  #homeScreen[hidden]{display:none}
  #homeScreen[data-theme=dark]{--h-bg:#1d1d1d;--h-surface:#2c2c2c;--h-surface-2:#353535;--h-border:#3e3e3e;--h-border-hi:#5a5a5a;--h-ink:#ebebeb;--h-muted:#a8a8a8}
  #homeScreen[data-theme=light]{--h-bg:#f5f5f5;--h-surface:#ffffff;--h-surface-2:#f0f0f0;--h-border:#dadada;--h-border-hi:#b1b1b1;--h-ink:#1f1f1f;--h-muted:#6d6d6d;--h-focus:#1473e6}
  #homeScreen[data-accent=gold]{--h-accent:#c9a45c;--h-accent-ink:#1b1407;--h-accent-soft:rgba(201,164,92,.18)}
  #homeScreen[data-accent=green]{--h-accent:#12805c;--h-accent-ink:#fff;--h-accent-soft:rgba(18,128,92,.18)}
  #homeScreen[data-accent=purple]{--h-accent:#7e4bf3;--h-accent-ink:#fff;--h-accent-soft:rgba(126,75,243,.18)}
  #homeScreen[data-accent=red]{--h-accent:#d31510;--h-accent-ink:#fff;--h-accent-soft:rgba(211,21,16,.15)}
  .home-inner{display:flex;flex-direction:column;gap:12px;width:100%;max-width:440px;margin:0 auto;
    padding:max(18px,env(safe-area-inset-top)) 16px max(22px,env(safe-area-inset-bottom));min-height:100%;box-sizing:border-box;justify-content:center}
  .home-top{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;margin-bottom:4px}
  .home-title{font-size:12px;letter-spacing:.12em;text-transform:uppercase;color:var(--h-muted);font-weight:700}
  .home-greet{font-size:22px;font-weight:800;line-height:1.15;margin-top:4px;color:var(--h-ink)}
  .home-date{font-size:12.5px;color:var(--h-muted);margin-top:2px}
  .home-theme-btn{flex:0 0 auto;display:flex;align-items:center;gap:6px;background:var(--h-surface);color:var(--h-ink);border:1px solid var(--h-border);
    border-radius:999px;padding:7px 11px;font-weight:600;font-size:12.5px;line-height:1;font-family:inherit;cursor:pointer}
  .home-theme-btn .dot{width:12px;height:12px;border-radius:50%;background:var(--h-accent)}
  .home-prefs{background:var(--h-surface);border:1px solid var(--h-border);border-radius:14px;padding:12px;display:flex;flex-direction:column;gap:10px}
  .home-prefs[hidden]{display:none}
  .home-prefs-lbl{font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--h-muted);font-weight:700}
  .seg{display:flex;background:var(--h-surface-2);border:1px solid var(--h-border);border-radius:10px;padding:3px;gap:3px}
  .seg button{flex:1;background:none;border:none;border-radius:7px;padding:9px 4px;color:var(--h-muted);font-weight:600;font-size:13px;line-height:1;font-family:inherit;cursor:pointer}
  .seg button[aria-checked=true]{background:var(--h-surface);color:var(--h-ink);box-shadow:0 1px 2px rgba(0,0,0,.25),inset 0 0 0 1px var(--h-border-hi)}
  .swatches{display:flex;gap:10px}
  .swatches button{width:30px;height:30px;border-radius:50%;border:2px solid transparent;cursor:pointer;padding:0;box-shadow:inset 0 0 0 2px var(--h-surface)}
  .swatches button[aria-checked=true]{border-color:var(--h-ink)}
  .home-sug{font-size:13px;color:var(--h-muted);min-height:18px;line-height:1.35}
  .home-sug b{color:var(--h-ink)}
  .home-card{position:relative;display:grid;grid-template-columns:44px 1fr auto;align-items:center;gap:12px;width:100%;text-align:left;
    background:var(--h-surface);color:var(--h-ink);border:1px solid var(--h-border);border-radius:16px;padding:14px 14px;cursor:pointer;
    font:inherit;transition:transform .12s,border-color .15s,background .15s,box-shadow .15s}
  .home-card:hover{border-color:var(--h-border-hi)}
  .home-card:active{transform:scale(.985)}
  .home-card .ic{width:44px;height:44px;border-radius:12px;display:grid;place-items:center;background:var(--h-surface-2);color:var(--h-accent)}
  .home-card .ic svg{width:24px;height:24px}
  .home-card .tt{display:block;font-size:17px;font-weight:800;line-height:1.2}
  .home-card .ds{display:block;font-size:12.5px;color:var(--h-muted);margin-top:3px;line-height:1.3}
  .home-card kbd{font:600 11px/1 ui-monospace,Menlo,monospace;color:var(--h-muted);border:1px solid var(--h-border);border-bottom-width:2px;border-radius:5px;padding:4px 6px}
  .home-card .badge{position:absolute;top:-7px;right:12px;background:var(--h-accent);color:var(--h-accent-ink);font-weight:800;font-size:11px;line-height:1;font-family:inherit;border-radius:999px;padding:4px 8px}
  .home-card .badge[hidden]{display:none}
  .home-card.suggested{border-color:var(--h-accent);background:linear-gradient(0deg,var(--h-accent-soft),var(--h-accent-soft)),var(--h-surface);box-shadow:0 0 0 1px var(--h-accent)}
  .home-card .home-chip{display:none;width:max-content;margin-bottom:4px;font-weight:800;font-size:10px;line-height:1;letter-spacing:.08em;text-transform:uppercase;
    color:var(--h-accent);border:1px solid var(--h-accent);border-radius:999px;padding:3px 7px}
  .home-card.suggested .home-chip{display:block}
  .home-card.suggested .ic{background:var(--h-accent);color:var(--h-accent-ink)}
  #homeScreen button:focus-visible,#homeScreen a:focus-visible{outline:2px solid var(--h-accent);outline-offset:2px}
  .home-skip{align-self:center;margin-top:2px;background:none;border:none;color:var(--h-muted);font-weight:600;font-size:14px;line-height:1;font-family:inherit;padding:10px;cursor:pointer}
  .home-apps{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;width:100%;margin-top:6px;padding-top:14px;border-top:1px solid var(--h-border)}
  .home-apps-lbl{width:100%;text-align:center;color:var(--h-muted);font-size:11px;letter-spacing:.1em;text-transform:uppercase;font-weight:700}
  .home-apps a{color:var(--h-ink);text-decoration:none;background:var(--h-surface);border:1px solid var(--h-border);border-radius:999px;padding:8px 13px;font-size:13px}
  .home-apps a:active{transform:scale(.98)}
  .home-hint{text-align:center;font-size:11px;color:var(--h-muted);opacity:.8}
  @media (hover:none){ .home-card kbd,.home-hint{display:none} }
  @media (prefers-reduced-motion:no-preference){
    #homeScreen:not([hidden]) .home-card{animation:homeIn .28s both}
    #homeScreen .home-card[data-act=update]{animation-delay:.04s}#homeScreen .home-card[data-act=lookup]{animation-delay:.08s}#homeScreen .home-card[data-act=check]{animation-delay:.12s}
  }
  @keyframes homeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
  #homeBtn{background:none;border:none;color:var(--ink);opacity:.5;font-size:18px;cursor:pointer;float:right}
</style>
</head>
<body>
<!-- onload chooser (TK-12346): Adobe Spectrum themes + accent, smart "Suggested" action, keyboard 1–4 / Enter -->
<div id="homeScreen" role="dialog" aria-modal="true" aria-labelledby="homeGreet" data-theme="darkest" data-accent="blue">
  <div class="home-inner">
    <div class="home-top">
      <div>
        <div class="home-title">DW Sample Station</div>
        <div class="home-greet" id="homeGreet">Welcome</div>
        <div class="home-date" id="homeDate"></div>
      </div>
      <button class="home-theme-btn" id="homeThemeBtn" aria-expanded="false" aria-controls="homePrefs"><span class="dot"></span>Theme</button>
    </div>
    <div class="home-prefs" id="homePrefs" hidden>
      <div class="home-prefs-lbl" id="lblTheme">Theme</div>
      <div class="seg" role="radiogroup" aria-labelledby="lblTheme" id="homeThemeSeg">
        <button role="radio" data-theme="light">Light</button>
        <button role="radio" data-theme="dark">Dark</button>
        <button role="radio" data-theme="darkest">Darkest</button>
        <button role="radio" data-theme="auto">Auto</button>
      </div>
      <div class="home-prefs-lbl" id="lblAccent">Accent</div>
      <div class="swatches" role="radiogroup" aria-labelledby="lblAccent" id="homeAccent">
        <button role="radio" data-accent="blue" aria-label="Blue" style="background:#2680eb"></button>
        <button role="radio" data-accent="gold" aria-label="DW Gold" style="background:#c9a45c"></button>
        <button role="radio" data-accent="green" aria-label="Green" style="background:#12805c"></button>
        <button role="radio" data-accent="purple" aria-label="Purple" style="background:#7e4bf3"></button>
        <button role="radio" data-accent="red" aria-label="Red" style="background:#d31510"></button>
      </div>
    </div>
    <div class="home-sug" id="homeSug" aria-live="polite"></div>
    <button class="home-card" data-act="add" aria-keyshortcuts="1">
      <span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="3"/><path d="M12 8v8M8 12h8"/></svg></span><span><span class="home-chip">Suggested</span><span class="tt">Add New SKU</span><span class="ds">Photograph a new sample front + back and create it</span></span><kbd>1</kbd>
      <span class="badge" hidden></span>
    </button>
    <button class="home-card" data-act="update" aria-keyshortcuts="2">
      <span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a9 9 0 1 1-2.64-6.36"/><path d="M21 3v6h-6"/></svg></span><span><span class="home-chip">Suggested</span><span class="tt">Update SKU</span><span class="ds">Snap the label, then refresh photos or details</span></span><kbd>2</kbd>
      <span class="badge" hidden></span>
    </button>
    <button class="home-card" data-act="lookup" aria-keyshortcuts="3">
      <span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg></span><span><span class="home-chip">Suggested</span><span class="tt">Look Up SKU</span><span class="ds">Scan a sample to identify it in the catalog</span></span><kbd>3</kbd>
      <span class="badge" hidden></span>
    </button>
    <button class="home-card" data-act="check" aria-keyshortcuts="4">
      <span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 8 12 3 3 8v8l9 5 9-5z"/><path d="m8 12 3 3 5-6"/></svg></span><span><span class="home-chip">Suggested</span><span class="tt">Check Sample In</span><span class="ds">Mark ordered samples as received</span></span><kbd>4</kbd>
      <span class="badge" hidden></span>
    </button>
    <button class="home-skip" data-act="skip">Browse catalog grid →</button>
    <div class="home-hint">Press 1–4 to choose · Enter for the suggestion</div>
    <!-- Other apps (TK-12162): the 4 simplified capture concepts -->
    <nav class="home-apps" aria-label="Other apps">
      <div class="home-apps-lbl">Other apps</div>
      <a href="/simple-minimal">Minimal Showroom</a>
      <a href="/simple-instant">Instant Film</a>
      <a href="/simple-probooth">Pro Booth</a>
      <a href="/simple-wizard">Guided Wizard</a>
      <a href="/vendor-photos">Vendor Photos</a>
      <a href="/captures">Capture History</a>
    </nav>
  </div>
</div>
<header>
  <h1>Mobile DW SKU <span>Photos</span> <a href="/selfcheck" class="ver" title="tap to check the app is working">__VER__</a> <button id="homeBtn" title="home">⌂</button></h1>
  <div class="bar"><i id="prog"></i></div>
  <div class="meta"><span id="count">…</span><span id="remain"></span></div>
  <div class="controls">
    <input type="search" id="q" placeholder="🔎 Search by name or code…" autocapitalize="characters">
    <button class="scanbtn" id="scanBtn" title="Scan a printed code (the SKU / model number)">🔢 Scan Code</button>
    <button class="scanbtn" id="micBtn" title="Search by voice — say a brand, a code, or what you are looking for">🎤 Speak</button>
    <button class="scanbtn" id="camBtn" title="Switch the camera (back or front)">📷 Back camera</button>
    <button class="scanbtn" id="simBtn" title="Take a photo of the pattern to find it and ones that look like it">🎨 Find Similar</button>
    <button class="scanbtn" id="fbBtn" title="Take 2 photos — the back label and the front pattern — to find the item">🔎 Label + Photo</button>
    <button class="scanbtn" id="addBtn" title="Add a brand-new item (it is saved as a draft)">➕ Add New</button>
    <input type="file" accept="image/*" capture="environment" id="simInput" hidden>
    <select id="sort" aria-label="Sort"></select>
  </div>
  <div class="controls">
    <div class="chips">
      <div class="chip" data-f="need">Needs photo</div>
      <div class="chip" data-f="live">🟢 Live</div>
      <div class="chip" data-f="done">Done</div>
      <div class="chip" data-f="new" id="chipNew">✨ New</div>
      <div class="chip on" data-f="twil" id="chipTwil">🧵 All TWIL</div>
      <div class="chip" data-f="all">All</div>
      <div class="chip" data-f="any">🔎 Any SKU</div>
      <div class="chip" data-f="shop">🌐 All Shopify</div>
      <div class="chip" data-f="similar" id="chipSim">🎨 Similar</div>
      <div class="chip" data-f="fav">⭐ Favorites</div>
      <div class="chip" data-f="recent">🕘 Recent</div>
    </div>
    <div class="dens">Size <input type="range" id="dens" min="1" max="4" value="1"></div>
  </div>
</header>
<main id="grid"></main>
<!-- Other apps: the 4 simplified capture concepts (TK-12162), linked at the bottom of the upload screen -->
<nav class="other-apps" aria-label="Other apps">
  <div class="oa-lbl">Other apps</div>
  <a href="/simple-minimal">Minimal Showroom</a>
  <a href="/simple-instant">Instant Film</a>
  <a href="/simple-probooth">Pro Booth</a>
  <a href="/simple-wizard">Guided Wizard</a>
  <a href="/vendor-photos">Vendor Photos</a>
  <a href="/captures">Capture History</a>
</nav>
<style>
  .other-apps{display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:center;
    margin:24px auto 32px;padding:14px 16px;border-top:1px solid var(--line);max-width:900px}
  .other-apps .oa-lbl{width:100%;text-align:center;color:var(--muted);font-size:12px;
    letter-spacing:.08em;text-transform:uppercase;margin-bottom:2px}
  .other-apps a{color:var(--ink);text-decoration:none;border:1px solid var(--line);background:var(--card);
    border-radius:999px;padding:8px 14px;font-size:14px}
  .other-apps a:hover,.other-apps a:focus-visible{border-color:var(--gold);color:var(--gold)}
</style>

<!-- shared pre-capture "Photoshop" engine — manual canvas pixel math, must load before the main script (TK-12115) -->
<script src="/js/capture-pipeline.js"></script>
<!-- shared hardened camera-acquisition helper (bounded timeout + generation guard + bounded play), TK-12127 -->
<script src="/js/acquire-camera.js"></script>
<!-- shared capture engine (TK-12348): overlays + actions + components — /app and every /t/<theme> run on it -->
<script src="/js/dw-engine.js"></script>
<script>
/* global DWEngine */
// /app = the header + grid + home-chooser LAYOUT. Every overlay, action and capture flow lives in the shared
// engine (public/js/dw-engine.js, TK-12348) — the same one the themed apps at /t/<slug> run on.
const $=s=>document.querySelector(s), grid=$('#grid');
const E=DWEngine;
let filter='twil', collapsed=false; // default landing = All TWIL
const LS=k=>localStorage.getItem('dwphoto_'+k), setLS=(k,v)=>localStorage.setItem('dwphoto_'+k,v);

// Density: set grid-template-columns DIRECTLY (older iOS Safari ignores var() as
// the count inside repeat() — writing the resolved repeat(N,1fr) works everywhere).
function applyDensity(n){
  const c=Math.max(1,Math.min(4,parseInt(n,10)||1));
  document.documentElement.style.setProperty('--cols',c);
  $('#grid').style.gridTemplateColumns='repeat('+c+',1fr)';
}

// restore prefs
if(LS('dens')){$('#dens').value=LS('dens');applyDensity(LS('dens'));}
else applyDensity($('#dens').value);
$('#sort').innerHTML=E.SORTS.map(o=>`<option value="${o.key}">${o.label}</option>`).join('');   // one sort list for every grid (engine)
$('#sort').value=(LS('sort')&&E.SORTS.some(o=>o.key===LS('sort')))?LS('sort'):'newest';
if(LS('filter')){filter=LS('filter');document.querySelectorAll('.chip').forEach(c=>c.classList.toggle('on',c.dataset.f===filter));}

const toast=m=>E.toast(m);
function fmt(ts){if(!ts)return'';try{return new Date(ts).toLocaleString(undefined,{month:'short',day:'numeric',hour:'numeric',minute:'2-digit'})}catch(e){return''}}

async function load(){ await E.loadQueue(); render(); }
const priced=x=>E.priced(x);
function loadTwil(){ return E.loadTwil(); }   // global: the "Try again" link in render() calls it inline
let _lkT=0;
async function doLookup(){
  const q=($('#q').value||'').trim();
  if(!q){ E.lookup(''); render(); return; }
  try{ const d=await E.lookup(q);
    // No active TWIL hit? The engine already checked whether it is a DISCONTINUED / deleted SKU —
    // if so, show the RED card pointing at its live successor.
    if(!d.items.length){
      if(d.disco){ render(); return; }
      // otherwise auto-escalate to a store-wide search (mfr# + DW SKU across every vendor)
      toast('Not in TWIL — searching all Shopify…');
      document.querySelectorAll('.chip').forEach(z=>z.classList.remove('on')); const sc=document.querySelector('.chip[data-f="shop"]'); if(sc)sc.classList.add('on');
      filter='shop'; setLS('filter','shop'); $('#q').placeholder='🌐 Any Shopify SKU, name, vendor, or collection…';
      return doShopSearch();
    }
    render();
  }catch(e){ toast('Lookup failed'); }
}
function discoCardHtml(d){
  const succ = d.successor;
  const succUrl = succ && succ.handle ? ('https://www.designerwallcoverings.com/products/'+succ.handle) : '';
  const succHtml = succ
    ? `<div class="disco-succ">
         <span class="slabel">Now available as →</span>
         <span class="stitle">${(succ.title||'').replace(/</g,'&lt;')}</span>
         <span class="ssku">${(succ.sku||'').replace(/</g,'&lt;')}</span>
         ${succUrl?`<a class="go" href="${succUrl}" target="_blank" rel="noopener">View live product →</a>`:''}
       </div>`
    : `<div class="disco-nosucc">No live successor found — search the store for a comparable pattern.</div>`;
  return `<div class="disco-card">
      <div class="dhead">⛔ Discontinued <span class="dstatus">${(d.status||'').replace(/</g,'&lt;')}</span></div>
      <div>${(d.title||'').replace(/</g,'&lt;')} <span class="dsku">${(d.sku||'').replace(/</g,'&lt;')}</span></div>
      ${succHtml}
    </div>`;
}
// Live store-wide search — covers EVERY Shopify SKU, not just Fentucci.
async function doShopSearch(){
  const q=($('#q').value||'').trim();
  if(!q){ E.shopSearch(''); render(); return; }
  $('#count').textContent='Searching all Shopify…';
  try{ const d=await E.shopSearch(q);
    if(d.err) toast('Shopify: '+d.err); render();
  }catch(e){ toast('Shopify search failed'); }
}
// map a few common color NAMES → a hex chip so the recognized palette reads at a glance
const _CHEX={black:'#111',white:'#f4f1ea',cream:'#efe7d4',ivory:'#f3ecd8',beige:'#d9c9a8',taupe:'#b3a489',
  gray:'#9a9a9a',grey:'#9a9a9a',charcoal:'#3a3a3a',silver:'#c7c7c7',gold:'#c8a24a',brass:'#b08d2f',
  brown:'#6b4a2b',chocolate:'#3f2a1a',tan:'#c2a878',rust:'#9c4a23',terracotta:'#b5613c',
  red:'#a3322a',burgundy:'#5e2230',pink:'#d98aa0',blush:'#e3c3c5',coral:'#e0745a',orange:'#cc6a2c',
  yellow:'#d9b84a',mustard:'#b8902f',green:'#3f7a4a',olive:'#6b6a30',sage:'#9aa885',emerald:'#1f6b4a',
  teal:'#2f7b7b',blue:'#345f8a',navy:'#1f2f4a','slate blue':'#5a6b86',sky:'#7fa6c9',
  purple:'#6a4a82',lavender:'#b8a8cf',plum:'#5e3a55',mauve:'#9a7a86'};
function colorHex(name){const k=String(name||'').toLowerCase().trim();if(_CHEX[k])return _CHEX[k];
  for(const c in _CHEX){if(k.includes(c))return _CHEX[c];}return null;}
function recogBanner(){
  const RECOGNIZED=E.state.recognized;
  if(!RECOGNIZED)return'';
  const r=RECOGNIZED, parts=[];
  if(r.motif)parts.push(r.motif); if(r.style)parts.push(r.style); if(r.scale)parts.push(r.scale+' scale');
  const desc=r.description||(parts.length?parts.join(' · '):'pattern');
  const tags=[];
  if(r.material)tags.push(`<span>🧵 ${r.material}</span>`);
  if(r.motif)tags.push(`<span>${r.motif}</span>`);
  if(r.style)tags.push(`<span>${r.style}</span>`);
  (r.colors||[]).forEach(c=>{const h=colorHex(c);tags.push(h?`<span class="sw"><i style="background:${h}"></i>${c}</span>`:`<span>${c}</span>`);});
  if(r.background)tags.push(`<span class="sw"><i style="background:${colorHex(r.background)||'#444'}"></i>bg: ${r.background}</span>`);
  const codeLine=r.code?` <span class="rg-code" onclick="chipTo('shop');$('#q').value='${r.code}';doShopSearch();">· 🏷 ${r.code} ↗</span>`:'';
  return `<div class="recog"><div class="rg-d">🎨 Recognized: <b>${desc}</b>${codeLine}</div><div class="rg-tags">${tags.join('')}</div></div>`;
}
function render(){
  const st=E.state, ITEMS=st.queue, DISCO=st.disco, TWILSTATE=st.twilState, TWILITEMS=st.twil, RECOGNIZED=st.recognized, VRANK=st.vrank, LOOKUP_INDEXED=st.lookupIndexed, camFacing=E.camFacing;
  if(collapsed){
    const done=ITEMS.filter(x=>x.done).length, total=ITEMS.length;
    $('#prog').style.width=(total?done/total*100:0)+'%';
    $('#count').textContent=`${total} patterns · ${done} done`;
    $('#remain').textContent='pick a filter to begin';
    grid.innerHTML='<div class="empty">📸 Pick a filter, 🔎 search a SKU/collection, or tap 🔢 Scan to begin.'+(total?('<br><br>'+ (total-done) +' still need a photo.'):'')+'</div>';
    return;
  }
  // A resolved discontinued/deleted SKU → red successor card (takes over the grid for the 'any' lookup filter).
  if(DISCO && (filter==='any'||filter==='shop')){
    $('#prog').style.width='0%';
    $('#count').textContent='Discontinued SKU';
    $('#remain').textContent='→ see live successor';
    grid.innerHTML=discoCardHtml(DISCO);
    return;
  }
  const q=($('#q').value||'').toLowerCase(), sort=$('#sort').value;
  const recog = filter==='similar'; // server returned a ranked similarity set
  const liveSearch = filter==='any'||filter==='shop'; // server already filtered these
  const curated = filter==='fav'||filter==='recent'; // server-stored lists, local q-filter only
  const list=E.items(filter,q,sort);   // filter + typed-query + sort (shared with the themed apps)
  if(liveSearch){
    const typed=($('#q').value||'').trim();
    $('#prog').style.width='0%';
    $('#count').textContent = typed ? `${list.length} match${list.length===1?'':'es'}`
      : (filter==='shop' ? 'Type any SKU, name, or vendor to search the whole store'
                         : 'Type a SKU, model #, or name to find any product');
    $('#remain').textContent = filter==='shop' ? '🌐 all Shopify' : (LOOKUP_INDEXED?`${LOOKUP_INDEXED} indexed`:'');
  } else if(filter==='new'){
    $('#prog').style.width='0%';
    $('#count').textContent = `${list.length} new SKU${list.length===1?'':'s'} to create`;
    $('#remain').textContent = '📷 shoot to create + go live';
  } else if(filter==='twil'){
    const live=list.filter(x=>x.status==='ACTIVE').length;
    $('#prog').style.width='0%';
    $('#count').textContent = TWILSTATE==='loading' && !TWILITEMS.length ? '⏳ Loading the full catalog (~250k items)…'
      : TWILSTATE==='error' && !TWILITEMS.length ? '⚠️ Catalog failed to load' : `${list.length} TWIL items · ${live} live`;
    $('#remain').textContent = 'all grasscloth — photo or not';
  } else if(recog){
    $('#prog').style.width='0%';
    $('#count').textContent = RECOGNIZED ? `${list.length} similar match${list.length===1?'':'es'}` : '🎨 Find Similar';
    $('#remain').textContent = !RECOGNIZED ? '📷 photo a swatch to start'
      : VRANK==='running' ? '🎨 ranking by image…'
      : VRANK==='done' ? '✓ sorted by actual pattern image'
      : 'ranked by pattern · whole catalog';
  } else if(curated){
    $('#prog').style.width='0%';
    $('#count').textContent = filter==='fav' ? `${list.length} ⭐ favorite${list.length===1?'':'s'}` : `${list.length} recently updated`;
    $('#remain').textContent = filter==='fav' ? 'tap ☆ on any card to add' : 'newest first';
  } else {
    const done=ITEMS.filter(x=>x.done).length, total=ITEMS.length;
    $('#prog').style.width=(total?done/total*100:0)+'%';
    $('#count').textContent=`${done} / ${total} photographed`;
    $('#remain').textContent=`${total-done} remaining`;
  }
  const typedQ=($('#q').value||'').trim();
  const emptyMsg = filter==='shop' ? (typedQ?'No Shopify product matches that — try the SKU, pattern name, or vendor.':'🌐 Search EVERY Shopify SKU above — add or update a photo from your phone.')
    : filter==='any' ? (typedQ?'No Fentucci product matches that — try the model # or DW SKU.':'🔎 Search any TWIL/Fentucci SKU above to add or replace its photo.')
    : filter==='new' ? (typedQ?'No new SKU matches that.':'✅ All new SKUs created — none left to add.')
    : filter==='fav' ? (typedQ?'No favorite matches that.':'⭐ No favorites yet — tap the ☆ star on any card to pin SKUs you update often.')
    : filter==='recent' ? (typedQ?'No recent SKU matches that.':'🕘 Nothing updated yet — SKUs you photograph show up here, newest first.')
    : filter==='similar' ? (RECOGNIZED?'No similar items found — try a flatter, closer shot of just the pattern.':'🎨 Tap 🎨 Similar above and photograph a swatch — the local AI reads its colors, motif & material, then finds this + lookalikes across the whole catalog.')
    : filter==='twil' && TWILSTATE==='loading' && !TWILITEMS.length ? '⏳ Loading the full catalog — this can take up to a minute on a slow connection…'
    : filter==='twil' && TWILSTATE==='error' && !TWILITEMS.length ? '⚠️ Couldn’t load the catalog. <a href="#" onclick="event.preventDefault();loadTwil()">Try again</a>'
    : 'Nothing here — switch filter or clear search.';
  grid.innerHTML = list.length? '' : `<div class="empty">${emptyMsg}</div>`;
  if(recog && RECOGNIZED) grid.insertAdjacentHTML('afterbegin', recogBanner());
  // MEMORY GUARD (iOS OOM-freeze fix): never build more than RENDER_CAP card nodes. The TWIL list is
  // ~88k items (36MB) — rendering them all drove the browser to 13GB and crashed the tab. Cap it and
  // tell the user to search/filter to narrow. (Search/filter shrink `list` before this point.)
  const RENDER_CAP=500;
  const shown = list.length>RENDER_CAP ? list.slice(0,RENDER_CAP) : list;
  for(const x of shown){
    const isNew = x.needs_create && !x.done;  // on the sheet but not yet created in Shopify
    const photographed = !isNew && (x.done || !!x.image || x.live); // already has a real photo
    const el=document.createElement('div'); el.className='card'+(photographed?' done':'');
    const badge=x.push_err?'<span class="badge b-err">PUSH ERR</span>':x.live?'<span class="badge b-live">● LIVE</span>':isNew?'<span class="badge b-new">✨ NEW</span>':photographed?'<span class="badge b-done">✓ PHOTO</span>':'<span class="badge b-need">NEEDS</span>';
    const thumbUrl=x.photo||(isNew?'':x.image)||'';
    const thumb=thumbUrl?`data-bg="${thumbUrl}"`:'';   // lazy: bg applied on-visible (see observer below)
    const adjustable=photographed; // anything with a real photo can be re-opened in the editor
    el.innerHTML=`
      <div class="thumb ${adjustable?'tap':''}" ${thumb} title="${adjustable?'click to adjust':''}">${thumbUrl?'':'▦'}${badge}${x.product_id?`<button class="fav-btn${x.fav?' on':''}" title="favorite">${x.fav?'★':'☆'}</button>`:''}</div>
      <div class="body">
        <div class="ttl">${(x.title||'').replace(/ \\| Fentucci$/,'')}</div>
        <div class="model" title="tap to copy the model number" onclick="navigator.clipboard&&navigator.clipboard.writeText('${x.mfr||''}')"><small>Model&nbsp;#</small><b>${x.mfr||'—'}</b></div>
        <div class="sub">
          <span class="sku">${x.dw_sku||'—'}</span>
          <span class="price ${priced(x)?'':'no'}">${priced(x)?'$'+x.price:'no price'}</span>
          <span>${isNew?'NEW · not on Shopify':x.status}</span>
        </div>
        ${(x.collections&&x.collections.length)?`<div class="cols">${x.collections.slice(0,4).map(c=>'<span class="colchip">📚 '+c+'</span>').join('')}</div>`:''}
        ${x.ts?`<div class="when">🕓 ${fmt(x.ts)}${x.live?(x.created?' · 🟢 CREATED + LIVE':' · 🟢 LIVE on store'):x.shopify_pushed?(x.live_reason?(' · photo on Shopify ('+x.live_reason+')'):' · photo on Shopify'):''}</div>`:''}
        ${x.push_err?`<div class="when" style="color:var(--red)">${x.push_err}</div>`:''}
        ${(x.keep_images&&x.image)?`<label class="onlyimg${x._replaceAll?' armed':''}"><input type="checkbox" class="onlyimg-cb"${x._replaceAll?' checked':''}> Replace all — make this the only image</label>`:''}
        <div class="actions">
          ${adjustable?'<button class="enh">✨ Enhance</button>':''}
          ${x.product_id?'<button class="gal">🖼 Photos</button>':''}
          <label class="shoot">${isNew?'📷 Create + Go Live':photographed?'📷 Update Photo':'📷 Add Photo'}<input type="file" accept="image/*" capture="${camFacing==='user'?'user':'environment'}"></label>
          ${(x.done||x.live||x.image||isNew)?'':'<button class="skip">Skip</button>'}
        </div>
      </div>`;
    const editSrc=()=> (x.photo||('/api/current-image?pid='+x.product_id))+(x.photo?'?':'&')+'t='+Date.now();
    el.querySelector('.shoot input').addEventListener('change',e=>E.shoot(x,e.target.files[0]));
    const gal=el.querySelector('.gal'); if(gal) gal.addEventListener('click',()=>E.Gallery.open(x));
    const oi=el.querySelector('.onlyimg-cb'); if(oi) oi.addEventListener('change',e=>{ x._replaceAll=e.target.checked; el.querySelector('.onlyimg').classList.toggle('armed',e.target.checked); });
    const enh=el.querySelector('.enh'); if(enh) enh.addEventListener('click',()=>E.Editor.open(editSrc(),{item:x}));
    const th=el.querySelector('.thumb.tap'); if(th) th.addEventListener('click',()=>E.Editor.open(editSrc(),{item:x}));
    const fb=el.querySelector('.fav-btn'); if(fb) fb.addEventListener('click',ev=>{ev.stopPropagation();E.toggleFav(x,fb);});
    const sk=el.querySelector('.skip'); if(sk) sk.addEventListener('click',()=>E.skip(x));
    grid.appendChild(el);
  }
  if(list.length>shown.length){ const n=document.createElement('div'); n.className='empty'; n.style.gridColumn='1/-1';
    n.innerHTML=`Showing first <b>${shown.length}</b> of <b>${list.length.toLocaleString()}</b> — 🔎 search a SKU / name / vendor to narrow.`; grid.appendChild(n); }
  lazyThumbs();   // only load thumbnails as they scroll into view (huge mobile win vs 100s of images at once)
}
// CSS background-images can't use loading="lazy" — apply each thumb's bg only when it nears the viewport.
let _thumbObs=null;
function lazyThumbs(){
  if(_thumbObs) _thumbObs.disconnect();
  if(!('IntersectionObserver' in window)){ grid.querySelectorAll('.thumb[data-bg]').forEach(t=>{ t.style.backgroundImage=`url('${t.getAttribute('data-bg')}')`; t.removeAttribute('data-bg'); }); return; }
  _thumbObs=new IntersectionObserver((es,o)=>{ for(const e of es){ if(e.isIntersecting){ const t=e.target,u=t.getAttribute('data-bg'); if(u){ t.style.backgroundImage=`url('${u}')`; t.removeAttribute('data-bg'); } o.unobserve(t); } } },{rootMargin:'400px'});
  grid.querySelectorAll('.thumb[data-bg]').forEach(t=>_thumbObs.observe(t));
}

$('#q').addEventListener('input',()=>{
  collapsed=false;
  if(filter==='any'){ clearTimeout(_lkT); _lkT=setTimeout(doLookup,250); }
  else if(filter==='shop'){ clearTimeout(_lkT); _lkT=setTimeout(doShopSearch,350); } // a touch longer — each keystroke hits Shopify
  else render();
});
$('#sort').addEventListener('change',e=>{setLS('sort',e.target.value);render();});
['input','change'].forEach(ev=>$('#dens').addEventListener(ev,e=>{applyDensity(e.target.value);setLS('dens',e.target.value);}));
document.querySelectorAll('.chip').forEach(c=>c.addEventListener('click',()=>{
  collapsed=false;
  document.querySelectorAll('.chip').forEach(z=>z.classList.remove('on'));c.classList.add('on');filter=c.dataset.f;setLS('filter',filter);
  if(filter==='any'){ $('#q').placeholder='🔎 SKU, model #, name, or collection…'; $('#q').focus(); doLookup(); }
  else if(filter==='shop'){ $('#q').placeholder='🌐 Any Shopify SKU, name, vendor, or collection…'; $('#q').focus(); doShopSearch(); }
  else if(filter==='new'){ $('#q').placeholder='🔎 Filter the new SKUs…'; E.loadNew(); }
  else if(filter==='twil'){ $('#q').placeholder='🔎 Filter TWIL items…'; E.loadTwil(); }
  else if(filter==='fav'){ $('#q').placeholder='🔎 Filter favorites…'; render(); E.loadFav(); }
  else if(filter==='recent'){ $('#q').placeholder='🔎 Filter recent…'; render(); E.loadRecent(); }
  else if(filter==='similar'){ $('#q').placeholder='🎨 Photo a swatch to find similar'; if(!E.state.similar.length)$('#simInput').click(); else render(); }
  else { $('#q').placeholder='🔎 Search name or model #…'; render(); }
}));
function chipTo(f){
  document.querySelectorAll('.chip').forEach(z=>z.classList.remove('on'));
  const sc=document.querySelector('.chip[data-f="'+f+'"]'); if(sc)sc.classList.add('on');
  filter=f; setLS('filter',f); $('#q').placeholder='🔎 SKU, model #, name, or collection…';
}
// ── engine → layout: the engine asks for lists / reports data changes; /app maps them onto its chips + grid ──
E.on('search',e=>{                       // {filter, q, run, focus}: scan / voice / pattern results land here
  if(e.focus){ $('#q').focus(); return; }
  collapsed=false;
  if(e.filter) chipTo(e.filter);
  if(e.q!=null) $('#q').value=e.q;
  if(!e.run) return;
  if(e.filter==='any') return doLookup();
  if(e.filter==='shop') return doShopSearch();
  render();
});
E.on('items',e=>{                        // a data set changed → repaint when it is the one on screen
  if(e.source==='new'){ const ch=$('#chipNew'); if(ch) ch.textContent='✨ New ('+(e.total||0)+')'; if(filter==='new') render(); }
  else if(e.source==='fav'){ if(filter==='fav') render(); }
  else if(e.source==='recent'){ if(filter==='recent') render(); }
  else if(e.source==='similar'){ if(filter==='similar') render(); }
  else if(e.source==='card') render();   // a card's photo / skip state changed
});
E.on('twilState',e=>{ if(e.total!=null){ const ch=$('#chipTwil'); if(ch) ch.textContent='🧵 All TWIL ('+e.total+')'; } if(filter==='twil') render(); });
E.on('status',e=>{ $('#count').textContent=e.text; });
E.on('galleryClosed',()=>{ if(filter!=='any'&&filter!=='shop'&&filter!=='new') load(); });
E.on('camFacing',()=>syncCamBtn());

// ── onload chooser (TK-12346): Add · Update · Look Up · Check In — themed (Adobe Spectrum) + smart suggestion ──
const HOME_KEYS={'1':'add','2':'update','3':'lookup','4':'check'};
function hGet(k,d){ try{ return localStorage.getItem(k)||d; }catch(e){ return d; } }
function hSet(k,v){ try{ localStorage.setItem(k,v); }catch(e){} }
function routeHome(act){ $('#homeScreen').hidden=true; if(act!=='skip') hSet('dwHomeLast',act);
  try{ sessionStorage.setItem('dwHomeSeen','1'); }catch(e){}   // shown once per tab — Steve: "why does this page keep coming up"
  // onload → straight to the camera; details show after the first scan
  if(act==='add') E.CaptureForm.open(null,{mode:'add',camera:true});
  else if(act==='update') E.CaptureForm.open(null,{mode:'update',camera:true});
  else if(act==='lookup') E.Identify.lookup();          // camera-first front→back → identify
  else if(act==='check') E.Identify.checkIn();          // show the incoming (ordered-not-received) queue first
  else if(act==='skip'){ if(!E.state.twil.length) E.loadTwil(); }   // lazy-load the big TWIL grid only when actually viewing it
}
// theme: light | dark | darkest | auto (auto follows the OS light/dark setting)
const _homeMQ=window.matchMedia?matchMedia('(prefers-color-scheme: light)'):null;
function applyHomeTheme(){ const hs=$('#homeScreen'), pick=hGet('dwHomeTheme','darkest'), acc=hGet('dwHomeAccent','blue');
  hs.dataset.theme = pick==='auto' ? (_homeMQ&&_homeMQ.matches?'light':'darkest') : pick;
  hs.dataset.accent = acc;
  document.querySelectorAll('#homeThemeSeg [role=radio]').forEach(b=>b.setAttribute('aria-checked',String(b.dataset.theme===pick)));
  document.querySelectorAll('#homeAccent [role=radio]').forEach(b=>b.setAttribute('aria-checked',String(b.dataset.accent===acc)));
}
if(_homeMQ&&_homeMQ.addEventListener) _homeMQ.addEventListener('change',()=>{ if(hGet('dwHomeTheme','darkest')==='auto') applyHomeTheme(); });
document.querySelectorAll('#homeThemeSeg [role=radio]').forEach(b=>b.addEventListener('click',()=>{ hSet('dwHomeTheme',b.dataset.theme); applyHomeTheme(); }));
document.querySelectorAll('#homeAccent [role=radio]').forEach(b=>b.addEventListener('click',()=>{ hSet('dwHomeAccent',b.dataset.accent); applyHomeTheme(); }));
$('#homeThemeBtn').addEventListener('click',()=>{ const p=$('#homePrefs'), open=p.hidden; p.hidden=!open; $('#homeThemeBtn').setAttribute('aria-expanded',String(open)); });
// suggestion: samples waiting to check in beat everything; else the last action used here; else Add New SKU
let _homeSuggest='add';
function markSuggested(act,why){ _homeSuggest=act;
  document.querySelectorAll('#homeScreen .home-card').forEach(c=>c.classList.toggle('suggested',c.dataset.act===act));
  $('#homeSug').innerHTML=why?('✨ '+why):'';
}
async function refreshHomeSuggestion(){
  const h=new Date().getHours();
  $('#homeGreet').textContent=h<12?'Good morning':h<17?'Good afternoon':'Good evening';
  $('#homeDate').textContent=new Date().toLocaleDateString(undefined,{weekday:'long',month:'short',day:'numeric'});
  const last=hGet('dwHomeLast','');
  const lastName={add:'Add New SKU',update:'Update SKU',lookup:'Look Up SKU',check:'Check Sample In'}[last];
  markSuggested(lastName?last:'add', lastName?('Picking up where you left off — <b>'+lastName+'</b>'):'Start by adding a new sample');
  const badge=$('#homeScreen .home-card[data-act=check] .badge');
  try{
    const ctl=new AbortController(), t=setTimeout(()=>ctl.abort(),3000);
    const r=await(await fetch('/api/incoming-samples',{signal:ctl.signal})).json(); clearTimeout(t);
    const n=((r&&r.samples)||[]).length;
    badge.hidden=!n; badge.textContent=n>99?'99+':String(n);
    if(n) markSuggested('check','<b>'+n+' sample'+(n===1?'':'s')+'</b> ordered and waiting to be checked in');
  }catch(e){ badge.hidden=true; }   // offline / slow → keep the last-used suggestion
  if(!$('#homeScreen').hidden && matchMedia('(hover:hover)').matches){ const c=$('#homeScreen .home-card.suggested'); if(c) c.focus({preventScroll:true}); }
}
function showHome(){ $('#homeScreen').hidden=false; applyHomeTheme(); refreshHomeSuggestion(); }
document.querySelectorAll('#homeScreen .home-card, #homeScreen .home-skip').forEach(b=>b.addEventListener('click',()=>routeHome(b.dataset.act)));
document.addEventListener('keydown',e=>{
  if($('#homeScreen').hidden||e.metaKey||e.ctrlKey||e.altKey) return;
  const tag=(e.target&&e.target.tagName)||''; if(/INPUT|TEXTAREA|SELECT/.test(tag)) return;
  if(HOME_KEYS[e.key]){ e.preventDefault(); routeHome(HOME_KEYS[e.key]); }
  else if(e.key==='Enter' && !(e.target&&e.target.closest&&e.target.closest('#homeScreen button,#homeScreen a'))){ e.preventDefault(); routeHome(_homeSuggest); }
  else if(e.key==='Escape'){ const p=$('#homePrefs'); if(!p.hidden){ p.hidden=true; $('#homeThemeBtn').setAttribute('aria-expanded','false'); } }
});
$('#homeBtn').addEventListener('click',showHome);
applyHomeTheme();
// Once a task was chosen in this tab, later loads of /app (e.g. "← capture" links, the front picker's
// "Go to the app") skip the chooser and land on the app; the home button still re-opens it.
let _homeSeen=false; try{ _homeSeen=sessionStorage.getItem('dwHomeSeen')==='1'; }catch(e){}
if(_homeSeen) routeHome('skip'); else if(!$('#homeScreen').hidden) refreshHomeSuggestion();
function syncCamBtn(){ $('#camBtn').textContent=(E.camFacing==='user'?'🤳 Front camera':'📷 Back camera'); }
$('#camBtn').addEventListener('click',()=>E.flipCamera());
$('#micBtn').addEventListener('click',()=>E.voiceSearch($('#micBtn')));
$('#scanBtn').addEventListener('click',()=>E.LiveScan.open());   // vendors + live scan (HTTP → single-snap)
$('#fbBtn').addEventListener('click',()=>E.Identify.open());
$('#addBtn').addEventListener('click',()=>E.CaptureForm.open());
// 🎨 Similar — photograph the PATTERN itself (not a label) → recognize + find lookalikes.
$('#simBtn').addEventListener('click',()=>$('#simInput').click());
$('#simInput').addEventListener('change',async e=>{
  const f=e.target.files[0]; e.target.value=''; if(!f)return;
  collapsed=false; await E.recognizePhoto(f);
});
E.applyCamCapture(); syncCamBtn();   // apply saved camera choice to capture inputs + header toggle
load(); E.loadNew(); E.loadFav(); E.loadRecent(); E.vendors();   // vendors(): populate + auto-restore the sticky camera vendor bar on boot. NOTE: loadTwil() (36MB/88k) is lazy — fires on 'skip'/TWIL view, not page-load (iOS memory)
// TK-11962: restore an in-progress capture after an iOS reload (the engine persists typed field edits).
try{ E.CaptureForm.restoreIfSaved(); }catch(e){}
setInterval(()=>{ if(filter!=='any'&&filter!=='shop'&&filter!=='fav'&&filter!=='recent'&&filter!=='similar') load(); }, 60000);
</script>
</body>
</html>