// ============================================================ // Monarch — Socle partagé des simulateurs (window.MSC) // Chargé AVANT les fichiers de simulateurs. Aucune dépendance externe. // ============================================================ (function () { const { useState, useEffect, useRef } = React; const C = { blanc: "#FFFFFF", albatre: "#FAF9F6", encre: "#1B1D1C", ardoise: "#5B7C99", ardoiseF: "#3D5468", sauge: "#5C6F5C", saugeF: "#48584A", gris: "#737373", grisC: "#9A9A95", trait: "#E4E1DA" }; const SERIF = "'Fraunces', Georgia, serif"; const BODY = "'Archivo', system-ui, sans-serif"; const CAL_LINK = "hugo-groslambert/diagnostic1h"; const CAL_ORIGIN = "https://cal.eu"; // ---- fiscalité partagée ---- const IS_SEUIL = 42500; function IS(base) { const bas = Math.min(Math.max(base, 0), IS_SEUIL); const haut = Math.max(0, base - IS_SEUIL); return bas * 0.15 + haut * 0.25; } const flatTax = (m) => m * 0.314; // Barème progressif DMTG en ligne directe (par part, après abattement) const DMTG_TRANCHES = [ [8072, 0.05], [12109, 0.10], [15932, 0.15], [552324, 0.20], [902838, 0.30], [1805677, 0.40], [Infinity, 0.45] ]; function dmtg(baseTaxable) { let reste = Math.max(0, baseTaxable), prev = 0, tax = 0; for (const [plafond, taux] of DMTG_TRANCHES) { const part = Math.min(reste, plafond - prev); if (part <= 0) break; tax += part * taux; reste -= part; prev = plafond; if (reste <= 0) break; } return tax; } // ---- format ---- const euro0 = new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR", maximumFractionDigits: 0 }); const fmt = (n) => euro0.format(Math.round(n || 0)); const pct = (n, b) => b > 0 ? Math.round((n / b) * 100) : 0; // ---- CountUp ---- function useCountUp(target, run, duration) { const [val, setVal] = useState(0); useEffect(() => { if (!run) { setVal(0); return; } const d = duration || 1100; let raf, start; const ease = (t) => 1 - Math.pow(1 - t, 3); const tick = (ts) => { if (!start) start = ts; const p = Math.min(1, (ts - start) / d); setVal(target * ease(p)); if (p < 1) raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [target, run, duration]); return val; } function CountEuro({ value, run, style }) { return {fmt(useCountUp(value, run))}; } // ---- styles partagés ---- function injectCore() { if (document.getElementById("msc-styles")) return; const s = document.createElement("style"); s.id = "msc-styles"; s.textContent = ` .msc-grid2 { display:grid; grid-template-columns:1fr 1fr; gap:22px; align-items:stretch; } .msc-inputs { display:grid; grid-template-columns:1fr 1fr; gap:24px 34px; } .msc-line { stroke-dasharray:760; stroke-dashoffset:760; transition:stroke-dashoffset 1s ease; } .msc-line.on { stroke-dashoffset:0; } .msc-soft { opacity:0; transition:opacity .8s ease; } .msc-soft.on { opacity:1; } .msc-slideup { opacity:0; transform:translateY(22px); transition:opacity .6s ease, transform .6s cubic-bezier(.2,.7,.3,1); } .msc-slideup.on { opacity:1; transform:none; } .msc-lockbar { transition: stroke-dashoffset 1.1s ease .2s; } @media (max-width:820px){ .msc-grid2{grid-template-columns:1fr !important;} .msc-inputs{grid-template-columns:1fr !important; gap:20px !important;} } `; document.head.appendChild(s); } // ---- atomes UI ---- function Eyebrow({ children, color }) { return {children}; } function Label({ children }) { return ; } function MoneyInput({ value, onChange, min, max, step }) { return (
{ const n = e.target.value.replace(/[^\d]/g, ""); onChange(n === "" ? "" : Number(n)); }} style={{ fontFamily: SERIF, fontSize: "clamp(26px,3.4vw,40px)", fontWeight: 500, color: C.encre, border: "none", background: "transparent", outline: "none", width: "100%", padding: 0 }} />
{max != null && onChange(Number(e.target.value))} style={{ width: "100%", marginTop: 14, accentColor: C.sauge }} />}
); } function IntInput({ value, onChange, min, max }) { return (
{value}
); } const stepBtn = { fontFamily: BODY, fontSize: 20, width: 46, border: "none", background: "transparent", color: C.encre, cursor: "pointer" }; function Toggle({ options, value, onChange, full }) { return (
{options.map((o) => { const on = o.id === value; return ; })}
); } // Enveloppe de section (cohérente avec le simulateur d'extraction) function Shell({ eyebrow, title, intro, children }) { return (
{eyebrow}

{title}

{intro &&

{intro}

}
{children}
); } function InputsCard({ children, onCompute, computing, busyLabel, label }) { return (
{children}
); } // Encart doctrine — fond anthracite, apparition slide-up function Doctrine({ open, eyebrow, title, children, accent }) { return (
{eyebrow && {eyebrow}}

{title}

{children}
); } // Alerte doctrinale claire (fond clair, bordure gauche) function Alert({ open, title, children }) { return (
Vigilance juridique

{title}

{children}
); } // CTA + portail email (modale) -> ouvre Cal function CTAEmail({ note }) { const book = (e) => { if (window.Cal) { if (e) e.preventDefault(); window.Cal("modal", { calLink: CAL_LINK, config: { layout: "month_view" } }); } }; return (
Réserver mon 1er rendez-vous offert {note &&

{note}

}
); } // Hook : déclenche `reveal` après montage des résultats function useReveal(active, delay) { const [on, setOn] = useState(false); useEffect(() => { if (active) { const id = setTimeout(() => setOn(true), delay || 200); return () => clearTimeout(id); } else setOn(false); }, [active, delay]); return on; } // Jauge horizontale (valeur conservée vs perte hachurée) function Gauge({ segments, base, run, height }) { const [w, setW] = useState(false); useEffect(() => { if (run) { const id = setTimeout(() => setW(true), 60); return () => clearTimeout(id); } else setW(false); }, [run]); return (
{segments.map((s, i) =>
)}
); } function Stat({ label, value, run, color, sub, small }) { return (
{label}
{sub &&
{sub}
}
); } // ---- Capture de leads (email) ---- const LEAD_KEY = "monarch.lead.email"; const LEAD_FULL_KEY = "monarch.lead.full"; function getLeadEmail() { try { return localStorage.getItem(LEAD_KEY) || ""; } catch (e) { return ""; } } function getLeadFull() { try { return JSON.parse(localStorage.getItem(LEAD_FULL_KEY) || "null"); } catch (e) { return null; } } function hasFullProfile() { const f = getLeadFull(); return !!(f && f.nom && f.prenom && f.telephone && f.email); } function leadCapture(info, source) { const data = typeof info === "string" ? { email: info } : (info || {}); if (data.email) { try { localStorage.setItem(LEAD_KEY, data.email); } catch (e) {} } if (data.nom && data.prenom && data.telephone && data.email) { try { localStorage.setItem(LEAD_FULL_KEY, JSON.stringify({ nom: data.nom, prenom: data.prenom, telephone: data.telephone, email: data.email })); } catch (e) {} } try { fetch("lead.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ nom: data.nom || "", prenom: data.prenom || "", telephone: data.telephone || "", email: data.email || "", source: source || "" }) }).catch(() => {}); } catch (e) {} } // Modale d'accès — `full` => nom/prénom/téléphone/email ; sinon email seul function LeadModal({ open, onUnlock, onClose, source, title, subtitle, cta, full }) { const saved = full ? (getLeadFull() || {}) : {}; const [nom, setNom] = useState(saved.nom || ""); const [prenom, setPrenom] = useState(saved.prenom || ""); const [telephone, setTelephone] = useState(saved.telephone || ""); const [email, setEmail] = useState(saved.email || getLeadEmail() || ""); if (!open) return null; const submit = (e) => { e.preventDefault(); if (!/.+@.+\..+/.test(email)) return; if (full && (!nom.trim() || !prenom.trim() || telephone.replace(/[^\d]/g, "").length < 8)) return; const payload = full ? { nom: nom.trim(), prenom: prenom.trim(), telephone: telephone.trim(), email: email.trim() } : { email: email.trim() }; leadCapture(payload, source || "acces"); onUnlock(email); }; const field = { fontFamily: BODY, fontSize: 15, color: C.encre, background: C.blanc, border: `1px solid ${C.trait}`, borderRadius: 2, padding: "14px 14px", outline: "none", width: "100%" }; return (
onClose && onClose()} style={{ position: "fixed", inset: 0, zIndex: 300, display: "flex", alignItems: "center", justifyContent: "center", padding: 20, background: "rgba(244,242,238,.55)", backdropFilter: "blur(7px)", WebkitBackdropFilter: "blur(7px)" }}>
e.stopPropagation()} style={{ position: "relative", background: C.blanc, maxWidth: 480, width: "100%", padding: "clamp(28px,5vw,44px)", borderTop: `3px solid ${C.sauge}`, boxShadow: "0 30px 80px -30px rgba(0,0,0,.4)" }}> {onClose && } Accès réservé

{title || "Accédez aux simulateurs"}

{subtitle || "Renseignez votre adresse professionnelle pour ouvrir l'ensemble des outils d'ingénierie patrimoniale."}

{full &&
setPrenom(e.target.value)} placeholder="Prénom" autoComplete="given-name" style={field} /> setNom(e.target.value)} placeholder="Nom" autoComplete="family-name" style={field} />
setTelephone(e.target.value)} placeholder="Téléphone" autoComplete="tel" style={field} />
} setEmail(e.target.value)} placeholder="Adresse e-mail" autoComplete="email" style={field} />

Confidentiel · vos données ne sont jamais cédées · vous pourrez vous désinscrire à tout moment.

); } // Bloc pédagogique « comment ça marche » (étapes simples) function Explainer({ title, steps }) { return (
Comment ça marche

{title || "En quelques mots"}

{steps.map((s, i) =>
{i + 1}
{s}
)}
); } // Avantages / Inconvénients — présentation honnête function ProsCons({ open, pour, contre, note }) { const tick = (color) => ; const col = (titre, items, color, bg) => (
{titre}
{items.map((it, i) =>
{tick(color)} {it}
)}
); return (
En toute transparence
{col("Avantages", pour, C.saugeF, C.albatre)} {col("Inconvénients & limites", contre, "#a23b3b", C.blanc)}
{note &&

{note}

}
); } window.MSC = { C, SERIF, BODY, CAL_LINK, CAL_ORIGIN, IS, flatTax, dmtg, fmt, pct, useCountUp, CountEuro, injectCore, useReveal, Eyebrow, Label, MoneyInput, IntInput, Toggle, Shell, InputsCard, Doctrine, Alert, CTAEmail, Gauge, Stat, Explainer, ProsCons, getLeadEmail, leadCapture, LeadModal, getLeadFull, hasFullProfile }; })();