/* ============================================================
   GENERIERT — nicht von Hand bearbeiten.
   Quelle: App/shared/motion.css
   Neu erzeugen: npm run sync:motion (läuft auch in build:portal)
   Portal-spezifische Ergänzungen gehören in portal/portal.css.
   ============================================================ */

/* ============================================================
   BK München Motion System — kanonische Quelle
   ------------------------------------------------------------
   Verwendung:
   - App/src/index.css          → @import '../shared/motion.css'
   - App/scoring/src/index.css  → @import '../../shared/motion.css'
   - App/portal/motion.css      → generierte Kopie (scripts/syncMotion.mjs,
                                  läuft automatisch in build:portal)

   Grundsatz: Bewegung erklärt etwas. Sie zeigt Herkunft, Zugehörigkeit,
   Reihenfolge oder Verbindlichkeit. Reine Dekoration gehört nicht hierher.

   Charakter: stolz, fest, gemeinschaftlich, kontrolliert.
   Nichts hüpft. Nichts zappelt. Dinge kommen an und stehen dann.

   Ergänzt shared/design-tokens.css (--motion-* bleibt für einfache
   Zustandswechsel bestehen); dieses File ist die Ebene darüber.
   ============================================================ */

:root {
  /* ── Dauern ──────────────────────────────────────────────────
     Nach Zweck benannt, nicht nach Zahl. Wer eine neue Animation
     baut, wählt die Absicht — nicht "so ungefähr 300ms". */
  --m-press:    90ms;  /* Tap-/Klick-Rückmeldung, unter der Wahrnehmungsschwelle */
  --m-quick:   160ms;  /* Hover, Farbe, Fokus */
  --m-enter:   240ms;  /* kleine Elemente kommen herein */
  --m-settle:  380ms;  /* Karten, Listeneinträge — ankommen und stehen */
  --m-weighted:520ms;  /* Tabellenplatz, Ergebnis, Rang — hat Gewicht */
  --m-story:   700ms;  /* Scroll-Storytelling, Hero, Vereinsgeschichte */

  /* ── Kurven ──────────────────────────────────────────────────
     --m-ease-settle ist die Standardkurve des Clubs: schneller
     Start, langes ruhiges Auslaufen. Liest sich als Kontrolle.
     --m-ease-firm hat einen minimalen Überschwinger (1.18, nicht
     1.56) — fest, nicht verspielt. Nur für bestätigte Aktionen. */
  --m-ease-settle: cubic-bezier(0.22, 1, 0.36, 1);
  --m-ease-out:    cubic-bezier(0.16, 1, 0.30, 1);
  --m-ease-firm:   cubic-bezier(0.34, 1.18, 0.64, 1);
  --m-ease-in:     cubic-bezier(0.55, 0, 1, 0.45);
  --m-ease-inout:  cubic-bezier(0.65, 0, 0.35, 1);

  /* ── Stagger (Mannschaftsaufstellung) ────────────────────────
     55ms ist der Takt, in dem eine Reihe als *eine* Gruppe liest.
     Darunter wirkt es wie ein Fehler, darüber wie eine Schlange. */
  --m-stagger:       55ms;
  --m-stagger-tight: 34ms;  /* dichte Listen, >10 Einträge */
  --m-stagger-loose: 80ms;  /* wenige, große Elemente */
  --m-stagger-cap:   9;     /* ab hier keine weitere Verzögerung */

  /* ── Wege ────────────────────────────────────────────────────
     Kurze Wege. Der Club bewegt sich nicht quer über den Schirm. */
  --m-rise:    14px;
  --m-rise-sm:  8px;
  --m-rise-lg: 22px;

  /* ── Verbindlichkeit ─────────────────────────────────────────
     Eine Zusage darf nicht in 0ms "fertig" sein — sonst glaubt sie
     niemand. 420ms Mindestdauer für den Pending-Zustand, danach
     setzt sich die Bestätigung fest hin. */
  --m-commit-min:   420ms;
  --m-commit-hold: 1600ms;
}

/* ── Reduced Motion ────────────────────────────────────────────
   Vollständig, nicht nur "kürzer". Alles Sichtbare bleibt sichtbar,
   nur der Weg dorthin entfällt. Einzelne Primitives haben unten
   zusätzlich eigene Overrides, weil Einblend-Animationen mit
   opacity:0 im Startbild sonst unsichtbar hängen bleiben. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --m-press:    0.01ms;
    --m-quick:    0.01ms;
    --m-enter:    0.01ms;
    --m-settle:   0.01ms;
    --m-weighted: 0.01ms;
    --m-story:    0.01ms;
    --m-stagger:       0ms;
    --m-stagger-tight: 0ms;
    --m-stagger-loose: 0ms;
    --m-rise:    0px;
    --m-rise-sm: 0px;
    --m-rise-lg: 0px;
  }
}

/* ══════════════════════════════════════════════════════════════
   1 — KINETISCHE TYPOGRAFIE (Clubidentität)
   Vereinsname und Slogan kommen mit Gewicht: die Zeile schiebt
   sich unter einer Kante hervor, statt einzublenden. Wortweise,
   damit "BK München" als Einheit ankommt und nicht buchstabiert.
   ══════════════════════════════════════════════════════════════ */

.m-kinetic {
  display: block;
}

/* Jede Zeile bekommt ein eigenes Overflow-Fenster — der Text
   schiebt sich darunter hervor. Das ist der "Gewicht"-Effekt. */
.m-kinetic-line {
  display: block;
  overflow: hidden;
  /* Descender (g, y, ß) dürfen nicht abgeschnitten werden */
  padding-bottom: 0.12em;
  margin-bottom: -0.12em;
}

.m-kinetic-word {
  display: inline-block;
  will-change: transform, opacity;
  transform: translate3d(0, 105%, 0);
  opacity: 0;
  animation: m-kinetic-in var(--m-story) var(--m-ease-settle) forwards;
  animation-delay: calc(var(--m-i, 0) * var(--m-stagger-loose) + var(--m-delay, 0ms));
}

@keyframes m-kinetic-in {
  from { transform: translate3d(0, 105%, 0); opacity: 0; }
  60%  { opacity: 1; }
  to   { transform: translate3d(0, 0, 0);    opacity: 1; }
}

/* Slogan darunter: kommt später, ruhiger, ohne Fenster-Effekt —
   die Schlagzeile soll die Bühne behalten. */
.m-kinetic-sub {
  opacity: 0;
  transform: translate3d(0, var(--m-rise-sm), 0);
  animation: m-fade-rise var(--m-settle) var(--m-ease-settle) forwards;
  animation-delay: calc(var(--m-i, 3) * var(--m-stagger-loose) + var(--m-delay, 0ms));
}

@media (prefers-reduced-motion: reduce) {
  .m-kinetic-word,
  .m-kinetic-sub {
    animation: none !important;
    transform: none !important;
    opacity: 1 !important;
  }
  .m-kinetic-line { overflow: visible; }
}

/* ══════════════════════════════════════════════════════════════
   2 — FORMATION (Mannschaftsaufstellung)
   Kader, Mitgliederraster, Trainingsgruppen. Die Elemente kommen
   nicht einzeln, sondern als Aufstellung: gleicher Weg, gleicher
   Takt, versetzter Einsatz. Das ist der Team-Effekt.
   `--m-i` setzt JS/React pro Kind (siehe motion.js / Formation.jsx);
   ohne JS greifen die nth-child-Fallbacks weiter unten.
   ══════════════════════════════════════════════════════════════ */

.m-formation > * {
  opacity: 0;
  transform: translate3d(0, var(--m-rise), 0);
  animation: m-fade-rise var(--m-settle) var(--m-ease-settle) forwards;
  animation-delay: calc(min(var(--m-i, 0), var(--m-stagger-cap)) * var(--m-stagger) + var(--m-delay, 0ms));
}

/* Dichte Listen (Mitglieder, Ergebnisse) laufen enger getaktet,
   sonst wartet man am Ende der Liste spürbar. */
.m-formation--tight > * {
  animation-duration: var(--m-enter);
  animation-delay: calc(min(var(--m-i, 0), var(--m-stagger-cap)) * var(--m-stagger-tight) + var(--m-delay, 0ms));
}

.m-formation--loose > * {
  animation-delay: calc(min(var(--m-i, 0), var(--m-stagger-cap)) * var(--m-stagger-loose) + var(--m-delay, 0ms));
}

/* Fallback ohne JS-Index: die ersten neun Kinder bekommen ihren
   Takt aus nth-child, alles danach startet gemeinsam. */
.m-formation > *:nth-child(1) { --m-i: 0; }
.m-formation > *:nth-child(2) { --m-i: 1; }
.m-formation > *:nth-child(3) { --m-i: 2; }
.m-formation > *:nth-child(4) { --m-i: 3; }
.m-formation > *:nth-child(5) { --m-i: 4; }
.m-formation > *:nth-child(6) { --m-i: 5; }
.m-formation > *:nth-child(7) { --m-i: 6; }
.m-formation > *:nth-child(8) { --m-i: 7; }
.m-formation > *:nth-child(9) { --m-i: 8; }
.m-formation > *:nth-child(n+10) { --m-i: 9; }

@keyframes m-fade-rise {
  from { opacity: 0; transform: translate3d(0, var(--m-rise), 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}

@media (prefers-reduced-motion: reduce) {
  .m-formation > * {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
}

/* ══════════════════════════════════════════════════════════════
   3 — SCROLL-STORYTELLING
   Vereinsgeschichte, Werte, Erfolge. Der Abschnitt richtet sich
   auf, wenn er dran ist. Einmalig — nichts animiert beim
   Zurückscrollen erneut, das wäre Jahrmarkt.
   JS setzt `.is-in` (motion.js / useReveal).
   ══════════════════════════════════════════════════════════════ */

.m-reveal {
  opacity: 0;
  transform: translate3d(0, var(--m-rise-lg), 0);
  transition:
    opacity var(--m-story) var(--m-ease-settle),
    transform var(--m-story) var(--m-ease-settle);
  transition-delay: var(--m-delay, 0ms);
}

.m-reveal.is-in {
  opacity: 1;
  transform: translate3d(0, 0, 0);
}

/* Reveal + Formation kombiniert: der Block richtet sich auf, die
   Kinder stellen sich darin auf. Kinder warten, bis der Block da ist. */
.m-reveal .m-formation > *,
.m-reveal.m-formation > * { animation-play-state: paused; }
.m-reveal.is-in .m-formation > *,
.m-reveal.is-in.m-formation > * { animation-play-state: running; }

@media (prefers-reduced-motion: reduce) {
  .m-reveal,
  .m-reveal.is-in {
    opacity: 1;
    transform: none;
    transition: none;
  }
  .m-reveal .m-formation > *,
  .m-reveal.m-formation > * { animation-play-state: running; }
}

/* ══════════════════════════════════════════════════════════════
   4 — WETTKAMPF: TABELLE, ERGEBNIS, PLATZIERUNG
   Der autoritative Teil. Hier darf nichts spielerisch wirken —
   eine Tabelle, die wackelt, wirkt unglaubwürdig.

   Tabellenwechsel läuft als FLIP (JS misst, CSS bewegt): die Zeile
   fährt auf ihren neuen Platz, statt zu springen. Danach eine
   einzelne, ruhige Kantenmarkierung in Richtung der Bewegung.
   ══════════════════════════════════════════════════════════════ */

.m-rank-row {
  /* transform wird von useFlipReorder/flipReorder gesetzt */
  will-change: transform;
}

.m-rank-row.is-moving {
  transition: transform var(--m-weighted) var(--m-ease-settle);
}

/* Richtungsmarkierung: linke Kante, einmalig, kein Blinken.
   Grün = aufgestiegen, gedämpftes Rot = abgerutscht. */
.m-rank-row.is-up::after,
.m-rank-row.is-down::after {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 3px;
  border-radius: 0 2px 2px 0;
  pointer-events: none;
  animation: m-rank-mark 1400ms var(--m-ease-settle) forwards;
}

.m-rank-row.is-up::after   { background: var(--t-success, #22c55e); }
.m-rank-row.is-down::after { background: var(--t-danger,  #ef4444); }

/* Positionsrelativ nötig, damit ::after greift — Tabellenzeilen
   bekommen das über die JS-gesetzte Klasse mit. */
.m-rank-row { position: relative; }

@keyframes m-rank-mark {
  0%   { opacity: 0; transform: scaleY(0.3); }
  22%  { opacity: 1; transform: scaleY(1); }
  70%  { opacity: 1; }
  100% { opacity: 0; }
}

/* Tabellen-Grundgerüst — bewusst hier und nicht in den App-Styles,
   damit Website, Club-App und Anzeigetafel dieselbe Tabelle zeigen. */
.m-standings {
  width: 100%;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
}

.m-standings th,
.m-standings td {
  padding: var(--sp-2, 0.5rem) var(--sp-3, 0.75rem);
  text-align: left;
  border-bottom: 1px solid var(--t-surface-border, rgba(96, 165, 250, 0.14));
}

.m-standings thead th {
  font-size: var(--fs-xs, 0.75rem);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--t-text-muted, #94a3b8);
}

.m-standings tbody th {
  font-weight: 700;
  color: var(--t-text, #f1f5f9);
}

.m-standings-pos { width: 3rem; text-align: center; color: var(--t-text-muted, #94a3b8); }
.m-standings-num { width: 4rem; text-align: right; }

/* Der eigene Verein bleibt dauerhaft markiert — das ist Zugehörigkeit,
   keine Animation.
   Die Fläche sitzt auf den Zellen, nicht auf der Zeile: bei
   border-collapse malt ein <tr>-Hintergrund nicht zuverlässig hinter
   allen Zellen, dadurch bricht die Markierung mitten in der Zeile ab. */
.m-standings .is-own > th,
.m-standings .is-own > td {
  background: rgba(var(--t-accent-rgb, 37, 99, 235), 0.12);
  color: var(--t-accent-light, #60a5fa);
}

/* Ergebnis rastet ein ("amtlich"). Kein Aufblitzen, sondern ein
   kurzes Zusammenziehen und ein Ring, der sich einmal schließt —
   wie ein Stempel, der aufgesetzt wird. */
.m-lock {
  animation: m-lock-settle var(--m-weighted) var(--m-ease-firm) both;
}

.m-lock::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  border: 2px solid var(--t-accent, #2563eb);
  opacity: 0;
  pointer-events: none;
  animation: m-lock-ring 900ms var(--m-ease-settle) 120ms forwards;
}

@keyframes m-lock-settle {
  0%   { transform: scale(1.028); }
  55%  { transform: scale(0.996); }
  100% { transform: scale(1); }
}

@keyframes m-lock-ring {
  0%   { opacity: 0;    transform: scale(1.035); }
  30%  { opacity: 0.85; transform: scale(1); }
  100% { opacity: 0;    transform: scale(1); }
}

/* Live-Score-Aktualisierung: die Zahl selbst zählt (JS), die Zelle
   bekommt nur einen kurzen Halo, damit klar ist WELCHE Zahl neu ist. */
.m-score-change {
  animation: m-score-halo 760ms var(--m-ease-settle) both;
}

@keyframes m-score-halo {
  0%   { background-color: rgba(var(--t-accent-rgb, 37, 99, 235), 0.28); }
  100% { background-color: rgba(var(--t-accent-rgb, 37, 99, 235), 0); }
}

@media (prefers-reduced-motion: reduce) {
  .m-rank-row.is-moving { transition: none !important; }
  .m-rank-row.is-up::after,
  .m-rank-row.is-down::after { animation: none !important; opacity: 1; }
  .m-lock,
  .m-lock::before,
  .m-score-change { animation: none !important; }
  .m-lock::before { opacity: 0; }
}

/* ══════════════════════════════════════════════════════════════
   5 — FORTSCHRITT & ENTWICKLUNG
   Schnitt, Ranking, Meilensteine. Der Balken wächst einmal auf
   seinen Wert — er pulsiert nicht und läuft nicht rückwärts.
   Wert kommt über --m-value (0..1).
   ══════════════════════════════════════════════════════════════ */

.m-progress {
  position: relative;
  overflow: hidden;
  border-radius: var(--r-full, 999px);
  background: var(--t-surface-3, rgba(8, 15, 29, 0.58));
}

.m-progress-fill {
  display: block;
  height: 100%;
  border-radius: inherit;
  transform-origin: left center;
  transform: scaleX(var(--m-value, 0));
  transition: transform var(--m-story) var(--m-ease-settle);
}

/* Verbesserung gegenüber vorher: der Delta-Wert steigt kurz auf und
   setzt sich. Nur bei echter Verbesserung einsetzen. */
.m-improve {
  animation: m-improve-rise var(--m-weighted) var(--m-ease-firm) both;
}

@keyframes m-improve-rise {
  0%   { transform: translate3d(0, 6px, 0); opacity: 0; }
  60%  { transform: translate3d(0, -2px, 0); opacity: 1; }
  100% { transform: translate3d(0, 0, 0); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .m-progress-fill { transition: none !important; }
  .m-improve { animation: none !important; opacity: 1; }
}

/* ══════════════════════════════════════════════════════════════
   6 — VERBINDLICHKEIT (Zusage, Teilnahme, Anmeldung)
   Drei Zustände: bereit → wird verbindlich → steht.
   Die Pause dazwischen ist Absicht: eine Zusage, die sofort
   "fertig" ist, fühlt sich an wie ein Klick ins Leere.
   ══════════════════════════════════════════════════════════════ */

.m-commit {
  position: relative;
  transition:
    transform var(--m-press) var(--m-ease-out),
    background-color var(--m-quick) var(--m-ease-out),
    border-color var(--m-quick) var(--m-ease-out),
    box-shadow var(--m-quick) var(--m-ease-out);
}

.m-commit:active:not(:disabled) { transform: scale(0.975); }

/* Läuft: die Fläche arbeitet sichtbar, aber ruhig. Kein Spinner-
   Karussell — ein Streifen, der einmal pro Sekunde durchläuft. */
.m-commit.is-pending {
  cursor: progress;
}

.m-commit.is-pending::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(
    100deg,
    transparent 20%,
    rgba(255, 255, 255, 0.20) 50%,
    transparent 80%
  );
  background-size: 220% 100%;
  animation: m-commit-sweep 1000ms var(--m-ease-inout) infinite;
}

@keyframes m-commit-sweep {
  from { background-position: 180% 0; }
  to   { background-position: -80% 0; }
}

/* Steht: einmal fest hinsetzen. Der Überschwinger ist minimal —
   es soll nach "erledigt" aussehen, nicht nach Belohnung. */
.m-commit.is-confirmed {
  animation: m-commit-settle var(--m-settle) var(--m-ease-firm) both;
}

@keyframes m-commit-settle {
  0%   { transform: scale(0.975); }
  45%  { transform: scale(1.022); }
  100% { transform: scale(1); }
}

/* Häkchen zeichnet sich, statt zu erscheinen */
.m-check-path {
  stroke-dasharray: 24;
  stroke-dashoffset: 24;
  animation: m-check-draw var(--m-enter) var(--m-ease-out) 60ms forwards;
}

@keyframes m-check-draw {
  to { stroke-dashoffset: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .m-commit.is-pending::after,
  .m-commit.is-confirmed,
  .m-check-path { animation: none !important; }
  .m-check-path { stroke-dashoffset: 0; }
  .m-commit:active:not(:disabled) { transform: none; }
}

/* ══════════════════════════════════════════════════════════════
   7 — KONTROLLIERTER JUBEL
   Sieg, Vereinsjubiläum, persönlicher Meilenstein.
   Energie, dann Ruhe. Kein Konfetti, keine Partikel — der Club
   feiert erwachsen. Ein Lichtschein blüht auf und geht zurück,
   die Überschrift atmet einmal.
   ══════════════════════════════════════════════════════════════ */

.m-celebrate {
  position: relative;
  animation: m-celebrate-breath 1200ms var(--m-ease-settle) both;
}

.m-celebrate::before {
  content: '';
  position: absolute;
  inset: -20%;
  border-radius: inherit;
  pointer-events: none;
  background: radial-gradient(
    circle at 50% 45%,
    rgba(var(--t-accent-rgb, 37, 99, 235), 0.38),
    transparent 62%
  );
  opacity: 0;
  animation: m-celebrate-bloom 1600ms var(--m-ease-settle) both;
}

@keyframes m-celebrate-breath {
  0%   { transform: scale(1); }
  26%  { transform: scale(1.035); }
  100% { transform: scale(1); }
}

@keyframes m-celebrate-bloom {
  0%   { opacity: 0;    transform: scale(0.86); }
  24%  { opacity: 1;    transform: scale(1.04); }
  100% { opacity: 0;    transform: scale(1.12); }
}

/* Errungenschaft im Profil: stolz, aber zurückhaltend. Das Abzeichen
   kommt aus der Tiefe nach vorn statt von der Seite hereinzufliegen. */
.m-achievement {
  opacity: 0;
  transform: scale(0.9);
  animation: m-achievement-in var(--m-settle) var(--m-ease-firm) forwards;
  animation-delay: calc(min(var(--m-i, 0), var(--m-stagger-cap)) * var(--m-stagger) + var(--m-delay, 0ms));
}

@keyframes m-achievement-in {
  0%   { opacity: 0; transform: scale(0.9); }
  100% { opacity: 1; transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .m-celebrate,
  .m-celebrate::before,
  .m-achievement { animation: none !important; }
  .m-celebrate::before { opacity: 0; }
  .m-achievement { opacity: 1; transform: none; }
}

/* ══════════════════════════════════════════════════════════════
   8 — MIKROINTERAKTIONEN
   Alles, was man anfasst, antwortet sofort und körperlich.
   Unter 100ms, sonst fühlt es sich nach Verzögerung an.
   ══════════════════════════════════════════════════════════════ */

.m-press {
  transition: transform var(--m-press) var(--m-ease-out);
  -webkit-tap-highlight-color: transparent;
}
.m-press:active:not(:disabled) { transform: scale(0.97); }

/* Karte hebt sich beim Zeigen leicht an und legt sich beim Drücken
   wieder hin — physisch, nicht schwebend. */
.m-card-press {
  transition:
    transform var(--m-quick) var(--m-ease-out),
    box-shadow var(--m-quick) var(--m-ease-out),
    border-color var(--m-quick) var(--m-ease-out);
}
.m-card-press:hover { transform: translate3d(0, -2px, 0); }
.m-card-press:active { transform: translate3d(0, 0, 0) scale(0.995); }

/* Listeneintrag: keine Vertikalbewegung (das würde die Liste
   unruhig machen), sondern ein kurzer Zug nach rechts. */
.m-row-press {
  transition:
    transform var(--m-quick) var(--m-ease-out),
    background-color var(--m-quick) var(--m-ease-out);
}
.m-row-press:hover  { transform: translate3d(2px, 0, 0); }
.m-row-press:active { transform: translate3d(0, 0, 0); }

@media (prefers-reduced-motion: reduce) {
  .m-press:active:not(:disabled),
  .m-card-press:hover,
  .m-card-press:active,
  .m-row-press:hover,
  .m-row-press:active { transform: none !important; }
}

/* ══════════════════════════════════════════════════════════════
   9 — THEMEWECHSEL
   Dark/Light/Maple/Neon. Farbflächen blenden über, Text nicht —
   überblendende Schrift liest sich als Fehler. JS setzt
   `data-theme-shift` für die Dauer des Wechsels (siehe motion.js).
   ══════════════════════════════════════════════════════════════ */

html[data-theme-shift] body,
html[data-theme-shift] body * {
  transition:
    background-color var(--m-settle) var(--m-ease-inout),
    border-color var(--m-settle) var(--m-ease-inout),
    box-shadow var(--m-settle) var(--m-ease-inout) !important;
}

/* Bilder, Medien und alles mit eigener Animation bleiben außen vor */
html[data-theme-shift] img,
html[data-theme-shift] video,
html[data-theme-shift] canvas,
html[data-theme-shift] svg { transition: none !important; }

@media (prefers-reduced-motion: reduce) {
  html[data-theme-shift] body,
  html[data-theme-shift] body * { transition: none !important; }
}

/* ══════════════════════════════════════════════════════════════
   10 — HILFSKLASSEN
   ══════════════════════════════════════════════════════════════ */

/* Zahlen in Bewegung brauchen feste Ziffernbreite, sonst springt
   die Spalte beim Hochzählen. */
.m-num { font-variant-numeric: tabular-nums; }

/* Verzögerungs-Anker für einzelne Elemente ohne eigenen Index */
.m-delay-1 { --m-delay: 60ms; }
.m-delay-2 { --m-delay: 120ms; }
.m-delay-3 { --m-delay: 200ms; }
.m-delay-4 { --m-delay: 300ms; }

/* Kompositions-Isolierung für Flächen mit dauerhafter Animation —
   hält Repaints aus dem Rest des Layouts heraus. */
.m-layer { contain: paint; }

/* Nur für Screenreader. Eigene Klasse statt Tailwinds sr-only, weil
   das Portal kein Tailwind hat und beide Seiten dieselbe kinetische
   Schlagzeile verwenden. */
.m-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
