El arte de la pre-carga: Eliminar el 'Flash' del tema con JavaScript y CSS
Publié le 15 November 2025
Resumen
En este artículo técnico en profundidad, exploramos cómo implementar un selector de tema (light/dark) sin el desagradable efecto de parpadeo que ocurre durante la carga de la página. Analizamos en detalle el ciclo de renderizado del navegador, las causas profundas del FOUC (Flash of Unstyled Content), y proponemos una solución robusta basada en la pre-carga síncrona en el`<head>`. Esta técnica garantiza una experiencia de usuario fluida y profesional.
El Problema : Este Maldito Parpadeo de Tema
Una pesadilla para la experiencia de usuario
Imagina la escena: has pasado horas diseñando un magnífico tema oscuro para tu aplicación web. Los colores están perfectamente equilibrados, el contraste es óptimo y tus usuarios adoran esta opción. Pero hay un problema embarazoso: cada vez que se recarga la página, durante una fracción de segundo, el tema claro predeterminado se muestra antes de que el tema oscuro tome el relevo.
Este "flash" visual, aunque breve (a veces menos de 100 ms), es inmediatamente perceptible por el ojo humano y crea una experiencia desagradable. Para los usuarios que han elegido el tema oscuro por razones de comodidad visual o accesibilidad, este parpadeo incluso puede ser doloroso, particularmente en un entorno poco iluminado.
El FOUC: Un viejo problema web
Este fenómeno es una variante de lo que se conoce como "Flash of Unstyled Content" (FOUC), un problema clásico del desarrollo web que se remonta a los primeros días del CSS. El FOUC ocurre cuando el navegador muestra temporalmente contenido HTML sin sus estilos CSS aplicados, creando un destello de contenido sin estilo.
En nuestro caso específico, no hablamos de un contenido completamente sin estilo, sino más bien de unDestello del tema equivocado(FOWT) - el contenido está estilizado, pero con el tema equivocado. Esto es particularmente frustrante porque muestra que nuestra aplicación "olvida" la préférence del usuario en cada carga de página.
Impacto en la Percepción de Calidad
Este problema, aunque técnico, tiene importantes repercusiones en la percepción de la calidad de su aplicación :
Falta de pulido: El parpadeo da la impresión de una aplicación inacabada o mal optimizada. Los usuarios suelen asociar estos pequeños defectos visuales con una falta de profesionalismo general.
Ruptura de coherencia: La aplicación parece 'olvidar' las preferencias del usuario, creando una sensación de desincronización entre la interfaz y las expectativas.
Fatiga visualPara los usuarios sensibles a la luz o que sufren de migrañas, este flash luminoso puede ser más que una simple molestia estética.
Rendimiento percibidoIrónicamente, incluso si tu sitio se carga rápidamente, este parpadeo puede dar la impresión de una aplicación lenta o poco receptiva.
Análisis Técnico del Problema
Para entender cómo resolver este problema, primero hay que entender por qué ocurre. El parpadeo se produce debido a un desfase temporal entre tres eventos críticos en el ciclo de vida de una página web :
-
El parseado inicial del HTML: El navegador lee y analiza la estructura de su página
-
La aplicación de los estilos CSS: El navegador aplica las reglas CSS y calcula el renderizado visual
-
La ejecución del JavaScript: Tu código que cambia el tema se ejecuta
El problema ocurre cuando el evento n°3 (ejecución del JavaScript) llega después de que el navegador ya haya comenzado o terminado el evento n°2 (aplicación de estilos). En ese momento, el navegador ya ha tomado una decisión sobre qué tema mostrar, y tu código llega demasiado tarde para influir antes del primer renderizado.
¿Por qué un Script Clásico no es suficiente?
El Enfoque Intuitivo pero Ineficaz
El enfoque más natural para un desarrollador sería colocar un script al final de nuestro`<body>`que verifica el tema preferido del usuario y lo aplica. Este enfoque sigue las mejores prácticas tradicionales de la web que recomiendan cargar los scripts al final de la página para no bloquear el renderizado.
// À la fin de <body> - L'APPROCHE INSUFFISANTE
document.addEventListener('DOMContentLoaded', () => {
const theme = localStorage.getItem('preferred-theme');
if (theme === 'dark') {
document.documentElement.setAttribute('data-bs-theme', 'dark');
}
});
Este enfoque parece lógico a primera vista. Esperamos a que el DOM esté listo, luego aplicamos el tema. ¿Simple, no? Desafortunadamente, esta simplicidad esconde un defecto fundamental relacionado con el tiempo del ciclo de renderizado del navegador.
Entender el evento DOMContentLoaded
El evento`DOMContentLoaded`se dispara cuando el documento HTML inicial ha sido completamente cargado y analizado por el navegador,sin esperarel final de la carga de las hojas de estilo, de las imágenes y de los submarcos. Es un punto importante de entender.
Esta es la secuencia típica de los eventos:
-
El navegador comienza a descargar el HTML
-
Analiza el HTML mientras lo recibe.
-
Él descubre las etiquetas`<link>`para los CSS y comienza a descargarlos
-
Él descubre las etiquetas`<script>`y los ejecuta (según su tipo y atributos)
-
Construye el DOM (Modelo de Objetos de Documento)
-
El evento
DOMContentLoadedse dispara -
Continúa aplicando los estilos y haciendo el layout
-
El primer paint (visualización) se produce
-
El evento`load`se dispara cuando todos los recursos están cargados
¿El problema? Entre el paso 6 (DOMContentLoaded) y el paso 8 (primer paint), el navegador ya ha tomado decisiones sobre cómo mostrar la página. Si tu script de cambio de tema se ejecuta en el paso 6, ya es demasiado tarde para evitar una primera visualización con los estilos por defecto.
El problema del Render Blocking
En realidad, el momento es aún más complejo. Los navegadores modernos utilizan técnicas de optimización sofisticadas para mejorar el rendimiento percibido. Tratan de hacer el primer pintado (First Contentful Paint) lo más rápidamente posible para que el usuario vea algo en la pantalla.
Los CSS son "render-blocking" por defecto, lo que significa que el navegador espera haber descargado y analizado las hojas de estilo antes de hacer la primera pintura. Es lógico: no queremos mostrar contenido sin estilo.
Pero aquí está la trampa: cuando el navegador aplica estos estilos CSS por primera vez, lo hace basándose en el estado actual del DOM. Si el atributo`data-bs-theme`aún no está definido en la etiqueta`<html>`, el navegador aplicará los estilos predeterminados (generalmente el tema claro).
Luego, cuando tu script se ejecuta y cambia este atributo, el navegador debe:
-
Recalcular todos los estilos afectados por este cambio
-
Rehacer el layout si es necesario
-
Repintar los elementos afectados
Este proceso de recálculo y repintado es lo que causa el parpadeo visible.
Visualización del Problema
Para comprender mejor esta secuencia problemática, examinemos un diagrama de secuencia detallado:
Failed to generate image: PlantUML preprocessing failed: [From <input> (line 28) ]
@startuml
actor Utilisateur
participant Navigateur
participant "Analizador HTML" as Parser
participant "CSSOM" as CSS
participant "Script.js (fin del cuerpo)" as Script
participant "Motor de renderizado" as Render
Utilisateur -> Navigateur: Demande la page
activate Navigateur
Navigateur -> Parser: Parse le HTML
activate Parser
Parser -> CSS: Découvre et charge les CSS
activate CSS
Parser -> Parser: Construit le DOM
Parser -> Navigateur: DOM Ready (DOMContentLoaded)
deactivate Parser
CSS -> CSS: Parse les feuilles de style
CSS -> Render: CSSOM Ready
deactivate CSS
activate Render
Render -> Render: Calcule les styles avec data-bs-theme=""note right: Tema por defecto (light)
Render -> Navegador: Primer Pintura (Tema Claro)
note right: El usuario ve el tema claro
desactivar Render
^^^^^
Syntax Error? (Assumed diagram type: sequence)
@startuml
actor Utilisateur
participant Navigateur
participant "Analizador HTML" as Parser
participant "CSSOM" as CSS
participant "Script.js (fin del cuerpo)" as Script
participant "Motor de renderizado" as Render
Utilisateur -> Navigateur: Demande la page
activate Navigateur
Navigateur -> Parser: Parse le HTML
activate Parser
Parser -> CSS: Découvre et charge les CSS
activate CSS
Parser -> Parser: Construit le DOM
Parser -> Navigateur: DOM Ready (DOMContentLoaded)
deactivate Parser
CSS -> CSS: Parse les feuilles de style
CSS -> Render: CSSOM Ready
deactivate CSS
activate Render
Render -> Render: Calcule les styles avec data-bs-theme=""note right: Tema por defecto (light)
Render -> Navegador: Primer Pintura (Tema Claro)
note right: El usuario ve el tema claro
desactivar Render
Navegador -> Script: Ejecuta el script
activar Script
Script -> Script: Lee localStorage
Script -> Navegador: setAttribute('data-bs-theme', 'dark')
desactivar Script
activar Render
Render -> Render: Recalcula los estilos con data-bs-theme="dark"
note right: Thème sombre
Render -> Navigateur: Repaint (Thème Sombre)
note right: Clignotement visible
deactivate Render
deactivate Navigateur
@enduml
Este diagrama ilustra claramente el problema: el primer paint ocurre antes de que nuestro script haya tenido la oportunidad de establecer el tema correcto. El repaint subsiguiente crea el parpadeo visible.
Los intentos de solución ineficaces
Se han intentado varios enfoques para resolver este problema, pero la mayoría tiene sus propios inconvenientes:
Enfoque 1: Ocultar el contenido hasta que se cargue
body {
opacity: 0;
transition: opacity 0.3s;
}
body.loaded {
opacity: 1;
}
Este enfoque oculta todo el contenido hasta que JavaScript haya definido el tema correcto. ¿El problema? Esto retrasa artificialmente la visualización del contenido, dando la impresión de un sitio más lento. Además, si JavaScript está desactivado, el usuario no ve nada en absoluto!
Enfoque 2 : Utilizar un loader/spinner
Similar al enfoque 1, pero con un spinner de carga. Esto oculta el problema pero no mejora el rendimiento real y agrega un retraso percibido innecesario.
Enfoque 3: Predeterminado al tema oscuro
Algunos desarrolladores definen el tema oscuro como predeterminado en el CSS. Esto evita el parpadeo para los usuarios del tema oscuro, pero crea el problema inverso para los usuarios del tema claro!
Ninguno de estos enfoques es satisfactorio porque tratan el síntoma en lugar de la causa raíz del problema.
La Verdadera Solución : Actuar Más Temprano
La clave para resolver este problema es darnos cuenta de que debemos definir el atributo`data-bs-theme` antesque el navegador no comience a aplicar los estilos CSS. Esto significa que nuestro script debe ejecutarse antes en el ciclo de vida de la página, y esto es exactamente lo que vamos a explorar en la siguiente sección.
La solución : La carga temprana (Early Loading)
Principio Fundamental
La solución elegante a nuestro problema de parpadeo se basa en un principio simple pero poderoso:sincronizar el estado de la aplicación con el proceso de renderizado del navegador. En lugar de esperar a que la página se cargue para definir el tema, debemos definirlomientrasla carga, antes incluso de que se apliquen los estilos CSS.
Esta aproximación se llama "Early Loading" o "Synchronous Preloading" en el jerga del desarrollo web. La idea es ejecutar nuestra lógica de detección de tema lo más pronto posible en el ciclo de vida de la página, idealmente dentro de la etiqueta`<head>`, incluso antes de que el navegador comience a descargar los archivos CSS.
¿Por qué el <head> es el Lugar Ideal
Le `<head>`Un documento HTML se procesa secuencialmente por el navegador, de arriba hacia abajo. Cada elemento se procesa en el orden en que aparece. Esta característica es crucial para nuestra solución.
Cuando el navegador encuentra una etiqueta`<script>`en el`<head>`sin los atributos`async` ou defer, il :
-
Interrumpe el análisis del HTML
-
Descarga el script(si externo) o la cama (si en línea)
-
Ejecuta inmediatamente el script
-
Continúa el parsing del HTML
Este comportamiento, a menudo considerado un problema de rendimiento (de ahí la recomendación habitual de colocar los scripts al final de la página), se convierte en nuestro aliado en este caso específico. Al colocar nuestro script de detección de tema al principio del`<head>`, nous garantissons qu’il s’exécute avant que le navigateur ne rencontre les balises`<link>`de nuestras hojas de estilo
Arquitectura de la solución en tres capas
Nuestra solución completa consta de tres capas interdependientes, cada una desempeñando un papel específico :
Capa 1 : Persistencia (localStorage) Esta capa es responsable de guardar y recuperar la elección del usuario entre sesiones.
Capa 2 : Sincronización temprana (Script inline en <head>) Esta capa sincroniza el estado de la aplicación con el DOM antes del renderizado inicial.
Capa 3: Estilos reactivos (CSS con selectores de atributos) Esta capa define los estilos visuales basados en el estado definido por la capa 2.
Exploremos ahora cada capa en detalle.
Paso 1: Guardar la Selección del Usuario
El localStorage : Tu Memoria Persistente
Le localStorage`es una API de Web Storage que permite almacenar pares clave-valor en el navegador de forma persistente. A diferencia de las cookies, los datos del`localStorage:
-
Nunca se envían automáticamente al servidor
-
Tienen una capacidad de almacenamiento mayor (generalmente 5-10MB)
-
No tienen fecha de expiración (persistente hasta su eliminación explícita)
-
Están limitadas al protocolo y al dominio (Same-Origin Policy)
Para nuestro caso de uso, el`localStorage`es perfecto porque :
-
No necesitamos compartir esta información con el servidor
-
Queremos que la preferencia persista indefinidamente
-
El tamaño de almacenamiento requerido es mínimo (unos pocos bytes)
Implementación de la copia de seguridad del tema
Así es como guardamos la elección del usuario cuando cambia de tema :
// Fonction complète pour changer le thème
function setTheme(newTheme) {
// Validation de l'entrée
if (!['light', 'dark', 'auto'].includes(newTheme)) {
console.error('Thème invalide:', newTheme);
return;
}
try {
// Sauvegarde dans localStorage
localStorage.setItem('preferred-theme', newTheme);
// Application immédiate dans le DOM
document.documentElement.setAttribute('data-bs-theme', newTheme);
// Dispatch d'un événement personnalisé pour notifier d'autres composants
window.dispatchEvent(new CustomEvent('theme-changed', {
detail: { theme: newTheme }
}));
console.log('Thème changé:', newTheme);
} catch (error) {
console.error('Erreur lors de la sauvegarde du thème:', error);
// Fallback : on applique quand même le thème visuellement
document.documentElement.setAttribute('data-bs-theme', newTheme);
}
}
// Exemple d'utilisation avec un bouton
document.getElementById('theme-toggle').addEventListener('click', () => {
const currentTheme = document.documentElement.getAttribute('data-bs-theme') || 'light';
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
setTheme(newTheme);
});
Gestión de Casos de Error
Es crucial gestionar los casos donde le`localStorage`no está disponible o accesible. Varios escenarios pueden impedir el acceso al`localStorage`:
Navegación privada estrictaSafari en modo de navegación privada lanza una excepción`QuotaExceededError`al intentar escribir en el`localStorage`.
Ajustes de privacidad: Algunos navegadores o extensiones de privacidad pueden bloquear el acceso al`localStorage`.
Limitaciones de dominio : Le `localStorage`no es accesible en el protocolo`file://`en algunos navegadores.
Espacio de almacenamiento saturadoAunque raro, el espacio de almacenamiento puede estar completamente lleno.
Es por eso que nuestro código utiliza un bloque`try…catch`para gestionar estos casos con elegancia, continuando de ofrecer la funcionalidad de cambio de tema incluso si la persistencia no está disponible.
Estrategias Avanzadas de Persistencia
Para aplicaciones más sofisticadas, puede considerar estrategias adicionales:
Sincronización servidor (opcional)
async function setTheme(newTheme) {
// Sauvegarde locale immédiate
localStorage.setItem('preferred-theme', newTheme);
document.documentElement.setAttribute('data-bs-theme', newTheme);
// Synchronisation serveur en arrière-plan (si l'utilisateur est connecté)
if (userIsAuthenticated()) {
try {
await fetch('/api/user/preferences', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ theme: newTheme })
});
} catch (error) {
console.warn('Échec de la synchronisation serveur:', error);
// L'échec n'est pas critique car la préférence est déjà sauvegardée localement
}
}
}
Este enfoque permite sincronizar las preferencias entre dispositivos para los usuarios conectados, manteniendo al mismo tiempo una capacidad de respuesta local inmediata.
Paso 2: El script de pre‑carga dentro del `<head>
El Corazón de la Solución
Es aquí donde la magia opera realmente. Vamos a colocar un pequeño script.en líneadirectamente en nuestro`<head>`, antes de todas nuestras etiquetas`<link>`de hojas de estilo. Este script es voluntariamente minimalista, autónomo y diseñado para ejecutarse lo más rápido posible.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mon Site Incroyable</title>
<!-- ==========================================
NOTRE SCRIPT MAGIQUE DE PRÉ-CHARGEMENT
Ce script DOIT être le premier élément
dans le <head> après les meta tags
========================================== -->
<script>
// IIFE pour ne pas polluer le scope global
(function() {
'use strict';
try {
// Lecture de la préférence sauvegardée
const savedTheme = localStorage.getItem('preferred-theme');
// Si une préférence existe, on l'applique immédiatement
if (savedTheme) {
document.documentElement.setAttribute('data-bs-theme', savedTheme);
}
// Optionnel : Détecter la préférence système si aucune sauvegarde
else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.setAttribute('data-bs-theme', 'dark');
}
// Sinon, le thème par défaut du CSS sera utilisé (généralement 'light')
} catch (error) {
// En cas d'erreur (localStorage bloqué, etc.), on log discrètement
// et on laisse le thème par défaut s'appliquer
console.warn('Impossible de charger la préférence de thème:', error);
}
})();
</script>
<!-- FIN DU SCRIPT MAGIQUE -->
<!-- Les feuilles de style sont chargées APRÈS le script -->
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/styles.css">
<!-- Autres ressources du head -->
<link rel="icon" href="favicon.ico">
</head>
<body>
<!-- Contenu de la page -->
</body>
</html>
Anatomía del Script: Cada Línea Cuenta
Desglosemos este script línea por línea para comprender cada decisión de diseño:
La IIFE (Expresión de Función Inmediatamente Invocada)
(function() {
// ...
})();
Esta estructura crea una función que se ejecuta inmediatamente. ¿Por qué? Para aislar nuestras variables en un ámbito local y evitar de contaminar el ámbito global. Aunque solo utilicemos`const`(que tiene un ámbito de bloque), la IIFE es una buena práctica que deja nuestras intenciones claras y protege contra posibles conflictos de nombres.
El Modo Estricto
'use strict';
Esta directiva activa el modo estricto de JavaScript, que: - Prohíbe el uso de variables no declaradas - Genera errores para las operaciones peligrosas - Mejora el rendimiento en algunos motores de JavaScript
Para un script crítico como este, queremos la máxima seguridad.
El bloque try…catch
try {
// Code principal
} catch (error) {
console.warn('Impossible de charger la préférence de thème:', error);
}
Este bloque es absolutamente crucial. Garantiza que si algo sale mal (localStorage bloqueado, error de sintaxis improbable, etc.), nuestro script no bloqueará la carga de toda la página. El uso de`console.warn`más que`console.error`indica que es un problema no crítico.
La Lectura del localStorage
const savedTheme = localStorage.getItem('preferred-theme');
Esta línea puede lanzar una excepción en ciertos contextos (navegación privada estricta de Safari). Por eso está dentro de un bloque try…catch.
La Aplicación Condicional
if (savedTheme) {
document.documentElement.setAttribute('data-bs-theme', savedTheme);
}
Aplicamos el tema solo si hemos encontrado uno guardado. En caso contrario, dejamos que el CSS use su tema predeterminado. Este enfoque es más robusto que un valor predeterminado codificado de forma fija en JavaScript.
Detección de la Preferencia del Sistema (Bonus)
Una mejora opcional pero elegante consiste en detectar la preferencia de tema del sistema operativo del usuario si aún no ha hecho una elección explícita en tu aplicación :
else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.setAttribute('data-bs-theme', 'dark');
}
Esta funcionalidad utiliza la Media Query`prefers-color-scheme`para consultar el sistema. En macOS, Windows 10+, iOS y Android moderno, esta consulta devuelve la preferencia del sistema del usuario.
Ventajas : - Experiencia personalizada desde la primera visita Coherencia con el entorno del sistema del usuario - No se requiere almacenamiento para la primera visita
Consideraciones : - No todos los navegadores admiten esta funcionalidad (pero el soporte es excelente desde 2020) - La verificación`window.matchMedia`garantiza la compatibilidad - El usuario siempre puede sobrescribir esta opción
Rendimiento: ¿Por qué este script es rápido?
Nuestro script de pre-carga está diseñado para ser extremadamente rápido:
Tamaño mínimo: Aproximadamente 300 bytes sin minificar, 200 bytes minificados. Es insignificante comparado con cualquier imagen o biblioteca JavaScript.
Inline: No hay solicitud HTTP adicional. El script está en el HTML, por lo que está inmediatamente disponible.
Operaciones síncronas simples: Lectura de una clave en el localStorage (operación ultra rápida) y modificación de un atributo DOM (operación nativa del navegador).
Ninguna dependencia: Sin framework, sin librería, solo JavaScript vanilla. Sin tiempo de arranque, sin análisis de dependencias.
Ejecución única: Este script se ejecuta una sola vez al cargar. No hay listeners de eventos, no hay bucles, no hay cálculos complejos.
En la práctica, en hardware moderno, este script se ejecuta en menos de 1 milisegundo, un tiempo imperceptible que no tiene ningún impacto en el rendimiento de carga de la página.
Colocación Óptima en el `<head>
El orden de los elementos en el`<head>`es importante. Aquí está el orden recomendado :
<head>
<!-- 1. Métadonnées critiques -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 2. Notre script de pré-chargement (IMMÉDIATEMENT après les meta) -->
<script>
(function() { /* notre code */ })();
</script>
<!-- 3. Titre de la page -->
<title>Mon Site</title>
<!-- 4. Feuilles de style -->
<link rel="stylesheet" href="styles.css">
<!-- 5. Autres ressources (fonts, favicons, etc.) -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="icon" href="favicon.ico">
<!-- 6. Autres scripts avec defer ou async -->
<script src="app.js" defer></script>
</head>
Esta orden garantiza que:
-
El charset se define antes de cualquier procesamiento de texto
-
Nuestro script se ejecuta antes de cargar los CSS
-
Los CSS se cargan después y aplican directamente el tema correcto
-
Los otros recursos no críticos se cargan al final
Paso 3: El poder de los selectores de atributos CSS
El Sistema de Tema de Bootstrap 5
Bootstrap 5 introdujo un sistema elegante de gestión de temas basado en las propiedades personalizadas CSS (variables CSS) y los selectores de atributos. Este sistema utiliza el atributo`data-bs-theme`sobre el elemento`<html>`para determinar qué conjunto de variables de color aplicar.
La belleza de este sistema reside en su simplicidad: en lugar de cargar diferentes hojas de estilo o de alternar clases en miles de elementos, simplemente cambiamos un atributo en un solo elemento, y el CSS se encarga del resto gracias a la cascada.
Estructura CSS para un Sistema de Tema
Aquí tiene una estructura CSS completa para implementar un sistema de tema robusto :
/**
* SYSTÈME DE THÈME COMPLET
* Utilise les Custom Properties CSS pour une maintenance facile
*/
/* ============================================
THÈME PAR DÉFAUT (LIGHT)
Défini sur :root pour être le fallback
============================================ */
:root {
/* Couleurs de base */
--color-primary: #0d6efd;
--color-secondary: #6c757d;
--color-success: #198754;
--color-danger: #dc3545;
--color-warning: #ffc107;
--color-info: #0dcaf0;
/* Couleurs de fond */
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--bg-tertiary: #e9ecef;
/* Couleurs de texte */
--text-primary: #212529;
--text-secondary: #6c757d;
--text-tertiary: #adb5bd;
/* Couleurs de bordure */
--border-color: #dee2e6;
--border-color-subtle: #e9ecef;
/* Couleurs d'ombre */
--shadow-sm: rgba(0, 0, 0, 0.075);
--shadow-md: rgba(0, 0, 0, 0.15);
--shadow-lg: rgba(0, 0, 0, 0.25