Objekt-Steckbrief im Exposé
Exposé-Eckdaten als ruhige Tabelle mit Tabellenziffern, Pflichtangaben zum Energieausweis mit Skala A+ bis H und ein eigener SVG-Grundriss, der mit der Raumliste verbunden ist.
Haus oder Eigentumswohnung zum Kauf: Kaufpreis, Käuferprovision, Bedarfs- oder Verbrauchsausweis, Grundriss je Geschoss.
Die Eckdaten eines Objekts stehen als ruhige Datentabelle: Fläche, Zimmer, Baujahr, Preis und Provision, alle Zahlen in Tabellenziffern. Darunter folgen die Pflichtangaben aus dem Energieausweis: Art des Ausweises, Kennwert, wesentlicher Energieträger, Baujahr und Effizienzklasse auf der Skala A+ bis H. Ein eigener Grundriss als SVG zeigt die Räume. Raumliste und Plan sind verbunden: Wer eine Zeile wählt, sieht den Raum im Plan.
Käufer vergleichen Exposés Zeile für Zeile. Eine klare Tabelle mit festen Ziffernbreiten macht das leicht und wirkt seriös. Die Energieangaben sind in Anzeigen Pflicht. Als gestalteter Block wirken sie wie ein Teil des Angebots und nicht wie Kleingedrucktes. Der Grundriss beantwortet die häufigste Frage, bevor jemand anruft.
Wann einsetzen
Exposé-Seiten von Maklern, Projektentwicklern und Hausverwaltungen. Objektkarten in Angebotslisten, dort mit der kurzen Pflichtzeile. Mietangebote mit Kaltmiete, Nebenkosten und Kaution.
Wann nicht
Wenn die Objektdaten aus einem Portal-Feed kommen und das Portal-Widget eingebunden werden muss. Wenn kein Energieausweis vorliegt: dann keine Werte erfinden, sondern den Grund nennen (z. B. Baudenkmal) und das vom Kunden prüfen lassen. Für Gewerbeobjekte gelten andere Angaben (Wärme und Strom getrennt).
Varianten
- Kauf: Haus mit Bedarfsausweis, Grundriss mit Geschossen, Kaufpreis und Käuferprovision
- Miete: Wohnung mit Verbrauchsausweis, Kaltmiete, Nebenkosten, Kaution
- Anzeige: Objektkarten in der Angebotsliste mit kurzer Pflichtzeile und Mini-Skala
- Fahrzeug (Autohaus): Neuwagen-Angebot mit Pflichtangaben nach Pkw-EnVKV (Verbrauch, CO₂-Emissionen, CO₂-Klasse) direkt neben den Motordaten, gut lesbar wie der Angebotstext; vom Kunden prüfen lassen
- Nichtwohngebäude: Endenergie für Wärme und Strom getrennt, ohne Effizienzklasse
Umsetzung
Eckdaten als <table> mit <caption> und <th scope=row>, Zahlen mit font-variant-numeric: tabular-nums und rechtsbündig. Werte mit Intl.NumberFormat('de-DE') formatieren (985.000 €, 118,4 kWh/(m²·a)). Energieblock als <dl> mit den fünf Pflichtangaben, darunter die Skala A+ bis H aus neun gleich breiten Feldern. Die Marke sitzt innerhalb der Klasse linear zwischen den Grenzen aus Anlage 10, H endet bei 300. Die Skala ist role=img mit ausgeschriebenem aria-label. Grundriss als inline-SVG aus Raumdaten (Rechtecke in Metern, 1 m = 40 Einheiten), Außenwand dick, Innenwände dünn, Türen als Viertelkreis, Fenster als Doppellinie, Nordpfeil und Maßstab. Raumliste aus Buttons mit aria-pressed hebt den Raum im Plan hervor, Hover im Plan hebt die Zeile hervor. Geschosse per Umschalter. Druckansicht per @media print mit beiden Geschossen. Mobil stehen die Eckdaten oben, die Anfrage als feste Leiste unten. Rechtliches (Pflichtangaben, Provisionstext, Stand des Gesetzes) vom Kunden prüfen lassen.
Robust umsetzen
Telefon. Eckdaten zuerst, dann Grundriss und Energieblock. Die Tabelle bleibt zweispaltig und bricht nicht in Karten. Die Raumliste ist groß genug zum Tippen. Die Anfrage sitzt als feste Leiste am unteren Rand.
Ohne Bewegung. Die Marke steht sofort am Kennwert, Geschosswechsel ohne Überblendung, Hervorhebungen ohne Übergang.
Barrierefrei. Echte Tabelle mit caption und Zeilenköpfen. Einheiten ausgeschrieben im Text (Quadratmeter) bzw. per abbr. Die Energieskala hat ein aria-label mit Klasse und Kennwert, die Werte stehen zusätzlich als Text. Farbe trägt nie allein die Bedeutung, die Klasse steht als Buchstabe da. Grundriss als role=img mit Beschreibung, die Raumliste ist die zugängliche Entsprechung. Umschalter mit aria-pressed, Fokus sichtbar.
Code
<!DOCTYPE html>
<!--
Objekt-Steckbrief im Exposé
Fundus · Sektionen & Funktionen · https://cecodesign.de/lab/fundus/sektionen/objekt-steckbrief-expose/
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>Objekt-Steckbrief im Exposé · 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: 'Libre Caslon Display'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/libre-caslon-display@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; font-display: swap; }
@font-face { font-family: 'Hanken Grotesk'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/hanken-grotesk:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
/* Variante Fahrzeug: Welt Werkstatt und Autohaus (Schriften und Farben aus handwerk-werkstatt-klartext) */
@font-face { font-family: 'Special Gothic'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/special-gothic:vf@latest/latin-standard-normal.woff2') format('woff2-variations'); font-weight: 400 700; font-stretch: 75% 125%; font-display: swap; }
@font-face { font-family: 'Atkinson Hyperlegible Next'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/atkinson-hyperlegible-next:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 200 800; 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; }
/* Rezept „Immobilien: diskret und hochwertig“: warme helle Fläche, Sand, ein tiefer Markenton, Akzent nur für den Haupt-CTA */
:root {
--grund: #fbf8f2; --sand: #efe7dc; --sand-tief: #e3d8c8; --papier: #fffdf9;
--marke: #17394f; --marke-hell: rgba(23, 57, 79, .1);
--text: #2f2e2c; --leise: #6a645b; --linie: #d3cabd; --linie-fein: rgba(23, 57, 79, .12);
--akzent: #b25f28;
--kurve: cubic-bezier(0.165, 0.84, 0.44, 1); /* easeOutQuart, gemessen bei inigo.com */
--ui: 400ms; --hover: 200ms; --marke-dauer: 1000ms;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--grund); color: var(--text); font-family: 'Hanken Grotesk', system-ui, sans-serif; font-size: 16px; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
a { color: inherit; }
.ziffern { font-variant-numeric: tabular-nums lining-nums; }
.label { font-size: .75rem; font-weight: 600; letter-spacing: .09em; text-transform: uppercase; color: var(--leise); }
:focus-visible { outline: 2px solid var(--marke); outline-offset: 3px; }
/* ── Kopf der Seite ─────────────────────────────── */
.seite { max-width: 1320px; margin: 0 auto; padding: 0 clamp(18px, 4vw, 56px) 80px; }
.leiste { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 22px 0; border-bottom: 1px solid var(--linie); }
.wortmarke { display: flex; align-items: center; gap: 12px; text-decoration: none; }
.wortmarke svg { width: 34px; height: 34px; flex: none; }
.wortmarke b { display: block; font-weight: 700; font-size: 14px; letter-spacing: .2em; color: var(--marke); }
.wortmarke span { display: block; font-size: 12px; color: var(--leise); margin-top: 2px; letter-spacing: .02em; }
.nav { display: flex; gap: 28px; list-style: none; font-size: 15px; }
.nav a { text-decoration: none; padding: 6px 0; background: linear-gradient(currentColor, currentColor) 0 100% / 0 1px no-repeat; transition: background-size var(--hover) var(--kurve); }
.nav a:hover, .nav a[aria-current] { background-size: 100% 1px; }
.nav a[aria-current] { color: var(--marke); font-weight: 600; }
.tel { font-size: 14px; color: var(--marke); text-decoration: none; font-weight: 600; }
.pfad { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 8px 24px; padding: 18px 0 0; }
.pfad ol { display: flex; gap: 10px; list-style: none; font-size: 13px; color: var(--leise); }
.pfad li + li::before { content: '/'; margin-right: 10px; color: var(--linie); }
/* ── Titel und Preis ────────────────────────────── */
.titel { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: end; gap: 16px 48px; padding: 28px 0 32px; }
.titel h1 { font: 400 clamp(2.25rem, 1.4rem + 3.2vw, 4.5rem)/1.08 'Libre Caslon Display', Georgia, serif; letter-spacing: -.005em; color: var(--marke); text-wrap: balance; }
.titel .unter { margin-top: 12px; font-size: 1.0625rem; color: var(--leise); }
.preis { text-align: right; }
.preis strong { display: block; font-size: clamp(1.75rem, 1.2rem + 1.4vw, 2.5rem); font-weight: 500; letter-spacing: -.01em; color: var(--marke); line-height: 1.1; margin-top: 6px; }
.preis small { display: block; font-size: 13px; color: var(--leise); margin-top: 6px; }
/* ── Raster des Exposés ─────────────────────────── */
.raster { display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); grid-template-areas: "plan daten" "plan energie"; gap: 28px clamp(28px, 4vw, 64px); align-items: start; }
.daten { grid-area: daten; }
.planfeld { grid-area: plan; }
.energie { grid-area: energie; }
/* Eckdaten: ruhige Tabelle, Ziffern rechtsbündig */
.tabelle { width: 100%; border-collapse: collapse; font-size: 15px; }
.tabelle caption { text-align: left; padding-bottom: 10px; }
.tabelle th, .tabelle td { padding: 9px 0; border-bottom: 1px solid var(--linie); vertical-align: baseline; }
.tabelle tr:first-child th, .tabelle tr:first-child td { border-top: 1px solid var(--marke); }
.tabelle th { text-align: left; font-weight: 400; color: var(--leise); padding-right: 16px; }
.tabelle td { text-align: right; font-weight: 500; font-variant-numeric: tabular-nums lining-nums; }
.tabelle tr.preiszeile td { font-weight: 700; color: var(--marke); }
.tabelle td small { font-weight: 400; color: var(--leise); }
.hinweis { font-size: 13px; line-height: 1.5; color: var(--leise); margin-top: 10px; }
/* Grundriss */
.planfeld { background: var(--sand); border-radius: 2px; padding: clamp(18px, 2.4vw, 32px); display: grid; gap: 18px; position: sticky; top: 24px; }
.plankopf { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; }
.plankopf h2 { font-size: 1.0625rem; font-weight: 600; color: var(--marke); }
.segment { display: inline-flex; padding: 3px; gap: 2px; background: var(--papier); border-radius: 999px; box-shadow: inset 0 0 0 1px var(--linie); }
.segment[hidden] { display: none; }
.segment button { font-size: 13px; font-weight: 500; padding: 7px 14px; border-radius: 999px; color: var(--leise); transition: background-color var(--hover) var(--kurve), color var(--hover) var(--kurve); }
.segment button[aria-pressed="true"] { background: var(--marke); color: #fff; }
.segment button:not([aria-pressed="true"]):hover { color: var(--marke); }
.plan { position: relative; align-self: center; }
.plan svg { display: block; width: 100%; height: auto; transition: opacity var(--ui) var(--kurve); }
.plan svg + svg { position: absolute; inset: 0; }
.plan svg[data-aus] { opacity: 0; pointer-events: none; visibility: hidden; transition: opacity var(--ui) var(--kurve), visibility 0s linear var(--ui); }
.plan text { font-family: 'Hanken Grotesk', system-ui, sans-serif; }
.raum { fill: var(--papier); transition: fill var(--ui) var(--kurve); cursor: pointer; }
.raum.an { fill: #d9e2e6; }
.raum.balkon { fill: none; stroke: var(--marke); stroke-width: 1.5; stroke-dasharray: 5 4; }
.raum.balkon.an { fill: #d9e2e6; }
.wand { fill: none; stroke: var(--marke); stroke-linejoin: miter; pointer-events: none; }
.raumtext { pointer-events: none; fill: var(--marke); }
.raumtext .n { font-size: 14px; font-weight: 500; }
.raumtext .f { font-size: 12.5px; fill: #6e685f; font-variant-numeric: tabular-nums; }
.zeiger { position: absolute; left: 0; top: 0; width: 26px; height: 26px; margin: -13px 0 0 -13px; border-radius: 50%; border: 2px solid var(--marke); background: rgba(255, 255, 255, .55); opacity: 0; pointer-events: none; transition: transform 700ms var(--kurve), opacity var(--hover) var(--kurve); }
.vorfuehrung .zeiger { opacity: 1; }
.raumliste { list-style: none; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 36px; align-content: start; font-size: 14.5px; border-top: 1px solid var(--linie); padding-top: 10px; }
.raumliste button { width: 100%; display: flex; justify-content: space-between; gap: 12px; padding: 6px 8px; margin: 0 -8px; width: calc(100% + 16px); text-align: left; border-radius: 3px; transition: background-color var(--hover) var(--kurve); }
.raumliste button span:last-child { font-variant-numeric: tabular-nums; color: var(--leise); }
.raumliste button:hover, .raumliste button.an { background: var(--papier); }
.raumliste button[aria-pressed="true"] { background: var(--marke); color: #fff; }
.raumliste button[aria-pressed="true"] span:last-child { color: rgba(255, 255, 255, .75); }
.raumliste .summe { grid-column: 1 / -1; display: flex; flex-wrap: wrap; justify-content: space-between; gap: 4px 24px; padding: 10px 0 0; margin-top: 8px; border-top: 1px solid var(--marke); font-weight: 600; color: var(--marke); font-variant-numeric: tabular-nums; }
.raumliste .summe span:first-child { font-weight: 400; color: var(--leise); }
.planfuss { font-size: 12.5px; color: var(--leise); }
/* Energieausweis: Pflichtangaben als Text, Skala als Bild mit Beschreibung */
.energie { border-top: 1px solid var(--marke); padding-top: 14px; }
.energie-kopf { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
.energie h2 { font-size: 1.0625rem; font-weight: 600; color: var(--marke); }
.pflicht { display: grid; grid-template-columns: auto 1fr; gap: 6px 18px; margin: 14px 0 22px; font-size: 14.5px; }
.pflicht dt { color: var(--leise); }
.pflicht dd { font-weight: 500; text-align: right; font-variant-numeric: tabular-nums; }
.skala { position: relative; padding-top: 38px; }
.skala-balken { display: grid; grid-template-columns: repeat(9, 1fr); gap: 2px; height: 30px; }
.skala-balken span { display: grid; place-items: center; font-size: 12.5px; font-weight: 700; color: #2f2e2c; background: color-mix(in srgb, var(--k) 50%, var(--grund)); transition: background-color var(--ui) var(--kurve), color var(--ui) var(--kurve), transform var(--ui) var(--kurve); }
/* blasse Klassen: Fläche halb (wie vorher opacity .5), Buchstabe dunkel lesbar; aktive Klasse: volle Farbe, Weiß oder Dunkel je nach Farbe (≥ 4,6:1) */
.skala-balken span.ist { color: #fff; background: var(--k); transform: scaleY(1.2); }
.skala-balken span.ist.dunkel { color: #1a1918; }
.skala-grenzen { display: grid; grid-template-columns: repeat(9, 1fr); font-size: 10.5px; color: var(--leise); margin-top: 5px; font-variant-numeric: tabular-nums; }
.skala-grenzen span { text-align: right; padding-right: 1px; }
.skala-grenzen span:last-child { visibility: hidden; }
.skala-marke { position: absolute; top: 0; left: 0; bottom: 18px; width: 0; transform: translateX(var(--x, 0px)); transition: transform var(--marke-dauer) var(--kurve); }
.skala-marke::after { content: ''; position: absolute; left: -5px; top: 21px; border: 5px solid transparent; border-top: 6px solid var(--marke); border-bottom: 0; }
.skala-marke::before { content: ''; position: absolute; left: -.75px; top: 27px; height: 8px; width: 1.5px; background: var(--marke); opacity: 0; }
.skala-marke b { position: absolute; top: 0; white-space: nowrap; font-size: 13px; font-weight: 700; color: var(--marke); font-variant-numeric: tabular-nums; transform: translateX(var(--zug, -50%)); }
.energie .hinweis { margin-top: 16px; }
.aktionen { display: flex; flex-wrap: wrap; gap: 12px 20px; align-items: center; margin-top: 26px; }
.cta { display: inline-flex; align-items: center; gap: 10px; background: var(--akzent); color: #fff; text-decoration: none; font-weight: 600; font-size: 15px; padding: 14px 22px; border-radius: 2px; transition: background-color var(--hover) var(--kurve); }
.cta:hover { background: #9c5021; }
.textknopf { font-size: 14px; font-weight: 500; color: var(--marke); text-decoration: underline; text-underline-offset: 4px; text-decoration-thickness: 1px; }
.mobil-cta { display: none; }
/* ── Variante Anzeige: Objektkarten mit Pflichtzeile ─ */
.liste { display: none; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: clamp(20px, 2.4vw, 36px); }
.karte { display: grid; gap: 14px; text-decoration: none; }
.karte .bild { aspect-ratio: 4 / 3; overflow: hidden; background: var(--sand); border-radius: 2px; }
.karte .bild svg { display: block; width: 100%; height: 100%; transform: scale(1); transition: transform 1000ms var(--kurve); }
.karte:hover .bild svg, .karte.an .bild svg { transform: scale(1.04); }
.karte .ort { display: flex; justify-content: space-between; gap: 10px; }
.karte h3 { font-size: 1.25rem; font-weight: 600; color: var(--marke); line-height: 1.25; }
.karte .fakten { display: flex; gap: 14px; font-size: 14.5px; font-variant-numeric: tabular-nums; border-top: 1px solid var(--linie); padding-top: 10px; }
.karte .fakten b { margin-left: auto; color: var(--marke); }
.karte .ea { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px 12px; align-items: center; font-size: 12.5px; line-height: 1.45; color: var(--leise); }
.karte .ea .kl { width: 30px; height: 22px; display: grid; place-items: center; font-weight: 700; font-size: 12.5px; color: #fff; background: var(--k); clip-path: polygon(0 0, 78% 0, 100% 50%, 78% 100%, 0 100%); padding-right: 4px; }
.karte .ea .kl.dunkel { color: #1a1918; }
.mini { grid-column: 1 / -1; position: relative; height: 6px; display: grid; grid-template-columns: repeat(9, 1fr); gap: 1px; }
.mini i { background: var(--k); opacity: .4; }
.mini i.ist { opacity: 1; }
.mini em { position: absolute; top: -3px; bottom: -3px; width: 2px; background: var(--marke); left: 0; transform: translateX(var(--x, 0px)); transition: transform var(--marke-dauer) var(--kurve); }
.listen-fuss { grid-column: 1 / -1; font-size: 13px; color: var(--leise); border-top: 1px solid var(--linie); padding-top: 14px; }
[data-v="anzeige"] .raster, [data-v="anzeige"] .preis, [data-v="anzeige"] .mobil-cta { display: none !important; }
[data-v="anzeige"] .liste { display: grid; }
/* ── Variante Fahrzeug: Angebot mit Pflichtangaben nach Pkw-EnVKV ─ */
.fz { display: none; }
[data-v="fahrzeug"] .fz { display: block; }
[data-v="fahrzeug"] .leiste, [data-v="fahrzeug"] .pfad, [data-v="fahrzeug"] .titel, [data-v="fahrzeug"] .raster, [data-v="fahrzeug"] .liste, [data-v="fahrzeug"] .mobil-cta { display: none !important; }
html:has(body[data-v="fahrzeug"]), body[data-v="fahrzeug"] { background: #efedea; }
.fz {
--f-grund: #efedea; --f-flaeche: #d3cdc2; --f-holz: #c3b4a5; --f-text: #1a1a1a; --f-leise: #5a554d; --f-linie: #c9c2b6; --f-signal: #fb6100;
--f-kurve: cubic-bezier(0.2, 0, 0.38, 0.9); --f-ein: cubic-bezier(0, 0, 0.38, 0.9); /* token-tech-praezise */
--f-hover: 150ms; --f-ui: 240ms; --f-marke: 400ms;
color: var(--f-text); font-family: 'Atkinson Hyperlegible Next', system-ui, sans-serif; font-size: 1.0625rem; line-height: 1.55;
}
.fz :focus-visible { outline: 3px solid var(--f-text); outline-offset: 3px; }
.fz-kopf { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 18px 0; border-bottom: 1px solid var(--f-linie); }
.fz-marke { display: flex; align-items: center; gap: 12px; text-decoration: none; }
.fz-marke svg { width: 38px; height: 38px; flex: none; }
.fz-marke b { display: block; font: 700 1.375rem/1 'Special Gothic', sans-serif; font-stretch: 75%; letter-spacing: .04em; text-transform: uppercase; }
.fz-marke span { display: block; font-size: .8125rem; color: var(--f-leise); margin-top: 3px; }
.fz-nav { display: flex; gap: 24px; list-style: none; font-size: .9375rem; }
.fz-nav a { text-decoration: none; }
.fz-nav a[aria-current] { text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 6px; }
.fz-tel { font-weight: 700; text-decoration: none; font-variant-numeric: tabular-nums; white-space: nowrap; }
.fz-pfad { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 6px 20px; padding-top: 16px; font-size: .8125rem; color: var(--f-leise); }
.fz-pfad ol { display: flex; gap: 8px; list-style: none; }
.fz-pfad li + li::before { content: '/'; margin-right: 8px; }
.fz-raster { display: grid; grid-template-columns: minmax(0, 6fr) minmax(0, 5fr); gap: clamp(24px, 4vw, 56px); padding-top: 24px; align-items: start; }
.fz-bild { background: var(--f-flaeche); display: grid; gap: 0; position: sticky; top: 20px; }
.fz-bild svg { display: block; width: 100%; height: auto; }
.fz-bild figcaption { display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding: 10px 14px; font-size: .8125rem; color: var(--f-leise); background: #e3ddd3; }
.fz-label { font: 700 .8125rem/1.2 'Special Gothic', sans-serif; font-stretch: 75%; letter-spacing: .04em; text-transform: uppercase; color: var(--f-leise); }
.fz-info { display: grid; gap: 18px; }
.fz-info h1 { font: 700 clamp(2.25rem, 1.5rem + 3vw, 3.5rem)/1.05 'Special Gothic', sans-serif; letter-spacing: -.01em; text-wrap: balance; }
.fz-unter { color: var(--f-leise); margin-top: -8px; }
.fz-antrieb { display: inline-flex; flex-wrap: wrap; gap: 0; border: 2px solid var(--f-text); width: fit-content; max-width: 100%; }
.fz-antrieb button { padding: 9px 14px; font-weight: 600; font-size: .9375rem; transition: background-color var(--f-hover) var(--f-kurve), color var(--f-hover) var(--f-kurve); }
.fz-antrieb button + button { border-left: 2px solid var(--f-text); }
.fz-antrieb button[aria-pressed="true"] { background: var(--f-text); color: #fff; }
.fz-preis strong { display: block; font: 700 clamp(1.75rem, 1.3rem + 1.4vw, 2.375rem)/1.1 'Special Gothic', sans-serif; font-variant-numeric: tabular-nums; }
.fz-preis small { display: block; font-size: .9375rem; color: var(--f-leise); margin-top: 4px; }
/* Pflichtangaben: gleiche Schriftgröße wie der Angebotstext, volle Textfarbe, kein Kleingedrucktes */
.fz-pflicht { border: 2px solid var(--f-text); background: #fff; padding: 16px 18px 18px; display: grid; gap: 14px; }
.fz-pflicht h2 { font: 700 1.0625rem/1.25 'Special Gothic', sans-serif; display: flex; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
.fz-pflicht h2 small { font: 400 .875rem/1.25 'Atkinson Hyperlegible Next', sans-serif; color: var(--f-leise); }
.fz-werte { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 6px 16px; font-size: 1.0625rem; }
.fz-werte dt { font-weight: 400; }
.fz-werte dd { font-weight: 700; text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.fz-werte .motor { padding-bottom: 6px; border-bottom: 1px solid var(--f-linie); margin-bottom: 2px; }
.fz-werte .neu { animation: fz-neu var(--f-ui) var(--f-ein); }
@keyframes fz-neu { from { opacity: 0; } }
.fz-klassen { display: grid; gap: 10px; }
.fz-klasse { display: grid; gap: 6px; }
.fz-klasse p { font-size: 1rem; }
.fz-klasse p b { font-weight: 700; }
.fz-skala { position: relative; display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 3px; height: 34px; }
.fz-skala span { display: grid; place-items: center; font: 700 1rem/1 'Special Gothic', sans-serif; color: var(--f-text); background: color-mix(in srgb, var(--k) 38%, #fff); transition: background-color var(--f-marke) var(--f-kurve), color var(--f-marke) var(--f-kurve), margin var(--f-marke) var(--f-kurve); }
.fz-skala span.ist { background: var(--k); color: var(--t); margin-block: -5px; box-shadow: 0 0 0 2px var(--f-text); font-size: 1.25rem; }
.fz-hinweis { font-size: .875rem; color: var(--f-leise); }
.fz-cta { display: inline-flex; align-items: center; gap: 10px; min-height: 52px; padding: 0 22px; background: var(--f-signal); color: var(--f-text); font-weight: 700; text-decoration: none; transition: background-color var(--f-hover) var(--f-kurve); }
.fz-cta:hover { background: #ff7a26; }
.fz-aktionen { display: flex; flex-wrap: wrap; gap: 12px 20px; align-items: center; }
.fz-aktionen a:not(.fz-cta) { font-weight: 600; text-underline-offset: 4px; }
.fz-daten { margin-top: clamp(32px, 6vh, 56px); display: grid; grid-template-columns: minmax(0, 6fr) minmax(0, 5fr); gap: clamp(24px, 4vw, 56px); }
.fz-daten h2 { font: 700 1.5rem/1.2 'Special Gothic', sans-serif; margin-bottom: 10px; }
.fz-tab { width: 100%; border-collapse: collapse; font-size: 1rem; }
.fz-tab th, .fz-tab td { padding: 9px 0; border-bottom: 1px solid var(--f-linie); text-align: left; vertical-align: baseline; }
.fz-tab th { font-weight: 400; color: var(--f-leise); padding-right: 16px; }
.fz-tab td { text-align: right; font-weight: 600; font-variant-numeric: tabular-nums; }
.fz-werkstatt { background: var(--f-flaeche); padding: 20px 22px; display: grid; gap: 10px; align-content: start; }
.fz-werkstatt ul { list-style: none; display: grid; gap: 6px; }
.fz-werkstatt li::before { content: '– '; }
.vorschau .fz-pfad, .vorschau .fz-daten, .vorschau .fz-nav, .vorschau .fz-tel, .vorschau .fz-aktionen, .vorschau .fz-hinweis, .vorschau .fz-bild figcaption { display: none; }
.vorschau .fz-info { gap: 12px; }
.vorschau .fz-info h1 { font-size: 2.25rem; }
@media (max-width: 860px) {
.fz-raster, .fz-daten { grid-template-columns: minmax(0, 1fr); }
.fz-bild { position: static; }
.fz-nav, .fz-tel { display: none; }
}
@media (max-width: 560px) {
.fz-werte { grid-template-columns: minmax(0, 1fr); gap: 0; }
.fz-werte dd { text-align: left; white-space: normal; padding-bottom: 8px; }
.fz-werte dd.motor { margin-bottom: 8px; }
.fz-werte dt.motor { border-bottom: 0; padding-bottom: 0; margin-bottom: 0; }
}
@media (max-width: 420px) { .fz-antrieb button { padding: 9px 10px; font-size: .875rem; } }
@media print { .fz-antrieb, .fz-aktionen { display: none !important; } }
/* ── Pult der Demo ──────────────────────────────── */
.pult { position: fixed; left: 16px; bottom: 16px; z-index: 20; display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; padding: 8px 10px 8px 14px; background: rgba(23, 57, 79, .94); color: #e9eef1; border-radius: 4px; box-shadow: 0 10px 30px -12px rgba(23, 57, 79, .55); max-width: calc(100vw - 32px); }
.pult, .pult button { font: 500 10.5px/1 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; }
.pult .gruppe { display: flex; gap: 2px; }
.pult button { color: rgba(233, 238, 241, .7); padding: 7px 9px; border-radius: 3px; transition: background-color 150ms var(--kurve), color 150ms var(--kurve); }
.pult button[aria-pressed="true"] { background: #e9eef1; color: var(--marke); }
.pult button:focus-visible { outline: 2px solid #e9eef1; outline-offset: 2px; }
.pult .werte { text-transform: none; letter-spacing: .02em; color: rgba(233, 238, 241, .68); }
/* Reduzierte Bewegung: alles sofort */
:root.reduziert *, :root.reduziert *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; }
:root.reduziert .karte:hover .bild svg, :root.reduziert .karte.an .bild svg { transform: none; }
/* Vorschau: nur die Bühne */
.vorschau body { overflow: hidden; }
.vorschau .seite { padding: 0 32px; }
.vorschau .leiste { padding: 14px 0; }
.vorschau .nav, .vorschau .pfad, .vorschau .aktionen, .vorschau .planfuss, .vorschau .hinweis, .vorschau .tel { display: none; }
.vorschau .titel { padding: 16px 0 18px; }
.vorschau .titel h1 { font-size: 40px; }
.vorschau .titel .unter { margin-top: 6px; font-size: 15px; }
.vorschau .raster { grid-template-columns: minmax(0, 6.4fr) minmax(0, 5fr); gap: 18px 28px; }
.vorschau .planfeld { padding: 18px 20px; gap: 10px; }
.vorschau .raumliste { display: none; }
.vorschau .tabelle { font-size: 13px; }
.vorschau .tabelle th, .vorschau .tabelle td { padding: 5px 0; }
.vorschau .tabelle tr.nebensache { display: none; }
.vorschau .pflicht { margin: 8px 0 12px; font-size: 13px; gap: 3px 14px; }
.vorschau .energie { padding-top: 10px; }
.vorschau .planfeld { position: static; }
/* Telefon */
@media (max-width: 860px) {
.nav, .tel { display: none; }
.raster { grid-template-columns: 1fr; grid-template-areas: "daten" "plan" "energie"; }
.planfeld { position: static; }
.titel { grid-template-columns: 1fr; }
.preis { text-align: left; }
.liste { grid-template-columns: 1fr; }
.aktionen .cta { display: none; }
.mobil-cta { display: flex; position: fixed; left: 0; right: 0; bottom: 0; z-index: 15; gap: 10px; padding: 12px 16px calc(12px + env(safe-area-inset-bottom)); background: rgba(251, 248, 242, .96); border-top: 1px solid var(--linie); }
.mobil-cta .cta { flex: 1; justify-content: center; }
.mobil-cta .anruf { display: grid; place-items: center; width: 50px; border: 1px solid var(--marke); color: var(--marke); border-radius: 2px; }
.pult { bottom: 84px; }
.seite { padding-bottom: 120px; }
}
@media (max-width: 560px) {
.raumliste { column-gap: 18px; font-size: 14px; }
.planfeld { padding: 16px 14px; margin: 0 -4px; }
.wortmarke span { display: none; }
.tabelle { font-size: 14.5px; }
}
@media (hover: none) {
.raumliste button { padding-block: 10px; } /* große Trefferflächen */
.segment button { padding: 10px 16px; }
}
@media print {
.pult, .nav, .aktionen, .mobil-cta, .segment, .zeiger, fundus-pult-knopf { display: none !important; }
html, body { background: #fff; }
.raster { grid-template-columns: 1fr 1fr; }
.plan svg[data-aus] { opacity: 1; visibility: visible; position: static; margin-top: 16px; }
.planfeld { break-inside: avoid; }
}
@media (forced-colors: active) { button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; } }
</style>
</head>
<body>
<div class="seite" data-seite>
<header class="leiste">
<a class="wortmarke" href="#start">
<svg viewBox="0 0 34 34" aria-hidden="true"><rect x=".75" y=".75" width="32.5" height="32.5" fill="none" stroke="#17394f" stroke-width="1.5"/><path d="M8 26V14l9-6 9 6v12" fill="none" stroke="#17394f" stroke-width="1.5"/><path d="M14 26v-7h6v7" fill="none" stroke="#c4682c" stroke-width="1.5"/></svg>
<div><b>KAMMERMEIER</b><span>Immobilien · Landshut</span></div>
</a>
<nav aria-label="Hauptnavigation"><ul class="nav"><li><a href="#kaufen" data-nav="kauf" aria-current="page">Kaufen</a></li><li><a href="#mieten" data-nav="miete">Mieten</a></li><li><a href="#verkaufen">Verkaufen</a></li><li><a href="#bewerten">Immobilie bewerten</a></li></ul></nav>
<a class="tel ziffern" href="#tel">0871 965 40 20</a>
</header>
<div class="pfad">
<nav aria-label="Brotkrumen"><ol data-pfad><li>Kaufen</li><li>Landshut</li><li>Altstadt</li></ol></nav>
<span class="label ziffern" data-objektnr>Objekt-Nr. KI-26-114</span>
</div>
<div class="titel">
<div>
<h1 data-h1>Stadthaus am Hofberg</h1>
<p class="unter" data-unter>Einfamilienhaus · Landshut, Altstadt</p>
</div>
<p class="preis"><span class="label" data-preislabel>Kaufpreis</span><strong class="ziffern" data-preis>985.000 €</strong><small data-preisnote>zzgl. 3,57 % Käuferprovision inkl. MwSt.</small></p>
</div>
<main class="raster" data-raster>
<section class="daten" aria-labelledby="daten-titel">
<table class="tabelle">
<caption><h2 id="daten-titel" class="label">Eckdaten</h2></caption>
<tbody data-tabelle></tbody>
</table>
<p class="hinweis" data-provision></p>
</section>
<section class="planfeld" aria-labelledby="plan-titel">
<div class="plankopf">
<h2 id="plan-titel">Grundriss</h2>
<div class="segment" role="group" aria-label="Geschoss" data-geschosse></div>
</div>
<div class="plan" data-plan><span class="zeiger" data-zeiger aria-hidden="true"></span></div>
<ul class="raumliste" data-raumliste aria-label="Räume, wählen zeigt den Raum im Grundriss"></ul>
<p class="planfuss" data-planfuss>Flächen nach Wohnflächenverordnung, gerundet. Grundriss nicht maßstabsgetreu, Möblierung nicht dargestellt.</p>
</section>
<section class="energie" aria-labelledby="energie-titel">
<div class="energie-kopf"><h2 id="energie-titel">Energieausweis</h2><span class="label">Angaben laut Ausweis</span></div>
<dl class="pflicht" data-pflicht></dl>
<div class="skala" role="img" data-skala>
<span class="skala-marke" data-marke aria-hidden="true"><b data-markenwert>0</b></span>
<div class="skala-balken" data-balken aria-hidden="true"></div>
<div class="skala-grenzen" data-grenzen aria-hidden="true"></div>
</div>
<p class="hinweis" data-ausweisnote></p>
<div class="aktionen">
<a class="cta" href="#anfrage">Besichtigung anfragen <span aria-hidden="true">→</span></a>
<button type="button" class="textknopf" data-drucken>Exposé drucken</button>
</div>
</section>
</main>
<section class="liste" aria-label="Aktuelle Angebote" data-liste></section>
<!-- Variante Fahrzeug: Neuwagen-Angebot eines Autohauses mit Pflichtangaben nach Pkw-EnVKV -->
<section class="fz" aria-labelledby="fz-titel" data-fz>
<header class="fz-kopf">
<a class="fz-marke" href="#start"><svg viewBox="0 0 38 38" aria-hidden="true"><rect width="38" height="38" fill="#1a1a1a"/><path d="M7 24h24M9 24l3-7h14l3 7" fill="none" stroke="#efedea" stroke-width="2"/><circle cx="13" cy="25" r="3" fill="#fb6100"/><circle cx="25" cy="25" r="3" fill="#fb6100"/></svg><div><b>Wendlinger</b><span>Autohaus und Werkstatt · Straubing</span></div></a>
<nav aria-label="Hauptnavigation Autohaus"><ul class="fz-nav"><li><a href="#neu" aria-current="page">Neuwagen</a></li><li><a href="#gebraucht">Gebrauchte</a></li><li><a href="#werkstatt">Werkstatt</a></li><li><a href="#termin">Termin</a></li></ul></nav>
<a class="fz-tel" href="tel:+499421784300">09421 78 43 00</a>
</header>
<div class="fz-pfad"><nav aria-label="Brotkrumen Fahrzeug"><ol><li>Neuwagen</li><li>Kompaktwagen</li><li>Norda Kesto</li></ol></nav><span>Angebot Nr. WS-2611-07</span></div>
<div class="fz-raster">
<figure class="fz-bild">
<svg viewBox="0 0 640 400" role="img" aria-label="Seitenansicht des Kompaktwagen in Kieselgrau (Zeichnung)">
<rect width="640" height="400" fill="#d3cdc2"/><rect y="300" width="640" height="100" fill="#c3b4a5"/><path d="M0 300h640" stroke="#a89a8b" stroke-width="2"/>
<g data-fz-auto>
<path d="M96 268c0-22 10-36 34-42l64-14 58-48c14-11 30-16 48-16h112c22 0 38 7 52 21l40 41 52 10c22 4 34 18 34 40v22c0 7-5 12-12 12H108c-7 0-12-5-12-12Z" fill="#8f949a"/>
<path d="M262 168c10-8 22-12 36-12h54v58H222Z" fill="#2c3238"/><path d="M366 156h52c14 0 26 5 36 15l38 39v4H366Z" fill="#2c3238"/>
<path d="M120 250h420" stroke="#7a7f85" stroke-width="3"/><path d="M360 160v120" stroke="#7a7f85" stroke-width="2"/>
<rect x="520" y="236" width="30" height="10" rx="3" fill="#f2efe8"/><rect x="98" y="238" width="22" height="10" rx="3" fill="#c62a26"/>
<g data-fz-stecker hidden><rect x="128" y="214" width="26" height="18" rx="3" fill="#1a1a1a"/><path d="M134 220v6m6-6v6m6-6v6" stroke="#fb6100" stroke-width="2"/></g>
</g>
<circle cx="190" cy="292" r="44" fill="#1a1a1a"/><circle cx="190" cy="292" r="22" fill="#b8bcc0"/><circle cx="470" cy="292" r="44" fill="#1a1a1a"/><circle cx="470" cy="292" r="22" fill="#b8bcc0"/>
</svg>
<figcaption><span>Kieselgrau Metallic · 18-Zoll-Felgen</span><span>Zeichnung, kein Foto</span></figcaption>
</figure>
<div class="fz-info">
<p class="fz-label">Neuwagen · sofort verfügbar</p>
<h1 id="fz-titel">Norda Kesto <span data-fz-modell>1.5 Hybrid</span></h1>
<p class="fz-unter" data-fz-unter>Kompaktwagen · Vollhybrid · Automatik</p>
<div class="fz-antrieb" role="group" aria-label="Antrieb">
<button type="button" data-antrieb="hybrid" aria-pressed="true">Hybrid</button>
<button type="button" data-antrieb="phev" aria-pressed="false">Plug-in-Hybrid</button>
<button type="button" data-antrieb="elektro" aria-pressed="false">Elektro</button>
</div>
<p class="fz-preis"><strong data-fz-preis>32.490 €</strong><small>Hauspreis inkl. MwSt. und Überführung</small></p>
<section class="fz-pflicht" aria-labelledby="fz-pflicht-titel">
<h2 id="fz-pflicht-titel"><span>Leistung, Verbrauch und CO<sub>2</sub></span> <small>Werte nach WLTP</small></h2>
<dl class="fz-werte" data-fz-werte></dl>
<div class="fz-klassen" data-fz-klassen></div>
</section>
<div class="fz-aktionen">
<a class="fz-cta" href="#probefahrt">Probefahrt vereinbaren <span aria-hidden="true">→</span></a>
<a href="#inzahlungnahme">Inzahlungnahme anfragen</a>
</div>
</div>
</div>
<div class="fz-daten">
<section aria-labelledby="fz-daten-titel">
<h2 id="fz-daten-titel">Technische Daten</h2>
<table class="fz-tab"><tbody data-fz-tab></tbody></table>
</section>
<aside class="fz-werkstatt" aria-labelledby="fz-werkstatt-titel">
<h2 id="fz-werkstatt-titel">Aus unserer Werkstatt</h2>
<ul><li>Erste Inspektion nach 12 Monaten zum Festpreis</li><li>Hol- und Bringdienst im Stadtgebiet</li><li>Winterräder auf Wunsch montiert</li></ul>
</aside>
</div>
</section>
</div>
<div class="mobil-cta"><a class="cta" href="#anfrage">Besichtigung anfragen</a><a class="anruf" href="#tel" aria-label="Anrufen: 0871 965 40 20"><svg width="18" height="18" viewBox="0 0 24 24" aria-hidden="true"><path d="M6.6 10.8a15 15 0 0 0 6.6 6.6l2.2-2.2c.3-.3.7-.4 1-.2 1.1.4 2.3.6 3.6.6.6 0 1 .4 1 1V20c0 .6-.4 1-1 1A17 17 0 0 1 3 4c0-.6.4-1 1-1h3.5c.6 0 1 .4 1 1 0 1.3.2 2.5.6 3.6.1.3 0 .7-.2 1z" fill="currentColor"/></svg></a></div>
<div class="pult">
<span>Variante</span>
<div class="gruppe" role="group" aria-label="Variante der Demo">
<button type="button" data-variante="kauf" aria-pressed="true">Kauf</button>
<button type="button" data-variante="miete" aria-pressed="false">Miete</button>
<button type="button" data-variante="anzeige" aria-pressed="false">Anzeige</button>
<button type="button" data-variante="fahrzeug" aria-pressed="false">Fahrzeug</button>
</div>
<span class="werte" data-werte>easeOutQuart · UI 400 ms · Marke 1000 ms</span>
</div>
<script>
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const $ = s => document.querySelector(s);
const zahl = (n, d = 0) => new Intl.NumberFormat('de-DE', { minimumFractionDigits: d, maximumFractionDigits: d }).format(n);
const euro = n => zahl(n) + ' €';
const NS = 'http://www.w3.org/2000/svg';
/* Klassen nach Anlage 10 GModG (Wohngebäude), Endenergie in kWh/(m²·a); H endet in der Grafik bei 300 */
const KLASSEN = [
['A+', 0, 30, '#3f7a55'], ['A', 30, 50, '#5d904f', 1], ['B', 50, 75, '#89a64a', 1], ['C', 75, 100, '#b5b54a', 1], ['D', 100, 130, '#d7b544', 1],
['E', 130, 160, '#d9953f', 1], ['F', 160, 200, '#cc773b', 1], ['G', 200, 250, '#bb5637'], ['H', 250, 300, '#a23f35'],
];
const klasseVon = w => KLASSEN.find(k => w <= k[2]) || KLASSEN[8];
const anteil = w => { const v = Math.min(w, 300), i = KLASSEN.indexOf(klasseVon(v)), k = KLASSEN[i]; return (i + (v - k[1]) / (k[2] - k[1])) / 9; };
/* Räume in Metern: r = [x, y, b, t] oder p = Polygon; l = Lage der Beschriftung */
const KAUF_EG = { name: 'Erdgeschoss', kurz: 'EG', b: 11.4, t: 8.2, raeume: [
{ id: 'kueche', n: 'Küche', r: [0, 0, 3.6, 3.6] },
{ id: 'diele', n: 'Diele', r: [3.6, 0, 2.4, 4.6], l: [5.35, 2.3], quer: 1 },
{ id: 'wc', n: 'Gäste-WC', kn: 'WC', r: [6, 0, 1.8, 2.2], l: [6.9, .78] },
{ id: 'hwr', n: 'Hauswirtschaft', kn: 'HWR', r: [6, 2.2, 1.8, 2.4], l: [6.95, 3.0] },
{ id: 'arbeiten', n: 'Arbeiten', r: [7.8, 0, 3.6, 3.8] },
{ id: 'wohnen', n: 'Wohnen & Essen', p: [[0, 3.6], [3.6, 3.6], [3.6, 4.6], [7.8, 4.6], [7.8, 3.8], [11.4, 3.8], [11.4, 8.2], [0, 8.2]], l: [5.7, 6.5] },
], tueren: [
{ x: 4.85, y: 0, o: 'h', w: 1.0, s: 1, haupt: 1 }, { x: 6, y: 1.3, o: 'v', w: .8, s: 1 }, { x: 6, y: 3.7, o: 'v', w: .8, s: 1 },
{ x: 8.3, y: 3.8, o: 'h', w: .9, s: -1 },
], offen: [{ x: .6, y: 3.6, o: 'h', w: 2.4 }, { x: 4.75, y: 4.6, o: 'h', w: 1.05 }],
fenster: [
{ x: .9, y: 0, o: 'h', w: 1.6 }, { x: 6.5, y: 0, o: 'h', w: .7 }, { x: 8.8, y: 0, o: 'h', w: 1.6 },
{ x: .8, y: 8.2, o: 'h', w: 2.2 }, { x: 4.4, y: 8.2, o: 'h', w: 2.6 }, { x: 8.2, y: 8.2, o: 'h', w: 2.2 },
{ x: 0, y: 1.0, o: 'v', w: 1.4 }, { x: 0, y: 5.2, o: 'v', w: 2.0 }, { x: 11.4, y: 1.0, o: 'v', w: 1.6 }, { x: 11.4, y: 5.0, o: 'v', w: 2.4 },
], treppe: [3.72, 1.4, .9, 3.0, 'auf'] };
const KAUF_OG = { name: 'Obergeschoss', kurz: 'OG', b: 11.4, t: 8.2, raeume: [
{ id: 'kind1', n: 'Kind 1', r: [0, 0, 3.6, 4.6] },
{ id: 'flur', n: 'Flur', p: [[3.6, 0], [6, 0], [6, 3.4], [11.4, 3.4], [11.4, 4.6], [3.6, 4.6]], l: [8.3, 4.05], kurzzeile: 1 },
{ id: 'bad', n: 'Bad', r: [6, 0, 2.6, 3.4] },
{ id: 'kind2', n: 'Kind 2', r: [8.6, 0, 2.8, 3.4] },
{ id: 'schlafen', n: 'Schlafen', r: [0, 4.6, 6.4, 3.6] },
{ id: 'dusche', n: 'Duschbad', r: [6.4, 4.6, 2, 3.6] },
{ id: 'galerie', n: 'Arbeiten', r: [8.4, 4.6, 3, 3.6] },
], tueren: [
{ x: 3.6, y: .4, o: 'v', w: .8, s: -1 }, { x: 6.4, y: 3.4, o: 'h', w: .8, s: -1 }, { x: 9.4, y: 3.4, o: 'h', w: .85, s: -1 },
{ x: 4.8, y: 4.6, o: 'h', w: .9, s: 1 }, { x: 6.9, y: 4.6, o: 'h', w: .75, s: 1 }, { x: 9.5, y: 4.6, o: 'h', w: .85, s: 1 },
], offen: [], fenster: [
{ x: .9, y: 0, o: 'h', w: 1.6 }, { x: 6.8, y: 0, o: 'h', w: 1.0 }, { x: 9.3, y: 0, o: 'h', w: 1.4 },
{ x: 1.0, y: 8.2, o: 'h', w: 1.6 }, { x: 3.6, y: 8.2, o: 'h', w: 1.6 }, { x: 7.0, y: 8.2, o: 'h', w: .8 }, { x: 9.2, y: 8.2, o: 'h', w: 1.4 },
{ x: 0, y: 1.4, o: 'v', w: 1.6 }, { x: 0, y: 5.6, o: 'v', w: 1.6 }, { x: 11.4, y: .8, o: 'v', w: 1.6 }, { x: 11.4, y: 5.4, o: 'v', w: 1.8 },
], treppe: [3.72, 1.4, .9, 3.0, 'ab'] };
const MIETE = { name: '2. Obergeschoss', kurz: '2. OG', b: 12.4, t: 8.0, unten: 1.6, raeume: [
{ id: 'kueche', n: 'Küche', r: [0, 0, 4.2, 3.0] },
{ id: 'bad', n: 'Bad', r: [0, 3, 4.2, 2.0] },
{ id: 'flur', n: 'Flur', r: [4.2, 0, 1.8, 5.0], quer: 1 },
{ id: 'wohnen', n: 'Wohnen', r: [6, 0, 6.4, 4.4] },
{ id: 'schlafen', n: 'Schlafen', r: [0, 5, 6, 3.0] },
{ id: 'arbeiten', n: 'Arbeiten', r: [6, 4.4, 3.6, 3.6] },
{ id: 'kind', n: 'Kind', r: [9.6, 4.4, 2.8, 3.6] },
{ id: 'balkon', n: 'Balkon', kn: 'Balkon (½)', r: [6.8, 8.0, 3.8, 1.4], balkon: 1, l: [8.7, 8.82] },
], tueren: [
{ x: 4.6, y: 0, o: 'h', w: 1.0, s: 1, haupt: 1 }, { x: 4.2, y: 1.0, o: 'v', w: .85, s: -1 }, { x: 4.2, y: 3.6, o: 'v', w: .8, s: -1 },
{ x: 6, y: 1.3, o: 'v', w: .95, s: 1 }, { x: 4.6, y: 5.0, o: 'h', w: .85, s: 1 }, { x: 6.6, y: 4.4, o: 'h', w: .9, s: 1 },
{ x: 10.4, y: 4.4, o: 'h', w: .85, s: 1 }, { x: 8.6, y: 8.0, o: 'h', w: .85, s: -1 },
], offen: [], fenster: [
{ x: 1.0, y: 0, o: 'h', w: 1.4 }, { x: 6.8, y: 0, o: 'h', w: 1.4 }, { x: 9.6, y: 0, o: 'h', w: 1.4 },
{ x: 1.2, y: 8, o: 'h', w: 1.2 }, { x: 3.4, y: 8, o: 'h', w: 1.2 }, { x: 7.0, y: 8, o: 'h', w: 1.2 }, { x: 10.4, y: 8, o: 'h', w: 1.2 },
{ x: 12.4, y: 1.2, o: 'v', w: 1.8 }, { x: 0, y: 3.6, o: 'v', w: .8 },
] };
/* Netto-Fläche: Rechteck abzüglich halber Wandstärke ringsum; Polygon pauschal 3,5 % Abzug; Balkon zur Hälfte */
function flaeche(r) {
if (r.r) { const a = (r.r[2] - .12) * (r.r[3] - .12); return r.balkon ? a / 2 : a; }
let a = 0; r.p.forEach((p, i) => { const q = r.p[(i + 1) % r.p.length]; a += p[0] * q[1] - q[0] * p[1]; });
return Math.abs(a / 2) * .965;
}
const rund = a => Math.round(a * 10) / 10;
const OBJEKTE = {
kauf: {
nr: 'KI-26-114', pfad: ['Kaufen', 'Landshut', 'Altstadt'], h1: 'Stadthaus am Hofberg', unter: 'Einfamilienhaus · Landshut, Altstadt',
preislabel: 'Kaufpreis', preis: 985000, preisnote: 'zzgl. 3,57 % Käuferprovision inkl. MwSt.',
geschosse: [KAUF_EG, KAUF_OG],
zeilen: wf => [
['Objektart', 'Einfamilienhaus, Stadthaus'],
['Wohnfläche', 'ca. ' + zahl(Math.round(wf)) + ' m²'],
['Grundstück', '412 m²', 'nebensache'],
['Zimmer', '6'],
['Schlafzimmer', '3', 'nebensache'],
['Badezimmer', '2 <small>+ Gäste-WC</small>', 'nebensache'],
['Baujahr', '1928 <small>· modernisiert 2019</small>'],
['Stellplatz', 'Garage', 'nebensache'],
['Bezugsfrei ab', '01.03.2027', 'nebensache'],
['Preis je m²', euro(Math.round(985000 / wf / 10) * 10), 'nebensache'],
['Käuferprovision', '3,57 % <small>inkl. MwSt.</small>'],
['Kaufpreis', euro(985000), 'preiszeile'],
],
provision: 'Die Käuferprovision beträgt 3,57 % des Kaufpreises inklusive 19 % Mehrwertsteuer und ist mit dem notariellen Kaufvertrag fällig. Die Verkäuferseite zahlt eine Provision in gleicher Höhe.',
ea: { art: 'Bedarfsausweis', kennwort: 'Endenergiebedarf', wert: 118.4, traeger: 'Strom (Luft-Wasser-Wärmepumpe)', baujahr: '1928',
note: 'Ausgestellt am 14.03.2024, gültig bis 13.03.2034. Der Ausweis liegt bei der Besichtigung vor.' },
},
miete: {
nr: 'KI-26-131', pfad: ['Mieten', 'Landshut', 'Neustadt'], h1: 'Altbauwohnung in der Neustadt', unter: 'Etagenwohnung, 2. Obergeschoss · Landshut, Neustadt',
preislabel: 'Kaltmiete', preis: 1290, preisnote: 'zzgl. 350 € Nebenkosten und Heizung · für Mieter provisionsfrei',
geschosse: [MIETE],
zeilen: wf => [
['Objektart', 'Etagenwohnung, Altbau'],
['Wohnfläche', 'ca. ' + zahl(Math.round(wf)) + ' m²'],
['Zimmer', '4'],
['Etage', '2. OG von 3 <small>· kein Aufzug</small>', 'nebensache'],
['Balkon', '4,7 m² <small>· Süd, zum Hof</small>', 'nebensache'],
['Baujahr', '1908'],
['Bezugsfrei ab', '01.01.2027', 'nebensache'],
['Kaltmiete', euro(1290)],
['Nebenkosten', euro(230) + ' <small>Vorauszahlung</small>', 'nebensache'],
['Heizkosten', euro(120) + ' <small>Vorauszahlung</small>', 'nebensache'],
['Kaution', euro(3870) + ' <small>· 3 Kaltmieten</small>'],
['Warmmiete', euro(1640), 'preiszeile'],
],
provision: 'Für Mieterinnen und Mieter provisionsfrei. Den Auftrag hat die Eigentümerseite erteilt.',
ea: { art: 'Verbrauchsausweis', kennwort: 'Endenergieverbrauch', wert: 142.0, traeger: 'Fernwärme', baujahr: '1908', ww: 1,
note: 'Kennwert mit Warmwasser. Ausgestellt am 02.09.2022, gültig bis 01.09.2032. Der Ausweis liegt bei der Besichtigung vor.' },
},
};
const ANZEIGEN = [
{ titel: 'Stadthaus am Hofberg', ort: 'Landshut, Altstadt', zi: 6, wf: 176, preis: '985.000 €', ea: ['Bedarfsausweis', 118.4, 'Strom (Wärmepumpe)', 1928], bild: 'stadthaus' },
{ titel: 'Altbauwohnung in der Neustadt', ort: 'Landshut, Neustadt', zi: 4, wf: 95, preis: '1.290 € kalt', ea: ['Verbrauchsausweis', 142.0, 'Fernwärme', 1908], bild: 'altbau' },
{ titel: 'Bungalow am Ortsrand', ort: 'Ergolding', zi: 4, wf: 131, preis: '749.000 €', ea: ['Bedarfsausweis', 41.2, 'Strom (Wärmepumpe)', 2021], bild: 'bungalow' },
];
/* ── Grundriss als SVG ───────────────────────── */
const S = 40, RAND = 1.1;
function el(name, attr, eltern) { const e = document.createElementNS(NS, name); for (const k in attr) e.setAttribute(k, attr[k]); if (eltern) eltern.appendChild(e); return e; }
function pfadVon(r) { if (r.r) { const [x, y, b, t] = r.r; return `M${x * S} ${y * S}h${b * S}v${t * S}h${-b * S}z`; } return 'M' + r.p.map(p => `${p[0] * S} ${p[1] * S}`).join('L') + 'z'; }
function mitte(r) { if (r.l) return r.l; if (r.r) return [r.r[0] + r.r[2] / 2, r.r[1] + r.r[3] / 2]; const xs = r.p.map(p => p[0]), ys = r.p.map(p => p[1]); return [(Math.min(...xs) + Math.max(...xs)) / 2, (Math.min(...ys) + Math.max(...ys)) / 2]; }
function zeichne(g) {
const unten = g.unten || 0;
const vb = [-RAND * S, -RAND * S, (g.b + 2 * RAND) * S, (g.t + 2 * RAND + unten) * S];
const svg = el('svg', { viewBox: vb.join(' '), role: 'img', 'aria-label': `Grundriss ${g.name}: ` + g.raeume.map(r => `${r.n} ${zahl(rund(flaeche(r)), 1)} Quadratmeter`).join(', ') });
svg.dataset.geschoss = g.kurz;
el('rect', { x: 0, y: 0, width: g.b * S, height: g.t * S, fill: '#fffdf9' }, svg);
g.raeume.forEach(r => { const p = el('path', { d: pfadVon(r), class: 'raum' + (r.balkon ? ' balkon' : '') }, svg); p.dataset.raum = r.id; });
g.raeume.filter(r => !r.balkon).forEach(r => el('path', { d: pfadVon(r), class: 'wand', 'stroke-width': 4.5 }, svg));
el('rect', { x: 0, y: 0, width: g.b * S, height: g.t * S, class: 'wand', 'stroke-width': 13 }, svg);
// Fenster: Lücke in der Außenwand mit Doppellinie
g.fenster.forEach(f => {
const h = f.o === 'h', x = f.x * S, y = f.y * S, w = f.w * S;
el('rect', h ? { x, y: y - 6.5, width: w, height: 13, fill: '#fffdf9' } : { x: x - 6.5, y, width: 13, height: w, fill: '#fffdf9' }, svg);
el('path', { d: h ? `M${x} ${y - 6.5}h${w}M${x} ${y}h${w}M${x} ${y + 6.5}h${w}` : `M${x - 6.5} ${y}v${w}M${x} ${y}v${w}M${x + 6.5} ${y}v${w}`, stroke: '#17394f', 'stroke-width': 1.1, fill: 'none' }, svg);
el('path', { d: h ? `M${x} ${y - 6.5}v13M${x + w} ${y - 6.5}v13` : `M${x - 6.5} ${y}h13M${x - 6.5} ${y + w}h13`, stroke: '#17394f', 'stroke-width': 1.6 }, svg);
});
// Offene Durchgänge
(g.offen || []).forEach(f => { const h = f.o === 'h', x = f.x * S, y = f.y * S, w = f.w * S;
el('rect', h ? { x, y: y - 4, width: w, height: 8, fill: '#fffdf9', class: 'luecke' } : { x: x - 4, y, width: 8, height: w, fill: '#fffdf9', class: 'luecke' }, svg); });
// Türen: Lücke, Blatt und Schwenkbogen
g.tueren.forEach(t => {
const h = t.o === 'h', x = t.x * S, y = t.y * S, w = t.w * S, d = t.haupt ? 13 : 5;
el('rect', h ? { x, y: y - d / 2 - .5, width: w, height: d + 1, fill: '#fffdf9', class: 'luecke' } : { x: x - d / 2 - .5, y, width: d + 1, height: w, fill: '#fffdf9', class: 'luecke' }, svg);
const L = h ? [x, y + t.s * w] : [x + t.s * w, y], C = h ? [x + w, y] : [x, y + w];
const kreuz = (L[0] - x) * (C[1] - y) - (L[1] - y) * (C[0] - x);
el('path', { d: `M${x} ${y}L${L[0]} ${L[1]}`, stroke: '#17394f', 'stroke-width': 1.6, fill: 'none' }, svg);
el('path', { d: `M${L[0]} ${L[1]}A${w} ${w} 0 0 ${kreuz > 0 ? 1 : 0} ${C[0]} ${C[1]}`, stroke: '#17394f', 'stroke-width': .9, fill: 'none', 'stroke-dasharray': '3 3' }, svg);
});
// Treppe
if (g.treppe) {
const [tx, ty, tb, tl, richtung] = g.treppe, gr = el('g', { stroke: '#17394f', 'stroke-width': 1, fill: 'none', 'pointer-events': 'none' }, svg);
el('rect', { x: tx * S, y: ty * S, width: tb * S, height: tl * S }, gr);
for (let i = 1; i < 12; i++) el('path', { d: `M${tx * S} ${(ty + i * tl / 12) * S}h${tb * S}` }, gr);
const mx = (tx + tb / 2) * S;
el('path', { d: richtung === 'auf' ? `M${mx} ${(ty + tl - .2) * S}V${(ty + .35) * S}m-4 7l4-7 4 7` : `M${mx} ${(ty + .2) * S}V${(ty + tl - .35) * S}m-4 -7l4 7 4-7`, 'stroke-width': 1.4 }, gr);
}
// Beschriftung
g.raeume.forEach(r => {
const [cx, cy] = mitte(r), tg = el('text', { class: 'raumtext', 'text-anchor': 'middle', x: cx * S, y: cy * S }, svg);
const klein = (r.r && Math.min(r.r[2], r.r[3]) < 2.3) || r.kurzzeile;
if (r.quer) tg.setAttribute('transform', `rotate(-90 ${cx * S} ${cy * S})`);
const n = el('tspan', { class: 'n', x: cx * S, dy: klein ? 0 : -3 }, tg); n.textContent = (klein && r.kn) ? r.kn : r.n;
const f = el('tspan', { class: 'f', x: cx * S, dy: 15 }, tg); f.textContent = zahl(rund(flaeche(r)), 1) + ' m²';
if (r.kurzzeile) { f.setAttribute('dy', 0); f.setAttribute('x', (cx + 1.0) * S); n.setAttribute('x', (cx - .55) * S); }
});
// Nordpfeil und Maßstab
const nx = (g.b + RAND * .5) * S, ny = -RAND * .55 * S;
const np = el('g', { transform: `translate(${nx} ${ny})`, fill: '#17394f' }, svg);
el('path', { d: 'M0 -14 L6 6 L0 2 L-6 6 Z' }, np);
const nt = el('text', { x: 0, y: 20, 'text-anchor': 'middle', 'font-size': 10.5, 'font-weight': 600 }, np); nt.textContent = 'N';
const ms = el('g', { transform: `translate(0 ${(g.t + unten + RAND * .55) * S})`, fill: '#17394f' }, svg);
for (let i = 0; i < 5; i++) el('rect', { x: i * S, y: -3, width: S, height: 4, fill: i % 2 ? '#fffdf9' : '#17394f', stroke: '#17394f', 'stroke-width': .8 }, ms);
[[0, '0'], [5, '5 m']].forEach(([m, t]) => { const e = el('text', { x: m * S, y: 14, 'text-anchor': m ? 'end' : 'start', 'font-size': 10.5, fill: '#6e685f' }, ms); e.textContent = t; });
const gt = el('text', { x: g.b * S, y: (g.t + unten + RAND * .55) * S + 4, 'text-anchor': 'end', 'font-size': 11, 'font-weight': 600, 'letter-spacing': 1.2, fill: '#6e685f' }, svg);
gt.textContent = g.name.toUpperCase();
return svg;
}
/* ── Energieskala ─────────────────────────────── */
const balken = $('[data-balken]'), grenzen = $('[data-grenzen]');
balken.innerHTML = KLASSEN.map(k => `<span style="--k:${k[3]}"${k[4] ? ' class="dunkel"' : ''}>${k[0]}</span>`).join('');
grenzen.innerHTML = KLASSEN.map(k => `<span>${k[2]}</span>`).join('');
let zaehler = 0;
function setzeMarke(wert, sofort) {
const skala = $('[data-skala]'), marke = $('[data-marke]'), b = $('[data-markenwert]');
const breite = balken.getBoundingClientRect().width, x = anteil(wert) * breite;
const k = klasseVon(wert);
[...balken.children].forEach((s, i) => s.classList.toggle('ist', KLASSEN[i] === k));
const zug = anteil(wert) < .12 ? '-6px' : anteil(wert) > .85 ? 'calc(-100% + 6px)' : '-50%';
b.style.setProperty('--zug', zug);
cancelAnimationFrame(zaehler);
skala.setAttribute('aria-label', `Energieeffizienzklasse ${k[0]} auf der Skala A+ bis H, Kennwert ${zahl(wert, 1)} Kilowattstunden je Quadratmeter und Jahr`);
if (sofort || reduziert()) {
marke.style.transition = 'none'; marke.style.setProperty('--x', x + 'px'); marke.offsetWidth; marke.style.transition = '';
b.textContent = zahl(wert, 1) + ' kWh/(m²·a)'; return;
}
marke.style.transition = 'none'; marke.style.setProperty('--x', '0px'); marke.offsetWidth; marke.style.transition = '';
marke.style.setProperty('--x', x + 'px');
const t0 = performance.now(), kurve = t => 1 - Math.pow(1 - t, 4); // easeOutQuart wie die Marke
const schritt = jetzt => { const t = Math.min(1, (jetzt - t0) / 1000); b.textContent = zahl(wert * kurve(t), 1) + ' kWh/(m²·a)'; if (t < 1) zaehler = requestAnimationFrame(schritt); };
zaehler = requestAnimationFrame(schritt);
}
/* ── Exposé aufbauen ──────────────────────────── */
let objekt = null, geschoss = 0, gewaehlt = null;
const planBox = $('[data-plan]'), liste = $('[data-raumliste]'), zeiger = $('[data-zeiger]');
function zeigeObjekt(key) {
objekt = OBJEKTE[key];
const wf = objekt.geschosse.flatMap(g => g.raeume).reduce((s, r) => s + flaeche(r), 0);
$('[data-objektnr]').textContent = 'Objekt-Nr. ' + objekt.nr;
$('[data-pfad]').innerHTML = objekt.pfad.map(p => `<li>${p}</li>`).join('');
$('[data-h1]').textContent = objekt.h1;
$('[data-unter]').textContent = objekt.unter;
$('[data-preislabel]').textContent = objekt.preislabel;
$('[data-preis]').textContent = euro(objekt.preis);
$('[data-preisnote]').textContent = objekt.preisnote;
$('[data-tabelle]').innerHTML = objekt.zeilen(wf).map(([k, v, c]) => `<tr${c ? ` class="${c}"` : ''}><th scope="row">${k}</th><td>${v}</td></tr>`).join('');
$('[data-provision]').textContent = objekt.provision;
// Pflichtangaben
const ea = objekt.ea, k = klasseVon(ea.wert);
$('[data-pflicht]').innerHTML = [
['Art des Ausweises', ea.art], [ea.kennwort, zahl(ea.wert, 1) + ' kWh/(m²·a)'], ['Wesentlicher Energieträger', ea.traeger],
['Baujahr laut Ausweis', ea.baujahr], ['Effizienzklasse', k[0]],
].map(([a, b]) => `<dt>${a}</dt><dd>${b}</dd>`).join('');
$('[data-ausweisnote]').textContent = ea.note;
// Grundriss
planBox.querySelectorAll('svg').forEach(s => s.remove());
objekt.geschosse.forEach((g, i) => { const s = zeichne(g); if (i) s.dataset.aus = ''; planBox.insertBefore(s, zeiger); });
const seg = $('[data-geschosse]');
seg.innerHTML = objekt.geschosse.map((g, i) => `<button type="button" aria-pressed="${!i}" data-g="${i}">${g.name}</button>`).join('');
seg.hidden = objekt.geschosse.length < 2;
objekt.wf = wf;
fuelleListe(0);
$('[data-planfuss]').textContent = 'Flächen gerundet' + (key === 'miete' ? ', Balkon zur Hälfte angerechnet' : ', Dachschrägen im OG eingerechnet') + '. Grundriss nicht maßstabsgetreu, ohne Möblierung.';
geschoss = 0; gewaehlt = null;
document.querySelectorAll('[data-nav]').forEach(a => a.toggleAttribute('aria-current', a.dataset.nav === key));
requestAnimationFrame(() => setzeMarke(ea.wert));
}
function fuelleListe(gi) {
const g = objekt.geschosse[gi], gs = g.raeume.reduce((s, r) => s + flaeche(r), 0), mehr = objekt.geschosse.length > 1;
liste.innerHTML = g.raeume.map(r => `<li><button type="button" aria-pressed="false" data-raum="${r.id}" data-g="${gi}"><span>${r.kn && r.balkon ? r.kn : r.n}</span><span>${zahl(rund(flaeche(r)), 1)} m²</span></button></li>`).join('')
+ `<li class="summe">${mehr ? `<span>${g.name} ${zahl(rund(gs), 1)} m²</span>` : '<span>Summe der Räume</span>'}<span>Wohnfläche ${zahl(rund(objekt.wf), 1)} m²</span></li>`;
}
function zeigeGeschoss(i) {
if (i !== geschoss || !liste.children.length) fuelleListe(i);
geschoss = i;
planBox.querySelectorAll('svg').forEach((s, j) => s.toggleAttribute('data-aus', j !== i));
document.querySelectorAll('[data-geschosse] button').forEach(b => b.setAttribute('aria-pressed', String(+b.dataset.g === i)));
}
function hebe(id, gi, fest) {
if (id && gi !== undefined && gi !== geschoss) zeigeGeschoss(gi);
planBox.querySelectorAll('.raum').forEach(p => p.classList.toggle('an', !!id && p.dataset.raum === id && p.closest('svg') === planBox.querySelectorAll('svg')[geschoss]));
liste.querySelectorAll('button').forEach(b => {
const treffer = !!id && b.dataset.raum === id && +b.dataset.g === geschoss;
b.classList.toggle('an', treffer);
if (fest !== undefined) b.setAttribute('aria-pressed', String(treffer && fest));
});
}
$('[data-geschosse]').addEventListener('click', e => { const b = e.target.closest('button'); if (!b) return; stoppen(); gewaehlt = null; hebe(null, undefined, false); zeigeGeschoss(+b.dataset.g); });
liste.addEventListener('click', e => {
const b = e.target.closest('button'); if (!b) return; stoppen();
const neu = b.getAttribute('aria-pressed') !== 'true';
gewaehlt = neu ? [b.dataset.raum, +b.dataset.g] : null;
hebe(neu ? b.dataset.raum : null, +b.dataset.g, neu);
});
liste.addEventListener('pointerover', e => { const b = e.target.closest('button'); if (b && e.isTrusted && matchMedia('(hover: hover)').matches) hebe(b.dataset.raum, +b.dataset.g); });
liste.addEventListener('pointerleave', () => hebe(gewaehlt && gewaehlt[0], gewaehlt && gewaehlt[1]));
planBox.addEventListener('pointerover', e => { const p = e.target.closest('.raum'); if (p && e.isTrusted) { stoppen(); hebe(p.dataset.raum); } });
planBox.addEventListener('pointerleave', () => hebe(gewaehlt && gewaehlt[0], gewaehlt && gewaehlt[1]));
planBox.addEventListener('click', e => { const p = e.target.closest('.raum'); if (!p) return; gewaehlt = [p.dataset.raum, geschoss]; hebe(p.dataset.raum, geschoss, true); });
$('[data-drucken]').addEventListener('click', () => { stoppen(); planBox.querySelectorAll('svg').forEach(s => s.style.transition = 'none'); window.print(); });
document.querySelectorAll('a[href^="#"]').forEach(a => a.addEventListener('click', e => e.preventDefault()));
/* ── Anzeigen ─────────────────────────────────── */
const FASSADE = {
stadthaus: `<svg viewBox="0 0 400 300" aria-hidden="true"><rect width="400" height="300" fill="#e9e0d3"/><rect y="236" width="400" height="64" fill="#ddd2c1"/>
<g fill="#fbf8f2" stroke="#17394f" stroke-width="2"><path d="M110 236V128L200 62l90 66v108z"/></g>
<g fill="none" stroke="#17394f" stroke-width="2"><path d="M96 138L200 54l104 84"/><path d="M236 84V60h16v37"/>
<rect x="132" y="150" width="30" height="40"/><rect x="185" y="150" width="30" height="40"/><rect x="238" y="150" width="30" height="40"/>
<rect x="132" y="200" width="30" height="36"/><path d="M185 236v-26a15 15 0 0 1 30 0v26"/><rect x="238" y="200" width="30" height="36"/>
<circle cx="200" cy="110" r="11"/><path d="M147 150v40M200 150v40M253 150v40M132 170h30M185 170h30M238 170h30" stroke-width="1"/></g>
<g stroke="#17394f" stroke-width="1" fill="none" opacity=".55"><path d="M40 236c0-40 10-80 22-92 12 12 22 52 22 92"/><path d="M62 236v-60"/><path d="M318 236c4-30 18-56 34-56s30 26 34 56"/></g>
<path d="M0 236h400" stroke="#17394f" stroke-width="2"/></svg>`,
altbau: `<svg viewBox="0 0 400 300" aria-hidden="true"><rect width="400" height="300" fill="#e6e0d6"/><rect y="262" width="400" height="38" fill="#d9cfc0"/>
<g fill="#fbf8f2" stroke="#17394f" stroke-width="2"><rect x="80" y="44" width="240" height="218"/></g>
<g fill="none" stroke="#17394f" stroke-width="2"><path d="M72 44h256M76 36h248l-4 8"/><path d="M80 102h240M80 156h240M80 210h240" stroke-width="1"/>
${[0, 1, 2].map(r => [0, 1, 2, 3].map(c => `<rect x="${100 + c * 56}" y="${58 + r * 54}" width="24" height="36"/><path d="M${96 + c * 56} ${56 + r * 54}h32" stroke-width="1.4"/>`).join('')).join('')}
<path d="M148 166h104v6H148z" fill="#17394f"/><path d="M150 172v16M250 172v16M150 188h100" stroke-width="1"/>
<path d="M186 262v-34a14 14 0 0 1 28 0v34"/><rect x="100" y="222" width="24" height="30"/><rect x="276" y="222" width="24" height="30"/></g>
<path d="M0 262h400" stroke="#17394f" stroke-width="2"/></svg>`,
bungalow: `<svg viewBox="0 0 400 300" aria-hidden="true"><rect width="400" height="300" fill="#e4e1d8"/><rect y="226" width="400" height="74" fill="#d6d3c3"/>
<g fill="#fbf8f2" stroke="#17394f" stroke-width="2"><rect x="56" y="140" width="288" height="86"/></g>
<g fill="none" stroke="#17394f" stroke-width="2"><path d="M44 140h312" stroke-width="3"/><rect x="76" y="156" width="120" height="70"/><path d="M116 156v70M156 156v70" stroke-width="1"/>
<rect x="214" y="156" width="40" height="70"/><g stroke-width="1">${Array.from({ length: 13 }, (_, i) => `<path d="M${268 + i * 5.6} 150v76"/>`).join('')}</g></g>
<g stroke="#17394f" stroke-width="1" fill="none" opacity=".55"><path d="M362 226c0-24 6-48 14-56 8 8 14 32 14 56"/><path d="M14 226c2-18 10-34 20-34s18 16 20 34"/></g>
<path d="M0 226h400" stroke="#17394f" stroke-width="2"/></svg>`,
};
const listeEl = $('[data-liste]');
listeEl.innerHTML = ANZEIGEN.map(a => {
const k = klasseVon(a.ea[1]);
return `<a class="karte" href="#expose">
<div class="bild">${FASSADE[a.bild]}</div>
<div class="ort"><span class="label">${a.ort}</span></div>
<h3>${a.titel}</h3>
<div class="fakten"><span>${a.zi} Zimmer</span><span>${a.wf} m²</span><b>${a.preis}</b></div>
<div class="ea"><span>${a.ea[0]} · ${zahl(a.ea[1], 1)} kWh/(m²·a) · ${a.ea[2]} · Baujahr ${a.ea[3]} · Klasse ${k[0]}</span>
<span class="kl${k[4] ? ' dunkel' : ''}" style="--k:${k[3]}" aria-hidden="true">${k[0]}</span>
<div class="mini" aria-hidden="true" data-wert="${a.ea[1]}">${KLASSEN.map(x => `<i style="--k:${x[3]}"${x === k ? ' class="ist"' : ''}></i>`).join('')}<em></em></div></div>
</a>`;
}).join('') + '<p class="listen-fuss">Angaben zum Energieausweis nach § 87 GEG bzw. GModG. Alle Preise zzgl. Provision, sofern nicht anders angegeben.</p>';
const karten = [...listeEl.querySelectorAll('.karte')];
function miniMarken(sofort) {
listeEl.querySelectorAll('.mini').forEach(m => {
const em = m.querySelector('em'), x = anteil(+m.dataset.wert) * m.getBoundingClientRect().width;
if (sofort || reduziert()) { em.style.transition = 'none'; em.style.setProperty('--x', x + 'px'); em.offsetWidth; em.style.transition = ''; return; }
em.style.transition = 'none'; em.style.setProperty('--x', '0px'); em.offsetWidth; em.style.transition = ''; em.style.setProperty('--x', x + 'px');
});
}
/* ── Variante Fahrzeug: Pflichtangaben nach Pkw-EnVKV (Beispielwerte, vom Kunden prüfen lassen) ── */
// CO2-Klassen A–G in g/km (Skala der Verordnung seit 01.05.2024), Farbe und Buchstabenfarbe (≥ 4,5 : 1)
const CO2 = [['A', 0, '#17703f', '#fff'], ['B', 95, '#367d28', '#fff'], ['C', 115, '#9cbf2b', '#1a1a1a'], ['D', 135, '#f0cf1f', '#1a1a1a'], ['E', 155, '#f2a12e', '#1a1a1a'], ['F', 175, '#e0612a', '#1a1a1a'], ['G', Infinity, '#c62a26', '#fff']];
const co2Klasse = g => CO2.find(k => g <= k[1])[0];
const FZ = {
hybrid: { modell: '1.5 Hybrid', unter: 'Kompaktwagen · Vollhybrid · Automatik', preis: 32490, stecker: false,
werte: [['Leistung (System)', '96 kW (131 PS)', 'motor'], ['Kraftstoffverbrauch kombiniert', '4,9 l/100 km'], ['CO₂-Emissionen kombiniert', '111 g/km']],
klassen: [['CO₂-Klasse', 111]],
tab: [['Antrieb', 'Benzin-Vollhybrid'], ['Hubraum', '1.498 cm³'], ['Getriebe', 'Automatik (e-CVT)'], ['0–100 km/h', '9,8 s'], ['Höchstgeschwindigkeit', '175 km/h'], ['Kofferraum', '430–1.290 l']] },
phev: { modell: '1.5 Plug-in-Hybrid', unter: 'Kompaktwagen · Plug-in-Hybrid · Automatik', preis: 39990, stecker: true,
werte: [['Leistung (System)', '150 kW (204 PS)', 'motor'], ['Energieverbrauch gewichtet kombiniert', '1,1 l/100 km und 15,8 kWh/100 km'], ['CO₂-Emissionen gewichtet kombiniert', '25 g/km'], ['Kraftstoffverbrauch bei entladener Batterie kombiniert', '5,6 l/100 km']],
klassen: [['CO₂-Klasse gewichtet kombiniert', 25], ['CO₂-Klasse bei entladener Batterie', 128]],
tab: [['Antrieb', 'Benzin-Plug-in-Hybrid'], ['Batterie (netto)', '17,8 kWh'], ['Elektrische Reichweite', '78 km (WLTP)'], ['Laden', 'AC 7,4 kW'], ['0–100 km/h', '7,6 s'], ['Kofferraum', '380–1.200 l']] },
elektro: { modell: 'Elektro', unter: 'Kompaktwagen · Elektro · 1-Gang', preis: 36990, stecker: true,
werte: [['Leistung', '150 kW (204 PS)', 'motor'], ['Stromverbrauch kombiniert', '16,4 kWh/100 km'], ['CO₂-Emissionen kombiniert', '0 g/km']],
klassen: [['CO₂-Klasse', 0]],
tab: [['Antrieb', 'Elektro, Frontantrieb'], ['Batterie (netto)', '64 kWh'], ['Elektrische Reichweite', '412 km (WLTP)'], ['Laden', 'AC 11 kW, DC bis 130 kW'], ['0–100 km/h', '7,9 s'], ['Kofferraum', '420–1.310 l']] },
};
let antrieb = 'hybrid';
function skala(titel, g) {
const kl = co2Klasse(g);
return `<div class="fz-klasse"><p>${titel}: <b>${kl}</b></p><div class="fz-skala" role="img" aria-label="${titel} ${kl} auf der Skala von A (0 g/km) bis G">`
+ CO2.map(([b, , f, t]) => `<span class="${b === kl ? 'ist' : ''}" style="--k:${f};--t:${t}" aria-hidden="true">${b}</span>`).join('') + '</div></div>';
}
function fzZeige(a, neu) {
antrieb = FZ[a] ? a : 'hybrid';
const d = FZ[antrieb];
document.querySelectorAll('[data-antrieb]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.antrieb === antrieb)));
$('[data-fz-modell]').textContent = d.modell;
$('[data-fz-unter]').textContent = d.unter;
$('[data-fz-preis]').textContent = euro(d.preis);
$('[data-fz-stecker]').hidden = !d.stecker;
const w = $('[data-fz-werte]');
w.innerHTML = d.werte.map(([k, v, c]) => `<dt class="${c || ''}">${k}</dt><dd class="${c || ''}">${v}</dd>`).join('');
$('[data-fz-klassen]').innerHTML = d.klassen.map(([t, g]) => skala(t, g)).join('');
$('[data-fz-tab]').innerHTML = d.tab.map(([k, v]) => `<tr><th scope="row">${k}</th><td>${v}</td></tr>`).join('') + '<tr><th scope="row">Farbe</th><td>Kieselgrau Metallic</td></tr><tr><th scope="row">Garantie</th><td>5 Jahre bis 150.000 km</td></tr>';
if (neu && !reduziert()) w.querySelectorAll('dd').forEach(x => x.classList.add('neu'));
}
document.querySelectorAll('[data-antrieb]').forEach(b => b.addEventListener('click', () => { stoppen(); fzZeige(b.dataset.antrieb, true); }));
/* ── Varianten (Regel 13) ─────────────────────── */
const VARIANTEN = ['kauf', 'miete', 'anzeige', 'fahrzeug'];
const WERTE = {
kauf: 'easeOutQuart · UI 400 ms · Hover 200 ms · Marke 1000 ms',
miete: 'easeOutQuart · UI 400 ms · Marke 1000 ms · Verbrauchsausweis',
anzeige: 'easeOutQuart · Bild 1000 ms scale 1.04 · Marke 1000 ms',
fahrzeug: 'cubic-bezier(0.2, 0, 0.38, 0.9) · Hover 150 ms · Werte 240 ms · CO₂-Klasse 400 ms',
};
let variante = 'kauf';
function setzeVariante(v, url) {
variante = VARIANTEN.includes(v) ? v : 'kauf';
document.body.dataset.v = variante;
document.querySelectorAll('[data-variante]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.variante === variante)));
$('[data-werte]').textContent = WERTE[variante];
if (variante === 'fahrzeug') fzZeige(antrieb);
else if (variante === 'anzeige') {
$('[data-objektnr]').textContent = '3 Objekte';
$('[data-pfad]').innerHTML = '<li>Kaufen und Mieten</li><li>Landshut und Umgebung</li>';
$('[data-h1]').textContent = 'Aktuelle Angebote';
$('[data-unter]').textContent = 'Landshut und Umgebung · sortiert nach Neuheit';
requestAnimationFrame(() => miniMarken());
} else zeigeObjekt(variante);
if (url) { const q = new URLSearchParams(location.search); q.set('v', variante); history.replaceState(null, '', '?' + q.toString().replace(/=(?=&|$)/g, '')); }
}
document.querySelectorAll('[data-variante]').forEach(b => b.addEventListener('click', () => { stoppen(); setzeVariante(b.dataset.variante, true); }));
addEventListener('resize', () => { if (variante === 'fahrzeug') return; if (variante === 'anzeige') miniMarken(true); else setzeMarke(objekt.ea.wert, true); });
/* ── Vorführung: gedachter Zeiger wählt Räume, Marke läuft zum Kennwert ── */
let uhren = [];
const nach = (ms, f) => uhren.push(setTimeout(f, ms));
function zeigeAuf(id) {
const svg = planBox.querySelectorAll('svg')[geschoss], p = svg && [...svg.querySelectorAll('.raum')].find(x => x.dataset.raum === id);
if (!p) return;
const a = p.getBoundingClientRect(), b = planBox.getBoundingClientRect();
zeiger.style.transform = `translate(${a.left - b.left + a.width / 2 + 34}px, ${a.top - b.top + a.height / 2 + 26}px)`; // neben die Beschriftung
hebe(id);
}
function runde() {
if (variante === 'fahrzeug') {
['hybrid', 'phev', 'elektro'].forEach((a, i) => nach(400 + i * 2600, () => fzZeige(a, true)));
nach(400 + 3 * 2600, runde);
return;
}
if (variante === 'anzeige') {
miniMarken();
karten.forEach((k, i) => { nach(900 + i * 1500, () => { karten.forEach(x => x.classList.toggle('an', x === k)); }); });
nach(900 + karten.length * 1500, () => karten.forEach(x => x.classList.remove('an')));
nach(6200, runde);
return;
}
zeigeGeschoss(0); hebe(null);
setzeMarke(objekt.ea.wert);
const folge = variante === 'kauf'
? [[1200, 'wohnen'], [2100, 'kueche'], [3000, 'arbeiten'], [3900, 'og'], [4500, 'schlafen'], [5400, 'kind1'], [6300, 'bad'], [7200, null]]
: [[1200, 'wohnen'], [2100, 'schlafen'], [3000, 'kueche'], [3900, 'balkon'], [4800, 'arbeiten'], [5700, null]];
folge.forEach(([t, id]) => nach(t, () => { if (id === 'og') { hebe(null); zeigeGeschoss(1); } else if (id) zeigeAuf(id); else hebe(null); }));
nach(folge[folge.length - 1][0] + 1400, runde);
}
function stoppen() { uhren.forEach(clearTimeout); uhren = []; html.classList.remove('vorfuehrung'); karten.forEach(x => x.classList.remove('an')); }
function neustart() {
stoppen();
antrieb = 'hybrid';
setzeVariante(variante);
}
function vorfuehren() {
stoppen();
setzeVariante(variante);
html.classList.add('vorfuehrung');
zeiger.style.transition = 'none'; zeiger.style.transform = `translate(${planBox.clientWidth / 2}px, ${planBox.clientHeight / 2}px)`; zeiger.offsetWidth; zeiger.style.transition = '';
runde();
}
function pause() { uhren.forEach(clearTimeout); uhren = []; }
function weiter() { if (html.classList.contains('vorfuehrung') && !uhren.length) runde(); }
setzeVariante(new URLSearchParams(location.search).get('v'));
addEventListener('load', () => { if (!html.classList.contains('vorfuehrung')) setzeVariante(variante); });
document.fonts && document.fonts.ready.then(() => { if (variante !== 'anzeige' && variante !== 'fahrzeug' && !html.classList.contains('vorfuehrung')) setzeMarke(objekt.ea.wert, true); });
})();
</script>
</body>
</html>