product-recognition-demo/recognization/code/web/styles.css
kiyo9w 8f750c15c5 feat: price-tag UI, still-image tag passes, and honest tag evidence
Web client: price-tag boxes in a distinct style (teal, chip with the parsed
price), an OCR section in the product card showing the tag's price, name,
discount and the reconciliation verdict with both values, and a compliance strip
under the stage. The photo stage now draws the image into the canvas and uses the
same containment as the media, so boxes cannot drift from the product (they did:
the canvas stretched to the stage while the photo was contained).

Server fixes the browser runs forced out:
- Photo mode reconciled before identities existed, so every tag read "tag only".
  The tag bundle is cached with the still and classify re-reconciles against the
  identities it just produced, returning them per detection.
- The still path only ever read the first four bands (plan hands out two per
  call); it now drains every band, twice at different upscales so the vote has
  independent reads.
- Voting is confidence-weighted and a dead heat stays unread; the plausible
  shelf-price range is 10-9999, which stops a misread from becoming a ¥26,300
  price. The compliance coverage bar moved to 0.7 after 0.5 produced a false
  "group without price tag" on a shelf where every facing has one.

Measured on add/image6.jpg: 8 readable tags with names and prices, 942 ms for
the whole still, 7 real price mismatches, no false violations, and hints that
say plainly when a row could not be read.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-24 14:11:23 +09:00

253 lines
14 KiB
CSS

:root {
--bg-body: #f4fbf6;
--bg-card: #ffffff;
--primary-green: #22c55e;
--primary-dark: #15803d;
--primary-light: #dcfce7;
--primary-subtle: #f0fdf4;
--border-color: #d1fae5;
--border-strong: #86efac;
--text-main: #0f172a;
--text-muted: #64748b;
--amber: #f59e0b;
--amber-soft: #fef3c7;
--danger: #ef4444;
--danger-soft: #fee2e2;
--slate-soft: #e2e8f0;
--night: #0f172a;
--shadow-soft: 0 4px 20px -2px rgba(34, 197, 94, 0.08), 0 2px 6px -1px rgba(0, 0, 0, 0.04);
--radius-lg: 18px;
--radius-md: 12px;
--radius-sm: 8px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
[hidden] { display: none !important; }
body {
background-color: var(--bg-body);
color: var(--text-main);
min-height: 100vh;
display: flex;
flex-direction: column;
font-family: 'Plus Jakarta Sans', 'Noto Sans JP', sans-serif;
}
header {
background: var(--bg-card);
border-bottom: 1px solid var(--border-color);
padding: 14px 32px;
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
z-index: 100;
}
.brand { display: flex; align-items: center; gap: 14px; }
.brand-logo-svg { width: 44px; height: 44px; filter: drop-shadow(0 2px 6px rgba(34, 197, 94, 0.25)); }
.brand-title { font-size: 20px; font-weight: 800; color: #14532d; letter-spacing: -0.5px; display: flex; align-items: center; gap: 6px; }
.brand-jp { font-size: 13px; font-weight: 600; color: #16a34a; background: var(--primary-light); padding: 2px 8px; border-radius: 6px; }
.brand-sub { font-size: 12px; color: var(--text-muted); font-weight: 500; }
.header-actions { display: flex; align-items: center; gap: 12px; }
.lang-switch-btn {
background: var(--primary-subtle);
border: 1px solid var(--border-color);
color: var(--primary-dark);
padding: 6px 14px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
font-family: inherit;
}
.lang-switch-btn:hover { background: var(--primary-light); border-color: var(--primary-green); }
.conn-pill {
display: flex; align-items: center; gap: 6px;
font-size: 11px; font-weight: 700; color: #166534;
background: var(--primary-subtle); border: 1px solid var(--border-color);
padding: 5px 12px; border-radius: 999px;
}
.conn-pill .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--text-muted); }
.conn-pill[data-state="live"] .dot { background: var(--primary-green); box-shadow: 0 0 6px var(--primary-green); }
.conn-pill[data-state="photo"] .dot { background: #38bdf8; box-shadow: 0 0 6px #38bdf8; }
.conn-pill[data-state="error"] .dot { background: var(--danger); box-shadow: 0 0 6px var(--danger); }
.conn-pill[data-state="connecting"] .dot { background: var(--amber); box-shadow: 0 0 6px var(--amber); }
main {
flex: 1;
padding: 20px 32px;
display: grid;
grid-template-columns: 1fr 420px;
gap: 24px;
align-items: start;
}
.stage-panel {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 18px;
display: flex;
flex-direction: column;
gap: 14px;
box-shadow: var(--shadow-soft);
}
.toolbar { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; padding-bottom: 12px; border-bottom: 1px solid var(--border-color); }
.toolbar-group { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.segmented { display: flex; background: var(--primary-subtle); border: 1px solid var(--border-color); border-radius: 10px; padding: 3px; gap: 2px; }
.seg-btn { background: transparent; border: none; color: var(--text-muted); padding: 6px 12px; border-radius: 8px; font-size: 12px; font-weight: 700; cursor: pointer; font-family: inherit; }
.seg-btn.active { background: var(--primary-green); color: white; box-shadow: 0 2px 8px rgba(34, 197, 94, 0.3); }
.tool-btn {
background: var(--bg-card); border: 1px solid var(--border-color); color: var(--primary-dark);
padding: 7px 14px; border-radius: 10px; font-size: 12px; font-weight: 700; cursor: pointer;
display: inline-flex; align-items: center; gap: 6px; font-family: inherit;
}
.tool-btn:hover:not(:disabled) { background: var(--primary-subtle); border-color: var(--primary-green); }
.tool-btn:disabled { opacity: 0.45; cursor: default; }
.tool-btn.primary { background: var(--primary-green); border-color: var(--primary-green); color: white; }
.tool-btn.danger { background: var(--danger-soft); border-color: #fca5a5; color: #b91c1c; }
.tool-btn.wide { width: 100%; justify-content: center; }
.select { background: var(--bg-card); color: var(--text-main); border: 1px solid var(--border-color); padding: 7px 12px; border-radius: 10px; font-size: 12px; font-weight: 600; font-family: inherit; max-width: 220px; }
.hud { font-size: 11px; font-weight: 700; color: #166534; background: var(--primary-subtle); border: 1px solid var(--border-color); padding: 6px 12px; border-radius: 999px; cursor: pointer; font-variant-numeric: tabular-nums; }
.hud[data-detail="1"] { border-radius: 10px; }
.stage {
position: relative;
background: var(--night);
border-radius: 14px;
overflow: hidden;
height: 70vh;
min-height: 420px;
display: flex;
align-items: center;
justify-content: center;
}
.stage video, .stage img { max-width: 100%; max-height: 100%; display: block; }
/* The canvas is the same contained box as the media it annotates: its backing
store is the media's own pixel size, so server coordinates map 1:1 and the
element rect (used for click mapping) is exactly the visible picture. */
.stage canvas {
position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
max-width: 100%; max-height: 100%;
}
.stage-empty { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; color: #cbd5e1; font-size: 13px; text-align: center; padding: 24px; }
.stage-empty-icon { font-size: 34px; }
.hint-banner {
position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%);
background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(8px);
border: 1px solid var(--primary-green); color: #14532d;
padding: 8px 20px; border-radius: 999px; font-size: 12px; font-weight: 700;
box-shadow: 0 4px 16px rgba(34, 197, 94, 0.2); pointer-events: none;
}
.hint-banner[data-kind="warning"] { border-color: var(--amber); color: #92400e; }
.hint-banner[data-kind="danger"] { border-color: var(--danger); color: #991b1b; }
.side-panel { display: flex; flex-direction: column; gap: 16px; }
.card { background: var(--bg-card); border: 1px solid var(--border-color); border-radius: var(--radius-lg); padding: 18px; display: flex; flex-direction: column; gap: 14px; box-shadow: var(--shadow-soft); }
.card-header { font-size: 12px; font-weight: 800; color: #166534; text-transform: uppercase; letter-spacing: 0.6px; display: flex; justify-content: space-between; align-items: center; padding-bottom: 8px; border-bottom: 1px solid var(--border-color); }
.score-label { font-size: 12px; color: #16a34a; font-weight: 700; text-transform: none; letter-spacing: 0; }
.placeholder { text-align: center; padding: 32px 10px; color: var(--text-muted); font-size: 13px; display: flex; flex-direction: column; gap: 8px; align-items: center; }
.placeholder.small { padding: 16px 8px; font-size: 12px; }
.placeholder-icon { font-size: 32px; }
.card-body { display: flex; flex-direction: column; gap: 14px; }
.product-hero { display: flex; gap: 14px; align-items: center; }
.product-thumb { width: 92px; height: 116px; border-radius: var(--radius-md); object-fit: cover; border: 2px solid var(--primary-light); background: #f8fafc; }
.product-details { flex: 1; display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.product-name { font-size: 16px; font-weight: 800; line-height: 1.3; }
.product-sku { font-size: 11px; color: #16a34a; font-family: ui-monospace, monospace; background: var(--primary-light); padding: 2px 6px; border-radius: 4px; width: fit-content; font-weight: 700; }
.product-price { font-size: 24px; font-weight: 800; color: var(--primary-dark); }
.candidates { display: flex; flex-direction: column; gap: 6px; background: var(--amber-soft); border: 1px solid #fcd34d; border-radius: var(--radius-md); padding: 10px 12px; }
.candidates-title { font-size: 11px; font-weight: 800; color: #92400e; text-transform: uppercase; letter-spacing: 0.5px; }
.candidate-row { display: flex; justify-content: space-between; font-size: 12px; color: #78350f; font-weight: 600; }
.stock-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.stock-metric { background: var(--primary-subtle); border: 1px solid var(--border-color); border-radius: var(--radius-md); padding: 10px 14px; }
.metric-title { font-size: 11px; color: var(--text-muted); margin-bottom: 4px; font-weight: 600; }
.metric-val { font-size: 16px; font-weight: 800; color: var(--primary-dark); }
.metric-val.small { font-size: 13px; }
.metric-val.warn { color: #b45309; }
.action-btn {
width: 100%; padding: 12px; border: none; border-radius: var(--radius-md);
background: linear-gradient(135deg, #22c55e, #16a34a); color: white;
font-weight: 800; font-size: 14px; cursor: pointer; font-family: inherit;
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
box-shadow: 0 4px 14px rgba(34, 197, 94, 0.3);
}
.action-btn:hover { transform: translateY(-1px); }
.match-box { width: 100%; min-height: 150px; background: var(--night); border-radius: var(--radius-md); border: 1px solid var(--border-color); display: flex; align-items: center; justify-content: center; overflow: hidden; }
.match-box img { width: 100%; height: 100%; object-fit: contain; }
.match-placeholder { color: #94a3b8; font-size: 12px; padding: 20px; text-align: center; }
.inview-list { display: flex; flex-direction: column; gap: 8px; max-height: 320px; overflow-y: auto; }
.inview-row { display: flex; align-items: center; gap: 10px; padding: 8px; border-radius: var(--radius-md); cursor: pointer; }
.inview-row:hover { background: var(--primary-subtle); }
.inview-thumb { width: 34px; height: 44px; border-radius: 6px; object-fit: cover; border: 1px solid var(--border-color); background: #f8fafc; }
.inview-text { flex: 1; min-width: 0; }
.inview-name { font-size: 12px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.inview-meta { font-size: 11px; color: var(--text-muted); }
.inview-facings { font-size: 12px; font-weight: 800; color: var(--primary-dark); }
.status-badge { font-size: 11px; padding: 3px 10px; border-radius: 999px; font-weight: 700; text-transform: none; letter-spacing: 0; }
.badge-neutral { background: var(--slate-soft); color: var(--text-muted); }
.badge-success { background: var(--primary-light); color: var(--primary-dark); }
.badge-warning { background: var(--amber-soft); color: #b45309; }
.badge-danger { background: var(--danger-soft); color: #b91c1c; }
.modal-backdrop { display: none; position: fixed; inset: 0; background: rgba(15, 23, 42, 0.6); backdrop-filter: blur(4px); z-index: 1000; align-items: center; justify-content: center; }
.modal-backdrop.open { display: flex; }
.modal-window { background: #fff; border-radius: 20px; width: 90%; max-width: 600px; padding: 22px; display: flex; flex-direction: column; gap: 14px; }
.modal-header { display: flex; justify-content: space-between; align-items: center; }
.modal-title { font-size: 17px; font-weight: 800; color: #14532d; }
.close-btn { background: none; border: none; font-size: 24px; color: var(--text-muted); cursor: pointer; }
.video-box { width: 100%; height: 340px; background: #000; border-radius: var(--radius-md); overflow: hidden; }
.video-box video { width: 100%; height: 100%; object-fit: cover; }
@media (max-width: 1024px) {
main { grid-template-columns: 1fr; padding: 16px; }
.stage { height: 56vh; min-height: 320px; }
}
.ocr-section { border-top: 1px solid var(--border-color); padding-top: 12px; display: flex; flex-direction: column; gap: 8px; }
.ocr-header { display: flex; justify-content: space-between; align-items: center; font-size: 11px; font-weight: 800; color: #0369a1; text-transform: uppercase; letter-spacing: 0.5px; }
.recon-chip { font-size: 10px; font-weight: 800; padding: 2px 8px; border-radius: 999px; text-transform: none; letter-spacing: 0; }
.recon-chip[data-state="confirmed"] { background: var(--primary-light); color: var(--primary-dark); }
.recon-chip[data-state="mismatch"] { background: var(--danger-soft); color: #b91c1c; }
.recon-chip[data-state="unverified"], .recon-chip[data-state="no_tag"] { background: var(--slate-soft); color: var(--text-muted); }
.recon-chip[data-state="tag_only"] { background: #e0f2fe; color: #0369a1; }
.ocr-rows { display: flex; flex-direction: column; gap: 4px; }
.ocr-row { display: flex; justify-content: space-between; gap: 10px; font-size: 12px; }
.ocr-row .ocr-key { color: var(--text-muted); font-weight: 600; }
.ocr-row .ocr-val { color: var(--text-main); font-weight: 700; text-align: right; }
.ocr-row .ocr-val.bad { color: #b91c1c; }
.compliance-strip {
position: absolute; top: 14px; left: 14px; right: 14px;
background: rgba(255, 255, 255, 0.94); backdrop-filter: blur(8px);
border: 1px solid #fca5a5; border-radius: 12px; padding: 8px 14px;
font-size: 12px; font-weight: 700; color: #b91c1c; display: flex; gap: 12px; flex-wrap: wrap;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); pointer-events: none;
}
.compliance-strip[data-kind="hint"] { border-color: var(--amber); color: #92400e; }
.compliance-strip .compliance-item { display: inline-flex; align-items: center; gap: 6px; }