canon extract batt onb
CANON EXTRACT — Battery Onboarding Operator (rechaj-inventory-app)
Section titled “CANON EXTRACT — Battery Onboarding Operator (rechaj-inventory-app)”Source of truth: /Users/Ridwan/Pictures/Claude/Rechaj 2.0/rechaj-design-system/products/rechaj-inventory-app/flows/battery-onboarding-operator/index.html (3531 lines; CSS lines 9–1493, markup 1495–2596, JS 2597–3529). All CSS below is verbatim.
1. Tokens
Section titled “1. Tokens”Verbatim :root block (lines 16–48):
:root { --g-primary: #00D964; --brand: #01E46F; --g-dark: #05853E; --g-3: #DCFBE2; --g-2: #F4FBF5;
--grey12: #202020; --grey11: #646464; --grey10: #838383; --grey9: #8d8d8d; --grey6: #d9d9d9; --grey5: #e0e0e0; --grey4: #e8e8e8; --grey3: #f0f0f0; --grey2: #f9f9f9; --grey1: #fcfcfc; --white: #ffffff; --bg: #fcfcfc; --text-main: #0E0400;
--red9: #e32627; --red6: #ffbbb2; --red3: #ffebe8; --red2: #fff7f6;
--brand-dark: #0F1D13;
--sh-xs: 0 1px 2px rgba(16,24,40,0.05); --sh-card: 2.08px 2.78px 16px 0px rgba(193,197,210,0.03); --sh-sheet: 0 -8px 40px rgba(0,0,0,0.12); --transition: 0.18s ease;}Hard-coded non-token colors used repeatedly in the file (a de-facto tonal palette; not in :root but canonical usage):
- Amber tint pair:
#FEF3C7bg /#B45309glyph - Red tint pair:
#FEE2E2bg /#B91C1Cglyph (destructive button solid#B91C1C, hover#991B1B, disabled#FECACA) - Blue tint pair:
#E0F2FEbg /#075985glyph; alt bluergba(30,64,175,0.08–0.12)bg /#1E40AFglyph (Path B) - Toast info:
#e6f4fe/#0d74ce; toast success#dcfbe2/#05853e - Page/tab background:
#F5F6F8(.tab-screen); prototype backdrop#0E0F12 - Button hover green:
#00C45B - Focus rings:
rgba(1,228,111,0.12)(brand) andrgba(227,38,39,0.12)(error)
Font: 'General Sans', -apple-system, 'SF Pro Display', system-ui, sans-serif loaded from Fontshare at weights 400,500,600,700. Viewport meta: width=390,initial-scale=1,viewport-fit=cover.
Reset (verbatim, lines 50–54):
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; -webkit-font-smoothing: antialiased;}2. Shell
Section titled “2. Shell”Dimensions & structure
Section titled “Dimensions & structure”- Device: 390 × 844, radius 32px, on dark
#0E0F12full-viewport backdrop, centered flex. Device-only — no external chrome (global DESIGN.md hard rule). - Screens are absolutely-stacked layers inside
.phone; exactly one has.active(display:flex). Navigation = swapping.active. - A second
<script>scales.phonedown withtransform: scale()when the window is smaller than 390×844 (never scrolls the body).
ASCII sketch (tab screen)
Section titled “ASCII sketch (tab screen)”.phone (390x844, r32, overflow hidden)└── .screen.tab-screen.active (absolute inset:0, flex column, bg #F5F6F8) ├── .sb (status bar, 44px, fixed at top of column) ├── header.rsm-hero (dashboard only: brand-dark hero, flex-shrink 0) │ — or — ├── header.rsm-page-head (list/detail pages: centered 56px title row) ├── [.bo-tabs] (optional segmented control, flex-shrink 0) ├── .rsm-body (flex:1, overflow-y auto ← THE ONLY SCROLLER) ├── [.bo-detail-cta] (detail pages: sticky CTA strip, flex-shrink 0) ├── nav.rsm-nav (bottom tab bar, flex-shrink 0 — omitted on detail screens) └── .home-ind (absolute, bottom 8px)[.bo-fab] (absolute right:16 bottom:96, sessions list only).bo-sheet-overlay (absolute inset:0, z 50 — bottom sheets).alert-toast (absolute top:56px center, z 80)Scroll model: status bar, hero/page-head, segmented tabs, CTA strip, and bottom nav are all flex-shrink: 0 and fixed; only .rsm-body scrolls (scrollbar hidden). Sheet bodies (.bo-sheet-body) scroll independently inside their sheet, capped at max-height: 86% of the device.
Verbatim shell CSS
Section titled “Verbatim shell CSS”html, body { width: 100%; height: 100%; font-family: 'General Sans', -apple-system, 'SF Pro Display', system-ui, sans-serif; background: #0E0F12; display: flex; align-items: center; justify-content: center; overflow: hidden;}
.phone { width: 390px; height: 844px; background: var(--bg); border-radius: 32px; overflow: hidden; position: relative; flex-shrink: 0; box-shadow: 0 50px 130px rgba(0,0,0,0.7), inset 0 0 0 1px rgba(255,255,255,0.06);}
.screen { position: absolute; inset: 0; display: none; flex-direction: column; overflow: hidden;}.screen.active { display: flex; }.screen-enter { animation: screenEnter 0.3s ease-out; }@keyframes screenEnter { from { opacity: 0; transform: translateX(18px); } to { opacity: 1; transform: translateX(0); }}
.tab-screen { background: #F5F6F8; display: none; flex-direction: column; position: absolute; inset: 0; overflow: hidden;}.tab-screen.active { display: flex; }.rsm-body { flex: 1; overflow-y: auto; padding: 0 16px 24px; display: flex; flex-direction: column; gap: 18px;}.rsm-body::-webkit-scrollbar { display: none; }.rsm-body > * { flex-shrink: 0; }Status bar + home indicator (verbatim):
.sb { height: 44px; padding: 14px 24px 0; display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; position: relative; z-index: 2;}.sb-time { font-size: 15px; font-weight: 700; letter-spacing: -0.4px; color: var(--grey12);}.sb-icons { display: flex; align-items: center; gap: 5px; }.sb-icons svg { display: block; }.sb--dark .sb-time, .sb--dark .sb-icons svg path,.sb--dark .sb-icons svg rect { color: var(--white); fill: var(--white); }.sb--dark .sb-icons svg [data-faded] { opacity: 0.35; }
.home-ind { position: absolute; bottom: 8px; left: 50%; width: 134px; height: 5px; border-radius: 100px; transform: translateX(-50%); background: var(--text-main); z-index: 3;}.home-ind--light { background: rgba(255,255,255,0.5); }Status-bar rules: time is always 9:41; icon glyphs are inline SVGs recolored #202020 on light screens and #FFFFFF (via .sb--dark markup with white fills) on dark screens. On dark overlay screens (splash/camera) the .sb is absolutely positioned over the content.
Phone viewport-fit script (verbatim, second <script>):
(function () { var el = document.querySelector('.phone'); if (!el) return; function fit() { var sw = window.innerWidth; var sh = window.innerHeight; var scale = Math.min(sw / 390, sh / 844, 1); if (scale < 1) { el.style.transform = 'scale(' + scale + ')'; el.style.transformOrigin = 'top center'; el.style.marginTop = '0'; document.body.style.alignItems = 'flex-start'; document.body.style.paddingTop = Math.max(0, (sh - 844 * scale) / 2) + 'px'; document.body.style.minHeight = (844 * scale) + 'px'; } else { el.style.transform = ''; el.style.transformOrigin = ''; el.style.marginTop = ''; document.body.style.alignItems = ''; document.body.style.paddingTop = ''; document.body.style.minHeight = ''; } } fit(); window.addEventListener('resize', fit);})();3. Atoms
Section titled “3. Atoms”3.1 Bottom nav (.rsm-nav / .rsm-nav-tab)
Section titled “3.1 Bottom nav (.rsm-nav / .rsm-nav-tab)”.rsm-nav { display: grid; grid-template-columns: repeat(4, 1fr); background: var(--white); border-top: 1px solid var(--grey4); padding: 6px 8px; flex-shrink: 0; position: relative; z-index: 1;}.rsm-nav-tab { display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 6px 2px 4px; background: transparent; border: 0; cursor: pointer; font-family: inherit; font-size: 10px; font-weight: 500; color: var(--grey10); -webkit-appearance: none; appearance: none; transition: color var(--transition); min-width: 0;}.rsm-nav-tab span { white-space: nowrap; }.rsm-nav-tab svg { width: 22px; height: 22px; }.rsm-nav-tab.is-active { color: var(--g-dark); font-weight: 600; }.rsm-nav-tab.is-active svg { color: var(--g-dark); }Markup (from #s-dashboard):
<nav class="rsm-nav" aria-label="Primary"> <button class="rsm-nav-tab is-active" type="button" onclick="goTo('s-dashboard')"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/> </svg> <span>Home</span> </button> <button class="rsm-nav-tab" type="button" onclick="goTo('s-sessions')">…<span>Sessions</span></button> <button class="rsm-nav-tab" type="button" onclick="goTo('s-quarantine')">…<span>Quarantine</span></button> <button class="rsm-nav-tab" type="button" onclick="goTo('s-profile')">…<span>Profile</span></button></nav>Usage: 4 fixed tabs (Home / Sessions / Quarantine / Profile), 22px stroke icons + 10px labels. Active tab = --g-dark text + 600 weight (state set statically per screen — each tab screen has its own copy of the nav with is-active on itself). No badges on nav items in this file. Detail screens omit the nav entirely (Kit Assembler / Production QA convention; the back chevron is the only exit).
3.2 Topbar / header — two variants
Section titled “3.2 Topbar / header — two variants”A. Dashboard hero (.rsm-hero) — brand-dark banner with avatar + name + role pill + date, notification bell, faded brand-mark background art:
.rsm-hero { position: relative; background: var(--brand-dark); padding: 4px 16px 64px; flex-shrink: 0;}.rsm-hero-bg { position: absolute; top: 0; right: 0; width: 220px; height: 100%; pointer-events: none; opacity: 0.18; display: flex; align-items: center; justify-content: flex-end;}.rsm-hero-bg svg { width: 100%; height: auto; }.rsm-hero-row { position: relative; display: flex; align-items: center; justify-content: space-between; padding: 8px 0 16px;}.rsm-hero-id { display: flex; align-items: center; gap: 12px; min-width: 0; }.rsm-hero-avatar { width: 44px; height: 44px; border-radius: 50%; background: var(--brand); display: flex; align-items: center; justify-content: center; font-size: 16px; font-weight: 700; color: var(--text-main); letter-spacing: -0.4px; flex-shrink: 0;}.rsm-hero-meta { min-width: 0; }.rsm-hero-name { font-size: 17px; font-weight: 700; color: var(--white); letter-spacing: -0.3px; line-height: 22px;}.rsm-hero-date { font-size: 12px; font-weight: 400; color: rgba(255,255,255,0.6); line-height: 16px; margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;}.rsm-hero-role { display: inline-block; font-size: 10.5px; font-weight: 600; padding: 2px 8px; border-radius: 100px; background: rgba(1,228,111,0.16); color: var(--g-primary); letter-spacing: 0.02em; margin: 4px 0 2px;}.rsm-hero-bell { position: relative; width: 40px; height: 40px; border-radius: 50%; background: var(--white); border: 0; color: var(--text-main); display: flex; align-items: center; justify-content: center; cursor: pointer; -webkit-appearance: none; appearance: none; box-shadow: var(--sh-card);}.rsm-hero-bell svg { width: 18px; height: 18px; }.rsm-hero-bell-dot { position: absolute; top: 8px; right: 9px; width: 8px; height: 8px; border-radius: 50%; background: var(--red9); border: 2px solid var(--white);}Markup:
<header class="rsm-hero"> <div class="sb sb--dark">…</div> <div class="rsm-hero-bg" aria-hidden="true"><svg viewBox="0 0 220 220" fill="none">…brand mark, #01E46F…</svg></div> <div class="rsm-hero-row"> <div class="rsm-hero-id"> <div class="rsm-hero-avatar">TA</div> <div class="rsm-hero-meta"> <p class="rsm-hero-name">Tobiloba Adekoya</p> <span class="rsm-hero-role">BATTERY ONBOARDING</span> <p class="rsm-hero-date">Tue, 21 May 2026</p> </div> </div> <button class="rsm-hero-bell" type="button" aria-label="Notifications"> <svg …bell…></svg> <span class="rsm-hero-bell-dot" aria-hidden="true"></span> </button> </div></header>Usage: dashboard only. padding-bottom: 64px reserves room for the stats cards which pull up with margin: -52px 16px 0. Status bar inside hero uses .sb--dark.
B. Page head (.rsm-page-head) — see 3.3.
There is no search field anywhere in this app; the user pill/avatar exists only in the hero and the Profile card.
3.3 Page head (title + sub + actions row)
Section titled “3.3 Page head (title + sub + actions row)”.rsm-page-head { background: transparent; padding: 0 16px 16px; flex-shrink: 0;}.rsm-page-head-row { position: relative; height: 56px; display: flex; align-items: center; justify-content: center; padding: 0 20px; margin: 0 -16px;}.rsm-page-title { font-size: 20px; font-weight: 600; color: var(--grey12); letter-spacing: -0.3px; flex: 1; text-align: center;}Back chevron used on detail heads (.bo-back, absolutely positioned inside the row):
.bo-back { position: absolute; left: 20px; top: 50%; transform: translateY(-50%); width: 40px; height: 40px; border-radius: 50%; background: var(--white); color: var(--grey12); border: 1px solid var(--grey6); cursor: pointer; display: flex; align-items: center; justify-content: center; -webkit-appearance: none; appearance: none; transition: background 0.15s ease;}.bo-back:hover { background: var(--grey2); }.bo-back svg { width: 20px; height: 20px; }Markup:
<header class="rsm-page-head"> <div class="rsm-page-head-row"> <button class="bo-back" type="button" aria-label="Back to sessions" onclick="goTo('s-sessions')"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> <polyline points="15 18 9 12 15 6"/> </svg> </button> <h1 class="rsm-page-title" id="bo-session-title">Onboarding session</h1> </div></header>Usage: title is centered, 20px/600. Detail titles are short noun phrases (“Onboarding session”, “Quarantine item”); the record ID moves into the first panel’s meta line, never into the head. .bo-back mirrors .rsm-page-back from Kit Assembler — outlined circle, no shadow, 40×40, flush left at 20px.
Section heads inside the body (title + meta pairs between card groups):
.rsm-section-head { display: flex; align-items: center; justify-content: space-between; padding: 4px 0;}.rsm-section-head + * { margin-top: -10px; }.rsm-section-title { font-size: 15px; font-weight: 600; color: var(--grey12); letter-spacing: -0.2px;}.rsm-section-meta { font-size: 12px; font-weight: 500; color: var(--grey11);}<div class="rsm-section-head"> <h2 class="rsm-section-title">Today at a glance</h2> <span class="rsm-section-meta">4 metrics</span></div>3.4 Tabs & segmented controls
Section titled “3.4 Tabs & segmented controls”Only one variant exists: .bo-tabs pill segmented control (grey track, white raised active pill, count badges). Named copy of PM’s .pm-tabs. Used for Sessions list filtering (Active / Completed / Failed). No line tabs anywhere in this file.
.bo-tabs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px; padding: 4px; background: var(--grey2); border-radius: 12px; margin: 0 16px 12px;}.bo-tab { display: flex; align-items: center; justify-content: center; gap: 6px; padding: 9px 6px; background: transparent; border: 0; border-radius: 8px; font-family: inherit; font-size: 13px; font-weight: 500; color: var(--grey11); cursor: pointer; -webkit-appearance: none; appearance: none; transition: background 0.15s ease, color 0.15s ease;}.bo-tab:hover { color: var(--grey12); }.bo-tab[aria-pressed="true"] { background: var(--white); color: var(--g-dark); font-weight: 600; box-shadow: 0 1px 2px rgba(0,0,0,0.06);}.bo-tab-count { display: inline-flex; align-items: center; justify-content: center; min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px; background: var(--grey3); color: var(--grey11); font-size: 11px; font-weight: 600; line-height: 1; font-variant-numeric: tabular-nums;}.bo-tab[aria-pressed="true"] .bo-tab-count { background: var(--g-3); color: var(--g-dark); }[data-sessions-pane][hidden] { display: none !important; }Markup:
<div class="bo-tabs" role="tablist" aria-label="Session filters"> <button class="bo-tab" type="button" role="tab" aria-pressed="true" data-sessions-tab="active" onclick="setSessionsTab(this)">Active <span class="bo-tab-count" id="bo-sess-count-active">0</span></button> <button class="bo-tab" type="button" role="tab" aria-pressed="false" data-sessions-tab="completed" onclick="setSessionsTab(this)">Completed <span class="bo-tab-count" id="bo-sess-count-completed">0</span></button> <button class="bo-tab" type="button" role="tab" aria-pressed="false" data-sessions-tab="failed" onclick="setSessionsTab(this)">Failed <span class="bo-tab-count" id="bo-sess-count-failed">0</span></button></div>State lives on aria-pressed, panes are sibling [data-sessions-pane] divs toggled via hidden (the !important rule beats their inline display:flex). Active pill = white bg + --g-dark text + 600 + 1px shadow. Counts flip from grey to green tint when active.
3.5 Table / list system — mobile list-card rows
Section titled “3.5 Table / list system — mobile list-card rows”.bo-session-row — the canonical queue/list card (3-col grid: 40px tone icon / meta / right rail):
.bo-session-row { display: grid; grid-template-columns: 40px 1fr auto; gap: 12px; align-items: center; width: 100%; background: var(--white); border: 0; border-radius: 14px; box-shadow: var(--sh-card); padding: 12px 14px; text-align: left; font-family: inherit; cursor: pointer; -webkit-appearance: none; appearance: none; transition: filter 0.15s ease, transform 0.1s ease;}.bo-session-row:hover { filter: brightness(0.99); }.bo-session-row:active { transform: scale(0.998); }.bo-session-ic { width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; background: var(--grey2); color: var(--grey10);}.bo-session-ic[data-tone="amber"] { background: #FEF3C7; color: #B45309; }.bo-session-ic[data-tone="green"] { background: var(--g-2); color: var(--g-dark); }.bo-session-ic[data-tone="red"] { background: #FEE2E2; color: #B91C1C; }.bo-session-ic svg { width: 18px; height: 18px; }.bo-session-meta { min-width: 0; display: flex; flex-direction: column; gap: 3px; }.bo-session-top { display: flex; align-items: center; gap: 8px; }.bo-session-id { font-size: 13.5px; font-weight: 600; color: var(--grey12); letter-spacing: -0.1px; font-variant-numeric: tabular-nums;}.bo-session-pill { display: inline-flex; align-items: center; font-size: 10.5px; font-weight: 600; padding: 2px 8px; border-radius: 100px; letter-spacing: 0.02em; background: var(--grey2); color: var(--grey11);}.bo-session-pill[data-tone="amber"] { background: #FEF3C7; color: #B45309; }.bo-session-pill[data-tone="green"] { background: var(--g-2); color: var(--g-dark); }.bo-session-pill[data-tone="red"] { background: #FEE2E2; color: #B91C1C; }.bo-session-sub { font-size: 12px; font-weight: 500; color: var(--grey10); display: flex; align-items: center; gap: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;}.bo-session-sub .dot { width: 3px; height: 3px; border-radius: 50%; background: var(--grey6); flex-shrink: 0;}.bo-session-right { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; flex-shrink: 0;}.bo-session-age { font-size: 11.5px; font-weight: 500; color: var(--grey10); font-variant-numeric: tabular-nums;}.bo-session-chev { color: var(--grey9); }.bo-session-chev svg { width: 14px; height: 14px; display: block; }Markup (JS template sessionRowHtml):
<button class="bo-session-row" type="button" onclick="openOnbSession('OB-210526-008')"> <span class="bo-session-ic" data-tone="amber" aria-hidden="true"><svg …clock…></svg></span> <div class="bo-session-meta"> <div class="bo-session-top"> <span class="bo-session-id">OB-210526-008</span> <span class="bo-session-pill" data-tone="amber">In progress</span> </div> <div class="bo-session-sub"> <span>SN-44-091246</span> <span class="dot"></span> <span>BMS not read</span> <span class="dot"></span> <span>Path A</span> </div> </div> <div class="bo-session-right"> <span class="bo-session-age">09:48</span> <span class="bo-session-chev" aria-hidden="true"><svg …chevron-right…></svg></span> </div></button>.bo-qtn-row — the quarantine variant (same skeleton, reason-tinted icon, right rail = route pill + age):
.bo-qtn-row { display: grid; grid-template-columns: 40px 1fr auto; gap: 12px; align-items: center; width: 100%; background: var(--white); border: 0; border-radius: 14px; box-shadow: var(--sh-card); padding: 12px 14px; text-align: left; font-family: inherit; cursor: pointer; -webkit-appearance: none; appearance: none; transition: filter 0.15s ease, transform 0.1s ease;}.bo-qtn-row:hover { filter: brightness(0.99); }.bo-qtn-row:active { transform: scale(0.998); }.bo-qtn-ic { width: 40px; height: 40px; border-radius: 50%; background: #FEE2E2; color: #B91C1C; display: flex; align-items: center; justify-content: center; flex-shrink: 0;}.bo-qtn-ic[data-reason="bms_collision"] { background: rgba(180, 83, 9, 0.12); color: #B45309; }.bo-qtn-ic[data-reason="serial_collision"] { background: rgba(30, 64, 175, 0.10); color: #1E40AF; }.bo-qtn-ic[data-reason="lost_identity"] { background: var(--grey3); color: var(--grey11); }.bo-qtn-ic svg { width: 18px; height: 18px; }.bo-qtn-text { min-width: 0; }.bo-qtn-id { font-size: 13px; font-weight: 600; color: var(--grey12); letter-spacing: -0.05em; font-variant-numeric: tabular-nums; display: flex; align-items: center; gap: 6px;}.bo-qtn-sub { font-size: 11.5px; font-weight: 500; color: var(--grey10); margin-top: 3px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;}.bo-qtn-right { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; flex-shrink: 0;}.bo-qtn-route { display: inline-flex; align-items: center; font-size: 10.5px; font-weight: 600; padding: 2px 8px; border-radius: 100px; background: var(--grey2); color: var(--grey11); letter-spacing: 0.02em; max-width: 138px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;}.bo-qtn-age { font-size: 11.5px; font-weight: 500; color: var(--grey10); font-variant-numeric: tabular-nums;}Markup (JS template qtnRowHtml):
<button class="bo-qtn-row" type="button" onclick="openOnbQuarantineItem('QTN-OB-210526-001')"> <span class="bo-qtn-ic" data-reason="bms_unresponsive" aria-hidden="true">…svg…</span> <div class="bo-qtn-text"> <p class="bo-qtn-id">QTN-OB-210526-001 · BMS unresponsive</p> <p class="bo-qtn-sub">SN-44-091240 · BMS unresponsive — no CAN response within 8s…</p> </div> <div class="bo-qtn-right"> <span class="bo-qtn-route">Routed to Procurement Manager</span> <span class="bo-qtn-age">8h</span> </div></button>.bo-activity — compact activity list (single white card containing rows with inset hairline dividers):
.bo-activity { background: var(--white); border-radius: 16px; box-shadow: var(--sh-card); padding: 4px 0;}.bo-activity-row { display: flex; align-items: flex-start; gap: 12px; padding: 12px 16px; position: relative;}.bo-activity-row + .bo-activity-row::before { content: ''; position: absolute; left: 56px; right: 16px; top: 0; border-top: 1px solid var(--grey3);}.bo-activity-ic { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0;}.bo-activity-ic svg { width: 14px; height: 14px; }.bo-activity-ic--green { background: var(--g-2); color: var(--g-dark); }.bo-activity-ic--red { background: #FEE2E2; color: #B91C1C; }.bo-activity-body { flex: 1; min-width: 0; }.bo-activity-title { font-size: 13px; font-weight: 600; color: var(--grey12); letter-spacing: -0.05px; line-height: 17px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;}.bo-activity-meta { font-size: 11.5px; font-weight: 400; color: var(--grey11); line-height: 15px; margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;}.bo-activity-time { font-size: 11px; color: var(--grey10); flex-shrink: 0; padding-top: 2px; font-variant-numeric: tabular-nums;}<div class="bo-activity"> <div class="bo-activity-row"> <span class="bo-activity-ic bo-activity-ic--green" aria-hidden="true"><svg …check…></svg></span> <div class="bo-activity-body"> <p class="bo-activity-title">Onboarded · SN-44-091245 → BMS-SE-7821A</p> <p class="bo-activity-meta">Path A · label scan</p> </div> <span class="bo-activity-time">09:34</span> </div> …</div>Divider convention: inset hairline border-top: 1px solid var(--grey3) starting after the icon column (left offset 56px, right 16px) — same pattern in .rsm-profile-row. No pagination or list footer exists (mobile lists are short + tab-filtered). List rows are <button> elements; whole row is the tap target.
3.6 Buttons
Section titled “3.6 Buttons”.btn-primary { display: flex; align-items: center; justify-content: center; width: 100%; height: 48px; background: var(--brand); border: 1.5px solid var(--brand); border-radius: 8px; font-family: inherit; font-size: 16px; font-weight: 600; color: var(--text-main); letter-spacing: -0.2px; cursor: pointer; transition: background var(--transition), border-color var(--transition), transform 0.1s; -webkit-appearance: none; appearance: none;}.btn-primary:hover { background: #00C45B; border-color: #00C45B; }.btn-primary:active { transform: scale(0.98); }.btn-primary:disabled, .btn-primary.is-disabled { background: var(--g-3); border-color: var(--g-3); color: rgba(17,17,17,0.45); cursor: not-allowed; pointer-events: none;}.btn-primary--lg { height: 52px; font-size: 16px; }.btn-secondary { display: flex; align-items: center; justify-content: center; width: 100%; height: 48px; background: transparent; border: 1.5px solid var(--grey5); border-radius: 8px; font-family: inherit; font-size: 16px; font-weight: 600; color: var(--grey12); letter-spacing: -0.2px; cursor: pointer; transition: background var(--transition), border-color var(--transition), color var(--transition), transform 0.1s; -webkit-appearance: none; appearance: none;}.btn-secondary:hover { background: var(--grey2); border-color: var(--grey9); }.btn-secondary:active { transform: scale(0.98); }.btn-secondary--lg { height: 52px; font-size: 16px; }Destructive modifier (stacked on .btn-primary):
.btn-primary--red { background: #B91C1C; border-color: #B91C1C; color: var(--white);}.btn-primary--red:hover { background: #991B1B; border-color: #991B1B; }.btn-primary--red:disabled,.btn-primary--red.is-disabled { background: #FECACA; border-color: #FECACA; color: rgba(255,255,255,0.7);}FAB:
.bo-fab { position: absolute; right: 16px; bottom: 96px; width: 56px; height: 56px; border-radius: 50%; background: var(--g-primary); color: var(--text-main); border: 0; cursor: pointer; display: flex; align-items: center; justify-content: center; -webkit-appearance: none; appearance: none; box-shadow: 0 6px 16px rgba(0,0,0,0.18), 0 2px 4px rgba(0,0,0,0.10); z-index: 5; transition: transform 0.15s ease, box-shadow 0.15s ease;}.bo-fab:hover { transform: translateY(-1px); box-shadow: 0 8px 20px rgba(0,0,0,0.22), 0 2px 4px rgba(0,0,0,0.10); }.bo-fab:active { transform: translateY(0); }.bo-fab svg { width: 24px; height: 24px; }Ghost/utility grey button (.bo-sim-btn, viewfinder simulation rows):
.bo-sim-list { display: flex; flex-direction: column; gap: 8px; }.bo-sim-btn { display: flex; align-items: center; justify-content: space-between; width: 100%; background: var(--grey2); border: 0; border-radius: 12px; padding: 12px 14px; font-family: inherit; font-size: 13.5px; font-weight: 600; color: var(--grey12); letter-spacing: -0.05em; cursor: pointer; -webkit-appearance: none; appearance: none; transition: filter 0.15s ease;}.bo-sim-btn:hover { filter: brightness(0.98); }.bo-sim-btn[data-tone="red"] { background: #FFF7F6; color: #B91C1C; }.bo-sim-btn[data-tone="amber"] { background: #FFFBEB; color: #B45309; }.bo-sim-btn-chev { color: currentColor; opacity: 0.6; }.bo-sim-btn-chev svg { width: 14px; height: 14px; display: block; }Markup:
<button class="btn-primary btn-primary--lg" onclick="goTo('s-welcome')">Get started</button><button class="btn-primary btn-primary--lg btn-primary--red" type="button" onclick="confirmFailure('bms_unresponsive')">Confirm + route to Quarantine</button><button class="btn-secondary btn-secondary--lg" type="button" onclick="closePathPickerSheet()">Cancel</button><button class="bo-fab" type="button" onclick="openPathPickerSheet()" aria-label="Start new onboarding"><svg …plus…></svg></button>Spec summary: full-width buttons, 48px base / 52px --lg, radius 8px, 16px/600 label, brand-green #01E46F with dark --text-main text (never white-on-green — hard rule; the ONLY white-text button is the destructive red). Icon buttons are circular: 40px hero bell (white, shadow), 40px .bo-back (white, 1px grey6 outline), 36px .ka-camera-back (translucent white on black), 56px FAB (solid green). Press feedback: transform: scale(0.98).
3.7 Badges & status pills
Section titled “3.7 Badges & status pills”Every pill in the file is border-radius: 100px, ~10.5–12.5px, weight 600. Variants:
| Class | Size/weight | Padding | Default colors | Tones |
|---|---|---|---|---|
.bo-session-pill (status) |
10.5px/600, ls 0.02em | 2px 8px | grey2/grey11 | amber #FEF3C7/#B45309, green --g-2/--g-dark, red #FEE2E2/#B91C1C via data-tone |
.bo-qtn-route |
10.5px/600, ls 0.02em | 2px 8px | grey2/grey11, max-width 138px + ellipsis | — |
.rsm-hero-role |
10.5px/600, ls 0.02em | 2px 8px | rgba(1,228,111,0.16)/--g-primary |
— |
.rsm-profile-role-pill |
11px/600, ls 0.02em | 3px 10px | --g-2/--g-dark |
— |
.bo-path-chip |
12.5px/600, ls -0.05em, gap 8 | 6px 12px | --g-2/--g-dark (+14px icon) |
[data-path="B"]: rgba(30,64,175,0.08)/#1E40AF |
.bo-log-evt-id (ID chip) |
10.5px/600, tabular | 1px 6px | grey2/grey10 | — |
.bo-routed (read-only routing) |
12.5px/600, ls -0.02em, gap 8 | 8px 14px | grey2/grey11 (+14px icon) | — |
.bo-tab-count (count) |
11px/600, tabular | 0 5px, min-w 18, h 18, r 9 | grey3/grey11 | active: --g-3/--g-dark |
Path chip + routed badge CSS (verbatim):
.bo-path-chip { display: inline-flex; align-items: center; gap: 8px; padding: 6px 12px; border-radius: 100px; background: var(--g-2); color: var(--g-dark); font-size: 12.5px; font-weight: 600; letter-spacing: -0.05em;}.bo-path-chip[data-path="B"] { background: rgba(30, 64, 175, 0.08); color: #1E40AF; }.bo-path-chip svg { width: 14px; height: 14px; }.bo-path-note { font-size: 12px; font-weight: 500; color: var(--grey10); line-height: 16px; margin-top: 8px;}.bo-routed { display: inline-flex; align-items: center; gap: 8px; padding: 8px 14px; border-radius: 100px; background: var(--grey2); color: var(--grey11); font-size: 12.5px; font-weight: 600; letter-spacing: -0.02em;}.bo-routed svg { width: 14px; height: 14px; }.bo-log-evt-id { display: inline-block; font-size: 10.5px; font-weight: 600; padding: 1px 6px; border-radius: 100px; background: var(--grey2); color: var(--grey10); margin-left: 6px; font-variant-numeric: tabular-nums; letter-spacing: 0.02em;}Markup:
<span class="bo-session-pill" data-tone="green">Mapped</span><span class="bo-path-chip" data-path="A"><svg …>…</svg> Path A · Label scan</span><span class="bo-routed"><svg …arrow-right…></svg> Routed to Procurement Manager</span>Status-dot pattern: the only dots are the notification .rsm-hero-bell-dot (8px red circle, 2px white ring, top-right of bell) and the 3px middot separator .bo-session-sub .dot. Status is otherwise always conveyed by tonal circular icon + pill, never bare dots.
Note: this file’s pills predate/diverge from the global .ds-badge spec (12px/500/6px radius) — here mobile status pills are fully round, 600 weight, one instance uppercase (see §7).
3.8 KPI / stat cards
Section titled “3.8 KPI / stat cards”.rsm-stat — the 2-up header stats overlapping the hero:
.rsm-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin: -52px 16px 0; position: relative; z-index: 2;}.rsm-stat { background: var(--white); border-radius: 16px; box-shadow: var(--sh-card); padding: 14px; display: flex; align-items: center; gap: 10px; min-width: 0;}.rsm-stat-ic { width: 36px; height: 36px; border-radius: 50%; background: var(--g-2); display: flex; align-items: center; justify-content: center; flex-shrink: 0;}.rsm-stat-ic svg { width: 18px; height: 18px; color: var(--g-primary); }.rsm-stat-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }.rsm-stat-label { font-size: 12px; font-weight: 400; color: var(--grey11); line-height: 16px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;}.rsm-stat-num { font-size: 20px; font-weight: 700; color: var(--grey12); letter-spacing: -0.4px; line-height: 26px; display: flex; align-items: baseline; gap: 2px;}.rsm-stat-num-of { font-size: 13px; font-weight: 500; color: var(--grey10);}.bo-kpi — single-row KPI tiles (icon + label/sub + right-aligned number):
.bo-kpi-list { display: flex; flex-direction: column; gap: 10px;}.bo-kpi { display: flex; align-items: center; gap: 12px; width: 100%; background: var(--white); border: 0; border-radius: 16px; box-shadow: var(--sh-card); padding: 14px; text-align: left; font-family: inherit;}.bo-kpi-ic { width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0;}.bo-kpi-ic svg { width: 18px; height: 18px; }.bo-kpi-ic--amber { background: #FEF3C7; color: #B45309; }.bo-kpi-ic--red { background: #FEE2E2; color: #B91C1C; }.bo-kpi-ic--green { background: var(--g-3); color: var(--g-dark); }.bo-kpi-ic--blue { background: #E0F2FE; color: #075985; }.bo-kpi-ic--grey { background: var(--grey3); color: var(--grey11); }.bo-kpi-text { flex: 1; min-width: 0; }.bo-kpi-label { font-size: 13px; font-weight: 500; color: var(--grey11); letter-spacing: -0.05px; line-height: 18px;}.bo-kpi-sub { font-size: 11.5px; font-weight: 400; color: var(--grey10); line-height: 16px; margin-top: 2px;}.bo-kpi-num { font-size: 20px; font-weight: 700; color: var(--grey12); letter-spacing: -0.4px; line-height: 26px; font-variant-numeric: tabular-nums; flex-shrink: 0;}Markup:
<div class="rsm-stat"> <span class="rsm-stat-ic" aria-hidden="true"><svg …check…></svg></span> <div class="rsm-stat-text"> <p class="rsm-stat-label">Onboarded today</p> <p class="rsm-stat-num">12</p> </div></div>
<div class="bo-kpi"> <span class="bo-kpi-ic bo-kpi-ic--amber" aria-hidden="true"><svg …></svg></span> <div class="bo-kpi-text"> <p class="bo-kpi-label">Awaiting intake</p> <p class="bo-kpi-sub">From Procurement receiving</p> </div> <span class="bo-kpi-num">8</span></div>KPI numbers are 20px/700 tabular (the sanctioned 700 use). The severity color lives ONLY on the 36px tinted circular icon; the card itself stays neutral white. No delta/trend element in this file.
3.9 Cards / panels
Section titled “3.9 Cards / panels”Canonical card spec: white --white surface, radius 16px (14px for tappable list rows, 12px for flat nested cards), padding 14–16px, box-shadow: var(--sh-card), border: 0. NO card anywhere in this file has a border — the sole bordered elements are the .bo-back outlined icon button, the 1.5px-bordered buttons/inputs, the dashed .rt-capture-add tile, and hairline dividers (.rsm-nav border-top, .bo-detail-cta border-top, row ::before separators). Comments in-file restate the rule twice (“white surface, 16px radius, –sh-card, NO border”).
Detail panel with header pattern:
.bo-panel { background: var(--white); border-radius: 16px; box-shadow: var(--sh-card); padding: 16px 16px; display: flex; flex-direction: column; gap: 12px;}.bo-panel-head { display: flex; align-items: center; justify-content: space-between;}.bo-panel-title { font-size: 14px; font-weight: 600; color: var(--grey12); letter-spacing: -0.1px;}.bo-panel-meta { font-size: 12px; font-weight: 500; color: var(--grey11); }<div class="bo-panel"> <div class="bo-panel-head"> <h2 class="bo-panel-title">Battery</h2> <span class="bo-panel-meta">OB-210526-008 · SiaEcosys</span> </div> <div class="bo-kv-list">…</div></div>Alert card (neutral white, tone on icon only):
.bo-alert-list { display: flex; flex-direction: column; gap: 10px;}.bo-alert { display: flex; align-items: center; gap: 12px; width: 100%; background: var(--white); border: 0; border-radius: 16px; padding: 14px; box-shadow: var(--sh-card); text-align: left;}.bo-alert-ic { width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; background: var(--grey2); color: var(--grey11);}.bo-alert-ic svg { width: 18px; height: 18px; }.bo-alert--amber .bo-alert-ic { background: #FEF3C7; color: #B45309; }.bo-alert--blue .bo-alert-ic { background: #E0F2FE; color: #075985; }.bo-alert--red .bo-alert-ic { background: #FEE2E2; color: #B91C1C; }.bo-alert-text { flex: 1; min-width: 0; }.bo-alert-title { font-size: 13.5px; font-weight: 600; color: var(--grey12); letter-spacing: -0.1px; line-height: 18px;}.bo-alert-sub { font-size: 12px; font-weight: 400; color: var(--grey11); line-height: 16px; margin-top: 3px;}Status hero (top card on every detail page — same neutral-white + tonal-icon rule):
.bo-status-hero { background: var(--white); border: 0; border-radius: 16px; box-shadow: var(--sh-card); padding: 14px; display: flex; align-items: center; gap: 12px;}.bo-status-hero-ic { width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; background: var(--grey2); color: var(--grey11);}.bo-status-hero[data-tone="amber"] .bo-status-hero-ic { background: #FEF3C7; color: #B45309; }.bo-status-hero[data-tone="green"] .bo-status-hero-ic { background: var(--g-3); color: var(--g-dark); }.bo-status-hero[data-tone="red"] .bo-status-hero-ic { background: #FEE2E2; color: #B91C1C; }.bo-status-hero-ic svg { width: 18px; height: 18px; }.bo-status-hero-text { flex: 1; min-width: 0; }.bo-status-hero-label { font-size: 14.5px; font-weight: 600; color: var(--grey12); letter-spacing: -0.1px; line-height: 19px;}.bo-status-hero-sub { font-size: 12px; font-weight: 500; color: var(--grey10); line-height: 16px; margin-top: 2px;}<div class="bo-status-hero" data-tone="green"> <span class="bo-status-hero-ic" aria-hidden="true"><svg …check…></svg></span> <div class="bo-status-hero-text"> <p class="bo-status-hero-label">Mapping created · REGISTERED_UNASSIGNED</p> <p class="bo-status-hero-sub">Completed 09:36 · Path A</p> </div></div>Nested/secondary tappable cards sit on grey2 instead of white when INSIDE a white panel or sheet (.bo-path-tile 16px radius, .bo-linked-card 12px radius, .bo-sim-btn 12px radius) — grey-on-white nesting, never white-on-white:
.bo-path-tile { display: flex; align-items: flex-start; gap: 14px; width: 100%; background: var(--grey2); border: 0; border-radius: 16px; padding: 16px; font-family: inherit; text-align: left; cursor: pointer; -webkit-appearance: none; appearance: none; transition: filter 0.15s ease, transform 0.1s ease;}.bo-path-tile:hover { filter: brightness(0.99); }.bo-path-tile:active { transform: scale(0.99); }.bo-path-tile[data-path="B"] { background: rgba(30, 64, 175, 0.05); }.bo-path-tile-ic { width: 42px; height: 42px; border-radius: 50%; background: var(--g-3); color: var(--g-dark); display: flex; align-items: center; justify-content: center; flex-shrink: 0;}.bo-path-tile[data-path="B"] .bo-path-tile-ic { background: rgba(30, 64, 175, 0.10); color: #1E40AF;}.bo-path-tile-ic svg { width: 20px; height: 20px; }.bo-path-tile-text { flex: 1; min-width: 0; }.bo-path-tile-title { font-size: 14.5px; font-weight: 600; color: var(--grey12); letter-spacing: -0.1px;}.bo-path-tile-sub { font-size: 12px; font-weight: 500; color: var(--grey11); line-height: 17px; margin-top: 3px;}.bo-linked-card { display: grid; grid-template-columns: 36px 1fr auto; gap: 12px; align-items: center; width: 100%; background: var(--grey2); border: 0; border-radius: 12px; padding: 12px 14px; font-family: inherit; text-align: left; cursor: pointer; -webkit-appearance: none; appearance: none;}.bo-linked-card:hover { filter: brightness(0.99); }.bo-linked-ic { width: 36px; height: 36px; border-radius: 50%; background: var(--white); color: var(--grey11); display: flex; align-items: center; justify-content: center;}.bo-linked-ic svg { width: 16px; height: 16px; }.bo-linked-text { min-width: 0; }.bo-linked-id { font-size: 12.5px; font-weight: 600; color: var(--grey12); letter-spacing: -0.05em; font-variant-numeric: tabular-nums;}.bo-linked-sub { font-size: 11.5px; font-weight: 500; color: var(--grey10); margin-top: 2px;}.bo-linked-chev { color: var(--grey9); }.bo-linked-chev svg { width: 14px; height: 14px; display: block; }3.10 Overlays — bottom sheets (the ONLY overlay type) + fullscreen camera
Section titled “3.10 Overlays — bottom sheets (the ONLY overlay type) + fullscreen camera”No centered modal, no dialog, no side drawer exists in this file. All confirmations and pickers are bottom sheets (mobile hard rule). Fullscreen camera is a .screen, not an overlay.
.bo-sheet-overlay { position: absolute; inset: 0; z-index: 50; display: flex; flex-direction: column; justify-content: flex-end; pointer-events: none; overflow: hidden;}.bo-sheet-overlay.is-open { pointer-events: auto; }.bo-sheet-backdrop { position: absolute; inset: 0; background: rgba(14,15,18,0.45); opacity: 0; transition: opacity 0.22s ease;}.bo-sheet-overlay.is-open .bo-sheet-backdrop { opacity: 1; }.bo-sheet { position: relative; background: var(--white); border-radius: 24px 24px 0 0; padding: 0 24px; transform: translateY(100%); transition: transform 0.28s cubic-bezier(0.32,0.72,0,1); box-shadow: var(--sh-sheet); max-height: 86%; display: flex; flex-direction: column;}.bo-sheet-overlay.is-open .bo-sheet { transform: translateY(0); }.bo-sheet-handle-row { display: flex; align-items: center; justify-content: center; padding: 10px 0 6px; flex-shrink: 0;}.bo-sheet-handle { width: 40px; height: 4px; border-radius: 100px; background: var(--grey5);}.bo-sheet-head { padding: 8px 0 16px; flex-shrink: 0; }.bo-sheet-title { font-size: 20px; font-weight: 600; color: var(--grey12); letter-spacing: -0.3px; margin: 0;}.bo-sheet-sub { font-size: 13px; color: var(--grey11); line-height: 19px; margin: 6px 0 0;}.bo-sheet-body { display: flex; flex-direction: column; gap: 16px; padding: 0 0 8px; overflow-y: auto;}.bo-sheet-body::-webkit-scrollbar { display: none; }.bo-sheet-cta { display: flex; flex-direction: column; gap: 10px; padding: 16px 0 28px; flex-shrink: 0;}Markup (path picker):
<div class="bo-sheet-overlay" id="sheet-path-picker"> <div class="bo-sheet-backdrop" onclick="closePathPickerSheet()"></div> <div class="bo-sheet"> <div class="bo-sheet-handle-row"><span class="bo-sheet-handle"></span></div> <div class="bo-sheet-head"> <p class="bo-sheet-title">Choose onboarding path</p> <p class="bo-sheet-sub">Path A is the default. Use Path B only when the label is unreadable…</p> </div> <div class="bo-sheet-body"> <button class="bo-path-tile" data-path="A" type="button" onclick="startNewSession('A')">…</button> <button class="bo-path-tile" data-path="B" type="button" onclick="startNewSession('B')">…</button> </div> <div class="bo-sheet-cta"> <button class="btn-secondary btn-secondary--lg" type="button" onclick="closePathPickerSheet()">Cancel</button> </div> </div></div>Footer button placement: vertical stack in .bo-sheet-cta — destructive/primary action FIRST (top), Cancel .btn-secondary--lg LAST (bottom). Failure sheets: btn-primary--lg btn-primary--red confirm on top, secondary Cancel below. Picker sheets: only a Cancel (the tiles themselves are the actions). Backdrop click closes. Open/close = toggling .is-open on the overlay.
Sheet inventory: sheet-path-picker, sheet-scan-serial, sheet-fail-bms-unresponsive, sheet-fail-bms-collision, sheet-fail-serial-collision, sheet-fail-lost-identity.
Login sheet variant (static, non-dismissable, used on the welcome screen — brand-dark screen behind a white pinned sheet):
.sheet-screen { background: var(--brand-dark);}.sheet { position: absolute; inset: 60px 0 0 0; background: var(--white); border-radius: 24px 24px 0 0; display: flex; flex-direction: column; padding: 0 24px 32px; overflow: hidden;}.sheet-pill { width: 36px; height: 4px; border-radius: 100px; background: var(--grey5); margin: 8px auto 0; flex-shrink: 0;}Fullscreen camera (#s-camera-evidence, .ka-* = canonical Kit Assembler pattern):
.ka-camera-shell { background: #000; flex: 1; display: flex; flex-direction: column; padding-top: 44px; }.ka-camera-head { padding: 12px 16px 4px; display: flex; align-items: center; gap: 12px; }.ka-camera-back { width: 36px; height: 36px; border-radius: 50%; background: rgba(255,255,255,0.12); border: 0; color: var(--white); display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 0; -webkit-appearance: none; appearance: none; flex-shrink: 0; }.ka-camera-back svg { width: 16px; height: 16px; }.ka-camera-title-wrap { flex: 1; min-width: 0; }.ka-camera-title { font-size: 14.5px; font-weight: 600; color: var(--white); line-height: 18px; }.ka-camera-meta { font-size: 11px; color: rgba(255,255,255,0.5); line-height: 14px; margin-top: 2px; }.ka-viewfinder { position: relative; flex: 1; margin: 16px; border-radius: 24px; background: linear-gradient(180deg, #15151a 0%, #0a0a0d 100%); display: flex; align-items: center; justify-content: center; overflow: hidden; cursor: pointer; }.ka-viewfinder-grid { position: absolute; inset: 0; background-image: linear-gradient(rgba(255,255,255,0.05) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.05) 1px, transparent 1px); background-size: 32px 32px; pointer-events: none; }.ka-viewfinder-corner { position: absolute; width: 28px; height: 28px; border-color: rgba(1,228,111,0.85); border-style: solid; }.ka-viewfinder-corner-tl { top: 16px; left: 16px; border-width: 3px 0 0 3px; border-radius: 6px 0 0 0; }.ka-viewfinder-corner-tr { top: 16px; right: 16px; border-width: 3px 3px 0 0; border-radius: 0 6px 0 0; }.ka-viewfinder-corner-bl { bottom: 16px; left: 16px; border-width: 0 0 3px 3px; border-radius: 0 0 0 6px; }.ka-viewfinder-corner-br { bottom: 16px; right: 16px; border-width: 0 3px 3px 0; border-radius: 0 0 6px 0; }.ka-viewfinder-target { width: 64px; height: 64px; border-radius: 16px; background: rgba(255,255,255,0.06); display: flex; align-items: center; justify-content: center; color: rgba(255,255,255,0.55); }.ka-viewfinder-target svg { width: 32px; height: 32px; }.ka-viewfinder-label { position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%); font-size: 12px; color: rgba(255,255,255,0.6); white-space: nowrap; }.ka-camera-controls { display: flex; align-items: center; justify-content: space-between; padding: 16px 24px 32px; background: #000; }.ka-shutter { width: 72px; height: 72px; border-radius: 50%; border: 4px solid var(--white); background: rgba(255,255,255,0.92); cursor: pointer; padding: 0; -webkit-appearance: none; appearance: none; flex-shrink: 0; transition: transform 0.15s ease, background 0.15s ease; }.ka-shutter:hover { transform: scale(1.02); background: var(--white); }.ka-shutter:active { transform: scale(0.96); }.ka-camera-side { width: 84px; flex-shrink: 0; display: flex; justify-content: center; }In-sheet scan viewfinder (.bo-scan-stage, mirrors PM pm-scan-stage — 16/11 aspect, corner brackets, animated laser):
.bo-scan-stage { position: relative; width: 100%; aspect-ratio: 16 / 11; border-radius: 16px; overflow: hidden; background: linear-gradient(160deg, #1a1a1d 0%, #0a0a0c 60%, #050505 100%); padding: 16px; display: flex; flex-direction: column;}.bo-scan-stage::before { content: ''; position: absolute; inset: 0; background: radial-gradient(circle at 30% 25%, rgba(1,228,111,0.10), transparent 55%), radial-gradient(circle at 78% 75%, rgba(255,255,255,0.04), transparent 60%); pointer-events: none;}.bo-scan-reticle { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 64%; aspect-ratio: 1 / 1; max-height: 75%;}.bo-scan-corner { position: absolute; width: 22px; height: 22px; border: 2.5px solid var(--brand);}.bo-scan-corner--tl { top: 0; left: 0; border-right: 0; border-bottom: 0; border-top-left-radius: 6px; }.bo-scan-corner--tr { top: 0; right: 0; border-left: 0; border-bottom: 0; border-top-right-radius: 6px; }.bo-scan-corner--bl { bottom: 0; left: 0; border-right: 0; border-top: 0; border-bottom-left-radius: 6px; }.bo-scan-corner--br { bottom: 0; right: 0; border-left: 0; border-top: 0; border-bottom-right-radius: 6px; }.bo-scan-laser { position: absolute; left: 10%; right: 10%; top: 50%; height: 2px; background: linear-gradient(90deg, transparent, var(--brand), transparent); box-shadow: 0 0 12px rgba(1,228,111,0.6); animation: bo-scan-laser 2.4s ease-in-out infinite;}@keyframes bo-scan-laser { 0% { transform: translateY(-60%); opacity: 0.2; } 50% { transform: translateY(60%); opacity: 1; } 100% { transform: translateY(-60%); opacity: 0.2; }}.bo-scan-hint { position: relative; z-index: 1; align-self: center; margin-top: auto; display: inline-flex; align-items: center; gap: 6px; padding: 5px 12px; border-radius: 100px; background: rgba(255,255,255,0.08); font-size: 11.5px; font-weight: 600; color: rgba(255,255,255,0.75); letter-spacing: -0.02em;}3.11 Toast
Section titled “3.11 Toast”.alert-toast { position: absolute; top: 56px; left: 50%; transform: translate(-50%, -16px); z-index: 80; display: inline-flex; align-items: center; gap: 8px; border-radius: 999px; padding: 4px 12px 4px 4px; white-space: nowrap; box-shadow: 0 4px 20px rgba(0,0,0,0.12); opacity: 0; pointer-events: none; transition: opacity 0.25s ease, transform 0.25s ease;}.alert-toast.is-visible { opacity: 1; transform: translate(-50%, 0); pointer-events: auto; }.alert-toast-badge { display: flex; align-items: center; justify-content: center; background: rgba(255,255,255,0.85); border-radius: 12px; padding: 3px 10px; font-size: 13px; font-weight: 500; line-height: 20px;}.alert-toast-text { font-size: 13px; font-weight: 500; line-height: 20px; }.alert-toast-close { background: none; border: 0; cursor: pointer; padding: 0 2px; margin-left: 4px; display: flex; align-items: center; color: currentColor; opacity: 0.55;}.alert-toast-close:hover { opacity: 1; }.alert-toast-close svg { width: 12px; height: 12px; }.alert-toast--success { background: #dcfbe2; color: #05853e; }.alert-toast--info { background: #e6f4fe; color: #0d74ce; }.alert-toast--error { background: #FEE2E2; color: #B91C1C; }Markup (single instance, last child of .phone):
<div class="alert-toast alert-toast--success" id="toast" role="status" aria-live="polite"> <span class="alert-toast-badge">Saved</span> <span class="alert-toast-text">Done</span> <button class="alert-toast-close" type="button" onclick="hideToast()" aria-label="Dismiss"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="4" y1="4" x2="12" y2="12"/><line x1="12" y1="4" x2="4" y2="12"/></svg></button></div>JS helper (verbatim):
let _toastTimer = null;function showToast(message, badge, variant) { const t = document.getElementById('toast'); if (!t) return; if (_toastTimer) clearTimeout(_toastTimer); ['success','info','error'].forEach(v => t.classList.remove('alert-toast--' + v)); t.classList.add('alert-toast--' + (variant || 'success')); if (badge) t.querySelector('.alert-toast-badge').textContent = badge; if (message) t.querySelector('.alert-toast-text').textContent = message; t.classList.add('is-visible'); _toastTimer = setTimeout(hideToast, 3500);}function hideToast() { document.getElementById('toast')?.classList.remove('is-visible'); }Signature: showToast(message, badge, variant) — note badge is arg 2, variant ∈ ‘success’|‘info’|‘error’, auto-dismiss 3500ms. Pill anchored top-center at 56px, slides down 16px on entry. Example calls: showToast(s.serial + ' ↔ ' + s.bmsId, 'Mapping created', 'success'), showToast('Routed to Quarantine', 'Session closed', 'error'), showToast('Coming soon', 'Profile', 'info').
3.12 Inputs & forms
Section titled “3.12 Inputs & forms”.field-group { display: flex; flex-direction: column; gap: 6px; margin-bottom: 18px;}.field-label { font-size: 14px; font-weight: 500; color: var(--grey12); line-height: 20px;}.input { display: flex; align-items: center; gap: 8px; height: 48px; padding: 0 14px; background: var(--white); border: 1px solid var(--grey6); border-radius: 8px; box-shadow: var(--sh-xs); transition: border-color var(--transition), box-shadow var(--transition);}.input:focus-within { border-color: var(--brand); box-shadow: 0 0 0 4px rgba(1, 228, 111, 0.12);}.input.is-error { border-color: var(--red9); box-shadow: 0 0 0 4px rgba(227,38,39,0.12);}.input input { flex: 1; min-width: 0; border: 0; outline: 0; background: transparent; font-family: inherit; font-size: 16px; line-height: 24px; color: var(--grey12);}.input input::placeholder { color: var(--grey9); }.input-icon { width: 20px; height: 20px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; background: transparent; border: 0; padding: 0; outline: 0; -webkit-appearance: none; appearance: none; color: var(--grey10); cursor: pointer;}.input-icon:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: 2px; }.input-icon svg { width: 100%; height: 100%; display: block; }
.field-aside { text-align: right; margin-top: 8px;}.text-link { font-size: 14px; font-weight: 600; color: var(--g-dark); text-decoration: none; cursor: pointer;}Textarea (notes):
.bo-notes { width: 100%; background: var(--white); border: 1px solid var(--grey6); border-radius: 10px; padding: 10px 12px; font-family: inherit; font-size: 13.5px; line-height: 19px; color: var(--grey12); resize: none; outline: 0; min-height: 72px;}.bo-notes:focus { border-color: var(--brand); box-shadow: 0 0 0 4px rgba(1,228,111,0.12);}Toggle switch:
.rsm-toggle { position: relative; width: 36px; height: 22px; background: var(--grey5); border-radius: 100px; cursor: pointer; transition: background 0.2s ease; border: 0; -webkit-appearance: none; appearance: none; flex-shrink: 0;}.rsm-toggle::after { content: ''; position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; background: var(--white); border-radius: 50%; transition: transform 0.2s ease; box-shadow: 0 1px 2px rgba(0,0,0,0.1);}.rsm-toggle[aria-pressed="true"] { background: var(--g-primary); }.rsm-toggle[aria-pressed="true"]::after { transform: translateX(14px); }Markup:
<div class="field-group"> <label class="field-label" for="welcome-email">Email</label> <div class="input"> <input id="welcome-email" type="email" placeholder="e.g tobiloba@rechaj.com" oninput="onWelcomeInput()"> </div></div>
<div class="input"> <input id="welcome-password" type="password" placeholder="Enter password" oninput="onWelcomeInput()"> <button class="input-icon" type="button" onclick="togglePw('welcome-password', this)" aria-label="Show password"><svg …eye…></svg></button></div>
<textarea class="bo-notes" placeholder="Notes (optional) — what you observed, retries attempted, last firmware visible on the bench"></textarea>
<button class="rsm-toggle" type="button" aria-pressed="true" aria-label="Push notifications" onclick="toggleSwitch(this)"></button>Focus ring convention: 1px border swaps to --brand + 4px soft ring rgba(1,228,111,0.12); error is --red9 + rgba(227,38,39,0.12). No <select> or search input exists in this file.
3.13 Dropdown menus
Section titled “3.13 Dropdown menus”None present. No dropdown/popover menu in this app; selections happen via bottom-sheet tile pickers (.bo-path-tile).
3.14 Empty states
Section titled “3.14 Empty states”Two levels:
Full-screen stub .bo-empty (defined but not instantiated in markup — reserved for stub tab screens):
.bo-empty { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 24px; text-align: center; gap: 12px;}.bo-empty-ic { width: 56px; height: 56px; border-radius: 50%; background: var(--g-2); color: var(--g-dark); display: flex; align-items: center; justify-content: center;}.bo-empty-ic svg { width: 26px; height: 26px; }.bo-empty-title { font-size: 16px; font-weight: 600; color: var(--grey12); letter-spacing: -0.2px;}.bo-empty-sub { font-size: 13px; color: var(--grey11); line-height: 18px; max-width: 260px;}In-pane empty .bo-pane-empty (rendered by JS when a filter pane has no rows):
.bo-pane-empty { padding: 32px 24px; text-align: center; color: var(--grey10);}.bo-pane-empty-ic { width: 48px; height: 48px; border-radius: 50%; background: var(--grey2); color: var(--grey10); display: inline-flex; align-items: center; justify-content: center; margin-bottom: 10px;}.bo-pane-empty-ic svg { width: 22px; height: 22px; }.bo-pane-empty-title { font-size: 14px; font-weight: 600; color: var(--grey11); margin-bottom: 4px;}.bo-pane-empty-sub { font-size: 12.5px; max-width: 320px; margin: 0 auto; }Markup (JS template sessionsPaneEmptyHtml):
<div class="bo-pane-empty"> <span class="bo-pane-empty-ic" aria-hidden="true"><svg …clipboard…></svg></span> <p class="bo-pane-empty-title">No active onboarding sessions</p> <p class="bo-pane-empty-sub">Tap "Start new onboarding" below to begin a session.</p></div>3.15 Icon-container convention
Section titled “3.15 Icon-container convention”Circular (border-radius: 50%) tinted-background + colored-glyph, always. The tint carries severity; the container’s card stays neutral. Sizes observed: 28px (activity, steps), 32px (log avatars, profile-row exception), 36px (stats/KPI/alerts/status-hero/linked), 40px (session/qtn rows, bell, back), 42px (path tiles), 44px (hero avatar), 48/56px (empty states), 56px (FAB, profile avatar). Glyph = stroke SVG at exactly half the container (14/16/18/20px). Tonal pairs:
- grey:
var(--grey2)orvar(--grey3)bg /--grey10–--grey11glyph - green:
var(--g-2)orvar(--g-3)bg /--g-darkglyph - amber:
#FEF3C7/#B45309 - red:
#FEE2E2/#B91C1C - blue:
#E0F2FE/#075985orrgba(30,64,175,0.08–0.12)/#1E40AF
Solid (non-tonal) circles are reserved for identity/action: brand avatar (--brand bg, dark initials), FAB (--g-primary), splash logo.
Documented exception: .rsm-profile-row-ic is a 10px rounded square (32×32, grey2 bg) — canonical Kit Assembler precedent, explicitly commented “Don’t ‘fix’ it”:
.rsm-profile-row-ic { width: 32px; height: 32px; border-radius: 10px; background: var(--grey2); display: flex; align-items: center; justify-content: center; color: var(--grey12); flex-shrink: 0;}.rsm-profile-row-ic svg { width: 18px; height: 18px; }Rounded squares also allowed for photo/media thumbnails per hard rule: .rt-capture-add/.rt-capture-thumb 88×88 r14, .bo-evidence-thumb square r12, .ka-viewfinder-target 64×64 r16.
3.16 Profile block (canonical .rsm-profile-*, lifted verbatim from Kit Assembler)
Section titled “3.16 Profile block (canonical .rsm-profile-*, lifted verbatim from Kit Assembler)”.rsm-profile-user { background: var(--white); border-radius: 16px; box-shadow: var(--sh-card); padding: 16px; display: flex; align-items: center; gap: 12px; cursor: pointer; border: 0; text-align: left; font-family: inherit; width: 100%; -webkit-appearance: none; appearance: none; transition: background 0.15s ease;}.rsm-profile-user:hover { background: var(--grey1); }.rsm-profile-avatar { width: 56px; height: 56px; border-radius: 50%; background: var(--brand); display: flex; align-items: center; justify-content: center; font-size: 20px; font-weight: 600; color: var(--text-main); letter-spacing: -0.4px; flex-shrink: 0;}.rsm-profile-text { flex: 1; min-width: 0; }.rsm-profile-name { font-size: 15px; font-weight: 600; color: var(--grey12); letter-spacing: -0.2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;}.rsm-profile-meta { font-size: 12px; color: var(--grey11); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;}.rsm-profile-chev { width: 28px; height: 28px; border-radius: 50%; background: var(--grey2); display: flex; align-items: center; justify-content: center; color: var(--grey10); flex-shrink: 0;}.rsm-profile-chev svg { width: 14px; height: 14px; }.rsm-profile-section { background: var(--white); border-radius: 16px; box-shadow: var(--sh-card); padding: 4px 0;}.rsm-profile-section-title { font-size: 12px; font-weight: 600; color: var(--grey10); letter-spacing: 0.06em; text-transform: uppercase; padding: 0 4px 6px;}.rsm-profile-row { display: flex; align-items: center; gap: 12px; width: 100%; padding: 14px 16px; background: transparent; border: 0; text-align: left; cursor: pointer; font-family: inherit; -webkit-appearance: none; appearance: none; position: relative;}.rsm-profile-row + .rsm-profile-row::before { content: ''; position: absolute; left: 60px; right: 16px; top: 0; border-top: 1px solid var(--grey3);}.rsm-profile-row-label { flex: 1; font-size: 14.5px; font-weight: 500; color: var(--grey12); letter-spacing: -0.1px;}.rsm-profile-row-sub { display: block; font-size: 12px; font-weight: 500; color: var(--grey10); margin-top: 2px; letter-spacing: 0;}.rsm-profile-role-pill { display: inline-block; font-size: 11px; font-weight: 600; padding: 3px 10px; border-radius: 100px; background: var(--g-2); color: var(--g-dark); letter-spacing: 0.02em; margin: 6px 0 2px;}.rsm-profile-row-tail { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--grey10); flex-shrink: 0;}.rsm-profile-row-tail svg { width: 16px; height: 16px; color: var(--grey10); }.rsm-profile-row--danger .rsm-profile-row-ic { background: #FEE2E2; color: #B91C1C; }.rsm-profile-row--danger .rsm-profile-row-label { color: #B91C1C; }Markup:
<button class="rsm-profile-user" type="button" onclick="showToast('Coming soon', 'Profile', 'info')"> <span class="rsm-profile-avatar">TA</span> <div class="rsm-profile-text"> <p class="rsm-profile-name">Tobiloba Adekoya</p> <span class="rsm-profile-role-pill">Battery Onboarding Operator</span> <p class="rsm-profile-meta">tobiloba.adekoya@rechaj.com</p> </div> <span class="rsm-profile-chev"><svg …chevron…></svg></span></button>
<div> <p class="rsm-profile-section-title">Workspace</p> <div class="rsm-profile-section"> <button class="rsm-profile-row" type="button"> <span class="rsm-profile-row-ic"><svg …></svg></span> <span class="rsm-profile-row-label">My station <span class="rsm-profile-row-sub">Lagos Hub · Bench 1 · Mon shift</span> </span> <span class="rsm-profile-row-tail"><svg …chevron…></svg></span> </button> … </div></div>Sections: Workspace / Account / Preferences / Support; danger sign-out row is last. Toggles are rows with a .rsm-toggle tail; value rows show text + chevron in .rsm-profile-row-tail (e.g. “On”, “English”).
3.17 Steps / progress list & event log & KV facts (detail-page atoms)
Section titled “3.17 Steps / progress list & event log & KV facts (detail-page atoms)”Steps:
.bo-steps { display: flex; flex-direction: column; gap: 10px; }.bo-step-row { display: grid; grid-template-columns: 28px 1fr; gap: 12px; align-items: flex-start;}.bo-step-ic { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; background: var(--grey3); color: var(--grey10); font-size: 12px; font-weight: 600;}.bo-step-ic[data-state="done"] { background: var(--g-2); color: var(--g-dark); }.bo-step-ic[data-state="active"] { background: var(--g-3); color: var(--g-dark); }.bo-step-ic[data-state="failed"] { background: #FEE2E2; color: #B91C1C; }.bo-step-ic svg { width: 14px; height: 14px; }.bo-step-text { flex: 1; min-width: 0; }.bo-step-title { font-size: 13.5px; font-weight: 600; color: var(--grey12); letter-spacing: -0.1px; line-height: 18px;}.bo-step-row[data-state="pending"] .bo-step-title { color: var(--grey10); font-weight: 500; }.bo-step-row[data-state="active"] .bo-step-title { color: var(--g-dark); }.bo-step-sub { font-size: 11.5px; font-weight: 500; color: var(--grey10); line-height: 16px; margin-top: 2px;}Step icon content: number (pending/active) or check SVG (done) or X SVG (failed). States via data-state="pending|active|done|failed" on both row and icon.
Event log:
.bo-log-list { display: flex; flex-direction: column; gap: 12px; }.bo-log-row { display: grid; grid-template-columns: 32px 1fr; gap: 10px; align-items: flex-start;}.bo-log-row[data-pending="1"] .bo-log-text,.bo-log-row[data-pending="1"] .bo-log-time { color: var(--grey9); }.bo-log-row[data-pending="1"] .bo-log-avatar { background: transparent; color: var(--grey9); outline: 1px dashed var(--grey5);}.bo-log-avatar { width: 32px; height: 32px; border-radius: 50%; background: var(--g-3); color: var(--g-dark); display: flex; align-items: center; justify-content: center; flex-shrink: 0;}.bo-log-avatar[data-tone="red"] { background: #FEE2E2; color: #B91C1C; }.bo-log-avatar[data-tone="blue"] { background: rgba(30, 64, 175, 0.12); color: #1E40AF; }.bo-log-avatar[data-tone="amber"] { background: #FEF3C7; color: #B45309; }.bo-log-avatar svg { width: 14px; height: 14px; }.bo-log-text { font-size: 13px; font-weight: 500; color: var(--grey12); line-height: 18px;}.bo-log-text strong { font-weight: 600; }.bo-log-time { font-size: 11.5px; font-weight: 500; color: var(--grey10); margin-top: 2px;}Log rows use <strong> (600) for event names/IDs inside 500-weight body text and append a .bo-log-evt-id chip. Future events are data-pending="1" → grey text, dashed-outline transparent avatar.
KV facts (2-col label/value rows):
.bo-kv-list { display: flex; flex-direction: column; gap: 8px; }.bo-kv-row { display: flex; align-items: baseline; justify-content: space-between; gap: 10px;}.bo-kv-label { font-size: 12px; font-weight: 500; color: var(--grey10); letter-spacing: 0.02em; text-transform: uppercase;}.bo-kv-val { font-size: 13.5px; font-weight: 600; color: var(--grey12); letter-spacing: -0.1px; font-variant-numeric: tabular-nums; text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}.bo-kv-val[data-large="1"] { font-size: 15px; font-weight: 600; letter-spacing: -0.2px; white-space: normal; word-break: break-all;}.bo-kv-val[data-muted="1"] { color: var(--grey10); font-weight: 500; }Evidence-photo capture row (.rt-*, canonical Kit Assembler / Production QA):
.rt-capture-section { display: flex; flex-direction: column; gap: 10px; }.rt-capture-heading { font-size: 14px; font-weight: 600; color: var(--grey12); letter-spacing: -0.1px; }.rt-capture-row { display: flex; gap: 10px; overflow-x: auto; padding: 2px 2px 6px; -webkit-overflow-scrolling: touch; }.rt-capture-row::-webkit-scrollbar { display: none; }.rt-capture-add { width: 88px; height: 88px; border: 1.5px dashed var(--grey6); border-radius: 14px; background: var(--white); display: flex; align-items: center; justify-content: center; color: var(--grey10); flex-shrink: 0; cursor: pointer; padding: 0; -webkit-appearance: none; appearance: none; transition: background 0.18s ease, border-color 0.18s ease; }.rt-capture-add:hover { background: var(--grey2); border-color: var(--grey9); color: var(--grey12); }.rt-capture-add svg { width: 30px; height: 30px; }.rt-capture-thumb { position: relative; width: 88px; height: 88px; border-radius: 14px; background: linear-gradient(135deg, #1f2230 0%, #2c3140 100%); display: flex; align-items: center; justify-content: center; color: rgba(255,255,255,0.45); flex-shrink: 0; overflow: hidden; }.rt-capture-thumb-ic { width: 26px; height: 26px; }.rt-capture-thumb-del { position: absolute; top: 6px; right: 6px; width: 22px; height: 22px; border-radius: 50%; background: rgba(0,0,0,0.6); color: var(--white); display: flex; align-items: center; justify-content: center; border: 0; padding: 0; cursor: pointer; -webkit-appearance: none; appearance: none; transition: background 0.15s ease; }.rt-capture-thumb-del:hover { background: rgba(0,0,0,0.85); }.rt-capture-thumb-del svg { width: 10px; height: 10px; }.rt-capture-hint { font-size: 11.5px; color: var(--grey10); line-height: 16px; }<div class="rt-capture-section"> <p class="rt-capture-heading">Evidence photos</p> <div class="rt-capture-row" id="evidence-row-fail-bms-unresponsive"> <button class="rt-capture-add" type="button" onclick="openEvidenceCamera('sheet-fail-bms-unresponsive', 'evidence-row-fail-bms-unresponsive', {title:'Evidence photo', meta:'Bench display + battery label in one frame'})" aria-label="Take photo"> <svg …camera…></svg> </button> </div> <p class="rt-capture-hint">Bench display + battery label in one frame.</p></div>Evidence thumbnails on quarantine detail (read-only, rounded squares OK — photo thumbnails):
.bo-evidence-thumbs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px;}.bo-evidence-thumb { position: relative; aspect-ratio: 1 / 1; border-radius: 12px; background: linear-gradient(135deg, var(--grey3) 0%, var(--grey2) 100%); overflow: hidden; display: flex; align-items: center; justify-content: center; color: var(--grey10);}.bo-evidence-thumb svg { width: 22px; height: 22px; }.bo-evidence-thumb-label { position: absolute; left: 6px; right: 6px; bottom: 6px; font-size: 9.5px; font-weight: 600; color: var(--white); letter-spacing: 0.02em; background: rgba(14,15,18,0.55); padding: 2px 6px; border-radius: 6px; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;}.bo-evidence-thumbs--empty { font-size: 12px; font-weight: 500; color: var(--grey10);}Sticky detail CTA strip:
.bo-detail-cta { background: var(--white); border-top: 1px solid var(--grey3); padding: 14px 16px 8px; display: flex; gap: 10px; flex-shrink: 0;}.bo-detail-cta .btn-primary,.bo-detail-cta .btn-secondary { height: 48px; flex: 1; }.bo-detail-cta:empty { display: none; }3.18 Splash / onboarding / welcome screens (auth trio)
Section titled “3.18 Splash / onboarding / welcome screens (auth trio)”#s-splash { background: var(--brand-dark); align-items: center; justify-content: center; cursor: pointer;}#s-splash .splash-logo { width: 80px; height: 80px; background: var(--brand); border-radius: 50%; display: flex; align-items: center; justify-content: center;}#s-splash .splash-logo svg { width: 44px; height: 44px; }
#s-onboarding { background: var(--brand-dark); padding: 0 24px;}.ob-brand { display: flex; align-items: center; gap: 10px; justify-content: center; margin-top: 24px;}.ob-brand-mark { width: 36px; height: 36px; }.ob-brand-name { font-size: 22px; font-weight: 700; color: var(--white); letter-spacing: -0.5px;}.ob-photo { flex: 1; display: flex; align-items: center; justify-content: center; margin-top: 16px;}.ob-photo-img { display: block; width: 280px; height: auto; max-height: 360px; object-fit: contain;}.ob-text { text-align: center; padding: 8px 0 56px;}.ob-title { font-size: 24px; font-weight: 700; color: var(--white); line-height: 32px; letter-spacing: -0.4px; margin-bottom: 12px;}.ob-title .accent { color: var(--brand); }.ob-tag { font-size: 14px; font-weight: 400; color: rgba(255,255,255,0.7); line-height: 22px; max-width: 320px; margin: 0 auto;}.ob-cta-wrap { padding-bottom: 32px; }
.welcome-head { text-align: center; padding: 22px 0 28px;}.welcome-title { font-size: 28px; font-weight: 700; color: var(--grey12); letter-spacing: -0.4px; line-height: 36px; margin-bottom: 6px;}.welcome-sub { font-size: 14px; font-weight: 400; color: var(--grey11); line-height: 20px;}.welcome-cta-wrap { margin-top: auto; padding-top: 32px; display: flex; flex-direction: column; gap: 16px; align-items: center;}.welcome-faceid-wrap { display: flex; justify-content: center; padding: 24px 0 0;}.welcome-faceid { width: 44px; height: 44px; background: transparent; border: 0; cursor: pointer; color: var(--grey12); display: flex; align-items: center; justify-content: center; -webkit-appearance: none; appearance: none; transition: color var(--transition);}.welcome-faceid svg { width: 32px; height: 32px; }.welcome-faceid:hover { color: var(--g-dark); }Notes: splash = 80px brand-green circle with the two-tone dark mark, tap-to-skip + 2500ms auto-advance. Onboarding = brand row (white+green two-tone mark, 700 “Rechaj”), hex photo (/products/rechaj-retrofit-app/flows/rsm/onboarding-photo.png, absolute path), 24px/700 title with green .accent span, then full-width brand CTA. Welcome = brand-dark screen + white pinned .sheet (inset 60px 0 0 0), disabled-until-valid Login button (email regex + pw ≥ 4 chars).
4. Page anatomy
Section titled “4. Page anatomy”4.1 Splash (#s-splash)
Section titled “4.1 Splash (#s-splash)”Order: dark status bar (absolute) → centered 80px logo circle → light home indicator. Whole screen clickable → goTo('s-onboarding'); auto-advance 2500ms.
4.2 Onboarding welcome (#s-onboarding)
Section titled “4.2 Onboarding welcome (#s-onboarding)”Order: dark sb → .ob-brand (mark + name) → .ob-photo (flex:1) → .ob-text (title + tagline) → .ob-cta-wrap (primary Get started) → light home-ind. CTA → s-welcome.
4.3 Login (#s-welcome)
Section titled “4.3 Login (#s-welcome)”Brand-dark screen; white .sheet pinned from 60px down. Order inside sheet: .sheet-pill → .welcome-head (28px/700 title + sub) → Email field-group → Password field-group (eye toggle, right-aligned Forgot-password link) → Face ID button (centered, jumps straight to dashboard) → .welcome-cta-wrap pinned to bottom (margin-top:auto) with disabled-until-valid Login. Login → s-dashboard. Sign-out resets this form.
4.4 Dashboard (#s-dashboard)
Section titled “4.4 Dashboard (#s-dashboard)”Order: .rsm-hero (dark sb inside, id row + bell) → .rsm-stats 2-up overlapping hero by −52px → .rsm-body (padding-top: 18px inline): section-head “Today at a glance / 4 metrics” → .bo-kpi-list (4 tiles) → section-head “Alerts / 2 active” (margin-top: 8px inline) → .bo-alert-list → section-head “Recent activity / Today” → .bo-activity (5 rows) → .rsm-nav → home-ind. Alerts and activity rows are display-only here (no click handlers). KPI numbers recomputed live by renderDashKpis().
4.5 List page — Sessions (#s-sessions)
Section titled “4.5 List page — Sessions (#s-sessions)”Order: light sb → .rsm-page-head (centered title, no back) → .bo-tabs segmented (Active default-selected) → .rsm-body (padding-top: 4px) containing 3 [data-sessions-pane] divs (gap 10 columns), only one visible → .bo-fab (absolute) → .rsm-nav → home-ind.
Interaction: tab click = setSessionsTab(this) (aria-pressed + pane hidden); row click opens a separate detail SCREEN (openOnbSession(id) → hydrate + goTo('s-session-detail')), not a modal/inline expansion; FAB opens the path-picker bottom sheet. Default selection: Active pane. Empty panes render .bo-pane-empty.
4.6 Detail page — Session (#s-session-detail)
Section titled “4.6 Detail page — Session (#s-session-detail)”Order: light sb → .rsm-page-head with .bo-back + centered noun-phrase title → .rsm-body (hydrated by renderSessionDetail()): status hero → Battery panel (KV facts + path chip/note) → Steps panel (4-step progress) → Event log panel → .bo-detail-cta sticky strip → home-ind. No bottom nav.
CTA per state: in_progress/scan_serial → “Scan Serial Number” (opens scan sheet); power_on → “Power battery on + read BMS” (advances, 800ms simulated read); read_bms → disabled “Reading BMS…”; validate → “Confirm mapping” (completes + success toast); failed → red destructive CTA opening the matching failure sheet; completed → secondary “Start next onboarding”.
Confirmations: always via bottom sheet (scan sheet simulations, failure sheets with evidence + notes + red confirm). Success/failure feedback via toast; failure routes back to s-sessions.
4.7 List page — Quarantine (#s-quarantine)
Section titled “4.7 List page — Quarantine (#s-quarantine)”Order: light sb → page head (no back) → .rsm-body (gap 10) hydrated by renderQuarantinePane() with .bo-qtn-row buttons → nav → home-ind. Row click → openOnbQuarantineItem(id) → detail screen. Empty → .bo-pane-empty “Quarantine clear”.
4.8 Detail page — Quarantine item (#s-onb-quarantine-detail)
Section titled “4.8 Detail page — Quarantine item (#s-onb-quarantine-detail)”Order: sb → page head with back → body (hydrated): status hero (red) → Battery panel (KV) → “Why it failed” panel (plain text) → Evidence panel (3-col thumbs or “No photos attached.”) → Linked session panel (grey2 tappable card → cross-links back into session detail) → centered .bo-routed badge. Read-only for this persona (resolution belongs to Identity Admin / PM); no CTA strip, no nav.
4.9 Profile (#s-profile)
Section titled “4.9 Profile (#s-profile)”Order: sb → page head → body: .rsm-profile-user card → 4 titled sections (Workspace / Account / Preferences / Support) of .rsm-profile-section cards with rows (chevron rows, value+chevron rows, toggle rows, danger Sign out) → nav → home-ind. Sign out = resetWelcomeForm(); goTo('s-welcome').
4.10 Fullscreen camera (#s-camera-evidence)
Section titled “4.10 Fullscreen camera (#s-camera-evidence)”Order: dark sb (absolute) → .ka-camera-shell: head (X back + title/meta) → .ka-viewfinder (tap to capture) → controls (spacer / 72px shutter / spacer) → light home-ind. Opened from any failure sheet; on capture/cancel returns to the originating screen and re-opens the originating sheet, appending an .rt-capture-thumb before the add tile.
Global interaction rules
Section titled “Global interaction rules”- Row click → full detail screen (never inline expand, never modal).
- Detail screens: back chevron only; bottom nav hidden.
- Destructive/confirm actions → bottom sheet with evidence + notes + stacked red-primary-then-cancel buttons.
- Every action gives a toast (
successfor completion,errorfor quarantine routing,infofor stubs/“Coming soon”). - No search anywhere; filtering is segmented tabs.
- Deep link:
?screen=<id>(with auto-hydration of first in_progress session / first quarantine item for detail screens).
5. JS contracts
Section titled “5. JS contracts”Navigation & state:
SCREENS— array of the 10 screen ids;goTo(id)— swaps.active, guards against unknown ids,window.scrollTo(0,0).- Data:
ONBOARDING_SESSIONS(keyed byOB-…id),ONB_QUARANTINE_ITEMS(keyed byQTN-OB-…id); mutable module-levelcurrentSessionId,currentOnbQtnId,pendingFailReason. - Step model:
STEP_ORDER = ['scan_serial','power_on','read_bms','validate','mapping_created'],STEP_LABELS,STEP_NUM,FAIL_REASON_LABEL,FAIL_REASON_CTA,FAIL_REASON_SHEETlookup maps.
Lists / rendering:
sessionsByTab(tab),sessionToneIcon(s),sessionAge(s),sessionRowHtml(s),sessionsPaneEmptyHtml(tab),renderSessionsPane(tab),renderSessionsCounts()setSessionsTab(btn)— segmented-control switching (aria-pressed + pane hidden)openOnbSession(id),renderSessionDetail()— hydrate + navigateqtnRowHtml(id, q),renderQuarantinePane(),openOnbQuarantineItem(id),renderOnbQuarantineDetail(),reasonIconSvg(reason),reasonLongText(reason, s)renderDashKpis()— recomputes hero stats + KPI numbers from live data
Sheets (each sheet has its own open/close pair; open = classList.add('is-open')):
openPathPickerSheet()/closePathPickerSheet()openScanSerialSheet()/closeScanSerialSheet();scanSerialSimulate(kind)with kind ∈'ok' | 'pathB' | 'collision'openFailureSheet(reason)/closeFailureSheet(reason)— reason keys:bms_unresponsive | bms_collision | serial_collision | lost_identityconfirmFailure(reason)— marks failed, mintsQTN-OB-…record, re-renders, error toast, returns to sessions list
Flow actions:
startNewSession(path)— mints OB id, seeds session atscan_serial, navigates to detailadvanceToReadBms()— step → read_bms, 800ms setTimeout → bmsId assigned, step → validateconfirmMapping()— completes session + success toast
Camera:
openEvidenceCamera(sheetId, targetRowId, opts)—opts = {title, meta}; stashes_cameraReturn = {sheet, returnScreen, targetRowId}captureEvidencePhoto(),cancelEvidenceCamera(),closeEvidenceCamera(),removeEvidenceThumb(btn)
Toast: showToast(message, badge, variant) / hideToast() (see §3.11).
Misc: toggleSwitch(btn) (aria-pressed flip), togglePw(inputId, btn), onWelcomeInput(), attemptLogin(passwordInputId), resetWelcomeForm(), IIFE init() (hydrate panes, ?screen= deep-link, splash timer), IIFE phone-fit scaler.
6. Naming
Section titled “6. Naming”Class prefixes:
rsm-— shared shell atoms inherited from the Retrofit Station Manager shell (hero, stats, body, nav, page-head, section-head, profile block, toggle). Cross-app canon.bo-— this persona’s namespace (Battery Onboarding): kpi, alert, activity, tabs, session-row, qtn-row, detail panels, kv, steps, log, sheets, scan, fab, back, path, linked, routed, empty, notes, sim.ka-— Kit Assembler canonical camera shell (fullscreen camera).rt-— Retrofit canonical capture-row (evidence photo tiles).- Unprefixed shell/auth:
.phone,.screen,.tab-screen,.sb,.home-ind,.sheet,.ob-*,.welcome-*,.field-*,.input,.btn-primary/.btn-secondary,.text-link,.alert-toast,.splash-logo. - State conventions:
.is-active,.is-open,.is-visible,.is-disabled,.is-error,aria-pressed,data-tone,data-state,data-path,data-reason,data-pending,data-large,data-muted,[hidden].
Persona: Battery Onboarding Operator — Tobiloba Adekoya (“TA”), tobiloba.adekoya@rechaj.com, Lagos Hub · Bench 1. Adjacent roles referenced: Adaeze Okwu (Battery Identity Admin), Procurement Manager, Product Manager (PM).
ID formats:
- Session:
OB-DDMMYY-NNN(e.g.OB-210526-008) - Serial:
SN-44-NNNNNN(e.g.SN-44-091246) - BMS:
BMS-SE-XXXXX(e.g.BMS-SE-78225, hex-ish suffix) - Shipment:
SHP-DDMMYY-NNN(e.g.SHP-210526-002) - Quarantine:
QTN-OB-DDMMYY-NNN - Events:
EVT-OB-A85NN,EVT-RCV-NNN01 - Screen ids:
s-<kebab>(s-splash, s-onboarding, s-welcome, s-dashboard, s-sessions, s-session-detail, s-quarantine, s-onb-quarantine-detail, s-profile, s-camera-evidence). Sheet ids:sheet-<kebab>. Element ids:bo-<kebab>. - Identity states in copy:
REGISTERED_UNASSIGNED,WRITTEN_OFF; event names dot-namespaced lower_snake:battery.received,battery.onboarded,battery.onboarding_failed,onboarding.in_progress. - Middot
·is the universal meta separator (never em dash in UI copy; the file uses “—” only inside sentence-length sheet subtitles and placeholders, see §7).
7. Weight & spacing discipline
Section titled “7. Weight & spacing discipline”Where 700 appears (exhaustive list)
Section titled “Where 700 appears (exhaustive list)”Sanctioned (display numbers / display titles / iOS chrome):
.sb-time15px/700 — iOS clock (allowed).rsm-stat-num20px/700 — KPI digits (allowed).bo-kpi-num20px/700 — KPI digits (explicitly annotated in-file as the allowed use).ob-brand-name22px/700,.ob-title24px/700,.welcome-title28px/700 — splash/onboarding/welcome display titles (allowed).rsm-hero-avatar16px/700 — avatar initials. Borderline: memory/global rule says avatars default 600 (.rsm-profile-avataris correctly 600/20px); the 44px hero avatar at 700 is an internal inconsistency..rsm-hero-name17px/700 — violation of the “names are 600” rule as written, though it is copied verbatim from the RSM shell (precedent). Flag when reusing: prefer 600 unless matching this exact hero.
Everything else (section titles, panel titles, buttons, pills, row titles, sheet titles, page titles) is 600; body/meta 400–500. strong in log text = 600.
Spacing rhythm
Section titled “Spacing rhythm”- Horizontal page gutter: 16px (
.rsm-body, hero, stats margin, tabs margin); sheets use 24px gutters; auth screens 24px. - Vertical rhythm:
.rsm-bodygap 18px between sections; card lists gap 10px; in-card stacks gap 12px (panels), 8px (kv), 10px (steps), 12px (log); segmented control gap 4px inside 4px padding. - Card padding: 14px (stat/kpi/alert/status-hero/rows at 12×14) or 16px (panels, profile-user, path tiles).
- Radii scale: 8 (buttons/inputs) · 10 (notes textarea, profile row-ic) · 12 (nested cards, evidence thumbs, sim buttons, tabs track) · 14 (list rows, capture tiles) · 16 (cards/panels/scan stage) · 24 (sheets top corners, viewfinder) · 32 (phone) · 100px/999/50% (pills, circles).
- Section heads compensate body gap with
padding: 4px 0+.rsm-section-head + * { margin-top: -10px; }; extra 8pxmargin-topinline between dashboard sections. - Inline style usage is limited to:
padding-topoverrides on.rsm-body(18px dashboard, 4px others),margin-top: 8pxon later section heads, pane flex layout, sb positioning on overlay screens, and the one-off “Why it failed” paragraph (13.5px/500) — acceptable one-offs but note them when templating.
Violations / off-grid values noticed within the file itself
Section titled “Violations / off-grid values noticed within the file itself”.rsm-hero-name17px/700 and.rsm-hero-avatar700 — names/avatars should be 600 per the global rule (inherited verbatim from RSM shell)..rsm-hero-rolecontent is uppercase in markup (“BATTERY ONBOARDING”) while the global badge rule says no uppercase — but.bo-kv-labeland.rsm-profile-section-titlelegitimately usetext-transform: uppercaseas micro-labels, a separate pattern.- Off-4px-grid values: many half-pixel font sizes (10.5/11.5/12.5/13.5/14.5px) — established mobile-type scale, not spacing, so tolerated; spacing itself keeps to 2/4/6/8/10/12/14/16/18/24 (10/14/18 are off strict-4 but pervasive canon here); odd ones:
padding: 9px 6pxon.bo-tab(9 off-grid),.bo-fab bottom: 96px,top: 56pxtoast,left: 56px/60pxdivider insets,padding: 5px 12pxscan hint, sbgap: 5px,.home-ind134×5. - Em dashes appear in sheet subtitles/placeholders and JS data copy (“BMS unresponsive — no CAN…”, “Notes (optional) — what you observed…”) — conflicts with the no-em-dash copy rule; UI labels themselves use the middot.
.bo-session-pill/.bo-qtn-routeat 10.5px/600 fully-round diverge from the global.ds-badge12px/500/6px-radius spec — treat the mobile pill spec here as the mobile canon since this file is the source of truth..btn-primarydisabled text usesrgba(17,17,17,0.45)—#111111base rather than--text-main#0E0400(minor token drift).- The dashboard’s “Stuck >24h” KPI uses the green icon while its own alert list flags a stuck battery — tone choice inconsistency in data, not CSS.
Type scale summary (for reuse)
Section titled “Type scale summary (for reuse)”- Display: 28/700 (welcome), 24/700 (ob title), 22/700 (brand), 20/700 (KPI digits)
- Page title: 20/600; sheet title: 20/600
- Card/section titles: 15/600 (section), 14.5/600 (status hero, path tile, camera title, profile row-label at 500), 14/600 (panel title, capture heading)
- Row titles: 13.5/600 (session id, alert, step); 13/600 (activity, qtn id)
- Body/meta: 13/500 (log, kpi-label, tab), 12.5/500-600, 12/400-500 (subs, metas), 11.5/400-500 (fine meta), 11/500-600, 10.5/600 (pills), 10/500 (nav labels), 9.5/600 (thumb label)
- Buttons: 16/600; inputs 16/400