SPB Git forge

spb/ka-ui

Public
30commits 1branches 0releases
145.7 MBsize
maindefault branch
27 days agolast push
Python 33.5% JavaScript 30.1% TypeScript 25% CSS 10% Shell 1.4%

Assets de marque (og.png 1200x630 « par Groupe KA », favicon.svg, apple-touch-icon) x12 + générateur Playwright + validateur responsive

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

73 changed files +226 −0

added assets/api-ka/apple-touch-icon.png +0 −0

Binary file not shown.

added assets/api-ka/favicon.svg +5 −0
@@ -0,0 +1,5 @@
1 +<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
2 +<rect width="64" height="64" rx="14" fill="#141814"/>
3 +<rect x="4.5" y="4.5" width="55" height="55" rx="11" fill="none" stroke="#3b5bdb" stroke-opacity="0.35" stroke-width="2"/>
4 +<text x="32" y="43" text-anchor="middle" font-family="'Space Grotesk','Arial Black',sans-serif" font-size="30" font-weight="700" letter-spacing="-1" fill="#3b5bdb" transform="rotate(-4 32 32)">Ka</text>
5 +</svg>
\ No newline at end of file
added assets/api-ka/og.png +0 −0

Binary file not shown.

added assets/auto-ka/apple-touch-icon.png +0 −0

Binary file not shown.

added assets/auto-ka/favicon.svg +5 −0
@@ -0,0 +1,5 @@
1 +<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
2 +<rect width="64" height="64" rx="14" fill="#141814"/>
3 +<rect x="4.5" y="4.5" width="55" height="55" rx="11" fill="none" stroke="#ff5a2a" stroke-opacity="0.35" stroke-width="2"/>
4 +<text x="32" y="43" text-anchor="middle" font-family="'Space Grotesk','Arial Black',sans-serif" font-size="30" font-weight="700" letter-spacing="-1" fill="#ff5a2a" transform="rotate(-4 32 32)">Ka</text>
5 +</svg>
\ No newline at end of file
added assets/auto-ka/og.png +0 −0

Binary file not shown.

added assets/crea-ka/apple-touch-icon.png +0 −0

Binary file not shown.

added assets/crea-ka/favicon.svg +5 −0
@@ -0,0 +1,5 @@
1 +<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
2 +<rect width="64" height="64" rx="14" fill="#141814"/>
3 +<rect x="4.5" y="4.5" width="55" height="55" rx="11" fill="none" stroke="#7048e8" stroke-opacity="0.35" stroke-width="2"/>
4 +<text x="32" y="43" text-anchor="middle" font-family="'Space Grotesk','Arial Black',sans-serif" font-size="30" font-weight="700" letter-spacing="-1" fill="#7048e8" transform="rotate(-4 32 32)">Ka</text>
5 +</svg>
\ No newline at end of file
added assets/crea-ka/og.png +0 −0

Binary file not shown.

added assets/fabri-ka/apple-touch-icon.png +0 −0

Binary file not shown.

added assets/fabri-ka/favicon.svg +5 −0
@@ -0,0 +1,5 @@
1 +<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
2 +<rect width="64" height="64" rx="14" fill="#141814"/>
3 +<rect x="4.5" y="4.5" width="55" height="55" rx="11" fill="none" stroke="#c4532e" stroke-opacity="0.35" stroke-width="2"/>
4 +<text x="32" y="43" text-anchor="middle" font-family="'Space Grotesk','Arial Black',sans-serif" font-size="30" font-weight="700" letter-spacing="-1" fill="#c4532e" transform="rotate(-4 32 32)">Ka</text>
5 +</svg>
\ No newline at end of file
added assets/fabri-ka/og.png +0 −0

Binary file not shown.

added assets/food-ka/apple-touch-icon.png +0 −0

Binary file not shown.

added assets/food-ka/favicon.svg +5 −0
@@ -0,0 +1,5 @@
1 +<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
2 +<rect width="64" height="64" rx="14" fill="#141814"/>
3 +<rect x="4.5" y="4.5" width="55" height="55" rx="11" fill="none" stroke="#1f9d55" stroke-opacity="0.35" stroke-width="2"/>
4 +<text x="32" y="43" text-anchor="middle" font-family="'Space Grotesk','Arial Black',sans-serif" font-size="30" font-weight="700" letter-spacing="-1" fill="#1f9d55" transform="rotate(-4 32 32)">Ka</text>
5 +</svg>
\ No newline at end of file
added assets/food-ka/og.png +0 −0

Binary file not shown.

added assets/groupe-ka/apple-touch-icon.png +0 −0

Binary file not shown.

added assets/groupe-ka/favicon.svg +5 −0
@@ -0,0 +1,5 @@
1 +<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
2 +<rect width="64" height="64" rx="14" fill="#141814"/>
3 +<rect x="4.5" y="4.5" width="55" height="55" rx="11" fill="none" stroke="#d9f26b" stroke-opacity="0.35" stroke-width="2"/>
4 +<text x="32" y="43" text-anchor="middle" font-family="'Space Grotesk','Arial Black',sans-serif" font-size="30" font-weight="700" letter-spacing="-1" fill="#d9f26b" transform="rotate(-4 32 32)">KA</text>
5 +</svg>
\ No newline at end of file
added assets/groupe-ka/og.png +0 −0

Binary file not shown.

added assets/immo-ka/apple-touch-icon.png +0 −0

Binary file not shown.

added assets/immo-ka/favicon.svg +5 −0
@@ -0,0 +1,5 @@
1 +<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
2 +<rect width="64" height="64" rx="14" fill="#141814"/>
3 +<rect x="4.5" y="4.5" width="55" height="55" rx="11" fill="none" stroke="#e23744" stroke-opacity="0.35" stroke-width="2"/>
4 +<text x="32" y="43" text-anchor="middle" font-family="'Space Grotesk','Arial Black',sans-serif" font-size="30" font-weight="700" letter-spacing="-1" fill="#e23744" transform="rotate(-4 32 32)">Ka</text>
5 +</svg>
\ No newline at end of file
added assets/immo-ka/og.png +0 −0

Binary file not shown.

added assets/lou-ka/apple-touch-icon.png +0 −0

Binary file not shown.

added assets/lou-ka/favicon.svg +5 −0
@@ -0,0 +1,5 @@
1 +<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
2 +<rect width="64" height="64" rx="14" fill="#141814"/>
3 +<rect x="4.5" y="4.5" width="55" height="55" rx="11" fill="none" stroke="#ff6a00" stroke-opacity="0.35" stroke-width="2"/>
4 +<text x="32" y="43" text-anchor="middle" font-family="'Space Grotesk','Arial Black',sans-serif" font-size="30" font-weight="700" letter-spacing="-1" fill="#ff6a00" transform="rotate(-4 32 32)">Ka</text>
5 +</svg>
\ No newline at end of file
added assets/lou-ka/og.png +0 −0

Binary file not shown.

added assets/resto-ka/apple-touch-icon.png +0 −0

Binary file not shown.

added assets/resto-ka/favicon.svg +5 −0
@@ -0,0 +1,5 @@
1 +<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
2 +<rect width="64" height="64" rx="14" fill="#141814"/>
3 +<rect x="4.5" y="4.5" width="55" height="55" rx="11" fill="none" stroke="#f08c00" stroke-opacity="0.35" stroke-width="2"/>
4 +<text x="32" y="43" text-anchor="middle" font-family="'Space Grotesk','Arial Black',sans-serif" font-size="30" font-weight="700" letter-spacing="-1" fill="#f08c00" transform="rotate(-4 32 32)">Ka</text>
5 +</svg>
\ No newline at end of file
added assets/resto-ka/og.png +0 −0

Binary file not shown.

added assets/sorti-ka/apple-touch-icon.png +0 −0

Binary file not shown.

added assets/sorti-ka/favicon.svg +5 −0
@@ -0,0 +1,5 @@
1 +<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
2 +<rect width="64" height="64" rx="14" fill="#141814"/>
3 +<rect x="4.5" y="4.5" width="55" height="55" rx="11" fill="none" stroke="#d6336c" stroke-opacity="0.35" stroke-width="2"/>
4 +<text x="32" y="43" text-anchor="middle" font-family="'Space Grotesk','Arial Black',sans-serif" font-size="30" font-weight="700" letter-spacing="-1" fill="#d6336c" transform="rotate(-4 32 32)">Ka</text>
5 +</svg>
\ No newline at end of file
added assets/sorti-ka/og.png +0 −0

Binary file not shown.

added assets/trouve-ka/apple-touch-icon.png +0 −0

Binary file not shown.

added assets/trouve-ka/favicon.svg +5 −0
@@ -0,0 +1,5 @@
1 +<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
2 +<rect width="64" height="64" rx="14" fill="#141814"/>
3 +<rect x="4.5" y="4.5" width="55" height="55" rx="11" fill="none" stroke="#1c7ed6" stroke-opacity="0.35" stroke-width="2"/>
4 +<text x="32" y="43" text-anchor="middle" font-family="'Space Grotesk','Arial Black',sans-serif" font-size="30" font-weight="700" letter-spacing="-1" fill="#1c7ed6" transform="rotate(-4 32 32)">Ka</text>
5 +</svg>
\ No newline at end of file
added assets/trouve-ka/og.png +0 −0

Binary file not shown.

added assets/vrai-prix/apple-touch-icon.png +0 −0

Binary file not shown.

added assets/vrai-prix/favicon.svg +5 −0
@@ -0,0 +1,5 @@
1 +<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
2 +<rect width="64" height="64" rx="14" fill="#141814"/>
3 +<rect x="4.5" y="4.5" width="55" height="55" rx="11" fill="none" stroke="#ff5148" stroke-opacity="0.35" stroke-width="2"/>
4 +<text x="32" y="43" text-anchor="middle" font-family="'Space Grotesk','Arial Black',sans-serif" font-size="30" font-weight="700" letter-spacing="-1" fill="#ff5148" transform="rotate(-4 32 32)">Ka</text>
5 +</svg>
\ No newline at end of file
added assets/vrai-prix/og.png +0 −0

Binary file not shown.

added gen-assets.mjs +87 −0
@@ -0,0 +1,87 @@
1 +// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 +// ka-ui/gen-assets.mjs — génère pour chaque site : og.png (1200×630, partage
3 +// social « Nom — par Groupe KA »), favicon.svg et apple-touch-icon.png (180²),
4 +// aux couleurs de la marque (ecosystem.json). Sortie : ka-ui/assets/<id>/.
5 +// Usage : node gen-assets.mjs (node_modules playwright requis — symlink ora-ka)
6 +import { chromium } from "playwright";
7 +import { mkdirSync, readFileSync, writeFileSync } from "fs";
8 +import { dirname, join } from "path";
9 +import { fileURLToPath } from "url";
10 +
11 +const here = dirname(fileURLToPath(import.meta.url));
12 +const eco = JSON.parse(readFileSync(join(here, "ecosystem.json"), "utf8"));
13 +
14 +const GRAIN =
15 + "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.07'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)'/%3E%3C/svg%3E\")";
16 +
17 +// wordmark scindé : « Lou·Ka » → [« Lou », « Ka »] ; « Vrai-Prix » → [« Vrai-Prix »]
18 +function splitMark(w) {
19 + if (w.includes("·")) {
20 + const [l, r] = w.split("·");
21 + return { left: l, boxed: r };
22 + }
23 + if (w === "Groupe KA") return { left: "Groupe", boxed: "KA" };
24 + return { left: w, boxed: null };
25 +}
26 +
27 +function ogHtml(s) {
28 + const m = splitMark(s.wordmark);
29 + return `<!doctype html><html><head><meta charset="utf-8">
30 +<style>
31 +@font-face { font-family:'Space Grotesk'; src: local('Space Grotesk'); }
32 +* { margin:0; box-sizing:border-box; }
33 +body { width:1200px; height:630px; background:#f5f3ee; font-family:'Space Grotesk','Inter',system-ui,sans-serif; color:#141814; position:relative; overflow:hidden; }
34 +body::before { content:""; position:absolute; inset:0; background-image:${GRAIN}; opacity:.5; z-index:10; }
35 +.frame { position:absolute; inset:26px; border:3px solid #141814; border-radius:18px; }
36 +.kicker { position:absolute; top:64px; left:72px; font-family:'JetBrains Mono',monospace; font-size:20px; font-weight:700; letter-spacing:.14em; text-transform:uppercase; color:#1c5c41; display:flex; align-items:center; gap:14px; }
37 +.kicker::before { content:""; width:34px; height:3px; background:#1c5c41; }
38 +.mark { position:absolute; top:200px; left:72px; font-size:120px; font-weight:700; letter-spacing:-0.045em; display:flex; align-items:baseline; gap:10px; }
39 +.boxed { display:inline-block; background:#141814; color:${s.accent}; border-radius:18px; padding:0 26px 10px; transform:rotate(-2deg); }
40 +.tag { position:absolute; top:370px; left:72px; max-width:900px; font-family:'Inter',system-ui,sans-serif; font-size:31px; line-height:1.3; color:#4d5551; }
41 +.bottom { position:absolute; left:0; right:0; bottom:0; height:92px; background:#141814; display:flex; align-items:center; justify-content:space-between; padding:0 72px; }
42 +.par { color:#f5f3ee; font-size:27px; font-weight:700; letter-spacing:-0.02em; }
43 +.par .ka { color:${s.accent}; }
44 +.dom { font-family:'JetBrains Mono',monospace; font-size:21px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; color:${s.accent}; }
45 +.swatch { position:absolute; top:64px; right:72px; display:flex; gap:10px; }
46 +.swatch span { width:34px; height:34px; border:3px solid #141814; border-radius:9px; }
47 +</style></head><body>
48 +<div class="frame"></div>
49 +<p class="kicker">Écosystème ·Ka · Québec</p>
50 +<div class="swatch"><span style="background:${s.accent}"></span><span style="background:#141814"></span><span style="background:#ffffff"></span></div>
51 +<h1 class="mark">${m.left}${m.boxed ? `<span class="boxed">${m.boxed}</span>` : ""}</h1>
52 +<p class="tag">${s.tagline}.</p>
53 +<div class="bottom"><span class="par">par Groupe <span class="ka">KA</span></span><span class="dom">${s.domain}</span></div>
54 +</body></html>`;
55 +}
56 +
57 +function faviconSvg(s) {
58 + const m = splitMark(s.wordmark);
59 + const letters = m.boxed === "KA" ? "KA" : "Ka";
60 + return `<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
61 +<rect width="64" height="64" rx="14" fill="#141814"/>
62 +<rect x="4.5" y="4.5" width="55" height="55" rx="11" fill="none" stroke="${s.accent}" stroke-opacity="0.35" stroke-width="2"/>
63 +<text x="32" y="43" text-anchor="middle" font-family="'Space Grotesk','Arial Black',sans-serif" font-size="30" font-weight="700" letter-spacing="-1" fill="${s.accent}" transform="rotate(-4 32 32)">${letters}</text>
64 +</svg>`;
65 +}
66 +
67 +const browser = await chromium.launch();
68 +const page = await browser.newPage({ viewport: { width: 1200, height: 630 } });
69 +for (const s of eco.sites) {
70 + const dir = join(here, "assets", s.id);
71 + mkdirSync(dir, { recursive: true });
72 + await page.setContent(ogHtml(s), { waitUntil: "networkidle" });
73 + await page.screenshot({ path: join(dir, "og.png") });
74 + writeFileSync(join(dir, "favicon.svg"), faviconSvg(s));
75 + console.log("✓", s.id);
76 +}
77 +// apple-touch-icon 180×180 depuis le favicon
78 +const icon = await browser.newPage({ viewport: { width: 180, height: 180 } });
79 +for (const s of eco.sites) {
80 + const dir = join(here, "assets", s.id);
81 + await icon.setContent(
82 + `<body style="margin:0">${readFileSync(join(dir, "favicon.svg"), "utf8").replace('width="64" height="64"', 'width="180" height="180"')}</body>`,
83 + );
84 + await icon.screenshot({ path: join(dir, "apple-touch-icon.png"), omitBackground: true });
85 +}
86 +await browser.close();
87 +console.log("Assets générés dans ka-ui/assets/");
added screenshots/api-ka-desktop.png +0 −0

Binary file not shown.

added screenshots/api-ka-mobile.png +0 −0

Binary file not shown.

added screenshots/api-ka-tablette.png +0 −0

Binary file not shown.

added screenshots/auto-ka-desktop.png +0 −0

Binary file not shown.

added screenshots/auto-ka-tablette.png +0 −0

Binary file not shown.

added screenshots/crea-ka-desktop.png +0 −0

Binary file not shown.

added screenshots/crea-ka-mobile.png +0 −0

Binary file not shown.

added screenshots/crea-ka-tablette.png +0 −0

Binary file not shown.

added screenshots/fabri-ka-desktop.png +0 −0

Binary file not shown.

added screenshots/fabri-ka-mobile.png +0 −0

Binary file not shown.

added screenshots/fabri-ka-tablette.png +0 −0

Binary file not shown.

added screenshots/food-ka-desktop.png +0 −0

Binary file not shown.

added screenshots/food-ka-mobile.png +0 −0

Binary file not shown.

added screenshots/food-ka-tablette.png +0 −0

Binary file not shown.

added screenshots/groupe-ka-desktop.png +0 −0

Binary file not shown.

added screenshots/groupe-ka-mobile.png +0 −0

Binary file not shown.

added screenshots/groupe-ka-tablette.png +0 −0

Binary file not shown.

added screenshots/immo-ka-desktop.png +0 −0

Binary file not shown.

added screenshots/immo-ka-mobile.png +0 −0

Binary file not shown.

added screenshots/immo-ka-tablette.png +0 −0

Binary file not shown.

added screenshots/lou-ka-desktop.png +0 −0

Binary file not shown.

added screenshots/lou-ka-mobile.png +0 −0

Binary file not shown.

added screenshots/lou-ka-tablette.png +0 −0

Binary file not shown.

added screenshots/resto-ka-desktop.png +0 −0

Binary file not shown.

added screenshots/resto-ka-mobile.png +0 −0

Binary file not shown.

added screenshots/resto-ka-tablette.png +0 −0

Binary file not shown.

added screenshots/sorti-ka-desktop.png +0 −0

Binary file not shown.

added screenshots/sorti-ka-mobile.png +0 −0

Binary file not shown.

added screenshots/sorti-ka-tablette.png +0 −0

Binary file not shown.

added screenshots/trouve-ka-desktop.png +0 −0

Binary file not shown.

added screenshots/trouve-ka-mobile.png +0 −0

Binary file not shown.

added screenshots/trouve-ka-tablette.png +0 −0

Binary file not shown.

added screenshots/vrai-prix-desktop.png +0 −0

Binary file not shown.

added screenshots/vrai-prix-mobile.png +0 −0

Binary file not shown.

added screenshots/vrai-prix-tablette.png +0 −0

Binary file not shown.

added validate.mjs +79 −0
@@ -0,0 +1,79 @@
1 +// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 +// ka-ui/validate.mjs — validation responsive + harmonisation des 12 sites.
3 +// Usage : node validate.mjs [site…] (lancer depuis un dossier où playwright
4 +// est installé, ex. : cd ~/Desktop/ora-ka && node ~/Desktop/ka-ui/validate.mjs)
5 +// Pour chaque site × viewport (360, 768, 1440) : capture d'écran dans
6 +// ka-ui/screenshots/, débordement horizontal, badge/lien Groupe KA, footer
7 +// commun, title « — Un service Groupe KA », bouton KA au premier écran.
8 +import { chromium } from "playwright";
9 +import { mkdirSync, readFileSync } from "fs";
10 +import { dirname, join } from "path";
11 +import { fileURLToPath } from "url";
12 +
13 +const here = dirname(fileURLToPath(import.meta.url));
14 +const eco = JSON.parse(readFileSync(join(here, "ecosystem.json"), "utf8"));
15 +const outDir = join(here, "screenshots");
16 +mkdirSync(outDir, { recursive: true });
17 +
18 +const only = process.argv.slice(2);
19 +const sites = eco.sites.filter((s) => !only.length || only.includes(s.id));
20 +const viewports = [
21 + { name: "mobile", width: 360, height: 780 },
22 + { name: "tablette", width: 768, height: 1024 },
23 + { name: "desktop", width: 1440, height: 900 },
24 +];
25 +
26 +const browser = await chromium.launch();
27 +const rows = [];
28 +for (const site of sites) {
29 + for (const vp of viewports) {
30 + const ctx = await browser.newContext({
31 + viewport: { width: vp.width, height: vp.height },
32 + isMobile: vp.width < 700,
33 + hasTouch: vp.width < 700,
34 + });
35 + const page = await ctx.newPage();
36 + const r = { site: site.id, vp: vp.name, ok: true, issues: [] };
37 + try {
38 + const resp = await page.goto(`https://${site.domain}/`, {
39 + waitUntil: "networkidle",
40 + timeout: 45000,
41 + });
42 + if (!resp || resp.status() !== 200) {
43 + r.ok = false;
44 + r.issues.push(`HTTP ${resp ? resp.status() : "??"}`);
45 + }
46 + await page.waitForTimeout(1200);
47 + const m = await page.evaluate(() => ({
48 + overflow: document.documentElement.scrollWidth - window.innerWidth,
49 + title: document.title,
50 + gk: !!document.querySelector('a[href*="groupe-ka.com"]'),
51 + footer: !!document.querySelector(".ka-footer, footer"),
52 + emails: (document.body.innerHTML.match(/@groupe-ka\.com/g) || []).length,
53 + kaBtn: /Se connecter avec KA|KA ID/i.test(document.body.innerText),
54 + }));
55 + if (m.overflow > 1) { r.ok = false; r.issues.push(`débordement +${m.overflow}px`); }
56 + if (site.id !== "groupe-ka" && !/Un service Groupe KA/i.test(m.title))
57 + r.issues.push(`title: « ${m.title.slice(0, 60)} »`);
58 + if (!m.gk) { r.ok = false; r.issues.push("aucun lien groupe-ka.com"); }
59 + if (!m.footer) { r.ok = false; r.issues.push("footer absent"); }
60 + if (m.emails < 3) r.issues.push(`courriels contact: ${m.emails}/3`);
61 + if (site.id !== "groupe-ka" && !m.kaBtn) r.issues.push("bouton KA non vu (peut être dans menu)");
62 + await page.screenshot({
63 + path: join(outDir, `${site.id}-${vp.name}.png`),
64 + fullPage: vp.name === "desktop",
65 + });
66 + } catch (e) {
67 + r.ok = false;
68 + r.issues.push(String(e).split("\n")[0].slice(0, 100));
69 + }
70 + rows.push(r);
71 + console.log(
72 + `${r.ok && !r.issues.length ? "✅" : r.ok ? "⚠️ " : "❌"} ${site.id.padEnd(10)} ${vp.name.padEnd(8)} ${r.issues.join(" · ")}`
73 + );
74 + await ctx.close();
75 + }
76 +}
77 +await browser.close();
78 +const bad = rows.filter((r) => !r.ok);
79 +console.log(`\n${rows.length} vérifications, ${bad.length} échecs. Captures: ${outDir}`);
80