/* =========================================================
   Übergänge & Effekte – hochwertiger, moderner Stil
   (nach Feedback: kein strikter 2020er-Look mehr nötig, Vorbild
   sind aktuelle High-End-Websites: ruhige, smoothe Bewegungen)
   ========================================================= */

/* =========================================================
   INTRO / PRELOADER — Figma "www.3u-reinigung.ch (Intro)"
   (Node 2:21517, 1440×798, Hintergrund #FAFAFA). Siehe
   Kommentar in index.html für Details/Quelle. Der Fortschritt
   (Linien, Tropfen-Zusammensetzung, Prozentzahl) wird laufend
   per JS anhand von --intro-progress (0–1) gesteuert, siehe
   initPreloader() in effects.js - kein Zufalls-/Fake-Timer,
   sondern an echten Ladefortschritt gekoppelt.
   ========================================================= */
/* Runde 31: Der tropfenfoermige Mauszeiger ist entfallen.

   Zwei Gruende. Erstens war er fehlerhaft: Er wechselte je nach
   Untergrund zwischen einer goldenen und einer navyblauen Fassung, und
   zwar bei jeder Mausbewegung. Jeder Wechsel laedt im Browser ein neues
   Zeigerbild - genau das sah man als kurzes Flackern/"Laden". Zweitens
   nimmt ein eigener Mauszeiger dem Benutzer die gewohnte Rueckmeldung:
   Der Systemzeiger zeigt mit seiner Form an, ob etwas anklickbar ist
   oder ob Text markiert werden kann. Wer das nicht gewohnt ist oder
   schlechter sieht, verliert diese Orientierung.

   Die Vorlage sah ihn ohnehin nicht vor; der rote Figma-Hinweis
   "vierter Tropfen wird zur Maus" war eine Regieanweisung, die der
   Auftraggeber verworfen hatte. Alles andere am Zeiger bleibt wie
   gewohnt - Hand ueber Links, Textcursor ueber Text. */

/* Runde 30: Das Intro ist die Tropfen-Choreografie, eingedampft auf einen
   einzigen fallenden Tropfen, aus dem das Logo entspringt (siehe
   .intro-faller / .intro-drop weiter unten). Es gibt nur EINEN Regler:

     --iz   Gesamttempo. 1.0 ergibt 1700 ms Choreografie - danach uebernimmt
            der normale Seitenuebergang (Vorhang hoch, Vorhang weiter hoch).
            0.8 waere gut 1.3 s, 1.2 gut 2.0 s.

   Der frueher zweite Regler --is (Versatz der drei Tropfen) ist entfallen,
   weil nur noch ein Tropfen faellt. effects.js liest --iz aus, damit es nur
   EINE Stelle dafuer gibt. */
#preloader {
  --iz: 1.0;
  position: fixed;
  inset: 0;
  z-index: 10000;
  background: #FAFAFA;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}
#preloader.is-hidden { visibility: hidden; pointer-events: none; }

/* Grauer Kreuz-Linien-Effekt, gleicher Stil/Farbe wie .home-line-v/.home-line-h
   (rgba(21,21,21,0.16)), positioniert nach den Figma-Koordinaten "Line 1131"
   (x=321/1440) und "Line 1132" (y=607/798). Ablauf per JS (initPreloader):
   vertikal zieht sich zuerst langsam auf, horizontal startet erst, sobald die
   vertikale Linie ihren eigenen Kreuzungspunkt erreicht hat (Feedback: "man
   erkennt den Übergang nicht" -> deutlich verlangsamt, plus dieselbe
   Start-erst-wenn-vertikal-durch-Logik wie auf der übrigen Website). */
.intro-line {
  position: absolute;
  background: rgba(21, 21, 21, 0.16);
  pointer-events: none;
}
.intro-line-v {
  left: 22.29%;
  top: 0;
  width: 1px;
  height: 100%;
  transform: scaleY(0);
  transform-origin: top;
  transition: transform calc(1550ms * var(--iz)) cubic-bezier(0.4, 0, 0.2, 1);
}
.intro-line-h {
  left: 0;
  top: 76.07%;
  width: 100%;
  height: 1px;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform calc(521ms * var(--iz)) cubic-bezier(0.4, 0, 0.2, 1);
}
.intro-line-v.is-drawn { transform: scaleY(1); }
.intro-line-h.is-drawn { transform: scaleX(1); }

/* Sehr blasse, grosse Prozentzahl - gleiche "Bleed"-Bildsprache wie
   .home-numeral (Cremefarbe F9F5EF auf Gray-100-Hintergrund FAFAFA). Das
   echte Figma-Vektor-Asset ("Group 17322") konnte technisch nicht
   heruntergeladen werden (siehe Chat) - hier als Live-Text mit derselben
   Schrift/Farb-Sprache nachgebaut statt frei erfunden. Rechtsbündig: die
   Box ist "shrink-to-fit" (kein "left", nur "right"), dadurch bleibt das
   %-Zeichen (letztes Zeichen im Fluss) bei 1-/2-/3-stelligen Werten immer
   an derselben Stelle stehen, während die Ziffern nach links wachsen - bei
   "100" bleibt so auch der gewünschte Bleed-Effekt über den linken Rand
   hinaus erhalten. */
.intro-percent {
  position: absolute;
  /* Feedback-Runde 3, Punkt 2.1: bei "right:55vw" bleedete bereits jede
     zweistellige Zahl (0-99%) minimal am linken Rand, und bei "100%"
     verschwand die FÜHRENDE "1" komplett - sichtbar gebleedet wirkte
     dadurch nur die erste "0", umgekehrt zum Wunsch. Bei "right:48vw"
     (Runde 3) war "100%" dann zwar korrekt auf der "1", aber deutlich zu
     stark gebleedet (>90px, die "1" praktisch unsichtbar) und die
     nachfolgende "0" bekam durch den Font-Wechsel (Runde 4, opsz 15 +
     Zeichenabstand -2%) ebenfalls einen kleinen ungewollten Bleed ab.
     Feedback-Runde 4, Punkt 1: erneut empirisch nachgemessen (nach dem
     Font-Wechsel ist ein Zeichen bei 30vw/1440px-Referenz jetzt 207.4px
     breit statt 216px) und neu kalibriert - bei "right:42.8vw" bleiben
     0-99% weiterhin komfortabel (~189px Abstand) komplett im Bild,
     während bei "100%" nur noch ein KLEINER, klar sichtbarer Bleed
     (~18px) auf der linken Kante der "1" entsteht und "00%" vollständig
     sichtbar bleiben - kein Bleed mehr auf der "0". */
  /* Runde 17, Punkt 1: Die Vorlage (Figma "Intro" 2:21517, Gruppe 17322)
     setzt die Zahl auf x = -144 und macht sie 377 hoch - auf dem Desktop
     genau wie im Fluss. Die frueheren vw-Werte waren eine Naeherung aus
     Runde 3/4, noch ohne die Vorlage. Groesse und Hoehenlage misst
     effects.js nach (fitIntroPercent); diese Werte sind die
     Rueckfallebene ohne JavaScript. */
  left: -144px;
  right: auto;
  top: calc(76.07% - 0.509em);
  bottom: auto;
  font-family: var(--font-headline);
  font-style: italic;
  font-weight: 500;
  font-size: 548px;
  line-height: 1;
  letter-spacing: var(--font-headline-tracking);
  color: var(--color-cream);
  white-space: nowrap;
  pointer-events: none;
  z-index: 0;
  user-select: none;
}
/* Runde 17, Punkt 1: Das Prozentzeichen ist in der Vorlage genauso gross
   wie die Ziffern (Figma-Rahmen "Intro" 2:21517, Gruppe 17322: Pfad "%"
   377 hoch, Ziffern 374 und 367). Die bisherigen 0.62em waren eine alte
   Abweichung. Gilt jetzt auf ALLEN Breiten - so gewuenscht.
   Der Abstand davor ist das Leerzeichen aus der Vorlage ("84 %"). */
.intro-percent-sign { font-size: 1em; margin-left: 0; }
.intro-percent-sign::before { content: "\00a0"; }

/* =========================================================
   INTRO: EIN TROPFEN, AUS DEM DAS LOGO ENTSPRINGT (Runde 30)

   Zurueck zur Tropfen-Choreografie - aber auf das Wesentliche
   eingedampft, damit sie in weniger als zwei Sekunden durch ist:

     0 - 40 %   (0 - 680 ms) EIN Tropfen. Er steht vom ERSTEN BILD an
                sichtbar am oberen Bildschirmrand (nicht darueber, und
                ohne Einblenden) und faellt in EINEM Zug zur Bildmitte -
                durchgehend beschleunigend, ohne Zwischenstopp. Dabei zieht er sich in die
                Laenge und staucht sich beim Aufschlag - dieselbe
                Squash-and-Stretch-Figur wie bisher.
     40 - 48 %  (680 - 816 ms) Der Aufprall: Welle, Krone, Spritzer auf
                Wurfparabel, Rayleigh-Saeule. Unveraendert aus der
                bisherigen Fassung uebernommen.
     48 - 88 %  (816 - 1496 ms) Aus dem Aufprall entspringen die DREI
                Teile des Logos. Sie schiessen mit der Saeule nach oben,
                wachsen dabei auf ihre Groesse und gehen auseinander -
                jedes direkt an seinen Platz. OHNE Ueberschwinger: sie
                laufen auf ihre Endlage zu und werden dabei langsamer,
                statt daran vorbeizuschiessen und zurueckzufedern.
     88 - 100 % (1496 - 1700 ms) Das fertige Logo steht.

   Der Unterschied zur Fassung bis v43: dort fielen DREI Tropfen
   nacheinander, jeder mit eigenem Aufprall und eigenem Sprung - das
   dauerte allein 2.6 Sekunden. Jetzt faellt einer, und das Logo entsteht
   aus seinem Aufprall.

   Ein einziger Regler: --iz (Gesamttempo). 1.0 ergibt 1700 ms.
   ========================================================= */
.intro-mark {
  position: relative;
  width: 90px;
  height: 90px;
  z-index: 1;
}

/* --- der eine fallende Tropfen --------------------------------------- */
.intro-faller {
  position: absolute;
  inset: 0;
  display: block;
  z-index: 3;
  animation-duration: calc(1700ms * var(--iz));
  animation-timing-function: linear;
  animation-fill-mode: forwards;
  animation-play-state: paused;
}
.intro-faller svg { display: block; position: absolute; }
/* Fallform: generische, realistische Tropfen-Silhouette (rund unten, spitz
   oben), mittig im 90er-Bezugsraster verankert. */
.drop-fall-shape {
  left: 50%; top: 50%; width: 26px; height: 34px;
  margin: -17px 0 0 -13px;
  opacity: 1;
  animation-timing-function: linear;
  animation-fill-mode: forwards;
  animation-play-state: paused;
}

/* --- die drei Teile des Logos ---------------------------------------- */
.intro-drop {
  position: absolute;
  inset: 0;
  display: block;
  opacity: 0;
  animation-duration: calc(1700ms * var(--iz));
  animation-timing-function: linear;
  animation-fill-mode: forwards;
  animation-play-state: paused;
}
.intro-drop svg { display: block; position: absolute; }
.drop-real-shape { inset: 0; width: 100%; height: 100%; }

#preloader.is-playing .intro-faller,
#preloader.is-playing .intro-drop,
#preloader.is-playing .drop-fall-shape { animation-play-state: running; }

/* =========================================================
   DER FALL

   "bereits ab 0 % sichtbar": Der Tropfen startet nicht ausserhalb des
   Bildes, sondern mit seiner Spitze genau an der oberen Kante - darum
   50vh (halbe Bildschirmhoehe, die Buehne ist mittig) minus seine halbe
   Hoehe. Auf jedem Bildschirm steht er damit vom ersten Bild an sichtbar
   da, und die Fallstrecke passt sich der Fensterhoehe an.
   ========================================================= */
@keyframes introFall {
  /* EINE Kurve vom ersten bis zum letzten Bild - cubic-bezier(1/3, 0, 2/3, 1/3)
     ist exakt die Parabel y = x². Das ist freier Fall: Der Weg waechst mit
     dem Quadrat der Zeit, die Geschwindigkeit gleichmaessig.

     Bis v46 standen hier ZWEI Abschnitte (0-6 % und 6-40 %) mit je eigener
     Kurve. Jede Kurve beginnt und endet mit Geschwindigkeit null - an der
     Nahtstelle bremste der Tropfen deshalb bis zum Stillstand ab und lief
     wieder an. Genau das war das "zuerst schnell, dann langsamer, dann
     exponentiell". Mit einer einzigen Kurve gibt es keine Nahtstelle. */
  0%   { transform: translateY(calc(-50vh + 21px)); opacity: 1;
         animation-timing-function: cubic-bezier(0.3333, 0, 0.6667, 0.3333); }
  40%  { transform: translateY(16px); opacity: 1;
         animation-timing-function: linear; }
  46%  { transform: translateY(16px); opacity: 0; }
  100% { transform: translateY(16px); opacity: 0; }
}
/* Squash and Stretch: eigenes transform auf der Form selbst, das sich mit
   dem translate des Elternelements zusammensetzt. Die Stuetzpunkte liegen
   bewusst weit auseinander - eine zweite Bewegung mit eigenen Nahtstellen
   wuerde den gleichmaessigen Fall wieder zerhacken. */
@keyframes introFallSquash {
  0%   { transform: scale(1, 1);
         animation-timing-function: cubic-bezier(0.3333, 0, 0.6667, 0.3333); }
  34%  { transform: scale(0.74, 1.52);
         animation-timing-function: cubic-bezier(0.5, 0, 0.75, 1); }
  40%  { transform: scale(1.4, 0.52); }
  46%  { transform: scale(1.75, 0.28); }
  100% { transform: scale(1.75, 0.28); }
}
.intro-faller { animation-name: introFall; }
.drop-fall-shape {
  animation-name: introFallSquash;
  animation-duration: calc(1700ms * var(--iz));
}

/* =========================================================
   DAS LOGO ENTSPRINGT

   Alle drei Teile starten im Aufschlagpunkt (16 px unter der Mitte der
   Buehne, dort liegt die Wasserlinie) und klein. Sie schiessen mit der
   Saeule nach oben, wachsen dabei und fliegen auf einem kurzen Bogen
   auseinander. Die Wegpunkte sind Versatzwerte RELATIV ZUR FERTIGEN
   Position - 0,0 ist also jeweils der Platz im fertigen Logo.
   ========================================================= */
@keyframes introTeilTop {
  0%   { transform: translate(3.9px, 30.4px) scale(0.25); opacity: 0;
         animation-timing-function: linear; }
  48%  { transform: translate(3.9px, 30.4px) scale(0.25); opacity: 0;
         animation-timing-function: cubic-bezier(0.13, 0.62, 0.3, 1); }
  54%  { opacity: 1; }
  88%  { transform: translate(0, 0) scale(1); opacity: 1; }
  100% { transform: translate(0, 0) scale(1); opacity: 1; }
}
@keyframes introTeilLeft {
  0%   { transform: translate(17px, 2.6px) scale(0.25); opacity: 0;
         animation-timing-function: linear; }
  48%  { transform: translate(17px, 2.6px) scale(0.25); opacity: 0;
         animation-timing-function: cubic-bezier(0.13, 0.62, 0.3, 1); }
  54%  { opacity: 1; }
  88%  { transform: translate(0, 0) scale(1); opacity: 1; }
  100% { transform: translate(0, 0) scale(1); opacity: 1; }
}
@keyframes introTeilRight {
  0%   { transform: translate(-12.5px, 5.7px) scale(0.25); opacity: 0;
         animation-timing-function: linear; }
  48%  { transform: translate(-12.5px, 5.7px) scale(0.25); opacity: 0;
         animation-timing-function: cubic-bezier(0.13, 0.62, 0.3, 1); }
  54%  { opacity: 1; }
  88%  { transform: translate(0, 0) scale(1); opacity: 1; }
  100% { transform: translate(0, 0) scale(1); opacity: 1; }
}
.intro-drop-top   { animation-name: introTeilTop; }
.intro-drop-left  { animation-name: introTeilLeft; }
.intro-drop-right { animation-name: introTeilRight; }

.intro-splash {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 0; height: 0;
  pointer-events: none;
  z-index: 2;
}
.intro-splash > * {
  position: absolute;
  opacity: 0;
  animation-fill-mode: forwards;
  animation-play-state: paused;
}

/* --- 1. flach auslaufende Aufschlagwelle --- */
.splash-pool {
  /* Die "Wasserlinie" liegt auf der Unterkante der Fallform (+16px), nicht in
     ihrer Mitte - sonst entstuende die Krone mitten im Tropfen. */
  left: 50%; top: calc(50% + 16px);
  width: 18px; height: 5px;
  margin: -2.5px 0 0 -9px;
  border-radius: 50%;
  border: 1.5px solid rgba(208, 176, 132, 0.9);
  border-top-color: rgba(208, 176, 132, 0.3);
  animation-name: splashPool;
  animation-duration: calc(880ms * var(--iz));
  animation-timing-function: cubic-bezier(0.12, 0.8, 0.3, 1);
}
@keyframes splashPool {
  0%   { transform: scale(0.3, 0.6); opacity: 0; }
  10%  { transform: scale(1, 1);     opacity: 1; }
  100% { transform: scale(9.5, 1.7); opacity: 0; }
}

/* --- 2. Krone: zwei duenne, nach aussen geneigte Wasserwaende --- */
.splash-crown {
  left: 0; top: 0; width: 0; height: 0;
  opacity: 1;
  animation: none;
}
.splash-crown > span {
  position: absolute;
  bottom: -16px;
  width: 3px; height: 21px;
  background: linear-gradient(to top, rgba(208,176,132,0.9), rgba(208,176,132,0.55) 45%, rgba(208,176,132,0));
  opacity: 0;
  transform-origin: 50% 100%;
  animation-duration: calc(620ms * var(--iz));
  animation-fill-mode: forwards;
  animation-play-state: paused;
  animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}
.crown-l {
  left: -7.5px;
  border-radius: 60% 40% 0 0 / 90% 90% 0 0;
  animation-name: crownLeft;
}
.crown-r {
  left: 4.5px;
  border-radius: 40% 60% 0 0 / 90% 90% 0 0;
  animation-name: crownRight;
}
@keyframes crownLeft {
  0%   { transform: translateX(0) scale(0.6, 0.1) skewX(0deg);      opacity: 0; }
  14%  { transform: translateX(-2px) scale(1, 1) skewX(9deg);        opacity: 0.95; }
  42%  { transform: translateX(-6px) scale(1.15, 0.66) skewX(17deg); opacity: 0.45; }
  70%  { transform: translateX(-9px) scale(1.2, 0.14) skewX(24deg);  opacity: 0; }
  100% { transform: translateX(-9px) scale(1.2, 0.08) skewX(24deg);  opacity: 0; }
}
@keyframes crownRight {
  0%   { transform: translateX(0) scale(0.6, 0.1) skewX(0deg);      opacity: 0; }
  14%  { transform: translateX(2px) scale(1, 1) skewX(-9deg);        opacity: 0.95; }
  42%  { transform: translateX(6px) scale(1.15, 0.66) skewX(-17deg); opacity: 0.45; }
  70%  { transform: translateX(9px) scale(1.2, 0.14) skewX(-24deg);  opacity: 0; }
  100% { transform: translateX(9px) scale(1.2, 0.08) skewX(-24deg);  opacity: 0; }
}

/* --- 3. Spritzer auf echter Wurfparabel ---
   Aussen (i) = gleichfoermige Horizontalbewegung, innen (b) = Vertikale mit
   Bremsen beim Steigen und Beschleunigen beim Fallen. */
.intro-splash i {
  left: 0; top: 16px; width: 0; height: 0;
  opacity: 1;
  animation-name: splashFlyX;
  animation-duration: calc(900ms * var(--iz));
  animation-timing-function: linear;
  animation-delay: calc(var(--t, 0ms) * var(--iz));
}
.intro-splash i > b {
  position: absolute;
  left: 0; top: 0;
  width: 4px; height: 5.6px;
  margin: -2.8px 0 0 -2px;
  background: var(--color-gold);
  border-radius: 50% 50% 50% 50% / 62% 62% 38% 38%;
  opacity: 0;
  animation-name: splashFlyY;
  animation-duration: calc(900ms * var(--iz));
  animation-fill-mode: forwards;
  animation-play-state: paused;
  animation-delay: calc(var(--t, 0ms) * var(--iz));
}
@keyframes splashFlyX {
  0%   { transform: translateX(0); }
  100% { transform: translateX(var(--x, 0)); }
}
@keyframes splashFlyY {
  0%   { transform: translateY(2px) scale(calc(var(--s, 1) * 0.4)); opacity: 0;
         animation-timing-function: cubic-bezier(0.1, 0.75, 0.3, 1); }
  8%   { opacity: 1; }
  40%  { transform: translateY(var(--up, -40px)) scale(var(--s, 1));
         animation-timing-function: cubic-bezier(0.5, 0, 0.85, 0.4); }
  92%  { opacity: 0.85; }
  100% { transform: translateY(var(--dn, 32px)) scale(calc(var(--s, 1) * 0.72)); opacity: 0; }
}

/* --- 4. Rayleigh-Saeule: schiesst senkrecht hoch, wenn der Krater
       zusammenfaellt --- */
.splash-jet {
  left: -1.75px; top: calc(16px - 42px);
  width: 3.5px; height: 42px;
  background: linear-gradient(to top, rgba(208,176,132,0.2), rgba(208,176,132,0.95) 40%, var(--color-gold));
  border-radius: 60% 60% 40% 40% / 24% 24% 8% 8%;
  transform-origin: 50% 100%;
  animation-name: splashJet;
  animation-duration: calc(900ms * var(--iz));
  animation-delay: calc(240ms * var(--iz));
  animation-timing-function: linear;
}
@keyframes splashJet {
  0%   { transform: scaleY(0) scaleX(1.6); opacity: 0;
         animation-timing-function: cubic-bezier(0.05, 0.85, 0.3, 1); }
  6%   { opacity: 0.95; }
  34%  { transform: scaleY(1) scaleX(0.85); opacity: 0.95;
         animation-timing-function: cubic-bezier(0.45, 0, 0.9, 0.45); }
  76%  { transform: scaleY(0.3) scaleX(1.25); opacity: 0.5; }
  100% { transform: scaleY(0) scaleX(1.6); opacity: 0; }
}

/* --- 5. abgeschnuerter Tropfen auf der Saeulenspitze --- */
.splash-bead {
  left: -3px; top: 16px;
  width: 5.5px; height: 7.5px;
  margin-top: -4px;
  background: var(--color-gold);
  border-radius: 50% 50% 50% 50% / 62% 62% 38% 38%;
  animation-name: splashBead;
  animation-duration: calc(900ms * var(--iz));
  animation-delay: calc(300ms * var(--iz));
  animation-timing-function: linear;
}
@keyframes splashBead {
  0%   { transform: translateY(0) scale(0.4); opacity: 0;
         animation-timing-function: cubic-bezier(0.05, 0.85, 0.3, 1); }
  10%  { opacity: 1; }
  32%  { transform: translateY(-52px) scale(1); opacity: 1;
         animation-timing-function: cubic-bezier(0.45, 0, 0.9, 0.45); }
  90%  { opacity: 0.7; }
  100% { transform: translateY(6px) scale(0.75); opacity: 0; }
}

#preloader.is-playing .intro-splash > *,
#preloader.is-playing .splash-crown > span,
#preloader.is-playing .intro-splash i > b { animation-play-state: running; }


/* Ein einziger Aufprall, genau dann, wenn der Tropfen ankommt:
   40 % von 1700 ms = 680 ms. Die Eigenverzoegerungen der Bauteile
   (Saeule 240, Tropfen 300, Spritzer --t) kommen jeweils dazu.
   Die drei Logoteile setzen erst bei 48 % (816 ms) ein - also erst,
   wenn Welle und Krone schon stehen. */
.intro-splash { animation-delay: calc(680ms * var(--iz)); }
.intro-splash > *,
.intro-splash .splash-crown > span { animation-delay: calc(680ms * var(--iz)); }
.intro-splash i,
.intro-splash i > b { animation-delay: calc((680ms + var(--t, 0ms)) * var(--iz)); }
.intro-splash .splash-jet  { animation-delay: calc(920ms * var(--iz)); }
.intro-splash .splash-bead { animation-delay: calc(980ms * var(--iz)); }

/* =========================================================
   TEXT: Buchstaben-für-Buchstaben-Hochschweben ("Split-Text",
   bekanntes/bewährtes Muster - siehe z.B. GSAP SplitText o.ä.
   Effekte auf vielen High-End-Websites, hier nur mit reinem
   CSS/JS nachgebaut). wrapWords() (effects.js) gruppiert den
   Text zunächst wortweise in <span class="word"> (damit ein Wort
   nicht mitten drin umbricht), und darin jeden Buchstaben in
   <span class="letter"><span class="letter-inner">X</span></span>.
   .letter clippt (overflow:hidden), .letter-inner bewegt sich per
   translateY + Opacity nach oben ins Bild, mit pro Buchstabe
   gestaffelter Verzögerung (--letter-delay wird per JS gesetzt).
   ========================================================= */
/* Feedback-Runde 6, Punkt 2 ("doppeltes Laden" - eigentliche Ursache): bevor
   JS einen Text in Buchstaben-Spans verpackt hat, stand er als ganz normaler,
   fertiger Klartext im DOM (die Versteck-Logik hängt ausschliesslich an den
   erst per JS erzeugten .letter-inner-Spans) - dadurch sah man kurz die
   fertige, unanimierte Seite, bevor JS ihn nachträglich zerlegt und die
   Animation von vorne startet. Fix: der Container selbst ist unsichtbar,
   bis JS ihn verpackt hat (siehe .is-wrapped, wird von wrapWords() in
   effects.js gesetzt, passiert jetzt sofort beim Laden statt erst nach
   Intro/Seitenübergang) - kein Klartext-Zustand mehr sichtbar möglich. */
.word-reveal { opacity: 0; }
.word-reveal.is-wrapped { opacity: 1; }
.word-reveal .word {
  display: inline-block;
  white-space: nowrap;
}
/* Runde 17: Zusammensetzungen mit Bindestrich stehen in einer Klammer
   (siehe wrapWords in effects.js). Auf dem Desktop haelt sie die Teile
   zusammen - dort war ein Umbruch nach dem Bindestrich vorher nicht
   moeglich und soll es auch nicht werden. Im Fluss gibt responsive.css
   ihn frei, damit lange Woerter nicht mitten im Wort brechen. */
.word-reveal .word-grp { white-space: nowrap; }
/* Runde 54: Trennstrich an einer weichen Trennstelle (&shy;) - erscheint nur,
   wenn die Zeile dort wirklich umbricht (effects.js, setzeTrennstriche). */
.word-reveal .letter.trenn { display: none; }
.word-reveal .letter.trenn.is-umbruch { display: inline-block; }
.word-reveal .letter {
  display: inline-block;
  overflow: hidden;
  vertical-align: top;
  padding-bottom: 0.12em; /* Platz für Unterlängen (g, j, p...) */
  margin-bottom: -0.12em;
}
.word-reveal .letter-inner {
  display: inline-block;
  transform: translateY(115%);
  opacity: 0;
  /* Feedback-Runde 5, Punkt 4: alle Texte sollen schneller animieren -
     Transition-Dauer gegenüber Runde 4 (620ms/520ms) verkürzt. */
  transition: transform 420ms cubic-bezier(0.16, 1, 0.3, 1), opacity 360ms ease;
  transition-delay: calc(var(--letter-delay, 0ms) + var(--group-delay, 0ms));
}
.word-reveal.is-visible .letter-inner {
  transform: translateY(0);
  opacity: 1;
}
/* Feedback-Runde 5, Punkt 4 (zweiter Teil): wird während eine Animation noch
   läuft weitergescrollt und der Textblock verlässt den Viewport komplett,
   sollen die restlichen, noch nicht sichtbaren Buchstaben sofort (keine
   Verzögerung, kurze Dauer) einblenden statt ihre gestaffelte Verzögerung
   abzuwarten - damit beim späteren Zurückscrollen nichts "nachträglich"
   noch eintippt und der User keinen Text verpasst. Das eigentliche
   Überschreiben passiert per Inline-Style direkt am Element (siehe
   initFastForward in effects.js) - Inline-Styles gewinnen automatisch
   gegenüber dieser (nicht "!important") Klassenregel, ganz ohne
   Spezifitäts-Kampf. */
.word-reveal.is-fastforward .letter-inner {
  transition-delay: 0ms;
  transition-duration: 160ms;
}
/* Feedback-Runde 2: mehrere word-reveal-Texte, die GLEICHZEITIG in den
   Viewport eintreten (z.B. Hero H1 + Lead-Text + Button), sollen nicht
   alle im selben Moment lostippen, sondern nacheinander - dafür addiert
   --group-delay auf die ohnehin pro Buchstabe gestaffelte Verzögerung. */
.word-reveal-delay-1 { --group-delay: 460ms; }
.word-reveal-delay-2 { --group-delay: 760ms; }

/* Einfaches Fade-up für nicht-typografische Blöcke (Bilder, Karten) */
.reveal {
  opacity: 0;
  transform: translateY(28px);
  transition: opacity 760ms cubic-bezier(0.16, 1, 0.3, 1), transform 760ms cubic-bezier(0.16, 1, 0.3, 1);
}
.reveal.is-visible { opacity: 1; transform: translateY(0); }
.reveal-delay-1.is-visible { transition-delay: 90ms; }
.reveal-delay-2.is-visible { transition-delay: 180ms; }
.reveal-delay-3.is-visible { transition-delay: 270ms; }

/* --- Bild-Hover: sanfter Zoom --- */
.media-frame { position: relative; overflow: hidden; }
.media-frame img {
  transition: transform 1100ms cubic-bezier(0.16, 1, 0.3, 1);
}
.media-frame:hover img { transform: scale(1.06); }

/* --- Link-Unterstreichung --- */
.link-underline { position: relative; padding-bottom: 2px; }
/* Runde 37, Punkt 2.4: Die Linie sass je nach Ort unterschiedlich tief unter
   der Schrift. Grund ist die Zeilenhoehe: Bei einem BLOCK liegt "bottom: 0"
   an der Unterkante der Zeilenbox, und die Zeilenbox hat unter der Schrift
   noch die halbe Durchschusshoehe Luft. Gemessen bei Schriftgrad 15:

     Zeilenhoehe 16.5 (Startseite)  0.75 Luft + 2 Polster = 2.75 px
     Zeilenhoehe 21   (Menuekarte)  3.0  Luft + 2 Polster = 5.00 px
     Zeilenhoehe 30   (Fusszeile)   7.5  Luft + 0 Polster = 7.50 px

   Die Startseite ist der angenommene Zustand, also gelten ueberall 2.75 px.
   Die Differenz wird je Ort ueber --ul-tiefer wieder hereingeholt. Das
   verschiebt nur die Linie, nichts am Textfluss. */
.link-underline::after {
  content: "";
  position: absolute; left: 0; bottom: var(--ul-tiefer, 0px);
  width: 100%; height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 420ms cubic-bezier(0.16, 1, 0.3, 1);
}
.link-underline:hover::after { transform: scaleX(1); }

/* =========================================================
   PFEIL-LINK ("mehr erfahren" / "mehr über uns erfahren" /
   "weitere Fragen und Antworten" / "Reinigungstermin online
   buchen" im Footer-CTA-Band) - Pfeil links vom Text, mit
   Bounce-Effekt beim Hover (Pfeil federt nach rechts).
   Hinweis: Die exakte Bounce-Kurve/Distanz der Live-Seite
   (https://www.3u-reinigung.ch) konnte nicht eingesehen werden
   (Sandbox-Netzwerksperre) - dies ist eine plausible Annäherung
   mit Overshoot-Easing, bitte gegen die Live-Seite vergleichen.
   ========================================================= */
.arrow-link {
  display: inline-flex;
  align-items: center;
  /* Runde 19, Punkt 7: Der Abstand zwischen Pfeil und Text lag bisher als
     Flex-"gap" vor. Flex-gap kennt Safari erst ab 14.1 - aelteren
     iPhone-Versionen fiel er ersatzlos weg, Pfeil und Text klebten
     zusammen. Jetzt traegt das Pfeil-Feld den Abstand als rechten Rand;
     das kennt jeder Browser. Geometrisch ist es derselbe Wert, die
     Desktop-Darstellung bleibt bildpunktgleich (nachgemessen). */
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 18px;
  color: inherit;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
}
/* Pfeil-Icon exakt nach Figma-Asset "Gruppe 29747" (21x10, Strichstaerke 2,
   runde Enden) - vorher 21x8 mit Strichstaerke 1.3 und damit sichtbar duenner
   als in der Vorlage. */
.arrow-link .arrow-icon {
  flex: none;
  display: inline-flex;
  width: 21px;
  height: 10px;
  margin-right: 12px;
  transition: transform 420ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.arrow-link .arrow-icon svg { display: block; width: 100%; height: 100%; }
/* Runde 20, Punkt 7 - zweite Absicherung. Sollte die Breite des Pfeil-Feldes
   je wieder auf "auto" fallen, ergibt sie sich aus der festen Hoehe und
   diesem Seitenverhaeltnis (21:10 aus dem viewBox) - und nicht mehr aus der
   Browser-Vorgabe von 300 px fuer ersetzte Elemente, an der Safari auf dem
   iPhone den Pfeil mitten in ein 300 px breites Feld gezeichnet hat.
   Wo Breite UND Hoehe gesetzt sind (Pfeil im Gold-Band, 34x13), bleibt das
   Seitenverhaeltnis wirkungslos - dort aendert sich nichts. */
.arrow-link .arrow-icon { aspect-ratio: 21 / 10; }
.arrow-link:hover .arrow-icon { transform: translateX(7px); }

/* =========================================================
   SEITENÜBERGANG – zweistufiger "Vorhang"
   1) Beige Fläche (Corporate-Farbe) wird von unten über den
      ganzen Screen hochgezogen (deckt die alte Seite zu).
   2) Neue Seite lädt darunter; danach wird der Vorhang weiter
      nach oben aus dem Bild gezogen, während der neue Content
      selbst leicht "nachgezogen" von unten einschwebt.
   ========================================================= */
#page-curtain {
  position: fixed;
  inset: 0;
  z-index: 9999;
  background: var(--color-cream);
  transform: translateY(100%);
  pointer-events: none;
}
#page-curtain.curtain-cover {
  transition: transform 620ms cubic-bezier(0.76, 0, 0.24, 1);
  transform: translateY(0%);
  pointer-events: all;
}
#page-curtain.curtain-reveal {
  transition: transform 680ms cubic-bezier(0.76, 0, 0.24, 1) 60ms;
  transform: translateY(-100%);
  pointer-events: none;
}
/* Runde 29: Beim Ende des Intros zieht der Vorhang sichtbar ueber die
   Intro-Flaeche hoch (erstes der "zwei Mal hochziehen"). Dafuer muss er
   kurz ueber dem Preloader liegen (der hat z-index 10000); initPreloader
   nimmt die Klasse danach wieder weg. */
#page-curtain.curtain-ueber-intro { z-index: 10001; }
#page-curtain.curtain-instant {
  transition: none;
  transform: translateY(0%);
}

/* Der Content selbst wird beim Seitenaufbau leicht "hochgezogen" */
body.curtain-entering main,
body.curtain-entering .site-header,
body.curtain-entering .site-footer {
  opacity: 0;
  transform: translateY(46px);
}
body.curtain-entered main,
body.curtain-entered .site-header,
body.curtain-entered .site-footer {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 760ms cubic-bezier(0.16, 1, 0.3, 1) 220ms, transform 760ms cubic-bezier(0.16, 1, 0.3, 1) 220ms;
}

/* --- Header: bleibt komplett fixiert sichtbar ---
   Feedback-Runde 2, Punkt 5: weder das Logo noch "MENU" dürfen beim
   Scrollen verschwinden oder mitscrollen - der frühere "Header blendet
   beim Runterscrollen ganz aus"-Mechanismus (.header-hidden) wurde darum
   komplett entfernt (nicht nur für MENU, jetzt auch für den Logo-Bereich).
   .site-header selbst bekommt entsprechend auch kein transform mehr - nur
   der INHALT des Logos wechselt abhängig vom Scroll-Stand zwischen
   Wortmarke und Tropfen-Icon (.is-scrolled, siehe unten), Position und
   Sichtbarkeit von Logo UND MENU bleiben davon unberührt. */
.site-header {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 500;
  transition: background 420ms ease, box-shadow 420ms ease;
}
/* Korrektur: kein weisser Hintergrund mehr beim Scrollen (Feedback) -
   der Header bleibt immer transparent. Die frühere .header-solid-Regel
   (weisser Hintergrund) wurde entfernt. */

/* =========================================================
   LOGO-WECHSEL BEIM SCROLLEN (Feedback 7.1): beim Laden erscheint
   das ausgeschriebene Wort-Logo (h=34px). Sobald der User zu scrollen
   beginnt, verwandelt es sich "effektreich" - nicht bloss ein Opacity-
   Crossfade, sondern das Wort-Logo weicht mit leichtem Hochgleiten +
   Schrumpfen zurück, während das Drei-Tropfen-Icon (jetzt korrekt
   h=55px statt vorher 34px) wie ein fallender/sich setzender Tropfen
   mit kurzer Rotation + Overshoot ("Bounce") einschwingt - visuell an
   die Intro-Tropfen-Choreographie angelehnt. Bleibt danach dauerhaft
   gewechselt (kein Zurückwechseln beim Hochscrollen), siehe initHeader()
   in effects.js, Klasse "is-scrolled".
   ========================================================= */
.nav-logo { position: relative; display: block; }
.nav-logo-main {
  display: flex; align-items: center; height: 34px;
  transform-origin: 0% 50%;
  transition: opacity 420ms ease, transform 420ms cubic-bezier(0.6, 0, 0.4, 1);
}
.nav-logo-alt {
  position: absolute; left: 0; top: 50%;
  display: flex; align-items: center; height: 55px;
  opacity: 0; visibility: hidden;
  transform: translateY(calc(-50% - 10px)) scale(0.4) rotate(-24deg);
  transform-origin: 0% 50%;
  transition: opacity 460ms ease 90ms,
              transform 620ms cubic-bezier(0.34, 1.56, 0.64, 1) 90ms,
              visibility 0s linear 620ms;
}
/* Das Haupt-Logo erst ausblenden, sobald tatsächlich ein Ersatz-Logo im
   Markup vorhanden ist (:has-Guard) - so bleibt der Header nie leer,
   falls das Wassertropfen-Icon-Asset einmal fehlen sollte. */
.site-header.is-scrolled .nav-logo-main:has(~ .nav-logo-alt img) {
  opacity: 0;
  transform: translateY(-9px) scale(0.86);
}
.site-header.is-scrolled .nav-logo-alt {
  opacity: 1; visibility: visible;
  transform: translateY(-50%) scale(1) rotate(0deg);
  transition: opacity 460ms ease, transform 620ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* =========================================================
   HAMBURGER-ICON (Figma-Asset "Gruppe 508", 34x13)
   Zwei ungleich lange Striche: oben durchgehend (1.25 -> 32.75),
   unten kuerzer und rechtsbuendig (14.25 -> 32.75), Strichstaerke 2.5,
   runde Enden. Die Farbe kommt ueber currentColor vom Button - damit
   greift automatisch dieselbe Kontrastlogik wie bei Logo und
   MENU-Label (siehe .nav-menu-btn.on-light/.on-dark).
   Hover: die beiden Striche tauschen ihre Laenge (der lange wird kurz,
   der kurze lang), verankert am rechten Ende.
   ========================================================= */
.nav-menu-btn .bars-icon line {
  stroke: currentColor;
  stroke-width: 2.5;
  stroke-linecap: round;
  transform-box: fill-box;
  transform-origin: 100% 50%;
  transition: transform 480ms cubic-bezier(0.16, 1, 0.3, 1);
}
.nav-menu-btn:hover .bars-icon .bar-a { transform: scaleX(0.587); }   /* 31.5 -> 18.5 */
.nav-menu-btn:hover .bars-icon .bar-b { transform: scaleX(1.703); }   /* 18.5 -> 31.5 */

/* Die grauen Hamburger-Linien müssen VOR (über) dem Logo/Icon liegen,
   nicht dahinter. */
.nav-menu-btn { position: relative; z-index: 2; }
.nav-logo-wrap { z-index: 1; }

/* =========================================================
   MENU-TEXTLABEL (Feedback-Runde 2, Punkt 5): "MENU" soll nach
   Scroll-Start verschwinden, analog zum Logo-Wechsel - nur das
   Hamburger-Icon bleibt stehen. Der Button selbst bewegt/verschiebt
   sich dabei NICHT (bleibt an fester Position), nur das Textlabel
   blendet aus und gibt seinen Platz frei (width/margin auf 0).
   ========================================================= */
.nav-menu-btn-label {
  display: inline-block;
  overflow: hidden;
  opacity: 1;
  max-width: 60px;
  transition: opacity 320ms ease, max-width 420ms cubic-bezier(0.6, 0, 0.4, 1), margin 420ms cubic-bezier(0.6, 0, 0.4, 1);
}
.site-header.is-scrolled .nav-menu-btn-label {
  opacity: 0;
  max-width: 0;
  margin-right: -10px;
}

.count-up { font-variant-numeric: tabular-nums; }

/* =========================================================
   NAVIGATIONSMENÜ ("menu" / "menu active ...", Figma-Frames
   2:21534, 2:25966, 2:26049, 2:26132, 2:26223)
   Öffnet mit DERSELBEN Logik wie der Seitenübergang (#page-curtain):
   eine navy-blaue Card zieht von unten über den ganzen Screen hoch.
   Anders als beim echten Seitenwechsel bleibt sie danach stehen
   (zeigt das Menü) statt weiterzuziehen; beim Schliessen läuft exakt
   dieselbe transform-Transition rückwärts (eine einzige Regel für
   beide Richtungen, keine separate "Zurück"-Animation nötig).
   ========================================================= */
.menu-card {
  position: fixed;
  inset: 0;
  z-index: 950;
  background: var(--color-navy);
  transform: translateY(100%);
  transition: transform 620ms cubic-bezier(0.76, 0, 0.24, 1);
  pointer-events: none;
  overflow: hidden;
}
.menu-card.is-open {
  transform: translateY(0%);
  pointer-events: all;
}
body.menu-open { overflow: hidden; }

/* --- Bild-Ebene (nur sichtbar, wenn Services/Über uns aktiv ist) --- */
.menu-card-media {
  position: absolute;
  top: 0; right: 0; bottom: 0;
  left: 597px;
  opacity: 0;
  transition: opacity 460ms ease;
}
.menu-card-media.is-visible { opacity: 1; }
.menu-card-media-img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
}
.menu-card-media-overlay {
  position: absolute; inset: 0;
  background: linear-gradient(to right, rgba(47, 79, 123, 0.55), rgba(47, 79, 123, 0.34));
}

/* --- Inhalts-Ebene: fixe 1440er-Referenzbreite wie der Rest der Seite
       (.home-canvas), Positionen 1:1 aus Figma übernommen. Kopf- und
       Fussreihe an der echten Viewport-Kante verankert (top/bottom),
       damit die Card auf jeder Bildschirmhöhe als echtes Vollbild-Overlay
       funktioniert, nicht nur bei der 798px-Referenzhöhe. --- */
.menu-card-canvas {
  position: relative;
  width: 1440px;
  max-width: 100%;
  height: 100%;
  margin: 0 auto;
}
/* Runde 38, Punkt 4.1: "Logo so gross wie auf Header und gleiche
   Position." Die Kopfzeile setzt das Logo auf x34/y34 (site.css: Rand 34,
   Innenabstand 34) und 34 px hoch. Die Menuekarte stand bis v51 auf
   x55/y57 und 21 px hoch. Jetzt dieselben Werte - die Karte ist 1440 breit
   und mittig, der Bezug ist also derselbe wie in der Kopfzeile. */
.menu-card-top {
  position: absolute;
  top: 34px; left: 34px; right: 34px;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
}
.menu-card-logo { display: flex; align-items: center; gap: 16px; }
.menu-card-logo img { display: block; height: 34px; width: auto; filter: brightness(0) invert(1); }
.menu-card-tagline {
  font-family: var(--font-body); font-size: 11px; color: var(--color-white);
}
.menu-card-back {
  display: flex; align-items: center; gap: 12px;
  background: none; border: none; cursor: pointer;
  color: var(--color-white);
}
.menu-card-back-label {
  font-family: var(--font-body); font-size: 13px; letter-spacing: 0.216em; text-transform: uppercase;
}
/* Runde 11: Kreuz statt Pfeil (wie in den Erklaerungsfenstern des
   Preiskalkulators). Beim Ueberfahren dreht es sich eine Vierteldrehung
   und wird golden - dieselbe Sprache wie beim Hamburger-Icon. */
.menu-card-back-icon { display: inline-flex; width: 21px; height: 21px; }
.menu-card-back-icon svg {
  display: block; width: 21px; height: 21px;
  transition: transform 420ms cubic-bezier(0.16, 1, 0.3, 1);
}
.menu-card-back:hover .menu-card-back-icon svg { transform: rotate(90deg); }
.menu-card-back:hover { color: var(--color-gold); }
.menu-card-back { transition: color 260ms ease; }

/* Runde 24: "Menueleiste/-auflistungen (links) eher zentrieren zwischen Logo
   oben und Impressum & Datenschutzerklaerung (zu weit nach unten)."
   Statt einer festen Figma-Koordinate (top: 237px) spannt die Liste jetzt das
   Band zwischen den beiden Zeilen auf und legt sich mittig hinein:
     oben   55 (Abstand) + 23 (Hoehe der Logozeile)   = 78
     unten  55 (Abstand) + 15 (Hoehe der Impressumzeile) = 70
   Auf 1440x760 ruecken die Eintraege damit von 237 auf 159 hoch; bei 900 px
   Fensterhoehe bleibt es mit 229 praktisch bei der Vorlage. */
.menu-card-nav {
  position: absolute;
  top: 78px; bottom: 70px; left: 144px;
  display: flex;
  align-items: center;
}
.menu-card-nav ul { display: flex; flex-direction: column; list-style: none; margin: 0; padding: 0; }
.menu-card-nav li { height: 75px; position: relative; }
.menu-nav-link {
  display: block;
  font-family: var(--font-headline); font-size: 34px; line-height: 34px;
  color: var(--color-white);
  transition: color 320ms ease;
  letter-spacing: var(--font-headline-tracking);
}
.menu-nav-item.is-active .menu-nav-link,
.menu-nav-link:hover { color: var(--color-gold); }

/* --- Untermenü (Services -> Residential/Office/Commercial,
       Über uns -> Uns kennenlernen/Preisliste/AGB) - fest an derselben
       X-Position wie die Bild-Ebene, blendet zusammen mit ihr ein. --- */
.menu-sub-divider {
  position: absolute;
  left: 233px; top: 17px;
  width: 309px; height: 1px;
  background: rgba(255, 255, 255, 0.3);
  opacity: 0;
  transition: opacity 420ms ease;
}
.menu-nav-item.is-active .menu-sub-divider { opacity: 1; }
.menu-sub {
  position: absolute;
  left: 576px; top: 0;
  width: 377px;
  list-style: none; margin: 0; padding: 0;
  opacity: 0;
  visibility: hidden;
  transition: opacity 420ms ease, visibility 0s linear 420ms;
}
.menu-nav-item.is-active .menu-sub {
  opacity: 1;
  visibility: visible;
  transition: opacity 420ms ease;
}
.menu-sub li { height: 62px; }
.menu-sub-link {
  display: block;
  font-family: var(--font-headline); font-size: 34px; line-height: 34px;
  color: var(--color-white);
  transition: color 280ms ease;
  letter-spacing: var(--font-headline-tracking);
}
.menu-sub-link.is-active,
.menu-sub-link:hover,
.menu-sub-link:focus-visible { color: var(--color-gold); }

/* --- Kontakt-/CTA-Block rechts (nur im Grundzustand sichtbar, weicht
       Bild + Untermenü, sobald Services/Über uns aktiv ist) --- */
/* Runde 31: Die rechte Haelfte stand auf einem FESTEN top (321), die
   Navigation links dagegen mittig zwischen 78 und (Hoehe - 70). Auf einem
   900er Fenster fiel das kaum auf, auf einem 800er sass die rechte Seite
   56 px unter der Mitte, auf einem 1080er 88 px darueber. Jetzt rechnet
   sie mit derselben Formel wie die Navigation: dasselbe Band (78 oben,
   70 unten), der sichtbare Block (nachgemessen 277 px hoch:
   zwei Spalten, Luft, Knopf) mittig darin. Beide Haelften haben damit auf JEDER Fensterhoehe
   dieselbe Mittellinie. */
.menu-card-contact {
  position: absolute;
  top: calc(78px + (100% - 148px - 277px) / 2);
  left: 773px;
  display: flex;
  gap: 144px;
  opacity: 1; visibility: visible;
  transition: opacity 320ms ease, visibility 0s linear 0s;
}
.menu-card-contact h4 {
  font-family: var(--font-headline); font-weight: 400; font-size: 21px; line-height: 26px;
  color: var(--color-white); margin: 0 0 6px;
  letter-spacing: var(--font-headline-tracking);
}
/* Runde 11: "rund um die Uhr" sass tiefer als "info@3u-reinigung.ch" - das
   <p> brachte seinen Standard-Aussenabstand mit, das <a> als Inline-Element
   nicht. Beide jetzt als Block ohne Aussenabstand mit gleicher Zeilenhoehe. */
.menu-card-contact p,
.menu-card-contact a {
  display: block; margin: 0;
  font-family: var(--font-body); font-weight: 300;
  font-size: 15px; line-height: 21px;
  color: var(--color-white); white-space: nowrap;
}
.menu-card-contact-col { width: 162px; }
.menu-card-contact-col > div + div { margin-top: 24px; }
.menu-card-contact .footer-social { margin-top: 24px; }
/* Feedback-Runde 5, Punkt 3: Icons im Menü zuvor beige/Hover weiss - Wunsch
   war genau umgekehrt: Grundzustand weiss, bei Hover beige. */
.menu-card-contact .footer-social a { color: var(--color-white); }
.menu-card-contact .footer-social a:hover { color: var(--color-gold); }
/* Feedback-Runde 3: Text war nicht vertikal mittig, weil der Button bei
   zwei Textzeilen ("Reinigungstermin online buchen" bricht in der 377px-
   Breite um) nur über das geerbte .btn-Padding zentriert wurde - dadurch
   lief der Übersatz einseitig nach unten aus dem Button heraus. Jetzt
   echtes Flexbox-Centering (funktioniert unabhängig von 1 oder 2 Zeilen).
   Feedback-Runde 4, Punkt 7: Farbe doch wieder weiss als Grundzustand
   (wie ursprünglich), Hover füllt jetzt Beige ein (vorher umgekehrt) -
   1:1 derselbe Mechanismus wie beim Homebildschirm-Button "Reinigungs-
   offerte online einholen" (.home-hero .btn-primary, home.css), nur mit
   vertauschten Farben (dort weiss auf navy-Basis, hier navy auf weiss-
   Basis). Grösse/Gewicht/Letter-Spacing des Texts jetzt ebenfalls 1:1
   wie beim Vorbild-Button übernommen (15px/500/normal statt der vorher
   eigens getunten 18px) - "Text nichts verändern" heisst hier: keine
   eigene Grösse/Stärke mehr, exakt wie das Vorbild. */
.menu-card-cta {
  position: absolute;
  top: 214px; left: 0;
  width: 377px; height: 55px;
  display: flex; align-items: center; justify-content: center; text-align: center;
  overflow: hidden;
  background: var(--color-white);
  border: none; border-radius: 3px; cursor: pointer;
  font-family: var(--font-body); font-size: 15px; font-weight: 500; line-height: 1.25; color: var(--color-navy);
  letter-spacing: normal;
}
.menu-card-cta .btn-fill {
  position: absolute; inset: 0; z-index: 0;
  background: var(--color-gold);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 420ms cubic-bezier(0.65, 0, 0.35, 1);
}
.menu-card-cta .btn-label { position: relative; z-index: 1; }
.menu-card-cta:hover .btn-fill { transform: scaleX(1); }
/* Die generische .btn:hover-Regel (base.css) würde sonst Hintergrund auf
   navy, Text auf weiss und Letter-Spacing auf 0.06em umfärben/verändern -
   hier deshalb wie beim Vorbild-Button explizit neutralisiert/fixiert,
   damit sich am Text beim Hover wirklich GARNICHTS verändert. */
.menu-card-cta:hover { background: var(--color-white); letter-spacing: normal; }
/* Der Button ist seit dem Umbau auf echte Seitenlinks ein <a> und lag damit
   im Wirkungsbereich der Regel ".menu-card-contact a { color: #fff }" -
   dadurch stand weisse Schrift auf weissem Grund und wurde erst beim Hover
   sichtbar. Schriftfarbe und -stärke hier deshalb ausdrücklich und mit
   ausreichender Spezifität festgelegt: immer navyblau, auch im Hover. */
.menu-card-contact a.menu-card-cta,
.menu-card-contact a.menu-card-cta:hover,
.menu-card-cta .btn-label,
.menu-card-cta:hover .btn-label { color: var(--color-navy); font-weight: 500; }
.menu-card:has(.menu-nav-item.is-active) .menu-card-contact {
  opacity: 0; visibility: hidden;
  transition: opacity 260ms ease, visibility 0s linear 260ms;
}

.menu-card-bottom {
  position: absolute;
  left: 55px; right: 47px; bottom: 55px;
  display: flex; justify-content: space-between; align-items: center;
}
.menu-card-legal {
  font-family: var(--font-body); font-size: 13px; letter-spacing: 0.028em; color: var(--color-white);
}
.menu-card-legal:hover { color: var(--color-gold); }

/* --- Inhalts-Reveal: Menü-Links nutzen .word-reveal (Buchstaben-Einflug,
       siehe initWordReveal in effects.js), Kontaktblock/CTA/Social-Icons
       nutzen das bereits bestehende .reveal-Fade-up-System - beide werden
       automatisch von ihren jeweiligen IntersectionObservern ausgelöst,
       sobald die Card ins Bild fährt (kein separates Menü-spezifisches
       Reveal-System nötig). --- */

@media (max-width: 1480px) {
  .menu-card-canvas { width: 100%; }
}
@media (max-width: 960px) {
  .menu-card { overflow-y: auto; }
  .menu-card-media { display: none; }
  .menu-card-top { position: static; padding: 24px; }
  .menu-card-nav { position: static; margin-top: 24px; padding: 0 24px; }
  .menu-card-contact { position: static; margin: 48px 24px 0; flex-wrap: wrap; gap: 32px; }
  .menu-card-cta { position: static; margin: 24px 24px 0; }
  .menu-card-bottom { position: static; margin: 32px 24px 24px; }
  .menu-sub { position: static; margin-top: 12px; }
  .menu-sub-divider { display: none; }
}

/* Einstellung "Bewegung reduzieren" (iPhone/iPad, Mac, Android
   "Animationen entfernen", Windows "Animationseffekte" aus).
   v55 neu geordnet: Bisher setzte dieser Block auch Vorhang, Menue und Logos
   auf "transform:none; opacity:1". Deren RUHELAGE ist aber eine
   Verschiebung aus dem Bild (Vorhang, Menue) bzw. unsichtbar (zweites Logo,
   Trennlinien). Folge: Der cremefarbene Vorhang lag ueber JEDER Seite - mit
   eingeschalteter Einstellung war die ganze Website leer.
   Jetzt: Einblendungen stehen sofort im Endzustand; was einen Zustand hat
   (Menue, Logo, Untermenue), behaelt ihn und wechselt nur ohne Bewegung; den
   Vorhang gibt es nicht (Intro und Seitenwechsel ohne Ueberblendung, siehe
   bewegungReduziert() in effects.js). */
@media (prefers-reduced-motion: reduce) {
  .reveal, .media-frame img,
  .word-reveal .letter-inner,
  body.curtain-entering main, body.curtain-entering .site-header, body.curtain-entering .site-footer,
  body.curtain-entered main, body.curtain-entered .site-header, body.curtain-entered .site-footer {
    transition: none !important;
    animation: none !important;
    transform: none !important;
    opacity: 1 !important;
  }
  .site-header, .nav-logo-wrap, .nav-logo-main, .nav-logo-alt,
  .menu-card, .menu-card-media, .menu-sub, .menu-sub-divider {
    transition: none !important;
    animation: none !important;
  }
  #page-curtain { display: none !important; }
}


/* =========================================================
   KONTRAST-AUTOMATIK (Runde 8, Punkt 1.2)
   Logo, MENU-Label und Hamburger sind goldfarben. Treffen sie beim Scrollen
   auf einen Hintergrund in derselben oder einer sehr ähnlichen Farbe (creme,
   gold, Wellenmuster), sind sie kaum noch erkennbar. Statt das pro Seite
   hart einzustellen, misst initContrastGuard() in effects.js die tatsächliche
   Hintergrundfarbe unter Logo und MENU-Button und setzt eine dieser Klassen -
   das funktioniert dadurch automatisch auch auf allen noch folgenden Seiten.
   Dasselbe gilt für den Tropfen-Mauszeiger.
   ========================================================= */
.nav-logo.on-light img,
.nav-menu-btn.on-light { color: var(--color-navy); }
.nav-logo.on-light img { filter: brightness(0) saturate(100%) invert(27%) sepia(19%) saturate(1580%) hue-rotate(180deg) brightness(94%) contrast(88%); }
.nav-logo.on-dark img { filter: brightness(0) invert(1); }
.nav-menu-btn.on-dark { color: var(--color-white); }
.nav-logo img { transition: filter 320ms ease; }
.nav-menu-btn { transition: color 320ms ease; }

/* =========================================================
   MENÜ-AKTIVZUSTÄNDE JE SEITE
   Die Figma-Frames "menu active services residential/office/commercial"
   und "menu active über uns" unterscheiden sich vom Grundzustand nur
   dadurch, dass der Elternpunkt gold ist, sein Untermenue offen steht,
   das zugehoerige Foto rechts erscheint, der Kontaktblock verschwindet und
   der aktive Unterpunkt ebenfalls gold ist. Genau diesen Zustand stellt
   sonst der Hover her - deshalb setzt applyMenuPageState() in effects.js
   beim Laden einfach dieselben .is-active-Klassen anhand der von build.py
   gesetzten Klasse .is-page-<schluessel>. Hier braucht es darum nur noch
   die Markierung des Elternpunkts bei Seiten OHNE Untermenue.
   ========================================================= */
/* Runde 31: Jobs ist kein Hauptpunkt mehr, sondern ein Unterpunkt von
   "Ueber uns" - seinen Aktivzustand stellt darum applyMenuPageState() her
   (siehe PAGE_STATE in effects.js), nicht mehr diese Regel. */
/* Runde 55: die Links zeigen jetzt auf den Ordner (".../kontakt/"), nicht
   mehr auf ".../kontakt/index.html" - beide Schreibweisen gelten. */
.menu-card.is-page-preiskalkulator .menu-nav-link[href$="preiskalkulator/"],
.menu-card.is-page-ratgeber .menu-nav-link[href$="ratgeber/"],
.menu-card.is-page-kontakt .menu-nav-link[href$="kontakt/"],
.menu-card.is-page-preiskalkulator .menu-nav-link[href$="preiskalkulator/index.html"],
.menu-card.is-page-ratgeber .menu-nav-link[href$="ratgeber/index.html"],
.menu-card.is-page-kontakt .menu-nav-link[href$="kontakt/index.html"] {
  color: var(--color-gold);
}


/* =========================================================
   37 · Hover-Linie: gleicher Abstand zur Schrift, gleiche Laenge
   ========================================================= */
/* Fusszeile: Zeilenhoehe 30, kein Polster -> 7.5 statt 2.75, also 4.75 hoch */
.site-footer .link-underline { --ul-tiefer: 4.75px; }
/* Menuekarte: Zeilenhoehe 21, 2 px Polster -> 5.0, also 2.25 hoch */
.menu-card .link-underline { --ul-tiefer: 2.25px; }
/* Runde 54: Hier stand bis v53 ein Versatz von 6.75 px fuer die Ortsliste,
   den Fliesstext und die Ratgeber-Artikel ("Zeilenhoehe 30 -> 9.5"). Das war
   falsch gerechnet: Diese Links sind INLINE wie die Listen der Startseite,
   und ein Inline-Kasten ist so hoch wie die Schrift selbst (gemessen 21 px)
   - die Zeilenhoehe 30 legt ihre Luft AUSSERHALB des Kastens an. Die Linie
   wurde so um 6.75 px in die Schrift hineingezogen (gemessen: 1.75 px UEBER
   der Grundlinie statt 5 px darunter wie auf der Startseite). Ohne Versatz
   sitzen sie jetzt genau wie die Startseite - gleicher Aufbau, gleiches
   Ergebnis bei jeder Schrift. Nur BLOCK-Links (Fusszeile, Menuekarte)
   brauchen einen Versatz, weil dort die Zeilenhoehe den Kasten bestimmt. */

/* Punkt 4.3: In der Menuekarte sind E-Mail und Telefonnummer Bloecke und
   fuellen darum die ganze Spalte - die Linie lief bis zu 452 px ueber den
   Text hinaus (gemessen bei 768 px Fensterbreite). Mit fit-content ist der
   Kasten so breit wie die Schrift, und damit auch die Linie. */
.menu-card-contact a.link-underline,
.menu-card-contact a[href^="tel:"],
.menu-card-contact a[href^="mailto:"] { width: fit-content; }

/* Punkt 4.2: Der Text im Knopf "Reinigungstermin online buchen" sass 3 px zu
   hoch - gemessen 14 px Luft oben, 20 px unten. Ursache: Der Knopf liegt in
   ".menu-card-contact", und dort gilt "a { display: block }" mit zwei
   Klassen Spezifitaet. Damit verlor ".menu-card-cta { display: flex }" mit
   nur einer Klasse, und statt der Flexbox-Mitte zaehlte wieder das
   Innenpolster von ".btn" (14 px oben, der Rest unten). Hier derselbe
   Selektor mit zwei Klassen - damit greift die Mitte wieder. */
.menu-card-contact a.menu-card-cta,
.menu-card a.menu-card-cta,
button.menu-card-cta {
  display: flex; align-items: center; justify-content: center;
  padding-top: 0; padding-bottom: 0;
}
