@7ots/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/.env.example +140 -0
  2. package/LICENSE +21 -0
  3. package/README.md +655 -0
  4. package/brand/apple-touch-icon.png +0 -0
  5. package/brand/favicon.svg +4 -0
  6. package/brand/icon-512.png +0 -0
  7. package/brand/mark-mono.svg +3 -0
  8. package/brand/mark.svg +4 -0
  9. package/brand/og-es.png +0 -0
  10. package/brand/og-pt.png +0 -0
  11. package/brand/og.png +0 -0
  12. package/brand/ots.json +11 -0
  13. package/brand/tokens.css +59 -0
  14. package/brand/wordmark.svg +5 -0
  15. package/cli/7ots.mjs +400 -0
  16. package/cli/lib/brain.mjs +143 -0
  17. package/cli/lib/config.mjs +70 -0
  18. package/cli/lib/hooks.mjs +151 -0
  19. package/cli/lib/i18n.mjs +480 -0
  20. package/cli/lib/identity.mjs +51 -0
  21. package/cli/lib/install.mjs +79 -0
  22. package/cli/lib/lines.mjs +73 -0
  23. package/cli/lib/meet.mjs +166 -0
  24. package/cli/lib/open.mjs +19 -0
  25. package/cli/lib/orquesta.mjs +84 -0
  26. package/cli/lib/paths.mjs +49 -0
  27. package/cli/lib/pet-core.mjs +184 -0
  28. package/cli/lib/pet-server.mjs +296 -0
  29. package/cli/lib/prompts.mjs +75 -0
  30. package/cli/lib/terminal.mjs +145 -0
  31. package/cli/lib/ui.mjs +234 -0
  32. package/cli/lib/wizard.mjs +279 -0
  33. package/cli/pet/electron/main.cjs +86 -0
  34. package/cli/pet/pet.html +404 -0
  35. package/dist/7ots.esm.js +627 -0
  36. package/dist/7ots.esm.js.map +7 -0
  37. package/dist/7ots.iife.js +627 -0
  38. package/dist/7ots.iife.js.map +7 -0
  39. package/llms.txt +57 -0
  40. package/package.json +73 -0
  41. package/server/admin.mjs +213 -0
  42. package/server/apuchat-relay.mjs +170 -0
  43. package/server/channels/agent.mjs +136 -0
  44. package/server/channels/apuchat.mjs +364 -0
  45. package/server/channels/apumail.mjs +211 -0
  46. package/server/channels/index.mjs +69 -0
  47. package/server/contact.mjs +151 -0
  48. package/server/demo.mjs +154 -0
  49. package/server/i18n.mjs +54 -0
  50. package/server/identity.mjs +108 -0
  51. package/server/llm.mjs +413 -0
  52. package/server/platform/auth.mjs +290 -0
  53. package/server/platform/crypto.mjs +80 -0
  54. package/server/platform/db.mjs +130 -0
  55. package/server/platform/routes.mjs +388 -0
  56. package/server/platform/runtime.mjs +180 -0
  57. package/server/platform/store.mjs +331 -0
  58. package/server/sdk.mjs +125 -0
  59. package/server/server.mjs +456 -0
  60. package/server/settings.mjs +177 -0
  61. package/server/tts.mjs +186 -0
  62. package/skills/7ots/SKILL.md +59 -0
  63. package/src/actions/ActionRegistry.js +254 -0
  64. package/src/actions/PageTools.js +270 -0
  65. package/src/actions/builtins.js +565 -0
  66. package/src/auth/AuthManager.js +128 -0
  67. package/src/avatar/AvatarStage.js +473 -0
  68. package/src/character/AvatarEditor.js +829 -0
  69. package/src/character/Character.js +1191 -0
  70. package/src/character/motion.js +794 -0
  71. package/src/character/parts.js +559 -0
  72. package/src/context/ContextManager.js +472 -0
  73. package/src/core/AgentBrain.js +341 -0
  74. package/src/core/AgentWidget.js +1059 -0
  75. package/src/core/EventBus.js +61 -0
  76. package/src/core/Proactivity.js +363 -0
  77. package/src/core/storage.js +35 -0
  78. package/src/i18n/index.js +193 -0
  79. package/src/i18n/messages/character.js +543 -0
  80. package/src/i18n/messages/identity.js +141 -0
  81. package/src/i18n/messages/platform.js +114 -0
  82. package/src/i18n/messages/server.js +630 -0
  83. package/src/i18n/messages/widget.js +362 -0
  84. package/src/identity/ContactCard.js +351 -0
  85. package/src/identity/Face.js +148 -0
  86. package/src/identity/random.js +112 -0
  87. package/src/identity/schema.js +262 -0
  88. package/src/index.js +117 -0
  89. package/src/integrations/apuchat.js +212 -0
  90. package/src/integrations/apumail.js +88 -0
  91. package/src/llm/ProxyLLM.js +87 -0
  92. package/src/mcp/McpClient.js +153 -0
  93. package/src/ui/Companion.js +498 -0
  94. package/src/ui/UIManager.js +425 -0
  95. package/src/ui/VirtualPointer.js +746 -0
  96. package/src/ui/markdown.js +94 -0
  97. package/src/voice/VoiceEngine.js +227 -0
@@ -0,0 +1,559 @@
1
+ /**
2
+ * Catálogo de piezas del personaje 2D de 7ots (arte vectorial propio, MIT).
3
+ *
4
+ * Sistema de coordenadas: viewBox 0 0 200 200. Cada pieza se dibuja centrada en su ancla
5
+ * (0,0) y el renderizador la coloca: ojos y cejas en cada ojo, boca en la boca, accesorios
6
+ * en su ancla (top · eyes · nose · mouth · neck · center · bottom · shoulder).
7
+ *
8
+ * Añadir piezas: push a EYES / MOUTHS / ACCESSORIES… con { id, label, … }. Las funciones
9
+ * devuelven un string SVG; nunca incluyen datos del usuario sin pasar por el saneado de
10
+ * Character.js (colores hex y números).
11
+ *
12
+ * Nombres visibles: el `label` de cada pieza es solo el respaldo en español; la interfaz usa
13
+ * partLabel(t, tipo, id), que lee `character.<tipo>.<id>` del catálogo de i18n
14
+ * (src/i18n/messages/character.js). Una pieza nueva debe añadir su texto en es/en/pt allí
15
+ * (o con addMessages desde el sitio).
16
+ */
17
+
18
+ import '../i18n/messages/character.js';
19
+ import { t as globalT } from '../i18n/index.js';
20
+
21
+ const n = (v) => Math.round(v * 100) / 100;
22
+
23
+ /**
24
+ * Nombre traducido de una pieza del catálogo.
25
+ * @param {(key:string, vars?:object)=>string} [t] traductor (translator(locale)); por defecto el global
26
+ * @param {'shapes'|'eyes'|'brows'|'mouths'|'cheeks'|'patterns'|'finishes'|'accessories'|'groups'|'presets'|'moods'|'gestures'|'morphs'|'effects'} kind
27
+ * @param {string} id
28
+ * @returns {string} el texto traducido, el `label` en español de la pieza o, en último caso, el id
29
+ */
30
+ export function partLabel(t, kind, id) {
31
+ const tr = t || globalT;
32
+ const key = `character.${kind}.${id}`;
33
+ const s = tr(key);
34
+ if (s !== key) return s;
35
+ const list = CATALOG[kind];
36
+ const part = list?.find((x) => (Array.isArray(x) ? x[0] : x.id) === id);
37
+ return (Array.isArray(part) ? part[1] : part?.label) || String(id);
38
+ }
39
+
40
+ // ───────────────────────────── color ─────────────────────────────
41
+
42
+ export function hexToRgb(hex) {
43
+ const h = String(hex).replace('#', '');
44
+ const f = h.length === 3 ? h.split('').map((c) => c + c).join('') : h.padEnd(6, '0').slice(0, 6);
45
+ return [0, 2, 4].map((i) => parseInt(f.slice(i, i + 2), 16) || 0);
46
+ }
47
+ const toHex = (rgb) => `#${rgb.map((v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0')).join('')}`;
48
+ /** Mezcla con blanco (amt>0) o negro (amt<0). */
49
+ export function shade(hex, amt) {
50
+ const t = amt < 0 ? 0 : 255;
51
+ const p = Math.abs(amt);
52
+ return toHex(hexToRgb(hex).map((v) => v + (t - v) * p));
53
+ }
54
+ export function luminance(hex) {
55
+ const [r, g, b] = hexToRgb(hex).map((v) => v / 255);
56
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
57
+ }
58
+
59
+ // ───────────────────────────── formas del cuerpo ─────────────────────────────
60
+
61
+ /** Nº de puntos de control del contorno (editable arrastrando). Simétricos: i ↔ N-i. */
62
+ export const BODY_POINTS = 16;
63
+
64
+ /**
65
+ * Superelipse asimétrica: medias anchuras/alturas distintas arriba y abajo, y "cuadratura" n.
66
+ * @returns {number[][]} 16 puntos [x,y] empezando arriba y en sentido horario
67
+ */
68
+ function superShape({ cx = 100, cy = 112, rxT = 60, rxB = 60, ryT = 66, ryB = 66, nT = 2, nB = 2, tweak } = {}) {
69
+ const pts = [];
70
+ for (let i = 0; i < BODY_POINTS; i++) {
71
+ const a = -Math.PI / 2 + (i / BODY_POINTS) * Math.PI * 2;
72
+ const c = Math.cos(a);
73
+ const s = Math.sin(a);
74
+ const top = s < 0;
75
+ const e = 2 / (top ? nT : nB);
76
+ const rx = top ? rxT : rxB;
77
+ const w = Math.abs(s) < 1e-9 ? rx : rx; // continuidad lateral
78
+ let x = cx + w * Math.sign(c) * Math.abs(c) ** e;
79
+ let y = cy + (top ? ryT : ryB) * Math.sign(s) * Math.abs(s) ** e;
80
+ if (tweak) [x, y] = tweak(i, x, y);
81
+ pts.push([n(x), n(y)]);
82
+ }
83
+ return pts;
84
+ }
85
+
86
+ export const SHAPES = [
87
+ { id: 'pou', label: 'Pou', points: superShape({ rxT: 50, rxB: 66, ryT: 70, ryB: 56, nT: 2.1, nB: 2.6 }) },
88
+ { id: 'round', label: 'Redondo', points: superShape({ rxT: 64, rxB: 64, ryT: 64, ryB: 64 }) },
89
+ { id: 'egg', label: 'Huevo', points: superShape({ rxT: 50, rxB: 60, ryT: 76, ryB: 56 }) },
90
+ { id: 'pear', label: 'Pera', points: superShape({ rxT: 40, rxB: 70, ryT: 72, ryB: 52, nB: 2.4 }) },
91
+ { id: 'bean', label: 'Judía', points: superShape({ rxT: 56, rxB: 56, ryT: 74, ryB: 62, nT: 2.6, nB: 2.6 }) },
92
+ { id: 'squircle', label: 'Cuadrado suave', points: superShape({ rxT: 62, rxB: 62, ryT: 64, ryB: 60, nT: 4, nB: 4 }) },
93
+ { id: 'wide', label: 'Ancho', points: superShape({ rxT: 74, rxB: 78, ryT: 50, ryB: 50, nT: 2.4, nB: 3 }) },
94
+ { id: 'tall', label: 'Alto', points: superShape({ rxT: 46, rxB: 50, ryT: 82, ryB: 70, nT: 2.4, nB: 2.8 }) },
95
+ { id: 'drop', label: 'Gota', points: superShape({ rxT: 34, rxB: 62, ryT: 82, ryB: 56, nT: 1.3, nB: 2.2 }) },
96
+ {
97
+ id: 'ghost',
98
+ label: 'Fantasma',
99
+ points: superShape({
100
+ rxT: 58, rxB: 62, ryT: 70, ryB: 64, nT: 2, nB: 5,
101
+ tweak: (i, x, y) => (i >= 6 && i <= 10 ? [x, y + (i % 2 ? -10 : 6)] : [x, y]),
102
+ }),
103
+ },
104
+ {
105
+ id: 'cat',
106
+ label: 'Orejas',
107
+ points: superShape({
108
+ rxT: 58, rxB: 64, ryT: 64, ryB: 58, nT: 2.4, nB: 2.6,
109
+ tweak: (i, x, y) => (i === 2 || i === 14 ? [x + (i === 2 ? 6 : -6), y - 30] : i === 1 || i === 15 ? [x, y + 6] : [x, y]),
110
+ }),
111
+ },
112
+ {
113
+ id: 'cloud',
114
+ label: 'Nube',
115
+ points: superShape({ rxT: 66, rxB: 70, ryT: 58, ryB: 56, tweak: (i, x, y) => (i % 2 ? [100 + (x - 100) * 0.9, 112 + (y - 112) * 0.9] : [x, y]) }),
116
+ },
117
+ { id: 'blob', label: 'Blandito', points: superShape({ rxT: 58, rxB: 68, ryT: 66, ryB: 58, tweak: (i, x, y) => [x + Math.sin(i * 1.7) * 5, y + Math.cos(i * 2.3) * 4] }) },
118
+ { id: 'robot', label: 'Robot', points: superShape({ rxT: 60, rxB: 60, ryT: 66, ryB: 62, nT: 7, nB: 7 }) },
119
+ {
120
+ id: 'heart',
121
+ label: 'Corazón',
122
+ points: superShape({
123
+ rxT: 70, rxB: 64, ryT: 58, ryB: 70, nT: 2.4, nB: 1.6,
124
+ tweak: (i, x, y) => (i === 0 ? [x, y + 22] : i === 1 || i === 15 ? [x, y + 4] : i === 8 ? [x, y + 6] : [x, y]),
125
+ }),
126
+ },
127
+ { id: 'mushroom', label: 'Seta', points: superShape({ rxT: 78, rxB: 46, ryT: 62, ryB: 64, nT: 2.2, nB: 3.2 }) },
128
+ ];
129
+
130
+ // ───────────────────────────── ojos ─────────────────────────────
131
+ // p = { r, lx, ly (-1..1 mirada), side (-1 izq, 1 der), iris, ink, white, lid (color párpado), mood }
132
+
133
+ const shine = (x, y, r, o = 1) => `<circle cx="${n(x)}" cy="${n(y)}" r="${n(r)}" fill="#fff" opacity="${o}"/>`;
134
+ const star = (r, k = 0.45, pts = 5) => {
135
+ let d = '';
136
+ for (let i = 0; i < pts * 2; i++) {
137
+ const a = -Math.PI / 2 + (i * Math.PI) / pts;
138
+ const rr = i % 2 ? r * k : r;
139
+ d += `${i ? 'L' : 'M'}${n(Math.cos(a) * rr)} ${n(Math.sin(a) * rr)}`;
140
+ }
141
+ return `${d}Z`;
142
+ };
143
+ const heartPath = (r) => `M0 ${n(r * 0.9)}C${n(-r * 1.4)} ${n(-r * 0.1)} ${n(-r * 0.7)} ${n(-r * 1.05)} 0 ${n(-r * 0.35)}C${n(r * 0.7)} ${n(-r * 1.05)} ${n(r * 1.4)} ${n(-r * 0.1)} 0 ${n(r * 0.9)}Z`;
144
+ const look = (p, k) => [n(p.lx * p.r * k), n(p.ly * p.r * k)];
145
+ const pupil = (p, rr, k = 0.35, color = p.ink) => {
146
+ const [x, y] = look(p, k);
147
+ return `<circle cx="${x}" cy="${y}" r="${n(rr)}" fill="${color}"/>${shine(x - rr * 0.35, y - rr * 0.4, rr * 0.32)}`;
148
+ };
149
+
150
+ export const EYES = [
151
+ { id: 'round', label: 'Redondos', draw: (p) => `<circle r="${p.r}" fill="${p.white}"/>${pupil(p, p.r * 0.55)}` },
152
+ { id: 'dot', label: 'Puntitos', draw: (p) => { const [x, y] = look(p, 0.15); return `<ellipse cx="${x}" cy="${y}" rx="${n(p.r * 0.42)}" ry="${n(p.r * 0.58)}" fill="${p.ink}"/>${shine(x - p.r * 0.12, y - p.r * 0.22, p.r * 0.14)}`; } },
153
+ { id: 'kawaii', label: 'Kawaii', draw: (p) => { const [x, y] = look(p, 0.1); return `<circle cx="${x}" cy="${y}" r="${n(p.r * 0.95)}" fill="${p.ink}"/>${shine(x - p.r * 0.32, y - p.r * 0.34, p.r * 0.34)}${shine(x + p.r * 0.34, y + p.r * 0.36, p.r * 0.16)}`; } },
154
+ {
155
+ id: 'anime',
156
+ label: 'Anime',
157
+ draw: (p) => {
158
+ const [x, y] = look(p, 0.22);
159
+ return `<ellipse rx="${n(p.r * 0.9)}" ry="${n(p.r * 1.2)}" fill="${p.white}"/>
160
+ <ellipse cx="${x}" cy="${n(y + p.r * 0.1)}" rx="${n(p.r * 0.66)}" ry="${n(p.r * 0.92)}" fill="${p.iris}"/>
161
+ <ellipse cx="${x}" cy="${n(y + p.r * 0.34)}" rx="${n(p.r * 0.5)}" ry="${n(p.r * 0.5)}" fill="${shade(p.iris, 0.35)}" opacity=".7"/>
162
+ <ellipse cx="${x}" cy="${n(y + p.r * 0.05)}" rx="${n(p.r * 0.3)}" ry="${n(p.r * 0.46)}" fill="${p.ink}"/>
163
+ ${shine(x - p.r * 0.26, y - p.r * 0.36, p.r * 0.24)}${shine(x + p.r * 0.24, y + p.r * 0.4, p.r * 0.1)}
164
+ <path d="M${n(-p.r * 1.05)} ${n(-p.r * 0.7)}Q0 ${n(-p.r * 1.6)} ${n(p.r * 1.05)} ${n(-p.r * 0.7)}" stroke="${p.ink}" stroke-width="${n(p.r * 0.28)}" fill="none" stroke-linecap="round"/>`;
165
+ },
166
+ },
167
+ { id: 'cat', label: 'Gato', draw: (p) => { const [x, y] = look(p, 0.3); return `<ellipse rx="${n(p.r)}" ry="${n(p.r * 0.95)}" fill="${p.iris}"/><ellipse cx="${x}" cy="${y}" rx="${n(p.r * 0.16)}" ry="${n(p.r * 0.8)}" fill="${p.ink}"/>${shine(x - p.r * 0.4, y - p.r * 0.4, p.r * 0.16)}`; } },
168
+ { id: 'sleepy', label: 'Dormilones', draw: (p) => `<circle r="${p.r}" fill="${p.white}"/>${pupil(p, p.r * 0.55, 0.3)}<path d="M${-p.r - 1} ${n(-p.r - 1)}H${p.r + 1}V${n(p.r * 0.05)}Q0 ${n(p.r * 0.35)} ${-p.r - 1} ${n(p.r * 0.05)}Z" fill="${p.lid}"/><path d="M${-p.r} ${n(p.r * 0.05)}Q0 ${n(p.r * 0.35)} ${p.r} ${n(p.r * 0.05)}" stroke="${p.ink}" stroke-width="${n(p.r * 0.14)}" fill="none" stroke-linecap="round"/>` },
169
+ { id: 'happy', label: 'Felices', noBlink: true, draw: (p) => `<path d="M${n(-p.r * 0.9)} ${n(p.r * 0.3)}Q0 ${n(-p.r * 1.1)} ${n(p.r * 0.9)} ${n(p.r * 0.3)}" stroke="${p.ink}" stroke-width="${n(p.r * 0.3)}" fill="none" stroke-linecap="round"/>` },
170
+ { id: 'closed', label: 'Cerrados', noBlink: true, draw: (p) => `<path d="M${n(-p.r * 0.9)} ${n(-p.r * 0.1)}Q0 ${n(p.r * 0.8)} ${n(p.r * 0.9)} ${n(-p.r * 0.1)}" stroke="${p.ink}" stroke-width="${n(p.r * 0.26)}" fill="none" stroke-linecap="round"/>` },
171
+ {
172
+ id: 'wink',
173
+ label: 'Guiño',
174
+ draw: (p) =>
175
+ p.side > 0
176
+ ? `<path d="M${n(-p.r * 0.9)} 0Q0 ${n(-p.r * 0.9)} ${n(p.r * 0.9)} 0" stroke="${p.ink}" stroke-width="${n(p.r * 0.28)}" fill="none" stroke-linecap="round"/>`
177
+ : `<circle r="${p.r}" fill="${p.white}"/>${pupil(p, p.r * 0.55)}`,
178
+ },
179
+ { id: 'star', label: 'Estrellas', draw: (p) => `<path d="${star(p.r * 1.1)}" fill="${p.iris}" stroke="${shade(p.iris, -0.35)}" stroke-width="${n(p.r * 0.12)}" stroke-linejoin="round"/>${shine(-p.r * 0.25, -p.r * 0.3, p.r * 0.16)}` },
180
+ { id: 'heart', label: 'Corazones', draw: (p) => `<path d="${heartPath(p.r * 1.05)}" fill="#ef3b5d"/>${shine(-p.r * 0.4, -p.r * 0.3, p.r * 0.18)}` },
181
+ { id: 'cross', label: 'K.O.', noBlink: true, draw: (p) => `<path d="M${n(-p.r * 0.7)} ${n(-p.r * 0.7)}L${n(p.r * 0.7)} ${n(p.r * 0.7)}M${n(p.r * 0.7)} ${n(-p.r * 0.7)}L${n(-p.r * 0.7)} ${n(p.r * 0.7)}" stroke="${p.ink}" stroke-width="${n(p.r * 0.28)}" stroke-linecap="round"/>` },
182
+ {
183
+ id: 'spiral',
184
+ label: 'Mareado',
185
+ noBlink: true,
186
+ draw: (p) => {
187
+ let d = '';
188
+ for (let t = 0; t <= 14; t += 0.35) {
189
+ const rr = (t / 14) * p.r;
190
+ d += `${t ? 'L' : 'M'}${n(Math.cos(t * p.side) * rr)} ${n(Math.sin(t * p.side) * rr)}`;
191
+ }
192
+ return `<circle r="${p.r}" fill="${p.white}"/><path d="${d}" stroke="${p.ink}" stroke-width="${n(p.r * 0.14)}" fill="none" stroke-linecap="round"/>`;
193
+ },
194
+ },
195
+ { id: 'angry', label: 'Enfadados', draw: (p) => `<circle r="${p.r}" fill="${p.white}"/>${pupil(p, p.r * 0.5, 0.3)}<path d="M${n(-p.r * 1.3 * p.side)} ${n(-p.r * 1.3)}L${n(p.r * 1.3 * p.side)} ${n(-p.r * 1.3)}L${n(p.r * 1.3 * p.side)} ${n(-p.r * 0.05)}Z" fill="${p.lid}"/><path d="M${n(-p.r * 1.1 * p.side)} ${n(-p.r * 0.75)}L${n(p.r * 1.1 * p.side)} ${n(-p.r * 0.05)}" stroke="${p.ink}" stroke-width="${n(p.r * 0.2)}" stroke-linecap="round"/>` },
196
+ { id: 'sad', label: 'Tristes', draw: (p) => `<circle r="${p.r}" fill="${p.white}"/>${pupil(p, p.r * 0.5, 0.3)}<path d="M${n(-p.r * 1.3 * p.side)} ${n(-p.r * 1.3)}L${n(p.r * 1.3 * p.side)} ${n(-p.r * 1.3)}L${n(p.r * 1.3 * p.side)} ${n(-p.r * 0.7)}L${n(-p.r * 1.3 * p.side)} ${n(-p.r * 0.05)}Z" fill="${p.lid}"/><path d="M${n(-p.r * 1.1 * p.side)} ${n(-p.r * 0.1)}L${n(p.r * 1.1 * p.side)} ${n(-p.r * 0.75)}" stroke="${p.ink}" stroke-width="${n(p.r * 0.2)}" stroke-linecap="round"/>` },
197
+ { id: 'wide', label: 'Asombrados', draw: (p) => `<circle r="${n(p.r * 1.15)}" fill="${p.white}" stroke="${p.ink}" stroke-width="${n(p.r * 0.08)}"/>${pupil(p, p.r * 0.28, 0.6)}` },
198
+ { id: 'button', label: 'Botón', noBlink: true, draw: (p) => `<circle r="${p.r}" fill="${p.ink}"/><circle r="${n(p.r * 0.78)}" fill="none" stroke="${shade(p.ink, 0.25)}" stroke-width="${n(p.r * 0.1)}"/>${[[-1, -1], [1, -1], [-1, 1], [1, 1]].map(([a, b]) => `<circle cx="${n(a * p.r * 0.3)}" cy="${n(b * p.r * 0.3)}" r="${n(p.r * 0.14)}" fill="${shade(p.ink, 0.45)}"/>`).join('')}` },
199
+ { id: 'robot', label: 'LED', draw: (p) => `<rect x="${n(-p.r)}" y="${n(-p.r * 0.8)}" width="${n(p.r * 2)}" height="${n(p.r * 1.6)}" rx="${n(p.r * 0.35)}" fill="#0b1220"/><rect x="${n(-p.r * 0.7 + p.lx * p.r * 0.25)}" y="${n(-p.r * 0.5 + p.ly * p.r * 0.2)}" width="${n(p.r * 1.4)}" height="${n(p.r)}" rx="${n(p.r * 0.2)}" fill="${p.iris}"/><rect x="${n(-p.r * 0.7)}" y="${n(-p.r * 0.05)}" width="${n(p.r * 1.4)}" height="${n(p.r * 0.1)}" fill="#0b1220" opacity=".4"/>` },
200
+ { id: 'led', label: 'Barras', draw: (p) => `<rect x="${n(-p.r * 1.1)}" y="${n(-p.r * 0.32)}" width="${n(p.r * 2.2)}" height="${n(p.r * 0.64)}" rx="${n(p.r * 0.32)}" fill="${p.iris}"/><rect x="${n(-p.r * 0.9)}" y="${n(-p.r * 0.18)}" width="${n(p.r * 0.8)}" height="${n(p.r * 0.14)}" rx="1" fill="#fff" opacity=".7"/>` },
201
+ { id: 'cyclops', label: 'Cíclope', single: true, draw: (p) => { const [x, y] = look(p, 0.3); return `<circle r="${n(p.r * 1.7)}" fill="${p.white}" stroke="${shade(p.lid, -0.2)}" stroke-width="${n(p.r * 0.1)}"/><circle cx="${x}" cy="${y}" r="${n(p.r * 0.95)}" fill="${p.iris}"/><circle cx="${x}" cy="${y}" r="${n(p.r * 0.5)}" fill="${p.ink}"/>${shine(x - p.r * 0.4, y - p.r * 0.45, p.r * 0.28)}`; } },
202
+ {
203
+ id: 'lashes',
204
+ label: 'Pestañas',
205
+ draw: (p) =>
206
+ `<circle r="${p.r}" fill="${p.white}"/>${pupil(p, p.r * 0.58)}` +
207
+ [0.15, 0.45, 0.75].map((t) => {
208
+ const a = -Math.PI / 2 + t * p.side * 1.3;
209
+ const x1 = Math.cos(a) * p.r;
210
+ const y1 = Math.sin(a) * p.r;
211
+ return `<path d="M${n(x1)} ${n(y1)}L${n(x1 * 1.45)} ${n(y1 * 1.45)}" stroke="${p.ink}" stroke-width="${n(p.r * 0.16)}" stroke-linecap="round"/>`;
212
+ }).join(''),
213
+ },
214
+ {
215
+ id: 'almond',
216
+ label: 'Realistas',
217
+ draw: (p) => {
218
+ const [x, y] = look(p, 0.3);
219
+ const r = p.r;
220
+ const shape = `M${n(-r * 1.25)} 0Q${n(-r * 0.3)} ${n(-r * 1.15)} ${n(r * 1.25)} ${n(-r * 0.1)}Q${n(r * 0.3)} ${n(r * 0.95)} ${n(-r * 1.25)} 0Z`;
221
+ const id = `al${Math.round(Math.random() * 1e9)}`;
222
+ return `<clipPath id="${id}"><path d="${shape}"/></clipPath><path d="${shape}" fill="${p.white}"/>
223
+ <g clip-path="url(#${id})"><circle cx="${x}" cy="${y}" r="${n(r * 0.72)}" fill="${p.iris}"/><circle cx="${x}" cy="${y}" r="${n(r * 0.72)}" fill="none" stroke="${shade(p.iris, -0.45)}" stroke-width="${n(r * 0.12)}"/><circle cx="${x}" cy="${y}" r="${n(r * 0.34)}" fill="${p.ink}"/>${shine(x - r * 0.26, y - r * 0.28, r * 0.16)}
224
+ <path d="${shape}" fill="none" stroke="${shade(p.lid, -0.25)}" stroke-width="${n(r * 0.4)}" opacity=".35"/></g>
225
+ <path d="M${n(-r * 1.3)} 0Q${n(-r * 0.3)} ${n(-r * 1.2)} ${n(r * 1.3)} ${n(-r * 0.1)}" stroke="${p.ink}" stroke-width="${n(r * 0.2)}" fill="none" stroke-linecap="round"/>`;
226
+ },
227
+ },
228
+ { id: 'googly', label: 'Locos', draw: (p) => `<circle r="${n(p.r * 1.1)}" fill="#fff" stroke="${p.ink}" stroke-width="${n(p.r * 0.1)}"/><circle cx="${n(p.lx * p.r * 0.45)}" cy="${n(p.r * 0.35 + p.ly * p.r * 0.1)}" r="${n(p.r * 0.55)}" fill="${p.ink}"/>` },
229
+ { id: 'sparkle', label: 'Brillantes', draw: (p) => { const [x, y] = look(p, 0.1); return `<circle cx="${x}" cy="${y}" r="${n(p.r)}" fill="${p.ink}"/><path d="${star(p.r * 0.5, 0.25, 4)}" transform="translate(${n(x - p.r * 0.3)} ${n(y - p.r * 0.3)})" fill="#fff"/><path d="${star(p.r * 0.25, 0.3, 4)}" transform="translate(${n(x + p.r * 0.4)} ${n(y + p.r * 0.35)})" fill="#fff"/>`; } },
230
+ { id: 'dollar', label: 'Dólar', draw: (p) => `<circle r="${p.r}" fill="${p.white}"/><text y="${n(p.r * 0.45)}" text-anchor="middle" font-family="Arial Black,Arial,sans-serif" font-weight="900" font-size="${n(p.r * 1.5)}" fill="#16a34a">$</text>` },
231
+ { id: 'pixel', label: 'Píxel', draw: (p) => { const s = p.r * 0.62; const [x, y] = look(p, 0.2); return `<rect x="${n(x - s)}" y="${n(y - s)}" width="${n(s * 2)}" height="${n(s * 2)}" fill="${p.ink}"/><rect x="${n(x - s)}" y="${n(y - s)}" width="${n(s * 0.7)}" height="${n(s * 0.7)}" fill="#fff"/>`; } },
232
+ { id: 'bean', label: 'Alubia', draw: (p) => { const [x, y] = look(p, 0.12); return `<ellipse cx="${x}" cy="${y}" rx="${n(p.r * 0.5)}" ry="${n(p.r * 1.05)}" fill="${p.ink}"/>${shine(x - p.r * 0.15, y - p.r * 0.5, p.r * 0.16)}`; } },
233
+ { id: 'tired', label: 'Cansados', draw: (p) => `<circle r="${p.r}" fill="${p.white}"/>${pupil(p, p.r * 0.5, 0.25)}<path d="M${-p.r - 1} ${n(-p.r - 1)}H${p.r + 1}V${n(-p.r * 0.2)}H${-p.r - 1}Z" fill="${p.lid}"/><path d="M${-p.r} ${n(-p.r * 0.2)}H${p.r}" stroke="${p.ink}" stroke-width="${n(p.r * 0.14)}" stroke-linecap="round"/><path d="M${n(-p.r * 0.8)} ${n(p.r * 1.25)}Q0 ${n(p.r * 1.6)} ${n(p.r * 0.8)} ${n(p.r * 1.25)}" stroke="${shade(p.lid, -0.3)}" stroke-width="${n(p.r * 0.12)}" fill="none" stroke-linecap="round"/>` },
234
+ { id: 'villain', label: 'Villano', draw: (p) => { const [x, y] = look(p, 0.2); return `<path d="M${n(-p.r * 1.2 * p.side)} ${n(-p.r * 0.6)}L${n(p.r * 1.1 * p.side)} ${n(p.r * 0.1)}Q${n(p.r * 0.2 * p.side)} ${n(p.r * 0.9)} ${n(-p.r * 1.1 * p.side)} ${n(p.r * 0.2)}Z" fill="#fde047"/><ellipse cx="${x}" cy="${n(y + p.r * 0.15)}" rx="${n(p.r * 0.14)}" ry="${n(p.r * 0.45)}" fill="${p.ink}"/>`; } },
235
+ { id: 'glow', label: 'Fantasmales', draw: (p) => `<circle r="${n(p.r * 1.25)}" fill="${p.iris}" opacity=".25"/><circle r="${n(p.r * 0.95)}" fill="${p.iris}" opacity=".5"/><ellipse rx="${n(p.r * 0.6)}" ry="${n(p.r * 0.85)}" fill="#fff"/>` },
236
+ { id: 'teary', label: 'Llorosos', draw: (p) => { const [x, y] = look(p, 0.1); return `<circle cx="${x}" cy="${y}" r="${n(p.r * 0.95)}" fill="${p.ink}"/>${shine(x - p.r * 0.32, y - p.r * 0.34, p.r * 0.32)}${shine(x + p.r * 0.3, y + p.r * 0.34, p.r * 0.14)}<path d="M${n(p.r * 0.7 * p.side)} ${n(p.r * 0.9)}q${n(-p.r * 0.35)} ${n(p.r * 0.6)} 0 ${n(p.r * 0.8)}q${n(p.r * 0.35)} ${n(-p.r * 0.2)} 0 ${n(-p.r * 0.8)}Z" fill="#60a5fa"/>`; } },
237
+ { id: 'hypno', label: 'Hipnosis', draw: (p) => `<circle r="${p.r}" fill="#fff"/>${[0.8, 0.55, 0.3].map((k, i) => `<circle r="${n(p.r * k)}" fill="none" stroke="${i % 2 ? p.iris : p.ink}" stroke-width="${n(p.r * 0.14)}"/>`).join('')}` },
238
+ { id: 'focused', label: 'Decididos', draw: (p) => `<ellipse rx="${n(p.r)}" ry="${n(p.r * 0.7)}" fill="${p.white}"/>${pupil(p, p.r * 0.5, 0.3)}<path d="M${n(-p.r * 1.1)} ${n(-p.r * 0.55)}H${n(p.r * 1.1)}" stroke="${p.ink}" stroke-width="${n(p.r * 0.24)}" stroke-linecap="round"/>` },
239
+ ];
240
+
241
+ // ───────────────────────────── cejas ─────────────────────────────
242
+ // p = { r, side, color, tilt (-1 triste … 1 enfadado), lift }
243
+
244
+ const brow = (p, d, w = 0.22) => `<path d="${d}" stroke="${p.color}" stroke-width="${n(p.r * w)}" fill="none" stroke-linecap="round"/>`;
245
+ export const BROWS = [
246
+ { id: 'none', label: 'Sin cejas', draw: () => '' },
247
+ { id: 'soft', label: 'Suaves', draw: (p) => brow(p, `M${n(-p.r * 0.8)} 0Q0 ${n(-p.r * 0.45)} ${n(p.r * 0.8)} 0`) },
248
+ { id: 'thick', label: 'Gruesas', draw: (p) => brow(p, `M${n(-p.r * 0.75)} 0L${n(p.r * 0.75)} 0`, 0.42) },
249
+ { id: 'thin', label: 'Finas', draw: (p) => brow(p, `M${n(-p.r * 0.9)} ${n(p.r * 0.1)}Q0 ${n(-p.r * 0.5)} ${n(p.r * 0.9)} ${n(p.r * 0.1)}`, 0.12) },
250
+ { id: 'arched', label: 'Arqueadas', draw: (p) => brow(p, `M${n(-p.r * 0.9 * p.side)} ${n(p.r * 0.25)}Q${n(p.r * 0.1 * p.side)} ${n(-p.r * 0.7)} ${n(p.r * 0.95 * p.side)} ${n(p.r * 0.05)}`, 0.18) },
251
+ { id: 'bushy', label: 'Pobladas', draw: (p) => brow(p, `M${n(-p.r * 0.9)} ${n(p.r * 0.1)}q${n(p.r * 0.3)} ${n(-p.r * 0.45)} ${n(p.r * 0.6)} ${n(-p.r * 0.1)}q${n(p.r * 0.3)} ${n(-p.r * 0.45)} ${n(p.r * 0.6)} ${n(-p.r * 0.05)}q${n(p.r * 0.3)} ${n(-p.r * 0.3)} ${n(p.r * 0.6)} ${n(p.r * 0.1)}`, 0.34) },
252
+ { id: 'dots', label: 'Maro', draw: (p) => `<ellipse rx="${n(p.r * 0.38)}" ry="${n(p.r * 0.24)}" fill="${p.color}"/>` },
253
+ { id: 'unibrow', label: 'Uniceja', uni: true, draw: (p) => brow(p, `M${n(-p.sp - p.r * 0.9)} ${n(p.r * 0.1)}Q${n(-p.sp / 2)} ${n(-p.r * 0.45)} 0 ${n(p.r * 0.05)}Q${n(p.sp / 2)} ${n(-p.r * 0.45)} ${n(p.sp + p.r * 0.9)} ${n(p.r * 0.1)}`, 0.34) },
254
+ { id: 'angled', label: 'Picudas', draw: (p) => brow(p, `M${n(-p.r * 0.9 * p.side)} ${n(p.r * 0.15)}L${n(p.r * 0.35 * p.side)} ${n(-p.r * 0.35)}L${n(p.r * 0.9 * p.side)} ${n(-p.r * 0.05)}`, 0.2) },
255
+ ];
256
+
257
+ // ───────────────────────────── bocas ─────────────────────────────
258
+ // p = { w (media anchura), open 0..1, curve -1..1 (ánimo), ink (boca), lip, tongue, teeth, color }
259
+
260
+ function mouthShape(w, h, curve) {
261
+ const c = curve * w * 0.35;
262
+ const top = `M${n(-w)} ${n(-c)}Q0 ${n(c * 0.6 - h * 0.15)} ${n(w)} ${n(-c)}`;
263
+ return `${top}Q${n(w * 0.8)} ${n(h + c * 0.2)} 0 ${n(h + c * 0.4)}Q${n(-w * 0.8)} ${n(h + c * 0.2)} ${n(-w)} ${n(-c)}Z`;
264
+ }
265
+ function openMouth(p, { w = p.w, min = 0, max = 1, teeth = false, tongue = true, fangs = false, sharp = false, gap = false, braces = false } = {}) {
266
+ const h = (min + (max - min) * p.open) * w * 1.1;
267
+ const curve = p.curve;
268
+ const id = `m${Math.round(Math.random() * 1e9)}`;
269
+ const shape = mouthShape(w, Math.max(h, 1.5), curve);
270
+ let inner = '';
271
+ if (tongue && h > w * 0.35) inner += `<ellipse cx="0" cy="${n(h + curve * w * 0.35)}" rx="${n(w * 0.55)}" ry="${n(h * 0.45)}" fill="${p.tongue}"/>`;
272
+ if (teeth) {
273
+ const ty = -curve * w * 0.35;
274
+ if (sharp) {
275
+ let d = `M${n(-w)} ${n(ty - 2)}`;
276
+ for (let i = 0; i <= 8; i++) d += `L${n(-w + (i * w) / 4)} ${n(ty + (i % 2 ? w * 0.28 : -1))}`;
277
+ inner += `<path d="${d}V${n(ty - 4)}Z" fill="${p.teeth}"/>`;
278
+ } else inner += `<rect x="${n(-w)}" y="${n(ty - 4)}" width="${n(w * 2)}" height="${n(Math.max(4, w * 0.3) + 4)}" fill="${p.teeth}"/>`;
279
+ if (gap) inner += `<rect x="${n(-w * 0.12)}" y="${n(ty - 4)}" width="${n(w * 0.24)}" height="${n(w * 0.3 + 4)}" fill="${p.ink}"/>`;
280
+ if (braces) inner += `<path d="M${n(-w)} ${n(ty + w * 0.18)}H${n(w)}" stroke="#a3a3a3" stroke-width="2"/>${[-0.6, -0.2, 0.2, 0.6].map((k) => `<rect x="${n(k * w - 1.5)}" y="${n(ty + w * 0.18 - 2)}" width="3" height="4" fill="#d4d4d4"/>`).join('')}`;
281
+ }
282
+ if (fangs) inner += `<path d="M${n(-w * 0.55)} ${n(-curve * w * 0.3 - 1)}l${n(w * 0.12)} ${n(w * 0.4)}l${n(w * 0.12)} ${n(-w * 0.4)}ZM${n(w * 0.31)} ${n(-curve * w * 0.3 - 1)}l${n(w * 0.12)} ${n(w * 0.4)}l${n(w * 0.12)} ${n(-w * 0.4)}Z" fill="${p.teeth}"/>`;
283
+ return `<clipPath id="${id}"><path d="${shape}"/></clipPath><path d="${shape}" fill="${p.ink}"/><g clip-path="url(#${id})">${inner}</g><path d="${shape}" fill="none" stroke="${p.lip}" stroke-width="${n(Math.max(1.4, w * 0.1))}" stroke-linejoin="round"/>`;
284
+ }
285
+ const line = (p, d, wd = 0.18) => `<path d="${d}" stroke="${p.lip}" stroke-width="${n(Math.max(2, p.w * wd))}" fill="none" stroke-linecap="round" stroke-linejoin="round"/>`;
286
+ /** Bocas cerradas: al hablar se abren con una forma genérica. */
287
+ const closedOr = (fn) => (p) => (p.open > 0.12 ? openMouth(p, { w: p.w * 0.8, max: 0.8 }) : fn(p));
288
+
289
+ export const MOUTHS = [
290
+ { id: 'smile', label: 'Sonrisa', draw: closedOr((p) => line(p, `M${n(-p.w)} ${n(-p.w * 0.1 - p.curve * 3)}Q0 ${n(p.w * 0.75 + p.curve * p.w * 0.4)} ${n(p.w)} ${n(-p.w * 0.1 - p.curve * 3)}`)) },
291
+ { id: 'grin', label: 'Sonrisa amplia', draw: (p) => openMouth({ ...p, curve: Math.max(0.4, p.curve + 0.6) }, { min: 0.35, teeth: true }) },
292
+ { id: 'laugh', label: 'Carcajada', draw: (p) => openMouth({ ...p, curve: Math.max(0.5, p.curve + 0.7) }, { min: 0.6, max: 1.1 }) },
293
+ { id: 'open', label: 'Abierta', draw: (p) => `<ellipse cy="${n(p.w * 0.25)}" rx="${n(p.w * 0.55)}" ry="${n(p.w * (0.35 + p.open * 0.5))}" fill="${p.ink}" stroke="${p.lip}" stroke-width="2"/><ellipse cy="${n(p.w * (0.4 + p.open * 0.45))}" rx="${n(p.w * 0.35)}" ry="${n(p.w * 0.18)}" fill="${p.tongue}"/>` },
294
+ { id: 'oh', label: 'Oh', draw: (p) => `<ellipse rx="${n(p.w * 0.3)}" ry="${n(p.w * (0.3 + p.open * 0.3))}" fill="${p.ink}" stroke="${p.lip}" stroke-width="2"/>` },
295
+ { id: 'flat', label: 'Seria', draw: closedOr((p) => line(p, `M${n(-p.w * 0.8)} 0H${n(p.w * 0.8)}`)) },
296
+ { id: 'frown', label: 'Triste', draw: closedOr((p) => line(p, `M${n(-p.w * 0.85)} ${n(p.w * 0.35)}Q0 ${n(-p.w * 0.45)} ${n(p.w * 0.85)} ${n(p.w * 0.35)}`)) },
297
+ { id: 'smirk', label: 'Pícara', draw: closedOr((p) => line(p, `M${n(-p.w * 0.8)} ${n(p.w * 0.12)}Q${n(p.w * 0.2)} ${n(p.w * 0.35)} ${n(p.w * 0.9)} ${n(-p.w * 0.3)}`)) },
298
+ { id: 'cat', label: 'Gatuna', draw: closedOr((p) => line(p, `M${n(-p.w * 0.9)} 0Q${n(-p.w * 0.45)} ${n(p.w * 0.6)} 0 0Q${n(p.w * 0.45)} ${n(p.w * 0.6)} ${n(p.w * 0.9)} 0`, 0.15)) },
299
+ { id: 'tongue', label: 'Lengua fuera', draw: (p) => `<path d="M${n(-p.w * 0.35)} ${n(p.w * 0.12)}v${n(p.w * 0.35)}a${n(p.w * 0.35)} ${n(p.w * 0.35)} 0 0 0 ${n(p.w * 0.7)} 0v${n(-p.w * 0.35)}Z" fill="${p.tongue}" stroke="${shade(p.tongue, -0.3)}" stroke-width="1.5"/><path d="M0 ${n(p.w * 0.2)}v${n(p.w * 0.3)}" stroke="${shade(p.tongue, -0.3)}" stroke-width="1.2"/>${p.open > 0.12 ? openMouth(p, { w: p.w * 0.8, max: 0.7 }) : line(p, `M${n(-p.w)} ${n(-p.w * 0.1)}Q0 ${n(p.w * 0.45)} ${n(p.w)} ${n(-p.w * 0.1)}`)}` },
300
+ { id: 'vampire', label: 'Colmillos', draw: (p) => openMouth({ ...p, curve: Math.max(0.3, p.curve + 0.5) }, { min: 0.25, fangs: true }) },
301
+ { id: 'buck', label: 'Paletas', draw: (p) => `${openMouth({ ...p, curve: Math.max(0.3, p.curve + 0.5) }, { min: 0.12, max: 0.7, tongue: false })}<rect x="${n(-p.w * 0.3)}" y="${n(-p.w * 0.12)}" width="${n(p.w * 0.29)}" height="${n(p.w * 0.45)}" rx="2" fill="${p.teeth}" stroke="#d6d3d1"/><rect x="${n(p.w * 0.01)}" y="${n(-p.w * 0.12)}" width="${n(p.w * 0.29)}" height="${n(p.w * 0.45)}" rx="2" fill="${p.teeth}" stroke="#d6d3d1"/>` },
302
+ { id: 'tiny', label: 'Pequeñita', draw: (p) => (p.open > 0.12 ? `<ellipse rx="${n(p.w * 0.3)}" ry="${n(p.w * p.open * 0.4 + 1)}" fill="${p.ink}"/>` : line(p, `M${n(-p.w * 0.35)} 0Q0 ${n(p.w * 0.35)} ${n(p.w * 0.35)} 0`, 0.14)) },
303
+ { id: 'kiss', label: 'Beso', draw: (p) => (p.open > 0.2 ? `<ellipse rx="${n(p.w * 0.3)}" ry="${n(p.w * 0.35 + p.open * 3)}" fill="${p.ink}" stroke="#e11d48" stroke-width="3"/>` : `<path d="M${n(-p.w * 0.2)} ${n(-p.w * 0.3)}Q${n(p.w * 0.35)} ${n(-p.w * 0.2)} 0 0Q${n(p.w * 0.35)} ${n(p.w * 0.2)} ${n(-p.w * 0.2)} ${n(p.w * 0.3)}" stroke="#e11d48" stroke-width="${n(p.w * 0.2)}" fill="none" stroke-linecap="round"/>`) },
304
+ { id: 'wavy', label: 'Nerviosa', draw: closedOr((p) => line(p, `M${n(-p.w)} 0q${n(p.w / 4)} ${n(-p.w * 0.25)} ${n(p.w / 2)} 0t${n(p.w / 2)} 0t${n(p.w / 2)} 0t${n(p.w / 2)} 0`, 0.14)) },
305
+ {
306
+ id: 'grit',
307
+ label: 'Dientes apretados',
308
+ draw: (p) => {
309
+ const h = p.w * (0.5 + p.open * 0.5);
310
+ return `<rect x="${n(-p.w)}" y="${n(-h / 2)}" width="${n(p.w * 2)}" height="${n(h)}" rx="${n(h * 0.3)}" fill="${p.teeth}" stroke="${p.lip}" stroke-width="2"/><path d="M${n(-p.w)} 0H${n(p.w)}${[-0.5, 0, 0.5].map((k) => `M${n(k * p.w)} ${n(-h / 2)}V${n(h / 2)}`).join('')}" stroke="${p.lip}" stroke-width="1.5"/>`;
311
+ },
312
+ },
313
+ { id: 'beak', label: 'Pico', draw: (p) => { const o = p.open * p.w * 0.5; return `<path d="M${n(-p.w * 0.7)} 0Q0 ${n(-p.w * 0.5)} ${n(p.w * 0.7)} 0L0 ${n(p.w * 0.35)}Z" fill="#f59e0b" transform="translate(0 ${n(-o / 2)})"/><path d="M${n(-p.w * 0.6)} ${n(p.w * 0.05)}L${n(p.w * 0.6)} ${n(p.w * 0.05)}L0 ${n(p.w * 0.55)}Z" fill="#d97706" transform="translate(0 ${n(o / 2)})"/>`; } },
314
+ { id: 'robot', label: 'Rejilla', draw: (p) => { const h = p.w * (0.35 + p.open * 0.6); return `<rect x="${n(-p.w)}" y="${n(-h / 2)}" width="${n(p.w * 2)}" height="${n(h)}" rx="3" fill="#0b1220" stroke="#94a3b8" stroke-width="2"/>${[-0.5, 0, 0.5].map((k) => `<rect x="${n(k * p.w - 1)}" y="${n(-h / 2 + 2)}" width="2" height="${n(h - 4)}" fill="#22d3ee" opacity="${n(0.3 + p.open * 0.7)}"/>`).join('')}`; } },
315
+ { id: 'braces', label: 'Brackets', draw: (p) => openMouth({ ...p, curve: Math.max(0.4, p.curve + 0.6) }, { min: 0.35, teeth: true, braces: true }) },
316
+ { id: 'monster', label: 'Monstruo', draw: (p) => openMouth({ ...p, w: p.w * 1.25, curve: Math.max(0.4, p.curve + 0.6) }, { min: 0.45, teeth: true, sharp: true }) },
317
+ { id: 'gap', label: 'Mellada', draw: (p) => openMouth({ ...p, curve: Math.max(0.4, p.curve + 0.6) }, { min: 0.35, teeth: true, gap: true }) },
318
+ {
319
+ id: 'lips',
320
+ label: 'Labios',
321
+ draw: (p) => {
322
+ const w = p.w;
323
+ const o = p.open * w * 0.55;
324
+ const c = p.curve * w * 0.15;
325
+ return `<path d="M${n(-w)} ${n(-c)}Q${n(-w * 0.5)} ${n(-w * 0.45)} 0 ${n(-w * 0.22)}Q${n(w * 0.5)} ${n(-w * 0.45)} ${n(w)} ${n(-c)}Q0 ${n(-w * 0.05)} ${n(-w)} ${n(-c)}Z" fill="${p.lipColor}"/>
326
+ ${o > 1 ? `<path d="M${n(-w * 0.85)} ${n(-c * 0.8)}Q0 ${n(o * 1.4)} ${n(w * 0.85)} ${n(-c * 0.8)}Z" fill="${p.ink}"/><rect x="${n(-w * 0.5)}" y="${n(-2)}" width="${n(w)}" height="${n(Math.min(o * 0.5, 4))}" fill="${p.teeth}"/>` : ''}
327
+ <path d="M${n(-w)} ${n(-c)}Q0 ${n(w * 0.7 + o)} ${n(w)} ${n(-c)}Q0 ${n(o + 1)} ${n(-w)} ${n(-c)}Z" fill="${shade(p.lipColor, -0.1)}"/>
328
+ <ellipse cx="${n(w * 0.25)}" cy="${n(w * 0.3 + o * 0.8)}" rx="${n(w * 0.22)}" ry="${n(w * 0.07)}" fill="#fff" opacity=".35"/>`;
329
+ },
330
+ },
331
+ { id: 'pout', label: 'Puchero', draw: closedOr((p) => line(p, `M${n(-p.w * 0.45)} ${n(p.w * 0.2)}Q0 ${n(-p.w * 0.25)} ${n(p.w * 0.45)} ${n(p.w * 0.2)}`, 0.2)) },
332
+ { id: 'toothless', label: 'Desdentada', draw: (p) => openMouth({ ...p, curve: Math.max(0.5, p.curve + 0.7) }, { min: 0.45 }) },
333
+ { id: 'whistle', label: 'Silbando', draw: (p) => `<circle cx="${n(p.w * 0.35)}" r="${n(p.w * 0.22 + p.open * 3)}" fill="${p.ink}" stroke="${p.lip}" stroke-width="${n(p.w * 0.14)}"/>` },
334
+ { id: 'drool', label: 'Babeando', draw: (p) => `${openMouth({ ...p, curve: Math.max(0.4, p.curve + 0.6) }, { min: 0.2 })}<path d="M${n(p.w * 0.6)} ${n(p.w * 0.1)}q${n(-p.w * 0.12)} ${n(p.w * 0.5)} 0 ${n(p.w * 0.65)}q${n(p.w * 0.12)} ${n(-p.w * 0.15)} 0 ${n(-p.w * 0.65)}Z" fill="#93c5fd"/>` },
335
+ { id: 'square', label: 'Cuadrada', draw: (p) => { const h = p.w * (0.3 + p.open * 0.7); return `<rect x="${n(-p.w * 0.7)}" y="${n(-h / 2)}" width="${n(p.w * 1.4)}" height="${n(h)}" rx="2" fill="${p.ink}" stroke="${p.lip}" stroke-width="2"/>`; } },
336
+ { id: 'mustachio', label: 'Sonrisa ladeada', draw: (p) => openMouth({ ...p, curve: p.curve + 0.3 }, { min: 0.1, max: 0.8, teeth: true }) },
337
+ ];
338
+
339
+ // ───────────────────────────── mejillas y marcas ─────────────────────────────
340
+ // p = { sp (media distancia entre ojos), r (radio ojo), color }
341
+
342
+ export const CHEEKS = [
343
+ { id: 'none', label: 'Nada', draw: () => '' },
344
+ { id: 'blush', label: 'Rubor', draw: (p) => [-1, 1].map((s) => `<ellipse cx="${n(s * (p.sp + p.r * 0.4))}" cy="${n(p.r * 1.8)}" rx="${n(p.r * 0.8)}" ry="${n(p.r * 0.45)}" fill="${p.color}" opacity=".45"/>`).join('') },
345
+ { id: 'lines', label: 'Rubor anime', draw: (p) => [-1, 1].map((s) => [0, 1, 2].map((i) => `<path d="M${n(s * (p.sp + p.r * 0.1) + (i - 1) * p.r * 0.4)} ${n(p.r * 2.1)}l${n(p.r * 0.25)} ${n(-p.r * 0.5)}" stroke="${p.color}" stroke-width="${n(p.r * 0.12)}" stroke-linecap="round"/>`).join('')).join('') },
346
+ { id: 'freckles', label: 'Pecas', draw: (p) => [-1, 1].map((s) => [[0, 0], [0.5, 0.3], [-0.4, 0.4], [0.2, 0.75], [-0.2, -0.1]].map(([a, b]) => `<circle cx="${n(s * (p.sp + p.r * 0.2) + a * p.r)}" cy="${n(p.r * 1.7 + b * p.r)}" r="${n(p.r * 0.1)}" fill="${shade(p.color, -0.35)}"/>`).join('')).join('') },
347
+ { id: 'hearts', label: 'Corazones', draw: (p) => [-1, 1].map((s) => `<path d="${heartPath(p.r * 0.35)}" transform="translate(${n(s * (p.sp + p.r * 0.5))} ${n(p.r * 1.9)})" fill="${p.color}"/>`).join('') },
348
+ { id: 'whiskers', label: 'Bigotes de gato', draw: (p) => [-1, 1].map((s) => [-0.25, 0, 0.25].map((k) => `<path d="M${n(s * (p.sp * 0.7))} ${n(p.r * 2.4 + k * p.r * 2)}L${n(s * (p.sp + p.r * 2.6))} ${n(p.r * 2.2 + k * p.r * 3.2)}" stroke="#1f2937" stroke-width="${n(p.r * 0.1)}" stroke-linecap="round" opacity=".7"/>`).join('')).join('') },
349
+ { id: 'stars', label: 'Estrellitas', draw: (p) => [-1, 1].map((s) => `<path d="${star(p.r * 0.4)}" transform="translate(${n(s * (p.sp + p.r * 0.6))} ${n(p.r * 1.9)})" fill="#facc15"/>`).join('') },
350
+ { id: 'paint', label: 'Pintura de guerra', draw: (p) => [-1, 1].map((s) => `<path d="M${n(s * (p.sp - p.r * 0.2))} ${n(p.r * 1.4)}h${n(s * p.r * 1.6)}M${n(s * (p.sp - p.r * 0.2))} ${n(p.r * 1.9)}h${n(s * p.r * 1.6)}" stroke="${p.color}" stroke-width="${n(p.r * 0.25)}" stroke-linecap="round"/>`).join('') },
351
+ ];
352
+
353
+ // ───────────────────────────── patrones del cuerpo ─────────────────────────────
354
+ // p = { box {x,y,w,h}, color (patrón) } — se recortan con la silueta
355
+
356
+ export const PATTERNS = [
357
+ { id: 'none', label: 'Liso', draw: () => '' },
358
+ { id: 'belly', label: 'Barriga', draw: (p) => `<ellipse cx="${n(p.box.x + p.box.w / 2)}" cy="${n(p.box.y + p.box.h * 0.8)}" rx="${n(p.box.w * 0.34)}" ry="${n(p.box.h * 0.3)}" fill="${p.color}"/>` },
359
+ { id: 'spots', label: 'Manchas', draw: (p) => [[0.2, 0.3, 0.09], [0.75, 0.25, 0.07], [0.82, 0.62, 0.1], [0.15, 0.72, 0.08], [0.5, 0.9, 0.07], [0.6, 0.12, 0.05]].map(([a, b, r]) => `<circle cx="${n(p.box.x + a * p.box.w)}" cy="${n(p.box.y + b * p.box.h)}" r="${n(r * p.box.w)}" fill="${p.color}"/>`).join('') },
360
+ { id: 'stripes', label: 'Rayas', draw: (p) => Array.from({ length: 7 }, (_, i) => `<rect x="${n(p.box.x - 10)}" y="${n(p.box.y + (i + 0.5) * p.box.h / 7)}" width="${n(p.box.w + 20)}" height="${n(p.box.h / 16)}" fill="${p.color}"/>`).join('') },
361
+ { id: 'tiger', label: 'Tigre', draw: (p) => [0.2, 0.4, 0.6, 0.8].map((b, i) => `<path d="M${n(p.box.x - 4)} ${n(p.box.y + b * p.box.h)}l${n(p.box.w * 0.22)} ${n(p.box.h * 0.04)}l${n(-p.box.w * 0.18)} ${n(p.box.h * 0.05)}ZM${n(p.box.x + p.box.w + 4)} ${n(p.box.y + b * p.box.h + i)}l${n(-p.box.w * 0.22)} ${n(p.box.h * 0.04)}l${n(p.box.w * 0.18)} ${n(p.box.h * 0.05)}Z" fill="${p.color}"/>`).join('') },
362
+ { id: 'twotone', label: 'Dos tonos', draw: (p) => `<rect x="${n(p.box.x - 5)}" y="${n(p.box.y + p.box.h * 0.58)}" width="${n(p.box.w + 10)}" height="${n(p.box.h)}" fill="${p.color}"/>` },
363
+ { id: 'mask', label: 'Antifaz', draw: (p) => `<rect x="${n(p.box.x - 5)}" y="${n(p.eyeY - p.r * 1.7)}" width="${n(p.box.w + 10)}" height="${n(p.r * 3.4)}" rx="${n(p.r)}" fill="${p.color}"/>` },
364
+ { id: 'hearts', label: 'Corazones', draw: (p) => [[0.2, 0.35], [0.8, 0.3], [0.7, 0.75], [0.25, 0.8], [0.5, 0.95]].map(([a, b]) => `<path d="${heartPath(p.box.w * 0.06)}" transform="translate(${n(p.box.x + a * p.box.w)} ${n(p.box.y + b * p.box.h)})" fill="${p.color}"/>`).join('') },
365
+ { id: 'stars', label: 'Estrellas', draw: (p) => [[0.18, 0.3], [0.82, 0.28], [0.75, 0.72], [0.22, 0.78], [0.5, 0.93]].map(([a, b]) => `<path d="${star(p.box.w * 0.06)}" transform="translate(${n(p.box.x + a * p.box.w)} ${n(p.box.y + b * p.box.h)})" fill="${p.color}"/>`).join('') },
366
+ { id: 'circuit', label: 'Circuito', draw: (p) => [0.25, 0.5, 0.75].map((b, i) => `<path d="M${n(p.box.x)} ${n(p.box.y + b * p.box.h)}h${n(p.box.w * 0.18)}l${n(p.box.w * 0.06)} ${n(i % 2 ? 8 : -8)}h${n(p.box.w * 0.1)}M${n(p.box.x + p.box.w)} ${n(p.box.y + b * p.box.h + 6)}h${n(-p.box.w * 0.2)}" stroke="${p.color}" stroke-width="2" fill="none"/><circle cx="${n(p.box.x + p.box.w * 0.34)}" cy="${n(p.box.y + b * p.box.h + (i % 2 ? 8 : -8))}" r="2.5" fill="${p.color}"/>`).join('') },
367
+ ];
368
+
369
+ // ───────────────────────────── acabados ─────────────────────────────
370
+
371
+ export const FINISHES = [
372
+ { id: 'glossy', label: 'Brillante' },
373
+ { id: 'soft3d', label: '3D suave' },
374
+ { id: 'clay', label: 'Plastilina' },
375
+ { id: 'flat', label: 'Plano' },
376
+ { id: 'toon', label: 'Cómic' },
377
+ { id: 'neon', label: 'Neón' },
378
+ { id: 'sketch', label: 'Boceto' },
379
+ ];
380
+
381
+ // ───────────────────────────── accesorios ─────────────────────────────
382
+ // c = { k (escala por ancho de cabeza), w, h (caja del cuerpo), sp, r (ojos), color, color2, eyeDy, mouthDy }
383
+ // Cada uno: { id, label, group, anchor, layer: 'back'|'front', color, color2, draw(c) }
384
+
385
+ const S = (w) => `stroke="#1f2937" stroke-width="${w}" stroke-linejoin="round" stroke-linecap="round"`;
386
+
387
+ export const ACCESSORY_GROUPS = [
388
+ ['head', 'Cabeza'],
389
+ ['eyes', 'Ojos'],
390
+ ['face', 'Cara'],
391
+ ['neck', 'Cuello'],
392
+ ['back', 'Espalda'],
393
+ ['extra', 'Extras'],
394
+ ];
395
+
396
+ export const ACCESSORIES = [
397
+ // ── cabeza ──
398
+ {
399
+ id: 'crown', label: 'Corona', group: 'head', anchor: 'top', color: '#facc15', color2: '#ef4444',
400
+ draw: (c) => `<g transform="scale(${n(c.k)})"><path d="M-32 8L-36 -26L-18 -10L0 -34L18 -10L36 -26L32 8Z" fill="${c.color}" ${S(2.5)}/><rect x="-33" y="2" width="66" height="10" rx="3" fill="${shade(c.color, -0.12)}" ${S(2.5)}/><circle cy="-8" r="5" fill="${c.color2}" stroke="#1f2937" stroke-width="2"/><circle cx="-20" cy="-2" r="3.5" fill="#3b82f6" stroke="#1f2937" stroke-width="1.5"/><circle cx="20" cy="-2" r="3.5" fill="#22c55e" stroke="#1f2937" stroke-width="1.5"/>${[-36, 0, 36].map((x, i) => `<circle cx="${x}" cy="${[-26, -34, -26][i]}" r="3.5" fill="#fff" stroke="#1f2937" stroke-width="1.5"/>`).join('')}<path d="M-26 -2l6 -12" stroke="#fff" stroke-width="2" opacity=".6"/></g>`,
401
+ },
402
+ {
403
+ id: 'tiara', label: 'Tiara', group: 'head', anchor: 'top', color: '#e5e7eb', color2: '#ec4899',
404
+ draw: (c) => `<g transform="scale(${n(c.k)})"><path d="M-30 12Q0 -4 30 12" stroke="${c.color}" stroke-width="5" fill="none" stroke-linecap="round"/><path d="M-14 5L-8 -8L0 -18L8 -8L14 5Q0 0 -14 5Z" fill="${c.color}" ${S(2)}/><path d="${heartPath(6)}" transform="translate(0 -6)" fill="${c.color2}"/><circle cx="-22" cy="7" r="2.5" fill="${c.color2}"/><circle cx="22" cy="7" r="2.5" fill="${c.color2}"/></g>`,
405
+ },
406
+ {
407
+ id: 'pirate', label: 'Sombrero pirata', group: 'head', anchor: 'top', color: '#1f2937', color2: '#f5f5f4',
408
+ draw: (c) => `<g transform="scale(${n(c.k)})"><path d="M-62 8Q-40 -14 -30 -30Q0 -48 30 -30Q40 -14 62 8Q30 -4 0 -2Q-30 -4 -62 8Z" fill="${c.color}" ${S(3)}/><path d="M-56 4Q0 -12 56 4" stroke="#facc15" stroke-width="3.5" fill="none"/><g transform="translate(0 -22)" fill="${c.color2}"><circle r="8"/><rect x="-5" y="4" width="10" height="6" rx="2"/><circle cx="-3" cy="-1" r="2.2" fill="${c.color}"/><circle cx="3" cy="-1" r="2.2" fill="${c.color}"/><path d="M-12 12L12 20M12 12L-12 20" stroke="${c.color2}" stroke-width="3" stroke-linecap="round"/></g></g>`,
409
+ },
410
+ {
411
+ id: 'tophat', label: 'Chistera', group: 'head', anchor: 'top', color: '#111827', color2: '#dc2626',
412
+ draw: (c) => `<g transform="scale(${n(c.k)})"><ellipse cy="6" rx="46" ry="10" fill="${c.color}" ${S(3)}/><path d="M-28 4V-50Q0 -56 28 -50V4Q0 10 -28 4Z" fill="${c.color}" ${S(3)}/><path d="M-28 -8Q0 -2 28 -8V-2Q0 4 -28 -2Z" fill="${c.color2}"/><path d="M-20 -46V-10" stroke="#fff" stroke-width="3" opacity=".18"/></g>`,
413
+ },
414
+ {
415
+ id: 'beanie', label: 'Gorro de lana', group: 'head', anchor: 'top', color: '#ef4444', color2: '#fafafa',
416
+ draw: (c) => `<g transform="scale(${n(c.k)})"><path d="M-46 16Q-48 -34 0 -38Q48 -34 46 16Z" fill="${c.color}" ${S(3)}/>${[-30, -15, 0, 15, 30].map((x) => `<path d="M${x} -30V10" stroke="${shade(c.color, -0.2)}" stroke-width="3"/>`).join('')}<rect x="-50" y="4" width="100" height="18" rx="9" fill="${shade(c.color, -0.15)}" ${S(3)}/><circle cy="-42" r="12" fill="${c.color2}" ${S(3)}/></g>`,
417
+ },
418
+ {
419
+ id: 'cap', label: 'Gorra', group: 'head', anchor: 'top', color: '#2563eb', color2: '#fafafa',
420
+ draw: (c) => `<g transform="scale(${n(c.k)})"><path d="M-44 14Q-46 -34 0 -36Q46 -34 44 14Z" fill="${c.color}" ${S(3)}/><path d="M20 8Q62 4 76 16Q50 24 18 18Z" fill="${shade(c.color, -0.2)}" ${S(3)}/><circle cy="-36" r="5" fill="${c.color2}" ${S(2)}/><path d="M-10 -10h20" stroke="${c.color2}" stroke-width="6" stroke-linecap="round"/></g>`,
421
+ },
422
+ {
423
+ id: 'cowboy', label: 'Vaquero', group: 'head', anchor: 'top', color: '#a16207', color2: '#451a03',
424
+ draw: (c) => `<g transform="scale(${n(c.k)})"><path d="M-70 4Q-64 -10 -50 0Q0 18 50 0Q64 -10 70 4Q60 22 0 22Q-60 22 -70 4Z" fill="${c.color}" ${S(3)}/><path d="M-32 8Q-38 -30 -20 -38Q-6 -30 0 -38Q6 -30 20 -38Q38 -30 32 8Q0 16 -32 8Z" fill="${c.color}" ${S(3)}/><path d="M-32 0Q0 8 32 0V8Q0 16 -32 8Z" fill="${c.color2}"/></g>`,
425
+ },
426
+ {
427
+ id: 'wizard', label: 'Mago', group: 'head', anchor: 'top', color: '#4338ca', color2: '#facc15',
428
+ draw: (c) => `<g transform="scale(${n(c.k)})"><path d="M-40 8Q-12 -30 8 -92Q18 -60 40 8Z" fill="${c.color}" ${S(3)}/><ellipse cy="10" rx="58" ry="12" fill="${c.color}" ${S(3)}/><path d="${star(8)}" transform="translate(-6 -30)" fill="${c.color2}"/><path d="${star(5)}" transform="translate(12 -52)" fill="${c.color2}"/><circle cx="-14" cy="-6" r="2.5" fill="${c.color2}"/><path d="M-6 -6a8 8 0 1 0 12 -8a6 6 0 1 1 -12 8" fill="${c.color2}"/></g>`,
429
+ },
430
+ {
431
+ id: 'party', label: 'Fiesta', group: 'head', anchor: 'top', color: '#ec4899', color2: '#facc15',
432
+ draw: (c) => `<g transform="scale(${n(c.k)}) rotate(12)"><path d="M-24 10L4 -60L28 10Z" fill="${c.color}" ${S(3)}/><path d="M-16 -10L20 -2M-8 -30L14 -24M-18 6L26 6" stroke="${c.color2}" stroke-width="5"/><circle cx="4" cy="-62" r="8" fill="${c.color2}" ${S(2.5)}/></g>`,
433
+ },
434
+ {
435
+ id: 'chef', label: 'Chef', group: 'head', anchor: 'top', color: '#fafafa', color2: '#e5e7eb',
436
+ draw: (c) => `<g transform="scale(${n(c.k)})"><path d="M-30 12V-16Q-50 -24 -40 -44Q-30 -58 -14 -50Q-6 -66 10 -60Q24 -68 34 -52Q52 -42 34 -18V12Z" fill="${c.color}" ${S(3)}/><rect x="-32" y="-4" width="66" height="18" rx="4" fill="${c.color2}" ${S(3)}/></g>`,
437
+ },
438
+ {
439
+ id: 'viking', label: 'Vikingo', group: 'head', anchor: 'top', color: '#9ca3af', color2: '#fef3c7',
440
+ draw: (c) => `<g transform="scale(${n(c.k)})"><path d="M-46 14Q-46 -34 0 -36Q46 -34 46 14Z" fill="${c.color}" ${S(3)}/><path d="M-46 8H46" stroke="${shade(c.color, -0.35)}" stroke-width="7"/><path d="M0 -36V12" stroke="${shade(c.color, -0.35)}" stroke-width="6"/>${[-38, -20, 20, 38].map((x) => `<circle cx="${x}" cy="8" r="2.2" fill="#e5e7eb"/>`).join('')}<path d="M-42 -6Q-72 -12 -70 -48Q-60 -24 -38 -22Z" fill="${c.color2}" ${S(3)}/><path d="M42 -6Q72 -12 70 -48Q60 -24 38 -22Z" fill="${c.color2}" ${S(3)}/></g>`,
441
+ },
442
+ {
443
+ id: 'grad', label: 'Graduación', group: 'head', anchor: 'top', color: '#111827', color2: '#facc15',
444
+ draw: (c) => `<g transform="scale(${n(c.k)})"><path d="M-28 -4V12Q0 20 28 12V-4Z" fill="${c.color}" ${S(3)}/><path d="M-56 -14L0 -34L56 -14L0 6Z" fill="${c.color}" ${S(3)}/><path d="M0 -14L36 -4V20" stroke="${c.color2}" stroke-width="3" fill="none"/><path d="M32 20h8l-2 12h-4Z" fill="${c.color2}"/></g>`,
445
+ },
446
+ {
447
+ id: 'beret', label: 'Boina', group: 'head', anchor: 'top', color: '#b91c1c', color2: '#7f1d1d',
448
+ draw: (c) => `<g transform="scale(${n(c.k)}) rotate(-10)"><path d="M-50 4Q-54 -30 -6 -30Q46 -30 48 0Q30 14 -50 4Z" fill="${c.color}" ${S(3)}/><path d="M-2 -30v-8" stroke="#1f2937" stroke-width="4" stroke-linecap="round"/></g>`,
449
+ },
450
+ {
451
+ id: 'bow', label: 'Lazo', group: 'head', anchor: 'top', color: '#f472b6', color2: '#db2777',
452
+ draw: (c) => `<g transform="scale(${n(c.k)}) translate(26 2) rotate(18)"><path d="M0 0Q-26 -24 -30 -4Q-28 16 0 0Z" fill="${c.color}" ${S(2.5)}/><path d="M0 0Q26 -24 30 -4Q28 16 0 0Z" fill="${c.color}" ${S(2.5)}/><circle r="7" fill="${c.color2}" ${S(2.5)}/></g>`,
453
+ },
454
+ {
455
+ id: 'flower', label: 'Flor', group: 'head', anchor: 'top', color: '#fb7185', color2: '#fde047',
456
+ draw: (c) => `<g transform="scale(${n(c.k)}) translate(-24 6)">${[0, 72, 144, 216, 288].map((a) => `<ellipse cx="0" cy="-11" rx="8" ry="11" transform="rotate(${a})" fill="${c.color}" stroke="#1f2937" stroke-width="2"/>`).join('')}<circle r="7" fill="${c.color2}" stroke="#1f2937" stroke-width="2"/></g>`,
457
+ },
458
+ { id: 'halo', label: 'Aureola', group: 'head', anchor: 'top', color: '#fde047', draw: (c) => `<g transform="scale(${n(c.k)})"><ellipse cy="-22" rx="34" ry="9" fill="none" stroke="${c.color}" stroke-width="7"/><ellipse cy="-22" rx="34" ry="9" fill="none" stroke="#fff" stroke-width="2" opacity=".6"/></g>` },
459
+ { id: 'horns', label: 'Cuernos', group: 'head', anchor: 'top', color: '#dc2626', draw: (c) => `<g transform="scale(${n(c.k)})"><path d="M-30 14Q-44 -8 -30 -30Q-28 -8 -12 10Z" fill="${c.color}" ${S(3)}/><path d="M30 14Q44 -8 30 -30Q28 -8 12 10Z" fill="${c.color}" ${S(3)}/></g>` },
460
+ { id: 'antenna', label: 'Antenas', group: 'head', anchor: 'top', color: '#1f2937', color2: '#22d3ee', draw: (c) => `<g transform="scale(${n(c.k)})"><path d="M-14 10Q-18 -14 -30 -28M14 10Q18 -14 30 -28" stroke="${c.color}" stroke-width="3.5" fill="none" stroke-linecap="round"/><circle cx="-30" cy="-30" r="7" fill="${c.color2}" ${S(2.5)}/><circle cx="30" cy="-30" r="7" fill="${c.color2}" ${S(2.5)}/></g>` },
461
+ { id: 'bunny', label: 'Orejas de conejo', group: 'head', anchor: 'top', color: '#f5f5f4', color2: '#fbcfe8', draw: (c) => `<g transform="scale(${n(c.k)})"><g transform="translate(-18 8) rotate(-12)"><ellipse cy="-34" rx="12" ry="36" fill="${c.color}" ${S(3)}/><ellipse cy="-32" rx="6" ry="26" fill="${c.color2}"/></g><g transform="translate(18 8) rotate(14)"><ellipse cy="-34" rx="12" ry="36" fill="${c.color}" ${S(3)}/><ellipse cy="-32" rx="6" ry="26" fill="${c.color2}"/></g></g>` },
462
+ { id: 'catears', label: 'Orejas de gato', group: 'head', anchor: 'top', color: '#1f2937', color2: '#f9a8d4', draw: (c) => `<g transform="scale(${n(c.k)})"><path d="M-44 16L-36 -28L-8 6Z" fill="${c.color}" ${S(3)}/><path d="M-36 6L-32 -14L-18 4Z" fill="${c.color2}"/><path d="M44 16L36 -28L8 6Z" fill="${c.color}" ${S(3)}/><path d="M36 6L32 -14L18 4Z" fill="${c.color2}"/></g>` },
463
+ { id: 'santa', label: 'Papá Noel', group: 'head', anchor: 'top', color: '#dc2626', color2: '#fafafa', draw: (c) => `<g transform="scale(${n(c.k)})"><path d="M-42 10Q-30 -40 10 -44Q44 -44 56 -8Q40 -26 26 -20Q40 0 42 10Z" fill="${c.color}" ${S(3)}/><circle cx="56" cy="-6" r="10" fill="${c.color2}" ${S(2.5)}/><rect x="-48" y="2" width="96" height="18" rx="9" fill="${c.color2}" ${S(3)}/></g>` },
464
+ { id: 'hardhat', label: 'Casco de obra', group: 'head', anchor: 'top', color: '#facc15', draw: (c) => `<g transform="scale(${n(c.k)})"><path d="M-40 12Q-40 -34 0 -34Q40 -34 40 12Z" fill="${c.color}" ${S(3)}/><path d="M-56 12H56Q56 20 0 20Q-56 20 -56 12Z" fill="${shade(c.color, -0.1)}" ${S(3)}/><path d="M-8 -34V10M8 -34V10" stroke="${shade(c.color, -0.2)}" stroke-width="4"/></g>` },
465
+ { id: 'headband', label: 'Cinta ninja', group: 'head', anchor: 'eyes', clip: true, color: '#dc2626', color2: '#9ca3af', draw: (c) => `<rect x="${n(-c.w)}" y="${n(-c.r * 3.4)}" width="${n(c.w * 2)}" height="${n(c.r * 1.4)}" fill="${c.color}"/><rect x="-9" y="${n(-c.r * 3.25)}" width="18" height="${n(c.r * 1.1)}" rx="2" fill="${c.color2}"/>`, extra: (c) => `<path d="M${n(c.w * 0.5)} ${n(-c.r * 2.7)}q18 4 26 16M${n(c.w * 0.5)} ${n(-c.r * 2.7)}q22 -2 30 8" stroke="${c.color}" stroke-width="5" fill="none" stroke-linecap="round"/>` },
466
+ { id: 'bandana', label: 'Pañuelo pirata', group: 'head', anchor: 'top', color: '#dc2626', color2: '#fafafa', draw: (c) => `<g transform="scale(${n(c.k)})"><path d="M-48 18Q-50 -30 0 -32Q50 -30 48 18Q0 6 -48 18Z" fill="${c.color}" ${S(3)}/>${[[-24, -14], [0, -20], [22, -8], [-8, 4], [30, 8]].map(([x, y]) => `<circle cx="${x}" cy="${y}" r="3" fill="${c.color2}"/>`).join('')}<path d="M44 12l20 14l-4 -18Z" fill="${c.color}" ${S(2.5)}/></g>` },
467
+ { id: 'helmet', label: 'Casco espacial', group: 'head', anchor: 'center', color: '#e0f2fe', color2: '#94a3b8', draw: (c) => `<circle r="${n(Math.max(c.w, c.h) * 0.62)}" fill="${c.color}" fill-opacity=".22" stroke="${c.color2}" stroke-width="7"/><path d="M${n(-Math.max(c.w, c.h) * 0.4)} ${n(-Math.max(c.w, c.h) * 0.3)}a${n(Math.max(c.w, c.h) * 0.5)} ${n(Math.max(c.w, c.h) * 0.5)} 0 0 1 ${n(Math.max(c.w, c.h) * 0.3)} ${n(-Math.max(c.w, c.h) * 0.2)}" stroke="#fff" stroke-width="6" fill="none" stroke-linecap="round" opacity=".8"/>` },
468
+ { id: 'headphones', label: 'Auriculares', group: 'head', anchor: 'eyes', color: '#111827', color2: '#ef4444', draw: (c) => `<path d="M${n(-c.w * 0.52)} 0Q${n(-c.w * 0.6)} ${n(-c.eyeDy - 16)} 0 ${n(-c.eyeDy - 16)}Q${n(c.w * 0.6)} ${n(-c.eyeDy - 16)} ${n(c.w * 0.52)} 0" stroke="${c.color}" stroke-width="8" fill="none" stroke-linecap="round"/>${[-1, 1].map((s) => `<rect x="${n(s * c.w * 0.52 - 11)}" y="-16" width="22" height="34" rx="10" fill="${c.color2}" ${S(3)}/>`).join('')}` },
469
+ { id: 'hair', label: 'Tupé', group: 'head', anchor: 'top', color: '#78350f', draw: (c) => `<g transform="scale(${n(c.k)})"><path d="M-30 14Q-40 -20 -6 -26Q-10 -40 14 -36Q40 -30 34 14Q24 -8 0 -4Q-18 -2 -30 14Z" fill="${c.color}" ${S(3)}/></g>` },
470
+ { id: 'sprout', label: 'Brote', group: 'head', anchor: 'top', color: '#22c55e', draw: (c) => `<g transform="scale(${n(c.k)})"><path d="M0 6V-20" stroke="#15803d" stroke-width="4" stroke-linecap="round"/><path d="M0 -18Q-24 -34 -28 -14Q-10 -8 0 -18Z" fill="${c.color}" ${S(2.5)}/><path d="M0 -20Q20 -38 28 -20Q12 -10 0 -20Z" fill="${c.color}" ${S(2.5)}/></g>` },
471
+ // ── ojos ──
472
+ { id: 'glasses', label: 'Gafas redondas', group: 'eyes', anchor: 'eyes', color: '#1f2937', draw: (c) => `${[-1, 1].map((s) => `<circle cx="${n(s * c.sp)}" r="${n(c.r * 1.55)}" fill="#fff" fill-opacity=".12" stroke="${c.color}" stroke-width="3.5"/>`).join('')}<path d="M${n(-c.sp + c.r * 1.55)} -2Q0 -8 ${n(c.sp - c.r * 1.55)} -2" stroke="${c.color}" stroke-width="3" fill="none"/>` },
473
+ { id: 'square', label: 'Gafas cuadradas', group: 'eyes', anchor: 'eyes', color: '#1f2937', draw: (c) => `${[-1, 1].map((s) => `<rect x="${n(s * c.sp - c.r * 1.7)}" y="${n(-c.r * 1.3)}" width="${n(c.r * 3.4)}" height="${n(c.r * 2.6)}" rx="4" fill="#fff" fill-opacity=".12" stroke="${c.color}" stroke-width="4"/>`).join('')}<path d="M${n(-c.sp + c.r * 1.7)} -3H${n(c.sp - c.r * 1.7)}" stroke="${c.color}" stroke-width="4"/>` },
474
+ { id: 'nerd', label: 'Gafas empollón', group: 'eyes', anchor: 'eyes', color: '#111827', draw: (c) => `${[-1, 1].map((s) => `<rect x="${n(s * c.sp - c.r * 1.6)}" y="${n(-c.r * 1.4)}" width="${n(c.r * 3.2)}" height="${n(c.r * 2.8)}" rx="${n(c.r)}" fill="#fff" fill-opacity=".15" stroke="${c.color}" stroke-width="6"/>`).join('')}<rect x="-6" y="-7" width="12" height="9" fill="#fafafa" ${S(1.5)}/>` },
475
+ { id: 'sunglasses', label: 'Gafas de sol', group: 'eyes', anchor: 'eyes', color: '#0f172a', draw: (c) => `<path d="M${n(-c.sp - c.r * 2)} ${n(-c.r * 1.1)}H${n(c.sp + c.r * 2)}" stroke="${c.color}" stroke-width="4"/>${[-1, 1].map((s) => `<path d="M${n(s * c.sp - c.r * 1.8)} ${n(-c.r * 1.2)}H${n(s * c.sp + c.r * 1.8)}Q${n(s * c.sp + c.r * 1.7)} ${n(c.r * 1.6)} ${n(s * c.sp)} ${n(c.r * 1.5)}Q${n(s * c.sp - c.r * 1.7)} ${n(c.r * 1.6)} ${n(s * c.sp - c.r * 1.8)} ${n(-c.r * 1.2)}Z" fill="${c.color}"/><path d="M${n(s * c.sp - c.r * 1.1)} ${n(-c.r * 0.5)}l${n(c.r * 0.7)} ${n(c.r * 0.9)}" stroke="#fff" stroke-width="2.5" opacity=".5" stroke-linecap="round"/>`).join('')}` },
476
+ { id: 'aviator', label: 'Aviador', group: 'eyes', anchor: 'eyes', color: '#b45309', color2: '#7c2d12', draw: (c) => `${[-1, 1].map((s) => `<path d="M${n(s * c.sp - c.r * 1.8)} ${n(-c.r * 1.1)}H${n(s * c.sp + c.r * 1.8)}Q${n(s * c.sp + c.r * 2)} ${n(c.r * 1.9)} ${n(s * c.sp)} ${n(c.r * 1.7)}Q${n(s * c.sp - c.r * 2)} ${n(c.r * 1.9)} ${n(s * c.sp - c.r * 1.8)} ${n(-c.r * 1.1)}Z" fill="${c.color2}" fill-opacity=".75" stroke="${c.color}" stroke-width="2.5"/>`).join('')}<path d="M${n(-c.sp + c.r * 1.8)} ${n(-c.r * 1.1)}H${n(c.sp - c.r * 1.8)}M${n(-c.sp + c.r * 1.8)} ${n(-c.r * 0.5)}Q0 ${n(-c.r)} ${n(c.sp - c.r * 1.8)} ${n(-c.r * 0.5)}" stroke="${c.color}" stroke-width="2.5" fill="none"/>` },
477
+ { id: 'heartglasses', label: 'Gafas corazón', group: 'eyes', anchor: 'eyes', color: '#ec4899', draw: (c) => `${[-1, 1].map((s) => `<path d="${heartPath(c.r * 1.7)}" transform="translate(${n(s * c.sp)} ${n(-c.r * 0.1)})" fill="${c.color}" fill-opacity=".8" stroke="${shade(c.color, -0.35)}" stroke-width="3"/>`).join('')}<path d="M${n(-c.sp + c.r * 1.5)} ${n(-c.r * 0.6)}H${n(c.sp - c.r * 1.5)}" stroke="${shade(c.color, -0.35)}" stroke-width="3"/>` },
478
+ { id: 'starglasses', label: 'Gafas estrella', group: 'eyes', anchor: 'eyes', color: '#facc15', draw: (c) => `${[-1, 1].map((s) => `<path d="${star(c.r * 2)}" transform="translate(${n(s * c.sp)} 0)" fill="${c.color}" fill-opacity=".85" stroke="${shade(c.color, -0.4)}" stroke-width="2.5" stroke-linejoin="round"/>`).join('')}` },
479
+ { id: '3d', label: 'Gafas 3D', group: 'eyes', anchor: 'eyes', color: '#fafafa', draw: (c) => `<rect x="${n(-c.sp - c.r * 2.2)}" y="${n(-c.r * 1.5)}" width="${n(c.sp * 2 + c.r * 4.4)}" height="${n(c.r * 3)}" rx="3" fill="${c.color}" ${S(2.5)}/><rect x="${n(-c.sp - c.r * 1.5)}" y="${n(-c.r)}" width="${n(c.r * 3)}" height="${n(c.r * 2)}" fill="#ef4444" opacity=".85"/><rect x="${n(c.sp - c.r * 1.5)}" y="${n(-c.r)}" width="${n(c.r * 3)}" height="${n(c.r * 2)}" fill="#06b6d4" opacity=".85"/>` },
480
+ { id: 'monocle', label: 'Monóculo', group: 'eyes', anchor: 'eyes', color: '#ca8a04', draw: (c) => `<circle cx="${n(c.sp)}" r="${n(c.r * 1.6)}" fill="#fff" fill-opacity=".15" stroke="${c.color}" stroke-width="3.5"/><path d="M${n(c.sp + c.r * 1.2)} ${n(c.r * 1.1)}Q${n(c.sp + c.r * 2.6)} ${n(c.r * 4)} ${n(c.sp + c.r * 1.2)} ${n(c.r * 6.5)}" stroke="${c.color}" stroke-width="1.8" fill="none" stroke-dasharray="3 2"/>` },
481
+ { id: 'eyepatch', label: 'Parche', group: 'eyes', anchor: 'eyes', color: '#111827', clipExtra: true, draw: (c) => `<path d="M${n(-c.sp - c.r * 1.6)} ${n(-c.r * 1.1)}Q${n(-c.sp)} ${n(-c.r * 1.9)} ${n(-c.sp + c.r * 1.6)} ${n(-c.r * 1.1)}Q${n(-c.sp + c.r * 1.6)} ${n(c.r * 1.8)} ${n(-c.sp)} ${n(c.r * 1.8)}Q${n(-c.sp - c.r * 1.6)} ${n(c.r * 1.8)} ${n(-c.sp - c.r * 1.6)} ${n(-c.r * 1.1)}Z" fill="${c.color}"/>`, extra: (c) => `<path d="M${n(-c.w * 0.55)} ${n(-c.r * 3)}L${n(c.w * 0.55)} ${n(c.r * 0.4)}" stroke="${c.color}" stroke-width="3"/>` },
482
+ { id: 'goggles', label: 'Gafas de piloto', group: 'eyes', anchor: 'eyes', color: '#78350f', color2: '#7dd3fc', draw: (c) => `<path d="M${n(-c.w * 0.55)} 0H${n(c.w * 0.55)}" stroke="${c.color}" stroke-width="${n(c.r * 1.4)}"/>${[-1, 1].map((s) => `<circle cx="${n(s * c.sp)}" r="${n(c.r * 1.7)}" fill="${c.color2}" fill-opacity=".6" stroke="#a3a3a3" stroke-width="5"/><path d="M${n(s * c.sp - c.r * 0.8)} ${n(-c.r * 0.6)}a${n(c.r)} ${n(c.r)} 0 0 1 ${n(c.r)} ${n(-c.r * 0.4)}" stroke="#fff" stroke-width="2" fill="none" opacity=".8"/>`).join('')}` },
483
+ { id: 'visor', label: 'Visor', group: 'eyes', anchor: 'eyes', color: '#0ea5e9', draw: (c) => `<rect x="${n(-c.sp - c.r * 2.4)}" y="${n(-c.r * 1.4)}" width="${n(c.sp * 2 + c.r * 4.8)}" height="${n(c.r * 2.8)}" rx="${n(c.r * 1.4)}" fill="${c.color}" fill-opacity=".55" stroke="#0f172a" stroke-width="3"/><path d="M${n(-c.sp - c.r * 1.6)} ${n(-c.r * 0.6)}H${n(-c.sp)}" stroke="#fff" stroke-width="3" opacity=".6" stroke-linecap="round"/>` },
484
+ // ── cara ──
485
+ { id: 'mustache', label: 'Bigote', group: 'face', anchor: 'mouth', color: '#3f2a1d', draw: (c) => `<path d="M0 -9Q-10 -16 -20 -12Q-30 -8 -34 -14Q-32 -2 -20 -2Q-8 -2 0 -7Q8 -2 20 -2Q32 -2 34 -14Q30 -8 20 -12Q10 -16 0 -9Z" fill="${c.color}" transform="scale(${n(c.m)})"/>` },
486
+ { id: 'handlebar', label: 'Bigote manillar', group: 'face', anchor: 'mouth', color: '#1f2937', draw: (c) => `<path d="M0 -8Q-14 -16 -26 -8Q-34 -2 -40 -12Q-44 -20 -36 -22Q-42 -14 -34 -10Q-26 -6 -18 -6Q-6 -6 0 -4Q6 -6 18 -6Q26 -6 34 -10Q42 -14 36 -22Q44 -20 40 -12Q34 -2 26 -8Q14 -16 0 -8Z" fill="${c.color}" transform="scale(${n(c.m)})"/>` },
487
+ { id: 'beard', label: 'Barba', group: 'face', anchor: 'mouth', color: '#78350f', draw: (c) => `<path d="M${n(-c.w * 0.42)} -16Q${n(-c.w * 0.46)} 40 0 46Q${n(c.w * 0.46)} 40 ${n(c.w * 0.42)} -16Q${n(c.w * 0.3)} 10 16 8Q0 18 -16 8Q${n(-c.w * 0.3)} 10 ${n(-c.w * 0.42)} -16Z" fill="${c.color}"/><path d="M-14 -8Q0 -14 14 -8" stroke="${c.color}" stroke-width="6" fill="none" stroke-linecap="round"/>` },
488
+ { id: 'wizardbeard', label: 'Barba de mago', group: 'face', anchor: 'mouth', color: '#f5f5f4', draw: (c) => `<path d="M${n(-c.w * 0.4)} -14Q${n(-c.w * 0.4)} 40 -6 78Q0 70 6 78Q${n(c.w * 0.4)} 40 ${n(c.w * 0.4)} -14Q16 10 0 10Q-16 10 ${n(-c.w * 0.4)} -14Z" fill="${c.color}" stroke="#d6d3d1" stroke-width="2"/><path d="M0 -10Q-18 -18 -30 -8Q-18 -2 0 -4Q18 -2 30 -8Q18 -18 0 -10Z" fill="${c.color}" stroke="#d6d3d1" stroke-width="2"/>` },
489
+ { id: 'goatee', label: 'Perilla', group: 'face', anchor: 'mouth', color: '#3f2a1d', draw: (c) => `<path d="M-8 12Q0 34 8 12Z" fill="${c.color}" transform="scale(${n(c.m)})"/>` },
490
+ { id: 'pipe', label: 'Pipa', group: 'face', anchor: 'mouth', color: '#7c2d12', draw: (c) => `<g transform="translate(${n(c.mw * 0.5)} 2) scale(${n(c.m)})"><path d="M0 0Q14 4 22 12" stroke="#1f2937" stroke-width="4" fill="none" stroke-linecap="round"/><path d="M18 6h16v14q0 8 -8 8q-8 0 -8 -8Z" fill="${c.color}" ${S(2.5)}/><ellipse cx="26" cy="6" rx="8" ry="2.5" fill="#292524"/><path d="M28 0q-4 -8 2 -14q6 -6 0 -14" stroke="#d6d3d1" stroke-width="2.5" fill="none" opacity=".7" stroke-linecap="round"/></g>` },
491
+ { id: 'lollipop', label: 'Piruleta', group: 'face', anchor: 'mouth', color: '#ec4899', draw: (c) => `<g transform="translate(${n(c.mw * 0.5)} 0) scale(${n(c.m)})"><path d="M0 0L26 -10" stroke="#fafafa" stroke-width="3" ${''}/><circle cx="32" cy="-13" r="11" fill="${c.color}" ${S(2)}/><path d="M32 -13m-7 0a7 7 0 0 1 14 0a5 5 0 0 1 -10 0a3 3 0 0 1 6 0" stroke="#fff" stroke-width="2" fill="none"/></g>` },
492
+ { id: 'clownnose', label: 'Nariz de payaso', group: 'face', anchor: 'nose', color: '#ef4444', draw: (c) => `<circle r="${n(9 * c.m)}" fill="${c.color}" ${S(2)}/><circle cx="${n(-3 * c.m)}" cy="${n(-3 * c.m)}" r="${n(2.5 * c.m)}" fill="#fff" opacity=".7"/>` },
493
+ { id: 'nose', label: 'Nariz', group: 'face', anchor: 'nose', color: '#000000', draw: (c) => `<ellipse rx="${n(5 * c.m)}" ry="${n(3.5 * c.m)}" fill="${c.body}" style="filter:brightness(.8)"/><ellipse cx="${n(-1.5 * c.m)}" cy="${n(-1.2 * c.m)}" rx="${n(1.6 * c.m)}" ry="1" fill="#fff" opacity=".5"/>` },
494
+ { id: 'pignose', label: 'Nariz de cerdito', group: 'face', anchor: 'nose', color: '#f9a8d4', draw: (c) => `<ellipse rx="${n(10 * c.m)}" ry="${n(7 * c.m)}" fill="${c.color}" ${S(2)}/><ellipse cx="${n(-3.5 * c.m)}" rx="${n(1.8 * c.m)}" ry="${n(3 * c.m)}" fill="${shade(c.color, -0.45)}"/><ellipse cx="${n(3.5 * c.m)}" rx="${n(1.8 * c.m)}" ry="${n(3 * c.m)}" fill="${shade(c.color, -0.45)}"/>` },
495
+ { id: 'bandaid', label: 'Tirita', group: 'face', anchor: 'eyes', color: '#fcd9b6', draw: (c) => `<g transform="translate(${n(c.sp + c.r * 0.4)} ${n(c.r * 2.4)}) rotate(-30)"><rect x="-12" y="-4.5" width="24" height="9" rx="4.5" fill="${c.color}" stroke="#d6a77a" stroke-width="1.5"/><rect x="-4" y="-4.5" width="8" height="9" fill="${shade(c.color, -0.1)}"/></g>` },
496
+ { id: 'scar', label: 'Cicatriz', group: 'face', anchor: 'eyes', color: '#7f1d1d', draw: (c) => `<g transform="translate(${n(-c.sp)} 0)" stroke="${c.color}" stroke-width="2.5" stroke-linecap="round" opacity=".8"><path d="M-6 ${n(-c.r * 2.6)}L6 ${n(c.r * 2.4)}"/><path d="M-6 -8h8M-4 0h8M-2 8h8"/></g>` },
497
+ { id: 'ninjamask', label: 'Máscara ninja', group: 'face', anchor: 'mouth', clip: true, color: '#111827', draw: (c) => `<rect x="${n(-c.w)}" y="${n(-c.mouthDy + c.r * 1.6)}" width="${n(c.w * 2)}" height="${n(c.h)}" fill="${c.color}"/>` },
498
+ { id: 'facemask', label: 'Mascarilla', group: 'face', anchor: 'mouth', color: '#bae6fd', draw: (c) => `<path d="M${n(-c.mw - 12)} -12Q0 -18 ${n(c.mw + 12)} -12Q${n(c.mw + 10)} 18 0 22Q${n(-c.mw - 10)} 18 ${n(-c.mw - 12)} -12Z" fill="${c.color}" ${S(2)}/><path d="M${n(-c.mw)} -4H${n(c.mw)}M${n(-c.mw + 2)} 5H${n(c.mw - 2)}" stroke="${shade(c.color, -0.2)}" stroke-width="1.5"/>` },
499
+ // ── cuello ──
500
+ { id: 'bowtie', label: 'Pajarita', group: 'neck', anchor: 'neck', color: '#dc2626', draw: (c) => `<path d="M0 0L-22 -12V12Z" fill="${c.color}" ${S(2.5)}/><path d="M0 0L22 -12V12Z" fill="${c.color}" ${S(2.5)}/><rect x="-6" y="-6" width="12" height="12" rx="3" fill="${shade(c.color, -0.2)}" ${S(2.5)}/>` },
501
+ { id: 'tie', label: 'Corbata', group: 'neck', anchor: 'neck', color: '#1d4ed8', draw: (c) => `<path d="M-7 -6H7L5 4H-5Z" fill="${shade(c.color, -0.2)}" ${S(2)}/><path d="M-5 4H5L10 40L0 50L-10 40Z" fill="${c.color}" ${S(2)}/><path d="M-6 16L8 12M-8 28L9 24" stroke="${shade(c.color, 0.35)}" stroke-width="2.5"/>` },
502
+ { id: 'scarf', label: 'Bufanda', group: 'neck', anchor: 'neck', clip: true, color: '#16a34a', color2: '#fafafa', draw: (c) => `<rect x="${n(-c.w)}" y="-8" width="${n(c.w * 2)}" height="16" fill="${c.color}"/>${Array.from({ length: 10 }, (_, i) => `<rect x="${n(-c.w + i * c.w * 0.2)}" y="-8" width="${n(c.w * 0.08)}" height="16" fill="${c.color2}" opacity=".6"/>`).join('')}`, extra: (c) => `<path d="M${n(c.w * 0.2)} 4v34h14v-30Z" fill="${c.color}" ${S(2)}/>` },
503
+ { id: 'pearls', label: 'Collar de perlas', group: 'neck', anchor: 'neck', color: '#fafaf9', draw: (c) => Array.from({ length: 11 }, (_, i) => { const t = (i / 10) * Math.PI; return `<circle cx="${n(-Math.cos(t) * c.w * 0.32)}" cy="${n(Math.sin(t) * 12 - 6)}" r="4.5" fill="${c.color}" stroke="#d6d3d1" stroke-width="1.2"/>`; }).join('') },
504
+ { id: 'medal', label: 'Medalla', group: 'neck', anchor: 'neck', color: '#facc15', color2: '#2563eb', draw: (c) => `<path d="M-14 -10L0 12L14 -10" stroke="${c.color2}" stroke-width="7" fill="none"/><circle cy="20" r="11" fill="${c.color}" ${S(2.5)}/><path d="${star(6)}" transform="translate(0 20)" fill="${shade(c.color, -0.25)}"/>` },
505
+ { id: 'bell', label: 'Cascabel', group: 'neck', anchor: 'neck', color: '#facc15', color2: '#dc2626', clip: true, draw: (c) => `<rect x="${n(-c.w)}" y="-6" width="${n(c.w * 2)}" height="10" fill="${c.color2}"/>`, extra: () => `<circle cy="10" r="8" fill="#facc15" stroke="#1f2937" stroke-width="2"/><path d="M-6 10h12M0 10v6" stroke="#1f2937" stroke-width="1.8"/>` },
506
+ { id: 'ruff', label: 'Gorguera real', group: 'neck', anchor: 'neck', color: '#fafafa', draw: (c) => `<path d="${Array.from({ length: 12 }, (_, i) => { const x = -c.w * 0.4 + (i * c.w * 0.8) / 11; return `${i ? 'L' : 'M'}${n(x)} ${i % 2 ? 10 : -6}`; }).join('')}L${n(c.w * 0.4)} 14L${n(-c.w * 0.4)} 14Z" fill="${c.color}" ${S(2)}/>` },
507
+ // ── espalda ──
508
+ {
509
+ id: 'cape', label: 'Capa real', group: 'back', anchor: 'neck', layer: 'back', color: '#b91c1c', color2: '#fafafa',
510
+ draw: (c) => `<path d="M${n(-c.w * 0.3)} ${n(-c.above * 0.7)}Q${n(-c.w * 0.62)} ${n(-c.above * 0.2)} ${n(-c.w * 0.62)} ${n(c.below)}H${n(c.w * 0.62)}Q${n(c.w * 0.62)} ${n(-c.above * 0.2)} ${n(c.w * 0.3)} ${n(-c.above * 0.7)}Z" fill="${c.color}" ${S(3)}/><path d="M${n(-c.w * 0.64)} ${n(c.below - 7)}H${n(c.w * 0.64)}V${n(c.below + 3)}H${n(-c.w * 0.64)}Z" fill="${c.color2}" ${S(2)}/>${[-0.5, -0.25, 0, 0.25, 0.5].map((k) => `<path d="M${n(k * c.w)} ${n(c.below - 4)}v5" stroke="#111" stroke-width="2.5"/>`).join('')}`,
511
+ },
512
+ { id: 'superhero', label: 'Capa de héroe', group: 'back', anchor: 'neck', layer: 'back', color: '#2563eb', draw: (c) => `<path d="M${n(-c.w * 0.3)} ${n(-c.above * 0.7)}Q${n(-c.w * 0.75)} ${n(0)} ${n(-c.w * 0.66)} ${n(c.below + 6)}Q0 ${n(c.below - 4)} ${n(c.w * 0.66)} ${n(c.below + 6)}Q${n(c.w * 0.75)} 0 ${n(c.w * 0.3)} ${n(-c.above * 0.7)}Z" fill="${c.color}" ${S(3)}/>` },
513
+ { id: 'wings', label: 'Alas de ángel', group: 'back', anchor: 'center', layer: 'back', color: '#fafafa', draw: (c) => [-1, 1].map((s) => `<g transform="translate(${n(s * c.w * 0.4)} -10) scale(${s} 1)"><path d="M0 0Q30 -40 70 -30Q58 -18 66 -10Q50 -4 58 8Q36 10 40 22Q14 18 0 10Z" fill="${c.color}" ${S(2.5)}/></g>`).join('') },
514
+ { id: 'batwings', label: 'Alas de murciélago', group: 'back', anchor: 'center', layer: 'back', color: '#3b0764', draw: (c) => [-1, 1].map((s) => `<g transform="translate(${n(s * c.w * 0.4)} -10) scale(${s} 1)"><path d="M0 0L28 -36L70 -24Q60 -10 64 6Q50 -2 44 12Q34 2 24 16Q16 4 0 10Z" fill="${c.color}" ${S(2.5)}/></g>`).join('') },
515
+ { id: 'tail', label: 'Cola de diablillo', group: 'back', anchor: 'bottom', layer: 'back', color: '#dc2626', draw: (c) => `<path d="M${n(c.w * 0.3)} -12Q${n(c.w * 0.8)} 0 ${n(c.w * 0.62)} -40" stroke="${c.color}" stroke-width="5" fill="none" stroke-linecap="round"/><path d="M${n(c.w * 0.62)} -52L${n(c.w * 0.54)} -36L${n(c.w * 0.72)} -38Z" fill="${c.color}" ${S(2)}/>` },
516
+ { id: 'jetpack', label: 'Mochila cohete', group: 'back', anchor: 'center', layer: 'back', color: '#94a3b8', draw: (c) => [-1, 1].map((s) => `<rect x="${n(s * c.w * 0.42 - 12)}" y="-30" width="24" height="56" rx="10" fill="${c.color}" ${S(2.5)}/><path d="M${n(s * c.w * 0.42 - 8)} 26L${n(s * c.w * 0.42)} 46L${n(s * c.w * 0.42 + 8)} 26Z" fill="#f97316"/>`).join('') },
517
+ // ── extras ──
518
+ { id: 'parrot', label: 'Loro', group: 'extra', anchor: 'shoulder', color: '#16a34a', color2: '#ef4444', draw: () => `<g transform="translate(0 -14)"><path d="M-4 16Q-14 -2 -2 -14Q12 -20 14 -4Q14 10 4 18Z" fill="#16a34a" stroke="#1f2937" stroke-width="2"/><path d="M-2 -14Q6 -24 14 -14" fill="#ef4444" stroke="#1f2937" stroke-width="2"/><circle cx="4" cy="-6" r="2.2" fill="#111"/><path d="M12 -6q8 2 4 8l-4 -2Z" fill="#facc15" stroke="#1f2937" stroke-width="1.5"/><path d="M-4 16l-6 12M2 18l-2 12" stroke="#2563eb" stroke-width="3.5" stroke-linecap="round"/></g>` },
519
+ { id: 'sparkles', label: 'Destellos', group: 'extra', anchor: 'center', color: '#facc15', draw: (c) => [[-0.75, -0.55, 9], [0.8, -0.35, 7], [0.7, 0.55, 6], [-0.8, 0.35, 5]].map(([a, b, r]) => `<path d="${star(r, 0.3, 4)}" transform="translate(${n(a * c.w)} ${n(b * c.h)})" fill="${c.color}"/>`).join('') },
520
+ { id: 'hearts', label: 'Corazones', group: 'extra', anchor: 'top', color: '#f43f5e', draw: () => [[-40, -10, 7], [36, -18, 9], [8, -34, 6]].map(([x, y, r]) => `<path d="${heartPath(r)}" transform="translate(${x} ${y})" fill="#f43f5e"/>`).join('') },
521
+ { id: 'zzz', label: 'Zzz', group: 'extra', anchor: 'top', color: '#60a5fa', draw: (c) => `<text x="30" y="-10" font-family="Arial Black,Arial,sans-serif" font-weight="900" font-size="18" fill="${c.color}">Z</text><text x="46" y="-26" font-family="Arial Black,Arial,sans-serif" font-weight="900" font-size="13" fill="${c.color}">z</text><text x="58" y="-38" font-family="Arial Black,Arial,sans-serif" font-weight="900" font-size="10" fill="${c.color}">z</text>` },
522
+ { id: 'sweat', label: 'Gota de sudor', group: 'extra', anchor: 'eyes', color: '#60a5fa', draw: (c) => `<path d="M${n(c.sp + c.r * 2.4)} ${n(-c.r * 2.4)}q-7 10 0 14q7 -4 0 -14Z" fill="${c.color}" stroke="#1e40af" stroke-width="1.5"/>` },
523
+ ];
524
+
525
+ // ───────────────────────────── estilos (plantillas completas) ─────────────────────────────
526
+
527
+ const acc = (id, o = {}) => ({ id, x: 0, y: 0, scale: 1, rot: 0, ...o });
528
+
529
+ export const PRESETS = [
530
+ { id: 'classic', label: 'Clásico', spec: { body: { shape: 'pou', color: '#a0714f', pattern: 'belly', patternColor: '#c9a27e' }, finish: 'glossy', eyes: { type: 'round' }, mouth: { type: 'smile' }, cheeks: { type: 'blush' } } },
531
+ { id: 'pirate', label: 'Pirata', spec: { body: { shape: 'pou', color: '#e0a97e' }, finish: 'glossy', eyes: { type: 'focused' }, brows: { type: 'thick' }, mouth: { type: 'smirk' }, accessories: [acc('pirate'), acc('eyepatch'), acc('beard', { color: '#7c2d12' }), acc('parrot')] } },
532
+ { id: 'king', label: 'Rey', spec: { body: { shape: 'pou', color: '#f1c27d' }, finish: 'glossy', eyes: { type: 'round' }, brows: { type: 'bushy', color: '#6b4423' }, mouth: { type: 'smile' }, accessories: [acc('cape'), acc('crown'), acc('handlebar', { color: '#6b4423' }), acc('ruff')] } },
533
+ { id: 'queen', label: 'Reina', spec: { body: { shape: 'egg', color: '#f5d0b5' }, finish: 'soft3d', eyes: { type: 'lashes', iris: '#7c3aed' }, mouth: { type: 'lips' }, cheeks: { type: 'blush' }, accessories: [acc('cape', { color: '#7c3aed' }), acc('tiara'), acc('pearls')] } },
534
+ { id: 'wizard', label: 'Mago', spec: { body: { shape: 'bean', color: '#c7d2fe' }, finish: 'soft3d', eyes: { type: 'round' }, brows: { type: 'bushy', color: '#e5e7eb' }, mouth: { type: 'smile' }, accessories: [acc('wizard'), acc('wizardbeard'), acc('sparkles')] } },
535
+ { id: 'chef', label: 'Chef', spec: { body: { shape: 'round', color: '#fcd9b6' }, finish: 'glossy', eyes: { type: 'happy' }, mouth: { type: 'grin' }, cheeks: { type: 'blush' }, accessories: [acc('chef'), acc('mustache'), acc('bowtie')] } },
536
+ { id: 'detective', label: 'Detective', spec: { body: { shape: 'pou', color: '#d6b48c' }, finish: 'clay', eyes: { type: 'focused' }, brows: { type: 'thick' }, mouth: { type: 'flat' }, accessories: [acc('cap', { color: '#78716c' }), acc('monocle'), acc('pipe'), acc('mustache', { color: '#44403c' })] } },
537
+ { id: 'cowboy', label: 'Vaquero', spec: { body: { shape: 'pou', color: '#e8b98a' }, finish: 'clay', eyes: { type: 'focused' }, mouth: { type: 'smirk' }, accessories: [acc('cowboy'), acc('mustache', { color: '#78350f' }), acc('scarf', { color: '#dc2626' })] } },
538
+ { id: 'astronaut', label: 'Astronauta', spec: { body: { shape: 'round', color: '#f5f5f4', pattern: 'twotone', patternColor: '#e2e8f0' }, finish: 'soft3d', eyes: { type: 'kawaii' }, mouth: { type: 'open' }, accessories: [acc('helmet'), acc('jetpack')] } },
539
+ { id: 'robot', label: 'Robot', spec: { body: { shape: 'robot', color: '#94a3b8', pattern: 'circuit', patternColor: '#64748b' }, finish: 'soft3d', eyes: { type: 'robot', iris: '#22d3ee' }, mouth: { type: 'robot' }, accessories: [acc('antenna')] } },
540
+ { id: 'viking', label: 'Vikingo', spec: { body: { shape: 'wide', color: '#f0c9a0' }, finish: 'clay', eyes: { type: 'angry' }, brows: { type: 'thick', color: '#b45309' }, mouth: { type: 'grin' }, accessories: [acc('viking'), acc('beard', { color: '#ea580c' })] } },
541
+ { id: 'ninja', label: 'Ninja', spec: { body: { shape: 'bean', color: '#1f2937' }, finish: 'toon', eyes: { type: 'focused', white: '#ffffff' }, mouth: { type: 'flat' }, accessories: [acc('ninjamask'), acc('headband')] } },
542
+ { id: 'ghost', label: 'Fantasma', spec: { body: { shape: 'ghost', color: '#f8fafc' }, finish: 'soft3d', eyes: { type: 'bean' }, mouth: { type: 'oh' }, cheeks: { type: 'blush' } } },
543
+ { id: 'cat', label: 'Gatito', spec: { body: { shape: 'cat', color: '#f59e0b', pattern: 'tiger', patternColor: '#b45309' }, finish: 'glossy', eyes: { type: 'cat', iris: '#84cc16' }, mouth: { type: 'cat' }, cheeks: { type: 'whiskers' }, accessories: [acc('bell')] } },
544
+ { id: 'devil', label: 'Diablillo', spec: { body: { shape: 'pou', color: '#ef4444' }, finish: 'glossy', eyes: { type: 'villain' }, mouth: { type: 'vampire' }, accessories: [acc('horns', { color: '#7f1d1d' }), acc('tail', { color: '#7f1d1d' }), acc('batwings', { color: '#450a0a' })] } },
545
+ { id: 'angel', label: 'Ángel', spec: { body: { shape: 'round', color: '#fde7d4' }, finish: 'soft3d', eyes: { type: 'happy' }, mouth: { type: 'smile' }, cheeks: { type: 'blush' }, accessories: [acc('wings'), acc('halo')] } },
546
+ { id: 'scientist', label: 'Científica', spec: { body: { shape: 'egg', color: '#e9d5ff' }, finish: 'glossy', eyes: { type: 'round' }, mouth: { type: 'grin' }, accessories: [acc('goggles'), acc('sweat')] } },
547
+ { id: 'rocker', label: 'Rockero', spec: { body: { shape: 'pou', color: '#fbbf24' }, finish: 'toon', eyes: { type: 'round' }, mouth: { type: 'tongue' }, accessories: [acc('hair', { color: '#111827' }), acc('sunglasses'), acc('headphones')] } },
548
+ { id: 'student', label: 'Graduada', spec: { body: { shape: 'pou', color: '#bfdbfe' }, finish: 'glossy', eyes: { type: 'kawaii' }, mouth: { type: 'laugh' }, cheeks: { type: 'blush' }, accessories: [acc('grad'), acc('medal')] } },
549
+ { id: 'clown', label: 'Payaso', spec: { body: { shape: 'round', color: '#fafafa', pattern: 'spots', patternColor: '#f472b6' }, finish: 'glossy', eyes: { type: 'wide' }, mouth: { type: 'laugh' }, cheeks: { type: 'hearts' }, accessories: [acc('party'), acc('clownnose'), acc('ruff', { color: '#facc15' })] } },
550
+ { id: 'monster', label: 'Monstruo', spec: { body: { shape: 'blob', color: '#84cc16', pattern: 'spots', patternColor: '#65a30d' }, finish: 'clay', eyes: { type: 'cyclops', iris: '#f97316' }, mouth: { type: 'monster' }, accessories: [acc('horns', { color: '#fef3c7' })] } },
551
+ { id: 'nerd', label: 'Empollón', spec: { body: { shape: 'tall', color: '#fde68a' }, finish: 'flat', eyes: { type: 'round' }, mouth: { type: 'buck' }, cheeks: { type: 'freckles' }, accessories: [acc('nerd'), acc('bowtie', { color: '#2563eb' })] } },
552
+ { id: 'hero', label: 'Superhéroe', spec: { body: { shape: 'pou', color: '#60a5fa' }, finish: 'toon', eyes: { type: 'focused' }, brows: { type: 'angled' }, mouth: { type: 'grin' }, body2: null, accessories: [acc('superhero', { color: '#dc2626' }), acc('visor', { color: '#dc2626' })] } },
553
+ { id: 'neon', label: 'Neón', spec: { body: { shape: 'drop', color: '#a855f7' }, finish: 'neon', eyes: { type: 'glow', iris: '#22d3ee' }, mouth: { type: 'smile' } } },
554
+ { id: 'santa', label: 'Navidad', spec: { body: { shape: 'round', color: '#fcd9b6' }, finish: 'glossy', eyes: { type: 'happy' }, mouth: { type: 'laugh' }, cheeks: { type: 'blush', color: '#ef4444' }, accessories: [acc('santa'), acc('wizardbeard')] } },
555
+ { id: 'sprout', label: 'Brotecito', spec: { body: { shape: 'pear', color: '#86efac' }, finish: 'glossy', eyes: { type: 'kawaii' }, mouth: { type: 'tiny' }, cheeks: { type: 'blush' }, accessories: [acc('sprout')] } },
556
+ ];
557
+
558
+ /** Listas por tipo (para partLabel). Moods y gestos viven en Character.js y solo tienen catálogo. */
559
+ const CATALOG = { shapes: SHAPES, eyes: EYES, brows: BROWS, mouths: MOUTHS, cheeks: CHEEKS, patterns: PATTERNS, finishes: FINISHES, accessories: ACCESSORIES, groups: ACCESSORY_GROUPS, presets: PRESETS };