/* ==========================================================================
   Gama Reinigungsservice — Startseite (index.html), Kopf/Farb-Abweichungen
   Nur die Abweichungen gegenüber app.css. Reihenfolge im HTML:
     style.css → app.css → app4.css
   Alles andere (Tabs oben, Punkte unten, Slide-Übergang, Notventil,
   Höhen-Budget) bleibt unverändert aus app.css.

   Änderungen: Kopfzeile ohne Telefonnummer, Angebots-Button und Rechtslinks,
   dafür Logo mittig mit Claim rechts daneben — plus ein fester Footer unter
   der Punkt-Navigation.
   ========================================================================== */

/* --------------------------------------------------------------------------
   0. Shell: fünf Reihen statt vier
   app.css definiert: Kopf | Tabs | Bühne | Punkte.
   Variante 4 ergänzt den Footer als fünfte Reihe. Die Bühne bleibt die
   einzige dehnbare Reihe (minmax(0,1fr)), alle Leisten sind auto.
   -------------------------------------------------------------------------- */
html.app {
  /* Exakte Footer-Höhe als Variable — der WhatsApp-Float (liegt AUSSERHALB
     der App-Shell, als Geschwister von #app) braucht sie, um nicht auf dem
     Footer zu landen. Muss mit .app-footer .footer__bottom min-height
     weiter unten synchron bleiben. */
  --app-footer-h: 2.5rem;
}
@media (max-height: 640px) {
  html.app { --app-footer-h: 2rem; }
}
html.app .app {
  grid-template-rows: auto auto minmax(0, 1fr) auto auto;
}

/* --------------------------------------------------------------------------
   0b. Akzentfarbe: Gold statt Orange
   Die Seite ist durch Navy + Himmelblau bereits sehr blau. Das bisherige
   Orange (#c2410c) liegt nah an Rot und wirkte eher warnend als einladend.
   Gold ist die Gegenfarbe zu Blau (sticht also ohne Aggressivität heraus),
   passt zu „strahlender Sauberkeit", den Sternen im Logo und den Funken im
   Kopf — und beißt sich nicht mit dem WhatsApp-Grün daneben.

   Umgesetzt über die vorhandenen --cta-*-Tokens aus style.css, damit alle
   Stellen automatisch mitziehen (Buttons, Kontakt-Tab, Pflicht-Sternchen,
   Formular-Aufklapper). Gilt überall, wo app4.css geladen ist: index.html,
   impressum.html, datenschutz.html.

   Zwei Rollen, zwei Töne:
     - Fläche (Buttons): helles Gold #f5b700 mit NAVY-Schrift (8,45:1),
       Hover #e3a800 (7,15:1). Weiß auf Gold wäre unlesbar.
     - Schrift/Symbol auf Weiß (Kontakt-Tab, Sternchen): dunkles Gold
       #8a6500 (5,33:1), Hover #6f5100 (7,36:1) — beides WCAG AA.
   -------------------------------------------------------------------------- */
:root {
  --gold-500: #f5b700;
  --gold-600: #e3a800;
  --gold-700: #8a6500;
  --gold-800: #6f5100;
  --gold-100: #fff6d6;

  --cta-400: var(--gold-500);
  --cta-500: var(--gold-500);
  --cta-600: var(--gold-700);   /* Schrift/Symbol auf Weiß */
  --cta-700: var(--gold-800);
  --cta-800: var(--gold-800);
  --cta-100: var(--gold-100);   /* helle Fläche hinter Symbolen */
  --sh-cta: 0 6px 16px -4px rgba(160, 112, 0, 0.42);
}
/* Buttons: helles Gold als Fläche, Navy als Schrift (style.css setzt hier
   --cta-600 + Weiß, das wäre mit dem dunklen Gold zu trüb). */
.btn--cta { --btn-bg: var(--gold-500); --btn-fg: var(--navy-900); }
.btn--cta:hover { --btn-bg: var(--gold-600); --btn-fg: var(--navy-900); }
/* Aktiv-Balken unter dem Kontakt-Tab in hellem Gold wie die Buttons —
   dunkles Gold wirkte als Fläche bräunlich. */
.app-tab[aria-selected='true'].app-tab--cta::before { background: var(--gold-500); }

/* --------------------------------------------------------------------------
   1. Kopfzeile: Logo-Gruppe echt mittig
   Die Rechtslinks liegen absolut positioniert, damit sie die Zentrierung
   der Logo-Gruppe nicht verschieben. Ein Flex-Spacer würde je nach
   Textbreite der Links die Mitte verziehen.
   -------------------------------------------------------------------------- */
.app-head--centered {
  position: relative;
  justify-content: center;
  /* rechts Platz für die Rechtslinks freihalten, damit lange Claims nicht
     darunterlaufen; links derselbe Betrag, damit die Mitte echt die Mitte ist */
  padding-inline: var(--app-pad-x);
  /* Dunkler Marken-Hintergrund statt des halbtransparenten Weiß aus app.css —
     Blur wird hinfällig, weil der Grund jetzt deckend ist. */
  background: var(--navy-900);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

/* Dezenter „Funkenglanz": einzelne Glitzer-Funken, die an ZUFÄLLIGEN Stellen
   des dunklen Kopfes kurz aufblitzen und wieder verschwinden.

   Warum echte Elemente statt Pseudo-Element-Gradients (Vorgängerversion):
   Ein radial-gradient ist immer ein runder Fleck — als „Funke" wirkt das wie
   ein Staubkorn. Ein Funke braucht einen STERN-Umriss (heller Kern plus zwei
   gekreuzte Lichtstrahlen). Das lässt sich pro Funke nur mit eigenen
   Elementen bauen, und nur so ist auch eine zufällige Position je Durchlauf
   möglich (CSS allein kann nicht würfeln).

   Die Funken-Elemente erzeugt app.js (Abschnitt „Funkenglanz") und setzt pro
   Durchlauf Position, Größe und Dauer neu. Fällt JS aus, fehlt lediglich der
   Effekt — der Kopf bleibt vollständig intakt.

   Aufbau eines Funkens:
     .app-spark          Träger, positioniert; skaliert/dreht sich beim Blitzen
     .app-spark::before  waagerechter Lichtstrahl
     .app-spark::after   senkrechter Lichtstrahl
   Kern und Streulicht kommen aus dem Hintergrund des Trägers selbst. Die
   Strahlen sind an den Enden spitz (Gradient läuft nach transparent aus),
   dadurch entsteht die typische Vier-Zack-Form. */
.app-sparks {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  /* unter der Logo-Zeile (die hat z-index: 1) */
  z-index: 0;
}
.app-spark {
  position: absolute;
  width: var(--spark-size, 14px);
  aspect-ratio: 1;
  /* Kern mit weichem Streulicht — klein gehalten, die Strahlen machen die
     eigentliche Funken-Silhouette. */
  background: radial-gradient(circle,
              #fff 0%,
              rgba(255, 255, 255, 0.75) 18%,
              rgba(255, 255, 255, 0.12) 38%,
              transparent 62%);
  opacity: 0;
  /* Startzustand: eingedreht und klein — die Animation dreht leicht auf,
     das lässt den Funken „aufblühen" statt bloß aufzuleuchten. */
  transform: rotate(var(--spark-rot, 0deg)) scale(0.2);
  animation: app-spark-flash var(--spark-dur, 1600ms) ease-in-out forwards;
  will-change: opacity, transform;
}
.app-spark::before,
.app-spark::after {
  content: '';
  position: absolute;
  /* Strahlen sitzen mittig und laufen nach beiden Seiten spitz aus */
  top: 50%;
  left: 50%;
  translate: -50% -50%;
  background: linear-gradient(to right,
              transparent 0%,
              rgba(255, 255, 255, 0.9) 50%,
              transparent 100%);
}
.app-spark::before {
  width: 100%;
  height: 1px;
}
.app-spark::after {
  /* derselbe Verlauf, nur um 90° gedreht: schmaler senkrechter Strahl */
  width: 1px;
  height: 100%;
  background: linear-gradient(to bottom,
              transparent 0%,
              rgba(255, 255, 255, 0.9) 50%,
              transparent 100%);
}

/* Aufblitzen und Ausklingen. Der Funke erreicht seine volle Größe schon
   früh, hält die Spitze kurz und klingt dann langsamer aus — das wirkt wie
   ein echtes Glitzern und nicht wie ein gleichmäßiges Pulsieren. Das
   Weiterdrehen (+22°) ist minimal, gibt dem Aufblitzen aber Leben. */
@keyframes app-spark-flash {
  0% {
    opacity: 0;
    transform: rotate(var(--spark-rot, 0deg)) scale(0.2);
  }
  22% {
    opacity: 1;
    transform: rotate(calc(var(--spark-rot, 0deg) + 8deg)) scale(1);
  }
  45% {
    opacity: 0.55;
    transform: rotate(calc(var(--spark-rot, 0deg) + 14deg)) scale(0.92);
  }
  62% {
    opacity: 0.9;
    transform: rotate(calc(var(--spark-rot, 0deg) + 18deg)) scale(1);
  }
  100% {
    opacity: 0;
    transform: rotate(calc(var(--spark-rot, 0deg) + 22deg)) scale(0.3);
  }
}

@media (prefers-reduced-motion: reduce) {
  /* Kein Glitzern bei reduzierter Bewegung. app.js erzeugt dann ohnehin
     keine Funken (dieselbe Abfrage dort), das hier ist das Sicherheitsnetz. */
  .app-sparks { display: none; }
}

.app-brandline {
  display: flex;
  align-items: center;
  gap: clamp(0.5rem, 0.3rem + 1vw, 1.25rem);
  min-width: 0;              /* erlaubt dem Claim zu schrumpfen statt zu überlaufen */
  max-width: 100%;
  /* über den Funken (::before/::after des Headers) bleiben */
  position: relative;
  z-index: 1;
}

/* Logo deutlich größer als in Variante 3 (dort clamp(96px … 140px)).
   Wichtig: Das Logo hat ein Seitenverhältnis von 700:372 (~1,88). Wird die
   Höhe stärker begrenzt als die Breite es zulässt, skaliert `object-fit:
   contain` das Bild auf die Höhe herunter und der Rest der Box bleibt leer —
   das Logo wirkt dann klein, obwohl `width` groß ist. Deshalb wird hier die
   Höhe passend zur Breite mitgeführt statt hart abgeschnitten. */
.app-head--centered .brand {
  margin-right: 0;           /* app.css setzt hier margin-right:auto */
  flex: none;
  display: block;
  line-height: 0;            /* kein Zeilenabstand unter dem Bild */
}
.app-head--centered .brand img {
  /* Höhe ist hier die führende Größe: Sie bestimmt, wie viel vom Viewport
     der Kopf frisst, und der Rest muss für die scrollfreie Bühne reichen.
     Die Breite ergibt sich aus dem Verhältnis 700:372 (~1,88) — bei 76px
     Höhe also ~143px Breite, sichtbar mehr als in Variante 3 (dort ~98px
     effektiv, weil dort die Höhenbremse das Bild herunterskalierte). */
  height: clamp(52px, 34px + 3.4vh, 76px);
  width: auto;
  max-width: 100%;
  max-height: none;
}

/* Claim rechts neben dem Logo */
.app-claim {
  margin: 0;
  min-width: 0;
  font-family: 'Poppins', sans-serif;
  font-size: clamp(0.6875rem, 0.55rem + 0.55vw, 0.9375rem);
  font-weight: 500;
  line-height: 1.25;
  color: #fff;
  text-wrap: balance;
  /* optische Trennung zum Logo, greift nur wenn nebeneinander */
  padding-left: clamp(0.5rem, 0.3rem + 1vw, 1.25rem);
  border-left: 1px solid #fff;
}

/* Rechtslinks: in Variante 4 nicht im Kopf, sondern im Footer unter der
   Punkt-Navigation (siehe Abschnitt 3). Falls die Regel aus app.css noch
   greift, hier hart abschalten — sonst blitzten sie unter 560px doppelt auf. */
.app-head--centered .app-minilegal { display: none !important; }

/* Die Punkte-Leiste ist in Variante 4 nicht mehr das unterste Element —
   Safe-Area und Schatten nach unten wandern an den Footer. */
html.app .app-dots {
  height: var(--app-dots-h);
  padding-bottom: 0;
  box-shadow: none;
  border-bottom: 1px solid var(--line);
}

/* --------------------------------------------------------------------------
   3. Footer (statisch, unter der Punkt-Navigation)
   Dunkler Grund (--navy-900), Copyright links, Linkliste rechts. Basis aus
   style.css (.footer / .footer__bottom); hier nur die kompakte Variante für
   die scrollfreie Shell, weil jeder Pixel hier der Bühne fehlt.
   Derselbe Footer steht auch auf impressum.html und datenschutz.html (die
   dafür app4.css mitladen) — dort gibt es kein html.app, deshalb der
   Standardwert bei --app-footer-h.
   -------------------------------------------------------------------------- */
.app-footer {
  /* .footer aus style.css bringt padding-block: var(--sp-7) var(--sp-5) mit —
     das sind ~5rem und viel zu viel für eine fixe Fußzeile. */
  padding-block: 0;
  padding-bottom: env(safe-area-inset-bottom, 0px);
  z-index: var(--z-header);
  /* Schrift durchgehend Weiß statt der abgedunkelten Werte aus style.css */
  color: #fff;
}
.app-footer .footer__bottom {
  /* die Trennlinie und der große Abstand nach oben entfallen: der Footer
     grenzt hier direkt an die Punkt-Navigation, nicht an Fließtext */
  margin-top: 0;
  padding-top: 0;
  border-top: none;
  min-height: var(--app-footer-h, 2.5rem);
  gap: 0.25rem 1.5rem;
  /* --fs-xs (13px) wie auf impressum.html/datenschutz.html, damit die
     Fußzeile über alle Seiten identisch wirkt. Nur auf sehr flachen
     Fenstern (siehe unten) wird sie zurückgenommen. */
  font-size: var(--fs-xs);
  color: #fff;
}
.app-footer .footer__bottom p { margin: 0; }
.app-footer .footer__bottom a {
  /* Touch-Zielgröße ohne die Zeile optisch aufzublähen */
  display: inline-flex;
  align-items: center;
  min-height: 32px;
  color: #fff;
}
.app-footer .footer__bottom a:hover { color: #fff; }

/* Auf schmalen Geräten kann die Zeile umbrechen — dann mittig statt
   auseinandergezogen, das wirkt sonst wie ein Fehler. */
@media (max-width: 519px) {
  .app-footer .footer__bottom {
    justify-content: center;
    text-align: center;
    gap: 0 0.875rem;
    font-size: 0.6875rem;
  }
  .app-footer .footer__bottom ul { gap: 0 0.875rem; }
  .app-footer .footer__bottom a { min-height: 26px; }
  /* „Gama Reinigungsservice." ausschreiben kostet hier eine ganze Zeile —
     das Kürzel genügt, der volle Name steht im Logo direkt darüber. */
  .app-footer .footer__bottom .app-footer__long { display: none; }
}

@media (max-height: 640px) {
  .app-footer .footer__bottom {
    min-height: 2rem;
    font-size: 0.6875rem;
  }
  .app-footer .footer__bottom a { min-height: 26px; }
}


/* --------------------------------------------------------------------------
   2. Enge Viewports
   Unter ~620px reicht die Breite nicht für Logo + Claim + Rechtslinks in
   einer Zeile. Dann entfällt der Claim: Er ist eine Wiederholung der
   Hero-Überschrift in der Start-Ansicht, also kein Informationsverlust.
   Alternative wäre ein Umbruch in zwei Zeilen — das würde aber die
   Kopfhöhe erhöhen und das scrollfreie Höhen-Budget sprengen.
   -------------------------------------------------------------------------- */
@media (max-width: 619px) {
  .app-claim { display: none; }

  /* Ohne Claim UND ohne Rechtslinks im Kopf steht die volle Breite zur
     Verfügung — das Logo darf hier deutlich größer werden als in Variante 3. */
  .app-head--centered .brand img {
    height: clamp(46px, 28px + 3.4vh, 64px);
    max-width: calc(100vw - 3rem);
  }
  /* Innenabstand knapper: Jeder Pixel im Kopf fehlt der scrollfreien Bühne,
     und der Kontakt-View ist der engste. */
  .app-head--centered { padding-block: 0.375rem; }
}

/* Sehr flache Fenster: Logo zurücknehmen, damit die Bühne Platz behält.
   Hier geht es nicht um Optik, sondern darum, dass der Inhalt überhaupt
   in den Viewport passt. */
@media (max-height: 640px) {
  .app-head--centered .brand img { height: 42px; }
}

/* --------------------------------------------------------------------------
   3. Leistungen: Fotos statt Icons
   Die drei Kern-Leistungen erscheinen mit echten Fotos statt schlichter
   Linien-Icons — passend zum repräsentativen Antritt der Seite.
   Ab 560px (sobald die Karten dreispaltig stehen) wird das Foto zur vollen
   Bild-Kachel oben in der Karte (4:3 — die Quellfotos sind quadratisch,
   damit bleibt mehr vom Motiv sichtbar als bei 16:10); darunter bleibt es
   ein größeres Vorschaubild neben dem Titel. Seit der CTA-Button unter den
   Karten entfernt wurde, ist deutlich mehr Höhenbudget frei — genutzt für
   größere Bilder, größere Schrift und mehr Luft. */
.app-cards--photo .app-card__icon {
  position: relative;
  width: 4.5rem;
  height: 4.5rem;
  padding: 0;
  overflow: hidden;
  border-radius: var(--r-md);
  background: var(--sky-100);
}
.app-cards--photo .app-card__icon img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 400ms var(--ease);
}
.app-cards--photo .app-card:hover .app-card__icon img { transform: scale(1.04); }

/* Kein Zoom auf Touchscreens: Dort gibt es kein echtes Hover — schon das
   Antippen löst :hover aus und der Zoom startet ungewollt (früher mitten in
   der Wischbewegung des Karussells, dabei flackerte der Titel im Foto).
   Bewusst `pointer: coarse` zusätzlich zu
   `hover: none`, damit Geräte mit Maus den Effekt sicher behalten. */
@media (hover: none) and (pointer: coarse) {
  .app-cards--photo .app-card:hover .app-card__icon img { transform: none; }
}

@media (min-width: 560px) {
  .app-cards--photo { gap: clamp(0.75rem, 0.5rem + 1vh, 1.5rem); }
  .app-cards--photo .app-card {
    grid-template-columns: 1fr;
    padding: 0;
    overflow: hidden;
  }
  .app-cards--photo .app-card__icon {
    width: 100%;
    height: auto;
    aspect-ratio: 4 / 3;
    border-radius: 0;
  }
  /* Text zentrieren — sinnvoll nur hier, wo Bild oben und Text darunter
     steht. In der Handy-Zeilenform (Bild links, Titel rechts daneben)
     bleibt der Titel linksbündig, sonst wirkt er neben dem Bild verrutscht. */
  .app-cards--photo .app-card h3,
  .app-cards--photo .app-card p {
    text-align: center;
    padding-inline: clamp(1rem, 0.6rem + 1.4vh, 1.75rem);
  }
  .app-cards--photo .app-card h3 { font-size: var(--af-lg); margin-top: clamp(0.75rem, 0.4rem + 1vh, 1.25rem); }
  .app-cards--photo .app-card p { font-size: var(--af-sm); line-height: 1.5; margin-bottom: clamp(0.75rem, 0.4rem + 1vh, 1.25rem); }
}

/* Flache Fenster (z. B. 1024×768): Seit die Schriftskala auf Desktop-Zielgrößen
   kalibriert ist, braucht der Text hier mehr Höhe. Statt die Schrift wieder zu
   verkleinern, gibt das KARTENBILD Höhe ab — es ist der größte Einzelposten
   (gemessen: 395px Kartenhöhe, davon der 4:3-Anteil). 16:9 statt 4:3 spart pro
   Karte rund 25 % Bildhöhe, das Motiv bleibt erkennbar. */
@media (min-width: 560px) and (max-height: 820px) {
  .app-cards--photo .app-card__icon { aspect-ratio: 16 / 9; }
}
@media (max-width: 559px) {
  /* Handys, Grundform (ohne JS): einfache Liste. Jede Leistung ist eine
     flache Zeile mit Foto links und Titel daneben, darunter die
     Beschreibung. app.css setzt unter 560px ein Zeilenraster, ein Mini-Icon
     und kleine Titel und blendet die Beschreibung aus. Das wird hier alles
     zurückgenommen bzw. ersetzt.

     Mit JS werden daraus Foto-Kacheln (siehe unten): app4.js setzt dazu
     data-tiles an der Liste und macht die Titel zu Buttons.
     (Vorher: Wisch-Karussell, danach Aufklapp-Liste.) */
  .app-cards--photo {
    gap: 0.5rem;
    /* Kopfzeile: Das Foto füllt sie randlos (5:4) am linken Kartenrand.
       Höhe 56px auf 375×667; auf höheren Handys, wo Platz ist, wächst sie
       mit (bis 72px). */
    --lst-h: clamp(3.5rem, 8.4vh, 4.5rem);
    --lst-w: calc(var(--lst-h) * 1.25);
  }
  .app-cards--photo .app-card {
    position: relative;
    isolation: isolate;   /* hält die z-index-Ebenen unten in der Karte */
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto auto;
    align-items: stretch;
    align-content: start;
    gap: 0;
    padding: 0;
    overflow: hidden;
  }
  /* Foto und Titel teilen sich die Kopfzeile (dieselbe Grid-Zelle). Der
     Titel bzw. sein Button bekommt links Innenabstand in Fotobreite. Das
     Foto läuft oben, links und unten bis an den Kartenrand; die runden
     Ecken schneidet die Karte selbst zu (overflow: hidden). */
  .app-cards--photo .app-card__icon,
  .app-cards--photo .app-card h3 { grid-area: 1 / 1; }
  .app-cards--photo .app-card__icon {
    z-index: 1;
    align-self: stretch;
    justify-self: start;
    width: var(--lst-w);
    height: auto;
    margin: 0;
    border-radius: 0;
    pointer-events: none;
  }
  .app-cards--photo .app-card h3 {
    display: flex;
    align-items: center;
    min-height: var(--lst-h);
    margin: 0;
    padding: 0.375rem 0.875rem 0.375rem calc(var(--lst-w) + 0.875rem);
    font-size: var(--af-lg);
    line-height: 1.25;
    text-align: left;
  }
  .app-cards--photo .app-card p {
    display: block;
    grid-row: 2;
    margin: 0;
    padding: 0 0.875rem 0.875rem 0.75rem;
    font-size: var(--af-sm);
    line-height: 1.45;
    text-align: left;
  }

  /* --- Mit JS: Foto-Kacheln ---------------------------------------------
     Die drei Fotos stehen groß nebeneinander, der Titel darunter. Tippen
     wählt eine Leistung aus, ihr Text steht in einem Kasten darunter; ein
     kleiner Zipfel zeigt auf die gewählte Kachel. Immer genau eine ist
     gewählt, beim Laden die erste.

     Technik: Die Karten (<article>) werden per display:contents aufgelöst,
     Foto, Titel und Text jeder Karte stehen dadurch direkt im Raster der
     Liste. Foto + Titel kommen in Zeile 1 (Spalte = Nummer der Karte), alle
     drei Texte liegen übereinander in Zeile 2 über die volle Breite. Nur
     der gewählte ist sichtbar. Weil alle drei dieselbe Zelle teilen, ist die
     Zeile so hoch wie der längste Text: Beim Wechseln springt darunter
     nichts. Die Reihenfolge im Quelltext bleibt Titel 1, Text 1, Titel 2 …,
     Vorlesen ergibt also weiter Sinn.

     Muster: Button mit aria-expanded + aria-controls im h3 (legt app4.js
     an). */
  html.app .app-cards--photo[data-tiles] {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    grid-template-rows: auto auto;
    column-gap: 0.5rem;
    row-gap: 0.75rem;
  }
  html.app .app-cards--photo[data-tiles] .app-card { display: contents; }
  html.app .app-cards--photo[data-tiles] .app-card:nth-child(1) > :not(.app-card__panel) { grid-column: 1; }
  html.app .app-cards--photo[data-tiles] .app-card:nth-child(2) > :not(.app-card__panel) { grid-column: 2; }
  html.app .app-cards--photo[data-tiles] .app-card:nth-child(3) > :not(.app-card__panel) { grid-column: 3; }

  /* Foto: quadratisch über die ganze Spaltenbreite */
  html.app .app-cards--photo[data-tiles] .app-card__icon {
    grid-row: 1;
    align-self: start;
    width: 100%;
    height: auto;
    aspect-ratio: 1;
    border-radius: var(--r-md);
    box-shadow: var(--soft-1);
    transition: box-shadow 200ms var(--ease);
  }
  /* Gewählte Kachel: blauer Ring mit weißem Spalt ums Foto */
  html.app .app-cards--photo[data-tiles] .app-card[data-open] .app-card__icon {
    box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--sky-500), var(--soft-2);
  }
  /* … und das Foto zoomt leicht hinein, wie am Desktop beim Überfahren mit
     der Maus. Stärker als dort (8 statt 4 %), weil die Kachel viel kleiner
     ist — 4 % wären hier nur 4px. Das Foto bleibt dabei in der Kachel
     (overflow: hidden), der Übergang kommt aus der Grundregel (400ms). */
  html.app .app-cards--photo[data-tiles] .app-card[data-open] .app-card__icon img {
    transform: scale(1.08);
  }

  /* Titel: Der Button liegt über Foto UND Titel (durchsichtig), damit die
     ganze Kachel Tippfläche ist. Der Titel rutscht per Innenabstand unter
     das Foto: padding-top in % bezieht sich auf die BREITE, 100% ist also
     genau die Fotohöhe (quadratisch). */
  html.app .app-cards--photo[data-tiles] .app-card h3 {
    grid-row: 1;
    display: block;
    min-height: 0;
    margin: 0;
    padding: 0;
  }
  .app-card__toggle {
    position: relative;
    z-index: 2;
    /* flex statt block: Ein Button mit display:block zentriert seinen
       Inhalt senkrecht. Ist ein Nachbartitel zweizeilig (Zeile höher),
       rutschten einzeilige Titel dadurch vom Foto weg nach unten. */
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    align-items: center;
    width: 100%;
    height: 100%;
    padding: calc(100% + 0.375rem) 0 0;
    border: none;
    background: none;
    color: var(--ink);
    font: inherit;
    /* Nicht fett (Poppins 500 statt 700) und eine Stufe kleiner (12 statt
       14px): So passen alle drei Titel ab 360px Breite in eine Zeile. Die
       weichen Trennstellen im HTML greifen nur noch auf ganz schmalen
       Handys (320px). */
    font-weight: 500;
    font-size: var(--af-xs);
    letter-spacing: -0.01em;
    line-height: 1.25;
    text-align: center;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: color 200ms var(--ease);
  }
  .app-card__toggle[aria-expanded='true'] { color: var(--sky-700); }
  .app-card__toggle:focus-visible {
    outline: 2px solid var(--sky-600);
    outline-offset: 4px;
    border-radius: var(--r-md);
  }

  /* Text der gewählten Leistung: Kasten mit Zipfel zur Kachel */
  .app-card__panel {
    grid-area: 2 / 1 / 3 / -1;
    position: relative;
    padding: 0.625rem 0.75rem;
    background: #fff;
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    box-shadow: var(--soft-1);
    visibility: hidden;
    opacity: 0;
    transition: opacity 200ms var(--ease), visibility 0s linear 200ms;
  }
  .app-card[data-open] .app-card__panel {
    visibility: visible;
    opacity: 1;
    transition-delay: 0s;
  }
  html.app .app-cards--photo[data-tiles] .app-card p { padding: 0; text-align: center; }
  /* Mehr Luft zwischen Textkasten und „Zur flexiblen Sonderreinigung".
     Als padding, weil der Abstand darüber (margin-top) als Inline-Style im
     HTML steht und für Desktop gilt. */
  html.app #v-leistungen .app-cards--photo[data-tiles] + .app-leistungen__more { padding-top: 0.875rem; }
  /* Zipfel: gedrehtes Quadrat mit Linie oben/links, mittig unter der
     jeweiligen Spalte. Spaltenmitte bei 3 Spalten und 0.5rem Abstand:
     1. Spalte (100% − 1rem) / 6, 2. Spalte 50 %, 3. Spalte spiegelbildlich. */
  .app-card__panel::before {
    content: '';
    position: absolute;
    top: -0.4375rem;
    left: calc(var(--notch, 50%) - 0.4375rem);
    width: 0.875rem;
    height: 0.875rem;
    background: #fff;
    border-top: 1px solid var(--line);
    border-left: 1px solid var(--line);
    border-top-left-radius: 3px;
    transform: rotate(45deg);
  }
  .app-card:nth-child(1) .app-card__panel { --notch: calc((100% - 1rem) / 6); }
  .app-card:nth-child(3) .app-card__panel { --notch: calc(100% - (100% - 1rem) / 6); }
}
/* Hohe Handys (z. B. 390×844): Platz ist da, die Fotos werden hochkant
   (4:5) statt quadratisch. padding-top des Buttons wächst entsprechend mit
   (125 % der Breite = Fotohöhe). */
@media (max-width: 559px) and (min-height: 780px) {
  html.app .app-cards--photo[data-tiles] .app-card__icon { aspect-ratio: 4 / 5; }
  .app-card__toggle { padding-top: calc(125% + 0.375rem); }
}
@media (max-width: 559px) and (prefers-reduced-motion: reduce) {
  html.app .app-cards--photo[data-tiles] .app-card__icon,
  .app-card__toggle,
  .app-card__panel { transition: none; }
}

/* --------------------------------------------------------------------------
   3b. Sonderreinigung: Bild links, Text rechts
   Aufbau und Textformat folgen der Start-Ansicht (.app-hero), damit beide
   Bild-Text-Views gleich wirken. Zwei Abweichungen:

   a) Reihenfolge: Das Bild steht im Quelltext VOR dem Text (bewusst, damit
      auch ohne CSS die Lesefolge Bild → Text stimmt und auf Handys das Bild
      oben landet). Auf Desktop ergibt sich daraus automatisch links.
   b) Spaltenmaße: Das Motiv ist hochformatig (761×1341, ~0,57), die
      Startseite nutzt ein Querformat (900×630). Ein Hochformat kann eine
      breite Spalte nie füllen (bei 400px Breite bräuchte es ~705px Höhe, die
      die scrollfreie Bühne nicht hat) — eine feste fr-Aufteilung würde
      deshalb links viel Leerraum lassen. Darum: Bildspalte `auto` (so breit
      wie das Bild wirklich ist), Textspalte auf 36rem begrenzt (gleiche
      Zeilenlänge wie die Start-Ansicht, dort ~35rem) und das Ganze mit
      `justify-content: center` mittig — so bleiben die Außenabstände
   symmetrisch. Die Höhenbremse (45vh) ist kleiner als in app.css (dort
   clamp(10.5rem, 52vh, 39rem)), das Bild also bewusst etwas kleiner.
   -------------------------------------------------------------------------- */
@media (min-width: 860px) {
  .app-hero--media-left {
    grid-template-columns: auto minmax(0, 36rem);
    justify-content: center;
    column-gap: clamp(1.25rem, 0.8rem + 2vw, 3rem);
  }
}
html.app .app-hero--media-left .app-hero__media img {
  max-height: 45vh;
}
/* Hervorgehobener Teil der Überschrift in Sky wie das <em> in der
   Start-Ansicht — app.css stylt nur `.app-hero h1 em`. Gilt für alle
   Ansichten (Sonderreinigung, Dienstleistungen, Kontakt). */
.app-view h2 em { font-style: normal; color: var(--sky-600); }

/* Hervorhebungen im Fließtext — derzeit AUSGESCHALTET (Wunsch: vorerst
   ohne, aber merken). Die Stellen bleiben im HTML als
   <span class="app-mark"> markiert:
     Start:           „Raum Hildesheim" (als <strong>, also fett), „fachgerechte Reinigung"
     Sonderreinigung: „hartnäckige Verschmutzungen", „sensible Oberflächen",
                      „kurzfristige Notfälle", „wieder in Bestform"
     Kontakt:         „unverbindliches und kostenloses Angebot",
                      „innerhalb von 24 Stunden"
   Optik, falls sie zurück soll: fett und mit goldenem Textmarker-Streifen
   in der unteren Hälfte (Akzentfarbe der Buttons). Wieder einschalten:
   data-marks="on" am <html> setzen oder den Selektor unten auf `.app-mark`
   zurückstellen. box-decoration-break: Bricht die Stelle um, bekommt jede
   Zeile ihren eigenen, sauber abgeschlossenen Streifen. */
html[data-marks='on'] .app-mark {
  font-weight: 700;
  color: var(--navy-900);
  background: linear-gradient(transparent 58%, rgba(245, 183, 0, 0.38) 58%);
  padding-inline: 0.125em;
  margin-inline: -0.125em;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}

/* Handys/Tablets: Das Hochformat frisst hier am meisten Höhe, weil Bild und
   Text übereinander stehen. app.css bremst das Hero-Bild auf 44vh bzw. 22vh
   (unter 560px) — dieses Motiv braucht etwas weniger, damit Lead, Buttons und
   Vertrauenszeile ohne Scrollen sichtbar bleiben. */
@media (max-width: 859px) {
  html.app .app-hero--media-left .app-hero__media img { max-height: 32vh; }
}
@media (max-width: 559px) {
  html.app .app-hero--media-left .app-hero__media img { max-height: 26vh; }
}

/* app.css kürzt die Überschrift unter 560px auf das letzte Wort
   ("Herausforderungen") und verkleinert sie, damit die Ansicht ohne Scrollen
   passte. Seit die Bühne auf Mobile scrollt (Abschnitt 3f), steht die
   Langfassung auf allen Breiten — „Herausforderungen." allein trägt die
   Aussage nicht. */
@media (max-width: 559px) {
  #v-spezialreinigung h2 .app-h2-long { display: inline; }
  #v-spezialreinigung h2 { font-size: var(--af-h2); }
}
/* Sonderreinigung, die drei Punkte unter den Buttons (alle Größen):
   senkrechte Liste mit passendem Motiv im hellblauen Kreis (Regler, Uhr,
   Kamera) statt Häkchen, ohne Trennlinie darüber. Vorher: Häkchen-Zeile
   mit Linie (Desktop) bzw. 3er-Reihe mit Linie (Handy). Die Startseite
   behält ihre Häkchen. ID-Selektor, damit die allgemeinen Häkchen-Regeln
   (app.css, Abschnitt 3i hier) nicht greifen. */
#v-spezialreinigung .app-hero__trust {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 0.625rem;
  margin-top: clamp(1.75rem, 4.5vh, 2.75rem);
  padding-top: 0;
  border-top: none;
}
#v-spezialreinigung .app-hero__trust li {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 0.75rem;
  text-align: left;
  font-size: var(--af-sm);
  font-weight: 600;
  line-height: 1.3;
  color: var(--navy-800);
}
.app-trust__ico {
  display: grid;
  place-items: center;
  flex: none;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: 50%;
  background: var(--sky-100);
  color: var(--sky-700);
}
#v-spezialreinigung .app-trust__ico svg {
  width: 1.125rem;
  height: 1.125rem;
  color: inherit;
}

/* --------------------------------------------------------------------------
   3c. Flächen-Verläufe: alle Views in der Sky-Familie der Startseite
   (.app-view--hero in app.css: --sky-100 radial oben rechts + --sky-50
   linear nach Weiß). Damit es nicht langweilig wirkt, variiert pro View
   NICHT die Farbe, sondern die Geometrie: Position und Größe der
   Lichtquelle, Achse des Grundverlaufs und deren Anzahl.

   Farbdecke bleibt überall --sky-100 (#e0f2fe) — darauf hat --body (#3f5162)
   7,0:1 und --muted (#5c6b7a) 5,0:1, beides WCAG AA. Deshalb ist keine
   Textfarbe anzupassen.
   -------------------------------------------------------------------------- */
/* Leistungen: Lichtquelle mittig-oben, breiter und flacher als auf Start,
   plus ein zweiter schwacher Kegel unten links — die Karten stehen mittig,
   der Verlauf rahmt sie dadurch statt sie einseitig anzuleuchten. */
#v-leistungen {
  background:
    radial-gradient(52rem 20rem at 50% -12%, var(--sky-100) 0%, transparent 64%),
    radial-gradient(34rem 22rem at 6% 108%, var(--sky-50) 0%, transparent 60%),
    linear-gradient(180deg, #f7fcff 0%, #ffffff 72%);
}

/* Sonderreinigung: Lichtquelle oben LINKS (dort steht das Bild) und der
   Grundverlauf diagonal — spiegelt die Startseite, statt sie zu wiederholen. */
#v-spezialreinigung {
  background:
    radial-gradient(46rem 30rem at 10% -14%, var(--sky-100) 0%, transparent 62%),
    linear-gradient(145deg, var(--sky-50) 0%, #ffffff 78%);
}

/* Kontakt: zwei Lichtquellen an den oberen Ecken, Grundverlauf von unten
   nach oben — der ruhigste, hellste Aufbau, weil hier das Formular arbeitet. */
#v-kontakt {
  background:
    radial-gradient(38rem 24rem at 95% -10%, var(--sky-100) 0%, transparent 60%),
    radial-gradient(30rem 20rem at 2% -6%, var(--sky-50) 0%, transparent 58%),
    linear-gradient(0deg, #f4fbff 0%, #ffffff 62%);
}

/* --------------------------------------------------------------------------
   3d. Kontakt-View, Variante 4: Formular links, Mitarbeiterbild rechts,
   Kontaktwege mittig darunter in EINER Reihe.

   app.css baut den Kontakt-View als 0.8fr/1.2fr (Kontaktwege links,
   Formular rechts) und verdichtet die Kacheln auf Handys über
   `.app-contact .contact-method…`. Beides greift hier nicht mehr:
   Die Kacheln liegen jetzt als eigener Block UNTER dem Grid, deshalb
   sind die Handy-Regeln unten neu aufgesetzt.
   -------------------------------------------------------------------------- */
@media (min-width: 860px) {
  /* Formular bekommt mehr Platz als das Bild — es trägt die Aufgabe des
     Views. app.css setzt hier 0.8fr/1.2fr für die alte Reihenfolge. */
  .app-contact--split { grid-template-columns: 1.15fr 0.85fr; align-items: center; }
}

.app-contact__media { display: grid; place-items: center; min-height: 0; }
html.app .app-contact__media img {
  max-height: clamp(8rem, 34vh, 22rem);
  width: auto;
  max-width: 100%;
  border-radius: var(--r-lg);
  filter: drop-shadow(0 18px 32px rgba(12, 39, 64, 0.16));
}
/* Unter 860px stapelt das Grid — dann steht das Bild über dem Formular und
   darf nur noch wenig Höhe kosten (der View ist der engste der Seite). */
@media (max-width: 859px) {
  .app-contact--split .app-contact__media { order: -1; }
  html.app .app-contact__media img { max-height: 18vh; }
}
/* Handys: rundes Bild links neben Eyebrow und Überschrift statt
   ausgeblendet — derselbe Aufbau und dieselbe Kreisgröße wie das Porträt
   in der Sonderreinigung (Abschnitt 3i), damit beide Ansichten gleich
   beginnen. (Zwischenzeitlich stand der Kreis rechts.)
   Die Kontakt-Ansicht wird dazu zum zweispaltigen Raster: Kreis links,
   Eyebrow + H2 rechts daneben, alles andere (Einleitung, Formular,
   Kontaktwege) darunter über die volle Breite. .app-contact wird per
   display:contents aufgelöst, damit Formular und Bild getrennt im Raster
   stehen können. */
@media (max-width: 559px) {
  #v-kontakt .app-view__inner {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: 0.875rem;
    align-items: start;
  }
  #v-kontakt .app-view__inner > * { grid-column: 1 / -1; }
  /* Eyebrow oben bündig (nicht unten an der Zeile): So bleibt sie exakt
     im festen Abstand zur Navi, auch wenn der Kreis höher ist als
     Eyebrow + Überschrift zusammen. */
  #v-kontakt .app-view__inner > .eyebrow { grid-column: 2; grid-row: 1; align-self: start; }
  #v-kontakt .app-view__inner > h2 { grid-column: 2; grid-row: 2; }
  #v-kontakt .app-contact--split { display: contents; }
  #v-kontakt .app-form { grid-column: 1 / -1; }

  /* Formular direkt unter der Einleitung — zugeklappt an EXAKT derselben
     Stelle wie aufgeklappt (1.5rem unter dem Text). „Oder direkt
     erreichen" + Kontaktwege sitzen unten am Rand der Ansicht, der freie
     Platz liegt zwischen Formular und Kontaktwegen. (Zwischenzeitlich stand
     das Formular zugeklappt mittig — dann sprang es beim Aufklappen nach
     oben.)
     Dafür ist der Inhalt mindestens so hoch wie die Ansicht, und die letzte
     Rasterzeile (1fr) nimmt den freien Platz auf.
     min-height statt Strecken: Mit Strecken bliebe der Inhalt auch bei
     aufgeklapptem Formular auf Bildschirmhöhe, die Kontaktwege lägen dann
     über dem Formular. Ist das Formular offen, ist kein Platz frei — dann
     folgen die Kontaktwege direkt darunter und die Ansicht scrollt.
     Zeilen: 1 Eyebrow, 2 H2 (Kreisbild daneben), 3 Einleitung,
     4 Formular, 5 Kontaktwege. */
  html.app #v-kontakt > .app-view__inner {
    min-height: 100%;
    grid-template-rows: auto auto auto auto 1fr;
  }
  #v-kontakt .app-form { align-self: start; }
  #v-kontakt .contact-alt { align-self: end; }
  /* Der Abstand über dem Formular saß an .app-contact — das hat durch
     display:contents keine eigene Box mehr, also direkt am Formular. */
  html.app #v-kontakt .app-form,
  html.app #v-kontakt .contact-alt { margin-top: 1.5rem; }
  /* Einblend-Staffelung: .app-contact hat keine eigene Box mehr — Formular
     und Bild deshalb direkt animieren (Verzögerung wie vorher: 4. Kind). */
  html.app #v-kontakt[data-enter='true'] .app-contact--split > * {
    animation: app-enter 420ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
    animation-delay: 300ms;
  }

  #v-kontakt .app-contact__media {
    grid-column: 1;
    grid-row: 1 / span 2;
    align-self: center;
    position: relative;
    display: block;
    width: 4.25rem;
    height: 4.25rem;
    overflow: hidden;
    border-radius: 50%;
    background: radial-gradient(circle at 50% 30%, #fff 0%, var(--sky-100) 70%);
    box-shadow: 0 0 0 3px #fff, 0 6px 16px -6px rgba(12, 39, 64, 0.35);
  }
  /* Ausschnitt mit Kopf, Monitor und Tastatur (nicht nur der Kopf wie in
     der Sonderreinigung — das Motiv lebt vom Arbeitsplatz). Kreis um diese
     Punkte, am Alphakanal der Quelle (1694×1318) ausgemessen: Mittelpunkt
     x 938 / y 325, Radius 449px → Quadrat x 488 / y −124 / 898px.
     Alle Werte in %, der Ausschnitt bleibt bei jeder Kreisgröße gleich. */
  html.app #v-kontakt .app-contact__media img {
    position: absolute;
    left: -54.3%;
    top: 13.8%;
    width: 188.6%;
    max-width: none;
    height: auto;
    max-height: none;
    border-radius: 0;
    filter: none;
  }
}

/* „(optional)" am Telefon-Label: klar abgesetzt vom Pflicht-Sternchen
   (.field .req ist orange), damit der Unterschied auf einen Blick sitzt. */
.field__opt { font-weight: 400; color: var(--muted); }

/* Kontakt: Einleitung zwischen Überschrift und Formular (Handy + Desktop).
   Abstand nach oben wie bei den anderen Ansichten zwischen H2 und Text. */
#v-kontakt .app-view__inner > .lead { margin-top: 0.625rem; }
/* Handy/Tablet: etwas mehr Luft zum Formular, damit es als eigener Block
   beginnt (die Ansicht scrollt dort ohnehin). */
@media (max-width: 859px) {
  html.app #v-kontakt .app-contact { margin-top: clamp(1rem, 0.5rem + 1.5vh, 1.75rem); }
}
/* Desktop: Hier muss der ganze Kontakt-View in einen Bildschirm passen
   (1440×900 vorher 594 von 650px). Der Satz darf deshalb die volle Breite
   nutzen und steht in EINER Zeile statt zwei, der Abstand zum Formular
   bleibt beim bisherigen --app-gap — sonst kippt der View ins Notventil. */
@media (min-width: 860px) {
  #v-kontakt .app-view__inner > .lead { max-width: none; }
  html.app #v-kontakt .app-contact { margin-top: var(--app-gap); }
}

/* Kontaktwege: eine mittige Reihe unter allem, mit Hinleitung direkt darüber.
   Format ist das gleiche Eyebrow wie „— KONTAKT" (Strich, Poppins, Sky,
   Versalien, gesperrt) — nur zentriert statt linksbündig.

   .eyebrow ist inline-flex; damit die Box mittig sitzt, braucht der Absatz
   zusätzlich text-align: center (eyebrow--center zentriert nur den Inhalt
   INNERHALB der Box). Der große Abstand nach oben sitzt hier, weil die Zeile
   das erste Element des Blocks ist; die Reihe schließt direkt an sie an,
   sonst reißt die Zusammengehörigkeit auseinander. Bezug ist die
   Viewport-HÖHE: auf hohen Fenstern viel Luft, auf flachen nur so viel, dass
   der View nicht ins Notventil kippt. */
/* Der Block „Alternative Kanäle": Überschrift + Kacheln gehören zusammen.
   Er ist nur so breit wie die Kacheln (fit-content, mittig) — über die volle
   Inhaltsbreite gezogen läse sich die Zeile als Fußnote des Formulars, weil
   sie dann direkt an der linken Formularkante beginnt. */
.contact-alt {
  width: fit-content;
  max-width: 100%;
  margin-inline: auto;
  margin-top: clamp(0.875rem, 0.1rem + 3vh, 2.75rem);
}

.contact-methods__intro {
  /* flex statt block: so zählt der Strich zur Box-Breite und die GESAMTE
     Zeile (Strich + Text + Strich) wird mittig gesetzt. Bei text-align auf
     einem Block-Absatz würde nur der Text zentriert und die Striche
     schieben ihn optisch aus der Mitte (gemessen: 18px Versatz). */
  display: flex;
  justify-content: center;
  /* margin-bottom aus .eyebrow (var(--sp-3)) auf 0 — der Abstand zur
     Kachelreihe wird an .contact-methods--row gesetzt. Der Abstand nach oben
     sitzt am .contact-alt-Container. */
  margin: 0;
  /* Auf schmalen Viewports bricht der Satz zweizeilig. Ohne text-align
     bliebe die zweite Zeile linksbündig unter der ersten stehen. */
  text-align: center;
}
/* Strich VOR und NACH dem Text — .eyebrow bringt nur ::before mit, das
   ::after spiegelt es. flex: none verhindert, dass Flex die Striche beim
   Umbruch staucht (gemessen sonst: 28px → 18,9px). */
.contact-methods__intro::before,
.contact-methods__intro::after {
  flex: none;
}
.contact-methods__intro::after {
  content: '';
  width: 1.75rem;
  height: 2px;
  border-radius: 2px;
  background: var(--sky-500);
}
.contact-methods--row {
  margin-top: clamp(0.25rem, 0.15rem + 0.4vh, 0.5rem);
  grid-template-columns: repeat(3, minmax(0, auto));
  justify-content: center;
  gap: clamp(0.375rem, 0.25rem + 0.6vw, 1rem);
}
/* Flache Fenster (z. B. 1024×768, 13"-Notebooks): der große Luftraum passt
   hier nicht mehr ins Höhenbudget — ohne diese Rücknahme kippt der View ins
   Notventil und wird scrollbar. Zusätzlich gibt das Bild etwas Höhe ab; es
   ist hier Deko, das Formular trägt die Aufgabe. */
@media (max-height: 800px) and (min-width: 860px) {
  .contact-alt { margin-top: 0.5rem; }
  html.app .app-contact__media img { max-height: 26vh; }
  /* Höhentreiber ist hier ausschließlich das FORMULAR (gemessen: 361px vs.
     200px Bild) — deshalb wird dort gespart, nicht am Motiv. Die Feldgröße
     bleibt bei 16px (iOS-Auto-Zoom), alle Felder bleiben sichtbar. */
  #v-kontakt .app-form .field textarea { min-height: 2rem; }
  #v-kontakt .app-form .form-grid { gap: 0.25rem; }
  #v-kontakt .app-form { padding: 0.625rem 0.875rem; }
  #v-kontakt .app-form .field { gap: 0.125rem; }
  /* Kacheln flacher: die Unterzeile bleibt, nur die Polsterung geht runter. */
  .contact-methods--row .contact-method { padding: 0.4375rem 0.75rem; }
  /* Seit die Schriftskala auf Desktop-Zielgrößen kalibriert ist, fehlen hier
     die letzten ~12px. Die Unterzeilen der Kacheln entfallen deshalb: Der
     Kanal steht in der Hauptzeile, die Nummer/Adresse im href — es geht also
     keine erreichbare Information verloren. */
  .contact-methods--row .contact-method span span { display: none; }
  /* Seit der Einleitungssatz unter der Überschrift steht (+32px), fehlten
     auf 1366×768 wieder ~27px. Gespart wird an Stellen ohne Informations-
     verlust: Absende-Button auf 44px (Mindest-Tippgröße), knappere
     Abstände rund um den Satz und etwas flachere Kontaktkacheln.
     Ziel ist ein Puffer von ein paar Pixeln unter der Bühnenhöhe: Das
     Notventil in app.js misst direkt nach dem Tab-Wechsel, während die
     Einblend-Animation den Inhalt noch 10px nach unten versetzt — ohne
     Puffer kippt der View nach jedem Tab-Wechsel ins Scrollen. */
  #v-kontakt .app-view__inner > .lead { margin-top: 0.125rem; line-height: 1.35; }
  html.app #v-kontakt .app-contact { margin-top: 0.25rem; }
  #v-kontakt .app-form .btn { min-height: 44px; padding-block: 0.5rem; }
  .contact-alt { margin-top: 0.25rem; }
  .contact-methods--row { margin-top: 0.125rem; }
  .contact-methods--row .contact-method { padding-block: 0.25rem; }
}

/* Desktop: Einleitung rechts ÜBER dem Bild, neben dem Formular, statt über
   die volle Breite. Grund: Der Einleitungstext ist auf drei Sätze
   gewachsen und brauchte über die volle Breite zwei Zeilen — damit passte
   der Kontakt-View auf 1366×768 und 1536×864 nicht mehr ohne Scrollen. In
   der rechten Spalte war dagegen Platz frei: Das Formular ist deutlich
   höher als das Bild daneben (gemessen 1366×768: 350 zu 200px).
   Technik: .app-contact wird per display:contents aufgelöst, damit
   Formular, Einleitung und Bild gemeinsam im Raster der Ansicht stehen.
   Spalten wie vorher im .app-contact (1.15fr / 0.85fr).
   Zeilen: 1 Eyebrow, 2 H2, 3 Einleitung (rechts) + Formular (links, über
   3 und 4), 4 Bild (rechts), 5 Kontaktwege. */
@media (min-width: 860px) {
  #v-kontakt .app-view__inner {
    --kontakt-top: var(--app-gap);
    display: grid;
    grid-template-columns: 1.15fr 0.85fr;
    column-gap: var(--app-gap);
    align-items: start;
  }
  #v-kontakt .app-view__inner > * { grid-column: 1 / -1; }
  #v-kontakt .app-view__inner > .eyebrow { grid-row: 1; }
  #v-kontakt .app-view__inner > h2 { grid-row: 2; }
  #v-kontakt .app-contact--split { display: contents; }
  #v-kontakt .app-form {
    grid-column: 1;
    grid-row: 3 / span 2;
    margin-top: var(--kontakt-top);
  }
  /* html.app für Vorrang vor den allgemeinen Einleitungs-Abständen oben */
  html.app #v-kontakt .app-view__inner > .lead {
    grid-column: 2;
    grid-row: 3;
    margin-top: var(--kontakt-top);
  }
  #v-kontakt .app-contact__media {
    grid-column: 2;
    grid-row: 4;
    align-self: center;
    margin-top: var(--app-gap);
  }
  #v-kontakt .app-view__inner > .contact-alt { grid-row: 5; }
  /* Einblend-Staffelung: .app-contact hat keine eigene Box mehr —
     Formular und Bild deshalb direkt animieren (wie auf Handys). */
  html.app #v-kontakt[data-enter='true'] .app-contact--split > * {
    animation: app-enter 420ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
    animation-delay: 300ms;
  }
}
@media (max-height: 800px) and (min-width: 860px) {
  #v-kontakt .app-view__inner { --kontakt-top: 0.5rem; }
}
.contact-methods--row .contact-method {
  padding: 0.625rem 0.875rem;
  box-shadow: var(--soft-1);
}
.contact-methods--row .contact-method__icon { width: 2rem; height: 2rem; }
.contact-methods--row .contact-method__icon svg { width: 1rem; height: 1rem; }
.contact-methods--row .contact-method strong { font-size: var(--af-sm); }
.contact-methods--row .contact-method span span { font-size: var(--af-xs); }

/* --------------------------------------------------------------------------
   3e. Start-Ansicht: größeres Mitarbeiterbild, aber Text ohne Umbrüche
   app.css setzt die Spalten 1.05fr/0.95fr und bremst das Bild auf
   clamp(10.5rem, 52vh, 39rem) — bei üblichen Desktop-Maßen (z. B. 1440×900)
   ist dort die SPALTENBREITE der begrenzende Faktor, nicht max-height.

   WICHTIG (Korrektur): Ein erster Versuch verteilte die Spalten 0.77fr/1.23fr,
   um das Bild um 30 % zu vergrößern. Das war falsch — die Textspalte wurde so
   schmal, dass Überschrift und Buttons umbrachen. Der Text hat hier Vorrang:

     - `minmax(min-content, …)` auf der Textspalte: Zusammen mit dem nowrap
       weiter unten ist min-content die Breite der längsten Überschriftenzeile
       bzw. der Button-Reihe. Die Spalte kann also NIE unter die Breite fallen,
       die für umbruchfreien Text nötig ist.
     - Das Bild bekommt, was danach übrig ist (immer noch mehr als die
       ursprünglichen 0.95fr, weil die Textspalte nicht mehr als nötig nimmt).
     - Deutlich größerer Spaltenabstand als --app-gap (war ~20px).
   -------------------------------------------------------------------------- */
@media (min-width: 860px) {
  /* Der eigentliche Flaschenhals für die Bildgröße ist NICHT die Höhenbremse,
     sondern `.app-view__inner { max-width: 1080px }` aus app.css:
     1080px − 66px Spaltenabstand − ~560px Text (Minimum für die umbruchfreie
     Überschrift) = nur noch ~454px für das Bild. Das ist WENIGER als die
     507px, die es vor allen Änderungen hatte — daher wirkte es zu klein.

     Für diese Ansicht wird der Deckel deshalb angehoben. Das ist hier
     unkritisch, weil der Start-View nur aus Textspalte + Bild besteht (keine
     Textkolumne, deren Zeilenlänge dadurch unlesbar lang würde) — die
     Textspalte bleibt durch minmax(min-content, …) ohnehin bei ihrer
     natürlichen Breite und wächst nicht mit. */
  #v-start .app-view__inner { max-width: 1600px; }

  #v-start .app-hero {
    /* Textspalte: min-content als Untergrenze hält die Überschrift
       umbruchfrei; `max-content` als Obergrenze verhindert, dass sie mehr
       Platz nimmt als ihr längster Inhalt braucht — alles darüber gehört dem
       Bild. Vorher standen hier fr-Anteile, die dem Text mehr gaben als
       nötig. */
    grid-template-columns: minmax(min-content, max-content) minmax(0, 1fr);
    /* Luft zwischen Textblock und Bild — vorher nur var(--app-gap) ≈ 20px */
    column-gap: clamp(2rem, 1rem + 3.5vw, 5rem);
  }

  /* Überschrift und Buttons dürfen nicht umbrechen. Der gewollte Umbruch in
     der Überschrift steht als <br> im HTML ("Strahlende Sauberkeit," /
     "die Werte erhält."); jeder WEITERE Umbruch ist unerwünscht.
     Nur ab 860px, darunter ist einspaltig und Umbrüche sind dort nötig. */
  #v-start h1 { white-space: nowrap; }
  #v-start .app-hero__actions { flex-wrap: nowrap; }
  /* text-wrap: balance aus style.css würde die nowrap-Zeilen sonst erneut
     ausbalancieren wollen — hier explizit abschalten. */
  #v-start .btn { white-space: nowrap; text-wrap: nowrap; }
}

/* Zwischenbreiten 860–1279px: Hier ist die FENSTERbreite der Deckel, nicht
   mehr max-width. Text (~560px bei nowrap) + Abstand fressen den Platz, das
   Bild würde auf ~355px schrumpfen (gerechnet für 1024px) und damit kleiner
   sein als vor allen Änderungen.

   Der Text bleibt trotzdem umbruchfrei (ausdrücklicher Wunsch). Platz kommt
   stattdessen aus zwei Stellen, die niemandem wehtun:
     - kleinerer Spaltenabstand (der große Abstand ist Komfort, kein Muss)
     - die Überschrift skaliert hier zusätzlich mit der BREITE (vw), nicht nur
       mit der Höhe. Dadurch schrumpft ihre min-content-Breite mit und gibt
       dem Bild Platz, statt es zu erdrücken. */
@media (min-width: 860px) and (max-width: 1279px) {
  #v-start .app-hero { column-gap: clamp(1.25rem, 0.25rem + 2vw, 2.5rem); }
  #v-start h1 { font-size: clamp(1.5rem, 0.2rem + 2.6vw, var(--af-h1)); }
}
html.app #v-start .app-hero__media img {
  /* 52vh × 1.3 ≈ 68vh, 39rem × 1.3 ≈ 50.7rem, Minimum unverändert (bleibt
     die Untergrenze für sehr kleine/flache Fenster). Begrenzt wird das Bild
     auf Desktop ohnehin von der Spaltenbreite, die nach dem Text übrig ist. */
  max-height: clamp(10.5rem, 68vh, 50.7rem);
}
@media (max-width: 859px) {
  /* Unter 860px liegt das Bild ÜBER dem Text (order:-1, app.css) und ist
     hier der alleinige Höhentreiber (anders als bei <560px, siehe unten).
     44vh (= 44 × 1.3, „konsequent skaliert") lag exakt auf der
     Notventil-Kante (gemessen 800×1000: need 745 = avail 745, 0px Reserve).
     38vh belässt echte Reserve (~19px) und ist immer noch ein deutliches
     Wachstum gegenüber dem alten Wert (44vh × 0.864, also weiterhin größer
     als vorher, nur nicht die vollen +30 %, weil die Spalte hier keine
     zusätzliche Breite mehr hergibt). */
  html.app #v-start .app-hero__media img { max-height: 38vh; }
}
@media (max-width: 559px) {
  /* Handys: Text und Button zuerst (sofort sichtbar), die Illustration
     kleiner darunter als Abschluss — app.css stellt das Bild unter 860px per
     order:-1 nach oben, dort fraß es gemessen 193px (375×667), bevor die
     Überschrift überhaupt begann. Kompakt auf normalen Handys (136px),
     auf hohen Handys wächst sie mit, wo sonst nur Leerraum bliebe
     (390×844: ~169px). */
  #v-start .app-hero__media { order: 0; }
  html.app #v-start .app-hero__media img { max-height: clamp(8.5rem, 20vh, 12rem); }
}

/* Ab hier wird es eng: erst die Unterzeilen der Kacheln streichen (die
   Hauptzeile nennt den Kanal bereits), dann auf Icon-Reihe reduzieren. */
@media (max-width: 859px) {
  .contact-methods--row .contact-method span span { display: none; }
}
/* Unter 560px: drei Kacheln nebeneinander, Icon über der Beschriftung.
   Die Beschriftung war hier auf 11px (flache Handys sogar 10px) gestaucht,
   damit die Ansicht ohne Scrollen passte — jetzt lesbare Größe; zweizeiliger
   Umbruch („E-Mail / schreiben") ist dabei in Ordnung. */
@media (max-width: 559px) {
  .contact-methods--row {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 0.5rem;
  }
  .contact-methods--row .contact-method {
    grid-template-columns: 1fr;
    justify-items: center;
    text-align: center;
    gap: 0.375rem;
    padding: 0.75rem 0.375rem;
  }
  .contact-methods--row .contact-method strong {
    font-size: var(--af-xs);
    line-height: 1.25;
  }
}

/* --------------------------------------------------------------------------
   3f. Handy/Tablet: Kopf, Navi und Footer fest — nur der Inhalt scrollt
   Die drei Leisten sind ohnehin feste Grid-Reihen der Shell (app.css +
   Abschnitt 0 oben); hier geht es nur um die Bühne dazwischen.

   Vorher regelte das ein JS-Notventil (checkOverflow in app.js), das zwei
   Lücken hatte:
     1. Die Views zentrieren per `align-content: center`. Wird der Inhalt
        höher als die Bühne, läuft er dabei zu gleichen Teilen nach OBEN und
        unten über. `scrollHeight` misst nur den Überlauf nach unten — der
        obere Teil blieb unerkannt abgeschnitten (gemessen 375×667,
        Start-Ansicht: Bild ragte 7px über die Bühnenkante, kein Scrollen).
     2. 8px Toleranz: kleinere Überläufe wurden stillschweigend gekappt.

   Jetzt rein per CSS, ohne Messen:
     - Der View ist auf Mobile IMMER ein Scroll-Container. Scrollbalken
       erscheinen trotzdem nur, wenn wirklich etwas übersteht (overflow auto;
       auf Touch-Geräten ohnehin als Overlay ohne Platzbedarf).
     - Zentriert wird über `margin-block: auto` am einzigen Kind statt über
       align-content. Auto-Margins verteilen nur FREIEN Platz: Passt der
       Inhalt, sitzt er wie bisher mittig; passt er nicht, fallen die Margins
       auf 0, der Inhalt beginnt oben und scrollt nach unten — nichts kann
       mehr über die Oberkante verschwinden.
     - `align-content: stretch` lässt die (einzige) Grid-Zeile die Bühne
       füllen, damit die Auto-Margins überhaupt Platz zum Verteilen haben.
     - `overscroll-behavior: contain`: Am Ende des Inhalts läuft die Geste
       nicht auf die Seite durch (kein Gummiband, kein Pull-to-Refresh, der
       die festen Leisten mitziehen würde).

   Das JS-Notventil bleibt aktiv, ist hier aber nur noch redundant, und
   seine Optik wird unten zurückgenommen (align-content, Innenabstand): Die
   Ansichten scrollen hier ohnehin selbst. Grund: In der Kontakt-Ansicht
   ist der Inhalt mindestens bildschirmhoch (min-height: 100%). Während der
   Einblend-Animation steht der Inhalt 10px tiefer, das Notventil schlug
   an, sein align-content: start ließ den Inhalt schrumpfen, das Ventil
   ging wieder aus — und so weiter in jedem Bild. Das Formular zitterte
   dabei sichtbar um 15px auf und ab.

   Bis 859px, also alles, was einspaltig stapelt (Handys hoch UND quer,
   Tablets hochkant). Desktop bleibt unverändert beim Notventil. */
@media (max-width: 859px) {
  html.app .app-view,
  html.app .app-view[data-overflow='true'] {
    align-content: stretch;
    overflow-x: hidden;
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
  }
  html.app .app-view > .app-view__inner { margin-block: auto; }
  /* Zusätzlicher Innenabstand des Notventils (app.css) ebenfalls zurück,
     sonst ändert sich die Inhaltshöhe mit dem Ventil. */
  html.app .app-view[data-overflow='true'] .app-view__inner { padding-bottom: 0; }

  /* Immer ein kleines bisschen scrollbar: Passt eine Ansicht komplett auf
     den Bildschirm, war sie bisher gar kein Scrollbereich — die Seite
     wirkte beim Wischen „eingefroren". Ein unsichtbarer 1px-Punkt direkt
     unter der Unterkante macht jede Ansicht um 1px höher als ihr Fenster.
     Damit ist sie immer ein echter Scrollbereich, und der Browser zeigt
     seinen normalen Nachgeben-Effekt beim Hoch- und Runterwischen
     (iPhone: Gummiband, Android: Dehnen). Kopf, Navi und Footer bleiben
     dabei stehen, weil nur die Ansicht selbst scrollt.
     position:absolute statt eines Elements im Fluss: So verschiebt der
     Punkt weder die Zentrierung noch löst er das Notventil in app.js aus. */
  html.app .app-view::after {
    content: '';
    position: absolute;
    top: 100%;
    left: 0;
    width: 1px;
    height: 1px;
    pointer-events: none;
  }
}

/* --------------------------------------------------------------------------
   3g. Handy/Tablet: Inhalte vollständig und lesbar
   Die „Handy-Verdichtung" in app.css (Abschnitt 7b) blendet unter 560px
   Texte aus, versteckt Formular-Labels und schrumpft die Schrift mit der
   Viewport-Höhe — alles nur, damit jede Ansicht ohne Scrollen in einen
   Bildschirm passte. Seit die Bühne auf Mobile scrollt (Abschnitt 3f),
   entfällt dieser Zwang. Hier wird deshalb zurückgenommen, was Inhalt kostet.

   Bewusst NICHT zurückgeholt:
     - Claim im Kopf: Der Kopf ist fest, jede Zeile dort fehlt dauerhaft der
       scrollbaren Fläche. Der Claim wiederholt ohnehin die Hero-Überschrift.
     - Langfassung der Kontakt-Hinleitung („Wählen Sie Ihren bevorzugten
       Kommunikationsweg"): reine Anweisung ohne Information, die Kacheln
       darunter sprechen für sich.
   -------------------------------------------------------------------------- */
@media (max-width: 859px) {
  /* Schrift: feste Größen statt höhen-elastischer Formeln.
     Die vh-Kopplung aus app.css war für „alles in einen Bildschirm" gebaut:
     Auf flachen Handys schrumpfte Fließtext auf ~13px und Kachel-
     Beschriftungen auf 10px (375×667), auf hohen Handys wuchsen die
     Überschriften dagegen auf 40px (H1) bzw. 29px (H2, 390×844).
     Jetzt eine ruhige, kompakte Skala; Überschriften skalieren leicht mit
     der BREITE (vw), damit sie auf Tablets hochkant nicht winzig wirken.
     Eingabefelder bleiben bei 16px (app.css) — darunter zoomt iOS beim
     Antippen automatisch hinein. */
  html.app {
    --af-xs: 0.75rem;     /* 12px: Eyebrows, Labels, Häkchen-Zeile, Kacheln */
    --af-sm: 0.875rem;    /* 14px: Fließtext, Kartentexte, Buttons */
    --af-base: 0.9375rem; /* 15px: Einleitungstexte (.lead) */
    --af-lg: 1rem;        /* 16px: Kartentitel */
    /* Überschriften: EINE Größe für alle H2 — auf dem Handy sollen die
       Ansichten gleich beginnen. Die Größe wächst mit der Breite (≈ 20,7px
       auf 375, 22px auf 390, 20px-Minimum, Tablets bis 28px). Maßgeblich ist
       die Sonderreinigung: Dort steht die lange Überschrift neben dem
       Porträt, und mit fester Größe hatte sie auf 360px-Handys vier statt
       drei Zeilen. Der Teiler hält das Verhältnis Textbreite /
       Schriftgröße so, dass es bei drei Zeilen bleibt. */
    --af-h2: clamp(1.25rem, calc((100vw - 7.125rem) / 12.6), 1.75rem);
    /* Ausnahme Startseite: Die H1 bleibt bewusst größer (~25px auf Handys,
       ~33px auf Tablets), sie ist die Hauptüberschrift der Seite. */
    --af-h1: clamp(1.5rem, 1.1rem + 2vw, 2.25rem);
  }
  html.app .app-view h2 { line-height: 1.15; }
  /* Gleiche Abstände unter jeder Überschrift: 18px bis zur Einleitung
     (ursprünglich 14 / 0 / 12 / 10px auf Start / Dienstleistungen /
     Sonderreinigung / Kontakt, dann einheitlich 12px — auf Wunsch etwas
     mehr Luft). Die IDs im Selektor sind nötig, weil die einzelnen
     Ansichten ihre Einleitung sonst über ID-Regeln setzen. */
  html.app :is(#v-start, #v-leistungen, #v-spezialreinigung, #v-kontakt) :is(h1, h2) + .lead {
    margin-top: 1.125rem;
  }

  /* Einleitungstexte: app.css blendet sie auf flachen Handys aus */
  html.app .app-view .lead { display: block; }

  /* Alle drei Vertrauens-Häkchen (Start + Sonderreinigung), nicht nur zwei */
  html.app .app-hero__trust li:nth-child(n + 3) { display: flex; }

  /* Kontakt: volle Überschrift „Unverbindliches Angebot anfordern" in
     normaler H2-Größe statt Kurzfassung in Kartentitel-Größe */
  html.app #v-kontakt h2 { font-size: var(--af-h2); }
  /* Blauer Teil als eigene zweite Zeile, wie auf Start und Sonderreinigung
     („Schnell zum / sauberen Angebot", „Sichtbar saubere / Ergebnisse").
     Nur auf Handys — am Desktop bleibt die Überschrift einzeilig, sonst
     passen Kontakt und Dienstleistungen auf flachen Laptops nicht mehr. */
  html.app #v-kontakt h2 em,
  html.app #v-leistungen h2 em { display: block; }
  /* Einleitung unter der Überschrift: app.css blendet „h2 + .lead" im
     Kontakt unter 560px gezielt aus — hier wieder sichtbar. */
  html.app #v-kontakt .lead { display: block; }

  /* Kontaktformular: Die Labels über Name/E-Mail/Telefon/Leistung waren
     unter 560px nur für Screenreader da (Platzhalter mussten reichen).
     Sichtbare Labels sind hier klar besser — Platzhalter verschwinden beim
     Tippen, und „(optional)" am Telefon stand gar nicht im Platzhalter. */
  html.app #v-kontakt .app-form .form-grid--2 .field label {
    position: static;
    width: auto;
    height: auto;
    overflow: visible;
    clip-path: none;
  }
  /* Normale Abstände und ein Nachrichtenfeld, in das mehr als eine Zeile
     passt (vorher 2rem ≈ eine Zeile). Feldschrift bleibt bei 16px aus
     app.css (iOS-Auto-Zoom). */
  html.app #v-kontakt .app-form { padding: 1rem 1rem 1.125rem; }
  html.app #v-kontakt .app-form .form-grid { gap: 0.875rem; }
  html.app #v-kontakt .app-form .field { gap: 0.3125rem; }
  html.app #v-kontakt .app-form .field textarea { min-height: 6.5rem; }
  html.app #v-kontakt .app-form .checkbox { font-size: var(--af-xs); line-height: 1.45; }
}

/* --------------------------------------------------------------------------
   3g-2. Handys: alles proportional zur Bildschirmbreite
   Referenz ist das iPhone 15 Pro (393px breit), auf dem die Handy-Ansicht
   abgestimmt wurde. Vorher waren Schrift und Abstände auf jedem Handy gleich
   groß — auf schmaleren Geräten (z. B. 360px) brachen Überschriften und
   Texte deshalb anders um und Ansichten fingen an zu scrollen, auf größeren
   (z. B. 430px) wirkte alles kleiner und leerer.
   Jetzt wächst die Grundschrift (1rem) mit der Breite: 16px bei 393px,
   14,7px bei 360px, 17,5px bei 430px. Weil fast alle Maße im Projekt in rem
   angegeben sind, skalieren Schrift, Abstände, Kreisbilder, Kacheln und
   Leisten gemeinsam — die Seite sieht auf jedem Handy aus wie auf dem
   iPhone, nur größer oder kleiner. Zeilenumbrüche bleiben dadurch gleich.
   Grenzen 13–18px, damit sehr schmale bzw. sehr breite Geräte nicht
   unlesbar klein oder übergroß werden.
   Zusätzlich zählt die HÖHE: Referenz 659px (sichtbare Höhe in Safari auf
   dem iPhone 15 Pro). Maßgeblich ist der kleinere der beiden Faktoren. Auf
   Handys mit im Verhältnis kürzerem Bildschirm (z. B. iPhone SE, 375×553)
   wird dadurch alles etwas kleiner, statt dass die Ansichten scrollen. svh =
   sichtbare Höhe bei eingeblendeten Browserleisten; die Zeile davor mit vh
   ist die Rückfallebene für ältere Browser. Abstände, die zusätzlich direkt
   an der Bildschirmhöhe hängen (vh), bleiben daran gekoppelt. */
@media (max-width: 559px) {
  html.app {
    font-size: clamp(13px, min(100vw * 16 / 393, 100vh * 16 / 659), 18px);
    font-size: clamp(13px, min(100vw * 16 / 393, 100svh * 16 / 659), 18px);
    /* Überschriften als feste rem-Werte, damit sie genau mitskalieren
       (Werte wie bisher auf 393px: H2 ≈ 22px, H1 ≈ 25,4px) */
    --af-h2: 1.385rem;
    --af-h1: 1.59rem;
  }
  /* Eingabefelder nie unter 16px: Darunter zoomt iOS beim Antippen
     automatisch in die Seite hinein. */
  html.app .app-form .field input,
  html.app .app-form .field select,
  html.app .app-form .field textarea { font-size: max(16px, 1rem); }
  /* Unter 360px blendet app.css die Tab-Beschriftungen aus — bei
     proportionaler Schrift passen sie dort genauso wie auf dem iPhone. */
  .app-tab__label {
    position: static;
    width: auto;
    height: auto;
    overflow: visible;
    clip-path: none;
  }
}

/* Footer: der volle Name „Gama Reinigungsservice." passt seit dem Wegfall
   des Startseiten-Links auch auf schmalen Handys in die Zeile — seit die
   Schrift auf Handys mit der Breite skaliert (Abschnitt 3g-2), auch unter
   360px. */
@media (max-width: 519px) {
  .app-footer .footer__bottom .app-footer__long { display: inline; }
}

/* --------------------------------------------------------------------------
   3h. Kontakt auf Handys: Formular zum Aufklappen
   Das Formular ist auf dem Handy der mit Abstand größte Block der Seite
   (gemessen 375×667: rund zwei Bildschirmhöhen). Eingeklappt sieht man
   stattdessen auf einen Blick beide Wege: das Formular als Karte mit
   Aufklapp-Kopf, darunter die Direktkontakte (Anruf, WhatsApp, E-Mail).

   Muster: Disclosure (Button mit aria-expanded + aria-controls), kein
   <details>, weil das Formular auf Tablet/Desktop immer offen sein muss —
   das kann <details> nicht per CSS. Der Kopf ist per Default unsichtbar und
   erscheint nur, wenn app4.js data-collapsible gesetzt hat. Ohne JS bleibt
   das Formular deshalb einfach offen, wie bisher.

   Aufgeklappt wird ausschließlich durch Tippen auf den Kopf — auch wer über
   „Kostenloses Angebot" oder „Formular" kommt, sieht es zuerst eingeklappt.
   -------------------------------------------------------------------------- */
.app-form__toggle { display: none; }

@media (max-width: 559px) {
  /* Die Karte selbst ohne Innenabstand — Kopf und Formular bringen ihren
     eigenen mit, damit die ganze Kopfzeile Tippfläche ist. ID-Selektor, weil
     Abschnitt 3g das Padding ebenfalls über #v-kontakt setzt. */
  html.app #v-kontakt .app-form[data-collapsible='true'] {
    padding: 0;
    overflow: hidden;
    transition: border-color 200ms var(--ease), box-shadow 200ms var(--ease);
  }
  /* Aufgeklappt: blauer Rahmen wie bei anderen aktiven Karten (2px: die
     1px-Linie der Karte plus 1px Ring außen, kein Layout-Sprung). */
  html.app #v-kontakt .app-form[data-collapsible='true'][data-open] {
    border-color: var(--sky-500);
    box-shadow: 0 0 0 1px var(--sky-500), var(--soft-2);
  }
  html.app #v-kontakt .app-form[data-collapsible='true'] .app-form__toggle {
    display: grid;
    grid-template-columns: auto 1fr auto;
    align-items: center;
    gap: 0.875rem;
    width: 100%;
    min-height: 4rem;
    padding: 0.75rem 1rem;
    border: none;
    background: none;
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }
  .app-form__toggle-icon {
    display: grid;
    place-items: center;
    width: 2.5rem;
    height: 2.5rem;
    border-radius: var(--r-md);
    /* Orange wie Kontakt-Tab und Absende-Button: das Formular ist das
       Konversionsziel und soll sich von den Direktkontakt-Kacheln abheben. */
    background: var(--cta-100);
    color: var(--cta-600);
  }
  .app-form__toggle-icon svg { width: 1.25rem; height: 1.25rem; }
  .app-form__toggle-text strong {
    display: block;
    font-family: 'Poppins', sans-serif;
    font-size: var(--af-base);
    line-height: 1.25;
    color: var(--ink);
  }
  .app-form__toggle-text span {
    display: block;
    margin-top: 0.125rem;
    font-size: var(--af-xs);
    color: var(--muted);
  }
  .app-form__toggle-chevron {
    width: 1.375rem;
    height: 1.375rem;
    color: var(--muted);
    transition: transform 220ms var(--ease);
  }
  .app-form__toggle[aria-expanded='true'] .app-form__toggle-chevron { transform: rotate(180deg); }
  .app-form__toggle:active { background: var(--sky-50); }
  .app-form__toggle:focus-visible {
    outline: 2px solid var(--sky-600);
    outline-offset: -2px;
    border-radius: var(--r-lg);
  }

  /* Formular unter dem Kopf, durch eine Linie abgesetzt */
  html.app #v-kontakt .app-form[data-collapsible='true'] > form {
    padding: 1rem 1rem 1.125rem;
    border-top: 1px solid var(--line);
  }
  html.app #v-kontakt .app-form[data-collapsible='true'] .app-form__toggle[aria-expanded='false'] + form {
    display: none;
  }
  /* Beim Aufklappen kurz einblenden statt schlagartig aufzutauchen
     (Keyframes aus app.css, Abschnitt 9). */
  html.app #v-kontakt .app-form[data-collapsible='true'] .app-form__toggle[aria-expanded='true'] + form {
    animation: app-enter 280ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
  }
}
@media (max-width: 559px) and (prefers-reduced-motion: reduce) {
  html.app #v-kontakt .app-form[data-collapsible='true'] .app-form__toggle[aria-expanded='true'] + form { animation: none; }
  .app-form__toggle-chevron { transition: none; }
}

/* --------------------------------------------------------------------------
   3i. Handy: kompakter Aufbau für Kopf, Navi, Start und Sonderreinigung
   Ziel: Die Ansichten sollen auf einem normalen Handy (375×667) ungefähr
   ohne Scrollen passen und dabei aufgeräumter wirken. Gemessen vorher:
   Kopf 64 + Navi 62 + Footer 40 = 166px fest (ein Viertel des Bildschirms);
   Start 607px Inhalt, Sonderreinigung 649px — bei nur 500px Bühne.
   Alles nur unter 560px; Tablet und Desktop bleiben unverändert.
   -------------------------------------------------------------------------- */

/* Kurz-/Langfassung von Button-Beschriftungen. Die jeweils unsichtbare
   Fassung ist display:none und damit auch für Screenreader weg — es wird
   also nie doppelt vorgelesen. */
.app-label-short { display: none; }

@media (max-width: 559px) {
  .app-label-long { display: none; }
  .app-label-short { display: inline; }

  /* ---- Einheitlicher Seitenanfang ----
     Jede Ansicht beginnt mit der Eyebrow-Zeile („— DIENSTLEISTUNGEN" usw.).
     Die stand bisher überall woanders (gemessen 375×667: 25 / 53 / 70 /
     122px unter der Navi), weil die Ansichten ihren Inhalt senkrecht
     MITTIG setzen (Abschnitt 3f) und jede Ansicht unterschiedlich hoch ist.
     Auf Handys jetzt: Inhalt oben, fester Abstand von 2rem zur Navi —
     beim Wechseln der Tabs bleibt die Zeile damit exakt an ihrem Platz.
     Zu hohe Ansichten scrollen wie bisher nach unten. */
  html.app .app-view { padding-top: 2rem; }
  html.app .app-view > .app-view__inner { margin-top: 0; margin-bottom: auto; }
  /* .eyebrow ist inline-flex: Als erstes Kind eines Blocks sitzt es in
     einer Textzeile und rutscht dadurch um den Zeilenabstand (~9px) nach
     unten — bei Dienstleistungen und Kontakt, nicht aber bei Start und
     Sonderreinigung (dort ist es ein Grid-Element). Als Block-Flex fällt
     die Zeile weg; fit-content hält die gewohnte Breite. */
  html.app .app-view__inner > .eyebrow { display: flex; width: fit-content; }

  /* Kopf 64 → 52px, Navi 62 → 52px. Logo und Tab-Icons etwas kleiner;
     die Tab-Fläche bleibt mit 52px deutlich über der 44px-Mindestgröße. */
  html.app { --app-tab-h: 3.25rem; }
  .app-head--centered { padding-block: 0.3125rem; }
  .app-head--centered .brand img { height: 2.625rem; }   /* 42px auf 393px, skaliert mit (3g-2) */
  .app-tab { padding: 0.25rem; gap: 0.125rem; }
  .app-tab svg { width: 1.25rem; height: 1.25rem; }

  /* ---- Häkchen: ordentliche 3er-Reihe statt umbrechender Zeile ----
     Jede Spalte: Häkchen oben, kurzer Text darunter (maximal zwei Zeilen).
     Gilt für die Startseite (die Sonderreinigung hat ihre eigene Liste
     mit Symbolen, Abschnitt 3b). */
  html.app .app-hero__trust {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 0.5rem;
  }
  html.app .app-hero__trust li {
    flex-direction: column;
    gap: 0.25rem;
    text-align: center;
    line-height: 1.25;
  }

  /* ---- Start: Hauptbutton allein in seiner Zeile, „Leistungen" als
     schlanker Textlink rechts unter den Häkchen (siehe Raster unten).
     Vorher standen zwei volle Buttons untereinander (116px), danach beide
     in einer Zeile. Der Hauptbutton bleibt das klare Ziel; „Leistungen" ist
     ein Nebenweg und schließt jetzt die Häkchen-Liste ab. */
  #v-start .app-hero__actions {
    /* aufgelöst, damit Button und Link getrennt im Hero-Raster stehen
       können — der Link wandert in die rechte Spalte */
    display: contents;
  }
  #v-start .app-hero__actions .btn--cta {
    grid-column: 1 / -1;
    grid-row: 4;
    justify-self: start;
    padding-inline: 1rem;
    white-space: nowrap;
    /* Luft nach oben (Einleitung) und nach unten (Bild/Häkchen). Der
       untere Abstand sitzt hier statt am Bild, damit Häkchen und Link
       genau mittig neben dem Bild stehen (siehe unten). */
    margin-top: clamp(1.75rem, 4.5vh, 2.75rem);
    margin-bottom: clamp(2rem, 5.5vh, 3.25rem);
  }
  #v-start .app-hero__actions .btn--ghost {
    grid-column: 2;
    grid-row: 6;
    align-self: start;
    /* mittig unter dem Häkchen-Block (der ist ebenfalls mittig gesetzt,
       siehe .app-hero__trust unten) */
    justify-self: center;
    /* Optik wie am Desktop: Rahmen, Pillenform, Navy-Schrift und der
       Hover-Effekt kommen unverändert aus .btn / .btn--ghost (style.css).
       Hier nur kleinerer Innenabstand und Schrift, damit der Button in die
       halbe Bildschirmbreite passt. */
    min-height: 44px;
    margin-top: 0.75rem;
    padding: 0.5rem 0.875rem;
    font-size: 0.8125rem;
    /* zweizeilig „Dienstleistungen / entdecken", beide Zeilen mittig; der
       Pfeil steht als eigenes Flex-Element rechts daneben und ist durch
       align-items:center (aus .btn) senkrecht mittig zu beiden Zeilen */
    text-align: center;
    line-height: 1.3;
    /* Von Anfang an im Hover-Look von .btn--ghost (style.css): hellblaue
       Fläche, blauer Rahmen, blaue Schrift — auf Touchscreens gibt es kein
       echtes Hover, der Button soll trotzdem sofort als Button erkennbar
       sein. Schrift --sky-700 auf --sky-50: 5,6:1 (WCAG AA). */
    --btn-bg: var(--sky-50);
    --btn-bd: var(--sky-400);
    --btn-fg: var(--sky-700);
  }
  /* Beim Drüberfahren/Antippen eine Spur kräftiger, damit der Druck
     sichtbar bleibt */
  #v-start .app-hero__actions .btn--ghost:hover,
  #v-start .app-hero__actions .btn--ghost:active {
    --btn-bg: var(--sky-100);
    --btn-bd: var(--sky-500);
    --btn-fg: var(--sky-800);
  }
  #v-start .app-hero__actions .btn--ghost::after {
    /* Alternativtext leer: Der Pfeil ist Deko und soll nicht als
       „Pfeil nach rechts" vorgelesen werden. Erste Zeile = Fallback für
       Browser ohne die „/ ''"-Syntax. */
    content: '→';
    content: '→' / '';
    margin-left: 0.125rem;
  }
  /* (Früher: eigene Verkleinerung für 320px-Handys. Seit die Schrift auf
     Handys mit der Breite skaliert, Abschnitt 3g-2, ist der Button dort
     automatisch im selben Verhältnis kleiner — die Sonderregel entfällt.) */
  /* Einblend-Staffelung: app.css animiert die Kinder der Textspalte — durch
     display:contents hätte die Button-Zeile keine eigene Box mehr und würde
     ohne Animation erscheinen. Deshalb Button und Link hier direkt, mit der
     Verzögerung, die die Zeile vorher hatte (4. Kind = 300ms). */
  html.app #v-start[data-enter='true'] .app-hero__actions > * {
    animation: app-enter 420ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
    animation-delay: 300ms;
  }

  /* ---- Start: Häkchen + Link rechts NEBEN dem Bild ----
     Bild (links) und eine Spalte aus Häkchen-Liste und „Leistungen"-Link
     (rechts) teilen sich die letzten beiden Zeilen. Technik wie bei der
     Sonderreinigung: Die Textspalte wird per display:contents aufgelöst,
     damit Eyebrow, H1, Einleitung und Button über die volle Breite laufen.

     Zentrierung ohne Extra-Element: Das Bild überspannt die Zeilen 5 und 6,
     die Häkchen hängen unten an Zeile 5, der Link oben an Zeile 6. Den
     Höhenüberschuss des Bildes verteilt das Raster gleichmäßig auf beide
     Zeilen — dadurch steht der Block aus Häkchen + Link genau mittig neben
     dem Bild. */
  #v-start .app-hero {
    /* halbe/halbe statt 1.1/1: Der Dienstleistungs-Button rechts braucht mit
       Rahmen und Innenabstand die paar Pixel mehr Breite */
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    column-gap: 0.75rem;
    row-gap: 0;
  }
  #v-start .app-hero > div:not(.app-hero__media) { display: contents; }
  #v-start .app-hero > div > * { grid-column: 1 / -1; }
  #v-start .app-hero > div > .eyebrow { grid-row: 1; }
  #v-start .app-hero > div > h1 { grid-row: 2; }
  /* Abstand Überschrift → Einleitung: einheitlich in Abschnitt 3g */
  #v-start .app-hero > div > .lead { grid-row: 3; }
  #v-start .app-hero__media {
    grid-column: 1;
    grid-row: 5 / span 2;
    align-self: center;
  }
  html.app #v-start .app-hero__trust {
    grid-column: 2;
    grid-row: 5;
    align-self: end;
    /* Block nur so breit wie sein Inhalt und mittig in der Spalte — so
       teilen sich Häkchen-Block und Link darunter dieselbe Mittelachse.
       fit-content statt max-content: Auf schmalen Handys darf der Text
       weiter umbrechen, statt aus der Spalte zu ragen. */
    justify-self: center;
    width: fit-content;
    max-width: 100%;
    grid-template-columns: 1fr;
    gap: 0.625rem;
    margin-top: 0;
    padding-top: 0;
    border-top: none;
  }
  html.app #v-start .app-hero__trust li {
    flex-direction: row;
    align-items: flex-start;
    gap: 0.375rem;
    text-align: left;
  }
  html.app #v-start .app-hero__trust li svg { margin-top: 0.0625rem; }

  /* ---- Sonderreinigung: rundes Porträt statt schmalem Hochformat ----
     Das Motiv ist hochformatig (761×1341) und stand hier als 98×173px-
     Streifen mit viel Leerraum links und rechts. Jetzt: ein rundes
     Porträt neben Eyebrow und Überschrift, wie ein Ansprechpartner-Badge.

     Layout: Die Textspalte wird per display:contents aufgelöst, damit ihre
     Kinder direkt im Hero-Grid stehen — Porträt links neben Eyebrow + H2,
     Einleitung, Buttons und Häkchen darunter über die volle Breite. Der
     aufgelöste <div> hat keine Semantik, es geht nichts verloren. */
  #v-spezialreinigung .app-hero {
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: 0.875rem;
    row-gap: 0;
    align-items: start;
  }
  #v-spezialreinigung .app-hero > div:not(.app-hero__media) { display: contents; }
  #v-spezialreinigung .app-hero > div > * { grid-column: 1 / -1; }
  #v-spezialreinigung .app-hero > div > .eyebrow {
    grid-column: 2;
    grid-row: 1;
    align-self: end;
  }
  #v-spezialreinigung .app-hero > div > h2 {
    grid-column: 2;
    grid-row: 2;
  }

  #v-spezialreinigung .app-hero__media {
    grid-column: 1;
    grid-row: 1 / span 2;
    align-self: center;
    position: relative;
    display: block;
    width: 4.25rem;
    height: 4.25rem;
    overflow: hidden;
    border-radius: 50%;
    background: radial-gradient(circle at 50% 30%, #fff 0%, var(--sky-100) 70%);
    box-shadow: 0 0 0 3px #fff, 0 6px 16px -6px rgba(12, 39, 64, 0.35);
  }
  /* Ausschnitt: Kopf und Schultern. Das Bild wird so vergrößert und
     verschoben, dass der Bereich x 170–530 / y 30–390 der Quelle (761×1341)
     den Kreis füllt — gemessen am Alphakanal, dort sitzt der Kopf. */
  html.app #v-spezialreinigung .app-hero__media img {
    position: absolute;
    left: -47%;
    top: -8%;
    width: 211%;
    max-width: none;
    height: auto;
    max-height: none;
    filter: none;
  }

  /* Zwei Buttons nebeneinander (je halbe Breite, Kurz-Beschriftung) statt
     untereinander — vorher 116px, jetzt eine Zeile. */
  #v-spezialreinigung .app-hero__actions {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.5rem;
  }
  #v-spezialreinigung .app-hero__actions .btn { padding-inline: 0.75rem; }
  /* Mehr Luft zwischen Einleitung und Buttons (vorher --app-gap ≈ 11px) */
  #v-spezialreinigung .app-hero__actions { margin-top: clamp(1.25rem, 3vh, 2rem); }

  /* Hero-Buttons flacher (54 → 44px), 44px bleibt die Mindest-Tippgröße */
  html.app .app-hero__actions .btn { padding-block: 0.5rem; min-height: 44px; }
}

/* --------------------------------------------------------------------------
   4. WhatsApp-Kontakt: schwebender Button unten rechts
   Liegt bewusst AUSSERHALB der App-Shell (direktes Kind von <body>, nicht
   im Grid), damit er unabhängig von Tabs, Notventil und Höhen-Budget immer
   erreichbar bleibt. `position: fixed` auf einem body-Kind bezieht sich auf
   den Viewport — unabhängig davon, dass html/body selbst `position: fixed`
   für den Kein-Scroll-Trick verwenden.
   -------------------------------------------------------------------------- */
/* AUSGESCHALTET (bewusst, Entscheidung offen): Der schwebende WhatsApp-Button
    wird in Variante 4 nicht angezeigt. Die Regel steht ganz am ENDE der Datei
    (siehe unten). */
.wa-float {
  position: fixed;
  right: max(1rem, env(safe-area-inset-right, 0px));
  /* Muss über der fixen Fußzeile schweben, sonst verdeckt er Impressum/
     Datenschutz (§ 5 DDG) — daher Footer-Höhe + Sicherheitsabstand, nicht
     einfach "1rem vom Rand". Ohne JS/App-Modus gibt es keinen Footer über
     dem Viewport-Rand, dann zählt nur der safe-area-Fallback. */
  bottom: calc(var(--app-footer-h, 0px) + 0.75rem + env(safe-area-inset-bottom, 0px));
  z-index: var(--z-toast);
  display: flex;
  align-items: center;
  gap: 0.5rem;
   padding: 0.6875rem 1.125rem;
   /* Originelles WhatsApp-Markengrün (#25d366) wie im WhatsApp-Logo. */
   background: var(--whatsapp);
   color: #fff;
  border-radius: var(--r-full);
  box-shadow: var(--sh-lg);
  text-decoration: none;
  line-height: 1.2;
  transition: transform 200ms var(--ease), box-shadow 200ms var(--ease),
              background-color 200ms var(--ease);
}
.wa-float:hover,
.wa-float:focus-visible {
  transform: translateY(-2px);
   /* Beim Hover um eine Schattstufe dunkleres Markengrün */
   background: #1ebe5d;
  box-shadow: 0 14px 26px -10px rgba(12, 39, 64, 0.28), var(--sh-lg);
}
.wa-float:focus-visible { outline: 2px solid var(--sky-600); outline-offset: 3px; }
.wa-float__icon {
  display: grid;
  place-items: center;
  flex: none;
  color: #fff;
}
/* Kein farbiger Kreis hinter der Glyphe: Auf hellem #25d366 käme das weiße
   Logo nur auf 1,98:1 und würde WCAG 1.4.11 (3:1 für grafische Objekte)
   verfehlen. Direkt auf dem dunklen Grund sind es 4,53:1. */
.wa-float__icon svg { width: 1.375rem; height: 1.375rem; }
.wa-float__text {
  font-family: 'Poppins', sans-serif;
  font-weight: 600;
  font-size: var(--fs-xs);
  max-width: 11rem;
}
/* Sehr schmale Geräte: Text weg, nur das Icon bleibt (aria über den
   umgebenden Link-Text, der weiterhin für Screenreader erhalten ist).
   Runder Button mit 44px Zielgröße (WCAG 2.5.8). */
@media (max-width: 359px) {
  .wa-float__text {
    position: absolute;
    width: 1px; height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
  }
  .wa-float { padding: 0.6875rem; }
}
/* Auf sehr flachen Fenstern etwas kompakter — der bottom-Abstand bleibt
   trotzdem an --app-footer-h gekoppelt (siehe oben), damit er die Fußzeile
   nie verdeckt. */
@media (max-height: 500px) {
  .wa-float { padding: 0.5rem 0.875rem; }
  .wa-float__icon svg { width: 1.25rem; height: 1.25rem; }
}

/* Im Kontakt-Tab steht WhatsApp bereits als reguläre Kontaktkachel im
   Formular — der schwebende Button wäre hier doppelt UND würde auf engen
   Viewports den Absende-Button überlagern. Deshalb dort ausblenden.
   :has() wird von allen aktuellen Browsern unterstützt (Chrome/Edge/Safari,
   Firefox ≥121); im unwahrscheinlichen Fall eines älteren Browsers bleibt
   der Button sichtbar — unschön, aber nicht blockierend. */
 html.app:has(#v-kontakt:not([hidden])) .wa-float {
   opacity: 0;
   pointer-events: none;
   transition: opacity 150ms var(--ease);
}

/* AUSGESCHALTET (bewusst, Entscheidung offen): Der schwebende
   WhatsApp-Button wird in Variante 4 nicht angezeigt. STEHT ABSICHTLICH
   LETZTE in der Datei — jede weitere .wa-float-Regel (auch alle
   Media-Queries oben) überschreibt sonst das `display: none`. Zum
   Aktivieren: diese Zeile wieder löschen, HTML und Stile bleiben erhalten. */
.wa-float { display: none; }
