Skip to content

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.


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: #FEF3C7 bg / #B45309 glyph
  • Red tint pair: #FEE2E2 bg / #B91C1C glyph (destructive button solid #B91C1C, hover #991B1B, disabled #FECACA)
  • Blue tint pair: #E0F2FE bg / #075985 glyph; alt blue rgba(30,64,175,0.08–0.12) bg / #1E40AF glyph (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) and rgba(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;
}

  • Device: 390 × 844, radius 32px, on dark #0E0F12 full-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 .phone down with transform: scale() when the window is smaller than 390×844 (never scrolls the body).
.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.

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);
})();

.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).

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.

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

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.

.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).

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).

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

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;
}
.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').

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

None present. No dropdown/popover menu in this app; selections happen via bottom-sheet tile pickers (.bo-path-tile).

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>

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) or var(--grey3) bg / --grey10–--grey11 glyph
  • green: var(--g-2) or var(--g-3) bg / --g-dark glyph
  • amber: #FEF3C7 / #B45309
  • red: #FEE2E2 / #B91C1C
  • blue: #E0F2FE / #075985 or rgba(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).


Order: dark status bar (absolute) → centered 80px logo circle → light home indicator. Whole screen clickable → goTo('s-onboarding'); auto-advance 2500ms.

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.

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.

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().

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.

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.

  • 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 (success for completion, error for quarantine routing, info for 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).

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 by OB-… id), ONB_QUARANTINE_ITEMS (keyed by QTN-OB-… id); mutable module-level currentSessionId, 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_SHEET lookup 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 + navigate
  • qtnRowHtml(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_identity
  • confirmFailure(reason) — marks failed, mints QTN-OB-… record, re-renders, error toast, returns to sessions list

Flow actions:

  • startNewSession(path) — mints OB id, seeds session at scan_serial, navigates to detail
  • advanceToReadBms() — step → read_bms, 800ms setTimeout → bmsId assigned, step → validate
  • confirmMapping() — 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.


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).

Sanctioned (display numbers / display titles / iOS chrome):

  • .sb-time 15px/700 — iOS clock (allowed)
  • .rsm-stat-num 20px/700 — KPI digits (allowed)
  • .bo-kpi-num 20px/700 — KPI digits (explicitly annotated in-file as the allowed use)
  • .ob-brand-name 22px/700, .ob-title 24px/700, .welcome-title 28px/700 — splash/onboarding/welcome display titles (allowed)
  • .rsm-hero-avatar 16px/700 — avatar initials. Borderline: memory/global rule says avatars default 600 (.rsm-profile-avatar is correctly 600/20px); the 44px hero avatar at 700 is an internal inconsistency.
  • .rsm-hero-name 17px/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.

  • Horizontal page gutter: 16px (.rsm-body, hero, stats margin, tabs margin); sheets use 24px gutters; auth screens 24px.
  • Vertical rhythm: .rsm-body gap 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 8px margin-top inline between dashboard sections.
  • Inline style usage is limited to: padding-top overrides on .rsm-body (18px dashboard, 4px others), margin-top: 8px on 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”
  1. .rsm-hero-name 17px/700 and .rsm-hero-avatar 700 — names/avatars should be 600 per the global rule (inherited verbatim from RSM shell).
  2. .rsm-hero-role content is uppercase in markup (“BATTERY ONBOARDING”) while the global badge rule says no uppercase — but .bo-kv-label and .rsm-profile-section-title legitimately use text-transform: uppercase as micro-labels, a separate pattern.
  3. 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 6px on .bo-tab (9 off-grid), .bo-fab bottom: 96px, top: 56px toast, left: 56px/60px divider insets, padding: 5px 12px scan hint, sb gap: 5px, .home-ind 134×5.
  4. 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.
  5. .bo-session-pill/.bo-qtn-route at 10.5px/600 fully-round diverge from the global .ds-badge 12px/500/6px-radius spec — treat the mobile pill spec here as the mobile canon since this file is the source of truth.
  6. .btn-primary disabled text uses rgba(17,17,17,0.45) — #111111 base rather than --text-main #0E0400 (minor token drift).
  7. 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.
  • 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