@netzi/tokens 0.1.0-beta.8

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.
@@ -0,0 +1,665 @@
1
+ /* ===========================================================
2
+ @netzi/tokens — Design tokens (CSS custom properties)
3
+
4
+ Ejes de tema (los aplica NetziThemeService sobre <html>):
5
+ [data-theme=dark] → tema OSCURO (default: claro)
6
+ [data-preset=...] → color primario (blue|green|purple|orange|red|cyan)
7
+ [data-contrast=high] → más contraste (bordes/texto)
8
+ [data-glass=on] → capas flotantes de cristal (translúcidas)
9
+ [data-density=compact] → controles más compactos
10
+ dir=rtl → de derecha a izquierda (CSS usa props lógicas)
11
+ --netzi-font-sans inline → familia tipográfica
12
+
13
+ El PRIMARIO (`--netzi-primary*`) es themeable por preset. Los colores de
14
+ marca/semánticos (blue/green/purple/red/yellow) son constantes y se usan para
15
+ estados fijos, que además tienen su alias por lo que significan
16
+ (`--netzi-info`, `--netzi-success`, `--netzi-warning`, `--netzi-danger`).
17
+
18
+ OVERRIDE (white-label por app): redefine cualquier token en tu propio scope.
19
+ Las actualizaciones de este paquete cambian SOLO los defaults.
20
+ =========================================================== */
21
+
22
+ :root {
23
+ /* --- Acentos de marca / semánticos (constantes) --- */
24
+ --netzi-blue: #0ba5ff;
25
+ --netzi-blue-dark: #0476c2;
26
+ --netzi-green: #0ae972;
27
+ --netzi-purple: #8c4cff;
28
+ --netzi-red: #ff6b6b;
29
+ --netzi-yellow: #f0c040;
30
+
31
+ /* --- Los mismos colores, pero PARA TEXTO ---
32
+ Los de arriba son de RELLENO: saturados para que un fondo, un punto de
33
+ estado o una barra de gráfica se vean. Sobre una superficie clara, como
34
+ texto, no llegan ni de lejos al mínimo (el verde daba 1.63:1). Estos son su
35
+ versión legible, oscurecida en claro y aclarada en oscuro solo donde hace
36
+ falta. Si lo que pintas se lee, va el `-text`; si es relleno, el de arriba. */
37
+ --netzi-blue-text: #076aa3;
38
+ --netzi-green-text: #05773a;
39
+ --netzi-yellow-text: #7d6421;
40
+ --netzi-red-text: #a34444;
41
+ --netzi-purple-text: #743fd4;
42
+
43
+ /* --- Alias SEMÁNTICOS de estado ---
44
+ El mismo color con el nombre de lo que significa. Existen porque toda app
45
+ los da por hechos, y un token que no existe **muere en silencio**: la
46
+ declaración entera se descarta (sin fondo, sin color, sin radio) sin un
47
+ aviso en build ni en consola. Se declaran como referencia, no como copia,
48
+ para que sigan al color aunque este cambie. Mismo reparto de papeles que
49
+ arriba: el crudo es RELLENO, el `-text` es lo que se lee. */
50
+ --netzi-info: var(--netzi-blue);
51
+ --netzi-success: var(--netzi-green);
52
+ --netzi-warning: var(--netzi-yellow);
53
+ --netzi-danger: var(--netzi-red);
54
+ --netzi-info-text: var(--netzi-blue-text);
55
+ --netzi-success-text: var(--netzi-green-text);
56
+ --netzi-warning-text: var(--netzi-yellow-text);
57
+ --netzi-danger-text: var(--netzi-red-text);
58
+
59
+ /* --- Alias por SIMETRÍA ---
60
+ El primer escalón de cada escala no lleva número (`--netzi-surface` y
61
+ `--netzi-surface-2`), y quien ve el `-2` escribe el `-1`. Igual con el radio
62
+ medio, que se llama `--netzi-radius` a secas. Son el mismo valor con el
63
+ nombre que la app espera, por el mismo motivo que los de arriba. */
64
+ --netzi-surface-1: var(--netzi-surface);
65
+ --netzi-text-1: var(--netzi-text);
66
+ --netzi-radius-md: var(--netzi-radius);
67
+
68
+ /* --- Color PRIMARIO (themeable por preset; default = azul) --- */
69
+ --netzi-primary: var(--netzi-blue);
70
+ --netzi-primary-dark: var(--netzi-blue-dark);
71
+ /* La TINTA sobre el relleno del primario. Cada preset usa la que cumple sobre
72
+ SU color, no una fija: el blanco sobre el azul da 2.68:1 y la tinta oscura
73
+ 7.05:1. Verde, naranja y cyan ya venían así; el azul y el rojo se suman. */
74
+ --netzi-on-primary: #04121f;
75
+ /* El primario PARA TEXTO (enlaces, ítem activo del menú, cifras destacadas).
76
+ Cada preset lo redefine: el crudo no llega a 4.5:1 sobre superficie clara. */
77
+ --netzi-primary-text: #076ba6;
78
+ /* Lo que va ENCIMA de `--netzi-primary-dark`: el pulgar del interruptor, la
79
+ marca del checkbox, el punto del radio. Es blanco en TODOS los presets: el
80
+ primario oscuro es lo bastante oscuro para que el blanco pase de 3:1 (el
81
+ mínimo de un control) en cada uno —3.2 en naranja, 3.3 en verde, 4.7+ en
82
+ azul, rojo y morado—; lo verifica `tokens-contrast.spec.ts`. `--netzi-on-primary`
83
+ no sirve para esto: sobre el azul claro tiene que ser casi negro, y un pulgar
84
+ negro sobre un interruptor azul se ve raro (queja literal del usuario). */
85
+ --netzi-on-primary-dark: #ffffff;
86
+ /* El pulgar de un interruptor o de un deslizador. OPACO y literal a propósito:
87
+ iba con `--netzi-surface`, y dentro de un panel de cristal esa superficie es
88
+ un tinte translúcido (re-escopada por el panel), así que el pulgar salía como
89
+ un agujero oscuro con un aro. Blanco en los dos temas, como el de cualquier
90
+ sistema: sobre la pista apagada (`surface-3`) y sobre la encendida
91
+ (`primary-dark`) se ve igual de bien. */
92
+ --netzi-thumb: #ffffff;
93
+ /* Tinte del primario, derivado automáticamente (cambia con el preset). */
94
+ --netzi-primary-tint: color-mix(in srgb, var(--netzi-primary) 10%, transparent);
95
+ /* El mismo tinte, MARCADO. El del 10 % está calibrado para superficies
96
+ grandes (una fila resaltada, un panel); en una pieza pequeña —un chip, una
97
+ píldora— se pierde contra el fondo: sobre `surface-3` da 1.13:1 y no se
98
+ distingue. Este llega a 1.32:1, que sí se ve.
99
+ ⚠️ Su tinta es `--netzi-text` (≥9:1 con cualquier preset). NO vale
100
+ `--netzi-primary-text` encima: sobre este tinte se queda en 3.9:1, y por eso
101
+ el tinte suave no se puede subir sin más — es el techo del otro. */
102
+ --netzi-primary-tint-strong: color-mix(in srgb, var(--netzi-primary) 22%, transparent);
103
+
104
+ /* --- Tema CLARO: superficies --- */
105
+ --netzi-bg: #f7f9fb;
106
+ --netzi-surface: #ffffff;
107
+ --netzi-surface-2: #f4f6f8;
108
+ --netzi-surface-3: #eef1f4;
109
+
110
+ /* --- Tema CLARO: bordes --- */
111
+ --netzi-border: #eaedf0;
112
+ --netzi-border-2: #dce0e5;
113
+
114
+ /* --- Capas FLOTANTES ---
115
+ Las que se pintan ENCIMA del contenido: topbar, sidebar, paneles anclados,
116
+ modal, drawer, menús, tooltips y toasts. Por defecto son exactamente la
117
+ superficie opaca de siempre; el eje [data-glass=on] las convierte en
118
+ cristal sin que ningún componente tenga que enterarse (ver GLASS abajo).
119
+ Los valores van literales, no como `var(--netzi-surface)`, porque el modo
120
+ cristal los redefine y una referencia cruzada crearía un ciclo. */
121
+ --netzi-float-bg: #ffffff;
122
+ --netzi-float-border: #eaedf0;
123
+ --netzi-float-border-2: #dce0e5;
124
+ /* Superficies ANIDADAS dentro de una capa flotante (una tarjeta en el drawer,
125
+ la burbuja de un comentario en el popover, el hover de un botón). Sin
126
+ cristal son las superficies de siempre; con cristal, tintes translúcidos
127
+ (ver el bloque de cristal). Los paneles las re-escopan como
128
+ `--netzi-surface*` para que TODO lo que llevan dentro las herede. */
129
+ --netzi-float-surface: #ffffff;
130
+ --netzi-float-surface-2: #f4f6f8;
131
+ --netzi-float-surface-3: #eef1f4;
132
+ --netzi-float-filter: none;
133
+ /* La MISMA cadena que `--netzi-float-filter`, en identidad. Es el punto del
134
+ que PARTE la entrada de un panel de cristal y al que llega su salida: el
135
+ desenfoque se anima de 0 a su valor (un tirón de foco), porque un fundido
136
+ por `opacity` no vale — con opacidad < 1 el elemento (o su capa) deja de
137
+ ver la página y el cristal se apaga justo mientras entra. Mismo orden de
138
+ funciones que el token real: si no coinciden, no se interpola. */
139
+ --netzi-float-filter-identity: saturate(1) contrast(1) brightness(1) blur(0);
140
+ --netzi-float-glow: 0 0 rgba(0, 0, 0, 0);
141
+
142
+ /* --- VELO de las capas modales ---
143
+ Lo que se pinta DETRÁS de un modal, un drawer o la paleta ⌘K para apagar el
144
+ contenido. Es un token porque cada componente lo llevaba escrito a mano y no
145
+ coincidían: modal, drawer y ⌘K iban a `rgba(8,12,20,.55)` con `blur(2px)` y
146
+ `connection-guard` a `.62` con `blur(3px)`, así que la misma app se oscurecía
147
+ distinto según qué capa la tapara. */
148
+ --netzi-scrim: rgba(8, 12, 20, 0.28);
149
+ --netzi-scrim-filter: blur(1px);
150
+
151
+ /* --- Tema CLARO: texto ---
152
+ Tres niveles con papeles DISTINTOS, no tres grados de gris intercambiables
153
+ (ver la tabla de la sección TEXTO al final del archivo):
154
+ text → texto principal.
155
+ text-2 → CUALQUIER texto secundario legible (rótulos, ayudas, pies,
156
+ metadatos, placeholders). Es el suelo de 4.5:1 para texto.
157
+ text-3 → NO textual: iconos decorativos, separadores, chevrons, líneas y
158
+ estados deshabilitados. Cumple 3:1 (SC 1.4.11), no 4.5:1. */
159
+ --netzi-text: #1c252e;
160
+ --netzi-text-2: #5c6b78;
161
+ --netzi-text-3: #7a8996;
162
+ /* Tinta sobre un relleno de color de marca (verde, rojo, amarillo…): todos son
163
+ claros, así que la oscura es la que cumple. El blanco daba 2.78:1 sobre el
164
+ rojo del contador de notificaciones. */
165
+ --netzi-on-accent: #04121f;
166
+
167
+ /* --- Tema CLARO: sombras --- */
168
+ --netzi-shadow-sm: 0 1px 2px rgba(145, 158, 171, 0.16);
169
+ --netzi-shadow: 0 0 2px rgba(145, 158, 171, 0.2), 0 12px 24px -4px rgba(145, 158, 171, 0.12);
170
+ --netzi-shadow-lg: 0 0 2px rgba(145, 158, 171, 0.2), 0 16px 40px -8px rgba(145, 158, 171, 0.24);
171
+
172
+ /* --- Tinte de acento (alias histórico del tinte del primario) --- */
173
+ --netzi-tint: var(--netzi-primary-tint);
174
+
175
+ /* --- Forma --- */
176
+ --netzi-radius-sm: 6px;
177
+ --netzi-radius: 8px;
178
+ --netzi-radius-lg: 10px;
179
+
180
+ /* --- Espaciado ---
181
+ Escala de gaps/paddings de layout (base 4px). Usa estos tokens en la app en
182
+ vez de píxeles/rem sueltos para mantener el ritmo del sistema. La densidad
183
+ compacta reduce los pasos medios/grandes. */
184
+ --netzi-space-0: 0;
185
+ --netzi-space-1: 2px;
186
+ --netzi-space-2: 4px;
187
+ --netzi-space-3: 8px;
188
+ --netzi-space-4: 12px;
189
+ --netzi-space-5: 16px;
190
+ --netzi-space-6: 24px;
191
+ --netzi-space-7: 32px;
192
+ --netzi-space-8: 48px;
193
+
194
+ /* --- Densidad de controles (la cambia [data-density=compact]) --- */
195
+ --netzi-control-py: 12px;
196
+ --netzi-control-px: 14px;
197
+
198
+ /* --- Tamaño estándar de controles (sm/md/lg) ---
199
+ Altura FIJA por tamaño para que todos los inputs de valor y el botón midan
200
+ exactamente lo mismo y alineen al ponerlos en fila. Escalan con
201
+ `--netzi-font-scale` (ajuste de accesibilidad) para no recortar el texto.
202
+ La densidad compacta las reduce. */
203
+ --netzi-control-h-sm: calc(36px * var(--netzi-font-scale, 1));
204
+ --netzi-control-h-md: calc(44px * var(--netzi-font-scale, 1));
205
+ --netzi-control-h-lg: calc(52px * var(--netzi-font-scale, 1));
206
+ --netzi-control-px-sm: 12px;
207
+ --netzi-control-px-md: 14px;
208
+ --netzi-control-px-lg: 16px;
209
+ --netzi-control-fs-sm: calc(13.5px * var(--netzi-font-scale, 1));
210
+ --netzi-control-fs-md: calc(15px * var(--netzi-font-scale, 1));
211
+ --netzi-control-fs-lg: calc(16px * var(--netzi-font-scale, 1));
212
+
213
+ /* --- Tipografía --- */
214
+ --netzi-font-sans: 'Plus Jakarta Sans', -apple-system, system-ui, sans-serif;
215
+ --netzi-font-mono: 'Fira Code', ui-monospace, monospace;
216
+
217
+ /* Escala global del tamaño de letra (1 = 100%). Los componentes calculan
218
+ sus tamaños como calc(Npx * escala); la cambia el panel de ajustes. */
219
+ --netzi-font-scale: 1;
220
+
221
+ /* --- Escala tipográfica (#51) ---
222
+ El texto que NO es un control (una línea secundaria bajo un título, un pie
223
+ de tarjeta, un metadato, un título de página) no tenía token: cada app se
224
+ inventaba la suya y `var(--netzi-font-size-xs)` —que parece existir— se
225
+ descartaba en silencio. Los pasos salen de lo que el kit ya usa (picos en
226
+ 11 · 12.5 · 14 · 16 · 18 · 22 · 28), con ratio ≈1.15 y **md = 14px**, que es
227
+ el 100 % de `--netzi-font-scale` (el panel de ajustes divide entre 14).
228
+ Todos escalan con el ajuste de accesibilidad, como los controles. Los
229
+ tamaños de los controles (`--netzi-control-fs-*`) son otra familia: van
230
+ ligados a la altura del control, no a esta escala. La densidad compacta no
231
+ los toca: el texto no se encoge, se encoge el aire. */
232
+ --netzi-font-size-xs: calc(11px * var(--netzi-font-scale, 1));
233
+ --netzi-font-size-sm: calc(12.5px * var(--netzi-font-scale, 1));
234
+ --netzi-font-size-md: calc(14px * var(--netzi-font-scale, 1));
235
+ --netzi-font-size-lg: calc(16px * var(--netzi-font-scale, 1));
236
+ --netzi-font-size-xl: calc(18px * var(--netzi-font-scale, 1));
237
+ --netzi-font-size-2xl: calc(22px * var(--netzi-font-scale, 1));
238
+ --netzi-font-size-3xl: calc(28px * var(--netzi-font-scale, 1));
239
+
240
+ /* Interlineado, sin unidad (se multiplica por el tamaño de cada elemento).
241
+ `none` para lo que mide una línea (controles, chips); `tight` para
242
+ títulos; `normal` para texto corriente; `relaxed` para lectura larga. */
243
+ --netzi-line-height-none: 1;
244
+ --netzi-line-height-tight: 1.25;
245
+ --netzi-line-height-normal: 1.5;
246
+ --netzi-line-height-relaxed: 1.65;
247
+
248
+ /* Peso. Los cinco que usa el kit; la fuente cargada (`fonts.css`) trae
249
+ exactamente estos cortes, otro valor se sintetiza y se ve peor. */
250
+ --netzi-font-weight-regular: 400;
251
+ --netzi-font-weight-medium: 500;
252
+ --netzi-font-weight-semibold: 600;
253
+ --netzi-font-weight-bold: 700;
254
+ --netzi-font-weight-extrabold: 800;
255
+ }
256
+
257
+ [data-theme='dark'] {
258
+ /* --- Tema OSCURO: superficies --- */
259
+ --netzi-bg: #0d1117;
260
+ --netzi-surface: #161b22;
261
+ --netzi-surface-2: #0d1117;
262
+ --netzi-surface-3: #21262d;
263
+
264
+ /* --- Tema OSCURO: bordes --- */
265
+ --netzi-border: #21262d;
266
+ --netzi-border-2: #30363d;
267
+
268
+ /* --- Tema OSCURO: capas flotantes --- */
269
+ --netzi-float-bg: #161b22;
270
+ --netzi-float-border: #21262d;
271
+ --netzi-float-border-2: #30363d;
272
+ --netzi-float-surface: #161b22;
273
+ --netzi-float-surface-2: #0d1117;
274
+ --netzi-float-surface-3: #21262d;
275
+
276
+ /* --- Tema OSCURO: los semánticos como texto ---
277
+ Sobre fondo oscuro casi todos sirven crudos; solo el rojo y el morado piden
278
+ un punto de luz para llegar al mínimo. */
279
+ --netzi-blue-text: #0ba5ff;
280
+ --netzi-green-text: #0ae972;
281
+ --netzi-yellow-text: #f0c040;
282
+ --netzi-red-text: #ff7272;
283
+ --netzi-purple-text: #ac7eff;
284
+ --netzi-primary-text: #0da6ff;
285
+
286
+ /* --- Tema OSCURO: texto (mismos papeles que en claro) --- */
287
+ --netzi-text: #ffffff;
288
+ --netzi-text-2: #8b949e;
289
+ --netzi-text-3: #6e7681;
290
+ --netzi-on-accent: #04121f;
291
+
292
+ /* --- Tema OSCURO: sombras --- */
293
+ --netzi-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
294
+ --netzi-shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 12px 28px -6px rgba(0, 0, 0, 0.45);
295
+ --netzi-shadow-lg: 0 1px 2px rgba(0, 0, 0, 0.4), 0 20px 50px -10px rgba(0, 0, 0, 0.6);
296
+
297
+ /* El tinte sube un poco de alfa en oscuro. */
298
+ --netzi-primary-tint: color-mix(in srgb, var(--netzi-primary) 15%, transparent);
299
+ --netzi-primary-tint-strong: color-mix(in srgb, var(--netzi-primary) 28%, transparent);
300
+ }
301
+
302
+ /* ===========================================================
303
+ PRESETS de color primario
304
+ =========================================================== */
305
+ [data-preset='green'] {
306
+ --netzi-primary: #0ae972;
307
+ --netzi-primary-dark: #07a352;
308
+ --netzi-on-primary: #04121f;
309
+ --netzi-primary-text: #05793b;
310
+ }
311
+ /* El morado es el ÚNICO preset donde la tinta oscura no cumple (4.13): se queda
312
+ en blanco, que sí (4.57). La regla es «la que cumpla sobre su color». */
313
+ [data-preset='purple'] {
314
+ --netzi-primary: #8c4cff;
315
+ --netzi-primary-dark: #6a2fd6;
316
+ --netzi-on-primary: #ffffff;
317
+ --netzi-primary-text: #7841db;
318
+ }
319
+ [data-preset='orange'] {
320
+ --netzi-primary: #f0a020;
321
+ --netzi-primary-dark: #c97e0a;
322
+ --netzi-on-primary: #04121f;
323
+ --netzi-primary-text: #8e5e13;
324
+ }
325
+ [data-preset='red'] {
326
+ --netzi-primary: #ff5630;
327
+ --netzi-primary-dark: #d63916;
328
+ --netzi-on-primary: #04121f;
329
+ --netzi-primary-text: #b53d22;
330
+ }
331
+ [data-preset='cyan'] {
332
+ --netzi-primary: #00b8d9;
333
+ --netzi-primary-dark: #0892ab;
334
+ --netzi-on-primary: #04121f;
335
+ --netzi-primary-text: #007287;
336
+ }
337
+
338
+ /* El primario para TEXTO en oscuro: los saturados ya sirven, salvo dos. */
339
+ [data-theme='dark'][data-preset='green'] {
340
+ --netzi-primary-text: #0ae972;
341
+ }
342
+ [data-theme='dark'][data-preset='purple'] {
343
+ --netzi-primary-text: #ad80ff;
344
+ /* El morado oscuro de claro (#6a2fd6) se hunde en el fondo oscuro (2.7:1): un
345
+ interruptor encendido apenas se distinguía de la tarjeta. */
346
+ --netzi-primary-dark: #7d48f2;
347
+ }
348
+ [data-theme='dark'][data-preset='orange'] {
349
+ --netzi-primary-text: #f0a020;
350
+ }
351
+ [data-theme='dark'][data-preset='red'] {
352
+ --netzi-primary-text: #ff6a49;
353
+ }
354
+ [data-theme='dark'][data-preset='cyan'] {
355
+ --netzi-primary-text: #00b8d9;
356
+ }
357
+
358
+ /* ===========================================================
359
+ CONTRASTE alto: bordes y texto más marcados
360
+ =========================================================== */
361
+ /* Cada nivel de texto toma el valor del nivel de encima: text-2 sube al tono
362
+ reforzado y text-3 hereda el text-2 por defecto. Así `text-3` pasa de 3:1 a
363
+ más de 4.5:1 y el modo cumple incluso donde alguien lo haya usado para texto,
364
+ que es justo lo que se espera de un modo de alto contraste. */
365
+ [data-contrast='high'] {
366
+ --netzi-border: #c9cfd6;
367
+ --netzi-border-2: #aeb6bf;
368
+ --netzi-text-2: #2a333c;
369
+ --netzi-text-3: #5c6b78;
370
+ --netzi-float-border: #c9cfd6;
371
+ --netzi-float-border-2: #aeb6bf;
372
+ --netzi-shadow-sm: 0 1px 2px rgba(99, 115, 129, 0.3);
373
+ }
374
+
375
+ [data-theme='dark'][data-contrast='high'] {
376
+ --netzi-border: #3a434d;
377
+ --netzi-border-2: #525c66;
378
+ --netzi-text-2: #aab4bf;
379
+ --netzi-text-3: #8b949e;
380
+ --netzi-float-border: #3a434d;
381
+ --netzi-float-border-2: #525c66;
382
+ }
383
+
384
+ /* ===========================================================
385
+ GLASS: las capas flotantes, de cristal
386
+
387
+ Translúcidas, con el fondo desenfocado y saturado por detrás. Se activa con
388
+ `data-glass="on"` en <html> (lo pone NetziThemeService), y solo afecta a las
389
+ capas que FLOTAN sobre el contenido: las superficies de contenido —tarjetas,
390
+ secciones, tablas— siguen opacas, porque ahí el cristal solo resta.
391
+
392
+ Sobre cristal el texto NO se lee contra una superficie fija: se lee contra lo
393
+ que haya debajo, que cambia al scrollear. El peor caso es "negro puro debajo"
394
+ en claro y "blanco puro debajo" en oscuro, y es ESE el que tiene que cumplir
395
+ 4.5:1. Hay dos formas de conseguirlo y solo una deja que esto parezca cristal:
396
+
397
+ - Subir el alfa hasta tapar el fondo. Cumple, pero con 0.82 ya es
398
+ prácticamente una superficie opaca: se pierde el efecto entero.
399
+ - **Acotar el fondo con el propio filtro**: `contrast()` comprime su rango
400
+ hacia el medio y `brightness()` lo empuja hacia el tono de la capa (arriba
401
+ en claro, abajo en oscuro). Al entrar ya acotado, un alfa MUCHO más bajo
402
+ basta para el mismo suelo — y con menos alfa se ve lo de detrás, que es
403
+ justo de lo que va el efecto.
404
+
405
+ Por eso el filtro no es solo `blur`: cada función tiene un papel.
406
+ saturate(180%) el color de debajo se aviva al difuminarse (lo vistoso).
407
+ contrast(0.6) comprime el rango del fondo hacia el gris medio.
408
+ brightness() lo empuja al tono de la capa: >1 en claro, <1 en oscuro.
409
+ blur(28px) lo difumina; cuanto más ancho, más "vidrio esmerilado".
410
+
411
+ Medido con esa cadena: en los dos temas el plano
412
+ contrario puro debajo (negro en claro, blanco en oscuro) es el único caso
413
+ donde el texto secundario cede a ~4.1-4.2:1 con el primario ≥ 4.5; contra
414
+ el fondo del kit y contra un gris medio, ambos pasan de 6:1. Sin la compresión del filtro haría falta alfa 0.82 para
415
+ el mismo suelo, y a 0.82 esto ya no parece cristal. Lo verifica
416
+ `tokens-contrast.spec.ts`, que modela la cadena entera —no solo el alfa—, así
417
+ que tocar cualquiera de las cuatro funciones o el alfa mueve el veredicto.
418
+ =========================================================== */
419
+ [data-glass='on'] {
420
+ --netzi-float-bg: rgba(255, 255, 255, 0.44);
421
+ --netzi-float-border: rgba(255, 255, 255, 0.7);
422
+ --netzi-float-border-2: rgba(255, 255, 255, 0.85);
423
+ /* Lo anidado en un panel de cristal también es vidrio: una tarjeta opaca
424
+ dentro de un drawer translúcido era exactamente la queja («hay elementos
425
+ que no se ponen translúcidos»). Los tres son tintes OSCUROS que se alejan
426
+ de la lámina un paso, dos y tres, a LA MISMA distancia que las superficies
427
+ opacas que imitan: `#f4f6f8` es un 4,5 % de tinta sobre blanco y `#eef1f4`
428
+ un 7,5 %. Más fuertes (se probó al 9 y al 13 %) las tarjetas de un drawer
429
+ salían como placas grises, como deshabilitadas. Un tinte blanco no vale: la
430
+ lámina clara ya es casi blanca y la burbuja de un comentario desaparecía
431
+ sobre ella. Oscurecer resta contraste al texto, y por eso `--netzi-text-2`
432
+ baja aquí dos niveles; el guardián comprueba que sobre el tinte más fuerte
433
+ sigue por encima de 4.5:1 con negro puro debajo. */
434
+ --netzi-float-surface: rgba(28, 37, 46, 0.02);
435
+ --netzi-float-surface-2: rgba(28, 37, 46, 0.045);
436
+ --netzi-float-surface-3: rgba(28, 37, 46, 0.075);
437
+ /* Alfa 0.44, brillo 1.25 y solo 10 px de desenfoque, para que se vea lo que
438
+ hay debajo (el usuario eligió este punto tras probar cinco niveles, del más
439
+ denso al más abierto; se quedó el más abierto). El brillo bajó
440
+ de 1.5 a 1.25 porque a 1.5 todo lo de debajo se lavaba a blanco y en claro el
441
+ cristal «no se notaba». El precio, como en oscuro, lo paga el caso extremo:
442
+ con un plano NEGRO debajo el texto secundario (#2a333c) queda en 4.2:1 (el
443
+ primario en 5.1); sobre la tinta del kit, 5.1; contra el fondo del kit y
444
+ contra un gris medio, ambos pasan de 7:1. */
445
+ --netzi-float-filter: saturate(200%) contrast(0.6) brightness(1.25) blur(10px);
446
+ /* El texto sube un nivel con el cristal, en TODA la página y no solo dentro de
447
+ las capas. Va aquí, en el mismo elemento que `data-theme`, y con literales:
448
+ declararlo en cada capa como `var(--netzi-float-text-2)` NO se reinvalidaba
449
+ al cambiar de tema en caliente y el sidebar se quedaba con el color del tema
450
+ anterior (medido: 3.08:1 tras pulsar el interruptor). En una superficie
451
+ opaca el refuerzo tampoco estorba: sube de 4.84 a 7.94. */
452
+ --netzi-text-2: #2a333c;
453
+ --netzi-text-3: #5c6b78;
454
+ /* --- LENTE del canto (opt-in: `data-glass-lens="on"`) ---
455
+ Un anillo con su PROPIO desenfoque, más fuerte que el del interior, para que
456
+ el canto parezca tener espesor. No es refracción de verdad —el fondo no se
457
+ curva— porque eso hoy no se puede: Chromium acepta
458
+ `backdrop-filter: url(#filtro-svg)` y luego lo IGNORA (comprobado: el mismo
459
+ filtro como `filter` normal sí deforma), y Safari ni lo acepta.
460
+ La cadena sale de destripar una implementación que sí convence (tonnitools):
461
+ lo que parece refracción es `invert()` + `contrast()` alto en un anillo
462
+ ESTRECHO del canto. Ellos apilan siete capas de `backdrop-filter` para
463
+ conseguirlo; aquí va una sola, porque cada capa cuesta frames.
464
+ ⚠️ Cuesta caro: es una segunda capa de `backdrop-filter`. Con una sola capa
465
+ en pantalla sale gratis (60 fps medidos); apilada con el shell cae a ~26.
466
+ Por eso es opt-in y por eso el shell se apaga cuando hay una capa encima. */
467
+ --netzi-float-lens: blur(18px) invert(0.18) brightness(1.1) saturate(1.3) hue-rotate(-10deg)
468
+ contrast(2);
469
+ --netzi-float-lens-size: 5px;
470
+
471
+ /* Filo especular, con GROSOR: luz en el canto de arriba y sombra dentro del de
472
+ abajo. Un solo filo de luz se lee como un velo iluminado; los dos juntos dan
473
+ el canto de una pieza de vidrio, que es la diferencia entre «translúcido» y
474
+ «cristal». No toca ni el alfa ni el filtro, así que no mueve el contraste. */
475
+ --netzi-float-glow:
476
+ inset 0 1px 0 rgba(255, 255, 255, 0.75), inset 0 -1px 0 rgba(11, 22, 34, 0.07);
477
+ }
478
+
479
+ [data-theme='dark'][data-glass='on'] {
480
+ --netzi-float-bg: rgba(22, 27, 34, 0.12);
481
+ --netzi-float-border: rgba(255, 255, 255, 0.14);
482
+ --netzi-float-border-2: rgba(255, 255, 255, 0.22);
483
+ /* En oscuro los tintes van hacia el negro, que aquí además MEJORA el
484
+ contraste del texto claro. */
485
+ --netzi-float-surface: rgba(0, 0, 0, 0.12);
486
+ --netzi-float-surface-2: rgba(0, 0, 0, 0.22);
487
+ --netzi-float-surface-3: rgba(0, 0, 0, 0.32);
488
+ /* Alfa 0.12, brillo 0.56 y 10 px de desenfoque (el usuario eligió este punto
489
+ tras probar cinco niveles; se quedó el más abierto). El alfa es lo que deja
490
+ pasar el COLOR de debajo; el brillo lo topa el contraste: el texto primario
491
+ tiene que quedar en 4.5:1 con un plano BLANCO debajo (5.1 aquí) y el
492
+ secundario, subido dos niveles (#d8dee6), en 3:1 (4.1). Sobre el fondo del
493
+ kit y sobre un gris medio (una tarjeta clara o una foto) los tres siguen
494
+ holgados. Los números los fija `tokens-contrast.spec.ts`. */
495
+ --netzi-float-filter: saturate(200%) contrast(0.6) brightness(0.56) blur(10px);
496
+ --netzi-text-2: #d8dee6;
497
+ --netzi-text-3: #aab4bf;
498
+ /* En oscuro la sombra del canto inferior es más marcada: sobre fondo oscuro una
499
+ línea negra se pierde, y sin ella el canto vuelve a no tener grosor. */
500
+ --netzi-float-glow: inset 0 1px 0 rgba(255, 255, 255, 0.12), inset 0 -1px 0 rgba(0, 0, 0, 0.35);
501
+ /* El velo también se hunde en oscuro: el mismo alfa sobre un fondo ya oscuro
502
+ apenas separa la capa del contenido. */
503
+ --netzi-scrim: rgba(2, 5, 10, 0.45);
504
+ /* En oscuro se invierte menos: sobre fondo oscuro, invertir de más convierte el
505
+ canto en un halo blanco. */
506
+ --netzi-float-lens: blur(18px) invert(0.12) brightness(1.05) saturate(1.3) hue-rotate(-10deg)
507
+ contrast(1.7);
508
+ }
509
+
510
+ /* ===========================================================
511
+ FONDO AMBIENTAL (`data-ambient="on"`, lo pone `NetziThemeService`)
512
+ Manchas de color suaves DEBAJO del contenido, que el layout pinta bajo
513
+ `--netzi-bg`. Es lo que le da al cristal algo que desenfocar: sobre un fondo
514
+ liso, un panel translúcido se ve como una placa gris; sobre esto, como
515
+ vidrio. Va con los colores del preset (`--netzi-primary`) y dos de marca.
516
+ Los porcentajes son bajos a propósito: el texto se sigue leyendo encima
517
+ (medido: `--netzi-text-3` sobre la mancha más fuerte, 4.4:1 en claro y 4.6:1
518
+ en oscuro, por encima del 3:1 que exige). En alto contraste se apaga.
519
+ =========================================================== */
520
+ :root {
521
+ --netzi-ambient: none;
522
+ }
523
+
524
+ [data-ambient='on'] {
525
+ --netzi-ambient:
526
+ radial-gradient(
527
+ 55% 45% at 8% 0%,
528
+ color-mix(in srgb, var(--netzi-primary) 16%, transparent),
529
+ transparent 70%
530
+ ),
531
+ radial-gradient(
532
+ 45% 40% at 100% 100%,
533
+ color-mix(in srgb, var(--netzi-purple) 12%, transparent),
534
+ transparent 70%
535
+ ),
536
+ radial-gradient(
537
+ 35% 30% at 70% 10%,
538
+ color-mix(in srgb, var(--netzi-green) 8%, transparent),
539
+ transparent 70%
540
+ );
541
+ }
542
+
543
+ [data-theme='dark'][data-ambient='on'] {
544
+ --netzi-ambient:
545
+ radial-gradient(
546
+ 55% 45% at 8% 0%,
547
+ color-mix(in srgb, var(--netzi-primary) 22%, transparent),
548
+ transparent 70%
549
+ ),
550
+ radial-gradient(
551
+ 45% 40% at 100% 100%,
552
+ color-mix(in srgb, var(--netzi-purple) 18%, transparent),
553
+ transparent 70%
554
+ ),
555
+ radial-gradient(
556
+ 35% 30% at 70% 10%,
557
+ color-mix(in srgb, var(--netzi-green) 10%, transparent),
558
+ transparent 70%
559
+ );
560
+ }
561
+
562
+ /* En alto contraste no hay manchas: el fondo vuelve a ser liso. */
563
+ [data-contrast='high'][data-ambient='on'] {
564
+ --netzi-ambient: none;
565
+ }
566
+
567
+ /* Quien pide menos transparencia recibe la superficie opaca de siempre, igual
568
+ que el kit ya respeta `prefers-reduced-motion` en las animaciones. */
569
+ @media (prefers-reduced-transparency: reduce) {
570
+ [data-glass='on'] {
571
+ --netzi-float-bg: #ffffff;
572
+ --netzi-float-border: #eaedf0;
573
+ --netzi-float-border-2: #dce0e5;
574
+ --netzi-float-surface: #ffffff;
575
+ --netzi-float-surface-2: #f4f6f8;
576
+ --netzi-float-surface-3: #eef1f4;
577
+ --netzi-float-filter: none;
578
+ --netzi-float-glow: 0 0 rgba(0, 0, 0, 0);
579
+ --netzi-scrim-filter: none;
580
+ }
581
+
582
+ [data-theme='dark'][data-glass='on'] {
583
+ --netzi-float-bg: #161b22;
584
+ --netzi-float-border: #21262d;
585
+ --netzi-float-border-2: #30363d;
586
+ --netzi-float-surface: #161b22;
587
+ --netzi-float-surface-2: #0d1117;
588
+ --netzi-float-surface-3: #21262d;
589
+ }
590
+ }
591
+
592
+ /* Sin `backdrop-filter` no hay desenfoque, y una capa translúcida sin desenfocar
593
+ lo que hay debajo es texto sobre ruido: se degrada a opaco entero. */
594
+ @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
595
+ [data-glass='on'] {
596
+ --netzi-float-bg: #ffffff;
597
+ --netzi-float-border: #eaedf0;
598
+ --netzi-float-border-2: #dce0e5;
599
+ --netzi-float-surface: #ffffff;
600
+ --netzi-float-surface-2: #f4f6f8;
601
+ --netzi-float-surface-3: #eef1f4;
602
+ }
603
+
604
+ [data-theme='dark'][data-glass='on'] {
605
+ --netzi-float-bg: #161b22;
606
+ --netzi-float-border: #21262d;
607
+ --netzi-float-border-2: #30363d;
608
+ --netzi-float-surface: #161b22;
609
+ --netzi-float-surface-2: #0d1117;
610
+ --netzi-float-surface-3: #21262d;
611
+ }
612
+ }
613
+
614
+ /* ===========================================================
615
+ DENSIDAD compacta: controles y radios más ajustados
616
+ =========================================================== */
617
+ [data-density='compact'] {
618
+ --netzi-control-py: 8px;
619
+ --netzi-control-px: 11px;
620
+ --netzi-control-h-sm: calc(32px * var(--netzi-font-scale, 1));
621
+ --netzi-control-h-md: calc(40px * var(--netzi-font-scale, 1));
622
+ --netzi-control-h-lg: calc(46px * var(--netzi-font-scale, 1));
623
+ --netzi-control-px-sm: 10px;
624
+ --netzi-control-px-md: 11px;
625
+ --netzi-control-px-lg: 13px;
626
+ --netzi-radius-sm: 5px;
627
+ --netzi-radius: 6px;
628
+ --netzi-radius-lg: 8px;
629
+ /* Espaciado más ajustado (pasos medios/grandes; los finos no cambian). */
630
+ --netzi-space-4: 10px;
631
+ --netzi-space-5: 12px;
632
+ --netzi-space-6: 20px;
633
+ --netzi-space-7: 28px;
634
+ --netzi-space-8: 40px;
635
+ }
636
+
637
+ /* ===========================================================
638
+ TEXTO: para qué sirve cada nivel y cuánto contrasta
639
+
640
+ Ratios WCAG 2.1 medidos contra las cuatro superficies del tema (bg, surface,
641
+ surface-2 y surface-3); se anota el PEOR de los cuatro, que siempre es
642
+ surface-3. Los verifica `tokens-contrast.spec.ts` en @netzi/ui, así que si
643
+ alguien cambia un valor aquí y deja de cumplir, la suite se pone roja.
644
+
645
+ Mínimos que se aplican
646
+ texto normal 4.5:1 (SC 1.4.3)
647
+ no textual 3:1 (SC 1.4.11, iconos y bordes con significado)
648
+ deshabilitado exento (SC 1.4.3 no cubre componentes inactivos)
649
+
650
+ nivel claro oscuro para qué
651
+ text #1c252e 13.69 #ffffff 15.22 texto principal
652
+ text-2 #5c6b78 4.84 #8b949e 4.95 TODO texto secundario
653
+ text-3 #7a8996 3.17 #6e7681 3.31 NO textual, ver abajo
654
+
655
+ Con alto contraste
656
+ text-2 #45525e 7.07 #aab4bf 7.24
657
+ text-3 #5c6b78 4.84 #8b949e 4.95
658
+
659
+ `text-3` NO vale para texto legible. Es el tono de lo que acompaña: chevrons,
660
+ separadores, iconos decorativos, la barra de redimensionar, el brillo del
661
+ esqueleto de carga y los estados deshabilitados. Para un rótulo, una ayuda, un
662
+ pie, un metadato o un placeholder va SIEMPRE `text-2`. La regla existe porque
663
+ con 45 archivos usándolo indistintamente el kit entero se quedó por debajo del
664
+ mínimo sin que nadie lo notara.
665
+ =========================================================== */