/* ═══════════ SCHIFFE RETTEN · Spiel der T. Willibald Adorno Stiftung ═══════════ */

:root {
  --sea:        oklch(0.16 0.03 245);
  --sea-raise:  oklch(0.20 0.035 245);
  --panel:      oklch(0.20 0.035 245 / .72);
  --line:       oklch(0.75 0.05 245 / .16);
  --line-soft:  oklch(0.75 0.05 245 / .55);   /* Zellraster: gemessen 3,35:1 (1.4.11 verlangt 3:1) */
  --ink:        oklch(0.95 0.01 245);
  --ink-soft:   oklch(0.76 0.02 245);
  --ink-faint:  oklch(0.60 0.02 245);
  /* ═══ Die Marke: Die PARTEI ═══
     Aus dem Logo, #981436 — oklch(0.442 0.164 14.2), ein tiefes Bordeaux-Karmin.
     Sie steht im Designsystem des Hauses als --red-500 (die readme.md dort nennt
     #e3000f, das ist veraltet; die Token-Datei gilt).

     Vier Rollen, und die Rolle entscheidet, nicht der Farbton:

       --brand      DAS FELD. Immer Fläche, nie Strich auf dunklem Grund. Das
                    Plakat, jedes Marken-Plättchen, jeder Hauptknopf. Bei L 0,44
                    misst sie gegen die dunkle Karte 2,2:1 — als Strich wäre sie
                    dort schlicht weg, als Feld mit heller Schrift trägt sie 8,44:1.
       --brand-ink  WAS AUF DEM FELD STEHT. Reines Weiß: 8,44:1 auf --brand, also
                    AAA. Kühles --ink (Farbton 245) sähe auf dem Rot schmutzig aus,
                    deshalb ein eigener Ton und kein Rückgriff.
       --brand-lit  DIE MARKE AUF DER DUNKLEN KARTE. Wo ein Feld nicht geht — ein
                    gestrichelter Rahmen, eine Glyphe, ein Zeitstempel —, muss die
                    Marke hell sein. Gemessen 10,13:1 auf der Tafel, und der Abstand
                    zum Alarm wächst durch die Aufhellung, statt zu schrumpfen: sie
                    geht nach OBEN weg, der Alarm bleibt, wo er ist (siehe unten).
       --brand-deep Die gedrückte/überfahrene Stufe eines Markenfeldes (--red-700).

     WARUM DAS PLAKAT KIPPT. Das alte Orange lag bei L 0,72 und trug fast schwarze
     Schrift (6,52:1 gemessen). Dieselbe Schrift auf dem Marken-Rot ergibt 2,07:1 —
     unlesbar. Das Plakat dreht deshalb um: rotes Feld, helle Schrift. Das ist kein
     Zugeständnis, das ist der Agitprop-Modus des Designsystems.

     MARKE UND ALARM. Rot heißt in diesem Spiel Gefahr, und das darf die Marke ihm
     nicht wegnehmen. Der Abstand kommt deshalb aus Helligkeit und Buntheit, nicht
     aus dem Farbton (--brand H 14 gegen --distress H 25, nur 11° auseinander).
     Gemessen in OKLab-ΔE, gegen den Stand von heute:

       Sektor-Plättchen ruhig gegen dringend   heute ΔE 0,090  →  jetzt ΔE 0,265
       Sektorrahmen ruhig gegen dringend       heute ΔE 0,114  →  jetzt ΔE 0,177
       Glyphe ◉ Kontakt gegen ✕ gesunken       heute ΔE 0,134  →  jetzt ΔE 0,337
       --brand-lit gegen --distress            heute ΔE 0,115  →  jetzt ΔE 0,223

     In Leuchtdichte, also für jeden, der den Farbton gar nicht sehen kann:
     das ruhige Plättchen gegen das dringende steht bei 2,94:1 statt bei 1,22:1,
     die beiden Glyphen bei 2,81:1 statt 1,50:1, die Rahmen bei 1,71:1 statt 1,50:1.
     Der Alarm ist nach der Umstellung deutlicher als vorher, nicht schwächer.
     Alle Zahlen aus gelieferten Pixeln und nicht aus den Token — die Ränder liegen
     mit Alpha auf der Karte, als Token sind sie nicht das, was ankommt. */
  --brand:      oklch(0.442 0.164 14.2);   /* #981436 · das Feld */
  --brand-ink:  oklch(1 0 0);              /* 8,44:1 auf --brand */
  --brand-lit:  oklch(0.82 0.10 14);       /* die Marke als Strich auf dunklem Grund */
  --brand-deep: oklch(0.326 0.120 13.1);   /* #630a22 · gedrückt */
  /* ═══ Der Alarm ═══
     Unverändert an Ort und Stelle — die Marke ist zu ihm gewandert, nicht er zu ihr.
     Er bekommt aber dieselben benannten Rollen wie die Marke, weil sonst nicht
     lesbar ist, dass es zwei Systeme sind und welches gerade spricht:

       --distress      der Alarm selbst (Schrift, Rahmen, Blenden)
       --distress-lit  der Alarm als HELLE PLATTE und als angehobene Glyphe. Das ist
                       die Gegenpolung zur Marke: die trägt dunkle Fläche mit weißem
                       Wort, dieser helle Fläche mit schwarzem. Genau daran, und
                       nicht am Farbton, erkennt man die beiden auseinander.
       --distress-ink  das schwarze Wort auf dieser Platte (weiß auf --distress
                       käme nur auf 4,3:1)
       --lost          der erloschene Zustand: gesunken, vorbei

     Zu --lost eine Ehrlichkeit: er liegt ΔE 0,044 neben --brand, ist also von der
     Marke praktisch nicht zu unterscheiden. Er ist trotzdem geblieben. Begründung
     steht bei .boat .call — kurz: das Markenplättchen sitzt auf JEDER Karte und ist
     damit eine Konstante, --lost unterscheidet Karten voneinander; die beiden
     kodieren nie dieselbe Achse. Der helle Saum um das Plättchen hält sie zusätzlich
     auseinander. */
  --distress:      oklch(0.64 0.23 25);
  --distress-lit:  oklch(0.72 0.20 25);
  --distress-ink:  oklch(0.16 0.04 25);
  --distress-soft: oklch(0.64 0.23 25 / .18);
  --lost:       oklch(0.42 0.14 25);
  --saved:      oklch(0.78 0.16 150);
  --abducted:   oklch(0.72 0.11 300);         /* aufgehellt: Statuslabel braucht 4.5:1 */
  /* Das Rot der Gegenseite. »Festung Europa« schreibt in Dienststempel-Rot, und das
     ist ausdrücklich NICHT die Marke: das Fernschreiben ist der Gegner, es darf
     nicht aussehen, als käme es von uns. Gemessen liegen die beiden allerdings nur
     ΔE 0,052 auseinander — sie treffen sich nur nie, weil das eine als Schrift auf
     hellem Papier steht und die andere als Fläche mit weißer Schrift. Stand jetzt
     fünfmal als roher Wert in dieser Datei; als Token ist wenigstens gesagt, dass
     er absichtlich danebenliegt. */
  --telex-rot:  oklch(0.45 0.2 25);
  --display: "Anton", "Archivo Black", system-ui, sans-serif;
  --body: "Archivo", system-ui, sans-serif;
  --mono: "Sometype Mono", ui-monospace, monospace;
  /* Motion */
  --ease-out-strong: cubic-bezier(0.22, 1, 0.36, 1);  /* UI-Standard: Ein-/Austritte, Pops */
  --ease-burst: cubic-bezier(0.16, 1, 0.3, 1);        /* expandierende Ringe/Funken/Ripples */
  --ease-exit-accel: cubic-bezier(0.5, 0, 0.8, 0.4);  /* beschleunigende Abfahrt (nur Szenerie-Exits) */
  --cell: clamp(27px, min(8.2vw, 4.6vh), 46px);
  --radius: 14px;
  /* z-Skala */
  --z-locked: 2;
  --z-hud: 10;
  --z-card: 40;
  --z-flash: 50;
}

*, *::before, *::after { box-sizing: border-box; }
html { color-scheme: dark; }

/* Nur für Hilfstechnik sichtbar */
.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%);
  overflow: hidden; white-space: nowrap;
}
/* Sichtbarer Fokus überall, auch auf dunklem Grund */
:focus-visible {
  outline: 3px solid var(--brand-lit);
  outline-offset: 2px;
  border-radius: 3px;
}
/* Auf hellem Papier dreht der Ring um: --brand-lit misst auf dem Fernschreiben
   1,3:1 und ist dort nicht vorhanden, das Marken-Rot selbst trägt 6,89:1. */
.khero :focus-visible, .telex-paper :focus-visible { outline-color: var(--brand); }
/* Fokussiertes Element nicht unter die klebende Leiste scrollen lassen */
.cell, .boat, .panel, .screen { scroll-margin-top: 96px; }
body {
  margin: 0;
  min-height: 100dvh;
  font-family: var(--body);
  background: var(--sea);
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
  line-height: 1.5;
}
button { font-family: inherit; color: inherit; }
[hidden] { display: none !important; }

.screen { display: none; min-height: 100dvh; }
.screen.active { display: block; }
/* Screenwechsel: kurzer Rise beim Eintritt (Bruch bewusst ausgenommen) */
#screenGame.active > .wrap,
#screenEnd.active > .bilanz {
  animation: screenIn 240ms var(--ease-out-strong) both;
}
/* Bewusst OHNE opacity: bei fill:both bliebe der Screen unsichtbar, falls die
   Animation nie läuft (verborgener Tab, Renderer ohne Animationen). Sichtbarkeit
   darf nie an einer Animation hängen. */
@keyframes screenIn {
  from { transform: translateY(10px); }
}

/* ═══════════ Screen 1 · Plakat ═══════════ */
/* Das Plakat ist ein Feld aus Marken-Rot mit heller Schrift. Alles darauf folgt
   derselben Regel: was betont werden soll, wird zur HELLEN PLATTE mit rotem Wort —
   der Stempel, der Knopf, das gewählte Schild. Eine Umkehrung, dreimal angewandt. */
/* Die Polsterung läuft über Variablen, damit die Aussparungen des Geräts weiter unten
   ein max() darüberlegen können, ohne den Wert zu kennen, der gerade gilt. Wer hier
   einen Rand ändert, ändert die Variable und nicht das padding. */
#screenIntro.active {
  display: flex; flex-direction: column;
  align-items: stretch; justify-content: space-between;
  background: var(--brand);
  color: var(--brand-ink);
  --pad-y: clamp(20px, 5vw, 64px);
  --pad-x: clamp(20px, 5vw, 64px);
  /* Oben KEINE Polsterung: dort sitzt seit dem 5.8. der Markenbalken bündig an der
     Kante. Der Balken bräuchte sonst einen negativen Rand in Höhe von --pad-y, und
     genau diese Kopplung geht schief — der Querformat-Block weiter unten setzt
     `padding-top` auf 10px, während --pad-y dort 11 bis 18px ist. Ein negativer Rand
     hätte den Balken um bis zu 8px über die Bildkante gezogen. Lieber gar keine
     obere Polsterung als eine, die an zwei Stellen gleich sein muss. */
  padding: 0 var(--pad-x) var(--pad-y);
  gap: 24px;
}
/* ── Der Markenbalken ──
   Ein weißer Streifen über die volle Breite, das Logo mittig darin. Er sitzt bündig
   an der Oberkante und muss dafür aus der Polsterung des Plakats ausbrechen — mit
   `max()` und nicht mit dem nackten `--pad-x`, weil die Polsterung selbst weiter
   unten ein `max(…, env(safe-area-inset-*))` darüberlegt: ein negativer Rand, der
   davon nichts weiß, ließe auf einem Gerät mit Aussparung im Querformat einen roten
   Streifen an der Seite stehen.
   Das Logo bringt drei Farben mit: #111211, #FFFFFF und #981436 — das ist genau das
   CI-Rot des Projekts (`--brand`). Auf Weiß verschwindet die weiße Ebene, das ist
   aber gewollt: sie ist der Versatz zwischen der roten Schrift und ihrem schwarzen
   Schlagschatten und trennt die beiden, statt selbst zu tragen. */
.plakat-marke {
  background: #fff;
  margin-left: calc(-1 * max(var(--pad-x), env(safe-area-inset-left)));
  margin-right: calc(-1 * max(var(--pad-x), env(safe-area-inset-right)));
  /* Die obere Aussparung des Geräts nimmt das Plakat sonst nirgends in Anspruch.
     Hier muss sie hinein, sonst rutscht das Logo unter die Statusleiste. */
  padding: calc(10px + env(safe-area-inset-top)) 16px 10px;
  display: flex; justify-content: center; align-items: center;
  gap: clamp(9px, 2vw, 16px);
}
/* Siegel und Wortmarke. Das Siegel ist quadratisch, die Wortmarke breit — zusammen
   ergeben sie eine Zeile, die einen Balken füllt, was keines von beiden allein tut.
   Bemessen wird über die HÖHE des Siegels: es ist das Element, dessen Größe den Balken
   bestimmt, und die Wortmarke richtet sich daran aus. */
.marke-siegel { display: block; flex: 0 0 auto; }
.marke-siegel img {
  display: block;
  height: clamp(30px, 6.2vw, 44px);
  /* Breite MIT der Höhe und nicht `auto`: fällt diese Regel aus — alte Stilvorlage im
     Zwischenspeicher, Ladefehler —, zeigt der Browser die Maße aus dem Markup. Die
     standen auf 200×200 (der viewBox des Siegels) und ergaben ein Zeichen quer über
     das halbe Telefon. Jetzt stehen dort 44×44, und der Ausfall ist unauffällig.
     Keine Farbe hier: ein SVG hinter <img src> ist ein eigenes Dokument und erbt weder
     `color` noch Variablen von dieser Seite. Das Siegel bringt seine Tinte selbst mit. */
  width: auto; aspect-ratio: 1;
}
.marke-wort {
  font-family: var(--display); font-weight: 400;
  font-size: clamp(.92rem, 2.6vw, 1.3rem);
  letter-spacing: .02em; line-height: 1.1;
  color: #14110c;                   /* die Tinte des Hauses, nicht das Markenrot */
  white-space: nowrap;
}
.mw-name { text-transform: uppercase; }
.mw-stift { color: var(--brand); }
/* Der Stern ist eine Fußnote, kein Bedienelement mit Gewicht: hochgestellt, klein, aber
   mit 24 px Trefferfläche über das Polster (WCAG 2.5.8). */
.mw-stern {
  color: inherit; text-decoration: none; font-size: .62em;
  vertical-align: super; padding: 6px 4px; margin-left: 1px;
}
@media (hover: hover) { .mw-stern:hover { text-decoration: underline; } }
.plakat-main { max-width: 1000px; }
.plakat-main h1 {
  font-family: var(--display);
  font-weight: 400;
  text-transform: uppercase;
  font-size: clamp(3rem, 13vw, 6rem);
  line-height: .96;
  letter-spacing: .01em;
  margin: 0 0 clamp(18px, 4vw, 34px);
  text-wrap: balance;
}
.plakat-main h1 .strike { position: relative; white-space: nowrap; }
.plakat-main h1 .strike::after {
  content: "";
  position: absolute; left: -2%; right: -2%; top: 50%;
  height: clamp(5px, 1.2vw, 11px);
  background: currentColor;
  transform: translateY(-50%) rotate(-1.5deg);
  transform-origin: left center;
  animation: strikeDraw 450ms var(--ease-out-strong) 350ms both;
}
@keyframes strikeDraw {
  from { transform: translateY(-50%) rotate(-1.5deg) scaleX(0); }
  to   { transform: translateY(-50%) rotate(-1.5deg) scaleX(1); }
}
/* Der Stempel. Vorher dunkle Platte mit hellem Wort, jetzt umgekehrt: die helle
   Platte ist auf dem roten Feld das Auffälligste, was es gibt. Dasselbe Paar wie
   beim Knopf, nur andersherum gelesen — 8,44:1 in beide Richtungen. */
.plakat-main h1 .retten {
  display: block;
  color: var(--brand);
  background: var(--brand-ink);
  width: fit-content;
  padding: .04em .18em .02em;
  transform: rotate(-1.2deg);
  margin-top: .12em;
  animation: rettenStamp 300ms var(--ease-out-strong) 850ms both;
}
/* KEIN `opacity: 0` mehr im ersten Bild. Das Wort war 850 ms lang unsichtbar, weil die
   Animation mit `both` schon vor ihrem Beginn den 0-%-Zustand hält — und damit hing die
   SICHTBARKEIT des Wortes an einer Animation. Genau das verbietet die Regel, die dieses
   Projekt seit dem Barrierefreiheits-Audit vom 25.7. hat und an drei anderen Stellen
   befolgt: »Sichtbarkeit nie an Animationen koppeln.«
   Gemeldet wurde am 5.8. von einem Telefon, dass das Wort nach einem Wechsel ins
   Querformat und zurück verschwindet. In Chrome ließ sich das nicht nachstellen (drei
   Gerätegrößen, echte Orientierungs-Emulation, jedes Mal wieder da) — aber ein Fenster,
   in dem ein Wort nur deshalb existiert, weil eine Animation gelaufen ist, ist genau die
   Sorte Fenster, aus der so eine Meldung kommt. Der Stempel ist jetzt reine Bewegung:
   fällt die Animation aus, steht das Wort einfach da, statt zu fehlen. */
@keyframes rettenStamp {
  0%   { transform: rotate(-1.2deg) scale(1.5); }
  65%  { transform: rotate(-1.2deg) scale(0.97); }
  100% { transform: rotate(-1.2deg) scale(1); }
}
.plakat-sub {
  font-size: clamp(1.05rem, 2.6vw, 1.45rem);
  font-weight: 600;
  max-width: 34ch;
  margin: 0 0 clamp(26px, 5vw, 44px);
  text-wrap: pretty;
  font-size-adjust: 0.52;                  /* Begründung steht im HTML an dieser Zeile */
}
/* Knopf und Maßkörper teilen sich jede Angabe, die Breite oder Höhe macht:
   Schrift, Laufweite, Zeilenhöhe, Polsterung, Rahmen. Nur so misst der Körper
   wirklich den Knopf und nicht etwas, das ihm ähnlich sieht. line-height steht
   ausdrücklich da: ein Knopf erbt sie nicht, ein <span> schon. */
.plakat-cta, .cta-sizer {
  font-family: var(--display);
  font-weight: 400;
  text-transform: uppercase;
  font-size: clamp(1.35rem, 3.4vw, 2.05rem);
  letter-spacing: .04em;
  line-height: normal;
  border: none;
  padding: .55em 1.1em .5em;
  text-align: center;
}
.plakat-cta {
  display: inline-block;
  background: var(--brand-ink);
  color: var(--brand);
  cursor: pointer;
  transition: transform .15s var(--ease-out-strong), background .15s;
}
/* Die Aufschrift wechselt beim Druck auf »Schicht läuft an«, und das ist das
   kürzere Wort: mit Anton 363,7 → 310,7 px bei 2400, ohne Anton 459,2 → 390,5.
   Welche Zahl gilt, entscheidet die Schrift, die der Browser wirklich benutzt —
   ein aus einer Schrift abgeleiteter Boden (früher min-width: 11,09em) hält
   deshalb nur, solange genau diese Schrift da ist, und liegt sonst unter der
   Ruhebreite: dann zieht der Knopf seine rechte Kante unter dem Zeiger weg,
   gemessen -68,7 px. Also misst die Seite selbst. .cta-sizer trägt beide
   Aufschriften übereinander in derselben Rasterzelle wie der Knopf; die Zelle
   wird so breit und so hoch wie die größere von beiden, in jeder Schrift. */
.cta-sizer {
  display: grid;                 /* übereinander, also das Maximum und nicht die Summe */
  visibility: hidden;            /* bleibt im Layout, verschwindet aus Bild und Vorlesebaum */
  pointer-events: none;
  -webkit-user-select: none; user-select: none;
}
.cta-sizer > span { grid-area: 1 / 1; }
.plakat-cta:focus-visible { outline: 3px solid var(--brand-ink); outline-offset: 4px; }
.plakat-cta[aria-disabled="true"] { cursor: default; opacity: .72; }

/* ── Bildqualität ──
   Verwaltung, keine Schlagzeile: Mono-Versalien wie die Kopfzeile, kleine
   Schilder. Der Radioknopf selbst ist nur optisch weg — er behält Fokus, Pfeiltasten und Beschriftung, das
   sichtbare Schild ist sein Geschwister.
   Farblich genau das Paar, mit dem .plakat-cta durch die Prüfung gegangen ist:
   Rahmen und Schrift in --brand-ink, gewählt kippt es auf helle Platte mit rotem
   Wort. Eine Umkehrung im Plakat, das selbst schon eine ist. */
.qbox {
  /* Ohne 3D-Ebene gibt es den Regler nicht: display:none nimmt ihn aus Bild und
     Vorlesebaum, es bleibt kein totes Bedienelement stehen. Die Klasse setzt der
     Kopf von index.html noch vor dem ersten Bild — der Kasten wird also nie
     nachträglich aufgedeckt, und genau das war die Verschiebung von 0,220. */
  display: none;
  border: 0; padding: 0;
  /* Der Abstand steht seit dem 4.8. oben: der Kasten sitzt jetzt UNTER dem Knopf,
     also hängt er am Knopf und nicht mehr die Wahl am Kasten. */
  margin: clamp(14px, 2.6vw, 20px) 0 0;
  min-inline-size: 0;                       /* fieldset bringt eine eigene Mindestbreite mit */
}
html.sea3d-moeglich .qbox { display: block; }
/* Und der dritte Fall: WebGL2 war da, die 3D-Ebene ist trotzdem nicht hochgekommen.
   Der Regler ist dann wirkungslos und muss weg — aber sein Kasten ist zu diesem
   Zeitpunkt längst gezeichnet, und ihn per display:none einzuziehen kostete 0,041
   Layoutverschiebung auf dem Telefon und 0,050 bei 320 px, also mehr als die 0,05,
   die wir uns setzen. visibility nimmt ihn aus Bild, Vorlesebaum und Tabreihenfolge
   und lässt seinen Platz stehen: kein totes Bedienelement, keine Verschiebung. Die
   Lücke bleibt, und das ist der günstigere Fehler — sie tritt nur auf, wenn das
   Spiel ohnehin gerade auf die 2D-Fassung zurückfällt. */
html.sea3d-gescheitert .qbox { visibility: hidden; }
.qbox legend {
  padding: 0; margin-bottom: 8px;
  font-family: var(--mono); font-size: .74rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .1em;
}
.q-opts { display: flex; flex-wrap: wrap; align-items: center; column-gap: 10px; row-gap: 8px; }
.q-opt { cursor: pointer; }
.q-opt input {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}
.q-opt span {
  display: block;
  font-family: var(--mono); font-size: .76rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em;   /* trägt die Vorlesenamen: nicht wegnehmen */
  border: 2px solid var(--brand-ink);
  padding: .38em .68em .34em;                         /* bleibt über 24 px Zielgröße */
  transition: background-color .15s, color .15s;
}
.q-opt input:checked + span { background: var(--brand-ink); color: var(--brand); }
.q-opt input:focus-visible + span { outline: 3px solid var(--brand-ink); outline-offset: 3px; }
.q-opt input:disabled + span { opacity: .62; }
.q-opt input:disabled ~ * { cursor: default; }
.q-hint {
  font-family: var(--mono); font-size: .74rem;
  margin: 0;
}

/* ── Fortschritt der Vorbereitung ──
   --p ist eine Breite, keine Animation. Unter html.ruhig fällt der Übergang auf
   0,01 ms und der Balken steht einfach da, wo er steht; sichtbar ist er über das
   hidden-Attribut und nie über das Ende eines Übergangs (Punkt 6 der Abnahme).
   Die Zeile darunter sagt dasselbe in Worten, damit ein Standbild reicht.

   Der Platz ist reserviert, nicht erspielt. Gemessen hat der Knopf sich beim
   Aufdecken um -28,6 px (2400x1350), -10,7 px (1440x900) und -25,6 px (390x844)
   nach oben gezogen — auf dem Telefon die halbe Knopfhöhe, und das im Moment des
   Drucks. Also: .cta-block hält die Höhe des Balkens von Anfang an als
   padding-bottom frei, und der Balken liegt darin absolut. Beide Maße kommen aus
   denselben vier Variablen, sie können also nicht auseinanderlaufen. */
.cta-block {
  /* 9px, nicht 6: der Fokusring des Knopfes ist 3 px breit bei 4 px Abstand und
     endet damit 7 px unter der Knopfkante — bei 6 px läge er auf dem Balken. */
  --qp-lead: 9px;      /* Knopfunterkante → Balken */
  --qp-track: 10px;    /* Balkenhöhe, Rahmen eingerechnet */
  --qp-gap: 6px;       /* Balken → Wort */
  --qp-note: .78rem;   /* Schriftgrad des Worts */
  --qp-lh: 1.5;        /* genau eine Zeile, siehe .q-note */
  position: relative;
  /* Eine Zelle, zwei Bewohner: der Knopf und sein Maßkörper liegen aufeinander.
     Die Spalte nimmt das Maximum beider Inhalte, der Knopf füllt sie aus. Der
     Fortschrittsblock ist absolut und damit kein Rasterkind. */
  display: grid;
  width: fit-content;                       /* so breit wie der Knopf, nicht breiter */
  max-width: 100%;
  padding-bottom: calc(var(--qp-lead) + var(--qp-track) + var(--qp-gap)
                       + var(--qp-note) * var(--qp-lh));
}
.cta-block > .plakat-cta,
.cta-block > .cta-sizer { grid-area: 1 / 1; }
.q-progress {
  position: absolute; left: 0; right: 0; bottom: 0;
  display: flex; flex-direction: column;    /* Ränder von Flex-Kindern fallen nie zusammen */
}
.q-note, .sv-wort {
  margin: var(--qp-gap) 0 0;
  font-family: var(--mono); font-size: var(--qp-note); font-weight: 700;
  line-height: var(--qp-lh);
  text-transform: uppercase; letter-spacing: .06em;
  /* max-content statt Umbruch: die längste Meldung (»Kamera wird eingerichtet.«)
     misst 206,6 px ohne Anton, der Knopf 288 px im engsten geprüften Fenster
     (320 px) — die Zeile bleibt eine Zeile, und damit bleibt die reservierte
     Höhe exakt die tatsächliche, bei jeder Meldung und jeder Fensterbreite. */
  width: max-content; max-width: none;
}
/* Zwei Sätze im selben Rasterfeld. Die Zelle nimmt die Breite des längeren, also
   bewegt der Wechsel nichts; überblendet wird auf opacity, und das läuft auf dem
   Compositor-Faden — der einzige Faden, der in dieser Phase noch antwortet. Der
   Wechsel liegt bei 2,3 bis 2,7 s und damit mitten im gemessenen 4,55-s-Abschnitt. */
#qNote.zweisatz { display: grid; }
#qNote.zweisatz > span { grid-area: 1 / 1; }
#qNote.zweisatz > span:nth-child(1) { animation: qnEins 6s linear forwards; }
#qNote.zweisatz > span:nth-child(2) { opacity: 0; animation: qnZwei 6s linear forwards; }
@keyframes qnEins { 0%, 38% { opacity: 1; } 45%, 100% { opacity: 0; } }
@keyframes qnZwei { 0%, 38% { opacity: 0; } 45%, 100% { opacity: 1; } }
/* Schiene und Läufer sind für beide Anzeigen dieselben: den Anlauf vor dem Druck
   (.sv-anlauf) und den Fortschritt danach (#qProgress). Eine Sprache, ein
   Regelsatz — und der Übergang vom einen zum anderen ist kein Bildwechsel. */
/* Schiene, Schraffur und Läufer sind auf dem roten Feld hell — dieselbe Umkehrung
   wie beim Knopf darüber, sonst wäre die Anzeige das einzige dunkle Möbel auf dem
   Plakat. Die Anteile bleiben, wie sie waren; nur der Ton dreht. */
.q-track, .sv-schiene {
  position: relative; overflow: hidden;     /* der Läufer läuft innen, nicht daneben */
  height: var(--qp-track);
  border: 2px solid var(--brand-ink);
  background: color-mix(in oklch, var(--brand-ink) 12%, transparent);
}
/* transform statt width: eine Breite ist Layout und wird vom Hauptfaden gerechnet,
   also steht ihr Übergang genau dann, wenn der Aufbau den Faden blockiert. scaleX
   verwaltet der Compositor und läuft durch einen Block hindurch weiter. Sichtbar
   ist beides identisch — es ist derselbe gefüllte Kasten. */
.q-bar {
  display: block; height: 100%; width: 100%;
  transform: scaleX(var(--p, 0)); transform-origin: left center;
  background: var(--brand-ink);
  transition: transform 220ms var(--ease-out-strong);
}
/* Solange die Ebene selbst noch hochfährt, gibt es keinen Anteil zu melden, und
   ein Balken auf 0 sieht aus wie ein Fehler. Also eine statische Schraffur: sie
   sagt »läuft«, ohne eine Zahl zu behaupten, die niemand kennt. Sie ist auch der
   Stand, auf dem html.ruhig stehen bleibt — deshalb trägt sie die Aussage allein
   und nicht bloß als Untergrund des Läufers. */
.q-progress.unbestimmt .q-bar { transform: scaleX(0); }
.q-progress.unbestimmt .q-track, .sv-schiene {
  background-image: repeating-linear-gradient(135deg,
    color-mix(in oklch, var(--brand-ink) 0%, transparent) 0 7px,
    color-mix(in oklch, var(--brand-ink) 38%, transparent) 7px 14px);
}

/* ── Anlauf ──
   Der Läufer. Ein Block aus derselben Farbe wie der Balken, der von links nach
   rechts durch die Schiene wandert und wieder von vorn anfängt. Er behauptet
   keinen Anteil, er sagt nur: die Maschine arbeitet.

   Warum ausgerechnet transform, und warum in CSS statt in JavaScript: der Aufbau
   der 3D-Ebene blockiert den Hauptfaden gemessen 6,1 bis 6,5 s, in Blöcken von bis
   zu 2,0 s. Alles, was der Hauptfaden weiterschieben müsste — ein Intervall, ein
   requestAnimationFrame, eine Breite, ein background-position — steht in genau
   diesen Sekunden still. transform und opacity nicht: die liegen beim
   Compositor-Faden, der von der Blockade nichts merkt. Gemessen: drei
   verschiedene Bilder innerhalb eines 2,0-s-Blocks, in dem sonst 2,0 s lang
   dasselbe Bild stand.

   Die Wegstrecke folgt der Breite: der Läufer ist 38 % der Schiene breit, startet
   bei -100 % seiner selbst (rechte Kante auf 0) und endet bei 100/38 · 100 % =
   263,2 % (linke Kante am rechten Rand). Wer die 38 % ändert, ändert die 263,2 %
   mit, sonst springt der Läufer. */
.sv-lauf {
  position: absolute; top: 0; bottom: 0; left: 0;
  display: block; width: 38%;
  background: var(--brand-ink);
  transform: translateX(-100%);
  animation: svLauf 1150ms linear infinite;
}
@keyframes svLauf {
  from { transform: translateX(-100%); }
  to   { transform: translateX(263.2%); }
}
/* Im Fortschritt nach dem Druck läuft er nur, solange es keinen Anteil gibt.
   Sobald game.js einen meldet, nimmt der Balken die Aussage und der Läufer geht. */
.q-track > .sv-lauf { display: none; }
.q-progress.unbestimmt .q-track > .sv-lauf { display: block; }

.sv-anlauf {
  position: absolute; left: 0; right: 0; bottom: 0;
  /* Ohne 3D-Ebene wird nichts aufgebaut, und dann gibt es auch nichts anzuzeigen:
     das Spiel startet auf den Druck hin sofort. Ein Ladebalken für eine Wartezeit,
     die es nicht gibt, ist eine Lüge. Dieselbe Klasse, an der auch der
     Qualitätsregler hängt, und sie steht seit dem Kopf der Seite fest. */
  display: none;
}
html.sea3d-moeglich .sv-anlauf { display: block; }
/* Ab dem Druck übernimmt #qProgress denselben Platz — beide liegen absolut in der
   Aussparung von .cta-block, also nie beide zugleich. */
.q-progress:not([hidden]) ~ .sv-anlauf { display: none; }
/* WebGL2 war da, die Ebene ist trotzdem nicht hochgekommen: es wird nichts mehr
   aufgebaut, also läuft auch nichts mehr an. Zieht kein Layout — der Kasten liegt
   absolut in reserviertem Platz und trägt keine Höhe. */
html.sea3d-gescheitert .sv-anlauf { display: none; }
/* Der Abschluss. Der Läufer geht, die Schiene füllt sich einmal ganz — wieder über
   transform, damit auch dieser letzte Schritt nicht auf einen freien Hauptfaden
   wartet. Er kommt nicht früher als das Spiel: html.sea3d setzt der Bootstrap in
   index.html erst, wenn die Ebene wirklich steht. */
.sv-voll {
  position: absolute; top: 0; bottom: 0; left: 0;
  display: block; width: 100%;
  background: var(--brand-ink);
  transform: scaleX(0); transform-origin: left center;
  transition: transform 380ms var(--ease-out-strong);
}
html.sea3d .sv-anlauf .sv-voll { transform: scaleX(1); }
/* Ausdrücklich nur der Läufer DIESER Anzeige. Der in #qProgress hängt nicht an
   html.sea3d, und das ist keine Feinheit: wer nach dem Aufbau eine andere
   Bildqualität wählt und dann drückt, lässt die Ebene neu messen (game.js:
   setQuality, dann preload, und zeigen() bei !SEA.warm). Dann steht html.sea3d
   und trotzdem läuft eine Vorbereitung — mit `html.sea3d .sv-lauf` wäre der
   Balken in genau diesem Fall eine tote Schraffur. */
html.sea3d .sv-anlauf .sv-lauf { animation: none; opacity: 0; }
/* Drei Stände, drei Sätze, und immer genau einer. Die Bedingungen schließen
   einander aus, also hängt hier nichts an der Reihenfolge der Regeln: wer eine
   vierte einfügt, muss die anderen nicht nachziehen.

   Das :not(.sea3d) an der mittleren Regel ist keine Kosmetik. Der Bootstrap
   nimmt sea3d-warm weg und setzt sea3d — stünden beide je zugleich am
   Wurzelelement, zeigte der Kasten zwei Zeilen, und weil er von unten verankert
   ist, wüchse er nach oben in den Knopf hinein. Gemessen an einer erzwungenen
   Doppelklasse: zwei Sätze, Schiene hinter der Knopfkante. Eine Zeile
   Bedingung, und der Zustand kann nicht mehr entstehen. */
.sv-wort > span { display: none; }
html.sea3d-moeglich:not(.sea3d-warm):not(.sea3d) .sv-wort > .sv-w1 { display: block; }
html.sea3d-warm:not(.sea3d) .sv-wort > .sv-w2 { display: block; }
html.sea3d .sv-wort > .sv-w3 { display: block; }
.plakat-foot {
  font-size: .85rem;
  font-weight: 600;
  display: flex; flex-wrap: wrap; gap: 6px 22px;
  max-width: 70ch;
}
.plakat-foot span { opacity: .85; }
/* Der Ausgang für alle, die nicht spielen wollen. Steht als direktes Kind des Fußes
   und nicht in einem span, weil die 85 % Deckkraft dort ein Deckel wäre, den ein
   Kind nicht durchbrechen kann. Unterstrichen statt gefärbt: auf dem Markenrot gibt
   es keine zweite Textfarbe, die die 4,5:1 hält. */
/* Der Umbruch kommt vom Kasten, die Trefferfläche vom Link: flex-basis am <a>
   selbst hätte ihn über die ganze Zeile gespannt, und dann spendet auch, wer
   neben den Text tippt.
   Gilt so nur noch auf dem Schreibtisch. Auf dem Telefon läuft der Kasten seit dem
   11.8. über die ganze Spalte, siehe die Regel in @media (max-width: 430px) — wer
   diese hier ändert, sieht dort nichts davon. */
.plakat-foot .foot-zeile { flex-basis: 100%; }
/* Der Spendenweg steht seit dem 5.8. ÜBER dem Kleingedruckten und nicht darunter, und er
   ist präsenter als vorher — beides auf Wunsch des Projekteigners. Bewusst NICHT als
   zweiter gefüllter Knopf: das Plakat hat genau einen lauten Gegenstand, und das bleibt
   »Schicht übernehmen«. Ein Rahmen statt einer Fläche ist die Stufe dazwischen — er
   liest sich als Angebot, nicht als Aufforderung, und er kämpft nicht mit dem Knopf um
   denselben Blick. Der Fuß dimmt seine Spannen auf 85 % Deckkraft; dieser Link ist ein
   direktes Kind und trägt deshalb die vollen 8,44:1.
   `.foot-zeile a` und NICHT `.plakat-foot a`: der Fuß trägt seit den Rechtstexten noch
   zwei weitere Anker (Impressum · Datenschutz), und der breite Selektor machte aus ihnen
   zwei zusätzliche Versal-Kästen — drei laute Gegenstände, gegen genau die Regel, die
   dieser Kommentar begründet. Dazu kosteten die Kästen auf 375×667 das Falz-Budget. */
.plakat-foot .foot-zeile a {
  display: inline-block;
  padding: 9px 16px;
  color: inherit;
  font-family: var(--display); font-weight: 400;
  font-size: 1.05rem; letter-spacing: .04em; text-transform: uppercase;
  text-decoration: none;
  box-shadow: inset 0 0 0 2px currentColor;
}
@media (hover: hover) {
  .plakat-foot .foot-zeile a:hover { background: var(--brand-ink); color: var(--brand); }
}
.plakat-foot .foot-zeile a:active { transform: scale(.97); }
/* Der Hinweis der Stiftung an sich selbst. Er ist Kleingedrucktes und soll so aussehen,
   aber er darf nicht ganz verschwinden: er ist der Satz, mit dem die Absenderin ihren
   eigenen Namen einschränkt. */
#stiftungshinweis { font-style: italic; }
/* Die Pflichtangaben sind Kleingedrucktes mit Trefferfläche, kein Handlungsangebot:
   unterstrichen wie der ursprüngliche Fuß-Link, 4px-Polster für die 24 px der Norm. */
.plakat-foot .recht-zeile a {
  display: inline-block; padding: 4px 0;
  color: inherit; font-weight: 700;
  text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 3px;
}

/* ── Das Plakat auf dem Schreibtisch ──
   Gemeldet vom Projekteigner am 5.8.2026: »auf dem desktop klebt alles links und
   rechts ist toter raum«. Nachgemessen bei 1440×900 stimmte das für alles unter der
   Schlagzeile — die reicht bis 74 % der Breite, aber Unterzeile (34ch), Knopf und Fuß
   enden alle bei rund 40 %, weil eine Flex-Spalte jedes Kind an dieselbe Kante legt.

   Die untere Hälfte wird deshalb zweispaltig: links die Handlung (Unterzeile, Knopf,
   Bildqualität), rechts das Kleingedruckte, unten bündig. Links und nicht rechts für
   den Knopf, weil das der DOM-Reihenfolge folgt — wer mit der Tastatur oder einer
   Vorlesung durchgeht, kommt weiter in derselben Reihenfolge an wie mit dem Auge.

   `display: contents` an .plakat-main, damit Schlagzeile und Unterzeile getrennt in
   das Raster fallen: sonst müssten sie in derselben Zelle bleiben, und die Zeile
   säße wieder unter einem Titel, der die volle Breite braucht. Kein Eingriff ins
   HTML, kein zusätzlicher Kasten. Die Schriftgrade bleiben unangetastet — die sind
   an anderer Stelle teuer nachgemessen worden, und die Beschwerde war der Raum,
   nicht die Größe. */
/* Auf dem Telefon gibt es das Bild nicht. Die Schrift trägt dort die Fläche, und ein
   141-kB-Bild läge auf derselben Leitung, auf der gerade die 3D-Ebene kommt.
   Die Regel steht VOR der Medienabfrage. Dahinter schlüge sie deren `display: block`
   still — gleiche Spezifität, spätere Zeile gewinnt —, und das Bild bliebe auf jeder
   Größe aus. Genau das ist beim ersten Anlauf passiert. */
.plakat-bild { display: none; }

/* ── Das Plakat auf dem Schreibtisch ──
   Zwei Spalten, und die rechte hat eine Aufgabe: links der Text, rechts das Bild aus
   dem Spiel (Audit-Befund B). Der erste Anlauf hat stattdessen die untere Hälfte geteilt
   und rechts das Kleingedruckte hineingelegt — das war Möbelrücken, wo ein Möbelstück
   fehlte.

   KEINE Höhenverzweigung. Ein zweiter Aufbau für flache Fenster war der Fehler des
   zweiten Anlaufs: er brachte einen negativen Abstand zum Markenbalken mit, und zwei
   Aufbauten für dieselbe Seite laufen auseinander. Entscheidung des Projekteigners vom
   5.8.: auf flachen Fenstern wird gescrollt. Das ist die Nebenbedingung, die alles
   andere möglich macht. */
@media (min-width: 900px) {
  #screenIntro.active {
    display: grid;
    grid-template-columns: minmax(0, 1.28fr) minmax(0, 1fr);
    grid-template-areas:
      "marke marke"
      "text  bild"
      "fuss  bild";
    grid-template-rows: auto 1fr auto;
    column-gap: 0;            /* die Überblendung des Bildes IST der Abstand */
    row-gap: 0;
    padding-bottom: 0;        /* das Bild geht bis an die Kante */
  }
  .plakat-marke { grid-area: marke; }
  /* Text und Fuß liegen ÜBER dem Bild. Das Bild reicht mit einem negativen Rand weit in
     die linke Spalte hinein, und ohne Stapelkontext malte es sich über die Schrift —
     es steht im DOM hinter .plakat-main. */
  .plakat-main, .plakat-foot { position: relative; z-index: 1; }
  .plakat-main {
    grid-area: text;
    max-width: none;
    align-self: center;
    padding-right: clamp(24px, 3vw, 56px);
    /* DER ABSTAND ZUM BALKEN. Gemeldet am 5.8.: »das weiße Schiffe versenken ist zu nah
       am weißen Header-Banner«. Der Balken kam am selben Tag dazu wie die größere
       Schlagzeile, und niemand hat die beiden weißen Flächen zusammen angesehen. Weiß auf
       Rot direkt unter Weiß auf Weiß liest sich nicht als zwei Dinge, sondern als eines
       mit einem Sprung darin. */
    padding-top: clamp(28px, 6vh, 72px);
  }
  .plakat-main h1 {
    /* Kleiner als einspaltig, weil die Spalte kleiner ist. Liest die Höhe mit, damit ein
       flaches Fenster nicht auch noch eine schreiende Schlagzeile bekommt. */
    font-size: clamp(2.6rem, min(6.6vw, 11vh), 6rem);
  }
  /* Der Spendenkasten klebte am Regler für die Bildqualität — die beiden stehen in zwei
     Rasterzeilen, und die hatten keinen Abstand. */
  .plakat-foot { grid-area: fuss; max-width: none;
    padding: clamp(18px, 3vh, 34px) clamp(24px, 3vw, 56px) var(--pad-y) 0; }
  /* Randlos nach rechts und nach unten: das Bild ist eine Fläche, kein Bildchen. Die
     seitliche Aussparung des Geräts wird mit `max()` mitgenommen, wie beim Markenbalken —
     ein negativer Rand, der nur `--pad-x` kennt, ließe auf einem Gerät mit Aussparung
     einen roten Streifen an der Kante stehen. */
  .plakat-bild {
    grid-area: bild;
    display: block;
    align-self: stretch;
    position: relative;
    overflow: hidden;
    min-height: 0;
    margin-right: calc(-1 * max(var(--pad-x), env(safe-area-inset-right)));
    /* Weit nach links unter den Text. Die Überblendung ist nur so weich, wie sie lang
       ist: in der eigenen Spalte blieben 34 % von 44 % der Breite, also 15 % des Bildes
       für den ganzen Übergang — zu wenig, um weich zu wirken. Jetzt ist der Kasten rund
       zwei Drittel breit und die Maske hat Platz. */
    margin-left: -24vw;
  }
  .plakat-bild img {
    display: block;
    position: absolute; inset: 0;
    width: 100%; height: 100%;
    object-fit: cover;
    /* Oben, nicht mittig: das Schiff steht im oberen Drittel des Bildes, und ein
       mittiger Beschnitt schneidet in einer hohen Spalte genau daran vorbei. */
    object-position: 68% 30%;
    /* Die weiche Kante. Ohne sie steht eine harte senkrechte Naht zwischen Foto und
       Markenrot mitten im Bild — auf Wunsch des Projekteigners läuft das Foto stattdessen
       in den Text hinein aus. Zwei Achsen, weil es zwei Kanten gibt: links zum Text und
       oben zum weißen Balken. Nach rechts und unten bleibt es randlos.
       `mask-image` und nicht ein Verlauf darüber: ein Verlauf müsste die Grundfarbe
       kennen und wäre beim nächsten CI-Wechsel falsch, die Maske nicht. */
    -webkit-mask-image: linear-gradient(to right, transparent 0%, transparent 8%, #000 62%),
                        linear-gradient(to bottom, transparent 0%, #000 14%, #000 78%, transparent 100%);
    -webkit-mask-composite: source-in;
    mask-image: linear-gradient(to right, transparent 0%, transparent 8%, #000 62%),
                linear-gradient(to bottom, transparent 0%, #000 14%, #000 78%, transparent 100%);
    mask-composite: intersect;
  }
  /* Im Ladezustand gilt DASSELBE Raster wie auf dem Plakat — hier stand eine eigene
     Vorlage ohne das Areal »fuss«, aus der Zeit, als der Fuß im Direktmodus komplett
     ausgeblendet war. Seit die Pflichtangaben-Zeile sichtbar blieb, war der Fuß ein
     Grid-Element mit unbekanntem Areal und landete nach Spezifikation in impliziten
     Spuren HINTER dem Raster: gemessen bei 1440×900 eine vierte Spalte von 289,5 px,
     der Markenbalken und das Bild endeten 290 px vor der rechten Kante, die
     Impressum-Links schwebten in einem leeren roten Streifen. Das sah jeder
     Desktop-Besucher des Haupt-Trichterwegs während der gesamten Ladezeit.
     Die Lehre steht schon woanders in dieser Datei: eine Regel, die einen Zustand
     voraussetzt (»der Fuß ist eh unsichtbar«), bricht still, wenn der Zustand kippt.
     Ohne Vorlage sitzt der Text zentriert in seiner 1fr-Zeile (align-self: center),
     die Rechtszeile unten links im Areal »fuss«, das Bild bleibt randlos — genau das,
     was die Sondervorlage wollte und nicht lieferte. */
}

/* ── Der Direkteinstieg ──
   Kommt der Besucher von der Kampagnenseite (`spiel.html#start`), ist das Plakat kein
   Tor mehr, sondern ein Ladezustand — die Schicht läuft von selbst an, siehe game.js.
   Dann darf dieser Bildschirm nicht mehr aussehen wie die Seite, von der er kommt:
   genau das war Audit-Befund A der Vorseiten, zwei Tore mit demselben Bild.

   Was bleibt: der Markenbalken, die Geldregel, der Fortschritt, die Bildqualität, die
   Pflichtangaben — und auf dem Schreibtisch das Bild.
   Die Geldregel bleibt, weil sie die Regel ist, an der das ganze Spiel hängt — eine
   gemessene Partie ohne jedes Lesen endete bei Zug 10 mit 0 € und 7 abgesuchten
   Feldern.
   Was geht: die Schlagzeile — visuell, nicht aus dem Baum. Sie benennt den Abschnitt
   (`aria-labelledby="introTitle"`) und ist die einzige Erstüberschrift der Seite;
   `display: none` hätte beides mitgenommen, und axe hätte es gemeldet.
   Und der Fuß, dessen vier Zeilen auf der Kampagnenseite stehen — im Spendenband und
   im Seitenfuß, also auf der einzigen Seite, von der dieser Weg überhaupt herführt. */
/* Nur schmal. Ab 900 px setzt die Medienabfrage weiter unten auch für den Ladezustand
   ein Raster, und diese Regel drehte es mit ihrer höheren Spezifität (html.direkt +
   #screenIntro.active) still wieder auf Flex zurück — gemessen lief das Bild dann über
   96 % der Breite statt in seiner Spalte zu bleiben. */
@media (max-width: 899px) {
  html.direkt #screenIntro.active {
    display: flex; flex-direction: column;
    justify-content: flex-start;
    gap: clamp(20px, 4vh, 40px);
  }
}
html.direkt #screenIntro .plakat-main h1 {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%);
  overflow: hidden; white-space: nowrap;
}
/* Vom Fuß bleibt im Ladezustand genau eine Zeile stehen: die Pflichtangaben. Das
   Impressum muss von jeder Seite aus erreichbar sein, und dieser Bildschirm ist der,
   den der Weg von der Kampagnenseite zeigt. Alles andere des Fußes steht auf der
   Kampagnenseite, von der dieser Weg herführt. */
html.direkt #screenIntro .plakat-foot > *:not(.recht-zeile):not(#stiftungshinweis) { display: none; }
/* Der Stiftungshinweis bleibt mit stehen, weil der Stern im Markenbalken auf ihn zeigt
   und der Balken auch im Ladezustand da ist. Ein Sprungziel, das ausgeblendet ist, ist
   ein Sprung ins Nichts — und dieser Hinweis ist obendrein die Stelle, an der die
   Stiftung selbst sagt, dass sie noch keine ist. */
html.direkt #screenIntro .plakat-foot { flex-basis: auto; }
/* Der Rest rückt in die Mitte der freien Fläche statt an die Oberkante zu kleben —
   ein Ladezustand hat kein Plakat-Gefälle, er hat einen Schwerpunkt.
   ZWEI automatische Ränder, einer über und einer unter der Gruppe: mit nur einem
   (an .plakat-main) nahm er allein die ganze freie Höhe und schob den Block in die
   untere linke Ecke — gemessen saß er bei 1440×900 auf y 460 bis 840. Zwei teilen
   sich den Rest zu gleichen Teilen, und das ist die Mitte. */
html.direkt #screenIntro .plakat-marke { margin-bottom: auto; }
/* Der zweite automatische Rand gehört an .plakat-main und NICHT an .qbox: der Kasten
   ist kein Kind von #screenIntro, sondern liegt zusammen mit dem Knopf INNERHALB von
   .plakat-main. Ein automatischer Rand an einem Element, das kein Flex-Kind ist, tut
   nichts — und genau das war er, bis die Rechnung ihn verraten hat (Gruppe stand bei
   1440×900 auf y 465 statt in der Mitte, der Balken trug 373 px Rand allein). */
html.direkt #screenIntro .plakat-main { margin-bottom: auto; }
html.direkt #screenIntro .plakat-sub { max-width: 42ch; margin-bottom: clamp(20px, 3vh, 32px); }
/* Der Knopf ist hier kein Knopf mehr, sondern die Anzeige »Schicht läuft an«. Er
   behält jede Angabe, die seine Geometrie macht — der Maßkörper darunter misst ihn,
   und der Fortschrittsbalken hängt an dessen Unterkante —, wird aber leiser: eine
   Anzeige, die aussieht wie das lauteste Bedienelement der Seite, lädt zum Drücken
   ein, und drücken kann man hier nichts. */
html.direkt #screenIntro .plakat-cta {
  background: none;
  color: var(--brand-ink);
  box-shadow: inset 0 0 0 2px var(--brand-ink);
  cursor: default;
}
/* Die Bildqualität BLEIBT im Ladezustand. Sie war hier kurzzeitig ausgeblendet, weil
   `anlaufAnzeigen(true)` die Regler abschaltete und sie damit eine Attrappe waren — die
   richtige Antwort darauf ist aber, sie lebendig zu machen und nicht sie zu verstecken.
   Seit der Trichter direkt startet, ist dieser Bildschirm der EINZIGE Moment, in dem ein
   Besucher die Stufe überhaupt zu sehen bekommt; wer sie hier nicht hat, hat sie nie.
   Die Mechanik steht in game.js an `anlaufNeueWahl`: eine Wahl während des Anlaufs hängt
   eine Probe mit der neuen Stufe an, und gewartet wird auf die. */

/* Das flache Fenster auf dem Schreibtisch — 1366×768 ist die verbreitetste Laptopgröße
   überhaupt, und dort stand der Fuß nach dem Umbau noch 10 px unter der Falz. Geholt
   werden sie an den Abständen und nicht am Schriftgrad: die Abstände zahlen nur die
   Größen, die den Platz wirklich nicht haben, weil bei allen anderen die freie Zeile
   den Betrag ohnehin schluckt. Ein kleinerer Grad hätte dagegen jede Ansicht 5 Punkte
   Flächenfüllung gekostet, um eine einzige zu retten. */
@media (min-width: 900px) and (max-height: 820px) {
  .plakat-main h1 { margin-bottom: 18px; }
  .plakat-sub { margin-bottom: 22px; }
}

/* ═══════════ Screen 2 · Einsatz ═══════════ */
#screenGame .wrap {
  max-width: 1100px; margin: 0 auto;
  --wrap-px: clamp(12px, 3vw, 24px);
  padding: 16px var(--wrap-px) 56px;
}
.hud {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  border: 1px solid var(--line);
  background: var(--panel);
  border-radius: var(--radius);
  --hud-px: 16px;
  padding: 10px var(--hud-px);
  margin-bottom: 18px;
  position: sticky; top: 8px; z-index: var(--z-hud);
  backdrop-filter: blur(8px);
}
.hud-title {
  /* Die Zeile war bis zum 5.8.2026 das <h1> der Seite und ist jetzt ein <p>, weil
     die Kopfleiste für einen Kameraschnitt aus dem Bild fährt und die Überschrift
     nicht mitgehen darf (Befund 26, siehe index.html). Der Außenabstand steht hier
     ausgeschrieben, weil die beiden Elemente unterschiedliche Vorgaben mitbringen —
     0,67em beim h1, 1em beim p — und die Seite kein Reset dafür hat: ohne diese
     Zeile stand das gesamte Brett 11,09 px tiefer, auf allen sieben geprüften
     Ansichten. Gemessen mit three3d/tools/lage.mjs gegen den Commit davor. */
  margin: .67em 0;
  font-family: var(--display);
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: .05em;
  font-size: 1.05rem;
  color: var(--brand-lit);                    /* 10,13:1 auf der Leiste */
  display: flex; align-items: center; gap: 9px;
}
.hud-title::before {
  content: "";
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--saved);
  box-shadow: 0 0 8px var(--saved);
  animation: lampPulse 2s ease-in-out infinite;
}
@keyframes lampPulse { 50% { opacity: .45; } }
/* tabular-nums für die ganze Leiste: die Zahlen dürfen die Breite ihrer Nachbarn
   nicht bewegen. */
.hud-stats {
  display: flex; gap: 16px; margin-left: auto;
  font-family: var(--mono); font-size: .85rem;
  font-variant-numeric: tabular-nums;
}
.hud-stat b { font-size: 1.05rem; display: inline-block; }
.hud-stat b.bump { animation: statBump 300ms var(--ease-out-strong); }
@keyframes statBump { 40% { transform: scale(1.3); } }
/* Die Kasse ist eine Zahl und keine Marke. Sie war orange, weil Orange früher
   »das System« hieß; jetzt steht sie in --ink und wird rot, wenn sie leer ist.
   Das ist der schärfere Wechsel: --ink gegen --distress misst 3,25:1 gegeneinander,
   --brand-lit gegen --distress nur 2,06:1. Voll steht sie bei 15,95:1 auf der
   Leiste, leer bei 4,91:1 — beide über der Latte, und der Sprung dazwischen ist
   der größte, den die Palette hergibt. */
/* Die Breite ist auf »12.000 €« gestellt und wird nicht mehr kleiner. Vorher baute
   die Leiste genau im schlimmsten Moment um: bei »1.000 €« → »0 €« wurde der
   Statistikblock schmal genug, dass der Spendenaufruf aus seiner Zeile rutschte —
   204 px nach rechts und 54 px nach oben, gemessen auf 390 px. Ein Knopf, der genau
   dann umzieht, wenn man ihn braucht, ist die schlechteste Bewegung im Spiel. */
.hud-stat.kasse { position: relative; }
.hud-stat.kasse b { color: var(--ink); font-size: .95rem; min-width: 8ch; }
.hud-stat.kasse b.low { color: var(--distress); }
/* Das abfliegende Preisschild. Es sitzt über der Zahl statt auf ihr, damit es sie
   nicht verdeckt — auch nicht bei reduzierter Bewegung, wo es 1,4 s still steht. */
.kasse-tag {
  position: absolute; right: 0; bottom: 100%;
  font-family: var(--mono); font-weight: 700; font-size: .72rem;
  color: var(--distress);
  text-shadow: 0 1px 3px oklch(0.16 0.03 245 / .95);
  white-space: nowrap; pointer-events: none;
  animation: kasseWeg 1.2s var(--ease-out-strong) forwards;
}
@keyframes kasseWeg {
  0%   { opacity: 0; transform: translateY(5px); }
  16%  { opacity: 1; transform: translateY(0); }
  100% { opacity: 0; transform: translateY(-13px); }
}
/* Der Spendenaufruf ist das Markenfeld auf dem Einsatz-Screen: rote Fläche, weißes
   Wort, ein weißer Haarstrich als Kante. Der Strich ist kein Schmuck — ein Feld bei
   L 0,44 misst gegen die Leiste 2,19:1 und hätte sonst keine Kante, die 1.4.11
   genügt; der Strich trägt sie mit 18,47:1. Dasselbe Paar wie bei den Plättchen.
   Ausgerechnet dieser Knopf trägt die Marke, weil er der einzige ist, bei dem es
   ums Geld geht. */
.appealbtn {
  border: 1px solid var(--brand-ink);
  background: var(--brand); color: var(--brand-ink);
  font-weight: 700; font-size: .82rem; letter-spacing: .03em;
  padding: 9px 14px; border-radius: 10px; cursor: pointer;
  flex-shrink: 0;
  transition: background .15s, transform 120ms var(--ease-out-strong);
}
@media (hover: hover) { .appealbtn:hover { background: var(--brand-deep); } }
.appealbtn:active { transform: scale(0.97); }
/* Leere Kasse: das Feld bleibt die Marke, der Alarm kommt als pulsierender Ring in
   --distress dazu. Vorher war der Knopf selbst orange getönt — jetzt sagt das Feld
   weiter »hier gibt es Geld« und der Ring sagt »jetzt«. Zwei Aussagen, zwei Mittel,
   und der Alarmton bleibt dem Alarm vorbehalten. */
.appealbtn.broke {
  box-shadow: 0 0 0 2px var(--distress);
  animation: appealPulse 1.1s ease-in-out infinite;
}
@keyframes appealPulse {
  50% { box-shadow: 0 0 0 2px var(--distress), 0 0 0 8px var(--distress-soft); }
}
.hud-stat.saved b { color: var(--saved); }
.hud-stat.lost b { color: var(--distress); }
/* Verschleppt ist nicht ertrunken. Die Farbe dafür steht im System und wurde hier
   bisher nicht benutzt; die Bilanz trennt die beiden Zahlen längst, die Leiste nicht. */
.hud-stat.lost b .entf { color: var(--abducted); }
.hud-stat .lbl { color: var(--ink-faint); font-size: .68rem; display: block; text-transform: uppercase; letter-spacing: .08em; }
.iconbtn {
  border: 1px solid var(--line); background: none; color: var(--ink-soft);
  width: 38px; height: 38px; border-radius: 10px; cursor: pointer; font-size: 1rem;
  flex-shrink: 0;
  transition: transform 120ms var(--ease-out-strong), border-color .15s, color .15s;
}

.layout {
  display: flex; gap: 30px; justify-content: center; align-items: flex-start; flex-wrap: wrap;
}

/* Karte */
.chart-block { display: flex; flex-direction: column; gap: 8px; }
/* ── Die Spaltenbreite gehört der Karte, und nur ihr ──
   Der teuerste Fehler dieser Sitzung, in einer Zeile beschreibbar und in einer Zeile
   behoben. Der Kartenblock ist eine Flex-Spalte: seine Breite ist die des breitesten
   Kindes. Bildunterschrift und Statusleiste tragen aber Text wechselnder Länge, und
   ohne Bremse wanderte deren max-content-Breite nach oben in den Block. Gemessen bei
   390 px: mit zwei Auflagen wuchs das Dokument auf 433 px, mit vier auf 975; die
   Karte selbst von 350,9 auf 947,5 px, und weil das Layout zentriert, hing sie links
   über den Nullpunkt hinaus. Der Anfang der Zeile, für die S2 überhaupt gebaut wurde,
   stand außerhalb des Fensters und war auch nicht hinzurollen: »afen zu · 2 Suchen
   fallen aus«. In einer echten Schicht auf 390 px trat das in 6 bis 11 von 33
   Schritten auf, mit bis zu 218 px Querlauf. Der alte Stand: konstant 0.

   `width: 0` nimmt beiden Zeilen die intrinsische Breite, `min-width: 100 %` gibt
   ihnen die Spalte trotzdem ganz. Erst damit greift das overflow:hidden unten wie
   beabsichtigt, und erst damit hat die Rangfolge in renderStatus() (teuerste Auflage
   vorn) überhaupt einen Sinn: bis hierher wurde nicht rechts abgeschnitten, sondern
   links hinausgeschoben.

   Ein `min-width: 0` am Block stand hier einen Tag lang als Gürtel dazu und ist wieder
   weg. Es war nicht nur überflüssig — die zwei Zeilen tragen nach der Regel oben von
   sich aus nichts mehr zur Breite bei, gemessen identisch mit und ohne —, es hat auch
   etwas kaputt gemacht: bei 320 px drückte es die Spalte von 304,47 auf 296 px, und
   damit verschwand die Reserve von 0,77 px, mit der die 3D-Ebene ihre Spaltenköpfe um
   7 px nach rechts schiebt. Ergebnis waren 4 px Querlauf auf dem Spielschirm, genau in
   der Breite, in der WCAG 1.4.10 gemessen wird. Ein Gürtel, der nur zusätzlich sichern
   soll, darf keine eigene Wirkung haben; dieser hatte eine.

   Und die Lehre, weil sie teuer war: ich hatte auf sechs Geräten gemessen, dass sich
   die Kartenoberkante nicht bewegt, und daraus geschlossen, es gebe keine
   Verschiebung. Die Karte fuhr die ganze Zeit seitwärts. Wer Layout prüft, misst
   beide Achsen. */
.chart-caption, .chart-status { width: 0; min-width: 100%; }
/* Die Bildunterschrift bricht nicht um. Sie trägt links die laufenden Auflagen, und
   ein Umbruch dort schöbe die Karte nach unten. Was rechts nicht mehr hinpasst, wird
   abgeschnitten; deshalb steht in renderStatus() die teuerste Auflage vorn. Der Zug
   bleibt unverkürzbar, er ist die Uhr. */
.chart-caption {
  display: flex; justify-content: space-between; align-items: center; gap: 14px;
  flex-wrap: nowrap; overflow: hidden;
  font-family: var(--mono); font-size: .75rem; color: var(--ink-faint);
  padding: 0 4px;
  /* Feste Zeile. Ohne sie wächst die Reihe um 0,45 px, sobald das erste
     Auflagen-Plättchen erscheint: sein Rahmen liegt unter der Grundlinie der
     Schrift. Ein halbes Pixel ist unsichtbar und trotzdem eine Verschiebung der
     Karte, und diese Datei zählt Verschiebungen. Das Plättchen misst 17,5 px und
     passt damit in die 18. */
  line-height: 1.5; height: 1.5em;
}
/* Die Uhr ist unverkürzbar und unbeweglich: feste Breite auf »Zug 10«, damit der
   Sprung von einstellig auf zweistellig die Reihe nicht umbaut. Dieselbe Behandlung
   wie die Kasse in der Leiste. */
.chart-caption .zug {
  color: var(--ink-soft); margin-left: auto; flex-shrink: 0;
  font-variant-numeric: tabular-nums; min-width: 7ch; text-align: right;
}
.chart-caption > #chartOrt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chart-caption .mass { display: none; flex-shrink: 0; margin-left: auto; }
.chart-caption .auflagen { display: flex; gap: 6px; min-width: 0; overflow: hidden; }
/* Die teuerste Auflage steht vorn (renderStatus setzt die Reihenfolge) und bleibt ganz.
   Gekürzt wird ab der zweiten — sonst schrumpfen alle gleichmäßig und keine ist mehr
   vollständig zu lesen. */
.chart-caption .auflagen > :first-child { flex-shrink: 0; }
/* Eine Auflage ist kein Alarm, sondern eine Fessel: sie steht in der Alarmfarbe,
   aber als Umriss und nicht als Fläche. Die Fläche gehört dem, was jetzt passiert.
   Gemessen gegen den Grund der Bildunterschrift: die Schrift trägt 6,78:1, der
   Rahmen 3,55:1. Die ersten 62 % Deckung lagen bei 2,50:1 und damit unter der
   3:1-Latte, die in dieser Datei für jede tragende Kante gilt — 80 % bringen ihn
   mit Reserve darüber (75 % wären 3,23:1, also knapp). */
.auflage {
  border: 1px solid color-mix(in oklch, var(--distress) 80%, transparent);
  color: var(--distress-lit);
  border-radius: 5px; padding: 0 5px;
  /* Schrumpfen mit Auslassungspunkten statt hart abgeschnitten. Mit flex-shrink: 0
     endete die zweite Auflage im echten Spiel mitten im Wort an der Kante der Reihe —
     der Schnitt war unsichtbar und las sich wie ein Tippfehler. Jetzt steht dort
     »Verfahren läuft · 2 Rettungen ohne …«, und man sieht, dass etwas fehlt. */
  white-space: nowrap; flex-shrink: 1; min-width: 7ch;
  overflow: hidden; text-overflow: ellipsis;
  font-size: .92em; letter-spacing: .01em;
  /* Eigene Zeilenhöhe, sonst wächst die Bildunterschrift um 0,55 px, sobald die
     erste Auflage erscheint — Rahmen und Innenabstand kämen zur geerbten Zeile
     dazu. Gemessen und abgestellt: die Karte darf sich nicht bewegen, wenn eine
     Behörde etwas anordnet. */
  line-height: 1.4;
}

/* ── Die Zeile unter der Karte ──
   Feste Höhe, zwei Zeilen, immer. Der Text darin wechselt bei jedem Zug und ist
   zwischen 21 und rund 95 Zeichen lang; ohne feste Höhe wanderte die Karte bei
   jeder Meldung um eine Zeile. Zwei Zeilen sind der Kompromiss: die beiden Sätze,
   auf die es ankommt (die Geldregel im ersten Zug, der Ausweg bei leerer Kasse),
   passen bei 320 px hinein, eine überlange Funkmeldung wird abgeschnitten und
   steht vollständig weiterhin im Protokoll.

   line-clamp statt overflow:hidden, damit nie eine halbe Zeile stehen bleibt. */
.chart-status {
  font-family: var(--mono); font-size: .72rem; line-height: 1.4;
  color: var(--ink-soft);
  padding: 0 4px;
  height: 2.8em;
}
.chart-status .lage {
  display: -webkit-box; -webkit-box-orient: vertical;
  -webkit-line-clamp: 2; line-clamp: 2;
  overflow: hidden;
}
.chart-status .lage.grim { color: var(--distress); }
.chart-status .lage.good { color: var(--saved); }
.gridbox {
  position: relative;                    /* Bezug für die Stempelschicht, siehe game.js */
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-rows: auto 1fr;
  gap: 4px;
  user-select: none; -webkit-user-select: none;
}
.collabels, .rowlabels { display: grid; gap: 2px; }
.collabels { grid-column: 2; grid-template-columns: repeat(10, var(--cell)); }
.rowlabels { grid-row: 2; grid-template-rows: repeat(10, var(--cell)); }
.collabels span, .rowlabels span {
  display: flex; align-items: center; justify-content: center;
  font-family: var(--mono); font-size: .6rem; color: var(--ink-faint);
}
.rowlabels span { width: 1.3em; }
.grid {
  grid-column: 2; grid-row: 2;
  display: grid;
  grid-template-columns: repeat(10, var(--cell));
  grid-template-rows: repeat(10, var(--cell));
  gap: 2px;
  padding: 6px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 400'%3E%3Cg fill='none' stroke='%23a8c8e8' stroke-opacity='.06'%3E%3Cpath d='M-20 90 C 80 60, 150 130, 240 100 S 400 60, 430 90'/%3E%3Cpath d='M-20 150 C 70 120, 170 190, 260 160 S 400 130, 430 160'/%3E%3Cpath d='M-20 235 C 90 205, 160 270, 250 240 S 410 210, 430 240'/%3E%3Cpath d='M-20 320 C 80 295, 180 350, 270 322 S 400 300, 430 325'/%3E%3C/g%3E%3C/svg%3E") center / cover,
    linear-gradient(oklch(0.75 0.05 245 / .04) 1px, transparent 1px) 0 0 / 100% calc(var(--cell) + 2px),
    linear-gradient(160deg, oklch(0.22 0.04 245 / .5), oklch(0.14 0.03 245 / .7));
  touch-action: manipulation;
  position: relative;
  overflow: hidden;
}
/* Radar-Sweep. Der Strahl ist die Marke, nur sehr leise — er darf nie so weit
   hochlaufen, dass eine Zelle unter ihm nach Alarm aussieht. */
.grid::before {
  content: "";
  position: absolute; inset: -55%;
  background: conic-gradient(from 0deg at 50% 50%,
    transparent 0deg 290deg,
    color-mix(in oklch, var(--brand-lit) 2%, transparent) 300deg,
    color-mix(in oklch, var(--brand-lit) 10%, transparent) 345deg,
    color-mix(in oklch, var(--brand-lit) 22%, transparent) 359deg,
    transparent 360deg);
  animation: sweep 9s linear infinite;
  z-index: 0;
  pointer-events: none;
}
@keyframes sweep { to { transform: rotate(360deg); } }
/* Scanlines + Vignette */
.grid::after {
  content: "";
  position: absolute; inset: 0;
  background:
    repeating-linear-gradient(0deg, oklch(1 0 0 / .022) 0 1px, transparent 1px 3px),
    radial-gradient(ellipse at center, transparent 55%, oklch(0 0 0 / .3));
  pointer-events: none;
  z-index: 3;
}
.cell { z-index: 1; }
/* Sektor-Overlays (Lagebild) */
.overlay-layer {
  position: absolute; inset: 0;
  pointer-events: none;
  z-index: 2;
}
/* ── Der Sektorkasten, und damit die eine Stelle, an der Marke und Alarm sich
   direkt gegenüberstehen ──
   Ruhig und dringend unterscheiden sich hier nicht mehr im Farbton (11° trennen
   --brand von --distress, das trägt nichts), sondern in der HELLIGKEIT, und zwar in
   beide Richtungen zugleich:

     ruhig     heller, blasser Rahmen  ·  DUNKLES Plättchen mit weißem Wort
     dringend  satter, roter Rahmen    ·  HELLES  Plättchen mit schwarzem Wort

   Die Plättchen sind also gegensätzlich gepolt, nicht bloß verschieden gefärbt.
   Gemessen, jeweils ruhig gegen dringend:
     Rahmen    ΔE 0,177 · 1,71:1 Leuchtdichte   (heute: ΔE 0,114 · 1,50:1)
     Plättchen ΔE 0,265 · 2,94:1 Leuchtdichte   (heute: ΔE 0,090 · 1,22:1)
   Beides besser als heute, und die 2,94:1 heißen: auch wer den Farbton gar nicht
   sieht, sieht den Unterschied.
   Der helle Haarstrich am ruhigen Plättchen ist Pflicht und nicht Zierrat: die
   Markenfläche misst gegen die Karte 1,95:1, der Saum darum 7,08:1. Ein Paar wie
   überall in dieser Datei — die eine Hälfte trägt, wo die andere es nicht kann. */
/* Die Füllung gehört seit dem 4.8. nur noch dem Alarm.
   Gemessen lagen im Mittel 71,7 der 100 Zellen in mindestens einem Sektor, und 81,8 %
   der Kartenpixel ändern sich, wenn man die Füllungen abschaltet — eine Tönung, die
   fast überall liegt, unterscheidet fast nichts. Was den ruhigen Kasten trägt, ist der
   gestrichelte Rand und das Plättchen; das reicht, und es macht den Sprung auf
   »dringend« erst zu einem Sprung. */
.sector-box {
  position: absolute;
  border: 1px dashed color-mix(in oklch, var(--brand-lit) 80%, transparent);
  border-radius: 6px;                                  /* 6,13:1 gegen die Karte (1.4.11 will 3:1) */
}
.sector-box .tag {
  position: absolute; top: -1px; left: -1px;
  font-family: var(--mono); font-size: .58rem; letter-spacing: .05em;
  background: var(--brand); color: var(--brand-ink);
  box-shadow: 0 0 0 1px color-mix(in oklch, var(--brand-lit) 85%, transparent);
  padding: 1px 5px; border-radius: 5px 0 5px 0;
  font-weight: 700;
}
/* Die .75 waren zu wenig: der dringende Rahmen misst damit 2,96:1 gegen die Karte
   und verfehlt seine eigene, hier seit jeher notierte 3:1-Latte. Ein Alarm, der
   knapp unter der Schwelle liegt, ist beim Umstellen der Marke der falsche Ort
   zum Wegsehen — .88 bringt ihn darüber, und der Abstand zum ruhigen Rahmen
   wächst dabei mit. */
/* Dringend: durchgezogen statt gestrichelt, und erst hier kommt die Füllung dazu.
   Die Linienart ist die zweite, farbunabhängige Aussage — wer den Farbton nicht
   sieht, sieht den Wechsel von gestrichelt auf durchgezogen. */
.sector-box.urgent {
  border-style: solid;
  border-color: color-mix(in oklch, var(--distress) 88%, transparent);
  background: color-mix(in oklch, var(--distress) 5%, transparent);
}
/* Weiß auf --distress erreicht nur 4,3:1 — dunkle Schrift auf hellem Rot ist sicherer */
.sector-box.urgent .tag {
  background: var(--distress-lit); color: var(--distress-ink);
  box-shadow: none;
}
/* ── Kritisch, und zwar ohne Bewegung ──
   Bis zum 4.8. war »kritisch« ausschließlich eine Animation: unter html.ruhig änderten
   sich zwischen einem Boot mit 2 und einem mit 8 Restzügen **0 von 1.296.000 Pixeln**.
   Mit Bewegung war es schlechter als nichts — sectorBlink verbrachte die halbe Periode
   bei Deckkraft 0,35, ein Blick traf also mit 50 % Wahrscheinlichkeit den Moment, in
   dem der kritische Kasten der blasseste von dreien war. Aus einer echten Schicht:
   N2 mit 3 Zügen in Weiß, N3 mit 8 in Rot.

   Zwei Änderungen. Erstens eine doppelte Linie, die immer da ist: derselbe Griff, den
   die 3D-Ebene am Rand längst benutzt (staging.js legt bei CRITICAL einen zweiten
   Balken unter die Klammer). Kein Farbwechsel — der ist bei »dringend« aufgebraucht.
   Zweitens blinkt der Kasten jetzt nach OBEN: der Grundzustand ist der auffällige,
   die Animation legt noch etwas drauf. Ein Standbild kann damit nie untertreiben. */
.sector-box.critical {
  /* 85 %, nicht weniger: der Ring IST die dritte Stufe und muss die 3:1 dieser Datei
     selbst tragen. Gemessen gegen die Karte 3,93:1 (bei 62 % wären es 2,50:1 gewesen,
     also eine Auszeichnung knapp unter ihrer eigenen Latte).
     Und er liegt INNEN. Nach außen gelegt füllte er die 3-px-Rasterlücke zum
     Nachbarsektor, und 57,3 % der Kästen grenzen an einen anderen: aus zwei Rahmen mit
     Lücke wurde ein durchgehender Block, und die Auszeichnung gehörte optisch dem
     Nachbarn mit. Innen kann sie nie in der Lücke landen und bleibt bei ihrem Kasten. */
  box-shadow: inset 0 0 0 2px color-mix(in oklch, var(--distress) 85%, transparent);
  animation: sectorBlink 1s steps(2) infinite;
}
@keyframes sectorBlink {
  50% {
    background: color-mix(in oklch, var(--distress) 15%, transparent);
    box-shadow: inset 0 0 0 3px color-mix(in oklch, var(--distress) 88%, transparent);
  }
}
/* Boots-Silhouetten (Lagebild) */
.boat-visual {
  position: absolute;
  z-index: 2;
  pointer-events: none;
  color: var(--saved);
  filter: drop-shadow(0 0 12px oklch(0.78 0.16 150 / .55));
  animation: boatIn .8s var(--ease-out-strong) both;
}
@keyframes boatIn {
  0% { transform: scale(.45); opacity: 0; }
  55% { transform: scale(1.1); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}
.boat-visual svg { position: absolute; display: block; }
.boat-visual .hull { fill: color-mix(in oklch, currentColor 22%, transparent); stroke: currentColor; stroke-width: 2.5; stroke-linejoin: round; }
.boat-visual .cabin { fill: color-mix(in oklch, currentColor 40%, transparent); stroke: currentColor; stroke-width: 2; }
.boat-visual .detail { fill: none; stroke: currentColor; stroke-width: 2; }
.boat-visual .tube { fill: color-mix(in oklch, currentColor 12%, transparent); stroke: currentColor; stroke-width: 6.5; }
.boat-visual .p { fill: var(--ink); }
.boat-visual .break { display: none; fill: none; stroke: var(--sea); stroke-width: 5.5; stroke-linejoin: round; }
.boat-visual.sunk {
  color: var(--distress);
  filter: drop-shadow(0 0 9px oklch(0.5 0.2 25 / .55));
}
.boat-visual.sunk .break { display: block; }
.boat-visual.sunk .p { opacity: .75; }
.boat-visual.abducted {
  color: var(--abducted);
  filter: drop-shadow(0 0 8px oklch(0.55 0.06 300 / .5));
}
.boat-visual.abducted .hull, .boat-visual.abducted .tube { stroke-dasharray: 7 5; }
/* Glyphen unter Boots-Silhouette ausblenden */
.cell.covered::before, .cell.covered::after { display: none !important; }

/* Treffer-Burst: Ringe + Funken */
.burst {
  position: absolute;
  width: var(--cell); height: var(--cell);
  pointer-events: none;
}
.burst .ring {
  position: absolute; inset: 0;
  border: 2.5px solid color-mix(in oklch, var(--brand-lit) 85%, transparent);
  border-radius: 50%;
  animation: burstRing .8s var(--ease-burst) forwards;
}
.burst .ring.slow { animation-duration: 1.15s; animation-delay: .08s; border-width: 1.5px; opacity: .7; }
@keyframes burstRing {
  0% { transform: scale(.35); opacity: 1; }
  100% { transform: scale(3.1); opacity: 0; }
}
.burst .spark {
  position: absolute; left: 50%; top: 50%;
  width: 4px; height: 4px; margin: -2px;
  border-radius: 50%;
  background: var(--brand-lit);
  animation: sparkFly .7s var(--ease-burst) forwards;
}
@keyframes sparkFly {
  0% { transform: rotate(var(--a)) translateX(0); opacity: 1; }
  100% { transform: rotate(var(--a)) translateX(calc(var(--cell) * 1.5)); opacity: 0; }
}
.burst.rescue .ring { border-color: oklch(0.8 0.16 150 / .9); }
.burst.rescue .ring.slow { animation-duration: 1.3s; }
.burst.rescue .spark { background: oklch(0.82 0.15 150); }
.burst.rescue { animation: none; }
.burst.rescue .ring { animation-name: burstRingBig; }
@keyframes burstRingBig {
  0% { transform: scale(.4); opacity: 1; }
  100% { transform: scale(4.4); opacity: 0; }
}
/* Aufsteigendes Rettungs-Label */
.float-tag {
  position: absolute;
  transform: translate(-50%, 0);
  font-family: var(--mono); font-weight: 700;
  font-size: clamp(.72rem, 2.4vw, .95rem);
  letter-spacing: .06em;
  color: oklch(0.85 0.16 150);
  text-shadow: 0 0 14px oklch(0.78 0.16 150 / .8), 0 1px 2px oklch(0 0 0 / .8);
  white-space: nowrap;
  animation: floatUp 1.6s var(--ease-out-strong) forwards;
  z-index: 4;
}
@keyframes floatUp {
  0% { opacity: 0; translate: 0 6px; }
  18% { opacity: 1; translate: 0 0; }
  75% { opacity: 1; }
  100% { opacity: 0; translate: 0 -38px; }
}
/* Grüne Rettungs-Blende */
.flash.good-flash {
  background: radial-gradient(ellipse at center, transparent 50%, oklch(0.7 0.15 150 / .35));
}
/* Such-Ping */
.ripple {
  position: absolute;
  width: var(--cell); height: var(--cell);
  border: 2px solid color-mix(in oklch, var(--brand-lit) 80%, transparent);
  border-radius: 50%;
  transform: scale(.3);
  opacity: .9;
  animation: rippleOut .7s var(--ease-burst) forwards;
}
@keyframes rippleOut { to { transform: scale(2.4); opacity: 0; } }
.cell {
  position: relative;
  border: 1px solid var(--line-soft);
  border-radius: 4px;
  background: oklch(0.28 0.05 245 / .25);
  padding: 0; margin: 0;
  cursor: crosshair;
  transition: background .12s, border-color .12s, transform 100ms var(--ease-out-strong);
  font-family: var(--mono);
  font-size: calc(var(--cell) * .38);
}
/* Getragen wird der Zeiger vom RAHMEN, nicht von der Tönung: der Rahmen misst
   5,99:1 gegen die Zelle darunter (vorher 4,59:1), die Tönung 1,73:1 (vorher
   1,53:1). Eine 26-%-Tönung kann gegen ihren eigenen Grund gar keine 3:1
   erreichen, und sie muss es auch nicht — sie ist die Beigabe, der Rahmen ist
   die Aussage. Beide Zahlen sind gestiegen. */
@media (hover: hover) {
  .grid:not(.inert) .cell.free:hover {
    background: color-mix(in oklch, var(--brand-lit) 26%, transparent);
    border-color: var(--brand-lit);
  }
}
.cell:focus-visible { outline: 2px solid var(--brand-lit); outline-offset: -2px; }
.grid.inert .cell { cursor: default; }

.cell.water { cursor: default; }
.cell.water::after {
  content: ""; position: absolute; inset: 0; margin: auto;
  width: 24%; height: 24%; border-radius: 50%;
  background: oklch(0.62 0.03 245);            /* 3:1 gegen die Zellfläche (1.4.11) */
}
/* Kontakt ist ein FUND und kein Unglück — deshalb die helle Marke und nicht der
   Alarmton. Der Ton läuft dabei in die Gegenrichtung zur gesunkenen Zelle: hier
   hell auf dunklem Grund, dort rot. Gemessen stehen die beiden Glyphen ◉ und ✕
   jetzt bei ΔE 0,337 gegeneinander statt bei 0,134, in Leuchtdichte 2,81:1
   statt 1,50:1. */
.cell.contact {
  background: color-mix(in oklch, var(--brand-lit) 30%, transparent);
  border-color: var(--brand-lit);
  cursor: default;
  animation: contactFlash .6s var(--ease-out-strong);
  box-shadow: 0 0 10px 0 color-mix(in oklch, var(--brand-lit) 28%, transparent);
}
@keyframes contactFlash {
  0%   { transform: scale(.3); filter: brightness(3.2); box-shadow: 0 0 0 0 color-mix(in oklch, var(--brand-lit) 90%, transparent); }
  45%  { transform: scale(1.28); filter: brightness(2); box-shadow: 0 0 30px 8px color-mix(in oklch, var(--brand-lit) 60%, transparent); }
  100% { transform: scale(1); filter: brightness(1); box-shadow: 0 0 10px 0 color-mix(in oklch, var(--brand-lit) 28%, transparent); }
}
/* Weiß und nicht --brand-lit: beide kämen über die 4,5 (5,47:1 gegen gemessene
   8,06:1), aber Weiß stellt die Glyphe am weitesten von der roten ✕ der
   gesunkenen Zelle weg — und genau diese beiden dürfen nie verwechselt werden. */
.cell.contact::after { content: "◉"; color: var(--brand-ink); display: grid; place-items: center; position: absolute; inset: 0; }
.cell.rescued {
  background: oklch(0.78 0.16 150 / .26);
  border-color: var(--saved);
  cursor: default;
  animation: rescuePop .55s var(--ease-out-strong) both;
  box-shadow: 0 0 12px 0 oklch(0.78 0.16 150 / .3);
}
@keyframes rescuePop {
  0%   { transform: scale(.4); filter: brightness(2.6); box-shadow: 0 0 0 0 oklch(0.78 0.16 150 / .9); }
  50%  { transform: scale(1.32); filter: brightness(1.8); box-shadow: 0 0 28px 8px oklch(0.78 0.16 150 / .6); }
  100% { transform: scale(1); filter: brightness(1); box-shadow: 0 0 12px 0 oklch(0.78 0.16 150 / .3); }
}
.cell.rescued::after { content: "✓"; color: var(--saved); font-weight: 700; display: grid; place-items: center; position: absolute; inset: 0; }
.cell.sunkcell {
  background: color-mix(in oklch, var(--lost) 30%, transparent);
  border-color: var(--lost);
  cursor: default;
}
/* Angehoben, wie es die 3D-Fassung schon tut: --distress misst auf der eigenen
   Zellfläche 3,90:1 und verfehlt die 4,5. --distress-lit holt sie: 5,33:1.
   Beide Fassungen sagen jetzt dasselbe über denselben Token — sie standen vorher
   als zwei getrennte rohe Werte da (0,21 hier, 0,20 dort), also als zwei Farben,
   die nur zufällig fast gleich waren. */
.cell.sunkcell::after { content: "✕"; color: var(--distress-lit); display: grid; place-items: center; position: absolute; inset: 0; }
.cell.abducted-cell {
  background: oklch(0.55 0.06 300 / .2);
  border-color: var(--abducted);
  cursor: default;
}
.cell.abducted-cell::after { content: "▲"; color: var(--abducted); display: grid; place-items: center; position: absolute; inset: 0; font-size: .8em; }
.cell.pending { border-color: var(--ink-faint); cursor: default; }
.cell.pending::after { content: "?"; color: var(--ink-faint); display: grid; place-items: center; position: absolute; inset: 0; }
.cell.locked {
  cursor: not-allowed;
  background: repeating-linear-gradient(45deg,
    color-mix(in oklch, var(--distress) 12%, transparent) 0 5px, transparent 5px 10px);
  border-color: color-mix(in oklch, var(--distress) 40%, transparent);
  z-index: var(--z-locked);
}
.grid.shake { animation: shake .4s; }
@keyframes shake {
  20% { transform: translate(-3px, 1px); } 40% { transform: translate(3px, -1px); }
  60% { transform: translate(-2px, -2px); } 80% { transform: translate(2px, 1px); }
}

/* rote Blende beim Untergang */
.flash {
  position: fixed; inset: 0; pointer-events: none; z-index: var(--z-flash);
  background: radial-gradient(ellipse at center, transparent 45%, oklch(0.5 0.2 25 / .45));
  opacity: 0; transition: opacity .25s;
}
.flash.on { opacity: 1; }

/* Seitenpanels */
.side { display: flex; flex-direction: column; gap: 14px; width: 320px; max-width: 100%; }
.panel {
  border: 1px solid var(--line);
  background: var(--panel);
  border-radius: var(--radius);
  padding: 14px 16px;
  backdrop-filter: blur(6px);
}
.panel h2 {
  margin: 0 0 10px;
  font-family: var(--mono);
  font-size: .72rem; letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink-faint); font-weight: 400;
}

/* Mayday-Liste */
.mayday { display: flex; flex-direction: column; gap: 9px; }
.boat {
  border: 1px solid var(--line-soft);
  border-radius: 10px;
  padding: 9px 11px;
  display: grid;
  /* minmax(0, 1fr), nicht 1fr: eine Rasterspur ist von Haus aus mindestens so breit wie
     ihr Inhalt, und bei 200 % Textvergrößerung schob der Bootsname die Spalte auf, bis
     das Wort der erledigten Karte 97,5 px aus der Seite lief. Älter als S4, hier gleich
     mit erledigt. */
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 2px 10px;
  font-size: .84rem;
  transition: border-color .3s, background-color .3s, opacity .3s;
}
.boat .name { font-weight: 700; letter-spacing: .02em; grid-column: 1; grid-row: 1; align-self: center; }
/* Dasselbe Plättchen wie auf der Karte, und aus demselben Grund gegensätzlich
   gepolt zum dringenden darunter: dunkles Feld, weißes Wort. Es steht auf JEDER
   Karte, egal in welchem Zustand — es ist deshalb eine Konstante und kann gar nicht
   als Zustandsangabe missverstanden werden. Den Zustand tragen Rahmen, Wort und
   Countdown. */
.boat .call {
  font-family: var(--mono); font-size: .62rem; font-weight: 700;
  background: var(--brand); color: var(--brand-ink);
  box-shadow: 0 0 0 1px color-mix(in oklch, var(--brand-lit) 85%, transparent);
  padding: 1px 5px; border-radius: 4px; margin-right: 6px;
  vertical-align: 1px;
}
/* Weiß auf --distress erreicht nur 4,3:1 — dunkle Schrift auf hellem Rot ist sicherer.
   Der Saum entfällt hier: das helle Feld hat gegen die Tafel schon von sich aus eine
   Kante, und ohne Saum ist der Gegensatz zum ruhigen Plättchen vollständig. */
.boat.urgent .call { background: var(--distress-lit); color: var(--distress-ink); box-shadow: none; }
.boat.done .call { background: oklch(0.5 0.02 245 / .5); color: var(--ink); box-shadow: none; }
.boat .meta { color: var(--ink-soft); font-size: .78rem; grid-column: 1; grid-row: 2; }
.boat .meta .sector { font-family: var(--mono); color: var(--ink); }
.boat .countdown {
  grid-column: 2; grid-row: 1 / span 2; align-self: center; text-align: right;
  font-family: var(--mono); line-height: 1.15;
}
.boat .countdown b { font-size: 1.35rem; display: block; }
.boat .countdown .u { font-size: .62rem; color: var(--ink-faint); text-transform: uppercase; letter-spacing: .06em; }
.boat.urgent { border-color: var(--distress); background: var(--distress-soft); }
.boat.urgent .countdown b { color: var(--distress); }
/* Auf dem rot getönten Untergrund von .urgent reicht --ink-faint nicht mehr: gemessen
   4,05:1 gegen die nötigen 4,5:1. Nur hier anheben — global angehoben würde die ruhige
   Abstufung aller übrigen Karten mitverschoben. Ein Zustand ein Zug vor der Deadline ist
   Kernspielzustand, der Fehler ist im normalen Spiel also erreichbar. */
.boat.urgent .countdown .u { color: oklch(0.68 0.02 245); }
/* Dieselbe doppelte Linie wie am Sektorkasten, hier als abgesetzter Umriss. Sie steht
   immer, auch ohne Bewegung, und sie wechselt die Farbe nicht — »dringend« hat den
   Farbton schon verbraucht. Der Karte, die gleich untergeht, sieht man das jetzt auch
   im Standbild an. */
.boat.critical { outline: 1px solid var(--distress); outline-offset: 2px; }
/* Farbwechsel statt Opazität: bei opacity .25 fiel der Kontrast auf 1,36:1.
   Und die Richtung ist umgekehrt: vorher fiel die Zahl auf --ink, also ausgerechnet
   auf die Farbe der RUHIGEN Karten — die halbe Periode über log die Anzeige. Jetzt
   geht sie nach oben, auf --distress-lit. */
.boat.critical .countdown b { animation: urgentBlink 1.1s steps(2) infinite; }
@keyframes urgentBlink { 50% { color: var(--distress-lit); } }
/* Keine Gruppen-Opazität: sie drückt jeden Text darunter unter die Kontrastschwelle.
   Die Zurücknahme passiert stattdessen über die Rahmenfarbe. */
.boat.done { border-color: var(--line-soft); }
.boat .status { font-family: var(--mono); font-size: .95rem; grid-column: 2; grid-row: 1 / span 2; align-self: center; text-align: right; overflow-wrap: anywhere; }
.boat .name { overflow-wrap: anywhere; }
.boat.rescued-b { border-color: var(--saved); }
.boat.rescued-b .status { color: var(--saved); }
.boat.sunk-b { border-color: var(--lost); }
.boat.sunk-b .status { color: var(--distress); }
.boat.abducted-b { border-color: var(--abducted); }
.boat.abducted-b .status { color: var(--abducted); font-size: .78rem; }
/* Linke Spalte statt volle Breite: so steht der Fundbalken nicht mehr unter der Uhr
   und kann nicht mehr für sie gehalten werden. Die Beschriftung ist einspaltig gesetzt
   (tabular-nums, feste Breite), damit sie den Balken bei keinem Zählerstand verschiebt. */
.boat .progress {
  display: flex; align-items: center; gap: 7px;
  grid-column: 1; grid-row: 3; margin-top: 4px;
}
.boat .plabel {
  font-family: var(--mono); font-size: .62rem; color: var(--ink-faint);
  font-variant-numeric: tabular-nums; white-space: nowrap; flex-shrink: 0;
}
.boat.urgent .plabel { color: oklch(0.68 0.02 245); }   /* dieselbe Anhebung wie .countdown .u */
.boat .segs { display: flex; gap: 3px; flex: 1; }
.boat .seg {
  height: 6px; flex: 1; border-radius: 3px;
  background: oklch(0.75 0.05 245 / .6);       /* leere Segmente: gemessen 3,74:1 */
}
/* Das gefüllte Segment muss HELLER sein als das leere, sonst liest sich »gefunden«
   als »noch offen«: --brand läge bei L 0,44 unter dem gelieferten leeren Segment
   (L 0,54) und drehte die Anzeige um — 1,71:1, und zwar nach unten. Mit
   --brand-lit steht es darüber, gemessen 2,68:1 gegen 1,82:1 heute. */
.boat .seg.on { background: var(--brand-lit); }
.boat.rescued-b .seg.on { background: var(--saved); }

/* Funkverkehr */
.radio {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: 6px;
  font-family: var(--mono); font-size: .76rem;
  max-height: 180px; overflow-y: auto;
}
.radio li { color: var(--ink-faint); }
.radio li:first-child { color: var(--ink); }
.radio li:first-child::after {
  content: "▌"; color: var(--brand-lit);
  margin-left: 5px;
  animation: cursorBlink 1.1s steps(2) infinite;
}
@keyframes cursorBlink { 50% { opacity: 0; } }
.radio li .t { color: var(--brand-lit); margin-right: 6px; }
.radio li.grim { color: var(--distress); }
.radio li.good { color: var(--saved); }

/* ── Fernschreiben »Festung Europa« ── */
.chart-block { position: relative; }
.telex {
  position: absolute;
  top: 30px; left: 0; right: 0;
  z-index: var(--z-card);
  display: flex; justify-content: center;
  pointer-events: none;
}
.telex-paper {
  pointer-events: auto;
  width: min(96%, 520px);
  background:
    radial-gradient(circle at 7px 50%, transparent 3px, oklch(0.93 0.015 90) 3.5px) left center / 14px 14px repeat-y,
    radial-gradient(circle at calc(100% - 7px) 50%, transparent 3px, oklch(0.93 0.015 90) 3.5px) right center / 14px 14px repeat-y,
    oklch(0.93 0.015 90);
  background-color: oklch(0.93 0.015 90);
  color: oklch(0.22 0.02 90);
  font-family: var(--mono);
  padding: 14px 26px 16px;
  transform: rotate(-.5deg);
  box-shadow: 0 18px 44px oklch(0 0 0 / .55);
  transition: translate .45s var(--ease-out-strong), opacity .35s;
}
.telex[hidden] { display: none; }
.telex.off .telex-paper { translate: 0 -130%; opacity: 0; }
.telex-head {
  display: flex; justify-content: space-between; gap: 10px;
  font-size: .64rem; letter-spacing: .14em;
  border-bottom: 1px dashed oklch(0.22 0.02 90 / .4);
  padding-bottom: 7px; margin-bottom: 10px;
}
.telex-paper h2 {
  font-family: var(--display); font-weight: 400;
  text-transform: uppercase; letter-spacing: .03em;
  font-size: 1.45rem; line-height: 1.02;
  margin: 0 0 8px;
  color: var(--telex-rot);
  text-wrap: balance;
}
.telex-satz { margin: 0 0 10px; font-size: .84rem; line-height: 1.55; min-height: 2.4em; }
.telex-satz .caret { animation: cursorBlink .8s steps(2) infinite; }
.telex-fakt {
  margin: 0 0 14px; padding-top: 9px;
  border-top: 1px dashed oklch(0.22 0.02 90 / .4);
  font-size: .74rem; line-height: 1.5;
  transition: opacity .4s;
}
.telex-fakt.pending { opacity: 0; }
/* Steht jetzt als echter Text im DOM (war vorher CSS-content und damit
   für Hilfstechnik und Textkopie unsichtbar — WCAG-Failure F87). */
.telex-fakt .fakt-marke {
  color: var(--telex-rot);
  font-size: .64rem; letter-spacing: .12em; margin-right: 7px; font-weight: 700;
  text-transform: uppercase;
}
#telexOk {
  border: 2px solid oklch(0.22 0.02 90);
  background: none; color: oklch(0.22 0.02 90);
  font-family: var(--body); font-weight: 700; font-size: .85rem;
  padding: 8px 18px; cursor: pointer; width: 100%;
  transition: background .15s, color .15s, transform 120ms var(--ease-out-strong);
}
/* Der Fokusring gehört dem Spiel und nicht dem Absender: derselbe --brand wie auf
   jedem anderen hellen Grund (6,89:1 auf dem Papier). */
#telexOk:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }

/* ── Karten-Akteure ── */
.fx { position: absolute; pointer-events: none; z-index: 4; will-change: transform; }
.fx svg { display: block; overflow: visible; }
.fx-drone { color: var(--ink-soft); filter: drop-shadow(0 0 6px oklch(0 0 0 / .6)); }
.fx-patrol { color: oklch(0.62 0.03 245); filter: drop-shadow(0 0 8px oklch(0 0 0 / .6)); }
.fx-patrol .hull { fill: color-mix(in oklch, currentColor 30%, transparent); stroke: currentColor; stroke-width: 2.5; }
.fx-patrol .det { fill: currentColor; }
.fx-tow { color: var(--abducted); }
.fx-tow .hull, .fx-tow .tube { stroke: currentColor; }
/* Sperrzonen-Band */
.zone-tape {
  position: absolute; z-index: 2; pointer-events: none;
  border: 3px solid transparent;
  border-image: repeating-linear-gradient(45deg,
    var(--distress) 0 8px, oklch(0.2 0.03 245) 8px 16px) 3;
  animation: tapeIn .5s var(--ease-out-strong) both;
}
/* ── Umgekehrt gepolt zum Notruf-Plättchen, seit dem 4.8. ──
   Vorher waren die beiden identisch: Grundfarbe, Schriftfarbe, Schriftart und Schnitt
   von `.zone-tape .tag` und `.sector-box.urgent .tag` unterschieden sich um 0,32 px
   Schriftgrad und sonst um nichts. Auf derselben Karte, 80 px auseinander, meinten sie
   das Gegenteil: »da musst du sofort hin« und »da darfst du drei Züge lang nicht«. Die
   Sperrzone war dabei das lauteste Objekt der Karte — 9,8 % ihrer Fläche, 19,5 % ihrer
   Farbenergie —, und das für eine Fläche, auf die man nicht darf.

   Sie wird deshalb nicht leiser, sondern anders: dunkles Feld, helles Wort. Damit gilt
   auf der ganzen Karte eine Regel — HELLES Feld heißt Notruf, DUNKLES Feld heißt
   Verwaltungsakt. Dieselbe Polung hat das ruhige Sektorplättchen, und dieselbe Linie
   fährt das Fernschreiben: der Gegner schreibt in Stempelrot, nicht in der Marke.
   Das Schraffurband ringsum bleibt unverändert, es trägt den Alarm weiter. */
.zone-tape .tag {
  position: absolute; top: -1px; left: -1px;
  font-family: var(--mono); font-size: .56rem; font-weight: 700; letter-spacing: .1em;
  background: oklch(0.18 0.05 25); color: var(--distress-lit);
  /* Der Saum ist Pflicht wie am ruhigen Sektorplättchen und aus demselben Grund: das
     dunkle Feld misst gegen die Karte 1,0:1 und hätte ohne ihn gar keine Kante. Bei
     80 % Deckung trägt er 3,55:1, das Wort darauf 6,61:1. */
  box-shadow: 0 0 0 1px color-mix(in oklch, var(--distress) 80%, transparent);
  padding: 1px 6px;
}
@keyframes tapeIn { 0% { transform: scale(1.15); opacity: 0; } 100% { transform: scale(1); opacity: 1; } }
/* Die Schicht, in der der Amtsstempel liegt: Geschwister von #grid statt Kind, damit
   er einen Kameraschnitt überlebt. Über den Sektorkästen (z-index 2) und über den
   Bootssilhouetten, unter nichts. */
/* Sie liegt auf der Rasterzelle der KARTE, nicht auf dem ganzen .gridbox: mit
   `inset: 0` bezog sie sich auf den Kasten samt Zeilen- und Spaltenköpfen, saß damit
   nicht mehr in der Kartenmitte und ragte im Querformat über die Mayday-Tafel. Eine
   eindeutige Grid-Platzierung macht die Grid-Area von #grid zum Bezugsrahmen, und
   overflow:hidden beschneidet wieder, was vorher #grid beschnitten hat. */
.stamp-layer {
  position: absolute; inset: 0; grid-column: 2; grid-row: 2;
  overflow: hidden; pointer-events: none; z-index: 6;
}
/* Amtsstempel über der Karte */
.map-stamp {
  position: absolute; z-index: 5; pointer-events: none;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%) rotate(-8deg);
  font-family: var(--display); font-weight: 400;
  text-transform: uppercase; letter-spacing: .08em;
  font-size: clamp(1.3rem, 4.5vw, 2.2rem);
  color: var(--distress);
  border: 4px solid var(--distress);
  padding: .2em .5em;
  white-space: nowrap;
  opacity: 0;
  animation: stampIn 1.6s var(--ease-out-strong) forwards;
}
@keyframes stampIn {
  0% { opacity: 0; transform: translate(-50%, -50%) rotate(-8deg) scale(1.6); }
  12% { opacity: .95; transform: translate(-50%, -50%) rotate(-8deg) scale(1); }
  75% { opacity: .95; }
  100% { opacity: 0; transform: translate(-50%, -50%) rotate(-8deg) scale(1); }
}
/* Akten-Chip für unbestätigte Suche */
.pending-tag {
  position: absolute; z-index: 2; pointer-events: none;
  transform: translate(-50%, -110%);
  font-family: var(--mono); font-size: .55rem; font-weight: 700; letter-spacing: .08em;
  background: oklch(0.93 0.015 90); color: oklch(0.3 0.02 90);
  border: 1px solid var(--telex-rot);
  padding: 1px 5px;
  box-shadow: 2px 2px 0 color-mix(in oklch, var(--telex-rot) 50%, transparent);
}
/* In Reihe A ist über der Zelle kein Platz: der Marker landete dort über der
   Spaltenleiste und wurde beschnitten. Übrig blieb ein rot umrandeter Cremestreifen
   ohne ein einziges Zeichen — gezählt 3 Glyphenpixel gegen 121 in Reihe E, also 2,5 %
   der Schrift bei voller Trefferfläche. Zehn der hundert Zellen betroffen. Er hängt
   sich dort UNTER die Zelle; renderAll setzt ihn ohnehin neu.
   Er hängt ganz unter der Zelle und nicht halb hinein: die erste Fassung kippte ihn um
   10 % und begrub damit die eigene Glyphe und das Sektorplättchen, die zweite um 100 %
   und ließ immer noch 711 px² auf der eigenen Zelle liegen. Der Anker wandert deshalb
   in renderPendingTag() an die Unterkante, und hier bleibt nur die Zentrierung. */
.pending-tag.unten { transform: translate(-50%, 0); }

/* ═══════════ Screen 3 · Bilanz & Bruch ═══════════ */
#screenEnd.active { display: flex; align-items: center; justify-content: center; --end-pad: 24px; padding: var(--end-pad); }
.bilanz { max-width: 620px; width: 100%; text-align: center; }
.bilanz h2 {
  font-family: var(--display); font-weight: 400; text-transform: uppercase;
  font-size: clamp(2rem, 7vw, 3.4rem); letter-spacing: .02em;
  margin: 0 0 26px; line-height: 1;
}
.tally {
  display: flex; justify-content: center; gap: clamp(18px, 6vw, 52px);
  margin-bottom: 30px;
}
.tally div { min-width: 90px; }
.tally b {
  display: block; font-family: var(--display); font-weight: 400;
  font-size: clamp(2.4rem, 9vw, 4rem); line-height: 1;
}
.tally .saved b { color: var(--saved); }
.tally .drowned b { color: var(--distress); }
.tally .abducted b { color: var(--abducted); }
.tally .lbl {
  font-family: var(--mono); font-size: .7rem;
  text-transform: uppercase; letter-spacing: .12em; color: var(--ink-faint);
}
.bilanz .boats-recap {
  font-family: var(--mono); font-size: .8rem; color: var(--ink-soft);
  list-style: none; padding: 0; margin: 0 auto 34px;
  display: flex; flex-direction: column; gap: 4px; max-width: 380px;
  text-align: left;
}
/* Recap-Zeilen treten gestaffelt ein */
.bilanz .boats-recap li {
  animation: recapIn 240ms var(--ease-out-strong) both;
}
.bilanz .boats-recap li:nth-child(1) { animation-delay: .30s; }
.bilanz .boats-recap li:nth-child(2) { animation-delay: .36s; }
.bilanz .boats-recap li:nth-child(3) { animation-delay: .42s; }
.bilanz .boats-recap li:nth-child(4) { animation-delay: .48s; }
.bilanz .boats-recap li:nth-child(5) { animation-delay: .54s; }
@keyframes recapIn {
  from { opacity: 0; transform: translateY(6px); }
}
.bilanz-geld {
  font-family: var(--mono); font-size: .8rem; color: var(--ink-soft);
  max-width: 46ch; margin: -14px auto 32px;
}
.bilanz .boats-recap .ok { color: var(--saved); }
.bilanz .boats-recap .bad { color: var(--distress); }
.bilanz .boats-recap .abd { color: var(--abducted); }
/* Der Hauptknopf ist ein Markenfeld, und ein Markenfeld auf fast schwarzem Grund
   braucht seine Kante: die Fläche allein misst gegen die See 2,30:1 und verfehlt
   die 3:1 aus 1.4.11. Der weiße Haarstrich trägt sie mit 19,4:1 — dasselbe Paar
   wie bei Spendenaufruf und Sektor-Plättchen, dreimal dieselbe Regel. */
.btn-primary {
  border: 2px solid var(--brand-ink); background: var(--brand); color: var(--brand-ink);
  font-weight: 700; font-size: 1rem;
  padding: 12px 28px; cursor: pointer;
  transition: background .15s, transform .15s;
}
.btn-primary:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

/* Bruch */
#screenBruch.active {
  display: flex; align-items: center; justify-content: center;
  background: #000; color: #fff;
  padding: 24px;
}
.bruch { max-width: 560px; text-align: center; }
.bruch p {
  font-size: clamp(1.25rem, 4vw, 1.7rem);
  font-weight: 600; line-height: 1.45;
  margin: 0 0 1.1em;
  opacity: 0;
  animation: bruchIn .8s var(--ease-out-strong) forwards;
  text-wrap: balance;
}
/* nth-of-type, nicht nth-child: das erste Kind von .bruch ist die versteckte h1.
   Mit nth-child traf (1) deshalb nichts und (2) den ERSTEN Absatz — gemessen kam
   »Die Menschen im Mittelmeer nicht.« nach 114 ms und »Du kannst jederzeit neu
   starten.« erst nach 1716 ms. Die Pointe stand 1,6 s vor ihrem Aufbau. */
/* Die eigene Zahl steht über den IOM-Zahlen und in derselben Schrift wie sie: sie ist
   der Vergleich und nicht die Anklage. Sie zählt NICHT hoch — die IOM-Zahlen stehen
   still, das ist eine Entscheidung mit Begründung, und eine hochzählende Zahl daneben
   machte aus der Niederlage einen Punktestand. */
.bruch .deine-zahl {
  font-family: var(--mono); font-weight: 400;
  font-size: clamp(.95rem, 3vw, 1.1rem);
  color: oklch(0.8 0.02 245);
  animation-delay: 2.6s;
}
.bilanz-festung {
  font-family: var(--mono); font-size: .82rem;
  color: var(--distress-lit);
  margin: .6em 0 1.4em;
  text-wrap: pretty;
}
.bruch p:nth-of-type(1) { animation-delay: .4s; }
.bruch p:nth-of-type(2) { animation-delay: 1.6s; }
.bruch p.zahlen { animation-delay: 3s; font-family: var(--mono); font-weight: 400; font-size: clamp(.95rem, 3vw, 1.1rem); color: oklch(0.8 0.02 245); }
.bruch .quelle { animation-delay: 3.6s; font-size: .75rem; color: oklch(0.6 0.02 245); font-family: var(--mono); font-weight: 400; }
/* Die Zeile, die sagt, wohin das Geld geht. Sie gehört zum Knopf, nicht zu den
   großen Sätzen: kleiner als die Anklage, größer als die Quellenangabe, und sie
   kommt 0,3 s vor den Schaltflächen, damit der Knopf nicht in eine leere Behauptung
   hinein erscheint. Kein Markenrot — die IOM-Zahlen stehen direkt darüber, und ein
   farbiger Spendensatz neben echten Totenzahlen wäre genau der Ton, den PRODUCT.md
   ausschließt. */
.bruch p.wofuer {
  animation-delay: 3.9s;
  font-size: clamp(.88rem, 2.7vw, .98rem); font-weight: 600; line-height: 1.5;
  margin: 26px auto 0; max-width: 34ch; color: oklch(0.84 0.015 245);
  text-wrap: balance;
}
.bruch .ctas {
  display: flex; flex-direction: column; gap: 12px; align-items: center;
  margin-top: 34px;
  opacity: 0; animation: bruchIn .8s .5s var(--ease-out-strong) forwards;
}
.bruch .ctas.ready { animation-delay: 4.2s; }
/* Überspringen: das erste pointerdown/keydown auf dem Screen setzt game.js diese
   Klasse und nimmt gleichzeitig inert weg. MUSS nach .ctas.ready stehen — gleiche
   Spezifität, und `animation: none` setzt die Verzögerung nur dann zurück, wenn es
   später in der Quelle steht. opacity muss mit: ohne Animation fiele die Deckkraft
   auf den deklarierten Wert 0 zurück. Gleiche Bauweise wie der Ruhig-Pfad unten. */
.bruch.sofort p, .bruch.sofort .ctas { opacity: 1; animation: none; }
@keyframes bruchIn { to { opacity: 1; } }
/* Der wichtigste Knopf des Spiels, auf reinem Schwarz. Gleiche Bauweise wie der
   Hauptknopf: Markenfeld, weiße Kante, weißes Wort — 2,49:1 die Fläche allein,
   21:1 mit der Kante. */
.cta-spende {
  display: block; width: 100%; max-width: 380px;
  border: 2px solid var(--brand-ink);
  background: var(--brand); color: var(--brand-ink);
  text-decoration: none; font-weight: 700; font-size: 1.15rem;
  padding: 14px 24px; text-align: center;
  transition: background .15s, transform .15s;
}
.cta-row { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; }
.cta-sub {
  color: #fff; background: none;
  border: 1px solid oklch(0.4 0.01 245);
  text-decoration: none; font-size: .88rem; font-weight: 600;
  padding: 10px 18px; cursor: pointer;
  transition: border-color .15s, transform 120ms var(--ease-out-strong);
}
a.cta-sub { display: inline-block; }
/* Die beiden Wege, auf denen ein Spieler in der Kampagne bleibt, waren 16 und 19 px
   hoch — formal in Ordnung, weil die Abstandsausnahme von 2.5.8 greift, auf einem
   Telefon aber eine Trefferquote und kein Ziel. Die Polsterung hebt beide über 24 px,
   ohne Schriftgrad oder Rangfolge anzufassen: der laute Gegenstand des Bruchs bleibt
   der Spendenknopf mit 342x60. Der Bruch passt bei 390x844 exakt ohne Scrollen, der
   Zugewinn ist deshalb aus dem margin-top gegenfinanziert. */
.bruch .again {
  margin-top: 18px; background: none; border: none;
  color: oklch(0.64 0.02 245); font-size: .8rem; cursor: pointer;
  text-decoration: underline; text-underline-offset: 3px;
  padding: .45em .6em;
}

/* ── Hover nur für echte Zeiger, Press-Feedback für alle ── */
@media (hover: hover) {
  /* Der Knopf ist jetzt die helle Platte — »noch mehr Abstand zum Feld« gibt es
     hinter Weiß nicht mehr. Er geht deshalb in die Gegenrichtung: die Platte tönt
     sich auf den Markenton, das Wort geht eine Stufe tiefer. Beides zusammen, weil
     das Wort auf der getönten Platte sonst 4,34:1 misst und die 4,5 verfehlt —
     der Knopf trägt bei clamp(1.35rem, …) nicht sicher Großschrift. Mit der
     tieferen Stufe sind es 7,21:1. */
  .plakat-cta:hover {
    transform: translateY(-2px) rotate(-.5deg);
    background: var(--brand-lit); color: var(--brand-deep);
  }
  .btn-primary:hover { background: var(--brand-deep); transform: translateY(-1px); }
  .cta-spende:hover { background: var(--brand-deep); transform: translateY(-1px); }
  .iconbtn:hover { border-color: var(--brand-lit); color: var(--ink); }
  #telexOk:hover { background: oklch(0.22 0.02 90); color: oklch(0.93 0.015 90); }
  .cta-sub:hover { border-color: #fff; }
  .bruch .again:hover { color: #fff; }
}
.plakat-cta:active, .btn-primary:active, .cta-spende:active,
.cta-sub:active, #telexOk:active, .iconbtn:active, .fleetrow:active {
  transform: scale(0.97);
}
.grid:not(.inert) .cell.free:active { transform: scale(0.95); }

/* ═══════════ 3D-Ebene ═══════════
   Alles hier hängt an html.sea3d. Die Klasse setzt der Bootstrap in index.html erst,
   wenn die Szene wirklich läuft — nimmt man sie weg, steht wieder die geprüfte
   2D-Fassung da, ohne dass sonst irgendwo etwas zurückgebaut werden müsste. */
#sea3dLayer { display: none; }
/* Sichtbar nur auf dem Einsatz-Screen: Plakat, Bilanz und Bruch bleiben die geprüften
   Flächen, ihr Text soll nicht über wechselnd heller Gischt stehen. Ausgeblendet wird
   über visibility und nicht über display — die Leinwand behält so ihre echte Größe,
   statt der 3D-Ebene beim Hochfahren hinter dem Plakat 0×0 zu melden. */
html.sea3d #sea3dLayer {
  display: block;
  position: fixed; inset: 0;
  z-index: 0;
  visibility: hidden;
  pointer-events: none;   /* jeder Klick gehört den Zellen darüber */
}
html.sea3d:has(#screenGame.active) #sea3dLayer { visibility: visible; }
html.sea3d #sea3d { display: block; width: 100%; height: 100%; }
/* Vorbereitung hinter dem Plakat: die Leinwand braucht ihre echte Größe, sonst
   wärmt sie ein 0×0-Bild und misst eine Bildrate, die es nicht gibt. Zu sehen
   ist trotzdem nichts. html.sea3d kommt erst, wenn die Ebene warm ist. */
html.sea3d-warm #sea3dLayer {
  display: block;
  position: fixed; inset: 0;
  z-index: 0;
  visibility: hidden;
  pointer-events: none;
}
html.sea3d-warm #sea3d { display: block; width: 100%; height: 100%; }
html.sea3d #main { position: relative; z-index: 1; }

/* Die Seekarte wird zum Fenster: Papier, Rasterlinien, Radar-Sweep und Scanlines
   sind jetzt die See selbst.

   Die naheliegende Fassung — eine Rauchglasscheibe über der ganzen Karte — trug
   zwar jeden Kontrast, machte die Karte aber zu einer 2D-Platte auf einer 3D-See:
   im Zellinneren bewegte sich das Bild 0,48/255 pro Sekunde, auf offenem Wasser
   6,56, also 13,7-mal so viel. Die See darunter war schlicht nicht zu sehen.

   Die Scheibe ist deshalb weg, und der Kontrast hängt nicht mehr an ihr, weil er
   an einer bewegten See auch gar nicht hängen kann: Sonnenglitzer läuft bis fast
   ins Weiß, also ist keine halbdurchsichtige Fläche in jedem Bild dunkel genug.
   Zwei Register statt einer Scheibe:

     • Eine UNBEARBEITETE Zelle ist offenes Wasser. Sie hat nur ihre Kartenlinie,
       und die ist ein Paar — helle Linie, dunkle Unterlegung. Über hellem Wasser
       trägt die dunkle (12,2–15,2:1), über dunklem die helle (4,5:1), und
       gegeneinander messen die beiden immer 6,0–11,4:1. Ein einzelner Strich kann
       das nicht: er müsste zugleich heller und dunkler als das Wasser sein.
     • Eine BEARBEITETE Zelle friert ein: sie bekommt ihren eigenen Schleier, dicht
       genug, dass ihre Glyphe gegen reinweißes Wasser 4,5:1 hält (gerechnet gegen
       Weiß, nicht gegen einen Mittelwert). Die Karte füllt sich damit sichtbar,
       während die Schicht abgearbeitet wird — was ein echter Plot auch tut.

   Alle Verhältnisse unten sind gegen den ungünstigsten Fall gerechnet: 7 % reines
   Weiß durch den Schleier, also Sonnenglitzer im Anschlag direkt unter der Zelle. */
html.sea3d {
  --sea3d-rule:   oklch(0.84 0.045 245 / .62);   /* die Kartenlinie */
  --sea3d-casing: oklch(0.09 0.02 245 / .86);    /* ihre dunkle Unterlegung */
  --sea3d-ice:    .93;                           /* Deckung eines bearbeiteten Feldes */
  --sea3d-frost:  oklch(0.150 0.030 245 / var(--sea3d-ice));  /* der Schleier selbst */
  /* Der Zeiger ist ein Paar, und ein Paar ist genau so gut wie sein Abstand:
     zwischen zwei Tönen liegt immer ein Wasserstand, der beiden gleich nahe
     kommt, und dort misst es √((L₁+.05)/(L₂+.05)). Deckend und so weit
     auseinander wie der Farbraum es hergibt — deshalb ohne Alpha. */
  --sea3d-cursor: oklch(0.97 0.02 245);          /* der Zeiger — hell, nicht dunkel */
  --sea3d-seam:   oklch(0.08 0.02 245);          /* seine Fassung im Glitzerpfad */
}
/* Der Kartenrand bleibt dunkel: die helle Umrandung zeichnet die 3D-Ebene als
   Vermessungsrahmen mit Ecken und Randteilung aufs Wasser, und zwei helle
   Rechtecke übereinander sind ein Käfig und keine Karte. */
html.sea3d .grid {
  background: none;
  border-color: var(--sea3d-casing);
}
html.sea3d .grid::before, html.sea3d .grid::after { display: none; }

/* Die freie Zelle: nur das Linienpaar, sonst Wasser. Dunkel außen, hell innen —
   herum gedreht sah jede Zelle wie eine erhabene Kachel aus, so liest sie sich als
   Riss auf Glas, und das Wasser bleibt das Auffällige. Die helle Haarlinie hat
   damit immer den dunklen Rand als Nachbarin, egal was das Wasser tut.
   Nur die freie Zelle braucht das Paar: bei einer bearbeiteten ist ihr eigener
   Schleier schon die dunkle Nachbarin. */
html.sea3d .cell {
  background: none;
  border-color: var(--sea3d-casing);
}
html.sea3d .cell.free { box-shadow: inset 0 0 0 1px var(--sea3d-rule); }
/* Der Zeiger sagt »die hier als Nächstes«. Er muss dafür in die andere Richtung
   gehen als alles Bearbeitete, denn bearbeitet heißt auf dieser Karte zugefroren
   und dunkel: die alte Regel dunkelte eine unberührte Zelle auf Helligkeit 30,4
   ab, wo eine abgesuchte Wasserzelle bei 25,0 liegt und eine unberührte bei
   83–89 — der Zeiger sagte also »schon abgesucht«.
   Jetzt deckt er nichts zu. Die Zelle bleibt offenes Wasser — gemessen 90,2
   unter dem Zeiger gegen 81,6 daneben und 32,1 abgesucht, der Zeiger ist also
   HELLER als unberührt und liegt nicht mehr zwischen unberührt und abgesucht.
   Seine Kartenlinie wird hell und doppelt so breit, und in der Mitte steht ein
   Fadenkreuz.
   Beides ist ein PAAR, hell auf dunkler Fassung — dasselbe Mittel wie bei der
   Kartenlinie, und aus demselben Grund: über reinweißem Wasser misst der helle
   Strich allein 1,08:1 und ist schlicht weg. Ein weicher Schatten reicht dafür
   nicht; ein doppeltes drop-shadow kam über weißem Wasser nur auf 3,84:1, weil
   ein 1-px-Weichzeichner nie volle Deckung erreicht. Die Fassung ist deshalb
   gezeichnet: eine Lage breiter als der helle Strich, voll deckend.
   Gemessen im Bild: über reinem Weiß trägt die Fassung 17,91:1, über reinem
   Schwarz der helle Strich 16,68:1, auf lebender See 4,08:1 (Tag 0,80) bis
   6,99:1 (Tag 0,05). Die beiden gegeneinander 16,51:1 — und weil sie
   übereinanderliegen, gilt für JEDEN Wasserstand mindestens
   √((0,8135+,05)/(0,0023+,05)) = 4,06:1. Der schlechteste gemessene Wert war
   4,08, die Rechnung stimmt also mit dem Bild überein.
   Vom Fokusring bleibt der Zeiger dreifach unterscheidbar: der ist die Marke, liegt
   als Rechteck am Rand und friert seine Zelle ein.
   In der Hover-Abfrage, wie die 2D-Regel darüber: auf einem Telefon gibt es
   keinen Zeiger, aber einen klebenden :hover nach jedem Tippen. */
@media (hover: hover) {
  html.sea3d .grid:not(.inert) .cell.free:hover {
    background: none;
    /* die Fassung bleibt dunkel: die helle Regel braucht ihre dunkle Nachbarin,
       und das Markenrot gehört dem Fokus, damit die beiden nicht dasselbe sagen.
       Der zweite Schatten liegt unter dem ersten und schaut als ein Pixel
       Fassung innen heraus — dunkel · hell · dunkel quer durch die Kante. */
    border-color: var(--sea3d-casing);
    box-shadow: inset 0 0 0 2px var(--sea3d-cursor), inset 0 0 0 3px var(--sea3d-seam);
  }
  /* Reihenfolge ist hier die ganze Konstruktion: die erste Lage liegt oben, also
     stehen die hellen Striche über ihrer Fassung und bleiben durchgehend, wo sie
     einander kreuzen. */
  html.sea3d .grid:not(.inert) .cell.free:hover::after {
    content: "";
    position: absolute; inset: 0;
    background:
      linear-gradient(var(--sea3d-cursor), var(--sea3d-cursor)) 50% 50% / 44% 2px no-repeat,
      linear-gradient(var(--sea3d-cursor), var(--sea3d-cursor)) 50% 50% / 2px 44% no-repeat,
      linear-gradient(var(--sea3d-seam), var(--sea3d-seam)) 50% 50% / 48% 4px no-repeat,
      linear-gradient(var(--sea3d-seam), var(--sea3d-seam)) 50% 50% / 4px 48% no-repeat;
  }
  /* Liegt der Zeiger auf der fokussierten Zelle, gewinnt der Fokus und der Zeiger
     tritt zurück. Zwei Gründe, beide gemessen: die Hover-Regel (0,6,1) schlägt sonst
     die Fokusregel (0,3,1) und nimmt der Zelle ihre eingefrorene Platte — der Ring
     fiel damit von 3,87:1 auf 1,35:1 über weißem Wasser. Und der helle Innenstrich
     läge bei outline-offset:-2px genau unter dem Ring und wäre dessen innerer
     Nachbar, statt der dunklen Platte. Inhaltlich sagen beide dasselbe (»diese
     hier als Nächstes«), der Ring sagt es deutlicher. */
  html.sea3d .grid:not(.inert) .cell.free:hover:focus-visible {
    background: oklch(0.20 0.04 245 / .90);
    box-shadow: none;
  }
  html.sea3d .grid:not(.inert) .cell.free:hover:focus-visible::after { content: none; }
}
/* Der Fokus friert seine Zelle ein: der Ring liegt sonst über Glitzer, wo er
   1,4:1 misst — so misst er auf der Platte 7,53:1 über reinweißem Wasser, gegen
   5,12:1 mit dem alten Orange. Er steigt, weil --brand-lit heller ist
   (Leuchtdichte 0,526 gegen 0,341). ring.mjs misst ihn im Bild nach. */
html.sea3d .cell:focus-visible { background: oklch(0.20 0.04 245 / .90); }

/* Die bearbeiteten Zustände. Jede Zeile ist gemessen: Farbe der Zelle über weißem
   Wasser, dann das Verhältnis der Glyphe darauf. Die Glyphe bleibt der Träger —
   nichts hier wird allein über Farbe gesagt (1.4.1). */
html.sea3d .cell.water    { background: var(--sea3d-frost); }                          /* Punkt 4,69:1 */
html.sea3d .cell.contact  { background: oklch(0.205 0.075  14 / var(--sea3d-ice)); }   /* ◉ 15,71:1 */
html.sea3d .cell.rescued  { background: oklch(0.205 0.060 150 / var(--sea3d-ice)); }   /* ✓ 7,80:1 */
html.sea3d .cell.sunkcell { background: oklch(0.185 0.070  25 / var(--sea3d-ice)); }   /* ✕ 5,67:1 */
html.sea3d .cell.abducted-cell { background: oklch(0.195 0.055 300 / var(--sea3d-ice)); } /* ▲ 6,07:1 */
html.sea3d .cell.pending  { background: oklch(0.170 0.030 245 / var(--sea3d-ice)); }   /* ? 7,60:1 */
/* Zwei Glyphen kommen auf ihrem 2D-Ton nicht über 4,5:1 — Rot und Blassgrau haben
   dafür schlicht zu wenig Leuchtdichte. Angehoben wird die Glyphe, nicht der
   Schleier: ein dunklerer Schleier würde die See noch weiter zudecken. */
html.sea3d .cell.sunkcell::after { color: var(--distress-lit); }
html.sea3d .cell.pending::after  { color: var(--ink-soft); }
/* Die farbigen Ränder. Sie stehen hier noch einmal, weil `html.sea3d .cell` eine
   Klasse spezifischer ist als `.cell.contact` und sie sonst stillschweigend
   überschreiben würde — gemessen gegen den eigenen Schleier über weißem Wasser,
   1.4.11 will 3:1. */
html.sea3d .cell.contact       { border-color: var(--brand-lit); }      /* 8,62:1 */
html.sea3d .cell.rescued       { border-color: var(--saved); }          /* 7,80:1 */
html.sea3d .cell.abducted-cell { border-color: var(--abducted); }       /* 6,07:1 */
html.sea3d .cell.pending       { border-color: var(--ink-faint); }      /* 4,14:1 */
html.sea3d .cell.sunkcell      { border-color: oklch(0.62 0.20 25); }   /* 4,08:1 */
/* Gesperrt trägt sich über das Streifenmuster, nicht über den Farbton; auf dem
   Schleier misst der Streifen gegen seinen eigenen Grund 3,24:1, der Rand 3,49:1. */
html.sea3d .cell.locked {
  background:
    repeating-linear-gradient(45deg,
      color-mix(in oklch, var(--distress) 80%, transparent) 0 5px, transparent 5px 10px),
    linear-gradient(oklch(0.190 0.070 25 / var(--sea3d-ice)), oklch(0.190 0.070 25 / var(--sea3d-ice)));
  border-color: color-mix(in oklch, var(--distress) 85%, transparent);
}

/* Sektorkästen liegen über freien Zellen, also über offener See: der helle Strich
   bekommt einen dunklen Saum, damit er auch im Glitzerpfad eine Kante hat.
   Ein drop-shadow und kein zweiter Rahmen — die Kante ist gestrichelt.
   Mit --brand-lit ist der Strich heller als das alte Orange (Leuchtdichte 0,526
   gegen 0,341), und das verschiebt das Paar, ohne es zu schwächen. Gemessen, der
   Strich allein: über schwarzem Wasser 5,19 → 7,39:1, über reinweißem 2,23 → 1,62:1.
   Über Weiß trug ihn ohnehin nie er selbst — dort trägt der Saum, mit 19,19:1, und
   der ist unverändert. Genau dafür ist ein Paar da: eine Hälfte greift immer. */
html.sea3d .sector-box { filter: drop-shadow(0 0 1.5px oklch(0.08 0.02 245 / .95)); }
/* Beide Plättchen werden über Wasser volldeckend: der Saum des ruhigen ist in 2D
   eine 85-%-Mischung, und was halbdurchsichtig ist, hat über Gischt keine
   verlässliche Farbe. Der Grund selbst ist schon deckend. */
html.sea3d .sector-box .tag { box-shadow: 0 0 0 1px var(--brand-lit); }
html.sea3d .sector-box.urgent .tag { background: var(--distress-lit); box-shadow: none; }
/* Zwei Beschriftungen liegen frei über der Karte und damit über Wasser. Beide
   bekommen ihr eigenes Papier, sonst messen sie über Glitzer 1,3:1 (Stempel, rot)
   und 1,5:1 (Rettungs-Label, grün); darauf 4,86:1 und 12,2:1. Beim Stempel ist
   4,5:1 die Latte und nicht 3:1, weil clamp(1.3rem, 4.5vw, …) auf einem schmalen
   Fenster auch nur 20,8 px groß wird. */
html.sea3d .map-stamp { background: oklch(0.12 0.03 245 / .94); }
html.sea3d .float-tag {
  background: oklch(0.12 0.03 245 / .94);
  padding: 1px 6px; border-radius: 4px;
}
/* Zwei Bewegungen auf transform, die der 3D-Ebene die Karte unter den Füßen
   wegziehen würden: eine CSS-Animation schlägt den inline gesetzten matrix3d-Wert
   in der Kaskade, und ein Eintritts-Versatz am Vorfahren verschiebt die gemessene
   Lage der Karte gegen das Wasser. In 3D bebt die Kamera und der Screen kommt ohne
   Anlauf — beides Bewegung weniger, nie mehr. */
html.sea3d .grid.shake { animation: none; }
html.sea3d #screenGame.active > .wrap { animation: none; }
/* Alles, was Text trägt, bekommt wieder festen Grund: über bewegter See ist die
   Helligkeit hinter einer Schrift nicht vorhersagbar, und 4,5:1 ist keine Zahl, die
   man dem Seegang überlässt. Beschriftung und Bildunterschrift stehen in 2D auf
   --sea, Leiste und Panels auf ihrer Mischung darüber — beides hier deckend
   nachgestellt, damit jeder gemessene Kontrast unverändert bleibt. */
html.sea3d .chart-caption { background: var(--sea); padding: 2px 6px; border-radius: 5px; }
/* Die Statusleiste bekommt denselben festen Grund. Sie trägt bei leerer Kasse den
   einzigen Satz, der weitersagt, was zu tun ist — über bewegter See ist deren
   Kontrast sonst nicht vorhersagbar. */
html.sea3d .chart-status { background: var(--sea); padding: 2px 6px; border-radius: 5px; height: calc(2.8em + 4px); }
/* Die Zeilen- und Spaltenköpfe waren der eine Teil der Karte, der die Behandlung
   nicht bekommen hat: zwanzig deckende Kästchen, jedes so groß wie seine ganze
   Spur, aneinandergereiht ein schwarzes L neben einem durchsichtigen Plot.
   Sie bekommen jetzt dieselbe Antwort wie die Zellen — Schleier nur da, wo eine
   Marke zu tragen ist. Der Streifen selbst ist offenes Wasser; gefroren ist nur
   das Plättchen unter der Ziffer, und das ist ein Fünftel der alten Fläche. Es
   trägt denselben Schleier wie eine abgesuchte Zelle, also hängt sein Kontrast
   an derselben gemessenen Zahl und nicht an einer zweiten.
   Der Ton der Schrift geht dafür hoch. Über reinweißem Wasser liegt der Schleier
   bei rgb(20,29,39): --ink-faint misst darauf 4,34:1 und verfehlt die 4,5,
   --ink-soft 7,96, --ink 14,29. Im Bild gemessen, gegen eine eigens auf reines
   Weiß gesetzte See: 12,31:1 als schlechtester von zwanzig Köpfen, auf lebender
   See 13,4–14,1. Über schwarzem Wasser verschwindet das Plättchen (1,06:1) und
   die Ziffer trägt sich direkt gegen die See — 14,92:1; es hält also an beiden
   Enden, nicht nur an einem. Und fett, weil die Kartenebene perspektivisch
   umgetastet wird und eine 9,6-px-Haarlinie das nicht übersteht. */
html.sea3d .collabels span, html.sea3d .rowlabels span {
  /* Nur schrumpfen, nichts verschieben: place-self nimmt dem Kästchen die Spur,
     die es füllte, und lässt Breite und Höhe der Streifen genau so, wie sie
     geprüft wurden. Eine Zeile weniger Zurückhaltung — width:auto statt der
     1,3 em — machte den Zeilenstreifen 0,9 px schmaler, schob damit die ganze
     Karte und mit ihr die Tafeln rechts um 0,45 px, und die Rückfallmessung sah
     danach jede Textzeile im Panel anders gerastert (1,33 % statt 0,05 %). */
  place-self: center;
  padding: 0 2px;
  border-radius: 3px;
  background: var(--sea3d-frost);
  color: var(--ink);
  font-weight: 700;
}
html.sea3d .hud, html.sea3d .panel {
  background: color-mix(in srgb, oklch(0.20 0.035 245) 72%, var(--sea));
}

/* ── Die drei Karten: »Gerettet!« · »Gesunken!« · »Verschleppt!« ──
   bridge.js baut das Element und sagt nur, welcher der drei Momente läuft; wie es
   aussieht und wo es steht, entscheidet diese Datei. Es ist reine Dekoration —
   aria-hidden, keine Rolle, keine zweite Live-Region (der Text steht längst über
   radio() im #announcer), keine Zeigerereignisse, kein Fokus.

   Lage: die Spalte der Karte, im Band UNTER dem Plot — auf dem Einsatz-Screen frei
   von allem, im Schnitt offenes Wasser, also EINE Lage für beide Fälle. bridge.js
   misst die Kartenspur, prüft per elementFromPoint, ob das Band wirklich leer ist,
   und schreibt --banner-x/-y/-w. Wo es das nicht ist — gestapeltes schmales Layout
   mit den Tafeln direkt unter der Karte, oder ein Fenster unter ~520 px Höhe —
   setzt es die Karte mit ihrem Fuß auf den Fuß des Plots.
   Unter --z-card, damit sie das Fernschreiben unter keinen Umständen verdeckt.

   Kontrast: sie steht im Schnitt über bewegtem Wasser, das bis Luminanz 1,000
   läuft. Deshalb dieselbe Antwort wie bei den Mayday-Plättchen — ein fast
   deckendes, sehr dunkles Papier mit heller Schrift, das gegen jeden Wasserstand
   gleich misst. Im Bild gemessen (Differenzmasken auf eingefrorenem Bild, See per
   brightness() auf reines Weiß und reines Schwarz gezwungen, dazu drei
   Tageszeiten): das Papier liegt über reinweißem Wasser bei Luminanz 0,0062, die
   Schrift darauf misst **16,13:1** — über reinem Schwarz 17,28:1, auf lebender See
   16,86–17,07:1. Die 4 % Restdurchlass kosten so wenig, weil der Browser in
   sRGB mischt: 4 % von Weiß sind 10 von 255, also linear 0,006.
   Die Kante ist wieder ein PAAR, aus demselben Grund wie bei der Kartenlinie:
   über weißem Wasser trägt das dunkle Papier (18,68:1), über schwarzem der helle
   Saum (5,16:1) — keine einzelne Kante kann beides.
   Die Tonspur ist Dekoration und nie der Träger (grün 9,9–10,6 · rot 5,0–5,3 ·
   violett 7,3–7,8 gegen das eigene Papier). Das Wort steht da.

   Bewegung: Sichtbarkeit hängt an der Klasse .on, nie am Ende einer Animation
   (Punkt 6 der Abnahme). Unter html.ruhig fallen die Übergänge auf 0,01 ms —
   dann ist die Karte einfach da, statt hereinzukommen. */
/* Der Ruhezustand hängt an KEINER Klasse, und das ist gemessen nötig: bridge.js
   hängt die Karte in den Body, bevor html.sea3d gesetzt ist, und in diesem
   Fenster war sie ein unformatierter, sichtbarer Block. Kam die Klasse dann,
   blendete der Übergang sie von deckend auf 0 — eine dunkle Karte quer über dem
   Plakat, 200 ms lang, auf jedem Seitenaufruf (auf dem Stand davor
   ebenso, das ist kein neuer Fehler). Punkt 6 der Abnahme sagt es allgemein:
   Sichtbarkeit darf nie das Ende eines Übergangs sein. */
.sea3d-banner { position: fixed; opacity: 0; visibility: hidden; }
html.sea3d .sea3d-banner {
  position: fixed;
  z-index: 30;
  left: var(--banner-x, 50%);
  top: var(--banner-y, 58%);
  translate: -50% 10px;
  width: min(92vw, var(--banner-w, 460px));
  padding: .3em .5em;
  border-radius: 6px;
  border-left: 8px solid var(--saved);
  background: oklch(0.135 0.028 245 / .96);
  box-shadow: 0 0 0 1px oklch(0.84 0.045 245 / .62), 0 10px 26px oklch(0 0 0 / .45);
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  transition: opacity 200ms ease, translate 260ms var(--ease-out-strong),
              visibility 0s linear 260ms;
}
html.sea3d .sea3d-banner b {
  display: block;
  font-family: var(--display); font-weight: 400;
  font-size: clamp(1.35rem, 4.6vw, 3.5rem);
  line-height: 1.04;
  letter-spacing: .01em;
  text-align: center;
  color: var(--ink);
}
html.sea3d .sea3d-banner.on {
  opacity: 1;
  visibility: visible;
  translate: -50% 0;
  transition: opacity 200ms ease, translate 260ms var(--ease-out-strong);
}
html.sea3d .sea3d-banner[data-moment="sunk"] { border-left-color: var(--distress); }
html.sea3d .sea3d-banner[data-moment="abducted"] { border-left-color: var(--abducted); }

/* ── Der Marker ──
   »Die Boote sind fiktiv, Menschen sterben aber wirklich.« Der Satz stand als eine von
   fünf gleich aussehenden Zeilen im Fuß und ging darin unter; der Projekteigner nannte
   das einen öden Textklumpen, und das war die richtige Beschreibung.

   Warum eine umgekehrte Platte und kein farbiger Textmarker: das Plakat trägt Markenrot,
   ein roter Marker darauf wäre unsichtbar, und eine zweite Auszeichnungsfarbe gibt es in
   diesem System nicht. Die Platte ist dagegen das Paar, das hier ohnehin schon arbeitet —
   dasselbe wie beim Stempel »RETTEN«, und sein Kontrast ist gemessen: 8,44:1 in beide
   Richtungen. Ein Gerät, das die Auszeichnung nicht malt, zeigt die Zeile normal weiter;
   die Aussage hängt am Text und nicht an der Farbe.

   `box-decoration-break: clone` ist der ganze Trick am Umbruch: ohne das bekommt eine
   über zwei Zeilen laufende Auszeichnung ihr Polster nur außen, und die zweite Zeile
   klebt links an der Kante. */
.wirklich {
  background: var(--brand-ink);
  color: var(--brand);
  font-weight: 700;
  padding: .12em .38em;
  box-decoration-break: clone;
  -webkit-box-decoration-break: clone;
}
/* Der Fuß dimmt seine Spannen auf 85 % Deckkraft, und `.plakat-foot span` (0,2,0) schlägt
   `.wirklich` (0,1,0) — ein `opacity: 1` an der Klasse allein tut deshalb NICHTS. Gemessen
   kam die Platte als blasses Rosa heraus statt als Weiß, und die berechnete Farbe
   verschweigt das: `background-color` meldet weiter `oklch(1 0 0)`, weil die Deckkraft
   erst beim Malen wirkt. Wer eine Auszeichnung prüft, muss die PIXEL messen.
   Dieselbe Falle steht keine dreißig Zeilen weiter unten schon aufgeschrieben, für den
   Spendenlink im selben Fuß. */
.plakat-foot .wirklich { opacity: 1; }
/* Auf dunklem Grund — Kampagnenfuß, Rechtsseiten — dieselbe Platte. Sie steht dort
   ohnehin auf Papierweiß gegen Tinte und braucht keine eigene Fassung; nur der Abstand
   zur Zeile davor gehört dazu, sonst berührt die Platte das Wort davor. */
.kfooter .wirklich { margin-left: .25em; }

/* ── Der Schnitt ──
   bridge.js setzt html.sea3d-cut, solange die Kamera unten am Rumpf steht. Die
   Karte ist dann ohnehin ausgeblendet; was übrig blieb, waren die Mayday- und
   Funkverkehr-Tafeln über dem rechten Drittel des Bildes und die Bildunterschrift
   quer über dem Himmel — beide vor genau dem Moment, für den die Einstellung
   gebaut ist. Sie gehen weg und kommen wieder.

   Ausgeblendet wird mit visibility, verzögert bis nach dem Weg: eine Tafel, die
   man nicht sieht, aber noch antabben kann, ist ein Fokus ins Nichts (2.4.7).
   Nichts wird umgehängt und nichts bekommt den Fokus weggenommen — und liegt der
   Fokus schon in einer Tafel, bleibt sie stehen, dafür sorgt :focus-within.

   Die Kopfleiste ging bis zum 5.8.2026 als einzige NICHT mit, weil dort Ton,
   Bewegung und Neustart sitzen. Das war die zweite Hälfte von Audit-Befund 26 und
   ist jetzt gefallen: gemessen nimmt sie hochkant 177 von 844 Pixeln, also 21 % der
   Bildhöhe, und steht damit genau über dem Himmel, für den die Einstellung gebaut
   ist. Der Preis ist vertretbar, weil ein Schnitt auf JEDEN Tastendruck und JEDEN
   Zeiger endet (skipCut): die drei Knöpfe sind einen Druck entfernt, nicht weg. Wer
   Bewegung dauerhaft abstellt, sieht ohnehin keinen Schnitt mehr — cutTo gibt bei
   reduzierter Bewegung false zurück. Liegt der Fokus in der Leiste, bleibt sie
   stehen, gleiche Regel wie bei den anderen Tafeln.
   #announcer ist davon nicht betroffen: die Live-Region steht vor <main> und
   außerhalb der Leiste. */
html.sea3d .side, html.sea3d .chart-caption, html.sea3d .chart-status, html.sea3d .hud {
  transition: opacity 240ms ease, translate 240ms var(--ease-out-strong);
}
html.sea3d.sea3d-cut .side,
html.sea3d.sea3d-cut .chart-caption,
html.sea3d.sea3d-cut .chart-status,
html.sea3d.sea3d-cut .hud {
  opacity: 0;
  visibility: hidden;
  transition: opacity 240ms ease, translate 240ms var(--ease-out-strong),
              visibility 0s linear 240ms;
}
html.sea3d.sea3d-cut .side { translate: 34px 0; }
html.sea3d.sea3d-cut .chart-caption { translate: 0 -16px; }
/* Die Leiste geht mit der Bildunterschrift weg und kommt mit ihr wieder. Sie darf
   nicht stehen bleiben: im Schnitt beschreibt sie eine Karte, die nicht da ist.
   Die Höhe bleibt reserviert, es wird nur unsichtbar — sonst springt die Karte
   beim Ein- und Ausblenden. */
html.sea3d.sea3d-cut .chart-status { translate: 0 -16px; }
/* Sie fährt nach oben aus dem Bild, nicht zur Seite: dorthin ist der Weg am
   kürzesten, und sie liegt ohnehin an der Oberkante. */
html.sea3d.sea3d-cut .hud { translate: 0 -20px; }
html.sea3d.sea3d-cut .side:focus-within,
html.sea3d.sea3d-cut .hud:focus-within {
  opacity: 1; visibility: visible; translate: 0 0; transition-delay: 0s;
}

/* ═══════════ Responsive & Motion ═══════════ */

/* ── Das Plakat auf dem Telefon ──
   Drei Dinge auf einmal, weil es dieselbe Ursache hat: zu viel Möbel um den
   Knopf herum.

   1. Rangfolge. Auf dem Schreibtisch ist der Knopf 363,8 x 84,4 px und die
      Schilder sind 51,7 x 31 px — da konkurriert nichts. Bei 390 px kippt das
      Verhältnis in der Breite: die Schilderreihe spannte 345 px gegen einen
      239,5 px breiten Knopf, das breiteste Ding im unteren Plakat war die
      Einstellung. Also bekommt der Knopf hier die ganze Spalte, und die
      Schilder werden kleiner. Ein Wahlplakat hat einen großen Knopf.
   2. Höhe. Bei 320x740 lief das Plakat über (Rollhöhe 772 gegen 740, ohne
      Anton 812) und schnitt die letzte Fußzeile ab — »Die Boote sind fiktiv.
      Das Sterben nicht.«, also genau den Satz, um den es geht.
   3. Sprung beim Schriftwechsel. Die Schilderreihe lag bei 390 px auf der
      Kippe: mit Anton eine Zeile, ohne Anton zwei. Kommt Anton nach, packt
      die Reihe um und ein Schild springt quer über die Spalte. Das war der
      größte Einzelposten der Verschiebung. Kleiner gesetzt passt die Reihe in
      jeder Schrift in eine Zeile, mit 29 px Luft bei 320 px — der Umbruch
      kann gar nicht mehr kippen. */
@media (max-width: 430px) {
  /* Die Luft des Plakats hängt senkrecht an der Fensterhöhe und nicht an der
     Breite: auf einem 640 px hohen Telefon ist weniger Platz zu verteilen als
     auf einem 932 px hohen, und der Fuß muss in beiden Fällen über die Kante
     passen. Seitlich bleibt es an der Breite. */
  #screenIntro.active {
    --pad-y: clamp(11px, 2vh, 18px);
    --pad-x: clamp(16px, 4vw, 20px);
    gap: clamp(11px, 2vh, 18px);
  }
  /* Auf dem Telefon flacher — der Balken folgt dem Logo, also wird das Logo bemessen
     und nicht der Balken. Die Zahlen sind nicht gewählt, sondern gemessen: bei
     375×667 hatte der Fuß vorher 0,6 px Luft, und der erste Entwurf (Logo 126 px,
     8 px Polsterung) schob ihn 6,7 px UNTER die Falz. Das ist genau die Falle aus
     Sitzung S3. Der Balken ist an dieser Stelle also kein Gestaltungsspielraum,
     sondern ein Budget: was er oben nimmt, fehlt dem Fuß unten. */
  .marke-siegel img { height: clamp(26px, 5.6vw, 34px); }
  .marke-wort { font-size: clamp(.82rem, 2.4vw, 1rem); }
  /* Der Spendenweg ist seit dem 5.8. ein gerahmter Kasten statt einer unterstrichenen
     Zeile. Auf dem Telefon kostet das Höhe, und die ist dort der knappe Posten: mit den
     Werten des Schreibtischs fiel der Fuß bei 375×667 um 18 px unter die Falz, wo vorher
     4 px Luft waren. Knapper, aber weiter über den 24 px Trefferfläche der Norm. */
  /* Auf dem Telefon läuft der Kasten seit dem 11.8. über die ganze Spalte und trägt
     seine Aufschrift mittig — auf Wunsch des Projekteigners. Damit steht er auf
     derselben Kante und derselben Breite wie »Schicht übernehmen«, statt als schmaler
     Klotz links darunter zu hängen.
     Das kehrt die Regel um, die zwei Kommentare weiter oben stand: dort war der Link
     bewusst nur textbreit, damit nicht spendet, wer neben die Aufschrift tippt. Der
     Fehlgriff bleibt möglich, kostet aber nur einen neuen Tab, den man zumacht. Auf
     dem Schreibtisch bleibt es beim schmalen Kasten: dort hängt der Fuß in einer
     Spalte neben dem Bild, und ein Balken über die ganze Spalte wäre lauter als
     »Schicht übernehmen«. */
  .plakat-foot .foot-zeile a {
    display: block; text-align: center;
    padding: 7px 13px; font-size: .95rem;
  }
  /* Das `flex-basis: auto`, das hier stand, ist mit der vollen Breite hinfällig: der
     Kasten nimmt wieder die ganze Reihe, die Grundregel gilt.
     Was es gekostet hat, ist gemessen und nicht geschätzt, beide Zustände am selben
     Baum umgeschaltet: bei 320×568, 375×667 und 390×844 kostet es NICHTS — dort passte
     neben den 118 px Kasten ohnehin nie ein Nachbar, der Fuß bleibt exakt gleich hoch
     und die Falz exakt gleich weit weg. Nur bei 430 px wächst der Fuß um 23 px, und
     auch das geht in freien Raum: die Falz bleibt bei −18 px, weil der Fuß am unteren
     Rand hängt und die Reihe aus der Luft darüber nimmt.
     align-items:center bleibt, weil Kasten und Textzeile sonst oben statt mittig
     aneinander hängen. */
  .plakat-foot { align-items: center; gap: 4px 18px; }
  /* gap 4px statt 6: über die Reihen des Fußes sind das die letzten Pixel, die der
     Pflichtangaben-Umbau dem Falz-Budget schuldete. Gemessen bei 375×667: 14 px unter
     der Falz mit 6er-Abstand, 6 px mit diesem — exakt der abgenommene Stand vor den
     Rechtstexten. Bei 320×568 ebenso zurück auf die 116 px des abgenommenen Stands. */
  .plakat-marke { padding-top: calc(4px + env(safe-area-inset-top)); padding-bottom: 4px; }
  .plakat-main h1 { margin-bottom: clamp(12px, 3vw, 18px); }
  .plakat-sub { margin-bottom: clamp(18px, 4vw, 26px); }
  /* Hier stand `margin-bottom: 12px`. Das war der Abstand zum Knopf, solange der
     Kasten ÜBER ihm saß. Seit dem 4.8. sitzt er darunter und ist letztes Kind des
     Plakatkörpers — die 12 px hingen ins Leere und schoben den Fuß nach unten. Bei
     375x667 kostete das den Ausgang: die Unterkante des Fußes stand 11,4 px unter der
     Falz, »Jetzt spenden« war nur noch erscrollbar. Der Abstand steht jetzt oben und
     kommt aus der Grundregel. */
  .qbox legend { font-size: .68rem; margin-bottom: 6px; }
  .q-opts { column-gap: 6px; row-gap: 6px; }
  .q-opt span {
    font-size: .65rem;
    letter-spacing: .03em;
    padding: .36em .46em .32em;   /* 26,7 px hoch, bleibt über der 24-px-Zielgröße */
  }
  .q-hint { font-size: .68rem; }
  /* Der Knopf nimmt die volle Spalte. Damit ist er hier das breiteste Objekt
     des Plakats, und seine Breite hängt an der Spalte statt an der Aufschrift.
     Der Maßkörper bleibt trotzdem: er hält die Höhe, falls die Aufschrift in
     einer breiteren Schrift doch zweizeilig wird. */
  .cta-block { width: 100%; }
  /* Der Schriftgrad läuft hier mit der Fensterbreite statt auf einem festen
     Boden zu sitzen: bei 320 px passt die längere Aufschrift damit auch ohne
     Anton in eine Zeile. Bei 393 px und darüber ist es wieder derselbe Grad
     wie in der Grundregel, der Übergang ist also stufenlos. */
  .plakat-cta, .cta-sizer { font-size: clamp(1.05rem, 5.5vw, 1.35rem); }
  .plakat-foot { font-size: .8rem; }
}

@media (max-width: 920px) {
  .layout { gap: 18px; }
  .side { width: min(100%, calc(var(--cell) * 10 + 60px)); }
  .radio { max-height: 130px; }
}

/* ── Die Aussparungen des Geräts ──
   `viewport-fit=cover` steht seit jeher im Kopf der Seite, `env(safe-area-inset-*)`
   kam in dieser Datei kein einziges Mal vor: die Seite verspricht, sich um Notch und
   Home-Indicator zu kümmern, und tut es nirgends. klassik.html macht es richtig, die
   ausgelieferte Fassung hat es beim Umbau verloren. Belastungsproben: bei 390x844
   endet die letzte Plakatzeile 17 px über der Unterkante gegen die 34 px des
   Home-Indicators; bei 844x390 liegt der Neustart-Knopf innerhalb der 59 px, die ein
   Gerät mit Aussparung seitlich reserviert.
   `max()` und nicht blankes `env()`: wo die Insets 0 sind — also auf jedem
   Schreibtisch und in jedem Prüfstand — bliebe sonst der heutige Wert auf der
   Strecke und das Layout schrumpfte. Die Zahlen selbst sind Plattformwerte und hier
   nicht auf einem Gerät nachgemessen; das steht als Einschränkung schon im Audit. */
#screenIntro.active {
  padding-left:  max(var(--pad-x), env(safe-area-inset-left));
  padding-right: max(var(--pad-x), env(safe-area-inset-right));
}
/* Die Unterkante nur unterhalb des Schreibtisch-Rasters: ab 900 px setzt die
   Medienabfrage `padding-bottom: 0`, damit das Bild bis an die Kante läuft — und diese
   Regel hier stand später in der Datei, hatte dieselbe Spezifität und schrieb still
   wieder eine Polsterung darüber. Gemessen: 64 px roter Streifen unter dem »randlosen«
   Bild auf jedem Desktop. Der Fuß bringt seinen Abstand dort selbst mit (var(--pad-y)
   in seiner eigenen Polsterung), und die untere Aussparung eines Geräts betrifft die
   schmalen Ansichten. */
@media (max-width: 899px) {
  #screenIntro.active { padding-bottom: max(var(--pad-y), env(safe-area-inset-bottom)); }
}
.hud {
  padding-left:  max(var(--hud-px), env(safe-area-inset-left));
  padding-right: max(var(--hud-px), env(safe-area-inset-right));
}
#screenGame .wrap {
  padding-left:   max(var(--wrap-px), env(safe-area-inset-left));
  padding-right:  max(var(--wrap-px), env(safe-area-inset-right));
  padding-bottom: max(56px, env(safe-area-inset-bottom));
}
#screenEnd.active {
  padding-bottom: max(var(--end-pad), env(safe-area-inset-bottom));
  padding-left:   max(var(--end-pad), env(safe-area-inset-left));
  padding-right:  max(var(--end-pad), env(safe-area-inset-right));
}

/* ── Das schmalste Telefon ──
   Bei 320 px kostete die Kopfleiste 229,2 px, also 31 % der Fensterhöhe bei 320x740
   und 40 % bei 320x568 — der Neustart-Knopf rutschte dort allein in eine vierte Reihe.
   Die Symbolknöpfe gehen von 38 auf 34 px und der Abstand von 14 auf 8; 34 px liegt
   weiterhin deutlich über den 24 px aus 2.5.8, und die Latte bleibt genau dort, wo sie
   ist — 24 ist die Norm, nicht der Zielwert. */
@media (max-width: 360px) {
  .hud { gap: 8px; --hud-px: 12px; padding-top: 8px; padding-bottom: 8px; }
  .hud-title { font-size: .95rem; gap: 7px; }
  .hud-stats { gap: 11px; font-size: .8rem; }
  .iconbtn { width: 34px; height: 34px; }
  .appealbtn { padding: 8px 11px; font-size: .78rem; }
}

/* ── Querformat ──
   Bis zum 4.8. hing jede Medienabfrage dieser Datei an der Breite — 430 px und 920 px,
   keine einzige an der Höhe oder der Orientierung. Bei 844x390, also jedem iPhone quer
   und bei einem Spiel ein normaler Reflex, sah man Kopfzeile, Schlagzeile in 201,6 px
   und zwei Zeilen Text: kein Regler, kein »Schicht übernehmen«, kein Fuß, und nichts,
   was andeutet, dass es weitergeht. Der Knopf lag 178 px unter der Falz.
   Die Schlagzeile ist der Posten: das Plakat verträgt hier 48 bis 56 px statt 96. */
@media (max-height: 480px) and (orientation: landscape) {
  /* Kein padding-top mehr: der Markenbalken sitzt an der Kante, siehe oben. Im
     flachen Querformat ist die Höhe der Posten, also wird er hier am flachsten. */
  #screenIntro.active { gap: 8px; }
  /* Im flachen Querformat ist die Höhe der einzige knappe Posten, also ist der Balken
     hier am flachsten. Gemessen: mit 5 px Polsterung und 118 px Logo rutschte der Fuß
     bei 844×390 um 4,2 px tiefer als vorher; so ist er 0,6 px höher als vorher. */
  .plakat-marke { padding-top: calc(4px + env(safe-area-inset-top)); padding-bottom: 4px; }
  .marke-siegel img { height: clamp(24px, 3vw, 30px); }
  .marke-wort { font-size: clamp(.8rem, 1.5vw, .95rem); }
  .plakat-main h1 { font-size: clamp(2.6rem, 6.4vw, 3.4rem); margin-bottom: 10px; }
  .plakat-sub { font-size: 1rem; margin-bottom: 12px; max-width: 62ch; }
  .plakat-foot { font-size: .74rem; }
  .cta-block { margin-bottom: 2px; }
  .qbox { margin-top: 10px; }
  .qbox legend { font-size: .66rem; margin-bottom: 4px; }
  /* Die klebende Leiste kostet im Querformat 27 % der Fensterhöhe. Sie wird zur Zeile:
     der Titel schrumpft, die Knöpfe rücken zusammen, und der gewonnene Platz geht an
     die Karte — dort lagen die Reihen I und J unter der Falz. */
  .hud { gap: 10px; --hud-px: 14px; padding-top: 6px; padding-bottom: 6px; margin-bottom: 10px; }
  .hud-title { font-size: .9rem; }
  .hud-stats { gap: 12px; font-size: .78rem; }
  .hud-stat b { font-size: .92rem; }
  .hud-stat.kasse b { font-size: .85rem; }
  .iconbtn { width: 32px; height: 32px; font-size: .9rem; }
  .appealbtn { padding: 7px 11px; font-size: .76rem; }
  #screenGame .wrap { padding-top: 8px; }
  /* Bei 390 px Fensterhöhe passt die Karte nicht ganz ins Bild, und sie kann es auch
     nicht: der Boden von --cell steht bei 27 px und bleibt dort — 24 px ist die Norm
     aus 2.5.8 und nicht der Zielwert. Also wenigstens sagen, dass es weitergeht.
     Der Zug behält seinen Platz rechts, das Maß setzt sich davor. */
  .chart-caption .mass { display: inline; margin-left: auto; }
  .chart-caption .zug { margin-left: 12px; }
}
/* Gilt für System-Einstellung UND den Schalter im Spiel (2.2.2 braucht einen
   Mechanismus in der Seite selbst, nicht nur eine Betriebssystem-Option). */
@media (prefers-reduced-motion: reduce) {
  /* animation-iteration-count MUSS mit: sonst wiederholen sich Endlos-Animationen
     alle 0,01 ms und flimmern mit Bildrate — genau bei den Menschen, die weniger
     Bewegung angefordert haben. */
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    animation-delay: 0s !important;
    transition-duration: .01ms !important;
  }
}
html.ruhig *, html.ruhig *::before, html.ruhig *::after {
  animation-duration: .01ms !important;
  animation-iteration-count: 1 !important;
  animation-delay: 0s !important;
  transition-duration: .01ms !important;
}
@media (prefers-reduced-motion: reduce) {
  .bruch p, .bruch .ctas { opacity: 1; animation: none; }
  .flash { display: none; }
  .grid::before { animation: none; opacity: .5; }
  .ripple, .burst { display: none; }
  .float-tag { animation: none; opacity: 1; }
  /* Sichtbar bleiben, nur nicht bewegt: der Preis eines Klicks ist die Auskunft,
     die hier bisher ganz fehlte, und sie darf nicht an einer Animation hängen. */
  .kasse-tag { animation: none; opacity: 1; }
  .radio li:first-child::after, .hud-title::before, .sector-box.critical { animation: none; }
  .cell.contact, .cell.rescued { animation: none; }
  .appealbtn.broke { animation: none; }
  .fx { display: none; }
  .map-stamp { animation: stampFadeReduced 1.6s linear forwards; }
  .telex-satz .caret { display: none; }
  /* Der Läufer ist die einzige Endlos-Bewegung des Plakats und geht hier weg.
     Was bleibt, ist die Schiene mit ihrer statischen Schraffur und der Satz
     darunter: die Anzeige ist weiter da und sagt weiter, dass etwas läuft — sie
     sagt es nur als Zustand statt als Bewegung. Sichtbarkeit hängt an keiner
     Animation, hier so wenig wie sonst.

     Der zweite Selektor ist Pflicht, keine Doppelung: `.q-progress.unbestimmt
     .q-track > .sv-lauf` wiegt (0,4,0) und schlüge eine nackte `.sv-lauf`-Regel.
     Der Läufer im Fortschrittsbalken bliebe dann display:block und wäre nur
     deshalb unsichtbar, weil die entschärfte Animation ihn hinter den linken
     Rand zurückfallen lässt. Auf diesen Zufall darf sich niemand verlassen. */
  .sv-lauf,
  .q-progress.unbestimmt .q-track > .sv-lauf { display: none; }
  /* Ohne diese zwei Zeilen entschärft der Block oben die Überblendung auf 0,01 ms und
     lässt sie auf ihrem ENDzustand stehen — sichtbar wäre dann nur noch der zweite
     Satz, und der trägt die Pointe, nicht die Auskunft. Wer weniger Bewegung
     angefordert hat, bekommt den ersten Satz und keinen Wechsel. */
  #qNote.zweisatz > span:nth-child(1) { opacity: 1 !important; }
  #qNote.zweisatz > span:nth-child(2) { opacity: 0 !important; }
  /* Sanfte Fades bleiben: Opacity ist keine Bewegung */
  .telex-paper { transition: opacity 200ms ease !important; }
  .telex.off .telex-paper { translate: 0 0; }
  .boat { transition: border-color 200ms ease, background-color 200ms ease, opacity 200ms ease !important; }
}
html.ruhig .bruch p, html.ruhig .bruch .ctas { opacity: 1; animation: none; }
html.ruhig .flash { display: none; }
html.ruhig .grid::before { animation: none; opacity: .5; }
html.ruhig .ripple, html.ruhig .burst, html.ruhig .fx { display: none; }
html.ruhig .float-tag, html.ruhig .kasse-tag { animation: none; opacity: 1; }
html.ruhig .radio li:first-child::after,
html.ruhig .hud-title::before,
html.ruhig .sector-box.critical,
html.ruhig .boat.critical .countdown b,
html.ruhig .appealbtn.broke,
html.ruhig .cell.contact, html.ruhig .cell.rescued { animation: none; }
html.ruhig .telex-satz .caret { display: none; }
html.ruhig .sv-lauf,
html.ruhig .q-progress.unbestimmt .q-track > .sv-lauf { display: none; }
html.ruhig #qNote.zweisatz > span:nth-child(1) { opacity: 1 !important; }
html.ruhig #qNote.zweisatz > span:nth-child(2) { opacity: 0 !important; }
html.ruhig .telex-paper { transition: opacity 200ms ease !important; }
html.ruhig .telex.off .telex-paper { translate: 0 0; }
@keyframes stampFadeReduced { 0%, 80% { opacity: .95; } 100% { opacity: 0; }
}



/* ═══════════ Der Abschluss ═══════════
   Die Zahlen liegen über der Leinwand, nicht darin: dieselbe Schrift wie die Bilanz
   dahinter, damit die Blende nach Schwarz keinen Stilbruch überdeckt. Sie zählen NICHT
   hoch — über Wasser, im Gegenlicht, mit laufender Ziffer wäre es ein Punktestand, und
   die Bilanz dahinter hat ihre eigene Begründung dafür, dass die IOM-Zahlen stillstehen.
   Nur eine Deckkraft-Blende, also läuft sie auf dem Compositor-Faden und übersteht einen
   blockierten Hauptfaden — dieselbe Regel wie beim Ladebalken. */
.finale-zahlen {
  position: fixed; inset: auto 0 14vh 0; z-index: var(--z-banner, 60);
  display: flex; justify-content: center; gap: clamp(18px, 6vw, 54px);
  pointer-events: none;
  opacity: 0; transition: opacity 900ms ease;
  text-shadow: 0 2px 18px oklch(0 0 0 / .85), 0 0 3px oklch(0 0 0 / .9);
  /* Ein Schleier unter den Ziffern. Der Schatten allein reichte nicht: über hellem
     Dämmerungswasser maß das schlechteste Kernpixel 2,3:1 gegen die geforderten 4,5.
     Ein Verlauf statt einer Platte, damit das Bild darunter sichtbar bleibt — es ist
     der Grund, warum der Abschluss überhaupt gebaut wurde. */
  padding: clamp(22px, 5vh, 40px) 0 clamp(16px, 4vh, 30px);
  background: linear-gradient(to top,
    oklch(0 0 0 / .78) 0%, oklch(0 0 0 / .62) 45%, oklch(0 0 0 / 0) 100%);
}
.finale-zahlen.an { opacity: 1; }
.finale-zahlen .fz {
  font-family: var(--mono); font-size: clamp(.6rem, 2vw, .74rem);
  text-transform: uppercase; letter-spacing: .1em;
  color: oklch(0.9 0.01 245);
  display: flex; flex-direction: column; align-items: center; gap: 2px;
}
.finale-zahlen .fz b {
  font-family: var(--display); font-weight: 400;
  font-size: clamp(2rem, 8vw, 3.6rem); line-height: 1;
  font-variant-numeric: tabular-nums;
}
.finale-zahlen .gerettet b   { color: var(--saved); }
.finale-zahlen .ertrunken b  { color: var(--distress); }
.finale-zahlen .verschleppt b { color: var(--abducted); }

.finale-schwarz {
  position: fixed; inset: 0; z-index: calc(var(--z-banner, 60) + 1);
  background: #000; pointer-events: none;
  opacity: 0; transition: opacity 900ms ease;
}
.finale-schwarz.an { opacity: 1; }

/* Unter reduzierter Bewegung stehen beide sofort da beziehungsweise sofort schwarz.
   Sichtbarkeit hängt an keiner Animation, hier so wenig wie sonst. */
@media (prefers-reduced-motion: reduce) {
  .finale-zahlen, .finale-schwarz { transition: none; }
  .finale-zahlen { opacity: 1; }
}
html.ruhig .finale-zahlen, html.ruhig .finale-schwarz { transition: none; }
html.ruhig .finale-zahlen { opacity: 1; }
