SPB Git

spb/vrai-prix

Public

Vrai-Prix — l'évaluation du vrai prix des propriétés résidentielles au Québec.

Harmonisation Groupe KA — design system ka-ui, badge, footer commun, page contact

- ka-ui vendoré (src/ka/) : tokens.css importé dans layer(base) via globals.css
  (bat le preflight Tailwind, reste sous les utilitaires et le design local),
  bloc compat pour les titres, accent rouge canonique (--accent/--accent-soft/
  --accent-deep/--on-accent) avec alias historiques --lime/--lime-soft
- Header : badge « Un service Groupe KA » à côté du logo (desktop xl+) et dans
  le pied du menu mobile, cliquable vers groupe-ka.com
- Footer : KaFooter commun (bloc encre, contacts et sites centralisés
  d ecosystem.json) précédé de la rangée locale FooterNav restylée papier
- Nouvelle page /contact : courriels centralisés, avertissement d agrégateur,
  lien hub + création de compte KA ID, liste des sites de l écosystème
- SEO : titre par défaut « Vrai-Prix — Un service Groupe KA » (layout + OG/X)

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

9 changed files +545 −17

added src/app/contact/page.tsx +97 −0
@@ -0,0 +1,97 @@
1+// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2+// Page contact — infos centralisées de l'écosystème Groupe KA (ka-ui/ecosystem.json).
3+import type { Metadata } from "next";
4+import eco from "@/ka/ecosystem.json";
5+
6+export const metadata: Metadata = {
7+ title: "Contact",
8+ description:
9+ "Joindre le Groupe KA : projets et partenariats, médias, légal et vie privée. Vrai-Prix est un service Groupe KA.",
10+};
11+
12+const SELF_ID = "vrai-prix";
13+
14+export default function ContactPage() {
15+ const others = eco.sites.filter((s) => s.id !== SELF_ID);
16+ return (
17+ <div className="pb-6 pt-10">
18+ <p className="kicker">Contact</p>
19+ <h1 className="vp-display mt-3 max-w-3xl text-[clamp(30px,5vw,52px)] font-bold uppercase leading-[1] tracking-[-0.03em]">
20+ Écrire au <span className="hl">Groupe&nbsp;KA</span>
21+ </h1>
22+ <p className="mt-5 max-w-2xl text-[15px] leading-relaxed text-ink-2">
23+ Vrai-Prix est un service du <b className="text-ink">{eco.org.name}</b> —{" "}
24+ {eco.org.tagline} Toutes les demandes passent par les adresses
25+ centralisées ci-dessous.
26+ </p>
27+
28+ {/* les 3 courriels avec leurs rôles */}
29+ <div className="mt-9 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
30+ {eco.contacts.map((c) => (
31+ <a
32+ key={c.email}
33+ href={`mailto:${c.email}`}
34+ className="card card-hover block p-5"
35+ >
36+ <p className="klabel">{c.role}</p>
37+ <p className="vp-mono mt-2 break-all text-[13.5px] font-bold text-ink">
38+ {c.email}
39+ </p>
40+ </a>
41+ ))}
42+ </div>
43+
44+ {/* avertissement d'agrégateur */}
45+ <div className="card mt-8 max-w-2xl border-l-[6px] border-l-lime p-5">
46+ <p className="klabel">Avertissement</p>
47+ <p className="mt-2 text-[14px] leading-relaxed">{eco.org.disclaimer}</p>
48+ </div>
49+
50+ {/* hub + compte KA ID */}
51+ <div className="mt-10">
52+ <p className="kicker">Le hub</p>
53+ <p className="mt-3 max-w-2xl text-[14.5px] text-ink-2">
54+ Le portail de l&apos;écosystème vit sur{" "}
55+ <a
56+ href={eco.hub.url}
57+ target="_blank"
58+ rel="noopener noreferrer"
59+ className="font-bold text-ink underline decoration-lime decoration-2 underline-offset-4"
60+ >
61+ groupe-ka.com
62+ </a>
63+ . La création d&apos;un compte KA&nbsp;ID se fait sur{" "}
64+ <a
65+ href={`${eco.hub.url}${eco.hub.loginPath}`}
66+ target="_blank"
67+ rel="noopener noreferrer"
68+ className="font-bold text-ink underline decoration-lime decoration-2 underline-offset-4"
69+ >
70+ groupe-ka.com/connexion
71+ </a>{" "}
72+ — le même compte ouvre toutes les plateformes du groupe.
73+ </p>
74+ </div>
75+
76+ {/* les sites de l'écosystème */}
77+ <div className="mt-10">
78+ <p className="kicker">L&apos;écosystème</p>
79+ <ul className="mt-4 flex max-w-3xl list-none flex-wrap gap-2.5 p-0">
80+ {others.map((s) => (
81+ <li key={s.id}>
82+ <a
83+ href={`https://${s.domain}`}
84+ target="_blank"
85+ rel="noopener noreferrer"
86+ className="chip"
87+ title={s.tagline}
88+ >
89+ {s.wordmark}
90+ </a>
91+ </li>
92+ ))}
93+ </ul>
94+ </div>
95+ </div>
96+ );
97+}
modified src/app/globals.css +22 −3
@@ -7,8 +7,20 @@
7 · Accent rouge vif #ff5148 + bordeaux #9e2a25 sur encre 7 · Accent rouge vif #ff5148 + bordeaux #9e2a25 sur encre
8 ----------------------------------------------------------------------------- */ 8 ----------------------------------------------------------------------------- */
9 @import "tailwindcss"; 9 @import "tailwindcss";
10+/* ka-ui (Groupe KA) — tokens + composants communs (.gk-badge, .ka-footer, .card…).
11+ Importé dans layer(base) AVANT le corps de ce fichier : il bat le preflight
12+ Tailwind mais reste sous les utilitaires (layered) et sous nos règles locales
13+ (non-layered). Un import brut dans layout.tsx écraserait les text-[…]/leading-[…]
14+ des titres (règles éléments non-layered > utilitaires layered). */
15+@import "../ka/tokens.css" layer(base);
10 16
11 :root { 17 :root {
18+ /* ---- Accent Vrai-Prix (tokens canoniques ka-ui) — identité rouge ---- */
19+ --accent: #ff5148;
20+ --accent-soft: #ffe3e0;
21+ --accent-deep: #9e2a25;
22+ --on-accent: #ffffff;
23+
12 --paper: #f5f3ee; 24 --paper: #f5f3ee;
13 --surface: #ffffff; 25 --surface: #ffffff;
14 --surface-2: #faf9f5; 26 --surface-2: #faf9f5;
@@ -19,8 +31,8 @@
19 --line-strong: rgba(20, 24, 20, 0.85); 31 --line-strong: rgba(20, 24, 20, 0.85);
20 --green: #9e2a25; 32 --green: #9e2a25;
21 --green-deep: #771f1b; 33 --green-deep: #771f1b;
22− --lime: #ff5148; 34+ --lime: var(--accent); /* alias historiques → l'accent est le token canonique */
23− --lime-soft: #ffe3e0; 35+ --lime-soft: var(--accent-soft);
24 --amber: #e8a33d; 36 --amber: #e8a33d;
25 --amber-soft: #fdf3e2; 37 --amber-soft: #fdf3e2;
26 --danger: #b3423a; 38 --danger: #b3423a;
@@ -51,6 +63,13 @@
51 --font-mono: var(--font-jetbrains), ui-monospace, monospace; 63 --font-mono: var(--font-jetbrains), ui-monospace, monospace;
52 } 64 }
53 65
66+/* compat ka-ui : tokens.css impose marges + tailles fluides aux titres ; le
67+ design local dimensionne les siens via utilitaires → on rétablit le
68+ comportement preflight (même layer, source postérieure = prioritaire). */
69+@layer base {
70+ h1, h2, h3, h4 { margin: 0; font-size: inherit; line-height: inherit; }
71+}
72+
54 html { scroll-behavior: smooth; } 73 html { scroll-behavior: smooth; }
55 body { 74 body {
56 background: var(--paper); 75 background: var(--paper);
@@ -129,7 +148,7 @@ select.vp-input {
129 .btn:active { transform: translate(2px, 2px); box-shadow: none !important; } 148 .btn:active { transform: translate(2px, 2px); box-shadow: none !important; }
130 .btn-primary { background: var(--ink); color: var(--lime); box-shadow: 4px 4px 0 rgba(20,24,20,0.25); } 149 .btn-primary { background: var(--ink); color: var(--lime); box-shadow: 4px 4px 0 rgba(20,24,20,0.25); }
131 .btn-primary:hover { background: var(--green-deep); } 150 .btn-primary:hover { background: var(--green-deep); }
132−.btn-ghost { background: transparent; color: var(--ink); } 151+.btn-ghost { background: transparent; color: var(--ink); border-color: var(--ink); }
133 .btn-ghost:hover { background: var(--lime); box-shadow: 4px 4px 0 rgba(20,24,20,0.2); } 152 .btn-ghost:hover { background: var(--lime); box-shadow: 4px 4px 0 rgba(20,24,20,0.2); }
134 153
135 .stat-chip { 154 .stat-chip {
modified src/app/layout.tsx +29 −12
@@ -7,6 +7,8 @@ import AccountButton from "@/components/AccountButton";
7 import CookieConsent from "@/components/CookieConsent"; 7 import CookieConsent from "@/components/CookieConsent";
8 import FooterNav from "@/components/FooterNav"; 8 import FooterNav from "@/components/FooterNav";
9 import HeaderNav from "@/components/HeaderNav"; 9 import HeaderNav from "@/components/HeaderNav";
10+import GroupeKaBadge from "@/ka/GroupeKaBadge";
11+import KaFooter from "@/ka/KaFooter";
10 import Link from "next/link"; 12 import Link from "next/link";
11 import { SITE_URL } from "@/lib/seo"; 13 import { SITE_URL } from "@/lib/seo";
12 14
@@ -23,7 +25,7 @@ const jetbrains = JetBrains_Mono({
23 export const metadata: Metadata = { 25 export const metadata: Metadata = {
24 metadataBase: new URL(SITE_URL), 26 metadataBase: new URL(SITE_URL),
25 title: { 27 title: {
26− default: "Vrai-Prix — la vraie valeur, sans boîte noire", 28+ default: "Vrai-Prix — Un service Groupe KA",
27 template: "%s | Vrai-Prix", 29 template: "%s | Vrai-Prix",
28 }, 30 },
29 description: 31 description:
@@ -34,13 +36,13 @@ export const metadata: Metadata = {
34 siteName: "Vrai-Prix", 36 siteName: "Vrai-Prix",
35 locale: "fr_CA", 37 locale: "fr_CA",
36 url: SITE_URL, 38 url: SITE_URL,
37− title: "Vrai-Prix — la vraie valeur, sans boîte noire", 39+ title: "Vrai-Prix — Un service Groupe KA",
38 description: 40 description:
39 "Estimation immobilière transparente pour le Québec : modèle hédonique + comparables réels, fourchette honnête et indice de confiance.", 41 "Estimation immobilière transparente pour le Québec : modèle hédonique + comparables réels, fourchette honnête et indice de confiance.",
40 }, 42 },
41 twitter: { 43 twitter: {
42 card: "summary", 44 card: "summary",
43− title: "Vrai-Prix — la vraie valeur, sans boîte noire", 45+ title: "Vrai-Prix — Un service Groupe KA",
44 description: 46 description:
45 "Estimation immobilière transparente pour le Québec : 3,7 millions de propriétés estimées, calcul montré en entier.", 47 "Estimation immobilière transparente pour le Québec : 3,7 millions de propriétés estimées, calcul montré en entier.",
46 }, 48 },
@@ -107,6 +109,11 @@ export default function RootLayout({ children }: LayoutProps<"/">) {
107 Prix 109 Prix
108 </span> 110 </span>
109 </Link> 111 </Link>
112+ {/* badge Groupe KA — desktop seulement (l'espace manque sous xl ;
113+ le menu mobile porte sa propre entrée) */}
114+ <span className="hidden xl:inline-flex">
115+ <GroupeKaBadge />
116+ </span>
110 <div className="ml-auto flex items-center gap-2"> 117 <div className="ml-auto flex items-center gap-2">
111 <HeaderNav /> 118 <HeaderNav />
112 <AccountButton /> 119 <AccountButton />
@@ -122,23 +129,33 @@ export default function RootLayout({ children }: LayoutProps<"/">) {
122 </div> 129 </div>
123 </div> 130 </div>
124 <main className="mx-auto max-w-6xl px-4 pb-16 sm:px-6">{children}</main> 131 <main className="mx-auto max-w-6xl px-4 pb-16 sm:px-6">{children}</main>
125− <footer className="mt-6 bg-ink py-11 text-[13px] text-[rgba(245,243,238,0.75)]"> 132+ {/* rangée locale (papier) : marque + liens de navigation du site,
126− <div className="mx-auto max-w-6xl px-4 sm:px-6"> 133+ puis le bloc encre final = KaFooter (commun Groupe KA) */}
127− <p className="vp-display mb-3 text-[30px] font-bold tracking-[-0.04em] text-paper"> 134+ <div className="mx-auto max-w-6xl px-4 sm:px-6">
128− Vrai<span className="text-lime">Prix</span> 135+ <div className="border-t-2 border-ink pt-7 text-[13px] text-ink-2">
136+ <p className="vp-display mb-3 text-[26px] font-bold tracking-[-0.04em] text-ink">
137+ Vrai
138+ <span className="ml-1 inline-block rotate-[-2deg] rounded-md bg-ink px-[6px] pb-[3px] pt-[1px] text-lime">
139+ Prix
140+ </span>
129 </p> 141 </p>
130 <p className="max-w-2xl"> 142 <p className="max-w-2xl">
131 Estimations bâties sur les rôles d&apos;évaluation foncière du 143 Estimations bâties sur les rôles d&apos;évaluation foncière du
132 Québec (MAMH, données ouvertes) et 745 119 transactions réelles. 144 Québec (MAMH, données ouvertes) et 745 119 transactions réelles.
133 Modèle hédonique + comparables ajustés, calcul montré en entier. 145 Modèle hédonique + comparables ajustés, calcul montré en entier.
146+ Estimation statistique à titre indicatif.
134 </p> 147 </p>
135 <FooterNav /> 148 <FooterNav />
136− <p className="vp-mono mt-4 text-[11px] uppercase tracking-[0.08em] text-[rgba(245,243,238,0.45)]">
137− vrai-prix.com — Simon-Pierre Boucher · contact@spboucher.ai —
138− estimation statistique à titre indicatif
139− </p>
140 </div> 149 </div>
141− </footer> 150+ </div>
151+ <KaFooter
152+ siteId="vrai-prix"
153+ localLegal={[
154+ { label: "Conditions", href: "/conditions" },
155+ { label: "Confidentialité", href: "/confidentialite" },
156+ { label: "Méthodologie", href: "/methodologie" },
157+ ]}
158+ />
142 <CookieConsent /> 159 <CookieConsent />
143 </LangProvider> 160 </LangProvider>
144 </body> 161 </body>
modified src/components/FooterNav.tsx +5 −1
@@ -7,8 +7,9 @@ import { openCookiePrefs } from "./CookieConsent";
7 export default function FooterNav() { 7 export default function FooterNav() {
8 const { lang } = useLang(); 8 const { lang } = useLang();
9 const fr = lang === "fr"; 9 const fr = lang === "fr";
10+ // rangée locale sur fond papier (le bloc encre final est le KaFooter commun)
10 const cls = 11 const cls =
11− "vp-mono text-[11px] uppercase tracking-[0.08em] text-[rgba(245,243,238,0.65)] border-b border-transparent hover:text-lime hover:border-lime transition-colors"; 12+ "vp-mono text-[11px] uppercase tracking-[0.08em] text-ink-2 border-b border-transparent hover:text-green-deep hover:border-green-deep transition-colors";
12 return ( 13 return (
13 <nav className="mt-5 flex flex-wrap items-center gap-x-6 gap-y-2"> 14 <nav className="mt-5 flex flex-wrap items-center gap-x-6 gap-y-2">
14 <Link href="/ka" className={cls}> 15 <Link href="/ka" className={cls}>
@@ -32,6 +33,9 @@ export default function FooterNav() {
32 <Link href="/confidentialite" className={cls}> 33 <Link href="/confidentialite" className={cls}>
33 {fr ? "Politique de confidentialité" : "Privacy policy"} 34 {fr ? "Politique de confidentialité" : "Privacy policy"}
34 </Link> 35 </Link>
36+ <Link href="/contact" className={cls}>
37+ Contact
38+ </Link>
35 <button onClick={openCookiePrefs} className={`${cls} cursor-pointer`}> 39 <button onClick={openCookiePrefs} className={`${cls} cursor-pointer`}>
36 🍪 {fr ? "Préférences de témoins" : "Cookie preferences"} 40 🍪 {fr ? "Préférences de témoins" : "Cookie preferences"}
37 </button> 41 </button>
modified src/components/HeaderNav.tsx +6 −1
@@ -10,6 +10,7 @@ import { useEffect, useRef } from "react";
10 import Link from "next/link"; 10 import Link from "next/link";
11 import { usePathname } from "next/navigation"; 11 import { usePathname } from "next/navigation";
12 import { useLang } from "./LangContext"; 12 import { useLang } from "./LangContext";
13+import GroupeKaBadge from "../ka/GroupeKaBadge";
13 14
14 export default function HeaderNav() { 15 export default function HeaderNav() {
15 const { lang, setLang } = useLang(); 16 const { lang, setLang } = useLang();
@@ -134,6 +135,10 @@ export default function HeaderNav() {
134 135
135 {/* pied du menu */} 136 {/* pied du menu */}
136 <div className="shrink-0 border-t-2 border-ink px-6 pb-[max(20px,env(safe-area-inset-bottom))] pt-4"> 137 <div className="shrink-0 border-t-2 border-ink px-6 pb-[max(20px,env(safe-area-inset-bottom))] pt-4">
138+ {/* entrée « Un service Groupe KA » du menu mobile */}
139+ <div className="mb-3">
140+ <GroupeKaBadge />
141+ </div>
137 <button 142 <button
138 onClick={() => setLang(fr ? "en" : "fr")} 143 onClick={() => setLang(fr ? "en" : "fr")}
139 className="vp-mono mb-3 rounded-full border-[1.5px] border-ink bg-surface px-4 py-2 text-[12px] font-bold uppercase tracking-[0.08em]" 144 className="vp-mono mb-3 rounded-full border-[1.5px] border-ink bg-surface px-4 py-2 text-[12px] font-bold uppercase tracking-[0.08em]"
@@ -145,7 +150,7 @@ export default function HeaderNav() {
145 <Link href="/confidentialite"> 150 <Link href="/confidentialite">
146 {fr ? "Confidentialité" : "Privacy"} 151 {fr ? "Confidentialité" : "Privacy"}
147 </Link> 152 </Link>
148− <span>contact@spboucher.ai</span> 153+ <Link href="/contact">Contact</Link>
149 </div> 154 </div>
150 </div> 155 </div>
151 </div> 156 </div>
added 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 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 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 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; } }
230
Menu

Theme