Zum Inhalt
← Navigation

Command-Palette (⌘K) und KI-Suche im Header

Strg/⌘ K öffnet über dem Hotel-Header einen Dialog: Fuzzy-Suche über Zimmer, Seiten und Aktionen, Freitext-Fragen beantwortet das Haus mit Quellen.

Im Header steht ein schmales Suchfeld oder ein Knopf mit dem Tastaturhinweis ⌘K bzw. „Ask AI“. ⌘K oder Strg+K (oft auch /) öffnet einen zentrierten Dialog mit Eingabefeld und gruppierter Ergebnisliste: Seiten, Aktionen, zuletzt besucht. Die Suche ist unscharf (fuzzy), Tastaturkürzel stehen rechts neben den Einträgen. Pfeiltasten bewegen die Auswahl, Enter führt aus, Escape schließt. Seit 2026 kommt die KI-Erweiterung dazu: Freitext-Fragen beantwortet eine KI und verweist auf die passenden Seiten. Der Dialog öffnet mit kurzer Skalierung von 0.96 auf 1 und Einblenden, der Hintergrund wird abgedunkelt.

Die Palette wächst mit der Inhaltsmenge mit, ohne dass die Navigation voller wird. Geübte Nutzer kommen mit zwei Tasten überall hin. Das wirkt kompetent und schnell. Die KI-Variante beantwortet Fragen, statt nur Seiten aufzulisten.

Wann einsetzen

Dokumentationen, Produktseiten, Design-Systeme, große Portfolios und Archive, Seiten für Entwickler. Auch bei Hotels oder Dienstleistern mit vielen wiederkehrenden Fragen (Anreise, Haustiere, Zeiten), wenn die Antworten gepflegt vorliegen.

Wann nicht

Kleine Seiten mit fünf Links. Zielgruppen ohne Tastaturgewohnheit, wenn die Palette der einzige Zugang ist. KI-Antworten nicht ohne gepflegte, aktuelle Quellen.

Varianten

  • Reine Suche über Seiten und Inhalte (cmdk-Stil)
  • Ask-AI-Knopf im Header, der direkt Fragen annimmt (wie bei Vercel)
  • Kombiniert: Suche, Aktionen (Sprache wechseln, Adresse kopieren) und KI-Frage in einer Liste
  • Inline-Suchfeld im Header, das zum Dialog wird

Umsetzung

<dialog> mit showModal() liefert Fokusfalle, Escape und ::backdrop. Das Eingabefeld ist eine role=combobox mit aria-controls auf eine role=listbox; die markierte Option meldet aria-activedescendant. Ein globaler keydown-Listener prüft (metaKey || ctrlKey) && key === 'k' und ruft preventDefault auf. Den Index aus Sitemap oder JSON bauen, Fuzzy-Matching mit Fuse.js oder einer eigenen Bewertung. In React gibt es die Bibliothek cmdk. Den Tastaturhinweis je Plattform anzeigen (⌘ oder Strg). Fallstrick: Strg+K ist in manchen Browsern die Suchleiste, deshalb nur abfangen, solange die Seite den Fokus hat. Für das Schließen eine Klasse mit Ausblend-Animation setzen und erst danach close() aufrufen; das cancel-Ereignis dafür abfangen. Die KI-Antwort kommt im echten Einsatz von einem Suchindex plus Sprachmodell über eine eigene Server-Route; Quellen immer mitliefern und anzeigen. KI-Hinweis als feste Zeile zwischen Eingabefeld und Ergebnissen (Kürzel „KI“ in der Hausfarbe, ein fetter Satz, ein Halbsatz zur Quelle), nur in den Varianten mit KI-Frage.

Robust umsetzen

Telefon. Als Vollbild-Blatt mit großem Eingabefeld (mindestens 16 px, sonst zoomt iOS). Einstieg über ein Such-Icon statt über das Kürzel, Tastaturhinweise ausblenden.

Ohne Bewegung. Nur Einblenden oder sofort, ohne Skalierung. KI-Antworten vollständig zeigen statt Wort für Wort.

Barrierefrei. Combobox-Pattern nach WAI-ARIA APG. Die Trefferzahl gesammelt per aria-live=polite ansagen, nicht bei jedem Tastendruck. Gestreamte Antworten mit aria-busy markieren, bis sie fertig sind. Beim Schließen den Fokus zum Auslöser zurückgeben. Kürzel per aria-keyshortcuts am Auslöser angeben; eigene Einzeltasten-Kürzel nur mit Modifikator. KI-Kennzeichnung (Art. 50 KI-VO, seit 02.08.2026): Beantwortet eine KI die Fragen, steht das direkt beim Öffnen der Palette sichtbar über den Ergebnissen, nicht erst in Impressum oder Datenschutz (IHK Halle: „direkt beim Start der Unterhaltung“), etwa „Fragen beantwortet ein KI-Assistent.“. Die Antwort trägt den Hinweis im Kopf („KI-Antwort“). Reine Suche ohne KI braucht ihn nicht. Wortlaut vom Kunden prüfen lassen.

Code

<!DOCTYPE html>
<!--
  Command-Palette (⌘K) und KI-Suche im Header
  Fundus · Navigation · https://cecodesign.de/lab/fundus/navigation/command-palette-und-ki-suche/
  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>Command-Palette (⌘K) und KI-Suche im Header · 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: 'Newsreader'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/newsreader:vf@latest/latin-opsz-normal.woff2') format('woff2-variations'); font-weight: 200 800; font-style: normal; font-display: swap; }
@font-face { font-family: 'Newsreader'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/newsreader:vf@latest/latin-opsz-italic.woff2') format('woff2-variations'); font-weight: 200 800; font-style: italic; font-display: swap; }
@font-face { font-family: 'Mona Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/mona-sans:vf@latest/latin-wdth-normal.woff2') format('woff2-variations'); font-weight: 200 900; font-stretch: 75% 125%; font-display: swap; }
@font-face { font-family: 'Geist Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist-mono:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }

:root {
  /* Rezept boutique-hotel-gastgeber: Sand, Creme, warmes Dunkel, ein Hauston (Kupferpatina der Görlitzer Turmdächer) */
  --sand: #f3eee7; --creme: #fef2e3; --tinte: #3e3a37; --linie: #d5d1c8; --meta: #6b6d68; --grau: #82847f;
  --haus: #55685b; --haus-zart: rgba(85, 104, 91, .12); --licht: #f1d9ae;
  /* Palette: Messwerte des Musters */
  --auf: 180ms; --zu: 120ms;
  --kurve: cubic-bezier(0.2, 0.8, 0.2, 1);
  --breite: 640px;
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--sand); color: var(--tinte); font-family: 'Mona Sans', system-ui, sans-serif; font-stretch: 100%; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; display: grid; grid-template-rows: auto 1fr auto; }
button, input { font: inherit; color: inherit; }
button { background: none; border: 0; padding: 0; cursor: pointer; }
a { color: inherit; text-decoration: none; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.label { font: 500 11px/1.2 'Mona Sans', sans-serif; font-stretch: 118%; letter-spacing: .06em; text-transform: uppercase; }
kbd { display: inline-flex; align-items: center; gap: 3px; font: 500 11px/1 'Mona Sans', sans-serif; font-stretch: 110%; letter-spacing: .02em;
  padding: 4px 6px 3px; border: 1px solid var(--linie); border-bottom-width: 2px; border-radius: 4px; background: var(--sand); color: var(--meta); white-space: nowrap; }
svg.i { width: 18px; height: 18px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }

/* ---------- Kopf ---------- */
.kopf { display: flex; align-items: center; gap: 28px; padding: 18px clamp(16px, 3.4vw, 48px); border-bottom: 1px solid var(--linie); }
.marke { display: grid; line-height: 1; margin-right: auto; }
.marke__name { font: 400 25px/1 'Newsreader', serif; font-variation-settings: 'opsz' 36; letter-spacing: -.01em; }
.marke__ort { margin-top: 5px; color: var(--meta); font-size: 9.5px; letter-spacing: .14em; }
.nav { display: flex; gap: 26px; }
.nav a { color: var(--tinte); transition: color 150ms var(--kurve); }
.nav a:hover { color: var(--haus); }
.kopf__rechts { display: flex; align-items: center; gap: 14px; }
.sprache { color: var(--meta); }
.sprache b { color: var(--tinte); font-weight: 600; }
.buchen { background: var(--haus); color: var(--creme); padding: 12px 18px 11px; border-radius: 3px; }

.ausloeser { display: none; align-items: center; gap: 10px; border: 1px solid var(--linie); border-radius: 999px; height: 40px; padding: 0 6px 0 14px; color: var(--meta);
  font-size: 13.5px; background: rgba(254, 242, 227, .55); transition: border-color 150ms var(--kurve), background-color 150ms var(--kurve), color 150ms var(--kurve); }
.ausloeser:hover, .ausloeser.gedrueckt { border-color: var(--grau); background: var(--creme); color: var(--tinte); }
.ausloeser:focus-visible, .buchen:focus-visible, .opt:focus-visible, .modi button:focus-visible { outline: 2px solid var(--haus); outline-offset: 2px; }
.ausloeser kbd { background: var(--sand); }
.ausloeser--feld { width: 300px; }
.ausloeser--feld span { flex: 1; text-align: left; }
.ausloeser--ki { color: var(--tinte); border-color: rgba(85, 104, 91, .45); }
.ausloeser--ki .punkt { width: 7px; height: 7px; border-radius: 50%; background: var(--haus); box-shadow: 0 0 0 3px var(--haus-zart); }
.ausloeser--icon { width: 40px; padding: 0; justify-content: center; }
[data-variante="suche"] .ausloeser--feld, [data-variante="kombiniert"] .ausloeser--knopf, [data-variante="ki"] .ausloeser--ki { display: inline-flex; }

/* ---------- Bühne ---------- */
.buehne { display: grid; grid-template-rows: 1fr auto; gap: clamp(16px, 2.4vh, 28px); padding: clamp(20px, 3.4vh, 40px) clamp(16px, 3.4vw, 48px) clamp(16px, 2.6vh, 32px); min-height: 0; }
.held { display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr); gap: clamp(24px, 5vw, 80px); align-items: stretch; min-height: 0; }
.held__text { display: grid; gap: clamp(16px, 2.6vh, 28px); align-self: center; padding-bottom: 8px; }
.kicker { color: var(--meta); display: flex; gap: 10px; align-items: center; }
.kicker::before { content: ''; width: 28px; height: 1px; background: currentColor; }
h1 { font: 330 clamp(2.5rem, 1.5rem + 4vw, 5.5rem)/1.04 'Newsreader', serif; font-variation-settings: 'opsz' 72; letter-spacing: -.015em; text-wrap: balance; max-width: 11em; }
h1 em { font-style: italic; font-weight: 300; color: var(--haus); }
.lead { font-size: 17px; line-height: 1.6; max-width: 31em; color: #55514d; }
.frag { display: flex; align-items: center; gap: 8px 12px; flex-wrap: wrap; font-size: 14px; color: var(--meta); border-top: 1px solid var(--linie); max-width: 34em; padding-top: 16px; }
.frag button { color: var(--tinte); text-decoration: underline; text-decoration-color: var(--linie); text-underline-offset: 4px; text-decoration-thickness: 1px; display: inline-flex; gap: 8px; align-items: center; }
.frag button:hover { text-decoration-color: var(--haus); }
.held__bild { position: relative; justify-self: end; width: min(100%, 480px); height: 100%; max-height: 700px; min-height: 300px; border-radius: 999px 999px 4px 4px; overflow: hidden; background: #cbb8a0; }
.held__bild svg { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.held__bild figcaption { position: absolute; left: 0; right: 0; bottom: 0; padding: 14px 18px; display: flex; justify-content: space-between; color: rgba(254, 242, 227, .78); font-size: 10px; }

.buchung { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)) auto; align-items: stretch; background: var(--creme); border: 1px solid var(--linie); border-radius: 4px; }
.feld { display: grid; gap: 6px; align-content: center; padding: 14px 22px; border-right: 1px solid var(--linie); text-align: left; transition: background-color 150ms var(--kurve); }
.feld:hover { background: rgba(243, 238, 231, .7); }
.feld .label { color: var(--meta); font-size: 10px; }
.feld b { font: 400 19px/1.1 'Newsreader', serif; font-variation-settings: 'opsz' 24; }
.feld b small { font: 400 13px 'Mona Sans', sans-serif; color: var(--meta); margin-left: 6px; white-space: nowrap; }
.buchung .los { margin: 8px; padding: 0 26px; background: var(--haus); color: var(--creme); border-radius: 3px; font-size: 12px; }
.buchung.puls { animation: puls 900ms var(--kurve); }
@keyframes puls { 0% { box-shadow: 0 0 0 0 rgba(85, 104, 91, .45); } 100% { box-shadow: 0 0 0 14px rgba(85, 104, 91, 0); } }

/* ---------- Palette ---------- */
.schleier { position: fixed; inset: 0; z-index: 40; background: rgba(46, 41, 37, .38); backdrop-filter: blur(2px); opacity: 0; pointer-events: none; transition: opacity var(--zu) var(--kurve); }
.pal-offen .schleier { opacity: 1; transition-duration: var(--auf); }
.palette { position: fixed; inset: 0; z-index: 50; margin: 12vh auto auto; width: min(var(--breite), calc(100% - 32px)); max-height: 76vh; padding: 0; border: 1px solid var(--linie); border-radius: 10px; overflow: hidden;
  background: var(--creme); color: var(--tinte); box-shadow: 0 1px 0 rgba(255, 255, 255, .7) inset, 0 30px 70px -24px rgba(46, 38, 30, .55), 0 8px 18px -10px rgba(46, 38, 30, .25); flex-direction: column; }
.palette[open] { display: flex; animation: pal-auf var(--auf) var(--kurve) both; transform-origin: 50% 0; }
.palette.schliesst { animation: pal-zu var(--zu) var(--kurve) both; }
.palette::backdrop { background: transparent; }
@keyframes pal-auf { from { opacity: 0; transform: translateY(8px) scale(.96); } }
@keyframes pal-zu { to { opacity: 0; transform: translateY(8px) scale(.96); } }

.pal__kopf { display: flex; align-items: center; gap: 12px; padding: 0 14px 0 20px; height: 62px; border-bottom: 1px solid var(--linie); flex: none; }
.pal__kopf > svg.i { color: var(--grau); width: 20px; height: 20px; }
.pal__ki-ikon { display: none; }
[data-modus="ki"] .pal__kopf > .pal__such-ikon { display: none; }
[data-modus="ki"] .pal__ki-ikon { display: block; color: var(--haus); }
.pal__feld { position: relative; flex: 1; min-width: 0; }
.pal__feld input { width: 100%; border: 0; outline: 0; background: none; font-size: 18px; letter-spacing: -.005em; padding: 0; height: 60px; }
.pal__feld input::placeholder { color: #9b9a94; }
.caret { position: absolute; top: 19px; left: 0; width: 1.5px; height: 22px; background: var(--haus); display: none; animation: blink 1s steps(1) infinite; }
.vorfuehrung .pal-offen .caret { display: block; }
@keyframes blink { 50% { opacity: 0; } }
.pal__esc { display: inline-flex; }
.pal__esc .nur-touch { display: none; }

/* KI-Hinweis (Art. 50 KI-VO): steht beim Öffnen sichtbar über den Ergebnissen, also direkt beim Start der Unterhaltung */
.pal__ki { display: flex; align-items: center; gap: 10px; padding: 9px 20px; border-bottom: 1px solid var(--linie); background: var(--haus-zart); color: var(--tinte); font-size: 13px; line-height: 1.4; flex: none; }
.pal__ki b { flex: none; font: 600 10px/1 'Geist Mono', ui-monospace, monospace; letter-spacing: .12em; color: var(--creme); background: var(--haus); border-radius: 4px; padding: 4px 6px 3px; }
.pal__ki span { color: var(--tinte); }
.pal__ki strong { color: var(--tinte); font-weight: 600; }
[data-variante="suche"] .pal__ki { display: none; }
.pal__koerper { overflow-y: auto; max-height: 60vh; overscroll-behavior: contain; scroll-padding: 34px 0 8px; flex: 1 1 auto; }
.liste { padding: 6px 8px 8px; }
.gruppe { padding: 14px 12px 6px; color: var(--meta); font-size: 10px; }
.opt { display: grid; grid-template-columns: 32px minmax(0, 1fr) auto; align-items: center; gap: 12px; padding: 9px 12px; border-radius: 6px; cursor: pointer; position: relative; }
.opt__ikon { width: 32px; height: 32px; border-radius: 6px; display: grid; place-items: center; background: var(--sand); color: var(--grau); border: 1px solid rgba(213, 209, 200, .7); }
.opt__titel { display: block; font-size: 15px; line-height: 1.25; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.opt__meta { display: block; font-size: 12.5px; line-height: 1.35; color: var(--meta); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px; }
.opt__rechts { font-size: 13px; color: var(--meta); white-space: nowrap; display: flex; gap: 10px; align-items: center; }
.opt__rechts .enter { display: none; color: var(--haus); font-size: 15px; }
.opt mark { background: none; color: inherit; font-weight: 640; box-shadow: inset 0 -2px 0 rgba(85, 104, 91, .35); }
/* Markierung springt sofort (wie cmdk), nur Farbe mit UI-Tempo */
.opt[aria-selected="true"] { background: var(--haus-zart); }
.opt[aria-selected="true"]::before { content: ''; position: absolute; left: 0; top: 10px; bottom: 10px; width: 2px; border-radius: 2px; background: var(--haus); }
.opt[aria-selected="true"] .opt__ikon { color: var(--haus); background: var(--creme); }
.opt[aria-selected="true"] .opt__rechts .enter { display: inline; }
.opt--ki .opt__ikon { background: var(--haus); color: var(--creme); border-color: var(--haus); }
.opt--ki[aria-selected="true"] .opt__ikon { background: var(--haus); color: var(--creme); }
.opt--ki .opt__titel { font: italic 400 17px/1.2 'Newsreader', serif; font-variation-settings: 'opsz' 20; }
.leer { padding: 28px 20px; color: var(--meta); font-size: 14px; text-align: center; }

.antwort { padding: 22px 24px 20px; display: grid; gap: 14px; }
.antwort[hidden] { display: none; }
.antwort__kopf { display: flex; justify-content: space-between; align-items: center; color: var(--meta); font-size: 10px; }
.antwort__kopf span:first-child { display: inline-flex; gap: 8px; align-items: center; color: var(--haus); }
.antwort__kopf span:first-child::before { content: ''; width: 7px; height: 7px; border-radius: 50%; background: var(--haus); box-shadow: 0 0 0 3px var(--haus-zart); }
.antwort__frage { font: italic 380 24px/1.2 'Newsreader', serif; font-variation-settings: 'opsz' 30; letter-spacing: -.01em; }
.antwort__text { font-size: 15.5px; line-height: 1.62; color: #47433f; min-height: 4.9em; max-width: 40em; }
.antwort__text .wort { animation: wort 240ms var(--kurve) both; }
@keyframes wort { from { opacity: 0; } }
.quellen { display: flex; flex-wrap: wrap; gap: 8px; list-style: none; padding: 0; opacity: 0; transition: opacity 300ms var(--kurve); }
.quellen.da { opacity: 1; }
.quellen a { display: inline-flex; align-items: center; gap: 8px; padding: 7px 11px 6px; border: 1px solid var(--linie); border-radius: 999px; font-size: 12.5px; background: var(--sand); transition: border-color 150ms var(--kurve); }
.quellen a:hover { border-color: var(--haus); }
.quellen a::before { content: '↗'; color: var(--haus); font-size: 11px; }
.antwort__fuss { font-size: 12px; color: var(--meta); border-top: 1px solid var(--linie); padding-top: 12px; display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.antwort__fuss button { color: var(--tinte); text-decoration: underline; text-underline-offset: 3px; text-decoration-color: var(--linie); }

.pal__fuss { display: flex; justify-content: space-between; align-items: center; gap: 16px; padding: 10px 16px; border-top: 1px solid var(--linie); background: rgba(243, 238, 231, .6); color: var(--meta); font-size: 12px; flex: none; }
.pal__fuss .tasten-hilfe { display: flex; gap: 14px; align-items: center; }
.pal__fuss .tasten-hilfe span { display: inline-flex; gap: 6px; align-items: center; }
[data-antwortet] .tasten-hilfe span:nth-child(-n+2) { display: none; }
.pal__fuss kbd { padding: 3px 5px 2px; font-size: 10.5px; background: var(--creme); }

/* ---------- Vorführung: gedrückte Tasten und Hinweis ---------- */
.tastatur { position: fixed; left: 50%; bottom: 26px; z-index: 70; translate: -50% 0; display: flex; gap: 6px; pointer-events: none; }
.tastatur kbd { font-size: 15px; padding: 9px 13px 8px; min-width: 40px; justify-content: center; background: var(--tinte); color: var(--creme); border-color: #2a2725; border-bottom-width: 3px; border-radius: 7px; box-shadow: 0 10px 24px -10px rgba(0, 0, 0, .5);
  animation: taste 150ms var(--kurve) both; }
.tastatur.weg kbd { opacity: 0; transition: opacity 200ms linear; }
@keyframes taste { from { opacity: 0; transform: translateY(6px) scale(.92); } }

.meldung { position: fixed; left: 50%; top: 92px; z-index: 65; translate: -50% 8px; opacity: 0; pointer-events: none; display: flex; gap: 10px; align-items: center;
  background: var(--tinte); color: var(--creme); padding: 12px 16px; border-radius: 6px; font-size: 13.5px; white-space: nowrap; max-width: calc(100% - 32px);
  transition: opacity var(--zu) var(--kurve), translate var(--zu) var(--kurve); }
.meldung.da { opacity: 1; translate: -50% 0; transition-duration: var(--auf); }
.meldung code { font: 400 12px 'Geist Mono', monospace; color: var(--licht); }

/* ---------- Demo-Leiste ---------- */
.leiste { display: flex; justify-content: space-between; align-items: center; gap: 20px; padding: 12px clamp(16px, 3.4vw, 48px) 16px; border-top: 1px solid var(--linie); }
.mono { font: 500 11px/1.4 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--meta); }
.modi { display: flex; align-items: center; gap: 4px; }
.modi > span { margin-right: 8px; }
.modi button { white-space: nowrap; font: inherit; letter-spacing: inherit; text-transform: inherit; color: var(--meta); padding: 6px 10px; border-radius: 999px; border: 1px solid transparent; }
.modi button[aria-pressed="true"] { color: var(--tinte); border-color: var(--linie); background: var(--creme); }
.werte { text-align: right; letter-spacing: .06em; text-transform: none; }
.werte b { font-weight: 500; color: var(--tinte); }
.werte__red, .werte__mobil { display: none; }
.reduziert .werte__red { display: inline; }
.reduziert .werte__lang { display: none; }

/* ---------- Vorschau 960 × 720: nur die Bühne ---------- */
.vorschau body { overflow: hidden; height: 100svh; grid-template-rows: auto 1fr; }
.vorschau .leiste { display: none; }


/* ---------- Schmal ---------- */
@media (max-width: 1100px) { .nav { display: none; } }
@media (max-width: 860px) {
  .sprache { display: none; }
  .ausloeser--feld { width: 220px; }
  .held { grid-template-columns: 1fr; align-items: start; }
  .held__bild { justify-self: stretch; width: 100%; height: 380px; border-radius: 240px 240px 4px 4px; }
  .buchung { grid-template-columns: 1fr 1fr; }
  .feld:nth-child(2) { border-right: 0; }
  .feld:nth-child(3) { grid-column: 1 / -1; border-top: 1px solid var(--linie); border-right: 0; }
  .buchung .los { grid-column: 1 / -1; padding: 16px; }
  .leiste { flex-direction: column; align-items: flex-start; gap: 10px; }
  .werte { text-align: left; }
  .werte__lang { display: none; }
  :root:not(.reduziert) .werte__mobil { display: inline; }
}
@media (max-width: 640px) {
  .kopf { gap: 10px; padding-block: 14px; }
  .marke__name { font-size: 22px; }
  [data-variante] .ausloeser { display: none; }
  [data-variante] .ausloeser--icon { display: inline-flex; }
  .buchen { padding: 11px 14px 10px; }
  .held__bild { height: 300px; }
  .lead { font-size: 16px; }
  .feld { padding: 12px 16px; }
  .feld b small { display: block; margin: 5px 0 0; }
  .frag .nur-tastatur { display: none; }
  .frag .nur-touch { display: inline; }
  .meldung { top: 78px; }
  .modi { flex-wrap: wrap; }
  /* Palette als Vollbild-Blatt mit großem Feld */
  .palette { margin: 0; width: 100%; max-width: none; height: 100dvh; max-height: none; border: 0; border-radius: 0; }
  @keyframes pal-auf { from { opacity: 0; transform: translateY(24px); } }
  @keyframes pal-zu { to { opacity: 0; transform: translateY(24px); } }
  .pal__kopf { height: 68px; padding: 0 8px 0 16px; }
  .pal__feld input { font-size: 19px; height: 66px; }
  .pal__koerper { max-height: none; }
  .pal__ki { padding: 9px 16px; font-size: 12.5px; align-items: flex-start; }
  .pal__ki b { margin-top: 1px; }
  .pal__esc kbd { display: none; }
  .pal__esc .nur-touch { display: inline; padding: 10px 8px; color: var(--haus); font-size: 15px; }
  .pal__fuss .tasten-hilfe { display: none; }
  .opt { padding: 11px 10px; }
  .opt__rechts { font-size: 12px; }
  .opt__rechts kbd { display: none; }
  .antwort { padding: 20px 18px; }
  .antwort__frage { font-size: 22px; }
}
.ausloeser--icon { display: none; }
@media (hover: none) {
  .ausloeser kbd, .frag kbd, .opt__rechts kbd, .pal__esc kbd { display: none; }
  .pal__esc .nur-touch { display: inline; padding: 10px 8px; color: var(--haus); font-size: 15px; }
  .pal__fuss .tasten-hilfe { display: none; }
  .frag .nur-tastatur { display: none; }
}
@media (hover: hover) and (min-width: 641px) { .frag .nur-touch { display: none; } }

/* ---------- Reduzierte Bewegung: nur Überblenden ---------- */
@keyframes nur-blende-auf { from { opacity: 0; } }
@keyframes nur-blende-zu { to { opacity: 0; } }
.reduziert .palette[open] { animation: nur-blende-auf 100ms linear both; }
.reduziert .palette.schliesst { animation: nur-blende-zu 100ms linear both; }
.reduziert .schleier { transition: opacity 100ms linear; }
.reduziert .antwort__text .wort { animation: none; }
.reduziert .tastatur kbd { animation: none; }
.reduziert .meldung { transition: opacity 100ms linear; translate: -50% 0; }
.reduziert .buchung.puls { animation: none; outline: 2px solid var(--haus); outline-offset: 3px; }
.reduziert .caret { animation: none; }
@media (forced-colors: active) {
  button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; }
}
</style>
</head>
<body data-variante="kombiniert">
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
  <symbol id="i-such" viewBox="0 0 20 20"><circle cx="8.6" cy="8.6" r="5.6"/><path d="m12.8 12.8 4.2 4.2"/></symbol>
  <symbol id="i-frage" viewBox="0 0 20 20"><path d="M3.5 9.5c0-3.3 2.9-5.5 6.5-5.5s6.5 2.2 6.5 5.5S13.6 15 10 15c-.9 0-1.7-.1-2.5-.4L4 16l.9-3c-.9-1-1.4-2.2-1.4-3.5Z"/><path d="M8.2 8.2a1.9 1.9 0 0 1 3.6.6c0 1.2-1.8 1.4-1.8 2.4"/><path d="M10 12.9v.1"/></symbol>
  <symbol id="i-bett" viewBox="0 0 20 20"><path d="M2.5 15.5v-9M2.5 12.5h15v3M17.5 12.5v-2a2 2 0 0 0-2-2h-6v4"/><circle cx="5.8" cy="9.8" r="1.6"/></symbol>
  <symbol id="i-seite" viewBox="0 0 20 20"><path d="M5 2.8h6.5L15 6.3v10.9H5Z"/><path d="M11.3 2.8v3.7H15M7.6 10h4.8M7.6 13h4.8"/></symbol>
  <symbol id="i-blitz" viewBox="0 0 20 20"><path d="M11 2.5 4.8 11h4.6L8.8 17.5 15.2 9h-4.6Z"/></symbol>
  <symbol id="i-uhr" viewBox="0 0 20 20"><circle cx="10" cy="10" r="6.8"/><path d="M10 6.2V10l2.6 1.7"/></symbol>
</svg>

<header class="kopf">
  <a class="marke" href="#start" aria-label="Hallenhaus Neun, Startseite"><span class="marke__name">Hallenhaus Neun</span><span class="marke__ort label">Gästehaus · Görlitz</span></a>
  <nav class="nav label" aria-label="Hauptnavigation"><a href="#zimmer">Zimmer</a><a href="#fruehstueck">Frühstück</a><a href="#altstadt">Altstadt</a><a href="#anreise">Anreise</a></nav>
  <div class="kopf__rechts">
    <button class="ausloeser ausloeser--feld" type="button" data-ausloeser aria-haspopup="dialog" aria-keyshortcuts="Control+K Meta+K"><svg class="i" aria-hidden="true"><use href="#i-such"/></svg><span>Zimmer, Frühstück, Anreise …</span><kbd data-kuerzel>Strg K</kbd></button>
    <button class="ausloeser ausloeser--knopf" type="button" data-ausloeser aria-haspopup="dialog" aria-keyshortcuts="Control+K Meta+K"><svg class="i" aria-hidden="true"><use href="#i-such"/></svg><span>Suchen</span><kbd data-kuerzel>Strg K</kbd></button>
    <button class="ausloeser ausloeser--ki" type="button" data-ausloeser aria-haspopup="dialog" aria-keyshortcuts="Control+K Meta+K"><i class="punkt" aria-hidden="true"></i><span>Gastgeber fragen</span><kbd data-kuerzel>Strg K</kbd></button>
    <button class="ausloeser ausloeser--icon" type="button" data-ausloeser aria-haspopup="dialog" aria-label="Suchen oder fragen"><svg class="i" aria-hidden="true"><use href="#i-such"/></svg></button>
    <span class="sprache label" data-sprache><b>DE</b> / EN</span>
    <a class="buchen label" href="#buchen">Buchen</a>
  </div>
</header>

<main class="buehne" id="start">
  <section class="held">
    <div class="held__text">
      <p class="kicker label">Neun Zimmer · Altstadt Görlitz · seit 2019</p>
      <h1>Schlafen im Haus der <em>Tuchhändler</em>.</h1>
      <p class="lead">Ein Görlitzer Hallenhaus von 1526, behutsam saniert. Frühstück unter dem Kreuzgewölbe, zwei Minuten zur Neiße und über die Brücke nach Polen.</p>
      <p class="frag"><span>Hund dabei? Spät dran?</span>
        <button type="button" data-ausloeser><span class="nur-tastatur">Fragen Sie das Haus</span><span class="nur-touch">Tippen und fragen</span><kbd data-kuerzel class="nur-tastatur">Strg K</kbd></button>
        <span>Die Antworten kommen aus unseren Hausinfos, auch nachts.</span></p>
    </div>
    <figure class="held__bild" aria-label="Zeichnung: Hallenhaus mit Renaissancegiebel vor den Türmen der Peterskirche im Abendlicht">
      <svg viewBox="0 0 600 800" preserveAspectRatio="xMidYMax slice" aria-hidden="true">
        <rect width="600" height="800" fill="#cbb8a0"/>
        <circle cx="466" cy="392" r="24" fill="#f6efe4" opacity=".92"/>
        <g fill="#a69684">
          <path d="M176 560V404l124-70 124 70v156Z"/>
          <rect x="242" y="252" width="38" height="172"/><path d="M242 254 261 84l19 170Z"/>
          <rect x="320" y="252" width="38" height="172"/><path d="M320 254 339 84l19 170Z"/>
          <rect x="250" y="300" width="22" height="3" fill="#97887a"/><rect x="328" y="300" width="22" height="3" fill="#97887a"/>
        </g>
        <g fill="#867868">
          <path d="M0 800V500h18l28-44 28 44h10v-16c0-14 12-24 24-24v-14h20v14c12 0 24 10 24 24v16h20l30-50 30 50h14V800Z"/>
          <path d="M600 800V506h-16l-30-46-30 46h-12v-18c0-12-10-22-22-22v-16h-18v16c-12 0-22 10-22 22v18h-16l-26-40-26 40h-8V800Z"/>
        </g>
        <g fill="#74675a">
          <rect x="30" y="540" width="14" height="20"/><rect x="62" y="540" width="14" height="20"/><rect x="110" y="520" width="14" height="20"/>
          <rect x="474" y="545" width="14" height="20"/><rect x="540" y="545" width="14" height="20"/>
        </g>
        <rect x="140" y="520" width="14" height="20" fill="#e8cfa2"/><rect x="508" y="545" width="14" height="20" fill="#e8cfa2"/>
        <path fill="#4a433d" d="M92 800V566h60c0-26 18-40 34-40v-30h28c0-24 18-38 34-38v-28h22c4-22 16-34 30-38 14 4 26 16 30 38h22v28c16 0 34 14 34 38h28v30c16 0 34 14 34 40h60V800Z"/>
        <rect x="86" y="562" width="428" height="8" fill="#5b534c"/><rect x="86" y="694" width="428" height="5" fill="#5b534c"/>
        <path fill="#f1d9ae" d="M286 446c0-9 6-15 14-15s14 6 14 15v22h-28Z"/>
        <g fill="#5d554e">
          <rect x="210" y="482" width="20" height="28"/><rect x="370" y="482" width="20" height="28"/>
          <rect x="122" y="592" width="32" height="46"/><rect x="246" y="592" width="32" height="46"/><rect x="384" y="592" width="32" height="46"/><rect x="446" y="592" width="32" height="46"/>
          </g>
        <g fill="#f1d9ae">
          <rect x="184" y="592" width="32" height="46"/><rect x="322" y="592" width="32" height="46"/>
        </g>
        <g fill="#2a2623">
          <path d="M126 800v-52a44 44 0 0 1 88 0v52Z"/><path d="M256 800v-52a44 44 0 0 1 88 0v52Z"/><path d="M386 800v-52a44 44 0 0 1 88 0v52Z"/>
        </g>
        <path d="M282 800v-34a18 18 0 0 1 36 0v34Z" fill="#e3c48f" opacity=".85"/>
        <path fill="#3e3a37" d="M0 800V598l46-46 46 46V800Z"/><path fill="#3e3a37" d="M508 800V610l46-52 46 52V800Z"/>
        <rect x="34" y="640" width="24" height="34" fill="#f1d9ae"/><rect x="542" y="650" width="24" height="34" fill="#55504a"/>
        <circle cx="548" cy="716" r="20" fill="#f1d9ae" opacity=".22"/><circle cx="548" cy="716" r="5" fill="#f6e3bf"/>
        <rect x="547" y="721" width="2" height="79" fill="#2a2623"/>
      </svg>
      <figcaption class="label"><span>Hallenhaus · 1526</span><span>Abends, Untermarkt</span></figcaption>
    </figure>
  </section>

  <form class="buchung" id="buchen" data-buchung onsubmit="return false">
    <button type="button" class="feld"><span class="label">Anreise</span><b>Fr, 13. Nov.</b></button>
    <button type="button" class="feld"><span class="label">Abreise</span><b>So, 15. Nov.<small>2 Nächte</small></b></button>
    <button type="button" class="feld"><span class="label">Gäste</span><b>2 Erwachsene</b></button>
    <button type="submit" class="los label">Verfügbarkeit prüfen</button>
  </form>
</main>

<div class="leiste">
  <div class="modi mono" role="group" aria-label="Variante">
    <span>Variante</span>
    <button type="button" data-modus-knopf="suche" aria-pressed="false">Suche</button>
    <button type="button" data-modus-knopf="kombiniert" aria-pressed="true">Kombiniert</button>
    <button type="button" data-modus-knopf="ki" aria-pressed="false">KI-Frage</button>
  </div>
  <p class="werte mono"><span class="werte__lang" data-werte></span><span class="werte__mobil">Unter 640 px: Vollbild-Blatt · Öffnen <b>180 ms</b> · y <b>24 px</b></span><span class="werte__red">Reduziert: nur Überblenden <b>100 ms</b> linear · Antwort erscheint ganz</span></p>
</div>

<div class="schleier" aria-hidden="true"></div>
<dialog class="palette" aria-label="Suchen oder das Haus fragen" data-palette>
  <div class="pal__kopf">
    <svg class="i pal__such-ikon" aria-hidden="true"><use href="#i-such"/></svg>
    <svg class="i pal__ki-ikon" aria-hidden="true"><use href="#i-frage"/></svg>
    <div class="pal__feld">
      <label class="sr" for="pal-eingabe">Suchen oder eine Frage stellen</label>
      <input id="pal-eingabe" type="text" role="combobox" aria-expanded="true" aria-controls="pal-liste" aria-autocomplete="list" autocomplete="off" spellcheck="false" enterkeyhint="go" data-eingabe>
      <span class="caret" data-caret aria-hidden="true"></span>
    </div>
    <button class="pal__esc" type="button" data-schliessen aria-label="Schließen"><kbd>esc</kbd><span class="nur-touch" aria-hidden="true">Abbrechen</span></button>
  </div>
  <p class="pal__ki" id="pal-ki"><b aria-hidden="true">KI</b><span><strong>Fragen beantwortet ein KI-Assistent.</strong> Er antwortet nur aus den Hausinfos.</span></p>
  <div class="pal__koerper" data-koerper>
    <div class="liste" id="pal-liste" role="listbox" aria-label="Ergebnisse" data-liste></div>
    <section class="antwort" hidden data-antwort aria-labelledby="antwort-frage">
      <div class="antwort__kopf label"><span>KI-Antwort aus den Hausinfos</span><span>Stand Oktober 2026</span></div>
      <p class="antwort__frage" id="antwort-frage" data-frage></p>
      <p class="antwort__text" data-text aria-live="polite"></p>
      <ul class="quellen" data-quellen aria-label="Quellen"></ul>
      <div class="antwort__fuss"><span>Von einem KI-Assistenten aus den Hausinfos zusammengestellt. Verbindlich ist Ihre Buchungsbestätigung.</span><button type="button" data-zurueck>Zur Suche</button></div>
    </section>
  </div>
  <div class="pal__fuss">
    <div class="tasten-hilfe"><span><kbd>↑</kbd><kbd>↓</kbd> wählen</span><span><kbd>↵</kbd> öffnen</span><span><kbd>esc</kbd> schließen</span></div>
    <span data-anzahl></span>
  </div>
  <p class="sr" aria-live="polite" data-status></p>
</dialog>
<div class="meldung" role="status" data-meldung></div>
<div class="tastatur" aria-hidden="true" data-tastatur></div>

<script>
(() => {
  const html = document.documentElement, body = document.body;
  const $ = s => document.querySelector(s), $$ = s => [...document.querySelectorAll(s)];
  const reduziert = () => html.classList.contains('reduziert');
  const dlg = $('[data-palette]'), eingabe = $('[data-eingabe]'), liste = $('[data-liste]'), koerper = $('[data-koerper]');
  const antwort = $('[data-antwort]'), frageEl = $('[data-frage]'), textEl = $('[data-text]'), quellenEl = $('[data-quellen]');
  const statusEl = $('[data-status]'), anzahlEl = $('[data-anzahl]'), caret = $('[data-caret]');
  const meldungEl = $('[data-meldung]'), tastatur = $('[data-tastatur]'), werteEl = $('[data-werte]');

  const AUF = 180, ZU = 120, ZU_RED = 100, WORT_MS = 32;
  const mac = /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
  const KUERZEL = mac ? ['⌘', 'K'] : ['Strg', 'K'];
  $$('[data-kuerzel]').forEach(k => k.textContent = KUERZEL.join(mac ? '' : ' '));

  // ---------- Inhalte: Index des Hauses ----------
  const EINTRAEGE = [
    { id: 'gewoelbe', gruppe: 'zimmer', titel: 'Gewölbezimmer', meta: '24 m² · Doppelbett · Kreuzgewölbe, Erdgeschoss', rechts: 'ab 138 €', worte: 'zimmer erdgeschoss stufenlos hund doppelbett', pfad: '/zimmer/gewoelbezimmer' },
    { id: 'turm', gruppe: 'zimmer', titel: 'Turmstube', meta: '19 m² · Doppelbett · Blick auf die Peterskirche', rechts: 'ab 118 €', worte: 'zimmer turm kirche ausblick', pfad: '/zimmer/turmstube' },
    { id: 'suite', gruppe: 'zimmer', titel: 'Neißesuite', meta: '42 m² · Wohnraum · Blick über die Neiße', rechts: 'ab 196 €', worte: 'zimmer suite fluss familie gross', pfad: '/zimmer/neissesuite' },
    { id: 'dach', gruppe: 'zimmer', titel: 'Dachkammer', meta: '16 m² · Einzelbett · offene Balken', rechts: 'ab 84 €', worte: 'zimmer einzel allein guenstig', pfad: '/zimmer/dachkammer' },
    { id: 'frueh', gruppe: 'seiten', titel: 'Frühstück im Hallenraum', meta: '7:30 bis 10:30 Uhr, sonntags bis 11 Uhr', worte: 'essen kaffee morgen', pfad: '/fruehstueck' },
    { id: 'anreise', gruppe: 'seiten', titel: 'Anreise & Parken', meta: 'Vier Stellplätze im Hof · Bahnhof 15 Minuten zu Fuß', worte: 'auto parken bahn zug parkplatz anfahrt', pfad: '/anreise' },
    { id: 'altstadt', gruppe: 'seiten', titel: 'Altstadt-Tipps', meta: 'Untermarkt, Peterskirche, Altstadtbrücke', worte: 'tipps ausflug polen zgorzelec spaziergang', pfad: '/altstadt' },
    { id: 'regeln', gruppe: 'seiten', titel: 'Hausregeln', meta: 'Haustiere, Ruhezeiten, Stornierung', worte: 'hund haustier storno ruhe', pfad: '/hausregeln' },
    { id: 'gutschein', gruppe: 'seiten', titel: 'Gutschein verschenken', meta: 'Eine Nacht im Gewölbe, als Karte oder PDF', worte: 'geschenk', pfad: '/gutschein' },
    { id: 'verf', gruppe: 'aktionen', titel: 'Verfügbarkeit prüfen', meta: 'Springt zur Buchungsleiste', kbd: '⇧ B', taste: 'B', worte: 'buchen frei termin' },
    { id: 'anruf', gruppe: 'aktionen', titel: 'Rezeption anrufen', meta: 'Täglich 8 bis 20 Uhr', kbd: '⇧ R', taste: 'R', worte: 'telefon kontakt' },
    { id: 'adresse', gruppe: 'aktionen', titel: 'Adresse kopieren', meta: 'Für Navi oder Taxi', kbd: '⇧ C', taste: 'C', worte: 'navi' },
    { id: 'english', gruppe: 'aktionen', titel: 'Switch to English', meta: 'Sprache wechseln', kbd: '⇧ E', taste: 'E', worte: 'englisch sprache language' },
  ];
  const ZULETZT = ['turm', 'frueh'];
  const VORSCHLAEGE = [['Darf unser Hund mit?', 'Hausregeln · Haustiere'], ['Wo parken wir mit dem Auto?', 'Anreise & Parken'], ['Wann gibt es Frühstück?', 'Frühstück im Hallenraum'], ['Wir kommen erst um 23 Uhr an. Geht das?', 'Check-in und Schlüsselkasten']];
  const GRUPPEN = { zuletzt: 'Zuletzt angesehen', zimmer: 'Zimmer', seiten: 'Seiten', aktionen: 'Aktionen', fragen: 'Häufige Fragen', ki: 'Frage an das Haus', passend: 'Passende Seiten' };
  const IKON = { zimmer: 'bett', seiten: 'seite', aktionen: 'blitz', zuletzt: 'uhr', fragen: 'frage', ki: 'frage' };
  const ANTWORTEN = [
    { such: /hund|haustier|katze|tier/, text: 'Ja, Hunde sind bei uns willkommen. Wir berechnen 15 € pro Nacht, Decke und Napf liegen im Zimmer bereit. Am besten passt das Gewölbezimmer im Erdgeschoss: Steinboden und direkter Zugang zum Hof. Bitte geben Sie den Hund bei der Buchung an.', quellen: ['regeln', 'gewoelbe'] },
    { such: /park|auto|wagen|garage|stellplatz/, text: 'Im Innenhof haben wir vier Stellplätze für 12 € pro Nacht. Bitte reservieren Sie einen bei der Buchung. Ist der Hof voll, parken Sie am Rand der Altstadt und gehen etwa acht Minuten zu Fuß. Zum Ausladen dürfen Sie kurz vor dem Hoftor halten.', quellen: ['anreise', 'altstadt'] },
    { such: /fruh|fruehst|kaffee|essen|morgen/, text: 'Frühstück gibt es von 7:30 bis 10:30 Uhr unter dem Kreuzgewölbe im Hallenraum, sonntags bis 11 Uhr. Brot und Brötchen kommen vom Bäcker zwei Häuser weiter. Reisen Sie früh ab, packen wir Ihnen ab 6 Uhr eine Frühstückstüte.', quellen: ['frueh'] },
    { such: /spat|23|22|nacht|schluessel|check|anreise|ankommen|kommen/, text: 'Das geht. Check-in ist von 15 bis 20 Uhr. Kommen Sie später, schicken wir Ihnen am Anreisetag einen Code für den Schlüsselkasten am Hoftor. Ihr Zimmer ist dann vorbereitet, die Nachtlampe in der Halle brennt.', quellen: ['anreise', 'regeln'] },
    { such: /barriere|rollstuhl|aufzug|treppe|stufe/, text: 'Das Haus ist von 1526 und hat keinen Aufzug. Das Gewölbezimmer im Erdgeschoss erreichen Sie stufenlos über den Hof, das Bad hat eine bodengleiche Dusche. Die übrigen Zimmer liegen über eine Holztreppe im ersten und zweiten Stock.', quellen: ['gewoelbe', 'regeln'] },
    { such: /polen|zgorzelec|brucke|bruecke|altstadt|ausflug|tipp/, text: 'Über die Altstadtbrücke sind Sie in fünf Minuten zu Fuß in Zgorzelec auf der polnischen Seite der Neiße. Für den Abend empfehlen wir den Weg am Fluss entlang zur Peterskirche. Unsere Lieblingsorte stehen in den Altstadt-Tipps.', quellen: ['altstadt'] },
  ];
  const UNBEKANNT = { text: 'Dazu finde ich in unseren Hausinfos nichts Sicheres. Schreiben Sie uns oder rufen Sie an: Die Rezeption ist täglich von 8 bis 20 Uhr besetzt und antwortet auf Nachrichten meist innerhalb einer Stunde.', quellen: ['anruf'] };
  const STOPP = new Set(['darf', 'unser', 'unsere', 'kann', 'koennen', 'konnen', 'gibt', 'habt', 'haben', 'eine', 'einen', 'einem', 'wann', 'wird', 'welche', 'warum', 'erst', 'geht', 'dass', 'auch', 'noch', 'mit', 'wir', 'ich', 'ihr']);
  const byId = Object.fromEntries(EINTRAEGE.map(e => [e.id, e]));

  // ---------- Fuzzy-Suche ohne Bibliothek ----------
  // Faltung mit Positionskarte, damit ä/ö/ü/ß gefunden und trotzdem richtig markiert werden
  function falte(s) {
    let f = '', karte = [];
    for (let i = 0; i < s.length; i++) {
      const c = s[i].toLowerCase(), r = c === 'ä' ? 'a' : c === 'ö' ? 'o' : c === 'ü' ? 'u' : c === 'ß' ? 'ss' : c;
      for (const z of r) { f += z; karte.push(i); }
    }
    return { f, karte };
  }
  const fl = s => falte(s).f;
  EINTRAEGE.forEach(e => { e._t = falte(e.titel); e._w = fl(e.worte + ' ' + e.meta); });
  function frageArtig(q) {
    const w = fl(q).trim();
    return /\?\s*$/.test(q) || /^(wie|wo|wann|was|kann|konnen|darf|gibt|habt|haben|ist|sind|welche|warum|muss|wir|ich)\b/.test(w) || w.split(/\s+/).length >= 4;
  }
  function treffer(e, wort) {
    const t = e._t.f, p = t.indexOf(wort);
    if (p >= 0) {
      const marks = []; for (let k = p; k < p + wort.length; k++) marks.push(e._t.karte[k]);
      return { score: p === 0 ? 100 : /[\s&-]/.test(t[p - 1]) ? 85 : 60, marks };
    }
    if (e._w.includes(wort)) return { score: 40, marks: [] };
    // Teilfolge: alle Buchstaben in Reihenfolge, nicht zu verstreut
    let j = 0, marks = [], start = -1;
    for (let k = 0; k < t.length && j < wort.length; k++) if (t[k] === wort[j]) { if (start < 0) start = k; marks.push(e._t.karte[k]); j++; }
    if (j === wort.length && wort.length > 1 && marks[marks.length - 1] - marks[0] < wort.length * 2.5) return { score: 20, marks };
    return null;
  }
  function suche(q, nurInhalt) {
    const qf = fl(q).replace(/[?!.,]/g, ' ').trim();
    if (!qf) return [];
    const frage = frageArtig(q);
    let woerter = qf.split(/\s+/);
    if (frage) woerter = woerter.filter(w => w.length >= 4 && !STOPP.has(w));
    if (!woerter.length) return [];
    const out = [];
    for (const e of EINTRAEGE) {
      if (nurInhalt && e.gruppe === 'aktionen') continue;
      let score = 0, marks = [], alle = true, eins = false;
      for (const w of woerter) { const t = treffer(e, w); if (t) { score += t.score; marks.push(...t.marks); eins = true; } else alle = false; }
      if (frage ? eins : alle) out.push({ e, score, marks: new Set(marks) });
    }
    return out.sort((a, b) => b.score - a.score);
  }

  // ---------- Zustand ----------
  let variante = 'kombiniert', optionen = [], aktiv = 0, offen = false, zuTimer = null, ausloeser = null;
  let streamTimer = null, meldungTimer = null, tastenTimer = null, vorf = false;

  const esc = s => s.replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
  function markiert(text, marks) {
    if (!marks || !marks.size) return esc(text);
    let s = '', an = false;
    for (let i = 0; i < text.length; i++) {
      const m = marks.has(i);
      if (m && !an) { s += '<mark>'; an = true; } else if (!m && an) { s += '</mark>'; an = false; }
      s += esc(text[i]);
    }
    return s + (an ? '</mark>' : '');
  }
  const ikon = n => `<span class="opt__ikon" aria-hidden="true"><svg class="i"><use href="#i-${n}"/></svg></span>`;

  // ---------- Liste aufbauen ----------
  function rendern() {
    const q = eingabe.value, leer = !q.trim();
    const gruppen = [];
    const add = (name, rows) => { if (rows.length) gruppen.push({ name, rows }); };
    const eintragRow = (e, marks, gruppe) => ({ typ: 'eintrag', e, marks, gruppe: gruppe || e.gruppe });
    const kiRow = { typ: 'ki', frage: q.trim() };

    if (variante === 'ki') {
      if (leer) add('fragen', VORSCHLAEGE.map(([f, m]) => ({ typ: 'vorschlag', frage: f, meta: m })));
      else {
        add('ki', [kiRow]);
        add('passend', suche(q, true).slice(0, 3).map(r => eintragRow(r.e, r.marks, 'passend')));
      }
    } else if (leer) {
      add('zuletzt', ZULETZT.map(id => eintragRow(byId[id], null, 'zuletzt')));
      add('zimmer', EINTRAEGE.filter(e => e.gruppe === 'zimmer').map(e => eintragRow(e)));
      if (variante === 'kombiniert') add('aktionen', EINTRAEGE.filter(e => e.gruppe === 'aktionen').map(e => eintragRow(e)));
      else add('seiten', EINTRAEGE.filter(e => e.gruppe === 'seiten').map(e => eintragRow(e)));
    } else {
      const r = suche(q, variante === 'suche');
      const frage = frageArtig(q);
      if (variante === 'kombiniert' && frage) add('ki', [kiRow]);
      ['zimmer', 'seiten', 'aktionen'].forEach(g => add(g, r.filter(x => x.e.gruppe === g).map(x => eintragRow(x.e, x.marks))));
      if (variante === 'kombiniert' && !frage) add('ki', [kiRow]);
    }

    optionen = [];
    let h = '';
    gruppen.forEach((g, gi) => {
      h += `<div role="group" aria-labelledby="g-${gi}"><div class="gruppe label" id="g-${gi}" role="presentation">${GRUPPEN[g.name]}</div>`;
      g.rows.forEach(row => {
        const i = optionen.length; optionen.push(row);
        let titel, meta, rechts = '', ik;
        if (row.typ === 'eintrag') {
          titel = markiert(row.e.titel, row.marks); meta = esc(row.e.meta); ik = IKON[row.gruppe === 'passend' ? row.e.gruppe : row.gruppe];
          rechts = row.e.kbd ? `<kbd>${row.e.kbd}</kbd>` : row.e.rechts ? esc(row.e.rechts) : '';
        } else if (row.typ === 'ki') {
          titel = `„${esc(row.frage)}“`; meta = 'Antwort aus Zimmern, Hausregeln und Anreise-Infos'; ik = 'frage'; rechts = 'Fragen';
        } else { titel = esc(row.frage); meta = esc(row.meta); ik = 'frage'; }
        h += `<div class="opt${row.typ !== 'eintrag' ? ' opt--ki' : ''}" role="option" id="opt-${i}" aria-selected="false" data-i="${i}">${ikon(ik)}<span class="opt__text"><span class="opt__titel">${titel}</span><span class="opt__meta">${meta}</span></span><span class="opt__rechts">${rechts}<span class="enter" aria-hidden="true">↵</span></span></div>`;
      });
      h += '</div>';
    });
    if (!optionen.length) h = `<p class="leer">Nichts gefunden zu „${esc(q.trim())}“. Versuchen Sie „Zimmer“ oder stellen Sie eine Frage.</p>`;
    liste.innerHTML = h;
    zeigeListe();
    setzeAktiv(0, true);
    const n = optionen.filter(o => o.typ === 'eintrag').length;
    const text = leer ? (variante === 'ki' ? `${VORSCHLAEGE.length} Vorschläge` : `${optionen.length} Einträge`) : optionen.length ? `${n} Treffer` : 'Keine Treffer';
    anzahlEl.textContent = text;
    clearTimeout(rendern.t); rendern.t = setTimeout(() => statusEl.textContent = text, 400);   // gesammelt ansagen
    caretSetzen();
  }
  function setzeAktiv(i, ohneScroll) {
    if (!optionen.length) { eingabe.removeAttribute('aria-activedescendant'); return; }
    aktiv = (i + optionen.length) % optionen.length;
    liste.querySelectorAll('.opt').forEach(o => o.setAttribute('aria-selected', +o.dataset.i === aktiv ? 'true' : 'false'));
    eingabe.setAttribute('aria-activedescendant', 'opt-' + aktiv);
    const el = liste.querySelector(`#opt-${aktiv}`);
    if (el && !ohneScroll) el.scrollIntoView({ block: 'nearest' });
    if (ohneScroll && aktiv === 0) koerper.scrollTop = 0;
  }
  function zeigeListe() {
    stopStream(); antwort.hidden = true; liste.hidden = false; delete dlg.dataset.antwortet;
    dlg.dataset.modus = variante === 'ki' ? 'ki' : 'suche';
    eingabe.setAttribute('aria-expanded', 'true');
  }

  // ---------- KI-Antwort (simuliert aus dem Index, im echten Einsatz: Suchindex + Sprachmodell) ----------
  function fragen(frage) {
    const a = ANTWORTEN.find(x => x.such.test(fl(frage))) || UNBEKANNT;
    eingabe.value = frage;
    liste.hidden = true; antwort.hidden = false; dlg.dataset.modus = 'ki'; dlg.dataset.antwortet = '';
    eingabe.setAttribute('aria-expanded', 'false'); eingabe.removeAttribute('aria-activedescendant');
    frageEl.textContent = `„${frage}“`;
    quellenEl.classList.remove('da');
    quellenEl.innerHTML = a.quellen.map(id => `<li><a href="#${byId[id].id}" data-quelle="${id}">${esc(byId[id].titel)}</a></li>`).join('');
    anzahlEl.textContent = `${a.quellen.length} Quelle${a.quellen.length > 1 ? 'n' : ''}`;
    koerper.scrollTop = 0;
    stopStream();
    const woerter = a.text.split(' ');
    textEl.setAttribute('aria-busy', 'true');
    if (reduziert()) { textEl.textContent = a.text; fertig(); return Promise.resolve(); }
    textEl.innerHTML = '';
    let n = 0;
    return new Promise(res => {
      const schritt = () => {
        if (n >= woerter.length) { fertig(); res(); return; }
        const s = document.createElement('span'); s.className = 'wort'; s.textContent = (n ? ' ' : '') + woerter[n++];
        textEl.appendChild(s);
        streamTimer = setTimeout(schritt, WORT_MS);
      };
      stopStream.res = res;
      schritt();
    });
    function fertig() { textEl.setAttribute('aria-busy', 'false'); quellenEl.classList.add('da'); }
  }
  function stopStream() { clearTimeout(streamTimer); streamTimer = null; if (stopStream.res) { const r = stopStream.res; stopStream.res = null; r(); } }

  // ---------- Öffnen und Schließen ----------
  function oeffnen(von, ohneFokus) {
    clearTimeout(zuTimer);
    if (dlg.classList.contains('schliesst')) { dlg.classList.remove('schliesst'); dlg.close(); }
    if (dlg.open) { eingabe.select(); return; }
    ausloeser = von || document.activeElement;
    eingabe.value = '';
    const schmal = matchMedia('(max-width: 640px)').matches;
    eingabe.placeholder = variante === 'ki' ? (schmal ? 'Ihre Frage an das Haus' : 'Was möchten Sie über das Haus wissen?') : variante === 'suche' ? 'Zimmer, Seiten, Anreise …' : (schmal ? 'Suchen oder fragen …' : 'Suchen oder das Haus fragen …');
    rendern();
    if (ohneFokus) dlg.setAttribute('open', '');      // Vorführung: kein Fokusraub in der Galerie
    else { dlg.showModal(); eingabe.focus(); }
    body.classList.add('pal-offen');
    $$('[data-ausloeser]').forEach(b => b.classList.toggle('gedrueckt', b === ausloeser));
    offen = true;
  }
  function schliessen(sofort) {
    if (!dlg.open || dlg.classList.contains('schliesst')) return;
    stopStream();
    body.classList.remove('pal-offen');
    $$('[data-ausloeser]').forEach(b => b.classList.remove('gedrueckt'));
    offen = false;
    const fertig = () => {
      dlg.classList.remove('schliesst'); dlg.close();
      if (!vorf && ausloeser && ausloeser.focus) ausloeser.focus({ preventScroll: true });   // Fokus zurück zum Auslöser
    };
    if (sofort) return fertig();
    dlg.classList.add('schliesst');
    zuTimer = setTimeout(fertig, reduziert() ? ZU_RED : ZU);
  }
  dlg.addEventListener('cancel', e => { e.preventDefault(); schliessen(); });
  dlg.addEventListener('click', e => { if (e.target === dlg) schliessen(); });
  $('[data-schliessen]').addEventListener('click', () => schliessen());
  $('[data-zurueck]').addEventListener('click', () => { eingabe.value = ''; rendern(); eingabe.focus(); });
  $$('[data-ausloeser]').forEach(b => b.addEventListener('click', () => oeffnen(b)));

  // ---------- Ausführen ----------
  function ausfuehren(i) {
    const o = optionen[i]; if (!o) return;
    if (o.typ === 'ki' || o.typ === 'vorschlag') return fragen(o.frage);
    const e = o.e;
    if (e.gruppe === 'aktionen') {
      schliessen();
      if (e.id === 'verf') { const b = $('[data-buchung]'); b.classList.remove('puls'); void b.offsetWidth; b.classList.add('puls'); melden('Buchungsleiste: Anreise und Gäste wählen'); }
      if (e.id === 'anruf') melden('Rezeption: täglich 8 bis 20 Uhr');
      if (e.id === 'adresse') { try { navigator.clipboard && navigator.clipboard.writeText('Hallenhaus Neun, 02826 Görlitz').catch(() => {}); } catch (_) {} melden('Adresse kopiert: <code>Hallenhaus Neun, 02826 Görlitz</code>'); }
      if (e.id === 'english') { const s = $('[data-sprache]'); const en = s.dataset.en !== '1'; s.dataset.en = en ? '1' : '0'; s.innerHTML = en ? 'DE / <b>EN</b>' : '<b>DE</b> / EN'; melden(en ? 'Language: English' : 'Sprache: Deutsch'); }
      return;
    }
    schliessen();
    melden(`Öffnet „${esc(e.titel)}“ <code>${e.pfad}</code>`);
  }
  function melden(h) {
    clearTimeout(meldungTimer);
    meldungEl.innerHTML = h; meldungEl.classList.add('da');
    meldungTimer = setTimeout(() => meldungEl.classList.remove('da'), 2400);
  }

  // ---------- Tastatur (Combobox nach WAI-ARIA APG) ----------
  eingabe.addEventListener('input', () => rendern());
  eingabe.addEventListener('keydown', e => {
    if (e.key === 'ArrowDown') { e.preventDefault(); if (antwort.hidden) setzeAktiv(aktiv + 1); }
    else if (e.key === 'ArrowUp') { e.preventDefault(); if (antwort.hidden) setzeAktiv(aktiv - 1); }
    else if (e.key === 'Home' && antwort.hidden && optionen.length) { e.preventDefault(); setzeAktiv(0); }
    else if (e.key === 'End' && antwort.hidden && optionen.length) { e.preventDefault(); setzeAktiv(optionen.length - 1); }
    else if (e.key === 'Enter') { e.preventDefault(); if (antwort.hidden) ausfuehren(aktiv); else if (eingabe.value.trim()) fragen(eingabe.value.trim()); }
    else if (e.key === 'Escape') { e.preventDefault(); schliessen(); }
  });
  liste.addEventListener('pointermove', e => { const o = e.target.closest('.opt'); if (o && +o.dataset.i !== aktiv) setzeAktiv(+o.dataset.i, true); });
  liste.addEventListener('click', e => { const o = e.target.closest('.opt'); if (o) ausfuehren(+o.dataset.i); });
  quellenEl.addEventListener('click', e => { const a = e.target.closest('[data-quelle]'); if (!a) return; e.preventDefault(); const q = byId[a.dataset.quelle]; schliessen(); melden(q.pfad ? `Öffnet „${esc(q.titel)}“ <code>${q.pfad}</code>` : esc(q.titel)); });

  // Globales Kürzel: Strg/⌘ + K, außerdem „/“ außerhalb von Eingabefeldern
  addEventListener('keydown', e => {
    if (e.isTrusted && vorf) stoppeVorfuehrung();
    const tippt = /INPUT|TEXTAREA|SELECT/.test(document.activeElement.tagName) || document.activeElement.isContentEditable;
    if ((e.metaKey || e.ctrlKey) && !e.altKey && e.key.toLowerCase() === 'k') { e.preventDefault(); dlg.open && !dlg.classList.contains('schliesst') ? schliessen() : oeffnen(document.activeElement); return; }
    if (e.key === '/' && !tippt && !dlg.open) { e.preventDefault(); oeffnen(document.activeElement); return; }
    if (e.shiftKey && !e.metaKey && !e.ctrlKey && !tippt && !dlg.open && variante === 'kombiniert') {
      const ziel = EINTRAEGE.find(x => x.taste === e.key.toUpperCase());
      if (ziel) { optionen = [{ typ: 'eintrag', e: ziel }]; ausfuehren(0); }
    }
  });
  addEventListener('pointerdown', e => { if (e.isTrusted && vorf && !e.target.closest('[data-modus-knopf]')) stoppeVorfuehrung(); }, true);

  // ---------- Varianten ----------
  const WERTE = {
    suche: 'Suchfeld im Kopf · Öffnen <b>180 ms</b> · Schließen <b>120 ms</b> · <b>cubic-bezier(0.2, 0.8, 0.2, 1)</b> · scale <b>0.96 → 1</b> · y <b>8 px</b> · <b>640 px</b>',
    kombiniert: 'Öffnen <b>180 ms</b> · Schließen <b>120 ms</b> · <b>cubic-bezier(0.2, 0.8, 0.2, 1)</b> · scale <b>0.96 → 1</b> · y <b>8 px</b> · <b>640 px</b> · Liste ≤ <b>60vh</b>',
    ki: 'Öffnen <b>180 ms</b> · Schließen <b>120 ms</b> · scale <b>0.96 → 1</b> · Antwort <b>32 ms</b>/Wort · Quellen <b>300 ms</b>',
  };
  function setzeVariante(v) {
    variante = v; body.dataset.variante = v;
    $$('[data-modus-knopf]').forEach(b => b.setAttribute('aria-pressed', b.dataset.modusKnopf === v ? 'true' : 'false'));
    werteEl.innerHTML = WERTE[v];
    if (dlg.open) schliessen(true);
  }
  $$('[data-modus-knopf]').forEach(b => b.addEventListener('click', () => { const lief = vorf; setzeVariante(b.dataset.modusKnopf); if (lief) vorfuehren(); }));
  setzeVariante('kombiniert');

  // ---------- Vorführung ----------
  const messer = document.createElement('canvas').getContext('2d');
  function caretSetzen() {
    if (!vorf) return;
    const cs = getComputedStyle(eingabe);
    messer.font = `${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`;
    caret.style.left = Math.min(eingabe.clientWidth - 2, messer.measureText(eingabe.value).width + 1) + 'px';
    caret.style.top = (eingabe.offsetHeight / 2 - 11) + 'px';
  }
  function tasten(...k) {
    clearTimeout(tastenTimer);
    tastatur.classList.remove('weg');
    tastatur.innerHTML = k.map(x => `<kbd>${x}</kbd>`).join('');
    tastenTimer = setTimeout(() => tastatur.classList.add('weg'), 650);
  }
  let lauf = 0;
  const warte = (ms, id) => new Promise((res, rej) => setTimeout(() => (id === lauf ? res() : rej('stopp')), ms));
  async function tippe(text, ms, id) {
    for (const z of text) { await warte(ms, id); eingabe.value += z; rendern(); }
  }
  async function leeren(id) { tasten(mac ? '⌘' : 'Strg', 'A'); await warte(260, id); eingabe.value = ''; rendern(); }
  async function runde(id) {
    if (variante === 'kombiniert') {
      await warte(700, id); tasten(...KUERZEL); oeffnen($('.ausloeser--knopf'), true);
      await warte(700, id); await tippe('turm', 110, id);
      await warte(800, id); tasten('↓'); setzeAktiv(aktiv + 1);
      await warte(600, id); tasten('↑'); setzeAktiv(aktiv - 1);
      await warte(700, id); await leeren(id);
      await warte(300, id); await tippe('Darf unser Hund mit?', 62, id);
      await warte(900, id); tasten('↵'); await fragen(eingabe.value);
      await warte(2600, id); tasten('esc'); schliessen();
      await warte(1500, id);
    } else if (variante === 'suche') {
      await warte(700, id); tasten(...KUERZEL); oeffnen($('.ausloeser--feld'), true);
      await warte(700, id); await tippe('früh', 120, id);
      await warte(900, id); tasten('↵'); ausfuehren(aktiv);
      await warte(2000, id); tasten(...KUERZEL); oeffnen($('.ausloeser--feld'), true);
      await warte(600, id); await tippe('neisse', 110, id);
      await warte(1000, id); tasten('↓'); setzeAktiv(aktiv + 1);
      await warte(700, id); tasten('↑'); setzeAktiv(aktiv - 1);
      await warte(700, id); tasten('↵'); ausfuehren(aktiv);
      await warte(2200, id);
    } else {
      await warte(700, id); tasten(...KUERZEL); oeffnen($('.ausloeser--ki'), true);
      await warte(1000, id); tasten('↓'); setzeAktiv(aktiv + 1);
      await warte(700, id); tasten('↵'); await fragen(optionen[aktiv].frage);
      await warte(2400, id); await leeren(id); await tippe('Kann man nach Polen laufen?', 58, id);
      await warte(700, id); tasten('↵'); await fragen(eingabe.value);
      await warte(2400, id); tasten('esc'); schliessen();
      await warte(1500, id);
    }
  }
  async function vorfuehren() {
    const id = ++lauf;
    vorf = true; html.classList.add('vorfuehrung');
    schliessen(true); meldungEl.classList.remove('da');
    try { while (id === lauf) await runde(id); } catch (_) { /* abgebrochen */ }
  }
  function pause() { lauf++; stopStream(); }
  function stoppeVorfuehrung() { lauf++; vorf = false; html.classList.remove('vorfuehrung'); stopStream(); if (dlg.open && !dlg.matches(':modal')) schliessen(true); tastatur.innerHTML = ''; }
  function neustart() { stoppeVorfuehrung(); schliessen(true); clearTimeout(meldungTimer); meldungEl.classList.remove('da'); eingabe.value = ''; }
  function weiter() { if (html.classList.contains('vorfuehrung')) vorfuehren(); }

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