/* Öffentliche Seite — Farbwelt und Bausteine.
   ---------------------------------------------------------------------
   ZWEI FARBWELTEN (Founder-Entscheid 15.08.). Bis dahin war die Seite auf
   HELL festgelegt, mit der Begründung: eine Fassung, die niemand rendert,
   wird irgendwann versehentlich benutzt (Merkregel 30e/32c).

   Zurückgenommen, weil genau das schon eingetreten war — nur andersherum.
   Das SHOT-Makro liefert seit jeher BEIDE Aufnahmen je Motiv
   (`--shot`/`--shot-dark`, und ein Test erzwingt es), also gingen **35
   dunkle Bilder, 2,1 MB, mit jedem Deploy raus und wurden von nichts
   gerendert**. `public.js` setzte `data-theme` obendrein längst vor dem
   ersten Paint. Es gab die zweite Fassung also bereits, sie war nur
   unsichtbar — die Wahl stand zwischen „benutzen" und „aufhören,
   auszuliefern", nicht zwischen „einfarbig" und „zweifarbig".

   HELL bleibt der Entwurf, an dem die Seite gemessen wird; DUNKEL ist die
   Nachtschicht der App, nicht eine zweite Handschrift.

   Farben aus der Marke (Logo-Paket): Tinte #10143C, dritter Schlag
   Elektroblau #1F30E0. Die Wortmarke gibt es als `auf-hell` und
   `auf-dunkel` — welche gilt, entscheidet der Grund, auf dem sie liegt,
   nicht die Farbwelt (der Fuß ist in beiden dunkel). Das Deko-Gelb #FFE600
   trägt nach Markenregel NIE Text, nur Fläche und Linie.

   Der Grund ist ein KÜHLES Weiß, kein Creme: Creme ist die Voreinstellung,
   in die jede generierte Fläche fällt. Frisch wird die Seite durch Weiß,
   Weißraum und einen einzigen kräftigen Akzent — nicht durch Buntheit.

   Alles hängt unter `body.pub`, damit die geteilten Token der App
   (tokens.css/theme.css) hier nichts überschreiben und umgekehrt.

   ---------------------------------------------------------------------
   HAUSREGEL (Session 45). Zwei Sätze, gegen EINEN Fehler.

   Der Fehler: viermal auf dieser Seite hat ein Selektor, der nur sagt WO
   etwas steht, den Selektor überstimmt, der sagt WAS es ist. `body.pub a`
   (0,1,2) schlug `.pub-btn--cta` (0,1,0) — jede CTA-Beschriftung stand
   navy auf blau. `.pub-band-volt .pub-note` (0,2,0) schlug `.pub-note` und
   griff durch die weiße Karte hindurch — die Einwilligungszeile stand weiß
   auf weiß. `.pub-hero-media .pub-shot` (0,2,0) schlug `.pub-shot--phone`
   — die Kopf-Aufnahme war unter 960 px ein 288×153-Streifen quer durch ein
   Hochkant-Bild. `body.pub h1…h4` (0,1,2) schlug `.pub-foot h4` — die
   Spaltenköpfe im Fuß standen navy auf navy. Dieselbe Falle, viermal
   (Merkregel 25b/25h/31/45).

   1. JEDER KONTEXT STEHT IN `:where()`. Alles vor dem letzten Glied eines
      Selektors beschreibt nur die Umgebung und wird auf 0 gesetzt:
      `A B { }` heißt hier `:where(A) B { }`. Damit kommt Gewicht
      ausschließlich aus den Klassen des Elements SELBST, und eine Variante
      (`--phone`, `--cta`, `--ghost`) kann gegen ihren Ort nicht mehr
      verlieren. Gleichstand entscheidet die Reihenfolge — das ist die
      lesbare Kaskade, nicht die Falle. Ein Test hält die Regel:
      `test_public_css.py::test_every_context_selector_is_wrapped`.

   2. KONTEXTFARBE REIST NICHT ÜBER SELEKTOREN, SONDERN ALS VARIABLE.
      Ein Selektor greift beliebig tief — auch in eine weiße Karte, die im
      blauen Band liegt. Deshalb sagt das Band nicht mehr, welche Farbe der
      Text darin hat, sondern nur noch, welchen GRUND es stellt: die fünf
      Rollen unten erben, und jede Fläche, die ihren eigenen Grund streicht,
      stellt sie zurück. Es entscheidet die Nähe im Baum, nicht das Gewicht
      des Selektors und nicht die Zeilennummer. Damit ist alles, was jemand
      künftig in die Karte legt, von sich aus richtig. */

body.pub {
    /* ZWEI FARBWELTEN, EINE ZEILE JE TOKEN.
       `light-dark()` statt eines zweiten Blocks unter `prefers-color-scheme`:
       eine zweite Fassung derselben Liste ist die Kopie, die beim ersten
       Umzug auseinanderläuft. Der Schalter darunter setzt nur noch
       `color-scheme`.

       DUNKEL ist NICHT erfunden, sondern die Nachtschicht der App
       (`shared/theme.css`): Grund #0a0b0d, Karte #131416, Kante #222326,
       Tinte #e8e4dc, gedämpft #8f8a84 — dort schon auf AA nachgerechnet
       (Session 49). Schaufenster und Werkbank sollen nicht zwei
       verschiedene Nächte haben. */
    color-scheme: light dark;

    /* Gründe */
    --p-ground:   light-dark(#FFFFFF, #0a0b0d);
    --p-raised:   light-dark(#F3F5FC, #131416);
    --p-card:     light-dark(#FFFFFF, #131416);
    --p-sunken:   light-dark(#EDF0FA, #1a1b1e);

    /* Linien — Struktur aus Haarlinien, nicht aus Kästen */
    --p-line:     light-dark(rgba(16, 20, 60, 0.11), #222326);
    --p-line-2:   light-dark(rgba(16, 20, 60, 0.20), #33353a);

    /* Tinte */
    --p-ink:      light-dark(#10143C, #e8e4dc);
    --p-dim:      light-dark(rgba(16, 20, 60, 0.66), rgba(232, 228, 220, 0.74));
    --p-mute:     light-dark(rgba(16, 20, 60, 0.62), #8f8a84);

    /* Akzent — der dritte Schlag.
       ZWEI Token, seit es DUNKEL gibt, und das ist keine Bequemlichkeit:
       `--p-volt` ist die TINTE (Verweis, Markierungsstrich) und muss auf
       schwarzem Grund heller werden — Elektroblau auf #0a0b0d sind 2,4:1.
       `--p-volt-field` ist die FLÄCHE des Wartelisten-Bandes und bleibt
       gesättigt, weil weiße Schrift darauf steht (8,2:1). Ein Token, das je
       Farbwelt etwas anderes bedeutet, zwingt die Seite zum Umweg — genau
       die Falle aus Session 61. */
    --p-volt:       light-dark(#1F30E0, #8B97FF);
    --p-volt-field: #1F30E0;
    --p-volt-hi:    light-dark(#1526C4, #A3ADFF);
    --p-volt-lo:    light-dark(rgba(31, 48, 224, 0.08), rgba(139, 151, 255, 0.14));
    /* Die Schrift AUF einer Volt-Fläche (Schaltfläche, Sprungmarke). Sie
       kippt mit, weil die Fläche mitkippt: weiß auf dem hellen Elektroblau
       (8,2:1), dunkle Tinte auf dem aufgehellten (7,5:1). Weiß stehen zu
       lassen hätte 2,6:1 ergeben — gesehen im ersten dunklen Bild, nicht
       im Code. */
    --p-volt-ink:   light-dark(#FFFFFF, #0a0b0d);

    /* Die Fläche des Fusses. Sie war `var(--p-ink)` — in DUNKEL wäre der
       Fuss damit fast weiss geworden, weil die Tinte kippt. Eine Fläche
       ist keine Tinte. */
    --p-foot-field: light-dark(#10143C, #131416);

    /* Deko (nie Text) */
    --p-deco:     #FFE600;

    /* Signale */
    --p-pos:      light-dark(#1B7A5C, #4FC49C);

    /* Kontextfarben — die fünf Rollen, die über den Grund wandern.
       Sie erben, also entscheidet die NÄHE im Baum: `.pub-band-volt`
       stellt sie auf Weiß, `.pub-form-card` darin stellt sie zurück.
       Wer eine Fläche baut, die ihren eigenen Grund streicht, setzt diese
       fünf — und muss über keinen einzigen Textbaustein darin nachdenken. */
    --p-strong:   var(--p-ink);      /* Überschrift, Hervorhebung */
    --p-body:     var(--p-dim);      /* Fließtext */
    --p-fine:     var(--p-mute);     /* Fußnote, Bildunterschrift */
    --p-link:     var(--p-volt);     /* Verweis im Fließtext */
    --p-mark:     var(--p-volt);     /* der Strich, der einen Kopf markiert */

    /* Maße */
    --p-nav-h:    70px;
    --p-r-sm:     4px;
    --p-r:        14px;
    --p-r-lg:     18px;
    --p-gut:      clamp(20px, 5vw, 44px);
    --p-sec:      clamp(76px, 11vw, 132px);

    background: var(--p-ground);
    color: var(--p-ink);
    font-family: 'DM Sans', system-ui, -apple-system, sans-serif;
    font-size: 17px;
    line-height: 1.62;
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
    margin: 0;
    overflow-x: hidden;
}

/* Regel 2, die andere Hälfte: jede Fläche, die ihren eigenen Grund
   streicht, stellt die fünf Rollen zurück. Ohne diese Zeile war die
   weiße Karte im blauen Band weiß auf weiß — der einzige rechtlich
   tragende Satz der Seite, an der einzigen Stelle, an der er zählt. */
.pub-form-card, .pub-card, .pub-cta, .pub-legal-box, .pub-toc {
    --p-strong: var(--p-ink);
    --p-body:   var(--p-dim);
    --p-fine:   var(--p-mute);
    --p-link:   var(--p-volt);
    --p-mark:   var(--p-volt);
}

@media (max-width: 640px) {
    body.pub { font-size: 16px; --p-nav-h: 62px; }
}

/* ===================================================================
   TYPOGRAFIE
   Eine Familie, große Spanne. DM Sans ist die Schrift der Wortmarke —
   das ist der Grund, aus dem sie hier steht, und der einzige, der zählt:
   die Seite lädt nichts von fremden Servern, und im Haus liegen genau
   zwei Familien. Der Auftritt kommt aus Grad, Größe und Laufweite.
   =================================================================== */

:where(body.pub) :is(h1, h2, h3, h4) {
    margin: 0;
    font-weight: 700;
    letter-spacing: -0.025em;
    line-height: 1.08;
    text-wrap: balance;
    color: var(--p-strong);
}

:where(body.pub) p { margin: 0; }
/* Ohne `:where()` war das hier (0,1,2) und schlug JEDE Schaltflächen- und
   Verweisfarbe der Seite: die CTA stand navy auf blau (2,15:1), der
   Sprungverweis ebenso, und jeder Verweis im Fließtext erbte grau statt
   Elektroblau zu tragen. */
:where(body.pub) a { color: inherit; }

.pub-display {
    font-size: clamp(2.35rem, 6.4vw, 4.9rem);
    font-weight: 760;
    letter-spacing: -0.04em;
    line-height: 0.98;
    /* Die Umbrueche im Markup sind gesetzt, nicht geraten — `balance`
       wuerde gegen sie arbeiten. Bis zur Hausregel hat `body.pub h1` diese
       Zeile ueberstimmt; der dokumentierte Schutz stand da und galt nicht. */
    text-wrap: initial;
}
/* Auf schmalen Schirmen bricht auch eine gesetzte Zeile noch einmal um.
   Dort soll `balance` die Reste verteilen, sonst steht im Englischen
   "The plan" allein auf der ersten Zeile. Die Abfrage steht HINTER der
   Grundregel: seit beide gleich schwer sind, entscheidet die Reihenfolge. */
@media (max-width: 640px) {
    .pub-display { text-wrap: balance; }
}

.pub-h2 {
    font-size: clamp(1.85rem, 3.9vw, 2.95rem);
    font-weight: 720;
    letter-spacing: -0.032em;
    line-height: 1.06;
    max-width: 20ch;
}
:where(.pub-center) .pub-h2 { max-width: 22ch; margin-inline: auto; }

/* Der markierte Kopf: ein gezogener Volt-Strich unter der letzten Zeile,
   kein Textmarker. Der Strich sitzt am Kasten, nicht am Wort — so bricht
   er mit der Zeile um, statt mitten im Satz zu hängen. Seine Farbe ist
   eine Kontextrolle: auf dem Volt-Feld wird er von selbst gelb. */
.pub-h2--marked { position: relative; padding-bottom: 20px; }
.pub-h2--marked::after {
    content: '';
    position: absolute; left: 0; bottom: 0;
    width: 66px; height: 4px;
    background: var(--p-mark);
    border-radius: 2px;
}
:where(.pub-center) .pub-h2--marked::after { left: 50%; transform: translateX(-50%); }


.pub-lead {
    font-size: clamp(1.08rem, 1.8vw, 1.28rem);
    line-height: 1.55;
    color: var(--p-body);
    max-width: 46ch;
    margin-top: 22px;
}

.pub-sub {
    font-size: clamp(1rem, 1.5vw, 1.1rem);
    line-height: 1.62;
    color: var(--p-body);
    max-width: 62ch;
    margin-top: 18px;
}
:where(.pub-center) .pub-sub { margin-inline: auto; }

.pub-note {
    font-size: 0.86rem; line-height: 1.55;
    color: var(--p-fine);
    margin-top: 18px;
}
:where(.pub-note) a { color: var(--p-link); text-underline-offset: 3px; }

/* ===================================================================
   RAHMEN
   =================================================================== */

.pub-wrap {
    width: 100%; max-width: 1220px;
    margin-inline: auto;
    padding-inline: var(--p-gut);
}
.pub-wrap--narrow { max-width: 780px; }
.pub-center { text-align: center; }

.pub-sec { padding-block: var(--p-sec); }

/* Die Leiste klebt, also verdeckt sie den Kopf jedes angesprungenen
   Abschnitts — und die wichtigste Schaltflaeche der Seite IST ein
   Sprung (#warteliste). Ohne diese Zeile landet man auf der Warteliste
   und sieht die Ueberschrift nicht. */
:where(body.pub) [id] { scroll-margin-top: calc(var(--p-nav-h) + 16px); }
.pub-sec--tight { padding-block: clamp(52px, 7vw, 84px); }
.pub-sec--alt { background: var(--p-raised); }

/* Haarlinie zwischen zwei gleichfarbigen Abschnitten, damit der Rhythmus
   sichtbar bleibt, ohne einen Kasten zu zeichnen. */
:where(.pub-sec) + .pub-sec:not(.pub-sec--alt) { border-top: 1px solid var(--p-line); }

/* Die Deko-Linie: drei Segmente, dieselbe Zählung wie die drei Schläge des
   Zeichens. Auf hellem Grund gilt die Markenregel für `auf-hell` — Tinte
   #10143C, dritter Schlag Elektroblau. Gelb traegt hier nichts: auf Weiss
   waere es unsichtbar. Auf dem Volt-Feld dreht sich beides um — das
   erledigen die Kontextrollen, nicht zwei weitere Selektoren. */
.pub-rule { display: flex; gap: 6px; margin: 0 0 26px; }
:where(.pub-rule) i {
    display: block; height: 4px; border-radius: 2px;
    background: var(--p-strong);
}
:where(.pub-rule) i:nth-child(1) { width: 34px; }
:where(.pub-rule) i:nth-child(2) { width: 18px; }
:where(.pub-rule) i:nth-child(3) { width: 8px; background: var(--p-mark); }

/* ===================================================================
   LEISTE
   Über dem Band durchsichtig, danach fest. `backdrop-filter` steht NUR
   auf der Leiste selbst — nie auf einem Vorfahren, sonst wird der zum
   Bezugsrahmen für alles Feste darin (Merkregel 41).
   =================================================================== */

.pub-skip {
    position: absolute; left: -9999px; top: 0; z-index: 100;
    background: var(--p-volt); color: var(--p-volt-ink);
    padding: 12px 18px; font-weight: 640;
}
.pub-skip:focus { left: 0; }

.pub-nav {
    position: sticky; top: 0; z-index: 60;
    background: transparent;
    border-bottom: 1px solid transparent;
    transition: background 240ms ease, border-color 240ms ease;
}
/* Die feste Leiste. Ihr Grund wird aus `--p-ground` GERECHNET, nicht
   danebengeschrieben: als festes `rgba(255,255,255,.86)` war sie in DUNKEL
   ein hellgrauer Balken mit heller Schrift — unlesbar, und zwar erst beim
   SCROLLEN, also genau in dem Zustand, den kein Vollseiten-Bild zeigt. Der
   Rig hat `pub_nav_stuck_dunkel.png` sehr wohl geschossen; angesehen hat es
   niemand. Eine Fläche ohne BLICK ist ungeprüft, nicht eine ohne Bild.

   MESSFALLE beim Nachprüfen, teurer als der Fehler selbst: `.pub-nav` hat
   `transition: background 240ms`. Wer die Klasse per JS setzt und SOFORT
   `getComputedStyle` liest, bekommt den Startwert der Überblendung
   (durchsichtig) und hält ihn für das Ergebnis — ich habe daraufhin
   `color-mix()` für kaputt erklärt und durch ein zweites Token ersetzt.
   Beides war falsch: mit 600 ms Wartezeit gemessen liefert `color-mix`
   sauber `color(srgb 1 1 1 / 0.86)` bzw. `color(srgb 0.039 0.043 0.051 /
   0.86)`. Wer einen Übergang misst, muss ihn zu Ende laufen lassen
   (dieselbe Klasse wie die 720-ms-Wische in Session 46).

   Abgeleitet statt danebengeschrieben, weil eine Farbe, die man herleiten
   kann, nicht gewählt wird: die Leiste folgt damit jedem künftigen Grund
   von selbst. */
.pub-nav.is-stuck {
    background: color-mix(in srgb, var(--p-ground) 86%, transparent);
    -webkit-backdrop-filter: saturate(180%) blur(14px);
    backdrop-filter: saturate(180%) blur(14px);
    border-bottom-color: var(--p-line);
}

.pub-nav-in {
    display: flex; align-items: center; gap: 28px;
    min-height: var(--p-nav-h);
    width: 100%; max-width: 1220px;
    margin-inline: auto; padding-inline: var(--p-gut);
}

.pub-mark {
    display: block; flex: none; position: relative;
    width: 106px; height: 30px;
    background: url('/static/brand/cox-header-auf-hell.svg') left center / contain no-repeat;
    text-decoration: none;
}
:where(.pub-mark) span {
    position: absolute; width: 1px; height: 1px;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

.pub-nav-links { display: flex; gap: 26px; margin-left: 8px; }
:where(.pub-nav-links) a {
    font-size: 0.94rem; font-weight: 500;
    color: var(--p-body); text-decoration: none;
    padding-block: 6px;
    border-bottom: 2px solid transparent;
    transition: color 160ms ease, border-color 160ms ease;
}
:where(.pub-nav-links) a:hover { color: var(--p-strong); }
:where(.pub-nav-links) a[aria-current="page"] {
    color: var(--p-strong); border-bottom-color: var(--p-volt);
}

.pub-nav-act { display: flex; align-items: center; gap: 18px; margin-left: auto; }
.pub-plain {
    font-size: 0.94rem; color: var(--p-body); text-decoration: none;
    transition: color 160ms ease;
}
.pub-plain:hover { color: var(--p-strong); }

@media (max-width: 880px) { .pub-nav-links { display: none; } }
@media (max-width: 460px) { .pub-plain { display: none; } }

/* ===================================================================
   SCHALTFLÄCHEN
   =================================================================== */

.pub-btn {
    display: inline-flex; align-items: center; justify-content: center;
    gap: 9px;
    padding: 15px 27px;
    font-family: inherit; font-size: 0.98rem; font-weight: 640;
    letter-spacing: -0.005em;
    border-radius: var(--p-r-sm);
    border: 1px solid transparent;
    text-decoration: none; cursor: pointer;
    transition: transform 150ms ease, background 160ms ease,
                border-color 160ms ease, color 160ms ease;
}
.pub-btn:active { transform: translateY(1px); }

/* Dieselbe Schaltfläche als `<a>` und als `<button>` muss dasselbe Bild
   ergeben. Genau das war sie nicht: der Knopf im Formular stand weiß auf
   blau (8,21:1), jeder Verweis daneben navy auf blau (2,15:1) — zwei
   gleich aussehende Schalter, zwei Farben, auf einer Seite. */
.pub-btn--cta, .pub-btn--solid {
    background: var(--p-volt); color: var(--p-volt-ink);
}
.pub-btn--cta:hover, .pub-btn--solid:hover { background: var(--p-volt-hi); }

.pub-btn--ghost {
    background: transparent; color: var(--p-strong);
    border-color: var(--p-line-2);
}
.pub-btn--ghost:hover { border-color: var(--p-ink); }

.pub-btn--sm { padding: 10px 18px; font-size: 0.9rem; }

/* Senden ist ein Zustand, kein Moment: public.js schaltet `disabled` und
   tauscht die Beschriftung — ohne diese Regel behielte der Knopf volle
   Farbe und Hover und saehe aus wie einer, der noch auf den Klick wartet. */
.pub-btn:disabled,
.pub-btn[aria-disabled="true"] {
    background: var(--p-sunken);
    color: var(--p-mute);
    border-color: var(--p-line);
    cursor: progress;
    transform: none;
}
.pub-btn:disabled:hover { background: var(--p-sunken); }

:where(body.pub) :focus-visible {
    outline: 2px solid var(--p-volt);
    outline-offset: 3px;
    border-radius: 2px;
}

/* ===================================================================
   BAND / KOPFBILD
   Kein schwebendes Telefon im Winkel. Die Aussage steht groß, darunter
   die Zahlen, die stimmen — daneben die Fläche, um die es geht.
   =================================================================== */

.pub-band {
    position: relative;
    margin-top: calc(var(--p-nav-h) * -1);
    padding-top: var(--p-nav-h);
    background:
        radial-gradient(110% 85% at 82% 0%, rgba(31, 48, 224, 0.10), transparent 60%),
        linear-gradient(180deg, var(--p-raised) 0%, var(--p-ground) 76%);
    overflow: hidden;
}


/* Einspaltig ist die Grundform — jede Seite, die den Kopf benutzt, steht
   damit richtig. Zweispaltig wird er nur, wo eine Fläche danebenliegt und
   das Markup dafür gebaut ist (`--split`, ein Kind je Spalte). */
.pub-hero {
    position: relative;
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: clamp(30px, 4vw, 48px);
    align-items: center;
    padding-block: clamp(40px, 5vw, 66px) clamp(44px, 5.5vw, 70px);
}
/* Der Textkopf der Unterseiten wird als INHALT begrenzt, nicht als Rahmen —
   sonst wandert seine linke Kante gegen die der Marke. Die zweite Zeile ist
   die Folge der Hausregel: `> *` ist (0,0,0) und verliert damit gegen die
   46ch von `.pub-lead`. Der Kopf einer Unterseite setzt sein Maß bewusst
   breiter, also sagt er das jetzt auch. */
:where(.pub-hero--sub) > * { max-width: 780px; }
:where(.pub-hero--sub) > .pub-lead { max-width: 780px; }
/* Die Zeile bekommt die VOLLE Breite, nicht die halbe. Sonst muss die
   Schrift schrumpfen, bis sie in eine Spalte passt — und genau daran ist
   die erste Fassung gescheitert: „erklaert ihn." fiel als Waise auf eine
   dritte Zeile. Kopf oben ueber alles, darunter Text neben Flaeche. */
.pub-hero--split {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: clamp(22px, 2.6vw, 34px) clamp(32px, 5vw, 66px);
    align-items: start;
}
.pub-hero-head { grid-column: 1 / -1; }
/* Oben ausrichten, nicht mittig: die Fläche daneben ist hoeher als der
   Text, und zentriert reisst das ein Loch zwischen Zeile und Absatz. */
.pub-hero-say { align-self: start; }
:where(.pub-hero-say) .pub-lead { margin-top: 0; }

@media (max-width: 960px) {
    .pub-hero--split { grid-template-columns: minmax(0, 1fr); gap: 32px; }
}

.pub-badge {
    display: inline-flex; align-items: center; gap: 9px;
    font-size: 0.78rem; font-weight: 580;
    letter-spacing: 0.05em;
    color: var(--p-strong);
    background: var(--p-volt-lo);
    border: 1px solid rgba(31, 48, 224, 0.22);
    border-radius: 100px;
    padding: 7px 15px 7px 12px;
    margin: 0 0 26px;
}
:where(.pub-badge) b { color: var(--p-volt); font-size: 0.7em; line-height: 1; }

.pub-hero-act { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 30px; }
.pub-hero-note { font-size: 0.85rem; color: var(--p-fine); margin-top: 16px; }

/* Auf dem Telefon stehen die beiden Schaltflaechen sonst unterschiedlich
   breit untereinander — das sieht nach Versehen aus, nicht nach Rangfolge. */
@media (max-width: 560px) {
    .pub-hero-act { gap: 10px; }
    :where(.pub-hero-act) .pub-btn { flex: 1 1 100%; }
    :where(.pub-nav-act) .pub-btn--sm { padding: 9px 14px; font-size: 0.85rem; }
}

/* Die Zahlenreihe (271 Uebungen · 4 Methoden · 10 Plaene · EU) ist in
   Session 46 ausgebaut worden — samt CSS, statt sie danebenliegen zu
   lassen. Ein Katalogumfang ueber dem Falz beantwortet die Frage „was
   kann das?" mit „viel davon", und genau so liest er sich: wie eine
   Anfaenger-App. Zahlen stehen jetzt auf den Modulseiten, dort wo sie
   eine Behauptung BELEGEN. */

.pub-sticky-sentinel { position: absolute; bottom: 0; height: 1px; width: 100%; }

.pub-hero-media { position: relative; min-width: 0; }

/* ===================================================================
   RASTER UND KARTEN
   =================================================================== */

.pub-grid { display: grid; gap: clamp(24px, 3vw, 40px); margin-top: clamp(36px, 5vw, 52px); }
.pub-grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.pub-grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }

@media (max-width: 900px) { .pub-grid--3 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 640px) { .pub-grid--2, .pub-grid--3 { grid-template-columns: 1fr; } }

.pub-point {
    min-width: 0;
    padding-top: 20px;
    border-top: 2px solid var(--p-strong);
    display: flex; flex-direction: column; gap: 10px;
}
.pub-point:first-child { border-top-color: var(--p-mark); }
:where(.pub-point) h3 {
    font-size: 1.14rem; font-weight: 680; letter-spacing: -0.022em;
}
:where(.pub-point) p { color: var(--p-body); font-size: 0.97rem; line-height: 1.6; }

.pub-card {
    /* Grid-Kinder brauchen min-width:0, sonst sprengt ein langes Wort die
       Spalte (Merkregel 22). */
    min-width: 0;
    background: var(--p-card);
    border: 1px solid var(--p-line);
    border-radius: var(--p-r);
    padding: clamp(24px, 2.6vw, 32px);
    display: flex; flex-direction: column; gap: 12px;
    text-decoration: none;
    transition: border-color 200ms ease, box-shadow 200ms ease,
                transform 200ms ease;
}
a.pub-card:hover {
    border-color: rgba(31, 48, 224, 0.34);
    box-shadow: 0 12px 30px rgba(16, 20, 60, 0.09);
    transform: translateY(-2px);
}
:where(.pub-card) h3 { font-size: 1.18rem; font-weight: 660; letter-spacing: -0.022em; }
:where(.pub-card) p { color: var(--p-body); font-size: 0.97rem; line-height: 1.6; }


.pub-card-more {
    margin-top: auto; padding-top: 8px;
    font-size: 0.9rem; font-weight: 620;
    color: var(--p-volt);
    display: inline-flex; align-items: center; gap: 7px;
}
.pub-card-more::after { content: '→'; transition: transform 180ms ease; }
:where(a.pub-card:hover) .pub-card-more::after { transform: translateX(4px); }

/* ===================================================================
   ZEILE MIT FLÄCHE
   =================================================================== */

.pub-row {
    display: grid;
    grid-template-columns: minmax(0, 0.88fr) minmax(0, 1.12fr);
    gap: clamp(28px, 4.5vw, 62px);
    align-items: center;
    margin-top: clamp(48px, 6vw, 76px);
}
:where(.pub-row) > div { min-width: 0; }
:where(.pub-row) h3 {
    font-size: clamp(1.25rem, 2vw, 1.5rem);
    font-weight: 680; letter-spacing: -0.026em;
    margin-bottom: 14px;
}
:where(.pub-row) p { color: var(--p-body); margin-bottom: 12px; }

/* Jede zweite Zeile dreht die Seiten — sonst läuft die Seite als Spalte
   herunter und der Blick schläft ein. Die Klasse steht im Markup statt
   `nth-of-type`: der Einleitungs-Block ist auch ein `div`, zaehlt also mit,
   und welche Zeile sich dreht haengt dann an der Reihenfolge fremder
   Geschwister statt an der Absicht. */
:where(.pub-row--flip) .pub-row-media { order: -1; }
@media (max-width: 880px) {
    .pub-row { grid-template-columns: 1fr; gap: 26px; }
    :where(.pub-row--flip) .pub-row-media { order: 0; }
}

/* ===================================================================
   OBERFLÄCHEN-AUFNAHMEN
   Die Seite ist hell, also zeigt sie die HELLE Aufnahme. Immer. Die
   dunkle Datei bleibt im Token stehen (das Makro liefert beide) und wird
   hier bewusst nicht benutzt — eine dunkle Aufnahme auf hellem Grund ist
   ein Loch in der Seite.
   =================================================================== */

.pub-shot {
    display: block; margin: 0;
    border-radius: var(--p-r-lg);
    overflow: hidden;
    background-color: var(--p-sunken);
    background-image: var(--shot);
    background-size: 100% auto;
    background-position: center top;
    background-repeat: no-repeat;
    aspect-ratio: 1120 / 700;
    border: 1px solid var(--p-line);
    box-shadow: 0 18px 44px rgba(16, 20, 60, 0.10);
}
.pub-shot--phone {
    max-width: 288px; margin-inline: auto;
    border-radius: 26px;
    aspect-ratio: 780 / 1520;
}
/* Der Ausschnitt: die Dynamic Island, und nur sie. Die Aufnahme darunter
   zeigt zwangslaeufig eine FREMDE App — der Rig oeffnet die Einstellungen,
   weil sich nur so beweisen laesst, dass die Pausenuhr ausserhalb von Cox
   weiterlaeuft. Als Vollbild waeren das 90 % Apple-Einstellungen; als
   Streifen ist es der Beleg. Das Verhaeltnis entspricht ISLAND_BOX in
   scripts/dev/publish_shots.py (1066 × 190) — wer eins von beiden aendert,
   aendert beide, sonst steht die Insel nicht mehr mittig. */
.pub-shot--strip {
    aspect-ratio: 1066 / 190;
    background-size: 100% auto;
    border-radius: 14px;
}
.pub-shot-cap {
    font-size: 0.79rem; color: var(--p-fine);
    margin: 12px 2px 0; line-height: 1.45;
}

/* Zwei Aufnahmen, eine je Geraet. Die Schreibtisch-Fläche auf 390 px
   heruntergerechnet ist kein Beweis, sondern ein graues Rechteck: darin
   ist keine Zeile mehr lesbar. Auf dem Telefon zeigt die Seite deshalb
   die Telefon-Aufnahme. Beide Sprachen fuehren dieselben Motive in
   derselben Reihenfolge (Paritaetstest in tests/test_public.py). */
.pub-only-phone { display: none; }
@media (max-width: 960px) {
    .pub-only-wide  { display: none; }
    .pub-only-phone { display: block; }
}

/* Kopfbild: die echte Oberfläche, groß und enger beschnitten als im Rest
   der Seite. Der Ausschnitt gilt AUSDRÜCKLICH nur der querformatigen
   Aufnahme — vorher stand er als (0,2,0) über `.pub-shot--phone` (0,1,0)
   und zwang dem Hochkant-Bild ein Querformat auf: unter 960 px war der
   Kopf ein 288×153-Streifen quer durch einen Screenshot, mitten im Wort
   abgeschnitten. Zwei Regeln statt einer, weil der Schatten beiden gilt
   und der Ausschnitt nur einer. Der Block steht hinter `.pub-shot`, damit
   er bei gleichem Gewicht gewinnt.

   116 % schnitt genau durch die RPE-Zeile (bei y≈514 von 700). 111 %
   legt die Kante in die Lücke darunter (y≈537), zwischen die Zahlenreihe
   und „DAUER (MIN)". */
:where(.pub-hero-media) .pub-shot {
    box-shadow:
        0 30px 70px rgba(16, 20, 60, 0.16),
        0 2px 8px rgba(16, 20, 60, 0.06);
}
:where(.pub-hero-media) .pub-shot:not(.pub-shot--phone) {
    aspect-ratio: 1120 / 596;
    background-size: 111% auto;
    background-position: 50% 0;
}
/* Seit der Kopf das TELEFON traegt (15.08.), ist die Hochkant-Aufnahme dort
   keine Beigabe mehr, sondern das Motiv — 288 px waeren neben einer
   Display-Zeile von 4,9rem eine Briefmarke. Die Regel hat dasselbe Gewicht
   wie `.pub-shot--phone` (0,1,0, der Kontext steht in `:where()`) und
   gewinnt ueber die Reihenfolge; genau so soll die Hausregel wirken. */
:where(.pub-hero-media) .pub-shot--phone {
    max-width: 340px;
}

/* ===================================================================
   FOTOGRAFIE
   Die Seite zeigt sonst ausschließlich die Oberfläche — also die
   AUSWERTUNG des Sports, nie den Sport. Ein Bild auf Seitenmaß ist die
   Pause dazwischen und der einzige Ort, an dem das Heben selbst vorkommt.

   Es trägt bewusst NICHTS: keine Bildunterschrift, keinen Satz darauf,
   keine Zuschreibung, kein Gesicht in die Kamera. Eine Aufnahme der
   Oberfläche sagt „Beispieldaten", weil sie etwas behauptet — ein Foto
   behauptet hier nichts und darf deshalb auch nicht die Form eines Belegs
   bekommen (DESIGN.md, „Nothing may wear the shape of proof").

   Kein Text darauf heißt auch: kein Kontrastproblem, kein Schleier, keine
   zweite Farbwelt. Der Grund darunter ist `--p-sunken`, damit ein Bild,
   das noch lädt, ein Bildbett ist und kein Loch.
   =================================================================== */

.pub-photo {
    display: block; margin: 0;
    background-color: var(--p-sunken);
    background-image: var(--photo);
    background-size: cover;
    background-position: 50% 50%;
    background-repeat: no-repeat;
    aspect-ratio: 21 / 9;
}
/* Auf dem Telefon wird aus dem Panorama sonst ein Streifen: dieselbe
   Aufnahme, höherer Ausschnitt — dieselbe Lehre wie bei `--phone`. */
@media (max-width: 760px) {
    .pub-photo { aspect-ratio: 5 / 4; }
}

/* ===================================================================
   LISTEN, ZITATE
   =================================================================== */

.pub-li { list-style: none; padding: 0; margin: 18px 0 0; }
:where(.pub-li) li {
    position: relative; padding-left: 26px; margin-bottom: 11px;
    color: var(--p-body); line-height: 1.6;
}
:where(.pub-li) li::before {
    content: ''; position: absolute; left: 2px; top: 0.6em;
    width: 11px; height: 3px; border-radius: 1px;
    background: var(--p-mark);
    transform: skewX(-11deg);
}
:where(.pub-li li) b { color: var(--p-strong); font-weight: 640; }

/* Wiedererkennungs-Saetze. Bewusst KEIN Zitat: kein blockquote, keine
   Anfuehrungszeichen, keine Zuschreibung. Cox hat noch keinen einzigen
   Kunden — nichts auf dieser Seite darf die Form eines Belegs tragen, auch
   nicht, wenn der Satz ehrlich gemeint ist. Getragen wird der Absatz von
   einer Haarlinie und seiner Groesse, nicht von einem Kasten. */
.pub-said {
    min-width: 0;
    font-size: clamp(1.05rem, 1.5vw, 1.22rem);
    line-height: 1.5;
    color: var(--p-strong);
    padding-top: 20px;
    border-top: 2px solid var(--p-mark);
    text-wrap: pretty;
}

/* `.pub-quote` ist in Session 46 ausgebaut. Es gab genau EINE Stelle, die
   ihn benutzte (/ernaehrung), und dort stand kein Zitat, sondern eine
   Aussage ueber die eigene Oberflaeche in der Form eines Zitats — mit
   Anfuehrungszeichen und `<cite>` ohne Sprecher. DESIGN.md „Banned here":
   nichts darf die Gestalt eines Belegs tragen. Ein ungenutzter Baustein
   wird irgendwann versehentlich benutzt, deshalb geloescht statt
   danebengelegt. */

/* ===================================================================
   VERBINDUNGEN
   =================================================================== */

.pub-conn {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(228px, 1fr));
    gap: 1px;
    margin-top: clamp(32px, 4vw, 44px);
    background: var(--p-line);
    border: 1px solid var(--p-line);
    border-radius: var(--p-r);
    overflow: hidden;
}
.pub-conn-item {
    background: var(--p-card);
    padding: 18px 20px;
    display: flex; align-items: flex-start; gap: 12px;
    min-width: 0;
}
:where(.pub-sec--alt) .pub-conn-item { background: var(--p-ground); }
:where(.pub-conn-item) > span:last-child {
    display: flex; flex-direction: column; gap: 2px; min-width: 0;
}
:where(.pub-conn-item) b { font-size: 0.97rem; font-weight: 640; color: var(--p-strong); }
:where(.pub-conn-item span) span { font-size: 0.83rem; color: var(--p-fine); line-height: 1.4; }

.pub-conn-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; margin-top: 8px; }
:where(.pub-conn-item[data-state="live"]) .pub-conn-dot {
    background: var(--p-pos);
    box-shadow: 0 0 0 3px rgba(46, 158, 123, 0.16);
}
:where(.pub-conn-item[data-state="planned"]) .pub-conn-dot {
    background: transparent; border: 1.5px solid var(--p-mute);
}
:where(.pub-conn-item[data-state="planned"]) b { color: var(--p-body); }

/* ===================================================================
   DAS BAND, DAS ZUM HANDELN AUFFORDERT
   Ein durchgehendes Volt-Feld — Farbe auf Seitenmaß, nicht als Tupfer.
   Gelesen wird darauf NICHT (Markenregel): der Fließtext sitzt auf der
   weißen Karte darin, das Band trägt nur Großes und Kurzes.

   Das Band sagt deshalb NICHT MEHR, welche Farbe ein Baustein darin hat.
   Es stellt nur den Grund — die fünf Rollen erben, und die weiße Karte
   darin stellt sie zurück. Vorher standen hier sechs Kontext-Selektoren;
   zwei davon waren tot (`h2`/`h3` verloren gegen `body.pub h1…h4`), und
   zwei griffen durch die Karte hindurch und machten die Einwilligungszeile
   samt Datenschutz-Verweis weiß auf weiß.
   =================================================================== */

.pub-band-volt {
    --p-strong: #FFFFFF;
    --p-body:   rgba(255, 255, 255, 0.86);
    --p-fine:   rgba(255, 255, 255, 0.72);
    --p-link:   #FFFFFF;
    --p-mark:   var(--p-deco);
    background: var(--p-volt-field);
    color: #FFFFFF;
    position: relative;
    overflow: hidden;
}

:where(.pub-band-volt) > * { position: relative; }

/* ===================================================================
   FORMULAR
   =================================================================== */

.pub-form-card {
    background: var(--p-card);
    border: 1px solid var(--p-line);
    border-radius: var(--p-r-lg);
    padding: clamp(26px, 3.4vw, 40px);
    box-shadow: 0 20px 50px rgba(16, 20, 60, 0.14);
    color: var(--p-ink);
}

.pub-field { display: flex; flex-direction: column; margin-bottom: 20px; }
:where(.pub-field) label {
    font-size: 0.94rem; font-weight: 640; color: var(--p-strong);
    margin-bottom: 8px;
}
.pub-hint {
    display: block; font-size: 0.82rem; font-weight: 400;
    color: var(--p-fine); margin-top: 3px;
}

:where(.pub-field) :is(input, textarea, select) {
    width: 100%; box-sizing: border-box;
    font-family: inherit; font-size: 1rem;
    color: var(--p-ink);
    background: var(--p-raised);
    border: 1px solid var(--p-line-2);
    border-radius: var(--p-r-sm);
    padding: 13px 15px;
    transition: border-color 150ms ease, background 150ms ease,
                box-shadow 150ms ease;
}
:where(.pub-field) textarea { min-height: 106px; resize: vertical; line-height: 1.55; }
/* Zwei Selektoren statt `:is()`: ein Pseudo-Element hinter `:is()` ist
   nicht ueberall zuverlaessig, und eine ungueltige Regel faellt hier
   still auf die Browservoreinstellung zurueck. */
:where(.pub-field) input::placeholder,
:where(.pub-field) textarea::placeholder {
    color: rgba(16, 20, 60, 0.52);
}
:where(.pub-field) :is(input, textarea, select):focus {
    outline: none; border-color: var(--p-volt);
    background: var(--p-ground);
    box-shadow: 0 0 0 3px var(--p-volt-lo);
}

/* Honigtopf: aus dem Bild geschoben, nicht display:none — ein Bot, der
   unsichtbare Felder überspringt, füllt dieses trotzdem. */
.pub-hp {
    position: absolute; left: -9999px; top: auto;
    width: 1px; height: 1px; overflow: hidden;
}

.pub-form-act {
    display: flex; flex-wrap: wrap; align-items: center; gap: 16px;
    margin-top: 26px;
}
:where(.pub-form-act) .pub-note { margin: 0; max-width: 40ch; }

.pub-msg { margin-top: 16px; font-size: 0.92rem; }
.pub-msg[data-kind="err"] { color: #C4302B; }
.pub-msg[data-kind="ok"]  { color: var(--p-pos); }

.pub-form-done { padding: 6px 0; }
:where(.pub-form-done) h3 { font-size: 1.26rem; margin-bottom: 10px; }
:where(.pub-form-done) p { color: var(--p-body); }

/* ===================================================================
   FRAGEN
   =================================================================== */

.pub-faq { margin-top: clamp(30px, 4vw, 42px); border-top: 1px solid var(--p-line); }
:where(.pub-faq) details { border-bottom: 1px solid var(--p-line); }
:where(.pub-faq) summary {
    list-style: none; cursor: pointer;
    padding: 21px 44px 21px 0;
    position: relative;
    font-size: 1.03rem; font-weight: 620; letter-spacing: -0.016em;
    color: var(--p-strong);
    transition: color 160ms ease;
}
:where(.pub-faq) summary::-webkit-details-marker { display: none; }
:where(.pub-faq) summary:hover { color: var(--p-volt); }
:where(.pub-faq) summary::after {
    content: ''; position: absolute; right: 6px; top: 50%;
    width: 11px; height: 11px;
    border-right: 2px solid var(--p-mute);
    border-bottom: 2px solid var(--p-mute);
    transform: translateY(-70%) rotate(45deg);
    transition: transform 220ms ease, border-color 220ms ease;
}
:where(.pub-faq details[open]) summary::after {
    transform: translateY(-20%) rotate(225deg);
    border-color: var(--p-volt);
}
:where(.pub-faq details) > div { padding: 0 0 22px; color: var(--p-body); max-width: 74ch; }
:where(.pub-faq details > div p) + p { margin-top: 12px; }
:where(.pub-faq) a { color: var(--p-link); text-underline-offset: 3px; }

/* ===================================================================
   UNTERSEITEN
   =================================================================== */

.pub-hero--sub {
    grid-template-columns: 1fr;
    padding-block: clamp(52px, 7vw, 88px) clamp(36px, 5vw, 58px);
}
:where(.pub-hero--sub) .pub-display { font-size: clamp(2.15rem, 5.4vw, 3.5rem); }

.pub-cta {
    display: flex; flex-wrap: wrap; align-items: center;
    justify-content: space-between; gap: 22px;
    background: var(--p-card);
    border: 1px solid var(--p-line);
    border-radius: var(--p-r-lg);
    padding: clamp(26px, 3.2vw, 38px);
    margin-top: clamp(40px, 5vw, 60px);
}
:where(.pub-sec--alt) .pub-cta { background: var(--p-ground); }
:where(.pub-cta) h3 { font-size: clamp(1.2rem, 2vw, 1.45rem); margin-bottom: 8px; }
:where(.pub-cta) p { color: var(--p-body); max-width: 52ch; }

/* Breite Tabellen scrollen in sich, nie die Seite. */
.pub-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* ===================================================================
   RECHTSTEXTE
   Nüchterner als die Verkaufsfläche: hier wird gelesen, nicht überzeugt.
   =================================================================== */

.pub-legal { padding-block: clamp(48px, 7vw, 84px); }
:where(.pub-legal) h1 { font-size: clamp(1.9rem, 4vw, 2.5rem); margin-bottom: 10px; }
:where(.pub-legal) h2 {
    font-size: 1.28rem; font-weight: 680;
    margin: 42px 0 12px; padding-top: 24px;
    border-top: 1px solid var(--p-line);
}
:where(.pub-legal) h3 { font-size: 1.05rem; margin: 26px 0 8px; }
:where(.pub-legal) :is(p, li) { color: var(--p-body); font-size: 0.97rem; line-height: 1.7; }
:where(.pub-legal) p { margin-bottom: 12px; }
:where(.pub-legal) :is(ul, ol) { padding-left: 22px; margin: 12px 0; }
:where(.pub-legal) li { margin-bottom: 7px; }
:where(.pub-legal) a { color: var(--p-link); text-underline-offset: 3px; }
:where(.pub-legal) table { width: 100%; border-collapse: collapse; margin: 18px 0; font-size: 0.92rem; }
:where(.pub-legal) :is(th, td) {
    text-align: left; padding: 11px 12px; vertical-align: top;
    border-bottom: 1px solid var(--p-line);
    color: var(--p-body);
}
:where(.pub-legal) th { color: var(--p-strong); font-weight: 640; }

.pub-legal-box {
    background: var(--p-volt-lo);
    border: 1px solid rgba(31, 48, 224, 0.20);
    border-radius: var(--p-r);
    padding: 20px 22px; margin: 22px 0;
}
:where(.pub-legal-box) p:last-child { margin-bottom: 0; }

.pub-toc {
    background: var(--p-raised);
    border: 1px solid var(--p-line);
    border-radius: var(--p-r);
    padding: 20px 24px; margin: 26px 0 34px;
}
:where(.pub-toc) :is(ol, ul) { margin: 0; padding-left: 20px; }
:where(.pub-toc) li { margin-bottom: 6px; font-size: 0.93rem; }
:where(.pub-toc) a { color: var(--p-body); }
:where(.pub-toc) a:hover { color: var(--p-volt); }

.pub-stand {
    font-size: 0.85rem; color: var(--p-fine);
    padding: 14px 0; margin-bottom: 26px;
    border-block: 1px solid var(--p-line);
}

/* ===================================================================
   FUSS
   Tiefes Marken-Navy schließt die helle Seite ab — eine Seite braucht
   unten einen Anker, sonst franst sie aus. Auch er ist ein eigener Grund,
   stellt also die fünf Rollen. Vorher trug er zwei Spaltenköpfe navy auf
   navy: `body.pub h1…h4` (0,1,2) schlug `.pub-foot h4` (0,1,1), und
   „PRODUKT" und „RECHTLICHES" standen auf jeder Seite unsichtbar über
   zwei unbeschrifteten Verweislisten.
   =================================================================== */

.pub-foot {
    --p-strong: #FFFFFF;
    --p-body:   rgba(255, 255, 255, 0.72);
    --p-fine:   rgba(255, 255, 255, 0.55);
    --p-link:   #FFFFFF;
    --p-mark:   var(--p-deco);
    background: var(--p-foot-field);
    color: rgba(255, 255, 255, 0.70);
    padding-block: clamp(48px, 6vw, 74px) 30px;
    margin-top: 0;
}
.pub-foot-in {
    display: grid;
    grid-template-columns: minmax(0, 1.6fr) repeat(2, minmax(0, 1fr));
    gap: clamp(28px, 4vw, 56px);
}
@media (max-width: 720px) {
    .pub-foot-in { grid-template-columns: 1fr 1fr; }
    :where(.pub-foot-in) > :first-child { grid-column: 1 / -1; }
}
:where(.pub-foot) .pub-mark {
    background-image: url('/static/brand/cox-header-auf-dunkel.svg');
    width: 112px; height: 30px; margin-bottom: 14px;
}
.pub-foot-note {
    color: rgba(255, 255, 255, 0.60); font-size: 0.88rem;
    margin-top: 0; max-width: 34ch; line-height: 1.55;
}
:where(.pub-foot) h4 {
    font-size: 0.76rem; font-weight: 640;
    letter-spacing: 0.14em; text-transform: uppercase;
    color: rgba(255, 255, 255, 0.62); margin-bottom: 14px;
}
:where(.pub-foot) ul { list-style: none; margin: 0; padding: 0; }
:where(.pub-foot) li { margin-bottom: 9px; }
:where(.pub-foot li) a {
    font-size: 0.92rem; color: rgba(255, 255, 255, 0.72);
    text-decoration: none; transition: color 160ms ease;
}
:where(.pub-foot li) a:hover { color: #FFFFFF; }

.pub-foot-base {
    display: flex; flex-wrap: wrap; align-items: center; gap: 12px 20px;
    margin-top: clamp(38px, 5vw, 54px);
    padding-top: 22px;
    border-top: 1px solid rgba(255, 255, 255, 0.14);
    font-size: 0.82rem; color: rgba(255, 255, 255, 0.55);
}
.pub-spacer { flex: 1 1 auto; }
.pub-lang {
    display: inline-flex; align-items: center; gap: 6px;
    border: 1px solid rgba(255, 255, 255, 0.20);
    border-radius: var(--p-r-sm);
    padding: 6px 13px; font-size: 0.8rem; font-weight: 600;
    color: rgba(255, 255, 255, 0.78); text-decoration: none;
}
.pub-lang::before { content: '⇄'; opacity: 0.7; }
.pub-lang:hover { background: rgba(255, 255, 255, 0.10); color: #FFFFFF; }
.pub-lang-note { font-size: 0.8rem; color: rgba(255, 255, 255, 0.55); }

/* ===================================================================
   DER SCHLUSS — ausgebaut in Session 46
   Der Abschnitt war „Du hebst. / Den Plan macht Cox." plus ein Fahrplan
   aus drei Punkten. Beides ist gegangen: das Wortspiel, weil die Seite
   ihre Professionalitaet aus Praezision zieht und nicht aus Rhythmik, und
   der Fahrplan, weil eine Startseite mit Changelog endet statt mit einer
   Handlung. Die Seite endet jetzt auf der Warteliste — dem einzigen, was
   ein Besucher hier tun kann. `.pub-close-*` ist mitgeloescht statt
   danebengelegt.
   =================================================================== */

/* ===================================================================
   BEWEGUNG
   EIN Auftritt, nicht zwanzig. Die Zeile im Kopf fuehrt vor, was das
   Produkt behauptet: erst rechnet es, dann erklaert es. „Der Plan
   rechnet." wird von links hereingezogen wie eine gezogene Linie, dann
   eine gehaltene Pause, dann folgt die zweite Haelfte. Die REIHENFOLGE
   ist die Aussage — deshalb ist die Pause laenger als jede andere
   Uebergangszeit auf der Seite, und deshalb steht sie im Kopf und
   nirgends sonst.

   Alles andere bewegt sich nur, wo Bewegung etwas erklaert: eine Gruppe,
   die als Gruppe erscheint; eine Antwort, die aufgeht; eine Meldung, die
   ankommt. Derselbe Auftritt auf zwanzig Abschnitten ist keine Regie,
   sondern Schuld — die Seite trug ihn vorher 28-mal.
   =================================================================== */

@keyframes pub-wipe {
    from { clip-path: inset(-0.25em 100% -0.25em 0); }
    to   { clip-path: inset(-0.25em -0.15em -0.25em 0); }
}
@keyframes pub-rise {
    from { opacity: 0; transform: translateY(14px); }
    to   { opacity: 1; transform: none; }
}
@keyframes pub-answer {
    from { opacity: 0; transform: translateY(-6px); }
    to   { opacity: 1; transform: none; }
}

/* Der eine Auftritt. Die beiden Haelften sind eigene Traeger, weil ein
   <br> sich nicht bewegen laesst. `backwards` haelt den Anfangszustand
   waehrend der Verzoegerung; ohne Animation steht die Zeile normal da,
   die Seite haengt also nie an diesem Effekt. */
:where(.pub-display) > span {
    display: block;
    animation: pub-wipe 720ms cubic-bezier(0.16, 1, 0.3, 1) backwards;
    will-change: clip-path;
}
:where(.pub-display) > span:nth-child(1) { animation-delay: 90ms; }
:where(.pub-display) > span:nth-child(2) { animation-delay: 580ms; }
/* Nach dem Auftritt nichts mehr reservieren. */
:where(.pub-display) > span { animation-fill-mode: backwards; }

/* Gruppen, die als Gruppe erscheinen — und nur die. Die Klasse steht
   deshalb nur noch auf gestaffelten Rasterkindern und dem
   Verbindungsraster; ueberall sonst wurde sie ausgebaut. */
:where(.pub-reveal-on) .pub-reveal {
    opacity: 0;
    transform: translateY(14px);
    transition: opacity 520ms cubic-bezier(0.16, 1, 0.3, 1),
                transform 520ms cubic-bezier(0.16, 1, 0.3, 1);
}
:where(.pub-reveal-on) .pub-reveal.is-in { opacity: 1; transform: none; }

/* Eine Antwort, die aufgeht: der Zustandswechsel war vorher ein Sprung. */
:where(.pub-faq details[open]) > div {
    animation: pub-answer 240ms cubic-bezier(0.16, 1, 0.3, 1);
}

/* Die eine Umwandlung der Seite. Eine Meldung, die einblendet, wird
   gelesen; eine, die aufblitzt, wird uebersehen. */
.pub-msg[data-kind] {
    animation: pub-answer 220ms cubic-bezier(0.16, 1, 0.3, 1);
}
.pub-form-done:not([hidden]) {
    animation: pub-rise 420ms cubic-bezier(0.16, 1, 0.3, 1);
}

@media (prefers-reduced-motion: reduce) {
    /* Drei Selektoren, kein `:is()`: Pseudo-Elemente duerfen darin nicht
       stehen — die ganze Regel waere ungueltig und die Abschaltung tot. */
    :where(body.pub) *,
    :where(body.pub) *::before,
    :where(body.pub) *::after {
        animation-duration: 0.001ms !important;
        animation-delay: 0.001ms !important;
        transition-duration: 0.001ms !important;
        scroll-behavior: auto !important;
    }
    /* Ohne diese beiden Zeilen bliebe der Kopf waehrend der Verzoegerung
       beschnitten und die Gruppen unsichtbar. */
    :where(.pub-display) > span { clip-path: none !important; will-change: auto; }
    :where(.pub-reveal-on) .pub-reveal { opacity: 1 !important; transform: none !important; }
}

/* ===================================================================
   DER SCHALTER, UND WAS AN IHM HÄNGT

   Die Farben selbst stehen als `light-dark()` ganz oben — hier steht nur
   noch, WELCHE der beiden gilt. `public.js` setzt `data-theme` vor dem
   ersten Paint (aus `fd_theme`, demselben Gerätecache wie die App); ohne
   Attribut entscheidet die OS-Präferenz, „system" ist also das Fehlen des
   Attributs und kein dritter Wert.

   Beide Selektoren tragen ihren Kontext in `:where()` und haben damit
   dasselbe Gewicht wie `body.pub` (0,1,1) bzw. `.pub-shot` (0,1,0) — es
   entscheidet die Reihenfolge, und die steht hier am Dateiende fest. Genau
   so soll die Hausregel wirken.
   =================================================================== */

:where(:root[data-theme="dark"])  body.pub { color-scheme: dark; }
:where(:root[data-theme="light"]) body.pub { color-scheme: light; }

/* Die dunkle Aufnahme. Das Makro liefert seit jeher BEIDE Dateien
   (`--shot` und `--shot-dark`, web/public.py::shot_files) — bis hierher
   wurden 35 dunkle Bilder mitausgeliefert und von nichts gerendert, also
   2,1 MB je Deploy für eine Fassung, die niemand sah. Eine helle Aufnahme
   auf schwarzem Grund wäre ein Loch in der Seite; jetzt ist sie es nicht
   mehr, weil die richtige Datei kommt.

   `light-dark()` hilft hier nicht: es rechnet mit FARBEN, nicht mit
   Bildquellen. Deshalb an dieser einen Stelle zwei Regeln statt einer. */
@media (prefers-color-scheme: dark) {
    :where(:root:not([data-theme="light"])) .pub-shot {
        background-image: var(--shot-dark);
    }
}
:where(:root[data-theme="dark"]) .pub-shot {
    background-image: var(--shot-dark);
}

/* Der Knopf in der Leiste. Er sagt, wohin er führt, nicht wo er steht —
   in HELL steht „Dunkel", weil das die Tat ist, die er auslöst. Das Symbol
   ist derselbe Halbmond wie in der App. */
.pub-theme {
    display: inline-flex; align-items: center; gap: 7px;
    background: none; border: 1px solid var(--p-line);
    border-radius: 100px; padding: 7px 13px;
    font: inherit; font-size: 0.85rem; font-weight: 560;
    color: var(--p-body); cursor: pointer;
    transition: border-color 180ms ease, color 180ms ease;
}
.pub-theme:hover { color: var(--p-strong); border-color: var(--p-line-2); }
.pub-theme:focus-visible { outline: 2px solid var(--p-volt); outline-offset: 2px; }
:where(.pub-theme) svg { width: 15px; height: 15px; flex: none; }

/* Unter 560 px bleibt nur das Zeichen: die Leiste trägt dort schon Marke,
   Anmelden und den Aufruf, und ein viertes Wort bricht sie um. */
@media (max-width: 560px) {
    :where(.pub-theme) span { display: none; }
    .pub-theme { padding: 7px 9px; }
}

/* Die Wortmarke in der Leiste. Der Fuss tauscht sie seit jeher gegen die
   `auf-dunkel`-Fassung, weil er auf dunklem Grund steht — in DUNKEL gilt
   dasselbe fuer die Leiste, sonst steht die Marke dunkel auf dunkel und
   ist schlicht weg. Gesehen im ersten dunklen Bild, nicht im Code.

   Die Marke ist eine Bilddatei, keine Farbe: `light-dark()` rechnet mit
   Farben, also hier zwei Regeln statt einer. Die Ausnahme des Fusses steht
   spaeter in der Datei und traegt dasselbe Gewicht — sie gewinnt also
   weiter, und das ist richtig: der Fuss ist in BEIDEN Farbwelten dunkel. */
@media (prefers-color-scheme: dark) {
    :where(:root:not([data-theme="light"])) .pub-mark {
        background-image: url('/static/brand/cox-header-auf-dunkel.svg');
    }
}
:where(:root[data-theme="dark"]) .pub-mark {
    background-image: url('/static/brand/cox-header-auf-dunkel.svg');
}
