/* =========================================================================
   Landing — die Seite als Rezept: Kopf → Zutaten → Zubereitung → Ergebnis.
   Dark first. Das Essen ist das einzig Helle: Fotos und die Bildsequenz
   leuchten aus dem dunklen Grund heraus, wie in der App.
   ========================================================================= */

/* ===================== Zahlen, die abrollen (Odometer) ==================== */
/* Statt eine Zahl hochzuzählen, rollt hier jede Ziffernstelle wie am
   Kilometerzähler in ihre Endlage. Zwei Gründe, warum das besser ist als
   das Hochzählen davor:
   1. Beim Hochzählen steht für eine Sekunde eine PLAUSIBEL FALSCHE Zahl da
      („153 Rezepte"). Beim Abrollen sieht man, dass etwas rollt — es
      behauptet nie einen Wert.
   2. Es läuft rein über `transform`, bleibt also auf dem Compositor. Der
      Zählweg davor schrieb in jedem Frame `textContent` und löste damit
      Layout und Paint aus.
   Die echte Zahl bleibt als visually-hidden im DOM stehen; die Walzen sind
   `aria-hidden`. Ohne JS steht schlicht die Zahl da. */
.odo {
  display: inline-flex;
  font-variant-numeric: lining-nums tabular-nums;
}
.odo__d {
  display: block;
  height: 1em;
  line-height: 1;
  overflow: hidden;
}
.odo__col {
  display: block;
  /* Jede Stelle dreht exakt eine volle Umdrehung plus ihren Zielwert —
     dadurch laufen alle Stellen gleich weit, statt dass die 9 sechsmal so
     weit rollt wie die 1. */
  transform: translateY(calc(var(--d, 0) * -1em));
  transition: transform 1050ms var(--ease-spring-soft);
  transition-delay: var(--od, 0ms);
}
.odo__col > i {
  display: block;
  height: 1em;
  line-height: 1;
  font-style: inherit;
}

/* ==================== Wort-Enthüllung an Überschriften =================== */
/* Zeile für Zeile aufzublenden ist Grundrauschen — das macht jeder
   Baukasten. Wortweise, gestaffelt und mit einer Maske darunter ist der
   Unterschied zwischen „animiert" und „gesetzt".
   Barrierefreiheit: Die Wörter sind `aria-hidden`, die Überschrift trägt
   ihren vollständigen Text als `aria-label`. Sonst liest ein Screenreader
   zerhackte Fragmente. */
.wr {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  /* Die Maske darf die Unterlängen nicht abschneiden. */
  padding-bottom: 0.12em;
  margin-bottom: -0.12em;
}
.js .wr > i {
  display: inline-block;
  font-style: inherit;
  translate: 0 110%;
  opacity: 0;
}
.js [data-words].is-in .wr > i {
  animation: word-up 760ms var(--ease-out-expo) both;
  animation-delay: calc(var(--i, 0) * 52ms);
}
@keyframes word-up {
  from {
    translate: 0 110%;
    opacity: 0;
  }
  to {
    translate: 0 0;
    opacity: 1;
  }
}
@media (prefers-reduced-motion: reduce) {
  .js .wr > i {
    translate: none;
    opacity: 1;
  }
}

/* --- Einblendungen. Nur aktiv, wenn JS läuft — sonst ist alles sichtbar. - */
.js .reveal {
  opacity: 0;
  translate: 0 26px;
  transition:
    opacity 700ms var(--ease-out-expo),
    translate 700ms var(--ease-out-expo);
  transition-delay: var(--d, 0ms);
}
.js .reveal.is-in {
  opacity: 1;
  translate: 0 0;
}

/* ========================== 1 · REZEPTKOPF =============================== */
/* Hier steht die Kernbotschaft — und zwar vorgeführt, nicht behauptet:
   links der Satz, rechts der bedienbare Beweis. Beides muss über der
   Falz stehen, deshalb ist der Kopfabstand knapper als sonst üblich. */

.head {
  position: relative;
  padding: clamp(5.75rem, 4.5rem + 4vw, 8rem) 0 clamp(1.25rem, 2.5vw, 2rem);
}

.head__grid {
  display: grid;
  gap: clamp(2.25rem, 4vw, 3.5rem);
  align-items: start;
}

/* Editoriale Kopfzeile: linksbündig, kein zentrierter Held.
   Sie war bewusst eine Spur kleiner als die Display-Skala, weil rechts
   daneben das Beweis-Panel stand und die Fläche mehr brauchte. Das Panel
   ist am 16.08.2026 aus der Seite genommen worden — jetzt trägt die
   Schlagzeile den Kopf allein und darf die Größe auch haben. Ohne diesen
   Schritt stünde eine mittelgroße Zeile in einer halbleeren Fläche. */
.head h1 {
  font-size: clamp(3rem, 1.5rem + 6.6vw, 8rem);
  max-width: 13ch;
  margin-bottom: var(--gap);
}
.head .lead {
  max-width: 38ch;
}

/* ---------------------- Der Kühlschrank-Beweis ------------------------- */

.proof {
  position: relative;
  border-radius: var(--radius-xl);
  background:
    radial-gradient(120% 90% at 12% -8%, var(--brand-veil) 0%, transparent 58%),
    var(--bg-raised);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-far);
  overflow: hidden;
}

.proof__pane {
  padding: clamp(1.1rem, 2.4vw, 1.6rem) clamp(1.1rem, 2.4vw, 1.75rem);
}
/* Kein zweiter Schüsselrand — der gehört dem Zutaten-Abschnitt allein.
   Hier schwingt die Kante nur auf EINER Seite hoch: Das Ergebnis läuft
   aus dem Vorrat heraus, es taucht nicht symmetrisch auf. */
.proof__pane--out {
  position: relative;
  border-top: 1px solid var(--border);
  border-radius: 6rem var(--radius-sm) 0 0 / 3.25rem var(--radius-sm) 0 0;
  background: linear-gradient(
    oklch(0.94 0.01 155 / 0.05),
    oklch(0.94 0.01 155 / 0.015) 40%
  );
  margin-top: 0.35rem;
  padding-top: clamp(1.4rem, 3.4vw, 2.4rem);
}

.proof__label {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--gap-tight);
  margin: 0 0 0.85rem;
  font: 500 var(--t-mono) / 1 var(--font-mono);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--brand-soft);
  max-width: none;
}
.proof__aside {
  letter-spacing: 0.08em;
  color: var(--fg-faint);
  text-transform: none;
  font-size: 0.94em;
}
/* Ohne JS gibt es nichts anzutippen — dann darf da auch nicht stehen,
   man solle etwas antippen. */
.proof:not([data-live="true"]) .proof__aside {
  display: none;
}

.pantry {
  display: flex;
  flex-wrap: wrap;
  gap: 0.45rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

/* Gefüllte Pill = vorhanden, Kontur = nicht vorhanden. Das ist das Muster
   aus den Referenzen (ds-geschmacksprofil #6) und zugleich das, was die
   App selbst tut. */
.chip {
  --chip-bg: var(--brand);
  --chip-fg: oklch(0.98 0.01 155);
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.42rem 0.85rem 0.42rem 0.66rem;
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  background: var(--chip-bg);
  color: var(--chip-fg);
  font: 600 var(--t-small) / 1.1 var(--font-sans);
  letter-spacing: -0.01em;
  cursor: pointer;
  transition:
    background var(--dur-fast) var(--ease-out-expo),
    color var(--dur-fast) var(--ease-out-expo),
    border-color var(--dur-fast) var(--ease-out-expo),
    transform var(--dur-fast) var(--ease-spring-soft);
}
.chip::before {
  content: "";
  width: 0.9rem;
  height: 0.9rem;
  flex: none;
  border-radius: 50%;
  background: currentColor;
  /* Häkchen als Maske: eine Form weniger im DOM, skaliert mit der Schrift. */
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M20 6 9 17l-5-5' fill='none' stroke='%23000' stroke-width='3.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E")
    center / 100% no-repeat;
  transition: opacity var(--dur-fast) var(--ease-out-expo);
}
.chip[aria-pressed="false"] {
  --chip-bg: transparent;
  --chip-fg: var(--fg-faint);
  border-color: var(--border-strong);
}
.chip[aria-pressed="false"]::before {
  opacity: 0.28;
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 5v14M5 12h14' fill='none' stroke='%23000' stroke-width='3.4' stroke-linecap='round'/%3E%3C/svg%3E");
}
.chip:hover {
  transform: translateY(-1px);
}
.chip[aria-pressed="false"]:hover {
  --chip-fg: var(--fg);
  border-color: var(--brand-soft);
}
.chip:active,
.chip.is-pressed {
  transform: translateY(0) scale(0.94);
}
/* Der Umschaltmoment ist die Kernhandlung dieser Seite — er darf federn.
   Die Klasse setzt proof.js beim Klick und nimmt sie nach dem Lauf wieder
   weg; über das Attribut allein ginge es nicht, weil die acht vorbelegten
   Chips dann schon beim Laden poppen würden. */
.chip.is-toggled {
  animation: chip-pop 380ms var(--ease-spring) both;
}
@keyframes chip-pop {
  40% {
    scale: 1.09;
  }
  to {
    scale: 1;
  }
}
/* Die Vorratsleiste ist auf dem Telefon die Hauptinteraktion der ganzen
   Seite. Gemessen waren die Pillen 27,8 px hoch: WCAG 2.2 (24 px) ist damit
   zwar erfüllt, Apples 44er-Empfehlung aber klar verfehlt. Auf grobem Zeiger
   wachsen sie deshalb, ohne die Dichte auf dem Desktop anzufassen. */
@media (pointer: coarse) {
  .chip {
    min-height: 2.5rem;
    padding-right: 1rem;
  }
}

/* Die Chips laufen beim Laden nacheinander ein — der Vorrat wird
   „eingeräumt", statt als Block dazustehen. */
.js .pantry li {
  animation: chip-in 480ms var(--ease-out-back) both;
  animation-delay: calc(var(--i, 0) * 38ms + 120ms);
}
@keyframes chip-in {
  from {
    opacity: 0;
    translate: 0 8px;
    scale: 0.9;
  }
}

/* Die Zahl bekommt einen Satz, der sie einordnet — "9" allein wäre
   unfertig (ds-geschmacksprofil #14). */
.proof__summary {
  margin: 0 0 0.9rem;
  max-width: none;
  font-size: var(--t-small);
  line-height: 1.45;
  color: var(--fg-dim);
}
.proof__summary b {
  display: inline-block;
  font-family: var(--font-display);
  font-size: 1.75em;
  font-weight: 600;
  font-variation-settings: "SOFT" 24, "WONK" 1;
  line-height: 0.9;
  letter-spacing: -0.03em;
  color: var(--brand-lift);
  margin-right: 0.12em;
  font-variant-numeric: lining-nums tabular-nums;
  /* Die Zahl ist das Ergebnis der Interaktion — sie rastet sichtbar ein. */
  animation: count-pop 520ms var(--ease-out-back) both;
}
@keyframes count-pop {
  from {
    scale: 0.55;
    opacity: 0;
  }
  60% {
    scale: 1.08;
    opacity: 1;
  }
}

.matches {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.4rem;
}

.match {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 0.85rem;
  padding: 0.5rem 0.7rem 0.5rem 0.5rem;
  border-radius: var(--radius);
  background: var(--bg-card);
  border: 1px solid var(--border);
  transition:
    border-color var(--dur-fast) var(--ease-out-expo),
    background var(--dur-fast) var(--ease-out-expo),
    transform var(--dur-fast) var(--ease-out-back);
}
.match:hover {
  transform: translateX(3px);
  border-color: var(--border-strong);
}
.match--full {
  border-color: oklch(0.68 0.075 158 / 0.38);
  background:
    linear-gradient(90deg, var(--brand-veil), transparent 42%),
    var(--bg-card);
}
.match--full:hover {
  border-color: var(--brand-soft);
}

/* Jede Neuberechnung ist eine Antwort auf einen Klick — sie darf man
   sehen. Die Zeilen laufen gestaffelt ein, der Ring füllt sich dabei von
   null auf seinen Wert (nur möglich, weil --ratio in base.css als Zahl
   registriert ist). */
.js .match {
  animation:
    match-in 460ms var(--ease-out-expo) both,
    ring-fill 820ms var(--ease-out-expo) both;
  animation-delay: calc(var(--i, 0) * 52ms);
}
@keyframes match-in {
  from {
    opacity: 0;
    translate: 0 12px;
  }
}
@keyframes ring-fill {
  from {
    --ratio: 0;
  }
}

.match__media {
  width: 2.9rem;
  height: 2.9rem;
  flex: none;
  border-radius: 14px;
  overflow: hidden;
  display: grid;
  place-items: center;
  background: var(--bg-lift);
}
.match__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.match__emoji {
  font-size: 1.35rem;
  line-height: 1;
}

.match__body {
  display: grid;
  gap: 0.1rem;
  min-width: 0;
}
.match__name {
  font-weight: 600;
  font-size: var(--t-small);
  line-height: 1.25;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.match__meta {
  font: 500 var(--t-micro) / 1 var(--font-mono);
  letter-spacing: 0.05em;
  color: var(--fg-faint);
}

.match__score {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  text-align: right;
}
.match__note {
  font-size: var(--t-micro);
  color: var(--fg-faint);
  max-width: 11ch;
  line-height: 1.2;
}
.match--full .match__note {
  color: var(--brand-soft);
  font-weight: 600;
}

/* Der Ring ist die Datenvisualisierung dieser Seite: der Anteil der
   Zutaten, die schon da sind. Aus den Tokens gebaut, nicht aus einer
   Library (ds-geschmacksprofil #14). */
.match__ring {
  position: relative;
  width: 2.5rem;
  height: 2.5rem;
  flex: none;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: conic-gradient(
    var(--brand-soft) calc(var(--ratio, 0) * 360deg),
    var(--border-strong) 0
  );
}
.match--full .match__ring {
  background: conic-gradient(var(--brand-lift) 360deg, var(--brand-lift) 0);
}
.match__ring::before {
  content: "";
  position: absolute;
  inset: 3px;
  border-radius: 50%;
  background: var(--bg-card);
}
.match--full .match__ring::before {
  background: oklch(0.24 0.03 158);
}
.match__ring b {
  position: relative;
  font: 600 var(--t-micro) / 1 var(--font-mono);
  letter-spacing: 0;
  color: var(--fg);
  font-variant-numeric: tabular-nums;
}

.matches__empty {
  padding: 1.35rem 1.1rem;
  border-radius: var(--radius);
  border: 1px dashed var(--border-strong);
  color: var(--fg-faint);
  font-size: var(--t-small);
  text-align: center;
}

.proof__foot {
  margin: 0.85rem 0 0;
  font-size: var(--t-micro);
  color: var(--fg-faint);
  max-width: none;
}

/* Auf Telefonen konkurrieren Ring und Fehlt-Notiz mit dem Rezeptnamen um
   dieselbe Zeile — und der Name verliert: gemessen blieben bei 375 px noch
   50 % von "Tomatensuppe mit Linsen" übrig, bei 320 px 18 %, also "Tom…".
   Der Name ist das Ergebnis, das diese Seite verspricht; er darf nicht das
   sein, was zuerst wegfällt.

   Deshalb wird die Notiz hier unter den Namen gesetzt statt neben ihn.
   `display: contents` löst dafür nur den Kasten von .match__score auf —
   Ring und Notiz werden direkte Rasterkinder und lassen sich einzeln
   platzieren, ohne dass proof.js ein anderes Markup erzeugen muss. */
@media (max-width: 29.99rem) {
  .match {
    row-gap: 0.25rem;
  }
  .match__media {
    grid-row: 1 / span 2;
  }
  .match__body {
    grid-column: 2;
    grid-row: 1;
  }
  .match__score {
    display: contents;
  }
  .match__ring {
    grid-column: 3;
    grid-row: 1 / span 2;
  }
  .match__note {
    grid-column: 2;
    grid-row: 2;
    max-width: none;
    text-align: left;
  }
}

/* Unter 352 px bleiben der Namensspalte sonst nur 112 px — dort bricht schon
   "20 Min · 471 kcal" um. Vorschaubild, Ring und Abstände geben hier je ein
   paar Pixel ab; zusammen sind das rund 26 px zurück in die Spalte, die den
   Rezeptnamen trägt. */
@media (max-width: 22rem) {
  .match {
    gap: 0.6rem;
    padding-right: 0.5rem;
  }
  .match__media {
    width: 2.5rem;
    height: 2.5rem;
  }
  .match__ring {
    width: 2.15rem;
    height: 2.15rem;
  }
}

@media (min-width: 30rem) {
  .match__media {
    width: 3.35rem;
    height: 3.35rem;
  }
  .match__name {
    font-size: var(--t-body);
  }
}

/* Die Zeile aus jedem Rezeptkopf: Zeit · Portionen · Aufwand.
   Hier trägt sie die Produktfakten — dieselbe Form, anderer Inhalt.
   Als 2×2-Raster in der Textspalte, nicht als Band über die ganze
   Breite: So schließt sie die Spalte auf der Höhe des Beweis-Panels ab. */
.recipe-meta {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.15rem clamp(1rem, 3vw, 2.5rem);
  margin: clamp(1.5rem, 3vw, 2.25rem) 0 0;
  padding: 1rem 0 0;
  list-style: none;
  border-top: 1px solid var(--border);
  max-width: 26rem;
}
@media (min-width: 30rem) {
  .recipe-meta {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    max-width: none;
  }
}
@media (min-width: 64rem) {
  .recipe-meta {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    max-width: 26rem;
  }
}
.recipe-meta li {
  display: grid;
  gap: 0.15rem;
  padding: 0.4rem 0;
}
.recipe-meta b {
  font: 500 var(--t-micro) / 1 var(--font-mono);
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--fg-faint);
  font-weight: 500;
}
.recipe-meta strong {
  font-family: var(--font-display);
  font-size: 1.32rem;
  font-weight: 600;
  font-variation-settings: "SOFT" 30, "WONK" 1;
  letter-spacing: -0.02em;
}

.head__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap-tight);
  align-items: center;
}
.head__pricenote {
  font-size: var(--t-small);
  color: var(--fg-faint);
  margin: var(--gap-tight) 0 0;
}

/* ===================== 2 · DIE VERWANDLUNG (Sequenz) ==================== */
/* 160 Einzelbilder, an den Scrollfortschritt gekoppelt: Aus einzelnen
   Zutaten wird eine Schüssel Salat.

   Sie steht direkt hinter dem Beweis, weil sie genau dessen Satz zu Ende
   erzählt — und sie ist bewusst eng gefasst: Die Bühne wurde vorher zu
   einem Vielfachen des Bildes aufgeblasen, dadurch stand oben und unten
   viel Leere. Jetzt bestimmt das Bild die Höhe, nicht umgekehrt.      */

.assembly {
  position: relative;
  /* Scrollstrecke. Gescrubbt wird über (Höhe − Viewport), hier also 180 vh;
     die vier Stationen bekommen damit je rund eine halbe Bildschirmhöhe
     Lesezeit. Bei den vorherigen 220 vh blieben je Station nur ~270 px —
     zu wenig, um den Satz überhaupt zu lesen. */
  height: 280vh;
}
.assembly__sticky {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: hidden;
  background: var(--bg);
  display: grid;
  place-items: center;
}

/* Das Ausgangsmaterial stammt aus einem hochskalierten GIF und trägt keine
   feine Zeichnung. Es formatfüllend über 1400 px zu ziehen vergrößert nur
   den Matsch — in einer begrenzten Bühne wirkt dasselbe Material scharf.

   Die dritte Grenze (112svh) koppelt die Breite an die VIEWPORT-HÖHE:
   Bei 1920/990 ergibt das ein Bild von rund 58 svh Höhe. Damit füllt die
   Szene die Bühne, statt in ihrer Mitte zu schwimmen — und für die Sätze
   darüber und darunter bleibt genau so viel Platz, wie sie brauchen. */
.assembly__frame {
  position: relative;
  width: min(100% - 2rem, 76rem, 112svh);
}
/* Auf schmalen Geräten begrenzt die Breite das Bild, nicht die Höhe: Die
   Szene wäre nur ein Streifen von rund 185 px in einer 844 px hohen Bühne.
   Deshalb wird das Fenster dort hochkantiger und die Zeichnung beschneidet
   seitlich (siehe sequence.js) — bei dieser Anzeigegröße passiert das ohne
   jeden Schärfeverlust, weil selbst der Ausschnitt noch herunterskaliert
   wird. Die Schüssel steht mittig, sie bleibt vollständig im Bild. */
.assembly canvas {
  width: 100%;
  aspect-ratio: 1 / 1;
  height: auto;
  border-radius: var(--radius-lg);
  display: block;
  /* Leichte Mikrokontrast-Anhebung: macht weiches Material knackiger,
     ohne die Halos, die echtes Nachschärfen erzeugen würde. */
  filter: saturate(1.06) contrast(1.04);
}
/* Ab 40 rem liegt der quer beschnittene Satz an; dort ist das native
   Verhältnis zugleich die randlose Passung. 1920×990: Die unteren 90 px
   des Originals sind weg, dort lag ein eingebranntes Veo-Wasserzeichen. */
@media (min-width: 40rem) {
  .assembly canvas {
    aspect-ratio: 1920 / 990;
  }
}

/* Die Szene ist hell, die Seite ist dunkel: Ein Verlauf an den Rändern
   bindet das Bild in die Fläche ein, statt es hart abzuschneiden. */
/* Seit die Sequenz "contain" zeichnet, liegen oben und unten ohnehin
   Flächen in Seitenfarbe. Der Schleier muss daher nur noch die Bildkanten
   weich anbinden — eine kräftige Vignette würde jetzt die Bildecken
   abdunkeln, statt etwas zu verbinden. */
/* Seit die Szene ein begrenztes Fenster ist, darf hier KEIN ganzflächiger
   Verlauf mehr liegen: Über dem einfarbigen Grund erzeugte er sichtbare
   Bänder. Die Lesbarkeit der Sätze löst jetzt jeder Satz für sich. */
.assembly__veil {
  display: none;
}

/* Kleiner Mono-Titel über dem Bild — er bleibt stehen, während unten die
   Stationen wechseln. */
.assembly__kicker {
  position: absolute;
  bottom: calc(100% + clamp(0.7rem, 2vh, 1.3rem));
  left: 50%;
  translate: -50% 0;
  margin: 0;
  font: 500 var(--t-mono) / 1 var(--font-mono);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--brand-soft);
  white-space: nowrap;
}

/* Der Textbereich unter dem Bild. Alle Stationen liegen übereinander im
   selben Kasten und wechseln sich per Fortschritt ab — dadurch springt
   das Layout nicht, egal wie lang der einzelne Satz ist. */
.assembly__script {
  position: absolute;
  top: calc(100% + clamp(0.8rem, 2.2vh, 1.5rem));
  left: 50%;
  translate: -50% 0;
  width: min(100% - 2rem, 44rem);
  /* Alle Stationen liegen im SELBEN Rasterfeld: Der Kasten ist damit exakt
     so hoch wie der längste Satz, und nichts springt beim Wechsel.
     Vorher stand hier eine feste Höhe (clamp(5.5rem, 12vh, 7.5rem)). Die war
     auf die Standard-Schriftgröße gerechnet und lief über, sobald der Satz
     mehr Zeilen brauchte: bei 320 px Breite um 5,5 px, bei 20 px
     Browser-Grundschrift um 16 px, bei 24 px um 39 px — der Text schob sich
     dann in die Ladeanzeige. Das Rasterfeld kennt diese Grenze nicht. */
  display: grid;
}

.assembly__caption {
  grid-area: 1 / 1;
  margin: 0;
  max-width: none;
  text-align: center;
  text-wrap: balance;
  /* Sichtfenster --a…--b mit weichen Rändern: voll sichtbar in der Mitte,
     an beiden Enden über 0,07 Fortschritt aus- bzw. eingeblendet. */
  opacity: max(
    0,
    min(
      1,
      calc((var(--seq, 0) - var(--a)) / 0.07),
      calc((var(--b) - var(--seq, 0)) / 0.07)
    )
  );
}
.assembly__caption b {
  display: block;
  font-family: var(--font-display);
  font-size: clamp(1.3rem, 0.85rem + 1.85vw, 2.3rem);
  font-weight: 600;
  font-variation-settings: "SOFT" 30, "WONK" 1;
  line-height: 1.1;
  letter-spacing: -0.02em;
  color: var(--fg);
  text-shadow: 0 1px 3px oklch(0.1 0.01 150 / 0.7);
}
.assembly__caption span {
  display: block;
  margin-top: 0.45rem;
  font-size: var(--t-small);
  line-height: 1.45;
  color: var(--fg-dim);
}
.assembly__caption b .breakout {
  display: inline;
  margin: 0;
  font-size: inherit;
  color: var(--brand-soft);
}

.assembly__loader {
  position: absolute;
  inset: auto 0 1.25rem 0;
  text-align: center;
  font: 500 var(--t-mono) / 1 var(--font-mono);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fg-faint);
  transition: opacity var(--dur-slow) var(--ease-out-expo);
}
.assembly__loader[data-hidden="true"] {
  opacity: 0;
}

/* Ohne JS bleibt die Fläche leer — dann lieber gar keine Scrollstrecke. */
html:not(.js) .assembly {
  height: auto;
}
html:not(.js) .assembly__sticky {
  height: auto;
  aspect-ratio: 16 / 9;
}

/* Bei reduzierter Bewegung zeigt sequence.js nur das letzte Standbild und
   setzt --seq auf 1. Dann wäre von den vier Stationen nur die letzte
   sichtbar — drei Aussagen fielen ersatzlos weg. Deshalb wird der
   Abschnitt hier zu einem ruhigen Block: ein Bild, darunter alle vier
   Sätze untereinander. Barrierefreiheit ist Teil des Geschmacks, nicht
   ein Nachtrag (ds-geschmacksprofil #11). */
@media (prefers-reduced-motion: reduce) {
  .assembly {
    height: auto;
    padding-block: var(--section-y-tight);
  }
  .assembly__sticky {
    position: static;
    height: auto;
  }
  .assembly__kicker {
    position: static;
    translate: none;
    inset: auto;
    width: 100%;
    margin: 0 0 0.9rem;
    text-align: center;
  }
  .assembly__script {
    position: static;
    translate: none;
    width: min(100% - 2rem, 44rem);
    height: auto;
    margin: clamp(1.25rem, 3vw, 2rem) auto 0;
    display: grid;
    gap: clamp(1rem, 2.5vw, 1.75rem);
  }
  .assembly__caption {
    /* Das Stapel-Rasterfeld muss hier aufgehoben werden: Bei reduzierter
       Bewegung sollen die vier Sätze UNTEREINANDER stehen, nicht
       übereinander. */
    grid-area: auto;
    opacity: 1;
  }
  /* Der Kochfaden wird sonst erst beim Scrollen gezeichnet — hier liegt
     er von Anfang an vollständig da. */
  .method__thread .live {
    stroke-dashoffset: 0;
  }
}

/* =================== Die gebrochenen Kanten der Seite ==================== */
/* Kein Radius, der überall gleich wiederholt wird — sondern ein kleines
   Vokabular. Jede Form kommt genau einmal vor und sagt etwas über den
   Abschnitt, an dem sie steht (ds-geschmacksprofil #8):
     · Schüsselrand  — Zutaten: die Fläche taucht aus dem Grund auf
     · Welle         — Ablaufdatum: etwas läuft
     · S-Kurve       — Zubereitung: der Weg durch die Schritte
     · Tellerrand    — Ergebnis: tiefer Bogen, das Gericht selbst
     · Schale        — die Schritt-Bühnen, seitenverkehrt zum Layout
   Die Fußzeile bleibt bewusst gerade. */

.bowl {
  border-radius: var(--bowl);
}

/* Welle. In Seitenfarbe gefüllt und über die angehobene Fläche gelegt:
   Die Unterkante des Pfades ist damit die sichtbare Abschnittsgrenze —
   ohne die Fläche darunter zu beschneiden. */
.wave {
  position: absolute;
  inset: -1px 0 auto 0;
  width: 100%;
  height: clamp(2.25rem, 4.5vw, 4.5rem);
  display: block;
  fill: var(--bg);
  pointer-events: none;
  z-index: 1;
}
/* Die Kante selbst. Ohne sie war die Welle unsichtbar: --bg (#101210) auf
   --bg-raised (#161916) sind zwei fast gleiche Dunkelgrau, der Formunterschied
   trägt auf dem Bildschirm nichts. Der Pfad hatte zudem `fill="none"` und gar
   keine stroke-Regel — er hat also überhaupt nichts gezeichnet.
   Jetzt liegt eine salbeifarbene Haarlinie auf dem Schwung: Die Welle ist
   gezeichnet, nicht bloß gefüllt. */
.wave__line {
  stroke: oklch(0.68 0.075 158 / 0.4);
  stroke-width: 1.5;
  vector-effect: non-scaling-stroke;
}

/* =========================== 3 · ZUTATEN ================================= */

.ingredients {
  /* Der Bogen frisst oben Fläche: ohne Zuschlag liefe die erste Zeile in
     die Rundung hinein. */
  padding: calc(var(--section-y) + clamp(1rem, 3vw, 2.5rem)) 0 var(--section-y);
  background: var(--bg-raised);
  border-block: 1px solid var(--border);
}
.ingredients__head {
  display: grid;
  gap: var(--gap);
  margin-bottom: var(--gap);
}
@media (min-width: 56rem) {
  .ingredients__head {
    grid-template-columns: 1fr 1fr;
    align-items: end;
  }
}
.ingredients__aside {
  align-self: end;
  color: var(--fg-dim);
}

/* Sechs Angaben im Raster statt untereinander. Die Karte trägt die Zahl
   groß, den Punkt kurz und die Erklärung klein — dieselbe Information wie
   die frühere Liste auf rund einem Drittel der Höhe. */
.ing-grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.6rem;
  grid-template-columns: 1fr;
}
@media (min-width: 34rem) {
  .ing-grid {
    grid-template-columns: 1fr 1fr;
  }
}
/* Sechs gleich große Karten mit gleichem Padding und gleichem Radius sind
   genau das Vorlagen-Raster, das hier eigentlich vermieden werden soll — und
   vier der sechs Zahlen sind 0 oder 1, die Großzahl-Behandlung zahlt sich
   also nur bei 449 aus. Deshalb ein Bento: Die beiden Karten, die etwas zu
   sagen haben (449 Rezepte, keine Tracker), nehmen doppelte Breite, die
   Zählwerte dazwischen bleiben schmal. Acht Felder, zwei saubere Reihen. */
@media (min-width: 62rem) {
  .ing-grid {
    grid-template-columns: repeat(4, 1fr);
  }
  .ing-grid li:first-child,
  .ing-grid li:last-child {
    grid-column: span 2;
  }
  /* Die 449 ist die einzige Zahl der Seite, die für sich steht — sie darf
     den Platz auch nutzen. */
  .ing-grid li:first-child .qty {
    font-size: clamp(2.6rem, 1.6rem + 2.6vw, 4rem);
  }
  .ing-grid li:first-child b {
    font-size: var(--t-body);
  }
}

.ing-grid li {
  position: relative;
  display: grid;
  gap: 0.15rem;
  align-content: start;
  padding: clamp(0.9rem, 1.8vw, 1.25rem) clamp(1rem, 2vw, 1.35rem);
  border-radius: var(--radius-lg);
  background: var(--bg-card);
  border: 1px solid var(--border);
  transition:
    border-color var(--dur-fast) var(--ease-out-expo),
    transform var(--dur-fast) var(--ease-out-back);
}
.ing-grid li:hover {
  border-color: var(--border-strong);
  transform: translateY(-2px);
}
.js .ing-grid li {
  opacity: 0;
  translate: 0 14px;
  transition:
    opacity 620ms var(--ease-out-expo),
    translate 620ms var(--ease-out-expo),
    border-color var(--dur-fast) var(--ease-out-expo),
    transform var(--dur-fast) var(--ease-out-back);
  transition-delay: var(--d, 0ms);
}
.js .ing-grid li.is-in {
  opacity: 1;
  translate: 0 0;
}

.ing-grid .qty {
  font-family: var(--font-display);
  font-size: clamp(1.9rem, 1.4rem + 1.4vw, 2.6rem);
  font-weight: 600;
  font-variation-settings: "SOFT" 20, "WONK" 1, "opsz" 120;
  line-height: 1;
  letter-spacing: -0.035em;
  color: var(--brand-soft);
  font-variant-numeric: tabular-nums;
}
.ing-grid .qty small {
  font-size: 0.45em;
  font-weight: 500;
  color: var(--fg-faint);
  letter-spacing: 0;
}
.ing-grid b {
  font-size: var(--t-small);
  font-weight: 600;
  line-height: 1.3;
  margin-top: 0.15rem;
}
.ing-grid em {
  font-style: normal;
  font-size: var(--t-micro);
  line-height: 1.45;
  color: var(--fg-faint);
}

/* ========================= 4 · ZUBEREITUNG =============================== */

.method {
  position: relative;
  padding: var(--section-y) 0;
}
.method__head {
  margin-bottom: clamp(1.75rem, 3vw, 3rem);
  max-width: 46rem;
}

/* Der Kochfaden. War eine gerade Linie — jetzt eine Kurve, die sich um die
   Schritte herumlegt und mit dem Scrollen gezeichnet wird.
   preserveAspectRatio="none" streckt die Kurve auf die Abschnittshöhe;
   vector-effect hält die Strichstärke dabei konstant. pathLength="1" im
   Markup normiert die Länge, damit stroke-dashoffset direkt der
   Fortschrittswert 0…1 sein kann. */
/* Der Faden spannt sich exakt über die Schrittliste — deshalb sitzt er in
   .method__flow und nicht im Abschnitt. */
.method__flow {
  position: relative;
}
.method__thread {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  pointer-events: none;
  overflow: visible;
  mask-image: linear-gradient(transparent, #000 5%, #000 95%, transparent);
}
/* Schritte über den Faden legen, damit er wirklich dahinter durchläuft. */
.method__steps {
  position: relative;
  z-index: 1;
}
.method__thread path {
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}
/* Der Faden ist die Signaturidee dieses Abschnitts — bei --border (Weiß auf
   10 %) war er auf dem dunklen Grund praktisch nicht vorhanden. Die
   ungezeichnete Strecke liegt jetzt als salbeifarbene Spur da, damit sichtbar
   ist, wohin der Weg noch führt. */
.method__thread .bed {
  stroke: oklch(0.68 0.075 158 / 0.28);
  stroke-width: 2.5;
}
.method__thread .live {
  stroke: var(--brand-soft);
  stroke-dasharray: 1;
  stroke-dashoffset: calc(1 - var(--read-method, 0));
}
/* Ein Glimmen auf dem gezeichneten Stück, damit der Faden lebt statt nur
   länger zu werden. */
.method__thread .live {
  filter: drop-shadow(0 0 5px oklch(0.68 0.075 158 / 0.5));
}
@media (max-width: 61.99rem) {
  .method__thread {
    display: none;
  }
}

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

.step {
  display: grid;
  gap: clamp(1.25rem, 3vw, 2.5rem);
  align-items: center;
  padding: clamp(1.25rem, 2.5vw, 2.25rem) 0;
  grid-template-columns: 1fr;
}
@media (min-width: 62rem) {
  .step {
    /* Die Bildspalte bekommt mehr Breite als die Textspalte: Der Screenshot
       ist hier der Beweis, der Text nur die Bildunterschrift. Bei 1fr/1fr
       war das Telefon 328 px breit, die App-Oberfläche darin landete bei
       8–11 px und war schlicht nicht lesbar. */
    grid-template-columns: 0.88fr 1.12fr;
    padding-left: 4.5rem;
  }
  .step:nth-child(even) .step__visual {
    order: -1;
  }
}

.step__no {
  display: flex;
  align-items: baseline;
  gap: 0.9rem;
  margin-bottom: var(--gap-tight);
}
.step__no b {
  font-family: var(--font-display);
  font-size: 1.1rem;
  font-weight: 600;
  color: oklch(0.98 0.01 155);
  background: var(--brand);
  width: 2.1rem;
  height: 2.1rem;
  display: grid;
  place-items: center;
  border-radius: 50%;
  flex: none;
  font-variant-numeric: lining-nums;
}
.step__no span {
  font: 500 var(--t-mono) / 1 var(--font-mono);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fg-faint);
}
.step h3 {
  margin-bottom: var(--gap-tight);
}
.step p {
  color: var(--fg-dim);
  margin-bottom: 0;
}

/* Auf dunklem Grund trägt kein zweites Dunkel — die Bühne wird durch eine
   angehobene Fläche plus Salbei-Licht von oben abgesetzt.

   Der Rahmen hat ein festes Seitenverhältnis und ist niedriger als das
   Gerät: Das Telefon steigt in den Rahmen hinein und läuft unten heraus.
   In voller Höhe stand daneben eine halbe Bildschirmhöhe Leere, weil der
   Text nur ein Fünftel davon füllt. */
.step__visual {
  position: relative;
  display: grid;
  place-items: start center;
  aspect-ratio: 1 / 0.9;
  padding: clamp(1.25rem, 2.4vw, 2rem) clamp(1rem, 2vw, 1.75rem) 0;
  /* Sechs Schritte sind eine Reihe — sie brauchen dieselbe Form, sonst
     wirken sie wie sechs verschiedene Dinge. Nur groß gerundet, ohne
     eigene Kurve: Die Kurve dieses Abschnitts ist der Faden dazwischen. */
  border-radius: var(--radius-xl);
  border: 1px solid var(--border);
  /* Die Bühne muss HELLER sein als das, was auf ihr steht. Vorher lag eine
     dunkle App-Oberfläche auf --bg-card auf dunklem Seitengrund — drei fast
     gleiche Dunkelwerte übereinander, das Ergebnis waren sechs graue Klötze.
     Jetzt trägt die Fläche --bg-lift und ein deutliches Salbei-Licht von
     oben; das Gerät hebt sich davon ab, statt darin zu versinken. */
  background:
    radial-gradient(
      120% 85% at 50% -14%,
      oklch(0.68 0.075 158 / 0.2) 0%,
      transparent 62%
    ),
    var(--bg-lift);
  box-shadow: var(--shadow-far);
  overflow: hidden;
}
/* Geister-Ziffer als Textur hinter dem Gerät — Text als Grafik. */
.step__visual::before {
  content: attr(data-ghost);
  position: absolute;
  inset: auto -2% -18% auto;
  font-family: var(--font-display);
  font-size: clamp(9rem, 22vw, 16rem);
  font-weight: 700;
  line-height: 0.72;
  /* 0.045 war nicht schwach, sondern unsichtbar — die Ziffer hat nur
     gerauscht. Entweder sie ist als Textur da oder sie gehört weg. */
  color: oklch(0.94 0.01 155 / 0.1);
  pointer-events: none;
  font-variant-numeric: lining-nums;
}
/* Die Quelle ist 1000 px breit (1320 px Original nach Breite resampelt,
   nicht nach der längeren Seite — das war der Grund für die vorherige
   Matschdarstellung). Bei 22 rem Anzeigebreite bleiben rund 2,8×
   Gerätepixel übrig, also auch auf Retina scharf. */
.step__visual img {
  position: relative;
  /* 20,5 rem waren 328 px — bei 1000 px Quellbreite ein Maßstab von 0,33,
     also App-Text bei 8–11 px. 27 rem sind 432 px und damit rund ein Drittel
     mehr Zeichnung; die Beschriftungen in der App werden lesbar. */
  width: min(100%, 27rem);
  /* Ohne height: auto zieht der Browser die Höhe aus dem height-Attribut
     (2172 px) statt aus dem Seitenverhältnis — solange das Bild wegen
     loading="lazy" noch nicht geladen ist, sprengt das den Abschnitt. */
  height: auto;
  border-radius: 20px 20px 0 0;
  /* Heller Rand als Gerätekante: Ohne ihn floss der dunkle Screenshot in die
     Bühne aus, mit ihm liest er sich als Display. */
  box-shadow:
    0 0 0 1px oklch(0.94 0.01 155 / 0.3),
    0 30px 60px -26px oklch(0 0 0 / 0.95);
}

/* Die Schnittkante unten weich auslaufen lassen: sonst wirkt das Gerät
   abgeschnitten statt eintauchend. */
/* Die Unterkante muss als Eintauchen lesbar sein, nicht als Beschnitt. Der
   Verlauf war mit 15 % zu kurz und traf mit --bg-card die falsche Farbe (die
   Bühne ist jetzt --bg-lift) — deshalb sah das Gerät abgeschnitten aus. */
.step__visual::after {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: 26%;
  pointer-events: none;
  background: linear-gradient(transparent, var(--bg-lift) 80%);
}

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .step__visual img {
      animation: rise both linear;
      animation-timeline: view(block);
      animation-range: entry 6% cover 62%;
    }
  }
}
@keyframes rise {
  from {
    translate: 0 44px;
    scale: 0.94;
  }
  to {
    translate: 0 0;
    scale: 1;
  }
}

/* ========================= 5 · DAS ABLAUFDATUM =========================== */
/* Der einzige Ort auf der ganzen Seite, an dem Gold vorkommt.
   Die Farbe markiert einen Zustand — sie dekoriert nicht.               */

.expiry {
  position: relative;
  padding: calc(var(--section-y) + clamp(1rem, 3vw, 2.5rem)) 0 var(--section-y);
  background: var(--bg-raised);
  /* Oben trennt die Welle, nicht eine Linie. */
  border-bottom: 1px solid var(--border);
  overflow: clip;
}
.expiry__grid {
  display: grid;
  gap: clamp(2.25rem, 5vw, 4rem);
  align-items: center;
  grid-template-columns: 1fr;
}
@media (min-width: 58rem) {
  .expiry__grid {
    grid-template-columns: 0.85fr 1.15fr;
  }
}

.bignum {
  font-family: var(--font-display);
  font-size: var(--t-numeral);
  font-weight: 600;
  font-variation-settings: "SOFT" 18, "WONK" 1, "opsz" 144;
  line-height: 0.82;
  letter-spacing: -0.045em;
  color: var(--gold);
  margin: 0 0 var(--gap-tight);
  font-variant-numeric: lining-nums tabular-nums;
}
.bignum small {
  font-size: 0.24em;
  letter-spacing: -0.01em;
  color: var(--fg-dim);
}
.expiry__claim {
  font-size: var(--t-h3);
  margin-bottom: var(--gap);
}
.expiry__text {
  color: var(--fg-dim);
}
.source {
  font-size: var(--t-micro);
  color: var(--fg-faint);
  max-width: 44ch;
  margin: 0;
}

/* Vorratszeilen: Salbei = frisch, Gold = läuft ab. Die Farbe erzählt hier
   tatsächlich die Zeit — sie ist nicht Dekoration. */
/* Gedeckelt: In der 1,15fr-Spalte wurden die Zeilen 645 px breit für einen
   Punkt, ein Wort und ein Datum — rund 450 px davon waren Leere, die Zeilen
   wirkten auseinandergezogen. */
.shelf {
  list-style: none;
  margin: 0 0 var(--gap-wide);
  padding: 0;
  display: grid;
  gap: 0.55rem;
  max-width: 32rem;
}
.shelf li {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  padding: 0.9rem 1.15rem;
  border-radius: var(--radius);
  background: var(--bg-card);
  border: 1px solid var(--border);
  font-size: var(--t-small);
}
.shelf .dot {
  width: 0.6rem;
  height: 0.6rem;
  border-radius: 50%;
  background: var(--brand-soft);
  flex: none;
  transition: background 500ms var(--ease-out-expo);
  transition-delay: var(--d, 0ms);
}
.js .shelf li.is-in .dot {
  background: var(--state, var(--brand-soft));
}
.shelf .name {
  font-weight: 600;
  flex: 1;
}
.shelf .when {
  font: 500 var(--t-micro) / 1 var(--font-mono);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-faint);
}
.shelf li[data-state="soon"] .when {
  color: var(--gold);
}

/* Sprechblasen-Form statt Standardradius: Das ist ein Vorschlag, keine
   weitere Zeile in einer Liste. */
.rescue {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  max-width: 32rem;
  padding: 1.05rem 1.35rem;
  border-radius: var(--radius-lg) var(--radius-lg) var(--radius-lg) 0.5rem;
  background: var(--brand-veil);
  border: 1px solid oklch(0.68 0.075 158 / 0.32);
  font-size: var(--t-small);
  font-weight: 500;
}
.rescue b {
  font-weight: 700;
  color: var(--brand-lift);
}

/* ========================== 6 · ERGEBNIS ================================= */

.result {
  /* Der Zuschlag für den überhängenden Kasten ist wieder weg — der Text
     steht jetzt im Fluss unter dem Foto und ragt nirgends hinaus. */
  padding: var(--section-y) 0 var(--section-y-tight);
  text-align: center;
  overflow: clip;
}
.result__head {
  width: var(--shell-narrow);
  margin-inline: auto;
}
.result .lead {
  margin-inline: auto;
  max-width: 44ch;
}
.result__kicker {
  justify-content: center;
}

/* Bildband, das sich mit dem Scrollen bewegt — an die Eingabe gekoppelt,
   nicht an einen Timer. Steht still, wenn der Nutzer still steht. */
.ribbon {
  margin-top: clamp(1.5rem, 3vw, 2.5rem);
  overflow: hidden;
  padding-block: 0.5rem;
  mask-image: linear-gradient(
    90deg,
    transparent,
    #000 8%,
    #000 92%,
    transparent
  );
}
.ribbon__track {
  display: flex;
  gap: 1rem;
  width: max-content;
  translate: -12% 0;
}
.ribbon figure {
  margin: 0;
  width: clamp(9rem, 17vw, 14rem);
  flex: none;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--bg-card);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-mid);
  position: relative;
}
/* height: auto ist Pflicht, sobald ein Bild width-/height-Attribute trägt:
   Ohne diese Angabe zieht der Browser die Höhe aus dem Attribut (575 px)
   statt aus dem Seitenverhältnis — die Karten wurden dadurch doppelt so
   hoch wie vorgesehen. Gleiches galt für die Schritt-Screenshots. */
.ribbon img {
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 5;
  object-fit: cover;
}
.ribbon figcaption {
  position: absolute;
  inset: auto 0 0 0;
  padding: 1.9rem 0.8rem 0.7rem;
  background: linear-gradient(transparent, oklch(0.12 0.01 150 / 0.92));
  color: oklch(0.97 0.008 150);
  font: 600 var(--t-micro) / 1.25 var(--font-sans);
  text-align: left;
}
.ribbon figcaption span {
  display: block;
  font-family: var(--font-mono);
  font-weight: 500;
  opacity: 0.72;
  margin-top: 0.15rem;
}

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .ribbon__track {
      animation: drift both linear;
      animation-timeline: view(block);
      animation-range: cover 0% cover 100%;
    }
  }
}
@keyframes drift {
  from {
    translate: 2% 0;
  }
  to {
    translate: -26% 0;
  }
}

/* ===================== Der Schlussakkord: das Gericht ==================== */
/* Zwei Anläufe an dieser Stelle sind gescheitert, weil beide die FORM
   repariert haben — erst die Ecke, dann den Kasten. Das Problem war aber nie
   die Form: Der Abschnitt hat nichts erzählt. Ein Foto und ein Name, mehr
   stand da nicht.

   Jetzt schließt er den Bogen der Seite. Oben im Ablaufdatum-Abschnitt liegen
   vier Posten im Fach, drei davon kurz vorm Verderben — und der Vorschlag
   dort heißt bereits „Zucchini-Lachs-Pasta, verbraucht drei der vier Posten".
   Genau das ist hier zu sehen: Das Foto ist beschriftet wie in einem
   Kochbuch, weil die ZUTATEN der Punkt sind und nicht das Bild. Gold heißt
   auch hier „läuft ab" — die Farbe wurde weiter oben eingeführt und muss
   nicht erklärt werden.

   Die Form ist deshalb bewusst ruhig: ein scharfkantiges Rechteck, das
   einzige der ganzen Seite, mit einer Passepartout-Linie im Abstand. Ein
   gerahmter Abzug, kein UI-Element. Daran kann nichts als verunglückter
   Radius gelesen werden, weil kein Radius da ist. */

.dish {
  position: relative;
  width: min(100% - 2.5rem, 62rem);
  margin: clamp(2rem, 4.5vw, 3.5rem) auto 0;
}

.dish__stage {
  position: relative;
  width: min(100%, 30rem);
  margin-inline: auto;
}

.dish__frame {
  position: relative;
  aspect-ratio: 4 / 5;
  overflow: hidden;
  background: var(--bg-card);
  /* Das Passepartout. `outline-offset` zeichnet die Linie AUSSERHALB des
     Rahmens, ohne dass sie vom overflow: hidden des Bildes beschnitten wird
     — deshalb outline und nicht border oder Pseudo-Element. */
  outline: 1px solid oklch(0.68 0.075 158 / 0.28);
  outline-offset: clamp(0.7rem, 1.8vw, 1.15rem);
}
.dish__frame img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 46%;
}

.dish__notes {
  position: absolute;
  inset: 0;
  list-style: none;
  margin: 0;
  padding: 0;
}
.dish__note {
  position: absolute;
  inset: 0;
}

/* Die Leitlinie läuft vom Punkt zum Rand — sie zeigt auf etwas. */
.dish__note::before {
  content: "";
  position: absolute;
  top: var(--y);
  height: 1px;
  background: var(--note-line, oklch(0.68 0.075 158 / 0.45));
}
.dish__note[data-side="left"]::before {
  left: 0;
  width: var(--x);
  transform-origin: 100% 50%;
}
.dish__note[data-side="right"]::before {
  left: var(--x);
  right: 0;
  transform-origin: 0 50%;
}

.dish__dot {
  position: absolute;
  left: var(--x);
  top: var(--y);
  width: 0.55rem;
  height: 0.55rem;
  translate: -50% -50%;
  border-radius: 50%;
  background: var(--note-color, var(--brand-lift));
  /* Dunkler Hof, damit der Punkt auf hellem Essen nicht verschwindet. */
  box-shadow: 0 0 0 3px oklch(0.1 0.01 150 / 0.5);
}

.dish__label {
  position: absolute;
  top: var(--y);
  translate: 0 -50%;
  width: max-content;
  max-width: 11rem;
  line-height: 1.25;
}
.dish__note[data-side="left"] .dish__label {
  right: calc(100% + clamp(1.25rem, 2.4vw, 2.1rem));
  text-align: right;
}
.dish__note[data-side="right"] .dish__label {
  left: calc(100% + clamp(1.25rem, 2.4vw, 2.1rem));
  text-align: left;
}
.dish__label b {
  display: block;
  font-family: var(--font-display);
  font-size: 1.08rem;
  font-weight: 600;
  font-variation-settings: "SOFT" 30, "WONK" 1;
  letter-spacing: -0.015em;
}
.dish__label em {
  display: block;
  margin-top: 0.12rem;
  font: 500 var(--t-micro) / 1.35 var(--font-mono);
  font-style: normal;
  letter-spacing: 0.06em;
  color: var(--fg-faint);
}
/* Gold ist auf dieser Seite der Zustand „läuft ab" — hier ebenso. */
.dish__note[data-state="soon"] {
  --note-color: var(--gold);
  --note-line: oklch(0.82 0.15 88 / 0.5);
}
.dish__note[data-state="soon"] .dish__label em {
  color: var(--gold);
}

.dish__caption {
  margin: clamp(2.5rem, 5vw, 4rem) auto 0;
  max-width: 34rem;
  text-align: center;
}
.dish__caption h3 {
  margin-bottom: 0.5rem;
}
.dish__caption p {
  margin-inline: auto;
  color: var(--fg-dim);
  font-size: var(--t-small);
}
.dish__caption p b {
  color: var(--gold);
  font-weight: 600;
}
.dish__caption .btn {
  margin-top: var(--gap-tight);
}

/* Unter 62 rem ist neben dem Bild kein Platz für Beschriftungen. Dann werden
   sie zu einer Reihe von Pillen darunter — dieselbe Sprache wie die
   Vorratszeilen weiter oben, inklusive Punktfarbe. */
@media (max-width: 61.99rem) {
  .dish__notes {
    position: static;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.45rem;
    margin-top: clamp(2rem, 7vw, 3rem);
  }
  .dish__note {
    position: static;
    display: inline-flex;
    align-items: center;
    gap: 0.55rem;
    padding: 0.5rem 0.95rem;
    border-radius: var(--radius-pill);
    border: 1px solid var(--border);
    background: var(--bg-card);
  }
  .dish__note::before {
    display: none;
  }
  .dish__dot {
    position: static;
    /* `display` MUSS mit: Ohne position: absolute fällt das <span> auf
       inline zurück, und ein Inline-Element nimmt weder width noch height
       an — der Punkt war dann 0 px breit und schlicht weg. */
    display: block;
    translate: none;
    flex: none;
    box-shadow: none;
  }
  .dish__note[data-side="left"] .dish__label,
  .dish__note[data-side="right"] .dish__label {
    position: static;
    translate: none;
    text-align: left;
    max-width: none;
  }
  .dish__label b {
    font-size: var(--t-small);
  }
}

/* --- Bewegung: das Bild deckt sich auf, dann zeigen die Punkte darauf --- */

.js .dish__note {
  opacity: 0;
  transition: opacity 620ms var(--ease-out-expo);
  transition-delay: var(--d, 0ms);
}
.js .dish__note.is-in {
  opacity: 1;
}
.js .dish__note::before {
  transform: scaleX(0);
  transition: transform 760ms var(--ease-out-expo);
  transition-delay: var(--d, 0ms);
}
.js .dish__note.is-in::before {
  transform: scaleX(1);
}
.js .dish__dot {
  scale: 0;
  transition: scale 560ms var(--ease-spring);
  transition-delay: var(--d, 0ms);
}
.js .dish__note.is-in .dish__dot {
  scale: 1;
}

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .dish__frame img {
      animation: dish-unveil both linear;
      animation-timeline: view(block);
      animation-range: entry 0% cover 58%;
    }
  }
}
@keyframes dish-unveil {
  from {
    clip-path: inset(0 0 34% 0);
    scale: 1.07;
  }
  to {
    clip-path: inset(0 0 0 0);
    scale: 1;
  }
}

/* ============================================================
   KI-KENNZEICHNUNG
   Steht unter den Gerichtsfotos, nicht im Kleingedruckten. Kein
   Warnkasten: Die Angabe ist eine Fußnote zum Bild, so wie eine
   Bildquelle in einer Zeitung — sichtbar, aber ohne Alarm.
   ============================================================ */
.ai-note {
  display: flex;
  gap: 0.65rem;
  align-items: baseline;
  justify-content: center;
  flex-wrap: wrap;
  margin-top: clamp(1.75rem, 4vw, 2.75rem);
  font-size: var(--t-micro);
  line-height: 1.55;
  color: var(--fg-faint);
  text-align: left;
}
.ai-note > span:last-child {
  max-width: 56ch;
}
.ai-note__tag {
  flex: none;
  align-self: center;
  padding: 0.2rem 0.45rem;
  border-radius: var(--radius-pill);
  background: var(--brand-veil);
  border: 1px solid var(--border);
  font: 600 var(--t-micro) / 1 var(--font-mono);
  letter-spacing: 0.1em;
  color: var(--brand-lift);
}

/* ============================================================
   ANMERKUNGEN — der Schluss des Rezepts
   Keine neue Fläche: Ein Rezept trennt seine Anmerkungen mit einer
   Linie vom Text ab, nicht mit einem Kasten. Darüber liegt das
   Schlussfoto, darunter die angehobene Fußzeile — eine dritte
   Fläche dazwischen würde beide entwerten.
   ============================================================ */
.finale {
  padding: var(--section-y-tight) 0 var(--section-y);
  border-top: 1px solid var(--border);
}
.finale__grid {
  display: grid;
  gap: clamp(2.25rem, 5vw, 4rem);
  align-items: start;
  grid-template-columns: 1fr;
}
@media (min-width: 56rem) {
  .finale__grid {
    grid-template-columns: 1.05fr 0.95fr;
  }
}
.finale__lede {
  margin-top: 1.1rem;
  max-width: 46ch;
  font-size: var(--t-small);
  line-height: 1.6;
  color: var(--fg-faint);
}

/* Liste mit Haarlinien statt Karten: Das Kartenraster gehört den
   Zutaten. Zweimal dieselbe Form auf einer Seite macht aus zwei
   Aussagen ein Muster. */
.finale__facts {
  list-style: none;
  margin: 0;
  padding: 0;
}
.finale__facts li {
  padding: 1rem 0;
  border-top: 1px solid var(--border);
  font-size: var(--t-small);
  line-height: 1.6;
  color: var(--fg-faint);
}
.finale__facts li:last-child {
  border-bottom: 1px solid var(--border);
}
.finale__facts b {
  display: block;
  font-weight: 600;
  color: var(--fg);
  margin-bottom: 0.2rem;
}
.finale__legal {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.35rem;
  margin-top: 1.35rem;
  font-size: var(--t-micro);
}
.finale__legal a {
  color: var(--fg-faint);
  text-decoration-color: var(--border-strong);
  text-underline-offset: 0.25em;
  transition: color var(--dur-fast) var(--ease-out-expo);
}
.finale__legal a:hover,
.finale__legal a:focus-visible {
  color: var(--brand-lift);
}
