SPB Git forge

spb/crea-ka

Public

Créa·Ka — annuaire public cross-plateforme des créateurs de contenu québécois (crea-ka.com)

52commits 1branches 0releases
11.3 MBsize
maindefault branch
19 days agolast push
Python 73.6% HTML 13.2% TypeScript 6% JavaScript 4.5% CSS 1.7% Dockerfile 0.6%
121.4 KB · 1,904 lines html
Raw Blame History
1<!--2  Author: Simon-Pierre Boucher <contact@spboucher.ai>3  File:   frontend/src/index.template.html4  Desc:   Gabarit du frontend Créa-Ka — marqueurs remplacés par5          scripts/build_frontend.py : __ICONS__ (tracés SVG simple-icons, CC0),6          __KA_TOKENS__ (ka/tokens.css — design system commun Groupe KA,7          importé EN PREMIER) et __KA_ECO__ (ka/ecosystem.json — contacts,8          sites, mentions légales du groupe : footer commun + page /contact).9          Système « éditorial sharp » du Groupe KA (papier grainé #f5f3ee,10          encre #141814, bordures encre + ombres décalées dures, Space Grotesk11          / Inter / JetBrains Mono) — accent attribué à Créa-Ka : VIOLET12          (--accent:#7048e8). Patrons mobiles calqués sur Lou-Ka (tab bar13          4 onglets, filtres défilants, anti-zoom iOS).14          Routes : / · /createur/{id} · /stats · /favoris · /retrait · /compte · /contact15-->16<!DOCTYPE html>17<html lang="fr-CA">18<head>19<meta charset="utf-8">20<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">21<meta name="theme-color" content="#141814">22<link rel="preconnect" href="https://fonts.googleapis.com">23<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>24<link rel="preconnect" href="https://unavatar.io">25<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">26<link rel="icon" type="image/svg+xml" href="/favicon.svg">27<link rel="apple-touch-icon" href="/apple-touch-icon.png">28<!--KA:SEO-->29<title>Créa-Ka — Annuaire des créateurs de contenu québécois</title>30<meta name="description" content="Annuaire public des créateurs de contenu et influenceurs québécois : tous leurs comptes (Instagram, TikTok, YouTube, Twitch…) regroupés, cherchables par niche, région, langue et taille d'audience.">31<link rel="canonical" href="https://www.crea-ka.com/">32<meta property="og:site_name" content="Créa-Ka">33<meta property="og:locale" content="fr_CA">34<meta property="og:type" content="website">35<meta property="og:title" content="Créa-Ka — Annuaire des créateurs de contenu québécois">36<meta property="og:description" content="Annuaire public des créateurs de contenu et influenceurs québécois : tous leurs comptes (Instagram, TikTok, YouTube, Twitch…) regroupés, cherchables par niche, région, langue et taille d'audience.">37<meta property="og:url" content="https://www.crea-ka.com/">38<meta property="og:image" content="https://www.crea-ka.com/og.png">39<meta property="og:image:width" content="1200">40<meta property="og:image:height" content="630">41<meta name="twitter:card" content="summary_large_image">42<meta name="twitter:title" content="Créa-Ka — Annuaire des créateurs de contenu québécois">43<meta name="twitter:description" content="Annuaire public des créateurs de contenu et influenceurs québécois : tous leurs comptes (Instagram, TikTok, YouTube, Twitch…) regroupés, cherchables par niche, région, langue et taille d'audience.">44<meta name="twitter:image" content="https://www.crea-ka.com/og.png">45<script type="application/ld+json">{"@context":"https://schema.org","@graph":[{"@type":"WebSite","@id":"https://www.crea-ka.com/#website","url":"https://www.crea-ka.com/","name":"Créa-Ka","description":"Annuaire public des créateurs de contenu et influenceurs québécois : tous leurs comptes (Instagram, TikTok, YouTube, Twitch…) regroupés, cherchables par niche, région, langue et taille d'audience.","inLanguage":"fr-CA","publisher":{"@id":"https://www.crea-ka.com/#org"},"potentialAction":{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https://www.crea-ka.com/?q={search_term_string}"},"query-input":"required name=search_term_string"}},{"@type":"Organization","@id":"https://www.crea-ka.com/#org","name":"Créa-Ka","url":"https://www.crea-ka.com/","logo":"https://www.crea-ka.com/og.png","email":"contact@groupe-ka.com","parentOrganization":{"@type":"Organization","name":"Groupe KA","url":"https://www.groupe-ka.com"}}]}</script>46<!--/KA:SEO-->47<style>48/* ka/tokens.css — design system commun Groupe KA, importé EN PREMIER49   (injecté au build par scripts/build_frontend.py) */50__KA_TOKENS__51</style>52<style>53/* Surcharges Créa-Ka par-dessus ka/tokens.css. Convention du groupe : les54   NOMS de variables sont partagés (--accent/--lime), seules les VALEURS55   changent — accent attribué à Créa-Ka : VIOLET. */56:root{57  --accent:#7048e8; --accent-soft:#ece5fc; --accent-deep:#5433b8; --on-accent:#ffffff;58  /* alias rétro-compatibles (le CSS historique du site utilise --lime) */59  --lime:var(--accent); --lime-soft:var(--accent-soft);60  --green:var(--accent-deep);   /* primaire du site = violet profond */61  --green-deep:#3d2178;62}63/* le layout du site est calibré sur des titres sans marge basse */64h1,h2,h3,h4{margin:0}65a{text-decoration:none} button{font-family:inherit;cursor:pointer}66.icn{width:1em;height:1em;display:block;fill:currentColor}67.container{max-width:1240px;margin:0 auto;padding:0 24px}68@media(max-width:640px){.container{padding:0 16px}}6970/* ---------- cartes & boutons (langage du groupe) ---------- */71.gk-card{background:var(--surface);border:1.5px solid var(--ink);72  border-radius:var(--r-card);box-shadow:var(--shadow-off-soft)}73.gk-hover{transition:transform .18s ease,box-shadow .18s ease}74.gk-hover:hover{transform:translate(-3px,-3px);box-shadow:var(--shadow-off)}75.btn:active{transform:translate(2px,2px);box-shadow:none!important}76.btn-primary{box-shadow:4px 4px 0 rgba(20,24,20,.25)}7778/* ===== header ===== */79.header{position:sticky;top:0;z-index:var(--z-header,500);background:var(--paper);80  border-bottom:1.5px solid var(--ink)}81.header-inner{display:flex;align-items:center;gap:16px;height:64px}82.brand{font-family:var(--font-display);font-weight:700;font-size:23px;letter-spacing:-.03em;83  display:flex;align-items:center;line-height:1;color:var(--ink)}84.brand svg{margin-right:11px;flex:none}85@media(pointer:coarse){.brand{position:relative}.brand::after{content:"";position:absolute;inset:-8px}}86.brand .ka{background:var(--accent);color:var(--on-accent);border-radius:6px;padding:0 6px 1px;margin-left:2px;transform:rotate(-1deg)}87.brand-tag{font-family:var(--font-mono);font-size:10px;font-weight:500;color:var(--ink-3);88  line-height:1.5;text-transform:uppercase;letter-spacing:.08em}89@media(max-width:1040px){.brand-tag{display:none}}90.header-spacer{flex:1}91.hbtn{border:1.5px solid var(--ink);background:var(--surface);color:var(--ink);92  border-radius:var(--r-ctl);padding:8px 16px;min-height:44px;font-family:var(--font-display);93  font-size:13px;font-weight:700;transition:.15s;display:inline-flex;align-items:center;94  gap:8px;white-space:nowrap;box-shadow:3px 3px 0 rgba(20,24,20,.12)}95.hbtn:hover{background:var(--accent);color:var(--on-accent)}96.hbtn:active{transform:translate(2px,2px);box-shadow:none}97.hbtn.primary{background:var(--ink);color:var(--accent)}98.hbtn.primary:hover{background:var(--accent-deep);color:var(--on-accent)}99@media(max-width:940px){.header .hbtn:not(.acct){display:none}}100/* bouton / jeton de compte KA ID — toujours visible, même sur mobile */101.hbtn.acct{padding:7px 14px}102.hbtn.acct img{width:22px;height:22px;border-radius:50%;object-fit:cover;103  border:1.5px solid var(--ink);margin-left:-4px}104.hbtn.acct .ka-badge{background:var(--accent);color:var(--on-accent);border-radius:4px;padding:0 5px;105  font-family:var(--font-mono);font-size:10px;font-weight:700}106.hbtn:hover .ka-badge{background:var(--on-accent);color:var(--accent)}107/* badge « Un service Groupe KA » (ka/GroupeKaBadge — styles dans tokens.css) */108.header .gk-badge{flex:none}109.hbtn .acct-short{display:none}110@media(max-width:560px){111  .header-inner{gap:10px}112  .header .gk-badge .pfx{display:none}113  .hbtn .acct-long{display:none}.hbtn .acct-short{display:inline}}114@media(max-width:400px){.brand svg{width:26px;height:26px;margin-right:8px}.brand{font-size:20px}.header .gk-badge{display:none}}115116/* ===== page compte (carte de membre Groupe KA) ===== */117.compte-page{max-width:640px;margin:0 auto;padding:44px 0 70px}118.membercard{background:var(--ink);border:1.5px solid var(--ink);border-radius:14px;119  padding:28px;color:var(--paper);box-shadow:var(--shadow-off);margin-top:24px;120  position:relative;overflow:hidden}121.membercard::after{content:"";position:absolute;right:-40px;top:-40px;width:160px;122  height:160px;border-radius:50%;background:rgba(112,72,232,.18)}123.mc-brand{font-family:var(--font-mono);font-size:10px;font-weight:700;124  text-transform:uppercase;letter-spacing:.14em;color:var(--accent-soft)}125.mc-row{display:flex;gap:18px;align-items:center;margin-top:18px}126.mc-avatar{width:72px;height:72px;border-radius:14px;border:2px solid var(--accent);127  background:var(--green-deep);overflow:hidden;flex:none;display:flex;128  align-items:center;justify-content:center;font-family:var(--font-display);129  font-weight:700;font-size:26px;color:var(--accent-soft)}130.mc-avatar img{width:100%;height:100%;object-fit:cover}131.mc-name{font-family:var(--font-display);font-size:22px;font-weight:700;color:var(--paper)}132.mc-kaid{font-family:var(--font-mono);font-size:12px;color:var(--accent-soft);margin-top:2px;letter-spacing:.04em}133.mc-role{display:inline-flex;background:var(--accent);color:var(--on-accent);border-radius:5px;134  padding:2px 10px;font-family:var(--font-mono);font-size:10px;font-weight:700;135  text-transform:uppercase;letter-spacing:.06em;margin-top:8px}136.mc-meta{margin-top:20px;display:grid;gap:6px;font-size:12.5px;color:rgba(245,243,238,.75)}137.mc-meta b{color:var(--paper);font-weight:600}138.compte-actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:24px}139140/* ===== héros ===== */141.hero{padding:54px 0 26px}142@media(max-width:640px){.hero{padding:30px 0 12px}}143.hero h1{font-size:clamp(30px,5.4vw,54px);font-weight:700;max-width:820px;line-height:1.06;144  text-transform:uppercase}145.hero p.lead{color:var(--ink-2);font-size:clamp(14.5px,2vw,16.5px);max-width:640px;margin:18px 0 0}146.stats-row{display:flex;gap:10px;flex-wrap:wrap;margin-top:24px}147.stat-pill{background:var(--surface);border:1.5px solid var(--ink);border-radius:var(--r-ctl);148  padding:8px 16px;font-family:var(--font-mono);font-size:11px;font-weight:700;149  text-transform:uppercase;letter-spacing:.06em;color:var(--ink-2);150  display:inline-flex;gap:8px;align-items:center;box-shadow:3px 3px 0 rgba(20,24,20,.1)}151.stat-pill b{font-family:var(--font-display);color:var(--ink);font-size:16px;letter-spacing:0}152.advantages{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin:32px 0 8px}153@media(max-width:900px){.advantages{grid-template-columns:repeat(2,1fr);gap:10px}}154.adv{background:var(--surface);border:1.5px solid var(--ink);border-radius:var(--r-card);155  padding:16px;box-shadow:var(--shadow-off-soft)}156.adv .ico{width:36px;height:36px;border-radius:8px;border:1.5px solid var(--ink);157  background:var(--accent);color:var(--on-accent);display:flex;align-items:center;158  justify-content:center;margin-bottom:10px;font-size:15px}159.adv:nth-child(2n) .ico{color:var(--ink)}160.adv:nth-child(2n) .ico{background:var(--amber-soft)}161.adv h3{font-size:14px;margin-bottom:3px}162.adv p{margin:0;font-size:12px;color:var(--ink-3);line-height:1.45}163164/* ===== recherche + filtres ===== */165.searchbar{position:sticky;top:64px;z-index:var(--z-sticky,300);background:rgba(245,243,238,.94);166  backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);167  padding:12px 0;border-bottom:1.5px solid var(--ink)}168.search-wrap{display:flex;gap:10px;align-items:center}169.search-input{flex:1 1 auto;display:flex;align-items:center;gap:10px;background:var(--surface);170  border:1.5px solid var(--ink);border-radius:var(--r-ctl);padding:10px 15px;171  transition:.15s;min-width:0;box-shadow:3px 3px 0 rgba(20,24,20,.08)}172.search-input:focus-within{box-shadow:3px 3px 0 rgba(112,72,232,.35)}173.search-input input{border:0;background:none;outline:none;font:inherit;flex:1;color:var(--ink);min-width:0}174.search-input svg{flex:none;color:var(--ink-3)}175.filters{display:flex;gap:8px;flex-wrap:wrap}176select.f{appearance:none;-webkit-appearance:none;border:1.5px solid var(--ink);177  background:var(--surface) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23141814' stroke-width='1.8' fill='none' stroke-linecap='round'/%3E%3C/svg%3E") no-repeat right 12px center;178  border-radius:var(--r-ctl);padding:9px 30px 9px 14px;font-family:var(--font-display);179  font-size:13px;font-weight:700;color:var(--ink-2);max-width:170px;180  box-shadow:3px 3px 0 rgba(20,24,20,.08)}181select.f:focus{outline:2px solid var(--green);outline-offset:1px}182select.f.active{background-color:var(--accent-soft);color:var(--ink);border-color:var(--accent-deep)}183@media(max-width:760px){184  .search-wrap{flex-wrap:wrap}185  .search-input{flex-basis:100%}186  .search-input input{font-size:16px}187  .filters{flex-wrap:nowrap;overflow-x:auto;-webkit-overflow-scrolling:touch;188    margin:0 -16px;padding:4px 16px 6px;scrollbar-width:none;width:calc(100% + 32px)}189  .filters::-webkit-scrollbar{display:none}190  select.f{flex:none;font-size:16px;padding:8px 30px 8px 13px}191}192193/* ===== grille créateurs ===== */194.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:18px;padding:24px 0 10px}195@media(max-width:640px){.grid{grid-template-columns:repeat(2,1fr);gap:12px}}196@media(max-width:420px){.grid{grid-template-columns:1fr;gap:14px}}197.card{background:var(--surface);border:1.5px solid var(--ink);border-radius:var(--r-card);198  overflow:hidden;box-shadow:var(--shadow-off-soft);transition:transform .18s ease,box-shadow .18s ease;199  display:flex;flex-direction:column}200.card:hover{transform:translate(-3px,-3px);box-shadow:var(--shadow-off)}201.card-photo{position:relative;aspect-ratio:1/1;background:202  repeating-linear-gradient(-45deg,var(--green-deep) 0 14px,#33195f 14px 28px);203  display:flex;align-items:center;justify-content:center;overflow:hidden;204  border-bottom:1.5px solid var(--ink)}205.card-photo .init{color:var(--accent-soft);font-family:var(--font-display);font-weight:700;206  font-size:46px;letter-spacing:.02em}207.card-photo img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}208.card-photo .kc-reco{position:absolute;bottom:8px;left:8px;padding:2px 8px 3px;border-radius:5px;209  background:var(--ink);color:var(--accent);font-family:var(--font-mono);font-size:9px;210  font-weight:700;text-transform:uppercase;letter-spacing:.07em;z-index:2}211.tier{position:absolute;top:10px;left:10px;z-index:2}212.card-photo .nplat{position:absolute;top:10px;right:10px;z-index:2;background:var(--ink);213  color:var(--accent-soft);border:1.5px solid var(--ink);border-radius:var(--r-ctl);214  padding:2px 8px;font-family:var(--font-mono);font-size:9.5px;font-weight:700;215  text-transform:uppercase;letter-spacing:.05em}216.tier{display:inline-flex;padding:2px 9px;border:1.5px solid var(--ink);217  border-radius:var(--r-ctl);font-family:var(--font-mono);font-size:9.5px;218  font-weight:700;text-transform:uppercase;letter-spacing:.06em;background:var(--surface);color:var(--ink)}219.tier.mega{background:var(--accent);color:var(--on-accent)}220.tier.macro{background:var(--amber-soft)}221.card-body{padding:14px 15px 15px;display:flex;flex-direction:column;gap:9px;flex:1}222.card-name{font-family:var(--font-display);font-size:16px;font-weight:700;223  letter-spacing:-.01em;line-height:1.25;display:flex;align-items:center;gap:6px}224.vbadge{color:var(--green);flex:none;width:15px;height:15px}225.card-sub{font-family:var(--font-mono);font-size:10px;color:var(--ink-3);226  text-transform:uppercase;letter-spacing:.05em;display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin-top:-4px}227.chips{display:flex;gap:6px;flex-wrap:wrap}228.chip{background:var(--surface-2);border:1px solid var(--line);border-radius:var(--r-ctl);229  padding:2px 10px;font-family:var(--font-body);font-size:11px;font-weight:600;color:var(--ink-2);230  white-space:nowrap;text-transform:none;letter-spacing:0}231.reach{font-size:12px;color:var(--ink-2)}232.reach b{font-family:var(--font-display);color:var(--ink);font-size:15.5px;font-weight:700}233.platrow{display:flex;gap:6px;flex-wrap:wrap;margin-top:auto;padding-top:2px}234.plat{width:32px;height:32px;border-radius:8px;border:1.5px solid var(--ink);235  display:flex;align-items:center;justify-content:center;color:#fff;transition:.15s;236  box-shadow:2px 2px 0 rgba(20,24,20,.25)}237.plat svg{width:15px;height:15px;fill:currentColor}238.plat:hover{transform:translate(-1px,-1px);box-shadow:3px 3px 0 var(--ink)}239/* ===== favoris ♥ « Mon univers Ka » (hub groupe-ka.com) ===== */240.favbtn{position:absolute;right:10px;bottom:10px;z-index:2;width:36px;height:36px;241  border-radius:50%;border:1.5px solid var(--ink);background:var(--surface);242  color:var(--ink-2);display:flex;align-items:center;justify-content:center;243  padding:0;box-shadow:2px 2px 0 rgba(20,24,20,.25);transition:.15s}244.favbtn svg{width:17px;height:17px;fill:none;stroke:currentColor;stroke-width:2;245  stroke-linecap:round;stroke-linejoin:round}246.favbtn:hover{transform:translate(-1px,-1px);box-shadow:3px 3px 0 var(--ink);color:var(--accent-deep)}247.favbtn:active{transform:translate(1px,1px);box-shadow:none}248.favbtn.on{background:var(--accent);color:var(--on-accent)}249.favbtn.on svg{fill:currentColor}250.favbtn[disabled]{opacity:.55;pointer-events:none}251.fav-cta{display:inline-flex;align-items:center;gap:9px;border:1.5px solid var(--ink);252  background:var(--surface);color:var(--ink);border-radius:var(--r-ctl);253  padding:10px 18px;min-height:44px;font-family:var(--font-display);font-size:13px;254  font-weight:700;transition:.15s;box-shadow:3px 3px 0 rgba(20,24,20,.25);cursor:pointer}255.fav-cta svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:2;256  stroke-linecap:round;stroke-linejoin:round;flex:none}257.fav-cta:hover{background:var(--accent);color:var(--on-accent)}258.fav-cta:active{transform:translate(2px,2px);box-shadow:none}259.fav-cta.on{background:var(--accent);color:var(--on-accent)}260.fav-cta.on svg{fill:currentColor}261.fav-cta[disabled]{opacity:.55;pointer-events:none}262.fav-empty{padding:50px 0;text-align:center;color:var(--ink-3)}263.fav-empty h2{text-transform:uppercase;color:var(--ink);margin-bottom:10px}264.favoris-page{padding:40px 0 60px}265.favoris-page .kicker+h1{font-size:clamp(26px,4.4vw,42px);margin:12px 0 0;text-transform:uppercase}266.favoris-page .lead{color:var(--ink-2);margin:14px 0 0;max-width:620px}267.more-wrap{display:flex;justify-content:center;padding:16px 0 44px}268.empty{padding:60px 0;text-align:center;color:var(--ink-3);grid-column:1/-1}269.count-line{font-family:var(--font-mono);font-size:11px;text-transform:uppercase;270  letter-spacing:.08em;color:var(--ink-3);padding-top:18px}271.count-line b{color:var(--ink)}272273/* ===== page créateur ===== */274.cpage{padding:30px 0 60px}275@media(max-width:640px){.cpage{padding:20px 0 40px}}276.backlink{display:inline-flex;align-items:center;gap:8px;font-family:var(--font-mono);277  font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;278  color:var(--green);margin-bottom:20px}279.cpage-head{display:flex;gap:26px;align-items:center;background:var(--ink);280  border:1.5px solid var(--ink);border-radius:var(--r-card);padding:32px;281  color:var(--paper);box-shadow:var(--shadow-off);flex-wrap:wrap}282@media(max-width:640px){.cpage-head{padding:22px 18px;gap:16px}}283.cavatar{width:104px;height:104px;border-radius:var(--r-card);284  background:repeating-linear-gradient(-45deg,var(--green-deep) 0 14px,#33195f 14px 28px);285  color:var(--accent-soft);flex:none;display:flex;align-items:center;justify-content:center;286  font-family:var(--font-display);font-weight:700;font-size:36px;overflow:hidden;287  position:relative;border:2px solid var(--accent)}288@media(max-width:640px){.cavatar{width:76px;height:76px;font-size:26px}}289.cavatar img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}290.cpage-head h1{font-size:clamp(24px,4vw,40px);font-weight:700;color:var(--paper);291  line-height:1.1;display:flex;align-items:center;gap:10px;flex-wrap:wrap;text-transform:uppercase}292.cpage-head h1 .vbadge{width:22px;height:22px;color:var(--accent-soft)}293.cpage-head .meta{display:flex;gap:7px;flex-wrap:wrap;margin-top:12px}294.mchip{background:rgba(245,243,238,.12);border:1px solid rgba(245,243,238,.3);295  border-radius:var(--r-ctl);padding:3px 11px;font-family:var(--font-mono);296  font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--paper)}297.mchip.o{background:var(--accent);border-color:var(--accent);color:var(--on-accent)}298.cpage-reach{margin-left:auto;text-align:right}299.cpage-reach b{display:block;font-family:var(--font-display);300  font-size:clamp(28px,3.6vw,40px);font-weight:700;color:var(--accent-soft);letter-spacing:-.02em}301.cpage-reach span{font-family:var(--font-mono);font-size:10px;color:rgba(245,243,238,.6);302  text-transform:uppercase;letter-spacing:.08em;font-weight:700}303@media(max-width:760px){.cpage-reach{margin-left:0;text-align:left;flex-basis:100%}}304.cpage-bio{margin:24px 0 0;max-width:820px;color:var(--ink-2);font-size:15px}305.cpage-notes{margin:14px 0 0;max-width:820px;color:var(--ink-2);font-size:12.5px;306  background:var(--surface);border:1.5px solid var(--ink);border-radius:var(--r-card);307  padding:13px 17px;box-shadow:var(--shadow-off-soft)}308.sect-title{font-size:19px;margin:36px 0 15px;display:flex;align-items:center;gap:10px;text-transform:uppercase}309/* ===== page créateur : métriques calculées + onglets par plateforme ===== */310.hkpi-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:10px;margin:22px 0 6px}311.hkpi{background:var(--surface);border:1.5px solid var(--ink);border-radius:var(--r-card);312  box-shadow:var(--shadow-off-soft);padding:13px 15px}313.hkpi b{display:block;font-family:var(--font-display);font-size:22px;font-weight:800;color:var(--ink);line-height:1}314.hkpi span{display:block;margin-top:5px;font-family:var(--font-mono);font-size:9.5px;color:var(--ink-3);315  text-transform:uppercase;letter-spacing:.05em;font-weight:700}316/* barre d'onglets */317.ptabs{display:flex;gap:8px;overflow-x:auto;margin:26px 0 0;padding-bottom:2px}318.ptabs::-webkit-scrollbar{height:5px}.ptabs::-webkit-scrollbar-thumb{background:var(--line);border-radius:3px}319.ptab{flex:none;display:inline-flex;align-items:center;gap:7px;cursor:pointer;320  background:var(--surface);border:1.5px solid var(--ink);border-bottom:none;321  border-radius:12px 12px 0 0;padding:10px 15px;font-family:var(--font-display);322  font-weight:700;font-size:13px;color:var(--ink-2);transition:.14s;margin-bottom:-1.5px}323.ptab:hover{background:var(--surface-2)}324.ptab.on{background:var(--accent);color:var(--on-accent);border-color:var(--ink)}325.ptab.on .ptab-ico{color:var(--on-accent)!important}326.ptab-ico{width:16px;height:16px;display:inline-flex}.ptab-ico svg{width:16px;height:16px}327.ptab-n{font-family:var(--font-mono);font-size:10px;opacity:.75;font-weight:700}328.ppanels{border:1.5px solid var(--ink);border-radius:0 12px 12px 12px;329  background:var(--surface);box-shadow:var(--shadow-off-soft);padding:20px 18px 22px}330.ppanel{display:none}.ppanel.on{display:block;animation:fade .2s ease}331@keyframes fade{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}332.ppanel-head{display:flex;align-items:center;gap:13px}333.ppanel-head .plat{width:46px;height:46px;border-radius:10px;flex:none;display:flex;align-items:center;justify-content:center}334.ppanel-head .plat svg{width:22px;height:22px}335.ppanel-id{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}336.ppanel-name{font-family:var(--font-display);font-weight:800;font-size:17px;display:flex;align-items:center;gap:6px}337.ppanel-handle{font-size:13px;color:var(--green);font-weight:600}338.conf{display:inline-flex;padding:2px 9px;border-radius:var(--r-ctl);339  font-family:var(--font-mono);font-size:10px;font-weight:700;letter-spacing:.03em;flex:none}340.conf.hi{background:var(--lime-soft);color:var(--green-deep)}341.conf.mid{background:var(--amber-soft);color:#8a5a12}342.abadge-row{display:flex;flex-wrap:wrap;gap:6px;margin:12px 0 4px}343.abadge{display:inline-flex;background:var(--lime-soft);color:var(--green-deep);344  border:1px solid var(--line);border-radius:var(--r-ctl);padding:3px 9px;345  font-family:var(--font-mono);font-size:10px;font-weight:700;letter-spacing:.02em}346.abadge.ok{background:var(--surface-2);color:var(--ink-2)}347.abadge.live{background:#ffe3e0;color:#b0201a;border-color:#b0201a}348/* grille KPI par plateforme */349.kpi-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:10px;margin:16px 0 4px}350.kpi{background:var(--surface-2);border:1px solid var(--line);border-radius:var(--r-ctl);padding:11px 13px}351.kpi b{display:block;font-family:var(--font-display);font-size:19px;font-weight:800;color:var(--ink);line-height:1}352.kpi span{display:block;margin-top:4px;font-family:var(--font-mono);font-size:9px;color:var(--ink-3);353  text-transform:uppercase;letter-spacing:.04em;font-weight:700}354.ppanel-top{display:inline-flex;margin:12px 0 2px;background:var(--surface-2);border:1px solid var(--line);355  border-radius:var(--r-ctl);padding:8px 13px;font-size:12.5px;font-weight:700;color:var(--ink-2)}356.ppanel-top:hover{color:var(--green)}357.ppanel-foot{margin-top:14px;font-family:var(--font-mono);font-size:10px;color:var(--ink-3)}358/* grille de contenu récent (images/vidéos) */359.cg-title{font-family:var(--font-display);font-size:14px;font-weight:800;margin:20px 0 12px;color:var(--ink)}360.cg-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:13px}361.cg-card{display:flex;flex-direction:column;text-decoration:none;color:inherit;362  border:1.5px solid var(--ink);border-radius:var(--r-card);overflow:hidden;363  background:var(--surface);box-shadow:var(--shadow-off-soft);transition:transform .15s,box-shadow .15s}364.cg-card:hover{transform:translate(-2px,-2px);box-shadow:var(--shadow-off)}365.cg-thumb{position:relative;display:block;width:100%;aspect-ratio:1/1;background:var(--surface-2);overflow:hidden}366.cg-thumb img{width:100%;height:100%;object-fit:cover;display:block}367.cg-thumb.noimg{display:flex;align-items:center;justify-content:center}368.cg-thumb.noimg svg{width:44px;height:44px;opacity:.3}369.cg-play{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;370  font-size:26px;text-shadow:0 2px 6px rgba(0,0,0,.7);pointer-events:none}371.cg-dur{position:absolute;right:6px;bottom:6px;background:rgba(0,0,0,.8);color:#fff;372  font-family:var(--font-mono);font-size:10px;padding:2px 6px;border-radius:5px}373.cg-pin{position:absolute;left:6px;top:6px;font-size:13px}374.cg-body{padding:9px 11px 11px;display:flex;flex-direction:column;gap:5px}375.cg-cap{font-size:12px;line-height:1.3;color:var(--ink-2);display:-webkit-box;376  -webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;min-height:31px}377.cg-cap.dim{color:var(--ink-3)}378.cg-meta{font-family:var(--font-mono);font-size:10px;color:var(--ink-3);display:flex;379  justify-content:space-between;gap:6px;flex-wrap:wrap}380.cg-when{color:var(--ink-3);opacity:.8}381.cg-empty{padding:26px;text-align:center;color:var(--ink-3);font-size:13px}382.cg-empty svg{width:20px;height:20px;vertical-align:-4px;opacity:.4;margin-right:6px}383/* vue d'ensemble : barres de portée */384.obars{display:flex;flex-direction:column;gap:11px}385.obar{display:flex;align-items:center;gap:11px}386.obar-ico{width:32px;height:32px;border-radius:8px;flex:none;display:flex;align-items:center;justify-content:center}387.obar-ico svg{width:16px;height:16px}388.obar-lab{width:96px;flex:none;font-family:var(--font-display);font-weight:700;font-size:13px}389.obar-track{flex:1;height:12px;background:var(--surface-2);border:1px solid var(--line);border-radius:6px;overflow:hidden}390.obar-fill{display:block;height:100%;border-radius:6px 0 0 6px;min-width:3px}391.obar-val{width:78px;flex:none;text-align:right;font-family:var(--font-mono);font-size:12px;font-weight:700;color:var(--ink)}392@media(max-width:640px){.obar-lab{width:64px;font-size:11px}.obar-val{width:60px;font-size:11px}393  .cg-grid{grid-template-columns:repeat(auto-fill,minmax(130px,1fr))}}394.linkinbio{display:inline-flex;align-items:center;gap:10px;margin-top:22px;395  background:var(--accent);color:var(--on-accent);border:1.5px solid var(--ink);396  border-radius:var(--r-ctl);padding:11px 20px;font-family:var(--font-display);397  font-weight:700;font-size:13.5px;transition:.15s;box-shadow:4px 4px 0 rgba(20,24,20,.25)}398.linkinbio:hover{background:var(--accent-soft);color:var(--ink)}399.linkinbio:active{transform:translate(2px,2px);box-shadow:none}400.linkinbio svg{width:16px;height:16px;fill:currentColor}401.cpage-foot{margin-top:38px;font-size:12px;color:var(--ink-3);402  border-top:1.5px solid var(--ink);padding-top:16px}403/* bannière héro (banner_url capté par les acteurs Apify) */404.cbanner{position:relative;border:1.5px solid var(--ink);border-bottom:none;405  border-radius:var(--r-card) var(--r-card) 0 0;overflow:hidden;height:clamp(120px,22vw,240px);406  background:var(--ink)}407.cbanner img{width:100%;height:100%;object-fit:cover;display:block}408.cbanner+.cpage-head{border-radius:0 0 var(--r-card) var(--r-card)}409/* Ka Score */410.kascore{display:flex;gap:22px;align-items:center;flex-wrap:wrap;margin:6px 0 2px}411.kascore-num{flex:none;width:112px;height:112px;border-radius:50%;display:flex;412  flex-direction:column;align-items:center;justify-content:center;413  border:2px solid var(--ink);background:var(--accent);color:var(--on-accent);414  box-shadow:var(--shadow-off-soft)}415.kascore-num b{font-family:var(--font-display);font-size:34px;font-weight:800;line-height:1}416.kascore-num span{font-family:var(--font-mono);font-size:9px;font-weight:700;417  text-transform:uppercase;letter-spacing:.06em;margin-top:3px}418.kascore-parts{flex:1;min-width:230px;display:flex;flex-direction:column;gap:7px}419.kspart{display:flex;align-items:center;gap:10px}420.kspart-lab{width:110px;flex:none;font-family:var(--font-mono);font-size:10px;421  font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--ink-3)}422.kspart-track{flex:1;height:9px;background:var(--surface-2);border:1px solid var(--line);423  border-radius:5px;overflow:hidden}424.kspart-fill{display:block;height:100%;background:var(--green);border-radius:5px 0 0 5px;min-width:2px}425.kspart-val{width:52px;flex:none;text-align:right;font-family:var(--font-mono);426  font-size:11px;font-weight:700;color:var(--ink)}427@media(max-width:640px){.kspart-lab{width:88px;font-size:9px}}428/* sparkline évolution d'audience */429.spark-wrap{margin-top:8px}430.spark-svg{width:100%;height:88px;display:block}431.spark-line{fill:none;stroke:var(--green);stroke-width:2.5;stroke-linejoin:round;stroke-linecap:round}432.spark-fill{fill:var(--lime-soft);opacity:.7;stroke:none}433.spark-meta{display:flex;justify-content:space-between;font-family:var(--font-mono);434  font-size:10px;color:var(--ink-3);margin-top:4px}435.growth-up{color:var(--green-deep);font-weight:700}436.growth-down{color:#b0201a;font-weight:700}437/* bloc EN DIRECT (miniature live Twitch/Kick) */438.live-card{display:flex;gap:14px;align-items:stretch;margin:14px 0 4px;439  border:1.5px solid #b0201a;border-radius:var(--r-card);overflow:hidden;440  background:var(--surface);text-decoration:none;color:inherit}441.live-card img{width:200px;min-height:100%;object-fit:cover;flex:none;display:block}442.live-card-body{padding:12px 14px;display:flex;flex-direction:column;gap:5px;justify-content:center}443.live-card-tag{font-family:var(--font-mono);font-size:10px;font-weight:700;color:#b0201a}444.live-card-title{font-size:13.5px;font-weight:700;color:var(--ink);line-height:1.3}445.live-card-meta{font-family:var(--font-mono);font-size:10.5px;color:var(--ink-3)}446@media(max-width:560px){.live-card{flex-direction:column}.live-card img{width:100%;height:150px}}447.cpage-foot a{color:var(--green);font-weight:600}448449/* ===== page stats ===== */450.stats-page{padding-top:40px;padding-bottom:70px}451.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:14px;margin:26px 0}452.tile{background:var(--surface);border:1.5px solid var(--ink);border-radius:var(--r-card);453  padding:18px 20px;box-shadow:var(--shadow-off-soft)}454.tile-v{font-family:var(--font-display);font-weight:700;font-size:clamp(23px,3vw,32px);455  letter-spacing:-.03em;line-height:1.05;color:var(--ink)}456.tile-k{font-family:var(--font-mono);font-size:9.5px;font-weight:700;text-transform:uppercase;457  letter-spacing:.1em;color:var(--ink-3);margin-top:7px}458.tile.hero-tile{background:var(--ink)}459.tile.hero-tile .tile-v{color:var(--accent-soft)}460.tile.hero-tile .tile-k{color:rgba(245,243,238,.55)}461.viz-card{background:var(--surface);border:1.5px solid var(--ink);border-radius:var(--r-card);462  box-shadow:var(--shadow-off-soft);padding:24px 26px 18px;margin-bottom:22px}463.viz-card h2{font-size:18px;text-transform:uppercase}464.viz-sub{font-family:var(--font-mono);font-size:10px;font-weight:700;letter-spacing:.08em;465  text-transform:uppercase;color:var(--ink-3);margin:4px 0 18px}466.hrow{display:flex;align-items:center;gap:12px;margin-bottom:10px}467.hrow .plat{width:28px;height:28px;border-radius:7px;flex:none}468.hrow .plat svg{width:13px;height:13px}469.hrow-label{flex:0 0 130px;font-size:12.5px;font-weight:600;color:var(--ink-2);470  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}471@media(max-width:640px){.hrow-label{flex-basis:96px;font-size:11.5px}}472.hbar{flex:1;height:12px;border:1.5px solid var(--ink);border-radius:4px;473  background:var(--surface-2);overflow:hidden}474.hbar i{display:block;height:100%;background:var(--lime);border-right:1.5px solid var(--ink)}475.hrow .n{flex:none;font-family:var(--font-display);font-size:12.5px;font-weight:700;476  color:var(--ink);min-width:44px;text-align:right}477478/* ===== page stats : tableau de bord ka-stats (SPEC) ===== */479.stats-head{display:flex;flex-wrap:wrap;gap:16px;align-items:flex-start;justify-content:space-between}480.pdf-actions{display:flex;gap:8px;flex-wrap:wrap}481.fresh-row{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-top:14px;482  font-family:var(--font-mono);font-size:11px;color:var(--ink-3);text-transform:uppercase;letter-spacing:.06em}483.pchips{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin:22px 0 2px}484.pchip{border:1.5px solid var(--ink);background:var(--surface);border-radius:var(--r-ctl);485  padding:8px 14px;min-height:44px;font-family:var(--font-display);font-size:12.5px;486  font-weight:700;color:var(--ink-2);box-shadow:3px 3px 0 rgba(20,24,20,.08);transition:.15s}487.pchip[aria-pressed="true"]{background:var(--accent);color:var(--on-accent)}488.pchip:active{transform:translate(2px,2px);box-shadow:none}489.pdates{display:inline-flex;gap:6px;align-items:center;flex-wrap:wrap}490.pdates input{border:1.5px solid var(--ink);border-radius:var(--r-ctl);padding:8px 10px;491  font:inherit;font-size:13px;background:var(--surface);min-height:44px;max-width:160px}492@media(max-width:760px){.pchips{flex-wrap:nowrap;overflow-x:auto;-webkit-overflow-scrolling:touch;493  padding:4px 0 8px;scrollbar-width:none}494 .pchips::-webkit-scrollbar{display:none}495 .pchip{flex:none}.pdates{flex:none}.pdates input{font-size:16px}}496@media(max-width:640px){.pchips{margin-left:-16px;margin-right:-16px;497  padding-left:16px;padding-right:16px}}498.tile-d{font-family:var(--font-mono);font-size:10.5px;font-weight:700;margin-top:8px}499.tile-d.up{color:var(--green)}.tile-d.down{color:var(--danger)}500.tile.hero-tile .tile-d.up{color:var(--accent-soft)}501.viz-grid{display:grid;gap:22px;grid-template-columns:1fr;margin-bottom:22px}502@media(min-width:960px){.viz-grid.two{grid-template-columns:1fr 1fr}}503.viz-grid .viz-card{margin-bottom:0}504.viz-grid>*,.dt,.lc{min-width:0}505.dt .viz-card,.lc .viz-card{margin-bottom:0}506.lc svg{width:100%;height:auto;display:block;touch-action:pan-y}507.lc-legend{display:flex;gap:14px;flex-wrap:wrap;margin:2px 0 0}508.leg-btn{display:inline-flex;align-items:center;gap:6px;border:0;background:none;509  font-family:var(--font-mono);font-size:10px;font-weight:700;text-transform:uppercase;510  letter-spacing:.06em;color:var(--ink-2);min-height:44px}511.leg-btn[aria-pressed="false"]{opacity:.35}512.leg-swatch{width:18px;height:0;border-top:3px solid var(--accent)}513.leg-swatch.cmp{border-top-style:dashed;border-top-color:var(--ink-3)}514.lc-tip{display:inline-flex;margin-top:6px;visibility:hidden}515.donut-wrap{display:flex;flex-wrap:wrap;gap:22px;align-items:center;margin-top:14px}516.donut-legend{list-style:none;margin:0;padding:0;display:grid;gap:7px;flex:1;min-width:200px}517.donut-legend li{display:flex;align-items:center;gap:8px;font-size:12.5px;min-width:0}518.donut-legend .dsw{width:11px;height:11px;border-radius:3px;border:1px solid var(--ink);519  background:var(--accent);flex:none}520.donut-legend .dlbl{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}521.donut-legend b{font-family:var(--font-mono);font-size:11px}522.tbl-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch;max-width:100%}523.dtable table{width:100%;border-collapse:collapse;font-size:13px}524.dtable th{cursor:pointer;text-align:left;padding:8px 10px;background:var(--ink);525  color:var(--paper);font-family:var(--font-mono);font-size:10px;text-transform:uppercase;526  letter-spacing:.06em;white-space:nowrap;user-select:none}527.dtable td{padding:7px 10px;border-bottom:1px solid var(--line);white-space:nowrap}528.dtable tbody tr:nth-child(even) td{background:var(--surface-2)}529.dtable .dt-q{border:1.5px solid var(--ink);border-radius:var(--r-ctl);padding:9px 13px;530  font:inherit;font-size:13px;background:var(--surface);max-width:230px;min-height:44px}531@media(max-width:760px){.dtable .dt-q{font-size:16px;max-width:100%;flex:1}}532.dtable-foot{display:flex;justify-content:space-between;align-items:center;margin-top:12px;533  flex-wrap:wrap;gap:8px}534.records-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px;margin-top:14px}535.record{background:var(--surface-2);border:1.5px solid var(--ink);border-radius:var(--r-card);536  padding:13px 16px;display:flex;justify-content:space-between;gap:12px;align-items:baseline;537  box-shadow:var(--shadow-off-soft)}538.record .r-lbl{font-size:12.5px;color:var(--ink-2)}539.record .r-val{text-align:right;font-family:var(--font-display);font-size:14.5px;font-weight:700}540.hm svg{min-width:520px;width:100%;height:auto}541.empty-block{border:1.5px dashed var(--ink);border-radius:var(--r-card);542  background:var(--surface-2);padding:20px}543.stats-sect{font-size:17px;text-transform:uppercase;margin:34px 0 14px}544545/* ===== page stats v2 : sparklines, jauges, histogrammes, heatmap horaire, menu PDF ===== */546.tile svg.spark{display:block;margin-top:9px;width:100%;height:28px}547.gauges{display:grid;grid-template-columns:repeat(auto-fit,minmax(168px,1fr));gap:14px;margin-bottom:8px}548.gauge{background:var(--surface);border:1.5px solid var(--ink);border-radius:var(--r-card);549  padding:16px 14px 13px;box-shadow:var(--shadow-off-soft);text-align:center}550.gauge svg{width:124px;max-width:100%;height:auto;margin:0 auto;display:block}551.gauge .g-v{font-family:var(--font-display);font-weight:700;font-size:21px;color:var(--ink);margin-top:-30px}552.gauge .g-l{font-family:var(--font-mono);font-size:9px;font-weight:700;text-transform:uppercase;553  letter-spacing:.05em;color:var(--ink-3);margin-top:10px;line-height:1.55}554.statsum{display:flex;gap:7px;flex-wrap:wrap;margin-top:10px}555.statsum .chip b{font-family:var(--font-mono);margin-left:4px}556.leg-swatch.ml1{border-top:3px solid var(--ink)}557.leg-swatch.ml2{border-top:3px dashed var(--accent)}558.leg-swatch.ml3{border-top:3px dotted var(--ink-3)}559.hrow .d{flex:none;font-family:var(--font-mono);font-size:10px;font-weight:700;min-width:54px;text-align:right}560.hrow .d.up{color:var(--green)}.hrow .d.down{color:var(--danger)}561.hh svg{min-width:560px;width:100%;height:auto}562.pdf-menu-wrap{position:relative;display:inline-flex;gap:8px;flex-wrap:wrap}563.pdf-menu{position:absolute;right:0;top:calc(100% + 8px);z-index:70;background:var(--surface);564  border:1.5px solid var(--ink);border-radius:var(--r-card);box-shadow:var(--shadow-off);565  display:none;min-width:270px;padding:6px}566.pdf-menu.open{display:block}567.pdf-menu button{display:block;width:100%;text-align:left;border:0;background:none;568  padding:11px 13px;border-radius:9px;font-family:var(--font-display);font-weight:700;569  font-size:13px;color:var(--ink);min-height:44px}570.pdf-menu button:hover{background:var(--accent-soft)}571.pdf-menu small{display:block;font-family:var(--font-body);font-weight:400;font-size:11px;572  color:var(--ink-3);margin-top:2px}573@media(max-width:760px){.pdf-menu{right:auto;left:0}}574575/* ===== retrait ===== */576.optout-page{max-width:620px;margin:0 auto;padding:44px 0 70px}577.optout-page h1{font-size:clamp(26px,4vw,32px);margin:12px 0;text-transform:uppercase}578.optout-page p{color:var(--ink-2)}579.field{margin-top:16px}580.field label{display:block;font-family:var(--font-mono);font-size:10px;font-weight:700;581  text-transform:uppercase;letter-spacing:.08em;margin-bottom:7px;color:var(--ink-2)}582.field input,.field textarea{width:100%;border:1.5px solid var(--ink);border-radius:var(--r-ctl);583  padding:11px 14px;font:inherit;background:var(--surface);color:var(--ink);584  box-shadow:3px 3px 0 rgba(20,24,20,.08);transition:box-shadow .15s ease}585.field input:focus,.field textarea:focus{outline:2px solid var(--green);outline-offset:1px;586  box-shadow:3px 3px 0 rgba(112,72,232,.25)}587@media(max-width:900px){.field input,.field textarea{font-size:16px}}588.optout-ok{background:var(--lime-soft);color:var(--green-deep);border:1.5px solid var(--ink);589  border-radius:var(--r-card);padding:16px 20px;font-weight:600;margin-top:20px;590  box-shadow:var(--shadow-off-soft)}591.optout-ok.warn{background:var(--amber-soft);color:#8a5a12}592593/* ===== footer : nav locale + footer commun Groupe KA (ka/KaFooter) ===== */594.footer-local{margin-top:56px;border-top:1.5px solid var(--ink);background:var(--surface);padding:16px 0}595.footer-local nav{display:flex;gap:10px 26px;flex-wrap:wrap;align-items:center;596  font-family:var(--font-mono);font-size:11px;font-weight:700;597  text-transform:uppercase;letter-spacing:.08em;color:var(--ink-2)}598.footer-local .klabel{color:var(--ink-3)}599.footer-local a{display:inline-flex;align-items:center;min-height:44px}600.footer-local a:hover{color:var(--accent-deep);text-decoration:underline;text-underline-offset:4px}601.footer-local+.ka-footer{margin-top:0}602.ka-footer .brand{font-size:19px;color:var(--paper)}603604/* ===== contact ===== */605.contact-page{max-width:880px;margin:0 auto;padding:44px 0 70px}606.contact-page h1{font-size:clamp(26px,4vw,38px);margin:12px 0;text-transform:uppercase}607.contact-page .lead{color:var(--ink-2);max-width:640px}608.contact-notice{background:var(--accent-soft);border:1.5px solid var(--ink);border-radius:var(--r-card);609  padding:16px 20px;margin:22px 0;box-shadow:var(--shadow-off-soft);font-weight:600;color:var(--ink)}610.contact-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:14px;margin:8px 0 4px}611.contact-card{display:block;background:var(--surface);border:1.5px solid var(--ink);612  border-radius:var(--r-card);padding:16px 18px;box-shadow:var(--shadow-off-soft);613  transition:transform .16s ease,box-shadow .16s ease;min-height:44px}614.contact-card:hover{transform:translate(-2px,-2px);box-shadow:var(--shadow-off)}615.contact-card b{display:block;font-family:var(--font-mono);font-size:13px;font-weight:700;616  color:var(--accent-deep);word-break:break-all}617.contact-card span{display:block;margin-top:5px;font-size:12px;color:var(--ink-2)}618.site-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:10px;margin:14px 0 0}619.site-item{display:flex;align-items:center;gap:10px;background:var(--surface);620  border:1.5px solid var(--ink);border-radius:var(--r-ctl);padding:10px 13px;min-height:44px;621  transition:transform .15s ease,box-shadow .15s ease}622.site-item:hover{transform:translate(-2px,-2px);box-shadow:3px 3px 0 var(--ink)}623.site-item i{width:10px;height:10px;border-radius:3px;border:1px solid var(--ink);flex:none}624.site-item b{font-family:var(--font-display);font-size:13.5px;font-weight:700}625.site-item span{font-size:11px;color:var(--ink-3);margin-left:auto;font-family:var(--font-mono)}626@media(max-width:480px){.site-item span{display:none}}627.tabbar{display:none}628@media(max-width:940px){629  .tabbar{display:grid;grid-template-columns:repeat(5,1fr);630    position:fixed;left:0;right:0;bottom:0;z-index:var(--z-bottombar,600);631    background:rgba(245,243,238,.94);632    backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);633    border-top:1.5px solid var(--ink);634    padding:6px 4px calc(6px + env(safe-area-inset-bottom))}635  .tabbar a{display:flex;flex-direction:column;align-items:center;gap:3px;636    padding:5px 2px;min-height:48px;justify-content:center;637    font-family:var(--font-mono);font-size:9.5px;font-weight:700;638    text-transform:uppercase;letter-spacing:.04em;color:var(--ink-3);639    border-radius:var(--r-ctl);transition:color .12s ease}640  .tabbar a.active{color:var(--green)}641  .tabbar a svg{display:block;width:20px;height:20px;fill:none;stroke:currentColor;642    stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}643  body{padding-bottom:calc(64px + env(safe-area-inset-bottom))}644  .ka-footer{padding-bottom:calc(76px + env(safe-area-inset-bottom))}645}646.spin{width:26px;height:26px;border:3px solid var(--line);border-top-color:var(--green);647  border-radius:50%;margin:50px auto;animation:sp .8s linear infinite}648@keyframes sp{to{transform:rotate(360deg)}}649</style>650</head>651<body>652<div id="app"></div>653<script>654"use strict";655/* Créa-Ka SPA — routes : / · /createur/{id} · /stats · /favoris · /retrait · /compte · /contact */656const ICONS=__ICONS__;657/* écosystème Groupe KA (ka/ecosystem.json, injecté au build) — footer commun + /contact */658const KA_ECO=__KA_ECO__;659const KA_SITE=KA_ECO.sites.find(s=>s.id==="crea-ka");660const $=s=>document.querySelector(s);661const app=$("#app");662const fmt=new Intl.NumberFormat("fr-CA",{notation:"compact",maximumFractionDigits:1});663const fmtFull=new Intl.NumberFormat("fr-CA");664const PLAT={665 instagram:{label:"Instagram",color:"#c13584",av:"instagram"},666 tiktok:{label:"TikTok",color:"#141814",av:"tiktok"},667 youtube:{label:"YouTube",color:"#e02424",av:"youtube"},668 twitch:{label:"Twitch",color:"#9146ff",av:"twitch"},669 kick:{label:"Kick",color:"#141814"},670 x:{label:"X (Twitter)",color:"#141814",av:"twitter"},671 facebook:{label:"Facebook",color:"#1877f2"},672 snapchat:{label:"Snapchat",color:"#d4a900"},673 substack:{label:"Substack",color:"#ff6719"},674 patreon:{label:"Patreon",color:"#f1465a"},675 onlyfans:{label:"OnlyFans",color:"#00aff0"},676 mym:{label:"MYM",color:"#141814"},677 fansly:{label:"Fansly",color:"#2699f7"},678 linkedin:{label:"LinkedIn",color:"#0a66c2"},679 threads:{label:"Threads",color:"#141814"},680 spotify:{label:"Spotify",color:"#1db954"},681 discord:{label:"Discord",color:"#5865f2"},682 podcast:{label:"Balado",color:"#8940fa"},683 "site-web":{label:"Site web",color:"#7048e8"},684 autre:{label:"Autre",color:"#8b928c"}};685const NICHE_LBL={"humour":"Humour","mode":"Mode","beaute":"Beauté","lifestyle":"Lifestyle",686 "famille-parentalite":"Famille","cuisine":"Cuisine","gaming":"Gaming","tech":"Tech",687 "sport-fitness":"Sport & fitness","plein-air":"Plein air","voyage":"Voyage","musique":"Musique",688 "arts":"Arts","danse":"Danse","education":"Éducation","finance-affaires":"Finance & affaires",689 "sante-mieux-etre":"Santé & mieux-être","bouffe-resto":"Bouffe & resto",690 "actualite-opinion":"Actualité & opinion","autre":"Autre"};691const TIER_LBL={nano:"Nano",micro:"Micro",macro:"Macro",mega:"Méga"};692const state={q:"",niche:"",region:"",langue:"",plateforme:"",tier:"",sort:"reach",693 offset:0,items:[],total:0,stats:null,tax:null};694695const esc=s=>String(s??"").replace(/[&<>"']/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c]));696const initials=n=>n.split(/\s+/).slice(0,2).map(w=>w[0]||"").join("").toUpperCase();697async function api(p){const r=await fetch(p);if(!r.ok)throw new Error(r.status);return r.json()}698function icon(p,extra){return `<svg class="icn" viewBox="0 0 24 24" aria-hidden="true"${extra||""}><path d="${ICONS[p]||ICONS.autre}"/></svg>`}699const checkSvg=`<svg class="vbadge" viewBox="0 0 24 24" aria-label="vérifié"><path fill="currentColor" d="M12 0l2.8 2.5 3.7-.7.9 3.7 3.4 1.7-1.5 3.5 1.5 3.5-3.4 1.7-.9 3.7-3.7-.7L12 24l-2.8-2.5-3.7.7-.9-3.7-3.4-1.7 1.5-3.5-1.5-3.5 3.4-1.7.9-3.7 3.7.7z"/><path fill="#f5f3ee" d="M10.6 16.2l-3.3-3.3 1.4-1.4 1.9 1.9 4.7-4.7 1.4 1.4z"/></svg>`;700701/* ---------- favoris ♥ « Mon univers Ka » — magasin central : hub groupe-ka.com702   (AUCUN stockage local : toggle synchrone via /api/favorites/toggle, lecture703   /api/favorites — cache serveur 30 s). item_id = id stable du créateur. ---------- */704const heartSvg=`<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 20.5S4.5 15.7 2.6 11.4C1.2 8.2 2.9 4.9 6.1 4.4c2-.3 3.9.6 5.9 2.9 2-2.3 3.9-3.2 5.9-2.9 3.2.5 4.9 3.8 3.5 7-1.9 4.3-9.4 9.1-9.4 9.1z"/></svg>`;705const FAV={ids:new Set(),loaded:false,src:{}};706function favItem(c){707 const main=(c.platforms||[]).slice().sort((x,y)=>(y.followers||0)-(x.followers||0))[0];708 const sub=main?(PLAT[main.platform]||PLAT.autre).label:(c.region||"");709 return {item_id:c.id,title:c.display_name||c.id,710  subtitle:sub,image_url:avatarSources(c)[0]||"",711  url:"https://www.crea-ka.com/createur/"+encodeURIComponent(c.id)};712}713function favBtn(c){714 FAV.src[c.id]=favItem(c);715 const on=FAV.ids.has(c.id);716 return `<button type="button" class="favbtn${on?" on":""}" data-fav="${esc(c.id)}"717  aria-pressed="${on}" aria-label="${on?"Retirer de mes favoris":"Ajouter à mes favoris"}"718  title="${on?"Retirer de mes favoris":"Ajouter à mes favoris"}"719  onclick="event.stopPropagation();favToggle(this.dataset.fav,this)">${heartSvg}</button>`;720}721function favPaint(){722 document.querySelectorAll("[data-fav]").forEach(b=>{723  const on=FAV.ids.has(b.dataset.fav);724  b.classList.toggle("on",on);b.setAttribute("aria-pressed",on);725  const lbl=on?"Retirer de mes favoris":"Ajouter à mes favoris";726  b.title=lbl;b.setAttribute("aria-label",lbl);727  const t=b.querySelector(".fav-cta-txt");if(t)t.textContent=lbl;728 });729}730async function favLoad(){731 if(FAV.loaded||!state.me)return favPaint();732 try{const r=await fetch("/api/favorites");733  if(r.ok){const j=await r.json();FAV.ids=new Set(j.ids||[]);FAV.loaded=true;}734 }catch(e){}735 favPaint();736}737window.favToggle=async(id,el)=>{738 if(state.me===null){location.href="/api/auth/ka/login";return}739 const on=!FAV.ids.has(id);740 if(el)el.disabled=true;741 try{742  const r=await fetch("/api/favorites/toggle",{method:"POST",743   headers:{"Content-Type":"application/json"},744   body:JSON.stringify({on,item:FAV.src[id]||{item_id:id}})});745  if(r.status===401){location.href="/api/auth/ka/login";return}746  if(!r.ok)throw new Error(r.status);747  if(on)FAV.ids.add(id);else FAV.ids.delete(id);748  if(!on&&location.pathname==="/favoris"&&el){749   const card=el.closest(".card");if(card)card.remove();750   const grid=$("#fav-grid");751   if(grid&&!grid.querySelector(".card"))renderFavoris();752  }753 }catch(e){alert("Impossible de synchroniser le favori avec le hub Groupe KA — réessayez.")}754 if(el)el.disabled=false;755 favPaint();756};757758/* cascade de photos : avatar capté à la source, puis unavatar par plateforme,759   repli final = initiales sur motif encre/violet (déjà en dessous) */760function avatarSources(c){761 const order=["instagram","tiktok","youtube","twitch","x"];762 const srcs=[];763 if(c.avatar_url)srcs.push(c.avatar_url);764 for(const p of order){765  const acc=(c.platforms||[]).find(a=>a.platform===p&&PLAT[p]&&PLAT[p].av);766  if(acc)srcs.push(`https://unavatar.io/${PLAT[p].av}/${encodeURIComponent(acc.handle)}?fallback=false`);767 }768 return srcs;769}770window.nextSrc=el=>{771 const srcs=JSON.parse(el.dataset.srcs||"[]");const i=+(el.dataset.i||0)+1;772 if(i<srcs.length){el.dataset.i=i;el.src=srcs[i]}else el.remove();773};774function photoImg(c){775 const srcs=avatarSources(c);776 if(!srcs.length)return"";777 return `<img loading="lazy" alt="${esc(c.display_name)}" src="${srcs[0]}"778   data-srcs='${esc(JSON.stringify(srcs))}' data-i="0" onerror="nextSrc(this)">`;779}780const brandSvg=`<svg width="30" height="30" viewBox="0 0 64 64"><rect width="64" height="64" rx="10" fill="#141814"/><circle cx="32" cy="23" r="9" fill="#7048e8"/><path d="M14 52c3-10 9-15 18-15s15 5 18 15" stroke="#7048e8" stroke-width="6" fill="none" stroke-linecap="round"/></svg>`;781782function header(){783 return `<header class="header"><div class="container header-inner">784  <a href="/" data-nav class="brand">${brandSvg}Créa<span class="ka">Ka</span></a>785  <a class="gk-badge" href="https://www.groupe-ka.com" target="_blank" rel="noopener noreferrer"786   aria-label="Un service Groupe KA — visiter le portail de l'écosystème"><span class="pfx">Un service</span>787   <b>Groupe<span class="ka">KA</span></b></a>788  <span class="brand-tag">Tous les créateurs québécois.<br>Tous leurs liens.</span>789  <span class="header-spacer"></span>790  <a class="hbtn" href="/stats" data-nav>Statistiques</a>791  <a class="hbtn" href="/favoris" data-nav>♥ Favoris</a>792  <a class="hbtn" href="/contact" data-nav>Contact</a>793  <a class="hbtn primary" href="/#annuaire" data-nav>Explorer l'annuaire</a>794  <span id="acct-slot"></span>795 </div></header>`;796}797/* jeton de compte KA ID dans l'en-tête (connecté → chip, sinon → bouton hub) */798async function loadAccount(){799 const slot=$("#acct-slot");if(!slot)return;800 if(state.me===undefined){801  try{const r=await fetch("/api/me");state.me=r.ok?await r.json():null}802  catch(e){state.me=null}}803 const me=state.me;804 if(me){805  slot.innerHTML=`<a class="hbtn acct" href="/compte" data-nav>806   ${me.picture?`<img src="${esc(me.picture)}" alt="">`:""}${esc((me.name||"").split(" ")[0])}807   <span class="ka-badge">KA</span></a>`;808 }else{809  slot.innerHTML=`<a class="hbtn acct" href="/api/auth/ka/login"810   title="Se connecter avec KA ID"><span class="acct-long">Se connecter avec</span><span class="acct-short">Connexion</span> <span class="ka-badge">KA</span></a>`;811 }812 bindNav(slot);813 if(me){favSlotPaint();favLoad();}814 else{FAV.ids=new Set();FAV.loaded=false;favSlotPaint();favPaint();}815}816/* CTA favori de la fiche créateur : ♥ si connecté, sinon « Se connecter avec KA ID » */817function favSlotPaint(){818 const s=document.getElementById("fav-slot");if(!s)return;819 const c=state.creator;if(!c)return;820 if(state.me){821  FAV.src[c.id]=favItem(c);822  const on=FAV.ids.has(c.id);823  s.innerHTML=`<button type="button" class="fav-cta${on?" on":""}" data-fav="${esc(c.id)}"824   aria-pressed="${on}" onclick="favToggle(this.dataset.fav,this)">${heartSvg}<span class="fav-cta-txt">${on?"Retirer de mes favoris":"Ajouter à mes favoris"}</span></button>`;825 }else if(state.me===null){826  s.innerHTML=`<a class="fav-cta" href="/api/auth/ka/login"827   title="Un seul compte pour tout le Groupe KA — favoris partagés « Mon univers Ka »">${heartSvg}<span>Se connecter avec KA ID</span></a>`;828 }829}830function tabbar(active){831 const t=(href,label,svg,key)=>`<a href="${href}" data-nav class="${active===key?"active":""}">${svg}<span>${label}</span></a>`;832 return `<nav class="tabbar">833  ${t("/","Annuaire",`<svg viewBox="0 0 24 24"><path d="M3 10.5 12 3l9 7.5"/><path d="M5 9.5V21h14V9.5"/></svg>`,"home")}834  ${t("/#annuaire","Recherche",`<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/></svg>`,"search")}835  ${t("/favoris","Favoris",`<svg viewBox="0 0 24 24"><path d="M12 20.5S4.5 15.7 2.6 11.4C1.2 8.2 2.9 4.9 6.1 4.4c2-.3 3.9.6 5.9 2.9 2-2.3 3.9-3.2 5.9-2.9 3.2.5 4.9 3.8 3.5 7-1.9 4.3-9.4 9.1-9.4 9.1z"/></svg>`,"fav")}836  ${t("/stats","Stats",`<svg viewBox="0 0 24 24"><path d="M4 20V10M10 20V4M16 20v-7M22 20H2"/></svg>`,"stats")}837  ${t("/retrait","Retrait",`<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M8 12h8"/></svg>`,"optout")}838 </nav>`;839}840/* footer commun Groupe KA (équivalent vanilla de ka/KaFooter.tsx, siteId841   "crea-ka", localLegal = /retrait) — nav locale conservée au-dessus */842function footer(){843 const year=new Date().getFullYear();844 const others=KA_ECO.sites.filter(s=>s.id!=="crea-ka");845 const localLegal=[{label:"Documentation",href:"/doc/"},{label:"Retrait (opt-out)",href:"/retrait",nav:true}];846 return `<div class="footer-local"><div class="container"><nav>847   <span class="klabel">Créa·Ka</span>848   <a href="/compte" data-nav>Mon compte KA ID</a>849   <a href="/favoris" data-nav>Mes favoris</a>850   <a href="/stats" data-nav>Statistiques</a>851   <a href="/retrait" data-nav>Demander un retrait</a>852   <a href="/contact" data-nav>Contact</a>853   <a href="/api/stats" target="_blank" rel="noopener">API publique</a>854  </nav></div></div>855 <footer class="ka-footer" id="contact-footer">856  <div class="container">857   <a class="wordmark" href="${esc(KA_ECO.hub.url)}" target="_blank" rel="noopener noreferrer"858    aria-label="Groupe KA — le portail de l'écosystème">Groupe <span class="ka">KA</span></a>859   <p class="desc">${esc(KA_ECO.org.tagline)} Des centaines de connecteurs lisent les sites à la860    source, normalisent la donnée et se resynchronisent seuls.861    <b style="color:var(--paper)">${esc(KA_SITE.wordmark)}</b> — ${esc(KA_SITE.tagline)} — est un service Groupe KA.</p>862   <p class="notice"><b>${esc(KA_ECO.org.disclaimer)}</b></p>863   <ul class="sites">864    ${others.map(s=>`<li><a href="https://${esc(s.domain)}" target="_blank" rel="noopener noreferrer">${esc(s.wordmark)}</a></li>`).join("")}865    ${(KA_ECO.extraFooterLinks||[]).map(l=>`<li><a href="${esc(l.href)}" target="_blank" rel="noopener noreferrer">${esc(l.label)}</a></li>`).join("")}866   </ul>867   <div class="contacts">868    ${KA_ECO.contacts.map(c=>`<p style="margin:0"><a href="mailto:${esc(c.email)}">${esc(c.email)}</a><span>${esc(c.role)}</span></p>`).join("")}869   </div>870   <p class="legal">© ${year} ${esc(KA_ECO.org.copyrightHolder)}871    ${localLegal.map(l=>` · <a href="${esc(l.href)}"${l.nav?" data-nav":""}>${esc(l.label)}</a>`).join("")}872    ${KA_ECO.legal.map(l=>` · <a href="${esc(l.href)}" target="_blank" rel="noopener noreferrer">${esc(l.label)}</a>`).join("")}873   </p>874  </div>875 </footer>`;876}877878/* ---------- annuaire ---------- */879function platIcon(a){880 const p=PLAT[a.platform]||PLAT.autre;881 return `<a class="plat" href="${esc(a.url)}" target="_blank" rel="noopener"882   title="${p.label} — @${esc(a.handle)}" style="background:${p.color}"883   onclick="event.stopPropagation()">${icon(a.platform)}</a>`;884}885function cardHtml(c){886 const plats=(c.platforms||[]).slice(0,7).map(platIcon).join("");887 const niches=(c.niches||[]).slice(0,2).map(n=>`<span class="chip">${NICHE_LBL[n]||n}</span>`).join("");888 const verified=(c.platforms||[]).some(a=>a.verified);889 const np=(c.platforms||[]).length;890 return `<div class="card" data-href="/createur/${esc(c.id)}" role="link" tabindex="0" style="cursor:pointer">891  <div class="card-photo"><span class="init">${esc(initials(c.display_name))}</span>${photoImg(c)}892   <span class="tier ${esc(c.audience_tier)}">${TIER_LBL[c.audience_tier]||c.audience_tier}</span>893   ${c.ka_reco?`<span class="kc-reco" title="Correspond à vos préférences KA ID — gérez-les sur groupe-ka.com/mon-ka">Recommandé pour vous</span>`:""}894   <span class="nplat">${np} plat.</span>${favBtn(c)}</div>895  <div class="card-body">896   <div class="card-name">${esc(c.display_name)}${verified?checkSvg:""}</div>897   <div class="card-sub">${c.region?`${esc(c.region)} · `:""}${(c.languages||[]).map(l=>({fr:"FR",en:"EN",bilingue:"FR/EN"}[l]||l)).join(", ")}</div>898   <div class="chips">${niches}</div>899   ${c.total_reach?`<div class="reach"><b>${fmt.format(c.total_reach)}</b> abonnés cumulés</div>`:""}900   <div class="platrow">${plats}</div></div></div>`;901}902function selectHtml(id,label,opts,val,lbls){903 const o=opts.map(v=>`<option value="${esc(v)}"${v===val?" selected":""}>${esc(lbls?(lbls[v]||v):v)}</option>`).join("");904 return `<select class="f${val?" active":""}" id="${id}"><option value="">${label}</option>${o}</select>`;905}906async function renderHome(){907 document.title="Créa-Ka — Annuaire des créateurs de contenu québécois";908 if(!state.tax)state.tax=await api("/api/taxonomies");909 if(!state.stats)state.stats=await api("/api/stats").catch(()=>null);910 const s=state.stats||{creators:0,accounts:0,by_platform:{}};911 app.innerHTML=header()+`912 <section class="hero"><div class="container">913  <span class="kicker">Annuaire des créateurs du Québec</span>914  <h1 style="margin-top:16px">Chaque créateur<br>québécois. <span class="outline-txt">Tous</span> ses <span class="hl">liens.</span></h1>915  <p class="lead">Un « Linktree collectif » du Québec : influenceurs, youtubeurs, streamers,916   podcasteurs — leurs comptes regroupés et découvrables par niche, région, langue et taille d'audience.</p>917  <div class="stats-row">918   <span class="stat-pill"><b>${fmtFull.format(s.creators)}</b> créateurs</span>919   <span class="stat-pill"><b>${fmtFull.format(s.accounts)}</b> comptes reliés</span>920   <span class="stat-pill"><b>${Object.keys(s.by_platform||{}).length}</b> plateformes</span>921  </div>922  <div class="advantages">923   <div class="adv"><div class="ico">${icon("autre")}</div><h3>Cross-plateforme</h3><p>Instagram, TikTok, YouTube, Twitch, X… tous les comptes d'une même personne, au même endroit.</p></div>924   <div class="adv"><div class="ico"><svg class="icn" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5" stroke-linecap="round"/></svg></div><h3>Découvrable</h3><p>Filtres par niche, région, langue, plateforme et taille d'audience.</p></div>925   <div class="adv"><div class="ico">✓</div><h3>Liens vérifiés</h3><p>Chaque rattachement porte un score de confiance ; mieux vaut un lien de moins qu'un lien erroné.</p></div>926   <div class="adv"><div class="ico">🛡</div><h3>Respectueux</h3><p>Profils publics seulement, retrait (opt-out) simple et respecté. Loi 25.</p></div>927  </div>928 </div></section>929 <div class="searchbar" id="annuaire"><div class="container search-wrap">930  <label class="search-input"><svg width="15" height="15" viewBox="0 0 24 24" fill="none"><circle cx="11" cy="11" r="7" stroke="currentColor" stroke-width="2"/><path d="M20 20l-3.5-3.5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>931   <input id="q" placeholder="Chercher un créateur…" value="${esc(state.q)}"></label>932  <div class="filters">933  ${selectHtml("f-niche","Niche",state.tax.niches,state.niche,NICHE_LBL)}934  ${selectHtml("f-region","Région",state.tax.regions,state.region)}935  ${selectHtml("f-plat","Plateforme",state.tax.plateformes,state.plateforme,Object.fromEntries(Object.entries(PLAT).map(([k,v])=>[k,v.label])))}936  ${selectHtml("f-tier","Taille",state.tax.tiers,state.tier,TIER_LBL)}937  ${selectHtml("f-langue","Langue",state.tax.langues,state.langue,{fr:"Français",en:"Anglais",bilingue:"Bilingue"})}938  ${selectHtml("f-sort","Tri",["reach","nom","recent"],state.sort==="reach"?"":state.sort,{reach:"Audience",nom:"Nom A-Z",recent:"Récents"})}939  </div>940 </div></div>941 <main class="container"><div class="count-line" id="countline"></div>942  <div class="grid" id="grid"><div class="spin"></div></div>943  <div class="more-wrap" id="more"></div></main>`+footer()+tabbar(location.hash==="#annuaire"?"search":"home");944 bindNav();945 $("#q").addEventListener("input",deb(()=>{state.q=$("#q").value;reload()},300));946 [["f-niche","niche"],["f-region","region"],["f-plat","plateforme"],947  ["f-tier","tier"],["f-langue","langue"],["f-sort","sort"]].forEach(([id,key])=>{948  $("#"+id).addEventListener("change",e=>{state[key]=e.target.value||(key==="sort"?"reach":"");949   e.target.classList.toggle("active",!!e.target.value);reload()})});950 if(location.hash==="#annuaire"){const el=$("#annuaire");if(el)el.scrollIntoView()}951 await reload();952}953function deb(fn,ms){let t;return()=>{clearTimeout(t);t=setTimeout(fn,ms)}}954async function reload(more){955 if(!more){state.offset=0;state.items=[]}956 const p=new URLSearchParams({q:state.q,niche:state.niche,region:state.region,957  langue:state.langue,plateforme:state.plateforme,tier:state.tier,sort:state.sort,958  limit:"60",offset:String(state.offset)});959 const data=await api("/api/creators?"+p).catch(()=>({items:[],total:0}));960 state.items=state.items.concat(data.items);state.total=data.total;961 const grid=$("#grid");if(!grid)return;962 $("#countline").innerHTML=`<b>${fmtFull.format(state.total)}</b> créateur${state.total>1?"s":""} trouvé${state.total>1?"s":""}`;963 grid.innerHTML=state.items.length?state.items.map(cardHtml).join(""):964  `<div class="empty">Aucun créateur ne correspond à ces filtres.</div>`;965 const moreDiv=$("#more");966 moreDiv.innerHTML=state.items.length<state.total?967  `<button class="btn btn-primary" id="loadmore">Afficher plus (${fmtFull.format(state.total-state.items.length)} restants)</button>`:"";968 const lm=$("#loadmore");if(lm)lm.onclick=()=>{state.offset+=60;reload(true)};969 bindNav(grid);970}971972/* ---------- page créateur ---------- */973/* --- bande de contenu récent par compte (enrichissement Apify) --- */974const _relTime=ts=>{if(!ts)return"";if(typeof ts==="string"&&!/^\d+$/.test(ts))return esc(ts.slice(0,10));975 const d=Math.floor((Date.now()/1000-Number(ts))/86400);976 return d<=0?"aujourd'hui":d===1?"hier":d<30?d+" j":d<365?Math.floor(d/30)+" mois":Math.floor(d/365)+" ans";};977const _relTimeC=ts=>_relTime(ts);978/* image de contenu : IG/CDN expirent souvent → onerror bascule sur repli icône */979window.thumbFail=el=>{const w=el.parentElement;if(w){w.classList.add("noimg");980 w.innerHTML=w.dataset.icon||"";}};981function _mediaItems(a){const m=a.metrics||{};982 const base=m.recent_posts||m.recent_videos||m.recent_tweets||[];983 if(base.length)return base;984 // Snapchat : les previews de story/spotlight sont des listes d'URLs d'images985 const prev=[...(m.story_previews||[]),...(m.spotlight_previews||[])];986 return prev.map(u=>({display_url:u,type:"story"}));}987988/* grille de contenu récent, images/vidéos bien affichées */989function contentGrid(a){990 const items=_mediaItems(a);991 if(!items.length)return `<div class="cg-empty">${icon(a.platform)} Aucun contenu récent capté pour ce compte.</div>`;992 const ico=icon(a.platform);993 const cards=items.slice(0,12).map(it=>{994  const img=it.display_url||it.thumbnail_url||it.thumbnail||it.cover||(it.media_urls||[])[0]||"";995  const isVid=it.type==="video"||it.type==="reel"||!!it.duration||it.video_views!=null;996  const cap=esc((it.caption||it.title||it.text||"").slice(0,140));997  const when=_relTimeC(it.timestamp||it.published_at||it.published||it.created_at);998  const stats=[999   it.views!=null?fmt.format(it.views)+" vues":(it.video_views!=null?fmt.format(it.video_views)+" vues":null),1000   it.likes!=null?fmt.format(it.likes)+" ❤":null,1001   it.comments!=null?fmt.format(it.comments)+" 💬":null,1002   it.retweets!=null?fmt.format(it.retweets)+" ↻":null,1003   it.shares!=null?fmt.format(it.shares)+" ↗":null,1004  ].filter(Boolean).join(" · ");1005  const thumb=img1006   ? `<span class="cg-thumb" data-icon='${esc(ico)}'><img loading="lazy" src="${esc(img)}" alt="" referrerpolicy="no-referrer" onerror="thumbFail(this)">1007       ${isVid?'<span class="cg-play">▶</span>':""}${it.duration?`<span class="cg-dur">${esc(it.duration)}</span>`:""}1008       ${it.is_pinned?'<span class="cg-pin">📌</span>':""}</span>`1009   : `<span class="cg-thumb noimg" data-icon='${esc(ico)}'>${ico}${isVid?'<span class="cg-play">▶</span>':""}</span>`;1010  return `<a class="cg-card" href="${esc(it.url||a.url)}" target="_blank" rel="noopener">1011   ${thumb}1012   <span class="cg-body">${cap?`<span class="cg-cap">${cap}</span>`:`<span class="cg-cap dim">Publication ${a.platform}</span>`}1013    <span class="cg-meta">${stats||"—"}${when?`<span class="cg-when">${when}</span>`:""}</span></span></a>`;1014 }).join("");1015 return `<div class="cg-grid">${cards}</div>`;1016}10171018/* définitions de métriques calculées, par plateforme */1019function platformKPIs(a){1020 const m=a.metrics||{}, out=[];1021 const push=(v,l,suf)=>{if(v!=null&&v!=="")out.push([typeof v==="number"?fmt.format(v)+(suf||""):esc(v)+(suf||""),l]);};1022 push(a.followers,a.platform==="discord"?"membres":a.platform==="patreon"?"patrons":"abonnés");1023 push(m.engagement_rate_pct!=null?m.engagement_rate_pct:null,"engagement","%");1024 push(m.avg_likes,"j'aime / pub (moy.)");1025 push(m.avg_views!=null?m.avg_views:m.avg_video_views,"vues / pub (moy.)");1026 push(m.avg_comments,"comm. / pub (moy.)");1027 push(m.avg_retweets,"RT / pub (moy.)");1028 push(m.posts_per_week!=null?m.posts_per_week:(m.tweets_per_week!=null?m.tweets_per_week:m.videos_per_week),"pubs / semaine");1029 push(m.total_views,"vues totales");1030 push(m.posts,"publications");1031 push(m.videos,"vidéos");1032 push(m.likes,a.platform==="tiktok"?"j'aime cumulés":"j'aime");1033 push(m.following,"abonnements");1034 push(m.online,"en ligne");1035 push(m.boosts,"boosts");1036 push(m.patrons,"patrons");1037 push(m.live_viewers,"spectateurs (live)");1038 return out;1039}1040function platformBadges(a){1041 const m=a.metrics||{};1042 return [1043  a.verified?'<span class="abadge ok">✓ Vérifié</span>':"",1044  m.is_partner?'<span class="abadge">Partenaire</span>':"",1045  m.is_affiliate&&!m.is_partner?'<span class="abadge">Affilié</span>':"",1046  m.partnered?'<span class="abadge">Serveur partenaire</span>':"",1047  m.is_live_now?'<span class="abadge live">● En direct</span>':"",1048  m.is_seller?'<span class="abadge">Boutique</span>':"",1049  m.is_free===true?'<span class="abadge">Gratuit</span>':"",1050  m.is_monthly?'<span class="abadge">Mensuel</span>':"",1051  m.has_story?'<span class="abadge">Story active</span>':"",1052  m.has_spotlight?'<span class="abadge">Spotlight</span>':"",1053  m.is_nsfw?'<span class="abadge live">18+</span>':"",1054  m.creation_name?`<span class="abadge">${esc(String(m.creation_name).slice(0,60))}</span>`:"",1055  m.joined_date?`<span class="abadge">${esc(m.joined_date)}</span>`:"",1056  m.is_blue_verified?'<span class="abadge ok">✓ Bleu</span>':"",1057  m.country?`<span class="abadge">${esc(m.country)}</span>`:"",1058  (m.recent_games||[]).slice(0,2).map(g=>`<span class="abadge">🎮 ${esc(g)}</span>`).join(""),1059  (m.top_hashtags||[]).slice(0,3).map(h=>`<span class="abadge">#${esc(h)}</span>`).join(""),1060 ].filter(Boolean).join("");1061}10621063/* panneau d'un compte (onglet plateforme) */1064function platformPanel(a,idx,active){1065 const p=PLAT[a.platform]||PLAT.autre;1066 const conf=Math.round((a.confidence||0)*100);1067 const confCls=(a.confidence||0)>=0.75?"hi":"mid";1068 const kpis=platformKPIs(a).map(([v,l])=>`<div class="kpi"><b>${v}</b><span>${l}</span></div>`).join("");1069 const badges=platformBadges(a);1070 const stamp=(a.metrics||{})[a.platform+"_apify_at"]||a.last_checked;1071 const top=(a.metrics||{}).top_post||(a.metrics||{}).top_video||(a.metrics||{}).top_tweet;1072 return `<section class="ppanel${active?" on":""}" data-panel="${idx}">1073  <div class="ppanel-head">1074   <span class="plat" style="background:${p.color}">${icon(a.platform)}</span>1075   <div class="ppanel-id"><span class="ppanel-name">${p.label}${a.verified?checkSvg:""}</span>1076    <a class="ppanel-handle" href="${esc(a.url)}" target="_blank" rel="noopener">@${esc(a.handle)} ↗</a></div>1077   <span class="conf ${confCls}" title="Score de rattachement">${conf}%</span>1078  </div>1079  ${badges?`<div class="abadge-row">${badges}</div>`:""}1080  ${(a.metrics||{}).is_live_now&&(a.metrics||{}).live_thumbnail?`1081  <a class="live-card" href="${esc(a.url)}" target="_blank" rel="noopener">1082   <img loading="lazy" src="${esc((a.metrics||{}).live_thumbnail)}" alt="" referrerpolicy="no-referrer" onerror="this.remove()">1083   <span class="live-card-body"><span class="live-card-tag">● EN DIRECT</span>1084    ${(a.metrics||{}).live_title?`<span class="live-card-title">${esc((a.metrics||{}).live_title)}</span>`:""}1085    <span class="live-card-meta">${(a.metrics||{}).live_viewers!=null?fmt.format((a.metrics||{}).live_viewers)+" spectateurs":""}${(a.metrics||{}).live_game?" · "+esc((a.metrics||{}).live_game):((a.metrics||{}).live_category?" · "+esc((a.metrics||{}).live_category):"")}</span>1086   </span></a>`:""}1087  <div class="kpi-grid">${kpis||'<div class="kpi"><b>—</b><span>pas encore de métriques</span></div>'}</div>1088  ${top&&top.url?`<a class="ppanel-top" href="${esc(top.url)}" target="_blank" rel="noopener">★ Top contenu — ${top.views!=null?fmt.format(top.views)+" vues":""}${top.likes!=null?" · "+fmt.format(top.likes)+" ❤":""} ↗</a>`:""}1089  <h3 class="cg-title">Contenu récent</h3>1090  ${contentGrid(a)}1091  <div class="ppanel-foot">${stamp?"Dernière vérification "+esc(String(stamp).slice(0,10)):""}</div>1092 </section>`;1093}10941095window.showPtab=(i,el)=>{1096 document.querySelectorAll(".ppanel").forEach(s=>s.classList.toggle("on",s.dataset.panel==String(i)));1097 document.querySelectorAll(".ptab").forEach(t=>t.classList.remove("on"));1098 if(el)el.classList.add("on");1099 const ov=document.getElementById("ov-panel");if(ov)ov.classList.toggle("on",i==="ov");1100};11011102function computeAgg(c){1103 const P=(c.platforms||[]);1104 const reach=c.total_reach||P.reduce((s,a)=>s+(a.followers||0),0);1105 const eng=P.map(a=>(a.metrics||{}).engagement_rate_pct).filter(v=>v!=null);1106 const avgEng=eng.length?(eng.reduce((s,v)=>s+v,0)/eng.length):null;1107 const content=P.reduce((s,a)=>s+_mediaItems(a).length,0);1108 const cad=P.map(a=>{const m=a.metrics||{};return m.posts_per_week!=null?m.posts_per_week:m.tweets_per_week;}).filter(v=>v!=null);1109 const main=P.slice().sort((x,y)=>(y.followers||0)-(x.followers||0))[0];1110 return {reach,avgEng,content,maxCad:cad.length?Math.max(...cad):null,1111  nPlat:P.length,nVerif:P.filter(a=>a.verified).length,main};1112}11131114/* sparkline SVG (série quotidienne des snapshots — insights.history.total) */1115function sparkSvg(pts){1116 if(!pts||pts.length<2)return"";1117 const w=600,h=88,pad=4;1118 const vals=pts.map(p=>p.followers);1119 const min=Math.min(...vals),max=Math.max(...vals),span=Math.max(1,max-min);1120 const x=i=>pad+i*(w-2*pad)/(pts.length-1);1121 const y=v=>h-pad-((v-min)/span)*(h-2*pad);1122 const line=pts.map((p,i)=>`${x(i).toFixed(1)},${y(p.followers).toFixed(1)}`).join(" ");1123 const area=`${pad},${h-pad} ${line} ${(w-pad).toFixed(1)},${h-pad}`;1124 return `<div class="spark-wrap"><svg class="spark-svg" viewBox="0 0 ${w} ${h}" preserveAspectRatio="none" aria-hidden="true">1125  <polygon class="spark-fill" points="${area}"/><polyline class="spark-line" points="${line}"/></svg>1126  <div class="spark-meta"><span>${esc(pts[0].day)}</span><span>${fmt.format(min)} → ${fmt.format(max)}</span><span>${esc(pts[pts.length-1].day)}</span></div></div>`;1127}11281129async function renderCreator(id){1130 app.innerHTML=header()+`<div class="container"><div class="spin"></div></div>`+footer()+tabbar("");bindNav();1131 let c;try{c=await api("/api/creators/"+encodeURIComponent(id))}1132 catch(e){app.innerHTML=header()+`<div class="container"><div class="empty"><h2>Créateur introuvable</h2>1133  <p style="margin-top:10px"><a href="/" data-nav style="color:var(--green);font-weight:600">← Retour à l'annuaire</a></p></div></div>`+footer()+tabbar("");bindNav();return}1134 state.creator=c;1135 document.title=`${c.display_name} — Créa-Ka`;1136 const verified=(c.platforms||[]).some(a=>a.verified);1137 const niches=(c.niches||[]).map(n=>`<span class="mchip">${NICHE_LBL[n]||n}</span>`).join("");1138 const P=(c.platforms||[]).slice().sort((x,y)=>(y.followers||0)-(x.followers||0));1139 const g=computeAgg(c);1140 const ins=c.insights||{};1141 const banner=c.banner_url||P.map(a=>(a.metrics||{}).banner).find(Boolean);1142 // bandeau de métriques CALCULÉES (multi-plateforme)1143 const kpi=(v,l)=>v==null?"":`<div class="hkpi"><b>${typeof v==="number"?fmt.format(v):esc(v)}</b><span>${l}</span></div>`;1144 const gkpi=(d,l)=>!d||!d.delta?"":`<div class="hkpi"><b class="${d.delta>=0?"growth-up":"growth-down"}">${d.delta>=0?"+":""}${fmt.format(d.delta)}</b><span>${l}</span></div>`;1145 const heroKpis=[1146  kpi(g.reach,"abonnés cumulés"),1147  gkpi(ins.growth_7d,"abonnés / 7 j"),1148  gkpi(ins.growth_30d,"abonnés / 30 j"),1149  kpi(g.nPlat,"plateformes"),1150  kpi(ins.avg_engagement_pct!=null?ins.avg_engagement_pct+" %":(g.avgEng!=null?Math.round(g.avgEng*100)/100+" %":null),"engagement moyen"),1151  kpi(ins.publications_per_week!=null?ins.publications_per_week:g.maxCad,"pubs / semaine"),1152  kpi(g.content||null,"contenus analysés"),1153  kpi(g.main?(PLAT[g.main.platform]||PLAT.autre).label:null,"plateforme nº 1"),1154 ].filter(Boolean).join("");1155 // Ka Score /100 (insights serveur) + sparkline d'audience (snapshots)1156 const KS_MAX={audience:40,engagement:25,presence:20,rythme:10,verification:5};1157 const KS_LBL={audience:"Audience",engagement:"Engagement",presence:"Multi-plateforme",1158  rythme:"Rythme de pub.",verification:"Vérification"};1159 const ksParts=ins.ka_score_parts||{};1160 const ksHtml=ins.ka_score!=null?`<h3 class="cg-title">Ka Score</h3><div class="kascore">1161   <div class="kascore-num"><b>${ins.ka_score}</b><span>/ 100</span></div>1162   <div class="kascore-parts">${Object.keys(KS_MAX).map(k=>`<div class="kspart">1163    <span class="kspart-lab">${KS_LBL[k]}</span>1164    <span class="kspart-track"><span class="kspart-fill" style="width:${Math.min(100,Math.round((ksParts[k]||0)/KS_MAX[k]*100))}%"></span></span>1165    <span class="kspart-val">${ksParts[k]!=null?ksParts[k]:0}/${KS_MAX[k]}</span></div>`).join("")}</div>1166  </div>`:"";1167 const hist=((ins.history||{}).total)||[];1168 const sparkHtml=hist.length>1?`<h3 class="cg-title">Évolution de l'audience</h3>${sparkSvg(hist)}`:"";1169 // barres comparatives de portée par plateforme (vue d'ensemble)1170 const maxF=Math.max(1,...P.map(a=>a.followers||0));1171 const bars=P.map(a=>{const p=PLAT[a.platform]||PLAT.autre;const w=Math.round((a.followers||0)/maxF*100);1172  return `<div class="obar"><span class="obar-ico" style="background:${p.color}">${icon(a.platform)}</span>1173   <span class="obar-lab">${p.label}</span>1174   <span class="obar-track"><span class="obar-fill" style="width:${w}%;background:${p.color}"></span></span>1175   <span class="obar-val">${a.followers!=null?fmt.format(a.followers):"—"}</span></div>`;}).join("");1176 // onglets + panneaux1177 const tabs=`<button class="ptab on" onclick="showPtab('ov',this)">Vue d'ensemble</button>`+1178  P.map((a,i)=>{const p=PLAT[a.platform]||PLAT.autre;1179   return `<button class="ptab" onclick="showPtab(${i},this)">1180    <span class="ptab-ico" style="color:${p.color}">${icon(a.platform)}</span>${p.label}1181    ${a.followers!=null?`<span class="ptab-n">${fmt.format(a.followers)}</span>`:""}</button>`;}).join("");1182 const ovPanel=`<section class="ppanel on" id="ov-panel" data-panel="ov">1183   ${ksHtml}1184   <h3 class="cg-title">Portée par plateforme</h3>1185   <div class="obars">${bars||"<p class=klabel>Aucune plateforme.</p>"}</div>1186   ${sparkHtml}1187   ${c.link_in_bio_url?`<a class="linkinbio" href="${esc(c.link_in_bio_url)}" target="_blank" rel="noopener">${icon("linktree")} Sa page de liens officielle ↗</a>`:""}1188  </section>`;1189 const panels=ovPanel+P.map((a,i)=>platformPanel(a,i,false)).join("");1190 const checked=(c.platforms||[]).map(a=>{const m=a.metrics||{};return m[a.platform+"_apify_at"]||a.last_checked;}).filter(Boolean).sort().pop();1191 app.innerHTML=header()+`1192 <div class="container cpage">1193  <a class="backlink" href="/" data-nav>← Tous les créateurs</a>1194  ${banner?`<div class="cbanner"><img src="${esc(banner)}" alt="" referrerpolicy="no-referrer" onerror="this.parentElement.remove()"></div>`:""}1195  <div class="cpage-head">1196   <div class="cavatar">${esc(initials(c.display_name))}${photoImg(c)}</div>1197   <div style="flex:1;min-width:200px"><h1>${esc(c.display_name)}${verified?checkSvg:""}</h1>1198    <div class="meta"><span class="mchip o">${TIER_LBL[c.audience_tier]||c.audience_tier}</span>1199     ${niches}${c.region?`<span class="mchip">${esc(c.region)}</span>`:""}1200     ${(c.languages||[]).map(l=>`<span class="mchip">${{fr:"Français",en:"Anglais",bilingue:"Bilingue"}[l]||l}</span>`).join("")}</div>1201    ${c.bio?`<p class="cpage-bio">${esc(c.bio)}</p>`:""}1202    <div id="fav-slot" style="margin-top:16px"></div></div>1203  </div>1204  <div class="hkpi-row">${heroKpis}</div>1205  ${c.notes?`<div class="cpage-notes"><span class="klabel">Contexte public</span><br>${esc(c.notes)}</div>`:""}1206  <div class="ptabs">${tabs}</div>1207  <div class="ppanels">${panels}</div>1208  <div class="cpage-foot">Sources : listes médias publiques, Wikidata, annuaires publics et profils publics des plateformes (enrichissement Apify).1209   Audiences et métriques approximatives${checked?`, dernière vérification ${esc(String(checked).slice(0,10))}`:""}.1210   Une erreur dans cette fiche ? <a href="/retrait" data-nav>Signaler / demander un retrait</a>.</div>1211 </div>`+footer()+tabbar("");1212 bindNav();window.scrollTo(0,0);1213}12141215/* ---------- page stats : tableau de bord analytique (contrat ka-stats) ---------- */1216const ST={period:"30j",from:"",to:"",data:null,hide:{},tbl:{}};1217const PERIODS_UI=[["auj","Aujourd'hui"],["7j","7 jours"],["30j","30 jours"],["3m","3 mois"],1218 ["6m","6 mois"],["12m","12 mois"],["annee","Année en cours"],["tout","Tout"]];1219const fmtPct=v=>(v>=0?"+":"")+v.toLocaleString("fr-CA",{maximumFractionDigits:1})+" %";1220const emptyBlock=t=>`<div class="empty-block"><b style="font-family:var(--font-display);font-size:14px">${esc(t)}</b>1221 <p class="klabel" style="margin:6px 0 0">Pas encore mesuré — aucune donnée disponible pour cette période.</p></div>`;12221223/* mini-tendance dans une carte KPI (série spark du contrat v2) */1224function sparkSvg(spark,hero){1225 if(!spark||spark.length<2)return"";1226 const W=120,H=28,vs=spark.map(p=>p.v);1227 const mn=Math.min(...vs),mx=Math.max(...vs),rng=(mx-mn)||1;1228 const pts=spark.map((p,i)=>`${(i/(spark.length-1)*W).toFixed(1)},${(H-2-(H-4)*((p.v-mn)/rng)).toFixed(1)}`).join(" ");1229 return `<svg class="spark" viewBox="0 0 ${W} ${H}" preserveAspectRatio="none" aria-hidden="true">1230  <polyline points="${pts}" fill="none" stroke="${hero?"var(--accent-soft)":"var(--accent)"}" stroke-width="2"/></svg>`;1231}1232function kpiHtml(k,i){1233 const v=k.value,hero=i===0;1234 const num=typeof v==="number"?(v>=1e6?fmt.format(v):fmtFull.format(v)):esc(v);1235 let delta="";1236 if(k.delta_pct!==undefined&&k.delta_pct!==null){1237  const up=(k.direction||(k.delta_pct>=0?"up":"down"))==="up";1238  delta=`<div class="tile-d ${up?"up":"down"}">${up?"▲":"▼"} ${fmtPct(k.delta_pct)} <span style="opacity:.6">vs période préc.</span></div>`;}1239 return `<div class="tile${hero?" hero-tile":""}"${typeof v==="number"?` title="${fmtFull.format(v)}"`:""}>1240  <div class="tile-v">${num}${k.unit?`<span style="font-size:.42em;opacity:.6"> ${esc(k.unit)}</span>`:""}</div>1241  <div class="tile-k">${esc(k.label)}</div>${delta}${sparkSvg(k.spark,hero)}</div>`;1242}12431244/* --- jauge demi-arc (taux, couvertures, complétude) --- */1245function gaugeHtml(g){1246 if(typeof g.value!=="number"||!g.max)return"";1247 const frac=Math.max(0,Math.min(1,g.value/g.max));1248 const R=52,C=Math.PI*R;1249 return `<div class="gauge" title="${esc(g.label)} — ${g.value.toLocaleString("fr-CA")}${g.unit?" "+esc(g.unit):""} (${Math.round(frac*100)} % de ${fmtFull.format(g.max)})">1250  <svg viewBox="0 0 120 68" role="img" aria-label="${esc(g.label)}">1251   <path d="M8 60 A${R} ${R} 0 0 1 112 60" fill="none" stroke="var(--line)" stroke-width="11" stroke-linecap="round"/>1252   ${frac>0?`<path d="M8 60 A${R} ${R} 0 0 1 112 60" fill="none" stroke="var(--accent)" stroke-width="11" stroke-linecap="round"1253    stroke-dasharray="${(frac*C).toFixed(1)} ${(C+20).toFixed(1)}"/>`:""}1254  </svg>1255  <div class="g-v">${g.value.toLocaleString("fr-CA",{maximumFractionDigits:1})}${g.unit?`<span style="font-size:.58em;opacity:.65"> ${esc(g.unit)}</span>`:""}</div>1256  <div class="g-l">${esc(g.label)}</div></div>`;1257}12581259/* --- statistiques d'une série : min / max / moyenne / médiane / écart-type --- */1260function statSummary(s){1261 const vs=(s.points||[]).map(p=>p.v).filter(v=>typeof v==="number");1262 if(vs.length<2)return"";1263 const sv=[...vs].sort((a,b)=>a-b),mean=vs.reduce((a,b)=>a+b,0)/vs.length;1264 const med=sv[Math.floor(sv.length/2)];1265 const sd=Math.sqrt(vs.reduce((a,v)=>a+(v-mean)*(v-mean),0)/vs.length);1266 const f=v=>fmtFull.format(Math.round(v*100)/100);1267 return `<div class="statsum"><span class="chip">min<b>${f(sv[0])}</b></span>1268  <span class="chip">max<b>${f(sv[sv.length-1])}</b></span>1269  <span class="chip">moyenne<b>${f(mean)}</b></span>1270  <span class="chip">médiane<b>${f(med)}</b></span>1271  <span class="chip">écart-type σ<b>${f(sd)}</b></span></div>`;1272}12731274/* --- barres verticales : série kind=bar (volumes/jour) & histogrammes (bins) --- */1275function vbarsHtml(spec){1276 const pts=(spec.points||(spec.bins||[]).map(b=>({t:b.label,v:b.value})))1277  .filter(p=>typeof p.v==="number");1278 if(!pts.length||!pts.some(p=>p.v>0))return emptyBlock(spec.title);1279 const W=720,H=250,PL=54,PR=12,PT=14,PB=40,cw=W-PL-PR,ch=H-PT-PB;1280 const vmax=Math.max(...pts.map(p=>p.v),1);1281 let grid="";1282 for(let k=0;k<5;k++){const y=PT+ch*k/4,v=vmax*(4-k)/4;1283  grid+=`<line x1="${PL}" x2="${W-PR}" y1="${y.toFixed(1)}" y2="${y.toFixed(1)}" stroke="var(--line)"/>1284   <text x="${PL-6}" y="${(y+3).toFixed(1)}" text-anchor="end" font-size="10" fill="var(--ink-3)" font-family="var(--font-mono)">${fmtFull.format(Math.round(v))}</text>`;}1285 const n=pts.length,bw=Math.max(2,cw/n-3);1286 const bars=pts.map((p,i)=>{const bh=ch*p.v/vmax,x=PL+cw*i/n+1.5;1287  return `<rect x="${x.toFixed(1)}" y="${(PT+ch-bh).toFixed(1)}" width="${bw.toFixed(1)}" height="${Math.max(bh,p.v>0?1:0).toFixed(1)}"1288   fill="var(--accent)" stroke="var(--ink)" stroke-width="${n>40?0.4:1}"><title>${esc(p.t)} — ${fmtFull.format(p.v)}${spec.unit?" "+esc(spec.unit):""}</title></rect>`;}).join("");1289 const lblIdx=n<=10?pts.map((_,i)=>i):[0,Math.floor(n/2),n-1];1290 const xl=lblIdx.map(i=>`<text x="${(PL+cw*i/n+bw/2).toFixed(1)}" y="${H-8}" text-anchor="middle"1291   font-size="${n<=10?10:10}" fill="var(--ink-3)" font-family="var(--font-mono)">${esc(String(pts[i].t))}</text>`).join("");1292 return `<div class="viz-card"><h2>${esc(spec.title)}</h2>1293  ${spec.unit?`<div class="viz-sub">${esc(spec.unit)}</div>`:'<div style="height:10px"></div>'}1294  <svg viewBox="0 0 ${W} ${H}" role="img" aria-label="${esc(spec.title)}">${grid}${bars}${xl}</svg></div>`;1295}12961297/* --- multi-courbes (≤ 4) : identité par MOTIF de trait en plus de la couleur --- */1298const ML_STYLES=[1299 {stroke:"var(--accent)",w:2.6,dash:"",cls:"",name:"trait plein accent"},1300 {stroke:"var(--ink)",w:2,dash:"",cls:"ml1",name:"trait plein encre"},1301 {stroke:"var(--accent)",w:2.2,dash:"7 4",cls:"ml2",name:"tirets accent"},1302 {stroke:"var(--ink-3)",w:2,dash:"2 3",cls:"ml3",name:"pointillé gris"}];1303function multiFigure(ms,hide){1304 const series=(ms.series||[]).filter(s=>(s.points||[]).length>1).slice(0,4);1305 if(!series.length)return emptyBlock(ms.title);1306 const W=720,H=250,PL=54,PR=12,PT=14,PB=26,cw=W-PL-PR,ch=H-PT-PB;1307 const shown=series.filter((_,i)=>!hide[i]);1308 const all=(shown.length?shown:series).flatMap(s=>s.points.map(p=>p.v));1309 const vmax=Math.max(...all,1),vmin=Math.min(0,...all),rng=(vmax-vmin)||1;1310 let grid="";1311 for(let k=0;k<5;k++){const y=PT+ch*k/4,v=vmax-(vmax-vmin)*k/4;1312  grid+=`<line x1="${PL}" x2="${W-PR}" y1="${y.toFixed(1)}" y2="${y.toFixed(1)}" stroke="var(--line)"/>1313   <text x="${PL-6}" y="${(y+3).toFixed(1)}" text-anchor="end" font-size="10" fill="var(--ink-3)" font-family="var(--font-mono)">${fmtFull.format(Math.round(v))}</text>`;}1314 const X=(i,n)=>PL+cw*i/((n-1)||1),Y=v=>PT+ch*(1-(v-vmin)/rng);1315 const paths=series.map((s,si)=>{if(hide[si])return"";1316  const st=ML_STYLES[si];1317  const d=s.points.map((p,i)=>`${i?"L":"M"}${X(i,s.points.length).toFixed(1)},${Y(p.v).toFixed(1)}`).join("");1318  return `<path d="${d}" fill="none" stroke="${st.stroke}" stroke-width="${st.w}"${st.dash?` stroke-dasharray="${st.dash}"`:""}/>`;}).join("");1319 const ref=series[0].points;1320 const xl=[0,Math.floor(ref.length/2),ref.length-1].map(i=>1321  `<text x="${X(i,ref.length).toFixed(1)}" y="${H-8}" text-anchor="middle" font-size="10" fill="var(--ink-3)" font-family="var(--font-mono)">${esc(ref[i].t)}</text>`).join("");1322 const leg=series.map((s,si)=>`<button type="button" class="leg-btn" data-mls="${si}" aria-pressed="${!hide[si]}"1323   title="${ML_STYLES[si].name}"><span class="leg-swatch ${ML_STYLES[si].cls}"></span>${esc(s.label)}</button>`).join("");1324 return `<div class="viz-card">1325  <div style="display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px;align-items:center">1326   <h2>${esc(ms.title)}</h2><span class="lc-legend">${leg}</span></div>1327  <svg viewBox="0 0 ${W} ${H}" role="img" aria-label="${esc(ms.title)}" style="margin-top:10px">1328   ${grid}${xl}${paths}1329   <g class="lc-cursor" style="display:none"><line y1="${PT}" y2="${H-PB}" stroke="var(--ink)" stroke-dasharray="2 3"/></g>1330  </svg><span class="chip lc-tip">&nbsp;</span></div>`;1331}1332function mountMulti(el,ms){1333 const hide=ST.hide["ml:"+ms.id]||(ST.hide["ml:"+ms.id]={});1334 const series=(ms.series||[]).filter(s=>(s.points||[]).length>1).slice(0,4);1335 const render=()=>{1336  el.innerHTML=multiFigure(ms,hide);1337  el.querySelectorAll("[data-mls]").forEach(b=>b.addEventListener("click",()=>{1338   hide[b.dataset.mls]=!hide[b.dataset.mls];render();}));1339  const svg=el.querySelector("svg");if(!svg||!series.length)return;1340  const W=720,PL=54,PR=12,n=series[0].points.length;1341  const cursor=el.querySelector(".lc-cursor"),tip=el.querySelector(".lc-tip");1342  const cl=cursor?cursor.querySelector("line"):null;1343  svg.addEventListener("pointermove",e=>{1344   const r=svg.getBoundingClientRect();1345   const fx=(e.clientX-r.left)/r.width*W;1346   let i=Math.round((fx-PL)/(W-PL-PR)*(n-1));i=Math.max(0,Math.min(n-1,i));1347   const x=(PL+(W-PL-PR)*i/((n-1)||1)).toFixed(1);1348   if(cl){cl.setAttribute("x1",x);cl.setAttribute("x2",x);cursor.style.display="";}1349   const vals=series.map((s,si)=>hide[si]?null:`${esc(s.label)} : <b>&nbsp;${fmtFull.format((s.points[i]||{}).v??0)}</b>`).filter(Boolean).join(" · ");1350   tip.innerHTML=`${esc(series[0].points[i].t)} — ${vals}${ms.unit?` <span style="color:var(--ink-3)">${esc(ms.unit)}</span>`:""}`;1351   tip.style.visibility="visible";});1352  svg.addEventListener("pointerleave",()=>{if(cursor)cursor.style.display="none";tip.style.visibility="hidden"});1353 };1354 render();1355}13561357/* --- barres empilées (composition dans le temps, nuances d'accent) --- */1358function stackedHtml(st){1359 const keys=(st.keys||[]).slice(0,6),pts=st.points||[];1360 if(!keys.length||!pts.length)return emptyBlock(st.title);1361 const totals=pts.map(p=>(p.values||[]).slice(0,keys.length).reduce((a,b)=>a+(b||0),0));1362 if(!totals.some(t=>t>0))return emptyBlock(st.title);1363 const W=720,H=250,PL=54,PR=12,PT=14,PB=26,cw=W-PL-PR,ch=H-PT-PB;1364 const vmax=Math.max(...totals,1),shades=[1,.78,.58,.42,.3,.22];1365 let grid="";1366 for(let k=0;k<5;k++){const y=PT+ch*k/4,v=vmax*(4-k)/4;1367  grid+=`<line x1="${PL}" x2="${W-PR}" y1="${y.toFixed(1)}" y2="${y.toFixed(1)}" stroke="var(--line)"/>1368   <text x="${PL-6}" y="${(y+3).toFixed(1)}" text-anchor="end" font-size="10" fill="var(--ink-3)" font-family="var(--font-mono)">${fmtFull.format(Math.round(v))}</text>`;}1369 const n=pts.length,bw=Math.max(2,cw/n-3);1370 const bars=pts.map((p,i)=>{let y=PT+ch;const x=PL+cw*i/n+1.5;1371  return keys.map((k,j)=>{const v=(p.values||[])[j]||0;if(!v)return"";1372   const bh=ch*v/vmax;y-=bh;1373   return `<rect x="${x.toFixed(1)}" y="${y.toFixed(1)}" width="${bw.toFixed(1)}" height="${Math.max(bh-.4,.5).toFixed(1)}"1374    fill="var(--accent)" fill-opacity="${shades[j%6]}" stroke="var(--paper)" stroke-width="0.4">1375    <title>${esc(p.t)} · ${esc(k)} — ${fmtFull.format(v)}${st.unit?" "+esc(st.unit):""} (total ${fmtFull.format(totals[i])})</title></rect>`;}).join("");}).join("");1376 const lblIdx=[0,Math.floor(n/2),n-1];1377 const xl=lblIdx.map(i=>`<text x="${(PL+cw*i/n+bw/2).toFixed(1)}" y="${H-8}" text-anchor="middle" font-size="10" fill="var(--ink-3)" font-family="var(--font-mono)">${esc(String(pts[i].t))}</text>`).join("");1378 const leg=keys.map((k,j)=>`<span class="leg-btn" style="min-height:auto"><span class="dsw" style="width:11px;height:11px;border-radius:3px;border:1px solid var(--ink);background:var(--accent);opacity:${shades[j%6]};display:inline-block"></span>&nbsp;${esc(k)}</span>`).join("");1379 return `<div class="viz-card">1380  <div style="display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px;align-items:center">1381   <h2>${esc(st.title)}</h2><span class="lc-legend">${leg}</span></div>1382  <svg viewBox="0 0 ${W} ${H}" role="img" aria-label="${esc(st.title)}" style="margin-top:10px">${grid}${bars}${xl}</svg></div>`;1383}13841385/* --- heatmap horaire 7 × 24 (dow 0=lun … 6=dim) --- */1386function hourHeatHtml(h){1387 const cells=h&&h.cells||[];1388 if(!cells.length)return emptyBlock(h?h.title:"Activité horaire");1389 const grid=new Map(cells.map(c=>[c.dow+":"+c.hour,c.value||0]));1390 const max=Math.max(...cells.map(c=>c.value||0),1);1391 const CS=22,LX=42,LY=18,DOWS=["Lun","Mar","Mer","Jeu","Ven","Sam","Dim"];1392 let out="";1393 [0,6,12,18,23].forEach(hh=>{out+=`<text x="${LX+hh*CS+CS/2}" y="12" text-anchor="middle" font-size="10" fill="var(--ink-3)" font-family="var(--font-mono)">${hh} h</text>`;});1394 for(let d=0;d<7;d++){1395  out+=`<text x="${LX-6}" y="${LY+d*CS+CS/2+3.5}" text-anchor="end" font-size="10" fill="var(--ink-3)" font-family="var(--font-mono)">${DOWS[d]}</text>`;1396  for(let hh=0;hh<24;hh++){const v=grid.get(d+":"+hh)||0;1397   out+=`<rect x="${LX+hh*CS}" y="${LY+d*CS}" width="${CS-2.5}" height="${CS-2.5}" rx="3.5"1398    fill="${v?"var(--accent)":"rgba(20,24,20,0.06)"}" fill-opacity="${v?(0.2+0.8*v/max).toFixed(2):1}"1399    stroke="rgba(20,24,20,0.14)" stroke-width="0.5"><title>${DOWS[d]} ${hh} h — ${fmtFull.format(v)}</title></rect>`;}}1400 return `<div class="viz-card hh"><h2>${esc(h.title)}</h2><div class="viz-sub">7 jours × 24 heures (heure de l'Est)</div>1401  <div class="tbl-wrap"><svg viewBox="0 0 ${LX+24*CS} ${LY+7*CS}" role="img" aria-label="${esc(h.title)}">${out}</svg></div></div>`;1402}14031404/* --- courbe SVG : infobulle (pointeur), légende cliquable, comparaison N-1 --- */1405function lineGeom(s,hide){1406 const W=720,H=240,PL=54,PR=12,PT=14,PB=26,pts=s.points||[];1407 const all=[...(hide.cur?[]:pts),...(!hide.cmp&&s.compare?s.compare:[])];1408 const vmax=Math.max(...all.map(p=>p.v),1),vmin=Math.min(0,...all.map(p=>p.v));1409 const X=(i,n)=>PL+(W-PL-PR)*i/((n||pts.length)-1);1410 const Y=v=>PT+(H-PT-PB)*(1-(v-vmin)/((vmax-vmin)||1));1411 return{W,H,PL,PR,PT,PB,vmax,vmin,X,Y};1412}1413function lineFigure(s,hide){1414 const pts=s.points||[];1415 if(pts.length<2)return emptyBlock(s.title);1416 const g=lineGeom(s,hide),{W,H,PL,PR,PT,PB,vmax,vmin,X,Y}=g;1417 const path=arr=>arr.map((p,i)=>`${i?"L":"M"}${X(i,arr.length).toFixed(1)},${Y(p.v).toFixed(1)}`).join("");1418 let grid="";1419 for(let k=0;k<5;k++){const y=PT+(H-PT-PB)*k/4,v=vmax-(vmax-vmin)*k/4;1420  grid+=`<line x1="${PL}" x2="${W-PR}" y1="${y.toFixed(1)}" y2="${y.toFixed(1)}" stroke="var(--line)"/>1421   <text x="${PL-6}" y="${(y+3).toFixed(1)}" text-anchor="end" font-size="10" fill="var(--ink-3)" font-family="var(--font-mono)">${fmtFull.format(Math.round(v))}</text>`;}1422 const xl=[0,Math.floor(pts.length/2),pts.length-1].map(i=>1423  `<text x="${X(i).toFixed(1)}" y="${H-8}" text-anchor="middle" font-size="10" fill="var(--ink-3)" font-family="var(--font-mono)">${esc(pts[i].t)}</text>`).join("");1424 return `<div class="viz-card">1425  <div style="display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px;align-items:center">1426   <h2>${esc(s.title)}</h2>1427   <span class="lc-legend">1428    <button type="button" class="leg-btn" data-leg="cur" aria-pressed="${!hide.cur}"><span class="leg-swatch"></span>Période courante</button>1429    ${s.compare?`<button type="button" class="leg-btn" data-leg="cmp" aria-pressed="${!hide.cmp}"><span class="leg-swatch cmp"></span>Période comparée</button>`:""}1430   </span></div>1431  <svg viewBox="0 0 ${g.W} ${g.H}" role="img" aria-label="${esc(s.title)}" style="margin-top:10px">1432   ${grid}${xl}1433   ${!hide.cmp&&s.compare&&s.compare.length>1?`<path d="${path(s.compare)}" fill="none" stroke="var(--ink-3)" stroke-width="1.4" stroke-dasharray="4 4"/>`:""}1434   ${!hide.cur&&s.kind==="area"?`<path d="${path(pts)} L${X(pts.length-1).toFixed(1)},${(H-PB).toFixed(1)} L${X(0).toFixed(1)},${(H-PB).toFixed(1)} Z" fill="var(--accent)" fill-opacity=".12" stroke="none"/>`:""}1435   ${!hide.cur?`<path d="${path(pts)}" fill="none" stroke="var(--accent)" stroke-width="2.4"/>`:""}1436   <g class="lc-cursor" style="display:none">1437    <line y1="${PT}" y2="${H-PB}" stroke="var(--ink)" stroke-dasharray="2 3"/>1438    <circle r="4" fill="var(--accent)" stroke="var(--ink)" stroke-width="1.5"/></g>1439  </svg><span class="chip lc-tip">&nbsp;</span>`+statSummary(s)+1440 `</div>`;1441}1442function mountLine(el,s){1443 if(s.kind==="bar"){el.innerHTML=vbarsHtml(s);return;}1444 const hide=ST.hide[s.id]||(ST.hide[s.id]={cur:false,cmp:false});1445 const render=()=>{1446  el.innerHTML=lineFigure(s,hide);1447  el.querySelectorAll("[data-leg]").forEach(b=>b.addEventListener("click",()=>{1448   hide[b.dataset.leg]=!hide[b.dataset.leg];render();}));1449  const svg=el.querySelector("svg");if(!svg)return;1450  const pts=s.points,g=lineGeom(s,hide);1451  const cursor=el.querySelector(".lc-cursor"),tip=el.querySelector(".lc-tip");1452  const cl=cursor.querySelector("line"),cc=cursor.querySelector("circle");1453  svg.addEventListener("pointermove",e=>{1454   const r=svg.getBoundingClientRect();1455   const fx=(e.clientX-r.left)/r.width*g.W;1456   let i=Math.round((fx-g.PL)/(g.W-g.PL-g.PR)*(pts.length-1));1457   i=Math.max(0,Math.min(pts.length-1,i));1458   const x=g.X(i).toFixed(1);1459   cl.setAttribute("x1",x);cl.setAttribute("x2",x);1460   cc.setAttribute("cx",x);cc.setAttribute("cy",g.Y(pts[i].v).toFixed(1));1461   cursor.style.display="";1462   const cmp=s.compare&&!hide.cmp&&s.compare[i]?` · N-1 : ${fmtFull.format(s.compare[i].v)}`:"";1463   tip.innerHTML=`${esc(pts[i].t)} — <b>&nbsp;${fmtFull.format(pts[i].v)}${s.unit?" "+esc(s.unit):""}</b><span style="color:var(--ink-3)">${cmp}</span>`;1464   tip.style.visibility="visible";});1465  svg.addEventListener("pointerleave",()=>{cursor.style.display="none";tip.style.visibility="hidden"});1466 };1467 render();1468}14691470/* --- anneau (top 7 + « Autres ») --- */1471function donutHtml(b){1472 let rows=(b.items||[]).filter(i=>i.value>0);1473 if(!rows.length)return emptyBlock(b.title);1474 if(rows.length>8){const rest=rows.slice(7).reduce((s,r)=>s+r.value,0);1475  rows=rows.slice(0,7);rows.push({label:"Autres",value:rest});}1476 const total=rows.reduce((s,r)=>s+r.value,0);1477 const R=74,C=2*Math.PI*R,shades=[1,.78,.58,.42,.3,.22,.15,.1];let acc=0;1478 const segs=rows.map((r,i)=>{const frac=r.value/total,off=acc;acc+=frac;1479  return `<circle cx="100" cy="100" r="${R}" fill="none" stroke="var(--accent)" stroke-opacity="${shades[i%8]}"1480   stroke-width="30" stroke-dasharray="${(frac*C).toFixed(2)} ${C.toFixed(2)}" stroke-dashoffset="${(-off*C).toFixed(2)}"1481   transform="rotate(-90 100 100)"><title>${esc(r.label)} — ${fmtFull.format(r.value)}</title></circle>`;}).join("");1482 const leg=rows.map((r,i)=>`<li><span class="dsw" style="opacity:${shades[i%8]}"></span>1483  <span class="dlbl" title="${esc(r.label)} — ${fmtFull.format(r.value)}">${esc(r.label)}</span>1484  <b>${(100*r.value/total).toFixed(1).replace(".",",")} %</b></li>`).join("");1485 return `<div class="viz-card"><h2>${esc(b.title)}</h2><div class="donut-wrap">1486  <svg viewBox="0 0 200 200" role="img" aria-label="${esc(b.title)}" style="width:180px;max-width:100%">${segs}1487   <circle cx="100" cy="100" r="${R}" fill="none" stroke="var(--ink)" opacity=".5"/></svg>1488  <ul class="donut-legend">${leg}</ul></div></div>`;1489}14901491/* --- barres horizontales (répartitions, géo) --- */1492function barsHtml(title,items,sub){1493 const rows=(items||[]).slice(0,14);1494 if(!rows.length)return emptyBlock(title);1495 const max=Math.max(...rows.map(r=>r.value),1);1496 return `<div class="viz-card"><h2>${esc(title)}</h2>${sub?`<div class="viz-sub">${esc(sub)}</div>`:`<div style="height:14px"></div>`}1497  ${rows.map(r=>`<div class="hrow" title="${esc(r.label)} — ${fmtFull.format(r.value)}${r.delta_pct!=null?` (${fmtPct(r.delta_pct)} sur la période)`:""}">1498   <span class="hrow-label">${esc(r.label)}</span>1499   <span class="hbar"><i style="width:${Math.max(2,Math.round(r.value/max*100))}%"></i></span>1500   <span class="n">${fmtFull.format(r.value)}</span>${r.delta_pct!=null?`<span class="d ${r.delta_pct>=0?"up":"down"}">${r.delta_pct>=0?"▲":"▼"} ${fmtPct(r.delta_pct)}</span>`:""}</div>`).join("")}</div>`;1501}15021503/* --- calendrier de chaleur (26 dernières semaines) --- */1504function heatmapHtml(h){1505 const cells=h&&h.cells||[];1506 if(!cells.length)return emptyBlock(h?h.title:"Activité");1507 const byDate=new Map(cells.map(c=>[c.date,c.value]));1508 const dates=cells.map(c=>c.date).sort();1509 const end=new Date(dates[dates.length-1]+"T12:00:00");1510 const max=Math.max(...cells.map(c=>c.value),1);1511 const weeks=26,cols=[];const cur=new Date(end);1512 cur.setDate(cur.getDate()-(weeks*7-1));1513 for(let w=0;w<weeks;w++){const col=[];1514  for(let d=0;d<7;d++){const iso=cur.toISOString().slice(0,10);1515   col.push({date:iso,v:byDate.get(iso)||0});cur.setDate(cur.getDate()+1);}1516  cols.push(col);}1517 const rects=cols.map((col,w)=>col.map((c,d)=>1518  `<rect x="${w*14}" y="${d*14}" width="12" height="12" rx="2.5"1519    fill="${c.v?"var(--accent)":"rgba(20,24,20,0.07)"}" fill-opacity="${c.v?(0.25+0.75*c.v/max).toFixed(2):1}"1520    stroke="rgba(20,24,20,0.15)" stroke-width="0.5"><title>${c.date} — ${fmtFull.format(c.v)}</title></rect>`).join("")).join("");1521 return `<div class="viz-card hm"><h2>${esc(h.title)}</h2><div class="viz-sub">26 dernières semaines</div>1522  <div class="tbl-wrap"><svg viewBox="0 0 ${weeks*14} ${7*14}" role="img" aria-label="${esc(h.title)}">${rects}</svg></div></div>`;1523}15241525/* --- tableau : tri par colonne, recherche, pagination 25/pg --- */1526function mountTable(el,spec){1527 const st=ST.tbl[spec.id]||(ST.tbl[spec.id]={q:"",sort:null,page:0});1528 el.innerHTML=`<div class="viz-card dtable">1529  <div style="display:flex;flex-wrap:wrap;gap:10px;justify-content:space-between;align-items:center">1530   <h2>${esc(spec.title)}</h2>1531   <input class="dt-q" placeholder="Rechercher…" value="${esc(st.q)}" aria-label="Rechercher dans ${esc(spec.title)}">1532  </div><div class="dt-body"></div></div>`;1533 const body=el.querySelector(".dt-body"),qi=el.querySelector(".dt-q");1534 const num=x=>typeof x==="number"?x:parseFloat(String(x).replace(/[^\d.,-]/g,"").replace(",","."));1535 const render=()=>{1536  let rows=spec.rows||[];1537  if(st.q){const q=st.q.toLowerCase();1538   rows=rows.filter(r=>r.some(c=>String(c).toLowerCase().includes(q)));}1539  if(st.sort){const{col,dir}=st.sort;1540   rows=[...rows].sort((a,b)=>{const nx=num(a[col]),ny=num(b[col]);1541    if(!Number.isNaN(nx)&&!Number.isNaN(ny))return(nx-ny)*dir;1542    return String(a[col]).localeCompare(String(b[col]),"fr")*dir;});}1543  const pages=Math.max(1,Math.ceil(rows.length/25));1544  st.page=Math.min(st.page,pages-1);const cur=st.page;1545  body.innerHTML=`<div class="tbl-wrap" style="margin-top:12px"><table><thead><tr>1546    ${spec.columns.map((c,i)=>`<th data-col="${i}" aria-sort="${st.sort&&st.sort.col===i?(st.sort.dir===1?"ascending":"descending"):"none"}">${esc(c)} ${st.sort&&st.sort.col===i?(st.sort.dir===1?"▲":"▼"):"↕"}</th>`).join("")}1547   </tr></thead><tbody>1548    ${rows.slice(cur*25,(cur+1)*25).map(r=>`<tr>${r.map(c=>`<td>${typeof c==="number"?fmtFull.format(c):esc(c)}</td>`).join("")}</tr>`).join("")||`<tr><td colspan="${spec.columns.length}" style="color:var(--ink-3)">Aucun résultat.</td></tr>`}1549   </tbody></table></div>1550   <div class="dtable-foot"><span class="klabel">${fmtFull.format(rows.length)} ligne${rows.length>1?"s":""}</span>1551    <span style="display:flex;gap:6px;align-items:center">1552     <button type="button" class="btn btn-ghost dt-prev"${cur===0?" disabled":""}>←</button>1553     <span class="chip">${cur+1} / ${pages}</span>1554     <button type="button" class="btn btn-ghost dt-next"${cur>=pages-1?" disabled":""}>→</button></span></div>`;1555  body.querySelectorAll("th").forEach(th=>th.addEventListener("click",()=>{1556   const col=+th.dataset.col;1557   st.sort={col,dir:st.sort&&st.sort.col===col&&st.sort.dir===1?-1:1};render();}));1558  const prev=body.querySelector(".dt-prev"),next=body.querySelector(".dt-next");1559  prev.addEventListener("click",()=>{st.page=Math.max(0,cur-1);render()});1560  next.addEventListener("click",()=>{st.page=Math.min(pages-1,cur+1);render()});1561 };1562 qi.addEventListener("input",deb(()=>{st.q=qi.value;st.page=0;render()},250));1563 render();1564}15651566/* --- barre de période + boutons PDF --- */1567function periodBar(){1568 const customOn=!!(ST.from&&ST.to);1569 return `<div class="pchips" role="group" aria-label="Période d'analyse">1570  ${PERIODS_UI.map(([id,l])=>`<button type="button" class="pchip" data-period="${id}" aria-pressed="${!customOn&&ST.period===id}">${l}</button>`).join("")}1571  <span class="pdates"><input type="date" id="pfrom" value="${esc(ST.from)}" aria-label="Du">1572   <span class="klabel">au</span><input type="date" id="pto" value="${esc(ST.to)}" aria-label="Au"></span></div>`;1573}1574function bindPeriod(){1575 document.querySelectorAll(".pchip").forEach(b=>b.addEventListener("click",()=>{1576  ST.period=b.dataset.period;ST.from="";ST.to="";loadStatsData();}));1577 const pf=$("#pfrom"),pt=$("#pto");1578 const go=()=>{if(pf.value&&pt.value){ST.from=pf.value;ST.to=pt.value;loadStatsData();}};1579 pf.addEventListener("change",go);pt.addEventListener("change",go);1580}1581/* --- menu PDF : 5 rapports (équivalent vanilla du PdfButton v2) --- */1582const PDF_MODES=[1583 ["synthese","Synthèse exécutive","Couverture, KPI, jauges & records (2-3 p.)"],1584 ["tendances","Tendances & évolution","Toutes les séries + min/max/moy/méd/σ"],1585 ["repartitions","Répartitions & géographie","Anneaux, distributions, géo, activité horaire"],1586 ["donnees","Données détaillées","Tous les tableaux en version longue (400 lignes)"]];1587function pdfActionsHtml(){1588 return `<span class="pdf-actions pdf-menu-wrap">1589  <button type="button" class="btn btn-primary" data-pdf="complet">⬇ Rapport PDF complet</button>1590  <button type="button" class="btn btn-ghost" id="pdf-more" aria-haspopup="true" aria-expanded="false">Autres rapports ▾</button>1591  <div class="pdf-menu" id="pdf-menu" role="menu">1592   ${PDF_MODES.map(([m,l,d])=>`<button type="button" role="menuitem" data-pdf="${m}">${l}<small>${d}</small></button>`).join("")}1593  </div></span>`;1594}1595function bindPdf(){1596 const url=m=>{const p=new URLSearchParams({period:ST.period,mode:m});1597  if(ST.from&&ST.to){p.set("from",ST.from);p.set("to",ST.to);}1598  return "/api/stats/report?"+p;};1599 const menu=document.getElementById("pdf-menu"),more=document.getElementById("pdf-more");1600 if(more&&menu){1601  more.addEventListener("click",e=>{e.stopPropagation();1602   const open=menu.classList.toggle("open");more.setAttribute("aria-expanded",open);1603   if(open)document.addEventListener("click",()=>{menu.classList.remove("open");1604    more.setAttribute("aria-expanded","false");},{once:true});});}1605 document.querySelectorAll("[data-pdf]").forEach(b=>{1606  b.addEventListener("click",async()=>{1607   if(menu)menu.classList.remove("open");1608   const t=b.innerHTML;b.disabled=true;1609   b.innerHTML=b.closest(".pdf-menu")?"Génération…":"⏳ Génération…";1610   try{const r=await fetch(url(b.dataset.pdf));if(!r.ok)throw new Error(r.status);1611    const blob=await r.blob();1612    const fn=((r.headers.get("Content-Disposition")||"").match(/filename="?([^";]+)/)||[])[1]||"rapport-crea-ka.pdf";1613    const a=document.createElement("a");a.href=URL.createObjectURL(blob);a.download=fn;1614    document.body.appendChild(a);a.click();a.remove();1615    setTimeout(()=>URL.revokeObjectURL(a.href),4000);}1616   catch(e){alert("Échec de la génération du PDF — réessayez.");}1617   b.disabled=false;b.innerHTML=t;});});1618}16191620async function renderStats(){1621 document.title="Statistiques — Créa-Ka";1622 app.innerHTML=header()+`<div class="container"><div class="spin"></div></div>`+footer()+tabbar("stats");bindNav();1623 await loadStatsData(true);1624 window.scrollTo(0,0);1625}1626async function loadStatsData(first){1627 const main=$("#stats-main");if(main)main.style.opacity=".45";1628 let d;1629 try{const p=new URLSearchParams({period:ST.period});1630  if(ST.from&&ST.to){p.set("from",ST.from);p.set("to",ST.to);}1631  d=await api("/api/stats/dashboard?"+p);}1632 catch(e){app.innerHTML=header()+`<div class="container empty">Statistiques indisponibles pour le moment.</div>`+footer()+tabbar("stats");bindNav();return}1633 ST.data=d;renderStatsPage();1634}1635function renderStatsPage(){1636 const d=ST.data;1637 const upd=new Date(d.updated).toLocaleString("fr-CA",{dateStyle:"medium",timeStyle:"short"});1638 const donuts=(d.breakdowns||[]).filter(b=>b.kind==="donut");1639 const barsB=(d.breakdowns||[]).filter(b=>b.kind!=="donut");1640 const gauges=(d.gauges||[]).filter(g=>typeof g.value==="number"&&g.max);1641 const hasEvo=(d.series||[]).length||(d.multiseries||[]).length||(d.stacked||[]).length;1642 app.innerHTML=header()+`1643 <div class="container stats-page" id="stats-main">1644  <div class="stats-head">1645   <div><span class="kicker">Tableau de bord — ${esc(d.period.label)}</span>1646    <h1 style="font-size:clamp(28px,4.6vw,46px);margin:12px 0 0;text-transform:uppercase">Les créateurs québécois <span class="hl">en chiffres.</span></h1>1647    <div class="fresh-row"><span>Mis à jour le ${esc(upd)}</span>1648     <button type="button" class="btn btn-ghost" id="st-refresh">↻ Rafraîchir</button></div></div>1649   ${pdfActionsHtml()}1650  </div>1651  <div class="tiles">${(d.kpis||[]).map(kpiHtml).join("")||emptyBlock("Indicateurs")}</div>1652  ${periodBar()}1653  <p class="klabel" style="margin:6px 0 0">Période analysée : ${esc(d.period.from)} → ${esc(d.period.to)}</p>1654  ${gauges.length?`<h2 class="stats-sect">Taux & couvertures</h2>1655   <div class="gauges">${gauges.map(gaugeHtml).join("")}</div>`:""}1656  <h2 class="stats-sect">Évolution</h2>1657  ${hasEvo?`<div class="viz-grid">1658   ${(d.series||[]).map(s=>`<div class="lc" data-serie="${esc(s.id)}"></div>`).join("")}1659   ${(d.multiseries||[]).map(m=>`<div class="lc" data-multi="${esc(m.id)}"></div>`).join("")}1660   ${(d.stacked||[]).map(st=>stackedHtml(st)).join("")}1661  </div>`:emptyBlock("Évolution")}1662  <h2 class="stats-sect">Répartitions & distributions</h2>1663  <div class="viz-grid two">1664   ${donuts.map(donutHtml).join("")}1665   ${barsB.map(b=>barsHtml(b.title,b.items)).join("")}1666   ${(d.distributions||[]).map(vbarsHtml).join("")}1667  </div>1668  ${d.geo?`<h2 class="stats-sect">Répartition géographique</h2>1669   <div class="viz-grid two">${barsHtml(d.geo.title,d.geo.items)}</div>`:""}1670  ${d.heatmap||d.hourly?`<h2 class="stats-sect">Activité</h2>1671   ${d.heatmap?heatmapHtml(d.heatmap):""}${d.hourly?hourHeatHtml(d.hourly):""}`:""}1672  ${(d.tables||[]).length?`<h2 class="stats-sect">Détails</h2><div style="display:grid;gap:22px">${(d.tables||[]).map(t=>`<div class="dt" data-table="${esc(t.id)}"></div>`).join("")}</div>`:""}1673  ${(d.records||[]).length?`<h2 class="stats-sect">Records & faits marquants</h2>1674   <div class="records-grid">${d.records.map(r=>`<div class="record"><span class="r-lbl">${esc(r.label)}</span>1675    <span style="text-align:right"><span class="r-val">${esc(r.value)}</span>${r.date?`<span class="klabel" style="display:block">${esc(r.date)}</span>`:""}</span></div>`).join("")}</div>`:""}1676  <p style="font-size:12px;color:var(--ink-3);margin-top:30px">Données réelles de l'annuaire (fiches actives, comptes vérifiés) — aucune statistique estimée ou inventée.1677   Rapports PDF estampillés Groupe-KA (5 formats) disponibles en haut de page.</p>1678 </div>`+footer()+tabbar("stats");1679 bindNav();bindPeriod();bindPdf();1680 $("#st-refresh").addEventListener("click",()=>loadStatsData());1681 (ST.data.series||[]).forEach(s=>{const el=document.querySelector(`.lc[data-serie="${s.id}"]`);if(el)mountLine(el,s);});1682 (ST.data.multiseries||[]).forEach(m=>{const el=document.querySelector(`.lc[data-multi="${m.id}"]`);if(el)mountMulti(el,m);});1683 (ST.data.tables||[]).forEach(t=>{const el=document.querySelector(`.dt[data-table="${t.id}"]`);if(el)mountTable(el,t);});1684}16851686/* ---------- page compte (KA ID) ---------- */1687async function renderCompte(){1688 document.title="Mon compte KA — Créa-Ka";1689 app.innerHTML=header()+`<div class="container"><div class="spin"></div></div>`+footer()+tabbar("");bindNav();1690 if(state.me===undefined){1691  try{const r=await fetch("/api/me");state.me=r.ok?await r.json():null}1692  catch(e){state.me=null}}1693 const me=state.me;1694 if(!me){1695  app.innerHTML=header()+`1696  <div class="container compte-page">1697   <span class="kicker">KA ID — un seul compte pour tout le groupe</span>1698   <h1 style="font-size:clamp(26px,4vw,34px);margin:12px 0;text-transform:uppercase">Connectez-vous avec votre <span class="hl">KA ID.</span></h1>1699   <p style="color:var(--ink-2)">Le même compte que sur groupe-ka.com, Lou-Ka, Vrai-Prix et1700    les autres plateformes du Groupe KA. Créez-le en une minute si vous n'en avez pas.</p>1701   <div class="compte-actions">1702    <a class="btn btn-primary" href="/api/auth/ka/login">Se connecter avec KA ID</a>1703    <a class="btn btn-accent" href="https://www.groupe-ka.com/connexion" target="_blank" rel="noopener">Créer un compte KA ID ↗</a>1704    <a class="btn btn-ghost" href="https://www.groupe-ka.com" target="_blank" rel="noopener">Qu'est-ce que KA ID ?</a>1705   </div>1706  </div>`+footer()+tabbar("");1707  bindNav();loadAccount();return;1708 }1709 const init=(me.name||"?").split(/\s+/).slice(0,2).map(w=>w[0]||"").join("").toUpperCase();1710 app.innerHTML=header()+`1711 <div class="container compte-page">1712  <span class="kicker">Mon compte</span>1713  <h1 style="font-size:clamp(26px,4vw,34px);margin:12px 0;text-transform:uppercase">Carte de <span class="hl">membre.</span></h1>1714  <div class="membercard">1715   <div class="mc-brand">Groupe KA · carte de membre</div>1716   <div class="mc-row">1717    <div class="mc-avatar">${me.picture?`<img src="${esc(me.picture)}" alt="">`:esc(init)}</div>1718    <div><div class="mc-name">${esc(me.name||"Membre")}</div>1719     <div class="mc-kaid">${esc(me.ka_id||"")}</div>1720     ${me.role_label||me.role?`<span class="mc-role">${esc(me.role_label||me.role)}</span>`:""}</div>1721   </div>1722   <div class="mc-meta">1723    ${me.email?`<span><b>Courriel :</b> ${esc(me.email)}</span>`:""}1724    ${me.city?`<span><b>Ville :</b> ${esc(me.city)}</span>`:""}1725    ${me.job_title||me.company?`<span><b>Occupation :</b> ${esc([me.job_title,me.company].filter(Boolean).join(" · "))}</span>`:""}1726    ${me.bio?`<span>${esc(me.bio)}</span>`:""}1727   </div>1728  </div>1729  <div class="compte-actions">1730   <a class="btn btn-ghost" href="https://www.groupe-ka.com/compte" target="_blank" rel="noopener">Modifier sur groupe-ka.com ↗</a>1731   ${me.public_url?`<a class="btn btn-ghost" href="${esc(me.public_url)}" target="_blank" rel="noopener">Profil public ↗</a>`:""}1732   <button class="btn btn-primary" id="logout">Se déconnecter</button>1733  </div>1734  <p style="font-size:12px;color:var(--ink-3);margin-top:18px">Le profil est dicté par le hub1735   groupe-ka.com (source de vérité) ; Créa-Ka n'entrepose aucune donnée de compte.</p>1736 </div>`+footer()+tabbar("");1737 bindNav();loadAccount();1738 $("#logout").addEventListener("click",async()=>{1739  await fetch("/api/auth/logout",{method:"POST"}).catch(()=>{});1740  state.me=null;history.pushState(null,"","/");route();});1741}17421743/* ---------- page favoris — « Mon univers Ka » (hub groupe-ka.com) ---------- */1744function favCardHtml(it){1745 const href=(it.url||"").replace(/^https?:\/\/(www\.)?crea-ka\.com/i,"")1746  ||("/createur/"+encodeURIComponent(it.item_id));1747 FAV.src[it.item_id]={item_id:it.item_id,title:it.title||"",subtitle:it.subtitle||"",1748  image_url:it.image_url||"",url:it.url||""};1749 return `<div class="card" data-href="${esc(href)}" role="link" tabindex="0" style="cursor:pointer">1750  <div class="card-photo"><span class="init">${esc(initials(it.title||"?"))}</span>1751   ${it.image_url?`<img loading="lazy" alt="${esc(it.title)}" src="${esc(it.image_url)}" onerror="this.remove()">`:""}1752   <button type="button" class="favbtn on" data-fav="${esc(it.item_id)}" aria-pressed="true"1753    aria-label="Retirer de mes favoris" title="Retirer de mes favoris"1754    onclick="event.stopPropagation();favToggle(this.dataset.fav,this)">${heartSvg}</button></div>1755  <div class="card-body">1756   <div class="card-name">${esc(it.title||it.item_id)}</div>1757   ${it.subtitle?`<div class="card-sub">${esc(it.subtitle)}</div>`:""}1758  </div></div>`;1759}1760async function renderFavoris(){1761 document.title="Mes favoris — Créa-Ka";1762 app.innerHTML=header()+`<div class="container"><div class="spin"></div></div>`+footer()+tabbar("fav");bindNav();1763 if(state.me===undefined){1764  try{const r=await fetch("/api/me");state.me=r.ok?await r.json():null}1765  catch(e){state.me=null}}1766 const shell=inner=>{app.innerHTML=header()+`1767  <div class="container favoris-page">1768   <span class="kicker">Mon univers Ka — favoris partagés</span>1769   <h1>Mes <span class="hl">favoris.</span></h1>1770   <p class="lead">Vos créateurs favoris, gardés dans votre compte KA ID sur groupe-ka.com :1771    les mêmes favoris vous suivent sur toutes les plateformes du Groupe KA.</p>1772   ${inner}1773  </div>`+footer()+tabbar("fav");bindNav();};1774 if(!state.me){1775  shell(`<div class="fav-empty" style="text-align:left;padding:34px 0">1776   <div class="compte-actions" style="margin-top:0">1777    <a class="btn btn-primary" href="/api/auth/ka/login">Se connecter avec KA ID</a>1778    <a class="btn btn-ghost" href="https://www.groupe-ka.com/connexion" target="_blank" rel="noopener">Créer un compte KA ID ↗</a>1779   </div>1780   <p style="font-size:12.5px;color:var(--ink-3);margin-top:16px">Un seul compte pour tout le1781    groupe — vos favoris sont entreposés au hub groupe-ka.com, jamais ici.</p></div>`);1782  loadAccount();return;1783 }1784 let data=null;1785 try{const r=await fetch("/api/favorites");if(r.ok)data=await r.json();}catch(e){}1786 if(!data){1787  shell(`<div class="fav-empty"><h2>Hub injoignable</h2>1788   <p>Impossible de lire vos favoris au hub Groupe KA pour le moment — réessayez dans un instant.</p></div>`);1789  loadAccount();return;1790 }1791 FAV.ids=new Set(data.ids||[]);FAV.loaded=true;1792 const items=(data.items||[]).filter(i=>i.item_id);1793 shell(items.length?`1794  <div class="count-line" style="padding-top:26px"><b>${fmtFull.format(items.length)}</b> créateur${items.length>1?"s":""} en favori</div>1795  <div class="grid" id="fav-grid">${items.map(favCardHtml).join("")}</div>`1796 :`<div class="fav-empty"><h2>Aucun favori pour l'instant</h2>1797   <p>Touchez le ♥ sur une carte ou une fiche de créateur pour l'ajouter à votre univers Ka.</p>1798   <p style="margin-top:16px"><a class="btn btn-primary" href="/#annuaire" data-nav>Explorer l'annuaire</a></p></div>`);1799 loadAccount();window.scrollTo(0,0);1800}18011802/* ---------- page contact (écosystème Groupe KA — ka/ecosystem.json) ---------- */1803function renderContact(){1804 document.title="Contact — Créa-Ka, un service Groupe KA";1805 const sites=KA_ECO.sites.map(s=>{1806  const cur=s.id==="crea-ka";1807  return `<a class="site-item" href="https://${esc(s.domain)}"${cur?"":' target="_blank" rel="noopener noreferrer"'}1808   title="${esc(s.tagline)}"><i style="background:${esc(s.accent)}"></i><b>${esc(s.wordmark)}${cur?" (ici)":""}</b><span>${esc(s.domain)}</span></a>`;1809 }).join("");1810 app.innerHTML=header()+`1811 <div class="container contact-page">1812  <span class="kicker">Contact — Groupe KA</span>1813  <h1>Nous <span class="hl">joindre.</span></h1>1814  <p class="lead">${esc(KA_SITE.wordmark)} — ${esc(KA_SITE.tagline)} — est un service du1815   <b>Groupe KA</b>. ${esc(KA_ECO.org.tagline)} Les contacts ci-dessous valent pour1816   l'ensemble des plateformes du groupe.</p>1817  <div class="contact-notice">${esc(KA_ECO.org.disclaimer)}</div>1818  <h2 class="sect-title" style="margin-top:26px">Courriels</h2>1819  <div class="contact-grid">1820   ${KA_ECO.contacts.map(c=>`<a class="contact-card" href="mailto:${esc(c.email)}">1821    <b>${esc(c.email)}</b><span>${esc(c.role)}</span></a>`).join("")}1822  </div>1823  <div class="compte-actions" style="margin-top:22px">1824   <a class="btn btn-primary" href="${esc(KA_ECO.hub.url)}" target="_blank" rel="noopener noreferrer">Visiter le hub groupe-ka.com ↗</a>1825   <a class="btn btn-ghost" href="/retrait" data-nav>Demander un retrait (Loi 25)</a>1826  </div>1827  <h2 class="sect-title">Les plateformes du groupe</h2>1828  <div class="site-list">${sites}</div>1829  <p style="font-size:12px;color:var(--ink-3);margin-top:20px">${esc(KA_ECO.hub.signupNote)}</p>1830 </div>`+footer()+tabbar("");1831 bindNav();window.scrollTo(0,0);1832}18331834/* ---------- retrait / opt-out ---------- */1835function renderOptout(){1836 document.title="Retrait (opt-out) — Créa-Ka";1837 app.innerHTML=header()+`1838 <div class="container optout-page">1839  <span class="kicker">Vie privée d'abord</span>1840  <h1>Demander un <span class="hl">retrait</span></h1>1841  <p>Vous êtes créateur ou créatrice et souhaitez ne plus figurer dans l'annuaire ?1842   La fiche est <b>masquée immédiatement</b> et exclue de toute ré-agrégation future.1843   Une correction d'erreur (mauvais compte rattaché) passe par le même formulaire.</p>1844  <form id="of">1845   <div class="field"><label>Nom public / nom de scène</label><input id="o-name" maxlength="200" placeholder="ex. Prénom Nom"></div>1846   <div class="field"><label>Un de vos comptes (plateforme:handle)</label><input id="o-acc" maxlength="200" placeholder="ex. instagram:monhandle"></div>1847   <div class="field"><label>Raison (facultatif)</label><textarea id="o-reason" rows="3" maxlength="500"></textarea></div>1848   <div class="field"><label>Courriel de contact (facultatif, pour confirmation)</label><input id="o-contact" maxlength="200" type="email"></div>1849   <div class="field"><button class="btn btn-primary" type="submit">Masquer ma fiche</button></div>1850  </form><div id="o-result"></div>1851 </div>`+footer()+tabbar("optout");1852 bindNav();1853 $("#of").addEventListener("submit",async e=>{e.preventDefault();1854  const body={name:$("#o-name").value.trim(),account:$("#o-acc").value.trim(),1855   reason:$("#o-reason").value.trim(),contact:$("#o-contact").value.trim()};1856  if(!body.name&&!body.account){$("#o-result").innerHTML=`<div class="optout-ok warn">Indiquez au moins un nom ou un compte.</div>`;return}1857  try{const r=await fetch("/api/optout",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)});1858   const j=await r.json();1859   $("#o-result").innerHTML=`<div class="optout-ok">✓ ${esc(j.message||"Demande enregistrée.")} ${j.masked?`(${j.masked} fiche(s) masquée(s))`:""}</div>`;1860   e.target.reset();}1861  catch(err){$("#o-result").innerHTML=`<div class="optout-ok warn">Erreur — réessayez ou écrivez à contact@spboucher.ai.</div>`}});1862}18631864/* ---------- routeur ---------- */1865function bindNav(root){1866 (root||document).querySelectorAll("[data-nav]").forEach(a=>{1867  if(a._nav)return;a._nav=1;1868  a.addEventListener("click",e=>{const href=a.getAttribute("href");1869   e.preventDefault();history.pushState(null,"",href);route();1870   if(href.endsWith("#annuaire")){const el=$("#annuaire");if(el){el.scrollIntoView();const q=$("#q");if(q)q.focus()}}})});1871 (root||document).querySelectorAll("[data-href]").forEach(card=>{1872  if(card._nav)return;card._nav=1;1873  const go=()=>{history.pushState(null,"",card.dataset.href);route()};1874  card.addEventListener("click",e=>{if(e.target.closest("a"))return;go()});1875  card.addEventListener("keydown",e=>{if(e.key==="Enter")go()});1876 });}1877function route(){1878 const p=location.pathname;1879 const m=p.match(/^\/createur\/([^/]+)$/);1880 const pl=p.match(/^\/plateforme\/([^/]+)$/); /* pages SEO /plateforme/{p} */1881 const qq=new URLSearchParams(location.search).get("q");if(qq&&!state.q)state.q=qq;1882 let r;1883 if(m)r=renderCreator(decodeURIComponent(m[1]));1884 else if(pl){state.plateforme=decodeURIComponent(pl[1]);r=renderHome();}1885 else if(p==="/retrait")r=renderOptout();1886 else if(p==="/favoris")r=renderFavoris();1887 else if(p==="/stats")r=renderStats();1888 else if(p==="/compte")r=renderCompte();1889 else if(p==="/contact")r=renderContact();1890 else r=renderHome();1891 Promise.resolve(r).then(()=>loadAccount()).catch(()=>{});1892}1893window.addEventListener("popstate",route);1894route();1895</script>1896<script>window.KA_AGENT={site:"crea-ka"}</script>1897<script src="/ka-agent.js" defer></script>1898<!-- KA ID v2 : signaux navigateur de la personnalisation (clics, dwell) -->1899<script>window.KAID_CONF={card:".card[data-href^='/createur/']",detail:"^/createur/(.+)$",entity:"creator",1900  uid:function(el){var h=el.getAttribute("data-href")||"";var m=h.match(/^\/createur\/(.+)$/);return m?decodeURIComponent(m[1]):null;}}</script>1901<script src="/ka-id.js" defer></script>1902</body>1903</html>1904