SPB Git forge

spb/fabri-ka

Public

Agrégateur de produits québécois — www.fabri-ka.com

217commits 1branches 0releases
66.1 MBsize
maindefault branch
3 h agolast push
Python 38.4% HTML 30.3% TypeScript 17.2% CSS 11% JavaScript 3.2%

Harmonisation Groupe KA (ka-ui) : tokens, badge, KaFooter, page /contact, SSO

- Vendorisation ka-ui (frontend/src/ka/) : tokens.css importé AVANT le CSS
  local ; :root réduit à l'accent TERRACOTTA (+ alias --lime/--lime-soft,
  --accent-dark → --accent-deep) et aux variables métier, socle dédupliqué,
  couleurs en dur remplacées par les tokens.
- Header : badge « Un service Groupe KA » (GroupeKaBadge) à côté du logo ;
  bouton « Se connecter avec KA » + lien « Créer un compte » (hub /connexion).
- Footer : remplacé par KaFooter (siteId fabri-ka) — contacts, sites de
  l'écosystème, mentions légales du hub ; dégagement de la barre mobile.
- Page /contact : route SPA (ecosystem.json — 3 courriels + rôles,
  avertissement, lien hub, sites) + rendu serveur SEO (seo.py) + sitemap.
- SEO : title « Fabri-Ka — Un service Groupe KA » (index.html).
- SSO KA ID : FABRIKA_BASE_URL par défaut → https://www.fabri-ka.com
  (auth.py) — le redirect_uri vers le hub n'est plus localhost.
- Tactile ≥ 44 px (bouton connexion, avatar) ; resolveJsonModule (tsconfig).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 1 mo ago (Aug 18, 2026) parent 20a2569

12 changed files +796 −236

modified fabrika/auth.py +1 −1
@@ -35,7 +35,7 @@ KA_HUB_URL = os.environ.get("KA_HUB_URL", "https://www.groupe-ka.com").rstrip("/
35 35 KA_SSO_SECRET = os.environ.get("KA_SSO_SECRET", "")
36 36 KA_CLIENT_ID = "fabri-ka"
37 37 # base publique de CETTE app (l'URL de rappel en découle)
38 −BASE_URL = os.environ.get("FABRIKA_BASE_URL", "http://localhost:8097").rstrip("/")
38 +BASE_URL = os.environ.get("FABRIKA_BASE_URL", "https://www.fabri-ka.com").rstrip("/")
39 39 REDIRECT_URI = f"{BASE_URL}/api/auth/ka/callback"
40 40 SECRET = os.environ.get("SESSION_SECRET", "") or hashlib.sha256(
41 41 (KA_SSO_SECRET or "fabrika-dev").encode()).hexdigest()
modified fabrika/seo.py +45 −2
@@ -331,7 +331,9 @@ def page_home() -> HTMLResponse:
331 331 <p>{esc(desc)}</p>
332 332 <p><a href="/produits">Voir les {fmt_int(totals["products"])} produits</a> ·
333 333 <a href="/boutiques">{fmt_int(totals["stores"])} boutiques</a> ·
334 − <a href="/stats">Statistiques</a> · <a href="/a-propos">À propos</a></p>
334 + <a href="/stats">Statistiques</a> · <a href="/a-propos">À propos</a> ·
335 + <a href="/contact">Contact</a> ·
336 + <a href="https://www.groupe-ka.com">Un service Groupe KA</a></p>
335 337 <h2>Parcourir par catégorie</h2>
336 338 {link_list([(f"/categorie/{CAT_SLUG.get(c['key'], slugify(c['key'] or 'autre'))}",
337 339 cat_label(c["key"]), f"{fmt_int(c['n'])} produits")
@@ -730,6 +732,45 @@ vers la boutique d'origine pour l'achat. Contact : contact@spboucher.ai</p>
730 732 return render(title=f"À propos | {SITE}", description=desc, canonical="/a-propos", body=body)
731 733
732 734
735 +def _ecosystem() -> dict:
736 + """ka-ui/ecosystem.json vendoré côté frontend — source unique des
737 + coordonnées et des sites du Groupe KA (partagée avec la page React)."""
738 + p = Path(__file__).resolve().parent.parent / "frontend" / "src" / "ka" / "ecosystem.json"
739 + try:
740 + return json.loads(p.read_text(encoding="utf-8"))
741 + except OSError:
742 + return {"org": {}, "hub": {"url": "https://www.groupe-ka.com"},
743 + "contacts": [], "sites": []}
744 +
745 +
746 +def page_contact() -> HTMLResponse:
747 + eco = _ecosystem()
748 + hub = (eco.get("hub") or {}).get("url", "https://www.groupe-ka.com")
749 + contacts = "".join(
750 + f'<li><a href="mailto:{esc(c["email"])}">{esc(c["email"])}</a>'
751 + f' — {esc(c["role"])}</li>'
752 + for c in eco.get("contacts", []))
753 + sites = "".join(
754 + f'<li><a href="https://{esc(st["domain"])}">{esc(st["wordmark"])}</a>'
755 + f' — {esc(st["tagline"])}</li>'
756 + for st in eco.get("sites", []))
757 + disclaimer = (eco.get("org") or {}).get("disclaimer", "")
758 + desc = ("Joindre le Groupe KA — Fabri-Ka est un service Groupe KA : "
759 + "projets et partenariats, médias, légal et vie privée (Loi 25).")
760 + body = f"""
761 +<div class="seo-page">
762 +<h1>Contact</h1>
763 +<p>Fabri-Ka est un service du <a href="{esc(hub)}">Groupe KA</a>. Les coordonnées
764 +ci-dessous sont communes à toutes les plateformes de l'écosystème.</p>
765 +<ul>{contacts}</ul>
766 +<p>{esc(disclaimer)}</p>
767 +<h2>Les sites de l'écosystème</h2>
768 +<ul>{sites}</ul>
769 +</div>"""
770 + return render(title=f"Contact | {SITE} — Un service Groupe KA",
771 + description=desc, canonical="/contact", body=body)
772 +
773 +
733 774 def page_404() -> HTMLResponse:
734 775 body = """
735 776 <div class="seo-page">
@@ -793,7 +834,7 @@ def sitemap(name: str) -> Response:
793 834 today = date.today().isoformat()
794 835 urls: list[tuple[str, str | None]] = [
795 836 ("/", today), ("/produits", today), ("/boutiques", today),
796 − ("/stats", today), ("/a-propos", None)]
837 + ("/stats", today), ("/a-propos", None), ("/contact", None)]
797 838 cat_last: dict[str, float] = {}
798 839 reg_last: dict[str, float] = {}
799 840 for c in combos:
@@ -870,6 +911,8 @@ def render_route(path: str, params) -> Response:
870 911 return page_stats()
871 912 if path == "a-propos":
872 913 return page_about()
914 + if path == "contact":
915 + return page_contact()
873 916 if path == "profil":
874 917 return render(title=f"Mon profil — {SITE}",
875 918 description="Profil du membre Groupe KA.",
modified frontend/index.html +1 −1
@@ -19,7 +19,7 @@
19 19 href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap"
20 20 rel="stylesheet"
21 21 />
22 − <title>Fabri-Ka — Tous les produits québécois. Un seul endroit.</title>
22 + <title>Fabri-Ka — Un service Groupe KA</title>
23 23 </head>
24 24 <body>
25 25 <div id="root"></div>
modified frontend/src/App.tsx +25 −37
@@ -4,8 +4,11 @@ import { fetchMe, logout, type Me } from './api'
4 4 import BottomNav from './components/BottomNav'
5 5 import SearchBar from './components/SearchBar'
6 6 import { FavoritesProvider } from './favorites'
7 +import GroupeKaBadge from './ka/GroupeKaBadge'
8 +import KaFooter from './ka/KaFooter'
7 9 import About from './pages/About'
8 10 import Catalog from './pages/Catalog'
11 +import Contact from './pages/Contact'
9 12 import Home from './pages/Home'
10 13 import NotFound from './pages/NotFound'
11 14 import ProductDetail from './pages/ProductDetail'
@@ -16,7 +19,7 @@ import Stores from './pages/Stores'
16 19
17 20 const HEADER_SEARCH_ID = 'header-search-input'
18 21
19 −/** Bouton « Connexion » (KA ID — SSO Groupe KA) ou avatar + menu compte. */
22 +/** Bouton « Se connecter avec KA » (KA ID — SSO Groupe KA) ou avatar + menu. */
20 23 function AccountMenu() {
21 24 const location = useLocation()
22 25 const navigate = useNavigate()
@@ -44,7 +47,15 @@ function AccountMenu() {
44 47 href={`/api/auth/ka/login?next=${encodeURIComponent(next)}`}
45 48 >
46 49 <span className="login-ka" aria-hidden="true">KA</span>
47 − Connexion
50 + Se connecter avec KA
51 + </a>
52 + <a
53 + className="signup-link"
54 + href="https://www.groupe-ka.com/connexion"
55 + target="_blank"
56 + rel="noopener noreferrer"
57 + >
58 + Créer un compte
48 59 </a>
49 60 </div>
50 61 )
@@ -144,14 +155,20 @@ function Header() {
144 155 return (
145 156 <header className="site-header">
146 157 <div className="site-header-inner">
147 − <Link to="/" className="logo" aria-label="Fabri-Ka — Accueil">
148 − Fabri<span className="logo-ka">·Ka</span>
149 − </Link>
158 + <div className="header-brand">
159 + <Link to="/" className="logo" aria-label="Fabri-Ka — Accueil">
160 + Fabri<span className="logo-ka">·Ka</span>
161 + </Link>
162 + {/* badge Groupe KA — desktop et mobile (masqué < 480 px, repris
163 + par le pied de page KaFooter) */}
164 + <GroupeKaBadge />
165 + </div>
150 166 <nav className="site-nav" aria-label="Navigation principale">
151 167 <NavLink to="/produits">Produits</NavLink>
152 168 <NavLink to="/boutiques">Boutiques</NavLink>
153 169 <NavLink to="/stats">Stats</NavLink>
154 170 <NavLink to="/a-propos">À propos</NavLink>
171 + <NavLink to="/contact">Contact</NavLink>
155 172 </nav>
156 173 <div className="site-header-search">
157 174 {/* remount on navigation so the field reflects the current query */}
@@ -174,37 +191,6 @@ function Header() {
174 191 )
175 192 }
176 193
177 −function Footer() {
178 − return (
179 − <footer className="site-footer">
180 − <div className="site-footer-inner">
181 − <div className="footer-brand">
182 − <span className="logo logo-footer">
183 − Fabri<span className="logo-ka">·Ka</span>
184 − </span>
185 − <p>Tous les produits québécois. Un seul endroit.</p>
186 − </div>
187 − <nav className="footer-col" aria-label="Explorer">
188 − <span className="footer-col-title">Explorer</span>
189 − <Link to="/produits">Produits</Link>
190 − <Link to="/boutiques">Boutiques</Link>
191 − <Link to="/produits?sort=recent">Nouveautés</Link>
192 − <Link to="/stats">Stats</Link>
193 − </nav>
194 − <nav className="footer-col" aria-label="Fabri-Ka">
195 − <span className="footer-col-title">Fabri-Ka</span>
196 − <Link to="/a-propos">À propos</Link>
197 − <a href="mailto:contact@spboucher.ai">Contact</a>
198 − </nav>
199 − </div>
200 − <p className="footer-legal">
201 − © 2026 Simon-Pierre Boucher — Fabri-Ka agrège des catalogues publics;
202 − les prix et disponibilités appartiennent aux boutiques sources.
203 − </p>
204 − </footer>
205 − )
206 −}
207 −
208 194 export default function App() {
209 195 return (
210 196 <FavoritesProvider>
@@ -224,10 +210,12 @@ export default function App() {
224 210 <Route path="/stats" element={<Stats />} />
225 211 <Route path="/profil" element={<Profile />} />
226 212 <Route path="/a-propos" element={<About />} />
213 + <Route path="/contact" element={<Contact />} />
227 214 <Route path="*" element={<NotFound />} />
228 215 </Routes>
229 216 </main>
230 − <Footer />
217 + {/* Pied de page commun Groupe KA (ka-ui) */}
218 + <KaFooter siteId="fabri-ka" />
231 219 <BottomNav />
232 220 </div>
233 221 </FavoritesProvider>
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 frontend/src/main.tsx +3 −0
@@ -2,6 +2,9 @@ import React from 'react'
2 2 import ReactDOM from 'react-dom/client'
3 3 import { BrowserRouter } from 'react-router-dom'
4 4 import App from './App'
5 +// Design system Groupe KA (ka-ui) — tokens + socle communs, importés AVANT
6 +// le CSS local qui ne surcharge que l'accent terracotta + les classes métier.
7 +import './ka/tokens.css'
5 8 import './styles.css'
6 9
7 10 ReactDOM.createRoot(document.getElementById('root')!).render(
added frontend/src/pages/Contact.tsx +62 −0
@@ -0,0 +1,62 @@
1 +// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 +// pages/Contact.tsx — page /contact commune Groupe KA : les 3 courriels de
3 +// l'écosystème (source : ka/ecosystem.json), l'avertissement d'agrégateur,
4 +// le lien vers le hub et la liste des sites du groupe.
5 +import { useEffect } from 'react'
6 +import eco from '../ka/ecosystem.json'
7 +
8 +export default function Contact() {
9 + useEffect(() => {
10 + document.title = 'Contact — Fabri-Ka — Un service Groupe KA'
11 + }, [])
12 +
13 + return (
14 + <div className="page contact-page">
15 + <span className="kicker">Nous joindre</span>
16 + <h1>Contact</h1>
17 + <p className="contact-lede">
18 + Fabri-Ka est un service du <b>Groupe KA</b> — {eco.org.tagline} Les
19 + coordonnées ci-dessous sont communes à toutes les plateformes de
20 + l&rsquo;écosystème.
21 + </p>
22 +
23 + <div className="contact-cards">
24 + {eco.contacts.map((c) => (
25 + <article className="contact-card" key={c.email}>
26 + <span className="klabel">{c.role}</span>
27 + <a href={`mailto:${c.email}`}>{c.email}</a>
28 + </article>
29 + ))}
30 + </div>
31 +
32 + <p className="contact-note">
33 + <b>{eco.org.disclaimer}</b>
34 + </p>
35 +
36 + <a
37 + className="contact-hub"
38 + href={eco.hub.url}
39 + target="_blank"
40 + rel="noopener noreferrer"
41 + >
42 + Visiter le portail Groupe KA ↗
43 + </a>
44 +
45 + <h2>Les sites de l&rsquo;écosystème</h2>
46 + <ul className="contact-sites">
47 + {eco.sites.map((s) => (
48 + <li key={s.id}>
49 + <a
50 + href={`https://${s.domain}`}
51 + target="_blank"
52 + rel="noopener noreferrer"
53 + >
54 + <b>{s.wordmark}</b>
55 + <span>{s.tagline}</span>
56 + </a>
57 + </li>
58 + ))}
59 + </ul>
60 + </div>
61 + )
62 +}
modified frontend/src/styles.css +272 −195
@@ -1,44 +1,44 @@
1 1 /* ==========================================================================
2 − Fabri-Ka — identité famille KA « éditorial sharp » (cf. Food-Ka / Lou-Ka)
2 + Fabri-Ka — surcouche locale du design system Groupe KA (ka-ui/tokens.css,
3 + importé AVANT ce fichier dans main.tsx) « éditorial sharp » :
3 4 · Typo display Space Grotesk / texte Inter / micro-étiquettes JetBrains Mono
4 5 · Signature : bordures encre + ombres décalées (néo-brutalisme raffiné)
5 − · Palette Fabri-Ka conservée : papier crème, encre chaude, terracotta
6 − #C4532E en accent, pin #234438 en profond, miel #F2CF63 en surlignage
6 + · ACCENT du site : TERRACOTTA #C4532E — seule surcharge de marque ;
7 + tout le reste (papier, encre, lignes, ombres, typo) vient des tokens.
7 8 ========================================================================== */
8 9
9 10 :root {
10 − --bg: #FAF6EE;
11 − --ink: #1E1B16;
12 − --accent: #C4532E;
13 − --accent-dark: #A94525;
14 − --pine: #234438;
15 − --sand: #F1E9DD;
16 − --border: #E4D9C8;
17 − --white: #FFFFFF;
18 − --muted: #6E6455;
19 −
20 − /* jetons famille KA (mêmes rôles que Food-Ka/Lou-Ka) */
21 − --line: rgba(30, 27, 22, 0.14);
22 − --line-strong: rgba(30, 27, 22, 0.85);
23 − --highlight: #F2CF63;
24 − --highlight-soft: #F9F0D2;
25 −
26 − --origin-a: #234438;
27 − --origin-b: #C4532E;
28 − --origin-c: #8A6D3B;
29 − --origin-d: #5B5B8A;
30 − --origin-e: #999999;
31 −
32 − --font-display: 'Space Grotesk', system-ui, sans-serif;
33 − --font-body: 'Inter', system-ui, -apple-system, sans-serif;
34 − --font-mono: 'JetBrains Mono', ui-monospace, monospace;
35 −
36 − --radius: 6px;
37 − --radius-lg: 10px;
38 − --shadow-hover: 0 6px 18px rgba(30, 27, 22, 0.08);
39 − --shadow-off: 6px 6px 0 var(--ink);
40 − --shadow-off-soft: 8px 8px 0 rgba(30, 27, 22, 0.08);
41 − --shadow-sheet: 0 -12px 40px rgba(30, 27, 22, 0.18);
11 + /* ---- ACCENT Fabri-Ka (terracotta) — surcharge des tokens ka-ui ---- */
12 + --accent: #c4532e;
13 + --accent-soft: #f7e3da;
14 + --accent-deep: #a94525;
15 + --on-accent: #ffffff;
16 + --accent-dark: var(--accent-deep); /* alias historique local */
17 + --lime: var(--accent);
18 + --lime-soft: var(--accent-soft);
19 +
20 + /* ---- Alias locaux → tokens communs (classes métier conservées) ---- */
21 + --bg: var(--paper);
22 + --sand: var(--surface-2);
23 + --border: var(--line);
24 + --white: var(--surface);
25 + --muted: var(--ink-2);
26 + --pine: var(--green);
27 + --highlight: var(--amber);
28 + --highlight-soft: var(--amber-soft);
29 +
30 + /* badges d'origine des produits (métier Fabri-Ka) */
31 + --origin-a: var(--green);
32 + --origin-b: var(--accent);
33 + --origin-c: #8a6d3b;
34 + --origin-d: #5b5b8a;
35 + --origin-e: var(--ink-3);
36 +
37 + /* géométrie / mise en page locales */
38 + --radius: var(--r-ctl);
39 + --radius-lg: var(--r-card);
40 + --shadow-hover: 0 6px 18px rgba(20, 24, 20, 0.08);
41 + --shadow-sheet: 0 -12px 40px rgba(20, 24, 20, 0.18);
42 42 --maxw: 1280px;
43 43
44 44 --header-h: 56px;
@@ -46,33 +46,17 @@
46 46 --safe-b: env(safe-area-inset-bottom, 0px);
47 47
48 48 --focus-ring: 0 0 0 3px rgba(196, 83, 46, 0.28);
49 −}
50 −
51 −* {
52 − box-sizing: border-box;
53 −}
54 49
55 −html {
56 − scroll-behavior: smooth;
57 − -webkit-text-size-adjust: 100%;
50 + /* marque KA ID (lime du hub — volontairement fixe) */
51 + --ka-ink: var(--ink);
52 + --ka-lime: #d9f26b;
58 53 }
59 54
55 +/* socle (*, html, body, img, ::selection…) : fourni par ka/tokens.css */
60 56 body {
61 − margin: 0;
62 − background: var(--bg);
63 − color: var(--ink);
64 − font-family: var(--font-body);
65 − font-size: 15px;
66 − line-height: 1.55;
67 − -webkit-font-smoothing: antialiased;
68 57 overscroll-behavior-y: none;
69 58 }
70 59
71 −img {
72 − max-width: 100%;
73 − display: block;
74 −}
75 −
76 60 a {
77 61 color: var(--accent);
78 62 text-decoration: none;
@@ -80,10 +64,10 @@ a {
80 64
81 65 h1, h2, h3 {
82 66 font-family: var(--font-display);
83 − font-weight: 900;
67 + font-weight: 700;
84 68 font-optical-sizing: auto;
85 69 line-height: 1.12;
86 − letter-spacing: -0.015em;
70 + letter-spacing: -0.03em;
87 71 margin: 0 0 0.5em;
88 72 color: var(--ink);
89 73 }
@@ -189,7 +173,7 @@ button:focus-visible,
189 173 position: sticky;
190 174 top: 0;
191 175 z-index: 60;
192 − background: rgba(250, 246, 240, 0.94);
176 + background: rgba(245, 243, 238, 0.94);
193 177 backdrop-filter: blur(12px);
194 178 -webkit-backdrop-filter: blur(12px);
195 179 border-bottom: 1px solid var(--border);
@@ -208,7 +192,7 @@ button:focus-visible,
208 192
209 193 .logo {
210 194 font-family: var(--font-display);
211 − font-weight: 900;
195 + font-weight: 700;
212 196 font-size: 1.45rem;
213 197 color: var(--ink);
214 198 letter-spacing: -0.02em;
@@ -357,7 +341,7 @@ button:focus-visible,
357 341 z-index: 70;
358 342 display: grid;
359 343 grid-template-columns: repeat(4, 1fr);
360 − background: rgba(250, 246, 240, 0.96);
344 + background: rgba(245, 243, 238, 0.96);
361 345 backdrop-filter: blur(14px);
362 346 -webkit-backdrop-filter: blur(14px);
363 347 border-top: 1px solid var(--border);
@@ -484,84 +468,17 @@ button:focus-visible,
484 468 }
485 469
486 470 /* --------------------------------------------------------------------------
487 − Footer
471 + Footer — KaFooter (ka-ui) : styles .ka-footer dans ka/tokens.css.
472 + Localement : dégager la barre d'onglets mobile (BottomNav fixe en bas).
488 473 -------------------------------------------------------------------------- */
489 474
490 −.site-footer {
491 − background: var(--sand);
492 − border-top: 1px solid var(--border);
493 − padding: 2.25rem 1rem 1.25rem;
494 − margin-top: 2.5rem;
495 − /* clear the mobile tab bar */
496 − padding-bottom: calc(1.25rem + var(--tabbar-h) + var(--safe-b));
497 −}
498 −
499 −.site-footer-inner {
500 − max-width: var(--maxw);
501 − margin: 0 auto;
502 − display: grid;
503 − grid-template-columns: 1fr;
504 − gap: 1.75rem;
505 −}
506 −
507 −.logo-footer {
508 − font-size: 1.3rem;
509 −}
510 −
511 −.footer-brand p {
512 − margin: 0.4rem 0 0;
513 − color: var(--muted);
514 − font-size: 0.92rem;
515 −}
516 −
517 −.footer-col {
518 − display: flex;
519 − flex-direction: column;
520 − gap: 0.45rem;
521 − align-items: flex-start;
522 −}
523 −
524 −.footer-col-title {
525 − font-size: 0.72rem;
526 − font-weight: 700;
527 − text-transform: uppercase;
528 − letter-spacing: 0.16em;
529 − color: var(--muted);
530 − margin-bottom: 0.2rem;
531 −}
532 −
533 −.footer-col a {
534 − color: var(--ink);
535 − font-weight: 500;
536 − font-size: 0.95rem;
537 − padding: 0.15rem 0;
538 −}
539 −
540 −@media (hover: hover) {
541 − .footer-col a:hover {
542 − color: var(--accent);
543 − text-decoration: none;
544 − }
545 −}
546 −
547 −.footer-legal {
548 − max-width: var(--maxw);
549 − margin: 1.75rem auto 0;
550 − padding-top: 1.1rem;
551 − border-top: 1px solid var(--border);
552 − color: var(--muted);
553 − font-size: 0.8rem;
554 − line-height: 1.5;
475 +.ka-footer {
476 + padding-bottom: calc(44px + var(--tabbar-h) + var(--safe-b));
555 477 }
556 478
557 479 @media (min-width: 768px) {
558 − .site-footer {
559 − padding: 2.75rem 1.25rem 1.5rem;
560 − }
561 −
562 − .site-footer-inner {
563 − grid-template-columns: 2fr 1fr 1fr;
564 − gap: 2rem;
480 + .ka-footer {
481 + padding-bottom: 44px;
565 482 }
566 483 }
567 484
@@ -596,7 +513,7 @@ button:focus-visible,
596 513
597 514 .hero-title {
598 515 font-size: clamp(2.35rem, 8.5vw, 4.75rem);
599 − font-weight: 900;
516 + font-weight: 700;
600 517 letter-spacing: -0.03em;
601 518 line-height: 1.04;
602 519 margin-bottom: 0.55em;
@@ -648,7 +565,7 @@ button:focus-visible,
648 565
649 566 .stat-number {
650 567 font-family: var(--font-display);
651 − font-weight: 900;
568 + font-weight: 700;
652 569 font-optical-sizing: auto;
653 570 font-size: clamp(1.9rem, 5vw, 2.5rem);
654 571 color: var(--pine);
@@ -863,7 +780,7 @@ a.card:active {
863 780 @media (hover: hover) {
864 781 a.card:hover {
865 782 text-decoration: none;
866 − border-color: #D3C4AC;
783 + border-color: rgba(20, 24, 20, 0.3);
867 784 box-shadow: var(--shadow-hover);
868 785 }
869 786 }
@@ -1080,7 +997,7 @@ a.card:active {
1080 997 color: var(--white);
1081 998 font-size: 0.78rem;
1082 999 line-height: 1;
1083 − box-shadow: 0 1px 4px rgba(30, 27, 22, 0.18);
1000 + box-shadow: 0 1px 4px rgba(20, 24, 20, 0.18);
1084 1001 }
1085 1002
1086 1003 .origin-badge-letter {
@@ -1208,7 +1125,7 @@ a.card:active {
1208 1125 z-index: 40;
1209 1126 margin: -0.5rem -1rem 0.9rem;
1210 1127 padding: 0.65rem 1rem;
1211 − background: rgba(250, 246, 240, 0.94);
1128 + background: rgba(245, 243, 238, 0.94);
1212 1129 backdrop-filter: blur(12px);
1213 1130 -webkit-backdrop-filter: blur(12px);
1214 1131 transition: padding 0.2s ease, box-shadow 0.2s ease;
@@ -1312,7 +1229,7 @@ a.card:active {
1312 1229
1313 1230 .filters-title {
1314 1231 font-family: var(--font-display);
1315 − font-weight: 900;
1232 + font-weight: 700;
1316 1233 font-size: 1.15rem;
1317 1234 }
1318 1235
@@ -1456,7 +1373,7 @@ a.card:active {
1456 1373 font-weight: 600;
1457 1374 font-size: 0.95rem;
1458 1375 cursor: pointer;
1459 − box-shadow: 0 8px 22px rgba(30, 27, 22, 0.3);
1376 + box-shadow: 0 8px 22px rgba(20, 24, 20, 0.3);
1460 1377 transition: transform 0.12s ease;
1461 1378 }
1462 1379
@@ -1501,7 +1418,7 @@ a.card:active {
1501 1418 .sheet-backdrop {
1502 1419 position: absolute;
1503 1420 inset: 0;
1504 − background: rgba(30, 27, 22, 0.45);
1421 + background: rgba(20, 24, 20, 0.45);
1505 1422 animation: backdrop-in 0.2s ease;
1506 1423 }
1507 1424
@@ -1924,7 +1841,7 @@ a.card:active {
1924 1841 align-items: center;
1925 1842 gap: 0.75rem;
1926 1843 padding: 0.6rem 1rem;
1927 − background: rgba(250, 246, 240, 0.97);
1844 + background: rgba(245, 243, 238, 0.97);
1928 1845 backdrop-filter: blur(12px);
1929 1846 -webkit-backdrop-filter: blur(12px);
1930 1847 border-top: 1px solid var(--border);
@@ -2130,7 +2047,7 @@ a.card:active {
2130 2047 border: none;
2131 2048 color: var(--white);
2132 2049 font-family: var(--font-display);
2133 − font-weight: 900;
2050 + font-weight: 700;
2134 2051 font-optical-sizing: auto;
2135 2052 line-height: 1;
2136 2053 user-select: none;
@@ -2149,7 +2066,7 @@ a.card:active {
2149 2066 }
2150 2067
2151 2068 .store-logo-ring {
2152 − box-shadow: 0 0 0 3px var(--white), 0 4px 14px rgba(30, 27, 22, 0.18);
2069 + box-shadow: 0 0 0 3px var(--white), 0 4px 14px rgba(20, 24, 20, 0.18);
2153 2070 }
2154 2071
2155 2072 /* --------------------------------------------------------------------------
@@ -2180,7 +2097,7 @@ a.card:active {
2180 2097 position: relative;
2181 2098 height: 118px;
2182 2099 overflow: hidden;
2183 − background: linear-gradient(120deg, var(--sand), #E8DBC4 55%, var(--sand));
2100 + background: linear-gradient(120deg, var(--sand), var(--line) 55%, var(--sand));
2184 2101 }
2185 2102
2186 2103 .store-hero-cover-img {
@@ -2199,8 +2116,8 @@ a.card:active {
2199 2116
2200 2117 .store-hero-covered .store-hero-scrim {
2201 2118 background: linear-gradient(
2202 − rgba(30, 27, 22, 0.24),
2203 − rgba(30, 27, 22, 0.48)
2119 + rgba(20, 24, 20, 0.24),
2120 + rgba(20, 24, 20, 0.48)
2204 2121 );
2205 2122 }
2206 2123
@@ -2374,7 +2291,7 @@ a.card:active {
2374 2291
2375 2292 .store-stat-value {
2376 2293 font-family: var(--font-display);
2377 − font-weight: 900;
2294 + font-weight: 700;
2378 2295 font-optical-sizing: auto;
2379 2296 font-size: 1.55rem;
2380 2297 color: var(--pine);
@@ -2474,7 +2391,7 @@ a.card:active {
2474 2391 align-items: center;
2475 2392 gap: 0.75rem;
2476 2393 padding: 0.6rem 1rem;
2477 − background: rgba(250, 246, 240, 0.97);
2394 + background: rgba(245, 243, 238, 0.97);
2478 2395 backdrop-filter: blur(12px);
2479 2396 -webkit-backdrop-filter: blur(12px);
2480 2397 border-top: 1px solid var(--border);
@@ -2679,10 +2596,6 @@ a.card:active {
2679 2596 .page {
2680 2597 padding: 2rem 1.25rem 4rem;
2681 2598 }
2682 −
2683 − .site-footer {
2684 − padding-bottom: 1.5rem;
2685 − }
2686 2599 }
2687 2600
2688 2601 /* --------------------------------------------------------------------------
@@ -2763,7 +2676,7 @@ a.card:active {
2763 2676
2764 2677 .stats-kpi-value {
2765 2678 font-family: var(--font-display);
2766 − font-weight: 900;
2679 + font-weight: 700;
2767 2680 font-optical-sizing: auto;
2768 2681 font-size: clamp(1.35rem, 4vw, 1.7rem);
2769 2682 color: var(--pine);
@@ -3203,7 +3116,7 @@ a.card:active {
3203 3116 .stats-rank {
3204 3117 flex: 0 0 1.6rem;
3205 3118 font-family: var(--font-display);
3206 − font-weight: 900;
3119 + font-weight: 700;
3207 3120 font-optical-sizing: auto;
3208 3121 color: var(--accent);
3209 3122 font-size: 0.95rem;
@@ -3604,12 +3517,6 @@ a.card:active {
3604 3517 Compte — connexion KA ID (SSO Groupe KA) : bouton d'en-tête + menu
3605 3518 -------------------------------------------------------------------------- */
3606 3519
3607 −:root {
3608 − --ka-ink: #141814;
3609 − --ka-lime: #d9f26b;
3610 − --font-mono: 'SF Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
3611 −}
3612 −
3613 3520 .site-header-account {
3614 3521 margin-left: auto;
3615 3522 position: relative;
@@ -3629,7 +3536,7 @@ a.card:active {
3629 3536 align-items: center;
3630 3537 gap: 0.5rem;
3631 3538 padding: 0.45rem 0.85rem;
3632 − min-height: 38px;
3539 + min-height: var(--touch, 44px);
3633 3540 border: 1px solid var(--border);
3634 3541 border-radius: 999px;
3635 3542 background: var(--white);
@@ -3663,8 +3570,8 @@ a.card:active {
3663 3570 }
3664 3571
3665 3572 .account-btn {
3666 − width: 36px;
3667 − height: 36px;
3573 + width: 44px;
3574 + height: 44px;
3668 3575 border-radius: 999px;
3669 3576 border: 2px solid var(--accent);
3670 3577 background: var(--sand);
@@ -3685,7 +3592,7 @@ a.card:active {
3685 3592
3686 3593 .account-initial {
3687 3594 font-family: var(--font-display);
3688 − font-weight: 900;
3595 + font-weight: 700;
3689 3596 font-size: 1rem;
3690 3597 color: var(--accent);
3691 3598 }
@@ -3797,11 +3704,11 @@ a.card:active {
3797 3704 max-width: 520px;
3798 3705 overflow: hidden;
3799 3706 background: var(--ka-ink);
3800 − color: #f5f3ee;
3707 + color: var(--paper);
3801 3708 border: 2px solid var(--ka-ink);
3802 3709 border-radius: 16px;
3803 3710 padding: 24px 26px 0;
3804 − box-shadow: 10px 10px 0 rgba(30, 27, 22, 0.15);
3711 + box-shadow: 10px 10px 0 rgba(20, 24, 20, 0.15);
3805 3712 }
3806 3713
3807 3714 .pc-watermark {
@@ -3810,7 +3717,7 @@ a.card:active {
3810 3717 top: -34px;
3811 3718 pointer-events: none;
3812 3719 font-family: var(--font-display);
3813 − font-weight: 900;
3720 + font-weight: 700;
3814 3721 font-size: 170px;
3815 3722 letter-spacing: -0.06em;
3816 3723 color: rgba(217, 242, 107, 0.07);
@@ -3828,7 +3735,7 @@ a.card:active {
3828 3735
3829 3736 .pc-brand {
3830 3737 font-family: var(--font-display);
3831 − font-weight: 900;
3738 + font-weight: 700;
3832 3739 font-size: 24px;
3833 3740 letter-spacing: -0.02em;
3834 3741 }
@@ -4134,7 +4041,7 @@ a.card:active {
4134 4041 margin: 1.6em 0 0.5em;
4135 4042 }
4136 4043 .seo-stats b {
4137 − color: #c4532e;
4044 + color: var(--accent);
4138 4045 }
4139 4046 .seo-links {
4140 4047 list-style: none;
@@ -4230,7 +4137,7 @@ h1, h2, h3, h4 {
4230 4137
4231 4138 /* ---- header : trait d'encre + chip ·Ka rotée --------------------------- */
4232 4139 .site-header {
4233 − background: rgba(250, 246, 238, 0.88);
4140 + background: rgba(245, 243, 238, 0.88);
4234 4141 border-bottom: 2px solid var(--ink);
4235 4142 }
4236 4143 .logo {
@@ -4296,8 +4203,7 @@ h1, h2, h3, h4 {
4296 4203
4297 4204 /* ---- kicker / entêtes de section : micro-mono -------------------------- */
4298 4205 .hero-eyebrow,
4299 −.filter-label,
4300 −.footer-col-title {
4206 +.filter-label {
4301 4207 font-family: var(--font-mono);
4302 4208 font-size: 11.5px;
4303 4209 font-weight: 500;
@@ -4336,7 +4242,7 @@ a.card:active { border-color: var(--ink); }
4336 4242 .chip:hover {
4337 4243 border-color: var(--ink);
4338 4244 background: var(--white);
4339 − box-shadow: 3px 3px 0 rgba(30, 27, 22, 0.12);
4245 + box-shadow: 3px 3px 0 rgba(20, 24, 20, 0.12);
4340 4246 transform: translate(-1px, -1px);
4341 4247 }
4342 4248 }
@@ -4393,7 +4299,7 @@ a.card:active { border-color: var(--ink); }
4393 4299 }
4394 4300 @media (hover: hover) {
4395 4301 .btn-secondary:hover {
4396 − box-shadow: 3px 3px 0 rgba(30, 27, 22, 0.15);
4302 + box-shadow: 3px 3px 0 rgba(20, 24, 20, 0.15);
4397 4303 transform: translate(-1px, -1px);
4398 4304 }
4399 4305 }
@@ -4411,32 +4317,203 @@ a.card:active { border-color: var(--ink); }
4411 4317
4412 4318 /* ---- barre d'onglets mobile ---------------------------------------------- */
4413 4319 .bottom-nav {
4414 − background: rgba(250, 246, 238, 0.96);
4320 + background: rgba(245, 243, 238, 0.96);
4415 4321 border-top: 2px solid var(--ink);
4416 4322 }
4417 4323
4418 −/* ---- footer : bloc pin profond ------------------------------------------- */
4419 −.site-footer {
4420 − background: var(--pine);
4421 − border-top: 2px solid var(--ink);
4422 − color: rgba(250, 246, 238, 0.92);
4423 −}
4424 −.site-footer .logo,
4425 −.site-footer a,
4426 −.footer-brand p,
4427 −.footer-legal {
4428 − color: rgba(250, 246, 238, 0.92);
4429 −}
4430 −.site-footer .footer-col-title { color: var(--highlight); }
4431 −.site-footer a:hover { color: var(--highlight); }
4432 −.site-footer .logo-ka {
4433 − background: var(--highlight);
4434 − color: var(--pine);
4435 −}
4436 −
4437 4324 /* ---- contenu SSR (seo.py) aligné sur le thème ---------------------------- */
4438 4325 .seo-page h1, .seo-page h2 { font-family: var(--font-display); }
4439 4326 .seo-stats b { color: var(--accent); font-family: var(--font-mono); }
4440 4327 .seo-card a { font-weight: 600; }
4441 4328 .seo-card-price { font-family: var(--font-mono); color: var(--accent); }
4442 4329 .seo-card-media img { border: 1.5px solid var(--line); }
4330 +
4331 +/* --------------------------------------------------------------------------
4332 + Groupe KA — badge d'en-tête + « Créer un compte » (KA ID)
4333 + -------------------------------------------------------------------------- */
4334 +
4335 +.site-header .gk-badge {
4336 + flex: none;
4337 +}
4338 +
4339 +@media (max-width: 479px) {
4340 + .site-header .gk-badge {
4341 + display: none;
4342 + }
4343 +}
4344 +
4345 +.header-brand {
4346 + display: flex;
4347 + align-items: center;
4348 + gap: 0.7rem;
4349 + min-width: 0;
4350 +}
4351 +
4352 +.signup-link {
4353 + display: none;
4354 + align-items: center;
4355 + min-height: var(--touch, 44px);
4356 + padding: 0 0.55rem;
4357 + font-size: 0.85rem;
4358 + font-weight: 500;
4359 + color: var(--muted);
4360 + white-space: nowrap;
4361 +}
4362 +
4363 +@media (min-width: 768px) {
4364 + .signup-link {
4365 + display: inline-flex;
4366 + }
4367 +}
4368 +
4369 +/* --------------------------------------------------------------------------
4370 + Page /contact — coordonnées de l'écosystème (ka/ecosystem.json)
4371 + -------------------------------------------------------------------------- */
4372 +
4373 +.contact-page .contact-lede {
4374 + color: var(--muted);
4375 + max-width: 620px;
4376 + margin: 0.35rem 0 1.6rem;
4377 +}
4378 +
4379 +.contact-cards {
4380 + display: grid;
4381 + grid-template-columns: 1fr;
4382 + gap: 0.9rem;
4383 + margin-bottom: 1.8rem;
4384 +}
4385 +
4386 +@media (min-width: 768px) {
4387 + .contact-cards {
4388 + grid-template-columns: repeat(3, 1fr);
4389 + }
4390 +}
4391 +
4392 +.contact-card {
4393 + background: var(--surface);
4394 + border: 1.5px solid var(--line-strong);
4395 + border-radius: var(--r-card);
4396 + box-shadow: var(--shadow-off-mid);
4397 + padding: 1.1rem 1.15rem 1.2rem;
4398 + display: flex;
4399 + flex-direction: column;
4400 + gap: 0.45rem;
4401 + min-width: 0;
4402 +}
4403 +
4404 +.contact-card .klabel {
4405 + color: var(--accent);
4406 +}
4407 +
4408 +.contact-card a {
4409 + font-family: var(--font-mono);
4410 + font-weight: 700;
4411 + font-size: 0.92rem;
4412 + color: var(--ink);
4413 + word-break: break-all;
4414 + display: inline-flex;
4415 + align-items: center;
4416 + min-height: var(--touch, 44px);
4417 +}
4418 +
4419 +@media (hover: hover) {
4420 + .contact-card a:hover {
4421 + color: var(--accent);
4422 + }
4423 +}
4424 +
4425 +.contact-card p {
4426 + margin: 0;
4427 + color: var(--muted);
4428 + font-size: 0.9rem;
4429 +}
4430 +
4431 +.contact-note {
4432 + max-width: 720px;
4433 + border-left: 3px solid var(--accent);
4434 + background: var(--accent-soft);
4435 + padding: 0.9rem 1rem;
4436 + border-radius: 0 var(--r-ctl) var(--r-ctl) 0;
4437 + margin-bottom: 1.8rem;
4438 + font-size: 0.92rem;
4439 +}
4440 +
4441 +.contact-hub {
4442 + display: inline-flex;
4443 + align-items: center;
4444 + gap: 0.5rem;
4445 + min-height: var(--touch, 44px);
4446 + padding: 0 1.1rem;
4447 + border: 1.5px solid var(--ink);
4448 + border-radius: var(--r-pill);
4449 + background: var(--accent);
4450 + color: var(--on-accent);
4451 + font-weight: 600;
4452 + box-shadow: 3px 3px 0 var(--ink);
4453 + margin-bottom: 2.2rem;
4454 +}
4455 +
4456 +.contact-hub:active {
4457 + transform: translate(2px, 2px);
4458 + box-shadow: 1px 1px 0 var(--ink);
4459 +}
4460 +
4461 +@media (hover: hover) {
4462 + .contact-hub:hover {
4463 + text-decoration: none;
4464 + background: var(--accent-deep);
4465 + }
4466 +}
4467 +
4468 +.contact-sites {
4469 + list-style: none;
4470 + margin: 0.8rem 0 0;
4471 + padding: 0;
4472 + display: grid;
4473 + grid-template-columns: 1fr;
4474 + gap: 0.5rem;
4475 +}
4476 +
4477 +@media (min-width: 480px) {
4478 + .contact-sites {
4479 + grid-template-columns: repeat(2, 1fr);
4480 + }
4481 +}
4482 +
4483 +@media (min-width: 1024px) {
4484 + .contact-sites {
4485 + grid-template-columns: repeat(3, 1fr);
4486 + }
4487 +}
4488 +
4489 +.contact-sites a {
4490 + display: flex;
4491 + align-items: baseline;
4492 + gap: 0.55rem;
4493 + min-height: var(--touch, 44px);
4494 + padding: 0.55rem 0.8rem;
4495 + border: 1px solid var(--line);
4496 + border-radius: var(--r-ctl);
4497 + background: var(--surface);
4498 + color: var(--ink);
4499 +}
4500 +
4501 +@media (hover: hover) {
4502 + .contact-sites a:hover {
4503 + text-decoration: none;
4504 + border-color: var(--line-strong);
4505 + box-shadow: var(--shadow-off-mid);
4506 + }
4507 +}
4508 +
4509 +.contact-sites b {
4510 + font-family: var(--font-display);
4511 + font-size: 0.95rem;
4512 + white-space: nowrap;
4513 +}
4514 +
4515 +.contact-sites span {
4516 + color: var(--muted);
4517 + font-size: 0.82rem;
4518 + min-width: 0;
4519 +}
modified frontend/tsconfig.app.json +1 −0
@@ -10,6 +10,7 @@
10 10
11 11 /* Bundler mode */
12 12 "moduleResolution": "bundler",
13 + "resolveJsonModule": true,
13 14 "allowImportingTsExtensions": true,
14 15 "isolatedModules": true,
15 16 "moduleDetection": "force",
16 17