Auto — Trade-In Valuation
A three-step trade-in appraisal desk for an auto dealership, walking a customer from vehicle details through condition and equipment to an animated value estimate. Make, year, trim, odometer, an overall condition grade, value-adding options and disclosure flags all feed a live valuation model that drives a sweeping needle gauge, a counting headline figure, a low-to-high confidence range and an itemized breakdown of every adjustment, with a button to apply the credit straight to a purchase.
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;
--info: #2b7fff;
--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.05);
--shadow-md: 0 6px 20px rgba(20, 21, 24, 0.08), 0 2px 6px rgba(20, 21, 24, 0.05);
--shadow-lg: 0 18px 50px rgba(20, 21, 24, 0.14);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(900px 420px at 100% -10%, rgba(255, 106, 19, 0.08), transparent 60%),
var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
padding: 28px 16px 56px;
}
.mono { font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }
.visually-hidden {
position: absolute; width: 1px; height: 1px;
margin: -1px; padding: 0; border: 0; clip: rect(0 0 0 0); overflow: hidden;
}
.shell {
max-width: 880px;
margin: 0 auto;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow-lg);
overflow: hidden;
}
/* ---- Masthead ---- */
.masthead {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 18px 22px;
background: linear-gradient(120deg, var(--garage), var(--garage-2));
color: #fff;
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand__mark {
display: grid; place-items: center;
width: 40px; height: 40px;
border-radius: 11px;
color: #fff;
background: linear-gradient(160deg, var(--orange), var(--orange-d));
box-shadow: 0 6px 16px rgba(255, 106, 19, 0.4);
}
.brand__text { display: flex; flex-direction: column; line-height: 1.25; }
.brand__text strong { font-size: 15px; font-weight: 700; }
.brand__text span { font-size: 12px; color: var(--steel-l); }
.masthead__meta { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }
.quote-id {
font-size: 12px; font-weight: 700; letter-spacing: 0.04em;
font-variant-numeric: tabular-nums;
padding: 4px 9px; border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.16);
}
.valid { font-size: 11px; color: var(--steel-l); }
/* ---- Steps ---- */
.wizard { padding: 22px; }
.steps {
display: flex; align-items: center; gap: 8px;
list-style: none; margin: 0 0 22px; padding: 0;
}
.steps__item {
display: flex; align-items: center; gap: 9px;
flex: 1; min-width: 0;
padding: 10px 12px;
border: 1px solid var(--line);
border-radius: var(--r-md);
background: #fafafb;
color: var(--muted);
transition: border-color 0.2s, background 0.2s, color 0.2s;
}
.steps__num {
display: grid; place-items: center;
width: 24px; height: 24px; flex: none;
border-radius: 999px;
font-size: 13px; font-weight: 700;
background: #fff; color: var(--steel);
border: 1.5px solid var(--line-2);
}
.steps__lbl { font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.steps__item.is-active {
border-color: var(--orange);
background: var(--orange-50);
color: var(--ink);
}
.steps__item.is-active .steps__num {
background: var(--orange); color: #fff; border-color: var(--orange);
}
.steps__item.is-done { color: var(--ink); border-color: rgba(47, 158, 111, 0.4); background: rgba(47, 158, 111, 0.07); }
.steps__item.is-done .steps__num { background: var(--ok); color: #fff; border-color: var(--ok); }
/* ---- Panel ---- */
.panel__head { margin-bottom: 18px; }
.panel__head h2 { margin: 0 0 4px; font-size: 19px; font-weight: 700; }
.panel__head p { margin: 0; font-size: 13.5px; color: var(--muted); }
.panel__foot {
display: flex; align-items: center; justify-content: space-between;
gap: 12px; margin-top: 22px;
padding-top: 18px; border-top: 1px solid var(--line);
}
.hint { font-size: 12px; color: var(--muted); }
.step { animation: rise 0.32s ease both; }
@keyframes rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
/* ---- Fields ---- */
.grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 14px;
}
.field { display: flex; flex-direction: column; gap: 6px; }
.field__label { font-size: 12px; font-weight: 600; color: var(--ink-2); }
input[type="text"], input[type="number"], select {
width: 100%;
font: inherit;
font-size: 14px;
color: var(--ink);
padding: 11px 12px;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
background: #fff;
transition: border-color 0.16s, box-shadow 0.16s;
}
input[type="number"] { font-variant-numeric: tabular-nums; }
select { appearance: none; cursor: pointer;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23737a85' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
background-repeat: no-repeat; background-position: right 11px center; padding-right: 34px;
}
input:focus, select:focus {
outline: none;
border-color: var(--orange);
box-shadow: 0 0 0 3px rgba(255, 106, 19, 0.18);
}
.input-suffix { position: relative; }
.input-suffix .suffix {
position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
font-size: 12px; font-weight: 600; color: var(--muted); pointer-events: none;
}
.input-suffix input { padding-right: 36px; }
.field-error {
margin: 14px 0 0; font-size: 13px; font-weight: 600; color: var(--danger);
background: rgba(212, 73, 62, 0.08); border: 1px solid rgba(212, 73, 62, 0.25);
padding: 9px 12px; border-radius: var(--r-sm);
}
/* ---- Buttons ---- */
.btn {
display: inline-flex; align-items: center; gap: 7px;
font: inherit; font-size: 14px; font-weight: 700;
padding: 11px 18px;
border-radius: var(--r-sm);
border: 1px solid transparent;
cursor: pointer;
transition: transform 0.08s, background 0.16s, box-shadow 0.16s, border-color 0.16s;
}
.btn:active { transform: translateY(1px); }
.btn:focus-visible { outline: 3px solid rgba(255, 106, 19, 0.4); outline-offset: 2px; }
.btn--primary {
color: #fff;
background: linear-gradient(165deg, var(--orange), var(--orange-d));
box-shadow: 0 6px 16px rgba(255, 106, 19, 0.32);
}
.btn--primary:hover { box-shadow: 0 8px 22px rgba(255, 106, 19, 0.42); }
.btn--ghost { color: var(--ink-2); background: #fff; border-color: var(--line-2); }
.btn--ghost:hover { border-color: var(--steel); background: #fafafb; }
.btn--quiet { color: var(--muted); background: transparent; padding: 11px 10px; }
.btn--quiet:hover { color: var(--ink); }
/* ---- Condition ---- */
fieldset { border: 0; margin: 0 0 20px; padding: 0; min-width: 0; }
legend { font-size: 13px; font-weight: 700; color: var(--ink); padding: 0; margin-bottom: 11px; }
.condition__opts {
display: grid; grid-template-columns: repeat(2, 1fr); gap: 11px;
}
.cond { position: relative; cursor: pointer; }
.cond input { position: absolute; opacity: 0; inset: 0; cursor: pointer; }
.cond__card {
display: grid; gap: 4px;
height: 100%;
padding: 13px 13px 36px;
border: 1.5px solid var(--line-2);
border-radius: var(--r-md);
background: #fff;
transition: border-color 0.16s, box-shadow 0.16s, transform 0.12s;
}
.cond__grade { font-size: 14.5px; font-weight: 700; }
.cond__desc { font-size: 12px; color: var(--muted); line-height: 1.4; }
.cond__factor {
position: absolute; left: 13px; bottom: 12px;
font-size: 12px; font-weight: 700; font-variant-numeric: tabular-nums;
padding: 2px 8px; border-radius: 999px;
background: #f1f2f4; color: var(--steel);
}
.cond input:hover + .cond__card { border-color: var(--steel); }
.cond input:checked + .cond__card {
border-color: var(--orange);
box-shadow: 0 0 0 3px rgba(255, 106, 19, 0.16), var(--shadow-sm);
transform: translateY(-1px);
}
.cond__card[data-tone="ok"] .cond__factor { background: rgba(47,158,111,0.12); color: var(--ok); }
.cond__card[data-tone="warn"] .cond__factor { background: rgba(224,150,42,0.14); color: var(--warn); }
.cond__card[data-tone="danger"] .cond__factor { background: rgba(212,73,62,0.12); color: var(--danger); }
.cond__card[data-tone="info"] .cond__factor { background: rgba(43,127,255,0.12); color: var(--info); }
.cond input:focus-visible + .cond__card { outline: 3px solid rgba(255,106,19,0.4); outline-offset: 2px; }
.condition__note { margin: 11px 0 0; font-size: 12px; color: var(--muted); }
/* ---- Options & flags ---- */
.options__grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 9px; }
.opt, .flag { position: relative; cursor: pointer; display: block; }
.opt input, .flag input { position: absolute; opacity: 0; inset: 0; cursor: pointer; }
.opt__box, .flag__box {
display: flex; align-items: center; justify-content: space-between; gap: 10px;
padding: 11px 13px;
border: 1.5px solid var(--line-2);
border-radius: var(--r-sm);
background: #fff;
transition: border-color 0.16s, background 0.16s;
}
.opt__name, .flag__name { font-size: 13px; font-weight: 600; }
.opt__val { font-size: 12.5px; font-weight: 700; color: var(--ok); font-variant-numeric: tabular-nums; }
.flag__val { font-size: 12.5px; font-weight: 700; color: var(--danger); font-variant-numeric: tabular-nums; }
.opt input:hover + .opt__box, .flag input:hover + .flag__box { border-color: var(--steel); }
.opt input:checked + .opt__box { border-color: var(--ok); background: rgba(47,158,111,0.06); }
.flag input:checked + .flag__box { border-color: var(--danger); background: rgba(212,73,62,0.06); }
.opt input:focus-visible + .opt__box, .flag input:focus-visible + .flag__box {
outline: 3px solid rgba(255,106,19,0.4); outline-offset: 2px;
}
.flags { display: grid; grid-template-columns: repeat(3, 1fr); gap: 9px; margin-bottom: 0; }
.flags legend { grid-column: 1 / -1; }
/* ---- Result ---- */
.result { display: grid; gap: 18px; }
.vehicle-card {
display: grid; grid-template-columns: 96px 1fr; gap: 14px;
align-items: center;
padding: 12px;
border: 1px solid var(--line);
border-radius: var(--r-md);
background: #fafafb;
}
.vehicle-card__photo {
position: relative;
height: 70px; border-radius: var(--r-sm);
background:
linear-gradient(135deg, rgba(255,255,255,0.16), transparent 60%),
linear-gradient(160deg, var(--garage-2), var(--steel));
overflow: hidden;
}
.vehicle-card__tag {
position: absolute; left: 6px; bottom: 6px;
font-size: 9px; font-weight: 800; letter-spacing: 0.08em;
color: #fff; padding: 2px 6px; border-radius: 5px;
background: var(--orange);
}
.vehicle-card__spec { display: flex; gap: 18px; margin: 0; flex-wrap: wrap; }
.vehicle-card__spec dt { font-size: 11px; color: var(--muted); margin-bottom: 2px; }
.vehicle-card__spec dd { margin: 0; font-size: 13.5px; font-weight: 700; }
.gauge-wrap {
padding: 18px;
border: 1px solid var(--line);
border-radius: var(--r-md);
background:
radial-gradient(360px 200px at 50% 0, rgba(255,106,19,0.07), transparent 65%),
#fff;
text-align: center;
}
.gauge { position: relative; width: 240px; max-width: 100%; margin: 0 auto; }
.gauge__svg { width: 100%; height: auto; overflow: visible; }
.gauge__track { fill: none; stroke: #eceef0; stroke-width: 14; stroke-linecap: round; }
.gauge__fill {
fill: none; stroke: url(#gaugeGrad); stroke: var(--orange); stroke-width: 14; stroke-linecap: round;
stroke-dasharray: 251.3; stroke-dashoffset: 251.3;
transition: stroke-dashoffset 1.1s cubic-bezier(0.22, 1, 0.36, 1);
}
.gauge__needle {
stroke: var(--garage); stroke-width: 3; stroke-linecap: round;
transform-origin: 100px 110px; transform: rotate(-90deg);
transition: transform 1.1s cubic-bezier(0.22, 1, 0.36, 1);
}
.gauge__hub { fill: var(--garage); }
.gauge__center { margin-top: -34px; }
.gauge__label { display: block; font-size: 11px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; }
.gauge__value { display: block; font-size: 34px; font-weight: 800; color: var(--ink); margin-top: 2px; }
.range { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 12px; margin-top: 16px; }
.range__end { display: flex; flex-direction: column; }
.range__end span { font-size: 11px; color: var(--muted); }
.range__end strong { font-size: 15px; font-weight: 700; }
.range__end--hi { text-align: right; }
.range__bar { position: relative; height: 7px; border-radius: 999px; background: linear-gradient(90deg, #d7dadf, var(--orange)); }
.range__pip {
position: absolute; top: 50%; left: 50%;
width: 16px; height: 16px; border-radius: 999px;
background: #fff; border: 3px solid var(--orange);
box-shadow: var(--shadow-sm);
transform: translate(-50%, -50%); transition: left 1s cubic-bezier(0.22,1,0.36,1);
}
.breakdown { list-style: none; margin: 0; padding: 0; display: grid; gap: 1px; background: var(--line); border: 1px solid var(--line); border-radius: var(--r-md); overflow: hidden; }
.breakdown li {
display: flex; align-items: center; justify-content: space-between; gap: 12px;
padding: 11px 14px; background: #fff; font-size: 13.5px;
}
.breakdown li .bd-label { color: var(--ink-2); display: flex; align-items: center; gap: 8px; }
.breakdown li .bd-dot { width: 8px; height: 8px; border-radius: 999px; flex: none; }
.breakdown li .bd-amt { font-weight: 700; font-variant-numeric: tabular-nums; }
.bd-amt.is-pos { color: var(--ok); }
.bd-amt.is-neg { color: var(--danger); }
.breakdown li.is-total { background: var(--garage); color: #fff; }
.breakdown li.is-total .bd-label { color: #fff; font-weight: 700; }
.breakdown li.is-total .bd-amt { color: var(--orange); font-size: 16px; }
.apply {
display: flex; align-items: center; justify-content: space-between; gap: 14px;
margin-top: 18px; padding: 16px 18px;
border: 1px solid rgba(255,106,19,0.3);
border-radius: var(--r-md);
background: var(--orange-50);
}
.apply__copy { display: flex; flex-direction: column; }
.apply__copy strong { font-size: 14.5px; }
.apply__copy span { font-size: 12.5px; color: var(--ink-2); }
.apply.is-applied { border-color: rgba(47,158,111,0.4); background: rgba(47,158,111,0.08); }
/* ---- Toast ---- */
.toast {
position: fixed; left: 50%; bottom: 26px; transform: translate(-50%, 20px);
background: var(--garage); color: #fff;
font-size: 13.5px; font-weight: 600;
padding: 12px 18px; border-radius: 999px;
box-shadow: var(--shadow-lg);
opacity: 0; pointer-events: none;
transition: opacity 0.25s, transform 0.25s;
z-index: 40; max-width: calc(100% - 32px);
}
.toast.is-show { opacity: 1; transform: translate(-50%, 0); }
.toast--ok { background: var(--ok); }
/* ---- Responsive ---- */
@media (max-width: 520px) {
body { padding: 14px 10px 40px; }
.masthead { flex-direction: column; align-items: flex-start; gap: 12px; }
.masthead__meta { align-items: flex-start; }
.wizard { padding: 16px; }
.steps__lbl { display: none; }
.steps__item { justify-content: center; }
.grid, .condition__opts, .options__grid { grid-template-columns: 1fr; }
.flags { grid-template-columns: 1fr; }
.panel__foot { flex-wrap: wrap; }
.panel__foot .btn { flex: 1; justify-content: center; }
.vehicle-card { grid-template-columns: 1fr; }
.apply { flex-direction: column; align-items: stretch; }
.apply .btn { justify-content: center; }
.gauge__value { font-size: 30px; }
}
@media (prefers-reduced-motion: reduce) {
.gauge__fill, .gauge__needle, .range__pip, .step, .toast { transition: none; animation: none; }
}(function () {
"use strict";
var form = document.getElementById("appraise-form");
if (!form) return;
var steps = Array.prototype.slice.call(form.querySelectorAll(".step"));
var pips = Array.prototype.slice.call(document.querySelectorAll(".steps__item"));
var current = 1;
var applied = false;
/* ---- Toast helper ---- */
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg, ok) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.toggle("toast--ok", !!ok);
toastEl.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-show");
}, 2600);
}
/* ---- Populate year dropdown ---- */
var yearSel = document.getElementById("year");
var thisYear = 2026;
for (var y = thisYear; y >= thisYear - 22; y--) {
var o = document.createElement("option");
o.value = String(y);
o.textContent = String(y);
yearSel.appendChild(o);
}
/* ---- Formatters ---- */
function money(n) {
return "$" + Math.round(n).toLocaleString("en-US");
}
function commas(n) {
return Number(n || 0).toLocaleString("en-US");
}
/* ---- Live odometer grouping (display via title only, keep number input clean) ---- */
/* ---- Step navigation ---- */
function goTo(step) {
current = step;
steps.forEach(function (s) {
s.hidden = Number(s.getAttribute("data-step")) !== step;
});
pips.forEach(function (p) {
var n = Number(p.getAttribute("data-step-pip"));
p.classList.toggle("is-active", n === step);
p.classList.toggle("is-done", n < step);
});
var panel = steps[step - 1];
if (panel) {
var focusable = panel.querySelector("input, select, button");
if (focusable) focusable.focus({ preventScroll: true });
panel.scrollIntoView({ behavior: "smooth", block: "nearest" });
}
}
/* ---- Validate step 1 ---- */
function validateStep1() {
var make = document.getElementById("make").value;
var model = document.getElementById("model").value.trim();
var year = document.getElementById("year").value;
var miles = document.getElementById("mileage").value;
var err = document.getElementById("step1-error");
var ok = make && model && year && miles !== "" && Number(miles) >= 0;
err.hidden = ok;
if (!ok) toast("Complete the vehicle details to continue.");
return ok;
}
/* ---- Core valuation model ---- */
function compute() {
var makeOpt = document.getElementById("make").selectedOptions[0];
var makeTier = makeOpt ? Number(makeOpt.getAttribute("data-tier")) || 1 : 1;
var year = Number(document.getElementById("year").value) || thisYear;
var miles = Number(document.getElementById("mileage").value) || 0;
var trim = Number(document.getElementById("trim").value) || 1;
// Base MSRP-ish anchor by make tier.
var base = 34000 * makeTier;
// Age depreciation: ~14%/yr compounding, floored.
var age = Math.max(0, thisYear - year);
var ageFactor = Math.max(0.18, Math.pow(0.86, age));
// Mileage penalty: 12k/yr baseline expected; deviation costs/credits ~$0.11/mi.
var expectedMiles = age * 12000;
var mileageDelta = (expectedMiles - miles) * 0.11; // positive if low miles
mileageDelta = Math.max(-9000, Math.min(6000, mileageDelta));
var subtotal = base * ageFactor * trim + mileageDelta;
// Condition multiplier.
var condInput = form.querySelector('input[name="condition"]:checked');
var condFactor = condInput ? Number(condInput.value) : 1;
var condGrade = condInput ? condInput.getAttribute("data-grade") : "Good";
// Disclosure flags (multipliers, compounded).
var flagFactor = 1;
var flagInputs = form.querySelectorAll('input[name="flag"]:checked');
flagInputs.forEach(function (f) {
flagFactor *= Number(f.value);
});
var afterCond = subtotal * condFactor * flagFactor;
// Option add-ons (flat $).
var optTotal = 0;
var optInputs = form.querySelectorAll('input[name="opt"]:checked');
optInputs.forEach(function (f) {
optTotal += Number(f.value);
});
var mid = Math.max(900, afterCond + optTotal);
// Confidence range widens with age & condition uncertainty.
var spread = 0.06 + age * 0.005 + (condFactor < 1 ? 0.03 : 0.01);
spread = Math.min(0.17, spread);
var low = mid * (1 - spread);
var high = mid * (1 + spread);
return {
base: base * ageFactor * trim,
mileageDelta: mileageDelta,
condGrade: condGrade,
condFactor: condFactor,
condDelta: subtotal * (condFactor - 1),
flagFactor: flagFactor,
flagDelta: subtotal * condFactor * (flagFactor - 1),
optTotal: optTotal,
miles: miles,
mid: mid,
low: low,
high: high,
year: year,
make: makeOpt ? makeOpt.value : "",
model: document.getElementById("model").value.trim()
};
}
/* ---- Gauge animation ---- */
// Scale gauge needle/fill from a fixed display window so the value reads meaningfully.
var GAUGE_MIN = 0;
var GAUGE_MAX = 60000;
var ARC_LEN = 251.3; // path length of the semicircle
function setGauge(value) {
var t = Math.max(0, Math.min(1, (value - GAUGE_MIN) / (GAUGE_MAX - GAUGE_MIN)));
var fill = document.getElementById("gauge-fill");
var needle = document.getElementById("gauge-needle");
// Force reflow so transition replays each time.
fill.style.strokeDashoffset = ARC_LEN;
needle.style.transform = "rotate(-90deg)";
void fill.getBoundingClientRect();
requestAnimationFrame(function () {
fill.style.strokeDashoffset = String(ARC_LEN * (1 - t));
needle.style.transform = "rotate(" + (-90 + t * 180) + "deg)";
});
// Count-up the headline figure.
countUp(document.getElementById("gauge-value"), value);
}
function countUp(el, target) {
var start = 0;
var dur = 950;
var startT = null;
function frame(ts) {
if (startT === null) startT = ts;
var p = Math.min(1, (ts - startT) / dur);
var eased = 1 - Math.pow(1 - p, 3);
el.textContent = money(start + (target - start) * eased);
if (p < 1) requestAnimationFrame(frame);
else el.textContent = money(target);
}
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
el.textContent = money(target);
} else {
requestAnimationFrame(frame);
}
}
/* ---- Render result ---- */
function renderResult() {
var r = compute();
document.getElementById("result-vehicle").textContent =
r.year + " " + r.make + " " + (r.model || "") + " · appraised today";
document.getElementById("vc-miles").textContent = commas(r.miles) + " mi";
document.getElementById("vc-cond").textContent = r.condGrade;
document.getElementById("range-low").textContent = money(r.low);
document.getElementById("range-high").textContent = money(r.high);
// Pip position along the range bar = midpoint sits at 50% by definition.
document.getElementById("range-pip").style.left = "50%";
setGauge(r.mid);
// Breakdown rows.
var rows = [];
rows.push({ label: "Market base (age + trim)", amt: r.base, tone: "#5b6470" });
if (Math.abs(r.mileageDelta) > 5)
rows.push({ label: r.mileageDelta >= 0 ? "Low-mileage credit" : "High-mileage adjustment", amt: r.mileageDelta, tone: r.mileageDelta >= 0 ? "#2f9e6f" : "#d4493e", signed: true });
if (Math.abs(r.condDelta) > 5)
rows.push({ label: "Condition: " + r.condGrade, amt: r.condDelta, tone: r.condDelta >= 0 ? "#2f9e6f" : "#e0962a", signed: true });
if (Math.abs(r.flagDelta) > 5)
rows.push({ label: "Disclosures", amt: r.flagDelta, tone: "#d4493e", signed: true });
if (r.optTotal > 0)
rows.push({ label: "Optional equipment", amt: r.optTotal, tone: "#2f9e6f", signed: true });
var list = document.getElementById("breakdown");
list.innerHTML = "";
rows.forEach(function (row) {
var li = document.createElement("li");
var signed = row.signed ? (row.amt >= 0 ? "+" : "−") + money(Math.abs(row.amt)).slice(1) : money(row.amt);
var cls = row.signed ? (row.amt >= 0 ? "is-pos" : "is-neg") : "";
li.innerHTML =
'<span class="bd-label"><span class="bd-dot" style="background:' + row.tone + '"></span>' +
row.label + "</span>" +
'<span class="bd-amt ' + cls + '">' + (row.signed && row.amt >= 0 ? "+" : "") +
(row.signed ? signed : money(row.amt)) + "</span>";
list.appendChild(li);
});
var total = document.createElement("li");
total.className = "is-total";
total.innerHTML =
'<span class="bd-label">Estimated trade-in value</span>' +
'<span class="bd-amt mono">' + money(r.mid) + "</span>";
list.appendChild(total);
// Reset apply state.
applied = false;
var applyWrap = document.querySelector(".apply");
var applyBtn = document.getElementById("apply-btn");
var applyNote = document.getElementById("apply-note");
applyWrap.classList.remove("is-applied");
applyBtn.disabled = false;
applyBtn.textContent = "Apply to purchase";
applyNote.textContent = "Roll this credit straight into your next vehicle.";
return r;
}
/* ---- Events ---- */
form.addEventListener("click", function (e) {
var nav = e.target.closest("[data-go]");
if (nav) {
e.preventDefault();
var dest = Number(nav.getAttribute("data-go"));
if (dest === 2 && current === 1 && !validateStep1()) return;
goTo(dest);
}
});
form.addEventListener("submit", function (e) {
e.preventDefault();
if (!validateStep1()) {
goTo(1);
return;
}
renderResult();
goTo(3);
toast("Estimate ready — fictional appraisal.", true);
});
// Live re-compute if user tweaks condition/options while on the result step.
form.addEventListener("change", function (e) {
if (current !== 3) return;
if (e.target.name === "condition" || e.target.name === "opt" || e.target.name === "flag") {
renderResult();
}
});
// Apply to purchase.
document.getElementById("apply-btn").addEventListener("click", function () {
if (applied) return;
applied = true;
var r = compute();
var applyWrap = document.querySelector(".apply");
var applyNote = document.getElementById("apply-note");
applyWrap.classList.add("is-applied");
this.disabled = true;
this.textContent = "Credit applied ✓";
applyNote.textContent = money(r.mid) + " credit reserved on quote #TI-4827.";
toast("Trade-in credit applied to your purchase.", true);
});
// Restart.
document.getElementById("restart-btn").addEventListener("click", function () {
form.reset();
// Re-check the default "Good" condition radio after reset.
var good = form.querySelector('input[name="condition"][data-grade="Good"]');
if (good) good.checked = true;
document.getElementById("step1-error").hidden = true;
goTo(1);
toast("Cleared. Start a fresh appraisal.");
});
// Keep step1 error hidden as user types valid input.
["make", "model", "year", "mileage"].forEach(function (id) {
document.getElementById(id).addEventListener("input", function () {
var err = document.getElementById("step1-error");
if (!err.hidden) err.hidden = true;
});
});
goTo(1);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Auto — Trade-In Valuation</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="shell" aria-labelledby="appraise-title">
<header class="masthead">
<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="M5 13l1.5-4.5A2 2 0 0 1 8.4 7h7.2a2 2 0 0 1 1.9 1.5L19 13" />
<path d="M3 13h18v4a1 1 0 0 1-1 1h-1a1 1 0 0 1-1-1v-1H6v1a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z" />
<circle cx="7" cy="16" r="1" /><circle cx="17" cy="16" r="1" />
</svg>
</span>
<div class="brand__text">
<strong>Ironwood Auto & Diesel</strong>
<span>Trade-In Appraisal Desk</span>
</div>
</div>
<div class="masthead__meta">
<span class="quote-id">QUOTE #TI-4827</span>
<span class="valid">Estimate valid 7 days</span>
</div>
</header>
<section class="wizard" aria-labelledby="appraise-title">
<h1 id="appraise-title" class="visually-hidden">Trade-In Valuation</h1>
<ol class="steps" aria-label="Progress">
<li class="steps__item is-active" data-step-pip="1"><span class="steps__num">1</span><span class="steps__lbl">Vehicle</span></li>
<li class="steps__item" data-step-pip="2"><span class="steps__num">2</span><span class="steps__lbl">Condition</span></li>
<li class="steps__item" data-step-pip="3"><span class="steps__num">3</span><span class="steps__lbl">Estimate</span></li>
</ol>
<form id="appraise-form" novalidate>
<!-- STEP 1 -->
<section class="panel step" data-step="1" aria-label="Vehicle details">
<div class="panel__head">
<h2>Vehicle details</h2>
<p>Tell us what you're trading in. Every field shapes the offer.</p>
</div>
<div class="grid">
<label class="field">
<span class="field__label">Make</span>
<select id="make" name="make" required>
<option value="" disabled selected>Select make</option>
<option value="Ford" data-tier="1">Ford</option>
<option value="Toyota" data-tier="1.12">Toyota</option>
<option value="Honda" data-tier="1.1">Honda</option>
<option value="Chevrolet" data-tier="0.96">Chevrolet</option>
<option value="BMW" data-tier="1.28">BMW</option>
<option value="Subaru" data-tier="1.06">Subaru</option>
</select>
</label>
<label class="field">
<span class="field__label">Model</span>
<input id="model" name="model" type="text" placeholder="e.g. F-150" required autocomplete="off" />
</label>
<label class="field">
<span class="field__label">Year</span>
<select id="year" name="year" required>
<option value="" disabled selected>Year</option>
</select>
</label>
<label class="field">
<span class="field__label">Trim</span>
<select id="trim" name="trim">
<option value="1" selected>Base</option>
<option value="1.08">Mid / SE</option>
<option value="1.18">Premium / Limited</option>
<option value="1.34">Sport / Performance</option>
</select>
</label>
<label class="field">
<span class="field__label">Odometer</span>
<div class="input-suffix">
<input id="mileage" name="mileage" type="number" inputmode="numeric" min="0" max="350000" step="500" placeholder="64,200" required />
<span class="suffix">mi</span>
</div>
</label>
<label class="field">
<span class="field__label">ZIP code</span>
<input id="zip" name="zip" type="text" inputmode="numeric" maxlength="5" placeholder="80424" autocomplete="off" />
</label>
</div>
<p class="field-error" id="step1-error" role="alert" hidden>Add make, model, year and odometer to continue.</p>
<div class="panel__foot">
<span class="hint">Fictional appraisal — for demonstration.</span>
<button type="button" class="btn btn--primary" data-go="2">Continue<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"/></svg></button>
</div>
</section>
<!-- STEP 2 -->
<section class="panel step" data-step="2" hidden aria-label="Condition and options">
<div class="panel__head">
<h2>Condition & equipment</h2>
<p>Be honest — the desk inspects before final offer.</p>
</div>
<fieldset class="condition" aria-describedby="condition-note">
<legend>Overall condition</legend>
<div class="condition__opts" role="radiogroup" aria-label="Overall condition">
<label class="cond">
<input type="radio" name="condition" value="1.12" data-grade="Excellent" />
<span class="cond__card" data-tone="ok">
<span class="cond__grade">Excellent</span>
<span class="cond__desc">No mechanical issues, clean body, full records.</span>
<span class="cond__factor">+12%</span>
</span>
</label>
<label class="cond">
<input type="radio" name="condition" value="1.0" data-grade="Good" checked />
<span class="cond__card" data-tone="info">
<span class="cond__grade">Good</span>
<span class="cond__desc">Minor wear, runs well, a few cosmetic marks.</span>
<span class="cond__factor">Baseline</span>
</span>
</label>
<label class="cond">
<input type="radio" name="condition" value="0.88" data-grade="Fair" />
<span class="cond__card" data-tone="warn">
<span class="cond__grade">Fair</span>
<span class="cond__desc">Visible damage or a deferred repair or two.</span>
<span class="cond__factor">−12%</span>
</span>
</label>
<label class="cond">
<input type="radio" name="condition" value="0.7" data-grade="Rough" />
<span class="cond__card" data-tone="danger">
<span class="cond__grade">Rough</span>
<span class="cond__desc">Active fault codes, dents, or accident history.</span>
<span class="cond__factor">−30%</span>
</span>
</label>
</div>
<p class="condition__note" id="condition-note">A diagnostic scan adjusts the final figure on-site.</p>
</fieldset>
<fieldset class="options">
<legend>Value-adding options</legend>
<div class="options__grid">
<label class="opt"><input type="checkbox" name="opt" value="700" /><span class="opt__box"><span class="opt__name">Leather / heated seats</span><span class="opt__val">+$700</span></span></label>
<label class="opt"><input type="checkbox" name="opt" value="950" /><span class="opt__box"><span class="opt__name">All-wheel drive</span><span class="opt__val">+$950</span></span></label>
<label class="opt"><input type="checkbox" name="opt" value="450" /><span class="opt__box"><span class="opt__name">Tow package</span><span class="opt__val">+$450</span></span></label>
<label class="opt"><input type="checkbox" name="opt" value="600" /><span class="opt__box"><span class="opt__name">Sunroof / panoramic</span><span class="opt__val">+$600</span></span></label>
<label class="opt"><input type="checkbox" name="opt" value="800" /><span class="opt__box"><span class="opt__name">Adv. driver assist</span><span class="opt__val">+$800</span></span></label>
<label class="opt"><input type="checkbox" name="opt" value="320" /><span class="opt__box"><span class="opt__name">New tires (<5k mi)</span><span class="opt__val">+$320</span></span></label>
</div>
</fieldset>
<fieldset class="flags">
<legend>Disclosures</legend>
<label class="flag"><input type="checkbox" name="flag" value="0.82" /><span class="flag__box"><span class="flag__name">Reported accident</span><span class="flag__val">−18%</span></span></label>
<label class="flag"><input type="checkbox" name="flag" value="0.94" /><span class="flag__box"><span class="flag__name">2+ previous owners</span><span class="flag__val">−6%</span></span></label>
<label class="flag"><input type="checkbox" name="flag" value="0.9" /><span class="flag__box"><span class="flag__name">Active warning light</span><span class="flag__val">−10%</span></span></label>
</fieldset>
<div class="panel__foot">
<button type="button" class="btn btn--ghost" data-go="1"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5M11 6l-6 6 6 6"/></svg>Back</button>
<button type="submit" class="btn btn--primary">Get my estimate<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"/></svg></button>
</div>
</section>
<!-- STEP 3 -->
<section class="panel step step--result" data-step="3" hidden aria-label="Estimated value">
<div class="panel__head">
<h2>Estimated trade-in value</h2>
<p id="result-vehicle">Your vehicle, appraised.</p>
</div>
<div class="result">
<div class="vehicle-card" aria-hidden="true">
<div class="vehicle-card__photo"><span class="vehicle-card__tag">TRADE-IN</span></div>
<dl class="vehicle-card__spec">
<div><dt>Plate</dt><dd class="mono">8KLM-204</dd></div>
<div><dt>Odometer</dt><dd class="mono" id="vc-miles">—</dd></div>
<div><dt>Condition</dt><dd id="vc-cond">—</dd></div>
</dl>
</div>
<div class="gauge-wrap">
<div class="gauge" id="gauge" role="img" aria-label="Estimated value gauge">
<svg viewBox="0 0 200 120" class="gauge__svg">
<path class="gauge__track" d="M20 110 A 80 80 0 0 1 180 110" />
<path class="gauge__fill" id="gauge-fill" d="M20 110 A 80 80 0 0 1 180 110" />
<line class="gauge__needle" id="gauge-needle" x1="100" y1="110" x2="100" y2="40" />
<circle class="gauge__hub" cx="100" cy="110" r="6" />
</svg>
<div class="gauge__center">
<span class="gauge__label">Estimated mid-point</span>
<span class="gauge__value mono" id="gauge-value">$0</span>
</div>
</div>
<div class="range">
<div class="range__end"><span>Low</span><strong class="mono" id="range-low">$0</strong></div>
<div class="range__bar"><span class="range__pip" id="range-pip"></span></div>
<div class="range__end range__end--hi"><span>High</span><strong class="mono" id="range-high">$0</strong></div>
</div>
</div>
<ul class="breakdown" id="breakdown" aria-label="Value adjustments"></ul>
</div>
<div class="apply">
<div class="apply__copy">
<strong>Apply to a purchase</strong>
<span id="apply-note">Roll this credit straight into your next vehicle.</span>
</div>
<button type="button" class="btn btn--primary" id="apply-btn">Apply to purchase</button>
</div>
<div class="panel__foot">
<button type="button" class="btn btn--ghost" data-go="2"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5M11 6l-6 6 6 6"/></svg>Adjust details</button>
<button type="button" class="btn btn--quiet" id="restart-btn">Start over</button>
</div>
</section>
</form>
</section>
</main>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Trade-In Valuation
A three-step appraisal flow for Ironwood Auto & Diesel that takes a customer from raw vehicle details to a finished offer. Step one collects make, model, year, trim, odometer and ZIP with tabular number figures and inline validation. Step two is a card-based condition selector — Excellent, Good, Fair or Rough, each with a visible percentage factor — alongside checkable value-adding options and disclosure flags for accidents, extra owners or active warning lights.
Submitting runs a self-contained valuation model: an age-and-trim market base, a per-mile credit or penalty against expected mileage, a compounding condition multiplier, disclosure haircuts and flat option add-ons. The result animates in as a sweeping needle gauge with a counting headline figure, a low-to-high confidence range whose width grows with vehicle age, and an itemized breakdown that colour-codes every positive and negative adjustment. Tweaking condition or options on the result screen recomputes everything instantly, and an apply-to-purchase button reserves the credit against the quote.
The whole thing is vanilla HTML, CSS and JavaScript — no frameworks, build step or external assets
beyond the Inter font. The gauge is a single inline SVG arc driven by stroke-dashoffset and a
rotating needle, the layout reflows to a stacked mobile view below 520px, motion respects
prefers-reduced-motion, and every control stays keyboard-usable.
Illustrative UI only — fictional shop/dealership, not a real service system.