SPB Git forge

spb/resto-ka

Public

Resto·Ka — tous les restaurants du Québec, menus complets et prix réels (famille ·Ka)

52commits 1branches 0releases
11.6 MBsize
maindefault branch
19 days agolast push
Python 69.3% TypeScript 16.7% CSS 7.9% JavaScript 4.7% HTML 1.4%

Harmonisation ka-ui : accent safran, badge Groupe KA, KaFooter, page /contact

- Vendorisation du package ka-ui (frontend/src/ka/) : tokens.css, ecosystem.json,
  KaFooter.tsx, GroupeKaBadge.tsx
- tokens.css importé avant styles.css ; styles.css réduit à l accent SAFRAN
  (--accent #f08c00, soft #fdeed7, deep #b96a00, on-accent #141814) + alias --lime,
  socle dupliqué retiré, classes métier passées sur var(--accent)
- Header : badge « Un service Groupe KA » (desktop >= 900 px + menu mobile)
- Footer : KaFooter commun (contacts, avertissement, écosystème) + rangée de nav
  locale et crédit OpenStreetMap au-dessus
- Nouvelle page /contact (infos d ecosystem.json, lien hub, création de compte
  KA ID -> groupe-ka.com/connexion) + prefixe SPA « contact » dans web.py
- SEO : titre « Resto-Ka — Un service Groupe KA »

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

10 changed files +604 −165

modified frontend/index.html +1 −1
@@ -8,7 +8,7 @@
8 8 <head>
9 9 <meta charset="UTF-8" />
10 10 <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
11 − <title>Resto-Ka — Tous les restaurants du Québec, menus et prix</title>
11 + <title>Resto-Ka — Un service Groupe KA</title>
12 12 <meta name="description" content="Resto-Ka agrège les restaurants du Québec avec leurs menus complets et leurs prix réels — comparables, cherchables, dans les 17 régions. Chaque resto, chaque plat, chaque prix." />
13 13 <meta name="theme-color" content="#ffffff" />
14 14 <meta name="mobile-web-app-capable" content="yes" />
modified frontend/src/App.tsx +23 −32
@@ -15,6 +15,9 @@ import {
15 15 IcoChart, IcoCompass, IcoFolder, IcoPlate, IcoSearch,
16 16 } from "./components/Icons";
17 17 import { LogoIcon } from "./components/Logo";
18 +import GroupeKaBadge from "./ka/GroupeKaBadge";
19 +import KaFooter from "./ka/KaFooter";
20 +import ContactPage from "./pages/Contact";
18 21 import FavorisPage from "./pages/Favoris";
19 22 import Home from "./pages/Home";
20 23 import RestoPage from "./pages/Resto";
@@ -122,6 +125,7 @@ const NAV_LINKS = [
122 125 { to: "/", label: "Restos", icon: <IcoPlate size={17} />, end: true },
123 126 { to: "/stats", label: "Stats", icon: <IcoChart size={17} />, end: false },
124 127 { to: "/sources", label: "Sources", icon: <IcoFolder size={17} />, end: false },
128 + { to: "/contact", label: "Contact", icon: <IcoCompass size={17} />, end: false },
125 129 ];
126 130
127 131 function Header() {
@@ -144,6 +148,7 @@ function Header() {
144 148 Resto-<span className="ka">Ka</span>
145 149 <span className="brand-tag">Chaque resto, chaque plat, chaque prix.</span>
146 150 </NavLink>
151 + <span className="hdr-badge"><GroupeKaBadge /></span>
147 152 <nav className="nav" aria-label="Navigation principale">
148 153 {NAV_LINKS.map((l) => (
149 154 <NavLink key={l.to} to={l.to} end={l.end}
@@ -179,6 +184,7 @@ function Header() {
179 184 <span className="mm-arrow" aria-hidden="true">→</span>
180 185 </NavLink>
181 186 ))}
187 + <div className="mm-badge"><GroupeKaBadge /></div>
182 188 <div className="mm-foot">
183 189 Agrégateur indépendant — menus et prix captés aux sources, chaque
184 190 fiche renvoie au resto original et affiche le contexte du prix.
@@ -191,43 +197,27 @@ function Header() {
191 197 );
192 198 }
193 199
200 +/** Rangée de nav locale + footer commun Groupe KA (ka-ui/KaFooter). */
194 201 function Footer() {
195 202 return (
196 − <footer className="footer">
197 − <div className="container">
198 − <div className="fbrand">
199 − <LogoIcon size={32} dark />
200 − Resto-<span className="ka">Ka</span>
201 − </div>
202 − <div className="fbaseline">
203 − Chaque resto, chaque plat, <span className="hl">chaque prix.</span>
204 − </div>
205 − <div className="frow">
206 − <div>
207 − <b>Agrégateur indépendant</b> des restaurants du Québec — menus
208 − complets et prix réels dans les 17 régions. Les menus proviennent
209 − des plateformes de commande des restos eux-mêmes ; chaque prix est
210 − étiqueté selon son contexte (salle, emporter, livraison) et chaque
211 − fiche renvoie à la source originale.
212 − </div>
213 − <div>
214 − Resto-Ka est une plateforme du{" "}
215 − <a href="https://www.groupe-ka.com" target="_blank" rel="noopener noreferrer">
216 − <b>Groupe KA</b>
217 − </a>{" "}
218 − — contact :{" "}
219 − <a href="mailto:contact@groupe-ka.com">contact@groupe-ka.com</a>
203 + <>
204 + <div className="prefoot">
205 + <div className="container prefoot-inner">
206 + <nav className="prefoot-nav" aria-label="Navigation du pied de page">
207 + {NAV_LINKS.map((l) => (
208 + <NavLink key={l.to} to={l.to} end={l.end}>{l.label}</NavLink>
209 + ))}
210 + <NavLink to="/favoris">Favoris</NavLink>
211 + </nav>
212 + <div className="prefoot-osm">
213 + Données de découverte ©{" "}
214 + <a href="https://www.openstreetmap.org/copyright" target="_blank"
215 + rel="noopener noreferrer">contributeurs OpenStreetMap</a> (ODbL)
220 216 </div>
221 217 </div>
222 − <div className="fmono">
223 − © {new Date().getFullYear()} Groupe KA — Resto-Ka
224 − {" · "}
225 − Données de découverte ©{" "}
226 − <a href="https://www.openstreetmap.org/copyright" target="_blank"
227 − rel="noopener noreferrer">contributeurs OpenStreetMap</a> (ODbL)
228 − </div>
229 218 </div>
230 − </footer>
219 + <KaFooter siteId="resto-ka" />
220 + </>
231 221 );
232 222 }
233 223
@@ -262,6 +252,7 @@ export default function App() {
262 252 <Route path="/favoris" element={<FavorisPage />} />
263 253 <Route path="/stats" element={<StatsPage />} />
264 254 <Route path="/sources" element={<SourcesPage />} />
255 + <Route path="/contact" element={<ContactPage />} />
265 256 <Route
266 257 path="*"
267 258 element={
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 +1 −0
@@ -7,6 +7,7 @@ import React from "react";
7 7 import ReactDOM from "react-dom/client";
8 8 import { BrowserRouter } from "react-router-dom";
9 9 import App from "./App";
10 +import "./ka/tokens.css"; // socle ka-ui (importé AVANT styles.css)
10 11 import "./styles.css";
11 12
12 13 ReactDOM.createRoot(document.getElementById("root")!).render(
added frontend/src/pages/Contact.tsx +75 −0
@@ -0,0 +1,75 @@
1 +// ==============================================================================
2 +// Author: Simon-Pierre Boucher <contact@spboucher.ai>
3 +// File: pages/Contact.tsx
4 +// Desc: Page Contact — infos officielles du Groupe KA tirées de
5 +// ka-ui/ecosystem.json (courriels + rôles, avertissement, hub,
6 +// écosystème des sites), composants du design system éditorial sharp.
7 +// ==============================================================================
8 +import { useEffect } from "react";
9 +import eco from "../ka/ecosystem.json";
10 +
11 +export default function ContactPage() {
12 + useEffect(() => {
13 + document.title = "Contact — Resto-Ka — Un service Groupe KA";
14 + window.scrollTo(0, 0);
15 + return () => { document.title = "Resto-Ka — Un service Groupe KA"; };
16 + }, []);
17 +
18 + const sites = eco.sites.filter((s) => s.id !== "resto-ka");
19 +
20 + return (
21 + <div className="contact-page container">
22 + <span className="kicker">Contact</span>
23 + <h1>
24 + Écrire au <span className="hl">Groupe KA</span>
25 + </h1>
26 + <p className="sub">
27 + Resto-Ka est un service du Groupe KA. Toutes les demandes — projets,
28 + médias, données, légal — passent par les adresses officielles du groupe.
29 + </p>
30 +
31 + <div className="contact-grid">
32 + {eco.contacts.map((c) => (
33 + <div className="contact-card" key={c.email}>
34 + <span className="role">{c.role}</span>
35 + <a className="mail" href={`mailto:${c.email}`}>{c.email}</a>
36 + </div>
37 + ))}
38 + </div>
39 +
40 + <p className="contact-note">
41 + <b>{eco.org.disclaimer}</b> Chaque fiche de Resto-Ka renvoie au
42 + restaurant original et affiche le contexte de chaque prix.
43 + </p>
44 +
45 + <div className="contact-hub">
46 + <a className="btn btn-primary" href={eco.hub.url} target="_blank" rel="noopener noreferrer">
47 + Visiter le hub groupe-ka.com →
48 + </a>
49 + <a
50 + className="btn"
51 + href={`${eco.hub.url}${eco.hub.loginPath}`}
52 + target="_blank"
53 + rel="noopener noreferrer"
54 + >
55 + Créer un compte KA ID
56 + </a>
57 + <span className="hint">{eco.hub.signupNote}</span>
58 + </div>
59 +
60 + <h2>L'écosystème ·Ka</h2>
61 + <p className="sub" style={{ marginBottom: 0 }}>{eco.org.tagline}</p>
62 + <ul className="contact-sites">
63 + {sites.map((s) => (
64 + <li key={s.id}>
65 + <a href={`https://${s.domain}`} target="_blank" rel="noopener noreferrer"
66 + title={s.tagline}>
67 + <span className="dot" style={{ background: s.accent }} aria-hidden="true" />
68 + {s.wordmark}
69 + </a>
70 + </li>
71 + ))}
72 + </ul>
73 + </div>
74 + );
75 +}
modified frontend/src/styles.css +117 −131
@@ -2,89 +2,29 @@
2 2 Author: Simon-Pierre Boucher <contact@spboucher.ai>
3 3 File: styles.css
4 4 Desc: Identité visuelle Resto·Ka — système de design « éditorial sharp »
5 − du GROUPE KA (identité partagée avec groupe-ka.com, Lou·Ka, Vrai-Prix,
6 − ValoPlex) :
7 − · Typo display Space Grotesk / texte Inter / micro-étiquettes JetBrains Mono
8 − · Palette : papier #f5f3ee · encre #141814 · vert profond #1c5c41 ·
9 − lime électrique #d9f26b · ambre #e8a33d
10 − · Signature : bordures encre 1,5 px + ombres décalées dures (néo-brutalisme
11 − raffiné), grain de film pleine page, surlignés lime inclinés
5 + du GROUPE KA. Le socle (palette fixe, typo, grain, boutons, cartes…)
6 + vient de ka-ui (src/ka/tokens.css, importé AVANT ce fichier) ; ici on
7 + ne définit que l'ACCENT SAFRAN de Resto-Ka + les classes métier.
8 + · Accent Resto-Ka : safran #f08c00 (soft #fdeed7 · deep #b96a00)
12 9 · Baseline : « Chaque resto, chaque plat, chaque prix. »
13 10 · 100 % adaptatif mobile (safe-areas iOS)
14 11 ----------------------------------------------------------------------------- */
15 12 :root {
16 − /* palette officielle Groupe KA (éditorial sharp) */
17 − --paper: #f5f3ee;
18 − --surface: #ffffff;
19 − --surface-2: #faf9f5;
20 − --ink: #141814;
21 − --ink-2: #4d5551;
22 − --ink-3: #8b928c;
23 − --line: rgba(20, 24, 20, 0.14);
24 − --line-strong: rgba(20, 24, 20, 0.85);
25 − --green: #1c5c41;
26 − --green-deep: #123f2e;
27 − --lime: #d9f26b;
28 − --lime-soft: #f0f9d2;
29 − --amber: #e8a33d;
30 − --amber-soft: #fdf3e2;
31 − --danger: #b3423a;
32 − --danger-soft: #fbe9e7;
33 −
34 − /* géométrie sharp : petits rayons, bordures franches */
35 − --r-card: 10px;
36 − --r-ctl: 6px;
37 − --r-pill: 999px;
38 −
39 − /* ombres décalées dures — la signature du groupe */
40 − --shadow-flat: 0 1px 2px rgba(20, 24, 20, 0.05);
41 − --shadow-off: 6px 6px 0 var(--ink);
42 − --shadow-off-soft: 8px 8px 0 rgba(20, 24, 20, 0.08);
43 − --shadow-off-mid: 4px 4px 0 rgba(20, 24, 20, 0.18);
44 −
45 − --font-display: "Space Grotesk", system-ui, sans-serif;
46 − --font-body: "Inter", system-ui, sans-serif;
47 − --font-mono: "JetBrains Mono", ui-monospace, monospace;
48 −}
49 −
50 −* { box-sizing: border-box; }
51 −html { scroll-behavior: smooth; }
52 −body {
53 − margin: 0;
54 − background: var(--paper);
55 − color: var(--ink);
56 − font-family: var(--font-body);
57 − font-size: 15px;
58 − line-height: 1.55;
59 − -webkit-font-smoothing: antialiased;
60 − padding-bottom: env(safe-area-inset-bottom);
61 −}
62 −/* grain de film pleine page (signature Groupe KA) */
63 −body::before {
64 − content: "";
65 − position: fixed; inset: 0; pointer-events: none; opacity: 0.35; z-index: 9999;
66 − 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");
67 −}
68 −#root { position: relative; z-index: 1; }
69 −
70 −h1, h2, h3, h4 { font-family: var(--font-display); font-weight: 700; letter-spacing: -0.03em; margin: 0; }
71 −a { color: inherit; text-decoration: none; }
72 −button { font-family: inherit; }
73 −img { display: block; }
74 −::selection { background: var(--lime); color: var(--ink); }
75 −:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
13 + /* ACCENT du site — seule surcharge de tokens.css (ka-ui) */
14 + --accent: #f08c00;
15 + --accent-soft: #fdeed7;
16 + --accent-deep: #b96a00;
17 + --on-accent: #141814;
76 18
77 −/* surligné lime incliné — la marque des titres du groupe */
78 −h1 .hl, h2 .hl, .fbaseline .hl {
79 − display: inline-block; background: var(--lime); color: var(--ink);
80 − border-radius: 8px; padding: 0 10px 2px; transform: rotate(-1deg);
81 −}
82 −.kicker {
83 − display: inline-flex; align-items: center; gap: 10px;
84 − font-family: var(--font-mono); font-size: 11.5px; font-weight: 500;
85 − text-transform: uppercase; letter-spacing: 0.12em; color: var(--green);
19 + /* alias rétro-compatibles (le code historique utilise --lime) */
20 + --lime: var(--accent);
21 + --lime-soft: var(--accent-soft);
86 22 }
87 −.kicker::before { content: ""; width: 22px; height: 2px; background: var(--green); }
23 +
24 +h1, h2, h3, h4 { font-weight: 700; margin: 0; }
25 +a { text-decoration: none; }
26 +button { font-family: inherit; }
27 +img { display: block; }
88 28
89 29 .container { max-width: 1240px; margin: 0 auto; padding: 0 24px; }
90 30 @media (max-width: 640px) { .container { padding: 0 16px; } }
@@ -105,7 +45,7 @@ h1 .hl, h2 .hl, .fbaseline .hl {
105 45 .brand .logo-ico { margin-right: 11px; flex: none; }
106 46 /* « Ka » en pastille lime inclinée — même geste que la carte membre Groupe KA */
107 47 .brand .ka {
108 − background: var(--lime); color: var(--ink);
48 + background: var(--accent); color: var(--ink);
109 49 padding: 1px 6px 3px; border-radius: 5px; margin-left: 3px;
110 50 display: inline-block; transform: rotate(-2deg);
111 51 }
@@ -122,8 +62,8 @@ h1 .hl, h2 .hl, .fbaseline .hl {
122 62 color: var(--ink); transition: all 0.13s ease; min-height: 38px;
123 63 display: inline-flex; align-items: center;
124 64 }
125 −.nav a:hover { background: var(--lime); border-color: var(--ink); }
126 −.nav a.active { background: var(--ink); color: var(--lime); border-color: var(--ink); }
65 +.nav a:hover { background: var(--accent); border-color: var(--ink); }
66 +.nav a.active { background: var(--ink); color: var(--accent); border-color: var(--ink); }
127 67
128 68 /* ================= Ticker ================= */
129 69 .ticker {
@@ -134,7 +74,7 @@ h1 .hl, h2 .hl, .fbaseline .hl {
134 74 }
135 75 .ticker-track { display: inline-flex; gap: 0; animation: ticker 40s linear infinite; will-change: transform; }
136 76 .ticker span { padding: 0 26px; position: relative; }
137 −.ticker span::after { content: "◆"; position: absolute; right: -6px; color: var(--lime); opacity: 0.9; font-size: 8px; top: 3px; }
77 +.ticker span::after { content: "◆"; position: absolute; right: -6px; color: var(--accent); opacity: 0.9; font-size: 8px; top: 3px; }
138 78 @keyframes ticker { from { transform: translateX(0); } to { transform: translateX(-50%); } }
139 79 @media (prefers-reduced-motion: reduce) {
140 80 .ticker-track { animation: none; }
@@ -159,7 +99,7 @@ h1 .hl, h2 .hl, .fbaseline .hl {
159 99 .stat-chip b { color: var(--ink); font-weight: 700; font-family: var(--font-display); }
160 100 .stat-chip .pulse {
161 101 width: 8px; height: 8px; border-radius: 50%; background: var(--green);
162 − box-shadow: 0 0 0 4px var(--lime-soft); animation: pulse 2.4s ease infinite;
102 + box-shadow: 0 0 0 4px var(--accent-soft); animation: pulse 2.4s ease infinite;
163 103 }
164 104 @keyframes pulse { 50% { box-shadow: 0 0 0 7px rgba(28, 92, 65, 0.18); } }
165 105
@@ -172,7 +112,7 @@ h1 .hl, h2 .hl, .fbaseline .hl {
172 112 .benefit-ico {
173 113 width: 42px; height: 42px; border: 1.5px solid var(--ink); border-radius: var(--r-ctl);
174 114 flex: none; display: flex; align-items: center; justify-content: center;
175 − background: var(--lime); color: var(--ink); box-shadow: 3px 3px 0 rgba(20, 24, 20, 0.15);
115 + background: var(--accent); color: var(--ink); box-shadow: 3px 3px 0 rgba(20, 24, 20, 0.15);
176 116 }
177 117 .benefit-ico.blue { background: var(--surface); color: var(--green); }
178 118 .benefit-label {
@@ -234,12 +174,12 @@ h1 .hl, h2 .hl, .fbaseline .hl {
234 174 .pills { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0 2px; }
235 175 .pill {
236 176 display: inline-flex; align-items: center; gap: 7px;
237 − border: 1.5px solid var(--ink); background: var(--lime); color: var(--ink);
177 + border: 1.5px solid var(--ink); background: var(--accent); color: var(--ink);
238 178 border-radius: var(--r-ctl); padding: 5px 12px; font-size: 12.5px; font-weight: 700;
239 179 font-family: var(--font-display); cursor: pointer;
240 180 box-shadow: 2px 2px 0 rgba(20, 24, 20, 0.2); transition: all 0.12s ease;
241 181 }
242 −.pill:hover { background: var(--ink); color: var(--lime); }
182 +.pill:hover { background: var(--ink); color: var(--accent); }
243 183 .pill-x { font-size: 10px; opacity: 0.65; }
244 184 .pill-clear { background: var(--surface); border-color: var(--danger); color: var(--danger); }
245 185 .pill-clear:hover { background: var(--danger); color: #fff; }
@@ -255,10 +195,10 @@ h1 .hl, h2 .hl, .fbaseline .hl {
255 195 transition: background 0.15s ease, transform 0.1s ease, box-shadow 0.1s ease;
256 196 }
257 197 .btn:active { transform: translate(2px, 2px); box-shadow: none !important; }
258 −.btn-primary { background: var(--ink); color: var(--lime); box-shadow: var(--shadow-off-mid); }
198 +.btn-primary { background: var(--ink); color: var(--accent); box-shadow: var(--shadow-off-mid); }
259 199 .btn-primary:hover { background: var(--green-deep); }
260 200 .btn-ghost { background: transparent; color: var(--ink); }
261 −.btn-ghost:hover { background: var(--lime); }
201 +.btn-ghost:hover { background: var(--accent); }
262 202 .btn:disabled { opacity: 0.4; cursor: default; }
263 203
264 204 /* ================= Chips ================= */
@@ -270,8 +210,8 @@ h1 .hl, h2 .hl, .fbaseline .hl {
270 210 cursor: pointer; font-family: var(--font-display); white-space: nowrap; min-height: 40px;
271 211 box-shadow: 3px 3px 0 rgba(20, 24, 20, 0.1); transition: all 0.13s ease;
272 212 }
273 −.chip:hover { background: var(--lime-soft); transform: translate(-1px, -1px); box-shadow: 4px 4px 0 rgba(20, 24, 20, 0.15); }
274 −.chip.on { background: var(--ink); color: var(--lime); box-shadow: 4px 4px 0 rgba(20, 24, 20, 0.3); }
213 +.chip:hover { background: var(--accent-soft); transform: translate(-1px, -1px); box-shadow: 4px 4px 0 rgba(20, 24, 20, 0.15); }
214 +.chip.on { background: var(--ink); color: var(--accent); box-shadow: 4px 4px 0 rgba(20, 24, 20, 0.3); }
275 215
276 216 /* ================= Results ================= */
277 217 .results-head { display: flex; align-items: baseline; gap: 14px; margin: 28px 0 18px; }
@@ -297,7 +237,7 @@ h1 .hl, h2 .hl, .fbaseline .hl {
297 237 display: flex; align-items: center; justify-content: center; height: 100%;
298 238 color: var(--ink-3);
299 239 background:
300 − radial-gradient(circle at 82% 20%, var(--lime-soft) 0, transparent 46%),
240 + radial-gradient(circle at 82% 20%, var(--accent-soft) 0, transparent 46%),
301 241 radial-gradient(circle at 12% 85%, var(--amber-soft) 0, transparent 42%),
302 242 var(--surface-2);
303 243 }
@@ -310,10 +250,10 @@ h1 .hl, h2 .hl, .fbaseline .hl {
310 250 background: rgba(255, 255, 255, 0.95); color: var(--ink);
311 251 box-shadow: 2px 2px 0 rgba(20, 24, 20, 0.25);
312 252 }
313 −.badge.type { background: var(--ink); color: var(--lime); }
253 +.badge.type { background: var(--ink); color: var(--accent); }
314 254 .badge.right { left: auto; right: 10px; }
315 255 /* badge de contexte de prix — TOUJOURS visible (un prix sans contexte est inutilisable) */
316 −.badge.ctx-takeout, .ctx-pill.ctx-takeout { background: var(--lime); color: var(--ink); }
256 +.badge.ctx-takeout, .ctx-pill.ctx-takeout { background: var(--accent); color: var(--ink); }
317 257 .badge.ctx-dine-in, .ctx-pill.ctx-dine-in { background: var(--green); color: #fff; }
318 258 .badge.ctx-delivery, .ctx-pill.ctx-delivery { background: var(--amber); color: var(--ink); }
319 259 .card-body { padding: 15px 17px; display: flex; flex-direction: column; gap: 6px; flex: 1; }
@@ -327,7 +267,7 @@ h1 .hl, h2 .hl, .fbaseline .hl {
327 267 .source-tag {
328 268 font-family: var(--font-mono); font-size: 9.5px; font-weight: 700;
329 269 text-transform: uppercase; letter-spacing: 0.06em; color: var(--green);
330 − background: var(--lime-soft); border: 1px solid rgba(28, 92, 65, 0.35);
270 + background: var(--accent-soft); border: 1px solid rgba(28, 92, 65, 0.35);
331 271 border-radius: var(--r-ctl); padding: 3px 9px;
332 272 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 60%;
333 273 }
@@ -345,7 +285,7 @@ h1 .hl, h2 .hl, .fbaseline .hl {
345 285 min-height: 46px;
346 286 }
347 287 .mode-toggle button + button { border-left: 1.5px solid var(--ink); }
348 −.mode-toggle button.on { background: var(--ink); color: var(--lime); }
288 +.mode-toggle button.on { background: var(--ink); color: var(--accent); }
349 289
350 290 /* ================= Résultats par plat ================= */
351 291 .dish-list {
@@ -397,8 +337,8 @@ h1 .hl, h2 .hl, .fbaseline .hl {
397 337 box-shadow: 3px 3px 0 rgba(20, 24, 20, 0.1);
398 338 transition: all 0.12s ease;
399 339 }
400 −.pg-btn:hover:not(:disabled) { background: var(--lime-soft); }
401 −.pg-btn.on { background: var(--ink); color: var(--lime); }
340 +.pg-btn:hover:not(:disabled) { background: var(--accent-soft); }
341 +.pg-btn.on { background: var(--ink); color: var(--accent); }
402 342 .pg-btn:disabled { opacity: 0.35; cursor: default; box-shadow: none; }
403 343 .pg-btn:active:not(:disabled) { transform: translate(2px, 2px); box-shadow: none; }
404 344 .pg-ellipsis { color: var(--ink-3); font-family: var(--font-mono); padding: 0 4px; }
@@ -442,7 +382,7 @@ h1 .hl, h2 .hl, .fbaseline .hl {
442 382 .panel .price {
443 383 font-family: var(--font-display); font-size: 30px; font-weight: 700;
444 384 letter-spacing: -0.03em; color: var(--ink);
445 − background: var(--lime); border-radius: 8px; padding: 0 10px 2px;
385 + background: var(--accent); border-radius: 8px; padding: 0 10px 2px;
446 386 display: inline-block; transform: rotate(-1deg);
447 387 }
448 388 .panel h1 { font-size: 23px; margin: 10px 0 2px; }
@@ -455,11 +395,11 @@ h1 .hl, h2 .hl, .fbaseline .hl {
455 395 .klabel { font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); font-weight: 700; margin-bottom: 8px; }
456 396 .amenity-row { display: flex; flex-wrap: wrap; gap: 7px; margin: 0 0 20px; }
457 397 .amenity {
458 − background: var(--lime-soft); color: var(--green); font-size: 12px; font-weight: 600;
398 + background: var(--accent-soft); color: var(--green); font-size: 12px; font-weight: 600;
459 399 border: 1px solid rgba(28, 92, 65, 0.35); border-radius: var(--r-ctl); padding: 4px 11px;
460 400 }
461 401 .cta {
462 − display: block; text-align: center; background: var(--ink); color: var(--lime);
402 + display: block; text-align: center; background: var(--ink); color: var(--accent);
463 403 font-weight: 700; font-family: var(--font-display); border: 1.5px solid var(--ink);
464 404 border-radius: var(--r-ctl); padding: 14px; min-height: 48px;
465 405 box-shadow: var(--shadow-off-mid); transition: all 0.14s ease;
@@ -479,7 +419,7 @@ h1 .hl, h2 .hl, .fbaseline .hl {
479 419 font-family: var(--font-display); font-size: 12.5px; font-weight: 700; color: var(--ink-2);
480 420 }
481 421 .view-toggle button + button { border-left: 1.5px solid var(--ink); }
482 −.view-toggle button.on { background: var(--ink); color: var(--lime); }
422 +.view-toggle button.on { background: var(--ink); color: var(--accent); }
483 423 .ctx-toggle { margin-bottom: 14px; }
484 424
485 425 .menu-block { display: flex; flex-direction: column; gap: 16px; }
@@ -490,7 +430,7 @@ h1 .hl, h2 .hl, .fbaseline .hl {
490 430 }
491 431 .ctx-banner b { font-family: var(--font-display); font-size: 14px; }
492 432 .ctx-banner span { font-family: var(--font-mono); font-size: 11px; color: var(--ink-2); }
493 −.ctx-banner.ctx-takeout { background: var(--lime-soft); }
433 +.ctx-banner.ctx-takeout { background: var(--accent-soft); }
494 434 .ctx-banner.ctx-takeout b { color: var(--green); }
495 435 .ctx-banner.ctx-dine-in { background: var(--surface); }
496 436 .ctx-banner.ctx-dine-in b { color: var(--green-deep); }
@@ -507,7 +447,7 @@ h1 .hl, h2 .hl, .fbaseline .hl {
507 447 padding: 14px 18px; text-align: left;
508 448 }
509 449 .menu-section-head h3 { font-size: 17px; color: var(--ink); letter-spacing: -0.02em; flex: 1; }
510 −.menu-section-head:hover { background: var(--lime-soft); }
450 +.menu-section-head:hover { background: var(--accent-soft); }
511 451 .ms-img {
512 452 width: 36px; height: 36px; object-fit: cover; border-radius: var(--r-ctl);
513 453 border: 1.5px solid var(--ink); flex: none;
@@ -531,7 +471,7 @@ h1 .hl, h2 .hl, .fbaseline .hl {
531 471 font-style: normal; font-family: var(--font-mono); font-size: 9px; font-weight: 700;
532 472 text-transform: uppercase; letter-spacing: 0.06em; border-radius: 4px;
533 473 padding: 2px 7px; margin-left: 7px;
534 − background: var(--lime-soft); color: var(--green); border: 1px solid rgba(28, 92, 65, 0.3);
474 + background: var(--accent-soft); color: var(--green); border: 1px solid rgba(28, 92, 65, 0.3);
535 475 vertical-align: 2px;
536 476 }
537 477 .mi-tag-epice { background: var(--amber-soft); color: #9a6414; border-color: rgba(232, 163, 61, 0.5); }
@@ -553,7 +493,7 @@ h1 .hl, h2 .hl, .fbaseline .hl {
553 493 text-transform: uppercase; letter-spacing: 0.05em; border-radius: 4px; padding: 2px 7px;
554 494 background: var(--surface); color: var(--ink-3); border: 1px solid var(--line);
555 495 }
556 −.og-req.on { background: var(--lime); color: var(--ink); border-color: var(--ink); }
496 +.og-req.on { background: var(--accent); color: var(--ink); border-color: var(--ink); }
557 497 .og-choices { display: flex; flex-wrap: wrap; gap: 6px; }
558 498 .og-choice {
559 499 display: inline-flex; align-items: center; gap: 6px;
@@ -578,7 +518,7 @@ h1 .hl, h2 .hl, .fbaseline .hl {
578 518 }
579 519 .src-table td { padding: 13px 18px; border-bottom: 1px solid var(--line); font-size: 14px; vertical-align: top; }
580 520 .src-table tr:last-child td { border-bottom: none; }
581 −.src-table tr:hover td { background: var(--lime-soft); }
521 +.src-table tr:hover td { background: var(--accent-soft); }
582 522 .src-table a { color: var(--green); font-weight: 600; border-bottom: 1.5px solid rgba(28, 92, 65, 0.4); }
583 523 .src-integrations { font-size: 11.5px; color: var(--ink-3); margin-top: 4px; max-width: 380px; }
584 524 .src-table .pill, .pill.ok, .pill.todo {
@@ -587,7 +527,7 @@ h1 .hl, h2 .hl, .fbaseline .hl {
587 527 text-transform: uppercase; letter-spacing: 0.05em; border: 1px solid var(--ink);
588 528 box-shadow: none; cursor: default; transform: none;
589 529 }
590 −.pill.ok { background: var(--lime); color: var(--ink); }
530 +.pill.ok { background: var(--accent); color: var(--ink); }
591 531 .pill.todo { background: var(--amber-soft); color: #9a6414; border-color: rgba(232, 163, 61, 0.6); }
592 532 .count-pill { font-weight: 700; font-family: var(--font-display); font-size: 16px; color: var(--ink); }
593 533
@@ -603,7 +543,7 @@ h1 .hl, h2 .hl, .fbaseline .hl {
603 543 .tile-v { font-family: var(--font-display); font-weight: 700; font-size: clamp(24px, 3vw, 34px); letter-spacing: -0.03em; line-height: 1.05; color: var(--ink); }
604 544 .tile-k { font-family: var(--font-mono); font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3); margin-top: 6px; }
605 545 .hero-tile { background: var(--ink); color: var(--paper); border-color: var(--ink); box-shadow: 6px 6px 0 rgba(20, 24, 20, 0.25); }
606 −.hero-tile .tile-v { color: var(--lime); }
546 +.hero-tile .tile-v { color: var(--accent); }
607 547 .hero-tile .tile-k { color: rgba(245, 243, 238, 0.6); }
608 548
609 549 .viz-card {
@@ -628,7 +568,7 @@ h1 .hl, h2 .hl, .fbaseline .hl {
628 568 display: block; height: 100%; background: var(--green); border-radius: 0;
629 569 min-width: 2px; transition: background 0.12s ease;
630 570 }
631 −.hbar-row:hover .hbar-fill { background: var(--lime); }
571 +.hbar-row:hover .hbar-fill { background: var(--accent); }
632 572 .hbar-value { font-family: var(--font-mono); font-size: 11px; font-weight: 700; white-space: nowrap; color: var(--ink); }
633 573 @media (max-width: 640px) {
634 574 .hbar-row { grid-template-columns: minmax(80px, 120px) 1fr auto; gap: 8px; }
@@ -689,7 +629,7 @@ h1 .hl, h2 .hl, .fbaseline .hl {
689 629 display: flex; align-items: center; gap: 6px;
690 630 position: fixed; left: 50%; transform: translateX(-50%);
691 631 bottom: calc(74px + env(safe-area-inset-bottom)); z-index: 80;
692 − background: var(--ink); color: var(--lime); border: 1.5px solid var(--ink);
632 + background: var(--ink); color: var(--accent); border: 1.5px solid var(--ink);
693 633 border-radius: var(--r-ctl); padding: 13px 24px; font-family: var(--font-display);
694 634 font-weight: 700; font-size: 15px; cursor: pointer;
695 635 box-shadow: 5px 5px 0 rgba(20, 24, 20, 0.3);
@@ -728,7 +668,7 @@ h1 .hl, h2 .hl, .fbaseline .hl {
728 668 .tabbar a.active { color: var(--green); }
729 669 .tabbar a svg { display: block; }
730 670 body { padding-bottom: calc(64px + env(safe-area-inset-bottom)); }
731 − .footer { padding-bottom: calc(72px + env(safe-area-inset-bottom)); }
671 + .ka-footer { padding-bottom: calc(72px + env(safe-area-inset-bottom)); }
732 672 }
733 673
734 674 /* Anti-zoom iOS : les champs doivent faire >= 16px */
@@ -736,23 +676,25 @@ h1 .hl, h2 .hl, .fbaseline .hl {
736 676 .f-search input, .f-ctl select { font-size: 16px; }
737 677 }
738 678
739 −/* ================= Footer ================= */
740 −.footer {
741 − background: var(--ink); color: rgba(245, 243, 238, 0.72); margin-top: 20px;
742 − padding: 44px 0 max(40px, env(safe-area-inset-bottom)); font-size: 13px;
743 − border-top: 2px solid var(--ink);
679 +/* ================= Footer (KaFooter — ka-ui) ================= */
680 +/* Le footer commun vient de tokens.css (.ka-footer) + KaFooter.tsx. On garde
681 + ici seulement la rangée de nav locale posée au-dessus. */
682 +.prefoot { border-top: 2px solid var(--ink); padding: 22px 0; margin-top: 40px; background: var(--surface-2); }
683 +.ka-footer { margin-top: 0; }
684 +.prefoot-inner { display: flex; flex-wrap: wrap; gap: 10px 22px; align-items: center; }
685 +.prefoot-nav { display: flex; flex-wrap: wrap; gap: 4px 8px; }
686 +.prefoot-nav a {
687 + display: inline-flex; align-items: center; min-height: var(--touch);
688 + padding: 8px 12px; border-radius: var(--r-ctl);
689 + font-family: var(--font-display); font-weight: 700; font-size: 13.5px; color: var(--ink);
744 690 }
745 −.footer .fbrand { font-family: var(--font-display); font-weight: 700; font-size: 30px; color: var(--paper); letter-spacing: -0.04em; margin-bottom: 6px; display: flex; align-items: center; }
746 −.footer .fbrand .logo-ico { margin-right: 13px; }
747 −.footer .fbrand .ka {
748 − background: var(--lime); color: var(--ink);
749 − padding: 1px 7px 3px; border-radius: 5px; margin-left: 4px;
750 − display: inline-block; transform: rotate(-2deg);
691 +.prefoot-nav a:hover { background: var(--accent); color: var(--on-accent); }
692 +.prefoot-osm {
693 + margin-left: auto; font-family: var(--font-mono); font-size: 10.5px;
694 + letter-spacing: 0.03em; color: var(--ink-3);
751 695 }
752 −.footer .fbaseline { color: rgba(245, 243, 238, 0.65); font-size: 14px; margin-bottom: 16px; }
753 −.footer b { color: var(--paper); }
754 −.footer .frow { display: flex; flex-direction: column; gap: 5px; max-width: 720px; }
755 −.footer .fmono { font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.04em; color: rgba(245, 243, 238, 0.42); margin-top: 18px; }
696 +.prefoot-osm a { color: var(--green); border-bottom: 1px solid rgba(28, 92, 65, 0.4); }
697 +@media (max-width: 640px) { .prefoot-osm { margin-left: 0; } }
756 698
757 699 /* --- Compte membre (« Se connecter avec KA ») ------------------------------- */
758 700 .login-btn {
@@ -764,11 +706,11 @@ h1 .hl, h2 .hl, .fbaseline .hl {
764 706 box-shadow: 3px 3px 0 rgba(20, 24, 20, 0.12);
765 707 transition: background 0.13s ease, transform 0.1s ease, box-shadow 0.1s ease;
766 708 }
767 −.login-btn:hover { background: var(--lime-soft); }
709 +.login-btn:hover { background: var(--accent-soft); }
768 710 .login-btn:active { transform: translate(2px, 2px); box-shadow: none; }
769 711 .ka-badge {
770 712 display: inline-flex; align-items: center; justify-content: center;
771 − background: var(--lime); color: var(--ink); border: 1px solid var(--ink);
713 + background: var(--accent); color: var(--ink); border: 1px solid var(--ink);
772 714 padding: 1px 6px 2px; border-radius: 4px;
773 715 font-family: var(--font-display); font-weight: 700; font-size: 11px;
774 716 transform: rotate(-2deg);
@@ -776,7 +718,7 @@ h1 .hl, h2 .hl, .fbaseline .hl {
776 718 .account { position: relative; }
777 719 .account-btn {
778 720 width: 40px; height: 40px; border-radius: 50%; overflow: hidden;
779 − border: 1.5px solid var(--ink); background: var(--lime);
721 + border: 1.5px solid var(--ink); background: var(--accent);
780 722 cursor: pointer; padding: 0;
781 723 box-shadow: 3px 3px 0 rgba(20, 24, 20, 0.15);
782 724 }
@@ -805,7 +747,7 @@ h1 .hl, h2 .hl, .fbaseline .hl {
805 747 padding: 10px; border-radius: var(--r-ctl);
806 748 font-family: var(--font-display); font-weight: 700; font-size: 13.5px; color: var(--ink);
807 749 }
808 −.account-link:hover, .account-menu button:hover { background: var(--lime-soft); }
750 +.account-link:hover, .account-menu button:hover { background: var(--accent-soft); }
809 751
810 752 /* cœur favori sur les cartes */
811 753 .fav-btn {
@@ -817,7 +759,7 @@ h1 .hl, h2 .hl, .fbaseline .hl {
817 759 transition: transform 0.12s ease, background 0.12s ease;
818 760 }
819 761 .fav-btn:hover { transform: scale(1.08); }
820 −.fav-btn.on { background: var(--lime); color: var(--ink); }
762 +.fav-btn.on { background: var(--accent); color: var(--ink); }
821 763
822 764 /* --- Menu déroulant mobile ------------------------------------------------- */
823 765 .menu-btn {
@@ -861,7 +803,7 @@ h1 .hl, h2 .hl, .fbaseline .hl {
861 803 .mm-link.active .mm-arrow { opacity: 1; color: var(--green); }
862 804 .mm-ico { width: 26px; text-align: center; font-size: 17px; }
863 805 .mm-arrow { margin-left: auto; opacity: 0.25; color: var(--green); transition: opacity 0.15s ease; }
864 −.mm-link:active { background: var(--lime-soft); border-radius: var(--r-ctl); }
806 +.mm-link:active { background: var(--accent-soft); border-radius: var(--r-ctl); }
865 807 .mm-foot {
866 808 padding: 12px 10px 4px;
867 809 font-family: var(--font-mono); font-size: 10px; color: var(--ink-3); letter-spacing: 0.02em;
@@ -875,3 +817,47 @@ h1 .hl, h2 .hl, .fbaseline .hl {
875 817 .nav { display: none; }
876 818 .mobile-menu { display: block; }
877 819 }
820 +
821 +/* ================= Badge « Un service Groupe KA » (ka-ui) ================= */
822 +.hdr-badge { display: none; }
823 +@media (min-width: 900px) { .hdr-badge { display: inline-flex; } }
824 +.mm-badge { padding: 14px 10px 2px; }
825 +
826 +/* ================= Page Contact ================= */
827 +.contact-page { padding: 44px 0 90px; }
828 +.contact-page h1 { font-size: clamp(30px, 4.6vw, 48px); margin: 12px 0 6px; color: var(--ink); }
829 +.contact-page .sub { color: var(--ink-2); max-width: 640px; margin: 0 0 30px; }
830 +.contact-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 16px; margin-bottom: 26px; }
831 +.contact-card {
832 + background: var(--surface); border: 1.5px solid var(--ink); border-radius: var(--r-card);
833 + box-shadow: var(--shadow-off-soft); padding: 20px;
834 + display: flex; flex-direction: column; gap: 6px;
835 +}
836 +.contact-card .role {
837 + font-family: var(--font-mono); font-size: 10px; font-weight: 700;
838 + text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-3);
839 +}
840 +.contact-card a.mail {
841 + display: inline-flex; align-items: center; min-height: var(--touch);
842 + font-family: var(--font-display); font-weight: 700; font-size: 16.5px;
843 + letter-spacing: -0.01em; color: var(--ink); word-break: break-all;
844 +}
845 +.contact-card a.mail:hover { color: var(--accent-deep); text-decoration: underline; text-underline-offset: 4px; }
846 +.contact-note {
847 + border: 1.5px solid var(--ink); border-left: 6px solid var(--accent);
848 + border-radius: var(--r-card); background: var(--accent-soft);
849 + padding: 16px 18px; max-width: 760px; margin-bottom: 26px; font-size: 14px;
850 + box-shadow: 4px 4px 0 rgba(20, 24, 20, 0.1);
851 +}
852 +.contact-hub { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; margin-bottom: 38px; }
853 +.contact-hub .hint { font-size: 12.5px; color: var(--ink-2); max-width: 460px; }
854 +.contact-sites { list-style: none; margin: 14px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }
855 +.contact-sites a {
856 + display: inline-flex; align-items: center; gap: 7px; min-height: var(--touch);
857 + border: 1.5px solid var(--ink); border-radius: var(--r-ctl); background: var(--surface);
858 + padding: 8px 14px; font-family: var(--font-display); font-weight: 700; font-size: 13.5px;
859 + color: var(--ink); box-shadow: 3px 3px 0 rgba(20, 24, 20, 0.1);
860 + transition: all 0.13s ease;
861 +}
862 +.contact-sites a:hover { background: var(--accent); color: var(--on-accent); transform: translate(-1px, -1px); box-shadow: 4px 4px 0 rgba(20, 24, 20, 0.2); }
863 +.contact-sites .dot { width: 9px; height: 9px; border-radius: 50%; border: 1px solid rgba(20, 24, 20, 0.35); flex: none; }
modified restoka/web.py +1 −1
@@ -409,7 +409,7 @@ if FRONTEND_DIST.exists():
409 409 resp.headers["Cache-Control"] = "no-cache"
410 410 return resp
411 411
412 − _CLIENT_PREFIXES = ("resto/", "sources", "stats", "favoris")
412 + _CLIENT_PREFIXES = ("resto/", "sources", "stats", "favoris", "contact")
413 413
414 414 @app.get("/{full_path:path}")
415 415 def spa(full_path: str):
416 416