/* ClientBase, blog SEO (pages-blog.jsx)
   --------------------------------------------------------------------------
   Page /blog : index liste tous les articles + page article /blog/<slug>.
   Objectif principal : ranking Google sur les requêtes clés des indépendants
   francophones (gestion RDV, alternatives Planity / Iara / Treatwell,
   guides métier coiffeur / esthéticienne / tatoueur / etc., problèmes
   concrets comme les no-show, la TVA, les acomptes).

   Chaque article est un objet { slug, title, desc, keywords, date, audience,
   category, body[] } pour rester data-driven, faciles à ajouter, et offrir
   un rendu cohérent. body[] est un array de blocs typés ({ type, ... }) ce
   qui évite d'avoir à manipuler du HTML brut tout en gardant la sémantique
   propre (h2, h3, p, list, quote, cta).

   Quand on ajoute un article il suffit de pousser un nouvel objet dans
   ARTICLES — il apparaît automatiquement dans l'index, dans le sitemap
   futur, et est accessible à /blog/<slug>. */

const BLOG_CATEGORIES = {
  guide:       { label: "Guide pratique",   tone: "var(--accent)",      ink: "var(--accent-ink)" },
  comparatif:  { label: "Comparatif",       tone: "var(--rose, oklch(60% 0.18 340))", ink: "var(--rose-ink, oklch(42% 0.14 340))" },
  metier:      { label: "Métier",           tone: "var(--sage, oklch(55% 0.12 160))", ink: "var(--sage-ink, oklch(38% 0.10 160))" },
  problemes:   { label: "Problème → solution", tone: "oklch(60% 0.17 30)", ink: "oklch(42% 0.14 30)" },
  conseil:     { label: "Conseil",          tone: "var(--cli-accent)",   ink: "var(--cli-ink)" },
};

const ARTICLES = [
  {
    slug: "logiciel-gestion-rendez-vous-independants",
    title: "Logiciel de gestion de rendez-vous pour indépendants : ce qu'il faut vraiment",
    desc: "Agenda en ligne, fiches clients, facturation, fidélité, page de RDV publique : ce que doit faire un vrai outil de gestion de rendez-vous en 2026 pour les pros indépendants.",
    keywords: ["logiciel gestion rendez-vous", "gestion rdv indépendant", "agenda en ligne pro", "logiciel agenda indépendant"],
    date: "2026-05-12",
    audience: "pro",
    category: "guide",
    readMin: 6,
    body: [
      { type: "p", text: "Quand on est indépendant, la gestion des rendez-vous dévore vite la journée. Entre les appels qui interrompent une prestation, les SMS de confirmation à taper le soir, les no-show non anticipés et la compta à boucler en fin de mois, le temps « non productif » se cumule. Le bon outil de gestion de rendez-vous n'est pas celui qui en fait le plus — c'est celui qui élimine ces frictions sans en créer d'autres." },
      { type: "h2", text: "Ce que doit faire un logiciel de gestion de rendez-vous pour indépendant" },
      { type: "p", text: "On parle souvent de fonctionnalités, mais l'angle utile pour un solo, c'est : qu'est-ce qui me fait gagner du temps chaque jour ?" },
      { type: "list", items: [
        "Un agenda visuel, lisible en 1 seconde, qui marche aussi sur mobile entre deux rendez-vous.",
        "Un lien public de réservation que les clients ouvrent pour réserver eux-mêmes (40 % des RDV sont pris sans appel quand le lien est partagé sur Instagram ou WhatsApp).",
        "Des fiches clients qui s'enrichissent toutes seules au fil des prestations (historique, allergies, préférences, fidélité).",
        "Une facturation conforme aux normes françaises (SIRET, TVA, mentions légales) qu'on émet en 3 clics, pas en 10 minutes.",
        "Des rappels SMS automatiques 24 h avant le RDV — le geste qui fait baisser le no-show de 7 % à environ 2 %.",
      ]},
      { type: "h2", text: "Les fausses bonnes idées" },
      { type: "p", text: "Beaucoup d'outils ajoutent une marketplace, une IA, un module marketing, une vidéo, un coach virtuel. Pour un solo, chaque module supplémentaire = une option de plus à comprendre, configurer, oublier. Le bon réflexe : choisir un outil qui fait peu, mais qui le fait très bien. La vraie économie de temps n'est pas dans les fonctionnalités, elle est dans la friction qu'on supprime." },
      { type: "h2", text: "Le piège des marketplaces" },
      { type: "p", text: "Les plateformes type Planity ou Treatwell sont des marketplaces : elles capturent vos clients dans LEUR base et vous facturent une commission sur chaque rendez-vous (souvent 1 à 3 € par RDV pris via leur app). Sur 30 RDV / semaine, c'est 100 à 300 € par mois qui partent. Sans compter que vos clients deviennent leurs clients — ils peuvent vous remplacer par un concurrent en un clic." },
      { type: "quote", text: "« Le vrai indépendant garde ses clients dans SA base, pas dans celle d'une plateforme. »" },
      { type: "h2", text: "Comment choisir" },
      { type: "list", items: [
        "Sans engagement : un outil qui vous garde par la qualité, pas par un contrat de 12 mois.",
        "Données hébergées en France, RGPD : vos clients vous font confiance, ne les vendez pas.",
        "Lisible sur mobile : 70 % des RDV se prennent depuis un téléphone.",
        "Pas de marketplace : VOS clients restent VOS clients.",
        "Prix prévisible : un tarif simple, pas une grille à 12 cases.",
      ]},
      { type: "cta", label: "Essayer ClientBase gratuitement", page: "signup" },
    ],
  },

  {
    slug: "alternative-planity-treatwell",
    title: "Alternative à Planity, Treatwell, Iara : pourquoi passer à un outil sans commission",
    desc: "Vous payez 1 à 3 € par RDV via Planity, Treatwell ou Iara ? Voici les alternatives sans commission, sans marketplace, qui vous laissent garder vos clients.",
    keywords: ["alternative Planity", "alternative Treatwell", "alternative Iara", "logiciel sans commission", "alternative planning"],
    date: "2026-05-10",
    audience: "pro",
    category: "comparatif",
    readMin: 5,
    body: [
      { type: "p", text: "Si vous lisez ces lignes, c'est probablement que la facture Planity ou Treatwell de ce mois vous a fait grincer des dents. C'est normal — ces plateformes ne sont pas conçues pour vous, elles sont conçues pour leurs investisseurs. Décryptage et alternatives concrètes." },
      { type: "h2", text: "Pourquoi les marketplaces coûtent vraiment cher" },
      { type: "p", text: "Le modèle économique des marketplaces (Planity, Treatwell, Iara, Yelp, Doctolib pour le médical) repose sur 2 leviers : un abonnement mensuel ET une commission par RDV pris via leur app/site. Pour un pro qui fait 100 RDV / mois dont 40 viennent de la plateforme, ça représente facilement 80 à 150 € de commissions en plus de l'abonnement de base." },
      { type: "p", text: "Multipliez par 12 mois — sur un an, c'est entre 1 000 et 2 000 € qui partent en commissions pour des clients qui auraient pu vous trouver autrement." },
      { type: "h2", text: "Le vrai coût caché : la dépendance" },
      { type: "p", text: "Au-delà du prix, le problème est plus profond. Quand un client réserve via Planity, c'est UN client de Planity qui passe chez vous. Sa fiche, son historique, ses préférences, son numéro — tout est dans LEUR base. Si vous quittez la plateforme, vous repartez de zéro. Les marketplaces ont volontairement rendu l'export client compliqué pour éviter ça." },
      { type: "h2", text: "Les alternatives sans commission" },
      { type: "p", text: "Une nouvelle génération d'outils a émergé ces dernières années, conçus différemment : zéro marketplace, zéro commission, vos clients vous appartiennent. Le modèle est un abonnement simple, comme un loyer de bureau." },
      { type: "list", items: [
        "ClientBase — outil tout-en-un français, gratuit pendant la bêta. Agenda, fiches clients, factures, page de RDV publique, fidélité, sans aucune commission.",
        "Calendly — agenda en ligne très bien, mais pas adapté aux métiers de service (pas de fiches clients riches, pas de facturation conforme française).",
        "Outils maison (Google Calendar + Notion) — gratuits mais demandent du temps de configuration et ne couvrent ni la facturation ni les rappels automatiques.",
      ]},
      { type: "h2", text: "Migrer sans tout perdre" },
      { type: "p", text: "La peur la plus fréquente : « je vais perdre tous mes clients ». En réalité, dès que vous communiquez votre nouveau lien de réservation (Instagram, WhatsApp, SMS à votre fichier client), 90 % de vos habitués basculent en moins de 2 mois. Les nouveaux clients vous trouvent via Google, Instagram, le bouche-à-oreille — pas via la marketplace." },
      { type: "quote", text: "« Le jour où j'ai quitté Planity, j'ai économisé 145 € le premier mois. Au bout d'un an, c'est presque deux semaines de vacances en plus. »" },
      { type: "cta", label: "Voir comment ClientBase remplace votre marketplace", page: "features" },
    ],
  },

  {
    slug: "coiffeur-independant-gestion-rdv",
    title: "Coiffeur ou coiffeuse indépendant·e : comment gérer ses RDV sans se compliquer la vie",
    desc: "Astuces concrètes pour les coiffeurs et coiffeuses indépendants : agenda en ligne, fiche cliente (couleur, longueur, allergies), no-show, lien de réservation Instagram.",
    keywords: ["logiciel coiffeur", "gestion rdv coiffeuse", "agenda coiffeur indépendant", "coiffeur à domicile rdv"],
    date: "2026-05-08",
    audience: "pro",
    category: "metier",
    readMin: 5,
    body: [
      { type: "p", text: "Coiffeur ou coiffeuse à domicile, en salon partagé, ou avec votre propre cabine — la gestion des RDV peut vite devenir un casse-tête quand les prestations vont de 30 minutes à 3 heures et que chaque cliente a son historique technique (couleur précise, longueur, dernière décoloration…). Voici comment simplifier sans rien perdre." },
      { type: "h2", text: "Le défi spécifique aux coiffeurs : la durée variable" },
      { type: "p", text: "Une coupe rapide = 30 minutes, un balayage + brushing = 3 heures. Un agenda mal configuré finit avec des chevauchements, des temps morts, ou pire — un RDV qu'on rate parce qu'on a sous-estimé la durée. La règle d'or : configurez chaque prestation avec sa durée par défaut, pas chaque RDV individuellement." },
      { type: "h2", text: "La fiche cliente qui change tout" },
      { type: "p", text: "Pour un coiffeur, la fiche cliente n'est pas un gadget. C'est mémoriser :" },
      { type: "list", items: [
        "Le numéro de couleur exact (Wella 7.3 + 6.45, oxydant 6%) et la dernière date d'application.",
        "La longueur cible et les zones à éviter (mèches autour du visage, frange à garder).",
        "Les allergies (PPD, ammoniaque, parfums forts).",
        "Les préférences hors-technique : musique, conversation, café, ne pas être dérangé·e.",
      ]},
      { type: "p", text: "Une fiche bien tenue, c'est une cliente qui se sent suivie et qui revient. C'est aussi la différence entre un solo amateur et un solo qui sait ce qu'il fait." },
      { type: "h2", text: "Le lien de réservation Instagram" },
      { type: "p", text: "70 % des coiffeurs indépendants trouvent leurs nouveaux clients via Instagram. Le réflexe gagnant : mettre votre lien de réservation public dans la bio Instagram. Le client tape un nom de produit ou suit votre travail, voit votre dernière story, clique → réserve. Aucun téléphone, aucun DM, aucun « est-ce que t'as un créneau samedi ». Vous récupérez 30 minutes par jour minimum." },
      { type: "h2", text: "Réduire les no-show" },
      { type: "p", text: "Le no-show est l'ennemi du coiffeur indépendant. Un RDV de 2 h non honoré, c'est 60 à 90 € perdus et un créneau qu'on ne peut plus revendre. Deux leviers efficaces :" },
      { type: "list", items: [
        "SMS de rappel automatique 24 h avant le RDV (réduit le no-show de 7 % à 2 % en moyenne).",
        "Acompte de 30 % à la réservation pour les nouveaux clients (suffisant pour décourager les RDV pris à la légère, pas dissuasif pour les vrais prospects).",
      ]},
      { type: "cta", label: "Tester un agenda fait pour coiffeurs indépendants", page: "signup" },
    ],
  },

  {
    slug: "esthéticienne-comment-réduire-no-show",
    title: "Esthéticienne : comment réduire les no-show avec un acompte (sans faire fuir les clientes)",
    desc: "Le no-show coûte cher aux esthéticiennes indépendantes. Comment mettre en place un acompte de réservation efficace, légal, et bien accepté par la clientèle.",
    keywords: ["esthéticienne no-show", "acompte réservation rdv", "esthéticienne indépendante", "logiciel esthétique"],
    date: "2026-05-05",
    audience: "pro",
    category: "problemes",
    readMin: 4,
    body: [
      { type: "p", text: "Une cliente qui ne se présente pas à son RDV, c'est 50 à 120 € de chiffre d'affaires perdu, et un créneau d'une heure qu'on ne peut presque jamais revendre la veille. Pour une esthéticienne solo qui fait 25-30 prestations / semaine, 2 no-show = -10 % de revenu sur la semaine. Voici une méthode qui marche : l'acompte à la réservation." },
      { type: "h2", text: "Pourquoi l'acompte marche" },
      { type: "p", text: "Le no-show n'est presque jamais malveillant — c'est de l'oubli, du « j'avais pas vu que c'était aujourd'hui », du « finalement j'ai un autre truc ». Quand la cliente a posé 15 ou 30 € à la réservation, elle se présente. C'est psychologique : on n'oublie pas un RDV qu'on a déjà payé en partie." },
      { type: "h2", text: "Le bon montant : 30 % du prix de la prestation" },
      { type: "p", text: "30 % est le sweet spot. Assez pour que la cliente y pense, pas trop pour que ça la dissuade de réserver. Pour une prestation à 60 €, c'est 18 € — un montant accepté sans débat." },
      { type: "list", items: [
        "Soin visage 60 € → acompte 18 €",
        "Épilation jambes complètes 35 € → acompte 10 €",
        "Maquillage 80 € → acompte 25 €",
        "Forfait spa 120 € → acompte 35 €",
      ]},
      { type: "h2", text: "Comment l'annoncer sans braquer la cliente" },
      { type: "p", text: "L'erreur classique : dire « je prends un acompte parce que j'en ai marre des no-show ». Ça met la cliente sur la défensive. Préférez :" },
      { type: "quote", text: "« Pour réserver votre créneau, je vous demande un petit acompte de 30 %, déduit du montant total le jour du RDV. Ça permet de garantir votre place et de gérer les annulations équitablement. »" },
      { type: "p", text: "Le mot clé est « garantir votre place ». La cliente entend : « ma place m'est réservée ». Pas « tu es présumée tricheuse »." },
      { type: "h2", text: "Le cadre légal en France" },
      { type: "p", text: "L'acompte est légal et reconnu (article 1590 du Code civil). Si la cliente annule moins de 48 h avant, l'acompte est conservé. Si vous annulez de votre côté, vous le remboursez intégralement. Pensez à le préciser dans vos CGU sur votre page de réservation." },
      { type: "cta", label: "Activer les acomptes sur ClientBase", page: "features" },
    ],
  },

  {
    slug: "tatoueur-organisation-rdv-flash",
    title: "Tatoueur indépendant : organiser RDV custom, flashs et walk-in sans s'arracher les cheveux",
    desc: "Comment un tatoueur indépendant peut gérer en parallèle les RDV custom (longs, planifiés des mois à l'avance), les flashs (rapides) et les walk-in (imprévus).",
    keywords: ["tatoueur indépendant agenda", "logiciel tatoueur", "gestion rdv tatouage", "tattoo flash booking"],
    date: "2026-05-03",
    audience: "pro",
    category: "metier",
    readMin: 4,
    body: [
      { type: "p", text: "Le tatoueur a un défi particulier : il gère 3 types de RDV très différents en parallèle. Le custom (4-8 h, planifié 2-3 mois à l'avance), le flash (1-2 h, dispo dans la semaine), et le walk-in (imprévu, on prend si on a le temps). Un agenda mal organisé = surbooking, créneaux gaspillés, ou pire, un client custom qui se prend une attente d'1 heure parce qu'on a accepté un flash juste avant." },
      { type: "h2", text: "Bloquer des créneaux par type" },
      { type: "p", text: "L'astuce : segmenter votre semaine. Par exemple :" },
      { type: "list", items: [
        "Lundi-mardi : custom uniquement (créneaux longs réservés sur la page publique).",
        "Mercredi-vendredi : mix custom + flash.",
        "Samedi : flash + walk-in (pas de custom pour garder de la souplesse).",
      ]},
      { type: "p", text: "Sur votre page publique de réservation, ne montrez que les créneaux ouverts au type concerné. Un client qui veut un custom ne doit pas pouvoir poser un slot du samedi (réservé aux flashs)." },
      { type: "h2", text: "L'acompte custom : non-négociable" },
      { type: "p", text: "Pour un custom à 800 €, demandez 30 % d'acompte (240 €) à la confirmation. C'est l'engagement mutuel : vous bloquez la journée, le client confirme qu'il est sérieux. Sans cette barrière, vous prendrez 1 à 2 no-show / mois sur les customs — et chaque no-show, c'est une journée perdue qui coûte une semaine de loyer." },
      { type: "h2", text: "Photos de référence dans la fiche client" },
      { type: "p", text: "Le tatoueur a besoin de retrouver vite : photos de l'idée, lieu sur le corps, taille en cm, style (linework, color, dotwork), références d'inspiration. Une fiche client avec un champ « notes + photos » est plus précieuse que dix réglages d'agenda fancy. Quand le client arrive 3 mois après la première discussion, vous avez tout sous les yeux en 5 secondes." },
      { type: "cta", label: "Voir l'agenda + fiches clients ClientBase", page: "features" },
    ],
  },

  {
    slug: "client-prendre-rdv-en-ligne",
    title: "Client : pourquoi prendre RDV en ligne change votre relation à votre pro",
    desc: "Côté client : pourquoi un compte ClientBase chez votre coiffeuse, esthéticienne, tatoueur, ostéo vous rend la vie tellement plus simple. Annulation en un clic, historique, fidélité.",
    keywords: ["prendre rdv en ligne", "compte client coiffeur", "annulation rdv", "fidélité salon"],
    date: "2026-05-01",
    audience: "client",
    category: "conseil",
    readMin: 4,
    body: [
      { type: "p", text: "Vous avez un coiffeur, une esthéticienne, un tatoueur ou un ostéopathe qui utilise ClientBase ? Voici comment vraiment tirer parti de votre compte client centralisé." },
      { type: "h2", text: "Un seul compte pour tous vos pros" },
      { type: "p", text: "Le principe central : un seul compte ClientBase, plusieurs pros. Votre coiffeuse, votre esthéticienne, votre ostéo — s'ils sont tous sur ClientBase, vous gérez tous vos RDV depuis le même espace. Plus besoin de retrouver dix mots de passe ou de jongler entre dix apps." },
      { type: "h2", text: "Annulation et report en un clic, sans culpabilité" },
      { type: "p", text: "Le téléphone qu'on n'a pas envie de passer pour annuler ? Fini. Vous ouvrez votre espace, vous cliquez « annuler » ou « reporter », votre pro est prévenue immédiatement. Pas de message gêné, pas de SMS qu'on rédige et qu'on supprime trois fois." },
      { type: "h2", text: "Votre historique vous suit" },
      { type: "p", text: "Vous changez de ville et trouvez une nouvelle esthéticienne qui utilise ClientBase ? Votre historique (allergies, sensibilités, préférences) reste avec vous. Vous n'avez plus à expliquer à chaque nouveau pro que vous êtes allergique aux parfums forts ou que vous préférez les RDV en fin d'après-midi." },
      { type: "h2", text: "Le compteur fidélité automatique" },
      { type: "p", text: "Chez les pros qui activent la fidélité, votre compteur de visites monte tout seul à chaque RDV honoré. Vous voyez en direct combien de visites avant la récompense (souvent une prestation offerte à la 10e visite). Pas de carte papier à perdre, pas de tampon oublié." },
      { type: "cta", label: "Créer mon compte client gratuit", page: "clientSignup" },
    ],
  },

  // === ARTICLES CLIENT-ORIENTÉS (audience: "client") ===
  {
    slug: "annuler-rdv-poliment-sans-culpabiliser",
    title: "Annuler un rendez-vous poliment : la méthode qui ne fait culpabiliser personne",
    desc: "Annuler chez le coiffeur ou l'esthéticienne sans craindre le froid au prochain RDV : ce qu'il faut dire, quand prévenir, et pourquoi un message court vaut mieux qu'aucune excuse.",
    keywords: ["annuler rdv coiffeur", "annuler rdv esthéticienne", "comment annuler poliment", "annulation rendez-vous"],
    date: "2026-05-15",
    audience: "client",
    category: "conseil",
    readMin: 4,
    body: [
      { type: "p", text: "Tout le monde a déjà été là : un imprévu, une grosse fatigue, un planning qui change. Le RDV qu'on a pris il y a 2 semaines ne tombe plus au bon moment. Et là, la petite voix : « si j'annule, est-ce qu'elle va m'en vouloir ? » Décryptage rapide d'une situation banale qu'on rend trop compliquée." },
      { type: "h2", text: "Annuler tôt, c'est respectueux. Annuler tard, ça pique." },
      { type: "p", text: "La règle officieuse dans le métier des prestations à domicile, des soins, du tatouage : 24 h à 48 h de préavis = aucun problème. Le créneau peut souvent être revendu. Moins de 24 h, ça devient compliqué — votre pro perd un revenu et n'a souvent pas le temps de remplacer." },
      { type: "list", items: [
        "Plus de 48 h : annulez tranquillement, c'est zéro souci.",
        "Entre 24 h et 48 h : annulez quand même, mais soyez bref·ve et proposez tout de suite un report.",
        "Moins de 24 h : sauf vraie urgence (maladie, accident), évitez. Si c'est inévitable, dites-le honnêtement.",
      ]},
      { type: "h2", text: "Le message qui marche" },
      { type: "p", text: "Pas besoin d'une excuse longue ou inventée. Trois lignes suffisent :" },
      { type: "quote", text: "« Bonjour Marie, je suis désolée mais je dois annuler mon RDV de jeudi 14 h. Est-ce que vous auriez un créneau la semaine prochaine ? Merci ! »" },
      { type: "p", text: "C'est court, c'est clair, ça propose une suite. Pas de tartine d'explications — votre pro n'a pas besoin de savoir POURQUOI vous annulez. Elle a besoin de savoir QUAND vous revenez." },
      { type: "h2", text: "Avec un compte ClientBase : encore plus simple" },
      { type: "p", text: "Si votre pro utilise ClientBase, vous pouvez annuler depuis votre espace en 2 clics. La pro reçoit la notification immédiatement, sans message à rédiger. C'est aussi l'occasion de reporter directement sur un créneau libre — vous gardez la main, elle ne perd pas son temps à chercher quand vous arrange." },
      { type: "cta", label: "Voir mon espace client", page: "clientLogin" },
    ],
  },

  {
    slug: "comment-trouver-un-bon-coiffeur-independant",
    title: "Trouver un bon coiffeur ou une bonne coiffeuse indépendant·e près de chez vous",
    desc: "Comment dénicher la perle rare en coiffure ou esthétique indépendante : les bons réflexes, les questions à poser au premier RDV, et où regarder.",
    keywords: ["trouver coiffeur indépendant", "bonne coiffeuse près de chez moi", "annuaire coiffeur", "esthéticienne à domicile"],
    date: "2026-05-13",
    audience: "client",
    category: "guide",
    readMin: 5,
    body: [
      { type: "p", text: "Trouver un·e indépendant·e qui colle vraiment à ce qu'on cherche, c'est rare. Et quand on tombe dessus, on a souvent une fidélité qui dure des années. Voici les bonnes pistes pour augmenter vos chances." },
      { type: "h2", text: "Instagram avant tout" },
      { type: "p", text: "La majorité des coiffeurs et esthéticiennes indépendants montrent leur travail sur Instagram. Cherchez « coiffeur + votre ville » ou « esthéticienne + arrondissement » dans la barre de recherche. Regardez la régularité des posts (≥1/semaine = quelqu'un d'investi), la cohérence du style, et surtout les commentaires des vraies clientes." },
      { type: "h2", text: "Le bouche-à-oreille, version 2026" },
      { type: "p", text: "Demandez sur votre groupe Facebook de quartier ou Nextdoor. « Quelqu'un connait une bonne esthéticienne discrète dans le 11e ? » Ce genre de poste reçoit toujours 5-10 réponses pertinentes en quelques heures. Privilégiez les recommandations multiples (la même personne citée par 2-3 voisines) — c'est le meilleur signal." },
      { type: "h2", text: "L'annuaire ClientBase" },
      { type: "p", text: "Sur ClientBase, tous les pros indépendants inscrits ont une page publique de réservation. Vous pouvez parcourir l'annuaire, voir leur activité, leur ville, leurs horaires, et réserver directement. Pas de marketplace cachée, pas de commission — vous traitez directement avec le pro." },
      { type: "h2", text: "Les 3 questions à poser au premier RDV" },
      { type: "list", items: [
        "« Vous travaillez avec quelles marques de produits ? » — révèle leur niveau et leur philosophie.",
        "« C'est votre cabine à vous, ou vous partagez ? » — donne le contexte (à domicile, partagé, indépendant pur).",
        "« Est-ce que vous prévoyez un suivi entre les RDV ? » — les vrais pros vous donnent des conseils d'entretien.",
      ]},
      { type: "cta", label: "Voir l'annuaire des pros ClientBase", page: "clientAnnuaire" },
    ],
  },

  {
    slug: "carte-fidelite-papier-vs-app",
    title: "Carte de fidélité papier vs application : pourquoi le digital change tout",
    desc: "Les cartes de fidélité papier qu'on perd, qu'on oublie, qu'on déchire. Pourquoi un compteur de visites digital chez chacun de vos pros est tellement plus simple.",
    keywords: ["carte fidélité salon", "fidélité coiffeur", "tampons carte fidélité", "compte client fidélité"],
    date: "2026-05-11",
    audience: "client",
    category: "conseil",
    readMin: 3,
    body: [
      { type: "p", text: "Combien de cartes de fidélité tamponnées avez-vous dans votre portefeuille ou tiroir ? Combien sont à moitié remplies, oubliées au fond d'un sac, déchirées par l'humidité ? La carte papier est un système qui marche à 70 % — les 30 % qui se perdent, c'est de l'argent que vous offrez à vos pros sans le vouloir." },
      { type: "h2", text: "Le coût caché de la carte papier" },
      { type: "p", text: "Imaginez : 10 visites pour 1 prestation offerte. Vous en faites 8, vous perdez la carte, vous recommencez à zéro. Vous venez de payer 2 prestations qui auraient dû être bonus. Sur une vie de cliente fidèle (10-15 ans chez la même esthéticienne), c'est facilement 200 à 400 € perdus." },
      { type: "h2", text: "Le compteur digital ClientBase" },
      { type: "p", text: "Chez les pros qui utilisent ClientBase, votre compteur de fidélité se met à jour tout seul à chaque RDV honoré. Vous voyez en direct dans votre espace : « 7 sur 10 visites · plus que 3 pour 1 prestation offerte ». Impossible de l'oublier, impossible de la perdre." },
      { type: "h2", text: "Bonus : vous voyez la progression chez TOUS vos pros" },
      { type: "p", text: "Si votre coiffeuse, votre esthéticienne et votre ostéo sont tous sur ClientBase, vous voyez les 3 cartes de fidélité au même endroit. Plus de surprise « ah, j'aurais pu avoir une réduction si j'avais su »." },
      { type: "cta", label: "Voir ma fidélité", page: "clientLogin" },
    ],
  },

  // === SEO PRO — facturation, fiscal, organisation ===
  {
    slug: "auto-entrepreneur-logiciel-facturation",
    title: "Auto-entrepreneur : faut-il vraiment un logiciel de facturation en 2026 ?",
    desc: "Avec la facture électronique obligatoire qui arrive, est-ce qu'un auto-entrepreneur a besoin d'un logiciel de facturation ? Réponse concrète selon votre activité.",
    keywords: ["logiciel facturation auto-entrepreneur", "facturation indépendant", "facture électronique 2026", "logiciel facture micro-entreprise"],
    date: "2026-05-20",
    audience: "pro",
    category: "guide",
    readMin: 6,
    body: [
      { type: "p", text: "La facture électronique devient obligatoire en France pour toutes les entreprises (y compris auto-entrepreneurs) entre 2026 et 2027 selon la taille. Beaucoup de solos se posent la question : un simple PDF Word suffit-il, ou faut-il un vrai logiciel ? Décryptage." },
      { type: "h2", text: "Ce que la loi exige vraiment" },
      { type: "p", text: "Toute facture émise en France doit contenir 13 mentions obligatoires (article L441-9 du Code de commerce + article 242 nonies A du CGI). Un PDF Word peut les contenir, mais à la main vous risquez d'en oublier, surtout sur des mentions piégeuses comme :" },
      { type: "list", items: [
        "Numéro de facture séquentiel sans rupture (interdit de sauter ou recommencer à 1).",
        "Mention « TVA non applicable, art. 293 B du CGI » pour les auto-ent en franchise.",
        "Date d'exécution de la prestation (≠ date d'émission).",
        "Conditions de paiement + taux de pénalités de retard.",
        "SIRET (pas le SIREN) sur toutes les factures.",
      ]},
      { type: "h2", text: "Le PDF Word ou Excel : OK, mais à risque" },
      { type: "p", text: "Techniquement légal tant que vous tenez un registre des factures, mais en pratique : oubli de numéros, fautes dans les mentions, copies-colles qui dupliquent un numéro. Une facture non conforme = client qui peut refuser de payer + URSSAF qui peut requalifier en cas de contrôle." },
      { type: "h2", text: "La facture électronique obligatoire : ce que ça change" },
      { type: "p", text: "À partir de septembre 2026 (réception) et au plus tard septembre 2027 (émission pour les TPE), les factures B2B devront passer par une plateforme certifiée (PPF) ou une plateforme partenaire (PDP). Word/Excel = mort. Vous devrez utiliser un outil qui sait générer du Factur-X (PDF + XML) conforme." },
      { type: "h2", text: "Pour qui un logiciel est vraiment utile" },
      { type: "list", items: [
        "Plus de 5 factures/mois : le gain de temps amortit tout.",
        "Vous avez un site/Instagram avec vos prix : factures à générer rapidement.",
        "Vous avez des clients récurrents : suivi des paiements/relances.",
        "Vous voulez vous projeter sur la facture électronique sans tout refaire en 2027.",
      ]},
      { type: "h2", text: "Ce qu'il faut regarder" },
      { type: "p", text: "Un bon logiciel pour auto-entrepreneur doit : générer des factures conformes en 3 clics, numéroter automatiquement, gérer la franchise TVA (et la sortie de franchise quand le seuil approche), exporter en CSV pour la compta, et idéalement déjà préparer la facture électronique pour 2027." },
      { type: "cta", label: "Voir le module facturation ClientBase", page: "features" },
    ],
  },

  {
    slug: "alternative-calendly-services",
    title: "ClientBase vs Calendly : lequel choisir quand on est dans les services à la personne ?",
    desc: "Calendly excelle pour les RDV de coaching B2B et les call meetings. Pour les métiers de service (coiffure, esthétique, massage), un outil métier change la donne.",
    keywords: ["alternative Calendly", "Calendly pour coiffeur", "Calendly esthéticienne", "agenda en ligne service à la personne"],
    date: "2026-05-18",
    audience: "pro",
    category: "comparatif",
    readMin: 5,
    body: [
      { type: "p", text: "Calendly est un excellent outil — pour les RDV où la prestation est interchangeable (un call Zoom de 30 min reste un call de 30 min). Mais quand votre prestation a une durée variable, un prix variable, un historique client riche, vous touchez les limites en quelques semaines." },
      { type: "h2", text: "Ce que Calendly fait bien" },
      { type: "list", items: [
        "Lien public propre à partager, on ne discutera pas.",
        "Intégration calendrier Google / Outlook nickel.",
        "Notification email automatique.",
        "Free tier généreux pour démarrer.",
      ]},
      { type: "h2", text: "Ce qui manque vite quand vous êtes coiffeur, esthéticienne, masseur" },
      { type: "list", items: [
        "Pas de fiche client riche : aucune mémoire de la cliente entre 2 RDV (formule de couleur, allergies, préférences).",
        "Pas de facturation conforme française : ni SIRET, ni mentions légales, ni TVA franchise — vous devez sortir un autre outil pour facturer.",
        "Pas de programme de fidélité : votre rétention dépend de vous, sans automatisation.",
        "Pas de gestion des acomptes : impossible de demander 30 % à la réservation pour réduire les no-show.",
        "Pas d'historique des RDV passés par cliente : vous ne pouvez pas dire « la dernière fois c'était il y a 6 semaines ».",
      ]},
      { type: "h2", text: "Le vrai sujet : un agenda ne suffit pas" },
      { type: "p", text: "Quand vous êtes dans le service, votre outil n'est pas qu'un agenda. C'est une mémoire de votre activité. Calendly stocke des slots ; ClientBase (et d'autres outils métiers) stockent une RELATION cliente. C'est ça qui fait revenir une personne 3 ans de suite." },
      { type: "quote", text: "« Mon agenda Calendly fonctionnait. Mais quand une cliente me redemandait sa dernière couleur, je devais aller chercher dans mes notes Notion. Avec ClientBase la fiche s'ouvre en 1 clic. »" },
      { type: "h2", text: "Quand Calendly est encore le bon choix" },
      { type: "list", items: [
        "Vous faites uniquement du coaching/consulting B2B avec des slots interchangeables.",
        "Vos clients ne reviennent pas (one-shot).",
        "Vous n'avez pas besoin de facturer en France conformément (ex : prestations à l'export, clients hors UE).",
      ]},
      { type: "p", text: "Pour tous les métiers où la cliente revient, où le suivi compte, où la facturation française est obligatoire, un outil métier vous fait gagner des heures chaque semaine ET protège votre activité." },
      { type: "cta", label: "Essayer ClientBase gratuitement", page: "signup" },
    ],
  },

  {
    slug: "acompte-rendez-vous-guide-complet",
    title: "Acompte à la réservation : le guide complet pour indépendant·e (combien, comment, quel cadre légal)",
    desc: "Tout ce qu'il faut savoir pour mettre en place un acompte à la réservation : montant juste, formulation qui ne braque pas, cadre légal en France, conservation en cas d'annulation.",
    keywords: ["acompte rendez-vous", "acompte prestation", "acompte coiffeur", "acompte esthéticienne", "no-show acompte légal"],
    date: "2026-05-16",
    audience: "pro",
    category: "problemes",
    readMin: 7,
    body: [
      { type: "p", text: "Un acompte à la réservation est l'outil le plus efficace pour réduire les no-show, mais c'est aussi le plus délicat à introduire. Mal présenté, vos clientes le vivent comme un manque de confiance. Bien présenté, elles l'acceptent sans broncher. Guide pratique." },
      { type: "h2", text: "Le bon montant : 30 % de la prestation" },
      { type: "p", text: "30 % est le sweet spot reconnu dans le métier : assez pour que la cliente y pense (et se déplace), pas assez pour la dissuader de réserver. Sous les 15 €, ça vaut la peine de l'absorber dans le prix plutôt que de le demander." },
      { type: "list", items: [
        "Soin visage 60 € → acompte 18 €",
        "Coupe + couleur 80 € → acompte 25 €",
        "Tatouage custom 800 € → acompte 240 €",
        "Massage 1h 70 € → acompte 20 €",
      ]},
      { type: "h2", text: "Le cadre légal en France" },
      { type: "p", text: "L'acompte est régi par l'article 1590 du Code civil. À distinguer absolument des arrhes :" },
      { type: "list", items: [
        "Acompte : engagement ferme. La cliente DOIT vous payer le solde même si elle annule (sauf cas de force majeure documenté).",
        "Arrhes : la cliente PEUT annuler, en perdant les arrhes. Vous, vous pouvez annuler en restituant le double.",
      ]},
      { type: "p", text: "Pour la prise de RDV, l'acompte est généralement plus protecteur. Précisez-le dans vos CGU sur votre page de réservation publique : « Le versement vaut acompte et non arrhes. »" },
      { type: "h2", text: "Comment l'annoncer sans braquer" },
      { type: "p", text: "L'erreur classique : justifier l'acompte par les no-show passés. Ça met la cliente dans la case « tricheuse présumée ». Préférez :" },
      { type: "quote", text: "« Pour réserver votre créneau, un acompte de 30 % vous est demandé. Il est déduit du montant total le jour du RDV. Ça permet de bloquer votre place et de gérer les annulations équitablement. »" },
      { type: "p", text: "Le mot clé : « bloquer votre place ». La cliente entend « ma place m'appartient » au lieu de « je suis présumée tricheuse »." },
      { type: "h2", text: "Annulation : quoi faire" },
      { type: "list", items: [
        "Annulation >48 h avant : remboursement intégral (politique commerciale, optionnelle).",
        "Annulation 24-48 h avant : acompte conservé à 50 % (compromis).",
        "Annulation <24 h ou no-show : acompte conservé à 100 %, c'est l'engagement.",
        "Cliente qui demande à reporter : le créneau est libéré, l'acompte est transféré sur le nouveau RDV. Garde-la !",
      ]},
      { type: "h2", text: "Mise en place avec ClientBase" },
      { type: "p", text: "Le module Acomptes de ClientBase gère tout : configuration du % par service, paiement Stripe à la réservation, transfert automatique sur votre compte, ligne de comptabilité prête. Pas besoin d'intégrer Stripe vous-même, c'est inclus." },
      { type: "cta", label: "Activer les acomptes sur mon compte", page: "features" },
    ],
  },

  {
    slug: "rgpd-indépendants-ce-quil-faut-savoir",
    title: "RGPD pour indépendants : les 5 obligations à connaître (et celles qu'on vous fait croire à tort)",
    desc: "Vous traitez des données clientes (numéro, email, historique) ? Vous êtes concerné par le RGPD. Mais beaucoup d'obligations qu'on vous vend sont inutiles. Tri.",
    keywords: ["RGPD indépendant", "RGPD coiffeur", "données clients RGPD", "obligations RGPD micro-entreprise"],
    date: "2026-05-14",
    audience: "pro",
    category: "guide",
    readMin: 6,
    body: [
      { type: "p", text: "Le RGPD (Règlement Général sur la Protection des Données) s'applique dès que vous stockez des données personnelles de clients. Pour un coiffeur, esthéticienne, tatoueur, naturopathe : numéro, email, historique = données perso. Vous êtes responsable du traitement. Mais beaucoup d'obligations qu'on vous fait croire sont surdimensionnées pour un solo." },
      { type: "h2", text: "Ce que la loi exige VRAIMENT" },
      { type: "list", items: [
        "Tenir un registre des traitements (simple Word ou Excel, pas besoin d'outil dédié).",
        "Informer vos clients de ce que vous collectez et pourquoi (mention sur vos CGU ou page contact).",
        "Permettre à chaque client de demander à voir / modifier / supprimer ses données (article 15 et 17).",
        "Sécuriser le stockage (pas de fichier Excel sur Dropbox public).",
        "Notifier la CNIL en cas de fuite de données dans les 72 h (rare).",
      ]},
      { type: "h2", text: "Ce que la loi N'exige PAS (et qu'on vous vend quand même)" },
      { type: "list", items: [
        "Un DPO (Délégué à la Protection des Données) — réservé aux entreprises de >250 employés ou aux activités à risque (santé sensible, profilage massif).",
        "Une certification RGPD payante — la CNIL ne reconnaît AUCUN label privé. Toute « certification RGPD » à 500 € est du vent.",
        "Un audit RGPD annuel par un consultant — totalement facultatif.",
        "Un bandeau cookies obligatoire — uniquement si votre site utilise des cookies non-essentiels (analytics, pub).",
      ]},
      { type: "h2", text: "Hébergement en France ou UE" },
      { type: "p", text: "Le RGPD n'interdit PAS d'utiliser un outil hébergé hors UE, à condition que le pays soit considéré comme adéquat OU que des garanties contractuelles existent (clauses types de la Commission européenne). Mais en pratique : pour de la sérénité, choisir un outil hébergé en France (ou UE) évite les questions." },
      { type: "h2", text: "Le risque réel" },
      { type: "p", text: "La CNIL a sanctionné peu d'indépendants à ce jour. Le risque concret pour vous est plutôt : un client mécontent qui demande l'effacement de ses données, vous ne savez pas le faire → plainte CNIL → amende administrative jusqu'à 4 % du CA annuel. Un outil qui gère le droit à l'effacement en 2 clics règle ça." },
      { type: "h2", text: "ClientBase et le RGPD" },
      { type: "list", items: [
        "Hébergement OVH France, données isolées par compte (Row-Level Security PostgreSQL).",
        "Export RGPD téléchargeable par chaque cliente depuis son espace.",
        "Effacement compte client en 2 clics depuis l'espace pro.",
        "Mentions légales et CGU pré-rédigées, à personnaliser.",
      ]},
      { type: "cta", label: "Voir nos engagements RGPD", page: "legal" },
    ],
  },

  {
    slug: "migrer-de-planity-en-30-minutes",
    title: "Comment migrer de Planity vers un outil sans commission en 30 minutes",
    desc: "Quitter Planity ou Treatwell sans perdre vos clientes : la méthode pas-à-pas pour exporter votre base, communiquer le changement, et basculer en moins d'1 heure.",
    keywords: ["quitter Planity", "migrer Planity", "export client Planity", "résilier Planity", "alternative Planity sans commission"],
    date: "2026-05-13",
    audience: "pro",
    category: "guide",
    readMin: 6,
    body: [
      { type: "p", text: "Vous payez Planity, Treatwell ou Iara depuis des années et vous avez calculé : entre l'abonnement et les commissions, ça vous coûte 150-300 € par mois. Vous voulez quitter, mais vous avez peur de perdre vos clientes. Méthode pratique en 5 étapes pour basculer en moins d'1 heure." },
      { type: "h2", text: "Étape 1 — Exporter votre base clientes" },
      { type: "p", text: "Planity et Treatwell ont rendu l'export client difficile, mais pas impossible. Demandez par email écrit (article 20 RGPD - droit à la portabilité) l'export complet de votre fichier client au format CSV. Légalement, ils ont 30 jours pour vous le fournir. En pratique, vous l'avez sous 48-72 h si vous insistez." },
      { type: "list", items: [
        "Email type : « Conformément à l'article 20 du RGPD, je vous demande de me communiquer l'intégralité de ma base de données clients dans un format structuré, couramment utilisé et lisible (CSV de préférence). Délai de 30 jours. »",
      ]},
      { type: "h2", text: "Étape 2 — Choisir et configurer votre nouvel outil" },
      { type: "p", text: "Comptez 20 minutes pour créer votre compte ClientBase, importer le CSV de votre base, configurer vos prestations + horaires, et activer votre page publique de RDV. Le tuto pas-à-pas est dans le module Aide de l'app." },
      { type: "h2", text: "Étape 3 — Tester le lien public sur soi-même" },
      { type: "p", text: "Avant d'annoncer le changement, testez votre nouveau lien de RDV depuis votre téléphone. Réservez un faux créneau, voyez le flow client. Si quelque chose accroche (durée mal réglée, jour férié pas bloqué), corrigez avant la communication." },
      { type: "h2", text: "Étape 4 — Communiquer le changement" },
      { type: "p", text: "Le message à envoyer (par SMS, Instagram Story, ou email selon votre canal habituel) :" },
      { type: "quote", text: "« Petite nouveauté : j'ai changé de système de réservation. Vous prenez maintenant RDV directement chez moi, sans intermédiaire ni commission. Le lien : [votre-lien]. Vos données et historique sont conservés ✨. Merci de noter ce nouveau lien dans vos favoris ! »" },
      { type: "p", text: "Évitez de critiquer Planity dans le message — ça met les clientes mal à l'aise. Focalisez sur le « plus simple, plus direct »." },
      { type: "h2", text: "Étape 5 — Résilier l'abonnement Planity" },
      { type: "p", text: "Attendez 30 jours après la communication pour résilier (le temps que vos clientes habituelles aient basculé). Planity a une procédure de résiliation par email à customer@planity.com avec préavis variable selon votre contrat. Demandez confirmation écrite de la date d'arrêt de facturation." },
      { type: "h2", text: "Ce qu'il faut accepter" },
      { type: "p", text: "Environ 10-15 % de vos clientes Planity ne suivront pas. C'est OK. Ce sont souvent des one-shot qui prenaient via la marketplace plus que chez vous. Vos vraies fidèles (80-90 %) bascullent en 2-3 RDV. Vos nouveaux clients arriveront via Instagram, bouche-à-oreille, Google Maps — pas via Planity." },
      { type: "cta", label: "Créer mon compte ClientBase", page: "signup" },
    ],
  },

  {
    slug: "se-faire-connaitre-sans-publicite",
    title: "Indépendant·e : 7 façons de se faire connaître sans payer un euro de publicité",
    desc: "Pas de budget pub ? Pas grave. Voici les 7 canaux qui marchent vraiment pour les indépendants en France : Instagram, Google My Business, bouche-à-oreille, fidélité…",
    keywords: ["se faire connaître indépendant", "trouver des clients indépendant", "communication indépendant", "Instagram pour pro"],
    date: "2026-05-11",
    audience: "pro",
    category: "metier",
    readMin: 7,
    body: [
      { type: "p", text: "Beaucoup d'indépendants pensent que pour démarrer, il faut budgéter 200 € de pub Facebook par mois. C'est faux. Les canaux qui marchent en 2026 pour les solos sont gratuits — il faut juste y mettre du temps et de la régularité." },
      { type: "h2", text: "1. Instagram (le plus puissant pour les métiers visuels)" },
      { type: "p", text: "Coiffure, esthétique, tatouage, ongles, maquillage : votre travail SE VOIT. Postez systématiquement avant/après, vidéos courtes (15-30s) du process, stories quotidiennes. Lien de RDV dans la bio. Engagez avec vos commentaires sous 24 h. 70 % des nouveaux clients d'indépendants visuels viennent d'Instagram." },
      { type: "h2", text: "2. Google My Business (le canal sous-estimé)" },
      { type: "p", text: "Quand quelqu'un tape « coiffeur près de chez moi » sur Google, il voit en premier la carte Maps. Si vous y êtes avec 20+ avis 5★, vous êtes choisi avant les autres. Créer la fiche prend 10 minutes. Demandez à chaque cliente satisfaite un avis Google — la majorité le fera si vous demandez." },
      { type: "h2", text: "3. Le bouche-à-oreille structuré" },
      { type: "p", text: "Le bouche-à-oreille « naturel » est lent. Pour l'accélérer : programme de parrainage simple. « Si vous me ramenez une amie, vous avez -10 € sur votre prochain RDV. » Communiquez-le. 30 % de vos nouvelles clientes peuvent venir de là si vous le formalisez." },
      { type: "h2", text: "4. Les groupes Facebook locaux" },
      { type: "p", text: "Les groupes « Mamans du 11e », « Voisins de Bordeaux Caudéran » sont des mines d'or. Pas de pub directe (interdit dans 80 % des groupes) — mais répondez aux questions « Vous connaissez une bonne esthéticienne ? » avec votre nom. Régularité + générosité = 5-10 clientes/mois." },
      { type: "h2", text: "5. Les partenariats locaux" },
      { type: "p", text: "Vous êtes coiffeuse ? Posez vos cartes chez la naturopathe d'à côté. Elle pose les siennes chez vous. Vous touchez chacune la clientèle de l'autre, sans frais. Le réseau d'indépendants locaux est sous-exploité." },
      { type: "h2", text: "6. Votre fichier client existant (le plus rentable)" },
      { type: "p", text: "Vos vraies clientes vous voient comme une amie. Activez-les : SMS de relance à celles qu'on n'a pas vues depuis 6 mois, programme de fidélité visible (vous voyez où vous en êtes), offre anniversaire automatique. Avec ClientBase, ces 3 automatismes prennent 0 minute de votre temps." },
      { type: "h2", text: "7. Le contenu utile (blog, YouTube, podcast)" },
      { type: "p", text: "Si vous êtes naturopathe, coach, sophrologue, votre expertise est votre marketing. 1 article blog par mois sur un problème de votre clientèle (« comment mieux dormir en 3 nuits », « routine matinale anti-fatigue ») = du trafic Google qui dure des années. C'est plus lent que la pub, mais ça scale." },
      { type: "h2", text: "Ce qu'il faut éviter" },
      { type: "list", items: [
        "La pub Facebook/Instagram payante quand on démarre (ROI catastrophique sans expertise).",
        "Les « apps » qui promettent des clients (toutes des marketplaces avec commission).",
        "Les flyers papier distribués dans la rue (taux de conversion <0.1 %).",
        "Acheter des followers Instagram (zéro impact sur les RDV pris).",
      ]},
      { type: "cta", label: "Configurer mon programme de fidélité", page: "features" },
    ],
  },

  // === SEO CLIENT — pourquoi l'acompte ===
  {
    slug: "pourquoi-votre-pro-demande-acompte",
    title: "Pourquoi votre coiffeur, esthéticienne, tatoueur vous demande un acompte (et pourquoi c'est OK)",
    desc: "Vous trouvez ça louche qu'on vous demande un acompte pour réserver chez une indépendante ? Pas du tout. Voici pourquoi cette pratique se généralise et protège tout le monde.",
    keywords: ["acompte rendez-vous obligatoire", "acompte coiffeur normal", "pourquoi acompte esthéticienne", "acompte réservation client"],
    date: "2026-05-09",
    audience: "client",
    category: "conseil",
    readMin: 4,
    body: [
      { type: "p", text: "Vous prenez RDV en ligne chez une coiffeuse ou une esthéticienne et au moment de valider, on vous demande 30 % d'acompte. Réaction normale : « Mais pourquoi ? Elle ne me fait pas confiance ? » Spoiler : c'est l'inverse, et voici pourquoi." },
      { type: "h2", text: "Le no-show, le vrai ennemi des indépendantes" },
      { type: "p", text: "Une coiffeuse à son compte qui a un trou de 2 h dans son agenda parce qu'une cliente ne s'est pas présentée perd 80-150 € qu'elle ne récupèrera jamais. Sur 4 no-show par mois, c'est presque 1 semaine de vacances en moins par an. Sur 10, c'est son chiffre d'affaires qui s'écroule." },
      { type: "h2", text: "L'acompte n'est pas une punition, c'est un engagement mutuel" },
      { type: "p", text: "Quand vous versez 30 % à la réservation, deux choses se passent :" },
      { type: "list", items: [
        "Vous, vous y pensez vraiment. Plus de « ah merde, j'avais oublié » la veille du RDV.",
        "Elle, elle bloque sa journée pour vous, sans craindre un trou. Elle peut refuser une autre cliente sur ce créneau sans risque.",
      ]},
      { type: "p", text: "L'acompte transforme une promesse vague en engagement réciproque. Vous êtes traitée comme une cliente sérieuse, parce que vous l'êtes." },
      { type: "h2", text: "Et si je dois annuler pour une vraie raison ?" },
      { type: "p", text: "Toutes les pros sérieuses ont une politique d'annulation raisonnable :" },
      { type: "list", items: [
        "Annulation >48 h avant → acompte remboursé intégralement, généralement.",
        "Annulation 24-48 h avant → acompte conservé partiellement (souvent 50 %).",
        "Annulation <24 h ou no-show → acompte conservé à 100 %, parce qu'elle ne peut plus revendre le créneau.",
        "Vous voulez reporter ? Demandez. L'acompte se reporte sur le nouveau RDV. Pas d'argent perdu.",
      ]},
      { type: "p", text: "En cas de force majeure (urgence médicale, deuil), prévenez par message — la majorité des pros sont humaines et géreront sans souci." },
      { type: "h2", text: "Comment savoir si c'est un acompte « propre »" },
      { type: "list", items: [
        "Le montant est raisonnable : 20-30 % du prix de la prestation, pas 100 %.",
        "La politique d'annulation est claire et écrite (souvent dans les CGU sur sa page de RDV).",
        "Le paiement passe par un système sécurisé (Stripe, PayPal) — pas un virement direct à un IBAN inconnu.",
        "Sur la facture finale, l'acompte apparaît bien en déduction du total payé.",
      ]},
      { type: "p", text: "Bref : un acompte de 30 % par carte chez une pro sérieuse n'est pas « louche », c'est juste une pratique professionnelle. Et c'est même ce qui permet à votre coiffeuse préférée de continuer son activité sans avoir à augmenter ses prix pour compenser les no-show." },
      { type: "cta", label: "Voir mes RDV à venir", page: "clientLogin" },
    ],
  },
];

if (typeof window !== "undefined") {
  window.BLOG_ARTICLES = ARTICLES;
}

// =========================================================================
// COMPOSANTS
// =========================================================================

const BLOG_CATEGORY_ORDER = ["guide", "comparatif", "metier", "problemes", "conseil"];

const _fmtDate = (iso) => {
  try {
    const d = new Date(iso + "T00:00:00");
    const months = ["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"];
    return `${d.getDate()} ${months[d.getMonth()]} ${d.getFullYear()}`;
  } catch { return iso; }
};

// BlogPage est partagée entre pro et client. La prop `audience` détermine :
//   - le filtre des articles affichés (pro/all OU client)
//   - le slug de base de l'URL (/blog OU /blog-client)
//   - les couleurs (accent indigo OU cli-accent)
//   - le titre/sous-titre du header
const BlogPage = ({ go, slug, onSlugChange, audience = "pro" }) => {
  const isClient = audience === "client";
  const basePath = isClient ? "/blog-client" : "/blog";
  // Filtre des articles : côté pro on montre les pro + tous les neutres ;
  // côté client on montre uniquement les articles tagués audience === "client".
  const filteredArticles = React.useMemo(() => (
    isClient
      ? ARTICLES.filter(a => a.audience === "client")
      : ARTICLES.filter(a => a.audience !== "client")
  ), [isClient]);

  // Navigation interne entre index et article : on push l'URL, on met à jour
  // le slug via le callback parent, et on scrolle en haut. Pas de full reload.
  const navigateBlog = React.useCallback((nextSlug) => {
    const path = nextSlug ? `${basePath}/${nextSlug}` : basePath;
    try {
      if (window.location.pathname !== path) {
        window.history.pushState({ page: isClient ? "clientBlog" : "blog" }, "", path);
      }
    } catch {}
    if (onSlugChange) onSlugChange(nextSlug || null);
    try { window.scrollTo({ top: 0, behavior: "instant" }); } catch {}
  }, [onSlugChange, basePath, isClient]);

  // Met à jour la balise <title> et la meta description pour la SEO de chaque
  // article (le routeur ne sait pas distinguer /blog/<slug> à ce niveau).
  React.useEffect(() => {
    const article = slug ? ARTICLES.find(a => a.slug === slug) : null;
    if (!article) return;
    try {
      document.title = `${article.title} · Blog ClientBase`;
      const desc = document.querySelector('meta[name="description"]');
      if (desc) desc.setAttribute("content", article.desc);
      const og = document.querySelector('meta[property="og:title"]');
      if (og) og.setAttribute("content", article.title);
      const ogd = document.querySelector('meta[property="og:description"]');
      if (ogd) ogd.setAttribute("content", article.desc);
      const canonical = document.querySelector('link[rel="canonical"]');
      if (canonical) canonical.setAttribute("href", `https://clientbase.fr${basePath}/${slug}`);
    } catch {}
  }, [slug, basePath]);

  // Article view
  if (slug) {
    const article = ARTICLES.find(a => a.slug === slug);
    if (!article) {
      return <BlogNotFound onBack={() => navigateBlog(null)} audience={audience}/>;
    }
    return <BlogArticle article={article} go={go} onBack={() => navigateBlog(null)} onArticle={navigateBlog} audience={audience}/>;
  }

  // Index view
  return <BlogIndex articles={filteredArticles} go={go} onOpen={navigateBlog} audience={audience}/>;
};

// =========================================================================
// INDEX BENTO MAGAZINE
// =========================================================================
// Layout éditorial inspiré des magazines en ligne :
//   1. Le plus récent prend la place HÉRO (full-width, gros visuel gradient
//      + titre 36px + description + meta).
//   2. Les 2 suivants en cartes MEDIUM (50/50 sur desktop, empilées mobile),
//      visuel gradient + titre 22px.
//   3. Le reste en cartes SMALL (grid 3 cols desktop, 1 col mobile),
//      sans visuel, plus dense.
// Chaque carte a un visuel SVG décoratif lié à sa catégorie + un dégradé
// audience-color. Au hover : lift + glow + grossissement visuel.
//
// Bar de catégories en haut pour filtrer (state local), pill active mise
// en valeur dans la tone de la cat.
// =========================================================================

const BlogIndex = ({ articles, go, onOpen, audience = "pro" }) => {
  const isMobile = useIsMobile(720);
  const isClient = audience === "client";
  const [catFilter, setCatFilter] = React.useState("all");
  const [search, setSearch] = React.useState("");

  // Tri par date desc + filtre catégorie + filtre search
  const sorted = React.useMemo(() => {
    const q = search.trim().toLowerCase();
    return [...articles]
      .sort((a, b) => (b.date || "").localeCompare(a.date || ""))
      .filter(a => catFilter === "all" || a.category === catFilter)
      .filter(a => {
        if (!q) return true;
        return (
          a.title.toLowerCase().includes(q)
          || a.desc.toLowerCase().includes(q)
          || (a.keywords || []).some(k => k.toLowerCase().includes(q))
        );
      });
  }, [articles, catFilter, search]);

  // Comptage par cat pour les filtres (badge nombre)
  const catCounts = React.useMemo(() => {
    const c = {};
    for (const a of articles) c[a.category] = (c[a.category] || 0) + 1;
    return c;
  }, [articles]);

  const audienceAccent = isClient ? "var(--cli-accent)" : "var(--accent)";
  const audienceSoft   = isClient ? "var(--cli-soft)"   : "var(--accent-soft)";

  const title = isClient
    ? "Le blog des clients ClientBase."
    : "Conseils, guides, retours d'expérience.";
  const subtitle = isClient
    ? "Astuces pour mieux gérer vos rendez-vous chez vos pros, profiter de la fidélité, et tirer le meilleur parti d'un compte client centralisé."
    : "Tout ce qu'on a appris en construisant un outil pour indépendants : gestion de rendez-vous, alternatives aux marketplaces, astuces par métier, problèmes concrets et solutions qui marchent.";

  return (
    <>
      <PageHeader
        eyebrow={isClient ? "Blog · côté client" : "Blog ClientBase"}
        title={title}
        subtitle={subtitle}
      />

      <section style={{ padding: "8px 0 80px" }}>
        <div className="container">
          {/* === Toolbar : sur DESKTOP, pills horizontales + search à
              droite (comme avant). Sur MOBILE, search en haut + grille
              de boutons catégorie 2 colonnes en dessous (tout visible
              d'un coup, pas de scroll, gros touch targets). === */}
          <div style={{
            display: "flex",
            flexDirection: isMobile ? "column" : "row",
            alignItems: isMobile ? "stretch" : "center",
            gap: 12,
            marginBottom: 24, padding: "12px 14px",
            background: "var(--surface)", border: "1px solid var(--line)",
            borderRadius: 14,
          }}>
            {isMobile ? (
              <>
                <input type="search" value={search}
                  onChange={e => setSearch(e.target.value)}
                  placeholder="🔍 Rechercher un article…"
                  style={{
                    width: "100%", padding: "11px 14px",
                    background: "var(--bg)", border: "1px solid var(--line)",
                    borderRadius: 10, fontSize: 14, fontFamily: "inherit",
                    color: "var(--ink)", outline: "none",
                  }}/>
                {/* Grille compacte de tiles cat (sobre, neutre, seul le
                    texte change de couleur). « Tous » en haut full-width. */}
                <div style={{ display: "grid", gridTemplateColumns: "1fr", gap: 6 }}>
                  <BlogCatTile
                    active={catFilter === "all"}
                    onClick={() => setCatFilter("all")}
                    label="Tous"
                    count={articles.length}
                    tone={audienceAccent}
                    full
                  />
                  <div style={{
                    display: "grid",
                    gridTemplateColumns: "1fr 1fr",
                    gap: 6,
                  }}>
                    {BLOG_CATEGORY_ORDER.filter(c => catCounts[c]).map(catId => {
                      const cat = BLOG_CATEGORIES[catId];
                      return (
                        <BlogCatTile key={catId}
                          active={catFilter === catId}
                          onClick={() => setCatFilter(catId)}
                          label={cat.label}
                          count={catCounts[catId]}
                          tone={cat.tone}
                          ink={cat.ink}
                        />
                      );
                    })}
                  </div>
                </div>
              </>
            ) : (
              <>
                <div style={{ display: "flex", flexWrap: "wrap", gap: 6, flex: 1, minWidth: 0 }}>
                  <BlogCatPill
                    active={catFilter === "all"}
                    onClick={() => setCatFilter("all")}
                    label="Tous"
                    count={articles.length}
                    tone={audienceAccent}
                  />
                  {BLOG_CATEGORY_ORDER.filter(c => catCounts[c]).map(catId => {
                    const cat = BLOG_CATEGORIES[catId];
                    return (
                      <BlogCatPill key={catId}
                        active={catFilter === catId}
                        onClick={() => setCatFilter(catId)}
                        label={cat.label}
                        count={catCounts[catId]}
                        tone={cat.tone}
                        ink={cat.ink}
                      />
                    );
                  })}
                </div>
                <input type="search" value={search}
                  onChange={e => setSearch(e.target.value)}
                  placeholder="🔍 Rechercher…"
                  style={{
                    width: 200, padding: "8px 12px",
                    background: "var(--bg)", border: "1px solid var(--line)",
                    borderRadius: 9, fontSize: 13.5, fontFamily: "inherit",
                    color: "var(--ink)", outline: "none",
                  }}/>
              </>
            )}
          </div>

          {/* === Grille uniforme : toutes les cards même format. Le 1er
              article (le plus récent) reçoit un petit badge « ★ Nouveau »
              pour le distinguer sans casser l'alignement. === */}
          {sorted.length === 0 ? (
            <div style={{
              padding: "48px 24px", textAlign: "center",
              background: "var(--bg-alt)", border: "1px dashed var(--line-strong)",
              borderRadius: 14,
            }}>
              <div style={{ fontSize: 40, marginBottom: 8 }}>🔍</div>
              <div style={{ fontSize: 14.5, color: "var(--ink-2)", fontWeight: 540 }}>
                Aucun article pour ces critères.
              </div>
              <button onClick={() => { setCatFilter("all"); setSearch(""); }}
                style={{
                  marginTop: 14, padding: "8px 14px",
                  background: "var(--surface)", border: "1px solid var(--line)",
                  borderRadius: 999, fontSize: 13, fontFamily: "inherit",
                  color: "var(--ink-2)", cursor: "pointer",
                }}>
                Réinitialiser les filtres
              </button>
            </div>
          ) : (
            <div style={{
              display: "grid",
              gridTemplateColumns: isMobile ? "1fr" : "repeat(auto-fill, minmax(300px, 1fr))",
              gap: 16,
            }}>
              {sorted.map((a, i) => (
                <BlogCard key={a.slug}
                  article={a}
                  onOpen={() => onOpen(a.slug)}
                  audience={audience}
                  isLatest={i === 0 && catFilter === "all" && !search}
                />
              ))}
            </div>
          )}

          {/* === CTA bas de page : « Proposer un sujet » === */}
          <div style={{
            marginTop: 56, padding: "28px 26px",
            background: `linear-gradient(135deg, ${audienceSoft} 0%, var(--surface) 70%)`,
            border: `1px solid ${isClient ? "var(--cli-soft-2)" : "var(--accent-soft-2)"}`,
            borderRadius: 18,
            display: "flex", alignItems: "center", gap: 16, flexWrap: "wrap",
          }}>
            <span aria-hidden style={{ fontSize: 40, flexShrink: 0 }}>✉️</span>
            <div style={{ flex: 1, minWidth: 220 }}>
              <h3 style={{
                margin: 0, fontFamily: "var(--ff-display)", fontSize: 20, fontWeight: 600,
                letterSpacing: "-0.022em",
              }}>
                Un sujet à traiter ?
              </h3>
              <p style={{
                margin: "4px 0 0",
                fontSize: 14, color: "var(--ink-3)", lineHeight: 1.5,
              }}>
                On écrit le blog selon les vraies questions {isClient ? "des clients" : "des pros"}. Proposez un sujet → on s'en occupe.
              </p>
            </div>
            <button onClick={() => go(isClient ? "clientContact" : "contact")}
              className="btn btn-lg"
              style={{
                background: audienceAccent, color: "#fff", border: "none",
                flexShrink: 0,
              }}>
              Proposer
            </button>
          </div>
        </div>
      </section>
    </>
  );
};

// Tile compacte pour la sélection catégorie sur MOBILE. Design sobre :
// juste le label centré, hauteur 40px, fond uniforme audience-neutre
// (var(--bg-alt)) → seule la couleur du TEXTE change quand actif (passe
// dans le ink de la cat). Le `full` (Tous) reste pleine largeur pour
// servir de CTA.
const BlogCatTile = ({ active, onClick, label, count, tone, ink, full = false }) => {
  const [press, setPress] = React.useState(false);
  const activeColor = ink || tone;
  return (
    <button onClick={onClick}
      onTouchStart={() => setPress(true)}
      onTouchEnd={() => setPress(false)}
      onMouseDown={() => setPress(true)}
      onMouseUp={() => setPress(false)}
      onMouseLeave={() => setPress(false)}
      style={{
        display: "flex", alignItems: "center", justifyContent: "center",
        gap: 6,
        padding: "10px 12px",
        minHeight: 40,
        // Fond et bordure identiques pour TOUS les tiles → look uniforme.
        // Seule la couleur du texte (et un fin underline au survol/actif)
        // signale la cat active. Bien plus calme visuellement.
        background: active
          ? `color-mix(in oklab, ${tone} 12%, var(--surface))`
          : "var(--bg-alt)",
        color: active ? activeColor : "var(--ink-2)",
        border: active
          ? `1px solid color-mix(in oklab, ${tone} 40%, var(--line))`
          : "1px solid var(--line)",
        borderRadius: 10,
        fontSize: 13, fontWeight: active ? 600 : 540,
        cursor: "pointer", fontFamily: "inherit",
        textAlign: "center",
        transform: press ? "scale(0.97)" : "scale(1)",
        transition: "transform .12s ease, background .15s ease, color .15s ease, border-color .15s ease",
      }}>
      <span style={{ letterSpacing: "-0.005em" }}>{label}</span>
      <span style={{
        fontSize: 11,
        opacity: 0.55,
        fontVariantNumeric: "tabular-nums",
        fontWeight: 540,
      }}>{count}</span>
    </button>
  );
};

// Pill pour les filtres de catégorie. Active = fond plein dans la tone
// de la cat, inactive = ghost. Le mode `big` (mobile) augmente la
// hauteur et le padding pour des touch targets confortables (≥44px).
const BlogCatPill = ({ active, onClick, label, count, tone, ink, big = false }) => {
  const [hover, setHover] = React.useState(false);
  return (
    <button onClick={onClick}
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
      style={{
        display: "inline-flex", alignItems: "center", gap: big ? 7 : 6,
        padding: big ? "10px 14px 10px 12px" : "6px 12px 6px 10px",
        minHeight: big ? 40 : 0,
        background: active ? tone : (hover ? "var(--bg-alt)" : "transparent"),
        color: active ? "#fff" : (ink || "var(--ink-2)"),
        border: active ? "1px solid transparent" : "1px solid var(--line)",
        borderRadius: 999, fontSize: big ? 13.5 : 12.5, fontWeight: 540,
        cursor: "pointer", fontFamily: "inherit",
        flexShrink: 0, whiteSpace: "nowrap",
        transition: "background .15s ease, color .15s ease, transform .12s ease",
      }}
      onMouseDown={e => { e.currentTarget.style.transform = "scale(0.97)"; }}
      onMouseUp={e => { e.currentTarget.style.transform = "scale(1)"; }}>
      <span aria-hidden style={{
        width: big ? 7 : 6, height: big ? 7 : 6, borderRadius: 50,
        background: active ? "rgba(255,255,255,0.75)" : tone,
        flexShrink: 0,
      }}/>
      {label}
      <span style={{
        fontSize: big ? 11.5 : 11, opacity: 0.75,
        fontVariantNumeric: "tabular-nums",
      }}>{count}</span>
    </button>
  );
};

// Mapping cat → emoji déco grand format sur les cartes hero/medium.
// Choix d'émojis qui évoquent immédiatement la nature de l'article.
const BLOG_CAT_DECO = {
  guide:       "📚",
  comparatif:  "⚖️",
  metier:      "✂️",
  problemes:   "🛠️",
  conseil:     "💡",
};

// BlogCard uniforme : toutes les cards ont le même design propre et
// alignées en grille. Un badge ★ Nouveau apparaît sur la 1ère carte si
// `isLatest` est passé. Banner top compact 100px avec emoji moyen
// (48px) — plus de variations chaotiques de tailles.
const BlogCard = ({ article, onOpen, audience = "pro", isLatest = false }) => {
  const cat = BLOG_CATEGORIES[article.category] || BLOG_CATEGORIES.guide;
  const [hover, setHover] = React.useState(false);
  const isClient = audience === "client";
  const audienceAccent = isClient ? "var(--cli-accent)" : "var(--accent)";

  return (
    <a href={`/blog/${article.slug}`}
      onClick={e => { e.preventDefault(); onOpen(); }}
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
      style={{
        display: "flex", flexDirection: "column",
        background: "var(--surface)", border: "1px solid var(--line)",
        borderRadius: 16, overflow: "hidden",
        textDecoration: "none", color: "inherit",
        cursor: "pointer",
        // Hauteur fixe → grille parfaitement alignée, plus de cards qui
        // dépassent ou se rétrécissent. Plus calme visuellement.
        height: 380,
        boxShadow: hover
          ? `0 16px 32px -18px color-mix(in oklab, ${audienceAccent} 45%, transparent), var(--sh-1)`
          : "var(--sh-1)",
        transform: hover ? "translateY(-3px)" : "translateY(0)",
        transition: "transform .22s cubic-bezier(.22,1,.36,1), box-shadow .22s ease",
      }}>
      {/* Banner top : 100px, gradient cat-color subtil + emoji moyen.
          Plus de halo radial + drop-shadow agressif → on calme le jeu. */}
      <div style={{
        position: "relative", height: 100, flexShrink: 0,
        background: `linear-gradient(135deg, ${cat.tone} 0%, color-mix(in oklab, ${cat.tone} 70%, var(--ink)) 100%)`,
        display: "flex", alignItems: "center", justifyContent: "center",
        overflow: "hidden",
      }}>
        {/* Pattern dot-grid très discret (opacité 0.3 au lieu de 0.6) */}
        <div aria-hidden style={{
          position: "absolute", inset: 0,
          backgroundImage: "radial-gradient(circle, rgba(255,255,255,0.10) 1px, transparent 1.5px)",
          backgroundSize: "12px 12px",
          opacity: 0.35,
        }}/>
        {/* Emoji 48px (au lieu de 72/120) : présent mais ne crie pas */}
        <span style={{
          position: "relative", fontSize: 48, lineHeight: 1,
          transform: hover ? "scale(1.08) rotate(-3deg)" : "scale(1) rotate(0deg)",
          transition: "transform .3s cubic-bezier(.5,1.6,.4,1)",
        }}>{BLOG_CAT_DECO[article.category] || "📰"}</span>
        {/* Tag catégorie en haut-gauche */}
        <span style={{
          position: "absolute", top: 10, left: 12,
          padding: "3px 9px",
          background: "rgba(255,255,255,0.20)",
          border: "1px solid rgba(255,255,255,0.32)",
          color: "#fff",
          borderRadius: 999, fontSize: 10, fontWeight: 700,
          letterSpacing: "0.06em", textTransform: "uppercase",
          backdropFilter: "blur(6px)",
          WebkitBackdropFilter: "blur(6px)",
        }}>
          {cat.label}
        </span>
        {/* Badge « ★ Nouveau » uniquement sur la 1ère card (la plus récente) */}
        {isLatest && (
          <span style={{
            position: "absolute", top: 10, right: 12,
            padding: "3px 9px",
            background: "#fff", color: cat.ink,
            borderRadius: 999, fontSize: 10, fontWeight: 700,
            letterSpacing: "0.04em",
            boxShadow: "0 4px 10px rgba(0,0,0,0.15)",
          }}>
            ★ Nouveau
          </span>
        )}
      </div>
      {/* Contenu bas : flex column space-between pour que le titre soit
          en haut et le footer (date + lire) en bas, peu importe la
          longueur du desc. Alignement parfait sur toute la grille. */}
      <div style={{
        padding: "18px 20px",
        display: "flex", flexDirection: "column",
        flex: 1, minHeight: 0,
      }}>
        <h3 style={{
          margin: 0,
          fontFamily: "var(--ff-display)", fontSize: 17, fontWeight: 580,
          letterSpacing: "-0.018em", lineHeight: 1.25, color: "var(--ink)",
          display: "-webkit-box", WebkitLineClamp: 2, WebkitBoxOrient: "vertical",
          overflow: "hidden",
        }}>
          {article.title}
        </h3>
        <p style={{
          margin: "8px 0 0", fontSize: 13.5, color: "var(--ink-3)", lineHeight: 1.5,
          display: "-webkit-box", WebkitLineClamp: 3, WebkitBoxOrient: "vertical",
          overflow: "hidden",
        }}>
          {article.desc}
        </p>
        <div style={{
          display: "flex", alignItems: "center", justifyContent: "space-between",
          gap: 10, marginTop: "auto", paddingTop: 12,
          fontSize: 11.5, color: "var(--ink-4)",
          borderTop: "1px solid var(--line)",
        }}>
          <span>{_fmtDate(article.date)} · {article.readMin} min</span>
          <span style={{
            display: "inline-flex", alignItems: "center", gap: 4,
            color: cat.ink, fontWeight: 600,
            transform: hover ? "translateX(3px)" : "translateX(0)",
            transition: "transform .2s ease",
          }}>
            Lire <Icon name="arrow" size={11}/>
          </span>
        </div>
      </div>
    </a>
  );
};

const BlogArticle = ({ article, go, onBack, onArticle, audience = "pro" }) => {
  const cat = BLOG_CATEGORIES[article.category] || BLOG_CATEGORIES.guide;
  const isClient = audience === "client";
  // Articles connexes : même audience, même catégorie en priorité, hors
  // article courant, 3 max. Pas de mélange pro/client (sinon clientes
  // perdues sur un article hyper-technique sur la facturation).
  const related = React.useMemo(() => {
    const sameAudience = ARTICLES.filter(a =>
      a.slug !== article.slug
      && ((isClient && a.audience === "client") || (!isClient && a.audience !== "client"))
    );
    // Priorité : même catégorie d'abord, puis le reste pour combler à 3.
    const same  = sameAudience.filter(a => a.category === article.category);
    const other = sameAudience.filter(a => a.category !== article.category);
    return [...same, ...other].slice(0, 3);
  }, [article, isClient]);

  return (
    <article style={{ padding: "32px 0 80px" }}>
      <div className="container" style={{ maxWidth: 760 }}>
        {/* Top bar : Retour à gauche, Catégorie cliquable à droite.
            Distinction visuelle nette : le retour est un bouton outlined
            grisé, la catégorie est un tag coloré bien différent. La cat
            est cliquable et ramène à l'index filtré par cette cat. */}
        <div style={{
          display: "flex", alignItems: "center", justifyContent: "space-between",
          gap: 12, marginBottom: 28,
        }}>
          <button onClick={onBack} style={{
            display: "inline-flex", alignItems: "center", gap: 6,
            padding: "8px 14px 8px 10px",
            background: "transparent", border: "1px solid var(--line-strong)",
            borderRadius: 999, cursor: "pointer", fontFamily: "inherit",
            fontSize: 13, color: "var(--ink-2)", fontWeight: 540,
            transition: "background .15s ease, border-color .15s ease",
          }}
            onMouseEnter={e => { e.currentTarget.style.background = "var(--bg-alt)"; }}
            onMouseLeave={e => { e.currentTarget.style.background = "transparent"; }}>
            <Icon name="arrow" size={13} style={{ transform: "rotate(180deg)" }}/>
            Retour au blog
          </button>
          {/* Pill catégorie cliquable : aller à /blog?cat=X pour re-filtrer */}
          <button onClick={onBack}
            title={`Voir tous les articles ${cat.label.toLowerCase()}`}
            style={{
              display: "inline-flex", alignItems: "center", gap: 6,
              padding: "5px 11px 5px 9px",
              background: cat.tone, color: "#fff",
              border: "none", borderRadius: 999,
              cursor: "pointer", fontFamily: "inherit",
              fontSize: 11, fontWeight: 700,
              letterSpacing: "0.05em", textTransform: "uppercase",
              boxShadow: `0 4px 12px -4px ${cat.tone}`,
              transition: "transform .12s ease, filter .15s ease",
            }}
            onMouseDown={e => { e.currentTarget.style.transform = "scale(0.97)"; }}
            onMouseUp={e => { e.currentTarget.style.transform = "scale(1)"; }}>
            <span aria-hidden style={{
              width: 6, height: 6, borderRadius: 50,
              background: "rgba(255,255,255,0.7)",
            }}/>
            {cat.label}
          </button>
        </div>

        <h1 style={{
          margin: 0,
          fontFamily: "var(--ff-display)", fontSize: "clamp(28px, 4vw, 42px)",
          fontWeight: 600, letterSpacing: "-0.03em", lineHeight: 1.1,
        }}>
          {article.title}
        </h1>

        <div style={{
          marginTop: 12, fontSize: 13, color: "var(--ink-4)",
          display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap",
        }}>
          <span>{_fmtDate(article.date)}</span>
          <span aria-hidden>·</span>
          <span>{article.readMin} min de lecture</span>
        </div>

        <div style={{
          marginTop: 32, fontSize: 16.5, lineHeight: 1.75, color: "var(--ink)",
          fontFamily: "var(--ff-text)",
        }}>
          {article.body.map((block, i) => {
            if (block.type === "h2") {
              return <h2 key={i} style={{
                marginTop: 36, marginBottom: 14,
                fontFamily: "var(--ff-display)", fontSize: 24, fontWeight: 580,
                letterSpacing: "-0.025em", color: "var(--ink)",
              }}>{block.text}</h2>;
            }
            if (block.type === "h3") {
              return <h3 key={i} style={{
                marginTop: 26, marginBottom: 10,
                fontFamily: "var(--ff-display)", fontSize: 18, fontWeight: 580,
                color: "var(--ink)",
              }}>{block.text}</h3>;
            }
            if (block.type === "p") {
              return <p key={i} style={{ margin: "0 0 14px", color: "var(--ink-2)" }}>{block.text}</p>;
            }
            if (block.type === "list") {
              return <ul key={i} style={{
                margin: "0 0 16px", paddingLeft: 22,
                color: "var(--ink-2)",
              }}>
                {block.items.map((item, j) => (
                  <li key={j} style={{ marginBottom: 8, lineHeight: 1.65 }}>{item}</li>
                ))}
              </ul>;
            }
            if (block.type === "quote") {
              return <blockquote key={i} style={{
                margin: "22px 0",
                padding: "16px 22px",
                background: "var(--bg-alt)",
                borderLeft: `4px solid ${cat.tone}`,
                borderRadius: 8,
                fontFamily: "var(--ff-display)", fontStyle: "italic",
                fontSize: 18, color: cat.ink, lineHeight: 1.5,
              }}>{block.text}</blockquote>;
            }
            if (block.type === "cta") {
              return (
                <div key={i} style={{
                  margin: "32px 0 8px", padding: "22px 24px",
                  background: "var(--accent-soft)", border: "1px solid var(--accent-soft-2)",
                  borderRadius: 14, textAlign: "center",
                }}>
                  <button onClick={() => go(block.page || "signup")} className="btn btn-accent btn-lg">
                    {block.label} <Icon name="arrow" size={14}/>
                  </button>
                </div>
              );
            }
            return null;
          })}
        </div>

        {related.length > 0 && (
          <div style={{
            marginTop: 56, paddingTop: 32,
            borderTop: "1px solid var(--line)",
          }}>
            <h2 style={{
              margin: "0 0 18px",
              fontFamily: "var(--ff-display)", fontSize: 18, fontWeight: 580,
              color: "var(--ink)",
            }}>
              À lire aussi
            </h2>
            <div style={{
              display: "grid",
              gridTemplateColumns: "repeat(auto-fill, minmax(260px, 1fr))",
              gap: 14,
            }}>
              {related.map(a => {
                const relCat = BLOG_CATEGORIES[a.category] || BLOG_CATEGORIES.guide;
                const basePath = isClient ? "/blog-client" : "/blog";
                return (
                  <a key={a.slug}
                    href={`${basePath}/${a.slug}`}
                    onClick={e => {
                      e.preventDefault();
                      // Navigation SPA via callback parent : pas de reload,
                      // garde le contexte React (state, animations, etc).
                      if (onArticle) onArticle(a.slug);
                    }}
                    style={{
                      display: "block", padding: "14px 16px",
                      background: "var(--surface)", border: "1px solid var(--line)",
                      borderRadius: 10, textDecoration: "none", color: "inherit",
                      fontSize: 13, lineHeight: 1.4,
                      transition: "border-color .18s ease, transform .15s ease",
                    }}
                    onMouseEnter={e => {
                      e.currentTarget.style.borderColor = relCat.tone;
                      e.currentTarget.style.transform = "translateY(-2px)";
                    }}
                    onMouseLeave={e => {
                      e.currentTarget.style.borderColor = "var(--line)";
                      e.currentTarget.style.transform = "translateY(0)";
                    }}>
                    <div style={{
                      fontSize: 10.5, fontWeight: 600, color: relCat.ink,
                      textTransform: "uppercase", letterSpacing: "0.04em",
                      marginBottom: 6,
                    }}>{relCat.label}</div>
                    <div style={{ color: "var(--ink)", fontWeight: 540 }}>
                      {a.title}
                    </div>
                  </a>
                );
              })}
            </div>
          </div>
        )}
      </div>
    </article>
  );
};

const BlogNotFound = ({ onBack, audience = "pro" }) => (
  <section style={{ padding: "80px 20px", textAlign: "center" }}>
    <div className="container" style={{ maxWidth: 480 }}>
      <div style={{ fontSize: 48, marginBottom: 12 }}>📭</div>
      <h1 style={{
        fontFamily: "var(--ff-display)", fontSize: 28, fontWeight: 580,
        letterSpacing: "-0.025em", margin: "0 0 10px",
      }}>
        Article introuvable
      </h1>
      <p style={{ fontSize: 14, color: "var(--ink-3)", margin: "0 0 20px", lineHeight: 1.5 }}>
        Cet article n'existe pas ou n'est plus disponible. Retour à la liste des articles.
      </p>
      <button onClick={onBack} className="btn btn-accent">
        ← Voir tous les articles
      </button>
    </div>
  </section>
);

if (typeof window !== "undefined") {
  window.BlogPage = BlogPage;
}
