/* TVMenu Player — Styles
 *
 * Grundregeln, die sich aus dem Einsatzort ergeben:
 *  - Gelesen wird aus 2–5 m Entfernung -> alles gross, hoher Kontrast.
 *  - Groessen in em, abgeleitet von der Grundschrift in vh (siehe render.js).
 *    Damit sieht 720p genauso aus wie 4K, ohne Media Queries.
 *  - 2,5 % Innenabstand als Overscan-Reserve: manche Fernseher schneiden am
 *    Rand ab, und dann fehlt sonst der erste Buchstabe.
 *  - Keine Webfont-Abhaengigkeit fuers Layout: laedt Google Fonts nicht,
 *    bleibt der Fallback-Stack, und es verschiebt sich nichts Wesentliches.
 */

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: #0a0a0a;
  color: #fff;
  /* Auf dem TV gibt es keinen Mauszeiger und nichts zu markieren */
  cursor: none;
  -webkit-user-select: none;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

body {
  font-family: Inter, 'Helvetica Neue', Arial, sans-serif;
  --bg: #101010;
  --panel: rgba(0, 0, 0, 0.8);
  --text: #ffffff;
  --accent: #ffb300;
  --price: #ffffff;
  --radius: 18px;
  --line: rgba(255, 255, 255, 0.20);
  --line-strong: rgba(255, 255, 255, 0.32);
  --wash: rgba(255, 255, 255, 0.08);
}

/* ===================== Hintergrund ===================== */

.bg {
  position: fixed;
  inset: 0;
  top: 0; left: 0; right: 0; bottom: 0;
  z-index: 0;
  background: var(--bg);
}

/* Sichtbar, solange kein Bild/Video geladen ist: dezenter Verlauf statt
   Schwarzflaeche — ein schwarzer Fernseher sieht immer defekt aus. */
.bg-fallback {
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  background:
    radial-gradient(120% 90% at 15% 20%, rgba(255, 255, 255, 0.10), transparent 60%),
    radial-gradient(90% 80% at 85% 90%, rgba(255, 255, 255, 0.06), transparent 60%),
    var(--bg);
}

.bg-media {
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  --dim: 0.35;
}

.bg-media .bg-fill {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Abdunkeln des Hintergrunds, damit Text darauf lesbar bleibt */
.bg-media::after {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  background: rgba(0, 0, 0, var(--dim));
  pointer-events: none;
}

/* Die Kurve jeder Bewegung in diesem Player.
   Steigung 0 am Anfang und am Ende: nichts schnellt los, nichts schwingt
   ueber, nichts springt auf den Endwert. Dieselben vier Zahlen stehen in
   player/carousel.js (EASE) — die Ebenenordnung des Karussells rechnet
   damit, und sie muss zur sichtbaren Bewegung passen. */
:root { --tv-ease: cubic-bezier(0.4, 0, 0.2, 1); }

/* ===================== Seiten-Ebenen =====================
   Zwei Ebenen wechseln sich ab. Jede traegt ihre Seite MIT eigenem,
   deckendem Hintergrund (.scene-bg); den Uebergang spielt app.js ueber die
   Web Animations API ab (player/transitions.js) — hier stehen nur die
   Ruhelagen. `translate`/`scale` der Uebergaenge kommen zum transform
   hinzu, mit dem die Ebene im Format zentriert wird. */

.layer {
  position: fixed;
  top: 0; left: 0; right: 0; bottom: 0;
  z-index: 1;
  opacity: 0;
  /* Eigene GPU-Ebene: der Wechsel bewegt nur fertig gerasterte Ebenen. */
  transform: translateZ(0);
  pointer-events: none;
  overflow: hidden;
}

.layer.visible { opacity: 1; }
.layer.is-front { z-index: 2; }

/* Die naechste Seite wartet fertig aufgebaut und VOLL gezeichnet HINTER der
   sichtbaren (.behind): jede Seite ist dank eigenem Hintergrund deckend, man
   sieht sie also nicht. Eine Ebene mit Deckkraft 0 rastert der Browser nicht,
   und WebKit zeichnet eine von aussen hereingeschobene Ebene sonst erst beim
   Hereinkommen — das waren die Fragmente und Elemente, die spaeter kamen.
   Ohne deckende Seite davor (Start, anderes Format): fast unsichtbar,
   0,002 liegt unter einer Farbstufe, aber nicht 0. */
.layer.staged { opacity: 0.002; z-index: 0; }
.layer.staged.behind { opacity: 1; }

/* Hintergrund der Seite in ihrer Ebene: liegt unter .page (die ist
   positioniert und folgt im DOM), ohne eigenen Stapelkontext. */
.layer > .scene-bg {
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  z-index: auto;
}

/* Sobald Inhalt laeuft, liegt zwischen zwei Seiten Schwarz (Abblende). */
html.has-content body { background: #000; }
html.has-content body > .bg { visibility: hidden; }

/* ===================== Seite ===================== */

.page {
  position: relative;      /* Bezugsrahmen der frei platzierten Ebenen */
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  padding: 2.5vh 2.5vw;
  color: var(--text);
  /* font-size wird von render.js gesetzt (vh * scale) */
}

/* Seite ohne jeden Inhalt (render.js): weiss statt Hinweistext. */
.page.is-blank { background: #fff; }

.page-body {
  flex: 1 1 auto;
  display: flex;
  min-height: 0;          /* damit die Kinder wirklich schrumpfen duerfen */
  gap: 2vw;
}

.layout-split .page-free {
  flex: 1 1 auto;
  min-width: 0;
}

.page-panel {
  flex: 0 0 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 1.4em;
  overflow: hidden;       /* fitToHeight() verkleinert, statt zu scrollen */
  background: var(--panel);
  border-radius: var(--radius);
  padding: 1.6em 1.8em;
  backdrop-filter: blur(2px);
}

.layout-stack .page-panel {
  flex: 1 1 auto;
  width: 100% !important;
  /* Eine Angebotsseite mit drei Zeilen soll nicht oben kleben und darunter
     halb leer sein. "safe center" zentriert nur, solange der Inhalt passt —
     bei zu viel Inhalt faellt es auf "start" zurueck, statt oben und unten
     abzuschneiden. Aeltere Browser ignorieren die zweite Zeile und behalten
     die einfache Zentrierung. */
  justify-content: center;
  justify-content: safe center;
}

/* Eine Seite nur aus frei platzierten Elementen soll keinen leeren Kasten
   zeigen. */
.page-panel.is-plain {
  background: none;
  backdrop-filter: none;
  border-radius: 0;
  padding-left: 0;
  padding-right: 0;
}

.page-panel.is-empty {
  background: none;
  backdrop-filter: none;
  padding: 0;
}

/* ===================== Frei platzierte Elemente ===================== */

/* Die Ebene liegt ueber der GANZEN Seite, auch ueber dem Innenabstand:
   die Koordinaten sind Prozent des Bildschirms, nicht des Textbereichs.

   Bewusst OHNE z-index: sonst waere sie ein eigener Stapelkontext, und die
   z-index der einzelnen Elemente koennten sich nicht mehr mit dem der
   Textflaeche (.page-body) messen. Welche Zahl ein Element bekommt, setzt
   render.js aus seiner Stelle in der Elementliste. */
.free-layer {
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  pointer-events: none;
  overflow: hidden;        /* ueberstehende Elemente sauber am Rand abschneiden */
}

.free-el {
  position: absolute;
  /* left/top/width/transform/opacity setzt render.js */
  transform-origin: center center;
}

/* Screeneffekt (render.js renderPage): Kopie des Hintergrunds IN der Seite,
   damit mix-blend-mode: screen etwas zum Mischen hat. z-index -1: hinter
   allem, was die Seite zeigt, aber noch in ihrer Ebene. */
.page-backdrop {
  z-index: -1;
  background: var(--bg);
  pointer-events: none;
}

/* Ohne Flaeche: genau das, was ein freigestelltes PNG braucht. */
.free-el > .blk-image,
.free-el > .blk-video,
.free-el > .blk-carousel {
  background: none;
  border: 0;
}

/* --- Hoehe kommt vom Inhalt (h = 0) ---
   Das Bild bringt sein Seitenverhaeltnis selbst mit; der Container richtet
   sich danach. Wichtig fuer freigestellte Logos, die nicht beschnitten werden
   duerfen. */
.free-auto > .blk-image,
.free-auto > .blk-video {
  height: auto;
}

/* Karussell und Video kennen ihre Hoehe nicht aus dem Inhalt (die Bilder
   kommen erst spaeter und koennen verschiedene Formate haben) -> 16:9 als
   Platzhalter, sonst faellt der Block auf null zusammen. */
.free-auto > .blk-carousel {
  aspect-ratio: 16 / 9;
}

.free-auto > .blk-image > .media-fill {
  width: 100%;
  height: auto;
  object-fit: initial;      /* nicht zuschneiden, das Bild bestimmt die Hoehe */
}

/* Video kennt seine Hoehe erst nach dem Laden -> 16:9 als Platzhalter,
   sonst springt das Layout beim Abspielstart. */
.free-auto > .blk-video {
  aspect-ratio: 16 / 9;
}

.free-auto > .blk-video > .media-fill {
  width: 100%;
  height: 100%;
}

/* --- Feste Hoehe (h > 0): das Medium fuellt den Rahmen --- */
.free-fixed > .blk-image,
.free-fixed > .blk-video,
.free-fixed > .blk-carousel {
  height: 100%;
}

.free-fixed > .blk-image > .media-fill,
.free-fixed > .blk-video > .media-fill {
  height: 100%;
}

.free-el > .blk-image.is-missing,
.free-el > .blk-video.is-missing {
  min-height: 6vh;
  border: 0.12em dashed var(--line-strong);
}

/* Optionale Flaeche hinter dem Element — fuer Text und Listen, die sonst auf
   einem Foto untergehen. */
.free-plate {
  background: var(--panel);
  border-radius: var(--radius);
  /* Aendert sich der Innenabstand, auch render.js renderFree anpassen
     (Unterlage mit plateColor: Breite + 2.2em, Hoehe + 1.8em). */
  padding: 0.9em 1.1em;
}

.free-plate > .blk-image,
.free-plate > .blk-video {
  border-radius: calc(var(--radius) * 0.6);
  overflow: hidden;
}

/* Verlaufsschrift in einer eigenen Huelle — noetig, wenn zusaetzlich eine
   Flaeche hinter dem Text liegt (render.js: applyTextStyle). */
.txt-gradient {
  display: inline-block;
  background-clip: text;
  -webkit-background-clip: text;
}

/* --- Formen: reine CSS-Flaechen, keine Bilder --- */
.blk-shape {
  width: 100%;
  box-sizing: border-box;
  transform-origin: center center;
}

/* Frei mit fester Hoehe: die Form fuellt die Huelle. */
.free-fixed > .blk-shape { height: 100%; }

/* Ohne feste Hoehe bringt die Form ihr Seitenverhaeltnis mit. */
.free-auto > .blk-shape.shape-rectangle,
.free-auto > .blk-shape.shape-roundedRectangle { aspect-ratio: 3 / 2; }
.free-auto > .blk-shape.shape-circle { aspect-ratio: 1 / 1; }
.free-auto > .blk-shape.shape-ellipse { aspect-ratio: 2 / 1; }
.free-auto > .blk-shape.shape-polygon { aspect-ratio: 3 / 2; }
/* Freie Form: das SVG fuellt den Kasten, der Rahmen darf ueber den Pfad hinausragen */
.blk-shape.shape-polygon { position: relative; }
.blk-shape > .shape-poly { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
/* Eine Linie hat ihre Dicke aus dem Strich (render.js), nie aus der Huelle. */
.free-el > .blk-shape.shape-line { height: auto; }
.free-fixed > .blk-shape.shape-line { height: auto; }

/* Beschriftung auf einer Form: die Innenflaeche, in die der Text eingepasst
   wird (render.js, fitShapeLabels). Beim Kreis und der Ellipse ist das das
   eingeschriebene Rechteck (Seiten = 70,7 % des Durchmessers) mit etwas Luft,
   beim Rechteck ein Rand ringsum. */
.blk-shape.has-label { position: relative; }
.shape-fill { position: absolute; inset: 0; border-radius: inherit; }
.shape-label {
  position: absolute;
  inset: 12% 9%;
  display: flex;
  align-items: center;
  justify-content: center;
  container-type: size;
  pointer-events: none;
}
.shape-circle > .shape-label,
.shape-ellipse > .shape-label { inset: 17%; }
.shape-roundedRectangle > .shape-label { inset: 14% 11%; }
.shape-label-text {
  width: 100%;
  font-weight: 800;
  line-height: 1.02;
  text-align: center;
  white-space: nowrap;
}
.shape-label-text > .text-line { white-space: pre-wrap; overflow-wrap: normal; word-break: normal; }

/* Eine Form braucht keine Platte — sie ist selbst eine. */
.free-plate.free-shape { background: none; padding: 0; }

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

.blk { flex: 0 0 auto; }

.align-left { text-align: left; }
.align-center { text-align: center; }
.align-right { text-align: right; }

/* --- Kopfzeile --- */

.blk-header {
  display: flex;
  align-items: center;
  gap: 0.8em;
}

.blk-header.align-center { justify-content: center; }
.blk-header.align-right { justify-content: flex-end; }

.hdr-logo {
  height: 3.2em;
  width: auto;
  max-width: 30%;
  object-fit: contain;
  flex: 0 0 auto;
}

.hdr-text { min-width: 0; }

.hdr-title {
  font-size: 2.4em;
  line-height: 1.05;
  font-weight: 800;
  color: var(--accent);
  letter-spacing: 0.01em;
}

.hdr-subtitle {
  font-size: 1.05em;
  opacity: 0.82;
  margin-top: 0.2em;
}

/* --- Speisekarte --- */

.menu-title {
  font-size: 1.45em;
  font-weight: 700;
  color: var(--accent);
  margin-bottom: 0.45em;
  padding-bottom: 0.25em;
  border-bottom: 0.09em solid currentColor;
  /* Die Linie soll dezenter sein als die Schrift selbst — und zur
     Textfarbe passen, sonst ist sie auf hellen Themes unsichtbar. */
  border-bottom-color: var(--line-strong);
}

.menu-items { display: grid; gap: 0.55em 1.6em; }
.menu-items.cols-1 { grid-template-columns: 1fr; }
.menu-items.cols-2 { grid-template-columns: 1fr 1fr; }
.menu-items.cols-3 { grid-template-columns: 1fr 1fr 1fr; }

.item {
  display: flex;
  align-items: baseline;
  gap: 0.6em;
  min-width: 0;
}

/* Die laufende Nummer steckt immer im Markup, sichtbar ist sie nur im Stil
   "numbered" — so bleibt der Wechsel zwischen den Vorgaben verlustfrei. */
.item-no { display: none; }

.item-thumb {
  width: 2.6em;
  height: 2.6em;
  object-fit: cover;
  border-radius: 0.35em;
  flex: 0 0 auto;
  align-self: center;
}

.item-main {
  flex: 1 1 auto;
  min-width: 0;
  /* Punktlinie zum Preis: klassisches Speisekarten-Muster, fuehrt das Auge */
  border-bottom: 1px dotted var(--line);
  padding-bottom: 0.1em;
}

.item-name-row {
  display: flex;
  align-items: baseline;
  gap: 0.45em;
  min-width: 0;
}

.item-name {
  font-size: 1.12em;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.item-badge {
  font-size: 0.62em;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding: 0.2em 0.5em;
  border-radius: 999px;
  background: var(--accent);
  color: #000;
  flex: 0 0 auto;
  white-space: nowrap;
}

.item-desc {
  font-size: 0.8em;
  opacity: 0.72;
  line-height: 1.25;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.item-price {
  flex: 0 0 auto;
  font-size: 1.18em;
  font-weight: 700;
  color: var(--price);
  white-space: nowrap;      /* der Preis bricht nie um */
  font-variant-numeric: tabular-nums;
}

/* ===================== Listen-Vorgaben ===================== */

/* --- Kompakt: ohne Beschreibung, enger gesetzt.
   Fuer lange Getraenkekarten, wo jede Zeile Platz kostet. --- */
.menu-style-compact .item-desc { display: none; }
.menu-style-compact .menu-items { gap: 0.28em 1.4em; }
.menu-style-compact .item-main { border-bottom: 0; }
.menu-style-compact .item-name { font-size: 1.05em; font-weight: 500; }

/* --- Nummeriert: der Klassiker am Imbiss ("einmal die 7, bitte"). --- */
.menu-style-numbered .item-no {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  align-self: center;
  min-width: 1.9em;
  height: 1.9em;
  padding: 0 0.35em;
  border-radius: 0.35em;
  background: var(--accent);
  color: #000;
  font-size: 0.85em;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}

/* --- Großer Preis: Preis dominiert, Name tritt zurueck.
   Fuer Angebotsseiten und Tageskarten. --- */
.menu-style-bigprice .menu-items { gap: 0.75em 1.6em; }
.menu-style-bigprice .item-price {
  font-size: 1.9em;
  line-height: 1;
  color: var(--accent);
}
.menu-style-bigprice .item-name { font-size: 1.05em; font-weight: 600; }
.menu-style-bigprice .item-main { border-bottom: 0; }

/* --- Kacheln: jede Position ein Kaestchen, mit Foto oben.
   Braucht wenige Positionen, wirkt dafuer wie eine Werbetafel. --- */
.menu-style-cards .menu-items { gap: 0.7em; }

.menu-style-cards .item {
  flex-direction: column;
  align-items: stretch;
  gap: 0.35em;
  padding: 0.6em 0.7em;
  border-radius: calc(var(--radius) * 0.7);
  background: var(--wash);
}

.menu-style-cards .item-thumb {
  width: 100%;
  height: 7em;
  border-radius: calc(var(--radius) * 0.5);
  align-self: stretch;
  /* Das ganze Produktfoto, nie ein Ausschnitt: ein Burger ohne Brötchen-
     deckel ist auf einer Werbetafel schlimmer als ein bisschen Rand. */
  object-fit: contain;
}

.menu-style-cards .item-main { border-bottom: 0; }
.menu-style-cards .item-name { white-space: normal; }
.menu-style-cards .item-desc { white-space: normal; }

.menu-style-cards .item-price {
  align-self: flex-start;
  font-size: 1.35em;
  color: var(--accent);
}

/* --- Tafel: Kreidetafel-Anmutung, gesperrte Versalien.
   Passt zu Cafés und zu Serifen- oder Handschriften. --- */
.menu-style-board .menu-title {
  text-transform: uppercase;
  letter-spacing: 0.12em;
  border-bottom-style: dashed;
}

.menu-style-board .item-name {
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 500;
}

.menu-style-board .item-main { border-bottom-style: dashed; }
.menu-style-board .menu-items { gap: 0.7em 1.6em; }
.menu-style-board .item-desc { font-style: italic; }

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

.blk-image, .blk-video {
  position: relative;
  overflow: hidden;
  background: var(--wash);
}

.media-fill {
  width: 100%;
  height: 100%;
  display: block;
}

.fit-cover { object-fit: cover; }
.fit-contain { object-fit: contain; }
.fit-fill { object-fit: fill; }    /* nur ausdruecklich gewaehlt (Editor: feste Hoehe) */

.media-caption {
  font-size: 0.85em;
}

/* Ohne dunklen Verlauf darunter: der lag wie ein Schleier auf dem Foto.
   Lesbar bleibt der Text ueber seine eigenen Mittel (Schatten, Flaeche). */
.media-caption.cap-overlay {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: 0.5em 0.8em;
  color: #fff;
}

/* Farbe eines Bildes (tint) — liegt genau auf dem Motiv. */
.img-tint {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* Unter dem Bild: der Kasten muss dafuer ueberstehen duerfen. */
.media-caption.cap-below {
  position: absolute;
  left: 0; right: 0;
  top: 100%;
  padding-top: 0.4em;
}

/* Fehlendes/kaputtes Medium: klar erkennbar, aber nicht haesslich —
   der Betreiber soll in der App-Vorschau sehen, dass hier etwas fehlt. */
.is-missing {
  border: 0.12em dashed var(--line-strong);
  border-radius: var(--radius);
}

.is-missing::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  width: 2.2em; height: 2.2em;
  margin: -1.1em 0 0 -1.1em;
  border-radius: 0.3em;
  background: var(--line-strong);
}

/* --- Freier Text --- */

.blk-text { line-height: 1.3; }
.text-line { min-height: 1em; }
.emph-accent { color: var(--accent); font-weight: 700; }

.emph-panel {
  background: var(--wash);
  border-radius: var(--radius);
  padding: 0.7em 0.9em;
}

/* ===================== 3D-Drehung (Sprite) ===================== */

/* Ein PNG-Raster aus Einzelbildern einer Drehung, zeilenweise
   durchgeblaettert. Bewegt wird ausschliesslich transform — siehe render.js.
   Der Block hat bewusst keine Flaeche: das Objekt schwebt freigestellt. */

.blk-sprite {
  position: relative;
  background: none;
}

.sprite-stage {
  position: relative;
  width: 100%;
  height: 0;                 /* Hoehe kommt aus padding-top (Seitenverhaeltnis) */
  overflow: hidden;
  transform: translateZ(0);  /* eigene Compositor-Ebene */
}

.sprite-rows,
.sprite-cols {
  position: absolute;
  top: 0;
  left: 0;
  will-change: transform;
}

.sprite-rows { width: 100%; }     /* Hoehe = Zeilen x 100 %, setzt render.js */
.sprite-cols { height: 100%; }    /* Breite = Spalten x 100 %, setzt render.js */

.sprite-sheet {
  position: absolute;
  top: 0; left: 0;
  width: 100%;
  height: 100%;
  display: block;
}

/* Waagerecht durch die Spalten einer Zeile, senkrecht durch die Zeilen.
   -100 % meint jeweils das eigene Element, das cols bzw. rows Bilder breit
   bzw. hoch ist — steps(n) macht daraus genau n Einzelbilder. */
@keyframes sprite-x {
  from { transform: translate3d(0, 0, 0); }
  to { transform: translate3d(-100%, 0, 0); }
}

@keyframes sprite-y {
  from { transform: translate3d(0, 0, 0); }
  to { transform: translate3d(0, -100%, 0); }
}

.sprite-stage.is-missing {
  min-height: 6em;
  border: 0.12em dashed var(--line-strong);
  border-radius: var(--radius);
}

.sprite-caption {
  font-size: 0.95em;
  font-weight: 600;
  text-align: center;
}

.sprite-caption.cap-below { padding-top: 0.35em; }

.sprite-caption.cap-overlay {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: 0.4em 0.6em;
  color: #fff;
  text-shadow: 0 0.06em 0.3em rgba(0, 0, 0, 0.8);
}

/* Frei platziert: der Rahmen gibt die Breite vor, die Hoehe folgt dem
   Seitenverhaeltnis — deshalb ist ein Sprite immer "free-auto". */
.free-el > .blk-sprite {
  width: 100%;
  background: none;
  border: 0;
}

/* ===================== Bilderkarussell ===================== */

/* Grundsatz fuer alles hier: animiert werden NUR transform und opacity.
   Beides laeuft auf dem Compositor, ohne Layout und ohne Neuzeichnen —
   nur so bleibt es auf einem Fire TV Stick fluessig. */

.blk-carousel {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.car-title {
  font-size: 1.45em;
  font-weight: 700;
  color: var(--accent);
  margin-bottom: 0.4em;
}

.car-stage {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
  /* Bewusst ohne Hintergrundflaeche: ein freigestelltes Produktfoto soll frei
     im Bild schweben, nicht in einem Kasten sitzen. Wer eine Flaeche will,
     schaltet in der App "Fläche dahinter" ein. */
  /* Eigene Compositor-Ebene: verhindert, dass der Browser bei jedem
     Folienwechsel die ganze Seite neu zeichnet. */
  transform: translateZ(0);
}

.car-slide {
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  display: flex;
  flex-direction: column;
  opacity: 0;
  will-change: transform, opacity;
  transition-property: transform, opacity;
  transition-timing-function: var(--tv-ease, cubic-bezier(0.4, 0, 0.2, 1));
  /* transition-duration setzt render.js aus der eingestellten Geschwindigkeit */
}

/* Der Kind-Selektor ist hier kein Zufall: die Uebergangsarten weiter unten
   setzen den Ausgangs-transform auf .car-anim-X .car-slide — gleiche
   Spezifitaet wie .car-slide.is-active, und weil sie spaeter stehen, wuerden
   sie gewinnen. Die aktive Folie bliebe dann ausserhalb des Bildes stehen.
   Mit .car-stage > liegt der Aktiv-Zustand eine Stufe hoeher und setzt sich
   durch — die Austritts-Transforms (.car-anim-X .car-slide.is-leaving) sind
   ihrerseits spezifischer und greifen weiterhin. */
.car-stage > .car-slide.is-active {
  opacity: 1;
  transform: none;
  z-index: 2;
}

.car-stage > .car-slide.is-leaving { z-index: 1; }

/* Zuruecksetzen ohne sichtbare Rueckreise */
.car-slide.car-reset { transition: none !important; }

.car-media {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
}

.car-media > .media-fill { width: 100%; height: 100%; }

.car-media.is-missing {
  border: 0.12em dashed var(--line-strong);
  border-radius: calc(var(--radius) * 0.5);
}

.car-stage.is-missing {
  border: 0.12em dashed var(--line-strong);
  border-radius: var(--radius);
  min-height: 6em;
}

/* --- Beschriftung --- */

.car-cap {
  display: flex;
  align-items: baseline;
  gap: 0.5em;
  flex: 0 0 auto;
}

.cap-overlay .car-cap {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: 0.9em 1em 0.8em;
  background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
  /* Ueber dem Bild gilt fast immer Weiss, unabhaengig vom Theme —
     der Farbverlauf darunter ist schwarz. */
  color: #fff;
}

.cap-below .car-cap {
  padding-top: 0.5em;
  justify-content: space-between;
}

.car-cap-name {
  font-size: 1.15em;
  font-weight: 700;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.car-cap-price {
  font-size: 1.25em;
  font-weight: 800;
  color: var(--accent);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  margin-left: auto;
}

.cap-overlay .car-cap-price { color: var(--accent); }

/* --- Fortschrittspunkte --- */

.car-dots {
  flex: 0 0 auto;
  display: flex;
  justify-content: center;
  gap: 0.45em;
  padding-top: 0.55em;
}

.car-dot {
  width: 0.5em;
  height: 0.5em;
  border-radius: 50%;
  background: var(--line-strong);
  transition: transform 0.25s ease, background-color 0.25s ease;
}

.car-dot.is-on {
  background: var(--accent);
  transform: scale(1.35);
}

/* --- Uebergangsarten --- */

/* Ueberblenden: der ruhigste Uebergang, passt immer. */
.car-anim-fade .car-slide { transform: none; }

/* Seitlich schieben: neue Folie kommt von rechts, alte geht nach links. */
.car-anim-slide .car-slide { transform: translate3d(100%, 0, 0); }
.car-anim-slide .car-slide.is-leaving { transform: translate3d(-100%, 0, 0); opacity: 1; }

/* Hochschieben — wirkt auf hochkant haengenden Bildschirmen natuerlicher. */
.car-anim-push .car-slide { transform: translate3d(0, 100%, 0); }
.car-anim-push .car-slide.is-leaving { transform: translate3d(0, -100%, 0); opacity: 1; }

/* Ken Burns: ueberblenden plus langsame Fahrt ins Bild. Das ist der Effekt,
   der aus Standbildern von Gerichten "Bewegtbild" macht. */
.car-anim-kenburns .car-slide { transform: none; }

.car-anim-kenburns .car-slide.is-active .car-media > .media-fill {
  animation: kenburns 14s ease-out both;
  will-change: transform;
}

@keyframes kenburns {
  from { transform: scale(1) translate3d(0, 0, 0); }
  to { transform: scale(1.14) translate3d(-1.5%, -1.5%, 0); }
}

/* Umblaettern. Am teuersten von allen — auf sehr alten Sticks sparsam
   einsetzen. */
.car-anim-flip .car-stage { perspective: 1400px; }
.car-anim-flip .car-slide {
  transform: rotateY(90deg);
  backface-visibility: hidden;
}
.car-anim-flip .car-slide.is-leaving { transform: rotateY(-90deg); opacity: 0; }

/* Ohne Uebergang: harter Schnitt. */
.car-anim-none .car-slide { transition: none !important; transform: none; }

/* ===================== Einblend-Animationen ===================== */

/* Laufen einmal beim Seitenaufbau. Die Ausgangswerte stehen NUR im Keyframe,
   nie als Grundzustand — bleibt die Animation aus (alter Browser, reduzierte
   Bewegung), ist das Element trotzdem sichtbar statt unsichtbar. */

/* Alle Einblendungen benutzen dieselbe Kurve wie das Karussell: Steigung 0
   am Anfang und am Ende. Nichts schnellt los, nichts schwingt ueber, nichts
   springt auf den Endwert. (Vorher lagen hier vier verschiedene Kurven,
   zwei davon mit Ueberschwingen.) */
.anim-fade  { animation: anim-fade 0.6s var(--tv-ease) both; }
.anim-up    { animation: anim-up 0.55s var(--tv-ease) both; }
.anim-down  { animation: anim-down 0.55s var(--tv-ease) both; }
.anim-left  { animation: anim-left 0.55s var(--tv-ease) both; }
.anim-right { animation: anim-right 0.55s var(--tv-ease) both; }
.anim-zoom  { animation: anim-zoom 0.5s var(--tv-ease) both; }

@keyframes anim-fade { from { opacity: 0; } to { opacity: 1; } }

@keyframes anim-up {
  from { opacity: 0; transform: translate3d(0, 12%, 0); }
  to { opacity: 1; transform: none; }
}

@keyframes anim-down {
  from { opacity: 0; transform: translate3d(0, -12%, 0); }
  to { opacity: 1; transform: none; }
}

@keyframes anim-left {
  from { opacity: 0; transform: translate3d(-10%, 0, 0); }
  to { opacity: 1; transform: none; }
}

@keyframes anim-right {
  from { opacity: 0; transform: translate3d(10%, 0, 0); }
  to { opacity: 1; transform: none; }
}

@keyframes anim-zoom {
  from { opacity: 0; transform: scale(0.9); }
  to { opacity: 1; transform: none; }
}

/* Wer Bewegung nicht vertraegt, bekommt sie nicht. Gilt fuer die Vorschau in
   der App; ein Fernseher meldet diese Einstellung praktisch nie. */
@media (prefers-reduced-motion: reduce) {
  .anim-fade, .anim-up, .anim-down, .anim-left, .anim-right, .anim-zoom {
    animation: none;
  }
  .car-anim-kenburns .car-slide.is-active .car-media > .media-fill {
    animation: none;
  }
}

/* ===================== Laufschrift ===================== */

.page-ticker {
  flex: 0 0 auto;
  margin-top: 1.2em;
  overflow: hidden;
  white-space: nowrap;
  background: var(--accent);
  color: #000;
  border-radius: 999px;
  padding: 0.42em 0;
  font-size: 1.05em;
  font-weight: 700;
}

.ticker-track {
  display: inline-block;
  white-space: nowrap;
  /* Bewegung per transform -> laeuft auf dem Compositor, kein Layout-Reflow */
  animation: ticker-scroll 30s linear infinite;
  will-change: transform;
}

.ticker-text { padding-right: 4em; }

@keyframes ticker-scroll {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }   /* Text ist doppelt -> nahtlos */
}

/* ===================== Leerer Bildschirm =====================
   Am Fernseher (index.html, html.screen): solange nichts zu zeigen ist —
   Start, Anmelden, keine Seite, Abo pausiert —, ist der Bildschirm einfach
   weiss. Keine Texte; was los ist, zeigt nur ?debug=1. Kommt die erste
   Seite, geht der Grund weich in das Schwarz zwischen den Seiten ueber. */
html.screen body { transition: background-color 0.8s cubic-bezier(0.4, 0, 0.2, 1); }
html.screen:not(.has-content) body { background: #fff; }
html.screen:not(.has-content) body > .bg { display: none; }

/* ===================== Vollbild-Zustaende ===================== */

/* Nur noch der Kopplungscode: weiss, der Code gross in der Mitte. */
.state {
  position: fixed;
  top: 0; left: 0; right: 0; bottom: 0;
  z-index: 3;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2vh;
  padding: 6vh 8vw;
  text-align: center;
  background: #fff;
  color: #111;
}

.state.hidden { display: none; }

/* Der Kopplungscode ist das Einzige, was jemand vom Bildschirm abtippt —
   entsprechend gross und monospaced. */
.state-code {
  font-family: 'SF Mono', Menlo, Consolas, monospace;
  font-size: 14vh;
  line-height: 1.05;
  font-weight: 800;
  color: #111;
}

.state-hint { font-size: 2.4vh; color: #6b6b6b; }

/* ===================== Offline-Hinweis ===================== */

/* Absichtlich winzig: der Gast soll es nicht bemerken, der Betreiber schon. */
.offline {
  position: fixed;
  right: 1.2vw;
  bottom: 1.2vh;
  z-index: 4;
  display: flex;
  align-items: center;
  gap: 0.5vh;
  padding: 0.5vh 1vh;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.55);
  font-size: 1.5vh;
  opacity: 0.55;
}

.offline.hidden { display: none; }

.offline-dot {
  width: 1vh;
  height: 1vh;
  border-radius: 50%;
  background: #ff5a4d;
}

/* ===================== Debug-Overlay (?debug=1) ===================== */

/* Oben links, damit es die Laufschrift am unteren Rand nicht verdeckt */
.debug {
  position: fixed;
  left: 1vw;
  top: 1vh;
  z-index: 5;
  display: none;
  white-space: pre;
  font-family: Menlo, Consolas, monospace;
  font-size: 1.6vh;
  line-height: 1.45;
  padding: 1vh 1.4vh;
  border-radius: 0.8vh;
  background: rgba(0, 0, 0, 0.72);
  color: #9effa0;
}

.debug.debug { display: block; }

/* ===================== Wachhalter ===================== */

/* Weder display:none noch visibility:hidden — beides laesst Fire OS die
   Wiedergabe drosseln, und genau die Wiedergabe ist der Zweck. Also: 2x2 px,
   praktisch unsichtbar, in der Ecke geparkt. */
.keepawake-video {
  position: fixed;
  right: 0;
  bottom: 0;
  width: 2px;
  height: 2px;
  opacity: 0.01;
  z-index: 6;
  pointer-events: none;
  border: 0;
}

/* Dauerlaufende Compositor-Animation auf einem unsichtbaren Pixel:
   hilft auf Geraeten, die nur bei voelliger Ruhe abdunkeln. */
.keepawake-pulse {
  position: fixed;
  left: 0;
  bottom: 0;
  width: 1px;
  height: 1px;
  opacity: 0.01;
  z-index: 6;
  pointer-events: none;
  background: #fff;
  animation: keepawake-drift 4s linear infinite;
  will-change: transform;
}

@keyframes keepawake-drift {
  from { transform: translateX(0); }
  50% { transform: translateX(1px); }
  to { transform: translateX(0); }
}

/* ===================== Hochformat ===================== */

/* Hochkant haengende Bildschirme: der rechte Panel-Streifen ergibt dann
   keinen Sinn, also nimmt der Inhalt die ganze Breite. */
@media (orientation: portrait) {
  .layout-split .page-free { display: none; }
  .layout-split .page-panel { width: 100% !important; flex: 1 1 auto; }
}


/* ===================== Seitenformat am Fernseher =====================
   Gesetzt von app.js (applyFormat): --stage-r = Breite/Hoehe der Seite,
   --stage-scale < 1, wenn die Seite breiter als der Bildschirm ist. Seite
   und Hintergrund teilen sich die Box; ausserhalb bleibt Schwarz. */
html.has-format body { background: #000; }

html.has-format body > .bg,
html.has-format .layer {
  right: auto;
  bottom: auto;
  top: 50%;
  left: 50%;
  width: calc(100vh * var(--stage-r, 1.7778));
  height: 100vh;
  transform: translate(-50%, -50%) scale(var(--stage-scale, 1)) translateZ(0);
  transform-origin: center center;
  overflow: hidden;
}

/* ===================== Bildschirmwand (Gruppe) =====================
   Gesetzt von app.js (fitSpan, Rechnung in screengroup.js): gezeichnet wird
   die GANZE Leinwand der Gruppe — 100vh hoch, im Seitenverhaeltnis der
   Leinwand breit —, vergroessert und so verschoben, dass genau der
   Ausschnitt dieses Bildschirms das Fenster fuellt. Jeder Bildschirm zeigt
   so ein Stueck derselben Seite; was ueber die Kante laeuft, erscheint
   nahtlos auf dem Nachbarn. */
html.has-span body { background: #000; }

html.has-span body > .bg,
html.has-span .layer {
  right: auto;
  bottom: auto;
  top: 0;
  left: 0;
  width: calc(100vh * var(--stage-r, 1.7778));
  height: 100vh;
  transform: translate(var(--span-tx, 0px), var(--span-ty, 0px)) scale(var(--span-scale, 1)) translateZ(0);
  transform-origin: 0 0;
  overflow: hidden;
}

/* =====================================================================
   Produktkarussell (player/carousel.js)
   =====================================================================
   Eigener Namensraum `pc-`, damit nichts mit dem alten Bilderkarussell
   kollidiert. Alle Groessen haengen an `em` und damit an der Grundschrift
   der Seite: das Karussell waechst mit dem Block, nicht mit dem Fenster. */

.pc-stage {
  position: absolute;
  inset: 0;
  /* Nicht hart am Rahmen abschneiden: Schlagschatten (pc-media) und
     Bodenschatten der Produkte reichen absichtlich darueber hinaus — vor
     allem nach unten. Seitlich bleibt ein schmaler Rand, damit beim
     Wechsel hinausgleitende Produkte nicht weit ausserhalb erscheinen. */
  overflow: visible;
  clip-path: inset(-30% -6% -60% -6%);
  border-radius: inherit;
  perspective: 1600px;
  --pc-duration: 1s;
  --pc-ease: var(--tv-ease, cubic-bezier(0.4, 0, 0.2, 1));
}

.pc-box {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 86%;
  height: 82%;
  transform: translate(-50%, -50%);
  transform-style: flat;
  isolation: isolate;
}

.pc-item {
  position: absolute;
  left: 50%;
  top: 50%;
  /* Grundbreite eines Produkts in der Box. Mehrfach-Helden und
     Hintergrundplaetze bekommen ihre Breite von der Engine; alles andere
     lebt von diesem Wert. */
  width: 38%;
  aspect-ratio: 1;
  translate: -50% -50%;
  display: grid;
  place-items: center;
  transform-style: flat;
  isolation: isolate;
  backface-visibility: hidden;
  will-change: transform, opacity, filter;
  /* EINE Kurve und EINE Dauer fuer alles, was sich bewegt.
     Vorher liefen Lage, Breite, Deckkraft und Schaerfe mit drei
     verschiedenen Kurven und drei Dauern: das Bild war schon an seinem
     Platz, waehrend die Unschaerfe noch nachzog — und am Ende sprang der
     Rest hinterher. Die Kurve startet und endet in Ruhe (Steigung 0), also
     schnellt nichts los und nichts wird abgeschnitten. Dieselben Zahlen
     stehen in player/carousel.js (EASE). */
  transition:
    transform var(--pc-duration) var(--pc-ease),
    width var(--pc-duration) var(--pc-ease),
    opacity var(--pc-duration) var(--pc-ease),
    filter var(--pc-duration) var(--pc-ease);
}

.pc-inner {
  width: 100%;
  height: 100%;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  align-items: end;
  gap: 0.35em;
}

.pc-media {
  min-height: 0;
  display: grid;
  place-items: center;
  padding: 2% 2% 0;
  position: relative;
}

.pc-media img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  pointer-events: none;
  filter: drop-shadow(0 1.2em 1.4em rgba(0, 0, 0, .42));
}

.pc-media.is-empty::after {
  content: '';
  width: 72%;
  height: 72%;
  border-radius: 0.6em;
  border: 1px dashed rgba(255, 255, 255, .18);
}

/* Die Beschriftung sitzt im reservierten Textbereich des Slots — sie
   ueberlappt das Produkt nie, weil die Engine den Platz dafuer freihaelt. */
.pc-copy {
  align-self: end;
  margin-inline: auto;
  width: min(94%, 22em);
  border-radius: 0.7em;
  padding: 0.5em 0.6em 0.55em;
  background: linear-gradient(180deg, rgba(21, 21, 21, .82), rgba(8, 8, 8, .92));
  border: 1px solid rgba(255, 255, 255, .09);
  box-shadow: 0 0.5em 1.4em rgba(0, 0, 0, .28);
  backdrop-filter: blur(10px);
}

.pc-title-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.5em;
}

.pc-name {
  font-size: 0.78em;
  font-weight: 700;
  line-height: 1.08;
  color: #f7f7f7;
}

.pc-price {
  flex: 0 0 auto;
  white-space: nowrap;
  font-size: 0.66em;
  font-weight: 700;
  color: #f5f5f5;
  background: rgba(255, 255, 255, .08);
  border: 1px solid rgba(255, 255, 255, .07);
  border-radius: 999px;
  padding: 0.16em 0.45em;
}

.pc-sub {
  margin-top: 0.3em;
  color: #b6b6b6;
  font-size: 0.6em;
  line-height: 1.2;
}

.pc-item.is-compact .pc-copy { padding: 0.42em 0.5em; width: min(96%, 17em); }
.pc-item.is-compact .pc-name { font-size: 0.66em; }
.pc-item.is-compact .pc-price { font-size: 0.56em; }
.pc-item.is-micro .pc-name { font-size: 0.58em; }
.pc-item.is-micro .pc-sub { display: none; }
/* Kein Uebergang ausserhalb des Wechsels.
   Der erste Aufbau und jedes Nachmessen setzen die Lage, sie bewegen nichts.
   Ohne das flogen die Produkte nach jedem Nachmessen aus der Mitte an ihren
   Platz — von aussen sah es aus, als spraenge etwas. */
.pc-stage.pc-no-anim .pc-item,
.pc-stage.pc-no-anim .pc-item * { transition: none !important; }

/* Tiefenordnung: wer neu nach vorn kommt, ist waehrend des Wechsels voll
   deckend — sonst schimmert der alte Held durch das neue Produkt. */
.pc-item.pc-incoming { opacity: 1 !important; }
/* Geht ein Produkt in der neuen Reihenfolge nach hinten, blendet es waehrend
   der Bewegung ab, wechselt in der Mitte die Ebene und blendet wieder auf
   (carousel.js sink). */
.pc-item.pc-sinking .pc-media {
  animation: pc-sink var(--pc-sink-ms, 1000ms) cubic-bezier(0.4, 0, 0.2, 1) both;
}
@keyframes pc-sink {
  0%   { opacity: 1; }
  45%  { opacity: 0.28; }
  55%  { opacity: 0.28; }
  100% { opacity: 1; }
}

/* Der Zwilling in der hinteren Ebene: nur Buehne, kein Rahmen, kein Titel —
   und niemals anfassbar, sonst faengt er die Tipper des Editors ab. */
.free-el.free-carousel-back { pointer-events: none; }
.blk-product-carousel.is-back { background: none; box-shadow: none; }

/* Nur in der Vorschau der App: wo bei mehreren Helden der Text sitzt. */
.pc-safe-layer {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 999;
}

.pc-safe-zone {
  position: absolute;
  border: 1px dashed rgba(200, 250, 54, .55);
  border-radius: 0.4em;
  background: rgba(200, 250, 54, .06);
}

/* Beschriftung: erscheint mit einem kurzen Anlauf statt aus dem Nichts.
   Sie wird ein- und ausgeblendet, indem die Zeile ganz verschwindet (das
   Bild bekommt dann den Platz) — ohne diese kleine Bewegung poppt der Name
   im selben Augenblick ins Bild, in dem sich das Produkt noch bewegt. */
.pc-item:not(.hide-copy) .pc-copy {
  animation: pcCopyIn 0.38s var(--tv-ease) both;
}

@keyframes pcCopyIn {
  from { opacity: 0; transform: translateY(0.35em); }
  to { opacity: 1; transform: none; }
}

.pc-stage.pc-reduced .pc-copy { animation: none !important; }

.pc-item.hide-copy .pc-copy { display: none !important; }
.pc-item.hide-name .pc-name { display: none !important; }
.pc-item.hide-price .pc-price { display: none !important; }
.pc-item.hide-sub .pc-sub { display: none !important; }

/* Ruhebewegung: immer nur auf dem BILD, nie auf der Beschriftung — sonst
   wackelt der Preis, und das liest sich auf drei Metern unruhig. */
.pc-motion-float .pc-media img { animation: pcFloat 5.8s ease-in-out infinite alternate; }
.pc-motion-zerog .pc-media img { animation: pcZeroG 6.5s ease-in-out infinite alternate; }
.pc-motion-breathe .pc-media img { animation: pcBreathe 4.8s ease-in-out infinite alternate; }
.pc-motion-sway .pc-media img { animation: pcSway 5.7s ease-in-out infinite alternate; }
.pc-motion-drift .pc-media img { animation: pcDrift 5.4s ease-in-out infinite alternate; }
.pc-motion-lift .pc-media img { animation: pcLift 4.7s ease-in-out infinite alternate; }
.pc-motion-tilt .pc-media img { animation: pcTilt 5.2s ease-in-out infinite alternate; transform-origin: 50% 72%; }
.pc-motion-pendulum .pc-media img { animation: pcPendulum 4.9s ease-in-out infinite alternate; transform-origin: 50% 10%; }
.pc-motion-pulse .pc-media img { animation: pcPulse 4.2s ease-in-out infinite alternate; }

/* --- 2.5D: raeumlich schweben ------------------------------------------
   Ein Bild ohne Ebenen bekommt beim Spatial-Preset das normale Schweben —
   der Rueckfall ist derselbe Weg wie 'float', nur unter anderem Namen.
   Mit Ebenen liegt statt des Bildes ein .spatial-media da: vier
   deckungsgleiche, durchsichtige Bilder, jedes auf seiner Z-Lage (--z, in
   Pixeln, vom Server). Der Container kippt ganz leicht um beide Achsen;
   weil die Ebenen verschieden weit vorn stehen, wandern sie verschieden weit
   ueber den Bildschirm — das ist die Parallaxe. Kein filter auf dem
   Container: filter erzeugt einen eigenen Stapel und wuerde preserve-3d
   ausschalten; der Schatten sitzt deshalb nur auf der hintersten Ebene. */
.pc-motion-spatialfloat .pc-media > img { animation: pcFloat 5.8s ease-in-out infinite alternate; }

/* --- Der Aufbau eines 2.5D-Produkts ------------------------------------
   Der Medienrahmen ist sonst ein Gitter, das seine Hoehe vom Bild bekommt.
   Ein Ebenenstapel hat keine eigene Hoehe (seine Bilder liegen absolut) — in
   einem Gitter mit automatischer Zeile waeren 100 % gleich 0 und das Produkt
   unsichtbar. Deshalb Block mit voller Hoehe.

   UND: kein Innenabstand. Die Ebenen rechnen die perspektivische
   Vergroesserung um ihre eigene Mitte zurueck; die Perspektive selbst
   vergroessert um perspective-origin, also um die Mitte des RAHMENS. Sitzt
   der Stapel wegen eines Innenabstands nicht genau mittig im Rahmen, liegen
   die Ebenen schon in Ruhe gegeneinander versetzt — genau die „doppelte
   Ladebuchse". */
.pc-media.has-spatial {
  --persp: 1400;
  display: block;
  height: 100%;
  align-self: stretch;
  padding: 0;
  perspective: calc(var(--persp) * 1px);
  perspective-origin: 50% 50%;
}

.spatial-media {
  position: relative;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
}

/* Jede Ebene auf ihrer Z-Lage.
   `--z-gain` verstaerkt den Abstand aus der Datei (±18 px), weil ±18 px bei
   3° unter einem Pixel Versatz ergeben — unsichtbar. Nach oben ist er aber
   eng begrenzt: benachbarte Ebenen sind aus DEMSELBEN flachen Foto
   geschnitten, und wo die vordere Ebene beim Kippen zur Seite wandert, kommt
   die hintere mit ihrer eigenen Kopie derselben Stelle zum Vorschein. Diese
   Geisterkante ist so breit wie der Versatz zwischen zwei Ebenen. Bei
   ±3,5° und drei Zwischenraeumen bleibt sie mit `gain: 2.6` unter zwei
   Pixeln und liest sich als weiche Kante statt als Doppelbild. Ein
   Ebenenstapel kann echte Verdeckung nicht aufloesen — dafuer braeuchte es
   ein Modell, das die verdeckten Stellen neu erfindet.

   Die Vergroesserung durch die Perspektive (p / (p − z)) wird je Ebene wieder
   herausgerechnet, damit in Ruhe alle Ebenen deckungsgleich liegen und nur
   die Drehung sie gegeneinander verschiebt. */
.pc-media img.spatial-layer {
  --zz: calc(var(--z, 0) * var(--z-gain, 2.6));
  /* Ueberabtastung. Ein Stapel mit preserve-3d und laufender Animation wird
     vom Browser EINMAL in Layoutgroesse in eine Textur gezeichnet; danach
     schiebt die Grafikkarte nur noch diese Textur. Das Karussell vergroessert
     das fuehrende Produkt aber um 1,13, und in der Vorschau kommt die Skalierung
     der Buehne dazu — die Textur wird also gedehnt, und das Produkt sieht
     matschig aus, waehrend ein gewoehnliches <img> bei jedem Zeichnen neu
     aufgeloest wird. Deshalb liegt jede Ebene etwas zu gross im Layout und
     wird per Transformation wieder verkleinert: gleiche Groesse am Schirm,
     aber feinere Textur. Der Wert deckt genau die Vergroesserung des
     fuehrenden Produkts (1,13) mit etwas Luft ab — mehr waere nur
     Bildspeicher, denn er geht quadratisch ein. */
  --ss: 1.25;
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(100% * var(--ss));
  height: calc(100% * var(--ss));
  object-fit: contain;
  pointer-events: none;
  filter: none;
  transform: translate(-50%, -50%)
             translateZ(calc(var(--zz) * 1px))
             scale(calc((var(--persp) - var(--zz)) / var(--persp) / var(--ss)));
  backface-visibility: hidden;
}

.pc-motion-spatialfloat .spatial-media {
  animation: pcSpatialFloat 6.2s ease-in-out infinite alternate;
  animation-delay: calc(var(--phase, 0) * -0.9s);
}

@keyframes pcSpatialFloat {
  from { transform: translateY(-1.4%) rotateY(-3.5deg) rotateX(1.2deg); }
  to   { transform: translateY(1.6%) rotateY(3.5deg) rotateX(-1.2deg); }
}

.pc-stage.pc-reduced .spatial-media { animation: none !important; }


/* Jedes Produkt startet die Ruhebewegung etwas versetzt — sonst atmen alle
   im Gleichschritt, und das wirkt mechanisch. */
.pc-item .pc-media img { animation-delay: calc(var(--phase, 0) * -0.7s); }

.pc-motion-lightsweep::after {
  content: '';
  position: absolute;
  inset: -10% -20%;
  pointer-events: none;
  background: linear-gradient(105deg, transparent 28%, rgba(255, 255, 255, 0) 42%,
    rgba(255, 255, 255, .16) 50%, rgba(255, 255, 255, 0) 58%, transparent 72%);
  transform: translateX(-130%) skewX(-14deg);
  mix-blend-mode: screen;
  animation: pcLightSweep 4.6s ease-in-out infinite;
}

@keyframes pcFloat { from { transform: translateY(-2%) rotateZ(-.7deg); } to { transform: translateY(2.5%) rotateZ(.7deg); } }
@keyframes pcZeroG { 0% { transform: translate(-1%, -2%) rotate(-1.5deg); } 50% { transform: translate(1.2%, 2.6%) rotate(1deg); } 100% { transform: translate(-.5%, -1%) rotate(.2deg); } }
@keyframes pcBreathe { from { transform: scale(.985); } to { transform: scale(1.015); } }
@keyframes pcSway { from { transform: translateX(-1.5%) rotateZ(-1.1deg); } to { transform: translateX(1.5%) rotateZ(1.1deg); } }
@keyframes pcDrift { from { transform: translateX(-2.4%); } to { transform: translateX(2.4%); } }
@keyframes pcLift { from { transform: translateY(1.7%) scale(.99); } to { transform: translateY(-2.2%) scale(1.012); } }
@keyframes pcTilt { from { transform: rotateZ(-1.8deg) translateY(.5%); } to { transform: rotateZ(1.8deg) translateY(-.5%); } }
@keyframes pcPendulum { from { transform: rotateZ(-2.2deg); } to { transform: rotateZ(2.2deg); } }
@keyframes pcPulse { from { transform: scale(.975); } to { transform: scale(1.025); } }
@keyframes pcLightSweep {
  0% { transform: translateX(-130%) skewX(-14deg); opacity: 0; }
  17% { opacity: .75; }
  42% { transform: translateX(20%) skewX(-14deg); opacity: .35; }
  62% { opacity: 0; }
  100% { transform: translateX(130%) skewX(-14deg); opacity: 0; }
}

/* Wer Bewegung reduziert haben will, bekommt die Komposition ohne Zappeln:
   die Produkte stehen, der Wechsel bleibt. */
.pc-stage.pc-reduced .pc-media img,
.pc-stage.pc-reduced::after { animation: none !important; }
@media (prefers-reduced-motion: reduce) {
  .pc-media img, .pc-motion-lightsweep::after, .pc-copy { animation: none !important; }
}


/* =====================================================================
   Auftritt eines Blocks — die neuen Familien
   =====================================================================
   `pop` laesst eine Schlagzeile hervorspringen, `letters` setzt sie
   Buchstabe fuer Buchstabe, `blur` zieht sie scharf, `sweep` wischt sie
   frei, `tilt` kippt sie herein. Alles einmalig beim Aufbau der Seite —
   danach steht der Text ruhig, damit man ihn lesen kann. */

.anim-pop    { animation: anim-pop 0.62s var(--tv-ease) both; }
.anim-blur   { animation: anim-blur 0.7s var(--tv-ease) both; }
.anim-sweep  { animation: anim-sweep 0.75s var(--tv-ease) both; }
.anim-tilt   { animation: anim-tilt 0.6s var(--tv-ease) both; transform-origin: 50% 90%; }

@keyframes anim-pop {
  from { opacity: 0; transform: scale(.72); }
  to   { opacity: 1; transform: scale(1); }
}
@keyframes anim-blur {
  from { opacity: 0; filter: blur(14px); transform: scale(1.04); }
  to   { opacity: 1; filter: blur(0); transform: scale(1); }
}
@keyframes anim-sweep {
  from { opacity: 1; clip-path: inset(0 100% 0 0); }
  to   { opacity: 1; clip-path: inset(0 0 0 0); }
}
@keyframes anim-tilt {
  from { opacity: 0; transform: rotateX(38deg) translateY(14%); }
  to   { opacity: 1; transform: rotateX(0) translateY(0); }
}

/* Buchstabenweise: jeder Buchstabe ist ein eigenes Element mit eigener
   Verzoegerung (--i wird im Player gesetzt). Leerzeichen bleiben stehen,
   damit die Zeile beim Setzen nicht springt. */
.anim-letters .text-char {
  display: inline-block;
  animation: anim-pop 0.5s var(--tv-ease) both;
  animation-delay: calc(var(--i, 0) * 0.045s + var(--anim-delay, 0s));
}
.anim-letters { animation: none !important; }
/* Ein Wort bricht nicht zwischen seinen Buchstaben um (render.js splitIntoChars). */
.anim-letters .text-word { display: inline-block; white-space: nowrap; }

@media (prefers-reduced-motion: reduce) {
  .anim-pop, .anim-blur, .anim-sweep, .anim-tilt { animation: none !important; }
  .anim-letters .text-char { animation: none !important; }
}

/* =====================================================================
   Auftritt — die Kino-Familie („After-Effects-Stil“)
   =====================================================================
   Gebaut wie Motion-Design: kräftiger Anfang, langes weiches Auslaufen
   (Expo-Out), wo es passt ein kleines Federn. Animiert wird nur, was die
   Grafik direkt rechnet — transform, opacity, filter, clip-path —, damit es
   auf dem Fernseher und in der Vorschau mit voller Bildrate läuft. Wie oben:
   die Ausgangswerte stehen nur im Keyframe. */

:root {
  --ae-out: cubic-bezier(0.16, 1, 0.3, 1);        /* Expo-Out */
  --ae-back: cubic-bezier(0.34, 1.56, 0.64, 1);   /* federt leicht über */
}

.anim-rise     { animation: anim-rise 0.9s var(--ae-out) both; }
.anim-drop     { animation: anim-drop 1.0s linear both; }
.anim-swing    { animation: anim-swing 1.1s linear both; transform-origin: 50% 0; }
.anim-flip     { animation: anim-flip 0.85s var(--ae-out) both; }
.anim-wipe     { animation: anim-wipe 0.9s var(--ae-out) both; }
.anim-iris     { animation: anim-iris 0.95s var(--ae-out) both; }
.anim-stretch  { animation: anim-stretch 0.95s linear both; }
.anim-glitch   { animation: anim-glitch 0.7s steps(1, end) both; }
.anim-push     { animation: anim-push 1.0s var(--ae-out) both; }
.anim-spin     { animation: anim-spin 0.9s var(--ae-back) both; }

/* Steigt auf und wird scharf — wie mit Bewegungsunschärfe. */
@keyframes anim-rise {
  from { opacity: 0; transform: translate3d(0, 38%, 0); filter: blur(10px); }
  60%  { filter: blur(0); }
  to   { opacity: 1; transform: none; filter: blur(0); }
}
/* Fällt herein und federt zweimal nach. */
@keyframes anim-drop {
  0%   { opacity: 0; transform: translate3d(0, -70%, 0); animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  40%  { opacity: 1; transform: translate3d(0, 0, 0); animation-timing-function: cubic-bezier(0, 0.55, 0.45, 1); }
  58%  { transform: translate3d(0, -12%, 0); animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  74%  { transform: translate3d(0, 0, 0); animation-timing-function: cubic-bezier(0, 0.55, 0.45, 1); }
  86%  { transform: translate3d(0, -3%, 0); animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
  100% { opacity: 1; transform: none; }
}
/* Hängt oben und schwingt ein — wie ein Schild an zwei Ketten. */
@keyframes anim-swing {
  0%   { opacity: 0; transform: perspective(800px) rotateX(-80deg); }
  35%  { opacity: 1; transform: perspective(800px) rotateX(22deg); }
  60%  { transform: perspective(800px) rotateX(-9deg); }
  80%  { transform: perspective(800px) rotateX(3deg); }
  100% { opacity: 1; transform: none; }
}
/* Klappt um die Hochachse herein. */
@keyframes anim-flip {
  from { opacity: 0; transform: perspective(900px) rotateY(-95deg) scale(0.92); }
  to   { opacity: 1; transform: none; }
}
/* Wird von unten aufgedeckt und gleitet dabei hoch. */
@keyframes anim-wipe {
  from { clip-path: inset(100% 0 0 0); transform: translate3d(0, 18%, 0); }
  to   { clip-path: inset(0 0 0 0); transform: none; }
}
/* Öffnet sich aus der Mitte wie eine Blende. */
@keyframes anim-iris {
  from { opacity: 0.4; clip-path: circle(0% at 50% 50%); transform: scale(1.08); }
  to   { opacity: 1; clip-path: circle(75% at 50% 50%); transform: none; }
}
/* Gummiband: schnalzt in Form. */
@keyframes anim-stretch {
  0%   { opacity: 0; transform: scale3d(0.3, 1.5, 1); }
  30%  { opacity: 1; transform: scale3d(1.25, 0.8, 1); }
  45%  { transform: scale3d(0.85, 1.12, 1); }
  60%  { transform: scale3d(1.06, 0.95, 1); }
  78%  { transform: scale3d(0.98, 1.02, 1); }
  100% { opacity: 1; transform: none; }
}
/* Ein kurzes digitales Flackern, dann steht es. */
@keyframes anim-glitch {
  0%   { opacity: 0; }
  8%   { opacity: 1; transform: translate3d(-6%, 0, 0) skewX(-12deg); clip-path: inset(10% 0 55% 0); }
  16%  { transform: translate3d(5%, 0, 0); clip-path: inset(60% 0 8% 0); }
  24%  { opacity: 0.3; transform: translate3d(-3%, 0, 0) skewX(8deg); clip-path: inset(30% 0 30% 0); }
  32%  { opacity: 1; transform: translate3d(2%, 0, 0); clip-path: inset(0 0 70% 0); }
  44%  { transform: translate3d(-1%, 0, 0); clip-path: inset(45% 0 0 0); }
  56%  { transform: none; clip-path: inset(0 0 0 0); }
  70%  { opacity: 0.85; }
  100% { opacity: 1; transform: none; clip-path: none; }
}
/* Rauscht auf die Kamera zu und kommt zur Ruhe. */
@keyframes anim-push {
  from { opacity: 0; transform: scale(1.7); filter: blur(16px); }
  55%  { filter: blur(0); }
  to   { opacity: 1; transform: none; filter: blur(0); }
}
/* Dreht sich aus der Tiefe herein. */
@keyframes anim-spin {
  from { opacity: 0; transform: rotate(-200deg) scale(0.2); }
  to   { opacity: 1; transform: none; }
}

/* Text: Buchstaben steigen weich und scharf auf — und Schreibmaschine. */
.anim-lettersrise .text-char {
  display: inline-block;
  animation: anim-rise 0.8s var(--ae-out) both;
  animation-delay: calc(var(--i, 0) * 0.035s + var(--anim-delay, 0s));
}
.anim-typewriter .text-char {
  display: inline-block;
  animation: anim-type 0.01s steps(1, end) both;
  animation-delay: calc(var(--i, 0) * 0.06s + var(--anim-delay, 0s));
}
@keyframes anim-type { from { opacity: 0; } to { opacity: 1; } }
.anim-lettersrise, .anim-typewriter { animation: none !important; }
.anim-lettersrise .text-word, .anim-typewriter .text-word { display: inline-block; white-space: nowrap; }

@media (prefers-reduced-motion: reduce) {
  .anim-rise, .anim-drop, .anim-swing, .anim-flip, .anim-wipe, .anim-iris,
  .anim-stretch, .anim-glitch, .anim-push, .anim-spin,
  .anim-lettersrise .text-char, .anim-typewriter .text-char { animation: none !important; }
}

/* ===================== Produkteffekte ===================== */

/* Schatten, Spiegelung und Licht eines Bildes (player/effects.js, Aufbau dort).

   Zwei Huellen liegen deckungsgleich auf dem GEZEICHNETEN Bild (bei contain
   kleiner als der Kasten): hinten Schatten und Spiegelung, vorn das Licht. Die
   Ebenen darin stehen in Prozent dieser Huelle und reichen absichtlich ueber
   sie hinaus — genau das tut ein Schatten. Deshalb hat der Kasten kein
   overflow: hidden. Keine Huelle faengt Zeiger ab: im Editor greift man das
   Produkt, nicht seinen Schatten. */
.blk-image.has-fx { position: relative; overflow: visible; }
.blk-image.has-fx > .media-fill { position: relative; z-index: 1; }

.fx-wrap {
  position: absolute;
  left: 0; top: 0; width: 100%; height: 100%;
  pointer-events: none;
  z-index: 0;
}
.fx-wrap.fx-front { z-index: 2; }

.fx-layer {
  position: absolute;
  display: block;
  pointer-events: none;
  /* Die Ebenen sind kleiner gerechnet als gezeigt — weich vergroessern. */
  image-rendering: auto;
}

/* Die eigene Ebene des Schattens — deckungsgleich hinter dem Text.

   Warum ueberhaupt eine eigene: Liegen Strich und Schatten auf demselben
   Text, zeichnet CSS den Schatten aus der Form MIT Strich. Die Kontur sieht
   dann aus, als umliefe sie den Schatten und den Schein. Siehe render.js,
   paintTreatments.

   Hier lag einmal die KONTUR statt des Schattens — und war unsichtbar: Eine
   Ebene mit `z-index: -1` liegt hinter dem Text, und dort sieht man einen
   Strich nicht mehr. Der Schatten dagegen gehoert genau dorthin. */
.txt-shadow-layer {
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  pointer-events: none;
  /* KEIN z-index: Die Kopie steht als erstes Kind im Fluss und wird deshalb
     vor dem Text gezeichnet — also dahinter. Ein negativer z-index wuerde sie
     hinter den Hintergrund schieben. */
}

/* Der sichtbare Text ueber seiner Schattenebene.

   `display: block`, weil hier Zeilen-DIVs hineinwandern; ein Inline-Span
   wuerde ihr Layout zerlegen. `z-index: 1`, damit er ueber der absolut
   gesetzten Schattenkopie liegt — siehe render.js, paintTreatments. */
.txt-front {
  display: block;
  position: relative;
  z-index: 1;
}

/* --- Produkteinheit (Menue-Stil "unit"): Name, Varianten, grosses Bild,
   Beschreibung in EINEM Block; die Flaeche ist der Rahmen (plate). --- */
.menu-style-unit {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  text-align: center;
  gap: 0.3em;
  height: 100%;
}
.free-fixed > .menu-style-unit { height: 100%; }
.menu-style-unit .unit-title { font-weight: 800; line-height: 1.05; }
.menu-style-unit .unit-rows { display: flex; flex-direction: column; gap: 0.1em; }
.menu-style-unit .unit-row { display: flex; justify-content: center; align-items: baseline; gap: 0.45em; font-weight: 700; }
.menu-style-unit .unit-price { white-space: nowrap; }
.menu-style-unit .unit-stage { flex: 1 1 auto; min-height: 0; display: flex; align-items: center; justify-content: center; }
.menu-style-unit .unit-stage img { max-width: 100%; max-height: 100%; width: 100%; height: 100%; object-fit: contain; }
.menu-style-unit .unit-desc { font-size: 0.72em; opacity: 0.85; line-height: 1.2; }
/* Ausbruch (render.js renderUnit): die Buehne ragt ueber die Flaeche, der Text bleibt davor. */
.menu-style-unit .unit-out-top { order: -1; }
.menu-style-unit > .unit-title, .menu-style-unit > .unit-rows, .menu-style-unit > .unit-desc { position: relative; z-index: 1; }

/* =====================================================================
   Animation — dauerhafte Bewegung eines sichtbaren Elements
   =====================================================================
   Unabhaengig von der Einblendung (die laeuft innen auf dem Block). Ruhig,
   fuer Produktwerbung: Sinus-Kurven, lange Perioden, kleine Ausschlaege.
   Werte kommen als Variablen aus render.js (applyLoop). */

.loop-float, .loop-drift {
  animation: loop-move var(--loop-dur, 6s) cubic-bezier(0.45, 0, 0.55, 1) var(--loop-delay, 0s) infinite alternate;
}
.loop-tilt {
  animation: loop-tilt var(--loop-dur, 7s) cubic-bezier(0.45, 0, 0.55, 1) var(--loop-delay, 0s) infinite alternate;
}
.loop-breathe {
  animation: loop-scale var(--loop-dur, 5.5s) cubic-bezier(0.45, 0, 0.55, 1) var(--loop-delay, 0s) infinite alternate;
}
.loop-pulse {
  animation: loop-pulse var(--loop-dur, 2.8s) cubic-bezier(0.22, 1, 0.36, 1) var(--loop-delay, 0s) infinite;
}
.loop-rotate {
  animation: loop-spin var(--loop-dur, 60s) linear var(--loop-delay, 0s) infinite;
}

@keyframes loop-move {
  from { translate: 0 0; }
  to   { translate: var(--loop-x, 0) var(--loop-y, -1.2vh); }
}
@keyframes loop-tilt {
  from { rotate: calc(var(--loop-r, 1.8deg) * -1); }
  to   { rotate: var(--loop-r, 1.8deg); }
}
@keyframes loop-scale {
  from { scale: 1; }
  to   { scale: var(--loop-s, 1.025); }
}
@keyframes loop-pulse {
  0%   { scale: 1; }
  16%  { scale: var(--loop-s, 1.035); }
  42%  { scale: 1; }
  100% { scale: 1; }
}
@keyframes loop-spin {
  from { rotate: 0deg; }
  to   { rotate: var(--loop-turn, 360deg); }
}

@media (prefers-reduced-motion: reduce) {
  .loop-float, .loop-drift, .loop-tilt, .loop-breathe, .loop-pulse, .loop-rotate { animation: none !important; }
}
