SPB Git forge

spb/sorti-ka

Public

Toutes les sorties et tous les événements du Québec, un seul endroit — 7 connecteurs, fiches SSR, design Groupe KA.

58commits 1branches 0releases
13.7 MBsize
maindefault branch
17 days agolast push
HTML 82.9% Python 15.2% TypeScript 0.9% JavaScript 0.7%
229.3 KB · 3,725 lines html
Raw Blame History
1<!--2  Author: Simon-Pierre Boucher <contact@spboucher.ai>3  File:   index.html4  Desc:   Frontend Sorti-Ka v2 « guide culturel » — SPA sans build, routeur5          History API, consomme /api. Accueil éditorial par sections (À la une,6          Ce soir, Fin de semaine, Gratuit, catégories, À découvrir), recherche7          centrale + filtres en bottom sheet, timeline de jours, vue Liste|Carte8          (Leaflet chargé à la demande), fiches image-first, CTA sticky mobile.9          Design system Groupe KA via ka-ui (src/ka/tokens.css, chargé en10          premier) : papier #f5f3ee · encre #141814 · ACCENT FRAMBOISE #d6336c11          (signal culturel : dates, états actifs, CTA) · Space Grotesk / Inter /12          JetBrains Mono · grain de film. Footer commun KaFooter et page13          /contact : contenu issu de src/ka/ecosystem.json (source de vérité —14          resynchroniser le HTML si le JSON change). SSR : sortika/seo.py15          injecte <head> et contenu dans les placeholders SSR_HEAD/SSR_BODY.16-->17<!DOCTYPE html>18<html lang="fr-CA">19<head>20<meta charset="utf-8">21<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">22<title>Sorti-Ka — Un service Groupe KA</title>23<meta name="description" content="Agrégateur indépendant de sorties et d'événements — concerts, festivals, spectacles, expositions, famille — dans les 17 régions du Québec, avec dates, lieux et gratuité.">24<link rel="icon" type="image/svg+xml" href="/favicon.svg">25<link rel="apple-touch-icon" href="/apple-touch-icon.png">26<meta property="og:image" content="https://www.sorti-ka.com/og.png">27<meta property="og:image:width" content="1200">28<meta property="og:image:height" content="630">29<meta name="twitter:card" content="summary_large_image">30<meta name="twitter:image" content="https://www.sorti-ka.com/og.png">31<!--SSR_HEAD-->32<link rel="preconnect" href="https://fonts.googleapis.com">33<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>34<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">35<link rel="stylesheet" href="/src/ka/tokens.css">36<style>37/* ka-ui/tokens.css (chargé ci-dessus) fournit palette, socle et composants38   communs ; ici : SEULEMENT l'accent framboise de Sorti·Ka + le CSS métier. */39:root{40  --accent:#d6336c; --accent-soft:#fbe0eb; --accent-deep:#a12551; --on-accent:#ffffff;41  /* alias rétro-compatibles — tout le CSS local en --lime suit l'accent */42  --lime:var(--accent); --lime-soft:var(--accent-soft);43}44h1,h2,h3,h4{margin:0}45a{text-decoration:none}46button{font-family:inherit}47.container{max-width:1240px;margin:0 auto;padding:0 24px}48@media(max-width:640px){.container{padding:0 16px}}49/* ---------- cartes & boutons signature ---------- */50.gk-card{background:var(--surface);border:1.5px solid var(--ink);51  border-radius:var(--r-card);box-shadow:var(--shadow-off-soft)}52.gk-hover{transition:transform .18s ease,box-shadow .18s ease}53.gk-hover:hover{transform:translate(-3px,-3px);box-shadow:var(--shadow-off)}54.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:44px;55  padding:10px 20px;border:1.5px solid var(--ink);border-radius:var(--r-ctl);56  font-family:var(--font-display);font-size:14px;font-weight:700;cursor:pointer;57  transition:background .15s ease,transform .1s ease,box-shadow .1s ease}58.btn:active{transform:translate(2px,2px);box-shadow:none!important}59.btn-primary{background:var(--ink);color:var(--accent);box-shadow:4px 4px 0 rgba(20,24,20,.25)}60.btn-primary:hover{background:var(--accent-deep)}61.btn-ghost{background:var(--surface);color:var(--ink);box-shadow:4px 4px 0 rgba(20,24,20,.12)}62.btn-ghost:hover{background:var(--accent-soft)}6364/* ---------- header / ticker (compact 2026-08-25) ---------- */65.ticker{background:var(--ink);color:rgba(245,243,238,.85);overflow:hidden;66  font-family:var(--font-mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;67  padding:6px 0;white-space:nowrap}68.ticker-track{display:inline-flex;animation:ticker 48s linear infinite;will-change:transform}69.ticker span{padding:0 26px;position:relative}70.ticker span::after{content:"◆";position:absolute;right:-6px;color:var(--lime);font-size:8px;top:3px}71@keyframes ticker{from{transform:translateX(0)}to{transform:translateX(-50%)}}72@media(prefers-reduced-motion:reduce){.ticker-track{animation:none}*{transition-duration:.01ms!important}}73/* header opaque, échelle --z-* (standard header/menu KA 2026-08-22) —74   v2 : une seule rangée mince ; sur mobile le contenu événementiel doit75   apparaître tout de suite → ticker masqué, badge/tag repliés dans le menu */76.header{position:sticky;top:0;z-index:var(--z-header,500);background:var(--paper);77  border-bottom:1.5px solid var(--ink)}78.header-inner{display:flex;align-items:center;gap:14px;min-height:58px;padding:4px 0}79.header-inner .gk-badge{flex:none}80@media(max-width:980px){.header-inner .gk-badge{display:none}}81@media(max-width:760px){82  .ticker{display:none}83  .header-inner{min-height:52px;padding:4px 0;gap:10px}84}85/* le reste des règles mobiles du header est déclaré APRÈS toutes les règles de86   base (nav, mnav, btn-ka, user-chip) — voir bloc « header mobile » plus bas :87   à spécificité égale, c'est la dernière déclaration qui gagne. */88.brand{font-family:var(--font-display);font-weight:700;font-size:23px;letter-spacing:-.03em;89  display:flex;align-items:baseline;line-height:1;cursor:pointer;position:relative;flex:none}90/* extension tactile : le logo (98×26) atteint >=44px de cible sans changer le rendu */91.brand::after{content:"";position:absolute;inset:-8px}92.brand .ka{background:var(--accent);color:var(--on-accent);border-radius:6px;padding:0 6px 2px;margin-left:2px;transform:rotate(-1deg)}93.brand-tag{font-family:var(--font-mono);font-size:10px;text-transform:uppercase;94  letter-spacing:.08em;color:var(--ink-3)}95@media(max-width:1100px){.brand-tag{display:none}}96.nav{margin-left:auto;display:flex;gap:2px}97.nav a{display:inline-flex;align-items:center;min-height:44px;padding:8px 13px;98  border-radius:999px;font-family:var(--font-display);font-weight:700;font-size:13.5px;99  color:var(--ink-2);transition:all .15s ease}100.nav a:hover{background:var(--accent-soft);color:var(--ink)}101102/* ===== KA Nav v2 — menu mobile plein écran (standard 2026-08-25) =====103   Panneau position:fixed inset:0 (z-modal) → visible peu importe le scroll. */104.ka-mnav-btn{display:none;width:44px;height:44px;flex:none;105  flex-direction:column;align-items:center;justify-content:center;gap:5px;106  background:var(--paper);border:1.5px solid var(--ink);border-radius:999px;107  cursor:pointer;padding:0}108.ka-mnav-btn span{display:block;width:18px;height:2px;background:var(--ink);109  border-radius:2px;transition:transform .18s ease,opacity .14s ease}110.ka-mnav-btn[aria-expanded="true"] span:nth-child(1){transform:translateY(7px) rotate(45deg)}111.ka-mnav-btn[aria-expanded="true"] span:nth-child(2){opacity:0}112.ka-mnav-btn[aria-expanded="true"] span:nth-child(3){transform:translateY(-7px) rotate(-45deg)}113.ka-mnav{position:fixed;inset:0;z-index:var(--z-modal,900);background:var(--paper);114  display:none;flex-direction:column;overflow-y:auto;overscroll-behavior:contain}115.ka-mnav.open{display:flex;animation:ka-mnav-in .16s ease}116@keyframes ka-mnav-in{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}117.ka-mnav-top{display:flex;align-items:center;justify-content:space-between;flex:none;118  min-height:64px;padding:0 18px;border-bottom:2px solid var(--ink);background:var(--paper)}119.ka-mnav-close{width:44px;height:44px;flex:none;display:flex;align-items:center;120  justify-content:center;border:1.5px solid var(--ink);border-radius:999px;121  background:var(--ink);color:var(--lime);font-size:17px;font-weight:700;122  font-family:var(--font-display);cursor:pointer;padding:0;line-height:1}123.ka-mnav-links{display:flex;flex-direction:column;justify-content:center;flex:1;124  padding:28px 24px;counter-reset:mnav}125.ka-mnav-links a{display:flex;align-items:baseline;gap:14px;padding:17px 0;126  border-bottom:1.5px dashed rgba(20,24,20,.18);color:var(--ink)}127.ka-mnav-links a:last-child{border-bottom:0}128.ka-mnav-links a::before{counter-increment:mnav;content:"0" counter(mnav);129  font-family:var(--font-mono);font-size:12px;font-weight:700;color:var(--accent-deep)}130.ka-mnav-links a b{font-family:var(--font-display);font-weight:700;131  font-size:27px;line-height:1;letter-spacing:-.03em;text-transform:uppercase}132.ka-mnav-links a.active b{background:var(--ink);color:var(--lime);133  border-radius:6px;padding:4px 10px 6px}134.ka-mnav-foot{flex:none;border-top:2px solid var(--ink);padding:16px 24px;135  padding-bottom:max(20px,env(safe-area-inset-bottom))}136.ka-mnav-sec{display:flex;flex-wrap:wrap;gap:6px 20px;margin-top:12px;137  font-family:var(--font-mono);font-size:10.5px;text-transform:uppercase;138  letter-spacing:.06em}139.ka-mnav-sec a{color:var(--ink-3)}140html.ka-menu-locked,html.ka-menu-locked body{overflow:hidden}141.btn-ka{display:inline-flex;align-items:center;gap:8px;padding:8px 15px;min-height:38px;142  border:1.5px solid var(--ink);border-radius:var(--r-ctl);background:var(--ink);143  color:var(--lime);font-family:var(--font-display);font-weight:700;font-size:13px;144  cursor:pointer;box-shadow:3px 3px 0 rgba(20,24,20,.25);transition:background .15s ease}145.btn-ka:hover{background:var(--accent-deep)}146#auth{display:inline-flex;align-items:center;gap:10px;flex:none}147.ka-signup{font-family:var(--font-mono);font-size:10px;font-weight:700;text-transform:uppercase;148  letter-spacing:.06em;color:var(--ink-2);min-height:44px;display:inline-flex;align-items:center}149.ka-signup:hover{color:var(--accent-deep);text-decoration:underline}150.btn-ka .kdot{background:var(--accent);color:var(--on-accent);border-radius:4px;151  padding:0 5px;font-weight:800;transform:rotate(-1deg)}152.user-chip{display:inline-flex;align-items:center;gap:9px;border:1.5px solid var(--ink);153  border-radius:var(--r-pill,999px);background:var(--surface);padding:4px 12px 4px 5px;154  box-shadow:3px 3px 0 rgba(20,24,20,.12)}155.user-chip img{width:28px;height:28px;border-radius:50%;border:1.5px solid var(--ink);object-fit:cover}156.user-chip .uname{font-family:var(--font-display);font-weight:700;font-size:12.5px;max-width:140px;157  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}158.user-chip .kaid{font-family:var(--font-mono);font-size:9px;color:var(--ink-3);display:block;line-height:1}159.user-chip .out{border:none;background:none;cursor:pointer;font-family:var(--font-mono);160  font-size:9.5px;font-weight:700;text-transform:uppercase;color:var(--danger);padding:2px}161.user-chip .out:hover{text-decoration:underline}162@media(max-width:640px){.user-chip .uname{max-width:80px}}163/* ---- header mobile (KA Nav v2) — déclaré après TOUTES les règles de base ---- */164@media(max-width:760px){165  .nav{display:none}166  #auth{margin-left:auto}167  .ka-mnav-btn{display:flex}168  .ka-signup{display:none}169  .btn-ka .tx{display:none}170  .btn-ka{padding:8px 11px;min-height:36px}171  .user-chip .kaid{display:none}172  .user-chip .out{display:none}173}174175/* ---------- hero éditorial (mosaïque d'images réelles en fond) ---------- */176.hero{position:relative;padding:clamp(30px,6vw,72px) 0 8px;overflow:clip}177@media(max-width:640px){178  .hero h1{font-size:clamp(30px,8.6vw,38px)}179  .hero-cats{font-size:10px;line-height:1.9;margin-top:12px}180  .hero-stats{font-size:13px;margin-top:10px;line-height:1.7}181}182.hero-mosaic{position:absolute;inset:-24px -30px auto;height:130%;z-index:0;183  display:grid;grid-template-columns:repeat(6,1fr);grid-auto-rows:1fr;gap:8px;184  transform:rotate(-2deg);opacity:.3;pointer-events:none;filter:saturate(.85)}185@media(max-width:640px){.hero-mosaic{grid-template-columns:repeat(3,1fr)}}186.hero-mosaic .tile{border-radius:12px;overflow:hidden;background:var(--accent-soft)}187.hero-mosaic img{width:100%;height:100%;object-fit:cover}188.hero::after{content:"";position:absolute;inset:0;z-index:1;189  background:linear-gradient(180deg,rgba(245,243,238,.55) 0%,rgba(245,243,238,.86) 46%,var(--paper) 96%)}190.hero .container{position:relative;z-index:2}191.hero h1{font-size:clamp(34px,5.4vw,60px);font-weight:700;line-height:1.04;max-width:940px;margin-top:14px}192.hero-cats{font-family:var(--font-mono);font-size:11.5px;font-weight:500;text-transform:uppercase;193  letter-spacing:.13em;color:var(--ink-2);margin:16px 0 0;line-height:2}194.hero-cats i{font-style:normal;color:var(--accent);font-weight:700;padding:0 3px}195.hero-stats{font-size:14.5px;color:var(--ink-2);margin:14px 0 0;max-width:720px;line-height:1.8}196.hero-stats b{font-family:var(--font-display);font-size:1.35em;font-weight:700;197  letter-spacing:-.02em;color:var(--ink);padding-right:1px}198.hero-stats i{font-style:normal;color:var(--accent);font-weight:700;padding:0 4px}199200/* ---------- recherche au centre + accès rapides + timeline de jours ---------- */201.searchzone{margin-top:14px}202.searchbar{display:flex;align-items:center;gap:11px;background:var(--surface);203  border:1.5px solid var(--ink);border-radius:16px;min-height:56px;padding:0 8px 0 17px;204  box-shadow:var(--shadow-off-soft);transition:box-shadow .18s ease,transform .18s ease}205.searchbar:focus-within{box-shadow:4px 4px 0 var(--accent);transform:translate(-1px,-1px)}206.searchbar .sico{flex:none;width:19px;height:19px;color:var(--ink-3)}207.searchbar input{flex:1;min-width:0;border:0;background:none;outline:none;208  font-size:16px;font-family:inherit;color:var(--ink);padding:14px 0}209.searchbar input::placeholder{color:var(--ink-3)}210.searchbar input::-webkit-search-cancel-button{display:none}211.sclear{flex:none;width:38px;height:38px;border-radius:50%;border:0;background:var(--surface-2);212  color:var(--ink-2);font-size:13px;cursor:pointer;line-height:1}213.sclear:hover{background:var(--accent-soft);color:var(--ink)}214.quick{display:flex;gap:8px;margin-top:12px;overflow-x:auto;scrollbar-width:none;padding:2px}215.quick::-webkit-scrollbar{display:none}216@media(max-width:640px){.quick{margin-left:-16px;margin-right:-16px;padding:2px 16px}}217.qchip{flex:none;display:inline-flex;align-items:center;gap:7px;min-height:42px;padding:9px 16px;218  border:1.5px solid rgba(20,24,20,.35);border-radius:999px;background:var(--surface);219  font-family:var(--font-display);font-weight:700;font-size:13.5px;color:var(--ink);220  cursor:pointer;transition:all .14s ease;white-space:nowrap}221.qchip svg{width:15px;height:15px;flex:none}222.qchip:hover{border-color:var(--ink);background:var(--accent-soft)}223.qchip.on{background:var(--accent);border-color:var(--accent-deep);color:#fff;224  box-shadow:2px 2px 0 rgba(20,24,20,.22)}225.qchip .fb{min-width:19px;height:19px;border-radius:10px;background:var(--accent);color:#fff;226  font-family:var(--font-mono);font-size:10.5px;font-weight:700;227  display:inline-flex;align-items:center;justify-content:center;padding:0 5px}228.qchip.on .fb{background:#fff;color:var(--accent-deep)}229.days{display:flex;gap:8px;overflow-x:auto;margin-top:14px;padding:3px 2px 10px;230  scroll-snap-type:x proximity;scrollbar-width:none}231.days::-webkit-scrollbar{display:none}232@media(max-width:640px){.days{margin-left:-16px;margin-right:-16px;padding:3px 16px 10px}}233.day{flex:none;scroll-snap-align:start;min-width:60px;padding:8px 6px 7px;234  border:1.5px solid var(--line);border-radius:13px;background:var(--surface);235  text-align:center;cursor:pointer;transition:all .14s ease}236.day small{display:block;font-family:var(--font-mono);font-size:9px;font-weight:700;237  text-transform:uppercase;letter-spacing:.08em;color:var(--ink-3)}238.day b{display:block;font-family:var(--font-display);font-size:19px;line-height:1.2;color:var(--ink)}239.day i{display:block;font-style:normal;font-family:var(--font-mono);font-size:8.5px;240  text-transform:uppercase;letter-spacing:.05em;color:var(--ink-3)}241.day:hover{border-color:var(--ink)}242.day.on{background:var(--accent);border-color:var(--accent-deep);243  box-shadow:2px 2px 0 rgba(20,24,20,.22)}244.day.on small,.day.on i{color:rgba(255,255,255,.85)}245.day.on b{color:#fff}246.day.all b{font-size:13px;padding:6px 4px 5px;letter-spacing:0}247248/* chips génériques (filtres du sheet + périodes de /stats) */249.chips{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}250.chip{border:1.5px solid rgba(20,24,20,.4);background:var(--surface);border-radius:999px;251  padding:8px 14px;font-family:var(--font-display);font-size:12.5px;font-weight:700;252  text-transform:none;letter-spacing:0;min-height:38px;display:inline-flex;align-items:center;gap:6px;253  cursor:pointer;color:var(--ink);line-height:1;transition:all .12s ease}254.chip:hover{background:var(--accent-soft);border-color:var(--ink)}255.chip.on{background:var(--ink);border-color:var(--ink);color:var(--accent);box-shadow:2px 2px 0 rgba(20,24,20,.25)}256.chip.free.on{background:var(--green);border-color:var(--green);color:#fff}257258/* ---------- sections éditoriales de l'accueil ---------- */259.sec{margin:36px 0 6px}260.sec:first-child{margin-top:26px}261.sec-head{display:flex;align-items:flex-end;justify-content:space-between;gap:14px;margin:0 0 15px}262.sec-head .sk{display:block;font-family:var(--font-mono);font-size:10px;font-weight:700;263  text-transform:uppercase;letter-spacing:.12em;color:var(--accent-deep);margin-bottom:5px}264.sec-head h2{font-size:clamp(21px,2.7vw,31px);letter-spacing:-.025em;line-height:1.1;margin:0}265.sec-all{flex:none;font-family:var(--font-mono);font-size:11px;font-weight:700;text-transform:uppercase;266  letter-spacing:.06em;color:var(--accent-deep);cursor:pointer;background:none;border:0;padding:0;267  min-height:44px;display:inline-flex;align-items:center;gap:5px}268.sec-all:hover{text-decoration:underline;text-underline-offset:4px}269270/* ---------- fiches événement v2 — l'image domine ---------- */271.ev{display:flex;flex-direction:column;gap:10px;cursor:pointer;min-width:0}272.ev-img{position:relative;aspect-ratio:4/3;border-radius:15px;overflow:hidden;273  background:linear-gradient(135deg,var(--accent-soft) 0%,var(--surface-2) 70%)}274.ev-img::after{content:"";position:absolute;inset:0;border-radius:inherit;275  border:1px solid rgba(20,24,20,.16);pointer-events:none}276.ev-img img{width:100%;height:100%;object-fit:cover;transition:transform .4s ease}277.ev-img.noimg::before{content:"Sorti·Ka";position:absolute;inset:auto 10px 8px auto;278  font-family:var(--font-display);font-weight:700;font-size:13px;color:rgba(161,37,81,.4);279  transform:rotate(-2deg)}280@media(hover:hover){281  .ev:hover .ev-img img{transform:scale(1.05)}282  .ev:hover h3{text-decoration:underline;text-decoration-color:var(--accent);283    text-decoration-thickness:2px;text-underline-offset:3px}284  .feat:hover img{transform:scale(1.03)}285}286.ev-body{display:flex;flex-direction:column;gap:4px;min-width:0}287.ev-date{font-family:var(--font-mono);font-size:11px;font-weight:700;text-transform:uppercase;288  letter-spacing:.05em;color:var(--accent-deep)}289/* KA ID v2 : badge « Recommandé pour vous » (parcimonieux, posé par le serveur) */290.ev-reco{display:inline-block;margin-left:6px;padding:1px 7px 2px;border-radius:5px;291  background:var(--ink);color:var(--accent);font-size:9px;letter-spacing:.07em}292.ev h3{font-family:var(--font-display);font-size:16.5px;font-weight:700;line-height:1.22;293  letter-spacing:-.015em;margin:0;display:-webkit-box;-webkit-line-clamp:2;294  -webkit-box-orient:vertical;overflow:hidden}295.ev-where{font-size:12.5px;color:var(--ink-2);margin:0;296  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}297.ev-meta{display:flex;align-items:center;gap:10px;margin:2px 0 0;font-family:var(--font-mono);298  font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;299  color:var(--ink-3);white-space:nowrap;overflow:hidden}300.ev-meta .src{color:rgba(139,146,140,.75);font-weight:500;overflow:hidden;text-overflow:ellipsis}301.ev-tag{position:absolute;left:10px;bottom:10px;z-index:2;font-family:var(--font-mono);302  font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.03em;303  background:rgba(20,24,20,.82);color:#fff;border-radius:9px;padding:5px 10px;304  backdrop-filter:blur(3px)}305.ev-tag.free{background:var(--green)}306.ev-tag.stat-cancelled{background:#b3261e}307.ev-tag.stat-soldout{background:#5f6368}308.ev-tag.stat-postponed{background:#a05a00}309310/* grand événement vedette (À la une) */311.feat{position:relative;display:block;border-radius:20px;overflow:hidden;312  aspect-ratio:16/8.5;min-height:300px;cursor:pointer;background:var(--ink);313  box-shadow:var(--shadow-off-soft);margin-bottom:16px}314@media(max-width:640px){.feat{aspect-ratio:4/4.6;min-height:0;border-radius:17px}}315.feat img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;316  transition:transform .5s ease}317.feat::before{content:"";position:absolute;inset:0;z-index:1;318  background:linear-gradient(198deg,rgba(20,24,20,0) 34%,rgba(20,24,20,.55) 62%,rgba(20,24,20,.88) 96%)}319.feat-kick{position:absolute;top:14px;left:14px;z-index:2;font-family:var(--font-mono);320  font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.1em;321  background:var(--paper);color:var(--accent-deep);border-radius:999px;padding:6px 12px}322.feat .fav-btn{top:12px;right:12px}323.feat-body{position:absolute;inset:auto 0 0 0;z-index:2;padding:20px 22px 22px;color:#fff}324.feat-date{display:inline-flex;background:var(--accent);color:#fff;border-radius:9px;325  padding:6px 11px;font-family:var(--font-mono);font-size:11px;font-weight:700;326  text-transform:uppercase;letter-spacing:.05em;margin-bottom:11px}327.feat h3{color:#fff;font-size:clamp(22px,3.6vw,36px);line-height:1.07;letter-spacing:-.02em;328  margin:0 0 7px;max-width:760px;display:-webkit-box;-webkit-line-clamp:3;329  -webkit-box-orient:vertical;overflow:hidden}330.feat-where{font-size:13.5px;color:rgba(255,255,255,.85);margin:0;331  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}332.feat-where b{color:#fff;font-weight:600}333334/* carrousel horizontal (scroll-snap, débord plein écran sur mobile) */335.hscroll{display:grid;grid-auto-flow:column;grid-auto-columns:min(44vw,238px);gap:15px;336  overflow-x:auto;scroll-snap-type:x mandatory;padding:4px 2px 12px;scrollbar-width:none}337.hscroll::-webkit-scrollbar{display:none}338.hscroll .ev{scroll-snap-align:start}339@media(max-width:640px){340  .hscroll{grid-auto-columns:60vw;margin-left:-16px;margin-right:-16px;padding:4px 16px 12px}341}342343/* liste compacte (À découvrir) */344.compact{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:2px 26px}345.evc{display:flex;gap:13px;align-items:center;padding:11px 0;min-width:0;cursor:pointer;346  border-bottom:1px dashed var(--line)}347.evc-img{position:relative;flex:none;width:76px;height:76px;border-radius:13px;overflow:hidden;348  background:linear-gradient(135deg,var(--accent-soft),var(--surface-2))}349.evc-img::after{content:"";position:absolute;inset:0;border-radius:inherit;350  border:1px solid rgba(20,24,20,.14)}351.evc-img img{width:100%;height:100%;object-fit:cover}352.evc-body{min-width:0;flex:1}353.evc .ev-date{font-size:10px}354.evc h3{font-family:var(--font-display);font-size:14.5px;font-weight:700;line-height:1.25;355  letter-spacing:-.01em;margin:1px 0 2px;display:-webkit-box;-webkit-line-clamp:2;356  -webkit-box-orient:vertical;overflow:hidden}357.evc .ev-where{font-size:12px}358359/* ---------- résultats : compteur, pilules actives, Liste|Carte ---------- */360.rtool{display:flex;align-items:center;justify-content:space-between;gap:12px;361  flex-wrap:wrap;margin:20px 2px 8px}362.meta-line{display:flex;align-items:center;gap:10px;363  font-family:var(--font-mono);font-size:11.5px;text-transform:uppercase;364  letter-spacing:.08em;color:var(--ink-3)}365.meta-line b{color:var(--ink)}366.afilters{display:flex;flex-wrap:wrap;gap:7px;margin:2px 2px 6px}367.apill{display:inline-flex;align-items:center;gap:8px;min-height:34px;padding:5px 7px 5px 13px;368  border:1.5px solid var(--accent-deep);border-radius:999px;background:var(--accent-soft);369  font-family:var(--font-display);font-weight:700;font-size:12px;color:var(--accent-deep);370  cursor:pointer;transition:all .12s ease}371.apill i{font-style:normal;width:20px;height:20px;border-radius:50%;background:var(--accent-deep);372  color:#fff;font-size:10px;display:inline-flex;align-items:center;justify-content:center}373.apill:hover{background:var(--accent);color:#fff}374.vtoggle{display:inline-flex;border:1.5px solid var(--ink);border-radius:999px;375  background:var(--surface);padding:3px;box-shadow:2px 2px 0 rgba(20,24,20,.12)}376.vtoggle button{border:0;background:none;min-height:36px;padding:6px 16px;border-radius:999px;377  font-family:var(--font-display);font-weight:700;font-size:12.5px;cursor:pointer;378  display:inline-flex;gap:7px;align-items:center;color:var(--ink-2);transition:all .14s ease}379.vtoggle button svg{width:13px;height:13px}380.vtoggle button.on{background:var(--ink);color:var(--paper)}381.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(236px,1fr));gap:26px 20px;margin:12px 0 34px}382@media(max-width:640px){.grid{grid-template-columns:1fr 1fr;gap:22px 12px}383  .grid .ev h3{font-size:14.5px}}384385/* ---------- carte (Leaflet, chargé à la demande) ---------- */386.mapwrap{position:relative;isolation:isolate;border:1.5px solid var(--ink);border-radius:17px;387  overflow:hidden;height:min(68dvh,640px);margin:12px 0 44px;background:var(--surface-2)}388@media(max-width:760px){.mapwrap{height:calc(var(--vh100, 100dvh) - 190px);margin:10px 0 30px}}389#map{position:absolute;inset:0;z-index:0}390.map-note{position:absolute;left:12px;bottom:12px;z-index:1200;font-family:var(--font-mono);391  font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;392  background:rgba(255,255,255,.92);border:1px solid rgba(20,24,20,.3);color:var(--ink-2);393  border-radius:999px;padding:6px 12px;pointer-events:none;max-width:70%}394.map-load{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;395  z-index:1100;font-family:var(--font-mono);font-size:11px;text-transform:uppercase;396  letter-spacing:.08em;color:var(--ink-3);background:var(--surface-2)}397.leaflet-popup-content-wrapper{border-radius:14px;border:1.5px solid var(--ink);398  box-shadow:4px 4px 0 rgba(20,24,20,.18);font-family:var(--font-body)}399.leaflet-popup-content{margin:0;width:220px!important}400.mpop{display:block;color:var(--ink);text-decoration:none}401.mpop img{width:100%;height:104px;object-fit:cover;border-radius:12px 12px 0 0}402.mpop .mp-b{padding:10px 13px 12px}403.mpop .ev-date{font-size:10px}404.mpop h4{font-family:var(--font-display);font-size:13.5px;font-weight:700;line-height:1.25;405  margin:3px 0 3px;letter-spacing:-.01em}406.mpop p{font-size:11.5px;color:var(--ink-2);margin:0;white-space:nowrap;407  overflow:hidden;text-overflow:ellipsis}408409/* badges conservés (fiche événement + bannières de statut) */410.badge-free{font-family:var(--font-mono);font-size:10px;font-weight:700;text-transform:uppercase;411  color:#fff;background:var(--green);border-radius:7px;padding:4px 8px;flex:none}412.badge-price{font-family:var(--font-mono);font-size:10px;font-weight:700;text-transform:uppercase;413  color:var(--ink);background:var(--amber-soft);border:1px solid var(--amber);414  border-radius:7px;padding:3px 8px;flex:none}415.badge-status{font-family:var(--font-mono);font-size:10px;font-weight:700;text-transform:uppercase;416  color:#fff;background:#b3261e;border-radius:7px;padding:4px 8px;flex:none}417.badge-status--soldout{background:#5f6368}418.badge-status--postponed{background:#a05a00}419.status-banner{margin:0 0 14px;padding:11px 15px;border-radius:12px;font-weight:700;420  color:#fff;background:#b3261e}421.status-banner--soldout{background:#5f6368}422.status-banner--postponed{background:#a05a00}423.cat{font-family:var(--font-mono);font-size:9.5px;font-weight:700;letter-spacing:.06em;424  text-transform:uppercase;color:var(--ink-2);border:1px solid var(--line-strong);425  border-radius:999px;padding:3px 9px}426.more{display:block;margin:0 auto 54px}427.more[hidden]{display:none}428.empty{padding:60px 10px;text-align:center;color:var(--ink-3);grid-column:1/-1}429.empty b{font-family:var(--font-display);font-size:19px;color:var(--ink);display:block;margin-bottom:6px}430431/* ---------- fiche événement v2 ---------- */432.detail{padding-top:24px;padding-bottom:60px}433.back{display:inline-flex;align-items:center;gap:8px;min-height:44px;font-family:var(--font-mono);434  font-size:11.5px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;435  color:var(--accent-deep);cursor:pointer;margin-bottom:6px}436@media(pointer:coarse){.chip{min-height:44px}.user-chip .out{min-height:32px;padding:2px 8px}}437.back:hover{text-decoration:underline}438.detail-grid{display:grid;grid-template-columns:1.55fr 1fr;gap:30px;align-items:start}439@media(max-width:860px){.detail-grid{grid-template-columns:1fr;gap:20px}}440.detail-main{min-width:0}441.d-imgwrap{position:relative;aspect-ratio:16/9;border-radius:19px;overflow:hidden;442  background:linear-gradient(135deg,var(--accent-soft),var(--surface-2))}443.d-imgwrap::after{content:"";position:absolute;inset:0;border-radius:inherit;444  border:1px solid rgba(20,24,20,.18);pointer-events:none}445.d-imgwrap img{width:100%;height:100%;object-fit:cover}446@media(max-width:640px){.d-imgwrap{aspect-ratio:4/3;border-radius:15px;margin:0 0 2px}}447.d-body{padding:20px 2px 0}448.d-body h1{font-size:clamp(26px,3.6vw,42px);line-height:1.07;margin:10px 0 14px}449.d-body .desc{color:var(--ink-2);font-size:15px;white-space:pre-line;max-width:70ch}450.detail-cats{display:flex;flex-wrap:wrap;gap:8px;margin-top:18px}451.side{display:flex;flex-direction:column;gap:14px;position:sticky;top:76px}452@media(max-width:860px){.side{position:static}}453.side .box{background:var(--surface);border:1px solid rgba(20,24,20,.25);border-radius:16px;454  padding:18px 20px;box-shadow:var(--shadow-flat)}455.fact{display:flex;justify-content:space-between;gap:14px;padding:10px 0;456  border-bottom:1px dashed var(--line);font-size:13.5px}457.fact:first-child{padding-top:2px}458.fact:last-child{border-bottom:none;padding-bottom:2px}459.fact .k{font-family:var(--font-mono);font-size:10px;font-weight:700;460  text-transform:uppercase;letter-spacing:.08em;color:var(--ink-3);padding-top:3px;flex:none}461.fact .v{text-align:right;font-weight:600}462.side .btn{width:100%}463.btn-cta{background:var(--accent);color:#fff;border-color:var(--accent-deep);464  box-shadow:4px 4px 0 rgba(161,37,81,.3)}465.btn-cta:hover{background:var(--accent-deep);color:#fff}466.maplink{font-family:var(--font-mono);font-size:11px;color:var(--accent-deep);font-weight:700;text-transform:uppercase}467.maplink:hover{text-decoration:underline}468.similar h2{font-size:22px;margin:46px 0 4px}469/* barre d'action collée en bas — mobile seulement (billets toujours à un pouce) */470.d-ctabar{display:none}471@media(max-width:860px){472  .d-ctabar{position:fixed;left:0;right:0;bottom:0;z-index:var(--z-bottombar,600);473    display:flex;align-items:center;gap:14px;background:var(--surface);474    border-top:1.5px solid var(--ink);padding:10px 16px calc(10px + env(safe-area-inset-bottom))}475  .d-ctabar .p{flex:1;min-width:0}476  .d-ctabar .p b{font-family:var(--font-display);font-size:15.5px;letter-spacing:-.01em;477    display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}478  .d-ctabar .p span{display:block;font-family:var(--font-mono);font-size:9.5px;479    text-transform:uppercase;letter-spacing:.05em;color:var(--accent-deep);margin-top:1px}480  .d-ctabar .btn{flex:none;min-width:132px}481  .detail{padding-bottom:120px}482  /* le bouton ka-agent remonte au-dessus de la barre CTA sur la fiche mobile */483  body:has(#view-detail:not(.hidden)) .kaa-btn{484    bottom:calc(86px + env(safe-area-inset-bottom,0px))!important}485}486.d-imgwrap.noimg{aspect-ratio:16/5}487488/* ---------- sources / footer ---------- */489.sources{background:var(--surface-2);border-top:1px solid rgba(20,24,20,.35);padding:46px 0 40px;margin-top:26px}490.sources h2{font-size:26px;margin-top:12px}491.sources p.lede2{color:var(--ink-2);font-size:14.5px;max-width:720px;margin:10px 0 0}492.src-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px;margin-top:24px}493.src-card{background:var(--surface);border:1px solid rgba(20,24,20,.18);border-radius:14px;494  padding:15px 17px;box-shadow:var(--shadow-flat)}495.src-card h4{font-size:14.5px;margin-bottom:5px}496.src-card .lic{font-family:var(--font-mono);font-size:9.5px;font-weight:700;497  color:var(--green);text-transform:uppercase;letter-spacing:.06em}498.src-card .n{font-size:12.5px;color:var(--ink-3);margin-top:6px}499.src-card .n b{color:var(--accent-deep)}500.ka-footer{margin-top:0}501502/* ---------- page /contact ---------- */503.contact-page{padding-top:44px;padding-bottom:64px}504.contact-page h1{font-size:clamp(30px,4.2vw,48px);line-height:1.06;margin-top:16px;max-width:820px}505.contact-page .lede{color:var(--ink-2);font-size:16px;max-width:660px;margin:16px 0 0}506.contact-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(260px,100%),1fr));507  gap:18px;margin-top:28px}508.contact-card{padding:20px 22px;display:flex;flex-direction:column;gap:6px}509.contact-card a.mail{display:inline-flex;align-items:center;min-height:44px;510  font-family:var(--font-mono);font-weight:700;font-size:14px;color:var(--accent-deep);511  word-break:break-all}512.contact-card a.mail:hover{text-decoration:underline;text-underline-offset:4px}513.contact-note{margin-top:26px;padding:18px 20px;border-left:3px solid var(--accent);514  background:var(--surface);border-radius:0 var(--r-card) var(--r-card) 0;515  box-shadow:var(--shadow-flat);max-width:760px}516.contact-note b{color:var(--ink)}517.contact-sites{margin-top:36px}518.contact-sites ul{list-style:none;display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 0;padding:0}519.contact-sites a{display:inline-flex;align-items:center;min-height:44px;padding:8px 14px;520  border:1.5px solid var(--ink);border-radius:var(--r-ctl);background:var(--surface);521  font-family:var(--font-display);font-weight:700;font-size:13px;522  box-shadow:3px 3px 0 rgba(20,24,20,.12);transition:background .15s ease}523.contact-sites a:hover{background:var(--accent-soft)}524.contact-sites a small{font-family:var(--font-mono);font-weight:500;font-size:10px;525  color:var(--ink-3);margin-left:8px}526/* ---------- page /stats (module ka-stats, vanilla + SVG) ---------- */527.stats-page{padding-top:40px;padding-bottom:60px}528@media(max-width:640px){.stats-page{padding-top:28px;padding-bottom:44px}}529.st-head{display:flex;flex-wrap:wrap;gap:16px;align-items:flex-end;justify-content:space-between}530.st-head h1{font-size:clamp(28px,4vw,44px);line-height:1.06;margin-top:12px}531.st-fresh{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin:10px 0 0}532.st-pdf{display:inline-flex;gap:8px;flex-wrap:wrap}533.st-periods{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-top:20px}534.st-periods .input{width:150px;padding:8px 10px;font-size:13px;min-height:44px}535.st-custom{display:inline-flex;gap:6px;align-items:center;flex-wrap:wrap}536.st-kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(180px,100%),1fr));gap:14px;margin-top:22px}537.st-kpi{padding:14px 16px;min-width:0}538.st-kpi .v{font-family:var(--font-display);font-weight:700;font-size:clamp(22px,2.4vw,30px);letter-spacing:-.02em}539.st-kpi .v small{font-size:.6em;color:var(--ink-2);font-weight:600}540.st-kpi .d{margin-top:8px;font-family:var(--font-mono);font-size:11px;font-weight:700}541.st-kpi .d .ctx{color:var(--ink-3);font-weight:500}542.st-sec{display:grid;gap:18px;margin-top:18px}543.st-2col{display:grid;grid-template-columns:1fr 1fr;gap:18px;margin-top:18px}544@media(max-width:900px){.st-2col{grid-template-columns:1fr}}545/* les enfants de grille doivent pouvoir rétrécir : le contenu large (tableaux546   nowrap, heatmap) défile alors DANS .tbl-wrap au lieu d'élargir la page */547.st-sec>div,.st-2col>div,.st-records>*{min-width:0;max-width:100%}548.st-fig .tbl-wrap{max-width:100%}549.st-fig{margin:0;padding:16px;min-width:0}550.st-fig figcaption{display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px;align-items:center}551.st-fig figcaption b{font-family:var(--font-display);font-size:15px}552.st-fig svg{width:100%;height:auto;margin-top:10px;touch-action:pan-y}553.st-legend{display:flex;gap:10px;flex-wrap:wrap}554.st-legend button{display:inline-flex;align-items:center;gap:6px;border:0;background:none;cursor:pointer;555  font-family:var(--font-mono);font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;min-height:44px;color:var(--ink)}556.st-legend button.off{opacity:.4}557.st-legend .sw{width:18px;height:0;border-top:3px solid var(--accent)}558.st-legend .sw.cmp{border-top-style:dashed;border-top-color:var(--ink-3)}559.st-tip{margin-top:8px;display:none}560.st-tip.show{display:inline-flex;align-items:center}561.st-bars{margin-top:12px;display:grid;gap:9px}562.st-bar-lbl{display:flex;justify-content:space-between;gap:10px;font-size:12.5px}563.st-bar-lbl span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}564.st-bar-lbl b{font-family:var(--font-mono);font-size:11.5px;flex:none}565.st-bar-track{margin-top:3px;height:12px;background:rgba(20,24,20,.06);border-radius:0 3px 3px 0}566.st-bar-fill{height:100%;background:var(--accent);border:1px solid var(--ink);border-radius:0 3px 3px 0;box-sizing:border-box}567.st-donut-wrap{display:flex;flex-wrap:wrap;gap:18px;align-items:center;margin-top:12px}568.st-donut-wrap svg{width:180px;max-width:100%;margin-top:0;flex:none}569.st-donut-wrap ul{list-style:none;margin:0;padding:0;display:grid;gap:6px;min-width:min(200px,100%);flex:1}570.st-donut-wrap li{display:flex;align-items:center;gap:8px;font-size:12.5px;min-width:0}571.st-donut-wrap li .sw{width:11px;height:11px;border-radius:3px;border:1px solid var(--ink);background:var(--accent);flex:none}572.st-donut-wrap li span.lb{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}573.st-donut-wrap li b{font-family:var(--font-mono);font-size:11px}574.st-tbl-head{display:flex;flex-wrap:wrap;gap:10px;justify-content:space-between;align-items:center}575.st-tbl-head .input{max-width:240px;padding:8px 12px;font-size:13px}576.st-table{width:100%;border-collapse:collapse;font-size:13px}577.st-table th{cursor:pointer;text-align:left;padding:8px 10px;background:var(--ink);color:var(--paper);578  font-family:var(--font-mono);font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;white-space:nowrap;user-select:none}579.st-table td{padding:7px 10px;border-bottom:1px solid var(--line);white-space:nowrap}580.st-table tr:nth-child(even){background:var(--surface-2)}581.st-tbl-foot{display:flex;justify-content:space-between;align-items:center;margin-top:10px;flex-wrap:wrap;gap:8px}582.st-tbl-foot .pg{display:flex;gap:6px;align-items:center}583.st-records{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(280px,100%),1fr));gap:14px;margin-top:14px}584.st-record{padding:12px 16px;display:flex;justify-content:space-between;gap:12px;align-items:baseline;background:var(--surface-2)}585.st-record .lb{font-size:13px;color:var(--ink-2)}586.st-record .rv{text-align:right}587.st-record .rv b{font-family:var(--font-display);font-size:15px}588.st-record .rv .klabel{display:block}589/* v2 : menu PDF, sparklines KPI, jauges, résumé statistique, heatmap horaire */590.st-pdf-menu{position:relative;display:inline-block}591.st-pdf-drop{position:absolute;right:0;top:calc(100% + 6px);z-index:var(--z-dropdown,700);min-width:240px;592  display:flex;flex-direction:column;background:var(--paper);border:1.5px solid var(--ink);593  border-radius:10px;box-shadow:0 10px 28px rgba(20,24,20,.18);overflow:hidden}594.st-pdf-drop button{border:0;background:none;cursor:pointer;text-align:left;padding:12px 14px;595  font:inherit;font-size:13.5px;font-weight:600;color:var(--ink);min-height:44px;596  display:flex;justify-content:space-between;gap:10px;align-items:baseline}597.st-pdf-drop button+button{border-top:1px solid var(--line)}598.st-pdf-drop button:hover{background:var(--accent-soft)}599.st-pdf-drop small{font-family:var(--font-mono);font-size:10px;color:var(--ink-3)}600/* ---------- v3 : constructeur de rapports personnalisés ---------- */601.stb-overlay{position:fixed;inset:0;z-index:var(--z-modal,900);background:rgba(20,24,20,.45);602  display:flex;align-items:flex-start;justify-content:center;padding:4vh 14px;overflow:auto}603.stb-panel{background:var(--surface);border:1px solid var(--ink);border-radius:12px;604  width:min(980px,100%);max-height:92vh;display:flex;flex-direction:column;605  box-shadow:0 18px 48px rgba(20,24,20,.3)}606.stb-head{display:flex;justify-content:space-between;align-items:center;gap:10px;607  padding:16px 20px;border-bottom:1px solid var(--line)}608.stb-head h2{margin:0;font-size:18px}609.stb-body{display:grid;grid-template-columns:1fr 1fr;gap:0;overflow:auto;flex:1}610@media(max-width:760px){.stb-body{grid-template-columns:1fr}}611.stb-col{padding:14px 20px;min-width:0}612.stb-col+.stb-col{border-left:1px solid var(--line)}613@media(max-width:760px){.stb-col+.stb-col{border-left:0;border-top:1px solid var(--line)}}614.stb-col h3{margin:4px 0 10px;font-size:12px;font-family:var(--font-mono);615  text-transform:uppercase;letter-spacing:.06em;color:var(--ink-3)}616.stb-group{margin:0 0 6px;font-family:var(--font-mono);font-size:10.5px;font-weight:700;617  text-transform:uppercase;letter-spacing:.06em;color:var(--ink-2);margin-top:12px}618.stb-av{display:flex;justify-content:space-between;align-items:center;gap:8px;619  padding:7px 10px;border:1px solid var(--line);border-radius:8px;margin-bottom:6px;font-size:13px}620.stb-av button{flex:none}621.stb-av.stb-in{opacity:.45}622.stb-item{display:flex;align-items:center;gap:8px;padding:8px 10px;border:1px solid var(--ink);623  border-radius:8px;margin-bottom:6px;background:var(--surface-2);font-size:13px}624.stb-item .t{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}625.stb-item select{border:1px solid var(--line);border-radius:6px;background:var(--surface);626  padding:4px 6px;font-size:12px;max-width:150px}627.stb-item .mv{border:0;background:none;cursor:pointer;font-size:14px;padding:2px 4px;min-height:0}628.stb-item .mv:disabled{opacity:.25;cursor:default}629.stb-foot{display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;630  padding:14px 20px;border-top:1px solid var(--line)}631.stb-tpl{display:flex;gap:8px;align-items:center;flex-wrap:wrap}632.stb-err{color:var(--danger);font-size:12.5px;margin:6px 0 0}633.stb-empty{border:1px dashed var(--line);border-radius:8px;padding:16px;color:var(--ink-3);634  font-size:13px;text-align:center}635.st-kpi .spark{margin-top:8px}636.st-kpi .spark svg{display:block;width:100%;height:26px}637.st-gauges{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(200px,100%),1fr));gap:14px;margin-top:18px}638.st-gauge{padding:14px 16px;display:flex;gap:14px;align-items:center;min-width:0}639.st-gauge svg{width:86px;flex:none}640.st-gauge .gv{font-family:var(--font-display);font-weight:700;font-size:21px;letter-spacing:-.02em}641.st-gauge .gv small{font-size:.62em;color:var(--ink-2)}642.st-sum{display:flex;flex-wrap:wrap;gap:8px 18px;margin-top:10px;padding-top:10px;border-top:1px dashed var(--line)}643.st-sum span{font-family:var(--font-mono);font-size:10.5px;color:var(--ink-3);text-transform:uppercase;letter-spacing:.05em}644.st-sum b{color:var(--ink);font-size:11.5px;margin-left:4px}645.st-legend .sw.p1{border-top-style:dashed}646.st-legend .sw.p2{border-top-style:dotted}647.st-legend .sw.p3{border-top-style:double;border-bottom:1px solid var(--accent);height:3px}648.st-legend .sw.fill{width:12px;height:12px;border:1px solid var(--ink);border-radius:3px;background:var(--accent)}649.st-bar-lbl .bd{font-family:var(--font-mono);font-size:10px;font-weight:700;flex:none;margin-left:6px}650.st-empty{padding:20px;background:var(--surface-2);border-style:dashed}651.st-empty b{font-family:var(--font-display);font-size:14px}652.st-loading{padding:40px 0;font-family:var(--font-mono);font-size:12px;color:var(--ink-3);text-transform:uppercase;letter-spacing:.08em}653/* ---- favoris ♥ « Mon univers Ka » (hub groupe-ka.com) ---- */654.fav-btn{position:absolute;top:8px;right:8px;z-index:2;width:34px;height:34px;655  display:inline-flex;align-items:center;justify-content:center;cursor:pointer;656  background:var(--surface);border:1.5px solid var(--ink);border-radius:50%;657  color:var(--ink);padding:0;transition:transform .12s ease,background .12s ease}658.fav-btn svg{width:17px;height:17px;fill:none;stroke:currentColor;stroke-width:2;stroke-linejoin:round}659.fav-btn:hover{transform:scale(1.08);background:var(--accent-soft)}660.fav-btn.on{background:var(--accent);border-color:var(--accent-deep);color:var(--on-accent)}661.fav-btn.on svg{fill:currentColor}662.fav-btn.big{position:static;flex:none;width:44px;height:44px}663.fav-btn.big svg{width:21px;height:21px}664.d-title-row{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}665.d-title-row h1{min-width:0}666.fav-empty{grid-column:1/-1}667.fav-empty .btn-ka{margin-top:12px}668/* ---------- panneau de filtres : bottom sheet mobile / modale centrée desktop ---------- */669.sheet-ov{position:fixed;inset:0;z-index:var(--z-overlay,800);background:rgba(20,24,20,.45);670  opacity:0;transition:opacity .22s ease;pointer-events:none}671.sheet-ov.open{opacity:1;pointer-events:auto}672.sheet{position:fixed;left:0;right:0;bottom:0;z-index:var(--z-modal,900);background:var(--paper);673  border:1.5px solid var(--ink);border-bottom:0;border-radius:22px 22px 0 0;674  max-height:88dvh;display:flex;flex-direction:column;675  transform:translateY(103%);transition:transform .3s cubic-bezier(.32,.72,.28,1);676  visibility:hidden}677.sheet.open{transform:none;visibility:visible}678.sheet-grab{width:44px;height:5px;border-radius:3px;background:rgba(20,24,20,.22);679  margin:10px auto 2px;flex:none}680.sheet-head{display:flex;justify-content:space-between;align-items:center;681  padding:8px 20px 12px;border-bottom:1px solid var(--line);flex:none}682.sheet-head h2{font-size:17px;margin:0}683.sheet-x{width:38px;height:38px;border-radius:50%;border:1.5px solid var(--ink);684  background:var(--surface);cursor:pointer;font-size:14px;line-height:1;padding:0;685  display:inline-flex;align-items:center;justify-content:center}686.sheet-x:hover{background:var(--accent-soft)}687.sheet-body{overflow-y:auto;-webkit-overflow-scrolling:touch;overscroll-behavior:contain;688  padding:18px 20px 12px}689.sheet-sec{margin:0 0 22px}690.sheet-sec>.klabel{display:block;margin-bottom:10px}691.sheet-chips{display:flex;flex-wrap:wrap;gap:8px}692.sheet .select{max-width:420px}693.sheet-dates{display:flex;gap:8px;align-items:center;margin-top:12px;flex-wrap:wrap}694.sheet-dates .input{width:158px;padding:9px 11px;font-size:14px;border-radius:11px}695.sheet-foot{display:flex;gap:10px;padding:12px 20px calc(14px + env(safe-area-inset-bottom));696  border-top:1.5px solid var(--ink);flex:none;background:var(--surface);border-radius:0}697.sheet-foot .btn{flex:1;border-radius:12px}698@media(min-width:761px){699  .sheet{left:50%;right:auto;bottom:auto;top:50%;width:min(620px,94vw);max-height:82vh;700    border-radius:20px;border-bottom:1.5px solid var(--ink);701    box-shadow:12px 12px 0 rgba(20,24,20,.16);702    transform:translate(-50%,-46%) scale(.97);opacity:0;703    transition:opacity .18s ease,transform .18s ease}704  .sheet.open{opacity:1;transform:translate(-50%,-50%) scale(1)}705  .sheet-grab{display:none}706  .sheet-foot{border-radius:0 0 18px 18px}707}708709/* ---------- toast ---------- */710.toast{position:fixed;left:50%;bottom:calc(22px + env(safe-area-inset-bottom));711  transform:translateX(-50%) translateY(10px);z-index:var(--z-toast,950);712  background:var(--ink);color:var(--paper);border-radius:13px;padding:12px 19px;713  font-size:13.5px;font-weight:600;opacity:0;transition:all .25s ease;714  pointer-events:none;max-width:92vw;text-align:center;box-shadow:0 8px 24px rgba(20,24,20,.3)}715.toast.show{opacity:1;transform:translateX(-50%)}716717#ssr{display:none}718.hidden{display:none!important}719720/* ============================================================721   KA Tabbar v1 — barre de navigation mobile commune Groupe KA722   (canonique : ka-ui.git/nav/ka-tabbar.css — accent magenta via723   var(--accent) du site) ============================================ */724.ka-tabbar{display:none}725@media (max-width:760px){726  .ka-tabbar{727    --ktb-bg:rgba(250,249,245,.94);728    --ktb-ink:var(--ink,#16191a);729    --ktb-dim:var(--ink-3,#70756d);730    --ktb-accent:var(--accent,#d6336c);731    --ktb-line:var(--hairline-strong,rgba(20,24,20,.16));732    --ktb-shadow:0 10px 30px rgba(20,24,20,.16),0 2px 6px rgba(20,24,20,.07);733    position:fixed;left:max(12px,env(safe-area-inset-left));right:max(12px,env(safe-area-inset-right));734    bottom:calc(10px + env(safe-area-inset-bottom));z-index:var(--z-bottombar,600);735    display:grid;grid-auto-flow:column;grid-auto-columns:1fr;max-width:440px;margin-inline:auto;736    padding:5px;background:var(--ktb-bg);737    -webkit-backdrop-filter:blur(14px) saturate(1.1);backdrop-filter:blur(14px) saturate(1.1);738    border:1px solid var(--ktb-line);border-radius:18px;box-shadow:var(--ktb-shadow)}739  .ka-tabbar a,.ka-tabbar button{appearance:none;background:none;border:0;cursor:pointer;740    display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;741    min-height:50px;padding:6px 2px 5px;border-radius:13px;742    font-family:var(--font-display,inherit);font-size:10px;font-weight:650;letter-spacing:.01em;743    line-height:1;color:var(--ktb-dim);text-decoration:none;-webkit-tap-highlight-color:transparent;744    transition:color .15s ease}745  .ka-tabbar svg{display:block;transition:transform .15s ease}746  .ka-tabbar a:active svg,.ka-tabbar button:active svg{transform:translateY(1px) scale(.9)}747  .ka-tabbar .active{color:var(--ktb-active-label,var(--ktb-ink))}748  .ka-tabbar .active svg{color:var(--ktb-accent);animation:ktb-pop .25s ease}749  .ka-tabbar .active::after{content:"";width:16px;height:2.5px;border-radius:2px;750    background:var(--ktb-accent);animation:ktb-under .22s ease}751  body:has(.ka-tabbar){padding-bottom:calc(84px + env(safe-area-inset-bottom))}752  body:has(.ka-tabbar) .kaa-btn.kaa-btn{bottom:calc(88px + env(safe-area-inset-bottom,0px));right:14px}753  body:has(.ka-tabbar) .kaa-panel.kaa-panel:not(.kaa-full){bottom:calc(154px + env(safe-area-inset-bottom,0px))}754}755@keyframes ktb-under{from{transform:scaleX(0);opacity:0}}756@keyframes ktb-pop{40%{transform:translateY(-2px)}}757@media (prefers-reduced-motion:reduce){758  .ka-tabbar svg{transition:none}759  .ka-tabbar .active svg,.ka-tabbar .active::after{animation:none}760}761762763/* =====================================================================764   Fiche événement premium (refonte 2026-09-07, même socle que les fiches765   Lou-Ka v3 / Immo-Ka v3) — tokens --sk-*, styles SCOPÉS sous .sk-*.766   Fond papier, cartes blanches bord 1 px, FRAMBOISE réservée aux CTA / prix /767   états actifs. Aucun `order` : ordre DOM = ordre visuel.768   ===================================================================== */769:root{770  --sk-bg:#f5f3ee; --sk-surface:#ffffff; --sk-surface-2:#faf9f5;771  --sk-border:#e6e3dc; --sk-border-2:#d3cfc6;772  --sk-text:#141814; --sk-text-2:#4d5551; --sk-muted:#7c837e;773  --sk-accent:#d6336c; --sk-accent-deep:#a12551; --sk-accent-soft:#fbe0eb;774  --sk-success:#1e7b4a; --sk-success-soft:#e7f4ec; --sk-warning:#a8690a; --sk-warning-soft:#fcf3e1;775  --sk-danger:#b3423a; --sk-danger-soft:#fbe9e7; --sk-info:#3b5bdb; --sk-info-soft:#e9edfb;776  --sk-r-sm:10px; --sk-r-md:14px; --sk-r-lg:18px; --sk-r-xl:22px;777  --sk-shadow-sm:0 2px 12px rgba(0,0,0,.04); --sk-shadow-md:0 8px 28px rgba(0,0,0,.07);778  --sk-header-h:56px; --sk-nav-h:52px;779}780body.sk-fiche-page{background:var(--sk-bg)}781body.sk-fiche-page .ticker{display:none}782body.sk-fiche-page .ka-tabbar{display:none!important}783body.sk-fiche-page .kaa-btn,body.sk-fiche-page .kaa-hello{display:none!important}784body.sk-fiche-page .header{background:rgba(245,243,238,.92);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-bottom:1px solid var(--sk-border)}785body.sk-fiche-page .header-inner{min-height:var(--sk-header-h);gap:10px}786body.sk-fiche-page .header .brand{font-size:21px}787body.sk-fiche-page .header .brand-tag,body.sk-fiche-page .header .gk-badge{display:none}788body.sk-fiche-page .header .nav{margin-left:0}789.hdr-actions{display:flex;align-items:center;gap:6px;margin-left:auto}790.hdr-btn{display:inline-grid;place-items:center;width:40px;height:40px;border-radius:999px;border:1px solid var(--sk-border);791  background:var(--sk-surface);color:var(--sk-text);cursor:pointer;padding:0;transition:background .15s,transform .15s}792.hdr-btn:active{transform:scale(.95)}793.hdr-btn.on{color:var(--sk-accent);border-color:var(--sk-accent);background:var(--sk-accent-soft)}794@media(hover:hover){.hdr-btn:hover{background:var(--sk-surface-2)}}795/* le cœur ♥ existant, posé dans la rangée d'actions de la fiche */796.sk-fiche .fav-btn{position:static;width:46px;height:46px;border-radius:12px;border:1px solid var(--sk-border);background:var(--sk-surface);color:var(--sk-text);box-shadow:none}797.sk-fiche .fav-btn.on{color:var(--sk-accent);border-color:var(--sk-accent);background:var(--sk-accent-soft)}798.sk-fiche .fav-btn svg{width:19px;height:19px;stroke:currentColor;fill:none;stroke-width:2}799.sk-fiche .fav-btn.on svg{fill:currentColor}800/* ---- gabarit ---- */801.sk-fiche { padding: 10px 0 110px; color: var(--sk-text); }802.sk-wrap { max-width: 1200px; margin: 0 auto; padding: 0 16px; }803.sk-grid { display: block; }804.sk-main { min-width: 0; display: flex; flex-direction: column; gap: 14px; }805.sk-aside { display: none; }806@media (min-width: 768px) {807  .sk-wrap { padding: 0 24px; }808  .sk-fiche { padding-top: 18px; }809  .sk-main { gap: 16px; }810}811@media (min-width: 1024px) {812  .sk-grid { display: grid; grid-template-columns: minmax(0, 1fr) 356px; gap: 32px; align-items: start; }813  .sk-aside { display: block; position: sticky; top: calc(var(--sk-header-h) + 16px); }814  .sk-fiche { padding-bottom: 80px; }815}816.sk-crumbs { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 12px; color: var(--sk-muted); margin: 0 0 10px; }817.sk-crumbs a { color: var(--sk-text-2); }818.sk-crumbs span:last-child { color: var(--sk-text); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 60vw; }819.sk-fiche section[id] { scroll-margin-top: calc(var(--sk-header-h) + var(--sk-nav-h) + 10px); }820821/* ---- carte de section ---- */822.sk-card {823  background: var(--sk-surface); border: 1px solid var(--sk-border);824  border-radius: var(--sk-r-lg); box-shadow: var(--sk-shadow-sm);825  padding: 16px; min-width: 0;826}827@media (min-width: 768px) { .sk-card { padding: 20px 22px; } }828.sk-card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 12px; }829.sk-card-title { display: flex; align-items: center; gap: 9px; margin: 0; font-family: var(--font-display); font-weight: 700; font-size: 17px; line-height: 1.2; letter-spacing: -0.02em; color: var(--sk-text); }830.sk-card-title svg { color: var(--sk-accent-deep); flex: none; }831.sk-card-sub { font-size: 12.5px; color: var(--sk-muted); margin: 3px 0 0; }832.sk-card-aside { flex: none; display: flex; align-items: center; gap: 8px; }833.sk-link { background: none; border: 0; padding: 0; color: var(--sk-text-2); font: 600 13px var(--font-body); cursor: pointer; text-decoration: underline; text-underline-offset: 3px; text-decoration-color: var(--sk-border-2); }834.sk-link:hover { color: var(--sk-accent-deep); }835.sk-more {836  display: inline-flex; align-items: center; justify-content: center; gap: 8px;837  width: 100%; min-height: 44px; margin-top: 12px; padding: 8px 14px;838  border: 1px solid var(--sk-border); border-radius: var(--sk-r-sm); background: var(--sk-surface);839  color: var(--sk-text); font: 600 13.5px var(--font-body); cursor: pointer;840  transition: background 0.15s, border-color 0.15s;841}842.sk-more:hover { background: var(--sk-surface-2); border-color: var(--sk-border-2); }843.sk-more svg { color: var(--sk-muted); }844845/* ---- boutons ---- */846.sk-btn {847  display: inline-flex; align-items: center; justify-content: center; gap: 8px;848  min-height: 46px; padding: 10px 16px; border-radius: 12px; border: 1px solid transparent;849  font: 600 14.5px var(--font-body); cursor: pointer; text-decoration: none;850  transition: transform 0.12s, background 0.15s, box-shadow 0.15s; white-space: nowrap;851}852.sk-btn:active { transform: translateY(1px); }853.sk-btn-primary { background: var(--sk-accent); color: #fff; box-shadow: 0 6px 18px rgba(226, 55, 68, 0.22); }854.sk-btn-primary:hover { background: var(--sk-accent-deep); }855.sk-btn-ghost { background: var(--sk-surface); border-color: var(--sk-border); color: var(--sk-text); }856.sk-btn-ghost:hover { background: var(--sk-surface-2); border-color: var(--sk-border-2); }857.sk-btn-icon { width: 46px; padding: 0; flex: none; }858.sk-btn-icon.on { color: var(--sk-accent); border-color: var(--sk-accent); background: var(--sk-accent-soft); }859860/* ---- héro ---- */861.sk-hero { display: flex; flex-direction: column; gap: 10px; padding: 4px 0 0; }862.sk-hero-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }863.sk-price { font-family: var(--font-display); font-weight: 700; font-size: clamp(32px, 8.4vw, 38px); line-height: 1; letter-spacing: -0.035em; color: var(--sk-text); display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 8px; }864.sk-price small { font: 500 14px var(--font-body); color: var(--sk-muted); letter-spacing: 0; }865.sk-price-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }866.sk-capsule {867  display: inline-flex; align-items: center; gap: 6px; padding: 5px 10px;868  border-radius: 999px; font: 600 12.5px var(--font-body); border: 1px solid transparent; white-space: nowrap;869}870.sk-capsule b { font-weight: 700; }871.sk-capsule.good { background: var(--sk-success-soft); color: var(--sk-success); }872.sk-capsule.ok { background: var(--sk-surface); color: var(--sk-text-2); border-color: var(--sk-border); }873.sk-capsule.high { background: var(--sk-warning-soft); color: var(--sk-warning); }874.sk-capsule.neutral { background: var(--sk-surface-2); color: var(--sk-muted); border-color: var(--sk-border); }875.sk-capsule.brand { background: var(--sk-accent-soft); color: var(--sk-accent-deep); }876.sk-h1 { margin: 0; font-family: var(--font-body); font-weight: 600; font-size: 16px; line-height: 1.35; letter-spacing: 0; color: var(--sk-text); }877.sk-h1 .sk-city { display: block; font-weight: 500; font-size: 14px; color: var(--sk-text-2); }878.sk-summary { margin: 0; font-size: 14px; color: var(--sk-text-2); display: flex; flex-wrap: wrap; gap: 4px 8px; align-items: center; }879.sk-summary .sep { color: var(--sk-border-2); }880.sk-actions { display: flex; gap: 8px; align-items: center; }881.sk-actions .sk-btn-primary { flex: 1; }882.sk-hero .sk-actions { margin-top: 2px; }883@media (min-width: 1024px) {884  .sk-hero .sk-actions { display: none; }   /* actions dans l'aside sticky */885}886887/* ---- galerie ---- */888.sk-gallery { position: relative; border-radius: var(--sk-r-lg); overflow: hidden; background: #ecece8; }889.sk-gallery-track { display: flex; overflow-x: auto; scroll-snap-type: x mandatory; aspect-ratio: 4 / 3; scrollbar-width: none; -webkit-overflow-scrolling: touch; }890.sk-gallery-track::-webkit-scrollbar { display: none; }891.sk-gallery-track img, .sk-gallery > img { flex: 0 0 100%; width: 100%; height: 100%; object-fit: cover; scroll-snap-align: center; cursor: zoom-in; display: block; }892.sk-gallery > img { aspect-ratio: 4 / 3; }893@media (min-width: 768px) { .sk-gallery-track, .sk-gallery > img { aspect-ratio: 16 / 9; } }894.sk-gallery-count {895  position: absolute; left: 12px; bottom: 12px; z-index: 2;896  background: rgba(20, 24, 20, 0.66); color: #fff; font: 600 12px var(--font-body);897  padding: 4px 10px; border-radius: 999px; backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);898  font-variant-numeric: tabular-nums;899}900.sk-gallery-full, .sk-gallery-nav {901  position: absolute; z-index: 2; display: grid; place-items: center;902  width: 40px; height: 40px; border-radius: 999px; border: 0; cursor: pointer;903  background: rgba(255, 255, 255, 0.92); color: var(--sk-text); box-shadow: 0 2px 10px rgba(0, 0, 0, 0.12);904}905.sk-gallery-full { right: 12px; bottom: 12px; }906.sk-gallery-nav { top: 50%; transform: translateY(-50%); display: none; }907.sk-gallery-nav.prev { left: 12px; } .sk-gallery-nav.next { right: 12px; }908@media (hover: hover) and (min-width: 768px) { .sk-gallery-nav { display: grid; } }909.sk-thumbs { display: none; }910@media (min-width: 768px) {911  .sk-thumbs { display: grid; grid-template-columns: repeat(auto-fill, minmax(76px, 1fr)); gap: 6px; margin-top: 8px; }912  .sk-thumbs button { border: 0; padding: 0; border-radius: 8px; overflow: hidden; aspect-ratio: 4 / 3; background: #ecece8; cursor: pointer; outline-offset: 2px; opacity: 0.75; transition: opacity 0.15s; }913  .sk-thumbs button.on, .sk-thumbs button:hover { opacity: 1; }914  .sk-thumbs button.on { outline: 2px solid var(--sk-accent); }915  .sk-thumbs img { width: 100%; height: 100%; object-fit: cover; }916}917/* lightbox (plein écran) */918.sk-lightbox { position: fixed; inset: 0; z-index: var(--z-modal, 900); background: rgba(12, 14, 12, 0.96); }919.sk-lightbox-track { display: flex; overflow-x: auto; scroll-snap-type: x mandatory; height: 100%; scrollbar-width: none; touch-action: pan-x pinch-zoom; }920.sk-lightbox-track::-webkit-scrollbar { display: none; }921.sk-lightbox-cell { flex: 0 0 100%; height: 100%; scroll-snap-align: center; display: flex; align-items: center; justify-content: center; padding: max(56px, env(safe-area-inset-top)) 10px max(28px, env(safe-area-inset-bottom)); overflow: hidden; }922.sk-lightbox-cell img { max-width: 100%; max-height: 100%; border-radius: 10px; user-select: none; -webkit-user-drag: none; will-change: transform; }923.sk-lightbox-close { position: fixed; top: max(12px, env(safe-area-inset-top)); right: 12px; z-index: 3; width: 44px; height: 44px; border-radius: 50%; border: 1px solid rgba(255, 255, 255, 0.3); background: rgba(0, 0, 0, 0.5); color: #fff; display: grid; place-items: center; cursor: pointer; }924.sk-lightbox-count { position: fixed; top: max(22px, env(safe-area-inset-top)); left: 50%; transform: translateX(-50%); z-index: 3; color: #fff; font: 600 13px var(--font-body); background: rgba(0, 0, 0, 0.45); padding: 4px 12px; border-radius: 999px; }925.sk-lightbox .sk-gallery-nav { display: none; }926@media (hover: hover) { .sk-lightbox .sk-gallery-nav { display: grid; position: fixed; } }927928/* ---- Score ---- */929.sk-score { display: grid; grid-template-columns: auto 1fr; gap: 16px; align-items: center; }930.sk-score-ring { position: relative; width: 88px; height: 88px; flex: none; }931.sk-score-ring svg { width: 88px; height: 88px; transform: rotate(-90deg); }932.sk-score-ring .bg { fill: none; stroke: var(--sk-surface-2); stroke-width: 7; }933.sk-score-ring .arc { fill: none; stroke: var(--sk-accent); stroke-width: 7; stroke-linecap: round; transition: stroke-dasharray 0.9s cubic-bezier(0.2, 0.8, 0.2, 1); }934.sk-score-ring .arc.partial { stroke: var(--sk-text-2); }935.sk-score-val { position: absolute; inset: 0; display: grid; place-items: center; font-family: var(--font-display); font-weight: 700; font-size: 27px; letter-spacing: -0.03em; line-height: 1; }936.sk-score-val small { display: block; text-align: center; font: 600 10px var(--font-body); color: var(--sk-muted); margin-top: 2px; letter-spacing: 0.04em; }937.sk-score-lbl { font-family: var(--font-display); font-weight: 700; font-size: 17px; letter-spacing: -0.02em; }938.sk-score-sub { font-size: 13px; color: var(--sk-text-2); margin-top: 2px; line-height: 1.4; }939.sk-score-parts { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }940.sk-score-part { display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; padding: 4px 9px; border-radius: 8px; background: var(--sk-surface-2); border: 1px solid var(--sk-border); color: var(--sk-text-2); }941.sk-score-part b { color: var(--sk-text); font-variant-numeric: tabular-nums; }942.sk-score-part.na { color: var(--sk-muted); border-style: dashed; }943944/* ---- En bref ---- */945.sk-brief { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }946.sk-brief li { display: grid; grid-template-columns: 24px 1fr; gap: 10px; align-items: start; }947.sk-brief-ico { width: 24px; height: 24px; border-radius: 8px; display: grid; place-items: center; font-size: 12px; font-weight: 700; margin-top: 1px; }948.sk-brief-ico.good { background: var(--sk-success-soft); color: var(--sk-success); }949.sk-brief-ico.warn { background: var(--sk-warning-soft); color: var(--sk-warning); }950.sk-brief-ico.bad { background: var(--sk-danger-soft); color: var(--sk-danger); }951.sk-brief-ico.neutral { background: var(--sk-surface-2); color: var(--sk-muted); border: 1px solid var(--sk-border); }952.sk-brief-t { font-weight: 600; font-size: 14px; line-height: 1.35; }953.sk-brief-d { font-size: 13px; color: var(--sk-text-2); line-height: 1.4; margin-top: 1px; }954955/* ---- navigation par sections (sticky) ---- */956.sk-nav {957  position: sticky; top: var(--sk-header-h); z-index: var(--z-sticky, 300);958  margin: 0 -16px; padding: 6px 16px;959  background: rgba(245, 243, 238, 0.97); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);960  border-bottom: 1px solid var(--sk-border);961}962@media (min-width: 768px) { .sk-nav { margin: 0 -24px; padding: 6px 24px; } }963@media (min-width: 1024px) { .sk-nav { margin: 0; padding: 6px 0; border-radius: 0; } }964.sk-nav-track { display: flex; gap: 6px; overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; }965.sk-nav-track::-webkit-scrollbar { display: none; }966.sk-nav a {967  flex: 0 0 auto; display: inline-flex; align-items: center; min-height: 40px; padding: 8px 13px;968  border-radius: 999px; font: 600 13px var(--font-body); color: var(--sk-text-2);969  border: 1px solid transparent; transition: background 0.15s, color 0.15s; scroll-snap-align: start;970}971.sk-nav a:hover { background: var(--sk-surface); border-color: var(--sk-border); }972.sk-nav a.on { background: var(--sk-text); color: #fff; border-color: var(--sk-text); }973974/* ---- caractéristiques ---- */975.sk-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }976@media (min-width: 560px) { .sk-facts { grid-template-columns: repeat(3, minmax(0, 1fr)); } }977@media (min-width: 900px) { .sk-facts { grid-template-columns: repeat(4, minmax(0, 1fr)); } }978.sk-fact { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border: 1px solid var(--sk-border); border-radius: 12px; background: var(--sk-surface-2); min-width: 0; }979.sk-fact-ico { width: 32px; height: 32px; border-radius: 9px; background: var(--sk-surface); border: 1px solid var(--sk-border); display: grid; place-items: center; color: var(--sk-accent-deep); flex: none; }980.sk-fact-v { font-weight: 700; font-size: 14px; line-height: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }981.sk-fact-l { font-size: 11.5px; color: var(--sk-muted); margin-top: 1px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }982.sk-fact-txt { min-width: 0; }983.sk-facts-rows { margin-top: 12px; }984.sk-kv { display: flex; justify-content: space-between; gap: 12px; padding: 8px 0; border-top: 1px solid var(--sk-border); font-size: 13.5px; }985.sk-kv .k { color: var(--sk-muted); } .sk-kv .v { font-weight: 600; text-align: right; }986.sk-note { margin: 10px 0 0; padding: 9px 12px; border-radius: 10px; font-size: 13px; line-height: 1.4; }987.sk-note.good { background: var(--sk-success-soft); color: var(--sk-success); }988.sk-note.info { background: var(--sk-info-soft); color: var(--sk-info); }989.sk-note.warn { background: var(--sk-warning-soft); color: var(--sk-warning); }990991/* ---- description ---- */992.sk-desc { position: relative; }993.sk-desc-body p { font-size: 15px; line-height: 1.6; color: var(--sk-text-2); margin: 0 0 10px; white-space: pre-line; }994.sk-desc-body h4 { margin: 12px 0 3px; font: 700 14px var(--font-body); color: var(--sk-text); }995.sk-desc-lead { font-size: 15.5px !important; color: var(--sk-text) !important; font-weight: 500; }996.sk-desc.clamped .sk-desc-body { max-height: 200px; overflow: hidden; -webkit-mask-image: linear-gradient(#000 62%, transparent); mask-image: linear-gradient(#000 62%, transparent); }997.sk-desc-orig { margin-top: 8px; }998.sk-desc-orig p { font-size: 13.5px; color: var(--sk-muted); }9991000/* ---- inclusions ---- */1001.sk-amen { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 16px; }1002@media (min-width: 640px) { .sk-amen { grid-template-columns: repeat(3, minmax(0, 1fr)); } }1003.sk-amen-it { display: flex; align-items: center; gap: 9px; min-height: 42px; padding: 5px 0; border-bottom: 1px solid var(--sk-border); font-size: 13.5px; min-width: 0; }1004.sk-amen-ico { width: 28px; height: 28px; border-radius: 8px; background: var(--sk-accent-soft); color: var(--sk-accent-deep); display: grid; place-items: center; flex: none; }1005.sk-amen-it.unconfirmed { color: var(--sk-text-2); }1006.sk-amen-it.unconfirmed .sk-amen-ico { background: var(--sk-surface-2); color: var(--sk-muted); border: 1px dashed var(--sk-border-2); }1007.sk-amen-txt { min-width: 0; line-height: 1.25; }1008.sk-amen-conf { margin-left: auto; color: var(--sk-success); flex: none; }10091010/* ---- KPI / tuiles ---- */1011.sk-kpis { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }1012@media (min-width: 600px) { .sk-kpis { grid-template-columns: repeat(3, minmax(0, 1fr)); } .sk-kpi-v { font-size: 20px; } }1013.sk-kpis.cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }1014.sk-kpi { padding: 10px 12px; border: 1px solid var(--sk-border); border-radius: 12px; background: var(--sk-surface); min-width: 0; }1015.sk-kpi.accent { background: var(--sk-accent-soft); border-color: #f5c2c6; }1016.sk-kpi-v { font-family: var(--font-display); font-weight: 700; font-size: clamp(16px, 4.8vw, 20px); letter-spacing: -0.025em; line-height: 1.1; font-variant-numeric: tabular-nums; color: var(--sk-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }1017.sk-kpi-v.wrap { white-space: normal; font-size: 16px; line-height: 1.2; }1018.sk-kpi-v small { font: 600 11px var(--font-body); color: var(--sk-muted); margin-left: 3px; }1019.sk-kpi-l { font-size: 11.5px; color: var(--sk-muted); margin-top: 3px; line-height: 1.3; }1020.sk-kpi.anim .sk-kpi-v { animation: sk-rise 0.5s ease both; }1021@keyframes sk-rise { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }10221023/* ---- rangées compactes (accessibilité, mesures) ---- */1024.sk-rows { display: flex; flex-direction: column; gap: 4px; }1025.sk-row { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; align-items: center; gap: 10px; min-height: 30px; font-size: 13.5px; }1026.sk-row-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--sk-text); }1027.sk-row-name small { color: var(--sk-muted); font-size: 12px; margin-left: 4px; }1028.sk-row-bar { width: 78px; height: 6px; border-radius: 3px; background: var(--sk-surface-2); border: 1px solid var(--sk-border); overflow: hidden; }1029.sk-row-bar i { display: block; height: 100%; background: var(--sk-text); border-radius: 3px; }1030.sk-row-bar i.good { background: var(--sk-success); }1031.sk-row-bar i.warn { background: var(--sk-warning); }1032.sk-row-bar i.bad { background: var(--sk-danger); }1033.sk-row-val { font-weight: 700; font-size: 13px; font-variant-numeric: tabular-nums; min-width: 28px; text-align: right; }1034.sk-row-lbl { font-size: 12px; color: var(--sk-muted); min-width: 68px; text-align: right; }1035.sk-row-lbl.good { color: var(--sk-success); } .sk-row-lbl.warn { color: var(--sk-warning); } .sk-row-lbl.bad { color: var(--sk-danger); }10361037/* ---- pastilles d'état ---- */1038.sk-badge { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 999px; font: 600 12px var(--font-body); border: 1px solid transparent; white-space: nowrap; }1039.sk-badge::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }1040.sk-badge.good { background: var(--sk-success-soft); color: var(--sk-success); }1041.sk-badge.warn { background: var(--sk-warning-soft); color: var(--sk-warning); }1042.sk-badge.bad { background: var(--sk-danger-soft); color: var(--sk-danger); }1043.sk-badge.neutral { background: var(--sk-surface-2); color: var(--sk-text-2); border-color: var(--sk-border); }1044.sk-badge.info { background: var(--sk-info-soft); color: var(--sk-info); }1045.sk-badge.lg { font-size: 13.5px; padding: 6px 12px; }1046.sk-pill { display: inline-block; font: 600 10.5px var(--font-body); text-transform: uppercase; letter-spacing: 0.05em; padding: 2px 7px; border-radius: 6px; margin-left: 6px; vertical-align: 1px; }1047.sk-pill.included { background: var(--sk-success-soft); color: var(--sk-success); }1048.sk-pill.observed { background: var(--sk-surface-2); color: var(--sk-text-2); border: 1px solid var(--sk-border); }1049.sk-pill.estimated { background: var(--sk-accent-soft); color: var(--sk-accent-deep); }1050.sk-pill.unknown { background: var(--sk-surface-2); color: var(--sk-muted); border: 1px dashed var(--sk-border-2); }10511052/* ---- listes d'items (lieux, comparables, stations) ---- */1053.sk-list { list-style: none; margin: 0; padding: 0; }1054.sk-item { display: flex; align-items: center; gap: 12px; padding: 10px 0; border-top: 1px solid var(--sk-border); min-width: 0; }1055.sk-list > .sk-item:first-child { border-top: 0; padding-top: 2px; }1056.sk-item-ico { width: 34px; height: 34px; border-radius: 10px; display: grid; place-items: center; flex: none; background: var(--sk-surface-2); border: 1px solid var(--sk-border); color: var(--sk-text-2); }1057.sk-item-ico svg.cm-ico { width: 26px; height: 26px; }1058.sk-item-main { flex: 1; min-width: 0; }1059.sk-item-t { font-weight: 600; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }1060.sk-item-s { font-size: 12.5px; color: var(--sk-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }1061.sk-item-r { text-align: right; flex: none; }1062.sk-item-v { font-weight: 700; font-size: 14px; font-variant-numeric: tabular-nums; }1063.sk-item-m { font-size: 12px; color: var(--sk-muted); }1064.sk-item-best { color: var(--sk-success); font-weight: 700; font-size: 11.5px; margin-left: 6px; }10651066/* ---- carrousel horizontal ---- */1067.sk-carousel { display: flex; gap: 10px; overflow-x: auto; scroll-snap-type: x mandatory; scrollbar-width: none; -webkit-overflow-scrolling: touch; margin: 0 -16px; padding: 2px 16px 6px; }1068.sk-carousel::-webkit-scrollbar { display: none; }1069@media (min-width: 768px) { .sk-carousel { margin: 0; padding: 2px 0 6px; } }1070.sk-ccard { flex: 0 0 44%; min-width: 148px; max-width: 210px; scroll-snap-align: start; border: 1px solid var(--sk-border); border-radius: var(--sk-r-md); padding: 12px; background: var(--sk-surface); display: flex; flex-direction: column; gap: 6px; min-height: 104px; }1071@media (min-width: 768px) { .sk-ccard { flex-basis: 176px; } }1072.sk-ccard-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }1073.sk-ccard-c { font: 600 10.5px var(--font-body); text-transform: uppercase; letter-spacing: 0.06em; color: var(--sk-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }1074.sk-ccard-d { font-family: var(--font-display); font-weight: 700; font-size: 18px; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }1075.sk-ccard-n { font-size: 12.5px; color: var(--sk-text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }1076.sk-ccard-m { font-size: 11.5px; color: var(--sk-muted); }10771078/* ---- filtres (chips) ---- */1079.sk-chips { display: flex; gap: 6px; overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; padding-bottom: 10px; }1080.sk-chips::-webkit-scrollbar { display: none; }1081.sk-chip { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 7px; min-height: 38px; padding: 6px 12px; border-radius: 999px; border: 1px solid var(--sk-border); background: var(--sk-surface); color: var(--sk-text-2); font: 600 12.5px var(--font-body); cursor: pointer; transition: background 0.15s, color 0.15s, border-color 0.15s; }1082.sk-chip .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--c, var(--sk-text)); flex: none; }1083.sk-chip small { font-weight: 600; opacity: 0.65; }1084.sk-chip.on { background: var(--sk-text); color: #fff; border-color: var(--sk-text); }1085.sk-chip:disabled { opacity: 0.45; cursor: default; }10861087/* ---- carte ---- */1088.sk-map { position: relative; height: 340px; border-radius: var(--sk-r-lg); overflow: hidden; border: 1px solid var(--sk-border); background: #ecece8; }1089@media (min-width: 768px) { .sk-map { height: 440px; } }1090@media (min-width: 1024px) { .sk-map { height: 520px; } }1091.sk-map .ka-map {1092  position: absolute; inset: 0;1093  --ka-accent: var(--sk-accent); --ka-on-accent: #fff; --ka-surface: var(--sk-surface);1094  --ka-ink: var(--sk-text); --ka-line: var(--sk-border-2); --ka-radius: 10px;1095  --ka-shadow: 0 8px 24px rgba(20, 24, 20, 0.14); --ka-font: var(--font-body);1096}1097.sk-map-legend { position: absolute; left: 10px; bottom: 10px; z-index: 5; display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 999px; background: rgba(255, 255, 255, 0.92); border: 1px solid var(--sk-border); font: 500 11px var(--font-body); color: var(--sk-text-2); pointer-events: none; }1098.sk-map-legend i { width: 10px; height: 10px; border-radius: 3px; background: var(--sk-accent); }1099.sk-map-skel { position: absolute; inset: 0; }1100.sk-map-hint { font-size: 12px; color: var(--sk-muted); margin: 8px 0 0; }11011102/* ---- accordéon ---- */1103.sk-acc { border-top: 1px solid var(--sk-border); }1104.sk-acc:first-child { border-top: 0; }1105.sk-acc-btn { width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 10px; min-height: 48px; padding: 10px 0; background: none; border: 0; font: 600 14px var(--font-body); color: var(--sk-text); cursor: pointer; text-align: left; }1106.sk-acc-btn .sk-acc-meta { font-weight: 500; font-size: 12.5px; color: var(--sk-muted); margin-left: auto; white-space: nowrap; }1107.sk-acc-btn .chev { color: var(--sk-muted); flex: none; transition: transform 0.2s ease; }1108.sk-acc-btn[aria-expanded="true"] .chev { transform: rotate(180deg); }1109.sk-acc-body { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.22s ease; }1110.sk-acc-body.open { grid-template-rows: 1fr; }1111.sk-acc-body > div { min-height: 0; overflow: hidden; }1112.sk-acc-inner { padding: 0 0 14px; font-size: 13.5px; color: var(--sk-text-2); line-height: 1.5; }1113.sk-acc-inner p { margin: 0 0 8px; }1114.sk-acc-inner a { color: var(--sk-text-2); text-decoration: underline; text-underline-offset: 2px; }1115.sk-acc.sm .sk-acc-btn { min-height: 40px; font-size: 13px; color: var(--sk-text-2); }11161117/* ---- bottom sheet (mobile) / modale (desktop) ---- */1118.sk-sheet-backdrop { position: fixed; inset: 0; z-index: var(--z-overlay, 800); background: rgba(20, 24, 20, 0.42); animation: sk-fade 0.18s ease; }1119.sk-sheet {1120  position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-modal, 900);1121  height: var(--sk-sheet-h, 68dvh); max-height: 94dvh;1122  background: var(--sk-surface); border-radius: 20px 20px 0 0;1123  display: flex; flex-direction: column; box-shadow: 0 -10px 40px rgba(0, 0, 0, 0.18);1124  animation: sk-up 0.28s cubic-bezier(0.2, 0.8, 0.2, 1);1125  padding-bottom: env(safe-area-inset-bottom);1126  touch-action: pan-y;1127}1128.sk-sheet.full { height: 94dvh; }1129.sk-sheet-handle { width: 40px; height: 4px; border-radius: 2px; background: var(--sk-border-2); margin: 8px auto 0; flex: none; }1130.sk-sheet-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 10px 16px 10px; border-bottom: 1px solid var(--sk-border); flex: none; }1131.sk-sheet-title { font-family: var(--font-display); font-weight: 700; font-size: 16px; letter-spacing: -0.02em; margin: 0; }1132.sk-sheet-sub { font-size: 12.5px; color: var(--sk-muted); margin: 2px 0 0; }1133.sk-sheet-x { width: 36px; height: 36px; border-radius: 50%; border: 1px solid var(--sk-border); background: var(--sk-surface-2); display: grid; place-items: center; cursor: pointer; color: var(--sk-text); flex: none; }1134.sk-sheet-body { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain; padding: 12px 16px 18px; }1135.sk-sheet-foot { flex: none; padding: 10px 16px; border-top: 1px solid var(--sk-border); background: var(--sk-surface); }1136@keyframes sk-up { from { transform: translateY(40px); opacity: 0.6; } to { transform: none; opacity: 1; } }1137@keyframes sk-fade { from { opacity: 0; } to { opacity: 1; } }1138@keyframes sk-pop { from { transform: translate(-50%, -50%) scale(0.97); opacity: 0; } to { transform: translate(-50%, -50%) scale(1); opacity: 1; } }1139@media (min-width: 900px) {1140  .sk-sheet, .sk-sheet.full {1141    left: 50%; right: auto; top: 50%; bottom: auto; transform: translate(-50%, -50%);1142    width: min(680px, calc(100vw - 48px)); height: auto; max-height: min(82vh, 780px);1143    border-radius: var(--sk-r-lg); animation: sk-pop 0.18s ease; padding-bottom: 0;1144  }1145  .sk-sheet-handle { display: none; }1146  .sk-sheet-head { padding: 16px 20px 12px; }1147  .sk-sheet-body { padding: 14px 20px 20px; }1148}1149.sk-sheet-filters { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px; }1150.sk-seg { display: inline-flex; border: 1px solid var(--sk-border); border-radius: 999px; overflow: hidden; background: var(--sk-surface); }1151.sk-seg button { border: 0; background: transparent; padding: 7px 12px; min-height: 36px; font: 600 12.5px var(--font-body); color: var(--sk-text-2); cursor: pointer; }1152.sk-seg button.on { background: var(--sk-text); color: #fff; }1153.sk-seg button + button { border-left: 1px solid var(--sk-border); }11541155/* ---- CTA sticky ---- */1156.sk-cta-bar {1157  position: fixed; left: 0; right: 0; bottom: var(--consent-h, 0px); z-index: var(--z-bottombar, 600);1158  display: flex; align-items: center; gap: 12px;1159  padding: 10px 16px calc(10px + env(safe-area-inset-bottom));1160  background: rgba(255, 255, 255, 0.94); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);1161  border-top: 1px solid var(--sk-border);1162  transform: translateY(110%); transition: transform 0.25s ease; will-change: transform;1163}1164.sk-cta-bar.show { transform: none; }1165.sk-cta-txt { min-width: 0; flex: 0 1 auto; }1166.sk-cta-price { font-family: var(--font-display); font-weight: 700; font-size: 18px; letter-spacing: -0.02em; line-height: 1.1; white-space: nowrap; }1167.sk-cta-price small { font: 500 11px var(--font-body); color: var(--sk-muted); letter-spacing: 0; }1168.sk-cta-sub { font-size: 12px; color: var(--sk-text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }1169.sk-cta-sub.good { color: var(--sk-success); font-weight: 600; }1170.sk-cta-bar .sk-btn-primary { flex: 1; min-height: 44px; margin-left: auto; max-width: 260px; }1171@media (min-width: 1024px) { .sk-cta-bar { display: none; } }11721173/* ---- Demander à Ka ---- */1174.sk-ka-btn {1175  position: fixed; right: 14px; bottom: calc(78px + env(safe-area-inset-bottom) + var(--consent-h, 0px)); z-index: var(--z-dropdown, 700);1176  display: inline-flex; align-items: center; gap: 7px; min-height: 42px; padding: 8px 14px 8px 12px;1177  border-radius: 999px; border: 0; background: var(--sk-text); color: #fff;1178  font: 600 13px var(--font-body); cursor: pointer; box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);1179  transition: transform 0.15s, opacity 0.2s;1180}1181.sk-ka-btn svg { color: var(--sk-accent); }1182.sk-ka-btn:active { transform: scale(0.97); }1183.sk-ka-btn.hide { opacity: 0; pointer-events: none; transform: translateY(8px); }1184@media (min-width: 1024px) { .sk-ka-btn { display: none; } }   /* desktop : bouton dans l'aside */1185.sk-ka-intro { display: flex; gap: 12px; align-items: flex-start; padding: 4px 0 12px; }1186.sk-ka-avatar { width: 38px; height: 38px; border-radius: 12px; background: var(--sk-text); color: var(--sk-accent); display: grid; place-items: center; flex: none; }1187.sk-ka-intro p { margin: 0; font-size: 13.5px; color: var(--sk-text-2); line-height: 1.45; }1188.sk-ka-ctx { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 12px; background: var(--sk-surface-2); border: 1px solid var(--sk-border); font-size: 12.5px; color: var(--sk-text-2); margin-bottom: 12px; }1189.sk-ka-ctx img { width: 44px; height: 34px; object-fit: cover; border-radius: 6px; flex: none; }1190.sk-ka-ctx b { color: var(--sk-text); display: block; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }1191.sk-ka-sugs { display: flex; flex-direction: column; gap: 6px; }1192.sk-ka-sug { display: flex; align-items: center; justify-content: space-between; gap: 10px; width: 100%; min-height: 46px; padding: 10px 14px; border-radius: 12px; border: 1px solid var(--sk-border); background: var(--sk-surface); font: 500 14px var(--font-body); color: var(--sk-text); cursor: pointer; text-align: left; transition: background 0.15s, border-color 0.15s; }1193.sk-ka-sug:hover { background: var(--sk-accent-soft); border-color: #f5c2c6; }1194.sk-ka-sug svg { color: var(--sk-muted); flex: none; }1195.sk-ka-in { display: flex; gap: 8px; align-items: center; }1196.sk-ka-in input { flex: 1; min-height: 46px; padding: 10px 14px; border-radius: 12px; border: 1px solid var(--sk-border); background: var(--sk-surface); font: 400 16px var(--font-body); color: var(--sk-text); }1197.sk-ka-in input:focus { outline: 2px solid var(--sk-accent); outline-offset: 1px; }1198.sk-ka-in button { width: 46px; height: 46px; border-radius: 12px; border: 0; background: var(--sk-accent); color: #fff; display: grid; place-items: center; cursor: pointer; flex: none; }1199.sk-ka-in button:disabled { opacity: 0.4; cursor: default; }12001201/* ---- aside desktop ---- */1202.sk-aside-card { display: flex; flex-direction: column; gap: 12px; }1203.sk-aside .sk-btn-primary { white-space: normal; text-align: center; line-height: 1.25; }1204.sk-aside .sk-ka-inline { justify-content: flex-start; }1205.sk-aside .sk-ka-inline svg { color: var(--sk-accent); }1206.sk-aside .sk-price { font-size: 32px; }1207.sk-aside-addr { font-size: 14px; color: var(--sk-text-2); line-height: 1.4; }1208.sk-aside-sep { border: 0; border-top: 1px solid var(--sk-border); margin: 4px 0; }1209.sk-aside-score { display: flex; align-items: center; gap: 12px; }1210.sk-aside-score .sk-score-ring { width: 56px; height: 56px; }1211.sk-aside-score .sk-score-ring svg { width: 56px; height: 56px; }1212.sk-aside-score .sk-score-val { font-size: 18px; }1213.sk-aside-score .sk-score-val small { display: none; }1214.sk-aside-score-txt { font-size: 13px; color: var(--sk-text-2); line-height: 1.4; }1215.sk-aside-score-txt b { display: block; color: var(--sk-text); font-size: 14px; }1216.sk-aside .sk-brief { gap: 8px; }1217.sk-aside .sk-brief-t { font-size: 13.5px; }1218.sk-aside .sk-brief-d { display: none; }1219.sk-aside-meta { font-size: 12px; color: var(--sk-muted); text-align: center; }12201221/* ---- source & méthodologie (pied de section) ---- */1222.sk-source { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--sk-border); font-size: 12px; color: var(--sk-muted); }1223.sk-source b { font-weight: 600; color: var(--sk-text-2); }1224.sk-source a, .sk-source button { color: var(--sk-text-2); background: none; border: 0; padding: 0; font: inherit; text-decoration: underline; text-underline-offset: 2px; text-decoration-color: var(--sk-border-2); cursor: pointer; }1225.sk-source a:hover, .sk-source button:hover { color: var(--sk-accent-deep); }1226.sk-fine { font-size: 12.5px; color: var(--sk-muted); line-height: 1.5; margin: 8px 0 0; }1227.sk-fine a { color: var(--sk-text-2); text-decoration: underline; text-underline-offset: 2px; }1228.sk-fine.meth { margin-top: 0; }12291230/* ---- états : squelettes, vides, erreurs ---- */1231.sk-skel { border-radius: 10px; background: linear-gradient(90deg, #ecece8 25%, #f4f4f1 50%, #ecece8 75%); background-size: 400% 100%; animation: sk-shimmer 1.3s infinite linear; }1232@keyframes sk-shimmer { from { background-position: 100% 0; } to { background-position: 0 0; } }1233.sk-skel-lines { display: flex; flex-direction: column; gap: 8px; }1234.sk-skel-lines .sk-skel { height: 12px; }1235.sk-skel-lines .sk-skel.short { width: 55%; }1236.sk-empty { padding: 14px; border-radius: 12px; background: var(--sk-surface-2); border: 1px dashed var(--sk-border-2); color: var(--sk-muted); font-size: 13.5px; text-align: center; line-height: 1.45; }1237.sk-error { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 12px 14px; border-radius: 12px; background: var(--sk-warning-soft); color: var(--sk-warning); font-size: 13.5px; }1238.sk-error button { border: 1px solid currentColor; background: transparent; color: inherit; border-radius: 999px; padding: 6px 12px; font: 600 12.5px var(--font-body); cursor: pointer; min-height: 36px; }1239.sk-page-error { max-width: 520px; margin: 60px auto; text-align: center; padding: 0 16px; }1240.sk-page-error h2 { font-family: var(--font-display); letter-spacing: -0.02em; }1241.sk-page-error p { color: var(--sk-text-2); }1242.sk-toast { position: fixed; left: 50%; bottom: calc(96px + env(safe-area-inset-bottom) + var(--consent-h, 0px)); transform: translateX(-50%); z-index: var(--z-toast, 950); background: var(--sk-text); color: #fff; font: 600 13px var(--font-body); padding: 10px 16px; border-radius: 999px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); animation: sk-up 0.25s ease; max-width: calc(100vw - 32px); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }12431244/* ---- prix / marché ---- */1245.sk-market-head { display: grid; grid-template-columns: 1fr auto; gap: 12px; align-items: start; }1246.sk-market-big { font-family: var(--font-display); font-weight: 700; font-size: 28px; letter-spacing: -0.03em; line-height: 1; }1247.sk-market-big small { font: 500 13px var(--font-body); color: var(--sk-muted); margin-left: 4px; letter-spacing: 0; }1248.sk-market-sub { font-size: 13px; color: var(--sk-text-2); margin-top: 4px; }1249.sk-histo { display: block; width: 100%; max-width: 520px; height: auto; margin: 12px 0 4px; }1250.sk-histo-bar { fill: #dedfd9; }1251.sk-histo-bar.on { fill: var(--sk-accent); }1252.sk-histo-lbl { font: 700 10px var(--font-body); fill: var(--sk-text); }1253.sk-histo-lbl.fv { fill: var(--sk-text-2); }1254.sk-histo-axis { font: 500 9.5px var(--font-body); fill: var(--sk-muted); }1255.sk-meta { display: flex; flex-wrap: wrap; gap: 4px 14px; font-size: 12.5px; color: var(--sk-text-2); margin: 6px 0 0; }1256.sk-meta b { color: var(--sk-text); }12571258/* jauge du registre des loyers */1259.sk-gauge { margin: 14px 0 6px; }1260.sk-gauge-lbls { display: flex; justify-content: space-between; font: 600 11px var(--font-body); color: var(--sk-muted); margin-bottom: 6px; }1261.sk-gauge-lbls .good { color: var(--sk-success); } .sk-gauge-lbls .bad { color: var(--sk-danger); }1262.sk-gauge svg { display: block; width: 100%; max-width: 520px; height: auto; overflow: visible; }1263.sk-gauge, .sk-gauge-lbls { max-width: 520px; }1264.sk-gauge-track { fill: var(--sk-surface-2); stroke: var(--sk-border); }1265.sk-gauge-box { fill: #ecece8; }1266.sk-gauge-med { stroke: var(--sk-text-2); stroke-width: 1.5; }1267.sk-gauge-me { fill: var(--sk-accent); stroke: #fff; stroke-width: 2.5; }1268.sk-gauge-txt { font: 600 10.5px var(--font-body); fill: var(--sk-muted); }1269.sk-gauge-txt.me { fill: var(--sk-text); font-weight: 700; }1270.sk-bars { display: block; width: 100%; max-width: 520px; height: auto; margin: 6px 0 0; }1271.sk-bar { fill: #dedfd9; }1272.sk-bar.on { fill: var(--sk-accent); }1273.sk-bar-val { font: 600 10px var(--font-body); fill: var(--sk-text-2); }1274.sk-bar-cat { font: 600 11px var(--font-body); fill: var(--sk-text-2); }1275.sk-bar-cat.on { fill: var(--sk-text); font-weight: 700; }1276.sk-bar-n { font: 500 9.5px var(--font-body); fill: var(--sk-muted); }1277.sk-bars-axe { stroke: var(--sk-border); }12781279/* ---- risques / environnement ---- */1280.sk-status { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }1281.sk-status-t { font-family: var(--font-display); font-weight: 700; font-size: 18px; letter-spacing: -0.02em; }1282.sk-status-d { font-size: 13.5px; color: var(--sk-text-2); margin: 8px 0 0; line-height: 1.5; }1283.sk-air { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin-top: 12px; }1284@media (max-width: 400px) { .sk-air { grid-template-columns: repeat(2, minmax(0, 1fr)); } }1285.sk-air-c { padding: 10px 12px; border-radius: 12px; border: 1px solid var(--sk-border); background: var(--sk-surface-2); min-width: 0; }1286.sk-air-p { font: 600 11.5px var(--font-body); color: var(--sk-muted); text-transform: uppercase; letter-spacing: 0.05em; }1287.sk-air-v { font-family: var(--font-display); font-weight: 700; font-size: 18px; letter-spacing: -0.02em; margin-top: 2px; font-variant-numeric: tabular-nums; white-space: nowrap; }1288.sk-air-v small { font: 500 11px var(--font-body); color: var(--sk-muted); margin-left: 3px; }1289.sk-air-s { font-size: 11.5px; margin-top: 3px; line-height: 1.3; }1290.sk-air-s.good { color: var(--sk-success); } .sk-air-s.warn { color: var(--sk-warning); } .sk-air-s.bad { color: var(--sk-danger); } .sk-air-s.neutral { color: var(--sk-muted); }12911292/* ---- coût réel ---- */1293.sk-cost { list-style: none; margin: 0; padding: 0; }1294.sk-cost li { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; padding: 8px 0; border-top: 1px solid var(--sk-border); font-size: 13.5px; }1295.sk-cost li:first-child { border-top: 0; }1296.sk-cost .n { color: var(--sk-text-2); min-width: 0; }1297.sk-cost .v { font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }1298.sk-cost .v.na { color: var(--sk-muted); font-weight: 500; }1299.sk-cost li.total { border-top: 1px solid var(--sk-border-2); margin-top: 4px; padding-top: 10px; font-size: 15px; }1300.sk-cost li.total .n, .sk-cost li.total .v { color: var(--sk-text); font-weight: 700; }1301.sk-cost li.annuel { font-size: 12.5px; color: var(--sk-muted); border-top: 0; padding-top: 0; }1302.sk-cost-note { font-size: 12.5px; color: var(--sk-text-2); margin: 8px 0 0; }13031304/* ---- dossier de l'immeuble (accordéons) ---- */1305.sk-tl { list-style: none; margin: 4px 0 0; padding: 0 0 0 12px; border-left: 2px solid var(--sk-border); display: flex; flex-direction: column; gap: 8px; }1306.sk-tl li { position: relative; font-size: 13px; color: var(--sk-text-2); }1307.sk-tl li::before { content: ""; position: absolute; left: -17.5px; top: 5px; width: 8px; height: 8px; border-radius: 50%; background: var(--sk-surface); border: 2px solid var(--sk-border-2); }1308.sk-tl li.prix::before { border-color: var(--sk-accent); }1309.sk-tl li.disparition::before { border-color: var(--sk-danger); }1310.sk-tl li.reapparition::before { border-color: var(--sk-success); }1311.sk-tl-date { display: inline-block; min-width: 84px; font: 600 11.5px var(--font-body); color: var(--sk-muted); }1312.sk-star { font-family: var(--font-display); font-weight: 700; font-size: 20px; color: var(--sk-text); display: inline-flex; align-items: center; gap: 5px; }1313.sk-star svg { color: #e0a800; }1314.sk-quote { margin: 10px 0 0; padding: 8px 12px; font-size: 13px; color: var(--sk-text-2); border-left: 3px solid var(--sk-border-2); background: var(--sk-surface-2); border-radius: 8px; }1315.sk-quote footer { margin-top: 4px; font-size: 11.5px; color: var(--sk-muted); }1316.sk-tags { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 4px; }1317.sk-tag { font: 600 11.5px var(--font-body); padding: 2px 8px; border-radius: 999px; background: var(--sk-danger-soft); color: var(--sk-danger); }1318.sk-tag.n { background: var(--sk-surface-2); color: var(--sk-text-2); border: 1px solid var(--sk-border); }13191320/* ---- KA Scores (cercles compacts) ---- */1321.sk-ks { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 6px; }1322@media (max-width: 400px) { .sk-ks { grid-template-columns: repeat(3, minmax(0, 1fr)); } }1323.sk-ks-c { text-align: center; padding: 8px 4px; border-radius: 12px; border: 1px solid var(--sk-border); background: var(--sk-surface-2); min-width: 0; }1324.sk-ks-c svg { width: 52px; height: 52px; transform: rotate(-90deg); }1325.sk-ks-c .bg { fill: none; stroke: #e6e6e1; stroke-width: 5; }1326.sk-ks-c .arc { fill: none; stroke: var(--sk-text); stroke-width: 5; stroke-linecap: round; }1327.sk-ks-c.haut .arc { stroke: var(--sk-success); } .sk-ks-c.bon .arc { stroke: var(--sk-text); } .sk-ks-c.moyen .arc { stroke: var(--sk-warning); } .sk-ks-c.bas .arc { stroke: var(--sk-danger); }1328.sk-ks-wrap { position: relative; width: 52px; height: 52px; margin: 0 auto; }1329.sk-ks-v { position: absolute; inset: 0; display: grid; place-items: center; font: 700 15px var(--font-display); letter-spacing: -0.02em; }1330.sk-ks-n { font: 600 11.5px var(--font-body); margin-top: 6px; color: var(--sk-text); }1331.sk-ks-l { font-size: 10.5px; color: var(--sk-muted); margin-top: 1px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }1332.sk-ks-detail { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 8px 18px; }1333.sk-ks-detail h4 { margin: 8px 0 4px; font: 700 12.5px var(--font-body); color: var(--sk-text); }1334.sk-ks-detail ul { margin: 0; padding-left: 16px; font-size: 13px; color: var(--sk-text-2); }1335.sk-ks-detail li { margin: 2px 0; }13361337/* ---- fin de fiche ---- */1338.sk-end { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }1339@media (min-width: 640px) { .sk-end { grid-template-columns: repeat(4, minmax(0, 1fr)); } }1340.sk-end a, .sk-end button { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; padding: 12px; border-radius: 12px; border: 1px solid var(--sk-border); background: var(--sk-surface); color: var(--sk-text); font: 600 13px var(--font-body); cursor: pointer; text-align: left; min-height: 64px; }1341.sk-end a:hover, .sk-end button:hover { background: var(--sk-surface-2); }1342.sk-end svg { color: var(--sk-accent-deep); }1343.sk-end small { font-weight: 500; color: var(--sk-muted); font-size: 12px; }1344.sk-sources { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }1345.sk-sources li { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 4px 12px; padding: 9px 0; border-top: 1px solid var(--sk-border); font-size: 13px; align-items: center; }1346.sk-sources li:first-child { border-top: 0; }1347.sk-sources .n { font-weight: 600; grid-column: 1; grid-row: 1; }1348.sk-sources .r { font-size: 12.5px; color: var(--sk-text-2); grid-column: 1; grid-row: 2; }1349.sk-sources .d { font-size: 12px; color: var(--sk-muted); grid-column: 2; grid-row: 1 / span 2; text-align: right; white-space: nowrap; align-self: start; }1350.sk-sources a { color: var(--sk-text-2); text-decoration: underline; text-underline-offset: 2px; }135113521353/* ---- ajouts Immo-Ka : héro, galerie, formulaire, tableaux, rôle ---- */1354.sk-kicker { font-family: var(--font-mono); font-size: 11px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.12em; color: var(--sk-muted); margin-bottom: 4px; }1355.sk-gallery-empty { display: grid; place-items: center; aspect-ratio: 4 / 3; }1356@media (min-width: 768px) { .sk-gallery-empty { aspect-ratio: 16 / 9; } }1357.sk-gallery-empty .type-fallback { position: static; width: 100%; height: 100%; }1358.sk-gallery-caption {1359  position: absolute; left: 12px; top: 12px; z-index: 2; max-width: calc(100% - 24px);1360  background: rgba(20, 24, 20, 0.62); color: #fff; font: 500 12px var(--font-body);1361  padding: 4px 10px; border-radius: 999px; backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);1362  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;1363}1364.sk-subtitle { margin: 14px 0 8px !important; font-weight: 600 !important; color: var(--sk-text-2) !important; font-size: 13px !important; }1365.sk-subtitle small { font-weight: 500; color: var(--sk-muted); }1366.sk-more .flip { transform: rotate(180deg); }1367.sk-acc-ico { vertical-align: -2px; margin-right: 8px; color: var(--sk-muted); }1368.sk-role { margin-top: 14px; }1369.sk-tl li.baisse::before { border-color: var(--sk-success); }1370.sk-tl li.hausse::before { border-color: var(--sk-warning); }1371/* formulaire du calculateur */1372.sk-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }1373@media (min-width: 640px) { .sk-form { grid-template-columns: repeat(4, minmax(0, 1fr)); } }1374.sk-form label { display: flex; flex-direction: column; gap: 4px; min-width: 0; }1375.sk-form label span { font-size: 11.5px; color: var(--sk-muted); font-weight: 600; }1376.sk-form input, .sk-form select {1377  min-height: 42px; padding: 8px 10px; border-radius: 10px; border: 1px solid var(--sk-border); background: var(--sk-surface);1378  font: 500 15px var(--font-body); color: var(--sk-text); width: 100%; min-width: 0; appearance: none; -webkit-appearance: none;1379}1380.sk-form select { background-image: linear-gradient(45deg, transparent 50%, var(--sk-muted) 50%), linear-gradient(135deg, var(--sk-muted) 50%, transparent 50%); background-position: calc(100% - 16px) 50%, calc(100% - 11px) 50%; background-size: 5px 5px; background-repeat: no-repeat; padding-right: 28px; }1381.sk-form input:focus, .sk-form select:focus { outline: 2px solid var(--sk-accent); outline-offset: 1px; }1382/* tableaux (pièces, banques, amortissement) */1383.sk-table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; margin: 0 -4px; padding: 0 4px; }1384.sk-table { width: 100%; border-collapse: collapse; font-size: 13px; }1385.sk-table th { text-align: left; font: 600 11.5px var(--font-body); color: var(--sk-muted); text-transform: uppercase; letter-spacing: 0.04em; padding: 6px 8px; border-bottom: 1px solid var(--sk-border); white-space: nowrap; }1386.sk-table td { padding: 8px 8px; border-bottom: 1px solid var(--sk-border); vertical-align: top; color: var(--sk-text-2); }1387.sk-table td:first-child { color: var(--sk-text); font-weight: 500; }1388.sk-table tr:last-child td { border-bottom: 0; }1389.sk-table a { color: var(--sk-text-2); text-decoration: underline; text-underline-offset: 2px; }1390/* aside : courtier */1391.sk-aside-broker { display: flex; flex-direction: column; gap: 2px; font-size: 13.5px; color: var(--sk-text-2); }1392.sk-aside-broker-k { font: 600 11px var(--font-body); text-transform: uppercase; letter-spacing: 0.06em; color: var(--sk-muted); }1393.sk-aside-broker b { color: var(--sk-text); }1394.sk-aside-broker a { display: inline-flex; align-items: center; gap: 6px; color: var(--sk-text-2); }1395/* jauge Vrai-Prix : libellés */1396.sk-gauge-lbls { margin-top: 4px; margin-bottom: 0; }1397.sk-kv .v a { color: var(--sk-text-2); text-decoration: underline; text-underline-offset: 2px; }1398.sk-kv .v { min-width: 0; overflow-wrap: anywhere; }1399140014011402body.sk-fiche-page .header-inner{padding-left:16px;padding-right:16px}1403@media(min-width:641px){body.sk-fiche-page .header-inner{padding-left:24px;padding-right:24px}}1404.hdr-actions .fav-btn{position:static;width:40px;height:40px;border-radius:999px;border:1px solid var(--sk-border);background:var(--sk-surface);color:var(--sk-text);box-shadow:none;display:inline-grid;place-items:center}1405.hdr-actions .fav-btn.on{color:var(--sk-accent);border-color:var(--sk-accent);background:var(--sk-accent-soft)}1406.hdr-actions .fav-btn svg{width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:2}1407.hdr-actions .fav-btn.on svg{fill:currentColor}1408@media(max-width:980px){body.sk-fiche-page .header .nav{display:none}}14091410/* ---- ajouts Sorti·Ka : image, accordéons natifs, carte, statut ---- */1411.sk-image{position:relative;border-radius:var(--sk-r-lg);overflow:hidden;background:linear-gradient(135deg,var(--sk-accent-soft),var(--sk-surface-2));aspect-ratio:16/9}1412@media(max-width:640px){.sk-image{aspect-ratio:4/3}}1413.sk-image img{width:100%;height:100%;object-fit:cover;display:block}1414.sk-image.noimg{aspect-ratio:16/6;display:grid;place-items:center;color:var(--sk-accent-deep);font-family:var(--font-display);font-weight:700;font-size:20px}1415.sk-capsule.date{background:var(--sk-text);color:#fff}1416.sk-details{border-top:1px solid var(--sk-border)}1417.sk-details:first-child{border-top:0}1418.sk-details summary{list-style:none;display:flex;align-items:center;justify-content:space-between;gap:10px;min-height:44px;padding:8px 0;font:600 13.5px var(--font-body);color:var(--sk-text-2);cursor:pointer}1419.sk-details summary::-webkit-details-marker{display:none}1420.sk-details summary::after{content:"";width:8px;height:8px;border-right:2px solid var(--sk-muted);border-bottom:2px solid var(--sk-muted);transform:rotate(45deg);transition:transform .2s;flex:none;margin-right:6px}1421.sk-details[open] summary::after{transform:rotate(-135deg)}1422.sk-details .sk-acc-inner{padding:0 0 14px;font-size:13.5px;color:var(--sk-text-2);line-height:1.5}1423.sk-details .sk-acc-inner p{margin:0 0 8px}1424.sk-map{height:300px}1425@media(min-width:768px){.sk-map{height:380px}}1426.sk-map .leaflet-container{position:absolute;inset:0;font:inherit}1427.sk-fiche .grid{margin-top:0}1428.sk-sheet .sk-ka-sugs{margin-top:0}1429.sk-sheet-x{font-size:18px;line-height:1}1430.sk-fiche h3[id]{scroll-margin-top:calc(var(--sk-header-h) + var(--sk-nav-h) + 10px)}1431.sk-status{margin-top:6px}14321433/* ---- accessibilité mouvement réduit ---- */1434@media (prefers-reduced-motion: reduce) {1435  .sk-sheet, .sk-sheet-backdrop, .sk-toast, .sk-kpi.anim .sk-kpi-v { animation: none; }1436  .sk-score-ring .arc, .sk-acc-body, .sk-cta-bar { transition: none; }1437}14381439/* ---- utilitaire a11y ---- */1440.visually-hidden { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }14411442</style>1443</head>1444<body>14451446<div id="ssr"><!--SSR_BODY--></div>14471448<div class="ticker"><div class="ticker-track">1449  <span>Concerts</span><span>Festivals</span><span>Humour</span><span>Théâtre</span>1450  <span>Expositions</span><span>Danse</span><span>Sport</span><span>Famille</span>1451  <span>Plein air</span><span>17 régions du Québec</span>1452  <span>Concerts</span><span>Festivals</span><span>Humour</span><span>Théâtre</span>1453  <span>Expositions</span><span>Danse</span><span>Sport</span><span>Famille</span>1454  <span>Plein air</span><span>17 régions du Québec</span>1455</div></div>14561457<header class="header">1458  <div class="container header-inner">1459    <a class="brand" data-nav href="/">Sorti<span class="ka">·Ka</span></a>1460    <a class="gk-badge" href="https://www.groupe-ka.com" target="_blank" rel="noopener noreferrer"1461       aria-label="Un service Groupe KA — visiter le portail de l'écosystème">1462      Un service <b>Groupe<span class="ka">KA</span></b></a>1463    <span class="brand-tag">Sorties &amp; événements — Québec</span>1464    <nav class="nav">1465      <a data-nav href="/">Sorties</a>1466      <a data-nav href="/favoris" title="Mes favoris — Mon univers Ka">♥ Favoris</a>1467      <a data-nav href="/stats">Stats</a>1468      <a data-nav href="/contact">Contact</a>1469    </nav>1470    <span id="auth"></span>1471    <button id="ka-mnav-btn" class="ka-mnav-btn" aria-label="Ouvrir le menu"1472            aria-expanded="false" aria-controls="ka-mnav">1473      <span></span><span></span><span></span>1474    </button>1475  </div>1476</header>14771478<!-- KA Nav v2 : panneau mobile plein écran (fixed inset:0 — visible peu importe le scroll) -->1479<div id="ka-mnav" class="ka-mnav" role="dialog" aria-modal="true" aria-label="Menu">1480  <div class="ka-mnav-top">1481    <a class="brand" data-nav href="/">Sorti<span class="ka">·Ka</span></a>1482    <button class="ka-mnav-close" data-ka-mnav-close aria-label="Fermer le menu">✕</button>1483  </div>1484  <nav class="ka-mnav-links">1485    <a data-nav href="/"><b>Sorties</b></a>1486    <a href="/#sources"><b>Sources</b></a>1487    <a data-nav href="/favoris"><b>♥ Favoris</b></a>1488    <a data-nav href="/stats"><b>Stats</b></a>1489    <a data-nav href="/contact"><b>Contact</b></a>1490    <a href="/docs" target="_blank" rel="noopener"><b>API</b></a>1491  </nav>1492  <div class="ka-mnav-foot">1493    <a class="gk-badge" href="https://www.groupe-ka.com" target="_blank" rel="noopener noreferrer"1494       aria-label="Un service Groupe KA — visiter le portail de l'écosystème">1495      Un service <b>Groupe<span class="ka">KA</span></b></a>1496    <div class="ka-mnav-sec">1497      <a href="/doc/">Documentation</a>1498      <a href="https://www.groupe-ka.com/conditions" target="_blank" rel="noopener noreferrer">Conditions</a>1499      <a href="https://www.groupe-ka.com/confidentialite" target="_blank" rel="noopener noreferrer">Confidentialité</a>1500    </div>1501  </div>1502</div>15031504<!-- ======== VUE LISTE : accueil éditorial + résultats Liste|Carte ======== -->1505<main id="view-list">1506  <section class="hero">1507    <div class="hero-mosaic" id="hero-mosaic" aria-hidden="true"></div>1508    <div class="container">1509      <span class="kicker">Agrégateur indépendant · mise à jour continue</span>1510      <h1>Toutes les sorties du Québec, <span class="hl">un seul endroit.</span></h1>1511      <p class="hero-cats">concerts <i>·</i> festivals <i>·</i> humour <i>·</i> théâtre1512        <i>·</i> expos <i>·</i> famille <i>·</i> sport</p>1513      <p class="hero-stats" id="stats"></p>1514    </div>1515  </section>15161517  <section class="container searchzone">1518    <div class="searchbar" role="search">1519      <svg class="sico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"1520           stroke-linecap="round" aria-hidden="true">1521        <circle cx="11" cy="11" r="7"/><path d="m20 20-3.8-3.8"/></svg>1522      <input type="search" id="q" placeholder="Artiste, événement, salle ou ville…"1523             autocomplete="off" aria-label="Rechercher une sortie">1524      <button class="sclear hidden" id="q-clear" aria-label="Effacer la recherche">✕</button>1525    </div>1526    <div class="quick" id="quick"></div>1527    <div class="days" id="days" aria-label="Naviguer par jour"></div>1528  </section>15291530  <!-- accueil éditorial : sections composées par renderHome() -->1531  <section class="container" id="home"></section>15321533  <!-- résultats de recherche / filtres : liste ou carte -->1534  <section class="container hidden" id="results">1535    <div class="rtool">1536      <div class="meta-line"><span id="count">Chargement…</span></div>1537      <div class="vtoggle" role="group" aria-label="Affichage">1538        <button type="button" id="v-list" class="on" aria-pressed="true">1539          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4"1540               stroke-linecap="round" aria-hidden="true">1541            <path d="M4 6h16M4 12h16M4 18h16"/></svg>Liste</button>1542        <button type="button" id="v-map" aria-pressed="false">1543          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"1544               stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">1545            <path d="M12 21s-7-5.5-7-11a7 7 0 0 1 14 0c0 5.5-7 11-7 11z"/>1546            <circle cx="12" cy="10" r="2.6"/></svg>Carte</button>1547      </div>1548    </div>1549    <div class="afilters" id="afilters"></div>1550    <section class="grid" id="grid"></section>1551    <button class="more btn btn-primary" id="more" hidden>Afficher plus d'événements</button>1552    <div class="mapwrap hidden" id="mapwrap">1553      <div id="map"></div>1554      <div class="map-load" id="map-load">Chargement de la carte…</div>1555      <span class="map-note hidden" id="map-note"></span>1556    </div>1557  </section>15581559  <section class="sources" id="sources">1560    <div class="container">1561      <span class="kicker">Index fidèle · attribution systématique</span>1562      <h2>Les sources</h2>1563      <p class="lede2">Rien d'inventé : chaque événement est attribué à sa source et lié1564         vers la fiche originale. Billetteries au robots.txt permissif et1565         <b>données ouvertes officielles</b> (licences Creative Commons).</p>1566      <div class="src-grid" id="src-grid"></div>1567    </div>1568  </section>1569</main>15701571<!-- ======== PANNEAU DE FILTRES (bottom sheet mobile / modale desktop) ======== -->1572<div class="sheet-ov" id="sheet-ov"></div>1573<div class="sheet" id="sheet" role="dialog" aria-modal="true" aria-label="Filtres">1574  <div class="sheet-grab" aria-hidden="true"></div>1575  <div class="sheet-head">1576    <h2>Filtres</h2>1577    <button type="button" class="sheet-x" id="sheet-x" aria-label="Fermer les filtres">✕</button>1578  </div>1579  <div class="sheet-body">1580    <div class="sheet-sec">1581      <span class="klabel">Quand</span>1582      <div class="sheet-chips" id="sh-when"></div>1583      <div class="sheet-dates">1584        <input class="input" type="date" id="sh-from" aria-label="Du">1585        <span class="klabel">au</span>1586        <input class="input" type="date" id="sh-to" aria-label="Au">1587      </div>1588    </div>1589    <div class="sheet-sec">1590      <span class="klabel">Région</span>1591      <select class="select" id="region"><option value="">Toutes les régions</option></select>1592    </div>1593    <div class="sheet-sec">1594      <span class="klabel">Catégories</span>1595      <div class="sheet-chips" id="cats"></div>1596    </div>1597    <div class="sheet-sec">1598      <span class="klabel">Prix</span>1599      <div class="sheet-chips">1600        <button type="button" class="chip free" id="chip-free">✦ Gratuit seulement</button>1601      </div>1602    </div>1603  </div>1604  <div class="sheet-foot">1605    <button type="button" class="btn btn-ghost" id="sh-reset">Réinitialiser</button>1606    <button type="button" class="btn btn-cta" id="sh-apply">Voir les sorties</button>1607  </div>1608</div>16091610<div class="toast" id="toast" role="status" aria-live="polite"></div>16111612<!-- ======== VUE FICHE ÉVÉNEMENT ======== -->1613<main id="view-detail" class="hidden sk-fiche"><div id="sk-root"></div></main>16141615<!-- ======== VUE FAVORIS (/favoris) — ♥ « Mon univers Ka » (hub groupe-ka.com) ======== -->1616<main id="view-favoris" class="hidden">1617  <section class="hero container">1618    <span class="kicker">Mon univers Ka · synchronisés sur toutes les plateformes Groupe KA</span>1619    <h1>Mes <span class="hl">favoris</span></h1>1620    <p class="lede" id="fav-lede">Vos sorties et événements favoris, rattachés à votre compte KA ID.</p>1621  </section>1622  <section class="container">1623    <div class="meta-line"><span id="fav-count"></span></div>1624    <section class="grid" id="fav-grid"></section>1625  </section>1626</main>16271628<!-- ======== VUE STATS (/stats) — module ka-stats (SPEC.md), vanilla + SVG ======== -->1629<main id="view-stats" class="hidden">1630  <div class="container stats-page">1631    <div class="st-head">1632      <div>1633        <span class="kicker">Tableau de bord · données réelles</span>1634        <h1>Statistiques <span class="hl">Sorti·Ka</span></h1>1635        <p class="st-fresh" id="st-fresh"></p>1636      </div>1637      <div class="st-pdf">1638        <button type="button" class="btn btn-primary" id="st-pdf-full">⬇ Rapport PDF complet</button>1639        <span class="st-pdf-menu">1640          <button type="button" class="btn btn-ghost" id="st-pdf-more"1641                  aria-haspopup="true" aria-expanded="false">Autres rapports ▾</button>1642          <span class="st-pdf-drop hidden" id="st-pdf-drop" role="menu">1643            <button type="button" data-mode="synthese">Synthèse exécutive <small>2-3 pages</small></button>1644            <button type="button" data-mode="tendances">Tendances &amp; évolution</button>1645            <button type="button" data-mode="repartitions">Répartitions &amp; géographie</button>1646            <button type="button" data-mode="donnees">Données détaillées <small>tableaux longs</small></button>1647          </span>1648        </span>1649        <button type="button" class="btn btn-ghost" id="st-custom-btn">🛠 Rapport personnalisé</button>1650      </div>1651    </div>1652    <div class="st-kpis" id="st-kpis"></div>1653    <div class="st-periods" id="st-periods"></div>1654    <div class="st-gauges" id="st-gauges"></div>1655    <div class="st-sec" id="st-series"></div>1656    <div class="st-sec" id="st-multi"></div>1657    <div class="st-2col" id="st-breaks"></div>1658    <div class="st-2col" id="st-geosrc"></div>1659    <div class="st-sec" id="st-heat"></div>1660    <div class="st-sec" id="st-hourly"></div>1661    <div class="st-sec" id="st-tables"></div>1662    <section id="st-records-wrap" class="hidden" style="margin-top:34px">1663      <span class="kicker">Records &amp; faits marquants</span>1664      <div class="st-records" id="st-records"></div>1665    </section>1666  </div>1667</main>16681669<!-- ======== VUE CONTACT (/contact) — contenu : src/ka/ecosystem.json ======== -->1670<main id="view-contact" class="hidden">1671  <div class="container contact-page">1672    <span class="kicker">Contact · Groupe KA</span>1673    <h1>Écrire au <span class="hl">Groupe KA</span></h1>1674    <p class="lede">Sorti·Ka est un service du Groupe KA, holding québécois d'agrégateurs de1675       produits et services entièrement automatisés. Trois adresses, selon votre besoin :</p>16761677    <div class="contact-grid">1678      <div class="contact-card gk-card">1679        <span class="klabel">Projets, partenariats &amp; données</span>1680        <a class="mail" href="mailto:contact@groupe-ka.com">contact@groupe-ka.com</a>1681      </div>1682      <div class="contact-card gk-card">1683        <span class="klabel">Médias &amp; questions générales</span>1684        <a class="mail" href="mailto:info@groupe-ka.com">info@groupe-ka.com</a>1685      </div>1686      <div class="contact-card gk-card">1687        <span class="klabel">Légal, vie privée &amp; Loi 25</span>1688        <a class="mail" href="mailto:admin@groupe-ka.com">admin@groupe-ka.com</a>1689      </div>1690    </div>16911692    <p class="contact-note"><b>Groupe KA est un agrégateur de contenu :</b> nous ne vendons rien,1693       ne louons rien et ne sommes partie à aucune transaction. Pour les billets, contactez la1694       billetterie ou l'organisateur indiqué sur chaque fiche.</p>1695    <p class="contact-note">La création de compte <b>KA ID</b> se fait sur le hub1696       <a href="https://www.groupe-ka.com/connexion" target="_blank" rel="noopener noreferrer"1697          style="color:var(--accent-deep);font-weight:700">groupe-ka.com/connexion</a> ;1698       chaque site de l'écosystème délègue sa connexion au hub.</p>16991700    <div class="contact-sites">1701      <span class="kicker">L'écosystème ·Ka</span>1702      <ul>1703        <li><a href="https://www.groupe-ka.com" target="_blank" rel="noopener noreferrer">Groupe KA<small>le portail</small></a></li>1704        <li><a href="https://www.trouve-ka.com" target="_blank" rel="noopener noreferrer">Trouve·Ka<small>recherche</small></a></li>1705        <li><a href="https://www.lou-ka.com" target="_blank" rel="noopener noreferrer">Lou·Ka<small>logements</small></a></li>1706        <li><a href="https://www.immo-ka.com" target="_blank" rel="noopener noreferrer">Immo·Ka<small>propriétés</small></a></li>1707        <li><a href="https://www.vrai-prix.com" target="_blank" rel="noopener noreferrer">Vrai-Prix<small>valeur immo</small></a></li>1708        <li><a href="https://www.auto-ka.com" target="_blank" rel="noopener noreferrer">Auto·Ka<small>autos usagées</small></a></li>1709        <li><a href="https://www.fabri-ka.com" target="_blank" rel="noopener noreferrer">Fabri·Ka<small>fait au Québec</small></a></li>1710        <li><a href="https://www.food-ka.com" target="_blank" rel="noopener noreferrer">Food·Ka<small>épicerie</small></a></li>1711        <li><a href="https://www.resto-ka.com" target="_blank" rel="noopener noreferrer">Resto·Ka<small>restos</small></a></li>1712        <li><a href="https://www.crea-ka.com" target="_blank" rel="noopener noreferrer">Créa·Ka<small>créateurs</small></a></li>1713        <li><a href="https://www.api-ka.com" target="_blank" rel="noopener noreferrer">API·Ka<small>données API</small></a></li>1714        <li><a href="https://www.job-ka.com" target="_blank" rel="noopener noreferrer">Job·Ka<small>emplois</small></a></li>1715        <li><a href="https://www.ka-stats.com" target="_blank" rel="noopener noreferrer">Ka·Stats<small>statistiques</small></a></li>1716      </ul>1717    </div>1718  </div>1719</main>17201721<!-- ======== FOOTER COMMUN GROUPE KA (ka-ui/KaFooter, siteId="sorti-ka") ======== -->1722<footer class="ka-footer" id="contact">1723  <div class="container">1724    <a class="wordmark" href="https://www.groupe-ka.com" target="_blank" rel="noopener noreferrer"1725       aria-label="Groupe KA — le portail de l'écosystème">Groupe <span class="ka">KA</span></a>1726    <p class="desc">Holding québécois d'agrégateurs de produits et services entièrement automatisés.1727       Des centaines de connecteurs lisent les sites à la source, normalisent la donnée et se1728       resynchronisent seuls. <b style="color:var(--paper)">Sorti·Ka</b> — Toutes les sorties,1729       dans les 17 régions — est un service Groupe KA.</p>1730    <p class="notice"><b>Groupe KA est un agrégateur de contenu : nous ne vendons rien,1731       ne louons rien et ne sommes partie à aucune transaction.</b></p>1732    <ul class="sites">1733      <li><a href="https://www.groupe-ka.com" target="_blank" rel="noopener noreferrer">Groupe KA</a></li>1734      <li><a href="https://www.trouve-ka.com" target="_blank" rel="noopener noreferrer">Trouve·Ka</a></li>1735      <li><a href="https://www.lou-ka.com" target="_blank" rel="noopener noreferrer">Lou·Ka</a></li>1736      <li><a href="https://www.immo-ka.com" target="_blank" rel="noopener noreferrer">Immo·Ka</a></li>1737      <li><a href="https://www.vrai-prix.com" target="_blank" rel="noopener noreferrer">Vrai-Prix</a></li>1738      <li><a href="https://www.auto-ka.com" target="_blank" rel="noopener noreferrer">Auto·Ka</a></li>1739      <li><a href="https://www.fabri-ka.com" target="_blank" rel="noopener noreferrer">Fabri·Ka</a></li>1740      <li><a href="https://www.food-ka.com" target="_blank" rel="noopener noreferrer">Food·Ka</a></li>1741      <li><a href="https://www.resto-ka.com" target="_blank" rel="noopener noreferrer">Resto·Ka</a></li>1742      <li><a href="https://www.crea-ka.com" target="_blank" rel="noopener noreferrer">Créa·Ka</a></li>1743      <li><a href="https://www.api-ka.com" target="_blank" rel="noopener noreferrer">API·Ka</a></li>1744      <li><a href="https://www.job-ka.com" target="_blank" rel="noopener noreferrer">Job·Ka</a></li>1745      <li><a href="https://www.ka-stats.com" target="_blank" rel="noopener noreferrer">Ka·Stats</a></li>1746      <li><a href="https://www.valoplex.com" target="_blank" rel="noopener noreferrer">ValoPlex</a></li>1747      <li><a href="https://www.ka2.bot" target="_blank" rel="noopener noreferrer">Ka2</a></li>1748      <li><a href="https://www.ka4.bot" target="_blank" rel="noopener noreferrer">Ka4</a></li>1749      <li><a href="https://www.ka6.bot" target="_blank" rel="noopener noreferrer">Ka6</a></li>1750    </ul>1751    <div class="contacts">1752      <p style="margin:0"><a href="mailto:contact@groupe-ka.com">contact@groupe-ka.com</a>1753        <span>Projets, partenariats &amp; données</span></p>1754      <p style="margin:0"><a href="mailto:info@groupe-ka.com">info@groupe-ka.com</a>1755        <span>Médias &amp; questions générales</span></p>1756      <p style="margin:0"><a href="mailto:admin@groupe-ka.com">admin@groupe-ka.com</a>1757        <span>Légal, vie privée &amp; Loi 25</span></p>1758    </div>1759    <p class="legal">Données : SIT Québec (MTO, CC 4.0) · Ville de Montréal (CC BY 4.0) ·1760       Ville de Laval (CC 4.0 &amp; laval.ca) · Villes de Québec (Québec animée), Sherbrooke,1761       Longueuil, Brossard &amp; Saguenay (Sorties Saguenay) · Tourisme Cantons-de-l'Est,1762       Outaouais, Lanaudière &amp; Bas-Saint-Laurent · Place des Arts · LHJMQ ·1763       Le point de vente · evenko · Atuvu.ca · La Vitrine · Eventbrite · Ticketmaster ·1764       Bandsintown · salles officielles · Répertoire des municipalités (MAMH) ·1765       lieux : © contributeurs OpenStreetMap (ODbL).</p>1766    <p class="legal">© 2026 Groupe KA — Simon-Pierre Boucher1767      · <a data-nav href="/contact">Contact</a>1768      · <a href="/doc/">Documentation</a>1769      · <a href="https://www.groupe-ka.com/conditions" target="_blank" rel="noopener noreferrer">Conditions d'utilisation</a>1770      · <a href="https://www.groupe-ka.com/confidentialite" target="_blank" rel="noopener noreferrer">Politique de confidentialité</a>1771      · <a href="https://www.groupe-ka.com/loi-25" target="_blank" rel="noopener noreferrer">Protection des renseignements personnels (Loi 25)</a>1772      · <a href="https://www.groupe-ka.com/bots" target="_blank" rel="noopener noreferrer">Transparence des robots d'indexation</a></p>1773  </div>1774</footer>17751776<script>1777/* Sorti-Ka — frontend + routeur. Auteur : Simon-Pierre Boucher <contact@spboucher.ai> */1778"use strict";1779const $ = id => document.getElementById(id);1780const state = { q:"", region:"", category:"", free:false, from:"", to:"", near:"",1781                view:"list", offset:0, limit:30, total:0 };17821783const CAT_LABELS = {1784  "festival":"Festivals","musique":"Musique","arts-scene":"Arts de la scène",1785  "exposition-musee":"Expos & musées","cinema":"Cinéma","sport":"Sport",1786  "plein-air":"Plein air","famille":"Famille","gastronomie":"Gastronomie",1787  "marche-foire":"Marchés & foires","conference":"Conférences & ateliers",1788  "communautaire":"Communautaire","patrimoine":"Patrimoine","autre":"Autre"1789};1790const SRC_LABELS = {1791  "sitq":"SIT Québec — Ministère du Tourisme","montreal":"Ville de Montréal",1792  "laval":"Ville de Laval","lepointdevente":"Le point de vente",1793  "evenko":"evenko","atuvu":"Atuvu.ca","lavitrine":"La Vitrine","sherbrooke":"Ville de Sherbrooke",1794  "brossard":"Ville de Brossard","ticketmaster":"Ticketmaster","bandsintown":"Bandsintown",1795  "eventbrite":"Eventbrite","longueuil":"Ville de Longueuil",1796  "cantonsdelest":"Tourisme Cantons-de-l'Est",1797  "lavalweb":"Ville de Laval (laval.ca)","outaouais":"Tourisme Outaouais",1798  "lanaudiere":"Tourisme Lanaudière","saguenaysorties":"Sorties Saguenay (Ville de Saguenay)",1799  "bassaintlaurent":"Tourisme Bas-Saint-Laurent","quebecanimee":"Québec animée (Ville de Québec)",1800  "placedesarts":"Place des Arts","lhjmq":"LHJMQ",1801  "sallesjsonld":"Site officiel de la salle"1802};1803const MONTHS = ["jan","fév","mar","avr","mai","juin","juil","août","sep","oct","nov","déc"];1804const MONTHS_FULL = ["janvier","février","mars","avril","mai","juin","juillet",1805  "août","septembre","octobre","novembre","décembre"];18061807function fmtDate(iso, full=false){1808  if(!iso) return "Date à confirmer";1809  const [y,m,d] = iso.split("-").map(Number);1810  return `${d} ${(full?MONTHS_FULL:MONTHS)[m-1]} ${y}`;1811}1812function fmtTime(t){1813  if(!t) return "";1814  const [h,m] = t.split(":");1815  return `${Number(h)} h ${m}`;1816}1817function dateBadge(ev, full=false){1818  if(!ev.start_date) return "Date à confirmer";1819  if(ev.end_date && ev.end_date !== ev.start_date)1820    return `${fmtDate(ev.start_date, full)} → ${fmtDate(ev.end_date, full)}`;1821  if(ev.start_time === "00:00")                 /* minuit = suspect, jamais affiché tel quel */1822    return `${fmtDate(ev.start_date, full)} · heure non confirmée`;1823  return fmtDate(ev.start_date, full)1824    + (ev.start_time ? ` · ${fmtTime(ev.start_time)}`1825       + (ev.end_time && ev.end_time !== ev.start_time ? ` – ${fmtTime(ev.end_time)}` : "") : "");1826}1827function esc(s){ const d=document.createElement("div"); d.textContent=s||""; return d.innerHTML; }1828const STATUS_LABELS = {cancelled:"Annulé", postponed:"Reporté", soldout:"Complet"};1829function statusBadge(ev){1830  if(!ev.status || !STATUS_LABELS[ev.status]) return "";1831  return `<span class="badge-status badge-status--${esc(ev.status)}">${STATUS_LABELS[ev.status]}</span>`;1832}1833function money(v){1834  return `${v.toFixed(2).replace(".",",").replace(/,00$/,"")} $`;1835}1836function priceTxt(ev){1837  if(ev.is_free === true) return "Gratuit";1838  if(ev.price_min != null) return `Dès ${money(ev.price_min)}`;1839  return "";1840}1841const WDAYS = ["dim","lun","mar","mer","jeu","ven","sam"];1842/* date éditoriale des fiches : « Mer 27 août · 20 h » / « 27 août → 3 sept » */1843function evDate(ev){1844  if(!ev.start_date) return "Date à confirmer";1845  const yNow = new Date().getFullYear();1846  const [y,m,d] = ev.start_date.split("-").map(Number);1847  if(ev.end_date && ev.end_date !== ev.start_date){1848    const [y2,m2,d2] = ev.end_date.split("-").map(Number);1849    return `${d} ${MONTHS[m-1]} → ${d2} ${MONTHS[m2-1]}${y2!==yNow?` ${y2}`:""}`;1850  }1851  const wd = WDAYS[new Date(y, m-1, d).getDay()];1852  let s = `${wd} ${d} ${MONTHS[m-1]}${y!==yNow?` ${y}`:""}`;1853  if(ev.start_time && ev.start_time !== "00:00") s += ` · ${fmtTime(ev.start_time)}`;1854  return s;1855}1856/* étiquette posée sur l'image : statut sinon prix */1857function evTag(ev){1858  if(ev.status && STATUS_LABELS[ev.status])1859    return `<span class="ev-tag stat-${esc(ev.status)}">${STATUS_LABELS[ev.status]}</span>`;1860  if(ev.is_free === true) return `<span class="ev-tag free">Gratuit</span>`;1861  if(ev.price_min != null) return `<span class="ev-tag">Dès ${money(ev.price_min)}</span>`;1862  return "";1863}1864function evImg(ev){1865  return ev.image1866    ? `<img src="${esc(ev.image)}" alt="" loading="lazy"1867        onerror="this.parentElement.classList.add('noimg');this.remove()">` : "";1868}18691870/* fiche standard (grilles + carrousels) — l'image domine, hiérarchie1871   DATE / titre / lieu · ville / prix (sur l'image) / catégorie · source */1872function card(ev){1873  const where = [ev.venue, ev.city].filter(Boolean).map(esc).join(" · ");1874  const cat = (ev.categories||[]).filter(c=>c!=="autre")[0];1875  return `<a class="ev" data-nav href="/evenement/${esc(ev.uid)}">1876    <div class="ev-img${ev.image?"":" noimg"}">${evImg(ev)}${evTag(ev)}${favBtn(ev)}</div>1877    <div class="ev-body">1878      <span class="ev-date">${evDate(ev)}${ev.ka_reco?` <span class="ev-reco" title="Correspond à vos préférences KA ID — gérez-les sur groupe-ka.com/mon-ka">Recommandé pour vous</span>`:""}</span>1879      <h3>${esc(ev.title)}</h3>1880      <p class="ev-where">${where || esc(ev.region || "Québec")}</p>1881      <p class="ev-meta">${cat?`<span>${esc(CAT_LABELS[cat]||cat)}</span>`:""}1882        <span class="src">${esc(SRC_LABELS[ev.source]||ev.source)}</span></p>1883    </div></a>`;1884}18851886/* grand événement vedette — image pleine largeur, texte en surimpression */1887function featCard(ev){1888  const where = [ev.venue, ev.city].filter(Boolean).map(esc).join(" · ");1889  const p = priceTxt(ev);1890  return `<a class="feat" data-nav href="/evenement/${esc(ev.uid)}">1891    ${evImg(ev)}<span class="feat-kick">À la une</span>${favBtn(ev)}1892    <div class="feat-body">1893      <span class="feat-date">${evDate(ev)}</span>1894      <h3>${esc(ev.title)}</h3>1895      <p class="feat-where">${where}${p?` — <b>${p}</b>`:""}</p>1896    </div></a>`;1897}18981899/* rangée compacte (À découvrir) */1900function compactCard(ev){1901  const where = [ev.venue, ev.city].filter(Boolean).map(esc).join(" · ");1902  return `<a class="evc" data-nav href="/evenement/${esc(ev.uid)}">1903    <span class="evc-img${ev.image?"":" noimg"}">${evImg(ev)}</span>1904    <span class="evc-body">1905      <span class="ev-date">${evDate(ev)}</span>1906      <h3>${esc(ev.title)}</h3>1907      <p class="ev-where">${where || esc(ev.region || "Québec")}</p>1908    </span></a>`;1909}19101911/* ---------- état des filtres ---------- */1912function isoLocal(d){1913  return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`;1914}1915const todayISO = () => isoLocal(new Date());1916/* fin de semaine : vendredi → dimanche (ou aujourd'hui si déjà entamée) */1917function weekendRange(){1918  const t = new Date(), day = t.getDay();1919  const fri = new Date(t);1920  if(day >= 1 && day <= 4) fri.setDate(t.getDate() + (5 - day));1921  const sun = new Date(fri);1922  sun.setDate(fri.getDate() + (fri.getDay() === 0 ? 0 : 7 - fri.getDay()));1923  return [isoLocal(fri), isoLocal(sun)];1924}1925function plusDays(n){1926  const d = new Date(); d.setDate(d.getDate()+n); return isoLocal(d);1927}1928function isFiltering(){1929  return !!(state.q || state.region || state.category || state.free1930            || state.from || state.to || state.near1931            || state.mapAll); // onglet Carte (KA Tabbar) : résultats sans filtre1932}1933function params(){1934  const p = new URLSearchParams();1935  if(state.q) p.set("q", state.q);1936  if(state.region) p.set("region", state.region);1937  if(state.category) p.set("category", state.category);1938  if(state.free) p.set("free","true");1939  if(state.from) p.set("from", state.from);1940  if(state.to) p.set("to", state.to);1941  if(state.near){ p.set("near", state.near); p.set("radius_km","30"); p.set("sort","near"); }1942  p.set("limit", state.limit); p.set("offset", state.offset);1943  return p.toString();1944}1945async function fetchEvents(qs){1946  return await (await fetch(`/api/events?${qs}`)).json();1947}19481949/* ---------- accueil éditorial : sections composées ---------- */1950let HOME_DONE = false;1951const HOME_DEFS = [1952  {id:"alaune",    sk:"Sélection Sorti·Ka",     t:"À la une",              mode:"feat",1953   q:{limit:30}},1954  {id:"cesoir",    sk:"Aujourd'hui",            t:"Ce soir",               mode:"row",1955   q:()=>({from:todayISO(), to:todayISO(), limit:24}), all:()=>({from:todayISO(), to:todayISO()})},1956  {id:"weekend",   sk:"Vendredi → dimanche",    t:"Cette fin de semaine",  mode:"grid",1957   q:()=>{ const [a,b]=weekendRange(); return {from:a, to:b, limit:8}; },1958   all:()=>{ const [a,b]=weekendRange(); return {from:a, to:b}; }},1959  {id:"gratuit",   sk:"0 $",                    t:"Gratuit",               mode:"row",1960   q:{free:true, limit:14}, all:{free:true}},1961  {id:"musique",   sk:"Catégorie",              t:"Concerts & musique",    mode:"row",1962   q:{category:"musique", limit:14}, all:{category:"musique"}},1963  {id:"festival",  sk:"Catégorie",              t:"Festivals",             mode:"row",1964   q:{category:"festival", limit:14}, all:{category:"festival"}},1965  {id:"famille",   sk:"Catégorie",              t:"Famille",               mode:"row",1966   q:{category:"famille", limit:14}, all:{category:"famille"}},1967  {id:"decouvrir", sk:"Fraîchement ajoutés",    t:"À découvrir",           mode:"compact",1968   q:{sort:"recent", limit:10}},1969];1970function secHead(d, total){1971  const n = total ? `<button type="button" class="sec-all" data-all="${d.id}">1972      Tout voir (${total.toLocaleString("fr-CA")}) →</button>` : "";1973  return `<div class="sec-head"><div><span class="sk">${d.sk}</span><h2>${d.t}</h2></div>${n}</div>`;1974}1975function heroMosaic(evs){1976  const tiles = evs.filter(e=>e.image).slice(0,12);1977  if(!tiles.length) return;1978  $("hero-mosaic").innerHTML = tiles.map(e =>1979    `<span class="tile"><img src="${esc(e.image)}" alt="" loading="lazy"1980       onerror="this.remove()"></span>`).join("");1981}1982/* durée d'un événement en jours (0 = un seul jour) */1983function evSpan(e){1984  if(!e.end_date || !e.start_date || e.end_date === e.start_date) return 0;1985  return (new Date(e.end_date) - new Date(e.start_date)) / 864e5;1986}1987async function renderHome(){1988  if(HOME_DONE) return;1989  HOME_DONE = true;1990  const host = $("home");1991  host.innerHTML = HOME_DEFS.map(d=>`<section class="sec" id="sec-${d.id}"></section>`).join("");1992  const results = await Promise.all(HOME_DEFS.map(async d => {1993    try{1994      const spec = typeof d.q === "function" ? d.q() : d.q;1995      const p = new URLSearchParams();1996      Object.entries(spec).forEach(([k,v]) => p.set(k, v));1997      return [d, await fetchEvents(p.toString())];1998    }catch(e){ return [d, null]; }1999  }));2000  // rendu séquentiel avec dédoublonnage inter-sections : un événement déjà mis2001  // en vitrine plus haut ne se répète pas ; les expos/festivals au long cours2002  // cèdent la vedette aux événements datés (le rythme vient de la variété)2003  const seen = new Set();2004  const take = (evs, n) => {2005    const out = evs.slice(0, n);2006    out.forEach(e => seen.add(e.uid));2007    return out;2008  };2009  for(const [d, data] of results){2010    const el = $(`sec-${d.id}`);2011    if(!el) continue;2012    if(!data){ el.remove(); continue; }2013    let evs = (data.events || []).filter(e => !seen.has(e.uid));2014    if(d.id === "alaune"){2015      evs = evs.filter(e => e.image);2016      evs.sort((a,b) => (evSpan(a) > 3) - (evSpan(b) > 3));2017    } else if(d.id === "cesoir" || d.id === "weekend"){2018      evs.sort((a,b) => (evSpan(a) > 3) - (evSpan(b) > 3));2019    }2020    if(!evs.length){ el.remove(); continue; }2021    if(d.mode === "feat"){2022      const picked = take(evs, 10);2023      el.innerHTML = secHead(d, 0) + featCard(picked[0])2024        + (picked.length > 1 ? `<div class="hscroll">${picked.slice(1).map(card).join("")}</div>` : "");2025      heroMosaic(evs);2026    } else if(d.mode === "row"){2027      el.innerHTML = secHead(d, data.total) + `<div class="hscroll">${take(evs, 14).map(card).join("")}</div>`;2028    } else if(d.mode === "grid"){2029      el.innerHTML = secHead(d, data.total) + `<div class="grid">${take(evs, 8).map(card).join("")}</div>`;2030    } else {2031      el.innerHTML = secHead(d, 0) + `<div class="compact">${take(evs, 10).map(compactCard).join("")}</div>`;2032    }2033    bindNav(el);2034    const btn = el.querySelector("[data-all]");2035    if(btn) btn.onclick = () => {2036      const f = typeof d.all === "function" ? d.all() : (d.all || {});2037      resetFilters(false);2038      if(f.from){ state.from = f.from; state.to = f.to || ""; }2039      if(f.free) state.free = true;2040      if(f.category) state.category = f.category;2041      load();2042      window.scrollTo({top: $("results").offsetTop - 130, behavior:"smooth"});2043    };2044  }2045  favPaint();2046}20472048/* ---------- pilules de filtres actifs (résultats) ---------- */2049function filterPills(){2050  const pills = [];2051  if(state.q) pills.push(["q", `« ${state.q} »`]);2052  if(state.from && state.from === state.to && state.from === todayISO()) pills.push(["dates","Aujourd'hui"]);2053  else if(state.from || state.to){2054    const w = weekendRange();2055    pills.push(["dates", state.from===w[0]&&state.to===w[1] ? "Cette fin de semaine"2056      : [state.from&&fmtDate(state.from), state.to&&fmtDate(state.to)].filter(Boolean).join(" → ")]);2057  }2058  if(state.region) pills.push(["region", state.region]);2059  if(state.category) pills.push(["category", CAT_LABELS[state.category]||state.category]);2060  if(state.free) pills.push(["free", "Gratuit"]);2061  if(state.near) pills.push(["near", "Près de moi"]);2062  $("afilters").innerHTML = pills.map(([k,label]) =>2063    `<button type="button" class="apill" data-clear="${k}">${esc(label)}<i>✕</i></button>`).join("");2064  $("afilters").querySelectorAll("[data-clear]").forEach(b => b.onclick = () => {2065    const k = b.dataset.clear;2066    if(k==="q"){ state.q=""; $("q").value=""; }2067    else if(k==="dates"){ state.from=""; state.to=""; }2068    else if(k==="region") state.region="";2069    else if(k==="category") state.category="";2070    else if(k==="free") state.free=false;2071    else if(k==="near") state.near="";2072    load();2073  });2074}20752076/* ---------- chargement : accueil éditorial OU résultats ---------- */2077async function load(reset=true){2078  const filtering = isFiltering();2079  $("home").classList.toggle("hidden", filtering);2080  $("results").classList.toggle("hidden", !filtering);2081  syncControls();2082  if(!filtering){ renderHome(); return; }2083  if(reset) state.offset = 0;2084  filterPills();2085  let data;2086  try{ data = await fetchEvents(params()); }2087  catch(e){ $("count").textContent = "Erreur de chargement — réessayez."; return; }2088  state.total = data.total;2089  const html = data.events.map(card).join("");2090  if(reset){2091    $("grid").innerHTML = html ||2092      `<div class="empty"><b>Aucun événement trouvé</b>Essayez d'élargir les filtres ou la période.</div>`;2093  } else {2094    $("grid").insertAdjacentHTML("beforeend", html);2095  }2096  $("count").innerHTML = `<b>${data.total.toLocaleString("fr-CA")}</b>&nbsp;sortie${data.total>1?"s":""} trouvée${data.total>1?"s":""}`;2097  $("sh-apply").textContent = `Voir ${data.total.toLocaleString("fr-CA")} sortie${data.total>1?"s":""}`;2098  $("more").hidden = state.view === "map" || state.offset + data.events.length >= data.total;2099  bindNav($("grid"));2100  favPaint();2101  if(state.view === "map") refreshMarkers();2102}2103let loadT;2104function loadDebounced(){ clearTimeout(loadT); loadT = setTimeout(load, 220); }21052106/* ---------- accès rapides + timeline de jours ---------- */2107const QUICK_DEFS = [2108  ["cesoir","Ce soir"], ["weekend","Cette fin de semaine"], ["free","Gratuit"],2109];2110const PIN_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"2111  stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">2112  <path d="M12 21s-7-5.5-7-11a7 7 0 0 1 14 0c0 5.5-7 11-7 11z"/><circle cx="12" cy="10" r="2.6"/></svg>`;2113const SLIDERS_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"2114  stroke-linecap="round" aria-hidden="true">2115  <path d="M4 7h9M17 7h3M4 17h3M11 17h9"/><circle cx="15" cy="7" r="2.2"/><circle cx="9" cy="17" r="2.2"/></svg>`;2116function renderQuick(){2117  $("quick").innerHTML =2118    QUICK_DEFS.map(([k,l]) => `<button type="button" class="qchip" data-qk="${k}">${l}</button>`).join("")2119    + `<button type="button" class="qchip" data-qk="near">${PIN_SVG}Près de moi</button>`2120    + `<button type="button" class="qchip" id="btn-filters">${SLIDERS_SVG}Filtres2121        <span class="fb hidden" id="fcount"></span></button>`;2122  $("quick").querySelectorAll("[data-qk]").forEach(b => b.onclick = () => {2123    const k = b.dataset.qk;2124    if(k === "cesoir"){2125      const on = state.from === todayISO() && state.to === todayISO();2126      state.from = on ? "" : todayISO(); state.to = on ? "" : todayISO();2127    } else if(k === "weekend"){2128      const [a,z] = weekendRange();2129      const on = state.from === a && state.to === z;2130      state.from = on ? "" : a; state.to = on ? "" : z;2131    } else if(k === "free"){2132      state.free = !state.free;2133    } else if(k === "near"){2134      toggleNear(); return;2135    }2136    load();2137  });2138  $("btn-filters").onclick = openSheet;2139}2140function renderDays(){2141  const t = new Date();2142  let html = `<button type="button" class="day all" data-d=""><small>·Ka</small><b>Tout</b><i>&nbsp;</i></button>`;2143  for(let i=0; i<15; i++){2144    const d = new Date(t); d.setDate(t.getDate()+i);2145    const iso = isoLocal(d);2146    const lab = i===0 ? "auj" : i===1 ? "dem" : WDAYS[d.getDay()];2147    html += `<button type="button" class="day" data-d="${iso}">2148      <small>${lab}</small><b>${d.getDate()}</b><i>${MONTHS[d.getMonth()]}</i></button>`;2149  }2150  $("days").innerHTML = html;2151  $("days").querySelectorAll(".day").forEach(b => b.onclick = () => {2152    const d = b.dataset.d;2153    if(!d){ state.from = ""; state.to = ""; }2154    else if(state.from === d && state.to === d){ state.from = ""; state.to = ""; }2155    else { state.from = d; state.to = d; }2156    load();2157  });2158}2159/* reflète l'état dans les accès rapides, la timeline et le panneau */2160function syncControls(){2161  const today = todayISO(), [wa,wz] = weekendRange();2162  document.querySelectorAll("[data-qk]").forEach(b => {2163    const k = b.dataset.qk;2164    b.classList.toggle("on",2165      k==="cesoir" ? (state.from===today && state.to===today)2166      : k==="weekend" ? (state.from===wa && state.to===wz)2167      : k==="free" ? state.free2168      : k==="near" ? !!state.near : false);2169  });2170  document.querySelectorAll(".day[data-d]").forEach(b =>2171    b.classList.toggle("on", b.dataset.d ? (state.from===b.dataset.d && state.to===b.dataset.d)2172                                         : !state.from && !state.to));2173  const n = ["q","region","category","from","near"].filter(k=>state[k]).length + (state.free?1:0);2174  const fc = $("fcount");2175  if(fc){ fc.textContent = n; fc.classList.toggle("hidden", !n); }2176  $("q-clear").classList.toggle("hidden", !state.q);2177  // panneau de filtres2178  if($("region").value !== state.region) $("region").value = state.region;2179  document.querySelectorAll("#cats .chip[data-cat]").forEach(x =>2180    x.classList.toggle("on", x.dataset.cat === state.category));2181  $("chip-free").classList.toggle("on", state.free);2182  $("sh-from").value = state.from; $("sh-to").value = state.to;2183  const whenKey = (!state.from && !state.to) ? "" :2184    (state.from===today && state.to===today) ? "today" :2185    (state.from===wa && state.to===wz) ? "weekend" :2186    (state.from===today && state.to===plusDays(7)) ? "7" :2187    (state.from===today && state.to===plusDays(30)) ? "30" : "custom";2188  document.querySelectorAll("#sh-when .chip").forEach(c =>2189    c.classList.toggle("on", c.dataset.w === whenKey));2190}2191function resetFilters(reload=true){2192  state.region=""; state.category=""; state.free=false;2193  state.from=""; state.to=""; state.near="";2194  if(reload) load();2195}21962197/* ---------- géolocalisation « près de moi » ---------- */2198function toast(msg, ms=2600){2199  const t = $("toast");2200  t.textContent = msg;2201  t.classList.add("show");2202  clearTimeout(t._t);2203  t._t = setTimeout(()=>t.classList.remove("show"), ms);2204}2205function toggleNear(){2206  if(state.near){ state.near = ""; load(); return; }2207  if(!navigator.geolocation){ toast("Géolocalisation non disponible sur cet appareil"); return; }2208  toast("Recherche de votre position…");2209  navigator.geolocation.getCurrentPosition(2210    pos => {2211      state.near = `${pos.coords.latitude.toFixed(5)},${pos.coords.longitude.toFixed(5)}`;2212      load();2213      toast("Sorties à moins de 30 km de vous");2214    },2215    () => toast("Position indisponible — vérifiez les permissions de localisation"),2216    {timeout: 9000, maximumAge: 600000});2217}22182219/* ---------- panneau de filtres (bottom sheet / modale) ---------- */2220const WHEN_DEFS = [["","À venir"],["today","Aujourd'hui"],["weekend","Fin de semaine"],2221  ["7","7 jours"],["30","30 jours"]];2222function openSheet(){2223  $("sh-apply").textContent = isFiltering() && state.total2224    ? `Voir ${state.total.toLocaleString("fr-CA")} sorties` : "Voir les sorties";2225  $("sheet").classList.add("open");2226  $("sheet-ov").classList.add("open");2227  document.documentElement.classList.add("ka-scroll-lock");2228  syncControls();2229}2230function closeSheet(){2231  $("sheet").classList.remove("open");2232  $("sheet-ov").classList.remove("open");2233  document.documentElement.classList.remove("ka-scroll-lock");2234}2235function initSheet(){2236  $("sh-when").innerHTML = WHEN_DEFS.map(([v,l]) =>2237    `<button type="button" class="chip" data-w="${v}">${l}</button>`).join("");2238  $("sh-when").querySelectorAll(".chip").forEach(c => c.onclick = () => {2239    const v = c.dataset.w;2240    if(v === "today"){ state.from = todayISO(); state.to = todayISO(); }2241    else if(v === "weekend"){ const [a,z] = weekendRange(); state.from = a; state.to = z; }2242    else if(v === "7"){ state.from = todayISO(); state.to = plusDays(7); }2243    else if(v === "30"){ state.from = todayISO(); state.to = plusDays(30); }2244    else { state.from = ""; state.to = ""; }2245    loadDebounced(); syncControls();2246  });2247  const onDate = () => {2248    state.from = $("sh-from").value; state.to = $("sh-to").value;2249    loadDebounced(); syncControls();2250  };2251  $("sh-from").onchange = onDate; $("sh-to").onchange = onDate;2252  $("region").onchange = e => { state.region = e.target.value; loadDebounced(); };2253  $("chip-free").onclick = () => { state.free = !state.free; loadDebounced(); syncControls(); };2254  $("sh-reset").onclick = () => { resetFilters(); };2255  $("sh-apply").onclick = closeSheet;2256  $("sheet-x").onclick = closeSheet;2257  $("sheet-ov").onclick = closeSheet;2258  document.addEventListener("keydown", e => { if(e.key === "Escape") closeSheet(); });2259}22602261/* ---------- vue carte (Leaflet, chargé à la demande) ---------- */2262const MAPS = {inst:null, layer:null, loading:false};2263function ensureLeaflet(){2264  if(window.L) return Promise.resolve();2265  return new Promise((res, rej) => {2266    const c = document.createElement("link");2267    c.rel = "stylesheet";2268    c.href = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css";2269    document.head.appendChild(c);2270    const s = document.createElement("script");2271    s.src = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js";2272    s.onload = res; s.onerror = rej;2273    document.body.appendChild(s);2274  });2275}2276function setView(v){2277  state.view = v;2278  $("v-list").classList.toggle("on", v === "list");2279  $("v-list").setAttribute("aria-pressed", v === "list");2280  $("v-map").classList.toggle("on", v === "map");2281  $("v-map").setAttribute("aria-pressed", v === "map");2282  $("grid").classList.toggle("hidden", v === "map");2283  $("mapwrap").classList.toggle("hidden", v !== "map");2284  $("more").hidden = v === "map" || state.offset + state.limit >= state.total;2285  if(v === "map") openMap();2286}2287async function openMap(){2288  try{ await ensureLeaflet(); }2289  catch(e){ $("map-load").textContent = "Carte indisponible — vérifiez la connexion."; return; }2290  if(!MAPS.inst){2291    MAPS.inst = L.map("map", {zoomControl:true}).setView([46.83,-71.25], 6);2292    L.tileLayer("https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png", {2293      attribution:"© <a href='https://www.openstreetmap.org/copyright'>OpenStreetMap</a> · © <a href='https://carto.com/attributions'>CARTO</a>",2294      maxZoom: 19, subdomains:"abcd"}).addTo(MAPS.inst);2295    MAPS.layer = L.layerGroup().addTo(MAPS.inst);2296    MAPS.inst.on("popupopen", e => bindNav(e.popup.getElement()));2297  }2298  $("map-load").classList.add("hidden");2299  setTimeout(() => MAPS.inst.invalidateSize(), 80);2300  refreshMarkers();2301}2302function popupHTML(ev){2303  return `<a class="mpop" data-nav href="/evenement/${esc(ev.uid)}">2304    ${ev.image?`<img src="${esc(ev.image)}" alt="" loading="lazy" onerror="this.remove()">`:""}2305    <span class="mp-b"><span class="ev-date">${evDate(ev)}</span>2306    <h4>${esc(ev.title)}</h4>2307    <p>${[ev.venue, ev.city].filter(Boolean).map(esc).join(" · ")}${priceTxt(ev)?` — ${priceTxt(ev)}`:""}</p>2308    </span></a>`;2309}2310/* jusqu'à 600 événements géolocalisés pour les filtres courants */2311async function refreshMarkers(){2312  if(!MAPS.inst || MAPS.loading) return;2313  MAPS.loading = true;2314  try{2315    const evs = [];2316    for(let page=0; page<3; page++){2317      const p = new URLSearchParams(params());2318      p.set("limit","200"); p.set("offset", String(page*200));2319      const data = await fetchEvents(p.toString());2320      evs.push(...data.events);2321      if((page+1)*200 >= data.total) break;2322    }2323    MAPS.layer.clearLayers();2324    const pts = [];2325    evs.filter(e => e.lat && e.lng).forEach(ev => {2326      pts.push([ev.lat, ev.lng]);2327      L.circleMarker([ev.lat, ev.lng], {radius:7, color:"#a12551", weight:1.5,2328        fillColor:"#d6336c", fillOpacity:.85})2329        .bindPopup(popupHTML(ev), {closeButton:false}).addTo(MAPS.layer);2330    });2331    if(state.near){2332      const [la,ln] = state.near.split(",").map(Number);2333      L.circleMarker([la,ln], {radius:9, color:"#141814", weight:2.5,2334        fillColor:"#1c5c41", fillOpacity:1}).bindTooltip("Vous êtes ici").addTo(MAPS.layer);2335      MAPS.inst.setView([la,ln], 11);2336    } else if(pts.length){2337      MAPS.inst.fitBounds(L.latLngBounds(pts).pad(0.12), {maxZoom: 13});2338    }2339    const note = $("map-note");2340    note.textContent = `${pts.length.toLocaleString("fr-CA")} sur ${state.total.toLocaleString("fr-CA")} sorties géolocalisées — affinez avec les filtres`;2341    note.classList.toggle("hidden", !pts.length);2342  }catch(e){}2343  MAPS.loading = false;2344}23452346/* ---------- fiche événement ---------- */2347/* ==================== fiche événement — refonte premium 2026-09-07 ==========2348   Même socle que les fiches Lou-Ka v3 / Immo-Ka v3, en JS natif : héro2349   (catégorie · source, prix, capsules date/statut, titre, résumé, image,2350   actions) → En bref (constats déterministes) → navigation sticky (scroll-spy)2351   → Quand et où (faits + carte Leaflet) → Programme (description, artistes,2352   étiquettes) → Billets (prix, billetterie) → À voir aussi → Dossier →2353   Sources. CTA sticky mobile, aside desktop, « Demander à Ka » (widget KA2354   Agent). Ordre DOM = ordre visuel, aucun scrollIntoView à l'ouverture. */2355const SK_ICO = {2356  share: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="18" cy="5" r="2.5"/><circle cx="6" cy="12" r="2.5"/><circle cx="18" cy="19" r="2.5"/><path d="M8.2 10.8 15.8 6.3M8.2 13.2l7.6 4.5"/></svg>',2357  ext: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14.4 4h5.6v5.6"/><path d="M20 4 11.4 12.6"/><path d="M18.6 13.6V18a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V7.4a2 2 0 0 1 2-2h4.4"/></svg>',2358  spark: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3l1.9 5.6 5.6 1.9-5.6 1.9L12 18l-1.9-5.6-5.6-1.9 5.6-1.9L12 3z"/><path d="M19 16l.8 2.2 2.2.8-2.2.8L19 22l-.8-2.2-2.2-.8 2.2-.8L19 16z"/></svg>',2359  pin: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 21.2S5.4 15.7 5.4 10.8a6.6 6.6 0 0 1 13.2 0c0 4.9-6.6 10.4-6.6 10.4z"/><circle cx="12" cy="10.6" r="2.3"/></svg>',2360  cal: '<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="4" y="5.6" width="16" height="14.8" rx="2"/><path d="M4 10.2h16M8.2 3.4v4M15.8 3.4v4"/></svg>',2361  tag: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m12.9 3.6 7.5 7.5a1.8 1.8 0 0 1 0 2.5l-6.8 6.8a1.8 1.8 0 0 1-2.5 0L3.6 12.9V3.6z"/><circle cx="8" cy="8" r="1.5"/></svg>',2362  folder: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h6l2 2.5h10V19H3z"/></svg>',2363  music: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>',2364  ticket: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 9a2 2 0 0 0 0 6v3a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1v-3a2 2 0 0 0 0-6V6a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1z"/><path d="M13 5v14"/></svg>',2365  alert: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 4.2 2.9 19.4h18.2z"/><path d="M12 10.2v4.4"/><path d="M12 17.4v.05"/></svg>',2366  chev: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9.5 5 7 7-7 7"/></svg>',2367  people: '<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="9" cy="8" r="3.2"/><path d="M3.6 20a5.4 5.4 0 0 1 10.8 0"/><path d="M15.4 5.4a3.2 3.2 0 0 1 0 5.9M17 14.8a5.4 5.4 0 0 1 3.4 5.2"/></svg>',2368};2369const SK_STATE = { uid: null, cleanup: [] };23702371function skDays(ev){2372  if(!ev.start_date) return null;2373  const d = new Date(`${ev.start_date}T12:00:00`), t = new Date(); t.setHours(12,0,0,0);2374  return Math.round((d - t) / 86400000);2375}2376function skQuand(ev){2377  const n = skDays(ev);2378  if(n == null) return { tone:"neutral", titre:"Date à confirmer", detail:"La source ne publie pas de date précise" };2379  const fin = ev.end_date && ev.end_date !== ev.start_date;2380  const finN = fin ? Math.round((new Date(`${ev.end_date}T12:00:00`) - new Date(new Date().setHours(12,0,0,0))) / 86400000) : n;2381  if(finN < 0) return { tone:"bad", titre:"Événement terminé", detail:`${fmtDate(ev.start_date, true)}${fin ? ` au ${fmtDate(ev.end_date, true)}` : ""}` };2382  if(n <= 0 && finN >= 0 && fin) return { tone:"good", titre:"En cours", detail:`Jusqu'au ${fmtDate(ev.end_date, true)}${ev.end_time ? ` · ${fmtTime(ev.end_time)}` : ""}` };2383  if(n === 0) return { tone:"good", titre:"C'est aujourd'hui", detail:`${fmtDate(ev.start_date, true)}${ev.start_time ? ` · ${fmtTime(ev.start_time)}` : ""}` };2384  if(n === 1) return { tone:"good", titre:"C'est demain", detail:`${fmtDate(ev.start_date, true)}${ev.start_time ? ` · ${fmtTime(ev.start_time)}` : ""}` };2385  const dow = new Date(`${ev.start_date}T12:00:00`).getDay();2386  if(n <= 7) return { tone:"good", titre:(dow === 0 || dow === 6) ? "Ce week-end" : `Dans ${n} jours`, detail:`${fmtDate(ev.start_date, true)}${ev.start_time ? ` · ${fmtTime(ev.start_time)}` : ""}` };2387  return { tone:"neutral", titre:`Dans ${n} jours`, detail:`${fmtDate(ev.start_date, true)}${fin ? ` au ${fmtDate(ev.end_date, true)}` : ""}${ev.start_time ? ` · ${fmtTime(ev.start_time)}` : ""}` };2388}2389function skPrix(ev){2390  if(ev.is_free === true) return "Gratuit";2391  if(ev.price_min != null) return `Dès ${money(ev.price_min)}`;2392  if(ev.is_free === false) return "Billetterie";2393  return "Voir la source";2394}2395function skEnBref(ev){2396  const out = [];2397  const q = skQuand(ev); out.push({ cle:"quand", ...q });2398  if(ev.status && STATUS_LABELS[ev.status])2399    out.push({ cle:"statut", tone: ev.status === "soldout" ? "warn" : "bad", titre: ev.status === "cancelled" ? "Événement annulé" : ev.status === "postponed" ? "Événement reporté" : "Complet — plus de billets",2400               detail:"Statut publié par la source — vérifiez la fiche originale" });2401  if(ev.is_free === true) out.push({ cle:"prix", tone:"good", titre:"Entrée gratuite", detail:"Selon la source — inscription parfois requise" });2402  else if(ev.price_min != null) out.push({ cle:"prix", tone:"neutral", titre:`Billets dès ${money(ev.price_min)}`, detail: ev.price_label || "Prix publié par la billetterie, frais en sus possibles" });2403  else if(ev.is_free === false) out.push({ cle:"prix", tone:"neutral", titre:"Événement payant", detail:"Prix non publié — voir la billetterie" });2404  if(ev.venue || ev.city) out.push({ cle:"lieu", tone:"info", titre:[ev.venue, ev.city].filter(Boolean).join(" · "), detail:[ev.address, ev.region && ev.region !== ev.city ? ev.region : null].filter(Boolean).join(" · ") || "Lieu publié par la source" });2405  if(ev.artists && ev.artists.length) out.push({ cle:"artistes", tone:"info", titre:`À l'affiche : ${ev.artists.slice(0,3).join(", ")}${ev.artists.length > 3 ? ` +${ev.artists.length - 3}` : ""}`, detail:`${ev.artists.length} artiste${ev.artists.length > 1 ? "s" : ""} annoncé${ev.artists.length > 1 ? "s" : ""}` });2406  if(ev.audience) out.push({ cle:"public", tone:"neutral", titre:`Public : ${ev.audience}`, detail:"Indication de la source" });2407  return out.slice(0, 6);2408}2409function skBrief(items, compact){2410  const g = t => t === "good" ? "✓" : t === "bad" ? "✕" : t === "warn" ? "!" : "○";2411  return `<ul class="sk-brief">${items.map(c => `<li><span class="sk-brief-ico ${c.tone === "info" ? "neutral" : c.tone}" aria-hidden="true">${g(c.tone)}</span>2412    <div><div class="sk-brief-t">${esc(c.titre)}</div>${!compact && c.detail ? `<div class="sk-brief-d">${esc(c.detail)}</div>` : ""}</div></li>`).join("")}</ul>`;2413}2414function skCard(id, title, ico, body, sub, aside){2415  return `<section id="sk-${id}" class="sk-card">${title ? `<div class="sk-card-head"><div><h2 class="sk-card-title">${ico || ""}${esc(title)}</h2>${sub ? `<p class="sk-card-sub">${sub}</p>` : ""}</div>${aside ? `<div class="sk-card-aside">${aside}</div>` : ""}</div>` : ""}${body}</section>`;2416}2417function skFact(ico, v, l){ return `<div class="sk-fact" role="listitem"><span class="sk-fact-ico" aria-hidden="true">${ico}</span><span class="sk-fact-txt"><div class="sk-fact-v" title="${esc(v)}">${esc(v)}</div><div class="sk-fact-l">${esc(l)}</div></span></div>`; }2418function skKv(k, v){ return `<div class="sk-kv"><span class="k">${esc(k)}</span><span class="v">${v}</span></div>`; }2419function skDetails(title, body, meta){ return `<details class="sk-details"><summary><span>${title}</span>${meta ? `<span class="sk-acc-meta">${esc(meta)}</span>` : ""}</summary><div class="sk-acc-inner">${body}</div></details>`; }24202421async function showDetail(uid){2422  let ev;2423  try{ ev = await (await fetch(`/api/events/${encodeURIComponent(uid)}`)).json(); }2424  catch(e){ go("/"); return; }2425  if(ev.detail){ go("/"); return; }2426  SK_STATE.cleanup.forEach(f => { try{ f(); }catch(_){} }); SK_STATE.cleanup = []; SK_STATE.uid = ev.uid;2427  FAV.meta[ev.uid] = favItem(ev);24282429  document.title = `${ev.title} — ${ev.city || "Québec"} | Sorti·Ka`;2430  const cats = (ev.categories||[]).filter(c=>c!=="autre").map(c=>CAT_LABELS[c]||c);2431  const catTxt = cats.join(" · ") || "Événement";2432  const src = SRC_LABELS[ev.source] || ev.source;2433  const brief = skEnBref(ev);2434  const quand = skQuand(ev);2435  const prix = skPrix(ev);2436  const resume = [ev.venue, ev.city, evDate(ev)].filter(Boolean);2437  const geo = ev.lat && ev.lng;2438  const statusCap = ev.status && STATUS_LABELS[ev.status] ? `<span class="sk-capsule high"><b>${esc(STATUS_LABELS[ev.status])}</b></span>` : "";2439  const dateCap = quand ? `<span class="sk-capsule ${quand.tone === "good" ? "good" : quand.tone === "bad" ? "high" : "date"}"><b>${esc(quand.titre)}</b></span>` : "";2440  const shareBtn = (cls) => `<button type="button" class="${cls}" data-sk-share aria-label="Partager">${SK_ICO.share}</button>`;2441  const cta = (cls, txt) => `<a class="${cls}" href="${esc(ev.url)}" target="_blank" rel="noopener noreferrer">${txt} ${SK_ICO.ext}</a>`;2442  const nav = [["resume","Résumé"],["quand","Quand et où"],["programme","Programme"],["billets","Billets"],["similaires","À voir aussi"],["dossier","Dossier"],["sources","Sources"]];24432444  // --- faits « Quand et où »2445  const facts = [];2446  if(ev.start_date) facts.push(skFact(SK_ICO.cal, fmtDate(ev.start_date, true), ev.end_date && ev.end_date !== ev.start_date ? `Du … au ${fmtDate(ev.end_date, true)}` : "Date"));2447  if(ev.start_time) facts.push(skFact(SK_ICO.cal, `${fmtTime(ev.start_time)}${ev.end_time ? ` – ${fmtTime(ev.end_time)}` : ""}`, "Heure"));2448  if(ev.venue) facts.push(skFact(SK_ICO.pin, ev.venue, "Lieu"));2449  if(ev.city) facts.push(skFact(SK_ICO.pin, ev.city, ev.region && ev.region !== ev.city ? `Ville · ${ev.region}` : "Ville"));2450  if(ev.audience) facts.push(skFact(SK_ICO.people, ev.audience, "Public"));2451  if(ev.organizer) facts.push(skFact(SK_ICO.people, ev.organizer, "Organisé par"));2452  const rows = [];2453  if(ev.address) rows.push(skKv("Adresse", esc([ev.address, ev.city, ev.postal_code].filter(Boolean).join(", "))));2454  if(ev.tourist_region) rows.push(skKv("Région touristique", esc(ev.tourist_region)));2455  if(geo) rows.push(skKv("Itinéraire", `<a href="https://www.google.com/maps?q=${ev.lat},${ev.lng}" target="_blank" rel="noopener">Google Maps ↗</a> · <a href="https://maps.apple.com/?q=${ev.lat},${ev.lng}" target="_blank" rel="noopener">Plans ↗</a>`));24562457  // --- programme2458  const desc = (ev.description || "").trim();2459  const long = desc.length > 420;2460  const raw = (ev.raw_categories||[]).filter(c => c && c.length < 40).slice(0, 8);24612462  const html = `2463  <div class="sk-wrap">2464    <nav class="sk-crumbs" aria-label="Fil d'Ariane"><a data-nav href="/">Sorties</a><span aria-hidden="true">›</span>${ev.region ? `<a data-nav href="/region/${esc(kaSlug(ev.region))}">${esc(ev.region)}</a><span aria-hidden="true">›</span>` : ""}<span>${esc(ev.title)}</span></nav>2465    <div class="sk-grid">2466      <div class="sk-main">2467        <header class="sk-hero" aria-label="Résumé de l'événement">2468          <div class="sk-kicker">${esc(catTxt)} · ${esc(src)}</div>2469          <div class="sk-price">${esc(prix)}${ev.price_label && ev.price_min != null ? `<small>${esc(ev.price_label)}</small>` : ""}</div>2470          <div class="sk-price-row" style="margin-top:8px">${dateCap}${statusCap}${ev.is_free === true ? `<span class="sk-capsule good"><b>Gratuit</b></span>` : ""}</div>2471          <h1 class="sk-h1">${esc(ev.title)}${resume.length ? `<span class="sk-city">${resume.map(esc).join(" · ")}</span>` : ""}</h1>2472          <div class="sk-image ${ev.image ? "" : "noimg"}">${ev.image ? `<img src="${esc(ev.image)}" alt="" onerror="this.parentNode.classList.add('noimg');this.remove()">` : esc(catTxt)}</div>2473          <div class="sk-actions" id="sk-actions">${favBtn(ev, true)}${shareBtn("sk-btn sk-btn-ghost sk-btn-icon")}${cta("sk-btn sk-btn-primary", ev.is_free === true ? "Voir la fiche originale" : "Billets")}</div>2474        </header>2475        ${skCard("resume", "En bref", "", skBrief(brief))}2476        <nav class="sk-nav" aria-label="Sections de la fiche"><div class="sk-nav-track" id="sk-nav">${nav.map(([id,l], i) => `<a href="#sk-${id}" data-sk-nav="sk-${id}" class="${i === 0 ? "on" : ""}">${l}</a>`).join("")}</div></nav>2477        ${skCard("quand", "Quand et où", SK_ICO.pin, `${facts.length ? `<div class="sk-facts" role="list">${facts.join("")}</div>` : ""}${rows.length ? `<div class="sk-facts-rows">${rows.join("")}</div>` : ""}${geo ? `<div class="sk-map" style="position:relative;border-radius:var(--sk-r-lg);overflow:hidden;border:1px solid var(--sk-border);margin-top:12px"><div id="sk-map" style="position:absolute;inset:0"></div></div><div class="sk-source"><span>Source : <b>OpenStreetMap</b> · position géocodée de l'adresse publiée</span></div>` : ""}`, ev.venue ? `${esc(ev.venue)}${ev.city ? ` — ${esc(ev.city)}` : ""}` : "")}2478        ${skCard("programme", "Programme", SK_ICO.music, `${desc ? `<div class="sk-desc ${long ? "clamped" : ""}" id="sk-desc"><div class="sk-desc-body"><p>${esc(desc)}</p></div></div>${long ? `<button type="button" class="sk-more" data-sk-more aria-expanded="false">Lire la suite</button>` : ""}` : `<p class="sk-fine" style="margin-top:0">La source ne fournit pas de description pour cet événement.</p>`}${ev.artists && ev.artists.length ? `<h3 class="sk-card-sub sk-subtitle" id="artistes">À l'affiche <small>· ${ev.artists.length}</small></h3><div class="sk-amen" role="list">${ev.artists.map(a => `<div class="sk-amen-it" role="listitem"><span class="sk-amen-ico" aria-hidden="true">${SK_ICO.music}</span><span class="sk-amen-txt">${esc(a)}</span></div>`).join("")}</div>` : ""}${raw.length ? `<div class="sk-tags" style="margin-top:12px">${raw.map(c => `<span class="sk-tag n">${esc(c)}</span>`).join("")}</div>` : ""}`)}2479        ${skCard("billets", "Billets et prix", SK_ICO.ticket, `<div class="sk-kpis cols-3"><div class="sk-kpi accent"><div class="sk-kpi-v">${esc(prix)}</div><div class="sk-kpi-l">${ev.is_free === true ? "Entrée" : ev.price_min != null ? "Prix d'entrée minimum" : "Tarification"}</div></div>${ev.price_label ? `<div class="sk-kpi"><div class="sk-kpi-v wrap">${esc(ev.price_label)}</div><div class="sk-kpi-l">Détail publié</div></div>` : ""}<div class="sk-kpi"><div class="sk-kpi-v wrap">${esc(src)}</div><div class="sk-kpi-l">Billetterie / source</div></div></div>${cta("sk-btn sk-btn-primary", ev.is_free === true ? "Fiche originale" : "Acheter des billets chez " + src).replace('class="sk-btn sk-btn-primary"', 'class="sk-btn sk-btn-primary" style="margin-top:12px;width:100%"')}<p class="sk-fine">Prix affichés par la source lors de la dernière synchronisation — frais de service et taxes en sus possibles. Sorti·Ka ne vend pas de billets.</p>`)}2480        ${skCard("similaires", "À voir aussi", SK_ICO.tag, `<div class="grid" id="d-similar"><div class="sk-skel-lines" aria-busy="true"><div class="sk-skel"></div><div class="sk-skel"></div><div class="sk-skel short"></div></div></div>`, cats[0] ? `Autres sorties « ${esc(cats[0])} »${ev.region ? ` en ${esc(ev.region)}` : ""}` : "Autres sorties à proximité")}2481        ${skCard("dossier", "Dossier de la fiche", SK_ICO.folder, `${skDetails(`${SK_ICO.people} Organisateur et source`, `<p style="margin:0 0 6px"><b style="font-size:15px">${esc(ev.organizer || src)}</b></p><p style="margin:0">Fiche publiée par <b>${esc(src)}</b>${ev.website ? ` · <a href="${esc(ev.website)}" target="_blank" rel="noopener noreferrer">site de l'événement ↗</a>` : ""}. Sorti·Ka est un agrégateur indépendant : billets, horaires et annulations relèvent de l'organisateur.</p>`, esc(ev.organizer || src))}${skDetails(`${SK_ICO.cal} Suivi Sorti·Ka`, `<div class="sk-facts-rows" style="margin-top:0">${skKv("Première observation", ev.first_seen ? new Date(ev.first_seen*1000).toLocaleDateString("fr-CA", {day:"numeric", month:"long", year:"numeric"}) : "—")}${skKv("Dernière synchronisation", ev.updated_at ? new Date(ev.updated_at*1000).toLocaleDateString("fr-CA", {day:"numeric", month:"long", year:"numeric"}) : "—")}${skKv("Statut", ev.active ? "Publié par la source" : "Retiré")}</div>`, ev.updated_at ? `synchro ${new Date(ev.updated_at*1000).toLocaleDateString("fr-CA")}` : "")}${skDetails(`${SK_ICO.tag} Identifiants`, `<div class="sk-facts-rows" style="margin-top:0">${skKv("Identifiant source", esc(ev.external_id || ""))}${skKv("Identifiant Sorti·Ka", esc(ev.uid))}${skKv("Fiche originale", `<a href="${esc(ev.url)}" target="_blank" rel="noopener noreferrer">${esc(src)} ↗</a>`)}</div>`, esc(ev.external_id || ""))}`, "Ce que Sorti·Ka observe réellement : organisateur, suivi, identifiants")}2482        ${skCard("sources", "Sources et méthodologie", SK_ICO.folder, `<ul class="sk-sources"><li><span class="n"><a href="${esc(ev.url)}" target="_blank" rel="noopener noreferrer">${esc(src)}</a></span><span class="r">Titre, dates, lieu, prix, image et description (source originale)</span></li><li><span class="n">Sorti·Ka</span><span class="r">Catégorisation, géocodage, dédoublonnage entre sources, suivi des statuts — calculs maison, indicatifs</span></li>${geo ? `<li><span class="n">OpenStreetMap</span><span class="r">Fond de carte et position du lieu</span></li>` : ""}</ul><div class="sk-end" style="margin-top:14px"><a href="/#sources">${SK_ICO.folder}Sources<small>Toutes les sources Sorti·Ka</small></a><a data-nav href="/contact">${SK_ICO.alert}Signaler une erreur<small>Date, lieu, prix…</small></a><button type="button" data-sk-share>${SK_ICO.share}Partager<small>Lien de la fiche</small></button><a data-nav href="/stats">${SK_ICO.tag}Statistiques<small>Sorties par région</small></a></div><p class="sk-fine" style="margin-top:12px">Les dates, prix et disponibilités sont ceux affichés par la source — chaque fiche renvoie à la page originale. Sorti·Ka est un agrégateur indépendant.</p>`, "Chaque donnée de cette fiche renvoie à sa source ; les calculs Sorti·Ka sont indicatifs et documentés")}2483      </div>2484      <aside class="sk-aside" aria-label="Résumé et actions"><div class="sk-card sk-aside-card">2485        <div><div class="sk-price">${esc(prix)}</div><div class="sk-price-row" style="margin-top:8px">${dateCap}${statusCap}</div></div>2486        <div class="sk-aside-addr"><b style="color:var(--sk-text)">${esc(ev.title)}</b><br>${resume.map(esc).join(" · ")}</div>2487        ${cta("sk-btn sk-btn-primary", ev.is_free === true ? "Voir la fiche originale" : "Billets chez " + esc(src))}2488        <div class="sk-actions" id="sk-aside-actions">${favBtn(ev, true)}${shareBtn("sk-btn sk-btn-ghost")}</div>2489        <button type="button" class="sk-btn sk-btn-ghost sk-ka-inline" data-sk-ka>${SK_ICO.spark} Demander à Ka</button>2490        ${brief.length ? `<hr class="sk-aside-sep">${skBrief(brief.slice(0, 4), true)}` : ""}2491        <div class="sk-aside-meta">${ev.updated_at ? `Synchronisé le ${new Date(ev.updated_at*1000).toLocaleDateString("fr-CA")} · ` : ""}${esc(src)}</div>2492      </div></aside>2493    </div>2494  </div>2495  <div class="sk-cta-bar" id="sk-ctabar" aria-hidden="true"><div class="sk-cta-txt"><div class="sk-cta-price">${esc(prix)}</div><div class="sk-cta-sub ${quand.tone === "good" ? "good" : ""}">${esc(quand.titre)} · ${esc(ev.venue || ev.city || "")}</div></div>${cta("sk-btn sk-btn-primary", ev.is_free === true ? "Fiche" : "Billets")}</div>2496  <button type="button" class="sk-ka-btn hide" id="sk-ka-btn" data-sk-ka aria-label="Demander à Ka, l'assistant Groupe KA">${SK_ICO.spark} Demander à Ka</button>`;24972498  const root = $("sk-root");2499  root.innerHTML = html;2500  bindNav(root);2501  // header compact : favoris + partage à côté de la marque (retirés en quittant la fiche)2502  let hdr = $("sk-hdr");2503  if(!hdr){ hdr = document.createElement("div"); hdr.id = "sk-hdr"; hdr.className = "hdr-actions"; const b = document.querySelector(".header .brand"); b.parentNode.insertBefore(hdr, b.nextSibling); }2504  hdr.innerHTML = `${favBtn(ev)}<button type="button" class="hdr-btn" data-sk-share aria-label="Partager">${SK_ICO.share}</button>`;2505  favPaint();25062507  // partage2508  document.querySelectorAll("[data-sk-share]").forEach(b => b.onclick = async () => {2509    const url = `https://www.sorti-ka.com/evenement/${encodeURIComponent(ev.uid)}`;2510    try{2511      if(navigator.share){ await navigator.share({ title: `${ev.title} — Sorti·Ka`, url }); return; }2512      await navigator.clipboard.writeText(url); toast("Lien copié");2513    }catch(_){}2514  });2515  // description « Lire la suite »2516  const more = root.querySelector("[data-sk-more]");2517  if(more) more.onclick = () => { const open = $("sk-desc").classList.toggle("clamped") === false; more.textContent = open ? "Réduire" : "Lire la suite"; more.setAttribute("aria-expanded", open); };2518  // navigation par sections : scroll-spy + défilement doux (jamais à l'ouverture)2519  const navEl = $("sk-nav");2520  const links = [...navEl.querySelectorAll("a[data-sk-nav]")];2521  links.forEach(a => a.onclick = e => { e.preventDefault(); const t = $(a.dataset.skNav); if(t) t.scrollIntoView({ behavior:"smooth", block:"start" }); });2522  const visible = new Map();2523  const io = new IntersectionObserver(entries => {2524    entries.forEach(en => visible.set(en.target.id, en.isIntersecting ? en.intersectionRatio : 0));2525    const first = nav.map(([id]) => "sk-" + id).find(id => (visible.get(id) || 0) > 0);2526    if(first){ links.forEach(a => a.classList.toggle("on", a.dataset.skNav === first)); const on = navEl.querySelector(".on"); if(on){ const l = on.offsetLeft, r = l + on.offsetWidth; if(l < navEl.scrollLeft) navEl.scrollTo({ left: Math.max(0, l - 12), behavior:"smooth" }); else if(r > navEl.scrollLeft + navEl.clientWidth) navEl.scrollTo({ left: r - navEl.clientWidth + 12, behavior:"smooth" }); } }2527  }, { rootMargin:"-120px 0px -55% 0px", threshold:[0, .1, .5] });2528  nav.forEach(([id]) => { const el = $("sk-" + id); if(el) io.observe(el); });2529  SK_STATE.cleanup.push(() => io.disconnect());2530  // CTA sticky + bouton Ka : après le héro2531  const actions = $("sk-actions"), bar = $("sk-ctabar"), kab = $("sk-ka-btn");2532  const onScroll = () => { const past = actions.getBoundingClientRect().bottom < 0; bar.classList.toggle("show", past); bar.setAttribute("aria-hidden", !past); kab.classList.toggle("hide", !past); };2533  window.addEventListener("scroll", onScroll, { passive:true }); onScroll();2534  SK_STATE.cleanup.push(() => window.removeEventListener("scroll", onScroll));2535  // Demander à Ka2536  root.querySelectorAll("[data-sk-ka]").forEach(b => b.onclick = () => skOpenKa(ev));25372538  show("view-detail");2539  window.scrollTo({ top:0 });25402541  // carte Leaflet (après l'affichage : le conteneur doit être mesurable)2542  if(geo){2543    ensureLeaflet().then(() => {2544      const m = L.map("sk-map", { zoomControl:true, scrollWheelZoom:false }).setView([ev.lat, ev.lng], 14);2545      L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution:"© <a href='https://www.openstreetmap.org/copyright'>OpenStreetMap</a>", maxZoom:19 }).addTo(m);2546      L.marker([ev.lat, ev.lng]).addTo(m).bindPopup(`<b>${esc(ev.venue || ev.title)}</b><br>${esc([ev.address, ev.city].filter(Boolean).join(", "))}`);2547      setTimeout(() => m.invalidateSize(), 120);2548      SK_STATE.cleanup.push(() => { try{ m.remove(); }catch(_){} });2549    }).catch(() => { const w = $("sk-map"); if(w) w.parentNode.innerHTML = `<div class="sk-empty">Carte indisponible — vérifiez la connexion.</div>`; });2550  }25512552  // événements similaires : même catégorie prioritaire, sinon même ville2553  const cat = (ev.categories||[]).filter(c=>c!=="autre")[0] || "";2554  const q = new URLSearchParams({ limit: 7 });2555  if(cat) q.set("category", cat); else if(ev.city) q.set("city", ev.city);2556  if(ev.region && cat) q.set("region", ev.region);2557  try{2558    const sim = await (await fetch(`/api/events?${q}`)).json();2559    const others = sim.events.filter(e => e.uid !== ev.uid).slice(0, 6);2560    const host = $("d-similar");2561    if(host && SK_STATE.uid === ev.uid){ host.innerHTML = others.map(card).join("") || `<div class="sk-empty">Rien de similaire pour l'instant.</div>`; bindNav(host); favPaint(); }2562  }catch(e){ const host = $("d-similar"); if(host) host.innerHTML = ""; }2563}25642565/* « ✦ Demander à Ka » : bottom sheet de démarrage → widget KA Agent (ka-agent.js) */2566const SK_SUGS = ["Cet événement vaut-il le détour ?", "Que faire autour avant ou après ?", "Trouve des sorties semblables ce week-end", "Y a-t-il des billets moins chers ?", "Résume le programme"];2567function skEnvoyerKa(question){2568  const btn = document.querySelector(".kaa-btn"), ta = document.querySelector(".kaa-panel textarea"), send = document.querySelector(".kaa-in button");2569  if(!btn || !ta || !send) return false;2570  btn.click(); ta.value = question; setTimeout(() => send.click(), 60); return true;2571}2572function skOpenKa(ev){2573  const ctx = `(Événement consulté sur Sorti·Ka : ${ev.title}${ev.venue ? ` — ${ev.venue}` : ""}${ev.city ? `, ${ev.city}` : ""}${ev.start_date ? ` — ${fmtDate(ev.start_date, true)}` : ""} — https://www.sorti-ka.com/evenement/${encodeURIComponent(ev.uid)})`;2574  const old = $("sk-sheet"); if(old) old.remove();2575  const el = document.createElement("div"); el.id = "sk-sheet";2576  el.innerHTML = `<div class="sk-sheet-backdrop" data-close aria-hidden="true"></div>2577  <div class="sk-sheet" role="dialog" aria-modal="true" aria-label="Demander à Ka" tabindex="-1">2578    <div class="sk-sheet-handle"></div>2579    <div class="sk-sheet-head"><div><h3 class="sk-sheet-title">Demander à Ka</h3><p class="sk-sheet-sub">Assistant Groupe KA · connaît cette fiche</p></div><button type="button" class="sk-sheet-x" data-close aria-label="Fermer">✕</button></div>2580    <div class="sk-sheet-body">2581      <div class="sk-ka-intro"><span class="sk-ka-avatar" aria-hidden="true">${SK_ICO.spark}</span><p>Je peux situer cette sortie, proposer quoi faire autour, comparer les billets et chercher des événements semblables partout au Québec.</p></div>2582      <div class="sk-ka-ctx">${ev.image ? `<img src="${esc(ev.image)}" alt="">` : ""}<span style="min-width:0"><b>${esc(ev.title)}</b>${[ev.venue || ev.city, evDate(ev), skPrix(ev)].filter(Boolean).map(esc).join(" · ")}</span></div>2583      <div class="sk-ka-sugs">${SK_SUGS.map(s => `<button type="button" class="sk-ka-sug" data-q="${esc(s)}">${esc(s)} ${SK_ICO.chev}</button>`).join("")}</div>2584      <p class="sk-note warn hidden" id="sk-ka-err">L'assistant n'est pas encore chargé — réessayez dans un instant.</p>2585    </div>2586    <div class="sk-sheet-foot"><form class="sk-ka-in" id="sk-ka-form"><input placeholder="Posez votre question sur cette sortie…" aria-label="Votre question"><button type="submit" aria-label="Envoyer">${SK_ICO.chev}</button></form></div>2587  </div>`;2588  document.body.appendChild(el);2589  document.documentElement.classList.add("ka-scroll-lock");2590  const close = () => { el.remove(); document.documentElement.classList.remove("ka-scroll-lock"); window.removeEventListener("keydown", onKey); };2591  const onKey = e => { if(e.key === "Escape") close(); };2592  window.addEventListener("keydown", onKey);2593  el.querySelectorAll("[data-close]").forEach(b => b.onclick = close);2594  const poser = q => { if(skEnvoyerKa(`${q}\n\n${ctx}`)) close(); else $("sk-ka-err").classList.remove("hidden"); };2595  el.querySelectorAll(".sk-ka-sug").forEach(b => b.onclick = () => poser(b.dataset.q));2596  $("sk-ka-form").onsubmit = e => { e.preventDefault(); const v = e.target.querySelector("input").value.trim(); if(v) poser(v); };2597  setTimeout(() => el.querySelector(".sk-sheet").focus(), 30);2598}25992600/* ==================== vue /stats — module ka-stats (vanilla + SVG) =========2601   Reproduit le kit ka-ui/stats/kacharts.tsx : cartes KPI (delta ▲/▼),2602   sélecteur de période en chips, courbe avec infobulle + légende cliquable,2603   barres horizontales, anneau, heatmap calendrier 26 semaines, tableaux2604   triables/recherche/pagination 25, records, bouton PDF. Zéro dépendance. */2605const ST = { period:"30j", from:"", to:"", data:null, busy:false };2606const ST_PERIODS = [["auj","Aujourd'hui"],["7j","7 jours"],["30j","30 jours"],2607  ["3m","3 mois"],["6m","6 mois"],["12m","12 mois"],["annee","Année en cours"],["tout","Tout"]];2608const fmtI = n => Math.round(n).toLocaleString("fr-CA");2609const fmtN = n => Number.isInteger(n) ? fmtI(n)2610  : n.toLocaleString("fr-CA",{maximumFractionDigits:2});2611const ST_SHADES = [1,.78,.58,.42,.3,.22,.15,.1];26122613function stEmpty(title){2614  return `<div class="gk-card st-empty"><b>${esc(title)}</b>2615    <p class="klabel" style="margin:6px 0 0">Pas encore mesuré — aucune donnée disponible pour cette période.</p></div>`;2616}26172618function stParams(mode){2619  const p = new URLSearchParams();2620  p.set("period", ST.period);2621  if(ST.from && ST.to){ p.set("from", ST.from); p.set("to", ST.to); }2622  if(mode) p.set("mode", mode);2623  return p.toString();2624}26252626async function stFetch(){2627  if(ST.busy) return;2628  ST.busy = true;2629  $("st-kpis").innerHTML = `<div class="st-loading">Chargement des statistiques…</div>`;2630  try{2631    ST.data = await (await fetch(`/api/stats/dashboard?${stParams()}`)).json();2632    stRender();2633  }catch(e){2634    $("st-kpis").innerHTML = stEmpty("Statistiques indisponibles");2635  }2636  ST.busy = false;2637}26382639/* ---- sélecteur de période (chips + plage personnalisée) ---- */2640function stRenderPeriods(){2641  const custom = !!(ST.from && ST.to);2642  $("st-periods").innerHTML = ST_PERIODS.map(([id,lab]) =>2643    `<button type="button" class="chip${!custom && ST.period===id ? " on":""}" data-p="${id}" aria-pressed="${!custom && ST.period===id}">${lab}</button>`).join("")2644    + `<span class="st-custom">2645        <input class="input" type="date" id="st-from" value="${ST.from}" aria-label="Du">2646        <span class="klabel">au</span>2647        <input class="input" type="date" id="st-to" value="${ST.to}" aria-label="Au"></span>`;2648  $("st-periods").querySelectorAll(".chip[data-p]").forEach(ch => ch.onclick = () => {2649    ST.period = ch.dataset.p; ST.from = ""; ST.to = "";2650    stRenderPeriods(); stFetch();2651  });2652  const onDate = () => {2653    ST.from = $("st-from").value; ST.to = $("st-to").value;2654    if(ST.from && ST.to){ stRenderPeriods(); stFetch(); }2655  };2656  $("st-from").onchange = onDate; $("st-to").onchange = onDate;2657}26582659/* ---- sparkline (tendance dans les cartes KPI) ---- */2660function stSpark(points){2661  const pts = (points||[]).filter(p=>typeof p.v==="number");2662  if(pts.length < 2) return "";2663  const W=120, H=26, n=pts.length;2664  const vmax = Math.max(...pts.map(p=>p.v), 1), vmin = Math.min(0, ...pts.map(p=>p.v));2665  const X = i => 2 + (W-4)*i/(n-1);2666  const Y = v => 2 + (H-4)*(1-(v-vmin)/((vmax-vmin)||1));2667  const line = pts.map((p,i)=>`${i?"L":"M"}${X(i).toFixed(1)},${Y(p.v).toFixed(1)}`).join("");2668  return `<div class="spark" aria-hidden="true"><svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="none">2669    <path d="${line} L${W-2},${H-1} L2,${H-1} Z" fill="var(--accent)" fill-opacity=".12" stroke="none"/>2670    <path d="${line}" fill="none" stroke="var(--accent)" stroke-width="1.6"/></svg></div>`;2671}26722673/* ---- statistiques de série : min / max / moyenne / médiane / écart-type ---- */2674function stSummary(points, unit){2675  const vs = (points||[]).map(p=>p.v).filter(v=>typeof v==="number");2676  if(vs.length < 5) return "";2677  const n = vs.length, mean = vs.reduce((s,v)=>s+v,0)/n;2678  const sorted = [...vs].sort((a,b)=>a-b);2679  const med = n%2 ? sorted[(n-1)/2] : (sorted[n/2-1]+sorted[n/2])/2;2680  const sd = Math.sqrt(vs.reduce((s,v)=>s+(v-mean)**2,0)/n);2681  const u = unit ? ` ${esc(unit)}` : "";2682  const f = v => fmtN(Math.round(v*10)/10);2683  return `<div class="st-sum">2684    <span>min<b>${f(sorted[0])}${u}</b></span><span>max<b>${f(sorted[n-1])}${u}</b></span>2685    <span>moyenne<b>${f(mean)}${u}</b></span><span>médiane<b>${f(med)}${u}</b></span>2686    <span>écart-type<b>${f(sd)}</b></span></div>`;2687}26882689/* ---- KPI (delta ▲/▼ + sparkline) ---- */2690function stKpi(k){2691  let delta = "";2692  if(k.delta_pct !== null && k.delta_pct !== undefined){2693    const up = (k.direction || (k.delta_pct >= 0 ? "up" : "down")) === "up";2694    delta = `<div class="d" style="color:${up?"var(--green)":"var(--danger)"}">2695      ${up?"▲":"▼"} ${k.delta_pct>=0?"+":""}${fmtN(k.delta_pct)} % <span class="ctx">vs période préc.</span></div>`;2696  }2697  return `<article class="gk-card st-kpi">2698    <div class="v">${typeof k.value === "number" ? fmtN(k.value) : esc(k.value)}${k.unit?` <small>${esc(k.unit)}</small>`:""}</div>2699    <div class="klabel" style="margin-top:6px">${esc(k.label)}</div>${delta}${stSpark(k.spark)}</article>`;2700}27012702/* ---- jauge demi-arc (taux, couvertures, complétude) ---- */2703function stGauge(g){2704  const max = g.max || 100, frac = Math.max(0, Math.min(1, g.value/max));2705  const R = 34, C = Math.PI*R;2706  return `<article class="gk-card st-gauge" role="img"2707      aria-label="${esc(g.label)} : ${fmtN(g.value)}${g.unit?` ${esc(g.unit)}`:""} sur ${fmtN(max)}">2708    <svg viewBox="0 0 86 50">2709      <path d="M 9 44 A ${R} ${R} 0 0 1 77 44" fill="none" stroke="rgba(20,24,20,.09)" stroke-width="9" stroke-linecap="round"/>2710      <path d="M 9 44 A ${R} ${R} 0 0 1 77 44" fill="none" stroke="var(--accent)" stroke-width="9" stroke-linecap="round"2711        stroke-dasharray="${(frac*C).toFixed(1)} ${C.toFixed(1)}"/>2712      <path d="M 9 44 A ${R} ${R} 0 0 1 77 44" fill="none" stroke="var(--ink)" stroke-opacity=".25" stroke-width="1"/>2713    </svg>2714    <div><div class="gv">${fmtN(g.value)}${g.unit?`<small> ${esc(g.unit)}</small>`:""}</div>2715      <div class="klabel" style="margin-top:4px">${esc(g.label)}</div></div></article>`;2716}27172718/* ---- courbe (infobulle au survol + légende cliquable + N-1) ---- */2719function stLine(host, serie){2720  const stt = { cur:true, cmp:true };2721  const W=720, H=240, PL=54, PR=10, PT=14, PB=26;2722  const pts = serie.points || [];2723  if(pts.length < 2){ host.innerHTML = stEmpty(serie.title); return; }2724  const X = (i,n) => PL + (W-PL-PR)*i/(n-1);2725  const render = () => {2726    const cmp = stt.cmp && serie.compare && serie.compare.length > 1 ? serie.compare : null;2727    const all = [...(stt.cur?pts:[]), ...(cmp||[])];2728    const vmax = Math.max(...all.map(p=>p.v), 1);2729    const vmin = Math.min(0, ...all.map(p=>p.v));2730    const Y = v => PT + (H-PT-PB)*(1-(v-vmin)/((vmax-vmin)||1));2731    const path = s => s.map((p,i)=>`${i?"L":"M"}${X(i,s.length).toFixed(1)},${Y(p.v).toFixed(1)}`).join("");2732    let grid = "";2733    for(let g=0;g<5;g++){2734      const y = PT + (H-PT-PB)*g/4, v = vmax-(vmax-vmin)*g/4;2735      grid += `<line x1="${PL}" x2="${W-PR}" y1="${y}" y2="${y}" stroke="var(--line)" stroke-width="1"/>2736        <text x="${PL-6}" y="${y+3}" text-anchor="end" font-size="10" fill="var(--ink-3)" font-family="var(--font-mono)">${fmtI(v)}</text>`;2737    }2738    const xlab = [0, Math.floor(pts.length/2), pts.length-1].map(i =>2739      `<text x="${X(i,pts.length)}" y="${H-8}" text-anchor="middle" font-size="10" fill="var(--ink-3)" font-family="var(--font-mono)">${esc(pts[i].t)}</text>`).join("");2740    host.innerHTML = `<figure class="gk-card st-fig">2741      <figcaption><b>${esc(serie.title)}</b>2742        <span class="st-legend">2743          <button type="button" data-l="cur" class="${stt.cur?"":"off"}" aria-pressed="${stt.cur}"><span class="sw"></span>Période courante</button>2744          ${serie.compare?`<button type="button" data-l="cmp" class="${stt.cmp?"":"off"}" aria-pressed="${stt.cmp}"><span class="sw cmp"></span>Période comparée</button>`:""}2745        </span></figcaption>2746      <svg viewBox="0 0 ${W} ${H}" role="img" aria-label="${esc(serie.title)}">2747        ${grid}${xlab}2748        ${cmp?`<path d="${path(cmp)}" fill="none" stroke="var(--ink-3)" stroke-width="1.4" stroke-dasharray="4 4"/>`:""}2749        ${stt.cur?`<path d="${path(pts)}" fill="none" stroke="var(--accent)" stroke-width="2.4"/>`:""}2750        <g data-marker style="visibility:hidden">2751          <line y1="${PT}" y2="${H-PB}" stroke="var(--ink)" stroke-width="1" stroke-dasharray="2 3"/>2752          <circle r="4" fill="var(--accent)" stroke="var(--ink)" stroke-width="1.5"/>2753        </g>2754        <rect x="${PL}" y="0" width="${W-PL-PR}" height="${H}" fill="transparent"/>2755      </svg>2756      <p class="chip st-tip"></p>${stSummary(pts, serie.unit)}</figure>`;2757    const svg = host.querySelector("svg"), mk = svg.querySelector("[data-marker]"),2758      mline = mk.querySelector("line"), mdot = mk.querySelector("circle"),2759      tip = host.querySelector(".st-tip");2760    const move = clientX => {2761      const r = svg.getBoundingClientRect();2762      const fx = (clientX - r.left)/r.width*W;2763      const i = Math.min(pts.length-1, Math.max(0, Math.round((fx-PL)/(W-PL-PR)*(pts.length-1))));2764      const x = X(i, pts.length);2765      mk.style.visibility = "visible";2766      mline.setAttribute("x1",x); mline.setAttribute("x2",x);2767      mdot.setAttribute("cx",x); mdot.setAttribute("cy",Y(pts[i].v));2768      tip.classList.add("show");2769      tip.innerHTML = `${esc(pts[i].t)} — <b>&nbsp;${fmtN(pts[i].v)}${serie.unit?` ${esc(serie.unit)}`:""}</b>`2770        + (cmp && cmp[i] ? `<span style="color:var(--ink-3)">&nbsp;· N-1 : ${fmtN(cmp[i].v)}</span>` : "");2771    };2772    svg.addEventListener("mousemove", e => move(e.clientX));2773    svg.addEventListener("touchstart", e => move(e.touches[0].clientX), {passive:true});2774    svg.addEventListener("touchmove", e => move(e.touches[0].clientX), {passive:true});2775    svg.addEventListener("mouseleave", () => { mk.style.visibility="hidden"; tip.classList.remove("show"); });2776    host.querySelectorAll(".st-legend button").forEach(b => b.onclick = () => {2777      stt[b.dataset.l] = !stt[b.dataset.l];2778      if(!stt.cur && !stt.cmp) stt[b.dataset.l] = true;2779      render();2780    });2781  };2782  render();2783}27842785/* ---- barres horizontales (delta_pct optionnel par barre) ---- */2786function stBars(host, title, items, unit){2787  const rows = (items||[]).filter(r=>typeof r.value==="number").slice(0,14);2788  if(!rows.length){ host.innerHTML = stEmpty(title); return; }2789  const max = Math.max(...rows.map(r=>r.value), 1);2790  const delta = r => (r.delta_pct === null || r.delta_pct === undefined) ? "" :2791    `<span class="bd" style="color:${r.delta_pct>=0?"var(--green)":"var(--danger)"}">${r.delta_pct>=0?"▲ +":"▼ "}${fmtN(r.delta_pct)} %</span>`;2792  host.innerHTML = `<figure class="gk-card st-fig">2793    <figcaption><b>${esc(title)}</b></figcaption>2794    <div class="st-bars">${rows.map(r=>`2795      <div title="${esc(r.label)} — ${fmtN(r.value)}${unit?` ${unit}`:""}">2796        <div class="st-bar-lbl"><span>${esc(r.label)}</span><b>${fmtN(r.value)}${unit?` ${esc(unit)}`:""}</b>${delta(r)}</div>2797        <div class="st-bar-track"><div class="st-bar-fill" style="width:${Math.max(r.value/max*100,1)}%"></div></div>2798      </div>`).join("")}</div></figure>`;2799}28002801/* ---- barres verticales (volumes quotidiens & histogrammes) ---- */2802function stVBar(host, title, bins, unit, sub){2803  const rows = (bins||[]).filter(b=>typeof b.value==="number");2804  if(!rows.length || !rows.some(b=>b.value>0)){ host.innerHTML = stEmpty(title); return; }2805  const W=720, H=240, PL=54, PR=10, PT=14, PB=40;2806  const max = Math.max(...rows.map(b=>b.value), 1);2807  const n = rows.length, bw = (W-PL-PR)/n;2808  let grid = "";2809  for(let g=0; g<5; g++){2810    const y = PT + (H-PT-PB)*g/4, v = max*(4-g)/4;2811    grid += `<line x1="${PL}" x2="${W-PR}" y1="${y}" y2="${y}" stroke="var(--line)" stroke-width="1"/>2812      <text x="${PL-6}" y="${y+3}" text-anchor="end" font-size="10" fill="var(--ink-3)" font-family="var(--font-mono)">${fmtI(v)}</text>`;2813  }2814  const few = n <= 12;2815  const bars = rows.map((b,i)=>{2816    const h = (H-PT-PB)*b.value/max, x = PL + i*bw;2817    const lab = few ? `<text x="${x+bw/2}" y="${H-PB+14}" text-anchor="middle" font-size="9.5"2818        fill="var(--ink-3)" font-family="var(--font-mono)">${esc(String(b.label).slice(0,12))}</text>` :2819      (i===0 || i===n-1 || i===Math.floor(n/2) ?2820        `<text x="${x+bw/2}" y="${H-PB+14}" text-anchor="middle" font-size="9.5"2821          fill="var(--ink-3)" font-family="var(--font-mono)">${esc(String(b.label))}</text>` : "");2822    return `<rect x="${(x+bw*0.12).toFixed(1)}" y="${(H-PB-h).toFixed(1)}" width="${(bw*0.76).toFixed(1)}"2823      height="${Math.max(h, b.value>0?1.5:0).toFixed(1)}" fill="var(--accent)" stroke="var(--ink)" stroke-width=".8">2824      <title>${esc(b.label)} — ${fmtN(b.value)}${unit?` ${unit}`:""}</title></rect>${lab}`;2825  }).join("");2826  host.innerHTML = `<figure class="gk-card st-fig">2827    <figcaption><b>${esc(title)}</b>${sub?` <span class="klabel">${esc(sub)}</span>`:""}</figcaption>2828    <svg viewBox="0 0 ${W} ${H}" role="img" aria-label="${esc(title)}">${grid}${bars}</svg></figure>`;2829}28302831/* ---- multi-courbes ≤ 4 séries : motifs de trait distincts + légende ---- */2832const ST_DASH = ["", "7 4", "2 4", "10 4 2 4"];2833function stMulti(host, ms){2834  const sers = (ms.series||[]).slice(0,4).filter(s=>(s.points||[]).length>1);2835  if(!sers.length){ host.innerHTML = stEmpty(ms.title); return; }2836  const on = sers.map(()=>true);2837  const W=720, H=260, PL=54, PR=10, PT=14, PB=26;2838  const n = Math.max(...sers.map(s=>s.points.length));2839  const X = i => PL + (W-PL-PR)*i/(n-1);2840  const render = () => {2841    const vis = sers.filter((_,i)=>on[i]);2842    const all = vis.flatMap(s=>s.points.map(p=>p.v));2843    const vmax = Math.max(...(all.length?all:[1]), 1), vmin = Math.min(0, ...(all.length?all:[0]));2844    const Y = v => PT + (H-PT-PB)*(1-(v-vmin)/((vmax-vmin)||1));2845    let grid = "";2846    for(let g=0; g<5; g++){2847      const y = PT + (H-PT-PB)*g/4, v = vmax-(vmax-vmin)*g/4;2848      grid += `<line x1="${PL}" x2="${W-PR}" y1="${y}" y2="${y}" stroke="var(--line)" stroke-width="1"/>2849        <text x="${PL-6}" y="${y+3}" text-anchor="end" font-size="10" fill="var(--ink-3)" font-family="var(--font-mono)">${fmtI(v)}</text>`;2850    }2851    const ts = sers[0].points;2852    const xlab = [0, Math.floor(ts.length/2), ts.length-1].map(i =>2853      `<text x="${X(i)}" y="${H-8}" text-anchor="middle" font-size="10" fill="var(--ink-3)" font-family="var(--font-mono)">${esc(ts[i].t)}</text>`).join("");2854    const paths = sers.map((s,si)=> !on[si] ? "" :2855      `<path d="${s.points.map((p,i)=>`${i?"L":"M"}${X(i).toFixed(1)},${Y(p.v).toFixed(1)}`).join("")}"2856        fill="none" stroke="var(--accent)" stroke-opacity="${(1-si*0.16).toFixed(2)}" stroke-width="2.2"2857        ${ST_DASH[si]?`stroke-dasharray="${ST_DASH[si]}"`:""}/>`).join("");2858    host.innerHTML = `<figure class="gk-card st-fig">2859      <figcaption><b>${esc(ms.title)}</b>2860        <span class="st-legend">${sers.map((s,i)=>2861          `<button type="button" data-i="${i}" class="${on[i]?"":"off"}" aria-pressed="${on[i]}">2862            <span class="sw ${i?`p${i}`:""}"></span>${esc(s.label)}</button>`).join("")}</span></figcaption>2863      <svg viewBox="0 0 ${W} ${H}" role="img" aria-label="${esc(ms.title)}">2864        ${grid}${xlab}${paths}2865        <g data-marker style="visibility:hidden">2866          <line y1="${PT}" y2="${H-PB}" stroke="var(--ink)" stroke-width="1" stroke-dasharray="2 3"/></g>2867        <rect x="${PL}" y="0" width="${W-PL-PR}" height="${H}" fill="transparent"/>2868      </svg><p class="chip st-tip"></p></figure>`;2869    const svg = host.querySelector("svg"), mk = svg.querySelector("[data-marker]"),2870      mline = mk.querySelector("line"), tip = host.querySelector(".st-tip");2871    const move = clientX => {2872      const r = svg.getBoundingClientRect();2873      const fx = (clientX - r.left)/r.width*W;2874      const i = Math.min(n-1, Math.max(0, Math.round((fx-PL)/(W-PL-PR)*(n-1))));2875      mk.style.visibility = "visible";2876      mline.setAttribute("x1",X(i)); mline.setAttribute("x2",X(i));2877      tip.classList.add("show");2878      tip.innerHTML = `${esc(ts[i]?ts[i].t:"")} — ` + sers.map((s,si)=> !on[si] ? "" :2879        `<span style="margin-left:8px">${esc(s.label)} : <b>${s.points[i]?fmtN(s.points[i].v):"—"}</b></span>`).join("");2880    };2881    svg.addEventListener("mousemove", e => move(e.clientX));2882    svg.addEventListener("touchstart", e => move(e.touches[0].clientX), {passive:true});2883    svg.addEventListener("touchmove", e => move(e.touches[0].clientX), {passive:true});2884    svg.addEventListener("mouseleave", () => { mk.style.visibility="hidden"; tip.classList.remove("show"); });2885    host.querySelectorAll(".st-legend button").forEach(b => b.onclick = () => {2886      const i = Number(b.dataset.i);2887      on[i] = !on[i];2888      if(!on.some(Boolean)) on[i] = true;2889      render();2890    });2891  };2892  render();2893}28942895/* ---- barres empilées (composition dans le temps, nuances d'accent) ---- */2896function stStacked(host, sp){2897  const keys = sp.keys || [], pts = sp.points || [];2898  if(!keys.length || !pts.length || !pts.some(p=>(p.values||[]).some(v=>v>0))){2899    host.innerHTML = stEmpty(sp.title); return;2900  }2901  const W=720, H=260, PL=54, PR=10, PT=14, PB=26;2902  const totals = pts.map(p=>(p.values||[]).reduce((s,v)=>s+(v||0),0));2903  const max = Math.max(...totals, 1);2904  const n = pts.length, bw = (W-PL-PR)/n;2905  let grid = "";2906  for(let g=0; g<5; g++){2907    const y = PT + (H-PT-PB)*g/4, v = max*(4-g)/4;2908    grid += `<line x1="${PL}" x2="${W-PR}" y1="${y}" y2="${y}" stroke="var(--line)" stroke-width="1"/>2909      <text x="${PL-6}" y="${y+3}" text-anchor="end" font-size="10" fill="var(--ink-3)" font-family="var(--font-mono)">${fmtI(v)}</text>`;2910  }2911  const xlab = [0, Math.floor(n/2), n-1].map(i =>2912    `<text x="${PL+i*bw+bw/2}" y="${H-8}" text-anchor="middle" font-size="10" fill="var(--ink-3)" font-family="var(--font-mono)">${esc(pts[i].t)}</text>`).join("");2913  let bars = "";2914  pts.forEach((p,i)=>{2915    let yTop = H-PB;2916    const tt = `${p.t} — total ${fmtN(totals[i])}${sp.unit?` ${sp.unit}`:""} · ` +2917      keys.map((k,ki)=>`${k} : ${fmtN((p.values||[])[ki]||0)}`).join(" · ");2918    (p.values||[]).forEach((v,ki)=>{2919      const h = (H-PT-PB)*(v||0)/max;2920      if(h>0){2921        yTop -= h;2922        bars += `<rect x="${(PL+i*bw+bw*0.1).toFixed(1)}" y="${yTop.toFixed(1)}" width="${(bw*0.8).toFixed(1)}"2923          height="${h.toFixed(1)}" fill="var(--accent)" fill-opacity="${ST_SHADES[ki%8]}"2924          stroke="var(--ink)" stroke-width=".6"><title>${esc(tt)}</title></rect>`;2925      }2926    });2927  });2928  host.innerHTML = `<figure class="gk-card st-fig">2929    <figcaption><b>${esc(sp.title)}</b>2930      <span class="st-legend">${keys.map((k,i)=>2931        `<button type="button" disabled style="cursor:default"><span class="sw fill" style="opacity:${ST_SHADES[i%8]}"></span>${esc(k)}</button>`).join("")}</span></figcaption>2932    <svg viewBox="0 0 ${W} ${H}" role="img" aria-label="${esc(sp.title)}">${grid}${xlab}${bars}</svg></figure>`;2933}29342935/* ---- heatmap horaire 7 jours × 24 h (dow 0 = lundi) ---- */2936function stHourly(host, h){2937  const cells = h.cells || [];2938  if(!cells.length){ host.innerHTML = stEmpty(h.title); return; }2939  const grid = Array.from({length:7}, ()=>Array(24).fill(0));2940  cells.forEach(c => { if(c.dow>=0 && c.dow<7 && c.hour>=0 && c.hour<24) grid[c.dow][c.hour] = c.value; });2941  const max = Math.max(...cells.map(c=>c.value), 1);2942  const DAYS = ["Lun","Mar","Mer","Jeu","Ven","Sam","Dim"];2943  const CW=17, CH=17, OX=38, OY=16;2944  let rects = "";2945  for(let d=0; d<7; d++){2946    rects += `<text x="${OX-6}" y="${OY+d*CH+12}" text-anchor="end" font-size="9.5" fill="var(--ink-3)" font-family="var(--font-mono)">${DAYS[d]}</text>`;2947    for(let hr=0; hr<24; hr++){2948      const v = grid[d][hr];2949      rects += `<rect x="${OX+hr*CW}" y="${OY+d*CH}" width="${CW-2}" height="${CH-2}" rx="2.5"2950        fill="${v?"var(--accent)":"rgba(20,24,20,0.07)"}" fill-opacity="${v?(.22+.78*v/max).toFixed(3):1}"2951        stroke="rgba(20,24,20,0.15)" stroke-width=".5">2952        <title>${DAYS[d]} ${hr} h — ${fmtN(v)} événement${v>1?"s":""}</title></rect>`;2953    }2954  }2955  for(let hr=0; hr<24; hr+=3){2956    rects += `<text x="${OX+hr*CW+(CW-2)/2}" y="${OY-5}" text-anchor="middle" font-size="9" fill="var(--ink-3)" font-family="var(--font-mono)">${hr} h</text>`;2957  }2958  host.innerHTML = `<figure class="gk-card st-fig">2959    <figcaption><b>${esc(h.title)}</b></figcaption>2960    <div class="tbl-wrap" style="margin-top:12px">2961      <svg viewBox="0 0 ${OX+24*CW} ${OY+7*CH}" style="min-width:480px" role="img" aria-label="${esc(h.title)}">${rects}</svg>2962    </div></figure>`;2963}29642965/* ---- anneau ---- */2966function stDonut(host, title, items){2967  const rows = (items||[]).filter(r=>r.value>0).slice(0,8);2968  const total = rows.reduce((s,r)=>s+r.value,0);2969  if(!total){ host.innerHTML = stEmpty(title); return; }2970  const R = 74, C = 2*Math.PI*R;2971  let acc = 0;2972  const segs = rows.map((r,i)=>{2973    const frac = r.value/total, off = acc; acc += frac;2974    return `<circle cx="100" cy="100" r="${R}" fill="none" stroke="var(--accent)"2975      stroke-opacity="${ST_SHADES[i%8]}" stroke-width="30"2976      stroke-dasharray="${(frac*C).toFixed(2)} ${C.toFixed(2)}" stroke-dashoffset="${(-off*C).toFixed(2)}"2977      transform="rotate(-90 100 100)"><title>${esc(r.label)} — ${fmtN(r.value)} (${(100*r.value/total).toFixed(1)} %)</title></circle>`;2978  }).join("");2979  host.innerHTML = `<figure class="gk-card st-fig">2980    <figcaption><b>${esc(title)}</b></figcaption>2981    <div class="st-donut-wrap">2982      <svg viewBox="0 0 200 200" role="img" aria-label="${esc(title)}">${segs}2983        <circle cx="100" cy="100" r="${R}" fill="none" stroke="var(--ink)" stroke-width="1" opacity=".5"/></svg>2984      <ul>${rows.map((r,i)=>`<li><span class="sw" style="opacity:${ST_SHADES[i%8]}"></span>2985        <span class="lb">${esc(r.label)}</span><b>${(100*r.value/total).toFixed(1).replace(".",",")} %</b></li>`).join("")}</ul>2986    </div></figure>`;2987}29882989/* ---- heatmap calendrier (26 semaines) ---- */2990function stHeat(host, hm){2991  const cells = hm.cells || [];2992  if(!cells.length){ host.innerHTML = stEmpty(hm.title); return; }2993  const max = Math.max(...cells.map(c=>c.value), 1);2994  const weeks = Math.ceil(cells.length/7);2995  let rects = "";2996  cells.forEach((c,i)=>{2997    const w = Math.floor(i/7), d = i%7;2998    rects += `<rect x="${w*14}" y="${d*14}" width="12" height="12" rx="2.5"2999      fill="${c.value?"var(--accent)":"rgba(20,24,20,0.07)"}"3000      fill-opacity="${c.value?(.25+.75*c.value/max).toFixed(3):1}"3001      stroke="rgba(20,24,20,0.15)" stroke-width=".5"><title>${esc(c.date)} — ${fmtN(c.value)} événement${c.value>1?"s":""}</title></rect>`;3002  });3003  host.innerHTML = `<figure class="gk-card st-fig">3004    <figcaption><b>${esc(hm.title)}</b> <span class="klabel">26 prochaines semaines · ${esc(cells[0].date)} → ${esc(cells[cells.length-1].date)}</span></figcaption>3005    <div class="tbl-wrap" style="margin-top:12px">3006      <svg viewBox="0 0 ${weeks*14} ${7*14}" style="min-width:480px" role="img" aria-label="${esc(hm.title)}">${rects}</svg>3007    </div></figure>`;3008}30093010/* ---- tableau : tri par colonne, recherche, pagination 25 ---- */3011function stTable(host, spec){3012  const stt = { q:"", col:null, dir:1, page:0 };3013  const num = x => typeof x === "number" ? x3014    : parseFloat(String(x).replace(/[^\d.,-]/g,"").replace(",","."));3015  const render = () => {3016    let rows = spec.rows || [];3017    if(stt.q){3018      const q = stt.q.toLowerCase();3019      rows = rows.filter(r => r.some(c => String(c).toLowerCase().includes(q)));3020    }3021    if(stt.col !== null){3022      rows = [...rows].sort((a,b)=>{3023        const nx = num(a[stt.col]), ny = num(b[stt.col]);3024        if(!Number.isNaN(nx) && !Number.isNaN(ny)) return (nx-ny)*stt.dir;3025        return String(a[stt.col]).localeCompare(String(b[stt.col]),"fr")*stt.dir;3026      });3027    }3028    const pages = Math.max(1, Math.ceil(rows.length/25));3029    const cur = Math.min(stt.page, pages-1);3030    host.innerHTML = `<section class="gk-card st-fig">3031      <div class="st-tbl-head"><b style="font-family:var(--font-display);font-size:15px">${esc(spec.title)}</b>3032        <input class="input" placeholder="Rechercher…" value="${esc(stt.q)}" aria-label="Rechercher dans ${esc(spec.title)}"></div>3033      <div class="tbl-wrap" style="margin-top:10px"><table class="st-table">3034        <thead><tr>${spec.columns.map((c,i)=>3035          `<th data-c="${i}" aria-sort="${stt.col===i?(stt.dir===1?"ascending":"descending"):"none"}">${esc(c)} ${stt.col===i?(stt.dir===1?"▲":"▼"):"↕"}</th>`).join("")}</tr></thead>3036        <tbody>${rows.slice(cur*25,(cur+1)*25).map(r=>3037          `<tr>${r.map(c=>`<td>${typeof c==="number"?fmtN(c):esc(c)}</td>`).join("")}</tr>`).join("")}</tbody>3038      </table></div>3039      <div class="st-tbl-foot"><span class="klabel">${fmtI(rows.length)} lignes</span>3040        <span class="pg">3041          <button type="button" class="btn btn-ghost" data-pg="-1" ${cur===0?"disabled":""}>←</button>3042          <span class="chip">${cur+1} / ${pages}</span>3043          <button type="button" class="btn btn-ghost" data-pg="1" ${cur>=pages-1?"disabled":""}>→</button>3044        </span></div></section>`;3045    const inp = host.querySelector("input");3046    let t;3047    inp.addEventListener("input", e => {3048      clearTimeout(t);3049      t = setTimeout(()=>{ stt.q = e.target.value; stt.page = 0; render();3050        host.querySelector("input").focus(); }, 250);3051    });3052    host.querySelectorAll("th").forEach(th => th.onclick = () => {3053      const c = Number(th.dataset.c);3054      stt.dir = stt.col === c && stt.dir === 1 ? -1 : 1;3055      stt.col = c; render();3056    });3057    host.querySelectorAll("[data-pg]").forEach(b => b.onclick = () => {3058      stt.page = cur + Number(b.dataset.pg); render();3059    });3060  };3061  render();3062}30633064/* ---- rendu global ---- */3065function stRender(){3066  const d = ST.data;3067  if(!d) return;3068  const upd = new Date(d.updated);3069  $("st-fresh").innerHTML = `<span class="klabel">Mis à jour le3070      ${upd.toLocaleString("fr-CA",{dateStyle:"medium",timeStyle:"short"})}3071      · période : ${esc((d.period||{}).label||"")}</span>3072    <button type="button" class="btn btn-ghost" id="st-refresh">↻ Rafraîchir</button>`;3073  $("st-refresh").onclick = stFetch;30743075  $("st-kpis").innerHTML = (d.kpis||[]).map(stKpi).join("") || stEmpty("Indicateurs");3076  $("st-gauges").innerHTML = (d.gauges||[]).map(stGauge).join("");30773078  const mount = (parent) => { const el = document.createElement("div"); parent.appendChild(el); return el; };3079  $("st-series").innerHTML = "";3080  (d.series||[]).forEach(s => {3081    const el = mount($("st-series"));3082    if(s.kind === "bar") stVBar(el, s.title, (s.points||[]).map(p=>({label:p.t,value:p.v})), s.unit);3083    else stLine(el, s);3084  });30853086  $("st-multi").innerHTML = "";3087  (d.multiseries||[]).forEach(ms => stMulti(mount($("st-multi")), ms));3088  (d.stacked||[]).forEach(sp => stStacked(mount($("st-multi")), sp));30893090  $("st-breaks").innerHTML = "";3091  (d.breakdowns||[]).filter(b=>b.id!=="sources").forEach(b => {3092    const el = mount($("st-breaks"));3093    if(b.kind === "donut") stDonut(el, b.title, b.items);3094    else stBars(el, b.title, b.items);3095  });3096  (d.distributions||[]).forEach(di =>3097    stVBar(mount($("st-breaks")), di.title, di.bins, di.unit));30983099  $("st-geosrc").innerHTML = "";3100  if(d.geo) stBars(mount($("st-geosrc")), d.geo.title || "Par région", d.geo.items);3101  const src = (d.breakdowns||[]).find(b=>b.id==="sources");3102  if(src) stBars(mount($("st-geosrc")), src.title, src.items);31033104  $("st-heat").innerHTML = "";3105  if(d.heatmap) stHeat(mount($("st-heat")), d.heatmap);31063107  $("st-hourly").innerHTML = "";3108  if(d.hourly) stHourly(mount($("st-hourly")), d.hourly);31093110  $("st-tables").innerHTML = "";3111  (d.tables||[]).forEach(t => stTable(mount($("st-tables")), t));31123113  const recs = d.records || [];3114  $("st-records-wrap").classList.toggle("hidden", !recs.length);3115  $("st-records").innerHTML = recs.map(r => `<article class="gk-card st-record">3116    <span class="lb">${esc(r.label)}</span>3117    <span class="rv"><b>${esc(r.value)}</b>${r.date?`<span class="klabel">${esc(r.date)}</span>`:""}</span></article>`).join("");3118}31193120/* ---- boutons PDF : rapport complet + menu des 4 autres rapports ---- */3121function stPdf(mode, btn){3122  const original = btn.textContent;3123  btn.disabled = true; btn.textContent = "Génération…";3124  const a = document.createElement("a");3125  a.href = `/api/stats/report?${stParams(mode)}`;3126  a.download = "";3127  document.body.appendChild(a); a.click(); a.remove();3128  setTimeout(()=>{ btn.disabled = false; btn.textContent = original; }, 2500);3129}31303131function stPdfMenu(open){3132  const drop = $("st-pdf-drop"), more = $("st-pdf-more");3133  const on = open === undefined ? drop.classList.contains("hidden") : open;3134  drop.classList.toggle("hidden", !on);3135  more.setAttribute("aria-expanded", String(on));3136}3137$("st-pdf-more").onclick = e => { e.stopPropagation(); stPdfMenu(); };3138$("st-pdf-drop").querySelectorAll("[data-mode]").forEach(b => b.onclick = e => {3139  stPdfMenu(false);3140  stPdf(b.dataset.mode, $("st-pdf-more"));3141});3142document.addEventListener("click", e => {3143  if(!e.target.closest(".st-pdf-menu")) stPdfMenu(false);3144});31453146function showStats(){3147  document.title = "Statistiques — Sorti-Ka | Un service Groupe KA";3148  show("view-stats");3149  window.scrollTo({top:0});3150  if(!ST.data) stFetch();3151}31523153$("st-pdf-full").onclick = e => stPdf("complet", e.currentTarget);3154stRenderPeriods();31553156/* ==================== v3 : constructeur de rapports personnalisés ==========3157   Compose un PDF bloc par bloc : choix des données (catalogue du dashboard),3158   du rendu par bloc (courbe/aire/barres/anneau/heatmap/tableau…), de l'ordre,3159   et modèles sauvegardés (localStorage, propre au site = origine). */3160const SB = { cat:null, sel:[], title:"", busy:false, err:"" };3161const SB_TPL_KEY = "ka-stats-rapports";3162const SB_RENDERS = { line:"Courbe", area:"Aire", bar:"Barres verticales",3163  bars:"Barres horizontales", donut:"Anneau", lines:"Multi-courbes",3164  stacked:"Barres empilées", histogram:"Histogramme", heatmap:"Heatmap",3165  cards:"Cartes", gauges:"Jauges", table:"Tableau" };3166const SB_SECTIONS = { kpis:"Indicateurs", gauges:"Jauges", series:"Évolution",3167  multiseries:"Multi-courbes", stacked:"Compositions", breakdowns:"Répartitions",3168  distributions:"Distributions", geo:"Géographie", heatmap:"Calendrier",3169  hourly:"Activité horaire", tables:"Tableaux", records:"Records" };31703171function sbTemplates(){3172  try{ return JSON.parse(localStorage.getItem(SB_TPL_KEY) || "[]"); }3173  catch(e){ return []; }3174}3175function sbSaveTemplates(t){ localStorage.setItem(SB_TPL_KEY, JSON.stringify(t)); }31763177async function sbOpen(){3178  let ov = $("st-builder");3179  if(!ov){3180    ov = document.createElement("div");3181    ov.id = "st-builder";3182    ov.className = "stb-overlay hidden";3183    ov.onclick = e => { if(e.target === ov) sbClose(); };3184    document.body.appendChild(ov);3185  }3186  ov.classList.remove("hidden");3187  document.body.style.overflow = "hidden";3188  SB.err = "";3189  if(!SB.cat){3190    ov.innerHTML = `<div class="stb-panel"><div class="stb-head">3191      <h2>Rapport personnalisé</h2></div>3192      <div class="stb-col"><p class="klabel">Chargement du catalogue…</p></div></div>`;3193    try{3194      SB.cat = (await (await fetch(`/api/stats/catalog?${stParams()}`)).json()).blocks || [];3195    }catch(e){3196      ov.innerHTML = `<div class="stb-panel"><div class="stb-head"><h2>Rapport personnalisé</h2>3197        <button type="button" class="btn btn-ghost" onclick="sbClose()">✕ Fermer</button></div>3198        <div class="stb-col"><p class="stb-err">Catalogue indisponible — réessayez plus tard.</p></div></div>`;3199      return;3200    }3201  }3202  sbRender();3203}3204function sbClose(){3205  const ov = $("st-builder");3206  if(ov) ov.classList.add("hidden");3207  document.body.style.overflow = "";3208}32093210function sbAdd(key){3211  const b = SB.cat.find(x => x.key === key);3212  if(!b || SB.sel.some(s => s.key === key && s.render === b.default_render)) return;3213  SB.sel.push({ key, render: b.default_render });3214  sbRender();3215}3216function sbDel(i){ SB.sel.splice(i, 1); sbRender(); }3217function sbMove(i, d){3218  const j = i + d;3219  if(j < 0 || j >= SB.sel.length) return;3220  [SB.sel[i], SB.sel[j]] = [SB.sel[j], SB.sel[i]];3221  sbRender();3222}3223function sbAll(){3224  SB.sel = SB.cat.map(b => ({ key: b.key, render: b.default_render }));3225  sbRender();3226}32273228function sbRender(){3229  const ov = $("st-builder");3230  const groups = {};3231  SB.cat.forEach(b => (groups[b.section] = groups[b.section] || []).push(b));3232  const selKeys = new Set(SB.sel.map(s => s.key));3233  const perLabel = (ST.from && ST.to) ? `${ST.from} → ${ST.to}`3234    : (ST_PERIODS.find(p => p[0] === ST.period) || ["","30 jours"])[1];3235  const tpls = sbTemplates();32363237  ov.innerHTML = `<div class="stb-panel">3238    <div class="stb-head">3239      <h2>Rapport personnalisé <span class="klabel">· période : ${esc(perLabel)}</span></h2>3240      <button type="button" class="btn btn-ghost" id="stb-close">✕ Fermer</button>3241    </div>3242    <div class="stb-body">3243      <div class="stb-col">3244        <h3>Blocs disponibles (${SB.cat.length})</h3>3245        <p class="klabel" style="margin:0 0 10px">Cliquez « + » pour ajouter un bloc au rapport.3246          Un même bloc peut être ajouté sous plusieurs rendus.</p>3247        ${Object.entries(groups).map(([sec, bs]) => `3248          <p class="stb-group">${esc(SB_SECTIONS[sec] || sec)}</p>3249          ${bs.map(b => `<div class="stb-av${selKeys.has(b.key) ? " stb-in" : ""}">3250            <span style="min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"3251              title="${esc(b.title)}">${esc(b.title)}</span>3252            <button type="button" class="btn btn-ghost" data-add="${esc(b.key)}"3253              aria-label="Ajouter ${esc(b.title)}">+</button></div>`).join("")}`).join("")}3254      </div>3255      <div class="stb-col">3256        <h3>Composition du rapport (${SB.sel.length})</h3>3257        <label class="klabel" for="stb-title">Titre du rapport</label>3258        <input class="input" id="stb-title" style="width:100%;margin:4px 0 12px"3259          maxlength="80" placeholder="Ex. : Revue mensuelle" value="${esc(SB.title)}">3260        ${SB.sel.length ? SB.sel.map((s, i) => {3261          const b = SB.cat.find(x => x.key === s.key) || { title: s.key, renders: [s.render] };3262          return `<div class="stb-item">3263            <button type="button" class="mv" data-up="${i}" ${i === 0 ? "disabled" : ""} aria-label="Monter">▲</button>3264            <button type="button" class="mv" data-dn="${i}" ${i === SB.sel.length - 1 ? "disabled" : ""} aria-label="Descendre">▼</button>3265            <span class="t" title="${esc(b.title)}"><b>${i + 1}.</b> ${esc(b.title)}</span>3266            ${b.renders.length > 1 ? `<select data-rd="${i}" aria-label="Rendu">3267              ${b.renders.map(r => `<option value="${r}"${r === s.render ? " selected" : ""}>${SB_RENDERS[r] || r}</option>`).join("")}3268            </select>` : `<span class="klabel">${SB_RENDERS[s.render] || s.render}</span>`}3269            <button type="button" class="mv" data-del="${i}" aria-label="Retirer">✕</button>3270          </div>`;3271        }).join("") : `<div class="stb-empty">Aucun bloc — ajoutez des blocs depuis la colonne de gauche,3272          ou chargez un modèle ci-dessous.</div>`}3273        <p style="display:flex;gap:8px;margin:10px 0 0">3274          <button type="button" class="btn btn-ghost" id="stb-all">Tout ajouter</button>3275          <button type="button" class="btn btn-ghost" id="stb-clear" ${SB.sel.length ? "" : "disabled"}>Vider</button>3276        </p>3277        ${SB.err ? `<p class="stb-err">${esc(SB.err)}</p>` : ""}3278      </div>3279    </div>3280    <div class="stb-foot">3281      <span class="stb-tpl">3282        <select class="input" id="stb-tpl" aria-label="Modèles sauvegardés" style="max-width:210px">3283          <option value="">Modèles (${tpls.length})…</option>3284          ${tpls.map((t, i) => `<option value="${i}">${esc(t.name)}</option>`).join("")}3285        </select>3286        <button type="button" class="btn btn-ghost" id="stb-save" ${SB.sel.length ? "" : "disabled"}>💾 Sauvegarder</button>3287        <button type="button" class="btn btn-ghost" id="stb-tpl-del" ${tpls.length ? "" : "disabled"}>🗑 Supprimer</button>3288      </span>3289      <button type="button" class="btn btn-primary" id="stb-go" ${SB.sel.length && !SB.busy ? "" : "disabled"}>3290        ${SB.busy ? "Génération…" : "⬇ Générer le PDF"}</button>3291    </div>3292  </div>`;32933294  $("stb-close").onclick = sbClose;3295  $("stb-all").onclick = sbAll;3296  $("stb-clear").onclick = () => { SB.sel = []; sbRender(); };3297  $("stb-title").oninput = e => { SB.title = e.target.value; };3298  ov.querySelectorAll("[data-add]").forEach(b => b.onclick = () => sbAdd(b.dataset.add));3299  ov.querySelectorAll("[data-up]").forEach(b => b.onclick = () => sbMove(+b.dataset.up, -1));3300  ov.querySelectorAll("[data-dn]").forEach(b => b.onclick = () => sbMove(+b.dataset.dn, 1));3301  ov.querySelectorAll("[data-del]").forEach(b => b.onclick = () => sbDel(+b.dataset.del));3302  ov.querySelectorAll("[data-rd]").forEach(sl => sl.onchange = () => {3303    SB.sel[+sl.dataset.rd].render = sl.value;3304  });3305  $("stb-save").onclick = () => {3306    const name = prompt("Nom du modèle :", SB.title || "Mon rapport");3307    if(!name) return;3308    const tpl = sbTemplates().filter(t => t.name !== name);3309    tpl.push({ name, title: SB.title, blocks: SB.sel.map(s => ({...s})) });3310    sbSaveTemplates(tpl);3311    sbRender();3312  };3313  $("stb-tpl").onchange = e => {3314    const t = sbTemplates()[+e.target.value];3315    if(!t) return;3316    SB.title = t.title || t.name;3317    SB.sel = (t.blocks || []).filter(s => SB.cat.some(b => b.key === s.key)).map(s => ({...s}));3318    sbRender();3319  };3320  $("stb-tpl-del").onclick = () => {3321    const sel = $("stb-tpl");3322    const t = sbTemplates()[+sel.value];3323    if(!t || !confirm(`Supprimer le modèle « ${t.name} » ?`)) return;3324    sbSaveTemplates(sbTemplates().filter(x => x.name !== t.name));3325    sbRender();3326  };3327  $("stb-go").onclick = sbGenerate;3328}33293330async function sbGenerate(){3331  if(SB.busy || !SB.sel.length) return;3332  SB.busy = true; SB.err = ""; sbRender();3333  try{3334    const body = { title: SB.title, period: ST.period, blocks: SB.sel };3335    if(ST.from && ST.to){ body.from = ST.from; body.to = ST.to; }3336    const r = await fetch("/api/stats/report/custom", {3337      method: "POST", headers: { "Content-Type": "application/json" },3338      body: JSON.stringify(body) });3339    if(!r.ok) throw new Error(`HTTP ${r.status}`);3340    const blob = await r.blob();3341    const cd = r.headers.get("Content-Disposition") || "";3342    const m = cd.match(/filename="?([^";]+)/);3343    const a = document.createElement("a");3344    a.href = URL.createObjectURL(blob);3345    a.download = m ? m[1] : "rapport-personnalise.pdf";3346    document.body.appendChild(a); a.click(); a.remove();3347    setTimeout(() => URL.revokeObjectURL(a.href), 4000);3348  }catch(e){3349    SB.err = "La génération a échoué — réessayez.";3350  }3351  SB.busy = false; sbRender();3352}33533354$("st-custom-btn").onclick = () => { SB.cat = null; SB.err = ""; sbOpen(); };33553356/* ---------- favoris ♥ « Mon univers Ka » ----------3357   Le hub Groupe KA (groupe-ka.com) est le magasin central des favoris du3358   groupe : AUCUN stockage navigateur. Lecture/écriture via l'API locale3359   /api/favorites, qui relaie au hub (session KA ID requise). */3360const FAV = {ready:false, connected:false, ids:new Set(), items:[], meta:{}};3361const FAV_HEART = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>`;33623363/* événement -> item de favori poussé au hub (titre, lieu · date, prix, image) */3364function favItem(ev){3365  let price = "";3366  if(ev.is_free === true) price = "Gratuit";3367  else if(ev.price_min != null) price = `Dès ${ev.price_min.toFixed(2).replace(".",",")} $`;3368  return {3369    item_id: ev.uid,3370    title: ev.title || "Événement",3371    subtitle: [ev.venue || ev.city, dateBadge(ev)].filter(Boolean).join(" · "),3372    price_label: price,3373    image_url: ev.image || "",3374    url: `https://www.sorti-ka.com/evenement/${encodeURIComponent(ev.uid)}`,3375  };3376}33773378/* cœur ♥ — cartes (par défaut) et fiche événement (big) */3379function favBtn(ev, big=false){3380  FAV.meta[ev.uid] = favItem(ev);3381  const on = FAV.ids.has(ev.uid);3382  const label = on ? "Retirer de mes favoris (Mon univers Ka)"3383                   : "Ajouter à mes favoris (Mon univers Ka)";3384  return `<button type="button" class="fav-btn${on?" on":""}${big?" big":""}"3385    data-fav="${esc(ev.uid)}" aria-pressed="${on}" aria-label="${label}"3386    title="${label}">${FAV_HEART}</button>`;3387}33883389/* repeint tous les cœurs affichés selon l'état courant */3390function favPaint(){3391  document.querySelectorAll(".fav-btn[data-fav]").forEach(b => {3392    const on = FAV.ids.has(b.dataset.fav);3393    b.classList.toggle("on", on);3394    b.setAttribute("aria-pressed", on);3395    const label = on ? "Retirer de mes favoris (Mon univers Ka)"3396                     : "Ajouter à mes favoris (Mon univers Ka)";3397    b.setAttribute("aria-label", label); b.title = label;3398  });3399}34003401/* lit la liste au hub (via l'API locale) — 401 = pas connecté, cœurs vides */3402async function favLoad(){3403  try{3404    const r = await fetch("/api/favorites");3405    if(r.status === 401){ FAV.connected = false; return; }3406    if(!r.ok) return;3407    const d = await r.json();3408    FAV.connected = true;3409    FAV.items = d.items || [];3410    FAV.ids = new Set(d.ids || []);3411    favPaint();3412  }catch(e){}3413  finally{ FAV.ready = true; }3414}34153416/* toggle optimiste — le hub confirme (sinon on rétablit) */3417async function favToggle(uid){3418  if(!FAV.connected){ location.href = "/api/auth/ka/login"; return; }3419  const on = !FAV.ids.has(uid);3420  const item = FAV.meta[uid] || FAV.items.find(i => i.item_id === uid) || {item_id: uid};3421  const apply = v => {3422    if(v){ FAV.ids.add(uid); if(!FAV.items.some(i=>i.item_id===uid)) FAV.items.push(item); }3423    else { FAV.ids.delete(uid); FAV.items = FAV.items.filter(i => i.item_id !== uid); }3424    favPaint();3425    if(location.pathname === "/favoris") renderFavs();3426  };3427  apply(on);3428  try{3429    const r = await fetch("/api/favorites/toggle", {method:"POST",3430      headers:{"Content-Type":"application/json"},3431      body: JSON.stringify({on, item})});3432    if(!r.ok) throw new Error(r.status);3433  }catch(e){ apply(!on); }3434}34353436/* clic sur un cœur — en phase capture : la carte entière est un lien */3437document.addEventListener("click", e => {3438  const b = e.target.closest(".fav-btn[data-fav]");3439  if(!b) return;3440  e.preventDefault(); e.stopPropagation();3441  favToggle(b.dataset.fav);3442}, true);34433444/* carte d'un favori (données du hub — l'événement peut avoir quitté l'index) */3445function favCard(it){3446  FAV.meta[it.item_id] = it;3447  const img = it.image_url3448    ? `<img src="${esc(it.image_url)}" alt="" loading="lazy"3449        onerror="this.parentElement.classList.add('noimg');this.remove()">` : "";3450  const local = (it.url || "").replace(/^https?:\/\/[^/]*sorti-ka\.com/i, "")3451    || `/evenement/${encodeURIComponent(it.item_id)}`;3452  return `<a class="ev" data-nav href="${esc(local)}">3453    <div class="ev-img${it.image_url?"":" noimg"}">${img}3454      ${it.price_label?`<span class="ev-tag${it.price_label==="Gratuit"?" free":""}">${esc(it.price_label)}</span>`:""}3455      <button type="button" class="fav-btn on" data-fav="${esc(it.item_id)}" aria-pressed="true"3456        aria-label="Retirer de mes favoris (Mon univers Ka)"3457        title="Retirer de mes favoris (Mon univers Ka)">${FAV_HEART}</button>3458    </div>3459    <div class="ev-body">3460      <h3>${esc(it.title)}</h3>3461      ${it.subtitle?`<p class="ev-where">${esc(it.subtitle)}</p>`:""}3462      <p class="ev-meta"><span class="src">Mon univers Ka</span></p>3463    </div>3464  </a>`;3465}34663467function renderFavs(){3468  if(!FAV.connected){3469    $("fav-count").innerHTML = "Connectez-vous pour retrouver vos favoris";3470    $("fav-grid").innerHTML = `<div class="empty fav-empty">3471      <b>Vos favoris vous suivent partout dans l'univers Ka</b>3472      Connectez-vous avec votre KA ID pour ajouter des sorties à vos favoris —3473      ils sont synchronisés sur toutes les plateformes du Groupe KA.<br>3474      <a class="btn-ka" href="/api/auth/ka/login"><span class="kdot">·Ka</span> Se connecter avec KA ID</a></div>`;3475    return;3476  }3477  const items = FAV.items;3478  $("fav-count").innerHTML = items.length3479    ? `<b>${items.length.toLocaleString("fr-CA")}</b>&nbsp;favori${items.length>1?"s":""} sur Sorti·Ka`3480    : "Aucun favori pour l'instant";3481  $("fav-grid").innerHTML = items.map(favCard).join("") ||3482    `<div class="empty fav-empty"><b>Aucun favori pour l'instant</b>3483     Touchez le cœur ♥ d'un événement pour le garder ici — et le retrouver3484     sur toutes les plateformes du Groupe KA.</div>`;3485  bindNav($("fav-grid"));3486}34873488async function showFavoris(){3489  document.title = "Mes favoris — Sorti-Ka | Mon univers Ka";3490  show("view-favoris");3491  window.scrollTo({top:0});3492  await favLoad();      // le hub est la source de vérité — relire à l'ouverture3493  renderFavs();3494}34953496/* ---------- vues ---------- */3497const VIEWS = ["view-list","view-detail","view-contact","view-stats","view-favoris"];3498function show(id){3499  VIEWS.forEach(v => $(v).classList.toggle("hidden", v !== id));3500  // fiche événement : fond papier, header compact, ticker/tabbar/bulle KA masqués3501  document.body.classList.toggle("sk-fiche-page", id === "view-detail");3502  if(id !== "view-detail"){ const hd = $("sk-hdr"); if(hd) hd.remove(); }3503  if(id !== "view-detail" && SK_STATE.cleanup.length){ SK_STATE.cleanup.forEach(f => { try{ f(); }catch(_){} }); SK_STATE.cleanup = []; }3504}35053506function showList(){3507  document.title = "Sorti-Ka — Un service Groupe KA";3508  show("view-list");3509}35103511function showContact(){3512  document.title = "Contact — Sorti-Ka | Un service Groupe KA";3513  show("view-contact");3514  window.scrollTo({top:0});3515}35163517/* ---------- routeur ---------- */3518/* slug ASCII identique à sortika/seo.py (pages /region/… et /categorie/…) */3519function kaSlug(s){3520  return s.normalize("NFKD").replace(/[̀-ͯ]/g,"")3521    .toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"");3522}3523let allRegions = [];3524function route(){3525  const m = location.pathname.match(/^\/evenement\/(.+)$/);3526  const r = location.pathname.match(/^\/region\/([^/]+)$/);3527  const c = location.pathname.match(/^\/categorie\/([^/]+)$/);3528  if(m) showDetail(decodeURIComponent(m[1]));3529  else if(location.pathname === "/contact") showContact();3530  else if(location.pathname === "/favoris") showFavoris();3531  else if(location.pathname === "/stats") showStats();3532  else {3533    if(r){3534      // page SSR /region/{slug} : la SPA ouvre la recherche filtrée sur la région3535      const name = allRegions.find(x => kaSlug(x) === decodeURIComponent(r[1]));3536      if(name) state.region = name;3537    } else if(c){3538      // page SSR /categorie/{cat} : recherche filtrée sur la catégorie3539      state.category = decodeURIComponent(c[1]);3540    }3541    showList();3542    load();3543  }3544}3545function go(path){3546  history.pushState({}, "", path);3547  route();3548}3549function bindNav(root){3550  (root||document).querySelectorAll("a[data-nav]").forEach(a => a.onclick = e => {3551    if(e.metaKey||e.ctrlKey||e.shiftKey) return;3552    e.preventDefault(); go(a.getAttribute("href"));3553  });3554}3555window.addEventListener("popstate", route);3556/* (le lien « Toutes les sorties » de l'ancienne fiche est remplacé par le fil d'Ariane de la fiche v3) */35573558/* ---------- KA Nav v2 : menu mobile plein écran (2026-08-25) ---------- */3559(function(){3560  const btn = document.getElementById("ka-mnav-btn");3561  const panel = document.getElementById("ka-mnav");3562  if(!btn || !panel) return;3563  function setMenu(open){3564    panel.classList.toggle("open", open);3565    btn.setAttribute("aria-expanded", open ? "true" : "false");3566    document.documentElement.classList.toggle("ka-menu-locked", open);3567  }3568  btn.addEventListener("click", () => setMenu(!panel.classList.contains("open")));3569  panel.addEventListener("click", e => {3570    if(e.target.closest("a") || e.target.closest("[data-ka-mnav-close]")) setMenu(false);3571  });3572  document.addEventListener("keydown", e => { if(e.key === "Escape") setMenu(false); });3573})();35743575/* ---------- init ---------- */3576async function init(){3577  favLoad();   // favoris ♥ — repeint les cœurs dès la réponse du hub3578  renderQuick();3579  renderDays();3580  initSheet();35813582  // recherche au centre3583  let t; $("q").addEventListener("input", e=>{3584    clearTimeout(t); t=setTimeout(()=>{ state.q=e.target.value.trim(); load(); }, 280);3585  });3586  $("q-clear").onclick = () => { $("q").value=""; state.q=""; load(); };35873588  // bascule Liste | Carte + pagination3589  $("v-list").onclick = () => setView("list");3590  $("v-map").onclick = () => setView("map");3591  $("more").onclick = () => { state.offset += state.limit; load(false); };3592  bindNav(document);35933594  // référentiels (avant route() : /region/{slug} en a besoin)3595  try{3596    const regions = await (await fetch("/api/regions")).json();3597    allRegions = regions;3598    $("region").insertAdjacentHTML("beforeend",3599      regions.map(r=>`<option>${esc(r)}</option>`).join(""));3600  }catch(e){}3601  try{3602    const cats = await (await fetch("/api/categories")).json();3603    $("cats").innerHTML = cats.filter(c=>c!=="autre")3604      .map(c=>`<button type="button" class="chip" data-cat="${c}">${CAT_LABELS[c]||c}</button>`).join("");3605    document.querySelectorAll("#cats .chip[data-cat]").forEach(ch => ch.onclick = () => {3606      state.category = state.category===ch.dataset.cat ? "" : ch.dataset.cat;3607      loadDebounced(); syncControls();3608    });3609  }catch(e){}36103611  // compteurs = preuve de richesse, intégrés au hero en phrase éditoriale3612  try{3613    const s = await (await fetch("/api/stats")).json();3614    const nReg = Object.keys(s.by_region).filter(r=>r!=="Non rattachée").length;3615    const f = n => n.toLocaleString("fr-CA");3616    $("stats").innerHTML =3617      `<b>${f(s.upcoming)}</b> événements à venir <i>·</i> dans <b>${f(s.cities)}</b> villes `3618      + `<i>·</i> sur <b>${nReg}</b> régions <i>·</i> dont <b>${f(s.free_upcoming)}</b> gratuits`;3619  }catch(e){}36203621  try{3622    const reg = await (await fetch("/api/sources")).json();3623    $("src-grid").innerHTML = (reg.sources||[]).filter(s=>s.statut==="actif"||s.active_events>0).map(s=>`3624      <div class="src-card">3625        <h4>${esc(s.name)}</h4>3626        <div class="lic">${esc((s.acces_legal||"").split("—")[0])}</div>3627        <div class="n"><b>${(s.active_events||0).toLocaleString("fr-CA")}</b> événements actifs · ${esc((s.extraction||"").split("(")[0])}</div>3628      </div>`).join("");3629  }catch(e){}36303631  // --- KA ID (« Se connecter avec KA » — hub groupe-ka.com) ---3632  try{3633    const cfg = await (await fetch("/api/auth/config")).json();3634    if(cfg.ka){3635      const meRes = await fetch("/api/me");3636      if(meRes.ok){3637        const me = await meRes.json();3638        const pic = me.picture ? `<img src="${esc(me.picture)}" alt="" onerror="this.remove()">` : "";3639        $("auth").innerHTML = `<span class="user-chip">${pic}3640          <span><span class="uname">${esc(me.name||me.email||"Membre")}</span>3641          <span class="kaid">${esc(me.ka_id||"")}</span></span>3642          <button class="out" id="logout">Sortir</button></span>`;3643        $("logout").onclick = async () => {3644          await fetch("/api/auth/logout", {method:"POST"});3645          location.href = "/";3646        };3647      } else {3648        $("auth").innerHTML = `<a class="btn-ka" href="/api/auth/ka/login">3649          <span class="kdot">·Ka</span><span class="tx">Se connecter avec KA</span></a>3650          <a class="ka-signup" href="https://www.groupe-ka.com/connexion"3651             target="_blank" rel="noopener">Créer un compte</a>`;3652      }3653    }3654  }catch(e){}36553656  route();3657}3658init();3659</script>3660  <script>window.KA_AGENT={site:"sorti-ka"}</script>3661  <script src="/ka-agent.js" defer></script>3662  <!-- KA ID v2 : signaux navigateur de la personnalisation (clics, dwell) -->3663  <script>window.KAID_CONF={card:"a[href^='/evenement/']",detail:"^/evenement/(.+)$",entity:"event"}</script>3664  <script src="/ka-id.js" defer></script>36653666<!-- ======== KA Tabbar v1 — navigation mobile commune Groupe KA (ka-ui/nav) ======== -->3667<nav class="ka-tabbar" id="ka-tabbar" aria-label="Navigation mobile">3668  <a data-nav data-tab="decouvrir" href="/"><svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="m15.5 8.5-2.2 5-4.8 2 2.2-5 4.8-2z"/></svg>Découvrir</a>3669  <button type="button" data-tab="recherche" id="ktb-search"><svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20.5 20.5-4.2-4.2"/></svg>Recherche</button>3670  <button type="button" data-tab="carte" id="ktb-carte"><svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 21s-7-5.6-7-11a7 7 0 0 1 14 0c0 5.4-7 11-7 11z"/><circle cx="12" cy="10" r="2.6"/></svg>Carte</button>3671  <a data-nav data-tab="favoris" href="/favoris"><svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20.5S4.5 15.8 2.6 11.4A5.3 5.3 0 0 1 12 6.6a5.3 5.3 0 0 1 9.4 4.8C19.5 15.8 12 20.5 12 20.5z"/></svg>Favoris</a>3672</nav>3673<script>3674/* KA Tabbar : liaisons (routeur maison data-nav + actions locales) */3675(function(){3676  const bar = document.getElementById("ka-tabbar");3677  bindNav(bar); // les <a data-nav> passent par le routeur SPA36783679  function ktbSync(){3680    const p = location.pathname;3681    const mapOn = !$("results").classList.contains("hidden")3682               && !$("mapwrap").classList.contains("hidden");3683    let k = null;3684    if(p === "/favoris") k = "favoris";3685    else if(p === "/" || p.startsWith("/region/") || p.startsWith("/categorie/")3686            || p.startsWith("/evenement/")) k = mapOn ? "carte" : "decouvrir";3687    bar.querySelectorAll("[data-tab]").forEach(b =>3688      b.classList.toggle("active", b.dataset.tab === k));3689  }36903691  /* Recherche : accueil + focus du champ central */3692  $("ktb-search").onclick = () => {3693    const wasMap = state.mapAll;3694    state.mapAll = false;3695    if(location.pathname !== "/") go("/");3696    else if(wasMap) load();3697    window.scrollTo({top:0, behavior:"smooth"});3698    setTimeout(() => $("q").focus({preventScroll:true}), 60);3699    setTimeout(ktbSync, 80);3700  };37013702  /* Carte : toutes les sorties sur la carte (vue résultats forcée) */3703  $("ktb-carte").onclick = () => {3704    state.mapAll = true;3705    if(location.pathname !== "/") go("/");3706    else load();3707    setView("map");3708    setTimeout(() => $("results").scrollIntoView({behavior:"smooth", block:"start"}), 120);3709    setTimeout(ktbSync, 150);3710  };37113712  /* toute navigation hors tabbar sort du mode carte-de-tout (one-shot) */3713  document.addEventListener("click", e => {3714    const a = e.target.closest && e.target.closest("a[data-nav]");3715    if(a && !bar.contains(a)) state.mapAll = false;3716    setTimeout(ktbSync, 80);3717  }, true);3718  window.addEventListener("popstate", () => { state.mapAll = false; setTimeout(ktbSync, 80); });37193720  ktbSync();3721})();3722</script>3723</body>3724</html>3725