Auto — Vehicle Inventory Grid
A dealership pre-owned inventory grid built in an industrial, status-forward style. A sticky filter rail toggles make, body, and fuel chips, ranges trim price and mileage, and selects clamp the model-year window, while a search box scans across make, model, VIN, plate, and trim. Vehicle cards pair gradient photos with year, make, model, mileage, fuel badges, and tabular pricing. Sort the grid, save with the heart, quick-view any unit, then tick up to three cars into a compare tray that opens a best-value comparison table.
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;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 18px;
--shadow-1: 0 1px 2px rgba(20, 21, 24, 0.06), 0 4px 14px rgba(20, 21, 24, 0.05);
--shadow-2: 0 18px 50px rgba(20, 21, 24, 0.28);
--tnum: "tnum" 1, "lnum" 1;
}
* { 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;
}
.tnum { font-variant-numeric: tabular-nums lining-nums; font-feature-settings: var(--tnum); }
/* ---------- Topbar ---------- */
.topbar {
position: sticky;
top: 0;
z-index: 30;
display: flex;
align-items: center;
gap: 16px;
padding: 12px 20px;
background: var(--garage);
color: #fff;
border-bottom: 3px solid var(--orange);
}
.brand { display: flex; align-items: center; gap: 11px; flex-shrink: 0; }
.brand-mark {
width: 40px; height: 40px;
display: grid; place-items: center;
background: var(--orange);
color: var(--garage);
font-weight: 800;
font-size: 15px;
border-radius: var(--r-sm);
letter-spacing: -0.5px;
}
.brand-text { display: flex; flex-direction: column; line-height: 1.2; }
.brand-text strong { font-size: 15px; font-weight: 700; }
.brand-text span { font-size: 11.5px; color: var(--steel-l); letter-spacing: 0.2px; }
.search {
flex: 1;
max-width: 540px;
display: flex;
align-items: center;
gap: 9px;
background: var(--garage-2);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: var(--r-md);
padding: 0 14px;
height: 42px;
transition: border-color 0.15s, box-shadow 0.15s;
}
.search:focus-within {
border-color: var(--orange);
box-shadow: 0 0 0 3px rgba(255, 106, 19, 0.22);
}
.search-ico { width: 18px; height: 18px; color: var(--steel-l); flex-shrink: 0; }
.search input {
flex: 1;
background: none;
border: none;
outline: none;
color: #fff;
font: inherit;
font-size: 14px;
}
.search input::placeholder { color: var(--steel); }
.topbar-actions { display: flex; align-items: center; gap: 8px; margin-left: auto; }
.btn {
font: inherit;
font-weight: 600;
font-size: 13.5px;
border-radius: var(--r-sm);
border: 1px solid transparent;
padding: 9px 14px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 7px;
transition: transform 0.08s, background 0.15s, border-color 0.15s, color 0.15s;
}
.btn:active { transform: translateY(1px); }
.btn:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }
.btn-ghost { background: var(--garage-2); color: #fff; border-color: rgba(255, 255, 255, 0.12); }
.btn-ghost:hover { background: #2a2d35; }
.btn-primary { background: var(--orange); color: var(--garage); }
.btn-primary:hover { background: var(--orange-d); color: #fff; }
.count-pill {
min-width: 20px; height: 20px;
padding: 0 6px;
display: inline-grid; place-items: center;
background: var(--orange); color: var(--garage);
border-radius: 999px; font-size: 12px; font-weight: 800;
}
.btn-ghost .count-pill { background: rgba(255, 106, 19, 0.25); color: var(--orange); }
.only-mobile { display: none; }
/* ---------- Layout ---------- */
.layout {
display: grid;
grid-template-columns: 260px 1fr;
gap: 20px;
max-width: 1320px;
margin: 0 auto;
padding: 20px;
}
/* ---------- Rail ---------- */
.rail {
align-self: start;
position: sticky;
top: 78px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 16px;
box-shadow: var(--shadow-1);
}
.rail-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 6px; }
.rail-head h2 { margin: 0; font-size: 15px; font-weight: 700; letter-spacing: -0.2px; }
.link-btn {
background: none; border: none; cursor: pointer;
color: var(--orange-d); font: inherit; font-size: 12.5px; font-weight: 600;
padding: 2px 0;
}
.link-btn:hover { text-decoration: underline; }
.filter-block { border: none; margin: 0; padding: 14px 0 0; border-top: 1px solid var(--line); margin-top: 12px; }
.filter-block:first-of-type { border-top: none; margin-top: 4px; }
.filter-block legend {
font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.7px;
color: var(--steel); padding: 0; margin-bottom: 9px;
display: flex; justify-content: space-between; width: 100%; align-items: baseline;
}
.legend-val { color: var(--ink); font-weight: 700; font-size: 12px; text-transform: none; letter-spacing: 0; font-variant-numeric: tabular-nums; }
.chip-row { display: flex; flex-wrap: wrap; gap: 7px; }
.chip {
font: inherit; font-size: 12.5px; font-weight: 600;
padding: 6px 11px;
border: 1px solid var(--line-2);
background: var(--surface);
color: var(--ink-2);
border-radius: 999px;
cursor: pointer;
transition: all 0.12s;
}
.chip:hover { border-color: var(--steel); }
.chip[aria-pressed="true"] {
background: var(--garage); color: #fff; border-color: var(--garage);
}
.chip:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }
.range-row { display: flex; gap: 10px; }
.mini { flex: 1; font-size: 11px; font-weight: 600; color: var(--steel); text-transform: uppercase; letter-spacing: 0.4px; display: flex; flex-direction: column; gap: 5px; }
select {
font: inherit; font-size: 13.5px; font-weight: 500;
padding: 8px 10px;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
background: var(--surface);
color: var(--ink);
cursor: pointer;
font-variant-numeric: tabular-nums;
}
select:focus-visible { outline: 2px solid var(--orange); outline-offset: 1px; }
input[type="range"] {
width: 100%;
-webkit-appearance: none; appearance: none;
height: 6px; border-radius: 999px;
background: var(--line-2);
cursor: pointer; margin: 8px 0 2px;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none;
width: 20px; height: 20px; border-radius: 50%;
background: var(--orange); border: 3px solid var(--surface);
box-shadow: 0 1px 4px rgba(20, 21, 24, 0.3); cursor: pointer;
}
input[type="range"]::-moz-range-thumb {
width: 20px; height: 20px; border-radius: 50%;
background: var(--orange); border: 3px solid var(--surface);
box-shadow: 0 1px 4px rgba(20, 21, 24, 0.3); cursor: pointer;
}
/* ---------- Results ---------- */
.results { min-width: 0; }
.results-bar {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 16px; gap: 12px;
}
.result-count { margin: 0; color: var(--muted); font-size: 14px; }
.result-count strong { color: var(--ink); font-size: 18px; font-variant-numeric: tabular-nums; }
.sort { display: flex; align-items: center; gap: 8px; }
.sort label { font-size: 12.5px; font-weight: 600; color: var(--steel); }
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 16px;
}
/* ---------- Card ---------- */
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
overflow: hidden;
box-shadow: var(--shadow-1);
display: flex; flex-direction: column;
transition: transform 0.14s, box-shadow 0.14s, border-color 0.14s;
}
.card:hover { transform: translateY(-3px); box-shadow: var(--shadow-2); border-color: var(--line-2); }
.photo {
position: relative;
aspect-ratio: 16 / 10;
background: linear-gradient(135deg, var(--g1, #2b2f3a), var(--g2, #4a5260));
display: flex; align-items: flex-end;
padding: 10px;
}
.photo::after {
content: "";
position: absolute; inset: 0;
background: radial-gradient(120% 80% at 70% 10%, rgba(255, 255, 255, 0.22), transparent 55%),
linear-gradient(0deg, rgba(20, 21, 24, 0.35), transparent 45%);
pointer-events: none;
}
.photo-badges { position: relative; z-index: 1; display: flex; gap: 6px; flex-wrap: wrap; }
.badge {
font-size: 10.5px; font-weight: 800; letter-spacing: 0.5px; text-transform: uppercase;
padding: 4px 8px; border-radius: 999px;
background: rgba(20, 21, 24, 0.72); color: #fff; backdrop-filter: blur(4px);
}
.badge.hot { background: var(--orange); color: var(--garage); }
.badge.cert { background: var(--ok); }
.badge.reduced { background: var(--danger); }
.save-toggle {
position: absolute; top: 10px; right: 10px; z-index: 2;
width: 36px; height: 36px; border-radius: 50%;
border: none; cursor: pointer;
background: rgba(20, 21, 24, 0.55); color: #fff;
display: grid; place-items: center;
backdrop-filter: blur(4px);
transition: background 0.14s, transform 0.1s;
}
.save-toggle:hover { background: rgba(20, 21, 24, 0.8); }
.save-toggle:active { transform: scale(0.9); }
.save-toggle svg { width: 19px; height: 19px; }
.save-toggle[aria-pressed="true"] { background: var(--orange); color: var(--garage); }
.save-toggle[aria-pressed="true"] svg { fill: currentColor; }
.save-toggle:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.vin-strip {
position: absolute; bottom: 10px; right: 10px; z-index: 1;
font-size: 10px; font-weight: 700; letter-spacing: 0.6px;
color: rgba(255, 255, 255, 0.82);
background: rgba(20, 21, 24, 0.5); padding: 3px 7px; border-radius: 6px;
font-variant-numeric: tabular-nums;
}
.card-body { padding: 13px 14px 14px; display: flex; flex-direction: column; gap: 10px; flex: 1; }
.card-title { margin: 0; font-size: 15.5px; font-weight: 700; letter-spacing: -0.3px; line-height: 1.25; }
.card-title .yr { color: var(--orange-d); }
.card-trim { font-size: 12.5px; color: var(--muted); font-weight: 500; margin-top: -4px; }
.spec-row { display: flex; gap: 6px; flex-wrap: wrap; }
.spec {
font-size: 11.5px; font-weight: 600; color: var(--ink-2);
background: var(--bg); border: 1px solid var(--line);
padding: 4px 8px; border-radius: 6px;
display: inline-flex; align-items: center; gap: 5px;
font-variant-numeric: tabular-nums;
}
.spec b { color: var(--ink); font-weight: 700; }
.price-row {
display: flex; align-items: flex-end; justify-content: space-between;
margin-top: auto; padding-top: 4px;
}
.price { font-size: 21px; font-weight: 800; letter-spacing: -0.6px; font-variant-numeric: tabular-nums; }
.price s { display: block; font-size: 12px; font-weight: 600; color: var(--muted); margin-bottom: -2px; }
.quick-btn {
font: inherit; font-size: 12.5px; font-weight: 700;
border: 1px solid var(--line-2); background: var(--surface); color: var(--ink);
border-radius: var(--r-sm); padding: 8px 12px; cursor: pointer;
transition: all 0.12s;
}
.quick-btn:hover { background: var(--garage); color: #fff; border-color: var(--garage); }
.quick-btn:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }
.compare-check {
display: flex; align-items: center; gap: 7px;
font-size: 12px; font-weight: 600; color: var(--steel);
cursor: pointer; padding-top: 9px; border-top: 1px solid var(--line);
}
.compare-check input { width: 16px; height: 16px; accent-color: var(--orange); cursor: pointer; }
/* ---------- Empty ---------- */
.empty {
text-align: center; padding: 60px 20px;
display: flex; flex-direction: column; align-items: center; gap: 8px;
color: var(--muted);
border: 2px dashed var(--line-2); border-radius: var(--r-lg);
}
.empty strong { font-size: 18px; color: var(--ink); }
.empty .btn { margin-top: 10px; }
/* ---------- Modal ---------- */
.modal { position: fixed; inset: 0; z-index: 60; display: grid; place-items: center; padding: 20px; }
.modal[hidden] { display: none; }
.modal-backdrop { position: absolute; inset: 0; background: rgba(20, 21, 24, 0.62); backdrop-filter: blur(3px); animation: fade 0.18s ease; }
.modal-card {
position: relative; z-index: 1;
width: 100%; max-width: 540px; max-height: 90vh; overflow: auto;
background: var(--surface); border-radius: var(--r-lg);
box-shadow: var(--shadow-2);
animation: pop 0.2s cubic-bezier(0.2, 0.9, 0.3, 1.2);
}
.modal-wide { max-width: 880px; }
@keyframes fade { from { opacity: 0; } }
@keyframes pop { from { opacity: 0; transform: translateY(14px) scale(0.97); } }
.qv-photo {
aspect-ratio: 16 / 8; position: relative;
background: linear-gradient(135deg, var(--g1, #2b2f3a), var(--g2, #4a5260));
display: flex; align-items: flex-end; padding: 14px;
}
.qv-photo .photo-badges { position: relative; z-index: 1; }
.qv-close {
position: absolute; top: 12px; right: 12px; z-index: 2;
width: 36px; height: 36px; border-radius: 50%; border: none; cursor: pointer;
background: rgba(20, 21, 24, 0.6); color: #fff; font-size: 20px; line-height: 1;
display: grid; place-items: center;
}
.qv-close:hover { background: rgba(20, 21, 24, 0.85); }
.qv-body { padding: 18px 20px 22px; }
.qv-body h3 { margin: 0 0 2px; font-size: 22px; font-weight: 800; letter-spacing: -0.5px; }
.qv-body h3 .yr { color: var(--orange-d); }
.qv-trim { color: var(--muted); font-size: 13.5px; margin: 0 0 14px; }
.qv-price { font-size: 30px; font-weight: 800; letter-spacing: -1px; font-variant-numeric: tabular-nums; margin-bottom: 16px; }
.qv-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--line); border: 1px solid var(--line); border-radius: var(--r-md); overflow: hidden; margin-bottom: 18px; }
.qv-cell { background: var(--surface); padding: 11px 13px; }
.qv-cell .k { font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.6px; color: var(--steel); }
.qv-cell .v { font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums; }
.qv-actions { display: flex; gap: 10px; }
.qv-actions .btn { flex: 1; justify-content: center; }
/* ---------- Compare tray ---------- */
.compare-tray {
position: fixed; bottom: 0; left: 0; right: 0; z-index: 40;
background: var(--garage); color: #fff;
border-top: 3px solid var(--orange);
display: flex; align-items: center; gap: 14px;
padding: 12px 20px; box-shadow: 0 -10px 30px rgba(20, 21, 24, 0.25);
}
.compare-tray[hidden] { display: none; }
.tray-inner { display: flex; gap: 10px; flex: 1; overflow-x: auto; padding-bottom: 2px; }
.tray-item {
display: flex; align-items: center; gap: 8px; flex-shrink: 0;
background: var(--garage-2); border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: var(--r-sm); padding: 6px 8px 6px 6px;
}
.tray-thumb { width: 38px; height: 30px; border-radius: 5px; flex-shrink: 0; background: linear-gradient(135deg, var(--g1, #2b2f3a), var(--g2, #4a5260)); }
.tray-meta { display: flex; flex-direction: column; line-height: 1.25; }
.tray-meta strong { font-size: 12.5px; }
.tray-meta span { font-size: 11px; color: var(--steel-l); font-variant-numeric: tabular-nums; }
.tray-x { background: none; border: none; color: var(--steel-l); font-size: 17px; cursor: pointer; padding: 0 4px; line-height: 1; }
.tray-x:hover { color: #fff; }
/* compare table */
.cmp-head { padding: 16px 20px 12px; border-bottom: 1px solid var(--line); display: flex; align-items: center; justify-content: space-between; }
.cmp-head h3 { margin: 0; font-size: 18px; font-weight: 800; letter-spacing: -0.4px; }
.cmp-scroll { overflow-x: auto; }
.cmp-table { width: 100%; border-collapse: collapse; min-width: 460px; }
.cmp-table th, .cmp-table td { padding: 11px 13px; text-align: left; border-bottom: 1px solid var(--line); font-size: 13.5px; }
.cmp-table thead th { background: var(--bg); font-size: 11px; }
.cmp-table .veh-head { font-weight: 800; font-size: 14px; line-height: 1.3; }
.cmp-table .veh-head small { display: block; font-weight: 500; color: var(--muted); font-size: 11.5px; }
.cmp-table .rowlabel { font-weight: 700; color: var(--steel); font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; white-space: nowrap; }
.cmp-table td.val { font-weight: 600; font-variant-numeric: tabular-nums; }
.cmp-table td.best { color: var(--ok); }
.cmp-thumb { width: 100%; height: 50px; border-radius: var(--r-sm); background: linear-gradient(135deg, var(--g1, #2b2f3a), var(--g2, #4a5260)); margin-bottom: 7px; }
/* ---------- Toast ---------- */
.toast {
position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%) translateY(20px);
background: var(--garage); color: #fff;
padding: 11px 18px; border-radius: var(--r-md);
font-size: 13.5px; font-weight: 600;
box-shadow: var(--shadow-2);
border-left: 3px solid var(--orange);
opacity: 0; pointer-events: none; z-index: 80;
transition: opacity 0.2s, transform 0.2s;
max-width: calc(100vw - 40px);
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
/* ---------- Responsive ---------- */
@media (max-width: 920px) {
.layout { grid-template-columns: 1fr; }
.rail {
position: fixed; inset: 0; top: auto; z-index: 50;
max-height: 82vh; overflow: auto; border-radius: var(--r-lg) var(--r-lg) 0 0;
transform: translateY(100%); transition: transform 0.25s ease;
box-shadow: var(--shadow-2);
}
.rail.open { transform: translateY(0); }
.only-mobile { display: inline-flex; }
}
@media (max-width: 520px) {
.topbar { flex-wrap: wrap; padding: 10px 14px; gap: 10px; }
.search { order: 3; max-width: none; flex-basis: 100%; height: 40px; }
.brand-text span { display: none; }
.layout { padding: 14px; gap: 14px; }
.grid { grid-template-columns: 1fr; }
.results-bar { flex-wrap: wrap; }
.qv-grid { grid-template-columns: 1fr; }
.modal { padding: 0; align-items: flex-end; }
.modal-card { max-width: none; max-height: 92vh; border-radius: var(--r-lg) var(--r-lg) 0 0; }
}(function () {
"use strict";
// ---------- Data (fictional inventory) ----------
var VEHICLES = [
{ id: "v1", year: 2022, make: "Ford", model: "F-150 XLT", trim: "4WD · SuperCrew · 3.5L EcoBoost", body: "Truck", fuel: "Gas", price: 41980, msrp: 44900, miles: 28450, vin: "1FTFW1E84NK", plate: "8XT 4192", g1: "#2a3b4f", g2: "#4d6480", badges: ["Certified"], feat: 9 },
{ id: "v2", year: 2021, make: "Tesla", model: "Model 3", trim: "Long Range · Dual Motor AWD", body: "Sedan", fuel: "Electric", price: 33450, msrp: 33450, miles: 31200, vin: "5YJ3E1EB6MF", plate: "ELC 220", g1: "#1f2a44", g2: "#3a4f7a", badges: ["Hot deal"], feat: 8 },
{ id: "v3", year: 2020, make: "Toyota", model: "RAV4 LE", trim: "AWD · 2.5L · 8-Spd Auto", body: "SUV", fuel: "Gas", price: 24990, msrp: 27100, miles: 49870, vin: "2T3F1RFV4LC", plate: "7RV 901", g1: "#3a2f24", g2: "#6b5238", badges: ["Reduced"], feat: 6 },
{ id: "v4", year: 2023, make: "Honda", model: "Civic Sport", trim: "FWD · 2.0L · CVT", body: "Sedan", fuel: "Gas", price: 27340, msrp: 27340, miles: 12100, vin: "2HGFE2F58PH", plate: "9CV 778", g1: "#27343a", g2: "#46606a", badges: ["Certified"], feat: 7 },
{ id: "v5", year: 2019, make: "Chevrolet", model: "Silverado LT", trim: "4WD · 5.3L V8 · Crew Cab", body: "Truck", fuel: "Gas", price: 32600, msrp: 36500, miles: 64300, vin: "3GCUYDED9KG", plate: "5SL 330", g1: "#33231f", g2: "#5e3d33", badges: ["Reduced"], feat: 5 },
{ id: "v6", year: 2022, make: "Hyundai", model: "Ioniq 5 SEL", trim: "RWD · 77.4 kWh", body: "SUV", fuel: "Electric", price: 38900, msrp: 41200, miles: 19600, vin: "KM8KNDAF0NU", plate: "ION 5XX", g1: "#1e3340", g2: "#386076", badges: ["Hot deal", "Certified"], feat: 8 },
{ id: "v7", year: 2018, make: "Toyota", model: "Camry SE", trim: "FWD · 2.5L · 8-Spd Auto", body: "Sedan", fuel: "Gas", price: 18750, msrp: 18750, miles: 78400, vin: "4T1B11HK2JU", plate: "2CM 614", g1: "#2c2c34", g2: "#4d4d5c", badges: [], feat: 4 },
{ id: "v8", year: 2021, make: "Ford", model: "Escape SE", trim: "AWD · 1.5L Hybrid", body: "SUV", fuel: "Hybrid", price: 26450, msrp: 28000, miles: 41200, vin: "1FMCU9G63MU", plate: "6ES 207", g1: "#243a34", g2: "#3f6b5b", badges: ["Reduced"], feat: 6 },
{ id: "v9", year: 2023, make: "Tesla", model: "Model Y", trim: "Long Range · AWD", body: "SUV", fuel: "Electric", price: 46900, msrp: 46900, miles: 9800, vin: "7SAYGDEE8PF", plate: "MDY 023", g1: "#1c2740", g2: "#34487a", badges: ["Hot deal"], feat: 9 },
{ id: "v10", year: 2017, make: "Honda", model: "CR-V EX", trim: "AWD · 1.5L Turbo", body: "SUV", fuel: "Gas", price: 19980, msrp: 19980, miles: 92600, vin: "2HKRW2H56HH", plate: "1CR 845", g1: "#2e2a26", g2: "#54493f", badges: [], feat: 3 },
{ id: "v11", year: 2020, make: "Chevrolet", model: "Bolt EV LT", trim: "FWD · 66 kWh", body: "Sedan", fuel: "Electric", price: 17900, msrp: 21500, miles: 38900, vin: "1G1FY6S07L4", plate: "BLT 100", g1: "#1f3a3a", g2: "#357070", badges: ["Reduced"], feat: 5 },
{ id: "v12", year: 2022, make: "Hyundai", model: "Tucson SEL", trim: "AWD · 2.5L · 8-Spd Auto", body: "SUV", fuel: "Gas", price: 28700, msrp: 30100, miles: 24300, vin: "5NMJBCAE5NH", plate: "TUC 552", g1: "#2a3340", g2: "#465a72", badges: ["Certified"], feat: 7 }
];
var $ = function (s, r) { return (r || document).querySelector(s); };
var fmtMoney = function (n) { return "$" + n.toLocaleString("en-US"); };
var fmtMiles = function (n) { return n.toLocaleString("en-US") + " mi"; };
var state = {
q: "",
makes: new Set(),
bodies: new Set(),
fuels: new Set(),
yearMin: null,
yearMax: null,
maxPrice: 80000,
maxMiles: 150000,
sort: "featured",
saved: new Set(),
compare: new Set()
};
var grid = $("#grid");
var empty = $("#empty");
// ---------- Toast ----------
var toastEl = $("#toast"), toastT;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastT);
toastT = setTimeout(function () { toastEl.classList.remove("show"); }, 2200);
}
// ---------- Build filters ----------
function uniq(key) {
var seen = {}, out = [];
VEHICLES.forEach(function (v) { if (!seen[v[key]]) { seen[v[key]] = 1; out.push(v[key]); } });
return out.sort();
}
function buildChips(containerId, values, set) {
var c = $("#" + containerId);
values.forEach(function (val) {
var b = document.createElement("button");
b.type = "button";
b.className = "chip";
b.textContent = val;
b.setAttribute("aria-pressed", "false");
b.addEventListener("click", function () {
if (set.has(val)) { set.delete(val); b.setAttribute("aria-pressed", "false"); }
else { set.add(val); b.setAttribute("aria-pressed", "true"); }
render();
});
c.appendChild(b);
});
}
buildChips("makeFilters", uniq("make"), state.makes);
buildChips("bodyFilters", uniq("body"), state.bodies);
buildChips("fuelFilters", uniq("fuel"), state.fuels);
// year selects
var years = VEHICLES.map(function (v) { return v.year; });
var minY = Math.min.apply(null, years), maxY = Math.max.apply(null, years);
var ySelMin = $("#yearMin"), ySelMax = $("#yearMax");
for (var y = maxY; y >= minY; y--) {
var o1 = document.createElement("option"); o1.value = y; o1.textContent = y; ySelMin.appendChild(o1);
var o2 = document.createElement("option"); o2.value = y; o2.textContent = y; ySelMax.appendChild(o2);
}
ySelMin.value = minY; ySelMax.value = maxY;
state.yearMin = minY; state.yearMax = maxY;
// ---------- Inputs ----------
$("#search").addEventListener("input", function (e) { state.q = e.target.value.trim().toLowerCase(); render(); });
$("#sort").addEventListener("change", function (e) { state.sort = e.target.value; render(); });
ySelMin.addEventListener("change", function (e) { state.yearMin = +e.target.value; render(); });
ySelMax.addEventListener("change", function (e) { state.yearMax = +e.target.value; render(); });
var priceRange = $("#priceRange"), priceVal = $("#priceVal");
priceRange.addEventListener("input", function (e) {
state.maxPrice = +e.target.value;
priceVal.textContent = state.maxPrice >= 80000 ? "$80,000+" : fmtMoney(state.maxPrice);
render();
});
var mileRange = $("#mileRange"), mileVal = $("#mileVal");
mileRange.addEventListener("input", function (e) {
state.maxMiles = +e.target.value;
mileVal.textContent = state.maxMiles >= 150000 ? "150,000+ mi" : fmtMiles(state.maxMiles);
render();
});
function resetAll() {
state.q = ""; $("#search").value = "";
state.makes.clear(); state.bodies.clear(); state.fuels.clear();
state.maxPrice = 80000; priceRange.value = 80000; priceVal.textContent = "$80,000+";
state.maxMiles = 150000; mileRange.value = 150000; mileVal.textContent = "150,000+ mi";
state.yearMin = minY; state.yearMax = maxY; ySelMin.value = minY; ySelMax.value = maxY;
document.querySelectorAll(".chip[aria-pressed='true']").forEach(function (c) { c.setAttribute("aria-pressed", "false"); });
render();
toast("Filters reset");
}
$("#resetBtn").addEventListener("click", resetAll);
$("#emptyReset").addEventListener("click", resetAll);
// mobile filter toggle
var rail = $("#rail"), filterToggle = $("#filterToggle");
filterToggle.addEventListener("click", function () {
var open = rail.classList.toggle("open");
filterToggle.setAttribute("aria-expanded", open ? "true" : "false");
});
// ---------- Filter + sort ----------
function filtered() {
var list = VEHICLES.filter(function (v) {
if (state.makes.size && !state.makes.has(v.make)) return false;
if (state.bodies.size && !state.bodies.has(v.body)) return false;
if (state.fuels.size && !state.fuels.has(v.fuel)) return false;
if (v.year < state.yearMin || v.year > state.yearMax) return false;
if (v.price > state.maxPrice) return false;
if (v.miles > state.maxMiles) return false;
if (state.q) {
var hay = (v.year + " " + v.make + " " + v.model + " " + v.trim + " " + v.vin + " " + v.plate + " " + v.body + " " + v.fuel).toLowerCase();
if (hay.indexOf(state.q) === -1) return false;
}
return true;
});
var s = state.sort;
list.sort(function (a, b) {
if (s === "price-asc") return a.price - b.price;
if (s === "price-desc") return b.price - a.price;
if (s === "mile-asc") return a.miles - b.miles;
if (s === "year-desc") return b.year - a.year || a.miles - b.miles;
return b.feat - a.feat || a.price - b.price; // featured
});
return list;
}
// ---------- Card ----------
function badgeMarkup(v) {
return v.badges.map(function (b) {
var cls = b === "Hot deal" ? "hot" : b === "Certified" ? "cert" : b === "Reduced" ? "reduced" : "";
return '<span class="badge ' + cls + '">' + b + "</span>";
}).join("");
}
var heartPath = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 21s-7.5-4.6-10-9.3C.3 8.4 2 5 5.2 5c2 0 3.3 1.1 4 2.2C9.9 6.1 11.2 5 13.2 5 16.4 5 18 8.4 16.3 11.7 13.8 16.4 12 21 12 21z" fill="none" stroke="currentColor" stroke-width="2"/></svg>';
function cardMarkup(v) {
var saved = state.saved.has(v.id);
var inCmp = state.compare.has(v.id);
var reduced = v.price < v.msrp;
return '' +
'<article class="card" data-id="' + v.id + '" style="--g1:' + v.g1 + ';--g2:' + v.g2 + '">' +
'<div class="photo" style="--g1:' + v.g1 + ';--g2:' + v.g2 + '">' +
'<button class="save-toggle" type="button" aria-pressed="' + saved + '" aria-label="Save vehicle" data-save>' + heartPath + '</button>' +
'<div class="photo-badges">' + badgeMarkup(v) + '</div>' +
'<span class="vin-strip tnum">' + v.body.toUpperCase() + '</span>' +
'</div>' +
'<div class="card-body">' +
'<h3 class="card-title"><span class="yr">' + v.year + '</span> ' + v.make + " " + v.model + '</h3>' +
'<p class="card-trim">' + v.trim + '</p>' +
'<div class="spec-row">' +
'<span class="spec tnum"><b>' + fmtMiles(v.miles) + '</b></span>' +
'<span class="spec">' + v.fuel + '</span>' +
'<span class="spec">' + v.body + '</span>' +
'</div>' +
'<div class="price-row">' +
'<div class="price tnum">' + (reduced ? '<s>' + fmtMoney(v.msrp) + '</s>' : '') + fmtMoney(v.price) + '</div>' +
'<button class="quick-btn" type="button" data-quick>Quick view</button>' +
'</div>' +
'<label class="compare-check"><input type="checkbox" data-compare ' + (inCmp ? "checked" : "") + '> Compare</label>' +
'</div>' +
'</article>';
}
function render() {
var list = filtered();
$("#count").textContent = list.length;
if (!list.length) {
grid.innerHTML = "";
empty.hidden = false;
} else {
empty.hidden = true;
grid.innerHTML = list.map(cardMarkup).join("");
}
syncCompareUI();
}
// ---------- Card events (delegated) ----------
grid.addEventListener("click", function (e) {
var card = e.target.closest(".card");
if (!card) return;
var v = byId(card.getAttribute("data-id"));
if (e.target.closest("[data-save]")) {
var btn = e.target.closest("[data-save]");
if (state.saved.has(v.id)) { state.saved.delete(v.id); btn.setAttribute("aria-pressed", "false"); toast("Removed from saved"); }
else { state.saved.add(v.id); btn.setAttribute("aria-pressed", "true"); toast("Saved " + v.year + " " + v.make + " " + v.model); }
return;
}
if (e.target.closest("[data-quick]")) { openQuick(v); }
});
grid.addEventListener("change", function (e) {
if (!e.target.matches("[data-compare]")) return;
var card = e.target.closest(".card");
var v = byId(card.getAttribute("data-id"));
toggleCompare(v, e.target.checked);
});
function byId(id) { for (var i = 0; i < VEHICLES.length; i++) if (VEHICLES[i].id === id) return VEHICLES[i]; }
// ---------- Compare ----------
function toggleCompare(v, on) {
if (on) {
if (state.compare.size >= 3) {
toast("Compare up to 3 vehicles");
var cb = document.querySelector('.card[data-id="' + v.id + '"] [data-compare]');
if (cb) cb.checked = false;
return;
}
state.compare.add(v.id);
} else {
state.compare.delete(v.id);
}
syncCompareUI();
}
function syncCompareUI() {
var n = state.compare.size;
$("#compareCount").textContent = n;
var tray = $("#compareTray"), inner = $("#trayInner");
if (!n) { tray.hidden = true; inner.innerHTML = ""; return; }
tray.hidden = false;
inner.innerHTML = Array.from(state.compare).map(function (id) {
var v = byId(id);
return '<div class="tray-item">' +
'<div class="tray-thumb" style="--g1:' + v.g1 + ';--g2:' + v.g2 + '"></div>' +
'<div class="tray-meta"><strong>' + v.make + " " + v.model + '</strong><span class="tnum">' + v.year + " · " + fmtMoney(v.price) + '</span></div>' +
'<button class="tray-x" type="button" data-remove="' + id + '" aria-label="Remove">×</button>' +
'</div>';
}).join("");
}
$("#trayInner").addEventListener("click", function (e) {
var rm = e.target.closest("[data-remove]");
if (!rm) return;
var id = rm.getAttribute("data-remove");
state.compare.delete(id);
var cb = document.querySelector('.card[data-id="' + id + '"] [data-compare]');
if (cb) cb.checked = false;
syncCompareUI();
});
$("#compareBtn").addEventListener("click", openCompare);
$("#trayView").addEventListener("click", openCompare);
function openCompare() {
if (state.compare.size < 2) { toast("Pick at least 2 vehicles to compare"); return; }
var vs = Array.from(state.compare).map(byId);
var minP = Math.min.apply(null, vs.map(function (v) { return v.price; }));
var minM = Math.min.apply(null, vs.map(function (v) { return v.miles; }));
var maxYr = Math.max.apply(null, vs.map(function (v) { return v.year; }));
var rows = [
{ label: "Price", get: function (v) { return fmtMoney(v.price); }, best: function (v) { return v.price === minP; } },
{ label: "Mileage", get: function (v) { return fmtMiles(v.miles); }, best: function (v) { return v.miles === minM; } },
{ label: "Year", get: function (v) { return String(v.year); }, best: function (v) { return v.year === maxYr; } },
{ label: "Body", get: function (v) { return v.body; } },
{ label: "Fuel", get: function (v) { return v.fuel; } },
{ label: "Drivetrain", get: function (v) { return v.trim; } },
{ label: "VIN", get: function (v) { return v.vin + "…"; } }
];
var head = '<tr><th></th>' + vs.map(function (v) {
return '<th><div class="cmp-thumb" style="--g1:' + v.g1 + ';--g2:' + v.g2 + '"></div><div class="veh-head">' + v.make + " " + v.model + '<small>' + v.year + " · " + v.trim + '</small></div></th>';
}).join("") + "</tr>";
var body = rows.map(function (r) {
return "<tr><td class=\"rowlabel\">" + r.label + "</td>" + vs.map(function (v) {
var best = r.best && r.best(v) ? " best" : "";
return '<td class="val' + best + '">' + r.get(v) + (best ? " ✓" : "") + "</td>";
}).join("") + "</tr>";
}).join("");
$("#cmpCard").innerHTML =
'<div class="cmp-head"><h3 id="cmpTitle">Compare ' + vs.length + ' vehicles</h3>' +
'<button class="qv-close" type="button" data-close style="position:static">×</button></div>' +
'<div class="cmp-scroll"><table class="cmp-table"><thead>' + head + '</thead><tbody>' + body + '</tbody></table></div>';
showModal("#compareModal");
}
// ---------- Quick view ----------
function openQuick(v) {
var reduced = v.price < v.msrp;
$("#qvCard").innerHTML =
'<div class="qv-photo" style="--g1:' + v.g1 + ';--g2:' + v.g2 + '">' +
'<button class="qv-close" type="button" data-close aria-label="Close">×</button>' +
'<div class="photo-badges">' + badgeMarkup(v) + '</div>' +
'</div>' +
'<div class="qv-body">' +
'<h3 id="qvTitle"><span class="yr">' + v.year + '</span> ' + v.make + " " + v.model + '</h3>' +
'<p class="qv-trim">' + v.trim + '</p>' +
'<div class="qv-price tnum">' + (reduced ? '<s style="font-size:15px;color:var(--muted);font-weight:600;margin-right:8px;">' + fmtMoney(v.msrp) + '</s>' : '') + fmtMoney(v.price) + '</div>' +
'<div class="qv-grid">' +
cell("Mileage", fmtMiles(v.miles)) +
cell("Body / Fuel", v.body + " · " + v.fuel) +
cell("VIN", v.vin + "…") +
cell("Plate", v.plate) +
'</div>' +
'<div class="qv-actions">' +
'<button class="btn btn-primary" type="button" data-qsave>' + (state.saved.has(v.id) ? "Saved ✓" : "Save vehicle") + '</button>' +
'<button class="btn btn-ghost" type="button" data-close style="background:var(--bg);color:var(--ink);border-color:var(--line-2)">Close</button>' +
'</div>' +
'</div>';
var qsave = $("#qvCard").querySelector("[data-qsave]");
qsave.addEventListener("click", function () {
if (state.saved.has(v.id)) { state.saved.delete(v.id); qsave.textContent = "Save vehicle"; toast("Removed from saved"); }
else { state.saved.add(v.id); qsave.textContent = "Saved ✓"; toast("Saved " + v.make + " " + v.model); }
var cardBtn = document.querySelector('.card[data-id="' + v.id + '"] [data-save]');
if (cardBtn) cardBtn.setAttribute("aria-pressed", state.saved.has(v.id) ? "true" : "false");
});
showModal("#quickView");
}
function cell(k, v) { return '<div class="qv-cell"><div class="k">' + k + '</div><div class="v">' + v + "</div></div>"; }
// ---------- Modal helpers ----------
var lastFocus = null;
function showModal(sel) {
var m = $(sel);
lastFocus = document.activeElement;
m.hidden = false;
document.body.style.overflow = "hidden";
}
function closeModals() {
["#quickView", "#compareModal"].forEach(function (s) { $(s).hidden = true; });
document.body.style.overflow = "";
if (lastFocus && lastFocus.focus) lastFocus.focus();
}
document.addEventListener("click", function (e) { if (e.target.closest("[data-close]")) closeModals(); });
document.addEventListener("keydown", function (e) {
if (e.key === "Escape") {
if (!$("#quickView").hidden || !$("#compareModal").hidden) closeModals();
else if (rail.classList.contains("open")) { rail.classList.remove("open"); filterToggle.setAttribute("aria-expanded", "false"); }
}
});
priceVal.textContent = "$80,000+";
mileVal.textContent = "150,000+ mi";
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Northgate Motors — Inventory</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>
<header class="topbar">
<div class="brand">
<div class="brand-mark" aria-hidden="true">NM</div>
<div class="brand-text">
<strong>Northgate Motors</strong>
<span>Pre-Owned Inventory · Lot B</span>
</div>
</div>
<div class="search">
<svg viewBox="0 0 24 24" aria-hidden="true" class="search-ico"><path d="M21 21l-4.3-4.3M11 18a7 7 0 1 1 0-14 7 7 0 0 1 0 14z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
<input id="search" type="search" placeholder="Search make, model, VIN, plate…" aria-label="Search inventory" autocomplete="off" />
</div>
<div class="topbar-actions">
<button id="compareBtn" class="btn btn-ghost" type="button" aria-haspopup="dialog">
<span>Compare</span><span class="count-pill" id="compareCount">0</span>
</button>
<button id="filterToggle" class="btn btn-ghost only-mobile" type="button" aria-expanded="false">Filters</button>
</div>
</header>
<main class="layout">
<aside class="rail" id="rail" aria-label="Filters">
<div class="rail-head">
<h2>Filters</h2>
<button id="resetBtn" class="link-btn" type="button">Reset all</button>
</div>
<fieldset class="filter-block">
<legend>Make</legend>
<div id="makeFilters" class="chip-row"></div>
</fieldset>
<fieldset class="filter-block">
<legend>Body</legend>
<div id="bodyFilters" class="chip-row"></div>
</fieldset>
<fieldset class="filter-block">
<legend>Fuel</legend>
<div id="fuelFilters" class="chip-row"></div>
</fieldset>
<fieldset class="filter-block">
<legend>Model year</legend>
<div class="range-row">
<label class="mini">From
<select id="yearMin" aria-label="Minimum year"></select>
</label>
<label class="mini">To
<select id="yearMax" aria-label="Maximum year"></select>
</label>
</div>
</fieldset>
<fieldset class="filter-block">
<legend>Max price <span class="legend-val" id="priceVal">$80,000</span></legend>
<input id="priceRange" type="range" min="8000" max="80000" step="1000" value="80000" aria-label="Maximum price" />
</fieldset>
<fieldset class="filter-block">
<legend>Max mileage <span class="legend-val" id="mileVal">150,000 mi</span></legend>
<input id="mileRange" type="range" min="0" max="150000" step="5000" value="150000" aria-label="Maximum mileage" />
</fieldset>
</aside>
<section class="results" aria-label="Vehicle results">
<div class="results-bar">
<p class="result-count"><strong id="count">0</strong> vehicles</p>
<div class="sort">
<label for="sort">Sort</label>
<select id="sort" aria-label="Sort vehicles">
<option value="featured">Featured</option>
<option value="price-asc">Price: low to high</option>
<option value="price-desc">Price: high to low</option>
<option value="mile-asc">Mileage: low to high</option>
<option value="year-desc">Year: newest</option>
</select>
</div>
</div>
<div id="grid" class="grid" aria-live="polite"></div>
<div id="empty" class="empty" hidden>
<strong>No matches</strong>
<span>Try widening price, mileage, or clearing a make.</span>
<button class="btn btn-primary" type="button" id="emptyReset">Reset filters</button>
</div>
</section>
</main>
<div class="modal" id="quickView" hidden role="dialog" aria-modal="true" aria-labelledby="qvTitle">
<div class="modal-backdrop" data-close></div>
<div class="modal-card" id="qvCard"></div>
</div>
<div class="compare-tray" id="compareTray" hidden aria-label="Compare tray">
<div class="tray-inner" id="trayInner"></div>
<button class="btn btn-primary" id="trayView" type="button">View comparison</button>
</div>
<div class="modal" id="compareModal" hidden role="dialog" aria-modal="true" aria-labelledby="cmpTitle">
<div class="modal-backdrop" data-close></div>
<div class="modal-card modal-wide" id="cmpCard"></div>
</div>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Vehicle Inventory Grid
A pre-owned sales screen for a fictional dealership, styled in the same industrial, safety-orange language as the rest of the auto set. A sticky filter rail on the left carries make, body, and fuel chips, two range sliders for max price and max mileage, and a paired from/to model-year selector. The search field in the dark top bar scans across year, make, model, trim, body, fuel, VIN, and plate at once, so a partial code or a stock phrase narrows the grid instantly.
Each result is a card with a gradient vehicle photo, status badges (Hot deal, Certified, Reduced), the year/make/model in a clear hierarchy, mileage and fuel specs in tabular figures, and a price that strikes through MSRP when a unit is marked down. Every filter and the sort menu recompute the grid live, with a graceful empty state when nothing matches. Tap the heart to save a car, open Quick view for a full spec sheet in a modal, or tick the Compare box.
Up to three vehicles drop into a bottom compare tray; opening it builds a side-by-side table that highlights the lowest price, lowest mileage, and newest year with a check. Saving, comparing, and filtering all fire small toasts, the rail slides up as a sheet on mobile, and the whole layout reflows to a single column down to about 360px.
Illustrative UI only — fictional shop/dealership, not a real service system.