@higinio/design-mcp 0.1.1 → 0.2.1

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.
@@ -29,7 +29,7 @@
29
29
  - **Casing:** sentence case en headings y botones ("Crear proyecto", no "Crear Proyecto" ni "CREAR PROYECTO"). Overlines/labels en uppercase con tracking `+0.08em`.
30
30
  - **Copy de botones:** verbo + objeto, corto: "Guardar cambios", "Ver informe".
31
31
  - **Mensajes de estado:** concretos y accionables — el error dice qué pasó **y qué hacer**.
32
- - **Números y datos:** protagonistas — siempre en JetBrains Mono o `tabular-nums`, grandes en dashboards, con count-up al montar.
32
+ - **Números y datos:** protagonistas — siempre en JetBrains Mono o `tabular-nums`, grandes en dashboards, con valor final visible desde la carga.
33
33
  - **Fechas y números formateados:** siempre con `Intl.DateTimeFormat` / `Intl.NumberFormat` (`es-MX`), nunca formatos hardcodeados — un "12/07/2026" a mano se rompe al cambiar de locale.
34
34
  - **`translate="no"`** en nombres de marca y tokens de código (IDs, comandos, rutas), para que la traducción automática no los corrompa.
35
35
  - **Numerales para conteos:** "8 despliegues", no "ocho despliegues" — en un dashboard el dígito se escanea más rápido.
@@ -73,10 +73,10 @@
73
73
  | `night-900` | `#0b0d12` | Superficie 1 (tarjetas) |
74
74
  | `night-800` | `#14171f` | Superficie 2 (modales, dropdowns, inputs) |
75
75
  | `night-700` | `#1e222d` | Superficie 3, hover de filas |
76
- | `night-600` | `#2a2f3d` | Bordes destacados (énfasis) |
76
+ | `night-600` | `#2a2f3d` | Superficie de separadores sutiles |
77
77
  | `night-500` | `#3d4354` | Bordes por defecto |
78
78
  | `night-450` | `#5f6575` | Solo light: texto atenuado (`--text-muted`) — `night-400` caía a 4.27:1 sobre `--surface-3` |
79
- | `night-400` | `#6b7183` | Deshabilitado, placeholders |
79
+ | `night-400` | `#6b7183` | Solo controles deshabilitadoss |
80
80
  | `night-300` | `#9aa0b0` | Texto terciario, captions |
81
81
  | `night-200` | `#c5c9d4` | Texto secundario |
82
82
  | `night-100` | `#e8eaef` | Texto principal |
@@ -102,11 +102,11 @@ Los componentes **nunca** usan los primitivos de arriba directamente: consumen e
102
102
  | `--surface-2` | `night-800` | Modales, dropdowns, inputs |
103
103
  | `--surface-3` | `night-700` | Hover de filas, superficie máxima |
104
104
  | `--border` | `night-500` | Borde por defecto |
105
- | `--border-strong` | `night-600` | Borde de énfasis (elevación 3, accordion) |
105
+ | `--border-strong` | `night-400` | Borde de énfasis (elevación 3, accordion). **Mismo valor en ambos temas**: un gris medio cumple 1.4.11 sobre fondos oscuros (3.26-3.99:1) y claros (4.27-4.87:1). En dark **sube** de peldaño respecto a `--border` (night-500) porque oscurecerlo lo debilitaría (principio 2) |
106
106
  | `--text-primary` | `night-100` | Texto principal |
107
107
  | `--text-secondary` | `night-200` | Texto secundario |
108
108
  | `--text-muted` | `night-300` | Terciario, captions |
109
- | `--text-disabled` | `night-400` | Deshabilitado, placeholder |
109
+ | `--text-disabled` | `night-400` | Solo controles deshabilitados |
110
110
  | `--text-heading` | `night-50` | Headings, máximo énfasis |
111
111
  | `--text-on-accent` | `night-950` | Texto sobre CTAs de acento (**oscuro, nunca blanco**) |
112
112
  | `--accent-ghost-hover` | `rgba(82,186,237,.08)` | Hover de botones ghost |
@@ -115,17 +115,22 @@ Los componentes **nunca** usan los primitivos de arriba directamente: consumen e
115
115
  | `--overlay` | `rgba(1,2,3,.7)` | Overlay de modal |
116
116
  | `--success/--warning/--danger/--info` | sus `-400` | Estados semánticos |
117
117
 
118
- ### 3.6 Proporción de uso (regla 60-30-10 adaptada a dark)
118
+ ### 3.6 Uso del color y jerarquía
119
119
 
120
- - **60% — Fondos nocturnos** (`night-950/900/800`): el escenario oscuro.
121
- - **30% — Texto y estructura** (`night-50→300`, bordes, índigo en fondos de sección).
122
- - **10% — Acento eléctrico y semánticos**: CTAs, links, cifras clave, gráficas. (Excepción: una visualización de datos multiserie usa la paleta categórica validada de la skill `dataviz-marca`, exenta de este techo — son datos, no cromo decorativo.)
120
+ El primitivo de marca expresa identidad; el rol de acción asegura legibilidad. Una acción dominante por contexto de decisión: un formulario, un diálogo o una región independiente pueden tener su propia acción. Evita competir dentro del mismo grupo. La proporción de acento se decide por jerarquía y contenido; 60/30/10 es una orientación visual, no una cuota ni un criterio de conformidad.
123
121
 
124
- Reglas prácticas:
125
- - El celeste `accent-400` es **el único color que grita**: un CTA primario por pantalla.
126
- - El índigo es el "30% de marca": fondos de hero, sidebars, estados seleccionados — nunca compite con el acento y **nunca es texto sobre negro** (contraste 1.9:1).
127
- - Evitar blanco puro `#ffffff` en texto masivo; usar `night-100`.
128
- - **Fondos siempre planos:** sin imágenes de fondo, sin texturas, sin gradientes decorativos. El único "brillo" permitido es el glow de acento (§7).
122
+ | Rol semántico | Uso |
123
+ |---|---|
124
+ | `--action-bg`, `--action-hover`, `--action-active` | Relleno de acciones sólidas y selección con texto |
125
+ | `--text-on-action` | Texto e iconos sobre los tres fondos de acción; ≥4.5:1 |
126
+ | `--control-border` | Contorno necesario para reconocer campos, checkbox y radio; ≥3:1 |
127
+ | `--focus-color` | Contorno sólido de teclado, distinto del halo decorativo |
128
+ | `--placeholder` | Ayuda legible dentro de campos; nunca sustituye al label |
129
+ | `--selection-bg`, `--selection-text` | Selección nativa de texto, con contraste propio |
130
+
131
+ `--accent` conserva la firma cromática y no garantiza contraste como relleno de texto normal. No lo intercambies con `--action-bg`. Los separadores decorativos usan `--border`; los controles usan `--control-border`.
132
+
133
+ Para datos categóricos consulta `.claude/skills/dataviz-marca/references/paletas-graficas.md`: allí se declara la validación de cada paleta, sus límites y los fallbacks. La identidad de serie también necesita etiqueta, forma o patrón; un cambio de tendencia no significa éxito por defecto.
129
134
 
130
135
  ---
131
136
 
@@ -135,7 +140,7 @@ Reglas prácticas:
135
140
 
136
141
  | Rol | Fuente | Fallback | Uso |
137
142
  |---|---|---|---|
138
- | Display | **Space Grotesk** (500–700) | system-ui, sans-serif | **Solo h1–h3**, cifras hero, navegación, wordmark |
143
+ | Display | **Space Grotesk** (500–700) | system-ui, sans-serif | Marca y títulos editoriales, cifras hero, navegación, wordmark |
139
144
  | Body/UI | **Manrope** (400–700) | system-ui, sans-serif | Párrafos, formularios, tablas, **h4 en adelante** |
140
145
  | Mono | **JetBrains Mono** (400–500) | monospace | Datos numéricos, código, IDs |
141
146
 
@@ -179,6 +184,14 @@ Carga vía Google Fonts CDN (sin binarios adjuntos — si hay licencias locales,
179
184
 
180
185
  ---
181
186
 
187
+ ### Roles tipográficos por tarea
188
+
189
+ La etiqueta HTML expresa estructura, no una familia tipográfica. En **Operate** (formularios, datos, ajustes), usa `data-mode="operate"` en la región: la familia de UI lleva h1–h3, con una escala fija en rem de 32/20/18px. `.ui-title` permite aplicarla de forma local. Conserva un solo h1 de página y el orden de encabezados.
190
+
191
+ En **Read**, la familia display puede abrir una página editorial; `.reading-title` y `.display` consumen `--text-display-fluid`, con límites en rem y `text-wrap: balance`. El cuerpo mantiene 65–75ch y tamaños en rem. **Persuade** prioriza una decisión y **Experience** deja protagonismo a la obra: no copies la composición de un dashboard a una landing. Las cifras de tablas usan `tabular-nums`; reserva la mono para código, IDs o mediciones que lo necesiten.
192
+
193
+ ---
194
+
182
195
  ## 5. Espaciado
183
196
 
184
197
  Escala base 4px — tokens `--space-1…24`: `4 · 8 · 12 · 16 · 20 · 24 · 32 · 40 · 48 · 64 · 80 · 96`
@@ -196,6 +209,40 @@ Aliases de uso (consumir estos, no números sueltos):
196
209
 
197
210
  ---
198
211
 
212
+ ### Ritmo entre textos, controles y tarjetas
213
+
214
+ | Relación | Separación | Token |
215
+ |---|---:|---|
216
+ | Etiqueta → control; control → ayuda/error | 6px | `--field-gap` |
217
+ | Título → descripción del mismo grupo | 8px | `--heading-gap` |
218
+ | Párrafos o bloques relacionados | 16px | `--content-gap` |
219
+ | Bloques directos dentro de una tarjeta | 16px | `--card-content-gap` |
220
+ | Campos consecutivos y pie del formulario | 20px | `--form-gap` |
221
+ | Tarjetas consecutivas | 24px | `--card-group-gap` |
222
+ | Secciones independientes | 64px | `--section-gap` |
223
+
224
+ El padding protege el borde; el gap separa contenido. `.card` organiza sus hijos directos con `--card-content-gap`. Para anidar contenido usa `.card__content` o `.stack`; `.text-group` agrupa título y descripción a 8px. `.form-stack` separa campos a 20px; `.cluster` agrupa acciones con 12px y permite envolver. `.stack--cards` consume la separación de tarjetas de esta marca.
225
+
226
+ Un contenedor es dueño de la separación: dentro de stacks y grids se neutralizan los márgenes de sus hijos para no sumar margen y gap. Los `.field` consecutivos también se separan en flujo normal. Evita repartir márgenes manuales entre cada div; aplica el contenedor al grupo. Las alturas de control son mínimas: permiten crecer cuando aumenta el tamaño del texto.
227
+
228
+ ```html
229
+ <div class="stack stack--cards">
230
+ <section class="card" data-mode="operate">
231
+ <header class="text-group"><h2>Datos de contacto</h2><p>Usaremos estos datos para responder.</p></header>
232
+ <form class="form-stack">
233
+ <div class="field"><label class="field__label" for="nombre">Nombre</label><input class="input" id="nombre" autocomplete="name"></div>
234
+ <div class="field"><label class="field__label" for="correo">Correo</label><input class="input" id="correo" type="email" autocomplete="email"></div>
235
+ <div class="cluster"><button class="btn btn--primary" type="submit">Guardar</button><button class="btn btn--secondary" type="button">Cancelar</button></div>
236
+ </form>
237
+ </section>
238
+ <section class="card"><h2 class="ui-title">Otra sección</h2><p>El contenedor mantiene su distancia respecto a la tarjeta anterior.</p></section>
239
+ </div>
240
+ ```
241
+
242
+ Verifica la composición con ayuda/error multilínea, textos largos, 320px y texto al 200%. El texto debe envolver y los controles crecer; no reduzcas gaps para hacer caber contenido.
243
+
244
+ ---
245
+
199
246
  ## 6. Bordes, radios y focus
200
247
 
201
248
  | Token | Valor | Uso |
@@ -210,7 +257,7 @@ Tokens compuestos:
210
257
 
211
258
  ```css
212
259
  --border-default: 1px solid var(--night-500);
213
- --border-emphasis: 1px solid var(--night-600);
260
+ --border-emphasis: 1px solid var(--night-400);
214
261
  --focus-ring: 2px solid var(--accent-400);
215
262
  --focus-ring-offset: 2px;
216
263
  ```
@@ -228,7 +275,7 @@ En fondo casi negro las sombras solas no bastan: la elevación combina **superfi
228
275
  | 0 | `night-950` | — | Fondo de la app |
229
276
  | 1 | `night-900` | `--shadow-1: 0 1px 2px rgba(0,0,0,.4)` | Tarjetas en reposo |
230
277
  | 2 | `night-800` | `--shadow-2: 0 4px 12px rgba(0,0,0,.5)` | Dropdowns, hover |
231
- | 3 | `night-800` + borde `night-600` | `--shadow-3: 0 12px 32px rgba(0,0,0,.6)` | Modales, popovers |
278
+ | 3 | `night-800` + borde `night-400` | `--shadow-3: 0 12px 32px rgba(0,0,0,.6)` | Modales, popovers |
232
279
 
233
280
  Tokens adicionales:
234
281
 
@@ -323,7 +370,7 @@ Indicador de carga circular; también sustituye el label del botón en estado lo
323
370
 
324
371
  ### 10.5 Textarea
325
372
 
326
- Mismos estados que input. Min-height 96px, `resize: vertical`. Contador `text-xs` `night-400` a la derecha.
373
+ Mismos estados que input. Min-height 96px, `resize: vertical`. Contador (`.field__count`) `text-xs` `--text-muted` a la derecha.
327
374
 
328
375
  ### 10.6 Select
329
376
 
@@ -399,7 +446,7 @@ Banner de estado **inline y persistente** (a diferencia del Toast, transitorio):
399
446
 
400
447
  - 4 tonos: fondo suave semántico (`*-bg`, ej. `info-bg` `#0d1f2c`), borde `1px` del tono al 30%, radius `md`, padding 14px 16px.
401
448
  - Icono semántico 20px en el color del tono: info→`info` · success→`circle-check` · warning→`triangle-alert` · danger→`circle-alert`.
402
- - Título `text-sm` semibold y cuerpo `text-sm`, ambos en el color del tono (cuerpo con opacidad 88%).
449
+ - Título `text-sm` semibold y cuerpo `text-sm`, ambos en el color del tono, a contraste completo (sin opacidad).
403
450
  - Botón cerrar opcional (× 18px, hit-área 36px) vía `onDismiss`; hover fondo `currentColor` al 14%. `role="status"`.
404
451
 
405
452
  ### 10.17 Toast
@@ -464,7 +511,27 @@ Lista de acciones desplegable anclada a un trigger (acciones de fila, botón "m
464
511
  - **Cifra:** JetBrains Mono 49px peso 500, line-height 1.05, color `accent-400`, `tabular-nums`, `white-space: nowrap`.
465
512
  - **Delta** opcional debajo: mono 14px — `success-400` / `danger-400` / `night-300` (neutral).
466
513
  - Gap vertical 6px. Formato numérico con `Intl.NumberFormat` (`es-MX`).
467
- - **Count-up** 350ms ease-out al montar (techo `--duration-page`); con `prefers-reduced-motion` muestra el valor directo.
514
+ - **valor final visible desde la carga.
515
+
516
+ #### Chip de métrica
517
+
518
+ Pastilla de una línea con rótulo y valor —por ejemplo, «Agua **3 de 8**»— para métricas secundarias que no merecen una cifra de Stat.
519
+
520
+ - **`.metric-chip`:** radius `full`, borde `1px --border`, fondo `--surface-1`, padding `4px 12px`, `text-sm` `--text-secondary`; icono opcional de 16px en `--text-muted`.
521
+ - **Contenido:** `.metric-chip__label` y `.metric-chip__value`; el valor va en JetBrains Mono peso 500, `--link` y `tabular-nums`.
522
+ - **Fila:** `.metric-chips` agrupa los chips con `display: flex; flex-wrap: wrap; gap: var(--space-2)`, así que saltan de línea en móvil.
523
+ - **Interactivo** (por ejemplo, «sumar un vaso»): `button.metric-chip`, con `min-height: var(--touch-target)` (44px) y padding horizontal de 16px. Hover: fondo `--outline-hover-bg` y borde `--border-hover` (como Outline). Active: fondo `--outline-active-bg` y `scale(.98)`. Foco de teclado con el anillo del sistema (`--focus-ring`, offset `--focus-ring-offset`).
524
+
525
+ ```html
526
+ <div class="metric-chips">
527
+ <span class="metric-chip"><span class="metric-chip__label">Latencia</span> <span class="metric-chip__value">42 ms</span></span>
528
+ <button class="metric-chip" type="button" aria-label="Agua: 3 de 8 vasos. Sumar un vaso">
529
+ <span class="metric-chip__label">Agua</span> <span class="metric-chip__value">3 de 8</span>
530
+ </button>
531
+ </div>
532
+ ```
533
+
534
+ **Accesibilidad:** el chip interactivo es un `<button>` con nombre accesible completo, no solo la cifra («Agua: 3 de 8 vasos. Sumar un vaso»); el nombre empieza por el texto visible (WCAG 2.5.3) y se actualiza con el valor. El chip informativo es texto: no lleva rol ni foco.
468
535
 
469
536
  ### 10.23 Pagination
470
537
 
@@ -482,6 +549,37 @@ Navegador de páginas 1-indexado, bajo tablas y listas largas.
482
549
  - **Tabs:** borde inferior `night-600`; indicador 2px `accent-400` que se desliza (250ms).
483
550
  - **Breadcrumbs:** `text-sm` `night-300`, separador `/` (tipográfico), último ítem `night-100`.
484
551
 
552
+ #### Sidebar contraíble
553
+
554
+ Variante del Sidebar para consolas y dashboards con muchas secciones: alterna entre la lista con rótulos y un riel de iconos que deja más ancho a los datos.
555
+
556
+ - **Estados:** expandido, 260px (el ancho del Sidebar), y contraído, riel de iconos de 64px. Clases: `.sidebar.sidebar--collapsible` y, contraído, `.is-collapsed`. Cada ítem lleva su icono de 18px y el nombre en `.sidebar__label`.
557
+ - **Grupos con rótulo:** `.sidebar__group` con un `.sidebar__group-label` (overline: 12px, peso 500, mayúsculas, tracking `+0.08em`, `--text-muted`); 16px entre grupos. En el riel, cada rótulo se convierte en un separador de 1px `--border` y el del primer grupo se oculta.
558
+ - **Ítem activo:** `.is-active` (fondo `--sidebar-active-bg`, texto `--sidebar-active-text`, icono `--sidebar-active-icon`) más `aria-current="page"`.
559
+ - **Botón de contraer:** `.sidebar__item.sidebar__toggle` al pie (`margin-top: auto`), con un chevron que apunta hacia donde se moverá el borde (rota 180° en el riel) y el texto «Contraer» / «Expandir» en su `.sidebar__label`.
560
+ - **Tooltip en el riel:** el `.sidebar__label` queda oculto a la vista pero accesible (patrón de `.visually-hidden`) y aparece a la derecha del ítem con el look del Tooltip (§10.15: `--tooltip-bg`, borde `--tooltip-border`, `text-xs`, delay 400ms) al pasar el cursor —solo con puntero real, §11.5— y al enfocar el ítem con teclado (`:focus-visible`). No es un `title`: el nombre sigue en el árbol de accesibilidad. El riel no recorta su contenido (sin `overflow`) para que el tooltip pueda salir de él.
561
+ - **Por debajo de 768px** (`md`), `.sidebar--collapsible` no se muestra fijo: lo sustituye un botón ☰ `.icon-btn.sidebar-trigger` que abre un Drawer `.drawer--left` (§10.29) con los mismos grupos e ítems. El framework muestra `.sidebar-trigger` solo por debajo de 768px y lo oculta (`display: none`) desde 768px, el mismo breakpoint en el que aparece el sidebar fijo; así el ☰ y el sidebar nunca conviven. El botón lleva `aria-label="Abrir navegación"`, `aria-controls` con el id del drawer y `aria-expanded` (`false` cerrado, `true` abierto).
562
+ - **Motion:** el ancho cambia sin transición —el layout usa `grid-template-columns: auto 1fr` y el contenido sigue al sidebar al instante—; nunca se anima `width`. Solo los rótulos se funden al expandir: `opacity` 0→1 y `translateX(-4px)`→0 en `--duration-fast` `--ease-out`. Con `prefers-reduced-motion`, sin animación (piso global, §11.5).
563
+
564
+ ```html
565
+ <nav class="sidebar sidebar--collapsible" id="appNav" aria-label="Principal">
566
+ <div class="sidebar__group" role="group" aria-labelledby="navMonitor">
567
+ <span class="sidebar__group-label" id="navMonitor">Monitoreo</span>
568
+ <a class="sidebar__item is-active" href="/" aria-current="page"><svg aria-hidden="true">…</svg><span class="sidebar__label">Inicio</span></a>
569
+ </div>
570
+ <button class="sidebar__item sidebar__toggle" type="button" aria-expanded="true" aria-controls="appNav">
571
+ <svg aria-hidden="true">…</svg><span class="sidebar__label">Contraer</span>
572
+ </button>
573
+ </nav>
574
+
575
+ <!-- Solo por debajo de 768px -->
576
+ <button class="icon-btn sidebar-trigger" type="button" aria-label="Abrir navegación"
577
+ aria-expanded="false" aria-controls="navDrawer">…menu…</button>
578
+ <div class="drawer drawer--left" id="navDrawer">…los mismos grupos e ítems…</div>
579
+ ```
580
+
581
+ **Accesibilidad:** el contenedor es un `<nav>` con `aria-label`; el ítem de la página actual lleva `aria-current="page"` además de `.is-active`. El toggle es un `<button>` real con `aria-expanded` (`true` expandido, `false` en el riel) y `aria-controls` con el id del sidebar; al alternar, su texto cambia entre «Contraer» y «Expandir». El ☰ `.sidebar-trigger` también es un `<button>` con `aria-expanded` y `aria-controls` hacia el drawer, y el drawer sigue el contrato de foco del Modal. Los grupos se declaran con `role="group"` y `aria-labelledby` apuntando a su rótulo, que en el riel sigue siendo legible para el lector aunque se vea como una línea.
582
+
485
583
  ### 10.25 Skeleton y EmptyState
486
584
 
487
585
  - Skeleton: fondo `night-800` con shimmer (gradiente hacia `night-700`, barre en 1.4s loop). Radius igual al contenido.
@@ -551,8 +649,8 @@ Campo de fecha con calendario en popover: el input abre el calendario al hacer c
551
649
 
552
650
  - **`.datepicker`** (ancla `relative`), **`.datepicker__input`** (reutiliza el `Input` §10.4, con icono de calendario leading vía `.input-wrap`), **`.datepicker__calendar`** = panel que reutiliza el look de `.popover__panel` sobre `--surface-2` (borde `--border-strong`, sombra nivel 3, radius `md`).
553
651
  - **Calendario:** `.calendar` contiene `.calendar__header` (mes y año + prev/next con los iconos chevron-left/chevron-right), `.calendar__weekdays` (iniciales L–D) y `.calendar__grid` con los `.calendar__day`.
554
- - **Estados del día:** `.calendar__day--today` (aro interior `accent-400` + texto `accent-300`), `.calendar__day--selected` (fondo `accent-400`, texto `--text-on-accent`, peso 600 — no depende solo del color), `.calendar__day--outside` (días del mes vecino, atenuados) y `[disabled]` (fuera de rango).
555
- - **Accesibilidad:** `.calendar__grid` es `role="grid"` con filas `role="row"` (`.calendar__week`, con `display: contents` para no romper el grid); los encabezados de día son `role="columnheader"` con `aria-label` del nombre completo (lunes…); cada día es un `<button role="gridcell">` con `aria-label` de la fecha completa ("10 de julio de 2026") y `aria-selected`. El día de hoy lleva `aria-current="date"`.
652
+ - **Estados del día:** `.calendar__day--today` (aro interior `accent-400` + texto `accent-300`), `.calendar__day--selected` (fondo `accent-400`, texto `--text-on-accent`, peso 600 — no depende solo del color), `.calendar__day--outside` (días del mes vecino, `--text-muted`) y `[disabled]` (fuera de rango).
653
+ - **Accesibilidad:** `.calendar__table` es `role="grid"` (con `aria-labelledby` al título del mes) y envuelve la fila de cabeceras `.calendar__weekdays` (`role="row"`) y `.calendar__grid` (`role="rowgroup"`) con filas `.calendar__week` (`role="row"`, con `display: contents` para no romper el grid); el input lleva `aria-haspopup="dialog"` + `aria-controls`, sin `aria-expanded` (no es válido en un `input` de texto); los encabezados de día son `role="columnheader"` con `aria-label` del nombre completo (lunes…); cada día es un `<button role="gridcell">` con `aria-label` de la fecha completa ("10 de julio de 2026") y `aria-selected`. El día de hoy lleva `aria-current="date"`.
556
654
  - **Teclado:** el input abre el calendario (clic) y mueve el foco al día activo; las flechas mueven el día enfocado (±1 día, ±7 una semana) cruzando meses, Inicio/Fin saltan a los extremos de la semana, Enter/Espacio eligen y Esc cierra devolviendo el foco al input. Roving tabindex: solo el día activo es tabulable.
557
655
 
558
656
  ### 10.33 FileUpload
@@ -617,7 +715,7 @@ Antes de animar algo, pregúntate: ¿de dónde vino, qué cambió, a dónde se f
617
715
  ### 11.2 Principios de movimiento
618
716
 
619
717
  1. **Entra rápido, sale más rápido** — Salida ~20% más corta que la entrada.
620
- 2. **Solo `transform` y `opacity`** — Nunca animar propiedades de layout.
718
+ 2. **Propiedades según coste y propósito** — Prefiere `transform` y `opacity` para desplazamiento. Color y sombra pueden comunicar estados; las excepciones de layout son locales, cortas y medidas.
621
719
  3. **Distancias cortas** — Entradas de 4–8px.
622
720
  4. **Stagger en listas** — 30ms por ítem, máximo 6 ítems.
623
721
  5. **Firma de marca:** los elementos con acento entran con fade + glow (opacidad del glow 0→1 en 220ms). Usar solo en el elemento hero/CTA.
@@ -645,7 +743,7 @@ Antes de animar algo, pregúntate: ¿de dónde vino, qué cambió, a dónde se f
645
743
  | Toast | entrar | slide-in + fade 250ms |
646
744
  | Icon swap (p. ej. copiar→check) | cambio | cross-fade: opacity + scale(0.9→1), 150ms; nunca blur |
647
745
  | Página/vista | cambio | fade + translateY(8px→0) 350ms |
648
- | Cifra/dato clave | montar | count-up numérico 350ms `ease-out` (solo dashboards; techo `--duration-page`) |
746
+ | Cifra/dato clave | montar | valor final visible desde la carga|
649
747
 
650
748
  ### 11.4 Loading
651
749
 
@@ -656,26 +754,21 @@ Antes de animar algo, pregúntate: ¿de dónde vino, qué cambió, a dónde se f
656
754
 
657
755
  ### 11.5 Accesibilidad del movimiento
658
756
 
757
+ La preferencia reducida cambia los tokens de duración propios a 0ms y detiene spinner y skeleton. Los estados finales siguen visibles; el feedback de color de botones puede durar 80ms. No se acortan con un selector universal las animaciones de otras librerías. No uses eventos de animación como única condición para completar una operación o retirar un elemento.
758
+
659
759
  ```css
660
760
  @media (prefers-reduced-motion: reduce) {
661
- *, *::before, *::after {
662
- animation-duration: 0.01ms !important;
663
- animation-iteration-count: 1 !important;
664
- transition-duration: 0.01ms !important;
665
- scroll-behavior: auto !important;
761
+ :root {
762
+ --duration-instant: 0ms; --duration-fast: 0ms; --duration-base: 0ms;
763
+ --duration-slow: 0ms; --duration-page: 0ms; --stagger-step: 0ms;
666
764
  }
765
+ html { scroll-behavior: auto; }
766
+ .spinner, .skeleton, .skeleton::after { animation: none; }
767
+ .btn, .icon-btn { transform: none; transition: background-color 80ms linear, color 80ms linear; }
667
768
  }
668
769
  ```
669
770
 
670
- Este reset con `*` es el **piso de seguridad**: garantiza que ningún motion sobreviva si el usuario pide reducirlo, incluso en componentes que no se revisaron a mano. Donde el movimiento comunica información real (progreso, un check, un conteo) y eliminarlo del todo perjudicaría la comprensión, un componente puede sobrescribir el reset con un selector más específico y una duración corta (p. ej. `transition-duration: 80ms`) en vez de colapsar a 0.01ms — hoy ningún componente del sistema ejerce esta excepción; todos usan el piso global.
671
-
672
- **Gating de hover:** los efectos de hover que mueven o elevan (transform, box-shadow, glow) van dentro de `@media (hover: hover) and (pointer: fine)` — en touch no hay hover real y el tap dispara hovers falsos que se quedan pegados hasta el siguiente toque. Los hover de solo color/fondo pueden quedar fuera: un cambio de color pegado no estorba.
673
-
674
- ```css
675
- @media (hover: hover) and (pointer: fine) {
676
- .card--interactive:hover { transform: translateY(-2px); box-shadow: var(--shadow-2); }
677
- }
678
- ```
771
+ Los hover que desplazan o elevan solo se aplican con `(hover: hover) and (pointer: fine)`. Verifica la alternativa reducida con el mismo contenido y flujo, incluidos abrir, cerrar, cargar y confirmar. Los datos llegan con su valor final desde HTML: no hay valor final visible desde la carga.
679
772
 
680
773
  ### 11.6 Anti-patrones de motion
681
774
 
@@ -709,7 +802,7 @@ Un "motion gap" es UI que aparece o desaparece sin transición y debería tenerl
709
802
  - Estados de carga y de error.
710
803
  - Paneles de settings / inspector.
711
804
 
712
- Regla: un gap en UI primaria (el flujo principal) es crítico; en UI secundaria, medio. Toda entrada/salida condicional necesita su transición de `opacity`/`transform`, con su contraparte de salida más discreta.
805
+ La ausencia de transición no es un defecto por sí sola. Evalúa si el cambio se entiende y mantiene el foco. En tareas frecuentes, teclado o movimiento reducido, una actualización instantánea puede ser la respuesta correcta.
713
806
 
714
807
  ---
715
808
 
@@ -732,22 +825,36 @@ Ratios calculados con la fórmula WCAG 2.x (no estimados). El test `mcp/test/con
732
825
  ## 13. Tokens CSS consolidados
733
826
 
734
827
  ```css
828
+ /* ============================================================
829
+ Nocturna Eléctrica — Design Tokens v2.0
830
+ Sistema de diseño de Higinio Sosa · dark-first
831
+ Fuente de verdad: DESIGN-nocturna-electrica.md
832
+ Uso: importar ANTES de cualquier estilo propio.
833
+ Los componentes consumen SOLO aliases semánticos, nunca hex.
834
+ ============================================================ */
835
+
836
+ @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=Manrope:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
837
+
735
838
  :root {
736
- /* == Primitivos: acento == */
839
+ /* == Primitivos: acento (Celeste Eléctrico) ==
840
+ accent-600 sobre blanco da 4.01:1 — vale como UI (≥3) pero NO como texto (<4.5).
841
+ Para texto sobre claro se usa accent-700 (5.95:1) y accent-800 (8.74:1). */
737
842
  --accent-100: #d9f0fb; --accent-200: #a8ddf6; --accent-300: #7dccf1;
738
843
  --accent-400: #52baed; --accent-500: #2fa3dd; --accent-600: #1f87bb; --accent-700: #186a94;
844
+ --accent-800: #12506f;
739
845
 
740
- /* == Primitivos: índigo == */
846
+ /* == Primitivos: índigo (Índigo Profundo) — nunca texto sobre negro == */
741
847
  --indigo-300: #7a83c4; --indigo-400: #565fa3; --indigo-500: #353d7c;
742
848
  --indigo-600: #2b3264; --indigo-700: #22284f; --indigo-800: #1a1e3b;
743
849
 
744
850
  /* == Primitivos: escala nocturna == */
745
851
  --night-950: #010203; --night-900: #0b0d12; --night-800: #14171f;
746
852
  --night-700: #1e222d; --night-600: #2a2f3d; --night-500: #3d4354;
853
+ --night-450: #5f6575; /* texto atenuado en light: night-400 caía a 4.27:1 sobre --surface-3 */
747
854
  --night-400: #6b7183; --night-300: #9aa0b0; --night-200: #c5c9d4;
748
855
  --night-100: #e8eaef; --night-50: #f6f7f9;
749
856
 
750
- /* == Primitivos: semánticos == */
857
+ /* == Primitivos: semánticos (calibrados para dark) == */
751
858
  --success-400: #4ade80; --success-bg: #0a2617;
752
859
  --warning-400: #fbbf24; --warning-bg: #2a2005;
753
860
  --danger-400: #f87171; --danger-bg: #2c0f0f;
@@ -755,13 +862,24 @@ Ratios calculados con la fórmula WCAG 2.x (no estimados). El test `mcp/test/con
755
862
 
756
863
  /* == Aliases semánticos (los componentes consumen SOLO esto) == */
757
864
  --bg: var(--night-950);
758
- --surface-1: var(--night-900); --surface-2: var(--night-800); --surface-3: var(--night-700);
759
- --border: var(--night-500); --border-strong: var(--night-600);
760
- --text-primary: var(--night-100); --text-secondary: var(--night-200);
761
- --text-muted: var(--night-300); --text-disabled: var(--night-400);
762
- --text-heading: var(--night-50); --text-on-accent: var(--night-950);
865
+ --surface-1: var(--night-900);
866
+ --surface-2: var(--night-800);
867
+ --surface-3: var(--night-700);
868
+ --border: var(--night-500);
869
+ /* night-400, no night-600: en dark, oscurecer un borde lo DEBILITA (principio 2:
870
+ la elevación se comunica aclarando). n600 daba 1.46:1 vs los 1.97:1 del borde
871
+ normal — el énfasis se veía más flojo. n400 = 3.99:1 sobre surface-1. */
872
+ --border-strong: var(--night-400);
873
+ --text-primary: var(--night-100);
874
+ --text-secondary: var(--night-200);
875
+ --text-muted: var(--night-300);
876
+ --text-disabled: var(--night-400);
877
+ --text-heading: var(--night-50);
878
+ --text-on-accent: var(--night-950); /* texto de CTA oscuro, nunca blanco */
763
879
  --success: var(--success-400); --warning: var(--warning-400);
764
880
  --danger: var(--danger-400); --info: var(--info-400);
881
+ --link: var(--accent-400);
882
+ --link-hover: var(--accent-200);
765
883
  --accent-ghost-hover: rgba(82,186,237,.08);
766
884
  --accent-ghost-active: rgba(82,186,237,.14);
767
885
  --accent-selected: rgba(82,186,237,.12);
@@ -779,7 +897,7 @@ Ratios calculados con la fórmula WCAG 2.x (no estimados). El test `mcp/test/con
779
897
  --tracking-display: -0.02em; --tracking-overline: 0.08em;
780
898
  --measure: 65ch;
781
899
 
782
- /* == Espaciado == */
900
+ /* == Espaciado (escala base 4px) == */
783
901
  --space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px;
784
902
  --space-5: 20px; --space-6: 24px; --space-8: 32px; --space-10: 40px;
785
903
  --space-12: 48px; --space-16: 64px; --space-20: 80px; --space-24: 96px;
@@ -792,15 +910,15 @@ Ratios calculados con la fórmula WCAG 2.x (no estimados). El test `mcp/test/con
792
910
  --radius-sm: 8px; --radius-md: 12px; --radius-lg: 16px;
793
911
  --radius-xl: 24px; --radius-full: 9999px;
794
912
  --border-default: 1px solid var(--night-500);
795
- --border-emphasis: 1px solid var(--night-600);
796
- --focus-ring: 2px solid var(--accent-400);
913
+ --border-emphasis: 1px solid var(--night-400); /* ver --border-strong */
914
+ --focus-ring: 2px solid var(--focus-color);
797
915
  --focus-ring-offset: 2px;
798
916
 
799
- /* == Elevación == */
917
+ /* == Elevación (superficie + sombra + borde) == */
800
918
  --shadow-1: 0 1px 2px rgba(0,0,0,.4);
801
919
  --shadow-2: 0 4px 12px rgba(0,0,0,.5);
802
920
  --shadow-3: 0 12px 32px rgba(0,0,0,.6);
803
- --glow-accent: 0 0 0 1px rgba(82,186,237,.35), 0 4px 20px rgba(82,186,237,.18);
921
+ --glow-accent: 0 0 0 1px rgba(82,186,237,.35), 0 4px 20px rgba(82,186,237,.18); /* máx. uno por pantalla */
804
922
  --ring: 0 0 0 3px rgba(82,186,237,.2);
805
923
  --ring-danger: 0 0 0 3px rgba(248,113,113,.2);
806
924
 
@@ -809,10 +927,168 @@ Ratios calculados con la fórmula WCAG 2.x (no estimados). El test `mcp/test/con
809
927
  --duration-slow: 250ms; --duration-page: 350ms;
810
928
  --ease-out: cubic-bezier(0.2, 0, 0, 1);
811
929
  --ease-in: cubic-bezier(0.4, 0, 1, 1);
812
- --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
930
+ --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* moverse de A a B en pantalla */
813
931
  --ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);
932
+ --stagger-step: 30ms;
814
933
 
815
934
  color-scheme: dark;
935
+
936
+ --focus-color: var(--accent-400);
937
+ --border-hover: var(--night-400);
938
+ --outline-hover-bg: var(--surface-2);
939
+ --outline-active-bg: var(--surface-3);
940
+ --text-on-danger: var(--night-950);
941
+ --data-color: var(--accent-400);
942
+ --elevated-border: var(--night-600);
943
+ --card-border: var(--night-600);
944
+ --code-color: var(--accent-300);
945
+ --code-border: var(--night-700);
946
+ --table-header-bg: var(--night-900);
947
+ --row-border: var(--night-800);
948
+ --row-hover: var(--night-700);
949
+ --navbar-bg: rgba(1,2,3,.8);
950
+ --nav-active: var(--accent-400);
951
+ --sidebar-bg: var(--night-900);
952
+ --sidebar-active-bg: var(--indigo-800);
953
+ --sidebar-active-text: var(--night-50);
954
+ --sidebar-active-icon: var(--accent-400);
955
+ --switch-off: var(--night-600);
956
+ --tooltip-bg: var(--night-700);
957
+ --tooltip-border: var(--night-600);
958
+ --tooltip-text: var(--night-100);
959
+ --skeleton-base: var(--night-800);
960
+ --skeleton-sheen: var(--night-700);
961
+ --empty-icon: var(--indigo-400);
962
+
963
+ --action-bg: var(--accent);
964
+ --action-hover: var(--accent-hover);
965
+ --action-active: var(--accent-active);
966
+ --text-on-action: var(--text-on-accent);
967
+ --control-border: var(--border-strong);
968
+ --placeholder: var(--text-muted);
969
+ --selection-bg: var(--action-bg);
970
+ --selection-text: var(--text-on-action);
971
+ --font-ui-heading: var(--font-body);
972
+ --text-ui-title: 2rem;
973
+ --control-height-md: 44px;
974
+ --button-padding-md: 12px 20px;
975
+ --surface: var(--surface-1);
976
+ --surface-alt: var(--surface-2);
977
+ --accent: var(--accent-400);
978
+ --accent-hover: var(--accent-300);
979
+ --accent-active: var(--accent-500);
980
+ --text-display-fluid: clamp(2.5rem, 1.5rem + 3.4vw, 5rem);
981
+ --text-on-primary-solid: var(--link);
982
+ --weight-regular: 400;
983
+ --weight-medium: 500;
984
+ --weight-semibold: 600;
985
+ --weight-bold: 700;
986
+ --leading-base: 1.6;
987
+ --leading-lg: 1.55;
988
+ --gutter: 24px;
989
+ --gutter-mobile: 16px;
990
+
991
+ /* Series categóricas: orden estable; acompañar con etiqueta o forma. */
992
+ --chart-1: #52baed;
993
+ --chart-2: #a0587c;
994
+ --chart-3: #2aa79e;
995
+ --chart-4: #aa5849;
996
+ --chart-5: #67a369;
997
+ --chart-6: #946a1c;
998
+ --chart-7: #877cc3;
999
+ /* Ritmo de contenido y formularios. */
1000
+ --field-gap: 6px;
1001
+ --heading-gap: var(--space-2);
1002
+ --content-gap: var(--space-4);
1003
+ --card-content-gap: var(--space-4);
1004
+ --card-group-gap: var(--space-6);
1005
+
1006
+ }
1007
+
1008
+ /* == Modo claro (variante) — alternar data-theme en <html> == */
1009
+ [data-theme="light"] {
1010
+ --bg: #f5f8fb;
1011
+ --surface-1: #ffffff;
1012
+ --surface-2: #ffffff;
1013
+ --surface-3: #eaf1f7;
1014
+ --border: #d8dfe8;
1015
+ /* border-strong NO se redefine en claro: night-400 (:root) cumple 1.4.11 en
1016
+ AMBOS temas (3.26-3.99:1 en dark, 4.27-4.87:1 en light). #b9c3d1 daba
1017
+ 1.53-1.78:1, bajo el mínimo de 3:1 para UI. */
1018
+ --text-primary: #14171f;
1019
+ --text-secondary: #3d4354;
1020
+ --text-muted: var(--night-450); /* no night-400: ese caía a 4.27:1 sobre --surface-3 */
1021
+ --text-disabled: #9aa0b0;
1022
+ --text-heading: #14171f;
1023
+ --link: #186a94; /* accent-700 · 5.95:1 sobre blanco — accent-600 solo daba 4.01:1 */
1024
+ --link-hover: #12506f; /* accent-800 · en light el hover OSCURECE (en dark aclara) */
1025
+ --text-on-accent: #010203; /* CTA no cambia entre modos */
1026
+ /* Semánticos de light: son a la vez relleno y TEXTO sobre su propio --*-bg tintado,
1027
+ así que se calibran contra ese fondo, no contra blanco. */
1028
+ --success: #166534;
1029
+ --warning: #854d0e;
1030
+ --danger: #b02c2c;
1031
+ --info: #186a94;
1032
+ --success-bg: #e3f5ea; --warning-bg: #fbf3dd;
1033
+ --danger-bg: #fbe7e7; --info-bg: #e3f2fb;
1034
+ --accent-ghost-hover: rgba(31,135,187,.08);
1035
+ --accent-ghost-active: rgba(31,135,187,.14);
1036
+ --accent-selected: rgba(82,186,237,.12);
1037
+ --overlay: rgba(20,23,31,.45);
1038
+ --ring: 0 0 0 3px rgba(31,135,187,.25);
1039
+ --shadow-1: 0 1px 3px rgba(53,61,124,.08);
1040
+ --shadow-2: 0 4px 10px rgba(53,61,124,.10);
1041
+ --shadow-3: 0 12px 28px rgba(53,61,124,.14);
1042
+ --glow-accent: 0 0 0 1px rgba(82,186,237,.4), 0 4px 16px rgba(82,186,237,.25);
1043
+ color-scheme: light;
1044
+
1045
+ --focus-color: #186a94;
1046
+ --border-hover: #5f6575;
1047
+ --outline-hover-bg: #eaf1f7;
1048
+ --outline-active-bg: #d8dfe8;
1049
+ --text-on-danger: #ffffff;
1050
+ --data-color: #186a94;
1051
+ --elevated-border: #d8dfe8;
1052
+ --card-border: #d8dfe8;
1053
+ --code-color: #186a94;
1054
+ --code-border: #d8dfe8;
1055
+ --table-header-bg: #eaf1f7;
1056
+ --row-border: #eaf1f7;
1057
+ --row-hover: #f5f8fb;
1058
+ --navbar-bg: rgba(255,255,255,.8);
1059
+ --nav-active: #186a94;
1060
+ --sidebar-bg: #ffffff;
1061
+ --sidebar-active-bg: #e3f2fb;
1062
+ --sidebar-active-text: #14171f;
1063
+ --sidebar-active-icon: #1f87bb;
1064
+ --switch-off: var(--control-border);
1065
+ --tooltip-bg: #14171f;
1066
+ --tooltip-border: #14171f;
1067
+ --tooltip-text: #f6f7f9;
1068
+ --skeleton-base: #eaf1f7;
1069
+ --skeleton-sheen: #f5f8fb;
1070
+ --ring-danger: 0 0 0 3px rgba(192,53,53,.2);
1071
+
1072
+ --selection-bg: var(--action-bg);
1073
+ --selection-text: var(--text-on-action);
1074
+
1075
+ /* Series categóricas: orden estable; acompañar con etiqueta o forma. */
1076
+ --chart-1: #186a94;
1077
+ --chart-2: #853f63;
1078
+ --chart-3: #129a92;
1079
+ --chart-4: #8e4031;
1080
+ --chart-5: #5b975e;
1081
+ --chart-6: #8a600a;
1082
+ --chart-7: #857bc1;
1083
+ }
1084
+
1085
+ /* == Accesibilidad del movimiento == */
1086
+
1087
+ @media (prefers-reduced-motion: reduce) {
1088
+ :root {
1089
+ --duration-instant: 0ms; --duration-fast: 0ms; --duration-base: 0ms;
1090
+ --duration-slow: 0ms; --duration-page: 0ms; --stagger-step: 0ms;
1091
+ }
816
1092
  }
817
1093
  ```
818
1094
 
@@ -839,6 +1115,17 @@ Grid de 12 columnas, gutter 24px (16px móvil). Mobile-first.
839
1115
 
840
1116
  ---
841
1117
 
1118
+ ### Adaptación de los componentes
1119
+
1120
+ - Reflow a 320 CSS px sin recortar contenido ni ocultar el desbordamiento del documento. Revisa también texto al 200% y zoom al 400%.
1121
+ - Acciones de formularios se apilan cuando no caben; el primario puede ocupar el ancho disponible en móvil. Las etiquetas largas pueden envolver sin quedar cortadas.
1122
+ - La navegación lateral se convierte en disclosure o drawer con estado accesible, cierre con Escape, devolución de foco y contenido cerrado fuera del orden de tabulación. Tabs largas conservan desplazamiento horizontal local.
1123
+ - Una tabla ancha usa `.table-scroll`, con nombre accesible y región enfocables si el scroll necesita teclado; prioriza columnas o una vista de detalle sin retirar datos necesarios.
1124
+ - Calendarios y grupos compactos mantienen objetivos de al menos 24×24px o espaciado equivalente; 44×44px es la preferencia de comodidad táctil, no el mínimo AA universal. El tamaño visible del icono puede ser menor que su zona activa.
1125
+ - Popovers, combobox y calendarios respetan el viewport y se reposicionan cerca de sus bordes. En producción usa top layer, portal o posicionamiento con detección de colisiones para escapar de ancestros con overflow. El framework CSS por sí solo no gestiona colisiones ni foco.
1126
+
1127
+ ---
1128
+
842
1129
  ## 15. Modo claro (variante)
843
1130
 
844
1131
  El sistema es **dark-first**, pero todo producto debe soportar light mode con los mismos tokens semánticos. La energía de la marca se conserva; solo se invierte el escenario.
@@ -859,7 +1146,7 @@ El sistema es **dark-first**, pero todo producto debe soportar light mode con lo
859
1146
  | `--surface-2` | `#14171f` | `#ffffff` |
860
1147
  | `--surface-3` | `#1e222d` | `#eaf1f7` |
861
1148
  | `--border` | `#3d4354` | `#d8dfe8` |
862
- | `--border-strong` | `#2a2f3d` | `#b9c3d1` |
1149
+ | `--border-strong` | `#6b7183` (`night-400`) | `#6b7183` (`night-400`, mismo valor) |
863
1150
  | `--text-primary` | `#e8eaef` | `#14171f` |
864
1151
  | `--text-secondary` | `#c5c9d4` | `#3d4354` |
865
1152
  | `--text-muted` | `#9aa0b0` | `#5f6575` (`night-450`) |
@@ -907,7 +1194,7 @@ El glow de acento se mantiene como firma, pero más tenue: `0 0 0 1px rgba(82,18
907
1194
  - **Toast:** fondo blanco, borde `#d8dfe8`, borde izquierdo semántico.
908
1195
  - **Overlay de modal:** `rgba(20,23,31,.45)` + blur 4px.
909
1196
  - **Skeleton:** base `#eaf1f7`, shimmer hacia `#f5f8fb`.
910
- - **Count-up y motion:** sin cambios; el glow de entrada baja su opacidad máxima a 0.6.
1197
+ - **valor final visible desde la carga.6.
911
1198
 
912
1199
  ### 15.5 Contraste en light
913
1200
 
@@ -951,24 +1238,74 @@ Ratios calculados con la fórmula WCAG 2.x, no estimados a ojo. `mcp/test/contra
951
1238
  --surface-2: #ffffff;
952
1239
  --surface-3: #eaf1f7;
953
1240
  --border: #d8dfe8;
954
- --border-strong: #b9c3d1;
1241
+ /* border-strong NO se redefine en claro: night-400 (:root) cumple 1.4.11 en
1242
+ AMBOS temas (3.26-3.99:1 en dark, 4.27-4.87:1 en light). #b9c3d1 daba
1243
+ 1.53-1.78:1, bajo el mínimo de 3:1 para UI. */
955
1244
  --text-primary: #14171f;
956
1245
  --text-secondary: #3d4354;
957
- --text-muted: #5f6575; /* night-450 — night-400 caía a 4.27:1 sobre --surface-3 */
958
- --link: #186a94; /* accent-700 · 5.95:1 — accent-600 solo da 4.01:1 */
959
- --link-hover: #12506f; /* accent-800 · en light el hover oscurece */
960
- --text-on-accent: #010203; /* CTA no cambia */
961
- /* Semánticos: son texto sobre su propio --*-bg tintado, no sobre blanco. */
1246
+ --text-muted: var(--night-450); /* no night-400: ese caía a 4.27:1 sobre --surface-3 */
1247
+ --text-disabled: #9aa0b0;
1248
+ --text-heading: #14171f;
1249
+ --link: #186a94; /* accent-700 · 5.95:1 sobre blanco — accent-600 solo daba 4.01:1 */
1250
+ --link-hover: #12506f; /* accent-800 · en light el hover OSCURECE (en dark aclara) */
1251
+ --text-on-accent: #010203; /* CTA no cambia entre modos */
1252
+ /* Semánticos de light: son a la vez relleno y TEXTO sobre su propio --*-bg tintado,
1253
+ así que se calibran contra ese fondo, no contra blanco. */
962
1254
  --success: #166534;
963
1255
  --warning: #854d0e;
964
1256
  --danger: #b02c2c;
965
1257
  --info: #186a94;
1258
+ --success-bg: #e3f5ea; --warning-bg: #fbf3dd;
1259
+ --danger-bg: #fbe7e7; --info-bg: #e3f2fb;
1260
+ --accent-ghost-hover: rgba(31,135,187,.08);
1261
+ --accent-ghost-active: rgba(31,135,187,.14);
1262
+ --accent-selected: rgba(82,186,237,.12);
1263
+ --overlay: rgba(20,23,31,.45);
966
1264
  --ring: 0 0 0 3px rgba(31,135,187,.25);
967
1265
  --shadow-1: 0 1px 3px rgba(53,61,124,.08);
968
1266
  --shadow-2: 0 4px 10px rgba(53,61,124,.10);
969
1267
  --shadow-3: 0 12px 28px rgba(53,61,124,.14);
970
1268
  --glow-accent: 0 0 0 1px rgba(82,186,237,.4), 0 4px 16px rgba(82,186,237,.25);
971
1269
  color-scheme: light;
1270
+
1271
+ --focus-color: #186a94;
1272
+ --border-hover: #5f6575;
1273
+ --outline-hover-bg: #eaf1f7;
1274
+ --outline-active-bg: #d8dfe8;
1275
+ --text-on-danger: #ffffff;
1276
+ --data-color: #186a94;
1277
+ --elevated-border: #d8dfe8;
1278
+ --card-border: #d8dfe8;
1279
+ --code-color: #186a94;
1280
+ --code-border: #d8dfe8;
1281
+ --table-header-bg: #eaf1f7;
1282
+ --row-border: #eaf1f7;
1283
+ --row-hover: #f5f8fb;
1284
+ --navbar-bg: rgba(255,255,255,.8);
1285
+ --nav-active: #186a94;
1286
+ --sidebar-bg: #ffffff;
1287
+ --sidebar-active-bg: #e3f2fb;
1288
+ --sidebar-active-text: #14171f;
1289
+ --sidebar-active-icon: #1f87bb;
1290
+ --switch-off: var(--control-border);
1291
+ --tooltip-bg: #14171f;
1292
+ --tooltip-border: #14171f;
1293
+ --tooltip-text: #f6f7f9;
1294
+ --skeleton-base: #eaf1f7;
1295
+ --skeleton-sheen: #f5f8fb;
1296
+ --ring-danger: 0 0 0 3px rgba(192,53,53,.2);
1297
+
1298
+ --selection-bg: var(--action-bg);
1299
+ --selection-text: var(--text-on-action);
1300
+
1301
+ /* Series categóricas: orden estable; acompañar con etiqueta o forma. */
1302
+ --chart-1: #186a94;
1303
+ --chart-2: #853f63;
1304
+ --chart-3: #129a92;
1305
+ --chart-4: #8e4031;
1306
+ --chart-5: #5b975e;
1307
+ --chart-6: #8a600a;
1308
+ --chart-7: #857bc1;
972
1309
  }
973
1310
  ```
974
1311
 
@@ -994,3 +1331,11 @@ El sistema se distribuye en varios formatos para poder usarlo desde cualquier st
994
1331
  2. Los componentes consumen solo aliases semánticos (§3.5) — nunca hex.
995
1332
  3. Para dark/light: alternar `data-theme` en `<html>`; los tokens hacen el resto.
996
1333
  4. En el `<head>`: `<meta name="theme-color" content="#010203">` (el `--bg` nocturno) — sin él, la barra del navegador queda clara sobre la app oscura. Con el tema claro activo, actualizarlo a `#f5f8fb`.
1334
+
1335
+ ### Mantenimiento de las distribuciones
1336
+
1337
+ Los contratos se definen en esta especificación y sus valores ejecutables en `dist/nocturna-electrica/tokens.css`. Los ajustes comunes de componentes están en `web/component-contracts.css`; la presentación compartida de las guías, en `web/styleguide.css`. Tras editar, ejecuta `node scripts/build-design-assets.mjs`: regenera el prefijo de tokens del framework, el CSS embebido de las guías y los roles semánticos de Tailwind/DTCG. Los componentes propios del framework se conservan. Luego sincroniza skills, paquete CSS y MCP. Las guías siguen siendo HTML autocontenido, excepto la carga externa de fuentes.
1338
+
1339
+ ### Series categóricas
1340
+
1341
+ `--chart-1` a `--chart-7` mantienen su orden y cambian de paso con el tema. La prueba `mcp/test/chart-palettes.test.ts` verifica contraste ≥3:1 contra la superficie y separación entre colores adyacentes en OKLab: ≥15 en visión normal y ≥8 bajo simulaciones Machado de protanopía, deuteranopía y tritanopía. Son umbrales internos, no certificación WCAG ni garantía para todos los pares o personas; usa etiquetas, formas o patrones y tabla equivalente.