/* ==========================================================================
   Paul Rothe — Portfolio
   Frontend-Stylesheet (Desktop + Mobile)

   Aufbau der Datei:
     01  Schriftarten (@font-face)
     02  Design-Tokens: welche Variablen der Renderer setzt
     03  Reset und Grundlagen
     04  Typografie
     05  Links: Pillen-Hover und aktiver Zustand
     06  Layout: Sidebar (Desktop)
     07  Layout: Content und weisser Kasten (.page)
     08  Bloecke (block-*)
     09  Sketch-Hintergrund
     10  Entwurfs-Banner
     11  Lightbox (optional, von site.js genutzt)
     12  Mobile (max-width: 768px)
     13  Nur Desktop (alles ausser max-width: 768px)
     14  Bewegung reduzieren
     15  Druck

   Werte anpassen: am besten ueber das CMS (Einstellungen) — der Server
   schreibt daraus die Variablen in :root. Wer direkt im Code aendern will,
   findet unter 02 die Standardwerte, die hier als Fallback hinterlegt sind.
   ========================================================================== */


/* ==========================================================================
   01  Schriftarten
   Lokale Dateien in /assets/fonts/. woff2 zuerst (klein), woff als Fallback
   fuer aeltere Browser. font-display: swap zeigt sofort die Systemschrift,
   damit nie ein leerer Text steht.
   ========================================================================== */

@font-face {
  font-family: "GT Walsheim";
  src: url("/assets/fonts/GT-Walsheim-Light.woff2") format("woff2"),
       url("/assets/fonts/GT-Walsheim-Light.woff") format("woff");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "GT Walsheim";
  src: url("/assets/fonts/GT-Walsheim-Bold.woff2") format("woff2"),
       url("/assets/fonts/GT-Walsheim-Bold.woff") format("woff");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}


/* ==========================================================================
   02  Design-Tokens

   Diese Variablen setzt der Renderer (lib/render.js) aus content/site.json
   in einem <style>-Block im :root. Sie werden hier bewusst NICHT definiert,
   damit die Reihenfolge der Stylesheets egal ist. Jede Verwendung hat einen
   Fallback, dadurch sieht die Seite auch ohne Renderer korrekt aus.

     --body-bg       Hintergrund der ganzen Seite      #efefef
     --page-bg       Hintergrund des Inhaltsbereichs   transparent
     --text          Textfarbe                         rgba(0,0,0,0.9)
     --sidebar-w     Breite der Sidebar (mit Einheit)  26%
     --content-w     Breite des Contents (mit Einheit) 81%
     --nav-fs        Schriftgroesse Navigation         1.9rem
     --body-fs       Schriftgroesse Fliesstext         1.6rem
     --h1-fs         Schriftgroesse h1                 3.6rem
     --h2-fs         Schriftgroesse h2                 2.8rem
     --radius        Radius fuer Bilder/Videos         12px
     --menu-overlay  Hintergrund des Mobil-Menues      rgba(20,20,20,0.95)

   Die folgenden Variablen gehoeren zum Stylesheet selbst und duerfen hier
   frei geaendert werden.
   ========================================================================== */

:root {
  /* Abstand, wo Bild an Bild grenzt — und zugleich der Abstand des
     Inhalts zum Fensterrand. Beides derselbe Wert, damit die Seite
     ringsum gleich viel Luft hat wie zwischen den Bildern. */
  --media-gap: 0.5rem;
  --page-pad: var(--media-gap); /* Rand des Inhaltsbereichs */
  --block-gap: 2rem;         /* vertikaler Abstand zwischen zwei Bloecken */
  --rule: rgba(0, 0, 0, 0.9);/* Linienfarbe in der Navigation */
  --rule-soft: rgba(0, 0, 0, 0.15); /* Linienfarbe im Inhalt */
  --caption: rgba(0, 0, 0, 0.4);    /* Bildunterschriften */
  --small: rgba(0, 0, 0, 0.6);      /* kleine Texte */
  --menu-text: var(--text, rgba(0, 0, 0, 0.9)); /* Links im Mobil-Menue */
  --tap: 44px;               /* Mindesthoehe fuer Touch-Ziele */

  /* Hoehe, die der schwebende Umschalter Galerie/Grid oben freihaelt.
     Steckt in einer Variablen, weil zwei Stellen sie brauchen: der obere
     Innenabstand der Seite und die Rechnung fuer die Mockup-Groesse.

     Gerechnet statt fest, damit der Platz mitwaechst, wenn im CMS die
     Schriftgroesse der Navigation geaendert wird: eine Zeile Navigationsschrift
     plus Seitenrand, Innenabstand des Knopfes und etwas Luft. Mit festen
     4.4rem lag der Knopf gemessen 2 px auf dem ersten Plakat. */
  --switch-h: calc(var(--nav-fs, 1.9rem) * 1.2 + 3.2rem);

  /* --- Navigationsspalte und linke Kante des Inhalts ---------------------

     Beim Verkleinern des Fensters wird die Navigation NICHT mitskaliert.
     Ihre Schrift steht in rem und bleibt deshalb immer gleich gross; schmaler
     wird allein der Inhalt rechts daneben.

     --nav-w ist die Breite, die der laengste Eintrag der Navigation braucht.
     Der Wert hier ist aus der Schriftgroesse gerechnet und damit nur eine gute
     Schaetzung (rund elf Zeichenbreiten plus die beiden Innenabstaende der
     Pille). public/js/site.js misst nach dem Laden der Schrift die echte
     Breite und schreibt sie in dieselbe Variable. Ohne JavaScript bleibt es
     bei der Schaetzung — die Seite bricht dadurch nicht, sie hat links nur
     ein paar Pixel mehr oder weniger Luft.

     Wird ein Projektname sehr lang, waechst die Spalte mit und der Inhalt
     rueckt nach rechts. Das ist Absicht: lieber ein schmalerer Inhalt als
     eine Navigation, die im Text liegt. */
  --nav-w: calc(var(--nav-fs, 1.9rem) * 11 + var(--pill-x, 1.2rem) * 2);

  /* Luft zwischen dem Navigationstext und dem Inhalt. */
  --nav-gap: 2rem;

  /* --nav-card-w gehoert der Vollstaendigkeit halber hierher, steht aber
     nicht hier: public/js/site.js setzt sie an der Spalte selbst. Es ist die
     Breite der beiden FLAECHEN in der Navigation — der Cards hinter den
     Projektnamen und des Vorschaurahmens unter "Contact" (Abschnitt 06). Sie
     endet 12 px vor dem Inhalt, und dafuer muss man --content-w in eine
     Fensterbreite umrechnen koennen; in CSS geht das nicht. Fehlt die
     Variable, gilt --nav-w. */

  /* Linke Kante des Inhalts. Im Normalfall der Wert aus dem CMS (100 % minus
     --content-w, ab Werk also 19 %). Wird das Fenster so schmal, dass der
     Inhalt unter die Navigation rutschen wuerde, haelt max() ihn an der
     Navigation fest — von da an schrumpft der Inhalt nur noch von rechts.
     Genau das ist der Unterschied zu vorher: bis 1440 px sieht die Seite aus
     wie bisher, darunter bleibt die Navigation stehen statt in den Text zu
     laufen. */
  --content-x: max(
    calc(100% - var(--content-w, 81%)),
    calc(var(--nav-w) + var(--nav-gap))
  );
}


/* ==========================================================================
   03  Reset und Grundlagen
   ========================================================================== */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  /* Cargo rechnet mit 1rem = 10px. Statt fester 10px nehmen wir 62.5 % der
     Browser-Grundschrift: das ergibt bei Standardeinstellung ebenfalls 10px,
     respektiert aber eine groessere eingestellte Schrift. clamp() setzt eine
     Untergrenze (kleine Geraete bleiben lesbar) und eine Obergrenze. */
  font-size: 62.5%;
  font-size: clamp(9.5px, 62.5%, 13px);

  /* Nur die automatische Textvergroesserung von iOS im Querformat abschalten.
     Wichtig: 100% (nicht "none") — sonst waere das Zoomen gesperrt. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;

  scroll-behavior: smooth;
}

body {
  margin: 0;
  min-height: 100%;
  background: var(--body-bg, #efefef);
  color: var(--text, rgba(0, 0, 0, 0.9));
  font-family: "GT Walsheim", -apple-system, BlinkMacSystemFont, "Segoe UI",
    Roboto, sans-serif;
  font-weight: 400;
  font-size: var(--body-fs, 1.6rem);
  line-height: 1.2;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;

}

[hidden] {
  display: none !important;
}

img,
video,
canvas,
iframe {
  max-width: 100%;
}

figure {
  margin: 0;
}

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

/* Sichtbarer Fokusrahmen fuer Tastaturbedienung */
a:focus-visible,
button:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 3px;
}


/* ==========================================================================
   04  Typografie
   ========================================================================== */

h1,
h2,
h3,
p,
ul,
ol {
  margin: 0;
}

h1 {
  font-size: var(--h1-fs, 6.8rem);
  line-height: 1.3;
  font-weight: 400;
}

/* Wie in der Navigation (Abschnitt 06) gliedert allein die GROESSE, nicht
   das Gewicht: h1 und h2 stehen beide in GT Walsheim Light (400), h2 nur
   eine Stufe kleiner. Fett waren die Ueberschriften bis zuletzt — neben den
   schlanken Projektnamen der Spalte war das eine Stimme zu viel.
   Ausgenommen bleibt bewusst die Ueberschrift im Impressum-Overlay
   (Abschnitt 09): die ist so gross wie der Fliesstext, das Gewicht ist dort
   ihr einziges Unterscheidungsmerkmal. */
h2 {
  font-size: var(--h2-fs, 4.2rem);
  line-height: 1.2;
  font-weight: 400;
}

/* Lange Woerter und Webadressen brechen um, statt die ganze Seite seitwaerts
   zu schieben. Gemessen: eine Adresse in einer Stichpunktliste machte das
   Dokument bei 1100 px Fensterbreite 160 px breiter als das Fenster — man
   konnte quer scrollen, obwohl nichts zu sehen war.

   "anywhere" statt "break-word": nur so zaehlt der Umbruch auch bei der
   Berechnung der Spaltenbreite mit. Gebrochen wird trotzdem erst, wenn es
   ohne nicht mehr geht — normale Woerter bleiben unangetastet. */
h1,
h2,
h3,
p,
li,
figcaption,
.next-link {
  overflow-wrap: anywhere;
}

strong,
b {
  font-weight: 700;
}

small {
  font-size: 1rem;
  color: var(--small);
}

figcaption {
  /* unterste Stufe der Skala: 1.6rem / 1.618 = 1.0rem */
  font-size: 1rem;
  line-height: 1.3;
  color: var(--caption);
  margin-top: 0.6rem;
}

/* Im Original ist die Trennlinie UNSICHTBAR — sie dient nur als Abstand
   (Cargo: background rgba(0,0,0,0), border 0, height 2px). Das ist kein
   Versehen, sondern die Optik der Seite: die Navigationsgruppen stehen
   durch Luft getrennt, nicht durch Striche.

   SICHTBARE LINIEN EINSCHALTEN: hier in der Basisregel
       border-top: 1px solid var(--rule);
       height: 0;
   ergaenzen — damit werden ALLE Trenner zu Linien (Navigation, Mobil-Menue,
   .block-divider, .block-next). Soll eine Stelle eine andere Farbe haben,
   kommt dort eine eigene border-top-Kurzform dazu (z. B.
   "hr.block-divider { border-top: 1px solid var(--rule-soft) }").
   Ein blosses border-top-color genuegt NICHT, solange hier border: 0 steht —
   ohne Linienstil und -breite zeichnet der Browser nichts. */
hr {
  background: transparent;
  border: 0;
  height: 2px;
  display: block;
  margin: 1rem 0;
}


/* ==========================================================================
   05  Links: Pillen-Hover und aktiver Zustand

   Der charakteristische Effekt der Seite: beim Hover legt sich eine Pille
   (runder Rahmen) um den Link. Damit dabei nichts springt, sind Rahmen und
   Innenabstand schon im Ruhezustand vorhanden — der Rahmen nur transparent.

   mix-blend-mode: difference ist Teil des Originallooks. Damit Text auf
   weissem Grund nicht unsichtbar wird, kapseln wir die Blend-Gruppe mit
   isolation: isolate auf den Containern (siehe unten). Der Effekt greift
   dadurch nur innerhalb eines Blocks, z. B. ueber einem Bild.
   ========================================================================== */

.sidebar-inner,
.mobile-menu-inner,
.block {
  isolation: isolate;
}

/* Innenabstand der Pille. Bewusst in `em`, also an der Schriftgroesse des
   Links ausgerichtet — NICHT in Prozent.
   Prozentwerte beziehen sich bei Inline-Elementen auf die Breite des
   umgebenden Blocks: in der 361 px breiten Sidebar ergaben 4 % rund 15 px,
   im 1166 px breiten Inhaltsbereich aber 47 px. Die Pille passte dadurch
   nirgends zum Text. */
:root {
  /* Abstand Text -> Pillenrand, links wie rechts.
     Bewusst ein FESTER Wert (rem), nicht em: sonst waechst der Abstand mit
     der Schriftgroesse mit, und die groesseren Ueberschriften ("Paul Rothe",
     "Contact") staenden weiter rechts als die Projektliste. Das Original
     rechnete mit Prozent der Containerbreite — ebenfalls gleich fuer alle
     Links, nur eben viel zu breit. */
  --pill-x: 1.2rem;
}

.page a,
.sidebar a,
.mobile-menu a {
  color: inherit;
  text-decoration: none;
  border: 1px solid transparent;   /* Platzhalter, verhindert Layout-Sprung */
  border-radius: 100px;
  padding: 2px var(--pill-x);
  transition: border-color 0.25s ease, background-color 0.25s ease,
              color 0.25s ease;
  mix-blend-mode: difference;
}

/* Die Pille braucht links Platz, sonst wird ihr Rahmen am Fensterrand
   abgeschnitten (vorher begann sie bei x = -9 px). Den Platz liefert jetzt
   der Aussenrand der Sidebar (--page-pad), deshalb steht hier kein
   zusaetzlicher Einzug mehr — so fluchtet die Pille mit dem Inhalt.
   Im Mobil-Menue bleibt der Einzug, dort gibt es keinen Aussenrand. */
.mobile-menu-inner {
  padding-left: var(--pill-x);
}

/* Im Fliesstext darf die Pille keinen Platz wegnehmen. Innenabstand und
   Rahmen bleiben (die Pille sieht also aus wie ueberall), ein gleich grosser
   negativer Aussenabstand zieht sie aber wieder heraus. Unterm Strich
   braucht der Link damit genau die Breite seines Textes:

     -13px Aussen + 1px Rahmen + 12px Innen = 0

   Folge: die erste Zeile beginnt exakt unter der Ueberschrift, und mitten im
   Satz entstehen keine Luecken mehr vor und hinter einem Link. Die Pille
   selbst ragt jetzt links und rechts ueber den Text hinaus — im Inhalt ist
   dafuer Platz (--page-pad), am Fensterrand waere sie beschnitten. Darum
   gilt die Regel nur hier und nicht fuer die Navigation.

   In der Navigation bleibt alles beim Alten: dort ist jede Zeile ein Link,
   also sind auch alle gleich weit eingerueckt. */
.page .block-text a,
.page .block-timeline a {
  margin-inline: calc((var(--pill-x) + 1px) * -1);
}

/* Hover-Pille nur auf Geraeten mit echtem Zeiger — auf Touch bliebe der
   Zustand sonst nach dem Antippen "kleben". */
@media (hover: hover) {
  .page a:hover,
  .sidebar a:hover,
  .mobile-menu a:hover {
    border-color: currentColor;
  }
}

/* In der Navigation ist die Pille keine Pille mehr, sondern hat dieselbe
   Ecke wie die Cards und wie jedes Bild der Seite (--radius aus dem CMS).
   Sonst staende neben einer Card mit 12 px Radius ein Knopf mit 100 px —
   zwei Formsprachen in derselben Spalte. Im Fliesstext (.page a) bleibt die
   runde Pille: dort gibt es keine Flaechen, zu denen sie passen muesste. */
.sidebar a,
.mobile-menu a {
  border-radius: var(--radius, 12px);
}

/* Beim Hover rueckt der Eintrag ein Stueck nach rechts, die Pille wandert
   mit. Nur in der Navigation: Links im Fliesstext bleiben inline, sonst
   koennten sie nicht mehr ueber mehrere Zeilen umbrechen.

   `display: inline-block` ist dabei Pflicht — `transform` wirkt auf rein
   inline dargestellte Elemente nicht, die Verschiebung bliebe sonst
   wirkungslos. */
.sidebar a,
.mobile-menu a {
  display: inline-block;
  transition: border-color 0.25s ease, background-color 0.25s ease,
              color 0.25s ease, transform 0.25s ease;
}

@media (hover: hover) {
  .sidebar a:hover,
  .mobile-menu a:hover {
    transform: translateX(0.45rem);
  }
}

/* Wer im System "Bewegung reduzieren" eingestellt hat, bekommt nur die
   Pille — ohne Ruck nach rechts. */
@media (prefers-reduced-motion: reduce) {
  .sidebar a,
  .mobile-menu a {
    transition: border-color 0.25s ease, background-color 0.25s ease,
                color 0.25s ease;
  }
  .sidebar a:hover,
  .mobile-menu a:hover {
    transform: none;
  }
}

/* Aktiver Navigationspunkt: schwarze Pille, weisse Schrift.
   Der Kontakt-Eintrag ist im Markup eine Ueberschrift
   (<h2 class="nav-contact"><a class="active">) und traegt darum KEIN
   .nav-link — er braucht den zweiten Selektor, sonst bliebe der aktive
   Zustand auf /information unsichtbar. */
.nav-link.active,
.nav-contact a.active {
  background: #000;
  color: #fff;
  border-color: #000;
  border-radius: var(--radius, 12px);
  padding: 2px var(--pill-x);
  mix-blend-mode: normal;
}

@media (hover: hover) {
  .nav-link.active:hover,
  .nav-contact a.active:hover {
    border-color: #000;
  }
}

/* Startseite: hinter der Navigation laeuft der Sketch. Difference-Blending
   allein reicht dort nicht — ueber einem bewegten Bild kippt die Schrift
   staendig und wird stellenweise unlesbar. Darum liegt auf der Startseite
   hinter jedem Eintrag eine deckende Pille in der Hintergrundfarbe.

   mix-blend-mode muss dafuer weg: sonst wuerde die Pille selbst mit dem
   Sketch verrechnet, statt ihn zu verdecken. Der aktive Eintrag bringt
   seine eigene schwarze Pille mit und bleibt darum aussen vor. */
body.tpl-home .sidebar a:not(.active) {
  background: var(--body-bg, #efefef);
  mix-blend-mode: normal;
}

/* Pfeil hinter dem Label. Das geschuetzte Leerzeichen davor sorgt zusammen
   mit white-space: nowrap dafuer, dass Label und Pfeil nie umbrechen. */
.arrow {
  font-size: 0.8em;
  white-space: nowrap;
}

.arrow::before {
  content: "\00a0";
}


/* ==========================================================================
   06  Layout: Sidebar (Desktop)

   Sidebar und Content ueberlappen bewusst (26 % + 81 % > 100 %), genau wie
   im Original. Die Sidebar liegt darueber (z-index 10) und hat keinen
   eigenen Hintergrund.
   ========================================================================== */

.sidebar {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 10;
  /* Der CMS-Wert gilt, solange das Fenster breit genug ist. Schmaler als der
     laengste Eintrag wird die Spalte nie — sonst braechen die Projektnamen
     auf zwei Zeilen um (gemessen ab etwa 950 px Fensterbreite). */
  width: max(var(--nav-w), var(--sidebar-w, 26%));
  max-height: 100vh;
  max-height: 100dvh;
  /* Gleicher Rand wie der Inhalt: so liegt die Pille von "Paul Rothe"
     buendig mit der Oberkante des ersten Bildes. Vorher sass der Titel
     22 px hoeher als das Bild daneben. */
  padding: var(--page-pad);
  font-size: var(--nav-fs, 2.6rem);
  line-height: 1.2;

  /* Ist die Navigation hoeher als der Bildschirm, scrollt sie fuer sich —
     ohne dass die Seite dahinter mitscrollt. */
  overflow-y: auto;
  overscroll-behavior: contain;

  /* Der Balken bleibt unsichtbar. Die Sidebar ist 26 % breit, der Inhalt
     beginnt aber schon bei 19 % — ihr Scrollbalken saesse also mitten auf dem
     ersten Bild, als graue Linie quer ueber die Seite. Bedienen liesse er
     sich dort ohnehin nicht, weil die Flaeche pointer-events: none hat.
     Gescrollt wird die Navigation weiterhin ganz normal mit dem Rad. */
  scrollbar-width: none;
  -ms-overflow-style: none;

  /* Klicks gehen durch die leere Flaeche hindurch (wichtig fuer den Sketch
     auf der Startseite); nur der Inhalt selbst faengt sie ab. */
  pointer-events: none;
}

.sidebar::-webkit-scrollbar {
  width: 0;
  height: 0;
}

/* Spalte ueber die volle Hoehe: dadurch lassen sich die Social-Links unten
   absetzen, so wie im Original. Dort entsteht der Abstand durch eine lange
   Reihe von <br> — das haelt aber keiner Fenstergroesse stand. */
.sidebar-inner {
  display: flex;
  flex-direction: column;
  min-height: calc(100vh - 1rem);
}

/* Durch min-height und display:flex ist die Spalte eine voll deckende
   Flaeche ueber 26 % x 100 % des Fensters. Wuerde sie Klicks abfangen, waere
   dort weder der Sketch bedienbar noch ein Bild greifbar. Darum faengt nur
   das, was man wirklich bedient: die Links. */
.sidebar-inner,
.sidebar-inner * {
  pointer-events: none;
}

.sidebar a {
  pointer-events: auto;
}

/* Kopfzeile der Spalte: links der Name, rechts der Knopf zur Uebersicht
   (die Seite mit allen Projekten, lib/render.js renderOverviewLink).

   Die Zeile ist genau so breit wie die Cards darunter — deshalb steht der
   Knopf in einer Flucht mit den Pfeilen der Projektliste und mit der rechten
   Kante des Vorschaurahmens. Breiter darf sie nicht werden: die Spalte
   ueberlappt den Inhalt absichtlich (Abschnitt 06), und was ueber diese
   Kante hinausragt, laege auf dem ersten Bild der Seite.

   Ist das Fenster so schmal, dass Name und Knopf nicht nebeneinander passen,
   rutscht der Knopf in die naechste Zeile — und bleibt dort rechtsbuendig
   (margin-left: auto). Er wandert also nie in den Inhalt hinein. */
.site-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;

  /* Der Knopf steht rechtsbuendig (margin: 0 0 0 auto, siehe unten) — die
     freie Strecke zwischen Name und Knopf macht also der Aussenrand, nicht
     dieser Zwischenraum. Zu sehen waere er nur, wenn die Zeile randvoll ist;
     dann aber verbreitert er sie bloss und schiebt damit den ganzen Inhalt
     der Seite nach rechts. Darum null. */
  column-gap: 0;
  row-gap: 0.4rem;
  max-width: var(--nav-card-w, var(--nav-w));
}

/* Der Knopf: kleiner als die Projektliste, damit er neben dem grossen Namen
   ein Knopf bleibt und keine zweite Ueberschrift wird. */
.site-index {
  margin: 0 0 0 auto;
  font-size: calc(var(--nav-fs, 2.6rem) * 0.8);
  line-height: 1.2;
}

/* Er traegt dieselbe Flaeche wie die Cards der Projektliste — nur eben um
   seinen Text herum statt ueber die ganze Spalte. So sieht man ihm an, dass
   er ein Knopf ist und kein Wort neben dem Namen. Beim Zeigen kommt die
   Outline dazu (Abschnitt 05), auf der Uebersichtsseite selbst ist er
   schwarz gefuellt (.active, ebenfalls Abschnitt 05). */
/* Der lange Selektor ist Absicht: auf der Startseite legt Abschnitt 05 hinter
   JEDEN Eintrag der Spalte eine deckende Flaeche in der Hintergrundfarbe,
   damit die Schrift ueber dem bewegten Sketch lesbar bleibt. Diese Regel
   zaehlt schwerer und wuerde die graue Flaeche des Knopfes ueberschreiben —
   er saehe dort aus wie ein Wort neben dem Namen. Die Cards der Projektliste
   loesen dasselbe Problem auf demselben Weg. */
.sidebar .site-head .site-index .index-link:not(.active) {
  background: #e8e8e8;
  background: color-mix(in srgb, var(--body-bg, #efefef) 97%, #000);
  mix-blend-mode: normal;
}

/* "Paul Rothe" und "Contact" gliedern die Navigation: deutlich groesser als
   die Projektliste, aber im selben Schnitt — GT Walsheim Light (400). Fett
   waren sie bis zuletzt; neben den Cards war das eine Stimme zu viel.
   (Die beiden Schnitte stehen in Abschnitt 01: 400 = Light, 700 = Bold.) */
.site-title,
.nav-contact {
  font-size: var(--h2-fs, 4.2rem);
  font-weight: 400;
  line-height: 1.2;
  margin: 0;
}

/* Eine Flucht fuer die ganze Spalte: "Paul Rothe", die Projektnamen,
   "Contact" und die Social-Links fangen alle gleich weit vom Rand an.

   Der Einzug ist derselbe, den die Cards und "Contact" tragen (siehe dort):
   die Breite der unsichtbaren Pille. Ohne ihn begaenne der Name eine
   Pillenbreite weiter links als alles dazwischen, weil nur die Flaechen
   eingerueckt sind. Seine Pille beim Zeigen deckt sich jetzt mit der linken
   Kante der Cards.

   Die Social-Links tragen denselben Einzug, aber eine Ebene hoeher: er sitzt
   auf ihrer Liste (siehe unten), nicht auf den einzelnen Links. Seit die
   beiden nebeneinander stehen, bekaeme ihn sonst auch der rechte — und der
   saesse dann eine Pillenbreite vor der Kante statt an ihr. */
.sidebar .site-title a {
  margin-left: calc(var(--pill-x) + 1px);
}

.nav-group,
.nav-social {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* Schiebt die Social-Links an den unteren Rand der Sidebar — steht bewusst
   NACH dem margin-Reset darueber, sonst wuerde der ihn ueberschreiben. */
.nav-social {
  margin-top: auto;
  padding-top: 2rem;
}

/* LinkedIn und Instagram stehen in EINER Zeile, einer an jeder Kante der
   Spalte — nicht mehr untereinander wie eine dritte Liste. Bei genau zwei
   Eintraegen heisst space-between: der erste ganz links, der zweite ganz
   rechts, dazwischen nur Luft.

   Die beiden Masse sind woertlich die der Cards (Abschnitt 06): der Einzug
   setzt die linke Kante, die Obergrenze die rechte. Dadurch beginnt
   "LinkedIn" genau unter den Projektnamen und "Instagram" endet genau unter
   deren Pfeilen — den Innenabstand ihrer Pille bringen beide Links selbst
   mit, die Liste muss ihn nicht noch einmal einrechnen. */
.sidebar .nav-social {
  display: flex;
  justify-content: space-between;
  margin-left: calc(var(--pill-x) + 1px);
  max-width: calc(var(--nav-card-w, var(--nav-w)) - var(--pill-x) - 1px);
}

.nav-group li,
.nav-social li {
  margin: 0;
}


/* --- Cards hinter den Projektnamen ---------------------------------------

   Jeder Eintrag der Projektliste liegt auf einer eigenen Flaeche: so breit
   wie das Vorschaubild unter "Contact", mit dem Eckenradius der Bilder und
   einem Grau, das sich nur eben vom Hintergrund abhebt. Beim Zeigen bleibt
   es bei der Outline aus Abschnitt 05.

   Das Grau bekommt nur die Projektliste: "Paul Rothe" und "Contact" sind
   Ueberschriften, die Social-Links stehen in einer eigenen Liste — sie
   bleiben schlichter Text auf dem Hintergrund. "Contact" uebernimmt aber
   weiter unten die KANTEN der Cards, damit sein Knopf sich genauso anfassen
   laesst wie die Eintraege darueber. Der Knopf zur Uebersicht (.index-link)
   steht ausserhalb einer .nav-group und behaelt darum seine Pille.
   ------------------------------------------------------------------------ */

/* display: block ist der Kern: als inline-block waere der Link nur so breit
   wie sein Text (Abschnitt 05), die Card soll aber die ganze Spalte
   einnehmen.

   Die Obergrenze --nav-card-w setzt public/js/site.js — dieselbe Kante, an
   der auch das Vorschaubild endet: die Spalte ueberlappt den Inhaltsbereich
   absichtlich (Abschnitt 06), und eine Flaeche darf da nicht hineinragen.
   In reinem CSS geht das nicht, siehe dort.

   Der Rueckfallwert --nav-w ist die Schaetzung aus Abschnitt 02: die Breite,
   die die Spalte braucht. Sie gilt vor dem ersten Rechnen und wenn gar kein
   JavaScript laeuft — beides ohne Flackern und ohne dass eine Card je auf dem
   Inhalt laege, denn der haelt immer --nav-w + --nav-gap Abstand. */
.sidebar .nav-group .nav-link {
  /* Flex statt block: der Pfeil sitzt dadurch am rechten Ende der Flaeche
     und nicht direkt hinter dem Namen. Die Namen sind verschieden lang — so
     stehen die Pfeile trotzdem alle auf einer Linie, buendig mit der rechten
     Kante der Cards.

     align-items: baseline haelt den kleineren Pfeil (0.8em, Abschnitt 05) auf
     derselben Schriftlinie wie den Namen; mit "center" saesse er einen Hauch
     zu hoch. Der gap greift nur, wenn ein Name die Flaeche einmal ganz
     ausfuellt — dann stossen Name und Pfeil nicht aneinander. */
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--pill-x);

  /* Die Flaeche beginnt dort, wo das "P" von "Paul Rothe" beginnt, nicht am
     Spaltenrand: die Ueberschriften tragen ihre (unsichtbare) Pille, und
     deren Innenabstand ist genau dieses Mass. Ohne den Einzug stuenden die
     Cards um eine Pillenbreite weiter links als der Name darueber.

     Weiter links kann stattdessen der NAME nicht: seine Pille beim Zeigen
     braeuchte den Platz und wuerde am Fensterrand abgeschnitten.

     Die max-width wird um denselben Wert kleiner, damit die rechte Kante
     bleibt, wo sie ist — sie ist die Grenze zum Inhalt (site.js, 1c). */
  margin-left: calc(var(--pill-x) + 1px);
  max-width: calc(var(--nav-card-w, var(--nav-w)) - var(--pill-x) - 1px);
  border-radius: var(--radius, 12px);

  /* Eine deckende Flaeche soll verdecken, nicht mit dem Untergrund
     verrechnet werden (mix-blend-mode: difference, Abschnitt 05). */
  mix-blend-mode: normal;
}

/* Das Grau: ein Hauch dunkler als der Hintergrund, mehr nicht. Aus der
   Hintergrundfarbe gerechnet, damit es jeder Farbe aus dem CMS folgt — die
   Zeile davor ist der feste Wert fuer Browser ohne color-mix.

   Der aktive Eintrag bleibt aussen vor: er bringt seine schwarze Fuellung
   selbst mit (Abschnitt 05) und traegt hier nur die neue Form. */
.sidebar .nav-group .nav-link:not(.active) {
  background: #e8e8e8;
  background: color-mix(in srgb, var(--body-bg, #efefef) 97%, #000);
}

/* Das geschuetzte Leerzeichen vor dem Pfeil (Abschnitt 05) haelt Label und
   Pfeil zusammen, solange beide nebeneinander stehen. Auf der Card trennt sie
   das Flex-Raster — dort waere es nur eine zusaetzliche Luecke vor dem Pfeil
   und der Pfeil saesse nicht mehr genau in der Flucht der rechten Kante. */
.sidebar .nav-group .nav-link > .arrow::before {
  content: none;
}

/* Beim Zeigen dreht sich der Pfeil aus der Schraegen in die Waagerechte: aus
   ↘ wird →, er zeigt also geradeaus auf das Projekt, das der Klick oeffnet.
   45 Grad gegen den Uhrzeigersinn, denn ↘ zeigt nach Suedosten.

   Der Pfeil ist im Flex-Raster der Card ein eigener Kasten und laesst sich
   deshalb direkt drehen — eine reine Inline-Schrift koennte man nicht
   drehen (siehe die Anmerkung zu inline-block in Abschnitt 05). */
.sidebar .nav-group .nav-link > .arrow {
  transition: transform 0.25s ease;
}

/* Die Seite, auf der man steht, haelt den Pfeil waagerecht — dauerhaft, ohne
   Mauszeiger. Der eingerastete Eintrag sieht dadurch aus wie der angefahrene:
   der Pfeil zeigt auf den Inhalt daneben, nicht mehr ins Leere. Steht hier
   VOR der Hover-Regel, damit beide dasselbe sagen und keine die andere
   aufhebt, wenn man den aktiven Eintrag anfaehrt. */
.sidebar .nav-group .nav-link.active > .arrow {
  transform: rotate(-45deg);
}

@media (hover: hover) {
  .sidebar .nav-group .nav-link:hover > .arrow {
    transform: rotate(-45deg);
  }
}

/* Wer "Bewegung reduzieren" eingestellt hat, sieht die Drehung nicht als
   Bewegung: Abschnitt 16 nimmt jeden Uebergang weg, der Pfeil steht dann
   sofort waagerecht. Die Richtung bleibt als Hinweis erhalten, nur der Weg
   dorthin faellt weg — hier steht deshalb keine eigene Regel dafuer. */

/* Luft zwischen den Cards: ohne sie stossen die Flaechen aneinander und die
   runden Ecken verschwinden in einer durchgehenden grauen Spalte. */
.sidebar .nav-group li + li {
  margin-top: 0.2rem;
}

/* Der Ruck nach rechts beim Zeigen (Abschnitt 05) passt zu einer Zeile Text,
   nicht zu einer Flaeche: die Card wuerde ihren Abstand zum Inhalt
   aufbrauchen und links aus der Flucht der Ueberschriften laufen. Es bleibt
   bei der Outline. */
@media (hover: hover) {
  .sidebar .nav-group .nav-link:hover {
    transform: none;
  }
}

/* "Contact" ist im Markup eine Ueberschrift, aber eben auch ein Link — und
   er soll sich anfassen lassen wie die Eintraege darueber. Bisher war die
   Klickflaeche nur so breit wie das Wort: rechts daneben war die halbe
   Spalte tot, obwohl dort optisch noch der Knopf zu liegen schien.

   Die drei Masse sind woertlich die der Cards (siehe oben): der Einzug
   setzt die linke Kante, die Obergrenze die rechte, die Ecke ist die der
   Bilder. Damit deckt sich die Flaeche unter "Contact" genau mit den
   Projektnamen und mit dem Vorschaurahmen darunter.

   display: block ist dabei der Kern — als inline-block (Abschnitt 05)
   waere der Link nur so breit wie sein Text. Der Pfeil fehlt hier, darum
   genuegt block; die Cards brauchen dafuer Flex. */
.sidebar .nav-contact a {
  display: block;
  margin-left: calc(var(--pill-x) + 1px);
  max-width: calc(var(--nav-card-w, var(--nav-w)) - var(--pill-x) - 1px);
  border-radius: var(--radius, 12px);
}

/* Und dieselbe Flaeche: "Contact" sieht aus wie ein Knopf und laesst sich
   anfassen wie einer — also traegt er auch das Grau der Cards. Gerechnet
   aus derselben Formel wie dort (Abschnitt 06), damit er jeder
   Hintergrundfarbe aus dem CMS folgt; die Zeile davor ist der feste Wert
   fuer Browser ohne color-mix. Der aktive Zustand bleibt aussen vor, er
   bringt seine schwarze Fuellung selbst mit (Abschnitt 05).

   mix-blend-mode muss dafuer weg: die Basisregel verrechnet jeden Link der
   Sidebar mit dem Untergrund (difference, Abschnitt 05) — eine deckende
   Flaeche soll aber verdecken.

   Das fuehrende "body" im Selektor ist kein Zufall. Auf der Startseite legt
   body.tpl-home .sidebar a:not(.active) (Abschnitt 05) hinter jeden Link
   eine Pille in der Hintergrundfarbe. Jener Selektor zaehlt ein Element mehr
   als dieser hier und waere damit staerker — "Contact" bliebe ausgerechnet
   auf der Startseite als einziges Feld ohne Flaeche. Das "body" gleicht das
   Gewicht aus, und bei Gleichstand gewinnt die spaetere Regel: diese. Die
   Cards brauchen den Kniff nicht, sie bringen eine Klasse mehr mit. */
body .sidebar .nav-contact a:not(.active) {
  background: #e8e8e8;
  background: color-mix(in srgb, var(--body-bg, #efefef) 97%, #000);
  mix-blend-mode: normal;
}

/* Und wie bei den Cards: eine Flaeche rueckt beim Zeigen nicht zur Seite,
   sonst liefe sie aus der Flucht. Es bleibt bei der Outline. */
@media (hover: hover) {
  .sidebar .nav-contact a:hover {
    transform: none;
  }
}

/* Trennt die Navigationsgruppen durch Luft, nicht durch einen Strich. */
.sidebar hr {
  margin: 1.2rem 0;
}


/* ==========================================================================
   07  Layout: Content und weisser Kasten
   ========================================================================== */

.content {
  position: relative;
  z-index: 1;

  /* Frueher: width: --content-w plus margin-left: auto. Das Ergebnis ist
     dasselbe (rechts buendig, links bei --content-x), nur haengt die linke
     Kante jetzt nicht mehr allein am Prozentwert — sie bleibt an der
     Navigation haengen, sobald das Fenster schmal wird. Die Breite ergibt
     sich von selbst aus dem, was rechts davon uebrig bleibt. */
  margin-left: var(--content-x);
}

.page {
  /* Standard: transparent — der Body-Hintergrund scheint durch, genau wie
     im Original. Wer einen abgesetzten Kasten moechte, setzt im CMS unter
     Einstellungen > Gestaltung eine Farbe mit voller Deckkraft. */
  background: var(--page-bg, transparent);
  padding: var(--page-pad);
  min-height: 100vh;

  /* Sicherheitsnetz gegen seitliches Scrollen. Die grossen Ursachen sind
     behoben — lange Adressen brechen um (Abschnitt 04), und der Inhalt haelt
     Abstand zur Navigation (Abschnitt 02). Was bleibt, ist die Pille um einen
     Link: sie ragt links und rechts 13 px ueber den Text hinaus (Abschnitt
     05). Steht ein Link am rechten Rand einer Spalte, waren das auf der
     Kontakt- und der Playful-Journal-Seite gemessen 5 bis 8 px Scrollweg —
     genug fuer einen Balken unter der Seite.

     "clip" und nicht "hidden": hidden macht aus dem Kasten einen eigenen
     Scrollbereich, dann klebte das Portrait auf der Kontaktseite nicht mehr
     (position: sticky) und die Galerieansicht liesse sich nicht mehr wischen.
     clip schneidet nur ab; senkrecht bleibt alles, wie es war.

     Warum hier und nicht an <html> oder <body>: deren Wert reicht der Browser
     ans Fenster weiter, das Element selbst verhaelt sich weiter wie
     "visible" — gemessen in Chrome, es wurde dort weiterhin gescrollt. */
  overflow-x: clip;
}

/* Auf Seiten mit dem Umschalter Galerie/Grid bleibt oben Platz fuer ihn —
   er schwebt und laege sonst auf den Bildunterschriften. Gilt in BEIDEN
   Ansichten, damit beim Umschalten nichts springt. */
.page:has(> .view-switch) {
  padding-top: var(--switch-h);
}

/* Dasselbe am Fuss der Seite, wenn unten rechts Reiter angepinnt sind:
   sonst laege die letzte Textzeile unter "Impressum". */
.page:has(.block-legal) {
  padding-bottom: 6rem;
}

/* Startseite: kein weisser Kasten, volle Breite, Sketch als Hintergrund. */
body.tpl-home .content,
.content.is-home {
  width: 100%;
  margin-left: 0;

  /* Die leere Flaeche der Startseite laesst Klicks zum Sketch durch. */
  pointer-events: none;
}

body.tpl-home .page,
.content.is-home .page {
  background: transparent;
  padding: 0;
  min-height: 0;
}

/* Echte Inhalte auf der Startseite bleiben trotzdem bedienbar und liegen
   ueber dem Sketch (der Sketch selbst bleibt auf z-index 0). */
body.tpl-home .page > .block:not(.block-sketch),
.content.is-home .page > .block:not(.block-sketch) {
  position: relative;
  z-index: 1;
  pointer-events: auto;
}

body.tpl-home .page > .block-sketch,
.content.is-home .page > .block-sketch {
  pointer-events: auto;
}


/* ==========================================================================
   08  Bloecke

   Markup laut DOM-Contract: <div class="block block-xxx">.
   Abstaende entstehen ueber die Geschwister-Regel, damit der erste und der
   letzte Block buendig sitzen.
   ========================================================================== */

.block + .block {
  margin-top: var(--block-gap);
}

/* --- Textkarten ----------------------------------------------------------

   Jeder Textinhalt steht auf einer weissen Karte: Ueberschrift, Fliesstext,
   Aufzaehlung, Lebenslauf (Klasse is-card, gesetzt in lib/render.js). Bilder
   und Videos bekommen keine — die bringen ihre eigene Flaeche mit.

   Mehrere Textinhalte hintereinander sollen aussehen wie EINE Karte, nicht
   wie ein Stapel. Das geht ohne zusaetzliches Element im Markup, und das ist
   hier Bedingung: die Live-Vorschau im CMS erwartet jeden Inhalt als direktes
   Kind seiner Spalte (admin/preview.js, previewLiveBloecke) — ein Umschlag
   drumherum wuerde Auswaehlen, Einfuegen und Verschieben zerlegen.

   Stattdessen entscheidet jede Karte anhand ihrer Nachbarn, wo sie rund ist:

     erste im Lauf  (kein is-card davor)  -> oben rund,  oben voller Rand
     letzte im Lauf (kein is-card danach) -> unten rund, unten voller Rand
     mittendrin                           -> eckig, halber Rand

   Halb plus halb ergibt zwischen zwei Absaetzen wieder --block-gap, also
   genau den Abstand, den die Seite sonst auch hat.

   Nebenwirkung mit Absicht: Eine zweite Spalte ist ein eigener Kasten, ihre
   Inhalte sind also keine Geschwister der ersten. Zweispaltig entstehen
   dadurch von selbst zwei Karten nebeneinander — bei den Aufzaehlungen
   neben dem Projekttext zum Beispiel. Auch ein Abstandshalter zwischen zwei
   Texten trennt: gewollte Luft beendet die Karte.
   ------------------------------------------------------------------------ */

.page .is-card,
/* Ein Bild am Fuss einer Spaltenkarte gehoert mit auf die Karte — dieselben
   Masse, darum steht es hier mit in der Regel. Siehe "Bild auf der Karte"
   weiter unten. */
.page .block-columns .col > .is-card + .block-image {
  --card-pad: 4rem;
  /* Halber Rand innen, damit zwei gestapelte Karten zusammen auf --block-gap
     kommen und nicht auf das Doppelte. */
  --card-pad-innen: calc(var(--block-gap, 2rem) / 2);

  background: #fff;
  border-radius: var(--radius, 12px);
  padding: var(--card-pad);
}

/* Karten reihen sich wie Bilder: derselbe Abstand wie zwischen zwei Medien,
   nicht der groessere Blockabstand. Dadurch sitzt eine Karte im selben Raster
   wie die Galerie darueber und darunter. */
.page .is-card + .block,
.page .block + .is-card {
  margin-top: var(--media-gap);
}

/* Was eine Karte NICHT beendet: ein Abstandshalter.

   Im CMS ist der nur eine Leerzeile innerhalb eines Textes — er soll den
   Absatz auflockern, nicht eine zweite Flaeche aufmachen. Nur eine neue
   SPALTE beginnt eine neue Karte (die ist ein eigener Kasten, ihre Inhalte
   sind also gar keine Geschwister). Deshalb steht neben jedem "+ .is-card"
   auch die Fassung mit "+ .block-spacer + .is-card". */

/* Die Karte setzt nach oben fort: gerade Kante, kein Abstand davor.
   Steht bewusst NACH den Abstandsregeln oben — gleiche Staerke, und die
   spaetere Regel gewinnt. */
.page .is-card + .is-card,
.page .is-card + .block-spacer + .is-card {
  margin-top: 0;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}

/* Die Karte laeuft nach unten weiter: gerade Kante. */
.page .is-card:has(+ .is-card),
.page .is-card:has(+ .block-spacer + .is-card) {
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}

/* Direkt aneinander: halber Rand oben und unten, zusammen also ein ganzer. */
.page .is-card + .is-card {
  padding-top: var(--card-pad-innen);
}

.page .is-card:has(+ .is-card) {
  padding-bottom: var(--card-pad-innen);
}

/* Liegt ein Abstandshalter dazwischen, macht DER den Abstand — sonst kaeme
   seine Hoehe zum Rand der beiden Karten noch hinzu. */
.page .is-card:has(+ .block-spacer + .is-card) {
  padding-bottom: 0;
}

.page .is-card + .block-spacer + .is-card {
  padding-top: 0;
}

/* Und er wird selbst zur Kartenflaeche, sonst klaffte mitten in der Karte
   ein grauer Streifen. Der Abstand davor muss dabei weg: die Regel ganz oben
   gibt jedem Block nach einer Karte --media-gap mit, und die fuenf Pixel
   waeren hier ein grauer Spalt quer durch die Karte. */
.page .is-card + .block-spacer:has(+ .is-card) {
  background: #fff;
  margin-top: 0;
}

/* Die Ueberschrift bringt ihren eigenen Zeilenabstand mit und saesse sonst
   zu tief im Kasten. */
.page .block-heading.is-card {
  padding-bottom: calc(var(--card-pad) / 2);
}

.page .block-heading.is-card:has(+ .is-card) {
  padding-bottom: 0;
}

/* --- Eine Ueberschrift beginnt eine neue Karte ---------------------------

   Die Regeln oben fassen alles, was hintereinander steht, zu EINER Karte
   zusammen. Auf der Kontaktseite ergab das eine einzige Flaeche ueber die
   ganze Seite: Contact, Selected Clients, Awards, Exhibitions, Talks,
   Lectures und der Lebenslauf klebten aneinander.

   Eine Ueberschrift faengt darum immer neu an. Sie und alles, was bis zur
   naechsten Ueberschrift folgt, sind zusammen eine Karte — so hat jedes
   Kapitel seine eigene Flaeche. Die Rundungen und Innenabstaende des
   Kartenanfangs kommen hier zurueck, und die Karte davor wird wieder zur
   letzten ihres Laufs.
   ------------------------------------------------------------------------ */

.page .is-card + .block-heading.is-card,
.page .is-card + .block-spacer + .block-heading.is-card {
  margin-top: var(--media-gap);
  border-top-left-radius: var(--radius, 12px);
  border-top-right-radius: var(--radius, 12px);
  padding-top: var(--card-pad);
}

.page .is-card:has(+ .block-heading.is-card),
.page .is-card:has(+ .block-spacer + .block-heading.is-card) {
  border-bottom-left-radius: var(--radius, 12px);
  border-bottom-right-radius: var(--radius, 12px);
  padding-bottom: var(--card-pad);
}

/* Der Abstandshalter zwischen zwei Kapiteln faellt weg. Er war die Luft
   zwischen zwei Ueberschriften, als noch alles auf einer Flaeche stand;
   jetzt machen die beiden Karten den Abstand selbst — denselben, den auch
   zwei Bilder untereinander haben. Bliebe er stehen, klaffte zwischen den
   Kapiteln ein Loch von drei bis vier Zeilen Hoehe. */
.page .is-card + .block-spacer:has(+ .block-heading.is-card) {
  display: none;
}

/* --- Karten im Spaltenraster --------------------------------------------

   Stehen zwei Karten nebeneinander, sollen sie oben UND unten auf einer
   Linie liegen — sonst haengt eine von beiden im Raster der Bilder schief,
   und zwischen ihnen klafft unten ein Loch.

   Die Spalte ist im Raster ohnehin schon so hoch wie die hoechste (CSS Grid
   streckt sie). Fehlt hat bisher nur, dass die Karte DARIN diese Hoehe auch
   annimmt. Als Flex-Spalte gibt sie ihre Hoehe an die letzte Karte weiter.
   ------------------------------------------------------------------------ */

.page .block-columns .col:has(> .is-card) {
  display: flex;
  flex-direction: column;

  /* Eine Karte wirkt erst als Flaeche, wenn sie eine hat: die Spalte ist
     mindestens halb so hoch wie breit, der Text sitzt oben, darunter bleibt
     Luft. Das Verhaeltnis ist eine Untergrenze, keine feste Hoehe — reicht
     der Text weiter, waechst die Spalte darueber hinaus (aspect-ratio gibt
     nur die Wunschhoehe vor, min-height: auto laesst den Inhalt gewinnen). */
}

/* Das Mass gibt allein die ERSTE Spalte vor — die mit dem Fliesstext, also
   die breite. Bekaeme die schmale Spalte daneben dasselbe Verhaeltnis,
   rechnete sie sich aus IHRER Breite eine eigene, viel kleinere Hoehe aus
   und stuende wieder zu kurz (gemessen: 381 gegen 189 Pixel). So gibt es
   ein Mass fuer die Zeile, und das Raster streckt die uebrigen Spalten
   darauf. */
.page .block-columns:has(.block-heading) .col:first-child:has(> .is-card) {
  aspect-ratio: 2 / 1;
}

/* Ein Abstandshalter am ENDE einer Spalte ist Beiwerk aus dem Cargo-Import:
   er macht dort nur ein Loch unter der Karte und haelt sie davon ab, bis zum
   Fuss der Spalte zu reichen. */
.page .block-columns .col > .block-spacer:last-child {
  display: none;
}

/* Die letzte Karte der Spalte nimmt den Rest — damit endet die Spalte
   geschlossen und nicht mit grauer Flaeche unter der Karte.

   Nicht :last-child: display:none nimmt den Abstandshalter oben zwar aus dem
   Bild, im Baum bleibt er aber stehen und waere weiterhin das letzte Kind.
   Gemeint ist "auf diese Karte folgt keine weitere". */
.page .block-columns .col > .is-card:not(:has(~ .is-card)) {
  flex: 1 0 auto;
}

/* --- Bild auf der Karte --------------------------------------------------

   Steht in einer Spalte ein Bild direkt hinter einer Karte, gehoert es mit
   AUF die Karte und nicht als eigene Flaeche darunter. Gebraucht wird das
   fuer Auszeichnungen: bei "Perception Machine" sitzt der ADC-Nagel unter
   der Aufzaehlung, und beides zusammen ist eine Karte.

   Die Karte oben gibt dafuer ihre untere Rundung her, das Bild uebernimmt
   sie — dieselbe Mechanik wie bei zwei gestapelten Karten, nur dass ein
   Bild die Klasse is-card nicht tragen kann: es ist ein Medium, und die
   Abstandsregeln fuer Medien (.is-media + .is-media) gelten weiter.

   Bewusst nur fuer Bilder: ein Video auf einer Karte gab es noch nie und es
   braechte eigene Regeln mit.
   ------------------------------------------------------------------------ */

/* Die Karte darueber laeuft weiter: gerade Kante, halber Rand nach unten —
   und sie ist nicht mehr die letzte, darf sich also nicht mehr strecken. */
.page .block-columns .col > .is-card:has(+ .block-image) {
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
  padding-bottom: var(--card-pad-innen);
  flex: 0 0 auto;
}

.page .block-columns .col > .is-card + .block-image {
  /* Die Kartenflaeche ist so breit wie die Spalte. --scale wuerde hier die
     ganze Karte schmal machen — das Mass aus dem CMS gehoert ans Bild, nicht
     an die Flaeche darunter (siehe naechste Regel). */
  width: 100%;
  margin-top: 0;
  padding-top: var(--card-pad-innen);
  border-top-left-radius: 0;
  border-top-right-radius: 0;
  /* Jetzt haengt der Fuss der Spalte am Bild: es nimmt den Rest der Hoehe,
     damit die Karte unten geschlossen endet. Das Bild bleibt dabei oben
     stehen, darunter steht weisse Kartenflaeche. */
  flex: 1 0 auto;
}

/* Das Bild selbst behaelt sein Mass aus dem CMS (--scale erbt es von der
   figure, wo der Renderer es als style-Attribut hinschreibt). */
.page .block-columns .col > .is-card + .block-image > img,
.page .block-columns .col > .is-card + .block-image > video {
  width: calc(var(--scale, 100) * 1%);
}

/* Ein Spaltenblock mit Karten reiht sich wie ein Bild ein: derselbe Abstand
   nach oben und unten wie zwischen zwei Medien. Ohne das haette er den
   groesseren Blockabstand und saesse sichtbar anders als die Galerie
   darueber. */
.page .block-columns:has(.is-card) + .block,
.page .block + .block-columns:has(.is-card) {
  margin-top: var(--media-gap);
}

/* Und ein Abstandshalter, der zwischen Medium und Kartenblock steht, faellt
   auf dasselbe Mass zurueck. Seine Hoehe steht als style-Attribut im Markup,
   deshalb hier ausnahmsweise !important — im CMS bleibt der Wert erhalten,
   er wirkt nur an dieser einen Stelle nicht mehr. */
.page .block-spacer:has(+ .block-columns .is-card),
.page .block-columns:has(.is-card) + .block-spacer,
/* Dasselbe fuer Karten, die ueber die volle Breite stehen (Ueberschrift und
   Text ohne Spalten, etwa der Abschnitt "Plakatserie" bei Wahl-O-Mat): auch
   dort galt sonst weiter der Abstand aus dem CMS — gemessen 28 und 42 Pixel
   zwischen Video und Karte, wo ringsum 5 stehen. */
.page .is-card + .block-spacer:not(:has(+ .is-card)),
.page :not(.is-card) + .block-spacer:has(+ .is-card),
/* Und zwischen zwei Medien: dort galt der CMS-Wert bisher als Einziger noch
   weiter — bei AR Landesmuseum standen die Handy-Reihe und die Reihe
   darunter 14 Pixel auseinander, waehrend nebeneinander 5 Pixel liegen.
   Medien stehen ueberall gleich weit auseinander (siehe ".is-media +
   .is-media" weiter unten), also auch hier. */
.page .is-media + .block-spacer:has(+ .is-media) {
  height: 0 !important;
}

.page .block-spacer:has(+ .block-columns .is-card) + .block-columns,
.page .block-columns:has(.is-card) + .block-spacer + .block,
/* Beide Seiten ausdruecklich OHNE Karte: sonst faenge diese Regel auch den
   Fall Karte -> Abstandshalter -> Karte ab und riss einen grauen Spalt quer
   durch die Karte (gemessen an "Plakatserie"). */
.page .is-card + .block-spacer + :not(.is-card),
.page :not(.is-card) + .block-spacer:has(+ .is-card) + .is-card,
.page .is-media + .block-spacer + .is-media {
  margin-top: var(--media-gap);
}


/* --- "Next Project" reiht sich ein ---------------------------------------

   Der Block ist weder Karte noch Medium und fiel darum als Einziger aus dem
   Raster zurueck auf den Blockabstand. Wie weit er vom Inhalt darueber stand,
   hing dadurch am Zufall des letzten Blocks: hinter einer Karte 5 px (AR
   Landesmuseum, Wahl-O-Mat), hinter einer Galerie 20 px (Perception Machine,
   ADC Festival, Campus Day, Twees) und hinter einem Abstandshalter 14 px
   (Playful Journal). Seine Trennlinie setzt den Absatz ohnehin sichtbar ab;
   den Abstand macht jetzt dasselbe Mass wie ueberall sonst.
   ------------------------------------------------------------------------ */

.page .block + .block-next {
  margin-top: var(--media-gap);
}

/* Ein Abstandshalter davor faellt weg — wie zwischen zwei Medien. Seine Hoehe
   steht als style-Attribut im Markup, deshalb auch hier !important. */
.page .block-spacer:has(+ .block-next) {
  height: 0 !important;
}

.page .block-spacer + .block-next {
  margin-top: var(--media-gap);
}


/* Links auf der Karte: ohne Blend-Modus.

   Abschnitt 05 gibt jedem Link mix-blend-mode: difference — der Trick aus dem
   Original, damit ein Link auch ueber einem Bild lesbar bleibt. Gekapselt ist
   die Blend-Gruppe pro .block (isolation: isolate). Solange ein Textblock
   keinen eigenen Hintergrund hatte, blendete der Link gegen nichts und blieb
   schwarz.

   Die weisse Karte IST nun so ein Hintergrund, und damit rechnet difference
   aus schwarzer Schrift weisse: die Links waeren auf der Karte unsichtbar
   (gemessen an der Kontaktseite, wo die ganze Kundenliste verschwand).
   Auf einer Karte ist der Grund aber immer weiss und der Trick unnoetig —
   ueber Bildern, wo er zaehlt, gibt es keine Karten. Die Hover-Pille zeichnet
   weiterhin in Textfarbe und bleibt unveraendert. */
.page .is-card a {
  mix-blend-mode: normal;
}

/* Auf schmalen Schirmen frisst ein breiter Rand zu viel von der Zeile. */
@media (max-width: 768px) {
  .page .is-card,
  .page .block-columns .col > .is-card + .block-image {
    --card-pad: 2.4rem;
  }
}


/* Grenzt ein Medium direkt an ein Medium, gilt derselbe Abstand wie INNERHALB
   einer Galerie. Vorher standen Bilder aus einer Galerie 5 px auseinander,
   zwei aufeinanderfolgende Galerien aber 20 px — genau die Unregelmaessigkeit,
   die im Original schon steckte. Jetzt ist es ueberall derselbe Wert. */
.is-media + .is-media {
  margin-top: var(--media-gap);
}

/* Der Abstandsblock bringt sein Mass selbst mit (inline style height) — auf
   beiden Seiten ohne zusaetzlichen Blockabstand.

   Wichtig ist die Schreibweise ".block + .block-spacer": ein blosses
   ".block-spacer" hat weniger Gewicht als ".block + .block" weiter oben und
   wurde davon ueberstimmt. Jeder Abstandsblock bekam dadurch ungefragt
   20 px obendrauf — aus eingestellten 14 px wurden sichtbare 34 px. */
.block + .block-spacer,
.block-spacer + .block {
  margin-top: 0;
}

/* Alle Medien im Inhalt: Radius, blockweise, nie breiter als die Spalte. */
.page img,
.page video,
.page iframe {
  display: block;
  max-width: 100%;
  height: auto;
  border-radius: var(--radius, 12px);
}

/* --- Ueberschrift ------------------------------------------------------- */

.block-heading h1 {
  font-size: var(--h1-fs, 6.8rem);
  line-height: 1.3;
}

.block-heading h2 {
  font-size: var(--h2-fs, 4.2rem);
  line-height: 1.2;
  font-weight: 400;
}

/* --- Fliesstext --------------------------------------------------------- */

.block-text {
  font-size: var(--body-fs, 1.6rem);
  line-height: 1.2;
}

.block-text p + p {
  margin-top: 1.2rem;
}

.block-text ul,
.block-text ol {
  padding-left: 2rem;
}

/* --- Trennlinie --------------------------------------------------------- */

/* Auch der Inhalts-Trenner ist nur Abstand (siehe Basisregel bei hr). */
hr.block-divider {
  margin: 0;
}

/* --- Abstand ------------------------------------------------------------ */

.block-spacer {
  width: 100%;
}

/* --- Liste -------------------------------------------------------------- */

ul.block-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* Das Quadrat ist ein eigenes Kaestchen statt eines Listenpunkts: so laesst
   sich Groesse und Position genau setzen. Alle Werte in em, damit der Punkt
   immer zur Schriftgroesse passt. Der Punkt sitzt am linken Spaltenrand, der
   Text rueckt ein — dadurch bleibt die Kante buendig zum uebrigen Inhalt. */
ul.block-list li {
  position: relative;
  padding-left: 1.15em;
}

ul.block-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.42em;
  width: 0.3em;
  height: 0.3em;
  background: currentColor;
}

ul.block-list li + li {
  margin-top: 0.4rem;
}

/* --- Lebenslauf (Zeitstrahl) ---------------------------------------------

   Eine duenne Linie, an der die Stationen haengen. Gebuendelt wird nach ORT:
   oben der Name auf einem vollen Punkt, darunter die Rollen dort, jede an
   einem kleineren. Zwei Rollen bei derselben Agentur stehen dadurch
   untereinander statt an zwei Stellen der Liste (lib/render.js,
   timelineGroups) — so wie bei LinkedIn.

   Vorher war jede Station eine graue Card mit Logofeld. Die Flaeche kostete
   in der schmalen Spalte der Kontaktseite rund 70 px Textbreite; lange
   Rollennamen brachen dadurch mitten im Wort um. Die Linie braucht ein
   Viertel davon und zeigt zugleich, was eine Card nicht zeigen konnte:
   welche Stationen zusammengehoeren.

   Die Spur links ist so schmal wie ihr Punkt. Nur wenn wenigstens eine
   Station ein Logo mitbringt, wird sie so breit wie das Logo (Klasse
   has-logos, gesetzt in lib/render.js) — sonst staende dort eine leere
   Spalte von Logobreite.
   ------------------------------------------------------------------------ */

.block-timeline {
  font-size: var(--body-fs, 1.6rem);
  line-height: 1.2;

  /* Breite der Marke auf der Linie. Ohne Logos ist das der Punkt, mit Logos
     das Logofeld — gerechnet aus der Textgroesse, damit es mitwaechst, wenn
     im CMS die Schrift geaendert wird. */
  --cv-node: var(--cv-dot);
  /* Abstand vom linken Rand bis zum Text: die Marke plus Luft. */
  --cv-rail: calc(var(--cv-node) + 1.1rem);
  /* Punkt eines Ortes und Punkt einer Rolle darunter. */
  --cv-dot: 0.7rem;
  --cv-dot-small: 0.45rem;
}

.block-timeline.has-logos {
  --cv-node: calc(var(--body-fs, 1.6rem) * 2.2);
}

ol.timeline-list,
ol.timeline-stations {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* Kopf (Ort) und Rolle sind die beiden Zeilen, die eine Marke tragen. Beide
   ruecken um die Spur ein und sind Bezugspunkt fuer Linie und Punkt. */
.timeline-head,
.timeline-station {
  position: relative;
  padding-left: var(--cv-rail);
}

/* Die Linie. Sie laeuft von der Mitte des eigenen Punktes bis zur Mitte des
   naechsten — daher der Ueberstand von 0.6em nach unten: die Kaesten von Kopf
   und Rollen stossen luecken los aneinander (die Abstaende stecken in ihrem
   padding, nicht in margin), und der naechste Punkt sitzt 0.6em unter der
   gemeinsamen Kante. 0.6em ist die halbe Zeilenhoehe, also die Mitte der
   ersten Textzeile.

   Die Farbe folgt der Textfarbe aus dem CMS; die Zeile davor ist der feste
   Wert fuer Browser ohne color-mix. */
.timeline-head::before,
.timeline-station::before {
  content: "";
  position: absolute;
  left: calc(var(--cv-node) / 2 - 0.5px);
  top: 0.6em;
  bottom: -0.6em;
  width: 1px;
  background: rgba(0, 0, 0, 0.14);
  background: color-mix(in srgb, var(--text, #000) 14%, transparent);
}

/* Der Punkt sitzt mittig auf der Linie und mittig in der ersten Textzeile. */
.timeline-head::after,
.timeline-station::after {
  content: "";
  position: absolute;
  border-radius: 50%;
  left: calc((var(--cv-node) - var(--cv-dot)) / 2);
  top: calc(0.6em - var(--cv-dot) / 2);
  width: var(--cv-dot);
  height: var(--cv-dot);
  background: var(--text, rgba(0, 0, 0, 0.9));
}

/* Die Rolle traegt den kleineren, blasseren Punkt: sie ist eine Station
   INNERHALB des Ortes darueber, keine eigene. */
.timeline-station::after {
  left: calc((var(--cv-node) - var(--cv-dot-small)) / 2);
  top: calc(0.6em - var(--cv-dot-small) / 2);
  width: var(--cv-dot-small);
  height: var(--cv-dot-small);
  background: rgba(0, 0, 0, 0.3);
  background: color-mix(in srgb, var(--text, #000) 30%, transparent);
}

/* Gibt es an einem Ort nur eine Rolle, waere ihr Punkt ein zweiter Punkt fuer
   dieselbe Station — sie haengt am Punkt des Ortes darueber. */
.timeline-group.is-single .timeline-station::after {
  display: none;
}

/* Das Logofeld ersetzt den Punkt: es liegt auf der Linie und deckt sie an
   dieser Stelle ab (Kartenfarbe, siehe „Textkarten“ weiter oben). Das Logo
   behaelt darin sein Seitenverhaeltnis und bekommt ringsum etwas Luft. */
.timeline-logo {
  position: absolute;
  left: 0;
  top: calc(0.6em - var(--cv-node) / 2);
  width: var(--cv-node);
  height: var(--cv-node);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0.4rem;
  border-radius: calc(var(--radius, 12px) * 0.5);
  background: #fff;
  overflow: hidden;
}

.timeline-head:has(.timeline-logo)::after {
  display: none;
}

.timeline-logo img {
  width: 100%;
  height: 100%;
  object-fit: contain;

  /* Die Grundregel gibt jedem Bild der Seite die runde Ecke (--radius). Bei
     einem Logo von 35 px Kantenlaenge waere das fast ein Kreis und wuerde die
     Marke anschneiden. Die Ecke macht hier das Feld drumherum. */
  border-radius: 0;
}

/* Der Ort ist die Ueberschrift der Gruppe — dieselbe Groesse wie der Text,
   nur fett. Groesser waere eine dritte Ueberschriftstufe neben „Curriculum
   Vitae“ darueber. */
.timeline-org {
  font-weight: 700;
}

/* Die Spanne des Ortes: vom Anfang der aeltesten bis zum Ende der juengsten
   Rolle. Unterste Stufe der Skala (1.6rem / 1.618), so wie figcaption. */
.timeline-span,
.timeline-period {
  font-size: 1rem;
  line-height: 1.3;
  color: var(--small);
  margin-top: 0.25rem;
}

.timeline-note {
  font-size: 1rem;
  line-height: 1.3;
  color: var(--caption);
  margin-top: 0.3rem;
}

/* Die Abstaende stecken alle im padding und nie in margin: die Kaesten
   muessen aneinanderstossen, sonst risse die Linie zwischen ihnen. */
.timeline-head {
  padding-bottom: 1rem;
}

/* Steht nur eine Rolle darunter, gehoert sie zur Kopfzeile — dann bleibt es
   bei der Luft einer Zeile. */
.timeline-group.is-single .timeline-head {
  padding-bottom: 0.25rem;
}

.timeline-station {
  padding-bottom: 1.4rem;
}

/* Zwischen zwei Orten mehr Luft als zwischen zwei Rollen desselben Ortes —
   das ist die eigentliche Gliederung der Liste. */
.timeline-station:last-child {
  padding-bottom: 2.8rem;
}

/* Die Linie endet am letzten Punkt, nicht am letzten Buchstaben: unter der
   aeltesten Station haengt sonst ein Stueck Linie ins Leere. */
.timeline-group:last-child .timeline-station:last-child {
  padding-bottom: 0;
}

.timeline-group:last-child .timeline-station:last-child::before,
.timeline-group:last-child .timeline-head:last-child::before {
  display: none;
}

/* --- Einzelbild --------------------------------------------------------- */

/* --scale ist eine Zahl ohne Einheit (Prozent der Spaltenbreite). */
.block-image {
  width: calc(var(--scale, 100) * 1%);
  max-width: 100%;
}

/* --- Galerie ------------------------------------------------------------ */

.block-gallery {
  display: grid;
  gap: var(--gap, 0.5rem);
}

/* Die Rasterzeile ist immer so hoch wie ihr hoechstes Element. Stehen unter
   den Bildern verschieden lange Bildunterschriften (im Plakatarchiv reichen
   sie von einer bis zu drei Zeilen), werden die Zeilen unterschiedlich hoch
   und die Abstaende zwischen den Bildern wirken zufaellig — gemessen 28, 29,
   31 und 36 px in derselben Galerie.

   Loesung: das Bild steht oben, die Bildunterschrift wird an den unteren Rand
   der Zelle geschoben. Dadurch beginnen alle Bilder einer Zeile buendig und
   der Abstand zur naechsten Bildreihe ist ueberall gleich. */
.block-gallery .gallery-item {
  margin: 0;
  min-width: 0;
  display: grid;
  grid-template-rows: auto 1fr;
  align-content: start;
}

.block-gallery .gallery-item figcaption {
  align-self: end;
}

/* Bilder UND Videos fuellen ihre Rasterzelle: Galerien duerfen gemischte
   Medien enthalten (ARCHITECTURE.md §9). Ohne width:100% rendert ein Video
   in seiner Eigenbreite und sprengt das Raster. */
.block-gallery img,
.block-gallery video {
  width: 100%;
  height: auto;
}

/* Video-Items markiert der Renderer zusaetzlich mit .is-video. Sie werden
   genau wie Bild-Items behandelt; die Klasse ist der Haken fuer site.js
   (Lightbox oeffnet sie als Video) und fuer spaetere Sonderfaelle. */
.block-gallery .gallery-item.is-video {
  display: block;
}

/* Raster mit --cols Spalten (Zahl ohne Einheit) */
.block-gallery[data-layout="grid"] {
  grid-template-columns: repeat(var(--cols, 3), minmax(0, 1fr));
}

/* Einspaltig: Bilder ueber die volle Breite */
.block-gallery[data-layout="columns"] {
  grid-template-columns: minmax(0, 1fr);
}

/* Slideshow: horizontal scrollen mit Einrastpunkten */
.block-gallery[data-layout="slideshow"] {
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 80%);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scroll-padding: 0;
  padding-bottom: 0.5rem;
  scrollbar-width: thin;
  scrollbar-color: rgba(0, 0, 0, 0.25) transparent;
}

.block-gallery[data-layout="slideshow"] .gallery-item {
  scroll-snap-align: center;
}

.block-gallery[data-layout="slideshow"]::-webkit-scrollbar {
  height: 6px;
}

.block-gallery[data-layout="slideshow"]::-webkit-scrollbar-thumb {
  background: rgba(0, 0, 0, 0.25);
  border-radius: 3px;
}

/* Die Pfeil-Buttons der Slideshow erzeugt public/js/site.js samt Optik
   (4rem = 40px). Hier wird nur die Trefferflaeche auf das Mindestmass fuer
   Touch-Ziele angehoben. */
.slideshow-arrow {
  min-width: var(--tap);
  min-height: var(--tap);
}

/* --- Plakat-Mockup und Galerieansicht ------------------------------------

   Eine Galerie mit "mockup": true kann zwei Ansichten:

     data-view="grid"     das gewohnte Raster, Plakate pur
     data-view="gallery"  eine lange Reihe aus Plakat-Mockups, die waagerecht
                          scrollt (Mausrad nach unten = Reihe nach rechts,
                          siehe public/js/site.js)

   Das Markup ist in beiden Faellen dasselbe; umgeschaltet wird nur per CSS.

   Aufbau eines Mockups (render.js, wrapInMockup):

     .poster-mock            Huelle, Seitenverhaeltnis 999 : 1248
       .poster-mock-frame    der fotografierte Plakatkasten
       .poster-mock-slot     die Flaeche, in die das Plakat gehoert
       .poster-mock-gloss    Glanzlicht, Blend-Modus soft-light

   Die Werte im Slot sind AUSGEMESSEN, nicht geschaetzt: in der 999 x 1248 px
   grossen Vorlage liegt die Plakatflaeche bei x 129-872 und y 90-1143, also
   744 x 1054 px. Das entspricht 12,91 % / 7,21 % / 74,48 % / 84,46 % und
   ergibt das Verhaeltnis 1 : 1,4167 — A1 ist 1 : 1,4142. Der Unterschied
   betraegt 0,2 %, deshalb darf object-fit: cover die Plakate fuellen, ohne
   dass sichtbar etwas abgeschnitten wird.
   ------------------------------------------------------------------------ */

.poster-mock {
  position: relative;
  isolation: isolate;      /* haelt soft-light INNERHALB des Mockups */
  /* Die Datei ist 999 x 1248 px gross, der FOTOGRAFIERTE Kasten darin aber
     nur 797 x 1160 px — ringsum liegt durchsichtiger Rand (links 102 px,
     rechts 100 px, oben 88 px, unten keiner). Die Box umfasst deshalb nur
     den sichtbaren Teil. Sonst waere zwischen zwei Mockups ein Viertel mehr
     Luft als eingestellt, und die Bildunterschrift darueber schwebte 88 px
     ueber dem Kasten, statt an ihm zu kleben. */
  aspect-ratio: 797 / 1160;
  width: 100%;
}

/* Rahmen und Glanzlicht sind die GANZE Datei — sie ragen darum ueber die Box
   hinaus. Die Werte sind aus den Randmassen oben gerechnet:
   links -102/797, oben -88/1160, Breite 999/797, Hoehe 1248/1160. */
/* Der Elementname img gehoert hier dazu: die Basisregel ".page img" weiter
   oben setzt max-width: 100% und height: auto und ist mit einer Klasse allein
   nicht zu schlagen. Gemessen wurde der Rahmen dadurch auf 553 statt 693 px
   gestaucht — er sass verschoben und der Mast fiel unten weg. */
img.poster-mock-frame,
img.poster-mock-gloss {
  position: absolute;
  left: -12.798%;
  top: -7.586%;
  width: 125.345%;
  height: 107.586%;
  max-width: none;
  border-radius: 0;        /* der Rahmen ist freigestellt, kein Kasten */
  pointer-events: none;
}

.poster-mock-gloss {
  /* Das Glanzlicht liegt ueber Rahmen UND Plakat und wird eingerechnet,
     statt sie zu ueberdecken — dadurch wirkt das Plakat wie hinter Glas. */
  mix-blend-mode: soft-light;
  z-index: 2;
}

/* Dieselbe Plakatflaeche wie zuvor (x 129-872, y 90-1143 in der Datei), nur
   jetzt in Prozent der sichtbaren Box gerechnet. Das Verhaeltnis bleibt
   744 : 1054 = 1 : 1,4167. */
.poster-mock-slot {
  position: absolute;
  left: 3.388%;
  top: 0.172%;
  width: 93.35%;
  height: 90.862%;
  overflow: hidden;
  z-index: 1;
}

/* Das Plakat fuellt die Flaeche vollstaendig. border-radius muss weg: im
   Rahmen sitzt ein Papierbogen mit rechten Ecken, keine App-Kachel. */
.poster-mock-slot img,
.poster-mock-slot video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 0;
}

/* Gridansicht: Rahmen und Glanzlicht verschwinden, die Huelle loest sich
   auf. Das Plakat sitzt danach wieder direkt in der Rasterzelle — das
   Raster sieht also aus wie vorher. */
.block-gallery[data-view="grid"] .poster-mock {
  display: contents;
}

.block-gallery[data-view="grid"] img.poster-mock-frame,
.block-gallery[data-view="grid"] img.poster-mock-gloss {
  display: none;
}

/* Der Ausschnitt wird wieder ein ganz normaler Kasten in der Rasterzelle. */
.block-gallery[data-view="grid"] .poster-mock-slot {
  position: static;
  width: auto;
  height: auto;
  overflow: visible;
}

.block-gallery[data-view="grid"] .poster-mock-slot img,
.block-gallery[data-view="grid"] .poster-mock-slot video {
  height: auto;
  object-fit: fill;
  border-radius: var(--radius, 12px);
}

/* Galerieansicht: eine Reihe, die waagerecht scrollt.

   Die Groesse haengt an der Fensterhoehe, nicht an der Breite: ein Mockup
   soll immer ganz zu sehen sein. Von der Hoehe gehen der Seitenrand und ein
   fester Platz fuer die Bildunterschrift ab, der Rest ist das Mockup. Der
   Faktor 0.86 laesst oben und unten etwas Luft, damit die Reihe nicht an
   den Fensterkanten klebt. */
.block-gallery[data-view="gallery"] {
  /* Vom Fenster gehen oben der Platz des Umschalters und unten der
     Seitenrand ab — beides steckt im Innenabstand von .page. */
  --strip-h: calc(100vh - var(--switch-h) - var(--page-pad));
  --strip-h: calc(100dvh - var(--switch-h) - var(--page-pad));

  /* Breite EINES Mockups — alles andere haengt daran. Gerechnet wird aus der
     Fensterhoehe: 4.6rem gehen fuer die Bildunterschrift ueber dem Plakat ab,
     der Rest ist das Mockup, und 1.4555 ist das Seitenverhaeltnis des
     SICHTBAREN Kastens (1160 / 797) — nicht das der Datei.

     max() faengt sehr flache Fenster ab: ohne ihn waere der Wert bei unter
     46 px Fensterhoehe negativ und das Mockup verschwunden.

     min() mit 52vw ist die Gegenrichtung. Der Streifen ist so breit wie der
     Inhalt (--content-w, ab Werk 81 %). Ohne die Grenze waere auf einem
     hochkant gedrehten Monitor (z. B. 1080 x 1920) ein Mockup 1280 px breit
     geworden — breiter als der 875 px schmale Inhaltsbereich, ein ganzes
     Plakat haette nie ins Bild gepasst. */
  --mock-w: min(max(9.6rem, calc((var(--strip-h) - 4.6rem) / 1.4555)), 52vw);

  display: flex;
  /* Die Masten stehen alle auf derselben Linie, ganz unten am Fensterrand. */
  align-items: flex-end;

  /* Der Abstand haengt an der Plakatbreite, nicht an der Schriftgroesse: die
     Mockups wachsen mit der Fensterhoehe, ein fester rem-Wert saehe auf einem
     hohen Schirm eng und auf einem flachen weit aus. Ein Fuenftel einer
     Plakatbreite haelt den Eindruck auf jedem Fenster gleich. */
  gap: calc(var(--mock-w) * 0.2);
  height: var(--strip-h);
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-x: contain;

  /* Beim Umschalten wird aus einer fensterhohen Reihe eine meterlange
     Rasterseite. Chrome merkt sich dabei, welches Plakat gerade zu sehen war,
     und haelt die Ansicht daran fest — gemessen sprang die Seite dadurch auf
     8264 px, statt oben anzufangen. Hier ist das nicht erwuenscht: der Wechsel
     der Ansicht soll immer am Anfang beginnen. */
  overflow-anchor: none;

  /* Das Rad setzt scrollLeft direkt. Mit dem geerbten "smooth" aus html
     wuerde jede Radbewegung eine eigene Animation starten und die naechste
     abwuergen — die Reihe ruckelte dann. */
  scroll-behavior: auto;

  /* Kein sichtbarer Balken: er sass unter den Masten und hielt sie vom
     unteren Fensterrand fern. Gescrollt wird mit dem Rad (site.js) oder auf
     dem Handy mit dem Finger; dass es weitergeht, zeigt der Anschnitt des
     naechsten Plakats. */
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.block-gallery[data-view="gallery"]::-webkit-scrollbar {
  width: 0;
  height: 0;
}

/* --- Weiche linke Kante der Reihe ---------------------------------------

   Die Reihe scrollt unter der linken Kante des Inhaltsbereichs hindurch. Der
   Rand des Scrollbereichs schnitt die Plakate dort glatt ab — eine harte
   senkrechte Kante mitten im Bild. Eine Maske laesst die letzten Millimeter
   stattdessen in den Seitenhintergrund auslaufen.

   Maske statt einer gemalten Verlaufsflaeche davor: der Streifen liegt je
   nach Einstellung auf dem Body oder auf einem eingefaerbten Kasten
   (--page-bg). Eine Flaeche muesste die richtige Farbe raten, die Maske
   nimmt einfach das Plakat weg und laesst durchscheinen, was darunter liegt.

   Der Verlauf ist nur dann da, wenn wirklich etwas abgeschnitten wird: am
   Anfang der Reihe steht das erste Plakat vollstaendig im Bild und soll
   nicht grundlos verblassen. Die Klasse setzt public/js/site.js
   (Abschnitt 4b), sobald die Reihe gescrollt ist.
   ------------------------------------------------------------------------ */

/* Angemeldet, damit der Browser den Wert als Laenge kennt und weich
   ueberblenden kann — eine gewoehnliche Variable spraenge beim ersten
   Radtick von null auf volle Breite. Kennt ein Browser @property nicht,
   springt der Wert eben; die Maske selbst arbeitet trotzdem. */
@property --strip-fade {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}

.block-gallery[data-view="gallery"] {
  --strip-fade: 0px;

  /* Nach dem letzten Farbstopp setzt ein Verlauf seine Farbe fort: rechts
     vom Fade ist die Maske durchgehend deckend, das Plakat dort also voll
     zu sehen. Bei --strip-fade: 0px liegen beide Stopps aufeinander und die
     Maske verdeckt gar nichts. */
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--strip-fade));
  mask-image: linear-gradient(to right, transparent 0, #000 var(--strip-fade));
  transition: --strip-fade 0.35s ease;
}

/* Die Breite haengt wie der Abstand zwischen zwei Plakaten an --mock-w, nicht
   an der Schriftgroesse: der Verlauf soll auf jedem Fenster gleich breit
   WIRKEN. Ein knappes Zwoelftel einer Plakatbreite reicht — bei einem 590 px
   breiten Mockup sind das gemessen 47 px, genug fuer eine weiche Kante und
   zu wenig, um wie Nebel ueber dem Plakat zu liegen. */
.block-gallery[data-view="gallery"].is-cut-left {
  --strip-fade: calc(var(--mock-w) * 0.08);
}

/* --- Zurueck an den Anfang ----------------------------------------------

   Der Knopf steht als letzter Eintrag IN der Reihe — man scrollt also bis
   zum Ende und findet dort den Rueckweg. Gebaut wird er in
   public/js/site.js, und nur dann, wenn die Reihe breiter ist als der
   Bildschirm.

   Er sieht aus wie der Umschalter oben: gleiche Schriftgroesse, gleiche
   Pille beim Zeigen. Anders als die Plakate steht er aber nicht auf der
   Standlinie, sondern auf halber Hoehe — er gehoert nicht zu den Masten.

   Einen Grundstil in site.js (BASE_STYLES, Abschnitt 1) braucht er nicht:
   ohne Stylesheet bliebe er ein gewoehnlicher Knopf in der Reihe und waere
   weiterhin zu sehen und zu bedienen.
   ------------------------------------------------------------------------ */

.block-gallery[data-view="gallery"] .gallery-back {
  align-self: center;
  flex: 0 0 auto;

  /* Rechts derselbe Abstand wie zwischen zwei Plakaten. Ohne ihn klebte der
     Pfeil am Ende der Reihe am rechten Fensterrand — gemessen fuenf Pixel
     davon entfernt. */
  margin-right: calc(var(--mock-w) * 0.2);
  font-size: var(--nav-fs, 1.9rem);
  line-height: 1.2;
  border: 1px solid transparent;   /* Platzhalter, verhindert Layout-Sprung */
  border-radius: 100px;
  padding: 2px var(--pill-x);
  transition: border-color 0.25s ease, transform 0.25s ease;
}

@media (hover: hover) {
  .block-gallery[data-view="gallery"] .gallery-back:hover {
    border-color: currentColor;
    /* Spiegelbildlich zur Navigation: dort rueckt der Eintrag beim Zeigen
       nach rechts, hier geht es zurueck, also nach links. */
    transform: translateX(-0.45rem);
  }
}

/* Den Uebergang nimmt Abschnitt 16 ohnehin weg; hier faellt zusaetzlich der
   Ruck nach links weg — genau wie bei den Links in Abschnitt 05. */
@media (prefers-reduced-motion: reduce) {
  .block-gallery[data-view="gallery"] .gallery-back:hover {
    transform: none;
  }
}

/* In der Gridansicht gibt es keine Reihe, aus der man zurueckkehren
   koennte — dort waere der Knopf nur eine leere Rasterzelle. */
.block-gallery[data-view="grid"] .gallery-back {
  display: none;
}

/* Ein Abstandshalter am Fuss der Seite ist Beiwerk aus dem Cargo-Import —
   dieselbe Lage wie am Ende einer Spalte weiter oben. In der Galerieansicht
   ist er sogar schaedlich: die Reihe ist genau einen Bildschirm hoch, seine
   28 px machten die Seite hoeher als das Fenster. Am Ende der Reihe gab der
   Griff am Rad die Seite frei (site.js, Abschnitt 4b) und sie rutschte genau
   um diese 28 px nach unten — die Masten wurden unten angeschnitten und die
   Ansicht wackelte. */
.page:has(> .block-gallery[data-view="gallery"]) > .block-spacer:last-child {
  display: none;
}

.block-gallery[data-view="gallery"] .gallery-item {
  /* Feste Breite statt "so breit wie der Inhalt": sonst wuerde eine lange
     Bildunterschrift das Mockup daneben breiter machen.

     Der Eintrag ist so hoch wie die ganze Reihe und schiebt seinen Inhalt
     nach UNTEN. Dadurch steht jedes Mockup auf derselben Linie, egal ob die
     Bildunterschrift darueber eine oder drei Zeilen braucht — die waechst
     dann nach oben in die freie Flaeche. */
  flex: 0 0 var(--mock-w);
  width: var(--mock-w);
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: center;
  gap: 0.8rem;
}

/* Breite kommt vom Eintrag, Hoehe aus dem Seitenverhaeltnis der Basisregel
   weiter oben. Dadurch braucht die Handy-Ansicht nur --mock-w zu aendern. */
.block-gallery[data-view="gallery"] .poster-mock {
  flex: 0 0 auto;
}

/* order: -1 hebt die Bildunterschrift ueber das Plakat, ohne das Markup zu
   aendern — in der Gridansicht steht sie weiter darunter. */
.block-gallery[data-view="gallery"] .gallery-item figcaption {
  order: -1;
  align-self: center;
  text-align: center;
  max-width: 90%;
  margin-top: 0;
}

/* --- Umschalter Galerie / Grid ------------------------------------------

   Steht mittig am oberen Rand und schwebt ueber dem Inhalt. Er sieht aus wie
   ein Eintrag der Navigation: dieselbe Schriftgroesse, kein Rahmen, beim
   Zeigen die Pille. Statt eines Rahmens liegt er auf einer Flaeche in der
   Seitenfarbe — nur dadurch bleibt er lesbar, wenn darunter ein Plakat
   durchlaeuft.
   ------------------------------------------------------------------------ */

.view-switch {
  position: fixed;
  top: var(--page-pad);
  /* Mitte des INHALTSBEREICHS, nicht des Fensters: der Inhalt reicht von
     --content-x bis zum rechten Rand, seine Mitte liegt also genau dazwischen.
     Bei den Werkseinstellungen sind das 59,5 %. Ueber dem Fenster zentriert
     staende der Knopf sonst links neben den Plakaten, ueber der
     Navigationsspalte. Dieselbe Rechnung wie bei .content — beide muessen
     zusammen wandern, wenn das Fenster schmal wird. */
  left: calc((100% + var(--content-x)) / 2);
  transform: translateX(-50%);
  z-index: 15;
}

.view-switch-button {
  display: inline-block;
  /* Der Rahmen ist im Ruhezustand durchsichtig und nur ein Platzhalter —
     sonst spraenge der Knopf beim Zeigen um zwei Pixel. Genau wie bei
     .page a / .sidebar a weiter oben. */
  border: 1px solid transparent;
  border-radius: 100px;
  padding: 0.4rem var(--pill-x);
  background: var(--body-bg, #efefef);
  color: var(--text, rgba(0, 0, 0, 0.9));
  font-size: var(--nav-fs, 1.9rem);
  line-height: 1.2;
  white-space: nowrap;
  transition: border-color 0.25s ease;
}

@media (hover: hover) {
  .view-switch-button:hover {
    border-color: currentColor;
  }
}

/* --- Video -------------------------------------------------------------- */

.block-video video {
  width: 100%;
  object-fit: cover;
}

/* --- Einbettung (iframe, z. B. Vimeo) ----------------------------------- */

.block-embed {
  position: relative;
}

.block-embed iframe {
  width: 100%;
  aspect-ratio: 16 / 9;
  height: auto;
  border: 0;
}

/* --- Spalten (12er-Raster) ---------------------------------------------- */

.block-columns {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: var(--gap, 0rem);
}

.block-columns .col {
  grid-column: span var(--span, 12);
  min-width: 0;
}

/* --- Naechstes Projekt -------------------------------------------------- */

/* Die Trennlinie ist das erste Kind des Blocks. Ihr oberer Standardabstand
   (hr { margin: 1rem 0 } aus Abschnitt 03) faellt sonst aus dem Block heraus
   und bestimmt den Abstand nach oben mit: hinter einem Abstandsblock kamen
   so 10 px an, hinter einem normalen Block 20 px. Den Abstand nach oben
   macht allein das Blocksystem; der Abstand UNTER der Linie bleibt. */
.block-next hr {
  margin-top: 0;
}

/* Der Link traegt die Pillen-Optik aus Abschnitt 05 (Rahmen, Radius,
   Innenabstand) bereits von selbst. Hier kommt nur die Groesse dazu — und
   der negative linke Aussenabstand, der den Innenabstand der Pille
   ausgleicht, damit der Text buendig unter dem uebrigen Inhalt steht. */
.block-next .next-link {
  display: inline-block;

  /* Die Zeile "Next Project" ist rund 6,5-mal so breit wie hoch. Sobald das
     Fenster schmaler wird, gibt der zweite Wert die Groesse vor — sonst braeche
     die Zeile um (gemessen ab etwa 900 px Fensterbreite auf zwei Zeilen).
     Abgezogen wird, was der Zeile nicht gehoert: die Navigationsspalte, ihr
     Abstand und der Rand der Seite. Auf breiten Fenstern gewinnt der
     CMS-Wert, dort aendert sich nichts. */
  font-size: min(
    calc(var(--h1-fs, 6.8rem) * 1.4),
    calc((100vw - var(--nav-w) - var(--nav-gap) - var(--page-pad) * 2) / 6.5)
  );
  line-height: 1.2;
  margin-left: calc(var(--pill-x) * -1);
}

/* Die Vorschau auf das naechste Projekt schwebt beim Hover unter dem
   Mauszeiger. Das Element steckt NICHT im HTML: public/js/site.js baut es
   aus den data-Attributen des Links und haengt es direkt an <body> — nur
   dort liegt es sicher ueber der Sidebar. Deshalb hier auch kein
   .block-next davor.

   pointer-events: none ist Pflicht: sonst laege die Vorschau unter dem
   Zeiger, wuerde den Hover auf dem Link unterbrechen und sofort wieder
   verschwinden — ein Flackern im Takt der Mausbewegung. */
.next-peek {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 9000;
  width: min(32rem, 34vw);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.22s ease;
}

.next-peek.is-visible {
  opacity: 1;
}

/* height: auto haelt das eigene Seitenverhaeltnis. Nur wenn ein Hochformat
   ueber 60 % der Fensterhoehe hinausginge, greift max-height — dann
   schneidet object-fit: cover zu, statt das Bild zu verzerren. */
.next-peek img,
.next-peek video {
  display: block;
  width: 100%;
  height: auto;
  max-height: 60vh;
  object-fit: cover;
  border-radius: var(--radius, 12px);
  /* Duenner Rand in der Hintergrundfarbe: er setzt die Vorschau von dem ab,
     worueber sie gerade schwebt — ohne das verschwimmt eine dunkle Ecke des
     Vorschaubildes mit einem dunklen Bild darunter.

     box-shadow statt border: der Rand liegt dadurch AUSSEN und frisst nichts
     vom Bild weg; und anders als outline folgt er in jedem Browser
     zuverlaessig dem border-radius. */
  box-shadow: 0 0 0 3px var(--body-bg, #efefef);
  transform: scale(0.94);
  transition: transform 0.3s cubic-bezier(0.2, 0.7, 0.2, 1);
}

.next-peek.is-visible img,
.next-peek.is-visible video {
  transform: scale(1);
}


/* --- Vorschau in der Navigation ------------------------------------------ */

/* Fester Platz unter "Contact". Der Rahmen steht immer und immer im selben
   Format — dadurch springt in der Sidebar nichts, wenn ein Bild erscheint
   oder verschwindet. Leer ist er unsichtbar: er hat keinen eigenen
   Hintergrund. */
.nav-peek-frame {
  position: relative;
  /* Buendig mit den Cards der Projektliste, und damit mit der linken Kante
     von "Paul Rothe" und "Contact": alles in der Spalte steht in einer
     Flucht. (Frueher sass der Rahmen um --pill-x weiter rechts, buendig mit
     dem TEXT der Links — solange es keine Cards gab, war der Text die
     einzige sichtbare Kante.)

     Die Obergrenze der Breite setzt public/js/site.js: die Sidebar
     ueberlappt den Inhaltsbereich absichtlich, und ein Bild darf da nicht
     hineinragen. In reinem CSS geht das nicht, siehe dort. Cards und Rahmen
     teilen sich dieses Mass. */
  width: 100%;
  /* Derselbe Einzug wie bei den Cards (Abschnitt 06): alles, was eine
     Flaeche ist, beginnt unter dem "P" von "Paul Rothe". */
  margin-left: calc(var(--pill-x) + 1px);
  max-width: calc(var(--nav-card-w, var(--nav-w)) - var(--pill-x) - 1px);
  aspect-ratio: 16 / 9;
  margin-top: 2rem;
  border-radius: var(--radius, 12px);
  overflow: hidden;
}

/* Alle Vorschauen liegen deckungsgleich uebereinander; sichtbar ist immer
   nur die des gerade angefahrenen Eintrags. */
.nav-peek {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  opacity: 0;
  transition: opacity 0.18s ease;
}

.nav-peek.is-visible {
  opacity: 1;
}

/* Jedes Format wird auf 16:9 beschnitten — auch ein Hochkant-Video. */
.nav-peek img,
.nav-peek video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Ohne Mauszeiger wird der Rahmen nie gefuellt — dann braucht er auch
   keinen Platz. */
@media (hover: none) {
  .nav-peek-frame {
    display: none;
  }
}


/* --- Vorschau im Inhalt: das stehende Bild als Flaeche -------------------

   Ein Bild-Block, der im CMS als Flaeche markiert ist (Schalter "Vorschau-
   Flaeche"), zeigt sein eigenes Bild — und beim Hover ueber einen Link im
   Text das Bild, das zu diesem Link gehoert. Auf der Kontaktseite ist das
   das stehende Portrait links.

   Aufbau: die <figure> traegt das Standbild, darueber liegt ein Rahmen, in
   den public/js/site.js beim ersten Hover die Vorschauen haengt. Alle
   liegen deckungsgleich uebereinander, sichtbar ist hoechstens eine. */
.peek-stage {
  position: relative;
}

/* Der Rahmen muss genau das Standbild abdecken, nicht die ganze figure:
   das Portrait steht mit object-fit: contain in seiner Spalte und laesst
   je nach Fensterform rechts Luft. Darum dieselbe Hoehe, dasselbe
   Seitenverhaeltnis (--stage-ratio schreibt lib/render.js aus den echten
   Bildmassen) und dieselbe linke Kante. */
.peek-stage-frame {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  aspect-ratio: var(--stage-ratio, 4 / 5);
  max-width: 100%;
  border-radius: var(--radius, 12px);
  overflow: hidden;
  /* Der Rahmen selbst faengt keine Maus ab — sonst koennte er einen Link
     darunter verdecken. */
  pointer-events: none;
}

.peek-stage-peek {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  opacity: 0;
  transition: opacity 0.18s ease;
}

.peek-stage-peek.is-visible {
  opacity: 1;
}

/* Feste Flaeche, das Bild wird zugeschnitten: so springt beim Wechsel von
   einem Link zum naechsten nie die Kante, auch wenn ein Bild quer liegt. */
.peek-stage-peek img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Logos sind keine Fotos: randabfallend beschnitten saehe ein Schriftzug
   zerschnitten aus. Sie stehen darum vollstaendig und mit Luft auf einer
   ruhigen Flaeche in der Hintergrundfarbe der Seite. */
.peek-stage-peek.is-logo {
  background: var(--body-bg, #efefef);
}

.peek-stage-peek.is-logo img {
  object-fit: contain;
  /* Anteilig statt in Pixeln: der Rand soll bei jeder Fenstergroesse
     gleich wirken. */
  padding: 12%;
  box-sizing: border-box;
}


/* --- Reiter unten rechts (Impressum, Datenschutz) -------------------------

   Der legal-Block haengt fest in der unteren rechten Ecke: sichtbar sind nur
   die beiden Reiter, ein Klick klappt die Karte darueber auf. Auf- und
   Zuklappen macht <details> im Browser selbst (siehe lib/render.js).

   position: fixed nimmt den Block aus dem Textfluss — darum ist es egal, an
   welcher Stelle der Seite er im JSON steht.
   ------------------------------------------------------------------------ */

.block-legal {
  position: fixed;
  right: var(--page-pad);
  bottom: var(--page-pad);
  /* Ueber Inhalt (1), Sidebar (10), Umschalter (15) und mobilem Kopf (20) —
     aber unter dem Mobil-Menue (40/50) und seinem Knopf (60). */
  z-index: 30;
  display: flex;
  align-items: flex-end;
  gap: 0.5rem;
  margin: 0;
}

/* Der Reiter traegt dieselbe Pille wie der Umschalter Galerie/Grid: Flaeche
   in der Seitenfarbe, Rahmen erst beim Zeigen. Nur dadurch bleibt er lesbar,
   egal was darunter liegt. */
.legal-tab {
  display: inline-block;
  /* Anders als der Umschalter oben (der in Freiraum steht) bekommt der
     Reiter schon im Ruhezustand eine feine Kontur: er liegt ueber dem Text,
     und ohne Rahmen wirkt seine Flaeche wie ein Loch im Absatz dahinter. */
  border: 1px solid var(--rule-soft);
  border-radius: 100px;
  padding: 0.4rem var(--pill-x);
  background: var(--body-bg, #efefef);
  color: var(--text, rgba(0, 0, 0, 0.9));
  font-size: var(--body-fs, 1.6rem);
  line-height: 1.2;
  white-space: nowrap;
  cursor: pointer;
  list-style: none; /* nimmt Firefox das Dreieck vor dem Reiter */
  transition: border-color 0.25s ease, background-color 0.25s ease,
              color 0.25s ease;
}

/* Dasselbe Dreieck bei Safari und Chrome. */
.legal-tab::-webkit-details-marker {
  display: none;
}

@media (hover: hover) {
  .legal-tab:hover {
    border-color: currentColor;
  }
}

/* Offener Reiter: schwarze Pille wie der aktive Eintrag der Navigation. */
.legal-item[open] > .legal-tab {
  background: var(--text, rgba(0, 0, 0, 0.9));
  border-color: var(--text, rgba(0, 0, 0, 0.9));
  color: var(--body-bg, #efefef);
}

/* Die Karte steht ueber der ganzen Reiter-Reihe und immer rechtsbuendig —
   egal, welcher Reiter offen ist. Bezugspunkt ist .block-legal: ein Element
   mit position: fixed ist selbst der Rahmen fuer absolut gesetzte Kinder. */
.legal-sheet {
  position: absolute;
  right: 0;
  bottom: calc(100% + 0.5rem);
  width: min(42rem, calc(100vw - 2 * var(--page-pad)));
  /* Laenger als der Bildschirm darf die Karte nie werden — der Rest wird
     in der Karte gescrollt, nicht auf der Seite. */
  max-height: min(60vh, 52rem);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 2rem;
  border: 1px solid var(--rule-soft);
  border-radius: var(--radius, 12px);
  background: var(--body-bg, #efefef);
  font-size: var(--body-fs, 1.6rem);
  animation: legal-auf 0.25s ease;
}

.legal-sheet > :first-child {
  margin-top: 0;
}

.legal-sheet p + p,
.legal-sheet h2 + p {
  margin-top: 1em;
}

.legal-sheet h2 {
  font-size: var(--body-fs, 1.6rem);
  font-weight: 700;
  margin-top: 2em;
}

/* Die Karte waechst aus dem Reiter heraus. */
@keyframes legal-auf {
  from {
    opacity: 0;
    transform: translateY(0.6rem);
  }
  to {
    opacity: 1;
    transform: none;
  }
}


/* --- Projekt-Raster: alle Projekte auf einer Seite -----------------------

   Der zweite Startbildschirm (Block "Alle Projekte", lib/render.js). Jede
   Kachel zeigt das erste Bild oder Video ihres Projekts und behaelt dessen
   Seitenverhaeltnis — die Kacheln sind also verschieden hoch und fuegen sich
   zu einer Mauer.

   Das macht `columns`: ein mehrspaltiger Textfluss, in dem jede Kachel ein
   unteilbares Stueck ist. Der Browser fuellt die Spalten selbst auf und
   raeumt beim Umschalten der Spaltenzahl neu — es rechnet also kein Skript
   mit, und ohne JavaScript sieht die Seite genauso aus.

   --cols, --mcols und --gap kommen aus den Einstellungen des Blocks, genau
   wie bei einer Galerie.
   ------------------------------------------------------------------------ */

.block-projects {
  columns: var(--cols, 3);
  column-gap: var(--gap, 0.5rem);

  /* Senkrecht steht mehr Luft als waagerecht: unter jedem Bild haengen noch
     Titel und Schlagworte, ohne Zuschlag klebte der naechste Titel am Text
     darueber. Der Wert kommt aus dem Block ("Abstand senkrecht" im CMS) und
     gilt von der letzten Textzeile bis zum naechsten Bild. */
  --card-gap: var(--row-gap, 3rem);
}

/* Der Vorsatz ".page" ist kein Zierrat: die Pillenregel aus Abschnitt 05
   heisst ".page a" und waere sonst die staerkere von beiden — die Kachel
   truege dann Rahmen, Innenabstand und Blend-Modus einer Pille. */
.page .project-card {
  display: block;
  /* Eine Kachel wird nie zwischen zwei Spalten zerschnitten. */
  break-inside: avoid;
  margin: 0 0 var(--card-gap);

  /* Um ein ganzes Bild herum waere die Pille ein Rahmen — hier bekommt sie
     nur der Titel (.project-name). */
  border: 0;
  padding: 0;
  border-radius: 0;
  mix-blend-mode: normal;
}

.project-thumb {
  display: block;
  overflow: hidden;
  border-radius: var(--radius, 12px);
  /* Ruhige Flaeche, solange ein Video noch nicht geladen ist. */
  background: rgba(0, 0, 0, 0.04);
}

.project-thumb img,
.project-thumb video {
  display: block;
  width: 100%;
  height: auto;
  transition: transform 0.45s ease;
}

/* Notfall: Ohne Breite und Hoehe im JSON kennt der Browser das Verhaeltnis
   der Kachel nicht. Dann gilt ein ruhiges 4:3, damit das Raster beim Laden
   nicht zusammenklappt. */
.project-thumb img:not([width]),
.project-thumb video:not([width]) {
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

/* Unter dem Bild steht EINE Zeile: links der Titel, rechts die Schlagworte.
   Beides in der Groesse einer Bildunterschrift (Abschnitt 04).

   Passt beides nicht nebeneinander (schmales Fenster, langer Text), rutschen
   die Schlagworte in die naechste Zeile und stehen dort links — space-between
   wirkt nur innerhalb einer Zeile. */
.project-meta {
  display: flex;
  /* Am Rechner NICHT umbrechen: statt die Schlagworte in eine zweite Zeile zu
     schieben, sollen sie schrumpfen und hinten gekuerzt werden (siehe unten).
     Am Handy wird daraus wrap, dort ist die Spalte zu schmal. */
  flex-wrap: nowrap;
  align-items: baseline;
  justify-content: space-between;
  column-gap: var(--pill-x);
  margin-top: 0.8rem;
}

.project-name {
  /* Der Titel gibt nichts ab: gekuerzt wird immer nur die Erklaerung. */
  flex: 0 0 auto;
  white-space: nowrap;
  font-size: 1rem;
  line-height: 1.3;
  color: var(--text, rgba(0, 0, 0, 0.9));

  /* Die Pille beim Zeigen liegt GENAU im Bild: ihre linke Kante fluchtet mit
     der linken Bildkante, ihr Text sitzt eine Pillenbreite weiter innen —
     dieselbe Ordnung wie bei den Cards der Navigation (Abschnitt 06). Ein
     negativer Aussenabstand wuerde den Titel zwar buendig stellen, die Pille
     raegte dann aber ueber das Bild hinaus. */
  border: 1px solid transparent;
  border-radius: var(--radius, 12px);
  padding: 2px var(--pill-x);
  transition: border-color 0.25s ease;
}

.project-tags {
  font-size: 1rem;
  line-height: 1.3;
  color: var(--caption);
  text-align: right;

  /* Am Rechner bleibt die Zeile eine Zeile: reicht der Platz neben dem Titel
     nicht, wird der Text hinten gekuerzt ("…") statt umzubrechen. Sonst
     staenden im Raster mal einzeilige, mal zweizeilige Beschriftungen
     nebeneinander. Die vollstaendigen Schlagworte stehen ohnehin auf der
     Projektseite selbst. min-width: 0 ist Pflicht — ohne das schrumpft ein
     Flex-Element nie unter seine Textbreite und kuerzt darum auch nie. */
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

@media (hover: hover) {
  /* Beim Zeigen bekommt der Titel die Pille wie jeder Link der Seite, und
     das Bild wird eine Spur groesser. Mehr braucht es nicht — die Kachel ist
     als Ganzes anklickbar. */
  .project-card:hover .project-name {
    border-color: currentColor;
  }

  .project-card:hover .project-thumb img,
  .project-card:hover .project-thumb video {
    transform: scale(1.02);
  }
}

/* Versetztes Raster ("broken grid"), Schalter im CMS.

   Die Kacheln stehen nicht alle gleich breit in ihrer Spalte: jede zweite
   ist schmaler und lehnt abwechselnd links oder rechts an. Dadurch bekommt
   die Wand eine Unruhe, die gesetzt aussieht statt gerechnet — und weil die
   Bilder dabei verschieden gross werden, entsteht auch senkrecht ein
   Versatz. Das Muster wiederholt sich alle fuenf Kacheln.
   ------------------------------------------------------------------------ */

.page .block-projects[data-broken="1"] .project-card:nth-child(5n + 2) {
  width: 78%;
  margin-left: auto;
}

.page .block-projects[data-broken="1"] .project-card:nth-child(5n + 3) {
  width: 92%;
}

.page .block-projects[data-broken="1"] .project-card:nth-child(5n + 4) {
  width: 86%;
  margin-left: auto;
}

.page .block-projects[data-broken="1"] .project-card:nth-child(5n + 5) {
  width: 70%;
}


/* ==========================================================================
   09  Sketch-Hintergrund

   Wichtig (das war der Fehler im Original): der Container liegt auf
   z-index 0 — NICHT auf -1 — und behaelt pointer-events. Nur so laesst sich
   der Sketch auf dem Handy antippen. Die Sidebar liegt mit z-index 10
   darueber, der Inhalt mit z-index 1.

   Das Vollbild haengt an der Startseite (body.tpl-home / .content.is-home),
   nicht am Block selbst — sonst wuerde ein versehentlich eingefuegter
   Sketch jede beliebige Seite zudecken.
   ========================================================================== */

/* Standard: ein normaler Block im Textfluss. Landet ein Sketch-Block
   versehentlich auf einer Unterseite, legt er so KEIN bildschirmfuellendes
   Canvas ueber die Seite (das wuerde das Scrollen an den Raendern
   blockieren). Die Hoehe kommt aus dem Seitenverhaeltnis, gedeckelt auf
   80 % der Fensterhoehe. */
.block-sketch {
  position: relative;
  z-index: 0;
  margin: 0;
  width: 100%;
  aspect-ratio: 3 / 4;
  max-height: 80vh;
  pointer-events: auto;
  overflow: hidden;
}

/* Nur auf der Startseite: Vollbild hinter dem Inhalt. */
body.tpl-home .block-sketch,
.content.is-home .block-sketch {
  position: fixed;
  inset: 0;
  aspect-ratio: auto;
  max-height: none;
}

.block-sketch #sketch-holder {
  width: 100%;
  height: 100%;
}

.block-sketch canvas {
  display: block;
  width: 100% !important;
  height: 100% !important;

  /* Verhindert Ein-Finger-Wischen und Doppeltipp-Zoom auf dem Canvas, laesst
     aber das Aufziehen mit zwei Fingern zu. "none" wuerde das Vergroessern
     der ganzen Startseite sperren — ein Barrierefreiheits-Problem. */
  touch-action: pinch-zoom;
}


/* ==========================================================================
   10  Entwurfs-Banner
   Wird eingeblendet, wenn eine unveroeffentlichte Seite in der Vorschau
   geoeffnet wird.
   ========================================================================== */

.draft-banner {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  background: #ffe600;
  color: #000;
  font-size: 1.3rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-align: center;
  padding: 0.8rem 1.2rem;
  border-bottom: 1px solid rgba(0, 0, 0, 0.9);
}


/* ==========================================================================
   11  Lightbox (optional)
   Struktur wird von public/js/site.js erzeugt.
   ========================================================================== */

.lightbox {
  position: fixed;
  inset: 0;
  /* Muss ueber dem Entwurfs-Banner (z-index 100) liegen. */
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 3rem;
  background: rgba(20, 20, 20, 0.95);
  cursor: zoom-out;
}

.lightbox img {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  border-radius: var(--radius, 12px);
}


/* ==========================================================================
   11b  Cookie-Hinweis und Vorhang vor fremden Einbettungen

   Zwei Teile, die zusammengehoeren:

     .cookie-note    Der Kasten unten links. Er fragt einmal, merkt sich die
                     Antwort und ist danach weg. public/js/site.js baut ihn
                     (Abschnitt 10), hier steht nur, wie er aussieht.
     .embed-shield   Der Deckel ueber einem Video, das noch nicht geladen
                     ist. Den baut lib/render.js schon beim Rendern, damit
                     er auch ohne JavaScript dasteht.

   Beide sprechen die Sprache der Seite: Flaeche in der Seitenfarbe, ein Haar
   von Rahmen, Pillen als Knoepfe. Erfunden wird hier nichts — die Werte
   stehen alle in den Design-Tokens (Abschnitt 02).

   Die Mobilregeln stehen ausnahmsweise gleich hier statt in Abschnitt 12:
   der Hinweis ist ein Zusatz und laesst sich so an einem Stueck lesen — und
   an einem Stueck wieder herausnehmen, falls er je wegsoll.
   ========================================================================== */

.cookie-note {
  position: fixed;
  left: var(--page-pad);
  bottom: var(--page-pad);
  /* Ueber den Reitern Impressum/Datenschutz (30), aber unter dem Mobil-Menue
     (40): ist das Menue offen, gehoert der Bildschirm ihm allein. */
  z-index: 35;
  width: min(36rem, calc(100vw - 2 * var(--page-pad)));
  padding: 1.8rem 2rem;
  border: 1px solid var(--rule-soft);
  border-radius: var(--radius, 12px);
  background: var(--body-bg, #efefef);
  color: var(--text, rgba(0, 0, 0, 0.9));
  font-size: var(--body-fs, 1.6rem);
  line-height: 1.3;
  /* Dieselbe Kurve wie beim Einschweben der Bilder (Abschnitt 15): zuegiger
     Start, weiches Ankommen. Der Kasten faehrt von unten herein, als waere
     er die ganze Zeit schon da gewesen. */
  animation: cookie-auf 0.45s cubic-bezier(0.2, 0.7, 0.2, 1);
}

/* Kein display am Kasten selbst — sonst stiche es das hidden-Attribut aus,
   mit dem das Skript ihn wegraeumt. */

.cookie-title {
  margin: 0;
  font-weight: 700;
}

.cookie-text {
  margin: 0.6em 0 0;
}

.cookie-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem;
  margin: 1.4rem 0 0;
}

/* Eigene Zeile, kleiner gesetzt: ein Angebot, keine dritte Wahlmoeglichkeit. */
.cookie-more {
  margin: 1.2rem 0 0;
  font-size: 0.85em;
}

/* Die Knoepfe sind dieselbe Pille wie die Reiter unten rechts (Abschnitt 08)
   — hier nur als <button>, der seine Schrift nicht von allein erbt. */
.cookie-btn,
.embed-shield-load {
  font: inherit;
  line-height: 1.2;
  color: inherit;
  background: transparent;
  border: 1px solid var(--rule-soft);
  border-radius: 100px;
  padding: 0.5rem var(--pill-x);
  cursor: pointer;
  transition: border-color 0.25s ease, background-color 0.25s ease,
              color 0.25s ease;
}

/* Die Zustimmung traegt die schwarze Pille — wie der offene Reiter und wie
   der aktive Eintrag der Navigation. */
.cookie-btn.is-ja,
.embed-shield-load {
  background: var(--text, rgba(0, 0, 0, 0.9));
  border-color: var(--text, rgba(0, 0, 0, 0.9));
  color: var(--body-bg, #efefef);
}

@media (hover: hover) {
  .cookie-btn:hover,
  .embed-shield-load:hover {
    border-color: currentColor;
  }
}

/* Der Verweis auf den Datenschutz steht bewusst leiser als die Knoepfe: er
   ist ein Angebot, keine dritte Wahlmoeglichkeit. */
.cookie-link,
.embed-shield-link {
  color: var(--small, rgba(0, 0, 0, 0.6));
  text-decoration: none;
  border-bottom: 1px solid var(--rule-soft);
  transition: color 0.25s ease, border-color 0.25s ease;
}

@media (hover: hover) {
  .cookie-link:hover,
  .embed-shield-link:hover {
    color: var(--text, rgba(0, 0, 0, 0.9));
    border-bottom-color: currentColor;
  }
}

/* Abgang: derselbe Weg wie der Auftritt, nur rueckwaerts. Das Skript nimmt
   den Kasten erst danach aus dem Weg (hidden), damit man das Wegblenden
   auch sieht. */
.cookie-note.is-weg {
  opacity: 0;
  transform: translateY(1rem);
  pointer-events: none;
  transition: opacity 0.3s ease,
              transform 0.4s cubic-bezier(0.2, 0.7, 0.2, 1);
}

@keyframes cookie-auf {
  from {
    opacity: 0;
    transform: translateY(1.2rem);
  }
  to {
    opacity: 1;
    transform: none;
  }
}


/* --- Vorhang vor einem fremden Video ------------------------------------ */

/* Solange der Vorhang haengt, ist das Video gar nicht da.

   Der erste Versuch legte den Vorhang als Deckel ueber das leere iframe —
   damit blieb die Seite zwar exakt gleich hoch, aber mitten im Projekt stand
   ein 16:9 grosses graues Feld mit drei Zeilen Text unten links. Das sah aus
   wie ein Bild, das nicht geladen hat.

   Jetzt steht an der Stelle eine schmale Karte: so hoch wie ihr Text, mit
   demselben Rahmen und derselben Ecke wie alles andere. Sie sieht aus wie
   das, was sie ist — eine Frage. Beim Zustimmen waechst die Stelle auf
   Videogroesse; das passiert nur auf Klick, es springt also nichts
   ungefragt. */
.block-embed[data-consent="extern"] iframe {
  display: none;
}

.embed-shield {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  /* Text links, Knoepfe rechts — auf schmalen Kaesten rutschen sie
     untereinander, dafuer das flex-wrap. */
  justify-content: space-between;
  gap: 1.2rem 2rem;
  padding: 1.8rem 2rem;
  border: 1px solid var(--rule-soft);
  border-radius: var(--radius, 12px);
  background: var(--body-bg, #efefef);
  font-size: var(--body-fs, 1.6rem);
  line-height: 1.3;
}

.embed-shield-text {
  margin: 0;
  max-width: 52rem;
  color: var(--text, rgba(0, 0, 0, 0.9));
}

.embed-shield-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem 1.2rem;
  margin: 0;
}

/* Der Link steht im Inhalt und bekaeme sonst die Pille aus Abschnitt 05 —
   mitsamt mix-blend-mode. Auf hellem Grund wird dunkler Text dadurch fast
   weiss: der Link war da, aber nicht zu sehen. */
.page .embed-shield-link {
  mix-blend-mode: normal;
  color: var(--small, rgba(0, 0, 0, 0.6));
  border: 0;
  border-bottom: 1px solid var(--rule-soft);
  border-radius: 0;
  padding: 0;
  margin-inline: 0;
}

/* Das Video kommt nicht sprunghaft, sondern blendet auf. Die Regel gilt nur
   fuer frisch freigegebene Einbettungen — wer laengst zugestimmt hat, sieht
   sie beim normalen Laden der Seite gar nicht erst. */
.block-embed[data-consent="geladen"] iframe {
  animation: embed-auf 0.4s ease;
}

@keyframes embed-auf {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

/* Ohne JavaScript kann der Knopf nichts laden — dann bleibt der Link zum
   Dienst, und der fuehrt ans Ziel. Lieber ein Weg weniger als ein toter
   Knopf. */
@media (scripting: none) {
  .embed-shield-load {
    display: none;
  }
}


/* --- Handy -------------------------------------------------------------- */

@media (max-width: 768px) {
  /* Der Kasten nimmt die volle Breite: bei 390 px waere eine Spalte mit Luft
     daneben nur ein schmaler Streifen Text. */
  .cookie-note {
    left: var(--page-pad);
    right: var(--page-pad);
    width: auto;
    padding: 1.6rem;
  }

  /* Volle Antippflaeche, wie bei den Reitern (Abschnitt 12). */
  .cookie-btn,
  .embed-shield-load {
    display: inline-flex;
    align-items: center;
    min-height: var(--tap);
  }

  /* Auf dem Handy steht ohnehin alles untereinander — dann darf die Karte
     auch enger sitzen. */
  .embed-shield {
    gap: 1rem;
    padding: 1.4rem 1.6rem;
  }
}


/* ==========================================================================
   12  Mobile (max-width: 768px)
   ========================================================================== */

@media (max-width: 768px) {

  :root {
    /* Groesse und Zeilenhoehe des Titels im mobilen Kopf. Bewusst in
       Variablen: drei Stellen rechnen damit — der Titel selbst, der
       Menue-Knopf (er ist genau eine Titelzeile hoch und fluchtet dadurch
       mit der Schrift) und der obere Innenabstand des Inhalts. */
    --mobile-title-fs: 11.7vmin;
    --mobile-title-lh: 1.1;
    /* Breite der Strichgruppe im Menue-Knopf, am Titel ausgerichtet:
       etwa so breit wie ein Grossbuchstabe hoch ist. */
    --menu-icon-w: calc(var(--mobile-title-fs) * 0.62);
  }

  /* Sidebar weg — stattdessen fester Kopf und Overlay-Menue. */
  .sidebar {
    display: none;
  }

  .mobile-head {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 20;
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1rem;
    padding: 1rem 1.5rem 3rem;
    pointer-events: none; /* Klicks gehen durch, ausser auf Titel/Button */

    /* Der Titel steht fest, der Inhalt scrollt darunter weg — ohne diese
       Flaeche laeuft er sichtbar durch die Buchstaben. Der Verlauf blendet
       nach unten aus, damit keine harte Kante quer ueber die Seite liegt.
       Die Farbe ist dieselbe wie der Seitenhintergrund. */
    background: linear-gradient(to bottom,
      var(--body-bg, #efefef) 0%,
      var(--body-bg, #efefef) 70%,
      transparent 100%);
  }

  .mobile-title {
    margin: 0;
    font-size: var(--mobile-title-fs);
    line-height: var(--mobile-title-lh);
    font-weight: 400;
    pointer-events: auto;
  }

  .mobile-title a {
    color: inherit;
    text-decoration: none;
  }

  /* Menue-Knopf: nur drei Striche, keine Kreisflaeche und keine Icon-Schrift.

     Er steht als zweites Kind IM Kopf (nicht mehr fest am Fenster) und ist
     genau eine Titelzeile hoch. Dadurch liegt seine Mitte auf der Mitte der
     Zeile "Paul Rothe" — und weil der Kopf links wie rechts 1.5rem Rand hat,
     fluchtet er zugleich mit der Schrift und mit dem Seitenrand des Inhalts.
     Beides rechnet sich aus der Titelgroesse, stellt man sie um, wandert der
     Knopf von allein mit.

     Ohne Kreis fehlt die deckende Flaeche, die den Knopf frueher ueber jedem
     Bild lesbar hielt. Den Job macht jetzt die Verlaufsflaeche des Kopfes:
     sie liegt in der Seitenfarbe hinter Titel UND Knopf. */
  .menu-button {
    position: relative;
    z-index: 60;
    flex: 0 0 auto;
    width: var(--menu-icon-w);
    height: calc(var(--mobile-title-fs) * var(--mobile-title-lh));
    border: 0;
    background: none;
    color: var(--text, rgba(0, 0, 0, 0.9));
    pointer-events: auto;
  }

  /* Die drei Striche, mittig in der Zeilenhoehe. */
  .menu-button::before {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 100%;
    height: calc(var(--mobile-title-fs) * 0.38);
    transform: translate(-50%, -50%);
    background-image: linear-gradient(currentColor, currentColor),
      linear-gradient(currentColor, currentColor),
      linear-gradient(currentColor, currentColor);
    background-size: 100% 2px;
    background-position: left top, left center, left bottom;
    background-repeat: no-repeat;
  }

  /* Unsichtbare Trefferflaeche: die Striche und das Kreuz allein waeren zu
     kleine Ziele fuer einen Finger. Die sichtbare Groesse bleibt davon
     unberuehrt — nur so bleibt die Flucht mit der Schrift exakt. */
  .menu-button::after,
  .menu-close::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: max(var(--tap), 100%);
    height: max(var(--tap), 100%);
    transform: translate(-50%, -50%);
  }

  /* Ist das Menue offen, liegt der Button unter dem Overlay. */
  .menu-button[aria-expanded="true"] {
    z-index: 40;
  }

  /* Inhalt: volle Breite, Platz fuer den festen Kopf. */
  .content,
  body.tpl-home .content,
  .content.is-home {
    width: 100%;
    margin-left: 0;
  }

  .page {
    padding: calc(var(--mobile-title-fs) + 3rem) 1.5rem 4rem;
    min-height: 0;
  }

  body.tpl-home .page,
  .content.is-home .page {
    padding: calc(var(--mobile-title-fs) + 3rem) 1.5rem 4rem;
  }

  /* Umschalter Galerie/Grid: auf dem Handy NICHT schwebend. Oben rechts
     sitzt bereits der Menue-Knopf, dort waere er sonst darunter. Er steht
     deshalb rechtsbuendig im Textfluss, direkt ueber der Galerie. */
  /* Hier schwebt der Umschalter nicht, also braucht die Seite auch keinen
     Platz fuer ihn freizuhalten. */
  .page:has(> .view-switch) {
    padding-top: calc(var(--mobile-title-fs) + 3rem);
  }

  .view-switch {
    position: static;
    transform: none;
    display: flex;
    justify-content: center;
    /* Der Menue-Knopf endet 2 px ueber dem Inhalt. Ohne den Abstand klebten
       die beiden Pillen aneinander. */
    margin: 1.5rem 0 1.5rem;
  }

  /* Galerieansicht: die Groesse haengt hier an der Fensterbreite, nicht an
     der Hoehe — sonst waere ein Mockup auf einem hohen, schmalen Schirm
     breiter als das Display. Die Reihe ist so hoch wie ihr hoechster
     Eintrag, gewischt wird waagerecht. */
  .block-gallery[data-view="gallery"] {
    /* Hier gibt die Fensterbreite die Groesse vor, nicht die Hoehe: auf einem
       hohen, schmalen Display waere ein aus der Hoehe gerechnetes Mockup
       breiter als das Handy. Die Hoehe des Eintrags rechnet sich daraus von
       selbst, die Reihe ist so hoch wie ihr hoechster Eintrag. */
    --mock-w: 84vw;
    height: auto;
    /* Der Abstand bleibt ein Fuenftel der Plakatbreite, wie am grossen
       Schirm — die Regel oben rechnet ihn aus --mock-w, hier steht darum
       nichts mehr. */
  }

  /* Ohne feste Reihenhoehe gibt es auch keine 100 % fuer den Eintrag — er
     ist hier so hoch wie sein Inhalt. */
  .block-gallery[data-view="gallery"] .gallery-item {
    height: auto;
  }

  /* Prozentwerte beziehen sich auf die Breite des umgebenden Blocks: auf dem
     Handy ragen die Link-Pillen damit ueber den Seitenrand hinaus. Feste
     Werte halten den negativen Rand und den Innenabstand im Gleichgewicht. */
  /* Auf schmalen Schirmen die Pille etwas enger, damit lange Links nicht
     umbrechen. */
  .page a,
  .mobile-menu a,
  .mobile-menu-inner {
    --pill-x: 0.9rem;
  }

  /* Overlay-Menue: dieselbe Flaeche wie die Seitennavigation am Rechner —
     Grundfarbe der Seite, dunkle Schrift, gleiche Pillen. Bewusst DECKEND:
     scheint der Inhalt durch, kaempfen Bilder und Menuetext um dieselbe
     Flaeche und beides wird unruhig. */
  .mobile-menu {
    position: fixed;
    inset: 0;
    z-index: 50;
    background: var(--menu-overlay, #efefef);
    color: var(--menu-text);
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
    padding: 8rem 3rem 6rem;
  }

  /* Sitzt genau auf dem Menue-Knopf und sieht aus wie er — beim Oeffnen
     wechselt also nur das Zeichen, nichts springt. Dieselbe Rechnung wie
     dort: oben der Innenabstand des Kopfes, Hoehe eine Titelzeile. */
  .menu-close {
    position: fixed;
    top: 1rem;
    right: 1.5rem;
    z-index: 60;
    width: var(--menu-icon-w);
    height: calc(var(--mobile-title-fs) * var(--mobile-title-lh));
    display: flex;
    align-items: center;
    justify-content: center;
    background: none;
    border: 0;
    font-size: calc(var(--mobile-title-fs) * 0.7);
    line-height: 1;
    color: var(--menu-text, rgba(255, 255, 255, 0.75));
  }

  /* Folgt der Groesse der Navigation aus den Einstellungen — stellst du sie
     dort um, wandert das Menue mit. */
  .mobile-menu-inner {
    font-size: var(--nav-fs, 2.2rem);
    line-height: 1.2;
  }

  /* Wie am Rechner (Abschnitt 06): derselbe Schnitt fuer alles, die GROESSE
     gliedert. "Contact" und der Knopf zur Uebersicht sind Ueberschriften und
     stehen darum eine Stufe ueber der Projektliste — am Rechner macht das
     --h2-fs, hier ebenso.

     Die Obergrenze in vw ist der Handy-Anteil: die Groesse kommt aus dem CMS
     und laesst sich dort hochsetzen. Ohne Grenze braeche "All Projects" auf
     einem 320-px-Geraet auf zwei Zeilen um. (Dieselbe Rechnung wie bei
     "Next Project" weiter unten.) */
  .mobile-menu .site-title,
  .mobile-menu .nav-contact,
  .mobile-menu .site-index {
    font-size: min(var(--h2-fs, 4.2rem), 11vw);
    font-weight: 400;
    line-height: 1.2;
  }

  .mobile-menu .nav-group,
  .mobile-menu .nav-social {
    list-style: none;
    margin: 0;
    padding: 0;
  }

  /* Auch hier trennt Luft, kein Strich (siehe Basisregel bei hr). */
  .mobile-menu hr {
    margin: 1rem 0;
  }

  /* Grosszuegige Touch-Ziele.

     Der Innenabstand ist derselbe wie am Rechner (die Pillenbreite) — er ist
     jetzt zu sehen: die Eintraege liegen auf grauen Flaechen, und der Text
     darf nicht an deren Kante kleben. Zugleich fluchten dadurch die
     Social-Links (ohne Flaeche) mit dem Text auf den Flaechen darueber. */
  .mobile-menu a {
    display: flex;
    align-items: center;
    min-height: var(--tap);
    color: var(--menu-text);
    margin: 0;
    padding: 2px var(--pill-x);
    border-color: transparent;
    mix-blend-mode: normal;
  }

  /* --- Cards wie in der Seitennavigation (Abschnitt 06) -------------------

     Jeder Eintrag der Projektliste liegt auch am Handy auf einer eigenen
     grauen Flaeche, und der Pfeil steht an deren rechter Kante statt direkt
     hinter dem Namen. Dieselbe Form, dieselbe Farbe, dieselbe Ecke wie am
     Rechner — nur ueber die volle Breite des Menues statt ueber die Spalte.

     align-items: center statt baseline (so steht es am Rechner): die Flaeche
     ist hier mindestens einen Finger hoch (--tap), und bei Ausrichtung an
     der Schriftlinie saesse der Text dann am oberen Rand der Flaeche.
     ---------------------------------------------------------------------- */
  .mobile-menu .nav-group .nav-link,
  .mobile-menu .nav-contact a,
  .mobile-menu .index-link {
    justify-content: space-between;
    gap: var(--pill-x);
    border-radius: var(--radius, 12px);
  }

  /* Das Grau: dieselbe Rechnung wie am Rechner (Abschnitt 06) — ein Hauch
     dunkler als der Hintergrund, aus der Hintergrundfarbe gemischt, damit es
     jeder Farbe aus dem CMS folgt. Die Zeile davor ist der feste Wert fuer
     Browser ohne color-mix.

     Der aktive Eintrag bleibt aussen vor, er bringt seine schwarze Fuellung
     selbst mit (siehe unten). Die Social-Links bleiben schlichter Text: sie
     stehen am Rechner ebenfalls ohne Flaeche. */
  .mobile-menu .nav-group .nav-link:not(.active),
  .mobile-menu .nav-contact a:not(.active),
  .mobile-menu .index-link:not(.active) {
    background: #e8e8e8;
    background: color-mix(in srgb, var(--body-bg, #efefef) 97%, #000);
  }

  /* Das geschuetzte Leerzeichen vor dem Pfeil (Abschnitt 05) haelt Label und
     Pfeil zusammen, solange beide nebeneinander stehen. Auf der Card trennt
     sie das Flex-Raster — dort waere es nur eine zusaetzliche Luecke. */
  .mobile-menu .nav-group .nav-link > .arrow::before {
    content: none;
  }

  /* Luft zwischen den Flaechen: ohne sie stossen sie aneinander und die
     runden Ecken verschwinden in einer durchgehenden grauen Spalte. */
  .mobile-menu .nav-group li + li {
    margin-top: 0.2rem;
  }

  /* Der Knopf zur Uebersicht steht am Handy nicht neben "Paul Rothe" (dort
     ist der Menue-Knopf), sondern als erster Eintrag ueber der Liste. Er ist
     der uebergeordnete Weg zu allen Projekten — also so gross wie "Contact"
     und nicht, wie am Rechner, eine Stufe kleiner als die Liste. Die
     Groesse steht oben bei den Ueberschriften, hier nur der Abstand.

     Das margin-left: auto der Basisregel (Abschnitt 06) muss weg: es schoebe
     den Absatz an den rechten Rand, sobald er schmaler als das Menue waere. */
  .mobile-menu .site-index {
    margin: 0 0 0.6rem;
  }

  /* Dieselbe schwarze Flaeche wie in der Seitennavigation — und dieselbe
     Ecke (Abschnitt 05): am Rechner ist der Eintrag, auf dem man steht, eine
     schwarze Card ueber die volle Breite. Im Menue jetzt genauso; vorher lag
     die Fuellung nur um den Text und sass damit als einzige Form quer in der
     Reihe der Flaechen. */
  .mobile-menu .nav-link.active,
  .mobile-menu .nav-contact a.active {
    background: var(--text, rgba(0, 0, 0, 0.9));
    color: var(--body-bg, #efefef);
    border-color: var(--text, rgba(0, 0, 0, 0.9));
    border-radius: var(--radius, 12px);
  }

  /* Am Rechner darf ein Bild schmaler als die Spalte stehen ("Breite" im
     Bildblock). Am Handy ist die Spalte schon schmal — 47 % davon waeren
     eine Briefmarke. Hier fuellt jedes Bild die Breite. */
  .block-image {
    width: 100%;
  }

  /* Das Projekt-Raster nimmt die Spaltenzahl fuer Mobilgeraete (ab Werk 1).
     Der Versatz entfaellt: auf 390 px Breite waere eine Kachel mit 70 %
     Breite eine Briefmarke, und nebeneinander steht dort ohnehin nichts. */
  .block-projects {
    columns: var(--mcols, 1);
  }

  /* Am Handy ist die Spalte zu schmal fuer beides in einer Zeile: dort
     rutschen die Schlagworte unter den Titel und stehen vollstaendig da,
     statt auf ein paar Woerter gekuerzt zu werden. */
  .project-meta {
    flex-wrap: wrap;
  }

  .project-tags {
    white-space: normal;
    overflow: visible;
    text-align: left;
  }

  /* Das ":nth-child(n)" trifft jede Kachel und ist nur dafuer da, genauso
     stark zu zaehlen wie die Regeln des Versatzes weiter oben — ohne das
     bliebe dort die Breite von 70 % stehen. Ein Medienabschnitt gewinnt
     nicht von allein, er zaehlt beim Gewicht nicht mit. */
  .page .block-projects[data-broken="1"] .project-card:nth-child(n) {
    width: 100%;
    margin-left: 0;
  }

  /* Spalten stapeln sich. Das 12-spurige Raster muss dabei mit weichen:
     sonst bleiben 11 Spaltenluecken stehen (bei "gap": 5 sind das 11 x 50px)
     und die Seite scrollt seitwaerts.

     Die Zeilenluecke kommt NICHT aus dem Feld "Abstand" des Blocks: das ist
     ein waagerechter Wert und steht auf vielen Seiten auf 0, weil am Rechner
     nichts untereinander liegt. Gestapelt klebten Text und Liste dann
     aneinander.

     Sie ist derselbe schmale Abstand wie zwischen zwei Bildern und zwischen
     zwei Karten. Vorher stand hier der groessere Blockabstand — untereinander
     lagen Text- und Stichwortkarte dadurch 20 px auseinander, waehrend alles
     andere auf der Seite 5 px hat. Am Handy ist das die haeufigste Stelle
     ueberhaupt: jede Projektseite stapelt dort Text und Liste. */
  .block-columns {
    grid-template-columns: minmax(0, 1fr);
    column-gap: 0;
    row-gap: var(--media-gap);
  }

  /* Spalten ohne Inhalt sind waagerechte Abstandhalter (ARCHITECTURE.md §9).
     Gestapelt waeren sie eine leere Zeile samt Zeilenluecke — also weg.
     ":empty" reicht nicht: in den Spalten steht der Zeilenumbruch der
     Einrueckung. */
  .block-columns .col:not(:has(> *)) {
    display: none;
  }

  /* Ueber die eine verbliebene Spur — nicht "span 12", das wuerde 11 leere
     Spuren nachwachsen lassen. */
  .block-columns .col {
    grid-column: 1 / -1;
  }

  /* Galerien nutzen die Mobil-Spaltenzahl. Der Renderer schreibt --cols UND
     --mcols als Inline-Style auf das Element; eine Regel, die nur --cols
     umbiegt, koennte die Inline-Deklaration nie schlagen. Darum hier direkt
     die Spalten setzen. */
  .block-gallery[data-layout="grid"] {
    grid-template-columns: repeat(var(--mcols, 1), minmax(0, 1fr));
  }

  .block-gallery[data-layout="slideshow"] {
    grid-auto-columns: minmax(0, 88%);
  }

  /* "Next Project" eine Stufe kleiner: in Desktop-Groesse (h1 x 1.4) waere
     die Zeile auf 390 px Breite schon randvoll.

     Dazu eine Obergrenze in vw. Die Ueberschriftgroesse kommt aus dem CMS
     und laesst sich dort jederzeit hochsetzen — ohne die Grenze braeche
     "Next Project" dann auf zwei Zeilen um. Die Zeile ist rund 6,5-mal so
     breit wie hoch, 13vw passt damit auch auf 320 px Geraete. */
  .block-next .next-link {
    font-size: min(var(--h1-fs, 6.8rem), 13vw);
  }

  .lightbox {
    padding: 1.5rem;
  }

  /* Reiter Impressum/Datenschutz: am Handy NICHT schwebend. Fest unten rechts
     lagen sie quer ueber dem Text, durch den man gerade scrollt. Sie stehen
     deshalb rechtsbuendig am Ende der Seite im normalen Fluss — genau wie der
     Umschalter Galerie/Grid weiter oben. */
  .block-legal {
    position: static;
    right: auto;
    bottom: auto;
    flex-wrap: wrap;
    justify-content: flex-end;
    margin-top: 4rem;
  }

  .legal-tab {
    display: inline-flex;
    align-items: center;
    min-height: var(--tap);
  }

  /* Der offene Reiter bekommt eine eigene Zeile, die Karte steht darunter im
     Textfluss.

     Am Desktop haengt die Karte per `position: absolute` ueber den Reitern;
     ihr Bezugspunkt ist dort .block-legal, weil der Block fest am Fenster
     klebt. Hier steht der Block im Fluss und ist damit KEIN Bezugspunkt mehr:
     die Karte richtete sich am naechsten positionierten Vorfahren aus und
     landete 190 px oberhalb der Seite — aufgeklappt, aber unsichtbar. Darum
     im Fluss statt schwebend. */
  .legal-item[open] {
    flex: 1 0 100%;
    text-align: right;   /* haelt den Reiter rechtsbuendig */
  }

  .legal-sheet {
    position: static;
    width: 100%;
    margin-top: 1rem;
    text-align: left;
    /* Im Fluss darf die Karte so lang werden, wie ihr Text es braucht —
       gescrollt wird die Seite, nicht die Karte. */
    max-height: none;
    overflow: visible;
    padding: 1.5rem;
  }
}


/* ==========================================================================
   13  Nur Desktop

   Bewusst "not all and (max-width: 768px)" statt "min-width: 769px": das ist
   exakt die Gegenmenge zum Mobil-Block. Zwischenwerte (768.5px bei
   Browser-Zoom) fielen sonst durch beide Raster — dort waere weder die
   Sidebar ausgeblendet noch der mobile Kopf sichtbar.
   ========================================================================== */

@media not all and (max-width: 768px) {
  .mobile-head,
  .mobile-menu {
    display: none;
  }

  /* FRUEHER stand hier eine Regel, die die Liste in der zweiten Spalte um
     eine Ueberschriftenzeile nach unten schob: ohne Karten sollte der erste
     Stichpunkt auf Hoehe der ersten Textzeile beginnen und nicht neben der
     grossen Ueberschrift.

     Mit den Textkarten ist das falsch geworden. Jetzt ist die Karte die
     sichtbare Einheit, und zwei Karten nebeneinander muessen oben auf einer
     Linie sitzen — sonst haengt die rechte im Raster der Bilder schief.
     Der Ausgleich passiert nun von selbst: die Ueberschrift steht INNERHALB
     derselben Karte wie der Text. */
}


/* ==========================================================================
   14  Kontaktseite: Bild steht, Informationen scrollen

   Links das Portrait ueber die volle Fensterhoehe, rechts die Informationen.
   Das Bild bleibt beim Scrollen stehen — dafuer reicht position: sticky.
   Ein eigener Scroll-Container fuer die rechte Spalte waere die schlechtere
   Loesung: die Seite haette dann zwei Scrollflaechen, und Tastatur wie
   Trackpad-Schwung wuerden sich an deren Rand verhaken.

   Alles hier haengt an body[data-slug="information"] und am Desktop-Raster,
   damit weder eine andere Seite noch die Mobil-Ansicht etwas abbekommt.
   ========================================================================== */

@media not all and (max-width: 768px) {
  /* Ohne den oberen Innenabstand sitzt das Bild vom ersten Moment an genau
     am Fensterrand. Sonst ruckte es beim ersten Scrollen um 3rem nach oben,
     bevor es kleben bleibt. */
  body[data-slug="information"] .page {
    padding-top: 0;
    padding-bottom: 0;
  }

  /* Das 12er-Raster wird hier durch zwei echte Spalten ersetzt.
     Die Bildspalte ist so breit, wie das Portrait bei voller Fensterhoehe
     ist (1449 : 1812) — nur so fuellt es den Bildschirm wirklich von oben
     bis unten. Bleiben der Textspalte dabei weniger als 26rem, gibt die
     Bildspalte nach: minmax(0, …) erlaubt ihr zu schrumpfen, die 26rem sind
     die Untergrenze der Textspalte. Auf sehr flachen Fenstern wird das Bild
     dann kleiner statt die Informationen unlesbar schmal.

     align-items: start ist der Punkt, an dem sticky haengt — ohne das streckt
     das Raster die Bildspalte auf die Hoehe der langen Textspalte, und ein
     Element, das ohnehin schon die ganze Zeile fuellt, kann nirgends kleben. */
  body[data-slug="information"] .page > .block-columns {
    grid-template-columns: minmax(0, calc(100vh * 1449 / 1812)) minmax(26rem, 1fr);

    /* Derselbe schmale Abstand wie zwischen zwei Bildern und zwischen zwei
       Karten. Vorher standen hier 4rem — das Portrait und die Karte daneben
       sassen dadurch als Einzige der ganzen Seite 40 px auseinander. */
    column-gap: var(--media-gap);
    align-items: start;
  }

  /* Der Renderer schreibt --span aus dem CMS ins Raster ("span 6"). Bei nur
     zwei Spuren wuerde das sechs erfinden — darum hier zurueck auf "auto",
     die Spalten stehen einfach der Reihe nach. */
  body[data-slug="information"] .page > .block-columns > .col {
    grid-column: auto;
  }

  body[data-slug="information"] .page > .block-columns > .col:first-child {
    position: sticky;
    top: 0;
    height: 100vh;
    height: 100dvh;
  }

  /* --scale wuerde die figure in Prozent der Spalte legen; hier soll sie
     schlicht die Spalte fuellen. */
  body[data-slug="information"] .page > .block-columns > .col:first-child .block-image {
    width: 100%;
    height: 100%;
  }

  /* height: 100% schlaegt das "height: auto" der Medien-Grundregel — sonst
     richtete sich das Bild nach seinem Seitenverhaeltnis und waere kuerzer
     als das Fenster. "contain" haelt das ganze Portrait im Bild; beschnitten
     wird nie.

     "> img" ist wichtig: gemeint ist allein das Standbild der figure. Die
     Vorschaubilder der Links liegen tiefer (.peek-stage-frame >
     .peek-stage-peek > img) und sollen beschnitten werden, nicht
     eingepasst — sonst bliebe bei jedem Bild mit anderem Seitenverhaeltnis
     ein Streifen des Portraits daneben stehen. */
  body[data-slug="information"] .page > .block-columns > .col:first-child .block-image > img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    object-position: left center;
  }

  /* Die Textspalte bekommt den Innenabstand zurueck, den die Seite oben
     abgegeben hat: oben faengt sie damit auf Hoehe des Bildes an, unten
     klebt die letzte Zeile nicht am Fensterrand. */
  body[data-slug="information"] .page > .block-columns > .col:nth-child(2) {
    padding: var(--page-pad) 0;
  }

  /* Diese Seite gibt den Innenabstand der Seite ab (siehe oben), deshalb
     verpufft hier die allgemeine Regel .page:has(.block-legal). Den Platz
     fuer die angepinnten Reiter braucht also die Textspalte selbst — sonst
     liegt ihre letzte Zeile unter "Impressum". */
  body[data-slug="information"] .page:has(.block-legal) > .block-columns > .col:nth-child(2) {
    padding-bottom: 6rem;
  }
}


/* ==========================================================================
   15  Einschweben beim Scrollen

   Jedes Bild, jedes Galeriebild und jede Karte steht in der Ruhelage 1,6 rem
   tiefer und ist durchsichtig. Kommt das Element beim Scrollen ins Bild,
   setzt public/js/site.js (Abschnitt 9) die Klasse .is-da — dann faehrt es
   an seinen Platz im Raster und wird sichtbar.

   Warum die Ruhelage im Stylesheet steht und nicht im Skript: das Stylesheet
   liegt im <head> und gilt schon fuer das erste Bild, das der Browser
   zeichnet. Wuerde erst das Skript verstecken, blitzten die Bilder einmal
   auf, bevor sie verschwinden.

   Drei Bedingungen halten den Effekt da heraus, wo er nicht hingehoert:

     (scripting: enabled)
        Ohne JavaScript wird gar nichts erst versteckt — sonst bliebe die
        Seite leer, denn einblenden kann nur das Skript. Aeltere Browser, die
        diese Abfrage nicht kennen, zeigen die Seite ebenfalls unveraendert.

     (prefers-reduced-motion: no-preference)
        Wer im System "Bewegung reduzieren" eingestellt hat, bekommt die
        Seite wie bisher: nichts ist versteckt, nichts bewegt sich. Die
        Sammelregel in Abschnitt 16 reicht dafuer NICHT — sie nimmt nur die
        Uebergaenge weg, die Elemente blieben dann auf ihrer Ruhelage stehen.

     body:not([data-editor])
        In der Live-Vorschau des CMS tauscht jeder Tastendruck einen Block
        aus; der wuerde bei jeder Aenderung neu einblenden. Beim Layouten
        sieht Paul die Seite deshalb ohne Effekt.

   Bewegt werden nur transform und opacity. Beides rechnet der Browser auf
   der Grafikkarte; das Raster wird dabei nicht neu vermessen, und kein
   Element nimmt waehrenddessen mehr oder weniger Platz ein.
   ========================================================================== */

@media (scripting: enabled) and (prefers-reduced-motion: no-preference) {
  :root {
    /* Wie weit die Elemente anfangs tiefer stehen. 16 px sind bewusst wenig:
       spuerbar soll das Ankommen sein, nicht das Anfliegen. */
    --rise: 1.6rem;
  }

  /* Die Ruhelage.

     Galerien mit eigener Querbewegung — die Slideshow und die Galerieansicht
     der Plakate — steigen als GANZER Block: ihre Bilder stehen zum Teil
     ausserhalb des Fensters und wuerden sonst einzeln und viel zu spaet
     einblenden. Alle anderen Galerien setzen sich Bild fuer Bild ins Raster;
     genau das soll man sehen.

     Das ":not(.is-da)" ist der Schalter: sobald das Skript die Klasse setzt,
     passt diese Regel nicht mehr und das Element steht wieder in seinem
     normalen Zustand (deckend, ohne Verschiebung). Es braucht dafuer keine
     Gegenregel, die staerker waere als die Auswahl hier. */
  body:not([data-editor]) .page .block-image:not(.is-da),
  body:not([data-editor]) .page .block-video:not(.is-da),
  body:not([data-editor]) .page .block-embed:not(.is-da),
  body:not([data-editor]) .page .is-card:not(.is-da),
  body:not([data-editor]) .page .block-gallery[data-layout="slideshow"]:not(.is-da),
  body:not([data-editor]) .page .block-gallery[data-view]:not(.is-da),
  body:not([data-editor]) .page .block-gallery:not([data-layout="slideshow"]):not([data-view]) .gallery-item:not(.is-da),
  body:not([data-editor]) .page .block-projects .project-card:not(.is-da) {
    opacity: 0;
    transform: translate3d(0, var(--rise, 1.6rem), 0);
  }

  /* Der Uebergang steht bewusst NUR am Zielzustand: der Browser rechnet einen
     Uebergang immer mit den Angaben des NEUEN Zustands. Stuende er oben bei
     der Ruhelage, waere er in dem Moment, in dem .is-da dazukommt, schon
     nicht mehr gueltig — die Bilder wuerden springen statt zu gleiten.

     Die Kurve ist dieselbe wie bei der schwebenden Vorschau (Abschnitt 08):
     zuegiger Start, weiches Ankommen. Die Deckkraft ist frueher fertig als
     die Bewegung; dadurch steht das Bild schon da, waehrend es die letzten
     Millimeter noch zurueckliegt — das macht den Effekt ruhig statt hektisch. */
  body:not([data-editor]) .page .is-da {
    transition: opacity 0.5s ease,
                transform 0.85s cubic-bezier(0.2, 0.7, 0.2, 1);
  }
}


/* ==========================================================================
   16  Bewegung reduzieren
   Respektiert die Systemeinstellung "Bewegung reduzieren".
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    transition: none !important;
    animation: none !important;
    scroll-behavior: auto !important;
  }

  /* Die Sammelregel oben nimmt nur die Uebergaenge weg — die schwebende
     Vorschau wuerde sonst ohne Uebergang aufploppen. Hier faellt das
     Aufziehen ganz weg, sie erscheint einfach. (Die Vorschau im Rahmen der
     Navigation zieht gar nicht erst auf, sie blendet nur ein.) */
  .next-peek img,
  .next-peek video {
    transform: none;
  }
}


/* ==========================================================================
   17  Druck
   ========================================================================== */

@media print {
  .sidebar,
  .mobile-head,
  .mobile-menu,
  .menu-button,
  .draft-banner,
  .block-sketch,
  .lightbox,
  .view-switch,
  .cookie-note,
  .block-legal {
    display: none !important;
  }

  /* Auf Papier gibt es kein Scrollen: die Reihe wird zur Liste untereinander,
     sonst kaeme nur das erste Plakat aufs Blatt. Und ohne Scrollen gibt es
     auch keine angeschnittene linke Kante — die Maske muss weg, sonst
     druckte das erste Plakat links verblasst. */
  .block-gallery[data-view="gallery"] {
    display: block;
    height: auto;
    overflow: visible;
    -webkit-mask-image: none;
    mask-image: none;
  }

  .block-gallery[data-view="gallery"] .gallery-item {
    width: 100%;
    height: auto;
    break-inside: avoid;
    margin-bottom: 1rem;
  }

  body {
    background: #fff;
    color: #000;
  }

  .content {
    width: 100%;
    margin: 0;
  }

  .page {
    background: #fff;
    padding: 0;
    min-height: 0;
  }

  .page a {
    color: #000;
    border-color: transparent;
    mix-blend-mode: normal;
  }

  .block {
    break-inside: avoid;
  }

  /* Das Einschweben (Abschnitt 15) darf auf Papier nicht greifen: gedruckt
     wird nicht gescrollt, also kaeme kein Bild je an die Reihe und das Blatt
     bliebe leer. Ob die Abfrage (scripting: enabled) beim Drucken ueberhaupt
     zutrifft, handhaben die Browser unterschiedlich — darum hier im Klartext. */
  .page .block,
  .page .gallery-item {
    opacity: 1 !important;
    transform: none !important;
  }
}
