diff --git a/src/app/features/vehicles/components/vehicle-card/vehicle-card.component.html b/src/app/features/vehicles/components/vehicle-card/vehicle-card.component.html
new file mode 100644
index 0000000..276a819
--- /dev/null
+++ b/src/app/features/vehicles/components/vehicle-card/vehicle-card.component.html
@@ -0,0 +1,16 @@
+
+
+
![]()
+
+
+
+
{{ vehicle.name }}
+
From {{ vehicle.price }}
+
{{ vehicle.description }}
+
+
diff --git a/src/app/features/vehicles/components/vehicle-card/vehicle-card.component.scss b/src/app/features/vehicles/components/vehicle-card/vehicle-card.component.scss
new file mode 100644
index 0000000..a1b8164
--- /dev/null
+++ b/src/app/features/vehicles/components/vehicle-card/vehicle-card.component.scss
@@ -0,0 +1,101 @@
+// Animation runs on the host element so that the parent can control
+// animation-delay via [style.animation-delay] without ViewEncapsulation issues.
+:host {
+ display: block;
+ opacity: 0;
+ animation: vehicle-card-fade-in 0.5s ease forwards;
+}
+
+@keyframes vehicle-card-fade-in {
+ to {
+ opacity: 1;
+ }
+}
+
+.vehicle-card {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ padding: 16px;
+ border-bottom: 1px solid #e0e0e0;
+ background-color: #fff;
+
+ // ── Tablet and above: flip to vertical stacked layout ──
+ @media (min-width: 768px) {
+ flex-direction: column;
+ align-items: stretch;
+ padding: 0;
+ border-bottom: none;
+ }
+
+ // ─────────────────────────────────────────────
+ // Image wrapper
+ // The same
src is used at every breakpoint.
+ // Changing aspect-ratio + object-fit: cover gives the
+ // illusion of a different crop without swapping src.
+ // ─────────────────────────────────────────────
+ &__image-wrapper {
+ flex-shrink: 0;
+ width: 38%;
+ aspect-ratio: 1 / 1; // mobile: square crop
+ overflow: hidden;
+
+ @media (min-width: 768px) {
+ width: 100%;
+ aspect-ratio: 16 / 9; // tablet+: widescreen crop
+ }
+ }
+
+ &__image {
+ display: block;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+
+ // ─────────────────────────────────────────────
+ // Text body
+ // ─────────────────────────────────────────────
+ &__body {
+ flex: 1;
+ padding: 0 16px;
+
+ @media (min-width: 768px) {
+ padding: 20px 16px 28px;
+ text-align: center;
+ }
+ }
+
+ &__name {
+ font-family: Georgia, 'Times New Roman', serif;
+ font-size: 1rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ margin: 0 0 6px;
+
+ @media (min-width: 768px) {
+ font-size: 1.0625rem;
+ text-decoration: underline;
+ text-underline-offset: 5px;
+ margin-bottom: 14px;
+ }
+ }
+
+ &__price {
+ font-size: 0.875rem;
+ color: #333;
+ margin: 0 0 6px;
+
+ @media (min-width: 768px) {
+ margin-bottom: 10px;
+ }
+ }
+
+ &__description {
+ font-size: 0.875rem;
+ color: #555;
+ line-height: 1.45;
+ margin: 0;
+ }
+}
diff --git a/src/app/features/vehicles/components/vehicle-card/vehicle-card.component.ts b/src/app/features/vehicles/components/vehicle-card/vehicle-card.component.ts
new file mode 100644
index 0000000..380485a
--- /dev/null
+++ b/src/app/features/vehicles/components/vehicle-card/vehicle-card.component.ts
@@ -0,0 +1,36 @@
+import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
+import { AnyVehicle } from '../../../../core/interfaces/vehicle.interface';
+
+@Component({
+ selector: 'app-vehicle-card',
+ imports: [],
+ templateUrl: './vehicle-card.component.html',
+ styleUrl: './vehicle-card.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class VehicleCardComponent {
+ @Input({ required: true }) vehicle!: AnyVehicle;
+
+ /**
+ * Used by the parent list to stagger the fade-in animation via
+ * [style.animation-delay] bound on the host element.
+ */
+ @Input() index = 0;
+
+ /**
+ * Prefer the 16x9 image; CSS aspect-ratio + object-fit handles the
+ * visual crop at all breakpoints so we never need to swap src.
+ */
+ get imageUrl(): string {
+ return (
+ this.vehicle.media.find((m) => m.url.includes('16x9'))?.url ??
+ this.vehicle.media[0]?.url ??
+ ''
+ );
+ }
+
+ get emissionsLabel(): string {
+ const { template, value } = this.vehicle.meta.emissions;
+ return template.replace('$value', String(value));
+ }
+}
diff --git a/src/app/features/vehicles/components/vehicle-list/vehicle-list.component.html b/src/app/features/vehicles/components/vehicle-list/vehicle-list.component.html
new file mode 100644
index 0000000..216e61c
--- /dev/null
+++ b/src/app/features/vehicles/components/vehicle-list/vehicle-list.component.html
@@ -0,0 +1,43 @@
+
+
+ @if (vehicleService.loading()) {
+
+
+
+ @for (_ of skeletonCount; track $index) {
+ -
+
+
+ }
+
+
+ } @else if (vehicleService.error()) {
+
+
+
{{ vehicleService.error() }}
+
+
+ } @else {
+
+
+ @for (vehicle of vehicleService.vehicles(); track vehicle.id; let i = $index) {
+ -
+
+
+ }
+
+
+ }
+
+
diff --git a/src/app/features/vehicles/components/vehicle-list/vehicle-list.component.scss b/src/app/features/vehicles/components/vehicle-list/vehicle-list.component.scss
new file mode 100644
index 0000000..d14758d
--- /dev/null
+++ b/src/app/features/vehicles/components/vehicle-list/vehicle-list.component.scss
@@ -0,0 +1,134 @@
+// ─────────────────────────────────────────────────────────────────────────────
+// Shared shimmer animation used by skeleton elements
+// ─────────────────────────────────────────────────────────────────────────────
+@keyframes shimmer {
+ 0% {
+ background-position: 200% 0;
+ }
+ 100% {
+ background-position: -200% 0;
+ }
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Section wrapper
+// ─────────────────────────────────────────────────────────────────────────────
+.vehicle-list {
+ max-width: 1440px;
+ margin: 0 auto;
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Responsive grid
+//
+// Mobile (default): single-column stacked list — no grid needed.
+// Tablet (≥768px): 2-column grid. The 1px gap + background-color on the
+// container creates the dividing lines between cards without
+// needing borders on every card.
+// Desktop (≥1280px): 4-column grid.
+// ─────────────────────────────────────────────────────────────────────────────
+.vehicle-list__grid {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+
+ @media (min-width: 768px) {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 1px;
+ background-color: #d0d0d0; // shows through the 1px gap as grid lines
+ }
+
+ @media (min-width: 1280px) {
+ grid-template-columns: repeat(4, 1fr);
+ }
+}
+
+// Each
needs its own background to mask the grid container's background.
+.vehicle-list__item {
+ @media (min-width: 768px) {
+ background-color: #fff;
+ }
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Loading skeleton
+// Mirrors the card layout so there's no layout shift when data arrives.
+// ─────────────────────────────────────────────────────────────────────────────
+%shimmer {
+ background: linear-gradient(90deg, #efefef 25%, #e0e0e0 50%, #efefef 75%);
+ background-size: 200% 100%;
+ animation: shimmer 1.5s infinite;
+}
+
+.vehicle-list__skeleton {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ padding: 16px;
+ border-bottom: 1px solid #e0e0e0;
+ background-color: #fff;
+
+ @media (min-width: 768px) {
+ flex-direction: column;
+ padding: 0;
+ border-bottom: none;
+ }
+}
+
+.vehicle-list__skeleton-image {
+ @extend %shimmer;
+
+ flex-shrink: 0;
+ width: 38%;
+ aspect-ratio: 1 / 1;
+
+ @media (min-width: 768px) {
+ width: 100%;
+ aspect-ratio: 16 / 9;
+ }
+}
+
+.vehicle-list__skeleton-body {
+ flex: 1;
+ padding: 0 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+
+ @media (min-width: 768px) {
+ padding: 20px 16px 28px;
+ align-items: center;
+ }
+}
+
+.vehicle-list__skeleton-line {
+ @extend %shimmer;
+
+ height: 14px;
+ border-radius: 2px;
+ width: 90%;
+
+ &--title {
+ height: 18px;
+ width: 70%;
+ }
+
+ &--short {
+ width: 55%;
+ }
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Error state
+// ─────────────────────────────────────────────────────────────────────────────
+.vehicle-list__error {
+ padding: 48px 24px;
+ text-align: center;
+}
+
+.vehicle-list__error-message {
+ color: #c0392b;
+ font-size: 1rem;
+ margin: 0;
+}
diff --git a/src/app/features/vehicles/components/vehicle-list/vehicle-list.component.ts b/src/app/features/vehicles/components/vehicle-list/vehicle-list.component.ts
new file mode 100644
index 0000000..a366f60
--- /dev/null
+++ b/src/app/features/vehicles/components/vehicle-list/vehicle-list.component.ts
@@ -0,0 +1,17 @@
+import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
+import { VehicleService } from '../../../../core/services/vehicle.service';
+import { VehicleCardComponent } from '../vehicle-card/vehicle-card.component';
+
+@Component({
+ selector: 'app-vehicle-list',
+ imports: [VehicleCardComponent],
+ templateUrl: './vehicle-list.component.html',
+ styleUrl: './vehicle-list.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class VehicleListComponent {
+ protected readonly vehicleService = inject(VehicleService);
+
+ /** Drives the skeleton loading grid — one placeholder per expected vehicle. */
+ protected readonly skeletonCount = Array.from({ length: 6 });
+}