SPB Git

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.

feat(fiche): refonte premium mobile-first de la fiche événement (2026-09-07)

Même socle que les fiches Lou-Ka v3 / Immo-Ka v3, porté en JS natif dans
frontend/index.html (tokens --sk-*, classes sk-*, fond papier, cartes
blanches bord 1 px, framboise réservée aux CTA/prix) : héro (catégorie ·
source, prix « Dès X $ » / Gratuit / Billetterie, capsules date et statut,
titre + lieu · ville · date, image, actions favoris ♥ / partage / Billets) ;
« En bref » déterministe (aujourd'hui / demain / ce week-end / terminé,
statut annulé-reporté-complet, prix, lieu, artistes, public) ; navigation
sticky par sections (scroll-spy) ; « Quand et où » (faits, adresse,
itinéraires, carte Leaflet sur tuiles OSM — CARTO exige désormais une clé) ;
« Programme » (description tronquée, artistes, étiquettes) ; « Billets et
prix » ; « À voir aussi » (cartes existantes) ; Dossier de la fiche ; Sources
et méthodologie. CTA sticky après le héro, aside desktop sticky, « Demander à
Ka » (bottom sheet → widget KA Agent). Header compact sur /evenement/
(favoris + partage injectés, ticker/badge/tabbar masqués, nettoyage au retour
sur les autres vues). Ids de sections préfixés sk- (collision avec #sources de
l'accueil). Ancien lien « Toutes les sorties » remplacé par le fil d'Ariane.
Aucun order CSS ; pas de scrollIntoView à l'ouverture. Dossier qa/ (ignoré par
git) : serveur de prévisualisation + scripts Playwright (lien vers
vrai-prix/node_modules).

Validé sur M3U96a : check-order OK, interactions OK (accordéon, similaires,
Ka, navigation SPA, 404), captures 6 largeurs ; index.html basculé (rendu SEO
seo.py relit le fichier à chaque requête, aucun redémarrage).
Simon-Pierre Boucher committed (Sep 7, 2026) parent 06dd0e9 Browse files

2 changed files +924 −83

modified .gitignore +1 −0
@@ -18,3 +18,4 @@ data/sortika.db
18 data/*.bak-* 18 data/*.bak-*
19 data/future_counts.json 19 data/future_counts.json
20 data/osm_venues.json 20 data/osm_venues.json
21+qa/
modified frontend/index.html +923 −83
@@ -758,6 +758,687 @@ html.ka-menu-locked,html.ka-menu-locked body{overflow:hidden}
758 .ka-tabbar svg{transition:none} 758 .ka-tabbar svg{transition:none}
759 .ka-tabbar .active svg,.ka-tabbar .active::after{animation:none} 759 .ka-tabbar .active svg,.ka-tabbar .active::after{animation:none}
760 } 760 }
761+
762+
763+/* =====================================================================
764+ Fiche événement premium (refonte 2026-09-07, même socle que les fiches
765+ 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+}
780+body.sk-fiche-page{background:var(--sk-bg)}
781+body.sk-fiche-page .ticker{display:none}
782+body.sk-fiche-page .ka-tabbar{display:none!important}
783+body.sk-fiche-page .kaa-btn,body.sk-fiche-page .kaa-hello{display:none!important}
784+body.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)}
785+body.sk-fiche-page .header-inner{min-height:var(--sk-header-h);gap:10px}
786+body.sk-fiche-page .header .brand{font-size:21px}
787+body.sk-fiche-page .header .brand-tag,body.sk-fiche-page .header .gk-badge{display:none}
788+body.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); }
820+
821+/* ---- 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); }
844+
845+/* ---- 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); }
859+
860+/* ---- 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+}
886+
887+/* ---- 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; } }
927+
928+/* ---- 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; }
943+
944+/* ---- 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; }
954+
955+/* ---- 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); }
973+
974+/* ---- 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); }
990+
991+/* ---- 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); }
999+
1000+/* ---- 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; }
1009+
1010+/* ---- 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; } }
1022+
1023+/* ---- 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); }
1036+
1037+/* ---- 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); }
1051+
1052+/* ---- 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; }
1065+
1066+/* ---- 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); }
1077+
1078+/* ---- 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; }
1086+
1087+/* ---- 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; }
1101+
1102+/* ---- 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); }
1116+
1117+/* ---- 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); }
1154+
1155+/* ---- 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; } }
1172+
1173+/* ---- 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; }
1200+
1201+/* ---- 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; }
1220+
1221+/* ---- 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; }
1229+
1230+/* ---- é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; }
1243+
1244+/* ---- 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); }
1257+
1258+/* 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); }
1278+
1279+/* ---- 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); }
1291+
1292+/* ---- 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; }
1303+
1304+/* ---- 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); }
1319+
1320+/* ---- 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; }
1336+
1337+/* ---- 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; }
1351+
1352+
1353+/* ---- 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; }
1399+
1400+
1401+
1402+body.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}}
1409+
1410+/* ---- 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}
1432+
1433+/* ---- 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+}
1438+
1439+/* ---- utilitaire a11y ---- */
1440+.visually-hidden { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
1441+
761 </style> 1442 </style>
762 </head> 1443 </head>
763 <body> 1444 <body>
@@ -929,35 +1610,7 @@ html.ka-menu-locked,html.ka-menu-locked body{overflow:hidden}
929 <div class="toast" id="toast" role="status" aria-live="polite"></div> 1610 <div class="toast" id="toast" role="status" aria-live="polite"></div>
930 1611
931 <!-- ======== VUE FICHE ÉVÉNEMENT ======== --> 1612 <!-- ======== VUE FICHE ÉVÉNEMENT ======== -->
932−<main id="view-detail" class="hidden"> 1613+<main id="view-detail" class="hidden sk-fiche"><div id="sk-root"></div></main>
933− <div class="container detail">
934− <span class="back" id="back">← Toutes les sorties</span>
935− <div class="detail-grid">
936− <article class="detail-main">
937− <div class="d-imgwrap" id="d-img"></div>
938− <div class="d-body">
939− <span class="klabel" id="d-cats-label"></span>
940− <div id="d-status"></div>
941− <div class="d-title-row"><h1 id="d-title"></h1><span id="d-fav"></span></div>
942− <div class="desc" id="d-desc"></div>
943− <div class="detail-cats" id="d-cats"></div>
944− </div>
945− </article>
946− <aside class="side">
947− <div class="box" id="d-facts"></div>
948− <a class="btn btn-cta" id="d-cta" target="_blank" rel="noopener">Billets · fiche originale ↗</a>
949− <div class="box" id="d-src"></div>
950− </aside>
951− </div>
952− <section class="similar">
953− <span class="kicker">À voir aussi</span>
954− <h2 id="d-similar-title">Événements similaires</h2>
955− <div class="grid" id="d-similar"></div>
956− </section>
957− </div>
958− <!-- CTA sticky mobile : le billet toujours à portée de pouce -->
959− <div class="d-ctabar" id="d-ctabar"></div>
960−</main>
961 1614
962 <!-- ======== VUE FAVORIS (/favoris) — ♥ « Mon univers Ka » (hub groupe-ka.com) ======== --> 1615 <!-- ======== VUE FAVORIS (/favoris) — ♥ « Mon univers Ka » (hub groupe-ka.com) ======== -->
963 <main id="view-favoris" class="hidden"> 1616 <main id="view-favoris" class="hidden">
@@ -1691,74 +2344,257 @@ async function refreshMarkers(){
1691 } 2344 }
1692 2345
1693 /* ---------- fiche événement ---------- */ 2346 /* ---------- 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éro
2349+ (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 KA
2354+ Agent). Ordre DOM = ordre visuel, aucun scrollIntoView à l'ouverture. */
2355+const 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+};
2369+const SK_STATE = { uid: null, cleanup: [] };
2370+
2371+function 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+}
2376+function 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+}
2389+function 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+}
2395+function 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+}
2409+function 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+}
2414+function 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+}
2417+function 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>`; }
2418+function skKv(k, v){ return `<div class="sk-kv"><span class="k">${esc(k)}</span><span class="v">${v}</span></div>`; }
2419+function 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>`; }
2420+
1694 async function showDetail(uid){ 2421 async function showDetail(uid){
1695 let ev; 2422 let ev;
1696 try{ ev = await (await fetch(`/api/events/${encodeURIComponent(uid)}`)).json(); } 2423 try{ ev = await (await fetch(`/api/events/${encodeURIComponent(uid)}`)).json(); }
1697 catch(e){ go("/"); return; } 2424 catch(e){ go("/"); return; }
1698 if(ev.detail){ 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);
1699 2428
1700 document.title = `${ev.title} — ${ev.city || "Québec"} | Sorti·Ka`; 2429 document.title = `${ev.title} — ${ev.city || "Québec"} | Sorti·Ka`;
1701− $("d-img").innerHTML = ev.image 2430+ const cats = (ev.categories||[]).filter(c=>c!=="autre").map(c=>CAT_LABELS[c]||c);
1702− ? `<img src="${esc(ev.image)}" alt="" onerror="this.remove()">` : ""; 2431+ const catTxt = cats.join(" · ") || "Événement";
1703− $("d-img").classList.toggle("noimg", !ev.image); 2432+ const src = SRC_LABELS[ev.source] || ev.source;
1704− $("d-cats-label").textContent = (ev.categories||[]).filter(c=>c!=="autre") 2433+ const brief = skEnBref(ev);
1705− .map(c=>CAT_LABELS[c]||c).join(" · ") || "Événement"; 2434+ const quand = skQuand(ev);
1706− $("d-status").innerHTML = ev.status && STATUS_LABELS[ev.status] 2435+ const prix = skPrix(ev);
1707− ? `<div class="status-banner status-banner--${esc(ev.status)}">${ 2436+ const resume = [ev.venue, ev.city, evDate(ev)].filter(Boolean);
1708− ev.status==="cancelled" ? "Cet événement est annulé" 2437+ const geo = ev.lat && ev.lng;
1709− : ev.status==="postponed" ? "Cet événement est reporté — vérifiez la nouvelle date chez la source" 2438+ const statusCap = ev.status && STATUS_LABELS[ev.status] ? `<span class="sk-capsule high"><b>${esc(STATUS_LABELS[ev.status])}</b></span>` : "";
1710− : "Complet — plus de billets disponibles"}</div>` : ""; 2439+ const dateCap = quand ? `<span class="sk-capsule ${quand.tone === "good" ? "good" : quand.tone === "bad" ? "high" : "date"}"><b>${esc(quand.titre)}</b></span>` : "";
1711− $("d-title").textContent = ev.title; 2440+ const shareBtn = (cls) => `<button type="button" class="${cls}" data-sk-share aria-label="Partager">${SK_ICO.share}</button>`;
1712− $("d-fav").innerHTML = favBtn(ev, true); 2441+ const cta = (cls, txt) => `<a class="${cls}" href="${esc(ev.url)}" target="_blank" rel="noopener noreferrer">${txt} ${SK_ICO.ext}</a>`;
1713− $("d-desc").textContent = ev.description || ""; 2442+ const nav = [["resume","Résumé"],["quand","Quand et où"],["programme","Programme"],["billets","Billets"],["similaires","À voir aussi"],["dossier","Dossier"],["sources","Sources"]];
1714− $("d-cats").innerHTML = (ev.raw_categories||[]).slice(0,5).filter(c=>c.length<40) 2443+
1715− .map(c=>`<span class="cat">${esc(c)}</span>`).join(""); 2444+ // --- faits « Quand et où »
1716−
1717 const facts = []; 2445 const facts = [];
1718− facts.push(["Quand", dateBadge(ev, true)]); 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"));
1719− if(ev.artists && ev.artists.length) 2447+ if(ev.start_time) facts.push(skFact(SK_ICO.cal, `${fmtTime(ev.start_time)}${ev.end_time ? ` – ${fmtTime(ev.end_time)}` : ""}`, "Heure"));
1720− facts.push(["À l'affiche", ev.artists.map(esc).join(", ")]); 2448+ if(ev.venue) facts.push(skFact(SK_ICO.pin, ev.venue, "Lieu"));
1721− if(ev.venue) facts.push(["Lieu", esc(ev.venue)]); 2449+ if(ev.city) facts.push(skFact(SK_ICO.pin, ev.city, ev.region && ev.region !== ev.city ? `Ville · ${ev.region}` : "Ville"));
1722− if(ev.address) facts.push(["Adresse", esc(ev.address)]); 2450+ if(ev.audience) facts.push(skFact(SK_ICO.people, ev.audience, "Public"));
1723− if(ev.city) facts.push(["Ville", esc(ev.city)]); 2451+ if(ev.organizer) facts.push(skFact(SK_ICO.people, ev.organizer, "Organisé par"));
1724− if(ev.region) facts.push(["Région", esc(ev.region)]); 2452+ const rows = [];
1725− if(ev.is_free === true) facts.push(["Prix", `<span class="badge-free">Gratuit</span>`]); 2453+ if(ev.address) rows.push(skKv("Adresse", esc([ev.address, ev.city, ev.postal_code].filter(Boolean).join(", "))));
1726− else if(ev.price_min != null) facts.push(["Prix", `Dès ${ev.price_min.toFixed(2).replace(".",",")} $ CAD`]); 2454+ if(ev.tourist_region) rows.push(skKv("Région touristique", esc(ev.tourist_region)));
1727− else if(ev.is_free === false) facts.push(["Prix", "Payant — voir la billetterie"]); 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>`));
1728− if(ev.price_label) facts.push(["Détail prix", esc(ev.price_label)]); 2456+
1729− if(ev.audience) facts.push(["Public", esc(ev.audience)]); 2457+ // --- programme
1730− if(ev.organizer) facts.push(["Organisé par", esc(ev.organizer)]); 2458+ const desc = (ev.description || "").trim();
1731− if(ev.lat && ev.lng) facts.push(["Carte", 2459+ const long = desc.length > 420;
1732− `<a class="maplink" target="_blank" rel="noopener" href="https://www.google.com/maps?q=${ev.lat},${ev.lng}">Voir sur la carte ↗</a>`]); 2460+ const raw = (ev.raw_categories||[]).filter(c => c && c.length < 40).slice(0, 8);
1733− $("d-facts").innerHTML = facts.map(([k,v]) => 2461+
1734− `<div class="fact"><span class="k">${k}</span><span class="v">${v}</span></div>`).join(""); 2462+ const html = `
1735− 2463+ <div class="sk-wrap">
1736− $("d-cta").href = ev.url; 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>
1737− $("d-ctabar").innerHTML = `<div class="p"> 2465+ <div class="sk-grid">
1738− <b>${priceTxt(ev) || (ev.is_free === false ? "Payant — voir la billetterie" : "Voir la billetterie")}</b> 2466+ <div class="sk-main">
1739− <span>${evDate(ev)}</span></div> 2467+ <header class="sk-hero" aria-label="Résumé de l'événement">
1740− <a class="btn btn-cta" href="${esc(ev.url)}" target="_blank" rel="noopener">Billets ↗</a>`; 2468+ <div class="sk-kicker">${esc(catTxt)} · ${esc(src)}</div>
1741− $("d-src").innerHTML = `<span class="klabel">Source & attribution</span> 2469+ <div class="sk-price">${esc(prix)}${ev.price_label && ev.price_min != null ? `<small>${esc(ev.price_label)}</small>` : ""}</div>
1742− <div style="margin-top:6px;font-size:13px">${SRC_LABELS[ev.source]||ev.source}</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>
1743− <div style="font-family:var(--font-mono);font-size:10px;color:var(--ink-3);margin-top:4px"> 2471+ <h1 class="sk-h1">${esc(ev.title)}${resume.length ? `<span class="sk-city">${resume.map(esc).join(" · ")}</span>` : ""}</h1>
1744− Capté le ${new Date((ev.updated_at||0)*1000).toLocaleDateString("fr-CA")} · 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>
1745− identifiant ${esc(ev.uid)}</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>`;
2497+
2498+ 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();
2506+
2507+ // partage
2508+ 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éro
2531+ 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 à Ka
2536+ root.querySelectorAll("[data-sk-ka]").forEach(b => b.onclick = () => skOpenKa(ev));
2537+
2538+ show("view-detail");
2539+ window.scrollTo({ top:0 });
2540+
2541+ // 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+ }
1746 2551
1747 // événements similaires : même catégorie prioritaire, sinon même ville 2552 // événements similaires : même catégorie prioritaire, sinon même ville
1748 const cat = (ev.categories||[]).filter(c=>c!=="autre")[0] || ""; 2553 const cat = (ev.categories||[]).filter(c=>c!=="autre")[0] || "";
1749− const q = new URLSearchParams({limit: 7}); 2554+ const q = new URLSearchParams({ limit: 7 });
1750 if(cat) q.set("category", cat); else if(ev.city) q.set("city", ev.city); 2555 if(cat) q.set("category", cat); else if(ev.city) q.set("city", ev.city);
1751 if(ev.region && cat) q.set("region", ev.region); 2556 if(ev.region && cat) q.set("region", ev.region);
1752 try{ 2557 try{
1753 const sim = await (await fetch(`/api/events?${q}`)).json(); 2558 const sim = await (await fetch(`/api/events?${q}`)).json();
1754− const others = sim.events.filter(e=>e.uid!==ev.uid).slice(0,6); 2559+ const others = sim.events.filter(e => e.uid !== ev.uid).slice(0, 6);
1755− $("d-similar").innerHTML = others.map(card).join("") || 2560+ const host = $("d-similar");
1756− `<div class="empty">Rien de similaire pour l'instant.</div>`; 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(); }
1757− bindNav($("d-similar")); 2562+ }catch(e){ const host = $("d-similar"); if(host) host.innerHTML = ""; }
1758− }catch(e){ $("d-similar").innerHTML = ""; } 2563+}
1759− 2564+
1760− show("view-detail"); 2565+/* « ✦ Demander à Ka » : bottom sheet de démarrage → widget KA Agent (ka-agent.js) */
1761− window.scrollTo({top:0}); 2566+const 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"];
2567+function 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+}
2572+function 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);
1762 } 2598 }
1763 2599
1764 /* ==================== vue /stats — module ka-stats (vanilla + SVG) ========= 2600 /* ==================== vue /stats — module ka-stats (vanilla + SVG) =========
@@ -2661,6 +3497,10 @@ async function showFavoris(){
2661 const VIEWS = ["view-list","view-detail","view-contact","view-stats","view-favoris"]; 3497 const VIEWS = ["view-list","view-detail","view-contact","view-stats","view-favoris"];
2662 function show(id){ 3498 function show(id){
2663 VIEWS.forEach(v => $(v).classList.toggle("hidden", v !== id)); 3499 VIEWS.forEach(v => $(v).classList.toggle("hidden", v !== id));
3500+ // fiche événement : fond papier, header compact, ticker/tabbar/bulle KA masqués
3501+ 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 = []; }
2664 } 3504 }
2665 3505
2666 function showList(){ 3506 function showList(){
@@ -2713,7 +3553,7 @@ function bindNav(root){
2713 }); 3553 });
2714 } 3554 }
2715 window.addEventListener("popstate", route); 3555 window.addEventListener("popstate", route);
2716−$("back").onclick = () => go("/"); 3556+/* (le lien « Toutes les sorties » de l'ancienne fiche est remplacé par le fil d'Ariane de la fiche v3) */
2717 3557
2718 /* ---------- KA Nav v2 : menu mobile plein écran (2026-08-25) ---------- */ 3558 /* ---------- KA Nav v2 : menu mobile plein écran (2026-08-25) ---------- */
2719 (function(){ 3559 (function(){
2720 3560
Menu

Theme