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%

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 1 mo ago (Aug 18, 2026) parent 69b8641

6 changed files +603 −82

modified frontend/index.html +183 −81
@@ -3,9 +3,12 @@
3 3 File: index.html
4 4 Desc: Frontend Sorti-Ka — recherche + fiches événement (SPA sans build,
5 5 routeur History API, consomme /api). Design system Groupe KA
6 − « éditorial sharp » : papier #f5f3ee · encre #141814 · vert #1c5c41 ·
7 − lime #d9f26b · Space Grotesk / Inter / JetBrains Mono · bordures
8 − encre + ombres décalées + grain de film. SSR : sortika/seo.py
6 + « éditorial sharp » via ka-ui (src/ka/tokens.css, chargé en premier) :
7 + papier #f5f3ee · encre #141814 · vert #1c5c41 · ACCENT FRAMBOISE
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 12 injecte <head> et contenu dans les placeholders SSR_HEAD/SSR_BODY.
10 13 -->
11 14 <!DOCTYPE html>
@@ -13,54 +16,26 @@
13 16 <head>
14 17 <meta charset="utf-8">
15 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 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 21 <!--SSR_HEAD-->
19 22 <link rel="preconnect" href="https://fonts.googleapis.com">
20 23 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
21 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 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 29 :root{
24 − /* système de design Groupe KA « éditorial sharp » */
25 − --paper:#f5f3ee; --surface:#fff; --surface-2:#faf9f5;
26 − --ink:#141814; --ink-2:#4d5551; --ink-3:#8b928c;
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;
30 + --accent:#d6336c; --accent-soft:#fbe0eb; --accent-deep:#a12551; --on-accent:#ffffff;
31 + /* alias rétro-compatibles — tout le CSS local en --lime suit l'accent */
32 + --lime:var(--accent); --lime-soft:var(--accent-soft);
39 33 }
40 −*{box-sizing:border-box}
41 −html{scroll-behavior:smooth}
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}
34 +h1,h2,h3,h4{margin:0}
35 +a{text-decoration:none}
49 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 37 .container{max-width:1240px;margin:0 auto;padding:0 24px}
54 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 39 /* ---------- cartes & boutons signature ---------- */
65 40 .gk-card{background:var(--surface);border:1.5px solid var(--ink);
66 41 border-radius:var(--r-card);box-shadow:var(--shadow-off-soft)}
@@ -71,10 +46,10 @@ img{display:block}
71 46 font-family:var(--font-display);font-size:14px;font-weight:700;cursor:pointer;
72 47 transition:background .15s ease,transform .1s ease,box-shadow .1s ease}
73 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)}
75 −.btn-primary:hover{background:var(--green-deep)}
49 +.btn-primary{background:var(--ink);color:var(--accent);box-shadow:4px 4px 0 rgba(20,24,20,.25)}
50 +.btn-primary:hover{background:var(--accent-deep)}
76 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 54 /* ---------- header / ticker ---------- */
80 55 .ticker{background:var(--ink);color:rgba(245,243,238,.85);overflow:hidden;
@@ -88,22 +63,33 @@ img{display:block}
88 63 .header{position:sticky;top:0;z-index:50;background:rgba(245,243,238,.9);
89 64 backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-bottom:1.5px solid var(--ink)}
90 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 72 .brand{font-family:var(--font-display);font-weight:700;font-size:24px;letter-spacing:-.03em;
92 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 75 .brand-tag{font-family:var(--font-mono);font-size:10px;text-transform:uppercase;
95 76 letter-spacing:.08em;color:var(--ink-3);margin-left:8px}
96 77 @media(max-width:900px){.brand-tag{display:none}}
97 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 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 83 .btn-ka{display:inline-flex;align-items:center;gap:8px;padding:8px 15px;min-height:38px;
102 84 border:1.5px solid var(--ink);border-radius:var(--r-ctl);background:var(--ink);
103 85 color:var(--lime);font-family:var(--font-display);font-weight:700;font-size:13px;
104 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)}
106 −.btn-ka .kdot{background:var(--lime);color:var(--ink);border-radius:4px;
87 +.btn-ka:hover{background:var(--accent-deep)}
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 93 padding:0 5px;font-weight:800;transform:rotate(-1deg)}
108 94 .user-chip{display:inline-flex;align-items:center;gap:9px;border:1.5px solid var(--ink);
109 95 border-radius:var(--r-pill,999px);background:var(--surface);padding:4px 12px 4px 5px;
@@ -149,9 +135,10 @@ img{display:block}
149 135 .chips{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}
150 136 .chip{border:1.5px solid var(--ink);background:var(--surface);border-radius:var(--r-ctl);
151 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 139 cursor:pointer;color:var(--ink);line-height:1;transition:all .12s ease}
153 −.chip:hover{background:var(--lime-soft)}
154 −.chip.on{background:var(--ink);color:var(--lime);box-shadow:3px 3px 0 rgba(20,24,20,.25)}
140 +.chip:hover{background:var(--accent-soft)}
141 +.chip.on{background:var(--ink);color:var(--accent);box-shadow:3px 3px 0 rgba(20,24,20,.25)}
155 142 .chip.free.on{background:var(--green);border-color:var(--green);color:#fff}
156 143
157 144 /* ---------- résultats ---------- */
@@ -166,7 +153,7 @@ img{display:block}
166 153 .card-img img{width:100%;height:100%;object-fit:cover}
167 154 .badge-date{position:absolute;left:10px;bottom:10px;font-family:var(--font-mono);
168 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 157 border-radius:var(--r-ctl);padding:4px 9px;white-space:nowrap;box-shadow:2px 2px 0 var(--ink)}
171 158 .card-body{padding:15px 16px 14px;display:flex;flex-direction:column;gap:8px;flex:1}
172 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 182 /* ---------- fiche événement ---------- */
196 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 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 188 .back:hover{text-decoration:underline}
201 189 .detail-grid{display:grid;grid-template-columns:1.5fr 1fr;gap:26px;align-items:start}
202 190 @media(max-width:860px){.detail-grid{grid-template-columns:1fr}}
@@ -231,13 +219,32 @@ img{display:block}
231 219 color:var(--green);text-transform:uppercase;letter-spacing:.06em}
232 220 .src-card .n{font-size:12.5px;color:var(--ink-3);margin-top:6px}
233 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}
235 −footer .fam{display:flex;flex-wrap:wrap;gap:4px 14px;margin-bottom:14px}
236 −footer .fam a{font-family:var(--font-display);font-weight:700;color:#f5f3ee;padding:4px 0}
237 −footer .fam a:hover{color:var(--lime)}
238 −footer .fam .lbl{font-family:var(--font-mono);font-size:10px;text-transform:uppercase;
239 − letter-spacing:.08em;color:rgba(245,243,238,.45);align-self:center}
240 −footer a.mail{color:var(--lime)}
222 +.ka-footer{margin-top:0}
223 +
224 +/* ---------- page /contact ---------- */
225 +.contact-page{padding:44px 0 64px}
226 +.contact-page h1{font-size:clamp(30px,4.2vw,48px);line-height:1.06;margin-top:16px;max-width:820px}
227 +.contact-page .lede{color:var(--ink-2);font-size:16px;max-width:660px;margin:16px 0 0}
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 248 #ssr{display:none}
242 249 .hidden{display:none!important}
243 250 </style>
@@ -258,9 +265,13 @@ footer a.mail{color:var(--lime)}
258 265 <header class="header">
259 266 <div class="container header-inner">
260 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 271 <span class="brand-tag">Sorties &amp; événements — Québec</span>
262 272 <nav class="nav">
263 273 <a href="/#sources">Sources</a>
274 + <a data-nav href="/contact">Contact</a>
264 275 <a href="/docs" target="_blank" rel="noopener">API</a>
265 276 </nav>
266 277 <span id="auth"></span>
@@ -343,23 +354,101 @@ footer a.mail{color:var(--lime)}
343 354 </div>
344 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 409 <div class="container">
348 − <div class="fam">
349 − <span class="lbl">Famille ·Ka</span>
350 − <a href="https://www.lou-ka.com" target="_blank" rel="noopener">Lou·Ka</a>
351 − <a href="https://www.immo-ka.com" target="_blank" rel="noopener">Immo·Ka</a>
352 − <a href="https://www.auto-ka.com" target="_blank" rel="noopener">Auto·Ka</a>
353 − <a href="https://www.food-ka.com" target="_blank" rel="noopener">Food·Ka</a>
354 − <a href="https://www.fabri-ka.com" target="_blank" rel="noopener">Fabri·Ka</a>
355 − <a href="https://www.toit-ka.com" target="_blank" rel="noopener">Toit·Ka</a>
356 − <a href="https://www.forma-ka.com" target="_blank" rel="noopener">Forma·Ka</a>
357 − <a href="https://www.groupe-ka.com" target="_blank" rel="noopener">Groupe KA</a>
410 + <a class="wordmark" href="https://www.groupe-ka.com" target="_blank" rel="noopener noreferrer"
411 + aria-label="Groupe KA — le portail de l'écosystème">Groupe <span class="ka">KA</span></a>
412 + <p class="desc">Holding québécois d'agrégateurs de produits et services entièrement automatisés.
413 + Des centaines de connecteurs lisent les sites à la source, normalisent la donnée et se
414 + resynchronisent seuls. <b style="color:var(--paper)">Sorti·Ka</b> — Toutes les sorties,
415 + dans les 17 régions — est un service Groupe KA.</p>
416 + <p class="notice"><b>Groupe KA est un agrégateur de contenu : nous ne vendons rien,
417 + ne louons rien et ne sommes partie à aucune transaction.</b></p>
418 + <ul class="sites">
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 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) ·
360 − Le point de vente · evenko · Atuvu.ca · La Vitrine · Répertoire des municipalités (MAMH).</div>
361 − <div style="margin-top:8px">© 2026 Sorti·Ka — Simon-Pierre Boucher ·
362 − <a class="mail" href="mailto:contact@spboucher.ai">contact@spboucher.ai</a></div>
443 + <p class="legal">Données : SIT Québec (MTO, CC 4.0) · Ville de Montréal (CC BY 4.0) ·
444 + Ville de Laval (CC 4.0) · Le point de vente · evenko · Atuvu.ca · La Vitrine ·
445 + Répertoire des municipalités (MAMH).</p>
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 452 </div>
364 453 </footer>
365 454
@@ -519,21 +608,32 @@ async function showDetail(uid){
519 608 bindNav($("d-similar"));
520 609 }catch(e){ $("d-similar").innerHTML = ""; }
521 610
522 − $("view-list").classList.add("hidden");
523 − $("view-detail").classList.remove("hidden");
611 + show("view-detail");
524 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 621 function showList(){
528 − document.title = "Sorti·Ka — Toutes les sorties et tous les événements du Québec";
529 − $("view-detail").classList.add("hidden");
530 − $("view-list").classList.remove("hidden");
622 + document.title = "Sorti-Ka — Un service Groupe KA";
623 + show("view-list");
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 632 /* ---------- routeur ---------- */
534 633 function route(){
535 634 const m = location.pathname.match(/^\/evenement\/(.+)$/);
536 635 if(m) showDetail(decodeURIComponent(m[1]));
636 + else if(location.pathname === "/contact") showContact();
537 637 else showList();
538 638 }
539 639 function go(path){
@@ -606,7 +706,9 @@ async function init(){
606 706 };
607 707 } else {
608 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 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 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 179 @router.get("/robots.txt", include_in_schema=False)
147 180 def robots() -> PlainTextResponse:
148 181 return PlainTextResponse(
@@ -179,7 +212,7 @@ def sitemap_index() -> Response:
179 212
180 213 @router.get("/sitemap-pages.xml", include_in_schema=False)
181 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 218 @router.get("/sitemap-evenements-{num}.xml", include_in_schema=False)
186 219