Text-Hervorhebung mit ::highlight()
Suchtreffer, eigene Markierungen und verlinkte Textstellen leuchten im Text einer Apotheke auf, ohne ein einziges <mark> im HTML: Range-Objekte plus ::highlight(), Rückfall <mark>.
FAQ, Leistungslisten, Speisekarte, Download-Center: Suche, die im Text markiert statt nur zu filtern.
Suchtreffer, eigene Markierungen beim Lesen oder eine verlinkte Textstelle werden farbig hervorgehoben, ohne dass ein einziges <mark> oder <span> ins HTML kommt. JavaScript beschreibt die Stellen als Range-Objekte, CSS gestaltet sie mit ::highlight(name). Mehrere Arten (Treffer, aktueller Treffer, eigene Notiz) liegen als eigene Highlights übereinander. Browser ohne die API bekommen <mark> als Rückfall.
Treffer zu markieren macht eine Suche nachvollziehbar: Man sieht, warum ein Eintrag passt. Weil der DOM nicht umgebaut wird, bleiben Links, Auswahl, Screenreader-Text und Event-Handler heil, und auch große Texte bleiben schnell. Es ist der Text-Marker des Webs, nicht nur Gestaltung.
Wann einsetzen
FAQ- und Hilfeseiten mit Suche, Speisekarten (Allergene, vegan), Download-Center, Leistungslisten, Ratgeber mit „Stelle merken“, geteilte Links auf eine bestimmte Passage. Apotheke, Praxis, Gastronomie, Kurse.
Wann nicht
Nicht für dauerhafte, inhaltliche Betonung im Text (dafür <strong>, <em> oder highlight-marker-underline). Nicht, wenn der Treffer selbst interaktiv werden soll (Klick auf den Treffer): Highlights sind keine Elemente. Kein Ersatz für eine Suche, die Ergebnisse filtert, sie ergänzt sie.
Varianten
- Suche: Treffer live markieren, aktueller Treffer eigener Stil, mit Vor und Zurück
- Lesen: Text auswählen, „Markieren“, Markierungen bleiben (localStorage) und sind als Liste anspringbar
- Verlinkte Stelle: Link öffnet die Seite mit hervorgehobener Passage, die in Stufen abklingt
Umsetzung
- Text bleibt unverändert im HTML.
- Suchen: Mit einem TreeWalker (NodeFilter.SHOW_TEXT) alle Textknoten im Suchbereich durchgehen, Fundstellen per indexOf auf der kleingeschriebenen, entakzentuierten Fassung finden (normalize('NFD') ohne Kombinationszeichen, so findet „schussler“ auch „Schüßler“ nur, wenn man ß gesondert behandelt) und je Treffer new Range() mit setStart/setEnd anlegen.
- CSS.highlights.set('treffer', new Highlight(...ranges)) und für den aktuellen Treffer ein zweites Highlight mit höherer priority.
- Stil:
::highlight(treffer){ background-color; color } und::highlight(treffer-aktiv)mit kräftigerer Farbe plus text-decoration als Zusatz (Safari ignoriert Dicke und Abstand). - Zum aktuellen Treffer springen: range.getBoundingClientRect() und scrollBy; Ansage „Treffer 3 von 7“ in einer aria-live-Zeile.
- Lesen: window.getSelection().getRangeAt(0) im erlaubten Bereich in ein eigenes Highlight übernehmen, als Textversatz (Absatz-Index, Start, Ende) in localStorage sichern und beim Laden zurückbauen.
- Verlinkte Stelle: ?stelle=absatz:start-ende in der URL; beim Laden Range bauen, hinscrollen und das Highlight in vier Stufen über eigene Namen (stelle-1 … stelle-4, abnehmende Deckkraft) abklingen lassen, weil ::highlight keine Transition kann.
- Rückfall ohne CSS.highlights: Treffer mit <mark> umschließen und vor jeder neuen Suche wieder auspacken (normalize()).
Robust umsetzen
Telefon. Suchfeld oben klebend, Vor und Zurück als große Knöpfe daneben. Markieren beim Lesen über die native Textauswahl; der Knopf „Markieren“ erscheint unten fest statt über der Auswahl, damit er nicht mit dem System-Menü kollidiert.
Ohne Bewegung. Sprung zum Treffer ohne Gleiten. Die verlinkte Stelle bleibt markiert statt abzuklingen, bis man sie schließt.
Barrierefrei. Highlights sind für Screenreader unsichtbar. Deshalb Trefferzahl und Position per aria-live ansagen („Treffer 2 von 5“). Farbe nie allein: der aktive Treffer bekommt zusätzlich eine Unterstreichung. Kontrast der Markierung zum Text mindestens 4,5:1. Im Kontrastmodus (forced-colors) ::highlight mit Highlight und HighlightText. Eigene Markierungen haben eine Liste mit Text und Knopf „Entfernen“.
Code
<!DOCTYPE html>
<!--
Text-Hervorhebung mit ::highlight()
Fundus · Typo-Techniken · https://cecodesign.de/lab/fundus/typo-techniken/text-hervorhebung-highlight/
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>Text-Hervorhebung mit ::highlight() · 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: 'Funnel Display'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/funnel-display:vf@latest/latin-wght-normal.woff2') format('woff'); font-weight: 300 800; font-display: swap; }
@font-face { font-family: 'Funnel Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/funnel-sans:vf@latest/latin-wght-normal.woff2') format('woff'); font-weight: 300 800; font-display: swap; }
@font-face { font-family: 'Geist Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist-mono:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
/* Rezept apotheke-vor-ort: warmes Weiß, Salbei als Fläche, Petrol für Handlung */
:root {
--grund: #FAFAF8; --text: #1A2321; --flaeche: #E0ECE8; --dunkel: #0F3B3A; --signal: #0D6A62;
--leise: #55615E; --linie: #D2DAD7;
/* Hervorhebungen: Kontrast Text darauf ≥ 4,5 : 1 */
--treffer: #F2E19A; /* Marker-Gelb, Text #1A2321 darauf 13 : 1 */
--treffer-aktiv: #0D6A62; /* Petrol, weiße Schrift 6,5 : 1 */
--notiz: #CFE8DD; /* eigene Markierung, Salbei kräftiger */
--kurve: cubic-bezier(0.2, 0, 0.38, 0.9);
--kurve-ein: cubic-bezier(0, 0, 0.38, 0.9);
--kurve-aus: cubic-bezier(0.2, 0, 1, 0.9);
--rand: clamp(18px, 5vw, 64px);
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--grund); color: var(--text); font: 400 1.0625rem/1.6 'Funnel Sans', system-ui, sans-serif; }
body { min-height: 100svh; }
:focus-visible { outline: 2px solid var(--signal); outline-offset: 3px; }
.mono { font: 500 11px/1 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; }
button { font: inherit; color: inherit; }
/* ---------- Der Kern: Stil für benannte Highlights, kein <mark> im HTML ---------- */
::highlight(treffer) { background-color: var(--treffer); color: var(--text); }
::highlight(treffer-aktiv) { background-color: var(--treffer-aktiv); color: #fff; text-decoration: underline 2px #fff; }
::highlight(notiz) { background-color: var(--notiz); color: var(--text); text-decoration: underline 1px var(--signal); }
::highlight(stelle-1) { background-color: rgba(242, 225, 154, 1); color: var(--text); }
::highlight(stelle-2) { background-color: rgba(242, 225, 154, .72); color: var(--text); }
::highlight(stelle-3) { background-color: rgba(242, 225, 154, .45); color: var(--text); }
::highlight(stelle-4) { background-color: rgba(242, 225, 154, .2); color: var(--text); }
/* Rückfall ohne API: dieselben Farben auf <mark> */
mark.hl { background: var(--treffer); color: var(--text); border-radius: 2px; }
mark.hl-treffer-aktiv { background: var(--treffer-aktiv); color: #fff; text-decoration: underline 2px #fff; }
mark.hl-notiz { background: var(--notiz); text-decoration: underline 1px var(--signal); }
mark.hl-stelle { background: var(--treffer); }
/* ---------- Seite ---------- */
.kopf { display: flex; align-items: center; gap: 18px; padding: 18px var(--rand); border-bottom: 1px solid var(--linie); }
.marke { display: flex; align-items: center; gap: 12px; margin-right: auto; }
.marke svg { width: 30px; height: 30px; }
.marke b { font: 650 1.2rem/1 'Funnel Display', system-ui, sans-serif; letter-spacing: -.02em; }
.marke small { display: block; color: var(--leise); font-size: .82rem; margin-top: 3px; }
.status { display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: .95rem; background: var(--flaeche); padding: 8px 14px; border-radius: 999px; }
.status i { width: 8px; height: 8px; border-radius: 50%; background: var(--signal); }
.buehne { display: grid; grid-template-columns: minmax(0, 20rem) minmax(0, 1fr); gap: clamp(28px, 5vw, 80px); padding: clamp(28px, 5vw, 64px) var(--rand) 120px; max-width: 1240px; margin: 0 auto; }
.seite-kopf { grid-column: 1 / -1; display: grid; grid-template-columns: minmax(0, 20rem) minmax(0, 1fr); gap: 16px clamp(28px, 5vw, 80px); align-items: end; }
.seite-kopf > .mono, .seite-kopf > h1 { grid-column: 1 / -1; }
.seite-kopf > p { grid-column: 2; }
.seite-kopf .mono { color: var(--signal); }
h1 { font: 620 clamp(2rem, 1.3rem + 3vw, 3.4rem)/1.05 'Funnel Display', system-ui, sans-serif; letter-spacing: -.02em; text-wrap: balance; max-width: 22ch; }
.seite-kopf p { color: var(--leise); max-width: 56ch; }
/* Werkzeugtafel links */
.tafel { position: sticky; top: 20px; align-self: start; background: var(--flaeche); border-radius: 18px; padding: 20px; display: grid; gap: 14px; }
.tafel h2 { font: 620 1.15rem/1.2 'Funnel Display', sans-serif; letter-spacing: -.01em; }
.feld { position: relative; }
.feld input { width: 100%; font: inherit; font-size: 1.05rem; padding: 12px 44px 12px 42px; min-height: 48px; border-radius: 12px; border: 1px solid #7E8B88; background: #fff; color: var(--text); }
.feld input::-webkit-search-cancel-button { display: none; }
.feld svg { position: absolute; left: 14px; top: 50%; width: 18px; height: 18px; transform: translateY(-50%); color: var(--leise); pointer-events: none; }
.feld .leeren { position: absolute; right: 6px; top: 50%; transform: translateY(-50%); width: 36px; height: 36px; border-radius: 50%; border: 0; background: transparent; cursor: pointer; color: var(--leise); display: grid; place-items: center; }
.feld .leeren:hover { background: var(--flaeche); color: var(--text); }
.feld .leeren[hidden] { display: none; }
.zeile { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.zaehler { font-weight: 600; font-variant-numeric: tabular-nums; margin-right: auto; min-height: 1.6em; }
.zaehler span { color: var(--leise); font-weight: 400; }
.rund { width: 44px; height: 44px; border-radius: 12px; border: 1px solid #7E8B88; background: #fff; display: grid; place-items: center; cursor: pointer; transition: background-color 110ms var(--kurve); }
.rund:hover:not(:disabled) { background: var(--grund); }
.rund:disabled { opacity: .45; cursor: default; }
.rund svg { width: 16px; height: 16px; }
.chips { display: flex; flex-wrap: wrap; gap: 6px; }
.chips button { border: 1px solid #A9B6B2; background: transparent; border-radius: 999px; padding: 7px 12px; min-height: 36px; cursor: pointer; font-size: .9rem; }
.chips button:hover { background: #fff; }
.leer { background: #fff; border-radius: 12px; padding: 14px; font-size: .95rem; }
.leer b { display: block; margin-bottom: 4px; }
.leer[hidden] { display: none; }
.tafel .klein { font-size: .85rem; color: var(--leise); }
.knopf { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 46px; padding: 0 18px; border-radius: 12px; border: 0; background: var(--signal); color: #fff; font-weight: 600; cursor: pointer; transition: background-color 110ms var(--kurve), opacity 110ms var(--kurve); }
.knopf:hover:not(:disabled) { background: var(--dunkel); }
.knopf:disabled { background: #8FA19D; cursor: default; }
.knopf svg { width: 16px; height: 16px; }
.knopf.hell { background: #fff; color: var(--text); box-shadow: inset 0 0 0 1px #7E8B88; }
.liste { list-style: none; padding: 0; display: grid; gap: 8px; }
.liste li { background: #fff; border-radius: 12px; padding: 10px 12px; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 4px 8px; align-items: start; }
.liste q { font-size: .92rem; line-height: 1.45; quotes: '„' '“'; }
.liste .aktion { display: flex; gap: 2px; }
.liste .aktion button { width: 36px; height: 36px; border: 0; background: transparent; border-radius: 8px; cursor: pointer; display: grid; place-items: center; color: var(--leise); }
.liste .aktion button:hover { background: var(--flaeche); color: var(--text); }
.liste .aktion svg { width: 15px; height: 15px; }
.liste:empty::before { content: 'Noch nichts markiert.'; color: var(--leise); font-size: .92rem; }
/* Text rechts */
.text { max-width: 64ch; }
.text article { padding: 22px 0 24px; border-top: 1px solid var(--linie); position: relative; }
.text article:first-child { border-top: 0; padding-top: 0; }
.text h3 { font: 620 1.3rem/1.25 'Funnel Display', sans-serif; letter-spacing: -.01em; margin-bottom: 8px; display: flex; align-items: baseline; gap: 10px; }
.text h3 .anzahl { font: 600 .78rem/1 'Funnel Sans', sans-serif; font-variant-numeric: tabular-nums; background: var(--treffer); padding: 4px 8px; border-radius: 999px; }
.text h3 .anzahl:empty { display: none; }
.text p { margin-bottom: .8em; }
.text .meta { font-size: .92rem; color: var(--leise); }
.text h2.artikel-titel { font: 620 clamp(1.625rem, 1.2rem + 1.6vw, 2.375rem)/1.12 'Funnel Display', sans-serif; letter-spacing: -.015em; margin-bottom: 16px; }
.text .vorspann { font-size: 1.18rem; line-height: 1.55; margin-bottom: 1.2em; }
.text ul { padding-left: 1.2em; margin-bottom: .8em; }
.text li { margin-bottom: .3em; }
.text ::selection { background: rgba(13, 106, 98, .25); }
/* Hinweis-Band für die verlinkte Stelle */
.band { position: sticky; top: 12px; z-index: 4; box-shadow: 0 8px 24px rgba(15, 59, 58, .18); display: flex; align-items: center; gap: 12px; flex-wrap: wrap; background: var(--dunkel); color: #fff; border-radius: 14px; padding: 12px 12px 12px 16px; margin-bottom: 24px; transform-origin: top; }
.band p { margin: 0; margin-right: auto; font-size: .95rem; }
.band button { min-height: 40px; padding: 0 14px; border-radius: 10px; border: 1px solid rgba(255, 255, 255, .5); background: transparent; color: #fff; cursor: pointer; }
.band button:hover { background: rgba(255, 255, 255, .1); }
.band[hidden] { display: none; }
.band.neu { animation: band-ein 400ms var(--kurve-ein) both; }
@keyframes band-ein { from { opacity: 0; transform: translateY(-8px); } }
.reduziert .band.neu { animation: none; }
.toast { position: fixed; left: 50%; bottom: 24px; transform: translate(-50%, 12px); opacity: 0; background: var(--text); color: #fff; padding: 11px 18px; border-radius: 999px; font-size: .92rem; pointer-events: none; z-index: 30; 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 */
html[data-v="suche"] .nur-lesen, html[data-v="suche"] .nur-zitat, html[data-v="suche"] .nur-artikel { display: none; }
html[data-v="lesen"] .nur-suche, html[data-v="lesen"] .nur-zitat { display: none; }
html[data-v="zitat"] .nur-suche, html[data-v="zitat"] .nur-lesen { 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(15, 30, 29, .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); max-width: 46ch; }
.pult .werte b { color: #fff; font-weight: 500; }
.vorschau body { overflow: hidden; }
.vorschau .toast { display: none; }
.vorschau .kopf .status { display: none; }
@media (max-width: 860px) {
.buehne, .seite-kopf { grid-template-columns: minmax(0, 1fr); }
.seite-kopf > p { grid-column: 1; }
.tafel { position: sticky; top: 0; z-index: 5; border-radius: 0 0 16px 16px; margin: 0 calc(-1 * var(--rand)); padding: 12px var(--rand); gap: 10px; box-shadow: 0 6px 18px rgba(15, 59, 58, .08); }
.tafel h2, .tafel .chips, .tafel .klein { display: none; }
html[data-v="lesen"] .tafel, html[data-v="zitat"] .tafel { position: static; margin: 0; border-radius: 16px; padding: 16px; }
html[data-v="lesen"] .tafel h2, html[data-v="zitat"] .tafel h2 { display: block; }
.kopf .status { display: none; }
.pult .werte { display: none; }
}
@media (max-width: 860px) and (hover: none) {
/* Telefon: „Markieren“ unten fest statt neben der Auswahl, das System-Menü sitzt dort oben */
.markier-fest { position: fixed; left: 12px; right: 12px; bottom: 60px; z-index: 25; }
.markier-fest:has(.knopf:disabled) { display: none; }
.markier-fest .knopf { width: 100%; box-shadow: 0 10px 24px rgba(0, 0, 0, .18); }
}
@media (max-width: 360px) { .rund { width: 40px; height: 40px; } }
@media (forced-colors: active) {
button, [role=button], [role=tab], [role=switch], input, select, textarea { border: 1px solid ButtonText !important; }
::highlight(treffer), ::highlight(notiz), ::highlight(stelle-1), ::highlight(stelle-2), ::highlight(stelle-3), ::highlight(stelle-4) { background-color: Highlight; color: HighlightText; }
::highlight(treffer-aktiv) { background-color: Mark; color: MarkText; text-decoration: underline 2px; }
mark.hl { forced-color-adjust: none; background: Highlight; color: HighlightText; }
}
</style>
</head>
<body>
<header class="kopf">
<div class="marke">
<svg viewBox="0 0 30 30" aria-hidden="true"><path d="M15 27C8 21 6 14 9 6c5 2 8 7 6 21z" fill="#0D6A62"/><path d="M15 27c7-6 9-13 6-21-5 2-8 7-6 21z" fill="#8FC2AF"/></svg>
<span><b>Salbei-Apotheke</b><small>am Altmarkt · Löbau</small></span>
</div>
<span class="status"><i aria-hidden="true"></i>Mo–Fr 8–18:30, Sa 8–13</span>
</header>
<main class="buehne">
<div class="seite-kopf">
<span class="mono nur-suche">Leistungen · durchsuchen</span>
<span class="mono nur-artikel">Rat und Antwort · Reisezeit</span>
<h1 class="nur-suche">Was wir außer Medikamenten für Sie tun.</h1>
<h1 class="nur-artikel">Die Reiseapotheke, ohne zu viel einzupacken.</h1>
<p class="nur-suche">Suchen Sie nach einem Wort. Die Treffer leuchten im Text auf, der Text selbst bleibt, wie er ist.</p>
<p class="nur-lesen">Wählen Sie Text aus und markieren Sie ihn. Ihre Stellen bleiben auf diesem Gerät gespeichert.</p>
<p class="nur-zitat">Jemand hat Ihnen einen Link auf eine Textstelle geschickt. Sie ist hervorgehoben und klingt dann ab.</p>
</div>
<!-- Tafel: Suche -->
<aside class="tafel nur-suche" aria-labelledby="t-suche">
<h2 id="t-suche">Im Text suchen</h2>
<div class="feld" role="search">
<svg viewBox="0 0 18 18" aria-hidden="true"><circle cx="8" cy="8" r="5.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12.2 12.2 16 16" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
<label class="unsichtbar" for="suche" style="position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)">Leistungen durchsuchen</label>
<input id="suche" type="search" placeholder="z. B. Blutdruck" autocomplete="off" spellcheck="false" aria-describedby="treffer-zahl" data-suche>
<button class="leeren" type="button" aria-label="Suche leeren" hidden data-leeren><svg viewBox="0 0 14 14" width="14" height="14" aria-hidden="true"><path d="M3 3l8 8M11 3l-8 8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></button>
</div>
<div class="zeile">
<p class="zaehler" id="treffer-zahl" data-zaehler><span>Noch keine Suche</span></p>
<button class="rund" type="button" aria-label="Vorheriger Treffer" disabled data-zurueck><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4 10l4-4 4 4" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
<button class="rund" type="button" aria-label="Nächster Treffer" disabled data-vor><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4 6l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
</div>
<div class="leer" hidden data-leer><b>Kein Treffer für „<span data-leer-wort></span>“.</b>Fragen Sie uns an der Theke oder am Telefon, vieles steht nicht auf der Seite.</div>
<div class="chips" role="group" aria-label="Vorschläge"><button type="button" data-wort="Blutdruck">Blutdruck</button><button type="button" data-wort="Botendienst">Botendienst</button><button type="button" data-wort="impfen">impfen</button><button type="button" data-wort="Kompression">Kompression</button></div>
<p class="klein">Enter springt weiter, Umschalt + Enter zurück, Esc leert.</p>
<p class="aria-ansage" aria-live="polite" style="position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)" data-ansage></p>
</aside>
<!-- Tafel: Lesen -->
<aside class="tafel nur-lesen" aria-labelledby="t-lesen">
<h2 id="t-lesen">Ihre Markierungen</h2>
<div class="markier-fest"><button class="knopf" type="button" disabled data-markieren><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 13h10M5 10.5l6.5-6.5 1.5 1.5L6.5 12H5v-1.5z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>Auswahl markieren</button></div>
<p class="klein" data-lesen-hinweis>Text im Artikel auswählen, dann markieren.</p>
<ul class="liste" data-notizen aria-label="Gespeicherte Markierungen"></ul>
<p class="aria-ansage" aria-live="polite" style="position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)" data-ansage-lesen></p>
</aside>
<!-- Tafel: verlinkte Stelle -->
<aside class="tafel nur-zitat" aria-labelledby="t-zitat">
<h2 id="t-zitat">Stelle teilen</h2>
<p class="klein">Text auswählen und einen Link kopieren, der genau diese Stelle öffnet.</p>
<button class="knopf" type="button" disabled data-teilen><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M6.5 9.5l3-3M7 4.5l1-1a2.6 2.6 0 0 1 3.6 3.6l-1 1M9 11.5l-1 1A2.6 2.6 0 0 1 4.4 8.9l1-1" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>Link zur Auswahl kopieren</button>
<button class="knopf hell" type="button" data-wieder>Ankunft noch einmal zeigen</button>
</aside>
<div class="text">
<!-- Leistungen (Suche) -->
<div class="nur-suche" data-bereich="suche">
<article><h3>Blutdruck messen <span class="anzahl" aria-hidden="true"></span></h3><p>Ohne Termin, an jedem Werktag. Wir messen in Ruhe im Beratungsraum und tragen den Wert in Ihren Blutdruckpass ein. Bei auffälligen Werten raten wir, das mit der Hausarztpraxis zu besprechen.</p><p class="meta">kostenlos · etwa 10 Minuten</p></article>
<article><h3>Medikationsanalyse <span class="anzahl" aria-hidden="true"></span></h3><p>Wer fünf oder mehr Medikamente dauerhaft nimmt, kann sie mit uns durchgehen: Wechselwirkungen, Einnahmezeiten, doppelte Wirkstoffe. Bringen Sie alle Packungen mit, auch die aus der Selbstmedikation.</p><p class="meta">Termin nötig · Voraussetzungen der Kassenleistung vom Kunden prüfen lassen</p></article>
<article><h3>Kompressionsstrümpfe anmessen <span class="anzahl" aria-hidden="true"></span></h3><p>Morgens messen wir am besten, solange die Beine noch schlank sind. Die Kompression wird nach Maß bestellt und ist meist nach einer Woche da. Rezept bitte mitbringen.</p><p class="meta">Termin empfohlen · vormittags</p></article>
<article><h3>Impfen in der Apotheke <span class="anzahl" aria-hidden="true"></span></h3><p>Grippeschutz und weitere Impfungen, die Apotheken anbieten dürfen, ab 18 Jahren. Wir impfen dienstags und donnerstags nach Termin; den Impfpass tragen wir gleich ein. Welche Impfungen wir anbieten, vom Kunden prüfen lassen.</p><p class="meta">Termin nötig · Impfpass mitbringen</p></article>
<article><h3>Botendienst <span class="anzahl" aria-hidden="true"></span></h3><p>Was bis 12 Uhr bestellt oder vorbestellt ist, bringt unser Botendienst am selben Nachmittag in Löbau und den Ortsteilen. Rezepte holt der Bote auf Wunsch mit ab, auch den Blutdruckpass zur Kontrolle.</p><p class="meta">Mo–Fr · Bedingungen vom Kunden prüfen lassen</p></article>
<article><h3>Mutter und Kind <span class="anzahl" aria-hidden="true"></span></h3><p>Milchpumpen zum Ausleihen, Beratung zu Fieber und Zahnen, Babywaage zum Nachwiegen. Fragen zur Stillzeit beantworten wir gern im Beratungsraum, ohne Termin.</p><p class="meta">Leihgebühr laut Aushang</p></article>
</div>
<!-- Artikel (Lesen, verlinkte Stelle) -->
<div class="nur-artikel" data-bereich="artikel">
<div class="band nur-zitat" hidden data-band role="status"><p>Sie wurden auf eine Textstelle verlinkt.</p><button type="button" data-band-zu>Markierung entfernen</button></div>
<h2 class="artikel-titel">Was in die Reisetasche gehört</h2>
<p class="vorspann" data-p>Die beste Reiseapotheke ist klein. Sie deckt die drei Dinge ab, die unterwegs wirklich passieren: ein verdorbener Magen, eine Blase am Fuß und Sonne, die man unterschätzt hat.</p>
<p data-p>Packen Sie zuerst Ihre Dauermedikamente ein, und zwar für die ganze Reise plus drei Tage. Gehören sie ins Handgepäck, bleiben sie bei Ihnen, falls der Koffer einen anderen Flug nimmt. Für Flüssigkeiten über 100 ml hilft eine kurze ärztliche Bescheinigung.</p>
<p data-p>Gegen Durchfall zählt vor allem eines: trinken. Elektrolyte als Pulver wiegen fast nichts und gleichen aus, was der Körper verliert. Mittel, die den Darm bremsen, sind für kurze Strecken gedacht, nicht für mehrere Tage.</p>
<p data-p>Für die Haut reichen ein Sonnenschutz mit hohem Lichtschutzfaktor, ein kühlendes Gel und Blasenpflaster. Insektenschutz nur, wenn das Reiseziel ihn verlangt; wir sagen Ihnen gern, welcher Wirkstoff dort empfohlen wird.</p>
<p data-p>Ein Fieberthermometer und ein Schmerzmittel, das Sie kennen, gehören in jede Tasche. Neue Mittel probieren Sie besser zu Hause aus als im Hotelzimmer.</p>
<p class="meta" data-p>Kein Ersatz für ärztlichen Rat. Bei Reisen in ferne Länder rechtzeitig vorher beraten lassen.</p>
</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="suche" aria-pressed="true">Suche</button>
<button type="button" data-v="lesen" aria-pressed="false">Lesen</button>
<button type="button" data-v="zitat" aria-pressed="false">Verlinkt</button>
</div>
<p class="werte" data-werte></p>
</div>
<script>
(() => {
const html = document.documentElement;
const q0 = new URLSearchParams(location.search);
const reduziert = () => html.classList.contains('reduziert');
const $ = (s, r = document) => r.querySelector(s);
const $$ = (s, r = document) => [...r.querySelectorAll(s)];
// Rückfall erzwingen mit ?ohne-api (zum Vergleichen)
const API = !q0.has('ohne-api') && !!(window.CSS && CSS.highlights && typeof Highlight === 'function');
// ---------- Werkzeug: Text-Versatz ↔ Range, ohne den DOM anzufassen ----------
function textKnoten(wurzel) {
const w = document.createTreeWalker(wurzel, NodeFilter.SHOW_TEXT), out = [];
for (let n; (n = w.nextNode());) out.push(n);
return out;
}
function rangeAus(wurzel, start, ende) {
const r = new Range(); let pos = 0, s = false;
for (const n of textKnoten(wurzel)) {
const l = n.length;
if (!s && start <= pos + l) { r.setStart(n, start - pos); s = true; }
if (s && ende <= pos + l) { r.setEnd(n, ende - pos); return r; }
pos += l;
}
return null;
}
function versatzVon(wurzel, knoten, off) {
let pos = 0;
for (const n of textKnoten(wurzel)) { if (n === knoten) return pos + off; pos += n.length; }
return -1;
}
// Suchtext normalisieren, mit Rückweg auf die Originalstelle (ß → ss, ü → u)
function normal(text) {
let s = '', karte = [];
for (let i = 0; i < text.length; i++) {
let c = text[i].toLowerCase();
c = c === 'ß' ? 'ss' : c.normalize('NFD').replace(/[̀-ͯ]/g, '');
for (const z of c) { s += z; karte.push(i); }
}
return { s, karte };
}
// ---------- Highlights setzen (API) oder <mark> (Rückfall) ----------
const RF = {}; // Rückfall: Name → Liste gesetzter <mark>
function setze(name, ranges, prio = 0) {
if (API) {
if (!ranges.length) { CSS.highlights.delete(name); return; }
const h = new Highlight(...ranges); h.priority = prio; CSS.highlights.set(name, h); return;
}
loesche(name);
const marks = [];
// rückwärts, damit frühere Ranges gültig bleiben
[...ranges].reverse().forEach(r => {
const teile = textKnoten(r.commonAncestorContainer.nodeType === 3 ? r.commonAncestorContainer.parentNode : r.commonAncestorContainer).filter(n => r.intersectsNode(n));
teile.reverse().forEach(n => {
let a = n === r.startContainer ? r.startOffset : 0, e = n === r.endContainer ? r.endOffset : n.length;
if (e <= a) return;
let mitte = n; if (a > 0) mitte = n.splitText(a); if (e - a < mitte.length) mitte.splitText(e - a);
const m = document.createElement('mark'); m.className = 'hl hl-' + name.replace(/-\d$/, ''); mitte.replaceWith(m); m.appendChild(mitte); marks.push(m);
});
});
RF[name] = marks;
}
function loesche(name) {
if (API) { CSS.highlights.delete(name); return; }
(RF[name] || []).forEach(m => { const p = m.parentNode; if (!p) return; m.replaceWith(...m.childNodes); p.normalize(); });
RF[name] = [];
}
const toast = $('[data-toast]'); let tz;
function melde(t) { toast.textContent = t; toast.classList.add('an'); clearTimeout(tz); tz = setTimeout(() => toast.classList.remove('an'), 2200); }
function hinScrollen(r) {
const b = r.getBoundingClientRect();
if (b.top > innerHeight * .2 && b.bottom < innerHeight * .8) return;
scrollBy({ top: b.top - innerHeight * .3, behavior: reduziert() ? 'auto' : 'smooth' });
}
// ================= Variante „suche“ =================
const feld = $('[data-suche]'), bereich = $('[data-bereich=suche]');
const zaehler = $('[data-zaehler]'), ansage = $('[data-ansage]'), leer = $('[data-leer]');
const vor = $('[data-vor]'), zurueck = $('[data-zurueck]'), leeren = $('[data-leeren]');
let treffer = [], aktiv = -1, entprellt, ansageZeit;
function suchen() {
loesche('treffer-aktiv'); loesche('treffer');
const wort = feld.value.trim(); leeren.hidden = !wort;
treffer = []; aktiv = -1;
$$('.anzahl', bereich).forEach(a => a.textContent = '');
if (wort.length < 2) { zeigeStand(wort ? 'Mindestens zwei Buchstaben' : 'Noch keine Suche'); return; }
const nw = normal(wort).s;
$$('h3, p', bereich).forEach(block => {
if (block.classList.contains('anzahl')) return;
// Blockinhalt ohne Zählabzeichen
const ziel = block.matches('h3') ? block.firstChild : block;
const text = ziel.textContent, { s, karte } = normal(text);
let i = 0;
while ((i = s.indexOf(nw, i)) > -1) {
const a = karte[i], e = karte[i + nw.length - 1] + 1;
const r = block.matches('h3') ? (() => { const x = new Range(); x.setStart(ziel, a); x.setEnd(ziel, e); return x; })() : rangeAus(block, a, e);
if (r) treffer.push({ r, art: block.closest('article') });
i += nw.length;
}
});
// Reihenfolge im Dokument
treffer.sort((x, y) => x.r.compareBoundaryPoints(Range.START_TO_START, y.r));
const jeArtikel = new Map(); treffer.forEach(t => jeArtikel.set(t.art, (jeArtikel.get(t.art) || 0) + 1));
jeArtikel.forEach((n, art) => { art.querySelector('.anzahl').textContent = n + (n === 1 ? ' Treffer' : ' Treffer'); });
setze('treffer', treffer.map(t => t.r), 1);
if (!API) treffer = []; // Rückfall: Ranges sind nach dem Einpacken verbraucht, neu aus den <mark> bilden
if (!API) (RF['treffer'] || []).slice().reverse().forEach(m => { const r = new Range(); r.selectNodeContents(m); treffer.push({ r, art: m.closest('article'), m }); });
leer.hidden = treffer.length > 0; $('[data-leer-wort]').textContent = wort;
if (treffer.length) geheZu(0); else zeigeStand('0 Treffer');
}
function zeigeStand(t) { zaehler.innerHTML = '<span></span>'; zaehler.firstChild.textContent = t; vor.disabled = zurueck.disabled = treffer.length < 2; ansagen(t); }
function geheZu(i, scrollen = true) {
if (!treffer.length) return;
aktiv = (i + treffer.length) % treffer.length;
const t = treffer[aktiv];
if (API) setze('treffer-aktiv', [t.r], 2);
else { (RF['treffer'] || []).forEach(m => m.classList.remove('hl-treffer-aktiv')); t.m.classList.add('hl-treffer-aktiv'); }
zaehler.innerHTML = `${aktiv + 1} <span>von ${treffer.length}</span>`;
vor.disabled = zurueck.disabled = treffer.length < 2;
ansagen(`Treffer ${aktiv + 1} von ${treffer.length}`);
if (scrollen) hinScrollen(t.r);
}
function ansagen(t) { clearTimeout(ansageZeit); ansageZeit = setTimeout(() => { ansage.textContent = t; }, 500); }
feld.addEventListener('input', () => { clearTimeout(entprellt); entprellt = setTimeout(suchen, 120); });
feld.addEventListener('keydown', e => {
if (e.key === 'Enter') { e.preventDefault(); geheZu(aktiv + (e.shiftKey ? -1 : 1)); }
if (e.key === 'Escape') { feld.value = ''; suchen(); }
});
vor.addEventListener('click', () => geheZu(aktiv + 1));
zurueck.addEventListener('click', () => geheZu(aktiv - 1));
leeren.addEventListener('click', () => { feld.value = ''; suchen(); feld.focus(); });
$$('[data-wort]').forEach(b => b.addEventListener('click', () => { feld.value = b.dataset.wort; suchen(); }));
// ================= Variante „lesen“ =================
const artikel = $('[data-bereich=artikel]'), absaetze = $$('[data-p]', artikel);
const markierKnopf = $('[data-markieren]'), notizListe = $('[data-notizen]'), ansageLesen = $('[data-ansage-lesen]');
const SPEICHER = 'salbei-reise-markierungen';
let notizen = [];
try { notizen = JSON.parse(localStorage.getItem(SPEICHER) || '[]'); } catch (_) { notizen = []; }
function auswahlImArtikel() {
const sel = getSelection(); if (!sel.rangeCount || sel.isCollapsed) return null;
const r = sel.getRangeAt(0);
const pA = absaetze.find(p => p.contains(r.startContainer)), pE = absaetze.find(p => p.contains(r.endContainer));
if (!pA || pA !== pE) return null; // eine Markierung je Absatz, sonst unklar
const a = versatzVon(pA, r.startContainer, r.startOffset), e = versatzVon(pA, r.endContainer, r.endOffset);
if (a < 0 || e - a < 2) return null;
return { p: absaetze.indexOf(pA), a, e, text: pA.textContent.slice(a, e).trim() };
}
function zeichneNotizen() {
setze('notiz', notizen.map(n => rangeAus(absaetze[n.p], n.a, n.e)).filter(Boolean), 0);
notizListe.innerHTML = '';
notizen.forEach((n, i) => {
const li = document.createElement('li');
const qq = document.createElement('q'); qq.textContent = n.text.length > 90 ? n.text.slice(0, 88) + ' …' : n.text;
const ak = document.createElement('div'); ak.className = 'aktion';
ak.innerHTML = '<button type="button" data-geh aria-label="Zur Stelle springen"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 8h9M8.5 4.5 12 8l-3.5 3.5" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></button><button type="button" data-weg aria-label="Markierung entfernen"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></button>';
ak.querySelector('[data-geh]').addEventListener('click', () => { const r = rangeAus(absaetze[n.p], n.a, n.e); if (r) hinScrollen(r); absaetze[n.p].setAttribute('tabindex', '-1'); absaetze[n.p].focus({ preventScroll: true }); });
ak.querySelector('[data-weg]').addEventListener('click', () => { notizen.splice(i, 1); speichern(); zeichneNotizen(); ansageLesen.textContent = 'Markierung entfernt. ' + notizen.length + ' gespeichert.'; markierKnopf.focus(); });
li.append(qq, ak); notizListe.appendChild(li);
});
}
function speichern() { if (html.classList.contains('vorfuehrung')) return; try { localStorage.setItem(SPEICHER, JSON.stringify(notizen)); } catch (_) {} }
function markieren(aus) {
const a = aus || auswahlImArtikel(); if (!a) return;
if (notizen.some(n => n.p === a.p && n.a < a.e && a.a < n.e)) { melde('Diese Stelle ist schon markiert.'); return; }
notizen.push(a); notizen.sort((x, y) => x.p - y.p || x.a - y.a); speichern();
getSelection().removeAllRanges(); zeichneNotizen();
ansageLesen.textContent = 'Markiert. ' + notizen.length + ' Stellen gespeichert.';
markierKnopf.disabled = true;
}
markierKnopf.addEventListener('pointerdown', e => e.preventDefault()); // Auswahl beim Klick nicht verlieren
markierKnopf.addEventListener('click', () => markieren());
document.addEventListener('selectionchange', () => {
const a = auswahlImArtikel();
markierKnopf.disabled = !(a && html.dataset.v === 'lesen');
$('[data-teilen]').disabled = !(a && html.dataset.v === 'zitat');
});
// ================= Variante „zitat“ (verlinkte Stelle) =================
const band = $('[data-band]'); let abkling = [];
function stelleZeigen(st) {
abkling.forEach(clearTimeout); abkling = [];
['stelle-1', 'stelle-2', 'stelle-3', 'stelle-4'].forEach(loesche);
if (!st) { band.hidden = true; return; }
const r = rangeAus(absaetze[st.p], st.a, st.e); if (!r) return;
band.hidden = false; band.classList.remove('neu'); void band.offsetWidth; band.classList.add('neu');
setze('stelle-1', [r]);
setTimeout(() => hinScrollen(rangeAus(absaetze[st.p], st.a, st.e) || r), 60);
if (reduziert() || !API) return; // reduziert und Rückfall: bleibt markiert, bis man sie schließt
// ::highlight kann keine Transition: Abklingen in vier Stufen über eigene Namen
[2, 3, 4].forEach((s, i) => abkling.push(setTimeout(() => { CSS.highlights.delete('stelle-' + (s - 1)); setze('stelle-' + s, [rangeAus(absaetze[st.p], st.a, st.e)]); }, 1600 + i * 300)));
}
function stelleAusUrl() {
const m = /^(\d+):(\d+)-(\d+)$/.exec(new URLSearchParams(location.search).get('stelle') || '');
return m ? { p: +m[1], a: +m[2], e: +m[3] } : STANDARD_STELLE();
}
function STANDARD_STELLE() { const t = absaetze[2].textContent, a = t.indexOf('Elektrolyte'), e = t.indexOf('verliert.') + 9; return { p: 2, a, e }; }
$('[data-band-zu]').addEventListener('click', () => { stelleZeigen(null); melde('Markierung entfernt.'); });
$('[data-wieder]').addEventListener('click', () => { scrollTo({ top: 0, behavior: 'auto' }); stelleZeigen(stelleAusUrl()); });
$('[data-teilen]').addEventListener('pointerdown', e => e.preventDefault());
$('[data-teilen]').addEventListener('click', async () => {
const a = auswahlImArtikel(); if (!a) return;
const u = new URL(location.href); u.searchParams.set('v', 'zitat'); u.searchParams.set('stelle', `${a.p}:${a.a}-${a.e}`); u.hash = '';
try { await navigator.clipboard.writeText(u.toString()); melde('Link zur Stelle kopiert.'); } catch (_) { melde('Link: …?stelle=' + a.p + ':' + a.a + '-' + a.e); }
});
// ================= Varianten =================
const WERTE = {
suche: (API ? '<b>CSS.highlights</b>' : 'Rückfall <b><mark></b>') + ' · treffer priority 1 #F2E19A · treffer-aktiv priority 2 Petrol + underline · entprellt 120 ms · ß→ss, ü→u',
lesen: (API ? '<b>::highlight(notiz)</b>' : 'Rückfall <b><mark></b>') + ' · Auswahl → Absatz + Versatz → localStorage · eine Markierung je Absatz',
zitat: (API ? '<b>stelle-1 … stelle-4</b>' : 'Rückfall <b><mark></b>') + ' · hält 1600 ms, dann 4 Stufen à 300 ms (Deckkraft 1 → .2) · ?stelle=absatz:start-ende'
};
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]').innerHTML = WERTE[v];
stelleZeigen(null);
if (v === 'lesen') zeichneNotizen(); else loesche('notiz');
if (v === 'zitat') setTimeout(() => stelleZeigen(stelleAusUrl()), 300);
const q = new URLSearchParams(location.search);
if (q.get('v') !== v) { q.set('v', v); if (v !== 'zitat') q.delete('stelle'); history.replaceState(null, '', '?' + q.toString().replace(/=(&|$)/g, '$1')); }
}
$$('button[data-v]').forEach(b => b.addEventListener('click', () => { pause(); variante(b.dataset.v); scrollTo({ top: 0, behavior: 'auto' }); }));
variante(q0.get('v'));
// ================= Vorführung =================
let zeit = [], laeuft = false;
const nach = (ms, f) => zeit.push(setTimeout(f, ms));
function tippen(wort, ab) { [...wort].forEach((z, i) => nach(ab + i * 90, () => { feld.value = wort.slice(0, i + 1); suchen(); })); return ab + wort.length * 90; }
function auswahl(p, wortA, wortE) { const t = absaetze[p].textContent, a = t.indexOf(wortA), e = t.indexOf(wortE) + wortE.length; const r = rangeAus(absaetze[p], a, e); const s = getSelection(); s.removeAllRanges(); s.addRange(r); return { p, a, e, text: t.slice(a, e) }; }
function runde() {
if (!laeuft) return;
scrollTo({ top: 0, behavior: 'auto' });
if (v === 'suche') {
feld.value = ''; suchen();
let t = tippen('Blutdruck', 500);
nach(t + 900, () => geheZu(aktiv + 1)); nach(t + 1800, () => geheZu(aktiv + 1));
nach(t + 2800, () => { feld.value = ''; suchen(); });
t = tippen('Termin', t + 3200);
nach(t + 1000, () => geheZu(aktiv + 1));
nach(t + 2200, runde);
} else if (v === 'lesen') {
notizen = []; zeichneNotizen();
nach(700, () => auswahl(1, 'Packen', 'drei Tage.'));
nach(1700, () => markieren({ ...auswahl(1, 'Packen', 'drei Tage.') }));
nach(2700, () => auswahl(3, 'Sonnenschutz', 'Blasenpflaster'));
nach(3700, () => markieren({ ...auswahl(3, 'Sonnenschutz', 'Blasenpflaster') }));
nach(6200, () => { notizen = []; zeichneNotizen(); runde(); });
} else {
nach(300, () => stelleZeigen(STANDARD_STELLE()));
nach(4600, runde);
}
}
function neustart() {
zeit.forEach(clearTimeout); zeit = [];
feld.value = ''; suchen(); getSelection().removeAllRanges(); stelleZeigen(null);
scrollTo({ top: 0, behavior: 'auto' });
if (v === 'zitat') stelleZeigen(stelleAusUrl());
}
function vorfuehren() {
zeit.forEach(clearTimeout); zeit = [];
laeuft = true; html.classList.add('vorfuehrung'); runde();
}
function pause() {
const warVorfuehrung = html.classList.contains('vorfuehrung');
laeuft = false; zeit.forEach(clearTimeout); zeit = []; html.classList.remove('vorfuehrung');
// Vorführung speichert nichts: gespeicherten Stand zurückholen
if (warVorfuehrung) { try { notizen = JSON.parse(localStorage.getItem(SPEICHER) || '[]'); } catch (_) { notizen = []; } if (v === 'lesen') zeichneNotizen(); }
}
function weiter() { if (!laeuft) { laeuft = true; html.classList.add('vorfuehrung'); runde(); } }
})();
</script>
</body>
</html>