SPB Git forge

spb/sorti-ka

Public

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

58commits 1branches 0releases
13.7 MBsize
maindefault branch
17 days agolast push
HTML 82.9% Python 15.2% TypeScript 0.9% JavaScript 0.7%

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 17 days ago (Sep 7, 2026) parent 06dd0e9

2 changed files +924 −83

modified .gitignore +1 −0
@@ -18,3 +18,4 @@ data/sortika.db
18 18 data/*.bak-*
19 19 data/future_counts.json
20 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 758 .ka-tabbar svg{transition:none}
759 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 1442 </style>
762 1443 </head>
763 1444 <body>
@@ -929,35 +1610,7 @@ html.ka-menu-locked,html.ka-menu-locked body{overflow:hidden}
929 1610 <div class="toast" id="toast" role="status" aria-live="polite"></div>
930 1611
931 1612 <!-- ======== VUE FICHE ÉVÉNEMENT ======== -->
932 <main id="view-detail" class="hidden">
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>
1613 +<main id="view-detail" class="hidden sk-fiche"><div id="sk-root"></div></main>
961 1614
962 1615 <!-- ======== VUE FAVORIS (/favoris) — ♥ « Mon univers Ka » (hub groupe-ka.com) ======== -->
963 1616 <main id="view-favoris" class="hidden">
@@ -1691,74 +2344,257 @@ async function refreshMarkers(){
1691 2344 }
1692 2345
1693 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 2421 async function showDetail(uid){
1695 2422 let ev;
1696 2423 try{ ev = await (await fetch(`/api/events/${encodeURIComponent(uid)}`)).json(); }
1697 2424 catch(e){ go("/"); return; }
1698 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 2429 document.title = `${ev.title} — ${ev.city || "Québec"} | Sorti·Ka`;
1701 $("d-img").innerHTML = ev.image
1702 ? `<img src="${esc(ev.image)}" alt="" onerror="this.remove()">` : "";
1703 $("d-img").classList.toggle("noimg", !ev.image);
1704 $("d-cats-label").textContent = (ev.categories||[]).filter(c=>c!=="autre")
1705 .map(c=>CAT_LABELS[c]||c).join(" · ") || "Événement";
1706 $("d-status").innerHTML = ev.status && STATUS_LABELS[ev.status]
1707 ? `<div class="status-banner status-banner--${esc(ev.status)}">${
1708 ev.status==="cancelled" ? "Cet événement est annulé"
1709 : ev.status==="postponed" ? "Cet événement est reporté — vérifiez la nouvelle date chez la source"
1710 : "Complet — plus de billets disponibles"}</div>` : "";
1711 $("d-title").textContent = ev.title;
1712 $("d-fav").innerHTML = favBtn(ev, true);
1713 $("d-desc").textContent = ev.description || "";
1714 $("d-cats").innerHTML = (ev.raw_categories||[]).slice(0,5).filter(c=>c.length<40)
1715 .map(c=>`<span class="cat">${esc(c)}</span>`).join("");
1716
2430 + const cats = (ev.categories||[]).filter(c=>c!=="autre").map(c=>CAT_LABELS[c]||c);
2431 + const catTxt = cats.join(" · ") || "Événement";
2432 + const src = SRC_LABELS[ev.source] || ev.source;
2433 + const brief = skEnBref(ev);
2434 + const quand = skQuand(ev);
2435 + const prix = skPrix(ev);
2436 + const resume = [ev.venue, ev.city, evDate(ev)].filter(Boolean);
2437 + const geo = ev.lat && ev.lng;
2438 + const statusCap = ev.status && STATUS_LABELS[ev.status] ? `<span class="sk-capsule high"><b>${esc(STATUS_LABELS[ev.status])}</b></span>` : "";
2439 + const dateCap = quand ? `<span class="sk-capsule ${quand.tone === "good" ? "good" : quand.tone === "bad" ? "high" : "date"}"><b>${esc(quand.titre)}</b></span>` : "";
2440 + const shareBtn = (cls) => `<button type="button" class="${cls}" data-sk-share aria-label="Partager">${SK_ICO.share}</button>`;
2441 + const cta = (cls, txt) => `<a class="${cls}" href="${esc(ev.url)}" target="_blank" rel="noopener noreferrer">${txt} ${SK_ICO.ext}</a>`;
2442 + const nav = [["resume","Résumé"],["quand","Quand et où"],["programme","Programme"],["billets","Billets"],["similaires","À voir aussi"],["dossier","Dossier"],["sources","Sources"]];
2443 +
2444 + // --- faits « Quand et où »
1717 2445 const facts = [];
1718 facts.push(["Quand", dateBadge(ev, true)]);
1719 if(ev.artists && ev.artists.length)
1720 facts.push(["À l'affiche", ev.artists.map(esc).join(", ")]);
1721 if(ev.venue) facts.push(["Lieu", esc(ev.venue)]);
1722 if(ev.address) facts.push(["Adresse", esc(ev.address)]);
1723 if(ev.city) facts.push(["Ville", esc(ev.city)]);
1724 if(ev.region) facts.push(["Région", esc(ev.region)]);
1725 if(ev.is_free === true) facts.push(["Prix", `<span class="badge-free">Gratuit</span>`]);
1726 else if(ev.price_min != null) facts.push(["Prix", `Dès ${ev.price_min.toFixed(2).replace(".",",")} $ CAD`]);
1727 else if(ev.is_free === false) facts.push(["Prix", "Payant — voir la billetterie"]);
1728 if(ev.price_label) facts.push(["Détail prix", esc(ev.price_label)]);
1729 if(ev.audience) facts.push(["Public", esc(ev.audience)]);
1730 if(ev.organizer) facts.push(["Organisé par", esc(ev.organizer)]);
1731 if(ev.lat && ev.lng) facts.push(["Carte",
1732 `<a class="maplink" target="_blank" rel="noopener" href="https://www.google.com/maps?q=${ev.lat},${ev.lng}">Voir sur la carte ↗</a>`]);
1733 $("d-facts").innerHTML = facts.map(([k,v]) =>
1734 `<div class="fact"><span class="k">${k}</span><span class="v">${v}</span></div>`).join("");
1735
1736 $("d-cta").href = ev.url;
1737 $("d-ctabar").innerHTML = `<div class="p">
1738 <b>${priceTxt(ev) || (ev.is_free === false ? "Payant — voir la billetterie" : "Voir la billetterie")}</b>
1739 <span>${evDate(ev)}</span></div>
1740 <a class="btn btn-cta" href="${esc(ev.url)}" target="_blank" rel="noopener">Billets ↗</a>`;
1741 $("d-src").innerHTML = `<span class="klabel">Source & attribution</span>
1742 <div style="margin-top:6px;font-size:13px">${SRC_LABELS[ev.source]||ev.source}</div>
1743 <div style="font-family:var(--font-mono);font-size:10px;color:var(--ink-3);margin-top:4px">
1744 Capté le ${new Date((ev.updated_at||0)*1000).toLocaleDateString("fr-CA")} ·
1745 identifiant ${esc(ev.uid)}</div>`;
2446 + if(ev.start_date) facts.push(skFact(SK_ICO.cal, fmtDate(ev.start_date, true), ev.end_date && ev.end_date !== ev.start_date ? `Du … au ${fmtDate(ev.end_date, true)}` : "Date"));
2447 + if(ev.start_time) facts.push(skFact(SK_ICO.cal, `${fmtTime(ev.start_time)}${ev.end_time ? ` – ${fmtTime(ev.end_time)}` : ""}`, "Heure"));
2448 + if(ev.venue) facts.push(skFact(SK_ICO.pin, ev.venue, "Lieu"));
2449 + if(ev.city) facts.push(skFact(SK_ICO.pin, ev.city, ev.region && ev.region !== ev.city ? `Ville · ${ev.region}` : "Ville"));
2450 + if(ev.audience) facts.push(skFact(SK_ICO.people, ev.audience, "Public"));
2451 + if(ev.organizer) facts.push(skFact(SK_ICO.people, ev.organizer, "Organisé par"));
2452 + const rows = [];
2453 + if(ev.address) rows.push(skKv("Adresse", esc([ev.address, ev.city, ev.postal_code].filter(Boolean).join(", "))));
2454 + if(ev.tourist_region) rows.push(skKv("Région touristique", esc(ev.tourist_region)));
2455 + if(geo) rows.push(skKv("Itinéraire", `<a href="https://www.google.com/maps?q=${ev.lat},${ev.lng}" target="_blank" rel="noopener">Google Maps ↗</a> · <a href="https://maps.apple.com/?q=${ev.lat},${ev.lng}" target="_blank" rel="noopener">Plans ↗</a>`));
2456 +
2457 + // --- programme
2458 + const desc = (ev.description || "").trim();
2459 + const long = desc.length > 420;
2460 + const raw = (ev.raw_categories||[]).filter(c => c && c.length < 40).slice(0, 8);
2461 +
2462 + const html = `
2463 + <div class="sk-wrap">
2464 + <nav class="sk-crumbs" aria-label="Fil d'Ariane"><a data-nav href="/">Sorties</a><span aria-hidden="true">›</span>${ev.region ? `<a data-nav href="/region/${esc(kaSlug(ev.region))}">${esc(ev.region)}</a><span aria-hidden="true">›</span>` : ""}<span>${esc(ev.title)}</span></nav>
2465 + <div class="sk-grid">
2466 + <div class="sk-main">
2467 + <header class="sk-hero" aria-label="Résumé de l'événement">
2468 + <div class="sk-kicker">${esc(catTxt)} · ${esc(src)}</div>
2469 + <div class="sk-price">${esc(prix)}${ev.price_label && ev.price_min != null ? `<small>${esc(ev.price_label)}</small>` : ""}</div>
2470 + <div class="sk-price-row" style="margin-top:8px">${dateCap}${statusCap}${ev.is_free === true ? `<span class="sk-capsule good"><b>Gratuit</b></span>` : ""}</div>
2471 + <h1 class="sk-h1">${esc(ev.title)}${resume.length ? `<span class="sk-city">${resume.map(esc).join(" · ")}</span>` : ""}</h1>
2472 + <div class="sk-image ${ev.image ? "" : "noimg"}">${ev.image ? `<img src="${esc(ev.image)}" alt="" onerror="this.parentNode.classList.add('noimg');this.remove()">` : esc(catTxt)}</div>
2473 + <div class="sk-actions" id="sk-actions">${favBtn(ev, true)}${shareBtn("sk-btn sk-btn-ghost sk-btn-icon")}${cta("sk-btn sk-btn-primary", ev.is_free === true ? "Voir la fiche originale" : "Billets")}</div>
2474 + </header>
2475 + ${skCard("resume", "En bref", "", skBrief(brief))}
2476 + <nav class="sk-nav" aria-label="Sections de la fiche"><div class="sk-nav-track" id="sk-nav">${nav.map(([id,l], i) => `<a href="#sk-${id}" data-sk-nav="sk-${id}" class="${i === 0 ? "on" : ""}">${l}</a>`).join("")}</div></nav>
2477 + ${skCard("quand", "Quand et où", SK_ICO.pin, `${facts.length ? `<div class="sk-facts" role="list">${facts.join("")}</div>` : ""}${rows.length ? `<div class="sk-facts-rows">${rows.join("")}</div>` : ""}${geo ? `<div class="sk-map" style="position:relative;border-radius:var(--sk-r-lg);overflow:hidden;border:1px solid var(--sk-border);margin-top:12px"><div id="sk-map" style="position:absolute;inset:0"></div></div><div class="sk-source"><span>Source : <b>OpenStreetMap</b> · position géocodée de l'adresse publiée</span></div>` : ""}`, ev.venue ? `${esc(ev.venue)}${ev.city ? ` — ${esc(ev.city)}` : ""}` : "")}
2478 + ${skCard("programme", "Programme", SK_ICO.music, `${desc ? `<div class="sk-desc ${long ? "clamped" : ""}" id="sk-desc"><div class="sk-desc-body"><p>${esc(desc)}</p></div></div>${long ? `<button type="button" class="sk-more" data-sk-more aria-expanded="false">Lire la suite</button>` : ""}` : `<p class="sk-fine" style="margin-top:0">La source ne fournit pas de description pour cet événement.</p>`}${ev.artists && ev.artists.length ? `<h3 class="sk-card-sub sk-subtitle" id="artistes">À l'affiche <small>· ${ev.artists.length}</small></h3><div class="sk-amen" role="list">${ev.artists.map(a => `<div class="sk-amen-it" role="listitem"><span class="sk-amen-ico" aria-hidden="true">${SK_ICO.music}</span><span class="sk-amen-txt">${esc(a)}</span></div>`).join("")}</div>` : ""}${raw.length ? `<div class="sk-tags" style="margin-top:12px">${raw.map(c => `<span class="sk-tag n">${esc(c)}</span>`).join("")}</div>` : ""}`)}
2479 + ${skCard("billets", "Billets et prix", SK_ICO.ticket, `<div class="sk-kpis cols-3"><div class="sk-kpi accent"><div class="sk-kpi-v">${esc(prix)}</div><div class="sk-kpi-l">${ev.is_free === true ? "Entrée" : ev.price_min != null ? "Prix d'entrée minimum" : "Tarification"}</div></div>${ev.price_label ? `<div class="sk-kpi"><div class="sk-kpi-v wrap">${esc(ev.price_label)}</div><div class="sk-kpi-l">Détail publié</div></div>` : ""}<div class="sk-kpi"><div class="sk-kpi-v wrap">${esc(src)}</div><div class="sk-kpi-l">Billetterie / source</div></div></div>${cta("sk-btn sk-btn-primary", ev.is_free === true ? "Fiche originale" : "Acheter des billets chez " + src).replace('class="sk-btn sk-btn-primary"', 'class="sk-btn sk-btn-primary" style="margin-top:12px;width:100%"')}<p class="sk-fine">Prix affichés par la source lors de la dernière synchronisation — frais de service et taxes en sus possibles. Sorti·Ka ne vend pas de billets.</p>`)}
2480 + ${skCard("similaires", "À voir aussi", SK_ICO.tag, `<div class="grid" id="d-similar"><div class="sk-skel-lines" aria-busy="true"><div class="sk-skel"></div><div class="sk-skel"></div><div class="sk-skel short"></div></div></div>`, cats[0] ? `Autres sorties « ${esc(cats[0])} »${ev.region ? ` en ${esc(ev.region)}` : ""}` : "Autres sorties à proximité")}
2481 + ${skCard("dossier", "Dossier de la fiche", SK_ICO.folder, `${skDetails(`${SK_ICO.people} Organisateur et source`, `<p style="margin:0 0 6px"><b style="font-size:15px">${esc(ev.organizer || src)}</b></p><p style="margin:0">Fiche publiée par <b>${esc(src)}</b>${ev.website ? ` · <a href="${esc(ev.website)}" target="_blank" rel="noopener noreferrer">site de l'événement ↗</a>` : ""}. Sorti·Ka est un agrégateur indépendant : billets, horaires et annulations relèvent de l'organisateur.</p>`, esc(ev.organizer || src))}${skDetails(`${SK_ICO.cal} Suivi Sorti·Ka`, `<div class="sk-facts-rows" style="margin-top:0">${skKv("Première observation", ev.first_seen ? new Date(ev.first_seen*1000).toLocaleDateString("fr-CA", {day:"numeric", month:"long", year:"numeric"}) : "—")}${skKv("Dernière synchronisation", ev.updated_at ? new Date(ev.updated_at*1000).toLocaleDateString("fr-CA", {day:"numeric", month:"long", year:"numeric"}) : "—")}${skKv("Statut", ev.active ? "Publié par la source" : "Retiré")}</div>`, ev.updated_at ? `synchro ${new Date(ev.updated_at*1000).toLocaleDateString("fr-CA")}` : "")}${skDetails(`${SK_ICO.tag} Identifiants`, `<div class="sk-facts-rows" style="margin-top:0">${skKv("Identifiant source", esc(ev.external_id || ""))}${skKv("Identifiant Sorti·Ka", esc(ev.uid))}${skKv("Fiche originale", `<a href="${esc(ev.url)}" target="_blank" rel="noopener noreferrer">${esc(src)} ↗</a>`)}</div>`, esc(ev.external_id || ""))}`, "Ce que Sorti·Ka observe réellement : organisateur, suivi, identifiants")}
2482 + ${skCard("sources", "Sources et méthodologie", SK_ICO.folder, `<ul class="sk-sources"><li><span class="n"><a href="${esc(ev.url)}" target="_blank" rel="noopener noreferrer">${esc(src)}</a></span><span class="r">Titre, dates, lieu, prix, image et description (source originale)</span></li><li><span class="n">Sorti·Ka</span><span class="r">Catégorisation, géocodage, dédoublonnage entre sources, suivi des statuts — calculs maison, indicatifs</span></li>${geo ? `<li><span class="n">OpenStreetMap</span><span class="r">Fond de carte et position du lieu</span></li>` : ""}</ul><div class="sk-end" style="margin-top:14px"><a href="/#sources">${SK_ICO.folder}Sources<small>Toutes les sources Sorti·Ka</small></a><a data-nav href="/contact">${SK_ICO.alert}Signaler une erreur<small>Date, lieu, prix…</small></a><button type="button" data-sk-share>${SK_ICO.share}Partager<small>Lien de la fiche</small></button><a data-nav href="/stats">${SK_ICO.tag}Statistiques<small>Sorties par région</small></a></div><p class="sk-fine" style="margin-top:12px">Les dates, prix et disponibilités sont ceux affichés par la source — chaque fiche renvoie à la page originale. Sorti·Ka est un agrégateur indépendant.</p>`, "Chaque donnée de cette fiche renvoie à sa source ; les calculs Sorti·Ka sont indicatifs et documentés")}
2483 + </div>
2484 + <aside class="sk-aside" aria-label="Résumé et actions"><div class="sk-card sk-aside-card">
2485 + <div><div class="sk-price">${esc(prix)}</div><div class="sk-price-row" style="margin-top:8px">${dateCap}${statusCap}</div></div>
2486 + <div class="sk-aside-addr"><b style="color:var(--sk-text)">${esc(ev.title)}</b><br>${resume.map(esc).join(" · ")}</div>
2487 + ${cta("sk-btn sk-btn-primary", ev.is_free === true ? "Voir la fiche originale" : "Billets chez " + esc(src))}
2488 + <div class="sk-actions" id="sk-aside-actions">${favBtn(ev, true)}${shareBtn("sk-btn sk-btn-ghost")}</div>
2489 + <button type="button" class="sk-btn sk-btn-ghost sk-ka-inline" data-sk-ka>${SK_ICO.spark} Demander à Ka</button>
2490 + ${brief.length ? `<hr class="sk-aside-sep">${skBrief(brief.slice(0, 4), true)}` : ""}
2491 + <div class="sk-aside-meta">${ev.updated_at ? `Synchronisé le ${new Date(ev.updated_at*1000).toLocaleDateString("fr-CA")} · ` : ""}${esc(src)}</div>
2492 + </div></aside>
2493 + </div>
2494 + </div>
2495 + <div class="sk-cta-bar" id="sk-ctabar" aria-hidden="true"><div class="sk-cta-txt"><div class="sk-cta-price">${esc(prix)}</div><div class="sk-cta-sub ${quand.tone === "good" ? "good" : ""}">${esc(quand.titre)} · ${esc(ev.venue || ev.city || "")}</div></div>${cta("sk-btn sk-btn-primary", ev.is_free === true ? "Fiche" : "Billets")}</div>
2496 + <button type="button" class="sk-ka-btn hide" id="sk-ka-btn" data-sk-ka aria-label="Demander à Ka, l'assistant Groupe KA">${SK_ICO.spark} Demander à Ka</button>`;
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 2552 // événements similaires : même catégorie prioritaire, sinon même ville
1748 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 2555 if(cat) q.set("category", cat); else if(ev.city) q.set("city", ev.city);
1751 2556 if(ev.region && cat) q.set("region", ev.region);
1752 2557 try{
1753 2558 const sim = await (await fetch(`/api/events?${q}`)).json();
1754 const others = sim.events.filter(e=>e.uid!==ev.uid).slice(0,6);
1755 $("d-similar").innerHTML = others.map(card).join("") ||
1756 `<div class="empty">Rien de similaire pour l'instant.</div>`;
1757 bindNav($("d-similar"));
1758 }catch(e){ $("d-similar").innerHTML = ""; }
1759
1760 show("view-detail");
1761 window.scrollTo({top:0});
2559 + const others = sim.events.filter(e => e.uid !== ev.uid).slice(0, 6);
2560 + const host = $("d-similar");
2561 + if(host && SK_STATE.uid === ev.uid){ host.innerHTML = others.map(card).join("") || `<div class="sk-empty">Rien de similaire pour l'instant.</div>`; bindNav(host); favPaint(); }
2562 + }catch(e){ const host = $("d-similar"); if(host) host.innerHTML = ""; }
2563 +}
2564 +
2565 +/* « ✦ Demander à Ka » : bottom sheet de démarrage → widget KA Agent (ka-agent.js) */
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 2600 /* ==================== vue /stats — module ka-stats (vanilla + SVG) =========
@@ -2661,6 +3497,10 @@ async function showFavoris(){
2661 3497 const VIEWS = ["view-list","view-detail","view-contact","view-stats","view-favoris"];
2662 3498 function show(id){
2663 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 3506 function showList(){
@@ -2713,7 +3553,7 @@ function bindNav(root){
2713 3553 });
2714 3554 }
2715 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 3558 /* ---------- KA Nav v2 : menu mobile plein écran (2026-08-25) ---------- */
2719 3559 (function(){
2720 3560