/* ══════════════════════════ contact.css ══════════════════════════
   Kontakt-Sektion: Globus-Karte (Standort) + Formular mit Stateful-
   Button.

   Alle Farben, Radien, Abstände und Easings kommen aus tokens.css.
   In dieser Datei steht KEIN Hex-Wert und keine eigene Kurve. Was hier
   neu ist, ist Layout und Zustandslogik, nicht das Erscheinungsbild.

   Aufbau:
     1. Sektion und Raster
     2. Globus
     3. Formular
     4. Stateful-Button
     5. Breakpoints
     6. Reduzierte Bewegung
   ═══════════════════════════════════════════════════════════════ */

/* ── 1. Sektion und Raster ────────────────────────────────────────
   #contact überschreibt die provisorische Section-Regel aus global.css
   (Abschnitt 8). Gleiches Muster wie #about in about.css: Blockfluss
   statt vertikal zentriertem Flex, sonst würde die Sektion mittig
   "schweben" statt oben zu beginnen und nach unten zu wachsen. */
#contact{
    display:block;
    padding:calc(var(--nav-h) + var(--gutter) * 1.4) var(--gutter) calc(var(--gutter) * 1.6);
}

.contact{
    /* Breite der Textspalte. 520px ≈ 68 Zeichen in Inter, knapp unter
       --prose-max (62ch) für Fliesstext, passend für Eingabefelder. */
    --form-col:520px;
    --col-gap:clamp(24px,4vw,64px);
}

/* ── Volle-Breite-Raster ──────────────────────────────────────────
   Hier lag der Fehler mit der angeschnittenen Kugel, und er lag nicht
   bei der Kugel.

   Vorher rechnete ein --globe-bleed mit 100vw aus, wie weit das Bild
   über den Container hinausragen darf. 100vw ist aber die Breite des
   Fensters INKLUSIVE Scrollleiste, und global.css reserviert deren
   Platz dauerhaft (scrollbar-gutter:stable). Der Layoutbereich ist also
   rund 15px schmaler als 100vw, die Kugel ragte um genau diese 15px
   darunter, und overflow-x:clip auf <html> hat sie dort abgeschnitten.
   Es war eine senkrechte Kante an der Scrollleiste, kein Gestaltungsfehler
   an der Kugel: 15px, die bei einer 800px-Kugel wie ein Fehldruck aussehen.

   Statt die Scrollleistenbreite irgendwo herauszurechnen (sie ist je
   nach System und Overlay-Einstellung 0 bis 17px, in CSS nicht sicher
   bestimmbar) rechnet hier gar nichts mehr mit dem Fenster. Das Raster
   spannt die volle Breite mit benannten Linien, und die Kugel wird an
   `full-end` gehängt, also an die Innenkante des Elternelements. Die
   ist per Definition die des Layoutbereichs, mit oder ohne Scrollleiste.

   Die Spaltenfolge:
     full-start │ 1fr (min. --gutter)   Aussenrand, wächst mit
     content-start │ --form-col         Formular
                   │ --col-gap          Spalte
     globe-start   │ Rest bis --wrap-max Globus, Anfang
     content-end   │ 1fr (min. --gutter) Aussenrand, hier läuft die
     full-end                            Kugel hinein */
.contact-inner{
    display:grid;
    grid-template-columns:
          [full-start] minmax(var(--gutter),1fr)
          [content-start] minmax(0,var(--form-col))
          [form-end] var(--col-gap)
          [globe-start] minmax(0,calc(var(--wrap-max) - var(--form-col) - var(--col-gap)))
          [content-end] minmax(var(--gutter),1fr)
          [full-end];
    align-items:start;
    row-gap:var(--section-gap);
}

/* Überschrift und Einleitung bleiben in der Formularspalte, statt über
   die volle Inhaltsbreite zu laufen. Zwei Gründe, einer davon der
   Grund, warum oben rechts so viel Leere stand:

   Lief der Kopf über beide Spalten, begann die Kugel erst in Zeile 2,
   also unterhalb von Einleitungstext UND Überschrift. Rechts daneben
   blieb eine gute halbe Bildschirmhöhe leer, und die Kugel rutschte auf
   Höhe des dritten Eingabefelds.

   Ausserdem: Die Zeile bricht damit an derselben Kante wie die Felder
   darunter. Zwei verschiedene Kantenlängen übereinander lesen sich als
   Versehen. */
.contact-head{grid-column:content-start / form-end}

.contact-form{grid-column:content-start / form-end;grid-row:2}
.contact-aside{grid-column:content-start / form-end;grid-row:3}

/* Die Kugel läuft über ALLE Zeilen der linken Spalte, beginnt also ganz
   oben neben der Überschrift. Sie ist damit das, was man beim
   Hineinscrollen zuerst sieht, und die Sektion beginnt rechts und links
   auf derselben Höhe, statt oben rechts ein leeres Feld zu lassen.

   span 3 statt einer -1-Endlinie: Die Zeilen entstehen implizit, und auf
   implizite Zeilen zeigt -1 nicht. Kommt eine vierte Zeile dazu, muss
   die Zahl hier mit. */
.globe-figure{grid-column:globe-start / full-end;grid-row:1 / span 3}

.contact-title{
    position:relative;
    margin:0;
    padding-bottom:16px;
    font-family:'Space Grotesk',ui-sans-serif,system-ui,sans-serif;
    font-weight:600;
    /* Deutlich unter --hero-name-fs (max. 148px): Der Name im Hero ist die
       grösste Type der Seite und soll das bleiben. */
    font-size:clamp(34px,5.2vw,64px);
    line-height:1.04;letter-spacing:-.025em;
    color:var(--ink);
}
/* Derselbe rote Punkt wie in .brand: er ist das wiederkehrende
   Satzzeichen der Seite, nicht bloss Dekor an dieser Stelle. */
.contact-title .dot{color:var(--accent)}
/* Derselbe Signatur-Strich wie .about-title und .projects-title: roter
   Faden, der sich durch alle Haupttitel der Seite zieht. */
.contact-title::after{
    content:'';
    position:absolute;left:0;bottom:0;
    width:44px;height:3px;border-radius:2px;
    background:var(--accent);
}

.contact-lead{
    /* Keine eigene max-width mehr nötig: Der Kopf sitzt bereits in der
       Formularspalte, die Kante kommt vom Raster. Eine zweite Angabe hier
       wäre eine konkurrierende Quelle für dieselbe Kante. */
    margin:14px 0 0;
    color:var(--muted);
}

/* ── 2. Globus ────────────────────────────────────────────────────
   Kein Container, kein Rahmen, keine Fläche: Die Kugel steht direkt auf
   --bg und läuft rechts aus dem Bild. Eine Box würde sie zweimal
   begrenzen: einmal durch den Kartenrand, einmal durch ihren eigenen
   Umriss, und beides gleichzeitig ist eine Grenze zu viel.

   Damit fällt auch der dunkle Grund weg, den die frühere Karte hatte;
   der Globus wird dafür in js/contact/globe.js hell eingestellt
   (THEME = 'light'). Die beiden Stellen gehören zusammen: Wer hier
   wieder eine dunkle Fläche einführt, muss dort auf 'dark' schalten. */
.globe-figure{
    position:relative;
    margin:0;
    /* Spalte statt Blockfluss, damit die Bildunterschrift per order über
       die Kugel wandern kann (siehe .globe-meta unten). */
    display:flex;flex-direction:column;
    /* Keine Breitenangabe, keine negativen Aussenabstände: Die Spalte
       reicht bereits bis `full-end`. Negative Margins waren der Umweg,
       solange die Sektion in einem zentrierten Container sass. Sie sind
       mit dem Raster überflüssig geworden und wären jetzt eine zweite,
       konkurrierende Quelle für dieselbe Kante. */
}

/* Die Bühne reserviert das Quadrat, bevor irgendein WebGL-Kontext steht.
   Dieselbe Überlegung wie bei --hero-photo-ratio-num: kein
   Layoutsprung, wenn das Asset eintrifft (Cumulative Layout Shift).

   Die Obergrenze ist eine HÖHEN-Grenze, auch wenn sie als max-width
   dasteht: Die Bühne ist quadratisch, jeder Pixel Breite ist auch einer
   an Höhe. Ohne sie wächst die Kugel auf 1920px auf rund 970px und
   zieht die Sektion entsprechend in die Länge. Man scrollt dann an
   leerer Fläche neben dem Formular vorbei.

   margin-left:auto hält sie rechtsbündig an der Innenkante des Rasters,
   statt sie in der Spalte zu zentrieren. */
.globe-stage{
    position:relative;
    width:100%;max-width:820px;margin-left:auto;
    aspect-ratio:1;
}

.globe-canvas{
    display:block;width:100%;height:100%;
    cursor:grab;
    /* Ohne diese Zeile schluckt der Canvas auf dem Handy die vertikale
       Wischgeste, und die Seite lässt sich über der Kugel nicht mehr
       scrollen. pan-y gibt das Scrollen zurück und behält das Drehen. */
    touch-action:pan-y;
    opacity:0;
    transition:opacity .6s ease;
}
.globe-canvas.is-ready{opacity:1}
.globe-figure.is-dragging .globe-canvas{cursor:grabbing}

/* Fallback: kein WebGL, blockiertes CDN, Offline.
   Statt eines leeren Lochs eine gezeichnete Kugel mit Breitengraden und
   einem Pin. Sagt dasselbe aus, nur ohne Drehung. */
.globe-fallback{
    position:absolute;inset:0;display:none;
    border-radius:50%;
    background:
            repeating-linear-gradient(to bottom,transparent 0 13%,var(--hairline) 13% calc(13% + 1px)),
            radial-gradient(circle at 34% 30%,rgba(255,255,255,.9),transparent 62%),
            color-mix(in srgb,var(--ink) 6%,var(--bg));
    box-shadow:inset 0 0 0 1px var(--hairline);
}
.globe-figure.is-fallback .globe-fallback{display:block}
.globe-figure.is-fallback .globe-canvas{display:none}
/* Position des Pins: Zürich, grob auf der gezeichneten Kugel verortet. */
.globe-fallback::after{
    content:"";position:absolute;left:52%;top:33%;
    width:14px;height:14px;translate:-50% -50%;
    border-radius:50%;
    background:var(--accent);
    box-shadow:0 0 0 8px rgba(218,41,28,.16);
}

/* Bildunterschrift: steht ÜBER der Kugel, nicht darunter.

   Unter der Kugel hing sie in der Luft: Die Kugel endet mit einer
   Rundung, darunter kommt nichts mehr, und eine Textzeile im leeren Raum
   unter einer Kurve hat keine Kante, an der sie sich ausrichten könnte.
   Sie sah aus, als wäre sie heruntergefallen.

   Oben hat sie zwei: die Oberkante der Sektion und die Kugel darunter.
   Sie liegt damit auf einer Höhe mit dem „Kontakt"-Eyebrow links und
   liest sich als das, was sie ist: die Bildunterschrift zur Kugel,
   gesetzt wie eine Ortsmarke im Journalismus.

   order:-1 statt Umsortieren im HTML: Im Quelltext bleibt die
   figcaption dort, wo sie hingehört: nach dem Inhalt, den sie
   beschreibt. */
.globe-meta{
    order:-1;
    display:flex;flex-wrap:wrap;align-items:baseline;
    gap:6px 18px;
    max-width:820px;margin:0 0 20px auto;
    padding-right:var(--gutter);
    font-size:.86rem;color:var(--muted);
}
.globe-place{
    display:inline-flex;align-items:center;gap:9px;
    font-weight:500;letter-spacing:-.005em;
    color:var(--ink);
}
/* --accent als Fläche, nicht als Text, deshalb hier zulässig. */
.globe-place::before{
    content:"";flex:none;width:8px;height:8px;border-radius:50%;
    background:var(--accent);
}
.globe-time{
    /* Tabellenziffern: sonst zappelt die Uhr bei jedem Minutenwechsel. */
    font-variant-numeric:tabular-nums;
}
.globe-hint{margin-left:auto}
/* Der Hinweis „ziehen zum Drehen" gilt nur, wenn es etwas zu ziehen gibt. */
.globe-figure.is-fallback .globe-hint{visibility:hidden}


/* ── 3. Formular ──────────────────────────────────────────────────*/
.contact-form{
    display:flex;flex-direction:column;
    /* 16px zwischen den Feldern. Label und Feld gehören mit 7px eng
       zusammen, die Felder untereinander brauchen mehr, aber nur so viel,
       dass sie als eine Gruppe lesbar bleiben. */
    gap:16px;
}

.field{display:grid;gap:7px}

/* Zwei Felder nebeneinander. minmax(0,1fr) statt 1fr: Eine 1fr-Spalte
   schrumpft nicht unter die Mindestbreite ihres Inhalts, und ein Input
   bringt eine solche mit (size-Attribut, Standardwert 20 Zeichen). Ohne
   die 0 als Minimum sprengen die beiden Felder die Spalte, sobald es
   eng wird. */
.field-row{
    display:grid;
    grid-template-columns:repeat(2,minmax(0,1fr));
    gap:16px;
}
.field-label{
    font-size:.82rem;font-weight:500;letter-spacing:.01em;
    color:var(--muted);
}
.field-input,
.field-textarea{
    width:100%;
    padding:13px 15px;
    font:inherit;font-size:.97rem;
    color:var(--ink);
    background:var(--surface);
    border:1px solid var(--hairline);
    border-radius:var(--radius-input);
    transition:border-color .22s ease,box-shadow .22s ease;
}
.field-textarea{
    min-height:150px;
    /* Nur vertikal: horizontal liesse sich das Feld aus dem Raster ziehen. */
    resize:vertical;
    line-height:1.55;
}
.field-input::placeholder,
.field-textarea::placeholder{color:color-mix(in srgb,var(--muted) 55%,transparent)}

/* Der Fokusring kommt aus global.css (:focus-visible). Hier kommt nur
   der Rahmen dazu, damit auch Mausnutzende sehen, wo sie tippen. */
.field-input:focus,
.field-textarea:focus{
    border-color:var(--accent);
    box-shadow:0 0 0 3px rgba(218,41,28,.12);
}

.field-input[aria-invalid="true"],
.field-textarea[aria-invalid="true"]{border-color:var(--accent-ink)}
/* margin:0 ist hier keine Kosmetik: <p> bringt vom Browser rund 16px
   oben UND unten mit. Bei drei Feldern sind das ~96px unsichtbarer
   Abstand, der aussieht, als stünde er im Raster, und den man dann an
   der falschen Stelle sucht. */
.field-error{
    margin:0;
    min-height:1.15em;
    font-size:.78rem;color:var(--accent-ink);
}
.field-error:empty{min-height:0}

/* Honeypot. Nicht .vh benutzen: Screenreader sollen das Feld gar nicht
   erst finden, .vh würde es ihnen vorlesen. */
.field-trap{
    position:absolute;left:-9999px;
    width:1px;height:1px;overflow:hidden;
}

.form-foot{
    display:flex;flex-wrap:wrap;align-items:center;
    gap:14px 20px;
    margin-top:8px;
}
.form-status{
    margin:0;
    font-size:.86rem;color:var(--muted);
}
.form-status[data-tone="error"]{color:var(--accent-ink)}

.contact-aside{
    display:flex;flex-direction:column;
    gap:18px;
}
.form-direct{
    margin:0;
    font-size:.86rem;color:var(--muted);
}
.form-direct a{
    color:var(--accent-ink);
    text-decoration-color:var(--hairline);
    text-underline-offset:3px;
}
.form-direct a:hover{text-decoration-color:currentColor}

/* ── Profile ──────────────────────────────────────────────────────
   Umrisse statt Flächen, damit die Reihe neben dem gefüllten Absende-
   knopf nicht um Aufmerksamkeit konkurriert: Eine Sektion hat eine
   Hauptaktion, und das ist das Formular. */
.socials{
    display:flex;flex-wrap:wrap;gap:10px;
    margin:0;padding:0;list-style:none;
}
.social{
    display:grid;place-items:center;
    /* 44px erfüllt die WCAG-2.2-Empfehlung für Tippziele mit Reserve
       (SC 2.5.8 verlangt 24px). Bei Icons ohne Beschriftung ist die
       Fläche die einzige Trefferhilfe. */
    width:44px;height:44px;
    border:1px solid var(--hairline);border-radius:var(--radius-button);
    color:var(--muted);
    background:var(--surface);
    transition:color .22s ease,border-color .22s ease,transform .25s var(--settle);
}
.social svg{
    width:18px;height:18px;
    fill:none;stroke:currentColor;stroke-width:2;
    stroke-linecap:round;stroke-linejoin:round;
}
/* Dasselbe Anheben um 1px wie bei .nav-link: die Seite hat bereits eine
   Sprache für „hier kannst du klicken". */
.social:hover{
    color:var(--accent-ink);
    border-color:var(--accent);
    transform:translateY(-1px);
}

/* ── 4. Stateful-Button ───────────────────────────────────────────
   Vier Zustände in einem Element: idle → busy → done, mit fail als
   Abzweig. Alle vier Beschriftungen liegen dauerhaft im DOM, gestapelt
   auf derselben Rasterzelle. Dadurch gibt es keinen Moment, in dem der
   Button leer ist, und der Wechsel ist eine Überblendung statt eines
   Neuaufbaus.

   Die Breite: js/contact/statefulButton.js misst die vier Beschriftungen
   einmal nach dem Laden der Schrift und schreibt --sbtn-w-* zurück
   (dieselbe Mechanik wie die Wortmarkenmessung in js/nav.js). Läuft das
   JS nicht, greift der Fallback `auto`. Der Button ist dann so breit wie
   sein längster Text und springt nie. Kein hartkodiertes px, das bei
   anderer Sprache oder Ersatzschrift bricht. */
.sbtn{
    position:relative;
    display:grid;place-items:center;
    width:var(--sbtn-w,auto);
    min-width:0;height:52px;
    padding:0 24px;
    border:1.5px solid transparent;border-radius:var(--radius-button);
    background:var(--accent-ink);
    color:#fff;                              /* auf --accent-ink: 5.6:1 */
    font:inherit;font-size:.95rem;font-weight:500;letter-spacing:.01em;
    cursor:pointer;overflow:hidden;
    -webkit-tap-highlight-color:transparent;
    transition:
            width var(--dur-in) var(--morph),
            background-color .32s var(--morph),
            border-color .32s var(--morph),
            color .32s var(--morph),
            transform .14s ease;
}
.sbtn-face{
    grid-area:1/1;
    /* justify/align-self verhindern, dass die Rasterzelle die Beschriftung
       dehnt. Sonst misst das JS die Zellbreite statt der Textbreite. */
    justify-self:center;align-self:center;
    display:inline-flex;align-items:center;gap:9px;
    white-space:nowrap;
    opacity:0;transform:translateY(9px);
    pointer-events:none;
    transition:opacity .2s ease,transform .34s var(--settle);
}
.sbtn[data-state="idle"] .sbtn-face[data-face="idle"],
.sbtn[data-state="busy"] .sbtn-face[data-face="busy"],
.sbtn[data-state="done"] .sbtn-face[data-face="done"],
.sbtn[data-state="fail"] .sbtn-face[data-face="fail"]{
    opacity:1;transform:none;
}
.sbtn[data-state="idle"]{--sbtn-w:var(--sbtn-w-idle)}
.sbtn[data-state="busy"]{--sbtn-w:var(--sbtn-w-busy)}
.sbtn[data-state="done"]{--sbtn-w:var(--sbtn-w-done)}
.sbtn[data-state="fail"]{--sbtn-w:var(--sbtn-w-fail)}

.sbtn svg{
    width:16px;height:16px;flex:none;
    fill:none;stroke:currentColor;stroke-width:2;
    stroke-linecap:round;stroke-linejoin:round;
}

/* Erfolg NICHT in Grün: Die Palette hat kein Grün, und eines einzuführen
   hiesse, für eine Rückmeldung eine neue Farbe in die Marke zu holen.
   --ink liest sich als „abgeschlossen, hier passiert nichts mehr" und
   ist zugleich der stärkste Kontrastwechsel, den die Palette hergibt. */
.sbtn[data-state="done"]{background:var(--ink);border-color:var(--ink)}

/* Fehler ebenfalls nicht einfach rot: Rot ist hier die Ruhefarbe des
   Buttons. Der Zustand kippt stattdessen die Füllung: aus solide wird
   umrissen. Das ist auch ohne Farbwahrnehmung erkennbar (WCAG 1.4.1). */
.sbtn[data-state="fail"]{
    background:var(--surface);
    border-color:var(--accent);
    color:var(--accent-ink);
}

.sbtn:hover:not([data-state="busy"]){transform:translateY(-1px)}
.sbtn:active:not([data-state="busy"]){transform:scale(.98)}
.sbtn[data-state="busy"]{cursor:progress}
.sbtn[disabled]{opacity:1}   /* kein Ausgrauen: der Zustand steht im Text */

.sbtn-spinner{
    width:16px;height:16px;flex:none;
    border-radius:50%;
    border:2px solid rgba(255,255,255,.32);
    border-top-color:#fff;
    animation:sbtn-spin .72s linear infinite;
}
@keyframes sbtn-spin{to{rotate:360deg}}

/* ── 5. Breakpoints ───────────────────────────────────────────────
   Eigene Grenze bei 980px, bewusst weder 640 noch 1240: Dieses Layout
   kippt aus einem eigenen Grund: darunter bleibt neben der 520px
   breiten Formularspalte keine Restbreite mehr, in der eine Kugel noch
   als Kugel wirkt. Sie würde zum Icon schrumpfen, und dafür lohnt der
   WebGL-Kontext nicht.

   Gestapelt bleiben die Spalten des Rasters bestehen, nur die
   Zuordnung ändert sich: Text und Formular auf den Inhaltsbereich,
   die Kugel von Kante zu Kante. Kein zweites Raster, keine negativen
   Aussenabstände: dieselben benannten Linien, andere Enden. */
@media (max-width:980px){
    /* Formular zuerst, dann die Kugel. Wer bis hierher scrollt, will
       schreiben; der Standort erklärt, wohin die Nachricht geht, und das
       ist eine Fussnote, keine Hürde.
       grid-row:auto löst die feste Zeilenzuordnung des Desktops: Dort
       läuft die Kugel über beide Zeilen, gestapelt braucht jedes Element
       seine eigene. */
    .contact-head,
    .contact-form,
    .contact-aside{grid-column:content-start / content-end;grid-row:auto}
    .globe-figure{grid-column:full-start / full-end;grid-row:auto}

    /* Volle Breite ausnutzen: Die Deckelung auf 820px ist eine
       Desktop-Grenze gegen zu viel Höhe. Auf schmalen Fenstern IST die
       Fensterbreite die Grenze. */
    .globe-stage{max-width:none;margin-inline:auto}
    .globe-meta{
        max-width:none;
        margin-inline:0;
        padding-inline:var(--gutter);
    }
    .globe-hint{margin-left:0}
}

@media (max-width:640px){
    /* Nebeneinander blieben von jedem Feld ~150px: zu wenig, um eine
       E-Mail-Adresse beim Tippen im Blick zu behalten. */
    .field-row{grid-template-columns:1fr}
    .sbtn{width:100%;--sbtn-w:100%!important}
    .form-foot{gap:12px}
}

/* ── 6. Reduzierte Bewegung ───────────────────────────────────────
   global.css setzt alle Animationen auf .01ms, inklusive dieses
   Spinners, der damit als eingefrorener Kreisbogen stehenbliebe und
   nichts mehr anzeigte. Ein Ladeindikator, der nicht anzeigt, ist ein
   Fehler, kein Kompromiss.

   Die Klasse (0,1,0) schlägt den Universalselektor (0,0,0) auch bei
   !important. Genau der Weg, den der Kommentar in global.css für
   solche Fälle vorsieht. Ersetzt wird die Drehung durch reines
   Auf- und Abblenden: laut derselben Stelle gilt opacity nicht als
   problematische Bewegung.

   Die Globus-Rotation schaltet js/contact/globe.js separat ab; sie ist
   eine Canvas-Zeichnung und wird von CSS gar nicht erreicht. */
@media (prefers-reduced-motion:reduce){
    .sbtn-spinner{
        border-top-color:rgba(255,255,255,.32);
        animation:sbtn-fade 1.5s ease-in-out infinite!important;
        animation-duration:1.5s!important;
    }
    .sbtn[data-state="fail"] .sbtn-spinner{border-top-color:currentColor}
}
@keyframes sbtn-fade{50%{opacity:.3}}
