Zum Inhalt
← Mikroanimationen

Anker-Landung markieren

Sprung aus Inhaltsverzeichnis, FAQ-Link oder „Termin anfragen“ landet unter dem festen Kopf, eine Petrol-Linie läuft am Ziel herunter, die Fläche glimmt kurz apricot, der Fokus sitzt danach auf der Überschrift.

Lange Leistungs-, Ratgeber- oder Rechtsseiten mit Inhaltsverzeichnis oben oder als Seitenleiste (Praxis, Kanzlei, Handwerk).

Wer auf einen Sprunglink tippt (Inhaltsverzeichnis, geteilter FAQ-Link, „Zur Anfrage“), landet genau unter dem festen Kopf, und das Ziel zeigt sich kurz: eine Markenlinie läuft am Rand herunter, die Fläche ist einen Moment getönt und klingt dann ab. Der Tastaturfokus liegt danach auf dem Ziel, ohne zweiten Scroll-Sprung. Ein geteilter Link auf eine Frage öffnet die Antwort gleich mit.

Nach einem Sprung fehlt die Orientierung: Die Seite steht plötzlich woanders, und gerade auf dem Telefon sieht man nicht, welcher Absatz gemeint ist. Die kurze Markierung beantwortet „hier bist du“, der Fokus sorgt dafür, dass die nächste Tab-Taste dort weitergeht und Screenreader das Ziel vorlesen. Ohne scroll-margin verschwindet die Überschrift unter dem festen Kopf.

Wann einsetzen

Lange Seiten mit Inhaltsverzeichnis (Leistungsseite, Ratgeber, Rechtstexte), FAQ mit Direktlinks, „Zur Anfrage“- oder „Termin“-Knöpfe, die auf ein Formular weiter unten springen. Praxis, Kanzlei, Handwerk, Apotheke.

Wann nicht

Nicht für Navigation zwischen Seiten und nicht für Scrollspy-Leisten, die den aktiven Abschnitt laufend zeigen (dafür scrollspy-kapitelnavigation). Keine Dauer-Markierung: nach rund 2 Sekunden muss sie weg sein, sonst sieht es nach Fehler aus.

Varianten

  • Inhaltsverzeichnis: Abschnitt bekommt Randlinie und kurze Tönung, Fokus auf die Überschrift
  • FAQ-Direktlink: Frage per Hash öffnen (details open), Frage markieren, Link der Frage kopierbar
  • Zur Anfrage: Formular wird markiert, Fokus auf die Überschrift des Formulars statt aufs erste Feld

Umsetzung

  1. scroll-margin-top auf alle möglichen Ziele: :target, [id] und [tabindex="-1"] mit calc(Kopfhöhe + Luft). Die Kopfhöhe misst ein ResizeObserver und schreibt sie als --kopf an :root.
  2. Ziele bekommen tabindex="-1", damit sie fokussierbar sind, ohne in der Tab-Reihenfolge zu stehen.
  3. Ein Klick-Handler für a[href^="#"] lässt den Browser springen (scroll-behavior: smooth nur ohne reduzierte Bewegung) und ruft danach ziel.focus({ preventScroll: true }). Damit gibt es keinen zweiten Sprung. Beim Laden mit Hash und bei hashchange passiert dasselbe.
  4. Die Markierung hängt an :target und an einer Klasse .gelandet, die per JS neu gesetzt wird (Klasse entfernen, ein Frame warten, wieder setzen). So spielt sie auch beim zweiten Klick auf denselben Link.
  5. Animation: Randlinie per transform: scaleY(0 → 1), Tönung per background-color, beide klingen nach 1400 ms ab. :focus-visible am Ziel bleibt als Rahmen sichtbar, solange der Fokus dort ist.
  6. FAQ: Ist das Ziel ein <details> oder liegt darin, wird es vor dem Fokus geöffnet.
  7. Formular: Fokus auf die Formular-Überschrift (h2 tabindex=-1), nicht auf das erste Feld, damit Telefon-Tastaturen nicht aufspringen.

Robust umsetzen

Telefon. Gleiches Verhalten. Der feste Kopf ist auf dem Telefon oft niedriger, deshalb misst ein ResizeObserver die echte Höhe. Inhaltsverzeichnis als aufklappbares details oben; nach dem Sprung klappt es zu.

Ohne Bewegung. Kein Gleiten beim Sprung (scroll-behavior: auto). Die Randlinie steht sofort, die Tönung blendet ohne Bewegung aus.

Barrierefrei. Ziele mit tabindex="-1" und sichtbarem Fokusrahmen; der Fokus landet auf der Überschrift, damit Screenreader sie vorlesen. Die Markierung ist nur Zusatz: kein Text wird angesagt. scroll-margin sorgt dafür, dass der Fokus nicht unter dem festen Kopf liegt (WCAG 2.4.11). :root:has(:focus-visible) setzt scroll-behavior auf auto, damit Tab-Fokus nicht aus dem Bild gleitet.

Code

<!DOCTYPE html>
<!--
  Anker-Landung markieren
  Fundus · Mikroanimationen · https://cecodesign.de/lab/fundus/mikroanimationen/anker-landung-markieren/
  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>Anker-Landung markieren · 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: 'Hedvig Letters Serif'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/hedvig-letters-serif:vf@latest/latin-opsz-normal.woff2') format('woff2-variations'); font-weight: 400; font-display: swap; }
@font-face { font-family: 'Google Sans Flex'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/google-sans-flex:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 1000; font-display: swap; }
@font-face { font-family: 'Geist Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist-mono:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }

/* Rezept praxis-gesundheit-ruhig: gedecktes Petrol, warme Off-Whites, Apricot nur für Hervorhebungen */
:root {
  --grund: #F5F7F7; --flaeche: #E9E7E3; --karte: #FFFFFF;
  --marke: #005450; --marke-dunkel: #003E3B;
  --text: #363B3B; --leise: #566262; --linie: #D9DCDB;
  --apricot: #FFC776; --landung: rgba(255, 199, 118, .42);
  --kurve: cubic-bezier(0.2, 0, 0.38, 0.9);        /* --ease, token-tech-praezise */
  --kurve-ein: cubic-bezier(0, 0, 0.38, 0.9);
  --kurve-aus: cubic-bezier(0.2, 0, 1, 0.9);
  --kopf: 76px;                                     /* misst ein ResizeObserver */
  --luft: 24px;
  --rand: clamp(18px, 5vw, 72px);
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--grund); color: var(--text); font: 400 1.0625rem/1.65 'Google Sans Flex', system-ui, sans-serif; font-variation-settings: 'ROND' 40; scroll-behavior: smooth; }
html.reduziert, :root:has(:focus-visible) { scroll-behavior: auto; }
body { min-height: 100svh; }
a { color: inherit; }
:focus-visible { outline: 2px solid var(--marke); outline-offset: 3px; }
.mono { font: 500 11px/1 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; }

/* ---------- Der Kern: jedes mögliche Ziel landet unter dem festen Kopf ---------- */
:target, .ziel, [id][tabindex="-1"] { scroll-margin-top: calc(var(--kopf) + var(--luft)); }

/* Landung: Linie läuft herunter, Fläche kurz getönt, dann ruhig */
.ziel { position: relative; isolation: isolate; }
.ziel::before { /* Randlinie */
  content: ''; position: absolute; left: calc(-1 * clamp(14px, 2vw, 28px)); top: 0; bottom: 0; width: 3px; border-radius: 3px;
  background: var(--marke); transform: scaleY(0); transform-origin: top;
}
.ziel::after { /* Tönung */
  content: ''; position: absolute; inset: -14px calc(-1 * clamp(10px, 1.6vw, 22px)); border-radius: 14px; z-index: -1;
  background: var(--landung); opacity: 0;
}
:target.ziel::before, .ziel.gelandet::before { transform: scaleY(1); }   /* :target hält die Linie, solange der Hash steht */
.ziel.gelandet::before { animation: linie 400ms var(--kurve-ein) both; }
.ziel.gelandet::after { animation: toenung 1400ms linear both; }
@keyframes linie { from { transform: scaleY(0); } to { transform: scaleY(1); } }
@keyframes toenung { 0% { opacity: 0; } 14% { opacity: 1; } 64% { opacity: 1; } 100% { opacity: 0; } }
/* Fokus am Ziel: ruhiger Rahmen an der Überschrift statt Kasten um den ganzen Abschnitt */
.ziel-kopf:focus { outline: none; }
.ziel-kopf:focus-visible { outline: 2px solid var(--marke); outline-offset: 6px; border-radius: 4px; }
.reduziert .ziel.gelandet::before { animation: none; }
.reduziert .ziel.gelandet::after { animation: toenung-ruhig 1400ms linear both; }
@keyframes toenung-ruhig { 0%, 64% { opacity: 1; } 100% { opacity: 0; } }

/* ---------- Seite ---------- */
.kopf { position: fixed; inset: 0 0 auto; z-index: 10; display: flex; align-items: center; gap: 24px; padding: 14px var(--rand); background: rgba(245, 247, 247, .9); -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px); border-bottom: 1px solid var(--linie); }
.marke { display: flex; align-items: center; gap: 12px; text-decoration: none; margin-right: auto; }
.marke svg { width: 34px; height: 34px; flex: none; }
.marke b { font: 400 1.3rem/1 'Hedvig Letters Serif', Georgia, serif; letter-spacing: -.01em; }
.marke small { display: block; font-size: .78rem; color: var(--leise); margin-top: 3px; letter-spacing: .01em; }
.kopf nav { display: flex; gap: 22px; font-size: .95rem; }
.kopf nav a { text-decoration: none; color: var(--leise); padding: 6px 0; }
.kopf nav a:hover { color: var(--text); }
.knopf { display: inline-flex; align-items: center; gap: 10px; min-height: 44px; padding: 0 20px; border-radius: 999px; background: var(--marke); color: #fff; text-decoration: none; font-weight: 560; font-size: .98rem; border: 0; cursor: pointer; transition: background-color 150ms var(--kurve); }
.knopf:hover { background: var(--marke-dunkel); }
.knopf svg { width: 16px; height: 16px; }
.knopf .kurz { display: none; }
.knopf.zweit { background: transparent; color: var(--marke); box-shadow: inset 0 0 0 1.5px var(--marke); }
.knopf.zweit:hover { background: rgba(0, 84, 80, .07); }

main { padding: calc(var(--kopf) + clamp(40px, 7vw, 96px)) var(--rand) 120px; max-width: 1240px; margin: 0 auto; }
.einstieg { display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); gap: clamp(24px, 5vw, 80px); align-items: end; padding-bottom: clamp(40px, 6vw, 80px); border-bottom: 1px solid var(--linie); }
.einstieg .mono { color: var(--marke); display: block; margin-bottom: 18px; }
h1 { font: 400 clamp(2.25rem, 1.4rem + 3.2vw, 4.25rem)/1.08 'Hedvig Letters Serif', Georgia, serif; letter-spacing: -.01em; text-wrap: balance; }
h1 em, h2 em { font-style: normal; color: var(--marke); }
.einstieg p { color: var(--leise); max-width: 46ch; }
.einstieg .aktionen { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 22px; }

.koerper { display: grid; grid-template-columns: minmax(0, 15rem) minmax(0, 1fr); gap: clamp(28px, 6vw, 96px); padding-top: clamp(40px, 6vw, 80px); }
.inhalt { position: sticky; top: calc(var(--kopf) + 28px); align-self: start; }
.inhalt summary { list-style: none; cursor: default; }
.inhalt summary::-webkit-details-marker { display: none; }
.inhalt summary .mono { color: var(--leise); }
.inhalt summary .auf { display: none; }
.inhalt ol { list-style: none; padding: 0; margin-top: 16px; border-left: 1px solid var(--linie); counter-reset: n; }
.inhalt li { counter-increment: n; }
.inhalt a { display: grid; grid-template-columns: 2.2em 1fr; padding: 9px 0 9px 16px; margin-left: -1px; border-left: 2px solid transparent; text-decoration: none; color: var(--leise); font-size: .95rem; line-height: 1.35; transition: color 150ms var(--kurve), border-color 150ms var(--kurve); }
.inhalt a::before { content: counter(n, decimal-leading-zero); font: 500 11px/1.9 'Geist Mono', monospace; letter-spacing: .06em; color: var(--leise); }
.inhalt a:hover { color: var(--text); }
.inhalt a[aria-current="true"] { color: var(--text); border-left-color: var(--marke); }
.inhalt a.gedrueckt { background: rgba(0, 84, 80, .08); }

.text > section, .text > .faq, .text > .anfrage { margin-bottom: clamp(56px, 8vw, 104px); }
h2 { font: 400 clamp(1.75rem, 1.3rem + 1.6vw, 2.6rem)/1.15 'Hedvig Letters Serif', Georgia, serif; letter-spacing: -.005em; margin-bottom: 18px; text-wrap: balance; }
.text p { max-width: 64ch; margin-bottom: 1em; }
.text p.leise { color: var(--leise); }
.fakten { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1px; background: var(--linie); border: 1px solid var(--linie); border-radius: 12px; overflow: hidden; margin-top: 24px; max-width: 64ch; }
.fakten div { background: var(--karte); padding: 16px 18px; }
.fakten dt { font-size: .82rem; color: var(--leise); }
.fakten dd { font: 400 1.5rem/1.2 'Hedvig Letters Serif', serif; margin-top: 4px; font-variant-numeric: tabular-nums; }
.bild { margin: 28px 0 8px; max-width: 64ch; border-radius: 14px; overflow: hidden; aspect-ratio: 16 / 8; background: #dfe6e4; }
.bild img { width: 100%; height: 100%; object-fit: cover; display: block; }
.text .weiter { font-size: .95rem; color: var(--marke); font-weight: 560; text-underline-offset: .2em; }

/* FAQ */
.faq-liste { max-width: 64ch; border-top: 1px solid var(--linie); }
.faq-liste details { border-bottom: 1px solid var(--linie); }
.faq-liste summary { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 16px; align-items: center; padding: 18px 0; cursor: pointer; list-style: none; font-weight: 560; font-size: 1.05rem; }
.faq-liste summary::-webkit-details-marker { display: none; }
.faq-liste summary:focus-visible { outline: 2px solid var(--marke); outline-offset: 2px; border-radius: 6px; }
.faq-liste summary .plus { width: 28px; height: 28px; border-radius: 50%; display: grid; place-items: center; box-shadow: inset 0 0 0 1.5px var(--linie); transition: transform 240ms var(--kurve); }
.faq-liste details[open] summary .plus { transform: rotate(45deg); }
.faq-liste .antwort { padding: 0 0 20px; color: var(--text); }
.faq-liste .antwort p { margin-bottom: .7em; }
.faq-liste .direkt { display: inline-flex; align-items: center; gap: 8px; min-height: 36px; padding: 0 12px; margin-top: 4px; border-radius: 999px; border: 1px solid var(--linie); background: var(--karte); color: var(--leise); font: inherit; font-size: .85rem; cursor: pointer; }
.faq-liste .direkt:hover { color: var(--text); border-color: var(--leise); }
.faq-liste .direkt svg { width: 14px; height: 14px; }
.faq-liste details.ziel::before { top: 12px; bottom: 12px; }
.faq-liste details.ziel::after { inset: 0 calc(-1 * clamp(10px, 1.6vw, 22px)); border-radius: 10px; }

/* Anfrage */
.anfrage form { max-width: 64ch; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px 18px; background: var(--karte); border: 1px solid var(--linie); border-radius: 18px; padding: clamp(20px, 3vw, 32px); }
.anfrage .voll { grid-column: 1 / -1; }
.anfrage label { display: grid; gap: 6px; font-size: .9rem; font-weight: 560; }
.anfrage input, .anfrage select, .anfrage textarea { font: inherit; font-weight: 400; color: var(--text); background: var(--grund); border: 1px solid #8C9696; border-radius: 10px; padding: 11px 13px; min-height: 46px; width: 100%; }
.anfrage textarea { min-height: 96px; resize: vertical; }
.anfrage .hinweis { font-size: .85rem; color: var(--leise); font-weight: 400; }
.anfrage .status { grid-column: 1 / -1; font-size: .92rem; min-height: 1.4em; color: var(--marke); }
.anfrage.ziel::after { display: none; }   /* Formular: Ring um die Karte statt Fläche */
.anfrage.gelandet form { animation: ring 1400ms linear both; }
@keyframes ring { 0% { box-shadow: 0 0 0 0 var(--landung); } 14%, 64% { box-shadow: 0 0 0 8px var(--landung); } 100% { box-shadow: 0 0 0 8px rgba(255, 199, 118, 0); } }
.reduziert .anfrage.gelandet form { animation: ring-ruhig 1400ms linear both; }
@keyframes ring-ruhig { 0%, 64% { box-shadow: 0 0 0 8px var(--landung); } 100% { box-shadow: 0 0 0 8px rgba(255, 199, 118, 0); } }
.anfrage label .hinweis { display: inline; }
.anfrage.ziel::before { left: calc(-1 * clamp(14px, 2vw, 28px)); }

.toast { position: fixed; left: 50%; bottom: 24px; z-index: 30; transform: translate(-50%, 16px); opacity: 0; background: var(--text); color: #fff; padding: 11px 18px; border-radius: 999px; font-size: .9rem; pointer-events: none; transition: opacity 160ms var(--kurve-aus), transform 160ms var(--kurve-aus); }
.toast.an { opacity: 1; transform: translate(-50%, 0); transition-duration: 240ms; transition-timing-function: var(--kurve-ein); }

/* Varianten: gleicher Baustein, andere Seite */
html[data-v="inhalt"] .nur-faq, html[data-v="inhalt"] .nur-anfrage { display: none; }
html[data-v="faq"] .nur-inhalt, html[data-v="faq"] .nur-anfrage { display: none; }
html[data-v="anfrage"] .nur-inhalt, html[data-v="anfrage"] .nur-faq { display: none; }
html[data-v="anfrage"] .koerper { grid-template-columns: minmax(0, 1fr); max-width: 760px; }
html[data-v="anfrage"] .inhalt { display: none; }

/* Pult */
.pult { position: fixed; right: 16px; bottom: 16px; z-index: 20; display: flex; flex-wrap: wrap; align-items: center; gap: 10px; background: rgba(28, 34, 34, .94); color: #fff; padding: 8px 10px 8px 14px; border-radius: 10px; box-shadow: 0 10px 28px rgba(0, 0, 0, .2); max-width: calc(100vw - 32px); }
.pult .mono { font-size: 10px; color: rgba(255, 255, 255, .7); }
.pult .gruppe { display: flex; flex-wrap: wrap; background: rgba(255, 255, 255, .08); border-radius: 6px; padding: 2px; }
.pult button { background: none; border: 0; color: rgba(255, 255, 255, .8); font: 500 10.5px/1 'Geist Mono', monospace; letter-spacing: .06em; text-transform: uppercase; padding: 8px 10px; border-radius: 4px; cursor: pointer; }
.pult button[aria-pressed="true"] { background: #fff; color: var(--text); }
.pult button:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.pult .werte { flex-basis: 100%; font: 400 10.5px/1.5 'Geist Mono', monospace; color: rgba(255, 255, 255, .72); letter-spacing: .02em; max-width: 46ch; }

.vorschau body { overflow: hidden; }
.vorschau .toast { display: none; }

@media (max-width: 900px) {
  .kopf nav { display: none; }
  .einstieg { grid-template-columns: minmax(0, 1fr); align-items: start; }
  .koerper { grid-template-columns: minmax(0, 1fr); }
  .inhalt { position: static; background: var(--karte); border: 1px solid var(--linie); border-radius: 14px; padding: 14px 16px; }
  .inhalt summary { cursor: pointer; display: flex; justify-content: space-between; align-items: center; min-height: 28px; }
  .inhalt summary .auf { display: inline; color: var(--marke); font-size: .9rem; }
  .inhalt ol { border-left: 0; margin-top: 10px; }
  .inhalt a { padding-left: 0; border-left: 0; }
  .inhalt a[aria-current="true"] { color: var(--marke); }
}
@media (max-width: 640px) {
  .kopf { padding: 10px var(--rand); gap: 12px; }
  .marke small { display: none; }
  .kopf .knopf { padding: 0 16px; font-size: .95rem; }
  .kopf .knopf .lang { display: none; } .kopf .knopf .kurz { display: inline; }
  .fakten { grid-template-columns: minmax(0, 1fr); }
  .anfrage form { grid-template-columns: minmax(0, 1fr); }
  .pult .werte { display: none; }
}
@media (max-width: 360px) {
  .kopf .knopf { padding: 0 14px; }
  .marke b { font-size: 1.1rem; }
}
@media (hover: none) { .inhalt a { padding-top: 12px; padding-bottom: 12px; } }
@media (forced-colors: active) {
  button, [role=button], [role=tab], [role=switch], input, select, textarea { border: 1px solid ButtonText !important; }
  .ziel::before { background: Highlight; }
  .ziel::after { background: transparent; outline: 2px solid Highlight; }
}
</style>
</head>
<body>
<header class="kopf" data-kopf>
  <a class="marke" href="#oben">
    <svg viewBox="0 0 34 34" aria-hidden="true"><circle cx="17" cy="17" r="16" fill="#005450"/><path d="M9 21c3-7 5-10 8-10s5 3 8 10" fill="none" stroke="#FFC776" stroke-width="2.2" stroke-linecap="round"/><circle cx="17" cy="11" r="2.4" fill="#F5F7F7"/></svg>
    <span><b>Weidenkamp</b><small>Physiotherapie · Ravensburg</small></span>
  </a>
  <nav aria-label="Hauptnavigation"><a href="#leistung">Manuelle Therapie</a><a href="#fragen">Fragen</a><a href="#team">Team</a></nav>
  <a class="knopf" href="#anfrage" data-sprung><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 6.5h10M3 6.5v6.5h10V6.5M3 6.5V3.5h10v3M5.5 2v3M10.5 2v3" fill="none" stroke="currentColor" stroke-width="1.4"/></svg><span class="lang">Termin anfragen</span><span class="kurz">Termin</span></a>
</header>

<main id="oben">
  <div class="einstieg">
    <div>
      <span class="mono nur-inhalt">Leistung · Manuelle Therapie</span>
      <span class="mono nur-faq">Häufige Fragen · Praxis</span>
      <span class="mono nur-anfrage">Termin · Erstbefund</span>
      <h1 class="nur-inhalt">Wenn der Rücken <em>zumacht</em>, fangen wir mit Zuhören an.</h1>
      <h1 class="nur-faq">Was Sie vor dem <em>ersten Termin</em> wissen wollen.</h1>
      <h1 class="nur-anfrage">Erstbefund in <em>dieser Woche</em>, ohne lange Warteschleife.</h1>
    </div>
    <div>
      <p class="nur-inhalt">Manuelle Therapie bei Rücken-, Nacken- und Gelenkbeschwerden. Mit Rezept oder als Selbstzahler, Hausbesuch auf Anfrage.</p>
      <p class="nur-faq">Jede Frage hat einen eigenen Link. Schicken Sie ihn weiter, dann landet die andere Person genau bei der Antwort.</p>
      <p class="nur-anfrage">Schreiben Sie uns kurz, worum es geht. Wir rufen innerhalb eines Werktags zurück und schlagen zwei Zeiten vor.</p>
      <div class="aktionen nur-anfrage">
        <a class="knopf" href="#anfrage" data-sprung>Zur Anfrage <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 3v10M3.5 8.5 8 13l4.5-4.5" fill="none" stroke="currentColor" stroke-width="1.6"/></svg></a>
        <a class="knopf zweit" href="#ablauf">Wie der Termin abläuft</a>
      </div>
    </div>
  </div>

  <div class="koerper">
    <details class="inhalt" open data-inhalt>
      <summary><span class="mono">Auf dieser Seite</span><span class="auf" aria-hidden="true">zeigen</span></summary>
      <ol class="nur-inhalt">
        <li><a href="#leistung">Was manuelle Therapie ist</a></li>
        <li><a href="#ablauf">So läuft der erste Termin</a></li>
        <li><a href="#rezept">Mit Rezept oder privat</a></li>
        <li><a href="#team">Wer Sie behandelt</a></li>
        <li><a href="#anfrage">Termin anfragen</a></li>
      </ol>
      <ol class="nur-faq">
        <li><a href="#frage-rezept">Brauche ich ein Rezept?</a></li>
        <li><a href="#frage-mitbringen">Was bringe ich mit?</a></li>
        <li><a href="#frage-absagen">Wie sage ich ab?</a></li>
        <li><a href="#frage-hausbesuch">Kommen Sie nach Hause?</a></li>
        <li><a href="#frage-parken">Wo parke ich?</a></li>
      </ol>
    </details>

    <div class="text">
      <section class="ziel nur-inhalt" id="leistung" aria-labelledby="h-leistung">
        <h2 id="h-leistung" class="ziel-kopf" tabindex="-1">Was manuelle Therapie <em>ist</em></h2>
        <p>Mit den Händen prüfen wir, welches Gelenk zu wenig oder zu viel Spiel hat, und lösen Blockaden mit sanftem Druck und Zug. Dazu kommen Übungen, die Sie zu Hause in zehn Minuten schaffen.</p>
        <p class="leise">Typisch bei Hexenschuss, steifem Nacken nach langer Bildschirmarbeit, Kieferspannung oder nach einer Operation am Knie.</p>
        <dl class="fakten"><div><dt>Dauer je Termin</dt><dd>25 Min.</dd></div><div><dt>übliche Serie</dt><dd>6 Termine</dd></div><div><dt>erste freie Zeit</dt><dd>Do., 8:40</dd></div></dl>
      </section>

      <section class="ziel" id="ablauf" aria-labelledby="h-ablauf">
        <h2 id="h-ablauf" class="ziel-kopf" tabindex="-1">So läuft der <em>erste Termin</em></h2>
        <p>Wir beginnen mit einem Gespräch: Seit wann, wann schlimmer, was hilft. Danach Befund im Stehen und Liegen, dann die erste Behandlung. Am Ende wissen Sie, was los ist und was Sie selbst tun können.</p>
        <figure class="bild"><img src="bilder/fenster-licht.jpg" alt="Ruhiger Behandlungsraum mit salbeigrüner Wand, auf die die Abendsonne ein Fensterbild wirft" width="1600" height="900" loading="lazy"></figure>
      </section>

      <section class="ziel nur-inhalt" id="rezept" aria-labelledby="h-rezept">
        <h2 id="h-rezept" class="ziel-kopf" tabindex="-1">Mit Rezept oder <em>privat</em></h2>
        <p>Mit ärztlicher Verordnung rechnen wir direkt mit Ihrer Krankenkasse ab; Sie zahlen nur die gesetzliche Zuzahlung. Ohne Rezept behandeln wir als Selbstzahler nach unserer Preisliste.</p>
        <p class="leise">Zuzahlung und Preise stehen in der Praxis aus. Angaben zur Abrechnung vom Kunden prüfen lassen.</p>
      </section>

      <section class="ziel nur-inhalt" id="team" aria-labelledby="h-team">
        <h2 id="h-team" class="ziel-kopf" tabindex="-1">Wer Sie <em>behandelt</em></h2>
        <p>Drei Physiotherapeutinnen mit Zusatzausbildung in manueller Therapie, eine davon mit Schwerpunkt Kiefer. Sie bleiben über die ganze Serie bei derselben Person.</p>
        <a class="weiter" href="#anfrage" data-sprung>Termin bei uns anfragen</a>
      </section>

      <div class="faq nur-faq" id="fragen">
        <div class="faq-liste">
          <details class="ziel" id="frage-rezept" name="faq">
            <summary><span>Brauche ich ein Rezept?</span><span class="plus" aria-hidden="true">+</span></summary>
            <div class="antwort"><p>Für die Abrechnung mit der Krankenkasse ja: eine ärztliche Verordnung über manuelle Therapie. Ohne Rezept behandeln wir gern als Selbstzahler.</p><button class="direkt" type="button" data-kopieren="frage-rezept"><svg viewBox="0 0 14 14" aria-hidden="true"><path d="M5.5 8.5l3-3M6 3.5l1-1a2.5 2.5 0 0 1 3.5 3.5l-1 1M8 10.5l-1 1A2.5 2.5 0 0 1 3.5 8l1-1" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>Link zu dieser Frage kopieren</button></div>
          </details>
          <details class="ziel" id="frage-mitbringen" name="faq">
            <summary><span>Was bringe ich zum ersten Termin mit?</span><span class="plus" aria-hidden="true">+</span></summary>
            <div class="antwort"><p>Rezept, Versichertenkarte, ein großes Handtuch und bequeme Kleidung. Befunde oder Röntgenbilder, falls vorhanden.</p><button class="direkt" type="button" data-kopieren="frage-mitbringen"><svg viewBox="0 0 14 14" aria-hidden="true"><path d="M5.5 8.5l3-3M6 3.5l1-1a2.5 2.5 0 0 1 3.5 3.5l-1 1M8 10.5l-1 1A2.5 2.5 0 0 1 3.5 8l1-1" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>Link zu dieser Frage kopieren</button></div>
          </details>
          <details class="ziel" id="frage-absagen" name="faq">
            <summary><span>Wie sage ich einen Termin ab?</span><span class="plus" aria-hidden="true">+</span></summary>
            <div class="antwort"><p>Bis 24 Stunden vorher telefonisch oder per Mail. Später abgesagte Termine dürfen wir in Rechnung stellen; Regel und Betrag vom Kunden prüfen lassen.</p><button class="direkt" type="button" data-kopieren="frage-absagen"><svg viewBox="0 0 14 14" aria-hidden="true"><path d="M5.5 8.5l3-3M6 3.5l1-1a2.5 2.5 0 0 1 3.5 3.5l-1 1M8 10.5l-1 1A2.5 2.5 0 0 1 3.5 8l1-1" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>Link zu dieser Frage kopieren</button></div>
          </details>
          <details class="ziel" id="frage-hausbesuch" name="faq">
            <summary><span>Kommen Sie auch nach Hause?</span><span class="plus" aria-hidden="true">+</span></summary>
            <div class="antwort"><p>Ja, wenn auf dem Rezept „Hausbesuch“ angekreuzt ist, innerhalb von Ravensburg und Weingarten. Dienstag und Donnerstag vormittags.</p><button class="direkt" type="button" data-kopieren="frage-hausbesuch"><svg viewBox="0 0 14 14" aria-hidden="true"><path d="M5.5 8.5l3-3M6 3.5l1-1a2.5 2.5 0 0 1 3.5 3.5l-1 1M8 10.5l-1 1A2.5 2.5 0 0 1 3.5 8l1-1" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>Link zu dieser Frage kopieren</button></div>
          </details>
          <details class="ziel" id="frage-parken" name="faq">
            <summary><span>Wo kann ich parken?</span><span class="plus" aria-hidden="true">+</span></summary>
            <div class="antwort"><p>Zwei Plätze direkt vor dem Haus, mit Schild „Praxis“. Sonst im Parkhaus am Bahnhof, fünf Minuten zu Fuß, Aufzug bis zur Praxis.</p><button class="direkt" type="button" data-kopieren="frage-parken"><svg viewBox="0 0 14 14" aria-hidden="true"><path d="M5.5 8.5l3-3M6 3.5l1-1a2.5 2.5 0 0 1 3.5 3.5l-1 1M8 10.5l-1 1A2.5 2.5 0 0 1 3.5 8l1-1" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>Link zu dieser Frage kopieren</button></div>
          </details>
        </div>
      </div>

      <section class="anfrage ziel" id="anfrage" aria-labelledby="h-anfrage">
        <h2 id="h-anfrage" class="ziel-kopf" tabindex="-1">Termin <em>anfragen</em></h2>
        <form novalidate data-form>
          <label>Name<input name="name" autocomplete="name" required></label>
          <label>Telefon<input name="tel" type="tel" autocomplete="tel" inputmode="tel" required></label>
          <label class="voll">Worum geht es?<select name="anliegen"><option>Rücken oder Nacken</option><option>Gelenk nach Operation</option><option>Kiefer</option><option>Etwas anderes</option></select></label>
          <label class="voll"><span>Nachricht <span class="hinweis">(freiwillig)</span></span><textarea name="nachricht"></textarea></label>
          <p class="hinweis voll">Wir nutzen Ihre Angaben nur für den Rückruf. Datenschutzhinweis vom Kunden prüfen lassen.</p>
          <div class="voll"><button class="knopf" type="submit">Rückruf anfordern</button></div>
          <p class="status" role="status" data-status></p>
        </form>
      </section>
    </div>
  </div>
</main>

<p class="toast" role="status" aria-live="polite" data-toast></p>

<div class="pult">
  <span class="mono">Variante</span>
  <div class="gruppe" role="group" aria-label="Variante">
    <button type="button" data-v="inhalt" aria-pressed="true">Inhalt</button>
    <button type="button" data-v="faq" aria-pressed="false">FAQ-Link</button>
    <button type="button" data-v="anfrage" aria-pressed="false">Zur Anfrage</button>
  </div>
  <p class="werte" data-werte></p>
</div>

<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 kopf = $('[data-kopf]');

  // 1. Kopfhöhe messen: scroll-margin-top folgt dem echten Kopf (Telefon, Zoom, Textabstände)
  new ResizeObserver(() => html.style.setProperty('--kopf', kopf.offsetHeight + 'px')).observe(kopf);

  // 2. Landen: Details öffnen, Markierung neu abspielen, Fokus ohne zweiten Sprung
  function landen(id, { springen = false } = {}) {
    const ziel = id && document.getElementById(id); if (!ziel) return;
    const details = ziel.matches('details') ? ziel : ziel.closest('details');
    if (details && !details.classList.contains('inhalt')) details.open = true;
    if (springen) ziel.scrollIntoView({ behavior: reduziert() ? 'auto' : 'smooth', block: 'start' });
    const markiert = ziel.closest('.ziel') || ziel;
    $$('.ziel.gelandet').forEach(z => z.classList.remove('gelandet'));
    void markiert.offsetWidth;                           // Animation neu starten
    markiert.classList.add('gelandet');
    const f = markiert.matches('details') ? markiert.querySelector('summary') : (markiert.querySelector('.ziel-kopf') || markiert);
    if (!f.matches('summary') && !f.hasAttribute('tabindex')) f.setAttribute('tabindex', '-1');
    f.focus({ preventScroll: true });                  // kein Scroll-Sprung, der Browser gleitet schon
    markiereInhalt(id);
  }
  function markiereInhalt(id) { $$('.inhalt a').forEach(a => a.setAttribute('aria-current', a.hash === '#' + id ? 'true' : 'false')); }

  // Der Browser springt selbst (scroll-margin, :target). Wir hängen uns nur an.
  addEventListener('hashchange', () => landen(location.hash.slice(1)));
  document.addEventListener('click', e => {
    const a = e.target.closest('a[href^="#"]'); if (!a || a.hash.length < 2) return;
    if (a.hash === '#oben') return;
    // Telefon: Inhaltsverzeichnis VOR dem Sprung zuklappen, sonst rutscht das Ziel danach unter den Kopf
    if (a.closest('[data-inhalt]') && breit.matches) $('[data-inhalt]').open = false;
    if (a.hash === location.hash) { e.preventDefault(); landen(a.hash.slice(1), { springen: true }); } // gleicher Hash: kein hashchange
  });
  addEventListener('load', () => { if (location.hash.length > 1) landen(location.hash.slice(1), { springen: true }); });

  // Mobil: Inhaltsverzeichnis startet zugeklappt
  const breit = matchMedia('(max-width: 900px)');
  const inh = $('[data-inhalt]');
  const stelleInhalt = () => { inh.open = !breit.matches; };
  stelleInhalt(); breit.addEventListener('change', stelleInhalt);
  inh.querySelector('summary').addEventListener('click', e => { if (!breit.matches) e.preventDefault(); });
  inh.addEventListener('toggle', () => { inh.querySelector('.auf').textContent = inh.open ? 'schließen' : 'zeigen'; });

  // FAQ-Direktlink kopieren
  const toast = $('[data-toast]'); let toastZeit;
  function melde(t) { toast.textContent = t; toast.classList.add('an'); clearTimeout(toastZeit); toastZeit = setTimeout(() => toast.classList.remove('an'), 2200); }
  $$('[data-kopieren]').forEach(b => b.addEventListener('click', async () => {
    const url = location.href.split('#')[0] + '#' + b.dataset.kopieren;
    try { await navigator.clipboard.writeText(url); melde('Link kopiert: …#' + b.dataset.kopieren); } catch (_) { melde('Link: …#' + b.dataset.kopieren); }
  }));

  // Formular: Pflichtfelder, ruhige Bestätigung
  $('[data-form]').addEventListener('submit', e => {
    e.preventDefault();
    const f = e.target, leer = [...f.querySelectorAll('[required]')].filter(i => !i.value.trim());
    f.querySelectorAll('[required]').forEach(i => i.setAttribute('aria-invalid', leer.includes(i) ? 'true' : 'false'));
    const st = $('[data-status]');
    if (leer.length) { st.textContent = 'Bitte noch ausfüllen: ' + leer.map(i => i.closest('label').firstChild.textContent.trim()).join(', ') + '.'; leer[0].focus(); return; }
    st.textContent = 'Danke. Wir rufen Sie innerhalb eines Werktags zurück.'; f.reset();
  });

  // ---------- Varianten ----------
  const WERTE = {
    inhalt: 'scroll-margin-top = Kopf + 24 px · Linie 3 px scaleY 400 ms cubic-bezier(0, 0, 0.38, 0.9) · Tönung 1400 ms (hält bis 64 %) · focus({ preventScroll: true })',
    faq: 'Hash öffnet <details> · Fokus auf die Frage · Tönung 1400 ms · Link je Frage kopierbar · details name="faq"',
    anfrage: 'Fokus auf die Formular-Überschrift, nicht aufs erste Feld (keine Telefon-Tastatur) · Linie 400 ms · Tönung 1400 ms'
  };
  const IDS = Object.keys(WERTE);
  let v = IDS[0];
  function variante(neu) {
    v = IDS.includes(neu) ? neu : IDS[0];
    html.dataset.v = v;
    $$('button[data-v]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.v === v)));
    $('[data-werte]').textContent = WERTE[v];
    $$('.ziel.gelandet').forEach(z => z.classList.remove('gelandet'));
    const q = new URLSearchParams(location.search);
    if (q.get('v') !== v) { q.set('v', v); history.replaceState(null, '', '?' + q.toString().replace(/=(&|$)/g, '$1') + location.hash); }
  }
  $$('button[data-v]').forEach(b => b.addEventListener('click', () => { pause(); variante(b.dataset.v); scrollTo({ top: 0, behavior: 'auto' }); }));
  variante(new URLSearchParams(location.search).get('v'));

  // ---------- Vorführung: Link antippen, Landung zeigen, nächster Link ----------
  let zeit = [], laeuft = false;
  const nach = (ms, f) => zeit.push(setTimeout(f, ms));
  const FOLGE = { inhalt: ['ablauf', 'team', 'leistung'], faq: ['frage-absagen', 'frage-parken', 'frage-rezept'], anfrage: ['anfrage', 'ablauf'] };
  function tippe(id) {
    const a = $$(`a[href="#${id}"]`).find(x => x.offsetParent !== null);
    if (a) { a.classList.add('gedrueckt'); setTimeout(() => a.classList.remove('gedrueckt'), 260); }
    if (location.hash === '#' + id) landen(id, { springen: true });
    else { history.replaceState(null, '', location.pathname + location.search + '#' + id); landen(id, { springen: true }); }
  }
  function runde() {
    if (!laeuft) return;
    const folge = FOLGE[v];
    $$('.faq-liste details').forEach(d => d.open = false);
    scrollTo({ top: 0, behavior: 'auto' });
    folge.forEach((id, i) => nach(900 + i * 2600, () => tippe(id)));
    nach(900 + folge.length * 2600 + 400, runde);
  }
  function neustart() {
    zeit.forEach(clearTimeout); zeit = [];
    $$('.ziel.gelandet').forEach(z => z.classList.remove('gelandet'));
    $$('.faq-liste details').forEach(d => d.open = false);
    history.replaceState(null, '', location.pathname + location.search);
    scrollTo({ top: 0, behavior: 'auto' }); markiereInhalt('');
  }
  function vorfuehren() { neustart(); laeuft = true; html.classList.add('vorfuehrung'); if (breit.matches) inh.open = false; runde(); }
  function pause() { laeuft = false; zeit.forEach(clearTimeout); zeit = []; html.classList.remove('vorfuehrung'); }
  function weiter() { if (!laeuft) { laeuft = true; html.classList.add('vorfuehrung'); runde(); } }

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