diff --git a/package-lock.json b/package-lock.json
index 8c564f05..2dd723cb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -32,6 +32,7 @@
"@astrojs/check": "^0.9.8",
"@cloudflare/workers-types": "^4.20260317.1",
"ajv": "^8.20.0",
+ "fontkitten": "^1.0.3",
"playwright": "^1.59.1",
"preact-render-to-string": "^6.6.7",
"typescript": "^5.9.3",
diff --git a/package.json b/package.json
index 2c427c21..9e179084 100644
--- a/package.json
+++ b/package.json
@@ -43,6 +43,7 @@
"@astrojs/check": "^0.9.8",
"@cloudflare/workers-types": "^4.20260317.1",
"ajv": "^8.20.0",
+ "fontkitten": "^1.0.3",
"playwright": "^1.59.1",
"preact-render-to-string": "^6.6.7",
"typescript": "^5.9.3",
diff --git a/public/badge/built-with-nan-compact.svg b/public/badge/built-with-nan-compact.svg
new file mode 100644
index 00000000..a5d9ceca
--- /dev/null
+++ b/public/badge/built-with-nan-compact.svg
@@ -0,0 +1,6 @@
+
diff --git a/public/badge/built-with-nan-dark.svg b/public/badge/built-with-nan-dark.svg
new file mode 100644
index 00000000..f3f4bd05
--- /dev/null
+++ b/public/badge/built-with-nan-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/public/badge/built-with-nan-light.svg b/public/badge/built-with-nan-light.svg
new file mode 100644
index 00000000..825ce9cd
--- /dev/null
+++ b/public/badge/built-with-nan-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/scripts/gen-badges.mjs b/scripts/gen-badges.mjs
new file mode 100644
index 00000000..b0230f07
--- /dev/null
+++ b/scripts/gen-badges.mjs
@@ -0,0 +1,104 @@
+/**
+ * Genera los badges "Built with NaN" que la comunidad pega en sus READMEs.
+ *
+ * node scripts/gen-badges.mjs
+ *
+ * Salida: public/badge/built-with-nan-{dark,light,compact}.svg, servidos en
+ * https://nan.builders/badge/… (ver /docs/built-with-nan).
+ *
+ * Los SVG son autocontenidos: GitHub los muestra dentro de un
, donde no
+ * se cargan fuentes externas, así que "BUILT WITH" va convertido a trazos con
+ * fontkitten (JetBrains Mono de @fontsource) y "NaN" es el primer trazo del
+ * wordmark vectorial de public/brand. El resto del wordmark es ".builders".
+ *
+ * Las URLs publicadas son contrato: hay READMEs ajenos apuntando a ellas y
+ * buscarlas en GitHub es como encontramos los proyectos. Se puede cambiar el
+ * diseño, no el nombre de los ficheros.
+ */
+import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
+import { dirname, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { create } from 'fontkitten';
+
+const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
+
+// tokens (src/styles/nan-system.css)
+const BG = '#0B0B0C';
+const SNOW = '#EFEEEB';
+const SNOW_INK = '#101010';
+const VIOLET = '#7D39EB';
+const VIOLET_2 = '#9B6BF0';
+
+// isotipo (public/brand/nan-isotipo-color.svg, viewBox 100x100)
+const HEX = 'M54.33 11.5 L81.17 27 Q85.5 29.5 85.5 34.5 L85.5 65.5 Q85.5 70.5 81.17 73 L54.33 88.5 Q50 91 45.67 88.5 L18.83 73 Q14.5 70.5 14.5 65.5 L14.5 34.5 Q14.5 29.5 18.83 27 L45.67 11.5 Q50 9 54.33 11.5 Z';
+const INF = 'M24 50 C24 42.5 29.5 38 36 38 C43 38 46.5 43.5 50 50 C53.5 56.5 57 62 64 62 C70.5 62 76 57.5 76 50 C76 42.5 70.5 38 64 38 C57 38 53.5 43.5 50 50 C46.5 56.5 43 62 36 62 C29.5 62 24 57.5 24 50 Z';
+
+const iso = (x, y, size, color, sw = 4.5) =>
+ ``;
+
+/** Texto a trazos. x,y = origen de la línea base; size en px; tracking en em. */
+function text(font, str, x, y, size, fill, tracking = 0) {
+ const s = size / font.unitsPerEm;
+ let cx = 0;
+ const parts = [];
+ for (const g of font.glyphsForString(str)) {
+ const d = g.path.toSVG();
+ if (d) parts.push(``);
+ cx += g.advanceWidth + tracking * font.unitsPerEm;
+ }
+ return { svg: `${parts.join('')}`, width: (cx - tracking * font.unitsPerEm) * s };
+}
+
+const svg = (w, h, body) =>
+ `\n`;
+
+/** Devuelve { 'built-with-nan-dark.svg': '