@higinio/design-mcp 0.1.0 → 0.1.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.
@@ -114,4 +114,8 @@
114
114
  --color-accent-soft: var(--accent-soft);
115
115
  --color-accent-foreground: var(--text-on-primary);
116
116
  --color-link: var(--link);
117
+ --color-success: var(--success-500);
118
+ --color-warning: var(--warning-500);
119
+ --color-danger: var(--danger-500);
120
+ --color-info: var(--info-500);
117
121
  }
@@ -48,8 +48,11 @@
48
48
  | `accent-300` | `#7dccf1` | Iconos activos, gráficas secundarias, código inline |
49
49
  | `accent-400` | `#52baed` | **Acento principal** — CTAs, links, focus, datos clave |
50
50
  | `accent-500` | `#2fa3dd` | Hover de botones de acento |
51
- | `accent-600` | `#1f87bb` | Pressed |
52
- | `accent-700` | `#186a94` | Bordes de elementos activos |
51
+ | `accent-600` | `#1f87bb` | Pressed; en light, **solo UI** (focus, bordes, iconos) — 4.01:1 sobre blanco |
52
+ | `accent-700` | `#186a94` | Bordes de elementos activos; en light, **texto de acción** — 5.95:1 sobre blanco |
53
+ | `accent-800` | `#12506f` | Solo light: hover del texto de acción — 8.74:1 sobre blanco |
54
+
55
+ > ⚠️ **Regla AA del acento en claro:** `accent-600` (`#1f87bb`) sobre blanco da **4.01:1**, no 4.5. Pasa el mínimo de **componentes UI** (3:1) pero **no el de texto** (4.5:1). Todo texto de acción sobre fondo claro usa `accent-700`; su hover, `accent-800`. `accent-400` jamás como texto en claro (2.19:1).
53
56
 
54
57
  ### 3.2 Secundario — Índigo Profundo (base `#353d7c`)
55
58
 
@@ -72,6 +75,7 @@
72
75
  | `night-700` | `#1e222d` | Superficie 3, hover de filas |
73
76
  | `night-600` | `#2a2f3d` | Bordes destacados (énfasis) |
74
77
  | `night-500` | `#3d4354` | Bordes por defecto |
78
+ | `night-450` | `#5f6575` | Solo light: texto atenuado (`--text-muted`) — `night-400` caía a 4.27:1 sobre `--surface-3` |
75
79
  | `night-400` | `#6b7183` | Deshabilitado, placeholders |
76
80
  | `night-300` | `#9aa0b0` | Texto terciario, captions |
77
81
  | `night-200` | `#c5c9d4` | Texto secundario |
@@ -304,6 +308,7 @@ Indicador de carga circular; también sustituye el label del botón en estado lo
304
308
 
305
309
  - Fondo `night-800`, borde `1px night-500`, radius `sm`, altura 44px, padding `0 14px`, texto `night-100`.
306
310
  - **Label:** `text-sm` peso 500 `night-200`, margen inferior 6px. Siempre visible (nunca placeholder-como-label).
311
+ - **Campo obligatorio (`.field--required`):** el label muestra un asterisco `danger-400` **decorativo** — pseudo-elemento (`aria-hidden` implícito), nunca un carácter que el lector anuncie. La obligatoriedad real se comunica en el control con `required` + `aria-required="true"`; el asterisco es solo un refuerzo visual (estado nunca solo por color). Contrapartida: marca lo **opcional** con "(opcional)" si el formulario tiene mayoría de campos obligatorios.
307
312
  - **Placeholder:** `night-400`.
308
313
  - **Hover:** borde `night-400`.
309
314
  - **Focus:** borde `accent-400` + `--ring`. Transición 150ms.
@@ -312,7 +317,7 @@ Indicador de carga circular; también sustituye el label del botón en estado lo
312
317
  - **Disabled:** fondo `night-900`, texto `night-400`, cursor `not-allowed`.
313
318
  - **Ayuda:** `text-sm` `night-300` debajo.
314
319
  - Icono leading: padding-left 40px, icono 18px `night-400`.
315
- - **`inputmode` según el dato** (`numeric`, `email`, `tel`, `decimal`) para el teclado correcto en móvil; `spellcheck="false"` en emails, códigos y usernames.
320
+ - **`inputmode` según el dato** (`numeric`, `email`, `tel`, `decimal`) para el teclado correcto en móvil; `spellcheck="false"` en emails, códigos y usernames; **`autocomplete` apropiado** (`email`, `current-password`, `one-time-code`, `name`…) para el autorrelleno del navegador.
316
321
  - **Nunca bloquear el pegado** (`onpaste`): rompe gestores de contraseñas y códigos de verificación.
317
322
  - **Al enviar con errores,** mover el foco al primer campo inválido (no solo pintarlo de rojo).
318
323
 
@@ -349,6 +354,7 @@ Selector de valor único en un rango (`min`/`max`/`step`/`value`).
349
354
 
350
355
  - 20px, borde `2px night-500`, radius 4px / full.
351
356
  - Checked: fondo `accent-400`, check `night-950` animado (stroke-dashoffset, 200ms).
357
+ - **Indeterminate (`input:indeterminate`):** tercer estado, no un "checked a medias" — un checkbox maestro cuyos hijos están parcialmente marcados. Se pinta con fondo `accent-400` y un **guion horizontal** `text-on-accent` (2px de alto), nunca el check. Solo puede activarse por JS (`el.indeterminate = true`); al marcarlo/desmarcarlo el maestro pasa a checked/unchecked normal.
352
358
  - Hover: borde `accent-300`. Focus: ring como inputs.
353
359
  - Label clicable, gap 10px.
354
360
 
@@ -422,9 +428,17 @@ Progreso **determinado** (para carga indeterminada usar `Skeleton`, no Progress)
422
428
  - Panel: fondo `night-800`, borde `night-600`, radius `lg`, sombra nivel 3, max-width 480/640px, padding 24px.
423
429
  - Entrada: fade + scale(0.96→1) 250ms. Salida 180ms.
424
430
  - Header `text-xl` + IconButton cerrar; footer con primaria al extremo derecho.
425
- - Esc y click en overlay cierran (excepto flujos destructivos).
431
+ - Esc y click en overlay cierran (excepto flujos destructivos: marca el overlay con `data-modal-persistent` para exigir una elección explícita — ni Esc ni click fuera lo cierran).
426
432
  - `overscroll-behavior: contain` en el panel scrolleable — al llegar al fondo, el scroll no se encadena a la página de atrás.
427
433
 
434
+ **Accesibilidad y gestión de foco (obligatorias):**
435
+
436
+ - El panel es `role="dialog"` + `aria-modal="true"` + `aria-labelledby` (id del `.modal__title`); si hay un párrafo de descripción, `aria-describedby` con su id.
437
+ - **Al abrir:** guarda el elemento disparador y mueve el foco al panel (`tabindex="-1"`) o al primer control — así el lector anuncia el título; el panel no muestra anillo porque el foco es programático (`:focus-visible` no se dispara).
438
+ - **Focus-trap:** Tab desde el último control vuelve al primero y Shift+Tab desde el primero salta al último; el foco nunca escapa al fondo mientras el modal está abierto.
439
+ - **Al cerrar:** devuelve el foco al disparador que lo abrió (retorno de foco), venga el cierre de Esc, del overlay o de un botón `data-close-modal`.
440
+ - Motion sin cambios: overlay fade+blur 200ms, panel fade + `scale(0.96→1)` 250ms; ambos respetan `prefers-reduced-motion` vía el piso global.
441
+
428
442
  ### 10.20 Menu
429
443
 
430
444
  Lista de acciones desplegable anclada a un trigger (acciones de fila, botón "más", menú de cuenta) — mismo patrón visual que el menú de `Select`, pero se abre desde cualquier trigger.
@@ -436,18 +450,21 @@ Lista de acciones desplegable anclada a un trigger (acciones de fila, botón "m
436
450
 
437
451
  ### 10.21 Tabla
438
452
 
439
- - Header: fondo `night-900`, `text-xs` uppercase `+0.08em` `night-300`, borde inferior `night-600`.
453
+ - Header: fondo `night-900`, `text-xs` uppercase `+0.08em` `night-300`, borde inferior `night-600`. Siempre `scope="col"` en los `<th>`.
440
454
  - Filas: altura 52px, borde inferior `night-800`, hover fondo `night-700`.
441
455
  - Numéricos a la derecha con `tabular-nums`.
442
- - Fila seleccionada: fondo `--accent-selected` + borde izquierdo 2px `accent-400`.
456
+ - **Zebra (`.table--zebra`):** filas pares en `--surface-2`; el hover (`--surface-3`) y la selección mandan sobre la banda. Úsala en tablas anchas y densas para seguir la fila; una tabla corta no la necesita.
457
+ - **Sticky header (`.table--sticky`):** `thead` con `position: sticky; top: 0` para que los encabezados queden fijos al hacer scroll dentro de un contenedor con altura acotada (`overflow: auto`).
458
+ - **Header ordenable:** el `<th>` contiene `<button class="th-sort">` con el label + `<svg class="sort-icon">`; el estado vive en `aria-sort` del `<th>` (`none` | `ascending` | `descending`). El icono está atenuado cuando la columna no ordena y se tiñe de `accent-400` (rotando 180° para descendente) cuando sí. Una sola columna ordenada a la vez.
459
+ - **Selección (`.row--selected`):** columna con checkbox en el `<th>` (maestro, con `aria-label`) y en cada fila (checkbox con `aria-label` que describe la fila). La fila marcada usa `.row--selected`: fondo `--accent-selected` + borde izquierdo 2px `accent-400` — no depende solo del color, el checkbox marcado lo refuerza.
443
460
 
444
461
  ### 10.22 Stat
445
462
 
446
463
  - **Label** overline arriba: 12px, peso 500, uppercase, tracking `+0.08em`, `night-300`.
447
464
  - **Cifra:** JetBrains Mono 49px peso 500, line-height 1.05, color `accent-400`, `tabular-nums`, `white-space: nowrap`.
448
465
  - **Delta** opcional debajo: mono 14px — `success-400` / `danger-400` / `night-300` (neutral).
449
- - Gap vertical 6px. Formato numérico `es-ES`.
450
- - **Count-up** 600ms ease-out al montar; con `prefers-reduced-motion` muestra el valor directo.
466
+ - 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.
451
468
 
452
469
  ### 10.23 Pagination
453
470
 
@@ -478,6 +495,91 @@ Navegador de páginas 1-indexado, bajo tablas y listas largas.
478
495
  - **Link:** color `accent-400`; hover `accent-200` (150ms). Subrayado animado: línea 1px `currentColor` a -2px, `scaleX(0)→scaleX(1)` desde la izquierda, 200ms ease-out. Externo: `target="_blank" rel="noopener noreferrer"` + icono flecha 12px, gap 4px.
479
496
  - **Code (inline):** JetBrains Mono `0.9em`, texto `accent-300`, fondo `night-800`, borde `1px night-700`, radius 6px, padding `1px 6px`.
480
497
 
498
+ ### 10.27 AvatarGroup
499
+
500
+ Pila de avatares solapados para representar un conjunto de colaboradores/participantes (complementa §10.12 Avatar).
501
+
502
+ - **`.avatar-group`:** contenedor `inline-flex`; cada avatar reutiliza `.avatar` (cualquier tamaño) y se solapa 8px (`margin-left: -8px`, salvo el primero).
503
+ - **Borde separador:** cada avatar del grupo lleva un borde `2px` **del color del fondo** — `--surface-1` sobre tarjetas, `--bg` sobre el lienzo — para recortarse del de atrás. Es el único caso donde el borde del avatar cambia de color respecto a §10.12.
504
+ - **`.avatar-group__more`:** chip `+N` (modificador sobre `.avatar`) con fondo neutro `--surface-3` y texto `--text-secondary` en Manrope (sin uppercase ni tracking de display), para los avatares ocultos. Va siempre al final.
505
+ - **Accesibilidad:** dale al grupo un nombre accesible (`aria-label="Equipo del proyecto"` o texto adyacente); los avatares con imagen conservan su `alt`. El `+N` no es interactivo por defecto; si abre una lista, conviértelo en `<button>` con `aria-label` ("Ver 3 personas más").
506
+
507
+ ### 10.28 Combobox / Autocomplete y Search
508
+
509
+ Dos patrones de entrada con filtrado. Complementan al `Select` (§10.6): el combobox se escribe y filtra en vivo; el search recorre contenido.
510
+
511
+ **Combobox (`.combobox`):**
512
+
513
+ - `.combobox__input` reutiliza el `Input` (§10.4); `.combobox__list` es el desplegable sobre `--surface-2` (superficie de dropdowns), borde `--border-strong`, sombra nivel 3, radius `md`; `.combobox__option` es cada opción.
514
+ - **Dos estados distintos, nunca solo por color:** `.is-active` = la opción resaltada por teclado (fondo `--surface-3`); `[aria-selected="true"]` = la opción ya elegida (fondo `--accent-selected` + texto `accent-300` + icono check en `accent-400`). La elegida se refuerza con el icono, no solo con el tinte.
515
+ - **Accesibilidad:** el input es `role="combobox"` + `aria-expanded` + `aria-controls` (id de la lista) + `aria-autocomplete="list"` + `aria-activedescendant` (id de la opción activa). La lista es `role="listbox"` con id; cada opción `role="option"` + `aria-selected` + id. `autocomplete="off"` para no chocar con el autorrelleno del navegador.
516
+ - **Teclado:** ↑/↓ mueven la opción activa, Enter la selecciona, Esc cierra. El foco nunca sale del input (patrón listbox); las opciones no son tabulables.
517
+
518
+ **Search (`.search`):**
519
+
520
+ - Wrapper `.search` con `.search__icon` (lupa, `aria-hidden`), `.search__input` (reutiliza el `Input`, con padding para el icono y el botón) y `.search__clear` (× con `aria-label="Limpiar"`, visible solo cuando hay texto).
521
+ - El botón limpiar vacía el campo y devuelve el foco al input. Se oculta el botón nativo de `type="search"` de WebKit para no duplicar el affordance.
522
+
523
+ ### 10.29 Drawer / Sheet
524
+
525
+ Panel que entra deslizándose desde un borde de la pantalla — filtros, detalle contextual, navegación en móvil. Mismo contrato de foco que el `Modal` (§10.19).
526
+
527
+ - **`.drawer`** (contenedor `fixed`), **`.drawer__overlay`** (mismo `--overlay` del modal, **sin blur nuevo** — el blur solo existe en navbar y overlay de modal, §7), **`.drawer__panel`** sobre `--surface-2` con borde `--border-strong` y sombra nivel 3.
528
+ - **Variantes:** `.drawer--right` (default), `.drawer--left`, `.drawer--bottom` (sheet, con radius superior).
529
+ - **Accesibilidad y foco (obligatorios):** panel `role="dialog"` + `aria-modal="true"` + `aria-labelledby`; **focus-trap + retorno de foco** idénticos al Modal (guarda el disparador al abrir, lo restaura al cerrar); cierra con Esc y con clic en el overlay. `overscroll-behavior: contain` en el panel para que el scroll no se encadene a la página de atrás.
530
+ - **Motion:** `transform: translateX(100%)→0` (translateY para `--bottom`) + fade del overlay, `--duration-slow`/`--ease-out`, dentro del techo (350ms). Entrada y salida interrumpibles vía la clase `.is-open` (`transition`, no keyframes); respetan `prefers-reduced-motion` por el piso global.
531
+
532
+ ### 10.30 Popover
533
+
534
+ Panel flotante anclado a un disparador que se abre por **clic** — a diferencia del `Tooltip` (§10.15), que aparece por hover y solo informa. El popover puede contener controles (enlaces, botones, campos).
535
+
536
+ - **`.popover`** (ancla `relative`), **`.popover__trigger`** (cualquier botón), **`.popover__panel`** sobre `--surface-2`, borde `--border-strong`, sombra nivel 3, radius `md`. Origin-aware: crece desde el `transform-origin` del disparador (`--end` lo alinea a la derecha).
537
+ - **Accesibilidad:** trigger `aria-haspopup="dialog"` + `aria-expanded`; panel `role="dialog"` + `aria-label`. El foco entra al panel al abrir; cierra con Esc (devuelve el foco al trigger) y con clic fuera.
538
+ - **Motion:** fade + `scale(0.96→1)` desde el origen, `--duration-base`/`--ease-out`. La visibilidad se retrasa hasta el final de la salida (`visibility`), así el panel cerrado no queda en el orden de tabulación.
539
+
540
+ ### 10.31 ButtonGroup
541
+
542
+ Botones adyacentes que se leen como un control agrupado de acciones (no de selección — para selección excluyente usa `SegmentedControl`, §10.7).
543
+
544
+ - **`.btn-group`** agrupa `.btn` existentes: el radius queda solo en los extremos y los bordes internos se colapsan (`margin-left: -1px`). El botón en hover/foco sube de `z-index` para que su borde no lo tape el vecino.
545
+ - **Accesibilidad:** contenedor `role="group"` + `aria-label` que nombra el conjunto.
546
+ - Funciona con cualquier variante de `.btn`; lo habitual es `outline` para que los bordes compartidos se lean.
547
+
548
+ ### 10.32 DatePicker
549
+
550
+ Campo de fecha con calendario en popover: el input abre el calendario al hacer clic, se elige un día y el valor vuelve al campo.
551
+
552
+ - **`.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
+ - **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"`.
556
+ - **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
+
558
+ ### 10.33 FileUpload
559
+
560
+ Zona de arrastre con lista de archivos.
561
+
562
+ - **`.file-upload`** envuelve el `.file-upload__input` (file real, oculto accesiblemente con `.visually-hidden` — sigue en el orden de tabulación) y el `.file-upload__label` (zona clicable, borde discontinuo). Dentro del label, `.file-upload__button` resalta la palabra "explora" en `--link`, y `.file-upload__hint` da formatos y tamaño.
563
+ - **`.file-upload.is-dragover`:** al arrastrar un archivo encima, el borde pasa a `accent-400` y el fondo a `--accent-ghost-hover`.
564
+ - **Lista:** `.file-upload__list` con un `.file-upload__item` por archivo — icono + `.file-upload__name` + `.file-upload__size` (mono) + `.file-upload__remove` (× con `aria-label`). Un ítem subiendo incrusta el `.progress` (§10.18). El error usa `.file-upload__item--error`: borde y texto en `danger` **con icono de alerta + texto** ("Error: supera los 10 MB") — nunca solo por color.
565
+ - **Accesibilidad:** el input file lleva label asociado (`for`/`id`) y `aria-describedby` que apunta a la ayuda de formatos/tamaño; el anillo de foco se pinta sobre el label (`:focus-visible` del input, oculto). El botón quitar nombra su archivo ("Quitar informe.pdf").
566
+
567
+ ### 10.34 Stepper / Wizard
568
+
569
+ Indicador de progreso por pasos.
570
+
571
+ - **`.stepper`** es un `<ol>`; cada `.step` tiene `.step__marker` (círculo con el número, o el icono check cuando está completo), `.step__label` y `.step__connector` (la línea al paso siguiente; el último paso no la lleva).
572
+ - **Estados:** `.step--complete` (marcador con fondo `accent-400` + check en `--text-on-accent`; el conector se tiñe de `accent-400`), `.step--current` (marcador con borde `accent-400` y número en `accent-300`, label en `--text-primary` peso 600) y `.step--upcoming` (marcador y label atenuados).
573
+ - **Variante `.stepper--vertical`:** apila los pasos; el marcador va a la izquierda y el conector es vertical.
574
+ - **Accesibilidad:** al ser un `<ol>` el orden y el conteo se anuncian solos; el paso en curso lleva `aria-current="step"`; los pasos completos muestran el check (estado no solo por color) y refuerzan con texto oculto ("Completado:", "Paso actual:", "Pendiente:"). El conector es decorativo (`aria-hidden`).
575
+
576
+ ### 10.35 Kbd
577
+
578
+ Tecla o atajo de teclado.
579
+
580
+ - **`.kbd`** (sobre `<kbd>`): fondo `--surface-2`, borde `--border-strong`, sombra sutil (nivel 1), JetBrains Mono `text-xs`, radius 6px, alto y ancho mínimo 20px. Para combos se encadenan varias con un "+" literal entre ellas (`⌘ + K`).
581
+ - Úsala en tooltips de atajos, menús y documentación; nunca como botón — es informativa, no interactiva.
582
+
481
583
  ---
482
584
 
483
585
  ## 11. Animaciones y motion
@@ -522,7 +624,7 @@ Antes de animar algo, pregúntate: ¿de dónde vino, qué cambió, a dónde se f
522
624
  6. **Origen consciente** — Popovers, menús y dropdowns aparecen desde el `transform-origin` de su disparador; los modales, desde el centro. Nunca un origen arbitrario.
523
625
  7. **Interrumpible** — Toda animación puede invertirse desde su valor visible actual, sin saltar al destino y reanimar. Clave en hover y gestos. En la práctica: para UI que se dispara con frecuencia (toasts, dropdowns, hover) usa `transition` — con `@starting-style` para la entrada al montar —, no `@keyframes`: un keyframe no puede invertirse a mitad de camino. Los `@keyframes` quedan para loops de carga (spinner, shimmer).
524
626
  8. **La salida es más discreta que la entrada** — Además de ~20% más corta, atenúa: menor desplazamiento y menor opacidad. Entrar se nota; salir se difumina.
525
- 9. **El easing según el trabajo** — Entrar → `--ease-out`; salir → `--ease-in`; un elemento que se mueve de un punto a otro de la pantalla (indicador de tabs o segmented, reorder) → `--ease-in-out`, que acelera y frena en espejo; hover y cambios de color → `--ease-out` corto; loops de carga → `linear`.
627
+ 9. **El easing según el trabajo** — Entrar → `--ease-out`; salir → `--ease-in`; un elemento que se mueve de un punto a otro de la pantalla (indicador de tabs que se desliza, reorder) → `--ease-in-out`, que acelera y frena en espejo; hover y cambios de color → `--ease-out` corto; loops de carga → `linear`.
526
628
  10. **`will-change` solo mientras anima** — Se pone justo antes de animar y se quita al terminar; nunca permanente en el CSS. Presupuesto de capas GPU: menos de 10 elementos promovidos a la vez.
527
629
 
528
630
  ### 11.3 Micro-interacciones por componente
@@ -613,11 +715,13 @@ Regla: un gap en UI primaria (el flujo principal) es crítico; en UI secundaria,
613
715
 
614
716
  ## 12. Accesibilidad y contraste
615
717
 
616
- - `night-100` sobre `night-950`: **≈ 16:1** ✅ AAA.
617
- - `accent-400` sobre `night-950`: **≈ 8.5:1** ✅ — sirve como texto/link.
618
- - `night-950` sobre `accent-400` (CTAs): **≈ 8.5:1** ✅ — por eso el texto del CTA es oscuro.
619
- - `indigo-500` sobre `night-950`: **≈ 1.9:1** ⚠️ — solo fondo/decorativo, nunca texto.
620
- - `night-50` sobre `indigo-500`: **≈ 9:1** ✅ — texto sobre fondos índigo funciona.
718
+ Ratios calculados con la fórmula WCAG 2.x (no estimados). El test `mcp/test/contraste-tokens.test.ts` los recalcula desde `tokens.css` en cada corrida: si alguien retoca la paleta y rompe un mínimo, la suite falla.
719
+
720
+ - `night-100` sobre `night-950`: **17.25:1** ✅ AAA.
721
+ - `accent-400` sobre `night-950`: **9.50:1** ✅ — sirve como texto/link.
722
+ - `night-950` sobre `accent-400` (CTAs): **9.50:1** ✅ — por eso el texto del CTA es oscuro.
723
+ - `indigo-500` sobre `night-950`: **2.09:1** ⚠️ — solo fondo/decorativo, nunca texto.
724
+ - `night-50` sobre `indigo-500`: **9.29:1** ✅ — texto sobre fondos índigo funciona.
621
725
  - Mínimos WCAG AA: 4.5:1 texto normal, 3:1 texto grande y UI.
622
726
  - Focus visible siempre (`:focus-visible` global, §6); estado nunca solo con color (color + icono + texto).
623
727
  - Touch targets 44×44px.
@@ -742,8 +846,8 @@ El sistema es **dark-first**, pero todo producto debe soportar light mode con lo
742
846
  ### 15.1 Filosofía de la inversión
743
847
 
744
848
  - Los fondos claros se tiñen levemente de azul (derivados del acento), nunca gris genérico ni blanco plano en toda la pantalla.
745
- - El celeste `#52baed` sobre blanco tiene contraste bajo (≈ 2.2:1): **no puede ser texto ni link en light**. Los links y textos de acción bajan a `accent-600 (#1f87bb)` y `accent-700 (#186a94)`.
746
- - El CTA conserva la firma de marca: **fondo `#52baed` con texto `#010203`** funciona igual sobre claro (contraste ≈ 8.5:1) — el botón se ve idéntico en ambos modos.
849
+ - El celeste `#52baed` sobre blanco tiene contraste bajo (2.19:1): **no puede ser texto ni link en light**. El texto de acción baja a `accent-700 (#186a94)` (5.95:1) y su hover a `accent-800 (#12506f)` (8.74:1). `accent-600 (#1f87bb)` **no sirve como texto** —da 4.01:1, por debajo del 4.5 de AA— pero sí como UI (focus, bordes, iconos: mínimo 3:1), y ahí conserva la firma de marca.
850
+ - El CTA conserva la firma de marca: **fondo `#52baed` con texto `#010203`** funciona igual sobre claro (9.50:1) — el botón se ve idéntico en ambos modos.
747
851
  - El índigo `#353d7c` gana protagonismo en light: aquí **sí** funciona como texto, headings de marca y fondos de sección con texto blanco.
748
852
 
749
853
  ### 15.2 Mapeo de tokens
@@ -758,18 +862,23 @@ El sistema es **dark-first**, pero todo producto debe soportar light mode con lo
758
862
  | `--border-strong` | `#2a2f3d` | `#b9c3d1` |
759
863
  | `--text-primary` | `#e8eaef` | `#14171f` |
760
864
  | `--text-secondary` | `#c5c9d4` | `#3d4354` |
761
- | `--text-muted` | `#9aa0b0` | `#6b7183` |
762
- | `--link` / texto de acción | `#52baed` | `#1f87bb` |
763
- | `--link-hover` | `#a8ddf6` | `#186a94` |
865
+ | `--text-muted` | `#9aa0b0` | `#5f6575` (`night-450`) |
866
+ | `--link` / texto de acción | `#52baed` | `#186a94` (`accent-700`) |
867
+ | `--link-hover` | `#a8ddf6` | `#12506f` (`accent-800`) |
764
868
  | CTA primario (fondo/texto) | `#52baed` / `#010203` | `#52baed` / `#010203` (sin cambio) |
765
869
  | Secundario índigo | `#353d7c` fondo | `#353d7c` fondo, texto blanco |
766
- | `--success` | `#4ade80` | `#15803d` |
767
- | `--warning` | `#fbbf24` | `#a16207` |
768
- | `--danger` | `#f87171` | `#c03535` |
870
+ | `--success` | `#4ade80` | `#166534` |
871
+ | `--warning` | `#fbbf24` | `#854d0e` |
872
+ | `--danger` | `#f87171` | `#b02c2c` |
873
+ | `--info` | `#52baed` | `#186a94` |
769
874
  | `--ring` | `rgba(82,186,237,.2)` | `rgba(31,135,187,.25)` |
770
875
 
771
876
  Fondos suaves semánticos en light: `success #e3f5ea` · `warning #fbf3dd` · `danger #fbe7e7` · `info #e3f2fb`.
772
877
 
878
+ > En light, el hover del texto de acción **oscurece** (`accent-700` → `accent-800`); en dark **aclara** (`accent-400` → `accent-200`). Es la misma intención —aumentar el contraste contra el fondo— aplicada a cada modo.
879
+ >
880
+ > Los semánticos de light son a la vez relleno y **texto sobre su propio `--*-bg` tintado**, así que se calibran contra ese fondo (no contra blanco): sobre blanco sobraría margen, pero sobre el fondo del badge se quedaban cortos. `--text-muted` no es `night-400` (`#6b7183`) porque ese caía a 4.27:1 sobre `--surface-3`.
881
+
773
882
  ### 15.3 Elevación en light
774
883
 
775
884
  Vuelven las sombras clásicas (teñidas de índigo, no negro puro); las superficies dejan de aclararse por nivel:
@@ -786,12 +895,14 @@ El glow de acento se mantiene como firma, pero más tenue: `0 0 0 1px rgba(82,18
786
895
  ### 15.4 Ajustes de componentes en light
787
896
 
788
897
  - **Botón primario:** idéntico al dark (fondo `#52baed`, texto `#010203`) — consistencia de marca entre modos.
789
- - **Botón secundario:** fondo `#353d7c`, texto blanco (contraste ≈ 9:1 ✅).
790
- - **Outline/Ghost:** borde `#b9c3d1`, texto `#14171f`; ghost con texto `#1f87bb` y hover `rgba(31,135,187,.08)`.
791
- - **Inputs:** fondo blanco, borde `#d8dfe8`; focus borde `#1f87bb` + ring.
792
- - **Tabla:** header `#eaf1f7`, hover de fila `#f5f8fb`, seleccionada `rgba(82,186,237,.12)` + borde izquierdo `#1f87bb`.
898
+ - **Botón secundario:** fondo `#353d7c`, texto blanco (9.95:1 ✅).
899
+ - **Outline/Ghost:** borde `#b9c3d1`, texto `#14171f`; ghost con texto `#186a94` (`accent-700`, 5.95:1) y hover `rgba(31,135,187,.08)`. El texto del ghost **no** es `#1f87bb`: como texto se queda en 4.01:1.
900
+ - **Inputs:** fondo blanco, borde `#d8dfe8`; focus borde `#1f87bb` + ring (es UI, 4.01:1 ≥ 3 ✅).
901
+ - **Tabla:** header `#eaf1f7`, hover de fila `#f5f8fb`, seleccionada `rgba(82,186,237,.12)` + borde izquierdo `#1f87bb` (borde = UI ✅).
793
902
  - **Navbar:** blanco 80% + blur, borde inferior `#d8dfe8`; link activo `#186a94`.
794
- - **Sidebar activo:** fondo `#e3f2fb`, texto `#14171f`, icono `#1f87bb`.
903
+ - **Sidebar activo:** fondo `#e3f2fb`, texto `#14171f`, icono `#1f87bb` (icono = gráfico, mínimo 3:1 ✅).
904
+ - **Cifras y `.progress__value`:** `#186a94` — son **texto**, no relleno; con `#1f87bb` se quedaban en 4.01:1.
905
+ - **Badges de estado:** texto y punto en el semántico correspondiente sobre su `--*-bg`; ver §15.5.
795
906
  - **Tooltip:** fondo `#14171f`, texto `#f6f7f9` (se invierte).
796
907
  - **Toast:** fondo blanco, borde `#d8dfe8`, borde izquierdo semántico.
797
908
  - **Overlay de modal:** `rgba(20,23,31,.45)` + blur 4px.
@@ -800,10 +911,36 @@ El glow de acento se mantiene como firma, pero más tenue: `0 0 0 1px rgba(82,18
800
911
 
801
912
  ### 15.5 Contraste en light
802
913
 
803
- - `#14171f` sobre `#f5f8fb`: ≈ 16:1 ✅ AAA.
804
- - `#1f87bb` sobre blanco: ≈ 4.5:1 ✅ AA — link/texto de acción válido.
805
- - `#52baed` sobre blanco: ≈ 2.2:1 ❌ — **solo relleno decorativo o fondo de CTA con texto oscuro**.
806
- - Blanco sobre `#353d7c`: ≈ 9:1 ✅ AAA.
914
+ Ratios calculados con la fórmula WCAG 2.x, no estimados a ojo. `mcp/test/contraste-tokens.test.ts` los recalcula desde `tokens.css` en cada corrida.
915
+
916
+ **Texto (mínimo 4.5:1):**
917
+
918
+ | Color | Sobre | Ratio | |
919
+ |---|---|---|---|
920
+ | `#14171f` (`--text-primary`) | `#f5f8fb` | **16.81:1** | ✅ AAA |
921
+ | `#3d4354` (`--text-secondary`) | `#f5f8fb` | **9.25:1** | ✅ AAA |
922
+ | `#5f6575` (`night-450`, `--text-muted`) | `#eaf1f7` (`--surface-3`) | **5.11:1** | ✅ AA |
923
+ | `#186a94` (`accent-700`, `--link`) | blanco | **5.95:1** | ✅ AA |
924
+ | `#186a94` | `#f5f8fb` (`--bg`) | **5.58:1** | ✅ AA |
925
+ | `#12506f` (`accent-800`, `--link-hover`) | blanco | **8.74:1** | ✅ AAA |
926
+ | Blanco | `#353d7c` (índigo) | **9.95:1** | ✅ AAA |
927
+ | `#010203` | `#52baed` (CTA) | **9.50:1** | ✅ AAA |
928
+
929
+ **Semánticos — sobre su propio fondo tintado, que es donde de verdad se leen:**
930
+
931
+ | Token | Color | Sobre | Ratio | |
932
+ |---|---|---|---|---|
933
+ | `--success` | `#166534` | `#e3f5ea` | **6.29:1** | ✅ AA |
934
+ | `--warning` | `#854d0e` | `#fbf3dd` | **6.19:1** | ✅ AA |
935
+ | `--danger` | `#b02c2c` | `#fbe7e7` | **5.45:1** | ✅ AA |
936
+ | `--info` | `#186a94` | `#e3f2fb` | **5.20:1** | ✅ AA |
937
+
938
+ **No aptos como texto en claro:**
939
+
940
+ - `#52baed` (`accent-400`) sobre blanco: **2.19:1** ❌ — solo relleno decorativo o fondo de CTA con texto oscuro.
941
+ - `#1f87bb` (`accent-600`) sobre blanco: **4.01:1** ❌ como texto (AA pide 4.5) — ✅ como **componente UI** (AA pide 3): focus ring, bordes de input, borde de fila seleccionada, iconos. Ahí se queda, y conserva la firma de marca.
942
+
943
+ > **Corrección (2026-07-14).** Este bloque afirmaba que `#1f87bb` sobre blanco daba 4.5:1. Da **4.01:1**: el número se estimó y nunca se calculó. Como `--link` y `--info` salían de ahí, el texto de acción y el badge de estado nacían fallando AA — un badge sobre `--info-bg` caía a **3.50:1**. Corregido bajando el texto de acción a `accent-700`, añadiendo `accent-800` para su hover y recalibrando los cuatro semánticos contra su fondo tintado. Amarrado con test para que no vuelva a pasar.
807
944
 
808
945
  ### 15.6 Implementación
809
946
 
@@ -817,13 +954,15 @@ El glow de acento se mantiene como firma, pero más tenue: `0 0 0 1px rgba(82,18
817
954
  --border-strong: #b9c3d1;
818
955
  --text-primary: #14171f;
819
956
  --text-secondary: #3d4354;
820
- --text-muted: #6b7183;
821
- --link: #1f87bb;
822
- --link-hover: #186a94;
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 */
823
960
  --text-on-accent: #010203; /* CTA no cambia */
824
- --success: #15803d;
825
- --warning: #a16207;
826
- --danger: #c03535;
961
+ /* Semánticos: son texto sobre su propio --*-bg tintado, no sobre blanco. */
962
+ --success: #166534;
963
+ --warning: #854d0e;
964
+ --danger: #b02c2c;
965
+ --info: #186a94;
827
966
  --ring: 0 0 0 3px rgba(31,135,187,.25);
828
967
  --shadow-1: 0 1px 3px rgba(53,61,124,.08);
829
968
  --shadow-2: 0 4px 10px rgba(53,61,124,.10);