Zum Inhalt
← Galerien & Slider

Raster am Desktop, Wischreihe am Telefon

Leistungen eines Bremer SHK-Betriebs und zwölf Getriebe-Baureihen als ruhiges Raster, das per Container-Query zur Wischreihe wird; Karussell-Rollen, Pfeile und Marker gibt es nur im schmalen Zustand.

Handwerk, Praxis, Kanzlei, Friseur, Pflege: 4 bis 8 Leistungen oder Teammitglieder, am Desktop alle sichtbar, auf dem Telefon als Wischreihe.

Eine Gruppe gleichrangiger Karten (Leistungen, Team, Produktgruppen) steht auf breiten Flächen als ruhiges Raster, alles auf einen Blick. Wird ihr Container schmal, schaltet eine Container-Query auf eine Wischreihe mit Scroll-Snap um, statt die Karten endlos untereinander zu stapeln. Erst in diesem Zustand bekommt die Reihe Karussell-Rollen, Marker und Pfeile. Variante zweireihig: zwei Reihen scrollen gemeinsam, gut für viele kleine Kacheln.

Am Desktop ist ein Karussell meist die schlechtere Wahl: Inhalte verstecken sich hinter Pfeilen, die kaum jemand klickt. Behörden-Designsysteme wie das der schottischen Regierung verzichten deshalb bewusst auf Karussells. Auf dem Telefon dagegen spart eine Wischreihe Höhe, und die angeschnittene nächste Karte zeigt, dass es weitergeht. Dieses Muster nimmt von beiden die gute Seite. Weil die Umschaltung an der Containerbreite hängt, funktioniert dieselbe Komponente auch in einer schmalen Seitenspalte.

Wann einsetzen

Handwerk, Praxis, Kanzlei, Friseur und Pflege für Leistungen oder Team. Gastro für Gerichte, Industrie und D2C für Produktgruppen (zweireihig). Immer dann, wenn 4 bis 12 gleichrangige Karten am Desktop als Raster gut lesbar sind, auf dem Telefon aber zu viel Höhe kosten würden.

Wann nicht

Wenn die Karten auf dem Telefon einzeln gelesen werden müssen (Preise, Pflichtangaben, Vergleich), dann lieber gestapelt oder als Akkordeon. Nicht bei mehr als etwa 12 Karten, dann Filter oder Liste. Nicht für die wichtigste Information der Seite, die gehört sichtbar an den Anfang. Zweireihig nicht, wenn die Karten viel Text tragen.

Varianten

  • einreihig: Raster mit 3 bzw. 2 Spalten, unter 44rem Container eine Wischreihe mit Karten von 80 % Breite, Zähler, Pfeilen und Markern
  • zweireihig: Raster 6 × 2 bzw. 4 × 3, unter 44rem zwei Reihen mit grid-auto-flow column, eine Spalte (2 Kacheln) pro Schritt; Lesefolge spaltenweise gleich der DOM-Reihenfolge

Umsetzung

Der Abschnitt bekommt container-type inline-size. Breit: display grid mit grid-template-columns repeat(3, minmax(0, 1fr)). In @container (width < 44rem): grid-auto-flow column, grid-auto-columns etwa 80 %, overflow-x auto, scroll-snap-type x mandatory, scroll-padding-inline gleich dem Seitenrand, Karten scroll-snap-align start. Zweireihig: grid-template-rows repeat(2, auto) mit grid-auto-flow column, Schnappen an den Karten der oberen Reihe. Die CSS ist die einzige Quelle für den Zustand: Ein ResizeObserver am Container liest getComputedStyle(band).overflowX und setzt nur im Karussell-Zustand aria-roledescription „Karussell“, role group und „2 von 6“ an den Karten, tabindex 0 am Band und blendet Pfeile und Marker ein (hidden). Native Marker per scroll-marker-group nur innerhalb der Container-Query, sonst eigene Buttons. Lesefolge: DOM-Reihenfolge so wählen, dass sie in beiden Zuständen stimmt; bei zwei Reihen läuft sie spaltenweise.

Robust umsetzen

Telefon. Das ist der Kern: Auf dem Telefon wird aus dem Raster eine Wischreihe mit angeschnittener nächster Karte, Markern und 44-px-Pfeilen. Kein Selbstlauf.

Ohne Bewegung. Pfeile und Marker springen ohne Gleiten. Der Wechsel zwischen Raster und Reihe ist ohnehin ohne Animation.

Barrierefrei. Im Raster-Zustand ist es eine normale Liste ohne Karussell-Rollen. Erst im Karussell-Zustand: aria-roledescription „Karussell“ am Abschnitt, Karten als group mit „2 von 6: Heizung“, Band mit tabindex 0 und Pfeiltasten, Pfeile mit aria-label und aria-controls. Lesefolge in beiden Zuständen gleich der DOM-Reihenfolge, bei zwei Reihen spaltenweise und an den Nummern erkennbar. Kein Querscrollen der Seite bei 320 px.

Code

<!DOCTYPE html>
<!--
  Raster am Desktop, Wischreihe am Telefon
  Fundus · Galerien & Slider · https://cecodesign.de/lab/fundus/galerien-slider/raster-zu-wischreihe/
  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>Raster am Desktop, Wischreihe am Telefon · 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: 'Strichpunkt Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/strichpunkt-sans:vf@latest/latin-full-normal.woff2') format('woff'); font-weight: 400 900; font-stretch: 100% 200%; font-display: swap; }
@font-face { font-family: 'Archivo'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/archivo:vf@latest/latin-full-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-stretch: 62% 125%; 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; }

/* ============ Gemeinsam ============ */
:root { --rand: clamp(20px, 3.6vw, 52px); --g: #EFEDEA; --t: #1A1A1A; --l: #696867; --ln: #CDCBC9; --fokus: #1A1A1A; }
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--g); color: var(--t); -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; overflow-x: hidden; display: grid; grid-template-rows: minmax(0, 1fr) auto; grid-template-columns: minmax(0, 1fr); }
a { color: inherit; text-decoration: none; }
button, input { font: inherit; color: inherit; }
img { display: block; }
:focus-visible { outline: 2px solid var(--fokus); outline-offset: 3px; }
.sr { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.welt { display: flex; flex-direction: column; min-width: 0; }
.welt[hidden] { display: none; }
[hidden] { display: none !important; }
.pfeil svg { width: 18px; height: 18px; display: block; }
/* Die Bühne ist der Container: die Umschaltung hängt an ihrer Breite, nicht am Fenster */
/* Anschnitt: auf der echten Seite läuft die Wischreihe bis an den Bildschirmrand; in der verkleinerten Pult-Bühne bleibt sie im Rahmen */
.buehne { --anschnitt: var(--rand); container: reihe / inline-size; width: 100%; margin-inline: auto; position: relative; }
.buehne[data-schmal] { --anschnitt: 0px; }
.buehne[data-schmal] { outline: 1px dashed var(--l); outline-offset: 0; }
.buehne-mass { position: absolute; top: -22px; left: 0; font: 500 10.5px/1 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--l); }
/* Gemeinsamer Karussell-Zustand (nur in der Container-Query aktiv) */
.reihe { position: relative; list-style: none; display: grid; }
.reihe::-webkit-scrollbar { display: none; }

/* Pult */
.fuss { display: flex; justify-content: space-between; align-items: center; gap: 10px 28px; flex-wrap: wrap; padding: 12px var(--rand) 16px; border-top: 1px solid var(--ln); background: var(--g); }
.mono { font: 500 10.5px/1.45 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--l); }
.modi { display: flex; gap: 4px; flex-wrap: wrap; align-items: center; }
.modi > span, .modi > label { margin-right: 6px; }
.modi button { font: inherit; letter-spacing: inherit; text-transform: inherit; color: var(--l); background: none; border: 1px solid var(--ln); border-radius: 999px; min-height: 30px; padding: 0 12px; cursor: pointer; transition: color 150ms ease-out, background-color 150ms ease-out, border-color 150ms ease-out; }
.modi button:hover { color: var(--t); border-color: var(--l); }
.modi button[aria-pressed=true] { background: var(--t); color: var(--g); border-color: var(--t); }
.modi input[type=range] { width: min(260px, 50vw); accent-color: var(--t); }
.modi output { min-width: 5.5em; color: var(--t); }
.pultspalte { display: grid; gap: 8px; }
.werte { max-width: 680px; text-align: right; }
.werte b { color: var(--t); font-weight: 500; }
.zeiger { position: fixed; left: 0; top: 0; z-index: 30; width: 22px; height: 22px; pointer-events: none; display: none; transition: transform 700ms cubic-bezier(0.33, 1, 0.68, 1); }
.vorfuehrung .zeiger { display: block; }
.vorschau body { overflow: hidden; }

/* ============ Variante „Einreihig“: Ohlsen Haustechnik, Rezept handwerk-werkstatt-klartext ============ */
html[data-v='einreihig'] { --g: #EFEDEA; --t: #1A1A1A; --l: #696867; --ln: #CDCBC9; --fokus: #1A1A1A; }
.w-e { --kurve: cubic-bezier(0.2, 0, 0.38, 0.9); --zement: #D3CDC2; --holz: #C3B4A5; --gruen: #BDC1A9; --signal: #FB6100; font-family: 'Atkinson Hyperlegible Next', system-ui, sans-serif; }
.e-kopf { display: flex; align-items: center; gap: 16px 30px; padding: 14px var(--rand); border-bottom: 1px solid var(--ln); }
.e-marke { display: flex; align-items: baseline; gap: 12px; }
.e-marke b { font: 700 24px/1 'Special Gothic', sans-serif; letter-spacing: -.01em; }
.e-marke span, .e-label, .e-nr, .e-zaehler { font: 600 .8125rem/1.2 'Special Gothic', sans-serif; font-stretch: 75%; letter-spacing: .04em; text-transform: uppercase; }
.e-marke span { color: var(--l); }
.e-kopf nav { display: flex; gap: 24px; margin-left: auto; font-size: 1rem; }
.e-kopf nav a[aria-current] { box-shadow: 0 2px 0 var(--t); }
.e-anruf { min-height: 42px; display: inline-flex; align-items: center; gap: 8px; padding: 0 16px; background: var(--signal); color: #1A1A1A; font-weight: 700; border-radius: 3px; font-variant-numeric: tabular-nums; transition: background-color 150ms var(--kurve); }
.e-anruf:hover { background: #ff7a26; }
.e-main { flex: 1; display: grid; align-content: center; padding: clamp(22px, 3.6svh, 44px) var(--rand) clamp(16px, 3svh, 32px); }
.e-abschnitt { display: grid; gap: clamp(14px, 2.4svh, 24px); }
.e-titel { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr) auto; gap: 12px 40px; align-items: end; }
.e-label { display: block; color: var(--l); margin-bottom: 10px; }
.e-titel h2 { font: 700 clamp(1.625rem, 1.1rem + 1.8vw, 2.5rem)/1.05 'Special Gothic', sans-serif; letter-spacing: -.01em; text-wrap: balance; }
.e-titel > p { font-size: 1.0625rem; line-height: 1.55; color: var(--l); max-width: 40ch; }
.e-steuer { display: flex; align-items: center; gap: 8px; }
.e-zaehler { min-width: 4.5em; text-align: center; font-variant-numeric: tabular-nums; color: var(--l); }
.e-pfeil { width: 44px; height: 44px; border-radius: 3px; border: 1.5px solid var(--t); background: transparent; display: grid; place-items: center; cursor: pointer; transition: background-color 150ms var(--kurve), color 150ms var(--kurve), opacity 150ms var(--kurve); }
.e-pfeil:hover:not(:disabled), .e-pfeil.druck { background: var(--t); color: var(--g); }
.e-pfeil:disabled { opacity: .3; cursor: default; }
.e-reihe { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: clamp(12px, 1.4vw, 20px); }
.e-karte { position: relative; display: grid; grid-template-rows: auto 1fr; background: #fff; border-radius: 3px; overflow: hidden; min-width: 0; }
.e-karte h3 a::after { content: ''; position: absolute; inset: 0; }
.e-karte h3 a:focus-visible { outline: none; }
.e-karte:has(h3 a:focus-visible) { outline: 2px solid var(--t); outline-offset: 3px; }
.e-karte h3 a { text-decoration: underline; text-decoration-thickness: 1.5px; text-underline-offset: 4px; text-decoration-color: rgba(26, 26, 26, .35); transition: text-decoration-color 150ms var(--kurve); }
.e-karte:hover h3 a { text-decoration-color: currentColor; }
.e-karte figure { height: clamp(110px, 16svh, 200px); overflow: hidden; background: var(--zement); }
.e-karte img { width: 100%; height: 100%; object-fit: cover; transition: scale 400ms var(--kurve); }
@media (hover: hover) { .e-karte:hover img { scale: 1.04; } }
.e-text { display: grid; gap: 5px; align-content: start; padding: 12px 16px 16px; }
.e-nr { color: var(--l); }
.e-karte h3 { font: 700 1.25rem/1.15 'Special Gothic', sans-serif; letter-spacing: -.005em; }
.e-karte p { font-size: 1rem; line-height: 1.45; color: #3d3c3b; }
.e-karte--typo { background: var(--gruen); }
.e-karte--typo.e-not { background: var(--signal); }
.e-gross { display: grid; align-content: end; height: clamp(110px, 16svh, 200px); padding: 16px; border-bottom: 1px solid rgba(26, 26, 26, .25); }
.e-gross b { font: 700 clamp(2rem, 1.4rem + 1.6vw, 2.8rem)/.95 'Special Gothic', sans-serif; font-stretch: 85%; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.e-gross small { font: 600 .8125rem/1.3 'Special Gothic', sans-serif; font-stretch: 75%; letter-spacing: .04em; text-transform: uppercase; margin-bottom: 8px; }
.e-karte--typo p { color: #1A1A1A; }
.e-marker { display: flex; justify-content: center; gap: 4px; }
.e-marker button { width: 44px; height: 24px; border: 0; background: none; cursor: pointer; display: grid; place-items: center; }
.e-marker button::before { content: ''; width: 22px; height: 4px; border-radius: 2px; background: rgba(26, 26, 26, .25); transition: background-color 150ms var(--kurve), width 150ms var(--kurve); }
.e-marker button[aria-current='true']::before { background: var(--t); width: 36px; }
/* Mittlere Breite: zwei Spalten */
@container reihe (width < 64rem) { .e-reihe { grid-template-columns: repeat(2, minmax(0, 1fr)); } .e-titel { grid-template-columns: minmax(0, 1fr) auto; } .e-titel > p { grid-column: 1 / -1; grid-row: 2; } }
/* Schmal: Wischreihe mit Scroll-Snap, native Marker nur hier */
@container reihe (width < 44rem) {
  .e-reihe { grid-template-columns: none; grid-auto-flow: column; grid-auto-columns: 80%; overflow-x: auto; overscroll-behavior-x: contain; scroll-snap-type: x mandatory; scrollbar-width: none;
    margin-inline: calc(-1 * var(--anschnitt)); padding: 2px var(--anschnitt) 6px; scroll-padding-inline: var(--anschnitt); outline-offset: -2px; }
  .e-reihe > li { scroll-snap-align: start; }
  .e-reihe[data-marker='nativ'] { scroll-marker-group: after; }
  .e-titel { grid-template-columns: minmax(0, 1fr); }
  .e-titel > p { grid-row: auto; }
  .e-steuer { justify-content: space-between; }
}
.e-reihe::scroll-marker-group { display: flex; justify-content: center; gap: 4px; margin-top: 12px; }
.e-reihe > li::scroll-marker { content: '' / attr(data-titel); width: 22px; height: 4px; margin: 10px 11px; border-radius: 2px; background: rgba(26, 26, 26, .25); cursor: pointer; transition: background-color 150ms var(--kurve), width 150ms var(--kurve); }
.e-reihe > li::scroll-marker:target-current { background: var(--t); width: 36px; }
.e-reihe > li::scroll-marker:focus-visible { outline: 2px solid var(--t); outline-offset: 6px; }

/* ============ Variante „Zweireihig“: Tannreuther Antriebstechnik, Rezept industrie-b2b-mittelstand ============ */
html[data-v='zweireihig'] { --g: #F3F3F0; --t: #111417; --l: #4E555B; --ln: rgba(17, 20, 23, .16); --fokus: #0E518D; }
.w-z { --kurve: cubic-bezier(0.25, 1, 0.5, 1); --flaeche: #E4E5E1; --dunkel: #16202A; --signal: #0E518D; --raster: rgba(17, 20, 23, .13); font-family: 'Archivo', system-ui, sans-serif; }
.t-kopf { display: flex; align-items: center; gap: 16px 30px; padding: 14px var(--rand); background: var(--dunkel); color: #fff; }
.t-marke { display: flex; align-items: baseline; gap: 12px; }
.t-marke b { font: 600 22px/1 'Strichpunkt Sans', sans-serif; font-stretch: 116%; letter-spacing: -.03em; }
.t-marke span, .t-code, .t-nr, .t-zaehler, .t-kopf nav a { font: 600 .75rem/1.2 'Archivo', sans-serif; font-stretch: 75%; letter-spacing: .08em; text-transform: uppercase; }
.t-marke span { color: rgba(255, 255, 255, .78); }
.t-kopf nav { display: flex; gap: 24px; margin-left: auto; }
.t-kopf nav a { color: rgba(255, 255, 255, .86); }
.t-kopf nav a[aria-current] { color: #fff; box-shadow: 0 2px 0 #fff; }
.t-anfrage { min-height: 40px; display: inline-flex; align-items: center; padding: 0 16px; background: var(--signal); color: #fff; font-weight: 600; font-size: .9375rem; transition: background-color 200ms var(--kurve); }
.t-anfrage:hover { background: #0b4479; }
.t-main { flex: 1; display: grid; align-content: center; padding: clamp(26px, 4.4svh, 48px) var(--rand) clamp(16px, 3svh, 32px); }
.t-abschnitt { display: grid; gap: clamp(14px, 2.4svh, 24px); }
.t-titel { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr) auto; gap: 12px 40px; align-items: end; }
.t-code { display: block; color: var(--l); margin-bottom: 10px; }
.t-titel h2 { font: 600 clamp(1.75rem, 1.2rem + 1.8vw, 2.75rem)/.98 'Strichpunkt Sans', sans-serif; font-stretch: 110%; letter-spacing: -.02em; text-wrap: balance; }
.t-titel > p { font-size: 1.0625rem; line-height: 1.55; color: var(--l); max-width: 44ch; }
.t-steuer { display: flex; align-items: center; gap: 8px; }
.t-zaehler { min-width: 6em; text-align: center; color: var(--l); font-variant-numeric: tabular-nums; }
.t-pfeil { width: 44px; height: 44px; border: 1px solid var(--t); background: transparent; display: grid; place-items: center; cursor: pointer; transition: background-color 200ms var(--kurve), color 200ms var(--kurve), opacity 200ms var(--kurve); }
.t-pfeil:hover:not(:disabled), .t-pfeil.druck { background: var(--t); color: #fff; }
.t-pfeil:disabled { opacity: .3; cursor: default; }
/* Breit: 6 × 2 Raster mit sichtbaren Rasterlinien */
.t-reihe { grid-template-columns: repeat(6, minmax(0, 1fr)); border-top: 1px solid var(--raster); border-left: 1px solid var(--raster); }
.t-reihe > li { border-right: 1px solid var(--raster); border-bottom: 1px solid var(--raster); min-width: 0; }
.t-kachel { position: relative; display: grid; grid-template-rows: auto 1fr; gap: 8px; height: 100%; padding: 12px 14px 14px; transition: background-color 200ms var(--kurve); }
.t-kachel:hover { background: var(--flaeche); }
.t-kachel:focus-visible { outline-offset: -3px; }
.t-kopfzeile { display: flex; justify-content: space-between; color: var(--l); }
.t-zeichnung { color: var(--t); }
.t-zeichnung svg { width: 100%; height: clamp(70px, 12svh, 112px); display: block; overflow: visible; }
.t-kachel h3 { font: 600 1.0625rem/1.15 'Archivo', sans-serif; }
.t-daten { font-size: .875rem; font-stretch: 87%; color: var(--l); font-variant-numeric: tabular-nums; line-height: 1.4; }
.t-kachel:hover h3 { color: var(--signal); }
@container reihe (width < 64rem) { .t-reihe { grid-template-columns: repeat(4, minmax(0, 1fr)); } .t-titel { grid-template-columns: minmax(0, 1fr) auto; } .t-titel > p { grid-column: 1 / -1; grid-row: 2; } }
/* Schmal: zwei Reihen scrollen gemeinsam, eine Spalte pro Schritt; Lesefolge = Spalten = DOM */
@container reihe (width < 44rem) {
  .t-reihe { grid-template-columns: none; grid-template-rows: repeat(2, auto); grid-auto-flow: column; grid-auto-columns: 46%; overflow-x: auto; overscroll-behavior-x: contain; scroll-snap-type: x mandatory; scrollbar-width: none;
    margin-inline: calc(-1 * var(--anschnitt)); padding-inline: var(--anschnitt); scroll-padding-inline: var(--anschnitt); border-left: 0; outline-offset: -2px; reading-flow: grid-columns; }
  .t-reihe > li { scroll-snap-align: start; }
  .t-reihe > li:nth-child(odd) { border-left: 1px solid var(--raster); }
  .t-reihe > li:nth-child(even) { scroll-snap-align: none; border-left: 1px solid var(--raster); }
  .t-titel { grid-template-columns: minmax(0, 1fr); }
  .t-titel > p { grid-row: auto; }
  .t-steuer { justify-content: space-between; }
}

/* ============ Vorschau, Telefon, Touch, reduziert ============ */
.vorschau .e-kopf nav, .vorschau .t-kopf nav, .vorschau .e-titel > p, .vorschau .t-titel > p { display: none; }
@media (max-width: 1000px) { .e-kopf nav, .t-kopf nav { display: none; } .e-anruf, .t-anfrage { margin-left: auto; } }
@media (max-width: 640px) { .e-marke span, .t-marke span { display: none; } .werte { text-align: left; } .e-anruf .t { display: none; } }
@media (max-width: 360px) { .e-anruf, .t-anfrage { padding: 0 10px; font-size: .8125rem; } }
@media (hover: none) { .zeiger { display: none !important; } }
.reduziert .e-karte img, .reduziert :is(.e-pfeil, .t-pfeil, .t-kachel, .e-marker button::before), .reduziert .e-reihe > li::scroll-marker, .reduziert .zeiger { transition: none; }
@media (forced-colors: active) { button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; } .e-marker button[aria-current='true'] { outline: 2px solid Highlight; } }
</style>
</head>
<body>
<!-- ===================== Einreihig: Ohlsen Haustechnik, Bremen-Vegesack ===================== -->
<div class="welt w-e" data-welt="einreihig">
<header class="e-kopf">
  <a class="e-marke" href="#"><b>Ohlsen</b><span>Haustechnik seit 1987 · Bremen-Vegesack</span></a>
  <nav aria-label="Hauptnavigation"><a href="#" aria-current="page">Leistungen</a><a href="#">Referenzen</a><a href="#">Team</a><a href="#">Ausbildung</a></nav>
  <a class="e-anruf" href="tel:+494216658300"><span class="t">Anrufen</span> 0421 665 830</a>
</header>
<main class="e-main">
  <div class="buehne" data-buehne>
    <section class="e-abschnitt" aria-labelledby="e-titel" data-abschnitt>
      <div class="e-titel">
        <div><span class="e-label">Leistungen · Bremen-Nord und Umland</span><h2 id="e-titel">Bad, Heizung, Wärmepumpe. Aus einer Hand.</h2></div>
        <p>Zwölf Leute, drei Gewerke, ein fester Ansprechpartner. Wir kommen vorher gucken, dann gibt es einen Festpreis.</p>
        <div class="e-steuer" data-steuer hidden>
          <button type="button" class="e-pfeil pfeil" data-schritt="-1" aria-label="Vorherige Leistung" aria-controls="e-reihe"><svg viewBox="0 0 18 18" aria-hidden="true"><path d="M15 9H3m5-5L3 9l5 5" fill="none" stroke="currentColor" stroke-width="1.6"/></svg></button>
          <p class="e-zaehler" aria-hidden="true" data-zaehler>01 / 06</p>
          <button type="button" class="e-pfeil pfeil" data-schritt="1" aria-label="Nächste Leistung" aria-controls="e-reihe"><svg viewBox="0 0 18 18" aria-hidden="true"><path d="M3 9h12m-5-5 5 5-5 5" fill="none" stroke="currentColor" stroke-width="1.6"/></svg></button>
        </div>
      </div>
      <ul class="reihe e-reihe" id="e-reihe" data-reihe data-marker="nativ">
        <li class="e-karte" data-titel="Bad und Sanitär"><figure><img src="bilder/handwerk-bad.webp" width="1500" height="1000" alt="Neues Bad mit bodengleicher Dusche, Wandnische und Holzhocker, Waschtisch vor salbeigrüner Wand"></figure><div class="e-text"><p class="e-nr">01 · Sanitär</p><h3><a href="#">Bad und Sanitär</a></h3><p>Komplettbad in drei Wochen, von der Fliese bis zur Steckdose.</p></div></li>
        <li class="e-karte" data-titel="Heizung"><figure><img src="bilder/handwerk-heizraum-hoch.webp" width="800" height="1000" alt="Aufgeräumter Heizraum mit Warmwasserspeicher, isolierten Rohren und Heizkreisverteiler" loading="lazy"></figure><div class="e-text"><p class="e-nr">02 · Heizung</p><h3><a href="#">Heizung</a></h3><p>Gas, Öl, Pellets: Wartung, Tausch und hydraulischer Abgleich.</p></div></li>
        <li class="e-karte" data-titel="Wärmepumpe"><figure><img src="bilder/handwerk-waermepumpe.webp" width="1200" height="800" alt="Außengerät einer Wärmepumpe auf einem Betonsockel an der Hauswand, Kies und Rasen" loading="lazy"></figure><div class="e-text"><p class="e-nr">03 · Energie</p><h3><a href="#">Wärmepumpe</a></h3><p>Wir rechnen vorher, ob Ihr Haus passt, und helfen beim Förderantrag.</p></div></li>
        <li class="e-karte" data-titel="Barrierefreies Bad"><figure><img src="bilder/handwerk-bad-hoch.webp" width="800" height="1000" alt="Waschtisch mit Holzunterschrank unter einem runden Spiegel vor salbeigrüner Wand" loading="lazy" style="object-position: 50% 62%"></figure><div class="e-text"><p class="e-nr">04 · Umbau</p><h3><a href="#">Barrierefreies Bad</a></h3><p>Bodengleiche Dusche, Griffe, breite Tür. Zuschüsse klären wir mit.</p></div></li>
        <li class="e-karte e-karte--typo e-not" data-titel="Notdienst"><div class="e-gross"><small>Rohrbruch · Heizung aus</small><b>0421 665 830</b></div><div class="e-text"><p class="e-nr">05 · Notdienst</p><h3><a href="tel:+494216658300">Notdienst rund um die Uhr</a></h3><p>Nachts und am Wochenende ist ein Meister erreichbar.</p></div></li>
        <li class="e-karte e-karte--typo" data-titel="Wartungsvertrag"><div class="e-gross"><small>Wartung pro Jahr</small><b>ab 129 €</b></div><div class="e-text"><p class="e-nr">06 · Wartung</p><h3><a href="#">Wartungsvertrag</a></h3><p>Heizung oder Wärmepumpe. Den Termin schlagen wir vor.</p></div></li>
      </ul>
      <div class="e-marker" role="group" aria-label="Leistung wählen" data-eigene-marker hidden></div>
      <p class="sr" aria-live="polite" aria-atomic="true" data-status></p>
    </section>
  </div>
</main>
</div>

<!-- ===================== Zweireihig: Tannreuther Antriebstechnik, Schweinfurt ===================== -->
<div class="welt w-z" data-welt="zweireihig" hidden>
<header class="t-kopf">
  <a class="t-marke" href="#"><b>Tannreuther</b><span>Antriebstechnik · Schweinfurt</span></a>
  <nav aria-label="Hauptnavigation"><a href="#" aria-current="page">Produkte</a><a href="#">Branchen</a><a href="#">Service</a><a href="#">Downloads</a></nav>
  <a class="t-anfrage" href="#">Projekt anfragen</a>
</header>
<main class="t-main">
  <div class="buehne" data-buehne>
    <section class="t-abschnitt" aria-labelledby="t-titel" data-abschnitt>
      <div class="t-titel">
        <div><span class="t-code">02 · Produktgruppen</span><h2 id="t-titel">Zwölf Baureihen, ein Baukasten</h2></div>
        <p>Getriebe, Motoren und Zubehör aus einer Fertigung. Jede Baureihe mit Datenblatt, CAD und Lieferzeit.</p>
        <div class="t-steuer" data-steuer hidden>
          <button type="button" class="t-pfeil pfeil" data-schritt="-1" aria-label="Vorherige Produktgruppen" aria-controls="t-reihe"><svg viewBox="0 0 18 18" aria-hidden="true"><path d="M15 9H3m5-5L3 9l5 5" fill="none" stroke="currentColor" stroke-width="1.3"/></svg></button>
          <p class="t-zaehler" aria-hidden="true" data-zaehler>01–04 / 12</p>
          <button type="button" class="t-pfeil pfeil" data-schritt="1" aria-label="Nächste Produktgruppen" aria-controls="t-reihe"><svg viewBox="0 0 18 18" aria-hidden="true"><path d="M3 9h12m-5-5 5 5-5 5" fill="none" stroke="currentColor" stroke-width="1.3"/></svg></button>
        </div>
      </div>
      <ul class="reihe t-reihe" id="t-reihe" data-reihe></ul>
      <p class="sr" aria-live="polite" aria-atomic="true" data-status></p>
    </section>
  </div>
</main>
</div>

<footer class="fuss mono">
  <div class="pultspalte">
    <div class="modi" role="group" aria-label="Variante">
      <span>Variante</span>
      <button type="button" data-v-wahl="einreihig" aria-pressed="true">Einreihig</button>
      <button type="button" data-v-wahl="zweireihig" aria-pressed="false">Zweireihig</button>
    </div>
    <div class="modi">
      <label for="breite">Container</label>
      <input type="range" id="breite" min="0" max="100" value="100" step="1" data-breite>
      <output for="breite" data-breite-wert>100 %</output>
      <button type="button" data-breite-knopf="100">Breit</button>
      <button type="button" data-breite-knopf="36">Seitenspalte</button>
      <button type="button" data-breite-knopf="0">Telefon</button>
    </div>
  </div>
  <p class="werte" data-werte></p>
</footer>

<svg class="zeiger" data-zeiger viewBox="0 0 22 22" aria-hidden="true"><path d="M3 2 18.5 11l-7 1.5-3.4 6.5Z" fill="#222" stroke="#fff" stroke-width="1.4" stroke-linejoin="round"/></svg>
<script>
var STEUER = {};
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const werteEl = document.querySelector('[data-werte]');
const zeiger = document.querySelector('[data-zeiger]');
const zwei = i => String(i + 1).padStart(2, '0');
const markerOk = CSS.supports('selector(::scroll-marker)');

function bezier(x1, y1, x2, y2) {
  const cx = 3 * x1, bx = 3 * (x2 - x1) - cx, ax = 1 - cx - bx, cy = 3 * y1, by = 3 * (y2 - y1) - cy, ay = 1 - cy - by;
  const X = t => ((ax * t + bx) * t + cx) * t, Y = t => ((ay * t + by) * t + cy) * t, dX = t => (3 * ax * t + 2 * bx) * t + cx;
  return x => {
    if (x <= 0) return 0; if (x >= 1) return 1;
    let t = x;
    for (let i = 0; i < 8; i++) { const e = X(t) - x; if (Math.abs(e) < 1e-5) return Y(t); const d = dX(t); if (Math.abs(d) < 1e-6) break; t -= e / d; }
    let lo = 0, hi = 1; t = x;
    for (let i = 0; i < 24; i++) { if (X(t) < x) lo = t; else hi = t; t = (lo + hi) / 2; }
    return Y(t);
  };
}
function tween(ms, kurve, fn) {   // kleine Zeitleiste für die Containerbreite der Vorführung
  return new Promise(r => {
    if (reduziert() || !ms) { fn(1); r(); return; }
    const t0 = performance.now();
    const s = j => { const p = Math.min(1, (j - t0) / ms); fn(kurve(p)); if (p < 1) requestAnimationFrame(s); else r(); };
    requestAnimationFrame(s);
  });
}

/* Reihe: liest den Zustand aus der CSS (overflow-x der Reihe) und setzt Rollen, Pfeile und Marker nur im Karussell-Zustand */
function Reihe(welt, opt) {
  const buehne = welt.querySelector('[data-buehne]'), abschnitt = welt.querySelector('[data-abschnitt]'), band = welt.querySelector('[data-reihe]');
  const steuer = welt.querySelector('[data-steuer]'), zaehler = welt.querySelector('[data-zaehler]'), status = welt.querySelector('[data-status]');
  const knoepfe = [...welt.querySelectorAll('[data-schritt]')], eigene = welt.querySelector('[data-eigene-marker]');
  const items = () => [...band.children];
  const schrittweite = opt.schritt || 1;
  let karussell = null, aktiv = 0, anim = 0, gleitet = false, ruheUhr = 0;
  const pad = () => parseFloat(getComputedStyle(band).scrollPaddingInlineStart) || 0;
  const max = () => band.scrollWidth - band.clientWidth;
  const pos = i => Math.max(0, Math.min(items()[i].offsetLeft - pad(), max()));
  function index() {
    const x = band.scrollLeft; let b = 0, d = Infinity;
    items().forEach((s, i) => { if (i % schrittweite) return; const e = Math.abs(pos(i) - x); if (e < d - .5) { d = e; b = i; } });
    return b;
  }
  function sichtbar() {   // ganz sichtbare Karten: Grundlage für Zähler und Ansage
    const l = band.scrollLeft, r = l + band.clientWidth;
    const v = items().map((k, i) => (k.offsetLeft >= l - 2 && k.offsetLeft + k.offsetWidth <= r + 2 ? i : -1)).filter(i => i >= 0);
    return v.length ? [v[0], v[v.length - 1]] : [aktiv, aktiv];
  }
  function anzeigen() {
    if (!karussell) return;
    const [a, z] = sichtbar(), n = items().length;
    zaehler.textContent = `${zwei(a)}${z > a ? '–' + zwei(z) : ''} / ${String(n).padStart(2, '0')}`;
    knoepfe[0].disabled = band.scrollLeft < 2 && aktiv <= 0;
    knoepfe[1].disabled = pos(aktiv) >= max() - 1;
    if (eigene) [...eigene.children].forEach((b, i) => b.setAttribute('aria-current', String(i === (pos(aktiv) >= max() - 1 ? n - 1 : aktiv))));
  }
  function ruhe() { if (!karussell) return; anzeigen(); const [a, z] = sichtbar(); status.textContent = `${opt.wort} ${a + 1}${z > a ? ' bis ' + (z + 1) : ''} von ${items().length}`; }
  function abbrechen() { if (!gleitet) return; cancelAnimationFrame(anim); gleitet = false; band.style.scrollSnapType = ''; }
  function zu(i) {
    const n = items().length; i = Math.max(0, Math.min(n - 1, i)); i -= i % schrittweite;
    aktiv = i; abbrechen();
    const start = band.scrollLeft, d = pos(i) - start;
    if (reduziert() || Math.abs(d) < 1) { band.scrollLeft = pos(i); anzeigen(); clearTimeout(ruheUhr); ruheUhr = setTimeout(ruhe, 60); return; }
    band.style.scrollSnapType = 'none'; gleitet = true;
    const t0 = performance.now();
    const s = j => { const p = Math.min(1, (j - t0) / opt.dauer); band.scrollLeft = start + d * opt.kurve(p); if (p < 1) anim = requestAnimationFrame(s); else { gleitet = false; band.style.scrollSnapType = ''; ruhe(); } };
    anim = requestAnimationFrame(s); anzeigen();
  }
  band.addEventListener('scroll', () => { if (gleitet || !karussell) return; aktiv = index(); anzeigen(); clearTimeout(ruheUhr); ruheUhr = setTimeout(ruhe, 140); }, { passive: true });
  ['wheel', 'touchstart', 'pointerdown'].forEach(t => band.addEventListener(t, abbrechen, { passive: true }));
  band.addEventListener('keydown', e => {
    if (!karussell || e.target !== band) return;
    const z = { ArrowRight: aktiv + schrittweite, ArrowLeft: aktiv - schrittweite, Home: 0, End: items().length - 1 }[e.key];
    if (z === undefined) return; e.preventDefault(); zu(z);
  });
  knoepfe.forEach(k => k.addEventListener('click', () => zu(aktiv + +k.dataset.schritt * schrittweite)));
  if (eigene) {
    eigene.innerHTML = items().map((li, i) => `<button type="button" aria-label="${i + 1} von ${items().length}: ${li.dataset.titel}" aria-controls="${band.id}"></button>`).join('');
    [...eigene.children].forEach((b, i) => b.addEventListener('click', () => zu(i)));
  }
  function zustand() {
    const k = getComputedStyle(band).overflowX !== 'visible';
    if (k === karussell) { anzeigen(); return; }
    karussell = k;
    const n = items().length;
    if (k) {
      abschnitt.setAttribute('aria-roledescription', 'Karussell');
      band.tabIndex = 0; band.setAttribute('aria-label', opt.bandName);
      items().forEach((li, i) => { li.setAttribute('role', 'group'); li.setAttribute('aria-roledescription', 'Folie'); li.setAttribute('aria-label', `${i + 1} von ${n}: ${li.dataset.titel}`); });
      steuer.hidden = false;
      if (eigene) eigene.hidden = markerOk && band.dataset.marker === 'nativ';
      band.scrollLeft = 0; aktiv = 0; anzeigen();
    } else {
      abschnitt.removeAttribute('aria-roledescription');
      band.removeAttribute('tabindex'); band.removeAttribute('aria-label');
      items().forEach(li => { li.removeAttribute('role'); li.removeAttribute('aria-roledescription'); li.removeAttribute('aria-label'); });
      steuer.hidden = true; if (eigene) eigene.hidden = true;
      status.textContent = '';
    }
    if (opt.umschalt) opt.umschalt(k);
  }
  new ResizeObserver(zustand).observe(buehne);
  return { zu, weiter: r => zu(aktiv + r * schrittweite), amEnde: () => pos(aktiv) >= max() - 1, istKarussell: () => karussell, knoepfe, buehne, band, zustand };
}

/* Containerbreite: Pult-Regler und Vorführung setzen die Breite der Bühne */
const regler = document.querySelector('[data-breite]'), reglerWert = document.querySelector('[data-breite-wert]');
const MIN = 320;
function voll(buehne) { return buehne.parentElement.clientWidth - parseFloat(getComputedStyle(buehne.parentElement).paddingLeft) - parseFloat(getComputedStyle(buehne.parentElement).paddingRight); }
function setzeBreite(buehne, wert) {   // wert 0–100: 0 = 320 px, 100 = volle Breite
  const v = voll(buehne), px = Math.round(MIN + (v - MIN) * Math.max(0, Math.min(100, wert)) / 100);
  if (wert >= 100 || px >= v) { buehne.style.width = ''; buehne.removeAttribute('data-schmal'); masse(buehne, null); }
  else { buehne.style.width = px + 'px'; buehne.setAttribute('data-schmal', ''); masse(buehne, px); }
  regler.value = Math.round(wert); reglerWert.textContent = wert >= 100 ? '100 %' : px + ' px';
}
function masse(buehne, px) {
  let m = buehne.querySelector('.buehne-mass');
  if (px === null) { if (m) m.remove(); return; }
  if (!m) { m = document.createElement('span'); m.className = 'buehne-mass'; m.setAttribute('aria-hidden', 'true'); buehne.prepend(m); }
  m.textContent = `Container ${px} px · ${px / 16 < 44 ? 'Wischreihe' : 'Raster'}`;
}
const wertFuer = (buehne, px) => (px === 'voll' ? 100 : Math.max(0, Math.min(100, (px - MIN) / Math.max(1, voll(buehne) - MIN) * 100)));

function Vorfuehrung(schritt) {
  let id = 0, uhr = 0;
  const warte = ms => new Promise(r => { uhr = setTimeout(r, ms); });
  return {
    start() { const mein = ++id; html.classList.add('vorfuehrung'); (async () => { while (mein === id) await schritt(() => mein === id, warte); })(); },
    stopp() { id++; clearTimeout(uhr); }
  };
}
function zeigerZu(el) {
  const r = el.getBoundingClientRect();
  if (!r.width || r.bottom < 0 || r.top > innerHeight) return false;
  zeiger.style.transform = `translate(${r.left + r.width / 2 - 4}px, ${r.top + r.height / 2 - 2}px)`; return true;
}
function druck(el) { el.classList.add('druck'); setTimeout(() => el.classList.remove('druck'), 170); }

// Vorführung je Variante: Container schrumpft auf Telefonbreite, Pfeile blättern, dann wieder breit
function baueVorfuehrung(R, kurve) {
  return Vorfuehrung(async (ok, warte) => {
    const b = R.buehne, start = +regler.value;
    await warte(1000); if (!ok()) return;
    await tween(1100, kurve, p => setzeBreite(b, start + (wertFuer(b, 360) - start) * p)); if (!ok()) return;
    await warte(500);
    for (let i = 0; i < 3 && ok(); i++) {
      const k = R.knoepfe[1]; const s = zeigerZu(k); await warte(s ? 700 : 300); if (!ok()) return;
      druck(k); R.weiter(1); await warte(1100);
    }
    if (!ok()) return;
    const jetzt = +regler.value;
    await tween(1100, kurve, p => setzeBreite(b, jetzt + (100 - jetzt) * p));
    await warte(1400);
  });
}

/* ============ Einreihig: Ohlsen Haustechnik ============ */
(() => {
  const welt = document.querySelector('[data-welt=einreihig]');
  const K = bezier(0.2, 0, 0.38, 0.9);
  const R = Reihe(welt, { dauer: 400, kurve: K, wort: 'Leistung', bandName: 'Leistungen, wischen oder Pfeiltasten', umschalt: () => { if (html.dataset.v === 'einreihig') werteEl.innerHTML = STEUER.einreihig.werte(); } });
  const V = baueVorfuehrung(R, K);
  STEUER.einreihig = {
    R,
    werte: () => (R.istKarussell()
      ? `Zustand <b>Wischreihe</b> (Container &lt; 44rem) · Karte <b>80 %</b>, snap <b>start</b>, scroll-padding <b>= Rand</b> · Marker <b>${markerOk ? 'scroll-marker-group: after (nur in der Container-Query)' : 'eigene Buttons'}</b> · Rollen <b>Karussell/Folie nur jetzt</b> · Pfeil <b>400 ms cubic-bezier(0.2, 0, 0.38, 0.9)</b>`
      : `Zustand <b>Raster</b> · <b>3 Spalten</b> ab 64rem, <b>2</b> ab 44rem Containerbreite · keine Karussell-Rollen, keine Pfeile · Umschalten per <b>@container reihe (width &lt; 44rem)</b>, JS liest nur <b>overflow-x</b>`),
    start() { setzeBreite(R.buehne, 100); R.zustand(); },
    stopp() { V.stopp(); },
    neustart() { V.stopp(); html.classList.remove('vorfuehrung'); setzeBreite(R.buehne, 100); },
    vorfuehren() { V.stopp(); setzeBreite(R.buehne, 100); V.start(); },
    pause() { V.stopp(); },
    weiter() { if (html.classList.contains('vorfuehrung')) V.start(); }
  };
})();

/* ============ Zweireihig: Tannreuther Antriebstechnik ============ */
(() => {
  const welt = document.querySelector('[data-welt=zweireihig]');
  const band = welt.querySelector('[data-reihe]');
  // Technische Strichzeichnungen als SVG (eigene Grafik, keine Fotos)
  const rad = (cx, cy, r, z, h) => { let d = ''; for (let i = 0; i < z; i++) { const a = [0, .2, .5, .7].map(f => (i + f) / z * Math.PI * 2), rr = [r, r + h, r + h, r]; a.forEach((w, j) => { d += (i || j ? 'L' : 'M') + (cx + rr[j] * Math.cos(w)).toFixed(1) + ' ' + (cy + rr[j] * Math.sin(w)).toFixed(1); }); } return d + 'Z'; };
  const L = 'stroke="currentColor" fill="none" stroke-width="1.3" stroke-linejoin="round"', M = 'stroke="#4E555B" fill="none" stroke-width=".8"', S = 'stroke="currentColor" fill="none" stroke-width="1" stroke-dasharray="3 2.5"';
  const mass = (x1, x2, y) => `<path ${M} d="M${x1} ${y - 4}V${y + 4}M${x2} ${y - 4}V${y + 4}M${x1} ${y}H${x2}"/>`;
  const Z = {
    stirnrad: `<rect ${L} x="34" y="20" width="92" height="60" rx="5"/><path ${S} d="${rad(66, 50, 19, 18, 3)}"/><path ${S} d="${rad(101, 50, 11, 11, 3)}"/><path ${L} stroke-width="3" d="M10 50H34M126 50H150"/><path ${L} d="M40 80v8h22v-8M98 80v8h22v-8"/>${mass(34, 126, 10)}`,
    flach: `<rect ${L} x="38" y="30" width="96" height="42" rx="4"/><rect ${L} x="14" y="36" width="24" height="30"/><circle ${L} cx="112" cy="51" r="13"/><circle ${L} cx="112" cy="51" r="6"/><path ${M} d="M112 30V20M112 72v10"/>${mass(14, 134, 90)}`,
    kegelrad: `<rect ${L} x="52" y="18" width="62" height="58" rx="4"/><path ${L} stroke-width="3" d="M14 47H52M83 76v20"/><path ${S} d="M70 36l18 0 6 10H64Z"/><path ${S} d="M74 50h16l-4 14h-8Z"/>${mass(52, 114, 8)}`,
    schnecke: `<path ${L} d="${rad(96, 42, 24, 22, 3)}"/><circle ${L} cx="96" cy="42" r="7"/><rect ${L} x="18" y="72" width="124" height="16" rx="8"/><path ${L} d="M34 72l6 16M46 72l6 16M58 72l6 16M70 72l6 16M82 72l6 16M94 72l6 16M106 72l6 16M118 72l6 16"/>`,
    planeten: `<circle ${L} cx="80" cy="50" r="40"/><circle ${S} cx="80" cy="50" r="33"/><path ${L} d="${rad(80, 50, 8, 10, 2.5)}"/>${[0, 120, 240].map(w => { const a = w * Math.PI / 180, x = 80 + 21 * Math.cos(a), y = 50 + 21 * Math.sin(a); return `<path ${L} d="${rad(x, y, 8.5, 10, 2.5)}"/><circle ${L} cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" r="2"/>`; }).join('')}`,
    zahnstange: `<path ${L} d="${rad(80, 42, 20, 16, 4)}"/><circle ${L} cx="80" cy="42" r="6"/><path ${L} d="M12 70${Array.from({ length: 17 }, (_, i) => `H${16 + i * 8}l2 -5h3l2 5`).join('')}H148V86H12Z"/>${mass(12, 148, 96)}`,
    kupplung: `<path ${L} stroke-width="3" d="M8 50H40M120 50H152"/><rect ${L} x="40" y="28" width="34" height="44" rx="3"/><rect ${L} x="86" y="28" width="34" height="44" rx="3"/><path ${L} d="M74 30l12 8-12 8 12 8-12 8 12 8"/>${mass(40, 120, 86)}`,
    bremse: `<circle ${L} cx="74" cy="52" r="34"/><circle ${L} cx="74" cy="52" r="10"/>${Array.from({ length: 8 }, (_, i) => { const a = i / 8 * Math.PI * 2; return `<circle ${L} cx="${(74 + 22 * Math.cos(a)).toFixed(1)}" cy="${(52 + 22 * Math.sin(a)).toFixed(1)}" r="2.5"/>`; }).join('')}<rect ${L} x="94" y="16" width="30" height="40" rx="4" transform="rotate(28 109 36)"/>`,
    motor: `<rect ${L} x="30" y="30" width="82" height="46" rx="6"/><path ${L} d="${Array.from({ length: 9 }, (_, i) => `M${40 + i * 8} 30V76`).join('')}"/><rect ${L} x="58" y="16" width="26" height="14"/><rect ${L} x="112" y="38" width="10" height="30"/><path ${L} stroke-width="3" d="M122 53H150"/><path ${L} d="M36 76v10h20V76M86 76v10h20V76"/>${mass(30, 122, 96)}`,
    umrichter: `<rect ${L} x="52" y="10" width="56" height="82" rx="3"/><rect ${L} x="60" y="18" width="40" height="18"/><path ${M} d="M64 27h16"/><circle ${L} cx="64" cy="46" r="2.5"/><circle ${L} cx="74" cy="46" r="2.5"/><path ${L} d="${Array.from({ length: 6 }, (_, i) => `M62 ${58 + i * 5}H98`).join('')}"/>`,
    lager: `<circle ${L} cx="62" cy="50" r="34"/><circle ${L} cx="62" cy="50" r="16"/>${Array.from({ length: 10 }, (_, i) => { const a = i / 10 * Math.PI * 2; return `<circle ${L} cx="${(62 + 25 * Math.cos(a)).toFixed(1)}" cy="${(50 + 25 * Math.sin(a)).toFixed(1)}" r="5.5"/>`; }).join('')}<path ${L} d="${rad(126, 64, 13, 12, 3)}"/><circle ${L} cx="126" cy="64" r="4"/>`,
    sonder: `<path ${L} d="M30 76V40l20-18h60l20 18v36Z"/><circle ${S} cx="80" cy="52" r="16"/><path ${M} d="M80 30v44M58 52h44"/>${mass(30, 130, 90)}<path ${M} d="M140 22V76M136 22h8M136 76h8"/><text x="80" y="15" text-anchor="middle" font-family="Archivo, sans-serif" font-size="8" font-stretch="75%" letter-spacing=".8" fill="#4E555B">NACH ZEICHNUNG</text>`
  };
  const GRUPPEN = [
    ['stirnrad', 'TA-S', 'Stirnradgetriebe', '0,12–160 kW · i 1,3–300'], ['flach', 'TA-F', 'Flachgetriebe', 'Hohlwelle 20–110 mm'],
    ['kegelrad', 'TA-K', 'Kegelradgetriebe', '0,12–200 kW · Winkel 90°'], ['schnecke', 'TA-W', 'Schneckengetriebe', 'i 5–100 · selbsthemmend'],
    ['planeten', 'TA-P', 'Planetengetriebe', 'bis 25.000 Nm · spielarm'], ['zahnstange', 'TA-Z', 'Zahnstangen & Ritzel', 'Modul 1–8 · gehärtet'],
    ['kupplung', 'TK', 'Kupplungen', 'Klauen und Lamellen · bis 4.000 Nm'], ['bremse', 'TB', 'Bremsen', 'Federdruck · 24 / 205 V'],
    ['motor', 'TM', 'Drehstrommotoren', 'IE3 und IE4 · 0,09–75 kW'], ['umrichter', 'TU', 'Frequenzumrichter', '0,37–22 kW · STO'],
    ['lager', 'TE', 'Ersatzteile', 'ab Lager · 24 h Versand'], ['sonder', 'TX', 'Sonderbau', 'nach Zeichnung · ab 1 Stück']
  ];
  band.innerHTML = GRUPPEN.map(([z, code, name, daten], i) => `<li data-titel="${name}"><a class="t-kachel" href="#"><div class="t-kopfzeile"><span class="t-nr">${zwei(i)}</span><span class="t-code">${code}</span></div><div><div class="t-zeichnung" aria-hidden="true"><svg viewBox="0 0 160 100">${Z[z]}</svg></div><h3>${name}</h3><p class="t-daten">${daten}</p></div></a></li>`).join('');
  const K = bezier(0.25, 1, 0.5, 1);
  const R = Reihe(welt, { dauer: 500, kurve: K, schritt: 2, wort: 'Produktgruppe', bandName: 'Produktgruppen in zwei Reihen, wischen oder Pfeiltasten', umschalt: () => { if (html.dataset.v === 'zweireihig') werteEl.innerHTML = STEUER.zweireihig.werte(); } });
  const V = baueVorfuehrung(R, K);
  STEUER.zweireihig = {
    R,
    werte: () => (R.istKarussell()
      ? `Zustand <b>zwei Reihen</b> · <b>grid-template-rows: repeat(2, auto); grid-auto-flow: column</b> · Spalte <b>46 %</b>, snap nur an der oberen Reihe · Schritt <b>eine Spalte = 2 Kacheln</b> · Lesefolge <b>spaltenweise = DOM</b>, reading-flow <b>grid-columns</b> · Pfeil <b>500 ms cubic-bezier(0.25, 1, 0.5, 1)</b>`
      : `Zustand <b>Raster</b> · <b>6 × 2</b> ab 64rem, <b>4 × 3</b> ab 44rem · Lesefolge <b>zeilenweise = DOM</b> · Rasterlinien <b>13 % Graphit</b> · Umschalten per <b>@container reihe (width &lt; 44rem)</b>`),
    start() { setzeBreite(R.buehne, 100); R.zustand(); },
    stopp() { V.stopp(); },
    neustart() { V.stopp(); html.classList.remove('vorfuehrung'); setzeBreite(R.buehne, 100); },
    vorfuehren() { V.stopp(); setzeBreite(R.buehne, 100); V.start(); },
    pause() { V.stopp(); },
    weiter() { if (html.classList.contains('vorfuehrung')) V.start(); }
  };
})();

/* ============ Varianten-Umschalter (Regel 13): ?v=einreihig | zweireihig ============ */
(() => {
  const IDS = ['einreihig', 'zweireihig'];
  let v = new URLSearchParams(location.search).get('v');
  if (!IDS.includes(v)) v = IDS[0];
  let vorf = false;
  const knoepfe = [...document.querySelectorAll('[data-v-wahl]')];
  function setze(neu, url) {
    const alt = html.dataset.v;
    if (alt && STEUER[alt]) STEUER[alt].stopp();
    v = neu; html.dataset.v = neu;
    document.querySelectorAll('[data-welt]').forEach(w => { w.hidden = w.dataset.welt !== neu; });
    knoepfe.forEach(b => b.setAttribute('aria-pressed', String(b.dataset.vWahl === neu)));
    if (url) { const q = new URLSearchParams(location.search); q.set('v', neu); history.replaceState(null, '', location.pathname + '?' + q.toString().replace(/=(?=&|$)/g, '') + location.hash); }
    STEUER[neu].start();
    werteEl.innerHTML = STEUER[neu].werte();
  }
  knoepfe.forEach(b => b.addEventListener('click', () => { setze(b.dataset.vWahl, true); if (vorf) STEUER[v].vorfuehren(); }));
  regler.addEventListener('input', () => { STEUER[v].pause(); html.classList.remove('vorfuehrung'); setzeBreite(STEUER[v].R.buehne, +regler.value); });
  document.querySelectorAll('[data-breite-knopf]').forEach(b => b.addEventListener('click', () => {
    STEUER[v].pause(); html.classList.remove('vorfuehrung');
    const ziel = { 100: 'voll', 36: 560, 0: MIN }[b.dataset.breiteKnopf];
    setzeBreite(STEUER[v].R.buehne, wertFuer(STEUER[v].R.buehne, ziel));
  }));
  addEventListener('resize', () => { const b = STEUER[v].R.buehne; if (+regler.value < 100) setzeBreite(b, +regler.value); });
  setze(v, false);
  const ende = e => {
    if (!e.isTrusted || !html.classList.contains('vorfuehrung') || (e.target.closest && e.target.closest('[data-fundus-pult], fundus-pult-knopf'))) return;
    vorf = false; STEUER[v].pause(); html.classList.remove('vorfuehrung');
  };
  ['pointerdown', 'wheel', 'touchstart', 'keydown'].forEach(t => document.addEventListener(t, ende, { capture: true, passive: true }));

})();
</script>
</body>
</html>