const {useState,useEffect,useMemo,useRef} = React;
const P=window.MG_P, W=window.MG_W;

const KAT=[
 ['Gesichtsbehandlungen','beh-gesicht.jpg','Schöne Haut ist kein Zufall – sie ist Pflege, die bleibt.',[
  ['Kosmetische Grundbehandlung','90 Min.',85,'Reinigung, Hautanalyse, Peeling, Enzym-Peeling, Tiefenreinigung, Wirkstoffampulle, Gesichts- & Dekolleté-Massage, Spezialmaske – inklusive Abschlusspflege.'],
  ['Basis Kurzbehandlung','60 Min.',75,'Die kompakte Gesichtsbehandlung: Reinigung, Hautanalyse, Peeling, Enzym-Peeling, Tiefenreinigung, Spezialampulle mit Maske.'],
  ['Comfort Männerbehandlung','90 Min.',85,'Speziell auf Männerhaut abgestimmt: Reinigung, Hautanalyse, Peeling, Enzym-Peeling, Tiefenreinigung, Spezialampulle mit Maske.'],
  ['Akne ade (bis 18 Jahre)','45 Min.',65,'Für junge Haut: Reinigung, Hautanalyse, Enzym-Peeling, Tiefenreinigung, Wirkstoffkonzentrat mit Spezialmaske und Abschlusspflege.'],
  ['Frische Kick Behandlung','45 Min.',65,'Der schnelle Glow: Reinigung, Peeling, Enzym-Peeling und Feuchtigkeitsmaske mit Abschlusspflege.'],
  ['Augenbehandlung','30 Min.',35,'Belebend-kühlende Modellage für die zarte Augenpartie. Strafft, glättet Linien und reduziert Müdigkeitsspuren.'],
  ['Manuelle Lymphdrainage','45 Min.',55,'Gesicht & Dekolleté: aktiviert den Lymphabfluss, entstaut das Gewebe und sorgt für ein frisches Hautbild.']]],
 ['Luxusbehandlungen','studio-5375.jpg','Zwei Masken, zwei Massagen – und 120 Minuten nur für dich.',[
  ['Soin Mille Lumière','120 Min.',155,'Unsere luxuriöseste Anti-Aging-Gesichtsbehandlung mit zwei Massagen und zwei Masken. Regeneriert, strafft und verleiht unvergleichliche Ausstrahlung.'],
  ['Maria Galland Lift Expert','90 Min.',110,'100 % manuelle Gesichtsbehandlung für sichtbare Festigung und Definition der Konturen.'],
  ['Masque Modelant','105 Min.',100,'Klassische Gesichtsbehandlung mit selbst erwärmender Mineralienmaske. Vitalisiert und sorgt für einen natürlich gelifteten Teint.']]],
 ['Mikrodermabrasion','beh-microderm.jpg','Feine Abtragung, große Wirkung: dein Teint wirkt sofort ebenmäßiger.',[
  ['Economy Behandlung','ca. 60 Min.',75,'Reinigung, Enzym-Peeling, Microdermabrasion, Wirkstoffampulle, Abschlusspflege.'],
  ['Business Behandlung','ca. 90 Min.',100,'Wie Economy, zusätzlich mit Tiefenausreinigung und Feuchtigkeitsmaske.'],
  ['Comfort Behandlung','ca. 120 Min.',110,'Das volle Programm inklusive Tiefenausreinigung, Dekolleté- und Spezialmaske.']]],
 ['Microneedling & BB Glow','beh-microneedling.jpg','Kollagen entsteht nicht über Nacht – aber heute fängt es an.',[
  ['Microneedling Gesicht, Hals & Dekolleté','ca. 120 Min.',150,'Feinste Mikroimpulse aktivieren die Zellerneuerung und regen Kollagen & Elastin an.'],
  ['BB Glow Gesicht','ca. 60 Min.',100,'Die sanfte Anti-Aging-Behandlung für strahlende, ebenmäßige Haut.'],
  ['BB Glow Gesicht, Hals & Dekolleté','ca. 90 Min.',130,'BB Glow für das große Areal in einer Sitzung.'],
  ['BB Glow 3er-Paket','3 Termine',280,'Drei Behandlungen im Paket – für ein länger anhaltendes Ergebnis zum Vorteilspreis.']]],
 ['Rücken & Körper','studio-5397.jpg','Der Rücken bekommt selten Aufmerksamkeit – hier schon.',[
  ['Rückenbehandlung','ca. 60 Min.',65,'Gegen akute Hautunreinheiten am Rücken: Reinigung, Peeling, Bedampfung und Tiefenausreinigung.'],
  ['Rückenbehandlung mit Mikrodermabrasion','ca. 75 Min.',85,'Die Rückenbehandlung, ergänzt um eine Mikrodermabrasion für ein feineres Hautbild.']]],
 ['Wimpern & Brauen','beh-wimpern-hero.jpg','Ein wacher Blick sagt mehr als tausend Worte.',[
  ['Wimpernverlängerung Erstbehandlung','ca. 120 Min.',140,'Wimper-für-Wimper-Technik für einen natürlich vollen Augenaufschlag.'],
  ['Refill','ca. 50 Min.',58,'Auffüllen der Wimpernverlängerung – empfohlen alle 2–4 Wochen.'],
  ['Refill lang','ca. 75 Min.',63,'Ausführliches Refill bei längerer Pause seit der letzten Behandlung.'],
  ['Büscheltechnik','ca. 50 Min.',73,'Kleine Wimpernbüschel statt einzelner Wimpern – schneller aufgebaut, sichtbar voller.'],
  ['Wimpernlifting','ca. 75 Min.',78,'Natürlicher Schwung ohne Extensions – inklusive Färben.'],
  ['Wimpern entfernen','ca. 30 Min.',33,'Schonendes, vollständiges Ablösen der Extensions ohne die eigenen Wimpern zu strapazieren.'],
  ['Wimpern färben','15 Min.',23,'Intensiviert helle Wimpern für einen wachen Blick.'],
  ['Augenbrauen färben','15 Min.',14,'Intensiviert helle Brauen für mehr Tiefe und Definition.','ab 14 €'],
  ['Augenbrauen zupfen','15 Min.',14,'Präzise Form, abgestimmt auf deine Gesichtszüge.','ab 14 €']]],
 ['Make-up','hf-makeup.jpg','Das beste Make-up lässt dich aussehen wie dich – nur ausgeschlafener.',[
  ['Kleines Make-up nach der Behandlung','ca. 20 Min.',25,'Der dezente Feinschliff direkt im Anschluss an deine Kosmetikbehandlung – in der Behandlung selbst nicht enthalten.'],
  ['Tages Make-up','30 Min.',47,'Frisches, natürliches Make-up für den Tag – typgerecht abgestimmt.'],
  ['Abend Make-up','45 Min.',67,'Ausdrucksstarkes Make-up für besondere Anlässe.'],
  ['Make-up Beratung','60 Min.',78,'Persönliche Beratung mit Übungsteil zum Selbermachen.']]],
 ['Depilation mit Wachs','hf-waxing-bienenwachs.jpg','Glatte Haut für Wochen – ausschließlich mit Bienenwachs.',[
  ['Oberlippe','ca. 15 Min.',15,'Schnell und präzise – in der Mittagspause erledigt.'],
  ['Kinn','ca. 15 Min.',15,'Gründliche Entfernung feiner wie kräftiger Härchen.'],
  ['Wangen','ca. 15 Min.',16,'Für einen ebenmäßigen, weichen Teint.'],
  ['Paketpreis Gesicht','ca. 30 Min.',35,'Oberlippe, Kinn und Wangen in einem Termin – günstiger als einzeln.'],
  ['Achseln','ca. 15 Min.',30,'Zügig und gründlich, mit beruhigender Pflege im Anschluss.'],
  ['Unterarme','ca. 15 Min.',30,'Gleichmäßig glatte Arme für mehrere Wochen.'],
  ['Unterschenkel','ca. 30 Min.',40,'Vom Knie abwärts – der Klassiker für den Sommer.'],
  ['Beine komplett','ca. 45 Min.',63,'Ober- und Unterschenkel in einem Durchgang.'],
  ['Bikinizone (nur für Frauen)','ca. 30 Min.',35,'Saubere Kante entlang der Bikinilinie.'],
  ['Bikinizone komplett (nur für Frauen)','ca. 45 Min.',55,'Die vollständige Enthaarung der Intimzone.'],
  ['Rücken','ca. 15 Min.',45,'Auch die Stellen, an die du selbst nicht kommst.'],
  ['Brust','ca. 15 Min.',45,'Gründlich und mit beruhigender Nachpflege.'],
  ['Paketpreis Rücken & Brust','ca. 30 Min.',70,'Beide Zonen in einem Termin – günstiger als einzeln.']]],
 ['Hände & Nägel','beh-manikuere.jpg','Gepflegte Hände sind deine schönste Visitenkarte.',[
  ['Maniküre','45 Min.',45,'Formen, Nagelhautpflege und Abschluss mit pflegendem Öl.'],
  ['Maniküre intensiv','60 Min.',51,'Ausführliche Maniküre mit zusätzlicher Pflege für beanspruchte Hände.'],
  ['Wellnessmaniküre mit Parfümbad','60 Min.',55,'Maniküre plus entspannendes Parfümbad und Handmassage.'],
  ['Shellac','60 Min.',55,'Langanhaltender Glanz, der 2–3 Wochen perfekt hält.'],
  ['Naturnagelverstärkung','60 Min.',55,'Stabilisiert weiche und brüchige Nägel – ganz ohne Kunstnagel.'],
  ['Shellac lösen','ca. 15 Min.',30,'Schonendes Ablösen ohne den Naturnagel anzugreifen.'],
  ['Reparatur des Nagels','ca. 15 Min.',9,'Ein eingerissener Nagel wird stabilisiert und wieder in Form gebracht.']]],
 ['Fußpflege','beh-fusspflege.jpg','Verwöhne die Füße, die dich jeden Tag tragen.',[
  ['Pediküre','45 Min.',46,'Fußbad, Hornhautentfernung, Nagel- und Nagelhautpflege.'],
  ['Pediküre mit Lack','60 Min.',62,'Pediküre inklusive Lackierung in deiner Wunschfarbe.'],
  ['French Pediküre mit Lack','60 Min.',62,'Pediküre mit klassischem French-Finish in Lack.'],
  ['French Pediküre mit Gel','60 Min.',62,'French-Finish in Gel – besonders lange haltbar.'],
  ['Gel-/Carbonlack Pediküre','60 Min.',62,'Robuster Gel- oder Carbonlack für dauerhaften Glanz.'],
  ['Wellnesspediküre','75 Min.',57,'Pediküre mit zusätzlicher Maske und entspannender Fußmassage.'],
  ['Fußmassage','ca. 15 Min.',20,'Kurze, wohltuende Massage – einzeln oder im Anschluss.']]]];

/* Add-ons: 4. Feld = Kategorien, in denen das Add-on angeboten wird
   (die Preisliste nennt diese Preise "innerhalb einer Kosmetikbehandlung"). */
const KOSMETIK=['Gesichtsbehandlungen','Luxusbehandlungen','Mikrodermabrasion','Microneedling & BB Glow','Rücken & Körper'];
/* 5. Feld = ZUSATZZEIT in Minuten. Bis 26.08.2026 hatten Add-ons nur einen
   Preis — eine 90-Minuten-Behandlung mit Wimpernfärben blockte im Kalender
   trotzdem nur 90 Minuten, und der Folgetermin lag zu früh.

   Die Zeiten stammen aus Svetlanas Sprachnachricht vom 26.08.2026:
   "wenn man zum Beispiel Wimpern färben bucht, dann müssen wir vielleicht
    15 Minuten nur zusammen buchen. Oder wenn man Wimpern färben, Augenbrauen
    färben und Zupfen bucht, dann brauchen wir mehr oder weniger eine halbe
    Stunde noch dazu … dann ist es ja dann 120 Minuten zusammen."

   Daraus: Wimpern färben allein 15, alle drei zusammen 30. Die Aufteilung
   15/10/5 erfüllt beide Aussagen — Zupfen geht am schnellsten, Färben
   braucht Einwirkzeit. Bei 15-Minuten-Takt und 15 Minuten Puffer ist das
   fein genug; wenn Svetlana es anders aufteilt, sind es drei Zahlen hier. */
const ADDONS=[
 ['Mikrodermabrasion als Add-on',40,'Sanfte Abtragung der obersten Hautschicht – direkt in deine Behandlung integriert.',['Gesichtsbehandlungen','Luxusbehandlungen'],15],
 ['Kleines Make-up danach',25,'Der dezente Feinschliff direkt im Anschluss – du gehst fertig gestylt aus dem Studio.',KOSMETIK,20],
 ['Wimpern färben',19,'Intensiviert helle Wimpern während deiner Behandlung – ohne Extra-Termin.',KOSMETIK,15],
 ['Augenbrauen färben',13,'Mehr Tiefe und Definition – im Anschluss an deine Behandlung.',KOSMETIK,10],
 ['Augenbrauen zupfen',13,'Präzise Form, direkt mitgemacht.',KOSMETIK,5],
 ['Oberlippe oder Kinn mit Wachs',14,'Kleine Depilation innerhalb der Kosmetikbehandlung – günstiger als einzeln.',KOSMETIK,15],
 ['Nagelreparatur',8,'Ein eingerissener Nagel wird während der Behandlung mitrepariert.',['Hände & Nägel','Fußpflege'],15]];
const STAFF=[['Egal','Nächste freie Mitarbeiterin'],['Svetlana','Inhaberin · seit 2004'],['Eugenia','Kosmetikerin · seit 2006']];
/* Die frueher hier fest verdrahtete Slot-Liste ist entfallen: das
   Zeitraster kommt seit 26.08.2026 vom Server (/api/frei, db/013) und
   richtet sich nach Oeffnungszeiten, Puffer und Behandlungsdauer. */
const STEPS=['Behandlung','Termin','Deine Daten','Bestätigung'];

/* --- Anzahlung ----------------------------------------------------------
   Bei JEDER Online-Buchung werden ANZ_BETRAG € im Voraus gezahlt (Ansage
   der Inhaberin vom 24.08.2026 — vorher galt eine 100-€-Schwelle).
   Rechtlich ist das eine Vorauszahlung auf den Behandlungspreis, KEINE
   "Reservierungsgebühr" – die Bezeichnung ändert nach § 306a BGB ohnehin
   nichts. Der Einbehalt bei No-Show ist pauschalierter Schadensersatz und
   braucht den Gegenbeweisvorbehalt aus § 309 Nr. 5 BGB. */
const ANZ_BETRAG=25;
const anzPflicht=summe=>summe>0;

/* --- Willkommensgutschein (?code=LAVIE10-XXXX) --------------------------
   Kommt aus der Newsletter-Willkommens-Mail. Die Anzeige hier ist nur
   Vorschau — ob der Code wirklich gilt (eigener Code, unbenutzt, ERSTE
   Buchung), entscheidet der Server bei der Buchung; er rechnet den Preis
   selbst und weist das Ergebnis in der Bestätigungs-Mail aus. */
const GUTSCHEIN_CODE=(()=>{try{const c=new URLSearchParams(location.search).get('code')||'';
  return /^[A-Z0-9-]{6,20}$/i.test(c)?c.toUpperCase():null}catch(e){return null}})();
/* Quelle der Wahrheit ist das Gutscheincode-FELD (Schritt 3) — der URL-Code
   füllt es nur vor. So kann die Kundin den Code auch von Hand eintragen
   oder korrigieren, und Anzeige wie Buchung nutzen denselben Wert. */
const codeNorm=c=>{const x=(c||'').trim().toUpperCase();return /^[A-Z0-9-]{6,20}$/.test(x)?x:null};
const rabattWert=(s,code)=>code?Math.round(s*0.9*100)/100:s;
const eurF=n=>n.toFixed(2).replace('.',',').replace(',00','');

/* Zitate der Kundin fuer die Wartescreens. `a` = Urheberin, wird als
   Eyebrow unter dem Zitat gesetzt. Die Auswahl passiert EINMAL beim Start
   des jeweiligen Screens (State in MgBuchung) – wuerde man im Render
   wuerfeln, wechselte das Zitat bei jedem Re-Render mitten in der
   Einblendung. */
const ZITATE=[
 {t:'Deine Haut – das einzig wahre Accessoire, das du ein Leben lang trägst. Pflege, was dich täglich begleitet.'},
 {t:'Kosmetik ist die Kunst, die Geburtsurkunde zu dementieren.',a:'Olga Tschechowa'}];
const waehle=arr=>arr[Math.floor(Math.random()*arr.length)];

/* --- Einwilligungen -----------------------------------------------------
   Ein Kanal = eine eigene Checkbox. Gebündelte Werbe-Einwilligungen sind
   nicht "spezifisch" im Sinne von Art. 4 Nr. 11 DSGVO. Keine Box ist
   vorausgewählt (EuGH "Planet49"). Der Wortlaut wird mitprotokolliert,
   damit später beweisbar ist, wofür genau zugestimmt wurde (Art. 7 I DSGVO). */
const CONSENT_VERSION='2026-08-16.1';
const KANAELE=[
 ['m_email','E-Mail','Angebote, Pflegetipps und Aktionen – bestätigt per Double-Opt-in.'],
 ['m_sms','SMS','Kurze Nachrichten zu kurzfristig frei gewordenen Terminen und Aktionen.'],
 ['m_wa','WhatsApp','Angebote und Neuigkeiten per WhatsApp – zusätzlich im Chat zu bestätigen.']];
const TXT_WERBUNG=k=>{const f=KANAELE.find(x=>x[0]===k);return f?'Ich willige ein, Werbung von La Vie per '+f[1]+' zu erhalten ('+f[2]+'). Widerruf jederzeit und kostenfrei möglich.':''};
const TXT_GESUND='Ich willige ausdrücklich ein, dass La Vie meine Angaben zu Haut, Allergien und Gesundheit zur Vorbereitung und Durchführung der Behandlung verarbeitet (Art. 9 Abs. 2 lit. a DSGVO).';
/* Die Ausfallpauschale hängt an der Anzahlung: Sie darf den typischen Schaden
   nicht übersteigen (§ 309 Nr. 5 lit. a BGB). Bei einer 30-€-Behandlung wären
   25 € unverhältnismäßig – dort wird deshalb nichts pauschaliert. */
const TXT_STORNO=anz=>anz
 ? 'Ich habe die Buchungs- und Stornobedingungen gelesen und akzeptiere sie, einschließlich der Anzahlung von '+ANZ_BETRAG+' € und der Ausfallregelung bei Absagen weniger als 24 Stunden vor dem Termin.'
 : 'Ich habe die Buchungs- und Stornobedingungen gelesen und akzeptiere sie und sage meinen Termin rechtzeitig ab, falls ich ihn nicht wahrnehmen kann.';
const TXT_DS='Ich habe die Datenschutzerklärung zur Kenntnis genommen.';

/* --- Datum ---------------------------------------------------------------
   `tag` ist seit 26.08.2026 ein ISO-Datum 'YYYY-MM-DD' (vorher ein
   toDateString()-Schlüssel). Grund: es geht so, wie es ist, an /api/frei und
   in die Buchung — kein Hin- und Herwandeln, keine Zeitzonen-Überraschung.
   dParse baut daraus ein LOKALES Date; `new Date('2026-09-01')` wäre UTC und
   läge je nach Zeitzone einen Tag daneben. */
const isoVon=d=>{const p=n=>String(n).padStart(2,'0');return d.getFullYear()+'-'+p(d.getMonth()+1)+'-'+p(d.getDate())};
const dParse=s=>{const[j,m,t]=String(s||'').split('-').map(Number);return new Date(j||1970,(m||1)-1,t||1)};
const minUhr=z=>{const[a,b]=String(z||'0:0').split(':').map(Number);return (a||0)*60+(b||0)};
const fmt=d=>d.toLocaleDateString('de-DE',{weekday:'long',day:'numeric',month:'long'});
const fmtS=d=>d.toLocaleDateString('de-DE',{weekday:'short',day:'numeric',month:'short'});
const fmtISO=s=>fmt(dParse(s));
const fmtISOS=s=>fmtS(dParse(s));

/* --- Monatskalender ------------------------------------------------------
   Ansage der Inhaberin: "Richtiges Kalendar in Buchungssystem auch auf
   webiste, sodass sie auch den ganzen Jahr buchen Können. Also nicht nur
   1 woche zeitraum." Vorher zeigte der Funnel sieben Tage ab morgen.

   Der Raster beginnt montags (deutsche Konvention; getDay() liefert 0 = So). */
const WOCHENTAGE=['Mo','Di','Mi','Do','Fr','Sa','So'];
const MONATSNAMEN=['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'];
const monatsRaster=(jahr,monat)=>{
  const erster=new Date(jahr,monat,1);
  const versatz=(erster.getDay()+6)%7;          // Mo=0 … So=6
  const tage=new Date(jahr,monat+1,0).getDate();
  const zellen=[];
  for(let i=0;i<versatz;i++)zellen.push(null);
  for(let t=1;t<=tage;t++)zellen.push(new Date(jahr,monat,t));
  return zellen;
};

function Steps({step}){
  return <div className="mg-steps" style={{display:'flex',borderBottom:'1px solid var(--line)',marginBottom:'clamp(30px,4vw,54px)'}}>
    {STEPS.map((l,i)=><div key={l} className="mg-step" data-l={l} data-cur={i===step?'1':'0'} style={{display:'flex',alignItems:'center',gap:10,padding:'0 clamp(14px,2vw,28px) 16px',borderBottom:'1px solid '+(i===step?'var(--ink)':'transparent'),marginBottom:-1,opacity:i>step?.45:1,transition:'opacity .4s var(--ease),border-color .4s var(--ease)'}}>
      <span style={{fontSize:12,letterSpacing:'.14em',color:i<=step?'var(--ink)':'var(--ink-mute)'}}>{String(i+1).padStart(2,'0')}</span>
      <span className="mg-step-l" style={{fontSize:14.5,color:i<=step?'var(--ink)':'var(--ink-mute)'}}>{l}</span>
      <span className="mg-step-cur" style={{display:'none',fontSize:13.5,marginLeft:8}}>{l}</span>
    </div>)}
  </div>;
}

function Step1({sel,setSel,addons,setAddons,persons,setPersons}){
  const [open,setOpen]=useState(0);
  const [info,setInfo]=useState('');
  return <div>
    <div style={{borderTop:'1px solid var(--line)'}}>
      {KAT.map(([kat,img,quote,items],ki)=><div key={kat} style={{borderBottom:'1px solid var(--line)'}}>
        <button onClick={()=>setOpen(open===ki?-1:ki)} style={{width:'100%',display:'flex',alignItems:'center',justifyContent:'space-between',gap:16,background:'none',border:'none',cursor:'pointer',padding:'22px 2px',fontFamily:'Jost,sans-serif',fontSize:18,fontWeight:300,color:'var(--ink)',textAlign:'left'}}>
          <span style={{display:'flex',alignItems:'center',gap:16}}>
            <img loading="lazy" src={P+img} alt="" style={{width:52,height:52,objectFit:'cover',flexShrink:0}}/>
            <span>{kat}<span style={{display:'block',fontSize:12.5,color:'var(--ink-mute)',marginTop:3}}>{items.length} Behandlungen · ab {Math.min(...items.map(x=>x[2]))} €</span></span>
          </span>
          <span style={{fontSize:20,transform:open===ki?'rotate(45deg)':'none',transition:'transform .4s var(--ease)'}}>+</span>
        </button>
        <div style={{display:'grid',gridTemplateRows:open===ki?'1fr':'0fr',transition:'grid-template-rows .45s var(--ease)'}}><div style={{overflow:'hidden'}}>
          <div style={{paddingBottom:14,opacity:open===ki?1:0,transition:'opacity .4s var(--ease)'}}>
            {items.map(([n,dur,pr,desc,plabel])=>{const on=sel&&sel.name===n;const io=info===n;
            return <div key={n} onClick={()=>{setSel({name:n,dur,preis:pr,kat,img,quote,plabel});setAddons(a=>a.filter(x=>{const ad=ADDONS.find(y=>y[0]===x);return !ad||!ad[3]||ad[3].includes(kat)}))}} style={{cursor:'pointer',padding:'16px 18px',marginBottom:8,background:on?'var(--paper-2)':'transparent',border:'1px solid '+(on?'var(--ink)':'var(--line)'),transition:'border-color .3s var(--ease),background .3s var(--ease)'}}>
              <div style={{display:'flex',alignItems:'center',justifyContent:'space-between',gap:14}}>
                <span style={{display:'flex',alignItems:'center',gap:10,fontSize:15.5}}>
                  <span style={{width:15,height:15,borderRadius:'50%',border:'1px solid '+(on?'var(--ink)':'var(--ink-mute)'),display:'inline-flex',alignItems:'center',justifyContent:'center',flexShrink:0}}>{on&&<span style={{width:7,height:7,borderRadius:'50%',background:'var(--ink)'}}></span>}</span>
                  {n}
                  <button onClick={e=>{e.stopPropagation();setInfo(io?'':n)}} title="Details" style={{width:20,height:20,borderRadius:'50%',border:'1px solid var(--line)',background:io?'var(--ink)':'transparent',color:io?'#fff':'var(--ink-mute)',cursor:'pointer',fontSize:11,lineHeight:1,flexShrink:0,fontFamily:'Jost,sans-serif'}}>i</button>
                </span>
                <span style={{fontSize:14,color:'var(--ink-mute)',whiteSpace:'nowrap'}}>{dur} · <span style={{color:'var(--ink)'}}>{plabel||pr+' €'}</span></span>
              </div>
              <div style={{display:'grid',gridTemplateRows:io?'1fr':'0fr',transition:'grid-template-rows .35s var(--ease)'}}><div style={{overflow:'hidden'}}>
                <p className="mg-p" style={{fontSize:13.5,padding:'10px 0 2px 25px',margin:0,maxWidth:560}}>{desc}</p>
              </div></div>
            </div>;})}
          </div>
        </div></div>
      </div>)}
    </div>
    {sel&&<div style={{marginTop:34}}>
      <div className="mg-eyebrow">Ergänzen</div>
      <div style={{marginTop:14,display:'flex',flexDirection:'column',gap:10}}>
        {ADDONS.filter(([,,,kats])=>!kats||kats.includes(sel.kat)).map(([n,pr,desc])=>{const on=addons.includes(n);
        return <label key={n} style={{display:'flex',gap:12,alignItems:'flex-start',padding:'15px 18px',border:'1px solid '+(on?'var(--ink)':'var(--line)'),cursor:'pointer',transition:'border-color .3s var(--ease)'}}>
          <input type="checkbox" checked={on} onChange={e=>setAddons(e.target.checked?[...addons,n]:addons.filter(a=>a!==n))} style={{accentColor:'var(--ink)',marginTop:3}}/>
          <span style={{flex:1}}><span style={{fontSize:15}}>{n}</span><span className="mg-p" style={{display:'block',fontSize:13,marginTop:3}}>{desc}</span></span>
          <span style={{fontSize:14,whiteSpace:'nowrap'}}>+{pr} €</span>
        </label>;})}
      </div>
      <div className="mg-eyebrow" style={{marginTop:30}}>Für wie viele Personen?</div>
      <div style={{display:'flex',gap:10,marginTop:14,flexWrap:'wrap'}}>
        {[1,2,3].map(p=><button key={p} onClick={()=>setPersons(p)} style={{border:'1px solid '+(persons===p?'var(--ink)':'var(--line)'),background:persons===p?'var(--ink)':'transparent',color:persons===p?'#fff':'var(--ink)',cursor:'pointer',fontFamily:'Jost,sans-serif',fontSize:14,padding:'12px 24px',transition:'all .3s var(--ease)'}}>{p===1?'Nur ich':p+' Personen'}</button>)}
      </div>
      {persons>1&&<p className="mg-p" style={{fontSize:13,marginTop:10}}>Wir reservieren {persons} Plätze parallel – bei Bedarf melden wir uns zur Feinabstimmung.</p>}
    </div>}
  </div>;
}

/* --- Schritt 2: Kosmetikerin, Tag, Uhrzeit, weitere Termine ---------------
   Alles Zeitliche kommt seit 26.08.2026 vom SERVER (db/013, /api/frei):
   Viertelstunden-Takt aus den Öffnungszeiten des Wochentags, 15 Minuten
   Puffer nach jeder Behandlung, Sperrzeiten der Inhaberin, Vorlauf. Hier
   wird nichts mehr gerechnet — vorher stand eine feste 8-Slot-Liste im JS,
   die der Server gar nicht kannte. */
function Kalender({monat,setMonat,tag,setTag,tage,laedt,horizontBis}){
  const heute=new Date();heute.setHours(0,0,0,0);
  const info=Object.fromEntries((tage||[]).map(t=>[t.datum,t]));
  const zellen=monatsRaster(monat.getFullYear(),monat.getMonth());
  const grenze=horizontBis?dParse(horizontBis):null;
  /* Nicht vor den aktuellen Monat und nicht über den Buchungshorizont hinaus. */
  const zurueckMoeglich=monat.getFullYear()>heute.getFullYear()||
    (monat.getFullYear()===heute.getFullYear()&&monat.getMonth()>heute.getMonth());
  const vorMoeglich=!grenze||new Date(monat.getFullYear(),monat.getMonth()+1,1)<=grenze;
  const springe=n=>setMonat(new Date(monat.getFullYear(),monat.getMonth()+n,1));

  return <div>
    <div style={{display:'flex',alignItems:'center',justifyContent:'space-between',gap:12,marginTop:14}}>
      <button type="button" onClick={()=>zurueckMoeglich&&springe(-1)} disabled={!zurueckMoeglich}
        aria-label="Vorheriger Monat"
        style={{border:'1px solid var(--line)',background:'transparent',color:'var(--ink)',
          cursor:zurueckMoeglich?'pointer':'not-allowed',opacity:zurueckMoeglich?1:.3,
          fontFamily:'Jost,sans-serif',fontSize:16,width:44,height:44,lineHeight:1}}>‹</button>
      <span style={{fontSize:16.5,letterSpacing:'.02em'}}>
        {MONATSNAMEN[monat.getMonth()]} {monat.getFullYear()}
      </span>
      <button type="button" onClick={()=>vorMoeglich&&springe(1)} disabled={!vorMoeglich}
        aria-label="Nächster Monat"
        style={{border:'1px solid var(--line)',background:'transparent',color:'var(--ink)',
          cursor:vorMoeglich?'pointer':'not-allowed',opacity:vorMoeglich?1:.3,
          fontFamily:'Jost,sans-serif',fontSize:16,width:44,height:44,lineHeight:1}}>›</button>
    </div>

    <div style={{display:'grid',gridTemplateColumns:'repeat(7,1fr)',gap:6,marginTop:18}}>
      {WOCHENTAGE.map(w=><div key={w} className="mg-eyebrow" style={{fontSize:10.5,textAlign:'center',paddingBottom:4}}>{w}</div>)}
      {zellen.map((d,i)=>{
        if(!d)return <div key={'l'+i}/>;
        const iso=isoVon(d);
        const t=info[iso];
        const vergangen=d<heute||(grenze&&d>grenze);
        /* Ohne geladene Übersicht wird nichts als "zu" behauptet — sonst
           blitzt beim Monatswechsel kurz ein komplett geschlossener Monat auf. */
        const zu=vergangen||(t?(t.modus==='zu'||t.gesperrt):false);
        const anfrage=!!t&&t.modus==='anfrage'&&!zu;
        const voll=!!t&&!zu&&t.offen_min>0&&t.belegt_min>=t.offen_min;
        const aus=zu||voll;
        const on=tag===iso;
        return <button key={iso} type="button" onClick={()=>{if(!aus)setTag(iso)}} disabled={aus}
          aria-label={fmt(d)+(anfrage?' – auf Anfrage':'')+(aus?' – nicht verfügbar':'')}
          title={zu?'Geschlossen':voll?'Ausgebucht':anfrage?'Nach Vereinbarung – wir bestätigen kurzfristig':undefined}
          style={{position:'relative',aspectRatio:'1',minHeight:42,
            border:'1px solid '+(on?'var(--ink)':'var(--line)'),
            background:on?'var(--ink)':'transparent',
            color:on?'#fff':aus?'var(--ink-mute)':'var(--ink)',
            cursor:aus?'not-allowed':'pointer',opacity:aus?.4:1,
            textDecoration:voll?'line-through':'none',
            fontFamily:'Jost,sans-serif',fontSize:14.5,padding:0,
            transition:'all .25s var(--ease)'}}>
          {d.getDate()}
          {anfrage&&<span aria-hidden="true" style={{position:'absolute',bottom:5,left:'50%',transform:'translateX(-50%)',
            width:4,height:4,borderRadius:'50%',background:on?'#fff':'var(--ink)',opacity:on?.8:.45}}/>}
        </button>;
      })}
    </div>

    <div style={{display:'flex',gap:18,flexWrap:'wrap',marginTop:14}}>
      <span className="mg-p" style={{fontSize:12,display:'flex',alignItems:'center',gap:7,margin:0}}>
        <span aria-hidden="true" style={{width:4,height:4,borderRadius:'50%',background:'var(--ink)',opacity:.45}}/>
        Montag &amp; Samstag: nach Vereinbarung
      </span>
      {laedt&&<span className="mg-p" style={{fontSize:12,margin:0}}>Verfügbarkeit wird geladen …</span>}
    </div>
  </div>;
}

function Step2({staff,setStaff,tag,setTag,slot,setSlot,frei,freiLaedt,belegtHinweis,
                monat,setMonat,tage,tageLaedt,horizontBis,weitere,setWeitere,sel,dauerMin}){
  const slots=(frei&&Array.isArray(frei.slots))?frei.slots:[];
  const anfrage=!!(frei&&frei.anfrage);
  const zu=!!(frei&&frei.modus==='zu');
  const nichtsFrei=!freiLaedt&&!!tag&&!zu&&slots.length>0&&!slots.some(s=>s.frei);

  return <div>
    <div className="mg-eyebrow">Mitarbeiterin</div>
    <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fit,minmax(180px,1fr))',gap:10,marginTop:14}}>
      {STAFF.map(([s,d])=><button key={s} onClick={()=>setStaff(s)} style={{textAlign:'left',padding:'16px 18px',border:'1px solid '+(staff===s?'var(--ink)':'var(--line)'),background:staff===s?'var(--paper-2)':'transparent',cursor:'pointer',fontFamily:'Jost,sans-serif',transition:'all .3s var(--ease)'}}>
        <span style={{fontSize:15.5,display:'block',color:'var(--ink)'}}>{s}</span>
        <span style={{fontSize:12.5,color:'var(--ink-mute)'}}>{d}</span>
      </button>)}
    </div>

    <div className="mg-eyebrow" style={{marginTop:34}}>Tag</div>
    <Kalender monat={monat} setMonat={setMonat} tag={tag} setTag={setTag}
      tage={tage} laedt={tageLaedt} horizontBis={horizontBis}/>

    {tag&&<>
      <div className="mg-eyebrow" style={{marginTop:34}}>
        Uhrzeit{freiLaedt&&<span style={{textTransform:'none',letterSpacing:0,fontSize:12}}> · Verfügbarkeit wird geprüft …</span>}
      </div>

      {zu&&!freiLaedt
        ? <p className="mg-p" style={{marginTop:14,maxWidth:560}}>An diesem Tag haben wir geschlossen. Wähl bitte einen anderen Tag im Kalender.</p>
        : <>
          <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fit,minmax(88px,1fr))',gap:8,marginTop:14}}>
            {(freiLaedt?[]:slots).map(s=>{const off=s.frei;const on=slot===s.zeit&&off;
            return <button key={s.zeit} onClick={()=>{if(off)setSlot(s.zeit)}} disabled={!off} aria-disabled={!off}
              title={off?undefined:'Bereits vergeben'}
              style={{padding:'12px 6px',border:'1px solid '+(on?'var(--ink)':'var(--line)'),
                background:on?'var(--ink)':'transparent',color:on?'#fff':'var(--ink)',
                cursor:off?'pointer':'not-allowed',opacity:off?1:.32,
                textDecoration:off?'none':'line-through',
                fontFamily:'Jost,sans-serif',fontSize:14,transition:'all .25s var(--ease)'}}>{s.zeit}</button>;})}
          </div>
          {freiLaedt&&<p className="mg-p" style={{fontSize:12.5,marginTop:10}}>Einen Moment …</p>}
          {nichtsFrei&&<p className="mg-p" style={{fontSize:13.5,marginTop:12,maxWidth:560}}>
            An diesem Tag ist alles vergeben{staff&&staff!=='Egal'?' – bei „Egal" ist oft noch etwas frei':''}. Schau gern auf einen anderen Tag.
          </p>}
          {!freiLaedt&&!nichtsFrei&&slots.some(s=>!s.frei)&&<p className="mg-p" style={{fontSize:12.5,marginTop:10}}>
            Durchgestrichene Zeiten sind bereits vergeben{staff==='Egal'?'':' – bei „Egal" ist oft mehr frei'}.
          </p>}
        </>}

      {/* Montag und Samstag öffnet das Studio nach Vereinbarung: die Kundin
          wählt eine Zeit, die Inhaberin bestätigt — und erst dann wird die
          Anzahlung eingezogen. Das muss VOR dem Absenden klar sein. */}
      {anfrage&&!zu&&<div style={{display:'flex',gap:12,alignItems:'flex-start',background:'var(--paper-2)',
        border:'1px solid var(--line)',padding:'14px 18px',marginTop:24,maxWidth:560}}>
        <span aria-hidden="true" style={{fontSize:14,lineHeight:1.6}}>✦</span>
        <p className="mg-p" style={{fontSize:13,margin:0}}>
          Montags und samstags öffnen wir nach Vereinbarung. Du wählst hier deine Wunschzeit – wir prüfen sie und
          melden uns kurzfristig. <span style={{color:'var(--ink)'}}>Erst nach unserer Bestätigung</span> wird die
          Anzahlung fällig; jetzt wird nichts abgebucht.
        </p>
      </div>}

      {belegtHinweis&&<p className="mg-p" style={{marginTop:12,color:'#a33c3c',fontSize:13.5,maxWidth:560}}>{belegtHinweis}</p>}

      <div style={{display:'flex',gap:12,alignItems:'flex-start',background:'var(--paper-2)',border:'1px solid var(--line)',padding:'14px 18px',marginTop:24,maxWidth:560}}>
        <span aria-hidden="true" style={{fontSize:14,lineHeight:1.6}}>✦</span>
        <p className="mg-p" style={{fontSize:13,margin:0}}>Zum ersten Mal bei uns? Dann gehört eine ausführliche persönliche Beratung dazu – mit Hautanalyse und Produktempfehlung, als Geschenk. Plane dafür etwa 30 Minuten zusätzlich ein; wir reservieren die Zeit automatisch mit.</p>
      </div>

      {slot&&<Serie weitere={weitere} setWeitere={setWeitere} sel={sel} dauerMin={dauerMin}
        staff={staff} ersterTag={tag} ersterSlot={slot} anfrage={anfrage}/>}
    </>}
  </div>;
}

/* --- Weitere Termine im Voraus -------------------------------------------
   Ansage der Inhaberin: "Kunden buchen direkt mehrere behandlung im Vorraus",
   "meistens machen die gleiche Behandlung für 1 jahr schonmal vorraus",
   "Weiter termin dann kann man auch neue oder zusätzliche behandlung machen
    kann und anzahlung wrd immer genommen im Vorraus."

   Deshalb: Vorschläge im gewohnten Rhythmus (4/6/8 Wochen) mit derselben
   Behandlung und Uhrzeit — ein Klick statt vier. Wer etwas anderes will,
   wählt Datum, Uhrzeit und Behandlung frei. Die Anzahlung addiert sich, weil
   jeder dieser Tage geblockt wird. */
function Serie({weitere,setWeitere,sel,dauerMin,staff,ersterTag,ersterSlot,anfrage}){
  const [offen,setOffen]=useState(false);
  const api=window.LAVIE_API;
  const [entwurf,setEntwurf]=useState(null);       // {datum,uhrzeit,behandlung,minuten,summe}
  const [slots,setSlots]=useState(null);

  const alleBehandlungen=useMemo(()=>{
    const o=[];KAT.forEach(([kat,,,items])=>items.forEach(([n,dur,preis])=>o.push({name:n,dur,preis,kat})));
    return o;},[]);
  const dauerVon=b=>{const m=(String(b&&b.dur||'').match(/\d+/g)||[]).map(Number);return m.length?Math.max(...m):60};

  const plus=(wochen)=>{
    const d=dParse(ersterTag);d.setDate(d.getDate()+wochen*7);
    return {datum:isoVon(d),uhrzeit:ersterSlot,behandlung:sel.name,minuten:dauerMin,summe:sel.preis};
  };

  /* Verfügbarkeit des Entwurfs-Tages — dieselbe Quelle wie oben. */
  useEffect(()=>{
    if(!entwurf||!entwurf.datum||!api){setSlots(null);return}
    let aktiv=true;setSlots(null);
    const q='?datum='+entwurf.datum+'&minuten='+(entwurf.minuten||60)
      +(staff?'&mit='+encodeURIComponent(staff):'');
    fetch(api+'/frei'+q).then(r=>r.ok?r.json():null)
      .then(d=>{if(aktiv)setSlots(d&&Array.isArray(d.slots)?d.slots:[])})
      .catch(()=>{if(aktiv)setSlots([])});
    return()=>{aktiv=false};
  },[entwurf&&entwurf.datum,entwurf&&entwurf.minuten,staff]);

  const hinzu=(t)=>{
    if(!t||!t.datum||!t.uhrzeit)return;
    if(weitere.some(w=>w.datum===t.datum&&w.uhrzeit===t.uhrzeit))return;
    setWeitere([...weitere,t].sort((a,b)=>(a.datum+a.uhrzeit).localeCompare(b.datum+b.uhrzeit)));
    setEntwurf(null);
  };

  return <div style={{marginTop:34,borderTop:'1px solid var(--line)',paddingTop:26}}>
    <div className="mg-eyebrow">Weitere Termine im Voraus</div>
    <p className="mg-p" style={{fontSize:13.5,marginTop:10,maxWidth:560}}>
      Viele Kundinnen legen ihre Behandlungen gleich für ein ganzes Jahr fest. Du kannst hier beliebig viele
      Folgetermine dazunehmen – auch mit einer anderen Behandlung.
      {anfrage
        ? ' Bestätigt werden sie gemeinsam mit deinem Wunschtermin.'
        : ' Die Anzahlung fassen wir dann zusammen, weil wir jeden dieser Tage für dich blocken.'}
    </p>

    {weitere.length>0&&<div style={{border:'1px solid var(--line)',marginTop:16}}>
      {weitere.map((w,i)=><div key={w.datum+w.uhrzeit} style={{display:'flex',justifyContent:'space-between',
        alignItems:'center',gap:14,padding:'13px 16px',flexWrap:'wrap',
        borderBottom:i<weitere.length-1?'1px solid var(--line)':'none'}}>
        <span style={{fontSize:14.5}}>
          <span style={{display:'block',fontSize:11,letterSpacing:'.1em',textTransform:'uppercase',color:'var(--ink-mute)'}}>
            Termin {i+2}
          </span>
          {w.behandlung}
        </span>
        <span style={{display:'flex',alignItems:'center',gap:14}}>
          <span style={{fontSize:14.5,whiteSpace:'nowrap'}}>{fmtISOS(w.datum)}, {w.uhrzeit}</span>
          <button type="button" onClick={()=>setWeitere(weitere.filter((_,j)=>j!==i))}
            aria-label="Termin entfernen"
            style={{border:'1px solid var(--line)',background:'transparent',color:'var(--ink-mute)',
              cursor:'pointer',fontFamily:'Jost,sans-serif',fontSize:16,width:36,height:36,lineHeight:1}}>×</button>
        </span>
      </div>)}
    </div>}

    {!offen&&<button type="button" className="mg-btn mg-btn--ghost" style={{marginTop:16}}
      onClick={()=>setOffen(true)}>Weiteren Termin hinzufügen</button>}

    {offen&&<div style={{marginTop:18,border:'1px solid var(--line)',padding:'20px 20px 24px'}}>
      <div className="mg-eyebrow" style={{fontSize:11}}>Im gewohnten Rhythmus</div>
      <div style={{display:'flex',gap:8,flexWrap:'wrap',marginTop:12}}>
        {[4,6,8,12].map(w=><button key={w} type="button" onClick={()=>setEntwurf(plus(w))}
          style={{border:'1px solid var(--line)',background:'transparent',color:'var(--ink)',cursor:'pointer',
            fontFamily:'Jost,sans-serif',fontSize:13.5,padding:'11px 16px'}}>
          in {w} Wochen</button>)}
      </div>

      <div className="mg-eyebrow" style={{fontSize:11,marginTop:22}}>Oder frei wählen</div>
      <div style={{display:'flex',gap:10,flexWrap:'wrap',marginTop:12,alignItems:'center'}}>
        <input type="date" value={entwurf&&entwurf.datum||''} min={ersterTag}
          onChange={e=>setEntwurf({...(entwurf||plus(4)),datum:e.target.value,uhrzeit:''})}
          style={{border:'1px solid var(--line)',background:'transparent',color:'var(--ink)',
            fontFamily:'Jost,sans-serif',fontSize:14.5,padding:'11px 14px'}}/>
        <select value={entwurf&&entwurf.behandlung||sel.name}
          onChange={e=>{const b=alleBehandlungen.find(x=>x.name===e.target.value);
            setEntwurf({...(entwurf||plus(4)),behandlung:e.target.value,
              minuten:dauerVon(b),summe:b?b.preis:sel.preis,uhrzeit:''})}}
          style={{border:'1px solid var(--line)',background:'transparent',color:'var(--ink)',
            fontFamily:'Jost,sans-serif',fontSize:14.5,padding:'11px 14px',maxWidth:'100%'}}>
          {alleBehandlungen.map(b=><option key={b.name} value={b.name}>{b.name} · {b.preis} €</option>)}
        </select>
      </div>

      {entwurf&&entwurf.datum&&<>
        <div className="mg-eyebrow" style={{fontSize:11,marginTop:22}}>
          Uhrzeit am {fmtISO(entwurf.datum)}{slots===null&&' · wird geprüft …'}
        </div>
        {Array.isArray(slots)&&slots.length===0&&
          <p className="mg-p" style={{fontSize:13.5,marginTop:10}}>An diesem Tag haben wir geschlossen.</p>}
        <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fit,minmax(88px,1fr))',gap:8,marginTop:12}}>
          {(slots||[]).map(s=>{const on=entwurf.uhrzeit===s.zeit&&s.frei;
          return <button key={s.zeit} type="button" disabled={!s.frei}
            onClick={()=>setEntwurf({...entwurf,uhrzeit:s.zeit})}
            style={{padding:'11px 6px',border:'1px solid '+(on?'var(--ink)':'var(--line)'),
              background:on?'var(--ink)':'transparent',color:on?'#fff':'var(--ink)',
              cursor:s.frei?'pointer':'not-allowed',opacity:s.frei?1:.32,
              textDecoration:s.frei?'none':'line-through',
              fontFamily:'Jost,sans-serif',fontSize:13.5}}>{s.zeit}</button>;})}
        </div>
      </>}

      <div style={{display:'flex',gap:12,marginTop:24,flexWrap:'wrap'}}>
        <button type="button" className="mg-btn mg-btn--ghost" onClick={()=>{setOffen(false);setEntwurf(null)}}>Abbrechen</button>
        <button type="button" className="mg-btn" disabled={!entwurf||!entwurf.datum||!entwurf.uhrzeit}
          onClick={()=>hinzu(entwurf)}>Termin übernehmen</button>
      </div>
    </div>}
  </div>;
}

function Step3({daten,setDaten}){
  const [err,setErr]=useState({});
  const set=(k,v)=>setDaten({...daten,[k]:v});
  const notiz=(daten.notiz||'').trim();
  const alleAn=KANAELE.every(([k])=>daten[k]);
  window.__validate3=()=>{
    const e={};
    if(!daten.name)e.name='Bitte gib deinen Namen an.';
    if(!daten.email||!/.+@.+\..+/.test(daten.email))e.email='Bitte gib eine gültige E-Mail-Adresse an.';
    if(!daten.tel)e.tel='Für die Terminbestätigung brauchen wir deine Nummer.';
    if(!daten.ds)e.ds='Bitte bestätige, dass du die Datenschutzerklärung gelesen hast.';
    /* Gesundheitsangaben sind Art.-9-Daten: dafür reicht der Vertrag nicht,
       es braucht eine ausdrückliche Einwilligung. Nur abfragen, wenn die
       Notiz überhaupt ausgefüllt ist – sonst ist es unnötige Reibung. */
    if(notiz&&!daten.gesund)e.gesund='Deine Notiz kann Gesundheitsangaben enthalten – dafür brauchen wir deine ausdrückliche Einwilligung.';
    setErr(e);return Object.keys(e).length===0;
  };
  /* Pflichtfelder werden mit * markiert – das ist die gewohnte Konvention,
     die Legende steht am Ende des Schritts. */
  const F=(k,label,type,pflicht)=><div>
    <label className="mg-eyebrow" style={{fontSize:11,display:'block',marginBottom:7}}>{label}{pflicht&&<span aria-hidden="true"> *</span>}</label>
    <input type={type||'text'} required={!!pflicht} aria-required={pflicht?'true':undefined} value={daten[k]||''} onChange={e=>set(k,e.target.value)} style={{width:'100%',background:'var(--paper-2)',border:'1px solid '+(err[k]?'#a33':'var(--line)'),outline:'none',padding:'14px 18px',fontFamily:'Jost,sans-serif',fontSize:15,color:'var(--ink)'}}/>
    {err[k]&&<div style={{fontSize:12,color:'#a33',marginTop:6}}>{err[k]}</div>}
  </div>;
  const CB=(k,children)=><label style={{display:'flex',gap:11,alignItems:'flex-start',cursor:'pointer',lineHeight:1.6}}>
    <input type="checkbox" checked={!!daten[k]} onChange={e=>set(k,e.target.checked)} style={{accentColor:'var(--ink)',marginTop:3}}/>
    <span style={{fontSize:12.5,color:'var(--ink-soft)'}}>{children}</span>
  </label>;
  return <div style={{display:'flex',flexDirection:'column',gap:18,maxWidth:620}}>
    <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fit,minmax(200px,1fr))',gap:14}}>{F('name','Name',null,true)}{F('tel','Telefon / WhatsApp','tel',true)}</div>
    {F('email','E-Mail','email',true)}
    <div>
      <label className="mg-eyebrow" style={{fontSize:11,display:'block',marginBottom:7}}>Notiz an uns (optional)</label>
      <textarea rows={3} value={daten.notiz||''} onChange={e=>set('notiz',e.target.value)} placeholder="Wünsche, Hautthemen, Allergien – alles, was uns hilft." style={{width:'100%',background:'var(--paper-2)',border:'1px solid '+(err.gesund?'#a33':'var(--line)'),outline:'none',padding:'14px 18px',fontFamily:'Jost,sans-serif',fontSize:15,resize:'vertical',color:'var(--ink)'}}></textarea>
      {notiz&&<div style={{marginTop:12}}>
        {CB('gesund',<><span style={{color:'var(--ink)'}} aria-hidden="true">* </span>{TXT_GESUND}</>)}
        {err.gesund&&<div style={{fontSize:12,color:'#a33',marginTop:6,marginLeft:26}}>{err.gesund}</div>}
      </div>}
    </div>
    {F('code','Gutscheincode (optional)')}

    {/* Terminerinnerung ist Vertragsnebenleistung, keine Werbung – hier wird
        nur der Kanal gewählt, keine Einwilligung eingeholt. */}
    <div style={{borderTop:'1px solid var(--line)',paddingTop:20}}>
      <div className="mg-eyebrow">Terminerinnerung</div>
      <p className="mg-p" style={{fontSize:13,margin:'10px 0 12px'}}>Wir erinnern dich 24 Stunden vor dem Termin. Wähle, wie es dir am liebsten ist.</p>
      <div style={{display:'flex',gap:10,flexWrap:'wrap'}}>
        {[['E-Mail','email'],['SMS','sms'],['WhatsApp','wa']].map(([l,v])=><button key={v} type="button" onClick={()=>set('erinnerung',v)} style={{border:'1px solid '+((daten.erinnerung||'email')===v?'var(--ink)':'var(--line)'),background:(daten.erinnerung||'email')===v?'var(--ink)':'transparent',color:(daten.erinnerung||'email')===v?'#fff':'var(--ink)',cursor:'pointer',fontFamily:'Jost,sans-serif',fontSize:14,padding:'11px 22px',transition:'all .3s var(--ease)'}}>{l}</button>)}
      </div>
    </div>

    {/* Werbe-Einwilligungen: getrennt pro Kanal, keine vorausgewählt,
        und die Buchung funktioniert ohne jede davon (kein Kopplungsverbot-
        Verstoß nach Art. 7 Abs. 4 DSGVO). */}
    <div style={{border:'1px solid var(--line)',padding:'22px 24px'}}>
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'baseline',gap:14,flexWrap:'wrap'}}>
        <div className="mg-eyebrow">Bleib in Kontakt</div>
        <button type="button" onClick={()=>{const n={...daten};KANAELE.forEach(([k])=>n[k]=!alleAn);setDaten(n)}} style={{border:'none',background:'none',cursor:'pointer',fontFamily:'Jost,sans-serif',fontSize:12.5,color:'var(--ink)',textDecoration:'underline',textUnderlineOffset:3,padding:0}}>{alleAn?'Alle abwählen':'Alle aktivieren'}</button>
      </div>
      <p className="mg-p" style={{fontSize:13.5,margin:'12px 0 16px'}}>Als Dankeschön bekommst du <span style={{color:'var(--ink)'}}>10 % auf deine übernächste Behandlung</span> und einen Gutschein zum Geburtstag. Wir melden uns höchstens zweimal im Monat.</p>
      <div style={{display:'flex',flexDirection:'column',gap:11,borderTop:'1px solid var(--line)',paddingTop:16}}>
        {KANAELE.map(([k,label,desc])=><label key={k} style={{display:'flex',gap:11,alignItems:'flex-start',cursor:'pointer',lineHeight:1.6}}>
          <input type="checkbox" checked={!!daten[k]} onChange={e=>set(k,e.target.checked)} style={{accentColor:'var(--ink)',marginTop:3}}/>
          <span style={{fontSize:12.5,color:'var(--ink-soft)'}}><span style={{color:'var(--ink)'}}>{label}</span> – {desc}</span>
        </label>)}
      </div>
      <p className="mg-p" style={{fontSize:11.5,marginTop:14,lineHeight:1.7}}>Freiwillig: Dein Termin kommt auch ohne zustande. Du kannst jede Einwilligung jederzeit und kostenfrei widerrufen – per Abmeldelink, mit „STOPP" als Antwort oder formlos an <a href="mailto:info@lavie-wirdschoener.de" style={{color:'var(--ink-soft)'}}>info@lavie-wirdschoener.de</a>.</p>
    </div>

    <div style={{borderTop:'1px solid var(--line)',paddingTop:20,display:'flex',flexDirection:'column',gap:12}}>
      {CB('ds',<><span style={{color:'var(--ink)'}} aria-hidden="true">* </span>{TXT_DS} Meine Daten werden zur Durchführung des Termins verarbeitet (Art. 6 Abs. 1 lit. b DSGVO) – Details in der <a href="datenschutz.html" style={{color:'var(--ink)'}}>Datenschutzerklärung</a>.</>)}
      {err.ds&&<div style={{fontSize:12,color:'#a33',marginLeft:26}}>{err.ds}</div>}
      {/* Pflichthinweis aus § 7 Abs. 3 Nr. 3 UWG – muss schon bei der Erhebung
          der Adresse erfolgen, nicht erst in der ersten Werbemail. */}
      <p className="mg-p" style={{fontSize:11.5,lineHeight:1.7,marginTop:2}}>Nach deinem Besuch schicken wir dir gelegentlich Hinweise zu vergleichbaren Behandlungen an deine E-Mail-Adresse. Du kannst dem jederzeit widersprechen, ohne dass dafür andere als die Übermittlungskosten nach Basistarifen entstehen – in jeder Nachricht per Link oder formlos an uns.</p>
      <p className="mg-p" style={{fontSize:11.5,marginTop:14,paddingTop:12,borderTop:'1px solid var(--line)'}}>* Pflichtfeld</p>
    </div>
  </div>;
}

/* Die Klauseln direkt im Funnel lesbar zu machen ist nicht nur bequemer –
   es ist die Einbeziehung nach § 305 Abs. 2 BGB: zumutbare Kenntnisnahme
   VOR Vertragsschluss. Ein Link, der aus dem Funnel herausfuehrt, erfuellt
   das schlechter (und kostet auf dem Handy den Buchungsstand).
   Auszug – die vollstaendigen Fassungen liegen auf agb.html / widerruf.html. */
const RECHTSTEXT={
 storno:{t:'Buchungs- und Stornobedingungen',datei:'agb.html',abs:[
  ['Anzahlung',['Bei jeder Online-Buchung erheben wir eine Anzahlung von 25 €. Wir reservieren dafür die vollständige Behandlungszeit exklusiv für dich.','Die Anzahlung wird vollständig auf den Preis der Behandlung angerechnet. Der Restbetrag ist nach der Behandlung im Studio zu zahlen.','Die Zahlung läuft über unseren Zahlungsdienstleister Whop. Kartendaten gibst du ausschließlich dort ein; wir erhalten und speichern sie nicht.']],
  ['Absage und Umbuchung',['Du kannst deinen Termin bis 24 Stunden vor Behandlungsbeginn kostenfrei absagen oder verschieben. Eine geleistete Anzahlung erstatten wir in diesem Fall vollständig.','Für die Absage genügt eine Nachricht per E-Mail, Telefon oder WhatsApp. Maßgeblich ist der Zeitpunkt des Zugangs bei uns.']],
  ['Ausfallregelung',['Sagst du einen anzahlungspflichtigen Termin später als 24 Stunden vorher ab oder nimmst ihn ohne Absage nicht wahr, behalten wir die Anzahlung von 25 € als pauschalierten Schadensersatz für die reservierte und nicht anderweitig vergebene Behandlungszeit ein.','Dir bleibt ausdrücklich der Nachweis vorbehalten, dass uns kein Schaden oder ein wesentlich geringerer Schaden entstanden ist. In diesem Fall erstatten wir die Anzahlung ganz oder anteilig.','Können wir den frei gewordenen Termin kurzfristig anderweitig vergeben, entsteht uns kein Schaden. Wir erstatten die Anzahlung dann vollständig.','Bei Terminen ohne Anzahlung – etwa telefonisch oder im Studio vereinbart – erheben wir keine Ausfallpauschale.','Bei Verhinderung aus wichtigem Grund, etwa Krankheit, finden wir eine einvernehmliche Lösung. Sprich uns einfach an.']],
  ['Verspätung',['Erscheinst du später als vereinbart, verkürzt sich die Behandlungszeit entsprechend, damit nachfolgende Termine eingehalten werden können. Der vereinbarte Preis bleibt unverändert.']],
  ['Angaben zu deiner Gesundheit',['Bitte teile uns vor der Behandlung alles mit, was für die Durchführung wichtig ist – insbesondere Allergien, Unverträglichkeiten, Hauterkrankungen, Medikamente oder eine Schwangerschaft.']]]},
 widerruf:{t:'Widerruf und Stornierung',datei:'widerruf.html',abs:[
  ['Kein Widerrufsrecht bei Terminbuchungen',['Ein Termin ist eine Dienstleistung zu einem bestimmten Zeitpunkt – dafür sieht das Gesetz kein Widerrufsrecht vor (§ 312g Abs. 2 Nr. 9 BGB).']],
  ['Was stattdessen gilt',['Bis 24 Stunden vorher kannst du kostenfrei absagen oder verschieben, die Anzahlung bekommst du vollständig zurück. Danach behalten wir sie als pauschalierten Schadensersatz ein; der Nachweis eines geringeren Schadens bleibt dir vorbehalten.']]]}
};
function RechtModal({thema,onClose}){
  window.useScrollLock(true);
  useEffect(()=>{
    const f=e=>{if(e.key==='Escape')onClose()};
    window.addEventListener('keydown',f);
    return()=>window.removeEventListener('keydown',f);
  },[onClose]);
  const d=RECHTSTEXT[thema];
  /* Portal an document.body ist hier Pflicht, nicht Geschmack: Der Schritt-
     Container laeuft mit `animation:mg-rise` auf transform und wird dadurch
     zum Containing Block fuer position:fixed. Innerhalb davon klebte das
     Overlay am Container statt am Bildschirm – der Schliessen-Knopf landete
     ausserhalb des sichtbaren Bereichs. */
  return ReactDOM.createPortal(
    <div onClick={onClose} style={{position:'fixed',inset:0,zIndex:130,background:'rgba(20,18,17,.5)',display:'flex',alignItems:'center',justifyContent:'center',padding:'clamp(10px,4vw,32px)',animation:'mg-fade .35s var(--ease) both'}}>
    {/* Der Rahmen ist hoehenbegrenzt, gescrollt wird NUR die Textspalte –
        sonst haengt auf kleinen Displays der Schliessen-Knopf ausserhalb. */}
    <div onClick={e=>e.stopPropagation()} role="dialog" aria-modal="true" aria-label={d.t} style={{background:'#fff',border:'1px solid var(--line)',width:'100%',maxWidth:640,maxHeight:'min(86vh,860px)',display:'flex',flexDirection:'column'}}>
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'flex-start',gap:16,padding:'20px clamp(18px,3vw,30px) 15px',borderBottom:'1px solid var(--line)',flexShrink:0}}>
        <div><div className="mg-eyebrow">Rechtliches</div><h3 className="mg-h3" style={{fontSize:19,margin:'8px 0 0'}}>{d.t}</h3></div>
        <button onClick={onClose} aria-label="Schließen" style={{border:'none',background:'none',cursor:'pointer',fontSize:24,lineHeight:1,color:'var(--ink-mute)',padding:0,marginTop:-2,fontFamily:'Jost,sans-serif'}}>×</button>
      </div>
      <div style={{overflowY:'auto',WebkitOverflowScrolling:'touch',overscrollBehavior:'contain',padding:'clamp(16px,2.6vw,24px) clamp(18px,3vw,30px) 22px'}}>
        {d.abs.map(([h,ps],i)=><div key={h} style={{marginTop:i?24:0}}>
          <h4 style={{fontFamily:'Jost,sans-serif',fontSize:15,fontWeight:400,margin:0,color:'var(--ink)'}}>{h}</h4>
          {ps.map((p,j)=><p key={j} className="mg-p" style={{fontSize:13.5,marginTop:8,lineHeight:1.75}}>{p}</p>)}
        </div>)}
      </div>
      <div style={{borderTop:'1px solid var(--line)',padding:'13px clamp(18px,3vw,30px)',display:'flex',justifyContent:'space-between',alignItems:'center',gap:12,flexWrap:'wrap',flexShrink:0}}>
        <a href={d.datei} target="_blank" rel="noopener" className="mg-link" style={{fontSize:12}}>Vollständige Fassung</a>
        <button className="mg-btn" onClick={onClose} style={{height:40}}>Verstanden</button>
      </div>
    </div>
  </div>, document.body);
}

function Step4({sel,addons,staff,tag,slot,daten,setDaten,persons,total,onDone,weitere,anfrageTag}){
  const [modal,setModal]=useState('');
  const [err,setErr]=useState('');
  const anz=anzPflicht(total*persons);
  /* Serientermine: der Kopf plus jeder Folgetermin. Die Anzahlung addiert
     sich, weil jeder dieser Tage geblockt wird. */
  const alleTermine=1+(weitere?weitere.length:0);
  const anzGesamt=alleTermine*ANZ_BETRAG;
  /* Die Folgetermine haben eigene Preise (sie dürfen andere Behandlungen sein). */
  const serieSumme=(weitere||[]).reduce((s,w)=>s+(w.summe||0),0);
  const code=codeNorm(daten.code);
  const pay=()=>{
    if(!daten.storno){setErr('Bitte bestätige die Buchungs- und Stornobedingungen.');return}
    setErr('');onDone();
  };
  window.__pay=pay;
  const row=(l,r,mute)=><div style={{display:'flex',justifyContent:'space-between',gap:16,fontSize:14.5,color:mute?'var(--ink-mute)':'var(--ink)',padding:'9px 0'}}><span>{l}</span><span>{r}</span></div>;
  return <div style={{maxWidth:620}}>
    <div style={{border:'1px solid var(--line)',padding:'26px 28px'}}>
      <div className="mg-eyebrow">Zusammenfassung</div>
      <div style={{marginTop:14,borderTop:'1px solid var(--line)'}}>
        {row(sel.name+' ('+sel.dur+')',sel.plabel||sel.preis+' €')}
        {ADDONS.filter(([n])=>addons.includes(n)).map(([n,p])=><div key={n}>{row('+ '+n,p+' €',true)}</div>)}
        {persons>1&&row('Personen',persons,true)}
        {row(fmtISO(tag)+' · '+slot+' Uhr',staff==='Egal'?'nächste freie Mitarbeiterin':staff,true)}
        {(weitere||[]).map((w,i)=><div key={w.datum+w.uhrzeit}>
          {row((i===0?'Weitere Termine: ':'')+fmtISO(w.datum)+' · '+w.uhrzeit+' Uhr · '+w.behandlung,(w.summe||0)+' €',true)}
        </div>)}
        {code&&row('Gutschein '+code+' · −10 %','−'+eurF(total*persons-rabattWert(total*persons,code))+' €',true)}
        <div style={{borderTop:'1px solid var(--line)',marginTop:8,paddingTop:12,display:'flex',justifyContent:'space-between',fontSize:17}}>
          <span>Gesamt{alleTermine>1?' · alle '+alleTermine+' Termine':''}</span>
          <span>{(code?rabattWert(total*persons,code):total*persons)+serieSumme} €</span></div>
        {alleTermine>1&&<p className="mg-p" style={{fontSize:12,marginTop:8}}>
          Jede Behandlung wird beim jeweiligen Termin vor Ort bezahlt – abzüglich der bereits geleisteten Anzahlung.
        </p>}
      </div>
    </div>
    {anz&&<div style={{border:'1px solid var(--line)',padding:'26px 28px',marginTop:16}}>
      <div className="mg-eyebrow">Anzahlung {anzGesamt} €{alleTermine>1?" · "+alleTermine+" Termine":""}</div>
      {code&&<div style={{display:'flex',justifyContent:'space-between',gap:12,alignItems:'baseline',background:'var(--paper-2)',border:'1px solid var(--line)',padding:'12px 16px',margin:'10px 0 4px',flexWrap:'wrap'}}>
        <span className="mg-eyebrow" style={{fontSize:11}}>Gutschein {code}</span>
        <span style={{fontSize:13.5}}>−10 % für deine erste Behandlung: <span style={{textDecoration:'line-through',color:'var(--ink-mute)'}}>{eurF(total*persons)} €</span> → <strong>{eurF(rabattWert(total*persons,code))} €</strong></span>
      </div>}
      <p className="mg-p" style={{fontSize:13.5,margin:'10px 0 16px'}}>Mit der Anzahlung reservieren wir die volle Behandlungszeit exklusiv für dich.{alleTermine>1?' Weil wir alle '+alleTermine+' Tage für dich blocken, fassen wir die Anzahlung zusammen: '+alleTermine+' × '+ANZ_BETRAG+' €.':''} Die {anzGesamt} € werden vollständig auf den Behandlungspreis angerechnet{alleTermine>1?'':' – offen bleiben '+eurF(rabattWert(total*persons,code)-ANZ_BETRAG)+' € vor Ort'}.</p>
      {/* Bewusst kein Kartenfeld auf dieser Seite: Kartendaten werden erst im
          gehosteten Whop-Checkout eingegeben und berühren La Vie nie. */}
      <div style={{display:'flex',gap:12,alignItems:'flex-start',background:'var(--paper-2)',padding:'16px 18px'}}>
        <span aria-hidden="true" style={{fontSize:14,lineHeight:1.5}}>→</span>
        <p className="mg-p" style={{fontSize:13,margin:0}}>Im nächsten Schritt öffnet sich die gesicherte Zahlungsseite unseres Zahlungsdienstleisters <span style={{color:'var(--ink)'}}>Whop</span>. Deine Kartendaten werden dort eingegeben – wir sehen und speichern sie nicht.</p>
      </div>
    </div>}

    {/* Einbeziehung nach § 305 Abs. 2 BGB: die Regel steht im Klartext hier,
        nicht nur als Link. Der Gegenbeweisvorbehalt (§ 309 Nr. 5 lit. b BGB)
        und die Erstattung bei Neuvergabe sind Wirksamkeitsvoraussetzung. */}
    <div style={{border:'1px solid var(--line)',padding:'26px 28px',marginTop:16}}>
      <div className="mg-eyebrow">Wenn etwas dazwischenkommt</div>
      <div style={{marginTop:14,borderTop:'1px solid var(--line)',paddingTop:14,display:'flex',flexDirection:'column',gap:10}}>
        {(anz?[['Bis 24 Stunden vorher','Absage kostenfrei, Anzahlung wird vollständig erstattet'],
               ['Später als 24 Stunden oder ohne Absage',ANZ_BETRAG+' € werden als pauschalierter Schadensersatz einbehalten'],
               ['Termin wird neu vergeben','Du bekommst die Anzahlung vollständig zurück']]
              :[['Bis 24 Stunden vorher','Absage kostenfrei – kurze Nachricht genügt'],
               ['Später als 24 Stunden oder ohne Absage','Keine Gebühr – sag uns bitte trotzdem kurz Bescheid']]
        ).map(([l,r])=><div key={l} style={{display:'flex',justifyContent:'space-between',gap:16,fontSize:13.5,lineHeight:1.6}}><span style={{color:'var(--ink-mute)',flex:'1 1 46%'}}>{l}</span><span style={{flex:'1 1 46%',textAlign:'right'}}>{r}</span></div>)}
      </div>
      <p className="mg-p" style={{fontSize:12,marginTop:14,lineHeight:1.7}}>{anz?'Dir bleibt der Nachweis vorbehalten, dass uns kein oder ein wesentlich geringerer Schaden entstanden ist. ':'Die reservierte Zeit gehört dir – eine rechtzeitige Absage hilft uns, sie jemand anderem anzubieten. '}Ausführlich in den <button type="button" onClick={()=>setModal('storno')} style={{all:'unset',cursor:'pointer',color:'var(--ink)',textDecoration:'underline',textUnderlineOffset:3}}>Buchungs- und Stornobedingungen</button>.</p>
      <label style={{display:'flex',gap:11,alignItems:'flex-start',cursor:'pointer',lineHeight:1.6,marginTop:16,borderTop:'1px solid var(--line)',paddingTop:16}}>
        <input type="checkbox" checked={!!daten.storno} onChange={e=>setDaten({...daten,storno:e.target.checked})} style={{accentColor:'var(--ink)',marginTop:3}}/>
        <span style={{fontSize:12.5,color:'var(--ink-soft)'}}><span style={{color:'var(--ink)'}} aria-hidden="true">* </span>{TXT_STORNO(anz)}</span>
      </label>
      {err&&<div style={{fontSize:12,color:'#a33',marginTop:10,marginLeft:26}}>{err}</div>}
      <button type="button" onClick={()=>setModal('storno')} className="mg-btn mg-btn--ghost" style={{marginTop:16,height:42,fontSize:13}}>Bedingungen lesen</button>
    </div>

    {/* Der Hinweis auf ein 14-tägiges Widerrufsrecht ist am 26.08.2026 auf
        ausdrückliche Ansage der Inhaberin entfallen ("Das darf nirgendswo
        auftauchen"). Er stand ohnehin im Widerspruch zur kommunizierten
        Regelung: Es gilt die 24-Stunden-Frist, danach wird die Anzahlung
        einbehalten — beides steht direkt darüber und ist zustimmungspflichtig. */}
    <p className="mg-p" style={{fontSize:11.5,marginTop:10}}>* Pflichtfeld</p>
    {modal&&<RechtModal thema={modal} onClose={()=>setModal('')}/>}
  </div>;
}

function Aside({sel,addons,persons,total,staff,tag,slot,step,next,can,label,code,weitere}){
  const alleTermine=1+(weitere?weitere.length:0);
  const anzGesamt=alleTermine*ANZ_BETRAG;
  const serieSumme=(weitere||[]).reduce((s,w)=>s+(w.summe||0),0);
  if(!sel)return <div style={{border:'1px solid var(--line)',padding:'30px 28px'}}>
    <div className="mg-eyebrow">Deine Auswahl</div>
    <p className="mg-p" style={{fontSize:14,marginTop:14}}>Wähle links eine Behandlung – wir zeigen dir hier laufend deine Zusammenfassung.</p>
  </div>;
  return <div style={{border:'1px solid var(--line)'}}>
    <img loading="lazy" src={P+sel.img} alt="" style={{width:'100%',height:170,objectFit:'cover'}}/>
    <div style={{padding:'24px 26px'}}>
      <div className="mg-eyebrow">Deine Auswahl</div>
      <div style={{fontSize:18,marginTop:12,lineHeight:1.3}}>{sel.name}</div>
      <div className="mg-p" style={{fontSize:13.5,marginTop:5}}>{sel.kat} · {sel.dur}</div>
      <div style={{borderTop:'1px solid var(--line)',marginTop:18,paddingTop:14,display:'flex',flexDirection:'column',gap:8,fontSize:14}}>
        <div style={{display:'flex',justifyContent:'space-between'}}><span style={{color:'var(--ink-mute)'}}>Behandlung</span><span>{sel.plabel||sel.preis+' €'}</span></div>
        {ADDONS.filter(([n])=>addons.includes(n)).map(([n,p])=><div key={n} style={{display:'flex',justifyContent:'space-between'}}><span style={{color:'var(--ink-mute)'}}>{n}</span><span>+{p} €</span></div>)}
        {persons>1&&<div style={{display:'flex',justifyContent:'space-between'}}><span style={{color:'var(--ink-mute)'}}>Personen</span><span>× {persons}</span></div>}
        {staff&&<div style={{display:'flex',justifyContent:'space-between'}}><span style={{color:'var(--ink-mute)'}}>Bei</span><span>{staff}</span></div>}
        {tag&&slot&&<div style={{display:'flex',justifyContent:'space-between'}}><span style={{color:'var(--ink-mute)'}}>Termin</span><span>{fmtISOS(tag)}, {slot}</span></div>}
        {(weitere||[]).map((w,i)=><div key={w.datum+w.uhrzeit} style={{display:'flex',justifyContent:'space-between',gap:10}}>
          <span style={{color:'var(--ink-mute)'}}>{i===0?'Weitere':''}</span>
          <span style={{textAlign:'right'}}>{fmtISOS(w.datum)}, {w.uhrzeit}<span style={{display:'block',color:'var(--ink-mute)',fontSize:12.5}}>{w.behandlung}</span></span>
        </div>)}
        {code&&<div style={{display:'flex',justifyContent:'space-between'}}><span style={{color:'var(--ink-mute)'}}>Gutschein {code}</span><span>−{eurF(total*persons-rabattWert(total*persons,code))} €</span></div>}
      </div>
      <div style={{borderTop:'1px solid var(--line)',marginTop:14,paddingTop:14,display:'flex',justifyContent:'space-between',fontSize:18}}><span>Gesamt{alleTermine>1?' · '+alleTermine+' Termine':''}</span><span>{code?<span><span style={{textDecoration:'line-through',color:'var(--ink-mute)',fontSize:14,marginRight:8}}>{total*persons+serieSumme} €</span>{eurF(rabattWert(total*persons,code)+serieSumme)} €</span>:(total*persons+serieSumme)+' €'}</span></div>
      {code&&<p className="mg-p" style={{fontSize:12,marginTop:8}}>10-%-Willkommensgutschein angewendet – gilt für deine erste Behandlung, wird bei der Buchung geprüft.</p>}
      {anzPflicht(total*persons)&&<p className="mg-p" style={{fontSize:12,marginTop:10}}>
        Davon {anzGesamt} € Anzahlung bei der Buchung – wird angerechnet.
        {alleTermine>1&&' ('+alleTermine+' × '+ANZ_BETRAG+' €, weil wir alle Tage für dich blocken.)'}
      </p>}
      <button className="mg-btn" onClick={next} style={{width:'100%',marginTop:18,opacity:can?1:.4,pointerEvents:can?'auto':'none'}}>{label}</button>
      {step<3&&<p className="mg-p" style={{fontSize:11.5,marginTop:10,textAlign:'center'}}>Noch unverbindlich – du bestätigst erst am Ende.</p>}
    </div>
  </div>;
}

/* Gemeinsame Huelle beider Wartescreens: links das Motiv, rechts der Text.
   Die Seite dahinter wird festgehalten, damit man nicht ins Leere scrollt. */
function LoaderShell({img,zoom,children}){
  window.useScrollLock(true);
  return <div style={{position:'fixed',inset:0,zIndex:120,background:'#fff',display:'flex',animation:'mg-fade .5s var(--ease) both'}}>
    <div className="mg-hide-m" style={{width:'46%',position:'relative',overflow:'hidden'}}>
      <img src={img} alt="" style={{width:'100%',height:'100%',objectFit:'cover',transform:zoom?'scale(1.06)':'scale(1)',transition:'transform 1.6s var(--ease)'}}/>
    </div>
    <div style={{flex:1,display:'flex',flexDirection:'column',justifyContent:'center',padding:'clamp(36px,6vw,90px)'}}>
      <img className="mg-load-img" src={img} alt="" style={{display:'none',width:'100%',maxWidth:340,height:170,objectFit:'cover',marginBottom:28,transform:zoom?'scale(1.03)':'scale(1)',transition:'transform 1.6s var(--ease)'}}/>
      {children}
    </div>
  </div>;
}

const Punkte=()=><div style={{display:'flex',gap:5}}>{[0,1,2].map(n=><span key={n} style={{width:6,height:6,borderRadius:'50%',background:'var(--ink)',animation:'mg-dot 1.3s '+(n*.18)+'s infinite'}}></span>)}</div>;

const Zitat=({z,delay})=>!z?null:<div style={{animation:'mg-rise .9s '+(delay||0)+'s var(--ease) both'}}>
  <blockquote className="mg-h2" style={{fontSize:'clamp(21px,2.4vw,32px)',fontWeight:200,lineHeight:1.4,margin:'26px 0 0',maxWidth:520}}>„{z.t}"</blockquote>
  {z.a&&<div className="mg-eyebrow" style={{marginTop:16}}>{z.a}</div>}
</div>;

function Loader({phase,sel,tag,slot,zitat}){
  const g=phase==='grats';
  return <LoaderShell img={P+sel.img} zoom={g}>
    {!g?<div key="c">
      <Punkte/>
      <Zitat z={zitat||{t:sel.quote}} delay={.15}/>
      <p className="mg-p" style={{marginTop:30,maxWidth:460,animation:'mg-rise .9s .9s var(--ease) both'}}>Einen Moment bitte – wir prüfen, ob <span style={{color:'var(--ink)'}}>{sel.name}</span> am {fmtISO(tag)} um {slot} Uhr noch frei ist.</p>
      <p className="mg-p" style={{fontSize:13,marginTop:12,animation:'mg-rise .9s 1.7s var(--ease) both'}}>Unsere Termine sind sehr gefragt – danke für deine Geduld.</p>
    </div>:<div key="g">
      <div className="mg-eyebrow" style={{animation:'mg-rise .8s var(--ease) both'}}>Verfügbar</div>
      <h2 className="mg-h2" style={{margin:'18px 0 0',animation:'mg-rise .8s .25s var(--ease) both'}}>Dein Wunschtermin<br/>ist frei.</h2>
      <p className="mg-p" style={{marginTop:20,fontSize:16,animation:'mg-rise .8s .6s var(--ease) both'}}>{fmtISO(tag)}, {slot} Uhr — wir halten ihn kurz für dich reserviert.</p>
      <div style={{width:0,height:1,background:'var(--ink)',marginTop:34,animation:'mg-line 1.2s .8s var(--ease) forwards'}}></div>
    </div>}
  </LoaderShell>;
}

/* Wartescreen nach dem Absenden der Buchung. Zwei Zustaende: erst das
   Eintragen, dann eine kurze Bestaetigung, bevor die Bestaetigungsseite
   uebernimmt. Bewusst ruhig gehalten – kein Konfetti, kein Haken-Icon. */
function BuchungsLoader({phase,sel,tag,slot,anz,name,zitat}){
  const g=phase==='gebucht';
  return <LoaderShell img={P+sel.img} zoom={g}>
    {!g?<div key="s">
      <Punkte/>
      <Zitat z={zitat} delay={.15}/>
      <p className="mg-p" style={{marginTop:30,maxWidth:460,animation:'mg-rise .9s .9s var(--ease) both'}}>Einen Moment – wir tragen deinen Termin ein{anz?' und bestätigen deine Anzahlung':''}.</p>
      <p className="mg-p" style={{fontSize:13,marginTop:12,animation:'mg-rise .9s 1.7s var(--ease) both'}}>Bitte schließe dieses Fenster nicht.</p>
    </div>:<div key="f">
      <div className="mg-eyebrow" style={{animation:'mg-rise .8s var(--ease) both'}}>Gebucht</div>
      <h2 className="mg-h2" style={{margin:'18px 0 0',animation:'mg-rise .8s .25s var(--ease) both'}}>Dein Termin steht{name?', '+name:''}.</h2>
      <p className="mg-p" style={{marginTop:20,fontSize:16,animation:'mg-rise .8s .6s var(--ease) both'}}>{fmtISO(tag)}, {slot} Uhr — wir freuen uns auf dich.</p>
      <div style={{width:0,height:1,background:'var(--ink)',marginTop:34,animation:'mg-line 1.2s .8s var(--ease) forwards'}}></div>
    </div>}
  </LoaderShell>;
}

function Done({sel,tag,slot,staff,daten,total,persons}){
  const ics='data:text/calendar;charset=utf-8,'+encodeURIComponent('BEGIN:VCALENDAR\nVERSION:2.0\nBEGIN:VEVENT\nSUMMARY:'+sel.name+' – La Vie Wildeshausen\nLOCATION:Burgstraße 7, 27793 Wildeshausen\nDESCRIPTION:Termin um '+slot+' Uhr\nEND:VEVENT\nEND:VCALENDAR');
  return <section className="mg-sec">
    <div className="mg-narrow" style={{maxWidth:900}}>
      <div className="mg-split" style={{border:'1px solid var(--line)'}}>
        <img loading="lazy" src={P+sel.img} alt="" style={{width:'100%',height:'100%',minHeight:320,objectFit:'cover'}}/>
        <div style={{padding:'clamp(36px,4vw,58px)'}}>
          <div className="mg-eyebrow">Bestätigt</div>
          <h1 className="mg-h2" style={{margin:'16px 0 0',fontSize:'clamp(26px,3vw,38px)'}}>Wir freuen uns auf dich{daten.name?', '+daten.name.split(' ')[0]:''}.</h1>
          <div style={{marginTop:26,borderTop:'1px solid var(--line)'}}>
            {[['Behandlung',sel.name],['Termin',fmtISO(tag)+', '+slot+' Uhr'],['Bei',staff==='Egal'?'nächste freie Mitarbeiterin':staff],[persons>1?'Personen':'Gesamt',persons>1?persons:total+' €']].map(([l,v])=><div key={l} style={{display:'flex',justifyContent:'space-between',gap:16,padding:'12px 0',borderBottom:'1px solid var(--line)',fontSize:14.5}}><span style={{color:'var(--ink-mute)'}}>{l}</span><span style={{textAlign:'right'}}>{v}</span></div>)}
          </div>
          <p className="mg-p" style={{fontSize:13.5,marginTop:18}}>Du erhältst gleich eine Bestätigung per E-Mail. Wir erinnern dich 24 Stunden vorher{daten.erinnerung&&daten.erinnerung!=='email'?' per '+(daten.erinnerung==='sms'?'SMS':'WhatsApp'):''}.</p>
          {(daten.m_email||daten.m_wa)&&<p className="mg-p" style={{fontSize:12.5,marginTop:10}}>Für {daten.m_email&&daten.m_wa?'Newsletter und WhatsApp':daten.m_email?'den Newsletter':'WhatsApp'} liegt noch eine Bestätigung von dir an – wir schicken dir gleich den Link. Erst danach hörst du von uns.</p>}
          <div style={{display:'flex',gap:12,marginTop:26,flexWrap:'wrap'}}>
            <a href={ics} className="mg-btn">Zum Kalender</a>
            <a href="index.html" className="mg-btn mg-btn--ghost">Zur Startseite</a>
          </div>
        </div>
      </div>
    </div>
  </section>;
}

/* Einwilligungsnachweis: WER hat WANN WOFÜR in WELCHER Textfassung zugestimmt.
   Ohne diesen Datensatz ist jede Einwilligung im Streitfall wertlos – die
   Beweislast liegt beim Studio (Art. 7 Abs. 1 DSGVO). Die IP-Adresse gehört
   dazu, lässt sich aber nur serverseitig sauber erfassen: Der Endpunkt muss
   sie beim Empfang selbst an den Datensatz hängen. */
function consentRecord(daten,ctx){
  const ts=new Date().toISOString();
  const e=(zweck,erteilt,text)=>({zweck,erteilt:!!erteilt,text,ts});
  return {
    version:CONSENT_VERSION, ts, quelle:'buchungsfunnel', url:location.href,
    person:{name:daten.name||'',email:daten.email||'',tel:daten.tel||''},
    eintraege:[
      e('datenschutz_kenntnisnahme',daten.ds,TXT_DS),
      e('stornobedingungen',daten.storno,TXT_STORNO(!!ctx.anzahlung)),
      e('gesundheitsdaten',daten.gesund,TXT_GESUND),
      ...KANAELE.map(([k])=>e('werbung_'+k.slice(2),daten[k],TXT_WERBUNG(k))),
      e('erinnerungskanal',true,'Terminerinnerung 24 h vorher per '+(daten.erinnerung||'email'))
    ],
    /* Vor der Bestätigung darf über diese Kanäle nichts Werbliches raus. */
    bestaetigung:{email:daten.m_email?'ausstehend':'entfällt',whatsapp:daten.m_wa?'ausstehend':'entfällt'},
    buchung:ctx
  };
}
function consentSenden(rec){
  const key='lavie-consent-queue';
  const q=JSON.parse(localStorage.getItem(key)||'[]');
  q.push(rec);localStorage.setItem(key,JSON.stringify(q));
  const api=window.LAVIE_API;
  if(!api){console.info('[La Vie] Kein Backend gesetzt – Einwilligung wartet lokal in der Warteschlange.');return Promise.resolve(null)}
  /* Gibt die Server-Antwort zurück ({ok,kundin,termin,kid}) – die Termin-UUID
     braucht der Anzahlungs-Schritt (Whop). Fehler ⇒ null, der
     Datensatz bleibt in der Warteschlange. */
  return fetch(api+'/consent',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(rec)})
    .then(r=>{if(!r.ok)throw new Error('HTTP '+r.status);return r.json()})
    .then(ant=>{
      const rest=JSON.parse(localStorage.getItem(key)||'[]').filter(x=>x.ts!==rec.ts);
      localStorage.setItem(key,JSON.stringify(rest));
      return ant;})
    .catch(err=>{console.warn('[La Vie] Einwilligung nicht übertragen, bleibt in der Warteschlange:',err.message);return null});
}

/* Anzahlung über Whop – bewusst "optional redirect": Erst wenn der Server
   die Buchung angenommen hat UND /api/anzahlung eine Checkout-URL liefert
   (d.h. der Whop-Zugang ist hinterlegt), wird umgeleitet. In jedem anderen
   Fall (503 = noch nicht konfiguriert, Netzfehler) läuft die Buchung wie
   bisher ohne Zahlung weiter – der Termin ist dann trotzdem angefragt. */
function anzahlungStarten(consentAntwort,ctx){
  const api=window.LAVIE_API;
  if(!api)return;
  /* Nimmt Promise ODER bereits aufgelöste Antwort — der Aufrufer wartet die
     Serverantwort inzwischen selbst ab, um Anfrage-Tage und Serien zu
     erkennen, und reicht dann das fertige Objekt herein. */
  Promise.resolve(consentAntwort).then(ant=>{
    if(!ant||!ant.termin)return null;
    return fetch(api+'/anzahlung',{method:'POST',headers:{'Content-Type':'application/json'},
      body:JSON.stringify({termin:ant.termin,email:ctx.email||'',behandlung:ctx.behandlung,datum:ctx.datum,uhrzeit:ctx.uhrzeit})})
      .then(r=>r.ok?r.json():null).catch(()=>null);
  }).then(a=>{if(a&&a.url)location.href=a.url}).catch(()=>{});
}

/* Rückkehr vom Whop-Checkout (?anzahlung=erfolgt|abgebrochen): Der Funnel-
   Zustand ist nach dem Redirect weg — der Termin ist aber längst angefragt
   (er entsteht VOR der Zahlung). Also keine leere Buchungsmaske zeigen,
   sondern eine ehrliche Bestätigung. */
function AnzahlungRueckkehr({status}){
  const ok=status==='erfolgt';
  return <section className="mg-sec">
    <div className="mg-wrap" style={{maxWidth:640,padding:'clamp(60px,10vw,120px) 0',textAlign:'left'}}>
      <div className="mg-eyebrow">{ok?'Anzahlung erhalten':'Zahlung abgebrochen'}</div>
      <h1 className="mg-h1" style={{margin:'18px 0 0',fontSize:'clamp(32px,4vw,54px)'}}>
        {ok?'Danke – dein Termin ist eingetragen.':'Dein Termin ist trotzdem eingetragen.'}</h1>
      <p className="mg-p" style={{marginTop:20,maxWidth:520}}>
        {ok
          ?'Wir haben deine Anzahlung über 25 € erhalten – sie wird vollständig auf den Behandlungspreis angerechnet. Du bekommst in Kürze eine Bestätigung per E-Mail.'
          :'Die Zahlung wurde nicht abgeschlossen. Dein Termin ist aber eingetragen – wir melden uns bei dir wegen der Anzahlung, oder du rufst uns kurz an: 04431 7209569.'}</p>
      <div style={{marginTop:30}}>
        <a className="mg-btn" href="/">Zur Startseite</a>
      </div>
    </div>
  </section>;
}

function MgBuchung(){
  const anzStatus=new URLSearchParams(location.search).get('anzahlung');
  const [step,setStep]=useState(0);
  const [sel,setSel]=useState(null);
  const [addons,setAddons]=useState([]);
  const [persons,setPersons]=useState(1);
  const [staff,setStaff]=useState('');
  const [tag,setTag]=useState('');
  const [slot,setSlot]=useState('');
  const [daten,setDaten]=useState(GUTSCHEIN_CODE?{code:GUTSCHEIN_CODE}:{});
  const [phase,setPhase]=useState('');
  const [buchPhase,setBuchPhase]=useState('');
  const [zitat,setZitat]=useState(null);
  const [fin,setFin]=useState(false);
  /* Verfügbarkeit kommt seit 26.08.2026 fertig vom Server (db/013, /api/frei):
     das Viertelstunden-Raster des Tages inklusive Puffer, Sperrzeiten und
     Öffnungszeiten. null = lädt gerade. freiV erzwingt einen Neuabruf (z. B.
     nachdem der Server eine Buchung als 'belegt' abgewiesen hat).
     Vorher rechnete das Frontend die Kapazität selbst nach — dieselbe Regel
     an zwei Orten zu pflegen war auf Dauer nicht haltbar. */
  const [frei,setFrei]=useState(null);
  const [freiV,setFreiV]=useState(0);
  const [belegtHinweis,setBelegtHinweis]=useState('');
  /* Monatskalender über den ganzen Buchungshorizont. */
  const [monat,setMonat]=useState(()=>{const d=new Date();return new Date(d.getFullYear(),d.getMonth(),1)});
  const [tage,setTage]=useState(null);
  const [horizontBis,setHorizontBis]=useState('');
  /* Weitere Termine im Voraus (Serie). */
  const [weitere,setWeitere]=useState([]);

  const addSum=ADDONS.filter(([n])=>addons.includes(n)).reduce((s,[,p])=>s+p,0);
  const total=(sel?sel.preis:0)+addSum;
  /* Behandlungsdauer + Zusatzzeit der gewählten Add-ons. Geht an /api/frei
     und in die Buchung — sonst blockte eine 90-Min-Behandlung mit
     Wimpernfärben nur 90 statt 105 Minuten, und der Folgetermin läge zu früh
     (Ansage Svetlana, 26.08.2026). Der Erstbesuchs-Zuschlag von 30 Minuten
     kommt zusätzlich vom Server. */
  const addonMin=ADDONS.filter(([n])=>addons.includes(n)).reduce((s,a)=>s+(a[4]||0),0);
  const behMin=sel?(()=>{const m=(String(sel.dur||'').match(/\d+/g)||[]).map(Number);return m.length?Math.max(...m):60})():60;
  const dauerMin=behMin+addonMin;

  /* Monatsübersicht: welche Tage sind zu, welche auf Anfrage, welche voll. */
  useEffect(()=>{
    const api=window.LAVIE_API;
    if(!api){setTage([]);return}
    let aktiv=true;
    const von=new Date(monat.getFullYear(),monat.getMonth(),1);
    const bis=new Date(monat.getFullYear(),monat.getMonth()+1,0);
    fetch(api+'/tage?von='+isoVon(von)+'&bis='+isoVon(bis)+(staff?'&mit='+encodeURIComponent(staff):''))
      .then(r=>r.ok?r.json():null)
      .then(d=>{if(!aktiv)return;
        setTage(d&&Array.isArray(d.tage)?d.tage:[]);
        if(d&&d.horizont_bis)setHorizontBis(d.horizont_bis)})
      .catch(()=>{if(aktiv)setTage([])});
    return()=>{aktiv=false};
  },[monat,staff]);

  /* Slots des gewählten Tages — die Dauer der Behandlung geht mit, damit gar
     nicht erst Zeiten angeboten werden, die nicht mehr in den Tag passen. */
  useEffect(()=>{
    if(!tag){setFrei(null);return}
    let aktiv=true;setFrei(null);
    const api=window.LAVIE_API;
    if(!api){setFrei({ok:true,slots:[]});return}
    const q='?datum='+tag+'&minuten='+dauerMin+'&personen='+persons
      +(staff?'&mit='+encodeURIComponent(staff):'');
    fetch(api+'/frei'+q)
      .then(r=>r.ok?r.json():null)
      /* Abruf gescheitert → leeres Raster statt falscher Zusagen; der
         Server-Wächter in lavie_ingest fängt eine Kollision ohnehin hart ab. */
      .then(d=>{if(aktiv)setFrei(d&&d.ok?d:{ok:true,slots:[]})})
      .catch(()=>{if(aktiv)setFrei({ok:true,slots:[]})});
    return()=>{aktiv=false};
  },[tag,freiV,dauerMin,persons,staff]);

  const slotFrei=z=>{
    if(!frei||!Array.isArray(frei.slots))return false;
    const s=frei.slots.find(x=>x.zeit===z);
    return !!(s&&s.frei);
  };
  /* Wird die gewählte Zeit durch einen Wechsel von Kraft, Personenzahl oder
     Behandlung unmöglich, löst sich die Auswahl von selbst. */
  useEffect(()=>{if(slot&&frei&&Array.isArray(frei.slots)&&!slotFrei(slot))setSlot('')},[frei]);
  /* Tageswechsel verwirft die Uhrzeit — sonst bliebe eine Zeit stehen, die es
     am neuen Tag vielleicht gar nicht gibt. */
  useEffect(()=>{setSlot('')},[tag]);

  const anfrageTag=!!(frei&&frei.anfrage);
  const can=step===0?!!sel:step===1?!!(staff&&tag&&slot&&frei&&slotFrei(slot)):true;

  /* Beschriftung des Haupt-Buttons. Sie muss die Wahrheit sagen: an einem
     Anfrage-Tag wird NICHTS abgebucht, bei einer Serie ist der Betrag
     addiert. Ein Button "25 € anzahlen & buchen", der in Wahrheit nur eine
     Anfrage stellt, wäre ein Versprechen, das die Seite nicht hält. */
  const anzGesamt=(1+weitere.length)*ANZ_BETRAG;
  const ctaLabel=step===0?'Weiter zum Termin'
    :step===1?'Verfügbarkeit prüfen'
    :step===2?'Weiter zur Bestätigung'
    :anfrageTag?'Termin unverbindlich anfragen'
    :anzPflicht(total*persons)?anzGesamt+' € anzahlen & buchen'
    :'Verbindlich buchen';
  /* Nach jedem Schrittwechsel an den Anfang.
     Wichtig: Das darf NICHT synchron im Klick-Handler passieren. Der
     Ladebildschirm sperrt das Scrollen und stellt beim Entsperren die alte
     Position wieder her (useScrollLock in mg-ui.jsx) – ein sofortiges
     scrollTo wuerde davon ueberschrieben, und man landete wieder unten.
     Als Effekt laeuft es garantiert nach dem Aufraeumen der Sperre. */
  useEffect(()=>{
    const id=requestAnimationFrame(()=>window.scrollTo({top:0,behavior:'smooth'}));
    return()=>cancelAnimationFrame(id);
  },[step,fin]);
  const next=()=>{
    if(step===0){setStep(1);return}
    if(step===1){
      /* Beim Pruefen darf auch die behandlungsbezogene Zeile drankommen. */
      setZitat(waehle([{t:sel.quote},...ZITATE]));
      setPhase('check');setTimeout(()=>setPhase('grats'),5000);setTimeout(()=>{setPhase('');setStep(2)},8000);return}
    if(step===2){if(window.__validate3&&window.__validate3())setStep(3);return}
    if(step===3){window.__pay&&window.__pay()}
  };
  if(anzStatus)return <div><window.MgNav/><AnzahlungRueckkehr status={anzStatus}/><window.MgFooter/></div>;
  if(fin)return <div><window.MgNav/><Done sel={sel} tag={tag} slot={slot} staff={staff} daten={daten} total={total*persons} persons={persons}/><window.MgFooter/></div>;
  return <div>
    <window.MgNav/>
    <section style={{borderBottom:'1px solid var(--line)',background:'var(--paper-2)'}}>
      <div className="mg-wrap" style={{padding:'clamp(40px,5vw,70px) 0'}}>
        <div className="mg-eyebrow">Online-Buchung</div>
        <h1 className="mg-h1" style={{margin:'16px 0 0',fontSize:'clamp(32px,4vw,54px)'}}>Dein Termin bei La Vie</h1>
        <p className="mg-p" style={{marginTop:16,maxWidth:520}}>In vier Schritten gebucht – rund um die Uhr, mit sofortiger Bestätigung.</p>
      </div>
    </section>
    <section className="mg-sec" style={{paddingTop:'clamp(30px,3.4vw,48px)'}}>
      <div className="mg-wrap">
        <Steps step={step}/>
        <div style={{display:'grid',gridTemplateColumns:'minmax(0,1fr) 340px',gap:'clamp(28px,4vw,64px)',alignItems:'start'}} className="mg-book">
          <div key={step} style={{animation:'mg-rise .6s var(--ease) both'}}>
            {step===0&&<Step1 sel={sel} setSel={setSel} addons={addons} setAddons={setAddons} persons={persons} setPersons={setPersons}/>}
            {step===1&&<Step2 staff={staff} setStaff={setStaff} tag={tag} setTag={setTag} slot={slot}
              setSlot={s=>{setSlot(s);if(belegtHinweis)setBelegtHinweis('')}}
              frei={frei} freiLaedt={!!tag&&frei===null} belegtHinweis={belegtHinweis}
              monat={monat} setMonat={setMonat} tage={tage} tageLaedt={tage===null}
              horizontBis={horizontBis} weitere={weitere} setWeitere={setWeitere}
              sel={sel} dauerMin={dauerMin}/>}
            {step===2&&<Step3 daten={daten} setDaten={setDaten}/>}
            {step===3&&<Step4 sel={sel} addons={addons} staff={staff} tag={tag} slot={slot} daten={daten} setDaten={setDaten} persons={persons} total={total} weitere={weitere} anfrageTag={anfrageTag} onDone={()=>{
              const antwort=consentSenden(consentRecord(daten,{behandlung:sel.name,datum:tag,uhrzeit:slot,mitarbeiterin:staff,personen:persons,
                summe:rabattWert(total*persons,codeNorm(daten.code)),summe_vor_rabatt:total*persons,gutschein:codeNorm(daten.code)||undefined,
                /* Behandlungsdauer für die Kalender-Blockung; bei Erstbesuch
                   legt der Server automatisch 30 Beratungs-Minuten drauf. */
                minuten:dauerMin,
                /* Weitere Termine im Voraus. Der Server legt sie als Serie an
                   und addiert die Anzahlung — die Tage werden ja alle geblockt. */
                weitere:weitere.length?weitere:undefined,
                anzahlung:anzPflicht(total*persons)?ANZ_BETRAG:0}));
              /* Drei Wege in die Zahlung, und der Server entscheidet welcher:
                 - Anfrage-Tag (Mo/Sa): gar keiner. Erst nach der Bestätigung.
                 - Serie: /zahlung?t=<token> mit der ADDIERTEN Summe.
                 - Einzeltermin: der feste 25-€-Plan wie bisher. */
              if(anzPflicht(total*persons)){
                Promise.resolve(antwort).then(ant=>{
                  if(!ant||ant.ok===false)return;
                  if(ant.anfrage)return;
                  if(ant.zahl_token){location.href='/zahlung?t='+ant.zahl_token;return}
                  anzahlungStarten(ant,{email:daten.email,behandlung:sel.name,datum:tag,uhrzeit:slot});
                }).catch(()=>{});
              }
              /* Erst der Wartescreen, dann die Bestaetigungsseite.
                 Hier bewusst nur die Zitate der Kundin, keine Kategoriezeile.
                 NEU: Die Serverantwort wird abgewartet — weist der Wächter die
                 Zeit als 'belegt' ab (Rennen zweier Buchungen), geht es zurück
                 zur Zeitwahl statt in eine falsche Bestätigung. Antwort null
                 (offline, Warteschlange) läuft wie bisher weiter. */
              setZitat(waehle(ZITATE));
              setBuchPhase('senden');
              Promise.all([Promise.resolve(antwort).catch(()=>null),new Promise(r=>setTimeout(r,2300))]).then(([ant])=>{
                if(ant&&ant.ok===false&&ant.fehler==='belegt'){
                  setBuchPhase('');
                  setBelegtHinweis('Diese Zeit wurde gerade eben anderweitig vergeben – bitte wähle einen anderen Slot. Es wurde noch nichts gebucht und nichts bezahlt.');
                  setSlot('');setFreiV(v=>v+1);setStep(1);
                  return;
                }
                setBuchPhase('gebucht');
                setTimeout(()=>{setBuchPhase('');setFin(true)},1800);
              });
            }}/>}
            <div style={{display:'flex',justifyContent:'space-between',gap:14,marginTop:36,borderTop:'1px solid var(--line)',paddingTop:24}}>
              {step>0?<button className="mg-btn mg-btn--ghost" onClick={()=>setStep(step-1)}>Zurück</button>:<span></span>}
              <button className="mg-btn" onClick={next} style={{opacity:can?1:.4,pointerEvents:can?'auto':'none'}}>{ctaLabel}</button>
            </div>
          </div>
          <aside className="mg-hide-m" style={{position:'sticky',top:104}}>
            <Aside sel={sel} addons={addons} persons={persons} total={total} staff={staff} tag={tag} slot={slot} step={step} next={next} can={can} code={codeNorm(daten.code)} label={ctaLabel} weitere={weitere}/>
            <div style={{marginTop:16,border:'1px solid var(--line)',padding:'20px 22px'}}>
              <div className="mg-eyebrow">Lieber persönlich?</div>
              <p className="mg-p" style={{fontSize:13.5,margin:'10px 0 14px'}}>Ruf uns an – wir finden gemeinsam den passenden Termin.</p>
              <a href="tel:+4944317209569" className="mg-link" style={{fontSize:12}}>04431 720 95 69</a>
            </div>
          </aside>
        </div>
      </div>
    </section>
    {sel&&step<2&&<div className="mg-sticky">
      <div style={{display:'flex',alignItems:'center',gap:14,background:'#fff',borderTop:'1px solid var(--line)',padding:'12px clamp(16px,4vw,32px)'}}>
        <img loading="lazy" src={P+sel.img} alt="" style={{width:44,height:44,objectFit:'cover',flexShrink:0}}/>
        <div style={{flex:1,minWidth:0}}>
          <div style={{fontSize:14.5,whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>{sel.name}</div>
          <div style={{fontSize:12.5,color:'var(--ink-mute)'}}>{sel.dur} · {total*persons} €</div>
        </div>
        <button className="mg-btn" style={{height:42}} onClick={next} disabled={!can}>{step===0?'Weiter':'Prüfen'}</button>
      </div>
    </div>}
    {phase&&<Loader phase={phase} sel={sel} tag={tag} slot={slot} zitat={zitat}/>}
    {buchPhase&&<BuchungsLoader phase={buchPhase} sel={sel} tag={tag} slot={slot} anz={anzPflicht(total*persons)} name={(daten.name||'').split(' ')[0]} zitat={zitat}/>}
    <window.MgFooter/>
    <window.MgCookie/>
  </div>;
}
window.MgBuchung=MgBuchung;
