Sažetak

U ovom detaljnom tehničkom članku istražujemo kako implementirati izabir teme (light/dark) bez neprijatnog efekta mrkanja koji se javlja prilikom učitavanja stranice. Detaljno analiziramo ciklus renderovanja pretraživača, dubokе uzroke FOUC-a (Flash of Unstyled Content) i predlažemo robustno rešanje temeljno na sinkronom prethodnom učitanju u`<head>`. Ova tehnika osigurava glatko i profesionalno iskustvo korisnika.

Problem: Ovo prokletog mirganja teme

Кошмар за корисничко искуство

Pretpostavite scenu: provedli ste sate da dizajnirate prekrasnu tamnu temu za vašu web aplikaciju. Boje su savršeno ubalansirane, kontrast je optimalan, a vaši korisnici vole ovu opciju. Ali postoji sramotan problem: pri svakom ponovnom učitavanju stranice, tokom frakcije sekunde, prikazuje se podrazumevana svetla tema pre nego što tema tamna preuzme kontrolu.

Ova vizuelna „blikavica“, iako kratka (ponekad manje od 100ms), odmah je primetljiva ljudskom oku i stvara neprijatno iskustvo. Za korisnike koji su izabrali tamu tema iz razloga vizuelnog komforta ili pristupačnosti, ovaj blik može biti i bolestan, naročito u slabo osvetljenom okruženju.

FOUC: Stari problem na webu

Ovaj fenomen je varijanta onog što se naziva "Flash of Unstyled Content" (FOUC), klasicni problem u razvoju web-a koji se vodi u ranim danima CSS-a. FOUC se pojavljuje kada pregledač privremeno prikaže HTML sadržaj bez primijenjenih CSS stilova, stvarajući bljesak nesstilizovanog sadržaja.

U našem specifičnom slučaju ne govorimo o potpuno ne stilizovan sadržaju, već oIsbljek pogrešne teme(FOWT) - sadržaj je oblikovan, ali sa lošom temom. To je posebno frustrirajuće jer pokazuje da naša aplikacija "zaboravi" korisnikov preference pri svakom učitavanju stranice.

Uticaj na percepciju kvalitete

Ovaj problem, iako je tehnički, ima važna posledica na percepciju kvaliteta vaše aplikacije :

Nedostatak poliranja: Migotanje dava utisak da je aplikacija nedovršena ili loše optimizovana. Korisnici često povezivaju ove male vizuelne greške sa nedostatkom opšte profesionalnosti.

Prekid koherencije: Апликација каж да "заборавља" корисничке поставке, стварајући осет десинхронизације међу интерфесом и очекиванияма

Vizuelna umora: Za korisnike osjetljive na svetlo ili koji patnje od migrene, ovaj svetlosni blisk može biti više nego samo estetski nezgodnost.

Перцепција перформаниIronično, čak i ako vaš sajt brzo se učitava, ovaj mrgudanje može dati utisak da je aplikacija spora ili neodgovarajuća.

Tehnička analiza problema

Da bi razumeli kako rešiti ovaj problem, prvo treba da razumemo zašto se pojavljuje. Treptanje se pojavljuje zbog vremenskog odmaka između tri kritična događaja u životnom ciklusu jedne web stranice:

  1. Početno parsiranje HTML-a: Прегледач чита и анализира структуру ваше странице

  2. Примена CSS стиловаPreglednik primenjuje CSS pravila i izračunava vizuelni prikaz

  3. Izvršenje JavaScriptaVaš kod koji menja temu se izvršava

Problema nastaje kada se dogadaj broj 3 (izvršavanje JavaScripta) desava nakon što je preglednik već počeo ili završio dogadaj broj 2 (primenjivanje stilova). U tom trenutku, preglednik je već donuo odluku koja tema će biti prikazana, a vaš kod dolazi previše kasno da utice na to pre prvog prikaza.

Zašto klasički skript nije dovoljan?

Intuitivni pristup ali neefikasan

Najprirodniji pristup za programera bi bio da se skript postavi na kraju našeg.`<body>`Ovaj pristup sledi tradicionalne najbolje prakse veba koje preporučuju da se skripte učitavaju na kraju stranice da ne blokiraju renderovanje.

// À 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');
  }
});

Ovaj pristup čini se logičnim na prvi pogled. Čekamo da DOM bude spreman, zatim primenjujemo temu. Jednostavno, ne? Nažalost, jednostavnost skriva temeljni grešak povezan sa timingu ciklusa renderovanja pretraživača.

Разумевање догађаја DOMContentLoaded

Догађај`DOMContentLoaded`покреће се када је првобитни HTML документ потпуно учитаван и анализиран од стране прегледача,bez čekanjakraj učitavanja stilova, slika i podokvira. To je važna tačka koju treba razumeti.

Evo tipične sekvence događaja:

  1. Прегледник počinje da preuzima HTML

  2. On analizira HTML dok se prima

  3. On otkriva oznake`<link>`za CSS i počinje da ih preuzima

  4. Он открије ознаке`<script>`i izvršava ih (na osnovu njihovog tipa i atributa)

  5. Gradi DOM (Document Object Model)

  6. Događaj DOMContentLoaded se pokreće

  7. On nastavlja da primenjuje stilove i radi layout

  8. Prvi paint (prikaz) se dešava

  9. Događaj`load`pokreće se kada su svi resursi učitani

Problem ? Medu korakom 6 (DOMContentLoaded) i korakom 8 (prvi paint), pregledač je već donio odluke o tome kako da prikaže stranu. Ako vaš skript za promenu teme izvrši se na koraku 6, već je kasno da se izbegne prvi prikaz sa zadanim stilovima.

Проблема блокирања рендеринга

U stvari, vremenski raspored je još kompleksniji. Moderni pretraživači koriste sofisticirane tehnike optimizacije da poboljšaju percepcionu performanse. Pokušavaju da učine prvo bojanje sadržaja (First Contentful Paint) što je moguće brže da korisnik vidi nešto na ekranu.

CSS-ovi su po defaultu "render-blocking", što znači da pretraživač čeka da skinuo i parsira stilove pre nego što uradi prvo crtanje. Logično: ne želimo da prikažemo ne stilizovan sadržaj.

Ali ovde je kap: kada pretraživač primeni ove CSS stilove za prvi put, on to radi na osnovu trenutnog stanja DOM-a. Ako atribut`data-bs-theme`Još nije definisan na etiketi`<html>`, pretraživač će primeniti podrazumevane stilove (obično svetla tema).

Zatim, kada vaš skript se izvrši i promeni ovaj atribut, pretraživač mora:

  1. Прерачунати све стилове које је променела ова промена

  2. Понови лајоут ако је потребно

  3. Prebojati oštećeni elementi

Ovaj proces ponovnog računanja i ponovnog bojenja jeste onaj što izaziva vidljivo mrskanje.

Визуализација проблема

Da bolje razumemo ovu problematičnu sekvencu, pogledajmo detalljan dijagram sekvence:

@startuml
actor Utilisateur
participant Navigateur
participant "Parser HTML" as Parser
participant "CSSOM" as CSS
participant "Script.js (kraj tela)" as Script
participant "Mehanizam renderovanja" 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: Подразумеваема тема (light)
Render -> Navigateur: Premier Paint (Светла тема)
note right: Корисник види светлу тему
deactivate Render

Navigateur -> Script: Изврши скрипт
activate Script
Script -> Script: Чита localStorage
Script -> Navigateur: `setAttribute('data-bs-theme', 'dark')`
deactivate Script

activate Render
Render -> Render: Прерачунај стилове са `data-bs-theme="dark"
note right: Thème sombre
Render -> Navigateur: Repaint (Thème Sombre)
note right: Clignotement visible
deactivate Render

deactivate Navigateur
@enduml

Ovaj dijagram jasno ilustruje problem: prvi paint se dešava pre nego što je naš skript imao priliku da definiše pravu temu. Sledeći repaint stvara vidljiv blinkanje.

Neefikasni pokušaji rešenja

Pokušalo je više pristupa da se reši ovaj problem, ali većina njih ima sopstvene nedostatke:

Приступ 1 : Сакривати садржај док се не учита

body {
  opacity: 0;
  transition: opacity 0.3s;
}

body.loaded {
  opacity: 1;
}

Овај приступ сакрива сав садржај док JavaScript не подеси правилу теме. Проблем? Ово искуштвено одлага приказ садржаја, дający утисак да је сајт спорији. Додатно, ако је JavaScript онемогућен, корисник не види ништа уопште!

Prilazak 2: Korišćenje loader/spinner

Similiran je pristup 1, ali sa spinnerom za učitavanje. To sakriva problem, ali ne poboljšava stvarnu performanse i dodaje nepotrebno perceprano kašnjenje.

Приступ 3 : подразумевана тема у тмони

Neki programeri definišu temnu temu kao podrazumevano u CSS-u. Ovo sprečava treperenje za korisnike temne teme, ali pravi obrnuti problem za korisnike svetle teme!

Ни једна од ових приступа није задоволљива јер лече симптом уместо основног узроцa проблема.

Pravi Rešenje : Delovati ranije

Ključ za rešavanje ovog problema je da budemo svesni da moramo da definišemo atribut`data-bs-theme` преda pregledač ne počne da primenjuje CSS stilove. Ovo znači da nas skripta mora da se izvrši ranije u životnom ciklusu stranice, i to je tačno što ćemo istražiti u sledećoj sekciji.

Решење : Предремено заређивање (Early Loading)

Osnovni princip

Elegantno rešenje naše problema sa miricanjem se temelji na jednostavnom, ali jakom principe :Synchronizuj stanje aplikacije sa procesom renderovanja pregledača.. Umesto da čekamo da se stranica učita da bismo postavili temu, moramo je postavitidokučitavanje, još pre nego što se stilovi CSS primene

Ovaj se pristup naziva "Early Loading" ili "Synchronous Preloading" u žargonu web razvoja. Ideja je izvršiti našu logiku detekcije teme što je moguće ranije u životnom ciklusu stranice, idealno u etiketi`<head>`, пре него што прегледник започне да скачива CSS датотеке.

Zašto je <head> idealno mesto

Le `<head>`HTML dokument se obradjuje sekvencijalno pretraživačem, od gore prema dolje. Svaki element se obrađuje u redu u kojemu se pojavljuje. Ova karakteristika je ključna za naše rešenje.

Kada pretraživač naiđe na oznaku`<script>`у`<head>`bez atributa`async` ou defer, il :

  1. Prekini parsiranje HTML-a

  2. Preuzmi skriptu(ako je eksterno) ili krevet (ako je inline)

  3. Изврши скрипту одмах

  4. Nastavi parsiranje HTML

Ovo ponašanje, koje se često smatra problemom performansi (iz kog razloga je uobičajena preporuka da se skriptovi postavljaju na dnu stranice), postaje naš saveznik u ovom specifičnom slučaju. Postavljanjem našeg skripta za detekciju teme na početku`<head>`, garantujemo da se izvrši pre nego što pretraživač naiđe na oznake`<link>`naših stilova.

Архитектура решения у три слоя

Naše kompletno rešenje se sastoji od tri međuzavisne sloja, svaki od njih igra specijalnu ulogu:

Слой 1: Постојаност (localStorage) Ovaj sloj je odgovoran za čuvanje i vraćanje izbora korisnika između sesija.

Слој 2 : Рана синхронизација (убачен скрипт у <head>) Ovaj sloj sinhronizira stanje aplikacije sa DOM-om pre inicijalnog renderovanja.

Слој 3 : Реактивни стилови (CSS са селекторима атрибута) Ovaj sloj definiše vizuelne stilove koji su zasnovani na stanju definisanom od strane sloja 2.

Sada detaljno istražujemo svaki sloj.

Korak 1: Čuvanje izbora korisnika

localStorage : Vaša stalna memorija

Le localStorage`je Web Storage API koja omogućuje skladištenje parova ključ-vrednost u pretraživaču na trajani način. U suprotnosti od kolačića, podaci`localStorage(Translation of empty string is empty string, so output nothing)

  • Nikad se automatski ne šalju na server

  • Imaju veći kapacitet skladištenja (obično 5-10 MB)

  • Nemaju datum isteka (traju do explicitnog brisanja)

  • Ограничују се на протокол и домен (Политика истог пореклошца)

За наш случај употребе,`localStorage`је савршен jarra :

  1. Нам није потребно да поделимо ову информацију са сервером

  2. Želimo da preferencija traje neograničeno.

  3. Potrebna veličina skladišta je minimalna (nekoliko bajtova)

Implementacija čuvanja teme

Ovo je kako čuvamo izbor korisnika kada menja temu:

// 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);
});

Upravljanje slučajevima greške

Izuzetno je važno da se upravlja slučajevima kod kojih`localStorage`nije dostupan ili pristupačan. Prikoliko scenarija mogu sprečiti pristup`localStorage`:

Striktno privatno pretraživanje: Safari u privatnom načinu preglađanja baca izuzetak`QuotaExceededError`u pokušajima pisanja u`localStorage`.

Подешавања приватности: neki pregledači ili proširenja za privatnost mogu blokirati pristup`localStorage`.

Ograničenja domena : Le `localStorage`nije dostupan na protokolu`file://`у неким претраживачам.

Popunjen prostor za skladištenjeIako retko, prostor za skladištenje može biti potpuno popunjen.

Zato naš kod koristi blok`try…​catch`Za rukovanje tim slučajevima uz ljubaznost, nastavljajući da nudi funkciju promene teme čak i kada perzistencija nije dostupna.

Napredne strategije perzistencije

За састављење савијањих апликација, можете да размотрите додатне стратегије :

Синхронизација сервера (опцијонално)

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
    }
  }
}

Ovaj pristup omogućava sinhronizaciju preferenci između uređaja za prijavljene korisnike, i zadržavajući lokalnu odzivnost u stvarnom vremenu.

Korak 2 : Skript prethodnog učitavanja unutar `<head>

Srce rešenja

Ово је где магия заиста ради. Ми ћемо поставити мали скриптu linijiправо у наш`<head>`, prije svih naших oznake`<link>`стилеве. Оvaj скрипт је намишњено минималистичан, аутономан и дизајниран да се изврши као што је могуће брже.

<!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>

Anatomija skripte: Svaka linija važi

Razložimo ovaj skript linija po liniji da razumemo svako odluku u dizajnu:

IIFE (Izraz odmah pozivane funkcije)

(function() {
  // ...
})();

Ova struktura kreira funkciju koja se odmah izvršava. Zašto? Da izoljamo svoje promenljive u lokalnom opsegu i da izbegnemo zagađavanje globalnog opsegu. Čak i ako koristimo samo`const`(koji ima blok opseg), IIFE je dobra praksa koja čini naše namere jasne i zaštičuje od potencijalnih konflikata imena.

Строги режим

'use strict';

Ova direktiva aktivira strict mode u JavaScriptu, koji: - Забрањено коришћење недекларисаних променљивих - Generiše greške za opasne operacije - Poboljšava performanse u nekim JavaScript motorima

За критички скрипт као овај, желимо максималну безбедност.

Blok try…​catch

try {
  // Code principal
} catch (error) {
  console.warn('Impossible de charger la préférence de thème:', error);
}

Ovaj blok je apsolutno ključan. On garantuje da ako se nešto pokvari (localStorage je blokiran, neprobabilna sintaksna greška, itd.), naš skript neće blokirati učitavanje cele stranice. Korišćenje`console.warn`umesto`console.error`укаже да је то неопрезив проблем.

Čitanje iz localStorage

const savedTheme = localStorage.getItem('preferred-theme');

Ova linija može baciti izuzetak u nekim kontekstima (striktno privatno pretraživanje u Safari-u). Zato je u bloku try…​catch.

Uslovna primena

if (savedTheme) {
  document.documentElement.setAttribute('data-bs-theme', savedTheme);
}

Primenjujemo temu samo ako smo pronašli sačuvanu. Inače, dopuštamo CSS-u da koristi svoj podrazumevani temu. Ovaj pristup je čvrši od podrazumevane vrednosti koja je hardkodirana u JavaScriptu.

Detekcija preferencija sistema (Bonus)

Opцiona no eleganтna nadogradnja sastoji se u detektovanjem preferenca temea operativnog sistema korisnika ako jos nije izradio eksplcitni izbor u vašoj aplikaciji :

else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
  document.documentElement.setAttribute('data-bs-theme', 'dark');
}

Ова функција користи медијски упит`prefers-color-scheme`На macOS, Windows 10+, iOS i Android moderno, ovaj zahtev vraća sistemsku preferencu korisnika.

Prednosti: Prilagođeno iskustvo odmah od prve posete - Саглашеност са системским окружењем корисника - Нема потребе за складиштење за први посетак

Размотрања : - Ne svi preglednici podržavaju ovu funkcionalnost (ali podrška je odlična od 2020.) Проверка`window.matchMedia`osigurava kompatibilnost - Корисник може увек да преодоле овог избора.

Performance : Zašto je ovo skript brzo

Naš skript za pre-učitavanje je dizajniran da bude ekstremno brz:

Minimalna veličina: Okoliko 300 bajtova ne-minifikovanih, 200 bajtova minifikovanih. To je zanemarljivo u poređenju sa bilo kojom slikom ili JavaScript bibliotekom.

U linijiNema dodatnog HTTP zahteva. Skript se nalazi u HTML-u, pa je odmah dostupan.

Jednostavne sinkrone operacije: Čitanje ključa u localStorage (ultra brza operacija) i izmena atributa DOM (native operacija pregledača).

Nema zavisnosti: Nema frameworka, nema biblioteke, samo vanila JavaScript. Nema vremena pokretanja, nema parsanja zavisnosti.

Jedno izvršavanje: Ovaj skript se izvršava samo jednom pri učitanju. Nema slušalaca događaja, nema petlji, nema složenih izračuna.

U praksi, na modernom hardveru, ovaj skript se izvršava u manje od 1 milisekunde, vreme koje je neprimetno i nema uticaja na performanse učitavanja stranice.

Optimalno postavljanje u `<head>

Redosled elemenata u`<head>`est važno. Sledeći je preporučeni redosled:

<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>

Ova naredba garantuje da:

  1. Karakterni skup je definisan pre bilo kog obrade teksta

  2. Naša skripta se izvršava pre učitavanja CSS

  3. CSS se zatim učitavaju i primenju se direktno pravu temu.

  4. Ostali ne-kritični resursi se učitavaju poslednji

Korak 3: Snaga CSS selektora atributa

Sistem teme Bootstrap 5

Bootstrap 5 je uveo elegantan sistem za upravljanje temama koji se temelji na CSS custom property-ima (CSS promenljivim) i selektorima atributa. Ovaj sistem koristi atribut`data-bs-theme`na elementu`<html>`da se odredi koji skup promenljivih boja treba primeniti

Lepota ovog sistema je u njena prostota: umesto da učitamo različite stilove ili preklapamo klase na hiljade elemenata, jednostavno menjamo jedan atribut na jedinom elementu, a CSS ostalo radi zahvaljujući kaskadi.

CSS struktura za sistem teme

Evo kompletne CSS strukture za implementaciju jakog sistema tema:

/**
 * 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

Повезани чланци