/* Console partenaire — Documentation (design dev-docs.jsx) : même contenu, URL de base réelle, exemples
   neutres, « Essayer » appelle vraiment la sandbox avec la clé lms_test_ que vous collez. */
const BASE = API_V1_BASE;
function ex(m, p, body, key){
  const b = body ? JSON.stringify(body, null, 2) : null, bi = b && b.split('\n').join('\n  ');
  return {
    cURL: `curl -X ${m} ${BASE}${p} \\\n  -H "Authorization: Bearer ${key}"${b?` \\\n  -H "Content-Type: application/json" \\\n  -d '${b}'`:''}`,
    JavaScript: `const res = await fetch("${BASE}${p}", {\n  method: "${m}",\n  headers: {\n    "Authorization": "Bearer ${key}"${b?',\n    "Content-Type": "application/json"':''}\n  }${b?`,\n  body: JSON.stringify(${bi})`:''}\n});\nconst data = await res.json();`,
    PHP: `$ch = curl_init("${BASE}${p}");\ncurl_setopt($ch, CURLOPT_CUSTOMREQUEST, "${m}");\ncurl_setopt($ch, CURLOPT_HTTPHEADER, [\n  "Authorization: Bearer ${key}",\n  "Content-Type: application/json"\n]);${b?`\ncurl_setopt($ch, CURLOPT_POSTFIELDS, '${b.replace(/\n\s*/g,' ')}');`:''}\ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\n$data = json_decode(curl_exec($ch), true);`,
    Python: `import requests\n\nres = requests.${m.toLowerCase()}(\n    "${BASE}${p}",\n    headers={"Authorization": "Bearer ${key}"}${b?`,\n    json=${b.replace(/\n/g,'\n    ').replace(/true/g,'True').replace(/false/g,'False')}`:''}\n)\ndata = res.json()`,
    Java: `HttpRequest req = HttpRequest.newBuilder()\n  .uri(URI.create("${BASE}${p}"))\n  .header("Authorization", "Bearer ${key}")\n  .header("Content-Type", "application/json")\n  .method("${m}", ${b?`HttpRequest.BodyPublishers.ofString("""\n${b}\n""")`:'HttpRequest.BodyPublishers.noBody()'})\n  .build();\nHttpResponse<String> res = HttpClient.newHttpClient()\n  .send(req, HttpResponse.BodyHandlers.ofString());`,
  };
}
const J = o => JSON.stringify(o, null, 2);
const CTR = 'ctr_4fd9bc4f48e242408b13edc0899311ec', CTR_TP = 'ctr_226a6622ee8e419fb62c07adfccf36de', OFF = 'off_c244e133e9d049d184465f3c6126f0bc';
const DEVIS_BODY = {vehicule:{genre:'VP', energie:'ESSENCE', puissance_fiscale:7, places:5, valeur_venale:6500000}, duree_mois:12, garanties:['defense_recours','personnes_transportees']};
const CONTRAT_BODY = {offre:OFF, reference:'PART-2026-640601', date_effet:'2026-09-26', souscripteur:{nom:'Diop', prenom:'Awa', telephone:'+221771234567', email:'awa.diop@example.sn'}, vehicule:{immatriculation:'DK-6406-AB', marque:'Toyota', modele:'Corolla'}};
const OFFRE = (id, a, pn, tx, pc, rp, fs, md) => ({id, assureur:a, garanties:['responsabilite_civile','defense_recours','personnes_transportees'], prime_nette:pn, fga:1022, taxes:tx, accessoire:3000, prix_client:pc, remise_partenaire:rp, frais_service:fs, montant_debite:md});
const DEVIS_RES = {id:'dev_26a19724db07406bb55579414f09a152', mode:'test', expire_a:'2026-09-25T13:42:38+00:00', duree_mois:12, offres:[OFFRE(OFF,'providence',51662,7653,63337,9249,1108,55196), OFFRE('off_6f35b2c3ef6b4b27b33be8658c2b5c9c','askia',54662,8073,66757,9699,1168,58226)], indisponibles:[]};
const CONTRAT_RES = {id:CTR, objet:'contrat_auto', mode:'test', reference:'PART-2026-640601', statut:'actif', assureur:'providence', police:'TEST-BAA2D3935E', attestation:'SPECIMEN-BAA2D3935E', immatriculation:'DK6406AB', date_effet:'2026-09-26', date_echeance:'2027-09-25', garanties:['responsabilite_civile','defense_recours','personnes_transportees'], prix_client:63337, montant_debite:55196,
  souscripteur:{nom:'DIOP', prenom:'AWA', telephone:'221771234567'}, assure:{nom:'DIOP', prenom:'AWA'}, vehicule:{marque:'TOYOTA', modele:'COROLLA', immatriculation:'DK6406AB'}, remorque:null, documents:{attestation:'', carte_brune:'', specimen:true}, annule_le:null, erreur:null, cree_le:'2026-09-25T13:12:56+00:00'};
const CATALOGUE_RES = {assureurs:[{code:'askia', nom:'Askia Assurances'},{code:'providence', nom:'Providence Assurances'}], energies:['ESSENCE','DIESEL'], duree_mois:{min:1, max:12},
  genres:[{code:'VP', libelle:'Voiture particulière', puissance:'puissance_fiscale', assureurs:['askia','providence']},{code:'TPC', libelle:'Transport public de personnes', puissance:'puissance_fiscale', assureurs:['askia','providence']},{code:'TPC3T500', libelle:"Transport public jusqu'à 3,5 t", puissance:'puissance_fiscale', assureurs:['askia','providence']},{'…':'10 autres genres'}],
  garanties:[{code:'responsabilite_civile', libelle:'Responsabilité civile', incluse:true},{code:'defense_recours', libelle:'Défense et recours', duree_min_mois:1},{code:'personnes_transportees', libelle:'Personnes transportées', duree_min_mois:1},{code:'vol', libelle:'Vol', duree_min_mois:{askia:3, providence:6}, exige:'vehicule.valeur_venale'},{'…':'4 autres garanties'}],
  regles:['Deux-roues, engins et tracteurs : responsabilité civile seule.','Transport de marchandises chez Providence : bris de glace, vol et incendie seulement.','Une offre est valable 30 minutes.','Askia exige, à la souscription, assure.piece_identite et assure.date_naissance.']};
const GENRES = 'VP, TPC, TPC3T500, TPC3T500P, TPM, TPM3T500, TPM3T500P, 2RCYC, 2RSCO, 2RMOT, 2RSID, ENGIN, TRACTEUR';
const Q = (s) => '?'+s;
/* [id, méthode, chemin, titre, description, paramètres, corps, réponse, erreurs, chemin d'exemple, règles] */
const EP = [
  ['solde','GET','/solde','Consulter le solde','Solde prépayé unique, montant retenu en vérification, plafonds et services ouverts sur le compte.',[],null,J({mode:'test', solde:31889040, devise:'XOF', en_verification:0, seuil_alerte:2000000, plafond_operation:5000000, plafonds_jour:{}, services_actifs:['assurance'], mis_a_jour_le:'2026-09-25T13:12:28+00:00'}),['cle_absente','cle_invalide'],'/solde'],
  ['mouvements','GET','/solde/mouvements','Lister les mouvements','Relevé paginé du solde. Chaque mouvement indique le service, le libellé et l\'objet concerné (off_…, ctr_…, av_…).',[['service','string',0,'assurance, billetterie, esim, sms, compte'],['page','integer',0,'1 par défaut'],['par_page','integer',0,'25 par défaut'],['type · depuis','—',0,'Bientôt']],null,J({objet:'liste', page:1, total:105, page_suivante:2, donnees:[{date:'2026-09-25T13:13:16+00:00', service:'assurance', type:'debit', montant:-107905, solde_apres:31781135, libelle:'Assurance auto DK6406TP (providence)', objet:'off_7f9403b2fc674a958010ce16e18a1be2'},{date:'2026-09-25T13:13:00+00:00', service:'assurance', type:'remboursement', montant:65700, solde_apres:31889040, libelle:'Annulation DK6406AB (providence)', objet:'ctr_4fd9bc4f48e242408b13edc0899311ec'}]}),[],'/solde/mouvements?service=assurance&par_page=3'],
  ['sb_recharge','POST','/sandbox/solde/recharger','Recharger le solde de test','Sandbox uniquement. Crédite le solde de test. 10 000 000 F CFA au plus par appel.',[['montant','integer',1,'En F CFA, 10 000 000 au plus']],{montant:1000000},J({mode:'test', solde:1000000, devise:'XOF', '…':'(même forme que GET /solde)'}),['donnees_invalides'],'/sandbox/solde/recharger',null,1],
  ['sb_rappro','POST','/sandbox/rapprochement','Lancer le rapprochement','Sandbox uniquement. Traite tout de suite les contrats a_verifier (en production, le rapprochement tourne toutes les 5 minutes).',[],{},J({'…':'Contrats a_verifier passés à actif ou echec'}),[],'/sandbox/rapprochement',null,1],
  ['catalogue','GET','/assurance/auto/catalogue','Catalogue','Genres de véhicule, énergies, durées et garanties acceptés par chaque assureur, avec les règles de compatibilité.',[],null,J(CATALOGUE_RES),['cle_absente','cle_invalide'],'/assurance/auto/catalogue'],
  ['marques','GET','/assurance/auto/marques','Marques','Propose l\'orthographe exacte d\'une marque. Askia refuse une marque absente de son référentiel.',[['recherche','string',1,'Début du nom, par exemple TOY']],null,J({total:2, marques:['TOYOTA','TOYO']}),['cle_invalide'],'/assurance/auto/marques'+Q('recherche=TOY')],
  ['verif','GET','/assurance/auto/verification','Vérifier un véhicule','Interroge le registre national : le véhicule est-il déjà couvert, et jusqu\'à quand ?',[['immatriculation','string',1,'Avec ou sans tirets. Renvoyée compactée : DK4898BG'],['date_effet','date',0,'AAAA-MM-JJ. « Sera-t-il couvert à cette date ? » Aujourd\'hui par défaut.']],null,J({immatriculation:'DK4898BG', date_effet:'2026-09-25', deja_assure:true, attestation:'SN0094R8U4M', couvert_jusqua:'2026-10-22', message:'L’attestation d’assurance N° SN0094R8U4M du véhicule de marque 65  - RENAULT MASTER immatriculé DK4898BG est valide du 2026-09-23 au 2026-10-22 23:59:59 chez PROVIDENCE Assurances'}),['immatriculation_invalide','registre_indisponible'],'/assurance/auto/verification'+Q('immatriculation=DK-4898-BG')],
  ['devis','POST','/assurance/auto/devis','Créer un devis','Interroge Askia et Providence et renvoie une offre par assureur. Chaque offre a son propre id off_…, valable 30 minutes : c\'est lui que vous souscrivez. Les tarifs sont réels, même en sandbox.',
    [['vehicule.genre','string',1,GENRES],['vehicule.energie','string',1,'ESSENCE ou DIESEL'],['vehicule.puissance_fiscale','integer',0,'En CV fiscaux. Requis pour les voitures et transports.'],['vehicule.cylindree','integer',0,'En cm³. Requis pour les deux-roues (2RCYC, 2RSCO, 2RMOT, 2RSID).'],['vehicule.places','integer',0,''],['vehicule.valeur_venale','integer',0,'Requis pour vol, incendie, dommage'],['duree_mois','integer',1,'De 1 à 12'],['garanties','string[]',0,'Codes du catalogue en minuscules. responsabilite_civile est toujours incluse.'],['assureur','string',0,'askia ou providence, pour n\'interroger que l\'un des deux']],
    DEVIS_BODY,J(DEVIS_RES),['genre_invalide','energie_invalide','duree_invalide','puissance_invalide','cylindree_incoherente','garantie_inconnue','valeur_venale_requise','assureur_indisponible'],'/assurance/auto/devis'],
  ['contrat','POST','/assurance/auto/contrats','Souscrire un contrat','Émet le contrat chez l\'assureur de l\'offre et débite votre solde. Idempotent sur reference. La réponse est le contrat complet.',
    [['offre','string',1,'id off_… d\'une offre du devis, moins de 30 minutes'],['reference','string',1,'Votre identifiant unique, 64 caractères au plus. Le rejouer ne débite jamais deux fois.'],['date_effet','date',1,'AAAA-MM-JJ, entre aujourd\'hui et J+60'],['souscripteur','object',1,'nom, prenom, telephone ; en option email, adresse, piece_identite, date_naissance. Askia exige piece_identite et date_naissance.'],['assure','object',0,'Même forme. Par défaut, l\'assuré est le souscripteur.'],['vehicule','object',1,'immatriculation, marque, modele ; en option carrosserie, chassis, date_mise_circulation']],
    CONTRAT_BODY,J(CONTRAT_RES),['offre_introuvable','offre_expiree','offre_deja_utilisee','reference_deja_utilisee','reference_en_cours','tarif_modifie','vehicule_deja_assure','marque_inconnue','identite_incomplete','solde_insuffisant','etat_indetermine'],'/assurance/auto/contrats',
    ['Les textes sont renvoyés normalisés : majuscules, sans accents (Diop → DIOP).','Immatriculation compactée (DK6406AB), téléphone en chiffres seuls (221771234567).','En production, statut vaut actif, echec (remboursé) ou a_verifier.']],
  ['liste','GET','/assurance/auto/contrats','Lister les contrats','Liste paginée de vos contrats. Pour retrouver un contrat par votre référence, filtrez sur reference.',[['reference','string',0,'Votre référence'],['immatriculation','string',0,''],['statut','string',0,'actif, a_verifier, echec, annule, resilie…'],['page','integer',0,'1 par défaut'],['par_page','integer',0,'25 par défaut']],null,J({objet:'liste', page:1, par_page:25, total:1, page_suivante:null, donnees:[{id:CTR, objet:'contrat_auto', mode:'test', reference:'PART-2026-640601', statut:'actif', assureur:'providence', '…':'(contrat complet)'}]}),[],'/assurance/auto/contrats'+Q('reference=PART-2026-640601')],
  ['get','GET','/assurance/auto/contrats/{id}','Récupérer un contrat','Détail d\'un contrat par son identifiant ctr_… uniquement.',[['id','string',1,'ctr_… (pas votre référence : utilisez la liste filtrée sur reference)']],null,J(CONTRAT_RES),['contrat_introuvable'],'/assurance/auto/contrats/'+CTR],
  ['docs','GET','/assurance/auto/contrats/{id}/documents','Documents','Liens de l\'assureur : attestation officielle, facture, conditions particulières. En sandbox, les liens sont vides et specimen vaut true.',[['id','string',1,'ctr_…']],null,J({contrat:CTR, police:'TEST-BAA2D3935E', remorque:{}, documents:{attestation:'', facture:'', conditions:'', specimen:true}}),['contrat_introuvable','documents_indisponibles'],'/assurance/auto/contrats/'+CTR+'/documents'],
  ['avenant','POST','/assurance/auto/contrats/{id}/avenants','Avenant','Modifie un contrat actif. Un seul type par avenant.',[['reference','string',1,'Idempotence'],['type','string',1,'vehicule, duree, souscripteur ou garanties'],['vehicule','object',0,'Pour type vehicule, ou valeur_venale si une garantie l\'exige'],['duree_mois','integer',0,'Pour type duree'],['souscripteur','object',0,'Pour type souscripteur'],['garanties','string[]',0,'Liste complète. Pour la RC seule : ["responsabilite_civile"], jamais une liste vide.']],
    {reference:'PART-AV-64061', type:'garanties', garanties:['defense_recours','personnes_transportees','vol'], vehicule:{valeur_venale:6500000}},J({id:'av_53fdd7cf7d4d4602ae22327b5e865922', objet:'avenant', reference:'PART-AV-64061', type:'garanties', statut:'effectue', debite:10504, frais:0, contrat:{id:CTR, statut:'actif', garanties:['responsabilite_civile','defense_recours','personnes_transportees','vol'], prix_client:75193, montant_debite:65700, '…':'(contrat complet)'}}),
    ['type_invalide','aucune_modification','garanties_requises','avenant_limite_atteinte','avenant_hors_delai','avenant_refuse','solde_insuffisant'],'/assurance/auto/contrats/'+CTR+'/avenants',
    ['Une hausse de prime est débitée, plus d\'éventuels frais d\'avenant.','Une baisse n\'est pas remboursée. Pour corriger une erreur : annuler sous 24 h et réémettre.','La durée ne peut que s\'allonger, 12 mois au plus.','3 avenants au plus par contrat.','Askia : pas d\'avenant plus de 2 jours après la prise d\'effet, pas de changement de souscripteur.']],
  ['remorque','POST','/assurance/auto/contrats/{id}/remorques','Ajouter une remorque','Providence uniquement, véhicules TPC et TPM, une remorque par contrat. La réponse est le contrat, avec son champ remorque rempli.',[['reference','string',1,'Idempotence'],['remorque','object',1,'immatriculation, marque, modele']],
    {reference:'PART-RM-64061', remorque:{immatriculation:'DK-6406-R', marque:'Schmitz', modele:'S01'}},J({id:CTR_TP, objet:'contrat_auto', reference:'PART-2026-640602', statut:'actif', assureur:'providence', immatriculation:'DK6406TP', vehicule:{marque:'TOYOTA', modele:'HIACE', immatriculation:'DK6406TP'}, remorque:{immatriculation:'DK6406R', marque:'SCHMITZ', modele:'S01', attestation:'SPECIMEN-R6E1FFE273', lien_attestation:''}, '…':'(contrat complet)'}),['remorque_non_autorisee','remorque_existante','remorque_refusee'],'/assurance/auto/contrats/'+CTR_TP+'/remorques'],
  ['resil','POST','/assurance/auto/contrats/{id}/resiliation','Résilier','Crée une demande de résiliation à une date future. Traitée par LeMultiservice en production (en_attente), immédiatement en sandbox (traitee). Aucun remboursement.',[['reference','string',1,'Idempotence'],['date_effet','date',1,'Future, dans la période du contrat'],['motif','string',1,'Par exemple vente_vehicule']],
    {reference:'PART-RS-64061', date_effet:'2026-11-24', motif:'vente_vehicule'},J({id:'dem_678f7632f9934a4e98875cc335e7f707', objet:'demande', type:'resiliation', contrat:CTR_TP, statut:'traitee', date_effet:'2026-11-24', rembourse:0, cree_le:'2026-09-25T13:13:17+00:00'}),['contrat_non_resiliable','date_effet_invalide'],'/assurance/auto/contrats/'+CTR_TP+'/resiliation'],
  ['annul','POST','/assurance/auto/contrats/{id}/annulation','Annuler','Annule un contrat dans les 24 h suivant son émission. Tout ce qui a été débité, avenants compris, est remboursé. La réponse est le contrat au statut annule.',[['motif','string',1,'Par exemple erreur_saisie']],
    {motif:'erreur_saisie'},J({...CONTRAT_RES, statut:'annule', attestation:'SPECIMEN-DE2892AA86', garanties:['responsabilite_civile','defense_recours','personnes_transportees','vol'], prix_client:75193, montant_debite:65700, annule_le:'2026-09-25T13:13:00+00:00'}),['contrat_non_annulable','annulation_hors_delai'],'/assurance/auto/contrats/'+CTR+'/annulation'],
];
const ERRS = [
  ['cle_absente',401,'En-tête Authorization manquant.','Ajoutez Authorization: Bearer lms_…'],['cle_invalide',401,'Clé inconnue.','Vérifiez la clé et l\'environnement.'],['cle_revoquee',401,'Clé révoquée.','Créez une nouvelle clé.'],
  ['cle_hors_portee',403,'Clé non autorisée pour ce service.','Utilisez une clé qui couvre le service.'],['ip_non_autorisee',403,'IP non autorisée (clés live).','Ajoutez l\'IP dans Paramètres développeur.'],['compte_suspendu',403,'Compte partenaire suspendu.','Contactez LeMultiservice.'],['service_non_active',403,'Service non ouvert sur le compte.','Contactez LeMultiservice.'],['plafond_operation_depasse',403,'Opération au-delà du plafond par opération.','Contactez LeMultiservice.'],['plafond_journalier_atteint',403,'Plafond du jour du service atteint.','Attendez le lendemain ou faites relever le plafond.'],
  ['solde_insuffisant',402,'Le solde ne couvre pas l\'opération.','Rechargez, puis rejouez avec la même reference.'],['operation_concurrente',429,'Trop d\'opérations simultanées sur le solde ; rien n\'a été débité.','Rejouez avec la même reference.'],
  ['donnees_invalides',400,'Champ obligatoire absent ou de mauvais type.','Voir le message.'],['genre_invalide · energie_invalide · duree_invalide',400,'Valeur hors catalogue.','Utilisez GET /catalogue.'],['puissance_invalide · cylindree_invalide · cylindree_incoherente',400,'Puissance ou cylindrée incohérente.','CV fiscaux pour une voiture, cm³ pour un deux-roues.'],['garantie_inconnue',400,'Code de garantie inconnu.','Utilisez les codes du catalogue.'],['valeur_venale_requise · valeur_neuve_requise',400,'Vol, incendie ou dommage sans valeur du véhicule.','Ajoutez vehicule.valeur_venale / valeur_neuve.'],['reference_invalide',400,'reference absente ou trop longue.','64 caractères au plus.'],['identite_incomplete · telephone_invalide',400,'Identité incomplète (Askia : pièce et date de naissance).','Complétez.'],['vehicule_incomplet · immatriculation_invalide · marque_inconnue · carrosserie_invalide',400,'Véhicule incomplet ou inconnu de l\'assureur.','Corrigez ; GET /marques propose les bonnes marques.'],['date_effet_invalide',400,'Date hors des bornes.','AAAA-MM-JJ, entre aujourd\'hui et J+60.'],['type_invalide · aucune_modification · modification_impossible · garanties_requises · avenant_impossible · avenant_refuse',400,'Règles de l\'avenant.','Voir la page Avenant.'],['remorque_non_autorisee · remorque_refusee · operation_indisponible',400,'Règles de la remorque.','Voir la page Remorque.'],
  ['offre_introuvable',404,'Offre inconnue pour cette clé.','Refaites un devis (une offre sandbox ne se souscrit pas en live).'],['contrat_introuvable',404,'Contrat inconnu pour cette clé.','Vérifiez l\'id ctr_….'],
  ['offre_expiree',410,'Offre de plus de 30 minutes.','Refaites un devis.'],
  ['offre_deja_utilisee',409,'Offre déjà souscrite.','Refaites un devis.'],['reference_deja_utilisee',409,'reference déjà prise pour une autre offre.','Nouvelle reference.'],['reference_en_cours',409,'Souscription avec cette reference encore en cours.','Consultez GET /assurance/auto/contrats?reference=… un peu plus tard.'],['tarif_modifie',409,'Le tarif de l\'assureur a changé depuis le devis.','Refaites un devis.'],['vehicule_deja_assure',409,'Véhicule déjà couvert à cette date.','Date d\'effet après l\'échéance indiquée.'],['contrat_non_modifiable · contrat_non_annulable · contrat_non_resiliable',409,'Opération impossible dans ce statut.','—'],['documents_indisponibles',409,'Pas de documents dans ce statut (annulé, échec…).','—'],['annulation_hors_delai',409,'Plus de 24 h depuis l\'émission.','Demandez une résiliation.'],['avenant_limite_atteinte · avenant_hors_delai',409,'3 avenants déjà faits, ou fenêtre Askia passée.','Contactez LeMultiservice.'],['remorque_existante',409,'Le contrat porte déjà une remorque.','—'],
  ['erreur_interne',500,'Incident de notre côté.','Réessayez ; si ça persiste, contactez LeMultiservice.'],['etat_indetermine',502,'Réponse de l\'assureur perdue.','Ne rejouez pas : consultez le contrat quelques minutes plus tard.'],['registre_indisponible · assureur_indisponible',503,'Registre national ou assureur injoignable.','Réessayez dans quelques minutes.'],
];
const HTTPS = [[400,'Données invalides ou règle de saisie'],[401,'Clé absente, inconnue ou révoquée'],[402,'solde_insuffisant'],[403,'Clé hors portée, IP non autorisée, compte suspendu, service non ouvert, plafonds atteints'],[404,'Contrat ou offre introuvable'],[409,'Conflit avec l\'état actuel : déjà assuré, reference ou offre déjà utilisée, statut, délais, tarif changé'],[410,'offre_expiree'],[429,'operation_concurrente'],[502,'etat_indetermine'],[503,'Assureur ou registre indisponible']];
const EPN = Object.fromEntries(EP.map(e=>[e[0],[e[0],e[3],e[1],e[11]?'Sandbox':'']]));
const G = (...ids) => ids.map(i=>EPN[i]);
const SOON = k => [['s_'+k, SVCS[k].n, '', 'Bientôt']];
const DTABS = [
  ['docs','Documentation','book-open-text',[['Démarrer',[['start','Démarrer en 5 minutes'],['auth','Authentification'],['url','URL de base']]],['Concepts',[['sandbox','Sandbox'],['idem','Idempotence'],['errors','Erreurs']]],['Ressources développeur',[['llm','LLM et assistants de code'],['webhooks','Webhooks']]]]],
  ['api','Référence API','code',[['Solde',G('solde','mouvements')],['Sandbox',G('sb_recharge','sb_rappro')],['Assurance auto · Référentiel',G('catalogue','marques','verif')],['Assurance auto · Souscription',G('devis','contrat')],['Assurance auto · Contrats',G('liste','get','docs')],['Assurance auto · Après-vente',G('avenant','remorque','resil','annul')],['Assurance voyage',SOON('voyage')],['Billetterie',SOON('billet')],['eSIM',SOON('esim')],['SMS',SOON('sms')]]],
  ['guides','Guides','compass',[['Assurance automobile',[['guide','Vendre une assurance en 2 appels']]],['Solde',[['soldes','Comprendre le solde']]]]],
];
const ALL = DTABS.flatMap(t=>t[3].flatMap(g=>g[1].map(x=>({id:x[0], l:x[1], m:x[2], tab:t[0], tabL:t[1], grp:g[0]}))));
const DNAV = DTABS.flatMap(t=>t[3]);
function mdFor(id){
  const e = EP.find(x=>x[0]===id), it = ALL.find(x=>x.id===id);
  if(e) return [`# ${e[3]}`, '', `\`${e[1]} ${BASE}${e[2]}\``, '', e[4], '', 'Authentification : `Authorization: Bearer lms_test_…` (sandbox) ou `lms_live_…` (production).', '',
    '## Paramètres', ...(e[5].length ? ['| Nom | Type | Requis | Description |','|---|---|---|---|', ...e[5].map(p=>`| \`${p[0]}\` | ${p[1]} | ${p[2]?'oui':'non'} | ${p[3]} |`)] : ['Aucun.']), '',
    ...(e[6] ? ['## Exemple de requête','```json', J(e[6]), '```',''] : []), '## Réponse (200)','```json', e[7], '```', '',
    ...(e[10] ? ['## Règles', ...e[10].map(r=>'- '+r), ''] : []), ...(e[8].length ? ['## Erreurs possibles', ...e[8].map(c=>{ const x = ERRS.find(r=>r[0].split(' · ').includes(c)); return `- \`${c}\`${x?` (${x[1]}) : ${x[2]}`:''}`; })] : [])].join('\n');
  if(id==='errors') return ['# Erreurs','', 'Format : `{ "erreur": { "code": "…", "message": "…" } }`. Le code est stable, le message peut changer.', '', '| Code | HTTP | Sens | Que faire |','|---|---|---|---|', ...ERRS.map(x=>`| \`${x[0]}\` | ${x[1]} | ${x[2]} | ${x[3]} |`)].join('\n');
  return `# ${it ? it.l : 'Documentation'}\n\nURL de base : ${BASE}`;
}
function llmPrompt(key){
  return [`Tu es un assistant d'intégration pour l'API LeMultiservice (assurance automobile Askia et Providence, solde prépayé unique).`, '',
    `URL de base : ${BASE}`, `Authentification : en-tête Authorization: Bearer ${key}`, '',
    'Parcours : 1) POST /assurance/auto/devis renvoie une offre par assureur (id off_…, valable 30 minutes). 2) POST /assurance/auto/contrats avec offre + reference unique (idempotence) émet le contrat ctr_… et débite le solde.',
    'Règles : codes de garanties en minuscules (responsabilite_civile toujours incluse), durée de 1 à 12 mois, deux-roues avec cylindree en cm³, Askia exige piece_identite et date_naissance.',
    'Erreurs : objet { erreur: { code, message } }. Ne rejoue jamais etat_indetermine ; rejoue solde_insuffisant et operation_concurrente avec la même reference.', '',
    "Aide-moi à intégrer cette API dans mon projet. Commence par me demander mon langage et mon framework."].join('\n');
}
const P = ({children}) => <p style={{margin:0, fontSize:14.5, lineHeight:1.7, fontWeight:500, color:V.ink2}}>{children}</p>;
const H3 = ({children}) => <h3 style={{margin:'10px 0 0', fontFamily:V.disp, fontWeight:800, fontSize:18, letterSpacing:'-0.02em', color:V.ink}}>{children}</h3>;
const C = ({children}) => <code style={{fontFamily:V.mono, fontSize:13, padding:'2px 6px', borderRadius:6, background:V.sub, color:V.ink}}>{children}</code>;
function Acc({title, n, open:o0=true, children, tone}){
  const [o, setO] = React.useState(o0);
  return <section style={{border:`1px solid ${V.line}`, borderRadius:18, overflow:'hidden', background:V.card}}>
    <button className="lb lr" aria-expanded={o} onClick={()=>setO(!o)} style={{display:'flex', alignItems:'center', gap:10, width:'100%', minHeight:48, padding:'0 16px', border:0, background:V.sub, cursor:'pointer', fontFamily:V.body, textAlign:'left'}}>
      <I n="caret-right" s={15} c={V.ink3} style={{transform:o?'rotate(90deg)':'none', transition:'transform .2s'}}/>
      <span style={{flex:1, fontFamily:V.disp, fontWeight:800, fontSize:15, color:V.ink}}>{title}</span>
      {n!=null && <span style={{fontSize:11.5, fontWeight:700, padding:'2px 8px', borderRadius:99, background:tone?TN[tone][0]:V.card, color:tone?TN[tone][1]:V.ink3}}>{n}</span>}</button>
    {o && <div style={{animation:'lfade .2s ease'}}>{children}</div>}</section>;
}
function Params({rows, flat}){ return rows.length ? <div style={flat?{}:{border:`1px solid ${V.line}`, borderRadius:18, overflow:'hidden'}}>{rows.map((r,i)=><div key={r[0]} style={{display:'flex', gap:12, padding:'12px 16px', borderTop:i?`1px solid ${V.line}`:'none', flexWrap:'wrap'}}>
  <span style={{minWidth:190}}><Mono c={V.ink} s={13}>{r[0]}</Mono><span style={{display:'block', marginTop:3, fontSize:12, color:V.ink3}}>{r[1]}{r[2]?<b style={{color:V.warn}}> · requis</b>:''}</span></span><span style={{flex:1, minWidth:180, fontSize:13.5, color:V.ink2, lineHeight:1.5}}>{r[3]}</span></div>)}</div> : <P>Aucun paramètre.</P>; }

function DocsPage({id, keyTxt, go, connecte}){
  const m = useMob(); const e = EP.find(x=>x[0]===id);
  let main, side;
  if(e){
    const key = keyTxt; main = <>
      <H s={30}>{e[3]}</H>
      <div style={{display:'flex', alignItems:'center', gap:10, padding:'8px 8px 8px 12px', borderRadius:16, border:`1px solid ${V.line}`, background:V.sub}}>
        <span style={{padding:'4px 9px', borderRadius:8, background:V.card, border:`1px solid ${V.line}`}}><Method m={e[1]}/></span>
        <span title={BASE+e[2]} style={{flex:1, minWidth:0, display:'flex', flexDirection:'column'}}><span style={{fontFamily:V.mono, fontSize:13.5, fontWeight:700, color:V.ink, wordBreak:'break-all'}}>{e[2]}</span><span style={{fontFamily:V.mono, fontSize:11, color:V.ink3, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis', direction:'rtl', textAlign:'left'}}>{BASE}</span></span>
        <Btn sm icon="play" onClick={()=>go('@try:'+e[0])}>Essayer</Btn></div>
      <P>{e[4]}</P>
      {e[11] && <Notice tone="warn" icon="flask">Disponible uniquement avec une clé lms_test_…</Notice>}
      <div id="sec-auth"><Acc title="Autorisation" n={1}><div style={{display:'flex', gap:12, padding:'12px 16px', flexWrap:'wrap'}}><span style={{minWidth:190}}><Mono c={V.ink} s={13}>Authorization</Mono><span style={{display:'block', marginTop:3, fontSize:12, color:V.ink3}}>header<b style={{color:V.warn}}> · requis</b></span></span><span style={{flex:1, minWidth:180, fontSize:13.5, color:V.ink2, lineHeight:1.5}}>Bearer lms_test_… en sandbox, lms_live_… en production.</span></div></Acc></div>
      <div id="sec-params"><Acc title={e[1]==='GET'?'Paramètres de requête':'Corps de la requête'} n={e[5].length}>{e[5].length ? <Params rows={e[5]} flat/> : <div style={{padding:16}}><P>Aucun paramètre.</P></div>}</Acc></div>
      {e[10] && <div id="sec-rules"><Acc title="Règles" n={e[10].length} tone="warn"><ul style={{margin:0, padding:'14px 16px 14px 36px', display:'flex', flexDirection:'column', gap:6}}>{e[10].map(r=><li key={r} style={{fontSize:14, lineHeight:1.6, color:V.ink2, fontWeight:500}}>{r}</li>)}</ul></Acc></div>}
      {e[8].length>0 && <div id="sec-errs"><Acc title="Erreurs possibles" n={e[8].length} tone="bad" open={false}><div style={{display:'flex', flexDirection:'column'}}>{e[8].map((c,i)=>{ const x = ERRS.find(r=>r[0].split(' · ').includes(c)); return <button key={c} className="lb lr" onClick={()=>go('errors')} style={{display:'flex', alignItems:'center', gap:10, padding:'11px 16px', border:0, borderTop:i?`1px solid ${V.line}`:'none', background:'none', cursor:'pointer', textAlign:'left', fontFamily:V.body}}>{x && <Http code={x[1]}/>}<Mono c={V.ink} s={12.5}>{c}</Mono><span style={{flex:1, fontSize:13, color:V.ink3, minWidth:0}}>{x?x[2]:''}</span><I n="arrow-right" s={14} c={V.ink3}/></button>; })}</div></Acc></div>}</>;
    side = <><Code title="Requête" code={ex(e[1], e[9], e[6], key)}/><RespTabs e={e}/></>;
  } else if(id==='start'){ main = <><Lbl>Démarrer</Lbl><H s={32}>Votre premier appel en 5 minutes</H><P>L'API LeMultiservice vous permet de vendre l'assurance automobile d'Askia et de La Providence depuis vos propres applications, avec un seul contrat et un solde prépayé.</P>
      {[['Créez un compte','L\'inscription ouvre immédiatement votre sandbox.','user-plus'],['Créez une clé sandbox','Dans Clés d\'API. Elle commence par lms_test_.','key'],['Faites votre premier appel','Récupérez le catalogue avec la commande ci-contre.','terminal'],['Vendez en 2 appels','Suivez le guide devis → contrat.','shopping-cart']].map((s,i)=><div key={i} style={{display:'flex', gap:14, padding:'14px 0', borderTop:`1px solid ${V.line}`}}><span style={{width:30, height:30, borderRadius:99, background:V.mint, color:V.accD, display:'grid', placeItems:'center', fontWeight:800, fontSize:13, flex:'none'}}>{i+1}</span><span><span style={{display:'block', fontWeight:800, fontSize:15, color:V.ink}}>{s[0]}</span><span style={{display:'block', fontSize:13.5, color:V.ink3, marginTop:2}}>{s[1]}</span></span></div>)}
      <div style={{display:'flex', gap:8}}><Btn onClick={()=>go('@signup')}>{connecte ? 'Créer une clé sandbox' : 'Créer un compte'}</Btn><Btn kind="secondary" onClick={()=>go('guide')}>Lire le guide</Btn></div></>;
    side = <Code title="Premier appel" code={ex('GET','/assurance/auto/catalogue',null,keyTxt)}/>;
  } else if(id==='auth'){ main = <><Lbl>Démarrer</Lbl><H s={30}>Authentification</H><P>Chaque requête porte votre clé dans l'en-tête <C>Authorization: Bearer lms_test_…</C>. Les clés <C>lms_test_</C> agissent en sandbox, les clés <C>lms_live_</C> en production. L'URL et les endpoints sont identiques.</P>
      <Grid cols="1fr 1fr">{[['Sandbox','lms_test_…','Dès l\'inscription · aucun débit réel','warn'],['Production','lms_live_…','Après validation du dossier · IP autorisées','ok']].map(x=><Card key={x[0]} pad={16}><Badge tone={x[3]} dot>{x[0]}</Badge><span style={{display:'block', marginTop:10}}><Mono c={V.ink} s={14}>{x[1]}</Mono></span><span style={{display:'block', marginTop:4, fontSize:12.5, color:V.ink3}}>{x[2]}</span></Card>)}</Grid>
      <Notice tone="warn" title="Ne l'exposez jamais côté client">Les clés donnent accès à vos soldes. Utilisez-les uniquement depuis vos serveurs.</Notice></>;
    side = <Code code={ex('GET','/solde',null,keyTxt)}/>;
  } else if(id==='url'){ main = <><Lbl>Démarrer</Lbl><H s={30}>URL de base</H><P>Toutes les routes de cette documentation sont relatives à l'URL de base ci-dessous. Elle est la même en sandbox et en production.</P>
      <div style={{display:'flex', alignItems:'center', gap:10, padding:'16px 18px', borderRadius:18, background:V.sub, border:`1px solid ${V.line}`, flexWrap:'wrap'}}><Lbl>LMS_API_BASE_URL</Lbl><Mono c={V.ink} s={14}>{BASE}</Mono><span style={{flex:1}}></span><CopyBtn text={BASE}/></div>
      <Notice tone="warn" title="Conservez-la dans votre configuration ; elle pourra évoluer">Ne l'écrivez pas en dur dans votre code. Nous annoncerons tout changement 90 jours à l'avance.</Notice></>;
    side = <Code code={{JavaScript:`// config.js\nexport const LMS_API_BASE_URL =\n  process.env.LMS_API_BASE_URL;\nexport const LMS_API_KEY =\n  process.env.LMS_API_KEY;`, Python:`import os\n\nLMS_API_BASE_URL = os.environ["LMS_API_BASE_URL"]\nLMS_API_KEY = os.environ["LMS_API_KEY"]`, Java:`String base = System.getenv("LMS_API_BASE_URL");\nString key = System.getenv("LMS_API_KEY");`, PHP:`$base = getenv("LMS_API_BASE_URL");\n$key = getenv("LMS_API_KEY");`}}/>;
  } else if(id==='idem'){ main = <><Lbl>Démarrer</Lbl><H s={30}>Idempotence</H><P>Les souscriptions, avenants, remorques et résiliations portent une <C>reference</C> unique chez vous. Si vous rejouez la même requête (coupure réseau, délai dépassé), l'API renvoie le résultat de la première tentative, sans second débit.</P>
      <Notice tone="ok" icon="shield-check" title="Jamais deux débits pour la même reference">Rejouer la souscription ci-contre renvoie le même contrat <C>ctr_4fd9…</C>.</Notice>
      <div style={{border:`1px solid ${V.line}`, borderRadius:18, overflow:'hidden'}}>{[[409,'reference_deja_utilisee','Même reference avec une autre offre.','Utilisez une nouvelle reference.'],[409,'reference_en_cours','La première tentative est encore en cours. La seconde n\'attend pas.','Consultez GET /assurance/auto/contrats?reference=… un peu plus tard.']].map((x,i)=><div key={x[1]} style={{display:'grid', gridTemplateColumns:m?'1fr':'230px 1fr', gap:m?4:14, padding:'13px 16px', borderTop:i?`1px solid ${V.line}`:'none'}}><span style={{display:'flex', alignItems:'center', gap:8}}><Http code={x[0]}/><Mono c={V.ink} s={12.5}>{x[1]}</Mono></span><span style={{fontSize:13.5, lineHeight:1.5}}><span style={{display:'block', color:V.ink, fontWeight:600}}>{x[2]}</span><span style={{display:'block', color:V.ink3}}>→ {x[3]}</span></span></div>)}</div></>;
    side = <><Code title="Rejouer la même requête" code={ex('POST','/assurance/auto/contrats',CONTRAT_BODY,keyTxt)} h={300}/><Code title="Exemple de réponse · 200 (même contrat)" code={J({id:CTR, objet:'contrat_auto', reference:'PART-2026-640601', statut:'actif', montant_debite:55196, '…':'(contrat complet)'})} langs={false}/></>;
  } else if(id==='errors'){ main = <><Lbl>Démarrer</Lbl><H s={30}>Erreurs</H><P>Toute erreur, sur tous les endpoints, renvoie un statut HTTP 4xx ou 5xx et un objet <C>erreur</C>, rien d'autre. Le <C>code</C> est stable : c'est lui qu'on teste. Le <C>message</C> est en français, lisible par vos équipes, et peut changer.</P>
      <H3>Statuts HTTP</H3><div style={{display:'grid', gridTemplateColumns:m?'1fr':'1fr 1fr', gap:8}}>{HTTPS.map(h=><div key={h[0]} style={{display:'flex', gap:10, alignItems:'flex-start', padding:'10px 12px', borderRadius:14, background:V.sub}}><Http code={h[0]}/><span style={{fontSize:13, color:V.ink2, lineHeight:1.45}}>{h[1]}</span></div>)}</div>
      <H3>Codes par statut</H3>{HTTPS.map(([c,l],k)=>{ const rows = ERRS.filter(x=>x[1]===c); return rows.length ? <Acc key={c} title={<span style={{display:'flex', alignItems:'center', gap:10}}><Http code={c}/><span style={{fontFamily:V.body, fontWeight:700, fontSize:13.5, color:V.ink2}}>{l}</span></span>} n={rows.length} open={k<3} tone={c>=500?'bad':c===402||c===429?'warn':undefined}>
        {rows.map((x,i)=><div key={x[0]} style={{display:'grid', gridTemplateColumns:m?'1fr':'minmax(0,1fr) minmax(0,1.1fr)', gap:m?4:14, padding:'13px 16px', borderTop:i?`1px solid ${V.line}`:'none'}}>
        <span style={{fontFamily:V.mono, fontSize:12.5, color:V.ink, lineHeight:1.6, wordBreak:'break-word'}}>{x[0]}</span><span style={{fontSize:13.5, lineHeight:1.5}}><span style={{display:'block', color:V.ink, fontWeight:600}}>{x[2]}</span><span style={{display:'block', color:V.ink3}}>→ {x[3]}</span></span></div>)}</Acc> : null; })}
      {ERRS.filter(x=>x[1]===500).map(x=><Notice key={x[0]} tone="bad" icon="warning-octagon" title={<><Mono c={V.bad}>{x[0]}</Mono> · 500</>}>{x[2]} {x[3]}</Notice>)}</>;
    side = <><Code title="Exemple de réponse · 402" code={J({erreur:{code:'solde_insuffisant', message:'Solde insuffisant : 0 F disponibles, 5682 F requis.'}})} langs={false}/><Code title="Exemple de réponse · 400" code={J({erreur:{code:'genre_invalide', message:"vehicule.genre doit être l'un de : "+GENRES+"."}})} langs={false}/><Code title="Exemple de réponse · 409" code={J({erreur:{code:'vehicule_deja_assure', message:"Ce véhicule est déjà couvert (attestation SN000TEST000, échéance 2099-12-31). Choisissez une date d'effet postérieure à l'échéance."}})} langs={false}/><Code title="Exemple de réponse · 404" code={J({erreur:{code:'contrat_introuvable', message:'Aucun contrat ctr_inexistant pour cette clé.'}})} langs={false}/></>;
  } else if(id==='sandbox'){ main = <><Lbl>Démarrer</Lbl><H s={30}>Sandbox</H><P>La sandbox reproduit la production sans débit réel. Le solde de test est crédité à l'ouverture du compte ; rechargez-le avec <C>POST /sandbox/solde/recharger</C>, 10 000 000 F CFA au plus par appel.</P>
      <Grid cols="1fr 1fr"><Card pad={16}><Badge tone="ok" icon="check-circle">Réel</Badge><div style={{marginTop:8}}><P>Les devis interrogent vraiment Askia et Providence. Le registre national est interrogé pour de vrai. Règles et validations identiques.</P></div></Card><Card pad={16}><Badge tone="warn" icon="flask">Simulé</Badge><div style={{marginTop:8}}><P>Émission : police <C>TEST-…</C>, attestation <C>SPECIMEN-…</C>, liens de documents vides avec <C>specimen: true</C>. Résiliations traitées immédiatement.</P></div></Card></Grid>
      <H3>Immatriculations de test</H3><div style={{border:`1px solid ${V.line}`, borderRadius:18, overflow:'hidden'}}>{[['TEST-DEJAASSURE','vehicule_deja_assure (sur /contrats et /verification)'],['TESTAMBIGUOK…','a_verifier, puis actif au rapprochement'],['TESTAMBIGUKO…','a_verifier, puis echec et remboursement']].map((x,i)=><div key={x[0]} style={{display:'flex', gap:14, padding:'12px 16px', borderTop:i?`1px solid ${V.line}`:'none', flexWrap:'wrap'}}><span style={{width:170}}><Mono c={V.ink}>{x[0]}</Mono></span><span style={{fontSize:13.5, color:V.ink2}}>{x[1]}</span></div>)}</div>
      <H3>Endpoints de la sandbox</H3>{[['POST','/sandbox/solde/recharger','Crédite le solde de test'],['POST','/sandbox/rapprochement','Lance tout de suite le rapprochement des contrats a_verifier (toutes les 5 minutes sinon)']].map(x=><div key={x[1]} style={{display:'flex', alignItems:'center', gap:10, padding:'12px 16px', borderRadius:16, border:`1px solid ${V.line}`, flexWrap:'wrap'}}><Method m={x[0]}/><Mono c={V.ink} s={13}>{x[1]}</Mono><span style={{flex:1}}></span><span style={{fontSize:13, color:V.ink3}}>{x[2]}</span></div>)}</>;
    side = <><Code title="Recharger le solde de test" code={ex('POST','/sandbox/solde/recharger',{montant:1000000},keyTxt)}/><Code title="Tester un véhicule déjà assuré" code={ex('POST','/assurance/auto/contrats',{...CONTRAT_BODY, reference:'PART-2026-640604', vehicule:{immatriculation:'TEST-DEJAASSURE', marque:'Kia', modele:'Rio'}},keyTxt)} h={260}/></>;
  } else if(id==='guide'){ main = <><Lbl>Guide</Lbl><H s={30}>Vendre une assurance en 2 appels</H><P>Un devis renvoie une offre par assureur, chacune avec son id off_…. Vous souscrivez l'offre choisie dans les 30 minutes : le contrat est émis et votre solde débité.</P>
      <div style={{display:'flex', alignItems:'center', gap:m?6:10, padding:18, borderRadius:20, background:V.sub, flexWrap:'wrap'}} aria-label="Schéma du parcours">
        {[['POST /devis → off_…','calculator'],['POST /contrats','file-text']].map((s,i)=><React.Fragment key={s[0]}><span style={{display:'flex', alignItems:'center', gap:8, padding:'10px 14px', borderRadius:14, background:V.card, border:`1px solid ${V.line}`}}><I n={s[1]} s={18} c={V.accD}/><Mono c={V.ink}>{s[0]}</Mono></span><I n="arrow-right" s={16} c={V.ink3}/></React.Fragment>)}
        <span style={{display:'flex', flexDirection:'column', gap:6}}>{[['actif','ok'],['a_verifier','warn'],['echec','bad']].map(x=><Badge key={x[0]} tone={x[1]}>{x[0]}</Badge>)}</span></div>
      <H3>Les statuts</H3><div style={{display:'flex', flexDirection:'column', gap:8}}>{[['actif','Émis. Documents disponibles.'],['a_verifier','La réponse de l\'assureur s\'est perdue : le montant est retenu (en_verification). Ne rejouez pas, consultez le contrat plus tard.'],['echec','Refusé. Le montant débité est remboursé.']].map(x=><div key={x[0]} style={{display:'flex', gap:12, alignItems:'center'}}><StatusB st={x[0]}/><span style={{fontSize:13.5, color:V.ink2}}>{x[1]}</span></div>)}</div></>;
    side = <><Code title="1 · Devis" code={ex('POST','/assurance/auto/devis',DEVIS_BODY,keyTxt)} h={260}/><Code title="2 · Contrat" code={ex('POST','/assurance/auto/contrats',CONTRAT_BODY,keyTxt)} h={260}/></>;
  } else if(id==='soldes'){ main = <><Lbl>Solde</Lbl><H s={30}>Solde et mouvements</H><P>Votre compte a un seul solde prépayé, commun à tous les services. Chaque opération le débite ; les annulations le recréditent. <C>en_verification</C> est l'argent retenu sur des contrats dont la réponse de l'assureur s'est perdue.</P>
      {[['GET','/solde','Solde, plafonds et services actifs'],['GET','/solde/mouvements','Relevé paginé, filtrable par service']].map(x=><div key={x[1]} style={{display:'flex', alignItems:'center', gap:10, padding:'12px 16px', borderRadius:16, border:`1px solid ${V.line}`, flexWrap:'wrap'}}><Method m={x[0]}/><Mono c={V.ink} s={13}>{x[1]}</Mono><span style={{flex:1}}></span><span style={{fontSize:13, color:V.ink3}}>{x[2]}</span></div>)}
      <H3>Paramètres de /solde/mouvements</H3><Params rows={[['service','string',0,'assurance, billetterie, esim, sms, compte'],['page','integer',0,'1 par défaut'],['par_page','integer',0,'25 par défaut'],['type · depuis','—',0,'Bientôt']]}/></>;
    side = <><Code title="Exemple de réponse · GET /solde" code={J({mode:'test', solde:31889040, devise:'XOF', en_verification:0, seuil_alerte:2000000, plafond_operation:5000000, plafonds_jour:{}, services_actifs:['assurance'], mis_a_jour_le:'2026-09-25T13:12:28+00:00'})} langs={false}/><Code code={ex('GET','/solde/mouvements?service=assurance&par_page=3',null,keyTxt)}/><Code title="Exemple de réponse · 200" h={360} code={J({objet:'liste', page:1, total:105, page_suivante:2, donnees:[{date:'2026-09-25T13:13:16+00:00', service:'assurance', type:'debit', montant:-107905, solde_apres:31781135, libelle:'Assurance auto DK6406TP (providence)', objet:'off_7f9403b2fc674a958010ce16e18a1be2'},{date:'2026-09-25T13:13:00+00:00', service:'assurance', type:'remboursement', montant:65700, solde_apres:31889040, libelle:'Annulation DK6406AB (providence)', objet:CTR}]})} langs={false}/></>;
  } else if(id==='webhooks'){ main = <><Lbl>Webhooks</Lbl><span style={{display:'flex', alignItems:'center', gap:12, flexWrap:'wrap'}}><H s={30}>Format et signature</H></span><Notice tone="ok" icon="webhooks-logo" title="En service">Ajoutez vos URL de réception dans la page Webhooks de la console, une par système, en sandbox comme en production.</Notice><P>Nous envoyons un <C>POST</C> JSON à votre URL. Répondez 2xx en moins de 10 secondes. Sinon, nous réessayons 6 fois sur 24 heures (1 min, 5 min, 30 min, 2 h, 6 h, 24 h).</P>
      <H3>Événements</H3><div style={{display:'flex', flexWrap:'wrap', gap:8}}>{EVENTS.map(e=><Badge key={e[0]} tone="mute">{e[0]}</Badge>)}</div>
      <H3>Vérifier la signature</H3><P>L'en-tête <C>X-LMS-Signature</C> contient <C>t=…,v1=…</C> : un HMAC SHA-256 de <C>t + "." + corps</C> avec votre secret. Refusez les messages de plus de 5 minutes.</P></>;
    side = <><Code title="Corps reçu" code={`{\n  "id": "evt_5Hq1",\n  "type": "contrat.actif",\n  "cree_le": "2026-09-25T13:12:57+00:00",\n  "donnees": {\n    "id": "ctr_4fd9bc4f48e242408b13edc0899311ec",\n    "reference": "PART-2026-640601"\n  }\n}`} langs={false}/><Code code={{JavaScript:`import crypto from "crypto";\n\nconst [t, v1] = sig.split(",").map(p => p.split("=")[1]);\nconst attendu = crypto\n  .createHmac("sha256", process.env.LMS_WEBHOOK_SECRET)\n  .update(t + "." + corps)\n  .digest("hex");\nif (attendu !== v1) return res.status(400).end();`, Python:`import hmac, hashlib\n\nt, v1 = [p.split("=")[1] for p in sig.split(",")]\nattendu = hmac.new(secret.encode(), f"{t}.{corps}".encode(), hashlib.sha256).hexdigest()\nif not hmac.compare_digest(attendu, v1):\n    return 400`, PHP:`[$t, $v1] = array_map(fn($p) => explode("=", $p)[1], explode(",", $sig));\n$attendu = hash_hmac("sha256", "$t.$corps", $secret);\nif (!hash_equals($attendu, $v1)) http_response_code(400);`}}/></>;
  } else if(id==='llm'){ main = <><Lbl>Ressources développeur</Lbl><H s={30}>LLM et assistants de code</H><P>Donnez à votre assistant de code tout le contexte de l'API : un prompt prêt à l'emploi, à ouvrir dans ChatGPT ou Claude, et chaque page copiable en Markdown.</P><LlmBody keyTxt={keyTxt}/></>; side = null;
  } else if(id && id.startsWith('s_')){ const k = id.slice(2), s = SVCS[k]; main = <><div style={{maxWidth:560, borderRadius:28, overflow:'hidden', lineHeight:0}} dangerouslySetInnerHTML={{__html:window.lmsIllus?lmsIllus(k, document.documentElement.dataset.theme==='dark', {soon:1, w:560}):''}}></div><H s={30}>{s.n}</H><Badge>Bientôt</Badge><P>{({voyage:'Devis par zone et durée, souscription, attestation.', billet:'Recherche de vols, tarification, réservation, émission du billet.', esim:'Forfaits data internationaux activables par QR code.', sms:'Envoi unitaire et en masse, avec accusés de réception.'})[k]}</P>
      <Card pad={18} style={{background:V.sub}}><span style={{display:'block', fontWeight:800, fontSize:14}}>Soyez prévenu à l'ouverture</span><Muted style={{marginTop:4}}>Nous écrivons aux contacts techniques des partenaires dès que la sandbox est prête.</Muted></Card></>;
    side = null;
  } else { main = <><H s={30}>Page introuvable</H><P>Cette page de la documentation n'existe pas.</P><div><Btn onClick={()=>go('start')}>Démarrer</Btn></div></>; side = null; }
  return <div style={{display:'grid', gridTemplateColumns:side&&!m?'minmax(0,1fr) minmax(0,.95fr)':'minmax(0,1fr)', gap:m?20:36, alignItems:'start'}}>
    <article className="lpage" style={{display:'flex', flexDirection:'column', gap:16, minWidth:0}}>{main}</article>
    {side && <aside style={{display:'flex', flexDirection:'column', gap:14, position:m?'static':'sticky', top:0, minWidth:0}}>{side}</aside>}</div>;
}
function LlmMenu({id, keyTxt}){
  const [o, setO] = React.useState(false), [c, copy] = useCopy(); const ref = React.useRef(null);
  React.useEffect(()=>{ if(!o) return; const f = e => { if(ref.current && !ref.current.contains(e.target)) setO(false); }; document.addEventListener('mousedown', f); return ()=>document.removeEventListener('mousedown', f); }, [o]);
  const md = mdFor(id), q = encodeURIComponent(`Lis cette page de la documentation LeMultiservice et aide-moi à l'intégrer :\n\n${md.slice(0,3500)}`);
  const mi = (icon, l, sub, fn) => <button key={l} className="lb lr" onClick={()=>{fn(); setO(false);}} style={{display:'flex', alignItems:'center', gap:12, width:'100%', padding:'10px 12px', border:0, borderRadius:12, background:'none', cursor:'pointer', textAlign:'left', fontFamily:V.body}}>
    <Tile icon={icon} s={34} c={V.ink2} bg={V.sub}/><span style={{flex:1}}><span style={{display:'block', fontWeight:700, fontSize:13.5, color:V.ink}}>{l}</span><span style={{display:'block', fontSize:12, color:V.ink3}}>{sub}</span></span></button>;
  return <div ref={ref} style={{position:'relative', display:'flex'}}>
    <button className="lb" onClick={()=>copy(md)} style={{height:36, padding:'0 12px', borderRadius:'12px 0 0 12px', border:`1px solid ${V.line}`, borderRight:0, background:V.card, cursor:'pointer', display:'flex', alignItems:'center', gap:7, fontFamily:V.body, fontWeight:700, fontSize:12.5, color:c?V.accD:V.ink2, whiteSpace:'nowrap'}}><I n={c?'check':'copy'} s={15}/>{c?'Copié en Markdown':'Copier pour un LLM'}</button>
    <button className="lb" aria-label="Plus d'options LLM" aria-expanded={o} onClick={()=>setO(!o)} style={{width:34, height:36, borderRadius:'0 12px 12px 0', border:`1px solid ${V.line}`, background:o?V.sub:V.card, cursor:'pointer', display:'grid', placeItems:'center', color:V.ink2}}><I n="caret-down" s={14}/></button>
    {o && <div style={{position:'absolute', right:0, top:44, width:300, zIndex:40, background:V.card, border:`1px solid ${V.line}`, borderRadius:18, boxShadow:'0 16px 40px rgba(8,20,16,.14)', padding:6, animation:'lpop .15s ease'}}>
      {mi('copy','Copier la page','En Markdown, prête à coller',()=>copy(md))}
      {mi('open-ai-logo','Ouvrir dans ChatGPT','Avec cette page en contexte',()=>window.open('https://chatgpt.com/?q='+q,'_blank','noopener'))}
      {mi('sparkle','Ouvrir dans Claude','Avec cette page en contexte',()=>window.open('https://claude.ai/new?q='+q,'_blank','noopener'))}
    </div>}
  </div>;
}
function RespTabs({e}){
  const codes = [200, ...[...new Set(e[8].map(c=>{ const x = ERRS.find(r=>r[0].split(' · ').includes(c)); return x && x[1]; }).filter(Boolean))]];
  const [i, setI] = React.useState(0), c = codes[i];
  const body = c===200 ? e[7] : (()=>{ const x = ERRS.find(r=>r[1]===c && e[8].some(k=>r[0].split(' · ').includes(k))); const k = e[8].find(k=>x[0].split(' · ').includes(k)); return J({erreur:{code:k, message:x[2]}}); })();
  return <div style={{borderRadius:20, overflow:'hidden', border:'1px solid rgba(255,255,255,.06)'}}>
    <div style={{display:'flex', gap:4, padding:'8px 8px 0', background:'#0E1F19', flexWrap:'wrap'}}><span style={{fontFamily:V.mono, fontSize:12, color:'rgba(255,255,255,.55)', padding:'6px 6px 0', marginRight:4}}>{e[11]?'Exemple de réponse':'Réponse'}</span>
      {codes.map((k,j)=><button key={k} className="lb" onClick={()=>setI(j)} style={{height:28, padding:'0 10px', borderRadius:8, border:0, cursor:'pointer', background:i===j?'rgba(159,224,198,.14)':'transparent', color:i===j?(k<300?'#9FE0C6':k<500?'#F2C679':'#F08A80'):'rgba(255,255,255,.5)', fontFamily:V.mono, fontWeight:700, fontSize:12}}>{k}</button>)}</div>
    <Code code={body} langs={false} h={440}/></div>;
}
// « Essayer » : appel réel de l'API v1 en sandbox, avec la clé lms_test_ collée par le développeur (jamais mémorisée)
function TryIt({e, onClose}){
  const [chemin, setChemin] = React.useState(e[9]), [body, setBody] = React.useState(e[6]?J(e[6]):''), [key, setKey] = React.useState(''), [st, setSt] = React.useState(0), [res, setRes] = React.useState(null);
  const envoyer = async () => { setSt(1); setRes(null); const t0 = performance.now();
    try { let corps; if(e[1]!=='GET' && body.trim()){ try { corps = JSON.stringify(JSON.parse(body)); } catch(x){ setRes({err:'Corps JSON invalide : ' + x.message}); setSt(2); return; } }
      const r = await fetch(BASE + chemin, {method:e[1], headers:{Authorization:`Bearer ${key.trim()}`, ...(corps ? {'Content-Type':'application/json'} : {})}, body:corps});
      const txt = await r.text(); let js; try { js = J(JSON.parse(txt)); } catch(x){ js = txt || '(corps vide)'; }
      setRes({http:r.status, ms:Math.round(performance.now()-t0), corps:js}); }
    catch(x){ setRes({err:`Appel impossible depuis le navigateur (${x.message}). Utilisez l'exemple cURL depuis votre serveur.`}); }
    setSt(2); };
  const ok = /^lms_test_[A-Za-z0-9]+$/.test(key.trim());
  return <Modal title="Essayer l'endpoint" icon="play" onClose={onClose} w={760}>
    <div style={{display:'flex', alignItems:'center', gap:10, padding:'10px 12px', borderRadius:14, background:V.sub, flexWrap:'wrap'}}><Method m={e[1]}/><Mono c={V.ink} s={13}>{e[2]}</Mono><span style={{flex:1}}></span><Badge tone="warn" icon="flask">Sandbox</Badge></div>
    <Field label="Authorization" hint="Collez une clé lms_test_… (créée dans Clés d'API). Elle n'est pas enregistrée ; elle part uniquement vers l'API."><Inp value={key} onChange={x=>setKey(x.target.value)} mono placeholder="lms_test_…" autoComplete="off" lead={<span style={{fontSize:12, fontWeight:700}}>Bearer</span>} style={{paddingLeft:72}}/></Field>
    <Field label="Chemin" hint={`Relatif à ${BASE}`}><Inp value={chemin} onChange={x=>setChemin(x.target.value)} mono/></Field>
    {e[1]!=='GET' && <Field label="Corps JSON"><textarea className="li" value={body} onChange={x=>setBody(x.target.value)} rows={9} spellCheck={false} style={{...inpCss, height:'auto', padding:12, resize:'vertical', fontFamily:V.mono, fontSize:12.5, lineHeight:1.6}}></textarea></Field>}
    <div style={{display:'flex', gap:8, alignItems:'center', flexWrap:'wrap'}}><Btn icon={st===1?'spinner':'paper-plane-tilt'} disabled={st===1 || !ok} onClick={envoyer}>{st===1?'Envoi…':'Envoyer la requête'}</Btn>{key && !ok && <span style={{fontSize:12.5, color:V.bad, fontWeight:600}}>Utilisez une clé sandbox lms_test_….</span>}</div>
    {res && (res.err ? <Notice tone="bad">{res.err}</Notice> : <div style={{display:'flex', flexDirection:'column', gap:8, animation:'lfade .2s ease'}}><div style={{display:'flex', alignItems:'center', gap:8}}><Http code={res.http}/><Mono s={12}>{fmt(res.ms)} ms</Mono></div><Code code={res.corps} langs={false} h={280}/></div>)}
  </Modal>;
}
function LlmBody({keyTxt}){
  const prompt = llmPrompt(keyTxt), q = encodeURIComponent(prompt);
  return <div style={{display:'flex', flexDirection:'column', gap:16}}>
    <Code title="Prompt de démarrage" code={prompt} langs={false} h={260}/>
    <div style={{display:'flex', gap:8, flexWrap:'wrap'}}><Btn icon="open-ai-logo" onClick={()=>window.open('https://chatgpt.com/?q='+q,'_blank','noopener')}>Ouvrir dans ChatGPT</Btn><Btn kind="secondary" icon="sparkle" onClick={()=>window.open('https://claude.ai/new?q='+q,'_blank','noopener')}>Ouvrir dans Claude</Btn><CopyBtn text={prompt} sm={false}/></div>
    <Notice tone="warn" icon="flask">Remplacez lms_test_VOTRE_CLE par une clé <b>sandbox</b>. Ne collez jamais une clé lms_live_… dans un assistant.</Notice>
    <Muted style={{fontSize:12.5}}>Sur chaque page de la documentation, « Copier pour un LLM » copie la page en Markdown.</Muted>
  </div>;
}
function LlmSetup({keyTxt, onClose}){
  return <Modal title="Configurer avec un LLM" icon="sparkle" onClose={onClose} w={680}><Muted>Donnez à votre assistant (ChatGPT, Claude, Cursor, Copilot…) tout le contexte de l'API en un clic.</Muted><LlmBody keyTxt={keyTxt}/></Modal>;
}
// connected : console ouverte (retour au tableau de bord) ; sinon accès public depuis l'écran de connexion
function Docs({connected, id:id0, onPick, go, onApp, prefixe}){
  const m = useMob(); const [idL, setIdL] = React.useState('start'), [q, setQ] = React.useState(''), [shut, setShut] = React.useState({}), [llm, setLlm] = React.useState(false), [tryE, setTry] = React.useState(null), [toc, setToc] = React.useState([]);
  const id = id0 == null ? idL : ALL.some(x=>x.id===id0) ? id0 : 'start';
  const scRef = React.useRef(null); const [hasSide, setHasSide] = React.useState(false);
  React.useEffect(()=>{ const el = scRef.current; if(!el) return; el.scrollTop = 0; const hs = [...el.querySelectorAll('article h3')]; hs.forEach((h,i)=>h.id='toc-'+i); setToc(hs.map((h,i)=>[h.id, h.textContent])); setHasSide(!!el.querySelector('article + aside')); },[id]);
  const keyTxt = 'lms_test_VOTRE_CLE';
  const cur = ALL.find(x=>x.id===id) || ALL[0], tab = DTABS.find(t=>t[0]===cur.tab);
  const pick = k => { setQ(''); if(onPick) onPick(k); else setIdL(k); };
  const hits = q ? ALL.filter(x=>(x.l+' '+x.grp+' '+x.tabL).toLowerCase().includes(q.toLowerCase())) : null;
  const item = x => <button key={x[0]} className="lb ln" onClick={()=>pick(x[0])} aria-current={id===x[0]?'page':undefined} style={{display:'flex', alignItems:'center', gap:8, minHeight:34, padding:'6px 10px 6px 28px', border:0, borderRadius:10, cursor:'pointer', textAlign:'left', background:id===x[0]?V.mint:'transparent', color:id===x[0]?V.accD:V.ink2, fontFamily:V.body, fontWeight:id===x[0]?700:600, fontSize:13}}>
    {x[2] && <Method m={x[2]}/>}<span style={{flex:1}}>{x[1]}</span>{x[3] && <span style={{fontSize:10.5, fontWeight:700, color:V.warn}}>{x[3]}</span>}</button>;
  const nav = <nav aria-label={`Documentation · ${tab[1]}`} style={{display:'flex', flexDirection:'column', gap:6}}>
    {hits ? <>{hits.map(x=><button key={x.id} className="lb ln" onClick={()=>{pick(x.id);}} style={{display:'flex', flexDirection:'column', alignItems:'flex-start', gap:2, padding:'8px 10px', border:0, borderRadius:10, cursor:'pointer', textAlign:'left', background:'transparent', fontFamily:V.body}}>
        <span style={{display:'flex', alignItems:'center', gap:8, fontWeight:700, fontSize:13, color:V.ink}}>{x.m && <Method m={x.m}/>}{x.l}</span><span style={{fontSize:11.5, color:V.ink3}}>{x.tabL} › {x.grp}</span></button>)}{!hits.length && <Muted style={{padding:10}}>Aucun résultat.</Muted>}</>
    : tab[3].map(([g, items])=>{ const k = tab[0]+g, open = !shut[k];
      return <div key={k}>
        <button className="lb ln" aria-expanded={open} onClick={()=>setShut({...shut,[k]:open})} style={{display:'flex', alignItems:'center', gap:8, width:'100%', height:34, padding:'0 10px', border:0, borderRadius:10, background:'none', cursor:'pointer', fontFamily:V.body}}>
          <I n="caret-right" s={13} c={V.ink3} style={{transform:open?'rotate(90deg)':'none', transition:'transform .2s'}}/><span style={{flex:1, textAlign:'left', fontWeight:800, fontSize:11, letterSpacing:'.1em', textTransform:'uppercase', color:V.ink3}}>{g}</span><span style={{fontSize:11, fontWeight:700, color:V.ink3}}>{items.length}</span></button>
        {open && <div style={{display:'flex', flexDirection:'column', gap:1, marginTop:2, animation:'lfade .2s ease'}}>{items.map(item)}</div>}</div>; })}
  </nav>;
  const flat = tab[3].flatMap(g=>g[1]);
  const aller2 = k => k.startsWith('@try:') ? setTry(EP.find(x=>x[0]===k.slice(5))) : k.startsWith('@') ? go(k) : pick(k);
  return <div style={{display:'flex', flexDirection:'column', height:'100%', width:'100%', background:V.card, position:'relative'}}>
    <header style={{height:m?56:64, flex:'none', display:'flex', alignItems:'center', gap:m?8:12, padding:m?'0 14px':'0 24px', borderBottom:`1px solid ${V.line}`}}>
      <Brand sub="Docs"/><span style={{flex:1}}></span>
      {!m && <div style={{width:280}}><Inp value={q} onChange={e=>setQ(e.target.value)} placeholder="Rechercher dans la doc" aria-label="Rechercher dans la documentation" lead={<I n="magnifying-glass" s={16}/>} style={{height:38, fontSize:13, background:V.sub}}/></div>}
      <Btn kind="soft" sm icon="sparkle" label="Configurer avec un LLM" onClick={()=>setLlm(true)}>{m?null:'Configurer avec un LLM'}</Btn>
      {connected ? <Btn kind="secondary" sm icon="squares-four" label="Tableau de bord" onClick={onApp}>{m?null:'Tableau de bord'}</Btn> : <><Btn kind="ghost" sm onClick={()=>go('@login')}>Se connecter</Btn>{!m && <Btn sm onClick={()=>go('@signup')}>Créer un compte</Btn>}</>}
    </header>
    <div role="tablist" aria-label="Sections" style={{flex:'none', display:'flex', gap:2, padding:m?'0 8px':'0 18px', borderBottom:`1px solid ${V.line}`, overflowX:'auto'}}>
      {DTABS.map(t=>{ const on = t[0]===cur.tab;
        return <button key={t[0]} role="tab" aria-selected={on} className="lb" onClick={()=>pick(t[3][0][1][0][0])} style={{height:44, padding:'0 12px', border:0, background:'none', cursor:'pointer', display:'flex', alignItems:'center', gap:7, whiteSpace:'nowrap', borderBottom:`2px solid ${on?V.acc:'transparent'}`, marginBottom:-1, color:on?V.ink:V.ink3, fontFamily:V.body, fontWeight:700, fontSize:13.5}}>
          <I n={t[2]} s={17} fill={on} c={on?V.accD:undefined}/>{t[1]}</button>; })}
    </div>
    {connected && <div style={{flex:'none', display:'flex', alignItems:'center', justifyContent:'center', gap:8, minHeight:32, padding:'4px 12px', background:V.mint, color:V.accD, fontWeight:700, fontSize:12.5, textAlign:'center', flexWrap:'wrap'}}><I n="key" s={15}/>Dans les exemples, remplacez <Mono c={V.accD}>lms_test_VOTRE_CLE</Mono> par votre clé sandbox{prefixe ? <> (<Mono c={V.accD}>{prefixe}…</Mono>)</> : ''}</div>}
    <div style={{flex:1, minHeight:0, display:'flex'}}>
      {!m && <div style={{width:268, flex:'none', overflowY:'auto', padding:'16px 12px', borderRight:`1px solid ${V.line}`}}>{nav}</div>}
      <div ref={scRef} style={{flex:1, minWidth:0, overflowY:'auto', background:V.card, display:'flex'}}><div key={id} style={{flex:1, minWidth:0, maxWidth:1160, margin:'0 auto', padding:m?'16px 16px 40px':'22px 40px 60px'}}>
        {m && <div style={{marginBottom:14, display:'flex', flexDirection:'column', gap:8}}><Inp value={q} onChange={e=>setQ(e.target.value)} placeholder="Rechercher dans la doc" aria-label="Rechercher dans la documentation" lead={<I n="magnifying-glass" s={16}/>} style={{height:40, background:V.sub}}/>{hits ? nav : <Sel label="Page" opts={flat.map(x=>[x[0], x[1]])} value={flat.some(x=>x[0]===id)?id:flat[0][0]} onChange={v=>pick(v)}/>}</div>}
        <div style={{display:'flex', alignItems:'center', gap:10, marginBottom:18, flexWrap:'wrap'}}>
          <span style={{flex:1, minWidth:0, display:'flex', alignItems:'center', gap:6, fontSize:12.5, fontWeight:600, color:V.ink3, flexWrap:'wrap'}}>{tab[1]}<I n="caret-right" s={12}/>{cur.grp}<I n="caret-right" s={12}/><span style={{color:V.ink}}>{cur.l}</span></span>
          <LlmMenu id={id} keyTxt={keyTxt}/></div>
        <DocsPage id={id} keyTxt={keyTxt} connecte={connected} go={aller2}/>
        {(()=>{ const i = flat.findIndex(x=>x[0]===id), p = flat[i-1], n = flat[i+1]; return (p||n) && <div style={{display:'flex', gap:12, marginTop:36, paddingTop:20, borderTop:`1px solid ${V.line}`}}>
          {p ? <button className="lb lc" onClick={()=>pick(p[0])} style={{flex:1, padding:'14px 16px', borderRadius:16, border:`1px solid ${V.line}`, background:V.card, cursor:'pointer', textAlign:'left', fontFamily:V.body}}><span style={{display:'block', fontSize:11.5, color:V.ink3, fontWeight:600}}>← Précédent</span><span style={{display:'block', fontWeight:700, fontSize:14, color:V.ink, marginTop:2}}>{p[1]}</span></button> : <span style={{flex:1}}></span>}
          {n ? <button className="lb lc" onClick={()=>pick(n[0])} style={{flex:1, padding:'14px 16px', borderRadius:16, border:`1px solid ${V.line}`, background:V.card, cursor:'pointer', textAlign:'right', fontFamily:V.body}}><span style={{display:'block', fontSize:11.5, color:V.ink3, fontWeight:600}}>Suivant →</span><span style={{display:'block', fontWeight:700, fontSize:14, color:V.ink, marginTop:2}}>{n[1]}</span></button> : <span style={{flex:1}}></span>}</div>; })()}
      </div>
      {!m && toc.length>1 && !hasSide && <aside style={{width:220, flex:'none', padding:'28px 20px', position:'sticky', top:0, alignSelf:'flex-start'}}><Lbl style={{fontSize:10.5, marginBottom:10}}>Sur cette page</Lbl>
        {toc.map(t=><button key={t[0]} className="lb" onClick={()=>{ const el = document.getElementById(t[0]); if(el && scRef.current) scRef.current.scrollTop = el.offsetTop - 80; }} style={{display:'block', width:'100%', padding:'5px 0 5px 10px', border:0, borderLeft:`2px solid ${V.line}`, background:'none', cursor:'pointer', textAlign:'left', fontFamily:V.body, fontWeight:600, fontSize:12.5, color:V.ink3}}>{t[1]}</button>)}</aside>}
      </div>
    </div>
    {llm && <LlmSetup keyTxt={keyTxt} onClose={()=>setLlm(false)}/>}
    {tryE && <TryIt e={tryE} onClose={()=>setTry(null)}/>}
  </div>;
}
Object.assign(window, {Docs, EP, ERRS, BASE});
