Fahrzeugbestand aus dem Börsen-Feed
Fahrzeugkarten aus dem Händler-Feed mit Gesamtpreis, Steuerart, Eckdaten und der Pflichtzeile zu Verbrauch, CO₂ und CO₂-Klasse in gleicher Größe; Stand mit Uhrzeit, Feed-Lücken und Störung sauber abgefangen.
Startseite eines Autohauses oder einer Werkstatt mit Verkauf: drei bis sechs ausgewählte Fahrzeuge aus dem Feed, darunter „Alle Fahrzeuge ansehen“.
Der Bestand eines Autohauses kommt aus der Fahrzeugbörse oder dem Händlersystem und erscheint als Karten: Foto in festem Seitenverhältnis, Modell und Ausstattungslinie, Gesamtpreis inkl. MwSt. mit Steuerart, Erstzulassung, Kilometer, kW (PS), Kraftstoff, Getriebe und Bestandsnummer. Sobald Motordaten auf der Karte stehen, folgt die Zeile mit Verbrauch, CO₂-Emissionen und CO₂-Klasse in gleicher Größe. Über der Liste steht, von wann der Stand ist und woher er kommt. Feed-Lücken werden abgefangen: Fehlt ein Pflichtwert, fallen die Motordaten weg und die Karte wird intern zum Prüfen markiert.
Wer ein Auto sucht, vergleicht Preis, Alter und Kilometer in Sekunden. Feste Bildformate und Tabellenziffern machen die Karten vergleichbar. Der Stand mit Uhrzeit schafft Vertrauen, dass das Auto noch da ist. Die Pflichtzeile als Teil der Karte wirkt sauber und nicht wie Kleingedrucktes. Und es gibt nie „k. A.“ oder „undefined“, die eine Seite sofort unseriös wirken lassen.
Wann einsetzen
Autohaus oder Werkstatt mit Verkauf, deren Bestand in einer Börse oder einem Händlersystem gepflegt wird. Als Teaser mit drei bis sechs Fahrzeugen auf der Startseite, als Liste mit Schnellsuche auf der Bestandsseite. Ohne gepflegten Feed als reiner Verweis auf die Händlerseite in der Börse.
Wann nicht
Wenn der Bestand von Hand auf der Website gepflegt werden müsste: Preise und Verfügbarkeit veralten, dann lieber die Variante portal. Ein einzelnes Fahrzeug mit Steckbrief ist objekt-steckbrief-expose (Variante fahrzeug). Ab etwa zehn Fahrzeugen und mehr als zwei Filtern übernimmt facettenfilter-ergebnisliste die Filter. Nutzfahrzeuge (Klasse N1) fallen nicht unter die Pkw-EnVKV; dort gelten andere Angaben.
Varianten
- Teaser: Reihe mit drei bis sechs Karten auf der Startseite, „Alle Fahrzeuge“ darunter; auf dem Telefon untereinander
- Liste: alle Fahrzeuge mit Schnellsuche (Preisgrenze, Elektro und Plug-in, Automatik, wenig Kilometer, neu und Tageszulassung), Sortierung und Trefferzahl; ab etwa zehn Fahrzeugen Übergabe an facettenfilter-ergebnisliste
- Portal: kein Feed gepflegt, nur Verweis auf die Händlerseite in der Börse mit Schnellsuchen und Verkaufsberater; keine Preise auf der eigenen Seite
Umsetzung
Feed serverseitig holen und zwischenspeichern (z. B. stündlich), nie beim Besuch live aus der Börse. Jede Karte ist ein <article> mit Bild im festen Format 3:2 (<img loading="lazy" sizes="auto" width height alt>; Kennzeichen auf echten Fotos unkenntlich), Titel als gestreckter Link auf die Fahrzeugseite und Eckdaten als <dl>. Zahlen mit Intl.NumberFormat('de-DE'), tabular-nums und geschütztem Leerzeichen vor der Einheit. Eine Funktion entscheidet je Fahrzeug: Pflicht = neu oder Tageszulassung; nötige Werte = Verbrauch, CO₂ und Klasse (Elektro: Stromverbrauch; Plug-in-Hybrid: gewichtet kombiniert und Kraftstoffverbrauch bei entladener Batterie, zwei Klassen). Fehlt bei Pflicht ein Wert, fallen Leistung und andere Motordaten weg und die Karte bekommt intern „prüfen“; fehlende Felder werden weggelassen. Die CO₂-Klasse aus § 3a berechnen und gegen den Feed prüfen. Über der Liste „Stand: Datum, Uhrzeit“ und die Quelle in einer aria-live-Region; ist der Feed nicht erreichbar, bleibt der letzte Stand mit Datum stehen und ein Hinweis nennt die Telefonnummer. Verkaufte Fahrzeuge verschwinden beim nächsten Abgleich, reservierte tragen ein Schild. Monatsrate nur zusammen mit dem repräsentativen Beispiel direkt darunter (Umschalter „Gesamtpreis / Monatsrate“); der Gesamtpreis bleibt immer stehen. JSON-LD Car nur auf der Fahrzeugseite und nur mit sichtbaren Werten (Google zeigt seit September 2025 keine Fahrzeug-Rich-Results mehr). In der Demo sind Fahrzeugseite, Suchauftrag und Börse simuliert (Hinweis statt Seitenwechsel). Texte, Steuerart und Pflichtangaben vom Kunden prüfen lassen.
Robust umsetzen
Telefon. Teaser und Liste einspaltig untereinander (eine Wischreihe würde bei 320 px und 200 % Zoom Text seitlich abschneiden), Schnellsuche als umbrechende Chips. Pflichtzeile bleibt in voller Größe unter den Eckdaten.
Ohne Bewegung. Rate und neue Treffer erscheinen ohne Übergang, verkaufte Fahrzeuge verschwinden sofort.
Barrierefrei. Trefferzahl und Stand in einer aria-live-Region. Schnellsuche als Knöpfe mit aria-pressed, Sortierung als beschriftetes select. Jede Karte hat einen Link mit dem vollen Modellnamen; Bilder mit Alt-Text. Die CO₂-Klasse steht als Buchstabe im Text, die Farbe ist nur Beiwerk. Reserviert steht als Text, nicht nur als Farbe. Pflichtzeile in voller Textfarbe und gleicher Größe wie die Eckdaten.
Code
<!DOCTYPE html>
<!--
Fahrzeugbestand aus dem Börsen-Feed
Fundus · Sektionen & Funktionen · https://cecodesign.de/lab/fundus/sektionen/fahrzeugbestand-feed-teaser/
Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fahrzeugbestand aus dem Börsen-Feed · Fundus-Demo</title>
<script>document.documentElement.classList.add('js');if(matchMedia('(prefers-reduced-motion: reduce)').matches)document.documentElement.classList.add('reduziert')</script>
<style>
@font-face { font-family: 'Mona Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/mona-sans:vf@latest/latin-wdth-normal.woff2') format('woff2-variations'); font-weight: 200 900; font-stretch: 75% 125%; font-display: swap; }
@font-face { font-family: 'Google Sans Flex'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/google-sans-flex:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 1000; font-display: swap; }
@font-face { font-family: 'Geist Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist-mono:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
:root {
--grund: #f2f1ed; --karte: #ffffff; --text: #121314; --leise: #595e63; --linie: rgba(18, 19, 20, .14);
--asphalt: #232629; --asphalt-leise: #b9bec4; --gelb: #f5b700; --gruen: #1f7a3e; --rot: #b3261e;
--auf: cubic-bezier(0.16, 1, 0.3, 1); /* öffnen, expo.out */
--zu: cubic-bezier(0.7, 0, 0.84, 0); /* schließen, expo.in */
--pfeil: cubic-bezier(0.5, 1, 0.89, 1); /* Pfeil-Morph aus icon-squash-stretch */
--t-ui: 120ms; --t-auf: 250ms; --t-zu: 170ms;
--rand: clamp(16px, 4vw, 48px);
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--grund); color: var(--text); font-family: 'Google Sans Flex', system-ui, sans-serif; -webkit-text-size-adjust: 100%; }
body { min-height: 100svh; font-size: 1.0625rem; line-height: 1.55; }
:root:has(:focus-visible) { scroll-behavior: auto; }
:focus-visible { outline: 3px solid var(--text); outline-offset: 3px; }
button, select { font: inherit; color: inherit; }
button { cursor: pointer; }
a { color: inherit; }
[hidden] { display: none !important; }
.nur-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.label { font: 600 .8125rem/1.2 'Google Sans Flex', system-ui, sans-serif; letter-spacing: .06em; text-transform: uppercase; color: var(--leise); }
.zahl { font-variant-numeric: tabular-nums; }
sub { font-size: .72em; vertical-align: -.18em; line-height: 0; }
/* ── Kopf der Seite (Kontext) ─────────────────────────────── */
.kopf { background: var(--asphalt); color: #fff; }
.kopf-innen { max-width: 1320px; margin: 0 auto; padding: 12px var(--rand); display: flex; align-items: center; gap: 12px 28px; flex-wrap: wrap; }
.logo { display: grid; text-decoration: none; line-height: 1; gap: 4px; margin-right: auto; }
.logo b { font: 800 1.5rem/1 'Mona Sans', sans-serif; font-stretch: 75%; letter-spacing: .03em; }
.logo span { font-size: .8125rem; color: var(--asphalt-leise); letter-spacing: .02em; }
.kopf nav ul { display: flex; gap: 22px; list-style: none; padding: 0; }
.kopf nav a { text-decoration: none; font-weight: 500; font-size: .9375rem; padding: 10px 0; display: inline-block; border-bottom: 2px solid transparent; transition: border-color var(--t-ui) linear; }
.kopf nav a:hover { border-color: var(--asphalt-leise); }
.kopf nav a[aria-current] { border-color: #fff; }
.kopf :focus-visible { outline-color: #fff; }
.tel { display: inline-flex; align-items: center; gap: 8px; min-height: 44px; font: 700 1.125rem/1 'Google Sans Flex', sans-serif; font-variant-numeric: tabular-nums; text-decoration: none; }
.tel svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2; }
/* Tiefen-Taste (tiefen-taste-3d, Variante stapel) nur für „Termin buchen“, auf Asphalt ohne Rand */
.taste { position: relative; border: 0; padding: 0; background: none; border-radius: 10px; }
.taste .schatten { position: absolute; inset: 0; border-radius: 10px; background: rgba(0, 0, 0, .45); filter: blur(2px); transform: translateY(2px); transition: transform 600ms cubic-bezier(0.3, 0.7, 0.4, 1); }
.taste .kante { position: absolute; inset: 0; border-radius: 10px; background: linear-gradient(to left, #5c4500 0%, #8a6800 8%, #8a6800 92%, #5c4500 100%); }
.taste .front { position: relative; display: flex; align-items: center; min-height: 44px; padding: 0 18px; border-radius: 10px; background: var(--gelb); color: var(--text); font-weight: 700; font-size: .9375rem; transform: translateY(-4px); transition: transform 600ms cubic-bezier(0.3, 0.7, 0.4, 1); }
.taste:hover .front { transform: translateY(-6px); transition: transform 250ms cubic-bezier(0.3, 0.7, 0.4, 1.5); }
.taste:hover .schatten { transform: translateY(4px); transition: transform 250ms cubic-bezier(0.3, 0.7, 0.4, 1.5); }
.taste:active .front { transform: translateY(-2px); transition-duration: 34ms; }
.taste:active .schatten { transform: translateY(1px); transition-duration: 34ms; }
.taste:focus-visible { outline: 3px solid #fff; outline-offset: 4px; }
/* ── Sektion Fahrzeugbestand ─────────────────────────────── */
.bestand { max-width: 1320px; margin: 0 auto; padding: clamp(28px, 5vw, 64px) var(--rand) clamp(40px, 6vw, 80px); }
.kopfzeile { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: end; gap: 20px 40px; }
.kopfzeile h2 { font: 800 clamp(2.25rem, 1.3rem + 3.6vw, 4.25rem)/.98 'Mona Sans', sans-serif; font-stretch: 80%; letter-spacing: -.02em; margin-top: 10px; text-wrap: balance; }
.lead { max-width: 58ch; margin-top: 14px; text-wrap: pretty; }
.stand { border: 2px solid var(--asphalt); border-radius: 8px; padding: 12px 16px; display: grid; gap: 2px; min-width: 250px; background: var(--karte); }
.stand .zeile1 { display: flex; align-items: center; gap: 8px; font-size: .8125rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--gruen); }
.stand .zeile1::before { content: ''; width: 9px; height: 9px; border-radius: 50%; background: currentColor; }
.stand b { font-size: 1.125rem; font-variant-numeric: tabular-nums; }
.stand small { font-size: .875rem; color: var(--leise); }
.stand.gestoert { border-color: var(--rot); }
.stand.gestoert .zeile1 { color: var(--rot); }
.stand.blitz { animation: blitz 900ms var(--auf); }
@keyframes blitz { from { background: #fff6d6; } to { background: var(--karte); } }
.werkzeug { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 24px; margin-top: clamp(24px, 3.5vw, 40px); padding-bottom: 16px; border-bottom: 2px solid var(--asphalt); }
.schnell { display: flex; flex-wrap: wrap; gap: 8px; margin-right: auto; }
.chip { min-height: 44px; padding: 0 16px; border: 2px solid #8a8f94; border-radius: 999px; background: transparent; font-weight: 600; font-size: .9375rem; display: inline-flex; align-items: center; gap: 8px; transition: background-color var(--t-ui) linear, border-color var(--t-ui) linear, color var(--t-ui) linear; }
.chip:hover { border-color: var(--asphalt); }
.chip svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; display: none; }
.chip[aria-pressed="true"] { background: var(--asphalt); border-color: var(--asphalt); color: #fff; }
.chip[aria-pressed="true"] svg { display: block; }
.chip .n { font-variant-numeric: tabular-nums; font-weight: 500; opacity: .85; }
.rechts { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 16px; }
.segment { display: inline-flex; border: 2px solid var(--asphalt); border-radius: 8px; padding: 2px; gap: 2px; }
.segment button { min-height: 40px; padding: 0 14px; border: 0; border-radius: 5px; background: transparent; font-weight: 600; font-size: .9375rem; transition: background-color var(--t-ui) linear, color var(--t-ui) linear; }
.segment button[aria-pressed="true"] { background: var(--asphalt); color: #fff; }
.segment button:focus-visible { outline-offset: 1px; }
.sortierung { display: inline-flex; align-items: center; gap: 10px; font-size: .9375rem; }
.sortierung select { min-height: 44px; padding: 0 40px 0 14px; border: 2px solid var(--asphalt); border-radius: 8px; background: var(--karte) 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='%23121314' stroke-width='2.5' stroke-linecap='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E") no-repeat right 14px center; appearance: none; font-weight: 600; }
.treffer-zeile { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: baseline; gap: 8px 20px; margin: 16px 0 18px; }
.treffer { font-weight: 700; font-variant-numeric: tabular-nums; }
.treffer-zeile small { font-size: .875rem; color: var(--leise); }
/* Hinweis: Feed nicht erreichbar */
.feed-hinweis { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 4px 14px; align-items: start; background: var(--karte); border: 2px solid var(--rot); border-left-width: 6px; border-radius: 8px; padding: 14px 18px; margin-bottom: 18px; }
.feed-hinweis svg { width: 22px; height: 22px; fill: none; stroke: var(--rot); stroke-width: 2; grid-row: span 2; margin-top: 2px; }
.feed-hinweis b { font-size: 1rem; }
.feed-hinweis p { font-size: .9375rem; }
.feed-hinweis a { font-weight: 700; font-variant-numeric: tabular-nums; }
/* Pflege-Ansicht (nur intern) */
.pflegeliste { border: 2px dashed var(--rot); border-radius: 8px; padding: 12px 16px; margin-bottom: 18px; background: #fbeceb; color: #6f1712; font-size: .9375rem; }
.pflegeliste b { display: block; margin-bottom: 4px; }
.pflegeliste ul { padding-left: 1.1em; }
/* Karten */
.karten { list-style: none; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(min(270px, 100%), 1fr)); gap: clamp(14px, 1.8vw, 22px); }
.teaser .karten { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.karten > li { display: grid; min-width: 0; }
.karten > li.kommt { animation: kommt var(--t-auf) var(--auf) both; }
@keyframes kommt { from { opacity: 0; transform: translateY(8px); } }
.karten > li.geht { animation: geht var(--t-zu) var(--zu) both; }
@keyframes geht { to { opacity: 0; transform: scale(.98); } }
.karte { position: relative; display: flex; flex-direction: column; background: var(--karte); border: 1px solid var(--linie); border-radius: 10px; overflow: hidden; transition: border-color var(--t-ui) linear, box-shadow var(--t-ui) linear; }
.karte:hover { border-color: var(--asphalt); box-shadow: 0 1px 0 var(--asphalt); }
.karte:has(.titel:focus-visible) { outline: 3px solid var(--text); outline-offset: 3px; }
.bild { position: relative; aspect-ratio: 3 / 2; background: #dcdbd6; }
.bild img { display: block; width: 100%; height: 100%; object-fit: cover; }
.schilder { position: absolute; left: 10px; top: 10px; right: 10px; display: flex; flex-wrap: wrap; gap: 6px; }
.schild { font-size: .75rem; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; padding: 5px 9px; border-radius: 5px; background: rgba(255, 255, 255, .94); color: var(--text); }
.schild.res { background: var(--asphalt); color: #fff; }
.koerper { display: flex; flex-direction: column; gap: 12px; padding: 16px 18px 18px; flex: 1; }
.karte h3 { font-size: 1rem; line-height: 1.3; font-weight: 400; }
.titel { text-decoration: none; }
.titel::after { content: ''; position: absolute; inset: 0; z-index: 1; }
.titel:focus-visible { outline: none; }
.modell { font: 750 1.375rem/1.1 'Mona Sans', sans-serif; font-stretch: 85%; letter-spacing: -.005em; }
.linie { font-size: .9375rem; font-weight: 400; color: var(--leise); }
.karte:hover .modell { text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 3px; }
.preis { display: grid; gap: 4px; }
.preis b { font: 800 2.125rem/1 'Mona Sans', sans-serif; font-stretch: 75%; font-variant-numeric: tabular-nums; letter-spacing: -.01em; }
.preis span { font-size: .875rem; color: var(--leise); }
.preis .steuer { justify-self: start; font-size: .75rem; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--text); border: 1.5px solid #8a8f94; border-radius: 4px; padding: 3px 7px; }
.daten { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(6.75rem, 100%), 1fr)); gap: 10px 12px; padding-top: 12px; border-top: 1px solid var(--linie); }
.daten div { min-width: 0; }
.daten dt { font-size: .75rem; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--leise); }
.daten dd { font-size: .9375rem; font-weight: 500; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
/* Pflichtzeile nach Pkw-EnVKV: gleiche Größe und Farbe wie die Eckdaten */
.pflicht { margin-top: auto; padding-top: 12px; border-top: 2px solid var(--asphalt); font-size: .9375rem; font-weight: 500; line-height: 1.45; }
.pflicht > span { display: block; }
.pflicht b { font-weight: 700; font-variant-numeric: tabular-nums; }
.kl { display: inline-grid; place-items: center; min-width: 1.6em; height: 1.45em; padding: 0 .3em; border-radius: 4px; margin-left: 2px; font-weight: 800; background: var(--k); color: var(--kt); }
.ohne { margin-top: auto; }
.pruef-notiz { display: none; font-size: .875rem; background: #fbeceb; color: #6f1712; border-radius: 6px; padding: 8px 10px; position: relative; z-index: 2; }
.pflege .karte.pruefen { outline: 2px dashed var(--rot); outline-offset: -2px; }
.pflege .pruef-notiz { display: block; }
.pflege .ohne-hinweis { display: block; }
.ohne-hinweis { display: none; font-size: .875rem; color: var(--leise); }
/* Monatsrate mit repräsentativem Beispiel */
.rate-huelle { display: grid; grid-template-rows: 0fr; visibility: hidden; transition: grid-template-rows var(--t-zu) var(--zu), visibility 0s var(--t-zu); }
.rate-huelle > div { overflow: hidden; min-height: 0; }
.zeige-rate .rate-huelle { grid-template-rows: 1fr; visibility: visible; transition: grid-template-rows var(--t-auf) var(--auf), visibility 0s; }
.rate { background: var(--grund); border-radius: 8px; padding: 12px 14px; display: grid; gap: 6px; }
.rate-zahl { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 8px; }
.rate-zahl b { font: 800 1.625rem/1 'Mona Sans', sans-serif; font-stretch: 75%; font-variant-numeric: tabular-nums; }
.rate-zahl span { font-size: .9375rem; font-weight: 600; }
.beispiel { font-size: .875rem; line-height: 1.45; font-variant-numeric: tabular-nums; }
.beispiel strong { font-weight: 700; }
.platzhalter { background: #fff1c2; box-shadow: 0 0 0 1px #c9a227 inset; border-radius: 3px; padding: 0 3px; }
.keine-rate { font-size: .875rem; color: var(--leise); }
.warnung { font-size: .9375rem; font-weight: 700; }
/* Leerzustand */
.leer { border: 2px dashed #8a8f94; border-radius: 10px; padding: clamp(20px, 4vw, 40px); display: grid; gap: 12px; justify-items: start; background: var(--karte); }
.leer h3 { font: 750 1.75rem/1.1 'Mona Sans', sans-serif; font-stretch: 85%; }
.leer .knoepfe { display: flex; flex-wrap: wrap; gap: 10px; }
/* Flache Knöpfe mit Pfeil (icon-squash-stretch, Pfad-Morph) */
.knopf { display: inline-flex; align-items: center; gap: 10px; min-height: 48px; padding: 0 20px; border: 2px solid var(--asphalt); border-radius: 8px; background: transparent; color: var(--text); font-weight: 700; font-size: 1rem; text-decoration: none; transition: background-color var(--t-ui) linear, color var(--t-ui) linear; }
.knopf:hover { background: var(--asphalt); color: #fff; }
.knopf.voll { background: var(--asphalt); color: #fff; }
.knopf.voll:hover { background: #000; }
.knopf:active { transform: translateY(1px); }
.pfeil { width: 1.25em; height: 1.25em; flex: none; overflow: visible; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.pfeil .schaft { d: path('M 5,12 h 14'); transition: d 300ms var(--pfeil); }
.pfeil .spitze { d: path('M 12,5 l 7,7 l -7,7'); transition: d 300ms var(--pfeil); }
.knopf:hover .schaft, .knopf:focus-visible .schaft, .knopf.zeigt .schaft { d: path('M 5,12 h 17'); }
.knopf:hover .spitze, .knopf:focus-visible .spitze, .knopf.zeigt .spitze { d: path('M 15,7 l 7,5 l -7,5'); }
.knopf.zeigt { background: var(--asphalt); color: #fff; }
.fuss { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 24px; margin-top: 24px; }
.fuss p { font-size: .9375rem; color: var(--leise); }
/* Portal-Variante: kein Feed, nur Verweis */
.portal { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); gap: clamp(20px, 3vw, 36px); margin-top: clamp(24px, 3.5vw, 40px); }
.portal-haupt { background: var(--karte); border: 1px solid var(--linie); border-radius: 10px; overflow: hidden; display: grid; }
.portal-haupt img { display: block; width: 100%; height: auto; aspect-ratio: 1300 / 460; object-fit: cover; }
.portal-text { padding: clamp(18px, 2.4vw, 28px); display: grid; gap: 14px; justify-items: start; }
.portal-text h3 { font: 750 clamp(1.5rem, 1.1rem + 1.4vw, 2.125rem)/1.08 'Mona Sans', sans-serif; font-stretch: 85%; text-wrap: balance; }
.portal-text p { max-width: 56ch; }
.portal-text small { font-size: .875rem; color: var(--leise); }
.schnelllinks { list-style: none; padding: 0; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; width: 100%; }
.schnelllinks a { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-height: 48px; padding: 0 14px; border: 2px solid #8a8f94; border-radius: 8px; text-decoration: none; font-weight: 600; transition: border-color var(--t-ui) linear; }
.schnelllinks a:hover, .schnelllinks a.zeigt { border-color: var(--asphalt); }
.schnelllinks .pfeil { width: 1.1em; height: 1.1em; }
.schnelllinks a:hover .schaft, .schnelllinks a.zeigt .schaft, .schnelllinks a:focus-visible .schaft { d: path('M 5,12 h 17'); }
.schnelllinks a:hover .spitze, .schnelllinks a.zeigt .spitze, .schnelllinks a:focus-visible .spitze { d: path('M 15,7 l 7,5 l -7,5'); }
.berater { background: var(--asphalt); color: #fff; border-radius: 10px; padding: clamp(18px, 2.4vw, 28px); display: grid; gap: 14px; align-content: start; align-self: start; }
.berater .label { color: var(--asphalt-leise); }
.person { display: flex; gap: 14px; align-items: center; }
.person .bildchen { width: 64px; height: 64px; border-radius: 50%; flex: none; background: #3a3f44; display: grid; place-items: center; font: 800 1.375rem/1 'Mona Sans', sans-serif; font-stretch: 80%; color: #fff; border: 2px solid #5b6168; }
.person b { display: block; font-size: 1.125rem; }
.person span { font-size: .9375rem; color: var(--asphalt-leise); }
.berater .tel { font-size: 1.375rem; }
.berater p { font-size: .9375rem; color: #dfe2e5; }
.berater :focus-visible { outline-color: #fff; }
.berater .knopf { border-color: #fff; color: #fff; }
.berater .knopf:hover, .berater .knopf.zeigt { background: #fff; color: var(--text); }
.berater .platzhalter { color: var(--text); }
.recht { margin-top: 28px; font-size: .875rem; color: var(--leise); max-width: 90ch; }
.recht .platzhalter { color: var(--text); }
/* Toast für simulierte Seitenwechsel */
.toast { position: fixed; left: 50%; bottom: 20px; z-index: 30; translate: -50% 0; max-width: min(560px, calc(100vw - 32px)); background: var(--asphalt); color: #fff; border-radius: 8px; padding: 12px 16px; font-size: .9375rem; opacity: 0; transform: translateY(8px); pointer-events: none; transition: opacity var(--t-zu) linear, transform var(--t-zu) var(--zu); }
.toast.an { opacity: 1; transform: none; transition: opacity 200ms linear, transform var(--t-auf) var(--auf); }
/* Gedachter Zeiger für die Vorführung */
.zeiger { position: fixed; left: 0; top: 0; z-index: 40; width: 22px; height: 22px; margin: -11px 0 0 -11px; border-radius: 50%; border: 2px solid var(--text); background: rgba(245, 183, 0, .55); pointer-events: none; opacity: 0; transition: transform 700ms var(--auf), opacity 200ms linear; }
.vorfuehrung .zeiger { opacity: 1; }
.zeiger.drueckt { scale: .8; }
/* Pult der Demo */
.pult { position: fixed; right: 16px; bottom: 16px; z-index: 20; display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; padding: 8px 10px 8px 14px; max-width: calc(100vw - 32px); background: rgba(18, 19, 20, .94); color: #eef0f2; border-radius: 6px; box-shadow: 0 10px 30px -12px rgba(0, 0, 0, .5); }
.pult, .pult button { font: 500 10.5px/1 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; }
.pult .gruppe { display: flex; flex-wrap: wrap; gap: 2px; }
.pult button { color: rgba(238, 240, 242, .78); background: transparent; border: 0; padding: 8px 9px; border-radius: 3px; transition: background-color var(--t-ui) linear, color var(--t-ui) linear; }
.pult button[aria-pressed="true"] { background: #eef0f2; color: var(--text); }
.pult button:focus-visible { outline: 2px solid #eef0f2; outline-offset: 2px; }
.pult .werte { text-transform: none; letter-spacing: .02em; color: rgba(238, 240, 242, .72); }
/* Varianten */
[data-variante="portal"] .werkzeug, [data-variante="portal"] .treffer-zeile, [data-variante="portal"] .karten, [data-variante="portal"] .fuss, [data-variante="portal"] .feed-hinweis, [data-variante="portal"] .pflegeliste, [data-variante="portal"] .stand, [data-variante="portal"] .leer { display: none !important; }
[data-variante="teaser"] .schnell, [data-variante="teaser"] .sortierung { display: none; }
[data-variante="liste"] .fuss { display: none; }
[data-variante="teaser"] .portal, [data-variante="liste"] .portal { display: none; }
@media (max-width: 1000px) {
.portal { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 860px) {
.teaser .karten { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 760px) {
.kopfzeile { grid-template-columns: minmax(0, 1fr); }
.stand { min-width: 0; justify-self: start; }
.kopf nav { display: none; }
.werkzeug { gap: 12px; }
.rechts { width: 100%; justify-content: space-between; }
}
@media (max-width: 640px) {
/* Teaser: untereinander (keine Wischreihe: bei 320 px und 200 % Zoom bliebe Text seitlich außerhalb) */
.teaser .karten { grid-template-columns: minmax(0, 1fr); }
.schnelllinks { grid-template-columns: minmax(0, 1fr); }
.tel .nummer { display: none; }
.tel { width: 44px; justify-content: center; border: 2px solid #5b6168; border-radius: 10px; }
.pult { bottom: 12px; right: 12px; }
}
@media (max-width: 440px) {
.logo span { display: none; }
}
@media (max-width: 360px) {
.segment button { padding: 0 10px; }
.koerper { padding: 14px; }
.preis b { font-size: 1.875rem; }
.sortierung { flex-wrap: wrap; width: 100%; gap: 6px; }
.sortierung select { width: 100%; min-width: 0; }
}
/* Vorschau 960 × 720: nur die Bühne */
.vorschau body { overflow: hidden; }
.vorschau .kopf, .vorschau .lead, .vorschau .recht, .vorschau .pult, .vorschau fundus-pult-knopf { display: none !important; }
.vorschau .bestand { padding-top: 22px; }
.vorschau .kopfzeile h2 { font-size: 2.5rem; margin-top: 4px; }
.vorschau .werkzeug { margin-top: 14px; padding-bottom: 10px; }
.vorschau .treffer-zeile { margin: 10px 0 12px; }
.vorschau .karten { gap: 14px; }
.vorschau .bild { aspect-ratio: 2 / 1; }
:root.reduziert *, :root.reduziert *::before, :root.reduziert *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; }
:root.reduziert .zeiger { transition: opacity 200ms linear !important; }
@media (hover: none) {
.karte:hover { border-color: var(--linie); box-shadow: none; }
.karte:hover .modell { text-decoration: none; }
.karte:active { border-color: var(--asphalt); }
.knopf:hover { background: transparent; color: var(--text); }
.knopf.voll:hover { background: var(--asphalt); color: #fff; }
.knopf:active { background: var(--asphalt); color: #fff; }
}
@media (forced-colors: active) {
button, [role=button], [role=tab], [role=switch], input, select, textarea { border: 1px solid ButtonText !important; }
.kl { border: 1px solid CanvasText; }
.chip[aria-pressed="true"], .segment button[aria-pressed="true"] { outline: 2px solid Highlight; }
}
</style>
</head>
<body>
<header class="kopf">
<div class="kopf-innen">
<a class="logo" href="#bestand" aria-label="Autohaus Lüttmann, Startseite"><b>LÜTTMANN</b><span>Autohaus · Werkstatt · Vechta</span></a>
<nav aria-label="Hauptmenü">
<ul>
<li><a href="#bestand" aria-current="page">Fahrzeuge</a></li>
<li><a href="#bestand" data-demo-link="Werkstatt">Werkstatt</a></li>
<li><a href="#bestand" data-demo-link="Festpreise">Festpreise</a></li>
<li><a href="#bestand" data-demo-link="Kontakt">Kontakt</a></li>
</ul>
</nav>
<a class="tel" href="tel:+4944410987650"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 4h4l2 5-2.5 1.5a11 11 0 0 0 5 5L15 13l5 2v4a2 2 0 0 1-2 2A16 16 0 0 1 3 6a2 2 0 0 1 2-2"/></svg><span class="nummer">04441 098765-0</span><span class="nur-sr">Anrufen: 04441 098765-0</span></a>
<button class="taste" type="button" data-demo-link="Termin buchen"><span class="schatten" aria-hidden="true"></span><span class="kante" aria-hidden="true"></span><span class="front">Termin buchen</span></button>
</div>
</header>
<main id="bestand">
<section class="bestand teaser" data-variante="teaser" aria-labelledby="h-bestand">
<div class="kopfzeile">
<div>
<p class="label" id="kicker">Fahrzeugbestand</p>
<h2 id="h-bestand">Auf dem Hof in Vechta</h2>
<p class="lead" id="lead">Neu- und Gebrauchtwagen, Elektro und Plug-in-Hybrid. Jedes Fahrzeug steht bei uns auf dem Hof an der Oldenburger Straße und ist probefahrbereit.</p>
</div>
<div class="stand" id="stand">
<span class="zeile1" id="stand-status">Aktuell</span>
<b id="stand-zeit">Stand: 02.11.2026, 07:40 Uhr</b>
<small id="stand-quelle">aus unserem Händlersystem, stündlicher Abgleich</small>
</div>
</div>
<div class="werkzeug">
<div class="schnell" role="group" aria-label="Schnellsuche">
<button class="chip" type="button" data-filter="preis" aria-pressed="false"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12l5 5 9-10"/></svg>Bis 20.000 €</button>
<button class="chip" type="button" data-filter="strom" aria-pressed="false"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12l5 5 9-10"/></svg>Elektro und Plug-in</button>
<button class="chip" type="button" data-filter="automatik" aria-pressed="false"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12l5 5 9-10"/></svg>Automatik</button>
<button class="chip" type="button" data-filter="km" aria-pressed="false"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12l5 5 9-10"/></svg>Unter 30.000 km</button>
<button class="chip" type="button" data-filter="neu" aria-pressed="false"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12l5 5 9-10"/></svg>Neu und Tageszulassung</button>
</div>
<div class="rechts">
<div class="segment" role="group" aria-label="Preis anzeigen als">
<button type="button" data-anzeige="preis" aria-pressed="true">Gesamtpreis</button>
<button type="button" data-anzeige="rate" aria-pressed="false">Monatsrate</button>
</div>
<label class="sortierung">Sortieren
<select id="sortierung">
<option value="neu">Neu eingestellt</option>
<option value="preis-auf">Preis aufsteigend</option>
<option value="preis-ab">Preis absteigend</option>
<option value="km">Kilometer aufsteigend</option>
<option value="ez">Erstzulassung, jüngste zuerst</option>
</select>
</label>
</div>
</div>
<div class="treffer-zeile">
<p class="treffer" id="treffer" aria-live="polite">12 Fahrzeuge</p>
<small>Alle Preise sind Endpreise. Verbrauchswerte nach WLTP.</small>
</div>
<div class="feed-hinweis" id="feed-hinweis" role="status" hidden>
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3l10 18H2z"/><path d="M12 10v5M12 18v.5"/></svg>
<b id="feed-titel">Der Abgleich mit unserem Händlersystem klappt gerade nicht.</b>
<p>Sie sehen den letzten Stand vom 02.11.2026, 07:40 Uhr. Ob ein Fahrzeug noch da ist, sagen wir Ihnen gern am Telefon: <a href="tel:+49444109876521">04441 098765-21</a>.</p>
</div>
<div class="pflegeliste" id="pflegeliste" hidden>
<b>Pflege-Ansicht, nur intern sichtbar</b>
<ul id="pflegepunkte"></ul>
</div>
<ul class="karten" id="karten" aria-labelledby="h-bestand"></ul>
<div class="leer" id="leer" hidden>
<p class="label">Keine Treffer</p>
<h3>Gerade passt kein Fahrzeug.</h3>
<p id="leer-text">Zu Ihrer Auswahl steht gerade nichts auf dem Hof.</p>
<div class="knoepfe">
<button class="knopf voll" type="button" id="zuruecksetzen">Auswahl zurücksetzen</button>
<a class="knopf" href="#bestand" data-demo-link="Suchauftrag">Suchauftrag anlegen <svg class="pfeil" viewBox="0 0 24 24" aria-hidden="true"><path class="schaft"/><path class="spitze"/></svg></a>
</div>
</div>
<div class="fuss">
<button class="knopf voll" type="button" id="alle"><span>Alle <span class="zahl" id="alle-n">12</span> Fahrzeuge ansehen</span> <svg class="pfeil" viewBox="0 0 24 24" aria-hidden="true"><path class="schaft"/><path class="spitze"/></svg></button>
<p>Mit Schnellsuche nach Preis, Antrieb, Getriebe und Kilometern.</p>
</div>
<div class="portal" id="portal">
<div class="portal-haupt">
<img id="portal-bild" alt="Zeichnung: drei Fahrzeuge auf dem Hof des Autohauses" width="1300" height="460">
<div class="portal-text">
<h3>Unseren ganzen Bestand finden Sie tagesaktuell in der Fahrzeugbörse.</h3>
<p>Dort pflegen wir Preise, Bilder und alle Angaben zu Verbrauch und CO<sub>2</sub>. So steht hier nie ein Auto, das schon verkauft ist.</p>
<a class="knopf voll" href="#bestand" data-demo-link="Händlerseite in der Fahrzeugbörse (externe Seite)">Zur Händlerseite in der Börse <svg class="pfeil" viewBox="0 0 24 24" aria-hidden="true"><path class="schaft"/><path class="spitze"/></svg></a>
<small>Externe Seite. Name und Link des Portals: <span class="platzhalter">vom Kunden eintragen</span></small>
<ul class="schnelllinks" aria-label="Schnellsuche in der Börse">
<li><a href="#bestand" data-demo-link="Börse: Elektro und Plug-in">Elektro und Plug-in <svg class="pfeil" viewBox="0 0 24 24" aria-hidden="true"><path class="schaft"/><path class="spitze"/></svg></a></li>
<li><a href="#bestand" data-demo-link="Börse: bis 20.000 €">Bis 20.000 € <svg class="pfeil" viewBox="0 0 24 24" aria-hidden="true"><path class="schaft"/><path class="spitze"/></svg></a></li>
<li><a href="#bestand" data-demo-link="Börse: Automatik">Automatik <svg class="pfeil" viewBox="0 0 24 24" aria-hidden="true"><path class="schaft"/><path class="spitze"/></svg></a></li>
<li><a href="#bestand" data-demo-link="Börse: Neu und Tageszulassung">Neu und Tageszulassung <svg class="pfeil" viewBox="0 0 24 24" aria-hidden="true"><path class="schaft"/><path class="spitze"/></svg></a></li>
</ul>
</div>
</div>
<aside class="berater" aria-labelledby="h-berater">
<p class="label" id="h-berater">Ihr Ansprechpartner im Verkauf</p>
<div class="person">
<span class="bildchen" aria-hidden="true">HS</span>
<div><b>Hendrik Steenken</b><span>Neu- und Gebrauchtwagen, Inzahlungnahme</span></div>
</div>
<a class="tel" href="tel:+49444109876521"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 4h4l2 5-2.5 1.5a11 11 0 0 0 5 5L15 13l5 2v4a2 2 0 0 1-2 2A16 16 0 0 1 3 6a2 2 0 0 1 2-2"/></svg>04441 098765-21</a>
<p>Mo bis Fr 9 bis 18 Uhr, Sa 9 bis 13 Uhr. Fahrzeug gefunden? Sagen Sie uns die Bestandsnummer, dann steht es zur Probefahrt bereit.</p>
<a class="knopf" href="#bestand" data-demo-link="Probefahrt anfragen">Probefahrt anfragen <svg class="pfeil" viewBox="0 0 24 24" aria-hidden="true"><path class="schaft"/><path class="spitze"/></svg></a>
<p>Foto der Person: <span class="platzhalter">echtes Foto mit Einwilligung</span></p>
</aside>
</div>
<p class="recht" id="recht"><span class="platzhalter">Vom Kunden prüfen lassen:</span> Pflichtangaben nach Pkw-EnVKV, Steuerart, Preisangaben und Finanzierungsbeispiel. Fahrzeuge, Marken, Werte und Bank sind für diese Demo erfunden, die Bilder sind Zeichnungen.</p>
</section>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<div class="zeiger" id="zeiger" aria-hidden="true"></div>
<div class="pult">
<span>Variante</span>
<div class="gruppe" role="group" aria-label="Variante der Demo">
<button type="button" data-v="teaser" aria-pressed="true">Teaser</button>
<button type="button" data-v="liste" aria-pressed="false">Liste</button>
<button type="button" data-v="portal" aria-pressed="false">Portal</button>
</div>
<span>Zustand</span>
<div class="gruppe" role="group" aria-label="Zustand des Feeds">
<button type="button" id="p-abgleich">Abgleich</button>
<button type="button" id="p-stoerung" aria-pressed="false">Feed gestört</button>
<button type="button" id="p-pflege" aria-pressed="false">Pflege-Ansicht</button>
</div>
<span class="werte" id="werte">öffnen 250 ms expo.out · schließen 170 ms expo.in · Hover 120 ms</span>
</div>
<script>
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const $ = (s, r = document) => r.querySelector(s);
const $$ = (s, r = document) => [...r.querySelectorAll(s)];
const zahl = (n, d = 0) => new Intl.NumberFormat('de-DE', { minimumFractionDigits: d, maximumFractionDigits: d }).format(n);
const euro = (n, d = 0) => zahl(n, d) + ' €';
const ps = kw => Math.round(kw * 1.35962);
const nb = t => t.replace(/ (?=(l|kWh|g|km|kW|PS)\b)/g, '\u00a0').replace(/ km\b/g, '\u00a0km');
const CO2 = '<span>CO<sub>2</sub></span>';
/* CO₂-Klassen nach § 3a Pkw-EnVKV (kombinierte CO₂-Emissionen in g/km) */
const KLASSEN = [['A', 0, '#17703f', '#fff'], ['B', 95, '#367d28', '#fff'], ['C', 115, '#9cbf2b', '#121314'], ['D', 135, '#f0cf1f', '#121314'],
['E', 155, '#f2a12e', '#121314'], ['F', 175, '#e0612a', '#121314'], ['G', Infinity, '#c62a26', '#fff']];
const klasse = g => KLASSEN.find(k => g <= k[1]);
/* Feed (Beispiel, wie aus dem Händlersystem; Reihenfolge = neu eingestellt zuerst).
art: neu | tz (Tageszulassung) | gebraucht · antrieb: verbrenner | elektro | phev · null = fehlt im Feed */
const FEED = [
{ nr: '26-0433', marke: 'Arvena', modell: 'Talu', linie: '1.5 Turbo Style', art: 'neu', ez: null, km: 5, kw: 110, kraftstoff: 'Benzin', antrieb: 'verbrenner', getriebe: 'Automatik', preis: 31250, steuer: 'ausweisbar', l: 6.0, co2: null, farbe: '#24272b', form: 'suv', rate: true },
{ nr: '26-0431', marke: 'Arvena', modell: 'Pina', linie: '1.0 Komfort', art: 'neu', ez: null, km: 5, kw: 55, kraftstoff: 'Benzin', antrieb: 'verbrenner', getriebe: 'Schaltgetriebe', preis: 18990, steuer: 'ausweisbar', l: 5.4, co2: 123, farbe: '#9b2c24', form: 'klein', rate: true },
{ nr: '26-0418', marke: 'Orvin', modell: 'Kanto Kombi', linie: '1.5 Benzin Komfort', art: 'tz', ez: '10/2026', km: 10, kw: 110, kraftstoff: 'Benzin', antrieb: 'verbrenner', getriebe: 'Automatik', preis: 33990, steuer: 'ausweisbar', l: 6.9, co2: 161, farbe: '#a69a83', form: 'kombi', rate: true },
{ nr: '26-0412', marke: 'Arvena', modell: 'Talu', linie: '1.5 Hybrid Komfort', art: 'tz', ez: '09/2026', km: 15, kw: 96, kraftstoff: 'Hybrid (Benzin/Elektro)', antrieb: 'verbrenner', getriebe: 'Automatik', preis: 29480, steuer: 'ausweisbar', l: 5.1, co2: 116, farbe: '#2c4a6e', form: 'suv', rate: true },
{ nr: '26-0405', marke: 'Arvena', modell: 'Talu', linie: '1.5 Plug-in-Hybrid Style', art: 'neu', ez: null, km: 5, kw: 180, kraftstoff: 'Plug-in-Hybrid (Benzin/Elektro)', antrieb: 'phev', getriebe: 'Automatik', preis: 41290, steuer: 'ausweisbar', l: 0.9, kwh: 18.2, co2: 21, lEnt: 6.4, co2Ent: 149, farbe: '#5b2333', form: 'suv', rate: true },
{ nr: '26-0398', marke: 'Kalvo', modell: 'e3', linie: 'Standard Range', art: 'neu', ez: null, km: 8, kw: 150, kraftstoff: 'Elektro', antrieb: 'elektro', getriebe: 'Automatik', preis: 36900, steuer: 'ausweisbar', kwh: 15.9, co2: 0, farbe: '#e6e6e1', form: 'limo', rate: true },
{ nr: '25-1102', marke: 'Arvena', modell: 'Sorte Kombi', linie: '1.6 Plug-in-Hybrid Business', art: 'gebraucht', ez: '05/2025', km: 8900, kw: 165, kraftstoff: 'Plug-in-Hybrid (Benzin/Elektro)', antrieb: 'phev', getriebe: 'Automatik', preis: 34750, steuer: 'ausweisbar', wltp: true, l: 1.0, kwh: 16.8, co2: 23, lEnt: 5.9, co2Ent: 137, farbe: '#6b7178', form: 'kombi', rate: true },
{ nr: '23-0954', marke: 'Tessra', modell: 'Mova', linie: 'Elektro Allrad', art: 'gebraucht', ez: '11/2023', km: 21300, kw: 210, kraftstoff: 'Elektro', antrieb: 'elektro', getriebe: 'Automatik', preis: 33900, steuer: 'ausweisbar', wltp: true, kwh: 17.4, co2: 0, farbe: '#3f5d4a', form: 'suv', rate: true },
{ nr: '22-1187', marke: 'Arvena', modell: 'Sorte', linie: '1.5 Hybrid', art: 'gebraucht', ez: '02/2022', km: 41800, kw: null, kraftstoff: 'Hybrid (Benzin/Elektro)', antrieb: 'verbrenner', getriebe: 'Automatik', preis: 22990, steuer: 'ausweisbar', wltp: true, l: 4.7, co2: 107, farbe: '#4f5d6b', form: 'kombi', rate: true },
{ nr: '21-0640', marke: 'Kalvo', modell: 'e1', linie: 'City', art: 'gebraucht', ez: '08/2021', km: 38500, kw: 70, kraftstoff: 'Elektro', antrieb: 'elektro', getriebe: 'Automatik', preis: 13490, steuer: 'differenz', wltp: true, kwh: 14.2, co2: 0, farbe: '#3f7f86', form: 'klein' },
{ nr: '19-0877', marke: 'Orvin', modell: 'Kanto', linie: '2.0 Diesel Style', art: 'gebraucht', ez: '03/2019', km: 86400, kw: 110, kraftstoff: 'Diesel', antrieb: 'verbrenner', getriebe: 'Automatik', preis: 21490, steuer: 'differenz', wltp: true, l: 5.6, co2: 148, farbe: '#8d9196', form: 'limo' },
{ nr: '16-0233', marke: 'Arvena', modell: 'Pina', linie: '1.2 Basis', art: 'gebraucht', ez: '06/2016', km: 64200, kw: 51, kraftstoff: 'Benzin', antrieb: 'verbrenner', getriebe: 'Schaltgetriebe', preis: 8990, steuer: 'differenz', wltp: false, farbe: '#b9bcc0', form: 'klein', status: 'reserviert', verkauftBeimAbgleich: true },
];
const TEASER = ['26-0398', '26-0405', '16-0233', '26-0418']; // gepflegte Auswahl; die ersten drei verfügbaren
const ZUSTAND = { neu: 'Neuwagen', tz: 'Tageszulassung', gebraucht: 'Gebrauchtwagen' };
const FARBNAME = { '#24272b': 'Schwarz', '#9b2c24': 'Rot', '#a69a83': 'Sandbeige', '#2c4a6e': 'Dunkelblau', '#5b2333': 'Bordeaux', '#e6e6e1': 'Weiß', '#6b7178': 'Grau', '#3f5d4a': 'Dunkelgrün', '#4f5d6b': 'Graublau', '#3f7f86': 'Petrol', '#8d9196': 'Silbergrau', '#b9bcc0': 'Silber' };
/* Prüft, was eine Karte zeigen darf. Pflicht = neu oder Tageszulassung (Pkw-EnVKV § 5, „neue Pkw“). */
function pruefe(f) {
const pflicht = f.art !== 'gebraucht';
const noetig = f.antrieb === 'elektro' ? [['kwh', 'Stromverbrauch'], ['co2', 'CO₂-Emissionen']]
: f.antrieb === 'phev' ? [['l', 'Kraftstoffverbrauch gewichtet'], ['kwh', 'Stromverbrauch gewichtet'], ['co2', 'CO₂-Emissionen gewichtet'], ['lEnt', 'Verbrauch bei entladener Batterie'], ['co2Ent', 'CO₂ bei entladener Batterie']]
: [['l', 'Kraftstoffverbrauch'], ['co2', 'CO₂-Emissionen']];
const fehlt = (pflicht || f.wltp) ? noetig.filter(([k]) => f[k] == null || Number.isNaN(f[k])).map(([, n]) => n) : [];
const werteDa = (pflicht || f.wltp) && fehlt.length === 0;
const motorErlaubt = !pflicht || werteDa; // Pflichtwert fehlt → Motordaten weg
const hinweise = [];
if (pflicht && !werteDa) hinweise.push(fehlt.join(', ') + ' fehlt im Feed. Leistung ausgeblendet.');
if (!pflicht && f.wltp && !werteDa) hinweise.push(fehlt.join(', ') + ' fehlt im Feed.');
if (f.kw == null) hinweise.push('Leistung fehlt im Feed, Feld weggelassen.');
return { pflicht, werteDa, motorErlaubt, hinweise };
}
/* Monatsrate mit repräsentativem Beispiel (§ 17 PAngV): Annuität, effektiver Jahreszins ohne Nebenkosten */
function finanzierung(f) {
const soll = f.art === 'gebraucht' ? 5.99 : 3.99, n = 48;
const anz = Math.round(f.preis * .2 / 500) * 500, netto = f.preis - anz, r = soll / 100 / 12;
const rate = Math.round(netto * r / (1 - Math.pow(1 + r, -n)) * 100) / 100;
const eff = (Math.pow(1 + r, 12) - 1) * 100;
return { soll, n, anz, netto, rate, eff, gesamt: Math.round(rate * n * 100) / 100 };
}
/* ── Eigene Fahrzeugzeichnungen (SVG, Seitenansicht; Kennzeichen sind von der Seite nicht sichtbar) ── */
const mix = (hex, ziel, t) => { const a = hex.match(/\w\w/g).map(h => parseInt(h, 16)), b = ziel.match(/\w\w/g).map(h => parseInt(h, 16)); return '#' + a.map((v, i) => Math.round(v + (b[i] - v) * t).toString(16).padStart(2, '0')).join(''); };
const FORMEN = {
klein: { raeder: [182, 418], r: 40, koerper: 'M112,300 L112,240 C112,224 118,213 130,207 L160,158 C166,149 176,145 188,145 L318,145 C334,145 346,151 358,161 L398,199 C440,205 470,213 482,223 C490,231 492,247 490,263 L488,300 Z', glas: 'M170,163 C173,157 180,153 190,153 L312,153 C324,153 334,158 342,165 L374,197 L162,197 Z', saeule: 268, tuer: [206, 340], licht: 'M470,216 L486,222 L487,236 L474,232 Z', heck: 'M113,218 L124,214 L124,236 L113,238 Z' },
kombi: { raeder: [164, 438], r: 41, koerper: 'M82,300 L82,238 C82,222 88,212 100,207 L112,162 C116,152 124,147 136,147 L342,145 C360,145 374,151 388,163 L430,201 C476,207 506,215 516,227 C522,237 522,251 520,267 L518,300 Z', glas: 'M122,162 C124,156 128,154 136,154 L338,153 C352,153 364,159 374,167 L404,199 L118,199 Z', saeule: 272, saeule2: 176, tuer: [196, 352], licht: 'M500,218 L517,226 L518,240 L503,236 Z', heck: 'M83,214 L94,212 L94,240 L83,242 Z' },
suv: { raeder: [172, 432], r: 45, koerper: 'M92,300 L92,226 C92,206 98,194 112,188 L126,140 C130,128 140,122 154,122 L332,120 C352,120 366,128 380,140 L422,184 C472,190 502,200 510,212 C516,222 516,240 514,262 L512,300 Z', glas: 'M138,137 C140,131 146,129 154,129 L328,128 C344,128 356,134 366,144 L394,180 L132,180 Z', saeule: 262, saeule2: 188, tuer: [200, 336], licht: 'M494,203 L511,210 L512,226 L497,222 Z', heck: 'M93,200 L106,196 L106,226 L93,228 Z' },
limo: { raeder: [166, 436], r: 40, koerper: 'M82,300 L82,250 C82,234 90,224 106,220 L150,212 L192,162 C200,152 212,148 226,148 L326,148 C344,148 358,154 370,164 L410,204 C468,210 502,218 512,230 C520,240 520,256 518,272 L516,300 Z', glas: 'M204,164 C208,158 216,156 226,156 L322,156 C336,156 348,161 356,168 L384,202 L178,206 Z', saeule: 286, tuer: [214, 356], licht: 'M496,222 L514,230 L515,244 L499,240 Z', heck: 'M83,232 L96,228 L96,250 L83,252 Z' },
};
function autoSvg(f, id) {
const F = FORMEN[f.form], c = f.farbe, hell = mix(c, 'ffffff', .28), dunkel = mix(c, '000000', .35), unten = mix(c, '000000', .55);
const rad = x => `<circle cx="${x}" cy="300" r="${F.r + 7}" fill="#141516" clip-path="url(#k${id})"/><circle cx="${x}" cy="300" r="${F.r}" fill="#1b1c1e"/><circle cx="${x}" cy="300" r="${F.r * .62}" fill="url(#f${id})"/>`
+ [0, 72, 144, 216, 288].map(w => `<line x1="${x}" y1="300" x2="${x}" y2="${300 - F.r * .56}" stroke="#7d8389" stroke-width="5" stroke-linecap="round" transform="rotate(${w + (x % 37)} ${x} 300)"/>`).join('')
+ `<circle cx="${x}" cy="300" r="${F.r * .16}" fill="#5d6268"/>`;
const saeulen = [F.saeule, F.saeule2].filter(Boolean).map(x => `<rect x="${x - 6}" y="110" width="12" height="100" fill="${dunkel}" clip-path="url(#g${id})"/>`).join('');
return `<defs><linearGradient id="l${id}" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="${hell}"/><stop offset=".42" stop-color="${c}"/><stop offset="1" stop-color="${dunkel}"/></linearGradient>`
+ `<linearGradient id="s${id}" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#3a4249"/><stop offset="1" stop-color="#15191c"/></linearGradient>`
+ `<radialGradient id="f${id}"><stop offset="0" stop-color="#e2e5e8"/><stop offset="1" stop-color="#9da3a9"/></radialGradient>`
+ `<clipPath id="k${id}"><rect x="0" y="0" width="600" height="301"/></clipPath><clipPath id="g${id}"><path d="${F.glas}"/></clipPath></defs>`
+ `<path d="${F.koerper}" fill="url(#l${id})"/>`
+ `<path d="${F.glas}" fill="url(#s${id})"/>` + saeulen
+ `<path d="${F.glas}" fill="none" stroke="${dunkel}" stroke-width="3"/>`
+ `<path d="M${F.raeder[0] - F.r - 20},284 L${F.raeder[1] + F.r + 20},284 L${F.raeder[1] + F.r + 20},300 L${F.raeder[0] - F.r - 20},300 Z" fill="${unten}" opacity=".55"/>`
+ F.tuer.map(x => `<line x1="${x}" y1="${x === F.tuer[0] ? 203 : 203}" x2="${x - 4}" y2="282" stroke="${dunkel}" stroke-width="2"/>`).join('')
+ `<line x1="${F.saeule}" y1="203" x2="${F.saeule - 3}" y2="282" stroke="${dunkel}" stroke-width="2"/>`
+ `<rect x="${F.saeule - 44}" y="216" width="22" height="5" rx="2.5" fill="${dunkel}"/><rect x="${F.tuer[1] - 44}" y="216" width="22" height="5" rx="2.5" fill="${dunkel}"/>`
+ `<path d="M${F.raeder[0] - 60},212 L${F.raeder[1] + 60},210" stroke="#fff" stroke-opacity=".28" stroke-width="2"/>`
+ `<path d="${F.licht}" fill="#f4f1e6" stroke="${dunkel}" stroke-width="1.5"/><path d="${F.heck}" fill="#a3221c"/>`
+ rad(F.raeder[0]) + rad(F.raeder[1]);
}
function hof(innen, id, breite = 600, hoehe = 400, boden = 340) {
const fugen = Array.from({ length: Math.ceil(breite / 150) }, (_, i) => `<line x1="${75 + i * 150}" y1="0" x2="${75 + i * 150}" y2="${boden - 110}" stroke="#000" stroke-opacity=".05" stroke-width="2"/>`).join('');
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${breite} ${hoehe}"><defs><linearGradient id="w${id}" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#e9e8e3"/><stop offset="1" stop-color="#d6d5cf"/></linearGradient>`
+ `<linearGradient id="b${id}" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#bebdb7"/><stop offset="1" stop-color="#a9a8a2"/></linearGradient><filter id="u${id}" x="-20%" y="-200%" width="140%" height="500%"><feGaussianBlur stdDeviation="7"/></filter></defs>`
+ `<rect width="${breite}" height="${boden - 110}" fill="url(#w${id})"/><rect y="38" width="${breite}" height="14" fill="#232629" opacity=".9"/>${fugen}`
+ `<rect y="${boden - 110}" width="${breite}" height="${hoehe - boden + 110}" fill="url(#b${id})"/><line x1="0" y1="${boden - 110}" x2="${breite}" y2="${boden - 110}" stroke="#000" stroke-opacity=".14" stroke-width="2"/>`
+ `<path d="M${breite * .06},${hoehe} L${breite * .16},${boden - 110}" stroke="#fff" stroke-opacity=".5" stroke-width="5"/><path d="M${breite * .94},${hoehe} L${breite * .84},${boden - 110}" stroke="#fff" stroke-opacity=".5" stroke-width="5"/>`
+ innen + '</svg>';
}
const schatten = (id, cx, rx) => `<ellipse cx="${cx}" cy="342" rx="${rx}" ry="13" fill="#000" opacity=".34" filter="url(#u${id})"/>`;
const bildUrl = svg => 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg);
function fahrzeugBild(f, i) { return bildUrl(hof(schatten(i, 300, 230) + autoSvg(f, i), i)); }
/* ── Karte bauen ── */
const NAME = f => `${f.marke} ${f.modell} ${f.linie}`;
function pflichtzeile(f) {
if (f.antrieb === 'elektro') {
const k = klasse(f.co2);
return nb(`<span>Stromverbrauch kombiniert: <b>${zahl(f.kwh, 1)} kWh/100 km</b></span><span>${CO2}-Emissionen kombiniert: <b>${zahl(f.co2)} g/km</b></span><span>${CO2}-Klasse: <b class="kl" style="--k:${k[2]};--kt:${k[3]}">${k[0]}</b></span>`);
}
if (f.antrieb === 'phev') {
const k = klasse(f.co2), ke = klasse(f.co2Ent);
return nb(`<span>Energieverbrauch gewichtet kombiniert: <b>${zahl(f.l, 1)} l/100 km und ${zahl(f.kwh, 1)} kWh/100 km</b></span><span>${CO2}-Emissionen gewichtet kombiniert: <b>${zahl(f.co2)} g/km</b></span><span>Kraftstoffverbrauch bei entladener Batterie kombiniert: <b>${zahl(f.lEnt, 1)} l/100 km</b></span>`
+ `<span>${CO2}-Klasse gewichtet kombiniert: <b class="kl" style="--k:${k[2]};--kt:${k[3]}">${k[0]}</b></span><span>${CO2}-Klasse bei entladener Batterie: <b class="kl" style="--k:${ke[2]};--kt:${ke[3]}">${ke[0]}</b></span>`);
}
const k = klasse(f.co2);
return nb(`<span>Kraftstoffverbrauch kombiniert: <b>${zahl(f.l, 1)} l/100 km</b></span><span>${CO2}-Emissionen kombiniert: <b>${zahl(f.co2)} g/km</b></span><span>${CO2}-Klasse: <b class="kl" style="--k:${k[2]};--kt:${k[3]}">${k[0]}</b></span>`);
}
function karte(f, i) {
const p = pruefe(f);
const preisText = f.art === 'gebraucht' ? 'Gesamtpreis inkl. MwSt.' : 'Hauspreis inkl. Überführung und MwSt.';
const steuer = f.steuer === 'differenz' ? 'Differenzbesteuert' : 'MwSt. ausweisbar';
const daten = [
['Erstzulassung', f.ez || 'Neufahrzeug'],
['Kilometer', zahl(f.km) + '\u00a0km'],
(p.motorErlaubt && f.kw != null) ? [f.antrieb === 'phev' ? 'Leistung (System)' : 'Leistung', `${zahl(f.kw)}\u00a0kW (${ps(f.kw)}\u00a0PS)`] : null,
['Kraftstoff', f.kraftstoff],
['Getriebe', f.getriebe],
['Bestands-Nr.', f.nr],
].filter(Boolean);
let rate = '';
if (f.rate) {
const r = finanzierung(f);
rate = `<div class="rate"><p class="rate-zahl"><b>${euro(r.rate, 2)}</b><span>im Monat, ${r.n} Raten</span><b>${zahl(r.eff, 2)}\u00a0%</b><span>effektiver Jahreszins</span></p>`
+ `<p class="warnung">Achtung! Kreditaufnahme kostet Geld.</p>`
+ `<p class="beispiel">Repräsentatives Beispiel: Fahrzeugpreis ${euro(f.preis)}, Anzahlung ${euro(r.anz)}, Nettodarlehensbetrag ${euro(r.netto)}, Laufzeit ${r.n} Monate, gebundener Sollzinssatz ${zahl(r.soll, 2)} % p. a., <strong>effektiver Jahreszins ${zahl(r.eff, 2)} %</strong>, ${r.n} Monatsraten zu ${euro(r.rate, 2)}, Gesamtbetrag ${euro(r.gesamt, 2)}. Darlehensgeber: <span class="platzhalter">Bank, Anschrift</span></p></div>`;
} else {
rate = `<p class="keine-rate">Finanzierung für dieses Fahrzeug auf Anfrage.</p>`;
}
const zeile = p.werteDa ? `<p class="pflicht">${pflichtzeile(f)}</p>`
: `<div class="ohne"><p class="ohne-hinweis">${f.art === 'gebraucht' && !f.wltp ? 'Gebrauchtfahrzeug ohne WLTP-Werte im Feed: keine Verbrauchszeile.' : ''}</p></div>`;
const notiz = p.hinweise.length ? `<p class="pruef-notiz"><b>Prüfen (intern):</b> ${p.hinweise.join(' ')}</p>` : '';
return `<article class="karte${p.hinweise.length ? ' pruefen' : ''}" data-nr="${f.nr}">
<div class="bild"><img src="${fahrzeugBild(f, i)}" alt="${f.marke} ${f.modell} in ${FARBNAME[f.farbe] || 'Wagenfarbe'}, Seitenansicht auf dem Hof (Zeichnung)" width="600" height="400" loading="lazy" decoding="async" sizes="auto">
<div class="schilder"><span class="schild">${ZUSTAND[f.art]}</span>${f.status === 'reserviert' ? '<span class="schild res">Reserviert</span>' : ''}</div></div>
<div class="koerper">
<h3><a class="titel" href="#fahrzeug-${f.nr}" data-demo-link="Fahrzeugseite ${f.nr}"><span class="modell">${f.marke} ${f.modell}</span> <span class="linie">${f.linie}</span></a></h3>
<p class="preis"><b>${euro(f.preis)}</b><span>${preisText}</span><span class="steuer">${steuer}</span></p>
<div class="rate-huelle"><div>${rate}</div></div>
<dl class="daten">${daten.map(([t, d]) => `<div><dt>${t}</dt><dd>${d}</dd></div>`).join('')}</dl>
${notiz}${zeile}
</div>
</article>`;
}
/* ── Zustand und Darstellung ── */
const sek = $('.bestand'), liste = $('#karten');
const VARIANTEN = ['teaser', 'liste', 'portal'];
const S = { v: 'teaser', anzeige: 'preis', filter: new Set(), sort: 'neu', abgeglichen: false, gestoert: false, pflege: false };
const FILTER = {
preis: f => f.preis <= 20000, strom: f => f.antrieb !== 'verbrenner', automatik: f => f.getriebe === 'Automatik',
km: f => f.km < 30000, neu: f => f.art !== 'gebraucht',
};
const FILTERNAME = { preis: 'Bis 20.000 €', strom: 'Elektro und Plug-in', automatik: 'Automatik', km: 'Unter 30.000 km', neu: 'Neu und Tageszulassung' };
const SORT = {
neu: () => 0, 'preis-auf': (a, b) => a.preis - b.preis, 'preis-ab': (a, b) => b.preis - a.preis, km: (a, b) => a.km - b.km,
ez: (a, b) => (b.ez ? +b.ez.slice(3) * 12 + +b.ez.slice(0, 2) : 1e6) - (a.ez ? +a.ez.slice(3) * 12 + +a.ez.slice(0, 2) : 1e6),
};
const bestand = () => FEED.filter(f => !(S.abgeglichen && f.verkauftBeimAbgleich));
let gezeigt = [];
function auswahl() {
const alle = bestand();
if (S.v === 'teaser') return TEASER.map(nr => alle.find(f => f.nr === nr)).filter(Boolean).slice(0, 3);
return alle.filter(f => [...S.filter].every(k => FILTER[k](f))).sort(SORT[S.sort]);
}
function zeichne() {
const neu = auswahl(), alle = bestand();
const vorher = new Set(gezeigt);
liste.innerHTML = neu.map(f => `<li data-nr="${f.nr}"${vorher.size && !vorher.has(f.nr) && !reduziert() ? ' class="kommt"' : ''}>${karte(f, FEED.indexOf(f))}</li>`).join('');
gezeigt = neu.map(f => f.nr);
$('#leer').hidden = !(S.v === 'liste' && neu.length === 0);
const n = alle.length;
$('#treffer').textContent = S.v === 'teaser' ? `${neu.length} von ${n} Fahrzeugen` : (S.filter.size ? `${neu.length} von ${n} Fahrzeugen` : `${n} Fahrzeuge`);
$('#alle-n').textContent = n;
$$('.chip').forEach(c => {
const k = c.dataset.filter, an = S.filter.has(k);
c.setAttribute('aria-pressed', an);
const probe = alle.filter(f => [...S.filter, k].every(x => FILTER[x](f))).length;
let z = c.querySelector('.n'); if (!z) { z = document.createElement('span'); z.className = 'n'; c.append(z); }
z.textContent = `(${probe})`;
});
if (S.filter.size) $('#leer-text').textContent = `Zu „${[...S.filter].map(k => FILTERNAME[k]).join('“ und „')}“ steht gerade nichts auf dem Hof. Wir suchen gern für Sie: Ein Suchauftrag kostet nichts.`;
// Pflege-Ansicht
const punkte = FEED.filter(f => alle.includes(f)).map(f => [f, pruefe(f)]).filter(([, p]) => p.hinweise.length);
$('#pflegepunkte').innerHTML = punkte.map(([f, p]) => `<li>${f.nr} ${f.marke} ${f.modell}: ${p.hinweise.join(' ')}</li>`).join('');
$('#pflegeliste').hidden = !S.pflege;
sek.classList.toggle('pflege', S.pflege);
}
function setzeStand() {
const zeit = S.abgeglichen ? '08:40' : '07:40';
const st = $('#stand');
st.classList.toggle('gestoert', S.gestoert);
$('#stand-status').textContent = S.gestoert ? 'Abgleich gestört' : 'Aktuell';
$('#stand-zeit').textContent = (S.gestoert ? 'Letzter Stand: ' : 'Stand: ') + `02.11.2026, ${zeit} Uhr`;
$('#stand-quelle').textContent = S.gestoert ? 'nächster Versuch in 15 Minuten' : 'aus unserem Händlersystem, stündlicher Abgleich';
$('#feed-hinweis').hidden = !S.gestoert;
$('#feed-hinweis p').firstChild.textContent = `Sie sehen den letzten Stand vom 02.11.2026, ${zeit} Uhr. Ob ein Fahrzeug noch da ist, sagen wir Ihnen gern am Telefon: `;
$('#p-stoerung').setAttribute('aria-pressed', S.gestoert);
}
const TEXTE = {
teaser: ['Fahrzeugbestand', 'Auf dem Hof in Vechta', 'Neu- und Gebrauchtwagen, Elektro und Plug-in-Hybrid. Jedes Fahrzeug steht bei uns auf dem Hof an der Oldenburger Straße und ist probefahrbereit.'],
liste: ['Fahrzeugbestand', 'Alle Fahrzeuge', 'Direkt aus unserem Händlersystem: Gesamtpreise inklusive Mehrwertsteuer, Neuwagen inklusive Überführung. Bei Fragen zu einem Fahrzeug nennen Sie uns einfach die Bestandsnummer.'],
portal: ['Fahrzeugbestand', 'Unsere Fahrzeuge', 'Wir haben laufend rund 40 Neu- und Gebrauchtwagen auf dem Hof. Die aktuelle Auswahl mit allen Angaben steht in der Fahrzeugbörse.'],
};
const WERTE = {
teaser: 'Teaser 3 Karten · öffnen 250 ms expo.out · schließen 170 ms expo.in · Hover 120 ms',
liste: 'Liste · neue Treffer 8 px, 250 ms expo.out · verkauft 170 ms expo.in · Hover 120 ms',
portal: 'Portal · kein Feed, keine Preise · Pfeil-Morph 300 ms quad.out · Hover 120 ms',
};
function setzeVariante(v, url = true) {
S.v = VARIANTEN.includes(v) ? v : 'teaser';
sek.dataset.variante = S.v;
sek.classList.toggle('teaser', S.v === 'teaser');
$$('[data-v]').forEach(b => b.setAttribute('aria-pressed', b.dataset.v === S.v));
const [k, h, l] = TEXTE[S.v];
$('#kicker').textContent = k; $('#h-bestand').textContent = h; $('#lead').textContent = l;
$('#werte').textContent = WERTE[S.v];
if (url) { const q = new URLSearchParams(location.search); q.set('v', S.v); history.replaceState(null, '', '?' + q.toString().replace(/=(?=&|$)/g, '')); }
gezeigt = []; zeichne();
}
function setzeAnzeige(a) {
S.anzeige = a;
$$('[data-anzeige]').forEach(b => b.setAttribute('aria-pressed', b.dataset.anzeige === a));
sek.classList.toggle('zeige-rate', a === 'rate');
}
function abgleich() {
if (S.gestoert) { toast('Abgleich nicht möglich: Feed gestört. Der letzte Stand bleibt stehen.'); return; }
if (S.abgeglichen) { toast('Abgleich: keine Änderungen seit 08:40 Uhr.'); return; }
const weg = FEED.find(f => f.verkauftBeimAbgleich);
const li = liste.querySelector(`li[data-nr="${weg.nr}"]`);
const fertig = () => { S.abgeglichen = true; setzeStand(); zeichne(); const st = $('#stand'); st.classList.remove('blitz'); void st.offsetWidth; if (!reduziert()) st.classList.add('blitz'); };
if (li && !reduziert()) { li.classList.add('geht'); setTimeout(fertig, 180); } else fertig();
toast(`Abgleich 08:40 Uhr: ${weg.marke} ${weg.modell} (${weg.nr}) ist verkauft und nicht mehr in der Liste.`);
}
let toastUhr;
function toast(t) {
const el = $('#toast'); el.textContent = t; el.classList.add('an');
clearTimeout(toastUhr); toastUhr = setTimeout(() => el.classList.remove('an'), 3200);
}
/* Bedienung */
$$('.chip').forEach(c => c.addEventListener('click', () => { const k = c.dataset.filter; S.filter.has(k) ? S.filter.delete(k) : S.filter.add(k); zeichne(); }));
$$('[data-anzeige]').forEach(b => b.addEventListener('click', () => setzeAnzeige(b.dataset.anzeige)));
$('#sortierung').addEventListener('change', e => { S.sort = e.target.value; gezeigt = []; zeichne(); });
$('#zuruecksetzen').addEventListener('click', () => { S.filter.clear(); zeichne(); $('.chip').focus(); });
$('#alle').addEventListener('click', () => { setzeVariante('liste'); $('#h-bestand').setAttribute('tabindex', '-1'); $('#h-bestand').focus({ preventScroll: true }); sek.scrollIntoView({ block: 'start' }); });
$$('[data-v]').forEach(b => b.addEventListener('click', () => { stopp(); setzeVariante(b.dataset.v); }));
$('#p-abgleich').addEventListener('click', abgleich);
$('#p-stoerung').addEventListener('click', () => { S.gestoert = !S.gestoert; setzeStand(); });
$('#p-pflege').addEventListener('click', () => { S.pflege = !S.pflege; $('#p-pflege').setAttribute('aria-pressed', S.pflege); zeichne(); });
document.addEventListener('click', e => {
const a = e.target.closest('[data-demo-link]');
if (!a) return;
e.preventDefault();
toast(`Demo: Hier öffnet „${a.dataset.demoLink}“.`);
});
/* Portal-Bild: drei Fahrzeuge nebeneinander */
(() => {
const z = [FEED[3], FEED[5], FEED[7]];
const innen = z.map((f, i) => `<g transform="translate(${40 + i * 420 - 66},128) scale(.8)">${schatten('p' + i, 300, 220)}${autoSvg(f, 'p' + i)}</g>`).join('');
const svg = hof(innen, 'p', 1300, 460, 400).replace('</defs>', ['p0', 'p1', 'p2'].map(i => `<filter id="u${i}" x="-20%" y="-200%" width="140%" height="500%"><feGaussianBlur stdDeviation="7"/></filter>`).join('') + '</defs>');
$('#portal-bild').src = bildUrl(svg);
})();
/* ── Vorführung ── */
const zeiger = $('#zeiger');
let uhren = [], schleife = null;
const nach = (ms, fn) => uhren.push(setTimeout(fn, ms));
function stopp() { uhren.forEach(clearTimeout); uhren = []; clearTimeout(schleife); html.classList.remove('vorfuehrung'); $$('.zeigt').forEach(e => e.classList.remove('zeigt')); }
function zeigeAuf(el, klick) {
if (!el) return;
const r = el.getBoundingClientRect();
zeiger.style.transform = `translate(${Math.round(r.left + r.width * .5)}px, ${Math.round(r.top + r.height * .55)}px)`;
if (klick) setTimeout(() => { zeiger.classList.add('drueckt'); setTimeout(() => zeiger.classList.remove('drueckt'), 140); }, reduziert() ? 0 : 650);
}
function grundzustand() {
S.filter.clear(); S.sort = 'neu'; $('#sortierung').value = 'neu'; S.abgeglichen = false; S.gestoert = false; S.pflege = false;
$('#p-pflege').setAttribute('aria-pressed', 'false');
setzeAnzeige('preis'); setzeStand(); gezeigt = []; zeichne();
}
function neustart() { stopp(); grundzustand(); window.scrollTo(0, 0); }
function vorfuehren() {
stopp(); grundzustand(); window.scrollTo(0, 0);
html.classList.add('vorfuehrung');
const t = reduziert() ? 1.3 : 1;
zeiger.style.transform = `translate(${innerWidth * .62}px, ${innerHeight * .9}px)`;
if (S.v === 'teaser') {
nach(500, () => zeigeAuf($('[data-anzeige="rate"]'), true));
nach(1250, () => setzeAnzeige('rate'));
nach(3900, () => zeigeAuf($('[data-anzeige="preis"]'), true));
nach(4650, () => setzeAnzeige('preis'));
nach(5600, () => zeigeAuf($('#stand')));
nach(6200, abgleich);
nach(8600, () => { S.gestoert = true; setzeStand(); });
nach(11200, () => { S.gestoert = false; setzeStand(); });
schleife = setTimeout(vorfuehren, 12400 * t);
} else if (S.v === 'liste') {
const chip = k => $(`.chip[data-filter="${k}"]`);
nach(500, () => zeigeAuf(chip('strom'), true));
nach(1250, () => chip('strom').click());
nach(2600, () => zeigeAuf($('#sortierung'), true));
nach(3350, () => { $('#sortierung').value = 'preis-auf'; S.sort = 'preis-auf'; gezeigt = []; zeichne(); });
nach(4700, () => zeigeAuf(chip('preis'), true));
nach(5450, () => chip('preis').click());
nach(6800, () => zeigeAuf(chip('neu'), true));
nach(7550, () => chip('neu').click());
nach(9200, () => zeigeAuf($('#zuruecksetzen'), true));
nach(9950, () => { S.filter.clear(); zeichne(); });
nach(10800, () => { S.pflege = true; $('#p-pflege').setAttribute('aria-pressed', 'true'); zeichne(); zeigeAuf($('.karte.pruefen')); });
nach(13400, () => { S.pflege = false; $('#p-pflege').setAttribute('aria-pressed', 'false'); zeichne(); });
schleife = setTimeout(vorfuehren, 14400 * t);
} else {
const ziele = [$('.portal-text .knopf'), ...$$('.schnelllinks a').slice(0, 2), $('.berater .knopf')];
ziele.forEach((el, i) => {
nach(500 + i * 1500, () => { $$('.zeigt').forEach(e => e.classList.remove('zeigt')); zeigeAuf(el); });
nach(1150 + i * 1500, () => el.classList.add('zeigt'));
});
nach(6600, () => $$('.zeigt').forEach(e => e.classList.remove('zeigt')));
schleife = setTimeout(vorfuehren, 7200 * t);
}
}
function pause() { stopp(); }
function weiter() { vorfuehren(); }
setzeVariante(new URLSearchParams(location.search).get('v'), false);
setzeStand();
})();
</script>
</body>
</html>