Auto — Quote Approval
A customer-facing digital quote approval and sign-off screen for an auto shop, pairing a vehicle card with an itemized repair quote split into labor, parts and shop supplies. Each line carries approve and decline controls, bulk approve-all and decline-all actions, and a live authorized total that recomputes tax as decisions change. A working canvas signature pad plus a printed name unlock an authorize button that stamps the total, locks the controls and confirms with a toast.
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;
}
* { 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;
}
.tab { font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1; }
.wrap {
max-width: 1040px;
margin: 0 auto;
padding: 22px 18px 64px;
}
/* topbar */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
padding: 14px 18px;
background: var(--garage);
color: #fff;
border-radius: var(--r-lg);
box-shadow: 0 10px 26px rgba(20, 21, 24, 0.16);
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand-mark {
width: 38px; height: 38px;
display: grid; place-items: center;
background: var(--orange);
color: #fff;
border-radius: 10px;
}
.brand-text { display: flex; flex-direction: column; line-height: 1.25; }
.brand-text strong { font-size: 15px; font-weight: 700; letter-spacing: -0.01em; }
.brand-text span { font-size: 11.5px; color: var(--steel-l); }
.ro-tag {
display: flex; flex-direction: column; align-items: flex-end;
padding: 6px 12px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: var(--r-sm);
}
.ro-label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--steel-l); }
.ro-num { font-size: 14px; font-weight: 700; color: var(--orange); font-variant-numeric: tabular-nums; }
/* vehicle */
.vehicle {
display: flex;
gap: 18px;
margin-top: 16px;
padding: 16px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: 0 4px 14px rgba(20, 21, 24, 0.05);
}
.veh-photo {
position: relative;
flex: 0 0 190px;
height: 124px;
border-radius: var(--r-md);
background:
linear-gradient(135deg, #2a2d34 0%, #3c4049 45%, #565b66 100%);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.06);
overflow: hidden;
}
.veh-photo::after {
content: "";
position: absolute; inset: 0;
background: radial-gradient(120% 80% at 80% 0%, rgba(255, 106, 19, 0.22), transparent 60%);
}
.veh-badge {
position: absolute; left: 10px; bottom: 10px;
z-index: 1;
padding: 4px 9px;
font-size: 11px; font-weight: 700;
color: var(--garage);
background: var(--orange);
border-radius: 6px;
}
.veh-meta { min-width: 0; flex: 1; }
.veh-meta h1 { margin: 2px 0 0; font-size: 20px; font-weight: 800; letter-spacing: -0.02em; }
.veh-meta h1 .trim { font-weight: 600; color: var(--steel); font-size: 15px; }
.veh-owner { margin: 2px 0 12px; font-size: 13px; color: var(--muted); }
.veh-specs {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
margin: 0;
}
.veh-specs dt { font-size: 10px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--muted); }
.veh-specs dd { margin: 1px 0 0; font-size: 13.5px; font-weight: 600; color: var(--ink); }
/* grid */
.grid {
display: grid;
grid-template-columns: minmax(0, 1fr) 340px;
gap: 18px;
margin-top: 18px;
align-items: start;
}
.quote-head {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 12px;
}
.quote-head h2 { margin: 0; font-size: 16px; font-weight: 700; letter-spacing: -0.01em; }
.bulk { display: flex; gap: 8px; }
/* line items */
.lines { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 12px; }
.line {
position: relative;
padding: 14px 15px;
background: var(--surface);
border: 1px solid var(--line);
border-left: 3px solid var(--line-2);
border-radius: var(--r-md);
box-shadow: 0 2px 8px rgba(20, 21, 24, 0.04);
transition: border-color 0.18s, box-shadow 0.18s, opacity 0.18s;
}
.line.is-approved { border-left-color: var(--ok); }
.line.is-declined { border-left-color: var(--danger); opacity: 0.62; }
.line-top { display: flex; gap: 12px; justify-content: space-between; }
.line-id { min-width: 0; }
.line-id h3 { margin: 4px 0 1px; font-size: 14.5px; font-weight: 700; letter-spacing: -0.01em; }
.line-sub { margin: 0; font-size: 12.5px; color: var(--muted); }
.line-amt { font-size: 16px; font-weight: 800; white-space: nowrap; }
.dtc {
display: inline-block;
padding: 2px 7px;
font-size: 10.5px; font-weight: 700; letter-spacing: 0.03em;
color: var(--garage);
background: #e8eaee;
border-radius: 5px;
font-variant-numeric: tabular-nums;
}
.dtc.rec { background: var(--orange-50); color: var(--orange-d); }
.dtc.opt { background: #e7eef7; color: #2b6fd6; }
.line-breakdown {
display: flex; flex-wrap: wrap; gap: 14px;
margin: 10px 0 12px;
padding-top: 10px;
border-top: 1px dashed var(--line);
font-size: 12px; color: var(--ink-2);
}
.line-breakdown em { font-style: normal; color: var(--muted); margin-right: 4px; }
.line-actions { display: flex; gap: 8px; }
.seg {
flex: 1;
padding: 8px 10px;
font: inherit; font-size: 13px; font-weight: 600;
border: 1px solid var(--line-2);
background: var(--surface);
color: var(--ink-2);
border-radius: var(--r-sm);
cursor: pointer;
transition: background 0.15s, color 0.15s, border-color 0.15s, transform 0.05s;
}
.seg:hover { border-color: var(--steel); }
.seg:active { transform: translateY(1px); }
.seg:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }
.line.is-approved .seg-approve { background: var(--ok); border-color: var(--ok); color: #fff; }
.line.is-declined .seg-decline { background: var(--danger); border-color: var(--danger); color: #fff; }
.line-state {
position: absolute; top: 14px; right: 15px;
display: none;
padding: 2px 8px;
font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;
border-radius: 999px;
}
.line.is-approved .line-state { display: inline-block; background: rgba(47, 158, 111, 0.14); color: var(--ok); }
.line.is-declined .line-state { display: inline-block; background: rgba(212, 73, 62, 0.14); color: var(--danger); }
.line.is-approved .line-amt,
.line.is-declined .line-amt { margin-top: 16px; }
/* side panels */
.col-side { display: flex; flex-direction: column; gap: 16px; position: sticky; top: 16px; }
.panel {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 16px 16px 18px;
box-shadow: 0 4px 14px rgba(20, 21, 24, 0.05);
}
.panel h2 { margin: 0 0 12px; font-size: 14px; font-weight: 700; letter-spacing: -0.01em; }
.meter {
height: 8px;
border-radius: 999px;
background: #e8eaee;
overflow: hidden;
}
.meter-fill {
display: block; height: 100%;
width: 0;
background: linear-gradient(90deg, var(--orange), var(--orange-d));
border-radius: 999px;
transition: width 0.3s ease;
}
.meter-note { margin: 7px 0 14px; font-size: 12px; color: var(--muted); }
.meter-note span { font-weight: 700; color: var(--ink); }
.totals { margin: 0; display: flex; flex-direction: column; gap: 8px; }
.totals > div { display: flex; justify-content: space-between; align-items: baseline; }
.totals dt { margin: 0; font-size: 13px; color: var(--ink-2); }
.totals dd { margin: 0; font-size: 13.5px; font-weight: 600; }
.totals-grand {
margin-top: 6px; padding-top: 10px;
border-top: 1px solid var(--line);
}
.totals-grand dt { font-size: 14px; font-weight: 700; color: var(--ink); }
.totals-grand dd { font-size: 19px; font-weight: 800; color: var(--orange-d); }
.counts {
display: flex; flex-wrap: wrap; gap: 8px;
margin-top: 14px;
}
.cnt {
flex: 1; min-width: 86px;
padding: 7px 9px;
font-size: 11.5px; color: var(--muted);
background: var(--bg);
border-radius: var(--r-sm);
text-align: center;
}
.cnt b { display: block; font-size: 16px; font-weight: 800; color: var(--ink); }
.cnt-ok b { color: var(--ok); }
.cnt-no b { color: var(--danger); }
.cnt-wait b { color: var(--warn); }
/* signature */
.sign-hint { margin: -4px 0 10px; font-size: 12px; color: var(--muted); }
.pad-wrap {
position: relative;
border: 1px solid var(--line-2);
border-radius: var(--r-md);
background:
repeating-linear-gradient(90deg, rgba(20,21,24,0.02) 0 11px, transparent 11px 22px),
#fbfbfc;
overflow: hidden;
touch-action: none;
}
#pad { display: block; width: 100%; height: 150px; cursor: crosshair; }
.pad-line {
position: absolute; left: 16px; right: 16px; bottom: 34px;
border-top: 1.5px solid var(--line-2);
pointer-events: none;
}
.pad-x {
position: absolute; left: 16px; bottom: 18px;
font-size: 13px; font-weight: 700; color: var(--steel-l);
pointer-events: none;
}
.pad-wrap.has-ink .pad-line,
.pad-wrap.has-ink .pad-x { opacity: 0; }
.sign-row {
display: flex; gap: 10px; align-items: flex-end;
margin: 12px 0 14px;
}
.sign-name { flex: 1; display: flex; flex-direction: column; gap: 4px; }
.sign-name span { font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); }
.sign-name input {
font: inherit; font-size: 13.5px; font-weight: 600;
padding: 8px 10px;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
background: var(--surface);
color: var(--ink);
}
.sign-name input:focus-visible { outline: 2px solid var(--orange); outline-offset: 1px; border-color: var(--orange); }
.sign-foot { margin: 12px 0 0; font-size: 11px; line-height: 1.4; color: var(--muted); }
/* buttons */
.btn {
font: inherit; font-weight: 600;
border: 1px solid transparent;
border-radius: var(--r-sm);
cursor: pointer;
transition: background 0.15s, color 0.15s, border-color 0.15s, box-shadow 0.15s, transform 0.05s;
}
.btn:active { transform: translateY(1px); }
.btn:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }
.btn-sm { padding: 7px 12px; font-size: 12.5px; }
.btn-ghost {
padding: 7px 12px; font-size: 12.5px;
background: var(--surface);
border-color: var(--line-2);
color: var(--ink-2);
}
.btn-ghost:hover { border-color: var(--steel); background: #fafafb; }
.topbar .btn-ghost { background: transparent; }
.btn-primary {
background: var(--orange);
color: #fff;
box-shadow: 0 6px 16px rgba(255, 106, 19, 0.28);
}
.btn-primary:hover { background: var(--orange-d); }
.btn-primary:disabled {
background: #d6d9de; color: #fff; box-shadow: none; cursor: not-allowed;
}
.btn-block {
display: flex; align-items: center; justify-content: center; gap: 8px;
width: 100%;
padding: 12px 14px;
font-size: 14.5px; font-weight: 700;
}
.btn-block.is-done { background: var(--ok); box-shadow: 0 6px 16px rgba(47, 158, 111, 0.28); }
/* toast */
.toast {
position: fixed; left: 50%; bottom: 26px;
transform: translate(-50%, 18px);
max-width: 90vw;
padding: 12px 18px;
background: var(--garage);
color: #fff;
font-size: 13.5px; font-weight: 600;
border-radius: var(--r-md);
box-shadow: 0 14px 34px rgba(20, 21, 24, 0.34);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s, transform 0.25s;
z-index: 50;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
.toast.ok { background: var(--ok); }
/* responsive */
@media (max-width: 880px) {
.grid { grid-template-columns: 1fr; }
.col-side { position: static; }
}
@media (max-width: 520px) {
.wrap { padding: 14px 12px 56px; }
.vehicle { flex-direction: column; }
.veh-photo { flex-basis: auto; width: 100%; height: 132px; }
.veh-specs { grid-template-columns: repeat(2, 1fr); }
.veh-meta h1 { font-size: 18px; }
.quote-head { flex-direction: column; align-items: flex-start; gap: 10px; }
.line-top { flex-direction: column; }
.line-amt { margin-top: 4px; }
.line.is-approved .line-amt,
.line.is-declined .line-amt { margin-top: 4px; }
.line-state { position: static; display: inline-block; margin-top: 8px; }
#pad { height: 132px; }
}(function () {
"use strict";
var TAX_RATE = 0.086;
var submitted = false;
var lines = Array.prototype.slice.call(document.querySelectorAll(".line"));
var meterFill = document.getElementById("meterFill");
var decidedCount = document.getElementById("decidedCount");
var sumLabor = document.getElementById("sumLabor");
var sumParts = document.getElementById("sumParts");
var sumTax = document.getElementById("sumTax");
var sumTotal = document.getElementById("sumTotal");
var btnTotal = document.getElementById("btnTotal");
var cApprove = document.getElementById("cApprove");
var cDecline = document.getElementById("cDecline");
var cPending = document.getElementById("cPending");
var submitBtn = document.getElementById("submitBtn");
function money(n) {
return "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
/* ---- toast helper ---- */
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg, kind) {
toastEl.textContent = msg;
toastEl.className = "toast show" + (kind ? " " + kind : "");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.className = "toast" + (kind ? " " + kind : "");
}, 2600);
}
/* ---- line breakdown: derive labor vs parts+shop from data ---- */
function lineParts(li) {
var amt = parseFloat(li.getAttribute("data-amount")) || 0;
var txt = li.querySelector(".line-breakdown").textContent;
var labMatch = txt.match(/\$([\d,]+\.\d{2})/g) || [];
// first money in breakdown after "Labor" is labor
var labor = 0;
var spans = li.querySelectorAll(".line-breakdown span");
spans.forEach(function (s) {
var m = s.textContent.match(/\$([\d,]+\.\d{2})/);
if (m && /Labor/i.test(s.textContent)) labor = parseFloat(m[1].replace(/,/g, ""));
});
var rest = Math.max(0, amt - labor);
return { total: amt, labor: labor, rest: rest };
}
/* ---- recompute summary ---- */
function recompute() {
var labor = 0, rest = 0, approved = 0, declined = 0, pending = 0;
lines.forEach(function (li) {
var state = li.dataset.decided === "false" ? "pending" : li.dataset.state;
if (state === "pending") {
pending++;
} else if (state === "approve") {
approved++;
var p = lineParts(li);
labor += p.labor;
rest += p.rest;
} else {
declined++;
}
});
var pre = labor + rest;
var tax = pre * TAX_RATE;
var total = pre + tax;
sumLabor.textContent = money(labor);
sumParts.textContent = money(rest);
sumTax.textContent = money(tax);
sumTotal.textContent = money(total);
btnTotal.textContent = money(total);
cApprove.textContent = approved;
cDecline.textContent = declined;
cPending.textContent = pending;
var decided = lines.length - pending;
decidedCount.textContent = decided;
meterFill.style.width = Math.round((decided / lines.length) * 100) + "%";
updateSubmit(approved);
}
/* ---- per-line decisions ---- */
function setDecision(li, act) {
if (submitted) return;
var current = li.dataset.decided === "true" ? li.dataset.state : null;
if (current === act) {
// toggle back to pending
li.dataset.decided = "false";
li.dataset.state = "pending";
li.classList.remove("is-approved", "is-declined");
} else {
li.dataset.decided = "true";
li.dataset.state = act;
li.classList.toggle("is-approved", act === "approve");
li.classList.toggle("is-declined", act === "decline");
var label = li.querySelector(".line-state");
label.textContent = act === "approve" ? "Approved" : "Declined";
}
recompute();
}
lines.forEach(function (li) {
li.querySelectorAll(".seg").forEach(function (btn) {
btn.addEventListener("click", function () {
setDecision(li, btn.dataset.act);
});
});
});
document.getElementById("approveAll").addEventListener("click", function () {
if (submitted) return;
lines.forEach(function (li) {
li.dataset.decided = "true";
li.dataset.state = "approve";
li.classList.add("is-approved");
li.classList.remove("is-declined");
li.querySelector(".line-state").textContent = "Approved";
});
recompute();
toast("All items approved");
});
document.getElementById("declineAll").addEventListener("click", function () {
if (submitted) return;
lines.forEach(function (li) {
li.dataset.decided = "true";
li.dataset.state = "decline";
li.classList.add("is-declined");
li.classList.remove("is-approved");
li.querySelector(".line-state").textContent = "Declined";
});
recompute();
toast("All items declined");
});
/* ---- signature pad ---- */
var canvas = document.getElementById("pad");
var padWrap = canvas.parentElement;
var ctx = canvas.getContext("2d");
var drawing = false;
var hasInk = false;
var last = null;
function fitCanvas() {
var ratio = window.devicePixelRatio || 1;
var rect = canvas.getBoundingClientRect();
// preserve drawing
var prev = hasInk ? ctx.getImageData(0, 0, canvas.width, canvas.height) : null;
canvas.width = Math.round(rect.width * ratio);
canvas.height = Math.round(rect.height * ratio);
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
ctx.lineWidth = 2.4;
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.strokeStyle = "#16181c";
if (prev) {
try { ctx.putImageData(prev, 0, 0); } catch (e) { /* size changed */ }
}
}
function pos(e) {
var rect = canvas.getBoundingClientRect();
var t = e.touches ? e.touches[0] : e;
return { x: t.clientX - rect.left, y: t.clientY - rect.top };
}
function start(e) {
if (submitted) return;
e.preventDefault();
drawing = true;
last = pos(e);
if (!hasInk) { hasInk = true; padWrap.classList.add("has-ink"); }
}
function move(e) {
if (!drawing) return;
e.preventDefault();
var p = pos(e);
ctx.beginPath();
ctx.moveTo(last.x, last.y);
ctx.lineTo(p.x, p.y);
ctx.stroke();
last = p;
updateSubmit();
}
function end() {
if (!drawing) return;
drawing = false;
updateSubmit();
}
canvas.addEventListener("mousedown", start);
canvas.addEventListener("mousemove", move);
window.addEventListener("mouseup", end);
canvas.addEventListener("touchstart", start, { passive: false });
canvas.addEventListener("touchmove", move, { passive: false });
canvas.addEventListener("touchend", end);
document.getElementById("clearPad").addEventListener("click", function () {
if (submitted) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
hasInk = false;
padWrap.classList.remove("has-ink");
updateSubmit();
});
/* ---- submit gating ---- */
function updateSubmit(approvedOverride) {
if (submitted) return;
var approved = approvedOverride;
if (approved === undefined) {
approved = lines.filter(function (li) {
return li.dataset.decided === "true" && li.dataset.state === "approve";
}).length;
}
var nameOk = document.getElementById("signName").value.trim().length > 1;
submitBtn.disabled = !(approved > 0 && hasInk && nameOk);
}
document.getElementById("signName").addEventListener("input", function () { updateSubmit(); });
submitBtn.addEventListener("click", function () {
if (submitBtn.disabled || submitted) return;
submitted = true;
var name = document.getElementById("signName").value.trim();
var total = sumTotal.textContent;
submitBtn.classList.add("is-done");
submitBtn.innerHTML = "Authorized · " + total;
submitBtn.disabled = true;
lines.forEach(function (li) {
li.querySelectorAll(".seg, .line-actions button").forEach(function (b) { b.disabled = true; });
});
document.getElementById("approveAll").disabled = true;
document.getElementById("declineAll").disabled = true;
document.getElementById("clearPad").disabled = true;
document.getElementById("signName").readOnly = true;
canvas.style.cursor = "default";
toast("Quote authorized by " + name + " · " + total, "ok");
});
/* ---- init ---- */
fitCanvas();
window.addEventListener("resize", fitCanvas);
recompute();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Auto — Quote Approval</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>
<main class="wrap">
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12h18M7 12V7a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v5M5 16h.01M19 16h.01M2 16h20l-1 3H3l-1-3Z"/></svg>
</span>
<div class="brand-text">
<strong>Cascade Auto & Diesel</strong>
<span>Quote Approval · Digital Sign-Off</span>
</div>
</div>
<div class="ro-tag">
<span class="ro-label">Work Order</span>
<span class="ro-num">RO-48217</span>
</div>
</header>
<section class="vehicle" aria-label="Vehicle">
<div class="veh-photo" role="img" aria-label="Vehicle photo placeholder, 2018 Subaru Outback, slate gray">
<span class="veh-badge">Bay 4</span>
</div>
<div class="veh-meta">
<h1>2018 Subaru Outback <span class="trim">3.6R Limited</span></h1>
<p class="veh-owner">Customer · Marisol Vega</p>
<dl class="veh-specs">
<div><dt>VIN</dt><dd class="tab">4S4BSANC2J3•••417</dd></div>
<div><dt>Plate</dt><dd class="tab">WA · BRX-2204</dd></div>
<div><dt>Odometer</dt><dd class="tab">86,412 mi</dd></div>
<div><dt>Advisor</dt><dd>D. Okonkwo</dd></div>
</dl>
</div>
</section>
<section class="grid">
<div class="col-main">
<div class="quote-head">
<h2>Itemized Quote</h2>
<div class="bulk">
<button type="button" class="btn btn-ghost" id="approveAll">Approve all</button>
<button type="button" class="btn btn-ghost" id="declineAll">Decline all</button>
</div>
</div>
<ul class="lines" id="lines" aria-label="Quote line items">
<li class="line" data-id="L1" data-amount="289.40" data-decided="false">
<div class="line-top">
<div class="line-id">
<span class="dtc">P0301</span>
<h3>Cylinder 1 misfire repair</h3>
<p class="line-sub">Replace ignition coil + spark plug set</p>
</div>
<div class="line-amt tab">$289.40</div>
</div>
<div class="line-breakdown">
<span><em>Labor</em> 1.4 hr · $182.00</span>
<span><em>Parts</em> $97.20</span>
<span><em>Shop</em> $10.20</span>
</div>
<div class="line-actions" role="group" aria-label="Decision for Cylinder 1 misfire repair">
<button type="button" class="seg seg-approve" data-act="approve">Approve</button>
<button type="button" class="seg seg-decline" data-act="decline">Decline</button>
</div>
<span class="line-state" data-state="pending">Pending</span>
</li>
<li class="line" data-id="L2" data-amount="146.75" data-decided="false">
<div class="line-top">
<div class="line-id">
<span class="dtc rec">Recommended</span>
<h3>Front brake pads & rotor resurface</h3>
<p class="line-sub">Ceramic pads, machine front rotors</p>
</div>
<div class="line-amt tab">$146.75</div>
</div>
<div class="line-breakdown">
<span><em>Labor</em> 0.9 hr · $117.00</span>
<span><em>Parts</em> $24.50</span>
<span><em>Shop</em> $5.25</span>
</div>
<div class="line-actions" role="group" aria-label="Decision for front brake pads and rotor resurface">
<button type="button" class="seg seg-approve" data-act="approve">Approve</button>
<button type="button" class="seg seg-decline" data-act="decline">Decline</button>
</div>
<span class="line-state" data-state="pending">Pending</span>
</li>
<li class="line" data-id="L3" data-amount="64.00" data-decided="false">
<div class="line-top">
<div class="line-id">
<span class="dtc opt">Optional</span>
<h3>Cabin & engine air filter</h3>
<p class="line-sub">Replace both filters, inspect housing</p>
</div>
<div class="line-amt tab">$64.00</div>
</div>
<div class="line-breakdown">
<span><em>Labor</em> 0.4 hr · $38.00</span>
<span><em>Parts</em> $24.00</span>
<span><em>Shop</em> $2.00</span>
</div>
<div class="line-actions" role="group" aria-label="Decision for cabin and engine air filter">
<button type="button" class="seg seg-approve" data-act="approve">Approve</button>
<button type="button" class="seg seg-decline" data-act="decline">Decline</button>
</div>
<span class="line-state" data-state="pending">Pending</span>
</li>
<li class="line" data-id="L4" data-amount="118.30" data-decided="false">
<div class="line-top">
<div class="line-id">
<span class="dtc rec">Recommended</span>
<h3>Coolant flush & thermostat</h3>
<p class="line-sub">Drain, refill OAT coolant, new thermostat</p>
</div>
<div class="line-amt tab">$118.30</div>
</div>
<div class="line-breakdown">
<span><em>Labor</em> 0.7 hr · $84.00</span>
<span><em>Parts</em> $30.30</span>
<span><em>Shop</em> $4.00</span>
</div>
<div class="line-actions" role="group" aria-label="Decision for coolant flush and thermostat">
<button type="button" class="seg seg-approve" data-act="approve">Approve</button>
<button type="button" class="seg seg-decline" data-act="decline">Decline</button>
</div>
<span class="line-state" data-state="pending">Pending</span>
</li>
</ul>
</div>
<aside class="col-side">
<div class="panel summary" aria-live="polite">
<h2>Authorized Total</h2>
<div class="meter">
<span class="meter-fill" id="meterFill" style="width:0%"></span>
</div>
<p class="meter-note"><span id="decidedCount">0</span> of 4 lines decided</p>
<dl class="totals">
<div><dt>Approved labor</dt><dd class="tab" id="sumLabor">$0.00</dd></div>
<div><dt>Approved parts & shop</dt><dd class="tab" id="sumParts">$0.00</dd></div>
<div><dt>Tax (8.6%)</dt><dd class="tab" id="sumTax">$0.00</dd></div>
<div class="totals-grand"><dt>Authorized total</dt><dd class="tab" id="sumTotal">$0.00</dd></div>
</dl>
<div class="counts">
<span class="cnt cnt-ok"><b id="cApprove">0</b> approved</span>
<span class="cnt cnt-no"><b id="cDecline">0</b> declined</span>
<span class="cnt cnt-wait"><b id="cPending">4</b> pending</span>
</div>
</div>
<div class="panel sign">
<h2>Signature</h2>
<p class="sign-hint">Sign below to authorize the approved items.</p>
<div class="pad-wrap">
<canvas id="pad" width="520" height="180" aria-label="Signature pad"></canvas>
<span class="pad-line" aria-hidden="true"></span>
<span class="pad-x" aria-hidden="true">✕</span>
</div>
<div class="sign-row">
<button type="button" class="btn btn-ghost btn-sm" id="clearPad">Clear</button>
<label class="sign-name">
<span>Printed name</span>
<input type="text" id="signName" value="Marisol Vega" autocomplete="name" />
</label>
</div>
<button type="button" class="btn btn-primary btn-block" id="submitBtn" disabled>
Authorize <span class="tab" id="btnTotal">$0.00</span>
</button>
<p class="sign-foot">By authorizing you approve only the checked line items. Declined items are not performed.</p>
</div>
</aside>
</section>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Quote Approval
A single-screen digital sign-off for Cascade Auto & Diesel, led by a vehicle card that pairs a gradient photo placeholder with the VIN, plate, odometer, bay number and assigned advisor for a 2018 Subaru Outback. Below it, the itemized quote lists four jobs — a P0301 misfire repair, front brakes, filters and a coolant flush — each split into labor hours, parts and shop supplies, and tagged as a diagnostic code, recommended or optional.
Every line is interactive. Approve and decline buttons toggle each item’s state (and toggle back to pending on a second click), flipping the line’s accent border, status pill and the running approved / declined / pending counts. Approve-all and decline-all apply a decision to every row at once. The sticky summary panel recomputes approved labor, approved parts and shop, an 8.6% tax and the authorized total live, with a progress meter tracking how many of the four lines are decided.
A canvas signature pad captures a real pointer- and touch-drawn signature, with a clear button and a printed-name field. The authorize button stays disabled until at least one line is approved, the pad has ink and a name is entered; on submit it stamps the authorized total onto the button, locks every control and confirms with a toast.
Illustrative UI only — fictional shop/dealership, not a real service system.