/* ============================================================
   Bewegungssystem
   ------------------------------------------------------------
   Vier Bewegungen, mehr nicht:

     1. Der Auftritt   Die Überschrift steigt wortweise hinter
                       einer Maske auf. Einmal, beim Laden.
     2. Die Enthüllung Inhalte erscheinen beim Heranscrollen,
                       jeweils genau einmal.
     3. Die Zahlen     Kennzahlen zählen hoch, wenn man sie sieht.
     4. Der Wechsel    Der Projektreiter gleitet, statt zu springen.

   Regeln, die überall gelten:
     - Bewegt werden nur transform und opacity. Alles andere
       zwingt den Browser zu Layout und Neuzeichnen.
     - Eintritte laufen mit ease-out: Die Bewegung startet
       schnell und wird langsamer. Genau umgekehrt fühlt sich
       eine Oberfläche träge an.
     - Nichts erscheint aus dem Nichts. Kein scale(0), kein
       Sprung aus opacity 0 ohne Weg.
     - Wer im Betriebssystem weniger Bewegung eingestellt hat,
       bekommt sie nicht. Der Inhalt bleibt vollständig.
   ============================================================ */

:root {
  /* Die eingebauten CSS-Kurven sind zu weich; sie wirken unentschlossen. */
  --ease-auftritt: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-bewegung: cubic-bezier(0.77, 0, 0.175, 1);
  --ease-ui: cubic-bezier(0.23, 1, 0.32, 1);

  --dauer-auftritt: 760ms;
  --dauer-enthuellung: 620ms;
  --dauer-ui: 200ms;
  --dauer-druck: 140ms;
}

/* ------------------------------------------------------------
   1. Der Auftritt — wortweise hinter einer Maske
   ------------------------------------------------------------
   Jedes Wort bekommt einen Rahmen mit overflow:hidden und darin
   einen Träger, der von unten hereinfährt. Der Rahmen ist unten
   um 0.14em höher als die Zeile und wird per margin wieder
   eingezogen — sonst schneidet er die Unterlängen von g, j und p ab.
   ------------------------------------------------------------ */

.wort {
  display: inline-block;
  /* Rueckfalloption fuer aeltere Browser: Padding schafft Platz fuer
     die Unterlaengen von g, j und p, das negative Margin zieht die
     Box wieder ein. Das kostet vier Pixel Zeilenhoehe. */
  overflow: hidden;
  padding-bottom: 0.14em;
  margin-bottom: -0.14em;
  vertical-align: bottom;
}

/* Wo verfuegbar, sauberer geloest: clip schneidet ab, die
   Clip-Marge laesst die Unterlaengen stehen. Ohne Padding und
   ohne Margin - die Zeilenhoehe bleibt damit exakt die des
   unzerlegten Textes. */
@supports (overflow-clip-margin: 1px) {
  .wort {
    overflow: clip;
    overflow-clip-margin: 0.16em;
    padding-bottom: 0;
    margin-bottom: 0;
    vertical-align: baseline;
  }
}

.wort-traeger {
  /* inline-block, damit die Woerter wie Text umbrechen. Die
     zerlegte Ueberschrift ist dadurch vier Pixel hoeher als der
     unzerlegte Text. Gemessen verschiebt das nichts: Die Zerlegung
     laeuft, solange die Ueberschrift noch verdeckt ist - der
     Layout-Shift-Wert der Seite bleibt bei 0. */
  display: inline-block;
  will-change: transform;
}

.js-motion .hero h1 .wort-traeger,
.js-motion .hero .eyebrow,
.js-motion .hero-lead,
.js-motion .hero-actions,
.js-motion .availability,
.js-motion .hero-visual,
.js-motion .proof-strip > div {
  opacity: 0;
}

/* Bis die Woerter zerlegt sind, bleibt die Ueberschrift verdeckt.
   Sonst blitzt sie einen Bildrahmen lang vollstaendig auf, bevor
   sie hinter die Maske faehrt. */
.js-motion .hero h1 { opacity: 0; }
.hero.ist-aufgetreten h1 { opacity: 1; }

.js-motion .hero h1 .wort-traeger {
  opacity: 1;
  transform: translateY(105%);
}

/* Der Auftritt läuft, sobald die Schrift steht. */
.hero.ist-aufgetreten h1 .wort-traeger {
  transform: translateY(0);
  transition: transform var(--dauer-auftritt) var(--ease-auftritt);
  transition-delay: calc(var(--index, 0) * 42ms);
}

.hero.ist-aufgetreten .eyebrow,
.hero.ist-aufgetreten .hero-lead,
.hero.ist-aufgetreten .hero-actions,
.hero.ist-aufgetreten .availability,
.hero.ist-aufgetreten .hero-visual,
.hero.ist-aufgetreten .proof-strip > div {
  opacity: 1;
  transform: none;
  transition: opacity var(--dauer-enthuellung) var(--ease-auftritt),
              transform var(--dauer-enthuellung) var(--ease-auftritt);
}

.js-motion .hero .eyebrow { transform: translateY(10px); }
.js-motion .hero-lead { transform: translateY(14px); }
.js-motion .hero-actions { transform: translateY(14px); }
.js-motion .availability { transform: translateY(14px); }
.js-motion .proof-strip > div { transform: translateY(16px); }

/* Das Bild kommt nicht von unten, sondern schält sich frei:
   eine Maske gibt es von unten nach oben frei, während es
   minimal aus der Tiefe kommt. */
.js-motion .hero-visual {
  transform: scale(1.03);
  clip-path: inset(0 0 12% 0);
}
.hero.ist-aufgetreten .hero-visual {
  clip-path: inset(0 0 0 0);
  transition: opacity 900ms var(--ease-auftritt) 120ms,
              transform 1100ms var(--ease-auftritt) 120ms,
              clip-path 1100ms var(--ease-auftritt) 120ms;
}

.hero.ist-aufgetreten .eyebrow { transition-delay: 60ms; }
.hero.ist-aufgetreten .hero-lead { transition-delay: 320ms; }
.hero.ist-aufgetreten .hero-actions { transition-delay: 400ms; }
.hero.ist-aufgetreten .availability { transition-delay: 470ms; }
.hero.ist-aufgetreten .proof-strip > div {
  transition-delay: calc(540ms + var(--index, 0) * 70ms);
}

/* Endzustand, unabhängig von jedem Übergang. Greift, wenn die
   Seite nicht gezeichnet wurde und die Bewegung deshalb nie lief -
   siehe die Frist in motion.js. Läuft alles normal, stehen diese
   Werte ohnehin schon an. */
.hero.bewegung-abgeschlossen h1,
.hero.bewegung-abgeschlossen h1 .wort-traeger,
.hero.bewegung-abgeschlossen .eyebrow,
.hero.bewegung-abgeschlossen .hero-lead,
.hero.bewegung-abgeschlossen .hero-actions,
.hero.bewegung-abgeschlossen .availability,
.hero.bewegung-abgeschlossen .hero-visual,
.hero.bewegung-abgeschlossen .proof-strip > div {
  opacity: 1;
  transform: none;
  clip-path: none;
  /* Ohne dieses Abschalten bliebe ein eingefrorener Uebergang
     stehen und hielte den Startwert fest - der Zielwert griffe
     nicht. Genau der Fall, den diese Regel abfangen soll. */
  transition: none;
}

/* ------------------------------------------------------------
   2. Die Enthüllung — beim Heranscrollen, genau einmal
   ------------------------------------------------------------ */

.js-motion [data-auftritt] {
  opacity: 0;
  transform: translateY(18px);
}

.js-motion [data-auftritt].ist-sichtbar {
  opacity: 1;
  transform: none;
  transition: opacity var(--dauer-enthuellung) var(--ease-auftritt),
              transform var(--dauer-enthuellung) var(--ease-auftritt);
  transition-delay: calc(var(--index, 0) * 65ms);
}

/* Vom Sicherheitsnetz gesetzt, wenn der Übergang nicht laufen
   konnte - siehe die Frist in motion.js. */
.js-motion [data-auftritt].ohne-uebergang {
  opacity: 1;
  transform: none;
  transition: none;
}

/* Überschriften einer Sektion kommen eine Spur weiter von unten
   und etwas langsamer - sie sind der Einstieg, nicht die Beilage. */
.js-motion [data-auftritt="kopf"] { transform: translateY(26px); }
.js-motion [data-auftritt="kopf"].ist-sichtbar {
  transition-duration: 780ms;
}

/* Eine Linie, die sich beim Erscheinen aufzieht. Sie markiert
   den Sektionsanfang, ohne ein weiteres Element zu erfinden. */
.section-kicker {
  position: relative;
}
.js-motion .section-kicker::after {
  content: "";
  position: absolute;
  top: 50%;
  left: calc(100% + 14px);
  width: clamp(40px, 12vw, 128px);
  height: 1px;
  background: currentColor;
  opacity: .32;
  transform: scaleX(0);
  transform-origin: left center;
}
.js-motion [data-auftritt].ist-sichtbar .section-kicker::after,
.js-motion .section-kicker.ist-sichtbar::after {
  transform: scaleX(1);
  transition: transform 900ms var(--ease-auftritt) 180ms;
}

/* ------------------------------------------------------------
   3. Die Zahlen — sie zählen, wenn man sie sieht
   ------------------------------------------------------------
   Die Ziffern laufen in Tabellenbreite, sonst zappelt das Layout
   bei jedem Zwischenwert. */

[data-zaehler] {
  font-variant-numeric: tabular-nums;
}

/* ------------------------------------------------------------
   4. Der Wechsel — der Projektreiter gleitet
   ------------------------------------------------------------
   Statt die Hintergrundfarbe von sieben Schaltflächen zu
   animieren, bewegt sich ein einziges Feld hinter ihnen. Das ist
   billiger und liest sich als eine zusammenhängende Bewegung. */

.project-tabs {
  position: relative;
}

.tab-indikator {
  position: absolute;
  z-index: 0;
  top: 0;
  left: 12px;
  right: 12px;
  height: 0;
  border: 1px solid rgba(76, 202, 144, .28);
  border-radius: 8px;
  background: rgba(76, 202, 144, .12);
  transform: translateY(0);
  pointer-events: none;
  opacity: 0;
}

.tab-indikator.ist-bereit {
  opacity: 1;
  transition: transform 420ms var(--ease-bewegung),
              height 420ms var(--ease-bewegung),
              opacity 200ms var(--ease-ui);
}

/* Der Reiter selbst bringt keinen eigenen Hintergrund mehr mit,
   sonst liefen zwei Flächen gegeneinander. */
.project-explorer[data-indikator] .project-tab.is-active {
  background: transparent;
  border-color: transparent;
}
.project-tab { position: relative; z-index: 1; }

/* Der Panelinhalt wird beim Wechsel kurz neu aufgebaut. Kein
   Wegblenden des ganzen Kastens - nur der Inhalt bewegt sich. */
/* Erst ab dem ersten Reiterwechsel, und auf dem Panel selbst
   statt auf seinen acht Blöcken.

   Bewusst ohne animation-fill-mode: Ein Füllmodus hielte den
   Startzustand fest, solange die Animation nicht läuft - in einem
   Hintergrundtab oder bei einem Vorschau-Renderer stünde das
   Projekt dann leer da. Ohne Füllmodus ist der Ruhezustand der
   natürliche: Läuft die Bewegung, sieht man sie; läuft sie nicht,
   ist der Inhalt trotzdem vollständig sichtbar.

   Eine gemeinsame Bewegung statt acht gestaffelter liest sich
   außerdem ruhiger - was zu einer Bewerbungsseite besser passt
   als ein Aufflackern in Wellen. */
.js-motion .project-explorer.hat-gewechselt .project-panel.is-active {
  animation: panel-auftritt 420ms var(--ease-auftritt);
}

@keyframes panel-auftritt {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: none; }
}

/* ------------------------------------------------------------
   Der Sektionsindex — das Element, das man wiedererkennt
   ------------------------------------------------------------
   Greift die Nummerierung auf, die die Seite ohnehin verwendet
   (01 · Profil, 02 · Kompetenz …), und macht daraus eine
   Standortanzeige am Rand. Auf schmalen Bildschirmen entfällt er.
   ------------------------------------------------------------ */

.sektions-index {
  position: fixed;
  z-index: 60;
  top: 50%;
  left: max(18px, calc((100vw - var(--shell-breite, 1240px)) / 2 - 62px));
  display: grid;
  gap: 2px;
  transform: translateY(-50%);
  opacity: 0;
  transition: opacity 500ms var(--ease-ui);
}
.sektions-index.ist-sichtbar { opacity: 1; }

.sektions-index a {
  display: grid;
  grid-template-columns: 22px auto;
  gap: 10px;
  align-items: center;
  padding: 5px 0;
  color: var(--muted);
  font-family: var(--font-mono);
  font-size: 9px;
  letter-spacing: .1em;
  text-transform: uppercase;
}

.sektions-index .index-strich {
  width: 22px;
  height: 1px;
  background: currentColor;
  opacity: .38;
  transform: scaleX(.55);
  transform-origin: left center;
  transition: transform 320ms var(--ease-ui), opacity 320ms var(--ease-ui);
}

.sektions-index .index-name {
  opacity: 0;
  transform: translateX(-5px);
  transition: opacity 260ms var(--ease-ui), transform 260ms var(--ease-ui);
}

.sektions-index a.ist-aktiv { color: var(--copper); }
.sektions-index a.ist-aktiv .index-strich { opacity: 1; transform: scaleX(1); }
.sektions-index a.ist-aktiv .index-name { opacity: 1; transform: none; }

@media (hover: hover) and (pointer: fine) {
  .sektions-index a:hover { color: var(--forest); }
  .sektions-index a:hover .index-strich { opacity: .9; transform: scaleX(1); }
  .sektions-index a:hover .index-name { opacity: 1; transform: none; }
}

@media (max-width: 1420px) {
  .sektions-index { display: none; }
}

/* ------------------------------------------------------------
   Der Lesefortschritt im Kopf
   ------------------------------------------------------------
   Eine Haarlinie an der Unterkante der Kopfzeile. Sie ersetzt
   keine Navigation, sie beantwortet nur die Frage "wie viel
   kommt noch". Sie wird per transform skaliert, nicht per width. */

.lesefortschritt {
  position: absolute;
  inset: auto 0 -1px 0;
  height: 2px;
  background: var(--copper);
  transform: scaleX(0);
  transform-origin: left center;
  opacity: 0;
  transition: opacity 300ms var(--ease-ui);
}
.site-header.is-scrolled .lesefortschritt { opacity: 1; }

/* ------------------------------------------------------------
   Druckpunkt — jedes bedienbare Element antwortet
   ------------------------------------------------------------
   Ohne Rückmeldung beim Drücken wirkt eine Oberfläche tot. Die
   Skalierung bleibt knapp unter der Wahrnehmungsschwelle; man
   spürt sie, ohne sie zu sehen. */

.button,
.project-tab,
.case-link,
.nav-cv,
.case-shot,
.link-more {
  transition: transform var(--dauer-druck) var(--ease-ui),
              background var(--dauer-ui) var(--ease-ui),
              border-color var(--dauer-ui) var(--ease-ui),
              color var(--dauer-ui) var(--ease-ui);
}

.button:active,
.nav-cv:active,
.case-link:active { transform: scale(.975); }
.project-tab:active { transform: scale(.99); }

/* Karten heben sich beim Überfahren minimal an - nur auf Geräten
   mit echtem Zeiger. Auf Touch löst :hover beim Tippen aus und
   bleibt hängen. */
@media (hover: hover) and (pointer: fine) {
  .project-card,
  .credential-card,
  .szenario-karte {
    transition: transform 320ms var(--ease-ui), box-shadow 320ms var(--ease-ui);
  }
  .project-card:hover,
  .credential-card:hover {
    transform: translateY(-3px);
    box-shadow: 0 22px 48px rgba(9, 33, 36, .1);
  }
}

/* Navigationslinks: der Strich läuft ein und beim Verlassen
   wieder hinaus - er springt nicht weg. Das ist bereits in
   styles.css angelegt; hier bekommt er nur die stärkere Kurve. */
.site-nav > a:not(.nav-cv)::after {
  transition: transform 280ms var(--ease-ui);
}

/* ------------------------------------------------------------
   Weniger Bewegung
   ------------------------------------------------------------
   Nicht "keine Animation", sondern keine Ortsveränderung.
   Ein- und Ausblenden bleibt, weil es beim Verstehen hilft;
   Verschieben und Skalieren entfällt. Vor allem: Jeder Inhalt
   ist sofort und vollständig da.
   ------------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  .js-motion .hero h1 .wort-traeger,
  .js-motion .hero .eyebrow,
  .js-motion .hero-lead,
  .js-motion .hero-actions,
  .js-motion .availability,
  .js-motion .hero-visual,
  .js-motion .proof-strip > div,
  .js-motion [data-auftritt] {
    opacity: 1;
    transform: none;
    clip-path: none;
    transition: opacity 240ms linear;
  }

  .js-motion .project-panel.is-active > * { animation: none; }

  .tab-indikator.ist-bereit { transition: opacity 200ms linear; }

  .js-motion .section-kicker::after { transform: scaleX(1); transition: none; }

  .button:active,
  .nav-cv:active,
  .case-link:active,
  .project-tab:active { transform: none; }

  .project-card:hover,
  .credential-card:hover { transform: none; }

  .lesefortschritt { transition: none; }
}
