SPB Git forge

spb/groupe-ka

Public

Groupe KA — site du holding + KA ID (compte unique & SSO des 7 plateformes). Next.js 16, SQLite, Google & Apple login.

81commits 1branches 0releases
89.2 MBsize
maindefault branch
22 days agolast push
TypeScript 70.4% HTML 18.4% JavaScript 4% Python 3.8% CSS 3.4%

feat: PDF « L'agrégation, expliquée » — 12 pages de schémas, téléchargeable sur le site

- Document A4 paysage aux couleurs du groupe : couverture, définition ·Ka,
  sablier à 10 plateformes, pipeline, cycle, éclaireurs Ka2/Ka4/Ka6, Trouve·Ka,
  inventaire vivant, flotte de 340 connecteurs, moteurs d'évaluation, socle, manifeste
- Source HTML autonome (figures générées en JS, rendu chromium --print-to-pdf)
- Servi sur /documents/groupe-ka-agregation.pdf + bouton dans la section #flux

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

4 changed files +1,020 −0

added assets/poster/groupe-ka-agregation.html +1007 −0
@@ -0,0 +1,1007 @@
1 +<!DOCTYPE html>
2 +<!-- Auteur : Simon-Pierre Boucher — contact@spboucher.ai -->
3 +<!-- Document « L'agrégation, expliquée » — 12 pages A4 paysage.
4 + Rendu PDF : chromium --headless --print-to-pdf (les figures sont générées en JS). -->
5 +<html lang="fr">
6 +<head>
7 +<meta charset="UTF-8" />
8 +<title>Groupe KA — L'agrégation, expliquée</title>
9 +<link rel="preconnect" href="https://fonts.googleapis.com" />
10 +<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
11 +<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;700&family=Inter:wght@400;600&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet" />
12 +<style>
13 + :root {
14 + --paper: #f5f3ee;
15 + --surface: #ffffff;
16 + --ink: #141814;
17 + --ink-2: #4d5551;
18 + --ink-3: #8b928c;
19 + --green: #1c5c41;
20 + --green-deep: #123f2e;
21 + --lime: #d9f26b;
22 + --lime-soft: #f0f9d2;
23 + --display: "Space Grotesk", system-ui, sans-serif;
24 + --body: "Inter", system-ui, sans-serif;
25 + --mono: "JetBrains Mono", ui-monospace, monospace;
26 + }
27 + * { margin: 0; padding: 0; box-sizing: border-box; }
28 + @page { size: 297mm 210mm; margin: 0; }
29 + html, body { background: #1a1e1a; }
30 + body { font-family: var(--body); color: var(--ink); }
31 +
32 + .page {
33 + width: 297mm;
34 + height: 210mm;
35 + background: var(--paper);
36 + position: relative;
37 + overflow: hidden;
38 + page-break-after: always;
39 + padding: 12mm 14mm 11mm;
40 + display: flex;
41 + flex-direction: column;
42 + }
43 + .page::after {
44 + content: "";
45 + position: absolute; inset: 0;
46 + pointer-events: none;
47 + opacity: 0.05;
48 + background-image: radial-gradient(var(--ink) 0.6px, transparent 0.6px);
49 + background-size: 5px 5px;
50 + }
51 + .page.dark { background: var(--ink); color: var(--paper); }
52 + .page.dark::after { background-image: radial-gradient(var(--lime) 0.6px, transparent 0.6px); opacity: 0.04; }
53 +
54 + .mono { font-family: var(--mono); }
55 + .display { font-family: var(--display); }
56 +
57 + /* ---------- En-tête / pied de page communs ---------- */
58 + .phead {
59 + display: flex;
60 + align-items: baseline;
61 + justify-content: space-between;
62 + border-bottom: 2px solid var(--ink);
63 + padding-bottom: 4mm;
64 + }
65 + .dark .phead { border-bottom-color: rgba(245,243,238,0.25); }
66 + .wordmark {
67 + font-family: var(--display);
68 + font-weight: 700;
69 + font-size: 17px;
70 + letter-spacing: -0.04em;
71 + display: inline-flex;
72 + align-items: baseline;
73 + gap: 5px;
74 + }
75 + .kabox {
76 + display: inline-block;
77 + background: var(--ink);
78 + color: var(--lime);
79 + border-radius: 5px;
80 + padding: 0 6px 2px;
81 + transform: rotate(-2deg);
82 + }
83 + .dark .kabox { background: var(--lime); color: var(--ink); }
84 + .phead .doc {
85 + font-family: var(--mono);
86 + font-weight: 700;
87 + font-size: 9px;
88 + letter-spacing: 0.16em;
89 + text-transform: uppercase;
90 + color: var(--ink-3);
91 + }
92 + .dark .phead .doc { color: rgba(245,243,238,0.55); }
93 + .pnum { font-family: var(--mono); font-weight: 700; font-size: 10px; color: var(--green); }
94 + .dark .pnum { color: var(--lime); }
95 +
96 + .pfoot {
97 + margin-top: auto;
98 + border-top: 1.5px dashed rgba(20,24,20,0.3);
99 + padding-top: 3mm;
100 + display: flex;
101 + justify-content: space-between;
102 + font-family: var(--mono);
103 + font-size: 8.5px;
104 + letter-spacing: 0.1em;
105 + text-transform: uppercase;
106 + color: var(--ink-3);
107 + }
108 + .dark .pfoot { border-top-color: rgba(245,243,238,0.2); color: rgba(245,243,238,0.5); }
109 +
110 + /* ---------- Typo de section ---------- */
111 + .kicker {
112 + font-family: var(--mono);
113 + font-weight: 700;
114 + font-size: 10px;
115 + letter-spacing: 0.18em;
116 + text-transform: uppercase;
117 + color: var(--green);
118 + display: inline-flex;
119 + align-items: center;
120 + gap: 8px;
121 + margin-top: 6mm;
122 + }
123 + .kicker::before { content: ""; width: 22px; height: 2px; background: var(--green); }
124 + .dark .kicker { color: var(--lime); }
125 + .dark .kicker::before { background: var(--lime); }
126 + h2 {
127 + font-family: var(--display);
128 + font-weight: 700;
129 + font-size: 34px;
130 + line-height: 1;
131 + letter-spacing: -0.03em;
132 + text-transform: uppercase;
133 + margin-top: 3.5mm;
134 + }
135 + .hl {
136 + display: inline-block;
137 + background: var(--lime);
138 + color: var(--ink);
139 + border-radius: 6px;
140 + padding: 0 8px 2px;
141 + transform: rotate(-1deg);
142 + }
143 + .outline { color: transparent; -webkit-text-stroke: 2px var(--ink); }
144 + .dark .outline { -webkit-text-stroke: 2px var(--paper); }
145 + .lede { margin-top: 3.5mm; font-size: 12px; line-height: 1.55; color: var(--ink-2); max-width: 150mm; }
146 + .dark .lede { color: rgba(245,243,238,0.75); }
147 +
148 + /* ---------- Cartes & pilules ---------- */
149 + .card {
150 + background: var(--surface);
151 + border: 1.5px solid var(--ink);
152 + border-radius: 9px;
153 + box-shadow: 6px 6px 0 rgba(20,24,20,0.12);
154 + }
155 + .chip {
156 + display: inline-flex;
157 + align-items: center;
158 + gap: 6px;
159 + padding: 5px 11px;
160 + background: var(--surface);
161 + border: 1.5px solid var(--ink);
162 + border-radius: 999px;
163 + font-family: var(--mono);
164 + font-size: 9.5px;
165 + font-weight: 700;
166 + box-shadow: 3px 3px 0 rgba(20,24,20,0.12);
167 + white-space: nowrap;
168 + }
169 + .caption {
170 + border-top: 1.5px solid var(--ink);
171 + background: #faf9f5;
172 + padding: 3mm 5mm;
173 + font-size: 10.5px;
174 + color: var(--ink-2);
175 + border-radius: 0 0 8px 8px;
176 + }
177 + .caption b { font-family: var(--mono); color: var(--green); }
178 +
179 + /* ---------- Page 1 : couverture ---------- */
180 + .cover h1 {
181 + font-family: var(--display);
182 + font-weight: 700;
183 + font-size: 96px;
184 + line-height: 0.95;
185 + letter-spacing: -0.04em;
186 + text-transform: uppercase;
187 + margin-top: 8mm;
188 + }
189 + .cover .sub { margin-top: 7mm; font-size: 14.5px; color: var(--ink-2); max-width: 165mm; line-height: 1.6; }
190 + .cover-chips { display: flex; gap: 8px; margin-top: 8mm; flex-wrap: wrap; }
191 + .cover-marks { display: flex; gap: 6px; margin-top: auto; flex-wrap: wrap; padding-bottom: 2mm; }
192 + .markpill {
193 + font-family: var(--display); font-weight: 700; font-size: 12.5px;
194 + border: 1.5px solid var(--ink); border-radius: 999px; padding: 4px 12px 5px;
195 + background: var(--surface); box-shadow: 3px 3px 0 rgba(20,24,20,0.15);
196 + }
197 + .markpill i { font-style: normal; border-radius: 4px; padding: 0 4px 1px; margin-left: 2px; display: inline-block; transform: rotate(-2deg); }
198 +
199 + /* ---------- Page 2 : définition ---------- */
200 + .def-grid { display: grid; grid-template-columns: 1.15fr 1fr; gap: 8mm; margin-top: 7mm; align-items: stretch; }
201 + .defcard { padding: 8mm 9mm; }
202 + .defcard .word { font-family: var(--display); font-weight: 700; font-size: 52px; letter-spacing: -0.03em; }
203 + .defcard .pos { font-family: var(--mono); font-size: 12px; color: var(--ink-3); margin-left: 8px; }
204 + .defcard ol { margin-top: 6mm; list-style: none; }
205 + .defcard li { display: flex; gap: 10px; font-size: 12.5px; line-height: 1.55; color: var(--ink-2); margin-top: 4mm; }
206 + .defcard li b.n { font-family: var(--mono); color: var(--green); }
207 + .defcard .quote { margin-top: 6mm; border-top: 1.5px dashed rgba(20,24,20,0.3); padding-top: 5mm; font-style: italic; font-size: 12.5px; color: var(--ink-2); }
208 + .eqcard { background: var(--ink); color: var(--paper); border-radius: 9px; border: 1.5px solid var(--ink); padding: 8mm 9mm; box-shadow: 6px 6px 0 rgba(20,24,20,0.25); display: flex; flex-direction: column; }
209 + .eqcard .k { font-family: var(--mono); font-weight: 700; font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--lime); }
210 + .eqcard .eq { font-family: var(--display); font-weight: 700; font-size: 33px; line-height: 1.12; letter-spacing: -0.03em; text-transform: uppercase; margin-top: 5mm; }
211 + .eqcard .eq .lime { color: var(--lime); }
212 + .eqcard p { margin-top: 5mm; font-size: 11.5px; line-height: 1.6; color: rgba(245,243,238,0.75); }
213 +
214 + /* ---------- Figures pleine page ---------- */
215 + .figwrap { margin-top: 5mm; border: 1.5px solid var(--ink); border-radius: 9px; background: var(--surface); box-shadow: 6px 6px 0 rgba(20,24,20,0.12); overflow: hidden; flex: 1; display: flex; flex-direction: column; }
216 + .figwrap svg { display: block; width: 100%; flex: 1; }
217 + .figbody { flex: 1; display: flex; align-items: center; justify-content: center; padding: 4mm 6mm; }
218 +
219 + /* ---------- Pages barres & matrice ---------- */
220 + .bars { padding: 5.5mm 9mm 4mm; }
221 + .barrow { margin-top: 3.8mm; }
222 + .barrow:first-child { margin-top: 0; }
223 + .barhead { display: flex; justify-content: space-between; align-items: baseline; }
224 + .barhead .nm { font-family: var(--display); font-weight: 700; font-size: 13px; display: inline-flex; align-items: center; gap: 7px; }
225 + .brand { width: 13px; height: 13px; border-radius: 3.5px; background: var(--ink); display: inline-flex; align-items: center; justify-content: center; }
226 + .brand i { width: 8px; height: 8px; border-radius: 2px; display: block; }
227 + .barhead .u { font-family: var(--mono); font-size: 9px; color: var(--ink-3); font-weight: 400; margin-left: 4px; }
228 + .barhead .v { font-family: var(--mono); font-weight: 700; font-size: 11.5px; }
229 + .bartrack { margin-top: 1.5mm; height: 4.4mm; background: rgba(20,24,20,0.06); border-radius: 0 3px 3px 0; }
230 + .barfill { height: 100%; background: var(--green); border-radius: 0 3px 3px 0; }
231 +
232 + .dots-block { margin-top: 4.2mm; }
233 + .dots-block:first-child { margin-top: 0; }
234 + .dots { display: flex; flex-wrap: wrap; gap: 2.1px; margin-top: 1.6mm; max-width: 235mm; }
235 + .dots i { width: 6.5px; height: 6.5px; border-radius: 2px; background: var(--green); display: block; }
236 +
237 + /* ---------- Page moteurs d'évaluation ---------- */
238 + .eval-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8mm; margin-top: 6mm; flex: 1; }
239 + .evalcard { padding: 8mm 9mm; display: flex; flex-direction: column; }
240 + .evalcard .k { font-family: var(--mono); font-weight: 700; font-size: 9.5px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink-3); display: flex; align-items: center; gap: 7px; }
241 + .evalcard .big { font-family: var(--display); font-weight: 700; font-size: 46px; letter-spacing: -0.03em; margin-top: 4mm; }
242 + .evalcard .lbl { font-family: var(--display); font-weight: 700; font-size: 13px; text-transform: uppercase; color: var(--green); margin-top: 1mm; }
243 + .evalcard p { font-size: 11px; color: var(--ink-2); margin-top: 3mm; line-height: 1.55; }
244 + .gauge { margin-top: auto; padding-top: 6mm; }
245 + .gauge .glab { display: flex; justify-content: space-between; font-family: var(--mono); font-size: 8.5px; color: var(--ink-3); margin-bottom: 1.4mm; }
246 + .gtrack { position: relative; height: 7mm; border: 1.5px solid var(--ink); border-radius: 5px; background: var(--surface); overflow: hidden; }
247 + .gband { position: absolute; top: 0; bottom: 0; background: var(--lime); opacity: 0.85; }
248 + .gmid { position: absolute; top: -1mm; bottom: -1mm; width: 2.2px; background: var(--ink); }
249 +
250 + /* ---------- Page socle : tableau ---------- */
251 + .tbl { margin-top: 6mm; border: 1.5px solid var(--ink); border-radius: 9px; overflow: hidden; background: var(--surface); box-shadow: 6px 6px 0 rgba(20,24,20,0.12); }
252 + table { width: 100%; border-collapse: collapse; font-size: 11px; }
253 + th { font-family: var(--mono); font-size: 8.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; text-align: left; color: var(--ink-3); background: #faf9f5; padding: 2.6mm 4mm; border-bottom: 1.5px solid var(--ink); }
254 + td { padding: 2.45mm 4mm; border-bottom: 1px solid rgba(20,24,20,0.14); vertical-align: middle; }
255 + tr:last-child td { border-bottom: none; }
256 + td.nm { font-family: var(--display); font-weight: 700; white-space: nowrap; }
257 + td.url { font-family: var(--mono); font-size: 9.5px; white-space: nowrap; color: var(--green); }
258 + td .brand { vertical-align: -2px; margin-right: 6px; }
259 +
260 + /* ---------- Page manifeste (fin, sombre) ---------- */
261 + .mani-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 7mm; margin-top: 8mm; }
262 + .mani { border: 1.5px solid rgba(245,243,238,0.3); border-radius: 9px; padding: 7mm; }
263 + .mani .n { font-family: var(--mono); font-weight: 700; font-size: 11px; color: var(--lime); }
264 + .mani h3 { font-family: var(--display); font-weight: 700; font-size: 16.5px; text-transform: uppercase; margin-top: 3mm; }
265 + .mani p { font-size: 11px; line-height: 1.6; color: rgba(245,243,238,0.75); margin-top: 3mm; }
266 + .contact { margin-top: 10mm; display: flex; gap: 6mm; align-items: baseline; flex-wrap: wrap; }
267 + .contact .mail { font-family: var(--mono); font-weight: 700; font-size: 13px; color: var(--lime); }
268 + .contact .role { font-family: var(--mono); font-size: 9px; color: rgba(245,243,238,0.5); }
269 +
270 + /* ---------- Légendes SVG en HTML ---------- */
271 + svg text { font-family: var(--mono); }
272 + svg .t-display { font-family: var(--display); }
273 + svg .t-body { font-family: var(--body); }
274 +</style>
275 +</head>
276 +<body>
277 +
278 +<!-- ============================ PAGE 1 — COUVERTURE ============================ -->
279 +<section class="page cover">
280 + <header class="phead">
281 + <span class="wordmark">Groupe <span class="kabox">KA</span></span>
282 + <span class="doc">Document de référence · 2026</span>
283 + <span class="pnum">01 / 12</span>
284 + </header>
285 + <p class="kicker">Holding d'agrégateurs automatisés · Québec</p>
286 + <h1>L'agrégation,<br /><span class="outline">expliquée</span> <span class="hl">·Ka</span></h1>
287 + <p class="sub">
288 + Comment mille sites deviennent un seul endroit : les connecteurs, le pipeline, le cycle,
289 + les éclaireurs, le moteur de recherche et les dix plateformes du Groupe KA — chaque
290 + mécanisme dessiné, chaque chiffre compté. Zéro saisie manuelle, zéro boîte noire.
291 + </p>
292 + <div class="cover-chips">
293 + <span class="chip">10 agrégateurs en ligne</span>
294 + <span class="chip">1 moteur de recherche québécois</span>
295 + <span class="chip">450+ connecteurs sources</span>
296 + <span class="chip">22 M+ d'observations immobilières</span>
297 + <span class="chip">100 % données à la source</span>
298 + </div>
299 + <div class="cover-marks" id="cover-marks"></div>
300 + <footer class="pfoot"><span>groupe-ka.com</span><span>© 2026 Groupe KA — Simon-Pierre Boucher</span></footer>
301 +</section>
302 +
303 +<!-- ============================ PAGE 2 — LE NOM ============================ -->
304 +<section class="page">
305 + <header class="phead">
306 + <span class="wordmark">Groupe <span class="kabox">KA</span></span>
307 + <span class="doc">L'agrégation, expliquée</span>
308 + <span class="pnum">02 / 12</span>
309 + </header>
310 + <p class="kicker">Chapitre 01 · Le nom</p>
311 + <h2>·Ka, le suffixe qui veut dire <span class="hl">agréger</span></h2>
312 + <div class="def-grid">
313 + <div class="card defcard">
314 + <span class="word">·Ka</span><span class="pos">/ka/ suffixe</span>
315 + <ol>
316 + <li><b class="n">1.</b><span><b>Agréger</b> — rassembler en un seul endroit ce qui était éparpillé
317 + sur des centaines de sites : annonces, prix, menus, événements, profils.</span></li>
318 + <li><b class="n">2.</b><span>Marque de fabrique du groupe : Lou·Ka loge, Immo·Ka vend, Auto·Ka roule,
319 + Fabri·Ka fabrique, Food·Ka nourrit, Resto·Ka régale, Sorti·Ka divertit, Créa·Ka rassemble.</span></li>
320 + <li><b class="n">3.</b><span>Par extension : toute machine qui lit les sources en continu, normalise la
321 + donnée et publie sans jamais rien inventer.</span></li>
322 + </ol>
323 + <p class="quote">« Tout ce qui finit en <b class="display">·Ka</b> agrège. »</p>
324 + </div>
325 + <div class="eqcard">
326 + <span class="k">L'équation ·Ka</span>
327 + <p class="eq">1 000+ sites lus<br /><span class="lime">→ 450+ connecteurs</span><br />→ 10 plateformes</p>
328 + <p>La donnée entre éparpillée, elle ressort rangée. Entre les deux, aucune main humaine —
329 + c'est le suffixe ·Ka qui travaille : lecture à la source, normalisation, dédoublonnage,
330 + géocodage, publication traçable. Les pages qui suivent montrent la machine, à découvert.</p>
331 + </div>
332 + </div>
333 + <footer class="pfoot"><span>groupe-ka.com</span><span>Chapitre 01 · Le nom</span></footer>
334 +</section>
335 +
336 +<!-- ============================ PAGE 3 — LE SABLIER ============================ -->
337 +<section class="page">
338 + <header class="phead">
339 + <span class="wordmark">Groupe <span class="kabox">KA</span></span>
340 + <span class="doc">L'agrégation, expliquée</span>
341 + <span class="pnum">03 / 12</span>
342 + </header>
343 + <p class="kicker">Chapitre 02 · Le geste fondateur</p>
344 + <h2>Plein de sites. <span class="hl">Une seule flèche.</span></h2>
345 + <div class="figwrap">
346 + <div class="figbody" id="fig-sablier"></div>
347 + <div class="caption"><b>LE SABLIER ·KA —</b> des centaines de sites éparpillés, lus à la source par des
348 + connecteurs dédiés ; tout converge vers KA, qui normalise, dédoublonne et redistribue vers dix
349 + plateformes. Une flèche entre, dix flèches sortent : c'est ça, agréger.</div>
350 + </div>
351 + <footer class="pfoot"><span>groupe-ka.com</span><span>Chapitre 02 · Le sablier</span></footer>
352 +</section>
353 +
354 +<!-- ============================ PAGE 4 — LE PIPELINE ============================ -->
355 +<section class="page">
356 + <header class="phead">
357 + <span class="wordmark">Groupe <span class="kabox">KA</span></span>
358 + <span class="doc">L'agrégation, expliquée</span>
359 + <span class="pnum">04 / 12</span>
360 + </header>
361 + <p class="kicker">Chapitre 03 · De la page web à la donnée propre</p>
362 + <h2>Le pipeline, <span class="hl">station par station</span></h2>
363 + <div class="figwrap">
364 + <div class="figbody" id="fig-pipeline"></div>
365 + <div class="caption"><b>LE PIPELINE —</b> cinq stations, aucune main humaine. La boucle verte est le secret :
366 + tout le pipeline se rejoue en continu, donc une annonce retirée à la source disparaît d'elle-même.</div>
367 + </div>
368 + <footer class="pfoot"><span>groupe-ka.com</span><span>Chapitre 03 · Le pipeline</span></footer>
369 +</section>
370 +
371 +<!-- ============================ PAGE 5 — LE CYCLE ============================ -->
372 +<section class="page">
373 + <header class="phead">
374 + <span class="wordmark">Groupe <span class="kabox">KA</span></span>
375 + <span class="doc">L'agrégation, expliquée</span>
376 + <span class="pnum">05 / 12</span>
377 + </header>
378 + <p class="kicker">Chapitre 04 · La boucle jour et nuit</p>
379 + <h2>Pas un instantané, <span class="hl">un battement</span></h2>
380 + <div class="figwrap">
381 + <div class="figbody" id="fig-cycle"></div>
382 + <div class="caption"><b>LE CYCLE ·KA —</b> quatre gestes, en boucle, sans intervention humaine : lire les
383 + sources, comparer à hier, mettre à jour, publier et tracer. C'est la définition maison de l'agrégation.</div>
384 + </div>
385 + <footer class="pfoot"><span>groupe-ka.com</span><span>Chapitre 04 · Le cycle</span></footer>
386 +</section>
387 +
388 +<!-- ============================ PAGE 6 — LES ÉCLAIREURS ============================ -->
389 +<section class="page">
390 + <header class="phead">
391 + <span class="wordmark">Groupe <span class="kabox">KA</span></span>
392 + <span class="doc">L'agrégation, expliquée</span>
393 + <span class="pnum">06 / 12</span>
394 + </header>
395 + <p class="kicker">Chapitre 05 · Avant d'agréger, découvrir</p>
396 + <h2>Les éclaireurs : <span class="hl">Ka2 · Ka4 · Ka6</span></h2>
397 + <div class="figwrap">
398 + <div class="figbody" id="fig-bots"></div>
399 + <div class="caption"><b>LA DÉCOUVERTE —</b> les connecteurs lisent des sources connues ; les bots IA les
400 + trouvent. Ka2, Ka4 et Ka6 cartographient le web québécois entité par entité (robots.txt respecté,
401 + données publiques seulement) et alimentent tout l'écosystème en nouvelles sources.</div>
402 + </div>
403 + <footer class="pfoot"><span>groupe-ka.com</span><span>Chapitre 05 · Les éclaireurs</span></footer>
404 +</section>
405 +
406 +<!-- ============================ PAGE 7 — TROUVE·KA ============================ -->
407 +<section class="page">
408 + <header class="phead">
409 + <span class="wordmark">Groupe <span class="kabox">KA</span></span>
410 + <span class="doc">L'agrégation, expliquée</span>
411 + <span class="pnum">07 / 12</span>
412 + </header>
413 + <p class="kicker">Chapitre 06 · Le produit phare</p>
414 + <h2>Trouve·Ka <span class="hl">cherche le Québec</span></h2>
415 + <div class="figwrap">
416 + <div class="figbody" id="fig-trouveka"></div>
417 + <div class="caption"><b>LE MOTEUR —</b> les grands moteurs cherchent le monde ; Trouve·Ka ne contient que
418 + le web d'ici. Un commerce de Rimouski ou un blogue de Limoilou, noyés dans un index mondial, sont en
419 + première page dans un index 100 % Québec. C'est l'agrégation poussée à sa conclusion logique.</div>
420 + </div>
421 + <footer class="pfoot"><span>groupe-ka.com</span><span>Chapitre 06 · Trouve·Ka</span></footer>
422 +</section>
423 +
424 +<!-- ============================ PAGE 8 — L'INVENTAIRE ============================ -->
425 +<section class="page">
426 + <header class="phead">
427 + <span class="wordmark">Groupe <span class="kabox">KA</span></span>
428 + <span class="doc">L'agrégation, expliquée</span>
429 + <span class="pnum">08 / 12</span>
430 + </header>
431 + <p class="kicker">Chapitre 07 · La preuve par les nombres</p>
432 + <h2>L'inventaire <span class="hl">vivant</span></h2>
433 + <div class="figwrap" style="flex:1">
434 + <div class="bars" id="fig-bars" style="flex:1"></div>
435 + <div class="caption"><b>L'INVENTAIRE —</b> chaque barre est vivante : elle grossit quand les connecteurs
436 + trouvent du neuf, elle maigrit quand une annonce disparaît à la source. Compté aujourd'hui, pas promis.</div>
437 + </div>
438 + <footer class="pfoot"><span>groupe-ka.com</span><span>Chapitre 07 · L'inventaire</span></footer>
439 +</section>
440 +
441 +<!-- ============================ PAGE 9 — LA FLOTTE ============================ -->
442 +<section class="page">
443 + <header class="phead">
444 + <span class="wordmark">Groupe <span class="kabox">KA</span></span>
445 + <span class="doc">L'agrégation, expliquée</span>
446 + <span class="pnum">09 / 12</span>
447 + </header>
448 + <p class="kicker">Chapitre 08 · Un point = un connecteur</p>
449 + <h2>340 connecteurs, <span class="hl">dessinés un par un</span></h2>
450 + <div class="figwrap" style="flex:1">
451 + <div class="bars" id="fig-dots" style="flex:1"></div>
452 + <div class="caption"><b>LA FLOTTE —</b> chaque point est un petit programme qui connaît un site par cœur et
453 + le relit sans arrêt. S'y ajoutent les flux des 1 204 boutiques de Fabri·Ka, les données publiques de
454 + Vrai-Prix et ValoPlex, et l'annuaire multi-plateformes de Créa·Ka.</div>
455 + </div>
456 + <footer class="pfoot"><span>groupe-ka.com</span><span>Chapitre 08 · La flotte</span></footer>
457 +</section>
458 +
459 +<!-- ============================ PAGE 10 — LES MOTEURS D'ÉVALUATION ============================ -->
460 +<section class="page">
461 + <header class="phead">
462 + <span class="wordmark">Groupe <span class="kabox">KA</span></span>
463 + <span class="doc">L'agrégation, expliquée</span>
464 + <span class="pnum">10 / 12</span>
465 + </header>
466 + <p class="kicker">Chapitre 09 · Agréger pour évaluer</p>
467 + <h2>Deux moteurs, <span class="hl">tout le Québec</span></h2>
468 + <div class="eval-grid">
469 + <div class="card evalcard">
470 + <span class="k"><span class="brand"><i style="background:#ff5148"></i></span>Vrai-Prix · résidentiel</span>
471 + <span class="big">3 747 008</span>
472 + <span class="lbl">propriétés couvertes</span>
473 + <p>Toutes les unités du rôle foncier du Québec, estimées par un moteur hybride — modèle hédonique +
474 + comparables réels ajustés (745 119 ventes publiées 2021-2026, MdAPE 11 %). Chaque ajustement est
475 + affiché en dollars.</p>
476 + <div class="gauge">
477 + <div class="glab"><span>P10</span><span>fourchette honnête — jamais un faux prix unique</span><span>P90</span></div>
478 + <div class="gtrack"><div class="gband" style="left:18%;width:64%"></div><div class="gmid" style="left:50%"></div></div>
479 + </div>
480 + </div>
481 + <div class="card evalcard">
482 + <span class="k"><span class="brand"><i style="background:#ff9f45"></i></span>ValoPlex · immeubles à revenus</span>
483 + <span class="big">393 867</span>
484 + <span class="lbl">plex évalués · 1 733 744 portes</span>
485 + <p>Le moteur spécialisé des plex, entraîné exclusivement sur ~91 060 ventes réelles d'immeubles à
486 + revenus. L'unité d'analyse : la porte. Fourchette calibrée — 8 fois sur 10, le vrai prix tombe dedans.</p>
487 + <div class="gauge">
488 + <div class="glab"><span>P10</span><span>valeur totale et valeur par porte, calcul montré en entier</span><span>P90</span></div>
489 + <div class="gtrack"><div class="gband" style="left:22%;width:56%"></div><div class="gmid" style="left:50%"></div></div>
490 + </div>
491 + </div>
492 + </div>
493 + <footer class="pfoot"><span>groupe-ka.com</span><span>Chapitre 09 · Les moteurs d'évaluation</span></footer>
494 +</section>
495 +
496 +<!-- ============================ PAGE 11 — LE SOCLE ============================ -->
497 +<section class="page">
498 + <header class="phead">
499 + <span class="wordmark">Groupe <span class="kabox">KA</span></span>
500 + <span class="doc">L'agrégation, expliquée</span>
501 + <span class="pnum">11 / 12</span>
502 + </header>
503 + <p class="kicker">Chapitre 10 · Une seule fondation</p>
504 + <h2>Le socle : <span class="hl">onze plateformes</span></h2>
505 + <div class="tbl">
506 + <table id="socle-table">
507 + <thead><tr><th>Plateforme</th><th>Marché</th><th>Moteur</th><th>Adresse</th></tr></thead>
508 + <tbody></tbody>
509 + </table>
510 + </div>
511 + <footer class="pfoot"><span>groupe-ka.com</span><span>Chapitre 10 · Le socle</span></footer>
512 +</section>
513 +
514 +<!-- ============================ PAGE 12 — MANIFESTE ============================ -->
515 +<section class="page dark">
516 + <header class="phead">
517 + <span class="wordmark">Groupe <span class="kabox">KA</span></span>
518 + <span class="doc">L'agrégation, expliquée</span>
519 + <span class="pnum">12 / 12</span>
520 + </header>
521 + <p class="kicker">Notre manifeste</p>
522 + <h2>Trois principes, <span class="hl">aucune exception</span></h2>
523 + <div class="mani-grid">
524 + <div class="mani"><span class="n">01</span><h3>Zéro boîte noire</h3>
525 + <p>Un score magique n'est pas une réponse. Chaque estimation montre ses comparables, ses ajustements
526 + en dollars et sa fourchette d'incertitude. Si le calcul ne peut pas être montré, il ne sort pas.</p></div>
527 + <div class="mani"><span class="n">02</span><h3>Données réelles</h3>
528 + <p>Rôles d'évaluation foncière, ventes publiées, annonces vérifiées à la source. Aucun prix inventé :
529 + quand la donnée n'existe pas, on affiche qu'elle n'existe pas. Validé sur des ventes jamais vues.</p></div>
530 + <div class="mani"><span class="n">03</span><h3>Indépendance</h3>
531 + <p>Aucun courtier ni annonceur ne paie pour influencer un résultat. Pas de traceur publicitaire, pas de
532 + revente de données. Le français d'abord, le Québec comme terrain, la Loi 25 comme plancher.</p></div>
533 + </div>
534 + <div class="contact">
535 + <span><span class="mail">contact@groupe-ka.com</span><br /><span class="role">Projets, partenariats &amp; données</span></span>
536 + <span><span class="mail">info@groupe-ka.com</span><br /><span class="role">Médias &amp; questions générales</span></span>
537 + <span><span class="mail">admin@groupe-ka.com</span><br /><span class="role">Légal, vie privée &amp; Loi 25</span></span>
538 + </div>
539 + <footer class="pfoot"><span>groupe-ka.com</span><span>© 2026 Groupe KA — Simon-Pierre Boucher · contact@spboucher.ai</span></footer>
540 +</section>
541 +
542 +<script>
543 +"use strict";
544 +/* Figures générées : mêmes données et même langage graphique que le site groupe-ka.com. */
545 +
546 +const INK = "#141814", GREEN = "#1c5c41", LIME = "#d9f26b", PAPER = "#f5f3ee",
547 + INK3 = "#8b928c", WHITE = "#ffffff";
548 +
549 +const PLATFORMS = [
550 + { name: "Lou·Ka", accent: "#d9f26b", domain: "lou-ka.com" },
551 + { name: "Immo·Ka", accent: "#e23744", domain: "immo-ka.com" },
552 + { name: "Vrai-Prix",accent: "#ff5148", domain: "vrai-prix.com" },
553 + { name: "ValoPlex", accent: "#ff9f45", domain: "valoplex.com" },
554 + { name: "Auto·Ka", accent: "#ff5a2a", domain: "auto-ka.com" },
555 + { name: "Fabri·Ka", accent: "#C4532E", domain: "fabri-ka.com" },
556 + { name: "Food·Ka", accent: "#d9f26b", domain: "food-ka.com" },
557 + { name: "Resto·Ka", accent: "#ffc53d", domain: "resto-ka.com" },
558 + { name: "Sorti·Ka", accent: "#ff6a00", domain: "sorti-ka.com" },
559 + { name: "Créa·Ka", accent: "#7da2ff", domain: "crea-ka.com" },
560 +];
561 +const ACCENT = Object.fromEntries(PLATFORMS.map(p => [p.name, p.accent]));
562 +
563 +const SOURCES = [
564 + "Gestionnaires immobiliers", "Agences de courtage", "Rôles fonciers (MAMH)",
565 + "Registres de ventes", "Concessionnaires autos", "Marchands d'occasion",
566 + "Boutiques québécoises", "Sites de fabricants", "Bannières d'épicerie",
567 + "Circulaires & soldes", "Menus de restaurants", "Billetteries d'événements",
568 + "Profils de créateurs",
569 +];
570 +
571 +/* ---------- Couverture : pilules des marques ---------- */
572 +(function () {
573 + const wrap = document.getElementById("cover-marks");
574 + const marks = [
575 + ["Trouve","Ka","#b9cfee","#141814"], ["Lou","Ka","#d9f26b","#141814"],
576 + ["Immo","Ka","#e23744","#1a1214"], ["Vrai","Prix","#ff5148","#141814"],
577 + ["Valo","Plex","#ff9f45","#141814"], ["Auto","Ka","#ff5a2a","#17181c"],
578 + ["Fabri","Ka","#C4532E","#1E1B16"], ["Food","Ka","#d9f26b","#14231a"],
579 + ["Resto","Ka","#ffc53d","#1a1410"], ["Sorti","Ka","#ff6a00","#0b1330"],
580 + ["Créa","Ka","#7da2ff","#10162b"],
581 + ];
582 + for (const [l, r, accent, box] of marks) {
583 + const s = document.createElement("span");
584 + s.className = "markpill";
585 + s.innerHTML = `${l}<i style="background:${box};color:${accent}">${r}</i>`;
586 + wrap.appendChild(s);
587 + }
588 +})();
589 +
590 +/* ---------- Utilitaire SVG ---------- */
591 +function svgEl(tag, attrs, text) {
592 + const el = document.createElementNS("http://www.w3.org/2000/svg", tag);
593 + for (const k in attrs) el.setAttribute(k, attrs[k]);
594 + if (text != null) el.textContent = text;
595 + return el;
596 +}
597 +function makeSvg(w, h, label) {
598 + const svg = svgEl("svg", { viewBox: `0 0 ${w} ${h}`, role: "img", "aria-label": label });
599 + const marker = svgEl("marker", { id: "arr" + Math.floor(w), viewBox: "0 0 10 10", refX: 8, refY: 5, markerWidth: 6.5, markerHeight: 6.5, orient: "auto-start-reverse" });
600 + marker.appendChild(svgEl("path", { d: "M0,0 L10,5 L0,10 z", fill: INK }));
601 + const defs = svgEl("defs", {});
602 + defs.appendChild(marker);
603 + svg.appendChild(defs);
604 + svg.dataset.marker = "arr" + Math.floor(w);
605 + return svg;
606 +}
607 +function brandChip(svg, x, y, accent, s = 16) {
608 + svg.appendChild(svgEl("rect", { x, y, width: s, height: s, rx: 4, fill: INK }));
609 + svg.appendChild(svgEl("rect", { x: x + 3, y: y + 3, width: s - 6, height: s - 6, rx: 2.5, fill: accent }));
610 +}
611 +
612 +/* ---------- 1. Le sablier (page 3) ---------- */
613 +(function () {
614 + const W = 1150, H = 640;
615 + const svg = makeSvg(W, H, "Le sablier de l'agrégation KA");
616 + const M = svg.dataset.marker;
617 + const srcX = 218, kaL = 500, kaR = 648, kaY = H / 2, dstX = 900;
618 +
619 + const srcYs = SOURCES.map((_, i) => 50 + i * (H - 100) / (SOURCES.length - 1));
620 + const dstYs = PLATFORMS.map((_, i) => 62 + i * (H - 124) / (PLATFORMS.length - 1));
621 +
622 + srcYs.forEach((y, i) => svg.appendChild(svgEl("path", {
623 + d: `M ${srcX} ${y} C ${srcX + 140} ${y}, ${kaL - 120} ${kaY}, ${kaL - 8} ${kaY + (i - (SOURCES.length - 1) / 2) * 6}`,
624 + fill: "none", stroke: INK, "stroke-width": 1.5, "stroke-opacity": 0.5, "stroke-dasharray": "7 7",
625 + })));
626 + dstYs.forEach((y, i) => svg.appendChild(svgEl("path", {
627 + d: `M ${kaR + 4} ${kaY + (i - (PLATFORMS.length - 1) / 2) * 8} C ${kaR + 120} ${kaY}, ${dstX - 130} ${y}, ${dstX - 12} ${y}`,
628 + fill: "none", stroke: INK, "stroke-width": 2, "stroke-opacity": 0.75, "marker-end": `url(#${M})`, "stroke-dasharray": "7 7",
629 + })));
630 +
631 + SOURCES.forEach((label, i) => {
632 + svg.appendChild(svgEl("rect", { x: 16, y: srcYs[i] - 15, width: srcX - 16, height: 30, rx: 6, fill: WHITE, stroke: INK, "stroke-width": 1.5 }));
633 + svg.appendChild(svgEl("circle", { cx: 33, cy: srcYs[i], r: 3.5, fill: GREEN }));
634 + svg.appendChild(svgEl("text", { x: 46, y: srcYs[i] + 3.5, "font-size": 11, "font-weight": 700, fill: INK }, label));
635 + });
636 + svg.appendChild(svgEl("text", { x: 16, y: 24, "font-size": 11.5, "font-weight": 700, "letter-spacing": 1.5, fill: INK3 }, "DES CENTAINES DE SITES"));
637 +
638 + const gLbl = svgEl("g", { transform: `translate(${(srcX + kaL) / 2 - 6} ${kaY - 128}) rotate(-4)` });
639 + gLbl.appendChild(svgEl("rect", { x: -102, y: -25, width: 204, height: 50, rx: 13, fill: LIME, stroke: INK, "stroke-width": 1.5 }));
640 + gLbl.appendChild(svgEl("text", { x: 0, y: -4, "text-anchor": "middle", "font-size": 12, "font-weight": 700, "letter-spacing": 1, fill: INK }, "450+ CONNECTEURS"));
641 + gLbl.appendChild(svgEl("text", { x: 0, y: 14, "text-anchor": "middle", "font-size": 9, fill: "#2c3a2c" }, "lecture à la source · normalisation"));
642 + svg.appendChild(gLbl);
643 +
644 + const gKa = svgEl("g", { transform: `translate(${(kaL + kaR) / 2} ${kaY}) rotate(-2)` });
645 + gKa.appendChild(svgEl("rect", { x: -(kaR - kaL) / 2 + 6, y: -66, width: kaR - kaL, height: 146, rx: 12, fill: "rgba(20,24,20,0.18)" }));
646 + gKa.appendChild(svgEl("rect", { x: -(kaR - kaL) / 2, y: -72, width: kaR - kaL, height: 146, rx: 12, fill: INK }));
647 + const kaTxt = svgEl("text", { x: 0, y: 8, "text-anchor": "middle", "font-size": 62, "font-weight": 700, "letter-spacing": -2, fill: LIME }, "KA");
648 + kaTxt.setAttribute("class", "t-display");
649 + gKa.appendChild(kaTxt);
650 + gKa.appendChild(svgEl("text", { x: 0, y: 37, "text-anchor": "middle", "font-size": 11, "font-weight": 700, "letter-spacing": 2, fill: "rgba(245,243,238,0.75)" }, "= AGRÉGATION"));
651 + gKa.appendChild(svgEl("text", { x: 0, y: 55, "text-anchor": "middle", "font-size": 8.5, fill: "rgba(245,243,238,0.55)" }, "dédoublonner · géocoder"));
652 + svg.appendChild(gKa);
653 +
654 + PLATFORMS.forEach((p, i) => {
655 + const y = dstYs[i];
656 + svg.appendChild(svgEl("rect", { x: dstX, y: y - 21, width: W - dstX - 14, height: 42, rx: 8, fill: WHITE, stroke: INK, "stroke-width": 1.5 }));
657 + brandChip(svg, dstX + 13, y - 8, p.accent);
658 + const nm = svgEl("text", { x: dstX + 40, y: y - 1, "font-size": 15.5, "font-weight": 700, fill: INK }, p.name);
659 + nm.setAttribute("class", "t-display");
660 + svg.appendChild(nm);
661 + svg.appendChild(svgEl("text", { x: dstX + 40, y: y + 14, "font-size": 9, fill: INK3 }, p.domain));
662 + });
663 + svg.appendChild(svgEl("text", { x: dstX, y: 30, "font-size": 11.5, "font-weight": 700, "letter-spacing": 1.5, fill: INK3 }, "DIX PLATEFORMES"));
664 +
665 + document.getElementById("fig-sablier").appendChild(svg);
666 +})();
667 +
668 +/* ---------- 2. Le pipeline horizontal (page 4) ---------- */
669 +(function () {
670 + const STEPS = [
671 + ["01", "Site source", "La page publique, telle que tout le monde la voit."],
672 + ["02", "Connecteur", "Un lecteur dédié par site — il connaît sa structure par cœur."],
673 + ["03", "Normalisation", "Mêmes champs, mêmes unités : un 3½ est un 3½ partout."],
674 + ["04", "Dédoublonnage & géocodage", "Une seule fiche par annonce réelle, posée sur la carte."],
675 + ["05", "Publication", "En ligne, datée, traçable jusqu'à la source originale."],
676 + ];
677 + const W = 1150, H = 430, boxW = 205, boxH = 210, gap = (W - 40 - 5 * boxW) / 4, top = 90;
678 + const svg = makeSvg(W, H, "Le pipeline de la donnée KA");
679 + const M = svg.dataset.marker;
680 +
681 + STEPS.forEach((s, i) => {
682 + const x = 20 + i * (boxW + gap);
683 + const last = i === STEPS.length - 1;
684 + svg.appendChild(svgEl("rect", { x: x + 5, y: top + 5, width: boxW, height: boxH, rx: 12, fill: "rgba(20,24,20,0.14)" }));
685 + svg.appendChild(svgEl("rect", { x, y: top, width: boxW, height: boxH, rx: 12, fill: last ? INK : WHITE, stroke: INK, "stroke-width": 1.5 }));
686 + svg.appendChild(svgEl("text", { x: x + 18, y: top + 36, "font-size": 13, "font-weight": 700, fill: last ? LIME : GREEN }, s[0]));
687 + const t = svgEl("text", { x: x + 18, y: top + 68, "font-size": 16.5, "font-weight": 700, fill: last ? PAPER : INK });
688 + t.setAttribute("class", "t-display");
689 + // titre sur 1-2 lignes
690 + const words = s[1].split(" ");
691 + let line = "", lines = [];
692 + for (const w of words) {
693 + if ((line + " " + w).trim().length > 15) { lines.push(line.trim()); line = w; } else line += " " + w;
694 + }
695 + lines.push(line.trim());
696 + lines.forEach((ln, li) => t.appendChild(svgEl("tspan", { x: x + 18, dy: li === 0 ? 0 : 21 }, ln)));
697 + svg.appendChild(t);
698 + // description
699 + const d = svgEl("text", { x: x + 18, y: top + 68 + lines.length * 21 + 12, "font-size": 10.5, fill: last ? "rgba(245,243,238,0.7)" : "#4d5551" });
700 + d.setAttribute("class", "t-body");
701 + let dl = "", dls = [];
702 + for (const w of s[2].split(" ")) {
703 + if ((dl + " " + w).trim().length > 28) { dls.push(dl.trim()); dl = w; } else dl += " " + w;
704 + }
705 + dls.push(dl.trim());
706 + dls.forEach((ln, li) => d.appendChild(svgEl("tspan", { x: x + 18, dy: li === 0 ? 0 : 15 }, ln)));
707 + svg.appendChild(d);
708 + if (!last) svg.appendChild(svgEl("line", {
709 + x1: x + boxW + 6, y1: top + boxH / 2, x2: x + boxW + gap - 8, y2: top + boxH / 2,
710 + stroke: INK, "stroke-width": 2, "marker-end": `url(#${M})`, "stroke-dasharray": "7 7",
711 + }));
712 + });
713 +
714 + // boucle de resynchronisation : dernière station → première
715 + const y0 = top + boxH + 44;
716 + svg.appendChild(svgEl("path", {
717 + d: `M ${20 + 4 * (boxW + gap) + boxW / 2} ${top + boxH + 6} L ${20 + 4 * (boxW + gap) + boxW / 2} ${y0}
718 + L ${20 + boxW / 2} ${y0} L ${20 + boxW / 2} ${top + boxH + 14}`,
719 + fill: "none", stroke: GREEN, "stroke-width": 2.5, "marker-end": `url(#${M})`, "stroke-dasharray": "7 7",
720 + }));
721 + const gR = svgEl("g", { transform: `translate(${W / 2} ${y0}) rotate(-2)` });
722 + gR.appendChild(svgEl("rect", { x: -110, y: -17, width: 220, height: 34, rx: 17, fill: LIME, stroke: INK, "stroke-width": 1.5 }));
723 + gR.appendChild(svgEl("text", { x: 0, y: 4.5, "text-anchor": "middle", "font-size": 11.5, "font-weight": 700, "letter-spacing": 1, fill: INK }, "RESYNC EN CONTINU"));
724 + svg.appendChild(gR);
725 + svg.appendChild(svgEl("text", { x: 20, y: 34, "font-size": 11.5, "font-weight": 700, "letter-spacing": 1.5, fill: INK3 }, "CINQ STATIONS · AUCUNE MAIN HUMAINE"));
726 +
727 + document.getElementById("fig-pipeline").appendChild(svg);
728 +})();
729 +
730 +/* ---------- 3. Le cycle (page 5) ---------- */
731 +(function () {
732 + const STEPS = [
733 + ["01", "Lire les sources", -90], ["02", "Comparer à hier", 0],
734 + ["03", "Mettre à jour", 90], ["04", "Publier & tracer", 180],
735 + ];
736 + const W = 1150, S = 440, cx = W / 2, cy = S / 2 + 4, R = 150;
737 + const svg = makeSvg(W, S, "Le cycle KA, jour et nuit");
738 + const M = svg.dataset.marker;
739 +
740 + svg.appendChild(svgEl("circle", { cx, cy, r: R, fill: "none", stroke: INK, "stroke-width": 2, "stroke-opacity": 0.65, "stroke-dasharray": "7 7" }));
741 + [-45, 45, 135, 225].forEach((a) => {
742 + const rad = a * Math.PI / 180, x = cx + R * Math.cos(rad), y = cy + R * Math.sin(rad);
743 + const tx = -Math.sin(rad), ty = Math.cos(rad);
744 + svg.appendChild(svgEl("line", { x1: x - tx, y1: y - ty, x2: x + tx * 2, y2: y + ty * 2, stroke: INK, "stroke-width": 2, "marker-end": `url(#${M})` }));
745 + });
746 +
747 + const gKa = svgEl("g", { transform: `translate(${cx} ${cy}) rotate(-2)` });
748 + gKa.appendChild(svgEl("rect", { x: -43, y: -37, width: 96, height: 88, rx: 10, fill: "rgba(20,24,20,0.18)" }));
749 + gKa.appendChild(svgEl("rect", { x: -48, y: -42, width: 96, height: 88, rx: 10, fill: INK }));
750 + const t = svgEl("text", { x: 0, y: 8, "text-anchor": "middle", "font-size": 40, "font-weight": 700, "letter-spacing": -1.5, fill: LIME }, "KA");
751 + t.setAttribute("class", "t-display");
752 + gKa.appendChild(t);
753 + gKa.appendChild(svgEl("text", { x: 0, y: 30, "text-anchor": "middle", "font-size": 8, "letter-spacing": 1.5, fill: "rgba(245,243,238,0.65)" }, "JOUR ET NUIT"));
754 + svg.appendChild(gKa);
755 +
756 + STEPS.forEach(([num, label, angle]) => {
757 + const rad = angle * Math.PI / 180, x = cx + R * Math.cos(rad), y = cy + R * Math.sin(rad), w = 178;
758 + const g = svgEl("g", { transform: `translate(${x} ${y})` });
759 + g.appendChild(svgEl("rect", { x: -w / 2, y: -21, width: w, height: 42, rx: 9, fill: WHITE, stroke: INK, "stroke-width": 1.5 }));
760 + g.appendChild(svgEl("text", { x: -w / 2 + 13, y: 5, "font-size": 11, "font-weight": 700, fill: GREEN }, num));
761 + const l = svgEl("text", { x: -w / 2 + 38, y: 5, "font-size": 14, "font-weight": 700, fill: INK }, label);
762 + l.setAttribute("class", "t-display");
763 + g.appendChild(l);
764 + svg.appendChild(g);
765 + });
766 +
767 + // légendes latérales
768 + const notes = [
769 + [30, "Une annonce retirée à la source", "disparaît d'elle-même du site."],
770 + [W - 260, "Une baisse de prix est détectée", "et historisée automatiquement."],
771 + ];
772 + notes.forEach(([x, l1, l2]) => {
773 + svg.appendChild(svgEl("rect", { x, y: cy - 34, width: 230, height: 68, rx: 9, fill: "#faf9f5", stroke: INK, "stroke-width": 1.5, "stroke-dasharray": "5 5" }));
774 + const n = svgEl("text", { x: x + 16, y: cy - 8, "font-size": 11, fill: "#4d5551" });
775 + n.setAttribute("class", "t-body");
776 + n.appendChild(svgEl("tspan", { x: x + 16, dy: 0 }, l1));
777 + n.appendChild(svgEl("tspan", { x: x + 16, dy: 17 }, l2));
778 + svg.appendChild(n);
779 + });
780 +
781 + document.getElementById("fig-cycle").appendChild(svg);
782 +})();
783 +
784 +/* ---------- 4. Les éclaireurs (page 6) ---------- */
785 +(function () {
786 + const W = 1300, H = 470;
787 + const svg = makeSvg(W, H, "Les bots de découverte Ka2, Ka4 et Ka6");
788 + const M = svg.dataset.marker;
789 +
790 + // nuage du web québécois (points)
791 + const cloud = { x: 30, y: 60, w: 260, h: 340 };
792 + svg.appendChild(svgEl("rect", { x: cloud.x, y: cloud.y, width: cloud.w, height: cloud.h, rx: 14, fill: WHITE, stroke: INK, "stroke-width": 1.5 }));
793 + let seed = 7;
794 + const rnd = () => (seed = (seed * 16807) % 2147483647) / 2147483647;
795 + for (let i = 0; i < 130; i++) {
796 + svg.appendChild(svgEl("circle", {
797 + cx: cloud.x + 18 + rnd() * (cloud.w - 36), cy: cloud.y + 34 + rnd() * (cloud.h - 56),
798 + r: 2 + rnd() * 2.4, fill: rnd() > 0.82 ? GREEN : "rgba(20,24,20,0.35)",
799 + }));
800 + }
801 + svg.appendChild(svgEl("text", { x: cloud.x + 16, y: cloud.y + 22, "font-size": 10.5, "font-weight": 700, "letter-spacing": 1.2, fill: INK3 }, "LE WEB QUÉBÉCOIS"));
802 +
803 + // bots
804 + const bots = [
805 + ["Ka", "2", "#d9f26b", "cartographie les entités", 96],
806 + ["Ka", "4", "#ff9f45", "activité à découvert", 232],
807 + ["Ka", "6", "#b9cfee", "nourrit Trouve·Ka", 368],
808 + ];
809 + const botX = 430, botW = 250;
810 + bots.forEach(([l, d, accent, sub, y]) => {
811 + svg.appendChild(svgEl("path", { d: `M ${cloud.x + cloud.w + 6} ${cloud.y + cloud.h / 2} C ${botX - 90} ${cloud.y + cloud.h / 2}, ${botX - 90} ${y}, ${botX - 8} ${y}`, fill: "none", stroke: INK, "stroke-width": 1.7, "stroke-opacity": 0.6, "stroke-dasharray": "7 7", "marker-end": `url(#${M})` }));
812 + svg.appendChild(svgEl("rect", { x: botX, y: y - 36, width: botW, height: 72, rx: 11, fill: WHITE, stroke: INK, "stroke-width": 1.5 }));
813 + const t = svgEl("text", { x: botX + 18, y: y + 2, "font-size": 25, "font-weight": 700, "letter-spacing": -1, fill: INK });
814 + t.setAttribute("class", "t-display");
815 + t.appendChild(svgEl("tspan", {}, l));
816 + svg.appendChild(t);
817 + svg.appendChild(svgEl("rect", { x: botX + 55, y: y - 20, width: 28, height: 28, rx: 6, fill: INK, transform: `rotate(-4 ${botX + 69} ${y - 6})` }));
818 + const dt = svgEl("text", { x: botX + 69, y: y + 2, "text-anchor": "middle", "font-size": 19, "font-weight": 700, fill: accent });
819 + dt.setAttribute("class", "t-display");
820 + dt.textContent = d;
821 + svg.appendChild(dt);
822 + svg.appendChild(svgEl("text", { x: botX + 100, y: y - 2, "font-size": 10.5, "font-weight": 700, fill: GREEN }, "BOT IA"));
823 + const s = svgEl("text", { x: botX + 100, y: y + 15, "font-size": 10.5, fill: "#4d5551" }, sub);
824 + s.setAttribute("class", "t-body");
825 + svg.appendChild(s);
826 + svg.appendChild(svgEl("path", { d: `M ${botX + botW + 4} ${y} C ${botX + botW + 80} ${y}, ${botX + botW + 80} ${H / 2}, ${botX + botW + 130} ${H / 2}`, fill: "none", stroke: INK, "stroke-width": 1.7, "stroke-opacity": 0.6, "stroke-dasharray": "7 7", "marker-end": `url(#${M})` }));
827 + });
828 +
829 + // registre d'entités
830 + const regX = botX + botW + 140, regW = 180;
831 + svg.appendChild(svgEl("rect", { x: regX + 5, y: H / 2 - 55 + 5, width: regW, height: 110, rx: 12, fill: "rgba(20,24,20,0.14)" }));
832 + svg.appendChild(svgEl("rect", { x: regX, y: H / 2 - 55, width: regW, height: 110, rx: 12, fill: INK }));
833 + const rt = svgEl("text", { x: regX + regW / 2, y: H / 2 - 12, "text-anchor": "middle", "font-size": 17, "font-weight": 700, fill: LIME });
834 + rt.setAttribute("class", "t-display");
835 + rt.textContent = "Registre";
836 + svg.appendChild(rt);
837 + const rt2 = svgEl("text", { x: regX + regW / 2, y: H / 2 + 10, "text-anchor": "middle", "font-size": 17, "font-weight": 700, fill: LIME });
838 + rt2.setAttribute("class", "t-display");
839 + rt2.textContent = "d'entités";
840 + svg.appendChild(rt2);
841 + svg.appendChild(svgEl("text", { x: regX + regW / 2, y: H / 2 + 34, "text-anchor": "middle", "font-size": 8.5, "letter-spacing": 1, fill: "rgba(245,243,238,0.6)" }, "entreprises · personnes · sites"));
842 +
843 + // vers les connecteurs
844 + svg.appendChild(svgEl("line", { x1: regX + regW + 6, y1: H / 2, x2: regX + regW + 78, y2: H / 2, stroke: INK, "stroke-width": 2.2, "marker-end": `url(#${M})`, "stroke-dasharray": "7 7" }));
845 + const outX = regX + regW + 90;
846 + svg.appendChild(svgEl("rect", { x: outX, y: H / 2 - 62, width: W - outX - 16, height: 124, rx: 12, fill: LIME, stroke: INK, "stroke-width": 1.5 }));
847 + const ot = svgEl("text", { x: outX + 18, y: H / 2 - 26, "font-size": 16, "font-weight": 700, fill: INK });
848 + ot.setAttribute("class", "t-display");
849 + ot.textContent = "Nouvelles sources";
850 + svg.appendChild(ot);
851 + const ol = svgEl("text", { x: outX + 18, y: H / 2 - 2, "font-size": 10.5, fill: "#2c3a2c" });
852 + ol.setAttribute("class", "t-body");
853 + ["Boutique qui ouvre, gestionnaire,", "concessionnaire : repérés par les", "bots, branchés par les ·Ka."].forEach((ln, i) =>
854 + ol.appendChild(svgEl("tspan", { x: outX + 18, dy: i === 0 ? 0 : 16 }, ln)));
855 + svg.appendChild(ol);
856 +
857 + document.getElementById("fig-bots").appendChild(svg);
858 +})();
859 +
860 +/* ---------- 5. Trouve·Ka (page 7) ---------- */
861 +(function () {
862 + const W = 1150, H = 470;
863 + const svg = makeSvg(W, H, "Trouve·Ka, le moteur de recherche du web québécois");
864 + const M = svg.dataset.marker;
865 +
866 + // robot → index → barre de recherche → résultats
867 + const steps = [
868 + { x: 30, w: 250, title: "Le robot", sub: ["Parcourt le web québécois", "jour et nuit — et lui seul.", "robots.txt respecté."], tag: "CRAWLER DÉDIÉ" },
869 + { x: 330, w: 250, title: "L'index", sub: ["Des milliers de domaines d'ici,", "recherche plein texte,", "croissance heure par heure."], tag: "100 % QUÉBEC" },
870 + ];
871 + steps.forEach((s) => {
872 + svg.appendChild(svgEl("rect", { x: s.x, y: 80, width: s.w, height: 170, rx: 12, fill: WHITE, stroke: INK, "stroke-width": 1.5 }));
873 + svg.appendChild(svgEl("text", { x: s.x + 18, y: 112, "font-size": 10, "font-weight": 700, "letter-spacing": 1.2, fill: GREEN }, s.tag));
874 + const t = svgEl("text", { x: s.x + 18, y: 144, "font-size": 21, "font-weight": 700, fill: INK });
875 + t.setAttribute("class", "t-display");
876 + t.textContent = s.title;
877 + svg.appendChild(t);
878 + const d = svgEl("text", { x: s.x + 18, y: 170, "font-size": 11, fill: "#4d5551" });
879 + d.setAttribute("class", "t-body");
880 + s.sub.forEach((ln, i) => d.appendChild(svgEl("tspan", { x: s.x + 18, dy: i === 0 ? 0 : 17 }, ln)));
881 + svg.appendChild(d);
882 + });
883 + svg.appendChild(svgEl("line", { x1: 284, y1: 165, x2: 322, y2: 165, stroke: INK, "stroke-width": 2, "marker-end": `url(#${M})`, "stroke-dasharray": "7 7" }));
884 + svg.appendChild(svgEl("line", { x1: 584, y1: 165, x2: 622, y2: 165, stroke: INK, "stroke-width": 2, "marker-end": `url(#${M})`, "stroke-dasharray": "7 7" }));
885 +
886 + // barre de recherche
887 + const sbX = 634, sbW = 480;
888 + svg.appendChild(svgEl("rect", { x: sbX + 4, y: 134 + 4, width: sbW, height: 62, rx: 31, fill: "rgba(20,24,20,0.5)" }));
889 + svg.appendChild(svgEl("rect", { x: sbX, y: 134, width: sbW, height: 62, rx: 31, fill: WHITE, stroke: INK, "stroke-width": 2 }));
890 + svg.appendChild(svgEl("text", { x: sbX + 26, y: 172, "font-size": 18, "font-weight": 700, fill: INK }, "⌕"));
891 + svg.appendChild(svgEl("text", { x: sbX + 54, y: 171, "font-size": 12.5, fill: INK3 }, "poutine · plex à vendre · Gaspésie…"));
892 + svg.appendChild(svgEl("rect", { x: sbX + sbW - 116, y: 145, width: 102, height: 40, rx: 20, fill: INK }));
893 + const st = svgEl("text", { x: sbX + sbW - 65, y: 170, "text-anchor": "middle", "font-size": 13.5, "font-weight": 700, fill: "#b9cfee" });
894 + st.setAttribute("class", "t-display");
895 + st.textContent = "Chercher";
896 + svg.appendChild(st);
897 + const wm = svgEl("text", { x: sbX, y: 118, "font-size": 21, "font-weight": 700, "letter-spacing": -0.7, fill: INK });
898 + wm.setAttribute("class", "t-display");
899 + wm.textContent = "Trouve";
900 + svg.appendChild(wm);
901 + svg.appendChild(svgEl("rect", { x: sbX + 78, y: 98, width: 34, height: 26, rx: 6, fill: INK, transform: `rotate(-3 ${sbX + 95} 111)` }));
902 + const wk = svgEl("text", { x: sbX + 95, y: 117, "text-anchor": "middle", "font-size": 16, "font-weight": 700, fill: "#b9cfee" });
903 + wk.setAttribute("class", "t-display");
904 + wk.textContent = "Ka";
905 + svg.appendChild(wk);
906 + svg.appendChild(svgEl("text", { x: sbX + 124, y: 114, "font-size": 9.5, fill: INK3 }, "www.trouve-ka.com"));
907 +
908 + // résultats (3 lignes)
909 + ["Microbrasserie — Rimouski", "Érablière — Lanaudière", "Blogue de quartier — Limoilou"].forEach((r, i) => {
910 + const y = 232 + i * 46;
911 + svg.appendChild(svgEl("rect", { x: sbX, y, width: sbW, height: 36, rx: 8, fill: "#faf9f5", stroke: INK, "stroke-width": 1.2 }));
912 + svg.appendChild(svgEl("circle", { cx: sbX + 20, cy: y + 18, r: 3.5, fill: GREEN }));
913 + const rt = svgEl("text", { x: sbX + 34, y: y + 23, "font-size": 12.5, "font-weight": 600, fill: INK });
914 + rt.setAttribute("class", "t-body");
915 + rt.textContent = r;
916 + svg.appendChild(rt);
917 + svg.appendChild(svgEl("text", { x: sbX + sbW - 20, y: y + 23, "text-anchor": "end", "font-size": 9, fill: GREEN }, "1re page"));
918 + });
919 +
920 + // note comparaison
921 + svg.appendChild(svgEl("rect", { x: 30, y: 290, width: 554, height: 130, rx: 12, fill: INK }));
922 + const nt = svgEl("text", { x: 56, y: 330, "font-size": 20, "font-weight": 700, "letter-spacing": -0.5, fill: PAPER });
923 + nt.setAttribute("class", "t-display");
924 + nt.appendChild(svgEl("tspan", { x: 56, dy: 0 }, "LES GRANDS MOTEURS CHERCHENT LE MONDE."));
925 + const sp = svgEl("tspan", { x: 56, dy: 28, fill: LIME }, "TROUVE·KA CHERCHE LE QUÉBEC.");
926 + nt.appendChild(sp);
927 + svg.appendChild(nt);
928 + svg.appendChild(svgEl("text", { x: 56, y: 396, "font-size": 10, "letter-spacing": 1, fill: "rgba(245,243,238,0.6)" }, "NOURRI PAR LES ÉCLAIREURS KA2 · KA4 · KA6 — ZÉRO PUBLICITÉ, ZÉRO PROFILAGE"));
929 +
930 + document.getElementById("fig-trouveka").appendChild(svg);
931 +})();
932 +
933 +/* ---------- 6. Barres d'inventaire (page 8) ---------- */
934 +(function () {
935 + const INVENTORY = [
936 + ["Fabri·Ka", 281000, "produits québécois"],
937 + ["Resto·Ka", 61000, "plats & prix réels"],
938 + ["Immo·Ka", 54000, "propriétés à vendre"],
939 + ["Food·Ka", 23700, "prix d'épicerie"],
940 + ["Auto·Ka", 15900, "véhicules d'occasion"],
941 + ["Sorti·Ka", 11100, "événements & sorties"],
942 + ["Lou·Ka", 7000, "logements à louer"],
943 + ["Créa·Ka", 2200, "créateurs d'ici"],
944 + ];
945 + const max = Math.max(...INVENTORY.map((d) => d[1]));
946 + const fmt = (n) => n.toLocaleString("fr-CA");
947 + const wrap = document.getElementById("fig-bars");
948 + for (const [name, value, unit] of INVENTORY) {
949 + const row = document.createElement("div");
950 + row.className = "barrow";
951 + row.innerHTML =
952 + `<div class="barhead"><span class="nm"><span class="brand"><i style="background:${ACCENT[name]}"></i></span>${name}<span class="u">${unit}</span></span>` +
953 + `<span class="v">${fmt(value)}+</span></div>` +
954 + `<div class="bartrack"><div class="barfill" style="width:${Math.max((value / max) * 100, 1.6)}%"></div></div>`;
955 + wrap.appendChild(row);
956 + }
957 +})();
958 +
959 +/* ---------- 7. Matrice de connecteurs (page 9) ---------- */
960 +(function () {
961 + const CONNECTORS = [
962 + ["Auto·Ka", 124, "concessionnaires"],
963 + ["Lou·Ka", 108, "gestionnaires"],
964 + ["Immo·Ka", 65, "agences"],
965 + ["Food·Ka", 20, "bannières"],
966 + ["Resto·Ka", 19, "chaînes de restos"],
967 + ["Sorti·Ka", 4, "billetteries & données ouvertes"],
968 + ];
969 + const wrap = document.getElementById("fig-dots");
970 + for (const [name, count, kind] of CONNECTORS) {
971 + const b = document.createElement("div");
972 + b.className = "dots-block";
973 + b.innerHTML =
974 + `<div class="barhead"><span class="nm"><span class="brand"><i style="background:${ACCENT[name]}"></i></span>${name}<span class="u">${kind}</span></span>` +
975 + `<span class="v">${count}</span></div>` +
976 + `<div class="dots">${"<i></i>".repeat(count)}</div>`;
977 + wrap.appendChild(b);
978 + }
979 +})();
980 +
981 +/* ---------- 8. Tableau du socle (page 11) ---------- */
982 +(function () {
983 + const ROWS = [
984 + ["Trouve·Ka", "#b9cfee", "Recherche — tout le web québécois", "Crawler dédié + index de recherche plein texte", "www.trouve-ka.com"],
985 + ["Lou·Ka", "#d9f26b", "Location résidentielle", "108 connecteurs sources + synchronisation horaire", "www.lou-ka.com"],
986 + ["Immo·Ka", "#e23744", "Propriétés à vendre", "65 connecteurs d'agences + détection des ventes", "www.immo-ka.com"],
987 + ["Vrai-Prix", "#ff5148", "Résidentiel — tout le Québec", "Hédonique LightGBM + comparables réels ajustés", "www.vrai-prix.com"],
988 + ["ValoPlex", "#ff9f45", "Immeubles à revenus (plex)", "Hédonique monotone + ventes de plex comparables", "www.valoplex.com"],
989 + ["Auto·Ka", "#ff5a2a", "Voitures usagées", "125 connecteurs concessionnaires + suivi des prix", "www.auto-ka.com"],
990 + ["Fabri·Ka", "#C4532E", "Produits québécois", "1 204 boutiques en ligne indexées par région", "www.fabri-ka.com"],
991 + ["Food·Ka", "#d9f26b", "Épicerie", "20 bannières — prix, soldes et prix unitaires", "www.food-ka.com"],
992 + ["Resto·Ka", "#ffc53d", "Restaurants — menus & prix", "19 chaînes intégrées — 61 000+ plats suivis", "www.resto-ka.com"],
993 + ["Sorti·Ka", "#ff6a00", "Sorties & événements", "Billetteries + données ouvertes — MàJ horaire", "www.sorti-ka.com"],
994 + ["Créa·Ka", "#7da2ff", "Créateurs de contenu", "Annuaire multi-plateformes + résolution d'identité", "www.crea-ka.com"],
995 + ];
996 + const tbody = document.querySelector("#socle-table tbody");
997 + for (const [name, accent, market, engine, url] of ROWS) {
998 + const tr = document.createElement("tr");
999 + tr.innerHTML =
1000 + `<td class="nm"><span class="brand"><i style="background:${accent}"></i></span>${name}</td>` +
1001 + `<td>${market}</td><td>${engine}</td><td class="url">${url}</td>`;
1002 + tbody.appendChild(tr);
1003 + }
1004 +})();
1005 +</script>
1006 +</body>
1007 +</html>
added assets/poster/pdf/groupe-ka-agregation.pdf +0 −0

Binary file not shown.

added public/documents/groupe-ka-agregation.pdf +0 −0

Binary file not shown.

modified src/app/page.tsx +13 −0
@@ -715,6 +715,19 @@ export default function Home() {
715 715 ramène la donnée brute, et KA la transforme en un seul endroit
716 716 propre. Voici la machine, à découvert.
717 717 </p>
718 + <div className="mt-5 flex flex-wrap items-center gap-3">
719 + <a
720 + href="/documents/groupe-ka-agregation.pdf"
721 + target="_blank"
722 + rel="noopener noreferrer"
723 + className="btn btn-ghost"
724 + >
725 + ⤓ Télécharger « L&apos;agrégation, expliquée » — PDF · 12 pages
726 + </a>
727 + <span className="gk-mono text-[11px] text-ink-3">
728 + Tous les schémas du groupe, en un document
729 + </span>
730 + </div>
718 731 <AggregationFigure />
719 732 <div className="mt-6 grid items-start gap-6 lg:grid-cols-2">
720 733 <PipelineFigure />
721 734