/*
  js-seite.css — justsoap, Demo.

  Farbwerte aus dem Kundenbriefing, Abschnitt 6.3 (dort ausdruecklich als
  Arbeitsgrundlage bezeichnet, nicht als festes Corporate Design).

  Harte Regeln aus dem Briefing, die hier verdrahtet sind:
    - vollflaechig dunkel, randlos, keine hellen Module zwischen dunklen
    - je Abschnitt hoechstens ZWEI Schriftgroessen: Display und Fliesstext
    - mindestens 40 % jeder Modulflaeche bleibt leer
    - keine Verlaeufe, keine Schlagschatten, keine Rahmen, keine runden Ecken
    - nie zwei Sortenfarben gleichzeitig in einem Modul

  Schrift: fuer die Demo Systemschriften. Der echte Bau braucht eine
  lizenzierte, selbst gehostete Schmalgrotesk — KEIN Google-CDN (DSGVO).

  --sorte wird je Abschnitt im HTML gesetzt. Beim Anhaengen von Sakura und
  Ocean aendert sich nur dieser eine Wert, keine Regel hier drin.
*/

:root {
  --tanne:     #1F2E26;
  /* Nach den Fahrten wird die Seite hell. Ein warmes Naturgrau, mehr Weiss
     als Grau — das Dunkle gehoert der Animation, nicht dem Lesen. */
  --hell:      #EDEBE6;
  --hellrand:  #DCD8D0;
  --tinte:     #1A1A18;
  --kalkweiss: #FFFDF8;
  --nebel:     #9AA79F;
  --tief:      #16211C;

  --sorte: #2C2C2A;                    /* wird je Abschnitt ueberschrieben */

  --display: ui-sans-serif, "Helvetica Neue", "Arial Narrow", system-ui, sans-serif;
  --text:    ui-sans-serif, system-ui, -apple-system, sans-serif;

  --rand: clamp(20px, 5vw, 72px);
  --luft: clamp(72px, 14vh, 190px);    /* Abstand zwischen Abschnitten */
}

* { box-sizing: border-box; }
html { scroll-behavior: auto; }

body {
  margin: 0;
  background: var(--tanne);
  color: var(--kalkweiss);
  font-family: var(--text);
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

img { max-width: 100%; height: auto; display: block; }

/* ── Typografie ──────────────────────────────────────────────────────── */
.gross {
  font-family: var(--display);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: -0.02em;
  line-height: 0.86;
  font-size: clamp(2.4rem, 10vw, 8rem);
  margin: 0;
}
.mittel {
  font-family: var(--display);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.01em;
  line-height: 0.95;
  font-size: clamp(1.5rem, 4.6vw, 3.2rem);
  margin: 0;
}
.lauf {
  font-size: clamp(1rem, 1.4vw, 1.12rem);
  line-height: 1.62;
  color: var(--nebel);
  max-width: 34em;
  margin: 0;
}
.klein {
  margin: 0;
  letter-spacing: 0.42em;
  text-transform: uppercase;
  font-size: clamp(0.62rem, 1.05vw, 0.8rem);
  color: var(--kalkweiss);
}
.zart { opacity: 0.62; }

/* ── Der Einzug ──────────────────────────────────────────────────────
   Alles mit data-einzug startet unsichtbar und leicht tiefer. js-einzug.js
   setzt `da`, sobald es ins Bild kommt. Ohne JavaScript bleibt alles
   sichtbar — `wartet` wird erst per Skript vergeben, nie im Grundzustand. */
[data-einzug].wartet {
  opacity: 0;
  transform: translateY(26px);
}
[data-einzug].wartet.da {
  opacity: 1;
  transform: none;
  transition: opacity 0.75s cubic-bezier(0.22, 1, 0.36, 1),
              transform 0.75s cubic-bezier(0.22, 1, 0.36, 1);
}

/* ── Die Buehne ──────────────────────────────────────────────────────
   Eine klebende Flaeche, drei Lagen uebereinander. Man scrollt nicht zu
   einer neuen Stelle — die naechste Sorte wischt hier herein. */
.js-buehne { position: relative; }
.js-buehne .klebt {
  position: sticky;
  top: 0;
  height: 100svh;
  width: 100%;
  overflow: hidden;
  background: #0B0F0D;
}
.js-buehne .lagen { position: absolute; inset: 0; }
.js-buehne .lage {
  position: absolute;
  inset: 0;
  /* Der Wisch laeuft ueber clip-path, nicht ueber transform: so bleibt die
     Leinwand an ihrem Platz und muss nicht neu gemessen werden. */
  clip-path: inset(0 0 0 100%);
  will-change: clip-path;
}
.js-buehne .lage canvas { display: block; width: 100%; height: 100%; }

/* Das Sortenzeichen. Oben links, INNERHALB des Fensters, gross genug zum
   Lesen. Es wechselt mit der Sorte — die Tafeln stehen unten, sie kommen
   sich also nicht in die Quere. */
.js-buehne .zeichen {
  position: absolute;
  /* An der echten Bildkante, nicht am Fensterrand: --bildL/--bildO kommen
     aus js-kino.js und sagen, wo die Aufnahme tatsaechlich anfaengt. Plus
     etwas Innenabstand, damit das Zeichen nicht auf der Kante klebt. */
  left: calc(var(--bildL, 0px) + clamp(18px, 2.4vw, 42px));
  top: calc(var(--bildO, 0px) + clamp(18px, 2.4vw, 42px));
  width: clamp(96px, 12vw, 184px);
  z-index: 5;
  pointer-events: none;
  filter: drop-shadow(0 4px 18px rgba(0,0,0,0.5));
}

/* Immer nur EINE Tafel im Bild. --u ist der Fortschritt der gerade
   laufenden Fahrt; min() bildet Rampe hinauf und hinunter ab, und opacity
   kappt selbst auf 0 und 1 — dafuer braucht es keine Zeile JavaScript. */
.js-buehne .tafelsatz {
  position: absolute;
  left: calc(var(--bildL, 0px) + clamp(18px, 2.4vw, 42px));
  right: calc(var(--bildL, 0px) + clamp(18px, 2.4vw, 42px));
  bottom: calc(var(--bildO, 0px) + clamp(18px, 2.4vw, 42px) + 3.2rem);
  z-index: 5;
  pointer-events: none;
}
.js-buehne .tafelsatz[hidden] { display: none; }
.js-buehne .tafel { position: absolute; left: 0; bottom: 0; max-width: 18em; opacity: 0; }
.js-buehne .tafel[data-beat="1"] { opacity: min((var(--u,0) - 0.18) * 12, (0.44 - var(--u,0)) * 12); }
.js-buehne .tafel[data-beat="2"] { opacity: min((var(--u,0) - 0.42) * 12, (0.66 - var(--u,0)) * 12); }
.js-buehne .tafel[data-beat="3"] { opacity: min((var(--u,0) - 0.64) * 12, (0.86 - var(--u,0)) * 12); }
.js-buehne .tafel[data-beat="4"] { opacity: calc((var(--u,0) - 0.84) * 12); }
.js-buehne .tafel b {
  display: block; font-family: var(--display); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.02em;
  font-size: clamp(1rem, 1.9vw, 1.5rem);
}
.js-buehne .tafel span {
  display: block; color: var(--nebel); margin-top: 3px;
  font-size: clamp(0.8rem, 1.15vw, 1rem);
}
.js-buehne .tafel::after {
  content: ""; display: block; height: 1px; background: var(--nebel);
  opacity: 0.5; margin-top: 12px; width: 100%;
}

.js-buehne .sortenname {
  position: absolute;
  right: calc(var(--bildL, 0px) + clamp(18px, 2.4vw, 42px));
  bottom: calc(var(--bildO, 0px) + clamp(18px, 2.4vw, 42px));
  margin: 0; z-index: 5; pointer-events: none;
  text-transform: uppercase; letter-spacing: 0.42em; color: var(--nebel);
  font-size: clamp(0.62rem, 1vw, 0.78rem);
}

/* ── Der Empfang ─────────────────────────────────────────────────────
   Die Marke liegt auf der Seife und sagt, was zu tun ist. Ohne diesen
   Moment weiss niemand, dass die Seite ueberhaupt etwas kann.
   Verschwindet beim ersten Scrollimpuls — --t ist der Fortschritt der
   ganzen Buehne, 0.015 davon sind rund eine halbe Umdrehung am Rad. */
.js-buehne .empfang {
  position: absolute;
  inset: 0;
  z-index: 8;
  display: grid;
  place-content: center;
  justify-items: center;
  text-align: center;
  gap: 20px;
  padding: var(--rand);
  pointer-events: none;
  background: rgba(8, 12, 10, 0.42);
  opacity: calc(1 - var(--t, 0) * 60);
}
.js-buehne .empfang img {
  width: clamp(160px, 26vw, 320px);
  filter: brightness(0) invert(1);
}
.js-buehne .empfang .scroll {
  margin: clamp(18px, 4vh, 44px) 0 0;
  font-family: var(--display); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.4em;
  font-size: clamp(0.7rem, 1.2vw, 0.9rem);
}
.js-buehne .empfang .pfeil {
  display: block; width: 1px; height: 46px; margin: 16px auto 0;
  background: var(--kalkweiss); opacity: 0.6;
  animation: pfeil-atmen 2.2s ease-in-out infinite;
  transform-origin: top;
}
@keyframes pfeil-atmen {
  0%, 100% { transform: scaleY(0.35); opacity: 0.25; }
  50%      { transform: scaleY(1);    opacity: 0.75; }
}


/* ── Die Fakten je Sorte ─────────────────────────────────────────────
   Zwei Kundenaufnahmen, die beim Scrollen ineinander uebergehen: erst die
   Verpackung, dann die Inhaltsstoffe. Sie werden NICHT freigestellt — sie
   haben einen gestalteten Hintergrund, und den nimmt der Abschnitt an.
   (Regel und Messwerkzeug: scripts/bilder_waehlen.py)

   Der Abschnitt faerbt sich MIT: --grundA ist der Grund der ersten
   Aufnahme, --grundB der der zweiten. Vorher lag die Bildfarbe nur zu 16 %
   ueber dem alten Gruen — deshalb blieb es gruen, obwohl das Bild rosa ist. */
.js-fakten {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(24px, 5vw, 90px);
  align-items: start;
  padding: var(--luft) var(--rand);
  background: var(--grundA);
  animation: grund-wechsel linear both;
  animation-timeline: view();
  animation-range: contain 10% contain 60%;
}
@keyframes grund-wechsel { to { background-color: var(--grundB); } }
/* Auf hellem Grund (Sakura) muss die Schrift umschlagen. */
.js-fakten[data-hell] { color: var(--tinte); }
.js-fakten[data-hell] .lauf, .js-fakten[data-hell] .klein { color: #5A5A54; }

/* Der Nebel. Zwei sehr weiche Wolken, die langsam gegeneinander treiben —
   das Bild atmet, ohne dass etwas blinkt. Farbe kommt aus dem Grund des
   Bildes, aufgehellt; so passt er in jedem Abschnitt von selbst.
   Vorbild: die handbemalte Studiowand aus Ramons Ocean-Drive-Aufnahme. */
.js-fakten .nebel {
  position: absolute; inset: 0; pointer-events: none; z-index: 0;
  opacity: 0.5;
  background:
    radial-gradient(58vw 44vh at 22% 28%,
      color-mix(in oklab, var(--grundB) 40%, #FFFFFF) 0%, transparent 62%),
    radial-gradient(52vw 40vh at 78% 72%,
      color-mix(in oklab, var(--grundA) 55%, #FFFFFF) 0%, transparent 60%);
  animation: nebel-treiben 46s ease-in-out infinite alternate;
  will-change: transform;
}
@keyframes nebel-treiben {
  from { transform: translate3d(-4%, -2%, 0) scale(1.08); }
  to   { transform: translate3d(5%, 3%, 0)  scale(1.16); }
}
.js-fakten .produkt, .js-fakten .strang { position: relative; z-index: 1; }
.js-fakten .produkt { position: sticky; top: 12svh; }

/* Der Bildwechsel: das neue Bild kommt VON UNTEN und schiebt das alte nach
   oben hinaus. Ein Ueberblenden liest sich als Fehler — man sieht kurz zwei
   halbe Bilder. Ein Schub liest sich als Absicht. */
.js-fakten .bildpaar {
  position: relative;
  overflow: hidden;
  aspect-ratio: 1 / 1;
}
.js-fakten .bildpaar img {
  position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: cover;
  animation: linear both;
  animation-timeline: view();
  animation-range: contain 18% contain 62%;
  will-change: transform;
}
.js-fakten .bildpaar img:first-child  { animation-name: bild-raus; }
.js-fakten .bildpaar img:last-child   { animation-name: bild-rein; }
@keyframes bild-raus { to   { transform: translateY(-100%); } }
@keyframes bild-rein { from { transform: translateY(100%); } to { transform: none; } }

@supports not (animation-timeline: view()) {
  .js-fakten { background: var(--grundA); }
  .js-fakten .bildpaar { aspect-ratio: auto; overflow: visible; }
  .js-fakten .bildpaar img { position: static; animation: none; height: auto; }
  .js-fakten .bildpaar img + img { margin-top: 14px; }
  .js-fakten .nebel { animation: none; }
}

.js-fakten .strang { display: grid; gap: clamp(56px, 12vh, 150px); }
.js-fakten .kopf { padding-bottom: 8px; }
.js-fakten .karte h3 { margin: 14px 0 16px; overflow-wrap: anywhere; hyphens: auto; }

/* Der Kaufteil zeigt jetzt Ramons Originalaufnahmen, nicht Freisteller. */
.js-kaufen article img { width: 100%; aspect-ratio: 1/1; object-fit: cover; }

/* ── Siegel ──────────────────────────────────────────────────────────── */
.js-siegel { padding: var(--luft) var(--rand); background: var(--hell); color: var(--tinte); }
.js-siegel .reihe {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  gap: clamp(26px, 4vw, 56px);
  margin-top: clamp(36px, 6vh, 72px);
}
.js-siegel figure { margin: 0; text-align: center; }
.js-siegel img { width: clamp(64px, 7vw, 96px); margin: 0 auto 14px; }
.js-siegel figcaption {
  font-size: 0.72rem; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--nebel);
}
.fakten-liste {
  list-style: none; margin: clamp(40px, 7vh, 84px) 0 0; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 18px clamp(24px, 4vw, 60px);
}
.fakten-liste li {
  padding-top: 14px;
  border-top: 1px solid color-mix(in oklab, var(--nebel) 40%, transparent);
  color: var(--nebel); font-size: clamp(0.9rem, 1.2vw, 1rem);
}

/* ── Kaufen ──────────────────────────────────────────────────────────── */
.js-kaufen { padding: var(--luft) var(--rand); background: var(--hell); color: var(--tinte); }
.js-kaufen .reihe {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: clamp(24px, 3.5vw, 56px); margin-top: clamp(36px, 6vh, 72px);
}
.js-kaufen article {
  display: grid; justify-items: center; gap: 14px;
  padding: clamp(22px, 3vw, 42px);
  background: color-mix(in oklab, var(--sorte) 40%, var(--tief));
}
.js-kaufen article img { width: 76%; }
.js-kaufen .preis { font-family: var(--display); font-weight: 800; margin: 0;
  font-size: clamp(1.3rem, 3vw, 2rem); }
.js-kaufen .preis.offen { color: var(--nebel); }
.js-kaufen .preis.offen span {
  display: block; font-family: var(--text); font-weight: 400;
  font-size: 0.68rem; letter-spacing: 0.2em; text-transform: uppercase; margin-top: 6px;
}
.js-kaufen .knopf {
  display: inline-block; padding: 15px 34px; background: var(--kalkweiss);
  color: #121212; text-decoration: none; font-family: var(--display);
  font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; font-size: 0.85rem;
}

/* ── Der Fuss ────────────────────────────────────────────────────────── */
.js-fuss {
  display: grid; justify-items: center; gap: 16px;
  padding: var(--luft) var(--rand); background: var(--tief);
}
.js-fuss img { width: clamp(120px, 16vw, 200px); filter: brightness(0) invert(1); opacity: 0.92; }

/* ── Handy ───────────────────────────────────────────────────────────── */
@media (max-width: 900px) {
  .js-fakten { grid-template-columns: 1fr; }
  .js-fakten .produkt { position: static; }
  .js-fakten .produkt img { width: min(66%, 300px); animation: none; }
  .js-buehne .zeichen { width: 84px; }
  .js-buehne .tafelsatz { bottom: calc(var(--rand) + 4.4rem); }
}

/* ── Wer keine Bewegung will, bekommt trotzdem eine ganze Seite ──────── */
@media (prefers-reduced-motion: reduce) {
  .js-buehne .klebt { position: static; }
  .js-buehne .lage { clip-path: none; position: static; }
  .js-buehne .empfang { position: static; opacity: 1; }
  .js-buehne .tafelsatz { position: static; display: grid; gap: 22px; padding: var(--rand); }
  .js-buehne .tafelsatz[hidden] { display: grid; }
  .js-buehne .tafel { position: static; opacity: 1; }
  .js-buehne .zeichen, .js-buehne .sortenname { position: static; }
  .js-buehne .empfang .pfeil { animation: none; }
  .js-fakten .produkt img { animation: none; }
}

/* ── Die Reifung ─────────────────────────────────────────────────────
   Der Zaehler steigt beim Scrollen — Vorbild ist das Thermometer aus dem
   Doener-Kapitel „The Roast", nur mit Zeit statt Hitze.

   @property macht --tage zu einer echten ZAHL statt zu Text. Nur dann kann
   der Browser sie animieren, und nur dann kann counter-reset sie ausgeben.
   Ohne diese Zeile springt der Zaehler von 0 auf 56, statt zu laufen. */
@property --tage {
  syntax: '<integer>';
  initial-value: 0;
  inherits: true;
}
.js-reifung { position: relative; height: 260svh; background: var(--hell); color: var(--tinte); }
.js-reifung .klebt {
  position: sticky; top: 0; height: 100svh;
  display: grid; place-content: center; justify-items: center;
  gap: clamp(16px, 3vh, 34px); padding: var(--rand); text-align: center;
  --tage: 0;
  animation: reifen linear both;
  animation-timeline: view();
  animation-range: contain 0% contain 100%;
}
@keyframes reifen { to { --tage: 56; } }

.js-reifung .zaehler {
  margin: 0; display: flex; align-items: baseline; gap: 0.3em;
  font-family: var(--display); font-weight: 800; line-height: 0.85;
}
.js-reifung .zahl { font-size: clamp(4rem, 18vw, 15rem); }
.js-reifung .zahl::after { counter-reset: t var(--tage); content: counter(t); }
.js-reifung .einheit {
  font-size: clamp(1rem, 2.4vw, 2rem); color: var(--nebel);
  text-transform: uppercase; letter-spacing: 0.2em;
}

.js-reifung .skala {
  width: min(820px, 86vw);
  display: grid; grid-template-columns: repeat(5, 1fr);
  gap: 10px; margin-top: 8px; position: relative;
  padding-top: 26px;
}
/* Der Pegel steigt mit dem Zaehler. 56 Tage = volle Breite. */
.js-reifung .pegel {
  position: absolute; left: 0; top: 0; height: 3px;
  width: calc(var(--tage) / 56 * 100%);
  background: var(--kalkweiss);
}
.js-reifung .skala::before {
  content: ""; position: absolute; left: 0; right: 0; top: 1px; height: 1px;
  background: var(--nebel); opacity: 0.35;
}
.js-reifung .skala span {
  font-size: clamp(0.58rem, 0.95vw, 0.74rem);
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--nebel);
  /* Die Marke leuchtet auf, sobald der Pegel sie erreicht hat. */
  opacity: calc((var(--tage) / 56 - var(--bei)) * 8 + 0.25);
}
.js-reifung .mitte { margin: clamp(18px, 4vh, 40px) auto 0; text-align: center; }
@supports not (animation-timeline: view()) {
  .js-reifung { height: auto; }
  .js-reifung .klebt { position: static; height: auto; padding: var(--luft) var(--rand); --tage: 56; }
}

@media (prefers-reduced-motion: reduce) {
  .js-reifung { height: auto; }
  .js-reifung .klebt { position: static; height: auto; animation: none; --tage: 56; }
}


/* ── Der helle Teil ──────────────────────────────────────────────────
   Nach den Fahrten wird die Seite hell. Alles, was fuer dunklen Grund
   gebaut war, muss hier umschlagen: Nebelgrau wird zu warmem Grau, die
   Siegel-Strichzeichnung von Kalkweiss zu Tinte. */
.js-siegel .lauf, .js-kaufen .lauf, .js-siegel figcaption, .js-reifung .klein, .js-reifung .lauf,
.js-siegel .klein, .js-kaufen .klein, .js-siegel h3, .js-kaufen h3, /* Die Siegel sind Kalkweiss auf Transparenz — auf hellem Grund unsichtbar.
   invert() dreht sie zu Tinte, ohne eine zweite Bilddatei. */
.js-siegel .reihe img { filter: invert(1) brightness(0.15); }
.fakten-liste li { color: #5A5A54; border-top-color: var(--hellrand); }
.js-reifung .skala::before { background: #B9B4AB; opacity: 1; }
.js-reifung .pegel { background: var(--tinte); }
.js-reifung .skala span { color: #5A5A54; }
.js-kaufen article { background: var(--sorte); color: var(--kalkweiss); }
.js-kaufen article .klein { color: var(--kalkweiss); }
.js-kaufen .preis.offen { color: rgba(255,253,248,0.7); }
.js-fuss { background: var(--tinte); }

@media (max-width: 900px) {
  /* Am Handy steht die Aufnahme in einem Band; die Aufschriften duerfen
     dort nicht mehr an der Bildkante haengen, sonst rutschen sie in die
     unscharfe Umgebung. Zurueck an den Fensterrand. */
  .js-buehne .zeichen { left: var(--rand); top: calc(var(--rand) + 4px); width: 76px; }
  .js-buehne .tafelsatz { left: var(--rand); right: var(--rand);
                          bottom: calc(var(--rand) + 3.6rem); }
  .js-buehne .sortenname { right: var(--rand); bottom: var(--rand); }
  .js-fakten .produkt { position: static; }
}

/* ── Kaufkarten: warme Toene aus den eigenen Aufnahmen ───────────────
   Vorher standen hier die rohen Markenfarben #993556 und #185FA5 — knallrosa
   und knallblau auf hellem Grau, ohne Bezug zu den Fotos darueber. Jetzt der
   gemessene Grund des jeweiligen Kaufbildes: #382922, #D5C6B5, #353C40. */
.js-kaufen article[data-hell] { color: var(--tinte); }
.js-kaufen article[data-hell] .klein { color: var(--tinte); }
.js-kaufen article[data-hell] .preis.offen { color: rgba(26,26,24,0.6); }
.js-kaufen article[data-hell] .knopf { background: var(--tinte); color: var(--hell); }

@media (max-width: 900px) {
  }

/* ── Die Bahn ────────────────────────────────────────────────────────
   Die sieben A++ Banner fahren am SCROLL entlang von rechts nach links.
   Man scrollt also weiter nach unten wie ueberall — und das Gleis bewegt
   sich waagerecht. Kein Wischen noetig, kein zweites Bediengeraet.

   Der Abschnitt ist so hoch wie die Bahn breit ist; die Buehne klebt und
   das Gleis wird ueber `animation-timeline: view()` verschoben. Damit
   haengt die Fahrt am Scrollbalken statt an einer Uhr und laeuft
   rueckwaerts von selbst rueckwaerts.

   Hell wie der Schlussteil — dunkel wirkte der Block hier wie ein Loch
   zwischen zwei hellen Flaechen. */
.js-bahn {
  position: relative;
  height: 330svh;
  background: var(--hell);
  color: var(--tinte);
  /* Die Zeitleiste haengt am ABSCHNITT, nicht am Gleis. Das Gleis liegt in
     einem klebenden Element — solange das klebt, bewegt es sich relativ zum
     Bildschirm gar nicht, und `view()` meldet deshalb dauerhaft 0. Genau
     daran stand die Bahn still. Der Abschnitt dagegen scrollt wirklich. */
  view-timeline-name: --bahn;
  view-timeline-axis: block;
}
.js-bahn .klebt {
  position: sticky; top: 0; height: 100svh;
  display: grid; align-content: center; gap: clamp(24px, 5vh, 56px);
  overflow: hidden;
}
.js-bahn h3 { padding: 0 var(--rand); }
.js-bahn .gleis {
  display: flex;
  /* Breite SELBST ausrechnen statt max-content. `-100%` bezieht sich sonst
     auf eine Breite, ueber die sich die Browser nicht einig sind: Chrome
     kommt auf 5285 px (Kachelbreite), WebKit auf 10493 px (natuerliche
     Bildbreite 1464 px). In Safari war die Bahn deshalb schon durch, bevor
     der Abschnitt zu Ende war — man sah nur noch die Ueberschrift. */
  --kachel: min(64vw, 720px);
  --luecke: clamp(14px, 2vw, 32px);
  --bahnbreite: calc(7 * var(--kachel) + 6 * var(--luecke) + var(--rand));
  gap: var(--luecke);
  padding-left: var(--rand);
  width: var(--bahnbreite);
  animation: bahn-fahren linear both;
  animation-timeline: --bahn;
  /* Genau die Phase, in der der Abschnitt den Bildschirm ausfuellt — also
     genau die Zeit, in der die Buehne klebt. `contain` waere hier entartet,
     weil der Abschnitt hoeher ist als der Bildschirm. */
  animation-range: entry 100% exit 0%;
  will-change: transform;
}
/* Von der ersten Kachel bis zur letzten: die Bahn ist breiter als das
   Fenster, deshalb der Rest ueber 100 % hinaus. */
@keyframes bahn-fahren {
  from { transform: translateX(0); }
  to   { transform: translateX(calc(92vw - var(--bahnbreite))); }
}
.js-bahn figure { margin: 0; flex: 0 0 var(--kachel); }
.js-bahn img { width: 100%; border-radius: 0; }

/* Ohne scrollgetriebene Animationen bleibt es das, was es vorher war:
   eine wischbare Reihe. Kein Effekt, aber nichts kaputt. */
@supports not (animation-timeline: view()) {
  .js-bahn { height: auto; padding: var(--luft) 0; }
  .js-bahn .klebt { position: static; height: auto; }
  .js-bahn .gleis { width: auto; overflow-x: auto; animation: none;
                    scroll-snap-type: x mandatory; padding-right: var(--rand); }
  .js-bahn figure { scroll-snap-align: center; }
}
@media (max-width: 900px) {
  .js-bahn { height: 260svh; }
  .js-bahn figure { flex: 0 0 82vw; }
  .js-bahn .gleis { --kachel: 82vw; animation-name: bahn-fahren-handy; }
}
@media (prefers-reduced-motion: reduce) {
  .js-bahn { height: auto; padding: var(--luft) 0; }
  .js-bahn .klebt { position: static; height: auto; }
  .js-bahn .gleis { animation: none; overflow-x: auto; width: auto; }
}

@keyframes bahn-fahren-handy {
  from { transform: translateX(0); }
  to   { transform: translateX(calc(88vw - var(--bahnbreite))); }
}
