Skip to content

DESIGN

Multi-persona retrofit station app. RSM, Team Lead, Retrofit Tech, Auto-mech, QA Technician — same app, role-aware dashboards. Built on the canonical .phone mobile shell + rsm-* scaffold.

(Why this product looks the way it does. Audience, primary jobs-to-be-done, the visual decisions that diverge from the global system and why.)

Start here when adding a new screen or extending a persona. All these patterns are inherited verbatim from the master DESIGN.md — read them first:

The remainder of this file covers retrofit-specific extensions: typography lock-ins, the 3-photo-slot retrofit-tech capture pattern, MEI v2 weighted scoring, status timeline ordering, list picker usage.

Cards never carry a visible border or stroke — shadow + background only. Inherited from the master DESIGN.md and restated here so it isn’t lost when this product’s CSS gets extended.

  • ❌ border: 1px solid var(--grey3) (or any colour) on any card, readonly tile, sheet, row, or grouping container.
  • ❌ Single-side accent stripes — no border-left, border-top, border-right, or border-bottom to colour-code state. Use the card’s background tint alone to communicate state (amber bg = warning, red bg = fail, etc.).
  • ✅ background: <neutral or tinted bg> + box-shadow: var(--sh-card) + border-radius: 14px (or the matching radius token).

Sibling spacing — labels hug their content, similar elements 16px, different 24px+

Section titled “Sibling spacing — labels hug their content, similar elements 16px, different 24px+”

Inherited from the master spacing rule. Restated here because every retrofit screen has stacks of similar cards (MEI components, step cards, session-header sections, crew cards) and getting this wrong reads as visual drift instantly.

  • 8–12px from a section head to its first card. The section head is a label for the content beneath — they should read as a unit. Apply via .rsm-body .rsm-section-head + * { margin-top: -10px; } to pull the next sibling closer than the body’s 18px flex gap.
  • 16px between two or more cards/rows of the same kind in a stack (e.g. session-header cards; the 7 MEI component cards; the 5 retrofit step cards; the crew progress cards). Implement by wrapping the stack in a flex-column container with gap: 16px — do not compose the parent body’s gap with a per-item margin-top.
  • 8px is reserved for tightly grouped same-kind items (a row of photo tiles, adjacent score chips, sub-step pills). Use sparingly.
  • 24px+ between two elements of a different kind on the same screen that are not in a label-to-content relationship — hero card → section head, content stack → bottom CTA block. Pick higher values (32–48px) for stronger visual separation when a section ends and another begins.

Inherits from the global DESIGN.md typography rules and locks them in for this product:

  • 400 / 500 — body copy, captions, metadata, labels (e.g. BUNDLE SCANNED, Forgot password?).
  • 600 (semibold) — titles, section heads, names, IDs (WO-2407-1188, plate numbers, customer names), inline numerics (capacity counts, date rows, step numbers, avatars), button labels.
  • 700 (bold) — reserved for:
    • True KPI display numbers (.rsm-stat-num, wallet amounts) where the digit is the focal point of the card.
    • Splash / onboarding / welcome / success display titles (.ob-title, .welcome-title, .activated-title, .ob-brand-name).
    • The simulated iOS status-bar clock (.sb-time) — system chrome only.

Never use 700 for: customer names, WO numbers, profile names, avatar initials, section titles, result band headlines, or any ordinary inline emphasis. If you reach for bold, ask whether 600 + colour contrast already carries the weight.

Status chips (WO state, task state, evidence state) across every persona — RSM, Team Lead, Retrofit Technician, Auto-mech — use exactly three colors:

State Background Text Use for
Pending #FEF3C7 #B45309 Awaiting kit, Awaiting evidence, Pending, Booked
In progress #DBEAFE #1E40AF In progress, With QA, currently being worked on
Completed var(--g-3) var(--g-dark) QA passed, MEI passed, Done, Completed

Use a 4th red variant (#FEE2E2 / #B91C1C) only for failure outcomes (MEI failed, QA failed).

Do not use an “Active” status. “In progress” covers the same meaning — collapse any “Active” pill to “In progress” so the chip vocabulary stays tight.

Canonical class names (apply on .rsm-q-card-status, .rsm-wo-card-status, .rsm-wo-detail-status):

<span class="rsm-q-card-status rsm-q-card-status--pending">Awaiting kit</span>
<span class="rsm-q-card-status rsm-q-card-status--in-progress">In progress</span>
<span class="rsm-q-card-status rsm-q-card-status--completed">QA passed</span>
<span class="rsm-q-card-status rsm-q-card-status--fail">MEI failed</span>

Legacy aliases (--active, --pass, --info) are kept temporarily for backward-compat but are remapped to the canonical colors and should be removed when touched.

Note: this rule does NOT apply to workflow-type chips (e.g. Retrofit / Inspection / Auto-mech / MEI) which use the brand-green and inspection-blue tints to identify the kind of work, not its state. Those keep their existing palette per the Workflow type cell pattern documented above.

See screens/ for canonical interactive HTML mockups.

See prototypes/ for narrated end-to-end flows.

Retrofit Technician — 3-checkpoint × 3-photo-slot capture

Section titled “Retrofit Technician — 3-checkpoint × 3-photo-slot capture”

The Retrofit Technician evidence flow follows the Retrofit Evidence Capture App Surface Specification (docs/attachments/TH-Retrofit Evidence Capture App Surface Specification for Team Lead and Technician-150526-055211.pdf) verbatim.

Three checkpoints, three labelled photo slots each:

# Approved title Slot labels
1 Decommission + bay clearance Empty bay wide shot · Fuel isolation close-up · Storage / extraction proof
2 Mechanical mount + alignment Motor seated / mounted · Alignment view · Fastener / torque proof
3 Electrical integration + live check Harness routing / wiring overview · VCU + DU connection close-up · Live DU proof

Rules:

  • Page header title = short form Checkpoint N (so it doesn’t wrap past the back button). The approved title + helper description live in the body (#step-instr-name + #step-instr-desc).
  • 3 fixed slots, 1 photo each. Each slot is a .rt-slot with a labelled .rt-slot-label underneath the .rt-slot-tile. Captured tiles get a .rt-slot-clear X button. Tap a captured tile to preview; tap X to clear; re-tap to recapture.
  • Helper description never gets shortened or rewritten — it’s quoted from the spec, full sentences, and wrapped in a white card (no stroke; background:var(--white); border-radius:14px; padding:14px 16px; box-shadow:var(--sh-card)).
  • Live N-of-3 count + gated save CTA — the Save / Mark checkpoint complete button is disabled until all 3 slots are captured. The “Checkpoint complete” pill (.rt-checkpoint-complete) reveals only after all 3 slots are filled.
  • Per-photo timestamps are stored in stepState[idx].slots[i].time so the Team Lead can show the per-photo timestamp in the read-only proof view.
  • Tech finished is the page-level submit CTA — disabled until all 3 checkpoints are green ("All 3 checkpoints must be green to finish." is the helper line).
  • Notes placeholder: "Anything noteworthy about this checkpoint..." (singular checkpoint, not step).

The Team Lead’s s-crew-detail-retrofit mirrors the checkpoint titles + 3-photo proof structure in read-only form. Tap a thumb to open the photo with its slot label + timestamp.

Camera-only evidence — no infrastructure hints

Section titled “Camera-only evidence — no infrastructure hints”

Capture screens (retrofit checkpoints, auto-mech steps, QA Technician sub-checks, Team Lead retrofit/auto-mech evidence) never surface platform invariants like “EXIF + SHA-256 stamped on every shot” / “in-app camera only” / “no gallery import” as helper copy. These are infrastructure, not user-actionable rules.

Keep behavioural copy that adjacent to the camera tile — "Mandatory before pass or defect", "1 photo from Production QA", "View only after submission." — but strip pure-infra fragments. If a hint mixes both (e.g. "Camera-only · EXIF + SHA-256 stamped on every shot · view only after submission."), keep only the behavioural part ("View only after submission.").

Audit grep: grep -rn 'EXIF\|SHA-256\|In-app camera only' flows/ must return zero hits.

The Auto-mech inspection flow defines the MEI v2 weighted composite model. Three persona screens render it (s-completed-inspection-detail in auto-mech, s-workflow-detail-inspection-completed + s-workflow-detail-inspection in RSM, s-completed-inspection + s-inspection-active + inspection-result-sheet in Team Lead) — all three must use the same components, weights, format, and band logic.

Components (in canonical order, weights sum to 100):

const MEI_LABELS = {
frame: 'Frame integrity', // 25%
electrical: 'Electrical wiring', // 20%
brake: 'Brake system', // 15%
suspension: 'Suspension & axle', // 15%
tire: 'Tire condition', // 10%
age: 'Keke age & model', // 10%
modifications: 'Modification history', // 5%
};
const MEI_ORDER = ['frame', 'electrical', 'brake', 'suspension', 'tire', 'age', 'modifications'];
const MEI_WEIGHTS = { frame: 25, electrical: 20, brake: 15, suspension: 15, tire: 10, age: 10, modifications: 5 };
function meiComposite(scores) {
return MEI_ORDER.reduce((a, k) =>
a + (MEI_WEIGHTS[k] / 100) * (typeof scores[k] === 'number' ? scores[k] : 0), 0);
}

Bands (composite on a 1–5 scale):

Letter Range Outcome Pill
A ≥ 4.0 Fast-track to retrofit queue Proceed
B ≥ 3.0 Retrofit with minor remediation · AM parts hold activated Proceed
C ≥ 2.0 Mandatory remediation before retrofit queue entry Conditional
D ≥ 1.5 VP approval required before retrofit Escalate
F < 1.5 Reject — rider receives deficiency report; return after repair Reject

Display format on every record screen:

<div class="rsm-result-band" data-result="pass">
<div class="rsm-result-band-text">
<span class="rsm-result-band-label">Final MEI composite</span>
<span class="rsm-result-band-score">3.85 / 5 · Band B</span>
</div>
<span class="rsm-result-band-pill">Proceed</span>
</div>
<p class="rsm-result-band-outcome">Retrofit with minor remediation · AM parts hold activated.</p>

Per-component score chips render as X.X / 5 (1 decimal) since subject scores are averages of sub-step scores. Use --low (amber) when score ≤ 3 with caveats, --fail (red) when ≤ 2.

Don’t:

  • Mix the old 7 components (Engine condition / Frame & chassis / Brakes / Suspension / Steering / Electrical & wiring / Body & cosmetic) — those are legacy and must not reappear.
  • Surface X / 100 percentage form (e.g. 88 / 100 · Band B) — always the X.XX / 5 · Band X form.
  • Use a generic Pass / Fail pill — always use the band-specific action pill (Proceed / Conditional / Escalate / Reject).

Read-only evidence sheet + edit-submission affordance

Section titled “Read-only evidence sheet + edit-submission affordance”

Completed retrofit checkpoints, auto-mech steps, and QA sub-checks all open as view-only bottom sheets so the Team Lead / QA can audit without re-entering the capture path. The pattern matches the kit-production sheet-evidence + .ka-evi-* treatment: a result chip + read-only photo thumbs + meta + optional note. No EXIF/SHA-256 caption on the meta line — "View only after submission." is the canonical copy.

The retrofit Team Lead’s image-preview sheet (#step-image-sheet) carries the same audit role for checkpoint photos: open a thumb → see the slot label, capture time, and actor in a .rsm-image-meta block.

Activated-sheet success modal — full-screen, not bottom-sheet

Section titled “Activated-sheet success modal — full-screen, not bottom-sheet”

Success confirmations that mark a major state transition use the canonical full-screen activated-sheet shell, not a bottom sheet. Examples in this app:

  • s-account-activated — account created
  • s-password-reset — password reset successful
  • s-queue-assigned — retrofit sent to QA
  • s-chassis-escalated — invalid chassis escalated to RSM

Shell:

<div class="screen" id="s-some-success">
<div class="activated-bg" aria-hidden="true"></div>
<div class="sb sb--dark" style="position:absolute;top:0;left:0;right:0;z-index:3;"><!-- status bar --></div>
<div class="activated-sheet">
<div class="activated-pill"></div>
<div class="activated-ic"><!-- green check --></div>
<h2 class="activated-title">Escalated to RSM</h2>
<p class="activated-sub">The invalid-chassis WO has been routed to the RSM for review.</p>
<button class="btn-primary btn-primary--lg" style="width:100%;" type="button" onclick="closeSuccess()">Done</button>
</div>
<div class="home-ind"></div>
</div>

Register the screen ID in the SCREENS array so goTo() routes to it. The Done CTA should reset any local state (clear inputs, clear in-flight photo, etc.) before navigating back to the queue / home tab.

Don’t put a major-transition confirmation in a bottom sheet — bottom sheets are for picker/confirmation patterns where the user might cancel. Once a WO is escalated / a kit is sent to QA / a password is reset, that’s a completed action and deserves the full-screen treatment.

Inline error sheet — bottom sheet for recoverable failures

Section titled “Inline error sheet — bottom sheet for recoverable failures”

For recoverable input errors (e.g. invalid chassis number, plate mismatch), use a bottom sheet with two CTAs — one primary path forward, one secondary “try again”:

  • Title in red (color: #B91C1C;)
  • Red status banner echoing the typed value + a short reason (var(--red3) background, alert-triangle icon in var(--red9))
  • Primary CTA: the forward path (“Escalate to RSM”, “Send for review”)
  • Secondary CTA: the retry path (“Re-enter chassis number”)
  • gap: 10px between CTAs; padding: 20px 0 24px on the CTA stack to space it off the body

Primary forward CTA opens an activated-sheet success modal once committed. Secondary retry closes the sheet, clears the relevant input + state, and re-focuses.

Use the canonical .lp-* list picker for any single-select. In this app: assign rider to workstation, pick a zone, pick a shift.

Pattern: rows with optional 36×36 icon → label + sub-label → radio (right). Selected row has green border + light-green tint background; the matching radio fills with a green dot. Disabled rows go to 45% opacity and lose pointer events.

Drop-in markup:

<div class="lp-group" role="radiogroup">
<button class="lp-item" type="button" role="radio" aria-checked="false" onclick="pickAssign(this)">
<div class="lp-content">
<div class="lp-label">Workstation 5</div>
<div class="lp-sublabel">Lead: Ifeoluwa Olukoga · Idle</div>
</div>
<span class="lp-radio"><span class="lp-radio-dot"></span></span>
</button>
<button class="lp-item" type="button" role="radio" aria-checked="false" aria-disabled="true" disabled>
<div class="lp-content">
<div class="lp-label">Workstation 1</div>
<div class="lp-sublabel">Lead: Babajide Hassan · 1/1 active</div>
</div>
<span class="lp-radio"><span class="lp-radio-dot"></span></span>
</button>
</div>

Drop-in JS:

function pickAssign(btn) {
const group = btn.closest('.lp-group');
if (!group) return;
group.querySelectorAll('.lp-item').forEach(b => {
const active = b === btn;
b.classList.toggle('is-selected', active);
b.setAttribute('aria-checked', active ? 'true' : 'false');
});
}

Don’ts:

  • ❌ Use box-shadow rings instead of border-color for selection state.
  • ❌ Skip the radio — it’s the canonical affordance.
  • ❌ Mix iconed and non-iconed rows in the same group — labels won’t align.

Use the canonical global Status timeline for any state-machine-style progress list. In this app: the WO lifecycle log on the Team Lead’s Active WO screen, the RSM Status timeline on the workflow detail screen, and any future per-WO history view.

Product-scoped class prefix: .rsm-wo-step-* (matches the generic .tl-step-* structure exactly — class name only; CSS, states, and markup are identical).

Lifecycle events the WO log surfaces (all others belong inside the relevant crew detail screen, never the lifecycle log):

  1. Assigned to workstation
  2. WO accepted at workstation
  3. Chassis validated
  4. Kit requested from Inventory
  5. Inventory delivered bundle
  6. Bundle scanned · timers started
  7. Retrofit started · Auto-mech started (concurrent — both timers begin at scan-in)
  8. Retrofit finished · Auto-mech finished
  9. Retrofit evidence captured · Auto-mech evidence captured
  10. Retrofit complete · sent to QA

Don’t log per-step crew captures (Engine removed, brake pads replaced, harness installed, etc.) in the WO timeline — those live inside the Retrofit / Auto-mech crew detail screens.

The canonical pattern lives in the global DESIGN.md → Alert Toast. The full prototype-flavored snippet is duplicated here so you don’t need to round-trip during a build.

Pattern: tinted pill, leading white badge, message, close button. Auto-dismiss 4s. No icons, no emojis. Top-center anchored.

Do use this pattern for save confirmations (Save changes → “Saved · Today’s slots updated”), publish events, errors, warnings.

Don’t use a dark capsule with an icon circle, bottom-of-screen toasts, or omit the close button.

Mount this once at the .phone level, after all screens. The variant class is set by showToast():

<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">Today's slots updated</span>
<button class="alert-toast-close" type="button" aria-label="Dismiss" onclick="hideToast()">
<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>

Note position: absolute (not fixed) — .phone has overflow: hidden, so absolute pins it to the device frame, not the page viewport. top: 56px clears the status bar.

.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; white-space: nowrap;
}
.alert-toast-text { font-size: 13px; font-weight: 500; line-height: 20px; white-space: nowrap; }
.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--warning { background: #fff7c2; color: #ab6400; }
.alert-toast--error { background: #ffebe8; color: #e32627; }
.alert-toast--gray { background: #f0f0f0; color: #646464; }
const TOAST_VARIANTS = ['success', 'info', 'warning', 'error', 'gray'];
let _toastTimer = null;
function showToast(message, badge, variant) {
const t = document.getElementById('toast');
if (!t) return;
variant = TOAST_VARIANTS.indexOf(variant) >= 0 ? variant : 'success';
TOAST_VARIANTS.forEach(v => t.classList.remove('alert-toast--' + v));
t.classList.add('alert-toast--' + variant);
if (badge !== undefined) t.querySelector('.alert-toast-badge').textContent = badge;
if (message) t.querySelector('.alert-toast-text').textContent = message;
t.classList.add('is-visible');
clearTimeout(_toastTimer);
_toastTimer = setTimeout(hideToast, 4000);
}
function hideToast() {
const t = document.getElementById('toast');
if (!t) return;
t.classList.remove('is-visible');
clearTimeout(_toastTimer);
}

Usage: showToast("Today's slots updated", 'Saved', 'success'). Variant defaults to success if omitted.