Vorher-Nachher-Vergleichsregler
Zwei deckungsgleiche Bilder übereinander, ein Griff teilt sie: Man prüft den Unterschied selbst, statt ihn behauptet zu bekommen.
Zwei deckungsgleiche Bilder liegen übereinander, etwa vorher und nachher, alt und neu, roh und bearbeitet. Ein senkrechter Regler mit Griff teilt die Fläche. Ziehen oder Pfeiltasten verschieben die Grenze, das obere Bild wird per clip-path beschnitten. In den Ecken stehen Labels für vorher und nachher, oft gibt es Reiter für mehrere Beispiele. Variante Lupe: Ein Fenster folgt dem Zeiger und zeigt dort die Nachher-Version.
Der Nutzer prüft den Unterschied selbst, statt ihn nur behauptet zu bekommen. Das überzeugt mehr als zwei Bilder nebeneinander. Die eigene Handbewegung macht die Verbesserung spürbar.
Wann einsetzen
Renovierung, Umbau und Architektur, Relaunch-Fallstudien (alte gegen neue Website), Retusche und Bildbearbeitung, Upscaling- und KI-Werkzeuge, Vergleiche von Farbe oder Theme.
Wann nicht
Wenn die Bilder nicht exakt deckungsgleich sind, denn Versatz zerstört den Effekt. Bei sehr kleinen Unterschieden ohne Zoom. Wenn das Ziehen auf Touch mit dem Scrollen der Seite kollidiert und das nicht sauber gelöst ist.
Varianten
- Natives input type=range über dem Bild, nur der Griff ist greifbar (Clipdrop)
- Reiter für mehrere Beispiele mit Vorher/Nachher-Chips (upscale.media)
- Lupe: Fenster folgt dem Zeiger, clip-path inset(… round 12px), gelerpt (Topaz Labs)
- Hover: die Grenze folgt der Maus ohne Klick
- Waagerechte Teilung oder diagonaler Schnitt
Umsetzung
Markup: figure mit zwei Bildern (beide grid-area 1/1) und einem input type=range. CSS: .nachher { clip-path: inset(0 0 0 var(--pos)) }, Trennlinie und Griff bei left: var(--pos). JS setzt nur --pos aus dem Wert des Reglers. Das Range-Input liegt über der ganzen Breite, transparent und mit pointer-events none; nur der Daumen bekommt pointer-events all. So bleiben Tastatur und Wertansage nativ. touch-action: pan-y am Container, damit die Seite auf Touch weiter scrollt. Lupe: Zeigerposition lerpen (0,15 bis 0,2) und als clip-path inset() mit round-Radius setzen. Beide Bilder exakt gleich groß exportieren.
Robust umsetzen
Telefon. Großer Griff mit mindestens 44 px Touch-Fläche und touch-action: pan-y. Die Lupe mobil durch den Regler ersetzen oder kleiner machen (Topaz teilt durch 1,5).
Ohne Bewegung. Den Regler nicht automatisch hin und her animieren. Lupe ohne Nachlauf (Lerp 1).
Barrierefrei. Natives input type=range mit aria-label und aria-valuetext (z. B. „60 % Nachher sichtbar“). Beide Bilder mit eigenem Alternativtext. Sichtbarer Fokus am Griff. Die Lupe ist nur Zusatz, der Inhalt bleibt ohne Maus erreichbar.
Code
<!DOCTYPE html>
<!--
Vorher-Nachher-Vergleichsregler
Fundus · Galerien & Slider · https://cecodesign.de/lab/fundus/galerien-slider/vorher-nachher-vergleichsregler/
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>Vorher-Nachher-Vergleichsregler · 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: '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; }
/* Lupengröße als echte Länge registriert, damit sie per transition wachsen kann */
@property --lupe { syntax: '<length>'; inherits: true; initial-value: 0px; }
:root {
/* Rezept „Handwerk: Werkstatt und Klartext“: Materialfarben als Flächen, dunkle Schrift, ein Signalton nur für Anfrage und Anruf */
--grund: #efedea; --flaeche: #d3cdc2; --holz: #c3b4a5; --salbei: #bdc1a9; --text: #1a1a1a; --leise: #57524c; --linie: rgba(26, 26, 26, .16); --signal: #fb6100;
--kurve: cubic-bezier(0.5, 0.1, 0.2, 1); /* gemessen (Topaz): Größenwechsel 350 ms */
--ui: cubic-bezier(0.4, 0, 0.2, 1); /* Rezept: Hover, Farbe 150 ms */
--zu: cubic-bezier(0.4, 0, 1, 1); /* Rezept: Schließen schneller als Öffnen */
--rand: clamp(16px, 3.4vw, 48px);
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--grund); color: var(--text); font-family: 'Atkinson Hyperlegible Next', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; display: grid; grid-template-rows: auto 1fr; }
button { font: inherit; color: inherit; }
.mono { font: 500 10.5px/1.4 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); }
.gewerk { font: 600 .8125rem/1.2 'Special Gothic', 'Arial Narrow', sans-serif; font-stretch: 75%; letter-spacing: .04em; text-transform: uppercase; font-variant-numeric: tabular-nums; }
/* Kopf: Telefon und Anfrage immer sichtbar */
.kopf { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 0 var(--rand); height: 68px; border-bottom: 1px solid var(--linie); background: var(--grund); }
.marke { display: grid; gap: 4px; text-decoration: none; color: inherit; }
.marke b { font: 700 22px/1 'Special Gothic', sans-serif; font-stretch: 112%; letter-spacing: .01em; }
.marke small { color: var(--leise); }
.nav { display: flex; gap: clamp(16px, 2.2vw, 30px); list-style: none; padding: 0; font-size: 15px; font-weight: 500; }
.nav a { color: var(--text); text-decoration: none; background: linear-gradient(currentColor 0 0) 0 100% / 0 1.5px no-repeat; padding-bottom: 3px; transition: background-size .15s var(--ui); }
.nav a:hover, .nav a[aria-current] { background-size: 100% 1.5px; }
.kontakt { display: flex; align-items: center; gap: 18px; }
.tel { display: flex; align-items: center; gap: 8px; color: var(--text); text-decoration: none; font-weight: 700; font-size: 15px; font-variant-numeric: tabular-nums; white-space: nowrap; }
.tel svg { width: 18px; height: 18px; }
.cta { display: inline-flex; align-items: center; gap: 10px; background: var(--signal); color: var(--text); text-decoration: none; font: 700 15px/1 'Atkinson Hyperlegible Next', sans-serif; padding: 13px 18px; border: 0; border-radius: 3px; cursor: pointer; white-space: nowrap; transition: background-color .15s var(--ui), scale .15s var(--ui); }
.cta:hover { background: #ff7a26; }
.cta:active { scale: .97; }
.menue { display: none; width: 42px; height: 42px; border: 1px solid var(--linie); border-radius: 3px; background: none; }
.menue i { display: block; width: 16px; height: 2px; margin: 4px auto; background: var(--text); }
/* Bühne */
.buehne { display: grid; grid-template-columns: minmax(320px, .78fr) minmax(0, 1.22fr); align-items: stretch; }
.text { padding: clamp(24px, 4.6vh, 60px) clamp(16px, 3.6vw, 52px); display: flex; flex-direction: column; justify-content: center; gap: clamp(16px, 2.4vh, 26px); }
.rubrik { display: flex; gap: 10px; align-items: center; color: var(--leise); }
.rubrik i { width: 18px; height: 1px; background: var(--leise); }
h1 { font: 700 clamp(2.25rem, 1.5rem + 3vw, 3.5rem)/1.05 'Special Gothic', sans-serif; font-stretch: 100%; letter-spacing: -.01em; max-width: 12ch; text-wrap: balance; }
.lead { font-size: 1.0625rem; line-height: 1.55; max-width: 42ch; text-wrap: pretty; }
/* Projekte als Reiter (Tabs) */
.projekte { display: grid; border-bottom: 1px solid var(--linie); }
.projekt { display: grid; grid-template-columns: 2.4em 1fr auto; align-items: baseline; gap: 12px; padding: 13px 0 13px 14px; border: 0; border-top: 1px solid var(--linie); background: none; text-align: left; cursor: pointer; position: relative; font-size: 16px; font-weight: 700; color: var(--text); transition: background-color .15s var(--ui); }
.projekt::before { content: ''; position: absolute; left: 0; top: 11px; bottom: 11px; width: 3px; background: var(--text); scale: 1 0; transition: scale .2s var(--zu); }
.projekt .nr, .projekt .tage { color: var(--leise); }
.projekt .ort { font-weight: 400; color: var(--leise); }
.projekt[aria-selected='true']::before { scale: 1 1; transition: scale .28s var(--ui); }
.projekt:hover { background: rgba(211, 205, 194, .35); }
.projekt:focus-visible { outline: 2px solid var(--text); outline-offset: -2px; }
.zahlen { display: grid; grid-template-columns: repeat(3, auto); justify-content: start; font-variant-numeric: tabular-nums; }
.zahlen div { padding: 0 18px 0 0; }
.zahlen div + div { padding-left: 18px; border-left: 1px solid var(--linie); }
.zahlen dt { margin-bottom: 5px; color: var(--leise); }
.zahlen dd { font-size: 16px; font-weight: 700; white-space: nowrap; }
.handlung { display: flex; align-items: center; gap: 22px; flex-wrap: wrap; }
/* Rechte Fläche mit dem Vergleich */
.feld { --bw: min(100%, 720px, calc((100svh - 190px) * 4 / 3)); background: var(--flaeche); display: grid; grid-template-columns: minmax(0, 1fr); min-width: 0; align-content: center; justify-items: center; gap: 16px; padding: clamp(20px, 4vh, 44px) clamp(16px, 3vw, 40px); }
.steuer { width: var(--bw); display: flex; justify-content: space-between; align-items: center; gap: 12px 20px; min-height: 28px; }
.modi { display: flex; gap: 4px; }
.modi button { letter-spacing: inherit; text-transform: inherit; font: inherit; color: var(--leise); background: none; border: 1px solid rgba(26,26,26,.24); border-radius: 999px; height: 28px; padding: 0 12px; cursor: pointer; transition: background-color .15s var(--ui), color .15s var(--ui), border-color .15s var(--ui); }
.modi button:hover { color: var(--text); }
.modi button[aria-pressed='true'] { background: var(--text); border-color: var(--text); color: var(--grund); }
.modi button:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }
.werte { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.werte b { color: var(--text); font-weight: 500; }
.beschreibung { width: var(--bw); font-size: 15px; line-height: 1.5; color: var(--leise); text-wrap: pretty; }
.beschreibung b { color: var(--text); }
/* Der Vergleich: zwei deckungsgleiche Bilder übereinander */
.vergleich {
--pos: 50%; --lx: 50%; --ly: 50%;
position: relative; width: var(--bw); aspect-ratio: 4 / 3; margin: 0;
display: grid; touch-action: pan-y; user-select: none; -webkit-user-select: none; overflow: hidden;
border-radius: 3px; box-shadow: 0 30px 50px -34px rgba(26, 26, 26, .55);
transition: --lupe .35s var(--kurve);
}
.bild { grid-area: 1 / 1; overflow: hidden; }
.bild svg { display: block; width: 100%; height: 100%; }
.bild .inhalt { width: 100%; height: 100%; transition: opacity .28s var(--ui); }
.wechselt .bild .inhalt { opacity: 0; transition: opacity .15s var(--zu); }
.nachher { clip-path: inset(0 0 0 var(--pos)); }
/* Trennlinie und Griff (gemessen: Clipdrop 38 × 30 px, Radius 12 px, 1,5 px weißer Rand) */
.trenner { position: absolute; top: 0; bottom: 0; left: var(--pos); width: 0; pointer-events: none; z-index: 2; }
.trenner::before { content: ''; position: absolute; inset: 0 auto 0 -1px; width: 2px; background: #fff; box-shadow: 0 0 0 1px rgba(26,26,26,.12); }
.griff { position: absolute; top: 50%; left: 0; width: 38px; height: 30px; translate: -50% -50%; display: grid; place-items: center; background: var(--text); border: 1.5px solid #fff; border-radius: 12px; box-shadow: 0 6px 18px -6px rgba(26,26,26,.6); transition: scale .15s var(--ui); }
.griff svg { width: 18px; height: 10px; }
.chip { position: absolute; top: 14px; z-index: 3; pointer-events: none; padding: 7px 10px 6px; border-radius: 3px; transition: opacity .15s var(--ui); }
.chip-vorher { left: 14px; background: rgba(239, 237, 234, .9); color: var(--text); }
.chip-nachher { right: 14px; background: var(--text); color: var(--grund); }
.chip.weg { opacity: 0; transition-timing-function: var(--zu); }
/* Natives Range-Input über der ganzen Breite. Nur der Daumen ist greifbar (Clipdrop), sonst scrollt die Seite auf Touch weiter.
Breite + Daumenbreite, damit Daumenmitte und --pos genau übereinander liegen. */
.regler { --dw: 44px; position: absolute; z-index: 4; top: calc(50% - 22px); left: calc(var(--dw) / -2); width: calc(100% + var(--dw)); height: 44px; margin: 0; appearance: none; -webkit-appearance: none; background: transparent; pointer-events: none; cursor: grab; outline: none; }
.regler::-webkit-slider-runnable-track { height: 100%; background: transparent; }
.regler::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: var(--dw); height: 44px; margin-top: 0; background: transparent; border: 0; pointer-events: all; cursor: grab; }
.regler::-moz-range-thumb { width: var(--dw); height: 44px; background: transparent; border: 0; pointer-events: all; cursor: grab; }
.regler::-moz-range-track { background: transparent; }
.regler:active::-webkit-slider-thumb { cursor: grabbing; }
.regler:focus-visible ~ .trenner .griff { outline: 2px solid var(--signal); outline-offset: 3px; }
.vergleich:has(.regler:focus-visible) { outline: 2px solid var(--text); outline-offset: 3px; }
.vergleich:hover .griff, .ziehen .griff { scale: 1.06; }
.vergleich.klickbar { cursor: pointer; }
/* Variante Hover: Grenze folgt dem Zeiger ohne Klick */
.modus-hover { cursor: ew-resize; }
.modus-hover .regler::-webkit-slider-thumb { pointer-events: none; }
.modus-hover .griff { width: 10px; height: 46px; border-radius: 99px; }
.modus-hover .griff svg { display: none; }
/* Variante Lupe: Fenster folgt dem Zeiger und zeigt dort den Nachher-Stand (gemessen: inset(… round 12px), Lerp 0,2) */
.modus-lupe { cursor: crosshair; }
.modus-lupe .nachher { clip-path: inset(calc(var(--ly) - var(--lupe) / 2) calc(100% - var(--lx) - var(--lupe) / 2) calc(100% - var(--ly) - var(--lupe) / 2) calc(var(--lx) - var(--lupe) / 2) round 12px); }
.modus-lupe .trenner, .modus-lupe .regler { display: none; }
.ring { position: absolute; z-index: 2; pointer-events: none; left: calc(var(--lx) - var(--lupe) / 2); top: calc(var(--ly) - var(--lupe) / 2); width: var(--lupe); height: var(--lupe); border-radius: 12px; box-shadow: 0 0 0 2px #fff, 0 24px 40px -18px rgba(26,26,26,.5); display: none; }
.ring span { position: absolute; left: 0; top: -26px; color: var(--grund); background: var(--text); padding: 5px 7px 4px; border-radius: 3px; opacity: 0; transition: opacity .15s var(--ui); }
.modus-lupe .ring { display: block; }
.modus-lupe.lupe-offen .ring span { opacity: 1; }
.modus-lupe .chip-vorher, .modus-lupe .chip-nachher { opacity: 1; }
/* Untere Leiste auf dem Telefon: Anrufen und Anfrage */
.unten { display: none; }
/* Gezeichneter Zeiger für die Vorführung */
.zeiger { position: fixed; left: 0; top: 0; z-index: 9; width: 20px; height: 20px; pointer-events: none; display: none; }
.vorfuehrung .zeiger { display: block; }
/* Reduzierte Bewegung: kein Nachlauf, kein Wachsen, Grenze springt statt zu gleiten */
.reduziert .vergleich { transition: none; }
.reduziert .projekt::before, .reduziert .griff { transition: none; }
.reduziert .bild .inhalt { transition: opacity .15s linear; }
/* Sehr schmal (320 px): Werte-Zeile umbrechen statt kürzen */
@media (max-width: 360px) { .steuer { flex-wrap: wrap; } .werte { white-space: normal; overflow-wrap: anywhere; } }
/* Kontrastmodus: Knöpfe und Reiter als Knopf erkennbar */
@media (forced-colors: active) { button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; } }
/* Vorschau 960 × 720: nur die Bühne */
.vorschau body { overflow: hidden; grid-template-rows: 1fr; }
.vorschau .kopf, .vorschau .lead, .vorschau .zahlen, .vorschau .handlung, .vorschau .beschreibung, .vorschau .steuer { display: none; }
.vorschau .buehne { grid-template-columns: minmax(0, .74fr) minmax(0, 1.26fr); }
.vorschau .feld { --bw: min(100%, 640px, calc((100svh - 80px) * 4 / 3)); }
.vorschau .projekt .ort { display: none; }
/* Touch: nur der Regler (Lupe und Hover brauchen eine Maus), großer Griff */
@media (hover: none) {
.modi { display: none; }
.griff { width: 44px; height: 34px; }
}
@media (max-width: 960px) {
.nav { display: none; }
}
@media (max-width: 860px) {
.kontakt .tel span { display: none; }
/* Telefon: Vergleich direkt unter die Überschrift, Erklärung danach */
.buehne { display: flex; flex-direction: column; gap: 22px; padding: 26px 16px 40px; }
.text { display: contents; }
.rubrik { order: 1; margin-bottom: -8px; } h1 { order: 2; } .feld { order: 3; } .projekte { order: 4; } .zahlen { order: 5; } .lead { order: 6; } .handlung { order: 7; }
.feld { --bw: min(100%, 560px); background: none; padding: 0; }
.vergleich { aspect-ratio: 4 / 4.4; }
}
@media (max-width: 640px) {
body { padding-bottom: 68px; }
.kopf { height: 60px; }
.kontakt .cta, .kontakt .tel { display: none; }
.menue { display: block; }
.marke b { font-size: 20px; }
.projekt { grid-template-columns: 2em 1fr auto; font-size: 15px; }
.projekt .ort { display: none; }
.zahlen { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.zahlen div { padding-right: 10px; } .zahlen div + div { padding-left: 10px; }
.zahlen dd { font-size: 14px; white-space: normal; }
.handlung .cta { display: none; }
.chip { top: 10px; } .chip-vorher { left: 10px; } .chip-nachher { right: 10px; }
.unten { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; position: fixed; z-index: 8; inset: auto 0 0; padding: 10px 12px calc(10px + env(safe-area-inset-bottom)); background: var(--grund); border-top: 1px solid var(--linie); }
.unten a { display: flex; justify-content: center; align-items: center; gap: 8px; height: 46px; border-radius: 3px; font-weight: 700; text-decoration: none; color: var(--text); border: 1px solid rgba(26,26,26,.3); }
.unten a.cta { border: 0; }
.vorschau .unten { display: none; }
}
</style>
</head>
<body>
<header class="kopf">
<a class="marke" href="#start"><b>KLEESCHULTE</b><small class="gewerk">Tischlerei · Innenausbau · Paderborn</small></a>
<ul class="nav">
<li><a href="#leistungen">Leistungen</a></li>
<li><a href="#referenzen" aria-current="page">Referenzen</a></li>
<li><a href="#ablauf">So läuft ein Auftrag</a></li>
<li><a href="#betrieb">Betrieb</a></li>
</ul>
<div class="kontakt">
<a class="tel" href="tel:+4952546907200"><svg viewBox="0 0 18 18" aria-hidden="true"><path d="M4.2 1.8 6.6 4.6 5.3 6.9c.9 2 2.4 3.6 4.4 4.6l2.3-1.3 2.8 2.4-1.1 2.6C8.2 15.4 2.6 9.8 1.6 2.9Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg><span>05254 690 720</span></a>
<a class="cta" href="#anfrage">Anfrage</a>
<button class="menue" type="button" aria-label="Menü öffnen"><i></i><i></i><i></i></button>
</div>
</header>
<main class="buehne" id="start">
<section class="text" aria-labelledby="titel">
<p class="rubrik gewerk"><span>Referenzen 2025/26</span><i></i><span>Vorher · Nachher</span></p>
<h1 id="titel">Gleicher Raum. Neu gebaut.</h1>
<p class="lead">Drei Aufträge aus Paderborn und Umgebung, jeweils vom selben Standpunkt aus vor und nach unserer Arbeit. Ziehen Sie den Regler und vergleichen Sie selbst.</p>
<div class="projekte" role="tablist" aria-orientation="vertical" aria-label="Projekt wählen" data-reiter></div>
<dl class="zahlen" data-zahlen aria-live="polite"></dl>
<div class="handlung"><a class="cta" href="#anfrage">Ähnliches Projekt anfragen</a><a class="tel" href="tel:+4952546907200"><svg viewBox="0 0 18 18" aria-hidden="true"><path d="M4.2 1.8 6.6 4.6 5.3 6.9c.9 2 2.4 3.6 4.4 4.6l2.3-1.3 2.8 2.4-1.1 2.6C8.2 15.4 2.6 9.8 1.6 2.9Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>05254 690 720</a></div>
</section>
<section class="feld" aria-label="Vergleich vorher und nachher">
<figure class="vergleich" data-vergleich>
<div class="bild vorher" role="img" data-vorher-rahmen><div class="inhalt" data-vorher></div></div>
<div class="bild nachher" role="img" data-nachher-rahmen><div class="inhalt" data-nachher></div></div>
<input class="regler" type="range" min="0" max="100" step="1" value="50" aria-label="Vergleich verschieben: links vorher, rechts nachher" data-regler>
<div class="trenner" aria-hidden="true"><span class="griff"><svg viewBox="0 0 18 10" fill="none" stroke="#fff" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M5 1 1 5l4 4M13 1l4 4-4 4"/></svg></span></div>
<div class="ring" aria-hidden="true"><span class="mono">Nachher</span></div>
<span class="chip chip-vorher gewerk" aria-hidden="true" data-chip-vorher>Vorher</span>
<span class="chip chip-nachher gewerk" aria-hidden="true" data-chip-nachher>Nachher</span>
</figure>
<p class="beschreibung" data-beschreibung></p>
<div class="steuer">
<div class="modi mono" role="group" aria-label="Variante">
<button type="button" aria-pressed="true" data-modus="regler">Regler</button>
<button type="button" aria-pressed="false" data-modus="hover">Hover</button>
<button type="button" aria-pressed="false" data-modus="lupe">Lupe</button>
</div>
<p class="werte mono" data-werte></p>
</div>
</section>
</main>
<nav class="unten" aria-label="Kontakt"><a href="tel:+4952546907200">Anrufen</a><a class="cta" href="#anfrage">Anfrage</a></nav>
<svg class="zeiger" data-zeiger viewBox="0 0 20 20" aria-hidden="true"><path d="M3 2 16.5 10l-6 1.4-3 5.8Z" fill="#1a1a1a" stroke="#fff" stroke-width="1.3" stroke-linejoin="round"/></svg>
<script>
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const $ = s => document.querySelector(s);
const fig = $('[data-vergleich]'), regler = $('[data-regler]'), zeiger = $('[data-zeiger]');
const elVorher = $('[data-vorher]'), elNachher = $('[data-nachher]'), werteEl = $('[data-werte]');
const chipV = $('[data-chip-vorher]'), chipN = $('[data-chip-nachher]'), beschreibungEl = $('[data-beschreibung]');
// ——— Eigene Bildwelt: drei Räume, jeweils vorher und nachher, deckungsgleich gezeichnet (1200 × 900) ———
const svgDoc = (z, inner, defs = '') => `<svg viewBox="0 0 1200 900" preserveAspectRatio="xMidYMid slice" aria-hidden="true" focusable="false"><defs>${defs}</defs>${inner}${z === 'vorher' ? '<rect width="1200" height="900" fill="#b8913f" opacity=".07"/>' : ''}</svg>`;
const rep = (n, f) => Array.from({ length: n }, (_, i) => f(i)).join('');
// Küche, Altbau im Riemekeviertel
function kueche(z) {
const alt = z === 'vorher', p = z;
let s = '';
// Wand und Boden
s += `<rect width="1200" height="720" fill="${alt ? '#e7d9b9' : '#ece9e3'}"/>`;
s += alt
? `<rect y="720" width="1200" height="180" fill="#c3ab79"/>` + rep(16, i => rep(4, r => (i + r) % 2 ? '' : `<rect x="${i * 75}" y="${720 + r * 45}" width="75" height="45" fill="#ad9462"/>`))
: `<rect y="720" width="1200" height="180" fill="#c9ad86"/>` + rep(7, i => `<path d="M0 ${738 + i * 26}H1200" stroke="#b39469" stroke-width="2"/>`) + rep(14, i => `<path d="M${(i * 97 + (i % 3) * 40) % 1200} ${738 + (i % 6) * 26}v26" stroke="#b39469" stroke-width="2"/>`);
// Fenster (Öffnung bleibt gleich)
s += `<rect x="700" y="130" width="260" height="270" fill="url(#${p}h)"/><path d="M700 330C760 316 860 318 960 330V400H700Z" fill="${alt ? '#9fae9b' : '#a9b7a2'}"/>`;
s += alt
? `<path d="M700 130H960V400H700Z M712 142V388H948V142Z" fill="#6b4a2c" fill-rule="evenodd"/><rect x="824" y="142" width="12" height="246" fill="#6b4a2c"/><rect x="712" y="258" width="236" height="10" fill="#6b4a2c"/>` +
`<path d="M712 268H948V330` + rep(12, i => `q-${9.8} 12 -19.6 0`) + `Z" fill="#f6f0df" opacity=".92"/><rect x="706" y="262" width="248" height="5" fill="#cbb48a"/>`
: `<path d="M700 130H960V400H700Z M706 136V394H954V136Z" fill="#f7f6f3" fill-rule="evenodd"/><rect x="827" y="136" width="6" height="258" fill="#f7f6f3"/>`;
s += `<rect x="688" y="400" width="284" height="12" fill="${alt ? '#d8c8a2' : '#f7f6f3'}"/>`;
// Rückwand zwischen Arbeitsplatte und Oberschränken
if (alt) {
s += `<rect x="100" y="330" width="1000" height="160" fill="#dcc79c"/>`;
s += rep(25, i => rep(4, r => { const x = 100 + i * 40, y = 330 + r * 40; if (x >= 688 && x < 972 && y < 412) return ''; return `<rect x="${x}" y="${y}" width="40" height="40" fill="none" stroke="#c4ad7f" stroke-width="2"/>` + ((i + r) % 3 === 0 ? `<circle cx="${x + 20}" cy="${y + 20}" r="9" fill="#a8743f" opacity=".7"/><circle cx="${x + 20}" cy="${y + 20}" r="3.5" fill="#e7d39f"/>` : ''); }));
s += `<rect x="688" y="400" width="284" height="12" fill="#d8c8a2"/>`;
} else {
s += `<rect x="100" y="400" width="1000" height="88" fill="#d3cdc2"/>` + rep(5, i => `<path d="M${100 + i * 200} 400v88" stroke="#c4bdb0" stroke-width="2"/>`) + `<path d="M100 444H1100" stroke="#c4bdb0" stroke-width="2"/>`;
}
// Oberschränke (vorher) bzw. offene Regale (nachher)
if (alt) {
const tuer = (x, w) => `<rect x="${x}" y="140" width="${w}" height="190" fill="#a86b38"/><path d="M${x + 14} 316V180Q${x + w / 2} 150 ${x + w - 14} 180V316Z" fill="none" stroke="#7d4b22" stroke-width="5"/><circle cx="${x + w / 2}" cy="300" r="5" fill="#d9b25c"/>`;
s += rep(5, i => tuer(100 + i * 112, 108)) + tuer(992, 108);
s += `<rect x="100" y="326" width="560" height="8" fill="#7d4b22"/><rect x="992" y="326" width="108" height="8" fill="#7d4b22"/><rect x="120" y="334" width="300" height="10" rx="4" fill="#f4f1e4"/>`;
s += `<path d="M300 334H460L444 380H316Z" fill="#7a5532"/><rect x="316" y="380" width="128" height="8" fill="#5e3f23"/>`;
s += `<rect x="960" y="190" width="24" height="34" fill="#f3efe4" stroke="#b8a37a"/><path d="M964 198h16M964 206h16M964 214h10" stroke="#b8a37a" stroke-width="2"/>`;
} else {
s += `<rect x="110" y="236" width="540" height="16" fill="#c3a07a"/><rect x="110" y="330" width="540" height="16" fill="#c3a07a"/><rect x="110" y="252" width="540" height="4" fill="#000" opacity=".06"/><rect x="110" y="346" width="540" height="4" fill="#000" opacity=".06"/>`;
s += `<rect x="140" y="196" width="34" height="40" rx="4" fill="#f4f2ee"/><rect x="180" y="206" width="30" height="30" rx="4" fill="#bdc1a9"/><rect x="224" y="186" width="22" height="50" rx="3" fill="#8e7f6c"/><ellipse cx="300" cy="226" rx="38" ry="10" fill="#e6e1d8"/><path d="M262 226q38 22 76 0" fill="#e6e1d8"/><rect x="380" y="192" width="40" height="44" rx="6" fill="#d3cdc2"/><rect x="430" y="200" width="40" height="36" rx="6" fill="#efedea" stroke="#c4bdb0"/>`;
s += `<rect x="150" y="296" width="70" height="34" rx="4" fill="#efedea" stroke="#c4bdb0"/><rect x="236" y="286" width="30" height="44" rx="14" fill="#6f7563"/><rect x="290" y="304" width="90" height="26" rx="3" fill="#c3a07a"/><rect x="420" y="290" width="36" height="40" rx="5" fill="#f4f2ee"/><circle cx="530" cy="312" r="18" fill="#bdc1a9"/><path d="M530 294v-18M522 280l8 -8 8 8" stroke="#6f7563" stroke-width="3" fill="none"/>`;
s += `<rect x="300" y="360" width="160" height="40" fill="#3f3f3c"/><rect x="300" y="396" width="160" height="4" fill="#2c2c2a"/>`;
}
// Arbeitsplatte, Spüle, Armatur
s += alt ? `<rect x="100" y="488" width="1000" height="20" fill="#d8c8a2"/><rect x="100" y="506" width="1000" height="4" fill="#b8a37a"/>`
: `<rect x="96" y="480" width="1008" height="28" fill="#c3a07a"/><rect x="96" y="502" width="1008" height="6" fill="#a9875f"/>`;
s += `<rect x="760" y="${alt ? 482 : 474}" width="140" height="8" rx="3" fill="${alt ? '#b9bcbe' : '#9ea29a'}"/>`;
s += alt ? `<path d="M830 482V438h26v10" stroke="#c9ccce" stroke-width="7" fill="none"/>` : `<path d="M830 476V420q0-16 16-16h12v14" stroke="#2c2c2a" stroke-width="6" fill="none"/>`;
// Unterschränke
if (alt) {
s += rep(10, i => { const x = 100 + i * 100; return `<rect x="${x + 2}" y="510" width="96" height="40" fill="#a86b38"/><rect x="${x + 40}" y="527" width="18" height="5" rx="2" fill="#d9b25c"/>` + ((x === 300 || x === 400) ? '' : `<rect x="${x + 2}" y="552" width="96" height="148" fill="#a86b38"/><path d="M${x + 14} 688V586Q${x + 50} 566 ${x + 86} 586V688Z" fill="none" stroke="#7d4b22" stroke-width="5"/><circle cx="${x + 50}" cy="610" r="5" fill="#d9b25c"/>`); });
s += `<rect x="302" y="552" width="196" height="148" fill="#efe6cf"/><rect x="318" y="580" width="164" height="96" rx="6" fill="#4b3a2c"/><path d="M318 568h164" stroke="#b8a37a" stroke-width="3"/>` + rep(4, i => `<circle cx="${336 + i * 42}" cy="566" r="6" fill="#6b4a2c"/>`);
s += `<rect x="100" y="700" width="1000" height="20" fill="#7d4b22"/>`;
s += `<rect x="520" y="440" width="60" height="48" rx="8" fill="#d9d4c4"/><rect x="530" y="430" width="14" height="12" fill="#4b3a2c"/><rect x="556" y="430" width="14" height="12" fill="#4b3a2c"/><rect x="600" y="452" width="70" height="36" rx="3" fill="#f0ead6" stroke="#cbb48a"/>`;
} else {
s += `<rect x="100" y="508" width="1000" height="192" fill="#959b84"/>`;
s += rep(9, i => `<path d="M${200 + i * 100 + (i > 1 ? 0 : 0)} 512V700" stroke="#7d836e" stroke-width="3"/>`) + `<path d="M100 560H300M500 560H1100" stroke="#7d836e" stroke-width="3"/>`;
s += `<rect x="300" y="512" width="200" height="188" fill="#959b84"/><rect x="312" y="526" width="176" height="140" rx="2" fill="#2f302e"/><rect x="312" y="526" width="176" height="14" fill="#3f403d"/><rect x="380" y="676" width="40" height="6" rx="3" fill="#7d836e"/>`;
s += `<rect x="104" y="700" width="992" height="20" fill="#4b4a45"/>`;
s += `<rect x="560" y="438" width="44" height="42" rx="6" fill="#efedea"/><path d="M604 450q16 4 0 18" stroke="#efedea" stroke-width="5" fill="none"/><rect x="980" y="410" width="80" height="70" rx="3" fill="#c3a07a" transform="rotate(-4 1020 445)"/>`;
}
const defs = `<linearGradient id="${p}h" x2="0" y2="1"><stop offset="0" stop-color="${alt ? '#cfd6d2' : '#d9e1e3'}"/><stop offset="1" stop-color="#f2ece0"/></linearGradient>`;
return svgDoc(z, s, defs);
}
// Treppe, Einfamilienhaus in Schloß Neuhaus
function treppe(z) {
const alt = z === 'vorher', N = 11, x0 = 170, y0 = 800, run = 64, rise = 48;
let s = `<rect width="1200" height="900" fill="${alt ? '#e4d8bf' : '#ece9e3'}"/>`;
if (alt) s += rep(40, i => `<rect x="${i * 30}" y="0" width="14" height="900" fill="#d9caa9" opacity=".55"/>`);
// Fenster und Tür (gleich)
s += `<rect x="190" y="120" width="170" height="250" fill="url(#${z}tf)"/>`;
s += alt ? `<path d="M190 120H360V370H190Z M200 130V360H350V130Z" fill="#6b4a2c" fill-rule="evenodd"/><rect x="270" y="130" width="10" height="230" fill="#6b4a2c"/>`
: `<path d="M190 120H360V370H190Z M196 126V364H354V126Z" fill="#f7f6f3" fill-rule="evenodd"/>`;
s += alt ? `<rect x="960" y="60" width="140" height="${y0 - N * rise - 60}" fill="#8a5a33"/><rect x="976" y="80" width="108" height="80" fill="none" stroke="#6b4426" stroke-width="5"/><rect x="976" y="180" width="108" height="80" fill="none" stroke="#6b4426" stroke-width="5"/><circle cx="1074" cy="230" r="6" fill="#d9b25c"/>`
: `<rect x="960" y="60" width="140" height="${y0 - N * rise - 60}" fill="#f4f2ee"/><rect x="960" y="60" width="140" height="${y0 - N * rise - 60}" fill="none" stroke="#d3cdc2" stroke-width="4"/><rect x="1074" y="160" width="6" height="40" rx="3" fill="#2c2c2a"/>`;
// Boden
s += `<rect y="${y0}" width="1200" height="${900 - y0}" fill="${alt ? '#a8865a' : '#c9ad86'}"/>`;
// Treppenwange und Stufen
const top = y0 - N * rise, xe = x0 + N * run;
s += `<path d="M${x0} ${y0}L${xe} ${top}H1200V${top + 60}H${xe + 40}L${x0 + 90} ${y0}Z" fill="${alt ? '#7b5132' : '#e3ded5'}"/>`;
for (let i = 0; i < N; i++) {
const x = x0 + i * run, y = y0 - (i + 1) * rise;
s += `<rect x="${x}" y="${y}" width="${run}" height="${rise}" fill="${alt ? '#8b3a30' : '#f4f2ee'}"/>`;
s += alt ? `<rect x="${x - 4}" y="${y - 4}" width="${run + 8}" height="10" rx="5" fill="#7a3128"/><path d="M${x} ${y + 18}h${run}" stroke="#a24a3d" stroke-width="2" opacity=".6"/>`
: `<rect x="${x - 6}" y="${y - 8}" width="${run + 10}" height="12" fill="#c69c6d"/><rect x="${x - 6}" y="${y + 2}" width="${run + 10}" height="3" fill="#a87f53"/><rect x="${x + 6}" y="${y + rise - 12}" width="${run - 12}" height="3" fill="#fff4d8" opacity=".9"/>`;
}
s += alt ? `<rect x="${xe}" y="${top}" width="${1200 - xe}" height="10" fill="#7a3128"/>` : `<rect x="${xe - 6}" y="${top - 8}" width="${1206 - xe}" height="12" fill="#c69c6d"/>`;
// Geländer: Handlauf parallel zur Stufenkante
const hl = 150, hx = i => x0 + i * run + run / 2, hy = i => y0 - (i + 1) * rise - 8;
for (let i = 0; i < N; i++) {
const x = hx(i), yb = hy(i), yt = yb - hl + 4;
s += alt ? `<path d="M${x - 5} ${yb}V${yt}h10V${yb}Z" fill="#6b4228"/><ellipse cx="${x}" cy="${yb - 40}" rx="9" ry="12" fill="#6b4228"/><ellipse cx="${x}" cy="${yb - 95}" rx="8" ry="10" fill="#6b4228"/><rect x="${x - 7}" y="${yb - 70}" width="14" height="6" fill="#5a361f"/>`
: `<rect x="${x - 2}" y="${yt}" width="4" height="${hl - 4}" fill="#2c2c2a"/>`;
}
const hxa = x0 - 20, hya = y0 - hl, hxe = xe + 20, hye = top - hl - 8;
s += alt ? `<path d="M${hxa} ${hya}L${hxe} ${hye}" stroke="#5a361f" stroke-width="18" stroke-linecap="round"/><rect x="${x0 - 30}" y="${y0 - hl - 20}" width="30" height="${hl + 20}" fill="#5a361f"/><circle cx="${x0 - 15}" cy="${y0 - hl - 24}" r="16" fill="#6b4228"/>`
: `<path d="M${hxa} ${hya}L${hxe} ${hye}" stroke="#c69c6d" stroke-width="12" stroke-linecap="round"/><path d="M${hxa} ${hya + 8}L${hxe} ${hye + 8}" stroke="#2c2c2a" stroke-width="4"/><rect x="${x0 - 24}" y="${y0 - hl}" width="6" height="${hl}" fill="#2c2c2a"/>`;
// Lampe
s += alt ? `<rect x="597" y="0" width="6" height="70" fill="#6b4a2c"/><circle cx="600" cy="96" r="34" fill="#f2e6c4"/><circle cx="600" cy="96" r="34" fill="none" stroke="#cbb48a" stroke-width="3"/>`
: `<rect x="599" y="0" width="2" height="120" fill="#2c2c2a"/><path d="M576 120h48l-10 34h-28z" fill="#2c2c2a"/><ellipse cx="600" cy="156" rx="14" ry="4" fill="#fff4d8"/>`;
// Bild an der Wand (vorher) bzw. nichts
if (alt) s += `<rect x="410" y="150" width="110" height="82" fill="#cbb48a"/><rect x="419" y="159" width="92" height="64" fill="#8aa0a5"/><path d="M419 214l28-24 22 16 20-13 22 21z" fill="#6b7f6a"/>`;
const defs = `<linearGradient id="${z}tf" x2="0" y2="1"><stop offset="0" stop-color="#d4dcde"/><stop offset="1" stop-color="#eee7da"/></linearGradient>`;
return svgDoc(z, s, defs);
}
// Dachschräge, Kinderzimmer in Wewer
function schraege(z) {
const alt = z === 'vorher';
// Schräge von (0,120) nach (1200,520), Kniestock bis Boden 800
const yS = x => 120 + x / 3;
let s = `<rect width="1200" height="900" fill="${alt ? '#e6dcc6' : '#ece9e3'}"/><path d="M0 0H1200V${yS(1200)}L0 ${yS(0)}Z" fill="${alt ? '#d8cdb5' : '#e3dfd7'}"/>`;
s += `<path d="M0 ${yS(0)}L1200 ${yS(1200)}" stroke="${alt ? '#cbbd9e' : '#d3cdc2'}" stroke-width="3"/>`;
if (alt) s += `<ellipse cx="720" cy="380" rx="70" ry="40" fill="#c2ad7e" opacity=".28"/><ellipse cx="736" cy="392" rx="40" ry="22" fill="#b39a66" opacity=".22"/>`;
// Dachfenster in der Schräge
s += `<path d="M260 ${yS(260) - 160}L460 ${yS(460) - 160}V${yS(460) - 20}L260 ${yS(260) - 20}Z" fill="${alt ? '#7c6a55' : '#4b4a45'}"/><path d="M276 ${yS(276) - 146}L444 ${yS(444) - 146}V${yS(444) - 34}L276 ${yS(276) - 34}Z" fill="url(#${z}df)"/>`;
// Boden
s += `<rect y="800" width="1200" height="100" fill="${alt ? '#8f8b84' : '#c9ad86'}"/>`;
if (!alt) s += rep(4, i => `<path d="M0 ${818 + i * 22}H1200" stroke="#b39469" stroke-width="2"/>`);
if (alt) {
// Freistehendes Regal, Kartons, Wäscheständer, alte Kommode
s += `<rect x="80" y="520" width="200" height="280" fill="none" stroke="#b9b2a2" stroke-width="8"/><path d="M80 610h200M80 700h200" stroke="#b9b2a2" stroke-width="8"/>`;
s += `<rect x="96" y="560" width="70" height="46" fill="#c39b62"/><rect x="176" y="574" width="86" height="32" fill="#d7b57a"/><rect x="100" y="644" width="56" height="52" fill="#9aa3a6"/><rect x="164" y="660" width="40" height="36" fill="#c39b62"/><rect x="96" y="736" width="170" height="60" fill="#e9e4d8"/>`;
s += `<rect x="330" y="640" width="150" height="160" fill="#c39b62"/><path d="M330 690h150" stroke="#a37c45" stroke-width="3"/><rect x="350" y="590" width="110" height="52" fill="#d7b57a"/><rect x="360" y="546" width="80" height="46" fill="#c39b62"/>`;
s += `<path d="M540 800L620 600M700 800L620 600M560 700h120M552 730h136M546 760h148" stroke="#c4c7c9" stroke-width="5" fill="none"/><path d="M580 700v40h22v-40M624 700v56h26v-56" fill="#b7c7cf"/><path d="M566 730v36h18v-36" fill="#d79a8b"/>`;
s += `<rect x="820" y="560" width="300" height="240" fill="#9a6a3c"/>` + rep(3, i => `<rect x="836" y="${578 + i * 72}" width="268" height="60" fill="none" stroke="#7a4f2a" stroke-width="4"/><rect x="950" y="${602 + i * 72}" width="40" height="8" rx="4" fill="#d9b25c"/>`);
s += `<rect x="860" y="530" width="60" height="30" fill="#7c8a96"/><rect x="940" y="512" width="40" height="48" fill="#d7b57a"/><circle cx="1060" cy="540" r="20" fill="#cf7f68"/>`;
} else {
// Einbauschrank folgt der Schräge, Sitzbank unter dem Dachfenster
const xa = 560, xe = 1200;
s += `<path d="M${xa} ${yS(xa) + 8}L${xe} ${yS(xe) + 8}V780H${xa}Z" fill="#f2f0ec"/>`;
const teile = 5, b = (xe - xa) / teile;
for (let i = 0; i <= teile; i++) { const x = xa + i * b; s += `<path d="M${x} ${yS(x) + 8}V780" stroke="#d3cdc2" stroke-width="3"/>`; }
s += `<path d="M${xa} ${yS(xa) + 8}L${xe} ${yS(xe) + 8}" stroke="#d3cdc2" stroke-width="3"/><path d="M${xa} 640H${xe}" stroke="#d3cdc2" stroke-width="3"/>`;
for (let i = 0; i < teile; i++) { const x = xa + i * b + b / 2; s += `<rect x="${x - 2}" y="650" width="4" height="22" rx="2" fill="#c3a07a"/>`; }
s += `<rect x="${xa}" y="780" width="${xe - xa}" height="20" fill="#4b4a45"/>`;
// Sitzbank mit Schubladen
s += `<rect x="120" y="640" width="440" height="140" fill="#f2f0ec"/><path d="M340 640v140" stroke="#d3cdc2" stroke-width="3"/><rect x="220" y="700" width="40" height="5" rx="2.5" fill="#c3a07a"/><rect x="440" y="700" width="40" height="5" rx="2.5" fill="#c3a07a"/>`;
s += `<rect x="112" y="612" width="456" height="30" fill="#c3a07a"/><rect x="112" y="636" width="456" height="6" fill="#a9875f"/><rect x="130" y="582" width="420" height="32" rx="14" fill="#bdc1a9"/><rect x="150" y="548" width="90" height="40" rx="14" fill="#d3cdc2" transform="rotate(-8 195 568)"/>`;
s += `<rect x="120" y="780" width="440" height="20" fill="#4b4a45"/>`;
s += `<rect x="70" y="400" width="4" height="120" fill="#2c2c2a"/><path d="M50 520h44l-8 28h-28z" fill="#2c2c2a"/>`;
}
const defs = `<linearGradient id="${z}df" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#dfe7ea"/><stop offset="1" stop-color="#b9c6cc"/></linearGradient>`;
return svgDoc(z, s, defs);
}
const SZENEN = [kueche, treppe, schraege];
// Drei Aufträge der (erfundenen) Tischlerei Kleeschulte, Paderborn
const PROJEKTE = [
{ nr: '01', titel: 'Küche', ort: 'Riemekeviertel', jahrAlt: 1986, jahrNeu: 2026, tage: '9 Arbeitstage', gewerke: 'Tischler, Elektro, Fliesen', preis: 'ab 18.400 €',
text: '<b>Küche im Altbau.</b> Eiche rustikal von 1986 raus, Fronten in Salbei und eine geölte Eichenplatte rein. Spüle, Herd und Fenster bleiben, wo sie waren, also auch alle Anschlüsse.',
altV: 'Vorher: Einbauküche aus Eiche rustikal mit Bogenfüllungen, gemusterte Fliesen, Scheibengardine', altN: 'Nachher: grifflose Unterschränke in Salbei, Eichenplatte, offene Eichenregale, Fliesenspiegel in Zementgrau' },
{ nr: '02', titel: 'Treppe', ort: 'Schloß Neuhaus', jahrAlt: 1979, jahrNeu: 2025, tage: '6 Arbeitstage', gewerke: 'Tischler, Schlosser', preis: 'ab 9.800 €',
text: '<b>Treppe im Einfamilienhaus.</b> Teppich runter, die alten Stufen mit massiver Eiche belegt, weiße Setzstufen mit Licht. Das gedrechselte Geländer weicht Flachstahl mit Eichenhandlauf.',
altV: 'Vorher: Holztreppe mit rotem Teppich, gedrechselte Geländerstäbe, gestreifte Tapete', altN: 'Nachher: Stufen aus Eiche, weiße Setzstufen mit Licht, Geländer aus Flachstahl mit Eichenhandlauf' },
{ nr: '03', titel: 'Dachschräge', ort: 'Wewer', jahrAlt: 2018, jahrNeu: 2026, tage: '4 Arbeitstage', gewerke: 'Tischler', preis: 'ab 6.200 €',
text: '<b>Kinderzimmer unter dem Dach.</b> Kartons und Kommode raus, ein Einbauschrank, der der Schräge auf den Zentimeter folgt, und eine Sitzbank mit Schubladen unter dem Dachfenster.',
altV: 'Vorher: Dachzimmer mit Regal, Kartons, Wäscheständer und alter Kommode', altN: 'Nachher: Einbauschrank entlang der Schräge, Sitzbank mit Schubladen unter dem Dachfenster' }
];
const reiter = $('[data-reiter]'), zahlen = $('[data-zahlen]');
PROJEKTE.forEach((k, i) => {
const b = document.createElement('button');
b.type = 'button'; b.className = 'projekt'; b.setAttribute('role', 'tab'); b.dataset.i = i;
b.innerHTML = `<span class="nr gewerk">${k.nr}</span><span>${k.titel} <span class="ort">· ${k.ort}</span></span><span class="tage gewerk">${k.tage}</span>`;
b.addEventListener('click', () => { stoppVorfuehrung(); zeigeProjekt(i); });
reiter.append(b);
});
reiter.addEventListener('keydown', e => {
if (!['ArrowDown', 'ArrowUp'].includes(e.key)) return;
e.preventDefault(); const n = (aktiv + (e.key === 'ArrowDown' ? 1 : -1) + PROJEKTE.length) % PROJEKTE.length;
zeigeProjekt(n); reiter.children[n].focus();
});
function fuellen(k) {
const i = PROJEKTE.indexOf(k), szene = SZENEN[i];
elVorher.innerHTML = szene('vorher'); elNachher.innerHTML = szene('nachher');
elVorher.parentNode.setAttribute('aria-label', k.altV); elNachher.parentNode.setAttribute('aria-label', k.altN);
chipV.textContent = `Vorher · ${k.jahrAlt}`; chipN.textContent = `Nachher · ${k.jahrNeu}`;
beschreibungEl.innerHTML = k.text;
zahlen.innerHTML = `<div><dt class="gewerk">Bauzeit</dt><dd>${k.tage}</dd></div><div><dt class="gewerk">Gewerke</dt><dd>${k.gewerke}</dd></div><div><dt class="gewerk">Preis</dt><dd>${k.preis}</dd></div>`;
}
let aktiv = -1, wechselUhr = null;
function zeigeProjekt(i) {
if (i === aktiv) return;
const erst = aktiv < 0; aktiv = i;
[...reiter.children].forEach((b, j) => { b.setAttribute('aria-selected', j === i); b.tabIndex = j === i ? 0 : -1; });
clearTimeout(wechselUhr);
if (erst || reduziert()) { fuellen(PROJEKTE[i]); return; }
fig.classList.add('wechselt'); // aus 150 ms, ein 280 ms (Schließen schneller als Öffnen)
wechselUhr = setTimeout(() => { fuellen(PROJEKTE[i]); fig.classList.remove('wechselt'); }, 150);
}
// Position der Grenze (0–100, Anteil Vorher von links)
let pos = 50, modus = 'regler';
function setzePos(v) {
pos = Math.max(0, Math.min(100, v));
const r = Math.round(pos * 10) / 10;
fig.style.setProperty('--pos', r + '%');
regler.value = Math.round(pos);
regler.setAttribute('aria-valuetext', `${Math.round(100 - pos)} % Nachher sichtbar`);
chipV.classList.toggle('weg', pos < 22);
chipN.classList.toggle('weg', pos > 78);
zeigeWerte();
}
function zeigeWerte() {
if (modus === 'lupe') werteEl.innerHTML = `Lupe ${Math.round(LUPE())} px · <b>round 12px</b> · Lerp <b>0,2</b>`;
else if (modus === 'hover') werteEl.innerHTML = `Grenze folgt dem Zeiger · <b>${Math.round(pos)} %</b>`;
else werteEl.innerHTML = `clip-path: inset(0 0 0 <b>${Math.round(pos)}%</b>) · Griff 38 × 30`;
}
// Kurve cubic-bezier(0.5, 0.1, 0.2, 1) als Funktion (für Sprünge und die Vorführung)
function bezier(x1, y1, x2, y2) {
const A = (a, b) => 1 - 3 * b + 3 * a, B = (a, b) => 3 * b - 6 * a, C = a => 3 * a;
const f = (t, a, b) => ((A(a, b) * t + B(a, b)) * t + C(a)) * t;
const d = (t, a, b) => 3 * A(a, b) * t * t + 2 * B(a, b) * t + C(a);
return x => { if (x <= 0 || x >= 1) return x; let t = x; for (let i = 0; i < 8; i++) { const e = f(t, x1, x2) - x; if (Math.abs(e) < 1e-5) break; t -= e / (d(t, x1, x2) || 1e-6); } return f(t, y1, y2); };
}
const KURVE = bezier(0.5, 0.1, 0.2, 1);
let tweenId = 0;
function gleite(ziel, dauer = 350) {
const id = ++tweenId, von = pos;
if (reduziert()) { setzePos(ziel); return Promise.resolve(); }
return new Promise(res => {
const t0 = performance.now();
const schritt = t => {
if (id !== tweenId) return res();
const p = Math.min(1, (t - t0) / dauer);
setzePos(von + (ziel - von) * KURVE(p));
p < 1 ? requestAnimationFrame(schritt) : res();
};
requestAnimationFrame(schritt);
});
}
const stoppGleiten = () => { tweenId++; };
// Regler: direkt, ohne Easing (gemessen)
regler.addEventListener('input', () => { stoppVorfuehrung(); stoppGleiten(); if (modus === 'lupe') setzeModus('regler'); setzePos(+regler.value); });
regler.addEventListener('pointerdown', () => fig.classList.add('ziehen'));
addEventListener('pointerup', () => fig.classList.remove('ziehen'));
const anteil = e => { const r = fig.getBoundingClientRect(); return ((e.clientX - r.left) / r.width) * 100; };
// Klick auf das Blatt springt mit der Kurve dorthin (350 ms)
fig.addEventListener('click', e => { if (modus !== 'regler' || e.target === regler) return; stoppVorfuehrung(); gleite(anteil(e)); });
// Hover-Variante
fig.addEventListener('pointermove', e => {
if (e.pointerType !== 'mouse' || !echteMaus(e)) return;
if (modus === 'hover') { stoppGleiten(); setzePos(anteil(e)); }
if (modus === 'lupe') lupeZiel(e.clientX, e.clientY);
});
fig.addEventListener('pointerenter', e => { if (e.pointerType === 'mouse' && echteMaus(e) && modus === 'lupe') { lupeZiel(e.clientX, e.clientY, true); lupeAuf(true); } });
fig.addEventListener('pointerleave', e => {
if (e.pointerType !== 'mouse' || !echteMaus(e)) return;
if (modus === 'hover') gleite(50);
if (modus === 'lupe') lupeAuf(false);
});
// Echte Mausbewegung beendet die Vorführung
const echteMaus = e => { if (vorfuehrt && e.isTrusted) stoppVorfuehrung(); return true; };
// Lupe: Fenster folgt gelerpt (0,2 pro Bild bei 60 Hz), Größe wächst per transition 350 ms
const LUPE = () => Math.min(240, fig.offsetWidth * 0.42);
const LERP = 0.2;
const ziel = { x: 0, y: 0 }, ist = { x: 0, y: 0 };
let lupeLaeuft = false, letzte = 0;
function lupeZiel(cx, cy, sofort) {
const r = fig.getBoundingClientRect();
ziel.x = cx - r.left; ziel.y = cy - r.top;
if (sofort || reduziert()) { ist.x = ziel.x; ist.y = ziel.y; malLupe(); }
if (!lupeLaeuft) { lupeLaeuft = true; letzte = performance.now(); requestAnimationFrame(lupeBild); }
}
function malLupe() { fig.style.setProperty('--lx', ist.x.toFixed(1) + 'px'); fig.style.setProperty('--ly', ist.y.toFixed(1) + 'px'); }
function lupeBild(t) {
const dt = Math.min(64, t - letzte) / 16.667; letzte = t;
const k = reduziert() ? 1 : 1 - Math.pow(1 - LERP, dt);
ist.x += (ziel.x - ist.x) * k; ist.y += (ziel.y - ist.y) * k; malLupe();
if (Math.abs(ziel.x - ist.x) < 0.01 && Math.abs(ziel.y - ist.y) < 0.01) { lupeLaeuft = false; return; }
requestAnimationFrame(lupeBild);
}
function lupeAuf(an) {
fig.style.setProperty('--lupe', an ? LUPE() + 'px' : '0px');
fig.classList.toggle('lupe-offen', an);
}
function setzeModus(m) {
modus = m;
fig.classList.toggle('modus-hover', m === 'hover');
fig.classList.toggle('modus-lupe', m === 'lupe');
fig.classList.toggle('klickbar', m === 'regler');
document.querySelectorAll('[data-modus]').forEach(b => b.setAttribute('aria-pressed', b.dataset.modus === m));
if (m !== 'lupe') lupeAuf(false);
if (m !== 'regler') stoppGleiten();
if (m === 'lupe') { ist.x = ziel.x = fig.offsetWidth / 2; ist.y = ziel.y = fig.offsetHeight / 2; malLupe(); }
else setzePos(50);
zeigeWerte();
}
document.querySelectorAll('[data-modus]').forEach(b => b.addEventListener('click', () => { stoppVorfuehrung(); setzeModus(b.dataset.modus); }));
// ——— Vorführung: gedachter Zeiger zieht den Griff, wechselt das Projekt, führt die Lupe ———
let lauf = 0, vorfuehrt = false;
const zp = { x: 0, y: 0 };
const lebt = id => id === lauf && vorfuehrt;
const warte = (ms, id) => new Promise(r => setTimeout(() => r(lebt(id)), ms));
function zeigerAuf(x, y) { zp.x = x; zp.y = y; zeiger.style.transform = `translate(${x - 3}px, ${y - 2}px)`; }
function bewege(fn, dauer, id) { // fn(p) für p 0…1 mit der Kurve
if (reduziert()) { fn(1); return warte(0, id); }
return new Promise(res => {
const t0 = performance.now();
const s = t => { if (!lebt(id)) return res(false); const p = Math.min(1, (t - t0) / dauer); fn(KURVE(p)); p < 1 ? requestAnimationFrame(s) : res(true); };
requestAnimationFrame(s);
});
}
function zeigerZu(x, y, dauer, id) { const ax = zp.x, ay = zp.y; return bewege(p => zeigerAuf(ax + (x - ax) * p, ay + (y - ay) * p), dauer, id); }
const griffPunkt = () => { const r = fig.getBoundingClientRect(); return { x: r.left + r.width * pos / 100, y: r.top + r.height / 2 }; };
async function ziehe(nach, dauer, id) {
const von = pos, r = fig.getBoundingClientRect();
if (reduziert()) { setzePos(nach); zeigerAuf(r.left + r.width * nach / 100, r.top + r.height / 2); return warte(1600, id); }
return bewege(p => { setzePos(von + (nach - von) * p); const g = griffPunkt(); zeigerAuf(g.x + 4, g.y + 6); }, dauer, id);
}
async function runde(id) {
const r0 = fig.getBoundingClientRect();
setzeModus('regler');
if (!zp.x) zeigerAuf(r0.left + r0.width * .82, r0.bottom + 40);
let g = griffPunkt();
if (!await zeigerZu(g.x + 4, g.y + 6, 700, id)) return;
fig.classList.add('ziehen');
if (!await ziehe(20, 1400, id) || !await warte(250, id)) return;
if (!await ziehe(80, 1900, id) || !await warte(250, id)) return;
if (!await ziehe(50, 1200, id)) return;
fig.classList.remove('ziehen');
if (!await warte(500, id)) return;
// nächstes Projekt
const n = (aktiv + 1) % PROJEKTE.length, tab = reiter.children[n].getBoundingClientRect();
if (tab.width && tab.top < innerHeight) { if (!await zeigerZu(tab.left + 60, tab.top + tab.height / 2, 800, id)) return; }
zeigeProjekt(n);
if (!await warte(700, id)) return;
// Lupe
setzeModus('lupe');
const r = fig.getBoundingClientRect();
if (!await zeigerZu(r.left + r.width * .5, r.top + r.height * .5, 900, id)) return;
lupeZiel(zp.x, zp.y, true); lupeAuf(true);
const t0 = performance.now(), dauer = reduziert() ? 0 : 5200;
if (reduziert()) {
for (const [fx, fy] of [[.3, .32], [.62, .55], [.4, .74]]) { zeigerAuf(r.left + r.width * fx, r.top + r.height * fy); lupeZiel(zp.x, zp.y, true); if (!await warte(1500, id)) return; }
} else {
const ok = await new Promise(res => {
const s = t => {
if (!lebt(id)) return res(false);
const p = (t - t0) / dauer; if (p >= 1) return res(true);
const a = p * Math.PI * 2;
zeigerAuf(r.left + r.width * (.5 + .26 * Math.sin(a)), r.top + r.height * (.5 + .22 * Math.sin(a * 2))); // liegende Acht
lupeZiel(zp.x, zp.y);
requestAnimationFrame(s);
};
requestAnimationFrame(s);
});
if (!ok) return;
}
lupeAuf(false);
if (!await zeigerZu(r.right + 30, r.top + r.height * .8, 700, id)) return;
if (!await warte(500, id)) return;
runde(id);
}
function stoppVorfuehrung() {
if (!vorfuehrt) return;
vorfuehrt = false; lauf++; html.classList.remove('vorfuehrung'); fig.classList.remove('ziehen');
}
function neustart() {
stoppVorfuehrung(); stoppGleiten();
setzeModus('regler'); zeigeProjekt(0); setzePos(50);
}
function vorfuehren() {
neustart();
vorfuehrt = true; const id = ++lauf; zp.x = 0;
html.classList.add('vorfuehrung');
runde(id);
}
function pause() { if (vorfuehrt) { vorfuehrt = false; lauf++; fig.classList.remove('ziehen'); } }
function weiter() { if (html.classList.contains('vorfuehrung') && !vorfuehrt) { vorfuehrt = true; runde(++lauf); } }
zeigeProjekt(0); setzeModus('regler');
})();
</script>
</body>
</html>