Auto — Customer Service Portal
A customer-facing auto service portal where drivers manage their garage, watch live repair status, and act on the shop. Switch between vehicles, follow a check-in to ready progress track with technician notes and diagnostic codes, review and authorize a work-order estimate broken into labor and parts, book new appointments, see upcoming maintenance reminders, and download past invoices. Industrial steel-and-orange styling, status panels, bay numbers, VIN and odometer readouts, and toast feedback throughout.
MCP
Code
:root {
--garage: #141518;
--garage-2: #1f2127;
--steel: #5b6470;
--steel-l: #8a929d;
--orange: #ff6a13;
--orange-d: #e2540a;
--orange-50: #fff0e6;
--ink: #16181c;
--ink-2: #3b4049;
--muted: #737a85;
--bg: #f3f4f6;
--surface: #ffffff;
--line: rgba(20, 21, 24, 0.1);
--line-2: rgba(20, 21, 24, 0.18);
--ok: #2f9e6f;
--warn: #e0962a;
--danger: #d4493e;
--waiting: #e0962a;
--inprogress: #2b7fff;
--done: #2f9e6f;
--hold: #d4493e;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 18px;
--shadow-sm: 0 1px 2px rgba(20, 21, 24, 0.06), 0 1px 3px rgba(20, 21, 24, 0.08);
--shadow-md: 0 4px 14px rgba(20, 21, 24, 0.1);
--shadow-lg: 0 18px 50px rgba(20, 21, 24, 0.22);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
background: var(--bg);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2, h3, p { margin: 0; }
.tnum { font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }
button { font-family: inherit; cursor: pointer; }
/* ---------- Top bar ---------- */
.topbar {
position: sticky;
top: 0;
z-index: 20;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 12px 22px;
background: var(--garage);
color: #fff;
border-bottom: 3px solid var(--orange);
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand-mark {
display: grid;
place-items: center;
width: 40px;
height: 40px;
border-radius: var(--r-sm);
background: linear-gradient(135deg, var(--orange), var(--orange-d));
color: #fff;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.15);
}
.brand-text { display: flex; flex-direction: column; line-height: 1.15; }
.brand-text strong { font-size: 16px; font-weight: 800; letter-spacing: -0.01em; }
.brand-text span { font-size: 11px; color: var(--steel-l); text-transform: uppercase; letter-spacing: 0.12em; font-weight: 600; }
.topbar-right { display: flex; align-items: center; gap: 12px; }
.ghost-btn {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 9px 14px;
border-radius: var(--r-sm);
border: 1px solid var(--line-2);
background: var(--surface);
color: var(--ink);
font-size: 13px;
font-weight: 600;
transition: background 0.15s, border-color 0.15s, transform 0.1s;
}
.ghost-btn:hover { background: var(--bg); border-color: var(--steel); }
.ghost-btn:active { transform: translateY(1px); }
.topbar .ghost-btn {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.16);
color: #fff;
}
.topbar .ghost-btn:hover { background: rgba(255, 255, 255, 0.16); }
.account {
display: flex;
align-items: center;
gap: 9px;
padding: 5px 12px 5px 6px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.07);
outline: none;
transition: background 0.15s;
}
.account:hover, .account:focus-visible { background: rgba(255, 255, 255, 0.14); }
.account-name { font-size: 13px; font-weight: 600; }
.avatar {
display: grid;
place-items: center;
width: 30px;
height: 30px;
border-radius: 999px;
background: linear-gradient(135deg, var(--steel), var(--steel-l));
color: #fff;
font-size: 12px;
font-weight: 700;
}
/* ---------- Layout ---------- */
.layout {
display: grid;
grid-template-columns: 290px 1fr;
gap: 22px;
max-width: 1240px;
margin: 0 auto;
padding: 22px;
}
/* ---------- Sidebar ---------- */
.sidebar { display: flex; flex-direction: column; gap: 14px; }
.sidebar-head {
display: flex;
align-items: center;
justify-content: space-between;
}
.sidebar-head h2 { font-size: 13px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--steel); font-weight: 700; }
.pill {
font-size: 11px;
font-weight: 700;
color: var(--steel);
background: var(--surface);
border: 1px solid var(--line);
padding: 3px 9px;
border-radius: 999px;
}
.vehicle-list { display: flex; flex-direction: column; gap: 10px; }
.vehicle-card {
display: flex;
gap: 12px;
align-items: center;
text-align: left;
width: 100%;
padding: 11px;
border-radius: var(--r-md);
border: 1px solid var(--line);
background: var(--surface);
box-shadow: var(--shadow-sm);
transition: border-color 0.15s, transform 0.12s, box-shadow 0.15s;
}
.vehicle-card:hover { transform: translateY(-1px); box-shadow: var(--shadow-md); }
.vehicle-card.active { border-color: var(--orange); box-shadow: 0 0 0 1px var(--orange), var(--shadow-md); }
.vehicle-thumb {
flex: none;
width: 56px;
height: 44px;
border-radius: var(--r-sm);
background: linear-gradient(135deg, var(--garage-2), var(--steel));
position: relative;
overflow: hidden;
}
.vehicle-thumb::after {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(120% 90% at 80% 10%, rgba(255, 255, 255, 0.22), transparent 60%);
}
.vehicle-thumb.v1 { background: linear-gradient(135deg, #2b3a55, #4a6fa5); }
.vehicle-thumb.v2 { background: linear-gradient(135deg, #3a2c2c, #8a5a3a); }
.vehicle-thumb.v3 { background: linear-gradient(135deg, #1f3a32, #3f8a6a); }
.vehicle-meta { min-width: 0; flex: 1; }
.vehicle-meta .vname { font-size: 14px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vehicle-meta .vplate { font-size: 11px; color: var(--muted); font-weight: 600; letter-spacing: 0.04em; }
.dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 999px;
margin-right: 6px;
vertical-align: middle;
}
.add-vehicle {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 7px;
padding: 11px;
border-radius: var(--r-md);
border: 1.5px dashed var(--line-2);
background: transparent;
color: var(--steel);
font-weight: 600;
font-size: 13px;
transition: border-color 0.15s, color 0.15s, background 0.15s;
}
.add-vehicle:hover { border-color: var(--orange); color: var(--orange-d); background: var(--orange-50); }
/* ---------- Main ---------- */
.main { display: flex; flex-direction: column; gap: 18px; min-width: 0; }
.vehicle-hero {
display: flex;
gap: 20px;
padding: 20px;
border-radius: var(--r-lg);
background: linear-gradient(120deg, var(--garage), var(--garage-2));
color: #fff;
box-shadow: var(--shadow-md);
position: relative;
overflow: hidden;
}
.vehicle-hero::before {
content: "";
position: absolute;
right: -40px;
top: -40px;
width: 200px;
height: 200px;
background: radial-gradient(circle, rgba(255, 106, 19, 0.22), transparent 70%);
}
.hero-photo {
flex: none;
width: 200px;
height: 130px;
border-radius: var(--r-md);
position: relative;
overflow: hidden;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12);
}
.hero-photo::after {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(130% 100% at 75% 0%, rgba(255, 255, 255, 0.25), transparent 55%),
linear-gradient(180deg, transparent 60%, rgba(0, 0, 0, 0.35));
}
.hero-photo.v1 { background: linear-gradient(135deg, #2b3a55, #4a6fa5); }
.hero-photo.v2 { background: linear-gradient(135deg, #3a2c2c, #8a5a3a); }
.hero-photo.v3 { background: linear-gradient(135deg, #1f3a32, #3f8a6a); }
.hero-info { flex: 1; min-width: 0; z-index: 1; display: flex; flex-direction: column; gap: 10px; }
.hero-title-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.hero-info h1 { font-size: 23px; font-weight: 800; letter-spacing: -0.02em; }
.hero-sub { font-size: 13px; color: var(--steel-l); font-weight: 500; }
.status-chip {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 4px 11px;
border-radius: 999px;
color: #fff;
}
.status-chip.s-inprogress { background: var(--inprogress); }
.status-chip.s-waiting { background: var(--waiting); color: var(--garage); }
.status-chip.s-done { background: var(--done); }
.status-chip.s-hold { background: var(--hold); }
.status-chip.s-ready { background: var(--orange); }
.hero-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
margin-top: auto;
padding-top: 8px;
}
.hero-stats .stat span { display: block; font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--steel-l); font-weight: 600; }
.hero-stats .stat strong { font-size: 15px; font-weight: 700; }
/* ---------- Panels ---------- */
.panel {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 18px;
box-shadow: var(--shadow-sm);
}
.panel-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 14px;
}
.panel-head h2 { font-size: 15px; font-weight: 700; letter-spacing: -0.01em; }
.bay-tag {
font-size: 12px;
font-weight: 700;
color: var(--orange-d);
background: var(--orange-50);
border: 1px solid rgba(226, 84, 10, 0.25);
padding: 4px 10px;
border-radius: var(--r-sm);
}
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
/* ---------- Active service ---------- */
.progress-track {
display: flex;
align-items: center;
margin: 6px 0 18px;
}
.step { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 7px; position: relative; }
.step:not(:last-child)::after {
content: "";
position: absolute;
top: 13px;
left: 50%;
width: 100%;
height: 3px;
background: var(--line);
z-index: 0;
}
.step.complete:not(:last-child)::after { background: var(--done); }
.step-dot {
width: 28px;
height: 28px;
border-radius: 999px;
background: var(--surface);
border: 3px solid var(--line);
display: grid;
place-items: center;
z-index: 1;
color: transparent;
font-size: 13px;
font-weight: 800;
}
.step.complete .step-dot { background: var(--done); border-color: var(--done); color: #fff; }
.step.current .step-dot {
border-color: var(--orange);
background: var(--orange-50);
color: var(--orange-d);
box-shadow: 0 0 0 4px rgba(255, 106, 19, 0.18);
}
.step-label { font-size: 11px; font-weight: 600; color: var(--muted); text-align: center; }
.step.current .step-label, .step.complete .step-label { color: var(--ink); }
.service-note {
display: flex;
gap: 10px;
align-items: flex-start;
padding: 12px 14px;
border-radius: var(--r-md);
background: var(--bg);
border: 1px solid var(--line);
font-size: 13px;
color: var(--ink-2);
}
.service-note .tech {
flex: none;
width: 34px;
height: 34px;
border-radius: 999px;
background: linear-gradient(135deg, var(--garage-2), var(--steel));
color: #fff;
display: grid;
place-items: center;
font-size: 12px;
font-weight: 700;
}
.service-note b { color: var(--ink); }
.dtc {
display: inline-block;
font-weight: 700;
color: var(--danger);
background: rgba(212, 73, 62, 0.1);
padding: 1px 6px;
border-radius: 5px;
font-variant-numeric: tabular-nums;
}
.quote-banner {
margin-top: 14px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
padding: 14px 16px;
border-radius: var(--r-md);
background: var(--orange-50);
border: 1px solid rgba(226, 84, 10, 0.3);
}
.quote-banner .qb-text strong { display: block; font-size: 14px; }
.quote-banner .qb-text span { font-size: 12px; color: var(--ink-2); }
.quote-banner .qb-amount { font-size: 18px; font-weight: 800; color: var(--orange-d); }
.approved-flag {
margin-top: 14px;
display: flex;
align-items: center;
gap: 8px;
padding: 12px 14px;
border-radius: var(--r-md);
background: rgba(47, 158, 111, 0.1);
border: 1px solid rgba(47, 158, 111, 0.3);
color: var(--ok);
font-size: 13px;
font-weight: 600;
}
/* ---------- Buttons ---------- */
.primary-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 11px 16px;
border-radius: var(--r-md);
border: none;
background: linear-gradient(135deg, var(--orange), var(--orange-d));
color: #fff;
font-size: 13px;
font-weight: 700;
box-shadow: 0 4px 12px rgba(226, 84, 10, 0.3);
transition: transform 0.1s, box-shadow 0.15s, filter 0.15s;
}
.primary-btn:hover { filter: brightness(1.05); box-shadow: 0 6px 18px rgba(226, 84, 10, 0.36); }
.primary-btn:active { transform: translateY(1px); }
.primary-btn:focus-visible, .ghost-btn:focus-visible { outline: 3px solid rgba(43, 127, 255, 0.45); outline-offset: 2px; }
#bookBtn { width: 100%; margin-top: 14px; }
/* ---------- Reminders ---------- */
.reminder-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.reminder {
display: flex;
align-items: center;
gap: 12px;
padding: 11px 12px;
border-radius: var(--r-md);
border: 1px solid var(--line);
background: var(--surface);
}
.reminder .r-icon {
flex: none;
width: 36px;
height: 36px;
border-radius: var(--r-sm);
display: grid;
place-items: center;
background: var(--bg);
color: var(--steel);
}
.reminder.due .r-icon { background: rgba(224, 150, 42, 0.14); color: var(--warn); }
.reminder.overdue .r-icon { background: rgba(212, 73, 62, 0.12); color: var(--danger); }
.reminder .r-body { flex: 1; min-width: 0; }
.reminder .r-body strong { font-size: 13px; display: block; }
.reminder .r-body span { font-size: 11.5px; color: var(--muted); }
.r-badge {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 3px 8px;
border-radius: 999px;
color: var(--steel);
background: var(--bg);
}
.r-badge.due { color: var(--warn); background: rgba(224, 150, 42, 0.14); }
.r-badge.overdue { color: var(--danger); background: rgba(212, 73, 62, 0.12); }
/* ---------- History ---------- */
.history-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.history-row {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 4px;
border-bottom: 1px solid var(--line);
}
.history-row:last-child { border-bottom: none; }
.history-row .h-date {
flex: none;
width: 52px;
text-align: center;
}
.history-row .h-date .hd-day { font-size: 17px; font-weight: 800; line-height: 1; }
.history-row .h-date .hd-mon { font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); font-weight: 700; }
.history-row .h-body { flex: 1; min-width: 0; }
.history-row .h-body strong { font-size: 13px; display: block; }
.history-row .h-body span { font-size: 11.5px; color: var(--muted); }
.history-row .h-right { text-align: right; display: flex; flex-direction: column; align-items: flex-end; gap: 5px; }
.history-row .h-amount { font-size: 14px; font-weight: 700; }
.invoice-btn {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 11px;
font-weight: 700;
color: var(--steel);
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-sm);
padding: 4px 9px;
transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.invoice-btn:hover { color: var(--orange-d); border-color: var(--orange); background: var(--orange-50); }
/* ---------- Modal ---------- */
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(20, 21, 24, 0.55);
backdrop-filter: blur(2px);
display: grid;
place-items: center;
padding: 20px;
z-index: 50;
animation: fade 0.18s ease;
}
.modal-backdrop[hidden] { display: none; }
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
.modal {
width: 100%;
max-width: 480px;
background: var(--surface);
border-radius: var(--r-lg);
box-shadow: var(--shadow-lg);
padding: 22px;
animation: pop 0.2s cubic-bezier(0.16, 1, 0.3, 1);
max-height: 90vh;
overflow: auto;
}
@keyframes pop { from { transform: translateY(14px) scale(0.98); opacity: 0; } to { transform: none; opacity: 1; } }
.modal-head { display: flex; align-items: center; justify-content: space-between; }
.modal-head h2 { font-size: 18px; font-weight: 800; letter-spacing: -0.01em; }
.modal-sub { font-size: 13px; color: var(--muted); margin: 4px 0 16px; }
.icon-btn {
display: grid;
place-items: center;
width: 34px;
height: 34px;
border-radius: var(--r-sm);
border: 1px solid var(--line);
background: var(--surface);
color: var(--steel);
transition: background 0.15s, color 0.15s;
}
.icon-btn:hover { background: var(--bg); color: var(--ink); }
.wo-table { border: 1px solid var(--line); border-radius: var(--r-md); overflow: hidden; }
.wo-line {
display: grid;
grid-template-columns: 1fr auto;
gap: 10px;
padding: 11px 13px;
border-bottom: 1px solid var(--line);
font-size: 13px;
align-items: center;
}
.wo-line:last-child { border-bottom: none; }
.wo-line .wl-name strong { font-weight: 600; }
.wo-line .wl-tag {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-left: 8px;
padding: 2px 6px;
border-radius: 5px;
color: var(--steel);
background: var(--bg);
}
.wo-line .wl-tag.parts { color: var(--inprogress); background: rgba(43, 127, 255, 0.1); }
.wo-line .wl-tag.labor { color: var(--orange-d); background: var(--orange-50); }
.wo-line .wl-amt { font-weight: 700; }
.wo-totals { margin-top: 14px; display: flex; flex-direction: column; gap: 6px; }
.wo-totals .tline { display: flex; justify-content: space-between; font-size: 13px; color: var(--ink-2); }
.wo-totals .tline.grand { font-size: 17px; font-weight: 800; color: var(--ink); padding-top: 8px; border-top: 1px solid var(--line); margin-top: 4px; }
.modal-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px; }
.modal-actions .ghost-btn { background: var(--surface); color: var(--ink); }
/* ---------- Form ---------- */
.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
.field > span { font-size: 12px; font-weight: 600; color: var(--ink-2); }
.field input, .field select, .field textarea {
font-family: inherit;
font-size: 14px;
padding: 10px 12px;
border-radius: var(--r-sm);
border: 1px solid var(--line-2);
background: var(--surface);
color: var(--ink);
resize: vertical;
transition: border-color 0.15s, box-shadow 0.15s;
}
.field input:focus, .field select:focus, .field textarea:focus {
outline: none;
border-color: var(--orange);
box-shadow: 0 0 0 3px rgba(255, 106, 19, 0.16);
}
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
/* ---------- Toast ---------- */
.toast-wrap {
position: fixed;
bottom: 22px;
right: 22px;
display: flex;
flex-direction: column;
gap: 10px;
z-index: 60;
}
.toast {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
border-radius: var(--r-md);
background: var(--garage);
color: #fff;
font-size: 13px;
font-weight: 600;
box-shadow: var(--shadow-lg);
animation: toastin 0.25s cubic-bezier(0.16, 1, 0.3, 1);
max-width: 320px;
}
.toast.ok { border-left: 3px solid var(--ok); }
.toast.info { border-left: 3px solid var(--inprogress); }
.toast.warn { border-left: 3px solid var(--warn); }
.toast.leaving { animation: toastout 0.25s ease forwards; }
@keyframes toastin { from { transform: translateX(40px); opacity: 0; } to { transform: none; opacity: 1; } }
@keyframes toastout { to { transform: translateX(40px); opacity: 0; } }
/* ---------- Responsive ---------- */
@media (max-width: 900px) {
.layout { grid-template-columns: 1fr; }
.sidebar { order: 2; }
.grid-2 { grid-template-columns: 1fr; }
}
@media (max-width: 520px) {
.topbar { padding: 10px 14px; }
.account-name { display: none; }
.ghost-btn span, #contactBtn { font-size: 12px; }
.layout { padding: 14px; gap: 16px; }
.vehicle-hero { flex-direction: column; gap: 14px; }
.hero-photo { width: 100%; height: 150px; }
.hero-stats { grid-template-columns: repeat(3, 1fr); gap: 8px; }
.hero-info h1 { font-size: 20px; }
.field-row { grid-template-columns: 1fr; }
.step-label { font-size: 10px; }
.toast-wrap { left: 14px; right: 14px; bottom: 14px; }
.toast { max-width: none; }
}(function () {
"use strict";
/* ---------- Data (fictional) ---------- */
var vehicles = [
{
id: "v1",
name: "2021 Subaru Outback",
sub: "2.5i Premium · AWD Wagon",
plate: "7KX-2840",
vin: "JF2SKAXC1MH401827",
odo: 58420,
cls: "v1",
status: "inprogress",
bay: 4,
stepIndex: 2,
tech: { initials: "RG", name: "Rosa G." },
note:
'Pulled stored code <span class="dtc">P0301</span> — cylinder 1 misfire. Replaced spark plugs & coil pack, road-testing now.',
quote: {
id: "EST-4471",
amount: 612.4,
lines: [
{ name: "Diagnostic — misfire trace", tag: "labor", amt: 89.0 },
{ name: "Ignition coil pack", tag: "parts", amt: 184.5 },
{ name: "Spark plugs (set of 4)", tag: "parts", amt: 62.0 },
{ name: "Labor — R&R coil + plugs", tag: "labor", amt: 210.0 }
],
approved: false
},
reminders: [
{ name: "Oil & filter change", due: "Due in 740 mi", state: "due" },
{ name: "Cabin air filter", due: "Due Sep 2026", state: "" },
{ name: "State safety inspection", due: "Overdue 11 days", state: "overdue" }
],
history: [
{ d: "12", m: "Mar", title: "Brake pads — front", sub: "Inv #INV-3992 · 57,610 mi", amt: 318.0 },
{ d: "04", m: "Jan", title: "Oil change + rotation", sub: "Inv #INV-3710 · 55,120 mi", amt: 96.5 },
{ d: "18", m: "Oct", title: "Battery replacement", sub: "Inv #INV-3401 · 52,880 mi", amt: 224.0 }
]
},
{
id: "v2",
name: "2018 Ford F-150",
sub: "XLT SuperCrew · 3.5L EcoBoost",
plate: "TRK-9012",
vin: "1FTEW1EG5JFA22019",
odo: 91250,
cls: "v2",
status: "ready",
bay: 2,
stepIndex: 3,
tech: { initials: "DM", name: "Devon M." },
note: "Tire rotation & brake inspection complete. Pads at 6mm front, 7mm rear — all within spec. Ready for pickup.",
quote: null,
reminders: [
{ name: "Transmission fluid", due: "Due in 4,800 mi", state: "" },
{ name: "Spark plugs (6)", due: "Due in 8,200 mi", state: "" }
],
history: [
{ d: "16", m: "Jun", title: "Tire rotation + brake inspect", sub: "Inv #INV-4120 · 91,250 mi", amt: 74.0 },
{ d: "22", m: "Feb", title: "Coolant flush", sub: "Inv #INV-3805 · 88,400 mi", amt: 149.0 }
]
},
{
id: "v3",
name: "2023 Toyota RAV4 Hybrid",
sub: "XLE Premium · AWD",
plate: "EV-5577",
vin: "JTMRWRFV8PD188043",
odo: 19870,
cls: "v3",
status: "waiting",
bay: null,
stepIndex: 0,
tech: { initials: "AK", name: "Amir K." },
note: "Checked in for first scheduled maintenance. Awaiting a free bay — estimated start 1:15 PM.",
quote: null,
reminders: [
{ name: "10k service inspection", due: "Due now", state: "due" },
{ name: "Tire rotation", due: "Due in 130 mi", state: "due" }
],
history: [
{ d: "09", m: "Dec", title: "Delivery PDI + first oil", sub: "Inv #INV-3540 · 5,010 mi", amt: 0.0 }
]
}
];
var STEPS = ["Checked in", "Diagnosis", "In progress", "Ready"];
var STATUS_LABEL = {
inprogress: "In progress",
waiting: "Waiting",
ready: "Ready for pickup",
done: "Done",
hold: "On hold"
};
var STATUS_COLOR = {
inprogress: "var(--inprogress)",
waiting: "var(--waiting)",
ready: "var(--orange)",
done: "var(--done)",
hold: "var(--hold)"
};
var current = vehicles[0];
/* ---------- Helpers ---------- */
function $(s, ctx) { return (ctx || document).querySelector(s); }
function money(n) {
return "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function num(n) { return n.toLocaleString("en-US"); }
function toast(msg, kind) {
var wrap = $("#toastWrap");
var t = document.createElement("div");
t.className = "toast " + (kind || "info");
t.innerHTML = '<span aria-hidden="true">' + (kind === "ok" ? "✓" : kind === "warn" ? "!" : "•") + "</span><span>" + msg + "</span>";
wrap.appendChild(t);
setTimeout(function () {
t.classList.add("leaving");
setTimeout(function () { t.remove(); }, 250);
}, 3200);
}
/* ---------- Sidebar ---------- */
function renderVehicleList() {
var list = $("#vehicleList");
list.innerHTML = "";
vehicles.forEach(function (v) {
var btn = document.createElement("button");
btn.type = "button";
btn.className = "vehicle-card" + (v.id === current.id ? " active" : "");
btn.setAttribute("role", "option");
btn.setAttribute("aria-selected", v.id === current.id ? "true" : "false");
btn.innerHTML =
'<span class="vehicle-thumb ' + v.cls + '"></span>' +
'<span class="vehicle-meta">' +
'<span class="vname">' + v.name + "</span>" +
'<span class="vplate"><span class="dot" style="background:' + STATUS_COLOR[v.status] + '"></span>' +
v.plate + " · " + STATUS_LABEL[v.status] + "</span>" +
"</span>";
btn.addEventListener("click", function () {
if (v.id === current.id) return;
current = v;
renderAll();
toast("Switched to " + v.name, "info");
});
list.appendChild(btn);
});
}
/* ---------- Hero ---------- */
function renderHero() {
$("#heroPhoto").className = "hero-photo " + current.cls;
$("#heroName").textContent = current.name;
var chip = $("#heroStatus");
chip.textContent = STATUS_LABEL[current.status];
chip.className = "status-chip s-" + current.status;
$("#heroSub").textContent = current.sub;
$("#heroStats").innerHTML =
'<div class="stat"><span>Odometer</span><strong class="tnum">' + num(current.odo) + ' mi</strong></div>' +
'<div class="stat"><span>Plate</span><strong class="tnum">' + current.plate + "</strong></div>" +
'<div class="stat"><span>VIN</span><strong class="tnum">…' + current.vin.slice(-6) + "</strong></div>";
}
/* ---------- Active service ---------- */
function renderActive() {
$("#bayTag").textContent = current.bay ? "Bay " + current.bay : "Not yet assigned";
var track = '<div class="progress-track">';
STEPS.forEach(function (label, i) {
var cls = i < current.stepIndex ? "complete" : i === current.stepIndex ? "current" : "";
track +=
'<div class="step ' + cls + '">' +
'<span class="step-dot">' + (i < current.stepIndex ? "✓" : i + 1) + "</span>" +
'<span class="step-label">' + label + "</span></div>";
});
track += "</div>";
var note =
'<div class="service-note"><span class="tech">' + current.tech.initials + "</span>" +
"<div><b>" + current.tech.name + "</b> — " + current.note + "</div></div>";
var extra = "";
if (current.quote && !current.quote.approved) {
extra =
'<div class="quote-banner"><div class="qb-text"><strong>Estimate ' +
current.quote.id + ' needs your approval</strong>' +
"<span>Work is paused until you authorize the repair.</span></div>" +
'<div style="display:flex;align-items:center;gap:14px">' +
'<span class="qb-amount tnum">' + money(current.quote.amount) + "</span>" +
'<button class="primary-btn" id="reviewQuote" type="button">Review estimate</button></div></div>';
} else if (current.quote && current.quote.approved) {
extra =
'<div class="approved-flag"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>' +
"Estimate " + current.quote.id + " authorized · " + money(current.quote.amount) + "</div>";
} else if (current.status === "ready") {
extra =
'<div class="approved-flag"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>' +
"Ready for pickup — no outstanding approvals.</div>";
}
$("#activeService").innerHTML = track + note + extra;
var rq = $("#reviewQuote");
if (rq) rq.addEventListener("click", openQuote);
}
/* ---------- Reminders ---------- */
function renderReminders() {
var ul = $("#reminderList");
ul.innerHTML = "";
current.reminders.forEach(function (r) {
var li = document.createElement("li");
li.className = "reminder " + r.state;
var badge = r.state === "overdue" ? "Overdue" : r.state === "due" ? "Due soon" : "Scheduled";
li.innerHTML =
'<span class="r-icon"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg></span>' +
'<span class="r-body"><strong>' + r.name + "</strong><span>" + r.due + "</span></span>" +
'<span class="r-badge ' + r.state + '">' + badge + "</span>";
ul.appendChild(li);
});
}
/* ---------- History ---------- */
function renderHistory() {
var ul = $("#historyList");
ul.innerHTML = "";
current.history.forEach(function (h, i) {
var li = document.createElement("li");
li.className = "history-row";
li.innerHTML =
'<span class="h-date"><span class="hd-day tnum">' + h.d + '</span><span class="hd-mon">' + h.m + "</span></span>" +
'<span class="h-body"><strong>' + h.title + "</strong><span>" + h.sub + "</span></span>" +
'<span class="h-right"><span class="h-amount tnum">' + money(h.amt) + "</span>" +
'<button class="invoice-btn" type="button" data-inv="' + i + '">' +
'<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>' +
"Invoice</button></span>";
li.querySelector(".invoice-btn").addEventListener("click", function () {
toast("Downloading " + h.sub.split(" · ")[0] + " (PDF)…", "ok");
});
ul.appendChild(li);
});
}
/* ---------- Quote modal ---------- */
var lastFocus = null;
function openQuote() {
if (!current.quote) return;
lastFocus = document.activeElement;
var q = current.quote;
$("#quoteVehicle").textContent = current.name + " · Estimate " + q.id;
var rows = "";
var sub = 0;
q.lines.forEach(function (l) {
sub += l.amt;
rows +=
'<div class="wo-line"><div class="wl-name"><strong>' + l.name + "</strong>" +
'<span class="wl-tag ' + l.tag + '">' + l.tag + "</span></div>" +
'<div class="wl-amt tnum">' + money(l.amt) + "</div></div>";
});
$("#quoteLines").innerHTML = rows;
var tax = +(sub * 0.0825).toFixed(2);
$("#quoteTotals").innerHTML =
'<div class="tline"><span>Subtotal</span><span class="tnum">' + money(sub) + "</span></div>" +
'<div class="tline"><span>Shop tax (8.25%)</span><span class="tnum">' + money(tax) + "</span></div>" +
'<div class="tline grand"><span>Total</span><span class="tnum">' + money(sub + tax) + "</span></div>";
openModal("#quoteModal");
}
function approveQuote() {
current.quote.approved = true;
closeModal("#quoteModal");
renderActive();
renderVehicleList();
toast("Estimate " + current.quote.id + " approved — work resumed.", "ok");
}
/* ---------- Book modal ---------- */
function openBook() {
lastFocus = document.activeElement;
$("#bookVehicle").textContent = current.name + " · " + current.plate;
var dateEl = $("#bookDate");
var d = new Date();
d.setDate(d.getDate() + 2);
dateEl.value = d.toISOString().slice(0, 10);
dateEl.min = new Date().toISOString().slice(0, 10);
openModal("#bookModal");
}
/* ---------- Modal plumbing ---------- */
function openModal(sel) {
var m = $(sel);
m.hidden = false;
var first = m.querySelector("button, select, input");
if (first) first.focus();
document.addEventListener("keydown", escClose);
}
function closeModal(sel) {
$(sel).hidden = true;
document.removeEventListener("keydown", escClose);
if (lastFocus) lastFocus.focus();
}
function escClose(e) {
if (e.key === "Escape") {
closeModal("#quoteModal");
closeModal("#bookModal");
}
}
/* ---------- Wire static controls ---------- */
function wire() {
$("#quoteClose").addEventListener("click", function () { closeModal("#quoteModal"); });
$("#quoteDecline").addEventListener("click", function () {
closeModal("#quoteModal");
toast("Estimate declined — we'll hold the vehicle and call you.", "warn");
});
$("#quoteApprove").addEventListener("click", approveQuote);
$("#bookBtn").addEventListener("click", openBook);
$("#bookClose").addEventListener("click", function () { closeModal("#bookModal"); });
$("#bookCancel").addEventListener("click", function () { closeModal("#bookModal"); });
$("#bookForm").addEventListener("submit", function (e) {
e.preventDefault();
var type = $("#bookType").value;
var date = $("#bookDate").value;
var time = $("#bookTime").value;
closeModal("#bookModal");
toast(type + " requested for " + date + " at " + time + ". We'll confirm by text.", "ok");
});
[$("#quoteModal"), $("#bookModal")].forEach(function (bd) {
bd.addEventListener("click", function (e) {
if (e.target === bd) bd.hidden = true;
});
});
$("#contactBtn").addEventListener("click", function () {
toast("Torque & Tread · (512) 555-0148 · open until 6:00 PM", "info");
});
$("#addVehicle").addEventListener("click", function () {
toast("Vehicle lookup by VIN or plate — coming to your portal soon.", "info");
});
$("#account").addEventListener("click", function () {
toast("Signed in as Marcus Vale · 3 vehicles on file.", "info");
});
}
/* ---------- Render all ---------- */
function renderAll() {
renderVehicleList();
renderHero();
renderActive();
renderReminders();
renderHistory();
}
renderAll();
wire();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Torque & Tread — Customer Service Portal</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="portal">
<!-- Top bar -->
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 13l2-5a2 2 0 0 1 1.9-1.3h10.2A2 2 0 0 1 19 8l2 5"/><path d="M3 13h18v4a1 1 0 0 1-1 1h-1a2 2 0 0 1-4 0H9a2 2 0 0 1-4 0H4a1 1 0 0 1-1-1z"/><circle cx="7.5" cy="17.5" r="1.2" fill="currentColor"/><circle cx="16.5" cy="17.5" r="1.2" fill="currentColor"/></svg>
</span>
<div class="brand-text">
<strong>Torque & Tread</strong>
<span>Service Portal</span>
</div>
</div>
<div class="topbar-right">
<button class="ghost-btn" id="contactBtn" type="button">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.9v3a2 2 0 0 1-2.2 2 19.8 19.8 0 0 1-8.6-3 19.5 19.5 0 0 1-6-6 19.8 19.8 0 0 1-3-8.7A2 2 0 0 1 4.1 2h3a2 2 0 0 1 2 1.7c.1.9.4 1.8.7 2.6a2 2 0 0 1-.5 2.1L8.1 9.6a16 16 0 0 0 6 6l1.2-1.2a2 2 0 0 1 2.1-.5c.8.3 1.7.6 2.6.7a2 2 0 0 1 1.7 2z"/></svg>
Contact shop
</button>
<div class="account" id="account" tabindex="0" role="button" aria-label="Account: Marcus Vale">
<span class="avatar">MV</span>
<span class="account-name">Marcus Vale</span>
</div>
</div>
</header>
<div class="layout">
<!-- Sidebar: my vehicles -->
<aside class="sidebar" aria-label="My vehicles">
<div class="sidebar-head">
<h2>My Garage</h2>
<span class="pill">3 vehicles</span>
</div>
<div class="vehicle-list" id="vehicleList" role="listbox" aria-label="Select a vehicle"></div>
<button class="add-vehicle" id="addVehicle" type="button">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
Add a vehicle
</button>
</aside>
<!-- Main -->
<main class="main">
<!-- Vehicle hero -->
<section class="vehicle-hero" id="vehicleHero" aria-live="polite">
<div class="hero-photo" id="heroPhoto" aria-hidden="true"></div>
<div class="hero-info">
<div class="hero-title-row">
<h1 id="heroName">—</h1>
<span class="status-chip" id="heroStatus">—</span>
</div>
<p class="hero-sub" id="heroSub">—</p>
<div class="hero-stats" id="heroStats"></div>
</div>
</section>
<!-- Active service status -->
<section class="panel">
<div class="panel-head">
<h2>Active service</h2>
<span class="bay-tag" id="bayTag">Bay —</span>
</div>
<div id="activeService"></div>
</section>
<div class="grid-2">
<!-- Upcoming maintenance -->
<section class="panel">
<div class="panel-head">
<h2>Upcoming maintenance</h2>
</div>
<ul class="reminder-list" id="reminderList"></ul>
<button class="primary-btn" id="bookBtn" type="button">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
Book a service
</button>
</section>
<!-- Service history & invoices -->
<section class="panel">
<div class="panel-head">
<h2>Service history</h2>
</div>
<ul class="history-list" id="historyList"></ul>
</section>
</div>
</main>
</div>
</div>
<!-- Quote approval modal -->
<div class="modal-backdrop" id="quoteModal" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="quoteTitle">
<div class="modal-head">
<h2 id="quoteTitle">Approve estimate</h2>
<button class="icon-btn" id="quoteClose" type="button" aria-label="Close">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<p class="modal-sub" id="quoteVehicle">—</p>
<div class="wo-table" id="quoteLines"></div>
<div class="wo-totals" id="quoteTotals"></div>
<div class="modal-actions">
<button class="ghost-btn" id="quoteDecline" type="button">Decline</button>
<button class="primary-btn" id="quoteApprove" type="button">Approve & authorize</button>
</div>
</div>
</div>
<!-- Book service modal -->
<div class="modal-backdrop" id="bookModal" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="bookTitle">
<div class="modal-head">
<h2 id="bookTitle">Book a service</h2>
<button class="icon-btn" id="bookClose" type="button" aria-label="Close">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<p class="modal-sub" id="bookVehicle">—</p>
<form id="bookForm">
<label class="field">
<span>Service type</span>
<select id="bookType" required>
<option value="Oil & filter change">Oil & filter change</option>
<option value="Brake inspection">Brake inspection</option>
<option value="Tire rotation">Tire rotation</option>
<option value="Full diagnostic">Full diagnostic</option>
<option value="A/C service">A/C service</option>
<option value="Annual safety inspection">Annual safety inspection</option>
</select>
</label>
<div class="field-row">
<label class="field">
<span>Preferred date</span>
<input type="date" id="bookDate" required />
</label>
<label class="field">
<span>Drop-off</span>
<select id="bookTime" required>
<option>07:30 AM</option>
<option>09:00 AM</option>
<option>11:00 AM</option>
<option>01:30 PM</option>
<option>03:00 PM</option>
</select>
</label>
</div>
<label class="field">
<span>Notes for the tech (optional)</span>
<textarea id="bookNotes" rows="2" placeholder="e.g. slight pull to the left when braking"></textarea>
</label>
<div class="modal-actions">
<button class="ghost-btn" type="button" id="bookCancel">Cancel</button>
<button class="primary-btn" type="submit">Request appointment</button>
</div>
</form>
</div>
</div>
<div class="toast-wrap" id="toastWrap" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Customer Service Portal
A self-contained customer portal for a fictional repair shop, Torque & Tread. A sidebar “My Garage” lists the customer’s three vehicles, each with a live status dot, plate, and gradient photo placeholder. Selecting a vehicle swaps the whole view: a dark hero panel shows the model, current service status chip, odometer, plate, and a truncated VIN with tabular figures.
The active-service panel renders a four-step progress track (Checked in, Diagnosis, In progress, Ready) with the assigned bay number and a technician note that can surface a diagnostic trouble code such as P0301. When a repair needs sign-off, a highlighted estimate banner appears; opening it reveals a work-order table split into labor and parts line items, subtotal, shop tax, and grand total. Approving authorizes the job, flips the panel to a confirmed state, and updates the sidebar. Customers can also book a new appointment through a validated form, scan upcoming maintenance reminders flagged due or overdue, and download invoices from the service history list.
Everything runs on vanilla JavaScript with no dependencies — vehicle switching, modal focus handling, keyboard-dismissable dialogs, and a small toast helper provide the micro-interactions. The layout collapses to a mobile-first single column down to 360px.
Illustrative UI only — fictional shop/dealership, not a real service system.