Votre diagnostic en un coup d'œil
Votre projet
Votre profil investisseur
Votre portefeuille recommandé
Profil
Voir le diagnostic
Maintenant

Pour passer à l'action.

Prenez rendez-vous avec un conseiller pour transformer ce pré-diagnostic en stratégie sur mesure.

Prendre rendez-vous →
Affichage du diagnostic complet dans 15 sec…
Voir le diagnostic
Chapitre suivant

Bienvenue

3 outils à votre disposition

Trois simulateurs à votre service

01 · 6 questions · 2 min
Mon profil d'investisseur
02 · 18 questions · 8 min
Pré-diagnostic patrimonial
03 · 3 champs · 30 sec
Économie d'impôt PER

Identifiez en 2 minutes votre profil — prudent, équilibré ou dynamique — et obtenez une orientation d'allocation indicative entre épargne sécurisée, SCPI et actifs dynamiques.

Une lecture détaillée de votre profil en 18 questions — un pré-diagnostic conçu pour préparer votre premier rendez-vous, sans remplacer le bilan complet réalisé en cabinet.

Calculez en 30 secondes votre économie d'impôt potentielle pour un versement sur un Plan d'Épargne Retraite, selon votre tranche marginale d'imposition.

Maintenant

À vous de jouer.

Trois outils, quelques minutes. Choisissez votre point de départ.

Cabinet de conseil patrimonial · Metz
HD Finance
Audit
Placements
Retraite
Fiscalité
Immobilier
Transmission

Construire, protéger, transmettre. HD Finance vous accompagne dans la durée, avec l'attention d'un conseiller dédié.

+20ans
d'expertise
+300
clients suivis
360°
approche globale
Faire défiler
Partenaires & distributeurs sélectionnés
Notre métier

Qu'est-ce qu'un cabinet CGP ?

Un Cabinet de Conseil en Gestion de Patrimoine, c'est un cabinet qui sélectionne librement, pour ses clients, les meilleures solutions du marché. Aucun produit maison — votre seul intérêt en ligne de mire.

Cabinet à taille humaine

Cabinet familial fondé en 2014 à Châtel-Saint-Germain, près de Metz. Une équipe restreinte au service de votre patrimoine.

Conseil de proximité

Aucun produit maison

Toutes classes d'actifs, sélectionnées librement auprès des meilleurs établissements.

Architecture ouverte

Sur-mesure

Une écoute préalable, un conseil personnalisé. Pas de formule type.

Conseil personnalisé

Suivi dans la durée

Votre situation évolue. Nos recommandations aussi.

Accompagnement long terme

Cabinet de Conseil en Gestion de Patrimoine fondé en 2014 à Metz · Adhérent CNCGP · AMF · ACPR · ORIAS

Le bénéfice concret d'un cabinet CGP

Sans CGP ou avec un cabinet CGP ?

Critère
Banque traditionnelle
Cabinet CGP HD Finance
Approche relationnelle
Structure standardisée
Cabinet à taille humaine
Choix des supports
Produits maison
Architecture ouverte
Suivi du dossier
Conseiller qui change
Référent dédié
Stratégie patrimoniale
Approche produit
Vision globale 360°
Rémunération transparente
Opaque, par produit
Affichée à l'écrit
Données indicatives — chaque situation est différente, parlons-en lors d'un premier rendez-vous.
Notre approche

Votre expérience client.

Chez HD Finance, votre accompagnement s'articule autour de trois rendez-vous clés et six étapes précises. Un protocole structuré, transparent et entièrement dédié à votre situation — chaque étape éclaire votre dossier en temps réel.

1er rendez-vous
Étape 1
Faire connaissance, sans engagement
Un premier échange sans engagement pour faire connaissance.
Ce que vous obtenez
Le Document d'Entrée en Relation (DER)
Signature du document d'entrée en relation (DER)
01
02
Étape 2
On apprend à vous connaître
Nous comprenons votre situation, vos projets et vos objectifs.
Ce que vous obtenez
Une cartographie claire de votre patrimoine
2ème rendez-vous
Étape 3
On analyse votre situation et on définit nos priorités
Un diagnostic complet et 3 à 5 priorités définies ensemble.
Ce que vous obtenez
Lettre de mission et annexes
Signature de la lettre de mission
03
04
Étape 4
Plusieurs solutions à comparer
Plusieurs options comparées pour chaque priorité — frais, risques, avantages.
Ce que vous obtenez
Documentation réglementaire complète
3ème rendez-vous
Étape 5
Un rapport écrit avant de signer quoi que ce soit
Un rapport écrit qui résume nos recommandations avant toute signature.
Ce que vous obtenez
Déclaration d'adéquation & rapport écrit
Signature de la déclaration d'adéquation
05
06
Étape 6
On lance et on reste à vos côtés
Reporting trimestriel et bilan annuel pour suivre votre patrimoine.
Ce que vous obtenez
Reporting & bilans annuels d'adéquation
Commencez votre parcours

Prenez rendez-vous,
sans engagement.

Premier entretien offert. Nous analysons votre situation patrimoniale et définissons ensemble votre feuille de route. À Châtel-Saint-Germain ou en visioconférence.

Prendre rendez-vous gratuitement →
Votre parcours patrimonial

Votre situation, notre méthode.

Cinq domaines d'expertise. Un accompagnement 360°. Nous construisons avec vous une stratégie patrimoniale cohérente, durable et parfaitement adaptée.

01
Audit & Bilan Patrimonial
Diagnostic 360° · Analyse fiscale · Bilan complet
Découvrir
02
Placements Financiers
Assurance-vie · PER · Private Equity · Diversification
Découvrir
03
Retraite & Prévoyance
PER individuel · Simulation · Protection famille
Découvrir
04
Optimisation Fiscale
Réduction IR & IFI · Stratégies légales · Coordination notaire
Découvrir
05
Immobilier d'Investissement
SCPI européennes · LMNP · Nue-propriété · LMP
Découvrir
06
Transmission & Succession
Donations · Démembrement · Pacte Dutreil · Assurance-vie
Découvrir
Vos guides patrimoniaux

Nous vous aidons
à avancer avec confiance.

Quatre profils complémentaires, un même engagement : vous accompagner avec rigueur, transparence et bienveillance dans toutes les décisions patrimoniales qui jalonneront votre vie.

Plus de 95 années cumulées d'expertise au service de plus de 300 clients suivis sur le long terme. Chez HD Finance, vous bénéficiez à la fois de la solidité d'une équipe expérimentée et de la fraîcheur d'un cabinet à taille humaine.

Didier Henrion
Didier Henrion
Gérant & CGP Fondateur
Didier Henrion
Gérant & CGP Fondateur
AUREP +20 ans Generali Fondateur 2014 CIF · IAS · CJA
Prendre rendez-vous
Quentin Wirtz
Quentin Wirtz
Conseiller en Gestion de Patrimoine
Quentin Wirtz
Conseiller en Gestion de Patrimoine
NEOMA BS Primonial REIM CIF · IAS — ORIAS Dirigeants d'entreprise
Prendre rendez-vous
Loris Witkowski
Loris Witkowski
Conseiller en Gestion de Patrimoine
Loris Witkowski
Conseiller en Gestion de Patrimoine
Droit Privé — Nancy Ingénierie patrimoniale Depuis 2025
Prendre rendez-vous
Christian Lefaure
Christian Lefaure
Gestionnaire spécialisé en gestion d'actifs
Christian Lefaure
Gestionnaire spécialisé en gestion d'actifs
+40 ans d'expérience Allocations d'actifs Assurance FFCI
Prendre rendez-vous
Vos garanties

Un cadre réglementaire
complet et rigoureux.

Un excellent conseil patrimonial ne suffit pas — vous devez aussi vous sentir en sécurité. Nos accréditations couvrent l'ensemble du champ patrimonial, financier, assurantiel, juridique et immobilier.

Notre exigence : un conseil rigoureux, légalement encadré, et une information transparente — notamment sur les modes de rémunération du cabinet (honoraires et, le cas échéant, commissions perçues auprès des producteurs partenaires) conformément aux obligations de la DDA et de MIF II. À ce titre, le cabinet exerce une activité de conseil non indépendant au sens de MIF II.

Nos habilitations

Des agréments sur tout le champ patrimonial.

IAS
Intermédiaire en Assurance — habilités à proposer et distribuer des contrats d'assurance-vie et de prévoyance.
CJA
Conseil Juridique en Accessoire — accompagnement sur les aspects juridiques liés à la gestion de patrimoine.
CIF
Conseiller en Investissements Financiers — agréés AMF pour conseiller sur les placements et instruments financiers.
Carte T
Carte de transaction immobilière — habilités à exercer les activités d'agent immobilier et d'intermédiation.
Classement Décideurs Magazine

Classés Excellent 6 années consécutives

Le cabinet HD Finance figure chaque année dans le classement des meilleurs conseillers en gestion de patrimoine de France.

Avis clients

Ce que pensent nos clients.

Au-delà des distinctions et des agréments, le meilleur regard sur notre métier reste celui de ceux qui nous accordent leur confiance. Voici, en toute transparence, quelques témoignages anonymisés de clients que nous accompagnons au quotidien dans la construction et la protection de leur patrimoine.

4,9/5
Avis Google certifiés
Restez informé

Actualités
patrimoniales 2026

Assurance-vie · Innovation
05 Mai 2026

Generali innove sur ses contrats d'assurance-vie

Generali intègre de nouvelles unités de compte axées sur le Private Equity accessible dès quelques milliers d'euros, ainsi qu'un fonds en euros dynamisé.

Lire la suite →
Immobilier · SCPI
10 Mars 2026

SCPI : vers un marché stabilisé et opportuniste

Le marché des SCPI se stabilise au T1 2026. HD Finance conseille les SCPI diversifiées européennes et sectorielles affichant des taux de distribution très attractifs.

Lire la suite →
Retraite · Fiscalité
15 Janvier 2026

Le PER au sommet de son attractivité en 2026

Le PER individuel s'impose comme le couteau suisse de la gestion patrimoniale. Intérêt record des cadres et professions libérales de Moselle.

Lire la suite →
Questions fréquentes

Ce que vous
nous demandez.

Notre processus commence par vous. Pourquoi votre patrimoine est-il important pour vous ?

Prendre rendez-vous →

Un Conseiller en Gestion de Patrimoine (CGP) analyse votre situation patrimoniale globale — fiscale, juridique, financière et immobilière — pour vous orienter vers des solutions adaptées à vos objectifs et à votre profil. HD Finance est un cabinet régulé (CIF — ANACOFI-CIF N°E001823, IAS/COBSP — ORIAS N°08 04 619), tenu au devoir de conseil renforcé prévu par MIF II et la DDA. Le cabinet exerce une activité de conseil non indépendant au sens de MIF II et perçoit, le cas échéant, des commissions auprès des producteurs partenaires, dans la transparence prévue par la réglementation.

Nous accompagnons des particuliers, professionnels libéraux, chefs d'entreprise et cadres du Grand Est souhaitant valoriser leur capital, préparer leur retraite, optimiser leur fiscalité ou organiser leur succession.

Le premier entretien est offert et sans engagement. Nous prenons le temps de comprendre votre situation, vos objectifs et vos contraintes. À l'issue de cet échange, nous vous présentons un premier diagnostic patrimonial.

Oui. Notre approche inclut des stratégies légales de réduction de l'impôt sur le revenu (IR) et de l'IFI, adaptées à votre tranche marginale d'imposition (TMI).

Convertir votre épargne en revenus complémentaires à la retraite nécessite une structuration sur mesure — rentes, retraits programmés, revenus immobiliers — adaptée à votre profil, vos objectifs et votre situation fiscale.

Non. Bien que notre cabinet soit basé au 32 rue de Verdun, 57160 Châtel-Saint-Germain (proche Metz), nous proposons des rendez-vous en visioconférence. Lundi au vendredi de 9h à 12h et de 14h à 18h, sur rendez-vous uniquement.

Nous contacter

Parlons de
votre projet.

Premier entretien offert et sans engagement. Rencontrez-nous à Châtel-Saint-Germain, près de Metz, ou en visioconférence.

Adresse
Horaires
Lun – Ven : 9h00 – 12h00 · 14h00 – 18h00
Sur rendez-vous uniquement
Préparer notre échange

En quelques mots, parlez-nous de vous.

Trois informations rapides pour que votre conseiller arrive préparé. Vous accéderez ensuite à notre agenda pour réserver votre créneau.

① Votre projet
② Montant à investir
③ Vos coordonnées

Premier entretien offert · Sans engagement · Accéder directement à l'agenda

Centre d'information

Actualités & ressources

Restez informé des dernières évolutions patrimoniales, fiscales et financières. Découvrez aussi nos fiches thématiques pour mieux comprendre les solutions d'investissement.

Articles récents

Dernières publications

Assurance-vie · Innovation
05 Mai 2026

Generali innove sur ses contrats d'assurance-vie

Generali intègre de nouvelles unités de compte axées sur le Private Equity accessible dès quelques milliers d'euros, ainsi qu'un fonds en euros dynamisé avec un objectif de 3,5%.

Lire l'article →
Immobilier · SCPI
10 Mars 2026

SCPI : vers un marché stabilisé et opportuniste

Le marché des SCPI se stabilise au T1 2026. HD Finance conseille les SCPI diversifiées européennes et sectorielles affichant des taux de distribution très attractifs.

Lire l'article →
Retraite · Fiscalité
15 Janvier 2026

Le PER au sommet de son attractivité en 2026

Le PER individuel s'impose comme le couteau suisse de la gestion patrimoniale. Intérêt record des cadres et professions libérales de Moselle.

Lire l'article →
Private Equity · Altaroc
20 Avril 2026

Altaroc : millésime 2026 — TRI cible indicatif de 12 à 18 % brut

Altaroc lance son nouveau millésime avec un portefeuille de fonds de Private Equity de premier rang mondial. Accès au non-coté à partir de 100 000 €. Performances passées non garanties — risque de perte en capital.

Lire l'article →
SCPI · Distribution
28 Février 2026

SCPI Remake Live : taux de distribution 2026 confirmé à 7,25%

Remake AM confirme un taux de distribution parmi les plus élevés du marché pour sa SCPI Remake Live, avec une forte diversification géographique en Europe.

Lire l'article →
Assurance-vie · Taux
05 Février 2026

Fonds en euros : une résistance remarquable en 2026

Malgré la normalisation des taux, les grands assureurs maintiennent des rendements fonds en euros compétitifs. Analyse des meilleures offres du marché.

Lire l'article →
Fiches pratiques

S'informer sur les grandes solutions

Six thématiques clés pour comprendre les principaux outils patrimoniaux à votre disposition.

Assurance Vie
L'enveloppe multi-supports la plus utilisée en France pour épargner, capitaliser et transmettre dans un cadre fiscal avantageux.
Épargne long termeTransmissionFiscalité avantageuse
Découvrir la fiche →
Assurance Vie Luxembourg
Protection renforcée, accès à des actifs premium et neutralité fiscale. La solution préférée des patrimoines élevés en Europe.
Protection maximaleActifs premiumMobilité internationale
Découvrir la fiche →
Private Equity
L'investissement dans des entreprises non cotées, qui a historiquement présenté un potentiel de rendement attractif. Performances passées non garanties — risque de perte en capital.
Potentiel de rendementNon cotéHorizon 8–12 ansRisque de perte
Découvrir la fiche →
SCPI
La "pierre-papier" : investir dans l'immobilier professionnel mutualisé pour percevoir des revenus réguliers sans les contraintes de la gestion locative.
Revenus trimestrielsDiversificationDès 185 €
Découvrir la fiche →
Épargne Salariale
PEE et PERECO : bénéficiez de l'abondement employeur et des avantages fiscaux pour optimiser votre rémunération globale et préparer votre retraite.
Abondement employeurExonération IRRetraite
Découvrir la fiche →
Forêt
Actif tangible et décorrélé des marchés financiers. L'investissement forestier (GFI / SCEF) bénéficie d'une fiscalité spécifique IFI et transmission, sous réserve d'engagement de gestion durable. Risques de marché et climatiques.
Actif tangibleRégime IFI / IRAmendement Monichon
Découvrir la fiche →

`; } // ══════════ GATE GÉNÉRIQUE — capture de leads ══════════ // 🔧 CONFIG ACTIVE : FormSubmit.co (relais d'email gratuit, hébergé en UE, sans inscription) // Le premier mail envoyé via ce système déclenche un mail d'activation envoyé par // FormSubmit à contact@henrion-cgpi.com. Cliquer sur le lien d'activation valide // l'adresse, après quoi tous les leads suivants arrivent automatiquement. // // Si vous souhaitez plus tard basculer vers Brevo + Cloudflare Worker (pour le mail // au format Premium patrimonial), il suffira de remplir WORKER_URL et le code basculera // automatiquement vers cette voie (le Worker est prioritaire). (function(){ // ── ENDPOINTS DE RELAIS — par ordre de priorité ── // Web3Forms : gratuit illimité, hébergé en UE, supporte le HTML custom (format Premium patrimonial) // 👉 OBTENIR LA CLÉ : aller sur https://web3forms.com → "Create your access key" → email du cabinet // → on vous envoie une clé du type "abc12345-67de-89f0-1234-..." → la coller ci-dessous. const WEB3FORMS_ACCESS_KEY = 'e01f9327-41ff-455f-8822-6074d286d81b'; // FormSubmit : fallback actif si Web3Forms n'a pas (encore) de clé const FORMSUBMIT_URL = 'https://formsubmit.co/ajax/contact@henrion-cgpi.com'; // Worker Cloudflare → Resend (envoie le mail HTML au format Premium patrimonial) const WORKER_URL = 'https://hdf-mail-relay.matthieuavon1906.workers.dev'; const FORMSPREE_ID = 'REMPLACER_PAR_ID_FORMSPREE'; let cfg = null; function setSubmitting(on, label){ const btn=document.getElementById('sg-submit'); if(on){btn.disabled=true;btn.textContent='Envoi en cours…'} else{btn.disabled=false;btn.textContent=label||'Continuer →'} } // ═══════════════════════════════════════════════════════════════ // Détecte le nom du formulaire Cloudflare à utiliser selon le payload. // Renvoie null si on ne peut pas déterminer (alors pas d'envoi Cloudflare). // ═══════════════════════════════════════════════════════════════ function detectCloudflareFormName(payload){ // 1) Si le code source a explicitement précisé _cfForm → on l'utilise if (payload && payload._cfForm) return payload._cfForm; // 2) Sinon on déduit à partir de "A — Source" const src = String(payload && payload['A — Source'] || '').toLowerCase(); if (src.includes('pré-qualification') || src.includes('calendly') || src.includes('rdv')) return 'lead-rdv'; if (src.includes('simulateur') || src.includes('profil')) return 'lead-simulateur'; if (src.includes('contact') || src.includes('rappel')) return 'lead-contact'; if (src.includes('téléchargement') || src.includes('document') || src.includes('fiche') || src.includes('bon plan')) return 'lead-telechargement'; // 3) Fallback générique return 'lead-contact'; } // ═══════════════════════════════════════════════════════════════ // Mapping des clés "A — X" / "B — Y" vers des noms simples sans // caractères spéciaux, pour que Cloudflare Pages accepte les champs. // ═══════════════════════════════════════════════════════════════ function _simplifyKey(key){ // Normalisation : retire préfixes A —, B —, Q —, accentuations, espaces let k = String(key).toLowerCase(); k = k.replace(/^[abcdq]\s*[—–-]\s*/i, ''); // retire "A — ", "B — ", etc. k = k.normalize('NFD').replace(/[̀-ͯ]/g, ''); // retire accents k = k.replace(/[^a-z0-9]+/g, '_'); // tout non-alphanum → underscore k = k.replace(/^_+|_+$/g, ''); // trim underscores // Quelques alias pour matcher les noms attendus côté admin const aliases = { 'prenom': 'prenom', 'nom': 'nom', 'email': 'email', 'telephone': 'telephone', 'tel': 'telephone', 'source': 'source', 'date_heure': 'date', 'consentement_rgpd': 'consentement', 'politique_de_confidentialite': 'consentement_politique', 'objectif_principal': 'objectif', 'objectif': 'objectif', 'montant_a_investir': 'montant', 'montant': 'montant', 'patrimoine': 'patrimoine', 'horizon': 'horizon', 'risque': 'risque', 'connaissances': 'connaissances', 'situation_pro': 'situation_pro', 'age': 'age', 'disponibilites': 'disponibilites', 'format': 'format', 'message': 'message', 'sujet': 'sujet', 'document': 'document', 'recontact': 'recontact', }; return aliases[k] || k; } // ═══════════════════════════════════════════════════════════════ // Envoi en parallèle à Cloudflare Pages (capture dans l'admin custom) // Fire-and-forget : ne bloque pas le flow utilisateur si Cloudflare est down. // ═══════════════════════════════════════════════════════════════ async function sendToCloudflareApi(payload){ try { const formName = detectCloudflareFormName(payload); if (!formName) return; const params = new URLSearchParams(); params.append('form-name', formName); // Recopie tous les champs métier en simplifiant leurs noms // (pour que Cloudflare accepte sans souci d'em dash / accents / espaces) const seenKeys = new Set(); Object.keys(payload).forEach(k => { if (k.startsWith('_')) return; const simpleKey = _simplifyKey(k); if (!simpleKey || seenKeys.has(simpleKey)) return; seenKeys.add(simpleKey); params.append(simpleKey, String(payload[k] ?? '')); }); // Honeypot vide (anti-spam) params.append('bot-field', ''); params.append('_submittedAt', new Date().toISOString()); const res = await fetch('/', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: params.toString() }); if (!res.ok) { console.warn('[HD Finance] Cloudflare Pages a renvoyé une erreur HTTP :', res.status); } else { console.log('[HD Finance] Lead envoyée à Cloudflare Pages (' + formName + ')'); } } catch (e) { console.warn('[HD Finance] Erreur envoi Cloudflare Pages (non-bloquant) :', e); } } // ═══════════════════════════════════════════════════════════════ // Envoi en parallèle à Cloudflare (capture dans l'admin sur Cloudflare Pages) // Actif uniquement sur *.pages.dev. Fire-and-forget. Les emails (Web3Forms) // restent l'envoi principal ; ceci alimente la liste des leads de l'admin. // ═══════════════════════════════════════════════════════════════ async function sendToCloudflareLead(payload){ try { if (!location.hostname.endsWith('.pages.dev')) return; // Cloudflare uniquement const formName = detectCloudflareFormName(payload); if (!formName) return; const data = {}; const seen = new Set(); Object.keys(payload).forEach(k => { if (k.startsWith('_')) return; const sk = _simplifyKey(k); if (!sk || seen.has(sk)) return; seen.add(sk); data[sk] = String(payload[k] ?? ''); }); await fetch('/api/lead', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ formName, data }) }); console.log('[HD Finance] Lead envoyée à Cloudflare (' + formName + ')'); } catch (e) { console.warn('[HD Finance] Erreur envoi Cloudflare (non-bloquant) :', e); } } async function sendLead(payload){ // ─── ENVOI PARALLÈLE CLOUDFLARE FORMS (pour admin custom sur Cloudflare) ─── // Fire-and-forget : on lance sans attendre pour ne pas ralentir le user sendToCloudflareApi(payload); // ─── ENVOI PARALLÈLE CLOUDFLARE (pour admin custom sur Cloudflare Pages) ─── sendToCloudflareLead(payload); // Construction du mail HTML (format Premium patrimonial) — utilisé par Worker et Web3Forms let htmlBody = ''; try{ htmlBody = (typeof buildLeadEmailHtml === 'function') ? buildLeadEmailHtml(payload) : ''; }catch(e){ console.error('[HD Finance] Erreur génération HTML :', e); } const workerOk = WORKER_URL && WORKER_URL !== 'REMPLACER_PAR_URL_WORKER'; const web3formsOk = WEB3FORMS_ACCESS_KEY && WEB3FORMS_ACCESS_KEY !== 'REMPLACER_PAR_CLE_WEB3FORMS'; const formspreeOk = FORMSPREE_ID && FORMSPREE_ID !== 'REMPLACER_PAR_ID_FORMSPREE'; const formsubmitOk = FORMSUBMIT_URL && FORMSUBMIT_URL.startsWith('https://formsubmit.co/'); // Priorité 1 : Worker Cloudflare (HTML Premium patrimonial via Brevo) — si déployé if(workerOk){ try{ const res = await fetch(WORKER_URL, { method:'POST', headers:{'Accept':'application/json','Content-Type':'application/json'}, body: JSON.stringify({ subject: payload._subject || '[HD Finance] Nouveau lead', replyTo: payload._replyto || null, html: htmlBody, payload: payload }) }); if(res.ok) return true; console.error('[HD Finance] Worker a renvoyé une erreur HTTP :', res.status); }catch(e){ console.error('[HD Finance] Erreur envoi Worker :', e); } } // Priorité 2 : Web3Forms (gratuit, UE) — si clé configurée // Web3Forms free liste les champs proprement ; il n'accepte pas de HTML custom dans le corps. // On envoie donc les données structurées avec des libellés français lisibles dans l'email. if(web3formsOk){ try{ const w3fBody = { access_key: WEB3FORMS_ACCESS_KEY, subject: payload._subject || '[HD Finance] Nouveau lead', from_name: 'Site HD Finance', reply_to: payload._replyto || '', botcheck: false }; // Recopie de tous les champs métier en respectant l'ordre (préfixes A/B/Q/→ déjà triés) Object.keys(payload).forEach(k => { if(k.startsWith('_')) return; // métadonnées techniques w3fBody[k] = String(payload[k]); }); const res = await fetch('https://api.web3forms.com/submit', { method:'POST', headers:{'Accept':'application/json','Content-Type':'application/json'}, body: JSON.stringify(w3fBody) }); if(res.ok){ const data = await res.json().catch(()=>({})); if(data.success !== false) return true; console.error('[HD Finance] Web3Forms a renvoyé une erreur :', data.message || data); } else { console.error('[HD Finance] Web3Forms HTTP', res.status); } }catch(e){ console.error('[HD Finance] Erreur envoi Web3Forms :', e); } } // Priorité 3 : FormSubmit.co (relais gratuit, UE, mail au format tableau par défaut) if(formsubmitOk){ try{ // FormSubmit attend les champs en JSON. Les clés _xxx sont des paramètres de configuration. const fsBody = { _subject: payload._subject || '[HD Finance] Nouveau lead', _replyto: payload._replyto || '', _template: 'table', // mise en forme tableau lisible _captcha: 'false', // désactive la vérification captcha (formulaires JS) _autoresponse: '', // pas d'auto-réponse au prospect }; // Recopie des champs métier (en filtrant les clés techniques) Object.keys(payload).forEach(k => { if(k.startsWith('_')) return; fsBody[k] = String(payload[k]); }); const res = await fetch(FORMSUBMIT_URL, { method:'POST', headers:{'Accept':'application/json','Content-Type':'application/json'}, body: JSON.stringify(fsBody) }); if(res.ok) return true; console.error('[HD Finance] FormSubmit a renvoyé une erreur HTTP :', res.status); }catch(e){ console.error('[HD Finance] Erreur envoi FormSubmit :', e); } } // Priorité 3 : Formspree (fallback texte brut) if(formspreeOk){ try{ const res = await fetch('https://formspree.io/f/'+FORMSPREE_ID, { method:'POST', headers:{'Accept':'application/json','Content-Type':'application/json'}, body: JSON.stringify(payload) }); if(res.ok) return true; }catch(e){ console.error('[HD Finance] Erreur envoi Formspree :', e); } } // Mode dégradé ultime : stockage localStorage try{ const k='hdf_leads'; const prev=JSON.parse(localStorage.getItem(k)||'[]'); prev.push(Object.assign({_date:new Date().toISOString()}, payload)); localStorage.setItem(k, JSON.stringify(prev)); console.warn('[HD Finance] Tous les relais ont échoué — lead stocké localement (clé "hdf_leads").'); }catch(e){} return false; } window.sendLeadDirect = sendLead; window.openLeadGate = function(config){ cfg = config || {}; const box = document.getElementById('sim-gate-box'); box.querySelector('.sg-title').textContent = cfg.title || 'Accédez à votre contenu.'; box.querySelector('.sg-sub').textContent = cfg.subtitle || 'Renseignez vos coordonnées pour continuer.'; const badgeEl = box.querySelector('.sg-badge'); badgeEl.lastChild.nodeValue = ' '+(cfg.badge || 'Dernière étape'); document.getElementById('sg-submit').textContent = cfg.ctaLabel || 'Continuer →'; document.getElementById('sim-gate-form').reset(); document.getElementById('sg-error').style.display='none'; document.getElementById('sim-gate-overlay').classList.add('show'); document.body.style.overflow='hidden'; setTimeout(()=>{document.getElementById('sg-firstname').focus()},220); }; window.closeSimGate = function(){ document.getElementById('sim-gate-overlay').classList.remove('show'); document.body.style.overflow=''; }; document.getElementById('sim-gate-form').addEventListener('submit', async function(e){ e.preventDefault(); if(!cfg) return; const fn=document.getElementById('sg-firstname').value.trim(); const ln=document.getElementById('sg-lastname').value.trim(); const em=document.getElementById('sg-email').value.trim(); const ok=document.getElementById('sg-consent').checked; const err=document.getElementById('sg-error'); const validEmail=/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(em); if(!fn||!ln||!validEmail||!ok){ err.textContent = !ok ? "Merci d'accepter la politique de confidentialité pour continuer." : "Merci de compléter tous les champs avec une adresse email valide."; err.style.display='block'; return; } err.style.display='none'; const ctaLabel = document.getElementById('sg-submit').textContent; setSubmitting(true); const base = { _subject: '[HD Finance] Nouveau lead — '+(cfg.source||'site')+' — '+fn+' '+ln, _replyto: em, _format: 'plain', 'A — Prénom': fn, 'A — Nom': ln, 'A — Email': em, 'A — Source': cfg.source || 'site HD Finance', 'A — Date / Heure': new Date().toLocaleString('fr-FR'), 'A — Consentement RGPD': 'Oui (case cochée avant envoi)', 'A — Politique de confidentialité': 'Acceptée', 'A — User-Agent': (navigator.userAgent||'').slice(0,200) }; const extra = (typeof cfg.payloadBuilder==='function') ? (cfg.payloadBuilder()||{}) : {}; await sendLead(Object.assign(base, extra)); setSubmitting(false, ctaLabel); closeSimGate(); if(typeof cfg.onSuccess==='function') cfg.onSuccess({fn, ln, em}); }); document.getElementById('sim-gate-overlay').addEventListener('click', e=>{ if(e.target.id==='sim-gate-overlay') closeSimGate(); }); document.addEventListener('keydown', e=>{ if(e.key==='Escape' && document.getElementById('sim-gate-overlay').classList.contains('show')) closeSimGate(); }); })(); // ── PRÉ-QUALIFICATION AVANT CALENDLY ────────────────── (function(){ const form = document.getElementById('prequal-form'); if(!form) return; // Gestion des pilules « projet » et « montant » function bindPills(selector, hiddenId){ const opts = form.querySelectorAll(selector); const hidden = document.getElementById(hiddenId); opts.forEach(opt=>{ opt.addEventListener('click', ()=>{ opts.forEach(o=>o.classList.remove('on')); opt.classList.add('on'); if(hidden) hidden.value = opt.dataset.value || ''; }); }); } bindPills('.pq-opt', 'pq-objectif'); bindPills('.pq-pill', 'pq-montant'); function revealCalendly(){ document.getElementById('prequal-wrap').style.display='none'; document.getElementById('calendly-wrap').style.display='block'; setTimeout(()=>{ if(window.Calendly){ try{ Calendly.initInlineWidgets(); }catch(e){} } }, 200); } window.prequalSkip = function(){ revealCalendly(); document.getElementById('calendly-wrap').scrollIntoView({behavior:'smooth', block:'start'}); }; form.addEventListener('submit', async function(e){ e.preventDefault(); const obj = document.getElementById('pq-objectif').value; const mnt = document.getElementById('pq-montant').value; const fn = document.getElementById('pq-firstname').value.trim(); const ln = document.getElementById('pq-lastname').value.trim(); const em = document.getElementById('pq-email').value.trim(); const tel = document.getElementById('pq-phone').value.trim(); const ok = document.getElementById('pq-consent').checked; const err = document.getElementById('pq-error'); const validEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(em); if(!obj||!mnt||!fn||!ln||!validEmail){ err.textContent = "Merci de compléter tous les champs obligatoires."; err.style.display='block'; return; } if(!ok){ err.textContent = "Merci d'accepter la politique de confidentialité."; err.style.display='block'; return; } err.style.display='none'; const btn = document.getElementById('pq-submit'); btn.disabled=true;btn.textContent='Envoi en cours…'; const payload = { _subject: '[HD Finance] Demande de RDV — '+fn+' '+ln, _replyto: em, _format: 'plain', 'A — Prénom': fn, 'A — Nom': ln, 'A — Email': em, 'A — Téléphone': tel || '(non renseigné)', 'A — Source': 'Pré-qualification avant Calendly', 'A — Date / Heure': new Date().toLocaleString('fr-FR'), 'A — Consentement RGPD': 'Oui (case cochée avant envoi)', 'A — Politique de confidentialité': 'Acceptée', 'B — Objectif principal': obj, 'B — Montant à investir': mnt }; try{ if(typeof window.sendLeadDirect === 'function'){ await window.sendLeadDirect(payload); } }catch(e){ console.error('Pré-qualif :', e); } btn.disabled=false;btn.textContent='Accéder à l\'agenda →'; revealCalendly(); }); // ── Bouton "Être rappelé" : enregistre la demande sans rediriger vers Calendly ── document.getElementById('pq-callback')?.addEventListener('click', async function(){ const obj = document.getElementById('pq-objectif').value; const mnt = document.getElementById('pq-montant').value; const fn = document.getElementById('pq-firstname').value.trim(); const ln = document.getElementById('pq-lastname').value.trim(); const em = document.getElementById('pq-email').value.trim(); const tel = document.getElementById('pq-phone').value.trim(); const ok = document.getElementById('pq-consent').checked; const err = document.getElementById('pq-error'); const validEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(em); if(!obj||!mnt||!fn||!ln||!validEmail){ err.textContent = "Merci de compléter tous les champs obligatoires."; err.style.display='block'; return; } if(!tel){ err.textContent = "Merci de renseigner votre téléphone pour être rappelé."; err.style.display='block'; document.getElementById('pq-phone').focus(); return; } if(!ok){ err.textContent = "Merci d'accepter la politique de confidentialité."; err.style.display='block'; return; } err.style.display='none'; const cb = document.getElementById('pq-callback'); cb.disabled = true; const originalLabel = cb.innerHTML; cb.textContent = 'Envoi en cours…'; const payload = { _subject: '[HD Finance] ☎ DEMANDE DE RAPPEL — '+fn+' '+ln, _replyto: em, _format: 'plain', 'A — Prénom': fn, 'A — Nom': ln, 'A — Email': em, 'A — Téléphone': tel, 'A — Source': 'Demande de rappel (formulaire contact)', 'A — Date / Heure': new Date().toLocaleString('fr-FR'), 'A — Consentement RGPD': 'Oui (case cochée avant envoi)', 'A — Politique de confidentialité': 'Acceptée', 'B — Objectif principal': obj, 'B — Montant à investir': mnt, 'B — Mode de contact souhaité': 'Rappel téléphonique sous 24 h ouvrées' }; try{ if(typeof window.sendLeadDirect === 'function'){ await window.sendLeadDirect(payload); } }catch(e){ console.error('Demande de rappel :', e); } // Confirmation : on masque les boutons et on affiche le bloc succès document.getElementById('pq-submit').style.display='none'; cb.style.display='none'; document.getElementById('pq-callback-success').style.display='block'; document.getElementById('pq-callback-success').scrollIntoView({behavior:'smooth',block:'center'}); }); })(); // ── EXPERIENCE TIMELINE (chemin doré + dashboards animés au scroll) ── function initExperience(){ const tl = document.getElementById('exp-timeline'); if(!tl) return; const svgEl = document.getElementById('exp-svg'); const pathBg = document.getElementById('exp-path-bg'); const pathFill = document.getElementById('exp-path-fill'); const nodes = [0,1,2,3,4,5].map(i=>document.getElementById('exn'+i)); const cards = [0,1,2,3,4,5].map(i=>document.getElementById('expc'+i)); function resizeSvg(){ svgEl.setAttribute('height', tl.offsetHeight); } function buildPath(){ resizeSvg(); const cr=tl.getBoundingClientRect(); const pts=nodes.map(n=>{const r=n.getBoundingClientRect();return{x:r.left-cr.left+r.width/2,y:r.top-cr.top+r.height/2}}); // [v3.8] Démarrer la ligne juste au-dessus du 1er RDV, courbure compactée (.50) const rdv0=document.getElementById('rdv0'); let d; if(rdv0){ const br=rdv0.getBoundingClientRect(); const badgeX=br.left-cr.left+br.width/2; const badgeY=br.top-cr.top+br.height/2; const startY=Math.max(0,badgeY-28); // 28px au-dessus du badge (était 45) const dy0=pts[0].y-startY; // Tracé : départ → badge (vertical) → nœud 01 (courbe arrondie compactée) d=`M ${badgeX} ${startY} L ${badgeX} ${badgeY} C ${badgeX} ${badgeY+dy0*.50}, ${pts[0].x} ${pts[0].y-dy0*.50}, ${pts[0].x} ${pts[0].y}`; } else { d=`M ${pts[0].x} ${pts[0].y}`; } // [v3.8] Coef Bézier .45 → .55 (courbes arrondies malgré le dy plus petit dû aux marges compactées) for(let i=1;i{ totalLen=buildPath(); }); setTimeout(()=>{ totalLen=buildPath(); }, 600); const innerCards=[0,1,2,3,4,5].map(i=>{const c=document.getElementById('expc'+i);return c?c.querySelector('.exp-card-inner'):null}); const badges=[0,1,2].map(i=>document.getElementById('rdv'+i)); function badgeFraction(b){ if(!b)return 1; const tr=tl.getBoundingClientRect(),br=b.getBoundingClientRect(); return Math.max(0,Math.min(1,(br.top-tr.top)/tr.height)); } const thresholds=[.04,.2,.38,.54,.72,.88]; // [v3.8] Boucle rAF continue — sync 1:1 avec le scroll, sans inertie ni throttling let lastScrollY=-1, lastWidth=-1; function applyProgress(progress){ pathFill.style.strokeDashoffset=totalLen*(1-progress); thresholds.forEach((t,i)=>{ if(progress>=t) nodes[i].classList.add('lit'); else nodes[i].classList.remove('lit'); }); badges.forEach(b=>{ if(!b) return; if(progress>=badgeFraction(b)) b.classList.add('lit'); else b.classList.remove('lit'); }); let active=-1; for(let i=0;i=thresholds[i]) active=i; } innerCards.forEach((c,i)=>{ if(!c) return; if(i===active) c.classList.add('current'); else c.classList.remove('current'); }); } function scrollLoop(){ const sy=window.scrollY, sw=window.innerWidth; if(sy!==lastScrollY || sw!==lastWidth){ lastScrollY=sy; lastWidth=sw; const r=tl.getBoundingClientRect(); const mid=window.innerHeight/2; const progress=Math.max(0,Math.min(1,(mid-r.top)/r.height)); applyProgress(progress); } requestAnimationFrame(scrollLoop); } requestAnimationFrame(scrollLoop); const cardObs=new IntersectionObserver(entries=>{ entries.forEach(e=>{ if(e.isIntersecting) e.target.classList.add('visible'); }); }, { threshold:.2 }); cards.forEach(c=>{ if(c) cardObs.observe(c); }); const vizs=document.querySelectorAll('#experience .exp-viz'); const vizObs=new IntersectionObserver(entries=>{ entries.forEach(e=>{ if(e.isIntersecting){ e.target.classList.add('visible'); animateExpViz(e.target); vizObs.unobserve(e.target); } }); }, { threshold:.3 }); vizs.forEach(v=>vizObs.observe(v)); function animateExpViz(el){ el.querySelectorAll('.exp-donut-seg').forEach(seg=>{ const pct=parseFloat(seg.dataset.pct); setTimeout(()=>{ seg.setAttribute('stroke-dasharray', pct+' '+(100-pct)); }, 120); }); el.querySelectorAll('.exp-bar-fill').forEach(b=>{ const pct=parseFloat(b.dataset.pct); setTimeout(()=>{ b.style.width=pct+'%'; }, 80); }); el.querySelectorAll('[data-target]').forEach(n=>{ const target=parseFloat(n.dataset.target); const format=n.dataset.format; const duration=1100; const start=performance.now(); function step(now){ const t=Math.min(1,(now-start)/duration); const eased=1-Math.pow(1-t,3); const val=Math.round(target*eased); n.textContent = (format==='space') ? val.toString().replace(/\B(?=(\d{3})+(?!\d))/g,' ') : val; if(t<1) requestAnimationFrame(step); } requestAnimationFrame(step); }); } } window.addEventListener('load', ()=>{ initExperience(); }); // ── WAYPOINTS AUTOPILOT ─────────────────────────────── (function(){ const section=document.getElementById('waypoints'); if(!section)return; const cards=Array.from(section.querySelectorAll('.sol-card')); const bg=document.getElementById('waypoints-bg'); if(!cards.length)return; let current=0,progress=0,running=false,lastTime=0; const STEP_DURATION=5600; function fillDetail(idx){ if(typeof wpData==='undefined'||!wpData[idx])return; const d=wpData[idx]; const img=document.getElementById('sol-detail-img'); if(img){ const empty=img.getAttribute('src')===''||img.getAttribute('src')===null; if(empty){img.src=d.img} else{img.style.opacity='0';setTimeout(()=>{img.src=d.img;img.style.opacity='1'},200)} } document.getElementById('sol-detail-tag').textContent=d.tag; document.getElementById('sol-detail-title').textContent=d.title; document.getElementById('sol-detail-hook').textContent=d.hook; document.getElementById('sol-detail-desc').innerHTML=d.summary||d.desc; document.getElementById('sol-detail-points').innerHTML=d.points.map(p=>`${p}`).join(''); } function setActive(idx){ cards.forEach((c,i)=>{ const bar=c.querySelector('.sol-progress-bar'); if(i===idx){c.classList.add('active');c.classList.remove('dimmed')} else{c.classList.remove('active');c.classList.add('dimmed');if(bar)bar.style.width='0%'} }); if(bg){bg.style.backgroundImage=`url(${cards[idx].dataset.bg})`;bg.classList.add('show')} fillDetail(idx); current=idx;progress=0; } function tick(time){ if(!running)return; if(!lastTime)lastTime=time; const delta=time-lastTime;lastTime=time; progress+=delta; const pct=Math.min(100,progress/STEP_DURATION*100); const bar=cards[current].querySelector('.sol-progress-bar'); if(bar)bar.style.width=pct+'%'; if(progress>=STEP_DURATION){current=(current+1)%cards.length;setActive(current)} requestAnimationFrame(tick); } cards.forEach((c,i)=>{c.addEventListener('click',e=>{if(e.target.closest('button,a'))return;setActive(i)})}); setActive(0); const io=new IntersectionObserver(entries=>{ entries.forEach(e=>{ if(e.isIntersecting){if(!running){running=true;lastTime=0;requestAnimationFrame(tick)}} else{running=false} }); },{threshold:.3}); io.observe(section); })(); // ── HERO CINÉMATIQUE — rotation des mots ── (function(){ const words = document.querySelectorAll('#heroRotator .hero-word'); if(words.length < 2) return; let i = 0; function startRotator(){ setInterval(() => { const current = words[i]; const next = words[(i+1) % words.length]; current.classList.remove('active'); current.classList.add('leaving'); next.classList.add('active'); setTimeout(() => current.classList.remove('leaving'), 1500); i = (i+1) % words.length; }, 3000); } // Démarre la rotation 700ms après que le hero soit révélé (laisse le temps de voir le premier mot fixe) // Démarre la rotation 2,5s après le chargement (laisse le temps de voir le premier mot fixe) setTimeout(startRotator, 2500); })(); // ── PAGE TRANSITION CURTAIN — usage générique (1 phase) ── function triggerCurtain(chapterLabel, titleHtml, onCovered){ const c = document.getElementById('curtain'); if(!c) return; const p1 = document.getElementById('curtainPhase1'); const p2 = document.getElementById('curtainPhase2'); const p3 = document.getElementById('curtainPhase3'); // S'assure qu'on est sur la phase 1 if(p1) p1.classList.add('active'); if(p2) p2.classList.remove('active'); if(p3) p3.classList.remove('active'); document.getElementById('curtainChapter').textContent = chapterLabel; document.getElementById('curtainTitle').innerHTML = titleHtml; c.classList.remove('hide','fade-out','no-transition'); c.classList.add('show'); setTimeout(() => { if(typeof onCovered === 'function') onCovered(); setTimeout(() => { c.classList.remove('show'); c.classList.add('hide'); setTimeout(() => { // Reset propre (sans glissement visible) c.classList.add('no-transition'); c.classList.remove('hide'); void c.offsetWidth; requestAnimationFrame(() => requestAnimationFrame(() => c.classList.remove('no-transition'))); }, 1200); }, 700); }, 1300); } window.triggerCurtain = triggerCurtain; // ── TRANSITION VERS SIMULATEURS — accès direct, sans rideau ── // On bascule directement sur la page sim et on active le mode cinématique : // l'intro et les 3 cartes apparaissent en cascade rapide (cf. CSS .cinematic-in). function transitionToSim(){ if(typeof goToSim === 'function') goToSim(); const sp = document.getElementById('sim-page'); if(sp){ // Clic depuis « Faire une simulation » du hero : on rejoue toujours la cascade sp.classList.remove('no-cinematic'); sp.classList.remove('cinematic-in'); // Force un reflow pour que la transition rejoue à chaque ouverture void sp.offsetWidth; sp.classList.add('cinematic-in'); } } window.transitionToSim = transitionToSim; // ── RÉVÉLATION CINÉMATIQUE DU DIAGNOSTIC — 2 fenêtres ── // Fenêtre 1 (récap) : projet + profil + portefeuille (camembert animé) — 7 s // Fenêtre 2 (RDV) : invitation au rendez-vous — 15 s de compte à rebours // Skip "Voir le diagnostic" disponible en bas-droite à tout moment. function triggerDiagnosticReveal(data, onComplete){ const dr = document.getElementById('diag-reveal'); if(!dr || !data){ if(typeof onComplete==='function') onComplete(); return; } // Reset const phases = dr.querySelectorAll('.dr-phase'); phases.forEach(p => p.classList.remove('active')); dr.classList.remove('show','fading'); // Hydratation des contenus document.getElementById('drProject').textContent = data.project || '—'; document.getElementById('drProfile').textContent = data.profile || '—'; document.getElementById('drProfileSub').textContent = data.profileSub || ''; // ── Timers et helpers de nettoyage ── const timers = []; let countdownInterval = null; const stopAll = () => { timers.forEach(id => clearTimeout(id)); if(countdownInterval) clearInterval(countdownInterval); }; const fadeAndShowResults = () => { stopAll(); dr.classList.add('fading'); setTimeout(() => { dr.classList.remove('show','fading'); phases.forEach(p => p.classList.remove('active')); document.body.style.overflow = ''; if(typeof onComplete === 'function') onComplete(); }, 1200); }; const fadeAndGoToContact = () => { stopAll(); dr.classList.add('fading'); setTimeout(() => { dr.classList.remove('show','fading'); phases.forEach(p => p.classList.remove('active')); document.body.style.overflow = ''; if(typeof goBackTo === 'function') goBackTo('contact'); else if(typeof goBack === 'function'){ goBack(); setTimeout(() => { const c = document.getElementById('contact'); if(c) c.scrollIntoView({behavior:'smooth'}); }, 400); } }, 1200); }; // ── Boutons "Voir le diagnostic" (skip) sur les 2 fenêtres ── ['drSkip1','drSkip2'].forEach(id => { const el = document.getElementById(id); if(el) el.onclick = (e) => { e.preventDefault(); fadeAndShowResults(); return false; }; }); // ── Bouton "Prendre rendez-vous" ── document.getElementById('drRdvBtn').onclick = (e) => { e.preventDefault(); fadeAndGoToContact(); return false; }; // ── Lance l'overlay ── document.body.style.overflow = 'hidden'; dr.classList.add('show'); // ── Orchestration ── // Fenêtre 1 (récap) : 7 s | Fenêtre 2 (RDV) : 15 s const t = { p1: 400, // fenêtre 1 apparaît donutStart: 1100, // camembert démarre 700 ms après l'apparition de la fenêtre p2: 7400, // fenêtre 1 visible pendant 7 s (jusqu'à 7,4 s) countdownStart: 8400, // 1 s après l'apparition de la fenêtre 2 fadeOut: 22400, // 15 s après le début du compte à rebours }; timers.push(setTimeout(() => phases[0].classList.add('active'), t.p1)); // Lance le camembert + légende synchronisés timers.push(setTimeout(() => drawDonutWithLegend(data.allocation || [], data.profile || ''), t.donutStart)); // Passage à la fenêtre 2 (RDV) timers.push(setTimeout(() => { phases[0].classList.remove('active'); phases[1].classList.add('active'); }, t.p2)); // Compte à rebours 15 → 0 sur la fenêtre 2 let count = 15; const cdEl = document.getElementById('drCountdown'); cdEl.textContent = count; timers.push(setTimeout(() => { countdownInterval = setInterval(() => { count--; cdEl.textContent = Math.max(0, count); if(count <= 0) clearInterval(countdownInterval); }, 1000); }, t.countdownStart)); // Fondu final + affichage du diagnostic complet timers.push(setTimeout(fadeAndShowResults, t.fadeOut)); } window.triggerDiagnosticReveal = triggerDiagnosticReveal; // ── Camembert SVG animé avec légende révélée segment par segment ── function drawDonutWithLegend(allocation, profileName){ const svg = document.getElementById('drDonutSvg'); const legend = document.getElementById('drLegend'); const profileTxt = document.getElementById('drDonutProfile'); if(!svg || !legend) return; if(profileTxt) profileTxt.textContent = profileName || '—'; // Nettoyage des segments + items précédents svg.querySelectorAll('.dr-seg').forEach(el => el.remove()); legend.innerHTML = ''; const SEG_DURATION = 1000; // durée de tracé d'un segment (ms) const SEG_GAP = 100; // pause entre chaque segment let cumulative = 0; const NS = 'http://www.w3.org/2000/svg'; (allocation || []).filter(a => a && a[1] > 0).forEach(([name, pct, color], i) => { // Création du segment SVG (commence à 0 longueur) const seg = document.createElementNS(NS, 'circle'); seg.setAttribute('class', 'dr-seg'); seg.setAttribute('cx', '21'); seg.setAttribute('cy', '21'); seg.setAttribute('r', '15.915'); seg.setAttribute('fill', 'none'); seg.setAttribute('stroke', color); seg.setAttribute('stroke-width', '3.4'); seg.setAttribute('stroke-linecap', 'butt'); seg.setAttribute('stroke-dasharray', '0 100'); seg.setAttribute('stroke-dashoffset', `-${cumulative}`); seg.setAttribute('transform', 'rotate(-90 21 21)'); seg.style.transition = `stroke-dasharray ${SEG_DURATION}ms cubic-bezier(.4,0,.2,1)`; // Curseur cinétique : on garde un linecap propre seg.style.filter = `drop-shadow(0 0 4px ${color}60)`; svg.appendChild(seg); cumulative += pct; // Création de la ligne de légende correspondante (cachée) const item = document.createElement('div'); item.className = 'dr-legend-item'; item.innerHTML = ` ${name} ${pct}%`; legend.appendChild(item); // Au moment où le segment commence à se tracer, on révèle aussi la ligne légende setTimeout(() => { seg.setAttribute('stroke-dasharray', `${pct} ${100 - pct}`); item.classList.add('visible'); }, i * (SEG_DURATION + SEG_GAP)); }); } window.drawDonutWithLegend = drawDonutWithLegend; // ── Tracking position souris pour le glow doré sur les cartes simulateur ── (function(){ if(window.matchMedia('(hover:none),(max-width:780px)').matches) return; document.addEventListener('mousemove', (e) => { document.querySelectorAll('.sim-hub-card').forEach(card => { const r = card.getBoundingClientRect(); if(e.clientX < r.left - 50 || e.clientX > r.right + 50 || e.clientY < r.top - 50 || e.clientY > r.bottom + 50) return; const x = ((e.clientX - r.left) / r.width) * 100; const y = ((e.clientY - r.top) / r.height) * 100; card.style.setProperty('--mx', x + '%'); card.style.setProperty('--my', y + '%'); }); }, { passive:true }); })(); // ── COOKIE BANNER (RGPD) ────────────────────────────── (function(){ const KEY='hdf_cookie_consent'; if(localStorage.getItem(KEY)) return; // déjà décidé const html=``; document.body.insertAdjacentHTML('beforeend', html); const banner=document.getElementById('cookie-banner'); setTimeout(()=>banner.classList.add('show'), 600); document.getElementById('cookie-accept').addEventListener('click', ()=>{ localStorage.setItem(KEY, 'accepted'); if(typeof gtag==='function') gtag('consent', 'update', { analytics_storage: 'granted' }); banner.classList.remove('show'); setTimeout(()=>banner.remove(), 400); }); document.getElementById('cookie-refuse').addEventListener('click', ()=>{ localStorage.setItem(KEY, 'refused'); banner.classList.remove('show'); setTimeout(()=>banner.remove(), 400); }); })();