Zum Inhalt
← Sektionen & Funktionen

Download-Center für technische Dokumente

Bibliothek mit rund 30 Dateien zu drei Baureihen: Chips mit Trefferzahl, Suche nach Artikel- oder Seriennummer, Version, Stand, Sprache und Größe an jeder Datei, dauerhafte Adressen und ältere Fassungen, die als ersetzt markiert abrufbar bleiben.

Hersteller mit mehreren Baureihen und Sprachen: eigene Seite ‚Downloads‘ mit allen Dateien, Versionen und Suche nach Artikel- oder Seriennummer.

Eine Bibliothek für Datenblätter, Betriebsanleitungen, CAD-Daten, Einbau- und Konformitätserklärungen und Zertifikate. Man findet Dateien nach Produkt, Dokumentart und Sprache (Chips mit Trefferzahl) oder über Artikel- und Seriennummer. Jede Zeile nennt Titel, Art, Sprache, Version, Stand, Format und Größe, hat einen Download-Knopf und „Adresse kopieren“. Die Adresse bleibt über Jahre gleich und führt immer zur gültigen Fassung; ältere Versionen bleiben unter eigenem Pfad erreichbar und sind als ersetzt markiert. Fehlt eine Sprache, bietet die Liste die vorhandenen an.

Wer eine Anleitung sucht, steht oft an der Maschine und hat nur das Typenschild. Seriennummer oder QR-Code führen dort direkt zur richtigen Fassung. Version, Stand und Größe an jeder Datei beantworten die Frage „ist das die aktuelle?“ ohne Öffnen. Dauerhafte Adressen halten Links in gedruckten Anleitungen, auf Typenschildern und in Kundensystemen gültig.

Wann einsetzen

Maschinen- und Anlagenbau, Komponenten- und Zulieferer, Medizin- und Labortechnik, Haus- und Gebäudetechnik: überall, wo Dokumente in mehreren Sprachen und Versionen über die Lebensdauer eines Produkts bereitstehen müssen. Für digitale Betriebsanleitungen nach der Maschinenverordnung (EU) 2023/1230 die naheliegende Form (vom Kunden prüfen lassen).

Wann nicht

Bei unter zehn Dokumenten reicht eine Liste auf der Produktseite. Nicht für Marketing-Broschüren hinter einem Formular. Keine Pflicht-Registrierung vor Betriebsanleitungen. Nicht als Ersatz für ein Ersatzteil- oder Serviceportal mit Login.

Varianten

  • Liste: die ganze Bibliothek mit Filtern, Suche und älteren Versionen
  • Produkt: nur die Dokumente einer Produktseite als Reiter, nach Art gruppiert, mit Sprache für alle Dateien
  • Typenschild: Einstieg über Seriennummer oder QR-Code am Gerät, führt direkt zur gültigen Anleitung und nennt neuere Fassungen

Umsetzung

Jede Datei ist ein Datensatz: Produkt, Art, Sprache, Version, Stand, Format, Größe in Byte, ersetzt_durch. Die Liste entsteht aus diesen Daten; Filter: ODER innerhalb einer Facette, UND zwischen Facetten; Trefferzahl je Chip ohne die eigene Facette gerechnet. Suche erkennt Artikelnummern (Präfix) und Seriennummern (Muster JJMM-NNNN) und löst die Seriennummer serverseitig in Baureihe und Lieferdatum auf. Adressen: /dokumente/<artikel>/<art>/<sprache>/ leitet per 302 auf die aktuelle Datei weiter, /dokumente/<artikel>/<art>/<sprache>/v3.1/ ist die feste Adresse einer alten Fassung; Dateinamen mit Artikel, Art, Sprache und Version. Ältere Versionen bleiben in der Liste (eingeklappt) mit „ersetzt durch v3.2“. Fehlt eine Sprache, steht an ihrer Stelle ein Hinweis mit den vorhandenen Sprachen. Dateien nur auf Anfrage (CAD von Sonderanfertigungen) haben „Anfordern“ statt Download. Download-Knopf zeigt Laden und Fertig (button-zustaende), Größe und Format stehen sichtbar daneben. Keine Registrierung vor Betriebsanleitungen. PDFs getaggt (PDF/UA) oder zusätzlich als HTML-Fassung. Auf dem Typenschild steht ein QR-Code mit der Kurzadresse /d/<seriennummer>.

Robust umsetzen

Telefon. Filter als waagerecht umbrechende Chip-Reihen, je Facette einklappbar. Jede Datei wird zur Karte: Titel oben, darunter Art, Sprache, Version, Stand; Format und Größe im Download-Knopf. Seriennummer-Feld mit inputmode und Beispiel; QR-Scan nur, wo die Kamera erlaubt ist.

Ohne Bewegung. Keine Masken: Ergebnisse stehen sofort. Der Fortschritt im Download-Knopf zählt ohne Balkenbewegung in Prozent.

Barrierefrei. Liste als Tabelle mit Kopfzellen bzw. auf dem Telefon als Liste mit Überschriften je Datei (WCAG 1.3.1). Chips als Knöpfe mit aria-pressed und Trefferzahl im Namen. Trefferzahl und Download-Status über role="status" (4.1.3). Download-Links nennen Format und Größe im Namen („Betriebsanleitung FGL, PDF, 4,8 MB“). „Adresse kopieren“ meldet das Ergebnis. Seriennummer-Feld mit sichtbarem Label, Format-Beispiel und Fehlermeldung am Feld (3.3.1, 3.3.3). Dokumente selbst als getaggte PDFs oder mit HTML-Fassung.

Code

<!DOCTYPE html>
<!--
  Download-Center für technische Dokumente
  Fundus · Sektionen & Funktionen · https://cecodesign.de/lab/fundus/sektionen/download-center-dokumente/
  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>Download-Center für technische Dokumente · Fundus-Demo</title>
<script>document.documentElement.classList.add('js');if(matchMedia('(prefers-reduced-motion: reduce)').matches)document.documentElement.classList.add('reduziert')</script>
<style>
/* Welt: Rezept „industrie-b2b-mittelstand“ – Vahlberg Vakuumtechnik GmbH, Olpe (erfunden) */
@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: 'Archivo'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/archivo:vf@latest/latin-ext-full-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-stretch: 62% 125%; font-display: swap; unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }

:root {
  --grund: #f3f3f0; --papier: #fbfbf9; --text: #111417; --leise: #4e555b; --kopfzelle: #e4e5e1;
  --raster: rgba(17, 20, 23, .13); --hilfe: rgba(17, 20, 23, .055);
  --stahl: #16202a; --signal: #0e518d; --signal-tief: #0a3f6f; --signal-hell: #e3eaf1;
  --warn: #8a4b00; --warn-flaeche: #f6ecdc; --fehler: #a3261b; --alt: #5d636a;
  --ease: cubic-bezier(0.25, 1, 0.5, 1); --ease-aus: cubic-bezier(0.5, 0, 0.75, 0);
  --d1: 200ms; --d2: 350ms; --d1-aus: 140ms; --d2-aus: 245ms;
  --cols: 12;
}
@media (max-width: 820px) { :root { --cols: 8; } }
@media (max-width: 640px) { :root { --cols: 4; } }
* { box-sizing: border-box; margin: 0; padding: 0; }
[hidden] { display: none !important; }
html { background: var(--grund); color: var(--text); font: 400 1.0625rem/1.55 'Archivo', system-ui, sans-serif; font-stretch: 100%; -webkit-font-smoothing: antialiased; }
:root:has(:focus-visible) { scroll-behavior: auto; }
body { min-height: 100svh; overflow-x: clip; }
button, input, select { font: inherit; color: inherit; }
button { background: none; border: 0; cursor: pointer; }
a { color: inherit; }
:focus-visible { outline: 2px solid var(--signal); outline-offset: 2px; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.code { font-size: .75rem; font-weight: 600; font-stretch: 75%; letter-spacing: .08em; text-transform: uppercase; color: var(--leise); }
.markiert { display: inline-block; font-size: .6875rem; font-weight: 600; font-stretch: 75%; letter-spacing: .08em; text-transform: uppercase; color: var(--warn); background: var(--warn-flaeche); padding: 2px 6px; line-height: 1.4; }

/* Pult der Demo: schmale Leiste über der Seite */
.pult { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 14px; padding: 6px clamp(16px, 3vw, 40px); background: var(--papier); border-bottom: 1px solid var(--raster); font-size: .75rem; }
.pult > span { font-weight: 600; font-stretch: 75%; letter-spacing: .08em; text-transform: uppercase; color: var(--leise); }
.pult .gruppe { display: flex; flex-wrap: wrap; gap: 2px; }
.pult button { min-height: 32px; padding: 0 10px; font-size: .8125rem; color: var(--leise); }
.pult button[aria-pressed=true] { background: var(--text); color: #fff; }
.pult button:not([aria-pressed=true]):hover { color: var(--text); box-shadow: inset 0 0 0 1px var(--raster); }
.pult .werte { color: var(--leise); font-stretch: 87%; font-weight: 400; letter-spacing: 0; text-transform: none; }

/* Kopf */
.kopf { background: var(--stahl); color: #fff; }
.kopf__innen { max-width: 1400px; margin: 0 auto; display: flex; align-items: center; gap: 12px 32px; padding: 0 clamp(16px, 3vw, 40px); min-height: 64px; }
.marke { display: flex; align-items: center; gap: 12px; text-decoration: none; }
.marke svg { width: 30px; height: 30px; flex: none; }
.marke b { font: 600 1.05rem/1 'Strichpunkt Sans', 'Archivo', sans-serif; font-stretch: 130%; letter-spacing: .02em; }
.marke span { display: block; font-size: .72rem; font-stretch: 75%; font-weight: 600; letter-spacing: .1em; text-transform: uppercase; color: rgba(255, 255, 255, .72); margin-top: 3px; }
.kopf nav ul { display: flex; gap: 26px; list-style: none; font-size: .9375rem; }
.kopf nav a { text-decoration: none; color: rgba(255, 255, 255, .86); padding: 8px 0; }
.kopf nav a[aria-current] { color: #fff; box-shadow: inset 0 -2px 0 #fff; }
.kopf nav a:hover { color: #fff; }
.sprache { margin-left: auto; color: rgba(255, 255, 255, .86); text-decoration: none; font-size: .875rem; }
@media (max-width: 980px) { .kopf nav { display: none; } }
@media (max-width: 640px) { .sprache .land { display: none; } }

/* Raster */
.achse { max-width: 1400px; margin: 0 auto; padding: 0 clamp(16px, 3vw, 40px); }
.achse ol { display: grid; grid-template-columns: repeat(var(--cols), minmax(0, 1fr)); list-style: none; border-left: 1px solid var(--raster); }
.achse li { border-right: 1px solid var(--raster); padding: 6px 8px 4px; font-size: .6875rem; font-weight: 600; font-stretch: 75%; letter-spacing: .08em; color: var(--leise); }
@media (max-width: 820px) { .achse li:nth-child(n+9) { display: none; } }
@media (max-width: 640px) { .achse li:nth-child(n+5) { display: none; } }
.blatt { max-width: 1400px; margin: 0 auto; padding: 0 clamp(16px, 3vw, 40px) 64px; }
.blatt__innen { position: relative; border-left: 1px solid var(--raster); border-right: 1px solid var(--raster); }
.blatt__innen::before { content: ''; position: absolute; inset: 0; pointer-events: none; background: repeating-linear-gradient(to right, transparent 0 calc(100% / var(--cols) - 1px), var(--hilfe) calc(100% / var(--cols) - 1px) calc(100% / var(--cols))); }
.zone { position: relative; display: grid; grid-template-columns: repeat(var(--cols), minmax(0, 1fr)); border-top: 1px solid var(--raster); }
.zone::before, .zone::after { content: ''; position: absolute; top: -6px; width: 11px; height: 11px; background: linear-gradient(var(--text), var(--text)) center / 11px 1px no-repeat, linear-gradient(var(--text), var(--text)) center / 1px 11px no-repeat; opacity: .6; }
.zone::before { left: -6px; } .zone::after { right: -6px; }
.zone > * { grid-column: 1 / -1; min-width: 0; }
.innen { padding-left: clamp(14px, 1.6vw, 24px); padding-right: clamp(14px, 1.6vw, 24px); }
.pfad { padding-top: 12px; padding-bottom: 12px; font-size: .8125rem; color: var(--leise); }
.pfad ol { display: flex; flex-wrap: wrap; gap: 4px 10px; list-style: none; }
.pfad li + li::before { content: '/'; margin-right: 10px; opacity: .6; }
.pfad a { text-decoration: none; } .pfad a:hover { text-decoration: underline; }

/* Kopfbereich */
.titel { padding-top: 14px; padding-bottom: 24px; display: grid; gap: 14px; }
h1 { font: 600 clamp(2.1rem, 1.3rem + 2.5vw, 3.5rem)/.98 'Strichpunkt Sans', 'Archivo', sans-serif; font-stretch: 114%; letter-spacing: -.03em; hyphens: auto; text-wrap: balance; }
.lead { max-width: 64ch; text-wrap: pretty; }
.zusagen { display: flex; flex-wrap: wrap; gap: 6px 22px; list-style: none; font-size: .875rem; color: var(--leise); }
.zusagen li { display: flex; flex-wrap: wrap; gap: 4px 8px; align-items: baseline; }
@media (max-width: 640px) { .zusagen li { display: grid; grid-template-columns: 9px minmax(0, 1fr); gap: 4px 8px; } .zusagen li .markiert { grid-column: 2; justify-self: start; } }
.zusagen li::before { content: ''; width: 9px; height: 9px; flex: none; background: linear-gradient(var(--text), var(--text)) center / 9px 1.5px no-repeat, linear-gradient(var(--text), var(--text)) center / 1.5px 9px no-repeat; transform: translateY(-1px); }

/* Knöpfe (button-zustaende + kontur-button-zeichnen, Variante ecken) */
.btn { position: relative; display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 44px; padding: 0 16px; font-size: .875rem; font-weight: 600; text-decoration: none; border-radius: 0; white-space: nowrap; font-variant-numeric: tabular-nums; transition: background-color var(--d1) var(--ease), color var(--d1) var(--ease); }
.btn--voll { background: var(--signal); color: #fff; }
.btn--voll:hover { background: var(--signal-tief); }
.btn:active { transform: translateY(1px); }
.btn--kontur { color: var(--text); box-shadow: inset 0 0 0 1px rgba(17, 20, 23, .4); }
.btn--kontur:hover { background: rgba(17, 20, 23, .04); }
.ecke { position: absolute; width: 10px; height: 10px; border: 0 solid var(--text); pointer-events: none; transition: width var(--d1-aus) var(--ease-aus), height var(--d1-aus) var(--ease-aus); }
.ecke--ol { top: 0; left: 0; border-top-width: 1.5px; border-left-width: 1.5px; }
.ecke--or { top: 0; right: 0; border-top-width: 1.5px; border-right-width: 1.5px; }
.ecke--ul { bottom: 0; left: 0; border-bottom-width: 1.5px; border-left-width: 1.5px; }
.ecke--ur { bottom: 0; right: 0; border-bottom-width: 1.5px; border-right-width: 1.5px; }
@media (hover: hover) { .btn--kontur:hover .ecke { width: 50%; height: 50%; transition: width var(--d2) var(--ease), height var(--d2) var(--ease); } }
.btn--kontur:focus-visible .ecke { width: 50%; height: 50%; }
.btn svg { width: 16px; height: 16px; flex: none; }
/* Download-Knopf: Laden mit Fortschritt, dann Fertig */
.laden { overflow: hidden; min-width: 128px; justify-content: flex-start; }
.laden::before { content: ''; position: absolute; inset: 0; background: var(--signal-hell); transform: scaleX(var(--p, 0)); transform-origin: left; pointer-events: none; }
.laden > :not(.ecke) { position: relative; }
.laden[data-zustand=fertig] { color: var(--signal); }
.laden[data-zustand=laedt] { cursor: progress; }
.laden .ok path { stroke-dasharray: 1; stroke-dashoffset: 1; }
.laden[data-zustand=fertig] .ok path { stroke-dashoffset: 0; transition: stroke-dashoffset 300ms var(--ease) 60ms; }
.laden .ok, .laden[data-zustand=fertig] .runter { display: none; }
.laden[data-zustand=fertig] .ok { display: block; }
.icon-btn { position: relative; display: inline-grid; place-items: center; width: 44px; height: 44px; color: var(--leise); }
.icon-btn:hover { color: var(--text); background: var(--kopfzelle); }
.icon-btn svg { width: 20px; height: 20px; }
.icon-btn .ok { display: none; }
.icon-btn[data-ok] { color: var(--signal); }
.icon-btn[data-ok] .ok { display: block; }
.icon-btn[data-ok] .kopie { display: none; }

/* Liste mit Filtern */
.bibliothek { display: grid; grid-template-columns: repeat(var(--cols), minmax(0, 1fr)); }
.filter { grid-column: 1 / -1; padding-top: 18px; padding-bottom: 18px; display: grid; gap: 18px; align-content: start; }
.ergebnis { grid-column: 1 / -1; min-width: 0; padding-top: 18px; padding-bottom: 28px; }
@media (min-width: 821px) {
  .filter { grid-column: 1 / span 3; border-right: 1px solid var(--raster); }
  .ergebnis { grid-column: 4 / -1; }
}
.suche label { display: block; font-size: .875rem; font-weight: 600; margin-bottom: 6px; }
.suche label span { font-weight: 400; color: var(--leise); }
.suchfeld { position: relative; }
.suchfeld input { width: 100%; min-height: 48px; padding: 0 44px 0 40px; border: 1px solid rgba(17, 20, 23, .45); border-radius: 0; background: var(--papier); font-variant-numeric: tabular-nums; }
.suchfeld > svg { position: absolute; left: 13px; top: 50%; width: 16px; height: 16px; transform: translateY(-50%); color: var(--leise); pointer-events: none; }
.suchfeld .leeren { position: absolute; right: 2px; top: 2px; width: 44px; height: 44px; display: grid; place-items: center; color: var(--leise); }
.suchfeld .leeren:hover { color: var(--text); }
.suche .hilfe { font-size: .8125rem; color: var(--leise); margin-top: 6px; }
.facette { border: 0; min-width: 0; }
.facette summary { display: flex; justify-content: space-between; align-items: center; min-height: 44px; cursor: pointer; font-size: .9375rem; font-weight: 600; border-top: 1px solid var(--text); list-style: none; }
.facette summary::-webkit-details-marker { display: none; }
.facette summary::after { content: ''; width: 10px; height: 10px; background: linear-gradient(var(--text), var(--text)) center / 10px 1.5px no-repeat, linear-gradient(var(--text), var(--text)) center / 1.5px 10px no-repeat; }
.facette[open] summary::after { background: linear-gradient(var(--text), var(--text)) center / 10px 1.5px no-repeat; }
.chips { display: flex; flex-wrap: wrap; gap: 6px; padding: 4px 0 6px; }
.chip { display: inline-flex; align-items: center; gap: 8px; min-height: 40px; padding: 0 12px; font-size: .875rem; background: var(--papier); box-shadow: inset 0 0 0 1px var(--raster); transition: box-shadow var(--d1) var(--ease), background-color var(--d1) var(--ease); }
.chip .n { font-stretch: 87%; font-variant-numeric: tabular-nums; color: var(--leise); }
.chip:hover { box-shadow: inset 0 0 0 1px var(--text); }
.chip[aria-pressed=true] { background: var(--signal); color: #fff; box-shadow: none; }
.chip[aria-pressed=true] .n { color: rgba(255, 255, 255, .85); }
.chip[aria-pressed=true]::before { content: ''; width: 10px; height: 6px; border-left: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor; transform: rotate(-45deg) translate(1px, -1px); }
.chip.null:not([aria-pressed=true]) { color: #5f656b; }
.kontakt-klein { font-size: .8125rem; color: var(--leise); border-top: 1px solid var(--raster); padding-top: 12px; }
.kontakt-klein a { color: var(--signal); font-weight: 600; text-decoration: none; }

.kopfzeile { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 20px; margin-bottom: 10px; }
.kopfzeile h2 { font: 600 1.25rem/1.2 'Archivo', sans-serif; font-variant-numeric: tabular-nums; margin-right: auto; }
.schalter { display: inline-flex; align-items: center; gap: 10px; min-height: 44px; font-size: .9375rem; cursor: pointer; }
.schalter__spur { position: relative; width: 38px; height: 22px; flex: none; border-radius: 11px; background: #8b9095; transition: background-color var(--d1) var(--ease); }
.schalter__spur::after { content: ''; position: absolute; left: 3px; top: 3px; width: 16px; height: 16px; border-radius: 50%; background: #fff; transition: transform var(--d1) var(--ease); }
.schalter[aria-checked=true] .schalter__spur { background: var(--signal); }
.schalter[aria-checked=true] .schalter__spur::after { transform: translateX(16px); }
.aktiv { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
.aktiv:empty { display: none; }
.aktiv button { display: inline-flex; align-items: center; gap: 8px; min-height: 36px; padding: 0 10px; font-size: .8125rem; background: var(--kopfzelle); }
.aktiv button svg { width: 10px; height: 10px; }
.aktiv button:hover { background: #d6d8d3; }
.aktiv .alle { background: none; text-decoration: underline; text-underline-offset: 3px; }
.serienhinweis { display: grid; gap: 2px; padding: 10px 12px; margin-bottom: 12px; background: var(--signal-hell); border-left: 3px solid var(--signal); font-size: .9375rem; }

/* Tabelle: ARIA-Rollen halten die Semantik, wenn CSS auf dem Telefon Karten daraus macht */
.dateien { width: 100%; border-collapse: separate; border-spacing: 0; font-size: .9375rem; background: var(--papier); border: 1px solid var(--raster); border-top: 1px solid var(--text); }
.dateien th, .dateien td { padding: 10px 12px; border-bottom: 1px solid var(--raster); text-align: left; vertical-align: top; }
.dateien thead th { background: var(--kopfzelle); font-weight: 600; font-size: .875rem; white-space: nowrap; vertical-align: bottom; }
.dateien td { font-stretch: 87%; font-variant-numeric: tabular-nums; }
.dateien td.dok { font-stretch: 100%; }
.dok b { display: block; font-weight: 600; line-height: 1.3; }
.dok .wo { display: block; font-size: .8125rem; color: var(--leise); }
.dok .adresse { display: block; font-size: .8125rem; color: var(--leise); font-stretch: 75%; overflow-wrap: anywhere; margin-top: 2px; }
.dateien td.ver { white-space: nowrap; }
.dateien td.ver span { display: block; font-size: .8125rem; color: var(--leise); }
.dateien tr.alt td.ver span { color: var(--alt); }
.art-klein { display: none; }
.dateien td.dok { width: 46%; }
.dateien td.aktion { white-space: nowrap; text-align: right; }
.dateien td.sp-art { white-space: nowrap; }
.aktion > div { display: inline-flex; gap: 4px; align-items: center; }
.dateien tr.alt td { color: var(--alt); background: #f2f2ef; }
.dateien tr.alt .dok b { font-weight: 500; }
.ersetzt { display: inline-block; margin-top: 4px; font-size: .75rem; font-weight: 600; font-stretch: 87%; color: var(--text); background: var(--kopfzelle); padding: 1px 6px; text-decoration: none; }
.ersetzt:hover { text-decoration: underline; }
.dateien tr.ziel td { background: var(--signal-hell); }
.dateien tr.fehlt td { background: var(--warn-flaeche); color: var(--text); }
.fehlt .wahl { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.fehlt .wahl button { min-height: 36px; padding: 0 10px; font-size: .8125rem; box-shadow: inset 0 0 0 1px rgba(17, 20, 23, .4); background: var(--papier); }
.fehlt .wahl button:hover { box-shadow: inset 0 0 0 1px var(--text); }
.neutral { color: var(--leise); }
.anfrage-feld { display: grid; gap: 8px; margin-top: 8px; padding: 10px; background: var(--grund); border: 1px solid var(--raster); }
.anfrage-feld label { font-size: .8125rem; font-weight: 600; }
.anfrage-feld input { min-height: 44px; padding: 0 10px; border: 1px solid rgba(17, 20, 23, .45); border-radius: 0; background: var(--papier); width: 100%; }
.anfrage-feld .reihe { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.anfrage-feld .fehlertext { color: var(--fehler); font-size: .8125rem; }
@media (max-width: 1180px) and (min-width: 641px) { .dateien .sp-art { display: none; } .art-klein { display: inline; } }
@media (max-width: 640px) {
  .dateien, .dateien tbody, .dateien tr, .dateien td { display: block; }
  .dateien thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
  .dateien { border: 0; background: none; }
  .dateien tr { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 2px 12px; padding: 12px; margin-bottom: 8px; background: var(--papier); border: 1px solid var(--raster); border-top: 1px solid var(--text); }
  .dateien tr.alt { background: #f2f2ef; }
  .dateien tr.fehlt { background: var(--warn-flaeche); }
  .dateien td { padding: 0; border: 0; background: none !important; }
  .dateien td.dok, .dateien td.aktion, .dateien td.voll { grid-column: 1 / -1; }
  .dateien td.dok { margin-bottom: 6px; width: auto; }
  .dateien td[data-label]::before { content: attr(data-label) ': '; color: var(--leise); }
  .dateien td.aktion { text-align: left; margin-top: 8px; }
}

/* Leerzustand (fehlerseite-leerzustand, Variante keine-treffer) */
.leer { padding: 28px clamp(16px, 3vw, 32px); background: var(--papier); border: 1px solid var(--raster); border-top: 1px solid var(--text); display: grid; gap: 12px; }
.leer h3 { font: 600 1.25rem/1.25 'Archivo', sans-serif; }
.leer p { color: var(--leise); max-width: 60ch; }
.leer .knoepfe { display: flex; flex-wrap: wrap; gap: 10px; }

/* Variante produkt: Reiter einer Produktseite */
.produktkopf { padding-top: 14px; padding-bottom: 6px; display: flex; flex-wrap: wrap; align-items: end; justify-content: space-between; gap: 10px 24px; }
.produktkopf h1 { font-size: clamp(1.9rem, 1.2rem + 2vw, 2.9rem); }
.produktkopf p { color: var(--leise); }
.reiter { display: flex; flex-wrap: wrap; gap: 0 4px; border-bottom: 1px solid var(--raster); margin-top: 8px; }
.reiter [role=tab] { position: relative; min-height: 48px; padding: 0 14px; font-size: 1rem; font-weight: 600; color: var(--leise); }
.reiter [role=tab]::after { content: ''; position: absolute; left: 14px; right: 14px; bottom: -1px; height: 3px; background: var(--signal); transform: scaleX(0); transform-origin: left; transition: transform var(--d1-aus) var(--ease-aus); }
.reiter [role=tab][aria-selected=true] { color: var(--text); }
.reiter [role=tab][aria-selected=true]::after { transform: scaleX(1); transition: transform var(--d1) var(--ease); }
.reiter [role=tab]:hover { color: var(--text); }
.reiter [role=tab] span { font-weight: 400; font-stretch: 87%; margin-left: 4px; }
[role=tabpanel] { padding-top: 18px; padding-bottom: 28px; }
.sprachwahl { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; margin-bottom: 14px; }
.sprachwahl label { font-size: .9375rem; }
.sprachwahl select { min-height: 44px; padding: 0 36px 0 12px; border: 1px solid rgba(17, 20, 23, .45); border-radius: 0; background: var(--papier) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%23111417' stroke-width='1.5'/%3E%3C/svg%3E") right 12px center no-repeat; appearance: none; }
.sprachwahl a { margin-left: auto; font-size: .9375rem; font-weight: 600; color: var(--signal); text-decoration: none; }
.sprachwahl a:hover { text-decoration: underline; }
.gruppe-dok { margin-bottom: 18px; }
.gruppe-dok h3 { font-size: .75rem; font-weight: 600; font-stretch: 75%; letter-spacing: .08em; text-transform: uppercase; color: var(--leise); margin-bottom: 6px; }
.dl-liste { list-style: none; border-top: 1px solid var(--text); background: var(--papier); border-left: 1px solid var(--raster); border-right: 1px solid var(--raster); }
.dl-liste > li { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 6px 16px; align-items: center; padding: 12px; border-bottom: 1px solid var(--raster); }
.dl-liste b { font-weight: 600; }
.dl-liste .meta { display: block; font-size: .8125rem; color: var(--leise); font-stretch: 87%; font-variant-numeric: tabular-nums; }
.dl-liste .hinweis { grid-column: 1 / -1; padding: 8px 10px; background: var(--warn-flaeche); border-left: 3px solid var(--warn); font-size: .875rem; }
.dl-liste details { grid-column: 1 / -1; font-size: .875rem; }
.dl-liste summary { cursor: pointer; min-height: 32px; display: inline-flex; align-items: center; color: var(--leise); }
.dl-liste details p { color: var(--alt); padding: 4px 0 0; font-stretch: 87%; }
.mini-daten { width: 100%; max-width: 640px; border-collapse: collapse; background: var(--papier); border: 1px solid var(--raster); border-top: 1px solid var(--text); font-size: .9375rem; }
.mini-daten th, .mini-daten td { text-align: left; padding: 9px 12px; border-bottom: 1px solid var(--raster); }
.mini-daten td { text-align: right; font-stretch: 87%; font-variant-numeric: tabular-nums; font-weight: 600; }
.mini-daten th { font-weight: 400; }
@media (max-width: 640px) { .dl-liste > li { grid-template-columns: minmax(0, 1fr); } .sprachwahl a { margin-left: 0; } }

/* Variante typenschild */
.schild-raster { display: grid; grid-template-columns: repeat(var(--cols), minmax(0, 1fr)); }
.schild-bild { grid-column: 1 / -1; padding-top: 22px; padding-bottom: 22px; display: grid; gap: 10px; align-content: start; }
.schild-form { grid-column: 1 / -1; padding-top: 22px; padding-bottom: 28px; display: grid; gap: 16px; align-content: start; }
@media (min-width: 821px) {
  .schild-bild { grid-column: 1 / span 5; border-right: 1px solid var(--raster); }
  .schild-form { grid-column: 6 / -1; }
}
.schild-bild svg { width: 100%; max-width: 460px; height: auto; display: block; }
.schild-bild figcaption { font-size: .8125rem; color: var(--leise); max-width: 46ch; }
.schild-bild .qr-ziel { fill: none; stroke: var(--signal); stroke-width: 3; opacity: 0; }
.scannt .qr-ziel { opacity: 1; }
.scan-linie { opacity: 0; }
.scannt .scan-linie { opacity: 1; animation: scan 900ms var(--ease) 1 both; }
@keyframes scan { from { transform: translateY(0); } to { transform: translateY(96px); } }
.serie label { display: block; font-size: .9375rem; font-weight: 600; margin-bottom: 6px; }
.serie .hilfe { font-size: .8125rem; color: var(--leise); margin-top: 6px; }
.serie .reihe { display: flex; flex-wrap: wrap; gap: 10px; }
.serie input { flex: 1 1 200px; min-width: 0; min-height: 48px; padding: 0 12px; border: 1px solid rgba(17, 20, 23, .45); border-radius: 0; background: var(--papier); font-size: 1.125rem; font-stretch: 87%; font-variant-numeric: tabular-nums; letter-spacing: .04em; }
.serie[data-fehler] input { border-color: var(--fehler); box-shadow: inset 3px 0 0 var(--fehler); }
.serie .fehlertext { display: none; color: var(--fehler); font-size: .875rem; margin-top: 6px; }
.serie[data-fehler] .fehlertext { display: block; }
.treffer-maske { overflow: hidden; }
.treffer { display: grid; gap: 14px; padding: 18px; background: var(--papier); border: 1px solid var(--raster); border-top: 3px solid var(--signal); }
.treffer.rein { animation: rein var(--d2) var(--ease) both; }
@keyframes rein { from { transform: translateY(-100%); } to { transform: none; } }
.treffer h2 { font: 600 1.5rem/1.15 'Strichpunkt Sans', sans-serif; font-stretch: 110%; letter-spacing: -.02em; }
.treffer dl { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(140px, 100%), 1fr)); gap: 8px 16px; font-size: .9375rem; }
.treffer dt { font-size: .8125rem; color: var(--leise); }
.treffer dd { font-weight: 600; font-stretch: 87%; font-variant-numeric: tabular-nums; }
.haupt-dl { display: grid; gap: 6px; padding: 14px; background: var(--grund); border: 1px solid var(--raster); }
.haupt-dl b { font-size: 1.0625rem; }
.haupt-dl .meta { font-size: .875rem; color: var(--leise); font-stretch: 87%; font-variant-numeric: tabular-nums; }
.haupt-dl .knoepfe, .treffer .knoepfe { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 6px; }
.sprachen-kurz { display: flex; flex-wrap: wrap; gap: 6px; }
.sprachen-kurz button { min-height: 36px; min-width: 44px; padding: 0 10px; font-size: .8125rem; box-shadow: inset 0 0 0 1px var(--raster); background: var(--papier); }
.sprachen-kurz button[aria-pressed=true] { background: var(--text); color: #fff; box-shadow: none; }
.archiv { font-size: .875rem; color: var(--alt); border-left: 3px solid var(--kopfzelle); padding-left: 10px; }
.weitere { list-style: none; display: grid; gap: 0; border-top: 1px solid var(--raster); }
.weitere li { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 6px 14px; padding: 8px 0; border-bottom: 1px solid var(--raster); font-size: .9375rem; }
.weitere .meta { font-size: .8125rem; color: var(--leise); font-stretch: 87%; }
.recht { font-size: .8125rem; color: var(--leise); }
.unbekannt { padding: 14px; background: var(--papier); border: 1px solid var(--raster); border-left: 3px solid var(--fehler); display: grid; gap: 6px; font-size: .9375rem; }
.unbekannt a { color: var(--signal); font-weight: 600; text-decoration: none; }

/* Varianten der Demo */
.v-liste, .v-produkt, .v-typenschild { display: none; }
[data-variante=liste] .v-liste, [data-variante=produkt] .v-produkt, [data-variante=typenschild] .v-typenschild { display: block; }
[data-variante=liste] .zone.v-liste, [data-variante=produkt] .zone.v-produkt, [data-variante=typenschild] .zone.v-typenschild { display: grid; }

/* Statusmeldung */
.ansage { position: fixed; left: 50%; bottom: 18px; transform: translate(-50%, calc(100% + 30px)); visibility: hidden; background: var(--stahl); color: #fff; padding: 10px 16px; font-size: .875rem; max-width: calc(100% - 24px); transition: transform var(--d1-aus) var(--ease-aus), visibility 0s linear var(--d1-aus); z-index: 20; }
.ansage[data-an] { transform: translate(-50%, 0); visibility: visible; transition: transform var(--d2) var(--ease); }

/* Vorschau 960 × 720 */
.vorschau body { overflow: hidden; }
.vorschau .pult, .vorschau .achse, .vorschau .pfad, .vorschau .lead, .vorschau .zusagen, .vorschau .kopf nav, .vorschau .suche .hilfe, .vorschau .kontakt-klein, .vorschau .dok .adresse { display: none; }
.vorschau .titel { padding-bottom: 12px; }
.vorschau h1 { font-size: 2.4rem; }
.vorschau .facette summary { min-height: 36px; }
.vorschau .chip { min-height: 34px; }
.vorschau .treffer dl, .vorschau .serie .hilfe, .vorschau .schild-bild figcaption, .vorschau .recht { display: none; }

@media (max-width: 360px) {
  .sprache { display: none; }
  .chip { padding: 0 10px; }
  .blatt { padding-left: 10px; padding-right: 10px; }
}
@media (hover: none) { .btn--kontur .ecke { width: 10px; height: 10px; } }
:root.reduziert *, :root.reduziert *::before, :root.reduziert *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; }
:root.reduziert .laden::before { transform: scaleX(0) !important; }
:root.reduziert .scannt .scan-linie { animation: none; opacity: 0; }
@media (forced-colors: active) {
  button, [role=button], [role=tab], [role=switch], input, select, textarea { border: 1px solid ButtonText !important; }
  .chip[aria-pressed=true], .sprachen-kurz button[aria-pressed=true], .reiter [role=tab][aria-selected=true] { outline: 2px solid Highlight; }
}
</style>
</head>
<body>
<div class="pult" role="region" aria-label="Steuerung der Demo">
  <span>Variante</span>
  <div class="gruppe" role="group" aria-label="Variante der Demo">
    <button type="button" data-v="liste" aria-pressed="true">Liste</button>
    <button type="button" data-v="produkt" aria-pressed="false">Produkt</button>
    <button type="button" data-v="typenschild" aria-pressed="false">Typenschild</button>
  </div>
  <span class="werte" data-werte></span>
</div>

<header class="kopf">
  <div class="kopf__innen">
    <a class="marke" href="#" aria-label="Vahlberg Vakuumtechnik, Startseite">
      <svg viewBox="0 0 30 30" aria-hidden="true"><rect x=".75" y=".75" width="28.5" height="28.5" fill="none" stroke="#fff" stroke-width="1.5"/><path d="M7 8l8 15 8-15" fill="none" stroke="#fff" stroke-width="2.4"/><circle cx="15" cy="8" r="2" fill="#fff"/></svg>
      <div><b>VAHLBERG</b><span>Vakuumtechnik · Olpe</span></div>
    </a>
    <nav aria-label="Hauptnavigation"><ul>
      <li><a href="#" data-nav="produkte">Produkte</a></li><li><a href="#">Anwendungen</a></li><li><a href="#" data-nav="downloads" aria-current="page">Downloads</a></li><li><a href="#">Service</a></li><li><a href="#">Unternehmen</a></li>
    </ul></nav>
    <a class="sprache" href="#">Deutsch<span class="land"> · Deutschland</span></a>
  </div>
</header>

<div class="achse" aria-hidden="true"><ol><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li></ol></div>

<main class="blatt">
 <div class="blatt__innen">

  <!-- ── Variante liste ───────────────────────────── -->
  <nav class="zone pfad innen v-liste" aria-label="Brotkrumen"><ol><li><a href="#">Start</a></li><li><a href="#">Service</a></li><li aria-current="page">Downloads</li></ol></nav>
  <section class="zone v-liste" aria-labelledby="t-liste">
    <div class="titel innen">
      <p class="code">04 · Downloads</p>
      <h1 id="t-liste">Dokumente und Downloads</h1>
      <p class="lead">Datenblätter, Betriebsanleitungen, CAD-Daten, Erklärungen und Zertifikate zu allen Baureihen. Ohne Anmeldung. Jede Datei nennt Version, Stand, Sprache und Größe; ihre Adresse bleibt gültig, auch wenn eine neue Fassung kommt.</p>
      <ul class="zusagen">
        <li>Keine Registrierung für Anleitungen</li>
        <li>Alte Versionen bleiben abrufbar</li>
        <li>Betriebsanleitungen mindestens zehn Jahre online <span class="markiert">vom Kunden prüfen lassen</span></li>
      </ul>
    </div>
  </section>
  <section class="zone v-liste" aria-label="Dokumente filtern und herunterladen">
    <div class="bibliothek">
      <div class="filter innen">
        <div class="suche">
          <label for="suche">Suche <span>nach Artikel- oder Seriennummer, Titel</span></label>
          <div class="suchfeld">
            <svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="6.5" cy="6.5" r="5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M10.2 10.2L15 15" stroke="currentColor" stroke-width="1.6"/></svg>
            <input id="suche" type="search" autocomplete="off" spellcheck="false" placeholder="z. B. 41.20.0900.21" aria-describedby="suche-hilfe" data-suche>
            <button class="leeren" type="button" aria-label="Suche leeren" data-leeren hidden><svg width="12" height="12" viewBox="0 0 12 12" aria-hidden="true"><path d="M1 1l10 10M11 1L1 11" stroke="currentColor" stroke-width="1.6"/></svg></button>
          </div>
          <p class="hilfe" id="suche-hilfe">Die Seriennummer steht auf dem Typenschild (Format JJMM-NNNN).</p>
        </div>
        <details class="facette" open data-facette="produkt"><summary>Produkt</summary><div class="chips" role="group" aria-label="Produkt"></div></details>
        <details class="facette" open data-facette="art"><summary>Dokumentart</summary><div class="chips" role="group" aria-label="Dokumentart"></div></details>
        <details class="facette" open data-facette="sprache"><summary>Sprache</summary><div class="chips" role="group" aria-label="Sprache"></div></details>
        <p class="kontakt-klein">Dokument nicht dabei? Technische Dokumentation: <a href="#">02761 9418-300</a> oder <a href="#">dokumentation@vahlberg.example</a></p>
      </div>
      <div class="ergebnis innen">
        <div class="kopfzeile">
          <h2 data-anzahl aria-describedby="anzahl-hinweis">30 Dateien</h2>
          <button class="schalter" type="button" role="switch" aria-checked="false" data-alte><span class="schalter__spur" aria-hidden="true"></span><span data-alte-text>Ältere Versionen zeigen</span></button>
        </div>
        <p class="sr" id="anzahl-hinweis">Die Liste ändert sich sofort beim Filtern.</p>
        <div class="aktiv" data-aktiv aria-label="Aktive Filter" role="group"></div>
        <div class="serienhinweis" data-serie hidden></div>
        <table class="dateien" role="table" aria-labelledby="cap-dateien" data-tabelle>
          <caption id="cap-dateien" class="sr">Dateien mit Art, Sprache, Version, Stand, Format und Größe</caption>
          <thead role="rowgroup"><tr role="row"><th role="columnheader" scope="col">Dokument</th><th role="columnheader" scope="col" class="sp-art">Art</th><th role="columnheader" scope="col">Sprache</th><th role="columnheader" scope="col">Version, Stand</th><th role="columnheader" scope="col">Datei herunterladen</th></tr></thead>
          <tbody role="rowgroup" data-zeilen></tbody>
        </table>
        <div class="leer" data-leer hidden tabindex="-1">
          <h3 data-leer-titel>Keine Dokumente gefunden</h3>
          <p data-leer-text></p>
          <div class="knoepfe" data-leer-knoepfe></div>
          <p>Fehlt ein Dokument, schicken wir es Ihnen: <a href="#">dokumentation@vahlberg.example</a>, 02761 9418-300.</p>
        </div>
      </div>
    </div>
  </section>

  <!-- ── Variante produkt ─────────────────────────── -->
  <nav class="zone pfad innen v-produkt" aria-label="Brotkrumen"><ol><li><a href="#">Produkte</a></li><li><a href="#">Vakuum-Greifsysteme</a></li><li><a href="#">Flächengreifer</a></li><li aria-current="page">Baureihe FGL</li></ol></nav>
  <section class="zone v-produkt" aria-labelledby="t-produkt">
    <div class="innen">
      <div class="produktkopf">
        <div><p class="code">01 · Baureihe FGL</p><h1 id="t-produkt">Flächengreifer FGL</h1></div>
        <p>Artikel 41.20 · acht Längen, 300 bis 1800 mm</p>
      </div>
      <div class="reiter" role="tablist" aria-label="Produktinformationen">
        <button role="tab" id="tab-daten" aria-controls="p-daten" aria-selected="false" tabindex="-1">Technische Daten</button>
        <button role="tab" id="tab-dl" aria-controls="p-dl" aria-selected="true" tabindex="0">Downloads<span data-p-anzahl>6</span></button>
        <button role="tab" id="tab-zub" aria-controls="p-zub" aria-selected="false" tabindex="-1">Zubehör</button>
      </div>
      <div role="tabpanel" id="p-daten" aria-labelledby="tab-daten" tabindex="0" hidden>
        <table class="mini-daten"><caption class="sr">Kenndaten FGL-900 IO</caption><tbody>
          <tr><th scope="row">Haltekraft bei −600 mbar</th><td>1140 N</td></tr><tr><th scope="row">Saugvolumen</th><td>300 l/min</td></tr><tr><th scope="row">Gewicht</th><td>6,1 kg</td></tr><tr><th scope="row">Anschluss</th><td>G 3/8</td></tr>
        </tbody></table>
      </div>
      <div role="tabpanel" id="p-dl" aria-labelledby="tab-dl" tabindex="0">
        <div class="sprachwahl">
          <label for="p-sprache">Sprache der Dokumente</label>
          <select id="p-sprache" data-p-sprache><option value="de">Deutsch</option><option value="en">English</option><option value="fr">Français</option><option value="pl">Polski</option><option value="cs">Čeština</option><option value="it">Italiano</option></select>
          <a href="#" data-zur-liste>Alle Dokumente im Download-Center</a>
        </div>
        <div data-p-gruppen></div>
      </div>
      <div role="tabpanel" id="p-zub" aria-labelledby="tab-zub" tabindex="0" hidden>
        <table class="mini-daten"><caption class="sr">Zubehör</caption><tbody>
          <tr><th scope="row">Schnellwechselflansch SWF-50</th><td>41.90.0020.00</td></tr><tr><th scope="row">Schalldämpfer SD-38</th><td>41.90.0310.00</td></tr><tr><th scope="row">Filtereinsatz 60 µm</th><td>41.90.0505.00</td></tr>
        </tbody></table>
      </div>
    </div>
  </section>

  <!-- ── Variante typenschild ─────────────────────── -->
  <nav class="zone pfad innen v-typenschild" aria-label="Brotkrumen"><ol><li><a href="#">Service</a></li><li aria-current="page">Anleitung zum Gerät</li></ol></nav>
  <section class="zone v-typenschild" aria-labelledby="t-schild">
    <div class="titel innen">
      <p class="code">Service · /d/&lt;Seriennummer&gt;</p>
      <h1 id="t-schild">Die Anleitung zu Ihrem Gerät</h1>
      <p class="lead">Geben Sie die Seriennummer vom Typenschild ein oder scannen Sie den QR-Code am Gerät. Sie kommen direkt zur Betriebsanleitung, die für Ihr Gerät gilt. Ohne Anmeldung.</p>
    </div>
  </section>
  <section class="zone v-typenschild" aria-label="Seriennummer eingeben">
    <div class="schild-raster">
      <figure class="schild-bild innen" data-schild>
        <svg viewBox="0 0 460 260" role="img" aria-labelledby="schild-text">
          <rect x="1" y="1" width="458" height="258" rx="10" fill="#e4e5e1" stroke="#111417" stroke-width="1.5"/>
          <circle cx="16" cy="16" r="4" fill="none" stroke="#4e555b" stroke-width="1.5"/><circle cx="444" cy="16" r="4" fill="none" stroke="#4e555b" stroke-width="1.5"/><circle cx="16" cy="244" r="4" fill="none" stroke="#4e555b" stroke-width="1.5"/><circle cx="444" cy="244" r="4" fill="none" stroke="#4e555b" stroke-width="1.5"/>
          <g font-family="Archivo, sans-serif" fill="#111417">
            <rect x="30" y="26" width="22" height="22" fill="none" stroke="#111417" stroke-width="1.5"/><path d="M34.5 31l6.5 12 6.5-12" fill="none" stroke="#111417" stroke-width="2"/>
            <text x="60" y="43" font-size="17" font-weight="600" font-stretch="125%" letter-spacing="1">VAHLBERG</text>
            <text x="30" y="66" font-size="10" fill="#4e555b" font-stretch="75%" letter-spacing="1">VAHLBERG VAKUUMTECHNIK GMBH · OLPE · DEUTSCHLAND</text>
            <g font-size="11" font-stretch="75%" fill="#4e555b" letter-spacing=".8"><text x="30" y="94">TYP</text><text x="30" y="128">ART.-NR.</text><text x="30" y="162">SERIEN-NR.</text><text x="30" y="196">BAUJAHR</text><text x="150" y="196">GEWICHT</text><text x="30" y="230">BETRIEBSDRUCK</text></g>
            <g font-size="16" font-weight="600" font-stretch="87%"><text x="30" y="111">FGL-900 IO</text><text x="30" y="145">41.20.0900.21</text><text x="30" y="179" data-schild-serie>2311-0457</text><text x="30" y="213">2023</text><text x="150" y="213">6,1 kg</text><text x="30" y="247" font-size="14">4 bis 6 bar</text></g>
          </g>
          <g transform="translate(300 78)" data-qr></g>
          <rect class="qr-ziel" x="292" y="70" width="128" height="128"/>
          <rect class="scan-linie" x="296" y="78" width="120" height="2" fill="#0e518d"/>
          <text x="356" y="222" font-family="Archivo, sans-serif" font-size="10" font-stretch="75%" fill="#4e555b" text-anchor="middle" letter-spacing=".6">ANLEITUNG: /d/2311-0457</text>
        </svg>
        <figcaption id="schild-text">Typenschild am Flächengreifer: Typ FGL-900 IO, Artikelnummer 41.20.0900.21, Seriennummer 2311-0457, Baujahr 2023. Rechts der QR-Code mit der Kurzadresse der Anleitung (in der Demo kein echter Code).</figcaption>
      </figure>
      <div class="schild-form innen">
        <form class="serie" novalidate data-serie-form>
          <label for="serie">Seriennummer</label>
          <div class="reihe">
            <input id="serie" name="serie" inputmode="numeric" autocomplete="off" placeholder="JJMM-NNNN" aria-describedby="serie-hilfe serie-fehler" data-serie-feld>
            <button class="btn btn--voll" type="submit">Anleitung finden</button>
            <button class="btn btn--kontur" type="button" data-scan><span class="ecke ecke--ol"></span><span class="ecke ecke--or"></span><span class="ecke ecke--ul"></span><span class="ecke ecke--ur"></span><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M1 5V1h4M11 1h4v4M15 11v4h-4M5 15H1v-4M4 8h8" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>QR-Code scannen</button>
          </div>
          <p class="hilfe" id="serie-hilfe">Format JJMM-NNNN, z. B. 2311-0457. Beispiele für die Demo: 2311-0457, 2502-0113, 1907-0871.</p>
          <p class="fehlertext" id="serie-fehler" data-serie-fehler></p>
        </form>
        <div class="treffer-maske" data-ergebnis aria-live="polite"></div>
        <p class="recht"><span class="markiert">Platzhalter</span> Digitale Betriebsanleitung nach Maschinenverordnung (EU) 2023/1230, Art. 10 Abs. 7: herunterladbar, druckbar, speicherbar, mindestens zehn Jahre online. Eine Papierfassung schicken wir kostenlos, wenn Sie sie beim Kauf verlangen. Text und Einordnung vom Kunden prüfen lassen.</p>
      </div>
    </div>
  </section>
 </div>
</main>

<p class="sr" role="status" data-status></p>
<p class="ansage" aria-hidden="true" data-ansage></p>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const $ = (s, r = document) => r.querySelector(s);
  const $$ = (s, r = document) => [...r.querySelectorAll(s)];
  const NF = new Intl.NumberFormat('de-DE', { maximumFractionDigits: 1, minimumFractionDigits: 1 });
  const NF0 = new Intl.NumberFormat('de-DE', { maximumFractionDigits: 0 });
  const groesse = kb => kb >= 1000 ? NF.format(kb / 1000) + ' MB' : NF0.format(kb) + ' kB';
  const HOST = 'https://www.vahlberg.example';

  /* ── Daten: jede Datei ein Datensatz ── */
  const PRODUKT = { fgl: ['Flächengreifer FGL', '41.20'], kev: ['Kompaktejektor KEV', '42.10'], vhl: ['Schlauchheber VHL', '45.30'], allg: ['Unternehmen', ''] };
  const ART = { ba: ['Betriebsanleitung', 'betriebsanleitung'], db: ['Datenblatt', 'datenblatt'], cad: ['CAD-Daten', 'cad'], erk: ['Erklärung', 'erklaerung'], zert: ['Zertifikat', 'zertifikat'], sw: ['Software', 'software'] };
  const SPR = { de: 'Deutsch', en: 'Englisch', fr: 'Französisch', pl: 'Polnisch', cs: 'Tschechisch', it: 'Italienisch' };
  const SPR_ORD = Object.keys(SPR);
  const DOKS = [
    { id: 'fgl-ba', p: 'fgl', a: 'ba', titel: 'Betriebsanleitung Flächengreifer FGL', vers: [
      { v: '3.2', stand: '04/2026', spr: ['de', 'en', 'fr', 'pl', 'cs'], f: 'PDF', kb: 4800, html: 1 },
      { v: '3.1', stand: '09/2024', spr: ['de', 'en'], f: 'PDF', kb: 4500 }] },
    { id: 'fgl-db', p: 'fgl', a: 'db', titel: 'Datenblatt Baureihe FGL', vers: [{ v: '09/2026', stand: '09/2026', spr: ['de', 'en', 'fr'], f: 'PDF', kb: 1200, ausgabe: 1 }] },
    { id: 'fgl-cad', p: 'fgl', a: 'cad', titel: 'CAD-Daten FGL, alle Längen', vers: [{ v: '2026.2', stand: '09/2026', spr: ['-'], f: 'STEP, ZIP', kb: 38400 }] },
    { id: 'fgl-ebe', p: 'fgl', a: 'erk', titel: 'EU-Einbauerklärung FGL', vers: [{ v: '04/2026', stand: '04/2026', spr: ['de', 'en'], f: 'PDF', kb: 210, ausgabe: 1, recht: 1 }] },
    { id: 'fgl-iodd', p: 'fgl', a: 'sw', titel: 'IODD Gerätebeschreibung FGL IO', vers: [{ v: '1.4', stand: '06/2026', spr: ['-'], f: 'XML, ZIP', kb: 96 }] },
    { id: 'fgl-s', p: 'fgl', a: 'cad', titel: 'CAD FGL-S 2290, Sonderanfertigung', vers: [{ v: '1.0', stand: '08/2026', spr: ['-'], f: 'STEP', kb: 6900, anfrage: 1 }] },
    { id: 'kev-ba', p: 'kev', a: 'ba', titel: 'Betriebsanleitung Kompaktejektor KEV', vers: [
      { v: '2.0', stand: '01/2026', spr: ['de', 'en', 'it'], f: 'PDF', kb: 2100, html: 1 },
      { v: '1.4', stand: '03/2023', spr: ['de'], f: 'PDF', kb: 1900 }] },
    { id: 'kev-db', p: 'kev', a: 'db', titel: 'Datenblatt Kompaktejektor KEV', vers: [{ v: '01/2026', stand: '01/2026', spr: ['de', 'en'], f: 'PDF', kb: 640, ausgabe: 1 }] },
    { id: 'kev-cad', p: 'kev', a: 'cad', titel: 'CAD-Daten KEV 15 bis 60', vers: [{ v: '2026.1', stand: '01/2026', spr: ['-'], f: 'STEP, ZIP', kb: 4200 }] },
    { id: 'vhl-ba', p: 'vhl', a: 'ba', titel: 'Betriebsanleitung Schlauchheber VHL', vers: [
      { v: '5.1', stand: '07/2026', spr: ['de', 'en', 'fr', 'pl'], f: 'PDF', kb: 6300, html: 1 },
      { v: '5.0', stand: '02/2025', spr: ['de', 'en'], f: 'PDF', kb: 6100 }] },
    { id: 'vhl-ke', p: 'vhl', a: 'erk', titel: 'EU-Konformitätserklärung VHL', vers: [{ v: '07/2026', stand: '07/2026', spr: ['de', 'en'], f: 'PDF', kb: 240, ausgabe: 1, recht: 1 }] },
    { id: 'vhl-db', p: 'vhl', a: 'db', titel: 'Datenblatt Schlauchheber VHL', vers: [{ v: '07/2026', stand: '07/2026', spr: ['de', 'en'], f: 'PDF', kb: 900, ausgabe: 1 }] },
    { id: 'iso9001', p: 'allg', a: 'zert', titel: 'Zertifikat ISO 9001:2015, gültig bis 03/2028', vers: [{ v: '2025', stand: '03/2025', spr: ['de', 'en'], f: 'PDF', kb: 320, recht: 1 }] },
    { id: 'iso14001', p: 'allg', a: 'zert', titel: 'Zertifikat ISO 14001:2015, gültig bis 03/2028', vers: [{ v: '2025', stand: '03/2025', spr: ['de'], f: 'PDF', kb: 300, recht: 1 }] },
  ];
  const DATEIEN = [];
  DOKS.forEach(d => d.vers.forEach((ver, vi) => ver.spr.forEach(s => DATEIEN.push({
    key: `${d.id}|${ver.v}|${s}`, dok: d, ver, s, aktuell: vi === 0, ersetztDurch: vi ? d.vers[0].v : null,
    pfad: `/dokumente/${PRODUKT[d.p][1] || 'unternehmen'}/${d.id.split('-').pop() === 's' ? 'fgl-s-2290' : ART[d.a][1]}/${s === '-' ? '' : s + '/'}${vi ? 'v' + ver.v + '/' : ''}`,
  }))));
  const SERIEN = {
    '2311-0457': { p: 'fgl', typ: 'FGL-900 IO', art: '41.20.0900.21', jahr: 2023, monat: '11/2023', ausgeliefert: '3.1', ba: 'fgl-ba', erk: 'Einbauerklärung zu diesem Gerät, ausgestellt 11/2023' },
    '2502-0113': { p: 'kev', typ: 'KEV 30', art: '42.10.0030.01', jahr: 2025, monat: '02/2025', ausgeliefert: '1.4', ba: 'kev-ba', erk: '' },
    '1907-0871': { p: 'vhl', typ: 'VHL-140', art: '45.30.0140.00', jahr: 2019, monat: '07/2019', ausgeliefert: '4.3', ba: 'vhl-ba', erk: 'EU-Konformitätserklärung zu diesem Gerät, ausgestellt 07/2019' },
  };

  /* ── Zustand ── */
  const q = new URLSearchParams(location.search);
  const VARIANTEN = ['liste', 'produkt', 'typenschild'];
  let modus = VARIANTEN.includes(q.get('v')) ? q.get('v') : 'liste';
  const filter = { produkt: new Set(), art: new Set(), sprache: new Set() };
  let suche = '', alteZeigen = false;

  /* ── Ansage (sichtbar + Screenreader) ── */
  let ansageT, statusT;
  function ansage(t, sichtbar = true) {
    $('[data-status]').textContent = t;
    if (!sichtbar) return;
    const a = $('[data-ansage]'); a.textContent = t; a.setAttribute('data-an', '');
    clearTimeout(ansageT); ansageT = setTimeout(() => a.removeAttribute('data-an'), 2000);
  }

  /* ── Suche: Artikelnummer, Seriennummer oder Text ── */
  function sucheAuswerten() {
    const t = suche.trim().toLowerCase();
    if (!t) return { art: 'leer' };
    if (/^\d{4}-\d{4}$/.test(t)) return SERIEN[t] ? { art: 'serie', serie: t, info: SERIEN[t] } : { art: 'serie-unbekannt', serie: t };
    if (/^\d{2}\.\d{2}/.test(t)) { const p = Object.keys(PRODUKT).find(k => PRODUKT[k][1] && t.startsWith(PRODUKT[k][1])); return { art: 'artikel', p }; }
    return { art: 'text', woerter: t.split(/\s+/) };
  }
  function passtSuche(f, s) {
    if (s.art === 'leer') return true;
    if (s.art === 'serie') return f.dok.p === s.info.p;
    if (s.art === 'serie-unbekannt') return false;
    if (s.art === 'artikel') return f.dok.p === s.p;
    const text = (f.dok.titel + ' ' + PRODUKT[f.dok.p][0] + ' ' + ART[f.dok.a][0] + ' ' + SPR[f.s] + ' ' + PRODUKT[f.dok.p][1]).toLowerCase();
    return s.woerter.every(w => text.includes(w));
  }
  const passtSprache = (f, set) => !set.size || f.s === '-' || set.has(f.s);
  function passt(f, ohne, s) {
    if (!alteZeigen && !f.aktuell) return false;
    if (ohne !== 'produkt' && filter.produkt.size && !filter.produkt.has(f.dok.p)) return false;
    if (ohne !== 'art' && filter.art.size && !filter.art.has(f.dok.a)) return false;
    if (ohne !== 'sprache' && !passtSprache(f, filter.sprache)) return false;
    return passtSuche(f, s);
  }

  /* ── Chips mit Trefferzahl (ohne die eigene Facette gerechnet) ── */
  const FACETTEN = { produkt: Object.keys(PRODUKT).map(k => [k, PRODUKT[k][0]]), art: Object.keys(ART).map(k => [k, ART[k][0]]), sprache: SPR_ORD.map(k => [k, SPR[k]]) };
  function baueChips() {
    Object.entries(FACETTEN).forEach(([fac, opts]) => {
      $(`[data-facette="${fac}"] .chips`).innerHTML = opts.map(([k, n]) => `<button class="chip" type="button" data-fac="${fac}" data-k="${k}" aria-pressed="false"><span>${n}</span><span class="n"></span></button>`).join('');
    });
  }
  function zaehleChips(s) {
    $$('.chip').forEach(c => {
      const fac = c.dataset.fac, k = c.dataset.k;
      const n = DATEIEN.filter(f => passt(f, fac, s) && (fac === 'produkt' ? f.dok.p === k : fac === 'art' ? f.dok.a === k : (f.s === k || f.s === '-'))).length;
      c.querySelector('.n').textContent = n;
      c.setAttribute('aria-pressed', filter[fac].has(k));
      c.setAttribute('aria-label', `${c.firstElementChild.textContent}, ${n} ${n === 1 ? 'Datei' : 'Dateien'}`);
      c.classList.toggle('null', n === 0);
    });
  }

  /* ── Liste rendern ── */
  const ecken = '<span class="ecke ecke--ol"></span><span class="ecke ecke--or"></span><span class="ecke ecke--ul"></span><span class="ecke ecke--ur"></span>';
  const icRunter = '<svg class="runter" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 1v10M3.5 6.5L8 11l4.5-4.5M2 15h12" fill="none" stroke="currentColor" stroke-width="1.6"/></svg>';
  const icOk = '<svg class="ok" viewBox="0 0 16 16" aria-hidden="true"><path d="M2.5 8.5l3.5 3.5 7.5-8" fill="none" stroke="currentColor" stroke-width="2" pathLength="1"/></svg>';
  const icKopie = '<svg class="kopie" viewBox="0 0 20 20" aria-hidden="true"><path d="M8.5 11.5l3-3M7 9L5 11a2.5 2.5 0 0 0 3.5 3.5l2-2M13 11l2-2A2.5 2.5 0 0 0 11.5 5.5l-2 2" fill="none" stroke="currentColor" stroke-width="1.5"/></svg><svg class="ok" viewBox="0 0 20 20" aria-hidden="true"><path d="M4 10.5l4 4 8-9" fill="none" stroke="currentColor" stroke-width="2"/></svg>';
  const sprText = s => s === '-' ? '<span class="neutral">sprachneutral</span>' : SPR[s];
  function versionText(f) { return f.ver.ausgabe ? 'Ausgabe ' + f.ver.v : 'v' + f.ver.v; }
  function zeile(f) {
    const d = f.dok, name = `${d.titel}, ${f.s === '-' ? '' : SPR[f.s] + ', '}${versionText(f)}`;
    const knopf = f.ver.anfrage
      ? `<button class="btn btn--kontur" type="button" data-anfordern="${f.key}" aria-expanded="false">${ecken}Anfordern</button>`
      : `<button class="btn btn--kontur laden" type="button" data-laden="${f.key}" aria-label="${name} herunterladen, ${f.ver.f}, ${groesse(f.ver.kb)}">${ecken}${icRunter}${icOk}<span data-t>${f.ver.f.split(',')[0]}, ${groesse(f.ver.kb)}</span></button>`;
    return `<tr role="row" class="${f.aktuell ? '' : 'alt'}" data-key="${f.key}">` +
      `<td role="cell" class="dok"><b>${d.titel}</b><span class="wo"><span class="art-klein">${ART[d.a][0]} · </span>${PRODUKT[d.p][1] ? 'Artikel ' + PRODUKT[d.p][1] : 'gilt für alle Produkte'}${f.ver.html ? ' · auch als HTML-Fassung' : ''}</span>` +
      (f.ver.recht ? '<span class="markiert">vom Kunden prüfen lassen</span>' : '') +
      (f.ver.anfrage ? '<span class="wo">Nur auf Anfrage: CAD von Sonderanfertigungen geben wir nach Prüfung frei.</span>' : `<span class="adresse">${f.pfad}</span>`) +
      (f.ersetztDurch ? `<a class="ersetzt" href="#" data-springe="${d.id}|${f.ersetztDurch}|${f.s}">ersetzt durch v${f.ersetztDurch}</a>` : '') +
      `<div data-anfrage-platz></div></td>` +
      `<td role="cell" data-label="Art" class="sp-art">${ART[d.a][0]}</td><td role="cell" data-label="Sprache">${sprText(f.s)}</td><td role="cell" data-label="Version" class="ver">${versionText(f)}<span>${f.ver.ausgabe ? '' : 'Stand ' + f.ver.stand}</span></td>` +
      `<td role="cell" class="aktion"><div>${knopf}${f.ver.anfrage ? '' : `<button class="icon-btn" type="button" data-kopieren="${f.pfad}" aria-label="Adresse kopieren: ${f.pfad}">${icKopie}</button>`}</div></td></tr>`;
  }
  function fehltZeile(d, set) {
    const akt = d.vers[0], namen = [...set].map(s => SPR[s]).join(' oder ');
    return `<tr role="row" class="fehlt"><td role="cell" class="dok voll" colspan="5"><b>${d.titel}</b><span class="wo">gibt es nicht auf ${namen}. Vorhanden in v${akt.v}: ${akt.spr.map(s => SPR[s]).join(', ')}.</span>` +
      `<div class="wahl">${akt.spr.map(s => `<button type="button" data-sprache-dazu="${s}">${SPR[s]} zeigen</button>`).join('')}</div></td></tr>`;
  }
  let zaehlT;
  function render(fokusLeer) {
    const s = sucheAuswerten();
    const treffer = DATEIEN.filter(f => passt(f, null, s));
    const ordnung = f => [Object.keys(PRODUKT).indexOf(f.dok.p), Object.keys(ART).indexOf(f.dok.a), DOKS.indexOf(f.dok), f.aktuell ? 0 : 1, SPR_ORD.indexOf(f.s)];
    treffer.sort((a, b) => { const x = ordnung(a), y = ordnung(b); for (let i = 0; i < x.length; i++) if (x[i] !== y[i]) return x[i] - y[i]; return 0; });
    // Sprache fehlt: Dokumente, die passen würden, aber keine Datei in der gewählten Sprache haben
    const fehlt = filter.sprache.size ? DOKS.filter(d => {
      const akt = d.vers[0]; if (akt.spr[0] === '-') return false;
      const probe = { dok: d, ver: akt, s: akt.spr[0], aktuell: true };
      return passt(probe, 'sprache', s) && !akt.spr.some(x => filter.sprache.has(x));
    }) : [];
    let html = '', letzte = null;
    treffer.forEach(f => { html += zeile(f); letzte = f.dok; });
    fehlt.forEach(d => html += fehltZeile(d, filter.sprache));
    $('[data-zeilen]').innerHTML = html;
    const leer = !treffer.length;
    $('[data-tabelle]').hidden = leer && !fehlt.length;
    $('[data-leer]').hidden = !leer || fehlt.length > 0;
    if (leer && !fehlt.length) leerzustand(s, fokusLeer);
    const n = treffer.length;
    $('[data-anzahl]').textContent = `${n} ${n === 1 ? 'Datei' : 'Dateien'}`;
    const alt = DATEIEN.filter(f => !f.aktuell).length;
    $('[data-alte-text]').textContent = `Ältere Versionen zeigen (${alt})`;
    zaehleChips(s);
    aktiveChips(s);
    // Seriennummer erkannt
    const hs = $('[data-serie]');
    if (s.art === 'serie') { hs.hidden = false; hs.innerHTML = `<b>Seriennummer ${s.serie}: ${s.info.typ}, Baujahr ${s.info.jahr}</b><span>Gezeigt werden alle Dokumente der Baureihe. Die für Ihr Gerät gültige Anleitung ist v${DOKS.find(d => d.id === s.info.ba).vers[0].v}.</span>`; }
    else hs.hidden = true;
    clearTimeout(zaehlT); zaehlT = setTimeout(() => ansage(`${n} ${n === 1 ? 'Datei' : 'Dateien'} gefunden`, false), 400);
  }
  function aktiveChips(s) {
    const teile = [];
    Object.entries(filter).forEach(([fac, set]) => set.forEach(k => {
      const n = fac === 'produkt' ? PRODUKT[k][0] : fac === 'art' ? ART[k][0] : SPR[k];
      teile.push(`<button type="button" data-weg="${fac}|${k}" aria-label="Filter entfernen: ${n}">${n}<svg viewBox="0 0 10 10" aria-hidden="true"><path d="M1 1l8 8M9 1L1 9" stroke="currentColor" stroke-width="1.5"/></svg></button>`);
    }));
    if (suche.trim()) teile.push(`<button type="button" data-weg="suche|" aria-label="Suche entfernen: ${suche.trim()}">„${suche.trim()}“<svg viewBox="0 0 10 10" aria-hidden="true"><path d="M1 1l8 8M9 1L1 9" stroke="currentColor" stroke-width="1.5"/></svg></button>`);
    if (teile.length > 1) teile.push('<button type="button" class="alle" data-weg="alle|">Alle Filter entfernen</button>');
    $('[data-aktiv]').innerHTML = teile.join('');
  }
  /* Leerzustand: nennt den Filter, der am meisten blockiert */
  function leerzustand(s, fokus) {
    const knoepfe = [];
    let text = '';
    if (s.art === 'serie-unbekannt') { text = `Die Seriennummer ${s.serie} kennen wir nicht. Prüfen Sie Ziffern wie 0 und 8 auf dem Typenschild, oder rufen Sie den Service an.`; }
    else {
      const kandidaten = [];
      Object.entries(filter).forEach(([fac, set]) => { if (set.size) { const n = DATEIEN.filter(f => passt(f, fac, s)).length; kandidaten.push([fac, n]); } });
      kandidaten.sort((a, b) => b[1] - a[1]);
      const namen = { produkt: 'Produkt', art: 'Dokumentart', sprache: 'Sprache' };
      kandidaten.filter(k => k[1] > 0).slice(0, 2).forEach(([fac, n]) => knoepfe.push(`<button class="btn btn--voll" type="button" data-weg="${fac}|*">Ohne Filter ${namen[fac]}: ${n} ${n === 1 ? 'Datei' : 'Dateien'}</button>`));
      text = suche.trim() ? `Für „${suche.trim()}“ gibt es mit diesen Filtern keine Datei.` : 'Diese Kombination aus Filtern ergibt keine Datei.';
    }
    if (suche.trim()) knoepfe.push(`<button class="btn btn--kontur" type="button" data-weg="suche|">${ecken}Suche leeren</button>`);
    knoepfe.push(`<button class="btn btn--kontur" type="button" data-weg="alle|">${ecken}Alle Filter entfernen</button>`);
    $('[data-leer-text]').textContent = text;
    $('[data-leer-knoepfe]').innerHTML = knoepfe.join('');
    if (fokus) $('[data-leer]').focus();
  }

  /* ── Bedienung Liste ── */
  baueChips();
  document.addEventListener('click', e => {
    const c = e.target.closest('.chip');
    if (c) { const set = filter[c.dataset.fac]; set.has(c.dataset.k) ? set.delete(c.dataset.k) : set.add(c.dataset.k); render(); return; }
    const w = e.target.closest('[data-weg]');
    if (w) {
      const [fac, k] = w.dataset.weg.split('|');
      if (fac === 'alle') { Object.values(filter).forEach(s => s.clear()); suche = ''; $('[data-suche]').value = ''; $('[data-leeren]').hidden = true; }
      else if (fac === 'suche') { suche = ''; $('[data-suche]').value = ''; $('[data-leeren]').hidden = true; }
      else if (k === '*') filter[fac].clear(); else filter[fac].delete(k);
      const imLeer = w.closest('[data-leer]'), imAktiv = w.closest('[data-aktiv]');
      render();
      if (imAktiv) { const rest = $$('[data-aktiv] button'); (rest[0] || $('[data-anzahl]')).focus?.(); if (!rest.length) { $('[data-anzahl]').setAttribute('tabindex', '-1'); $('[data-anzahl]').focus(); } }
      if (imLeer) $('[data-anzahl]').setAttribute('tabindex', '-1'), $('[data-anzahl]').focus();
      return;
    }
    const sp = e.target.closest('[data-sprache-dazu]');
    if (sp) { filter.sprache.add(sp.dataset.spracheDazu); render(); return; }
    const j = e.target.closest('[data-springe]');
    if (j) { e.preventDefault(); const tr = $(`tr[data-key="${CSS.escape(j.dataset.springe)}"]`) || $(`tr[data-key^="${CSS.escape(j.dataset.springe.split('|').slice(0, 2).join('|'))}"]`); if (tr) { $$('tr.ziel').forEach(t => t.classList.remove('ziel')); tr.classList.add('ziel'); tr.scrollIntoView({ block: 'center', behavior: reduziert() ? 'auto' : 'smooth' }); tr.querySelector('button')?.focus({ preventScroll: true }); } return; }
    const l = e.target.closest('[data-laden]');
    if (l) { laden(l); return; }
    const k = e.target.closest('[data-kopieren]');
    if (k) { kopiere(k, HOST + k.dataset.kopieren); return; }
    const a = e.target.closest('[data-anfordern]');
    if (a) { anfordern(a); return; }
  });
  $('[data-suche]').addEventListener('input', e => { suche = e.target.value; $('[data-leeren]').hidden = !suche; render(); });
  $('[data-leeren]').addEventListener('click', () => { suche = ''; $('[data-suche]').value = ''; $('[data-leeren]').hidden = true; render(); $('[data-suche]').focus(); });
  $('[data-alte]').addEventListener('click', e => { alteZeigen = !alteZeigen; e.currentTarget.setAttribute('aria-checked', alteZeigen); render(); });
  // Telefon: Facetten zu Beginn eingeklappt
  if (matchMedia('(max-width: 640px)').matches) $$('.facette').forEach(f => f.open = false);

  /* ── Herunterladen: Laden mit Fortschritt, dann Fertig (Demo, keine echte Datei) ── */
  function laden(b) {
    if (b.dataset.zustand) return;
    const f = DATEIEN.find(x => x.key === b.dataset.laden) || PDATEI[b.dataset.laden];
    const t = b.querySelector('[data-t]'), alt = t.textContent, dauer = Math.max(900, Math.min(2400, f.ver.kb / 3));
    b.dataset.zustand = 'laedt'; b.setAttribute('aria-disabled', 'true');
    ansage(`${f.dok.titel} wird geladen`, false);
    const t0 = performance.now();
    const tick = now => {
      const p = Math.min(1, (now - t0) / dauer);
      b.style.setProperty('--p', p);
      t.textContent = `Lädt ${Math.round(p * 100)} %`;
      if (p < 1) return requestAnimationFrame(tick);
      b.dataset.zustand = 'fertig'; b.style.setProperty('--p', 0); t.textContent = 'Gespeichert';
      ansage(`${f.dok.titel} gespeichert (Demo)`);
      setTimeout(() => { delete b.dataset.zustand; b.removeAttribute('aria-disabled'); t.textContent = alt; }, 2000);
    };
    requestAnimationFrame(tick);
  }
  /* Adresse kopieren */
  async function kopiere(b, text) {
    let ok = false;
    try { await navigator.clipboard.writeText(text); ok = true; } catch (_) {
      const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select();
      try { ok = document.execCommand('copy'); } catch (_) {} ta.remove();
    }
    b.setAttribute('data-ok', ''); ansage(ok ? 'Adresse kopiert: ' + text.replace(HOST, '') : 'Kopieren nicht möglich');
    setTimeout(() => b.removeAttribute('data-ok'), 2000);
  }
  /* Nur auf Anfrage */
  function anfordern(b) {
    const platz = b.closest('tr').querySelector('[data-anfrage-platz]');
    if (platz.innerHTML) { platz.innerHTML = ''; b.setAttribute('aria-expanded', 'false'); return; }
    b.setAttribute('aria-expanded', 'true');
    platz.innerHTML = `<form class="anfrage-feld" novalidate><label for="af-mail">Ihre E-Mail</label><input id="af-mail" type="email" autocomplete="email" required aria-describedby="af-f"><label for="af-auftrag">Auftrags- oder Projektnummer <span class="neutral">(optional)</span></label><input id="af-auftrag" type="text" placeholder="z. B. P-2026-0388"><div class="reihe"><button class="btn btn--voll" type="submit">CAD anfordern</button><span class="neutral">Antwort werktags binnen 24 Stunden.</span></div><p class="fehlertext" id="af-f" hidden>Bitte eine E-Mail-Adresse mit @ angeben.</p></form>`;
    const form = platz.querySelector('form');
    form.querySelector('input').focus();
    form.addEventListener('submit', ev => {
      ev.preventDefault();
      const m = form.querySelector('#af-mail'), ok = /\S+@\S+\.\S+/.test(m.value);
      form.querySelector('.fehlertext').hidden = ok; m.setAttribute('aria-invalid', !ok);
      if (!ok) { m.focus(); return; }
      platz.innerHTML = '<p class="wo" tabindex="-1">Anfrage D-261102-0093 ist angekommen. Die Dokumentation prüft die Freigabe und schickt den Link an Ihre E-Mail. (Demo: nichts gesendet.)</p>';
      platz.firstElementChild.focus(); ansage('Anfrage angekommen');
    });
  }

  /* ── Variante produkt: Dokumente einer Produktseite, nach Art gruppiert ── */
  const PDATEI = {};
  function renderProdukt() {
    const sprache = $('[data-p-sprache]').value;
    const doks = DOKS.filter(d => d.p === 'fgl' && !d.vers[0].anfrage);
    const gruppen = Object.keys(ART).filter(a => doks.some(d => d.a === a));
    $('[data-p-anzahl]').textContent = doks.length;
    $('[data-p-gruppen]').innerHTML = gruppen.map(a => `<div class="gruppe-dok"><h3>${ART[a][0]}</h3><ul class="dl-liste">` + doks.filter(d => d.a === a).map(d => {
      const akt = d.vers[0], neutral = akt.spr[0] === '-', hat = neutral || akt.spr.includes(sprache), s = neutral ? '-' : (hat ? sprache : (akt.spr.includes('en') ? 'en' : akt.spr[0]));
      const key = `p|${d.id}|${s}`; PDATEI[key] = { dok: d, ver: akt };
      const pfad = `/dokumente/${PRODUKT[d.p][1]}/${ART[d.a][1]}/${neutral ? '' : s + '/'}`;
      const alte = d.vers.slice(1);
      return `<li><div><b>${d.titel}</b>${akt.recht ? ' <span class="markiert">vom Kunden prüfen lassen</span>' : ''}<span class="meta">${neutral ? 'sprachneutral' : SPR[s]} · ${akt.ausgabe ? 'Ausgabe ' + akt.v : 'Version ' + akt.v} · Stand ${akt.stand} · ${akt.f} · ${groesse(akt.kb)}${akt.html ? ' · auch als HTML-Fassung' : ''}</span><span class="meta">${pfad}</span></div>` +
        `<div class="aktion"><div style="display:inline-flex;gap:4px"><button class="btn btn--kontur laden" type="button" data-laden="${key}" aria-label="${d.titel} herunterladen, ${akt.f}, ${groesse(akt.kb)}">${ecken}${icRunter}${icOk}<span data-t>${akt.f.split(',')[0]}, ${groesse(akt.kb)}</span></button><button class="icon-btn" type="button" data-kopieren="${pfad}" aria-label="Adresse kopieren: ${pfad}">${icKopie}</button></div></div>` +
        (hat ? '' : `<p class="hinweis" role="note"><b>Nicht auf ${SPR[sprache]} vorhanden.</b> Angeboten: ${SPR[s]}. Vorhanden: ${akt.spr.map(x => SPR[x]).join(', ')}.</p>`) +
        (alte.length ? `<details><summary>Ältere Versionen (${alte.length})</summary>${alte.map(v => `<p>Version ${v.v}, Stand ${v.stand}, ${v.spr.map(x => SPR[x]).join(', ')}: ersetzt durch ${akt.v}, weiter abrufbar unter /dokumente/${PRODUKT[d.p][1]}/${ART[d.a][1]}/de/v${v.v}/</p>`).join('')}</details>` : '') + '</li>';
    }).join('') + '</ul></div>').join('');
  }
  $('[data-p-sprache]').addEventListener('change', renderProdukt);
  $('[data-zur-liste]').addEventListener('click', e => { e.preventDefault(); filter.produkt = new Set(['fgl']); setzeModus('liste', true); });
  const tabs = $$('[role=tab]');
  function waehleTab(t) { tabs.forEach(x => { const an = x === t; x.setAttribute('aria-selected', an); x.tabIndex = an ? 0 : -1; $('#' + x.getAttribute('aria-controls')).hidden = !an; }); }
  tabs.forEach((t, i) => {
    t.addEventListener('click', () => waehleTab(t));
    t.addEventListener('keydown', e => {
      const m = { ArrowRight: tabs[(i + 1) % tabs.length], ArrowLeft: tabs[(i - 1 + tabs.length) % tabs.length], Home: tabs[0], End: tabs[tabs.length - 1] }[e.key];
      if (m) { e.preventDefault(); tabs.forEach(x => x.tabIndex = x === m ? 0 : -1); m.focus(); }
    });
  });

  /* ── Variante typenschild: Seriennummer oder QR → gültige Anleitung ── */
  (function qr() { /* Muster wie ein QR-Code (kein echter Code), fest aus einem Startwert */
    let r = 20231104, s = '';
    const rnd = () => (r = (r * 1103515245 + 12345) % 2147483648) / 2147483648;
    const N = 25, z = 4.4, finder = (x, y) => `<rect x="${x * z}" y="${y * z}" width="${7 * z}" height="${7 * z}" fill="#111417"/><rect x="${(x + 1) * z}" y="${(y + 1) * z}" width="${5 * z}" height="${5 * z}" fill="#e4e5e1"/><rect x="${(x + 2) * z}" y="${(y + 2) * z}" width="${3 * z}" height="${3 * z}" fill="#111417"/>`;
    s += `<rect x="-6" y="-6" width="${N * z + 12}" height="${N * z + 12}" fill="#e4e5e1"/>`;
    for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) {
      const imFinder = (x < 8 && y < 8) || (x > N - 9 && y < 8) || (x < 8 && y > N - 9);
      if (!imFinder && rnd() > .52) s += `<rect x="${x * z}" y="${y * z}" width="${z}" height="${z}" fill="#111417"/>`;
    }
    s += finder(0, 0) + finder(N - 7, 0) + finder(0, N - 7);
    $('[data-qr]').innerHTML = s;
  })();
  function sucheSerie(nr, sanft = true) {
    const feld = $('[data-serie-feld]'), form = $('[data-serie-form]'), ziel = $('[data-ergebnis]');
    const t = nr.trim().replace(/\s/g, '');
    let fehler = '';
    if (!/^\d{4}-\d{4}$/.test(t)) fehler = 'Die Seriennummer hat das Format JJMM-NNNN, zum Beispiel 2311-0457.';
    form.toggleAttribute('data-fehler', !!fehler); feld.setAttribute('aria-invalid', !!fehler);
    $('[data-serie-fehler]').textContent = fehler;
    if (fehler) { ziel.innerHTML = ''; feld.focus(); return; }
    const info = SERIEN[t];
    if (!info) {
      ziel.innerHTML = `<div class="unbekannt treffer-rein"><b>Diese Seriennummer kennen wir nicht: ${t}</b><span>Prüfen Sie Ziffern wie 0 und 8 auf dem Typenschild. Oder rufen Sie den Service an: <a href="#">02761 9418-300</a>, werktags 7 bis 17 Uhr (MEZ).</span></div>`;
      return;
    }
    const d = DOKS.find(x => x.id === info.ba), akt = d.vers[0];
    const andere = DOKS.filter(x => x.p === info.p && x.id !== d.id && !x.vers[0].anfrage);
    ziel.innerHTML = `<div class="treffer${sanft && !reduziert() ? ' rein' : ''}" tabindex="-1">
      <h2>${info.typ}</h2>
      <dl><div><dt>Seriennummer</dt><dd>${t}</dd></div><div><dt>Artikel</dt><dd>${info.art}</dd></div><div><dt>Ausgeliefert</dt><dd>${info.monat}</dd></div><div><dt>Dauerhafte Adresse</dt><dd>/d/${t}</dd></div></dl>
      <div class="haupt-dl"><span class="code">Gültige Betriebsanleitung</span><b>${d.titel}</b>
        <span class="meta" data-ba-meta>Version ${akt.v} · Stand ${akt.stand} · PDF · ${groesse(akt.kb)}</span>
        <div class="sprachen-kurz" role="group" aria-label="Sprache der Anleitung">${akt.spr.map((s, i) => `<button type="button" data-ba-sprache="${s}" aria-pressed="${i === 0}">${s.toUpperCase()}<span class="sr"> ${SPR[s]}</span></button>`).join('')}</div>
        <div class="knoepfe"><button class="btn btn--voll laden" type="button" data-laden="p|${d.id}|${akt.spr[0]}" aria-label="${d.titel}, Version ${akt.v}, herunterladen, PDF, ${groesse(akt.kb)}">${icRunter}${icOk}<span data-t>PDF, ${groesse(akt.kb)}</span></button><a class="btn btn--kontur" href="#">${ecken}HTML-Fassung lesen</a><button class="btn btn--kontur" type="button" data-kopieren="/d/${t}" aria-label="Adresse kopieren: /d/${t}">${ecken}Adresse kopieren</button></div>
      </div>
      ${info.ausgeliefert !== akt.v ? `<p class="archiv">Ihr Gerät kam mit Version ${info.ausgeliefert}. Version ${akt.v} ersetzt sie und gilt auch für Ihr Gerät. Die ausgelieferte Fassung bleibt abrufbar unter /dokumente/${PRODUKT[info.p][1]}/betriebsanleitung/de/v${info.ausgeliefert}/.</p>` : ''}
      <ul class="weitere">${info.erk ? `<li><span>${info.erk} <span class="markiert">vom Kunden prüfen lassen</span></span><span class="meta">PDF · 210 kB</span></li>` : ''}${andere.map(x => `<li><span>${x.titel}</span><span class="meta">${x.vers[0].f} · ${groesse(x.vers[0].kb)}</span></li>`).join('')}</ul>
    </div>`;
    PDATEI[`p|${d.id}|${akt.spr[0]}`] = { dok: d, ver: akt };
    ansage(`${info.typ} gefunden, gültige Anleitung Version ${akt.v}`, false);
  }
  $('[data-serie-form]').addEventListener('submit', e => { e.preventDefault(); sucheSerie($('[data-serie-feld]').value); const t = $('.treffer'); if (t) t.focus({ preventScroll: true }); });
  $('[data-serie-feld]').addEventListener('input', e => { if ($('[data-serie-form]').hasAttribute('data-fehler') && /^\d{4}-\d{4}$/.test(e.target.value.trim())) { $('[data-serie-form]').removeAttribute('data-fehler'); $('[data-serie-fehler]').textContent = ''; } });
  let scanT;
  function scan() {
    const fig = $('[data-schild]');
    fig.classList.add('scannt'); $('[data-serie-feld]').value = '';
    clearTimeout(scanT);
    scanT = setTimeout(() => { fig.classList.remove('scannt'); $('[data-serie-feld]').value = '2311-0457'; sucheSerie('2311-0457'); }, reduziert() ? 150 : 950);
  }
  $('[data-scan]').addEventListener('click', () => { ansage('Demo: Die Kamera wird nicht geöffnet, der Code am Schild wird gelesen.', false); scan(); });
  $('[data-ergebnis]').addEventListener('click', e => {
    const b = e.target.closest('[data-ba-sprache]'); if (!b) return;
    $$('[data-ba-sprache]').forEach(x => x.setAttribute('aria-pressed', x === b));
    const l = $('[data-ergebnis] [data-laden]'); const [, id] = l.dataset.laden.split('|');
    l.dataset.laden = `p|${id}|${b.dataset.baSprache}`; PDATEI[l.dataset.laden] = PDATEI[Object.keys(PDATEI).find(k => k.startsWith('p|' + id))];
    $('[data-ba-meta]').textContent = $('[data-ba-meta]').textContent.replace(/ · Sprache .*$/, '') + ' · Sprache ' + SPR[b.dataset.baSprache];
  });

  /* ── Variante der Demo ── */
  const WERTE = {
    liste: 'Filter ODER je Facette, UND zwischen Facetten · Trefferzahl 400 ms verzögert angesagt · Liste ohne Animation',
    produkt: 'Reiter der Produktseite · eine Sprache für alle Dateien · fehlende Sprache mit Ersatz',
    typenschild: 'JJMM-NNNN · Ergebnis 350 ms aus der Maske · gültige Fassung zuerst',
  };
  function setzeModus(m, url) {
    modus = VARIANTEN.includes(m) ? m : 'liste';
    document.body.dataset.variante = modus;
    $$('.pult [data-v]').forEach(b => b.setAttribute('aria-pressed', b.dataset.v === modus));
    $('[data-werte]').textContent = WERTE[modus];
    $$('[data-nav]').forEach(a => a.toggleAttribute('aria-current', a.dataset.nav === (modus === 'produkt' ? 'produkte' : 'downloads')));
    if (modus === 'liste') render(); if (modus === 'produkt') renderProdukt();
    if (url) { const p = new URLSearchParams(location.search); p.set('v', modus); history.replaceState(null, '', '?' + p.toString().replace(/=(?=&|$)/g, '')); }
  }
  $$('.pult [data-v]').forEach(b => b.addEventListener('click', () => { pause(); setzeModus(b.dataset.v, true); }));

  /* ── Vorführung ── */
  let schleife = null, schritt = 0;
  const klick = sel => { const e = $(sel); if (e) e.click(); };
  const tippe = text => { suche = text; $('[data-suche]').value = text; $('[data-leeren]').hidden = !text; render(); };
  const ABLAUF = {
    liste: [
      () => klick('.chip[data-k="ba"]'),
      () => klick('.chip[data-k="pl"]'),
      () => klick('[data-alte]'),
      () => klick('[data-laden]'),
      () => { klick('[data-weg="alle|"]'); },
      () => tippe('41.20'),
      () => tippe('2311-0457'),
      () => { tippe(''); klick('.chip[data-k="vhl"]'); },
      () => klick('.chip[data-k="it"]'),
      () => { klick('[data-weg="alle|"]'); tippe('2311-0999'); },
      () => { klick('[data-weg="alle|"]'); if (alteZeigen) klick('[data-alte]'); },
    ],
    produkt: [
      () => { $('[data-p-sprache]').value = 'pl'; renderProdukt(); },
      () => klick('#p-dl [data-laden]'),
      () => { $('[data-p-sprache]').value = 'cs'; renderProdukt(); },
      () => { $('[data-p-sprache]').value = 'it'; renderProdukt(); },
      () => { $('[data-p-sprache]').value = 'de'; renderProdukt(); },
    ],
    typenschild: [
      () => scan(),
      () => {},
      () => klick('[data-ergebnis] [data-laden]'),
      () => { $('[data-serie-feld]').value = '2311-457'; sucheSerie('2311-457'); },
      () => { $('[data-serie-feld]').value = '1907-0871'; sucheSerie('1907-0871'); },
      () => {},
      () => { $('[data-serie-feld]').value = '2311-0999'; sucheSerie('2311-0999'); },
    ],
  };
  function takt() {
    const a = ABLAUF[modus];
    a[schritt % a.length](); schritt++;
    schleife = setTimeout(takt, 1600);
  }
  function neustart() {
    clearTimeout(schleife); schleife = null; schritt = 0;
    Object.values(filter).forEach(s => s.clear()); suche = ''; $('[data-suche]').value = ''; $('[data-leeren]').hidden = true;
    alteZeigen = false; $('[data-alte]').setAttribute('aria-checked', 'false');
    $('[data-p-sprache]').value = 'de';
    $('[data-serie-feld]').value = ''; $('[data-ergebnis]').innerHTML = ''; $('[data-serie-form]').removeAttribute('data-fehler');
    setzeModus(modus);
  }
  function vorfuehren() { neustart(); schleife = setTimeout(takt, 800); }
  function pause() { clearTimeout(schleife); schleife = null; }
  function weiter() { if (!schleife) schleife = setTimeout(takt, 400); }

  setzeModus(modus);

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