/* ============================================================
   Kontrollpunkt Hamburg: Designsystem

   Gebaut nach docs/15-gestaltungssprache.md, dem Extrakt aus Puffer
   (Design.swift) und cl3verdesign (tokens.css). Die fuenf Saetze, die
   hier alles bestimmen:

   1. Tiefe kommt aus der Kante. Jede Flaeche traegt genau eine 1 px
      Kontur mit gerechnetem Kontrast. Der Schatten bestaetigt die
      Hoehe, er erzeugt sie nicht. Reihenfolge der Wichtigkeit:
      flache Flaeche, eine Kontur, ein enger Schatten.
   2. Die Karte ist der laute Kanal. Alle Bedienelemente werden
      leiser, nicht lauter. Kein Akzent auf Tafeln, keine Signalfarbe
      an etwas, das nichts meldet. Der Meldeknopf traegt Tinte.
   3. Typografie in zwei Stimmen mit weitem Abstand: winziger
      Versalien-Eyebrow, weit gesperrt, gegen grosse, eng laufende
      Zeilen. Dazwischen ist Platz, keine sechs Zwischengroessen.
   4. Bewegung ist Rueckmeldung: eine Ausklingkurve, vier Dauern,
      Druck als scale(0.96) mit Schattenkollaps.
   5. Wenige, grosse, klar getrennte Ziele, und jedes traegt sein
      Wort. Ein Symbol allein steht nur dort, wo die Handlung ohne
      Sprache eindeutig ist: Zoom, Standort, Schliessen. Alles, was
      etwas auswaehlt, umschaltet oder abschickt, wird beschriftet.
      Nachgetragen aus dem Vorbild des Auftraggebers (FreiFahren):
      Suche als breites Feld statt Symbol, Ansichtswechsel als Tafel
      mit Symbol und Wort, Meldeknopf mit Wort.

   Bedienlage: einhaendig, oft unterirdisch, oft in einer fahrenden
   Bahn. Daraus folgt: Ziele ab 44 px, Kartensteuerung im Daumenbogen,
   Kontrast nach Messung statt nach Gefuehl.
   ============================================================ */

@font-face {
  font-family: "Archivo";
  src: url("fonts/archivo.woff2") format("woff2-variations");
  font-weight: 400 800;
  font-style: normal;
  font-display: swap;
}

/* ------------------------------------------------------------
   DER TOKEN-BLOCK STEHT NICHT MEHR HIER.

   Radien, Abstaende, Hoehen, Schriftgroessen, Schriftgewichte, Zeilenhoehen,
   Farben und die Eckform stehen einmal in gestalt/gestalt.json und werden
   daraus in ZWEI Ausgaben erzeugt:

     web/gestalt.css                  fuer diese Datei (in index.html davor)
     ios/Kontrollpunkt/Gestalt.swift  fuer die native Huelle

   WARUM: dieselben Zahlen standen hier UND in HudAufsatz.swift. Kein Test hat
   das gefangen, nur der Abzug -- docs/25-natives-hud.md nennt es unter "Was die
   Doppelpflege kostet" ausdruecklich "die teuerste Stelle". Jetzt erzeugt ein
   Skript beide Seiten, und web/tests/gestalt.test.mjs erzeugt sie im Test noch
   einmal und vergleicht Zeichen fuer Zeichen.

   Wer einen Wert aendern will, aendert gestalt/gestalt.json und laesst
   `node gestalt/erzeugen.mjs` laufen. Die Begruendungen zu jedem Wert stehen
   dort ebenfalls, und sie reisen als Kommentar in beide Ausgaben mit.

   Was WEITERHIN hier steht: alles, was eine Regel ist und kein Wert.
   ------------------------------------------------------------ */

* {
  box-sizing: border-box;
}

/* Klassen mit eigenem display (.tor, .bogen, .schritte, .legende) wuerden das
   hidden-Attribut sonst ueberstimmen. */
[hidden] {
  display: none !important;
}

/*
 * DIE SCHRIFTBRUECKE: iOS-Dynamic-Type erreicht die WebView sonst gar nicht.
 *
 * GEMESSEN im Simulator mit `simctl ui content_size accessibility-extra-extra
 * -extra-large`: eine WKWebView wendet die Systemtextgroesse nicht auf `rem`
 * an, das Web-HUD aendert seine Groesse nicht um einen Pixel. Wer 200 Prozent
 * eingestellt hat, sah davon in dieser App nie etwas -- weder im HUD noch in
 * den Boegen, den Rechtstexten oder der Einfuehrung.
 *
 * Die Huelle kennt die Antwort und setzt sie hier ein
 * (`schriftgroesseSpiegeln` in ios/Kontrollpunkt/WebAnsicht.swift). Der Faktor
 * ist auf 2,0 gedeckelt, und der Deckel ist kein Geiz: 200 Prozent ist die
 * Groesse, gegen die dieses Layout gemessen ist (docs/messung/*-schrift2.png,
 * docs/18-handy-messung.md). Darueber gaebe es keine Messung, sondern nur die
 * Hoffnung.
 *
 * `100%` und nicht `16px`: 100 Prozent ist die Grundschriftgroesse, die der
 * Nutzer in seinem Browser eingestellt hat. Ueber die wird WCAG 1.4.4 in
 * dieser App erfuellt, und sie bleibt damit erhalten, statt ueberschrieben zu
 * werden. Ohne die Variable steht der Faktor auf 1, also genau der Bestand.
 */
html {
  font-size: calc(100% * var(--dynamic-type, 1));
}

html,
body {
  height: 100%;
  margin: 0;
  /* clip statt hidden: hidden macht das Element zum Scrollbehaelter, den iOS
     beim Tastaturfokus verschiebt. Geclippt bleibt das Vollbild stehen. */
  overflow: clip;
  overscroll-behavior: none;
}

/*
 * ALLE SCHRIFTGROESSEN STEHEN IN REM, ALLE LAENGEN IN PX.
 *
 * Das ist kein Geschmack, sondern der Unterschied zwischen "Text laesst sich
 * vergroessern" und "sieht so aus". In px gesetzte Schrift ignoriert die
 * Grundschriftgroesse, die der Nutzer in seinem Browser eingestellt hat; damit
 * war WCAG 1.4.4 (200 Prozent) nicht erfuellbar, egal wie das Layout gebaut
 * ist. Zusammen mit dem gestrichenen `maximum-scale` in index.html traegt die
 * Einstellung jetzt.
 *
 * Die Laengen bleiben absichtlich in px: Hoehen, Fugen und Raender dieser App
 * sind gegen Fingerbreiten und Daumenreichweiten gemessen (siehe die Rechnung
 * ueber .unten in index.html), und ein 44-px-Ziel bleibt 44 px, auch wenn die
 * Schrift waechst. Was mit der Schrift wachsen muss, traegt statt einer festen
 * Hoehe ein min-height. Nachgemessen bei 130 und 200 Prozent, siehe
 * docs/18-handy-messung.md.
 */
body {
  background: var(--grund);
  color: var(--schrift);
  font-family: var(--schrift-familie);
  font-size: 1rem;
  font-weight: 400;
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
}

button {
  font: inherit;
  color: inherit;
  border: none;
  background: none;
  cursor: pointer;
}

:focus-visible:not(.bogen) {
  /* Der sichtbare Ring ist box-shadow, nicht outline: nur damit lassen sich
     zwei Toene uebereinanderlegen (siehe --fokus-ring). Die TRANSPARENTE
     Kontur bleibt trotzdem stehen, weil der Windows-Kontrastmodus jeden
     Schatten verwirft und stattdessen die Kontur einfaerbt; ohne sie haette
     dort niemand einen Fokus. `:not(.bogen)` haelt den Bogen heraus, der seinen
     eigenen Schwebeschatten traegt und seinen Fokus nie zeigt. */
  outline: 3px solid transparent;
  outline-offset: 3px;
  box-shadow: var(--fokus-ring);
}

/*
 * Der Eyebrow. Winzig, halbfett, weit gesperrt, Versalien, gedaempft. Das ist
 * die eine Stelle, an der Versalien stehen; Fliesstext und Listenzeilen laufen
 * normal. Sperrung +0,16em nach Puffers tracking 0.9 auf 11pt und
 * cl3verdesigns 0,20em, zwischen beiden.
 */
/* .lage-marke stand hier als sechster Geschwisterselektor. Der Eyebrow "LAGE"
   ist aus index.html entfernt (Begruendung dort), also faellt er auch hier
   weg statt als toter Selektor stehen zu bleiben.

   .abschnitt stand hier bis zum 29.08.2026 als siebter. Der Eyebrow ist eine
   MARKE ueber einem Titel -- "EINSTELLUNGEN UND RECHTLICHES" ueber "Mehr" --
   und in dieser Rolle traegt er. Die Zwischenueberschriften einer langen
   Einstellungsliste sind aber keine Marken, sondern GLIEDERUNG: man springt
   an ihnen entlang. Grau, 11 px und 0,16em gesperrt ist dafuer die schwaechste
   verfuegbare Form, und der Auftraggeber hat genau das gesehen: "Die
   Einstellungen sehen noch sehr alt aus, wie das erste App-Design."

   Ihre eigene Regel steht unter "Einstellungen".

   .text h3 ist am selben Tag denselben Weg gegangen. Ein Rechtstext hat sechs
   bis acht Zwischenueberschriften, an denen man entlangspringt, um die eine
   zu finden, die man sucht -- "Umsatzsteuer", "Streitschlichtung". In
   gesperrten Versalien bei 11 px in --schrift-3 waren sie leiser als der
   Fliesstext, den sie gliedern sollten. */
.bogen-unter,
.tor-marke {
  text-transform: uppercase;
  font-size: var(--t-eyebrow-groesse);
  font-weight: var(--t-eyebrow-gewicht);
  letter-spacing: var(--t-eyebrow-sperrung);
  color: var(--schrift-3);
  font-variant-numeric: tabular-nums;
  /*
   * DER EYEBROW MUSS BRECHEN DUERFEN, UND ZWAR MITTEN IM WORT.
   *
   * Gemessen am 28.08.2026 (tests/schnitt.mess.mjs, elementweise): bei 320 px
   * und 200 Prozent Systemschrift brauchte "NÄHERUNGSWARNUNG" 307 px in einer
   * 284 px breiten Spalte -- 23 px, die .bogen-inhalt mit `clip` verschluckte,
   * ohne zu rollen. Deutsche Komposita plus Versalien plus 0,16em Sperrung
   * ergeben Woerter, die an keiner Fuge brechen; `anywhere` ist hier die
   * einzige Regel, die traegt. Ein Eyebrow ist eine Marke und kein Fliesstext,
   * ein Bruch mitten im Wort kostet dort nichts.
   */
  overflow-wrap: anywhere;
}

/* ---------- Grundgeruest ---------- */

#karte {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  background: var(--karte-grund);
}

/*
 * DREI MATERIALSTUFEN, NICHT EINE.
 *
 * Vorher trugen .oben, .status, .melden, .rund, .offline und .toast exakt
 * dieselben drei Zeilen, mit der Begruendung "damit es genau eine
 * Materialsprache gibt". Der Satz stimmt, die Umsetzung war die falsche
 * Schlussfolgerung: eine Sprache ist nicht ein einziges Wort. Wenn jede Flaeche
 * gleich weit ueber dem Grund liegt, sagt die Oberflaeche nichts ueber Rang und
 * Dauerhaftigkeit, der Blick findet keine Ordnung, und genau das liest sich als
 * brav. Der Toast schwebte nicht staerker als die Kopfleiste, obwohl er nach
 * vier Sekunden wieder weg ist.
 *
 * Die Achse ist DAUERHAFTIGKEIT, nicht Wichtigkeit. Je vergaenglicher eine
 * Flaeche, desto weiter ihr Schatten:
 *
 *   Stufe 2  Auflage    liegt dauerhaft auf   .oben .status .rund-frei
 *   Stufe 3  Schwebend  kommt und geht        .toast .offline .bogen
 *   Signal   quer dazu  anderer Stoff         .melden
 *
 * Stufe 1 (eingebettet, gehoert zur Tafel statt auf ihr zu liegen) traegt keinen
 * Schatten und steht bei .filter-knopf und den Listenzeilen.
 *
 * Die beiden freien Kreise sind auf Stufe 2 und nicht hoeher: sie liegen
 * dauerhaft da, genau wie die zwei Tafeln, und ein Weitschatten unter einem
 * 44-px-Kreis liest sich als Aufkleber. .werkzeuge und .suchknopf sind aus
 * dieser Aufzaehlung verschwunden, weil die Bedienzeile weg ist, auf der sie
 * lagen.
 */
/*
 * DIE 1-PX-KONTUR IST HIER RAUS, und das ist die teuerste Entscheidung dieser
 * Datei. Sie wird deshalb mit Zahlen begruendet, nicht mit Geschmack.
 *
 * Verlangt hat es der Auftraggeber, nachdem er die App auf dem Geraet benutzt
 * hat: "um die ganzen Objekte, um die Meldungen, um Suchleiste, um die
 * Bahnverbindung oben ist immer diese weisse Linie. Die einmal rausnehmen,
 * weil die trennt die Sachen viel zu stark vom Interface."
 *
 * WAS SIE GEKOSTET HAT. Ohne Kontur traegt die Tafelkante gegen die Karte nur
 * 1,00:1 hell (ueber einer weissen Strasse) und 1,01:1 dunkel (ueber einem
 * Platz) und verfehlt damit die 3:1 aus WCAG 1.4.11 fuer grafische Objekte.
 *
 * DIE VIER AUSWEGE SIND DURCHGERECHNET, nicht abgewunken. Skript und volle
 * Tabelle: docs/messung/kante.mjs, gerechnet gegen alle zehn Kartenflaechen
 * aus PALETTE in js/kartenstil.js und alle zehn Linienfarben, beide Modi.
 *
 *  A  HOEHERE DECKUNG DES GLASES. Tot, und zwar prinzipiell. Die Tafelfarbe
 *     IST die Kartenfarbe: --flaeche ist hell #ffffff und die Karte zeichnet
 *     Strassen und Stationen in #FFFFFF; dunkel liegt --flaeche #1b2028 neben
 *     dem Platzton #191F2C. Selbst bei 100 Prozent Deckung, also im
 *     rechnerischen Bestfall, bleiben 1,00:1 hell und 1,01:1 dunkel. 82, 90,
 *     95 und 100 Prozent ergeben denselben Wert. Mehr Deckung hilft nicht,
 *     sie kostet nur die Durchsicht, die Punkt 10 ausdruecklich will.
 *  B  WEICHER SCHLAGSCHATTEN STATT LINIE. Hell moeglich, aber nicht weich:
 *     3:1 entsteht erst, wenn der Untergrund neben der Tafel mit Schwarz bei
 *     39 bis 42 Prozent Deckkraft abgedunkelt wird. Das ist keine Tiefe mehr,
 *     das ist ein schwarzer Rand, also genau die Trennung, die weg sollte.
 *     Dunkel ist es unmoeglich, und die Schranke steht fest, bevor man eine
 *     Deckkraft waehlt: die Glastafel hat ueber dem Kartengrund die
 *     Leuchtdichte 0,013, und gegen REINES SCHWARZ erreicht sie damit
 *     hoechstens 1,26:1. Kein Schatten der Welt kommt dort auf 3.
 *  C  KONTUR IN DER FLAECHENFARBE. Rechnerisch dasselbe wie A bei voller
 *     Deckung, weil dieselbe Farbe gemessen wird: 1,00:1 und 1,01:1.
 *  D  DIE AM WENIGSTEN SICHTBARE KONTUR, die 3:1 noch haelt. Eine Kante muss
 *     zugleich gegen die hellste Kartenflaeche (Station #FFFFFF, in beiden
 *     Modi) und die dunkelste halten. Daraus folgt ein Fenster, und in ihm
 *     liegt der Bestand schon am guenstigsten Rand: die Suche ueber alle 256
 *     Grauwerte findet hell #757575 mit 4,51:1 gegen die eigene Tafel,
 *     --kontur #6f767f steht bei 4,49:1; dunkel #939393 mit 5,43:1 gegen
 *     5,44:1 des Bestands. Es ist also NICHTS zu holen. Weniger Trennung und
 *     3:1 sind hier dieselbe Zahl mit anderem Vorzeichen: wer die Kante
 *     leiser stellt, stellt sie unter die Schwelle.
 *
 * WAS STATTDESSEN TRAEGT. WCAG 1.4.11 verlangt keine Kante, es verlangt 3:1
 * fuer die visuelle Information, die ein Bedienelement KENNZEICHNET. Das ist
 * eine Wahl, und sie faellt jetzt anders:
 *  - .oben und .status sind Leseflaechen. Was sie kennzeichnet, ist ihr Text,
 *    und der faellt unter WCAG 1.4.3 mit 4,5:1. Gemessen gegen den
 *    schlechtesten Untergrund haelt die leise Schrift --schrift-2 auf dem Glas
 *    5,30:1 hell (unter S3) und 4,78:1 dunkel (unter der weissen Station).
 *  - Die freistehenden Kreise tragen ihr Symbol in --schrift-2, dieselben
 *    Werte, Ziel 3:1 fuer grafische Objekte, also mit weiter Reserve.
 *  - Der Meldeknopf traegt seine Kante in der FUELLUNG, siehe .melden.
 * WAS VERLOREN GEHT, ungeglaettet: Wer die FORM der Tafel braucht und nicht
 * ihren Inhalt, verliert sie. Das ist der Preis, er ist bezahlt, und er ist
 * mit einem Wert je Modus wieder einzusammeln (--kontur-tafel unten).
 *
 * Die Materialstufen bleiben, was sie waren. Nur die Kante faellt weg:
 *
 *   Stufe 2  Auflage    liegt dauerhaft auf   .oben .status .rund-frei
 *   Stufe 3  Schwebend  kommt und geht        .toast .offline .bogen
 *   Tinte    quer dazu  anderer Stoff         .melden (jetzt Signalfarbe)
 */
.oben,
.status,
.rund-frei,
.melden {
  background: var(--flaeche);
  border: var(--kontur-tafel);
  box-shadow: var(--schatten-auflage);
}

/*
 * GLAS ueber der Karte.
 *
 * Was ueber der Karte schwebt und sie nicht ersetzt, laesst sie durchscheinen:
 * die Bandtafel oben, die Lagetafel unten und die beiden freistehenden Kreise.
 * NICHT der Bogen, denn der deckt die Karte bewusst ab und traegt Formulare,
 * und NICHT der Meldeknopf, der eine Signalfarbe traegt und sie nicht
 * verduennen darf. Zwei Glasscheiben uebereinander summieren ihre Truebung, und
 * der gemessene Wert von 82 Prozent gilt fuer genau eine Scheibe; deshalb liegt
 * in dieser Fassung auch nichts mehr auf einer Glasflaeche, was selbst Glas
 * waere. Die Bedienzeile mit ihrer deckenden Suchzeile und Zoomkapsel ist
 * entfallen, damit ist der einzige Stapel weg, den es gab.
 *
 * DIE KONTUR IST GEFALLEN. Warum, mit welcher Rechnung und mit welchem
 * Restschaden, steht vollstaendig im Block ueber dieser Regel. Was Glas hier
 * jetzt ausmacht, ist die Lichtbrechung ohne Linie: --glas-kante als helle
 * INNENkante oben, wie an einer echten Scheibe, plus die Saettigung des
 * Untergrunds und der enge Kontaktschatten. Alle drei tragen Tiefe, keiner
 * behauptet eine gemessene Grenze.
 *
 * @supports, weil ohne backdrop-filter eine 82-Prozent-Flaeche einfach nur
 * durchsichtig waere. Dort bleibt es bei der vollen Deckung von oben.
 */
/* Der Filtervorrat aus index.html zeichnet nichts, er wird nur referenziert.
   Ohne diese Regel steht ein 0x0-SVG als Inline-Kasten im Fluss und schiebt
   die Karte um die Zeilenhoehe nach unten. */
.glas-brechung-vorrat {
  position: absolute;
  width: 0;
  height: 0;
  pointer-events: none;
}

/*
 * DER FALLSTRICK, AN DEM DIESE UMSETZUNG HAETTE SCHEITERN KOENNEN.
 *
 * Ein Element mit filter, backdrop-filter, opacity < 1, mask oder clip-path
 * wird zum BACKDROP ROOT. Ein KIND mit eigenem backdrop-filter findet hinter
 * sich dann nichts mehr, es filtert eine leere Flaeche. Genau das braeuchte
 * man aber, wenn das Randband IN der Glasflaeche laege.
 *
 * Deshalb ist die Bauweise hier: die Tafel selbst bekommt KEINEN Filter mehr,
 * sie ist nur noch Kasten und Schatten. Die beiden Materiallagen sind ihre
 * zwei Pseudoelemente, und die sind GESCHWISTER, nicht verschachtelt:
 *
 *   ::before   Flaeche   blur(20px) saturate(180%) + Glanz + Tupfer
 *   ::after    Randband  blur(9px)  saturate(300%) brightness(1.06)
 *   Kinder     Inhalt    Text und Symbole, unberuehrt
 *
 * Beide liegen auf z-index -1, also unter dem Inhalt und ueber der Karte;
 * .oben und .unten spannen mit position:fixed und z-index:10 je einen eigenen
 * Stapelzusammenhang auf, aus dem die -1 nicht herausfaellt.
 *
 * ::after wird nach ::before gemalt und sieht in seinem Backdrop deshalb, was
 * ::before gemalt hat. Damit das Band den ROHEN Kartengrund buendelt und nicht
 * die schon getruebte Scheibe, laesst ::before seine aeusseren 7 px frei
 * (mask-clip: content-box). Die Flaeche endet also, wo das Band anfaengt.
 *
 * Und: Transparenz steht ueberall im Alpha-Kanal von background-color, nie in
 * opacity. Ein opacity: 0.9 auf .oben wuerde beide Lagen mit einem Schlag
 * blind machen.
 */

/*
 * --tupfer-staerke muss registriert sein, sonst ist sie fuer den Browser ein
 * Text und laesst sich nicht ueberblenden: das Leuchten wuerde springen statt
 * aufzuziehen. @property gibt es in Safari ab 16.4, die App laeuft auf iOS 26.
 * inherits, weil der Wert auf der Tafel steht und das Pseudoelement ihn liest.
 */
@property --tupfer-staerke {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}

@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .oben,
  .status,
  .rund-frei {
    /* Kein Filter, keine Maske, kein opacity: sonst waere die Tafel Backdrop
       Root und beide Lagen darunter blind. Siehe den Block darueber. */
    background: transparent;
    position: relative;
    box-shadow: var(--schatten-auflage);
  }

  /*
   * DIE BEIDEN SCHWEBENDEN MELDER. Dieselbe Flaeche, aber ihr eigener
   * Schatten: sie liegen UEBER dem HUD, nicht darin. Und sie tragen KEIN
   * Randband -- das gebuendelte Licht an der Kante ist das Zeichen der
   * dauerhaften Werkzeuge; ein Melder, der drei Sekunden steht, borgte sich
   * damit eine Bedeutung, die er nicht hat.
   *
   * .toast blendet mit opacity ein und ist waehrend dieser 160 ms Backdrop
   * Root, filtert also eine leere Flaeche. Das ist der Preis und er ist
   * gerechnet: am Ende steht opacity genau auf 1, und dann greift das Glas.
   */
  .toast,
  .offline {
    background: transparent;
    box-shadow: var(--schatten-schwebend);
  }

  /*
   * LAGE 1, DIE FLAECHE. Drei Dinge, von unten nach oben: der Weichzeichner auf
   * dem Kartengrund, die 82-Prozent-Fuellung und der schwache Tupfer unter dem
   * Finger. Der spekulare Glanz steht NICHT hier, sondern auf dem Band; warum,
   * steht im Token-Block bei --glanz-spitze, und es ist eine gerechnete Grenze
   * und keine Vorliebe.
   */
  .oben::before,
  .status::before,
  .rund-frei::before,
  .toast::before,
  .offline::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    border-radius: inherit;
    pointer-events: none;
    background-color: var(--glas);
    background-image: radial-gradient(
      circle var(--tupfer-radius) at var(--tupfer-x) var(--tupfer-y),
      rgb(var(--tupfer-ton) / calc(var(--tupfer-staerke) * var(--tupfer-spitze))) 0%,
      rgb(var(--tupfer-ton) / 0) 72%
    );
    -webkit-backdrop-filter: var(--glas-filter);
    backdrop-filter: var(--glas-filter);
    /* Beide Kanten sitzen auf der Flaeche, nicht auf der Tafel: ein inset-
       Schatten der Tafel laege UNTER dem Pseudoelement und waere unsichtbar. */
    box-shadow: var(--glas-kante), var(--glas-gegenkante);
  }
}

/*
 * LAGE 2, DAS RANDBAND. Steht in einem eigenen @supports, weil ohne
 * mask-composite beide Masken ausfallen wuerden: das Band deckte dann die
 * ganze Tafel mit dem staerkeren Filter, und die Flaeche waere weg. Faellt
 * dieser Block aus, bleibt genau der Bestand von vorher stehen, nur ohne die
 * 7-px-Aussparung -- eine vollstaendige Glasflaeche, keine kaputte.
 *
 * mask-composite: exclude gibt es in Safari seit 15.4. WAS NICHT BELEGT IST:
 * ob WebKit eine Maske und ein backdrop-filter AUF DEMSELBEN Element sauber
 * zusammenrechnet. Das entscheidet ein Lauf auf dem Geraet, nicht diese Datei.
 */
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) and
  (mask-composite: exclude) {
  /* Die Flaeche hoert an der Bandkante auf. */
  .oben::before,
  .status::before,
  .rund-frei::before {
    padding: var(--glas-rand-breite);
    mask-image: linear-gradient(#000 0 0);
    mask-clip: content-box;
  }

  .oben::after,
  .status::after,
  .rund-frei::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    border-radius: inherit;
    pointer-events: none;
    padding: var(--glas-rand-breite);
    /*
     * Fuellung, spekularer Glanz und das austretende Licht, in einer Regel.
     * Beide Verlaeufe sitzen hier und nicht auf der Flaeche, weil auf diesen
     * 7 px kein Text steht und damit keine Kontrastschranke gilt -- und weil
     * eine Fase genau der Ort ist, an dem Licht sichtbar wird.
     */
    background-color: var(--glas-rand);
    background-image:
      radial-gradient(
        circle var(--tupfer-radius) at var(--tupfer-x) var(--tupfer-y),
        rgb(255 255 255 / calc(var(--tupfer-staerke) * var(--rand-tupfer-spitze))) 0%,
        rgb(255 255 255 / 0) 78%
      ),
      linear-gradient(
        var(--glanz-winkel),
        rgb(255 255 255 / var(--glanz-spitze)) 0%,
        rgb(255 255 255 / 0) 42%,
        rgb(255 255 255 / 0) 58%,
        rgb(255 255 255 / var(--glanz-gegen)) 100%
      );
    -webkit-backdrop-filter: var(--glas-rand-filter);
    /*
     * STUFE 2 HAENGT HIER HINTEN AN, und zwar nur ueber eine Variable.
     *
     * --brechung-rand ist leer, solange js/brechung.js nichts setzt; dann
     * steht hier woertlich die Stufe-1-Kette. Setzt das Modul sie, wird daraus
     * blur, dann saturate, dann die Verschiebung. DIE REIHENFOLGE IST GEMESSEN:
     * steht das url() VORN, faellt der Weichzeichner in Chromium sichtbar aus
     * (docs/messung/glas/chrom-reihenfolge.png).
     *
     * Und deshalb keine @supports-Weiche mehr: beide Engines bejahen die
     * Grammatik, nur eine rechnet wirklich, und WebKit verwirft bei einem
     * unbekannten url() die GANZE Kette. Diese eine Entscheidung liegt in
     * js/brechung.js, gemessen und begruendet, nicht in einer Abfrage, die
     * etwas anderes prueft als sie behauptet.
     */
    backdrop-filter: var(--glas-rand-filter) var(--brechung-rand, );
    /* Zwei gleiche Masken, eine auf den Inhaltskasten geklemmt, eine auf den
       Rahmenkasten. exclude zieht die erste von der zweiten ab: uebrig bleibt
       genau der 7-px-Ring, mit den Innenradien der Ecken. */
    mask-image: linear-gradient(#000 0 0), linear-gradient(#000 0 0);
    mask-clip: content-box, border-box;
    mask-composite: exclude;
  }
}

/*
 * STUFE 2 UND STUFE 3: WAS HIER NICHT MEHR STEHT, UND WARUM.
 *
 * Hier stand eine @supports-Weiche auf `-webkit-backdrop-filter: url(...)`,
 * die Stufe 2 in WebKit einschalten und Chromium aussperren sollte. Sie war
 * genau falsch herum, und sie hat nicht nichts getan, sondern Schaden
 * angerichtet. Gemessen am 18.08.2026, beide Engines, Streifentest:
 *
 *   Engine        CSS.supports        CSS.supports          wendet url()
 *                 backdrop-filter     -webkit-backdrop-     wirklich an?
 *                 url()               filter url()
 *   Chromium 151  ja                  NEIN                  JA
 *   WebKit 605    ja                  ja                    nein
 *
 * Die Weiche traf also in WebKit zu -- und WebKit verwirft bei einem
 * unbekannten url() die GANZE Filterkette. Auf iOS 26.5 stand deshalb keine
 * Glasflaeche mehr, sondern eine Folie: der Weichzeichner war weg, das
 * Streifengitter stand scharf durch beide Lagen (Abzug
 * docs/messung/glas/ios-bauweise.png, Tafel 2 gegen Tafel 1).
 *
 * Die alte Fassung hatte das Gegenteil notiert, weil ihr Beleg eine
 * WKWebView-Momentaufnahme auf dem Mac war -- und `takeSnapshot` zeichnet
 * backdrop-filter ueberhaupt nicht mit (nachgeprueft mit blur(12px) ohne
 * jede Fuellung: sichtbar nichts). Das Grau in jenem Abzug kam von der
 * 82-Prozent-Fuellung, nicht vom Filter.
 *
 * Was an ihre Stelle tritt:
 *   Stufe 2  js/brechung.js entscheidet und haengt --brechung-rand an das
 *            Randband (siehe dort und oben bei .oben::after).
 *   Stufe 3  Apples eigenes Material, von der nativen Huelle UEBER die
 *            WebView gelegt (ios/Kontrollpunkt/GlasAufsatz.swift). Die Seite
 *            macht dafuer nur Platz, siehe .nativ-belegt.
 */

/*
 * "MATT" HEISST MATT -- AUCH AM RAND.
 *
 * DIE BEANSTANDUNG, woertlich: "die Mitte der Buttons ist matt mit einem
 * glaenzenden Rand, es soll aber komplett matt sein, bei der dunklen Version
 * wie bei der hellen es schon ist."
 *
 * GEMESSEN am 28.08.2026 im Simulator, waagerechtes Profil quer durch die
 * Kopftafel bei Stufe "Matt", jeweils die LINKE Kante (rechts sitzt das
 * Mehr-Zeichen und faelscht die Zahlen):
 *
 *   dunkel   Karte 19-24  ->  Band 94-97  ->  Flaeche 33      Faktor 2,9
 *   hell     Karte 227-239 -> Band 249-251 -> Flaeche 250     Faktor 1,0
 *
 * Der Auftraggeber hat also genau gesehen, was da ist, und auch richtig, dass
 * es nur die dunkle Fassung trifft.
 *
 * DIE URSACHE IST NICHT DIE FUELLUNG. --glas-rand ist derselbe Ton wie --glas,
 * nur mit 55 statt 82 Prozent; ueber einer dunklen Karte gerechnet liegt das
 * Band damit bei rund 27 und die Flaeche bei rund 30 -- das Band waere also
 * eher dunkler. Was es aufhellt, sind drei Zutaten, die zusammen einen
 * SPEKULAREN GLANZ ergeben: ein weisser Verlauf mit bis zu --glanz-spitze
 * (0,34 dunkel), ein weisser Tupfer mit --rand-tupfer-spitze (0,32) und die
 * Aufhellung im Bandfilter. Auf hellem Grund sind alle drei unsichtbar, weil
 * Weiss auf 250 nichts mehr aufhellt. Auf dunklem Grund sind sie der ganze
 * Effekt.
 *
 * Ein spekularer Glanz ist genau das, was eine Flaeche NICHT matt macht. Er
 * gehoert zu den glaenzenden Stufen ("Linse", "System") und faellt hier weg.
 * Der 9-px-Weichzeichner des Bandes bleibt: er traegt die Fase, und eine Fase
 * ohne Glanz ist immer noch eine Fase.
 *
 * Warum die Regel fuer BEIDE Darstellungen gilt und nicht nur fuer die dunkle:
 * in der hellen aendert sie gemessen nichts (Band 249-251 gegen Flaeche 250),
 * und eine Regel, die nur unter einer Bedingung gilt, muss man beim naechsten
 * Mal wieder pruefen.
 */
:root[data-glas="matt"] {
  --glanz-spitze: 0;
  --glanz-gegen: 0;
  --rand-tupfer-spitze: 0;
}

/*
 * STUFE 2, DIE EINE ZEILE, DIE SIE IM STIL BRAUCHT.
 *
 * Wo wirklich gebrochen wird, hoert das Band auf zu streuen. 2 statt 9 px:
 * eine Verschiebung auf verwaschenem Grund ist wieder nur verwaschener Grund,
 * und dann waere Stufe 2 gebaut, aber nicht zu sehen. Gesetzt wird data-glas
 * von js/glas.js, und zwar auf das, was WIRKLICH liegt -- steht dort
 * "gebrochen", hat die Engine die Brechung gemessen bestanden.
 */
:root[data-glas="gebrochen"] {
  --glas-rand-blur: 2px;
}

/*
 * Stufe 3: der Platz, den die Seite freihaelt.
 *
 * visibility statt display: der Knopf behaelt seinen Kasten, denn genau den
 * meldet js/glas.js an die Huelle, damit das native Glas dort sitzt, wo sonst
 * der Kreis sass. Mit display:none waere das Rechteck leer und das Glas
 * landete in der Ecke.
 */
.rund-frei.nativ-belegt {
  visibility: hidden;
}

/*
 * DAS GANZE DAUERHAFTE HUD TRITT ZURUECK, WENN DIE HUELLE ES UEBERNIMMT.
 *
 * Bis zum 18.08.2026 galt das nur fuer die beiden Kreise, und genau das war
 * die Beschwerde: "kein Liquid Glass" und "der Schalter aendert die Optik gar
 * nicht". Beides war zutreffend -- unter 4000 pt² trugen Apples Material, der
 * Rest weiter das im Web nachgebaute Glas.
 *
 * DER GRUND, WARUM ES NICHT ANDERS GEHT: die Karte liegt IN der WebView, ein
 * Material, das den Untergrund bricht, muss DARUEBER liegen, und was darueber
 * liegt, verdeckt den Web-Inhalt an derselben Stelle. Was Glas tragen soll,
 * muss also nativ werden -- Flaeche UND Inhalt.
 *
 * `visibility` und nicht `display`: der Kasten bleibt stehen. js/glas.js misst
 * genau ihn und meldet ihn an die Huelle; mit `display: none` waeren die
 * Rechtecke leer und die Tafeln landeten in der Ecke. Und der Kasten ist auch
 * die Groesse, die die Huelle zeichnet -- das Layout bleibt damit vollstaendig
 * Sache dieser Datei, samt der Arbeit fuer 320 px bei 200 Prozent Schrift.
 *
 * DREI KLASSEN UND NICHT EINE, je Tafel eine -- aber sie fallen zusammen. Sie
 * haengen seit dem 28.08.2026 an der STUFE, nicht mehr daran, ob die Huelle
 * diese eine Tafel gerade zeichnet.
 *
 * Hier stand das Gegenteil, und es war der Fehler, den der Auftraggeber
 * gesehen hat: "sobald ein Bogen ueber dem HUD liegt, meldet js/glas.js
 * nichts, die Klasse kommt ab, und das Web-HUD kehrt zurueck -- gedaempft
 * unter dem Abdunkler des Bogens, so wie es dort hingehoert." Genau das
 * gehoert dort NICHT hin. Es ist ein Materialwechsel mitten im Bild, und
 * gemessen kostet er die Haelfte der Helligkeit
 * (docs/messung/glas/ursache-2026-08-28). Gedaempftes Glas liest jeder als
 * Fehler.
 *
 * Sie bleiben trotzdem drei: .offline haengt an der ersten und holt sich seine
 * Sichtbarkeit ueber genau sie zurueck, und die Huelle kann eine Tafel
 * zeichnen, waehrend eine andere ausserhalb des Bildes liegt.
 *
 * WAS AUSDRUECKLICH BLEIBT: alles, was hier NICHT steht. Das Web-HUD
 * verschwindet nicht, es tritt zurueck; im Browser gibt es kein UIGlassEffect,
 * und dort greifen diese Klassen nie.
 */
:root.hud-nativ-kopf .oben,
:root.hud-nativ-lage .status,
:root.hud-nativ-melden .melden {
  visibility: hidden;
}

/*
 * DAS HUD WIRD GERAEUMT, NICHT GEDAEMPFT.
 *
 * DER BEFUND, DER DAZU GEFUEHRT HAT. Am 28.08.2026 im Simulator gemessen
 * (docs/messung/glas/ursache-2026-08-28, zwei Abzuege mit offenem Mehr-Bogen):
 * die Kopftafel verlor beim Oeffnen der Einstellungen die HAELFTE ihrer
 * Helligkeit -- RGB(63,61,67) mit ausgeschaltetem Umschaltpfad gegen
 * RGB(30,33,38) im Ist-Zustand, bei RGB(62,67,72) ohne Bogen. Der
 * Auftraggeber hat es "es wird matt" genannt, und das war woertlich richtig.
 *
 * Die Ursache war kein Wert, sondern ein MATERIALWECHSEL: js/glas.js meldete
 * der Huelle "hier liegt nichts frei", die Klassen hud-nativ-* kamen ab, und
 * der matte Web-Nachbau kehrte unter dem Abdunkler des Bogens zurueck. Genau
 * dieser Tausch ist jetzt weg (siehe `hudRaeumen` in js/glas.js).
 *
 * ES GIBT ZWEI ZUSTAENDE UND KEINEN DRITTEN:
 *
 *   Der Bogen reicht in die Kopftafel     das HUD geht ganz weg
 *   Der Bogen reicht nicht so weit        das HUD bleibt VOLL stehen
 *
 * Ein halbhoher Bogen bekommt also kein blasses HUD, sondern das normale.
 * Was er verdeckt, verdeckt er; was daneben steht, steht ganz da.
 *
 * opacity und transform machen die Zone kurzzeitig zum Backdrop Root, das
 * Glas darunter filtert in diesen 200 ms also eine leere Flaeche. Das ist
 * gerechnet und nicht uebersehen: die Zone verschwindet dabei, und der Bogen
 * faehrt gleichzeitig darueber. Im STEHENDEN Zustand gilt keine der beiden
 * Eigenschaften -- die Regel greift nur unter .hud-geraeumt.
 *
 * `inert` setzt js/glas.js erst NACH der Blende, und beim Zurueckkommen nimmt
 * es das Attribut VOR der Blende wieder ab: `inert` auf dem Element, das
 * gerade den Fokus haelt, wirft die Vorlesestimme auf <body>.
 */
:root.hud-geraeumt .oben,
:root.hud-geraeumt .unten {
  opacity: 0;
  transform: translateY(var(--hud-raeum-weg));
}

/* Wer Bewegung abbestellt hat, bekommt dasselbe Ergebnis ohne den Weg: das
   HUD blendet aus, es faehrt nur nicht mehr dabei. Ganz ohne Uebergang
   verschwaende es hart, und ein hartes Verschwinden liest sich als Fehler. */
@media (prefers-reduced-motion: reduce) {
  :root.hud-geraeumt .oben,
  :root.hud-geraeumt .unten {
    transform: none;
  }
}

/*
 * Das Stoerband ist die eine Ausnahme, und zwar aus der Geometrie heraus: es
 * liegt IM Markup der oberen Tafel, sitzt aber mit `top: calc(100% + 8px)`
 * ausserhalb von deren Glas. Es bleibt Web, also muss es sich die Sichtbarkeit
 * zurueckholen -- `visibility` vererbt sich, ein Kind darf sie ueberschreiben.
 * Ohne diese Regel koennte die App "Offline" nicht mehr anzeigen, und die
 * Bedienlage lautet laut Auftrag "oft offline".
 */
:root.hud-nativ-kopf .offline {
  visibility: visible;
}

/*
 * DIE BARRIERE-BRUECKE UEBER DIE NATIVE HUELLE.
 *
 * prefers-reduced-transparency gibt es in WebKit nicht (Bug 175497, offen).
 * Die App weiss die Antwort trotzdem: ios/Kontrollpunkt/WebAnsicht.swift liest
 * UIAccessibility.isReduceTransparencyEnabled, setzt diese Klasse per
 * evaluateJavaScript und hoert auf reduceTransparencyStatusDidChangeNotification.
 *
 * Der Rueckfall ist DECKEND, nicht "frostig" wie bei Apple. Grund ist die
 * Rechnung: eine deckende --flaeche hat genau einen Untergrund, und der Text
 * darauf traegt 17,05:1 hell und 17,28:1 dunkel -- eine Zahl statt zwanzig.
 * Ein staerker getruebtes Glas muesste dagegen wieder gegen alle zehn
 * Kartenflaechen und alle zehn Linienfarben nachgerechnet werden, und wer die
 * Durchsicht abschaltet, will sie ohnehin nicht in schwaecherer Form zurueck.
 *
 * Die Regeln stehen mit :root davor, damit sie die @supports-Bloecke oben
 * schlagen (0,2,0 gegen 0,1,0), und sie fassen alle drei Lagen an: Flaeche,
 * Band und beide Verlaeufe.
 */
:root.weniger-durchsicht .oben,
:root.weniger-durchsicht .status,
:root.weniger-durchsicht .rund-frei {
  background: var(--flaeche);
  box-shadow: var(--schatten-auflage);
}

/* Die beiden Melder gehen denselben Weg. Sie tragen seit dem 28.08.2026 Glas,
   also gilt fuer sie dieselbe Barriere: wer Transparenz reduziert, bekommt
   eine deckende Flaeche, und der eigene Schwebeschatten bleibt. */
:root.weniger-durchsicht .toast,
:root.weniger-durchsicht .offline {
  background: var(--flaeche);
  box-shadow: var(--schatten-schwebend);
}

:root.weniger-durchsicht .oben::before,
:root.weniger-durchsicht .status::before,
:root.weniger-durchsicht .rund-frei::before,
:root.weniger-durchsicht .toast::before,
:root.weniger-durchsicht .offline::before,
:root.weniger-durchsicht .oben::after,
:root.weniger-durchsicht .status::after,
:root.weniger-durchsicht .rund-frei::after {
  content: none;
}

/*
 * OHNE BEWEGUNG. Der Glanzwechsel und das Leuchten sind die einzigen zwei
 * Dinge dieser Datei, die auf eine Geste hin ueber die Flaeche laufen. Wer
 * prefers-reduced-motion setzt, bekommt beide nicht: js/karte.js schreibt
 * --glanz-winkel dann nicht mehr fort, js/app.js meldet den Tupfer gar nicht
 * erst an, und diese Regel haelt den Rest fest, falls doch jemand schreibt.
 * Der Glanz als STEHENDER Verlauf bleibt: er ist Material, keine Bewegung.
 */
@media (prefers-reduced-motion: reduce) {
  /* Mit :root davor, nicht ohne. Gemessen: ohne den Vorsatz stehen beide Regeln
     auf Spezifitaet 0,1,0, und .status weiter unten in dieser Datei gewinnt --
     transitionProperty meldete dann weiter --tupfer-staerke. Der Schaden waere
     klein gewesen, weil js/app.js den Tupfer ohnehin gar nicht erst anmeldet,
     aber eine Regel, die nichts tut, behauptet etwas Falsches. */
  :root .oben,
  :root .status,
  :root .rund-frei {
    --tupfer-staerke: 0;
    transition: none;
  }
}

/*
 * Was kommt und geht, schwebt sichtbar hoeher. Der weite Schatten ist hier
 * keine Zierde, sondern die Aussage "das liegt nur kurz hier".
 */
/*
 * Was kommt und geht, schwebt sichtbar hoeher. Auch hier ist die Kontur weg:
 * "der Rahmen muss weg, ueberall" gilt fuer diese beiden genauso, und eine
 * Tafel ohne Kante neben einem Band mit Kante waere die schlechteste beider
 * Welten. Beide sind VOLL DECKEND, nicht Glas, also traegt ihr Text hier
 * 17,05:1 hell und 17,28:1 dunkel; erkennbar sind sie ausserdem am weiten
 * Schatten, den sonst nichts in dieser App traegt.
 * Das Stoerband behaelt zusaetzlich seine rote Kante links: die ist kein
 * Rahmen, sondern eine Meldung, und --warn haelt gegen jede Kartenflaeche
 * mindestens 3,16:1 (siehe .melden).
 */
/*
 * DIE BEIDEN SCHWEBENDEN MELDER, SEIT DEM 28.08.2026 AUS GLAS.
 *
 * Diese Regel ist der RUECKFALL. Sie gilt dort, wo backdrop-filter fehlt, und
 * dann ist die Flaeche voll deckend (17,05:1 hell, 17,28:1 dunkel). Wo Glas
 * geht, uebernimmt der @supports-Block weiter unten.
 *
 * WARUM DAS MATERIAL GEWECHSELT HAT. Das Materialgesetz fragte bis hierher
 * nach dem ORT ("auf der Karte Glas, statt der Karte deckend") und fuehrte
 * diese beiden trotzdem deckend -- obwohl sie auf der Karte liegen. Die neue
 * Achse fragt nach der HANDLUNG: laeuft darunter eine weiter? Bei Quittung
 * und Stoerband ja, die Karte bleibt bedienbar. Also Glas. Der Auftraggeber
 * hat es ausdruecklich so verlangt ("die ganzen Mitteilungen die kommen, zum
 * Beispiel wenn ich meinen Standort zentriere, auch gerne Liquid Glass").
 * Vollstaendig in docs/29-materialgesetz.md.
 *
 * DIE SCHRIFT WECHSELT MIT. Auf Glas ueber dem schlechtesten Kartenuntergrund
 * traegt die leise Tinte nur 5,30:1 hell und liegt damit unter der
 * Untergrenze dieses Auftrags (5,78:1). Volle Tinte traegt dort 13,36:1 hell
 * und 9,27:1 dunkel (docs/messung/glas/kontrast-28-08.mjs).
 */
.offline,
.toast {
  background: var(--flaeche);
  border: var(--kontur-tafel);
  box-shadow: var(--schatten-schwebend);
  color: var(--schrift);
}

.oben {
  /* Das Leuchten unter dem Finger zieht auf und wieder ab. 240 ms ist
     --d-basis, also die Dauer, die in dieser App alles hat, was erscheint;
     unter 300 ms und mit der Signaturkurve. Uebergeblendet wird die ZAHL
     --tupfer-staerke, nicht opacity und nicht der Weichzeichner-Radius: den zu
     animieren kostet bei einer WebGL-Karte darunter jeden Frame neu.
     Dazu die beiden Eigenschaften des Raeumens; sie stehen HIER und nicht nur
     in der Raeum-Regel, weil eine Ueberblendung beide Zustaende braucht. Die
     Werte selbst stehen nicht hier: ohne .hud-geraeumt gelten die
     Anfangswerte, und die sind opacity 1 und transform none. */
  transition:
    --tupfer-staerke var(--d-basis) var(--ease-out),
    opacity var(--hud-raeum-dauer) var(--ease-out),
    transform var(--hud-raeum-dauer) var(--ease-out);
  position: fixed;
  top: calc(var(--rand-aussen) + env(safe-area-inset-top));
  left: var(--rand-aussen);
  right: var(--rand-aussen);
  z-index: 10;
  border-radius: var(--r-tafel);
}

/*
 * Geclippt wird die BANDZEILE, nicht die ganze Tafel.
 *
 * Das Clipping muss sein: ohne es scrollen die Liniensignets ueber die
 * gerundete Tafelecke hinaus. Es sass bis hierher auf .oben, und dort hat es
 * das Stoerband mitgeschnitten: .offline haengt mit position:absolute 8 px
 * UNTER der Tafel, .oben ist position:fixed und damit sein Bezugsrahmen, also
 * lag es ausserhalb des Clippings. Nachgemessen bei 393 x 852: das Band wurde
 * bei y 75 bis 135 aufgebaut, die Tafel endet bei y 68, und ein
 * elementFromPoint auf seine Mitte traf die Karte statt das Band. Die App
 * konnte "Offline" also nicht anzeigen, obwohl die Bedienlage laut Auftrag
 * "oft offline" lautet. Aelter als der Umbau der Zonen, hier behoben.
 *
 * Die untere Rundung faellt weg, sobald die Legende darunter erscheint, sonst
 * schneidet die Ecke in die Farbfelder der Skala.
 *
 * Ein Scroller und ein einzelner Knopf, deshalb Flex ohne linken Innenabstand:
 * mit einem waere das gescrollte Band vor der Tafelkante zu Ende und der
 * Auslauf stuende in der Luft.
 */
.bandzeile {
  display: flex;
  align-items: center;
  padding-right: var(--band-rechts);
  border-radius: var(--r-tafel);
  overflow: clip;
}

.oben:has(#knopf-ansicht[aria-pressed="true"]) .bandzeile {
  border-radius: var(--r-tafel) var(--r-tafel) 0 0;
}

/*
 * Der Mehr-Knopf steht ausserhalb des Scrollers. Ohne den Abstand scrollen die
 * Liniensignets bis unter ihn; die Trennlinie sitzt an der Bandkante, nicht am
 * Knopf, damit sie ueber die volle Zeilenhoehe laeuft und als Spaltengrenze
 * liest statt als Knopfrahmen. Sie traegt --linie, weil sie nur gliedert und
 * nichts Bedienbares umschliesst; sichtbar wird die Grenze ohnehin schon durch
 * den Auslauf des Bandes (data-rand).
 */
/*
 * Die Fuge war 6 px und ist 10.
 *
 * Gemessen lagen zwischen der Schnittkante des Bandes und dem Mehr-Knopf genau
 * 6,0 px, bei jeder Breite und in jedem Scrollzustand. Am Rand des Scrollers
 * steht dort fast immer eine angeschnittene Kapsel: bei 393 px sind von der
 * S1-Kapsel noch 14 px sichtbar. Der weiche Auslauf laesst sie verblassen,
 * aber `mask-image` faerbt nur, es fasst das Klickziel nicht an; der Splitter
 * bleibt also antippbar. Ein Fehlgriff dorthin oeffnete die Einstellungen,
 * statt eine Linie zu filtern. Die 4 px kommen aus dem Scrollweg des Bandes,
 * nicht aus der Karte.
 */
.bandzeile > .knopf-feld {
  margin-left: var(--band-knopf-fuge);
}

/* ---------- Freistehende Kartenwerkzeuge ---------- */

/*
 * DIE BEDIENZEILE IST WEG, an ihrer Stelle steht eine Saeule aus zwei Kreisen.
 * Die Herleitung der Lage und die gerechneten Daumenabstaende stehen in
 * index.html ueber .unten; hier stehen nur die Masse und die Kontrastwerte.
 *
 * Die Saeule ist ein durchlaessiger Streifen: nur die beiden Kreise fangen
 * Tipps (pointer-events unten bei .unten), alles daneben gehoert der Karte.
 * Genau das war mit der 52 px hohen Zeile ueber die volle Breite nicht moeglich.
 *
 * Der zusaetzliche Abstand nach unten ist gerechnet: die Reihenfuge betraegt
 * 12 px, und der Daumen-Lens hatte 13 pt (2,3 mm) zwischen Zoom-raus und dem
 * Meldeknopf als Falle bemaengelt. Ein Fehlgriff nach unten reisst hier den
 * Meldebogen auf, also einen grossen hellen Bildschirm in einer Bahn. Mit
 * 12 px extra sind es 24 px (4,3 mm), und das ist die weiteste Fuge der App.
 */
.werkzeugspalte {
  align-self: flex-end;
  display: flex;
  flex-direction: column;
  /* 10 px zwischen zwei Zielen, die verschiedene Dinge tun. Die 8-px-Untergrenze
     dieser Datei plus 2 px, weil der obere Kreis einen Bogen aufreisst und der
     untere nur die Karte zentriert. */
  gap: 10px;
  margin-bottom: 12px;
}

/*
 * "Eine ganz kleine Lupe" war die Ansage, und sie ist woertlich umgesetzt,
 * ohne das Ziel zu verkleinern: der KREIS bleibt bei 44 px, weil das die
 * Fingernorm ist (WCAG 2.5.5/2.5.8) und weil er in einer fahrenden Bahn
 * getroffen werden muss; klein wird das SYMBOL, 18 statt 20 px. Das ist genau
 * der Hebel, den die Vorbilder benutzen: viel Luft um ein kleines Zeichen.
 *
 * Kreis und nicht Feld: ein Symbolknopf ohne Wort hat keine Leserichtung. Die
 * Tafelform bleibt dem Meldeknopf vorbehalten, der eine traegt.
 *
 * Er steht auf Glas und ohne Kontur, wie die beiden Tafeln. Was ihn
 * kennzeichnet, ist sein Symbol: --schrift-2 auf dem Glas traegt gemessen
 * 5,30:1 hell (schlechtester Untergrund S3) und 4,78:1 dunkel (schlechtester
 * Untergrund die weisse Station), gefordert sind 3:1 nach WCAG 1.4.11.
 */
.rund-frei {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  pointer-events: auto;
  transition:
    transform var(--d-druck) var(--ease-out),
    box-shadow var(--d-druck) var(--ease-out),
    /* Siehe .oben: die Zahl wird ueberblendet, nicht die Deckkraft. */
    --tupfer-staerke var(--d-basis) var(--ease-out);
}

.rund-frei:active {
  transform: scale(0.94);
  box-shadow: var(--schatten-druck);
}

.rund-frei svg {
  width: 18px;
  height: 18px;
  stroke: var(--schrift-2);
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Knoepfe in der Tafel tragen keine eigene Kontur: sie liegen auf einer
   Flaeche, die schon eine hat. Zwei Kanten nebeneinander waeren die doppelte
   Behauptung. */
.knopf-feld {
  width: 44px;
  height: 44px;
  /* Kreis, nicht Feld: ein Symbolknopf ohne Wort hat keine Leserichtung, und
     die Vorbilder machen daraus ausnahmslos einen Kreis. Die Tafelform bleibt
     dem Meldeknopf vorbehalten, der ein Wort traegt. */
  border-radius: 50%;
  display: grid;
  place-items: center;
  transition: transform var(--d-druck) var(--ease-out);
}

.knopf-feld:active {
  transform: scale(0.92);
  background: var(--flaeche-3);
}

.knopf-feld svg {
  width: var(--symbol-band);
  height: var(--symbol-band);
  stroke: var(--schrift-2);
  fill: none;
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* ---------- Liniensignet ---------- */

/*
 * Das Signet muss auf Bogenflaeche, Kartengrund und Linienband gleich gut
 * sitzen. Getragen wird das vom Innensaum, nicht von einem Rahmen: ein Rahmen
 * frisst Breite und schoebe die Schrift aus der Mitte. 2 px Radius, weil das
 * HVV-Signet diese Form hat.
 */
.linie {
  display: inline-grid;
  place-items: center;
  min-width: var(--signet-min);
  height: var(--signet-h);
  padding: 0 var(--signet-innen);
  border-radius: var(--r-signet);
  font-weight: var(--t-band-signet-gewicht);
  font-size: var(--t-band-signet-groesse);
  line-height: var(--t-band-signet-zeilenhoehe);
  letter-spacing: 0;
  font-variant-numeric: tabular-nums;
  flex: none;
  box-shadow: inset 0 0 0 1px var(--kapsel-saum);
}

/* 44 x 30 statt 52 x 40. Der hvv-Aushang und die hvv-App fuehren das Signet bei
   rund 28 px Hoehe; 40 px mit 19 px Schrift war eine Kachelbeschriftung, kein
   Signet. Seit die Kachel keinen Rahmen mehr hat, ist dieses Signet das
   einzige Sichtbare der Auswahl; das Klickziel darum bleibt bei 44 px Hoehe
   (.linien-kachel) und faellt nicht auf die 30 des Signets. */
.linie.gross {
  min-width: 44px;
  height: 30px;
  font-size: 0.96875rem;
  letter-spacing: -0.02em;
  box-shadow: inset 0 0 0 1.5px var(--kapsel-saum);
}

/* ---------- Linienband, zugleich Legende ---------- */

/*
 * Auch hier 8 statt 6 px. Zwischen zwei Liniensignets lagen gemessen 6,0 px,
 * und ein Fehlgriff filtert die Karte auf die falsche Linie. Das Band ist ein
 * Scroller, die zusaetzlichen 2 px je Fuge (22 px insgesamt) kosten deshalb
 * keine Breite, sondern nur Scrollweg.
 */
.filter {
  height: var(--band-h);
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: var(--band-fuge);
  padding: 0 var(--band-innen);
  border-right: 1px solid var(--linie);
  overflow-x: auto;
  scrollbar-width: none;
  overflow-y: clip;
}

.filter::-webkit-scrollbar {
  display: none;
}

/*
 * Auslauf an den Seiten, an denen noch etwas liegt. js/app.js setzt data-rand.
 * 28 px sind die Strecke, ueber die eine 44 px hohe Kapsel erkennbar
 * verschwindet, ohne dass die davor stehende schon blass wird.
 */
.filter[data-rand="rechts"] {
  mask-image: linear-gradient(to right, #000 calc(100% - var(--band-auslauf)), transparent 100%);
}

.filter[data-rand="links"] {
  mask-image: linear-gradient(to right, transparent 0, #000 var(--band-auslauf));
}

.filter[data-rand="beides"] {
  mask-image: linear-gradient(
    to right,
    transparent 0,
    #000 var(--band-auslauf),
    #000 calc(100% - var(--band-auslauf)),
    transparent 100%
  );
}

.filter-linien {
  display: flex;
  gap: var(--band-fuge);
}

/*
 * Filter sind der eine Ort, an dem die Kapselform etwas bedeutet: sie sind
 * Chips, keine Knoepfe einer Tafel. Ueberall sonst waere die Pille genau das
 * Slop-Merkmal, das er selbst aufgeschrieben hat.
 */
/*
 * AUCH HIER IST DER RAHMEN WEG. "Der Rahmen muss weg, ueberall" hat der
 * Auftraggeber gesagt und dabei ausdruecklich "die Bahnverbindung oben"
 * genannt; das waren diese Kapseln, denn die 1-px-Kante der Tafel darum ist
 * die zweite Sache und steht bei .oben.
 *
 * Sie trugen `--kontur-feld` auf `--flaeche`, und beides war seit dem Umstieg
 * auf Glas falsch geworden: Die Fuellung ist DECKEND und liegt auf einer
 * Glastafel, also lag Deckend auf Durchscheinend, was gemessen 1,05:1
 * ausmacht. Sichtbar war die Kapsel damit nur noch durch ihre Kante, und die
 * Kante war das, was stoerte.
 *
 * Jetzt tragen sie nichts: keine Fuellung, keine Kante. Was sie kennzeichnet,
 * ist ihr Wort (--schrift auf Glas, 14,95:1 dunkel und 18,47:1 hell) und beim
 * Ansichtswechsel zusaetzlich sein Ebenensymbol. Der GEWAEHLTE Zustand behaelt
 * seine Tintenfuellung, und der traegt die Unterscheidung jetzt allein: Tinte
 * gegen Glas sind 14,95:1 dunkel und 18,47:1 hell, also weit ueber den 3:1,
 * die WCAG 1.4.11 fuer die Kennzeichnung eines Zustands verlangt.
 *
 * Das Klickziel bleibt bei 44 px, es wird nur unsichtbar. Dieselbe Bewegung
 * hat .filter-linie schon vorher gemacht, siehe die Begruendung dort.
 */
.filter-knopf,
.filter-linie,
.ansicht-knopf {
  height: var(--ziel-h);
  min-width: var(--ziel-h);
  padding: 0 var(--kapsel-innen);
  border-radius: 999px;
  font-weight: var(--t-band-kapsel-gewicht);
  font-size: var(--t-band-kapsel-groesse);
  line-height: var(--t-band-kapsel-zeilenhoehe);
  display: grid;
  place-items: center;
  flex: none;
  transition: transform var(--d-druck) var(--ease-out);
}

.filter-knopf:active,
.filter-linie:active,
.ansicht-knopf:active {
  transform: scale(0.94);
}

/* Gewaehlt heisst umgekehrt: Tinte als Flaeche, Grund als Schrift. Das ist
   seit dem Wegfall der Kante die EINZIGE Unterscheidung, und sie traegt sie
   auch allein: 14,95:1 dunkel und 18,47:1 hell gegen die Glastafel. */
.filter-knopf[aria-pressed="true"],
.ansicht-knopf[aria-pressed="true"] {
  background: var(--schrift);
  color: var(--grund);
}

/*
 * Die Linienfelder im Band sind Liniensignets und tragen deshalb die Form des
 * Signets, nicht die Pillenform der Chips daneben. Sonst waere dieselbe U3 im
 * Band ein Kreis und im Bogen ein Rechteck. Der Unterschied ist damit sogar
 * bedeutungstragend: Pille steuert, Signet ist eine Linie.
 */
/*
 * Der Filterknopf ist jetzt der TRAEGER eines Signets, nicht selbst eines. Er
 * bleibt 44 px hoch, weil das die Fingernorm ist, wird aber durchsichtig; das
 * farbige Signet darin ist 26 px hoch wie im gedruckten Aushang. Vorher musste
 * das Signet so gross sein wie das Klickziel, und zehn 44-px-Farbfelder
 * nebeneinander sind eine Farbwand, kein Netzband.
 */
.filter-linie {
  background: none;
  border-color: transparent;
  padding: 0 var(--linienkapsel-innen);
  min-width: var(--ziel-h);
}

/* Der Auswahlring sitzt am Signet, nicht am durchsichtigen Traeger. */
.filter-linie[aria-pressed="true"] .linie {
  box-shadow:
    inset 0 0 0 1px var(--kapsel-saum),
    0 0 0 var(--auswahlring-luft) var(--flaeche),
    0 0 0 calc(var(--auswahlring-luft) + var(--auswahlring-b)) var(--schrift);
}

/* Die Linienfelder bekommen ihre Farbe inline von js/app.js. Der Auswahlring
   liegt deshalb aussen und in Tinte: ein farbiger Ring auf farbigem Feld waere
   bei U3-Gelb unsichtbar. */


/* Nicht gewaehlte Linien treten zurueck, statt zu verschwinden: das Band ist
   auch dann noch die Legende. */
.filter[data-gefiltert] .filter-linie[aria-pressed="false"] {
  opacity: var(--kapsel-blass);
}

/*
 * Der Ansichtswechsel ist kein Filter, er schaltet die Bedeutung aller Farben
 * um. Er ist deshalb eine eigene kleine Tafel mit Symbol und Wort, nicht die
 * elfte Kapsel: Die Kapselform steuert, die Tafelform schaltet um. Abgesetzt
 * ist er durch Form, Symbol und Abstand, nicht durch eine eigene Farbe.
 */
.ansicht-knopf {
  border-radius: var(--r-feld);
  margin-right: var(--ansicht-rechts);
  padding: 0 var(--ansicht-innen);
  font-size: var(--t-band-ansicht-groesse);
  display: flex;
  align-items: center;
  gap: var(--ansicht-fuge);
}

/*
 * Das Ebenensymbol als Maske, nicht als Element: js/app.js schreibt in diesen
 * Knopf textContent und wuerde jedes Kindelement wegwerfen. Die Maske traegt
 * keine Farbe, sie schneidet nur eine Form aus; gefaerbt wird ueber
 * background-color, also weiter aus dem Token-Block. Deshalb kippt das Symbol
 * im gedrueckten Zustand von selbst mit der Schrift auf die Grundfarbe.
 */
.ansicht-knopf::before {
  content: "";
  width: var(--ansicht-symbol);
  height: var(--ansicht-symbol);
  flex: none;
  background-color: currentColor;
  -webkit-mask: var(--symbol-ebenen) center / contain no-repeat;
  mask: var(--symbol-ebenen) center / contain no-repeat;
}

/* ---------- Legende der Meldungsansicht ---------- */

/*
 * Erscheint nur, wenn der Ansichtswechsel gedrueckt ist. js/app.js setzt dafuer
 * aria-pressed, mehr braucht es nicht: :has() liest den Zustand im Band und
 * schaltet die Zeile darunter.
 *
 * WARUM SIE NICHT DAUERHAFT STEHT, obwohl das Vorbild sie dauerhaft zeigt:
 * Das Vorbild hat nur eine Ansicht, wir haben zwei. In der Linienansicht
 * bedeutet Farbe die Linie, und die Legende dafuer ist das Linienband direkt
 * darueber, das dauerhaft steht. Eine Meldungsskala, die neben Linienfarben
 * haengt, behauptet eine Bedeutung, die die Karte gerade nicht zeigt. Dazu die
 * Messung: dauerhaft eingeblendet kostet die Zeile 38 px Tafelhoehe, der
 * Kartenanteil bei 320 x 568 faellt damit von 48,6 auf 42,0 Prozent. Es ist
 * also immer eine Legende sichtbar, aber immer die der laufenden Ansicht.
 *
 * Gruen heisst hier "keine Meldung". Nicht "sicher", nicht "frei". Die App
 * kennt nur Meldungen, nie deren Abwesenheit.
 */
.legende {
  display: none;
  margin: 0;
  padding: 0 var(--tafel-innen) var(--legende-unten);
  flex-direction: column;
  gap: var(--legende-fuge);
}

.oben:has(#knopf-ansicht[aria-pressed="true"]) .legende {
  display: flex;
}

/*
 * Die Skala: vier Stufen ohne Fuge, damit sie als ein Verlauf von wenig nach
 * viel liest und nicht als vier Marken. Die Kante umschliesst die ganze Skala
 * statt jedes Feld; innen stossen die Toene aneinander, und dafuer sind ihre
 * Paarkontraste in js/kartenstil.js gerechnet.
 */
.legende-skala {
  display: flex;
  height: var(--legende-h);
  border-radius: var(--legende-r);
  border: 1px solid var(--kontur-feld);
  overflow: clip;
}

/* Beschriftet sind nur die Enden. Eine Skala erklaert sich ueber ihre Enden,
   und "mehrfach gemeldet" gegen "frisch mehrfach gemeldet" ist als Wortpaar in
   der Mitte ohnehin nicht auf einen Blick zu unterscheiden. */
/* Steht in .oben, also auf Glas: --schrift-3 faellt dort auf 3,91:1. Siehe
   die Rechnung bei .lizenz. */
.legende-enden {
  display: flex;
  justify-content: space-between;
  gap: var(--legende-enden-fuge);
  font-size: var(--t-legende-enden-groesse);
  font-weight: var(--t-legende-enden-gewicht);
  letter-spacing: var(--t-legende-enden-sperrung);
  line-height: var(--t-legende-enden-zeilenhoehe);
  color: var(--schrift-2);
}

/*
 * Die vier Toene sind aus RISIKO_STUFEN in js/kartenstil.js gespiegelt. Das
 * ist bewusst ein Duplikat und die schwaechste Stelle dieser Datei: ein Wert,
 * der an zwei Stellen steht, sind zwei Werte. Dauerhaft gehoert die Legende
 * aus dem Kartenmodul befuellt, das die Toene ohnehin fuehrt.
 *
 * Und der Grund, warum die Skala eine Kontur traegt: die Toene sind gegen den
 * Kartengrund gerechnet, nicht gegen eine Tafel. Nachgemessen gegen die
 * Tafelflaeche faellt "keine Meldung" im Dunkelmodus auf 2,82:1 und damit
 * unter die Schwelle fuer grafische Objekte. Die Kante traegt die
 * Erkennbarkeit, nicht die Fuellung. Genau derselbe Fall wie bei der Tafel
 * ueber der Karte.
 */
.legende-feld {
  flex: 1;
}

.legende-feld[data-stufe="keine"] {
  background: var(--risiko-keine);
}

.legende-feld[data-stufe="gering"] {
  background: var(--risiko-gering);
}

.legende-feld[data-stufe="erhoeht"] {
  background: var(--risiko-erhoeht);
}

.legende-feld[data-stufe="hoch"] {
  background: var(--risiko-hoch);
}

/* ---------- Stoerband ---------- */

.offline {
  position: absolute;
  top: calc(100% + 8px);
  left: 0;
  right: 0;
  padding: 11px 14px;
  border-radius: var(--r-feld);
  /* Linke Kante in Signalfarbe: das Band meldet eine Stoerung. Hier darf die
     Farbe stehen, weil hier wirklich etwas gemeldet wird. */
  border-left: 5px solid var(--warn);
  font-size: 0.8125rem;
  font-weight: 500;
  line-height: 1.4;
  z-index: 12;
}

/*
 * DIE KURZE MARKE FUER EINEN DAUERZUSTAND.
 *
 * Das Stoerband oben ist fuer eine STOERUNG gebaut: fuer etwas, das kommt und
 * geht, das man liest und das danach verschwindet. Ein Zustand, der sich in
 * dieser Sitzung nicht mehr aendert, ist keine Stoerung, und in der vollen
 * Form kostete er gemessen 369 x 76 pt = 28.036 pt², also 8,4 Prozent des
 * Schirms -- in jedem Bild, dauerhaft, direkt unter der Kopftafel
 * (docs/messung/diagnose-2026-08-18, Befund 3).
 *
 * Was bleibt: die Aussage, die Signalkante und der Platz unter der Kopftafel.
 * Was geht: die volle Breite, der Fliesstext und zwei Drittel der Hoehe. Der
 * ganze Satz steht weiter in der Infoseite und in der Quittung nach dem
 * Absenden, also dort, wo die Einschraenkung wirklich etwas kostet.
 *
 * `right: auto` statt `width: fit-content`: der Kasten haengt an drei Kanten
 * (left, right, top), und nur das Loesen der rechten laesst ihn auf seinen
 * Inhalt schrumpfen, ohne die Verankerung links aufzugeben. `max-width` haelt
 * ihn bei 320 px und 200 Prozent Systemschrift in der Tafelbreite; dann bricht
 * er eben zweizeilig um, statt ueber den Rand zu laufen.
 */
/*
 * Die Schrift wird kleiner, aber NICHT leiser. Diese Regel stand hier schon,
 * als das Band deckend war; auf Glas gilt sie erst recht. Seit dem 28.08.2026
 * traegt das Band volle Tinte statt --schrift-2: auf Glas ueber dem
 * schlechtesten Kartenuntergrund kaeme die leise Stufe auf 5,30:1 hell und
 * laege damit unter der Untergrenze dieses Auftrags (5,78:1). Volle Tinte
 * traegt dort 13,36:1 hell und 9,27:1 dunkel. Zurueckhaltend bleibt das Band
 * ueber seine GROESSE, und eine Aussage, die man uebersehen soll, haette man
 * auch weglassen koennen.
 */
.offline[data-art="lokal"] {
  right: auto;
  max-width: 100%;
  padding: 5px 11px;
  border-left-width: 3px;
  font-size: 0.75rem;
}

/* ---------- Zoom: entfallen ---------- */

/*
 * HIER STANDEN .werkzeuge UND .rund, die Zoomkapsel mit Plus und Minus.
 * Sie sind ersatzlos raus, auf ausdrueckliche und zweimal wiederholte Ansage
 * des Auftraggebers: "Plus und Minus kannst du komplett rausnehmen, Zoom tut
 * man mit Fingern."
 *
 * DAS KIPPT EINE BEGRUENDUNG, DIE HIER STAND, und die Zahl dazu wird nicht
 * unterschlagen: die Felder waren der garantierte Weg nach WCAG 2.1
 * Erfolgskriterium 2.5.1 (Stufe A, Zeigergesten). Was bleibt und was fehlt:
 *
 *   Hineinzoomen  Doppeltipp auf die Karte. Ein einzelner Zeiger, kein Pfad,
 *                 ein Finger. Erfuellt 2.5.1 aus eigener Kraft.
 *   Hinauszoomen  nur noch Aufziehen (mehrfingrig) oder Doppeltipp-und-Ziehen
 *                 (pfadbasiert). BEIDE sind genau das, was 2.5.1 verbietet,
 *                 wenn kein einfacher Weg danebensteht. Diese Luecke ist offen.
 *   Tastatur      Plus, Minus und die Pfeiltasten bedienen die Karte weiter
 *                 (js/app.js). Das deckt WCAG 2.1.1 ab, aber NICHT 2.5.1:
 *                 eine Tastatur ist kein Zeiger.
 *
 * Die Luecke steht damit im Bericht und in docs/15-gestaltungssprache.md,
 * Abschnitt 13, statt still verschwunden zu sein. Die Karte selbst haelt
 * minZoom 9,5 und maxZoom 17 (js/karte.js), der Spielraum ist also klein;
 * niemand kann sich so weit verzoomen, dass er die Stadt verliert.
 */

/* ---------- Untere Zone ---------- */

/*
 * Eine Saeule und eine Zeile. Oben rechts die zwei freien Kreise, unten die
 * Lagetafel neben dem Meldeknopf. Die Herleitung der Lage und die gerechneten
 * Daumenabstaende stehen in index.html ueber .unten.
 *
 * Der Behaelter bleibt durchlaessig, und das ist hier wichtiger als vorher:
 * ueber der Lagetafel liegt jetzt ein 100 px hoher Streifen, in dem nur zwei
 * 44-px-Kreise ganz rechts fangen. Alles andere darin gehoert der Karte.
 */
.unten {
  /* Siehe .oben: die Ueberblendung des Raeumens braucht beide Zustaende. */
  transition:
    opacity var(--hud-raeum-dauer) var(--ease-out),
    transform var(--hud-raeum-dauer) var(--ease-out);
  position: fixed;
  left: var(--rand-aussen);
  right: var(--rand-aussen);
  bottom: calc(var(--rand-aussen) + env(safe-area-inset-bottom));
  z-index: 10;
  display: flex;
  flex-direction: column;
  gap: var(--reihe-fuge);
  pointer-events: none;
  /*
   * DIE UNTERE ZONE HAT EINEN DECKEL, WEIL DIE KARTE DAS PRODUKT IST.
   *
   * Ohne ihn wuchs sie mit der Systemschrift ins Unbegrenzte. Gemessen bei
   * 320 x 568 und 200 Prozent, mit Naeherungswarnung und Meldung: die Tafel war
   * 423 px hoch, die Werkzeugsaeule begann bei y = 21 und lag damit HINTER der
   * oberen Tafel, die erst bei y = 66 endet. Zwischen beiden Zonen blieben vier
   * Pixel Karte. Eine Vollbildkarte, auf der keine Karte mehr zu sehen ist,
   * hat kein Problem mit der Schriftgroesse, sondern mit sich selbst.
   *
   * Das Mass ist bewusst KEIN Anteil, sondern ein Rest: 180 px sind der
   * Aussenrand, die obere Tafel (66 px) und rund 100 px Karte, die immer
   * uebrig bleiben sollen. Ein Anteil wie 60 Prozent haette auf einem hohen
   * Schirm zu viel und auf einem niedrigen zu wenig gelassen; die Karte
   * braucht aber keinen Anteil, sondern einen Boden. Auf 852 px greift der
   * Deckel deshalb nie, auf 568 px genau dann, wenn er gebraucht wird.
   *
   * svh und nicht vh: svh ist die KLEINE Ansichtshoehe, also die mit
   * ausgefahrener Browserleiste. vh haette den Deckel genau dann zu hoch
   * angesetzt, wenn am wenigsten Platz ist. Was nicht mehr hineinpasst, rollt
   * innerhalb der Tafel (siehe .status-roll); nichts wird versteckt und nichts
   * abgeschnitten.
   */
  max-height: calc(100vh - 180px);
  max-height: calc(100svh - 180px);
}

/* Die beiden festen Groessen der Zone geben nichts ab: die Quittung steht nur
   kurz, und die zwei Kreise sind 44-px-Ziele. Gekuerzt wird allein die Tafel. */
.toast,
.werkzeugspalte {
  flex: none;
}

/*
 * LAGETAFEL UND MELDEN SIND JETZT GLEICH HOCH, auf Ansage ("den Melde-Button
 * genau so groß machen wie die Meldungsanzeige"). Hier stand vorher
 * `align-items: flex-end` samt der Begruendung, die Differenz halte den Knopf
 * von der Zeile darueber fern. Sichtbar war davon nur eins: ein Knopf, der
 * neben der Tafel zu kurz geraten aussah, gemessen 72 gegen 92 px, sobald eine
 * einzige Meldung vorlag.
 *
 * KEINE ZAHL MACHT DAS, SONDERN `stretch`. Die Tafel waechst mit ihrem Inhalt
 * (leer, mit Meldung, mit Naeherungsleiste) und mit der Systemschrift; jeder
 * feste Wert waere genau bis zum naechsten Inhalt richtig. Gemessen bei 393 px
 * und normaler Schrift: 72 px leer, 91 mit einer Meldung, 160 mit
 * Naeherungsleiste. Mit `stretch` bestimmt der hoehere der beiden die
 * Zeilenhoehe und der andere zieht nach:
 *   leere Tafel      der Knopf gibt mit seinem min-height 72 px das Mass
 *   mit Meldung      die Tafel gibt es, der Knopf zieht nach
 * Nachgemessen ueber fuenf Breiten, zwei Themen, drei Schriftgroessen und die
 * drei Tafelzustaende, also 90 Faelle: ueberall Differenz 0, kein waagerechter
 * Ueberlauf (docs/messung/melden-hoehe.mjs). Damit stimmt die Zusage in JEDEM
 * Zustand, ohne dass irgendwo eine Hoehe gebucht ist. Dieselbe Lehre wie beim
 * Toast, siehe --lagetafel-h im Token-Block: ein Wert, der eine Hoehe SCHAETZT,
 * kann nicht falsch werden, wenn ihn niemand liest.
 *
 * WAS DAS IM SCHLIMMSTEN FALL KOSTET, und die Zahl wird nicht unterschlagen:
 * Bei 320 px Breite, 200 Prozent Systemschrift UND eingeblendeter
 * Naeherungsleiste ist die Tafel 517 px hoch, und der Knopf ist es jetzt auch --
 * eine rote Saeule ueber die halbe Bildhoehe. Ein Deckel dagegen ist bewusst
 * NICHT gesetzt: Er waere wieder die feste Zahl, die beim naechsten Inhalt
 * bricht, und er wuerde die Zusage brechen, die hier gerade eingeloest wird. Wer
 * diesen Fall entschaerfen will, macht die TAFEL dort kleiner (sie ist es, die
 * 517 px verlangt), nicht den Knopf ungleich.
 *
 * DER ABSTAND NACH OBEN HAENGT NICHT DARAN. Er kommt aus `gap` in `.unten`
 * (--reihe-fuge, 12 px, auf niedrigen Schirmen 8), nicht aus der Differenz der
 * beiden Hoehen. Was der hoehere Knopf kostet, ist Karte hinter ihm; was er
 * bringt, ist ein Ziel, das dort, wo der Daumen ohnehin aufsetzt, nicht mehr
 * gesucht werden muss.
 *
 * Die Reihe selbst bleibt durchlaessig, nur ihre Kinder fangen.
 */
.unten-reihe {
  display: flex;
  align-items: stretch;
  gap: var(--reihe-fuge);
  pointer-events: none;
  /* Damit der Deckel an .unten ueberhaupt greifen kann: ein Flexkind schrumpft
     ohne diese Zeile nie unter die Hoehe seines Inhalts. */
  min-height: 0;
}

.unten-reihe > * {
  pointer-events: auto;
}

/*
 * Die Lagetafel. Die einzige Flaeche, die man im Vorbeigehen liest, deshalb
 * der Aufbau einer Puffer-Karte: die grosse Zeile, dann die erklaerende Zeile,
 * dann die Fusszeile.
 *
 * SIE IST KLEINER GEWORDEN, auf Ansage: "Meldungen etwas verkleinern, mehr
 * einbauen in die App, damit man den Unterschied nicht so sieht." Gerechnet
 * und gemessen bei 393 x 852, Tafel mit einer Meldung:
 *
 *   Eyebrow "LAGE" entfallen        -17 px  (11 px Zeile plus 6 px Fuge)
 *   Innenabstand 11/14/8 -> 9/12/7   -3 px
 *   Rasterfuge 6 -> 5 px             -2 px
 *   Titel 17 -> 15 px                -3 px  (1,0625rem -> 0,9375rem)
 *   Radius 20 -> 18 px                0
 *                                   ------
 *                                    -25 px, also 117 -> 92 px
 *
 * "Mehr eingebaut" ist die zweite Haelfte der Ansage, und die traegt nicht die
 * Groesse, sondern die fehlende Kante (siehe .oben): ohne Umriss endet die
 * Tafel nicht, sie geht in die Karte ueber. Die Deckung bleibt bei 82 Prozent,
 * weil unter ihr die Messung der leisen Schrift haengt (5,30:1 hell,
 * 4,78:1 dunkel); weniger Deckung waere mehr Durchsicht und weniger Text.
 *
 * Der Titel darf dabei nicht unter 15 px fallen: bei 0,875rem (14 px) braucht
 * "Keine aktuellen Meldungen" bei 320 px Geraetebreite zwar nur noch eine
 * Zeile, aber die Zeile ist dann kleiner als die Fusszeilen der Vorbilder und
 * liest sich im Stehen nicht mehr im Vorbeigehen.
 */
/*
 * Saeule statt blossem Kasten, seit die Reihe auf `stretch` steht: Ist der
 * Meldeknopf der hoehere von beiden (leere Tafel, siehe .unten-reihe), waechst
 * die Tafel mit, und ohne diese drei Zeilen bliebe unter der Lizenzzeile ein
 * Streifen leeres Glas stehen. `.lage` nimmt den Zuwachs, weil es die einzige
 * Flaeche ist, die ihn tragen kann: die Naeherungsleiste und die Lizenz sind
 * Text mit eigenem Innenabstand, ein gedehnter Text steht schief.
 */
.status {
  /* Nimmt den Deckel von .unten auf, siehe dort. */
  min-height: 0;
  /* Das Leuchten unter dem Finger zieht auf und wieder ab. 240 ms ist
     --d-basis, also die Dauer, die in dieser App alles hat, was erscheint;
     unter 300 ms und mit der Signaturkurve. Uebergeblendet wird die ZAHL
     --tupfer-staerke, nicht opacity und nicht der Weichzeichner-Radius: den zu
     animieren kostet bei einer WebGL-Karte darunter jeden Frame neu. */
  transition: --tupfer-staerke var(--d-basis) var(--ease-out);
  flex: 1;
  min-width: 0;
  border-radius: var(--r-tafel);
  overflow: clip;
  display: flex;
  flex-direction: column;
}

/*
 * DER ROLLBEREICH DER TAFEL. Er nimmt Lage und Naeherungszeile auf, die
 * Lizenzzeile bleibt bewusst DRAUSSEN: eine Urheberangabe, die man erst
 * herunterrollen muss, ist keine. Gerollt wird nur in dem Fall, fuer den der
 * Deckel an .unten da ist; bei normaler Schrift ist der Bereich kleiner als
 * sein Platz und verhaelt sich wie vorher.
 */
.status-roll {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.lage {
  width: 100%;
  min-height: var(--ziel-h);
  /* Nimmt den Zuwachs auf, siehe .status. `align-content: center` haelt dabei
     die beiden Zeilen beieinander, statt sie ueber die Hoehe zu verteilen. */
  flex: 1 0 auto;
  align-content: center;
  padding: var(--lage-oben) var(--tafel-innen) var(--lage-unten);
  display: grid;
  gap: var(--lage-fuge);
  text-align: left;
  min-width: 0;
  transition: background-color var(--d-druck) var(--ease-out);
}

.lage:active {
  background: var(--flaeche-3);
}

.lage-kopf {
  display: flex;
  align-items: center;
  gap: var(--lage-kopf-fuge);
  min-width: 0;
}

/*
 * Die grosse Zeile laeuft eng, der Eyebrow darueber weit. Das Paar ist das
 * typografische Signaturmerkmal seiner Arbeiten.
 *
 * SIE DARF JETZT ZWEI ZEILEN. Vorher stand hier `white-space: nowrap`, und das
 * schnitt den meistgelesenen Satz der App ab: gemessen bei 320 px braucht
 * "Keine aktuellen Meldungen" 210 px, neben dem Statusbalken sind 187 px da,
 * also stand dort "Keine aktuellen Meldu…". Dasselbe traf "3 Meldungen auf
 * 3 Linien". Zwei Zeilen loesen es ohne Verlust: die Tafel waechst ohnehin
 * schon von 86 auf 117 px, sobald eine Meldung vorliegt, und ihre Hoehe ist
 * nicht mehr irgendwo als Zahl gebucht.
 *
 * Bei zwei Zeilen bleibt es auch, wenn jemand die Systemschrift hochdreht:
 * -webkit-line-clamp schneidet dann mit Auslassungspunkten, statt die Tafel
 * ueber den halben Bildschirm wachsen zu lassen.
 */
/* 15 statt 17 px, siehe die Rechnung ueber .status. Der Rest der Regel bleibt
   Wort fuer Wort, insbesondere die zwei erlaubten Zeilen. */
.lage-titel {
  font-size: var(--t-lage-titel-groesse);
  font-weight: var(--t-lage-titel-gewicht);
  letter-spacing: var(--t-lage-titel-sperrung);
  color: var(--schrift);
  font-variant-numeric: tabular-nums;
  min-width: 0;
  overflow: hidden;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  /* Lange Stationsnamen ohne Leerzeichen sollen brechen statt herauszuragen. */
  overflow-wrap: anywhere;
  /* Ausdruecklich dazu: -webkit-line-clamp setzt die Auslassungspunkte selbst,
     aber nur solange die Box wirklich eine -webkit-box ist. Faellt sie einmal
     auf eine gewoehnliche Box zurueck, schnitt `overflow: hidden` ohne diese
     Zeile die dritte Zeile hart ab (gemessen scrollHeight 131 gegen
     clientHeight 87 bei 320 px und 200 Prozent). */
  text-overflow: ellipsis;
}

.lage-neueste {
  display: flex;
  align-items: center;
  gap: var(--neueste-fuge);
  min-width: 0;
  /*
   * SIE DARF UMBRECHEN, WEIL DIE STATION SONST GANZ VERSCHWINDET.
   *
   * Die Zeile hatte drei Kinder: Signet und Zeit mit `flex: none`, dazwischen
   * der Stationsname mit `flex: 1; min-width: 0`. Zwei starre Nachbarn und ein
   * schrumpfbares Kind in der Mitte heisst: bei 320 px und 200 Prozent
   * Systemschrift nahmen sich die beiden Nachbarn die ganze Zeile, und der Name
   * bekam gemessene clientWidth 0 bei scrollWidth 421. Die Ellipse lief ins
   * Leere, die Zeile las sich "S3 - vor 2 Min", die Station fehlte ganz. Fuer
   * eine Melde-App ist die Station DIE Information.
   *
   * Mit Umbruch rutscht die Zeitangabe in eine zweite Zeile, sobald der Name
   * sonst unter seine Mindestbreite fiele. Der Name bleibt dann gekuerzt
   * lesbar statt zu verschwinden. Eine Zeile mehr ist der Preis, und sie faellt
   * nur in dem Fall an, in dem sonst gar nichts mehr dastuende.
   */
  flex-wrap: wrap;
  row-gap: var(--neueste-umbruch);
}

/*
 * DIE ANKUNFT IN DER LAGETAFEL. Die eigene Meldung soll dort ankommen, wo sie
 * hingehoert: auf der Karte (der Ring am Punkt, siehe PUNKT_STIL in
 * js/karte.js) und hier. Gesetzt wird das Merkmal von `tafelAnkunft` in
 * js/app.js, und zwar erst, wenn der Bogen zu ist -- solange er offen steht,
 * liegt er ueber der Tafel, und eine Bewegung, die niemand sieht, ist keine
 * Rueckmeldung.
 *
 * 240 ms, transform und opacity, ease-out, genau einmal. Das ist dieselbe
 * Regel, unter der jede Bedienbewegung dieser App steht, und der bewusste
 * Gegensatz zur Naeherungsleiste zwei Bloecke weiter unten, in der sich NICHTS
 * bewegt: Die ist ein Dauerzustand, das hier ein Ereignis. Sechs Pixel von
 * unten, nicht zwanzig: Der Nutzer steht in einer Bahn und will nicht
 * auffallen, die Zeile soll erscheinen und nicht einfahren.
 */
.lage-neueste[data-ankunft] {
  animation: lage-ankommen var(--d-basis) var(--ease-out) 1 both;
}

@keyframes lage-ankommen {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  /* Die Regel am Ende dieser Datei kuerzt jede Dauer auf 0,01 ms, damit laeuft
     hier ohnehin nichts mehr. Sie steht trotzdem, weil `both` sonst den
     Anfangszustand fuer einen Frame stehen laesst und die Zeile blinkt. */
  .lage-neueste[data-ankunft] {
    animation: none;
  }
}

.lage-ort {
  /* 6ch als Grundbreite, nicht 0: die Zeile bricht damit um, BEVOR der Name auf
     nichts zusammenfaellt. ch waechst mit der Systemschrift, px taete es nicht. */
  flex: 1 1 6ch;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--t-lage-ort-groesse);
  font-weight: var(--t-lage-ort-gewicht);
  color: var(--schrift-2);
}

.lage-zeit {
  flex: none;
  /* Nach dem Umbruch steht sie allein in ihrer Zeile und gehoert an deren
     Ende, nicht an den Anfang. */
  margin-left: auto;
  font-size: var(--t-lage-zeit-groesse);
  font-weight: var(--t-lage-zeit-gewicht);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--t-lage-zeit-sperrung);
  color: var(--schrift-2);
}

/*
 * Die Lageflagge. Ein stehender Balken statt eines Punktes: er liest wie die
 * Farbmarke am Rand einer Anzeigetafel und verschwindet nicht neben 17-px-
 * Schrift. Rot heisst "Meldungen", Gruen heisst "keine Meldung", nie "sicher".
 */
.status-punkt {
  width: var(--flagge-b);
  height: var(--flagge-h);
  border-radius: var(--flagge-r);
  background: var(--warn);
  flex: none;
}

.status-punkt.ruhig {
  background: var(--ruhig);
}

/*
 * Die ruhige Leiste der Naeherungswarnung.
 *
 * NICHTS BEWEGT SICH HIER. Kein Puls, kein Farbwechsel, keine Einblendung: Der
 * Nutzer steht in einer fahrenden Bahn, und eine Flaeche, die dort zappelt, ist
 * genau die Auffaelligkeit, die diese App vermeidet. Sie unterscheidet sich vom
 * Rest der Tafel nur durch die Trennlinie darueber.
 *
 * Zwei Zeilen im Raster statt Flex: die obere darf abschneiden (sie ist kurz und
 * fest), die untere muss umbrechen duerfen (sie ist ein ganzer Satz und traegt
 * die Aussage, die aus "keine Meldung" keine Entwarnung macht). Ein
 * gemeinsames nowrap haette genau den Satz gekuerzt, auf den es ankommt.
 */
.naehe {
  margin: 0;
  /* Innenabstand wie .lage, siehe die Rechnung dort. */
  padding: var(--naehe-oben) var(--tafel-innen) var(--naehe-unten);
  border-top: 1px solid var(--linie);
  display: grid;
  gap: var(--naehe-fuge);
  min-width: 0;
}

.naehe-lage {
  font-size: var(--t-naehe-lage-groesse);
  font-weight: var(--t-naehe-lage-gewicht);
  letter-spacing: var(--t-naehe-lage-sperrung);
  color: var(--schrift);
  min-width: 0;
  /*
   * KEIN nowrap MEHR. Die Zeile war schon bei NORMALER Schrift abgeschnitten:
   * "in deiner Naehe - mehrfach gemeldet" braucht bei 320 px gemessene 213 px
   * und bekam 200; bei 200 Prozent waren es 427 gegen 166, und selbst auf einem
   * 768 px breiten Schirm 427 gegen 396. Eine Warnung, die man nicht zu Ende
   * lesen kann, ist keine Warnung. Sie darf jetzt umbrechen; Platz dafuer ist
   * da, seit die Tafel einen eigenen Rollbereich hat (siehe .status-roll).
   */
  overflow-wrap: anywhere;
  text-wrap: pretty;
}

/* Leise, aber gemessen lesbar: --schrift-2, dieselbe Stufe wie die Lizenzzeile
   darunter. Auf Glas ueber U3-Gelb sind das 5,28:1; --schrift-3 waere dort
   3,92:1 und damit unter der Schwelle. */
.naehe-hinweis {
  font-size: var(--t-naehe-hinweis-groesse);
  font-weight: var(--t-naehe-hinweis-gewicht);
  line-height: var(--t-naehe-hinweis-zeilenhoehe);
  color: var(--schrift-2);
  text-wrap: pretty;
}

/*
 * Pflichtangabe nach ODbL, so leise wie moeglich und jetzt wirklich gemessen.
 *
 * ZWEI KORREKTUREN, beide nachgerechnet mit der WCAG-2.1-Formel:
 *
 * 1. FARBE. Sie stand auf --schrift-3. Die Herleitung der 82 Prozent Deckung
 *    in docs/15-gestaltungssprache.md rechnet aber mit --schrift-2, und der
 *    Unterschied entscheidet: Das Glas mischt sich mit der Karte darunter, und
 *    der schlechteste Untergrund ist S3-Lila #622181 im Hellen und U3-Gelb
 *    #FFDD00 im Dunklen. Gemessen gegen genau diese beiden erreichte
 *    --schrift-3 nur 3,91:1 hell und 3,92:1 dunkel, verfehlte also die 4,5:1
 *    aus WCAG 1.4.3. --schrift-2 traegt dort 5,30:1 und 5,28:1.
 * 2. UMBRUCH. `white-space: nowrap` schnitt bei 200 Prozent Systemschrift
 *    "© OpenStreetMap-Mitwirkende" ab. Eine abgeschnittene Urheberangabe ist
 *    keine Urheberangabe, also darf sie brechen.
 */
.lizenz {
  margin: 0;
  /* Steht ausserhalb von .status-roll und gibt nichts ab, siehe dort. */
  flex: none;
  padding: 0 var(--tafel-innen) var(--lizenz-unten);
  font-size: var(--t-lizenz-groesse);
  font-weight: var(--t-lizenz-gewicht);
  letter-spacing: var(--t-lizenz-sperrung);
  color: var(--schrift-2);
  overflow-wrap: anywhere;
}

/* ---------- Meldeknopf ---------- */

/*
 * DER MELDEKNOPF TRAEGT JETZT ROT, und das kippt eine Regel, die genau hier
 * stand: "ein Bedienelement, dessen Handlung nichts aussagt, bekommt keine
 * semantische Farbe" und "Rot ist hier schon an die Meldungen vergeben"
 * (Design.swift:1041-1048). Der Auftraggeber will ihn praesenter und rot.
 *
 * WIE DER KONFLIKT GELOEST IST, denn die Meldungspunkte auf der Karte muessen
 * weiter als Meldungen lesbar bleiben. Nicht durch einen zweiten Rotton,
 * sondern durch das Gegenteil: es ist GENAU DERSELBE Rotton wie der
 * Meldungspunkt, PALETTE.signal aus js/kartenstil.js, #D62B1F hell und
 * #FF3B30 dunkel. Damit steht Rot in dieser App weiterhin fuer genau eine
 * Sache, naemlich "Meldung"; die Karte sagt "hier ist eine", der Knopf sagt
 * "gib eine ab". Ein eigenes Knopfrot waere die zweite Bedeutung gewesen,
 * vor der die alte Regel gewarnt hat.
 *
 * Auseinandergehalten werden die beiden ueber drei Kanaele, keiner davon Farbe:
 *  - ORT. Der Punkt liegt IN der Karte und bewegt sich mit ihr. Der Knopf
 *    steht im festen HUD, unten rechts, und bewegt sich nie.
 *  - FORM UND GROESSE. Der Punkt ist ein 12-px-Kreis mit weissem Ring und
 *    dunklem Aussensaum. Der Knopf ist eine 72 px hohe Tafel mit 20 px
 *    Eckradius, ohne Ring, und traegt ein Wort. Das Flaechenverhaeltnis ist
 *    rund 1 zu 46.
 *  - WORT. Auf dem Knopf steht "Melden". Auf einem Punkt steht nie etwas.
 * Der weisse Ring des Punktes bleibt unangetastet: er ist das Merkmal, das ihn
 * gegen zehn Linienfarben traegt (gemessen nie unter 4,94:1 dunkel und 3,86:1
 * hell, siehe js/kartenstil.js), und er ist zugleich das Merkmal, das der
 * Knopf nicht hat.
 *
 * DIE FUELLUNG IST JETZT AUCH DIE KANTE, und das loest Punkt 1 fuer diesen
 * Knopf mit: er braucht keine Kontur mehr, weil er selbst 3:1 gegen den
 * Untergrund traegt. Gemessen gegen alle Kartenflaechen (docs/messung/kante.mjs):
 *
 *   hell  #D62B1F   Wasser 3,29 | Gruen 3,37 | Gebaeude 3,49 | Platz 4,01 | Grund 4,38
 *   dunkel #FF3B30  Gruen 3,52 | Wasser 4,12 | Gebaeude 4,50 | Platz 4,88 | Grund 5,17
 *
 * Alle Flaechen ueber 3:1. Darunter liegen nur Striche: dunkel die grosse
 * Strasse mit 2,62 und die kleine mit 2,88. Das ist dieselbe Ausnahme, die
 * diese Datei fuer A1-Orange unter der alten Tafelkante schon gefuehrt hat und
 * die js/kartenstil.js fuer das Casing fuehrt: eine Strasse KREUZT eine
 * Kante, sie belegt sie nie auf ganzer Laenge. Eine Flaeche kann das, und
 * gegen jede Flaeche haelt der Knopf.
 *
 * DIE SCHRIFT WECHSELT MIT DEM MODUS, und zwar aus Messung: auf #D62B1F traegt
 * Weiss 4,97:1 und Tinte nur 3,72:1, auf dem helleren #FF3B30 ist es umgekehrt,
 * Tinte 5,21:1 gegen Weiss 3,55:1. Beide Male gewinnt der Wert ueber 4,5:1
 * (WCAG 1.4.3, das Wort ist 11,5 px gross und damit kein grosser Text).
 *
 * Wort unter dem Symbol statt daneben, weil das Mass hier von der Breite
 * kommt: Ein liegender Knopf mit Wort braucht rund 115 px und nimmt sie der
 * Lagetafel weg, deren Zeile bei 320 px dann mitten im Satz abschneidet. Bei
 * 64 px bleibt die Zeile daneben so lang wie vorher, der Kartenanteil
 * unveraendert, und die Form (Symbol ueber Wort) ist dieselbe, die das Vorbild
 * fuer seine beschriftete Tafel benutzt.
 *
 * Kein Kreis: Die runden Knoepfe darueber sind Kartensteuerung, ein Ziel
 * ohne Leserichtung. Sobald ein Wort daraufsteht, hat der Knopf eine
 * Leserichtung, und dann ist die Tafelform die ehrlichere.
 */
/*
 * DIE BREITE IST EIN MINDESTMASS, KEIN FESTMASS. Sie war fest, und das hielt
 * genau so lange, wie die Schrift nicht wuchs: Bei 200 Prozent Systemschrift
 * ist das Wort "Melden" gemessen 82 px breit und lief 13 px ueber den
 * Bildschirmrand hinaus, weil `body` clippt und der Knopf ganz rechts sitzt.
 * Mit `min-width` bleibt bei normaler Schrift alles, wie es war (das Wort
 * belegt dort 41 px plus 16 px Innenabstand), und bei grosser Schrift waechst
 * der Knopf, statt sein Wort zu verlieren. Die Breite nimmt er sich von der
 * Lagetafel daneben, und die traegt das: ihre Zeile darf ohnehin umbrechen.
 *
 * 72 STATT 64 PX, WEIL "PRAESENTER" NICHT NUR FARBE HEISST. Gemessen ist der
 * Unterschied wichtiger, als er klingt: gegen die Karte traegt das Rot hell
 * nur 4,38:1, waehrend die alte Tintenflaeche dort 16,37:1 hatte. Rot ist also
 * NICHT der lautere, sondern der buntere Ton, und die Praesenz muss zur
 * Haelfte aus der Flaeche kommen. 64 auf 72 px sind 27 Prozent mehr Flaeche.
 * Bei 320 px Geraetebreite bleiben der Lagetafel daneben 212 statt 220 px;
 * ihre Zeile darf zwei Zeilen laufen, es geht also nichts verloren.
 */
/*
 * DIE HOEHE IST JETZT EBENFALLS EIN MINDESTMASS, aus demselben Grund wie die
 * Breite eine Zeile darueber: Der Knopf ist so hoch wie die Lagetafel neben ihm
 * (siehe .unten-reihe), und die kennt ihre Hoehe erst zur Laufzeit. 72 px sind
 * damit nicht mehr die Hoehe, sondern der Boden, auf den er faellt, wenn die
 * Tafel leer und damit niedriger ist als er.
 */
.melden {
  min-width: var(--melden-min);
  padding: 0 var(--melden-innen);
  min-height: var(--melden-min);
  flex: none;
  border-radius: var(--r-tafel);
  /* Keine Kontur mehr: die Fuellung traegt die Kante selbst, siehe oben. */
  background: var(--melden-flaeche);
  color: var(--melden-schrift);
  display: grid;
  place-items: center;
  align-content: center;
  gap: var(--melden-fuge);
  transition:
    transform var(--d-druck) var(--ease-out),
    box-shadow var(--d-druck) var(--ease-out);
}

.melden:active:not(:disabled) {
  transform: scale(0.96);
  box-shadow: var(--schatten-druck);
}

.melden svg {
  width: var(--melden-symbol);
  height: var(--melden-symbol);
  stroke: currentColor;
  fill: none;
  stroke-width: 2.4;
  stroke-linecap: round;
}

/* Klein, halbfett, minimal gesperrt. Nicht in Versalien: Versalien sind in
   dieser Datei die Beschriftung ueber einem Inhalt, hier steht die Handlung
   selbst. */
.melden-wort {
  font-size: var(--t-melden-wort-groesse);
  font-weight: var(--t-melden-wort-gewicht);
  letter-spacing: var(--t-melden-wort-sperrung);
  line-height: var(--t-melden-wort-zeilenhoehe);
}

/* ---------- Meldebestaetigung ---------- */

/*
 * Sitzt ueber der ganzen unteren Zone, also ueber Saeule und Reihe.
 *
 * ER RECHNET SEINEN ABSTAND NICHT MEHR, ER HAENGT SICH AN. Vorher stand hier
 * `position: fixed` und ein `bottom`, das aus --lagetafel-h, --reihe-fuge und
 * --bedienzeile-h zusammengezaehlt war. Diese Token sind FESTE Zahlen, die
 * Lagetafel waechst aber mit der Systemschrift. Nachgemessen bei 393 x 852 und
 * 200 Prozent Schrift lag der Toast 87 px tief IN der Werkzeugsaeule, bei
 * 130 Prozent noch 8 px, und bei 320 x 568 mit 200 Prozent zusaetzlich 36 px
 * in der Lagetafel und 42 px im Stoerband. Der Fehler ist aelter als dieser
 * Umbau, die alte Fassung rechnete mit 117 px Lagetafel und lag genauso
 * daneben; er faellt jetzt nur auf, weil rechts eine Saeule steht.
 *
 * Die Behebung nimmt die Rechnung ganz weg: Der Toast liegt jetzt IN `.unten`
 * und misst mit `bottom: 100%` gegen dessen Oberkante. Damit sitzt er immer
 * 10 px ueber dem hoechsten Punkt der unteren Zone, egal wie hoch die Tafel
 * gerade ist, und `--lagetafel-h` traegt keine Anordnung mehr. Ein Wert, der
 * eine Hoehe SCHAETZT, kann nicht falsch werden, wenn ihn niemand liest.
 *
 * Volle Breite: neben ihm steht nichts mehr, "Meldung angekommen, danke" passt
 * damit bei 320 px in eine Zeile statt in zwei.
 */
.toast {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(100% + 10px);
  padding: 12px 16px;
  border-radius: var(--r-feld);
  font-weight: 500;
  font-size: 0.90625rem;
  text-align: center;
  opacity: 0;
  transform: translateY(8px);
  pointer-events: none;
  transition:
    opacity var(--d-schnell) var(--ease-out),
    transform var(--d-schnell) var(--ease-out);
  z-index: 30;
}

.toast[data-sichtbar] {
  opacity: 1;
  transform: translateY(0);
}

/* ---------- Bogen ---------- */

.bogen-grund {
  position: fixed;
  inset: 0;
  background: var(--abdunkler);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--d-basis) var(--ease-out);
  z-index: 20;
}

.bogen-grund[data-offen] {
  opacity: 1;
  pointer-events: auto;
}

.bogen {
  position: fixed;
  left: 0;
  right: 0;
  /*
   * DIE TASTATUR IST TEIL DER HOEHE.
   *
   * svh statt vh: mit eingeblendeter Browserleiste ist vh zu hoch und der
   * Bogenkopf rutscht unter den Rand. vh bleibt als Rueckfall.
   *
   * Beide Einheiten wissen aber NICHTS von der Bildschirmtastatur. Der Befund
   * des Auftraggebers am 28.08.2026: "wenn man die Suchfunktion nutzt, dann
   * überdeckt die Tastatur die ganze Suche." Genau so ist es -- in einer
   * WKWebView schrumpft die Layout-Ansicht beim Aufgehen der Tastatur nicht,
   * sie legt sich darueber. Ein Bogen mit `bottom: 0` und 88svh Hoehe steht
   * dann zur Haelfte hinter ihr.
   *
   * --tastatur-h kommt aus js/app.js und ist die Differenz zwischen der
   * Layout-Ansicht und der SICHTBAREN (visualViewport). Ohne die Variable --
   * im Browser ohne Tastatur, in jedem Prueflauf -- ist sie 0px, und dann steht
   * hier wortwoertlich, was vorher hier stand.
   */
  bottom: var(--tastatur-h, 0px);
  max-height: calc(88vh - var(--tastatur-h, 0px));
  max-height: calc(88svh - var(--tastatur-h, 0px));

  /*
   * DIE FUELLUNG LIEGT AUF ::before, NICHT HIER. Grund ist die Eckform: die
   * durchgehende Ecke wird ueber eine Neun-Schnitt-Maske nachgebildet, und
   * eine Maske schneidet ALLES weg, was ausserhalb liegt -- auch den
   * Schwebeschatten. Also traegt der Bogen weiter Schatten und Radius, und
   * die Flaeche darunter traegt die Maske.
   *
   * Der Schatten bleibt damit kreisbogig. Das ist gerechnet und nicht
   * uebersehen: er ist 48 px weit weichgezeichnet, und ein Unterschied von
   * 4,25 pt in einer 48-px-Unschaerfe ist kein Unterschied mehr.
   */
  background: transparent;
  border-radius: var(--r-bogen) var(--r-bogen) 0 0;
  /* Bleibt im Fluss, damit der Inhalt nicht um einen Pixel hochrutscht; die
     sichtbare Linie zieht ::before, denn nur die folgt der Maske. */
  border-top: 1px solid transparent;
  /* Stufe 3: der Bogen kommt und geht. Vorher trug er als einzige bewegliche
     Flaeche gar keinen Schatten und lag damit optisch flacher als die
     Kopfleiste, die dauerhaft da ist. */
  box-shadow: var(--schatten-schwebend);
  transform: translateY(101%);
  /* `bottom` faehrt mit: beim Auf- und Zugehen der Tastatur wandert die untere
     Kante, und ohne Uebergang springt der ganze Bogen. Laenger als --d-basis
     duerfte er nicht sein, sonst liefe er der Tastatur hinterher. */
  transition:
    transform var(--d-basis) var(--ease-out),
    bottom var(--d-basis) var(--ease-out);
  z-index: 21;
  display: flex;
  flex-direction: column;
  padding-bottom: env(safe-area-inset-bottom);
}

.bogen[data-offen] {
  transform: translateY(0);
}

/*
 * DIE FLAECHE DES BOGENS, MIT APPLES ECKE.
 *
 * Der Bogen ist die groesste durchgehende Flaeche der App, und er steht auf dem
 * Schirm, waehrend die native Kopftafel oben noch sichtbar ist -- gedaempft
 * unter dem Abdunkler, aber sichtbar. Traegt er den Kreisbogen und sie die
 * durchgehende Ecke, stehen zwei verschiedene Formsprachen uebereinander.
 * Gemessen sind das 4,25 pt Abstand am Radius 22 (docs/messung/eckform/).
 *
 * WebKit kann `corner-shape` nicht (gefragt, nicht geglaubt: CSS.supports gibt
 * false). Nachgebildet wird deshalb mit dem Neun-Schnitt: --eckform-bogen ist
 * eine Superellipse als SVG, erzeugt aus gestalt/gestalt.json. Die vier Ecken
 * bleiben unskaliert, die Kanten dazwischen werden gestreckt -- nur so haelt
 * die Ecke ihr Mass auf einer Flaeche, die die Bildschirmbreite hat.
 *
 * MASKE UND GLAS AUF DEMSELBEN ELEMENT -- gemessen, nicht geglaubt.
 *
 * Hier stand, eine Maske mache das Element zum Backdrop Root und nehme dem
 * Glas den Untergrund. Das gilt fuer KINDER eines maskierten Elements (daran
 * haengt die Bauweise der Tafeln, docs/15 Abschnitt 17.2), nicht fuer das
 * Element selbst: die Maske greift nach dem Filter und beschneidet nur das
 * Ergebnis. Nachgemessen in der WKWebView des Simulators am 29.08.2026
 * (docs/messung/maske-auf-glas/): ein Feld mit Filter UND Maske ist Pixel fuer
 * Pixel eines mit nur Filter -- Helligkeitsspanne ueber einem 6-px-Gitter 0,0
 * gegen 0,0, das blanke Gitter liegt bei 255,0.
 *
 * Also traegt der Bogen beides. Das Materialgesetz und die Eckform fallen an
 * dieser Stelle zusammen (docs/29-materialgesetz.md).
 */
.bogen::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  /*
   * DER BOGEN TRAEGT JETZT DASSELBE MATERIAL WIE DIE KOPFTAFEL -- 29.08.2026.
   *
   * Hier stand `background: var(--flaeche)`, eine deckende Flaeche. Zusammen
   * mit dem Abdunkler darunter waren das zwei Lagen, die dasselbe taten: die
   * Karte verdecken. Der Auftraggeber hat den Unterschied gesehen -- die
   * Kopftafel oben traegt Glas, der Bogen sah daneben aus "wie das erste
   * App-Design".
   *
   * `--glas` und `--glas-filter` sind dieselben Token, mit denen die
   * Web-Fassung der Tafeln arbeitet; in der Huelle bricht das native Material
   * daneben denselben Untergrund. Und weil das Material etwas zum Brechen
   * braucht, ist der Abdunkler mitgewandert (0,5 auf 0,28 hell).
   *
   * Der Rueckfall bleibt hart: wo `backdrop-filter` fehlt, deckt `--flaeche`
   * wie bisher. Eine durchsichtige Flaeche ohne Unschaerfe waere kein
   * schwaecheres Glas, sondern unlesbarer Text.
   */
  background: var(--flaeche);
  box-shadow: inset 0 1px 0 var(--kontur);
  -webkit-mask-box-image: var(--eckform-bogen);
  mask-border: var(--eckform-bogen);
  -webkit-mask-box-image-width: var(--eckbreite-bogen);
  mask-border-width: var(--eckbreite-bogen);
}

/* Nur wo die Engine den Untergrund wirklich bricht. Sonst bleibt es bei der
   deckenden Flaeche darueber -- Glas ohne Unschaerfe ist kein Material,
   sondern Text auf einer Karte. */
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .bogen::before {
    background: var(--glas);
    -webkit-backdrop-filter: var(--glas-filter);
    backdrop-filter: var(--glas-filter);
    box-shadow: var(--glas-kante);
  }
}

/* Der Bogen nimmt beim Oeffnen den Fokus auf, damit die Vorlesestimme im
   Dialog einsetzt, zeigt ihn aber nicht: er ist kein Ziel, das man betaetigt.
   Ohne diese Zeile stuende bei jedem Antippen von "Melden" der Fokusring da. */
.bogen:focus,
.bogen:focus-visible {
  outline: none;
}

/* Der Griff sagt, dass hier eine Flaeche angehoben wurde. */
.bogen-griff {
  width: 40px;
  height: 4px;
  border-radius: 2px;
  background: var(--kontur-feld);
  margin: 10px auto 0;
  flex: none;
}

.bogen-kopf {
  padding: 14px var(--bogen-rand) 16px;
  display: flex;
  align-items: flex-start;
  gap: 12px;
  flex: none;
}

.bogen-text {
  min-width: 0;
}

/* Grosse Ueberschrift, eng laufend. Der Sprung zum Eyebrow darueber ist Faktor
   2: das ist die Spanne, die seine Arbeiten tragen. */
.bogen-titel {
  font-size: 1.4375rem;
  font-weight: 700;
  letter-spacing: -0.025em;
  line-height: 1.15;
  margin: 0;
  overflow-wrap: anywhere;
}

.bogen-unter {
  margin: 0 0 6px;
  line-height: 1;
}

/*
 * DER ZURUECK-PFEIL. Dieselbe Kapsel wie der Schliesser, nur links und ohne
 * Flaeche -- er ist der SCHWAECHERE der beiden Ausgaenge, und eine zweite
 * graue Scheibe neben der ersten waere zwei gleich laute Angebote.
 *
 * `hidden` haelt ihn draussen, solange kein Rueckweg da ist. Er nimmt dann
 * auch keinen Platz: `display: none` gilt vor der Flex-Zeile.
 */
.bogen-zurueck {
  width: 40px;
  height: 44px;
  margin: -2px 0 -2px -12px;
  display: grid;
  place-items: center;
  flex: none;
  transition: transform var(--d-druck) var(--ease-out);
}

.bogen-zurueck[hidden] {
  display: none;
}

.bogen-zurueck svg {
  width: 20px;
  height: 20px;
  stroke: var(--schrift-2);
  fill: none;
  stroke-width: 2.2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.bogen-zurueck:active {
  transform: scale(0.9);
}

.bogen-schliessen {
  margin-left: auto;
  width: 44px;
  height: 44px;
  margin-top: -2px;
  border-radius: 50%;
  background: var(--flaeche-2);
  display: grid;
  place-items: center;
  flex: none;
  transition: transform var(--d-druck) var(--ease-out);
}

.bogen-schliessen:active {
  transform: scale(0.92);
  background: var(--flaeche-3);
}

.bogen-schliessen svg {
  width: 15px;
  height: 15px;
  stroke: var(--schrift-2);
  fill: none;
  stroke-width: 2.2;
  stroke-linecap: round;
}

/*
 * DER INHALT KOMMT GESTAFFELT HEREIN -- derselbe Griff wie in der Einfuehrung.
 *
 * Bis zum 28.08.2026 fuhr der Bogen herein und sein Inhalt war einfach da.
 * Die Einfuehrung staffelt ihre Bauteile seit dem 27.08.2026 (`.tut-teil` in
 * js/tutorial.js), und dass ausgerechnet der Bogen es nicht tat, war kein
 * Entschluss, sondern eine Luecke: zwei Blaetter derselben App, zwei
 * Bewegungen.
 *
 * DIE ZAHLEN, und keine davon ist gegriffen:
 *   --d-basis (240 ms)  dieselbe Dauer wie die Fahrt des Bogens selbst.
 *   8 px                der Weg, den in dieser App jedes zuruecktretende
 *                       Element geht (--hud-raeum-weg, .toast).
 *   40 ms Takt          der Wert, den die Einfuehrung fuer LISTEN benutzt und
 *                       dort begruendet: eine Liste ueberfliegt man, statt sie
 *                       zu lesen. Ein Bogen wird ueberflogen.
 *   80 ms Anlauf        der Bogen ist dann rund ein Drittel seines Weges
 *                       gefahren. Frueher liefen beide Bewegungen uebereinander
 *                       und keine war zu sehen.
 *
 * DER TAKT IST GEDECKELT (siehe `staffelnImBogen` in js/app.js). Ohne Deckel
 * hinge das letzte Bauteil einer langen Lageliste sekundenlang hinterher --
 * eine Staffelung soll eine Richtung zeigen und keine Warteschlange sein.
 *
 * `backwards` ist noetig, damit die Endlage vor dem Start schon gilt: ohne das
 * blitzt jedes Bauteil einmal an seiner Endstelle auf, bevor die Verzoegerung
 * es zurueckwirft.
 */
.bogen-teil {
  animation: bogen-teil var(--d-basis) var(--ease-out) backwards;
  animation-delay: calc(var(--anlauf, 80ms) + var(--takt, 0) * var(--staffel, 40ms));
}

@keyframes bogen-teil {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
}

/* Ohne Bewegung faellt die Staffelung ersatzlos weg. Die Endlage steht in der
   Grundregel, hier wird nur eine Zeit geschrieben -- es bleibt also nichts
   halb Sichtbares stehen. */
@media (prefers-reduced-motion: reduce) {
  .bogen-teil {
    animation: none;
  }
}

.bogen-inhalt {
  overflow-y: auto;
  /* clip und nicht auto: waagerecht soll hier nichts zu rollen sein. Das ist
     nur haltbar, solange auch nichts herauslaeuft; der eine Verursacher (.wahl)
     ist behoben, und tests/schnitt.test.mjs haelt die Zusage fest. */
  overflow-x: clip;
  border-top: 1px solid var(--linie);
  padding: 16px var(--bogen-rand) 24px;
  -webkit-overflow-scrolling: touch;
}

/*
 * Links in den Textseiten. Diese App fuehrt keine Linkfarbe, also traegt der
 * Link Tinte und einen Unterstrich - das voreingestellte Browser-Blau liegt im
 * Dunkelmodus unter jedem Kontrastwert (#0000ee auf #0d1017 ergibt 1,1:1).
 * js/seiten.js setzt genau das bisher als style-Attribut an jeden einzelnen
 * Link; mit dieser Regel kann es dort weg.
 */
.bogen-inhalt a {
  color: var(--schrift);
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
  /* Lange URLs sollen brechen statt den Bogen zu sprengen. */
  overflow-wrap: anywhere;
}

/* ---------- Schritte im Melde-Ablauf ---------- */

.schritte {
  display: flex;
  gap: 5px;
  padding: 0 var(--bogen-rand) 14px;
  flex: none;
}

.schritt-balken {
  height: 4px;
  flex: 1;
  border-radius: 2px;
  background: var(--flaeche-3);
  transition: background-color var(--d-schnell) var(--ease-out);
}

.schritt-balken[data-aktiv] {
  background: var(--schrift);
}

/* ---------- Linienauswahl als Gitter ---------- */

/*
 * DIE KACHELN SIND KEINE KACHELN MEHR. Auf Ansage des Auftraggebers: "Die
 * Halte-Anzahl pro Bahn rauslassen und nur die Bahn-Symbole nutzen statt dem
 * Rahmen drumherum."
 *
 * Was wegfaellt und warum es zu verschmerzen ist:
 *  - DER RAHMEN. Ein Signet ist bereits eine geschlossene Farbflaeche mit
 *    Saum; ein Feld darum ist ein zweiter Umriss um denselben Gegenstand. Er
 *    war ausserdem das letzte --kontur-feld ueber einer grossen Flaeche und
 *    stand damit gegen Punkt 1.
 *  - DIE ZEILE "47 HALTE". Sie beantwortete eine Frage, die in Schritt 1
 *    niemand stellt (man weiss, welche Linie man faehrt), und sie war die
 *    einzige Ursache dafuer, dass die Kachel 76 px hoch sein musste. Die
 *    Stationszahl steht weiter im aria-label, wo sie einer Vorlesestimme
 *    Auskunft gibt.
 *
 * DAS ZIEL BLEIBT GROSS GENUG, und das ist die Bedingung: das Signet selbst
 * ist 30 px hoch (.linie.gross), also unter der Fingernorm. Deshalb traegt der
 * KNOPF weiter 44 px Mindesthoehe und nimmt die frei gewordene Hoehe nicht
 * mit; er ist jetzt nur noch die Trefferflaeche um das Signet, nicht mehr
 * dessen Rahmen. Gemessen bei 320 px Geraetebreite bleiben im Bogen 284 px,
 * das ergibt weiter genau drei Spalten (3 x 84 + 2 x 8).
 *
 * Der Druck faerbt jetzt nichts mehr (es gibt keine Flaeche zum Faerben),
 * sondern staucht nur. Das ist dieselbe Rueckmeldung wie an jedem anderen
 * flaechenlosen Ziel dieser Datei.
 */
/*
 * DAS GITTER STEHT IN EINER GRUPPE -- 29.08.2026.
 *
 * Es ist die einzige Flaeche der Einstellungen, die nicht aus Zeilen besteht,
 * und stand deshalb als einziges frei im Bogen: zehn volle Signets ohne
 * Umschluss, mit acht Pixeln Fuge dazwischen und viel Weiss drumherum. Neben
 * den Gruppen darueber und darunter las sich das als "Rest".
 *
 * Dieselbe Flaeche und derselbe Radius wie .liste, also dieselbe Aussage:
 * "das hier gehoert zusammen". Der Innenabstand ist 12 statt 16 px, weil das
 * Gitter seine eigene Fuge von 8 px mitbringt und 16 plus 8 optisch doppelt
 * so weit aussieht wie eine Zeile mit 16.
 */
.gitter-linien {
  background: var(--flaeche-2);
  border-radius: var(--r-tafel);
  padding: 10px;
  display: grid;
  /*
   * 5rem, UND DIE ZAHL IST NACHGERECHNET -- 29.08.2026.
   *
   * Hier standen 5,25rem, begruendet mit dem Wort "merken" unter dem Signet.
   * Das Wort ist am 28.08.2026 gefallen (siehe den Block darueber), die Zahl
   * blieb. Als das Gitter am 29.08. seine Gruppe bekam, kosteten deren
   * Innenraender die vierte Spalte:
   *
   *   Geraet 402 pt, Bogenrand 2 x 16 -> 370
   *   Gruppe 2 x 10 Innenabstand      -> 350
   *   vier Spalten a 5rem plus 3 x 8   = 344   passt
   *   vier Spalten a 5,25rem plus 3x8  = 360   passt nicht
   *
   * Ohne die Rechnung standen die zehn Linien in vier Reihen, die letzte mit
   * einer einzelnen Kachel. Bei 320 px bleiben es drei Spalten wie vorher
   * (3 x 80 + 2 x 8 = 256 in 268).
   *
   * min(100%, ...) haelt die Regel auch dann gueltig, wenn der Bogen schmaler
   * ist als eine einzelne Spalte; rem statt px, damit die Spalte mit der
   * Systemschrift waechst.
   */
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 5rem), 1fr));
  gap: 8px;
}

.linien-kachel {
  min-height: 44px;
  padding: 6px;
  border-radius: var(--r-feld);
  display: grid;
  place-items: center;
  align-content: center;
  gap: 6px;
  transition: transform var(--d-druck) var(--ease-out);
}

.linien-kachel:active {
  transform: scale(0.94);
}

/*
 * DIE KAPSEL IST DER SCHALTER: FUELLUNG GEGEN UMRISS.
 *
 * Hier standen zwei Regeln, und beide sind am 28.08.2026 gefallen.
 *
 * Die erste war `.linien-kachel small`: unter jeder der zehn Linien stand in
 * Versalien "MERKEN". Zehnmal dasselbe Wort unter zehn Kacheln, gemessen die
 * halbe Bogenhoehe. Die zweite war ein vier Pixel starker Aussensaum in Tinte
 * um das gewaehlte Signet -- eine dritte Zustandssprache neben Fuellung und
 * Blaesse, und die einzige, die man ohne das Wort daneben nicht lesen konnte.
 *
 * Jetzt traegt die Kapsel den Zustand selbst. Gemerkt ist GEFUELLT (der
 * Linienton mit seiner geprueften Schriftfarbe, dieselbe Kapsel wie ueberall
 * sonst in dieser App), nicht gemerkt ist der UMRISS: derselbe Ton als
 * 2-px-Kontur, die Flaeche nur angehaucht, die Schrift in Tinte.
 *
 * DASS DIE SCHRIFT IN TINTE STEHT, IST DIE RECHNUNG UND NICHT DER GESCHMACK.
 * Ein Signet in seiner eigenen Farbe auf der Bogenflaeche waere bei U3-Gelb
 * (#ffdd00 auf #ffffff, 1,29:1) unlesbar. Tinte auf der angehauchten Flaeche
 * traegt in jedem der zehn Faelle ueber 14:1, hell wie dunkel
 * (docs/messung/glas/kontrast-28-08.mjs). Der Ton bleibt trotzdem am Zeichen,
 * er sitzt nur in der Kontur statt in der Fuellung.
 *
 * Unterschieden wird damit durch FUELLUNG, nicht durch Farbton -- WCAG 1.4.1
 * ist nicht beruehrt. Die Vorlesestimme bekommt aria-pressed und den vollen
 * Satz im aria-label; ihr fehlt kein Wort.
 */
/*
 * GEMERKT: EIN RING, KEINE ANDERE KAPSEL.
 *
 * Hier stand bis zum 28.08.2026 `.linie.umriss` -- die nicht gemerkte Linie
 * nahm ihre Fuellung ab und stand als Kontur mit 12 Prozent Hauch da. Der
 * Auftraggeber nannte das "gebleicht", und der Vergleich, den er zog, trifft:
 * OBEN im Meldungsband stehen alle zehn Signets VOLL. Gewaehlt ist dort nicht
 * die Linie, sondern der Filter, und den traegt die Kapsel "Alle". Voll ist
 * also der Normalzustand eines Signets in dieser App.
 *
 * Der Ring sitzt AUSSEN mit einer Fuge in der Grundfarbe dazwischen, sonst
 * verschwaende er auf einem dunklen Signet (U4, S3) und schriee auf einem
 * hellen (U3). Zwei Schatten statt einer Kontur, weil eine Kontur die Kapsel
 * groesser machen wuerde und das Gitter dann bei jedem Tipp zuckt.
 *
 * Der Unterschied ist eine FORM und kein Farbton -- WCAG 1.4.1 gilt
 * unveraendert, und die Vorlesestimme hoert ohnehin aria-pressed.
 */
.linien-kachel[aria-pressed="true"] .linie {
  box-shadow:
    0 0 0 2px var(--grund),
    0 0 0 4px var(--schrift);
}

/*
 * DER RING KOMMT AN, STATT DA ZU SEIN -- 29.08.2026.
 *
 * Er sprang bisher ohne Uebergang auf die Kapsel. Eine Linie zu merken ist die
 * einzige Handlung in den Einstellungen, die man mehrfach hintereinander tut
 * (zehn Kacheln, man tippt zwei oder drei) -- und die einzige, deren Wirkung
 * nur ein Ring ist. Ohne Bewegung ist nicht zu unterscheiden, ob man getroffen
 * hat oder ob die Kachel schon so aussah.
 *
 * `box-shadow` und nicht transform, und das ist hier ausnahmsweise richtig: der
 * Ring liegt AUSSERHALB der Kapsel, ein transform an der Kapsel wuerde ihren
 * Inhalt mitziehen. Es sind zehn 44-px-Ziele in einem Bogen, der nichts
 * anderes tut -- kein Compositor-Druck wie auf der Karte.
 *
 * --d-schnell (160 ms): das ist eine Quittung, keine Fahrt.
 */
.linien-kachel .linie {
  transition: box-shadow var(--d-schnell) var(--ease-out);
}

@media (prefers-reduced-motion: reduce) {
  .linien-kachel .linie {
    transition: none;
  }
}

/* ---------- Listen ---------- */

/*
 * DIE LISTE IST EINE GRUPPE, KEINE REIHE TRENNSTRICHE -- 29.08.2026.
 *
 * WAS HIER STAND: "Keine Karte je Zeile. Eine Zeile ist ein Raster plus eine
 * Trennlinie darunter [...] Die Linie laeuft ueber die volle Breite des
 * Bogens, dafuer zieht die Liste ihre Raender heraus."
 *
 * Der Satz stimmt in seinem ersten Teil weiter: eine Karte JE ZEILE waere
 * Unruhe. Falsch war der zweite. Randlose Vollbreiten-Trenner ueber einer
 * durchgehenden Flaeche sind die Bauform der iOS-Einstellungen von 2013, und
 * genau so hat der Auftraggeber sie am 29.08.2026 gelesen: "wie das erste
 * App-Design".
 *
 * Die heutige Form ist die GRUPPE: eine gerundete Flaeche je Abschnitt, die
 * Zeilen darin, Trenner nur ZWISCHEN ihnen und auf Hoehe des Textes
 * eingerueckt. Was das leistet, ist nicht Politur: Man sieht, wo eine Gruppe
 * anfaengt und aufhoert, ohne die Ueberschrift zu lesen. Die Trennlinie
 * gliedert dann nur noch innerhalb, und das ist genau die Rolle, die
 * --linie im Materialgesetz hat ("gliedert nur, umschliesst nichts
 * Bedienbares").
 *
 * Der Rand wird nicht mehr herausgezogen: die Gruppe steht im Textspiegel wie
 * jeder andere Absatz, sonst haette sie eine Rundung, die am Bogenrand
 * abgeschnitten waere.
 */
.liste {
  margin: 0;
  padding: 0;
  background: var(--flaeche-2);
  border-radius: var(--r-tafel);
  /* `clip` statt `hidden`: die Zeilen sollen an der Rundung beschnitten
     werden, ohne dass die Gruppe zu einem Rollbereich wird. */
  overflow: clip;
}

.listen-knopf {
  width: 100%;
  min-height: 60px;
  padding: 12px 16px;
  display: flex;
  align-items: center;
  gap: 12px;
  text-align: left;
  transition: background-color var(--d-druck) var(--ease-out);
}

/*
 * DIE EINE HERVORGEHOBENE ZEILE, und sie ist es ueber SCHRIFT, nicht ueber
 * Flaeche.
 *
 * Es gibt im ganzen Bogen genau eine (die Projektseite), denn Hervorhebung
 * wirkt nur, solange sie selten ist. Eine gefuellte Flaeche in einer Liste
 * sonst ruhiger Zeilen liest sich als Warnung -- die Signalfarbe dieser App
 * gehoert dem Melden-Knopf und den Meldungen auf der Karte, und eine Bitte um
 * Unterstuetzung darf nicht aussehen wie eine Kontrolle in der Naehe.
 *
 * Bleibt das Gewicht: Der Titel steht halbfett statt normal. Das traegt auch
 * ohne Farbe (WCAG 1.4.1) und hebt die Zeile, ohne sie zu ueberreden.
 */
.listen-knopf.betont .listen-titel {
  font-weight: 600;
}

/* Ein `a` in der Liste soll aussehen und sich anfuehlen wie ein Knopf. Der
   Browser gibt Links sonst seine eigene Farbe und Unterstreichung. */
a.listen-knopf {
  color: inherit;
  text-decoration: none;
}

/*
 * DAS TEXTFELD DER RUECKMELDUNG.
 *
 * Das einzige Freitextfeld der ganzen App, und es schreibt bewusst nicht an
 * unseren Server, sondern in eine E-Mail (siehe `rueckmeldungOeffnen` in
 * js/app.js). Deshalb steht es hier allein und nicht in einem Formularsatz:
 * es gibt keinen zweiten.
 */
.rueckmeldung-feld {
  width: 100%;
  margin-top: 12px;
  padding: 12px 14px;
  border-radius: var(--r-tafel);
  background: var(--flaeche-2);
  border: 1px solid var(--kontur-feld);
  color: var(--schrift);
  font: inherit;
  /* Waagerecht nicht ziehbar: eine Spalte, die breiter wird als ihr Bogen,
     schiebt den ganzen Bogen zur Seite. */
  resize: vertical;
  min-height: 120px;
}

.rueckmeldung-feld::placeholder {
  color: var(--schrift-3);
}

/*
 * Der Trenner sitzt jetzt ZWISCHEN den Zeilen und nicht unter jeder. Ohne
 * `:last-child` stuende eine Linie direkt auf der unteren Rundung der Gruppe,
 * und eine Linie, die an einer Rundung endet, liest sich als Fehler.
 *
 * Eingerueckt auf die Textkante (16 px Innenrand): so trennt sie die Inhalte
 * und nicht die Flaeche. Ueber ein Pseudoelement statt ueber `border-bottom`,
 * damit die Einrueckung nicht die Zeilenhoehe verschiebt.
 */
.liste > * {
  position: relative;
}

/*
 * `* + *` und nicht `.listen-knopf + .listen-knopf`: die Liste mischt Knoepfe
 * (.listen-knopf) mit Zeilen aus Knopf plus Nebenknopf (.listen-zeile), und
 * eine Aufzaehlung beider Paarungen waere vier Selektoren, die beim naechsten
 * Zeilentyp wieder unvollstaendig sind. Getrennt gehoert alles, was nicht
 * oben steht.
 */
.liste > * + *::before {
  content: "";
  position: absolute;
  left: 16px;
  right: 0;
  top: 0;
  height: 1px;
  background: var(--linie);
}

/*
 * DER DRUCK MUSS EINE STUFE TIEFER GEHEN ALS DIE GRUPPE -- 29.08.2026.
 *
 * Hier stand `--flaeche-2`, und das war richtig, solange die Liste auf
 * `--flaeche` stand. Seit die Liste selbst eine Gruppe in `--flaeche-2` ist,
 * faerbt der Druck die Zeile auf die Farbe, die sie ohnehin hat: gemessen
 * 1,000:1, also nichts. Ein Ziel ohne Rueckmeldung liest sich als kaputt.
 */
.listen-knopf:active {
  background: var(--flaeche-3);
}

@media (hover: hover) and (pointer: fine) {
  .listen-knopf:hover,
  .zeilen-mehr:hover {
    background: var(--flaeche-3);
  }

  .zeilen-schnell:hover:not([data-getan]) {
    background: var(--flaeche-3);
  }
}

.listen-zeile {
  display: flex;
  align-items: stretch;
  padding-right: 8px;
  /* Zwei Ziele nebeneinander brauchen eine sichtbare Fuge, sonst liest sich der
     Haken als linke Haelfte des Menueknopfes. Zwei Pixel, mehr ist bei 320 px
     nicht zu holen -- die Flaechen selbst bleiben bei 44 bzw. 48. */
  gap: 2px;
}

.listen-zeile .listen-knopf {
  flex: 1;
  min-width: 0;
  width: auto;
  padding-right: 4px;
}

.zeilen-mehr {
  flex: none;
  width: 48px;
  min-height: 48px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-feld);
  color: var(--schrift-3);
  font-size: 1.125rem;
  letter-spacing: 0.08em;
}

.zeilen-mehr:active {
  background: var(--flaeche-3);
}

/*
 * Der Schnellknopf in der Zeile ("stimmt noch").
 *
 * 44 x 44, die Untergrenze aus dem Auftrag, und keinen Pixel weniger -- der
 * Knopf wird in einer fahrenden Bahn getroffen. Er sitzt LINKS vom "⋯" und
 * traegt deshalb eine eigene Flaeche: zwei randlose Zeichen nebeneinander waeren
 * eine Zeichenkette, keine zwei Knoepfe.
 *
 * Tinte statt Gruen, dieselbe Entscheidung wie beim Schalter weiter unten: Gruen
 * ist in dieser App an "keine Meldung" vergeben und darf nicht zweimal etwas
 * anderes heissen. Rot ist der Meldeknopf. Bleibt die Schriftfarbe.
 *
 * `align-self: center` statt `stretch`: Bei zwei Zeilen Untertext waere ein
 * ueber die volle Zeilenhoehe gezogener Knopf 80 px hoch und saehe aus wie eine
 * Spalte. Die Trefferflaeche bleibt bei 44, sie waechst nur nicht mit.
 */
.zeilen-schnell {
  flex: none;
  align-self: center;
  width: 44px;
  min-height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-feld);
  background: var(--flaeche-2);
  box-shadow: inset 0 0 0 1px var(--kontur);
  color: var(--schrift);
  font-size: 1.0625rem;
  line-height: 1;
  transition: background-color var(--d-druck) var(--ease-out);
}

.zeilen-schnell:active {
  background: var(--flaeche-3);
}

/*
 * Getan. Kein Haken in Gruen und kein Verschwinden: Der Knopf bleibt stehen,
 * damit die Zeile ihre Hoehe behaelt und der Daumen nicht ins Leere faellt,
 * und er wird leise. `disabled` haelt ihn aus der Tabulatorreihenfolge; die
 * Bedeutung steht im `aria-label`, nicht in der Farbe.
 */
.zeilen-schnell[data-getan] {
  background: none;
  box-shadow: none;
  color: var(--schrift-2);
}

.listen-titel {
  font-weight: 500;
  font-size: 1rem;
  letter-spacing: -0.01em;
}

/* Bewusst keine Versalien: hier stehen mal Daten, mal ganze Saetze. Ein Satz
   in Versalien schreit, eine Beschriftung nicht. */
.listen-unter {
  font-size: 0.78125rem;
  font-weight: 400;
  color: var(--schrift-2);
  font-variant-numeric: tabular-nums;
  margin-top: 3px;
}

.listen-mitte {
  flex: 1;
  min-width: 0;
}

/* hidden statt clip: die Auslassungspunkte sind das Ziel, und hidden ist die
   Paarung, die jeder Browser mit text-overflow sicher beherrscht. */
.listen-mitte > div {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/*
 * DER TITEL EINER LISTENZEILE DARF ZWEI ZEILEN, DER REST NICHT.
 *
 * Eine Zeile plus Ellipse reicht fuer eine Unterzeile ("Bus - 8,7 km"), aber
 * nicht fuer den Namen: bei 320 px und 200 Prozent verlor er gemessene 137 bis
 * 332 px, und auf dem Schirm standen untereinander "Kleine ...", "U Klein..."
 * und "S Klein...". Drei Treffer, die sich nicht unterscheiden lassen, sind
 * keine Trefferliste. Zwei Zeilen bringen die Unterscheidung zurueck, ohne die
 * Zeilenhoehe der Liste unbegrenzt wachsen zu lassen.
 *
 * Die Unterzeile bekommt dasselbe. In der Trefferliste steht dort "Bus - 8,7 km"
 * und die zweite Zeile bleibt ungenutzt; in den Einstellungen steht dort der
 * erklaerende Satz, und der verlor bei 200 Prozent gemessene 8 bis 105 px.
 */
.listen-mitte > .listen-titel,
.listen-mitte > .listen-unter {
  white-space: normal;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow-wrap: anywhere;
}

/*
 * ZWEI ZEILEN SIND EINE ANNAHME UEBER DIE ZEILENLAENGE, UND DIE HAELT NICHT
 * IMMER -- 29.08.2026.
 *
 * Der Deckel von zwei Zeilen ist richtig, solange in eine Zeile ein halber
 * Stationsname passt. Bei 200 Prozent Systemschrift passt er nicht: gemessen
 * mit tests/schnitt.mess.mjs brauchte "Klein Hansdorf, Hansdorfer Damm" bei
 * 320 px fuenf Zeilen (278 px) in einem Kasten von 93 px. Sichtbar blieb
 * "Klein Hans...", und zwei Stationen desselben Ortsteils sind so nicht mehr
 * zu unterscheiden. WCAG 1.4.4 verlangt genau das Gegenteil: bei 200 Prozent
 * darf kein Inhalt verlorengehen.
 *
 * Aufgefallen ist es erst, als die Liste am selben Tag zur GRUPPE wurde: sie
 * zieht ihre Raender nicht mehr heraus, und mit dem Innenpolster sind das
 * 32 px weniger Textbreite. Vorher war die Spalte breit genug, dass der Deckel
 * nicht griff. Der Fehler lag also schon da und war nur nicht sichtbar.
 *
 * DER WEG UEBER EINE MEDIA QUERY IN em WAR EINE SACKGASSE, und der Grund
 * gehoert festgehalten: in einer Media Query bezieht sich `em` auf die
 * INITIALE Schriftgroesse des Browsers und nicht auf die gesetzte
 * Wurzelgroesse. `@media (max-width: 16em)` misst also 393/16 und nicht
 * 393/32 -- die Regel greift bei 200 Prozent genau dann nicht, wenn sie
 * gebraucht wird. Nachgemessen: unveraendert 93 px sichtbar.
 *
 * Deshalb steht der Deckel jetzt pauschal auf FUENF. Bei normaler Schrift
 * aendert das nichts -- gemessen ueber alle fuenf Breiten bei 100 und
 * 130 Prozent: kein einziger senkrechter Schnitt an .listen-titel, weder
 * vorher noch nachher. Bei 200 Prozent faellt die Zahl der geschnittenen
 * Zeilen von 19 (ueber die drei gemessenen Breiten) auf EINEN.
 *
 * UND DIESER EINE BLEIBT, MIT ABSICHT. Bei 320 px und 200 Prozent braucht
 * "Klein Hansdorf, Hansdorfer Damm" sechs Zeilen. Der laengste Name des
 * Verzeichnisses hat 50 Zeichen (AIRBUS-Werkgelaende, 9131 Orte, Mittel 21),
 * der braeuchte dort rund zehn -- 465 px, also fast den ganzen Schirm fuer
 * EINE Listenzeile. Es gibt an dieser Stelle keinen Deckel, der jeden Namen
 * ganz zeigt UND die Liste als Liste erhaelt.
 *
 * Was der Deckel deshalb leisten muss, ist die UNTERSCHEIDBARKEIT, und die
 * traegt er: fuenf Zeilen zeigen "Klein Hansdorf, Hansdorfer...", also weit
 * ueber den Punkt hinaus, an dem sich zwei Halte desselben Ortsteils trennen.
 * Der volle Name steht weiter im DOM und wird von der Vorlesestimme ganz
 * gelesen; WCAG 1.4.4 verlangt, dass Inhalt und Funktion erhalten bleiben,
 * nicht dass nichts gekuerzt wird.
 *
 * .listen-unter behaelt zwei Zeilen. Er ist ein Beitext, und dort ist Kuerzung
 * das gewollte Verhalten.
 */
.listen-mitte > .listen-titel {
  -webkit-line-clamp: 5;
  line-clamp: 5;
}

.linien-reihe {
  display: flex;
  gap: 5px;
  flex-wrap: wrap;
  row-gap: 5px;
  margin-top: 4px;
}

/* Steht die Signetreihe links statt unter dem Titel, darf sie dem Namen nicht
   die Zeile wegnehmen. */
.listen-knopf > .linien-reihe {
  flex: none;
  max-width: 40%;
  margin-top: 0;
}

/*
 * Der Schalter. Gleis plus Knauf, sonst nichts.
 *
 * Das Ziel ist die ganze Zeile (.listen-knopf, 60 px hoch), nicht das Gleis:
 * ein 31 px hohes Gleis waere in einer ruckelnden Bahn ein Fehlgriff, und ein
 * auf 44 px aufgeblasenes Gleis saehe aus wie ein Knopf. Deshalb traegt das
 * Gleis nur die Anzeige und `pointer-events: none`, damit ein Tipp darauf
 * dieselbe Zeile trifft.
 *
 * Die Kontur ist Pflicht, nicht Zierde: WCAG 1.4.11 verlangt fuer grafische
 * Objekte 3:1, und ein reines Flaechenpaar aus --flaeche-3 und --flaeche
 * erreicht das nicht. Im eingeschalteten Zustand traegt --schrift (Tinte), also
 * dieselbe Sprache wie der Meldeknopf, und nicht Gruen: Gruen ist in dieser App
 * an "keine Meldung" vergeben und darf nicht zweimal etwas anderes heissen.
 */
.schalter-gleis {
  flex: none;
  width: var(--schalter-gleis-b);
  height: var(--schalter-gleis-h);
  /* Die Kapselform ist die halbe Hoehe. Als Rechnung statt als 14, damit sie
     der Hoehe folgt statt neben ihr zu stehen. */
  border-radius: calc(var(--schalter-gleis-h) / 2);
  background: var(--flaeche-3);
  border: 1px solid var(--kontur);
  display: flex;
  align-items: center;
  padding: var(--schalter-gleis-innen);
  pointer-events: none;
  transition: background-color var(--d-druck) var(--ease-out);
}

/*
 * Der Knauf gleitet, und seit dem 28.08.2026 auf derselben Kurve wie der
 * Indikator des Segment-Schalters.
 *
 * Der Grund ist nicht Schoenheit, sondern Sprache: in einem Bogen stehen jetzt
 * zwei Dinge, die eine STRECKE zuruecklegen -- der Indikator und dieser Knauf.
 * Legten sie dieselbe Strecke auf verschiedenen Kurven zurueck, waeren es zwei
 * Bauteile mit derselben Aufgabe. --ease-out (0.23, 1, 0.32, 1) schiesst am
 * Ziel vorbei und faellt zurueck; an einem erscheinenden Element ist das
 * richtig, an einem fahrenden liest es sich als Wackeln.
 *
 * Die Dauer bleibt --d-druck: der Knauf legt 18 px zurueck, der Indikator bis
 * zu 240. Dieselbe Zeit fuer beide Wege waere fuer den kurzen zaeh.
 */
.schalter-knauf {
  width: var(--schalter-knauf);
  height: var(--schalter-knauf);
  border-radius: 50%;
  background: var(--flaeche);
  border: 1px solid var(--kontur);
  box-shadow: var(--schatten-druck);
  transition: transform var(--d-druck) var(--seg-kurve);
}

.schalter[aria-checked="true"] .schalter-gleis {
  background: var(--schrift);
  border-color: var(--schrift);
}

/*
 * DER WEG WAR 18 PX UND IST GERECHNET 20 -- korrigiert am 30.08.2026.
 *
 * Das Gleis ist 46 breit und traegt 2 px Innenabstand auf jeder Seite, der
 * Knauf misst 22 (border-box, die Kante zaehlt mit). Innen bleiben also 42,
 * der Knauf laesst 20 uebrig. Mit 18 stand er rechts 2 px vor dem Rand,
 * waehrend er links buendig sass -- asymmetrisch, und zwar genau um die
 * Differenz. Die Zahl kommt jetzt aus `abgeleitet` in gestalt/erzeugen.mjs
 * und folgt den drei Massen darueber, statt neben ihnen zu stehen.
 */
.schalter[aria-checked="true"] .schalter-knauf {
  transform: translateX(var(--schalter-weg));
  border-color: var(--grund);
}

/* Wer Bewegung abbestellt hat, bekommt den Zustand ohne Fahrt. Der Schalter
   bleibt lesbar, weil die Flaeche die Aussage traegt, nicht der Weg dorthin. */
@media (prefers-reduced-motion: reduce) {
  .schalter-gleis,
  .schalter-knauf {
    transition: none;
  }
}

.pfeil {
  color: var(--schrift-3);
  flex: none;
  font-weight: 400;
  font-size: 1.25rem;
  line-height: 1;
  padding-right: 2px;
}

/* ---------- Suchfelder ---------- */

.suche,
.suchfeld {
  width: 100%;
  /* min-height statt height: 50 px war ein Mass, das nur bei normaler
     Schriftgroesse stimmte. Bei 200 Prozent steht in dem Feld 32-px-Schrift,
     und eine feste Hoehe haette sie oben und unten angeschnitten. */
  min-height: 50px;
  padding: 0 14px;
  border-radius: var(--r-feld);
  background: var(--flaeche);
  /* Ein Eingabefeld wird allein durch seine Kante erkannt, die braucht nach
     WCAG 1.4.11 gemessene 3:1. */
  border: 1px solid var(--kontur-feld);
  color: var(--schrift);
  /* max() statt 1rem allein: unter 16 px zoomt iOS beim Fokus in das Feld
     hinein, und wer seine Grundschrift kleiner gestellt hat, kaeme mit reinem
     rem darunter. Nach oben waechst es weiter mit. */
  font-size: max(1rem, 16px);
  font-weight: 500;
  font-family: inherit;
  margin-bottom: 14px;
}

.suche::placeholder,
.suchfeld::placeholder {
  color: var(--schrift-3);
  font-weight: 400;
}

/* Der Platzhalter ist laenger als das Feld, sobald die Systemschrift steigt:
   gemessen 448 px Text in 254 px Innenbreite bei 200 Prozent, also 194 px, die
   ohne ein Zeichen der Kuerzung verschwanden. Ein Eingabefeld kann nicht
   umbrechen, also muss es wenigstens sagen, dass da noch etwas steht. */
.suche,
.suchfeld {
  text-overflow: ellipsis;
}

/* ---------- Hinweise ---------- */

.hinweis {
  padding: 14px 15px;
  border-radius: var(--r-feld);
  background: var(--flaeche-2);
  font-size: 0.84375rem;
  line-height: 1.6;
  color: var(--schrift-2);
}

.hinweis strong {
  color: var(--schrift);
  font-weight: 600;
}

/*
 * DERSELBE SATZ OHNE DEN KASTEN.
 *
 * Der Auftraggeber hat am 27.08.2026 die Kaesten im Mehr-Bogen beanstandet,
 * nicht die Saetze darin. Ein Kasten ist in dieser App die Form der MELDUNG
 * (Stoerband, Quittung, Zustandsanzeige); um eine Anleitung oder eine Zusage
 * gelegt macht er aus ihr eine Warnung, die keine ist.
 *
 * Gleiche Schriftgroesse und gleiche Tinte wie .hinweis, also dieselbe
 * gerechnete Lesbarkeit (--schrift-2 auf der deckenden Bogenflaeche: 7,32:1
 * hell, 8,52:1 dunkel). Was fehlt, ist Flaeche, Polsterung und Radius.
 */
.beitext {
  margin: 0 0 12px;
  font-size: 0.84375rem;
  line-height: 1.6;
  color: var(--schrift-2);
}

.beitext strong {
  color: var(--schrift);
  font-weight: 600;
}

/* ---------- Leerzustaende ---------- */

.leer {
  padding: 40px 20px;
  text-align: center;
  color: var(--schrift-2);
  font-size: 0.90625rem;
  line-height: 1.6;
}

.leer-gross {
  width: 60px;
  height: 60px;
  margin: 0 auto 14px;
  border-radius: 50%;
  background: var(--flaeche-2);
  color: var(--schrift-3);
  display: grid;
  place-items: center;
  font-size: 1.625rem;
  line-height: 1;
}

/* ---------- Einstellungen ---------- */

/*
 * DIE ZWISCHENUEBERSCHRIFT DER EINSTELLUNGEN -- neu am 29.08.2026.
 *
 * Vorher: `text-transform: uppercase`, 11 px, `--schrift-3`, 0,16em gesperrt,
 * also der Eyebrow des Bogenkopfes ein zweites Mal. Das hat zwei Dinge
 * gleichzeitig falsch gemacht. Erstens wiederholte es eine Marke an einer
 * Stelle, an der keine Marke steht. Zweitens war es die SCHWAECHSTE Schrift
 * der Seite ueber dem, was man tatsaechlich sucht -- die Gliederung war
 * leiser als der Fliesstext darunter.
 *
 * Jetzt traegt sie Schriftfarbe und Gewicht, und die Luft davor macht die
 * Arbeit, die vorher die Versalien machen sollten. 700 und -0,01em ist das
 * Gewicht des Bogentitels, eine gute Stufe kleiner gesetzt: die
 * Ueberschriften einer Liste gehoeren zu ihrem Titel und nicht zum Fliesstext.
 */
.abschnitt {
  margin: 32px 0 10px;
  font-size: 1.0625rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--schrift);
  line-height: 1.3;
}

.abschnitt:first-child {
  margin-top: 4px;
}

/*
 * DER SEGMENT-SCHALTER. EIN BAUTEIL, VIER STELLEN.
 *
 * WAS HIER STAND UND WARUM ES WEG IST. `.wahl` war keine Spur, sondern eine
 * Reihe Rechtecke mit -1 px Ueberlappung: harte Ecken, duenne Trennstriche,
 * das aktive Feld ein weisses Vollfeld ohne Rundung und ohne jede Bewegung.
 * Der Auftraggeber hat es am 27.08.2026 auf dem iPhone gesehen; sein Satz war
 * "sobald man die Einstellungen oeffnet, gibt es ganz, ganz viele Probleme".
 *
 * NEU IST DAS PRINZIP, NICHT DIE POLITUR: die Spur ist EIN Objekt, und darin
 * FAEHRT ein Indikator. Das Feld faerbt sich nicht mehr um; es tritt nur
 * vor, wenn der Indikator unter ihm ankommt. Deshalb gibt es auch keine
 * Trennstriche mehr -- getrennt wird durch Lage, nicht durch Linien.
 *
 * DIE GEOMETRIE IST GERECHNET (Quelle: gestalt/gestalt.json).
 *
 *   Spur           --ziel-h hoch (44 px), Radius --seg-r (14)
 *   Innenabstand   --seg-innen (3), rundum gleich
 *   Indikator      Radius --seg-r-innen (11) = 14 - 3, also konzentrisch
 *   Fahrt          --seg-dauer (220 ms) mit --seg-kurve
 *   Beschriftung   Farbwechsel in --d-druck (120 ms), sonst nichts
 *
 * Der Indikator faehrt mit `translate`, nicht mit `left`/`top`: nur transform
 * und opacity laufen ausserhalb des Layouts, und darunter steht in dieser App
 * eine WebGL-Karte.
 *
 * WARUM ZWEI ACHSEN. Bei 320 px und 200 Prozent Systemschrift passen drei
 * Beschriftungen nicht mehr nebeneinander (gemessen brauchte allein
 * "Haltestelle" 139 px in einem 73 px breiten Feld). Die Reihe wird dann zur
 * Spalte -- und der Indikator faehrt eben senkrecht statt waagerecht. Das ist
 * kein zweiter Bauteil und kein Sonderfall im Code: js/app.js misst das aktive
 * Feld und setzt --seg-x, --seg-y, --seg-b und --seg-hoehe. Wie herum die
 * Felder liegen, muss es dafuer nicht wissen.
 *
 * DER FEHLER, DER OHNE ResizeObserver ZURUECKKOMMT: der Indikator misst seine
 * Breite beim Einhaengen. Die Schriftbruecke der Huelle skaliert die
 * Systemtextgroesse DANACH -- "Dunkel" wird breiter, und die 220-ms-Fahrt
 * endet um die Differenz daneben. Der Beobachter steht in js/app.js
 * (`segmentSchalter`), diese Datei kann davon nichts wissen.
 */
.seg {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  /*
   * Der Zeilenabstand greift NUR in der umgebrochenen Spalte -- in einer
   * einzigen Reihe tut row-gap nichts. Er ist genau zweimal --seg-innen breit,
   * und damit stossen die 44-px-Auflagen der Felder (.seg-feld::after) dort
   * aneinander, statt sich zu ueberlappen. Ohne ihn gaebe es an jeder Fuge
   * einen 6 px hohen Streifen, in dem zwei Ziele uebereinanderliegen.
   */
  row-gap: calc(2 * var(--seg-innen));
  padding: var(--seg-innen);
  border-radius: var(--seg-r);
  background: var(--flaeche-2);
  /* Die Kontur ist Pflicht und nicht Zierde: die Spur ist ein grafisches
     Objekt im Sinn von WCAG 1.4.11, und --flaeche-2 gegen die Bogenflaeche
     allein traegt die geforderten 3:1 nicht. */
  border: 1px solid var(--kontur-feld);
}

/*
 * Der Indikator. Er liegt UNTER den Feldern (die tragen z-index 1) und
 * traegt deshalb keinen eigenen Text.
 *
 * Breite und Hoehe wandern mit, nicht nur die Lage: bei drei verschieden
 * langen Woertern ist jedes Feld anders breit, und ein Indikator fester
 * Breite stuende bei "Dunkel" ueber der Kante.
 */
.seg-indikator {
  position: absolute;
  /*
   * 0/0 UND NICHT --seg-innen. Der Innenabstand steckt schon in der Messung:
   * js/app.js rechnet die Lage des aktiven Feldes gegen die INNENkante der
   * Spur, und die liegt hinter der Polsterung. Hier stand --seg-innen, und
   * gemessen lag der Indikator dadurch bei jeder Breite und jeder
   * Schriftgroesse genau 3 px zu weit rechts und 3 px zu tief -- also um
   * exakt diesen Wert (docs/messung/glas/abzuege-28-08.mjs).
   */
  top: 0;
  left: 0;
  width: var(--seg-b, 0);
  height: var(--seg-hoehe, 0);
  border-radius: var(--seg-r-innen);
  background: var(--flaeche);
  /* Die Kontur ist Pflicht, nicht Zierde -- dieselbe Rechnung wie am
     Schalterknauf: --flaeche auf --flaeche-2 traegt 1,13:1 hell und 1,16:1
     dunkel, und WCAG 1.4.11 verlangt fuer ein grafisches Objekt 3:1. Die
     Kontur in --kontur traegt 4,05:1 hell und 4,61:1 dunkel gegen die Spur
     (docs/messung/glas/kontrast-28-08.mjs). Ohne sie waere der Indikator ein
     Schatten und sonst nichts. */
  border: 1px solid var(--kontur);
  box-shadow: var(--schatten-druck);
  transform: translate(var(--seg-x, 0px), var(--seg-y, 0px));
  transition:
    transform var(--seg-dauer) var(--seg-kurve),
    width var(--seg-dauer) var(--seg-kurve),
    height var(--seg-dauer) var(--seg-kurve);
  pointer-events: none;
}

/* Vor der ersten Messung steht der Indikator auf 0 x 0 und ist unsichtbar.
   Ohne diese Zeile blitzte bei jedem Oeffnen des Bogens ein Punkt links oben
   auf, bevor die Messung ihn an seinen Platz setzt. */
.seg:not([data-gemessen]) .seg-indikator {
  opacity: 0;
}

.seg-feld {
  position: relative;
  z-index: 1;
  /* 5,5rem Grundbedarf: bei normaler Schrift 88 px, drei davon passen in die
     284 px des schmalsten Bogens. Waechst die Systemschrift, waechst der
     Grundbedarf mit, und die Reihe bricht von selbst zur Spalte um. */
  flex: 1 1 5.5rem;
  min-width: 0;
  min-height: calc(var(--ziel-h) - 2 * var(--seg-innen));
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  padding: 6px 10px;
  overflow-wrap: break-word;
  hyphens: auto;
  border-radius: var(--seg-r-innen);
  font-weight: 500;
  font-size: 0.90625rem;
  color: var(--schrift-2);
  transition: color var(--d-druck) var(--ease-out);
}

/*
 * DAS KLICKZIEL IST 44 PX HOCH, DAS FELD NUR 38.
 *
 * Die Spur misst 44 px, davon gehen zweimal 3 px Innenabstand ab. Ein 38 px
 * hohes Ziel unterschreitet die Hausnorm (--ziel-h, WCAG 2.5.5). Die
 * fehlenden 6 px liegen aber im Innenabstand der Spur, und der gehoert
 * keinem anderen Ziel: diese Auflage holt sie zurueck, ohne die Geometrie
 * anzufassen. Waagerecht wird NICHT gedehnt -- dort stoesst das Nachbarfeld an.
 *
 * In der umgebrochenen Spalte stossen die Felder senkrecht aneinander. Dass
 * sich die Auflagen dort nicht ueberlappen, haelt der row-gap der Spur offen;
 * siehe dort.
 */
.seg-feld::after {
  content: "";
  position: absolute;
  inset: calc(-1 * var(--seg-innen)) 0;
}

.seg-feld[aria-pressed="true"] {
  color: var(--schrift);
  font-weight: 600;
}

/* Die zweite Zeile eines Feldes (im Melde-Ablauf die Zahl der Linien). Sie
   ist eine Auskunft, keine Beschriftung, und traegt deshalb die leise Tinte
   auch im gewaehlten Feld. */
.seg-feld small {
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--schrift-2);
}

.seg-feld:active {
  /* Kein Farbwechsel unter dem Finger: die Flaeche, die reagiert, ist der
     Indikator, und der faehrt gleich los. Zwei Rueckmeldungen auf einen Tipp
     sind eine zu viel. */
  opacity: 0.7;
}

/*
 * Wer Bewegung abbestellt hat, bekommt den Zustand ohne Fahrt: der Indikator
 * SPRINGT, nur die Farbe wechselt weich. Genau so steht es im Auftrag, und es
 * ist auch die einzige haltbare Lesart -- ein Indikator, der gar nicht mehr
 * umzieht, waere ein Schalter ohne Anzeige.
 */
@media (prefers-reduced-motion: reduce) {
  .seg-indikator {
    transition: none;
  }
}

/* --r-tafel wie jeder andere volle Knopf seit dem 29.08.2026. Die Kontur
   bleibt: sie ist hier nicht Begrenzung, sondern Warnung -- sie traegt die
   Signalfarbe, die die Flaeche bewusst nicht traegt. */
.gefahr-knopf {
  width: 100%;
  min-height: 50px;
  border-radius: var(--r-tafel);
  background: var(--flaeche);
  border: 1px solid var(--warn-schrift);
  color: var(--warn-schrift);
  font-weight: 600;
  font-size: 0.90625rem;
}

/* Zweiter Tipp: jetzt sieht der Knopf aus wie das, was er tut. */
.gefahr-knopf[data-sicher] {
  background: var(--warn);
  border-color: var(--warn);
  color: var(--auf-warn);
}

.fussnote {
  font-size: 0.75rem;
  color: var(--schrift-2);
  line-height: 1.6;
  margin: 16px 0 0;
}

/* ---------- Knopf im Bogen und im Tor ---------- */

/*
 * Der Hauptknopf traegt Tinte, nie eine semantische Farbe: seine Handlung sagt
 * nichts aus. Genau dieser Fehler ist in Puffer dokumentiert und behoben
 * worden (Design.swift:1041-1048).
 */
/*
 * DIE RUNDUNG IST AM 29.08.2026 AUF --r-tafel GEGANGEN, hier wie an .tut-haupt
 * und an den Gruppen der Einstellungen. Der Grund ist derselbe: --r-feld ist
 * die Ecke eines EINGABEFELDES, und die App fuehrt sie an Feldern weiter. Eine
 * 56 px hohe Flaeche ueber die volle Breite ist kein Feld, und mit 12 px Ecke
 * war sie die letzte kastige Form auf einem Schirm, auf dem sonst alles
 * gerundet ist. Der Auftraggeber hat das Tor am 29.08.2026 unter "sieht noch
 * aus wie das erste App-Design" mitgemeint.
 */
.tor-knopf {
  display: block;
  width: 100%;
  min-height: var(--tor-knopf-h);
  border-radius: var(--r-tafel);
  background: var(--schrift);
  color: var(--grund);
  font-weight: var(--t-tor-knopf-gewicht);
  font-size: var(--t-tor-knopf-groesse);
  letter-spacing: var(--t-tor-knopf-sperrung);
  transition:
    transform var(--d-druck) var(--ease-out),
    opacity var(--d-schnell) var(--ease-out);
}

.tor-knopf:active:not(:disabled) {
  transform: scale(0.96);
}

.tor-knopf:disabled {
  opacity: var(--tor-knopf-aus);
  cursor: not-allowed;
}

.tor-knopf.zweitrangig {
  background: var(--flaeche);
  /* Erbt --r-tafel von .tor-knopf; die Kontur bleibt aus demselben Grund wie
     an .tor-punkt (1,05:1 gegen den Grund). */
  color: var(--schrift);
  border: 1px solid var(--kontur-feld);
  font-weight: 500;
}

.tor-knopf.zweitrangig:active {
  background: var(--flaeche-3);
}

/* ---------- Quittung ---------- */

/*
 * WAS NACH DEM ABSENDEN IM BOGEN STEHT, und warum es so leise aussieht.
 *
 * Der Nutzer steht in einer fahrenden Bahn und will nicht auffallen. Ein
 * Erfolgsbildschirm mit grossem Haken auf gruener Flaeche waere in genau dem
 * Moment das hellste Bild der ganzen App, und zwar im Dunkelmodus erst recht.
 * Deshalb traegt die Quittung kein eigenes Gruen, kein eigenes Rot und keine
 * eigene Schriftgroesse: Sie ist die Bogenflaeche eine Stufe abgesetzt
 * (--flaeche-2), mit demselben Feldradius wie jedes andere Feld im Bogen.
 *
 * Die sichtbare Rueckmeldung liegt woanders, naemlich dort, wo die Meldung
 * hingehoert: der Ring am Punkt auf der Karte und die Zeile in der Lagetafel.
 * Hier steht nur, was rausgegangen ist -- und das ist nach drei Tipps mehr wert
 * als eine Feier, weil niemand mehr weiss, ob er die Richtung erwischt hat.
 */
.quittung {
  background: var(--flaeche-2);
  border-radius: var(--r-feld);
  padding: 12px 14px;
  display: grid;
  gap: 6px;
}

.quittung-kopf {
  display: flex;
  align-items: center;
  gap: 9px;
  min-width: 0;
}

/* Der Ortsname ist die Hauptangabe des Belegs, also so gross wie eine
   Listenzeile. Er darf umbrechen: "Hamburg Airport (Flughafen)" abzuschneiden
   waere die eine Angabe zu kuerzen, auf die es hier ankommt. */
.quittung-ort {
  min-width: 0;
  font-size: 0.9375rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--schrift);
  overflow-wrap: anywhere;
}

.quittung-zeile {
  margin: 0;
  font-size: 0.84375rem;
  font-weight: 400;
  line-height: 1.35;
  color: var(--schrift-2);
  text-wrap: pretty;
}

/* Knopf, Restzeit, Knopf. 8 px sind die Untergrenze fuer die Fuge zwischen zwei
   Zielen; hier stehen zwischen den beiden Knoepfen ausserdem die zwei Zeilen
   der Uhr, es sind also nie weniger. */
.quittung-taten {
  display: grid;
  gap: 10px;
  margin-top: 14px;
}

/*
 * DIE RESTZEIT DER MELDESPERRE, und warum sie NICHT im gesperrten Knopf steht.
 *
 * Der naechstliegende Platz waere die Beschriftung von "Noch eine melden".
 * `.tor-knopf:disabled` setzt aber opacity auf 0,35, und damit faellt jeder
 * gemessene Kontrast: Tinte auf der Bogenflaeche kaeme so auf rund 2,3:1. Fuer
 * ein abgeschaltetes Bedienelement erlaubt WCAG 1.4.3 das ausdruecklich, fuer
 * die AUSKUNFT darin nicht -- und die Restzeit ist die eigentliche Auskunft
 * dieses Zustands. Sie steht deshalb als eigene Zeile darunter, in voller
 * Deckung und in --schrift-2 (auf der Bogenflaeche gemessen 5,41:1 hell und
 * 6,32:1 dunkel, siehe die Rechnung bei .linien-kachel small).
 *
 * `tabular-nums`, damit die Sekunden nicht bei jedem Tick die Zeile umbrechen.
 */
.sperr-uhr {
  margin: 0;
  font-size: 0.84375rem;
  font-weight: 400;
  line-height: 1.4;
  color: var(--schrift-2);
  font-variant-numeric: tabular-nums;
  text-wrap: pretty;
}

/* Frei heisst: der Knopf darueber ist offen. Halbfett und in voller Tinte,
   damit der Wechsel auch ohne Blick auf den Knopf ankommt. Keine Farbe: Gruen
   hiesse in dieser App "keine Meldung" (siehe RISIKO_STUFEN in
   js/kartenstil.js), und diese Zeile sagt etwas ganz anderes. */
.sperr-uhr[data-frei] {
  color: var(--schrift);
  font-weight: 600;
}

/* ---------- Text-Knopf ---------- */

.text-knopf {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
  /* Grosszuegige senkrechte Polsterung: sie vergroessert die Trefflaeche auf
     ueber 40 px, ohne den Zeilenabstand im Fliesstext zu veraendern. */
  padding: 11px 4px;
  margin-inline: -4px;
  font-size: inherit;
  font-weight: 600;
}

/* ---------- Bestaetigungs-Tor ---------- */

.tor {
  position: fixed;
  inset: 0;
  background: var(--grund);
  z-index: 50;
  display: flex;
  flex-direction: column;
  /*
   * HIER STAND `justify-content: center`, UND DAS WAR DIE SCHWERSTE STELLE DER
   * GANZEN DATEI. Zentriert ein Flexcontainer seinen Inhalt und der Inhalt ist
   * hoeher als der Container, waechst er ueber BEIDE Kanten. Nach unten laesst
   * sich scrollen, nach oben nicht: scrollTop kann nicht unter 0. Gemessen bei
   * 320 x 568 und 200 Prozent Systemschrift lag die Wortmarke bei top = -627
   * und die H1 bei -550, waehrend scrollTop = 0 zugleich das Minimum und der
   * Anfang war. 627 px waren unerreichbar, darunter der Anfang der Aufklaerung,
   * die der Nutzer bestaetigen MUSS, um in die App zu kommen. Schon bei 100
   * Prozent fehlten 36 px.
   *
   * Die Zentrierung macht jetzt `margin: auto` an .tor-innen. Das ist kein
   * Ersatz aus Geschmack, sondern die eine Fassung, die der Flexbox-Spezifikation
   * nach sicher ist: automatische Aussenabstaende verteilen nur FREIEN Platz,
   * und ist der Platz negativ, werden sie zu Null. Der Inhalt steht dann oben
   * an und laeuft nach unten heraus, wo er scrollbar ist.
   */
  padding: var(--tor-oben) var(--tor-seite) calc(var(--tor-oben) + env(safe-area-inset-bottom));
  overflow-y: auto;
  overflow-x: clip;
  /* Das Tor liegt ueber allem; ein Wisch darin soll nicht die Seite darunter
     mitziehen. */
  overscroll-behavior: contain;
}

.tor-innen {
  width: 100%;
  max-width: var(--tor-breite);
  /* Siehe .tor: waagerecht zentrieren wie vorher, senkrecht sicher zentrieren
     statt mit justify-content. */
  margin: auto;
}

.tor-marke {
  margin: 0 0 var(--tor-marke-unten);
}

/* Die Heldenzeile: gross und eng, gegen den winzigen weiten Eyebrow darueber. */
.tor h1 {
  margin: 0 0 var(--tor-titel-unten);
  font-size: clamp(var(--tor-titel-min), var(--tor-titel-vw), var(--t-tor-titel-groesse));
  font-weight: var(--t-tor-titel-gewicht);
  letter-spacing: var(--t-tor-titel-sperrung);
  line-height: var(--t-tor-titel-zeilenhoehe);
  /* Bei 200 Prozent Systemschrift auf 320 px steht die Zeile in 56 px Schrift,
     und "bestaetigen," braucht dann 283 px in einer 280 px breiten Spalte.
     break-word trennt erst, wenn ein Wort allein nicht mehr passt; die Sprache
     steht als lang="de" am <html>, also trennt hyphens sinnvoll. */
  overflow-wrap: break-word;
  hyphens: auto;
}

.tor-vorspann {
  margin: 0 0 var(--tor-vorspann-unten);
  color: var(--schrift-2);
  font-size: var(--t-tor-vorspann-groesse);
  line-height: var(--t-tor-vorspann-zeilenhoehe);
}

/*
 * DIE KONTUR BLEIBT, DIE ECKE WIRD WEICHER -- 29.08.2026.
 *
 * Naheliegend waere gewesen, auch hier die Kontur abzunehmen und die Flaeche
 * tragen zu lassen, wie es die Gruppen der Einstellungen tun. Das geht an
 * dieser Stelle nicht, und zwar gerechnet: die Karte steht auf --grund
 * (#f4f6f8), ihre Flaeche ist --flaeche (#ffffff), das sind 1,05:1. WCAG
 * 1.4.11 verlangt 3:1 fuer die Begrenzung eines Bedienelements. Ohne Kontur
 * waere die Karte auf einem hellen Schirm schlicht nicht zu sehen -- und die
 * Gruppen der Einstellungen duerfen es, weil sie KEINE Bedienelemente
 * umschliessen, sondern nur gliedern.
 *
 * Geaendert ist deshalb nur die Ecke, und die aus demselben Grund wie am
 * Knopf darunter: --r-feld ist die Ecke eines Eingabefeldes.
 */
.tor-punkt {
  display: flex;
  /* Traegt das verborgene Feld, das `position: absolute` bekommt. Ohne diese
     Zeile klettert es an den naechsten positionierten Vorfahren und nimmt den
     Fokusring mit. */
  position: relative;
  gap: var(--tor-punkt-fuge);
  align-items: flex-start;
  padding: var(--tor-punkt-innen);
  border-radius: var(--r-tafel);
  background: var(--flaeche);
  border: 1px solid var(--kontur-feld);
  cursor: pointer;
  margin-bottom: var(--tor-punkt-unten);
  transition: transform var(--d-druck) var(--ease-out);
}

.tor-punkt:active {
  transform: scale(0.99);
  background: var(--flaeche-2);
}

/* Angehakt ist ein Zustand, den man aus einem Meter Entfernung sehen muss. */
.tor-punkt:has(input:checked) {
  border-color: var(--schrift);
  box-shadow: inset 0 0 0 1px var(--schrift);
}

/*
 * WAS HIER FRUEHER STAND, und warum es nicht verloren ist: ein Kaestchen mit
 * Haken, gezeichnet von dieser Datei statt von der Engine. Grund war eine
 * Messung vom 18.08.2026 in einer echten WKWebView -- mit `accent-color`
 * allein war der GESETZTE Zustand der leisere (Helligkeit 188,7 gegen 246,4),
 * die Rueckmeldung lief also gegen die Erwartung. Diese Lehre gilt weiter und
 * steht in docs/24-signetkontrast.md; das Kaestchen selbst ist am 30.08.2026
 * dem Schieber gewichen.
 */
/*
 * DER SCHIEBER, UND WARUM ER SEIT DEM 30.08.2026 KEIN HAKEN MEHR IST.
 *
 * Hier stand ein Kaestchen mit Haken, gezeichnet von dieser Datei statt von
 * der Engine (die Messung dazu steht weiter unten in der Geschichte dieser
 * Regel). Es funktionierte, war aber die falsche FORM: Ein Haken ist eine
 * Zustimmung nebenbei -- man setzt ihn im Vorbeigehen, wie in einem Formular.
 * Ein Schieber ist eine Handlung, und genau das soll die Bestaetigung sein.
 *
 * Es ist bewusst dieselbe Form wie bei Ortung und Mitteilungen (.schalter oben,
 * 46 x 28, Knauf 22, Weg 18 px): In dieser App heisst diese Form „ich schalte
 * das jetzt bewusst ein“. Zwei Formen fuer dieselbe Aussage waeren zwei
 * Sprachen.
 *
 * Das `input` bleibt eine Checkbox unter dem Schieber: Es traegt den Zustand,
 * den `torZustand()` in js/app.js fuer die native Huelle liest, und es bringt
 * Tastatur, Fokus und Vorlesestimme mit. `role="switch"` im Markup sagt der
 * Vorlesestimme, was die Augen sehen. Sichtbar ist es nicht -- gezeichnet wird
 * `.tor-schieber` daneben.
 */
.tor-punkt input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  margin: 0;
  pointer-events: none;
}

.tor-schieber {
  flex: none;
  width: var(--schalter-gleis-b);
  height: var(--schalter-gleis-h);
  border-radius: calc(var(--schalter-gleis-h) / 2);
  background: var(--flaeche-3);
  border: 1px solid var(--kontur);
  display: flex;
  align-items: center;
  padding: var(--schalter-gleis-innen);
  /* Der Schieber sitzt rechts, der Text links: dieselbe Leserichtung wie in
     den Einstellungen, wo die Zeile erst sagt WAS und dann OB. */
  margin-left: auto;
  align-self: center;
  transition: background-color var(--d-druck) var(--ease-out);
}

.tor-schieber::before {
  content: "";
  width: var(--schalter-knauf);
  height: var(--schalter-knauf);
  border-radius: 50%;
  background: var(--flaeche);
  border: 1px solid var(--kontur);
  box-shadow: var(--schatten-druck);
  transition: transform var(--d-druck) var(--seg-kurve);
}

.tor-punkt:has(input:checked) .tor-schieber {
  background: var(--schrift);
  border-color: var(--schrift);
}

.tor-punkt:has(input:checked) .tor-schieber::before {
  transform: translateX(var(--schalter-weg));
  border-color: var(--grund);
}

/* Der Fokusring haengt am verborgenen Feld, muss aber an der KARTE sichtbar
   werden -- sonst tabbt man auf ein Element, das nichts anzeigt. Dasselbe
   Muster wie :focus-visible ganz oben: der sichtbare Ring ist ein Schatten,
   die transparente Kontur bleibt fuer den Windows-Kontrastmodus stehen. */
.tor-punkt:has(input:focus-visible) {
  outline: 3px solid transparent;
  outline-offset: 3px;
  box-shadow: var(--fokus-ring);
}

@media (prefers-reduced-motion: reduce) {
  .tor-schieber,
  .tor-schieber::before {
    transition: none;
  }
}

.tor-punkt span {
  font-size: var(--t-tor-punkt-groesse);
  line-height: var(--t-tor-punkt-zeilenhoehe);
  /* Ohne min-width: 0 kann ein Flexkind nicht unter seine kleinste Wortbreite
     schrumpfen, und der Text schob die Karte bei 200 Prozent um gemessene
     22 px ueber ihren eigenen Rand. */
  min-width: 0;
  overflow-wrap: break-word;
  hyphens: auto;
}

.tor-innen .tor-knopf {
  margin-top: var(--tor-knopf-oben);
}

/*
 * ZEICHNET DIE HUELLE DAS TOR, TRITT DIE SEITE ZURUECK.
 *
 * Dieselbe Regel und derselbe Griff wie beim HUD (:root.hud-nativ-*):
 * `visibility` und nicht `hidden`, denn der Kasten muss stehen bleiben. Zwei
 * Gruende: die Seite liest den Wortlaut weiter aus dem Dokument, und
 * `click()` traegt auf einer unsichtbaren Flaeche weiter -- genau darauf
 * beruht der Rueckweg der Bruecke.
 *
 * Die Klasse haengt daran, ob die Huelle WIRKLICH zeichnet, nicht daran, ob
 * sie es koennte. Sie meldet es selbst (`__kpTorNativ`). Bleibt die Antwort
 * aus, bleibt das Web-Tor stehen.
 */
:root.tor-nativ .tor {
  visibility: hidden;
}

/*
 * IMPRESSUM UND DATENSCHUTZ STEHEN IM TOR, und ihr Bogen liegt auf z-index 21,
 * das Tor auf 50. Ohne diese beiden Zeilen oeffnete sich der Text also HINTER
 * der Pflichtabfrage: der Nutzer tippte, und nichts geschah. Dazu war der
 * Bogen bis heute in TOR_DAHINTER als `inert` gefuehrt, lag also unerreichbar
 * hinter einer Flaeche, die genau auf ihn verweist.
 */
:root.tor-offen .bogen-grund {
  z-index: 51;
}

:root.tor-offen .bogen {
  z-index: 52;
}

/*
 * DER FUSS TRAEGT --schrift-2 UND NICHT --schrift-3, und das ist nachgerechnet
 * und nicht Geschmack (docs/messung/tor-nativ/kontrast.mjs):
 *
 *   --schrift-3 auf --grund   4,99:1 hell   7,31:1 dunkel
 *   --schrift-2 auf --grund   6,76:1 hell   9,85:1 dunkel
 *
 * WCAG 1.4.3 waere mit 4,99 erfuellt. Die Hausschranke fuer leise Schrift aus
 * dem Auftrag (5,78:1 hell) aber nicht -- und in dieser Zeile stehen die
 * beiden Datenlizenzen (ODbL, dl-de/by-2-0) und die Wege zu Impressum und
 * Datenschutz. Eine Pflichtangabe ist der falsche Ort, um vier Zehntel
 * Kontrast zu sparen.
 *
 * Der Gewinn ist gratis, weil das Tor DECKEND ist: die dritte Tintenstufe
 * existiert ueberhaupt nur, weil deckende Flaechen sie tragen koennen
 * (docs/29-materialgesetz.md) -- hier ist der Untergrund aber --grund und
 * nicht --flaeche, und das sind die 0,42 Punkte Unterschied.
 *
 * .tor-marke daneben BEHAELT --schrift-3, ebenfalls mit Grund: sie ist ein
 * Eyebrow in Versalien, sie steht ueber der Heldenzeile, die dasselbe sagt,
 * und sie teilt ihre Regel mit .abschnitt, .bogen-unter und .text h3. Wer sie
 * anhebt, hebt vier Flaechen an, von denen drei auf --flaeche stehen.
 */
.tor-fuss {
  margin: var(--tor-fuss-oben) 0 0;
  font-size: var(--t-tor-fuss-groesse);
  color: var(--schrift-2);
  line-height: var(--t-tor-fuss-zeilenhoehe);
  text-transform: none;
  letter-spacing: 0;
  font-weight: 400;
}

/* ---------- Textseiten ---------- */

.text {
  max-width: 62ch;
}

/* Dieselbe Ueberschrift wie .abschnitt in den Einstellungen: gliedert eine
   lange Seite, ohne eine Marke zu wiederholen. */
.text h3 {
  margin: 28px 0 10px;
  font-size: 1.0625rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--schrift);
  line-height: 1.3;
}

.text h3:first-child {
  margin-top: 0;
}

.text p {
  font-size: 0.90625rem;
  line-height: 1.65;
  color: var(--schrift-2);
  margin: 0 0 12px;
}

.text .platzhalter {
  padding: 13px 15px;
  border-radius: var(--r-feld);
  background: var(--flaeche-2);
  border-left: 5px solid var(--warn);
  color: var(--schrift);
  font-weight: 500;
}

.daten {
  margin: 0 0 12px;
  font-size: 0.90625rem;
  line-height: 1.65;
}

.daten dt {
  font-weight: 600;
  margin-top: 12px;
}

.daten dd {
  margin: 0;
  color: var(--schrift-2);
}

.daten dd.platzhalter {
  color: var(--schrift);
  font-weight: 500;
}

/*
 * DER PLATZHALTERKASTEN IM IMPRESSUM SCHNITT SEINE EIGENE BESCHRIFTUNG AN.
 *
 * Belegt am Abzug docs/messung/tor-nativ/07-tor-impressum.png: ueber jedem
 * roten [PLATZHALTER]-Kasten steht "Anbieter", "Anschrift", "E-Mail" -- und
 * jedes dieser Woerter ist unten abgeschnitten, weil der Kasten darueber liegt.
 *
 * DIE URSACHE IST NICHT DER ABSTAND, SONDERN DER KASTENTYP. js/seiten.js setzt
 * den Platzhalter im Impressum als `<span class="platzhalter">` INNERHALB des
 * dd (in der Datenschutzseite dagegen als ganzes dd, dort ist es ein Block und
 * dort stimmt es auch). `.text .platzhalter` gibt ihm 13 px Innenabstand oben
 * und unten -- und der senkrechte Innenabstand einer INLINE-Box macht die
 * Zeile nicht hoeher, er wird nur gemalt. Der Kasten waechst also nach oben
 * ueber die Zeile hinaus und legt sich auf das dt darueber.
 *
 * `inline-block` gibt ihm eine eigene Hoehe, ohne ihn auf die volle Breite zu
 * ziehen: der Kasten bleibt so breit wie sein Wort, so wie er gezeichnet ist.
 * Gemessen nach der Aenderung, 393 x 852: kein dt wird mehr ueberdeckt.
 */
.daten dd > .platzhalter {
  display: inline-block;
}

/* ---------- Kartenbeschriftung (DOM statt Glyphen) ---------- */

.karten-namen {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 2;
  contain: strict;
}

.karten-name {
  position: absolute;
  top: 0;
  left: 0;
  white-space: nowrap;
  font-size: 0.71875rem;
  font-weight: 600;
  letter-spacing: -0.005em;
  color: var(--karte-schrift);
  /* Halo per Textschatten, damit die Namen ueber jedem Untergrund lesbar sind.
     Vier Richtungen reichen, acht kosten spuerbar Rechenzeit. */
  text-shadow:
    0 1px 2px var(--karte-grund),
    0 -1px 2px var(--karte-grund),
    1px 0 2px var(--karte-grund),
    -1px 0 2px var(--karte-grund);
  will-change: transform;
}

.karten-name[data-umsteige] {
  font-weight: 700;
  font-size: 0.75rem;
  color: var(--karte-schrift-stark);
}

.karten-name[data-gewaehlt] {
  color: var(--akzent-kontrast);
  font-weight: 800;
}

/* ---------- Meldungs-Plaketten (Altbestand) ---------- */

/*
 * Diese Auflagen werden gerade in js/karte.js ersetzt und bringen ihre Stile
 * selbst mit. Die Regeln bleiben stehen, damit bis dahin nichts nackt ist.
 */
.karten-plaketten {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 3;
  contain: strict;
}

.plakette {
  position: absolute;
  top: 0;
  left: 0;
  height: 30px;
  padding: 0 6px 6px;
  display: flex;
  align-items: center;
  gap: 4px;
  border-radius: 7px;
  background: var(--karte-grund);
  border: 2px solid var(--warn);
  box-shadow: 0 2px 8px var(--schatten-hart);
  will-change: transform;
}

.plakette-zahl {
  font-size: 0.78125rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  color: var(--karte-schrift-stark);
}

.plakette-zahl:empty {
  display: none;
}

.plakette-streifen-reihe {
  display: flex;
  align-items: center;
  gap: 2px;
}

.plakette-streifen {
  width: 5px;
  height: 15px;
  border-radius: 1px;
  flex: none;
  box-shadow: inset 0 0 0 1px var(--kapsel-saum);
}

.plakette-rest {
  position: absolute;
  left: 3px;
  right: 3px;
  bottom: 3px;
  height: 4px;
  background: var(--warn);
  border-radius: 2px;
  transform-origin: left center;
  transition: transform var(--d-schnell) var(--ease-out);
}

.plakette[data-bus]::after {
  content: "BUS";
  font-size: 0.53125rem;
  font-weight: 800;
  letter-spacing: 0.06em;
  line-height: 1;
  color: var(--karte-schrift-stark);
  padding-left: 1px;
}

.standort-punkt {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--standort);
  border: 3px solid var(--standort-rand);
  box-shadow: 0 0 0 1px var(--karte-schrift);
}

/* ---------- Das Blatt an der Haltestelle ---------- */

/*
 * WARUM EIN BLATT UND KEIN BOGEN.
 *
 * Der Auftrag lautete "jede Haltestelle soll dann kurz aufploppen mit
 * Meldefunktion und Namen". Ein Bogen von unten koennte dasselbe zeigen, und er
 * waere billiger zu haben -- es gibt ihn schon. Er beantwortet aber eine andere
 * Frage: Ein Bogen ist ein ORT, an den man geht, und waehrend er offen steht,
 * ist die Karte weg. Genau das darf hier nicht passieren: Wer eine Haltestelle
 * antippt, fragt "was ist HIER", und das Hier ist die Karte darum herum. Ein
 * Blatt an der Stelle laesst sie stehen und zeigt mit einer Nase auf den Ort,
 * den es meint.
 *
 * Gemessen ist der Unterschied auch, an derselben Haltestelle bei 393 x 852:
 * der Mehr-Bogen deckt 88,0 Prozent des Bildes ab, das Blatt 23,7.
 *
 * ES IST DECKEND, obwohl es auf der Karte liegt, und das ist kein Verstoss
 * gegen das Materialgesetz, sondern seine Anwendung (docs/29-materialgesetz.md
 * fuehrt es in der Tabelle). Drei Gruende, alle drei gemessen oder gerechnet:
 * es traegt Text auf den kleinen Stufen der Skala, und auf Glas ueber dem
 * schlechtesten Kartenuntergrund faellt --schrift-3 auf 3,90:1; es waere die
 * VIERTE Lage mit backdrop-filter ueber einer laufenden WebGL-Karte; und es
 * steht Sekunden, nicht dauernd -- Glas ist in dieser App das Material der
 * Werkzeuge, die immer da sind.
 *
 * DIE ECKE BLEIBT KREISBOGIG, wie beim Toast und aus demselben Grund: bei
 * --r-tafel betraegt der Abstand zu Apples Ecke gemessene 3,59 pt, und eine
 * Maske (mask-border) schneidet alles weg, was ausserhalb der Box liegt -- hier
 * also die Nase und den Schwebeschatten. Der Preis waere hoeher als der Gewinn.
 */
.halt-anker {
  position: absolute;
  left: 0;
  top: 0;
  width: 0;
  height: 0;
  /* Ueber Beschriftung (2) und Meldungspunkten (3), aber innerhalb von #karte:
     die Kartenflaeche ist position:fixed und damit ein eigener Stapelraum, das
     Blatt kann sich also nie ueber die Bedienung schieben (.oben/.unten, 10). */
  z-index: 4;
}

/*
 * ALLE MASSE DIESER FLAECHE IN REM, UND DAS IST DIE AUSNAHME VON DER HAUSREGEL.
 *
 * Die Regel dieser Datei lautet "Schriftgroessen in rem, Laengen in px", und
 * sie ist richtig: ein 44-px-Ziel bleibt 44 px, auch wenn die Schrift waechst.
 * Ein Blatt ist aber kein Ziel, sondern ein Behaelter fuer Text -- und seit die
 * Huelle die Systemtextgroesse in die Grundgroesse spiegelt (bis Faktor 2,0),
 * waere ein in px vermasstes Blatt bei 200 Prozent randvoll mit Schrift, die
 * ueber seine Kanten laeuft. Innenabstand, Hoechstbreite und Fugen wachsen
 * deshalb mit. Was NICHT mitwaechst, ist die Untergrenze der Ziele darin: der
 * Schliessknopf bleibt 44 px, der Meldeknopf behaelt --tor-knopf-h.
 */
.halt-blatt {
  position: absolute;
  left: var(--blatt-links, 0px);
  /*
   * DER ABSTAND ZUM ORT BLEIBT IN PIXELN, als einziges Mass dieser Flaeche.
   * Er ist keine Textumgebung, sondern die Fuge zwischen Blatt und Nase, und
   * er steht ausserdem ein zweites Mal in js/karte.js (BLATT_ABSTAND) -- dort
   * wird gerechnet, ob das Blatt ueber oder unter den Ort passt. In rem waren
   * es bei 200 Prozent Systemschrift 32 px hier gegen 16 px dort, und das
   * Blatt landete gemessene 14 px zu hoch, also unter der Kopftafel.
   */
  bottom: 16px;
  width: max-content;
  /*
   * DIE UNTERGRENZE MUSS SELBST EINE OBERGRENZE HABEN, und das ist am Abzug
   * aufgefallen, nicht beim Lesen: `min-width: 10rem` sind bei 200 Prozent
   * Systemschrift 320 px, und `min-width` schlaegt `max-width`. Auf einem
   * 320-px-Schirm stand das Blatt damit 320 px breit im 304 px breiten Freiraum
   * -- gemessen 8 px links, rechte Kante bei 328. Die Klemme nach oben gehoert
   * deshalb in beide Werte.
   */
  min-width: min(10rem, 55vw);
  max-width: min(19rem, 84vw);
  padding: 0.75rem 0.875rem 0.875rem;
  border-radius: var(--r-tafel);
  background: var(--flaeche);
  border: var(--kontur-tafel);
  box-shadow: var(--schatten-schwebend);
  pointer-events: auto;
  /* Der Ursprung sitzt an der Nase: das Blatt waechst aus seinem Ort heraus
     und nicht aus seiner eigenen Mitte. */
  transform-origin: var(--nase-x, 50%) 100%;
  animation: halt-blatt-auf var(--d-schnell) var(--ease-out) both;
}

.halt-anker[data-lage="unten"] .halt-blatt {
  bottom: auto;
  top: 16px;
  transform-origin: var(--nase-x, 50%) 0;
}

/*
 * Die Nase. Ein Dreieck per clip-path und nicht das gedrehte Quadrat, das man
 * sonst dafuer nimmt: das Quadrat muesste unter der Flaeche liegen, damit seine
 * innere Haelfte verschwindet, und ein Kind kann nicht hinter den Hintergrund
 * seines eigenen Stapelraums. Das Dreieck stoesst stattdessen genau an die
 * Kante und braucht keine Ueberlappung.
 */
.halt-nase {
  position: absolute;
  left: var(--nase-x, 50%);
  bottom: -9px;
  margin-left: -9px;
  width: 18px;
  height: 9px;
  background: var(--flaeche);
  clip-path: polygon(0 0, 100% 0, 50% 100%);
}

.halt-anker[data-lage="unten"] .halt-nase {
  bottom: auto;
  top: -9px;
  clip-path: polygon(50% 0, 0 100%, 100% 100%);
}

/* Aufploppen: 160 ms, nur transform und opacity, ease-out. Kein Federn und kein
   Ueberschwingen -- das Blatt erscheint, es huepft nicht. */
@keyframes halt-blatt-auf {
  from {
    opacity: 0;
    transform: scale(0.92);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .halt-blatt {
    animation: none;
  }
}

.halt-kopf {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
}

.halt-art {
  margin: 0;
  font-size: var(--t-eyebrow-groesse);
  font-weight: var(--t-eyebrow-gewicht);
  letter-spacing: var(--t-eyebrow-sperrung);
  text-transform: uppercase;
  color: var(--schrift-3);
}

/*
 * Der Name darf zwei Zeilen haben und wird danach abgeschnitten -- dieselbe
 * Regel wie bei der Lagetafel. "Bf. Wandsbeker Chaussee" braucht bei 320 px und
 * 200 Prozent Schrift mehr als eine Zeile, und ein Blatt, das dafuer beliebig
 * hoch wird, deckt die Karte ab, die es zeigen soll.
 */
.halt-name {
  margin: 0.125rem 0 0;
  font-size: var(--t-lage-titel-groesse);
  font-weight: var(--t-lage-titel-gewicht);
  letter-spacing: var(--t-lage-titel-sperrung);
  line-height: var(--t-lage-titel-zeilenhoehe);
  color: var(--schrift);
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}

.halt-blatt .linien-reihe {
  margin-top: 0.5rem;
}

/*
 * Die Lagezeile steht NUR, wenn dort wirklich etwas liegt. Ein "keine Meldung"
 * an jeder der 9131 Haltestellen waere genau die Sorte Zeile, von der diese App
 * gerade dreizehn losgeworden ist -- und die Karte sagt es ohnehin: wo nichts
 * gemeldet ist, liegt kein Punkt.
 */
.halt-lage {
  margin: 0.625rem 0 0;
  font-size: var(--t-naehe-lage-groesse);
  font-weight: var(--t-naehe-lage-gewicht);
  letter-spacing: var(--t-naehe-lage-sperrung);
  color: var(--schrift-2);
}

.halt-lage[data-frisch] {
  color: var(--warn-schrift);
}

/* "+18" hinter den ersten sechs Signeten. Es steht IN der Signetreihe und
   traegt deshalb deren Ausrichtung, nicht die einer eigenen Zeile. */
.halt-mehr-linien {
  align-self: center;
  font-size: var(--t-naehe-lage-groesse);
  font-weight: var(--t-naehe-lage-gewicht);
  color: var(--schrift-2);
}

.halt-tat {
  margin-top: 0.75rem;
}

/*
 * Die Nachbarzeile: was noch im selben Trefferfeld lag. Sie ist kein Schmuck,
 * sondern der Ausweg aus der Ueberlappung -- bei Zoom 13 hat jeder dritte Ort
 * einen Nachbarn im Feld (die Zaehlung steht bei TREFFERFELD in js/karte.js),
 * und zwei Paare teilen sich sogar die Koordinate. Ohne diese Zeile waeren die
 * zweiten von beiden mit dem Finger nicht erreichbar.
 */
.halt-nachbarn {
  margin-top: 0.625rem;
  padding-top: 0.5rem;
  border-top: 1px solid var(--linie);
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.halt-nachbar {
  display: block;
  width: 100%;
  min-height: 44px;
  padding: 0 0.5rem;
  text-align: left;
  border-radius: var(--r-feld);
  color: var(--schrift-2);
  font-size: var(--t-lage-ort-groesse);
  font-weight: 500;
  overflow-wrap: anywhere;
}

.halt-nachbar:active {
  background: var(--flaeche-2);
}

/* MapLibre bringt eigene Steuerelemente mit, die wir nicht nutzen. */
.maplibregl-ctrl-bottom-left,
.maplibregl-ctrl-bottom-right,
.maplibregl-ctrl-top-right {
  display: none !important;
}

/*
 * DER KARTENAUSSCHNITT IST DER ERSTE TABSTOPP DER APP. MapLibre haengt
 * tabindex="0" an den Canvas, und er ist mit den Pfeiltasten bedienbar. Hier
 * stand `outline: none`, also war der erste Halt der Tastaturbedienung
 * unsichtbar. Zurueckgenommen ist das nicht durch die Grundregel: die haette
 * der Canvas zwar getroffen, aber .maplibregl-map schneidet mit overflow:
 * hidden auf allen vier Seiten je 5 px weg, und ein Schatten AM Canvas selbst
 * wuerde von dessen eigener Bitmap verdeckt - Inhalt liegt ueber inset-Schatten.
 *
 * Deshalb zeichnet ihn die Kartenflaeche selbst, als eigene Lage ueber der
 * Karte und ueber den Beschriftungen (z-index 2), ohne Klickziel zu werden.
 */
.maplibregl-canvas:focus,
.maplibregl-canvas:focus-visible {
  outline: none;
  box-shadow: none;
}

#karte:has(.maplibregl-canvas:focus-visible)::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  box-shadow: var(--fokus-ring-innen);
}

/* ---------- Schmale Schirme ---------- */

/*
 * ES GIBT KEINE SONDERREGEL MEHR. Die einzige stand bei .wort-eng und schaltete
 * unter 360 px das Wort in der Suchzeile um; beides ist mit der Bedienzeile
 * entfallen, denn die Lupe traegt kein Wort mehr. Was bleibt, schrumpft von
 * selbst: in der unteren Reihe steht genau ein dehnbares Element (die
 * Lagetafel), alle uebrigen Ziele haben feste 44 oder 72 px.
 */

/* ---------- Niedrige Schirme ---------- */

/*
 * Auf einem 568 px hohen Schirm belegen die Zonen jetzt gemessene 56 px oben
 * und 140 px unten (171 px, sobald die Lagetafel eine Meldung traegt). Die
 * Untergrenze von 44 px fuer ein Ziel wird dabei nicht angetastet; gekuerzt
 * wird nur die Luft: die Bedienzeile von 52 auf 46 px (also von 4 auf 1 px
 * ueber und unter den Zielen), die beiden Fugen von 12 auf 8 px, der
 * Meldeknopf von 64 auf 58. Das gibt 10 px Karte zurueck. Der Meldeknopf
 * schrumpft nicht wegen dieser 10 px, sondern weil er sonst auf einem kleinen
 * Schirm der groesste Gegenstand im Bild waere.
 */
@media (max-height: 640px) {
  :root {
    --reihe-fuge: 8px;
  }

  .werkzeugspalte {
    gap: 8px;
    margin-bottom: 8px;
  }

  .melden {
    min-width: 64px;
    /* Auch hier ein Boden, kein Mass: die Tafel gibt weiter die Hoehe vor. */
    min-height: 64px;
  }
}

/* ---------- Breite Schirme ---------- */

@media (min-width: 600px) {
  /* Ab Tablettbreite wird aus dem Bogen eine mittige Tafel. Ein Bogen ueber
     768 px waere eine Bandwurmzeile. */
  .bogen {
    left: 50%;
    right: auto;
    width: min(560px, calc(100% - 32px));
    bottom: 16px;
    border-radius: var(--r-bogen);
    border: 1px solid var(--kontur);
    box-shadow: var(--schatten-schwebend);
    max-height: 82vh;
    max-height: 82svh;
    transform: translate(-50%, calc(101% + 16px));
  }

  .bogen[data-offen] {
    transform: translate(-50%, 0);
  }

  /* Die Lagetafel darf nicht ueber die ganze Breite laufen, sonst steht ein
     Satz allein in einer sehr langen Zeile. Weil sie aufhoert zu wachsen, wird
     der Meldeknopf nicht mehr von ihr an die Kante geschoben und stuende sonst
     mitten im Bild; `auto` als linker Aussenabstand frisst den Rest der Zeile
     auf und uebernimmt das. Die Werkzeugsaeule braucht nichts davon, sie steht
     ohnehin schon am rechten Rand (align-self). */
  .status {
    flex: 0 1 420px;
  }

  .melden {
    margin-left: auto;
  }
}

/* ---------- Zugaenglichkeit ---------- */

@media (prefers-reduced-motion: reduce) {
  * {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
  }
}

.nur-vorlesen {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ---------- Schriftfeld ---------- */

/*
 * Das Wasserzeichen nach ~/schaufenster/_shared/SCHRIFTFELD.md, angepasst an
 * eine App statt an eine Website. Die Herleitung der drei Abweichungen steht
 * ueber `schriftfeld` in js/seiten.js.
 *
 * Es traegt ausschliesslich Tokens dieser Datei: kein eigenes Rot, keine eigene
 * Schrift, kein Logo. Genau deshalb wirkt es nicht fremd. Und es steht im
 * Dokumentfluss der Textseiten, nie ueber der Karte, denn dort waere jede
 * Auflage per Definition schwebend, und die Spezifikation verbietet das.
 */
.schriftfeld {
  margin: 28px 0 0;
  padding-top: 14px;
  border-top: 1px solid var(--linie);
  display: grid;
  gap: 8px;
  font-size: 0.75rem;
  line-height: 1.4;
}

.schriftfeld > div {
  display: grid;
  /* Spalte fuer die Marke, damit die drei Zeilen eine Kante bilden. Bei 320 px
     Geraetebreite bleiben daneben 180 px, und die laengste Angabe
     ("OpenStreetMap-Mitwirkende (ODbL)") bricht dort auf zwei Zeilen statt zu
     ueberlaufen.
     min-content als Untergrenze statt der frueheren festen 88 px: bei 200
     Prozent Systemschrift braucht schon das Wort "KARTENDATEN" 174 px, und die
     feste Spalte liess es in die Wertspalte laufen. Die Kante bleibt, sie faellt
     nur groesser aus - und die Wertspalte bricht, statt zu ueberlaufen. */
  grid-template-columns: minmax(min-content, 5.5rem) minmax(0, 1fr);
  gap: 10px;
}

/* Derselbe Eyebrow wie im Rest der App, nur kleiner: er beschriftet hier eine
   Zeile und nicht einen Abschnitt. */
.schriftfeld dt {
  text-transform: uppercase;
  font-size: 0.59375rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  color: var(--schrift-3);
  line-height: 1.75;
}

.schriftfeld dd {
  margin: 0;
  color: var(--schrift-2);
  overflow-wrap: anywhere;
}

/* Der Markenname ist der einzige Link nach aussen. Er traegt Tinte statt einer
   Linkfarbe, weil diese App keine Linkfarbe fuehrt. */
.schriftfeld a {
  color: var(--schrift);
  font-weight: 700;
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* Die Spezifikation verlangt, dass das Feld den Druck ueberlebt. */
@media print {
  .schriftfeld {
    display: grid;
  }
}

/* ---------- Fokusring: die Ausnahmen ---------- */

/*
 * Die Grundregel steht weit oben bei :focus-visible; hier stehen nur die
 * Stellen, an denen sie allein nicht traegt. Sie MUSS am Ende der Datei stehen:
 * die Grundregel hat eine Klasse, mehrere Tafelregeln weiter unten haben
 * ebenfalls eine und wuerden den Ring sonst still mit ihrem eigenen Schatten
 * ueberschreiben.
 */

/* Diese drei tragen bereits eine Auflage. Ihr Schatten bleibt stehen, der Ring
   kommt dazu, statt ihn zu ersetzen. */
.rund-frei:focus-visible,
.melden:focus-visible {
  box-shadow:
    var(--schatten-auflage),
    var(--fokus-ring);
}

.rund-frei:active:focus-visible,
.melden:active:focus-visible {
  box-shadow:
    var(--schatten-druck),
    var(--fokus-ring);
}

/*
 * RING NACH INNEN, WO EINE KANTE IHN SONST WEGSCHNEIDET.
 *
 * Drei Flaechen dieser App schneiden hart: .status mit `overflow: clip` (damit
 * die Tafel ihre Rundung behaelt), .bandzeile ebenso, und .bogen-inhalt mit
 * `overflow-x: clip`. Gemessen am Pixel lag an der Sollstelle des Rings um den
 * Lage-Knopf schlicht Kartenfarbe, Kontrast 1:1 - der Ring war da, aber nicht
 * zu sehen. Nach innen gelegt kann ihn keine dieser Kanten mehr treffen.
 *
 * Das Linienband bleibt der eine Rest: .filter blendet seine Raender per
 * mask-image aus, ein Ring genau an der Ausblendkante wird also blass. Der
 * Browser rollt das fokussierte Signet aber selbst in die Mitte, bevor es dort
 * ankommt.
 */
.status :focus-visible,
.oben :focus-visible,
.bogen-inhalt :focus-visible {
  outline-offset: -3px;
  box-shadow: var(--fokus-ring-innen);
}

