/* ============================================================
   Komponente: motion
   ------------------------------------------------------------
   Übergangszeiten und Kurven für alle Komponenten. Eine Datei,
   damit Bewegung nicht in jeder Komponente neu erfunden wird.

   STATUS: FREIGEGEBEN am 28.08.2026 — alle Dauern und Kurven
   dieser Datei, einschließlich Bottom Nav (700 ms) und Spinner.
   Bis dahin galt „Motion ist nicht spezifiziert, Prototypen bauen
   statisch"; das ist überholt (SKILL.md §23).

   Diese Datei ist für Motion die FÜHRENDE QUELLE, weil die Werte
   in Figma nicht als Variablen existieren — wie Breakpoints (§0)
   und das Zustands-Vokabular (§16). Wo eine Figma-Komponente eine
   Dauer oder Kurve ausdrücklich trägt, schlägt sie diese Datei;
   dann ist der Wert hier nachzuziehen, nicht daneben zu halten.

   Setzt tokens.css und base.css voraus. Ändert kein Aussehen,
   nur den Weg zwischen zwei Zuständen.

     <link rel="stylesheet" href="components/motion.css">

   ZEITLUPE FÜRS REVIEW
   --motion-scale auf 3 oder 6 setzen. Bei 1x siehst du das
   Ergebnis, bei 6x die Entscheidung.

     document.documentElement.style.setProperty('--motion-scale', 6)

   GRUNDREGELN
   1. Nur transform und opacity animieren. Sie überspringen
      Layout und Paint. Ausnahme: height im Akkordeon, dafür gibt
      es keinen Ersatz — deshalb dort kurz halten.
   2. Nie ease-in. Es startet langsam, genau im Moment, in dem
      hingeschaut wird.
   3. Was hundertfach am Tag passiert, animiert nicht.
   4. Glow wird nie animiert, wo er nur Deko ist (Chip). Er ist
      ein Schatten und wird auf der CPU gemalt.
   ============================================================ */

:root {
  /* Kurven. Die eingebauten CSS-Kurven sind zu schwach. */
  --motion-ease-out:    cubic-bezier(0.23, 1, 0.32, 1);    /* rein und raus */
  --motion-ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);   /* Bewegung auf dem Screen */
  --motion-ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);    /* Sheet, iOS-nah */
  --motion-ease-nav:    cubic-bezier(0.16, 1, 0.3, 1);     /* Bottom Nav, weites Ausrollen */
  --motion-ease-bubble: cubic-bezier(0.34, 1.2, 0.64, 1);  /* leichtes Überschwingen, optional */

  /* Dauern */
  --motion-press:    160ms;   /* Druckfeedback */
  --motion-popover:  200ms;   /* Menü, Listen-Control */
  --motion-akkordeon: 200ms;
  --motion-dialog:   250ms;
  --motion-nav:      700ms;   /* Bubble-Wanderung + Icon-Aufstieg (festgelegt 28.08.2026) */
  --motion-toast:    400ms;   /* bewusst über 300 — siehe unten */
  --motion-sheet:    500ms;
  --motion-spinner:  500ms;   /* Basiswert, x2 Drehung / x3 Bogen */

  --motion-stagger:  50ms;    /* Versatz je Listenelement */

  /* Review-Zeitlupe. Im Produktivcode immer 1. */
  --motion-scale: 1;
}

/* ------------------------------------------------------------
   Buttons
   ------------------------------------------------------------
   scale(0.97) beim Drücken. Der Zielgruppe fehlt die Gewissheit,
   dass ein Tippen angekommen ist — das Nachgeben liefert sie,
   bevor die Anwendung reagiert hat.

   NIE bei disabled: ein Button, der nicht reagieren soll, darf
   auch nicht nachgeben. button.css schließt disabled bereits über
   :not(:disabled) von Hover und Press aus, hier ebenso.

   Der Glow läuft hier mit, weil Hover nur auf Zeigergeräten
   auftritt — dort ist die Rechenlast unkritisch. Auf Touch feuert
   nur :active. Ruckelt es auf alten Geräten trotzdem: box-shadow
   aus der transition-Liste nehmen, der Glow springt dann sofort.
   Das Aussehen ändert sich dadurch nicht, nur der Weg dorthin.
   ------------------------------------------------------------ */

.btn:not(:disabled) {
  transition:
    transform        calc(var(--motion-press) * var(--motion-scale)) var(--motion-ease-out),
    background-color calc(var(--motion-press) * var(--motion-scale)) var(--motion-ease-out),
    border-color     calc(var(--motion-press) * var(--motion-scale)) var(--motion-ease-out),
    box-shadow       calc(var(--motion-press) * var(--motion-scale)) var(--motion-ease-out);
}

.btn:active:not(:disabled) {
  transform: scale(0.97);
}

/* Erzwungener Zustand fürs Review — analog .is-hover in button.css */
.btn.is-pressed:not(:disabled) {
  transform: scale(0.97);
}

/* ------------------------------------------------------------
   Chip
   ------------------------------------------------------------
   Der Glow Blur 4 ist reine Verstärkung — den Aktiv-Zustand trägt
   der 2-px-Border (§5). Deshalb steht box-shadow NICHT in der
   transition: er springt sofort und kostet keine Bildrate. Fällt
   er auf schwachen Geräten ganz aus, bleibt der Zustand lesbar.

   KEIN SPRUNG BEIM UMSCHALTEN: Der Border bleibt in jedem Zustand
   1 px breit. Das zweite Pixel des Aktiv-Borders kommt als
   inset box-shadow dazu. Ein Wechsel von border-width 1 auf 2
   verschiebt bei box-sizing: border-box den Inhalt um 1 px — in
   Figma passiert das nicht, weil der Strich dort INSIDE liegt und
   den Rahmen nicht anfasst. Der inset-Schatten ist die CSS-
   Entsprechung dazu. Dieselbe Lösung nutzt button.css bereits für
   den 1-px-Inner-Stroke bei Hover.

   Das gilt auch für die Suchleiste (§6: 1 px → 2 px bei Fokus)
   und ist der sauberere Ersatz für den Padding-Ausgleich, den
   input.css heute fährt.
   ------------------------------------------------------------ */

/* box-shadow statt border-color: der Chip hat seit dem 31.08.2026
   keinen border mehr — der Strich liegt mittig und ist ein
   box-shadow (§5). Eine Transition auf border-color liefe ins
   Leere. box-shadow ist animierbar und nimmt Strichstärke, Farbe
   und Glow in einem Rutsch mit. */
.chip {
  transition:
    transform        calc(var(--motion-press) * var(--motion-scale)) var(--motion-ease-out),
    background-color calc(var(--motion-press) * var(--motion-scale)) var(--motion-ease-out),
    box-shadow       calc(var(--motion-press) * var(--motion-scale)) var(--motion-ease-out);
}

.chip:active {
  transform: scale(0.97);
}

/* ------------------------------------------------------------
   Bottom Nav
   ------------------------------------------------------------
   Zwei Bewegungen:

   1. Die Bubble WANDERT zum neuen Tab. 700 ms mit --motion-ease-nav,
      einer Kurve mit sehr langem Auslauf: der Kreis setzt sich im
      Moment des Tippens in Bewegung und rollt am Ziel weit aus.

      Die 700 ms sind die einzige Stelle im System, an der die Regel
      „UI bleibt unter 300 ms" bewusst gebrochen wird. Sie trägt hier
      nicht, weil die Bubble keine Antwort ist: Label und Icon-Farbe
      sagen sofort, wo man ist. Der Kreis kommt nach, und weil fast
      die ganze Zeit im Auslauf steckt, liest sich das als Ruhe und
      nicht als Warten. Festgelegt am 28.08.2026.

   2. Das ICON DES ZIELS steigt auf und wechselt oben die Farbe:
      weiß → nav-icon-active. Der Farbwechsel hat eine Verzögerung,
      damit er im Moment des Ankommens passiert, nicht davor. Beim
      Verlassen läuft es ohne Verzögerung zurück — das Icon wird
      sofort wieder weiß und sinkt ab.

   DER KREIS BLEIBT IMMER RUND. Keine Squash-and-Stretch-Verformung
   beim Wandern: ein Kreis, der sich zieht, liest sich als Material,
   und Material erwartet man anfassen zu können. Die Bubble ist eine
   Ortsmarke, kein Objekt.

   Das LABEL wechselt ohne Übergang. Gewicht und Farbe sind die
   Auskunft „du bist jetzt hier"; sie darf nie auf eine Animation
   warten. Nachlaufen darf die Form, nicht die Information.

   --nav-rise wird in JS aus der tatsächlichen Geometrie gesetzt,
   damit das Icon exakt im Bubble-Mittelpunkt landet.
   ------------------------------------------------------------ */

/* DER ÜBERGANG LIEGT AN .nav, NICHT AN DER BUBBLE (09.09.2026).
   Bewegt wird --nav-bubble-x, und aus diesem einen Wert lesen
   ZWEI Dinge: der Kreis sein transform und die Kerbe in der
   Leiste ihre Maskenposition (nav.css, „Ein Wert, zwei Leser").

   Vorher lief der Übergang am transform des Kreises, und die
   Kerbe sprang — eine nicht angemeldete Custom Property kennt
   keinen Übergang. Kreis und Kerbe liefen sichtbar auseinander,
   obwohl der Kreis in der Kerbe sitzen muss.

   Dauer und Kurve sind unverändert: 700 ms, --motion-ease-nav. */
.nav {
  transition: --nav-bubble-x calc(var(--motion-nav) * var(--motion-scale)) var(--motion-ease-nav);
}

/* KEIN will-change MEHR AM KREIS. Es stand hier, damit der
   Browser das transform auf einer eigenen Ebene animieren kann.
   Genau das soll er jetzt nicht: das transform hängt an einem
   Wert, den auch die Maske der Leiste liest, und die Maske wird
   im Hauptdurchlauf gemalt. Eine eigene Ebene könnte einen
   Frame vorauslaufen — und ein Frame Versatz zwischen Kreis und
   Kerbe ist genau der Fehler, der hier behoben wurde. */

/* Icon: steigt auf, wird oben dunkel. Gleiche Kurve wie die
   Bubble, damit beide zusammen ankommen. */
.nav__icon {
  transition:
    transform calc(var(--motion-nav) * var(--motion-scale)) var(--motion-ease-nav),
    color     calc(var(--motion-press) * var(--motion-scale)) linear;
}

.nav__item[aria-current="page"] .nav__icon {
  transform: translateY(calc(var(--nav-rise, 32px) * -1));
  color: var(--nav-icon-active);
  /* Farbe erst kurz vor dem Ankommen */
  transition-delay: 0ms, calc(var(--motion-nav) * var(--motion-scale) * 0.55);
}

/* Press-Feedback auf dem Tab selbst (nav-bg-fill-press, §7) */
.nav__item {
  transition: background-color calc(var(--motion-press) * var(--motion-scale)) var(--motion-ease-out);
}

/* ------------------------------------------------------------
   Scrim
   ------------------------------------------------------------
   Läuft mit Dialog und Sheet zusammen, sonst lesen sie als zwei
   Ereignisse. Nimmt die Dauer der Fläche, zu der er gehört.
   ------------------------------------------------------------ */

.scrim {
  transition: opacity calc(var(--motion-dialog) * var(--motion-scale)) var(--motion-ease-out);
}

.scrim.is-zu {
  opacity: 0;
  pointer-events: none;
}

.sheet ~ .scrim,
.scrim.scrim--sheet {
  transition-duration: calc(var(--motion-sheet) * var(--motion-scale));
}

/* ------------------------------------------------------------
   Dialog
   ------------------------------------------------------------
   Bleibt zentriert und skaliert aus der Mitte — ein Dialog gehört
   zu keinem Auslöser, aus dem er wachsen könnte.

   scale(0.96), nie scale(0): nichts erscheint aus dem Nichts.
   ------------------------------------------------------------ */

.dialog {
  transform-origin: center;
  transition:
    opacity   calc(var(--motion-dialog) * var(--motion-scale)) var(--motion-ease-out),
    transform calc(var(--motion-dialog) * var(--motion-scale)) var(--motion-ease-out);
}

.dialog.is-zu {
  opacity: 0;
  transform: scale(0.96);
  pointer-events: none;
}

/* ------------------------------------------------------------
   Sheet
   ------------------------------------------------------------
   Fährt um die eigene Höhe hoch, unabhängig vom Inhalt — deshalb
   Prozent, nie Pixel. Verlässt den Screen auf demselben Weg. Das
   ist die Voraussetzung dafür, dass Runterwischen sich richtig
   anfühlt (§19.4 nennt Wischen als einen von drei Wegen raus).

   .is-ziehend hebt den Übergang auf, solange der Finger führt:
   das Sheet muss dem Finger ohne Verzögerung folgen. Beim
   Loslassen greift die Kurve wieder.

   OFFEN: Das Zurückschnappen nach einem abgebrochenen Zug
   gehörte eigentlich einer Feder, die die Geschwindigkeit des
   Fingers mitnimmt. Mit CSS allein nicht möglich.
   ------------------------------------------------------------ */

.sheet {
  transition: transform calc(var(--motion-sheet) * var(--motion-scale)) var(--motion-ease-drawer);
}

.sheet.is-zu {
  transform: translateY(100%);
}

.sheet.is-ziehend {
  transition: none;
}

/* Im Web wird das Sheet ein zentriertes Overlay (§19.5) — dann
   gilt die Dialog-Bewegung, nicht die Schublade. */
.sheet--web {
  transition:
    opacity   calc(var(--motion-dialog) * var(--motion-scale)) var(--motion-ease-out),
    transform calc(var(--motion-dialog) * var(--motion-scale)) var(--motion-ease-out);
}

.sheet--web.is-zu {
  opacity: 0;
  transform: scale(0.96);
}

/* ------------------------------------------------------------
   Toast
   ------------------------------------------------------------
   Kommt von unten, geht nach unten — derselbe Weg in beide
   Richtungen.

   BEWUSSTE AUSNAHME von „unter 300 ms": 400 ms mit der weichen
   eingebauten ease-Kurve. Ein Toast ist eine beiläufige
   Rückmeldung, keine Antwort auf einen Tastendruck; niemand
   wartet auf ihn. Schneller wirkt hier hektisch.

   Transitions statt Keyframes: mehrere Toasts kurz nacheinander
   laufen ineinander, statt neu zu starten.
   ------------------------------------------------------------ */

.toast {
  transition:
    opacity   calc(var(--motion-toast) * var(--motion-scale)) ease,
    transform calc(var(--motion-toast) * var(--motion-scale)) ease;
}

.toast.is-zu {
  opacity: 0;
  transform: translateY(100%);
}

/* ------------------------------------------------------------
   message
   ------------------------------------------------------------
   Steht im Fluss und schiebt Inhalt. Nur Deckkraft, keine
   Bewegung — ein Sprung im Layout ist unruhiger als ein
   ruhiges Erscheinen.
   ------------------------------------------------------------ */

.message {
  transition: opacity calc(var(--motion-dialog) * var(--motion-scale)) var(--motion-ease-out);
}

.message.is-zu {
  opacity: 0;
}

/* ------------------------------------------------------------
   Listen-Control-Menü (Filter · Sortieren, §6)
   ------------------------------------------------------------
   Wächst aus der Pille, die es geöffnet hat. transform-origin
   sitzt deshalb oben links, nicht in der Mitte.
   ------------------------------------------------------------ */

.menue {
  transform-origin: top left;
  transition:
    opacity   calc(var(--motion-popover) * var(--motion-scale)) var(--motion-ease-out),
    transform calc(var(--motion-popover) * var(--motion-scale)) var(--motion-ease-out);
}

.menue.is-zu {
  opacity: 0;
  transform: scale(0.95);
  pointer-events: none;
}

/* ------------------------------------------------------------
   Akkordeon
   ------------------------------------------------------------
   Die einzige erlaubte height-Animation. Kostet Layout in jedem
   Bild, deshalb kurz. Die Höhe wird in JS gemessen — nie auf
   auto animieren, das tut der Browser nicht.
   ------------------------------------------------------------ */

.akkordeon__inhalt {
  overflow: hidden;
  transition:
    height  calc(var(--motion-akkordeon) * var(--motion-scale)) var(--motion-ease-out),
    opacity calc(var(--motion-akkordeon) * var(--motion-scale)) var(--motion-ease-out);
}

.akkordeon__pfeil {
  transition: transform calc(var(--motion-akkordeon) * var(--motion-scale)) var(--motion-ease-out);
}

.akkordeon[data-offen="true"] .akkordeon__pfeil {
  transform: rotate(180deg);
}

/* ------------------------------------------------------------
   Gestaffelter Listeneinstieg
   ------------------------------------------------------------
   Nur für Listen, die einmal pro Sitzung aufgebaut werden — etwa
   die Kategorien eines Abonnements. Nie für Listen, durch die
   gescrollt wird.

   Der Versatz darf die Bedienung nie blockieren.
   ------------------------------------------------------------ */

.stagger > * {
  opacity: 0;
  transform: translateY(8px);
  animation: motion-einblenden calc(300ms * var(--motion-scale)) var(--motion-ease-out) forwards;
  animation-delay: calc(var(--motion-stagger) * var(--motion-scale) * var(--i, 0));
}

@keyframes motion-einblenden {
  to { opacity: 1; transform: translateY(0); }
}

/* ------------------------------------------------------------
   Laden — loading spinner animation
   ------------------------------------------------------------
   Nachgebaut nach der Figma-Animation: dunkle Bahn als voller
   Ring, darauf ein Bogen in der Akzentfarbe, der beim Drehen
   wächst und wieder schrumpft. Zwei Größen.

   Der Figma-Frame trägt die Notiz „Nach Verzögerung" — der
   Spinner erscheint also nicht sofort. Das ist dieselbe Regel:
   unter 300 ms gar keinen Indikator zeigen, sonst lässt ein kurz
   aufblitzender Spinner eine schnelle Antwort langsam aussehen.
   Die Verzögerung gehört in die Logik, nicht ins CSS.

   ZWEI TEILE:
   · Drehung  — reines transform, läuft auf der GPU.
   · Wachsen  — stroke-dasharray, wird gemalt. Klein genug, um
     unkritisch zu sein. Wenn es auf schwachen Geräten stockt:
     .spinner--einfach nehmen, fester Bogen, nur Drehung.

   Umlaufdauer und Bogenlänge sind am 28.08.2026 festgelegt. Sie
   sind aus dem Frame-Bild abgeleitet, nicht aus Figma ausgelesen —
   falls die vier Frames dort andere Zeiten tragen, gilt Figma.
   ------------------------------------------------------------ */

.spinner {
  animation: motion-drehen calc(var(--motion-spinner) * 2 * var(--motion-scale)) linear infinite;
}

.spinner__bogen {
  transform-origin: center;
  animation: motion-bogen calc(var(--motion-spinner) * 3 * var(--motion-scale)) var(--motion-ease-in-out) infinite;
}

.spinner--einfach .spinner__bogen {
  animation: none;
  stroke-dasharray: 50 25;
}

@keyframes motion-drehen {
  to { transform: rotate(360deg); }
}

/* Umfang bei r = 12: rund 75. Die Werte sind am 28.08.2026 von
   r = 18 (Umfang 113) heruntergerechnet, als die Geometrie in
   spinner.css auf die Figma-Maße korrigiert wurde. Wer r ändert,
   muss sie erneut umrechnen — sonst passt der Bogen nicht mehr
   in den Ring. */
@keyframes motion-bogen {
  0%   { stroke-dasharray: 5 70;  stroke-dashoffset: 0; }
  50%  { stroke-dasharray: 53 22; stroke-dashoffset: -12; }
  100% { stroke-dasharray: 5 70;  stroke-dashoffset: -75; }
}

.skelett::after {
  animation: motion-schimmer calc(1200ms * var(--motion-scale)) linear infinite;
}

@keyframes motion-schimmer {
  from { transform: translateX(-100%); }
  to   { transform: translateX(100%); }
}

/* ============================================================
   REDUCED MOTION
   ------------------------------------------------------------
   Weniger und sanfter, nicht aus. Deckkraft und Farbe bleiben —
   sie tragen Verständnis. Ortsveränderung fällt weg — sie ist
   es, die Übelkeit auslösen kann.

   Flächen erscheinen dann, statt zu fahren. Die Bubble springt.
   ============================================================ */

@media (prefers-reduced-motion: reduce) {

  .btn:active:not(:disabled),
  .btn.is-pressed:not(:disabled),
  .chip:active {
    transform: none;
  }

  /* Die Bubble springt, das Icon steigt nicht auf — der
     Farbwechsel allein trägt den Zustand weiter, zusammen mit
     dem Label-Gewicht.

     Abgeschaltet wird der Übergang an .nav: dort läuft er, seit
     Kreis und Kerbe an einem Wert hängen. */
  .nav {
    transition: none;
  }

  .nav__icon,
  .nav__item[aria-current="page"] .nav__icon {
    transform: none;
    transition-delay: 0ms;
  }

  .sheet.is-zu,
  .toast.is-zu {
    transform: none;
    opacity: 0;
  }

  .dialog.is-zu,
  .sheet--web.is-zu,
  .menue.is-zu {
    transform: none;
  }

  .stagger > * {
    transform: none;
    animation-delay: 0ms;
  }

  .skelett::after {
    animation: none;
  }

  /* Der Spinner bleibt — er ist die Auskunft, dass etwas läuft,
     und ohne Drehung gibt es keine. Nur langsamer. */
  .spinner {
    animation-duration: 1200ms;
  }
}

/* ============================================================
   STAND
   ------------------------------------------------------------
   Freigegeben am 28.08.2026: alle Dauern und Kurven dieser Datei,
   einschließlich Bottom Nav (700 ms, --motion-ease-nav,
   Farbwechsel bei 55 %) und Spinner.

   Grundsatz: Trägt eine Figma-Komponente eine Dauer oder Kurve
   ausdrücklich, schlägt sie diese Datei — und ist hier
   nachzuziehen.

ZIELT AUF KOMPONENTEN, DIE ES NOCH NICHT GIBT
   .menue, .akkordeon__* und .stagger haben keine Datei in
   components/. Diese Regeln sind wirkungslos, bis die Komponenten
   freigegeben sind (AGENTS.md Abschnitt 9).

   .chip, .nav__*, .spinner__* und .skelett standen bis zum
   28.08.2026 in dieser Liste — sie haben seither Dateien. Der
   Spinner und der Laden-Button sind in preview.html und in
   examples/preview-motion.html zu sehen; vorher gab es sie in
   keiner gepflegten Vorschau, weshalb die Bewegung dort nicht
   beurteilbar war. Sie stehen hier, damit die Bewegung
   nicht später neu erfunden wird — nicht als Freigabe der
   Komponenten.

   OFFEN
   · Loading-Button: vier Frames in Figma, noch nicht ausgelesen.
   · Sheet-Zurückschnappen bräuchte eine Feder, nicht eine Kurve.
   · Der inset-Ring statt border-width gilt auch für die
     Suchleiste — input.css gleicht den Sprung heute über Padding
     aus, das kann darauf umgestellt werden.
   ============================================================ */
