SPB Git

spb/sorti-ka

Public

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

Harmonisation ka-ui — accent framboise, badge Groupe KA, KaFooter, page /contact

- tokens.css (ka-ui vendoré, src/ka/) chargé en premier ; le CSS local ne
  garde que le bloc accent framboise (--accent #d6336c + alias --lime) et
  le CSS métier — socle dédupliqué, couleurs en dur pointées vers les tokens
- Header : badge « Un service Groupe KA » (→ groupe-ka.com) à côté du logo,
  lien Contact, empilement propre sous 760 px (badge visible sur mobile)
- Footer : remplacé par le footer commun KaFooter (siteId sorti-ka) —
  3 courriels + rôles, avertissement agrégateur, 15 sites, liens légaux du
  hub + attribution des données conservée
- Page /contact : vue SPA (contenu ecosystem.json) + route SSR seo.py
  (title/canonical/og + contenu robots) + entrée sitemap-pages.xml
- KA ID : lien « Créer un compte » → groupe-ka.com/connexion à côté du
  bouton « Se connecter avec KA »
- SEO : title « Sorti-Ka — Un service Groupe KA »
- Responsive/tactile : overflow-x clip (tokens), nav/chips/liens ≥ 44 px,
  chips 44 px sur pointeur tactile

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Aug 18, 2026) parent 69b8641 Browse files

6 changed files +603 −82

modified frontend/index.html +183 −81
@@ -3,9 +3,12 @@
3 File: index.html 3 File: index.html
4 Desc: Frontend Sorti-Ka — recherche + fiches événement (SPA sans build, 4 Desc: Frontend Sorti-Ka — recherche + fiches événement (SPA sans build,
5 routeur History API, consomme /api). Design system Groupe KA 5 routeur History API, consomme /api). Design system Groupe KA
6− « éditorial sharp » : papier #f5f3ee · encre #141814 · vert #1c5c41 · 6+ « éditorial sharp » via ka-ui (src/ka/tokens.css, chargé en premier) :
7− lime #d9f26b · Space Grotesk / Inter / JetBrains Mono · bordures 7+ papier #f5f3ee · encre #141814 · vert #1c5c41 · ACCENT FRAMBOISE
8− encre + ombres décalées + grain de film. SSR : sortika/seo.py 8+ #d6336c · Space Grotesk / Inter / JetBrains Mono · bordures encre +
9+ ombres décalées + grain de film. Footer commun KaFooter et page
10+ /contact : contenu issu de src/ka/ecosystem.json (source de vérité —
11+ resynchroniser le HTML si le JSON change). SSR : sortika/seo.py
9 injecte <head> et contenu dans les placeholders SSR_HEAD/SSR_BODY. 12 injecte <head> et contenu dans les placeholders SSR_HEAD/SSR_BODY.
10 --> 13 -->
11 <!DOCTYPE html> 14 <!DOCTYPE html>
@@ -13,54 +16,26 @@
13 <head> 16 <head>
14 <meta charset="utf-8"> 17 <meta charset="utf-8">
15 <meta name="viewport" content="width=device-width, initial-scale=1"> 18 <meta name="viewport" content="width=device-width, initial-scale=1">
16−<title>Sorti·Ka — Toutes les sorties et tous les événements du Québec</title> 19+<title>Sorti-Ka — Un service Groupe KA</title>
17 <meta name="description" content="Agrégateur indépendant de sorties et d'événements — concerts, festivals, spectacles, expositions, famille — dans les 17 régions du Québec, avec dates, lieux et gratuité."> 20 <meta name="description" content="Agrégateur indépendant de sorties et d'événements — concerts, festivals, spectacles, expositions, famille — dans les 17 régions du Québec, avec dates, lieux et gratuité.">
18 <!--SSR_HEAD--> 21 <!--SSR_HEAD-->
19 <link rel="preconnect" href="https://fonts.googleapis.com"> 22 <link rel="preconnect" href="https://fonts.googleapis.com">
20 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 23 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
21 <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet"> 24 <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
25+<link rel="stylesheet" href="/src/ka/tokens.css">
22 <style> 26 <style>
27+/* ka-ui/tokens.css (chargé ci-dessus) fournit palette, socle et composants
28+ communs ; ici : SEULEMENT l'accent framboise de Sorti·Ka + le CSS métier. */
23 :root{ 29 :root{
24− /* système de design Groupe KA « éditorial sharp » */ 30+ --accent:#d6336c; --accent-soft:#fbe0eb; --accent-deep:#a12551; --on-accent:#ffffff;
25− --paper:#f5f3ee; --surface:#fff; --surface-2:#faf9f5; 31+ /* alias rétro-compatibles — tout le CSS local en --lime suit l'accent */
26− --ink:#141814; --ink-2:#4d5551; --ink-3:#8b928c; 32+ --lime:var(--accent); --lime-soft:var(--accent-soft);
27− --line:rgba(20,24,20,.14); --line-strong:rgba(20,24,20,.85);
28− --green:#1c5c41; --green-deep:#123f2e;
29− --lime:#d9f26b; --lime-soft:#f0f9d2;
30− --amber:#e8a33d; --amber-soft:#fdf3e2;
31− --danger:#b3423a; --danger-soft:#fbe9e7;
32− --r-card:10px; --r-ctl:6px;
33− --shadow-flat:0 1px 2px rgba(20,24,20,.05);
34− --shadow-off:6px 6px 0 var(--ink);
35− --shadow-off-soft:8px 8px 0 rgba(20,24,20,.08);
36− --font-display:"Space Grotesk",system-ui,sans-serif;
37− --font-body:"Inter",system-ui,sans-serif;
38− --font-mono:"JetBrains Mono",ui-monospace,monospace;
39 } 33 }
40−*{box-sizing:border-box} 34+h1,h2,h3,h4{margin:0}
41−html{scroll-behavior:smooth} 35+a{text-decoration:none}
42−body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--font-body);
43− font-size:15px;line-height:1.55;-webkit-font-smoothing:antialiased}
44−body::before{content:"";position:fixed;inset:0;pointer-events:none;opacity:.35;z-index:9999;
45− background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='linear' slope='0.06'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)'/%3E%3C/svg%3E")}
46−body>*{position:relative}
47−h1,h2,h3,h4{font-family:var(--font-display);letter-spacing:-.03em;margin:0}
48−a{color:inherit;text-decoration:none}
49 button{font-family:inherit} 36 button{font-family:inherit}
50−img{display:block}
51−::selection{background:var(--lime);color:var(--ink)}
52−:focus-visible{outline:2px solid var(--green);outline-offset:2px}
53 .container{max-width:1240px;margin:0 auto;padding:0 24px} 37 .container{max-width:1240px;margin:0 auto;padding:0 24px}
54 @media(max-width:640px){.container{padding:0 16px}} 38 @media(max-width:640px){.container{padding:0 16px}}
55−
56−/* ---------- micro-typo signature ---------- */
57−.kicker{display:inline-flex;align-items:center;gap:10px;font-family:var(--font-mono);
58− font-size:11.5px;font-weight:500;text-transform:uppercase;letter-spacing:.12em;color:var(--green)}
59−.kicker::before{content:"";width:22px;height:2px;background:var(--green)}
60−.klabel{font-family:var(--font-mono);font-size:10px;font-weight:700;
61− text-transform:uppercase;letter-spacing:.1em;color:var(--ink-3)}
62−.hl{display:inline-block;background:var(--lime);border-radius:8px;padding:0 10px 2px;transform:rotate(-1deg)}
63−
64 /* ---------- cartes & boutons signature ---------- */ 39 /* ---------- cartes & boutons signature ---------- */
65 .gk-card{background:var(--surface);border:1.5px solid var(--ink); 40 .gk-card{background:var(--surface);border:1.5px solid var(--ink);
66 border-radius:var(--r-card);box-shadow:var(--shadow-off-soft)} 41 border-radius:var(--r-card);box-shadow:var(--shadow-off-soft)}
@@ -71,10 +46,10 @@ img{display:block}
71 font-family:var(--font-display);font-size:14px;font-weight:700;cursor:pointer; 46 font-family:var(--font-display);font-size:14px;font-weight:700;cursor:pointer;
72 transition:background .15s ease,transform .1s ease,box-shadow .1s ease} 47 transition:background .15s ease,transform .1s ease,box-shadow .1s ease}
73 .btn:active{transform:translate(2px,2px);box-shadow:none!important} 48 .btn:active{transform:translate(2px,2px);box-shadow:none!important}
74−.btn-primary{background:var(--ink);color:var(--lime);box-shadow:4px 4px 0 rgba(20,24,20,.25)} 49+.btn-primary{background:var(--ink);color:var(--accent);box-shadow:4px 4px 0 rgba(20,24,20,.25)}
75−.btn-primary:hover{background:var(--green-deep)} 50+.btn-primary:hover{background:var(--accent-deep)}
76 .btn-ghost{background:var(--surface);color:var(--ink);box-shadow:4px 4px 0 rgba(20,24,20,.12)} 51 .btn-ghost{background:var(--surface);color:var(--ink);box-shadow:4px 4px 0 rgba(20,24,20,.12)}
77−.btn-ghost:hover{background:var(--lime-soft)} 52+.btn-ghost:hover{background:var(--accent-soft)}
78 53
79 /* ---------- header / ticker ---------- */ 54 /* ---------- header / ticker ---------- */
80 .ticker{background:var(--ink);color:rgba(245,243,238,.85);overflow:hidden; 55 .ticker{background:var(--ink);color:rgba(245,243,238,.85);overflow:hidden;
@@ -88,22 +63,33 @@ img{display:block}
88 .header{position:sticky;top:0;z-index:50;background:rgba(245,243,238,.9); 63 .header{position:sticky;top:0;z-index:50;background:rgba(245,243,238,.9);
89 backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-bottom:1.5px solid var(--ink)} 64 backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-bottom:1.5px solid var(--ink)}
90 .header-inner{display:flex;align-items:center;gap:18px;height:64px} 65 .header-inner{display:flex;align-items:center;gap:18px;height:64px}
66+.header-inner .gk-badge{flex:none}
67+@media(max-width:760px){
68+ /* le header s'empile (badge Groupe KA + nav visibles) — pas de débordement */
69+ .header-inner{flex-wrap:wrap;height:auto;min-height:56px;padding:8px 0;gap:6px 12px}
70+ .nav{margin-left:0;flex-wrap:wrap}
71+}
91 .brand{font-family:var(--font-display);font-weight:700;font-size:24px;letter-spacing:-.03em; 72 .brand{font-family:var(--font-display);font-weight:700;font-size:24px;letter-spacing:-.03em;
92 display:flex;align-items:baseline;line-height:1;cursor:pointer} 73 display:flex;align-items:baseline;line-height:1;cursor:pointer}
93−.brand .ka{background:var(--lime);border-radius:6px;padding:0 6px 2px;margin-left:2px;transform:rotate(-1deg)} 74+.brand .ka{background:var(--accent);color:var(--on-accent);border-radius:6px;padding:0 6px 2px;margin-left:2px;transform:rotate(-1deg)}
94 .brand-tag{font-family:var(--font-mono);font-size:10px;text-transform:uppercase; 75 .brand-tag{font-family:var(--font-mono);font-size:10px;text-transform:uppercase;
95 letter-spacing:.08em;color:var(--ink-3);margin-left:8px} 76 letter-spacing:.08em;color:var(--ink-3);margin-left:8px}
96 @media(max-width:900px){.brand-tag{display:none}} 77 @media(max-width:900px){.brand-tag{display:none}}
97 .nav{margin-left:auto;display:flex;gap:6px} 78 .nav{margin-left:auto;display:flex;gap:6px}
98−.nav a{padding:8px 14px;border:1.5px solid transparent;border-radius:var(--r-ctl); 79+.nav a{display:inline-flex;align-items:center;min-height:44px;padding:8px 14px;
80+ border:1.5px solid transparent;border-radius:var(--r-ctl);
99 font-family:var(--font-display);font-weight:700;font-size:13.5px;transition:all .15s ease} 81 font-family:var(--font-display);font-weight:700;font-size:13.5px;transition:all .15s ease}
100−.nav a:hover{border-color:var(--ink);background:var(--lime-soft)} 82+.nav a:hover{border-color:var(--ink);background:var(--accent-soft)}
101 .btn-ka{display:inline-flex;align-items:center;gap:8px;padding:8px 15px;min-height:38px; 83 .btn-ka{display:inline-flex;align-items:center;gap:8px;padding:8px 15px;min-height:38px;
102 border:1.5px solid var(--ink);border-radius:var(--r-ctl);background:var(--ink); 84 border:1.5px solid var(--ink);border-radius:var(--r-ctl);background:var(--ink);
103 color:var(--lime);font-family:var(--font-display);font-weight:700;font-size:13px; 85 color:var(--lime);font-family:var(--font-display);font-weight:700;font-size:13px;
104 cursor:pointer;box-shadow:3px 3px 0 rgba(20,24,20,.25);transition:background .15s ease} 86 cursor:pointer;box-shadow:3px 3px 0 rgba(20,24,20,.25);transition:background .15s ease}
105−.btn-ka:hover{background:var(--green-deep)} 87+.btn-ka:hover{background:var(--accent-deep)}
106−.btn-ka .kdot{background:var(--lime);color:var(--ink);border-radius:4px; 88+#auth{display:inline-flex;align-items:center;gap:10px;flex:none}
89+.ka-signup{font-family:var(--font-mono);font-size:10px;font-weight:700;text-transform:uppercase;
90+ letter-spacing:.06em;color:var(--ink-2);min-height:44px;display:inline-flex;align-items:center}
91+.ka-signup:hover{color:var(--accent-deep);text-decoration:underline}
92+.btn-ka .kdot{background:var(--accent);color:var(--on-accent);border-radius:4px;
107 padding:0 5px;font-weight:800;transform:rotate(-1deg)} 93 padding:0 5px;font-weight:800;transform:rotate(-1deg)}
108 .user-chip{display:inline-flex;align-items:center;gap:9px;border:1.5px solid var(--ink); 94 .user-chip{display:inline-flex;align-items:center;gap:9px;border:1.5px solid var(--ink);
109 border-radius:var(--r-pill,999px);background:var(--surface);padding:4px 12px 4px 5px; 95 border-radius:var(--r-pill,999px);background:var(--surface);padding:4px 12px 4px 5px;
@@ -149,9 +135,10 @@ img{display:block}
149 .chips{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px} 135 .chips{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}
150 .chip{border:1.5px solid var(--ink);background:var(--surface);border-radius:var(--r-ctl); 136 .chip{border:1.5px solid var(--ink);background:var(--surface);border-radius:var(--r-ctl);
151 padding:6px 12px;font-family:var(--font-display);font-size:12.5px;font-weight:700; 137 padding:6px 12px;font-family:var(--font-display);font-size:12.5px;font-weight:700;
138+ text-transform:none;letter-spacing:0;min-height:34px;
152 cursor:pointer;color:var(--ink);line-height:1;transition:all .12s ease} 139 cursor:pointer;color:var(--ink);line-height:1;transition:all .12s ease}
153−.chip:hover{background:var(--lime-soft)} 140+.chip:hover{background:var(--accent-soft)}
154−.chip.on{background:var(--ink);color:var(--lime);box-shadow:3px 3px 0 rgba(20,24,20,.25)} 141+.chip.on{background:var(--ink);color:var(--accent);box-shadow:3px 3px 0 rgba(20,24,20,.25)}
155 .chip.free.on{background:var(--green);border-color:var(--green);color:#fff} 142 .chip.free.on{background:var(--green);border-color:var(--green);color:#fff}
156 143
157 /* ---------- résultats ---------- */ 144 /* ---------- résultats ---------- */
@@ -166,7 +153,7 @@ img{display:block}
166 .card-img img{width:100%;height:100%;object-fit:cover} 153 .card-img img{width:100%;height:100%;object-fit:cover}
167 .badge-date{position:absolute;left:10px;bottom:10px;font-family:var(--font-mono); 154 .badge-date{position:absolute;left:10px;bottom:10px;font-family:var(--font-mono);
168 font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.04em; 155 font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;
169− color:var(--ink);background:var(--lime);border:1.5px solid var(--ink); 156+ color:var(--on-accent);background:var(--accent);border:1.5px solid var(--ink);
170 border-radius:var(--r-ctl);padding:4px 9px;white-space:nowrap;box-shadow:2px 2px 0 var(--ink)} 157 border-radius:var(--r-ctl);padding:4px 9px;white-space:nowrap;box-shadow:2px 2px 0 var(--ink)}
171 .card-body{padding:15px 16px 14px;display:flex;flex-direction:column;gap:8px;flex:1} 158 .card-body{padding:15px 16px 14px;display:flex;flex-direction:column;gap:8px;flex:1}
172 .card-top{display:flex;justify-content:space-between;gap:8px;align-items:flex-start} 159 .card-top{display:flex;justify-content:space-between;gap:8px;align-items:flex-start}
@@ -194,9 +181,10 @@ img{display:block}
194 181
195 /* ---------- fiche événement ---------- */ 182 /* ---------- fiche événement ---------- */
196 .detail{padding:34px 0 50px} 183 .detail{padding:34px 0 50px}
197−.back{display:inline-flex;align-items:center;gap:8px;font-family:var(--font-mono); 184+.back{display:inline-flex;align-items:center;gap:8px;min-height:44px;font-family:var(--font-mono);
198 font-size:11.5px;font-weight:700;text-transform:uppercase;letter-spacing:.08em; 185 font-size:11.5px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;
199− color:var(--green);cursor:pointer;margin-bottom:20px} 186+ color:var(--green);cursor:pointer;margin-bottom:8px}
187+@media(pointer:coarse){.chip{min-height:44px}.user-chip .out{min-height:32px;padding:2px 8px}}
200 .back:hover{text-decoration:underline} 188 .back:hover{text-decoration:underline}
201 .detail-grid{display:grid;grid-template-columns:1.5fr 1fr;gap:26px;align-items:start} 189 .detail-grid{display:grid;grid-template-columns:1.5fr 1fr;gap:26px;align-items:start}
202 @media(max-width:860px){.detail-grid{grid-template-columns:1fr}} 190 @media(max-width:860px){.detail-grid{grid-template-columns:1fr}}
@@ -231,13 +219,32 @@ img{display:block}
231 color:var(--green);text-transform:uppercase;letter-spacing:.06em} 219 color:var(--green);text-transform:uppercase;letter-spacing:.06em}
232 .src-card .n{font-size:12.5px;color:var(--ink-3);margin-top:6px} 220 .src-card .n{font-size:12.5px;color:var(--ink-3);margin-top:6px}
233 .src-card .n b{color:var(--green-deep)} 221 .src-card .n b{color:var(--green-deep)}
234−footer{background:var(--ink);color:rgba(245,243,238,.72);padding:36px 0 46px;font-size:13px} 222+.ka-footer{margin-top:0}
235−footer .fam{display:flex;flex-wrap:wrap;gap:4px 14px;margin-bottom:14px} 223+
236−footer .fam a{font-family:var(--font-display);font-weight:700;color:#f5f3ee;padding:4px 0} 224+/* ---------- page /contact ---------- */
237−footer .fam a:hover{color:var(--lime)} 225+.contact-page{padding:44px 0 64px}
238−footer .fam .lbl{font-family:var(--font-mono);font-size:10px;text-transform:uppercase; 226+.contact-page h1{font-size:clamp(30px,4.2vw,48px);line-height:1.06;margin-top:16px;max-width:820px}
239− letter-spacing:.08em;color:rgba(245,243,238,.45);align-self:center} 227+.contact-page .lede{color:var(--ink-2);font-size:16px;max-width:660px;margin:16px 0 0}
240−footer a.mail{color:var(--lime)} 228+.contact-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(260px,100%),1fr));
229+ gap:18px;margin-top:28px}
230+.contact-card{padding:20px 22px;display:flex;flex-direction:column;gap:6px}
231+.contact-card a.mail{display:inline-flex;align-items:center;min-height:44px;
232+ font-family:var(--font-mono);font-weight:700;font-size:14px;color:var(--accent-deep);
233+ word-break:break-all}
234+.contact-card a.mail:hover{text-decoration:underline;text-underline-offset:4px}
235+.contact-note{margin-top:26px;padding:18px 20px;border-left:3px solid var(--accent);
236+ background:var(--surface);border-radius:0 var(--r-card) var(--r-card) 0;
237+ box-shadow:var(--shadow-flat);max-width:760px}
238+.contact-note b{color:var(--ink)}
239+.contact-sites{margin-top:36px}
240+.contact-sites ul{list-style:none;display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 0;padding:0}
241+.contact-sites a{display:inline-flex;align-items:center;min-height:44px;padding:8px 14px;
242+ border:1.5px solid var(--ink);border-radius:var(--r-ctl);background:var(--surface);
243+ font-family:var(--font-display);font-weight:700;font-size:13px;
244+ box-shadow:3px 3px 0 rgba(20,24,20,.12);transition:background .15s ease}
245+.contact-sites a:hover{background:var(--accent-soft)}
246+.contact-sites a small{font-family:var(--font-mono);font-weight:500;font-size:10px;
247+ color:var(--ink-3);margin-left:8px}
241 #ssr{display:none} 248 #ssr{display:none}
242 .hidden{display:none!important} 249 .hidden{display:none!important}
243 </style> 250 </style>
@@ -258,9 +265,13 @@ footer a.mail{color:var(--lime)}
258 <header class="header"> 265 <header class="header">
259 <div class="container header-inner"> 266 <div class="container header-inner">
260 <a class="brand" data-nav href="/">Sorti<span class="ka">·Ka</span></a> 267 <a class="brand" data-nav href="/">Sorti<span class="ka">·Ka</span></a>
268+ <a class="gk-badge" href="https://www.groupe-ka.com" target="_blank" rel="noopener noreferrer"
269+ aria-label="Un service Groupe KA — visiter le portail de l'écosystème">
270+ Un service <b>Groupe<span class="ka">KA</span></b></a>
261 <span class="brand-tag">Sorties &amp; événements — Québec</span> 271 <span class="brand-tag">Sorties &amp; événements — Québec</span>
262 <nav class="nav"> 272 <nav class="nav">
263 <a href="/#sources">Sources</a> 273 <a href="/#sources">Sources</a>
274+ <a data-nav href="/contact">Contact</a>
264 <a href="/docs" target="_blank" rel="noopener">API</a> 275 <a href="/docs" target="_blank" rel="noopener">API</a>
265 </nav> 276 </nav>
266 <span id="auth"></span> 277 <span id="auth"></span>
@@ -343,23 +354,101 @@ footer a.mail{color:var(--lime)}
343 </div> 354 </div>
344 </main> 355 </main>
345 356
346−<footer> 357+<!-- ======== VUE CONTACT (/contact) — contenu : src/ka/ecosystem.json ======== -->
358+<main id="view-contact" class="hidden">
359+ <div class="container contact-page">
360+ <span class="kicker">Contact · Groupe KA</span>
361+ <h1>Écrire au <span class="hl">Groupe KA</span></h1>
362+ <p class="lede">Sorti·Ka est un service du Groupe KA, holding québécois d'agrégateurs de
363+ produits et services entièrement automatisés. Trois adresses, selon votre besoin :</p>
364+
365+ <div class="contact-grid">
366+ <div class="contact-card gk-card">
367+ <span class="klabel">Projets, partenariats &amp; données</span>
368+ <a class="mail" href="mailto:contact@groupe-ka.com">contact@groupe-ka.com</a>
369+ </div>
370+ <div class="contact-card gk-card">
371+ <span class="klabel">Médias &amp; questions générales</span>
372+ <a class="mail" href="mailto:info@groupe-ka.com">info@groupe-ka.com</a>
373+ </div>
374+ <div class="contact-card gk-card">
375+ <span class="klabel">Légal, vie privée &amp; Loi 25</span>
376+ <a class="mail" href="mailto:admin@groupe-ka.com">admin@groupe-ka.com</a>
377+ </div>
378+ </div>
379+
380+ <p class="contact-note"><b>Groupe KA est un agrégateur de contenu :</b> nous ne vendons rien,
381+ ne louons rien et ne sommes partie à aucune transaction. Pour les billets, contactez la
382+ billetterie ou l'organisateur indiqué sur chaque fiche.</p>
383+ <p class="contact-note">La création de compte <b>KA ID</b> se fait sur le hub
384+ <a href="https://www.groupe-ka.com/connexion" target="_blank" rel="noopener noreferrer"
385+ style="color:var(--accent-deep);font-weight:700">groupe-ka.com/connexion</a> ;
386+ chaque site de l'écosystème délègue sa connexion au hub.</p>
387+
388+ <div class="contact-sites">
389+ <span class="kicker">L'écosystème ·Ka</span>
390+ <ul>
391+ <li><a href="https://www.groupe-ka.com" target="_blank" rel="noopener noreferrer">Groupe KA<small>le portail</small></a></li>
392+ <li><a href="https://www.trouve-ka.com" target="_blank" rel="noopener noreferrer">Trouve·Ka<small>recherche</small></a></li>
393+ <li><a href="https://www.lou-ka.com" target="_blank" rel="noopener noreferrer">Lou·Ka<small>logements</small></a></li>
394+ <li><a href="https://www.immo-ka.com" target="_blank" rel="noopener noreferrer">Immo·Ka<small>propriétés</small></a></li>
395+ <li><a href="https://www.vrai-prix.com" target="_blank" rel="noopener noreferrer">Vrai-Prix<small>valeur immo</small></a></li>
396+ <li><a href="https://www.auto-ka.com" target="_blank" rel="noopener noreferrer">Auto·Ka<small>autos usagées</small></a></li>
397+ <li><a href="https://www.fabri-ka.com" target="_blank" rel="noopener noreferrer">Fabri·Ka<small>fait au Québec</small></a></li>
398+ <li><a href="https://www.food-ka.com" target="_blank" rel="noopener noreferrer">Food·Ka<small>épicerie</small></a></li>
399+ <li><a href="https://www.resto-ka.com" target="_blank" rel="noopener noreferrer">Resto·Ka<small>restos</small></a></li>
400+ <li><a href="https://www.crea-ka.com" target="_blank" rel="noopener noreferrer">Créa·Ka<small>créateurs</small></a></li>
401+ <li><a href="https://www.api-ka.com" target="_blank" rel="noopener noreferrer">API·Ka<small>données API</small></a></li>
402+ </ul>
403+ </div>
404+ </div>
405+</main>
406+
407+<!-- ======== FOOTER COMMUN GROUPE KA (ka-ui/KaFooter, siteId="sorti-ka") ======== -->
408+<footer class="ka-footer" id="contact">
347 <div class="container"> 409 <div class="container">
348− <div class="fam"> 410+ <a class="wordmark" href="https://www.groupe-ka.com" target="_blank" rel="noopener noreferrer"
349− <span class="lbl">Famille ·Ka</span> 411+ aria-label="Groupe KA — le portail de l'écosystème">Groupe <span class="ka">KA</span></a>
350− <a href="https://www.lou-ka.com" target="_blank" rel="noopener">Lou·Ka</a> 412+ <p class="desc">Holding québécois d'agrégateurs de produits et services entièrement automatisés.
351− <a href="https://www.immo-ka.com" target="_blank" rel="noopener">Immo·Ka</a> 413+ Des centaines de connecteurs lisent les sites à la source, normalisent la donnée et se
352− <a href="https://www.auto-ka.com" target="_blank" rel="noopener">Auto·Ka</a> 414+ resynchronisent seuls. <b style="color:var(--paper)">Sorti·Ka</b> — Toutes les sorties,
353− <a href="https://www.food-ka.com" target="_blank" rel="noopener">Food·Ka</a> 415+ dans les 17 régions — est un service Groupe KA.</p>
354− <a href="https://www.fabri-ka.com" target="_blank" rel="noopener">Fabri·Ka</a> 416+ <p class="notice"><b>Groupe KA est un agrégateur de contenu : nous ne vendons rien,
355− <a href="https://www.toit-ka.com" target="_blank" rel="noopener">Toit·Ka</a> 417+ ne louons rien et ne sommes partie à aucune transaction.</b></p>
356− <a href="https://www.forma-ka.com" target="_blank" rel="noopener">Forma·Ka</a> 418+ <ul class="sites">
357− <a href="https://www.groupe-ka.com" target="_blank" rel="noopener">Groupe KA</a> 419+ <li><a href="https://www.groupe-ka.com" target="_blank" rel="noopener noreferrer">Groupe KA</a></li>
420+ <li><a href="https://www.trouve-ka.com" target="_blank" rel="noopener noreferrer">Trouve·Ka</a></li>
421+ <li><a href="https://www.lou-ka.com" target="_blank" rel="noopener noreferrer">Lou·Ka</a></li>
422+ <li><a href="https://www.immo-ka.com" target="_blank" rel="noopener noreferrer">Immo·Ka</a></li>
423+ <li><a href="https://www.vrai-prix.com" target="_blank" rel="noopener noreferrer">Vrai-Prix</a></li>
424+ <li><a href="https://www.auto-ka.com" target="_blank" rel="noopener noreferrer">Auto·Ka</a></li>
425+ <li><a href="https://www.fabri-ka.com" target="_blank" rel="noopener noreferrer">Fabri·Ka</a></li>
426+ <li><a href="https://www.food-ka.com" target="_blank" rel="noopener noreferrer">Food·Ka</a></li>
427+ <li><a href="https://www.resto-ka.com" target="_blank" rel="noopener noreferrer">Resto·Ka</a></li>
428+ <li><a href="https://www.crea-ka.com" target="_blank" rel="noopener noreferrer">Créa·Ka</a></li>
429+ <li><a href="https://www.api-ka.com" target="_blank" rel="noopener noreferrer">API·Ka</a></li>
430+ <li><a href="https://www.valoplex.com" target="_blank" rel="noopener noreferrer">ValoPlex</a></li>
431+ <li><a href="https://www.ka2.bot" target="_blank" rel="noopener noreferrer">Ka2</a></li>
432+ <li><a href="https://www.ka4.bot" target="_blank" rel="noopener noreferrer">Ka4</a></li>
433+ <li><a href="https://www.ka6.bot" target="_blank" rel="noopener noreferrer">Ka6</a></li>
434+ </ul>
435+ <div class="contacts">
436+ <p style="margin:0"><a href="mailto:contact@groupe-ka.com">contact@groupe-ka.com</a>
437+ <span>Projets, partenariats &amp; données</span></p>
438+ <p style="margin:0"><a href="mailto:info@groupe-ka.com">info@groupe-ka.com</a>
439+ <span>Médias &amp; questions générales</span></p>
440+ <p style="margin:0"><a href="mailto:admin@groupe-ka.com">admin@groupe-ka.com</a>
441+ <span>Légal, vie privée &amp; Loi 25</span></p>
358 </div> 442 </div>
359− <div>Données : SIT Québec (MTO, CC 4.0) · Ville de Montréal (CC BY 4.0) · Ville de Laval (CC 4.0) · 443+ <p class="legal">Données : SIT Québec (MTO, CC 4.0) · Ville de Montréal (CC BY 4.0) ·
360− Le point de vente · evenko · Atuvu.ca · La Vitrine · Répertoire des municipalités (MAMH).</div> 444+ Ville de Laval (CC 4.0) · Le point de vente · evenko · Atuvu.ca · La Vitrine ·
361− <div style="margin-top:8px">© 2026 Sorti·Ka — Simon-Pierre Boucher · 445+ Répertoire des municipalités (MAMH).</p>
362− <a class="mail" href="mailto:contact@spboucher.ai">contact@spboucher.ai</a></div> 446+ <p class="legal">© 2026 Groupe KA — Simon-Pierre Boucher
447+ · <a data-nav href="/contact">Contact</a>
448+ · <a href="https://www.groupe-ka.com/conditions" target="_blank" rel="noopener noreferrer">Conditions d'utilisation</a>
449+ · <a href="https://www.groupe-ka.com/confidentialite" target="_blank" rel="noopener noreferrer">Politique de confidentialité</a>
450+ · <a href="https://www.groupe-ka.com/loi-25" target="_blank" rel="noopener noreferrer">Protection des renseignements personnels (Loi 25)</a>
451+ · <a href="https://www.groupe-ka.com/bots" target="_blank" rel="noopener noreferrer">Transparence des robots d'indexation</a></p>
363 </div> 452 </div>
364 </footer> 453 </footer>
365 454
@@ -519,21 +608,32 @@ async function showDetail(uid){
519 bindNav($("d-similar")); 608 bindNav($("d-similar"));
520 }catch(e){ $("d-similar").innerHTML = ""; } 609 }catch(e){ $("d-similar").innerHTML = ""; }
521 610
522− $("view-list").classList.add("hidden"); 611+ show("view-detail");
523− $("view-detail").classList.remove("hidden");
524 window.scrollTo({top:0}); 612 window.scrollTo({top:0});
525 } 613 }
526 614
615+/* ---------- vues ---------- */
616+const VIEWS = ["view-list","view-detail","view-contact"];
617+function show(id){
618+ VIEWS.forEach(v => $(v).classList.toggle("hidden", v !== id));
619+}
620+
527 function showList(){ 621 function showList(){
528− document.title = "Sorti·Ka — Toutes les sorties et tous les événements du Québec"; 622+ document.title = "Sorti-Ka — Un service Groupe KA";
529− $("view-detail").classList.add("hidden"); 623+ show("view-list");
530− $("view-list").classList.remove("hidden"); 624+}
625+
626+function showContact(){
627+ document.title = "Contact — Sorti-Ka | Un service Groupe KA";
628+ show("view-contact");
629+ window.scrollTo({top:0});
531 } 630 }
532 631
533 /* ---------- routeur ---------- */ 632 /* ---------- routeur ---------- */
534 function route(){ 633 function route(){
535 const m = location.pathname.match(/^\/evenement\/(.+)$/); 634 const m = location.pathname.match(/^\/evenement\/(.+)$/);
536 if(m) showDetail(decodeURIComponent(m[1])); 635 if(m) showDetail(decodeURIComponent(m[1]));
636+ else if(location.pathname === "/contact") showContact();
537 else showList(); 637 else showList();
538 } 638 }
539 function go(path){ 639 function go(path){
@@ -606,7 +706,9 @@ async function init(){
606 }; 706 };
607 } else { 707 } else {
608 $("auth").innerHTML = `<a class="btn-ka" href="/api/auth/ka/login"> 708 $("auth").innerHTML = `<a class="btn-ka" href="/api/auth/ka/login">
609− <span class="kdot">·Ka</span> Se connecter avec KA</a>`; 709+ <span class="kdot">·Ka</span> Se connecter avec KA</a>
710+ <a class="ka-signup" href="https://www.groupe-ka.com/connexion"
711+ target="_blank" rel="noopener">Créer un compte</a>`;
610 } 712 }
611 } 713 }
612 }catch(e){} 714 }catch(e){}
added frontend/src/ka/GroupeKaBadge.tsx +21 −0
@@ -0,0 +1,21 @@
1+// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2+// ka-ui/react/GroupeKaBadge.tsx — badge « Un service Groupe KA », cliquable
3+// vers le hub. À placer dans le header de chaque site (à côté du logo de la
4+// marque). Variante dark pour les surfaces encre. Zone de protection : le
5+// padding interne du badge suffit ; ne rien coller à moins de 8 px du badge.
6+export default function GroupeKaBadge({ dark = false }: { dark?: boolean }) {
7+ return (
8+ <a
9+ className={`gk-badge${dark ? " gk-badge--dark" : ""}`}
10+ href="https://www.groupe-ka.com"
11+ target="_blank"
12+ rel="noopener noreferrer"
13+ aria-label="Un service Groupe KA — visiter le portail de l'écosystème"
14+ >
15+ Un service
16+ <b>
17+ Groupe<span className="ka">KA</span>
18+ </b>
19+ </a>
20+ );
21+}
added frontend/src/ka/KaFooter.tsx +91 −0
@@ -0,0 +1,91 @@
1+// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2+// ka-ui/react/KaFooter.tsx — footer commun Groupe KA (fond encre), à vendorer
3+// dans chaque app Vite/React sous frontend/src/ka/. Consomme ecosystem.json :
4+// toute modification des infos de contact se fait dans ka-ui/ecosystem.json
5+// puis se resynchronise sur les 12 sites (voir ka-ui/README.md).
6+import eco from "./ecosystem.json";
7+
8+type LocalLink = { label: string; href: string };
9+
10+export default function KaFooter({
11+ siteId,
12+ localLegal = [],
13+}: {
14+ /** id du site courant dans ecosystem.json (ex. "resto-ka") */
15+ siteId: string;
16+ /** pages légales PROPRES au site (ex. /conditions), affichées avant celles du hub */
17+ localLegal?: LocalLink[];
18+}) {
19+ const year = new Date().getFullYear();
20+ const site = eco.sites.find((s) => s.id === siteId);
21+ const others = eco.sites.filter((s) => s.id !== siteId);
22+ return (
23+ <footer className="ka-footer" id="contact">
24+ <div className="container">
25+ <a
26+ className="wordmark"
27+ href={eco.hub.url}
28+ target={siteId === "groupe-ka" ? undefined : "_blank"}
29+ rel="noopener noreferrer"
30+ aria-label="Groupe KA — le portail de l'écosystème"
31+ >
32+ Groupe <span className="ka">KA</span>
33+ </a>
34+ <p className="desc">
35+ {eco.org.tagline} Des centaines de connecteurs lisent les sites à la
36+ source, normalisent la donnée et se resynchronisent seuls.{" "}
37+ {site ? (
38+ <>
39+ <b style={{ color: "var(--paper)" }}>{site.wordmark}</b> —{" "}
40+ {site.tagline} — est un service Groupe KA.
41+ </>
42+ ) : null}
43+ </p>
44+ <p className="notice">
45+ <b>{eco.org.disclaimer}</b>
46+ </p>
47+ <ul className="sites">
48+ {others.map((s) => (
49+ <li key={s.id}>
50+ <a href={`https://${s.domain}`} target="_blank" rel="noopener noreferrer">
51+ {s.wordmark}
52+ </a>
53+ </li>
54+ ))}
55+ {eco.extraFooterLinks.map((l) => (
56+ <li key={l.href}>
57+ <a href={l.href} target="_blank" rel="noopener noreferrer">
58+ {l.label}
59+ </a>
60+ </li>
61+ ))}
62+ </ul>
63+ <div className="contacts">
64+ {eco.contacts.map((c) => (
65+ <p key={c.email} style={{ margin: 0 }}>
66+ <a href={`mailto:${c.email}`}>{c.email}</a>
67+ <span>{c.role}</span>
68+ </p>
69+ ))}
70+ </div>
71+ <p className="legal">
72+ © {year} {eco.org.copyrightHolder}
73+ {localLegal.map((l) => (
74+ <span key={l.href}>
75+ {" · "}
76+ <a href={l.href}>{l.label}</a>
77+ </span>
78+ ))}
79+ {eco.legal.map((l) => (
80+ <span key={l.href}>
81+ {" · "}
82+ <a href={l.href} target="_blank" rel="noopener noreferrer">
83+ {l.label}
84+ </a>
85+ </span>
86+ ))}
87+ </p>
88+ </div>
89+ </footer>
90+ );
91+}
added frontend/src/ka/ecosystem.json +45 −0
@@ -0,0 +1,45 @@
1+{
2+ "org": {
3+ "name": "Groupe KA",
4+ "legalName": "Groupe KA — Simon-Pierre Boucher",
5+ "tagline": "Holding québécois d'agrégateurs de produits et services entièrement automatisés.",
6+ "disclaimer": "Groupe KA est un agrégateur de contenu : nous ne vendons rien, ne louons rien et ne sommes partie à aucune transaction.",
7+ "copyrightHolder": "Groupe KA — Simon-Pierre Boucher"
8+ },
9+ "hub": {
10+ "url": "https://www.groupe-ka.com",
11+ "loginPath": "/connexion",
12+ "signupNote": "La création de compte KA ID se fait sur le hub groupe-ka.com ; chaque site délègue sa connexion via /api/auth/ka/login."
13+ },
14+ "contacts": [
15+ { "email": "contact@groupe-ka.com", "role": "Projets, partenariats & données" },
16+ { "email": "info@groupe-ka.com", "role": "Médias & questions générales" },
17+ { "email": "admin@groupe-ka.com", "role": "Légal, vie privée & Loi 25" }
18+ ],
19+ "legal": [
20+ { "label": "Conditions d'utilisation", "href": "https://www.groupe-ka.com/conditions" },
21+ { "label": "Politique de confidentialité", "href": "https://www.groupe-ka.com/confidentialite" },
22+ { "label": "Protection des renseignements personnels (Loi 25)", "href": "https://www.groupe-ka.com/loi-25" },
23+ { "label": "Transparence des robots d'indexation", "href": "https://www.groupe-ka.com/bots" }
24+ ],
25+ "sites": [
26+ { "id": "groupe-ka", "wordmark": "Groupe KA", "domain": "www.groupe-ka.com", "accent": "#d9f26b", "accentSoft": "#f0f9d2", "accentDeep": "#123f2e", "onAccent": "#141814", "tagline": "Le portail de l'écosystème ·Ka" },
27+ { "id": "trouve-ka", "wordmark": "Trouve·Ka", "domain": "www.trouve-ka.com", "accent": "#1c7ed6", "accentSoft": "#e7f2fd", "accentDeep": "#14508f", "onAccent": "#ffffff", "tagline": "Le moteur de recherche du web québécois" },
28+ { "id": "lou-ka", "wordmark": "Lou·Ka", "domain": "www.lou-ka.com", "accent": "#ff6a00", "accentSoft": "#fff1e6", "accentDeep": "#cc5500", "onAccent": "#ffffff", "tagline": "Tous les logements à louer" },
29+ { "id": "immo-ka", "wordmark": "Immo·Ka", "domain": "www.immo-ka.com", "accent": "#e23744", "accentSoft": "#fbe0e2", "accentDeep": "#a8232e", "onAccent": "#ffffff", "tagline": "Toutes les propriétés à vendre" },
30+ { "id": "vrai-prix", "wordmark": "Vrai-Prix", "domain": "www.vrai-prix.com", "accent": "#ff5148", "accentSoft": "#ffe3e0", "accentDeep": "#9e2a25", "onAccent": "#ffffff", "tagline": "La valeur réelle de chaque propriété" },
31+ { "id": "auto-ka", "wordmark": "Auto·Ka", "domain": "www.auto-ka.com", "accent": "#ff5a2a", "accentSoft": "#ffe8de", "accentDeep": "#cc3f16", "onAccent": "#ffffff", "tagline": "Les voitures usagées du Québec" },
32+ { "id": "fabri-ka", "wordmark": "Fabri·Ka", "domain": "www.fabri-ka.com", "accent": "#c4532e", "accentSoft": "#f7e3da", "accentDeep": "#a94525", "onAccent": "#ffffff", "tagline": "Les produits fabriqués au Québec" },
33+ { "id": "food-ka", "wordmark": "Food·Ka", "domain": "www.food-ka.com", "accent": "#1f9d55", "accentSoft": "#e2f5ea", "accentDeep": "#157a40", "onAccent": "#ffffff", "tagline": "Les prix d'épicerie, suivis à la source" },
34+ { "id": "resto-ka", "wordmark": "Resto·Ka", "domain": "www.resto-ka.com", "accent": "#f08c00", "accentSoft": "#fdeed7", "accentDeep": "#b96a00", "onAccent": "#141814", "tagline": "Chaque resto, chaque plat, chaque prix" },
35+ { "id": "sorti-ka", "wordmark": "Sorti·Ka", "domain": "www.sorti-ka.com", "accent": "#d6336c", "accentSoft": "#fbe0eb", "accentDeep": "#a12551", "onAccent": "#ffffff", "tagline": "Toutes les sorties, dans les 17 régions" },
36+ { "id": "crea-ka", "wordmark": "Créa·Ka", "domain": "www.crea-ka.com", "accent": "#7048e8", "accentSoft": "#ece5fc", "accentDeep": "#5433b8", "onAccent": "#ffffff", "tagline": "Les créateurs d'ici, tous leurs liens" },
37+ { "id": "api-ka", "wordmark": "API·Ka", "domain": "www.api-ka.com", "accent": "#3b5bdb", "accentSoft": "#e4eafb", "accentDeep": "#2b44a8", "onAccent": "#ffffff", "tagline": "La donnée de l'écosystème, par API" }
38+ ],
39+ "extraFooterLinks": [
40+ { "label": "ValoPlex", "href": "https://www.valoplex.com" },
41+ { "label": "Ka2", "href": "https://www.ka2.bot" },
42+ { "label": "Ka4", "href": "https://www.ka4.bot" },
43+ { "label": "Ka6", "href": "https://www.ka6.bot" }
44+ ]
45+}
added frontend/src/ka/tokens.css +229 −0
@@ -0,0 +1,229 @@
1+/* -----------------------------------------------------------------------------
2+ Auteur : Simon-Pierre Boucher — contact@spboucher.ai
3+ Fichier : ka-ui/tokens.css — SOURCE CANONIQUE (repo ka-ui sur spbgit)
4+ Desc. : Design system commun GROUPE KA « éditorial sharp » — tokens + socle
5+ de composants partagés par les 12 plateformes. Chaque site importe ce
6+ fichier PUIS surcharge uniquement son accent (voir accents.css).
7+
8+ · Typo : display Space Grotesk / texte Inter / micro-étiquettes JetBrains Mono
9+ · Palette: papier #f5f3ee · encre #141814 · vert profond #1c5c41 + ACCENT du site
10+ · Signature : bordures encre 1,5–2 px + ombres décalées dures, grain de film,
11+ surlignés d'accent inclinés (néo-brutalisme raffiné)
12+ · Breakpoints communs : 360 / 768 / 1024 / 1440 px (mobile-first)
13+ · Zones tactiles ≥ 44×44 px, safe-areas iOS, typographie fluide (clamp)
14+----------------------------------------------------------------------------- */
15+
16+:root {
17+ /* ---- Palette fixe Groupe KA (identique sur les 12 sites) ---- */
18+ --paper: #f5f3ee;
19+ --surface: #ffffff;
20+ --surface-2: #faf9f5;
21+ --ink: #141814;
22+ --ink-2: #4d5551;
23+ --ink-3: #8b928c;
24+ --line: rgba(20, 24, 20, 0.14);
25+ --line-strong: rgba(20, 24, 20, 0.85);
26+ --green: #1c5c41;
27+ --green-deep: #123f2e;
28+ --amber: #e8a33d;
29+ --amber-soft: #fdf3e2;
30+ --danger: #b3423a;
31+ --danger-soft: #fbe9e7;
32+
33+ /* ---- ACCENT du site — SEULES variables surchargées par marque ---- */
34+ --accent: #d9f26b; /* défaut : lime Groupe KA */
35+ --accent-soft: #f0f9d2;
36+ --accent-deep: #123f2e;
37+ --on-accent: var(--ink); /* couleur du texte posé SUR l'accent */
38+
39+ /* alias rétro-compatibles (les satellites historiques utilisent --lime) */
40+ --lime: var(--accent);
41+ --lime-soft: var(--accent-soft);
42+
43+ /* ---- Géométrie sharp ---- */
44+ --r-card: 10px;
45+ --r-ctl: 6px;
46+ --r-pill: 999px;
47+
48+ /* ---- Ombres décalées — la signature du groupe ---- */
49+ --shadow-flat: 0 1px 2px rgba(20, 24, 20, 0.05);
50+ --shadow-off: 6px 6px 0 var(--ink);
51+ --shadow-off-soft: 8px 8px 0 rgba(20, 24, 20, 0.08);
52+ --shadow-off-mid: 4px 4px 0 rgba(20, 24, 20, 0.18);
53+
54+ /* ---- Typographie ---- */
55+ --font-display: "Space Grotesk", system-ui, sans-serif;
56+ --font-body: "Inter", system-ui, sans-serif;
57+ --font-mono: "JetBrains Mono", ui-monospace, monospace;
58+
59+ /* Échelle fluide (mobile-first, clamp) */
60+ --fs-h1: clamp(30px, 3.2vw + 18px, 54px);
61+ --fs-h2: clamp(23px, 1.8vw + 14px, 34px);
62+ --fs-h3: clamp(17px, 0.9vw + 12px, 22px);
63+ --fs-body: 15px;
64+ --fs-small: 13px;
65+
66+ /* Espacements */
67+ --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px;
68+ --sp-5: 24px; --sp-6: 32px; --sp-7: 48px; --sp-8: 64px;
69+
70+ /* Cible tactile minimale */
71+ --touch: 44px;
72+}
73+
74+/* ---- Socle ---- */
75+* { box-sizing: border-box; }
76+html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }
77+body {
78+ margin: 0;
79+ background: var(--paper);
80+ color: var(--ink);
81+ font-family: var(--font-body);
82+ font-size: var(--fs-body);
83+ line-height: 1.55;
84+ -webkit-font-smoothing: antialiased;
85+ padding-bottom: env(safe-area-inset-bottom);
86+ overflow-x: clip; /* jamais de débordement horizontal */
87+}
88+img, svg, video { max-width: 100%; height: auto; display: block; }
89+h1, h2, h3, h4 { font-family: var(--font-display); letter-spacing: -0.03em; margin: 0 0 0.5em; }
90+h1 { font-size: var(--fs-h1); line-height: 1.06; }
91+h2 { font-size: var(--fs-h2); line-height: 1.15; }
92+h3 { font-size: var(--fs-h3); line-height: 1.25; }
93+a { color: inherit; }
94+::selection { background: var(--accent); color: var(--on-accent); }
95+:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
96+
97+/* Grain de film pleine page — ⚠️ jamais de z-index sur body > * (position:
98+ relative seulement), sinon les utilitaires z-* sont écrasés. */
99+body::before {
100+ content: "";
101+ position: fixed; inset: 0; pointer-events: none; opacity: 0.35; z-index: 9999;
102+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='linear' slope='0.06'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)'/%3E%3C/svg%3E");
103+}
104+body > * { position: relative; }
105+
106+/* ---- Conteneur commun ---- */
107+.container { max-width: 1152px; margin: 0 auto; padding: 0 var(--sp-4); }
108+@media (min-width: 768px) { .container { padding: 0 var(--sp-5); } }
109+
110+/* ---- Micro-typographie signature ---- */
111+.kicker {
112+ display: inline-flex; align-items: center; gap: 10px;
113+ font-family: var(--font-mono); font-size: 11.5px; font-weight: 500;
114+ text-transform: uppercase; letter-spacing: 0.12em; color: var(--green);
115+}
116+.kicker::before { content: ""; width: 22px; height: 2px; background: var(--green); }
117+.klabel {
118+ font-family: var(--font-mono); font-size: 10px; font-weight: 700;
119+ text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3);
120+}
121+.hl {
122+ display: inline-block; background: var(--accent); color: var(--on-accent);
123+ border-radius: 8px; padding: 0 10px 2px; transform: rotate(-1deg);
124+}
125+.outline-txt { color: transparent; -webkit-text-stroke: 2px var(--ink); }
126+
127+/* ---- Boutons (cible ≥ 44 px) ---- */
128+.btn {
129+ display: inline-flex; align-items: center; justify-content: center; gap: 8px;
130+ min-height: var(--touch); padding: 10px 18px;
131+ font-family: var(--font-display); font-weight: 700; font-size: 14px;
132+ border: 1.5px solid var(--ink); border-radius: var(--r-ctl);
133+ background: var(--surface); color: var(--ink);
134+ cursor: pointer; text-decoration: none;
135+ transition: transform 0.15s, box-shadow 0.15s, background 0.15s, color 0.15s;
136+}
137+.btn:hover { transform: translate(-2px, -2px); box-shadow: var(--shadow-off-mid); }
138+.btn:active { transform: translate(0, 0); box-shadow: none; }
139+.btn:disabled { opacity: 0.45; pointer-events: none; }
140+.btn-primary { background: var(--ink); color: var(--accent); }
141+.btn-primary:hover { background: var(--accent-deep); }
142+.btn-accent { background: var(--accent); color: var(--on-accent); }
143+.btn-ghost { background: transparent; border-color: var(--line); }
144+.btn-ghost:hover { border-color: var(--ink); }
145+
146+/* ---- Cartes ---- */
147+.card {
148+ background: var(--surface); border: 1.5px solid var(--ink);
149+ border-radius: var(--r-card); box-shadow: var(--shadow-off-soft);
150+ overflow: hidden;
151+}
152+.card-hover { transition: transform 0.15s, box-shadow 0.15s; }
153+.card-hover:hover { transform: translate(-2px, -2px); box-shadow: var(--shadow-off); }
154+
155+/* ---- Chips / badges ---- */
156+.chip {
157+ display: inline-flex; align-items: center; gap: 6px;
158+ padding: 4px 11px; font-family: var(--font-mono); font-size: 11px; font-weight: 700;
159+ text-transform: uppercase; letter-spacing: 0.06em;
160+ border: 1.5px solid var(--ink); border-radius: var(--r-pill);
161+ background: var(--surface); color: var(--ink);
162+}
163+.chip-accent { background: var(--accent); color: var(--on-accent); }
164+.chip-soft { background: var(--accent-soft); border-color: var(--line); }
165+
166+/* ---- Champs ---- */
167+.input, .select, .textarea {
168+ width: 100%; min-height: var(--touch); padding: 10px 14px;
169+ font: inherit; color: var(--ink); background: var(--surface);
170+ border: 1.5px solid var(--ink); border-radius: var(--r-ctl);
171+}
172+.input:focus, .select:focus, .textarea:focus {
173+ outline: none; box-shadow: 3px 3px 0 var(--accent); border-color: var(--ink);
174+}
175+.input::placeholder { color: var(--ink-3); }
176+
177+/* ---- Badge « Un service Groupe KA » (header, cliquable → hub) ---- */
178+.gk-badge {
179+ display: inline-flex; align-items: center; gap: 7px;
180+ min-height: 30px; padding: 3px 10px 4px;
181+ font-family: var(--font-mono); font-size: 10px; font-weight: 700;
182+ letter-spacing: 0.08em; text-transform: uppercase; text-decoration: none;
183+ border: 1.5px solid var(--ink); border-radius: var(--r-pill);
184+ background: var(--surface); color: var(--ink-2); white-space: nowrap;
185+}
186+.gk-badge b {
187+ font-family: var(--font-display); font-size: 12px; letter-spacing: -0.02em;
188+ text-transform: none; color: var(--ink);
189+}
190+.gk-badge b .ka {
191+ display: inline-block; background: var(--ink); color: var(--accent);
192+ border-radius: 5px; padding: 0 5px 1px; margin-left: 3px; transform: rotate(-2deg);
193+}
194+.gk-badge:hover .ka { transform: rotate(0); }
195+.gk-badge--dark { background: transparent; border-color: rgba(245,243,238,0.4); color: rgba(245,243,238,0.75); }
196+.gk-badge--dark b { color: var(--paper); }
197+
198+/* ---- Footer commun (fond encre — voir react/KaFooter.tsx) ---- */
199+.ka-footer { margin-top: var(--sp-8); background: var(--ink); padding: 44px 0; font-size: 13px; color: rgba(245,243,238,0.75); }
200+.ka-footer a { text-decoration: none; }
201+.ka-footer .wordmark { font-family: var(--font-display); font-weight: 700; font-size: 30px; letter-spacing: -0.04em; color: var(--paper); text-decoration: none; }
202+.ka-footer .wordmark .ka { color: var(--accent); }
203+.ka-footer .desc { max-width: 640px; margin-top: var(--sp-4); }
204+.ka-footer .notice { max-width: 640px; margin-top: var(--sp-4); border-left: 2px solid var(--accent); padding-left: var(--sp-4); }
205+.ka-footer .notice b { color: var(--paper); }
206+.ka-footer .sites { list-style: none; display: flex; flex-wrap: wrap; gap: 10px 26px; margin: 26px 0 0; padding: 0; font-family: var(--font-mono); font-size: 11px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; }
207+.ka-footer .sites a { color: rgba(245,243,238,0.65); }
208+.ka-footer .sites a:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 4px; }
209+.ka-footer .contacts { display: grid; gap: 14px 32px; border-top: 1px solid rgba(245,243,238,0.15); margin-top: 30px; padding-top: 26px; }
210+@media (min-width: 768px) { .ka-footer .contacts { grid-template-columns: repeat(3, 1fr); } }
211+.ka-footer .contacts a { font-family: var(--font-mono); font-weight: 700; font-size: 12px; color: rgba(245,243,238,0.85); }
212+.ka-footer .contacts a:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 4px; }
213+.ka-footer .contacts span { display: block; margin-top: 3px; font-size: 11px; color: rgba(245,243,238,0.5); }
214+.ka-footer .legal { margin-top: 30px; font-family: var(--font-mono); font-size: 11px; color: rgba(245,243,238,0.45); }
215+.ka-footer .legal a { color: inherit; }
216+.ka-footer .legal a:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 4px; }
217+
218+/* ---- Tableaux → cartes empilées sous 768 px ---- */
219+.tbl-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
220+@media (max-width: 767px) {
221+ .tbl-stack thead { display: none; }
222+ .tbl-stack tr { display: block; border: 1.5px solid var(--ink); border-radius: var(--r-card); margin-bottom: var(--sp-3); background: var(--surface); }
223+ .tbl-stack td { display: flex; justify-content: space-between; gap: var(--sp-3); padding: 8px 12px; border: 0; }
224+ .tbl-stack td::before { content: attr(data-label); font-family: var(--font-mono); font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-3); }
225+}
226+
227+/* ---- Utilitaires responsive ---- */
228+.only-mobile { display: initial; } .only-desktop { display: none; }
229+@media (min-width: 768px) { .only-mobile { display: none; } .only-desktop { display: initial; } }
modified sortika/seo.py +34 −1
@@ -143,6 +143,39 @@ def event_page(uid: str) -> HTMLResponse:
143 return _render(head, ssr) 143 return _render(head, ssr)
144 144
145 145
146+@router.get("/contact", include_in_schema=False)
147+def contact_page() -> HTMLResponse:
148+ """Page /contact (SPA) — head SEO + contenu SSR pour les robots.
149+ Contenu aligné sur ka-ui/ecosystem.json (contacts Groupe KA)."""
150+ title = f"Contact — {SITE_NAME} | Un service Groupe KA"
151+ desc = ("Écrire au Groupe KA : contact@groupe-ka.com (projets, partenariats "
152+ "et données), info@groupe-ka.com (médias et questions générales), "
153+ "admin@groupe-ka.com (légal, vie privée et Loi 25).")
154+ canonical = f"{BASE_URL}/contact"
155+ head = (
156+ f"<title>{_e(title)}</title>\n"
157+ f'<meta name="description" content="{_e(desc)}">\n'
158+ f'<link rel="canonical" href="{canonical}">\n'
159+ f'<meta property="og:title" content="{_e(title)}">\n'
160+ f'<meta property="og:description" content="{_e(desc)}">\n'
161+ f'<meta property="og:type" content="website">\n'
162+ f'<meta property="og:url" content="{canonical}">'
163+ )
164+ ssr = (
165+ "<article><h1>Contact — Groupe KA</h1>"
166+ "<p>Sorti-Ka est un service du Groupe KA.</p>"
167+ '<p><a href="mailto:contact@groupe-ka.com">contact@groupe-ka.com</a> — '
168+ "projets, partenariats et données.</p>"
169+ '<p><a href="mailto:info@groupe-ka.com">info@groupe-ka.com</a> — '
170+ "médias et questions générales.</p>"
171+ '<p><a href="mailto:admin@groupe-ka.com">admin@groupe-ka.com</a> — '
172+ "légal, vie privée et Loi 25.</p>"
173+ '<p><a href="https://www.groupe-ka.com" rel="noopener">Groupe KA — '
174+ "le portail de l'écosystème</a></p></article>"
175+ )
176+ return _render(head, ssr)
177+
178+
146 @router.get("/robots.txt", include_in_schema=False) 179 @router.get("/robots.txt", include_in_schema=False)
147 def robots() -> PlainTextResponse: 180 def robots() -> PlainTextResponse:
148 return PlainTextResponse( 181 return PlainTextResponse(
@@ -179,7 +212,7 @@ def sitemap_index() -> Response:
179 212
180 @router.get("/sitemap-pages.xml", include_in_schema=False) 213 @router.get("/sitemap-pages.xml", include_in_schema=False)
181 def sitemap_pages() -> Response: 214 def sitemap_pages() -> Response:
182− return _urlset([(f"{BASE_URL}/", None)]) 215+ return _urlset([(f"{BASE_URL}/", None), (f"{BASE_URL}/contact", None)])
183 216
184 217
185 @router.get("/sitemap-evenements-{num}.xml", include_in_schema=False) 218 @router.get("/sitemap-evenements-{num}.xml", include_in_schema=False)
186 219
Menu

Theme