<!DOCTYPE html>
<!--
════════════════════════════════════════════════════════════════════════
© 2026 DEZVOLTA — SIREN 948 914 072 — RECOV™
Tous droits réservés. Ce code source, ses structures, son design et ses
contenus sont protégés par le droit d'auteur français (art. L111-1 et
suivants du Code de la propriété intellectuelle) et par le droit des
marques (marque RECOV™ en cours de dépôt à l'INPI).
Toute reproduction, représentation, adaptation, extraction substantielle,
diffusion ou rétro-ingénierie, même partielle, sans l'autorisation écrite
préalable de DEZVOLTA est interdite et constitue une contrefaçon
sanctionnée par les articles L335-2 et suivants du CPI (3 ans
d'emprisonnement, 300 000 € d'amende).
Pour toute demande légale ou partenariat : pedro.berbel@dezvolta.org
════════════════════════════════════════════════════════════════════════
-->
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="copyright" content="© 2026 DEZVOLTA. RECOV™ est une marque de DEZVOLTA (SIREN 948 914 072).">
<meta name="author" content="DEZVOLTA — RECOV™">
<meta name="rights" content="Tous droits réservés. Reproduction interdite.">
<title>RECOVMAX · Relance amiable multi-clients | RECOV Solo gratuit</title>
<meta name="description" content="RECOVMAX : la relance amiable multi-clients pour cabinets, secrétaires indépendantes et DAF externalisés. Pénalités L.441-10, mise en demeure PDF, audit trail. RECOV Solo gratuit pour les indépendants.">
<meta name="saashub-verification" content="sjdl58jmsdsu" />
<meta name="robots" content="index, follow, max-image-preview:large">
<link rel="canonical" href="https://recov.pro/">
<!-- Open Graph (Facebook, LinkedIn) -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://recov.pro/">
<meta property="og:title" content="RECOVMAX · Relance amiable multi-clients | RECOV Solo gratuit">
<meta property="og:description" content="RECOVMAX : le cockpit de relance amiable pour les pros qui suivent plusieurs clients TPE — cabinets, secrétaires indépendantes, office managers, DAF externalisés. ✓ Pénalités L.441-10 ✓ MED PDF ✓ Rapport mensuel refacturable ✓ Sans commission. RECOV Solo gratuit pour vos propres factures.">
<meta property="og:image" content="https://recov.pro/hero_recovr.png">
<meta property="og:locale" content="fr_FR">
<meta property="og:site_name" content="RECOV · RECOVMAX">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="RECOVMAX · Relance amiable multi-clients | RECOV Solo gratuit">
<meta name="twitter:description" content="RECOVMAX : le cockpit de relance amiable pour les pros qui suivent plusieurs clients TPE — cabinets, secrétaires indépendantes, office managers, DAF externalisés. ✓ Pénalités L.441-10 ✓ MED PDF ✓ Sans commission. RECOV Solo gratuit pour vos propres factures.">
<meta name="twitter:image" content="https://recov.pro/hero_recovr.png">
<link rel="alternate" type="application/rss+xml" title="Blog RECOV" href="https://recov.pro/feed.xml">
<script src="/pricing-injector.js" defer></script>
<!-- Favicon — raster EN PREMIER pour Google Search (ne gère pas le SVG comme favicon).
Les navigateurs choisissent le meilleur format quel que soit l'ordre (SVG en dernier). -->
<link rel="icon" type="image/x-icon" href="/favicon.ico" sizes="48x48">
<link rel="icon" type="image/png" sizes="48x48" href="/favicon-48x48.png">
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="144x144" href="/favicon-144x144.png">
<link rel="icon" type="image/png" sizes="192x192" href="/favicon-192x192.png">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<meta name="apple-mobile-web-app-title" content="Recov.pro" />
<link rel="manifest" href="/site.webmanifest" />
<meta name="theme-color" content="#84CC16">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:wght@500;600&family=DM+Sans:wght@400;500;600;700&display=block" rel="stylesheet">
<link rel="preload" as="image" href="hero_recovr.png">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/dist/umd/supabase.min.js"></script>
<!-- Schema.org JSON-LD -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "WebApplication",
"name": "RECOV",
"url": "https://recov.pro",
"description": "Aide à la rédaction de relances amiables pour indépendants. RECOV vous dit quoi écrire, quand envoyer et comment monter en pression. Messages modifiables, pénalités calculées, mise en demeure PDF, dossier de recouvrement transmissible.",
"applicationCategory": "BusinessApplication",
"operatingSystem": "Web",
"offers": [
{
"@type": "Offer",
"name": "Programme fondateur — bêta gratuite",
"price": "0",
"priceCurrency": "EUR",
"description": "Accès gratuit pendant la phase pilote. Activation du pricing commercial à la clôture du programme fondateur, sans engagement de durée."
},
{
"@type": "Offer",
"name": "RECOV Solo",
"price": "0",
"priceCurrency": "EUR",
"description": "RECOV Solo, toujours gratuit, pour les freelances et indépendants qui relancent leurs propres factures impayées. Séquence de relances graduée, pénalités L.441-10 calculées, mise en demeure PDF. Usage personnel uniquement : 1 entreprise créancière, 1 créance active à la fois, 1 mise en demeure par mois. 0 % de commission."
},
{
"@type": "Offer",
"name": "RECOVMAX — fondateur pilote",
"price": "19",
"priceCurrency": "EUR",
"priceSpecification": {
"@type": "UnitPriceSpecification",
"billingDuration": "P1M"
},
"description": "19 €/mois pour les pros multi-clients (cabinets, secrétaires indépendantes, assistantes administratives) — 50 clients, 50 créances actives, 1000 relances, reporting par client. Tarif fondateur maintenu tant que l'abonnement reste actif sur les 30 premières places."
}
],
"creator": {
"@type": "Organization",
"name": "DEZVOLTA",
"url": "https://recov.pro",
"founder": {
"@type": "Person",
"name": "Pedro Berbel",
"url": "https://www.linkedin.com/in/pedroberbel/"
}
},
"featureList": [
"Séquence de 8 relances amiables sur 60 jours",
"Messages rédigés, modifiables et prêts à envoyer ou programmer",
"6 profils de relation client pour adapter le ton",
"Calcul des pénalités de retard (art. L441-10, D441-5 Code de commerce)",
"Mise en demeure PDF générée",
"Dossier de recouvrement structuré transmissible à un professionnel",
"Copie BCC automatique à l'émetteur sur chaque relance",
"Vérification SIREN/SIRET du débiteur via API gouv.fr",
"Rappels et confirmations avant chaque envoi programmé",
"Compatible avec votre outil de facturation — RECOV intervient après l'émission de la facture"
]
},
{
"@type": "Organization",
"name": "DEZVOLTA",
"url": "https://recov.pro",
"logo": "https://recov.pro/logoRecovR.png",
"sameAs": ["https://www.linkedin.com/in/pedroberbel/"],
"contactPoint": {
"@type": "ContactPoint",
"email": "pedro.berbel@dezvolta.org",
"contactType": "customer service",
"availableLanguage": "French"
}
},
{
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "Dois-je fournir ma carte bancaire pour essayer RECOV ?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Non. Le programme fondateur donne un accès complet et gratuit pendant la phase pilote, sans carte bancaire. Aucun engagement de durée."
}
},
{
"@type": "Question",
"name": "Les emails générés par RECOV sont-ils personnalisés ?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Oui. RECOV utilise le nom du client, le montant exact, la référence facture, les jours de retard et le profil de la relation. Chaque séquence est unique."
}
},
{
"@type": "Question",
"name": "RECOV est-il un cabinet de recouvrement ?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Non. RECOV est un copilote de recouvrement amiable. Il ne contacte jamais vos clients et ne prélève aucune commission. Vous restez l'auteur et le responsable de chaque message."
}
},
{
"@type": "Question",
"name": "Mes données sont-elles en sécurité sur RECOV ?",
"acceptedAnswer": {
"@type": "Answer",
"text": "RECOV applique une logique de sécurité par dossier : accès authentifié, séparation des données, historique des actions, validation humaine avant envoi et conservation organisée des éléments utiles au suivi amiable. RECOVMAX ajoute un cadre multi-clients avec séparation des portefeuilles, audit trail et reporting par client."
}
},
{
"@type": "Question",
"name": "Combien coûte RECOV ?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Deux offres. RECOVMAX, 19 €/mois en tarif fondateur, est la solution pour les professionnels qui pilotent les créances de plusieurs clients TPE — cabinets comptables, secrétaires indépendantes, office managers, DAF externalisés : jusqu'à 50 clients distincts, 50 créances actives, 1000 relances par mois, reporting mensuel refacturable par client, audit trail horodaté, score santé portefeuille (tarif conservé tant que l'abonnement reste actif sur les 30 premières places, 29 €/mois ensuite). RECOV Solo reste gratuit pour les indépendants qui suivent leurs propres factures impayées (1 SIREN, 1 créance active, 1 mise en demeure par mois). 0 % de commission sur les deux offres — vous gardez 100 % de ce que vous récupérez."
}
},
{
"@type": "Question",
"name": "Comment relancer une facture impayée ?",
"acceptedAnswer": {
"@type": "Answer",
"text": "La relance amiable suit une progression en plusieurs étapes : rappel avant échéance, relance courtoise, relance ferme avec mention des pénalités, puis mise en demeure si nécessaire. L'important est de relancer rapidement et de garder une trace écrite."
}
},
{
"@type": "Question",
"name": "Combien de relances faut-il envoyer avant une mise en demeure ?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Il est recommandé d'envoyer au minimum 2 à 3 relances amiables avant d'envisager une mise en demeure. La mise en demeure est un prérequis juridique avant toute action en justice."
}
},
{
"@type": "Question",
"name": "Quel est le délai légal de paiement d'une facture ?",
"acceptedAnswer": {
"@type": "Answer",
"text": "En France, le délai de paiement est de 30 jours à compter de la réception de la facture, sauf accord contractuel (maximum 60 jours ou 45 jours fin de mois). Passé ce délai, des pénalités de retard sont exigibles de plein droit."
}
},
{
"@type": "Question",
"name": "La mise en demeure est-elle obligatoire ?",
"acceptedAnswer": {
"@type": "Answer",
"text": "La mise en demeure n'est pas toujours obligatoire, mais elle est fortement recommandée. Elle constitue un prérequis pour la plupart des procédures judiciaires (injonction de payer, assignation) et prouve votre bonne foi dans la démarche amiable."
}
},
{
"@type": "Question",
"name": "Pourquoi choisir RECOV plutôt qu'un autre outil ?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Les outils de facturation créent la facture mais ne gèrent pas la relance. Les cabinets de recouvrement prélèvent 8 à 15% de commission. ChatGPT rédige un email sans timing ni suivi. RECOV est conçu pour structurer tout le processus de relance — du premier rappel à la mise en demeure — sans commission, sans changer d'outil de facturation, avec un contrôle total sur chaque message."
}
},
{
"@type": "Question",
"name": "Quelle différence entre RECOV Solo et RECOVMAX ?",
"acceptedAnswer": {
"@type": "Answer",
"text": "RECOV Solo est conçu pour un indépendant qui relance ses propres factures impayées (mono-utilisateur). RECOVMAX est conçu pour un professionnel qui suit les créances de plusieurs clients (multi-tenant, mandat de gestion, rapport mensuel par client). Si vous êtes freelance et facturez en votre nom propre, RECOV. Si vous êtes secrétaire indépendante, comptable solo ou cabinet et que vous gérez l'administratif de plusieurs TPE, RECOVMAX."
}
},
{
"@type": "Question",
"name": "RECOV Solo est-il pour les professionnels de la facturation ?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Oui, RECOV est conçu pour les pros qui émettent des factures B2B et veulent récupérer celles qui ne sont pas payées. Cela inclut les freelances, consultants, formateurs, designers, développeurs, coachs, prestataires de services. RECOV intervient après l'émission de la facture (peu importe l'outil de facturation utilisé) et structure les relances pour récupérer le paiement."
}
},
{
"@type": "Question",
"name": "RECOVMAX est-il pour les secrétaires indépendantes ou les cabinets comptables ?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Oui, RECOVMAX cible spécifiquement les secrétaires indépendantes, assistantes virtuelles, office managers externalisés, comptables solo et cabinets compta TPE/PME. L'outil permet d'ajouter une prestation rentable à votre activité : suivi des factures impayées de chacun de vos clients, avec mandat de gestion signé, rapport mensuel PDF brandable, score santé portefeuille. Pratique observée : souvent valorisable entre 50 et 80 € HT/mois par client suivi, selon volume et niveau d'accompagnement."
}
},
{
"@type": "Question",
"name": "Combien puis-je facturer la prestation de suivi des impayés à mes clients TPE ?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Pratiques observées : secrétaire indépendante 30 à 80 €/mois par client suivi, cabinet comptable solo 50 à 150 €/mois par client. Le rapport mensuel PDF généré par RECOVMAX (actions menées, montants récupérés, score santé) justifie cette facturation auprès du client final. RECOVMAX ne prélève aucune commission sur les montants récupérés."
}
},
{
"@type": "Question",
"name": "RECOV peut-il aider avec la nouvelle procédure simplifiée 2026 entre commerçants ?",
"acceptedAnswer": {
"@type": "Answer",
"text": "La loi n° 2026-307 du 23 avril 2026 crée une procédure simplifiée pour les créances commerciales incontestées entre commerçants (sans seuil de montant, frais à la charge du débiteur), mise en œuvre par un commissaire de justice. Les modalités précises seront fixées par décret à venir. L'éligibilité dépend de la nature exacte de la créance (certaine, liquide, exigible, non contestée). RECOV et RECOVMAX ne délivrent aucun titre exécutoire et ne se substituent pas à un commissaire de justice — la procédure reste l'apanage du commissaire de justice. En revanche, comme la contestation met fin à la procédure, la qualité de la documentation amiable devient déterminante. RECOVMAX prépare un dossier amiable structuré pouvant faciliter la transmission lorsque la créance entre dans le cadre applicable."
}
},
{
"@type": "Question",
"name": "Mes clients verront-ils que j'utilise RECOV ?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Non. RECOV et RECOVMAX sont invisibles aux yeux de vos débiteurs. Les emails partent depuis votre messagerie professionnelle, signés à votre nom, sans aucune mention RECOV. Les courriers PDF (mise en demeure, lettre RAR) sont à votre en-tête. Le rapport mensuel pour le client final est brandable. RECOV est un outil de structuration interne."
}
}
]
},
{
"@type": "WebSite",
"name": "RECOV",
"url": "https://recov.pro",
"potentialAction": {
"@type": "SearchAction",
"target": "https://recov.pro/?q={search_term_string}",
"query-input": "required name=search_term_string"
}
},
{
"@type": "BreadcrumbList",
"itemListElement": [
{"@type":"ListItem","position":1,"name":"Accueil","item":"https://recov.pro/"}
]
},
{
"@type": "HowTo",
"name": "Comment relancer une facture impayée",
"description": "Les étapes concrètes d'un recouvrement amiable pour récupérer le paiement d'une facture en retard.",
"step": [
{"@type":"HowToStep","position":1,"name":"Identifier la facture en retard","text":"Vérifiez la date d'échéance, le montant dû et les coordonnées du client. Rassemblez les justificatifs (bon de commande, contrat, facture)."},
{"@type":"HowToStep","position":2,"name":"Vérifier les informations client","text":"Assurez-vous que l'adresse email et les coordonnées du client sont à jour. Identifiez le bon interlocuteur (comptabilité, direction)."},
{"@type":"HowToStep","position":3,"name":"Envoyer un premier rappel","text":"Contactez le client avec un message courtois rappelant la facture, son montant et sa date d'échéance. La majorité des retards sont des oublis."},
{"@type":"HowToStep","position":4,"name":"Relancer avec un message plus ferme","text":"Si le premier rappel reste sans réponse, envoyez une relance mentionnant les obligations contractuelles et les pénalités de retard applicables (art. L441-10 du Code de commerce)."},
{"@type":"HowToStep","position":5,"name":"Envisager une mise en demeure","text":"En dernier recours, adressez une mise en demeure par lettre recommandée avec accusé de réception. Ce document constitue un prérequis pour toute procédure judiciaire."}
]
}
]
}
</script>
<style>
:root {
--bg: #ffffff;
--bg2: #f8f9fb;
--bg3: #f1f3f7;
--noir: #0d0d14;
--noir2: #1e1e2e;
--gris: #334155;
--gris2: #64748b;
--gris3: #e2e8f0;
--vert: #65a30d;
--vert2: #4d7c0f;
--vert3: #ecfccb;
--vert4: #d9f99d;
--rouge: #dc2626;
--rouge2: #fee2e2;
--border: #e2e8f0;
--border2: #f1f5f9;
--sh1: 0 1px 3px rgba(0,0,0,.06),0 1px 2px rgba(0,0,0,.04);
--sh2: 0 4px 20px rgba(0,0,0,.08),0 1px 4px rgba(0,0,0,.04);
--sh3: 0 24px 64px rgba(0,0,0,.1),0 8px 24px rgba(0,0,0,.06);
}
*{margin:0;padding:0;box-sizing:border-box;}
html{scroll-behavior:smooth;overflow-x:hidden;max-width:100vw;width:100%;}
body{
font-family:'DM Sans',system-ui,sans-serif;
background:var(--bg);color:var(--noir);
-webkit-font-smoothing:antialiased;
-moz-osx-font-smoothing:grayscale;
text-rendering:optimizeLegibility;
font-feature-settings:'kern' 1, 'liga' 1, 'calt' 1;
font-optical-sizing:auto;
letter-spacing:-.003em;
font-size:16px;
line-height:1.65;
overflow-x:hidden;max-width:100vw;width:100%;
}
p{font-weight:400;line-height:1.7;}
p strong{font-weight:600;}
section,div,main,header,footer{max-width:100vw;box-sizing:border-box;}
/* ═══════════════════════════════════════════════════════════════════════ */
/* TYPOGRAPHIE UNIFIÉE — paragraphes & blocs explicatifs */
/* Règles globales pour cohérence (sans !important = overridable si besoin)*/
/* ═══════════════════════════════════════════════════════════════════════ */
/* Paragraphes body standard */
main p,
main .desc,
main .feature-desc,
main .pillier p,
main .acc-card .hint {
font-size: 15px;
line-height: 1.65;
color: var(--gris);
letter-spacing: -.003em;
}
main p strong,
main .feature-desc strong {
color: var(--noir);
font-weight: 600;
}
/* Lead (chapeau de section) — un peu plus gros */
main .section-lead,
main .lead,
main .hero-sub {
font-size: 17px;
line-height: 1.7;
color: var(--gris);
}
/* FAQ answers */
main .faq-a {
font-size: 14.5px;
line-height: 1.75;
color: var(--gris);
}
main .faq-a strong { color: var(--noir); font-weight: 600; }
/* Petites mentions (captions, fineprint, KPI labels) */
main .muted,
main .kpi-lbl,
main .pricing-fineprint,
main .footer-fineprint,
main small {
font-size: 12.5px;
line-height: 1.55;
color: var(--gris2);
}
/* Cards features standard */
main .feature-content .feature-title { font-weight: 600; color: var(--noir); }
main .feature-content .feature-desc { font-size: 14.5px; }
/* === Fin typographie unifiée === */
/* ── NAV ── */
header{position:fixed!important;top:0;left:0;right:0;z-index:100;transition:transform .3s ease;will-change:transform;}
header.nav-hidden{transform:translateY(-100%);}
body{padding-top:64px;}
@media(max-width:900px){body{padding-top:56px;}}
nav{
position:sticky;top:0;z-index:100;
background:#fff;
backdrop-filter:none;
border-bottom:1px solid var(--border);
height:64px;
display:flex;align-items:center;justify-content:space-between;
padding:0 clamp(16px,4vw,48px);
max-width:100%;
}
/* LOGO SVG institutionnel */
.logo-wrap{display:flex;align-items:center;gap:10px;text-decoration:none;flex-shrink:0;}
.logo-mark{
width:34px;height:34px;border-radius:8px;
background:var(--noir);
display:flex;align-items:center;justify-content:center;
flex-shrink:0;
}
.logo-type{
font-family:'Bricolage Grotesque',sans-serif;
font-size:17px;font-weight:500;color:var(--noir);
letter-spacing:-.03em;
}
.logo-type span{color:var(--vert);}
.logo-tagline{
font-size:10px;font-weight:500;color:var(--gris2);
letter-spacing:.04em;text-transform:uppercase;
border-left:1px solid var(--border);
padding-left:10px;margin-left:2px;
display:none;
}
.nav-links{display:flex;gap:28px;align-items:center;}
.nav-lnk{
font-size:14px;font-weight:500;color:var(--gris);
text-decoration:none;transition:color .15s;
white-space:nowrap;line-height:64px;
}
.nav-lnk:hover{color:var(--noir);}
.nav-dropdown{position:relative;}
.nav-drop-btn{cursor:pointer;background:none;border:none;font-family:inherit;padding:0;font-size:14px;font-weight:500;color:var(--gris);white-space:nowrap;transition:color .15s;line-height:64px;height:64px;}
.nav-drop-btn:hover{color:var(--noir);}
.nav-drop-menu{display:none;position:absolute;top:calc(100% + 8px);left:50%;transform:translateX(-50%);background:#fff;border:1px solid var(--border);border-radius:12px;padding:8px 0;box-shadow:0 8px 32px rgba(0,0,0,.1);min-width:220px;z-index:200;}
.nav-dropdown.open .nav-drop-menu{display:block;}
.nav-drop-menu a{display:block;padding:10px 18px;font-size:13px;color:var(--noir);text-decoration:none;transition:background .1s;white-space:nowrap;}
.nav-drop-menu a:hover{background:var(--bg2);}
.nav-right{display:flex;align-items:center;gap:8px;flex-shrink:0;}
.btn-ghost-sm{
font-size:13px;font-weight:500;color:var(--gris);
background:none;border:1px solid var(--border);
padding:7px 16px;border-radius:8px;cursor:pointer;
transition:all .15s;white-space:nowrap;
}
.btn-ghost-sm:hover{border-color:var(--noir);color:var(--noir);}
.btn-nav{
background:var(--noir);color:#fff;
padding:8px 18px;border-radius:8px;
font-size:13px;font-weight:500;border:none;cursor:pointer;
transition:background .15s,transform .1s;white-space:nowrap;
}
.btn-nav:hover{background:var(--noir2);transform:translateY(-1px);}
.btn-nav-short{display:none;}
@media(max-width:640px){
.btn-nav-long{display:none;}
.btn-nav-short{display:inline;}
}
/* ── ANIMATIONS GLOBALES ── */
@keyframes shake{0%,100%{transform:translateX(0)}20%{transform:translateX(-6px)}40%{transform:translateX(6px)}60%{transform:translateX(-4px)}80%{transform:translateX(4px)}}
/* ── HERO ── */
@keyframes heroReveal{
from{opacity:0;transform:translateY(40px)}
to{opacity:1;transform:translateY(0)}
}
@keyframes heroBgReveal{
from{opacity:0;transform:scale(1.08)}
to{opacity:1;transform:scale(1)}
}
.hero{
max-width:100%;margin:0;padding:0;
position:relative;
min-height:min(calc(100vh - 64px), 720px);
overflow:hidden;
display:flex;align-items:stretch;
}
.hero-left{animation:heroReveal 1.5s cubic-bezier(.16,1,.3,1) both;animation-delay:.3s;}
.hero-verb-wrap{
display:inline-block;
overflow:hidden;
vertical-align:-.08em;
height:1.18em;
position:relative;
line-height:1.18;
}
.hero-verb{
display:inline-block;
background:transparent;color:#84CC16;
padding:0;border-radius:0;
font-weight:800;
will-change:transform,opacity;
white-space:nowrap;
}
.hero-bg{animation:heroBgReveal 2s cubic-bezier(.16,1,.3,1) both;animation-delay:.1s;}
.hero-bg{
position:absolute;inset:0;z-index:0;
}
.hero-bg img{
width:50%;height:100%;
object-fit:cover;object-position:right center;
margin-left:auto;
margin-top:0;
margin-right:0;
display:block;
opacity:1;
/* Plus de filter saturate/brightness, plus de mask-image : image hero.png nette, full color, sans flou */
}
.hero-left{
position:relative;z-index:2;
display:flex;flex-direction:column;justify-content:center;
padding:48px clamp(28px,5.5vw,92px) 48px clamp(32px,6vw,96px);
width:50%;max-width:none;flex-shrink:0;
background:transparent;
}
/* Le H1 profite de la largeur dispo dans la zone gauche (50%) sans tronquer l'image */
.hero-left > h1{max-width:none;}
.hero-left > p{max-width:580px;}
.hero-left > .hero-btns,
.hero-left > div{max-width:none;}
@media(min-width:1400px){
.hero-left{padding-left:clamp(60px,8vw,140px);width:50%;}
}
@media(max-width:900px){
.hero-left{width:100%;background:#fff;}
}
.hero-mockup-wrap{
position:absolute;z-index:1;
right:5%;top:50%;transform:translateY(-50%);
width:42%;max-width:480px;
}
.hero-chip{
display:inline-flex;align-items:center;gap:6px;
background:var(--vert3);border:1px solid var(--vert4);
color:var(--vert2);padding:5px 12px;border-radius:30px;
font-size:11px;font-weight:700;letter-spacing:.03em;
margin-bottom:20px;width:fit-content;
}
.chip-dot{width:5px;height:5px;border-radius:50%;background:var(--vert);}
h1{
font-family:'Bricolage Grotesque',sans-serif;
font-size:clamp(30px,3.8vw,48px);
font-weight:500;line-height:1.04;
letter-spacing:-.045em;color:var(--noir);
margin-bottom:16px;
}
h1 .vert{color:#84CC16;}
h1 .underline{
position:relative;display:inline-block;
}
h1 .underline::after{
content:'';position:absolute;
bottom:-3px;left:0;right:0;height:3px;
background:linear-gradient(90deg,#84CC16,#84CC16);
border-radius:2px;
}
.hero-sub{
font-size:16px;font-weight:400;color:var(--gris);
line-height:1.6;margin-bottom:28px;max-width:440px;
}
.hero-slogan-item{
position:absolute;top:0;left:0;
opacity:0;transition:opacity .6s ease;
pointer-events:none;
}
.hero-slogan-item.active{
opacity:1;pointer-events:auto;position:relative;
}
.hero-btns{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:24px;}
.btn-main{
background:#84CC16;color:#fff;
padding:13px 24px;border-radius:10px;
font-size:15px;font-weight:500;border:none;cursor:pointer;
display:flex;align-items:center;gap:8px;
transition:background .15s,transform .15s,box-shadow .15s;
}
.btn-main:hover{
background:#65a30d;transform:translateY(-2px);
box-shadow:0 8px 24px rgba(132,204,22,.35);
}
.btn-outline{
background:var(--bg);color:var(--noir);
padding:13px 20px;border-radius:10px;
font-size:15px;font-weight:600;
border:1.5px solid var(--border);cursor:pointer;
transition:all .15s;
}
.btn-outline:hover{border-color:var(--noir);}
.hero-reassurance{
display:flex;flex-direction:column;gap:6px;
}
.reassurance-item{
display:flex;align-items:center;gap:8px;
font-size:13px;color:var(--gris);
}
.reassurance-item::before{content:'✓';color:var(--vert);font-weight:700;font-size:13px;}
/* HERO CLIC CURSOR */
@keyframes clic-pulse{0%{opacity:0}3%{opacity:1}6%{opacity:0}9%{opacity:1}12%{opacity:0}15%{opacity:1}40%{opacity:1}42%{opacity:0}100%{opacity:0}}
/* HERO SCROLL ARROW */
.hero-scroll{
position:absolute;bottom:20px;left:50%;transform:translateX(-50%);
z-index:10;display:flex;flex-direction:column;align-items:center;gap:4px;
cursor:pointer;opacity:.5;transition:opacity .2s;
}
.hero-scroll:hover{opacity:1;}
.hero-scroll-label{font-size:11px;font-weight:600;color:var(--gris);letter-spacing:.04em;}
.hero-scroll-arrow{
width:28px;height:28px;border-radius:50%;
border:1.5px solid var(--border);background:#fff;
display:flex;align-items:center;justify-content:center;
animation:bounce-arrow 2s infinite;
}
@keyframes bounce-arrow{0%,100%{transform:translateY(0)}50%{transform:translateY(4px)}}
/* HERO MOCKUP flottant */
.hero-mockup-card{
background:#fff;border-radius:14px;
box-shadow:0 24px 80px rgba(0,0,0,.18),0 4px 16px rgba(0,0,0,.08);
overflow:hidden;border:1px solid rgba(255,255,255,.6);
font-family:'DM Sans',sans-serif;
backdrop-filter:blur(4px);
}
/* ── SOCIAL PROOF BAND ── */
.proof-band{
background:var(--bg2);
border-top:1px solid var(--border);
border-bottom:1px solid var(--border);
padding:28px 48px;
display:flex;align-items:center;justify-content:center;gap:12px;flex-wrap:wrap;
}
.proof-label{font-size:13px;font-weight:500;color:var(--gris);white-space:nowrap;}
.proof-avatars{display:flex;align-items:center;}
.proof-avatar{
width:32px;height:32px;border-radius:50%;
border:2px solid var(--bg);
display:flex;align-items:center;justify-content:center;
font-size:12px;font-weight:700;color:#fff;
margin-left:-8px;
}
.proof-avatar:first-child{margin-left:0;}
.proof-stars{color:#f59e0b;letter-spacing:1px;font-size:14px;}
.logo-pill{background:var(--bg);border:1px solid var(--border);padding:5px 14px;border-radius:20px;font-size:13px;font-weight:500;color:var(--gris);}
.proof-separator{width:1px;height:20px;background:var(--border);}
/* ── AVANT / APRÈS ── */
.avap-section{
max-width:1100px;margin:0 auto;padding:64px clamp(16px,4vw,48px);
}
.section-eyebrow{
display:inline-flex;align-items:center;gap:8px;
font-size:12px;font-weight:700;letter-spacing:.08em;
text-transform:uppercase;color:var(--vert);margin-bottom:16px;
}
h2{
font-family:'Bricolage Grotesque',sans-serif;
font-size:clamp(28px,4vw,48px);
font-weight:500;letter-spacing:-.035em;line-height:1.06;
color:var(--noir);margin-bottom:10px;
}
h2 em{font-style:normal;color:var(--vert);}
.avap-grid{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:48px;}
.avap-card{border-radius:16px;padding:32px;position:relative;overflow:hidden;}
.avap-card.avant{
background:var(--rouge2);border:1px solid #fecaca;
}
.avap-card.apres{
background:var(--vert3);border:1px solid var(--vert4);
}
.avap-head{
display:flex;align-items:center;gap:10px;margin-bottom:24px;
}
.avap-pill{
font-size:12px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;
padding:5px 14px;border-radius:20px;
}
.avant .avap-pill{background:#fecaca;color:var(--rouge);}
.apres .avap-pill{background:var(--vert4);color:var(--vert2);}
.avap-items{display:flex;flex-direction:column;gap:12px;}
.avap-item{
display:flex;align-items:flex-start;gap:10px;
font-size:14px;line-height:1.55;
}
.avant .avap-item{color:#7f1d1d;}
.apres .avap-item{color:var(--vert2);}
.avap-icon{font-size:16px;flex-shrink:0;margin-top:1px;}
.avap-time{
margin-top:24px;padding-top:20px;
border-top:1px solid rgba(0,0,0,.06);
display:flex;align-items:center;gap:8px;
}
.avap-time-num{
font-family:'Bricolage Grotesque',sans-serif;
font-size:32px;font-weight:500;letter-spacing:-.02em;
}
.avant .avap-time-num{color:var(--rouge);}
.apres .avap-time-num{color:var(--vert);}
.avap-time-label{font-size:13px;}
.avant .avap-time-label{color:#7f1d1d;}
.apres .avap-time-label{color:var(--vert2);}
/* ── ÉTAPES SIMPLES ── */
.steps-section{
background:var(--noir);
padding:64px clamp(16px,4vw,48px);
}
.steps-inner{max-width:1100px;margin:0 auto;}
.steps-inner h2{color:#fff;}
.steps-inner .section-eyebrow{color:#84CC16;}
.steps-sub{font-size:16px;color:rgba(255,255,255,.5);margin-top:12px;margin-bottom:56px;}
.steps-grid{
display:grid;grid-template-columns:repeat(3,1fr);
gap:2px;background:rgba(255,255,255,.06);
border-radius:16px;overflow:hidden;
}
.step-card{
background:var(--noir2);padding:40px 32px;
position:relative;transition:background .2s;
}
.step-card:hover{background:#252535;}
.step-num{
font-family:'Bricolage Grotesque',sans-serif;
font-size:72px;font-weight:500;letter-spacing:-.04em;
color:rgba(255,255,255,.05);line-height:1;
margin-bottom:20px;
transition:color .2s;
}
.step-card:hover .step-num{color:rgba(74,222,128,.08);}
.step-icon-wrap{
width:48px;height:48px;border-radius:12px;
background:rgba(74,222,128,.08);border:1px solid rgba(74,222,128,.15);
display:flex;align-items:center;justify-content:center;
font-size:22px;margin-bottom:20px;
}
.step-title{
font-family:'Bricolage Grotesque',sans-serif;
font-size:20px;font-weight:500;color:#fff;
letter-spacing:-.02em;margin-bottom:10px;
}
.step-desc{font-size:14px;color:rgba(255,255,255,.5);line-height:1.7;}
.step-detail{
margin-top:16px;
display:flex;flex-direction:column;gap:6px;
}
.step-detail-item{
display:flex;align-items:center;gap:8px;
font-size:12px;color:rgba(255,255,255,.35);
}
.step-detail-item::before{content:'→';color:#84CC16;font-weight:700;}
.step-arrow{
position:absolute;top:50%;right:-1px;transform:translateY(-50%);
width:0;height:0;
border-top:10px solid transparent;
border-bottom:10px solid transparent;
border-left:12px solid var(--noir2);
display:none;
}
/* ── TÉMOIGNAGES ── */
.temoignages-section{
max-width:1100px;margin:0 auto;padding:64px clamp(16px,4vw,48px);
}
.temoignages-grid{
display:grid;grid-template-columns:repeat(3,1fr);
gap:16px;margin-top:48px;
}
.temoignage-card{
background:var(--bg2);border:1px solid var(--border);
border-radius:16px;padding:28px;
transition:box-shadow .2s,transform .2s;
}
.temoignage-card:hover{box-shadow:var(--sh2);transform:translateY(-3px);}
.temo-stars{color:#f59e0b;font-size:14px;letter-spacing:2px;margin-bottom:16px;}
.temo-quote{
font-size:16px;color:var(--gris);line-height:1.7;font-weight:400;
margin-bottom:20px;font-style:italic;
}
.temo-quote strong{color:var(--noir);font-style:normal;}
.temo-footer{
display:flex;align-items:center;gap:12px;
padding-top:16px;border-top:1px solid var(--border);
}
.temo-avatar{
width:38px;height:38px;border-radius:50%;
display:flex;align-items:center;justify-content:center;
font-size:14px;font-weight:700;color:#fff;flex-shrink:0;
}
.temo-name{font-size:13px;font-weight:700;color:var(--noir);}
.temo-role{font-size:12px;color:var(--gris);}
.temo-amount{
margin-left:auto;
background:var(--vert3);border:1px solid var(--vert4);
color:var(--vert2);padding:4px 12px;border-radius:20px;
font-size:12px;font-weight:700;white-space:nowrap;
}
/* ── OUTIL SECTION ── */
#outil{
background:#fff;
border:none;
}
.outil-wrap{
max-width:1100px;margin:0 auto;padding:80px clamp(12px,4vw,48px);
display:grid;grid-template-columns:1fr 1.1fr;gap:64px;align-items:start;
overflow-x:hidden;
}
#outil{overflow-x:hidden;max-width:100vw;}
.outil-left h2{font-size:38px;margin-bottom:16px;}
.outil-left p{font-size:16px;color:var(--gris);line-height:1.65;margin-bottom:28px;}
.outil-benefits{list-style:none;display:flex;flex-direction:column;gap:10px;}
.outil-benefits li{
display:flex;gap:10px;align-items:flex-start;
font-size:14px;color:var(--gris);
}
.outil-benefits li::before{content:'✓';color:var(--vert);font-weight:700;flex-shrink:0;margin-top:2px;}
.outil-card{
background:#fff;border:1px solid var(--border);
border-radius:18px;box-shadow:var(--sh2);overflow:hidden;
max-width:100%;
}
.outil-card-hd{
background:var(--bg2);border-bottom:1px solid var(--border);
padding:16px 24px;display:flex;justify-content:space-between;align-items:center;
}
.outil-card-title{font-size:14px;font-weight:700;color:var(--noir);}
.places-badge{
display:flex;align-items:center;gap:6px;
background:var(--vert3);border:1px solid var(--vert4);
padding:5px 12px;border-radius:20px;
}
.places-dot{
width:6px;height:6px;border-radius:50%;background:var(--vert);
animation:blink-dot 2s infinite;
}
@keyframes blink-dot{0%,100%{opacity:1}50%{opacity:.4}}
.places-txt{font-size:12px;font-weight:700;color:var(--vert2);}
.outil-card-body{padding:clamp(12px,3vw,24px);overflow-x:hidden;}
/* GATE */
.gate-title{
font-family:'Bricolage Grotesque',sans-serif;
font-size:22px;font-weight:500;letter-spacing:-.02em;margin-bottom:8px;
}
.gate-sub{font-size:14px;color:var(--gris);line-height:1.6;margin-bottom:20px;}
.gate-offer{
background:var(--vert3);border:1px solid var(--vert4);
border-radius:10px;padding:14px 16px;
font-size:13px;color:var(--vert2);margin-bottom:20px;line-height:1.5;
}
.gate-form{display:flex;flex-direction:column;gap:10px;}
.gate-input{
width:100%;padding:12px 14px;
background:var(--bg2);border:1.5px solid var(--border);
border-radius:9px;color:var(--noir);
font-family:'DM Sans',system-ui,sans-serif;font-size:14px;outline:none;
transition:border-color .15s,box-shadow .15s;
}
.gate-input:focus{border-color:var(--vert);box-shadow:0 0 0 3px rgba(5,150,105,.1);}
.gate-input::placeholder{color:var(--gris2);}
.gate-btn{
width:100%;padding:13px;
background:var(--vert);color:#fff;border:none;border-radius:9px;
font-family:'DM Sans',system-ui,sans-serif;
font-size:15px;font-weight:500;cursor:pointer;
transition:background .15s,transform .1s,box-shadow .15s;
}
.gate-btn:hover{background:var(--vert2);transform:translateY(-1px);box-shadow:0 6px 16px rgba(5,150,105,.25);}
.gate-fine{font-size:11px;color:var(--gris2);text-align:center;margin-top:8px;}
/* ── APP LAYOUT SIDEBAR ── */
#app{display:none;overflow-x:hidden;max-width:100vw;width:100%;box-sizing:border-box;}
.app-layout{display:flex;min-height:100vh;background:#f2f2f7;}
/* Sidebar */
.app-sidebar{
width:220px;flex-shrink:0;background:#fff;
border-right:1px solid rgba(0,0,0,.06);
display:flex;flex-direction:column;
position:fixed;top:0;left:0;bottom:0;z-index:100;
overflow-y:auto;-webkit-overflow-scrolling:touch;
}
.app-sidebar-logo{
padding:22px 18px 16px;
font-family:'Bricolage Grotesque',sans-serif;
font-size:20px;font-weight:500;color:#0d0d14;letter-spacing:-.03em;
display:flex;align-items:center;gap:9px;flex-shrink:0;
border-bottom:1px solid #f2f2f7;
}
.app-sidebar-logo-dot{width:9px;height:9px;border-radius:50%;background:#84cc16;flex-shrink:0;}
.app-sidebar-nav{padding:8px;flex:1;display:block;overflow-y:auto;}
.sidebar-item{
display:flex;align-items:center;gap:10px;
padding:10px 12px;border-radius:10px;
font-size:14px;font-weight:500;color:#64748b;
cursor:pointer;transition:background .1s,color .1s;
width:100%;border:none;background:none;
font-family:'DM Sans',system-ui,sans-serif;text-align:left;
line-height:1.2;
}
.sidebar-item.active{background:#f0fdf4;color:#15803d;font-weight:500;}
.sidebar-item:hover:not(.active){background:#f5f5f7;color:#0d0d14;}
.sidebar-item-icon{width:20px;height:20px;flex-shrink:0;display:flex;align-items:center;justify-content:center;}
.app-sidebar-footer{padding:10px 8px;border-top:1px solid #f2f2f7;flex-shrink:0;}
.sidebar-user{
display:flex;align-items:center;gap:10px;
padding:10px 12px;border-radius:10px;cursor:pointer;transition:background .12s;
}
.sidebar-user:hover{background:#f5f5f7;}
.sidebar-user-avatar{
width:32px;height:32px;border-radius:50%;
background:linear-gradient(135deg,#84cc16,#65a30d);
display:flex;align-items:center;justify-content:center;
font-size:12px;font-weight:800;color:#fff;flex-shrink:0;
}
.sidebar-user-name{font-size:13px;font-weight:700;color:#0d0d14;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.sidebar-user-co{font-size:11px;color:#94a3b8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
/* Main content area */
.app-main{
margin-left:220px;flex:1;min-width:0;
padding:28px 32px 80px;
max-width:calc(100% - 220px);box-sizing:border-box;
}
/* Main header */
.app-main-header{
display:flex;align-items:flex-start;justify-content:space-between;
margin-bottom:24px;gap:16px;
}
.app-main-greeting-title{
font-family:'Bricolage Grotesque',sans-serif;
font-size:clamp(22px,3vw,30px);font-weight:500;letter-spacing:-.04em;color:#0d0d14;
line-height:1.05;margin-bottom:3px;
}
.app-main-greeting-sub{font-size:13px;color:#64748b;}
/* 4 KPIs */
.dash-kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:20px;}
.dash-kpi{
background:#fff;border-radius:16px;padding:18px 18px 16px;
box-shadow:0 1px 3px rgba(0,0,0,.05),0 0 0 .5px rgba(0,0,0,.04);
position:relative;overflow:hidden;
}
.dash-kpi::before{content:'';position:absolute;top:0;left:0;right:0;height:3px;border-radius:16px 16px 0 0;background:#f2f2f7;}
.dash-kpi.kpi-vert::before{background:linear-gradient(90deg,#84cc16,#65a30d);}
.dash-kpi.kpi-lime::before{background:linear-gradient(90deg,#84CC16,#84CC16);}
.dash-kpi.kpi-orange::before{background:linear-gradient(90deg,#f59e0b,#f97316);}
.dash-kpi.kpi-neutral::before{background:linear-gradient(90deg,#cbd5e1,#94a3b8);}
.dash-kpi-label{font-size:10px;font-weight:700;color:#94a3b8;letter-spacing:.06em;text-transform:uppercase;margin-bottom:10px;}
.dash-kpi-value{font-family:'Bricolage Grotesque',sans-serif;font-size:clamp(24px,3vw,34px);font-weight:500;letter-spacing:-.04em;line-height:1;}
.dash-kpi-sub{font-size:11px;color:#94a3b8;margin-top:6px;display:flex;align-items:center;gap:4px;}
.kpi-trend{font-size:10px;font-weight:500;padding:2px 5px;border-radius:5px;margin-left:2px;}
.kpi-trend.up{color:#15803d;background:#f0fdf4;}
/* Tabs dossiers */
.dash-tabs{display:flex;gap:4px;padding:14px 20px 12px;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;border-bottom:1px solid #f2f2f7;}
.dash-tabs::-webkit-scrollbar{display:none;}
.dash-tab-btn{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;border-radius:20px;border:none;background:transparent;font-family:inherit;font-size:13px;font-weight:500;color:#94a3b8;cursor:pointer;transition:all .15s;white-space:nowrap;flex-shrink:0;}
.dash-tab-btn.active{background:#0d0d14;color:#fff;}
.dash-tab-btn.tab-urgent{color:#ef4444;}
.dash-tab-btn.tab-urgent.active{background:#ef4444;color:#fff;}
.dash-tab-count{font-size:11px;font-weight:500;padding:1px 7px;border-radius:10px;background:rgba(0,0,0,.06);min-width:18px;text-align:center;}
.dash-tab-btn.active .dash-tab-count{background:rgba(255,255,255,.2);}
.dash-tab-btn.tab-urgent .dash-tab-count{background:#fee2e2;}
.dash-tab-btn.tab-urgent.active .dash-tab-count{background:rgba(255,255,255,.25);}
/* Action card (refonte) */
.action-card{
background:linear-gradient(135deg,#0f172a 0%,#1e293b 100%);
border-radius:20px;padding:24px 28px;color:#fff;margin-bottom:20px;
display:flex;align-items:center;gap:24px;flex-wrap:wrap;
}
.action-card-icon{
width:52px;height:52px;border-radius:16px;
background:rgba(132,204,22,.15);border:1px solid rgba(132,204,22,.3);
display:flex;align-items:center;justify-content:center;font-size:24px;flex-shrink:0;
}
.action-card-body{flex:1;min-width:200px;}
.action-card-eyebrow{font-size:10px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:#84cc16;margin-bottom:6px;}
.action-card-title{font-family:'Bricolage Grotesque',sans-serif;font-size:clamp(18px,2.5vw,22px);font-weight:500;letter-spacing:-.03em;margin-bottom:2px;}
.action-card-sub{font-size:13px;color:rgba(255,255,255,.55);}
.action-card-right{display:flex;flex-direction:column;align-items:flex-end;gap:10px;flex-shrink:0;}
.action-card-btns{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end;}
.btn-action-primary{
display:inline-flex;align-items:center;gap:6px;
padding:11px 18px;background:#84cc16;color:#0f172a;
border:none;border-radius:10px;font-family:inherit;font-size:14px;font-weight:500;cursor:pointer;
transition:all .15s;white-space:nowrap;
}
.btn-action-primary:hover{background:#84CC16;transform:translateY(-1px);}
.btn-action-ghost{
padding:11px 16px;background:rgba(255,255,255,.1);color:#fff;
border:1px solid rgba(255,255,255,.15);border-radius:10px;
font-family:inherit;font-size:14px;font-weight:500;cursor:pointer;
transition:all .15s;white-space:nowrap;
}
.btn-action-ghost:hover{background:rgba(255,255,255,.18);}
/* Step bubbles */
.step-bubbles{display:flex;align-items:center;gap:3px;}
.step-bubble{
width:22px;height:22px;border-radius:50%;
display:flex;align-items:center;justify-content:center;
font-size:9px;font-weight:800;flex-shrink:0;
}
.step-bubble.done{background:#84cc16;color:#fff;}
.step-bubble.current{background:#0f172a;color:#84cc16;box-shadow:0 0 0 2px #84cc16;}
.step-bubble.pending{background:#f2f2f7;color:#cbd5e1;}
/* Dossiers section */
.dash-section{background:#fff;border-radius:20px;box-shadow:0 1px 3px rgba(0,0,0,.05),0 0 0 .5px rgba(0,0,0,.04);overflow:hidden;margin-bottom:16px;}
.dash-section-head{padding:18px 22px 14px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #f5f5f7;}
.dash-section-title{font-family:'Bricolage Grotesque',sans-serif;font-size:17px;font-weight:500;color:#0d0d14;letter-spacing:-.02em;}
/* Dossier rows */
.dossier-row{
display:flex;align-items:center;gap:14px;
padding:14px 22px;cursor:pointer;
border-top:1px solid #f8f8fa;
transition:background .1s;
}
.dossier-row:hover{background:#f9f9fb;}
.dossier-badge{
width:36px;height:36px;border-radius:11px;flex-shrink:0;
display:flex;align-items:center;justify-content:center;
font-family:'Bricolage Grotesque',sans-serif;font-size:15px;font-weight:500;color:#fff;
}
.dossier-main{flex:1;min-width:0;}
.dossier-client{font-size:14px;font-weight:700;color:#0d0d14;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.dossier-montant-small{font-size:12px;color:#84cc16;font-weight:700;margin-top:1px;}
.dossier-mid{display:flex;align-items:center;gap:10px;flex-shrink:0;}
.dossier-right{text-align:right;flex-shrink:0;min-width:90px;}
.dossier-montant{font-family:'Bricolage Grotesque',sans-serif;font-size:16px;font-weight:500;letter-spacing:-.03em;color:#0d0d14;}
.dossier-status{font-size:11px;font-weight:600;margin-top:2px;}
.dossier-chevron{color:#d1d5db;font-size:18px;font-weight:300;margin-left:4px;flex-shrink:0;}
.dossier-group-label{padding:8px 22px 4px;font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#94a3b8;background:#fafafa;border-top:1px solid #f2f2f7;}
/* À traiter section */
.traiter-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;padding:0 22px 18px;}
.traiter-card{
background:#f8fdf4;border:1px solid #d9f99d;border-radius:14px;padding:14px 16px;
display:flex;flex-direction:column;gap:10px;
}
.traiter-card.urgent{background:#fef9f2;border-color:#fed7aa;}
.traiter-card-client{font-size:13px;font-weight:500;color:#0d0d14;}
.traiter-card-montant{font-size:12px;color:#64748b;}
.traiter-card-label{font-size:11px;font-weight:500;color:#84cc16;}
.traiter-card.urgent .traiter-card-label{color:#f97316;}
.traiter-card-btn{
align-self:flex-start;padding:8px 14px;border:none;border-radius:8px;
font-family:inherit;font-size:13px;font-weight:500;cursor:pointer;
display:flex;align-items:center;gap:6px;
}
/* ── PAGE VIEWS (nouvelle, profil, detail) ── */
.back-btn{
display:inline-flex;align-items:center;gap:5px;
background:none;border:none;color:#94a3b8;font-size:13px;
cursor:pointer;font-family:'DM Sans',system-ui,sans-serif;padding:0;
margin-bottom:10px;transition:color .1s;
}
.back-btn:hover{color:#0d0d14;}
.page-header{margin-bottom:20px;}
.page-title{
font-family:'Bricolage Grotesque',sans-serif;
font-size:clamp(20px,3vw,26px);font-weight:500;letter-spacing:-.03em;color:#0d0d14;
margin-bottom:3px;
}
.page-sub{font-size:13px;color:#94a3b8;}
.view-card{
background:#fff;border-radius:20px;
box-shadow:0 1px 3px rgba(0,0,0,.05),0 0 0 .5px rgba(0,0,0,.04);
padding:24px;margin-bottom:16px;
}
.view-card-title{
font-family:'Bricolage Grotesque',sans-serif;font-size:15px;font-weight:500;
color:#0d0d14;letter-spacing:-.02em;margin-bottom:14px;
padding-bottom:12px;border-bottom:1px solid #f2f2f7;
}
.section-label{
font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;
color:#94a3b8;margin-bottom:10px;
}
/* Detail header card */
.detail-header-card{
background:#fff;border-radius:20px;
box-shadow:0 1px 3px rgba(0,0,0,.05),0 0 0 .5px rgba(0,0,0,.04);
padding:20px 24px;margin-bottom:16px;
display:flex;align-items:center;gap:16px;flex-wrap:wrap;
}
.detail-client-name{
font-family:'Bricolage Grotesque',sans-serif;
font-size:clamp(18px,3vw,22px);font-weight:500;letter-spacing:-.03em;color:#0d0d14;
}
.detail-ref{font-size:12px;color:#94a3b8;margin-top:2px;}
.detail-montant{
font-family:'Bricolage Grotesque',sans-serif;
font-size:clamp(22px,4vw,28px);font-weight:500;letter-spacing:-.04em;color:#0d0d14;
}
/* Step progress bar (horizontal) */
.step-progress-bar{display:flex;align-items:center;gap:3px;margin-top:12px;}
.spb-item{
flex:1;height:4px;border-radius:2px;
background:#f2f2f7;transition:background .3s;
}
.spb-item.done{background:#84cc16;}
.spb-item.current{background:#0f172a;}
/* Mobile top bar (hidden on desktop) */
.app-mobile-topbar{
display:none;position:sticky;top:0;z-index:90;
background:rgba(255,255,255,.95);backdrop-filter:blur(16px);
-webkit-backdrop-filter:blur(16px);
border-bottom:1px solid rgba(0,0,0,.07);
height:48px;align-items:center;justify-content:space-between;
padding:0 16px;
}
.app-mobile-topbar-logo{
display:flex;align-items:center;gap:7px;
font-family:'Bricolage Grotesque',sans-serif;font-size:17px;font-weight:500;color:#0d0d14;
}
/* Mobile bottom tabs */
.app-bottom-tabs{
display:none;position:fixed;bottom:0;left:0;right:0;
background:rgba(255,255,255,.97);backdrop-filter:blur(16px);
-webkit-backdrop-filter:blur(16px);
border-top:1px solid rgba(0,0,0,.08);
padding:5px 0 max(5px,env(safe-area-inset-bottom));z-index:100;
}
.bottom-tab{
flex:1;display:flex;flex-direction:column;align-items:center;gap:2px;
font-size:9px;font-weight:500;color:#94a3b8;
cursor:pointer;background:none;border:none;font-family:inherit;padding:3px 0;
}
.bottom-tab.active{color:#15803d;font-weight:500;}
.bottom-tab-icon{width:22px;height:22px;display:flex;align-items:center;justify-content:center;}
/* Legacy compatibility */
.app-shell{display:none;}
.app-surface{display:block;}
.app-bar{display:none;}
.app-nav{display:flex;gap:4px;align-items:center;flex:1;flex-wrap:nowrap;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none}
.app-nav::-webkit-scrollbar{display:none}
.app-hero{
display:grid;grid-template-columns:1.4fr .9fr;
gap:18px;margin-bottom:22px;
}
.hero-panel{
background:linear-gradient(135deg,#0f172a 0%,#1e293b 100%);
color:#fff;border-radius:24px;padding:26px;
box-shadow:0 24px 60px rgba(15,23,42,.18);
}
.hero-panel .eyebrow{
display:inline-flex;align-items:center;gap:8px;
padding:6px 12px;border-radius:999px;
background:rgba(255,255,255,.1);
color:#dbeafe;font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;
margin-bottom:14px;
}
.hero-panel h3{
font-family:'Bricolage Grotesque',sans-serif;
font-size:30px;line-height:1.02;letter-spacing:-.04em;margin-bottom:10px;
}
.hero-panel p{font-size:15px;line-height:1.65;color:rgba(255,255,255,.78);max-width:720px}
.hero-actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:18px}
.btn-soft-dark,.btn-soft-light{
border:none;border-radius:12px;padding:12px 16px;font:inherit;font-size:14px;font-weight:500;cursor:pointer;transition:all .16s ease;
}
.btn-soft-light{background:#fff;color:#0f172a}
.btn-soft-dark{background:rgba(255,255,255,.08);color:#fff;border:1px solid rgba(255,255,255,.12)}
.btn-soft-light:hover,.btn-soft-dark:hover{transform:translateY(-1px)}
.hero-side{
background:#fff;border:1px solid var(--border);
border-radius:24px;padding:22px;
box-shadow:0 12px 28px rgba(15,23,42,.05);
}
.hero-side-label{
font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--gris);font-weight:700;margin-bottom:10px;
}
.next-action-card{
border-radius:18px;padding:18px;
background:linear-gradient(180deg,#f8fafc 0%,#ffffff 100%);
border:1px solid var(--border);
}
.next-action-client{font-size:20px;font-weight:800;letter-spacing:-.03em;color:var(--noir);margin-bottom:6px}
.next-action-step{font-size:14px;color:var(--gris);line-height:1.6;margin-bottom:14px}
.action-meta{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:16px}
.meta-pill{
display:inline-flex;align-items:center;gap:6px;
padding:6px 10px;border-radius:999px;
background:var(--bg2);border:1px solid var(--border);font-size:12px;color:var(--gris);font-weight:700;
}
.dash-grid{
display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-bottom:22px;
}
.metric-card{
background:#fff;border:1px solid var(--border);
border-radius:20px;padding:20px;
box-shadow:0 10px 24px rgba(15,23,42,.04);
}
.metric-top{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:18px}
.metric-label{font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--gris);font-weight:700}
.metric-badge{
width:42px;height:42px;border-radius:14px;
background:var(--bg2);display:flex;align-items:center;justify-content:center;
font-size:18px;
}
.metric-value{
font-family:'Bricolage Grotesque',sans-serif;
font-size:34px;font-weight:500;letter-spacing:-.05em;color:var(--noir);line-height:1;
}
.metric-sub{font-size:13px;color:var(--gris);margin-top:8px;line-height:1.5}
.list-panel{
background:#fff;border:1px solid var(--border);
border-radius:24px;padding:20px;
box-shadow:0 14px 30px rgba(15,23,42,.04);
}
.list-head{
display:flex;justify-content:space-between;align-items:flex-end;gap:12px;flex-wrap:wrap;
margin-bottom:16px;
}
.list-title{
font-family:'Bricolage Grotesque',sans-serif;font-size:24px;font-weight:500;letter-spacing:-.03em;color:var(--noir);
}
.list-sub{font-size:14px;color:var(--gris);margin-top:4px}
.creance-list{display:flex;flex-direction:column;gap:14px}
.creance-row{
display:grid;grid-template-columns:minmax(0,1.4fr) minmax(150px,.8fr) minmax(160px,.8fr) auto;
gap:18px;align-items:center;
padding:18px;border:1px solid var(--border);border-radius:20px;
background:linear-gradient(180deg,#ffffff 0%,#fbfdff 100%);
transition:all .16s ease;cursor:pointer;
}
.creance-row:hover{transform:translateY(-1px);box-shadow:0 16px 34px rgba(15,23,42,.05);border-color:#d8e2ee}
.creance-main{min-width:0}
.creance-client{font-size:18px;font-weight:800;letter-spacing:-.03em;color:var(--noir);margin-bottom:4px;}
.creance-ref{font-size:12px;color:var(--gris2)}
.creance-col-label{font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--gris);font-weight:700;margin-bottom:8px;}
.creance-amount{font-family:'Bricolage Grotesque',sans-serif;font-size:28px;font-weight:500;letter-spacing:-.05em;color:var(--noir);}
.creance-helper{font-size:12px;color:var(--gris2);margin-top:6px}
.status-chip{
display:inline-flex;align-items:center;gap:8px;padding:8px 12px;border-radius:999px;
font-size:12px;font-weight:800;border:1px solid transparent;
}
.status-chip.current{background:#fff7ed;color:#c2410c;border-color:#fed7aa}
.status-chip.paid{background:#f0fdf4;color:#15803d;border-color:#bbf7d0}
.progress-track{height:6px;background:#e2e8f0;border-radius:999px;overflow:hidden;margin-top:10px}
.progress-fill{height:100%;background:linear-gradient(90deg,var(--vert),#84cc16);border-radius:999px}
.action-label{font-size:15px;font-weight:800;color:var(--vert2);line-height:1.35}
.action-date{font-size:12px;color:var(--gris2);margin-top:6px}
.row-cta{display:flex;align-items:center;gap:8px;justify-content:flex-end;flex-wrap:wrap;}
.row-btn{border:none;border-radius:12px;padding:10px 14px;font:inherit;font-size:13px;font-weight:500;cursor:pointer;}
.row-btn.primary{background:var(--noir);color:#fff}
.row-btn.secondary{background:var(--bg2);color:var(--noir);border:1px solid var(--border)}
.empty-state{text-align:center;padding:58px 20px 42px;}
.empty-state-icon{width:72px;height:72px;border-radius:22px;background:var(--vert3);display:flex;align-items:center;justify-content:center;margin:0 auto 18px;font-size:32px;}
.empty-state h3{font-family:'Bricolage Grotesque',sans-serif;font-size:30px;letter-spacing:-.03em;margin-bottom:10px}
.empty-state p{max-width:440px;margin:0 auto 20px;font-size:15px;line-height:1.7;color:var(--gris)}
.app-note{margin-top:14px;font-size:13px;color:var(--gris);line-height:1.6;}
/* Legacy dash-card (used in detail view) */
.dash-row{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:16px;}
.dash-card{
background:#fff;border:1px solid var(--border);
border-radius:12px;padding:16px;text-align:center;
box-shadow:0 1px 3px rgba(0,0,0,.08),0 1px 2px rgba(0,0,0,.06);
transition:box-shadow .15s,transform .15s;
}
.dash-card:hover{box-shadow:0 4px 12px rgba(0,0,0,.1);transform:translateY(-1px);}
.dash-num{font-family:'Bricolage Grotesque',sans-serif;font-size:20px;font-weight:500;letter-spacing:-.02em;color:var(--noir);}
.dash-num.green{color:var(--vert);}
.dash-label{font-size:10px;color:var(--gris);margin-top:2px;}
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px;}
.fg{display:flex;flex-direction:column;gap:5px;}
.fg.full{grid-column:1/-1;}
label{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--gris);}
input,select{
padding:10px 12px;
background:var(--bg2);border:1.5px solid var(--border);
border-radius:8px;color:var(--noir);
font-family:'DM Sans',system-ui,sans-serif;font-size:14px;outline:none;width:100%;
transition:border-color .15s,box-shadow .15s;
}
input:focus,select:focus{border-color:var(--vert);box-shadow:0 0 0 3px rgba(5,150,105,.1);}
input::placeholder{color:var(--gris2);}
select option{background:#fff;}
.profil-row{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;}
.profil-btn{
padding:10px 6px;border-radius:8px;border:1.5px solid var(--border);
background:var(--bg2);color:var(--gris);
font-family:'DM Sans',system-ui,sans-serif;font-size:11px;font-weight:600;
cursor:pointer;text-align:center;line-height:1.4;transition:all .15s;
}
.profil-btn:hover{border-color:var(--vert);color:var(--noir);}
.profil-btn.active{border-color:#84cc16;background:#f7fee7;color:#3f6212;}
.pb-icon{font-size:16px;display:block;margin-bottom:4px;}
.gen-btn{
width:100%;padding:14px;margin-top:10px;
background:#84cc16;color:#0f172a;border:none;border-radius:12px;
font-family:'Bricolage Grotesque',sans-serif;font-size:15px;font-weight:500;cursor:pointer;
letter-spacing:-.01em;
transition:background .15s,transform .1s,box-shadow .15s;
}
.gen-btn:hover:not(:disabled){background:#84CC16;transform:translateY(-1px);box-shadow:0 6px 20px rgba(132,204,22,.3);}
.gen-btn:disabled{opacity:.45;cursor:not-allowed;}
#results{margin-top:16px;display:none;}
.results-lbl{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--gris);margin-bottom:10px;}
.email-card{border:1px solid var(--border);border-radius:10px;margin-bottom:8px;overflow:hidden;}
.email-hd{
background:var(--bg2);border-bottom:1px solid var(--border);
padding:10px 14px;display:flex;justify-content:space-between;align-items:center;
}
.email-step{font-size:11px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--vert);}
.email-when{font-size:10px;color:var(--gris2);}
.copy-btn{
background:var(--bg);border:1px solid var(--border);color:var(--gris);
padding:4px 12px;border-radius:6px;font-size:11px;font-weight:500;cursor:pointer;
font-family:'DM Sans',system-ui,sans-serif;transition:all .15s;
}
.copy-btn:hover{border-color:var(--vert);color:var(--vert);}
.email-bd{padding:12px 14px;}
.email-objet{font-size:12px;color:var(--gris);margin-bottom:6px;}
.email-objet strong{color:var(--noir);font-weight:600;}
.email-text{font-size:12px;color:var(--gris);line-height:1.7;white-space:pre-wrap;}
.cash-banner{
margin-top:12px;padding:12px 14px;
background:var(--vert3);border:1px solid var(--vert4);
border-radius:9px;display:flex;align-items:center;gap:12px;
}
.cash-num{font-family:'Bricolage Grotesque',sans-serif;font-size:22px;font-weight:500;color:var(--vert);flex-shrink:0;letter-spacing:-.02em;}
.cash-txt{font-size:12px;color:var(--vert2);line-height:1.5;}
.loading{text-align:center;padding:28px;font-size:13px;color:var(--gris);}
.dot{display:inline-block;animation:bl 1.4s infinite;}
.dot:nth-child(2){animation-delay:.2s;}
.dot:nth-child(3){animation-delay:.4s;}
@keyframes bl{0%,80%,100%{opacity:.2}40%{opacity:1}}
@keyframes fadeInUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}
/* ── UNLOCK ANIMATION ── */
@keyframes unlockPop{0%{transform:scale(.4);opacity:0}60%{transform:scale(1.12)}100%{transform:scale(1);opacity:1}}
@keyframes unlockCheck{0%{stroke-dashoffset:60}100%{stroke-dashoffset:0}}
@keyframes unlockFadeOut{0%{opacity:1}80%{opacity:1}100%{opacity:0;pointer-events:none}}
@keyframes coinRain{0%{transform:translateY(-20px) rotate(0deg);opacity:1}100%{transform:translateY(80px) rotate(360deg);opacity:0}}
@keyframes pulseRing{0%{transform:scale(1);opacity:.6}100%{transform:scale(2.2);opacity:0}}
.unlock-overlay{position:fixed;inset:0;z-index:99999;display:flex;align-items:center;justify-content:center;background:rgba(15,23,42,.88);backdrop-filter:blur(6px);animation:unlockFadeOut 3.2s ease forwards}
.unlock-card{background:linear-gradient(135deg,#0f172a,#1e293b);border:2px solid #84cc16;border-radius:24px;padding:40px 48px;text-align:center;max-width:400px;width:90%;position:relative;animation:unlockPop .5s cubic-bezier(.34,1.56,.64,1) forwards}
.unlock-ring{position:absolute;inset:-16px;border-radius:40px;border:2px solid #84cc16;animation:pulseRing 1s ease 0.4s forwards}
.unlock-icon{font-size:56px;display:block;margin-bottom:12px;filter:drop-shadow(0 0 20px rgba(132,204,22,.6))}
.unlock-title{font-family:'Bricolage Grotesque',sans-serif;font-size:26px;font-weight:500;color:#fff;margin-bottom:6px}
.unlock-montant{font-family:'Bricolage Grotesque',sans-serif;font-size:42px;font-weight:500;color:#84cc16;margin:8px 0;letter-spacing:-.03em}
.unlock-sub{font-size:16px;color:rgba(255,255,255,.7);font-weight:300;line-height:1.5}
.unlock-client{font-size:13px;color:#84cc16;font-weight:700;margin-top:8px}
/* ── DEMO BADGE ── */
.demo-btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;background:linear-gradient(135deg,#fefce8,#fef9c3);border:1px solid #fde68a;border-radius:10px;font-size:13px;font-weight:500;color:#92400e;cursor:pointer;transition:all .15s;margin-bottom:16px;width:100%}
.demo-btn:hover{background:linear-gradient(135deg,#fef9c3,#fde68a);border-color:#f59e0b}
/* ── COCKPIT STRIP ── */
.cockpit-strip{display:flex;align-items:center;gap:10px;padding:12px 16px;border-radius:14px;margin-bottom:14px;font-size:13px}
.cockpit-urgent{background:linear-gradient(135deg,#fef2f2,#fee2e2);border:1px solid #fecaca;color:#991b1b}
.cockpit-ok{background:linear-gradient(135deg,#f0fdf4,#dcfce7);border:1px solid #bbf7d0;color:#166534}
/* ── ROI BLOCK ── */
.roi-block{background:linear-gradient(135deg,#f0fdf4,#ecfccb);border:1px solid #bbf7d0;border-radius:14px;padding:16px;margin-bottom:14px}
/* ── NOTIF EMAIL ── */
.notif-banner{background:linear-gradient(135deg,#eff6ff,#dbeafe);border:1px solid #bfdbfe;border-radius:12px;padding:12px 16px;display:flex;align-items:center;gap:12px;font-size:13px;color:#1e40af;margin-bottom:14px}
/* Tooltips aide */
.tip{position:relative;display:inline-flex;align-items:center;justify-content:center;width:15px;height:15px;border-radius:50%;background:#e8e8ed;color:#64748b;font-size:9px;font-weight:800;cursor:help;flex-shrink:0;margin-left:5px;vertical-align:middle;line-height:1;transition:background .15s,color .15s}
.tip:hover,.tip.active{background:#0d0d14;color:#fff;}
/* Tooltip global fixe — centré à l'écran, visible partout y compris mobile */
#g-tip{display:none;position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);background:#0d0d14;color:#fff;font-size:14px;font-weight:400;line-height:1.65;padding:20px 24px;border-radius:16px;text-align:left;white-space:normal;width:max-content;max-width:min(440px,88vw);z-index:9999;box-shadow:0 20px 60px rgba(0,0,0,.45),0 0 0 1px rgba(255,255,255,.06);pointer-events:none;font-family:'DM Sans',sans-serif;opacity:0;transition:opacity .15s ease}
#g-tip strong{font-weight:700;color:#84CC16}
#g-tip-overlay{cursor:pointer}
#g-tip.visible{opacity:1}
#g-tip-overlay{display:none;position:fixed;inset:0;z-index:9998;background:rgba(0,0,0,.18)}
/* ── PRICING ── */
.pricing-section{max-width:900px;margin:0 auto;padding:64px clamp(16px,4vw,48px);}
.pricing-grid{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:48px;}
.p-card{
background:var(--bg);border:1.5px solid var(--border);
border-radius:18px;padding:36px;
box-shadow:var(--sh1);position:relative;
transition:box-shadow .2s,transform .2s;
display:flex;flex-direction:column;
}
.p-card .p-cta,.p-card .p-cta-wrap{margin-top:auto}
.p-card:hover{box-shadow:var(--sh2);transform:translateY(-2px);}
.p-card.featured{border-color:var(--vert);border-width:2px;box-shadow:0 0 0 4px rgba(5,150,105,.07),var(--sh1);}
.p-badge{
position:absolute;top:-13px;left:24px;
background:var(--vert);color:#fff;
font-size:11px;font-weight:700;letter-spacing:.04em;
padding:4px 14px;border-radius:20px;
}
.p-name{font-size:12px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--gris);margin-bottom:16px;}
.p-price{
font-family:'Bricolage Grotesque',sans-serif;
font-size:54px;font-weight:500;letter-spacing:-.04em;line-height:1;margin-bottom:6px;
}
.p-price sup{font-size:24px;vertical-align:super;margin-top:10px;}
.p-period{font-size:13px;color:var(--gris);margin-bottom:28px;}
.p-feats{list-style:none;margin-bottom:28px;}
.p-feats li{
font-size:14px;color:var(--gris);
padding:9px 0;border-bottom:1px solid var(--border2);
display:flex;gap:10px;align-items:flex-start;
}
.p-feats li:last-child{border-bottom:none;}
.p-feats li::before{content:'✓';color:var(--vert);font-weight:700;flex-shrink:0;}
.p-cta{
width:100%;padding:14px;border-radius:9px;
font-family:'DM Sans',system-ui,sans-serif;
font-size:15px;font-weight:500;cursor:pointer;transition:all .15s;
}
.p-cta.solid{background:var(--vert);color:#fff;border:none;}
.p-cta.solid:hover{background:var(--vert2);box-shadow:0 6px 16px rgba(5,150,105,.25);}
.p-cta.ghost{background:transparent;border:1.5px solid var(--border);color:var(--gris);}
.p-cta.ghost:hover{border-color:var(--noir);color:var(--noir);}
/* Variante p-card sombre (RECOVMAX sur fond noir) — surcharges contraste */
.p-card.dark .p-name{color:rgba(255,255,255,.55)}
.p-card.dark .p-period{color:rgba(255,255,255,.7)}
.p-card.dark .p-feats li{color:rgba(255,255,255,.88);border-bottom-color:rgba(255,255,255,.08)}
.p-card.dark .p-feats li::before{color:#84CC16}
/* ── FAQ ── */
.faq-section{max-width:720px;margin:0 auto;padding:clamp(40px,5vw,64px) clamp(16px,4vw,48px) clamp(40px,5vw,64px);}
.faq-item{border-bottom:1px solid var(--border);padding:20px 0;cursor:pointer;}
.faq-q{font-size:16px;font-weight:600;display:flex;justify-content:space-between;align-items:center;gap:16px;color:var(--noir);}
.faq-ico{color:var(--gris);font-size:20px;transition:transform .2s;flex-shrink:0;}
.faq-item.open .faq-ico{transform:rotate(45deg);color:var(--vert);}
.faq-a{font-size:16px;color:var(--gris);line-height:1.7;font-weight:4005;margin-top:12px;display:none;}
.faq-item.open .faq-a{display:block;}
/* ── CTA FINAL ── */
.cta-section{
background:var(--vert3);
border-top:1px solid var(--vert4);
overflow:hidden;
padding:clamp(48px,6vw,72px) clamp(16px,4vw,48px) clamp(48px,6vw,72px);text-align:center;
}
.cta-inner{max-width:600px;margin:0 auto;}
.cta-section h2{color:var(--noir);font-size:42px;margin:0 0 24px;padding:0;}
.cta-section p{color:var(--gris);font-size:16px;line-height:1.65;margin-bottom:36px;}
.btn-cta-white{
background:var(--vert);color:#fff;
padding:16px 36px;border-radius:10px;
font-size:16px;font-weight:500;border:none;cursor:pointer;
transition:transform .15s,box-shadow .15s;
}
.btn-cta-white:hover{transform:translateY(-2px);box-shadow:0 10px 28px rgba(5,150,105,.25);}
.cta-note{margin-top:16px;font-size:13px;color:var(--gris);}
/* ── FOOTER ── */
footer{
background:var(--noir2);
padding:32px clamp(16px,4vw,48px);
display:flex;justify-content:space-between;align-items:center;
flex-wrap:wrap;gap:16px;
border-top:1px solid rgba(255,255,255,.06);
}
.footer-logo-wrap{display:flex;align-items:center;gap:8px;}
.footer-logo-mark{
width:28px;height:28px;border-radius:7px;
background:rgba(255,255,255,.1);
display:flex;align-items:center;justify-content:center;
}
.footer-logo-type{
font-family:'Bricolage Grotesque',sans-serif;
font-size:16px;font-weight:500;color:#fff;letter-spacing:-.02em;
}
.footer-logo-type span{color:#84CC16;}
.footer-tagline{font-size:12px;color:rgba(255,255,255,.6);margin-top:4px;}
.footer-links{display:flex;gap:20px;flex-wrap:wrap;}
.footer-links a{font-size:13px;color:rgba(255,255,255,.4);text-decoration:none;transition:color .15s;}
.footer-links a:hover{color:#fff;}
/* ── APP NAV BUTTONS ── */
.app-nav-btn{
padding:6px 13px;border-radius:20px;
border:none;background:transparent;
color:var(--gris);font-family:'DM Sans',system-ui,sans-serif;
font-size:13px;font-weight:500;cursor:pointer;
transition:all .15s;white-space:nowrap;
}
.app-nav-btn:hover{color:var(--noir);background:rgba(0,0,0,.06)}
.app-nav-btn.active{
background:#fff;color:var(--noir);font-weight:700;
box-shadow:0 1px 4px rgba(0,0,0,.12);
}
/* App logo in nav */
.app-logo{
font-family:'Bricolage Grotesque',sans-serif;
font-size:16px;font-weight:500;color:var(--noir);
letter-spacing:-.03em;flex-shrink:0;
display:flex;align-items:center;gap:6px;
}
.app-logo-dot{
width:8px;height:8px;border-radius:50%;
background:#84cc16;flex-shrink:0;
}
/* Dashboard greeting */
.dash-greeting{
padding:24px 0 20px;
}
.dash-greeting-title{
font-family:'Bricolage Grotesque',sans-serif;
font-size:clamp(26px,6vw,34px);font-weight:500;
letter-spacing:-.04em;color:var(--noir);
line-height:1.05;margin-bottom:4px;
}
.dash-greeting-sub{
font-size:14px;color:var(--gris);font-weight:400;
}
/* Apple-style metric cards */
.dash-metrics{
display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px;
}
.dash-metric{
background:#fff;border-radius:20px;padding:20px;
box-shadow:0 2px 8px rgba(0,0,0,.06),0 0 0 .5px rgba(0,0,0,.04);
}
.dash-metric-label{
font-size:12px;font-weight:600;color:var(--gris);
letter-spacing:.01em;margin-bottom:10px;
}
.dash-metric-value{
font-family:'Bricolage Grotesque',sans-serif;
font-size:clamp(24px,6vw,32px);font-weight:500;
letter-spacing:-.04em;line-height:1;
}
.dash-metric-sub{font-size:11px;color:var(--gris);margin-top:6px;}
/* Dossiers section */
.dash-section{
background:#fff;border-radius:20px;
box-shadow:0 2px 8px rgba(0,0,0,.06),0 0 0 .5px rgba(0,0,0,.04);
overflow:hidden;margin-bottom:16px;
}
.dash-section-head{
padding:16px 20px 12px;
display:flex;align-items:center;justify-content:space-between;
}
.dash-section-title{
font-family:'Bricolage Grotesque',sans-serif;
font-size:17px;font-weight:500;color:var(--noir);letter-spacing:-.02em;
}
/* Dossier row Apple style */
.dossier-row{
display:flex;align-items:center;gap:14px;
padding:14px 20px;cursor:pointer;
border-top:1px solid #f2f2f7;
transition:background .12s;
-webkit-tap-highlight-color:transparent;
}
.dossier-row:first-child{border-top:none;}
.dossier-row:hover{background:#f9f9fb;}
.dossier-row:active{background:#f2f2f7;}
.dossier-badge{
width:40px;height:40px;border-radius:12px;
display:flex;align-items:center;justify-content:center;
font-family:'Bricolage Grotesque',sans-serif;
font-size:15px;font-weight:500;color:#fff;flex-shrink:0;
}
.dossier-main{flex:1;min-width:0;}
.dossier-client{
font-size:15px;font-weight:700;color:var(--noir);
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.dossier-step{font-size:12px;color:var(--gris);margin-top:2px;}
.dossier-right{text-align:right;flex-shrink:0;}
.dossier-montant{
font-family:'Bricolage Grotesque',sans-serif;
font-size:16px;font-weight:500;letter-spacing:-.03em;color:var(--noir);
}
.dossier-chevron{color:var(--gris);font-size:16px;font-weight:300;margin-left:6px;opacity:.4;}
/* Group label inside section */
.dossier-group-label{
padding:8px 20px 4px;
font-size:11px;font-weight:700;letter-spacing:.06em;
text-transform:uppercase;color:var(--gris);
background:#f9f9fb;border-top:1px solid #f2f2f7;
}
.dossier-group-label:first-child{border-top:none;background:transparent;}
/* ── TOAST ── */
.toast{
position:fixed;bottom:24px;right:24px;
background:var(--noir);color:#fff;
padding:12px 20px;border-radius:10px;
font-size:14px;font-weight:500;z-index:999;
opacity:0;transform:translateY(8px);
transition:all .2s;pointer-events:none;
box-shadow:var(--sh2);
display:flex;align-items:center;gap:8px;
}
.toast.show{opacity:1;transform:translateY(0);}
.toast-check{color:#84CC16;font-size:16px;}
/* ── RESPONSIVE ── */
/* ── HAMBURGER ── */
.hamburger{display:none;background:none;border:none;cursor:pointer;padding:8px;flex-direction:column;gap:5px;}
.hamburger span{display:block;width:22px;height:2px;background:var(--noir);border-radius:2px;transition:all .2s;}
.nav-mobile-open .nav-links{display:flex !important;flex-direction:column;position:absolute;top:68px;left:0;right:0;background:#fff;border-bottom:1px solid var(--border);padding:16px 24px;gap:16px;box-shadow:var(--sh2);z-index:99;}
.nav-mobile-open .hamburger span:nth-child(1){transform:rotate(45deg) translate(5px,5px);}
.nav-mobile-open .hamburger span:nth-child(2){opacity:0;}
.nav-mobile-open .hamburger span:nth-child(3){transform:rotate(-45deg) translate(5px,-5px);}
@media(max-width:1100px){
.app-hero{grid-template-columns:1fr}
.dash-grid{grid-template-columns:repeat(2,1fr)}
.creance-row{grid-template-columns:1fr 1fr;align-items:flex-start}
.row-cta{grid-column:1/-1;justify-content:flex-start}
}
@media(max-width:900px){
nav{padding:0 16px;}
.nav-links{display:none;}
.hamburger{display:flex;}
.nav-drop-menu{position:static;transform:none;box-shadow:none;border:none;padding:0 0 0 16px;min-width:auto;}
.nav-dropdown.open .nav-drop-menu{display:block;}
.nav-drop-menu a{padding:8px 0;font-size:14px;}
.nav-right .btn-ghost-sm{display:none;}
.securite-grid{grid-template-columns:1fr !important;}
.comparatif-cards{grid-template-columns:1fr 1fr !important;}
.exemple-grid{grid-template-columns:1fr !important;}
.comp-table{grid-template-columns:1fr !important;border-radius:12px !important;}
.comp-table>div{border-right:none !important;border-bottom:1px solid var(--border);}
.comp-table>div:last-child{border-bottom:none !important;}
.comp-row{grid-template-columns:1fr !important;}
.comp-row>div:last-child{padding-left:0 !important;border-left:none !important;padding-top:12px !important;}
.pourquoi-grid{grid-template-columns:1fr !important;}
.pourquoi-grid>div{border-right:none !important;}
.concretement-grid{flex-direction:column !important;}
.concretement-grid>div:last-child{width:100% !important;max-width:100% !important;}
.hero{min-height:auto;flex-direction:column;}
.hero-left{width:100%;max-width:100%;padding:32px 16px 24px;background:rgba(255,255,255,.93) !important;}
.hero-mockup-wrap{position:relative;right:auto;top:auto;transform:none;width:92%;max-width:none;margin:0 auto 16px;}
.hero-bg{display:none !important;}
.hero-scroll{display:none !important;}
.avap-grid,.steps-grid,.temoignages-grid,.outil-wrap,.pricing-grid{grid-template-columns:1fr !important;}
.avap-section,.temoignages-section,.outil-wrap,.pricing-section,.faq-section{padding:40px 16px;}
.cta-section{padding:32px 16px 40px;}
.steps-section{padding:40px 16px;}
.proof-band{padding:20px 16px;}
footer{flex-direction:column;text-align:center;padding:24px 16px;}
.footer-links{justify-content:center;}
/* App mode mobile */
.outil-card-body{padding:12px !important;}
.app-layout{display:block !important}
.app-sidebar{display:none !important}
.app-mobile-topbar{display:flex !important;width:100%}
.app-bottom-tabs{display:flex !important}
.app-main{margin-left:0 !important;padding:12px 14px 88px !important;max-width:100% !important;width:100% !important;box-sizing:border-box !important}
.dash-kpis{grid-template-columns:1fr 1fr !important}
.traiter-grid{grid-template-columns:1fr !important}
.action-card{flex-direction:column;gap:16px}
.action-card-right{align-items:flex-start;width:100%}
.action-card-btns{justify-content:flex-start}
/* Image mobile dashboard */
.mobile-hero-img{display:block !important}
body.app-mode .mobile-hero-img{display:none !important}
.produit-texte{max-width:100% !important}
.produit-img-wrap{display:none !important}
.produit-flex>div:first-child{max-width:100% !important}
/* Outil / gate mobile */
#outil{padding:0 !important}
.outil-wrap{padding:24px 8px !important;display:block !important}
.outil-left{display:none !important}
.outil-card{border-radius:12px !important;border:none !important;box-shadow:none !important}
.outil-card-hd{padding:10px 12px !important}
.outil-card-body{padding:10px !important}
.gate-input{font-size:16px !important}
.fg input,.fg select,.fg textarea{font-size:16px !important}
.profil-row{grid-template-columns:1fr 1fr !important}
/* App mobile extra */
.dash-kpi{padding:14px}
.dash-kpi-value{font-size:22px}
.dossier-mid{display:none}
.app-hero{grid-template-columns:1fr !important}
.hero-panel{padding:20px;border-radius:16px}
.hero-panel h3{font-size:22px}
.hero-panel p{font-size:13px}
.hero-actions{gap:6px}
.btn-soft-light,.btn-soft-dark{padding:10px 14px;font-size:13px;border-radius:8px}
.hero-side{padding:16px;border-radius:16px}
.next-action-card{padding:14px;border-radius:12px}
.next-action-client{font-size:16px}
.next-action-step{font-size:13px}
.action-meta{gap:4px}
.meta-pill{padding:4px 8px;font-size:11px}
.dash-grid{grid-template-columns:1fr !important;gap:10px}
.metric-card{padding:16px;border-radius:14px}
.metric-value{font-size:26px}
.metric-badge{width:36px;height:36px;border-radius:10px;font-size:16px}
.metric-sub{font-size:12px}
.list-panel{padding:14px;border-radius:16px}
.list-title{font-size:18px}
.list-sub{font-size:12px}
.creance-list{gap:10px}
.creance-row{grid-template-columns:1fr !important;gap:12px;padding:14px;border-radius:14px}
.creance-client{font-size:15px}
.creance-amount{font-size:22px}
.creance-col-label{font-size:10px;margin-bottom:4px}
.row-cta{justify-content:flex-start;gap:6px}
.row-btn{padding:8px 12px;font-size:12px;border-radius:8px}
.empty-state{padding:32px 16px 24px}
.empty-state-icon{width:56px;height:56px;border-radius:16px;font-size:26px}
.empty-state h3{font-size:20px}
.empty-state p{font-size:13px}
.dash-row{grid-template-columns:1fr 1fr !important;gap:6px !important;}
.dash-card{padding:12px}
.dash-num{font-size:16px}
.list-head{flex-direction:column;align-items:flex-start;gap:8px}
.form-row{grid-template-columns:1fr !important;}
.profil-row{grid-template-columns:1fr 1fr !important;}
.gen-btn{font-size:14px;padding:12px;border-radius:8px}
table{font-size:11px;}
table th,table td{padding:6px 8px;}
}
@media(max-width:480px){
h1{font-size:32px !important;}
h2{font-size:26px !important;}
.hero-sub{font-size:15px;}
.hero-btns{flex-direction:column;}
.hero-btns .btn-outline{text-align:center;}
.hero-chip{font-size:11px;padding:5px 10px;}
.comparatif-cards{grid-template-columns:1fr !important;}
.comp-table{grid-template-columns:1fr !important;}
.comp-row{grid-template-columns:1fr !important;}
.comp-row>div:last-child{padding-left:0 !important;border-left:none !important;padding-top:10px !important;}
.desktop-only-img{display:none !important;}
.pricing-grid{gap:12px !important;}
.p-price{font-size:42px !important;}
.nav-right .btn-nav{padding:8px 14px;font-size:13px;}
input,select{font-size:16px;}
/* App 480 */
.app-main{padding:10px 8px 88px !important}
.dash-kpi-value{font-size:18px !important}
body.app-mode{overflow-x:hidden !important}
.dash-row{grid-template-columns:1fr !important;}
.profil-row{grid-template-columns:1fr !important;}
}
.desktop-only-img{display:block;}
/* Focus visible global */
*:focus-visible{outline:2px solid var(--vert);outline-offset:2px;border-radius:4px;}
/* Screen reader only */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;}
/* Reduced motion */
@media(prefers-reduced-motion:reduce){*{animation-duration:0.01ms !important;animation-iteration-count:1 !important;transition-duration:0.01ms !important;scroll-behavior:auto !important;}}
/* ── RECOVO ASSISTANT ── */
#recovo-btn{
position:fixed;bottom:80px;right:16px;z-index:1100;
width:56px;height:56px;border-radius:50%;border:none;
background:transparent;cursor:pointer;padding:0;
box-shadow:0 4px 20px rgba(0,0,0,.18),0 0 0 0 rgba(132,204,22,.4);
transition:transform .2s,box-shadow .2s;
animation:recovo-pulse 3s ease-in-out infinite;
}
#recovo-btn:hover{transform:scale(1.08);}
#recovo-btn img{width:56px;height:56px;border-radius:50%;display:block;}
@keyframes recovo-pulse{
0%,100%{box-shadow:0 4px 20px rgba(0,0,0,.18),0 0 0 0 rgba(132,204,22,.35);}
50%{box-shadow:0 4px 20px rgba(0,0,0,.18),0 0 0 8px rgba(132,204,22,.0);}
}
#recovo-panel{
position:fixed;bottom:0;left:0;right:0;z-index:1101;
background:#fff;border-radius:24px 24px 0 0;
box-shadow:0 -8px 40px rgba(0,0,0,.15);
transform:translateY(100%);transition:transform .32s cubic-bezier(.32,1,.23,1);
height:min(520px,75vh);display:flex;flex-direction:column;
max-width:560px;margin:0 auto;
}
#recovo-panel.open{transform:translateY(0);}
#recovo-overlay{
position:fixed;inset:0;z-index:1100;background:rgba(0,0,0,.25);
opacity:0;pointer-events:none;transition:opacity .25s;
}
#recovo-overlay.open{opacity:1;pointer-events:auto;}
#recovo-header{
padding:14px 20px 12px;display:flex;align-items:center;gap:12px;
border-bottom:1px solid #f2f2f7;flex-shrink:0;cursor:grab;
}
#recovo-drag{width:36px;height:4px;background:#e2e8f0;border-radius:2px;
margin:0 auto 10px;flex-shrink:0;}
.recovo-avatar{width:36px;height:36px;border-radius:50%;flex-shrink:0;}
.recovo-name{font-family:'Bricolage Grotesque',sans-serif;font-size:15px;font-weight:500;color:#0d0d14;letter-spacing:-.02em;}
.recovo-status{font-size:11px;color:#84cc16;font-weight:600;display:flex;align-items:center;gap:4px;}
.recovo-status::before{content:'';width:6px;height:6px;border-radius:50%;background:#84cc16;display:inline-block;}
#recovo-close{margin-left:auto;background:none;border:none;font-size:20px;color:#94a3b8;cursor:pointer;padding:4px;line-height:1;}
#recovo-messages{
flex:1;overflow-y:auto;padding:16px 18px;display:flex;
flex-direction:column;gap:12px;scroll-behavior:smooth;
}
.recovo-msg{display:flex;flex-direction:column;max-width:88%;}
.recovo-msg.bot{align-self:flex-start;}
.recovo-msg.user{align-self:flex-end;}
.recovo-bubble{
padding:11px 15px;border-radius:18px;font-size:14px;line-height:1.55;
font-family:'DM Sans',sans-serif;
}
.recovo-msg.bot .recovo-bubble{background:#f2f2f7;color:#0d0d14;border-radius:4px 18px 18px 18px;}
.recovo-msg.user .recovo-bubble{background:#0d0d14;color:#fff;border-radius:18px 18px 4px 18px;}
.recovo-bubble strong{font-weight:700;}
.recovo-time{font-size:10px;color:#94a3b8;margin-top:3px;padding:0 4px;}
.recovo-msg.user .recovo-time{text-align:right;}
.recovo-chips{display:flex;flex-wrap:wrap;gap:6px;padding:0 18px 12px;}
.recovo-chip{
padding:7px 13px;background:#f7fee7;color:#3f6212;border:1.5px solid #d9f99d;
border-radius:20px;font-size:12.5px;font-weight:500;cursor:pointer;
font-family:'DM Sans',sans-serif;transition:background .15s;white-space:nowrap;
}
.recovo-chip:hover,.recovo-chip:active{background:#ecfccb;}
.recovo-typing .recovo-bubble{background:#f2f2f7;padding:13px 18px;}
.recovo-dots{display:flex;gap:4px;align-items:center;}
.recovo-dots span{width:7px;height:7px;border-radius:50%;background:#94a3b8;animation:rdot .9s ease-in-out infinite;}
.recovo-dots span:nth-child(2){animation-delay:.2s;}
.recovo-dots span:nth-child(3){animation-delay:.4s;}
@keyframes rdot{0%,80%,100%{transform:scale(.7);opacity:.4;}40%{transform:scale(1);opacity:1;}}
#recovo-input-zone{
padding:12px 16px 16px;border-top:1px solid #f2f2f7;
display:flex;align-items:center;gap:8px;flex-shrink:0;
}
#recovo-input{
flex:1;border:1.5px solid #e8e8ed;border-radius:22px;
padding:10px 16px;font-size:14px;font-family:'DM Sans',sans-serif;
outline:none;resize:none;background:#fafafa;color:#0d0d14;
transition:border-color .15s;max-height:80px;overflow-y:auto;
line-height:1.4;
}
#recovo-input:focus{border-color:#84cc16;background:#fff;}
#recovo-send{
width:38px;height:38px;border-radius:50%;background:#84cc16;border:none;
cursor:pointer;display:flex;align-items:center;justify-content:center;
transition:background .15s,transform .1s;flex-shrink:0;
touch-action:manipulation;-webkit-touch-callout:none;-webkit-user-select:none;user-select:none;
}
#recovo-send:hover{background:#84CC16;transform:scale(1.05);}
#recovo-send svg{display:block;}
#recovo-mic{
width:38px;height:38px;border-radius:50%;background:#f2f2f7;border:none;
cursor:pointer;display:flex;align-items:center;justify-content:center;
transition:background .15s;flex-shrink:0;
touch-action:manipulation;-webkit-touch-callout:none;
-webkit-user-select:none;user-select:none;
}
#recovo-mic:hover{background:#e8e8ed;}
#recovo-mic.listening{background:#ef4444;animation:mic-pulse .8s ease-in-out infinite;}
@keyframes mic-pulse{0%,100%{transform:scale(1);}50%{transform:scale(1.12);}}
.recovo-speak{
background:none;border:none;cursor:pointer;font-size:13px;color:#94a3b8;
padding:2px 4px;transition:color .15s;
}
.recovo-speak:hover{color:#0d0d14;}
@media(min-width:600px){
#recovo-btn{bottom:28px;right:24px;}
#recovo-panel{
left:auto;right:24px;bottom:100px;width:380px;
border-radius:20px;height:520px;transform:scale(.95) translateY(10px);
opacity:0;pointer-events:none;
transition:transform .25s cubic-bezier(.32,1,.23,1),opacity .2s;
z-index:1101;
}
#recovo-panel.open{transform:scale(1) translateY(0);opacity:1;pointer-events:auto;}
#recovo-drag{display:none;}
#recovo-overlay{display:none !important;}
}
</style>
<script defer data-domain="recov.pro" src="https://plausible.io/js/script.js"></script>
<!-- Protection anti-copie legere -->
<meta name="author" content="RECOV - DEZVOLTA - Pedro Berbel">
<meta name="copyright" content="RECOV 2026 - Tous droits reserves - DEZVOLTA">
</head>
<body>
<!-- Tooltip global centré -->
<div id="g-tip-overlay" onclick="hideGTip()"></div>
<div id="g-tip"></div>
<!-- ── RECOVO ASSISTANT ── -->
<div id="recovo-overlay"></div>
<button id="recovo-btn" aria-label="Ouvrir RECOVO votre assistant" style="display:none" onclick="if(window.Recovo)window.Recovo.toggle()">
<img src="/recovo.png" alt="RECOVO"/>
</button>
<div id="recovo-panel" role="dialog" aria-label="RECOVO assistant" aria-hidden="true">
<div style="padding:12px 20px 0">
<div id="recovo-drag"></div>
</div>
<div id="recovo-header">
<img src="/recovo.png" class="recovo-avatar" alt="RECOVO"/>
<div>
<div class="recovo-name">RECOVO</div>
<div class="recovo-status">En ligne</div>
</div>
<button type="button" id="recovo-mute" data-action="mute" aria-hidden="true" tabindex="-1" style="background:none;border:none;font-size:11px;font-weight:500;color:#94a3b8;cursor:pointer;padding:8px 10px;line-height:1;margin-left:auto;touch-action:manipulation;-webkit-touch-callout:none;-webkit-user-select:none;user-select:none;letter-spacing:.03em;min-width:44px;min-height:44px;display:flex;align-items:center;justify-content:center" title="Activer le son">SON OFF</button>
<button type="button" id="recovo-close" data-action="close" aria-hidden="true" tabindex="-1" style="background:none;border:none;font-size:11px;font-weight:500;color:#94a3b8;cursor:pointer;padding:8px 10px;line-height:1;touch-action:manipulation;-webkit-touch-callout:none;-webkit-user-select:none;user-select:none;letter-spacing:.03em;min-width:44px;min-height:44px;display:flex;align-items:center;justify-content:center">FIN</button>
</div>
<div id="recovo-messages"></div>
<div class="recovo-chips" id="recovo-chips"></div>
<div id="recovo-input-zone">
<textarea id="recovo-input" rows="1" aria-label="Votre question" placeholder="Posez une question…"></textarea>
<button type="button" id="recovo-send" data-action="send" aria-hidden="true" tabindex="-1" style="font-size:13px;font-weight:500;min-width:44px;min-height:44px;display:flex;align-items:center;justify-content:center;touch-action:manipulation;-webkit-touch-callout:none;-webkit-user-select:none;user-select:none">OK</button>
</div>
</div>
<!-- ── NAV ── -->
<a href="#outil" style="position:absolute;left:-9999px;top:0;z-index:999;background:var(--vert);color:#fff;padding:8px 16px;font-size:14px;font-weight:500;border-radius:0 0 8px 0" onfocus="this.style.left='0'" onblur="this.style.left='-9999px'">Aller au contenu principal</a>
<header style="position:sticky;top:0;z-index:100">
<nav role="navigation" aria-label="Navigation principale">
<a class="logo-wrap" href="https://recov.pro/" style="display:inline-flex;align-items:center;gap:4px">
<img src="logoRecovR.png" alt="RECOV™" style="height:30px;width:auto">
<sup style="font-size:10px;font-weight:700;color:var(--gris2);margin-top:-8px;letter-spacing:.02em" title="Marque DEZVOLTA en cours de dépôt INPI">™</sup>
</a>
<button class="hamburger" onclick="document.querySelector('nav').classList.toggle('nav-mobile-open')" aria-label="Menu">
<span></span><span></span><span></span>
</button>
<div class="nav-links">
<div class="nav-dropdown">
<button class="nav-lnk nav-drop-btn" onclick="this.parentElement.classList.toggle('open')">RECOV ▾</button>
<div class="nav-drop-menu">
<a href="/" style="font-weight:600">Vue d'ensemble</a>
<a href="/comment.html">Comment ça marche</a>
<a href="/#pricing">Solo gratuit</a>
<a href="/#faq">FAQ</a>
</div>
</div>
<div class="nav-dropdown">
<button class="nav-lnk nav-drop-btn" onclick="this.parentElement.classList.toggle('open')" style="color:var(--vert);font-weight:500">RECOVMAX ▾</button>
<div class="nav-drop-menu">
<a href="/recovmax.html" style="font-weight:600">Vue d'ensemble</a>
<a href="/recovmax.html#fonctionnalites">Fonctionnalités</a>
<a href="/recovmax.html#outillage-cabinet">Outillage cabinet</a>
<a href="/recovmax.html#import">Import & minimisation</a>
<a href="/recovmax.html#apercu">Aperçu de l'app</a>
<a href="/recovmax.html#comparatif">Comparatif amiable / cabinet</a>
<a href="/recovmax.html#pricing">Tarif fondateur 19 €</a>
<a href="/recovmax.html#securite">Sécurité & conformité</a>
<a href="/plaquette-recovmax-v2.html" target="_blank" rel="noopener" style="color:var(--vert);font-weight:500">📄 Plaquette RECOVMAX (PDF)</a>
<a href="/sous-traitants.html">Sous-traitants RGPD</a>
<a href="/securite-audit.html">Audit sécurité</a>
<a href="/cgu-recovmax.html">CGU RECOVMAX</a>
<a href="/dpa-recovmax.html">DPA RECOVMAX</a>
<a href="/roadmap-recovmax.html">Roadmap produit</a>
<a href="/recovmax.html#faq">FAQ</a>
</div>
</div>
<div class="nav-dropdown">
<button class="nav-lnk nav-drop-btn" onclick="this.parentElement.classList.toggle('open')">Outils ▾</button>
<div class="nav-drop-menu">
<a href="score-freelance.html">Score Freelance</a>
<a href="quiz-impayes.html">Quiz impayés</a>
<a href="calculateur-penalites.html">Calculateur pénalités</a>
<a href="calculateur-tjm-freelance.html">Calculateur TJM</a>
<a href="guide-pdf.html">Guide relance PDF</a>
<a href="devis.html" id="navDevisLink" style="color:var(--gris);font-size:12px">Générateur devis</a>
<a href="facture.html" id="navFactureLink" style="color:var(--gris);font-size:12px">Générateur facture (indicatif)</a>
<script>if(new Date()>=new Date('2026-09-01')){var fl=document.getElementById('navFactureLink');if(fl)fl.textContent='Modèle facture PDF';}</script>
<a href="space-recovery.html" style="color:#67c80f;font-weight:700">Space Recovery</a>
</div>
</div>
<div class="nav-dropdown">
<button class="nav-lnk nav-drop-btn" onclick="this.parentElement.classList.toggle('open')">Blog ▾</button>
<div class="nav-drop-menu">
<a href="blog.html" style="font-weight:600">Tous les articles</a>
<div style="padding:8px 18px 2px;font-size:10px;font-weight:700;color:var(--vert);text-transform:uppercase;letter-spacing:.06em">Situations courantes</div>
<a href="client-ne-paie-pas.html" style="padding-left:28px">Mon client ne paie pas</a>
<a href="facture-en-retard-30-jours.html" style="padding-left:28px">Facture en retard 30 jours</a>
<a href="relancer-client-sans-braquer.html" style="padding-left:28px">Relancer sans braquer</a>
<a href="avant-cabinet-recouvrement.html" style="padding-left:28px">Avant un cabinet recouvrement</a>
<a href="client-silencieux-facture.html" style="padding-left:28px">Client silencieux</a>
<a href="facture-impayee-auto-entrepreneur.html" style="padding-left:28px">Facture impayée auto-entrepreneur</a>
<div style="padding:8px 18px 2px;font-size:10px;font-weight:700;color:var(--vert);text-transform:uppercase;letter-spacing:.06em">Relance & impayés</div>
<a href="blog-facture-impayee-que-faire.html" style="padding-left:28px">Facture impayée : que faire ?</a>
<a href="blog-modele-email-relance-facture.html" style="padding-left:28px">Modèles email de relance</a>
<a href="blog-mise-en-demeure-modele-guide.html" style="padding-left:28px">Mise en demeure : guide</a>
<div style="padding:8px 18px 2px;font-size:10px;font-weight:700;color:var(--vert);text-transform:uppercase;letter-spacing:.06em">Juridique & pénalités</div>
<a href="blog-penalites-retard-facture-calcul.html" style="padding-left:28px">Calcul des pénalités</a>
<a href="blog-injonction-payer-procedure.html" style="padding-left:28px">Injonction de payer</a>
<a href="blog-indemnite-forfaitaire-recouvrement-40-euros.html" style="padding-left:28px">Indemnité forfaitaire 40 €</a>
<a href="blog-prescription-facture-impayee.html" style="padding-left:28px">Prescription facture</a>
<div style="padding:8px 18px 2px;font-size:10px;font-weight:700;color:var(--vert);text-transform:uppercase;letter-spacing:.06em">Freelance & gestion</div>
<a href="blog-freelance-impaye-comment-reagir.html" style="padding-left:28px">Freelance impayé</a>
<a href="blog-cgv-freelance-clauses-obligatoires.html" style="padding-left:28px">CGV freelance</a>
<a href="comparatif-logiciel-relance-facture.html" style="padding-left:28px;font-weight:600">Comparatif logiciels relance</a>
<a href="facturation-electronique.html" style="padding-left:28px">Facturation électronique 2026</a>
</div>
</div>
<div class="nav-dropdown">
<button class="nav-lnk nav-drop-btn" onclick="this.parentElement.classList.toggle('open')">Talents ▾</button>
<div class="nav-drop-menu">
<div style="padding:8px 18px 4px;font-size:10px;font-weight:700;color:var(--vert);text-transform:uppercase;letter-spacing:.06em">Missions remote</div>
<a href="missions-remote.html" style="padding-left:28px">Voir les missions</a>
<div style="padding:8px 18px 4px;font-size:10px;font-weight:700;color:var(--vert);text-transform:uppercase;letter-spacing:.06em">Alternance comptable</div>
<a href="alternance-comptable.html" style="padding-left:28px">Espace cabinets comptables</a>
<a href="alternance-comptable-postuler.html" style="padding-left:28px">→ Postuler (je suis alternant·e)</a>
</div>
</div>
<a class="nav-lnk" href="partenaires.html">Partenaires</a>
</div>
<div class="nav-right">
<button class="search-trigger" aria-label="Rechercher sur le site" title="Rechercher (Ctrl+K)" style="background:transparent;border:1px solid var(--border);border-radius:8px;padding:8px 12px;color:var(--noir);cursor:pointer;display:inline-flex;align-items:center;gap:6px;font-family:'DM Sans',sans-serif;font-size:12px;min-height:36px;font-weight:500">
<svg width="14" height="14" viewbox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.35-4.35"/></svg>
<span class="search-kbd-hint" style="display:none" aria-hidden="true">⌘K</span>
</button>
<button class="btn-ghost-sm" onclick="openAuth('login')">Se connecter</button>
<button class="btn-nav" onclick="openAuth('signup')"><span class="btn-nav-long">Générer ma première relance →</span><span class="btn-nav-short">Lancer une relance</span></button>
<!-- lang switcher — tout à droite, masqué mobile -->
<a href="/en/" title="English version" class="lang-btn-en" style="display:inline-flex;align-items:center;gap:4px;text-decoration:none;font-size:11px;font-weight:500;color:var(--gris2);letter-spacing:.04em;padding:4px 2px;border-bottom:1.5px solid var(--border);transition:all .15s;white-space:nowrap;margin-left:4px" onmouseover="this.style.color='var(--noir)';this.style.borderColor='var(--noir)'" onmouseout="this.style.color='var(--gris2)';this.style.borderColor='var(--border)'">
<svg width="16" height="11" viewbox="0 0 16 11" fill="none" style="flex-shrink:0"><rect x=".5" y=".5" width="15" height="10" rx="1.5" stroke="var(--gris3)"/></svg>
EN
</a>
</div>
</nav>
<style>
@media(min-width:900px){.search-kbd-hint{display:inline!important;}}
@media(max-width:900px){.lang-btn-en{display:none!important;}}
</style>
</header>
<main>
<!-- proofBar deplace SOUS le hero (cf. plus bas) -->
<!-- ══════════════════════════════════════════════════════════════
HOME V2 — 7 sections, lecture 30-45s, conversion optimisée
══════════════════════════════════════════════════════════════ -->
<!-- ── 1. HERO ── -->
<section class="hero">
<div class="hero-left">
<!-- H1 universel ("Les" couvre vos factures ET celles de vos clients) -->
<h1 style="font-size:clamp(32px,5.2vw,56px);line-height:1.1;letter-spacing:-.035em;margin-bottom:18px;text-wrap:balance">
Les factures impayées,
<span style="font-weight:800;display:inline-flex;align-items:baseline;gap:0;flex-wrap:nowrap">RECOV <span class="hero-verb-wrap"><span class="hero-verb">analyse</span></span></span>
</h1>
<!-- Sous-titre — nomme les deux cibles, cibles RECOVMAX explicites -->
<p style="font-size:clamp(16px,1.7vw,18px);margin-bottom:24px;font-weight:400;color:var(--gris);line-height:1.65;text-wrap:pretty">
L'outil de relance amiable B2B. Pour les indépendants qui relancent leurs propres factures, et pour les cabinets comptables, secrétaires indépendantes et DAF externalisés qui pilotent celles de plusieurs clients TPE.
</p>
<!-- 2 CTAs : choix porte (principal) + diagnostic (secondaire) -->
<div class="hero-btns" style="display:flex;flex-wrap:wrap;gap:10px">
<button class="btn-main" onclick="document.getElementById('choisir-porte').scrollIntoView({behavior:'smooth'})" style="background:#0d0d14;color:#fff">Voir les 2 solutions →</button>
<button class="btn-main" onclick="document.getElementById('diagnostic').scrollIntoView({behavior:'smooth'})" style="background:transparent;color:var(--noir);border:1.5px solid var(--border);font-weight:500">Tester ma facture gratuitement</button>
</div>
<!-- Reassurance universelle 4 pills -->
<div style="display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-top:24px">
<div style="display:flex;align-items:center;gap:5px;background:var(--bg2);border-radius:20px;padding:5px 12px">
<span style="color:var(--vert);font-weight:700;font-size:12px">✓</span>
<span style="font-size:11px;color:var(--gris2);font-weight:500;letter-spacing:.02em">0 % commission</span>
</div>
<div style="display:flex;align-items:center;gap:5px;background:var(--bg2);border-radius:20px;padding:5px 12px">
<span style="color:var(--vert);font-weight:700;font-size:12px">✓</span>
<span style="font-size:11px;color:var(--gris2);font-weight:500;letter-spacing:.02em">Données UE · RGPD</span>
</div>
<div style="display:flex;align-items:center;gap:5px;background:var(--bg2);border-radius:20px;padding:5px 12px">
<span style="color:var(--vert);font-weight:700;font-size:12px">✓</span>
<span style="font-size:11px;color:var(--gris2);font-weight:500;letter-spacing:.02em">Validation à chaque envoi</span>
</div>
<div style="display:flex;align-items:center;gap:5px;background:var(--bg2);border-radius:20px;padding:5px 12px">
<span style="color:var(--vert);font-weight:700;font-size:12px">✓</span>
<span style="font-size:11px;color:var(--gris2);font-weight:500;letter-spacing:.02em">Édité en France 🇫🇷</span>
</div>
</div>
<style>
/* Petits badges produit au-dessus des titres de section */
.prod-tag{display:inline-flex;align-items:center;gap:5px;font-size:10.5px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;padding:4px 10px;border-radius:4px;margin-bottom:14px}
.prod-tag.recov{background:#ecfccb;color:#4d7c0f;border:1px solid rgba(132,204,22,.3)}
.prod-tag.recovmax{background:#0B0F14;color:#84CC16;border:1px solid rgba(193,255,114,.25)}
.prod-tag.both{background:#f1f5f9;color:#475569;border:1px solid #cbd5e1}
.prod-tag::before{content:'';width:6px;height:6px;border-radius:50%;display:inline-block}
.prod-tag.recov::before{background:#84CC16}
.prod-tag.recovmax::before{background:#84CC16}
.prod-tag.both::before{background:linear-gradient(90deg,#84CC16 0 50%,#0B0F14 50% 100%)}
</style>
</div>
</div>
<script>
(function(){
var verbs=['analyse','calcule','planifie','relance','suit','récupère'];
var i=0;
var wrap=document.querySelector('.hero-verb-wrap');
if(!wrap)return;
var verb=wrap.querySelector('.hero-verb');
if(!verb)return;
// Anim sur un seul span : fade-out -> swap text -> fade-in (plus de juxtaposition possible)
function next(){
verb.style.transition='opacity .18s ease-in, transform .18s ease-in';
verb.style.opacity='0';
verb.style.transform='translateY(-30%)';
setTimeout(function(){
i=(i+1)%verbs.length;
verb.textContent=verbs[i];
verb.style.transition='none';
verb.style.transform='translateY(30%)';
requestAnimationFrame(function(){
requestAnimationFrame(function(){
verb.style.transition='opacity .22s ease-out, transform .22s ease-out';
verb.style.opacity='1';
verb.style.transform='translateY(0)';
});
});
},200);
}
setInterval(next,1400);
})();
</script>
<!-- HERO BG IMAGE — hero.png (cache sur mobile via .hero-bg{display:none}) -->
<div class="hero-bg">
<img src="hero.png" alt="RECOV et RECOVMAX : relance amiable des factures impayées, en solo ou multi-clients" loading="eager" width="1366" height="768">
</div>
<!-- MOCKUP DASHBOARD flottant à droite -->
<!-- Mockup masqué (SEO/fallback) -->
<div class="hero-mockup-wrap"></div>
<!-- Flèche scroll -->
<div class="hero-scroll" onclick="document.getElementById('diagnostic').scrollIntoView({behavior:'smooth'})">
<span class="hero-scroll-label">Découvrir</span>
<div class="hero-scroll-arrow">
<svg width="14" height="14" viewbox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M7 10l5 5 5-5"/></svg>
</div>
</div>
</section>
<!-- ── IMAGE MOBILE retiree par Pedro : hero sans image sur mobile ── -->
<!-- ── BARRE PREUVE SOCIALE — marquee continu des avis utilisateurs RECOV (defilement lent infini, pause au hover) ── -->
<style>
@keyframes proofMarqueeAnim{
from{transform:translateX(0)}
to{transform:translateX(-50%)}
}
#proofBarTrack{
display:flex;
gap:48px;
width:max-content;
will-change:transform;
white-space:nowrap;
align-items:center;
animation:proofMarqueeAnim 50s linear infinite !important;
}
#proofBar:hover #proofBarTrack{animation-play-state:paused !important}
@media(prefers-reduced-motion:reduce){
#proofBarTrack{animation:proofMarqueeAnim 120s linear infinite !important;}
}
</style>
<div id="proofBar" style="display:block;background:transparent;padding:14px 0;overflow:hidden;font-size:13px;color:var(--noir);font-family:'DM Sans',sans-serif;letter-spacing:.01em;border-top:1px solid var(--border);border-bottom:1px solid var(--border);position:relative;cursor:pointer" onclick="document.getElementById('avisScrollSection').scrollIntoView({behavior:'smooth'})">
<!-- Masque dégradés gauche/droite pour fondu doux aux bords -->
<div aria-hidden="true" style="position:absolute;top:0;bottom:0;left:0;width:80px;background:linear-gradient(90deg,#fff 0%,rgba(255,255,255,0) 100%);z-index:2;pointer-events:none"></div>
<div aria-hidden="true" style="position:absolute;top:0;bottom:0;right:0;width:80px;background:linear-gradient(-90deg,#fff 0%,rgba(255,255,255,0) 100%);z-index:2;pointer-events:none"></div>
<!-- Tape defilante (remplie dynamiquement par JS) -->
<div id="proofBarTrack">
<!-- Etat initial avant chargement Supabase — au moins 2 copies pour que le translate -50% marche -->
<span style="display:inline-flex;align-items:center;gap:8px"><span style="color:var(--noir);letter-spacing:2px">★★★★★</span><span style="color:var(--noir)">« Exactement ce qu'il me fallait pour relancer sans stress… »</span><span style="color:var(--gris2);margin:0 6px">·</span><span style="color:var(--gris);font-size:12px">Avis utilisateur RECOV</span></span>
<span style="display:inline-flex;align-items:center;gap:8px"><span style="color:var(--noir);letter-spacing:2px">★★★★★</span><span style="color:var(--noir)">« Exactement ce qu'il me fallait pour relancer sans stress… »</span><span style="color:var(--gris2);margin:0 6px">·</span><span style="color:var(--gris);font-size:12px">Avis utilisateur RECOV</span></span>
</div>
</div>
<!-- ════════════════════════════════════════════════════════════════
BLOC 2 PORTES — Choix par audience (pattern Notion/Slack/Stripe)
════════════════════════════════════════════════════════════════ -->
<section id="choisir-porte" style="background:#fff;padding:clamp(48px,6vw,80px) clamp(16px,4vw,48px);border-bottom:1px solid var(--border)">
<div style="max-width:1080px;margin:0 auto">
<!-- Intro centree -->
<div style="text-align:center;margin-bottom:36px">
<span style="display:inline-block;font-size:11px;font-weight:700;color:var(--vert);letter-spacing:.14em;text-transform:uppercase;margin-bottom:12px">2 solutions, 1 même cadre</span>
<h2 style="font-family:'Bricolage Grotesque',sans-serif;font-size:clamp(26px,3.6vw,38px);font-weight:500;letter-spacing:-.03em;line-height:1.1;margin:0 0 12px;color:var(--noir)">
Choisissez votre <span style="background:#84CC16;padding:0 8px;border-radius:6px">porte d'entrée</span>.
</h2>
<p style="font-size:16px;color:var(--gris);max-width:640px;margin:0 auto;line-height:1.65">
Même outil, même cadre légal, même qualité. Ce qui change : si vous suivez vos propres factures, ou celles de plusieurs clients.
</p>
</div>
<!-- 2 cartes cote a cote -->
<div class="portes-grid" style="display:grid;grid-template-columns:1fr 1fr;gap:18px">
<!-- ─── PORTE 1 : RECOV Solo — pour vos propres impayés ─── -->
<a href="#diagnostic" class="porte-card" style="display:flex;flex-direction:column;background:#fff;border:1px solid var(--border);border-radius:18px;padding:32px 28px;text-decoration:none;color:inherit;transition:transform .15s,box-shadow .15s,border-color .15s;position:relative;overflow:hidden">
<!-- En-tête : label + titre -->
<div style="display:flex;align-items:center;gap:14px;margin-bottom:14px">
<div style="width:48px;height:48px;border-radius:12px;background:#ecfccb;color:#4d7c0f;display:flex;align-items:center;justify-content:center;font-size:22px;flex-shrink:0">●</div>
<div>
<div style="font-size:10.5px;font-weight:700;color:var(--vert);letter-spacing:.14em;text-transform:uppercase">RECOV Solo</div>
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:21px;font-weight:500;color:var(--noir);letter-spacing:-.025em;line-height:1.18;margin-top:3px">Recouvrer <span style="background:#84CC16;padding:0 6px;border-radius:4px">vos propres impayés</span></div>
</div>
</div>
<p style="font-size:14.5px;color:var(--gris);line-height:1.7;margin:0 0 22px">Conçu pour les indépendants, freelances, auto-entrepreneurs et micro-entreprises qui veulent <strong style="color:var(--noir);font-weight:500">relancer leurs propres factures impayées</strong>. Usage personnel, gratuit, sans commission.</p>
<div style="font-size:10px;font-weight:500;color:var(--gris2);letter-spacing:.12em;text-transform:uppercase;margin-bottom:10px">Ce qui est inclus, gratuitement</div>
<ul style="list-style:none;padding:0;margin:0 0 18px;font-size:14px;color:var(--noir);line-height:1.75;flex-grow:1">
<li style="display:flex;align-items:flex-start;gap:10px;margin-bottom:8px"><span style="color:var(--vert);font-weight:500;flex-shrink:0">✓</span><span>Audit de situation, sans création de compte</span></li>
<li style="display:flex;align-items:flex-start;gap:10px;margin-bottom:8px"><span style="color:var(--vert);font-weight:500;flex-shrink:0">✓</span><span>Séquence de relances graduée & pénalités L.441-10 calculées</span></li>
<li style="display:flex;align-items:flex-start;gap:10px;margin-bottom:8px"><span style="color:var(--vert);font-weight:500;flex-shrink:0">✓</span><span>Mise en demeure conforme prête à signature</span></li>
<li style="display:flex;align-items:flex-start;gap:10px"><span style="color:var(--vert);font-weight:500;flex-shrink:0">✓</span><span>Suivi du dossier jusqu'au règlement complet</span></li>
</ul>
<div style="font-size:11px;color:var(--gris);padding:10px 12px;background:var(--bg2);border-radius:8px;margin-bottom:18px;line-height:1.55">
<strong style="color:var(--noir);font-weight:500">Usage personnel uniquement :</strong> 1 entreprise créancière (votre SIREN) · 1 créance active à la fois · 1 mise en demeure par mois.
</div>
<div style="display:flex;align-items:baseline;justify-content:space-between;padding-top:18px;border-top:1px solid var(--border);margin-bottom:18px">
<div>
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:30px;font-weight:500;color:var(--noir);letter-spacing:-.025em;line-height:1">Gratuit <span style="font-size:14px;color:var(--gris);font-weight:500">· sans engagement</span></div>
<div style="font-size:11px;color:var(--gris2);margin-top:3px;letter-spacing:.005em">Pour vos propres factures · 0 % de commission · sans engagement</div>
</div>
</div>
<span style="display:inline-flex;align-items:center;justify-content:center;gap:8px;background:var(--lime,#84CC16);color:var(--noir);font-weight:500;font-size:14.5px;padding:14px 24px;border-radius:10px;letter-spacing:.005em;transition:background .15s">Créer mon compte gratuit <span aria-hidden="true">→</span></span>
<p style="font-size:10.5px;color:var(--gris2);text-align:center;margin-top:8px;line-height:1.45;letter-spacing:.01em">Inscription ouverte en France métropolitaine et Outre-mer.</p>
</a>
<!-- ─── PORTE 2 : RECOVMAX — solution professionnelle multi-clients ─── -->
<a href="/recovmax.html" class="porte-card" style="display:flex;flex-direction:column;background:#0d0d14;border:1px solid #0d0d14;border-radius:18px;padding:32px 28px;text-decoration:none;color:#fff;transition:transform .15s,box-shadow .15s;position:relative;overflow:hidden">
<!-- Badge en haut a droite -->
<span style="position:absolute;top:18px;right:20px;background:#84CC16;color:#0d0d14;font-size:9px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;padding:4px 10px;border-radius:4px">Offre professionnelle</span>
<!-- En-tête : label + titre -->
<div style="display:flex;align-items:center;gap:14px;margin-bottom:14px">
<div style="width:48px;height:48px;border-radius:12px;background:rgba(193,255,114,.18);color:#84CC16;display:flex;align-items:center;justify-content:center;font-size:22px;flex-shrink:0">◆</div>
<div>
<div style="font-size:10.5px;font-weight:700;color:#84CC16;letter-spacing:.14em;text-transform:uppercase">RECOVMAX</div>
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:21px;font-weight:500;color:#fff;letter-spacing:-.025em;line-height:1.18;margin-top:3px">Piloter les créances de <span style="background:#84CC16;color:#0d0d14;padding:0 6px;border-radius:4px">plusieurs clients</span></div>
</div>
</div>
<p style="font-size:14.5px;color:rgba(255,255,255,.78);line-height:1.7;margin:0 0 22px">Conçu pour les professionnels de la gestion administrative et financière — assistantes indépendantes, office managers, cabinets comptables, DAF externalisés — pilotant les créances de plusieurs entités. Abonnement mensuel multi-portefeuilles.</p>
<div style="font-size:10px;font-weight:700;color:rgba(193,255,114,.7);letter-spacing:.12em;text-transform:uppercase;margin-bottom:10px">Capacités avancées</div>
<ul style="list-style:none;padding:0;margin:0 0 18px;font-size:14px;color:#fff;line-height:1.75;flex-grow:1">
<li style="display:flex;align-items:flex-start;gap:10px;margin-bottom:8px"><span style="color:#84CC16;font-weight:500;flex-shrink:0">✓</span><span>Pilotage simultané de 50 portefeuilles clients distincts</span></li>
<li style="display:flex;align-items:flex-start;gap:10px;margin-bottom:8px"><span style="color:#84CC16;font-weight:500;flex-shrink:0">✓</span><span>Mandat formalisé · IBAN du créancier · journal d'audit horodaté</span></li>
<li style="display:flex;align-items:flex-start;gap:10px;margin-bottom:8px"><span style="color:#84CC16;font-weight:500;flex-shrink:0">✓</span><span>Reporting mensuel transmissible au client final, refacturable</span></li>
<li style="display:flex;align-items:flex-start;gap:10px;margin-bottom:8px"><span style="color:#84CC16;font-weight:500;flex-shrink:0">✓</span><span>Dossier consolidé pour transmission au commissaire de justice <span style="opacity:.7">(loi 2026-307)</span></span></li>
<li style="display:flex;align-items:flex-start;gap:10px"><span style="color:#84CC16;font-weight:500;flex-shrink:0">✓</span><span>Tableau de bord consolidé · indicateurs DSO · exports CSV</span></li>
</ul>
<!-- Bonus fondateur — différenciant exclusif visible -->
<div style="margin-bottom:18px;padding:14px 16px;background:rgba(193,255,114,.08);border:1px solid rgba(193,255,114,.22);border-radius:10px">
<div style="font-size:9.5px;font-weight:500;color:#84CC16;letter-spacing:.16em;text-transform:uppercase;margin-bottom:8px">Bonus fondateur — inclus</div>
<div style="display:flex;align-items:flex-start;gap:10px;margin-bottom:6px;font-size:13px;color:#fff;line-height:1.55"><span style="color:#84CC16;flex-shrink:0">★</span><span><strong style="color:#84CC16;font-weight:500">30 min d'onboarding personnalisé</strong> avec Pedro à l'inscription — méthode, mise en route, premières questions.</span></div>
<div style="display:flex;align-items:flex-start;gap:10px;margin-bottom:6px;font-size:13px;color:#fff;line-height:1.55"><span style="color:#84CC16;flex-shrink:0">★</span><span><strong style="color:#84CC16;font-weight:500">Tarif 19 €/mois conservé</strong> tant que l'abonnement reste actif, dans les limites du plan fondateur.</span></div>
<div style="display:flex;align-items:flex-start;gap:10px;font-size:13px;color:#fff;line-height:1.55"><span style="color:#84CC16;flex-shrink:0">★</span><span><strong style="color:#84CC16;font-weight:500">Accès anticipé</strong> aux connecteurs Pennylane & Axonaut (Q3 2026).</span></div>
</div>
<div style="display:flex;align-items:baseline;justify-content:space-between;padding-top:18px;border-top:1px solid rgba(255,255,255,.12);margin-bottom:18px">
<div>
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:30px;font-weight:500;color:#84CC16;letter-spacing:-.025em;line-height:1">19 € <span style="font-size:14px;color:rgba(255,255,255,.6);font-weight:500">/ mois · tarif fondateur</span></div>
<div style="font-size:11px;color:rgba(255,255,255,.55);margin-top:3px;letter-spacing:.005em">29 € /mois après les places fondateurs · 0 % de commission</div>
</div>
</div>
<span style="display:inline-flex;align-items:center;justify-content:center;gap:8px;background:#84CC16;color:#0d0d14;font-weight:500;font-size:14.5px;padding:14px 24px;border-radius:10px;letter-spacing:.005em">Découvrir RECOVMAX <span aria-hidden="true">→</span></span>
</a>
</div>
<style>
.porte-card:hover{transform:translateY(-4px);box-shadow:0 16px 40px rgba(13,13,20,.12)}
.porte-card[href="#diagnostic"]:hover{border-color:var(--vert)}
@media(max-width:780px){.portes-grid{grid-template-columns:1fr!important}}
</style>
</div>
</section>
<!-- ── LOGOS COMPATIBILITÉ — image unique combinée ── -->
<section style="background:#fff;padding:clamp(40px,5vw,64px) clamp(16px,4vw,48px);border-top:1px solid var(--border)">
<div style="max-width:1100px;margin:0 auto;text-align:center">
<div style="font-size:11px;color:var(--gris);text-transform:uppercase;letter-spacing:.16em;font-weight:700;margin-bottom:18px">Lecture des exports compatibles</div>
<img src="/logos.png" alt="Pennylane, Indy, Abby, Tiime, Sellsy, Chorus Pro" loading="lazy" style="display:block;max-width:min(1000px,92%);width:auto;height:auto;margin:0 auto">
<div style="margin-top:14px;font-size:10.5px;color:var(--gris2);letter-spacing:.02em;max-width:580px;margin-left:auto;margin-right:auto;line-height:1.5">
Marques et logos déposés par leurs propriétaires respectifs. RECOV lit les fichiers d'export sans relation commerciale avec ces éditeurs.
</div>
</div>
</section>
<!-- ── MINI-DIAGNOSTIC ── -->
<section id="diagnostic" style="background:var(--bg2);padding:clamp(40px,5vw,72px) clamp(16px,4vw,48px)">
<div style="max-width:680px;margin:0 auto">
<!-- Intro -->
<div style="text-align:center;margin-bottom:32px">
<span class="prod-tag recov">RECOV Solo</span>
<span style="display:inline-block;background:#f0fdf4;color:#15803d;border:1px solid #bbf7d0;border-radius:20px;padding:5px 14px;font-size:12px;font-weight:500;letter-spacing:.04em;text-transform:uppercase;margin-bottom:14px;margin-left:8px">Diagnostic gratuit · 30 secondes</span>
<h2 style="font-family:'Bricolage Grotesque',sans-serif;font-size:clamp(26px,4vw,38px);font-weight:500;letter-spacing:-.03em;line-height:1.1;margin:0 0 10px;color:var(--noir)">Décrivez votre impayé.<br>Obtenez le message adapté.</h2>
<p style="font-size:15px;color:var(--gris);margin:0">Sans créer de compte. Sans engagement.</p>
</div>
<!-- ÉTAPE 1 — Formulaire -->
<div id="diag-step1" style="background:#fff;border-radius:18px;padding:clamp(24px,4vw,36px);box-shadow:0 4px 32px rgba(0,0,0,.08),0 1px 4px rgba(0,0,0,.04)">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px" class="diag-grid">
<div>
<label style="display:block;font-size:12px;font-weight:700;color:var(--gris);letter-spacing:.04em;text-transform:uppercase;margin-bottom:7px">Montant de la facture</label>
<div style="position:relative">
<input id="diag-montant" type="number" min="1" placeholder="Ex : 1 800"
style="width:100%;padding:12px 36px 12px 14px;border:1.5px solid var(--border);border-radius:10px;font-size:17px;font-weight:500;font-family:inherit;outline:none;transition:border-color .15s;-moz-appearance:textfield"
onfocus="this.style.borderColor='#84CC16'" onblur="this.style.borderColor='var(--border)'">
<span style="position:absolute;right:12px;top:50%;transform:translateY(-50%);font-size:14px;font-weight:700;color:var(--gris);pointer-events:none">€</span>
</div>
</div>
<div>
<label style="display:block;font-size:12px;font-weight:700;color:var(--gris);letter-spacing:.04em;text-transform:uppercase;margin-bottom:7px">Retard (en jours)</label>
<input id="diag-jours" type="number" min="1" placeholder="Ex : 18"
style="width:100%;padding:12px 14px;border:1.5px solid var(--border);border-radius:10px;font-size:17px;font-weight:500;font-family:inherit;outline:none;transition:border-color .15s;-moz-appearance:textfield"
onfocus="this.style.borderColor='#84CC16'" onblur="this.style.borderColor='var(--border)'"
onkeydown="if(event.key==='Enter')diagAnalyser()">
</div>
</div>
<div id="diag-error" style="display:none;color:var(--rouge);font-size:13px;margin-bottom:12px;font-weight:600"></div>
<button onclick="diagAnalyser()"
style="width:100%;padding:14px 24px;background:#84CC16;color:#fff;border:none;border-radius:11px;font-size:16px;font-weight:500;cursor:pointer;font-family:inherit;transition:background .15s,transform .1s;display:flex;align-items:center;justify-content:center;gap:8px"
onmouseover="this.style.background='#65a30d'" onmouseout="this.style.background='#84CC16'"
onmousedown="this.style.transform='scale(.98)'" onmouseup="this.style.transform='scale(1)'">
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" viewbox="0 0 24 24"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35" stroke-linecap="round"/></svg>
Analyser ma situation →
</button>
</div>
<!-- ÉTAPE 2 — Résultat -->
<div id="diag-step2" style="display:none;margin-top:0">
<!-- Badge + type relance -->
<div style="background:#fff;border-radius:18px 18px 0 0;padding:24px 32px 20px;border-bottom:1px solid var(--border2);box-shadow:0 -1px 0 rgba(0,0,0,.03)">
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px">
<div>
<div id="diag-badge" style="display:inline-block;border-radius:20px;padding:5px 14px;font-size:12px;font-weight:500;letter-spacing:.04em;text-transform:uppercase;margin-bottom:8px"></div>
<div id="diag-type" style="font-size:20px;font-weight:800;font-family:'Bricolage Grotesque',sans-serif;color:var(--noir);line-height:1.2"></div>
<div id="diag-ton" style="font-size:13px;color:var(--gris);margin-top:4px"></div>
</div>
<div id="diag-montant-display" style="text-align:right">
<div style="font-size:11px;font-weight:600;color:var(--gris);text-transform:uppercase;letter-spacing:.05em">Montant récupérable</div>
<div style="font-size:28px;font-weight:800;color:var(--noir);letter-spacing:-.03em;line-height:1.1"></div>
<div id="diag-penalites" style="font-size:11px;color:#15803d;font-weight:600;margin-top:2px"></div>
</div>
</div>
</div>
<!-- Preview message -->
<div style="background:#fff;border-radius:0;padding:0 32px;position:relative;overflow:hidden">
<div style="padding:20px 0 0;border-top:none">
<div style="font-size:11px;font-weight:700;color:var(--gris);text-transform:uppercase;letter-spacing:.06em;margin-bottom:10px;display:flex;align-items:center;gap:6px">
<svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewbox="0 0 24 24"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m2 8 10 6 10-6"/></svg>
Aperçu du message généré
</div>
<div id="diag-preview" style="font-size:14px;line-height:1.8;color:var(--noir);font-family:'DM Sans',sans-serif;white-space:pre-line;padding-bottom:80px"></div>
<!-- Blur overlay -->
<div style="position:absolute;bottom:0;left:0;right:0;height:120px;background:linear-gradient(to bottom,rgba(255,255,255,0) 0%,rgba(255,255,255,.85) 40%,rgba(255,255,255,1) 100%);pointer-events:none"></div>
</div>
</div>
<!-- CTA -->
<div style="background:#fff;border-radius:0 0 18px 18px;padding:20px 32px 28px;box-shadow:0 4px 32px rgba(0,0,0,.08),0 1px 4px rgba(0,0,0,.04);text-align:center;border-top:1px solid var(--border2)">
<p style="font-size:13px;color:var(--gris);margin:0 0 14px;line-height:1.5">Ce message est adapté à votre situation.<br>Créez votre compte pour le finaliser et l'envoyer.</p>
<button onclick="openAuth('signup')"
style="display:inline-flex;align-items:center;gap:8px;padding:14px 28px;background:#84CC16;color:#fff;border:none;border-radius:11px;font-size:16px;font-weight:500;cursor:pointer;font-family:inherit;transition:background .15s;margin-bottom:10px"
onmouseover="this.style.background='#65a30d'" onmouseout="this.style.background='#84CC16'">
Générer la relance complète →
</button>
<div style="font-size:12px;color:var(--gris2);display:flex;align-items:center;justify-content:center;gap:10px;flex-wrap:wrap">
<span>✓ Gratuit</span>
<span style="color:var(--border)">·</span>
<span>✓ Sans carte bancaire</span>
<span style="color:var(--border)">·</span>
<span onclick="diagReset()" style="cursor:pointer;text-decoration:underline;color:var(--gris)">← Modifier ma situation</span>
</div>
</div>
</div>
</div>
</section>
<style>
@media(max-width:560px){
.diag-grid{grid-template-columns:1fr !important;}
#diag-step2 > div{padding-left:18px !important;padding-right:18px !important;}
}
</style>
<script>
function diagAnalyser() {
const montantEl = document.getElementById('diag-montant');
const joursEl = document.getElementById('diag-jours');
const errEl = document.getElementById('diag-error');
const montant = parseFloat(montantEl.value);
const jours = parseInt(joursEl.value);
if (!montant || montant <= 0 || !jours || jours <= 0) {
errEl.textContent = 'Merci de renseigner le montant et le nombre de jours.';
errEl.style.display = 'block';
return;
}
errEl.style.display = 'none';
const fmt = new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }).format;
const montantStr = fmt(montant);
// Pénalités indicatives (taux BCE actuel ~4,5% + 10pp = 14,5% / 365 * jours)
const tauxAnnuel = 0.145;
const penalites = montant * tauxAnnuel * jours / 365;
const totalStr = fmt(montant + penalites);
let type, ton, badgeText, badgeBg, badgeColor, preview;
if (jours <= 7) {
type = 'Première relance';
ton = 'Ton : neutre et cordial — préserver la relation';
badgeText = '✦ Rappel amiable';
badgeBg = '#dbeafe'; badgeColor = '#1d4ed8';
preview =
`Objet : Rappel — Facture ${montantStr}
Bonjour,
Je me permets de vous rappeler que la facture d'un montant de ${montantStr} est arrivée à échéance il y a ${jours} jour${jours>1?'s':''}. Il s'agit certainement d'un simple oubli de votre part.
Pourriez-vous procéder au règlement dans les prochains jours ? Je reste disponible si vous souhaitez échanger.
Cordialement,
[Votre prénom]`;
} else if (jours <= 30) {
type = 'Deuxième relance';
ton = 'Ton : professionnel et ferme — délai à préciser';
badgeText = '⚠ Relance ferme';
badgeBg = '#fef3c7'; badgeColor = '#92400e';
preview =
`Objet : 2ème relance — Facture ${montantStr} (${jours} jours de retard)
Bonjour,
Sauf erreur de ma part, la facture de ${montantStr} émise le [date] n'a toujours pas été réglée, alors que l'échéance est dépassée depuis ${jours} jours.
Je vous demande de bien vouloir régulariser cette situation avant le [date + 5 jours]. Passé ce délai, je me verrai contraint d'appliquer les pénalités de retard prévues par la loi.
Cordialement,
[Votre prénom]`;
} else if (jours <= 60) {
type = 'Relance avec pénalités légales';
ton = 'Ton : formel — pénalités calculées + délai final';
badgeText = '⚡ Pénalités applicables';
badgeBg = '#fee2e2'; badgeColor = '#991b1b';
preview =
`Objet : RELANCE URGENTE — Facture ${montantStr} + pénalités de retard
Bonjour,
Malgré mes relances précédentes, la facture de ${montantStr} demeure impayée depuis ${jours} jours. Conformément à l'article L. 441-10 du Code de commerce, des pénalités de retard s'appliquent de plein droit.
Montant principal : ${montantStr}
Pénalités calculées : ${fmt(penalites)} (taux légal en vigueur)
Total dû au ${new Date().toLocaleDateString('fr-FR')} : ${totalStr}
Je vous accorde un dernier délai de 8 jours pour régulariser...`;
} else {
type = 'Mise en demeure';
ton = 'Ton : juridique — dernière étape avant procédure';
badgeText = '🔴 Avant procédure';
badgeBg = '#f3e8ff'; badgeColor = '#6d28d9';
preview =
`Objet : MISE EN DEMEURE — ${montantStr} dû depuis ${jours} jours
Madame, Monsieur,
Par la présente, je vous mets en demeure de procéder au règlement immédiat de la somme de ${totalStr} (${montantStr} + ${fmt(penalites)} de pénalités légales), correspondant à la facture émise il y a ${jours} jours.
À défaut de règlement sous 8 jours à compter de la réception de ce courrier, je me réserve le droit de...`;
}
// Affichage
document.getElementById('diag-badge').textContent = badgeText;
document.getElementById('diag-badge').style.background = badgeBg;
document.getElementById('diag-badge').style.color = badgeColor;
document.getElementById('diag-type').textContent = type;
document.getElementById('diag-ton').textContent = ton;
document.querySelector('#diag-montant-display div:nth-child(2)').textContent = montantStr;
document.getElementById('diag-penalites').textContent = jours >= 31 ? `+ ${fmt(penalites)} pénalités légales` : '';
document.getElementById('diag-preview').textContent = preview;
// Transition
document.getElementById('diag-step1').style.borderRadius = '18px 18px 0 0';
document.getElementById('diag-step1').style.boxShadow = 'none';
document.getElementById('diag-step1').style.borderBottom = '1px solid var(--border2)';
document.getElementById('diag-step2').style.display = 'block';
document.getElementById('diag-step2').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
function diagReset() {
document.getElementById('diag-step2').style.display = 'none';
document.getElementById('diag-step1').style.borderRadius = '18px';
document.getElementById('diag-step1').style.boxShadow = '0 4px 32px rgba(0,0,0,.08),0 1px 4px rgba(0,0,0,.04)';
document.getElementById('diag-step1').style.borderBottom = 'none';
document.getElementById('diag-montant').focus();
}
</script>
<!-- ── CONVICTION ── -->
<section style="background:var(--noir);padding:clamp(32px,5vw,48px) clamp(16px,4vw,48px);text-align:center">
<div style="max-width:700px;margin:0 auto">
<p style="font-family:'Bricolage Grotesque',sans-serif;font-size:clamp(20px,3vw,28px);font-weight:500;color:#fff;letter-spacing:-.02em;line-height:1.4;margin:0 0 10px">Dans une grande partie des retards se règle encore en phase amiable quand les relances sont rapides, tracées et progressives.</p>
<p style="font-size:15px;color:rgba(255,255,255,.5);line-height:1.6;margin-bottom:24px">Le problème : elle n'est pas faite, ou elle est mal faite. RECOV s'en charge.</p>
<div style="display:flex;flex-wrap:wrap;justify-content:center;gap:8px;align-items:center">
<span style="font-size:12px;color:rgba(255,255,255,.4);letter-spacing:.08em;text-transform:uppercase;margin-right:8px">Compatible</span>
<span style="font-size:12px;color:rgba(255,255,255,.6);background:rgba(255,255,255,.08);border-radius:6px;padding:5px 12px">PDF</span>
<span style="font-size:12px;color:rgba(255,255,255,.6);background:rgba(255,255,255,.08);border-radius:6px;padding:5px 12px">Papier</span>
<span style="font-size:12px;color:rgba(255,255,255,.6);background:rgba(255,255,255,.08);border-radius:6px;padding:5px 12px">Factur-X</span>
<span style="font-size:12px;color:rgba(255,255,255,.6);background:rgba(255,255,255,.08);border-radius:6px;padding:5px 12px">Électronique 2026</span>
<span style="font-size:12px;color:rgba(255,255,255,.6);background:rgba(255,255,255,.08);border-radius:6px;padding:5px 12px">Chorus Pro</span>
</div>
<!-- Présences externes (retirées en attendant des avis vérifiables) -->
<div style="display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:14px;margin-top:28px;padding-top:20px;border-top:1px solid rgba(255,255,255,.1);font-size:11px;color:rgba(255,255,255,.55);line-height:1.55">
<span>Programme Partenaires Fondateurs RECOVMAX — candidatures ouvertes.</span>
<a href="/partenaires-fondateurs.html" style="color:#84CC16;text-decoration:underline;font-weight:600">Découvrir le programme →</a>
</div>
</div>
</section>
<!-- ── 2. PREUVE MARCHÉ (fond vert clair) ── -->
<section style="background:linear-gradient(180deg,#84CC16 0%,#84CC16 100%);padding:clamp(48px,6vw,72px) clamp(16px,4vw,48px) clamp(48px,6vw,72px);overflow:hidden">
<div style="max-width:800px;margin:0 auto">
<h2 style="text-align:center;margin:0 0 48px;padding:0;font-size:clamp(32px,6.4vw,67px);color:#0B0F14;letter-spacing:-.05em;line-height:1.05">85% des freelances subissent des retards de paiement<br>Le problème, c'est de ne pas relancer — RECOV s'en charge.</h2>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:16px" class="exemple-grid">
<div style="text-align:center;padding:20px 12px">
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:clamp(48px,12vw,88px);font-weight:500;color:#0B0F14;letter-spacing:-.06em;line-height:.9">85%</div>
<div style="font-size:14px;color:#14532D;margin-top:16px;font-weight:600">des entreprises subissent<br>des retards</div>
</div>
<div style="text-align:center;padding:12px 8px">
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:clamp(48px,12vw,88px);font-weight:500;color:#0B0F14;letter-spacing:-.06em;line-height:.9">60<span style="font-size:clamp(20px,4vw,36px);color:#14532D">j</span></div>
<div style="font-size:14px;color:#14532D;margin-top:16px;font-weight:600">fenêtre critique<br>pour relancer</div>
</div>
<div style="text-align:center;padding:12px 8px">
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:clamp(48px,12vw,88px);font-weight:500;letter-spacing:-.06em;line-height:.9"><span style="background:#0B0F14;color:#fff;padding:0 8px;border-radius:4px">0%</span></div>
<div style="font-size:14px;color:#14532D;margin-top:16px;font-weight:600">commission<br>prélevée par RECOV</div>
</div>
</div>
<div style="background:#0B0F14;border-radius:14px;padding:clamp(16px,3vw,24px);margin:24px auto 20px;max-width:520px;text-align:center">
<div style="font-size:17px;color:rgba(255,255,255,.78);font-weight:300;line-height:1.6">Un impayé de 3 000 €, c'est</div>
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:clamp(28px,6vw,42px);font-weight:500;color:#84CC16;letter-spacing:-.04em;line-height:1;margin:8px 0">7 jours de travail perdus</div>
<div style="font-size:14px;color:rgba(255,255,255,.5)">à 450 €/jour · <a href="calculateur-tjm-freelance.html" style="color:#84CC16;text-decoration:underline">Calculez le vôtre →</a></div>
</div>
<p style="text-align:center;font-size:16px;color:#14532D;font-weight:700;max-width:480px;margin:0 auto 28px">Dans une grande partie des retards se règle encore en phase amiable quand les relances sont rapides, tracées et progressives.</p>
<p style="text-align:center;font-size:14px;color:#166534;max-width:420px;margin:0 auto 28px;opacity:.8">Une facture non relancée a très peu de chances d'être payée.</p>
<div style="text-align:center"><button class="btn-main" onclick="openAuth('signup')" style="background:#0B0F14;display:inline-block">Générer ma première relance →</button></div>
</div>
</section>
<!-- ── 3. COMMENT ÇA MARCHE (fond noir) ── -->
<section id="comment" style="background:var(--noir);padding:clamp(48px,6vw,72px) clamp(16px,4vw,48px) clamp(48px,6vw,72px);overflow:hidden">
<div style="max-width:800px;margin:0 auto">
<div style="font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#84CC16;margin:0 0 10px;padding:0">Comment ça marche</div>
<h2 style="color:#84CC16;margin:0 0 48px;padding:0;font-size:clamp(40px,8vw,80px);letter-spacing:-.04em;line-height:1.05">Pas besoin de <span style="background:#84CC16;color:#0B0F14;padding:0 10px;border-radius:4px">réfléchir</span></h2>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:20px" class="exemple-grid">
<div style="padding:24px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);border-radius:14px">
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:36px;font-weight:500;color:#84CC16;margin-bottom:8px">1</div>
<div style="font-size:15px;font-weight:700;color:#fff;margin-bottom:6px">Vous ajoutez la facture</div>
<div style="font-size:13px;color:rgba(255,255,255,.6);line-height:1.6">Client, montant, échéance. 30 secondes.</div>
</div>
<div style="padding:24px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);border-radius:14px">
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:36px;font-weight:500;color:#84CC16;margin-bottom:8px">2</div>
<div style="font-size:15px;font-weight:700;color:#fff;margin-bottom:6px">RECOV prépare les relances</div>
<div style="font-size:13px;color:rgba(255,255,255,.6);line-height:1.6">Le bon message, au bon moment, adapté à la situation.</div>
</div>
<div style="padding:24px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);border-radius:14px">
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:36px;font-weight:500;color:#84CC16;margin-bottom:8px">3</div>
<div style="font-size:15px;font-weight:700;color:#fff;margin-bottom:6px">Vous relancez jusqu'au paiement</div>
<div style="font-size:13px;color:rgba(255,255,255,.6);line-height:1.6">Vous validez, vous envoyez. Sans stress, sans improviser.</div>
</div>
</div>
<div style="text-align:center;margin-top:20px;font-size:14px;color:rgba(255,255,255,.4)">Personne ne relance à votre place. RECOV le fait.</div>
<div style="text-align:center;margin-top:12px"><a href="comment.html" style="font-size:13px;color:#84CC16;font-weight:600;text-decoration:none;border-bottom:1px solid rgba(193,255,114,.3)">Voir le parcours détaillé →</a></div>
<!-- Screenshot dashboard réel -->
<div style="margin-top:40px;border-radius:16px;overflow:hidden;border:1px solid rgba(255,255,255,.1);box-shadow:0 20px 60px rgba(0,0,0,.4)">
<img src="screens/dashboard.jpg" alt="Dashboard RECOV — suivi de vos créances et relances" loading="lazy" style="width:100%;display:block">
</div>
</div>
</section>
<!-- ── 4. PRODUIT ── -->
<section style="position:relative;overflow:hidden;padding:clamp(48px,6vw,72px) clamp(16px,4vw,48px) clamp(48px,6vw,72px)">
<div class="produit-img-wrap" style="position:absolute;right:0;top:0;bottom:0;width:45%;pointer-events:none">
<img src="fondmobile.png" alt="" loading="lazy" style="position:absolute;top:0;right:0;height:100%;width:auto;display:block">
</div>
<div style="max-width:800px;margin:0 auto;position:relative;z-index:1">
<div class="produit-texte" style="max-width:55%">
<h2 style="font-size:clamp(40px,8vw,80px);letter-spacing:-.04em;line-height:1.05;margin:0 0 40px;padding:0;color:#0B0F14">Comment relancer une facture impayée avec RECOV</h2>
<p style="font-size:17px;color:var(--gris);line-height:1.7;font-weight:400;margin-bottom:24px">Chaque étape est prévue, planifiée, rédigée avec les bons textes de loi. Vous validez, vous envoyez. RECOV suit le dossier jusqu'au bout.</p>
<div style="display:flex;flex-direction:column;gap:12px">
<div style="display:flex;gap:10px;align-items:flex-start;font-size:15px;color:var(--noir)"><span style="color:var(--vert);font-weight:700;flex-shrink:0">✓</span> Vous savez quoi écrire et quand relancer</div>
<div style="display:flex;gap:10px;align-items:flex-start;font-size:15px;color:var(--noir)"><span style="color:var(--vert);font-weight:700;flex-shrink:0">✓</span> Pénalités et majorations légales calculées automatiquement</div>
<div style="display:flex;gap:10px;align-items:flex-start;font-size:15px;color:var(--noir)"><span style="color:var(--vert);font-weight:700;flex-shrink:0">✓</span> Dossier de mise en demeure prêt si ça ne suffit pas</div>
<div style="display:flex;gap:10px;align-items:flex-start;font-size:15px;color:var(--gris)"><span style="color:var(--gris);font-weight:700;flex-shrink:0;font-size:13px">→</span> <span style="font-size:13px">Si vous devez aller plus loin, vous transmettez un dossier complet à un avocat ou huissier — sans repartir de zéro.</span></div>
</div>
</div>
</div>
</section>
<!-- ── 5. POUR QUI : SUPPRIMEE (couverte par badge hero) ── -->
<!-- ── DIFFÉRENCIATION ── -->
<section style="max-width:800px;margin:0 auto;padding:clamp(48px,6vw,72px) clamp(16px,4vw,48px) clamp(48px,6vw,72px)">
<h2 style="margin:0 0 48px;padding:0;font-size:clamp(40px,8vw,80px);color:var(--noir);letter-spacing:-.04em;line-height:1.05;display:flex;align-items:center;gap:16px;flex-wrap:wrap">Pourquoi <img src="logoRecovR.png" alt="RECOV™" style="height:clamp(48px,8vw,72px);width:auto;display:inline-block;vertical-align:middle;position:relative;top:4px"><sup style="font-size:20px;font-weight:700;color:var(--gris2);margin-left:-6px;margin-top:-32px">™</sup></h2>
<div style="display:flex;flex-direction:column;gap:0">
<div style="display:flex;gap:16px;align-items:flex-start;padding:16px 0;border-bottom:1px solid var(--border)">
<div style="width:40px;height:40px;border-radius:10px;background:var(--bg2);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:18px;color:var(--gris2)">→</div>
<div>
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:17px;font-weight:500;color:var(--noir)">Votre logiciel de facturation</div>
<div style="font-size:14px;color:var(--gris);margin-top:2px">Il crée la facture. Il s'arrête là.</div>
</div>
</div>
<div style="display:flex;gap:16px;align-items:flex-start;padding:16px 0;border-bottom:1px solid var(--border)">
<div style="width:40px;height:40px;border-radius:10px;background:var(--bg2);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:18px;color:var(--gris2)">→</div>
<div>
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:17px;font-weight:500;color:var(--noir)">Passer par un cabinet</div>
<div style="font-size:14px;color:var(--gris);margin-top:2px">Ils récupèrent votre argent. Et gardent 15% au passage.</div>
</div>
</div>
<div style="display:flex;gap:16px;align-items:flex-start;padding:16px 0;border-bottom:1px solid var(--border)">
<div style="width:40px;height:40px;border-radius:10px;background:var(--bg2);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:18px;color:var(--gris2)">→</div>
<div>
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:17px;font-weight:500;color:var(--noir)">Demander à ChatGPT</div>
<div style="font-size:14px;color:var(--gris);margin-top:2px">Un email correct. Mais qui relance dans 10 jours si pas de réponse ?</div>
</div>
</div>
<div style="display:flex;gap:16px;align-items:flex-start;padding:20px 0">
<div style="width:40px;height:40px;border-radius:10px;background:var(--vert);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:18px;color:#fff;font-weight:700">✓</div>
<div>
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:17px;font-weight:500;color:var(--vert)">RECOV<sup style="font-size:9px;font-weight:700">™</sup></div>
<div style="font-size:14px;color:var(--noir);margin-top:2px;font-weight:500">Prend le relais après la facture — structure la relance jusqu'au paiement — 0% de commission</div>
</div>
</div>
</div>
<div style="margin-top:16px">
<a href="comparatif-logiciel-relance-facture.html" style="font-size:14px;color:var(--vert);font-weight:600;text-decoration:none">Voir le comparatif détaillé →</a>
</div>
</section>
<!-- ── 5b. SECTION SUPPRIMEE — DOUBLON COMMENT CA MARCHE deja en section 3 ── -->
<!-- ── CALLOUTS ASSISTANT IA + LIEN PAIEMENT (recuperes du doublon) ── -->
<section style="max-width:900px;margin:0 auto;padding:0 clamp(16px,4vw,48px) clamp(40px,5vw,64px)">
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:clamp(12px,2vw,16px)" class="doc-callouts-grid">
<div style="background:#0B0F14;border-radius:16px;padding:22px 24px">
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:15px;font-weight:500;color:#fff;margin-bottom:6px">Vous doutez du ton ?</div>
<div style="font-size:12px;color:rgba(255,255,255,.55);line-height:1.7">L'assistant IA vous donne la formulation exacte, adaptée à votre relation client. Sans improviser.</div>
<div style="margin-top:12px;display:inline-block;background:#84CC16;color:#0B0F14;font-size:11px;font-weight:500;padding:4px 10px;border-radius:8px">Assistant IA inclus</div>
</div>
<div style="background:#0B0F14;border-radius:16px;padding:22px 24px">
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:15px;font-weight:500;color:#fff;margin-bottom:6px">Lien de paiement</div>
<div style="font-size:12px;color:rgba(255,255,255,.55);line-height:1.7">Stripe, SumUp ou PayPal intégré automatiquement dans chaque relance. Votre client paie en un clic.</div>
<div style="margin-top:12px;display:inline-block;background:#84CC16;color:#0B0F14;font-size:11px;font-weight:500;padding:4px 10px;border-radius:8px">−40% de délai de règlement</div>
</div>
</div>
</section>
<style>
@media(max-width:700px){
.doc-callouts-grid{grid-template-columns:1fr !important}
}
</style>
<!-- ── 6. GARANTIES (fond vert clair — compact, pas disclaimer) ── -->
<section style="background:linear-gradient(180deg,#84CC16 0%,#84CC16 100%);padding:clamp(48px,6vw,72px) clamp(16px,4vw,48px) clamp(48px,6vw,72px);overflow:hidden">
<div style="max-width:800px;margin:0 auto">
<h2 style="text-align:center;margin:0 0 48px;padding:0;font-size:clamp(40px,8vw,80px);color:#0B0F14;letter-spacing:-.04em;line-height:1.05">Ce que vous <span style="background:#0B0F14;color:#84CC16;padding:0 10px;border-radius:4px">gardez</span></h2>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px;text-align:center" class="exemple-grid">
<div style="padding:20px 8px">
<div style="width:44px;height:44px;margin:0 auto 10px;background:#0B0F14;border-radius:10px;display:flex;align-items:center;justify-content:center"><svg width="22" height="22" fill="none" stroke="#84CC16" stroke-width="2" stroke-linecap="round"><rect x="3" y="10" width="16" height="10" rx="2"/><path d="M7 10V7a4 4 0 118 0v3"/></svg></div>
<div style="font-size:15px;font-weight:700;color:#0B0F14;margin-bottom:4px">Le contrôle</div>
<div style="font-size:13px;color:#14532D;line-height:1.5">Chaque message est relu et validé par vous avant envoi</div>
</div>
<div style="padding:20px 8px">
<div style="width:44px;height:44px;margin:0 auto 10px;background:#0B0F14;border-radius:10px;display:flex;align-items:center;justify-content:center"><svg width="22" height="22" fill="none" stroke="#84CC16" stroke-width="2" stroke-linecap="round"><path d="M12 21C7 17 2 13 2 8.5A5.5 5.5 0 0112 5a5.5 5.5 0 0110 3.5C22 13 17 17 12 21z"/></svg></div>
<div style="font-size:15px;font-weight:700;color:#0B0F14;margin-bottom:4px">La relation</div>
<div style="font-size:13px;color:#14532D;line-height:1.5">Relances progressives, sans agressivité inutile</div>
</div>
<div style="padding:20px 8px">
<div style="width:44px;height:44px;margin:0 auto 10px;background:#0B0F14;border-radius:10px;display:flex;align-items:center;justify-content:center"><svg width="22" height="22" fill="none" stroke="#84CC16" stroke-width="2" stroke-linecap="round"><path d="M12 2L3 7v6c0 5 4 8 9 10 5-2 9-5 9-10V7l-9-5z"/></svg></div>
<div style="font-size:15px;font-weight:700;color:#0B0F14;margin-bottom:4px">Vos données</div>
<div style="font-size:13px;color:#14532D;line-height:1.5">Aucune exploitation commerciale, aucune revente</div>
</div>
<div style="padding:20px 8px">
<div style="width:44px;height:44px;margin:0 auto 10px;background:#0B0F14;border-radius:10px;display:flex;align-items:center;justify-content:center"><svg width="22" height="22" fill="none" stroke="#84CC16" stroke-width="2" stroke-linecap="round"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" y1="22" x2="4" y2="15"/></svg></div>
<div style="font-size:15px;font-weight:700;color:#0B0F14;margin-bottom:4px">La conformité</div>
<div style="font-size:13px;color:#14532D;line-height:1.5">Conforme au cadre légal français</div>
</div>
</div>
<p style="text-align:center;font-size:14px;color:#14532D;font-weight:500;margin-top:24px;max-width:500px;margin-left:auto;margin-right:auto">La facturation crée la facture. RECOV récupère le paiement.</p>
<!-- Micro-CTA pricing : raccourci pour visiteurs déjà convaincus -->
<div style="text-align:center;margin-top:28px;padding-top:22px;border-top:1px solid rgba(11,15,20,.15)">
<span style="font-size:13.5px;color:#0B0F14;font-weight:400">RECOV Solo gratuit pour vos propres factures · sans engagement · 0 % commission</span>
<a href="#pricing" style="display:inline-block;margin-left:14px;font-size:13px;color:#0B0F14;font-weight:500;text-decoration:none;border-bottom:1px solid rgba(11,15,20,.4);padding-bottom:1px">Voir les tarifs →</a>
</div>
<p style="text-align:center;font-size:13px;color:#14532D;margin-top:8px;max-width:500px;margin-left:auto;margin-right:auto">Compatible facturation électronique 2026 — mêmes relances, même processus, que votre facture soit papier, PDF ou dématérialisée.</p>
</div>
</section>
<!-- ════════════════════════════════════════════════════════════════ -->
<!-- BLOC RECOVMAX — placé ici (juste avant pricing) pour upsell propre -->
<!-- Le visiteur a compris RECOV Solo, on lui propose la version -->
<!-- pour suivre les créances de plusieurs clients. -->
<!-- ════════════════════════════════════════════════════════════════ -->
<section id="recovmax-tease" style="background:#0B0F14;color:#fff;padding:clamp(48px,5vw,72px) clamp(16px,4vw,48px);overflow:hidden;position:relative;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale">
<!-- Image hero RECOVMAX en fond, ancrée à droite -->
<img src="recovmax_hero.png" alt="" aria-hidden="true" loading="lazy"
style="position:absolute;top:0;right:0;height:100%;width:auto;max-width:none;object-fit:cover;object-position:right center;pointer-events:none;z-index:0">
<!-- Voile gradient pour assurer la lisibilité du texte à gauche -->
<div aria-hidden="true" style="position:absolute;inset:0;background:linear-gradient(90deg,#0B0F14 0%,#0B0F14 42%,rgba(11,15,20,.78) 62%,rgba(11,15,20,0) 85%);pointer-events:none;z-index:1"></div>
<div style="max-width:1320px;margin:0 auto;position:relative;z-index:2;display:grid;grid-template-columns:1.7fr 1fr;gap:48px;align-items:center">
<!-- Texte -->
<div style="max-width:820px">
<span style="display:inline-block;background:rgba(193,255,114,.12);color:#84CC16;border:1px solid rgba(193,255,114,.3);font-size:11px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;padding:6px 13px;border-radius:5px;margin-bottom:26px">Vous suivez plusieurs clients ?</span>
<h2 style="font-family:'Bricolage Grotesque',sans-serif;font-size:clamp(28px,3.6vw,42px);font-weight:500;letter-spacing:-.04em;line-height:1.1;margin:0 0 20px;color:#fff">
RECOV<span style="color:#84CC16;font-weight:600">max</span> — pour les pros qui suivent les créances de <span style="color:#84CC16;font-weight:600">plusieurs clients</span>.
</h2>
<p style="font-size:16px;color:rgba(255,255,255,.78);line-height:1.65;margin:0 0 24px;max-width:680px;font-weight:400">
Secrétaires indépendantes, comptables solo, assistantes virtuelles, cabinets : ajoutez le suivi des factures impayées de vos clients à votre prestation.
<span style="display:inline-block;margin-top:8px;color:#84CC16;font-weight:500">Prestation souvent valorisable entre 50 et 80 € HT/mois par client suivi, selon volume et niveau d'accompagnement.</span>
</p>
<ul style="list-style:none;padding:0;margin:0 0 26px;font-size:14.5px;color:rgba(255,255,255,.85);line-height:1.55">
<li style="display:flex;align-items:flex-start;gap:12px;padding:6px 0;border-bottom:1px solid rgba(255,255,255,.08)">
<span style="color:#84CC16;font-weight:500;font-size:15px;flex-shrink:0;margin-top:1px">→</span>
<span>Multi-tenant — un dashboard, plusieurs clients suivis cloisonnés (RGPD)</span>
</li>
<li style="display:flex;align-items:flex-start;gap:12px;padding:6px 0;border-bottom:1px solid rgba(255,255,255,.08)">
<span style="color:#84CC16;font-weight:500;font-size:15px;flex-shrink:0;margin-top:1px">→</span>
<span>Mandat de gestion + score santé /100 + rapport mensuel PDF par client</span>
</li>
<li style="display:flex;align-items:flex-start;gap:12px;padding:6px 0">
<span style="color:#84CC16;font-weight:500;font-size:15px;flex-shrink:0;margin-top:1px">→</span>
<span>Pénalités L441-10 calculées · mise en demeure · dossier structuré pour faciliter la transmission si la créance entre dans le cadre applicable (loi 2026-307)</span>
</li>
</ul>
<div style="display:flex;gap:16px;flex-wrap:wrap;align-items:center;margin-bottom:24px">
<a href="/recovmax.html" style="display:inline-flex;align-items:center;gap:10px;background:#84CC16;color:#0B0F14;font-weight:500;font-size:15px;padding:16px 28px;border-radius:10px;text-decoration:none;letter-spacing:-.005em;box-shadow:0 8px 24px rgba(193,255,114,.25);transition:transform .15s">
Découvrir RECOVMAX <span style="font-size:18px">→</span>
</a>
<span style="font-size:13.5px;color:rgba(255,255,255,.65);font-weight:400">19 € / mois fondateur · <span data-pricing="founders-remaining" style="color:#84CC16;font-weight:500">100</span> places restantes</span>
</div>
<div style="padding:14px 20px;background:rgba(11,15,20,.55);border-left:3px solid #84CC16;border-radius:0 10px 10px 0;backdrop-filter:blur(8px);max-width:680px">
<div style="font-size:11px;color:#84CC16;font-weight:600;letter-spacing:.12em;text-transform:uppercase;margin-bottom:8px">Scénario Sophie</div>
<div style="font-size:14.5px;color:rgba(255,255,255,.85);line-height:1.65;font-weight:400">
12 clients × 60 € HT = <span style="color:#fff;font-weight:500">720 € / mois facturés</span>. RECOVMAX = 19 €. <span style="color:#84CC16;font-weight:500">Marge nette : 701 € / mois récurrents.</span>
</div>
</div>
</div>
<div></div>
</div>
<style>
@media(max-width:780px){
#recovmax-tease > img{display:none}
#recovmax-tease > div:last-of-type{grid-template-columns:1fr !important;gap:32px}
#recovmax-tease{min-height:auto!important;background:linear-gradient(135deg,#0B0F14 0%,#141a2d 100%)!important}
#recovmax-tease h2{font-size:30px!important}
}
</style>
</section>
<!-- ── 7. PRICING (fond blanc) ── -->
<section id="pricing" style="max-width:980px;margin:0 auto;padding:clamp(48px,6vw,80px) clamp(16px,4vw,48px)">
<h2 style="text-align:center;margin:0 0 10px;padding:0;font-size:clamp(30px,5vw,52px);letter-spacing:-.04em;line-height:1.05;color:var(--noir)">Simple.<br>Sans commission.</h2>
<p style="text-align:center;font-size:15px;color:var(--gris);margin:0 0 40px">Pas de pourcentage prélevé. Pas de frais cachés. Vous payez l'outil, pas le résultat.<br><span style="font-size:13px;color:var(--gris2)">Une offre par usage : freelance solo ou cabinet multi-clients.</span></p>
<!-- Comparatif marché -->
<div class="comp-table" style="display:grid;grid-template-columns:repeat(3,1fr);gap:0;border:1px solid var(--border);border-radius:14px;overflow:hidden;max-width:680px;margin:0 auto 52px">
<div style="padding:18px 20px;border-right:1px solid var(--border);background:var(--bg2)">
<div style="font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--gris);margin-bottom:6px">Cabinet de recouvrement</div>
<div style="font-size:22px;font-weight:800;color:var(--rouge);letter-spacing:-.03em">15 – 30%</div>
<div style="font-size:12px;color:var(--gris);margin-top:3px">de chaque facture récupérée</div>
</div>
<div style="padding:18px 20px;border-right:1px solid var(--border);background:var(--bg2)">
<div style="font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--gris);margin-bottom:6px">Avocat recouvrement</div>
<div style="font-size:22px;font-weight:800;color:var(--rouge);letter-spacing:-.03em">150 – 400 €</div>
<div style="font-size:12px;color:var(--gris);margin-top:3px">de l'heure, hors frais de justice</div>
</div>
<div style="padding:18px 20px;background:#f0fdf4">
<div style="font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--vert2);margin-bottom:6px">RECOV & RECOVMAX</div>
<div style="font-size:22px;font-weight:800;color:var(--vert2);letter-spacing:-.03em">0% de commission</div>
<div style="font-size:12px;color:var(--vert2);margin-top:3px">Sur les deux offres — vous gardez 100% de ce que vous récupérez pour vos clients</div>
</div>
</div>
<!-- Grille tarifs — 2 offres claires, sans ambiguïté (refonte 2026-05-17) -->
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:18px;max-width:840px;margin:0 auto" class="exemple-grid">
<!-- RECOV Solo — toujours gratuit, usage personnel -->
<div class="p-card" style="border:1.5px solid var(--border);background:#fff">
<div class="p-badge" style="background:var(--vert3);color:var(--vert2)">RECOV Solo · usage personnel</div>
<div class="p-name" style="display:flex;align-items:center;justify-content:flex-start;min-height:92px;margin-bottom:8px">RECOV Solo</div>
<div class="p-price">Gratuit</div>
<div class="p-period">Toujours gratuit · 0 % de commission · sans carte bancaire</div>
<ul class="p-feats">
<li>Séquence de relance complète (8 étapes graduées)</li>
<li>Pénalités L.441-10 calculées automatiquement</li>
<li>Mise en demeure PDF prête à signer (1 par mois)</li>
<li>Suivi du dossier jusqu'au règlement complet</li>
<li>Hébergement Union européenne · RGPD natif</li>
</ul>
<div style="font-size:11px;color:var(--gris);padding:10px 12px;background:var(--bg2);border-radius:8px;margin:14px 0;line-height:1.55">
<strong style="color:var(--noir);font-weight:500">Usage personnel uniquement :</strong> 1 entreprise créancière (votre SIREN) · 1 créance active à la fois · 1 mise en demeure par mois. Vous pilotez plusieurs clients ? <a href="/recovmax.html" style="color:var(--vert);text-decoration:underline;text-underline-offset:2px">Voir RECOVMAX</a>.
</div>
<button class="p-cta solid" onclick="openAuth('signup')">Commencer gratuitement →</button>
<p style="text-align:center;font-size:11px;color:var(--gris2);margin-top:10px">Sans engagement · Sans carte bancaire</p>
</div>
<!-- RECOVMAX — 19€/mois fondateur, multi-clients (variante dark + halo lime subtil) -->
<div class="p-card dark" style="background:radial-gradient(ellipse 75% 45% at 50% 0%,rgba(132,204,22,.12) 0%,rgba(132,204,22,.03) 35%,transparent 60%),#0d0d14;border:1.5px solid #0d0d14;position:relative">
<div class="p-badge" style="background:#84CC16;color:#0d0d14">Pour cabinets & pros multi-clients</div>
<div class="p-name" style="display:flex;align-items:center;justify-content:center;min-height:92px;margin-bottom:8px;position:relative;z-index:1"><img src="logoRecovMax.png" alt="RECOVMAX" style="height:76px;width:auto;display:block;filter:brightness(0) invert(1)"></div>
<div class="p-price" style="color:#84CC16">19<sup>€</sup></div>
<div class="p-period">/ mois · tarif fondateur conservé tant que l'abonnement reste actif</div>
<ul class="p-feats">
<li>Multi-clients — jusqu'à 50 portefeuilles suivis</li>
<li>Jusqu'à 50 créances actives simultanées · 1 000 relances/mois</li>
<li>Reporting mensuel par client (PDF refacturable)</li>
<li>Traçabilité structurée · audit trail horodaté et exploitable</li>
<li>Mise en demeure et procédure simplifiée 2026-307</li>
</ul>
<div style="font-size:12px;padding:12px 14px;background:rgba(193,255,114,.10);border:1px solid rgba(193,255,114,.32);border-radius:8px;margin:14px 0;line-height:1.6;color:#fff">
<strong style="color:#84CC16;font-weight:500;display:block;margin-bottom:6px;letter-spacing:.02em">★ Bonus fondateur inclus</strong>
<span style="color:rgba(255,255,255,.92)">30 min d'onboarding avec Pedro · Tarif 19 €/mois conservé tant que l'abonnement reste actif · Accès anticipé connecteurs Pennylane & Axonaut (Q3 2026).</span>
</div>
<a href="/recovmax.html" class="p-cta solid" style="background:#84CC16;color:#0d0d14;text-decoration:none;display:block;text-align:center">Découvrir RECOVMAX →</a>
<p style="text-align:center;font-size:11px;color:rgba(255,255,255,.6);margin-top:10px">Secrétaires indé · Office managers · Comptables solo · DAF externalisés</p>
</div>
</div>
<!-- Note garantie & cadre -->
<div style="margin-top:32px;padding:16px 20px;background:var(--bg2);border-radius:10px;max-width:680px;margin-left:auto;margin-right:auto;text-align:center">
<p style="font-size:13px;color:var(--gris);margin:0;line-height:1.7">
🔒 <strong style="color:var(--noir);font-weight:500">RECOV Solo est toujours gratuit.</strong> RECOVMAX est sans engagement (résiliable à tout moment, vos créances en cours restent accessibles jusqu'à leur résolution).<br>
<span style="font-size:12px">RECOV n'est pas un cabinet de recouvrement. Vous gardez le contrôle de chaque relance, le paiement va sur l'IBAN du créancier — RECOV ne touche jamais l'argent.</span>
</p>
</div>
</section>
<!-- ── OUTIL (formulaire inscription/connexion) ── -->
<section id="outil" style="position:relative;overflow:hidden;background:#fff">
<!-- Icône RECOV en fond -->
<div style="position:absolute;right:-5%;top:50%;transform:translateY(-50%);width:clamp(300px,45vw,500px);pointer-events:none;opacity:.15">
<img src="iconRecov.png" alt="" style="width:100%;display:block" loading="lazy">
</div>
<div class="outil-wrap" style="position:relative;border:none;box-shadow:none">
<div class="outil-left">
<h3 style="font-family:'Bricolage Grotesque',sans-serif;font-size:clamp(28px,5vw,38px);font-weight:500;letter-spacing:-.03em;line-height:1.15">Un client ne paie pas ?<br><span style="background:#84CC16;color:#fff;padding:0 10px;border-radius:4px">Relancez maintenant.</span></h3>
<p style="font-size:15px;color:var(--gris);margin-top:10px">30 secondes. Gratuit. Sans carte bancaire.</p>
<p style="font-size:11px;color:var(--gris2);margin-top:4px;letter-spacing:.01em">Inscription ouverte aux professionnels établis en France métropolitaine et Outre-mer (droit français applicable).</p>
</div>
<div class="outil-card">
<div class="outil-card-hd">
<div class="outil-card-title">RECOV — Votre système pour être payé</div>
<div class="places-badge">
<div class="places-dot"></div>
<span class="places-txt" id="placesTxt">Dernières places</span>
</div>
</div>
<div class="outil-card-body">
<!-- GATE -->
<div id="gate">
<div style="display:inline-flex;align-items:center;gap:6px;background:#dbeafe;color:#1d4ed8;font-size:11px;font-weight:500;letter-spacing:.06em;text-transform:uppercase;padding:5px 11px;border-radius:6px;margin-bottom:10px">
<span style="width:6px;height:6px;border-radius:50%;background:#3b82f6"></span>Pour freelances & entrepreneurs
</div>
<div class="gate-title">Votre 1ère relance en 30 secondes — gratuit</div>
<div class="gate-sub">Séquence complète planifiée, pénalités calculées, mise en demeure prête.</div>
<div style="margin-top:10px;padding:12px 14px;background:#f8f8fa;border:1px solid #e8e8ed;border-radius:10px;font-size:12.5px;color:var(--gris);line-height:1.6">
<strong style="color:var(--noir);font-weight:500;display:block;margin-bottom:6px">Vous relancez pour qui ?</strong>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:6px">
<span style="padding:4px 10px;background:var(--vert3);color:var(--vert2);border:1px solid var(--vert4);border-radius:6px;font-size:11.5px;font-weight:500">✓ Mes propres factures (RECOV Solo)</span>
<a href="/recovmax.html" style="padding:4px 10px;background:#0d0d14;color:#fff;border-radius:6px;font-size:11.5px;font-weight:500;text-decoration:none">Plusieurs clients → RECOVMAX</a>
</div>
<div style="font-size:11.5px;color:var(--gris2);margin-top:8px;line-height:1.5">Cette inscription est <strong style="color:var(--noir);font-weight:500">RECOV Solo gratuit</strong> — réservée aux freelances et indépendants qui suivent leurs propres factures (1 SIREN, 1 créance active, 1 mise en demeure par mois).</div>
</div>
<form class="gate-form" id="gateFormSignup" onsubmit="event.preventDefault();gateSignupStep().catch(function(e){showToast('Une erreur est survenue. Réessayez.');console.error(e.message);console.error(e);})">
<button type="button" id="btnGoogleAuth" onclick="signInWithGoogle()" style="display:flex;align-items:center;justify-content:center;gap:10px;width:100%;padding:12px;background:#fff;border:1.5px solid #dadce0;border-radius:9px;font-family:inherit;font-size:15px;font-weight:500;color:#3c4043;cursor:pointer;margin-bottom:14px;transition:border-color .15s,box-shadow .15s" onmouseover="this.style.borderColor='#4285f4';this.style.boxShadow='0 1px 3px rgba(66,133,244,.25)'" onmouseout="this.style.borderColor='#dadce0';this.style.boxShadow='none'"><img src="https://www.gstatic.com/firebasejs/ui/2.0.0/images/auth/google.svg" width="20" height="20" alt="G" style="flex-shrink:0">Continuer avec Google</button>
<div style="text-align:center;margin-bottom:12px;font-size:12px;color:var(--gris)">— ou par email —</div>
<input class="gate-input" type="email" id="gateEmail" placeholder="prenom@votresociete.fr" autocomplete="email"/>
<div id="gateStep2" style="display:none;margin-top:8px">
<div style="position:relative"><input class="gate-input" type="password" id="gatePassword" placeholder="Choisissez un mot de passe (6 car. min)" autocomplete="new-password"/><button type="button" onclick="togglePwd('gatePassword',this)" style="position:absolute;right:10px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;font-size:16px;color:var(--gris2);padding:4px" title="Afficher le mot de passe">👁</button></div>
<input class="gate-input" type="text" id="gateSiren" placeholder="SIREN de votre entreprise (optionnel — 9 chiffres)" inputmode="numeric" pattern="\d{9}" maxlength="9" autocomplete="off" style="margin-top:8px"/>
<div style="font-size:11px;color:var(--gris2);margin-top:4px;line-height:1.4">Votre propre SIREN (le créancier des factures que vous suivrez). Identifie votre compte Solo de manière unique — peut être renseigné plus tard dans votre profil.</div>
</div>
<!-- Honeypot anti-bot — invisible pour les humains -->
<input type="text" id="gateWebsite" name="website" autocomplete="off" tabindex="-1" style="position:absolute;left:-9999px;opacity:0;height:0;width:0;overflow:hidden"/>
<div id="gateCguWrap" style="display:none;margin-top:12px;padding:10px 12px;background:var(--bg2);border:1px solid var(--border);border-radius:8px">
<label style="display:flex;gap:10px;align-items:flex-start;font-size:13px;color:var(--noir);cursor:pointer;line-height:1.6">
<input type="checkbox" id="gateCgu" style="margin-top:4px;flex-shrink:0;accent-color:var(--vert);width:18px;height:18px">
<span>J'accepte les <a href="cgu.html" target="_blank" style="color:var(--vert);text-decoration:underline">CGU</a> et j'ai pris connaissance de la <a href="confidentialite.html" target="_blank" style="color:var(--vert);text-decoration:underline">politique de confidentialité</a></span>
</label>
</div>
<button class="gate-btn" id="gateBtnSignup" type="submit">Créer mon compte gratuit →</button>
<p style="font-size:11px;color:var(--gris2);text-align:center;margin-top:8px;line-height:1.45;letter-spacing:.01em">Inscription ouverte aux professionnels établis en France métropolitaine et Outre-mer (droit français applicable).</p>
<!-- Lien magic link -->
<div style="text-align:center;margin-top:10px">
<button type="button" id="btnMagicLink" onclick="sendMagicLink()" style="font-size:12px;color:var(--vert);background:none;border:none;padding:0;cursor:pointer;font-family:inherit;text-decoration:underline">Recevoir un lien de connexion par email (sans mot de passe)</button>
</div>
<div style="text-align:center;margin-top:8px;font-size:14px;color:var(--gris)">Déjà inscrit ? <button type="button" onclick="openAuth('login', false)" style="color:var(--vert);font-weight:500;font-size:14px;background:none;border:none;padding:0;cursor:pointer;font-family:inherit;text-decoration:underline">Se connecter</button></div>
</form>
<form class="gate-form" id="gateFormLogin" style="display:none" onsubmit="event.preventDefault();loginApp().catch(function(e){showToast('Une erreur est survenue. Réessayez.');console.error(e.message);console.error(e);})">
<button type="button" onclick="signInWithGoogle()" style="display:flex;align-items:center;justify-content:center;gap:10px;width:100%;padding:12px;background:#fff;border:1.5px solid #dadce0;border-radius:9px;font-family:inherit;font-size:15px;font-weight:500;color:#3c4043;cursor:pointer;margin-bottom:14px;transition:border-color .15s,box-shadow .15s" onmouseover="this.style.borderColor='#4285f4';this.style.boxShadow='0 1px 3px rgba(66,133,244,.25)'" onmouseout="this.style.borderColor='#dadce0';this.style.boxShadow='none'"><img src="https://www.gstatic.com/firebasejs/ui/2.0.0/images/auth/google.svg" width="20" height="20" alt="G" style="flex-shrink:0">Continuer avec Google</button>
<div style="text-align:center;margin-bottom:12px;font-size:12px;color:var(--gris)">— ou par email —</div>
<input class="gate-input" type="email" id="loginEmail" placeholder="Votre email" autocomplete="email"/>
<div style="position:relative;margin-top:8px"><input class="gate-input" type="password" id="loginPassword" placeholder="Votre mot de passe" autocomplete="current-password"/><button type="button" onclick="togglePwd('loginPassword',this)" style="position:absolute;right:10px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;font-size:16px;color:var(--gris2);padding:4px" title="Afficher le mot de passe">👁</button></div>
<div id="loginError" style="display:none;margin-top:8px;padding:10px 12px;background:#fef2f2;border:1px solid #fecaca;border-radius:8px;font-size:13px;color:#991b1b"></div>
<button class="gate-btn" type="submit">Se connecter →</button>
<div style="text-align:center;margin-top:8px">
<button type="button" onclick="sendMagicLinkLogin()" style="font-size:12px;color:var(--vert);background:none;border:none;padding:0;cursor:pointer;font-family:inherit;text-decoration:underline">Recevoir un lien de connexion par email</button>
</div>
<button type="button" onclick="openAuth('reset', false)" style="display:block;width:100%;margin-top:8px;padding:10px;background:none;border:1px solid var(--border);border-radius:9px;font-family:inherit;font-size:13px;color:var(--gris);cursor:pointer;transition:all .15s" onmouseover="this.style.borderColor='var(--vert)';this.style.color='var(--vert)'" onmouseout="this.style.borderColor='var(--border)';this.style.color='var(--gris)'">Mot de passe oublié ?</button>
<div style="text-align:center;margin-top:10px;font-size:12px;color:var(--gris)">Pas encore de compte ? <button type="button" onclick="openAuth('signup', false)" style="color:var(--vert);font-weight:500;background:none;border:none;padding:0;cursor:pointer;font:inherit">S'inscrire</button></div>
</form>
<div class="gate-form" id="gateFormReset" style="display:none">
<div style="text-align:center;margin-bottom:16px">
<div style="font-size:36px;margin-bottom:8px">🔑</div>
<div style="font-size:16px;font-weight:700;color:var(--noir)">Mot de passe oublié ?</div>
<div style="font-size:13px;color:var(--gris);margin-top:4px">Entrez votre email et nous vous envoyons un lien de réinitialisation.</div>
</div>
<input class="gate-input" type="email" id="resetEmail" placeholder="Votre adresse email"/>
<div id="resetMsg" style="display:none;margin-top:8px;padding:10px 12px;border-radius:8px;font-size:13px"></div>
<button class="gate-btn" style="margin-top:8px" onclick="doResetPassword()">Envoyer le lien de réinitialisation →</button>
<div style="text-align:center;margin-top:12px;font-size:12px;color:var(--gris)">
<button type="button" onclick="openAuth('login', false)" style="color:var(--vert);font-weight:500;background:none;border:none;padding:0;cursor:pointer;font:inherit">Se connecter</button>
· <button type="button" onclick="openAuth('signup', false)" style="color:var(--gris2);background:none;border:none;padding:0;cursor:pointer;font:inherit">Créer un compte</button>
</div>
</div>
<div class="gate-fine">Sans carte bancaire · <a href="confidentialite.html" style="color:var(--vert)">Politique de confidentialité</a> · <a href="cgu.html" style="color:var(--vert)">CGU</a></div>
<div style="margin-top:14px;padding-top:14px;border-top:1px solid var(--border);text-align:center;font-size:13px;color:var(--gris);line-height:1.6">
Pas encore prêt à créer un compte ? <a href="mailto:pedro.berbel@dezvolta.org?subject=Simulation%20gratuite%20RECOV%20%E2%80%94%20facture%20impay%C3%A9e&body=Bonjour%2C%0A%0AJ%27aimerais%20recevoir%20une%20simulation%20RECOV%20gratuite%20sur%20une%20de%20mes%20factures%20impay%C3%A9es.%0A%0ADonn%C3%A9es%20de%20la%20facture%20%3A%0A-%20Montant%20HT%20%3A%20%0A-%20Date%20d%27%C3%A9ch%C3%A9ance%20%3A%20%0A-%20Mon%20client%20%28d%C3%A9biteur%29%20%3A%20%0A-%20Type%20%3A%20B2B%20%2F%20B2C%0A%0AMerci%2C%0A" style="color:var(--vert);font-weight:600;text-decoration:underline">Demandez une simulation gratuite</a> de votre 1ère facture impayée — réponse sous 24h, sans engagement.
</div>
</div>
<!-- APP -->
<div id="app">
<div class="app-bar">
<div class="app-user" style="display:flex;align-items:center;gap:14px;flex-wrap:wrap">
<span>Bienvenue, </span>
<span id="userPlanBadge" style="display:none;align-items:center;gap:6px;font-size:11px;font-weight:500;letter-spacing:.05em;text-transform:uppercase;padding:4px 12px;border-radius:20px"></span>
</div>
<button class="app-logout" onclick="logout()">Déconnexion</button>
</div>
<div class="dash-row">
<div class="dash-card"><div class="dash-num green" id="dashCash">0 €</div><div class="dash-label">Cash récupéré</div></div>
<div class="dash-card"><div class="dash-num" id="dashSeq">0</div><div class="dash-label">Séquences</div></div>
<div class="dash-card"><div class="dash-num" id="dashJours">7</div><div class="dash-label">Jours restants</div></div>
</div>
<div class="form-row">
<div class="fg"><label>Nom du client</label><input type="text" id="clientNom" placeholder="Cabinet Horizon"/></div>
<div class="fg"><label>Montant (€)</label><input type="number" id="montant" placeholder="4 500"/></div>
<div class="fg"><label>N° de facture</label><input type="text" id="facture" placeholder="FACT-2025-018"/></div>
<div class="fg"><label>Jours de retard</label><input type="number" id="retard" placeholder="21"/></div>
<div class="fg full">
<label>Profil de la relation client</label>
<div class="profil-row">
<button class="profil-btn active" onclick="selectProfil(this,'fidele')"><span class="pb-icon">🤝</span>Client fidèle<br><small style="opacity:.6;font-size:10px">Longue relation</small></button>
<button class="profil-btn" onclick="selectProfil(this,'nouveau')"><span class="pb-icon">🆕</span>Nouveau client<br><small style="opacity:.6;font-size:10px">1ère mission</small></button>
<button class="profil-btn" onclick="selectProfil(this,'risque')"><span class="pb-icon">⚠️</span>Client à risque<br><small style="opacity:.6;font-size:10px">Déjà des retards</small></button>
</div>
</div>
<div class="fg full">
<label>Contexte (optionnel)</label>
<input type="text" id="contexte" placeholder="Ex : mission livrée en avance, DRH interlocuteur…"/>
</div>
</div>
<button class="gen-btn" id="genBtn" onclick="generer()">Générer ma séquence de relance →</button>
<div id="results">
<div class="results-lbl" style="margin-top:16px">Votre séquence de relance</div>
<div id="emailsContainer"></div>
<div class="cash-banner" id="cashWin" style="display:none">
<div class="cash-num" id="cashWinNum">0 €</div>
<div class="cash-txt">en cours de relance. Envoyez le premier email aujourd'hui.</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- ── FONDATEUR ── -->
<section style="max-width:700px;margin:0 auto;padding:clamp(40px,5vw,56px) clamp(16px,4vw,48px)">
<div style="display:flex;gap:20px;align-items:flex-start;flex-wrap:wrap">
<div style="flex-shrink:0;width:56px;height:56px;border-radius:50%;overflow:hidden"><img src="pedroberbel.png" alt="Pedro Berbel — Fondateur DEZVOLTA, créateur de RECOV™" style="width:100%;height:100%;object-fit:cover" loading="lazy" width="56" height="56"></div>
<div style="flex:1;min-width:240px">
<p style="font-size:14px;color:var(--noir);line-height:1.7;margin-bottom:8px"><span style="background:var(--vert3);padding:2px 6px;border-radius:4px">"Relancer un client qui ne paie pas, c'est la tâche qu'on repousse le plus."</span> J'ai créé RECOV pour enlever ce poids — simplement, sans agressivité.</p>
<div style="font-size:14px;font-weight:700;color:var(--noir)">Pedro Berbel — Fondateur DEZVOLTA · Créateur de RECOV™</div>
</div>
</div>
</section>
<!-- ── AVIS CLIENTS — SCROLL INFINI ── -->
<section id="avisScrollSection" style="display:block;padding:clamp(48px,6vw,72px) 0 clamp(56px,7vw,80px);background:#84CC16;overflow:hidden">
<div style="text-align:center;margin-bottom:14px">
<span style="display:inline-flex;align-items:center;gap:5px;font-size:10.5px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;padding:4px 12px;border-radius:4px;background:rgba(255,255,255,.18);color:#fff;border:1px solid rgba(255,255,255,.3)"><span style="width:6px;height:6px;border-radius:50%;background:#fff;display:inline-block"></span>RECOV Solo · Programme fondateur</span>
</div>
<h2 style="text-align:center;font-family:'Bricolage Grotesque',sans-serif;font-size:clamp(32px,5vw,56px);font-weight:500;letter-spacing:-.04em;color:#fff;margin:0 auto 10px;line-height:1.1;padding:0 clamp(16px,4vw,48px)">Ce que nos utilisateurs<br><span style="background:rgba(255,255,255,.2);padding:2px 16px;border-radius:8px">disent de nous</span></h2>
<p style="text-align:center;font-size:16px;color:rgba(255,255,255,.75);margin-bottom:clamp(36px,5vw,56px)">Retours des freelances pilotes (RECOVMAX en phase pilote, premiers retours à venir)</p>
<div style="position:relative;max-width:1200px;margin:0 auto;padding:0 60px">
<button id="avisLeft" onclick="scrollAvis(-1)" style="position:absolute;left:8px;top:50%;transform:translateY(-50%);z-index:2;width:44px;height:44px;border-radius:50%;border:none;background:#fff;color:var(--noir);font-size:22px;cursor:pointer;box-shadow:0 4px 16px rgba(0,0,0,.12);display:flex;align-items:center;justify-content:center;transition:transform .15s">‹</button>
<div id="avisScrollTrack">
<div class="avis-scroll-card"><div><div class="avis-scroll-stars">★★★★★</div><div class="avis-scroll-text">"Un client qui ne répond plus aux relances de facture : Recov a généré tout le nécessaire en 5 minutes, dont le calcul des pénalités. Simple à utiliser, très souple, et efficace. Parfait."</div></div><div class="avis-scroll-author"><div class="avis-scroll-avatar">F</div><div><div class="avis-scroll-name">Frédéric</div><div class="avis-scroll-role">Consultant IT · Paris</div></div></div></div>
<div class="avis-scroll-card"><div><div class="avis-scroll-stars">★★★★★</div><div class="avis-scroll-text">"Vraiment top, nous avions un client qui nous faisait tourner en rond depuis presque 2 mois, grâce à RECOV (et en auto), le client a fini par nous payer. Super appli, nous recommandons à 200% !"</div></div><div class="avis-scroll-author"><div class="avis-scroll-avatar">C</div><div><div class="avis-scroll-name">Claude Dujardin</div><div class="avis-scroll-role">Dirigeant · Narbonne — L'équipe Avenly</div></div></div></div>
<div class="avis-scroll-card"><div><div class="avis-scroll-stars">★★★★★</div><div class="avis-scroll-text">"Nous avons fait appel à RECOV pour plusieurs factures impayées. Réactif, professionnel, à l'écoute. Une grande partie des créances récupérée dans des délais raisonnables, tout en préservant la relation commerciale. Nous recommandons vivement."</div></div><div class="avis-scroll-author"><div class="avis-scroll-avatar">D</div><div><div class="avis-scroll-name">David B.</div><div class="avis-scroll-role">Ébéniste · Vaucluse</div></div></div></div>
</div>
<button id="avisRight" onclick="scrollAvis(1)" style="position:absolute;right:8px;top:50%;transform:translateY(-50%);z-index:2;width:44px;height:44px;border-radius:50%;border:none;background:#fff;color:var(--noir);font-size:22px;cursor:pointer;box-shadow:0 4px 16px rgba(0,0,0,.12);display:flex;align-items:center;justify-content:center;transition:transform .15s">›</button>
</div>
</section>
<style>
#avisScrollTrack{
display:flex;
gap:24px;
overflow-x:auto;
overflow-y:hidden;
scroll-snap-type:x mandatory;
scroll-behavior:smooth;
-webkit-overflow-scrolling:touch;
scrollbar-width:none;
-ms-overflow-style:none;
padding:4px 0;
align-items:flex-start;
}
#avisScrollTrack::-webkit-scrollbar{display:none}
.avis-scroll-card{
flex:0 0 calc(50% - 16px);
max-width:calc(50% - 16px);
background:#fff;
border:none;
border-radius:24px;
padding:28px 32px;
box-shadow:0 8px 32px rgba(0,0,0,.08);
transition:transform .2s,box-shadow .2s;
scroll-snap-align:start;
overflow:hidden;
box-sizing:border-box;
}
.avis-scroll-card:hover{
transform:translateY(-4px);
box-shadow:0 16px 48px rgba(0,0,0,.12);
}
#avisLeft:hover,#avisRight:hover{transform:translateY(-50%) scale(1.1)}
.avis-scroll-stars{
color:#84CC16;
font-size:16px;
letter-spacing:3px;
margin-bottom:12px;
}
.avis-scroll-text{
font-family:'DM Sans',sans-serif;
font-size:16px;
font-weight:300;
color:var(--noir);
line-height:1.7;
margin-bottom:16px;
word-wrap:break-word;
overflow-wrap:break-word;
}
.avis-scroll-author{
display:flex;
align-items:center;
gap:12px;
padding-top:14px;
border-top:1px solid #f1f5f9;
}
.avis-scroll-avatar{
width:40px;
height:40px;
border-radius:50%;
background:var(--vert3);
border:2px solid var(--vert4);
display:flex;
align-items:center;
justify-content:center;
font-size:15px;
font-weight:700;
color:var(--vert2);
flex-shrink:0;
}
.avis-scroll-name{
font-family:'Bricolage Grotesque',sans-serif;
font-size:14px;
font-weight:500;
color:var(--noir);
}
.avis-scroll-role{
font-size:12px;
color:var(--gris);
margin-top:2px;
}
@media(max-width:900px){
.avis-scroll-card{flex:0 0 80vw;max-width:80vw}
#avisScrollSection > div[style]{padding:0 48px !important}
#avisScrollSection #avisLeft{left:4px !important;width:36px !important;height:36px !important;font-size:18px !important}
#avisScrollSection #avisRight{right:4px !important;width:36px !important;height:36px !important;font-size:18px !important}
}
@media(max-width:600px){
.avis-scroll-card{flex:0 0 78vw;max-width:78vw;padding:24px}
.avis-scroll-text{font-size:14px;line-height:1.6}
#avisScrollSection > div[style]{padding:0 44px !important}
#avisScrollSection #avisLeft{left:2px !important;width:32px !important;height:32px !important;font-size:16px !important}
#avisScrollSection #avisRight{right:2px !important;width:32px !important;height:32px !important;font-size:16px !important}
}
</style>
<script>
/* Le chargement des avis est géré par le script principal (loadUserData → avis).
Ce bloc est conservé comme fallback uniquement si le script principal ne tourne pas. */
window.__avisLoaded=false;
// Fetch live depuis Supabase — écrase les cartes hardcodées si des avis existent
(function(){
var SUPA_URL='https://citcbywbelgbwqvhxfok.supabase.co';
var SUPA_KEY='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImNpdGNieXdiZWxnYndxdmh4Zm9rIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzUyMjU3MDIsImV4cCI6MjA5MDgwMTcwMn0.bUzBymNAY14aZ-02_zXkYn8xlw4L_K0KxSsU-_xqaFk';
fetch(SUPA_URL+'/rest/v1/avis?order=date.desc&limit=20&select=nom,prenom,metier,ville,note,texte',{
headers:{'apikey':SUPA_KEY,'Content-Type':'application/json'}
}).then(function(r){return r.json();}).then(function(avis){
if(!avis||!avis.length)return;
renderAvisScroll(avis);
}).catch(function(){});
})();
function renderAvisScroll(avis){
var section=document.getElementById('avisScrollSection');
var track=document.getElementById('avisScrollTrack');
if(!section||!track||!avis.length)return;
section.style.display='block';
var html='';
avis.forEach(function(a){
var stars='★'.repeat(a.note||5);
var name=(a.prenom||a.nom||'Utilisateur').replace(/</g,'<');
var role=[(a.metier||''),(a.ville||'')].filter(Boolean).join(' · ')||'Indépendant';
var initiale=name.charAt(0).toUpperCase();
var txt=(a.texte||'').replace(/</g,'<');
html+='<div class="avis-scroll-card">';
html+='<div><div class="avis-scroll-stars">'+stars+'</div>';
html+='<div class="avis-scroll-text">"'+txt+'"</div></div>';
html+='<div class="avis-scroll-author">';
html+='<div class="avis-scroll-avatar">'+initiale+'</div>';
html+='<div><div class="avis-scroll-name">'+name+'</div>';
html+='<div class="avis-scroll-role">'+role.replace(/</g,'<')+'</div></div>';
html+='</div></div>';
});
track.innerHTML=html;
}
function scrollAvis(dir){
var track=document.getElementById('avisScrollTrack');
if(!track)return;
var card=track.querySelector('.avis-scroll-card');
if(!card)return;
var scrollAmount=card.offsetWidth+24;
track.scrollBy({left:dir*scrollAmount,behavior:'smooth'});
}
</script>
<!-- ── BLOC BONUS MISSIONS (Adzuna attribution) — meme charte que sections noires ── -->
<section style="background:var(--noir);padding:clamp(48px,6vw,72px) clamp(16px,4vw,48px) clamp(48px,6vw,72px);overflow:hidden">
<div style="max-width:1000px;margin:0 auto">
<div class="bonus-grid" style="display:grid;grid-template-columns:1.1fr .9fr;gap:clamp(24px,4vw,48px);align-items:center">
<!-- Colonne texte -->
<div>
<div style="font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#84CC16;margin:0 0 10px">Bonus inclus · sans inscription</div>
<h2 style="color:#fff;margin:0 0 18px;padding:0;font-size:clamp(32px,5vw,54px);letter-spacing:-.04em;line-height:1.18">
Trouvez votre mission.<br>
<span style="background:#84CC16;color:#0B0F14;padding:6px 16px 8px;border-radius:6px;display:inline;box-decoration-break:clone;-webkit-box-decoration-break:clone">
<img src="logo.png" alt="RECOV" onerror="this.style.display='none';this.nextElementSibling.style.display='inline'" style="height:1.8em;width:auto;display:inline-block;vertical-align:-.65em;margin-right:.1em"><span style="display:none;font-family:'Bricolage Grotesque',sans-serif;font-weight:500;letter-spacing:-.04em">RECOV</span> vous aide à être payé ensuite.
</span>
</h2>
<p style="font-size:18px;color:rgba(255,255,255,.78);line-height:1.65;letter-spacing:-.005em;margin:0 0 28px;max-width:540px;font-weight:300">
Une mission gagnée finit toujours par une facture. Et une facture doit être payée.<br>
<span style="color:rgba(255,255,255,.55);font-weight:300">Une sélection de missions freelance en France, filtrée et mise à jour automatiquement — sans inscription, 100% gratuit.</span>
</p>
<!-- Stats inline -->
<div style="display:flex;gap:clamp(20px,3vw,32px);flex-wrap:wrap;margin-bottom:28px;padding:18px 0;border-top:1px solid rgba(255,255,255,.1);border-bottom:1px solid rgba(255,255,255,.1)">
<div>
<div id="bonusCount" style="font-family:'Bricolage Grotesque',sans-serif;font-size:clamp(28px,4vw,40px);font-weight:500;color:#84CC16;letter-spacing:-.03em;line-height:1">—</div>
<div style="font-size:11px;color:rgba(255,255,255,.5);font-weight:600;letter-spacing:.04em;text-transform:uppercase;margin-top:4px">Missions actives</div>
</div>
<div>
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:clamp(28px,4vw,40px);font-weight:500;color:#84CC16;letter-spacing:-.03em;line-height:1">7j/7</div>
<div style="font-size:11px;color:rgba(255,255,255,.5);font-weight:600;letter-spacing:.04em;text-transform:uppercase;margin-top:4px">Mis à jour</div>
</div>
<div>
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:clamp(28px,4vw,40px);font-weight:500;color:#84CC16;letter-spacing:-.03em;line-height:1">0 €</div>
<div style="font-size:11px;color:rgba(255,255,255,.5);font-weight:600;letter-spacing:.04em;text-transform:uppercase;margin-top:4px">Toujours gratuit</div>
</div>
</div>
<a href="missions-remote.html" class="btn-main" style="background:#84CC16;color:#0d0d14;padding:13px 24px;border-radius:10px;font-size:15px;font-weight:500;text-decoration:none;display:inline-flex;align-items:center;gap:8px;transition:background .15s">
Voir toutes les missions →
</a>
<div style="display:flex;align-items:center;gap:8px;margin-top:24px;font-size:12px;color:rgba(255,255,255,.45)">
<svg viewbox="0 0 60 60" width="18" height="18" aria-label="Adzuna" style="flex-shrink:0">
<path d="M30 4C15.7 4 4 15.7 4 30s11.7 26 26 26c5.5 0 10.6-1.7 14.8-4.6L42 47.5C38.6 49.7 34.4 51 30 51 18.4 51 9 41.6 9 30S18.4 9 30 9c8.7 0 16.2 5.3 19.4 12.8l4.4-2.4C49.8 11 40.5 4 30 4z" fill="#84CC16"/>
<circle cx="30" cy="30" r="9" fill="#84CC16"/>
</svg>
Données fournies par <a href="https://www.adzuna.fr" target="_blank" rel="noopener" style="color:#84CC16;text-decoration:none;font-weight:700">Adzuna</a> & <a href="https://www.francetravail.fr" target="_blank" rel="noopener" style="color:#84CC16;text-decoration:none;font-weight:700">France Travail</a>
</div>
</div>
<!-- Colonne droite : cards mockup + mini moteur de recherche -->
<div class="bonus-right" style="display:flex;flex-direction:column;gap:24px">
<!-- Mini moteur de recherche -->
<form onsubmit="return goToMissions(event)" style="background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);border-radius:14px;padding:14px;display:flex;flex-direction:column;gap:10px">
<div style="font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:#84CC16;display:flex;align-items:center;gap:6px">
<svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.5" viewbox="0 0 24 24"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35" stroke-linecap="round"/></svg>
Recherche rapide
</div>
<input type="search" id="bonusQ" placeholder="Métier, mission, emploi… (Ex : React, comptable, designer)" style="background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);border-radius:9px;outline:none;color:#fff;font-family:inherit;font-size:14px;font-weight:400;padding:10px 12px" aria-label="Recherche">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
<select id="bonusRegion" aria-label="Région" style="background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);border-radius:9px;color:#fff;font-family:inherit;font-size:13px;padding:10px;cursor:pointer;outline:none;min-width:0">
<option value="" style="background:#0d0d14">Toutes régions</option>
<option value="île-de-france" style="background:#0d0d14">IDF · Paris</option>
<option value="auvergne-rhône-alpes" style="background:#0d0d14">Lyon</option>
<option value="provence-alpes-côte" style="background:#0d0d14">PACA</option>
<option value="occitanie" style="background:#0d0d14">Toulouse</option>
<option value="nouvelle-aquitaine" style="background:#0d0d14">Bordeaux</option>
<option value="hauts-de-france" style="background:#0d0d14">Lille</option>
<option value="bretagne" style="background:#0d0d14">Rennes</option>
<option value="grand est" style="background:#0d0d14">Strasbourg</option>
</select>
<select id="bonusContract" aria-label="Type de contrat" style="background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);border-radius:9px;color:#fff;font-family:inherit;font-size:13px;padding:10px;cursor:pointer;outline:none;min-width:0">
<option value="" style="background:#0d0d14">Tous contrats</option>
<option value="freelance" style="background:#0d0d14">Freelance / Mission</option>
<option value="cdi" style="background:#0d0d14">CDI</option>
<option value="cdd" style="background:#0d0d14">CDD</option>
<option value="alternance" style="background:#0d0d14">Alternance</option>
<option value="stage" style="background:#0d0d14">Stage</option>
</select>
</div>
<button type="submit" style="background:#84CC16;color:#0d0d14;padding:11px 20px;border-radius:9px;font-size:14px;font-weight:500;border:none;cursor:pointer;font-family:inherit;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:background .15s" onmouseover="this.style.background='#84CC16'" onmouseout="this.style.background='#84CC16'">
Rechercher →
</button>
</form>
<!-- Cards mockup (decoratives, opacity reduite pour ne pas concurrencer le formulaire) -->
<div style="position:relative;min-height:280px;opacity:.45;pointer-events:none;transition:opacity .3s" class="bonus-mockup" onmouseover="this.style.opacity='.85'" onmouseout="this.style.opacity='.45'">
<!-- Card 1 (back) -->
<div style="position:absolute;top:0;right:8px;width:88%;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);border-radius:14px;padding:16px;backdrop-filter:blur(8px);transform:rotate(-1.5deg)">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">
<span style="display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:600;color:#38b14e"><svg viewbox="0 0 60 60" width="12" height="12"><path d="M30 4C15.7 4 4 15.7 4 30s11.7 26 26 26c5.5 0 10.6-1.7 14.8-4.6L42 47.5C38.6 49.7 34.4 51 30 51 18.4 51 9 41.6 9 30S18.4 9 30 9c8.7 0 16.2 5.3 19.4 12.8l4.4-2.4C49.8 11 40.5 4 30 4z" fill="#38b14e"/><circle cx="30" cy="30" r="9" fill="#38b14e"/></svg>Adzuna</span>
<span style="font-size:10px;color:rgba(255,255,255,.4)">il y a 2h</span>
</div>
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:14px;font-weight:500;color:#fff;line-height:1.25;margin-bottom:6px">Consultant ERP — SAP/Oracle</div>
<div style="font-size:11px;color:rgba(255,255,255,.5);display:flex;gap:10px;flex-wrap:wrap"><span>🏢 Cabinet conseil</span><span>📍 Lyon</span></div>
</div>
<!-- Card 2 (front-middle) -->
<div style="position:absolute;top:90px;right:0;width:92%;background:#fff;border-radius:14px;padding:18px;box-shadow:0 12px 40px rgba(0,0,0,.4);transform:rotate(1deg);z-index:2">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">
<span style="display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:600;color:#e63946"><svg viewbox="0 0 24 24" width="12" height="12"><circle cx="12" cy="12" r="10" fill="#e63946"/><path d="M8 7h8v2.5H10.5v2.5h4v2.5h-4V17H8V7z" fill="#fff"/></svg>France Travail</span>
<span style="font-size:10px;color:var(--gris2)">il y a 4h</span>
</div>
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:15px;font-weight:500;color:var(--noir);line-height:1.25;margin-bottom:8px">Développeur Full-Stack — Mission 6 mois</div>
<div style="font-size:11px;color:var(--gris);display:flex;gap:10px;flex-wrap:wrap;margin-bottom:8px"><span>🏢 Startup fintech</span><span>📍 Paris IDF</span><span>💰 550 €/j</span></div>
<div style="display:flex;gap:5px;flex-wrap:wrap"><span style="font-size:10px;color:var(--noir);background:var(--bg2);padding:2px 7px;border-radius:4px">React</span><span style="font-size:10px;color:var(--noir);background:var(--bg2);padding:2px 7px;border-radius:4px">Node.js</span><span style="font-size:10px;color:var(--noir);background:var(--bg2);padding:2px 7px;border-radius:4px">Freelance</span></div>
</div>
<!-- Card 3 (front, partial) -->
<div style="position:absolute;top:200px;right:18px;width:84%;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);border-radius:14px;padding:14px 16px 18px;backdrop-filter:blur(8px);transform:rotate(-0.5deg);z-index:1">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px">
<span style="display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:600;color:#38b14e"><svg viewbox="0 0 60 60" width="12" height="12"><path d="M30 4C15.7 4 4 15.7 4 30s11.7 26 26 26c5.5 0 10.6-1.7 14.8-4.6L42 47.5C38.6 49.7 34.4 51 30 51 18.4 51 9 41.6 9 30S18.4 9 30 9c8.7 0 16.2 5.3 19.4 12.8l4.4-2.4C49.8 11 40.5 4 30 4z" fill="#38b14e"/><circle cx="30" cy="30" r="9" fill="#38b14e"/></svg>Adzuna</span>
<span style="font-size:10px;color:rgba(255,255,255,.4)">il y a 6h</span>
</div>
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:13px;font-weight:500;color:#fff;line-height:1.25">Designer UX — Freelance Bordeaux</div>
</div>
</div>
</div>
</div>
</div>
</section>
<style>
@media(max-width:780px){
.bonus-grid{grid-template-columns:1fr !important}
.bonus-mockup{min-height:240px !important;margin-top:8px}
}
</style>
<script>
// Récupère le vrai compte de missions actives (honnête)
(function(){
var el = document.getElementById('bonusCount');
if(!el) return;
fetch('/api/admin?resource=jobs_feed').then(r => r.json()).then(d => {
if(d && typeof d.count === 'number'){
el.textContent = d.count > 0 ? d.count + '+' : '—';
}
}).catch(() => { el.textContent = '+'; });
})();
// Mini moteur de recherche : redirige vers missions-remote.html avec params
function goToMissions(e){
e.preventDefault();
var q = (document.getElementById('bonusQ').value || '').trim();
var region = document.getElementById('bonusRegion').value;
var params = new URLSearchParams();
if (q) params.set('q', q);
if (region) params.set('region', region);
var qs = params.toString();
window.location.href = 'missions-remote.html' + (qs ? '?' + qs : '');
return false;
}
</script>
<!-- ── FAQ ── -->
<!-- ── GUIDE SEO ── -->
<section style="max-width:720px;margin:0 auto;padding:clamp(40px,5vw,56px) clamp(16px,4vw,48px) 40px">
<a href="guide-relance-facture-impayee.html" style="display:flex;align-items:center;gap:20px;background:#fff;border:1px solid var(--border);border-radius:12px;padding:20px 24px;text-decoration:none;transition:border-color .15s,box-shadow .15s" onmouseover="this.style.borderColor='var(--vert)';this.style.boxShadow='var(--sh1)'" onmouseout="this.style.borderColor='var(--border)';this.style.boxShadow='none'">
<div style="flex-shrink:0;width:44px;height:44px;background:var(--vert3);border:1px solid var(--vert4);border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:20px">📖</div>
<div style="flex:1;min-width:0">
<div style="font-size:15px;font-weight:700;color:var(--noir)">Comment relancer une facture impayée</div>
<div style="font-size:13px;color:var(--gris);margin-top:2px">Le guide complet : délais, pénalités, modèles, mise en demeure</div>
</div>
<div style="flex-shrink:0;font-size:13px;font-weight:600;color:var(--vert)">Lire →</div>
</a>
</section>
<section class="faq-section">
<div class="section-eyebrow" style="margin-bottom:16px">Questions fréquentes</div>
<!-- ════════════════════════════════════════════════════════════════ -->
<!-- BLOC 1 — Comprendre la suite RECOV : RECOV Solo vs RECOVMAX -->
<!-- ════════════════════════════════════════════════════════════════ -->
<div style="font-size:11px;font-weight:600;letter-spacing:.12em;text-transform:uppercase;color:var(--vert);margin:8px 0 4px">Suite RECOV — quelle version pour qui ?</div>
<div class="faq-item" role="button" tabindex="0" aria-expanded="false" onclick="toggleFaq(this)" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleFaq(this)}">
<div class="faq-q">Quelle différence entre RECOV Solo et RECOVMAX ? <span class="faq-ico">+</span></div>
<div class="faq-a">RECOV Solo est conçu pour un indépendant qui relance ses propres factures impayées (mono-utilisateur, mono-tenant). RECOVMAX est conçu pour un professionnel qui suit les créances de plusieurs clients (multi-tenant, RGPD, mandat de gestion, rapport mensuel par client). En pratique : si vous êtes freelance et facturez en votre nom propre, RECOV. Si vous êtes secrétaire indépendante, comptable solo, assistante virtuelle ou cabinet et que vous gérez l'administratif de plusieurs TPE, RECOVMAX. Les deux outils partagent la même base technique, le même cadre juridique L441-10, et la même promesse 0 % commission.</div>
</div>
<div class="faq-item" role="button" tabindex="0" aria-expanded="false" onclick="toggleFaq(this)" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleFaq(this)}">
<div class="faq-q">RECOV Solo est-il pour les professionnels de la facturation ? <span class="faq-ico">+</span></div>
<div class="faq-a">Oui, RECOV est conçu pour les pros qui émettent des factures B2B et veulent récupérer celles qui ne sont pas payées. Cela inclut les freelances, consultants, formateurs, designers, développeurs, coachs, prestataires de services. RECOV intervient après l'émission de la facture (peu importe l'outil de facturation utilisé : Indy, Freebe, Qonto, Pennylane, Tiime, papier, PDF…) et structure les relances pour récupérer le paiement. Si vous gérez la facturation pour le compte d'autres pros, c'est RECOVMAX qu'il vous faut.</div>
</div>
<div class="faq-item" role="button" tabindex="0" aria-expanded="false" onclick="toggleFaq(this)" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleFaq(this)}">
<div class="faq-q">RECOVMAX est-il pour les secrétaires indépendantes ou les cabinets comptables ? <span class="faq-ico">+</span></div>
<div class="faq-a">Oui — RECOVMAX cible spécifiquement ces profils. Secrétaires indépendantes, assistantes virtuelles, office managers externalisés, comptables solo, cabinets compta TPE/PME. L'outil permet d'ajouter une prestation rentable à votre activité : suivi des factures impayées de chacun de vos clients, avec mandat de gestion signé, rapport mensuel PDF brandable, score santé portefeuille. La pratique observée chez nos premiers utilisateurs : facturer entre 50 et 80 € HT/mois par client suivi. Pour 12 clients suivis, cela représente ~720 €/mois récurrents — l'outil RECOVMAX coûte 19 €/mois (tarif fondateur maintenu tant que l'abonnement reste actif). <a href="/recovmax.html" style="color:var(--vert);font-weight:600">Découvrir RECOVMAX →</a></div>
</div>
<div class="faq-item" role="button" tabindex="0" aria-expanded="false" onclick="toggleFaq(this)" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleFaq(this)}">
<div class="faq-q">Combien puis-je facturer cette prestation à mes clients TPE ? <span class="faq-ico">+</span></div>
<div class="faq-a">Pratiques observées chez les pros qui utilisent un outil structuré : secrétaire indé / VA → 30 à 80 €/mois par client suivi. Cabinet compta solo → 50 à 150 €/mois par client. Office manager externalisé → forfait inclus dans la prestation globale. Le rapport mensuel PDF généré par RECOVMAX (actions menées, montants récupérés, score santé) est le livrable qui justifie cette facturation auprès de votre client final. Vous fixez votre tarif, vous gardez la marge. RECOVMAX ne prélève aucune commission sur les montants récupérés ni sur votre prestation.</div>
</div>
<div class="faq-item" role="button" tabindex="0" aria-expanded="false" onclick="toggleFaq(this)" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleFaq(this)}">
<div class="faq-q">RECOVMAX à 19 € et RECOV Solo gratuit : quelle différence ? <span class="faq-ico">+</span></div>
<div class="faq-a"><strong>RECOVMAX à 19 €/mois (tarif fondateur)</strong> est la solution pour l'usage professionnel multi-clients — cabinets comptables, secrétaires indépendantes, office managers, DAF externalisés qui suivent les créances de plusieurs clients TPE : jusqu'à 50 clients distincts × 50 créances actives × 1 000 relances/mois, multi-utilisateurs, traçabilité structurée, rapport mensuel par client refacturable, audit trail horodaté, score santé portefeuille. Tarif fondateur conservé tant que l'abonnement reste actif sur les <span data-pricing="founders-cap">30</span> premières places, 29 €/mois ensuite. <strong>RECOV Solo reste toujours gratuit</strong>, sans carte bancaire, pour le freelance qui relance ses propres factures impayées (1 SIREN, 1 créance active à la fois, 1 mise en demeure par mois). 0 % de commission sur les deux offres.</div>
</div>
<div class="faq-item" role="button" tabindex="0" aria-expanded="false" onclick="toggleFaq(this)" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleFaq(this)}">
<div class="faq-q">RECOV ou RECOVMAX peuvent-ils remplacer un cabinet de recouvrement ? <span class="faq-ico">+</span></div>
<div class="faq-a">Non — et ce n'est pas leur rôle. RECOV et RECOVMAX outillent la phase amiable structurée (de l'échéance au début du contentieux). Pour la phase contentieuse (injonction de payer, exécution forcée, procédure judiciaire), seuls les professionnels habilités sont compétents : avocats et commissaires de justice. RECOV/RECOVMAX préparent le dossier amiable propre (timeline horodatée, mises en demeure, preuves) qui pourra être transmis à un commissaire de justice si la phase amiable n'aboutit pas. C'est une complémentarité, pas une concurrence.</div>
</div>
<div class="faq-item" role="button" tabindex="0" aria-expanded="false" onclick="toggleFaq(this)" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleFaq(this)}">
<div class="faq-q">La nouvelle procédure simplifiée 2026 entre commerçants — RECOV peut-il aider ? <span class="faq-ico">+</span></div>
<div class="faq-a">La loi n° 2026-307 du 23 avril 2026 crée une procédure simplifiée pour les créances commerciales incontestées entre commerçants — sans seuil de montant, frais à la charge du débiteur : commandement de payer signifié par le commissaire de justice → 1 mois pour payer ou contester → procès-verbal de non-contestation → formule exécutoire par le greffier. Sans juge, sans audience. Les modalités précises seront fixées par décret à venir. Cette procédure ne peut être lancée que par un commissaire de justice — RECOV et RECOVMAX ne délivrent aucun titre exécutoire et ne se substituent pas à un commissaire de justice. En revanche, comme la contestation (même brève) met fin à la procédure, la qualité de la documentation amiable devient déterminante. RECOVMAX intègre une checklist d'éligibilité (10 critères vérifiés) et prépare le dossier transmissible au commissaire. <a href="/recovmax.html#cadre-2026" style="color:var(--vert);font-weight:600">En savoir plus →</a></div>
</div>
<div class="faq-item" role="button" tabindex="0" aria-expanded="false" onclick="toggleFaq(this)" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleFaq(this)}">
<div class="faq-q">Mes clients verront-ils que j'utilise RECOV ou RECOVMAX ? <span class="faq-ico">+</span></div>
<div class="faq-a">Non — RECOV et RECOVMAX sont invisibles aux yeux de vos débiteurs. Les emails partent depuis votre messagerie professionnelle (ou la signature de votre client final pour RECOVMAX), signés à votre nom, sans aucune mention RECOV/RECOVMAX/DEZVOLTA. Les courriers PDF (mise en demeure, lettre RAR) sont à votre en-tête. Le rapport mensuel généré pour le client final est brandable — c'est votre prestation, pas la nôtre. RECOV/RECOVMAX sont des outils de structuration interne : ils vous aident à préparer, vous validez, vous envoyez en votre nom.</div>
</div>
<!-- ════════════════════════════════════════════════════════════════ -->
<!-- BLOC 2 — Comment ça marche au quotidien (existant) -->
<!-- ════════════════════════════════════════════════════════════════ -->
<div style="font-size:11px;font-weight:600;letter-spacing:.12em;text-transform:uppercase;color:var(--vert);margin:32px 0 4px">Comment ça marche au quotidien</div>
<div class="faq-item" role="button" tabindex="0" aria-expanded="false" onclick="toggleFaq(this)" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleFaq(this)}">
<div class="faq-q">Est-ce que je risque de perdre mon client en relançant ? <span class="faq-ico">+</span></div>
<div class="faq-a">C'est la peur numéro un — et c'est justement pour ça que RECOV existe. Chaque message est professionnel, progressif et adapté à votre relation avec le client. Le ton monte graduellement sur 60 jours : on commence par un rappel amical, pas par une mise en demeure. Vous relisez et validez chaque message avant envoi. Et surtout : ne pas relancer, c'est accepter de ne pas être payé. Un client qui ne paie pas une facture échue ne sera pas surpris de recevoir un rappel — il s'y attend.</div>
</div>
<div class="faq-item" role="button" tabindex="0" aria-expanded="false" onclick="toggleFaq(this)" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleFaq(this)}">
<div class="faq-q">Dois-je fournir ma carte bancaire pour essayer ? <span class="faq-ico">+</span></div>
<div class="faq-a">Non. Vous accédez gratuitement à toutes les fonctionnalités pendant la phase pilote, sans carte bancaire et sans engagement de durée. Il suffit d'une adresse email. En échange, nous vous demandons de tester l'outil et de partager votre retour. RECOV ne stocke aucun PDF de facture ni pièce comptable — seules les données nécessaires à la rédaction des relances sont traitées (référence, montant, échéance).</div>
</div>
<div class="faq-item" role="button" tabindex="0" aria-expanded="false" onclick="toggleFaq(this)" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleFaq(this)}">
<div class="faq-q">Les emails générés sont-ils vraiment personnalisés ? <span class="faq-ico">+</span></div>
<div class="faq-a">Oui. RECOV<sup style="font-size:.6em">™</sup> utilise le nom du client, le montant exact, la référence facture, les jours de retard et le profil de la relation. Chaque séquence est unique, rédigée dans un registre professionnel B2B avec le bon niveau de formalisme.</div>
</div>
<div class="faq-item" role="button" tabindex="0" aria-expanded="false" onclick="toggleFaq(this)" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleFaq(this)}">
<div class="faq-q">Pourquoi 8 relances ? C'est pas trop ? <span class="faq-ico">+</span></div>
<div class="faq-a">Non — c'est le minimum efficace. La majorité des retards de paiement sont dus à l'oubli ou la désorganisation, pas à la mauvaise volonté. La plupart des règlements interviennent après 2 à 4 relances structurées. Mais sans séquence complète, on abandonne trop tôt. Plus la relance intervient tôt, plus les chances de résolution amiable sont élevées. Les 8 étapes de RECOV couvrent une fenêtre de 60 jours avec un ton progressif — du rappel amical à la mise en demeure.</div>
</div>
<div class="faq-item" role="button" tabindex="0" aria-expanded="false" onclick="toggleFaq(this)" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleFaq(this)}">
<div class="faq-q">RECOV est-il un cabinet de recouvrement ? <span class="faq-ico">+</span></div>
<div class="faq-a">Non. RECOV est un copilote de recouvrement amiable. Il ne contacte jamais vos clients, ne procède à aucun acte de recouvrement en votre nom, et ne prélève aucune commission sur les sommes récupérées. Vous restez l'auteur et le responsable de chaque message.<br><br>Si votre séquence de 8 relances n'aboutit pas — ce qui reste rare quand elle est menée jusqu'au bout — RECOV ne se substitue pas à un professionnel du recouvrement. Dans ce cas, vous pouvez transmettre votre dossier (avec l'historique complet de vos relances) à un cabinet ou un commissaire de justice. Votre dossier sera d'autant plus solide que vous aurez déjà relancé de manière structurée.</div>
</div>
<div class="faq-item" role="button" tabindex="0" aria-expanded="false" onclick="toggleFaq(this)" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleFaq(this)}">
<div class="faq-q">RECOV garantit-il le paiement ? <span class="faq-ico">+</span></div>
<div class="faq-a">Non — aucun outil ne peut garantir un paiement. En revanche, les chiffres sont clairs : selon la FIGEC, environ 70 % des créances sont récupérées en phase amiable, et la plupart des règlements interviennent dans les 30 jours suivant la première relance structurée. Plus on relance tôt et de manière organisée, plus les chances sont élevées. RECOV structure ce processus pour vous — mais si la phase amiable échoue, votre dossier de relances complet facilite la suite (avocat, commissaire de justice, injonction de payer).</div>
</div>
<div class="faq-item" role="button" tabindex="0" aria-expanded="false" onclick="toggleFaq(this)" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleFaq(this)}">
<div class="faq-q">Mes données sont-elles en sécurité ? <span class="faq-ico">+</span></div>
<div class="faq-a">RECOV applique une logique de sécurité par dossier : accès authentifié, séparation des données, historique des actions, validation humaine avant envoi (Supabase, hébergé en Europe). RECOVMAX ajoute un cadre multi-clients avec séparation des portefeuilles, audit trail et reporting par client. Vous pouvez exporter et supprimer vos données à tout moment depuis votre profil.</div>
</div>
<div class="faq-item" role="button" tabindex="0" aria-expanded="false" onclick="toggleFaq(this)" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleFaq(this)}">
<div class="faq-q">Puis-je résilier à tout moment ? <span class="faq-ico">+</span></div>
<div class="faq-a">Oui, à tout moment. Aucun engagement, aucun frais caché. <strong>RECOV Solo est toujours gratuit</strong> pour vos propres factures impayées (1 SIREN, 1 créance active à la fois, 1 mise en demeure par mois). <strong>RECOVMAX est à 19 €/mois fondateur</strong> si vous suivez les impayés de plusieurs clients (résiliable à tout moment ; vos dossiers en cours restent accessibles jusqu'à leur résolution). RECOV ne prend aucune commission sur les montants récupérés — l'argent va directement sur l'IBAN du créancier.</div>
</div>
<div class="faq-item" role="button" tabindex="0" aria-expanded="false" onclick="toggleFaq(this)" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleFaq(this)}">
<div class="faq-q">Pourquoi choisir RECOV plutôt qu'un autre outil ? <span class="faq-ico">+</span></div>
<div class="faq-a">Les outils de facturation (Indy, Freebe, Qonto) créent la facture — mais ne gèrent pas la relance quand le client ne paie pas. Les cabinets de recouvrement prélèvent 8 à 15% de commission et agissent à votre place. ChatGPT peut rédiger un email, mais sans timing, sans montée en pression, sans suivi ni dossier. RECOV est conçu pour structurer tout le processus de relance — du premier rappel à la mise en demeure — sans commission, sans changer d'outil de facturation, avec un contrôle total sur chaque message. <a href="comparatif-logiciel-relance-facture.html" style="color:var(--vert);font-weight:600">Voir le comparatif détaillé →</a></div>
</div>
<div class="faq-item" role="button" tabindex="0" aria-expanded="false" onclick="toggleFaq(this)" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleFaq(this)}">
<div class="faq-q">RECOV est-il compatible avec la facturation électronique 2026 ? <span class="faq-ico">+</span></div>
<div class="faq-a">Oui. RECOV intervient après l'émission de la facture — quel que soit son format (papier, PDF, Factur-X, facture électronique via Chorus Pro ou une PDP). Quand un client ne paie pas, le processus de relance amiable est le même : rappel, relance, pénalités, mise en demeure. RECOV n'est pas un outil de facturation — il prend le relais quand la facture existe mais que le paiement ne vient pas. <a href="facturation-electronique.html" style="color:var(--vert);font-weight:600">En savoir plus sur la facturation électronique →</a></div>
</div>
<div class="faq-item" role="button" tabindex="0" aria-expanded="false" onclick="toggleFaq(this)" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleFaq(this)}">
<div class="faq-q">Est-ce que ça marche vraiment ? <span class="faq-ico">+</span></div>
<div class="faq-a">Oui. RECOV est né d'un besoin réel : j'avais plusieurs factures en retard et aucune méthode pour relancer sans y passer des heures ni détériorer la relation. La plupart des retards de paiement sont des oublis — une relance structurée, au bon moment, avec le bon ton, suffit à débloquer la situation. RECOV fait exactement ça : il vous dit quoi écrire, quand, et comment monter en pression si nécessaire. Vous pouvez tester gratuitement sur une situation réelle.</div>
</div>
</section>
</main>
<style>
@keyframes scroll-avis{0%{transform:translateX(0)}100%{transform:translateX(-50%)}}
.avis-card-old-unused{
flex-shrink:0;width:320px;
background:rgba(255,255,255,.07);backdrop-filter:blur(8px);
border:1px solid rgba(255,255,255,.1);border-radius:16px;
padding:24px;color:#fff;
transition:transform .2s,box-shadow .2s;
}
.avis-card:hover{transform:translateY(-4px);box-shadow:0 12px 32px rgba(0,0,0,.3);animation-play-state:paused}
.avis-card-stars{color:#f59e0b;font-size:16px;letter-spacing:2px;margin-bottom:12px}
.avis-card-text{font-size:14px;color:rgba(255,255,255,.85);line-height:1.7;font-style:italic;margin-bottom:16px}
.avis-card-author{display:flex;align-items:center;gap:10px;padding-top:12px;border-top:1px solid rgba(255,255,255,.1)}
.avis-card-avatar{width:36px;height:36px;border-radius:50%;background:#84cc16;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:700;color:#0f172a}
.avis-card-name{font-size:13px;font-weight:700}
.avis-card-role{font-size:11px;color:rgba(255,255,255,.5)}
</style>
<!-- ── ARTICLES / MAILLAGE BLOG ── -->
<section style="max-width:800px;margin:0 auto;padding:clamp(24px,3vw,40px) clamp(16px,4vw,48px) clamp(40px,6vw,56px)">
<h3 style="font-size:clamp(20px,3vw,24px);font-weight:800;letter-spacing:-.03em;margin-bottom:20px;color:var(--noir)">Guides et ressources</h3>
<div style="display:flex;gap:10px;flex-wrap:wrap">
<a href="guide-relance-facture-impayee.html" style="font-size:13px;color:var(--vert);font-weight:600;text-decoration:none">Guide complet →</a>
<a href="comparatif-logiciel-relance-facture.html" style="font-size:13px;color:var(--vert);font-weight:600;text-decoration:none">Comparatif →</a>
<a href="blog.html" style="font-size:13px;color:var(--gris);font-weight:500;text-decoration:none">Tous les articles →</a>
</div>
</section>
<!-- ── DERNIERS ARTICLES — magazine 3 col (dynamique Supabase) ── -->
<style>
.journal-section{display:none;max-width:1200px;margin:0 auto;padding:clamp(48px,7vw,80px) clamp(16px,4vw,48px) clamp(40px,6vw,64px)}
.journal-header{display:flex;align-items:flex-end;justify-content:space-between;gap:24px;margin-bottom:clamp(24px,4vw,40px);flex-wrap:wrap}
.journal-eyebrow{font-size:11px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;color:var(--vert);margin-bottom:8px}
.journal-title{font-family:'Bricolage Grotesque',sans-serif;font-size:clamp(26px,4.2vw,40px);font-weight:500;letter-spacing:-.025em;line-height:1.1;color:var(--noir);margin:0}
.journal-subtitle{font-size:14.5px;color:var(--gris);line-height:1.55;margin-top:8px;max-width:520px}
.journal-link-all{font-size:13.5px;font-weight:600;color:var(--noir);text-decoration:none;border-bottom:1.5px solid var(--noir);padding-bottom:2px;white-space:nowrap;transition:opacity .15s;flex-shrink:0}
.journal-link-all:hover{opacity:.6}
.journal-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:clamp(16px,2vw,24px)}
@media(max-width:900px){.journal-grid{grid-template-columns:repeat(2,1fr)}}
@media(max-width:600px){.journal-grid{grid-template-columns:1fr}}
.journal-card{display:flex;flex-direction:column;padding:24px 22px 22px;background:var(--bg2);border:1px solid var(--border);border-radius:14px;text-decoration:none;color:inherit;transition:transform .18s ease,border-color .18s ease,box-shadow .18s ease;min-height:240px}
.journal-card:hover{transform:translateY(-3px);box-shadow:0 8px 24px -8px rgba(0,0,0,.08)}
.journal-card[data-product="recov"]:hover{border-color:var(--vert)}
.journal-card[data-product="recovmax"]:hover{border-color:#84CC16}
.journal-meta-row{display:flex;align-items:center;gap:8px;margin-bottom:14px;flex-wrap:wrap}
.journal-badge{display:inline-flex;align-items:center;font-size:10.5px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;padding:4px 8px;border-radius:4px;line-height:1}
.journal-badge[data-product="recov"]{background:rgba(101,163,13,.12);color:#4d7c0f}
.journal-badge[data-product="recovmax"]{background:rgba(132,204,22,.16);color:#3f6212}
.journal-cat{font-size:11px;color:var(--gris2);font-weight:600;letter-spacing:.04em;text-transform:uppercase}
.journal-card-title{font-family:'Bricolage Grotesque',sans-serif;font-size:18px;font-weight:500;line-height:1.3;letter-spacing:-.015em;color:var(--noir);margin:0 0 10px;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
.journal-card-desc{font-size:13.5px;color:var(--gris);line-height:1.55;margin:0 0 auto;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.journal-card-foot{display:flex;align-items:center;gap:10px;margin-top:18px;padding-top:14px;border-top:1px solid var(--border);font-size:12px;color:var(--gris2)}
.journal-card-foot .dot{width:3px;height:3px;border-radius:50%;background:var(--gris2)}
.journal-arrow{margin-left:auto;font-size:14px;color:var(--noir);transition:transform .2s}
.journal-card:hover .journal-arrow{transform:translateX(3px)}
</style>
<section id="latestArticles" class="journal-section">
<div class="journal-header">
<div>
<div class="journal-eyebrow">Ressources</div>
<h2 class="journal-title">Le journal RECOV / RECOVMAX</h2>
<p class="journal-subtitle">Décryptages juridiques et opérationnels pour les indépendants, cabinets et professionnels qui pilotent la relance amiable. Publié chaque semaine.</p>
</div>
<a href="blog-auto.html" class="journal-link-all">Tous les articles →</a>
</div>
<div id="latestArticlesGrid" class="journal-grid"></div>
</section>
<script>
(async function(){
try{
const r=await fetch('https://citcbywbelgbwqvhxfok.supabase.co/rest/v1/auto_articles?status=eq.published&order=created_at.desc&limit=6&select=title,slug,description,category,product,content,created_at',{
headers:{'apikey':'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImNpdGNieXdiZWxnYndxdmh4Zm9rIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzUyMjU3MDIsImV4cCI6MjA5MDgwMTcwMn0.bUzBymNAY14aZ-02_zXkYn8xlw4L_K0KxSsU-_xqaFk'}
});
if(!r.ok)return;
const articles=await r.json();
if(!articles||!Array.isArray(articles)||!articles.length)return;
var topThree=articles.slice(0,3);
var grid=document.getElementById('latestArticlesGrid');
var catLabels={'conseils':'Conseils','droit':'Droit','methode':'Méthode','actualite':'Actualité','temoignage':'Témoignage'};
// Heuristique de secours si product absent (anciens articles avant migration 058)
var recovmaxKeywords=/cabinet|secrétaire\s+indépendant|secrétariat\s+indépendant|office\s+manager|daf\s+externalisé|multi[-]?clients?|portefeuille\s+(?:de\s+)?clients?|recovmax|expert[-]?comptable|délégation/i;
topThree.forEach(function(a){
var product=a.product;
if(!product){product=recovmaxKeywords.test(a.title)?'recovmax':'recov';}
var date=new Date(a.created_at).toLocaleDateString('fr-FR',{day:'numeric',month:'short',year:'numeric'});
// Calcul temps de lecture (200 mots/min)
var wordCount=(a.content||a.description||'').replace(/<[^>]+>/g,' ').split(/\s+/).length;
var readMin=Math.max(2,Math.round(wordCount/200));
var badge=product==='recovmax'?'RECOVMAX':'RECOV';
var catLabel=catLabels[a.category]||'Article';
var desc=(a.description||'').replace(/</g,'<');
var title=(a.title||'').replace(/</g,'<');
var card=document.createElement('a');
card.href='blog-auto.html#'+a.slug;
card.className='journal-card';
card.setAttribute('data-product',product);
card.innerHTML=
'<div class="journal-meta-row">'+
'<span class="journal-badge" data-product="'+product+'">'+badge+'</span>'+
'<span class="journal-cat">'+catLabel+'</span>'+
'</div>'+
'<h3 class="journal-card-title">'+title+'</h3>'+
'<p class="journal-card-desc">'+desc+'</p>'+
'<div class="journal-card-foot">'+
'<span>'+date+'</span>'+
'<span class="dot"></span>'+
'<span>'+readMin+' min de lecture</span>'+
'<span class="journal-arrow">→</span>'+
'</div>';
grid.appendChild(card);
});
document.getElementById('latestArticles').style.display='block';
}catch(e){}
})();
</script>
<!-- ── CTA FINAL ── -->
<section class="cta-section">
<div class="cta-inner">
<h3 style="font-family:'Bricolage Grotesque',sans-serif;font-size:clamp(24px,4vw,32px);font-weight:500">Ne laissez pas une facture impayée dormir.</h3>
<p style="font-size:15px;color:var(--gris);margin-bottom:24px">Essayez RECOV gratuitement. Votre première relance est prête en 30 secondes.</p>
<button class="btn-cta-white" onclick="openAuth('signup')">Générer ma première relance →</button>
<div class="cta-note">Sans engagement de durée · Accès gratuit pendant la phase pilote</div>
</div>
</section>
<!-- ── CONTENU SEO (résumé court + lien guide) ── -->
<section style="max-width:800px;margin:0 auto;padding:32px clamp(16px,4vw,48px)">
<h3 style="font-family:'Bricolage Grotesque',sans-serif;font-size:clamp(18px,3vw,22px);font-weight:500;letter-spacing:-.03em;margin-bottom:12px;color:var(--noir)">Facture impayée : que faire ?</h3>
<p style="font-size:16px;color:var(--gris);line-height:1.7;font-weight:400;margin-bottom:16px">Votre client ne paie pas la facture ? RECOV prépare une <a href="blog-modele-email-relance-facture.html" style="color:var(--vert);text-decoration:none;font-weight:600">relance amiable structurée</a> en 8 étapes sur 60 jours : message courtois, rappel ferme, <a href="calculateur-penalites.html" style="color:var(--vert);text-decoration:none;font-weight:600">calcul des pénalités de retard</a>, puis <a href="blog-mise-en-demeure-modele-guide.html" style="color:var(--vert);text-decoration:none;font-weight:600">mise en demeure PDF</a>. Vous validez chaque envoi. La loi prévoit une <a href="blog-indemnite-forfaitaire-recouvrement-40-euros.html" style="color:var(--vert);text-decoration:none;font-weight:600">indemnité forfaitaire de 40 euros</a> automatiquement due entre professionnels. Le <a href="blog-delai-paiement-facture-loi.html" style="color:var(--vert);text-decoration:none;font-weight:600">délai de paiement d'une facture</a> est encadré (30 jours par défaut, 60 jours max). Si la phase amiable n'aboutit pas, l'<a href="blog-injonction-payer-procedure.html" style="color:var(--vert);text-decoration:none;font-weight:600">injonction de payer</a> reste la voie judiciaire la plus rapide — RECOV vous aide à constituer un dossier transmissible.</p>
<div style="display:flex;gap:16px;flex-wrap:wrap">
<a href="guide-relance-facture-impayee.html" style="font-size:13px;color:var(--vert);font-weight:600;text-decoration:none">Lire le guide complet →</a>
<a href="blog-modele-email-relance-facture.html" style="font-size:13px;color:var(--vert);font-weight:600;text-decoration:none">Modèles email →</a>
<a href="calculateur-penalites.html" style="font-size:13px;color:var(--vert);font-weight:600;text-decoration:none">Calculer les pénalités →</a>
<a href="blog-cgv-freelance-clauses-obligatoires.html" style="font-size:13px;color:var(--vert);font-weight:600;text-decoration:none">CGV freelance →</a>
<a href="blog.html" style="font-size:13px;color:var(--gris);font-weight:500;text-decoration:none">Tous les articles →</a>
</div>
</section>
<!-- ── FOOTER ── -->
<footer role="contentinfo">
<div>
<div class="footer-logo-wrap">
<div style="background:#fff;border-radius:6px;padding:4px 10px;display:inline-flex;align-items:center;gap:3px"><img src="logoRecovR.png" alt="RECOV™" style="height:20px;width:auto"><sup style="font-size:8px;font-weight:500;color:#64748b;margin-top:-6px">™</sup></div>
</div>
<div class="footer-tagline">Copilote de recouvrement amiable pour indépendants, freelances, auto-entrepreneurs et micro-entreprises</div>
<div style="font-size:11px;color:rgba(255,255,255,.35);margin-top:6px">© 2026 DEZVOLTA · SIREN 948 914 072 · RECOV<sup style="font-size:.6em">™</sup> marque en cours de dépôt INPI</div>
<div style="font-size:11px;color:rgba(255,255,255,.35);margin-top:4px">Service réservé aux professionnels établis en France métropolitaine et Outre-mer · droit français applicable (L.441-10, loi 2026-307, RGPD/UE)</div>
<div style="font-size:11px;color:rgba(255,255,255,.35);margin-top:4px">♿ Conçu avec un souci d'accessibilité · navigation clavier · lecteurs d'écran · accessibilité partielle (audit en cours)</div>
<div style="display:flex;align-items:center;gap:16px;margin-top:10px">
<span style="font-size:10px;color:rgba(255,255,255,.25);letter-spacing:.04em">Propulsé par</span>
<span style="font-size:11px;font-weight:600;color:rgba(255,255,255,.3);letter-spacing:.02em">▲ Vercel</span>
<span style="font-size:11px;font-weight:600;color:rgba(255,255,255,.3);letter-spacing:.02em">◆ Supabase</span>
<span style="font-size:11px;font-weight:600;color:rgba(255,255,255,.3);letter-spacing:.02em">✦ Anthropic</span>
</div>
</div>
<div style="display:grid;grid-template-columns:repeat(3,auto);gap:32px;font-size:12px">
<div>
<div style="font-size:11px;font-weight:700;color:rgba(255,255,255,.5);letter-spacing:.06em;text-transform:uppercase;margin-bottom:10px">Produit</div>
<a href="recovmax.html#simulation" style="display:block;color:#84CC16;text-decoration:none;margin-bottom:6px;font-weight:700">→ Simulation gratuite</a>
<a href="recovmax.html" style="display:block;color:rgba(255,255,255,.4);text-decoration:none;margin-bottom:6px">RECOVMAX (cabinets)</a>
<a href="comment.html" style="display:block;color:rgba(255,255,255,.4);text-decoration:none;margin-bottom:6px">Comment ça marche</a>
<a href="apropos.html" style="display:block;color:rgba(255,255,255,.4);text-decoration:none;margin-bottom:6px">À propos</a>
<a href="partenaires.html" style="display:block;color:rgba(255,255,255,.4);text-decoration:none;margin-bottom:6px">Devenir partenaire</a>
<a href="contact.html" style="display:block;color:rgba(255,255,255,.4);text-decoration:none;margin-bottom:6px">Contact</a>
<a href="https://www.linkedin.com/company/112614855/" target="_blank" rel="noopener" style="display:block;color:rgba(255,255,255,.4);text-decoration:none">LinkedIn</a>
</div>
<div>
<div style="font-size:11px;font-weight:700;color:rgba(255,255,255,.5);letter-spacing:.06em;text-transform:uppercase;margin-bottom:10px">Ressources</div>
<a href="guide-relance-facture-impayee.html" style="display:block;color:rgba(255,255,255,.4);text-decoration:none;margin-bottom:6px">Guide relance</a>
<a href="calculateur-penalites.html" style="display:block;color:rgba(255,255,255,.4);text-decoration:none;margin-bottom:6px">Calculateur pénalités</a>
<a href="plaquette-recovmax-v2.html" target="_blank" rel="noopener" style="display:block;color:#84CC16;text-decoration:none;margin-bottom:6px;font-weight:500">→ Plaquette RECOVMAX (PDF)</a>
<a href="blog.html" style="display:block;color:rgba(255,255,255,.4);text-decoration:none;margin-bottom:6px">Blog</a>
<a href="comparatif-logiciel-relance-facture.html" style="display:block;color:rgba(255,255,255,.4);text-decoration:none">Comparatif</a>
</div>
<div>
<div style="font-size:11px;font-weight:700;color:rgba(255,255,255,.5);letter-spacing:.06em;text-transform:uppercase;margin-bottom:10px">Légal</div>
<a href="cgu.html" style="display:block;color:rgba(255,255,255,.4);text-decoration:none;margin-bottom:6px">CGU RECOV</a>
<a href="mentions.html" style="display:block;color:rgba(255,255,255,.4);text-decoration:none;margin-bottom:6px">Mentions légales</a>
<a href="confidentialite.html" style="display:block;color:rgba(255,255,255,.4);text-decoration:none;margin-bottom:6px">Confidentialité</a>
<a href="dpa.html" style="display:block;color:rgba(255,255,255,.4);text-decoration:none;margin-bottom:6px">DPA RECOV</a>
<a href="cgu-recovmax.html" style="display:block;color:rgba(255,255,255,.4);text-decoration:none;margin-bottom:6px">CGU RECOVMAX</a>
<a href="dpa-recovmax.html" style="display:block;color:rgba(255,255,255,.4);text-decoration:none">DPA RECOVMAX</a>
</div>
</div>
</footer>
<!-- Bandeau informatif vie privée -->
<div id="rgpdBanner" style="display:none;position:fixed;bottom:0;left:0;right:0;background:#1a1a1a;color:#fff;padding:16px 24px;z-index:9999;font-size:13px;line-height:1.6;box-shadow:0 -4px 20px rgba(0,0,0,.2)">
<div style="max-width:900px;margin:0 auto;display:flex;align-items:center;gap:16px;flex-wrap:wrap">
<div style="flex:1;min-width:280px">
🔒 Respect de votre vie privée — Aucun cookie publicitaire. Aucun traceur tiers. Statistiques anonymes sans cookies. Données hébergées en Union européenne.
<a href="confidentialite.html" style="color:var(--vert);margin-left:4px">En savoir plus</a>
</div>
<button onclick="acceptRgpd()" style="padding:8px 20px;border-radius:8px;border:none;background:var(--vert);color:#fff;font-size:13px;font-weight:500;cursor:pointer;font-family:inherit;flex-shrink:0">Compris</button>
</div>
</div>
<div class="toast" id="toast" role="alert" aria-live="polite">
<span class="toast-check" aria-hidden="true">✓</span>
<span id="toastMsg"></span>
</div>
<script>
const STRIPE_URL='https://buy.stripe.com/cNidRb6IkgfVeYlh2VejK00';
const STRIPE_PONCTUEL='https://buy.stripe.com/eVq6oJ6Ikd3JbM93c5ejK01';
const STRIPE_PORTAL='https://billing.stripe.com/p/login/test_cNidRb6IkgfVeYlh2VejK00';
const PLACES=20;
const FOUNDER_DAYS=60;
const FOUNDER_DEADLINE='2026-05-31'; // Date limite programme fondateur
// ── SUPABASE CLIENT ──
const SUPABASE_URL='https://citcbywbelgbwqvhxfok.supabase.co';
const SUPABASE_ANON_KEY='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImNpdGNieXdiZWxnYndxdmh4Zm9rIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzUyMjU3MDIsImV4cCI6MjA5MDgwMTcwMn0.bUzBymNAY14aZ-02_zXkYn8xlw4L_K0KxSsU-_xqaFk';
const _safeStorage={getItem:k=>{try{return localStorage.getItem(k);}catch(e){return null;}},setItem:(k,v)=>{try{localStorage.setItem(k,v);}catch(e){}},removeItem:k=>{try{localStorage.removeItem(k);}catch(e){}}};
const supa=window.supabase.createClient(SUPABASE_URL,SUPABASE_ANON_KEY,{auth:{storage:_safeStorage,autoRefreshToken:true,persistSession:true,detectSessionInUrl:true}});
// ── DETECTION RECOVERY IMMEDIATE (avant DOMContentLoaded) ──
let _recoveryHandled=false;
// Détecter type=recovery dans hash OU query params
const _hashStr=window.location.hash||'';
const _searchStr=window.location.search||'';
const _isRecoveryUrl=_hashStr.includes('type=recovery')||_searchStr.includes('type=recovery');
supa.auth.onAuthStateChange((event)=>{
if(event==='PASSWORD_RECOVERY'&&!_recoveryHandled){
_recoveryHandled=true;
window.history.replaceState(null,'','/');
if(document.readyState==='loading'){
document.addEventListener('DOMContentLoaded',()=>showNewPasswordForm());
}else{
showNewPasswordForm();
}
}
});
// Fallback : si l'URL contient type=recovery mais Supabase n'émet pas l'événement
// (token expiré, erreur, etc.), afficher le formulaire de demande de réinitialisation
if(_isRecoveryUrl){
setTimeout(()=>{
if(!_recoveryHandled){
// Pas de PASSWORD_RECOVERY reçu → token probablement expiré
const errHash=new URLSearchParams(_hashStr.substring(1)).get('error_description');
const errSearch=new URLSearchParams(_searchStr).get('error_description');
const err=errHash||errSearch;
window.history.replaceState(null,'','/');
if(document.readyState==='loading'){
document.addEventListener('DOMContentLoaded',()=>{
if(err){showToast(err.includes('expired')?'Ce lien a expiré. Demandez un nouveau lien.':'Erreur : '+err);}
else{showToast('Ce lien a expiré ou est invalide. Demandez un nouveau lien.');}
const outil=document.getElementById('outil');
if(outil)outil.scrollIntoView({behavior:'smooth'});
if(typeof showResetForm==='function')showResetForm();
});
}else{
if(err){showToast(err.includes('expired')?'Ce lien a expiré. Demandez un nouveau lien.':'Erreur : '+err);}
else{showToast('Ce lien a expiré ou est invalide. Demandez un nouveau lien.');}
const outil=document.getElementById('outil');
if(outil)outil.scrollIntoView({behavior:'smooth'});
if(typeof showResetForm==='function')showResetForm();
}
}
},3000); // 3s pour laisser le temps à Supabase
}
// Taux par défaut (fallback si API indisponible) — révisés 2×/an (S1 et S2)
let TAUX_BCE=2.15; // BCE refi (taux directeur)
let MAJORATION=10; // L441-10 — applicable aux entreprises commerciales (BCE+10pts)
let MAJORATION_ADMIN=8; // Décret 2013-269 — administrations (BCE+8pts uniquement)
let INDEMNITE_FORFAITAIRE=40; // D441-5 — uniquement régimes B2B (entreprise + admin)
let TAUX_LEGAL_AUTRES=3.28; // Taux légal "autres cas" (B2B sans CGV, libéraux)
let TAUX_LEGAL_PARTICULIER=7.18; // Taux légal "particuliers" (B2C)
let TAUX_SEMESTRE='S1 2026';
// Chargement dynamique des taux en vigueur
(async function loadTaux(){
if(location.protocol==='file:') return;
try{
const r=await fetch('/api/taux');
if(r.ok){
const t=await r.json();
if(t.taux_bce!=null) TAUX_BCE=t.taux_bce;
if(t.majoration!=null) MAJORATION=t.majoration;
if(t.majoration_admin!=null) MAJORATION_ADMIN=t.majoration_admin;
if(t.indemnite_forfaitaire!=null) INDEMNITE_FORFAITAIRE=t.indemnite_forfaitaire;
if(t.taux_legal_autres!=null) TAUX_LEGAL_AUTRES=t.taux_legal_autres;
if(t.taux_legal_particulier!=null)TAUX_LEGAL_PARTICULIER=t.taux_legal_particulier;
if(t.semestre) TAUX_SEMESTRE=t.semestre;
}
}catch(e){/* fallback sur valeurs par défaut */}
})();
const PROFILS={
grand_compte:{label:'Grand compte / ETI',icon:'🏢',desc:'Process administratif lourd'},
pme_fidele:{label:'PME fidèle',icon:'🤝',desc:'Relation de confiance'},
pme_nouveau:{label:'PME nouveau client',icon:'🆕',desc:'1ère ou 2ème mission'},
difficulte:{label:'Difficulté financière',icon:'💰',desc:'Trésorerie tendue'},
litige:{label:'Litige',icon:'⚖️',desc:'Conteste la prestation'},
silencieux:{label:'Client silencieux',icon:'🔇',desc:'Ne répond plus'}
};
const ETAPES=[
{type:'pre_echeance',label:'Rappel pré-échéance',offset:-3},
{type:'courtois',label:'Relance courtoise',offset:3},
{type:'ferme',label:'Relance ferme',offset:10},
{type:'penalites',label:'Pénalités chiffrées',offset:20},
{type:'contact_direct',label:'Demande de contact',offset:25},
{type:'mise_en_demeure',label:'Mise en demeure',offset:35},
{type:'echeancier',label:'Proposition échéancier',offset:50},
{type:'contentieux',label:'Annonce de recours professionnel',offset:60}
];
// Guide contextuel pour chaque étape
const ETAPES_GUIDE=[
{quand:'3 jours avant l\'échéance',quoi:'Un rappel amical avant la date limite. Ton chaleureux.',conseil:'Envoyez-le systématiquement : il montre votre sérieux et donne au client le temps de réagir sans pression.'},
{quand:'3 jours après l\'échéance',quoi:'Premier rappel post-échéance. Hypothèse d\'un simple oubli.',conseil:'Restez courtois : 80% des retards sont des oublis. Cet email suffit souvent à déclencher le paiement.'},
{quand:'10 jours après l\'échéance',quoi:'Relance ferme avec mention des obligations contractuelles.',conseil:'Changez de ton : passez du "rappel" à la "demande formelle". Mentionnez un délai de 5 jours.'},
{quand:'20 jours après l\'échéance',quoi:'Chiffrage exact des pénalités de retard (articles L441-10 et D441-5).',conseil:'Le montant concret des pénalités a un fort impact psychologique. C\'est souvent le déclencheur du paiement.'},
{quand:'25 jours après l\'échéance',quoi:'Demande de rendez-vous téléphonique pour comprendre la situation.',conseil:'Si le client ne répond pas aux emails, proposez un appel. Cela montre votre volonté de résoudre à l\'amiable.'},
{quand:'35 jours après l\'échéance',quoi:'Mise en demeure formelle avec formalisme juridique.',conseil:'Envoyez-la en LRAR (lettre recommandée) pour valeur probatoire. C\'est un prérequis avant toute action judiciaire.'},
{quand:'50 jours après l\'échéance',quoi:'Dernière main tendue : proposition d\'échéancier en 2-3 versements.',conseil:'Un client en difficulté acceptera souvent un échéancier plutôt que de risquer une procédure. Dernière tentative amiable.'},
{quand:'60 jours après l\'échéance',quoi:'Annonce d\'un recours professionnel (cabinet de recouvrement).',conseil:'C\'est le dernier levier amiable. Si ça ne fonctionne pas, transmettez le dossier à un professionnel.'}
];
function addJours(dateStr,jours){const d=new Date(dateStr);d.setDate(d.getDate()+jours);return d.toISOString().split('T')[0];}
function safeArray(arr){return Array.isArray(arr)?arr:[];}
function normalizeEtape(e,num){
if(!e||typeof e!=='object') return {num:typeof num==='number'?num:0,type:'',label:'',dateEnvoi:'',statut:'a_envoyer',sujet:'',corps:'',genere:false};
return {
num:typeof e.num==='number'?e.num:(typeof num==='number'?num:0),
type:e.type||'',
label:e.label||'',
dateEnvoi:e.dateEnvoi||'',
statut:e.statut||'a_envoyer',
sujet:e.sujet||'',
corps:e.corps||'',
genere:!!e.genere,
dateEnvoiEffective:e.dateEnvoiEffective||''
};
}
function normalizeCreance(c){
if(!c||typeof c!=='object') return null;
const etapes=safeArray(c.etapes).map((e,i)=>normalizeEtape(e,i));
return {
...c,
id:c.id||genId(),
client:c.client||'',
clientEmail:c.clientEmail||c.client_email||'',
montant:Number(c.montant||0),
facture:c.facture||'',
dateEcheance:c.dateEcheance||c.date_echeance||'',
profil:c.profil||'',
contexte:c.contexte||'',
statut:c.statut||'en_relance',
etapes,
penalites:c.penalites&&typeof c.penalites==='object'?c.penalites:{},
legalConsent:!!(c.legalConsent||c.legal_consent),
legalConsentDate:c.legalConsentDate||c.legal_consent_date||null,
createdAt:c.createdAt||c.created_at||new Date().toISOString(),
outcome:c.outcome||null,
montant_escalade:c.montant_escalade!=null?parseFloat(c.montant_escalade):null,
montant_escalade_at:c.montant_escalade_at||null,
clientCompanyId:c.clientCompanyId||c.client_company_id||null,
// Régime juridique du débiteur (b2b par défaut pour rétrocompat) — détermine le calcul exact des pénalités
regimeDebiteur:(c.regimeDebiteur||c.regime_debiteur||'b2b').toLowerCase(),
// Snapshot du taux applicable au moment de la création (audit historique fiable)
tauxApplique:c.tauxApplique!=null?parseFloat(c.tauxApplique):(c.taux_applique!=null?parseFloat(c.taux_applique):null)
};
}
function diffJours(a,b){return Math.round((new Date(a)-new Date(b))/(864e5));}
function fmtDate(d){if(!d)return'—';const p=new Date(d);return p.toLocaleDateString('fr-FR',{day:'numeric',month:'short',year:'numeric'});}
function fmtMontant(n){return parseFloat(n).toLocaleString('fr-FR',{minimumFractionDigits:2,maximumFractionDigits:2})+' €';}
function esc(s){if(!s)return'';return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');}
// Alias historique : plusieurs vues (RECOVMAX, clients, templates) utilisent escHtml
const escHtml=esc;
window.escHtml=escHtml;
function genId(){return 'cr_'+Date.now()+'_'+Math.random().toString(36).substr(2,4);}
// ── STORE (Supabase + localStorage cache) ──
const Store={
_data:null,
_key:'recovr_data',
_SCHEMA_VERSION:3,
_SESSION_MAX_DAYS:60,
_AI_DAILY_LIMIT:10,
_AI_TOTAL_LIMIT:200,
_userId:null, // Supabase auth user id
_default(){return{user:{email:'',nom:'',societe:'',adresse:'',siret:'',ville:'',createdAt:'',objectifMensuel:0},creances:[],stats:{totalRecupere:0,totalCreances:0,totalEnvoyes:0},aiUsage:{today:0,todayDate:'',total:0},schemaVersion:this._SCHEMA_VERSION,lastActivity:new Date().toISOString()};},
init(){
this.migrate();
const raw=localStorage.getItem(this._key);
this._data=raw?JSON.parse(raw):this._default();
if(!this._data.stats)this._data.stats={totalRecupere:0,totalCreances:0,totalEnvoyes:0};
if(!this._data.creances)this._data.creances=[];
if(!this._data.aiUsage)this._data.aiUsage={today:0,todayDate:'',total:0};
if(!this._data.lastActivity)this._data.lastActivity=new Date().toISOString();
this._data.creances=safeArray(this._data.creances).map(normalizeCreance).filter(Boolean);
if(!this._data.schemaVersion||this._data.schemaVersion<this._SCHEMA_VERSION){
this._data.schemaVersion=this._SCHEMA_VERSION;this.save();
}
this.touchActivity();
},
// Sync depuis Supabase après login
async syncFromSupabase(){
if(!this._userId)return;
// Timeout de sécurité : 8 secondes max pour le sync
const timeout=new Promise(r=>setTimeout(()=>r('timeout'),8000));
const sync=this._doSyncFromSupabase();
const result=await Promise.race([sync,timeout]);
if(result==='timeout')console.warn('Sync Supabase timeout — using local cache');
},
async _doSyncFromSupabase(){
if(!this._userId)return;
try{
// Charger profil (maybeSingle au lieu de single pour éviter l'erreur si vide)
const{data:profile}=await supa.from('user_profiles').select('*').eq('id',this._userId).maybeSingle();
if(profile){
// DB = source de vérité MAIS protection : si une sauvegarde profil vient
// d'avoir lieu (<30s) ET que la DB n'a pas encore la valeur, on garde le
// local (évite la perte de donnée pendant le push async en cours).
const recentSave=Store._lastProfileSaveAt&&(Date.now()-Store._lastProfileSaveAt)<30000;
const keepLocal=(field,dbVal)=>recentSave&&this._data.user[field]&&!dbVal?this._data.user[field]:(dbVal||'');
this._data.user.nom=keepLocal('nom',profile.nom);
this._data.user.societe=keepLocal('societe',profile.societe);
this._data.user.adresse=keepLocal('adresse',profile.adresse);
this._data.user.siret=keepLocal('siret',profile.siret);
this._data.user.ville=keepLocal('ville',profile.ville);
this._data.user.lienPaiement=keepLocal('lienPaiement',profile.lien_paiement);
this._data.aiUsage.today=profile.ai_last_date===new Date().toISOString().split('T')[0]?profile.ai_daily_count:0;
this._data.aiUsage.todayDate=profile.ai_last_date||'';
this._data.aiUsage.total=profile.ai_total_count||0;
// Plan + quotas (migration 018 + 056)
this._data.user.plan=profile.plan||'solo';
this._data.user.sirenCreancier=profile.siren_creancier||null;
this._data.user.quotas={
creances:profile.quota_creances_count_month||0,
relances:profile.quota_relances_count_month||0,
clients:profile.quota_clients_count_month||0,
med:profile.quota_med_count_month||0,
reset_at:profile.quota_reset_at||null
};
this._data.user.stripeSubId=profile.stripe_subscription_id||null;
// Update badge si UI montée
try{ if(typeof updatePlanBadge==='function') updatePlanBadge(); }catch(e){}
}
// Charger créances
const{data:creances}=await supa.from('creances').select('*').eq('user_id',this._userId).order('created_at',{ascending:false});
if(creances&&creances.length>0){
this._data.creances=creances.map(c=>normalizeCreance({
id:c.id,
client:c.client,
clientEmail:c.client_email||'',
montant:parseFloat(c.montant),
facture:c.facture,
dateEcheance:c.date_echeance,
profil:c.profil,
contexte:c.contexte||'',
statut:c.statut||'en_relance',
etapes:c.etapes||[],
penalites:c.penalites||{},
legalConsent:c.legal_consent||false,
legalConsentDate:c.legal_consent_date||null,
createdAt:c.created_at,
outcome:c.outcome||null,
montant_escalade:c.montant_escalade!=null?parseFloat(c.montant_escalade):null,
montant_escalade_at:c.montant_escalade_at||null,
regimeDebiteur:c.regime_debiteur||'b2b',
tauxApplique:c.taux_applique!=null?parseFloat(c.taux_applique):null,
clientCompanyId:c.client_company_id||null
})).filter(Boolean);
} else {
this._data.creances=[];
}
this.recalcStats();
this.save();
}catch(e){console.warn('Sync Supabase failed, using local cache:',e);}
},
// Push les créances locales vers Supabase (migration one-shot)
async _pushLocalCreancesToSupabase(){
if(!this._userId||!this._data.creances.length)return;
const rows=this._data.creances.map(c=>({
user_id:this._userId,
client:c.client,
client_email:c.clientEmail||null,
montant:c.montant,
facture:c.facture,
date_echeance:c.dateEcheance,
profil:c.profil,
contexte:c.contexte||null,
statut:c.statut||'en_relance',
etapes:c.etapes||[],
penalites:c.penalites||{},
legal_consent:c.legalConsent||false,
legal_consent_date:c.legalConsentDate||null
}));
const{data,error}=await supa.from('creances').insert(rows).select();
if(data&&!error){
// Mettre à jour les IDs locaux avec les UUIDs Supabase
this._data.creances=data.map(c=>({
id:c.id,client:c.client,clientEmail:c.client_email||'',montant:parseFloat(c.montant),
facture:c.facture,dateEcheance:c.date_echeance,profil:c.profil,contexte:c.contexte||'',
statut:c.statut,etapes:c.etapes||[],penalites:c.penalites||{},
legalConsent:c.legal_consent,legalConsentDate:c.legal_consent_date,createdAt:c.created_at
}));
this.save();
console.log('[RECOV] Migration localStorage → Supabase OK:',data.length,'créances');
}
},
// Push profil vers Supabase (UPDATE direct, fallback INSERT si ligne absente)
// Normalise les chaînes vides en null (PG peut rejeter '' selon le typage colonne).
async _syncProfileToSupabase(){
if(!this._userId)return{ok:true,skipped:true};
const u=this._data.user;
// Ne pas écraser des données réelles avec un profil vide (ex: init connexion)
if(!u.nom&&!u.siret)return{ok:true,skipped:true};
Store._lastProfileSaveAt=Date.now();
// Normalise : chaînes vides → null (évite les rejets de contraintes / triggers PG)
const nn=function(v){return (v===undefined||v===null||(typeof v==='string'&&v.trim()===''))?null:v;};
const payload={
email:nn(u.email),
nom:nn(u.nom),
societe:nn(u.societe),
adresse:nn(u.adresse),
siret:nn(u.siret),
ville:nn(u.ville),
lien_paiement:nn(u.lienPaiement)
};
try{
// 1) UPDATE direct sur la ligne existante (cas standard : profil créé à l'inscription)
const{data:updData,error:updErr}=await supa.from('user_profiles')
.update(payload).eq('id',this._userId).select('id');
if(!updErr&&updData&&updData.length>0){
return{ok:true};
}
if(updErr){
console.error('[Store/_syncProfileToSupabase] UPDATE error:',updErr);
// Si erreur non récupérable (RLS, type, check), on remonte
return{ok:false,error:updErr};
}
// 2) Aucune ligne mise à jour → on tente un INSERT (cas Google OAuth sans /api/register)
console.warn('[Store/_syncProfileToSupabase] No row updated, trying INSERT…');
const{error:insErr}=await supa.from('user_profiles').insert({
id:this._userId,
...payload
});
if(insErr){
console.error('[Store/_syncProfileToSupabase] INSERT error:',insErr);
return{ok:false,error:insErr};
}
return{ok:true};
}catch(e){
console.error('[Store/_syncProfileToSupabase] exception:',e);
return{ok:false,error:e};
}
},
touchActivity(){this._data.lastActivity=new Date().toISOString();this.save();},
migrate(){
const oldU=localStorage.getItem('recovr_u');
const oldS=localStorage.getItem('recovr_s');
if(oldU&&!localStorage.getItem(this._key)){
const d=this._default();
d.user.email=oldU;d.user.createdAt=new Date().toISOString();
if(oldS){try{const s=JSON.parse(oldS);d.stats.totalRecupere=s.cash||0;d.stats.totalEnvoyes=s.sequences||0;}catch(e){}}
localStorage.setItem(this._key,JSON.stringify(d));
localStorage.removeItem('recovr_u');localStorage.removeItem('recovr_s');
}
},
getAll(){return this._data||this._default();},
getCreances(filtre){
const list=safeArray(this._data&&this._data.creances).map(normalizeCreance).filter(Boolean);
if(this._data)this._data.creances=list;
return filtre?list.filter(c=>c.statut===filtre):list;
},
getCreance(id){
return this.getCreances().find(c=>c.id===id)||null;
},
async addCreance({client,clientEmail,montant,facture,dateEcheance,profil,contexte,client_company_id,regime_debiteur,taux_applique,clientSiren,clientScoring}){
// ─── Limite démo : 2 créances max pour utilisateurs non inscrits ───
if(!this._userId){
const currentCount=(this._data?.creances||[]).length;
if(currentCount>=2){
if(typeof showDemoLimitModal==='function') showDemoLimitModal();
return null; // signal au caller : limite atteinte, modal affiché
}
}
// ─── RECOVMAX : client_company_id OBLIGATOIRE (multi-clients) ──────────
const _plan=this._data?.user?.plan||'fondateur';
if(_plan==='recovmax'&&!client_company_id){
if(typeof showToast==='function')showToast('⚠️ Sélectionnez un client avant de créer la créance');
return null;
}
// ─── Check quota créances (Solo 1 active / Freelance 1 / RECOVMAX 30) ───
// Migration 057 : pour Solo, le backend bloque aussi via trigger DB
if(_plan==='solo'||_plan==='freelance'||_plan==='recovmax'){
const cap=Store.quota.cap('creances',_plan);
const usage=Store.quota.usage('creances');
if(usage>=cap){
if(typeof showQuotaCapModal==='function')showQuotaCapModal('creances');
else if(typeof showToast==='function')showToast(`⚠️ Quota ${cap} créance${cap>1?'s':''} atteint — passez à RECOVMAX pour multi-clients`);
return null;
}
}
// Validation crédibilité : email client ≠ email user (évite "toto/titi" incohérents)
const userEmail=(this._data&&this._data.user&&this._data.user.email)||'';
if(clientEmail&&userEmail&&clientEmail.trim().toLowerCase()===userEmail.trim().toLowerCase()){
if(!confirm("\u26a0 L\u2019email du client est identique au v\u00f4tre.\n\nLes relances envoy\u00e9es \u00e0 votre propre adresse ne serviront \u00e0 rien en production. \u00cates-vous s\u00fbr de vouloir continuer ? (OK = continuer malgr\u00e9 tout / Annuler = modifier)")){
throw new Error("Email client identique \u00e0 l\u2019email utilisateur \u2014 cr\u00e9ance non cr\u00e9\u00e9e");
}
}
if(client&&/^(toto|titi|test|xxx|aaa|bbb|ccc|demo)$/i.test(client.trim())){
if(!confirm("\u26a0 Le nom du client semble \u00eatre un nom de test (\u00ab "+client+" \u00bb).\n\nV\u00e9rifiez qu\u2019il s\u2019agit bien du nom r\u00e9el de votre client. Les relances citant \u00ab toto \u00bb ou \u00ab test \u00bb perdent toute cr\u00e9dibilit\u00e9. Continuer quand m\u00eame ?")){
throw new Error("Nom client trop g\u00e9n\u00e9rique \u2014 cr\u00e9ance non cr\u00e9\u00e9e");
}
}
const _regime=(regime_debiteur||'b2b').toLowerCase();
const pen=Penalites.calculer(montant,dateEcheance,new Date().toISOString().split('T')[0],{regime:_regime,tauxFige:taux_applique});
const etapes=ETAPES.map((e,i)=>({
num:i,type:e.type,label:e.label,
dateEnvoi:addJours(dateEcheance,e.offset),
statut:'a_envoyer',sujet:'',corps:'',genere:false
}));
// Fallback: récupérer user_id depuis la session si pas encore set
if(!this._userId&&typeof supa!=='undefined'){
try{const{data}=await supa.auth.getUser();if(data?.user?.id)this._userId=data.user.id;}catch(e){}
}
// Capture attribution partenaire pour la créance (multi-tenant)
const _attrib=(typeof getPartnerAttribution==='function')?getPartnerAttribution():{};
const _partnerSlug=_attrib.partner||null;
console.log('[RECOV/addCreance] partner attribution:',_partnerSlug||'NONE',_attrib);
// INSERT Supabase
if(this._userId){
const _payload={
user_id:this._userId,client,client_email:clientEmail||null,
montant:parseFloat(montant),facture,date_echeance:dateEcheance,
profil,contexte:contexte||null,statut:'en_relance',etapes,penalites:pen,
client_company_id:client_company_id||null,
regime_debiteur:_regime,
taux_applique:typeof taux_applique==='number'?taux_applique:pen.tauxApplicable
};
// Lookup partner_id si on a un slug (best-effort, n'échoue pas la créance si la requête plante)
if(_partnerSlug){
try{
const{data:p,error:pErr}=await supa.from('partners').select('id').eq('slug',_partnerSlug).in('status',['pilot','active']).limit(1).maybeSingle();
if(p?.id){_payload.partner_id=p.id;console.log('[RECOV/addCreance] partner_id attribué:',p.id);}
else console.warn('[RECOV/addCreance] partner_id non trouvé pour slug:',_partnerSlug,pErr);
}catch(e){console.error('[RECOV/addCreance] erreur lookup partner:',e.message);}
}
const{data,error}=await supa.from('creances').insert(_payload).select().single();
if(data&&!error){
const creance=normalizeCreance({id:data.id,client,clientEmail:clientEmail||'',montant:parseFloat(montant),facture,dateEcheance,profil,contexte:contexte||'',
statut:'en_relance',etapes,penalites:pen,createdAt:data.created_at,clientCompanyId:client_company_id||null});
this._data.creances.unshift(creance);
this.recalcStats();this.save();this.sendBackup();
// ─── RECOVMAX : audit log + quota increment ───
try{
await Store.audit.log('creance.create',{creance_id:data.id,client_company_id:client_company_id||null,details:{client,montant:parseFloat(montant),facture}});
if(_plan==='freelance'||_plan==='recovmax')await Store.quota.increment('creance');
}catch(e){console.warn('[addCreance] audit/quota silent:',e.message);}
return creance;
}
}
// Fallback localStorage
const id=genId();
const creance=normalizeCreance({id,client,clientEmail:clientEmail||'',montant:parseFloat(montant),facture,dateEcheance,profil,contexte:contexte||'',
statut:'en_relance',etapes,penalites:pen,createdAt:new Date().toISOString()});
this._data.creances.unshift(creance);
this.recalcStats();this.save();this.sendBackup();
return creance;
},
async updateCreance(id,updates){
const c=this.getCreance(id);if(!c)return;
Object.assign(c,updates);this.save();
if(this._userId){
const supaUpdates={};
if(updates.client!==undefined)supaUpdates.client=updates.client;
if(updates.clientEmail!==undefined)supaUpdates.client_email=updates.clientEmail;
if(updates.montant!==undefined)supaUpdates.montant=updates.montant;
if(updates.statut!==undefined)supaUpdates.statut=updates.statut;
if(updates.contexte!==undefined)supaUpdates.contexte=updates.contexte;
if(updates.montant_escalade!==undefined)supaUpdates.montant_escalade=updates.montant_escalade;
if(updates.montant_escalade_at!==undefined)supaUpdates.montant_escalade_at=updates.montant_escalade_at;
if(Object.keys(supaUpdates).length>0){
supa.from('creances').update(supaUpdates).eq('id',id).eq('user_id',this._userId).then(()=>{}).catch(()=>{});
}
}
},
async deleteCreance(id){
// Supprimer localement immédiatement
this._data.creances=this._data.creances.filter(c=>c.id!==id);
this.recalcStats();this.save();
// Supprimer dans Supabase — attendre la réponse pour ne pas perdre la synchro
if(this._userId){
try{
const{error}=await supa.from('creances').delete().eq('id',id).eq('user_id',this._userId);
if(error)console.error('Supabase delete error:',error);
}catch(e){console.error('Supabase delete exception:',e);}
}
},
async updateEtape(creanceId,etapeNum,updates){
const c=this.getCreance(creanceId);if(!c)return;
c.etapes=safeArray(c.etapes).map((e,i)=>normalizeEtape(e,i));
const e=c.etapes.find(e=>e.num===etapeNum);if(!e)return;
Object.assign(e,updates);this.save();
// Sync etapes JSONB vers Supabase
if(this._userId){
supa.from('creances').update({etapes:c.etapes}).eq('id',creanceId).eq('user_id',this._userId).then(()=>{}).catch(()=>{});
}
// Snapshot escalade : si on marque l'étape 8 (index 7) comme envoyée,
// on gèle le total (principal + pénalités + indemnité) pour le dossier juridique
if(etapeNum===7&&updates.statut==='envoye'&&!c.montant_escalade){
const today=new Date().toISOString().split('T')[0];
const pen=Penalites.calculer(c.montant,c.dateEcheance,today,{regime:c.regimeDebiteur||'b2b',tauxFige:c.tauxApplique});
const snapshotAt=new Date().toISOString();
c.montant_escalade=pen.totalDu;
c.montant_escalade_at=snapshotAt;
this.save();
if(this._userId){
supa.from('creances').update({
montant_escalade:pen.totalDu,
montant_escalade_at:snapshotAt
}).eq('id',creanceId).eq('user_id',this._userId).then(()=>{}).catch(()=>{});
}
}
},
async marquerPaye(id){
const c=this.getCreance(id);if(!c)return;
c.statut='paye';
c.paidAt=new Date().toISOString();
c.etapes=safeArray(c.etapes).map((e,i)=>normalizeEtape(e,i));
c.etapes.forEach(e=>{if(e.statut==='a_envoyer')e.statut='ignore';});
this.recalcStats();this.save();this.sendBackup();
if(this._userId){
supa.from('creances').update({statut:'paye',etapes:c.etapes}).eq('id',id).eq('user_id',this._userId).then(()=>{}).catch(()=>{});
// Sync stats dans profil
supa.from('user_profiles').update({
stats_total_recupere:this._data.stats.totalRecupere,
stats_total_creances:this._data.stats.totalCreances,
stats_total_envoyes:this._data.stats.totalEnvoyes
}).eq('id',this._userId).then(()=>{}).catch(()=>{});
}
},
// Montant récupéré ce mois-ci (basé sur paidAt)
getMontantMoisCourant(){
const now=new Date();
const ym=`${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}`;
return safeArray(this._data.creances)
.filter(c=>c.statut==='paye'&&c.paidAt&&c.paidAt.startsWith(ym))
.reduce((acc,c)=>acc+(parseFloat(c.montant)||0),0);
},
sendBackup(){
const user=this._data.user;
if(!user||!user.email)return;
const now=Date.now();
if(this._lastBackup&&now-this._lastBackup<30000)return;
this._lastBackup=now;
const _data=this._data; // capture pour preserver le contexte
this.getAccessToken().then((tk)=>{
if(!tk){console.warn('[backup] no access token, skip');return;}
fetch('/api/backup',{
method:'POST',
headers:{'Content-Type':'application/json','Authorization':'Bearer '+tk},
body:JSON.stringify({email:user.email,data:_data})
}).catch(()=>{});
}).catch(()=>{});
},
getUser(){return this._data.user;},
async updateUser(updates){
Object.assign(this._data.user,updates);this.save();
return await this._syncProfileToSupabase();
},
recalcStats(){
const cr=this._data.creances;
this._data.stats.totalCreances=cr.length;
this._data.stats.totalRecupere=cr.filter(c=>c.statut==='paye').reduce((s,c)=>s+c.montant,0);
this._data.stats.totalEnvoyes=cr.reduce((s,c)=>s+c.etapes.filter(e=>e.statut==='envoye').length,0);
},
save(){localStorage.setItem(this._key,JSON.stringify(this._data));},
canUseAI(){
const u=this._data.aiUsage;
const today=new Date().toISOString().split('T')[0];
if(u.todayDate!==today){u.today=0;u.todayDate=today;this.save();}
if(u.today>=this._AI_DAILY_LIMIT)return{ok:false,reason:'Limite journalière atteinte ('+this._AI_DAILY_LIMIT+' générations/jour). Réessayez demain.'};
if(u.total>=this._AI_TOTAL_LIMIT)return{ok:false,reason:'Quota total atteint ('+this._AI_TOTAL_LIMIT+' générations). Passez à un abonnement payant pour continuer.'};
return{ok:true,dailyRemaining:this._AI_DAILY_LIMIT-u.today,totalRemaining:this._AI_TOTAL_LIMIT-u.total};
},
async trackAIUsage(){
const u=this._data.aiUsage;
const today=new Date().toISOString().split('T')[0];
if(u.todayDate!==today){u.today=0;u.todayDate=today;}
u.today++;u.total++;
this.touchActivity();
// Sync quotas serveur
if(this._userId){
supa.from('user_profiles').update({
ai_daily_count:u.today,ai_total_count:u.total,ai_last_date:today
}).eq('id',this._userId).then(()=>{}).catch(()=>{});
}
},
getAIUsage(){
const u=this._data.aiUsage;
const today=new Date().toISOString().split('T')[0];
if(u.todayDate!==today)return{today:0,total:u.total};
return{today:u.today,total:u.total};
},
getFounderDaysLeft(){
const now=new Date();
const deadline=new Date(FOUNDER_DEADLINE+'T23:59:59');
return Math.max(0,Math.ceil((deadline-now)/(864e5)));
},
isFounderExpired(){return this.getFounderDaysLeft()<=0;},
exportJSON(){return JSON.stringify(this._data,null,2);},
importJSON(json){
try{this._data=JSON.parse(json);this.save();return true;}catch(e){return false;}
},
// Obtenir le JWT pour les appels API
async getAccessToken(){
const{data}=await supa.auth.getSession();
return data?.session?.access_token||null;
},
// ═══════════════════════════════════════════════════════════════════════════
// RECOVMAX HELPERS (J1 — Phase 1.8 + 3 du plan stratégie)
// Migrations 018 + 019 appliquées
// RLS : auth.uid() = user_id (Supabase gère, pas de check côté code nécessaire)
// ═══════════════════════════════════════════════════════════════════════════
// ── client_companies (CRUD multi-clients RECOVMAX) ─────────────────────────
clients:{
async list(includeArchived=false){
if(!Store._userId)return[];
let q=supa.from('client_companies').select('*').eq('user_id',Store._userId);
if(!includeArchived)q=q.eq('archived',false);
const{data,error}=await q.order('nom',{ascending:true});
if(error){console.error('[RECOVMAX/clients.list]',error);return[];}
return data||[];
},
async getById(id){
if(!Store._userId||!id)return null;
const{data,error}=await supa.from('client_companies').select('*').eq('id',id).eq('user_id',Store._userId).maybeSingle();
if(error){console.error('[RECOVMAX/clients.getById]',error);return null;}
return data;
},
async create(payload){
if(!Store._userId)throw new Error('Non authentifié');
if(!payload?.nom||!payload.nom.trim())throw new Error('Le nom du client est obligatoire');
// ─── Check quota clients (Solo 1 / Freelance 1 / RECOVMAX 50) ──────────
// Migration 057 : pour Solo, le backend bloque aussi via trigger DB
const plan=Store._data?.user?.plan||'fondateur';
if(plan==='solo'||plan==='freelance'){
// Pour Solo, on compte le TOTAL existant (pas le quota mensuel)
const{count,error:countErr}=await supa
.from('client_companies')
.select('*',{count:'exact',head:true})
.eq('user_id',Store._userId);
if(countErr){
console.warn('[RECOVMAX/clients.create] count check failed:',countErr.message);
}else if((count||0)>=1){
if(typeof showQuotaCapModal==='function')showQuotaCapModal('clients');
throw new Error('Limite 1 client unique atteinte — passez à RECOVMAX pour multi-clients');
}
}
if(plan==='recovmax'){
const usage=Store._data?.user?.quotas?.clients||0;
if(usage>=50){
if(typeof showQuotaCapModal==='function')showQuotaCapModal('clients');
throw new Error('Limite 50 clients distincts atteinte ce mois');
}
}
const insertPayload={
user_id:Store._userId,
nom:payload.nom.trim(),
forme_juridique:payload.forme_juridique||null,
siret:payload.siret?String(payload.siret).replace(/\s/g,''):null,
email_contact:payload.email_contact||null,
telephone:payload.telephone||null,
adresse:payload.adresse||null,
code_postal:payload.code_postal||null,
ville:payload.ville||null,
iban:payload.iban?String(payload.iban).replace(/\s/g,''):null,
bic:payload.bic||null,
conditions_paiement_default:payload.conditions_paiement_default||30,
taux_penalite_default:payload.taux_penalite_default||null,
notes:payload.notes||null
};
const{data,error}=await supa.from('client_companies').insert(insertPayload).select().single();
if(error){console.error('[RECOVMAX/clients.create]',error);throw new Error(error.message||'Création client échouée');}
// Log audit + increment quota
await Store.audit.log('client.create',{client_company_id:data.id,nom:data.nom});
if(plan==='recovmax')await Store.quota.increment('client');
return data;
},
async update(id,payload){
if(!Store._userId||!id)throw new Error('id manquant');
const updates={};
['nom','forme_juridique','siret','email_contact','telephone','adresse','code_postal','ville','iban','bic','conditions_paiement_default','taux_penalite_default','notes'].forEach(k=>{
if(payload[k]!==undefined)updates[k]=payload[k];
});
if(updates.siret)updates.siret=String(updates.siret).replace(/\s/g,'');
if(updates.iban)updates.iban=String(updates.iban).replace(/\s/g,'');
const{data,error}=await supa.from('client_companies').update(updates).eq('id',id).eq('user_id',Store._userId).select().single();
if(error){console.error('[RECOVMAX/clients.update]',error);throw new Error(error.message);}
await Store.audit.log('client.update',{client_company_id:id,fields:Object.keys(updates)});
return data;
},
async archive(id){
if(!Store._userId||!id)return false;
const{error}=await supa.from('client_companies').update({archived:true,archived_at:new Date().toISOString()}).eq('id',id).eq('user_id',Store._userId);
if(error){console.error('[RECOVMAX/clients.archive]',error);return false;}
await Store.audit.log('client.archive',{client_company_id:id});
return true;
}
},
// ── relance_templates (templates par client OU global) ─────────────────────
templates:{
async list(clientCompanyId){
if(!Store._userId)return[];
let q=supa.from('relance_templates').select('*').eq('user_id',Store._userId);
if(clientCompanyId===null)q=q.is('client_company_id',null); // templates globaux
else if(clientCompanyId)q=q.eq('client_company_id',clientCompanyId);
const{data,error}=await q.order('etape_num',{ascending:true,nullsFirst:false});
if(error){console.error('[RECOVMAX/templates.list]',error);return[];}
return data||[];
},
async getForClient(clientCompanyId,etapeNum){
if(!Store._userId)return null;
// 1. Spécifique au client + étape
if(clientCompanyId){
const{data:t1}=await supa.from('relance_templates').select('*').eq('user_id',Store._userId).eq('client_company_id',clientCompanyId).eq('etape_num',etapeNum).maybeSingle();
if(t1)return t1;
// 2. Spécifique au client (toutes étapes)
const{data:t2}=await supa.from('relance_templates').select('*').eq('user_id',Store._userId).eq('client_company_id',clientCompanyId).is('etape_num',null).maybeSingle();
if(t2)return t2;
}
// 3. Global du user pour cette étape
const{data:t3}=await supa.from('relance_templates').select('*').eq('user_id',Store._userId).is('client_company_id',null).eq('etape_num',etapeNum).maybeSingle();
if(t3)return t3;
// 4. Global du user (toutes étapes)
const{data:t4}=await supa.from('relance_templates').select('*').eq('user_id',Store._userId).is('client_company_id',null).is('etape_num',null).maybeSingle();
return t4||null; // si null, le code utilisera le template par défaut RECOV
},
async create(payload){
if(!Store._userId)throw new Error('Non authentifié');
if(!payload?.nom||!payload?.corps_email)throw new Error('Nom et corps email obligatoires');
const insertPayload={
user_id:Store._userId,
client_company_id:payload.client_company_id||null,
nom:payload.nom.trim(),
ton:payload.ton||'cordial',
etape_num:payload.etape_num||null,
sujet_email:payload.sujet_email||null,
corps_email:payload.corps_email,
corps_demeure_pdf:payload.corps_demeure_pdf||null,
is_default:!!payload.is_default
};
const{data,error}=await supa.from('relance_templates').insert(insertPayload).select().single();
if(error){console.error('[RECOVMAX/templates.create]',error);throw new Error(error.message);}
await Store.audit.log('template.create',{template_id:data.id,client_company_id:data.client_company_id});
return data;
},
async update(id,payload){
if(!Store._userId||!id)throw new Error('id manquant');
const updates={};
['nom','ton','etape_num','sujet_email','corps_email','corps_demeure_pdf','is_default'].forEach(k=>{
if(payload[k]!==undefined)updates[k]=payload[k];
});
const{data,error}=await supa.from('relance_templates').update(updates).eq('id',id).eq('user_id',Store._userId).select().single();
if(error){console.error('[RECOVMAX/templates.update]',error);throw new Error(error.message);}
await Store.audit.log('template.update',{template_id:id});
return data;
},
async delete(id){
if(!Store._userId||!id)return false;
const{error}=await supa.from('relance_templates').delete().eq('id',id).eq('user_id',Store._userId);
if(error){console.error('[RECOVMAX/templates.delete]',error);return false;}
await Store.audit.log('template.delete',{template_id:id});
return true;
}
},
// ── audit_log (traçabilité immuable) ───────────────────────────────────────
audit:{
async log(action,ctx={}){
if(!Store._userId||!action)return;
try{
await supa.from('audit_log').insert({
user_id:Store._userId,
client_company_id:ctx.client_company_id||null,
creance_id:ctx.creance_id||null,
action:String(action),
target_type:ctx.target_type||null,
target_id:ctx.target_id||null,
details:ctx.details||{...ctx} // tout le ctx restant en JSONB
});
}catch(e){console.warn('[RECOVMAX/audit.log] silently failed:',e.message);}
},
async list(filters={}){
if(!Store._userId)return[];
let q=supa.from('audit_log').select('*').eq('user_id',Store._userId);
if(filters.client_company_id)q=q.eq('client_company_id',filters.client_company_id);
if(filters.creance_id)q=q.eq('creance_id',filters.creance_id);
if(filters.action)q=q.eq('action',filters.action);
const{data}=await q.order('created_at',{ascending:false}).limit(filters.limit||100);
return data||[];
}
},
// ── quota tracking (RPC vers fonction Postgres increment_quota) ────────────
quota:{
_CAPS:{
// V1 lancement (5 mai 2026) : caps réduits pour valider marché avant scaling
// V2 future : RECOVMAX Pro/Cabinet avec 50/200/1000 (à 29-39€/mois)
demo: {creances:1, relances:Infinity,clients:1, med_month:1},
fondateur: {creances:Infinity,relances:Infinity,clients:1, med_month:Infinity},
// Aliases historiques (avant migration 018) : traités comme fondateur
founder: {creances:Infinity,relances:Infinity,clients:1, med_month:Infinity},
direct: {creances:Infinity,relances:Infinity,clients:1, med_month:Infinity},
partner: {creances:Infinity,relances:Infinity,clients:1, med_month:Infinity},
// RECOV Solo (gratuit, usage personnel) : 1 créance active à la fois,
// 1 mise en demeure par mois, séquence de relances complète, 1 SIREN.
solo: {creances:1, relances:Infinity,clients:1, med_month:1},
// Alias historique 'freelance' → traité comme Solo (pour comptes existants)
freelance: {creances:1, relances:Infinity,clients:1, med_month:1},
recovmax: {creances:30, relances:300, clients:10, med_month:Infinity}
},
cap(kind,plan){
const p=plan||Store._data?.user?.plan||'fondateur';
return this._CAPS[p]?.[kind]??Infinity;
},
usage(kind){
const q=Store._data?.user?.quotas||{};
return q[kind]||0;
},
percent(kind){
const cap=this.cap(kind);
if(!isFinite(cap)||cap===0)return 0;
return Math.min(100,Math.round((this.usage(kind)/cap)*100));
},
isAtCap(kind){
return this.usage(kind)>=this.cap(kind);
},
maxPercent(){
return Math.max(this.percent('creances'),this.percent('relances'),this.percent('clients'));
},
async increment(kind){
if(!Store._userId)return;
// Map 'creance' → 'creances' (singulier en JS, pluriel en DB)
const dbKind=kind==='creance'?'creance':kind==='relance'?'relance':kind==='client'?'client':kind;
try{
const{data,error}=await supa.rpc('increment_quota',{p_user_id:Store._userId,p_kind:dbKind});
if(error){console.error('[RECOVMAX/quota.increment]',error);return;}
if(data){
// Refresh local quotas
if(!Store._data.user.quotas)Store._data.user.quotas={};
Store._data.user.quotas.creances=data.quota_creances_count_month||0;
Store._data.user.quotas.relances=data.quota_relances_count_month||0;
Store._data.user.quotas.clients=data.quota_clients_count_month||0;
if(typeof updateQuotaWidget==='function')updateQuotaWidget();
}
}catch(e){console.warn('[RECOVMAX/quota.increment] silent fail:',e.message);}
}
}
};
// ── PENALITES — calcul juridiquement correct selon le régime du débiteur ──
//
// Règles appliquées :
// b2b → Entreprise commerciale (SAS, SARL, EURL, micro-BIC commerciale)
// Taux BCE refi + 10 pts (L441-10) · Indemnité forfaitaire 40 € (D441-5)
// liberal → Profession libérale (avocat, médecin, kiné, architecte…)
// Taux légal "autres cas" (art. 1231-6 C. civ.) · Pas d'indemnité par défaut
// admin → Administration / Collectivité publique
// Taux BCE refi + 8 pts (Décret 2013-269) · Indemnité forfaitaire 40 €
// b2c → Particulier (consommateur)
// Taux légal "particuliers" (max 3× si CGV) · Pas d'indemnité (D441-5 réservé B2B)
//
// Si regime non spécifié → 'b2b' (90% des cas RECOV historiques)
const Penalites={
// Tableau des règles par régime — single source of truth
REGLES:{
b2b: {label:'Entreprise commerciale', sourceTaux:'BCE+10', indemnite:true, prescriptionAns:5, articleTaux:'L441-10 Code de commerce', articleIndemnite:'D441-5 Code de commerce'},
liberal:{label:'Profession libérale', sourceTaux:'TAUX_LEGAL_AUTRES', indemnite:false, prescriptionAns:5, articleTaux:'art. 1231-6 Code civil', articleIndemnite:null},
admin: {label:'Administration / Collectivité publique', sourceTaux:'BCE+8', indemnite:true, prescriptionAns:4, articleTaux:'Décret 2013-269', articleIndemnite:'D441-5 Code de commerce'},
b2c: {label:'Particulier (consommateur)', sourceTaux:'TAUX_LEGAL_PARTICULIER', indemnite:false, prescriptionAns:2, articleTaux:'art. L313-3 Code de la consommation', articleIndemnite:null}
},
// Résout le taux annuel à utiliser selon la règle
_resoudreTaux(regle){
if(regle.sourceTaux==='BCE+10') return TAUX_BCE+MAJORATION;
if(regle.sourceTaux==='BCE+8') return TAUX_BCE+MAJORATION_ADMIN;
if(regle.sourceTaux==='TAUX_LEGAL_AUTRES') return TAUX_LEGAL_AUTRES;
if(regle.sourceTaux==='TAUX_LEGAL_PARTICULIER') return TAUX_LEGAL_PARTICULIER;
return TAUX_BCE+MAJORATION; // fallback
},
calculer(montant,dateEcheance,dateCalcul,options){
const opts=options||{};
const regime=(opts.regime||'b2b').toLowerCase();
const regle=this.REGLES[regime]||this.REGLES.b2b;
const tauxFige=opts.tauxFige; // si on a un snapshot historique sur la créance
const indemniteOverride=opts.indemniteForcee; // pour les cas custom (ex: libéral avec CGV qui prévoit 40€)
const jr=Math.max(0,diffJours(dateCalcul,dateEcheance));
const taux=(typeof tauxFige==='number'&&tauxFige>0)?tauxFige:this._resoudreTaux(regle);
const mp=parseFloat(montant)*taux/100*jr/365;
const indemniteApplicable=(typeof indemniteOverride==='boolean')?indemniteOverride:regle.indemnite;
const indemnite=indemniteApplicable?INDEMNITE_FORFAITAIRE:0;
return {
regime,
regimeLabel:regle.label,
tauxBCE:TAUX_BCE,
tauxApplicable:Math.round(taux*100)/100,
sourceTaux:regle.sourceTaux,
articleTaux:regle.articleTaux,
articleIndemnite:regle.articleIndemnite,
prescriptionAns:regle.prescriptionAns,
joursRetard:jr,
montantPenalites:Math.round(mp*100)/100,
indemnite,
indemniteApplicable,
totalDu:Math.round((parseFloat(montant)+mp+indemnite)*100)/100
};
}
};
// ── PROMPTS (construction cote serveur, le client envoie les donnees) ──
// ── UI ──
const UI={
showView(viewId,data){
const app=document.getElementById('app');
if(viewId==='dashboard')this.renderDashboard(app);
else if(viewId==='nouvelle')this.renderNouvelle(app);
else if(viewId==='detail')this.renderDetail(app,data);
else if(viewId==='profil')this.renderProfil(app);
else if(viewId==='clients')this.renderClients(app);
else if(viewId==='client-detail')this.renderClientDetail(app,data);
setTimeout(()=>{try{updatePlanBadge();updateQuotaWidget();}catch(e){}},100);
},
_planBadge(){
const plan=Store._data?.user?.plan||'fondateur';
const map={
'demo': {label:'Démo', bg:'#f1f5f9', color:'#64748b', logo:null, dot:'#94a3b8', tip:'Mode démo gratuit — limité à 2 créances totales (pas de reset).<br><br>Pour continuer, créez votre compte freelance ou cabinet.'},
'fondateur': {label:'Fondateur',bg:'#ecfccb', color:'#4d7c0f', logo:'logo.png', dot:'#65a30d', tip:'Plan Fondateur historique — accès complet et gratuit. Conservé tel quel : compte basculera naturellement vers RECOV Solo (toujours gratuit) ou RECOVMAX selon votre usage.'},
'founder': {label:'Fondateur',bg:'#ecfccb', color:'#4d7c0f', logo:'logo.png', dot:'#65a30d', tip:'Plan Fondateur (alias historique founder) — accès illimité gratuit pendant la phase pilote, sans engagement de durée.'},
'direct': {label:'Fondateur',bg:'#ecfccb', color:'#4d7c0f', logo:'logo.png', dot:'#65a30d', tip:'Plan Fondateur direct — accès illimité pendant la phase pilote, sans engagement de durée.'},
'partner': {label:'Fondateur',bg:'#ecfccb', color:'#4d7c0f', logo:'logo.png', dot:'#65a30d', tip:'Plan Fondateur partenaire — accès illimité pendant la phase pilote, sans engagement de durée.'},
'freelance': {label:'Freelance',bg:'#dbeafe', color:'#1d4ed8', logo:'logo.png', dot:'#3b82f6', tip:'Plan Freelance — pour les indépendants qui suivent leurs propres factures.<br><br>• 10 créances/mois<br>• 100 relances/mois<br>• 1 utilisateur (vous)'},
'recovmax': {label:'RECOVMAX', bg:'#0d0d14', color:'#84CC16', logo:null, dot:'#84CC16', tip:'Plan RECOVMAX — pour les pros qui gèrent les impayés des autres (secrétaires indé, VA, cabinets).<br><br>Plafonds :<br>• 50 clients distincts<br>• 50 créances actives<br>• 1 000 relances/mois<br>• Reporting PDF mensuel par client'}
};
const cfg=map[plan]||map['fondateur'];
const isDark=cfg.bg==='#0d0d14';
const logoHTML=cfg.logo?`<img src="${cfg.logo}" alt="" style="height:14px;width:auto;display:block">`:`<span style="width:6px;height:6px;border-radius:50%;background:${cfg.dot};display:block;flex-shrink:0"></span>`;
// Tooltip: petit ? cliquable à côté du badge (au lieu de class="tip" qui casserait le style)
const tipDot=isDark?'rgba(132,204,22,.5)':'rgba(0,0,0,.18)';
const tipColor=isDark?'#84CC16':cfg.color;
const tipHTML=cfg.tip?`<span class="tip" style="margin-left:4px;background:transparent;color:${tipColor};border:1px solid ${tipDot};width:14px;height:14px;font-size:8px" data-tip="${cfg.tip.replace(/"/g,'"')}">?</span>`:'';
return `<div class="plan-badge" style="display:inline-flex;align-items:center;gap:6px;background:${cfg.bg};color:${cfg.color};padding:6px 11px;border-radius:8px;font-size:11px;letter-spacing:.02em;${isDark?'border:1px solid rgba(132,204,22,.4);':''}">${logoHTML}<span style="font-weight:500;letter-spacing:.04em;text-transform:uppercase">${cfg.label}</span>${tipHTML}</div>`;
},
_navIcon(type){
const icons={
dashboard:`<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><rect x="1" y="1" width="6" height="6" rx="1.5"/><rect x="9" y="1" width="6" height="6" rx="1.5"/><rect x="1" y="9" width="6" height="6" rx="1.5"/><rect x="9" y="9" width="6" height="6" rx="1.5"/></svg>`,
nouvelle:`<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1a1 1 0 011 1v5h5a1 1 0 010 2H9v5a1 1 0 01-2 0V9H2a1 1 0 010-2h5V2a1 1 0 011-1z"/></svg>`,
profil:`<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><circle cx="8" cy="5" r="3"/><path d="M2 13.5C2 11 4.7 9 8 9s6 2 6 4.5a.5.5 0 01-.5.5h-11a.5.5 0 01-.5-.5z"/></svg>`,
clients:`<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="5" r="2.5"/><path d="M1.5 13c0-2 2-3.5 4.5-3.5s4.5 1.5 4.5 3.5"/><circle cx="11.5" cy="4" r="1.8"/><path d="M14.5 11.5c0-1.5-1.3-2.7-3-2.7"/></svg>`,
logout:`<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M6 2H3a1 1 0 00-1 1v10a1 1 0 001 1h3a1 1 0 000-2H4V4h2a1 1 0 000-2z"/><path d="M10.3 5.3a1 1 0 011.4 0l3 3a1 1 0 010 1.4l-3 3a1 1 0 01-1.4-1.4L11.58 10H7a1 1 0 010-2h4.59L10.3 6.7a1 1 0 010-1.4z"/></svg>`,
};
return icons[type]||'';
},
_nav(active){
const u=Store.getUser();
const rawNom=u&&u.nom?u.nom:'';
const prenomRaw=rawNom.split(' ')[0]||'';
const prenom=prenomRaw.charAt(0).toUpperCase()+prenomRaw.slice(1).toLowerCase();
const initiales=rawNom.split(' ').filter(Boolean).map(n=>n[0].toUpperCase()).join('').substring(0,2)||'?';
const societe=u&&u.societe?u.societe:'RECOV';
const _plan=Store._data?.user?.plan||'fondateur';
const nav=[
{id:'dashboard',icon:'dashboard',label:'Tableau de bord'},
..._plan==='recovmax'?[{id:'clients',icon:'clients',label:'Mes clients'}]:[],
{id:'nouvelle',icon:'nouvelle',label:'Nouvelle facture'},
{id:'profil',icon:'profil',label:'Mon profil'},
];
const self=this;
return `<div class="app-layout">
<aside class="app-sidebar">
<div class="app-sidebar-logo">
<img src="iconRecov.png" alt="RECOV" style="width:28px;height:28px;object-fit:contain;flex-shrink:0" onerror="this.style.display='none'">
<span>RECOV</span>
</div>
<nav class="app-sidebar-nav">
${nav.map(n=>`<button class="sidebar-item ${active===n.id?'active':''}" onclick="UI.showView('${n.id}')"><span class="sidebar-item-icon">${self._navIcon(n.icon)}</span>${n.label}</button>`).join('')}
</nav>
${(()=>{
const objectif=u&&u.objectifMensuel?parseFloat(u.objectifMensuel):0;
const moisCourant=Store.getMontantMoisCourant();
const pct=objectif>0?Math.min(100,Math.round(moisCourant/objectif*100)):0;
const fmtO=n=>new Intl.NumberFormat('fr-FR',{style:'currency',currency:'EUR',maximumFractionDigits:0}).format(n);
const moisLabel=new Date().toLocaleDateString('fr-FR',{month:'long'});
if(objectif>0){
const barColor=pct>=100?'#84cc16':pct>=60?'#f59e0b':'#64748b';
return `<div style="padding:14px 14px 12px;border-top:1px solid #f2f2f7;cursor:pointer" onclick="setObjectifMensuel()" title="Modifier l'objectif">
<div style="font-size:10px;font-weight:700;color:#94a3b8;letter-spacing:.06em;text-transform:uppercase;margin-bottom:8px">🎯 Objectif ${moisLabel} <span class="tip" data-tip="Cliquez pour modifier votre objectif de recouvrement du mois. RECOV affiche votre progression en temps réel.">?</span></div>
<div style="display:flex;align-items:baseline;justify-content:space-between;margin-bottom:8px">
<span style="font-family:'Bricolage Grotesque',sans-serif;font-size:18px;font-weight:500;color:${pct>=100?'#84cc16':'#0d0d14'};letter-spacing:-.03em">${fmtO(moisCourant)}</span>
<span style="font-size:11px;color:#94a3b8">/ ${fmtO(objectif)}</span>
</div>
<div style="height:6px;background:#f2f2f7;border-radius:3px;overflow:hidden;margin-bottom:5px">
<div style="height:100%;width:${pct}%;background:${barColor};border-radius:3px;transition:width .4s"></div>
</div>
<div style="font-size:11px;color:${barColor};font-weight:700">${pct>=100?'🎯 Objectif atteint !':pct+'% — '+fmtO(objectif-moisCourant)+' restants'}</div>
</div>`;
} else {
return `<div style="padding:12px 14px;border-top:1px solid #f2f2f7;cursor:pointer" onclick="setObjectifMensuel()" title="Définir un objectif mensuel">
<div style="font-size:12px;color:#94a3b8;display:flex;align-items:center;gap:6px;font-weight:500">
<span>🎯</span> Définir un objectif mensuel <span class="tip" data-tip="Fixez un objectif mensuel pour suivre votre progression et savoir combien il vous reste à récupérer.">?</span>
</div>
</div>`;
}
})()}
<div class="app-sidebar-footer">
<div style="padding:0 14px 10px;border-bottom:1px solid #f2f2f7;margin-bottom:10px">${self._planBadge()}</div>
<div class="sidebar-user" onclick="UI.showView('profil')">
<div class="sidebar-user-avatar">${initiales}</div>
<div style="min-width:0;overflow:hidden">
<div class="sidebar-user-name">${prenom||'Mon compte'}</div>
<div class="sidebar-user-co">${societe}</div>
</div>
</div>
<button class="sidebar-item" onclick="logout()" style="color:#94a3b8;font-size:13px;margin-top:2px">
<span class="sidebar-item-icon">${self._navIcon('logout')}</span>Déconnexion
</button>
</div>
</aside>
<div class="app-mobile-topbar">
<div class="app-mobile-topbar-logo">
<img src="iconRecov.png" alt="RECOV" style="width:26px;height:26px;object-fit:contain" onerror="this.style.display='none'">
RECOV
</div>
<div style="display:flex;gap:8px;align-items:center">
${self._planBadge()}
</div>
</div>
<div class="app-bottom-tabs">
<button class="bottom-tab ${active==='dashboard'?'active':''}" onclick="UI.showView('dashboard')">
<span class="bottom-tab-icon">${self._navIcon('dashboard')}</span>Accueil
</button>
<button class="bottom-tab ${active==='nouvelle'?'active':''}" onclick="UI.showView('nouvelle')">
<span class="bottom-tab-icon" style="font-size:22px;line-height:1;font-weight:200">+</span>Ajouter
</button>
<button class="bottom-tab ${active==='profil'?'active':''}" onclick="UI.showView('profil')">
<span class="bottom-tab-icon">${self._navIcon('profil')}</span>Profil
</button>
<button class="bottom-tab" onclick="logout()" style="color:#ef4444">
<span class="bottom-tab-icon">${self._navIcon('logout')}</span>Quitter
</button>
</div>
<div class="app-main">`;
},
renderDashboard(app){
try{ return this._renderDashboardInner(app); }catch(e){ console.error('renderDashboard error:',e); app.innerHTML='<div style="padding:40px;color:red;font-size:16px">Erreur dashboard : '+e.message+'</div>'; }
},
_renderDashboardInner(app){
const s=(Store.getAll()&&Store.getAll().stats)||{totalRecupere:0,totalCreances:0,totalEnvoyes:0};
const creances=safeArray(Store.getCreances()).map(normalizeCreance).filter(Boolean);
const alertes=safeArray(this._getAlertes());
// Créances actives RECOV = non payées ET pas encore transmises en recouvrement/abandonnées
const isEscalade=c=>c.outcome==='escalated'||(!['paid_via_recov','abandoned','in_progress'].includes(c.outcome)&&(()=>{try{return(c.etapes?JSON.parse(typeof c.etapes==='string'?c.etapes:'null')||c.etapes:c.etapes||[]).filter(e=>e.statut==='envoye').length>=8;}catch{return false;}})());
const montantEnCours=creances.filter(c=>c.statut!=='paye'&&c.outcome!=='abandoned'&&!isEscalade(c)).reduce((acc,c)=>acc+(parseFloat(c.montant)||0),0);
const montantEscalade=creances.filter(c=>c.statut!=='paye'&&isEscalade(c)).reduce((acc,c)=>acc+(parseFloat(c.montant)||0),0);
const nextAction=alertes[0]||null;
// ── TRIAGE CRÉANCES ──
const nextMontant=nextAction?creances.find(c=>c.id===nextAction.creanceId)?.montant||0:0;
const urgent=[];const aVenir=[];const suivi=[];
const today=new Date().toISOString().split('T')[0];
creances.forEach(c=>{
if(c.statut==='paye'){suivi.push({c,label:'Payé',color:'paid'});return;}
const etapes=safeArray(c.etapes).map((e,i)=>normalizeEtape(e,i));
const prochaineIdx=etapes.findIndex(e=>e.statut==='a_envoyer');
const prochaine=prochaineIdx>=0?etapes[prochaineIdx]:null;
const envoyees=etapes.filter(e=>e.statut==='envoye').length;
const pct=Math.round(envoyees/8*100);
if(prochaine&&prochaine.dateEnvoi<=today){urgent.push({c,prochaine,prochaineIdx,pct});}
else if(prochaine){aVenir.push({c,prochaine,prochaineIdx,pct});}
else{suivi.push({c,label:'Terminé',color:'paid',pct:100});}
});
// ── GREETING + USER ──
const user=Store.getUser();
const prenomRaw=user&&user.nom?user.nom.split(' ')[0]:'';
const prenom=prenomRaw?prenomRaw.charAt(0).toUpperCase()+prenomRaw.slice(1).toLowerCase():'';
// ── KPIs ──
const totalDossiers=creances.length;
const totalPayes=creances.filter(c=>c.statut==='paye').length;
const tauxReussite=totalDossiers>0?Math.round(totalPayes/totalDossiers*100):0;
const economise=Math.round(s.totalRecupere*0.3);
// ── DOSSIERS LIST ──
function buildStepBubbles(etapes,etapeIdx,statut){
if(statut==='paye') return `<div style="display:flex;align-items:center;gap:3px">${Array(6).fill(0).map((_,i)=>`<div class="step-bubble done">✓</div>`).join('')}</div>`;
const total=Math.min(etapes.length,6);
return `<div class="step-bubbles">${Array(total).fill(0).map((_,i)=>{
const cls=i<etapeIdx?'done':i===etapeIdx?'current':'pending';
return `<div class="step-bubble ${cls}">${i+1}</div>`;
}).join('')}</div>`;
}
function renderDossierRow(item){
const c=item.c;const p=item.prochaine;
const etapes=safeArray(c.etapes).map((e,i)=>normalizeEtape(e,i));
const envoyees=etapes.filter(e=>e.statut==='envoye').length;
const etapeIdx=item.prochaineIdx!=null?item.prochaineIdx:envoyees;
const badgeColors=['#84cc16','#84cc16','#f59e0b','#f59e0b','#f97316','#ef4444','#dc2626','#991b1b'];
const badgeColor=c.statut==='paye'?'#65a30d':(badgeColors[Math.min(etapeIdx,7)]||'#84cc16');
const isUrgent=urgent.some(u=>u.c.id===c.id);
const statusColor=c.statut==='paye'?'#15803d':isUrgent?'#ef4444':'#94a3b8';
const etapeLabel=c.statut==='paye'?'Facture récupérée':(p?esc(p.label):(envoyees>=8?'Séquence complète':'En attente'));
const etapeInfo=c.statut==='paye'?'':(p?`Étape ${etapeIdx+1}/8 · `:envoyees>0?`${envoyees}/8 envoyées · `:'');
const statusLabel=c.statut==='paye'?'Payée ✓':(isUrgent?'⚡ À envoyer':(p?'Dans '+Math.max(1,Math.round((new Date(p.dateEnvoi)-new Date())/(86400000)))+' j.':'Terminé'));
const tabGroup=c.statut==='paye'?'termine':isUrgent?'relancer':'cours';
return `<div class="dossier-row" data-tab="${tabGroup}" onclick="UI.showView('detail','${c.id}')">
<div class="dossier-badge" style="background:${badgeColor}">${c.statut==='paye'?'✓':(etapeIdx+1)}</div>
<div class="dossier-main">
<div class="dossier-client">${esc(c.client)}</div>
<div class="dossier-montant-small" style="color:#94a3b8;margin-top:2px">${etapeInfo}<span style="color:#64748b;font-weight:600">${etapeLabel}</span></div>
</div>
<div class="dossier-right" style="display:flex;flex-direction:column;align-items:flex-end;gap:3px">
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:15px;font-weight:500;color:#0d0d14;letter-spacing:-.03em">${fmtMontant(c.montant)}</div>
<div style="color:${statusColor};font-weight:700;font-size:11px;text-align:right">${statusLabel}</div>
</div>
<span class="dossier-chevron">›</span>
</div>`;
}
// À TRAITER supprimé — redondant avec action card + liste dossiers
const traiterHtml='';
// ── DOSSIERS LIST ──
let dossiersHtml='';
if(creances.length){
const suiviActif=suivi.filter(item=>item.c.statut!=='paye');
const payes=suivi.filter(item=>item.c.statut==='paye');
const cEnCours=aVenir.length+suiviActif.length;
const cTermines=payes.length;
const tabBadge=(n,urgent)=>n>0?`<span class="dash-tab-count">${n}</span>`:urgent?`<span class="dash-tab-count">0</span>`:`<span class="dash-tab-count">0</span>`;
dossiersHtml=`<div class="dash-section" id="dash-dossiers">
<div class="dash-section-head">
<div class="dash-section-title">Mes dossiers <span class="tip" data-tip="Toutes les factures impayées que vous suivez.<br><br>Chaque ligne = un dossier (une facture qui doit être payée). Cliquez sur un dossier pour voir le détail des relances, le calcul des pénalités, et la prochaine action.<br><br>RECOV planifie automatiquement les 7 étapes de relance + la mise en demeure à J+45.">?</span></div>
<button onclick="UI.showView('nouvelle')" title="Ouvre le wizard de création d'une nouvelle facture impayée" style="font-size:13px;font-weight:500;color:#84cc16;background:none;border:none;cursor:pointer;padding:0">+ Ajouter</button>
</div>
<div class="dash-tabs">
<button class="dash-tab-btn active" data-tab="tous" onclick="filterDossierTab('tous')" title="Tous vos dossiers, tout statut confondu">Tous <span class="dash-tab-count">${creances.length}</span></button>
<button class="dash-tab-btn${urgent.length?` tab-urgent`:''}" data-tab="relancer" onclick="filterDossierTab('relancer')"><span class="tip" style="margin-left:0;margin-right:4px;background:${urgent.length?'#fee2e2':'rgba(0,0,0,.06)'};color:${urgent.length?'#ef4444':'#94a3b8'}" data-tip="Dossiers qui nécessitent votre action aujourd'hui.<br><br>La date d'envoi de la prochaine étape est arrivée — votre débiteur attend une relance. Si vous laissez ces dossiers s'accumuler, la séquence se désynchronise et perd en efficacité.">?</span>À relancer <span class="dash-tab-count">${urgent.length}</span></button>
<button class="dash-tab-btn" data-tab="cours" onclick="filterDossierTab('cours')"><span class="tip" style="margin-left:0;margin-right:4px" data-tip="Relances planifiées dans le futur.<br><br>RECOV vous alertera automatiquement (et par email si activé) le jour où la prochaine étape doit partir.">?</span>En cours <span class="dash-tab-count">${cEnCours}</span></button>
<button class="dash-tab-btn" data-tab="termine" onclick="filterDossierTab('termine')"><span class="tip" style="margin-left:0;margin-right:4px" data-tip="Dossiers clôturés.<br><br>• <em>Payés</em> — facture encaissée, marquée payée<br>• <em>Archivés</em> — abandonnés ou transmis à un avocat<br><br>Restent visibles pour vos statistiques (taux de succès, total récupéré).">?</span>Terminés <span class="dash-tab-count">${cTermines}</span></button>
</div>
${urgent.length?`<div data-tab-group="relancer" style="display:flex;align-items:center;gap:8px;padding:10px 20px 6px;background:#fff5f5;border-top:1px solid #fee2e2"><span style="display:inline-flex;align-items:center;gap:5px;background:#ef4444;color:#fff;font-size:10px;font-weight:500;letter-spacing:.06em;text-transform:uppercase;padding:3px 9px;border-radius:20px">⚡ ${urgent.length} urgent${urgent.length>1?'s':''}</span></div>${urgent.map(renderDossierRow).join('')}`:''}
${aVenir.length?`<div class="dossier-group-label" data-tab-group="cours">📅 À venir</div>${aVenir.map(renderDossierRow).join('')}`:''}
${suiviActif.length?`<div class="dossier-group-label" data-tab-group="cours">· En suivi</div>${suiviActif.map(renderDossierRow).join('')}`:''}
${payes.length?`<div class="dossier-group-label" data-tab-group="termine">✅ Payés</div>${payes.map(renderDossierRow).join('')}`:''}
</div>`;
} else {
const profilOk=isProfilComplet();
dossiersHtml=`<div class="view-card" style="padding:32px clamp(24px,4vw,40px);font-family:'DM Sans',system-ui,sans-serif">
<span style="display:inline-flex;align-items:center;gap:7px;font-size:11px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;color:#4d7c0f;margin-bottom:14px">
<span style="width:18px;height:1px;background:#65a30d"></span>Démarrage
</span>
<h2 style="font-family:'Bricolage Grotesque',sans-serif;font-size:clamp(24px,3.4vw,32px);font-weight:500;letter-spacing:-.03em;line-height:1.15;color:#0d0d14;margin:0 0 12px">Votre premier dossier en trois étapes.</h2>
<p style="font-size:15px;color:#64748b;line-height:1.7;margin:0 0 28px;max-width:580px">Séquence de relance planifiée, pénalités calculées au taux légal, mise en demeure prête. Vous validez chaque envoi.</p>
<div style="display:flex;flex-direction:column;gap:0;margin-bottom:24px">
<div style="display:flex;gap:16px;padding:16px 0;border-top:1px solid #f1f5f9">
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:14px;font-weight:500;color:#94a3b8;width:32px;flex-shrink:0">01</div>
<div style="flex:1">
<div style="font-size:15px;font-weight:700;color:#0d0d14;margin-bottom:4px;letter-spacing:-.01em">Remplissez votre profil</div>
<div style="font-size:14px;color:#64748b;line-height:1.6">Nom et SIRET pour la signature des relances. Trente secondes.</div>
</div>
${profilOk?`<div style="flex-shrink:0;font-size:11px;font-weight:700;color:#15803d;letter-spacing:.04em;text-transform:uppercase;align-self:center">✓ Fait</div>`:''}
</div>
<div style="display:flex;gap:16px;padding:16px 0;border-top:1px solid #f1f5f9">
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:14px;font-weight:500;color:#94a3b8;width:32px;flex-shrink:0">02</div>
<div style="flex:1">
<div style="font-size:15px;font-weight:700;color:#0d0d14;margin-bottom:4px;letter-spacing:-.01em">Ajoutez une facture impayée</div>
<div style="font-size:14px;color:#64748b;line-height:1.6">Client, montant, échéance. RECOV planifie toute la séquence.</div>
</div>
</div>
<div style="display:flex;gap:16px;padding:16px 0;border-top:1px solid #f1f5f9;border-bottom:1px solid #f1f5f9">
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:14px;font-weight:500;color:#94a3b8;width:32px;flex-shrink:0">03</div>
<div style="flex:1">
<div style="font-size:15px;font-weight:700;color:#0d0d14;margin-bottom:4px;letter-spacing:-.01em">Validez et envoyez</div>
<div style="font-size:14px;color:#64748b;line-height:1.6">Textes rédigés, références légales, pénalités. Vous validez, vous envoyez.</div>
</div>
</div>
</div>
<button onclick="UI.showView('${profilOk?'nouvelle':'profil'}')" style="background:#0d0d14;color:#fff;padding:13px 24px;border:none;border-radius:11px;font-family:'DM Sans',sans-serif;font-size:14px;font-weight:500;cursor:pointer;letter-spacing:-.005em;transition:background .15s" onmouseover="this.style.background='#1e1e2e'" onmouseout="this.style.background='#0d0d14'">${profilOk?'Ajouter ma première facture →':'Commencer — remplir mon profil →'}</button>
${profilOk?'':`<a href="#" onclick="UI.showView('nouvelle');return false" style="display:inline-block;margin-left:14px;font-size:13px;color:#64748b;text-decoration:none;font-weight:500">ou ajouter une facture directement</a>`}
</div>`;
}
// ── ACTION CARD ──
let actionCardHtml='';
if(nextAction){
const nc=creances.find(c=>c.id===nextAction.creanceId);
const netapes=nc?safeArray(nc.etapes).map((e,i)=>normalizeEtape(e,i)):[];
const nIdx=Math.max(0,nc?netapes.findIndex(e=>e.statut==='a_envoyer'):0);
const nEtape=netapes[nIdx]||null;
const nLabel=nEtape?esc(nEtape.label):'Relance';
const nDate=nEtape?fmtDate(nEtape.dateEnvoi):'';
const nEnvoyees=netapes.filter(e=>e.statut==='envoye').length;
const autresUrgents=alertes.length-1;
const gaugeTotal=Math.max(1,netapes.length);
const gaugePct=nEnvoyees/gaugeTotal;
const circumference=163.4;
const dashOffset=(circumference*(1-gaugePct)).toFixed(1);
const gaugeColor=nEnvoyees<3?'#84cc16':nEnvoyees<5?'#f59e0b':'#ef4444';
const gaugeSvg=`<svg width="76" height="76" viewBox="0 0 76 76" style="display:block">
<circle cx="38" cy="38" r="26" fill="none" stroke="rgba(255,255,255,.1)" stroke-width="5.5"/>
<circle cx="38" cy="38" r="26" fill="none" stroke="${gaugeColor}" stroke-width="5.5"
stroke-dasharray="${circumference}" stroke-dashoffset="${dashOffset}"
stroke-linecap="round" transform="rotate(-90 38 38)"/>
<text x="38" y="44" text-anchor="middle" font-size="22" font-weight="800" fill="#fff" font-family="Bricolage Grotesque,sans-serif">${nEnvoyees+1}</text>
</svg>`;
actionCardHtml=`<div class="action-card" style="margin-bottom:16px">
<div style="flex:1;min-width:0">
<div style="font-size:11px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:#84cc16;margin-bottom:10px">⚡ Relance à envoyer aujourd'hui</div>
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:clamp(18px,3vw,24px);font-weight:500;color:#fff;letter-spacing:-.02em;margin-bottom:2px">${esc(nextAction.client)}</div>
<div style="font-size:13px;color:rgba(255,255,255,.45);margin-bottom:18px">${fmtMontant(nextMontant)}${nc&&nc.facture?' · '+esc(nc.facture):''}</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
<button class="btn-action-primary" onclick="UI.showView('detail','${nextAction.creanceId}')">Préparer et envoyer →</button>
${autresUrgents>0?`<span style="font-size:12px;color:rgba(255,255,255,.4)">+ ${autresUrgents} autre${autresUrgents>1?'s':''} urgent${autresUrgents>1?'s':''} ↓</span>`:''}
</div>
</div>
<div style="flex-shrink:0;text-align:center;display:flex;flex-direction:column;align-items:center;gap:4px">
<div style="position:relative">${gaugeSvg}<span class="tip" style="position:absolute;top:-4px;right:-4px;margin:0;background:rgba(255,255,255,.15);color:#fff" data-tip="Progression de la séquence de relance. Chaque étape augmente la pression sur votre client. La couleur change : vert → orange → rouge.">?</span></div>
<div style="font-size:9px;color:rgba(255,255,255,.35);letter-spacing:.08em;text-transform:uppercase">sur ${gaugeTotal}</div>
<div style="font-size:11px;color:rgba(255,255,255,.75);font-weight:600;max-width:88px;line-height:1.3;text-align:center">${nLabel}</div>
</div>
</div>`;
} else if(creances.length>0){
// Prochaine relance à venir
const prochaine=aVenir[0]||null;
const pcr=prochaine?prochaine.c:null;
const petapes=pcr?safeArray(pcr.etapes).map((e,i)=>normalizeEtape(e,i)):[];
const pIdx=prochaine?prochaine.prochaineIdx:0;
const pEtape=petapes[pIdx]||null;
actionCardHtml=`<div class="action-card" style="margin-bottom:16px;background:linear-gradient(135deg,#064e3b 0%,#065f46 100%)">
<div style="flex:1;min-width:0">
<div style="font-size:11px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:#6ee7b7;margin-bottom:10px">✅ Rien à faire aujourd'hui</div>
${prochaine&&pEtape?`
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:18px;font-weight:500;color:#fff;margin-bottom:4px">Prochaine : ${esc(pcr.client)}</div>
<div style="font-size:13px;color:rgba(255,255,255,.5);margin-bottom:12px">${fmtMontant(pcr.montant)} · Étape ${pIdx+1} — ${esc(pEtape.label)} · le ${fmtDate(pEtape.dateEnvoi)}</div>
`:`<div style="font-size:16px;color:rgba(255,255,255,.7);font-weight:300;margin-bottom:12px">Tous vos dossiers sont à jour — RECOV vous alertera.</div>`}
</div>
</div>`;
}
app.innerHTML=this._nav('dashboard')+`
${Store.isFounderExpired()?'<div style="background:#fef2f2;border-radius:14px;padding:13px 16px;margin-bottom:20px;font-size:13px;color:#7f1d1d;display:flex;align-items:center;gap:10px"><span>⚠️</span>Accès fondateur expiré.<button onclick="goPaiement()" style="background:#0d0d14;color:#fff;border:none;padding:6px 12px;border-radius:7px;font-size:12px;font-weight:500;cursor:pointer;margin-left:auto">Souscrire</button></div>':''}
<div class="app-main-header">
<div style="flex:1;min-width:0">
<div class="app-main-greeting-title">${prenom?'Bonjour '+prenom:'Tableau de bord'}</div>
<div class="app-main-greeting-sub" style="margin-bottom:${creances.length?'10':'0'}px">${alertes.length?'<span style="color:#ef4444;font-weight:700">⚡ '+alertes.length+' dossier'+(alertes.length>1?'s':'')+' à relancer aujourd\'hui</span>':creances.length?'Tout est à jour — RECOV veille.':'Commencez par ajouter une facture impayée.'}</div>
${(()=>{
if(!creances.length)return '';
const weekAgo=new Date(Date.now()-7*24*60*60*1000).toISOString().split('T')[0];
const relancesSemaine=creances.flatMap(c=>safeArray(c.etapes).map((e,i)=>normalizeEtape(e,i))).filter(e=>e.statut==='envoye'&&e.dateEnvoi>=weekAgo).length;
const paiementsSemaine=creances.filter(c=>c.statut==='paye'&&c.paidAt&&c.paidAt.slice(0,10)>=weekAgo).length;
const parts=[];
if(relancesSemaine>0)parts.push(`${relancesSemaine} relance${relancesSemaine>1?'s':''} envoyée${relancesSemaine>1?'s':''}`);
if(paiementsSemaine>0)parts.push(`<span style="color:#15803d;font-weight:700">${paiementsSemaine} paiement${paiementsSemaine>1?'s':''} reçu${paiementsSemaine>1?'s':''}</span>`);
if(!parts.length)return '';
return `<div style="font-size:12px;color:#94a3b8;margin-top:2px">Cette semaine · ${parts.join(' · ')}</div>`;
})()}
</div>
</div>
${actionCardHtml}
<!-- Widget quota mensuel (Freelance / RECOVMAX) — ligne fine sous le titre -->
<div id="quotaWidget" style="display:none;margin:-8px 0 18px;max-width:1080px"></div>
<div class="dash-kpis">
<div class="dash-kpi kpi-vert">
<div class="dash-kpi-label">Récupéré <span class="tip" data-tip="Le montant total des factures encaissées depuis votre inscription, marquées <em>« Payée »</em> dans RECOV.<br><br>Inclut tous vos dossiers, anciens comme récents. Mis à jour automatiquement dès que vous cliquez sur <em>« Marquer payé »</em> dans le détail d'une créance.">?</span></div>
<div class="dash-kpi-value" style="color:#15803d">${fmtMontant(s.totalRecupere)}</div>
<div class="dash-kpi-sub">total via RECOV</div>
</div>
<div class="dash-kpi kpi-neutral">
<div class="dash-kpi-label">En attente <span class="tip" data-tip="Le total des factures impayées encore en cours.<br><br>Compte uniquement les dossiers au statut <em>« En cours »</em> ou <em>« À relancer »</em> — pas les payés ni les archivés.<br><br>Inclut le montant facturé d'origine (sans les pénalités de retard, calculées dossier par dossier).">?</span></div>
<div class="dash-kpi-value" style="color:#0d0d14">${fmtMontant(montantEnCours)}</div>
<div class="dash-kpi-sub">${creances.filter(c=>c.statut!=='paye').length} dossier${creances.filter(c=>c.statut!=='paye').length>1?'s':''} actif${creances.filter(c=>c.statut!=='paye').length>1?'s':''}</div>
</div>
<div class="dash-kpi ${tauxReussite>30?'kpi-vert':'kpi-neutral'}">
<div class="dash-kpi-label">Taux de succès <span class="tip" data-tip="Le pourcentage de vos dossiers résolus par un paiement.<br><br>Calcul : nombre de dossiers <em>« Payés »</em> / total dossiers (payés + en cours + archivés).<br><br>À titre indicatif : la moyenne du recouvrement amiable bien mené est autour de 65-75 %. En dessous de 30 %, regardez si vos relances partent bien et si vos clients ont vos coordonnées de paiement.">?</span></div>
<div class="dash-kpi-value" style="color:${tauxReussite>30?'#15803d':'#0d0d14'}">${tauxReussite}%</div>
<div class="dash-kpi-sub">${totalPayes} payé${totalPayes>1?'s':''} / ${totalDossiers}</div>
</div>
<div class="dash-kpi kpi-lime">
<div class="dash-kpi-label">Économisé <span class="tip" data-tip="Ce qu'aurait prélevé un cabinet de recouvrement classique sur les sommes que vous avez récupérées.<br><br>Calcul : 30 % de votre <em>Récupéré</em>. C'est la commission moyenne d'un cabinet de recouvrement (15-30 % selon le contrat).<br><br>Avec RECOV/RECOVMAX, vous gardez la totalité — pas de commission, juste l'abonnement mensuel.">?</span></div>
<div class="dash-kpi-value" style="color:#65a30d">${fmtMontant(economise)}</div>
<div class="dash-kpi-sub">vs cabinet 30%</div>
</div>
</div>
${(()=>{
const u2=Store.getUser();
const obj=u2&&u2.objectifMensuel?parseFloat(u2.objectifMensuel):0;
if(!obj)return `<div style="margin-bottom:16px;text-align:right"><button onclick="setObjectifMensuel()" style="font-size:12px;color:#94a3b8;background:none;border:none;cursor:pointer;font-family:inherit">🎯 Définir un objectif mensuel</button></div>`;
const moisCourant=Store.getMontantMoisCourant();
const pct=Math.min(100,Math.round(moisCourant/obj*100));
const barColor=pct>=100?'#65a30d':pct>=60?'#f59e0b':'#94a3b8';
const barBg=pct>=100?'#dcfce7':pct>=60?'#fef3c7':'#f2f2f7';
const cardBg=pct>=100?'linear-gradient(135deg,#f7fee7 0%,#ecfccb 100%)':pct>=60?'linear-gradient(135deg,#fffbeb 0%,#fef3c7 100%)':'#fff';
const labelColor=pct>=100?'#166534':pct>=60?'#92400e':'#94a3b8';
const moisLabel=new Date().toLocaleDateString('fr-FR',{month:'long'});
const fmtO=n=>new Intl.NumberFormat('fr-FR',{style:'currency',currency:'EUR',maximumFractionDigits:0}).format(n);
return `<div style="background:${cardBg};border-radius:16px;box-shadow:0 1px 3px rgba(0,0,0,.05),0 0 0 .5px rgba(0,0,0,.04);padding:18px 20px;margin-bottom:16px;cursor:pointer" onclick="setObjectifMensuel()">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">
<div>
<div style="font-size:10px;font-weight:700;color:${labelColor};letter-spacing:.08em;text-transform:uppercase;margin-bottom:6px">🎯 Objectif ${moisLabel}</div>
<div style="display:flex;align-items:baseline;gap:6px">
<span style="font-family:'Bricolage Grotesque',sans-serif;font-size:26px;font-weight:500;color:${pct>=100?'#15803d':'#0d0d14'};letter-spacing:-.04em;line-height:1">${fmtO(moisCourant)}</span>
<span style="font-size:12px;color:#94a3b8;font-weight:500">/ ${fmtO(obj)}</span>
</div>
</div>
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:36px;font-weight:900;color:${barColor};letter-spacing:-.04em;line-height:1">${pct}%</div>
</div>
<div style="height:10px;background:${barBg};border-radius:5px;overflow:hidden">
<div style="height:100%;width:${pct}%;background:${barColor};border-radius:5px;transition:width .5s cubic-bezier(.4,0,.2,1)"></div>
</div>
<div style="font-size:11.5px;color:${barColor};font-weight:700;margin-top:8px">${pct>=100?'🎯 Objectif atteint ce mois-ci !':fmtO(obj-moisCourant)+' restants'}</div>
</div>`;
})()}
${traiterHtml}
${dossiersHtml}
${(()=>{
const conseil=getConseilDuJour();
return `<div style="background:linear-gradient(135deg,#84CC16 0%,#84CC16 100%);border-radius:16px;padding:16px 20px;margin-bottom:16px">
<div style="font-size:9px;font-weight:700;color:#14532D;letter-spacing:.1em;text-transform:uppercase;margin-bottom:8px;display:flex;align-items:center;gap:4px">Conseil du jour <span class="tip" style="background:rgba(0,0,0,.12);color:#14532D;margin-left:0" data-tip="Un conseil professionnel différent chaque jour pour améliorer votre taux de recouvrement.">?</span></div>
<div style="display:flex;align-items:baseline;gap:10px;flex-wrap:wrap">
<span style="font-family:'Bricolage Grotesque',sans-serif;font-size:clamp(26px,6vw,34px);font-weight:500;color:#0B0F14;letter-spacing:-.04em;line-height:1;flex-shrink:0">${conseil.stat}</span>
<span style="font-size:12.5px;color:#14532D;line-height:1.5;font-weight:500"><span style="background:#0B0F14;color:#fff;padding:1px 8px;border-radius:4px;font-weight:500;font-size:11.5px;display:inline-block;margin-right:4px">${conseil.pill}</span>${conseil.text}</span>
</div>
</div>`;
})()}
<div id="dashAvis" style="display:none"></div>
<!-- Formulaire avis rapide -->
<div class="dash-section" style="padding:20px;margin-bottom:0">
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:18px;font-weight:500;color:var(--noir);margin-bottom:4px">Votre avis compte</div>
<div style="font-size:13px;color:var(--gris);margin-bottom:14px">Comment se passe votre expérience ? Votre retour est visible sur la page d'accueil.</div>
<div style="display:flex;gap:6px;margin-bottom:10px" id="avisStars">
<button onclick="selectAvisNote(1)" style="font-size:28px;background:none;border:none;cursor:pointer;padding:2px" data-star="1">☆</button>
<button onclick="selectAvisNote(2)" style="font-size:28px;background:none;border:none;cursor:pointer;padding:2px" data-star="2">☆</button>
<button onclick="selectAvisNote(3)" style="font-size:28px;background:none;border:none;cursor:pointer;padding:2px" data-star="3">☆</button>
<button onclick="selectAvisNote(4)" style="font-size:28px;background:none;border:none;cursor:pointer;padding:2px" data-star="4">☆</button>
<button onclick="selectAvisNote(5)" style="font-size:28px;background:none;border:none;cursor:pointer;padding:2px" data-star="5">☆</button>
</div>
<textarea id="avisTexte" rows="2" placeholder="Décrivez votre expérience..." style="width:100%;resize:vertical;font-family:'DM Sans',sans-serif;margin-bottom:8px"></textarea>
<div class="form-row">
<div class="fg"><label>Prénom</label><input type="text" id="avisPrenom" placeholder="Pierre"/></div>
<div class="fg"><label>Métier</label><input type="text" id="avisMetier" placeholder="Consultant IT"/></div>
<div class="fg"><label>Ville</label><input type="text" id="avisVille" placeholder="Lyon"/></div>
<div class="fg" style="display:flex;align-items:flex-end"><label style="display:flex;align-items:center;gap:6px;cursor:pointer;font-size:12px;text-transform:none;letter-spacing:0;font-weight:500;color:var(--gris)"><input type="checkbox" id="avisPublic" checked style="width:auto;margin:0"> Publier sur la page d'accueil</label></div>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:8px">
<span style="font-size:11px;color:var(--gris2)" id="avisStatus"></span>
<button class="row-btn primary" onclick="envoyerAvis()">Envoyer mon avis</button>
</div>
</div>
</div></div>`;
// loadDashAvis() désactivé — avis masqués dans l'interface post-connexion
// setTimeout(()=>loadDashAvis(),100);
},
_getAlertes(){
// Une seule alerte par créance : la prochaine étape à envoyer (pas toutes les passées)
const today=new Date().toISOString().split('T')[0];
const alertes=[];
Store.getCreances('en_relance').forEach(c=>{
const etapes=safeArray(c.etapes).map((e,i)=>normalizeEtape(e,i));
const prochaine=etapes.find(e=>e.statut==='a_envoyer');
if(prochaine&&prochaine.dateEnvoi<=today){
alertes.push({creanceId:c.id,client:c.client,label:prochaine.label,date:prochaine.dateEnvoi,etapeNum:etapes.filter(e=>e.statut==='envoye').length+1});
}
});
return alertes.sort((a,b)=>a.date.localeCompare(b.date));
},
renderNouvelle(app){
let _firstProfil=true;
const profilBtns=Object.entries(PROFILS).map(([k,v])=>{
const cls=_firstProfil?'profil-btn active':'profil-btn';
_firstProfil=false;
return `<button class="${cls}" data-profil="${k}" onclick="this.parentElement.querySelectorAll('.profil-btn').forEach(b=>b.classList.remove('active'));this.classList.add('active')">
<span class="pb-icon">${v.icon}</span>${v.label}<br><small style="opacity:.6;font-size:10px">${v.desc}</small></button>`;
}).join('');
app.innerHTML=this._nav('nouvelle')+`
<div class="app-main-header" style="margin-bottom:20px">
<div style="flex:1;min-width:0">
<button class="back-btn" onclick="UI.showView('dashboard')">← Tableau de bord</button>
<div class="app-main-greeting-title">Nouvelle facture</div>
<div class="app-main-greeting-sub" id="wizSubTitle">3 étapes — RECOV génère toute la séquence de relance</div>
</div>
</div>
<div style="max-width:1080px">
<div class="view-card">
<!-- Barre de progression étapes -->
<div id="stepIndicator" style="display:flex;align-items:center;padding:20px 24px 4px">
<!-- Step 0 (RECOVMAX uniquement) -->
<div id="stepWrap0" style="display:none;flex-direction:column;align-items:center;gap:5px">
<div id="stepDot0" style="width:34px;height:34px;border-radius:50%;background:#84cc16;color:#0f172a;display:flex;align-items:center;justify-content:center;font-family:'Bricolage Grotesque',sans-serif;font-size:14px;font-weight:500;transition:all .3s">1</div>
<span style="font-size:10px;font-weight:700;color:#84cc16;letter-spacing:.04em;white-space:nowrap">Client</span>
</div>
<div id="stepBarWrap0" style="display:none;flex:1;height:2px;background:#f2f2f7;margin-bottom:15px;overflow:hidden"><div id="stepBar0" style="width:0%;height:100%;background:#84cc16;transition:width .4s"></div></div>
<div style="display:flex;flex-direction:column;align-items:center;gap:5px">
<div id="stepDot1" style="width:34px;height:34px;border-radius:50%;background:#84cc16;color:#0f172a;display:flex;align-items:center;justify-content:center;font-family:'Bricolage Grotesque',sans-serif;font-size:14px;font-weight:500;transition:all .3s">1</div>
<span id="stepLbl1" style="font-size:10px;font-weight:700;color:#84cc16;letter-spacing:.04em;white-space:nowrap">Facture</span>
</div>
<div style="flex:1;height:2px;background:#f2f2f7;margin-bottom:15px;overflow:hidden"><div id="stepBar1" style="width:0%;height:100%;background:#84cc16;transition:width .4s"></div></div>
<div style="display:flex;flex-direction:column;align-items:center;gap:5px">
<div id="stepDot2" style="width:34px;height:34px;border-radius:50%;background:#f2f2f7;color:#94a3b8;display:flex;align-items:center;justify-content:center;font-family:'Bricolage Grotesque',sans-serif;font-size:14px;font-weight:500;transition:all .3s">2</div>
<span id="stepLbl2" style="font-size:10px;font-weight:700;color:#94a3b8;letter-spacing:.04em;white-space:nowrap">Ton</span>
</div>
<div style="flex:1;height:2px;background:#f2f2f7;margin-bottom:15px;overflow:hidden"><div id="stepBar2" style="width:0%;height:100%;background:#84cc16;transition:width .4s"></div></div>
<div style="display:flex;flex-direction:column;align-items:center;gap:5px">
<div id="stepDot3" style="width:34px;height:34px;border-radius:50%;background:#f2f2f7;color:#94a3b8;display:flex;align-items:center;justify-content:center;font-family:'Bricolage Grotesque',sans-serif;font-size:14px;font-weight:500;transition:all .3s">3</div>
<span id="stepLbl3" style="font-size:10px;font-weight:700;color:#94a3b8;letter-spacing:.04em;white-space:nowrap">Valider</span>
</div>
</div>
<!-- ÉTAPE 0 : Client RECOVMAX (caché pour les autres plans) -->
<div id="wizStep0" style="display:none;padding:16px 24px 24px">
<div style="margin-bottom:16px">
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:18px;font-weight:500;color:#0d0d14;letter-spacing:-.02em;margin-bottom:3px">Pour quel client ? <span class="tip" data-tip="En mode RECOVMAX, chaque facture impayée est rattachée à un client de votre portefeuille.<br><br>Vous gérez les impayés <em>de plusieurs entreprises clientes</em>, donc avant de créer une facture il faut savoir : pour qui ?<br><br>Sélectionnez un client existant ou ajoutez-en un nouveau (il apparaîtra dans Mes clients).">?</span></div>
<div style="font-size:13px;color:#94a3b8" id="wizStep0Sub">Sélectionnez un client de votre cabinet ou ajoutez-en un nouveau.</div>
</div>
<div id="wizStep0Existing" style="display:none">
<div class="fg full" style="margin-bottom:12px">
<label>Client RECOVMAX <span style="color:#dc2626;font-weight:700">*</span> <span class="tip" data-tip="Le client de votre cabinet pour lequel vous suivez cette facture impayée. C'est le créancier de la facture (pas le débiteur).">?</span></label>
<select id="wizClientSelect" style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-family:inherit;font-size:14px;background:#fff"></select>
</div>
<button type="button" onclick="wizShowNewClient()" style="background:transparent;border:1px dashed #84CC16;color:#65a30d;padding:10px 16px;border-radius:10px;font-size:13px;font-weight:500;cursor:pointer;width:100%;margin-bottom:8px">+ Créer un nouveau client</button>
</div>
<div id="wizStep0New" style="display:none;background:#f7fee7;border:1px solid #d9f99d;border-radius:12px;padding:14px 16px;margin-top:8px">
<div style="font-size:13px;font-weight:700;color:#166534;margin-bottom:10px">Nouveau client</div>
<div class="fg" style="margin-bottom:10px"><label>Nom du client * <span class="tip" data-tip="Nom commercial ou raison sociale de votre client final (le créancier des factures à relancer). Ex : <em>Cabinet Horizon</em>, <em>Boulangerie Martin</em>.">?</span></label><input type="text" id="wizNewClientNom" placeholder="Cabinet Horizon" style="width:100%"/></div>
<div class="fg" style="margin-bottom:10px"><label>Email contact <span class="tip" data-tip="Email du référent chez votre client (DAF, gérant). Optionnel à cette étape — vous pourrez le compléter plus tard depuis Mes clients.">?</span></label><input type="email" id="wizNewClientEmail" placeholder="contact@cabinet.fr" style="width:100%"/></div>
<div class="fg" style="margin-bottom:10px"><label>SIRET <span style="font-weight:400;color:#94a3b8">(optionnel)</span> <span class="tip" data-tip="9 ou 14 chiffres. Vérifié automatiquement via l'API INSEE. Optionnel à la création — peut être complété plus tard.">?</span></label><input type="text" id="wizNewClientSiret" placeholder="123 456 789 00012" style="width:100%"/></div>
<div style="display:flex;gap:8px">
<button type="button" id="wizCancelNewClientBtn" onclick="wizCancelNewClient()" style="flex:1;padding:10px;background:#fff;border:1px solid #d9f99d;color:#65a30d;border-radius:8px;font-size:13px;font-weight:500;cursor:pointer">Annuler</button>
<button type="button" onclick="wizCreateNewClient()" style="flex:2;padding:10px;background:#84cc16;border:none;color:#0d0d14;border-radius:8px;font-size:13px;font-weight:500;cursor:pointer">Créer et continuer →</button>
</div>
</div>
<div style="display:flex;gap:8px;margin-top:18px">
<button class="gen-btn" style="flex:1;margin-top:0" onclick="wizNext(1)">Continuer — Saisir la facture →</button>
</div>
</div>
<!-- ÉTAPE 1 : L'essentiel -->
<div id="wizStep1" style="padding:16px 24px 24px">
<div style="margin-bottom:16px">
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:18px;font-weight:500;color:#0d0d14;letter-spacing:-.02em;margin-bottom:3px">Combien vous doit-on ? <span class="tip" data-tip="Les infos de la facture impayée.<br><br>Avec montant + échéance + débiteur, RECOV génère :<br>• une séquence de 7 relances échelonnées (J+5 à J+45)<br>• les pénalités de retard automatiquement<br>• la mise en demeure PDF prête à signer<br><br>Tout est éditable, rien n'est envoyé sans votre validation.">?</span></div>
<div style="font-size:13px;color:#94a3b8">3 infos suffisent — RECOV fait le reste</div>
</div>
<button class="demo-btn" id="btnDemoFictive" onclick="remplirDemoFictive()" type="button">
🧪 <span>Tester sur une facture fictive</span> <span style="margin-left:auto;font-size:11px;opacity:.6">→ voir comment ça marche en 30 sec</span>
</button>
<div class="form-row">
<div class="fg">
<label>Montant de la facture * <span class="tip" data-tip="Le montant Hors Taxes (HT) de votre facture impayée.<br><br>RECOV calcule automatiquement à partir de ce montant :<br>• les pénalités de retard (taux BCE+10% par défaut)<br>• l'indemnité forfaitaire de 40 € (art. D441-5)<br>• le total dû à chaque relance<br><br>Si votre facture est en TTC, indiquez le HT — la TVA reste due par votre client mais elle n'entre pas dans le calcul des pénalités.">?</span></label>
<div style="position:relative"><input type="number" id="fMontant" placeholder="4 500" style="padding-right:60px;font-size:20px;font-weight:800;font-family:'Bricolage Grotesque',sans-serif;letter-spacing:-.02em"/><span style="position:absolute;right:12px;top:50%;transform:translateY(-50%);color:#94a3b8;font-size:13px;font-weight:600;pointer-events:none">€ HT</span></div>
<small style="font-size:11px;color:#94a3b8">Montant hors taxes de la facture impayée</small>
</div>
<div class="fg">
<label>Date d'échéance * <span class="tip" data-tip="La date limite de paiement inscrite sur la facture.<br><br>Concrètement : facture émise le 1er → délai 30 jours → échéance au 30. C'est à partir du <em>lendemain</em> de cette date que la facture devient impayée et que les pénalités courent.<br><br>RECOV planifie les relances en J+5, J+12, J+22, J+35 puis mise en demeure à J+45 après cette échéance.">?</span></label>
<input type="date" id="fEcheance"/>
<small style="font-size:11px;color:#94a3b8">Date à laquelle le paiement aurait dû être reçu</small>
</div>
<!-- Le sélecteur client RECOVMAX est désormais en Step 0 (avant la facture) -->
<input type="hidden" id="fClientCompany" value=""/>
<div class="fg" id="fClientWrap"><label>Nom du client * <span class="tip" data-tip="Le nom du débiteur — l'entreprise (ou la personne) qui doit l'argent.<br><br>⚠️ Ne pas confondre :<br>• <em>Mon profil</em> = vous<br>• <em>Mes clients</em> (RECOVMAX) = vos clients-créanciers<br>• <em>Ce champ</em> = le débiteur, celui qui ne paie pas<br><br>Apparaît dans l'objet et le corps de chaque relance : <em>« Bonjour [nom du client], votre facture FACT-XXX… »</em>">?</span></label><input type="text" id="fClient" placeholder="Cabinet Horizon"/></div>
<div class="fg"><label>Email du client * <span class="tip" data-tip="L'email du débiteur — la personne/société qui doit recevoir les relances.<br><br>RECOV envoie depuis cette adresse (en mode Resend) ou via votre messagerie personnelle (mode Gmail/Outlook). Vous pouvez ajouter plusieurs adresses séparées par <em>,</em> pour mettre en CC le comptable, le DAF, etc.">?</span></label><input type="email" id="fClientEmail" placeholder="contact@client.fr" required/><small style="font-size:11px;color:#94a3b8">Pour l'envoi des relances</small></div>
<div class="fg"><label>N° de facture * <span class="tip" data-tip="Le numéro/référence de la facture impayée tel qu'il apparaît sur le PDF original.<br><br>Cité dans toutes les relances pour que le débiteur identifie sans ambiguïté la facture concernée. Indispensable aussi pour la mise en demeure formelle.<br><br>Format libre : FACT-2025-018, F2025/042, etc.">?</span></label><input type="text" id="fFacture" placeholder="FACT-2025-018"/></div>
<div class="fg" id="fClientSirenWrap">
<label>SIREN / SIRET du client <span style="font-weight:400;color:#94a3b8">(optionnel)</span> <span class="tip" data-tip="Le SIREN/SIRET du débiteur (l'entreprise qui ne paie pas).<br><br>• 9 chiffres = SIREN<br>• 14 chiffres = SIRET<br><br>RECOV vérifie automatiquement via l'API INSEE :<br>• que l'entreprise existe<br>• qu'elle est toujours active (pas en cessation)<br>• la dénomination officielle (pour repérer une éventuelle erreur de saisie)">?</span></label>
<input type="text" id="fClientSiren" placeholder="123 456 789" onblur="checkClientSiren()"/>
<div id="fClientSirenStatus" style="display:none;font-size:11px;margin-top:4px;padding:6px 8px;border-radius:6px"></div>
</div>
<div class="fg full" id="fRegimeWrap">
<label>Régime juridique du débiteur <span class="tip" data-tip="Détermine le bon calcul de pénalités et l'application légitime de l'indemnité forfaitaire de 40 €.<br><br>• Entreprise commerciale (SAS, SARL, EURL, micro-BIC) → BCE+10 pts + 40 € (L441-10 + D441-5)<br><br>• Profession libérale (avocat, médecin, kiné, architecte) → taux légal, pas d'indemnité par défaut (1231-6 C. civ.)<br><br>• Administration / Collectivité → BCE+8 pts + 40 € (Décret 2013-269)<br><br>• Particulier (B2C) → taux légal particuliers, pas d'indemnité 40 € (D441-5 réservé B2B)<br><br>Surfacturer un débiteur particulier de 40 € est illégal — d'où ce sélecteur.">?</span></label>
<select id="fRegime" onchange="onRegimeChange()" style="width:100%;padding:10px 12px;border:1.5px solid #e8e8ed;border-radius:10px;font-family:inherit;font-size:14px;background:#fff;color:#0d0d14">
<option value="b2b" selected>Entreprise commerciale (SAS, SARL, EURL, micro-BIC…)</option>
<option value="liberal">Profession libérale (avocat, médecin, kiné, architecte…)</option>
<option value="admin">Administration / Collectivité publique</option>
<option value="b2c">Particulier (consommateur)</option>
</select>
<small id="fRegimeHelp" style="font-size:11px;color:#94a3b8;display:block;margin-top:6px;line-height:1.55">Taux applicable : BCE refi + 10 pts · Indemnité forfaitaire 40 € due (D441-5)</small>
</div>
</div>
<button class="gen-btn" onclick="wizNext(2)">Suivant — Choisir le ton de la relance →</button>
</div>
<!-- ÉTAPE 2 : Le profil client -->
<div id="wizStep2" style="display:none;padding:16px 24px 24px">
<div style="margin-bottom:18px">
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:18px;font-weight:500;color:#0d0d14;letter-spacing:-.02em;margin-bottom:3px">Ton de la relance <span class="tip" data-tip="Le style et le niveau d'autorité que RECOV applique à toute la séquence de 7 relances.<br><br>• Ferme : direct, sans préliminaires, ton de mise en demeure dès le départ. Pour les mauvais payeurs récurrents.<br>• Professionnel : courtois mais sans ambiguïté. Le standard B2B.<br>• Cordial : doux, mention de la relation, ouvre le dialogue. Pour les bons clients en retard exceptionnel.<br><br>Vous pourrez ajuster le ton à chaque étape avant envoi.">?</span></div>
<div style="font-size:13px;color:#94a3b8">Adapte le style de toute la séquence</div>
</div>
<div class="profil-row" style="gap:10px">${profilBtns}</div>
<div style="margin-top:20px;padding-top:18px;border-top:1px solid #f2f2f7">
<div style="font-size:13px;font-weight:700;color:#0d0d14;margin-bottom:12px">Historique avec ce client <span class="tip" data-tip="Le scoring du débiteur ajuste l'agressivité de la séquence :<br><br>• Premier impayé : ton standard, bénéfice du doute, étapes complètes<br>• Retards occasionnels : ton un peu plus ferme, mention de la régularité du retard<br>• Mauvais payeur récurrent : démarrage direct sur le ton ferme, mise en demeure proposée plus tôt (J+25 au lieu de J+45)<br><br>Information privée — n'apparaît jamais dans les emails envoyés.">?</span></div>
<div style="display:flex;flex-direction:column;gap:8px" id="scoringOpts">
<label style="display:flex;align-items:center;gap:10px;padding:12px 14px;border:1.5px solid #e8e8ed;border-radius:12px;cursor:pointer;font-size:13px;background:#f8f8fa">
<input type="radio" name="clientScoring" value="first" checked style="accent-color:#84cc16;width:16px;height:16px;flex-shrink:0"> <span>🆕 Premier impayé avec ce client</span>
</label>
<label style="display:flex;align-items:center;gap:10px;padding:12px 14px;border:1.5px solid #e8e8ed;border-radius:12px;cursor:pointer;font-size:13px;background:#f8f8fa">
<input type="radio" name="clientScoring" value="occasional" style="accent-color:#84cc16;width:16px;height:16px;flex-shrink:0"> <span>⚠️ Retards occasionnels (1-2 fois)</span>
</label>
<label style="display:flex;align-items:center;gap:10px;padding:12px 14px;border:1.5px solid #e8e8ed;border-radius:12px;cursor:pointer;font-size:13px;background:#f8f8fa">
<input type="radio" name="clientScoring" value="recurring" style="accent-color:#84cc16;width:16px;height:16px;flex-shrink:0"> <span>🔴 Mauvais payeur récurrent</span>
</label>
</div>
</div>
<div style="display:flex;gap:8px;margin-top:20px">
<button class="btn-outline" style="flex:1;text-align:center" onclick="wizNext(1)">← Retour</button>
<button class="gen-btn" style="flex:2;margin-top:0" onclick="wizNext(3)">Suivant — Valider →</button>
</div>
</div>
<!-- ÉTAPE 3 : Contexte + validation -->
<div id="wizStep3" style="display:none;padding:16px 24px 24px">
<div style="margin-bottom:18px">
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:18px;font-weight:500;color:#0d0d14;letter-spacing:-.02em;margin-bottom:3px">Contexte et validation <span class="tip" data-tip="Dernière étape avant génération de la séquence.<br><br>Vous pouvez ajouter du contexte libre qui aidera RECOV à personnaliser le ton de chaque email (mission livrée, interlocuteur précis, relance déjà tentée par téléphone, etc.).<br><br>Au clic sur <em>Lancer la séquence</em>, RECOV crée le dossier dans Mes dossiers, planifie les 7 relances et la mise en demeure.">?</span></div>
<div style="font-size:13px;color:#94a3b8">Optionnel — enrichit la personnalisation des relances</div>
</div>
<div class="fg full" style="margin-bottom:16px">
<label>Contexte <span style="font-weight:400;color:#94a3b8">(optionnel)</span> <span class="tip" data-tip="Toute info qui aide à personnaliser le ton :<br><br>• <em>Mission livrée en avance, bon relationnel jusqu'ici</em> → relance plus douce<br>• <em>DRH interlocuteur, paie via service comptable</em> → CC comptable mentionné<br>• <em>Déjà relancé deux fois par téléphone, sans réponse</em> → ton plus ferme dès la 1ère relance<br><br>RECOV ajuste les emails sans révéler ce contexte au débiteur.">?</span></label>
<input type="text" id="fContexte" placeholder="Ex : mission livrée en avance, DRH interlocuteur, déjà relancé par tél…"/>
<small style="font-size:11px;color:#94a3b8">Toute info utile pour personnaliser les relances</small>
</div>
<div style="background:#f7fee7;border:1.5px solid #d9f99d;border-radius:12px;padding:14px 16px;margin-bottom:16px;font-size:13px;line-height:1.7;color:#166534" id="wizSummary"></div>
<div style="display:flex;gap:8px">
<button class="btn-outline" style="flex:1;text-align:center" onclick="wizNext(2)">← Retour</button>
<button class="gen-btn" style="flex:2;margin-top:0" onclick="creerCreance()">Lancer la séquence →</button>
</div>
<div style="margin-top:14px;padding:10px 14px;background:#f8f8fa;border-radius:10px;font-size:11px;color:#94a3b8;line-height:1.6;display:flex;gap:8px;align-items:flex-start">
<span style="font-size:14px;flex-shrink:0">🔒</span>
<span>Coordonnées utilisées uniquement pour ce dossier. Jamais partagées. <a href="confidentialite.html" style="color:#84cc16">En savoir plus</a></span>
</div>
</div>
</div>
</div>`;
// RECOVMAX : afficher Step 0 (sélection/création client) avant la saisie facture
setTimeout(()=>{try{wizSetupStep0();}catch(e){console.warn('wizSetupStep0',e);}},50);
},
renderDetail(app,creanceId){
const c=normalizeCreance(Store.getCreance(creanceId));
if(!c){this.renderDashboard(app);return;}
const etapes=safeArray(c.etapes).map((e,i)=>normalizeEtape(e,i));
c.etapes=etapes;
const pen=Penalites.calculer(c.montant,c.dateEcheance,new Date().toISOString().split('T')[0],{regime:c.regimeDebiteur||'b2b',tauxFige:c.tauxApplique});
// Feedback après envoi
let feedbackHtml='';
if(this._lastSentClient){
feedbackHtml=`<div id="sendFeedback" style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:14px;padding:16px;margin-bottom:16px;text-align:center;animation:fadeInUp .4s ease">
<div style="font-size:24px;margin-bottom:6px">✅</div>
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:18px;font-weight:500;color:#166534;margin-bottom:4px">Relance envoyée</div>
<div style="font-size:13px;color:#166534">${this._lastSentLabel||'Étape'} pour ${this._lastSentClient}</div>
${this._lastSentNext?'<div style="font-size:12px;color:#15803d;margin-top:6px;opacity:.7">Prochaine étape prévue le '+this._lastSentNext+' — RECOV vous enverra un rappel</div>':''}
${this._firstSendCelebration?'<div style="margin-top:10px;padding-top:10px;border-top:1px solid #bbf7d0;font-size:13px;color:#166534;font-weight:600">🎉 Bravo, votre première relance est partie ! Vous recevrez un rappel quand la prochaine sera à envoyer.</div>':''}
</div>`;
this._lastSentClient=null;this._lastSentLabel=null;this._lastSentNext=null;this._firstSendCelebration=false;
}
const todayTs=new Date().toISOString().split('T')[0];
const sevenDaysAgo=new Date(Date.now()-7*86400000).toISOString().split('T')[0];
// Compter les étapes sautées (date dépassée > 7j, pas générées, pas envoyées)
const etapesSautees=etapes.filter(e=>e.statut==='a_envoyer'&&e.dateEnvoi<=sevenDaysAgo&&!e.genere&&!e.sujet);
const prochaineActionIdx=etapes.findIndex(e=>e.statut==='a_envoyer'&&!(e.dateEnvoi<=sevenDaysAgo&&!e.genere&&!e.sujet));
const envoyees=etapes.filter(e=>e.statut==='envoye').length;
const pctProgress=Math.round(envoyees/8*100);
// ── BANDEAU EXPLICATION ÉTAPES SAUTÉES + INFO JURIDIQUE ──
let skipInfoHtml='';
if(etapesSautees.length>0){
const joursRetard=Math.round((new Date()-new Date(c.dateEcheance))/(86400000));
const moisRetard=Math.round(joursRetard/30);
if(joursRetard>730){
// > 2 ans — prescription B2C atteinte, B2B encore possible
skipInfoHtml=`<div style="background:#fef2f2;border:2px solid #fecaca;border-radius:14px;padding:16px;margin-bottom:16px">
<div style="font-size:14px;font-weight:700;color:#991b1b;margin-bottom:6px">⚠️ Facture échue depuis plus de 2 ans (${moisRetard} mois)</div>
<div style="font-size:13px;color:#991b1b;line-height:1.7">
Information importante :<br>
• B2C (client particulier) : le délai de prescription est de 2 ans (art. L.218-2 Code de la consommation). Cette créance est potentiellement prescrite. Le débiteur peut invoquer la prescription.<br>
• B2B (client professionnel) : le délai de prescription est de 5 ans (art. L.110-4 Code de commerce). La créance est encore recouvrable.<br><br>
RECOV est un copilote de recouvrement amiable, pas un service juridique. Pour évaluer la pertinence d'une relance sur une créance de cette ancienneté, consultez un professionnel du droit.
</div>
</div>`;
} else if(joursRetard>180){
// > 6 mois — avertissement fort
skipInfoHtml=`<div style="background:#fef2f2;border:1px solid #fecaca;border-radius:14px;padding:16px;margin-bottom:16px">
<div style="font-size:14px;font-weight:700;color:#991b1b;margin-bottom:6px">📅 Facture échue depuis ${moisRetard} mois</div>
<div style="font-size:13px;color:#991b1b;line-height:1.7">
Les étapes de relance progressive ne sont plus adaptées à ce délai. À titre informatif :<br>
• Une mise en demeure reste possible à tout moment avant la prescription (5 ans B2B, 2 ans B2C)<br>
• Les pénalités de retard sont exigibles de plein droit (art. L.441-10 Code de commerce)<br>
• Aucune obligation légale de relance graduelle préalable<br><br>
RECOV vous propose de rédiger directement l'étape adaptée. Vous restez seul décisionnaire de chaque envoi.
</div>
</div>`;
} else if(etapesSautees.length>=7){
// > 60 jours — toutes les étapes standard dépassées
skipInfoHtml=`<div style="background:#fef2f2;border:1px solid #fecaca;border-radius:14px;padding:16px;margin-bottom:16px">
<div style="font-size:14px;font-weight:700;color:#991b1b;margin-bottom:6px">📅 Facture échue depuis ${joursRetard} jours</div>
<div style="font-size:13px;color:#991b1b;line-height:1.7">
La séquence standard de 60 jours est dépassée. Vous pouvez toutefois :<br>
• Envoyer une mise en demeure si ce n'est pas encore fait<br>
• Transmettre votre dossier de relances à un professionnel (avocat, commissaire de justice)<br><br>
La créance n'est pas prescrite (5 ans B2B, 2 ans B2C). Les pénalités de retard continuent de courir.
</div>
</div>`;
} else {
// Quelques étapes sautées mais situation encore récente
skipInfoHtml=`<div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:14px;padding:16px;margin-bottom:16px">
<div style="font-size:14px;font-weight:700;color:#166534;margin-bottom:6px">Facture échue depuis ${joursRetard} jours — RECOV vous propose la relance adaptée</div>
<div style="font-size:13px;color:#166534;line-height:1.7">Pas besoin de reprendre depuis le début. L'étape ci-dessous est calibrée pour votre situation.</div>
</div>`;
}
}
// ── BLOC ACTION (étape active) ──
const activeEtape=prochaineActionIdx>=0?etapes[prochaineActionIdx]:null;
const activeIdx=prochaineActionIdx;
const hasContent=activeEtape&&activeEtape.genere&&activeEtape.sujet;
let actionHtml='';
if(activeEtape){
const guide=ETAPES_GUIDE[activeIdx]||{quand:'',quoi:'',conseil:''};
// ── BLOCAGE TEMPOREL : vérifier si la dernière relance est trop récente ──
const DELAIS_MIN=[0,5,7,10,5,10,15,10]; // jours minimum entre chaque étape
const derniereEnvoyee=etapes.slice(0,activeIdx).reverse().find(e=>e.statut==='envoye');
let bloque=false;
let joursRestants=0;
if(derniereEnvoyee&&derniereEnvoyee.dateEnvoiEffective){
const dernierEnvoi=new Date(derniereEnvoyee.dateEnvoiEffective||derniereEnvoyee.dateEnvoi);
const delaiMin=DELAIS_MIN[activeIdx]||5;
const joursDepuis=Math.round((new Date()-dernierEnvoi)/(864e5));
if(joursDepuis<delaiMin){bloque=true;joursRestants=delaiMin-joursDepuis;}
}
// ── CONTEXTE : ligne simple précédente / suivante ──
const prevEnvoyee=etapes.slice(0,activeIdx).reverse().find(e=>e.statut==='envoye');
const nextEtape=activeIdx<7?etapes[activeIdx+1]:null;
let contexteHtml='';
if(prevEnvoyee){const prevDate=prevEnvoyee.dateEnvoiEffective||prevEnvoyee.dateEnvoi;const prevIdx=etapes.indexOf(prevEnvoyee);contexteHtml=`<div style="font-size:12px;color:rgba(255,255,255,.4);margin-bottom:14px;padding:10px 14px;border-left:3px solid rgba(255,255,255,.15);line-height:1.6">✓ Étape ${prevIdx+1} — ${prevEnvoyee.label} · envoyée le ${fmtDate(prevDate)}</div>`;}
let nextHtml='';
if(nextEtape){const nextIdx=activeIdx+1;nextHtml=`<div style="font-size:11px;color:rgba(255,255,255,.3);margin-top:10px;text-align:center">Étape suivante : ${nextIdx+1} — ${nextEtape.label} · ${fmtDate(nextEtape.dateEnvoi)}</div>`;}
actionHtml=`<div style="background:linear-gradient(135deg,#0f172a 0%,#1e293b 100%);border-radius:16px;padding:clamp(16px,3vw,24px);color:#fff;margin-bottom:16px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<div style="font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#84cc16">Prochaine action <span class="tip" style="background:rgba(132,204,22,.2);color:#84CC16" data-tip="L'étape de relance que RECOV vous suggère maintenant.<br><br>RECOV planifie 7 étapes échelonnées (J+5, J+12, J+22, J+35, J+45 mise en demeure, J+60 transmission avocat). Cette zone affiche celle qui correspond à la date du jour.<br><br>Si la dernière relance est trop récente, l'étape sera <em>verrouillée</em> avec un compte à rebours — pour laisser le temps au débiteur de répondre.">?</span></div>
<div style="font-size:11px;color:rgba(255,255,255,.4)" title="Date prévue de cet envoi (modifiable via Planifier)">${fmtDate(activeEtape.dateEnvoi)}</div>
</div>
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:clamp(16px,3.5vw,20px);font-weight:500;margin-bottom:4px">Étape ${activeIdx+1} — ${esc(activeEtape.label)}</div>
<div style="font-size:12px;color:rgba(255,255,255,.5);margin-bottom:14px">${guide.quoi}</div>
${contexteHtml}
${bloque?`<div style="width:100%;padding:18px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);border-radius:12px;text-align:center">
<div style="font-size:15px;font-weight:700;color:rgba(255,255,255,.6)">Disponible dans ${joursRestants} jour${joursRestants>1?'s':''}</div>
<div style="font-size:12px;color:rgba(255,255,255,.35);margin-top:6px">Laissez le temps à votre client de réagir à la relance précédente</div>
</div>`
:!hasContent?`<button onclick="genererEtape('${c.id}',${activeIdx})" title="Génère l'objet et le corps de l'email avec le ton choisi pour cette étape. Vous pourrez relire et modifier avant envoi." style="width:100%;padding:18px;background:#16a34a;color:#fff;border:none;border-radius:12px;font-family:inherit;font-size:17px;font-weight:500;cursor:pointer;box-shadow:0 4px 16px rgba(22,163,26,.3)">Préparer la relance →</button>
<div style="font-size:12px;color:rgba(255,255,255,.4);margin-top:8px;text-align:center">Vous relirez et validerez avant tout envoi</div>`
:`<div style="background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);border-radius:10px;padding:12px;margin-bottom:12px">
<label style="font-size:10px;font-weight:700;color:rgba(255,255,255,.4);letter-spacing:.06em;text-transform:uppercase;display:block;margin-bottom:4px">Objet</label>
<input id="sujet_${c.id}_${activeIdx}" type="text" value="${esc(activeEtape.sujet).replace(/"/g,'"')}" style="width:100%;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.15);border-radius:6px;padding:8px 10px;color:#fff;font-family:inherit;font-size:13px;font-weight:500;box-sizing:border-box">
<label style="font-size:10px;font-weight:700;color:rgba(255,255,255,.4);letter-spacing:.06em;text-transform:uppercase;display:block;margin:8px 0 4px">Corps</label>
<textarea id="corps_${c.id}_${activeIdx}" style="width:100%;min-height:100px;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.15);border-radius:6px;padding:10px;color:#fff;font-family:inherit;font-size:12px;line-height:1.6;resize:vertical;box-sizing:border-box;white-space:pre-wrap">${esc(activeEtape.corps)}</textarea>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px">
${c.clientEmail?'<button onclick="envoyerAuto(\''+c.id+'\','+activeIdx+')" title="Envoi immédiat depuis nos serveurs (Resend) avec votre signature. La relance part en votre nom au débiteur, vous recevez un BCC pour confirmation." style="flex:1;padding:14px;background:#84CC16;color:#0d0d14;border:none;border-radius:12px;font-family:inherit;font-size:15px;font-weight:500;cursor:pointer;min-width:160px;letter-spacing:-.005em;transition:background .15s" onmouseover="this.style.background=\'#84CC16\'" onmouseout="this.style.background=\'#84CC16\'">Envoyer maintenant</button>':''}
<button onclick="ouvrirEmail('${c.id}',${activeIdx})" title="Ouvre votre client mail par défaut (Gmail, Outlook, Apple Mail) avec l'email pré-rempli. Vous validez et envoyez depuis votre messagerie habituelle." style="flex:1;padding:14px;background:rgba(255,255,255,.06);color:#e2e8f0;border:1px solid rgba(255,255,255,.18);border-radius:12px;font-family:inherit;font-size:14px;font-weight:500;cursor:pointer;min-width:140px;transition:background .15s" onmouseover="this.style.background='rgba(255,255,255,.1)'" onmouseout="this.style.background='rgba(255,255,255,.06)'">Via votre messagerie</button>
</div>
<div style="display:flex;gap:6px;margin-top:6px;flex-wrap:wrap">
<button onclick="sauverEtapeInline('${c.id}',${activeIdx})" title="Enregistre vos modifications de l'objet/corps sans envoyer. Utile pour reprendre plus tard." style="padding:7px 12px;background:transparent;color:rgba(255,255,255,.55);border:1px solid rgba(255,255,255,.12);border-radius:7px;font-size:11px;cursor:pointer;font-family:inherit;transition:color .15s" onmouseover="this.style.color='rgba(255,255,255,.85)'" onmouseout="this.style.color='rgba(255,255,255,.55)'">Sauvegarder</button>
<button onclick="genererEtape('${c.id}',${activeIdx})" title="Régénère l'email avec un ton différent (ferme/professionnel/cordial). Vos modifications manuelles seront écrasées." style="padding:7px 12px;background:transparent;color:rgba(255,255,255,.55);border:1px solid rgba(255,255,255,.12);border-radius:7px;font-size:11px;cursor:pointer;font-family:inherit;transition:color .15s" onmouseover="this.style.color='rgba(255,255,255,.85)'" onmouseout="this.style.color='rgba(255,255,255,.55)'">Changer le ton</button>
<button onclick="marquerEnvoye('${c.id}',${activeIdx})" title="Si vous avez envoyé la relance par un autre moyen (téléphone, courrier, bouche à oreille), marquez l'étape comme effectuée pour que RECOV planifie la suite." style="padding:7px 12px;background:transparent;color:rgba(255,255,255,.55);border:1px solid rgba(255,255,255,.12);border-radius:7px;font-size:11px;cursor:pointer;font-family:inherit;transition:color .15s" onmouseover="this.style.color='rgba(255,255,255,.85)'" onmouseout="this.style.color='rgba(255,255,255,.55)'">Marquer envoyé</button>
${c.clientEmail&&activeEtape.dateEnvoi?'<button onclick="programmerEnvoi(\''+c.id+'\','+activeIdx+')" title="Programme l envoi pour une date et heure precises. Utile pour ne pas envoyer le vendredi soir ou le week-end." style="padding:7px 12px;background:transparent;color:rgba(255,255,255,.55);border:1px solid rgba(255,255,255,.12);border-radius:7px;font-size:11px;cursor:pointer;font-family:inherit">Planifier l\'envoi</button>':''}
${activeIdx===5&&hasContent?'<button onclick="exporterPDF(\''+c.id+'\')" title="Genere un PDF de mise en demeure officielle avec en-tete legal, montant du, penalites detaillees, mention de prescription et signature pre-formatee. Pret a envoyer en LRAR." style="padding:7px 12px;background:transparent;color:rgba(255,255,255,.55);border:1px solid rgba(255,255,255,.12);border-radius:7px;font-size:11px;cursor:pointer;font-family:inherit">PDF mise en demeure</button>':''}
</div>`}
${nextHtml||''}
</div>`;
} else if(c.statut!=='paye'&&etapesSautees.length>0){
// Toutes les étapes sont dépassées — proposer la dernière étape pertinente
const derniereIdx=etapes.length-1;
const derniere=etapes[derniereIdx];
const miseEnDemeureIdx=etapes.findIndex(e=>e.type==='mise_en_demeure');
const miseEnDemeure=miseEnDemeureIdx>=0?etapes[miseEnDemeureIdx]:null;
const bestIdx=miseEnDemeure&&miseEnDemeure.statut==='a_envoyer'?miseEnDemeureIdx:derniereIdx;
const bestEtape=etapes[bestIdx];
const joursR=Math.round((new Date()-new Date(c.dateEcheance))/(86400000));
const penEscalade=Penalites.calculer(c.montant,c.dateEcheance,new Date().toISOString().split('T')[0],{regime:c.regimeDebiteur||'b2b',tauxFige:c.tauxApplique});
const snapshotLabel=c.montant_escalade
?`Montant figé lors de la transmission : ${fmtMontant(c.montant_escalade)}`
:`Montant actuel estimé : ${fmtMontant(penEscalade.totalDu)}`;
actionHtml=`<div style="background:linear-gradient(135deg,#7f1d1d 0%,#991b1b 100%);border-radius:20px;padding:clamp(20px,4vw,28px);color:#fff;margin-bottom:16px">
<div style="font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#fca5a5;margin-bottom:12px">Séquence standard terminée</div>
<div style="font-size:15px;color:rgba(255,255,255,.85);line-height:1.6;margin-bottom:16px">Facture échue depuis ${joursR} jours. La séquence de relance progressive de 60 jours est dépassée.</div>
<div style="font-size:14px;font-weight:700;color:#fff;margin-bottom:12px">Actions possibles :</div>
<div style="display:flex;flex-direction:column;gap:8px;margin-bottom:16px">
${miseEnDemeure&&miseEnDemeure.statut==='a_envoyer'?`<button onclick="genererEtape('${c.id}',${miseEnDemeureIdx})" style="width:100%;padding:12px;background:#dc2626;color:#fff;border:none;border-radius:10px;font-family:inherit;font-size:14px;font-weight:500;cursor:pointer">Rédiger une mise en demeure</button>`:''}
<div style="font-size:13px;color:rgba(255,255,255,.7);line-height:1.7;padding:12px;background:rgba(255,255,255,.1);border-radius:8px">
📁 Votre dossier de relances (historique, pénalités, courriers) vous a été envoyé par email. Vous pouvez le transmettre à un professionnel de votre choix (avocat, commissaire de justice).
</div>
<div style="font-size:12px;color:rgba(255,255,255,.65);line-height:1.7;padding:10px 12px;background:rgba(255,255,255,.07);border-radius:8px;border-left:3px solid rgba(252,165,165,.5)">
⚖️ ${snapshotLabel}<br>
Les pénalités (art. L441-10) continuent de courir jusqu'au paiement effectif ou au jugement — le montant final sera supérieur.<br>
En cas de condamnation, le juge peut allouer les frais d'avocat engagés en sus (art. 700 CPC).
</div>
</div>
<span style="font-size:11px;opacity:.5">RECOV est un copilote de recouvrement amiable. L'appréciation de la suite à donner vous appartient.</span>
</div>`;
}
// ── RÉSUMÉ COMPACT ──
const isEscaladeDetail=c.outcome==='escalated'||(()=>{try{return etapes.filter(e=>e.statut==='envoye').length>=8;}catch{return false;}})();
// Si escaladée + snapshot disponible : afficher le montant figé
const totalAffiche=isEscaladeDetail&&c.montant_escalade?c.montant_escalade:pen.totalDu;
const totalLabel=isEscaladeDetail&&c.montant_escalade
?`<div style="font-size:10px;color:var(--gris)">Figé à la transmission</div><div style="font-size:9px;color:var(--gris2)">${c.montant_escalade_at?new Date(c.montant_escalade_at).toLocaleDateString('fr-FR',{day:'numeric',month:'short',year:'numeric'}):''}</div>`
:`<div style="font-size:10px;color:var(--gris)">Total dû (pénalités incluses)</div>`;
const resumeHtml=`<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:8px;margin-bottom:16px">
<div style="background:#fff;border:1px solid ${isEscaladeDetail&&c.montant_escalade?'#fde68a':' var(--border)'};border-radius:12px;padding:12px;text-align:center">
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:clamp(18px,4vw,22px);font-weight:500;color:var(--noir)">${fmtMontant(totalAffiche)}</div>
${totalLabel}
</div>
<div style="background:#fff;border:1px solid var(--border);border-radius:12px;padding:12px;text-align:center">
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:clamp(18px,4vw,22px);font-weight:500;color:var(--rouge)">${pen.joursRetard}j</div>
<div style="font-size:10px;color:var(--gris)">de retard</div>
</div>
</div>`;
// ── TIMELINE COLLAPSÉE ──
let timelineHtml='';
etapes.forEach((e,i)=>{
const isActive=i===prochaineActionIdx;
const isSent=e.statut==='envoye';
const isIgnore=e.statut==='ignore';
const isPast=e.statut==='a_envoyer'&&e.dateEnvoi<=todayTs&&!isActive;
const isFuture=e.statut==='a_envoyer'&&e.dateEnvoi>todayTs&&!isActive;
// Badge couleur
let statutColor='var(--gris2)';
if(isSent) statutColor='var(--vert)';
else if(isActive) statutColor='var(--vert)';
else if(isPast) statutColor='#d97706'; // ambre, pas rouge
// Badge icône
let statutLabel=String(i+1);
if(isSent) statutLabel='✓';
else if(isIgnore) statutLabel='—';
else if(isPast) statutLabel='↷'; // "skipped forward"
else if(isActive) statutLabel=String(i+1);
// Tooltip au survol
let tooltip='';
if(isSent){
tooltip=`Envoyée le ${e.dateEnvoiEffective||fmtDate(e.dateEnvoi)}`;
}else if(isIgnore){
tooltip='Étape ignorée manuellement';
}else if(isPast){
const joursEcart=Math.round((new Date()-new Date(e.dateEnvoi))/86400000);
tooltip=`Prévue il y a ${joursEcart} jour${joursEcart>1?'s':''}. Non envoyée — ce n'est pas une erreur. RECOV vous positionne directement sur l'étape adaptée à la situation actuelle. Vous pouvez toujours préparer cette relance si vous le souhaitez.`;
}else if(isFuture){
const joursAvant=Math.round((new Date(e.dateEnvoi)-new Date())/86400000);
tooltip=`Prévue dans ${joursAvant} jour${joursAvant>1?'s':''}. RECOV vous enverra un rappel à cette date.`;
}else if(isActive){
tooltip='Étape en cours — préparez cette relance maintenant.';
}
timelineHtml+=`<div onclick="toggleCollapse(this)" title="${tooltip.replace(/"/g,'"')}" style="display:flex;gap:10px;padding:10px 12px;border-radius:10px;cursor:pointer;transition:background .1s;${isActive?'background:var(--vert3);border:1px solid var(--vert4)':'border:1px solid transparent'}">
<div style="min-width:28px;height:28px;border-radius:50%;background:${statutColor};color:#fff;display:flex;align-items:center;justify-content:center;font-size:${isPast?'14px':'11px'};font-weight:700;flex-shrink:0;opacity:${isPast?'.65':'1'}">${statutLabel}</div>
<div style="flex:1;min-width:0">
<div style="display:flex;justify-content:space-between;align-items:center">
<span style="font-size:13px;font-weight:${isActive?'700':'500'};color:${isPast?'var(--gris2)':isActive?'var(--vert2)':'var(--noir)'};${isPast?'text-decoration:line-through;opacity:.6':''}">${esc(e.label)}</span>
<span style="font-size:10px;color:var(--gris2)">${fmtDate(e.dateEnvoi)}</span>
</div>
${isPast?`<div style="font-size:11px;color:#d97706;margin-top:2px">↑ Survolez pour comprendre pourquoi</div>`:''}
${isSent&&e.sujet?`<div data-collapse style="display:none;margin-top:6px;padding:8px;background:#fff;border-radius:6px;font-size:11px;color:var(--gris);line-height:1.5">Objet : ${esc(e.sujet)}<br><span style="color:var(--gris2)">Envoyé le ${e.dateEnvoiEffective||fmtDate(e.dateEnvoi)}</span></div>`:''}
</div>
</div>`;
});
// ── PROGRESSION ──
const progressHtml=`<div style="margin-bottom:16px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
<span style="font-size:12px;font-weight:700;color:var(--gris)">Progression <span class="tip" data-tip="Nombre de relances envoyées sur les 8 étapes de la séquence.">?</span></span>
<span style="font-size:12px;color:var(--gris2)">${envoyees}/8 relances envoyées</span>
</div>
<div style="height:6px;background:var(--border);border-radius:3px;overflow:hidden"><div style="height:100%;width:${pctProgress}%;background:linear-gradient(90deg,var(--vert),#84cc16);border-radius:3px;transition:width .3s"></div></div>
</div>`;
const justCreated=this._justCreated||false;
this._justCreated=false;
// Step progress bar
const spbHtml=`<div class="step-progress-bar">${etapes.slice(0,8).map((e,i)=>`<div class="spb-item ${e.statut==='envoye'?'done':i===prochaineActionIdx?'current':''}"></div>`).join('')}</div>`;
app.innerHTML=this._nav('')+`
<button class="back-btn" onclick="UI.showView('dashboard')">← Tableau de bord</button>
<div class="detail-header-card">
<div style="flex:1;min-width:0">
<div class="detail-client-name">${esc(c.client)}</div>
<div class="detail-ref">${esc(c.facture)} · Échéance ${fmtDate(c.dateEcheance)}</div>
${spbHtml}
</div>
<div style="text-align:right;flex-shrink:0">
<div class="detail-montant" style="color:${c.statut==='paye'?'#15803d':'#0d0d14'}">${fmtMontant(c.montant)}</div>
<div style="font-size:12px;color:#94a3b8;margin-top:3px">${c.statut==='paye'?'Payée ✓':envoyees+'/8 étapes'}</div>
${c.statut!=='paye'?`<button onclick="marquerPaye('${c.id}')" style="margin-top:8px;padding:7px 12px;background:#f0fdf4;color:#15803d;border:1px solid #bbf7d0;border-radius:8px;font-size:12px;font-weight:500;cursor:pointer;font-family:inherit">✓ Marquer payée</button>`:''}
</div>
</div>
${feedbackHtml}
${justCreated?'<div style="background:#f0fdf4;border-radius:12px;padding:12px 16px;margin-bottom:14px;font-size:13px;color:#166534;font-weight:500">Dossier créé — préparez votre première relance ci-dessous.</div>':''}
${skipInfoHtml}
<div style="display:none">
${c.statut!=='paye'?'<button onclick="marquerPaye(\''+c.id+'\')" style="padding:8px 14px;background:var(--bg2);color:var(--gris);border:1px solid var(--border);border-radius:8px;font-size:12px;font-weight:500;cursor:pointer;font-family:inherit;white-space:nowrap" title="Cliquez quand le client a payé cette facture">Marquer comme payé</button>':'<span style="padding:6px 12px;background:var(--vert3);color:var(--vert);border-radius:8px;font-size:12px;font-weight:500">✓ Payé</span>'}
</div>
${c.clientEmail?'':`<div style="margin-bottom:12px;padding:10px 14px;background:#fffbeb;border:1px solid #fde68a;border-radius:10px;font-size:12px;color:#92400e;display:flex;align-items:center;gap:8px">
<span>✉</span>
<input type="email" placeholder="Email du client (pour les envois)" style="flex:1;font-size:13px;padding:6px 10px;border:1px solid var(--border);border-radius:6px;font-family:inherit" onchange="Store.updateCreance('${c.id}',{clientEmail:this.value});Store.save();showToast('Email enregistré');UI.showView('detail','${c.id}')"/>
</div>`}
${actionHtml}
${progressHtml}
${resumeHtml}
<details class="dash-section" style="margin-bottom:12px;padding:4px">
<summary style="padding:12px 18px;font-size:14px;font-weight:500;color:#0d0d14;cursor:pointer;user-select:none;list-style:none;display:flex;justify-content:space-between">Séquence de relance <span style="color:#94a3b8;font-weight:400">${envoyees}/8</span></summary>
<div style="padding:4px">${timelineHtml}</div>
</details>
<details class="dash-section" style="margin-bottom:12px;padding:4px">
<summary style="padding:12px 18px;font-size:14px;font-weight:500;color:#0d0d14;cursor:pointer;user-select:none;list-style:none;display:flex;justify-content:space-between">Pénalités & montants <span style="color:#94a3b8;font-weight:400;font-size:12px">Détail légal</span></summary>
<div style="padding:4px 18px 14px;font-size:13px;color:#64748b;line-height:1.7">
${fmtMontant(c.montant)} (facture) + ${fmtMontant(pen.montantPenalites)} (pénalités : ${pen.tauxApplicable}% sur ${pen.joursRetard}j) + ${fmtMontant(pen.indemnite)} (indemnité forfaitaire) = ${fmtMontant(pen.totalDu)}
<br><span style="color:var(--gris2)">Art. L441-10 et D441-5 · Taux BCE ${TAUX_SEMESTRE}</span>
${isEscaladeDetail&&c.montant_escalade?`<div style="margin-top:8px;padding:8px 10px;background:#fef3c7;border-radius:8px;color:#92400e;font-size:11px">
Montant figé lors de la transmission : ${fmtMontant(c.montant_escalade)}<br>
Les pénalités (art. L441-10) continuent de courir chaque jour jusqu'au paiement effectif ou au jugement. Le montant au jugement sera supérieur à celui ci-dessus.<br>
En cas de condamnation, le juge peut en outre allouer les frais d'avocat engagés (art. 700 CPC), en sus de l'indemnité forfaitaire de 40 €.
</div>`:''}
${prochaineActionIdx===6?`<div style="margin-top:8px;padding:8px 10px;background:#f0f9ff;border-radius:8px;color:#0369a1;font-size:11px">
Mise en demeure : le montant mentionné dans ce courrier constitue votre référence juridique à la date d'envoi. Les pénalités continuent de courir jusqu'au paiement effectif.
</div>`:''}
</div>
</details>
<details class="dash-section" style="margin-bottom:12px;padding:4px">
<summary style="padding:12px 18px;font-size:14px;font-weight:500;color:#0d0d14;cursor:pointer;user-select:none;list-style:none;display:flex;justify-content:space-between">Options avancées <span style="color:#94a3b8;font-weight:400;font-size:12px">▾</span></summary>
<div style="display:flex;gap:8px;flex-wrap:wrap;padding:0 18px 14px">
<button onclick="programmerRappel('${c.id}',${prochaineActionIdx>=0?prochaineActionIdx:0})" style="padding:8px 14px;border:1px solid #e2e8f0;border-radius:8px;background:#fff;font-size:12px;cursor:pointer;font-family:inherit;color:#64748b;font-weight:500">🔔 Rappel email</button>
<button onclick="ajouterCalendrier('${c.id}',${prochaineActionIdx>=0?prochaineActionIdx:0})" style="padding:8px 14px;border:1px solid #e2e8f0;border-radius:8px;background:#fff;font-size:12px;cursor:pointer;font-family:inherit;color:#64748b;font-weight:500">📅 Calendrier</button>
<button onclick="supprimerCreance('${c.id}')" style="padding:8px 14px;border:1px solid #fecaca;border-radius:8px;background:#fff;font-size:12px;cursor:pointer;font-family:inherit;color:#ef4444;font-weight:500">Supprimer le dossier</button>
</div>
</details>
</div></div>`;
},
renderProfil(app){
const u=Store.getUser();
app.innerHTML=this._nav('profil')+`
<div class="app-main-header" style="margin-bottom:20px">
<div style="flex:1;min-width:0">
<div class="app-main-greeting-title">Mon profil</div>
<div class="app-main-greeting-sub">Informations utilisées dans vos relances</div>
</div>
</div>
<div style="max-width:1080px">
<!-- Infos personnelles -->
<div class="view-card" style="margin-bottom:16px;padding:22px 24px">
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:15px;font-weight:500;color:#0d0d14;letter-spacing:-.02em;margin-bottom:18px">Informations personnelles</div>
<div class="form-row">
<div class="fg"><label>Nom complet <span style="color:#ef4444;font-weight:700">*</span> <span class="tip" data-tip="Votre nom et prénom tels qu'ils apparaîtront en signature de vos relances et en haut des mises en demeure PDF.<br><br>En mode RECOVMAX, c'est votre nom à vous (le pro de l'admin) — pas celui de votre client. Le client final apparaît dans la mention de mandat.">?</span></label><input type="text" id="pNom" value="${u.nom||''}" placeholder="Votre nom complet" required/></div>
<div class="fg"><label>Société <span class="tip" data-tip="Le nom de votre structure juridique. EURL, SASU, micro-entreprise, ou simplement votre nom commercial.<br><br>Apparaît dans le pied de page des relances et sur les mises en demeure PDF. Optionnel mais renforce le sérieux.">?</span></label><input type="text" id="pSociete" value="${u.societe||''}"/></div>
<div class="fg"><label>SIRET / SIREN <span class="tip" data-tip="Optionnel mais recommandé.<br><br>• SIREN : 9 chiffres, l'identifiant national de votre entreprise<br>• SIRET : 14 chiffres (SIREN + 5 chiffres de l'établissement)<br><br>RECOV vérifie le numéro auprès de l'INSEE et l'inclut dans vos relances pour rassurer le débiteur sur votre légitimité juridique.">?</span></label><input type="text" id="pSiret" value="${u.siret||''}" placeholder="Ex : 948914072 (SIREN) ou 14 chiffres (SIRET)"/><div id="pSiretStatus" style="display:none;font-size:11px;margin-top:4px;padding:6px 8px;border-radius:6px"></div></div>
<div class="fg"><label>Adresse <span class="tip" data-tip="L'adresse postale de votre entreprise. Apparaît en pied de relance et obligatoirement sur les mises en demeure PDF (mention légale).<br><br>Indiquez le numéro, la rue, le code postal et la ville sur une seule ligne.">?</span></label><input type="text" id="pAdresse" value="${u.adresse||''}"/></div>
<div class="fg"><label>Ville <span class="tip" data-tip="Ville du siège social, mentionnée dans les courriers de mise en demeure (champ <em>'Fait à [ville], le [date]'</em>).">?</span></label><input type="text" id="pVille" value="${u.ville||''}"/></div>
<div class="fg"><label>Email <span class="tip" data-tip="Email de votre compte. Non modifiable directement depuis cette page (sécurité). Pour le changer, contactez le support.<br><br>Cet email apparaît dans la signature des relances pour permettre au débiteur de vous joindre.">?</span></label><input type="email" id="pEmail" value="${u.email||''}" readonly style="opacity:.5"/></div>
</div>
<div style="margin-top:20px;padding-top:16px;border-top:1px solid #f2f2f7">
<button class="gen-btn" style="width:auto;padding:12px 28px;margin-top:0;font-size:14px" onclick="sauverProfil()">Enregistrer →</button>
</div>
</div>
<!-- Lien de paiement -->
<div class="view-card" style="margin-bottom:16px;padding:22px 24px">
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:15px;font-weight:500;color:#0d0d14;letter-spacing:-.02em;margin-bottom:6px">
Lien de paiement <span style="font-weight:400;color:#94a3b8;font-size:13px">(optionnel)</span> <span class="tip" data-tip="Optionnel. Si vous avez un lien de paiement direct (Stripe, SumUp, PayPal, Lydia…), RECOV l'inclut dans vos relances pour faciliter le règlement.<br><br>⚠️ Mode RECOVMAX : ce lien doit être celui de <em>votre client final</em> (le créancier), jamais le vôtre. Encaisser pour un tiers est réglementé par l'ACPR.<br><br>Vous pouvez aussi rester sur un virement bancaire classique — c'est tout aussi valable juridiquement.">?</span>
</div>
<p style="font-size:13px;color:#64748b;line-height:1.6;margin-bottom:12px">Vous pouvez ajouter votre lien Stripe, SumUp, PayPal ou laisser vide pour proposer un virement bancaire classique. RECOV s'adapte selon ce que vous renseignez.</p>
<input type="url" id="pLienPaiement" value="${u.lienPaiement||''}" placeholder="https://buy.stripe.com/… ou https://paypal.me/… (optionnel)"
style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;font-family:inherit;color:#0d0d14;outline:none;transition:border .15s"
onfocus="this.style.borderColor='#65A30D'" onblur="this.style.borderColor='#e2e8f0'"/>
<div style="font-size:11px;color:#94a3b8;margin-top:6px;line-height:1.5">Si renseigné, ce lien est inséré dans le corps de chaque relance — jamais dans l'objet.</div>
</div>
<!-- Données -->
<div class="view-card" style="margin-bottom:16px;padding:22px 24px">
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:15px;font-weight:500;color:#0d0d14;letter-spacing:-.02em;margin-bottom:14px">Vos données <span class="tip" data-tip="Exportez toutes vos créances en JSON pour vos archives comptables. Importez pour restaurer ou migrer.">?</span></div>
<div style="background:#f7fee7;border:1.5px solid #d9f99d;border-radius:12px;padding:13px 16px;margin-bottom:16px;font-size:13px;color:#166534;line-height:1.6">
Données synchronisées sur un serveur sécurisé — retrouvez vos créances depuis n'importe quel appareil. Exportez en JSON pour vos archives.
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button style="display:inline-flex;align-items:center;gap:6px;padding:10px 18px;background:#84cc16;color:#0f172a;border:none;border-radius:10px;font-size:13px;font-weight:500;cursor:pointer;font-family:'Bricolage Grotesque',sans-serif" onclick="exporterDonnees()">Exporter (JSON)</button>
<label style="display:inline-flex;align-items:center;padding:10px 18px;background:#f2f2f7;color:#0d0d14;border:none;border-radius:10px;font-size:13px;font-weight:500;cursor:pointer;font-family:inherit">Importer des données<input type="file" accept=".json" style="display:none" onchange="importerDonnees(event)"/></label>
</div>
</div>
<!-- Abonnement -->
<div class="view-card" style="margin-bottom:16px;padding:22px 24px">
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:15px;font-weight:500;color:#0d0d14;letter-spacing:-.02em;margin-bottom:10px">Abonnement</div>
<p style="font-size:13px;color:#64748b;margin-bottom:16px;line-height:1.6">Gérez votre abonnement, changez de moyen de paiement ou résiliez depuis le portail Stripe.</p>
<button style="display:inline-flex;align-items:center;padding:10px 18px;background:#f2f2f7;color:#0d0d14;border:none;border-radius:10px;font-size:13px;font-weight:500;cursor:pointer;font-family:inherit;transition:background .15s" onmouseover="this.style.background='#e8e8ed'" onmouseout="this.style.background='#f2f2f7'" onclick="gererAbonnement()">Gérer mon abonnement →</button>
</div>
<!-- Avis -->
<div class="view-card" style="margin-bottom:16px;padding:22px 24px">
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:15px;font-weight:500;color:#0d0d14;letter-spacing:-.02em;margin-bottom:6px">Votre avis</div>
<p style="font-size:13px;color:#64748b;line-height:1.6;margin-bottom:14px">Votre retour est précieux. Si vous l'autorisez, il sera affiché sur la page d'accueil avec votre prénom.</p>
<div style="display:flex;gap:6px;margin-bottom:12px" id="avisStars">
<button onclick="selectAvisNote(1)" style="font-size:28px;background:none;border:none;cursor:pointer;padding:2px" data-star="1">☆</button>
<button onclick="selectAvisNote(2)" style="font-size:28px;background:none;border:none;cursor:pointer;padding:2px" data-star="2">☆</button>
<button onclick="selectAvisNote(3)" style="font-size:28px;background:none;border:none;cursor:pointer;padding:2px" data-star="3">☆</button>
<button onclick="selectAvisNote(4)" style="font-size:28px;background:none;border:none;cursor:pointer;padding:2px" data-star="4">☆</button>
<button onclick="selectAvisNote(5)" style="font-size:28px;background:none;border:none;cursor:pointer;padding:2px" data-star="5">☆</button>
</div>
<textarea id="avisTexte" rows="3" placeholder="Décrivez votre expérience avec RECOV..." style="width:100%;resize:vertical;font-family:'DM Sans',sans-serif;border:1.5px solid #e8e8ed;border-radius:10px;padding:10px 12px;font-size:13px;box-sizing:border-box;outline:none;transition:border-color .15s" onfocus="this.style.borderColor='#84cc16'" onblur="this.style.borderColor='#e8e8ed'"></textarea>
<div class="form-row" style="margin-top:10px">
<div class="fg"><label>Prénom</label><input type="text" id="avisPrenom" placeholder="Pierre"/></div>
<div class="fg"><label>Métier</label><input type="text" id="avisMetier" placeholder="Consultant IT"/></div>
<div class="fg"><label>Ville</label><input type="text" id="avisVille" placeholder="Lyon"/></div>
<div class="fg" style="display:flex;align-items:flex-end"><label style="display:flex;align-items:center;gap:6px;cursor:pointer;font-size:12px;font-weight:500;color:#64748b;text-transform:none;letter-spacing:0"><input type="checkbox" id="avisPublic" checked style="width:auto;margin:0;accent-color:#84cc16"> Afficher sur la page d'accueil</label></div>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:16px">
<span style="font-size:11px;color:#94a3b8" id="avisStatus"></span>
<button class="gen-btn" style="width:auto;padding:11px 24px;margin-top:0;font-size:14px" onclick="envoyerAvis()">Envoyer mon avis →</button>
</div>
</div>
<!-- Zone danger -->
<div class="view-card" style="margin-bottom:32px;padding:22px 24px">
<details style="cursor:pointer">
<summary style="font-size:13px;color:#94a3b8;user-select:none;font-weight:500;list-style:none">Zone danger — Supprimer mon compte…</summary>
<div style="margin-top:16px;display:flex;flex-direction:column;gap:12px">
<div style="padding:14px 16px;background:#fffbeb;border:1.5px solid #fde68a;border-radius:12px">
<p style="font-size:13px;color:#92400e;margin-bottom:10px;line-height:1.6">Supprimer toutes les créances (reset complet des dossiers, garde le compte).</p>
<button style="padding:9px 16px;background:#fff;color:#f59e0b;border:1.5px solid #fde68a;border-radius:9px;font-size:13px;font-weight:500;cursor:pointer;font-family:inherit" onclick="supprimerToutesCreances()">Vider tous mes dossiers</button>
</div>
<div style="padding:14px 16px;background:#fff5f5;border:1.5px solid #fecaca;border-radius:12px">
<p style="font-size:13px;color:#991b1b;margin-bottom:10px;line-height:1.6">La suppression de compte est définitive. Résiliez d'abord votre abonnement Stripe.</p>
<button style="padding:9px 16px;background:#fff;color:#ef4444;border:1.5px solid #fecaca;border-radius:9px;font-size:13px;font-weight:500;cursor:pointer;font-family:inherit" onclick="supprimerCompte()">Supprimer mon compte et mes données</button>
</div>
</div>
</details>
</div>
</div>
</div></div>`;
},
// ═══════════════════════════════════════════════════════════════════════════
// RECOVMAX : vue Mes clients (multi-clients)
// ═══════════════════════════════════════════════════════════════════════════
async renderClients(app){
const plan=Store._data?.user?.plan||'fondateur';
if(plan!=='recovmax'){
app.innerHTML=this._nav('clients')+`
<div class="app-main-header" style="margin-bottom:20px">
<div><div class="app-main-greeting-title">Mes clients</div></div>
</div>
<div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:14px;padding:24px;max-width:1080px">
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:18px;font-weight:500;color:#0d0d14;margin-bottom:8px">Fonctionnalité réservée à RECOVMAX</div>
<p style="font-size:14px;color:#64748b;line-height:1.65;margin-bottom:14px">Le suivi multi-clients est inclus dans l'offre RECOVMAX (19 €/mois — tarif fondateur maintenu tant que l'abonnement reste actif sur 30 places — 50 clients distincts, 50 créances actives, 1 000 relances par mois).</p>
<a href="/recovmax.html" style="display:inline-flex;align-items:center;gap:6px;background:#84CC16;color:#0d0d14;padding:11px 20px;border-radius:10px;font-size:14px;font-weight:500;text-decoration:none">Découvrir RECOVMAX →</a>
</div>`;
return;
}
// Affichage initial avec spinner
app.innerHTML=this._nav('clients')+`
<div class="app-main-header" style="margin-bottom:20px">
<div style="flex:1;min-width:0">
<div class="app-main-greeting-title">Mes clients</div>
<div class="app-main-greeting-sub">Suivi multi-clients RECOVMAX</div>
</div>
<button onclick="showClientForm()" style="background:#84CC16;color:#0d0d14;padding:10px 18px;border-radius:9px;font-family:inherit;font-size:14px;font-weight:500;border:none;cursor:pointer;display:inline-flex;align-items:center;gap:6px">
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14" stroke-linecap="round"/></svg>
Ajouter un client
</button>
</div>
<div id="clientsList" style="text-align:center;padding:40px;color:#94a3b8">Chargement…</div>`;
const clients=await Store.clients.list();
const creances=Store.getCreances();
const listEl=document.getElementById('clientsList');
if(!listEl)return;
if(clients.length===0){
listEl.innerHTML=`<div style="background:#fff;border:1px dashed #e2e8f0;border-radius:14px;padding:48px 24px;text-align:center;max-width:1080px">
<div style="width:48px;height:48px;background:#ecfccb;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;margin-bottom:14px">
<svg width="24" height="24" fill="none" stroke="#65a30d" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/></svg>
</div>
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:18px;font-weight:500;color:#0d0d14;margin-bottom:6px">Aucun client suivi pour l'instant</div>
<p style="font-size:14px;color:#64748b;line-height:1.6;margin-bottom:18px;max-width:380px;margin-left:auto;margin-right:auto">Avant de créer une créance dans RECOVMAX, ajoutez le ou les clients dont vous suivez les factures impayées.</p>
<button onclick="showClientForm()" style="background:#0d0d14;color:#fff;padding:11px 22px;border-radius:10px;font-family:inherit;font-size:14px;font-weight:500;border:none;cursor:pointer">Ajouter mon premier client →</button>
</div>`;
return;
}
// Render cards clients
const cards=clients.map(cl=>{
const cs=creances.filter(c=>c.clientCompanyId===cl.id);
const enCours=cs.filter(c=>c.statut==='en_relance');
const total=enCours.reduce((s,c)=>s+(parseFloat(c.montant)||0),0);
return `<div class="client-card" onclick="UI.showView('client-detail','${escHtml(cl.id)}')" style="background:#fff;border:1px solid #e2e8f0;border-radius:14px;padding:20px;cursor:pointer;transition:transform .15s,box-shadow .15s,border-color .15s" onmouseover="this.style.transform='translateY(-2px)';this.style.boxShadow='0 8px 24px rgba(0,0,0,.06)';this.style.borderColor='#d9f99d'" onmouseout="this.style.transform='';this.style.boxShadow='';this.style.borderColor='#e2e8f0'">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px">
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:16px;font-weight:500;color:#0d0d14;letter-spacing:-.01em">${escHtml(cl.nom)}</div>
<span style="font-size:10px;color:#65a30d;background:#ecfccb;border:1px solid #d9f99d;padding:3px 8px;border-radius:5px;font-weight:500;letter-spacing:.04em;text-transform:uppercase">${enCours.length} actif${enCours.length>1?'s':''}</span>
</div>
${cl.siret?`<div style="font-size:11px;color:#94a3b8;margin-bottom:8px">SIRET ${escHtml(cl.siret)}</div>`:''}
${cl.email_contact?`<div style="font-size:12px;color:#64748b;margin-bottom:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtml(cl.email_contact)}</div>`:''}
<div style="display:flex;gap:14px;padding-top:14px;border-top:1px solid #f1f5f9">
<div style="flex:1">
<div style="font-size:10px;color:#94a3b8;text-transform:uppercase;letter-spacing:.05em;font-weight:600;margin-bottom:2px">En cours</div>
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:18px;font-weight:500;color:#dc2626;letter-spacing:-.02em">${fmtMontant(total)}</div>
</div>
<div style="flex:1">
<div style="font-size:10px;color:#94a3b8;text-transform:uppercase;letter-spacing:.05em;font-weight:600;margin-bottom:2px">Créances</div>
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:18px;font-weight:500;color:#0d0d14;letter-spacing:-.02em">${cs.length}</div>
</div>
</div>
</div>`;
}).join('');
listEl.innerHTML=`<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:14px;max-width:1080px">${cards}</div>`;
},
// ═══════════════════════════════════════════════════════════════════════════
// RECOVMAX : vue détail d'un client
// ═══════════════════════════════════════════════════════════════════════════
async renderClientDetail(app,clientId){
if(!clientId){this.showView('clients');return;}
const cl=await Store.clients.getById(clientId);
if(!cl){this.showView('clients');return;}
const creances=Store.getCreances().filter(c=>c.clientCompanyId===clientId);
const enCours=creances.filter(c=>c.statut==='en_relance');
const payees=creances.filter(c=>c.statut==='paye');
const totalEnCours=enCours.reduce((s,c)=>s+(parseFloat(c.montant)||0),0);
const totalRecupere=payees.reduce((s,c)=>s+(parseFloat(c.montant)||0),0);
app.innerHTML=this._nav('clients')+`
<div style="margin-bottom:14px"><a href="#" onclick="UI.showView('clients');return false" style="font-size:13px;color:#64748b;text-decoration:none;display:inline-flex;align-items:center;gap:4px">
<svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M19 12H5M12 19l-7-7 7-7" stroke-linecap="round" stroke-linejoin="round"/></svg>
Retour aux clients
</a></div>
<div class="app-main-header" style="margin-bottom:20px">
<div style="flex:1;min-width:0">
<div class="app-main-greeting-title">${escHtml(cl.nom)}</div>
<div class="app-main-greeting-sub">${cl.siret?'SIRET '+escHtml(cl.siret)+' · ':''}${cl.ville?escHtml(cl.ville):''}</div>
</div>
<button onclick="showClientForm('${escHtml(cl.id)}')" style="background:transparent;color:#0d0d14;padding:9px 16px;border:1px solid #e2e8f0;border-radius:9px;font-family:inherit;font-size:13px;font-weight:500;cursor:pointer">Éditer la fiche</button>
</div>
<div style="max-width:1080px">
<!-- KPI grid : memes proportions que le tableau de bord (4 cards) -->
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px;margin-bottom:18px">
<div class="view-card" style="margin-bottom:0;padding:18px 20px">
<div style="font-size:10px;color:#94a3b8;text-transform:uppercase;letter-spacing:.06em;font-weight:600;margin-bottom:6px">En cours</div>
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:26px;font-weight:500;color:#dc2626;letter-spacing:-.03em">${fmtMontant(totalEnCours)}</div>
<div style="font-size:11px;color:#64748b;margin-top:4px">${enCours.length} créance${enCours.length>1?'s':''}</div>
</div>
<div class="view-card" style="margin-bottom:0;padding:18px 20px">
<div style="font-size:10px;color:#94a3b8;text-transform:uppercase;letter-spacing:.06em;font-weight:600;margin-bottom:6px">Récupéré</div>
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:26px;font-weight:500;color:#65a30d;letter-spacing:-.03em">${fmtMontant(totalRecupere)}</div>
<div style="font-size:11px;color:#64748b;margin-top:4px">${payees.length} payée${payees.length>1?'s':''}</div>
</div>
<div class="view-card" style="margin-bottom:0;padding:18px 20px">
<div style="font-size:10px;color:#94a3b8;text-transform:uppercase;letter-spacing:.06em;font-weight:600;margin-bottom:6px">Total créances</div>
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:26px;font-weight:500;color:#0d0d14;letter-spacing:-.03em">${creances.length}</div>
<div style="font-size:11px;color:#64748b;margin-top:4px">depuis l'inscription</div>
</div>
</div>
<!-- Actions client -->
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:18px">
<button onclick="UI.showView('nouvelle');setTimeout(()=>{var s=document.getElementById('fClientCompany');if(s)s.value='${escHtml(cl.id)}';},300)" style="background:#0d0d14;color:#fff;padding:11px 20px;border-radius:11px;font-family:inherit;font-size:13px;font-weight:500;border:none;cursor:pointer;letter-spacing:-.005em;transition:background .15s" onmouseover="this.style.background='#1e1e2e'" onmouseout="this.style.background='#0d0d14'">Créance pour ce client</button>
<button onclick="exportClientReport('${escHtml(cl.id)}')" style="background:transparent;color:#0d0d14;padding:11px 20px;border:1px solid #e2e8f0;border-radius:11px;font-family:inherit;font-size:13px;font-weight:500;cursor:pointer;transition:border-color .15s" onmouseover="this.style.borderColor='#0d0d14'" onmouseout="this.style.borderColor='#e2e8f0'">Exporter le rapport mensuel</button>
<button onclick="showClientHistory('${escHtml(cl.id)}')" style="background:transparent;color:#0d0d14;padding:11px 20px;border:1px solid #e2e8f0;border-radius:11px;font-family:inherit;font-size:13px;font-weight:500;cursor:pointer;transition:border-color .15s" onmouseover="this.style.borderColor='#0d0d14'" onmouseout="this.style.borderColor='#e2e8f0'">Historique</button>
</div>
<!-- IBAN — bandeau cohérent avec view-card (plus de jaune criard) -->
${cl.iban?`<div class="view-card" style="padding:16px 20px;display:flex;gap:14px;align-items:flex-start;border-left:3px solid #65a30d">
<div style="flex:1;min-width:0">
<div style="font-size:11px;font-weight:700;color:#94a3b8;letter-spacing:.06em;text-transform:uppercase;margin-bottom:4px">IBAN du client</div>
<div style="font-family:'Courier New',monospace;font-size:14px;font-weight:600;color:#0d0d14;margin-bottom:6px">${escHtml(cl.iban.replace(/(.{4})/g,'$1 ').trim())}</div>
<div style="font-size:12.5px;color:#64748b;line-height:1.55">Inclus dans les relances pour règlement direct au créancier. Vérifiez qu'il s'agit bien du compte de votre client final.</div>
</div>
</div>`:`<div class="view-card" style="padding:16px 20px;display:flex;gap:14px;align-items:flex-start;border-left:3px solid #d4d4d8">
<div style="flex:1;min-width:0">
<div style="font-size:11px;font-weight:700;color:#94a3b8;letter-spacing:.06em;text-transform:uppercase;margin-bottom:4px">IBAN du client</div>
<div style="font-size:13.5px;color:#0d0d14;font-weight:500;margin-bottom:6px">Non renseigné</div>
<div style="font-size:12.5px;color:#64748b;line-height:1.55;margin-bottom:10px">Les relances mentionneront <em>« Pour le règlement, merci de revenir vers votre interlocuteur habituel »</em>. Renseignez l'IBAN pour permettre au débiteur de payer directement.</div>
<a href="#" onclick="showClientForm('${escHtml(cl.id)}');return false" style="font-size:13px;color:#0d0d14;font-weight:600;text-decoration:none;border-bottom:1px solid #d4d4d8;padding-bottom:1px">Ajouter l'IBAN du client</a>
</div>
</div>`}
<!-- Liste créances -->
<div style="font-size:11px;font-weight:700;color:#94a3b8;letter-spacing:.06em;text-transform:uppercase;margin:24px 0 10px">Créances de ce client</div>
<div>
${creances.length===0?'<div style="background:#f8f9fb;border:1px solid #e2e8f0;border-radius:12px;padding:24px;text-align:center;color:#94a3b8;font-size:14px">Aucune créance pour ce client</div>':creances.map(c=>{
const stat={en_cours:{color:'#f59e0b',bg:'#fef3c7',label:'En cours'},paye:{color:'#65a30d',bg:'#ecfccb',label:'Payée'},sans_suite:{color:'#94a3b8',bg:'#f1f5f9',label:'Sans suite'}}[c.statut]||{color:'#64748b',bg:'#f8f9fb',label:c.statut};
return `<div onclick="UI.showView('detail','${escHtml(c.id)}')" style="background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:14px 18px;margin-bottom:8px;display:flex;justify-content:space-between;align-items:center;cursor:pointer;gap:14px;flex-wrap:wrap;transition:border-color .15s" onmouseover="this.style.borderColor='#d9f99d'" onmouseout="this.style.borderColor='#e2e8f0'">
<div style="flex:1;min-width:200px"><div style="font-size:14px;font-weight:600;color:#0d0d14">${escHtml(c.facture||'Sans réf')}</div><div style="font-size:12px;color:#64748b">${escHtml(c.client||'')}</div></div>
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:16px;font-weight:500;color:#0d0d14">${fmtMontant(c.montant)}</div>
<span style="font-size:10px;color:${stat.color};background:${stat.bg};padding:4px 10px;border-radius:6px;font-weight:500;letter-spacing:.04em;text-transform:uppercase">${stat.label}</span>
</div>`;
}).join('')}
</div>
</div>`;
}
};
// ── FONCTIONS GLOBALES ──
function toggleCollapse(el){
var col=el.querySelector('[data-collapse]');
if(col)col.style.display=col.style.display==='none'?'block':'none';
}
function sauverEtapeInline(creanceId,etapeNum){
var s=document.getElementById('sujet_'+creanceId+'_'+etapeNum);
var c=document.getElementById('corps_'+creanceId+'_'+etapeNum);
if(s&&c){Store.updateEtape(creanceId,etapeNum,{sujet:s.value,corps:c.value});Store.save();showToast('Sauvegardé');}
}
function togglePwd(inputId,btn){
const input=document.getElementById(inputId);
if(!input)return;
if(input.type==='password'){input.type='text';btn.textContent='🙈';}
else{input.type='password';btn.textContent='👁';}
}
function showLoginError(msg){
const el=document.getElementById('loginError');
if(!el)return;
el.style.display='block';
el.textContent=msg;
}
function hideLoginError(){
const el=document.getElementById('loginError');
if(el)el.style.display='none';
}
function showResetForm(){
const signup=document.getElementById('gateFormSignup');
const login=document.getElementById('gateFormLogin');
const reset=document.getElementById('gateFormReset');
if(signup)signup.style.display='none';
if(login)login.style.display='none';
if(reset)reset.style.display='flex';
const msg=document.getElementById('resetMsg');
if(msg)msg.style.display='none';
}
async function doResetPassword(){
const email=(document.getElementById('resetEmail')||{}).value.trim().toLowerCase();
const msg=document.getElementById('resetMsg');
if(!email||!email.includes('@')){
if(msg){msg.style.display='block';msg.style.background='#fef2f2';msg.style.borderColor='#fecaca';msg.style.color='#991b1b';msg.textContent='Entrez une adresse email valide.';}
return;
}
const btn=document.querySelector('#gateFormReset .gate-btn');
if(btn){btn.disabled=true;btn.textContent='Envoi en cours...';}
const{error}=await supa.auth.resetPasswordForEmail(email,{redirectTo:'https://recov.pro'});
if(btn){btn.disabled=false;btn.textContent='Envoyer le lien de réinitialisation →';}
if(error){
let errMsg=error.message;
if(errMsg.includes('security purposes'))errMsg='Pour des raisons de sécurité, veuillez patienter quelques secondes avant de réessayer.';
else if(errMsg.includes('User not found'))errMsg='Aucun compte trouvé avec cet email.';
else if(errMsg.includes('rate limit'))errMsg='Trop de tentatives. Réessayez dans une minute.';
if(msg){msg.style.display='block';msg.style.background='#fef2f2';msg.style.border='1px solid #fecaca';msg.style.color='#991b1b';msg.textContent=errMsg;}
return;
}
if(msg){msg.style.display='block';msg.style.background='#f0fdf4';msg.style.border='1px solid #bbf7d0';msg.style.color='#166534';msg.textContent='Email envoyé ! Vérifiez votre boîte de réception (et les spams). Le lien est valable 24h.';}
}
function showLoginForm(){
const signup=document.getElementById('gateFormSignup');
const login=document.getElementById('gateFormLogin');
const reset=document.getElementById('gateFormReset');
if(signup)signup.style.display='none';
if(login)login.style.display='flex';
if(reset)reset.style.display='none';
}
function showSignupForm(){
const signup=document.getElementById('gateFormSignup');
const login=document.getElementById('gateFormLogin');
const reset=document.getElementById('gateFormReset');
if(signup)signup.style.display='flex';
if(login)login.style.display='none';
if(reset)reset.style.display='none';
// Détection périmètre géographique — message AVANT toute saisie
// (la garde dure reste le middleware côté /api/register)
checkSignupGeoPerimeter();
}
// ─── Vérification périmètre juridique pour l'inscription ──────────────
// RECOV est conçu pour les créances soumises au droit français (L.441-10,
// loi 2026-307, Factur-X). Le middleware bloque /api/register par 403 dès
// qu'il détecte un pays hors France métropolitaine/Outre-mer via x-vercel-ip-country.
// Cette fonction utilise /api/register lui-même comme oracle (preflight
// GET) pour ne pas créer un 13ᵉ endpoint Vercel (limite Hobby = 12).
// Si oracle = 403, on remplace le formulaire par un message clair avec
// l'email de contact pour exception. Sinon, on laisse passer.
async function checkSignupGeoPerimeter(){
// En local (file://) on passe — pas de header Vercel
if(location.protocol==='file:') return;
const signup=document.getElementById('gateFormSignup');
if(!signup) return;
// Évite les multiples appels (utilisateur qui clique plusieurs fois)
if(signup.dataset.geoChecked==='1') return;
signup.dataset.geoChecked='1';
try{
// Preflight GET sur /api/register — le middleware bloque par 403 si
// pays hors périmètre, sans toucher register.js (qui ignore GET).
const r=await fetch('/api/register?preflight=1',{method:'GET',cache:'no-store'});
if(r.status!==403) return; // OK ou pays inconnu : on laisse le formulaire
let payload={};
try{payload=await r.json();}catch(_){}
if(payload.code!=='JURISDICTION_FR_ONLY') return; // autre 403 (rate-limit, etc.) : ne pas bloquer ici
const country=payload.detected_country||'inconnu';
const perimeter=payload.perimeter||'France métropolitaine et Outre-mer';
const wrap=signup.parentElement;
if(!wrap) return;
const block=document.createElement('div');
block.className='gate-juridiction-block';
block.style.cssText='padding:24px 22px;background:#fef3f2;border:1px solid #fecaca;border-radius:14px;color:#0d0d14;line-height:1.65;font-size:14px';
block.innerHTML=
'<div style="font-family:\'Bricolage Grotesque\',sans-serif;font-weight:600;font-size:16px;margin-bottom:10px;color:#0d0d14">Périmètre juridique de RECOV</div>'+
'<p style="margin:0 0 12px;color:#1e293b">RECOV est conçu pour les créances soumises au <strong>droit français</strong> — article L.441-10 du Code de commerce, loi 2026-307, mentions Factur-X, hébergement UE.</p>'+
'<p style="margin:0 0 12px;color:#1e293b">L\'inscription est ouverte aux professionnels établis en <strong>'+perimeter+'</strong>.</p>'+
'<p style="margin:0 0 14px;font-size:13px;color:#64748b">Connexion détectée depuis : <strong>'+country+'</strong>.</p>'+
'<p style="margin:0;font-size:13px;color:#64748b">Si vous êtes basé en France ou Outre-mer mais actuellement à l\'étranger, écrivez à <a href="mailto:pedro.berbel@dezvolta.org?subject=Inscription%20RECOV%20depuis%20l\'%C3%A9tranger" style="color:#65a30d;font-weight:600;text-decoration:none">pedro.berbel@dezvolta.org</a> pour une inscription manuelle.</p>';
signup.style.display='none';
wrap.insertBefore(block,signup);
}catch(e){
// Réseau coupé : fail-open. Si l'utilisateur soumet quand même, le
// middleware renverra 403 sur le vrai POST et l'erreur s'affichera.
}
}
// ─── RECOVMAX : Form modale ajout/édition client ──────────────────────────────
async function showClientForm(clientId){
const isEdit=!!clientId;
let existing=null;
if(isEdit){existing=await Store.clients.getById(clientId);if(!existing){showToast('Client introuvable');return;}}
if(document.getElementById('clientFormOverlay'))return;
const overlay=document.createElement('div');
overlay.id='clientFormOverlay';
overlay.style.cssText='position:fixed;inset:0;background:rgba(13,13,20,.78);backdrop-filter:blur(6px);z-index:9999;display:flex;align-items:flex-start;justify-content:center;padding:32px 20px;overflow-y:auto;animation:demoFadeIn .2s ease-out';
const e=existing||{};
overlay.innerHTML=`
<div style="background:#fff;border-radius:18px;max-width:580px;width:100%;padding:32px clamp(24px,4vw,36px);box-shadow:0 24px 60px rgba(0,0,0,.3);position:relative;animation:demoSlideUp .25s ease-out">
<button onclick="document.getElementById('clientFormOverlay')?.remove()" aria-label="Fermer" style="position:absolute;top:14px;right:14px;background:transparent;border:none;cursor:pointer;width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;color:#94a3b8" onmouseover="this.style.background='#f1f5f9'" onmouseout="this.style.background='transparent'">
<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" viewBox="0 0 24 24"><path d="M18 6L6 18M6 6l12 12" stroke-linecap="round"/></svg>
</button>
<h3 style="font-family:'Bricolage Grotesque',sans-serif;font-size:22px;font-weight:500;letter-spacing:-.03em;color:#0d0d14;margin:0 0 18px">${isEdit?'Modifier la fiche client':'Ajouter un client'}</h3>
<form id="clientForm" onsubmit="return saveClientForm(event,${isEdit?"'"+clientId+"'":'null'})">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:14px">
<div style="grid-column:span 2"><label style="font-size:12px;font-weight:500;color:#64748b;display:block;margin-bottom:4px">Nom du client <span style="color:#dc2626">*</span> <span class="tip" data-tip="Le nom commercial ou la raison sociale du client de votre cabinet (votre client final, créancier des factures à relancer).<br><br>Exemple : <em>Acme SAS</em>, <em>Cabinet Dupont</em>, <em>Boulangerie Martin</em>.<br><br>C'est ce nom qui apparaîtra dans la mention de mandat de chaque relance : <em>« intervention sur mandat de [nom du client] »</em>.">?</span></label><input type="text" id="cf_nom" required value="${escHtml(e.nom||'')}" placeholder="Ex : Acme SAS" style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-family:inherit;font-size:14px"></div>
<div><label style="font-size:12px;font-weight:500;color:#64748b;display:block;margin-bottom:4px">Forme juridique <span class="tip" data-tip="Optionnel. Forme légale de votre client : SAS, SASU, EURL, SARL, micro-entreprise, profession libérale…<br><br>Affiné dans les en-têtes de mises en demeure pour la rigueur juridique.">?</span></label><input type="text" id="cf_forme" value="${escHtml(e.forme_juridique||'')}" placeholder="SAS, EURL, Auto-entrepreneur…" style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-family:inherit;font-size:14px"></div>
<div><label style="font-size:12px;font-weight:500;color:#64748b;display:block;margin-bottom:4px">SIRET / SIREN <span class="tip" data-tip="Le numéro SIREN/SIRET de votre client final.<br><br>• 9 chiffres = SIREN<br>• 14 chiffres = SIRET<br><br>Apparaît dans le pied des relances et obligatoirement dans les mises en demeure formelles.<br><br>RECOV vérifie via l'API INSEE que l'entreprise est active.">?</span></label><input type="text" id="cf_siret" value="${escHtml(e.siret||'')}" placeholder="9 ou 14 chiffres" style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-family:inherit;font-size:14px"></div>
<div style="grid-column:span 2"><label style="font-size:12px;font-weight:500;color:#64748b;display:block;margin-bottom:4px">Email contact <span class="tip" data-tip="L'email de la personne référente chez votre client (DAF, gérant, comptable…).<br><br>Utilisé pour les notifications d'avancement (créance créée, mise en demeure générée) et le rapport mensuel automatique. <em>N'est pas l'email du débiteur</em> — celui-ci se renseigne au niveau de chaque créance.">?</span></label><input type="email" id="cf_email" value="${escHtml(e.email_contact||'')}" placeholder="contact@client.fr" style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-family:inherit;font-size:14px"></div>
<div><label style="font-size:12px;font-weight:500;color:#64748b;display:block;margin-bottom:4px">Téléphone <span class="tip" data-tip="Téléphone du référent chez votre client. Utile pour vous, n'apparaît pas dans les relances.">?</span></label><input type="tel" id="cf_tel" value="${escHtml(e.telephone||'')}" style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-family:inherit;font-size:14px"></div>
<div><label style="font-size:12px;font-weight:500;color:#64748b;display:block;margin-bottom:4px">Ville <span class="tip" data-tip="Ville du siège de votre client. Mentionnée dans les courriers (champ <em>'Fait à [ville], le [date]'</em>) et les mises en demeure PDF.">?</span></label><input type="text" id="cf_ville" value="${escHtml(e.ville||'')}" style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-family:inherit;font-size:14px"></div>
<div style="grid-column:span 2"><label style="font-size:12px;font-weight:500;color:#64748b;display:block;margin-bottom:4px">Adresse <span class="tip" data-tip="Adresse du siège social de votre client. Apparaît dans les en-têtes de mises en demeure PDF (mention légale obligatoire).">?</span></label><input type="text" id="cf_adr" value="${escHtml(e.adresse||'')}" style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-family:inherit;font-size:14px"></div>
</div>
<!-- IBAN avec warning légal -->
<div style="background:#fef3c7;border:1px solid #fde68a;border-left:4px solid #f59e0b;border-radius:0 12px 12px 0;padding:14px 16px;margin-bottom:14px">
<div style="font-size:13px;font-weight:700;color:#92400e;margin-bottom:6px">⚠️ IBAN du compte de votre client <span class="tip" data-tip="Règle absolue en RECOVMAX : l'IBAN doit être celui du <em>créancier</em> (votre client final), jamais le vôtre.<br><br>Encaisser pour un tiers fait basculer dans une activité de prestataire de services de paiement, réglementée par l'ACPR — agrément obligatoire, avec risque pénal en cas d'infraction.<br><br>RECOV intègre cet IBAN dans chaque relance pour permettre au débiteur de payer directement votre client.">?</span></div>
<div style="font-size:12px;color:#78350f;line-height:1.6;margin-bottom:10px">
Renseignez l'IBAN du compte bancaire de votre client final où le débiteur effectuera son paiement. Ne mettez jamais votre propre IBAN ici — vous deviendriez intermédiaire de paiement réglementé (ACPR), avec risque pénal.
</div>
<input type="text" id="cf_iban" value="${escHtml(e.iban||'')}" placeholder="FR76 ____ ____ ____ ____ ____ ___" style="width:100%;padding:10px 12px;border:1px solid #fde68a;border-radius:8px;font-family:'Courier New',monospace;font-size:13px;background:#fff">
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:14px">
<div><label style="font-size:12px;font-weight:500;color:#64748b;display:block;margin-bottom:4px">Délai paiement par défaut (jours) <span class="tip" data-tip="Délai standard de paiement que ce client accorde à ses propres clients.<br><br>• 30 jours = standard B2B France<br>• 45 jours fin de mois = grandes structures<br>• 60 jours = maximum légal entre pros (art. L441-10 Code de commerce)<br><br>RECOV pré-remplit la date d'échéance à partir de cette valeur.">?</span></label><input type="number" id="cf_delai" value="${e.conditions_paiement_default||30}" min="0" max="180" style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-family:inherit;font-size:14px"></div>
<div><label style="font-size:12px;font-weight:500;color:#64748b;display:block;margin-bottom:4px">Taux pénalité custom (% / an) <span class="tip" data-tip="Taux des pénalités de retard applicable aux factures de ce client.<br><br>• Vide (défaut) = RECOV applique le <em>taux BCE + 10 points</em> mis à jour automatiquement (au 04/2026 : ~14,5 %)<br>• Personnalisé = si les CGV de votre client mentionnent un autre taux (souvent 3× le taux légal pour les particuliers)<br><br>Plus l'indemnité forfaitaire de 40 € par facture impayée (art. D441-5).">?</span></label><input type="number" id="cf_taux" value="${e.taux_penalite_default||''}" placeholder="14.5 (défaut)" step="0.1" style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-family:inherit;font-size:14px"></div>
</div>
<div style="margin-bottom:18px"><label style="font-size:12px;font-weight:500;color:#64748b;display:block;margin-bottom:4px">Notes internes (jamais envoyées au client) <span class="tip" data-tip="Vos notes privées sur ce client. Habitudes de paiement, contacts spécifiques, particularités…<br><br>Visibles uniquement par vous dans RECOV. <em>Jamais incluses dans les relances envoyées</em>.">?</span></label><textarea id="cf_notes" rows="2" style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-family:inherit;font-size:14px;resize:vertical">${escHtml(e.notes||'')}</textarea></div>
<div id="cfStatus" style="font-size:13px;margin-bottom:10px;min-height:18px"></div>
<div style="display:flex;gap:8px">
<button type="submit" style="flex:1;background:#84CC16;color:#0d0d14;padding:13px 22px;border-radius:11px;font-size:14px;font-weight:500;border:none;cursor:pointer;font-family:inherit">${isEdit?'Enregistrer':'Ajouter le client'} →</button>
<button type="button" onclick="document.getElementById('clientFormOverlay')?.remove()" style="background:transparent;color:#0d0d14;padding:13px 22px;border:1px solid #e2e8f0;border-radius:11px;font-size:14px;font-weight:500;cursor:pointer;font-family:inherit">Annuler</button>
</div>
</form>
</div>`;
overlay.addEventListener('click',ev=>{if(ev.target===overlay)overlay.remove();});
document.body.appendChild(overlay);
setTimeout(()=>document.getElementById('cf_nom')?.focus(),50);
}
window.showClientForm=showClientForm;
async function saveClientForm(event,clientId){
event.preventDefault();
const status=document.getElementById('cfStatus');
const payload={
nom:document.getElementById('cf_nom').value.trim(),
forme_juridique:document.getElementById('cf_forme').value.trim()||null,
siret:document.getElementById('cf_siret').value.replace(/\s/g,'')||null,
email_contact:document.getElementById('cf_email').value.trim()||null,
telephone:document.getElementById('cf_tel').value.trim()||null,
ville:document.getElementById('cf_ville').value.trim()||null,
adresse:document.getElementById('cf_adr').value.trim()||null,
iban:document.getElementById('cf_iban').value.replace(/\s/g,'').toUpperCase()||null,
conditions_paiement_default:parseInt(document.getElementById('cf_delai').value)||30,
taux_penalite_default:parseFloat(document.getElementById('cf_taux').value)||null,
notes:document.getElementById('cf_notes').value.trim()||null
};
if(!payload.nom){status.textContent='⚠️ Nom obligatoire';status.style.color='#dc2626';return false;}
status.textContent='⏳ Enregistrement…';status.style.color='#64748b';
try{
if(clientId&&clientId!=='null'){
await Store.clients.update(clientId,payload);
showToast('Client mis à jour ✓');
}else{
await Store.clients.create(payload);
showToast('Client ajouté ✓');
}
document.getElementById('clientFormOverlay')?.remove();
if(typeof UI!=='undefined'&&UI.renderClients)UI.showView('clients');
}catch(err){
status.textContent='⚠️ '+(err.message||'Erreur');status.style.color='#dc2626';
}
return false;
}
window.saveClientForm=saveClientForm;
// ─── Export rapport mensuel client (J4 : génération PDF) ────────────────────
async function exportClientReport(clientId){
// V1 : ouvre une nouvelle page report.html avec le clientId en query
// J4 implémentera la vraie génération PDF
const url='/recovmax-client-report.html?client='+encodeURIComponent(clientId);
window.open(url,'_blank');
}
window.exportClientReport=exportClientReport;
async function showClientHistory(clientId){
const logs=await Store.audit.list({client_company_id:clientId,limit:50});
if(document.getElementById('historyOverlay'))return;
const overlay=document.createElement('div');
overlay.id='historyOverlay';
overlay.style.cssText='position:fixed;inset:0;background:rgba(13,13,20,.78);backdrop-filter:blur(6px);z-index:9999;display:flex;align-items:flex-start;justify-content:center;padding:32px 20px;overflow-y:auto';
const items=logs.length===0?'<div style="text-align:center;color:#94a3b8;padding:40px">Aucune action enregistrée pour ce client</div>':logs.map(l=>{
const d=new Date(l.created_at);
const ago=d.toLocaleDateString('fr-FR',{day:'numeric',month:'short',hour:'2-digit',minute:'2-digit'});
const labels={'client.create':'Client créé','client.update':'Fiche modifiée','client.archive':'Client archivé','creance.create':'Créance créée','creance.update':'Créance modifiée','relance.generate':'Relance générée','relance.send':'Relance envoyée','demeure.generate':'Mise en demeure générée','template.create':'Template créé','template.update':'Template modifié','template.delete':'Template supprimé'};
return `<div style="display:flex;gap:12px;padding:12px 0;border-bottom:1px solid #f1f5f9">
<div style="width:8px;height:8px;border-radius:50%;background:#84CC16;margin-top:6px;flex-shrink:0"></div>
<div style="flex:1"><div style="font-size:13px;color:#0d0d14;font-weight:600">${escHtml(labels[l.action]||l.action)}</div><div style="font-size:11px;color:#94a3b8">${ago}</div></div>
</div>`;
}).join('');
overlay.innerHTML=`<div style="background:#fff;border-radius:18px;max-width:520px;width:100%;padding:32px clamp(24px,4vw,32px);max-height:80vh;overflow-y:auto;position:relative">
<button onclick="document.getElementById('historyOverlay')?.remove()" style="position:absolute;top:14px;right:14px;background:transparent;border:none;cursor:pointer;width:32px;height:32px;color:#94a3b8"><svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" viewBox="0 0 24 24"><path d="M18 6L6 18M6 6l12 12" stroke-linecap="round"/></svg></button>
<h3 style="font-family:'Bricolage Grotesque',sans-serif;font-size:20px;font-weight:500;color:#0d0d14;margin:0 0 8px">Historique des actions <span class="tip" data-tip="journal d'audit append-only avec chaînage prévu de toutes les actions effectuées sur ce client.<br><br>Chaque action est horodatée avec son auteur :<br>• Création/modification/archivage du client<br>• Créances créées ou modifiées<br>• Relances générées et envoyées<br>• Mises en demeure générées<br>• Templates créés/modifiés/supprimés<br><br>Conforme aux obligations RGPD et OEC. Les données ne peuvent pas être modifiées rétroactivement.">?</span></h3>
<p style="font-size:12px;color:#94a3b8;margin:0 0 16px;line-height:1.55">Traçabilité complète et immuable — utile pour audits clients et obligations RGPD.</p>
${items}
</div>`;
overlay.addEventListener('click',ev=>{if(ev.target===overlay)overlay.remove();});
document.body.appendChild(overlay);
}
window.showClientHistory=showClientHistory;
// ─── Soft paywall quota mensuel atteint (Freelance / RECOVMAX) ────────────────
function showQuotaCapModal(kind){
if(document.getElementById('quotaCapOverlay')) return;
const plan=Store?._data?.user?.plan||'freelance';
const cap=Store?.quota?.cap?.(kind,plan)||10;
const usage=Store?.quota?.usage?.(kind)||0;
const labels={
creances:{title:'créances',unit:'créances actives',article:'votre quota mensuel de créances'},
relances:{title:'relances',unit:'relances générées',article:'votre quota mensuel de relances'},
clients: {title:'clients', unit:'clients distincts',article:'votre quota mensuel de clients'}
};
const L=labels[kind]||labels.creances;
// Date de reset (1er du mois prochain Europe/Paris)
const now=new Date();
const resetDate=new Date(now.getFullYear(),now.getMonth()+1,1);
const resetStr=resetDate.toLocaleDateString('fr-FR',{day:'numeric',month:'long'});
const isFreelance=plan==='freelance';
const ctaUpgradeHTML=isFreelance
? `<a href="/recovmax.html" class="qcap-cta-primary">Découvrir RECOVMAX (200/mois) →</a>`
: `<a href="mailto:pedro.berbel@dezvolta.org?subject=Demande%20offre%20d%C3%A9di%C3%A9e%20RECOVMAX" class="qcap-cta-primary">Demander une offre dédiée →</a>`;
const overlay=document.createElement('div');
overlay.id='quotaCapOverlay';
overlay.setAttribute('role','dialog');
overlay.setAttribute('aria-modal','true');
overlay.style.cssText='position:fixed;inset:0;background:rgba(13,13,20,.78);backdrop-filter:blur(6px);z-index:9999;display:flex;align-items:center;justify-content:center;padding:20px;animation:demoFadeIn .2s ease-out';
overlay.innerHTML=`
<div style="background:#fff;border-radius:18px;max-width:460px;width:100%;padding:32px clamp(24px,4vw,36px);box-shadow:0 24px 60px rgba(0,0,0,.3);position:relative;animation:demoSlideUp .25s ease-out">
<button onclick="document.getElementById('quotaCapOverlay')?.remove()" aria-label="Fermer" style="position:absolute;top:14px;right:14px;background:transparent;border:none;cursor:pointer;width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;color:#94a3b8" onmouseover="this.style.background='#f1f5f9'" onmouseout="this.style.background='transparent'">
<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" viewBox="0 0 24 24"><path d="M18 6L6 18M6 6l12 12" stroke-linecap="round"/></svg>
</button>
<div style="display:inline-flex;align-items:center;gap:6px;background:#fee2e2;color:#991b1b;font-size:11px;font-weight:500;letter-spacing:.05em;text-transform:uppercase;padding:5px 12px;border-radius:20px;margin-bottom:14px">
<span style="width:5px;height:5px;background:#dc2626;border-radius:50%"></span>
Quota mensuel atteint
</div>
<h3 style="font-family:'Bricolage Grotesque',sans-serif;font-size:22px;font-weight:500;letter-spacing:-.03em;line-height:1.2;color:#0d0d14;margin:0 0 12px">
Vous avez utilisé ${usage}/${cap} ${L.unit} ce mois.
</h3>
<p style="font-size:14px;color:#64748b;line-height:1.65;margin:0 0 18px">
Le compteur reset automatiquement le ${resetStr}. ${L.article.charAt(0).toUpperCase()+L.article.slice(1)} sera remis à zéro à cette date.
</p>
<div style="background:#f8f9fb;border:1px solid #e2e8f0;border-radius:12px;padding:14px 16px;margin-bottom:20px;font-size:13px;color:#0d0d14;line-height:1.55">
Besoin de plus de capacité dès maintenant ?<br>
${isFreelance ? 'RECOVMAX inclut 50 créances actives, 50 clients distincts et 1 000 relances par mois pour 19 €/mois.' : 'Pour un usage au-delà de 50 créances actives, nous proposons une offre dédiée sur mesure.'}
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap">
${ctaUpgradeHTML}
<button onclick="document.getElementById('quotaCapOverlay')?.remove()" style="flex:1;min-width:120px;background:transparent;color:#0d0d14;padding:13px 22px;border:1px solid #e2e8f0;border-radius:11px;font-size:14px;font-weight:500;cursor:pointer;font-family:inherit">Plus tard</button>
</div>
</div>
`;
overlay.addEventListener('click',e=>{ if(e.target===overlay) overlay.remove(); });
const onEsc=e=>{ if(e.key==='Escape'){ overlay.remove(); document.removeEventListener('keydown',onEsc); } };
document.addEventListener('keydown',onEsc);
document.body.appendChild(overlay);
if(window.plausible) plausible('QuotaCapReached',{props:{kind,plan}});
}
window.showQuotaCapModal=showQuotaCapModal;
// CSS pour CTA modal quota cap
(function(){
const s=document.createElement('style');
s.textContent='.qcap-cta-primary{flex:1;min-width:200px;background:#84CC16;color:#0d0d14;padding:13px 22px;border-radius:11px;font-size:14px;font-weight:500;text-decoration:none;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:background .15s;font-family:inherit}.qcap-cta-primary:hover{background:#84CC16}';
document.head.appendChild(s);
})();
// ─── Update quota widget visuel (3 barres) ────────────────────────────────────
function updateQuotaWidget(){
const el=document.getElementById('quotaWidget');
if(!el||!Store?.quota)return;
const plan=Store._data?.user?.plan;
if(!plan||(plan!=='freelance'&&plan!=='recovmax')){el.style.display='none';el.innerHTML='';return;}
const items=plan==='recovmax'
? [{k:'clients',l:'Clients'},{k:'creances',l:'Créances'},{k:'relances',l:'Relances'}]
: [{k:'creances',l:'Créances'},{k:'relances',l:'Relances'}];
const now=new Date();
const resetDate=new Date(now.getFullYear(),now.getMonth()+1,1).toLocaleDateString('fr-FR',{day:'numeric',month:'long'});
const bars=items.map(it=>{
const usage=Store.quota.usage(it.k);
const cap=Store.quota.cap(it.k,plan);
const pct=Store.quota.percent(it.k);
const color=pct>=100?'#dc2626':pct>=80?'#f59e0b':'#84CC16';
const trackBg='#f1f5f9';
return `<div style="flex:1;min-width:140px"><div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px"><span style="font-size:11px;color:#64748b;font-weight:600">${it.l}</span><span style="font-size:11px;color:#0d0d14;font-weight:700;font-variant-numeric:tabular-nums">${usage}/${isFinite(cap)?cap:'∞'}</span></div><div style="height:6px;background:${trackBg};border-radius:3px;overflow:hidden"><div style="height:100%;width:${Math.min(100,pct)}%;background:${color};transition:width .3s,background .3s"></div></div></div>`;
}).join('');
el.style.display='block';
const tipQuota='Votre usage du mois en cours.<br><br>'+(plan==='recovmax'
? '• Clients : entreprises clientes distinctes (cap 10 en V1)<br>• Créances : factures impayées créées (cap 30 en V1)<br>• Relances : emails de relance générés (cap 300 en V1)'
: '• Créances : factures impayées créées (cap 10/mois en Freelance)<br>• Relances : emails de relance générés (cap 100/mois)')
+'<br><br>Reset à la date anniversaire de votre inscription (style forfait mobile).';
// Format ligne fine : items inline avec mini-jauge + label + chiffres, séparateur point
const inlineItems=items.map(it=>{
const usage=Store.quota.usage(it.k);
const cap=Store.quota.cap(it.k,plan);
const pct=Store.quota.percent(it.k);
const color=pct>=100?'#dc2626':pct>=80?'#f59e0b':'#84CC16';
return `<span style="display:inline-flex;align-items:center;gap:6px;font-size:12px;color:#64748b;white-space:nowrap"><span style="display:inline-block;width:28px;height:3px;background:#f1f5f9;border-radius:2px;overflow:hidden;flex-shrink:0"><span style="display:block;width:${Math.min(100,pct)}%;height:100%;background:${color};transition:width .3s,background .3s"></span></span><span style="font-weight:500">${it.l}</span><span style="font-weight:700;color:#0d0d14;font-variant-numeric:tabular-nums">${usage}/${isFinite(cap)?cap:'∞'}</span></span>`;
}).join('<span style="color:#cbd5e1;margin:0 4px">·</span>');
el.innerHTML=`<div style="display:flex;align-items:center;gap:14px;flex-wrap:wrap;font-size:11px;color:#94a3b8"><span style="font-weight:600;letter-spacing:.04em;text-transform:uppercase">Usage du mois <span class="tip" data-tip="${tipQuota.replace(/"/g,'"')}">?</span></span>${inlineItems}<span style="color:#cbd5e1">·</span><span>Reset le ${resetDate}</span></div>`;
}
window.updateQuotaWidget=updateQuotaWidget;
// ─── Modal soft paywall : limite 2 créances en démo ───────────────────────────
function showDemoLimitModal(){
// Si déjà ouvert, no-op
if(document.getElementById('demoLimitOverlay')) return;
const overlay=document.createElement('div');
overlay.id='demoLimitOverlay';
overlay.setAttribute('role','dialog');
overlay.setAttribute('aria-modal','true');
overlay.style.cssText='position:fixed;inset:0;background:rgba(13,13,20,.78);backdrop-filter:blur(6px);z-index:9999;display:flex;align-items:center;justify-content:center;padding:20px;animation:demoFadeIn .2s ease-out';
overlay.innerHTML=`
<div style="background:#fff;border-radius:18px;max-width:480px;width:100%;padding:32px clamp(24px,4vw,36px);box-shadow:0 24px 60px rgba(0,0,0,.3);position:relative;animation:demoSlideUp .25s ease-out">
<button onclick="document.getElementById('demoLimitOverlay')?.remove()" aria-label="Fermer" style="position:absolute;top:14px;right:14px;background:transparent;border:none;cursor:pointer;width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;color:#94a3b8;transition:background .15s" onmouseover="this.style.background='#f1f5f9'" onmouseout="this.style.background='transparent'">
<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" viewBox="0 0 24 24"><path d="M18 6L6 18M6 6l12 12" stroke-linecap="round"/></svg>
</button>
<div style="display:inline-flex;align-items:center;gap:6px;background:#ecfccb;color:#4d7c0f;font-size:11px;font-weight:500;letter-spacing:.05em;text-transform:uppercase;padding:5px 12px;border-radius:20px;margin-bottom:14px">
<span style="width:5px;height:5px;background:#65a30d;border-radius:50%"></span>
Limite démo atteinte
</div>
<h3 style="font-family:'Bricolage Grotesque',sans-serif;font-size:24px;font-weight:500;letter-spacing:-.03em;line-height:1.15;color:#0d0d14;margin:0 0 12px">
Vous avez utilisé vos <span style="background:#84CC16;padding:0 8px;border-radius:4px">2 créances</span> de démonstration.
</h3>
<p style="font-size:15px;color:#64748b;line-height:1.6;margin:0 0 22px">
Pour continuer à structurer vos relances, créez votre compte RECOV Solo — toujours gratuit, pour vos propres factures impayées.
</p>
<div style="background:#f8f9fb;border:1px solid #e2e8f0;border-radius:12px;padding:14px 16px;margin-bottom:22px">
<div style="font-size:12px;font-weight:500;letter-spacing:.04em;text-transform:uppercase;color:#65a30d;margin-bottom:6px">RECOV Solo · Toujours gratuit</div>
<ul style="margin:0;padding:0;list-style:none;font-size:13px;color:#0d0d14;line-height:1.8">
<li>✓ Séquence de relance complète, 8 étapes graduées</li>
<li>✓ Pénalités L.441-10 calculées automatiquement</li>
<li>✓ Mise en demeure PDF prête à signer (1 par mois)</li>
<li>✓ 0 % de commission · usage personnel (1 SIREN, 1 créance active)</li>
</ul>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button onclick="document.getElementById('demoLimitOverlay')?.remove();if(typeof openAuth==='function')openAuth('signup');" style="flex:1;min-width:200px;background:#84CC16;color:#0d0d14;padding:13px 22px;border-radius:11px;font-size:14px;font-weight:500;border:none;cursor:pointer;font-family:inherit;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:background .15s" onmouseover="this.style.background='#84CC16'" onmouseout="this.style.background='#84CC16'">
Créer mon compte (gratuit)
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path d="M5 12h14M13 5l7 7-7 7" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<a href="#pricing" onclick="document.getElementById('demoLimitOverlay')?.remove()" style="flex:1;min-width:140px;background:transparent;color:#0d0d14;padding:13px 22px;border:1px solid #e2e8f0;border-radius:11px;font-size:14px;font-weight:500;cursor:pointer;font-family:inherit;display:inline-flex;align-items:center;justify-content:center;text-decoration:none;transition:border-color .15s" onmouseover="this.style.borderColor='#0d0d14'" onmouseout="this.style.borderColor='#e2e8f0'">
Voir les offres
</a>
</div>
</div>
`;
// Fermeture sur clic overlay (sauf clic dans la card)
overlay.addEventListener('click',e=>{ if(e.target===overlay) overlay.remove(); });
// Fermeture sur Escape
const onEsc=e=>{ if(e.key==='Escape'){ overlay.remove(); document.removeEventListener('keydown',onEsc); } };
document.addEventListener('keydown',onEsc);
document.body.appendChild(overlay);
// Tracking
if(window.plausible) plausible('DemoLimitReached');
}
window.showDemoLimitModal=showDemoLimitModal;
// Animations CSS pour le modal
(function(){
const s=document.createElement('style');
s.textContent='@keyframes demoFadeIn{from{opacity:0}to{opacity:1}}@keyframes demoSlideUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}';
document.head.appendChild(s);
})();
function openAuth(mode='signup', scroll=true){
const outil=document.getElementById('outil');
if(outil){
}
if(scroll && outil) outil.scrollIntoView({behavior:'smooth',block:'start'});
if(mode==='login'){
showLoginForm();
setTimeout(()=>{const el=document.getElementById('loginEmail'); if(el) el.focus();}, scroll?350:0);
return;
}
if(mode==='reset'){
showResetForm();
setTimeout(()=>{const el=document.getElementById('resetEmail');if(el)el.focus();}, scroll?350:0);
return;
}
showSignupForm();
setTimeout(()=>{const el=document.getElementById('gateEmail'); if(el) el.focus();}, scroll?350:0);
}
window.openAuth = openAuth;
window.showLoginForm = showLoginForm;
window.showResetForm = showResetForm;
window.doResetPassword = doResetPassword;
function showNewPasswordForm(){
// Masquer tout le reste
const header=document.querySelector('header');if(header)header.style.display='none';
document.querySelectorAll('main > section, main > div, .landing-only').forEach(el=>{el.style.display='none';});
document.querySelectorAll('body > section, body > div:not(#toast):not(#newPwdOverlay)').forEach(el=>{
if(!el.classList.contains('toast'))el.style.display='none';
});
const footer=document.querySelector('footer');if(footer)footer.style.display='none';
// Créer le formulaire
const overlay=document.createElement('div');
overlay.id='newPwdOverlay';
overlay.style.cssText='position:fixed;inset:0;background:linear-gradient(180deg,#f8fafc 0%,#f5f7fb 100%);z-index:10000;display:flex;align-items:center;justify-content:center;padding:20px';
overlay.innerHTML=`<div style="background:#fff;border-radius:16px;max-width:420px;width:100%;padding:32px;box-shadow:0 20px 60px rgba(0,0,0,.1)">
<form onsubmit="event.preventDefault();submitNewPassword()" autocomplete="on">
<div style="text-align:center;margin-bottom:20px">
<img src="https://recov.pro/logoRecovR.png" alt="RECOV" style="height:32px;margin-bottom:16px">
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:20px;font-weight:500;color:#0d0d14">Nouveau mot de passe</div>
<div style="font-size:13px;color:#64748b;margin-top:6px">Choisissez votre nouveau mot de passe (6 caractères minimum).</div>
</div>
<input type="hidden" name="username" autocomplete="username" value=""/>
<div style="position:relative;margin-bottom:10px">
<input class="gate-input" type="password" id="newPwd1" name="new-password" autocomplete="new-password" placeholder="Nouveau mot de passe" style="width:100%"/>
<button type="button" onclick="togglePwd('newPwd1',this)" style="position:absolute;right:10px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;font-size:16px;color:#6b7a8d;padding:4px">👁</button>
</div>
<div style="position:relative;margin-bottom:10px">
<input class="gate-input" type="password" id="newPwd2" name="confirm-password" autocomplete="new-password" placeholder="Confirmez le mot de passe" style="width:100%"/>
<button type="button" onclick="togglePwd('newPwd2',this)" style="position:absolute;right:10px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;font-size:16px;color:#6b7a8d;padding:4px">👁</button>
</div>
<div id="newPwdMsg" style="display:none;margin-bottom:10px;padding:10px 12px;border-radius:8px;font-size:13px"></div>
<button type="submit" class="gate-btn" style="width:100%">Enregistrer le nouveau mot de passe</button>
</form>
</div>`;
document.body.appendChild(overlay);
setTimeout(()=>{const el=document.getElementById('newPwd1');if(el)el.focus();},100);
}
async function submitNewPassword(){
const pwd1=(document.getElementById('newPwd1')||{}).value||'';
const pwd2=(document.getElementById('newPwd2')||{}).value||'';
const msg=document.getElementById('newPwdMsg');
if(!pwd1||pwd1.length<6){
if(msg){msg.style.display='block';msg.style.background='#fef2f2';msg.style.border='1px solid #fecaca';msg.style.color='#991b1b';msg.textContent='Le mot de passe doit faire au moins 6 caractères.';}
return;
}
if(pwd1!==pwd2){
if(msg){msg.style.display='block';msg.style.background='#fef2f2';msg.style.border='1px solid #fecaca';msg.style.color='#991b1b';msg.textContent='Les deux mots de passe ne correspondent pas.';}
return;
}
const btn=document.querySelector('#newPwdOverlay .gate-btn');
if(btn){btn.disabled=true;btn.textContent='Enregistrement...';}
const{error}=await supa.auth.updateUser({password:pwd1});
if(error){
if(btn){btn.disabled=false;btn.textContent='Enregistrer le nouveau mot de passe';}
let errMsg=error.message;
if(errMsg.includes('same as')||errMsg.includes('different from'))errMsg='Le nouveau mot de passe doit être différent de l\'ancien.';
else if(errMsg.includes('at least'))errMsg='Le mot de passe doit faire au moins 6 caractères.';
else if(errMsg.includes('security purposes'))errMsg='Veuillez patienter quelques secondes avant de réessayer.';
else if(errMsg.includes('session'))errMsg='Session expirée. Demandez un nouveau lien de réinitialisation.';
if(msg){msg.style.display='block';msg.style.background='#fef2f2';msg.style.border='1px solid #fecaca';msg.style.color='#991b1b';msg.textContent=errMsg;}
return;
}
if(msg){msg.style.display='block';msg.style.background='#f0fdf4';msg.style.border='1px solid #bbf7d0';msg.style.color='#166534';msg.textContent='Mot de passe modifié avec succès !';}
if(btn){btn.textContent='Mot de passe modifié ✓';btn.disabled=true;}
setTimeout(()=>{location.href='https://recov.pro/';},2000);
}
window.showNewPasswordForm=showNewPasswordForm;
window.submitNewPassword=submitNewPassword;
window.showSignupForm = showSignupForm;
async function gateSignupStep(){
const email=document.getElementById('gateEmail').value.trim().toLowerCase();
if(!email||!email.includes('@')){showToast('Entrez un email valide');return;}
const step2=document.getElementById('gateStep2');
if(step2.style.display==='none'){
// Etape 1 : montrer le champ mot de passe + checkbox CGU
step2.style.display='block';
const cguWrap=document.getElementById('gateCguWrap');
if(cguWrap)cguWrap.style.display='block';
document.getElementById('gateBtnSignup').textContent='Créer mon compte gratuit →';
document.getElementById('gatePassword').focus();
return;
}
// Etape 2 : vérifier checkbox CGU
const cguCheck=document.getElementById('gateCgu');
if(cguCheck&&!cguCheck.checked){showToast('Veuillez accepter les CGU et la politique de confidentialité');return;}
// Creer le compte
await unlockApp();
}
async function unlockApp(){
const email=document.getElementById('gateEmail').value.trim().toLowerCase();
const password=document.getElementById('gatePassword').value;
const sirenInput=(document.getElementById('gateSiren')?.value||'').replace(/\s/g,'').trim();
const siren=/^\d{9}$/.test(sirenInput)?sirenInput:null;
if(!email||!email.includes('@')){showToast('Entrez un email valide');return;}
if(!password||password.length<6){showToast('Choisissez un mot de passe (6 caractères minimum)');return;}
// SIREN optionnel mais si renseigné, doit être valide (9 chiffres)
if(sirenInput&&sirenInput.length>0&&!siren){showToast('Le SIREN doit comporter exactement 9 chiffres (vous pourrez le compléter plus tard)');return;}
// Deadline passée : on laisse s'inscrire, le pricing s'ajuste post-onboarding
// Honeypot
const honeypot=document.getElementById('gateWebsite');
if(honeypot&&honeypot.value.trim().length>0){showToast('Bienvenue !');return;}
// Supabase Auth signup
const btn=document.querySelector('#gateFormSignup .gate-btn');
if(btn){btn.disabled=true;btn.textContent='Création du compte…';}
const{data,error}=await supa.auth.signUp({email,password});
if(error){
if(btn){btn.disabled=false;btn.textContent='Créer mon compte gratuit →';}
if(error.message.includes('already registered')){
showToast('Cet email est déjà inscrit. Connectez-vous.');showLoginForm();return;
}
console.error('Auth error:',error.message);showToast('Une erreur est survenue. Réessayez.');return;
}
if(!data||!data.user||!data.user.id){
if(btn){btn.disabled=false;btn.textContent='Créer mon compte gratuit →';}
showToast('Compte créé, mais session incomplète. Vérifiez votre email ou reconnectez-vous.');
return;
}
// Register founder (tracking) — avec attribution partenaire si présente
if(location.protocol!=='file:'){
const attribution=getPartnerAttribution();
const payload={
email,
date:new Date().toISOString(),
website:honeypot?honeypot.value:''
};
if(attribution.partner) payload.partner = attribution.partner;
if(attribution.utm_source) payload.utm_source = attribution.utm_source;
if(attribution.utm_campaign) payload.utm_campaign = attribution.utm_campaign;
if(attribution.landing_url) payload.landing_url = attribution.landing_url;
try{fetch('/api/register',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)}).then(r=>r.json()).then(d=>{if(typeof d.remaining==='number')updatePlacesDisplay(d.remaining);}).catch(()=>{});}catch(e){}
}
// Init Store avec cle par compte
Store._key='recovr_'+email.replace(/[^a-z0-9]/gi,'_');
// Reset onboarding pour nouveau compte (même email = compte re-créé)
localStorage.removeItem('recovr_onboarding_'+Store._key);
localStorage.removeItem('recovr_profil_explained_'+Store._key);
Store.init();
Store._userId=data.user?.id||null;
Store.updateUser({email,createdAt:new Date().toISOString()});
// Propager le partner_id sur user_profiles (resolu cote /api/register via slug)
try{
const attribSlug=(getPartnerAttribution&&getPartnerAttribution().partner)||null;
if(attribSlug&&data.user?.id){
const {data:pp}=await supa.from('partners').select('id,status').eq('slug',attribSlug).maybeSingle();
if(pp&&['pilot','active'].includes(pp.status)){
await supa.from('user_profiles').update({partner_id:pp.id}).eq('id',data.user.id);
console.log('[RECOV] user_profiles.partner_id propagé:',pp.id);
}
}
}catch(e){console.warn('[RECOV] partner propagation failed:',e.message);}
// SIREN créancier RECOV Solo (migration 056) — propagé si renseigné
if(siren&&data.user?.id){
try{
const{error:sirenErr}=await supa.from('user_profiles').update({siren_creancier:siren}).eq('id',data.user.id);
if(sirenErr){
// Erreur typique : SIREN déjà utilisé par un autre compte Solo (UNIQUE index migration 056)
if(sirenErr.code==='23505'||/duplicate|unique/i.test(sirenErr.message||'')){
showToast('Ce SIREN est déjà associé à un compte Solo existant. Connectez-vous, ou contactez le support.');
}else{
console.warn('[RECOV] SIREN update failed:',sirenErr);
}
}else{
console.log('[RECOV] siren_creancier propagé:',siren);
if(Store._data?.user)Store._data.user.sirenCreancier=siren;
}
}catch(e){console.warn('[RECOV] siren propagation exception:',e.message);}
}
await Store.syncFromSupabase();
showApp();
showToast('Compte créé ! Bienvenue dans RECOV.');
if(window.plausible) plausible('Signup', {props: {source: document.referrer || 'direct'}});
}
async function loginApp(){
const email=document.getElementById('loginEmail').value.trim().toLowerCase();
const password=document.getElementById('loginPassword').value;
hideLoginError();
if(!email||!email.includes('@')){showLoginError('Entrez une adresse email valide.');return;}
if(!password){showLoginError('Entrez votre mot de passe.');return;}
const btn=document.querySelector('#gateFormLogin .gate-btn');
if(btn){btn.disabled=true;btn.textContent='Connexion…';}
const{data,error}=await supa.auth.signInWithPassword({email,password});
if(error){
if(btn){btn.disabled=false;btn.textContent='Se connecter →';}
if(error.message.includes('Invalid login')){
showLoginError('Email ou mot de passe incorrect. Vérifiez vos identifiants ou réinitialisez votre mot de passe.');
return;
}
if(error.message.includes('Email not confirmed')){
showLoginError('Votre email n\'a pas encore été confirmé. Vérifiez votre boîte mail (et les spams).');
return;
}
let errMsg=error.message;
if(errMsg.includes('security purposes'))errMsg='Pour des raisons de sécurité, veuillez patienter quelques secondes avant de réessayer.';
else if(errMsg.includes('rate limit'))errMsg='Trop de tentatives. Réessayez dans une minute.';
showLoginError(errMsg);
return;
}
if(!data||!data.user||!data.user.id){
if(btn){btn.disabled=false;btn.textContent='Se connecter →';}
showLoginError('Connexion incomplète. Réessayez dans quelques secondes.');
return;
}
Store._key='recovr_'+email.replace(/[^a-z0-9]/gi,'_');
Store.init();
Store.updateUser({email}); // _userId encore null → pas d'écrasement Supabase
Store._userId=data.user?.id||null;
await Store.syncFromSupabase();
showApp();
showToast('Bienvenue !');
if(btn){btn.disabled=false;btn.textContent='Se connecter →';}
}
// ── Badge forfait dans le dashboard (logo + libellé selon plan) ───────────────
function updatePlanBadge(){
const badge=document.getElementById('userPlanBadge');
if(!badge) return;
const plan=(Store?._data?.user?.plan)||'fondateur';
const map={
'demo': {label:'Démo', bg:'#f1f5f9', color:'#64748b', logo:null},
'fondateur': {label:'Fondateur', bg:'#ecfccb', color:'#4d7c0f', logo:'logo.png'},
'founder': {label:'Fondateur', bg:'#ecfccb', color:'#4d7c0f', logo:'logo.png'},
'direct': {label:'Fondateur', bg:'#ecfccb', color:'#4d7c0f', logo:'logo.png'},
'partner': {label:'Fondateur · Partenaire',bg:'#ecfccb', color:'#4d7c0f', logo:'logo.png'},
'freelance': {label:'RECOV Solo', bg:'#dbeafe', color:'#1d4ed8', logo:'logo.png'},
'recovmax': {label:'RECOVMAX', bg:'#0d0d14', color:'#84CC16', logo:'recovmaxlogo.png'}
};
const cfg=map[plan]||map['fondateur'];
badge.innerHTML=(cfg.logo?'<img src="'+cfg.logo+'" alt="" style="height:14px;width:auto">':'')+'<span>'+cfg.label+'</span>';
badge.style.background=cfg.bg;
badge.style.color=cfg.color;
badge.style.display='inline-flex';
}
window.updatePlanBadge=updatePlanBadge;
function showApp(){
sessionStorage.removeItem('recov_logged_out');
// 1. Sortir #app de la carte outil AVANT de masquer quoi que ce soit
const appEl=document.getElementById('app');
if(appEl.closest('.outil-card-body')||appEl.closest('#outil')){
document.body.appendChild(appEl);
}
// 2. Masquer toute la landing page
const header=document.querySelector('header');
if(header)header.style.display='none';
document.querySelectorAll('main > section, main > div, .landing-only').forEach(el=>{el.style.display='none';});
document.querySelectorAll('body > section, body > div:not(#toast):not(#app)').forEach(el=>{
if(!el.classList.contains('toast'))el.style.display='none';
});
const footer=document.querySelector('footer');
if(footer)footer.style.display='none';
// 3. Rendre l'app visible
appEl.style.display='block';
document.body.style.background='linear-gradient(180deg,#f8fafc 0%,#f5f7fb 100%)';
document.body.classList.add('app-mode');
document.body.style.paddingTop='0';
document.body.style.overflow='hidden';
document.body.style.overflowY='auto';
window.scrollTo(0,0);
// 4. Router vers la bonne vue
const hasCreances=safeArray(Store.getCreances()).length>0;
const onboardingDone=localStorage.getItem('recovr_onboarding_'+Store._key);
if(!onboardingDone){
// Première connexion : onboarding d'abord, puis profil après fermeture
if(!isProfilComplet()){
UI.showView('profil');
} else {
UI.showView('dashboard');
try{const _rp=(Store._data?.user?.nom||'').split(' ')[0];Recovo.init(_rp);}catch(e){}
}
setTimeout(()=>{
showOnboarding();
// Après fermeture de l'onboarding, montrer l'explication profil si nécessaire
const checkProfilAfter=setInterval(()=>{
if(!document.getElementById('onboarding-overlay')){
clearInterval(checkProfilAfter);
if(!isProfilComplet()&&!localStorage.getItem('recovr_profil_explained_'+Store._key)){
setTimeout(()=>showProfilExplanation(),300);
}
}
},200);
},500);
} else if(!isProfilComplet()){
UI.showView('profil');
if(!localStorage.getItem('recovr_profil_explained_'+Store._key)){
setTimeout(()=>showProfilExplanation(),300);
}
} else {
UI.showView('dashboard');
try{const _rp=(Store._data?.user?.nom||'').split(' ')[0];Recovo.init(_rp);}catch(e){}
}
// 5. Badge forfait visible (lit user.plan ou fallback fondateur)
setTimeout(()=>{ try{updatePlanBadge();}catch(e){} },200);
}
// ── Coordination éléments fixes en bas (RGPD + CTA sticky + chat btn) ──
function adjustBottomStack(){
const rgpd = document.getElementById('rgpdBanner');
const sticky = document.getElementById('stickyCta');
const btn = document.getElementById('recovo-btn');
let base = 0;
// 1. RGPD reste à bottom:0 — on mesure sa hauteur si visible
if(rgpd && rgpd.style.display !== 'none') base = rgpd.offsetHeight;
// 2. CTA sticky au-dessus du RGPD
if(sticky){
sticky.style.bottom = base + 'px';
if(sticky.style.display !== 'none' && parseFloat(sticky.style.opacity||0) > 0)
base += sticky.offsetHeight;
}
// 3. Bouton chat au-dessus de tout (+ 12px de marge)
if(btn) btn.style.bottom = (base + 12) + 'px';
}
window.addEventListener('resize', adjustBottomStack, {passive:true});
// Bandeau RGPD
function initRgpdBanner(){
if(!localStorage.getItem('recovr_rgpd_accepted')){
const b=document.getElementById('rgpdBanner');
if(b){ b.style.display='block'; adjustBottomStack(); }
}
}
function acceptRgpd(){
localStorage.setItem('recovr_rgpd_accepted','1');
const b=document.getElementById('rgpdBanner');
if(b){ b.style.display='none'; adjustBottomStack(); }
}
// Compteur temps réel style horloge de gare
(function(){
const el=document.getElementById('liveCounter');
if(!el)return;
const annualTotal=56000000000; // Source : COFACE / Banque de France
const perSecond=annualTotal/(365.25*24*3600);
const jan1=new Date('2026-01-01T00:00:00').getTime();
const isMobile=window.innerWidth<600;
const pad=isMobile?'3px 5px':'6px 8px';
const rad=isMobile?'5px':'8px';
const spc=isMobile?'6px':'12px';
const digitStyle='background:#0d0d14;color:#fff;padding:'+pad+';border-radius:'+rad+';display:inline-block;min-width:0.6em;text-align:center;box-shadow:0 3px 10px rgba(0,0,0,.4);border:1px solid rgba(255,255,255,.1)';
const euroStyle='color:#0d0d14;font-weight:800;display:inline-block;margin-left:8px;flex-shrink:0';
const spaceStyle='display:inline-block;width:'+spc;
function render(text){
let html='';
const chars=text.replace(' €','€');
for(let i=0;i<chars.length;i++){
const c=chars[i];
if(c>='0'&&c<='9'){html+='<span style="'+digitStyle+'">'+c+'</span>';}
else if(c==='€'){html+='<span style="'+euroStyle+'">€</span>';}
else if(c===' '){html+='<span style="'+spaceStyle+'"></span>';}
else{html+=c;}
}
el.innerHTML=html;
}
function update(){
const elapsed=(Date.now()-jan1)/1000;
const total=Math.floor(elapsed*perSecond);
render(total.toLocaleString('fr-FR')+' €');
requestAnimationFrame(update);
}
update();
})();
function openLightbox(src,alt){
const o=document.createElement('div');
o.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.85);z-index:10000;display:flex;align-items:center;justify-content:center;padding:20px;cursor:pointer';
o.onclick=()=>o.remove();
o.innerHTML=`<img src="${src}" alt="${alt}" style="max-width:90vw;max-height:90vh;border-radius:12px;box-shadow:0 20px 60px rgba(0,0,0,.5)">
<div style="position:absolute;top:20px;right:24px;color:#fff;font-size:32px;font-weight:300;cursor:pointer;line-height:1">×</div>`;
document.body.appendChild(o);
document.addEventListener('keydown',function esc(e){if(e.key==='Escape'){o.remove();document.removeEventListener('keydown',esc);}});
}
function showOnboarding(){
localStorage.setItem('recovr_onboarding_'+Store._key,'1');
if(document.getElementById('onboarding-overlay'))return;
const profilOk=isProfilComplet();
const overlay=document.createElement('div');
overlay.id='onboarding-overlay';
overlay.style.cssText='position:fixed;inset:0;background:rgba(15,23,42,0.7);backdrop-filter:blur(4px);z-index:10000;display:flex;align-items:center;justify-content:center;padding:16px;overflow-y:auto';
overlay.innerHTML=`<div style="background:#fff;border-radius:24px;max-width:460px;width:100%;box-shadow:0 32px 80px rgba(0,0,0,.22);overflow:hidden;font-family:'DM Sans',system-ui,sans-serif">
<!-- Bande accent top -->
<div style="height:5px;background:linear-gradient(90deg,#65a30d,#84cc16,#84CC16)"></div>
<!-- Header propre -->
<div style="padding:32px 32px 24px;text-align:center;border-bottom:1px solid #f1f5f9">
<div style="display:inline-flex;align-items:center;gap:8px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:12px;padding:6px 14px;margin-bottom:20px">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" fill="#84cc16"/><path d="M8 12l3 3 5-5" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span style="font-size:12px;font-weight:700;color:#15803d;letter-spacing:.03em">ACCÈS ACTIVÉ</span>
</div>
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:26px;font-weight:500;color:#0d0d14;letter-spacing:-.04em;line-height:1.1;margin-bottom:8px">Bienvenue dans RECOV</div>
<div style="font-size:14px;color:#64748b;line-height:1.6;max-width:320px;margin:0 auto">Vos relances sont planifiées, rédigées, prêtes à envoyer.<br>Voici comment démarrer.</div>
</div>
<!-- Étapes -->
<div style="padding:8px 32px 0">
<div style="display:flex;gap:16px;align-items:flex-start;padding:20px 0;border-bottom:1px solid #f1f5f9;position:relative">
<div style="width:36px;height:36px;border-radius:10px;background:#0d0d14;color:#84cc16;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:800;flex-shrink:0;letter-spacing:0">01</div>
<div style="flex:1">
<div style="font-size:14px;font-weight:700;color:#0d0d14;margin-bottom:3px">Remplissez votre profil</div>
<div style="font-size:13px;color:#64748b;line-height:1.5">Nom et SIRET — signature pro sur chaque relance. 30 secondes.</div>
</div>
${profilOk?'<div style="flex-shrink:0;width:22px;height:22px;border-radius:50%;background:#dcfce7;display:flex;align-items:center;justify-content:center"><svg width="12" height="12" viewBox="0 0 24 24" fill="none"><path d="M5 12l5 5 9-9" stroke="#16a34a" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/></svg></div>':''}
</div>
<div style="display:flex;gap:16px;align-items:flex-start;padding:20px 0;border-bottom:1px solid #f1f5f9">
<div style="width:36px;height:36px;border-radius:10px;background:#0d0d14;color:#84cc16;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:800;flex-shrink:0">02</div>
<div>
<div style="font-size:14px;font-weight:700;color:#0d0d14;margin-bottom:3px">Ajoutez une facture impayée</div>
<div style="font-size:13px;color:#64748b;line-height:1.5">Client, montant, échéance. RECOV planifie toute la séquence.</div>
</div>
</div>
<div style="display:flex;gap:16px;align-items:flex-start;padding:20px 0">
<div style="width:36px;height:36px;border-radius:10px;background:#0d0d14;color:#84cc16;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:800;flex-shrink:0">03</div>
<div>
<div style="font-size:14px;font-weight:700;color:#0d0d14;margin-bottom:3px">Validez et envoyez</div>
<div style="font-size:13px;color:#64748b;line-height:1.5">Textes rédigés avec textes de loi et pénalités. Vous validez, vous envoyez.</div>
</div>
</div>
</div>
<!-- CTA -->
<div style="padding:20px 32px 28px">
<button onclick="document.getElementById('onboarding-overlay').remove();${profilOk?'UI.showView(\'nouvelle\')':'UI.showView(\'profil\')'}" style="width:100%;padding:15px;background:#0d0d14;color:#fff;border:none;border-radius:12px;font-family:'DM Sans',sans-serif;font-size:15px;font-weight:500;cursor:pointer;letter-spacing:-.01em;transition:opacity .15s" onmouseover="this.style.opacity='.88'" onmouseout="this.style.opacity='1'">${profilOk?'Ajouter ma première facture →':'Commencer — remplir mon profil →'}</button>
<div style="text-align:center;margin-top:12px">
<button onclick="document.getElementById('onboarding-overlay').remove()" style="font-size:12px;color:#94a3b8;background:none;border:none;cursor:pointer;font-family:inherit;padding:4px">Je connais déjà, fermer</button>
</div>
</div>
</div>`;
document.body.appendChild(overlay);
}
function showProfilExplanation(){
const overlay=document.createElement('div');
overlay.id='profil-explain-overlay';
overlay.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:10000;display:flex;align-items:center;justify-content:center;padding:12px;overflow-y:auto';
overlay.innerHTML=`<div style="background:#fff;border-radius:16px;max-width:440px;width:100%;padding:clamp(20px,5vw,32px);box-shadow:0 20px 60px rgba(0,0,0,.3)">
<div style="margin-bottom:12px;text-align:center"><img src="iconRecov.png" alt="RECOV" style="width:36px;height:36px"></div>
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:clamp(16px,4vw,20px);font-weight:500;color:var(--noir);margin-bottom:10px;text-align:center">Complétez votre profil pour commencer</div>
<div style="font-size:16px;color:var(--gris);line-height:1.7;font-weight:400;margin-bottom:20px;text-align:center">
Deux informations suffisent : votre nom et votre SIRET.<br><br>
Elles apparaîtront dans la signature de vos relances et sur la mise en demeure PDF. C'est ce qui donne un cadre professionnel à vos courriers.
</div>
<div style="background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:12px 14px;margin-bottom:20px;font-size:13px;color:var(--gris);line-height:1.6">
Pourquoi le SIRET ? Un courrier de relance signé avec un numéro SIRET est pris au sérieux. Sans identification professionnelle, le client peut contester la légitimité de la démarche.
</div>
<button onclick="document.getElementById('profil-explain-overlay').remove();localStorage.setItem('recovr_profil_explained_'+Store._key,'1');document.getElementById('pNom')?.focus()" style="width:100%;padding:13px;background:var(--vert);color:#fff;border:none;border-radius:10px;font-family:'DM Sans',sans-serif;font-size:15px;font-weight:500;cursor:pointer">Compris, je remplis mon profil</button>
<div style="text-align:center;margin-top:8px;font-size:12px;color:var(--gris2)">30 secondes — les autres champs sont optionnels</div>
</div>`;
document.body.appendChild(overlay);
}
async function resetPassword(){
// Chercher l'email dans le champ login OU dans le champ signup
let email=(document.getElementById('loginEmail')||{}).value||'';
if(!email)email=(document.getElementById('gateEmail')||{}).value||'';
email=email.trim().toLowerCase();
if(!email||!email.includes('@')){
showLoginError('Entrez votre adresse email dans le champ ci-dessus, puis cliquez "Réinitialiser".');
const el=document.getElementById('loginEmail');
if(el)el.focus();
return;
}
const{error}=await supa.auth.resetPasswordForEmail(email,{redirectTo:'https://recov.pro'});
if(error){showToast('Erreur : '+error.message);return;}
showToast('Email de réinitialisation envoyé ! Vérifiez votre boîte mail.');
}
async function logout(){
if(confirm('Vous déconnecter ? Vos données sont sauvegardées sur le serveur.')){
try{const rBtn=document.getElementById('recovo-btn');if(rBtn)rBtn.style.display='none';Recovo.close();}catch(e){}
document.body.style.background='';
await supa.auth.signOut({scope:'global'});
Store._userId=null;
// Nettoyer toutes les sessions Supabase du localStorage
Object.keys(localStorage).forEach(function(k){
if(k.startsWith('sb-'))localStorage.removeItem(k);
});
sessionStorage.setItem('recov_logged_out','1');
location.reload();
}
}
// ─── RECOVMAX Step 0 : sélection / création client avant la facture ───
let _wizClientId=null; // UUID du client_company sélectionné/créé
let _wizIsRecovmax=false; // mode RECOVMAX activé
async function wizSetupStep0(){
const el=id=>document.getElementById(id);
const plan=Store._data?.user?.plan||'fondateur';
_wizIsRecovmax=(plan==='recovmax');
_wizClientId=null;
if(!_wizIsRecovmax){
// Plans freelance/fondateur : flow inchangé (3 étapes)
return;
}
// RECOVMAX : pas de bouton démo (vrai client déjà sélectionné)
const demo=document.getElementById('btnDemoFictive');
if(demo)demo.style.display='none';
// RECOVMAX : indicateur 4 dots, label step 1 = "Client" → renumérotation visuelle
const w0=el('stepWrap0'),b0=el('stepBarWrap0');
if(w0)w0.style.display='flex';
if(b0)b0.style.display='block';
// Re-numéroter les dots affichés (1=Client, 2=Facture, 3=Ton, 4=Valider)
if(el('stepDot0'))el('stepDot0').textContent='1';
if(el('stepDot1'))el('stepDot1').textContent='2';
if(el('stepDot2'))el('stepDot2').textContent='3';
if(el('stepDot3'))el('stepDot3').textContent='4';
// Step 1 commence inactive
if(el('stepDot1')){el('stepDot1').style.background='#f2f2f7';el('stepDot1').style.color='#94a3b8';}
if(el('stepLbl1'))el('stepLbl1').style.color='#94a3b8';
if(el('wizSubTitle'))el('wizSubTitle').textContent='4 étapes — sélectionnez le client puis saisissez la facture.';
// Affichage Step 0, masquer Step 1
el('wizStep0').style.display='block';
el('wizStep1').style.display='none';
// Charger les clients existants
const clients=await Store.clients.list();
const existing=el('wizStep0Existing'),neu=el('wizStep0New');
if(clients.length===0){
// Aucun client : on impose direct le formulaire de création
existing.style.display='none';
neu.style.display='block';
const cancelBtn=el('wizCancelNewClientBtn');if(cancelBtn)cancelBtn.style.display='none';
if(el('wizStep0Sub'))el('wizStep0Sub').textContent='Vous n\'avez encore aucun client. Créez votre premier client pour démarrer.';
}else{
existing.style.display='block';
neu.style.display='none';
const sel=el('wizClientSelect');
sel.innerHTML='<option value="">— Sélectionnez un client —</option>'+
clients.map(cl=>`<option value="${esc(cl.id)}">${esc(cl.nom)}${cl.siret?' · '+esc(cl.siret):''}</option>`).join('');
sel.onchange=()=>{ _wizClientId=sel.value||null; };
}
}
window.wizSetupStep0=wizSetupStep0;
function wizShowNewClient(){
document.getElementById('wizStep0Existing').style.display='none';
document.getElementById('wizStep0New').style.display='block';
const cancelBtn=document.getElementById('wizCancelNewClientBtn');if(cancelBtn)cancelBtn.style.display='block';
}
window.wizShowNewClient=wizShowNewClient;
function wizCancelNewClient(){
document.getElementById('wizStep0Existing').style.display='block';
document.getElementById('wizStep0New').style.display='none';
}
window.wizCancelNewClient=wizCancelNewClient;
async function wizCreateNewClient(){
const el=id=>document.getElementById(id);
const nom=el('wizNewClientNom').value.trim();
const email=el('wizNewClientEmail').value.trim();
const siret=el('wizNewClientSiret').value.trim().replace(/\s/g,'');
if(!nom){showToast('Nom du client obligatoire');el('wizNewClientNom').focus();return;}
if(email&&!email.includes('@')){showToast('Email invalide');el('wizNewClientEmail').focus();return;}
try{
const created=await Store.clients.create({nom,email_contact:email||null,siret:siret||null});
_wizClientId=created.id;
showToast('Client « '+created.nom+' » créé');
// Pré-remplir Step 1 avec les infos du client
if(el('fClient')&&!el('fClient').value)el('fClient').value=created.nom;
if(el('fClientEmail')&&!el('fClientEmail').value&&created.email_contact)el('fClientEmail').value=created.email_contact;
if(el('fClientSiren')&&!el('fClientSiren').value&&created.siret)el('fClientSiren').value=created.siret;
// Avancer vers Step 1
wizNext(1);
}catch(e){
showToast(e.message||'Erreur création client');
}
}
window.wizCreateNewClient=wizCreateNewClient;
function wizNext(step){
const el=id=>document.getElementById(id);
// RECOVMAX : validation Step 0 → 1 (client obligatoire)
if(_wizIsRecovmax&&step===1){
// Si le formulaire "nouveau client" est ouvert, l'user doit cliquer "Créer et continuer"
const newOpen=el('wizStep0New')?.style.display==='block';
const existingOpen=el('wizStep0Existing')?.style.display==='block';
if(newOpen&&!_wizClientId){
showToast('Cliquez sur « Créer et continuer » pour valider le nouveau client');return;
}
if(existingOpen){
// L'user doit avoir choisi un client dans le select
const sel=el('wizClientSelect');
_wizClientId=sel?sel.value:null;
if(!_wizClientId){showToast('Sélectionnez un client ou créez-en un nouveau');return;}
// Auto-fill Step 1 depuis le client choisi
try{
Store.clients.getById(_wizClientId).then(cl=>{
if(!cl)return;
if(el('fClient')&&!el('fClient').value)el('fClient').value=cl.nom||'';
if(el('fClientEmail')&&!el('fClientEmail').value&&cl.email_contact)el('fClientEmail').value=cl.email_contact;
if(el('fClientSiren')&&!el('fClientSiren').value&&cl.siret)el('fClientSiren').value=cl.siret;
});
}catch(e){}
}
// Stocker l'id dans le hidden input pour creerCreance
if(el('fClientCompany'))el('fClientCompany').value=_wizClientId||'';
}
// Validation etape 1 → 2 (facture)
if(step===2){
const client=el('fClient')?.value.trim();
const montant=el('fMontant')?.value.trim();
const facture=el('fFacture')?.value.trim();
const dateEch=el('fEcheance')?.value;
if(!client||!montant||!facture||!dateEch){showToast('Remplissez tous les champs obligatoires');return;}
}
// Validation etape 2 → 3 (ton)
if(step===3){
const profilBtn=document.querySelector('.profil-btn.active');
if(!profilBtn){showToast('Sélectionnez un profil client');return;}
}
// Afficher l'etape (et masquer Step 0 dès qu'on dépasse)
[0,1,2,3].forEach(s=>{
const stepEl=el('wizStep'+s);
if(stepEl)stepEl.style.display=s===step?'block':'none';
if(s>=1){
const dot=el('stepDot'+s);
if(dot){dot.style.background=s<=step?'#84cc16':'#f2f2f7';dot.style.color=s<=step?'#0f172a':'#94a3b8';}
const lbl=el('stepLbl'+s);
if(lbl)lbl.style.color=s<=step?'#84cc16':'#94a3b8';
if(s<3){const bar=el('stepBar'+s);if(bar)bar.style.width=s<step?'100%':'0%';}
}
});
// En RECOVMAX, le step 0 dot reste vert dès qu'on a passé le client
if(_wizIsRecovmax&&step>=1){
const d0=el('stepDot0');if(d0){d0.style.background='#84cc16';d0.style.color='#0f172a';}
const b0=el('stepBar0');if(b0)b0.style.width='100%';
}
// Resume etape 3
if(step===3){
const profilBtn=document.querySelector('.profil-btn.active');
const profilLabel=profilBtn?profilBtn.textContent.split('\n')[0].trim():'—';
const summary=el('wizSummary');
if(summary)summary.innerHTML='Récapitulatif :<br>'+
'🏢 '+(el('fClient')?.value||'—')+' · ✉ '+(el('fClientEmail')?.value||'non renseigné')+'<br>'+
'💰 '+(el('fMontant')?.value||'—')+' € · 📄 '+(el('fFacture')?.value||'—')+'<br>'+
'📅 Échéance : '+(el('fEcheance')?.value||'—')+' · 👤 Profil : '+profilLabel;
}
}
// RECOVMAX : peuple le sélecteur Client de la vue Nouvelle (appelé par renderNouvelle via setTimeout)
async function populateClientCompanySelect(){
const wrap=document.getElementById('fClientCompanyWrap');
const sel=document.getElementById('fClientCompany');
if(!wrap||!sel)return;
const _plan=Store._data?.user?.plan||'fondateur';
if(_plan!=='recovmax'){wrap.style.display='none';return;}
wrap.style.display='block';
const clients=await Store.clients.list();
if(clients.length===0){
sel.innerHTML='<option value="">— Aucun client. Cliquez Ajouter ci-dessous —</option>';
return;
}
sel.innerHTML='<option value="">— Sélectionnez un client —</option>'+clients.map(cl=>`<option value="${escHtml(cl.id)}">${escHtml(cl.nom)}${cl.siret?' · '+escHtml(cl.siret):''}</option>`).join('');
}
window.populateClientCompanySelect=populateClientCompanySelect;
// Auto-fill : quand un client est sélectionné, pré-remplir certains champs
async function onClientCompanyChange(){
const sel=document.getElementById('fClientCompany');
if(!sel||!sel.value)return;
const cl=await Store.clients.getById(sel.value);
if(!cl)return;
// Pré-remplir nom client (champ texte) si vide
const fClient=document.getElementById('fClient');
if(fClient&&!fClient.value)fClient.value=cl.nom||'';
// Pré-remplir email
const fEmail=document.getElementById('fClientEmail');
if(fEmail&&!fEmail.value&&cl.email_contact)fEmail.value=cl.email_contact;
// Pré-remplir SIREN
const fSiren=document.getElementById('fClientSiren');
if(fSiren&&!fSiren.value&&cl.siret)fSiren.value=cl.siret;
}
window.onClientCompanyChange=onClientCompanyChange;
async function creerCreance(){
if(!isProfilComplet()){showToast('Complétez d\'abord votre profil (nom et SIRET) pour créer un dossier.');UI.showView('profil');return;}
const client=document.getElementById('fClient').value.trim();
const clientEmail=document.getElementById('fClientEmail').value.trim();
const clientSiren=(document.getElementById('fClientSiren')?.value||'').trim();
const montant=document.getElementById('fMontant').value.trim();
const facture=document.getElementById('fFacture').value.trim();
const dateEcheance=document.getElementById('fEcheance').value;
const profilBtn=document.querySelector('.profil-btn.active');
if(!profilBtn){showToast('Sélectionnez le profil de la relation client (obligatoire)');return;}
const profil=profilBtn.dataset.profil;
const contexte=document.getElementById('fContexte').value.trim();
const scoringRadio=document.querySelector('input[name="clientScoring"]:checked');
const clientScoring=scoringRadio?scoringRadio.value:'first';
if(!client||!montant||!facture||!dateEcheance){showToast('Remplissez les champs obligatoires');return;}
// Email client obligatoire
if(!clientEmail||!clientEmail.includes('@')){showToast('L\'email du client est obligatoire pour l\'envoi des relances');document.getElementById('fClientEmail').focus();return;}
// Validation date
if(dateEcheance){
const d=new Date(dateEcheance);
if(isNaN(d.getTime())){showToast('Date d\'échéance invalide');document.getElementById('fEcheance').focus();return;}
if(d>new Date()){showToast('La date d\'échéance doit être dans le passé (facture déjà échue)');document.getElementById('fEcheance').focus();return;}
}
// Régime juridique du débiteur (détermine le calcul de pénalités correct)
const regime=document.getElementById('fRegime')?.value||'b2b';
const isB2C=regime==='b2c';
// Validation SIREN/SIRET client (optionnel — vérifie le format si fourni, pas pertinent pour B2C)
let clientSirenClean='';
if(!isB2C&&clientSiren){
const vs=validerSiren(clientSiren);
if(!vs.ok){showToast(vs.msg);document.getElementById('fClientSiren').focus();return;}
clientSirenClean=clientSiren.replace(/\s/g,'');
}
// RECOVMAX : récupérer le client_company_id sélectionné
const _plan=Store._data?.user?.plan||'fondateur';
const client_company_id=document.getElementById('fClientCompany')?.value||null;
if(_plan==='recovmax'&&!client_company_id){showToast('⚠️ Sélectionnez un client RECOVMAX pour créer la créance');document.getElementById('fClientCompany')?.focus();return;}
// Snapshot du taux applicable au moment de la création (audit historique)
const _calc=Penalites.calculer(montant,dateEcheance,new Date().toISOString().split('T')[0],{regime});
const tauxApplique=_calc.tauxApplicable;
const c=await Store.addCreance({client,clientEmail,clientSiren:clientSirenClean,montant,facture,dateEcheance,profil,contexte,clientScoring,client_company_id,regime_debiteur:regime,taux_applique:tauxApplique});
if(!c) return; // limite démo/quota atteinte (modal affiché) ou erreur silencieuse
if(window.plausible) plausible('CreanceCreated');
showToast('Créance créée');
UI._justCreated=true;
UI.showView('detail',c.id);
}
function onRegimeChange(){
const sel=document.getElementById('fRegime');
const help=document.getElementById('fRegimeHelp');
const sirenInput=document.getElementById('fClientSiren');
if(!sel)return;
const regime=sel.value;
const messages={
b2b: 'Taux applicable : BCE refi + 10 pts · Indemnité forfaitaire 40 € due (D441-5)',
liberal:'Taux applicable : taux légal « autres cas » · Pas d\'indemnité 40 € par défaut (régime civil — art. 1231-6 C. civ.)',
admin: 'Taux applicable : BCE refi + 8 pts · Indemnité forfaitaire 40 € due (Décret 2013-269)',
b2c: 'Taux applicable : taux légal « particuliers » · Pas d\'indemnité 40 € (D441-5 réservé B2B)'
};
if(help)help.innerHTML=messages[regime]||messages.b2b;
if(sirenInput){
if(regime==='b2c'){
sirenInput.disabled=true;sirenInput.style.opacity='.4';sirenInput.value='';
const status=document.getElementById('fClientSirenStatus');if(status)status.style.display='none';
} else {
sirenInput.disabled=false;sirenInput.style.opacity='1';
}
}
}
window.onRegimeChange=onRegimeChange;
// Compatibilité historique : ancienne checkbox particulier (maintenue inactive)
function toggleParticulier(){
const checked=document.getElementById('fClientParticulier')?.checked;
const wrap=document.getElementById('fClientSiren');
const status=document.getElementById('fClientSirenStatus');
if(wrap){wrap.disabled=checked;wrap.style.opacity=checked?'.4':'1';}
if(status)status.style.display='none';
}
async function checkClientSiren(){
const input=document.getElementById('fClientSiren');
const status=document.getElementById('fClientSirenStatus');
if(!input||!status)return;
const val=input.value.replace(/\s/g,'');
if(!val){status.style.display='none';return;}
const v=validerSiren(val);
if(!v.ok){
status.style.display='block';status.style.background='#fef2f2';status.style.color='#991b1b';
status.textContent='⚠ '+v.msg;return;
}
// Vérification API
status.style.display='block';status.style.background='var(--bg2)';status.style.color='var(--gris)';
status.textContent='Vérification en cours…';
const r=await verifierSirene(v.siren);
if(r.actif===false){
status.style.background='#fef2f2';status.style.color='#991b1b';
status.textContent='⚠ '+r.msg;
}else if(r.nom){
status.style.background='#f0fdf4';status.style.color='#166534';
status.textContent='✓ '+r.msg;
}else{
status.style.background='var(--bg2)';status.style.color='var(--gris)';
status.textContent='ℹ '+r.msg;
}
}
async function genererEtape(creanceId,etapeNum){
const c=Store.getCreance(creanceId);if(!c)return;
// Disclaimer obligatoire — consentement utilisateur
if(!Store.getAll().legalConsent){
const ok=confirm('IMPORTANT — En utilisant RECOV, vous reconnaissez que :\n\n'+
'• Vous êtes l\'auteur et le seul responsable de chaque email avant envoi\n'+
'• RECOV est un copilote de recouvrement amiable, pas un service juridique\n'+
'• Les textes générés doivent être relus et adaptés avant envoi\n'+
'• Vos données de créance sont transmises à Anthropic (USA) pour la génération\n\n'+
'Cliquez OK pour accepter et continuer.');
if(!ok)return;
Store._data.legalConsent=true;Store._data.legalConsentDate=new Date().toISOString();Store.save();
}
// Vérifier expiration fondateur
if(Store.isFounderExpired()){showToast('Votre accès fondateur a expiré. Souscrivez à l\'abonnement pour continuer.');return;}
// Vérifier quota IA (limite anti-abus quotidien)
const quota=Store.canUseAI();
if(!quota.ok){showToast(quota.reason);return;}
// ─── RECOVMAX/Freelance : check quota mensuel relances ──────────────────
const _plan=Store._data?.user?.plan||'fondateur';
if(_plan==='freelance'||_plan==='recovmax'){
if(Store.quota.isAtCap('relances')){
if(typeof showQuotaCapModal==='function')showQuotaCapModal('relances');
else showToast('⚠️ Quota mensuel relances atteint');
return;
}
}
const btn=event.target;
btn.disabled=true;btn.textContent='Génération…';
try{
const headers={'Content-Type':'application/json'};
const token=await Store.getAccessToken();
if(token)headers['Authorization']='Bearer '+token;
// RECOVMAX : récupérer IBAN du client final si plan=recovmax + clientCompanyId
let recovmaxContext={};
if(_plan==='recovmax'&&c.clientCompanyId){
try{
const cc=await Store.clients.getById(c.clientCompanyId);
if(cc){
recovmaxContext={
mode:'recovmax',
cabinetNom:Store.getUser()?.societe||Store.getUser()?.nom||'',
clientFinalNom:cc.nom||'',
clientFinalIban:cc.iban||null,
clientFinalBic:cc.bic||null,
// Si pas d'IBAN : mention "revenir vers le cabinet"
mentionPaiement:cc.iban
? `Pour effectuer le règlement, virez à : ${cc.nom} — IBAN : ${cc.iban}${cc.bic?' — BIC : '+cc.bic:''}`
: `Pour effectuer le règlement, merci de revenir vers ${Store.getUser()?.societe||Store.getUser()?.nom||'votre interlocuteur'} qui vous communiquera les coordonnées bancaires de ${cc.nom}.`,
// Template appliqué pour cette étape ?
template:await Store.templates.getForClient(c.clientCompanyId,etapeNum)
};
}
}catch(e){console.warn('[genererEtape] recovmax context:',e.message);}
}
const r=await fetch('/api/generate',{
method:'POST',headers,
body:JSON.stringify({
creance:{client:c.client,montant:c.montant,facture:c.facture,dateEcheance:c.dateEcheance,
profil:c.profil,contexte:c.contexte,clientEmail:c.clientEmail,
etapes:c.etapes.map(function(e){return{num:e.num,label:e.label,dateEnvoi:e.dateEnvoi,statut:e.statut};}),
lienPaiement:Store.getUser()?.lienPaiement||''},
etapeNum:etapeNum,
recovmaxContext:Object.keys(recovmaxContext).length?recovmaxContext:undefined
})
});
const d=await r.json();
const content0=d&&Array.isArray(d.content)&&d.content[0]&&typeof d.content[0].text==='string'?d.content[0].text:null;
if(!r.ok||!content0){throw new Error((d&&d.error)||'Réponse IA invalide');}
const txt=content0.replace(/```json|```/g,'').trim();
const parsed=JSON.parse(txt);
if(!parsed||typeof parsed.sujet!=='string'||typeof parsed.corps!=='string'){throw new Error('Format IA invalide');}
Store.updateEtape(creanceId,etapeNum,{sujet:parsed.sujet,corps:parsed.corps,genere:true});
Store.trackAIUsage();
Store.sendBackup();
// ─── RECOVMAX/Freelance : audit + increment quota relance ───
try{
await Store.audit.log('relance.generate',{creance_id:creanceId,client_company_id:c.clientCompanyId||null,details:{etape:etapeNum,client:c.client}});
if(_plan==='freelance'||_plan==='recovmax')await Store.quota.increment('relance');
}catch(e){console.warn('[genererEtape] audit/quota silent:',e.message);}
if(window.plausible) plausible('EmailGenerated', {props: {etape: etapeNum.toString()}});
UI.showView('detail',creanceId);
showToast('Email généré');
}catch(e){
showToast('Erreur : '+e.message);
}finally{
if(btn){btn.disabled=false;btn.textContent='Générer';}
}
}
function getSafeCreanceEtape(creanceId,etapeNum){
const c=normalizeCreance(Store.getCreance(creanceId));
if(!c)return {c:null,e:null};
const etapes=safeArray(c.etapes).map((et,i)=>normalizeEtape(et,i));
const e=etapes[etapeNum]||null;
return {c,e};
}
function copierEmail(creanceId,etapeNum){
const c=Store.getCreance(creanceId);if(!c)return;
const e=c.etapes[etapeNum];
navigator.clipboard.writeText('Objet : '+e.sujet+'\n\n'+e.corps).then(()=>showToast('Copié dans le presse-papier'));
}
async function programmerRappel(creanceId,etapeNum){
const {c,e}=getSafeCreanceEtape(creanceId,etapeNum);if(!c||!e){showToast('Étape introuvable');return;}
const user=Store.getUser()||{};
if(!e.dateEnvoi){showToast('Pas de date pour cette étape');return;}
if(!e.genere||!e.sujet){showToast('Générez d\'abord le texte de cette étape');return;}
if(e.dateEnvoi<=new Date().toISOString().split('T')[0]){showToast('Cette date est déjà passée. Envoyez la relance directement.');return;}
const ok=confirm('Programmer un rappel par email ?\n\n📧 Un email vous sera envoyé le '+fmtDate(e.dateEnvoi)+' pour vous rappeler d\'envoyer cette relance.\n\n→ '+e.label+' — '+c.client);
if(!ok)return;
try{
const r=await fetch('/api/register',{method:'GET'});// juste pour tester la connectivité
// Envoyer le rappel vers Supabase via un endpoint dédié
const _tk=await Store.getAccessToken();
const res=await fetch('/api/reminder',{
method:'POST',
headers:{'Content-Type':'application/json','Authorization':'Bearer '+(_tk||'')},
body:JSON.stringify({
user_email:user.email,
client_name:c.client,
facture:c.facture,
etape_num:etapeNum,
etape_label:e.label,
etape_sujet:e.sujet,
etape_corps:e.corps,
client_email:c.clientEmail||'',
remind_date:e.dateEnvoi
})
});
const d=await res.json();
if(d.ok){showToast('Rappel programmé pour le '+fmtDate(e.dateEnvoi)+' !');}
else{showToast('Erreur : '+(d.error||'impossible de programmer'));}
}catch(err){showToast('Erreur réseau');}
}
async function programmerEnvoi(creanceId,etapeNum){
const {c,e}=getSafeCreanceEtape(creanceId,etapeNum);if(!c||!e){showToast('Etape introuvable');return;}
if(!c.clientEmail){showToast('Email du client manquant — ajoutez-le dans la creance');return;}
if(!e.sujet||!e.corps){showToast('Generez d\'abord le texte de cette etape');return;}
if(!e.dateEnvoi){showToast('Pas de date pour cette etape');return;}
const today=new Date().toISOString().split('T')[0];
if(e.dateEnvoi<=today){showToast('Cette date est passee. Envoyez la relance directement.');return;}
const ok=confirm(
'Programmer l\'envoi de cette relance ?\n\n'+
'A : '+c.clientEmail+'\n'+
'Objet : '+e.sujet+'\n'+
'Date : '+fmtDate(e.dateEnvoi)+'\n\n'+
'Vous recevrez un email de confirmation la veille.\n'+
'Si vous ne faites rien, la relance sera envoyee a la date prevue.\n'+
'Vous pourrez annuler ou reporter a tout moment.'
);
if(!ok)return;
const user=Store.getUser()||{};
try{
const _tk2=await Store.getAccessToken();
const r=await fetch('/api/reminder',{
method:'POST',
headers:{'Content-Type':'application/json','Authorization':'Bearer '+(_tk2||'')},
body:JSON.stringify({
type:'scheduled_send',
user_email:user.email,
creance_id:c.id,
etape_num:etapeNum,
client_name:c.client,
client_email:c.clientEmail,
facture:c.facture,
sujet:e.sujet,
corps:e.corps,
from_name:user.nom||user.societe||'RECOV',
user_data:{nom:user.nom,societe:user.societe,adresse:user.adresse,ville:user.ville,siret:user.siret,email:user.email},
send_date:e.dateEnvoi
})
});
const d=await r.json();
if(d.ok){
Store.updateEtape(creanceId,etapeNum,{scheduled:true,scheduledDate:e.dateEnvoi});
showToast('Envoi programme pour le '+fmtDate(e.dateEnvoi));
UI.showView('detail',creanceId);
}else{showToast('Erreur : '+(d.error||'impossible de programmer'));}
}catch(err){showToast('Erreur reseau');}
}
function ajouterCalendrier(creanceId,etapeNum){
const {c,e}=getSafeCreanceEtape(creanceId,etapeNum);if(!c||!e){showToast('Étape introuvable');return;}
const date=e.dateEnvoi;
if(!date){showToast('Pas de date pour cette étape');return;}
// Proposer Google Calendar ou fichier .ics
const title='RECOV — '+e.label+' — '+c.client;
const desc='Relance à envoyer pour '+c.client+' ('+c.facture+', '+fmtMontant(c.montant)+')';
// Google Calendar link
const gcalDate=date.replace(/-/g,'');
const gcalUrl='https://calendar.google.com/calendar/render?action=TEMPLATE'+
'&text='+encodeURIComponent(title)+
'&dates='+gcalDate+'T090000/'+gcalDate+'T093000'+
'&details='+encodeURIComponent(desc+'\n\nGénéré par RECOV — recov.pro')+
'&sf=true';
// Fichier .ics
const icsContent='BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//RECOV//FR\nBEGIN:VEVENT\nDTSTART:'+gcalDate+'T090000\nDTEND:'+gcalDate+'T093000\nSUMMARY:'+title+'\nDESCRIPTION:'+desc.replace(/\n/g,'\\n')+'\nBEGIN:VALARM\nTRIGGER:-PT1H\nACTION:DISPLAY\nDESCRIPTION:Rappel RECOV\nEND:VALARM\nEND:VEVENT\nEND:VCALENDAR';
// Popup de choix
const choice=confirm('Ajouter au calendrier :\n\n📅 '+e.label+' — '+fmtDate(date)+'\n🏢 '+c.client+'\n\nOK = Google Calendar\nAnnuler = Télécharger fichier .ics (Outlook, Apple)');
if(choice){
window.open(gcalUrl,'_blank');
} else {
const blob=new Blob([icsContent],{type:'text/calendar'});
const a=document.createElement('a');a.href=URL.createObjectURL(blob);
a.download='recov-'+c.client.replace(/\s+/g,'-')+'-etape-'+(etapeNum+1)+'.ics';
a.click();
}
}
function buildSignatureText(){
const u=Store.getUser();
if(!u||(!u.nom&&!u.societe))return '';
let sig='\n\n--\n';
if(u.nom)sig+=u.nom+'\n';
if(u.societe)sig+=u.societe+'\n';
if(u.adresse)sig+=u.adresse;
if(u.ville)sig+=' - '+u.ville;
if(u.adresse||u.ville)sig+='\n';
if(u.siret&&/^\d{9,14}$/.test(u.siret.replace(/\s/g,'')))sig+=(u.siret.replace(/\s/g,'').length===9?'SIREN : ':'SIRET : ')+u.siret+'\n';
if(u.email)sig+=u.email;
return sig;
}
function ouvrirEmail(creanceId,etapeNum){
// Auto-save avant envoi
var sujetInput=document.getElementById('sujet_'+creanceId+'_'+etapeNum);
var corpsInput=document.getElementById('corps_'+creanceId+'_'+etapeNum);
if(sujetInput&&corpsInput){
Store.updateEtape(creanceId,etapeNum,{sujet:sujetInput.value,corps:corpsInput.value});
Store.save();
}
const {c,e}=getSafeCreanceEtape(creanceId,etapeNum);if(!c||!e){showToast('Étape introuvable');return;}
if(!e.sujet&&!e.corps){showToast('Générez d\'abord le texte');return;}
const to=c.clientEmail?encodeURIComponent(c.clientEmail):'';
const subject=encodeURIComponent(e.sujet||'');
const body=encodeURIComponent((e.corps||'')+buildSignatureText());
window.location.href='mailto:'+to+'?subject='+subject+'&body='+body;
}
async function envoyerAuto(creanceId,etapeNum){
// Auto-save avant envoi
var sujetInput=document.getElementById('sujet_'+creanceId+'_'+etapeNum);
var corpsInput=document.getElementById('corps_'+creanceId+'_'+etapeNum);
if(sujetInput&&corpsInput){
Store.updateEtape(creanceId,etapeNum,{sujet:sujetInput.value,corps:corpsInput.value});
Store.save();
}
const {c,e}=getSafeCreanceEtape(creanceId,etapeNum);if(!c||!e){showToast('Étape introuvable');return;}
if(!c.clientEmail){showToast('Email du client manquant');return;}
if(!e.genere||!e.sujet){showToast('Générez d\'abord le texte');return;}
const ok=confirm('Envoyer cette relance ?\n\n📧 À : '+c.clientEmail+'\n📝 Objet : '+e.sujet+'\n\nCet email est cordial et professionnel. Votre client ne saura pas que vous utilisez un outil.\n\nL\'email sera envoyé depuis noreply@recov.pro avec votre nom.');
if(!ok)return;
const user=Store.getUser();
try{
const _tkSend=await Store.getAccessToken();
const r=await fetch('/api/backup',{
method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+(_tkSend||'')},
body:JSON.stringify({
type:'send-relance',
clientEmail:c.clientEmail,
sujet:e.sujet,
corps:e.corps,
fromName:user.nom||user.societe||'RECOV',
fromEmail:user.email,
creanceRef:c.facture,
user:{nom:user.nom,societe:user.societe,adresse:user.adresse,ville:user.ville,siret:user.siret}
})
});
const d=await r.json();
if(d.success){
Store.updateEtape(creanceId,etapeNum,{statut:'envoye'});
Store.recalcStats();Store.save();
// Feedback immédiat
UI._lastSentClient=c.client;
UI._lastSentLabel=e.label;
const nextEtape=c.etapes[etapeNum+1];
UI._lastSentNext=nextEtape?fmtDate(nextEtape.dateEnvoi):null;
// Moment clé : animation d'envoi
showUnlockAnimation(c.montant, c.client, e.label);
// Envoyer le dossier de suivi
Store.getAccessToken().then(function(_tkD){fetch('/api/backup',{method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+(_tkD||'')},
body:JSON.stringify({type:'dossier',email:user.email,creance:c,etapeNum,user})}).catch(function(){});}).catch(function(){});
UI.showView('detail',creanceId);
} else {
showToast('Erreur : '+(d.error||'envoi impossible'));
}
}catch(err){showToast('Erreur réseau');}
}
function marquerEnvoye(creanceId,etapeNum){
const c=Store.getCreance(creanceId);
const e=c?c.etapes[etapeNum]:null;
Store.updateEtape(creanceId,etapeNum,{statut:'envoye',dateEnvoiEffective:new Date().toISOString().split('T')[0]});
Store.recalcStats();Store.save();
UI._lastSentClient=c?c.client:'';
UI._lastSentLabel=e?e.label:'';
const nextEtape=c&&c.etapes[etapeNum+1]?c.etapes[etapeNum+1]:null;
UI._lastSentNext=nextEtape?fmtDate(nextEtape.dateEnvoi):null;
// Message bravo si premier envoi global
const totalEnvoyes=Store.getAll().stats?.totalEnvoyes||0;
if(totalEnvoyes<=1){
UI._firstSendCelebration=true;
}
UI.showView('detail',creanceId);
// Envoyer le dossier de recouvrement par email
const user=Store.getUser();
const creance=Store.getCreance(creanceId);
if(user&&user.email&&creance){
Store.getAccessToken().then(function(_tkD2){fetch('/api/backup',{
method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+(_tkD2||'')},
body:JSON.stringify({type:'dossier',email:user.email,creance,etapeNum,user})
}).catch(()=>{});}).catch(()=>{});
}
}
function marquerPaye(creanceId){
if(!confirm('Votre client a payé cette facture ?\n\nCliquez OK pour clôturer ce dossier de relance.'))return;
const c=Store.getCreance(creanceId);
const payeesAvant=Store.getCreances().filter(x=>x.statut==='paye').length;
const totalAvant=(Store.getAll()&&Store.getAll().stats)?Store.getAll().stats.totalRecupere:0;
Store.marquerPaye(creanceId);
UI.showView('detail',creanceId);
const totalApres=(Store.getAll()&&Store.getAll().stats)?Store.getAll().stats.totalRecupere:0;
celebrationPaiement(c?c.montant:0, c?c.client:'votre client', payeesAvant===0, totalAvant, totalApres);
}
function filterDossierTab(tab){
// Mettre à jour boutons tabs
document.querySelectorAll('.dash-tab-btn').forEach(b=>b.classList.toggle('active',b.dataset.tab===tab));
// Afficher/masquer les lignes dossier
document.querySelectorAll('.dossier-row[data-tab]').forEach(row=>{
row.style.display=(tab==='tous'||row.dataset.tab===tab)?'flex':'none';
});
// Afficher/masquer les headers de groupe
document.querySelectorAll('[data-tab-group]').forEach(header=>{
header.style.display=(tab==='tous'||header.dataset.tabGroup===tab)?'':'none';
});
}
function setObjectifMensuel(){
const u=Store.getUser();
const actuel=u&&u.objectifMensuel?u.objectifMensuel:0;
const val=prompt('Votre objectif de recouvrement pour ce mois (€) :', actuel||'');
if(val===null)return;
const n=parseFloat(val.replace(',','.').replace(/[^0-9.]/g,''));
if(isNaN(n)||n<0){showToast('Montant invalide');return;}
const user=Store.getUser();
user.objectifMensuel=n;
Store.save();
UI.showView('dashboard');
showToast(n>0?`Objectif fixé à ${new Intl.NumberFormat('fr-FR',{style:'currency',currency:'EUR',maximumFractionDigits:0}).format(n)}`:'Objectif supprimé');
}
// ── CONSEILS DU JOUR ──
const CONSEILS_RECOV=[
{stat:'80%',pill:'1ère relance',text:'des impayés réglés dès les 2 premières relances. Agir tôt change tout.'},
{stat:'5%',pill:'chaque semaine',text:'de chances en moins de récupérer votre argent à chaque semaine de retard. Ne tardez pas.'},
{stat:'+34%',pill:'mardi/mercredi',text:'de réponses en plus pour les relances envoyées en milieu de semaine vs vendredi.'},
{stat:'2×',pill:'email court',text:'plus lu qu\'un long courrier. Une relance factuelle et concise est toujours plus efficace.'},
{stat:'60%',pill:'mise en demeure',text:'de probabilité de paiement en plus dans les 15 jours suivant son envoi.'},
{stat:'40€',pill:'indemnité légale',text:'forfaitaire due automatiquement pour tout retard B2B — sans clause dans le contrat.'},
{stat:'2×',pill:'appel téléphonique',text:'meilleur taux de résolution après 2 emails sans réponse. Décrochez le téléphone.'},
{stat:'+45%',pill:'échelonnement',text:'de chances de paiement partiel immédiat en proposant un plan de règlement.'},
{stat:'70%',pill:'5 relances',text:'des impayés PME récupérés avec une séquence de 5 relances bien espacées.'},
{stat:'+26%',pill:'numéro de facture',text:'de taux d\'ouverture en personnalisant l\'objet avec le numéro de facture concerné.'},
{stat:'70%',pill:'48h',text:'des paiements se font dans les 48h suivant une mise en demeure reçue.'},
{stat:'3×',pill:'J+1',text:'plus d\'impact en relançant le lendemain d\'une échéance manquée plutôt qu\'une semaine après.'},
{stat:'-40%',pill:'lien de paiement',text:'de délai de règlement en incluant un lien Stripe ou SumUp dans votre relance.'},
{stat:'0%',pill:'commission RECOV',text:'contre 15 à 30% pour un cabinet de recouvrement. Vous gardez tout ce que vous récupérez.'},
{stat:'L441-10',pill:'Code de commerce',text:'— citer cet article dans vos relances renforce leur portée et votre crédibilité juridique.'},
{stat:'Étape 3',pill:'relance formelle',text:'c\'est elle qui déclenche le plus de paiements spontanés. Ne la sautez pas.'},
{stat:'Semaine',pill:'trace écrite',text:'de chaque relance à conserver absolument — indispensable si l\'affaire va en justice.'},
{stat:'Weekend',pill:'à éviter',text:'pour vos relances : les emails pro envoyés vendredi soir ou dimanche sont presque ignorés.'},
{stat:'+60%',pill:'pénalités dans l\'objet',text:'d\'urgence perçue en les mentionnant dans le titre de l\'email — sans être agressif.'},
{stat:'Bons',pill:'clients fidèles',text:'— ceux qui paient après relance restent en moyenne d\'excellents partenaires commerciaux.'},
{stat:'Dès J+1',pill:'pénalités B2B',text:'les intérêts de retard courent légalement — pas besoin de clause contractuelle spécifique.'},
{stat:'Cédez',pill:'pas à la pression',text:'Un ton professionnel et ferme récupère plus que les menaces. Restez factuel, restez fort.'},
{stat:'CGV',pill:'délais de paiement',text:'vérifiez qu\'ils y figurent clairement — ça simplifie chaque recouvrement ultérieur.'},
{stat:'Relance',pill:'auto-générée',text:'par RECOV à chaque étape — vous validez et envoyez. Zéro rédaction, zéro oubli.'},
{stat:'Dossier',pill:'bien structuré',text:'avec dates, montants et échanges : il accélère toute procédure judiciaire éventuelle.'},
];
function getConseilDuJour(){
const dayOfYear=Math.floor((Date.now()-new Date(new Date().getFullYear(),0,0))/(1000*60*60*24));
return CONSEILS_RECOV[dayOfYear%CONSEILS_RECOV.length];
}
// ── MILESTONES ──
const MILESTONES=[500,1000,5000,10000,25000,50000,100000];
function checkMilestones(totalAvant,totalApres){
if(!Store._key)return;
const key='recovr_milestones_'+Store._key;
const done=JSON.parse(localStorage.getItem(key)||'[]');
const nouveau=MILESTONES.find(m=>totalAvant<m&&totalApres>=m&&!done.includes(m));
if(!nouveau)return;
done.push(nouveau);
localStorage.setItem(key,JSON.stringify(done));
setTimeout(()=>showMilestone(nouveau),500);
}
function showMilestone(montant){
const existing=document.getElementById('milestone-overlay');
if(existing)existing.remove();
const fmt=new Intl.NumberFormat('fr-FR',{style:'currency',currency:'EUR',maximumFractionDigits:0}).format(montant);
const el=document.createElement('div');
el.id='milestone-overlay';
el.style.cssText='position:fixed;inset:0;z-index:99999;display:flex;align-items:center;justify-content:center;padding:16px;background:rgba(15,23,42,.93);backdrop-filter:blur(10px);cursor:pointer';
el.onclick=()=>el.remove();
el.innerHTML=`<div style="background:#fff;border-radius:24px;max-width:420px;width:100%;overflow:hidden;box-shadow:0 32px 80px rgba(0,0,0,.5);animation:unlockPop .5s cubic-bezier(.34,1.56,.64,1) forwards">
<div style="background:linear-gradient(135deg,#0f172a 0%,#1a1a2e 50%,#0f172a 100%);padding:36px 28px 28px;text-align:center;position:relative;overflow:hidden">
<div style="position:absolute;inset:0;opacity:.06;background:repeating-linear-gradient(45deg,#84cc16 0,#84cc16 1px,transparent 0,transparent 50%);background-size:20px 20px"></div>
<div style="font-size:52px;margin-bottom:10px;position:relative">🏆</div>
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:13px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:#84cc16;margin-bottom:8px;position:relative">Palier atteint</div>
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:42px;font-weight:900;color:#fff;letter-spacing:-.04em;line-height:1;position:relative">${fmt}</div>
<div style="font-size:14px;color:rgba(255,255,255,.5);margin-top:8px;position:relative">récupérés avec RECOV</div>
</div>
<div style="padding:24px 28px">
<p style="font-size:14px;color:#0d0d14;font-weight:600;text-align:center;margin-bottom:16px;line-height:1.5">Vous venez de franchir le cap des ${fmt} récupérés.<br>Un résultat concret, sans intermédiaire.</p>
<div style="background:#f0fdf4;border-radius:12px;padding:12px 14px;font-size:13px;color:#166534;line-height:1.6;margin-bottom:20px">
💡 Un cabinet de recouvrement aurait prélevé jusqu'à ${new Intl.NumberFormat('fr-FR',{style:'currency',currency:'EUR',maximumFractionDigits:0}).format(montant*0.3)} sur cette somme.
</div>
<button onclick="document.getElementById('milestone-overlay').remove()" style="width:100%;padding:13px;background:#84cc16;color:#0f172a;border:none;border-radius:12px;font-family:inherit;font-size:15px;font-weight:500;cursor:pointer">Continuer →</button>
</div>
</div>`;
document.body.appendChild(el);
}
function celebrationPaiement(montant, client, isFirst, totalAvant, totalApres){
const existing=document.getElementById('celebration-overlay');
if(existing)existing.remove();
const fmt=montant?new Intl.NumberFormat('fr-FR',{style:'currency',currency:'EUR'}).format(parseFloat(montant)||0):'';
const el=document.createElement('div');
el.id='celebration-overlay';
el.style.cssText='position:fixed;inset:0;z-index:99999;display:flex;align-items:center;justify-content:center;padding:16px;background:rgba(15,23,42,.92);backdrop-filter:blur(8px);cursor:pointer';
el.onclick=()=>el.remove();
if(isFirst){
// Jalon spécial : première facture récupérée
el.innerHTML=`<div style="background:#fff;border-radius:24px;max-width:440px;width:100%;overflow:hidden;box-shadow:0 32px 80px rgba(0,0,0,.5);animation:unlockPop .5s cubic-bezier(.34,1.56,.64,1) forwards">
<div style="background:linear-gradient(135deg,#0f172a 0%,#14532d 100%);padding:32px 28px 24px;text-align:center">
<div style="font-size:56px;margin-bottom:8px">🏆</div>
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:26px;font-weight:500;color:#84cc16;letter-spacing:-.03em;margin-bottom:6px">Première victoire !</div>
<div style="font-size:16px;color:rgba(255,255,255,.7);font-weight:300">Vous venez de récupérer votre première facture avec RECOV</div>
</div>
<div style="padding:24px 28px;text-align:center">
${fmt?`<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:42px;font-weight:900;color:#0d0d14;letter-spacing:-.04em;margin-bottom:4px">${fmt}</div>`:''}
<div style="font-size:14px;color:#64748b;margin-bottom:20px">${client} a payé — dossier clôturé</div>
<div style="background:#f0fdf4;border-radius:12px;padding:14px 16px;font-size:13px;color:#166534;line-height:1.6;margin-bottom:20px;text-align:left">
💡 Un cabinet de recouvrement aurait prélevé jusqu'à 30% de cette somme. Avec RECOV, vous gardez tout.
</div>
<button onclick="document.getElementById('celebration-overlay').remove()" style="width:100%;padding:13px;background:#84cc16;color:#0f172a;border:none;border-radius:12px;font-family:inherit;font-size:15px;font-weight:500;cursor:pointer">Continuer →</button>
</div>
</div>`;
} else {
// Célébration standard
el.innerHTML=`<div style="background:#fff;border-radius:24px;max-width:400px;width:100%;overflow:hidden;box-shadow:0 32px 80px rgba(0,0,0,.5);animation:unlockPop .5s cubic-bezier(.34,1.56,.64,1) forwards">
<div style="background:linear-gradient(135deg,#0f172a 0%,#1e293b 100%);padding:28px 24px 20px;text-align:center">
<div style="font-size:48px;margin-bottom:8px">🎉</div>
<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:22px;font-weight:500;color:#84cc16;letter-spacing:-.03em">Facture récupérée !</div>
</div>
<div style="padding:20px 24px;text-align:center">
${fmt?`<div style="font-family:'Bricolage Grotesque',sans-serif;font-size:36px;font-weight:900;color:#0d0d14;letter-spacing:-.04em;margin-bottom:4px">${fmt}</div>`:''}
<div style="font-size:13px;color:#64748b;margin-bottom:16px">${client} a payé — dossier clôturé</div>
<div style="background:#f0fdf4;border-radius:10px;padding:12px 14px;font-size:12px;color:#166534;line-height:1.6;margin-bottom:16px;text-align:left">
💡 Un cabinet aurait pris jusqu'à 30% — vous gardez tout.
</div>
<button onclick="document.getElementById('celebration-overlay').remove()" style="width:100%;padding:12px;background:#84cc16;color:#0f172a;border:none;border-radius:10px;font-family:inherit;font-size:14px;font-weight:500;cursor:pointer">Super, continuer →</button>
</div>
</div>`;
}
document.body.appendChild(el);
// Auto-dismiss après 8s sauf si l'utilisateur a déjà cliqué
setTimeout(()=>{if(el.parentNode)el.remove();},8000);
// Vérifier milestone après la célébration principale
if(totalAvant!=null&&totalApres!=null){
setTimeout(()=>checkMilestones(totalAvant,totalApres), isFirst?1000:4500);
}
}
function showUnlockAnimation(montant, client, label){
const existing=document.getElementById('unlock-overlay');
if(existing)existing.remove();
const fmt=montant?new Intl.NumberFormat('fr-FR',{style:'currency',currency:'EUR'}).format(parseFloat(montant)||0):'';
const el=document.createElement('div');
el.id='unlock-overlay';
el.className='unlock-overlay';
el.innerHTML=`<div class="unlock-card">
<div style="position:absolute;top:-18px;left:50%;transform:translateX(-50%);width:36px;height:36px;border-radius:50%;border:2px solid #84cc16;animation:pulseRing 1s ease-out forwards"></div>
<div style="font-size:42px;margin-bottom:10px">📤</div>
<h2 style="color:#84cc16;font-size:18px;margin:0 0 6px;font-weight:800">Relance envoyée !</h2>
<p style="color:#e2e8f0;font-size:14px;margin:0 0 4px">${label||'Relance'} — ${client||''}</p>
${fmt?`<p style="color:#94a3b8;font-size:13px;margin:4px 0 0">Objectif : récupérer ${fmt}</p>`:''}
<p style="color:#64748b;font-size:11px;margin-top:14px">RECOV suit la suite automatiquement</p>
</div>`;
document.body.appendChild(el);
setTimeout(()=>{if(el.parentNode)el.remove();},3300);
}
function remplirDemoFictive(){
// Désactivé en mode RECOVMAX (un vrai client est déjà sélectionné/créé)
const _plan=Store._data?.user?.plan||'fondateur';
if(_plan==='recovmax'){
showToast('Démo désactivée en mode RECOVMAX — saisissez votre vraie facture');
return;
}
// Si l'utilisateur a déjà commencé à saisir, on ne remplace pas ses données
const fClient=document.getElementById('fClient');
const fMontant=document.getElementById('fMontant');
const fFacture=document.getElementById('fFacture');
const hasDataAlready=(fClient&&fClient.value.trim())||(fMontant&&fMontant.value.trim())||(fFacture&&fFacture.value.trim());
if(hasDataAlready){
if(!confirm('Vous avez déjà saisi des données. Remplacer par les données fictives de démo ?'))return;
}
const d=new Date();d.setDate(d.getDate()-32);
const echeance=d.toISOString().split('T')[0];
const fields={
'fMontant':'1850',
'fFacture':'FACT-2025-042',
'fEcheance':echeance,
'fClient':'BTP Martin SAS',
'fClientEmail':'comptabilite@btp-martin.fr',
'fContexte':'Mission livrée en avance, bon relationnel jusqu\'ici'
};
Object.entries(fields).forEach(([id,val])=>{
const el=document.getElementById(id);
if(el)el.value=val;
});
// Cocher "particulier" pour bypasser SIREN
const particulier=document.getElementById('fClientParticulier');
if(particulier){particulier.checked=true;toggleParticulier();}
showToast('Données fictives chargées — cliquez Suivant pour continuer');
}
async function supprimerCreance(creanceId){
if(!confirm('Supprimer cette facture ? Cette action est irréversible.'))return;
await Store.deleteCreance(creanceId);
UI.showView('dashboard');
showToast('Créance supprimée');
}
// ── VALIDATION SIRET (Luhn) ──
function validerSiret(siret){
const s=siret.replace(/\s/g,'');
if(!/^\d{14}$/.test(s))return{ok:false,msg:'Le SIRET doit contenir exactement 14 chiffres'};
// Algorithme de Luhn SIRET 14 chiffres : positions paires doublées (index 0-based)
let sum=0;
for(let i=0;i<14;i++){
let d=parseInt(s[i],10);
if(i%2===0){d*=2;if(d>9)d-=9;}
sum+=d;
}
if(sum%10!==0)return{ok:false,msg:'Ce SIRET est invalide (vérification Luhn échouée)'};
return{ok:true,siren:s.substring(0,9)};
}
function validerSiren(siren){
const s=siren.replace(/\s/g,'');
if(/^\d{14}$/.test(s))return validerSiret(s);
if(!/^\d{9}$/.test(s))return{ok:false,msg:'Le SIREN doit contenir 9 chiffres (ou 14 pour un SIRET)'};
// Algorithme de Luhn SIREN 9 chiffres : positions impaires doublées (index 0-based)
let sum=0;
for(let i=0;i<9;i++){
let d=parseInt(s[i],10);
if(i%2===1){d*=2;if(d>9)d-=9;}
sum+=d;
}
if(sum%10!==0)return{ok:false,msg:'Ce SIREN est invalide (vérification Luhn échouée)'};
return{ok:true,siren:s};
}
// ── VERIFICATION API SIRENE (entreprise active) — API publique gouv.fr, gratuite, côté client ──
async function verifierSirene(siren){
try{
const r=await fetch('https://recherche-entreprises.api.gouv.fr/search?q='+siren);
if(!r.ok)return{ok:true,msg:'Vérification indisponible — format valide'};
const d=await r.json();
if(!d.results||d.results.length===0)return{ok:false,msg:'Ce SIREN ne correspond à aucune entreprise enregistrée'};
const ent=d.results[0];
const actif=ent.etat_administratif==='A';
// Données protégées (auto-entrepreneur ayant demandé la non-diffusion INSEE)
const nomBrut=ent.nom_complet||ent.nom_raison_sociale||'';
const nonDiffusible=!ent.diffusable_commercialement||/\[non.diffusible\]/i.test(nomBrut)||nomBrut==='';
if(nonDiffusible){
return{ok:true,actif,nom:null,nonDiffusible:true,msg:actif?'Entreprise active — données protégées (non-diffusible INSEE)':'Entreprise cessée (données protégées)'};
}
return{ok:true,actif,nom:nomBrut,msg:actif?'Entreprise active : '+nomBrut:'Attention : cette entreprise est cessée ('+nomBrut+')'};
}catch(e){return{ok:true,msg:'Vérification indisponible — format valide'};}
}
// ── Mismatch SIRET ↔ nom société — état partagé entre checkProfilSiret et sauverProfil ──
let _siretApiNom = null; // nom légal retourné par Sirene
let _siretDisclaimerOk = false; // true si l'utilisateur a coché et confirmé
function _nomSimple(s){
// Normalise : minuscules, sans accents, sans tirets/espaces superflus
return (s||'').toLowerCase()
.normalize('NFD').replace(/[̀-ͯ]/g,'')
.replace(/[-_.,;]/g,' ').replace(/\s+/g,' ').trim();
}
function _siretNomMismatch(apiNom, userSociete, userNom){
if(!apiNom) return false;
const a=_nomSimple(apiNom);
const b=_nomSimple(userSociete);
const c=_nomSimple(userNom);
// Mots significatifs du nom légal (≥3 chars)
const mots=a.split(' ').filter(w=>w.length>=3);
if(!mots.length) return false;
// Si au moins 1 mot du nom légal apparaît dans la société OU le nom perso → OK
const matchSociete=mots.some(m=>b.includes(m));
const matchNom=mots.some(m=>c.includes(m));
return !(matchSociete||matchNom);
}
async function checkProfilSiret(){
const input=document.getElementById('pSiret');
const status=document.getElementById('pSiretStatus');
if(!input||!status)return;
_siretApiNom=null; _siretDisclaimerOk=false; // reset à chaque check
const val=input.value.replace(/\s/g,'');
if(!val){status.style.display='none';return;}
const v=validerSiret(val);
if(!v.ok){
status.style.display='block';status.style.background='#fef2f2';status.style.color='#991b1b';
status.textContent='⚠ '+v.msg;return;
}
status.style.display='block';status.style.background='var(--bg2)';status.style.color='var(--gris)';
status.textContent='Vérification en cours…';
const r=await verifierSirene(v.siren);
if(r.actif===false){
status.style.background='#fef2f2';status.style.color='#991b1b';
status.textContent='⚠ '+r.msg;
}else if(r.nonDiffusible){
// Données protégées INSEE — pas de comparaison possible, pas de mismatch
_siretApiNom=null;
status.style.background='#f0fdf4';status.style.color='#166534';
status.textContent='✓ '+r.msg;
}else if(r.nom){
_siretApiNom=r.nom;
const userSociete=(document.getElementById('pSociete')||{}).value||'';
const userNom=(document.getElementById('pNom')||{}).value||'';
const mismatch=_siretNomMismatch(r.nom, userSociete, userNom);
if(mismatch){
status.style.background='#fffbeb';status.style.color='#92400e';
status.textContent='⚠ SIRET enregistré au nom de "'+r.nom+'" — différent du nom renseigné. Une confirmation sera demandée à la sauvegarde.';
}else{
status.style.background='#f0fdf4';status.style.color='#166534';
status.textContent='✓ Entreprise vérifiée : '+r.nom;
}
}else{
status.style.background='var(--bg2)';status.style.color='var(--gris)';
status.textContent='ℹ '+r.msg;
}
}
function _showDisclaimerSiret(apiNom, onConfirm){
// Créer la modale si elle n'existe pas
let modal=document.getElementById('siret-disclaimer-modal');
if(modal) modal.remove();
modal=document.createElement('div');
modal.id='siret-disclaimer-modal';
modal.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:9999;display:flex;align-items:center;justify-content:center;padding:16px;animation:fadeIn .2s ease';
modal.innerHTML=`
<div style="background:#fff;border-radius:16px;max-width:480px;width:100%;padding:28px 24px;box-shadow:0 20px 60px rgba(0,0,0,.25);">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px">
<span style="font-size:24px">⚠️</span>
Vérification d'identité
</div>
<p style="font-size:14px;color:#334155;line-height:1.6;margin-bottom:8px">
Le SIRET renseigné correspond à l'entreprise enregistrée sous le nom légal :
</p>
<div style="background:#f1f5f9;border-radius:8px;padding:10px 14px;margin-bottom:14px;font-weight:500;font-size:15px;color:#0d0d14">${apiNom}</div>
<p style="font-size:13px;color:#64748b;line-height:1.6;margin-bottom:16px">
Ce nom diffère de celui renseigné dans votre profil. Vous pouvez continuer si vous êtes mandataire, co-gérant ou si vous utilisez un nom commercial différent.
</p>
<label style="display:flex;align-items:flex-start;gap:10px;cursor:pointer;margin-bottom:20px">
<input type="checkbox" id="siret-disclaimer-check" style="margin-top:2px;width:16px;height:16px;cursor:pointer;accent-color:#65a30d">
<span style="font-size:13px;color:#334155;line-height:1.5">
Je certifie sur l'honneur être dirigeant, mandataire ou autorisé à agir au nom de ${apiNom}.<br>
<span style="color:#94a3b8;font-size:11px">En confirmant, vous engagez votre responsabilité personnelle en cas d'usage frauduleux (Art. 313-1 Code pénal).</span>
</span>
</label>
<div style="display:flex;gap:10px">
<button onclick="document.getElementById('siret-disclaimer-modal').remove()" style="flex:1;padding:11px;border:1px solid #e2e8f0;background:#fff;border-radius:9px;font-size:14px;font-weight:500;cursor:pointer;color:#64748b">Annuler</button>
<button id="siret-disclaimer-confirm" onclick="
if(!document.getElementById('siret-disclaimer-check').checked){
this.style.animation='shake .3s ease';setTimeout(()=>this.style.animation='',400);return;
}
document.getElementById('siret-disclaimer-modal').remove();
window._siretDisclaimerCallback&&window._siretDisclaimerCallback();
" style="flex:2;padding:11px;border:none;background:#0d0d14;color:#fff;border-radius:9px;font-size:14px;font-weight:500;cursor:pointer">
Je confirme et enregistre →
</button>
</div>
</div>`;
document.body.appendChild(modal);
window._siretDisclaimerCallback=onConfirm;
}
function sauverProfil(){
const nom=document.getElementById('pNom').value.trim();
const siret=document.getElementById('pSiret').value.trim();
if(!nom){showToast('Le nom est obligatoire');document.getElementById('pNom').focus();return;}
// SIRET optionnel — pas de validation bloquante (entreprises étrangères, auto-entrepreneurs sans SIRET)
const societe=document.getElementById('pSociete').value.trim();
const doSave=async function(disclaimerAccepted){
const res=await Store.updateUser({
nom,
societe,
siret:siret.replace(/\s/g,''),
adresse:document.getElementById('pAdresse').value.trim(),
ville:document.getElementById('pVille').value.trim(),
lienPaiement:document.getElementById('pLienPaiement')?.value.trim()||'',
...(disclaimerAccepted?{siren_nom_legal:_siretApiNom,siren_disclaimer_accepted:true,siren_disclaimer_at:new Date().toISOString()}:{})
});
if(res&&res.ok===false){
console.error('[sauverProfil] DB save failed',res.error);
const _dbMsg=(res.error&&(res.error.message||res.error.details||res.error.hint))||'';
const _dbCode=res.error&&res.error.code;
const _short=_dbMsg?_dbMsg.length>80?_dbMsg.slice(0,80)+'…':_dbMsg:'erreur inconnue';
showToast('⚠️ Enregistrement serveur impossible ('+(_dbCode||'erreur')+' : '+_short+'). Vos données sont sauvegardées localement.');
return;
}
if(disclaimerAccepted){
// Log supplémentaire dans Supabase pour audit
try{
if(typeof supa!=='undefined'&&Store._userId){
supa.from('user_profiles').update({
siren_nom_legal:_siretApiNom,
siren_disclaimer_accepted:true,
siren_disclaimer_at:new Date().toISOString()
}).eq('id',Store._userId).then(()=>{}).catch(()=>{});
}
}catch(e){}
}
showToast('Profil enregistré'+(disclaimerAccepted?' ✓ Déclaration sur l\'honneur enregistrée':''));
};
// Si mismatch détecté et disclaimer pas encore accepté → modale
if(_siretApiNom && _siretNomMismatch(_siretApiNom, societe, nom) && !_siretDisclaimerOk){
_showDisclaimerSiret(_siretApiNom, function(){
_siretDisclaimerOk=true;
doSave(true);
});
return;
}
doSave(false);
}
function isProfilComplet(){
const u=Store.getUser();
return u&&u.nom;
}
// ── AVIS ──
let _avisNote=0;
function selectAvisNote(n){
_avisNote=n;
document.querySelectorAll('#avisStars button').forEach(btn=>{
const star=parseInt(btn.dataset.star);
btn.textContent=star<=n?'★':'☆';
btn.style.color=star<=n?'#f59e0b':'var(--gris2)';
});
}
async function envoyerAvis(){
const texte=(document.getElementById('avisTexte')||{}).value||'';
if(!_avisNote){showToast('Sélectionnez une note (1 à 5 étoiles)');return;}
const prenom=(document.getElementById('avisPrenom')||{}).value||'';
const metier=(document.getElementById('avisMetier')||{}).value||'';
const ville=(document.getElementById('avisVille')||{}).value||'';
const isPublic=document.getElementById('avisPublic')?document.getElementById('avisPublic').checked:false;
const status=document.getElementById('avisStatus');
if(status)status.textContent='Envoi...';
try{
const u=Store.getUser();
const supaUrl=typeof SUPABASE_URL!=='undefined'?SUPABASE_URL:null;
const supaKey=typeof SUPABASE_ANON_KEY!=='undefined'?SUPABASE_ANON_KEY:null;
if(supaUrl&&supaKey){
await fetch(supaUrl+'/rest/v1/avis',{
method:'POST',
headers:{'apikey':supaKey,'Authorization':'Bearer '+supaKey,'Content-Type':'application/json','Prefer':'return=minimal'},
body:JSON.stringify({email:u.email||'',nom:u.nom||'',prenom:prenom,metier:metier,ville:ville,note:_avisNote,texte:texte,is_public:isPublic,date:new Date().toISOString()})
});
}
const token=await Store.getAccessToken();
if(token){
fetch('/api/backup',{
method:'POST',
headers:{'Content-Type':'application/json','Authorization':'Bearer '+token},
body:JSON.stringify({type:'avis',email:u.email||'',nom:u.nom||'',prenom:prenom,metier:metier,ville:ville,note:_avisNote,texte:texte,is_public:isPublic})
}).catch(()=>{});
}
showToast('Merci pour votre avis !');
if(status)status.textContent='Envoyé — merci !';
if(document.getElementById('avisTexte'))document.getElementById('avisTexte').value='';
_avisNote=0;
document.querySelectorAll('#avisStars button').forEach(btn=>{btn.textContent='☆';btn.style.color='var(--gris2)';});
}catch(e){
showToast('Erreur lors de l\'envoi');
if(status)status.textContent='';
}
}
function exporterDonnees(){
const blob=new Blob([Store.exportJSON()],{type:'application/json'});
const a=document.createElement('a');a.href=URL.createObjectURL(blob);
a.download='recovr_export_'+new Date().toISOString().split('T')[0]+'.json';
a.click();
}
function importerDonnees(ev){
const file=ev.target.files[0];if(!file)return;
const reader=new FileReader();
reader.onload=function(e){
if(Store.importJSON(e.target.result)){showToast('Données importées');UI.showView('dashboard');}
else showToast('Fichier invalide');
};
reader.readAsText(file);
}
async function supprimerToutesCreances(){
const ids=Store.getCreances().map(c=>c.id);
if(!ids.length){showToast('Aucun dossier à supprimer');return;}
if(!confirm(`Supprimer les ${ids.length} dossier${ids.length>1?'s':''} ? Cette action est irréversible.`))return;
showToast('Suppression en cours…');
for(const id of ids){await Store.deleteCreance(id);}
UI.showView('dashboard');
showToast(`${ids.length} dossier${ids.length>1?'s':''} supprimé${ids.length>1?'s':''}`);
}
async function supprimerCompte(){
const ok=confirm('ATTENTION — Supprimer votre compte ?\n\n⚠️ Si vous avez un abonnement actif, résiliez-le d\'abord via le portail Stripe (bouton "Gérer mon abonnement" ci-dessus).\n\nLa suppression du compte n\'annule pas automatiquement votre abonnement Stripe.\n\nToutes vos factures et données seront effacées.\n\nContinuer ?');
if(!ok)return;
const ok2=confirm('Avez-vous bien résilié votre abonnement Stripe si vous en avez un ?\n\nOK = Oui, supprimer mon compte\nAnnuler = Non, je vais d\'abord résilier');
if(!ok2){gererAbonnement();return;}
// Supprimer de Supabase
const user=Store.getUser();
if(user&&user.email){
fetch('/api/register',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email:user.email,action:'delete'})}).catch(()=>{});
}
// Supabase signout (les données serveur seront supprimées par CASCADE si admin supprime le user)
await supa.auth.signOut();
// Supprimer localStorage
localStorage.removeItem('recovr_data');
Store._userId=null;
showToast('Compte supprimé. Vos données ont été effacées.');
setTimeout(()=>{location.reload();},2000);
}
function goOutil(){document.getElementById('outil').scrollIntoView({behavior:'smooth',block:'start'});}
function goPaiementPonctuel(){
if(!STRIPE_PONCTUEL){showToast('Offre ponctuelle bientôt disponible. Profitez du programme fondateur gratuit !');return;}
window.open(STRIPE_PONCTUEL,'_blank');
}
function goPaiement(){
if(!STRIPE_URL||STRIPE_URL.includes('REMPLACE'))showToast('Configurez votre lien Stripe d\'abord');
else window.open(STRIPE_URL,'_blank');
}
function gererAbonnement(){
if(!STRIPE_PORTAL||STRIPE_PORTAL.includes('REMPLACE'))showToast('Gestion d\'abonnement bientôt disponible.');
else window.open(STRIPE_PORTAL,'_blank');
}
function toggleFaq(el){el.classList.toggle('open');el.setAttribute('aria-expanded',el.classList.contains('open'));}
function toggleFullAvis(){
const list=window._avisFullList;
if(!list||!list.length)return;
const overlay=document.createElement('div');
overlay.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:10000;display:flex;align-items:center;justify-content:center;padding:20px;overflow-y:auto';
overlay.onclick=function(e){if(e.target===overlay)overlay.remove();};
let html='<div style="background:#fff;border-radius:16px;max-width:520px;width:100%;padding:clamp(24px,4vw,36px);box-shadow:0 20px 60px rgba(0,0,0,.2);max-height:90vh;overflow-y:auto;position:relative">';
html+='<button onclick="this.closest(\'div[style*=fixed]\').remove()" style="position:absolute;top:12px;right:16px;background:none;border:none;font-size:24px;color:var(--gris);cursor:pointer;line-height:1">×</button>';
list.forEach((a,i)=>{
if(i>0)html+='<div style="border-top:1px solid var(--border);margin:20px 0"></div>';
html+='<div style="color:#84CC16;font-size:14px;letter-spacing:3px;margin-bottom:8px">'+'★'.repeat(a.stars)+'</div>';
html+='<div style="font-size:15px;color:var(--noir);line-height:1.7;margin-bottom:10px">« '+a.text.replace(/</g,'<')+' »</div>';
html+='<div style="font-size:13px;color:var(--gris);font-weight:600">'+a.author.replace(/</g,'<')+'</div>';
});
html+='</div>';
overlay.innerHTML=html;
document.body.appendChild(overlay);
document.addEventListener('keydown',function esc(e){if(e.key==='Escape'){overlay.remove();document.removeEventListener('keydown',esc);}});
}
// Fermer dropdown nav au clic extérieur
document.addEventListener('click',function(e){if(!e.target.closest('.nav-dropdown')){document.querySelectorAll('.nav-dropdown.open').forEach(d=>d.classList.remove('open'));}});
// ── Tooltip global centré (desktop hover + mobile tap) ──
let _gtipTimer=null;
function showGTip(text,withOverlay){
const el=document.getElementById('g-tip');
const ov=document.getElementById('g-tip-overlay');
if(!el||!text)return;
clearTimeout(_gtipTimer);
// Permet le formatage riche (strong, br, ul) — les textes proviennent uniquement de nos data-tip hardcodés
el.innerHTML=text;
el.style.display='block';
// Force reflow pour transition CSS
el.offsetHeight;
el.classList.add('visible');
if(ov)ov.style.display=withOverlay?'block':'none';
}
function hideGTip(){
const el=document.getElementById('g-tip');
const ov=document.getElementById('g-tip-overlay');
if(!el)return;
el.classList.remove('visible');
_gtipTimer=setTimeout(()=>{el.style.display='none';},160);
if(ov)ov.style.display='none';
document.querySelectorAll('.tip.active').forEach(t=>t.classList.remove('active'));
}
// Hover desktop — mouseenter/mouseleave sur délégation propre
document.addEventListener('mouseover',function(e){
const t=e.target.closest('.tip');
if(t&&t.dataset.tip)showGTip(t.dataset.tip,false);
});
document.addEventListener('mouseout',function(e){
const t=e.target.closest('.tip');
if(t&&!t.contains(e.relatedTarget))hideGTip();
});
// Tap mobile — avec overlay pour fermer en touchant ailleurs
document.addEventListener('click',function(e){
const t=e.target.closest('.tip');
if(t&&t.dataset.tip){
e.preventDefault();e.stopPropagation();
t.classList.add('active');
showGTip(t.dataset.tip,true);
} else hideGTip();
});
function showToast(msg){
document.getElementById('toastMsg').textContent=msg;
const t=document.getElementById('toast');t.classList.add('show');
clearTimeout(t._timer);
t._timer=setTimeout(()=>t.classList.remove('show'),5000);
// Fermer au clic
t.onclick=()=>{clearTimeout(t._timer);t.classList.remove('show');};
t.style.cursor='pointer';
}
// ── INIT ──
function getDeadlineDaysLeft(){
const now=new Date();
const deadline=new Date(FOUNDER_DEADLINE+'T23:59:59');
return Math.max(0,Math.ceil((deadline-now)/(864e5)));
}
function updatePlacesDisplay(remaining){
const el=id=>document.getElementById(id);
const daysToDeadline=getDeadlineDaysLeft();
const urgency=daysToDeadline<=7?'Derniers jours !':daysToDeadline<=14?'Plus que '+daysToDeadline+'j':'Jusqu\'au 31 mai';
const txt=remaining<=0?'Complet':'Dernières places';
if(el('placesTxt'))el('placesTxt').textContent=txt+' · '+urgency;
if(el('placesGate'))el('placesGate').textContent=remaining<=0?'Complet':remaining;
if(el('heroPlaces'))el('heroPlaces').textContent=txt;
if(el('pricingPlaces'))el('pricingPlaces').textContent=remaining<=0?'Complet':remaining;
if(el('pricingPlaces2'))el('pricingPlaces2').textContent=remaining<=0?'Complet':remaining;
// ─── 2026-05-17 : ids pricingPeriod/2 retirés de la grille refondue —
// ces 2 lignes deviennent no-op (if(el) filtre). Conservées au cas où.
if(el('pricingPeriod'))el('pricingPeriod').textContent='Tarif fondateur · '+(remaining<=0?'Complet':'Dernières places');
if(el('pricingPeriod2'))el('pricingPeriod2').textContent='Tarif fondateur · '+(remaining<=0?'Complet':'Dernières places');
if(el('deadlineTag'))el('deadlineTag').textContent=urgency;
}
function loadHeroProof(){
if(location.protocol==='file:')return;
const supaUrl=typeof SUPABASE_URL!=='undefined'?SUPABASE_URL:null;
const supaKey=typeof SUPABASE_ANON_KEY!=='undefined'?SUPABASE_ANON_KEY:null;
if(!supaUrl||!supaKey)return;
fetch(supaUrl+'/rest/v1/avis?note=gte.3&is_public=eq.true&order=note.desc,date.desc&limit=20',{
headers:{'apikey':supaKey}
}).then(r=>{console.log('[avis] status:',r.status);return r.json();}).then(avis=>{
console.log('[avis] data:',avis);
if(!avis||!Array.isArray(avis)||!avis.length)return;
// 1. Meilleur avis dans le hero
const best=avis[0];
const el=id=>document.getElementById(id);
if(el('heroProof'))el('heroProof').style.display='block';
// Barre de preuve sociale — marquee continu (defilement lent infini)
if(el('proofBar')&&avis.length>0){
const truncate=(t,max)=>{
const s=(t||'').replace(/\s+/g,' ').trim();
if(s.length<=max)return s;
const cut=s.substring(0,max);
const sp=cut.lastIndexOf(' ');
return (sp>30?cut.substring(0,sp):cut)+'…';
};
const escapeHtml=s=>String(s||'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
const proofItems=avis.filter(a=>(a.texte||'').length>15).slice(0,10).map(a=>({
stars:Math.round(a.note||5),
text:truncate(a.texte,70),
author:(a.prenom||a.nom||'').trim(),
meta:[(a.metier||''),(a.ville||'')].filter(Boolean).join(' · ')
}));
if(proofItems.length>0){
const renderItem=it=>{
const starsStr='★'.repeat(it.stars||5);
return '<span style="display:inline-flex;align-items:center;gap:8px"><span style="color:var(--noir);letter-spacing:2px;font-size:12px">'+starsStr+'</span><span style="color:var(--noir)">« '+escapeHtml(it.text)+' »</span>'+(it.author?'<span style="color:var(--gris2);margin:0 4px">·</span><span style="color:var(--gris);font-size:12px">'+escapeHtml(it.author)+(it.meta?' · '+escapeHtml(it.meta):'')+'</span>':'')+'</span>';
};
// Duplication 2x pour boucle infinie sans saccade (translateX 0 -> -50%)
const html=proofItems.map(renderItem).join('')+proofItems.map(renderItem).join('');
const track=el('proofBarTrack');
if(track){track.innerHTML=html;}
el('proofBar').style.display='block';
// Vitesse adaptative : plus il y a d'avis, plus c'est lent (lecture confortable)
const speed=Math.max(40,proofItems.length*7);
if(track){track.style.animation='proofMarqueeAnim '+speed+'s linear infinite';}
}
}
// Préparer tous les avis
const avisList=avis.map(a=>{
const txt=a.texte||'';
let short=txt;
if(txt.length>200){short=txt.substring(0,200);const lastSpace=short.lastIndexOf(' ');if(lastSpace>100)short=short.substring(0,lastSpace);short+='…';}
const n=a.prenom||a.nom||'Utilisateur';
const r=[(a.metier||''),(a.ville||'')].filter(Boolean).join(' · ')||'Indépendant';
return{stars:a.note||5,text:short,author:n+' · '+r};
});
// Stocker les textes complets pour la popin
window._avisFullList=avis.map(a=>{
const n=a.prenom||a.nom||'Utilisateur';
const r=[(a.metier||''),(a.ville||'')].filter(Boolean).join(' · ')||'Indépendant';
return{stars:a.note||5,text:a.texte||'',author:n+' · '+r};
});
// Afficher le premier
let avisIdx=0;
function showAvis(idx){
const a=avisList[idx];
const proof=el('heroProof');
if(proof)proof.style.opacity='0';
setTimeout(()=>{
if(el('heroProofStars'))el('heroProofStars').textContent='★'.repeat(a.stars);
if(el('heroProofText'))el('heroProofText').textContent='« '+a.text+' »';
if(el('heroProofAuthor'))el('heroProofAuthor').textContent=a.author;
if(proof)proof.style.opacity='1';
},300);
}
showAvis(0);
// Rotation si 2+ avis
if(avisList.length>1){
if(el('heroProof'))el('heroProof').style.transition='opacity .3s ease';
setInterval(()=>{avisIdx=(avisIdx+1)%avisList.length;showAvis(avisIdx);},6000);
}
}).catch(()=>{});
}
function loadDashAvis(){
const container=document.getElementById('dashAvis');
if(!container)return;
const supaUrl=typeof SUPABASE_URL!=='undefined'?SUPABASE_URL:null;
const supaKey=typeof SUPABASE_ANON_KEY!=='undefined'?SUPABASE_ANON_KEY:null;
if(!supaUrl||!supaKey)return;
fetch(supaUrl+'/rest/v1/avis?note=gte.3&order=date.desc&limit=6',{
headers:{'apikey':supaKey}
}).then(r=>r.json()).then(avis=>{
if(!avis||!avis.length){container.innerHTML='';return;}
const cards=avis.map(a=>{
const stars='★'.repeat(a.note)+'☆'.repeat(5-a.note);
const name=a.prenom||a.nom||'Utilisateur';
const role=[(a.metier||''),(a.ville||'')].filter(Boolean).join(' · ')||'Indépendant';
const txt=a.texte.length>120?a.texte.substring(0,120)+'…':a.texte;
return '<div style="background:var(--bg2);border:1px solid var(--border);border-radius:14px;padding:16px">'+
'<div style="color:#f59e0b;font-size:14px;letter-spacing:2px;margin-bottom:8px">'+stars+'</div>'+
'<div style="font-size:13px;color:var(--gris);line-height:1.6;font-style:italic;margin-bottom:10px">"'+txt.replace(/</g,'<')+'"</div>'+
'<div style="font-size:12px;font-weight:700;color:var(--noir)">'+name.replace(/</g,'<')+'</div>'+
'<div style="font-size:11px;color:var(--gris2)">'+role.replace(/</g,'<')+'</div>'+
'</div>';
}).join('');
container.innerHTML='<div style="background:#fff;border:1px solid var(--border);border-radius:20px;padding:20px">'+
'<div style="font-family:\'Bricolage Grotesque\',sans-serif;font-size:18px;font-weight:500;color:var(--noir);margin-bottom:14px">Avis des utilisateurs</div>'+
'<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:12px">'+cards+'</div></div>';
}).catch(()=>{});
}
// ─── PENDING CRÉANCE depuis /start.html ───
// Si l'user vient de /start.html avec des infos pré-remplies,
// on stocke dans sessionStorage puis on crée la créance après login.
async function tryCreatePendingCreance(){
try{
const raw=sessionStorage.getItem('recov_pending_creance');
if(!raw)return;
const pending=JSON.parse(raw);
if(!pending||!pending.client||!pending.montant||!pending.date_echeance)return;
// Vérifier qu'on n'a pas déjà créé cette créance (idempotence)
const existing=Store.getCreances().find(c=>c.client===pending.client&&c.facture===pending.facture&&c.montant===parseFloat(pending.montant));
if(existing){
sessionStorage.removeItem('recov_pending_creance');
showToast('Créance déjà créée — bienvenue sur votre dashboard');
return;
}
// Créer la créance avec les données de start.html
const creance=await Store.addCreance({
client:pending.client,
clientEmail:pending.client_email||'',
montant:pending.montant,
facture:pending.facture||'Ref. à définir',
dateEcheance:pending.date_echeance,
profil:pending.profil||'silencieux',
contexte:pending.created_from==='start_page'?'Créance importée depuis '+(pending.partner_slug||'partenaire'):null
});
if(creance){
sessionStorage.removeItem('recov_pending_creance');
showToast('Créance créée depuis votre outil de facturation ✓');
// Rediriger vers la créance nouvellement créée
setTimeout(()=>{if(typeof UI!=='undefined'&&UI.showView)UI.showView('dashboard');},500);
}
}catch(e){console.warn('[RECOV] Pending créance error:',e);}
}
// ─── PARTNER ATTRIBUTION (multi-tenant) ───
// Capture ?partner=abby au premier passage, persistance sessionStorage
// Permet d'attribuer correctement l'inscription au partenaire d'origine
function capturePartnerFromUrl(){
try{
const params=new URLSearchParams(window.location.search);
const partner=(params.get('partner')||'').toLowerCase().replace(/[^a-z0-9-]/g,'').slice(0,64);
if(!partner)return;
// Ne pas écraser si déjà capturé (premier touch wins)
if(sessionStorage.getItem('recov_partner'))return;
const attribution={
partner:partner,
utm_source:params.get('utm_source')||null,
utm_campaign:params.get('utm_campaign')||null,
utm_medium:params.get('utm_medium')||null,
landing_url:window.location.href.slice(0,512),
captured_at:new Date().toISOString()
};
sessionStorage.setItem('recov_partner',JSON.stringify(attribution));
console.log('[RECOV] Partner attribution captured:',partner);
}catch(e){/* sessionStorage peut être bloqué (mode privé) */}
}
function getPartnerAttribution(){
try{
const raw=sessionStorage.getItem('recov_partner');
return raw?JSON.parse(raw):{};
}catch(e){return{};}
}
// Capture au chargement de la page
capturePartnerFromUrl();
function loadFounderCount(){
fetch('/api/register').then(r=>r.json()).then(d=>{
if(typeof d.remaining==='number')updatePlacesDisplay(d.remaining);
// Micro preuve sociale (afficher seulement si > 30)
const proof=document.getElementById('proofCounter');
const proofLine=document.getElementById('proofLine');
if(proof){proofLine.style.display='none';}
}).catch(()=>{});
}
// Smart nav: hide on scroll down, show on scroll up (seuil directionnel)
(function(){
let lastY=window.pageYOffset,ticking=false;
const hdr=document.querySelector('header');
if(!hdr)return;
window.addEventListener('scroll',function(){
if(!ticking){
requestAnimationFrame(function(){
const y=window.pageYOffset;
const diff=y-lastY;
if(y<80){hdr.classList.remove('nav-hidden');}
else if(diff>8){hdr.classList.add('nav-hidden');}
else if(diff<-8){hdr.classList.remove('nav-hidden');}
lastY=y;ticking=false;
});
ticking=true;
}
},{passive:true});
})();
window.addEventListener('DOMContentLoaded',async()=>{
initRgpdBanner();
updatePlacesDisplay(PLACES);
loadFounderCount();
loadHeroProof();
// Détecter un lien de réinitialisation ou une erreur auth dans l'URL
const hashParams=new URLSearchParams(window.location.hash.substring(1));
const urlParams=new URLSearchParams(window.location.search);
const isRecovery=hashParams.get('type')==='recovery'||urlParams.get('type')==='recovery';
const hashError=hashParams.get('error_description')||urlParams.get('error_description');
if(hashError){
// Lien expiré ou invalide
let msg=hashError;
if(msg.includes('expired'))msg='Ce lien a expiré. Demandez un nouveau lien de réinitialisation.';
else if(msg.includes('invalid'))msg='Ce lien est invalide. Demandez un nouveau lien.';
showToast(msg);
// Afficher le formulaire reset pour qu'il puisse redemander
setTimeout(()=>{
const outil=document.getElementById('outil');
if(outil)outil.scrollIntoView({behavior:'smooth'});
showResetForm();
},500);
window.history.replaceState(null,'','/');
// Continuer le chargement normal (pas de return)
}
if(isRecovery||_recoveryHandled){
// Le listener immédiat (après createClient) gère déjà le recovery
// Attendre encore un peu que Supabase traite le token
if(!_recoveryHandled){
// Forcer après 1s si pas encore traité
setTimeout(()=>{
if(!_recoveryHandled){
_recoveryHandled=true;
window.history.replaceState(null,'','/');
showNewPasswordForm();
}
},1000);
}
return;
}
// Guard anti double-init (critique pour PKCE OAuth : SIGNED_IN peut arriver avant OU après getSession)
let _appInitialized=false;
// ⚠️ IMPORTANT : enregistrer le listener AVANT getSession() pour ne pas rater l'event OAuth
// Supabase v2 PKCE : fire INITIAL_SESSION ou SIGNED_IN après échange du ?code=
supa.auth.onAuthStateChange(async(event,session)=>{
if(event==='PASSWORD_RECOVERY'){
showNewPasswordForm();
} else if((event==='SIGNED_IN'||event==='INITIAL_SESSION')&&session&&session.user){
if(_appInitialized||sessionStorage.getItem('recov_logged_out'))return;
_appInitialized=true;
Store._key='recovr_'+session.user.email.replace(/[^a-z0-9]/gi,'_');
Store.init();
Store.updateUser({email:session.user.email}); // _userId encore null → pas d'écrasement Supabase
Store._userId=session.user.id;
await Store.syncFromSupabase();
// ─── Auto-redirect plan recovmax → dashboard cabinet (mai 2026) ───
// Sauf si l'utilisateur a explicitement demandé à rester (?stay=1) ou est sur une route spécifique
try{
if(Store._data.user.plan==='recovmax' && !location.search.includes('stay=1') && !location.pathname.includes('admin')){
location.href='/recovmax-dashboard.html';
return;
}
}catch(e){console.warn('plan redirect check:',e);}
showApp();
window.history.replaceState(null,'','/');
await tryCreatePendingCreance();
} else if(event==='SIGNED_OUT'){
Store._userId=null;
_appInitialized=false;
}
});
// Vérifier session Supabase active (peut déclencher SIGNED_IN ci-dessus si PKCE exchange en cours)
const{data:{session}}=await supa.auth.getSession();
if(session&&session.user&&!sessionStorage.getItem('recov_logged_out')){
if(_appInitialized)return; // déjà géré par onAuthStateChange
_appInitialized=true;
Store._key='recovr_'+session.user.email.replace(/[^a-z0-9]/gi,'_');
Store.init();
Store.updateUser({email:session.user.email}); // _userId encore null → pas d'écrasement Supabase
Store._userId=session.user.id;
await Store.syncFromSupabase();
// ─── Auto-redirect plan recovmax → dashboard cabinet (mai 2026) ───
try{
if(Store._data.user.plan==='recovmax' && !location.search.includes('stay=1') && !location.pathname.includes('admin')){
location.href='/recovmax-dashboard.html';
return;
}
}catch(e){console.warn('plan redirect check:',e);}
showApp();
// Détecter une créance pending venue de /start.html
await tryCreatePendingCreance();
} else if(!_appInitialized){
// Pas de session — init Store avec cle par defaut
Store.init();
const user=Store.getUser();
if(user&&user.email&&!sessionStorage.getItem('recov_logged_out')){
showLoginForm();
}
// Si on vient de /start avec signup=1 → ouvrir directement le formulaire signup
const urlParams=new URLSearchParams(window.location.search);
if(urlParams.get('from')==='start_page'&&urlParams.get('signup')==='1'){
setTimeout(()=>openAuth('signup'),200);
}
}
});
async function exporterPDF(creanceId){
const c=Store.getCreance(creanceId);if(!c)return;
const user=Store.getUser();
const etape=c.etapes[5];
if(!etape||!etape.genere||!etape.corps){showToast('Générez d\'abord l\'email de mise en demeure');return;}
// ─── Vérification cap MED mensuel (migration 056 — plan solo/freelance: 1/mois) ───
const _plan=Store._data?.user?.plan||'solo';
const _medCap=Store.quota.cap('med_month',_plan);
const _medUsage=Store._data?.user?.quotas?.med||0;
if(isFinite(_medCap)&&_medUsage>=_medCap){
if(typeof showQuotaCapModal==='function'){
showQuotaCapModal('med');
}else{
alert('Limite mensuelle de mises en demeure atteinte ('+_medCap+'/mois). RECOVMAX (19 €/mois) permet un volume illimité pour les pros multi-clients.');
}
return;
}
// Incrément du compteur côté Supabase + rafraîchissement local
if(Store._userId){
try{
const{data,error}=await supa.rpc('increment_med_quota',{p_user_id:Store._userId});
if(error)console.warn('[exporterPDF/increment_med_quota]',error);
else if(data&&Store._data?.user?.quotas)Store._data.user.quotas.med=data.quota_med_count_month||(_medUsage+1);
}catch(e){console.warn('[exporterPDF/increment_med_quota] exception',e);}
}
const{jsPDF}=window.jspdf;
const doc=new jsPDF();
const pageW=doc.internal.pageSize.getWidth();
const margin=20;
const maxW=pageW-margin*2;
let y=20;
function addText(txt,size,style,align){
doc.setFontSize(size);
doc.setFont('helvetica',style||'normal');
const lines=doc.splitTextToSize(txt,maxW);
if(y+lines.length*size*0.45>280){doc.addPage();y=20;}
if(align==='center'){
lines.forEach(function(l){doc.text(l,pageW/2,y,{align:'center'});y+=size*0.45;});
}else{
doc.text(lines,margin,y);
y+=lines.length*size*0.45;
}
y+=2;
}
addText(user.societe||user.nom||'',12,'bold');
if(user.adresse)addText(user.adresse,10);
if(user.ville)addText(user.ville,10);
if(user.siret&&/^\d{9,14}$/.test(user.siret.replace(/\s/g,'')))addText((user.siret.replace(/\s/g,'').length===9?'SIREN : ':'SIRET : ')+user.siret,10);
y+=10;
addText('ENVOI EN RECOMMANDÉ AVEC ACCUSÉ DE RÉCEPTION',11,'bold','center');
y+=8;
addText('Destinataire : '+c.client,11,'bold');
y+=6;
var today=new Date();
var lieu=user.ville||'';
addText((lieu?lieu+', ':'')+'le '+today.toLocaleDateString('fr-FR',{day:'numeric',month:'long',year:'numeric'}),10);
y+=8;
addText('Objet : MISE EN DEMEURE DE PAYER',12,'bold');
y+=4;
addText('Facture '+c.facture+' — Montant : '+fmtMontant(c.montant),10);
y+=8;
var corps=etape.corps.replace(/\\n/g,'\n');
addText(corps,10);
y+=8;
var pen=Penalites.calculer(c.montant,c.dateEcheance,new Date().toISOString().split('T')[0],{regime:c.regimeDebiteur||'b2b',tauxFige:c.tauxApplique});
addText('PÉNALITÉS DE RETARD',11,'bold');
y+=2;
addText('Régime applicable : '+pen.regimeLabel,10);
addText('Taux applicable : '+pen.tauxApplicable+'% ('+pen.sourceTaux+')',10);
addText('Jours de retard : '+pen.joursRetard,10);
addText('Montant des pénalités : '+fmtMontant(pen.montantPenalites),10);
if(pen.indemnite>0){
addText('Indemnité forfaitaire de recouvrement : '+fmtMontant(pen.indemnite),10);
} else {
addText("(Indemnité forfaitaire de 40 € non applicable à ce régime)",10,'italic');
}
y+=4;
addText('Conformément à : '+pen.articleTaux+(pen.articleIndemnite?' + '+pen.articleIndemnite:''),10,'italic');
addText('Calcul indicatif — Taux BCE '+TAUX_SEMESTRE+' — Vérifiez le taux en vigueur avant envoi',9,'italic');
y+=6;
addText('TOTAL DÛ : '+fmtMontant(pen.totalDu),13,'bold');
y+=4;
addText('Ce document est un modèle généré par RECOV. Il doit être relu et adapté avant envoi.',8,'italic');
y+=10;
addText(user.nom||user.societe||'',11,'bold');
doc.save('mise-en-demeure-'+c.client.replace(/\s+/g,'-')+'.pdf');
showToast('PDF téléchargé');
}
</script>
<!-- Back to top -->
<button id="backToTop" onclick="window.scrollTo({top:0,behavior:'smooth'})" style="position:fixed;right:20px;bottom:24px;width:44px;height:44px;border-radius:50%;background:var(--noir);color:#fff;border:none;font-size:18px;cursor:pointer;opacity:0;transform:translateY(10px);transition:opacity .2s,transform .2s;z-index:90;box-shadow:0 4px 12px rgba(0,0,0,.15);display:flex;align-items:center;justify-content:center" aria-label="Retour en haut">↑</button>
<script>
// Reset zoom on pinch release (mobile)
let pinching=false;
document.addEventListener('touchstart',e=>{if(e.touches.length>=2)pinching=true;},{passive:true});
document.addEventListener('touchend',()=>{
if(pinching){pinching=false;setTimeout(()=>{
const vp=document.querySelector('meta[name="viewport"]');
if(vp){vp.content='width=device-width,initial-scale=1.0,maximum-scale=1.0';
setTimeout(()=>{vp.content='width=device-width,initial-scale=1.0';},100);}
},300);}
},{passive:true});
// Init stack bottom au chargement
adjustBottomStack();
// Back to top visibility
const btt=document.getElementById('backToTop');
window.addEventListener('scroll',()=>{
if(window.scrollY>600){btt.style.opacity='1';btt.style.transform='none';}
else{btt.style.opacity='0';btt.style.transform='translateY(10px)';}
},{passive:true});
// ── RECOVO — Moteur d'intents Layer 1 (zéro appel API) ──
const RecovoIntents = {
detect(text) {
const t = text.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'');
if(/tout va bien|ca va|comment.*(ca|ça)|resume|bilan|situation|etat/.test(t)) return 'health';
if(/combien|recupere|encaisse|gagne|total|stats|statistique/.test(t)) return 'stats';
if(/urgent|relancer|action|aujourd.hui|a faire|priorite/.test(t)) return 'urgent';
if(/prochaine|prochainement|bientot|semaine|agenda|calendrier|date/.test(t)) return 'agenda';
const clients=(Store._data?.creances||[]).map(c=>c.client?.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,''));
for(let i=0;i<clients.length;i++){const name=clients[i];if(!name)continue;const words=name.split(/\s+/);if(words.some(w=>w.length>3&&t.includes(w)))return{type:'client',idx:i};}
if(/objectif|mois|mensuel/.test(t)) return 'objectif';
if(/aide|help|quoi|comment|qu.est.ce|explique|comment ca marche/.test(t)) return 'help';
return null;
},
answer(intent) {
const data=Store._data||{};
const creances=data.creances||[];
const stats=data.stats||{};
const user=data.user||{};
const prenom=user.nom?.split(' ')[0]||'';
const fmt=n=>new Intl.NumberFormat('fr-FR',{style:'currency',currency:'EUR',maximumFractionDigits:0}).format(n||0);
const fmtDate=s=>s?new Date(s).toLocaleDateString('fr-FR',{day:'numeric',month:'long'}):'—';
if(intent==='health'){
const actives=creances.filter(c=>c.statut!=='paye');
const urgentes=actives.filter(c=>{const etapes=c.etapes||[];const idx=c.prochaineEtape||0;if(idx>=etapes.length)return false;return new Date(etapes[idx].dateEnvoi)<=new Date();});
const total=actives.reduce((s,c)=>s+parseFloat(c.montant||0),0);
if(actives.length===0)return `Tout est au vert${prenom?' '+prenom:''}! 🎉 Vous n'avez aucune facture impayée en cours. C'est rare, profitez-en !`;
const parts=[`**${actives.length} dossier${actives.length>1?'s':''} actif${actives.length>1?'s':''}** pour un total de **${fmt(total)}**.`];
if(urgentes.length)parts.push(`⚡ **${urgentes.length} urgent${urgentes.length>1?'s':''}** nécessite${urgentes.length>1?'nt':''} votre attention aujourd'hui.`);
else parts.push(`✅ Aucune relance urgente — RECOV gère le planning.`);
return parts.join(' ');
}
if(intent==='stats'){
const recupere=stats.totalRecupere||0;
const enCours=creances.filter(c=>c.statut!=='paye').reduce((s,c)=>s+parseFloat(c.montant||0),0);
const nb=creances.filter(c=>c.statut==='paye').length;
return `Voici vos chiffres :\n\n💚 **Récupéré :** ${fmt(recupere)}\n⏳ **En cours :** ${fmt(enCours)}\n✅ **Dossiers clôturés :** ${nb}\n💡 **Économisé vs cabinet :** ${fmt(recupere*0.3)}`;
}
if(intent==='urgent'){
const now=new Date();
const urgentes=creances.filter(c=>{if(c.statut==='paye')return false;const etapes=c.etapes||[];const idx=c.prochaineEtape||0;if(idx>=etapes.length)return false;return new Date(etapes[idx].dateEnvoi)<=now;});
if(!urgentes.length)return `Bonne nouvelle ! 🎉 Aucune relance urgente aujourd'hui. RECOV surveille pour vous.`;
const list=urgentes.slice(0,3).map(c=>`• **${c.client}** — ${fmt(c.montant)}`).join('\n');
return `⚡ **${urgentes.length} relance${urgentes.length>1?'s':''} à envoyer aujourd'hui :**\n\n${list}${urgentes.length>3?`\n• … et ${urgentes.length-3} autre(s)`:''}\n\nOuvrez le dossier depuis le tableau de bord pour les préparer.`;
}
if(intent==='agenda'){
const now=new Date();const today=new Date(now.getFullYear(),now.getMonth(),now.getDate());
const actives=creances.filter(c=>c.statut!=='paye');
const urgentes=actives.filter(c=>{const etapes=c.etapes||[];const idx=c.prochaineEtape||0;if(idx>=etapes.length)return false;return new Date(etapes[idx].dateEnvoi)<=now;});
const futures=actives.filter(c=>{const etapes=c.etapes||[];const idx=c.prochaineEtape||0;if(idx>=etapes.length)return false;return new Date(etapes[idx].dateEnvoi)>now;}).sort((a,b)=>{const ea=(a.etapes||[])[a.prochaineEtape||0];const eb=(b.etapes||[])[b.prochaineEtape||0];return new Date(ea.dateEnvoi)-new Date(eb.dateEnvoi);}).slice(0,3);
let rep='';
if(urgentes.length){const lu=urgentes.map(c=>{const e=(c.etapes||[])[c.prochaineEtape||0];return `• **${c.client}** — ${e?.label||'relance'} A ENVOYER AUJOURD\'HUI`;}).join('\n');rep+=`**${urgentes.length} relance${urgentes.length>1?'s':''} urgente${urgentes.length>1?'s':''} aujourd\'hui :**\n${lu}\n\n`;}
if(futures.length){const lf=futures.map(c=>{const e=(c.etapes||[])[c.prochaineEtape||0];return `• **${c.client}** — ${e?.label||'relance'} le ${fmtDate(e?.dateEnvoi)}`;}).join('\n');rep+=`**Prochaines relances :**\n${lf}`;}
if(!rep)return `Aucune relance planifiée. Pensez a ajouter vos nouvelles factures impayees !`;
return rep.trim();
}
if(typeof intent==='object'&&intent.type==='client'){
const c=creances[intent.idx];if(!c)return null;
const fmt2=n=>new Intl.NumberFormat('fr-FR',{style:'currency',currency:'EUR',minimumFractionDigits:2}).format(n);
if(c.statut==='paye')return `✅ **${c.client}** a payé ! La facture de **${fmt2(c.montant)}** est clôturée.`;
const etapes=c.etapes||[];const idx=c.prochaineEtape||0;const etape=etapes[idx];
const nbEnvoyes=etapes.filter(e=>e.statut==='envoye').length;
return `Dossier **${c.client}** :\n💶 **${fmt2(c.montant)}** — ${nbEnvoyes} relance${nbEnvoyes>1?'s':''} envoyée${nbEnvoyes>1?'s':''}\n📅 Prochaine étape : **${etape?.label||'—'}** le ${fmtDate(etape?.dateEnvoi)}\n\n${nbEnvoyes>=3?'⚠️ Ce dossier est avancé — pensez à la mise en demeure.':'📬 La séquence progresse normalement.'}`;
}
if(intent==='objectif'){
const obj=parseFloat(user.objectifMensuel||0);
const moisCourant=Store.getMontantMoisCourant?.()??0;
if(!obj)return `Vous n'avez pas encore défini d'objectif mensuel. Appuyez sur "🎯 Définir un objectif" dans votre tableau de bord !`;
const pct=Math.round(moisCourant/obj*100);
const fmt3=n=>new Intl.NumberFormat('fr-FR',{style:'currency',currency:'EUR',maximumFractionDigits:0}).format(n);
return `🎯 Objectif du mois : **${fmt3(obj)}**\n✅ Récupéré ce mois-ci : **${fmt3(moisCourant)}** (${pct}%)\n${pct>=100?'🏆 Objectif atteint !':pct>=60?`💪 Encore **${fmt3(obj-moisCourant)}** pour l'atteindre.`:`⚡ **${fmt3(obj-moisCourant)}** restants — concentrez-vous sur les urgences !`}`;
}
return null;
}
};
// ── RECOVO — Base de connaissance statique Layer 2 ──
const RecovoKB=[
{keys:['mise en demeure','lettre recommandee','recommande'],answer:`📮 **La mise en demeure** c'est une lettre officielle envoyée en recommandé avec accusé de réception. Elle donne **8 jours** à votre client pour payer avant de pouvoir aller en justice.\n\nRECOV génère la mise en demeure automatiquement à l'étape 4 de votre séquence.`},
{keys:['penalite','interet de retard','taux de retard'],answer:`💶 **Les pénalités de retard** sont dues dès le lendemain de la date d'échéance. Taux légal 2025 : **3 fois le taux légal** (~12% annuel pour les pro).\n\nRECOV les calcule et les mentionne automatiquement dans vos relances.`},
{keys:['injonction','tribunal','justice','huissier','avocat'],answer:`⚖️ **Après la mise en demeure** sans réponse :\n1. **Injonction de payer** (tribunal, rapide, ~80€)\n2. **Référé provision** (si urgence)\n3. **Huissier** pour signification\n\nRECOV vous accompagne jusqu'à cette étape.`},
{keys:['siret','siren','kbis'],answer:`🔍 Le **SIRET** (14 chiffres) ou **SIREN** (9 chiffres) identifient votre client officiellement. RECOV vérifie l'existence de l'entreprise via l'API gouvernement. Indispensable pour les relances légales.`},
{keys:['abonnement','tarif','combien ca coute','prix recov'],answer:`💳 RECOV est en **programme pilote gratuit** jusqu'au 31 mai. Dès le 1er juin :\n• **RECOV Solo · 9 €/mois** (jusqu'à 10 créances actives) — pour relancer vos propres factures.\n• **RECOVMAX Fondateur · 19 €/mois** — si vous suivez les impayés de plusieurs clients (cabinets, AVs, comptables solos). 3 places fondateurs disponibles jusqu'au 25 mai.\n• **RECOVMAX Standard · 29 €/mois** ensuite.\n0 % de commission — vous gardez tout ce que vous récupérez.`},
{keys:['fonctionnement','comment marche','sequence','relance automatique'],answer:`🚀 **Comment fonctionne RECOV :**\n1. Vous ajoutez une facture impayée\n2. RECOV génère une séquence de 5 relances personnalisées\n3. Vous validez et envoyez chaque relance au bon moment\n4. Escalade progressive : email → mise en demeure → avocat`},
];
function recovoKBSearch(text){const t=text.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'');for(const e of RecovoKB){if(e.keys.some(k=>t.includes(k)))return e.answer;}return null;}
// ── RECOVO — Objet principal ──
const Recovo={
_open:false,_history:[],_recognition:null,_listening:false,_muted:true,_pendingTranscript:'',
_synth:typeof speechSynthesis!=='undefined'?speechSynthesis:null,
init(prenom){
const btn=document.getElementById('recovo-btn');
if(btn){
btn.style.display='block';
btn.onclick=(e)=>{e.stopPropagation();window.Recovo.toggle();};
}
// Délégation unique sur le panel — capture tous les boutons même sur mobile
const panel=document.getElementById('recovo-panel');
if(panel){
const _handle=function(e){
const btn=e.target.closest('[data-action]');
if(!btn)return;
e.preventDefault();e.stopPropagation();
const a=btn.dataset.action;
if(a==='mic')window.Recovo.toggleVoice();
else if(a==='send')window.Recovo.send();
else if(a==='close')window.Recovo.close();
else if(a==='mute')window.Recovo.toggleMute(btn);
};
panel.addEventListener('touchend',_handle,{passive:false});
panel.addEventListener('click',_handle);
}
const inp=document.getElementById('recovo-input');
if(inp)inp.addEventListener('keydown',(e)=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();window.Recovo.send();}});
// Mode micro retiré — RECOVO fonctionne uniquement en saisie texte
this._history=[];
const greet=prenom
?`Bonjour **${prenom}** ! Je suis RECOVO, votre copilote recouvrement.\n\nPosez votre question par écrit. Je réponds sur vos dossiers en temps réel.`
:`Bonjour ! Je suis RECOVO, votre copilote recouvrement.\n\nPosez votre question par écrit, je vous réponds sur vos dossiers.`;
this._addMsg('bot',greet);
this._renderChips(['Tout va bien ?','Dossiers urgents','Mes stats','Prochaine relance']);
},
toggle(){this._open?this.close():this.open();},
open(){
this._open=true;
const p=document.getElementById('recovo-panel');
const o=document.getElementById('recovo-overlay');
if(p){
p.classList.add('open');
p.setAttribute('aria-hidden','false');
// Force inline — bypass tout conflit CSS
p.style.cssText='transform:translateY(0) scale(1);opacity:1;pointer-events:auto;';
}
if(o){o.classList.add('open');o.style.cssText='opacity:1;pointer-events:auto;';}
setTimeout(()=>this._scrollBottom(),50);
},
close(){
this._open=false;
const p=document.getElementById('recovo-panel');
const o=document.getElementById('recovo-overlay');
if(p){
p.classList.remove('open');
p.setAttribute('aria-hidden','true');
p.style.cssText='transform:translateY(100%);opacity:0;pointer-events:none;';
}
if(o){o.classList.remove('open');o.style.cssText='opacity:0;pointer-events:none;';}
this._stopVoice();
},
_getInp(){return document.getElementById('recovo-input');},
_inpVal(el){return(el?.value!==undefined?el.value:el?.innerText||'').trim();},
_inpSet(el,v){if(el?.value!==undefined)el.value=v;else if(el)el.innerText=v;},
send(){
const inp=this._getInp();
const text=this._inpVal(inp);
if(!text)return;
this._inpSet(inp,'');
this._addMsg('user',text);
this._clearChips();
this._showTyping();
setTimeout(()=>this._respond(text),600);
},
onKey(e){if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();this.send();}},
chip(text){const inp=this._getInp();this._inpSet(inp,text);this.send();},
async _respond(text){
this._removeTyping();
const intent=RecovoIntents.detect(text);
if(intent){const answer=RecovoIntents.answer(intent);if(answer){this._addMsg('bot',answer);this._suggestAfter(intent);return;}}
const kb=recovoKBSearch(text);
if(kb){this._addMsg('bot',kb);return;}
const check=Store.checkAIUsage?.();
if(check&&!check.ok){this._addMsg('bot',`⚠️ ${check.reason}\n\nMais je peux quand même répondre à vos questions sur vos dossiers !`);return;}
try{
this._showTyping();
const context=this._buildContext();
const token=await Store.getAccessToken?.();
const headers={'Content-Type':'application/json'};
if(token)headers['Authorization']='Bearer '+token;
const r=await fetch('/api/generate',{method:'POST',headers,body:JSON.stringify({action:'recovo',message:text,context,history:this._history.slice(-6)})});
this._removeTyping();
if(!r.ok)throw new Error('Erreur réseau');
const d=await r.json();
const reply=d.reply||d.text||d.content;
if(!reply)throw new Error('Réponse vide');
Store.trackAIUsage?.();
this._addMsg('bot',reply);
}catch(e){
this._removeTyping();
this._addMsg('bot',`Je n'ai pas compris. Essayez :\n• "Tout va bien ?"\n• "Dossiers urgents"\n• "Mes stats"\n• Le nom d'un de vos clients`);
}
},
_buildContext(){
const d=Store._data||{};const u=d.user||{};
const creances=(d.creances||[]).map(c=>({client:c.client,montant:c.montant,statut:c.statut,facture:c.facture,dateEcheance:c.dateEcheance,nbRelances:(c.etapes||[]).filter(e=>e.statut==='envoye').length,prochaineEtape:c.prochaineEtape}));
return{utilisateur:u.nom||u.email,dateAujourdhui:new Date().toLocaleDateString('fr-FR'),totalCreances:creances.length,totalRecupere:d.stats?.totalRecupere||0,creances:creances.slice(0,15)};
},
_addMsg(role,text){
const time=new Date().toLocaleTimeString('fr-FR',{hour:'2-digit',minute:'2-digit'});
this._history.push({role,text,time});
const wrap=document.getElementById('recovo-messages');if(!wrap)return;
const div=document.createElement('div');div.className='recovo-msg '+role;
const html=this._md(text);
div.innerHTML=`<div class="recovo-bubble">${html}</div><div class="recovo-time" style="display:flex;align-items:center;gap:6px">${time}</div>`;
wrap.appendChild(div);this._scrollBottom();
// Lecture auto des réponses bot (sauf si muet)
if(role==='bot'&&!this._muted)this._speakText(text);
},
_md(text){return text.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/\*\*(.+?)\*\*/g,'$1').replace(/\n/g,'<br>');},
_showTyping(){
const wrap=document.getElementById('recovo-messages');
if(!wrap||wrap.querySelector('.recovo-typing'))return;
const div=document.createElement('div');div.className='recovo-msg bot recovo-typing';
div.innerHTML='<div class="recovo-bubble"><div class="recovo-dots"><span></span><span></span><span></span></div></div>';
wrap.appendChild(div);this._scrollBottom();
},
_removeTyping(){document.querySelector('.recovo-typing')?.remove();},
_scrollBottom(){const w=document.getElementById('recovo-messages');if(w)w.scrollTop=w.scrollHeight;},
_renderChips(list){
const c=document.getElementById('recovo-chips');if(!c)return;
c.innerHTML=list.map(t=>`<button class="recovo-chip" onclick="Recovo.chip('${t.replace(/'/g,"\\'")}')">${t}</button>`).join('');
},
_clearChips(){const c=document.getElementById('recovo-chips');if(c)c.innerHTML='';},
_suggestAfter(intent){
const map={health:['Dossiers urgents','Prochaine relance','Mes stats'],stats:['Tout va bien ?','Objectif du mois','Dossiers urgents'],urgent:['Prochaine relance','Mes stats','Tout va bien ?'],agenda:['Dossiers urgents','Mes stats'],objectif:['Mes stats','Tout va bien ?']};
const key=typeof intent==='object'?'client':intent;
this._renderChips(map[key]||['Tout va bien ?','Mes stats','Dossiers urgents']);
},
_isIOS(){return /iPad|iPhone|iPod/.test(navigator.userAgent)||(/MacIntel/.test(navigator.platform)&&navigator.maxTouchPoints>1);},
toggleVoice(){
if(this._listening){this._stopVoice();return;}
const SR=window.SpeechRecognition||window.webkitSpeechRecognition;
if(!SR){showToast('Voix non disponible sur ce navigateur');return;}
const mic=document.getElementById('recovo-mic');
const self=this;
self._recognition=new SR();
self._recognition.lang='fr-FR';
self._recognition.interimResults=false;
self._recognition.continuous=false;
self._recognition.onresult=function(e){
const t=e.results[0][0].transcript.trim();
const inp=self._getInp();
self._inpSet(inp,t);
};
self._recognition.onerror=function(ev){
const msg=ev.error==='not-allowed'?'Autorisez le micro dans les réglages du navigateur':ev.error==='no-speech'?'Aucune voix détectée':'Erreur micro';
showToast(msg);self._stopVoice();
};
self._recognition.onend=function(){
self._listening=false;
if(mic)mic.textContent='MIC';
mic?.classList.remove('listening');
setTimeout(()=>{
if(self._inpVal(self._getInp()))self.send();
},250);
};
try{
self._recognition.start();
self._listening=true;
mic?.classList.add('listening');
if(mic)mic.textContent='...';
}catch(err){showToast('Micro non accessible — vérifiez les permissions');}
},
_stopVoice(){
if(this._recognition){try{this._recognition.stop();}catch(e){}this._recognition=null;}
this._listening=false;
const mic=document.getElementById('recovo-mic');
if(mic)mic.textContent='MIC';
mic?.classList.remove('listening');
},
_cleanForSpeech(text){
return text
.replace(/\*\*/g,'')
.replace(/<[^>]+>/g,'')
.replace(/[\u{1F000}-\u{1FFFF}]/gu,'')
.replace(/[\u2600-\u27BF]/g,'')
.replace(/[\u{1F300}-\u{1F9FF}]/gu,'')
.replace(/[•→←↑↓★☆◆▶❯»«]/g,'')
.replace(/\n+/g,'. ')
.replace(/\s{2,}/g,' ')
.trim();
},
_speakText(text){
if(!this._synth||this._muted)return;
this._synth.cancel();
const clean=this._cleanForSpeech(text);
if(!clean)return;
const utt=new SpeechSynthesisUtterance(clean);utt.lang='fr-FR';utt.rate=0.9;
this._synth.speak(utt);
},
toggleMute(btn){
this._muted=!this._muted;
if(this._synth)this._synth.cancel();
btn.textContent=this._muted?'SON OFF':'SON';
btn.title=this._muted?'Activer le son':'Couper le son';
},
};
// Exposer Recovo globalement
window.Recovo = Recovo;
// Fallback overlay close — init() rebind les autres boutons
(function(){
const ov=document.getElementById('recovo-overlay');
if(ov)ov.addEventListener('click',function(){window.Recovo&&window.Recovo.close();});
})();
window.gateSignupStep = gateSignupStep;
window.unlockApp = unlockApp;
// ── Google OAuth ──────────────────────────────────────────────────────────────
async function signInWithGoogle(){
const btn = document.getElementById('btnGoogleAuth');
if(btn){btn.disabled=true;btn.textContent='Redirection…';}
const{error}=await supa.auth.signInWithOAuth({
provider:'google',
options:{redirectTo:window.location.origin+'/'}
});
if(error){
if(btn){btn.disabled=false;}
document.getElementById('btnGoogleAuth').innerHTML='<svg width="18" height="18" viewBox="0 0 48 48"><path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.08 17.74 9.5 24 9.5z"/><path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/><path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/><path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.18 1.48-4.97 2.31-8.16 2.31-6.26 0-11.57-3.59-13.46-8.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/><path fill="none" d="M0 0h48v48H0z"/></svg> Continuer avec Google';
showToast('Google non configuré — utilisez l\'email pour l\'instant');
}
}
window.signInWithGoogle = signInWithGoogle;
// ── Magic link (signup) ───────────────────────────────────────────────────────
async function sendMagicLink(){
const email=(document.getElementById('gateEmail')||{}).value||'';
if(!email||!email.includes('@')){showToast('Entrez votre email d\'abord');return;}
const btn=document.getElementById('btnMagicLink');
if(btn){btn.textContent='Envoi…';btn.disabled=true;}
const{error}=await supa.auth.signInWithOtp({
email:email.trim().toLowerCase(),
options:{emailRedirectTo:window.location.origin}
});
if(error){
if(btn){btn.textContent='Recevoir un lien de connexion par email (sans mot de passe)';btn.disabled=false;}
showToast('Erreur : '+error.message);return;
}
if(btn){btn.textContent='✅ Lien envoyé ! Vérifiez votre boîte mail';}
showToast('Lien envoyé à '+email+' — vérifiez votre boîte mail 📧');
}
window.sendMagicLink = sendMagicLink;
// ── Magic link (login) ────────────────────────────────────────────────────────
async function sendMagicLinkLogin(){
const email=(document.getElementById('loginEmail')||{}).value||'';
if(!email||!email.includes('@')){showToast('Entrez votre email d\'abord');return;}
const{error}=await supa.auth.signInWithOtp({
email:email.trim().toLowerCase(),
options:{emailRedirectTo:window.location.origin}
});
if(error){showToast('Erreur : '+error.message);return;}
showToast('Lien envoyé à '+email+' — vérifiez votre boîte mail 📧');
}
window.sendMagicLinkLogin = sendMagicLinkLogin;
window.loginApp = loginApp;
window.resetPassword = resetPassword;
window.logout = logout;
</script>
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/sw.js').catch(function(){});}</script>
<script src="/search.js" defer></script>
<script src="/chatbot.js" defer></script>
<script>
(function(){
var items = document.querySelectorAll('.hero-slogan-item');
if(!items.length) return;
var current = 0;
setInterval(function(){
items[current].classList.remove('active');
current = (current + 1) % items.length;
items[current].classList.add('active');
}, 3800);
})();
</script>
</body>
</html>