@higinio/design-mcp 0.1.0 → 0.2.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.
@@ -26,6 +26,7 @@
26
26
  /* -- Primitivos: acento -- */
27
27
  --accent-100: #d9f0fb; --accent-200: #a8ddf6; --accent-300: #7dccf1;
28
28
  --accent-400: #52baed; --accent-500: #2fa3dd; --accent-600: #1f87bb; --accent-700: #186a94;
29
+ --accent-800: #12506f;
29
30
 
30
31
  /* -- Primitivos: índigo -- */
31
32
  --indigo-300: #7a83c4; --indigo-400: #565fa3; --indigo-500: #353d7c;
@@ -34,6 +35,7 @@
34
35
  /* -- Primitivos: escala nocturna -- */
35
36
  --night-950: #010203; --night-900: #0b0d12; --night-800: #14171f;
36
37
  --night-700: #1e222d; --night-600: #2a2f3d; --night-500: #3d4354;
38
+ --night-450: #5f6575; /* texto atenuado en light */
37
39
  --night-400: #6b7183; --night-300: #9aa0b0; --night-200: #c5c9d4;
38
40
  --night-100: #e8eaef; --night-50: #f6f7f9;
39
41
 
@@ -49,7 +51,10 @@
49
51
  --surface-2: var(--night-800);
50
52
  --surface-3: var(--night-700);
51
53
  --border: var(--night-500);
52
- --border-strong: var(--night-600);
54
+ /* night-400, no night-600: en dark, oscurecer un borde lo DEBILITA (principio 2:
55
+ la elevación se comunica aclarando). n600 daba 1.46:1 vs los 1.97:1 del borde
56
+ normal — el énfasis se veía más flojo. n400 = 3.99:1 sobre surface-1. */
57
+ --border-strong: var(--night-400);
53
58
  --text-primary: var(--night-100);
54
59
  --text-secondary: var(--night-200);
55
60
  --text-muted: var(--night-300);
@@ -120,7 +125,7 @@
120
125
  --radius-sm: 8px; --radius-md: 12px; --radius-lg: 16px;
121
126
  --radius-xl: 24px; --radius-full: 9999px;
122
127
  --border-default: 1px solid var(--night-500);
123
- --border-emphasis: 1px solid var(--night-600);
128
+ --border-emphasis: 1px solid var(--night-400); /* ver --border-strong */
124
129
  --focus-ring: 2px solid var(--accent-400);
125
130
  --focus-ring-offset: 2px;
126
131
 
@@ -153,30 +158,32 @@
153
158
  --surface-2: #ffffff;
154
159
  --surface-3: #eaf1f7;
155
160
  --border: #d8dfe8;
156
- --border-strong: #b9c3d1;
161
+ /* border-strong NO se redefine en claro: night-400 (:root) cumple 1.4.11 en
162
+ AMBOS temas (3.26-3.99:1 en dark, 4.27-4.87:1 en light). #b9c3d1 daba
163
+ 1.53-1.78:1, bajo el mínimo de 3:1 para UI. */
157
164
  --text-primary: #14171f;
158
165
  --text-secondary: #3d4354;
159
- --text-muted: #6b7183;
166
+ --text-muted: #5f6575;
160
167
  --text-disabled: #9aa0b0;
161
168
  --text-heading: #0b0d12;
162
169
  --text-on-accent: #010203; /* el CTA no cambia */
163
- --success: #15803d; --success-bg: #e3f5ea;
164
- --warning: #a16207; --warning-bg: #fbf3dd;
165
- --danger: #c03535; --danger-bg: #fbe7e7;
166
- --info: #1f87bb; --info-bg: #e3f2fb;
170
+ --success: #166534; --success-bg: #e3f5ea;
171
+ --warning: #854d0e; --warning-bg: #fbf3dd;
172
+ --danger: #b02c2c; --danger-bg: #fbe7e7;
173
+ --info: #186a94; --info-bg: #e3f2fb;
167
174
  --accent-ghost-hover: rgba(31,135,187,.08);
168
175
  --accent-ghost-active: rgba(31,135,187,.14);
169
176
  --accent-selected: rgba(82,186,237,.12);
170
177
  --overlay: rgba(20,23,31,.45);
171
178
 
172
- --link: #1f87bb;
173
- --link-hover: #186a94;
174
- --focus-color: #1f87bb;
179
+ --link: #186a94;
180
+ --link-hover: #12506f;
181
+ --focus-color: #1f87bb; /* UI (4.01:1 ≥ 3), no texto */
175
182
  --border-hover: #b9c3d1;
176
183
  --outline-hover-bg: #eaf1f7;
177
184
  --outline-active-bg: #d8dfe8;
178
185
  --text-on-danger: #ffffff;
179
- --data-color: #1f87bb;
186
+ --data-color: #186a94; /* cifras y .progress__value: es texto */
180
187
  --elevated-border: #d8dfe8;
181
188
  --card-border: #d8dfe8;
182
189
  --code-color: #186a94;
@@ -387,6 +394,8 @@ a:hover { color: var(--link-hover); }
387
394
  ============================================================ */
388
395
  .field { display: flex; flex-direction: column; margin-bottom: var(--form-gap); }
389
396
  .field__label { font-size: var(--text-sm); font-weight: 500; color: var(--text-secondary); margin-bottom: 6px; }
397
+ /* Campo obligatorio: asterisco decorativo; la semántica va en required/aria-required del control */
398
+ .field--required > .field__label::after { content: "*"; margin-left: 2px; color: var(--danger); font-weight: 600; }
390
399
  .field__help { font-size: var(--text-sm); color: var(--text-muted); margin-top: 6px; }
391
400
  .field__error {
392
401
  display: inline-flex; align-items: center; gap: 6px;
@@ -422,7 +431,7 @@ a:hover { color: var(--link-hover); }
422
431
  .input-wrap .input { padding-left: 40px; }
423
432
 
424
433
  .textarea { min-height: 96px; padding: 12px 14px; resize: vertical; line-height: var(--leading-body); }
425
- .field__count { font-size: var(--text-xs); color: var(--text-disabled); text-align: right; margin-top: 4px; }
434
+ .field__count { font-size: var(--text-xs); color: var(--text-muted); text-align: right; margin-top: 4px; }
426
435
 
427
436
  /* Select nativo estilizado (el menú custom usa .menu) */
428
437
  .select-wrap { position: relative; }
@@ -530,6 +539,12 @@ a:hover { color: var(--link-hover); }
530
539
  margin-top: -2px;
531
540
  }
532
541
  .checkbox:checked::after { transform: rotate(-45deg) scale(1); }
542
+ /* Indeterminate: guion horizontal, no el check. Solo por JS (el.indeterminate = true) */
543
+ .checkbox:indeterminate { background: var(--accent-400); border-color: var(--accent-400); }
544
+ .checkbox:indeterminate::after {
545
+ content: ""; width: 10px; height: 2px; margin-top: 0;
546
+ border: 0; border-radius: var(--radius-full); background: var(--text-on-accent); transform: none;
547
+ }
533
548
  .radio::after {
534
549
  content: ""; width: 8px; height: 8px; border-radius: var(--radius-full);
535
550
  background: var(--text-on-accent); transform: scale(0);
@@ -613,6 +628,17 @@ a:hover { color: var(--link-hover); }
613
628
  .avatar__dot--away { background: var(--warning); }
614
629
  .avatar__dot--offline { background: var(--border); }
615
630
 
631
+ /* AvatarGroup — avatares solapados + chip +N.
632
+ Borde 2px del color del fondo (--surface-1 en tarjetas, --bg en el lienzo) para separar. */
633
+ .avatar-group { display: inline-flex; align-items: center; }
634
+ .avatar-group .avatar { border: 2px solid var(--surface-1); }
635
+ .avatar-group .avatar:not(:first-child) { margin-left: -8px; }
636
+ .avatar-group__more {
637
+ background: var(--surface-3); color: var(--text-secondary);
638
+ font-family: var(--font-body); font-weight: 600;
639
+ letter-spacing: 0; text-transform: none;
640
+ }
641
+
616
642
  /* ============================================================
617
643
  12B. DIVIDER — .divider (hairline), .divider--label, .divider--vertical
618
644
  ============================================================ */
@@ -648,6 +674,32 @@ a:hover { color: var(--link-hover); }
648
674
  .stat__delta--down { color: var(--danger); }
649
675
  .stat__delta--neutral { color: var(--text-muted); }
650
676
 
677
+ /* Chip de métrica: rótulo y valor en una pastilla de una línea */
678
+ .metric-chips { display: flex; flex-wrap: wrap; gap: var(--space-2); }
679
+ .metric-chip {
680
+ display: inline-flex; align-items: center; gap: var(--space-2);
681
+ padding: var(--space-1) var(--space-3);
682
+ border: 1px solid var(--border); border-radius: var(--radius-full);
683
+ background: var(--surface-1);
684
+ font-family: var(--font-body); font-size: var(--text-sm); line-height: var(--leading-snug);
685
+ color: var(--text-secondary); white-space: nowrap;
686
+ }
687
+ .metric-chip svg { width: 16px; height: 16px; flex: none; color: var(--text-muted); }
688
+ .metric-chip__value {
689
+ font-family: var(--font-mono); font-weight: 500; color: var(--link);
690
+ font-variant-numeric: tabular-nums;
691
+ }
692
+ button.metric-chip {
693
+ min-height: var(--touch-target); padding-inline: var(--space-4);
694
+ cursor: pointer; text-align: left;
695
+ transition: background-color var(--duration-fast) var(--ease-out),
696
+ border-color var(--duration-fast) var(--ease-out),
697
+ transform var(--duration-instant) var(--ease-out);
698
+ }
699
+ button.metric-chip:hover { background: var(--outline-hover-bg); border-color: var(--border-hover); }
700
+ button.metric-chip:active { background: var(--outline-active-bg); transform: scale(.98); }
701
+ button.metric-chip:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-ring-offset); }
702
+
651
703
  /* ============================================================
652
704
  13B. PAGINATION — .pagination (activa: fondo acento + peso, no solo color)
653
705
  ============================================================ */
@@ -730,7 +782,7 @@ a:hover { color: var(--link-hover); }
730
782
  }
731
783
  .alert > svg { width: 20px; height: 20px; flex: none; margin-top: 1px; }
732
784
  .alert__title { font-weight: 600; margin: 0 0 2px; color: inherit; }
733
- .alert__body { margin: 0; color: inherit; opacity: .88; }
785
+ .alert__body { margin: 0; color: inherit; }
734
786
  .alert__close {
735
787
  margin-left: auto; flex: none; display: grid; place-items: center;
736
788
  width: 36px; height: 36px; border: 0; border-radius: var(--radius-sm);
@@ -820,9 +872,39 @@ a:hover { color: var(--link-hover); }
820
872
  }
821
873
  .table tbody tr { transition: background-color var(--duration-fast) var(--ease-out); }
822
874
  .table tbody tr:hover { background: var(--row-hover); }
823
- .table tbody tr.is-selected { background: var(--accent-selected); box-shadow: inset 2px 0 0 var(--accent-400); }
824
875
  .table .num, .table td.num { text-align: right; font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
825
876
 
877
+ /* Zebra — filas pares en surface-2; hover y selección mandan sobre la banda */
878
+ .table--zebra tbody tr:nth-child(even) { background: var(--surface-2); }
879
+ .table--zebra tbody tr:hover { background: var(--row-hover); }
880
+
881
+ /* Sticky header — dentro de un contenedor con overflow: auto y altura acotada */
882
+ .table--sticky thead th { position: sticky; top: 0; z-index: 1; }
883
+
884
+ /* Header ordenable — botón dentro del th + icono; el estado vive en aria-sort del th */
885
+ .th-sort {
886
+ display: inline-flex; align-items: center; gap: 6px;
887
+ margin: 0; padding: 0; border: 0; background: transparent;
888
+ font: inherit; color: inherit; letter-spacing: inherit; text-transform: inherit;
889
+ cursor: pointer;
890
+ }
891
+ .th-sort:hover { color: var(--text-secondary); }
892
+ .th-sort:focus-visible { outline: var(--focus-ring); outline-offset: 2px; border-radius: var(--radius-sm); }
893
+ .sort-icon {
894
+ width: 14px; height: 14px; flex: none; opacity: .45;
895
+ transition: opacity var(--duration-fast) var(--ease-out), transform var(--duration-fast) var(--ease-out);
896
+ }
897
+ .table th[aria-sort="ascending"] .sort-icon,
898
+ .table th[aria-sort="descending"] .sort-icon { opacity: 1; color: var(--accent-400); }
899
+ .table th[aria-sort="descending"] .sort-icon { transform: rotate(180deg); }
900
+
901
+ /* Columna de selección (checkbox en th y en cada fila) */
902
+ .table .col-select { width: 44px; text-align: center; padding-left: var(--space-2); padding-right: var(--space-2); }
903
+
904
+ /* Fila seleccionada — .row--selected (contrato); .is-selected se mantiene como alias */
905
+ .table tbody tr.is-selected,
906
+ .table tbody tr.row--selected { background: var(--accent-selected); box-shadow: inset 2px 0 0 var(--accent-400); }
907
+
826
908
  /* ============================================================
827
909
  19. NAVEGACIÓN — .navbar, .sidebar, .tabs, .breadcrumbs
828
910
  ============================================================ */
@@ -862,6 +944,70 @@ a:hover { color: var(--link-hover); }
862
944
  .sidebar__item svg { width: 18px; height: 18px; color: var(--text-muted); flex: none; }
863
945
  .sidebar__item.is-active svg { color: var(--sidebar-active-icon); }
864
946
 
947
+ /* Grupos con rótulo dentro del sidebar */
948
+ .sidebar__group { display: flex; flex-direction: column; gap: 2px; }
949
+ .sidebar__group + .sidebar__group { margin-top: var(--space-4); }
950
+ .sidebar__group-label {
951
+ padding: 0 12px var(--space-1);
952
+ font-size: var(--text-xs); font-weight: 500; text-transform: uppercase;
953
+ letter-spacing: var(--tracking-overline); color: var(--text-muted); white-space: nowrap;
954
+ }
955
+ .sidebar__label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
956
+
957
+ /* Sidebar contraíble: expandido 260 px, riel de iconos de 64 px.
958
+ El ancho cambia sin transición; solo los rótulos se funden con opacity y transform. */
959
+ .sidebar--collapsible .sidebar__label {
960
+ transition: opacity var(--duration-fast) var(--ease-out),
961
+ transform var(--duration-fast) var(--ease-out);
962
+ }
963
+ .sidebar__toggle {
964
+ margin-top: auto; width: 100%; border: 0; background: transparent;
965
+ font-family: var(--font-body); text-align: left; cursor: pointer;
966
+ }
967
+ .sidebar.is-collapsed { width: 64px; }
968
+ .sidebar.is-collapsed .sidebar__item { position: relative; justify-content: center; padding-inline: 0; }
969
+ .sidebar.is-collapsed .sidebar__toggle svg { transform: rotate(180deg); }
970
+ .sidebar.is-collapsed .sidebar__group + .sidebar__group { margin-top: var(--space-2); }
971
+ /* En el riel, el rótulo de grupo es un separador de 1 px; su texto sigue en el árbol de accesibilidad */
972
+ .sidebar.is-collapsed .sidebar__group-label {
973
+ height: 1px; padding: 0; margin: 0 var(--space-2) var(--space-2);
974
+ overflow: hidden; color: transparent; background: var(--border);
975
+ }
976
+ /* Oculto a la vista pero accesible (patrón de .visually-hidden) */
977
+ .sidebar.is-collapsed .sidebar__label,
978
+ .sidebar.is-collapsed .sidebar__group:first-child > .sidebar__group-label {
979
+ position: absolute; width: 1px; height: 1px; margin: -1px;
980
+ clip-path: inset(50%); overflow: hidden; white-space: nowrap;
981
+ }
982
+ .sidebar.is-collapsed .sidebar__label {
983
+ background: var(--tooltip-bg); border: 1px solid var(--tooltip-border); border-radius: var(--radius-sm);
984
+ color: var(--tooltip-text); font-size: var(--text-xs); line-height: 16px;
985
+ pointer-events: none; z-index: 70;
986
+ opacity: 0; transform: translateX(-4px);
987
+ }
988
+ /* Tooltip del riel: el propio rótulo aparece a la derecha del ítem, también con teclado */
989
+ .sidebar.is-collapsed .sidebar__item:focus-visible .sidebar__label {
990
+ left: calc(100% + 8px); top: calc(50% - 15px); width: auto; height: auto;
991
+ margin: 0; padding: 6px 10px; clip-path: none; overflow: visible;
992
+ opacity: 1; transform: none; transition-delay: 400ms;
993
+ }
994
+ /* Por hover solo con puntero real — en touch quedaría pegado tras el tap */
995
+ @media (hover: hover) and (pointer: fine) {
996
+ .sidebar.is-collapsed .sidebar__item:hover .sidebar__label {
997
+ left: calc(100% + 8px); top: calc(50% - 15px); width: auto; height: auto;
998
+ margin: 0; padding: 6px 10px; clip-path: none; overflow: visible;
999
+ opacity: 1; transform: none; transition-delay: 400ms;
1000
+ }
1001
+ }
1002
+ /* Por debajo de 768 px la navegación vive en un Drawer --left, que abre el botón ☰
1003
+ .sidebar-trigger (combinado con .icon-btn); desde 768 px el sidebar es fijo y el botón sobra */
1004
+ @media (max-width: 767px) {
1005
+ .sidebar--collapsible { display: none; }
1006
+ }
1007
+ @media (min-width: 768px) {
1008
+ .sidebar-trigger { display: none; }
1009
+ }
1010
+
865
1011
  .tabs { position: relative; display: flex; gap: var(--space-2); border-bottom: 1px solid var(--border-strong); }
866
1012
  .tabs__btn {
867
1013
  padding: 10px 14px; border: 0; background: transparent; cursor: pointer;
@@ -1007,6 +1153,273 @@ a:hover { color: var(--link-hover); }
1007
1153
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
1008
1154
  }
1009
1155
 
1156
+ /* ============================================================
1157
+ 25. COMBOBOX / AUTOCOMPLETE — .combobox + SEARCH — .search
1158
+ El listado vive sobre --surface-2 (superficie de dropdowns).
1159
+ ============================================================ */
1160
+ .combobox { position: relative; }
1161
+ .combobox__input { width: 100%; } /* consume .input para el resto de estilos */
1162
+ .combobox__list {
1163
+ position: absolute; left: 0; right: 0; top: calc(100% + 4px); z-index: 40;
1164
+ margin: 0; padding: 6px; list-style: none; max-height: 264px; overflow: auto;
1165
+ background: var(--surface-2); border: 1px solid var(--elevated-border);
1166
+ border-radius: var(--radius-md); box-shadow: var(--shadow-3);
1167
+ }
1168
+ .combobox__list[hidden] { display: none; }
1169
+ .combobox__option {
1170
+ display: flex; align-items: center; gap: 10px; width: 100%;
1171
+ padding: 10px 12px; border-radius: var(--radius-sm);
1172
+ color: var(--text-primary); font-size: var(--text-sm); cursor: pointer;
1173
+ transition: background-color var(--duration-fast) var(--ease-out);
1174
+ }
1175
+ .combobox__option[hidden] { display: none; }
1176
+ /* .is-active = resaltado por teclado; [aria-selected] = la elegida (fondo + icono, no solo color) */
1177
+ .combobox__option.is-active { background: var(--surface-3); }
1178
+ .combobox__option[aria-selected="true"] { background: var(--accent-selected); color: var(--accent-300); }
1179
+ [data-theme="light"] .combobox__option[aria-selected="true"] { color: var(--link); }
1180
+ .combobox__check { display: none; margin-left: auto; width: 16px; height: 16px; color: var(--accent-400); flex: none; }
1181
+ .combobox__option[aria-selected="true"] .combobox__check { display: block; }
1182
+
1183
+ /* Search — wrapper con lupa (aria-hidden) + botón limpiar (×, aria-label="Limpiar") */
1184
+ .search { position: relative; display: flex; align-items: center; }
1185
+ .search__icon {
1186
+ position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
1187
+ width: 18px; height: 18px; color: var(--text-disabled); pointer-events: none;
1188
+ }
1189
+ .search__input { width: 100%; padding-left: 40px; padding-right: 40px; } /* consume .input */
1190
+ /* Oculta el clear nativo de WebKit para no duplicar el affordance del .search__clear */
1191
+ .search__input::-webkit-search-cancel-button,
1192
+ .search__input::-webkit-search-decoration { -webkit-appearance: none; appearance: none; }
1193
+ .search__clear {
1194
+ position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
1195
+ display: inline-grid; place-content: center; width: 28px; height: 28px; padding: 0;
1196
+ border: 0; border-radius: var(--radius-full); background: transparent;
1197
+ color: var(--text-muted); cursor: pointer;
1198
+ transition: background-color var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out);
1199
+ }
1200
+ .search__clear:hover { background: var(--surface-3); color: var(--text-primary); }
1201
+ .search__clear svg { width: 16px; height: 16px; }
1202
+ .search__clear[hidden] { display: none; }
1203
+
1204
+ /* ============================================================
1205
+ 26. DRAWER / SHEET — .drawer + __overlay + __panel (focus-trap por JS)
1206
+ Motion: transform translate + fade del overlay (--duration-slow).
1207
+ El overlay reutiliza --overlay del modal; SIN blur nuevo.
1208
+ ============================================================ */
1209
+ .drawer { position: fixed; inset: 0; z-index: 100; }
1210
+ .drawer[hidden] { display: none; }
1211
+ .drawer__overlay {
1212
+ position: absolute; inset: 0; background: var(--overlay);
1213
+ opacity: 0; transition: opacity var(--duration-slow) var(--ease-out);
1214
+ }
1215
+ .drawer.is-open .drawer__overlay { opacity: 1; }
1216
+ .drawer__panel {
1217
+ position: absolute; top: 0; right: 0; bottom: 0;
1218
+ display: flex; flex-direction: column;
1219
+ width: min(400px, 100%); padding: var(--card-padding);
1220
+ background: var(--surface-2); border-left: 1px solid var(--elevated-border);
1221
+ box-shadow: var(--shadow-3);
1222
+ overflow-y: auto; overscroll-behavior: contain;
1223
+ transform: translateX(100%);
1224
+ transition: transform var(--duration-slow) var(--ease-out);
1225
+ }
1226
+ .drawer.is-open .drawer__panel { transform: none; }
1227
+ /* Variante izquierda */
1228
+ .drawer--left .drawer__panel {
1229
+ right: auto; left: 0; border-left: 0; border-right: 1px solid var(--elevated-border);
1230
+ transform: translateX(-100%);
1231
+ }
1232
+ .drawer--left.is-open .drawer__panel { transform: none; }
1233
+ /* Variante inferior (sheet) */
1234
+ .drawer--bottom .drawer__panel {
1235
+ top: auto; left: 0; right: 0; width: 100%; max-height: 80dvh;
1236
+ border-left: 0; border-top: 1px solid var(--elevated-border);
1237
+ border-radius: var(--radius-lg) var(--radius-lg) 0 0;
1238
+ transform: translateY(100%);
1239
+ }
1240
+ .drawer--bottom.is-open .drawer__panel { transform: none; }
1241
+ .drawer__header { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); margin-bottom: var(--space-4); }
1242
+ .drawer__title { font-family: var(--font-body); font-size: var(--text-xl); font-weight: 600; letter-spacing: normal; color: var(--text-heading); margin: 0; }
1243
+ .drawer__body { flex: 1; }
1244
+ .drawer__footer { display: flex; justify-content: flex-end; gap: var(--space-3); margin-top: var(--space-6); }
1245
+
1246
+ /* ============================================================
1247
+ 27. POPOVER — .popover + __trigger + __panel (abre por CLICK)
1248
+ Motion: fade + scale(0.96→1) desde el transform-origin del trigger.
1249
+ Panel sobre --surface-2. Se distingue del Tooltip: click, no hover.
1250
+ ============================================================ */
1251
+ .popover { position: relative; display: inline-flex; }
1252
+ .popover__panel {
1253
+ position: absolute; top: calc(100% + 8px); left: 0; z-index: 60;
1254
+ min-width: 240px; max-width: 320px; padding: var(--space-4);
1255
+ background: var(--surface-2); border: 1px solid var(--elevated-border);
1256
+ border-radius: var(--radius-md); box-shadow: var(--shadow-3);
1257
+ transform-origin: top left; /* origin-aware: crece desde el disparador */
1258
+ opacity: 0; transform: scale(.96); visibility: hidden;
1259
+ transition: opacity var(--duration-base) var(--ease-out),
1260
+ transform var(--duration-base) var(--ease-out),
1261
+ visibility 0s linear var(--duration-base);
1262
+ }
1263
+ .popover.is-open .popover__panel {
1264
+ opacity: 1; transform: none; visibility: visible;
1265
+ transition: opacity var(--duration-base) var(--ease-out),
1266
+ transform var(--duration-base) var(--ease-out),
1267
+ visibility 0s;
1268
+ }
1269
+ /* Alineado a la derecha del disparador */
1270
+ .popover--right .popover__panel { left: auto; right: 0; transform-origin: top right; }
1271
+
1272
+ /* ============================================================
1273
+ 28. BUTTON GROUP — .btn-group (bordes compartidos, radios en extremos)
1274
+ ============================================================ */
1275
+ .btn-group { display: inline-flex; }
1276
+ .btn-group > .btn { border-radius: 0; }
1277
+ .btn-group > .btn:not(:first-child) { margin-left: -1px; }
1278
+ .btn-group > .btn:first-child { border-top-left-radius: var(--radius-md); border-bottom-left-radius: var(--radius-md); }
1279
+ .btn-group > .btn:last-child { border-top-right-radius: var(--radius-md); border-bottom-right-radius: var(--radius-md); }
1280
+ /* El botón enfocado/hover sube de z-index para que su borde no lo tape el vecino */
1281
+ .btn-group > .btn:hover, .btn-group > .btn:focus-visible, .btn-group > .btn.is-active { z-index: 1; }
1282
+
1283
+ /* ============================================================
1284
+ 29. DATEPICKER — .datepicker + calendario en popover
1285
+ El panel reutiliza el look de .popover__panel sobre --surface-2.
1286
+ Grid role="grid"; días como <button role="gridcell">.
1287
+ ============================================================ */
1288
+ .datepicker { position: relative; }
1289
+ .datepicker__input { cursor: pointer; } /* consume .input; el icono va con .input-wrap */
1290
+ .datepicker__calendar {
1291
+ position: absolute; top: calc(100% + 8px); left: 0; z-index: 60;
1292
+ padding: var(--space-4);
1293
+ background: var(--surface-2); border: 1px solid var(--elevated-border);
1294
+ border-radius: var(--radius-md); box-shadow: var(--shadow-3);
1295
+ }
1296
+ .datepicker__calendar[hidden] { display: none; }
1297
+
1298
+ .calendar { width: 252px; }
1299
+ .calendar__header { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--space-3); }
1300
+ .calendar__title { font-size: var(--text-sm); font-weight: 600; color: var(--text-heading); } /* mes en minúscula (es) */
1301
+ .calendar__weekdays { display: grid; grid-template-columns: repeat(7, 1fr); margin-bottom: var(--space-1); }
1302
+ .calendar__weekdays span {
1303
+ display: grid; place-items: center; height: 28px;
1304
+ font-size: var(--text-xs); font-weight: 500; color: var(--text-muted);
1305
+ }
1306
+ .calendar__grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
1307
+ .calendar__week { display: contents; } /* fila semántica (role="row") sin romper el grid */
1308
+ .calendar__day {
1309
+ display: grid; place-items: center; width: 100%; height: 36px; padding: 0;
1310
+ border: 1px solid transparent; border-radius: var(--radius-sm); background: transparent;
1311
+ color: var(--text-primary); font-family: var(--font-mono); font-size: var(--text-sm);
1312
+ font-variant-numeric: tabular-nums; cursor: pointer;
1313
+ transition: background-color var(--duration-fast) var(--ease-out),
1314
+ color var(--duration-fast) var(--ease-out);
1315
+ }
1316
+ .calendar__day:hover:not([disabled]):not(.calendar__day--selected) { background: var(--surface-3); }
1317
+ /* Hoy: aro de acento por borde (no glow) + texto de acento */
1318
+ .calendar__day--today { border-color: var(--accent-400); color: var(--accent-300); }
1319
+ /* Seleccionado: fondo de acento + texto oscuro + peso — no depende solo del color */
1320
+ .calendar__day--selected { background: var(--accent-400); border-color: var(--accent-400); color: var(--text-on-accent); font-weight: 600; }
1321
+ .calendar__day--outside { color: var(--text-muted); }
1322
+ .calendar__day[disabled] { color: var(--text-disabled); opacity: .5; cursor: not-allowed; }
1323
+
1324
+ /* ============================================================
1325
+ 30. FILE UPLOAD — .file-upload (dropzone) + __list
1326
+ El input file real va oculto accesiblemente (.visually-hidden).
1327
+ ============================================================ */
1328
+ .file-upload { display: block; }
1329
+ .file-upload__label {
1330
+ display: flex; flex-direction: column; align-items: center; gap: var(--space-2);
1331
+ padding: var(--space-8) var(--space-6); text-align: center;
1332
+ background: var(--surface-1); border: 1px dashed var(--border-strong);
1333
+ border-radius: var(--radius-lg); color: var(--text-muted); cursor: pointer;
1334
+ transition: border-color var(--duration-fast) var(--ease-out),
1335
+ background-color var(--duration-fast) var(--ease-out),
1336
+ color var(--duration-fast) var(--ease-out);
1337
+ }
1338
+ .file-upload__label > svg { width: 28px; height: 28px; color: var(--text-muted); }
1339
+ .file-upload__button { color: var(--link); font-weight: 600; }
1340
+ .file-upload__hint { font-size: var(--text-xs); color: var(--text-muted); }
1341
+ @media (hover: hover) and (pointer: fine) {
1342
+ .file-upload__label:hover { border-color: var(--border-hover); background: var(--surface-2); color: var(--text-secondary); }
1343
+ }
1344
+ /* El ring se pinta sobre el label porque el input está oculto */
1345
+ .file-upload__input:focus-visible + .file-upload__label { outline: var(--focus-ring); outline-offset: var(--focus-ring-offset); }
1346
+ /* Arrastrando encima: acento (icono + borde, no solo color) */
1347
+ .file-upload.is-dragover .file-upload__label { border-color: var(--accent-400); background: var(--accent-ghost-hover); color: var(--text-secondary); }
1348
+
1349
+ .file-upload__list { list-style: none; margin: var(--space-3) 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2); }
1350
+ .file-upload__item {
1351
+ display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: var(--space-3);
1352
+ padding: var(--space-3); background: var(--surface-2);
1353
+ border: 1px solid var(--border); border-radius: var(--radius-md);
1354
+ }
1355
+ .file-upload__item > svg { width: 20px; height: 20px; color: var(--text-muted); flex: none; }
1356
+ .file-upload__item > div { min-width: 0; } /* permite truncar el nombre */
1357
+ .file-upload__item .progress { margin-top: var(--space-2); }
1358
+ .file-upload__name { display: block; font-size: var(--text-sm); color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1359
+ .file-upload__size { display: block; font-size: var(--text-xs); color: var(--text-muted); font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
1360
+ /* Error: borde + icono + texto en danger, nunca solo por color */
1361
+ .file-upload__item--error { border-color: color-mix(in srgb, var(--danger) 45%, transparent); background: var(--danger-bg); }
1362
+ .file-upload__item--error > svg { color: var(--danger); }
1363
+ .file-upload__item--error .file-upload__size { color: var(--danger); }
1364
+ .file-upload__remove {
1365
+ display: inline-grid; place-content: center; width: 28px; height: 28px; padding: 0; flex: none;
1366
+ border: 0; border-radius: var(--radius-full); background: transparent;
1367
+ color: var(--text-muted); cursor: pointer;
1368
+ transition: background-color var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out);
1369
+ }
1370
+ .file-upload__remove:hover { background: var(--surface-3); color: var(--text-primary); }
1371
+ .file-upload__remove svg { width: 16px; height: 16px; }
1372
+
1373
+ /* ============================================================
1374
+ 31. STEPPER / WIZARD — .stepper (<ol>) + .step
1375
+ Completo → check (no solo color); actual → acento.
1376
+ ============================================================ */
1377
+ .stepper { display: flex; list-style: none; margin: 0; padding: 0; }
1378
+ .step {
1379
+ position: relative; flex: 1; display: flex; flex-direction: column; align-items: center;
1380
+ gap: var(--space-2); text-align: center; min-width: 0;
1381
+ }
1382
+ .step__marker {
1383
+ position: relative; z-index: 1;
1384
+ display: grid; place-items: center; width: 32px; height: 32px; flex: none;
1385
+ border-radius: var(--radius-full); border: 2px solid var(--border);
1386
+ background: var(--surface-1); color: var(--text-muted);
1387
+ font-family: var(--font-mono); font-size: var(--text-sm); font-weight: 600; font-variant-numeric: tabular-nums;
1388
+ }
1389
+ .step__marker svg { width: 16px; height: 16px; }
1390
+ .step__label { font-size: var(--text-sm); color: var(--text-muted); }
1391
+ .step__connector {
1392
+ position: absolute; top: 15px; left: 50%; z-index: 0;
1393
+ width: 100%; height: 2px; background: var(--border);
1394
+ }
1395
+ /* Completo */
1396
+ .step--complete .step__marker { background: var(--accent-400); border-color: var(--accent-400); color: var(--text-on-accent); }
1397
+ .step--complete .step__label { color: var(--text-secondary); }
1398
+ .step--complete .step__connector { background: var(--accent-400); }
1399
+ /* Actual */
1400
+ .step--current .step__marker { border-color: var(--accent-400); color: var(--accent-300); }
1401
+ .step--current .step__label { color: var(--text-primary); font-weight: 600; }
1402
+ /* Próximo = valores por defecto (atenuado) */
1403
+
1404
+ /* Variante vertical */
1405
+ .stepper--vertical { flex-direction: column; }
1406
+ .stepper--vertical .step { flex-direction: row; align-items: flex-start; gap: var(--space-3); text-align: left; padding-bottom: var(--space-6); }
1407
+ .stepper--vertical .step:last-child { padding-bottom: 0; }
1408
+ .stepper--vertical .step__label { padding-top: var(--space-1); }
1409
+ .stepper--vertical .step__connector { top: 34px; left: 15px; width: 2px; height: calc(100% - 34px); }
1410
+
1411
+ /* ============================================================
1412
+ 32. KBD — .kbd (tecla / atajo de teclado)
1413
+ Combos: varias .kbd con un "+" literal entre ellas.
1414
+ ============================================================ */
1415
+ .kbd {
1416
+ display: inline-flex; align-items: center; justify-content: center;
1417
+ min-width: 20px; height: 20px; padding: 0 6px;
1418
+ font-family: var(--font-mono); font-size: var(--text-xs); font-weight: 500; line-height: 1;
1419
+ color: var(--text-secondary); background: var(--surface-2);
1420
+ border: 1px solid var(--border-strong); border-radius: 6px; box-shadow: var(--shadow-1);
1421
+ }
1422
+
1010
1423
 
1011
1424
  /* ============================================================
1012
1425
  ESTILOS DE LA PÁGINA DE DOCS (.ds-*)
@@ -1071,10 +1484,23 @@ html { scroll-behavior: smooth; }
1071
1484
  .ds-demo--grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); align-items: start; }
1072
1485
  .ds-demo:has(table) { overflow-x: auto; }
1073
1486
 
1487
+ /* Demo del sidebar contraíble: rejilla auto 1fr, el contenido sigue al riel sin animar el ancho */
1488
+ .ds-app-demo { display: grid; grid-template-columns: auto 1fr; align-items: stretch; gap: 0; min-height: 400px; padding: 0; }
1489
+ .ds-app-demo > .sidebar { border-radius: var(--radius-lg) 0 0 var(--radius-lg); }
1490
+ .ds-app-demo__main { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-3); min-width: 0; padding: var(--space-6); }
1491
+ @media (max-width: 767px) {
1492
+ .ds-app-demo { grid-template-columns: 1fr; min-height: 0; }
1493
+ }
1494
+
1074
1495
  .ds-code { margin-top: var(--space-3); border: 1px solid var(--row-border); border-radius: var(--radius-md); overflow: hidden; }
1496
+ /* «Copiar» es hermano del details, no hijo del summary (un control no puede
1497
+ anidarse en otro), y se superpone al extremo derecho de la cabecera. */
1498
+ .ds-code-wrap { position: relative; margin-top: var(--space-3); }
1499
+ .ds-code-wrap > .ds-code { margin-top: 0; }
1500
+ .ds-code-wrap > .ds-copy { position: absolute; top: 11px; right: 17px; }
1075
1501
  .ds-code summary {
1076
1502
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
1077
- padding: 10px 16px; cursor: pointer; user-select: none;
1503
+ min-height: 45px; padding: 10px 96px 10px 16px; cursor: pointer; user-select: none;
1078
1504
  font-size: var(--text-sm); color: var(--text-muted); list-style: none;
1079
1505
  }
1080
1506
  .ds-code summary::-webkit-details-marker { display: none; }
@@ -1220,6 +1646,14 @@ html { scroll-behavior: smooth; }
1220
1646
  <a href="#comp-navegacion">Navegación</a>
1221
1647
  <a href="#comp-skeleton">Skeleton y EmptyState</a>
1222
1648
  <a href="#comp-card">Card, Link y Code</a>
1649
+ <a href="#comp-combobox">Combobox y Search</a>
1650
+ <a href="#comp-drawer">Drawer</a>
1651
+ <a href="#comp-popover">Popover</a>
1652
+ <a href="#comp-buttongroup">ButtonGroup</a>
1653
+ <a href="#comp-datepicker">DatePicker</a>
1654
+ <a href="#comp-fileupload">FileUpload</a>
1655
+ <a href="#comp-stepper">Stepper</a>
1656
+ <a href="#comp-kbd">Kbd</a>
1223
1657
  </div>
1224
1658
  <div class="overline ds-group">Recursos</div>
1225
1659
  <a href="#motion">Motion</a>
@@ -1284,7 +1718,7 @@ html { scroll-behavior: smooth; }
1284
1718
  <div class="ds-swatches" id="swIndigo"></div>
1285
1719
  <div class="ds-callout ds-callout--warning">
1286
1720
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.7 18-8-14a2 2 0 0 0-3.5 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3z"/><path d="M12 9v4m0 4h.01"/></svg>
1287
- <div>El índigo <span class="code">#353d7c</span> sobre negro da un contraste de ≈ 1.9:1 — <strong>nunca es texto sobre fondos nocturnos</strong>, solo fondos de sección, sidebars y estados seleccionados.</div>
1721
+ <div>El índigo <span class="code">#353d7c</span> sobre negro da un contraste de 2.09:1 — <strong>nunca es texto sobre fondos nocturnos</strong>, solo fondos de sección, sidebars y estados seleccionados.</div>
1288
1722
  </div>
1289
1723
 
1290
1724
  <h3 style="margin-top:32px;">Neutrales — Escala Nocturna</h3>
@@ -1348,11 +1782,11 @@ html { scroll-behavior: smooth; }
1348
1782
  <div class="ds-elev-demo" style="background:var(--bg);border:1px dashed var(--border);">Nivel 0<br>night-950</div>
1349
1783
  <div class="ds-elev-demo" style="background:var(--surface-1);box-shadow:var(--shadow-1);border:1px solid var(--row-border);">Nivel 1<br>night-900 + shadow-1</div>
1350
1784
  <div class="ds-elev-demo" style="background:var(--surface-2);box-shadow:var(--shadow-2);border:1px solid var(--row-border);">Nivel 2<br>night-800 + shadow-2</div>
1351
- <div class="ds-elev-demo" style="background:var(--surface-2);box-shadow:var(--shadow-3);border:1px solid var(--border-strong);">Nivel 3<br>night-800 + borde night-600 + shadow-3</div>
1785
+ <div class="ds-elev-demo" style="background:var(--surface-2);box-shadow:var(--shadow-3);border:1px solid var(--border-strong);">Nivel 3<br>night-800 + borde night-400 + shadow-3</div>
1352
1786
  </div>
1353
1787
  <h3 style="margin-top:32px;">Glow de acento</h3>
1354
1788
  <div class="ds-demo">
1355
- <div class="ds-elev-demo" style="background:var(--surface-1);box-shadow:var(--glow-accent);max-width:280px;">glow-accent<br><span style="color:var(--text-disabled)">máximo uno por pantalla</span></div>
1789
+ <div class="ds-elev-demo" style="background:var(--surface-1);box-shadow:var(--glow-accent);max-width:280px;">glow-accent<br><span style="color:var(--text-muted)">máximo uno por pantalla</span></div>
1356
1790
  </div>
1357
1791
  </section>
1358
1792
 
@@ -1386,13 +1820,13 @@ html { scroll-behavior: smooth; }
1386
1820
  <section id="componentes">
1387
1821
  <span class="overline">Biblioteca</span>
1388
1822
  <h2>Componentes</h2>
1389
- <p class="ds-lead">Los 26 componentes del sistema, renderizados en vivo con las clases del framework <span class="code">nocturna-electrica.css</span>. Cada bloque incluye su código de uso.</p>
1823
+ <p class="ds-lead">Los 35 componentes del sistema, renderizados en vivo con las clases del framework <span class="code">nocturna-electrica.css</span>. Cada bloque incluye su código de uso.</p>
1390
1824
  </section>
1391
1825
 
1392
1826
  <!-- ================= BOTONES ================= -->
1393
1827
  <section id="comp-botones">
1394
1828
  <h2>Botones</h2>
1395
- <p>Alturas 36 / 44 / 52px. El primario lleva <strong>texto oscuro</strong> (contraste ≈ 8.5:1) — nunca blanco. Un solo primario por pantalla.</p>
1829
+ <p>Alturas 36 / 44 / 52px. El primario lleva <strong>texto oscuro</strong> (contraste 9.50:1) — nunca blanco. Un solo primario por pantalla.</p>
1396
1830
  <div class="ds-demo">
1397
1831
  <button class="btn btn--primary">Guardar cambios</button>
1398
1832
  <button class="btn btn--secondary">Ver informe</button>
@@ -1406,16 +1840,16 @@ html { scroll-behavior: smooth; }
1406
1840
  <button class="btn btn--primary">Mediano</button>
1407
1841
  <button class="btn btn--primary btn--lg">Grande</button>
1408
1842
  <button class="btn btn--outline"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14m-7-7v14"/></svg>Con icono</button>
1409
- <button class="btn btn--primary" style="min-width:160px;"><span class="spinner" style="color:currentColor;"></span></button>
1843
+ <button class="btn btn--primary" style="min-width:160px;" aria-label="Cargando"><span class="spinner" style="color:currentColor;"></span></button>
1410
1844
  </div>
1411
- <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;button class="btn btn--primary"&gt;Guardar cambios&lt;/button&gt;
1845
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;button class="btn btn--primary"&gt;Guardar cambios&lt;/button&gt;
1412
1846
  &lt;button class="btn btn--secondary"&gt;Ver informe&lt;/button&gt;
1413
1847
  &lt;button class="btn btn--outline"&gt;Exportar&lt;/button&gt;
1414
1848
  &lt;button class="btn btn--ghost"&gt;Cancelar&lt;/button&gt;
1415
1849
  &lt;button class="btn btn--danger"&gt;Eliminar cuenta&lt;/button&gt;
1416
1850
 
1417
1851
  &lt;!-- Tamaños: .btn--sm (36px) · default (44px) · .btn--lg (52px) --&gt;
1418
- &lt;!-- Loading: fijar min-width y sustituir el texto por .spinner --&gt;</code></pre></details>
1852
+ &lt;!-- Loading: fijar min-width y sustituir el texto por .spinner --&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
1419
1853
  </section>
1420
1854
 
1421
1855
  <!-- ================= ICON BUTTON ================= -->
@@ -1429,10 +1863,10 @@ html { scroll-behavior: smooth; }
1429
1863
  <button class="icon-btn icon-btn--outline icon-btn--sm" aria-label="Copiar"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="8" width="14" height="14" rx="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg></button>
1430
1864
  <button class="icon-btn icon-btn--outline" disabled aria-label="Deshabilitado"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
1431
1865
  </div>
1432
- <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;button class="icon-btn icon-btn--outline" aria-label="Buscar"&gt;
1866
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;button class="icon-btn icon-btn--outline" aria-label="Buscar"&gt;
1433
1867
  &lt;svg&gt;…&lt;/svg&gt;
1434
1868
  &lt;/button&gt;
1435
- &lt;!-- Variantes: --outline · --ghost · --accent | Tamaños: --sm (36) · default (44) · --lg (52) --&gt;</code></pre></details>
1869
+ &lt;!-- Variantes: --outline · --ghost · --accent | Tamaños: --sm (36) · default (44) · --lg (52) --&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
1436
1870
  </section>
1437
1871
 
1438
1872
  <!-- ================= SPINNER ================= -->
@@ -1444,8 +1878,8 @@ html { scroll-behavior: smooth; }
1444
1878
  <span class="spinner" style="width:24px;height:24px;"></span>
1445
1879
  <span class="spinner" style="width:32px;height:32px;color:var(--text-muted);"></span>
1446
1880
  </div>
1447
- <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;span class="spinner"&gt;&lt;/span&gt;
1448
- &lt;!-- Hereda currentColor; default accent-400 --&gt;</code></pre></details>
1881
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;span class="spinner"&gt;&lt;/span&gt;
1882
+ &lt;!-- Hereda currentColor; default accent-400 --&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
1449
1883
  </section>
1450
1884
 
1451
1885
  <!-- ================= INPUT ================= -->
@@ -1474,14 +1908,25 @@ html { scroll-behavior: smooth; }
1474
1908
  <label class="field__label" for="i4">Deshabilitado</label>
1475
1909
  <input class="input" id="i4" type="text" value="Solo lectura" disabled>
1476
1910
  </div>
1911
+ <div class="field field--required">
1912
+ <label class="field__label" for="i5">Nombre completo</label>
1913
+ <input class="input" id="i5" type="text" placeholder="Tu nombre" required aria-required="true">
1914
+ <span class="field__help">El asterisco es decorativo; lo obligatorio lo marca <span class="code">required</span>.</span>
1915
+ </div>
1477
1916
  </div>
1478
- <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;div class="field"&gt;
1917
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;div class="field"&gt;
1479
1918
  &lt;label class="field__label" for="email"&gt;Correo&lt;/label&gt;
1480
1919
  &lt;input class="input" id="email" type="email" placeholder="tu@correo.com"&gt;
1481
1920
  &lt;span class="field__help"&gt;Texto de ayuda.&lt;/span&gt;
1482
1921
  &lt;/div&gt;
1922
+ &lt;!-- Obligatorio: .field--required en el .field pinta el asterisco (decorativo); --&gt;
1923
+ &lt;!-- el control lleva required + aria-required="true" --&gt;
1924
+ &lt;div class="field field--required"&gt;
1925
+ &lt;label class="field__label" for="name"&gt;Nombre&lt;/label&gt;
1926
+ &lt;input class="input" id="name" type="text" required aria-required="true"&gt;
1927
+ &lt;/div&gt;
1483
1928
  &lt;!-- Estados: .input--error (+ .field__error) · .input--success · disabled --&gt;
1484
- &lt;!-- Icono leading: envolver en .input-wrap con un svg antes del input --&gt;</code></pre></details>
1929
+ &lt;!-- Icono leading: envolver en .input-wrap con un svg antes del input --&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
1485
1930
  </section>
1486
1931
 
1487
1932
  <!-- ================= TEXTAREA ================= -->
@@ -1495,11 +1940,11 @@ html { scroll-behavior: smooth; }
1495
1940
  <span class="field__count">0 / 280</span>
1496
1941
  </div>
1497
1942
  </div>
1498
- <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;div class="field"&gt;
1943
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;div class="field"&gt;
1499
1944
  &lt;label class="field__label" for="desc"&gt;Descripción&lt;/label&gt;
1500
1945
  &lt;textarea class="textarea" id="desc" placeholder="Describe el proyecto…"&gt;&lt;/textarea&gt;
1501
1946
  &lt;span class="field__count"&gt;0 / 280&lt;/span&gt;
1502
- &lt;/div&gt;</code></pre></details>
1947
+ &lt;/div&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
1503
1948
  </section>
1504
1949
 
1505
1950
  <!-- ================= SELECT ================= -->
@@ -1517,9 +1962,9 @@ html { scroll-behavior: smooth; }
1517
1962
  </div>
1518
1963
  </div>
1519
1964
  </div>
1520
- <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;div class="select-wrap"&gt;
1965
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;div class="select-wrap"&gt;
1521
1966
  &lt;select class="select"&gt;&lt;option&gt;Opción&lt;/option&gt;&lt;/select&gt;
1522
- &lt;/div&gt;</code></pre></details>
1967
+ &lt;/div&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
1523
1968
  </section>
1524
1969
 
1525
1970
  <!-- ================= SEGMENTEDCONTROL ================= -->
@@ -1538,11 +1983,11 @@ html { scroll-behavior: smooth; }
1538
1983
  <input type="radio" name="seg-per" id="seg-12m"><label for="seg-12m">12 m</label>
1539
1984
  </div>
1540
1985
  </div>
1541
- <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;div class="segmented" role="radiogroup" aria-label="Vista"&gt;
1986
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;div class="segmented" role="radiogroup" aria-label="Vista"&gt;
1542
1987
  &lt;input type="radio" name="vista" id="lista" checked&gt;&lt;label for="lista"&gt;Lista&lt;/label&gt;
1543
1988
  &lt;input type="radio" name="vista" id="tablero"&gt;&lt;label for="tablero"&gt;Tablero&lt;/label&gt;
1544
1989
  &lt;/div&gt;
1545
- &lt;!-- Compacto: añadir segmented--sm --&gt;</code></pre></details>
1990
+ &lt;!-- Compacto: añadir segmented--sm --&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
1546
1991
  </section>
1547
1992
 
1548
1993
  <!-- ================= SLIDER ================= -->
@@ -1559,12 +2004,12 @@ html { scroll-behavior: smooth; }
1559
2004
  </div>
1560
2005
  <input class="slider js-slider" type="range" min="0" max="100" value="70" disabled aria-label="Slider deshabilitado">
1561
2006
  </div>
1562
- <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;div class="slider-row"&gt;
2007
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;div class="slider-row"&gt;
1563
2008
  &lt;span class="slider-label"&gt;Límite de gasto&lt;/span&gt;
1564
2009
  &lt;span class="slider-value"&gt;$ 350&lt;/span&gt;
1565
2010
  &lt;/div&gt;
1566
2011
  &lt;input class="slider" type="range" min="0" max="1000" value="350"&gt;
1567
- &lt;!-- JS: --slider-fill = ((v-min)/(max-min))*100 + '%' --&gt;</code></pre></details>
2012
+ &lt;!-- JS: --slider-fill = ((v-min)/(max-min))*100 + '%' --&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
1568
2013
  </section>
1569
2014
 
1570
2015
  <!-- ================= CHECKBOX Y RADIO ================= -->
@@ -1573,11 +2018,14 @@ html { scroll-behavior: smooth; }
1573
2018
  <div class="ds-demo" style="gap:32px;">
1574
2019
  <label class="check-row"><input type="checkbox" class="checkbox" checked> Notificarme por correo</label>
1575
2020
  <label class="check-row"><input type="checkbox" class="checkbox"> Recordar sesión</label>
2021
+ <label class="check-row"><input type="checkbox" class="checkbox" id="cbIndeterminate"> Seleccionar todo <span class="text-muted">(indeterminate)</span></label>
1576
2022
  <label class="check-row"><input type="radio" name="r1" class="radio" checked> Mensual</label>
1577
2023
  <label class="check-row"><input type="radio" name="r1" class="radio"> Anual</label>
1578
2024
  </div>
1579
- <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;label class="check-row"&gt;&lt;input type="checkbox" class="checkbox"&gt; Etiqueta&lt;/label&gt;
1580
- &lt;label class="check-row"&gt;&lt;input type="radio" name="g" class="radio"&gt; Opción&lt;/label&gt;</code></pre></details>
2025
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;label class="check-row"&gt;&lt;input type="checkbox" class="checkbox"&gt; Etiqueta&lt;/label&gt;
2026
+ &lt;label class="check-row"&gt;&lt;input type="radio" name="g" class="radio"&gt; Opción&lt;/label&gt;
2027
+ &lt;!-- Indeterminate = tercer estado (guion horizontal). Solo por JS: --&gt;
2028
+ &lt;!-- document.querySelector('.checkbox').indeterminate = true; --&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
1581
2029
  </section>
1582
2030
 
1583
2031
  <!-- ================= SWITCH ================= -->
@@ -1586,7 +2034,7 @@ html { scroll-behavior: smooth; }
1586
2034
  <div class="ds-demo" style="gap:32px;">
1587
2035
  <label class="check-row"><input type="checkbox" class="switch" checked> Modo compacto</label>
1588
2036
  </div>
1589
- <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;label class="check-row"&gt;&lt;input type="checkbox" class="switch"&gt; Activar&lt;/label&gt;</code></pre></details>
2037
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;label class="check-row"&gt;&lt;input type="checkbox" class="switch"&gt; Activar&lt;/label&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
1590
2038
  </section>
1591
2039
 
1592
2040
  <!-- ================= BADGES ================= -->
@@ -1600,8 +2048,8 @@ html { scroll-behavior: smooth; }
1600
2048
  <span class="tag">diseño <button class="tag__x" aria-label="Quitar etiqueta diseño">×</button></span>
1601
2049
  <span class="tag">frontend <button class="tag__x" aria-label="Quitar etiqueta frontend">×</button></span>
1602
2050
  </div>
1603
- <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;span class="badge badge--success"&gt;Activo&lt;/span&gt;
1604
- &lt;span class="tag"&gt;diseño &lt;button class="tag__x" aria-label="Quitar"&gt;×&lt;/button&gt;&lt;/span&gt;</code></pre></details>
2051
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;span class="badge badge--success"&gt;Activo&lt;/span&gt;
2052
+ &lt;span class="tag"&gt;diseño &lt;button class="tag__x" aria-label="Quitar"&gt;×&lt;/button&gt;&lt;/span&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
1605
2053
  </section>
1606
2054
 
1607
2055
  <!-- ================= AVATAR ================= -->
@@ -1615,9 +2063,28 @@ html { scroll-behavior: smooth; }
1615
2063
  <span class="avatar avatar--lg">hs<span class="avatar__dot avatar__dot--busy"></span></span>
1616
2064
  <span class="avatar avatar--xl">hs<span class="avatar__dot avatar__dot--away"></span></span>
1617
2065
  </div>
1618
- <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;span class="avatar"&gt;HS&lt;span class="avatar__dot avatar__dot--online"&gt;&lt;/span&gt;&lt;/span&gt;
2066
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;span class="avatar"&gt;HS&lt;span class="avatar__dot avatar__dot--online"&gt;&lt;/span&gt;&lt;/span&gt;
1619
2067
  &lt;!-- Tamaños: --xs 24 · --sm 32 · default 40 · --lg 48 · --xl 64 --&gt;
1620
- &lt;!-- Con imagen: &lt;span class="avatar"&gt;&lt;img src="…" alt="Nombre"&gt;&lt;/span&gt; --&gt;</code></pre></details>
2068
+ &lt;!-- Con imagen: &lt;span class="avatar"&gt;&lt;img src="…" alt="Nombre"&gt;&lt;/span&gt; --&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
2069
+
2070
+ <h3 style="margin-top:var(--space-8);">AvatarGroup</h3>
2071
+ <p>Avatares solapados 8px con borde <span class="code">2px</span> del color del fondo; el chip <span class="code">.avatar-group__more</span> resume los ocultos.</p>
2072
+ <div class="ds-demo">
2073
+ <div class="avatar-group" role="group" aria-label="Equipo del proyecto: 4 personas y 3 más">
2074
+ <span class="avatar">hs</span>
2075
+ <span class="avatar">ml</span>
2076
+ <span class="avatar">ar</span>
2077
+ <span class="avatar">jt</span>
2078
+ <span class="avatar avatar-group__more">+3</span>
2079
+ </div>
2080
+ </div>
2081
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;div class="avatar-group" role="group" aria-label="Equipo del proyecto"&gt;
2082
+ &lt;span class="avatar"&gt;HS&lt;/span&gt;
2083
+ &lt;span class="avatar"&gt;ML&lt;/span&gt;
2084
+ &lt;span class="avatar"&gt;AR&lt;/span&gt;
2085
+ &lt;span class="avatar avatar-group__more"&gt;+3&lt;/span&gt;
2086
+ &lt;/div&gt;
2087
+ &lt;!-- Solapamiento -8px + borde del color del fondo (--surface-1 / --bg). --&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
1621
2088
  </section>
1622
2089
 
1623
2090
  <!-- ================= DIVIDER ================= -->
@@ -1631,9 +2098,9 @@ html { scroll-behavior: smooth; }
1631
2098
  <span>Filtros</span><span class="divider--vertical" role="separator"></span><span>Ordenar</span><span class="divider--vertical" role="separator"></span><span>Exportar</span>
1632
2099
  </div>
1633
2100
  </div>
1634
- <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;hr class="divider"&gt;
2101
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;hr class="divider"&gt;
1635
2102
  &lt;div class="divider--label" role="separator"&gt;o continúa con&lt;/div&gt;
1636
- &lt;span class="divider--vertical" role="separator"&gt;&lt;/span&gt;</code></pre></details>
2103
+ &lt;span class="divider--vertical" role="separator"&gt;&lt;/span&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
1637
2104
  </section>
1638
2105
 
1639
2106
  <!-- ================= ACCORDION ================= -->
@@ -1651,11 +2118,11 @@ html { scroll-behavior: smooth; }
1651
2118
  </div>
1652
2119
  <div class="accordion__item">
1653
2120
  <button class="accordion__header" aria-expanded="false">¿Por qué el CTA lleva texto oscuro?<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg></button>
1654
- <div class="accordion__panel"><div class="accordion__inner"><div class="accordion__content">Negro nocturno sobre celeste eléctrico da ≈ 8.5:1 de contraste; blanco sobre celeste no pasa AA.</div></div></div>
2121
+ <div class="accordion__panel"><div class="accordion__inner"><div class="accordion__content">Negro nocturno sobre celeste eléctrico da 9.50:1 de contraste; blanco sobre celeste no pasa AA.</div></div></div>
1655
2122
  </div>
1656
2123
  </div>
1657
2124
  </div>
1658
- <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;div class="accordion"&gt;
2125
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;div class="accordion"&gt;
1659
2126
  &lt;div class="accordion__item"&gt;
1660
2127
  &lt;button class="accordion__header" aria-expanded="false"&gt;
1661
2128
  Pregunta &lt;svg&gt;chevron&lt;/svg&gt;
@@ -1665,7 +2132,7 @@ html { scroll-behavior: smooth; }
1665
2132
  &lt;/div&gt;&lt;/div&gt;
1666
2133
  &lt;/div&gt;
1667
2134
  &lt;/div&gt;
1668
- &lt;!-- JS: alternar aria-expanded en el header --&gt;</code></pre></details>
2135
+ &lt;!-- JS: alternar aria-expanded en el header --&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
1669
2136
  </section>
1670
2137
 
1671
2138
  <!-- ================= TOOLTIP ================= -->
@@ -1682,10 +2149,10 @@ html { scroll-behavior: smooth; }
1682
2149
  <span class="tooltip" role="tooltip">Ajustes</span>
1683
2150
  </span>
1684
2151
  </div>
1685
- <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;span class="has-tooltip"&gt;
2152
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;span class="has-tooltip"&gt;
1686
2153
  &lt;button class="btn btn--outline"&gt;Pasa el cursor&lt;/button&gt;
1687
2154
  &lt;span class="tooltip" role="tooltip"&gt;Exportar como CSV&lt;/span&gt;
1688
- &lt;/span&gt;</code></pre></details>
2155
+ &lt;/span&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
1689
2156
  </section>
1690
2157
 
1691
2158
  <!-- ================= ALERT ================= -->
@@ -1726,7 +2193,7 @@ html { scroll-behavior: smooth; }
1726
2193
  <button class="alert__close js-alert-close" aria-label="Cerrar aviso"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
1727
2194
  </div>
1728
2195
  </div>
1729
- <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;div class="alert alert--warning" role="status"&gt;
2196
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;div class="alert alert--warning" role="status"&gt;
1730
2197
  &lt;svg&gt;…icono…&lt;/svg&gt;
1731
2198
  &lt;div&gt;
1732
2199
  &lt;p class="alert__title"&gt;Tu plan expira en 5 días&lt;/p&gt;
@@ -1734,7 +2201,7 @@ html { scroll-behavior: smooth; }
1734
2201
  &lt;/div&gt;
1735
2202
  &lt;button class="alert__close" aria-label="Cerrar aviso"&gt;…x…&lt;/button&gt;
1736
2203
  &lt;/div&gt;
1737
- &lt;!-- Variantes: --info · --success · --warning · --danger --&gt;</code></pre></details>
2204
+ &lt;!-- Variantes: --info · --success · --warning · --danger --&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
1738
2205
  </section>
1739
2206
 
1740
2207
  <!-- ================= TOAST ================= -->
@@ -1747,7 +2214,7 @@ html { scroll-behavior: smooth; }
1747
2214
  <button class="btn btn--outline" data-toast="warning">Toast de aviso</button>
1748
2215
  <button class="btn btn--outline" data-toast="danger">Toast de error</button>
1749
2216
  </div>
1750
- <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;div class="toast-stack" role="status" aria-live="polite"&gt;
2217
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;div class="toast-stack" role="status" aria-live="polite"&gt;
1751
2218
  &lt;div class="toast toast--success"&gt;
1752
2219
  &lt;svg&gt;…&lt;/svg&gt;
1753
2220
  &lt;div&gt;
@@ -1755,7 +2222,7 @@ html { scroll-behavior: smooth; }
1755
2222
  &lt;div class="toast__body"&gt;El proyecto se actualizó correctamente.&lt;/div&gt;
1756
2223
  &lt;/div&gt;
1757
2224
  &lt;/div&gt;
1758
- &lt;/div&gt;</code></pre></details>
2225
+ &lt;/div&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
1759
2226
  </section>
1760
2227
 
1761
2228
  <!-- ================= PROGRESS ================= -->
@@ -1765,14 +2232,14 @@ html { scroll-behavior: smooth; }
1765
2232
  <div class="ds-demo ds-demo--col" style="gap:24px;">
1766
2233
  <div>
1767
2234
  <div class="progress__head"><span class="progress__label">Subiendo archivos…</span><span class="progress__value" id="pgVal">64 %</span></div>
1768
- <div class="progress" role="progressbar" aria-valuenow="64" aria-valuemin="0" aria-valuemax="100"><div class="progress__bar" id="pgBar" style="width:64%;"></div></div>
2235
+ <div class="progress" role="progressbar" aria-label="Subiendo archivos" aria-valuenow="64" aria-valuemin="0" aria-valuemax="100"><div class="progress__bar" id="pgBar" style="width:64%;"></div></div>
1769
2236
  </div>
1770
2237
  <div>
1771
2238
  <div class="progress__head"><span class="progress__label">Indeterminado</span></div>
1772
- <div class="progress progress--indeterminate" role="progressbar"><div class="progress__bar"></div></div>
2239
+ <div class="progress progress--indeterminate" role="progressbar" aria-label="Indeterminado"><div class="progress__bar"></div></div>
1773
2240
  </div>
1774
2241
  <div style="display:flex;align-items:center;gap:24px;">
1775
- <div class="progress-ring" id="pgRing" data-value="64" role="progressbar" aria-valuenow="64" aria-valuemin="0" aria-valuemax="100">
2242
+ <div class="progress-ring" id="pgRing" data-value="64" role="progressbar" aria-label="Progreso circular" aria-valuenow="64" aria-valuemin="0" aria-valuemax="100">
1776
2243
  <svg width="56" height="56" viewBox="0 0 56 56" aria-hidden="true"><circle class="progress-ring__track" cx="28" cy="28" r="24" stroke-width="5"/><circle class="progress-ring__bar" id="pgRingBar" cx="28" cy="28" r="24" stroke-width="5"/></svg>
1777
2244
  <span class="progress-ring__value" id="pgRingVal">64%</span>
1778
2245
  </div>
@@ -1780,7 +2247,7 @@ html { scroll-behavior: smooth; }
1780
2247
  </div>
1781
2248
  <button class="btn btn--outline btn--sm" id="pgRandom" style="align-self:flex-start;">Cambiar progreso</button>
1782
2249
  </div>
1783
- <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;div class="progress__head"&gt;
2250
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;div class="progress__head"&gt;
1784
2251
  &lt;span class="progress__label"&gt;Subiendo…&lt;/span&gt;
1785
2252
  &lt;span class="progress__value"&gt;64 %&lt;/span&gt;
1786
2253
  &lt;/div&gt;
@@ -1796,30 +2263,36 @@ html { scroll-behavior: smooth; }
1796
2263
  &lt;/svg&gt;
1797
2264
  &lt;span class="progress-ring__value"&gt;64%&lt;/span&gt;
1798
2265
  &lt;/div&gt;
1799
- &lt;!-- JS: stroke-dasharray = 2πr; stroke-dashoffset = c * (1 - value/100) --&gt;</code></pre></details>
2266
+ &lt;!-- JS: stroke-dasharray = 2πr; stroke-dashoffset = c * (1 - value/100) --&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
1800
2267
  </section>
1801
2268
 
1802
2269
  <!-- ================= MODAL ================= -->
1803
2270
  <section id="comp-modal">
1804
2271
  <h2>Modal</h2>
1805
- <p>Overlay con blur 4px; se cierra con Esc o clic fuera (excepto flujos destructivos).</p>
2272
+ <p>Overlay con blur 4px; se cierra con Esc o clic fuera (excepto flujos destructivos con <span class="code">data-modal-persistent</span>). Al abrir, el foco entra al panel y queda atrapado (Tab/Shift+Tab ciclan dentro); al cerrar, el foco vuelve al botón que lo abrió.</p>
1806
2273
  <div class="ds-demo">
1807
2274
  <button class="btn btn--primary" id="openModal">Abrir modal</button>
1808
2275
  <button class="btn btn--outline" id="openModalLg">Abrir formulario</button>
1809
2276
  </div>
1810
- <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;div class="modal-overlay"&gt;
1811
- &lt;div class="modal" role="dialog" aria-modal="true" aria-labelledby="mTitle"&gt;
2277
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;div class="modal-overlay"&gt;
2278
+ &lt;!-- aria-modal, aria-labelledby (título) y aria-describedby (descripción, si la hay);
2279
+ tabindex="-1" permite mover el foco al panel al abrir --&gt;
2280
+ &lt;div class="modal" role="dialog" aria-modal="true"
2281
+ aria-labelledby="mTitle" aria-describedby="mDesc" tabindex="-1"&gt;
1812
2282
  &lt;div class="modal__header"&gt;
1813
2283
  &lt;h4 class="modal__title" id="mTitle"&gt;Título&lt;/h4&gt;
1814
- &lt;button class="icon-btn icon-btn--ghost" aria-label="Cerrar"&gt;×&lt;/button&gt;
2284
+ &lt;button class="icon-btn icon-btn--ghost" data-close-modal aria-label="Cerrar"&gt;×&lt;/button&gt;
1815
2285
  &lt;/div&gt;
1816
- &lt;p&gt;Contenido…&lt;/p&gt;
2286
+ &lt;p id="mDesc"&gt;Contenido…&lt;/p&gt;
1817
2287
  &lt;div class="modal__footer"&gt;
1818
- &lt;button class="btn btn--outline"&gt;Cancelar&lt;/button&gt;
1819
- &lt;button class="btn btn--primary"&gt;Confirmar&lt;/button&gt;
2288
+ &lt;button class="btn btn--outline" data-close-modal&gt;Cancelar&lt;/button&gt;
2289
+ &lt;button class="btn btn--primary" data-close-modal&gt;Confirmar&lt;/button&gt;
1820
2290
  &lt;/div&gt;
1821
2291
  &lt;/div&gt;
1822
- &lt;/div&gt;</code></pre></details>
2292
+ &lt;/div&gt;
2293
+ &lt;!-- JS: al abrir guarda document.activeElement y enfoca el panel; Tab cicla dentro
2294
+ (focus-trap); Esc / clic en overlay / data-close-modal cierran y devuelven el foco.
2295
+ Destructivo: añade data-modal-persistent al .modal-overlay. --&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
1823
2296
  </section>
1824
2297
 
1825
2298
  <!-- ================= MENU ================= -->
@@ -1836,32 +2309,82 @@ html { scroll-behavior: smooth; }
1836
2309
  </div>
1837
2310
  </div>
1838
2311
  </div>
1839
- <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;div class="menu"&gt;
2312
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;div class="menu"&gt;
1840
2313
  &lt;button class="menu__item"&gt;Duplicar&lt;/button&gt;
1841
2314
  &lt;button class="menu__item is-selected"&gt;Seleccionado&lt;/button&gt;
1842
- &lt;/div&gt;</code></pre></details>
2315
+ &lt;/div&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
1843
2316
  </section>
1844
2317
 
1845
2318
  <!-- ================= TABLA ================= -->
1846
2319
  <section id="comp-tabla">
1847
2320
  <h2>Tabla</h2>
1848
- <div class="ds-demo ds-demo--col" style="padding:0;overflow-x:auto;">
1849
- <table class="table">
1850
- <thead><tr><th>Proyecto</th><th>Estado</th><th class="num">Ingresos</th><th class="num">Cambio</th></tr></thead>
2321
+ <p>Zebra (<span class="code">.table--zebra</span>) + encabezado fijo (<span class="code">.table--sticky</span>, haz scroll dentro del recuadro) + columna ordenable (clic en «Proyecto» o «Ingresos») + columna de selección. La fila marcada usa <span class="code">.row--selected</span>; el checkbox maestro está en estado indeterminate.</p>
2322
+ <div class="ds-demo ds-demo--col" style="padding:0;max-height:264px;overflow:auto;">
2323
+ <table class="table table--zebra table--sticky" id="demoSortTable">
2324
+ <thead>
2325
+ <tr>
2326
+ <th scope="col" class="col-select"><input type="checkbox" class="checkbox" id="tblSelectAll" aria-label="Seleccionar todas las filas"></th>
2327
+ <th scope="col" aria-sort="ascending"><button class="th-sort" type="button">Proyecto <svg class="sort-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m18 15-6-6-6 6"/></svg></button></th>
2328
+ <th scope="col">Estado</th>
2329
+ <th scope="col" class="num" aria-sort="none"><button class="th-sort" type="button">Ingresos <svg class="sort-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m18 15-6-6-6 6"/></svg></button></th>
2330
+ <th scope="col" class="num">Cambio</th>
2331
+ </tr>
2332
+ </thead>
1851
2333
  <tbody>
1852
- <tr><td style="color:var(--text-primary);">Panel analítico</td><td><span class="badge badge--success"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>Activo</span></td><td class="num">$48 210</td><td class="num" style="color:var(--success);">+8,2 %</td></tr>
1853
- <tr class="is-selected"><td style="color:var(--text-primary);">API de pagos</td><td><span class="badge badge--info"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4m0-4h.01"/></svg>Beta</span></td><td class="num">$31 990</td><td class="num" style="color:var(--success);">+2,4 %</td></tr>
1854
- <tr><td style="color:var(--text-primary);">App móvil</td><td><span class="badge badge--warning"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.7 18-8-14a2 2 0 0 0-3.5 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3z"/><path d="M12 9v4m0 4h.01"/></svg>Pendiente</span></td><td class="num">$12 040</td><td class="num" style="color:var(--danger);">−1,1 %</td></tr>
2334
+ <tr>
2335
+ <td class="col-select"><input type="checkbox" class="checkbox" aria-label="Seleccionar API de pagos"></td>
2336
+ <td style="color:var(--text-primary);">API de pagos</td>
2337
+ <td><span class="badge badge--info"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4m0-4h.01"/></svg>Beta</span></td>
2338
+ <td class="num" data-sort="31990">$31 990</td>
2339
+ <td class="num" style="color:var(--success);">+2,4 %</td>
2340
+ </tr>
2341
+ <tr class="row--selected">
2342
+ <td class="col-select"><input type="checkbox" class="checkbox" checked aria-label="Seleccionar App móvil"></td>
2343
+ <td style="color:var(--text-primary);">App móvil</td>
2344
+ <td><span class="badge badge--warning"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.7 18-8-14a2 2 0 0 0-3.5 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3z"/><path d="M12 9v4m0 4h.01"/></svg>Pendiente</span></td>
2345
+ <td class="num" data-sort="12040">$12 040</td>
2346
+ <td class="num" style="color:var(--danger);">−1,1 %</td>
2347
+ </tr>
2348
+ <tr>
2349
+ <td class="col-select"><input type="checkbox" class="checkbox" aria-label="Seleccionar Panel analítico"></td>
2350
+ <td style="color:var(--text-primary);">Panel analítico</td>
2351
+ <td><span class="badge badge--success"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>Activo</span></td>
2352
+ <td class="num" data-sort="48210">$48 210</td>
2353
+ <td class="num" style="color:var(--success);">+8,2 %</td>
2354
+ </tr>
2355
+ <tr>
2356
+ <td class="col-select"><input type="checkbox" class="checkbox" aria-label="Seleccionar Portal de socios"></td>
2357
+ <td style="color:var(--text-primary);">Portal de socios</td>
2358
+ <td><span class="badge badge--success"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>Activo</span></td>
2359
+ <td class="num" data-sort="27500">$27 500</td>
2360
+ <td class="num" style="color:var(--success);">+5,0 %</td>
2361
+ </tr>
2362
+ <tr>
2363
+ <td class="col-select"><input type="checkbox" class="checkbox" aria-label="Seleccionar Reportes"></td>
2364
+ <td style="color:var(--text-primary);">Reportes</td>
2365
+ <td><span class="badge badge--info"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4m0-4h.01"/></svg>Beta</span></td>
2366
+ <td class="num" data-sort="9800">$9 800</td>
2367
+ <td class="num" style="color:var(--danger);">−0,4 %</td>
2368
+ </tr>
1855
2369
  </tbody>
1856
2370
  </table>
1857
2371
  </div>
1858
- <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;table class="table"&gt;
1859
- &lt;thead&gt;&lt;tr&gt;&lt;th&gt;Proyecto&lt;/th&gt;&lt;th class="num"&gt;Ingresos&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
2372
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;table class="table table--zebra table--sticky"&gt;
2373
+ &lt;thead&gt;&lt;tr&gt;
2374
+ &lt;th scope="col" class="col-select"&gt;
2375
+ &lt;input type="checkbox" class="checkbox" aria-label="Seleccionar todas"&gt;&lt;/th&gt;
2376
+ &lt;th scope="col" aria-sort="ascending"&gt;
2377
+ &lt;button class="th-sort" type="button"&gt;Proyecto
2378
+ &lt;svg class="sort-icon" …&gt;&lt;path d="m18 15-6-6-6 6"/&gt;&lt;/svg&gt;
2379
+ &lt;/button&gt;&lt;/th&gt;
2380
+ &lt;th scope="col" class="num"&gt;Ingresos&lt;/th&gt;
2381
+ &lt;/tr&gt;&lt;/thead&gt;
1860
2382
  &lt;tbody&gt;
1861
- &lt;tr&gt;&lt;td&gt;Panel analítico&lt;/td&gt;&lt;td class="num"&gt;$48 210&lt;/td&gt;&lt;/tr&gt;
1862
- &lt;tr class="is-selected"&gt;…&lt;/tr&gt;
2383
+ &lt;tr&gt;&lt;td class="col-select"&gt;&lt;input type="checkbox" class="checkbox" aria-label="Seleccionar API de pagos"&gt;&lt;/td&gt;…&lt;/tr&gt;
2384
+ &lt;tr class="row--selected"&gt;…&lt;/tr&gt;
1863
2385
  &lt;/tbody&gt;
1864
- &lt;/table&gt;</code></pre></details>
2386
+ &lt;/table&gt;
2387
+ &lt;!-- El icono se atenúa si aria-sort no está fijado; JS reordena y alterna aria-sort. --&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
1865
2388
  </section>
1866
2389
 
1867
2390
  <!-- ================= STAT ================= -->
@@ -1885,11 +2408,30 @@ html { scroll-behavior: smooth; }
1885
2408
  <span class="stat__delta stat__delta--neutral">— sin cambios</span>
1886
2409
  </div>
1887
2410
  </div>
1888
- <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;div class="stat"&gt;
2411
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;div class="stat"&gt;
1889
2412
  &lt;span class="stat__label"&gt;Ingresos del mes&lt;/span&gt;
1890
2413
  &lt;span class="stat__value"&gt;$128 450&lt;/span&gt;
1891
2414
  &lt;span class="stat__delta stat__delta--up"&gt;▲ +12,4 %&lt;/span&gt;
1892
- &lt;/div&gt;</code></pre></details>
2415
+ &lt;/div&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
2416
+ <h3 style="margin-top:32px;">Chip de métrica</h3>
2417
+ <p>Pastilla de una línea con rótulo y valor en <span class="code">--link</span> · fila con <span class="code">gap</span> que salta de línea en móvil · el chip interactivo es un <span class="code">&lt;button&gt;</span> de 44 px con nombre accesible completo.</p>
2418
+ <div class="ds-demo">
2419
+ <div class="metric-chips">
2420
+ <span class="metric-chip"><span class="metric-chip__label">Latencia</span> <span class="metric-chip__value">42 ms</span></span>
2421
+ <span class="metric-chip"><span class="metric-chip__label">Errores</span> <span class="metric-chip__value">0,3 %</span></span>
2422
+ <button class="metric-chip" type="button" id="dsWaterChip" aria-label="Agua: 3 de 8 vasos. Sumar un vaso"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg><span class="metric-chip__label">Agua</span> <span class="metric-chip__value">3 de 8</span></button>
2423
+ </div>
2424
+ </div>
2425
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;div class="metric-chips"&gt;
2426
+ &lt;span class="metric-chip"&gt;
2427
+ &lt;span class="metric-chip__label"&gt;Latencia&lt;/span&gt; &lt;span class="metric-chip__value"&gt;42 ms&lt;/span&gt;
2428
+ &lt;/span&gt;
2429
+ &lt;button class="metric-chip" type="button" aria-label="Agua: 3 de 8 vasos. Sumar un vaso"&gt;
2430
+ &lt;svg aria-hidden="true"&gt;…plus…&lt;/svg&gt;
2431
+ &lt;span class="metric-chip__label"&gt;Agua&lt;/span&gt; &lt;span class="metric-chip__value"&gt;3 de 8&lt;/span&gt;
2432
+ &lt;/button&gt;
2433
+ &lt;/div&gt;
2434
+ &lt;!-- El aria-label se actualiza con el valor; empieza por el texto visible --&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
1893
2435
  </section>
1894
2436
 
1895
2437
  <!-- ================= PAGINATION ================= -->
@@ -1899,14 +2441,14 @@ html { scroll-behavior: smooth; }
1899
2441
  <div class="ds-demo">
1900
2442
  <ul class="pagination" id="paginationDemo" aria-label="Paginación"></ul>
1901
2443
  </div>
1902
- <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;ul class="pagination" aria-label="Paginación"&gt;
2444
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;ul class="pagination" aria-label="Paginación"&gt;
1903
2445
  &lt;li&gt;&lt;button class="pagination__btn" data-page="prev" aria-label="Página anterior"&gt;‹&lt;/button&gt;&lt;/li&gt;
1904
2446
  &lt;li&gt;&lt;button class="pagination__btn" data-page="1"&gt;1&lt;/button&gt;&lt;/li&gt;
1905
2447
  &lt;li&gt;&lt;span class="pagination__ellipsis"&gt;…&lt;/span&gt;&lt;/li&gt;
1906
2448
  &lt;li&gt;&lt;button class="pagination__btn is-active" data-page="5" aria-current="page"&gt;5&lt;/button&gt;&lt;/li&gt;
1907
2449
  &lt;li&gt;&lt;button class="pagination__btn" data-page="6"&gt;6&lt;/button&gt;&lt;/li&gt;
1908
2450
  &lt;li&gt;&lt;button class="pagination__btn" data-page="next" aria-label="Página siguiente"&gt;›&lt;/button&gt;&lt;/li&gt;
1909
- &lt;/ul&gt;</code></pre></details>
2451
+ &lt;/ul&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
1910
2452
  </section>
1911
2453
 
1912
2454
  <!-- ================= NAVEGACIÓN ================= -->
@@ -1931,6 +2473,69 @@ html { scroll-behavior: smooth; }
1931
2473
  <a class="sidebar__item" href="#comp-navegacion"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>Proyectos</a>
1932
2474
  </div>
1933
2475
  </div>
2476
+ <h3 style="margin-top:32px;">Sidebar contraíble</h3>
2477
+ <p>Expandido 260 px · riel de iconos de 64 px con grupos separados por una línea · en el riel, el nombre aparece como tooltip al pasar el cursor o al enfocar con teclado · el ancho cambia sin transición y solo los rótulos se funden · por debajo de 768 px lo sustituye un Drawer <span class="code">--left</span>.</p>
2478
+ <div class="ds-demo ds-app-demo">
2479
+ <nav class="sidebar sidebar--collapsible" id="demoAppNav" aria-label="Navegación de ejemplo">
2480
+ <div class="sidebar__group" role="group" aria-labelledby="demoAppNavG1">
2481
+ <span class="sidebar__group-label" id="demoAppNavG1">Monitoreo</span>
2482
+ <a class="sidebar__item is-active" href="#comp-navegacion" aria-current="page"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/></svg><span class="sidebar__label">Inicio</span></a>
2483
+ <a class="sidebar__item" href="#comp-navegacion"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3v18h18"/><path d="M7 15v3m5-8v8m5-13v13"/></svg><span class="sidebar__label">Analítica</span></a>
2484
+ <a class="sidebar__item" href="#comp-navegacion"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg><span class="sidebar__label">Actividad</span></a>
2485
+ </div>
2486
+ <div class="sidebar__group" role="group" aria-labelledby="demoAppNavG2">
2487
+ <span class="sidebar__group-label" id="demoAppNavG2">Gestión</span>
2488
+ <a class="sidebar__item" href="#comp-navegacion"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg><span class="sidebar__label">Proyectos</span></a>
2489
+ <a class="sidebar__item" href="#comp-navegacion"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/></svg><span class="sidebar__label">Equipo</span></a>
2490
+ <a class="sidebar__item" href="#comp-navegacion"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg><span class="sidebar__label">Alertas</span></a>
2491
+ </div>
2492
+ <button class="sidebar__item sidebar__toggle" type="button" aria-expanded="true" aria-controls="demoAppNav"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg><span class="sidebar__label">Contraer</span></button>
2493
+ </nav>
2494
+ <div class="ds-app-demo__main">
2495
+ <button class="icon-btn icon-btn--outline sidebar-trigger" type="button" id="openNavDrawer" aria-label="Abrir navegación" aria-expanded="false" aria-controls="demoNavDrawer"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"/></svg></button>
2496
+ <h4 style="margin:0;">Inicio</h4>
2497
+ <p class="text-muted" style="margin:0;font-size:var(--text-sm);">El contenido ocupa el ancho que deja el sidebar: la rejilla <span class="code">auto 1fr</span> sigue al riel al instante, sin animar el ancho.</p>
2498
+ </div>
2499
+ </div>
2500
+ <div class="drawer drawer--left" id="demoNavDrawer" hidden>
2501
+ <div class="drawer__overlay" data-close-drawer></div>
2502
+ <aside class="drawer__panel" role="dialog" aria-modal="true" aria-labelledby="navDrawerTitle" tabindex="-1">
2503
+ <div class="drawer__header">
2504
+ <h3 class="drawer__title" id="navDrawerTitle">Navegación</h3>
2505
+ <button class="icon-btn icon-btn--ghost" data-close-drawer aria-label="Cerrar navegación"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
2506
+ </div>
2507
+ <nav class="drawer__body" aria-label="Navegación de ejemplo">
2508
+ <div class="sidebar__group" role="group" aria-labelledby="demoNavDrawerG1">
2509
+ <span class="sidebar__group-label" id="demoNavDrawerG1">Monitoreo</span>
2510
+ <a class="sidebar__item is-active" href="#comp-navegacion" aria-current="page" data-close-drawer><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/></svg><span class="sidebar__label">Inicio</span></a>
2511
+ <a class="sidebar__item" href="#comp-navegacion" data-close-drawer><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3v18h18"/><path d="M7 15v3m5-8v8m5-13v13"/></svg><span class="sidebar__label">Analítica</span></a>
2512
+ <a class="sidebar__item" href="#comp-navegacion" data-close-drawer><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg><span class="sidebar__label">Actividad</span></a>
2513
+ </div>
2514
+ <div class="sidebar__group" role="group" aria-labelledby="demoNavDrawerG2">
2515
+ <span class="sidebar__group-label" id="demoNavDrawerG2">Gestión</span>
2516
+ <a class="sidebar__item" href="#comp-navegacion" data-close-drawer><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg><span class="sidebar__label">Proyectos</span></a>
2517
+ <a class="sidebar__item" href="#comp-navegacion" data-close-drawer><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/></svg><span class="sidebar__label">Equipo</span></a>
2518
+ <a class="sidebar__item" href="#comp-navegacion" data-close-drawer><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg><span class="sidebar__label">Alertas</span></a>
2519
+ </div>
2520
+ </nav>
2521
+ </aside>
2522
+ </div>
2523
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;nav class="sidebar sidebar--collapsible" id="appNav" aria-label="Principal"&gt;
2524
+ &lt;div class="sidebar__group" role="group" aria-labelledby="navMonitor"&gt;
2525
+ &lt;span class="sidebar__group-label" id="navMonitor"&gt;Monitoreo&lt;/span&gt;
2526
+ &lt;a class="sidebar__item is-active" href="/" aria-current="page"&gt;
2527
+ &lt;svg aria-hidden="true"&gt;…&lt;/svg&gt;&lt;span class="sidebar__label"&gt;Inicio&lt;/span&gt;
2528
+ &lt;/a&gt;
2529
+ &lt;/div&gt;
2530
+ &lt;button class="sidebar__item sidebar__toggle" type="button"
2531
+ aria-expanded="true" aria-controls="appNav"&gt;
2532
+ &lt;svg aria-hidden="true"&gt;…chevron-left…&lt;/svg&gt;&lt;span class="sidebar__label"&gt;Contraer&lt;/span&gt;
2533
+ &lt;/button&gt;
2534
+ &lt;/nav&gt;
2535
+ &lt;!-- Contraído: .is-collapsed en el nav, aria-expanded="false" y texto «Expandir».
2536
+ Por debajo de 768 px: botón ☰ .sidebar-trigger que abre un .drawer.drawer--left con los mismos ítems. --&gt;
2537
+ &lt;button class="icon-btn sidebar-trigger" type="button" aria-label="Abrir navegación"
2538
+ aria-expanded="false" aria-controls="navDrawer"&gt;…menu…&lt;/button&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
1934
2539
  <h3 style="margin-top:32px;">Tabs y Breadcrumbs</h3>
1935
2540
  <div class="ds-demo ds-demo--col">
1936
2541
  <div class="tabs" id="demoTabs">
@@ -1945,10 +2550,10 @@ html { scroll-behavior: smooth; }
1945
2550
  <li aria-current="page">Panel analítico</li>
1946
2551
  </ul>
1947
2552
  </div>
1948
- <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;div class="navbar"&gt;…&lt;a class="navbar__link is-active"&gt;Resumen&lt;/a&gt;…&lt;/div&gt;
2553
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;div class="navbar"&gt;…&lt;a class="navbar__link is-active"&gt;Resumen&lt;/a&gt;…&lt;/div&gt;
1949
2554
  &lt;div class="sidebar"&gt;&lt;a class="sidebar__item is-active"&gt;&lt;svg&gt;…&lt;/svg&gt;Inicio&lt;/a&gt;&lt;/div&gt;
1950
2555
  &lt;div class="tabs"&gt;&lt;button class="tabs__btn is-active"&gt;Resumen&lt;/button&gt;…&lt;span class="tabs__indicator"&gt;&lt;/span&gt;&lt;/div&gt;
1951
- &lt;ul class="breadcrumbs"&gt;&lt;li&gt;&lt;a&gt;Inicio&lt;/a&gt;&lt;/li&gt;&lt;li aria-current="page"&gt;Actual&lt;/li&gt;&lt;/ul&gt;</code></pre></details>
2556
+ &lt;ul class="breadcrumbs"&gt;&lt;li&gt;&lt;a&gt;Inicio&lt;/a&gt;&lt;/li&gt;&lt;li aria-current="page"&gt;Actual&lt;/li&gt;&lt;/ul&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
1952
2557
  </section>
1953
2558
 
1954
2559
  <!-- ================= SKELETON ================= -->
@@ -1969,7 +2574,7 @@ html { scroll-behavior: smooth; }
1969
2574
  <button class="btn btn--primary btn--sm">Crear proyecto</button>
1970
2575
  </div>
1971
2576
  </div>
1972
- <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;span class="skeleton" style="height:12px;width:70%"&gt;&lt;/span&gt;
2577
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;span class="skeleton" style="height:12px;width:70%"&gt;&lt;/span&gt;
1973
2578
  &lt;span class="skeleton skeleton--circle" style="width:40px;height:40px"&gt;&lt;/span&gt;
1974
2579
 
1975
2580
  &lt;div class="empty-state"&gt;
@@ -1977,7 +2582,7 @@ html { scroll-behavior: smooth; }
1977
2582
  &lt;span class="empty-state__title"&gt;Aún no hay proyectos&lt;/span&gt;
1978
2583
  &lt;span class="empty-state__desc"&gt;Crea tu primer proyecto…&lt;/span&gt;
1979
2584
  &lt;button class="btn btn--primary btn--sm"&gt;Crear proyecto&lt;/button&gt;
1980
- &lt;/div&gt;</code></pre></details>
2585
+ &lt;/div&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
1981
2586
  </section>
1982
2587
 
1983
2588
  <!-- ================= CARD, LINK Y CODE ================= -->
@@ -1994,17 +2599,380 @@ html { scroll-behavior: smooth; }
1994
2599
  <p style="margin:0;font-size:var(--text-sm);color:var(--text-muted);">Al pasar el cursor sube de nivel: −2px + superficie más clara.</p>
1995
2600
  </div>
1996
2601
  </div>
1997
- <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;div class="card"&gt;…&lt;/div&gt;
2602
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;div class="card"&gt;…&lt;/div&gt;
1998
2603
  &lt;div class="card card--interactive"&gt;…&lt;/div&gt;
1999
- &lt;!-- .card--glow: firma de marca — MÁXIMO UNO POR PANTALLA --&gt;</code></pre></details>
2604
+ &lt;!-- .card--glow: firma de marca — MÁXIMO UNO POR PANTALLA --&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
2000
2605
  <h3 style="margin-top:32px;">Link y Code</h3>
2001
2606
  <div class="ds-demo" style="gap:32px;">
2002
2607
  <a class="link" href="#comp-card">Link con subrayado animado</a>
2003
2608
  <a class="link" href="https://lucide.dev" target="_blank" rel="noopener noreferrer">Link externo<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 17 17 7M7 7h10v10"/></svg></a>
2004
2609
  <span>Acento con <span class="code">var(--accent-400)</span></span>
2005
2610
  </div>
2006
- <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;a class="link" href="…"&gt;Link con subrayado animado&lt;/a&gt;
2007
- &lt;span class="code"&gt;var(--accent-400)&lt;/span&gt;</code></pre></details>
2611
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;a class="link" href="…"&gt;Link con subrayado animado&lt;/a&gt;
2612
+ &lt;span class="code"&gt;var(--accent-400)&lt;/span&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
2613
+ </section>
2614
+
2615
+ <!-- ================= COMBOBOX / SEARCH ================= -->
2616
+ <section id="comp-combobox">
2617
+ <h2>Combobox y Search</h2>
2618
+ <p>Combobox con autocompletado: filtra en vivo y se navega con ↑/↓, Enter selecciona, Esc cierra. El listado vive sobre <span class="code">--surface-2</span>. Al lado, un campo de búsqueda con botón para limpiar.</p>
2619
+ <div class="ds-demo ds-demo--grid" style="align-items:start;overflow:visible;">
2620
+ <div>
2621
+ <span class="field__label" style="display:block;margin-bottom:6px;">Framework</span>
2622
+ <div class="combobox">
2623
+ <input class="input combobox__input" type="text" id="cbInput"
2624
+ role="combobox" aria-expanded="false" aria-controls="cbList"
2625
+ aria-autocomplete="list" aria-activedescendant=""
2626
+ placeholder="Escribe para filtrar…" autocomplete="off" spellcheck="false">
2627
+ <ul class="combobox__list" id="cbList" role="listbox" aria-label="Frameworks" hidden>
2628
+ <li class="combobox__option" role="option" id="cbo-0" aria-selected="false">Angular<svg class="combobox__check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg></li>
2629
+ <li class="combobox__option" role="option" id="cbo-1" aria-selected="false">Astro<svg class="combobox__check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg></li>
2630
+ <li class="combobox__option" role="option" id="cbo-2" aria-selected="false">React<svg class="combobox__check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg></li>
2631
+ <li class="combobox__option" role="option" id="cbo-3" aria-selected="false">Solid<svg class="combobox__check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg></li>
2632
+ <li class="combobox__option" role="option" id="cbo-4" aria-selected="false">Svelte<svg class="combobox__check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg></li>
2633
+ <li class="combobox__option" role="option" id="cbo-5" aria-selected="false">Vue<svg class="combobox__check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg></li>
2634
+ </ul>
2635
+ </div>
2636
+ </div>
2637
+ <div>
2638
+ <span class="field__label" style="display:block;margin-bottom:6px;">Buscar</span>
2639
+ <div class="search">
2640
+ <svg class="search__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
2641
+ <input class="input search__input" type="search" id="searchInput" placeholder="Buscar proyectos…" aria-label="Buscar proyectos" autocomplete="off">
2642
+ <button class="search__clear" type="button" id="searchClear" aria-label="Limpiar" hidden><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
2643
+ </div>
2644
+ </div>
2645
+ </div>
2646
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;div class="combobox"&gt;
2647
+ &lt;input class="input combobox__input" role="combobox" aria-expanded="false"
2648
+ aria-controls="lista" aria-autocomplete="list" aria-activedescendant=""&gt;
2649
+ &lt;ul class="combobox__list" id="lista" role="listbox" aria-label="Frameworks" hidden&gt;
2650
+ &lt;li class="combobox__option" role="option" id="op-0" aria-selected="false"&gt;Angular&lt;/li&gt;
2651
+ &lt;!-- .is-active = resaltado por teclado · aria-selected="true" = elegida --&gt;
2652
+ &lt;/ul&gt;
2653
+ &lt;/div&gt;
2654
+
2655
+ &lt;div class="search"&gt;
2656
+ &lt;svg class="search__icon" aria-hidden="true"&gt;…lupa…&lt;/svg&gt;
2657
+ &lt;input class="input search__input" type="search" aria-label="Buscar"&gt;
2658
+ &lt;button class="search__clear" aria-label="Limpiar" hidden&gt;×&lt;/button&gt;
2659
+ &lt;/div&gt;
2660
+ &lt;!-- JS: filtra en vivo, ↑/↓/Enter/Esc; el clear vacía y devuelve el foco. --&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
2661
+ </section>
2662
+
2663
+ <!-- ================= DRAWER ================= -->
2664
+ <section id="comp-drawer">
2665
+ <h2>Drawer</h2>
2666
+ <p>Panel que entra deslizándose desde un borde (derecha por defecto; <span class="code">--left</span> / <span class="code">--bottom</span>). Igual que el modal: <span class="code">role="dialog"</span> + <span class="code">aria-modal</span>, foco atrapado, retorno de foco, y cierre con Esc o clic en el overlay. El panel usa <span class="code">overscroll-behavior: contain</span>.</p>
2667
+ <div class="ds-demo">
2668
+ <button class="btn btn--primary" id="openDrawer">Abrir panel</button>
2669
+ </div>
2670
+ <div class="drawer drawer--right" id="demoDrawer" hidden>
2671
+ <div class="drawer__overlay" data-close-drawer></div>
2672
+ <aside class="drawer__panel" role="dialog" aria-modal="true" aria-labelledby="drawerTitle" tabindex="-1">
2673
+ <div class="drawer__header">
2674
+ <h3 class="drawer__title" id="drawerTitle">Filtros</h3>
2675
+ <button class="icon-btn icon-btn--ghost" data-close-drawer aria-label="Cerrar panel"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
2676
+ </div>
2677
+ <div class="drawer__body">
2678
+ <div class="field">
2679
+ <label class="field__label" for="drawerRange">Rango de fechas</label>
2680
+ <div class="select-wrap">
2681
+ <select class="select" id="drawerRange">
2682
+ <option>Últimos 7 días</option>
2683
+ <option>Últimos 30 días</option>
2684
+ <option>Este trimestre</option>
2685
+ </select>
2686
+ </div>
2687
+ </div>
2688
+ <div style="display:flex;flex-direction:column;gap:var(--space-3);">
2689
+ <label class="check-row"><input type="checkbox" class="checkbox" checked> Solo proyectos activos</label>
2690
+ <label class="check-row"><input type="checkbox" class="checkbox"> Incluir archivados</label>
2691
+ </div>
2692
+ </div>
2693
+ <div class="drawer__footer">
2694
+ <button class="btn btn--outline" data-close-drawer>Cancelar</button>
2695
+ <button class="btn btn--primary" data-close-drawer>Aplicar filtros</button>
2696
+ </div>
2697
+ </aside>
2698
+ </div>
2699
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;div class="drawer drawer--right" hidden&gt;
2700
+ &lt;div class="drawer__overlay" data-close-drawer&gt;&lt;/div&gt;
2701
+ &lt;aside class="drawer__panel" role="dialog" aria-modal="true"
2702
+ aria-labelledby="dTitle" tabindex="-1"&gt;
2703
+ &lt;div class="drawer__header"&gt;
2704
+ &lt;h3 class="drawer__title" id="dTitle"&gt;Filtros&lt;/h3&gt;
2705
+ &lt;button class="icon-btn icon-btn--ghost" data-close-drawer aria-label="Cerrar"&gt;×&lt;/button&gt;
2706
+ &lt;/div&gt;
2707
+ &lt;div class="drawer__body"&gt;…&lt;/div&gt;
2708
+ &lt;div class="drawer__footer"&gt;…&lt;/div&gt;
2709
+ &lt;/aside&gt;
2710
+ &lt;/div&gt;
2711
+ &lt;!-- Variantes: --right (default) · --left · --bottom (sheet).
2712
+ JS: .is-open dispara el slide; Esc / overlay cierran y devuelven el foco. --&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
2713
+ </section>
2714
+
2715
+ <!-- ================= POPOVER ================= -->
2716
+ <section id="comp-popover">
2717
+ <h2>Popover</h2>
2718
+ <p>Se abre por <strong>clic</strong> —eso lo distingue del Tooltip, que es hover— y crece desde el <span class="code">transform-origin</span> del disparador. <span class="code">role="dialog"</span>, el foco entra al panel; cierra con Esc o clic fuera. Panel sobre <span class="code">--surface-2</span>.</p>
2719
+ <div class="ds-demo" style="overflow:visible;">
2720
+ <div class="popover">
2721
+ <button class="btn btn--outline popover__trigger" type="button" id="popTrigger" aria-haspopup="dialog" aria-expanded="false" aria-controls="popPanel">
2722
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><path d="m16 6-4-4-4 4"/><path d="M12 2v13"/></svg>
2723
+ Compartir
2724
+ </button>
2725
+ <div class="popover__panel" role="dialog" aria-label="Compartir proyecto" id="popPanel">
2726
+ <h3 style="margin:0 0 var(--space-2);font-family:var(--font-body);font-size:var(--text-base);font-weight:600;letter-spacing:normal;color:var(--text-heading);">Compartir proyecto</h3>
2727
+ <p style="font-size:var(--text-sm);color:var(--text-muted);margin:0 0 var(--space-3);max-width:none;">Cualquiera con el enlace podrá ver este proyecto en modo lectura.</p>
2728
+ <div style="display:flex;gap:var(--space-2);align-items:center;">
2729
+ <input class="input" type="text" readonly value="higinio.dev/p/8f2a" aria-label="Enlace para compartir">
2730
+ <button class="btn btn--primary btn--sm" type="button">Copiar</button>
2731
+ </div>
2732
+ <a class="link" href="#comp-popover" style="margin-top:var(--space-3);">Ver permisos avanzados</a>
2733
+ </div>
2734
+ </div>
2735
+ </div>
2736
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;div class="popover"&gt;
2737
+ &lt;button class="btn btn--outline popover__trigger" type="button"
2738
+ aria-haspopup="dialog" aria-expanded="false" aria-controls="panel"&gt;Compartir&lt;/button&gt;
2739
+ &lt;div class="popover__panel" role="dialog" aria-label="Compartir proyecto" id="panel"&gt;
2740
+ &lt;p&gt;Contenido de varias líneas…&lt;/p&gt;
2741
+ &lt;button class="btn btn--primary btn--sm"&gt;Copiar&lt;/button&gt;
2742
+ &lt;a class="link" href="…"&gt;Ver permisos avanzados&lt;/a&gt;
2743
+ &lt;/div&gt;
2744
+ &lt;/div&gt;
2745
+ &lt;!-- Abre por CLICK (no hover); el foco entra al panel; Esc / clic fuera cierran. --&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
2746
+ </section>
2747
+
2748
+ <!-- ================= BUTTON GROUP ================= -->
2749
+ <section id="comp-buttongroup">
2750
+ <h2>ButtonGroup</h2>
2751
+ <p>Botones adyacentes con bordes compartidos: el radio queda solo en los extremos y los bordes internos se colapsan. Reutiliza los <span class="code">.btn</span> existentes. <span class="code">role="group"</span> + <span class="code">aria-label</span>.</p>
2752
+ <div class="ds-demo">
2753
+ <div class="btn-group" role="group" aria-label="Alineación del texto">
2754
+ <button class="btn btn--outline" type="button">Izquierda</button>
2755
+ <button class="btn btn--outline" type="button">Centro</button>
2756
+ <button class="btn btn--outline" type="button">Derecha</button>
2757
+ </div>
2758
+ </div>
2759
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;div class="btn-group" role="group" aria-label="Alineación del texto"&gt;
2760
+ &lt;button class="btn btn--outline"&gt;Izquierda&lt;/button&gt;
2761
+ &lt;button class="btn btn--outline"&gt;Centro&lt;/button&gt;
2762
+ &lt;button class="btn btn--outline"&gt;Derecha&lt;/button&gt;
2763
+ &lt;/div&gt;
2764
+ &lt;!-- Radios solo en los extremos; bordes internos colapsados (margin-left:-1px). --&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
2765
+ </section>
2766
+
2767
+ <!-- ================= DATEPICKER ================= -->
2768
+ <section id="comp-datepicker">
2769
+ <h2>DatePicker</h2>
2770
+ <p>Campo de fecha con calendario en popover: el input abre el calendario al hacer clic, eliges un día con el ratón o las flechas y el valor vuelve al campo. El panel reutiliza el look de <span class="code">.popover__panel</span> sobre <span class="code">--surface-2</span>. El <span class="code">.calendar__grid</span> es <span class="code">role="grid"</span> y cada día un <span class="code">&lt;button role="gridcell"&gt;</span> con la fecha completa en <span class="code">aria-label</span>.</p>
2771
+ <div class="ds-demo" style="overflow:visible;">
2772
+ <div class="datepicker" id="datepicker">
2773
+ <span class="field__label" id="dpLabel" style="display:block;margin-bottom:6px;">Fecha de entrega</span>
2774
+ <div class="input-wrap" style="max-width:220px;">
2775
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>
2776
+ <input class="input datepicker__input" type="text" id="dpInput" readonly
2777
+ aria-haspopup="dialog" aria-controls="dpCalendar"
2778
+ aria-labelledby="dpLabel" value="15 de julio de 2026">
2779
+ </div>
2780
+ <div class="datepicker__calendar" id="dpCalendar" role="dialog" aria-label="Elegir fecha" hidden>
2781
+ <div class="calendar">
2782
+ <div class="calendar__header">
2783
+ <button class="icon-btn icon-btn--ghost icon-btn--sm" type="button" id="dpPrev" aria-label="Mes anterior"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg></button>
2784
+ <span class="calendar__title" id="dpTitle" aria-live="polite">julio 2026</span>
2785
+ <button class="icon-btn icon-btn--ghost icon-btn--sm" type="button" id="dpNext" aria-label="Mes siguiente"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg></button>
2786
+ </div>
2787
+ <div class="calendar__table" role="grid" aria-labelledby="dpTitle">
2788
+ <div class="calendar__weekdays" role="row">
2789
+ <span role="columnheader" aria-label="lunes">L</span>
2790
+ <span role="columnheader" aria-label="martes">M</span>
2791
+ <span role="columnheader" aria-label="miércoles">X</span>
2792
+ <span role="columnheader" aria-label="jueves">J</span>
2793
+ <span role="columnheader" aria-label="viernes">V</span>
2794
+ <span role="columnheader" aria-label="sábado">S</span>
2795
+ <span role="columnheader" aria-label="domingo">D</span>
2796
+ </div>
2797
+ <div class="calendar__grid" id="dpGrid" role="rowgroup"></div>
2798
+ </div>
2799
+ </div>
2800
+ </div>
2801
+ </div>
2802
+ </div>
2803
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;div class="datepicker"&gt;
2804
+ &lt;div class="input-wrap"&gt;
2805
+ &lt;svg aria-hidden="true"&gt;…calendario…&lt;/svg&gt;
2806
+ &lt;input class="input datepicker__input" readonly aria-haspopup="dialog"
2807
+ aria-controls="cal" value="15 de julio de 2026"&gt;
2808
+ &lt;/div&gt;
2809
+ &lt;div class="datepicker__calendar" id="cal" role="dialog" aria-label="Elegir fecha" hidden&gt;
2810
+ &lt;div class="calendar"&gt;
2811
+ &lt;div class="calendar__header"&gt;
2812
+ &lt;button class="icon-btn icon-btn--ghost icon-btn--sm" aria-label="Mes anterior"&gt;‹&lt;/button&gt;
2813
+ &lt;span class="calendar__title" aria-live="polite"&gt;julio 2026&lt;/span&gt;
2814
+ &lt;button class="icon-btn icon-btn--ghost icon-btn--sm" aria-label="Mes siguiente"&gt;›&lt;/button&gt;
2815
+ &lt;/div&gt;
2816
+ &lt;div class="calendar__table" role="grid" aria-label="julio 2026"&gt;
2817
+ &lt;div class="calendar__weekdays" role="row"&gt;
2818
+ &lt;span role="columnheader" aria-label="lunes"&gt;L&lt;/span&gt; …
2819
+ &lt;/div&gt;
2820
+ &lt;div class="calendar__grid" role="rowgroup"&gt;
2821
+ &lt;div class="calendar__week" role="row"&gt;
2822
+ &lt;button class="calendar__day" role="gridcell"
2823
+ aria-label="15 de julio de 2026" aria-selected="true"&gt;15&lt;/button&gt; …
2824
+ &lt;/div&gt;
2825
+ &lt;/div&gt;
2826
+ &lt;/div&gt;
2827
+ &lt;/div&gt;
2828
+ &lt;/div&gt;
2829
+ &lt;/div&gt;
2830
+ &lt;!-- JS: clic abre; flechas mueven el día (±1, ±7) cruzando meses; Enter elige; Esc cierra. --&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
2831
+ </section>
2832
+
2833
+ <!-- ================= FILE UPLOAD ================= -->
2834
+ <section id="comp-fileupload">
2835
+ <h2>FileUpload</h2>
2836
+ <p>Zona de arrastre con lista de archivos. El input file real va oculto accesiblemente (<span class="code">.visually-hidden</span>, sigue tabulable) y el ring de foco se pinta sobre el label. Al arrastrar un archivo encima, <span class="code">.is-dragover</span> tiñe el borde de acento. Un ítem subiendo incrusta el <span class="code">.progress</span>; el error se marca con icono + texto, no solo por color.</p>
2837
+ <div class="ds-demo">
2838
+ <div class="file-upload" id="fileUpload" style="width:100%;max-width:520px;">
2839
+ <input class="file-upload__input visually-hidden" type="file" id="fuInput" multiple aria-describedby="fuHint">
2840
+ <label class="file-upload__label" for="fuInput">
2841
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M17 8l-5-5-5 5"/><path d="M12 3v13"/></svg>
2842
+ <span>Arrastra archivos aquí o <span class="file-upload__button">explora</span></span>
2843
+ <span class="file-upload__hint" id="fuHint">PNG, JPG o PDF · máximo 10 MB</span>
2844
+ </label>
2845
+ <ul class="file-upload__list" id="fuList">
2846
+ <li class="file-upload__item">
2847
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg>
2848
+ <div>
2849
+ <span class="file-upload__name">informe-trimestral-q3.pdf</span>
2850
+ <span class="file-upload__size">2.4 MB · subiendo…</span>
2851
+ <div class="progress" role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100" aria-label="Subiendo informe-trimestral-q3.pdf"><div class="progress__bar" style="width:60%;"></div></div>
2852
+ </div>
2853
+ <button class="file-upload__remove" type="button" aria-label="Cancelar informe-trimestral-q3.pdf"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
2854
+ </li>
2855
+ <li class="file-upload__item">
2856
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg>
2857
+ <div>
2858
+ <span class="file-upload__name">mockup-portada.png</span>
2859
+ <span class="file-upload__size">840 KB · completo</span>
2860
+ </div>
2861
+ <button class="file-upload__remove" type="button" aria-label="Quitar mockup-portada.png"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
2862
+ </li>
2863
+ <li class="file-upload__item file-upload__item--error">
2864
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M12 8v4m0 4h.01"/></svg>
2865
+ <div>
2866
+ <span class="file-upload__name">video-demo.mov</span>
2867
+ <span class="file-upload__size">Error: supera los 10 MB</span>
2868
+ </div>
2869
+ <button class="file-upload__remove" type="button" aria-label="Quitar video-demo.mov"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
2870
+ </li>
2871
+ </ul>
2872
+ </div>
2873
+ </div>
2874
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;div class="file-upload"&gt;
2875
+ &lt;input class="file-upload__input visually-hidden" type="file" id="f" multiple
2876
+ aria-describedby="hint"&gt;
2877
+ &lt;label class="file-upload__label" for="f"&gt;
2878
+ &lt;svg aria-hidden="true"&gt;…subir…&lt;/svg&gt;
2879
+ &lt;span&gt;Arrastra archivos aquí o &lt;span class="file-upload__button"&gt;explora&lt;/span&gt;&lt;/span&gt;
2880
+ &lt;span class="file-upload__hint" id="hint"&gt;PNG, JPG o PDF · máximo 10 MB&lt;/span&gt;
2881
+ &lt;/label&gt;
2882
+ &lt;ul class="file-upload__list"&gt;
2883
+ &lt;li class="file-upload__item"&gt;
2884
+ &lt;svg aria-hidden="true"&gt;…archivo…&lt;/svg&gt;
2885
+ &lt;div&gt;
2886
+ &lt;span class="file-upload__name"&gt;informe.pdf&lt;/span&gt;
2887
+ &lt;span class="file-upload__size"&gt;2.4 MB · subiendo…&lt;/span&gt;
2888
+ &lt;div class="progress" role="progressbar" aria-valuenow="60" …&gt;
2889
+ &lt;div class="progress__bar" style="width:60%"&gt;&lt;/div&gt;&lt;/div&gt;
2890
+ &lt;/div&gt;
2891
+ &lt;button class="file-upload__remove" aria-label="Cancelar informe.pdf"&gt;×&lt;/button&gt;
2892
+ &lt;/li&gt;
2893
+ &lt;li class="file-upload__item file-upload__item--error"&gt;…icono + texto de error…&lt;/li&gt;
2894
+ &lt;/ul&gt;
2895
+ &lt;/div&gt;
2896
+ &lt;!-- JS: dragover añade .is-dragover; el botón × quita el ítem. --&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
2897
+ </section>
2898
+
2899
+ <!-- ================= STEPPER ================= -->
2900
+ <section id="comp-stepper">
2901
+ <h2>Stepper / Wizard</h2>
2902
+ <p>Indicador de progreso por pasos. Es un <span class="code">&lt;ol&gt;</span>, así que el orden y el conteo se anuncian solos; el paso en curso lleva <span class="code">aria-current="step"</span> y los pasos completos muestran un check (estado no solo por color). El conector se tiñe de acento en el tramo ya recorrido. Variante <span class="code">.stepper--vertical</span> para apilarlos.</p>
2903
+ <div class="ds-demo ds-demo--grid" style="align-items:start;">
2904
+ <ol class="stepper" aria-label="Progreso de la configuración" style="width:100%;">
2905
+ <li class="step step--complete">
2906
+ <span class="step__marker"><span class="visually-hidden">Completado: </span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg></span>
2907
+ <span class="step__label">Cuenta</span>
2908
+ <span class="step__connector" aria-hidden="true"></span>
2909
+ </li>
2910
+ <li class="step step--complete">
2911
+ <span class="step__marker"><span class="visually-hidden">Completado: </span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg></span>
2912
+ <span class="step__label">Perfil</span>
2913
+ <span class="step__connector" aria-hidden="true"></span>
2914
+ </li>
2915
+ <li class="step step--current" aria-current="step">
2916
+ <span class="step__marker"><span class="visually-hidden">Paso actual: </span>3</span>
2917
+ <span class="step__label">Pago</span>
2918
+ <span class="step__connector" aria-hidden="true"></span>
2919
+ </li>
2920
+ <li class="step step--upcoming">
2921
+ <span class="step__marker"><span class="visually-hidden">Pendiente: </span>4</span>
2922
+ <span class="step__label">Confirmación</span>
2923
+ </li>
2924
+ </ol>
2925
+ <ol class="stepper stepper--vertical" aria-label="Progreso del despliegue">
2926
+ <li class="step step--complete">
2927
+ <span class="step__marker"><span class="visually-hidden">Completado: </span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg></span>
2928
+ <span class="step__label">Compilar el proyecto</span>
2929
+ <span class="step__connector" aria-hidden="true"></span>
2930
+ </li>
2931
+ <li class="step step--current" aria-current="step">
2932
+ <span class="step__marker"><span class="visually-hidden">Paso actual: </span>2</span>
2933
+ <span class="step__label">Ejecutar pruebas</span>
2934
+ <span class="step__connector" aria-hidden="true"></span>
2935
+ </li>
2936
+ <li class="step step--upcoming">
2937
+ <span class="step__marker"><span class="visually-hidden">Pendiente: </span>3</span>
2938
+ <span class="step__label">Publicar en producción</span>
2939
+ </li>
2940
+ </ol>
2941
+ </div>
2942
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>&lt;ol class="stepper" aria-label="Progreso de la configuración"&gt;
2943
+ &lt;li class="step step--complete"&gt;
2944
+ &lt;span class="step__marker"&gt;&lt;svg aria-hidden="true"&gt;…check…&lt;/svg&gt;&lt;/span&gt;
2945
+ &lt;span class="step__label"&gt;Cuenta&lt;/span&gt;
2946
+ &lt;span class="step__connector" aria-hidden="true"&gt;&lt;/span&gt;
2947
+ &lt;/li&gt;
2948
+ &lt;li class="step step--current" aria-current="step"&gt;
2949
+ &lt;span class="step__marker"&gt;3&lt;/span&gt;
2950
+ &lt;span class="step__label"&gt;Pago&lt;/span&gt;
2951
+ &lt;span class="step__connector" aria-hidden="true"&gt;&lt;/span&gt;
2952
+ &lt;/li&gt;
2953
+ &lt;li class="step step--upcoming"&gt;
2954
+ &lt;span class="step__marker"&gt;4&lt;/span&gt;&lt;span class="step__label"&gt;Confirmación&lt;/span&gt;
2955
+ &lt;/li&gt;
2956
+ &lt;/ol&gt;
2957
+ &lt;!-- El último paso no lleva conector. Variante: .stepper--vertical. --&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
2958
+ </section>
2959
+
2960
+ <!-- ================= KBD ================= -->
2961
+ <section id="comp-kbd">
2962
+ <h2>Kbd</h2>
2963
+ <p>Tecla o atajo de teclado sobre <span class="code">&lt;kbd&gt;</span>: JetBrains Mono, borde <span class="code">--border-strong</span> y sombra sutil, alto y ancho mínimo 20px. Para combos se encadenan varias con un "+" literal. Es informativa, nunca un botón.</p>
2964
+ <div class="ds-demo" style="gap:var(--space-4);align-items:center;">
2965
+ <span style="display:inline-flex;align-items:center;gap:6px;"><kbd class="kbd">Esc</kbd></span>
2966
+ <span style="display:inline-flex;align-items:center;gap:6px;"><kbd class="kbd">Tab</kbd></span>
2967
+ <span style="display:inline-flex;align-items:center;gap:6px;"><kbd class="kbd">↵</kbd></span>
2968
+ <span style="display:inline-flex;align-items:center;gap:6px;color:var(--text-muted);"><kbd class="kbd">⌘</kbd> + <kbd class="kbd">K</kbd></span>
2969
+ <span style="display:inline-flex;align-items:center;gap:6px;color:var(--text-muted);"><kbd class="kbd">Ctrl</kbd> + <kbd class="kbd">S</kbd></span>
2970
+ <span style="display:inline-flex;align-items:center;gap:6px;color:var(--text-muted);"><kbd class="kbd">Shift</kbd> + <kbd class="kbd">⇥</kbd></span>
2971
+ </div>
2972
+ <div class="ds-code-wrap"><details class="ds-code"><summary>Ver código</summary><pre><code>Pulsa &lt;kbd class="kbd"&gt;Esc&lt;/kbd&gt; para cerrar.
2973
+
2974
+ Abre el buscador con &lt;kbd class="kbd"&gt;⌘&lt;/kbd&gt; + &lt;kbd class="kbd"&gt;K&lt;/kbd&gt;.
2975
+ Guarda con &lt;kbd class="kbd"&gt;Ctrl&lt;/kbd&gt; + &lt;kbd class="kbd"&gt;S&lt;/kbd&gt;.</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
2008
2976
  </section>
2009
2977
 
2010
2978
  <!-- ================= MOTION ================= -->
@@ -2042,16 +3010,22 @@ html { scroll-behavior: smooth; }
2042
3010
  <table class="table">
2043
3011
  <thead><tr><th>Combinación</th><th class="num">Contraste</th><th>Veredicto</th></tr></thead>
2044
3012
  <tbody>
2045
- <tr><td>night-100 sobre night-950</td><td class="num">≈ 16:1</td><td><span class="badge badge--success">AAA</span></td></tr>
2046
- <tr><td>accent-400 sobre night-950</td><td class="num">≈ 8.5:1</td><td><span class="badge badge--success">AA — sirve como texto/link</span></td></tr>
2047
- <tr><td>night-950 sobre accent-400 (CTA)</td><td class="num">≈ 8.5:1</td><td><span class="badge badge--success">AA — texto del CTA oscuro</span></td></tr>
2048
- <tr><td>indigo-500 sobre night-950</td><td class="num">≈ 1.9:1</td><td><span class="badge badge--danger">Solo fondo, nunca texto</span></td></tr>
2049
- <tr><td>night-50 sobre indigo-500</td><td class="num">≈ 9:1</td><td><span class="badge badge--success">AAA</span></td></tr>
3013
+ <tr><td>night-100 sobre night-950</td><td class="num">17.25:1</td><td><span class="badge badge--success">AAA</span></td></tr>
3014
+ <tr><td>accent-400 sobre night-950</td><td class="num">9.50:1</td><td><span class="badge badge--success">AA — sirve como texto/link</span></td></tr>
3015
+ <tr><td>night-950 sobre accent-400 (CTA)</td><td class="num">9.50:1</td><td><span class="badge badge--success">AA — texto del CTA oscuro</span></td></tr>
3016
+ <tr><td>indigo-500 sobre night-950</td><td class="num">2.09:1</td><td><span class="badge badge--danger">Solo fondo, nunca texto</span></td></tr>
3017
+ <tr><td>night-50 sobre indigo-500</td><td class="num">9.29:1</td><td><span class="badge badge--success">AAA</span></td></tr>
3018
+ <tr><td>accent-600 sobre blanco <em>(modo claro)</em></td><td class="num">4.01:1</td><td><span class="badge badge--warning">Solo UI — nunca texto</span></td></tr>
3019
+ <tr><td>accent-700 sobre blanco <em>(texto de acción en claro)</em></td><td class="num">5.95:1</td><td><span class="badge badge--success">AA</span></td></tr>
3020
+ <tr><td>accent-800 sobre blanco <em>(hover en claro)</em></td><td class="num">8.74:1</td><td><span class="badge badge--success">AAA</span></td></tr>
3021
+ <tr><td>info sobre info-bg <em>(badge en claro)</em></td><td class="num">5.20:1</td><td><span class="badge badge--success">AA</span></td></tr>
2050
3022
  </tbody>
2051
3023
  </table>
2052
3024
  </div>
2053
3025
  <ul style="color:var(--text-secondary);line-height:1.9;font-size:var(--text-sm);">
2054
3026
  <li>Mínimos WCAG AA: 4.5:1 texto normal, 3:1 texto grande y componentes UI.</li>
3027
+ <li>Ratios <strong>calculados</strong> con la fórmula WCAG 2.x, no estimados: <span class="code">mcp/test/contraste-tokens.test.ts</span> los recalcula desde <span class="code">tokens.css</span> en cada corrida.</li>
3028
+ <li>En modo claro, <span class="code">accent-600</span> (<span class="code">#1f87bb</span>) vale como <strong>UI</strong> (focus, bordes, iconos) pero <strong>no como texto</strong>: da 4.01:1. El texto de acción usa <span class="code">accent-700</span>.</li>
2055
3029
  <li>Focus visible siempre (<span class="code">:focus-visible</span> global); estado nunca solo con color — siempre color + icono + texto.</li>
2056
3030
  <li>Touch targets de 44×44px. IconButton siempre con <span class="code">aria-label</span>.</li>
2057
3031
  <li><span class="code">role="progressbar"</span> en Progress, <span class="code">aria-expanded</span> en Accordion, <span class="code">aria-current</span> en navegación.</li>
@@ -2064,11 +3038,11 @@ html { scroll-behavior: smooth; }
2064
3038
  <p>El sistema se distribuye en <span class="code">dist/nocturna-electrica/</span>: <strong>nocturna-electrica.css</strong> (framework completo), <strong>tokens.css</strong> (solo variables), <strong>tailwind.css</strong> (bloque <span class="code">@theme</span> para Tailwind v4) y <strong>tokens.json</strong> (formato estándar W3C DTCG 2025.10, interoperable con Figma / Tokens Studio / Style Dictionary).</p>
2065
3039
 
2066
3040
  <h3>Variables CSS (núcleo)</h3>
2067
- <details class="ds-code" open><summary>tokens :root (aliases semánticos)<button class="ds-copy" type="button">Copiar</button></summary><pre><code>:root {
3041
+ <div class="ds-code-wrap"><details class="ds-code" open><summary>tokens :root (aliases semánticos)</summary><pre><code>:root {
2068
3042
  /* Superficies */
2069
3043
  --bg: #010203;
2070
3044
  --surface-1: #0b0d12; --surface-2: #14171f; --surface-3: #1e222d;
2071
- --border: #3d4354; --border-strong: #2a2f3d;
3045
+ --border: #3d4354; --border-strong: #6b7183;
2072
3046
 
2073
3047
  /* Texto */
2074
3048
  --text-primary: #e8eaef; --text-secondary: #c5c9d4;
@@ -2102,10 +3076,10 @@ html { scroll-behavior: smooth; }
2102
3076
  --ease-out: cubic-bezier(0.2, 0, 0, 1);
2103
3077
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
2104
3078
  --ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);
2105
- }</code></pre></details>
3079
+ }</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
2106
3080
 
2107
3081
  <h3 style="margin-top:32px;">Tailwind CSS v4 — <span class="code">@theme</span></h3>
2108
- <details class="ds-code"><summary>tailwind.css<button class="ds-copy" type="button">Copiar</button></summary><pre><code>@import "tailwindcss";
3082
+ <div class="ds-code-wrap"><details class="ds-code"><summary>tailwind.css</summary><pre><code>@import "tailwindcss";
2109
3083
 
2110
3084
  @theme {
2111
3085
  --color-night-950: #010203; --color-night-900: #0b0d12;
@@ -2117,7 +3091,7 @@ html { scroll-behavior: smooth; }
2117
3091
  --color-accent-100: #d9f0fb; --color-accent-200: #a8ddf6;
2118
3092
  --color-accent-300: #7dccf1; --color-accent-400: #52baed;
2119
3093
  --color-accent-500: #2fa3dd; --color-accent-600: #1f87bb;
2120
- --color-accent-700: #186a94;
3094
+ --color-accent-700: #186a94; --color-accent-800: #12506f;
2121
3095
  --color-indigo-300: #7a83c4; --color-indigo-400: #565fa3;
2122
3096
  --color-indigo-500: #353d7c; --color-indigo-600: #2b3264;
2123
3097
  --color-indigo-700: #22284f; --color-indigo-800: #1a1e3b;
@@ -2139,7 +3113,7 @@ html { scroll-behavior: smooth; }
2139
3113
  --ease-in: cubic-bezier(0.4, 0, 1, 1);
2140
3114
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
2141
3115
  --ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);
2142
- }</code></pre></details>
3116
+ }</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
2143
3117
 
2144
3118
  <div class="ds-callout ds-callout--info" style="margin-top:32px;">
2145
3119
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4m0-4h.01"/></svg>
@@ -2150,33 +3124,44 @@ html { scroll-behavior: smooth; }
2150
3124
  <!-- ================= INSTALACIÓN ================= -->
2151
3125
  <section id="instalacion">
2152
3126
  <h2>Instalación</h2>
2153
- <p>El sistema se distribuye como archivos en <span class="code">dist/nocturna-electrica/</span> — cópialos a tu proyecto (aún no hay paquete npm). Elige la vía según tu stack. En todas: <strong>dark por defecto</strong>; <span class="code">&lt;html data-theme="light"&gt;</span> activa el modo claro, y los componentes consumen <strong>solo aliases semánticos</strong>, nunca hex.</p>
3127
+ <p>El sistema se distribuye como paquete npm (<span class="code">@higinio/design-css</span>) y también como archivos sueltos en <span class="code">dist/nocturna-electrica/</span>. Instálalo o cópialos, y elige qué importar según tu stack (A/B/C/D). En todas: <strong>dark por defecto</strong>; <span class="code">&lt;html data-theme="light"&gt;</span> activa el modo claro, y los componentes consumen <strong>solo aliases semánticos</strong>, nunca hex.</p>
3128
+
3129
+ <h3 style="margin-top:32px;">Instalar desde npm (recomendado)</h3>
3130
+ <p>Un solo paquete con todos los sistemas, cada uno en su subruta; Nocturna Eléctrica vive en <span class="code">/nocturna/</span>. Instálalo y usa las vías de abajo apuntando a la subruta, en vez de a un archivo copiado.</p>
3131
+ <div class="ds-code-wrap"><details class="ds-code" open><summary>terminal + CSS de entrada</summary><pre><code>npm i @higinio/design-css
3132
+
3133
+ /* framework completo (vía A) */
3134
+ @import "@higinio/design-css/nocturna/framework.css";
3135
+ /* o solo tokens (vía B) */
3136
+ @import "@higinio/design-css/nocturna/tokens.css";
3137
+ /* o Tailwind v4 (vía C) */
3138
+ @import "@higinio/design-css/nocturna/tailwind.css";</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
2154
3139
 
2155
3140
  <h3 style="margin-top:32px;">A · CSS drop-in (framework completo)</h3>
2156
3141
  <p>Para cualquier proyecto — HTML plano o cualquier framework. Trae variables + clases de componentes listas (tipo Bootstrap) y ya importa las fuentes.</p>
2157
- <details class="ds-code" open><summary>index.html<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;link rel="stylesheet" href="/estilos/nocturna-electrica.css"&gt;
3142
+ <div class="ds-code-wrap"><details class="ds-code" open><summary>index.html</summary><pre><code>&lt;link rel="stylesheet" href="/estilos/nocturna-electrica.css"&gt;
2158
3143
  &lt;!-- luego usas las clases directo: --&gt;
2159
3144
  &lt;button class="btn btn--primary"&gt;Guardar&lt;/button&gt;
2160
- &lt;div class="card"&gt;…&lt;/div&gt;</code></pre></details>
3145
+ &lt;div class="card"&gt;…&lt;/div&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
2161
3146
 
2162
3147
  <h3 style="margin-top:32px;">B · Solo tokens</h3>
2163
3148
  <p>Si diseñas tus propios componentes: solo las variables, sin clases.</p>
2164
- <details class="ds-code"><summary>styles.css<button class="ds-copy" type="button">Copiar</button></summary><pre><code>@import "./tokens.css";
3149
+ <div class="ds-code-wrap"><details class="ds-code"><summary>styles.css</summary><pre><code>@import "./tokens.css";
2165
3150
 
2166
3151
  .mi-panel {
2167
3152
  background: var(--surface-1);
2168
3153
  color: var(--text-primary);
2169
3154
  border: 1px solid var(--border);
2170
- }</code></pre></details>
3155
+ }</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
2171
3156
 
2172
3157
  <h3 style="margin-top:32px;">C · Tailwind v4</h3>
2173
3158
  <p>Tailwind v4 se configura por CSS. El <span class="code">tailwind.css</span> ya incluye <span class="code">@import "tailwindcss"</span>, los tokens y el bloque <span class="code">@theme</span> — úsalo como tu hoja de entrada (mantén <span class="code">tokens.css</span> a su lado, que es lo que importa).</p>
2174
- <details class="ds-code"><summary>terminal + CSS de entrada<button class="ds-copy" type="button">Copiar</button></summary><pre><code># 1) instala Tailwind v4
3159
+ <div class="ds-code-wrap"><details class="ds-code"><summary>terminal + CSS de entrada</summary><pre><code># 1) instala Tailwind v4
2175
3160
  npm i tailwindcss @tailwindcss/postcss
2176
3161
 
2177
3162
  # 2) copia tailwind.css + tokens.css a tu carpeta de CSS
2178
3163
  # y usa tailwind.css como entrada. Ya tienes utilidades:
2179
- &lt;div class="bg-night-900 text-accent-400"&gt;…&lt;/div&gt;</code></pre></details>
3164
+ &lt;div class="bg-night-900 text-accent-400"&gt;…&lt;/div&gt;</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
2180
3165
 
2181
3166
  <h3 style="margin-top:32px;">D · Pipelines de tokens</h3>
2182
3167
  <p>Para Figma, Tokens Studio o Style Dictionary: importa <span class="code">tokens.json</span> (formato estándar W3C DTCG 2025.10).</p>
@@ -2190,20 +3175,23 @@ npm i tailwindcss @tailwindcss/postcss
2190
3175
  <!-- ================= MCP ================= -->
2191
3176
  <section id="mcp">
2192
3177
  <h2>MCP</h2>
2193
- <p>El paquete <span class="code">@higinio/design-mcp</span> expone ambos sistemas de diseño — Celeste Clásica y Nocturna Eléctrica — a cualquier IDE o cliente compatible con Model Context Protocol: tokens, componentes, reglas duras y validación de código. Complementa a las skills de Claude, no las reemplaza: misma fuente de verdad, accesible también fuera del entorno de Claude.</p>
3178
+ <p>El paquete <span class="code">@higinio/design-mcp</span> expone los sistemas de diseño de Higinio Sosa, entre ellos Nocturna Eléctrica (<span class="code">system="nocturna"</span>), a cualquier IDE o cliente compatible con Model Context Protocol: tokens, componentes, reglas duras y validación de código. Complementa a las skills de Claude, no las reemplaza: misma fuente de verdad, accesible también fuera del entorno de Claude.</p>
2194
3179
 
2195
- <h3>Las 8 tools</h3>
3180
+ <h3>Las 11 tools</h3>
2196
3181
  <div class="ds-demo ds-demo--col" style="padding:0;overflow-x:auto;">
2197
3182
  <table class="table">
2198
3183
  <thead><tr><th>Tool</th><th>Qué hace</th></tr></thead>
2199
3184
  <tbody>
2200
3185
  <tr><td class="mono">list_design_systems</td><td>Lista los sistemas de diseño disponibles.</td></tr>
2201
- <tr><td class="mono">get_token</td><td>Devuelve el valor resuelto de un token o alias de un sistema.</td></tr>
3186
+ <tr><td class="mono">get_token</td><td>Resuelve un alias semántico o un token DTCG por path a su valor.</td></tr>
2202
3187
  <tr><td class="mono">search_tokens</td><td>Busca tokens por nombre, categoría o valor.</td></tr>
2203
3188
  <tr><td class="mono">list_components</td><td>Lista los componentes documentados de un sistema.</td></tr>
2204
3189
  <tr><td class="mono">get_component_spec</td><td>Devuelve la especificación completa de un componente: estados, medidas, variantes.</td></tr>
2205
3190
  <tr><td class="mono">get_rules</td><td>Devuelve las reglas duras (no negociables) del sistema.</td></tr>
2206
3191
  <tr><td class="mono">validate_code</td><td>Audita CSS/HTML contra las reglas del sistema — hex directos, contraste, aliases.</td></tr>
3192
+ <tr><td class="mono">validate_motion</td><td>Audita el motion contra las reglas del sistema: <span class="code">transition</span> sobre todas las propiedades, ease-in en entradas, propiedades de layout, duraciones por encima de <span class="code">--duration-page</span>.</td></tr>
3193
+ <tr><td class="mono">check_slop</td><td>Detecta clichés de diseño generado por IA: gradientes, glassmorphism, sombras de negro puro, fuentes genéricas y copy de marketing.</td></tr>
3194
+ <tr><td class="mono">checklist_preflight</td><td>Checklist estático de pre-producción sobre HTML/CSS: metadatos, headings, imágenes y accesibilidad de controles.</td></tr>
2207
3195
  <tr><td class="mono">generate_snippet</td><td>Genera un snippet de componente listo para usar, con los aliases correctos.</td></tr>
2208
3196
  </tbody>
2209
3197
  </table>
@@ -2211,7 +3199,7 @@ npm i tailwindcss @tailwindcss/postcss
2211
3199
 
2212
3200
  <h3 style="margin-top:32px;">Conectar (remoto)</h3>
2213
3201
  <p>Añade el servidor a tu <span class="code">.mcp.json</span>:</p>
2214
- <details class="ds-code" open><summary>.mcp.json<button class="ds-copy" type="button">Copiar</button></summary><pre><code>{
3202
+ <div class="ds-code-wrap"><details class="ds-code" open><summary>.mcp.json</summary><pre><code>{
2215
3203
  "mcpServers": {
2216
3204
  "design": {
2217
3205
  "type": "http",
@@ -2219,15 +3207,15 @@ npm i tailwindcss @tailwindcss/postcss
2219
3207
  "headers": { "Authorization": "Bearer &lt;tu-token&gt;" }
2220
3208
  }
2221
3209
  }
2222
- }</code></pre></details>
3210
+ }</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
2223
3211
 
2224
3212
  <h3 style="margin-top:32px;">Ejemplo</h3>
2225
3213
  <p>Consultar un token de este sistema:</p>
2226
- <details class="ds-code"><summary>get_token<button class="ds-copy" type="button">Copiar</button></summary><pre><code>get_token(system="nocturna", name="--accent")
2227
- → "#52baed"</code></pre></details>
3214
+ <div class="ds-code-wrap"><details class="ds-code"><summary>get_token</summary><pre><code>get_token(system="nocturna", name="--accent")
3215
+ → "#52baed"</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
2228
3216
  <p>Auditar código antes de integrarlo:</p>
2229
- <details class="ds-code"><summary>validate_code<button class="ds-copy" type="button">Copiar</button></summary><pre><code>validate_code(system="nocturna", code="…")
2230
- → { ok: false, issues: ["hex directo #52baed en línea 12 — usa var(--accent-400)"] }</code></pre></details>
3217
+ <div class="ds-code-wrap"><details class="ds-code"><summary>validate_code</summary><pre><code>validate_code(system="nocturna", code="…")
3218
+ → { ok: false, issues: ["hex directo #52baed en línea 12 — usa var(--accent-400)"] }</code></pre></details><button class="ds-copy" type="button">Copiar</button></div>
2231
3219
 
2232
3220
  <div class="ds-callout ds-callout--info" style="margin-top:32px;">
2233
3221
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4m0-4h.01"/></svg>
@@ -2242,14 +3230,14 @@ npm i tailwindcss @tailwindcss/postcss
2242
3230
 
2243
3231
  <div class="toast-stack" id="toastStack" role="status" aria-live="polite"></div>
2244
3232
  <div class="modal-overlay" id="demoModal" hidden>
2245
- <div class="modal" role="dialog" aria-modal="true" aria-labelledby="demoModalTitle">
3233
+ <div class="modal" role="dialog" aria-modal="true" aria-labelledby="demoModalTitle" aria-describedby="demoModalDesc" tabindex="-1">
2246
3234
  <div class="modal__header">
2247
3235
  <h4 class="modal__title" id="demoModalTitle">Archivar proyecto</h4>
2248
3236
  <button class="icon-btn icon-btn--ghost icon-btn--sm" data-close-modal aria-label="Cerrar">
2249
3237
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
2250
3238
  </button>
2251
3239
  </div>
2252
- <p style="font-size:var(--text-sm);color:var(--text-secondary);">El proyecto dejará de aparecer en el panel, pero conservará sus datos. Puedes restaurarlo cuando quieras.</p>
3240
+ <p id="demoModalDesc" style="font-size:var(--text-sm);color:var(--text-secondary);">El proyecto dejará de aparecer en el panel, pero conservará sus datos. Puedes restaurarlo cuando quieras.</p>
2253
3241
  <div class="modal__footer">
2254
3242
  <button class="btn btn--outline" data-close-modal>Cancelar</button>
2255
3243
  <button class="btn btn--primary" data-close-modal>Archivar</button>
@@ -2257,7 +3245,7 @@ npm i tailwindcss @tailwindcss/postcss
2257
3245
  </div>
2258
3246
  </div>
2259
3247
  <div class="modal-overlay" id="demoModalLg" hidden>
2260
- <div class="modal modal--lg" role="dialog" aria-modal="true" aria-labelledby="demoModalLgTitle">
3248
+ <div class="modal modal--lg" role="dialog" aria-modal="true" aria-labelledby="demoModalLgTitle" tabindex="-1">
2261
3249
  <div class="modal__header">
2262
3250
  <h4 class="modal__title" id="demoModalLgTitle">Invitar al equipo</h4>
2263
3251
  <button class="icon-btn icon-btn--ghost icon-btn--sm" data-close-modal aria-label="Cerrar">
@@ -2418,10 +3406,9 @@ spaces.forEach(function(s){
2418
3406
 
2419
3407
  /* ---------- Copiar código ---------- */
2420
3408
  document.querySelectorAll('.ds-copy').forEach(function(btn){
2421
- btn.addEventListener('click', function(e){
2422
- e.preventDefault(); e.stopPropagation();
2423
- var details = btn.closest('details');
2424
- var code = details ? details.querySelector('code') : null;
3409
+ btn.addEventListener('click', function(){
3410
+ var box = btn.closest('.ds-code-wrap');
3411
+ var code = box ? box.querySelector('code') : null;
2425
3412
  if (code) {
2426
3413
  copyText(code.textContent, function(){
2427
3414
  btn.textContent = '¡Copiado!'; btn.classList.add('is-done');
@@ -2512,6 +3499,31 @@ tabs.querySelectorAll('.tabs__btn').forEach(function(b){
2512
3499
  });
2513
3500
  setTimeout(function(){ moveIndicator(tabs.querySelector('.is-active')); }, 100);
2514
3501
 
3502
+ /* ---------- Sidebar contraíble: riel de iconos ---------- */
3503
+ document.querySelectorAll('.sidebar__toggle').forEach(function(btn){
3504
+ var nav = document.getElementById(btn.getAttribute('aria-controls'));
3505
+ var label = btn.querySelector('.sidebar__label');
3506
+ if (!nav) return;
3507
+ btn.addEventListener('click', function(){
3508
+ var collapsed = nav.classList.toggle('is-collapsed');
3509
+ btn.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
3510
+ if (label) label.textContent = collapsed ? 'Expandir' : 'Contraer';
3511
+ });
3512
+ });
3513
+
3514
+ /* ---------- Chip de métrica: sumar un vaso ---------- */
3515
+ (function(){
3516
+ var chip = document.getElementById('dsWaterChip');
3517
+ if (!chip) return;
3518
+ var value = chip.querySelector('.metric-chip__value');
3519
+ var max = 8, n = 3;
3520
+ chip.addEventListener('click', function(){
3521
+ n = n >= max ? 0 : n + 1;
3522
+ value.textContent = n + ' de ' + max;
3523
+ chip.setAttribute('aria-label', 'Agua: ' + n + ' de ' + max + ' vasos. ' + (n < max ? 'Sumar un vaso' : 'Empezar de nuevo'));
3524
+ });
3525
+ })();
3526
+
2515
3527
  /* ---------- Alert ---------- */
2516
3528
  document.querySelectorAll('.js-alert-close').forEach(function(btn){
2517
3529
  btn.addEventListener('click', function(){
@@ -2565,22 +3577,272 @@ document.querySelectorAll('[data-toast]').forEach(function(b){
2565
3577
  });
2566
3578
  });
2567
3579
 
2568
- /* ---------- Modal ---------- */
2569
- function wireModal(triggerId, modalId){
3580
+ /* ---------- Modal (focus-trap + retorno de foco + Esc) ---------- */
3581
+ var FOCUSABLE_SEL = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
3582
+ function getFocusable(container){
3583
+ return Array.prototype.slice.call(container.querySelectorAll(FOCUSABLE_SEL))
3584
+ .filter(function(el){ return el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length; });
3585
+ }
3586
+ function wireModal(triggerId, overlayId){
2570
3587
  var trigger = document.getElementById(triggerId);
2571
- var modalEl = document.getElementById(modalId);
2572
- if (!trigger || !modalEl) return;
2573
- trigger.addEventListener('click', function(){ modalEl.hidden = false; });
2574
- modalEl.addEventListener('click', function(e){
2575
- if (e.target === modalEl || e.target.closest('[data-close-modal]')) modalEl.hidden = true;
3588
+ var overlay = document.getElementById(overlayId);
3589
+ if (!trigger || !overlay) return;
3590
+ var panel = overlay.querySelector('.modal');
3591
+ var persistent = overlay.hasAttribute('data-modal-persistent'); /* flujos destructivos */
3592
+ var lastFocused = null;
3593
+
3594
+ function open(){
3595
+ lastFocused = document.activeElement;
3596
+ overlay.hidden = false;
3597
+ if (panel) panel.focus(); /* el panel tiene tabindex="-1"; el lector anuncia el título */
3598
+ }
3599
+ function close(){
3600
+ if (overlay.hidden) return;
3601
+ overlay.hidden = true;
3602
+ if (lastFocused && typeof lastFocused.focus === 'function') lastFocused.focus(); /* retorno de foco */
3603
+ }
3604
+
3605
+ trigger.addEventListener('click', open);
3606
+
3607
+ overlay.addEventListener('click', function(e){
3608
+ if (e.target.closest('[data-close-modal]')) { close(); return; }
3609
+ if (e.target === overlay && !persistent) close(); /* click en overlay */
2576
3610
  });
2577
- document.addEventListener('keydown', function(e){
2578
- if (e.key === 'Escape' && !modalEl.hidden) modalEl.hidden = true;
3611
+
3612
+ overlay.addEventListener('keydown', function(e){
3613
+ if (e.key === 'Escape' && !persistent) { e.stopPropagation(); close(); return; }
3614
+ if (e.key !== 'Tab' || !panel) return;
3615
+ var f = getFocusable(panel);
3616
+ if (!f.length) { e.preventDefault(); return; }
3617
+ var first = f[0], last = f[f.length - 1], active = document.activeElement;
3618
+ if (f.indexOf(active) === -1) { /* foco en el panel (tabindex=-1): entra al extremo correcto */
3619
+ e.preventDefault(); (e.shiftKey ? last : first).focus(); return;
3620
+ }
3621
+ if (e.shiftKey && active === first) { e.preventDefault(); last.focus(); }
3622
+ else if (!e.shiftKey && active === last) { e.preventDefault(); first.focus(); }
2579
3623
  });
2580
3624
  }
2581
3625
  wireModal('openModal', 'demoModal');
2582
3626
  wireModal('openModalLg', 'demoModalLg');
2583
3627
 
3628
+ /* ---------- Drawer (focus-trap + retorno de foco + Esc + overlay) ---------- */
3629
+ function wireDrawer(triggerId, drawerId){
3630
+ var trigger = document.getElementById(triggerId);
3631
+ var drawer = document.getElementById(drawerId);
3632
+ if (!trigger || !drawer) return;
3633
+ var panel = drawer.querySelector('.drawer__panel');
3634
+ var lastFocused = null;
3635
+ function open(){
3636
+ lastFocused = document.activeElement;
3637
+ drawer.hidden = false;
3638
+ if (trigger.hasAttribute('aria-expanded')) trigger.setAttribute('aria-expanded', 'true');
3639
+ /* dos pasos: pinta oculto y en el siguiente frame añade .is-open para que el transform anime */
3640
+ requestAnimationFrame(function(){ drawer.classList.add('is-open'); });
3641
+ if (panel) panel.focus(); /* tabindex="-1"; el lector anuncia el título */
3642
+ }
3643
+ function close(){
3644
+ if (drawer.hidden) return;
3645
+ drawer.classList.remove('is-open');
3646
+ if (trigger.hasAttribute('aria-expanded')) trigger.setAttribute('aria-expanded', 'false');
3647
+ var finish = function(){ drawer.hidden = true; if (lastFocused && typeof lastFocused.focus === 'function') lastFocused.focus(); };
3648
+ if (reduced) finish(); else setTimeout(finish, 250); /* espera el slide de salida (--duration-slow) */
3649
+ }
3650
+ trigger.addEventListener('click', open);
3651
+ drawer.addEventListener('click', function(e){
3652
+ if (e.target.closest('[data-close-drawer]')) close(); /* overlay o botón de cierre */
3653
+ });
3654
+ drawer.addEventListener('keydown', function(e){
3655
+ if (e.key === 'Escape') { e.stopPropagation(); close(); return; }
3656
+ if (e.key !== 'Tab' || !panel) return;
3657
+ var f = getFocusable(panel);
3658
+ if (!f.length) { e.preventDefault(); return; }
3659
+ var first = f[0], last = f[f.length - 1], active = document.activeElement;
3660
+ if (f.indexOf(active) === -1) { e.preventDefault(); (e.shiftKey ? last : first).focus(); return; }
3661
+ if (e.shiftKey && active === first) { e.preventDefault(); last.focus(); }
3662
+ else if (!e.shiftKey && active === last) { e.preventDefault(); first.focus(); }
3663
+ });
3664
+ }
3665
+ wireDrawer('openDrawer', 'demoDrawer');
3666
+ wireDrawer('openNavDrawer', 'demoNavDrawer');
3667
+
3668
+ /* ---------- Popover (abre por CLICK; foco entra; Esc / clic fuera cierran) ---------- */
3669
+ (function(){
3670
+ var pop = document.querySelector('#comp-popover .popover');
3671
+ var trigger = document.getElementById('popTrigger');
3672
+ var panel = document.getElementById('popPanel');
3673
+ if (!pop || !trigger || !panel) return;
3674
+ function open(){
3675
+ pop.classList.add('is-open');
3676
+ trigger.setAttribute('aria-expanded', 'true');
3677
+ var f = getFocusable(panel);
3678
+ if (f.length) f[0].focus();
3679
+ }
3680
+ function close(returnFocus){
3681
+ if (!pop.classList.contains('is-open')) return;
3682
+ pop.classList.remove('is-open');
3683
+ trigger.setAttribute('aria-expanded', 'false');
3684
+ if (returnFocus) trigger.focus();
3685
+ }
3686
+ trigger.addEventListener('click', function(e){
3687
+ e.stopPropagation();
3688
+ if (pop.classList.contains('is-open')) close(true); else open();
3689
+ });
3690
+ document.addEventListener('click', function(e){
3691
+ if (pop.classList.contains('is-open') && !pop.contains(e.target)) close(false);
3692
+ });
3693
+ document.addEventListener('keydown', function(e){
3694
+ if (e.key === 'Escape' && pop.classList.contains('is-open')) close(true);
3695
+ });
3696
+ })();
3697
+
3698
+ /* ---------- Combobox (filtra en vivo + teclado ↑/↓/Enter/Esc) ---------- */
3699
+ (function(){
3700
+ var input = document.getElementById('cbInput');
3701
+ var list = document.getElementById('cbList');
3702
+ if (!input || !list) return;
3703
+ var options = Array.prototype.slice.call(list.querySelectorAll('.combobox__option'));
3704
+ var activeIndex = -1;
3705
+ function visible(){ return options.filter(function(o){ return !o.hidden; }); }
3706
+ function openList(){ list.hidden = false; input.setAttribute('aria-expanded', 'true'); }
3707
+ function closeList(){ list.hidden = true; input.setAttribute('aria-expanded', 'false'); setActive(-1); }
3708
+ function setActive(i){
3709
+ options.forEach(function(o){ o.classList.remove('is-active'); });
3710
+ var vis = visible();
3711
+ activeIndex = i;
3712
+ if (i >= 0 && i < vis.length) {
3713
+ vis[i].classList.add('is-active');
3714
+ input.setAttribute('aria-activedescendant', vis[i].id);
3715
+ vis[i].scrollIntoView({ block: 'nearest' });
3716
+ } else {
3717
+ input.setAttribute('aria-activedescendant', '');
3718
+ }
3719
+ }
3720
+ function filter(){
3721
+ var q = input.value.trim().toLowerCase();
3722
+ options.forEach(function(o){
3723
+ o.hidden = o.textContent.toLowerCase().indexOf(q) === -1;
3724
+ });
3725
+ if (visible().length) openList(); else closeList();
3726
+ setActive(-1);
3727
+ }
3728
+ function choose(opt){
3729
+ options.forEach(function(o){ o.setAttribute('aria-selected', 'false'); o.hidden = false; });
3730
+ opt.setAttribute('aria-selected', 'true');
3731
+ input.value = opt.textContent.trim();
3732
+ closeList();
3733
+ input.focus();
3734
+ }
3735
+ input.addEventListener('input', filter);
3736
+ input.addEventListener('keydown', function(e){
3737
+ if (e.key === 'ArrowDown') {
3738
+ e.preventDefault();
3739
+ if (list.hidden) openList();
3740
+ var v = visible();
3741
+ setActive(activeIndex < 0 ? 0 : Math.min(activeIndex + 1, v.length - 1));
3742
+ } else if (e.key === 'ArrowUp') {
3743
+ e.preventDefault();
3744
+ if (list.hidden) openList();
3745
+ setActive(Math.max(activeIndex - 1, 0));
3746
+ } else if (e.key === 'Enter') {
3747
+ var v = visible();
3748
+ if (!list.hidden && activeIndex >= 0 && v[activeIndex]) { e.preventDefault(); choose(v[activeIndex]); }
3749
+ } else if (e.key === 'Escape') {
3750
+ if (!list.hidden) { e.preventDefault(); closeList(); }
3751
+ }
3752
+ });
3753
+ list.addEventListener('click', function(e){
3754
+ var opt = e.target.closest('.combobox__option');
3755
+ if (opt) choose(opt);
3756
+ });
3757
+ list.addEventListener('mousemove', function(e){
3758
+ var opt = e.target.closest('.combobox__option');
3759
+ if (!opt) return;
3760
+ var i = visible().indexOf(opt);
3761
+ if (i !== -1 && i !== activeIndex) setActive(i);
3762
+ });
3763
+ document.addEventListener('click', function(e){
3764
+ if (!list.hidden && e.target !== input && !list.contains(e.target)) closeList();
3765
+ });
3766
+ })();
3767
+
3768
+ /* ---------- Search (botón limpiar funcional) ---------- */
3769
+ (function(){
3770
+ var input = document.getElementById('searchInput');
3771
+ var clear = document.getElementById('searchClear');
3772
+ if (!input || !clear) return;
3773
+ function sync(){ clear.hidden = input.value.length === 0; }
3774
+ input.addEventListener('input', sync);
3775
+ clear.addEventListener('click', function(){ input.value = ''; sync(); input.focus(); });
3776
+ sync();
3777
+ })();
3778
+
3779
+ /* ---------- Checkbox indeterminate (solo se puede fijar por JS) ---------- */
3780
+ var cbInd = document.getElementById('cbIndeterminate');
3781
+ if (cbInd) cbInd.indeterminate = true;
3782
+
3783
+ /* ---------- Tabla: orden por columna + seleccionar todo ---------- */
3784
+ (function(){
3785
+ var table = document.getElementById('demoSortTable');
3786
+ if (!table) return;
3787
+ var tbody = table.tBodies[0];
3788
+ var headRow = table.tHead.rows[0];
3789
+
3790
+ function cellValue(row, idx){
3791
+ var cell = row.cells[idx];
3792
+ if (!cell) return '';
3793
+ var raw = cell.getAttribute('data-sort');
3794
+ if (raw !== null) return parseFloat(raw);
3795
+ if (cell.classList.contains('num')) return parseFloat(cell.textContent.replace(/[^\d.-]/g, '')) || 0;
3796
+ return cell.textContent.trim().toLowerCase();
3797
+ }
3798
+
3799
+ Array.prototype.forEach.call(headRow.cells, function(th, idx){
3800
+ var btn = th.querySelector('.th-sort');
3801
+ if (!btn) return;
3802
+ btn.addEventListener('click', function(){
3803
+ var current = th.getAttribute('aria-sort');
3804
+ var dir = current === 'ascending' ? 'descending' : 'ascending';
3805
+ /* una sola columna ordenada a la vez */
3806
+ Array.prototype.forEach.call(headRow.cells, function(other){
3807
+ if (other !== th && other.hasAttribute('aria-sort')) other.setAttribute('aria-sort', 'none');
3808
+ });
3809
+ th.setAttribute('aria-sort', dir);
3810
+ var rows = Array.prototype.slice.call(tbody.rows);
3811
+ rows.sort(function(a, b){
3812
+ var va = cellValue(a, idx), vb = cellValue(b, idx);
3813
+ if (va < vb) return dir === 'ascending' ? -1 : 1;
3814
+ if (va > vb) return dir === 'ascending' ? 1 : -1;
3815
+ return 0;
3816
+ });
3817
+ rows.forEach(function(r){ tbody.appendChild(r); });
3818
+ });
3819
+ });
3820
+
3821
+ /* Seleccionar todo: refleja el estado (todas / ninguna / indeterminate) */
3822
+ var selectAll = document.getElementById('tblSelectAll');
3823
+ function rowBoxes(){ return Array.prototype.slice.call(tbody.querySelectorAll('.col-select .checkbox')); }
3824
+ function syncSelectAll(){
3825
+ if (!selectAll) return;
3826
+ var boxes = rowBoxes(), checked = boxes.filter(function(b){ return b.checked; }).length;
3827
+ selectAll.checked = checked === boxes.length && boxes.length > 0;
3828
+ selectAll.indeterminate = checked > 0 && checked < boxes.length;
3829
+ }
3830
+ function syncRow(box){
3831
+ var tr = box.closest('tr');
3832
+ if (tr) tr.classList.toggle('row--selected', box.checked);
3833
+ }
3834
+ if (selectAll) {
3835
+ selectAll.addEventListener('change', function(){
3836
+ rowBoxes().forEach(function(b){ b.checked = selectAll.checked; syncRow(b); });
3837
+ selectAll.indeterminate = false;
3838
+ });
3839
+ }
3840
+ rowBoxes().forEach(function(b){
3841
+ b.addEventListener('change', function(){ syncRow(b); syncSelectAll(); });
3842
+ });
3843
+ syncSelectAll(); /* estado inicial: una fila marcada → indeterminate */
3844
+ })();
3845
+
2584
3846
  /* ---------- Progress demo ---------- */
2585
3847
  var seq = [22, 47, 64, 81, 95], seqI = 2;
2586
3848
  function paintProgressRing(v){
@@ -2708,6 +3970,128 @@ if (mReplayBtn) {
2708
3970
  });
2709
3971
  });
2710
3972
  }
3973
+
3974
+ /* ---------- DatePicker (calendario en popover; flechas + Enter/Esc) ---------- */
3975
+ (function(){
3976
+ var wrap = document.getElementById('datepicker');
3977
+ var input = document.getElementById('dpInput');
3978
+ var cal = document.getElementById('dpCalendar');
3979
+ var grid = document.getElementById('dpGrid');
3980
+ var title = document.getElementById('dpTitle');
3981
+ var prev = document.getElementById('dpPrev');
3982
+ var next = document.getElementById('dpNext');
3983
+ if (!wrap || !input || !cal || !grid) return;
3984
+
3985
+ var MONTHS = ['enero','febrero','marzo','abril','mayo','junio','julio','agosto','septiembre','octubre','noviembre','diciembre'];
3986
+ var today = new Date(2026, 6, 10); // 10 de julio de 2026 (fecha del demo)
3987
+ var selected = new Date(2026, 6, 15); // día seleccionado inicial
3988
+ var view = new Date(2026, 6, 1); // mes visible
3989
+ var focusDate = new Date(selected); // día con el roving tabindex
3990
+
3991
+ function sameDay(a, b){ return a.getFullYear()===b.getFullYear() && a.getMonth()===b.getMonth() && a.getDate()===b.getDate(); }
3992
+ function fmtLabel(d){ return d.getDate() + ' de ' + MONTHS[d.getMonth()] + ' de ' + d.getFullYear(); }
3993
+
3994
+ function render(){
3995
+ var y = view.getFullYear(), m = view.getMonth();
3996
+ title.textContent = MONTHS[m] + ' ' + y;
3997
+ grid.innerHTML = '';
3998
+ var first = new Date(y, m, 1);
3999
+ var startOffset = (first.getDay() + 6) % 7; // la semana empieza en lunes
4000
+ var start = new Date(y, m, 1 - startOffset);
4001
+ for (var w = 0; w < 6; w++) {
4002
+ var row = document.createElement('div');
4003
+ row.className = 'calendar__week'; row.setAttribute('role', 'row');
4004
+ for (var d = 0; d < 7; d++) {
4005
+ var cur = new Date(start.getFullYear(), start.getMonth(), start.getDate() + (w*7 + d));
4006
+ var btn = document.createElement('button');
4007
+ btn.type = 'button';
4008
+ btn.className = 'calendar__day';
4009
+ btn.setAttribute('role', 'gridcell');
4010
+ btn.textContent = cur.getDate();
4011
+ btn.setAttribute('aria-label', fmtLabel(cur));
4012
+ btn.dataset.y = cur.getFullYear(); btn.dataset.m = cur.getMonth(); btn.dataset.d = cur.getDate();
4013
+ if (cur.getMonth() !== m) btn.classList.add('calendar__day--outside');
4014
+ if (sameDay(cur, today)) { btn.classList.add('calendar__day--today'); btn.setAttribute('aria-current', 'date'); }
4015
+ var isSel = sameDay(cur, selected);
4016
+ btn.setAttribute('aria-selected', isSel ? 'true' : 'false');
4017
+ if (isSel) btn.classList.add('calendar__day--selected');
4018
+ btn.tabIndex = sameDay(cur, focusDate) ? 0 : -1;
4019
+ row.appendChild(btn);
4020
+ }
4021
+ grid.appendChild(row);
4022
+ }
4023
+ }
4024
+ function focusRoving(){ var f = grid.querySelector('.calendar__day[tabindex="0"]'); if (f) f.focus(); }
4025
+ function open(){ render(); cal.hidden = false; focusRoving(); }
4026
+ function close(returnFocus){
4027
+ if (cal.hidden) return;
4028
+ cal.hidden = true;
4029
+ if (returnFocus) input.focus();
4030
+ }
4031
+ function setView(y, m){ view = new Date(y, m, 1); render(); }
4032
+ function moveFocus(newDate){
4033
+ focusDate = newDate;
4034
+ setView(newDate.getFullYear(), newDate.getMonth());
4035
+ focusRoving();
4036
+ }
4037
+ function choose(y, m, d){
4038
+ selected = new Date(y, m, d);
4039
+ focusDate = new Date(selected);
4040
+ input.value = fmtLabel(selected);
4041
+ close(true);
4042
+ }
4043
+
4044
+ input.addEventListener('click', function(){ if (cal.hidden) open(); else close(true); });
4045
+ prev.addEventListener('click', function(){ focusDate = new Date(view.getFullYear(), view.getMonth() - 1, Math.min(focusDate.getDate(), 28)); setView(view.getFullYear(), view.getMonth() - 1); });
4046
+ next.addEventListener('click', function(){ focusDate = new Date(view.getFullYear(), view.getMonth() + 1, Math.min(focusDate.getDate(), 28)); setView(view.getFullYear(), view.getMonth() + 1); });
4047
+
4048
+ grid.addEventListener('click', function(e){
4049
+ var b = e.target.closest('.calendar__day');
4050
+ if (!b || b.disabled) return;
4051
+ choose(+b.dataset.y, +b.dataset.m, +b.dataset.d);
4052
+ });
4053
+ grid.addEventListener('keydown', function(e){
4054
+ var b = e.target.closest('.calendar__day');
4055
+ if (!b) return;
4056
+ var cur = new Date(+b.dataset.y, +b.dataset.m, +b.dataset.d), delta = 0;
4057
+ if (e.key === 'ArrowLeft') delta = -1;
4058
+ else if (e.key === 'ArrowRight') delta = 1;
4059
+ else if (e.key === 'ArrowUp') delta = -7;
4060
+ else if (e.key === 'ArrowDown') delta = 7;
4061
+ else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); choose(+b.dataset.y, +b.dataset.m, +b.dataset.d); return; }
4062
+ else if (e.key === 'Escape') { e.preventDefault(); close(true); return; }
4063
+ else if (e.key === 'Home') { e.preventDefault(); moveFocus(new Date(cur.getFullYear(), cur.getMonth(), cur.getDate() - ((cur.getDay()+6)%7))); return; }
4064
+ else if (e.key === 'End') { e.preventDefault(); moveFocus(new Date(cur.getFullYear(), cur.getMonth(), cur.getDate() + (6 - ((cur.getDay()+6)%7)))); return; }
4065
+ else return;
4066
+ e.preventDefault();
4067
+ moveFocus(new Date(cur.getFullYear(), cur.getMonth(), cur.getDate() + delta));
4068
+ });
4069
+ document.addEventListener('click', function(e){
4070
+ if (!cal.hidden && !wrap.contains(e.target)) close(false);
4071
+ });
4072
+
4073
+ render(); // estado inicial coherente aunque el panel esté oculto
4074
+ })();
4075
+
4076
+ /* ---------- FileUpload (dragover + quitar ítem) ---------- */
4077
+ (function(){
4078
+ var fu = document.getElementById('fileUpload');
4079
+ var list = document.getElementById('fuList');
4080
+ if (!fu) return;
4081
+ var depth = 0; // cuenta enter/leave anidados para no parpadear con hijos
4082
+ fu.addEventListener('dragenter', function(e){ e.preventDefault(); depth++; fu.classList.add('is-dragover'); });
4083
+ fu.addEventListener('dragover', function(e){ e.preventDefault(); });
4084
+ fu.addEventListener('dragleave', function(){ depth--; if (depth <= 0) { depth = 0; fu.classList.remove('is-dragover'); } });
4085
+ fu.addEventListener('drop', function(e){ e.preventDefault(); depth = 0; fu.classList.remove('is-dragover'); });
4086
+ if (list) {
4087
+ list.addEventListener('click', function(e){
4088
+ var btn = e.target.closest('.file-upload__remove');
4089
+ if (!btn) return;
4090
+ var item = btn.closest('.file-upload__item');
4091
+ if (item) item.parentNode.removeChild(item);
4092
+ });
4093
+ }
4094
+ })();
2711
4095
  })();
2712
4096
  </script>
2713
4097
  </body>