/* ============================================================================
   COREFRAME · der Uebergang von der Seite in eine Unterseite

   Ein Klick auf eine Oberflaeche fuehrt direkt dorthin, ohne Vorschau
   dazwischen. Damit der Sprung kein Schnitt ist, waechst die angeklickte
   Karte aus ihrem eigenen Rechteck durch den Bildschirm hindurch: sie wird
   groesser als die Buehne, laeuft an einem vorbei, und dahinter steht die
   Zielseite. Man wird hineingezogen statt weggeschickt.

   Warum nicht die View Transitions API: die braucht denselben Ursprung, und
   diese Seite wird auch als file:// geoeffnet. Dort ist jeder Ursprung
   undurchsichtig und der Uebergang faellt still aus. Von Hand gebaut laeuft er
   ueberall.

   Wird von allen fuenf Seiten geladen, zusammen mit warp.js.
   ========================================================================== */

:root {
  --warp-ease: cubic-bezier(0.4, 0, 0.1, 1);
  /* Die Kurve fuer die LANDUNG, und sie ist bewusst eine andere.

     --warp-ease ist Lusions Kurve und startet mit Steigung 0, ist also eine
     Ease-in-out. Auf dem langen Weg des Flugs liest sich dieser Anlauf als
     Masse. Auf dem kurzen Weg der Landung, sechs Prozent Massstab, liest er
     sich als Verzoegerung: gemessen bewegte sich in den ersten 80 der 320ms
     nichts Messbares, und genau dort lag danach wieder ein totes Fenster.
     Das ist dieselbe Teilung nach Weglaenge, die in CLAUDE.md steht, nur
     hier auch fuer den Uebergang angewandt. */
  --warp-land: cubic-bezier(0.23, 1, 0.32, 1);
  /* 340 statt 520. Nachgemessen an lusion.co: dort ist der ganze Wechsel nach
     rund 330ms durch, und er wirkt dabei nicht knapp, sondern entschlossen.
     Unserer lief 520ms und zeigte dabei WENIGER: 8 bis 11 Prozent geaenderte
     Bildpunkte je Frame gegen 42 bis 60 bei Lusion. Dieselbe Strecke in
     kuerzerer Zeit heisst mehr Aenderung je Bild, und genau das liest das
     Auge als Bewegung. Laenger ist hier nicht ruhiger, sondern zaeher. */
  --warp-out: 340ms;   /* hinaus: schnell und entschlossen, nicht gemaechlich */
  /* Der Rueckweg braucht seine eigene, kuerzere Dauer, und zwar aus einem
     messbaren Grund: hinaus faehrt der Flug von der Kartengroesse (etwa 20
     Prozent) auf 106, legt also gut 85 Prozent Massstab zurueck. Zurueck
     faellt er von 110 auf 105, also fuenf. Dieselbe Zeit fuer ein Sechzehntel
     der Strecke heisst, dass die Bewegung sich verliert: gemessen war sie
     nach 185 von 340ms nicht mehr zu sehen, und danach lag der Schirm 210ms
     bei 0,00 Prozent Aenderung. Kuerzer ist hier nicht hektischer, sondern
     der einzige Weg, dass die Bewegung bis zum Schluss eine bleibt. */
  --warp-back: 220ms;  /* zurueck: kurzer Weg, kurze Zeit */
  /* Wie weit der Flug UEBER Bildschirmgroesse hinausfaehrt. Der Wert ist
     nicht kosmetisch, er entscheidet, WANN der Flug deckt, und daran haengt
     der Moment des Tauschs.

     Bei 1.01 deckt er erst, wenn er praktisch fertig ist: gemessen lag die
     erste Deckung bei 462ms einer 520ms-Bewegung, und zwar bei Massstab
     1.0049 von 1.01, also nach 99,4 Prozent der Groessenaenderung. Der
     Stillstand des Tauschs landete damit am ENDE der Bewegung, genau dort,
     wo router.js ihn ausdruecklich nicht haben will. In WebKit waren es
     71ms Stillstand bei 99,6 Prozent.

     Bei 1.06 deckt er nach rund zwei Dritteln der Strecke, und das letzte
     Drittel laeuft sichtbar weiter, waehrend darunter getauscht wird. Das
     ist derselbe Wert wie in Revision 21; der Router-Umbau in Revision 22
     hatte ihn auf 1.01 zurueckgesetzt. */
  --warp-over: 1.06;
  /* Der Wechsel selbst kostet gemessen 66 bis 70ms, auf http wie auf file.
     Genau das muss der Vorhang abfangen, nicht mehr. Er stand auf 460, und
     zusammen mit 600ms Flug ergab das die knappe Sekunde, die sich wie ein
     Neuladen anfuehlte. Bei 220 bleibt genug Auflösung, damit es kein Schnitt
     ist, und nichts wartet mehr auf nichts. */
  --warp-in:  220ms;
}

/* ------------------------------------------------------------- ankommen --
   Die Zielseite blendet nicht auf, sie ist schon da. Darueber liegt fuer einen
   Moment ihr EIGENES Bild, dasselbe, mit dem der Flug gelandet ist, und das
   loest sich auf.

   Vorher lag hier ein Papiervorhang. Der war ehrlich, aber er war ein
   Ereignis: Bild, Papier, Seite. Ein Bild, das sich in dieselbe Ansicht
   aufloest, ist keins mehr, und es faengt nebenbei alles ab, was auf der
   frischen Seite noch nachrueckt: Schriften, Bilder, und im Fall der
   Landingpage die dreissig Kugeln, die sich erst zusammenpacken muessen.

   Die Papierfarbe bleibt als Grund darunter: laedt das Bild nicht rechtzeitig,
   ist es wieder der Vorhang von vorher und kein weisser Blitz. */
/* Der Ankunftsvorhang steht NICHT hier, sondern inline im Kopf jeder Seite.
   Er ist das Einzige, das vor allem anderen malen muss, und ein Stylesheet
   kommt dafuer zu spaet: gemessen 84ms zu spaet. Hier bliebe er wirkungslos
   und stuende nur doppelt. */

/* --------------------------------------------------------------- hinaus -- */

.cf-warp {
  position: fixed;
  inset: 0;
  z-index: 200;
  pointer-events: none;
}

/* Das mitfliegende Bild. Es startet exakt auf dem Rechteck der angeklickten
   Karte und faehrt von dort auf Bildschirmgroesse und darueber hinaus. */
.cf-warp__ghost {
  position: absolute;
  margin: 0;
  overflow: hidden;
  background: #fff;
  transform-origin: 50% 50%;
  will-change: transform;
  box-shadow: 0 1px 2px rgba(43,33,25,.10), 0 18px 40px -18px rgba(43,33,25,.34);
  /* box-shadow wird NICHT mitanimiert. Er ist die einzige Eigenschaft hier,
     die nicht auf der GPU laeuft, und auf einem am Ende fuenffach skalierten
     Element wird daraus ein riesiger Weichzeichner, der jeden Frame neu
     gemalt werden muss. Er steht im ersten Frame, damit der Flug wie die
     Karte aussieht, und faellt dann sofort weg: unter einem wachsenden Bild
     sieht das niemand. */
  /* NUR transform. border-radius ist eine Maleigenschaft, und eine Transition
     darauf zwingt die ganze Animation auf den Hauptstrang: dann bleibt der
     Flug stehen, sobald dort etwas laeuft, und genau das war der Ruckler von
     43ms beim Ausfuehren der Skripte. Auf dem Compositor kann kein Skript
     den Flug anhalten. Die Rundung steht im ersten Frame, damit der Flug wie
     die Karte aussieht, und faellt dann sofort weg: unter einem wachsenden
     Bild sieht das niemand, genauso wie beim Schatten. */
  transition: transform var(--warp-out) var(--warp-ease);
}
.cf-warp__ghost img {
  display: block; width: 100%; height: 100%;
  object-fit: cover; object-position: top left;
}
.cf-warp.is-go .cf-warp__ghost { border-radius: 0; box-shadow: none; }

/* Ueber einem Flug MIT Bild liegt kein Schleier mehr. Er hat das Bild
   zugedeckt, bevor es ankommen konnte, und damit den Wechsel erst sichtbar
   gemacht: Bild, weg, Seite. Das Bild bleibt jetzt bis zuletzt stehen, und
   weil es denselben Ausschnitt zeigt wie die Zielseite, ist der Wechsel
   darunter nicht zu sehen. */
.cf-warp__veil {
  position: absolute;
  inset: 0;
  background: var(--cf-paper, var(--paper, #F4EFE6));
  opacity: 0;
}

/* Alles ausser dem Flug tritt zurueck. Deckkraft, kein transform: sonst
   bricht das Pinning fuer die Dauer des Flugs. */
html.cf-warping body > *:not(.cf-warp) {
  opacity: 0;
  /* Ueber sieben Zehntel der Strecke, nicht ueber die halbe: vorher war die
     Seite nach 260ms weg, der Flug deckte aber erst nach 520. Dazwischen stand
     leeres Papier hinter einem noch wachsenden Bild. */
  transition: opacity calc(var(--warp-out) * 0.72) var(--warp-ease);
}

/* Ein Ziel ohne eigenes Bild, etwa eine Pille oder ein Fusszeilenlink, hat
   nichts, was fliegen koennte. Nur dort traegt der Schleier den Wechsel. */
.cf-warp--plain .cf-warp__veil {
  transition: opacity 260ms var(--warp-ease);
}
.cf-warp--plain.is-go .cf-warp__veil { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  html::before { animation: none; display: none; }
  .cf-warp--back .cf-warp__ghost { transition: none; }
  .cf-warp { display: none; }
  html.cf-warping body > *:not(.cf-warp) { opacity: 1; transition: none; }
}


/* ------------------------------------------------------------ zurueck -----
   Von einer Unterseite zurueck gibt es kein Rechteck, aus dem etwas wachsen
   koennte: die Seite, auf der man steht, ist keine Karte. Also laeuft die
   Bewegung andersherum. Das Ziel steht schon bildschirmfuellend da, aber zu
   nah, und faellt auf seine Groesse zurueck. Man wird nicht hineingezogen,
   man kommt heraus.

   Dieselbe Kurve, dieselbe Landung: das letzte Bild ist wieder genau das
   erste Bild der Zielseite. */
.cf-warp--back .cf-warp__ghost {
  border-radius: 0;
  box-shadow: none;
  /* transform und opacity, beides GPU. Sonst gilt dasselbe wie oben. */
  /* Die Deckkraft laeuft ueber ein gutes Drittel der Strecke, nicht ueber die
     halbe. Auf dem Rueckweg deckt das Bild von Anfang an, der Tausch wartet
     dort also nicht auf Deckung, sondern auf Undurchsichtigkeit. Mit 0.55 war
     das Bild erst nach gut der halben Bewegung dicht, und danach blieben nur
     noch 1,4 Prozent Massstab uebrig: der Stillstand lag wieder am Ende.
     Mit 0.35 ist es nach gut einem Drittel dicht und der Rest der Bewegung
     traegt den Tausch. */
  transition: transform var(--warp-back) var(--warp-ease),
              opacity calc(var(--warp-back) * 0.35) var(--warp-ease);
}


/* ------------------------------------------------ nach dem Tausch --------
   Beim clientseitigen Routen wird kein Dokument gewechselt. Der Flug deckt,
   darunter wird getauscht, und danach loest er sich in die fertige Seite auf.
   Kein Vorhang dazwischen: es wird nichts geladen, worauf zu warten waere. */
/* Erst ankommen, DANN verschwinden — nicht beides zugleich.

   Zugleich war der erste Entwurf, und er ist am Kontaktbogen durchgefallen:
   ein Geist bei 1.06, der ueber der Seite bei 1.00 durchsichtig wird, zeigt
   fuer gut hundert Millisekunden dieselbe Schlagzeile ZWEIMAL, um sechs
   Prozent versetzt. Auf grosser Display-Type ist das kein Weichzeichnen mehr,
   das ist ein Doppelbild, und es liest sich als Fehler. Der Pixelvergleich in
   check-warp.mjs hat es nicht gefangen, weil er im Moment des Tauschs misst
   und nicht waehrend des Aufloesens. Gesehen hat es erst das Auge.

   Also faehrt der Geist die letzten sechs Prozent zurueck, WAEHREND er noch
   deckt: eine echte weiterlaufende Bewegung, kein Ueberblenden. Erst wenn er
   auf dem Mass der Seite steht, blendet er weg — und dann ist der Wechsel
   nicht zu sehen, weil beide dasselbe zeigen. Genau die Verzoegerung ist der
   Punkt, sie darf nicht auf 0. */
.cf-warp.is-done {
  opacity: 0;
  transition: opacity 180ms var(--warp-land) 250ms;
}

/* Und waehrend er sich aufloest, faehrt er die letzten sechs Prozent zurueck
   auf Bildschirmgroesse. Das ist der Grund, warum der Ueberschuss ueberhaupt
   verantwortbar ist: er endet nicht als Ausschnitt, er kommt an.

   Es ist zugleich die einzige Bewegung, die die ANKUNFT hat. Gemessen war der
   Bildschirm nach dem Flug auf eine Unterseite bei 83 Prozent der Frames
   Bild fuer Bild identisch, Median 0,00 Prozent Aenderung: die Bewegung hoerte
   nicht auf, sie brach ab. Lusion hat an derselben Stelle ein Abklingen von
   20 auf 1 Prozent ueber gut 200ms. Genau das macht diese Regel, und sie kann
   dabei nichts verschieben: das Bild und die Seite darunter zeigen dasselbe,
   der Flug schrumpft nur auf ihr Mass, waehrend er durchsichtig wird. */
   Den Zielwert setzt router.js als Inline-Stil, wie den ganzen uebrigen Flug
   auch. Hier steht nur die Dauer, damit das Zuruecklaufen genau so lang
   dauert wie das Aufloesen und beide zusammen ankommen. */
.cf-warp.is-done .cf-warp__ghost {
  transition: transform 320ms var(--warp-land);
}

/* Der Rueckweg braucht hier eine flachere Kurve und mehr Zeit, und beides aus
   demselben gemessenen Grund: er hat nur fuenf Prozent Massstab
   zurueckzulegen, waehrend der Hinweg dieselben sechs Prozent NACH einer
   langen Fahrt zurueckfaehrt. Eine Ease-out legt vier Fuenftel davon im
   ersten Drittel zurueck; bei fuenf Prozent ist der Rest dann nicht mehr zu
   sehen, und es blieben 92ms bei 0,00 Prozent Aenderung stehen, bis die
   dreissig Kugeln der Landingpage bei etwa 430ms uebernahmen.

   Fast gleichmaessig verteilt, ueber 400 statt 320ms, traegt dieselbe kleine
   Strecke die ganze Luecke. Das ist keine Ausnahme von der Regel "kurzer Weg,
   Ease-out", sondern ihr anderer Fall: hier soll die Bewegung nicht
   zuschnappen, sondern eine Uebergabe ueberbruecken. */
.cf-warp--back.is-done .cf-warp__ghost {
  transition: transform 400ms cubic-bezier(0.35, 0.4, 0.6, 0.95);
}
.cf-warp--back.is-done {
  transition: opacity 180ms var(--warp-land) 330ms;
}

@media (prefers-reduced-motion: reduce) {
  .cf-warp.is-done .cf-warp__ghost { transition: none; }
}
