Principes
Sept règles qui priment sur tout le reste. En cas de doute, importer tokens.css et puiser dans les variables plutôt qu'inventer une valeur.
Couleurs
Deux accents profonds et désaturés — olive leaf et vintage grape — déclinés en rampes. Les teintes base servent aux fonds pleins, les soft au texte et aux liens sur fond sombre.
Surfaces
Accent primaire — Olive Leaf
Accent secondaire — Vintage Grape
Texte
Sémantique
Typographie
Stack système, aucune dépendance web-font. Corps à 17px. Mono pour le code et la donnée.
Boutons
Plats, nets, arrondis légers. Le survol décale d'une teinte — sans halo ni gradient. .zl-btn + variantes --primary (olive), --secondary (grape), --ghost, --destructive (rose-rouge plein), --danger (contour discret), --loading (voir États). Tailles --sm / défaut / --lg. Fonctionne à l'identique sur <button> et <a> — jamais de soulignement.
Action destructive — la paire canonique
Quand l'action principale détruit quelque chose (supprimer, révoquer, purger), elle passe en --destructive : rose-rouge plein, même construction que --primary. Elle est toujours accompagnée d'un « Annuler » en --secondary (ou --ghost sur les interfaces denses). La variante --danger (contour) reste la version discrète — lignes de tableau, barres d'outils.
<!-- copier/coller --> <button class="zl-btn zl-btn--primary">Action principale</button> <button class="zl-btn zl-btn--ghost zl-btn--sm">Annuler</button> <!-- dialogue destructif : action pleine + annuler en secondaire --> <button class="zl-btn zl-btn--destructive">Supprimer le projet</button> <button class="zl-btn zl-btn--secondary">Annuler</button>
Champs & formulaires
Focus = bordure olive + halo doux. Enveloppe : .zl-field + .zl-label + .zl-hint.
Select personnalisé — .zl-pick, partout sauf iOS
Hors iOS, le <select> natif est remplacé de bout en bout par .zl-pick — le même composant que le sélecteur de langue .zl-langpick (mêmes règles CSS dans tokens.css, le drapeau en moins) : bouton + menu stylé tokens, ✓ sur l'option choisie, clavier complet (↑/↓/Home/End/Entrée/Échap), fermeture au clic extérieur, valeur recopiée dans un input caché (ou un vrai <select>). Exception iOS : on garde le <select class="zl-select"> natif — le picker système (HIG Apple) offre une meilleure UX tactile que n'importe quel menu maison.
// exception iOS : garder le <select class="zl-select"> natif (picker système, HIG Apple) const isIOS = /iP(hone|ad|od)/.test(navigator.userAgent) || (navigator.maxTouchPoints > 1 && /Mac/.test(navigator.userAgent)); // .zl-pick — comportement complet (ouverture, clavier, clic extérieur, sync) function zlPick(root) { const btn = root.querySelector('.zl-pick__btn'); const menu = root.querySelector('.zl-pick__menu'); const opts = [...root.querySelectorAll('.zl-pick__opt')]; const nameEl = btn.querySelector('[data-name]'); const input = root.querySelector('input[type="hidden"]'); // ou un vrai <select> caché let hi = Math.max(0, opts.findIndex(o => o.getAttribute('aria-selected') === 'true')); const open = () => { root.setAttribute('aria-expanded', 'true'); setHi(hi); menu.focus(); }; const close = () => { root.setAttribute('aria-expanded', 'false'); btn.focus(); }; const isOpen = () => root.getAttribute('aria-expanded') === 'true'; const setHi = (i) => { hi = (i + opts.length) % opts.length; opts.forEach((o, k) => o.classList.toggle('is-active', k === hi)); opts[hi].scrollIntoView({ block: 'nearest' }); }; function select(opt) { opts.forEach(o => o.setAttribute('aria-selected', String(o === opt))); // le ✓ suit aria-selected nameEl.textContent = opt.querySelector('.zl-pick__name').textContent; if (input) { input.value = opt.dataset.value; input.dispatchEvent(new Event('change', { bubbles: true })); } } btn.addEventListener('click', () => isOpen() ? close() : open()); opts.forEach((o, i) => { o.addEventListener('mouseenter', () => setHi(i)); o.addEventListener('click', () => { select(o); close(); }); }); menu.addEventListener('keydown', (e) => { if (e.key === 'ArrowDown') { e.preventDefault(); setHi(hi + 1); } else if (e.key === 'ArrowUp') { e.preventDefault(); setHi(hi - 1); } else if (e.key === 'Home') { e.preventDefault(); setHi(0); } else if (e.key === 'End') { e.preventDefault(); setHi(opts.length - 1); } else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); select(opts[hi]); close(); } else if (e.key === 'Escape') { e.preventDefault(); close(); } }); btn.addEventListener('keydown', (e) => { if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(); } }); document.addEventListener('click', (e) => { if (isOpen() && !root.contains(e.target)) close(); }); } if (!isIOS) document.querySelectorAll('.zl-pick').forEach(zlPick);
Badges & tags
Arrondi léger (pas de pilule). .zl-badge + variantes --accent / --accent2 / sémantiques ; --dot ajoute une pastille d'état. La gamme « mood » suit le cycle de vie d'un produit ou d'une feature : --new à la sortie, --beta ouvert mais pas garanti, --wip en chantier, --draft brouillon non publié, --experimental peut disparaître, --live en production, --deprecated à quitter, --archived figé pour mémoire. Tailles --sm / défaut / --lg. --solid passe n'importe quelle variante en aplat encre sombre — réservé aux chips à haut signal, jamais en série.
Alertes
Callouts en ligne : fond lavis + liseré sémantique à gauche. .zl-alert + --info (grape, jamais bleu) / --success / --warning / --danger. Le titre .zl-alert__title se place sur sa propre ligne, au-dessus du corps — comme les cards, jamais sur la même ligne. Une alerte explique quoi faire, pas seulement ce qui s'est passé.
Cards
Arrondi 8px. .zl-card avec --hover (élévation au survol) et --accent (liseré olive).
Tableaux
Lignes en cheveu, en-têtes en étiquette, chiffres en mono tabulaire alignés à droite (.zl-num). Envelopper dans .zl-table-wrap — bord arrondi + défilement horizontal sur mobile, en-tête collant au scroll vertical.
| Projet | Statut | Requêtes (7 j) | p95 |
|---|---|---|---|
| fichedemaire.fr | En ligne | 184 302 | 96 ms |
| parole.zlef.fr | En ligne | 42 118 | 61 ms |
| essence.zlef.fr | Dégradé | 9 042 | 412 ms |
| montvid.zlef.fr | Privé | 1 260 | 88 ms |
Onglets, liens & infobulles
Onglets .zl-tabs — indicateur souligné olive, jamais de fond plein. Liens de texte .zl-link, raccourcis .zl-kbd, infobulle CSS via l'attribut data-zl-tip (survol + focus clavier).
La documentation vit dans le brief LLM et les valeurs dans tokens.css — un lien discret existe aussi en variante quiet.
Recherche rapide : ⌘ K · aide contextuelle
États & chargement
Règle sitewide : rien ne doit sembler figé. Chaque action asynchrone montre un état — spinner, squelette qui reflète la mise en page finale, barre de progression. Un écran vide invite à agir. prefers-reduced-motion arrête le shimmer mais jamais le spinner (c'est le signal d'état). Les primitives se composent par cas d'usage — tableau, graphique, formulaire, galerie, pleine page, téléversement : exemples fictifs ci-dessous.
Spinner & bouton en cours
Squelette — miroir de la mise en page finale
Progression
Cas d'usage — tableau en chargement
| Projet | Statut | Requêtes (7 j) | p95 |
|---|---|---|---|
Cas d'usage — graphique en chargement
Cas d'usage — formulaire à l'envoi
Cas d'usage — galerie / grille média
Cas d'usage — chargement pleine page (transitions de route)
Cas d'usage — téléversement
État vide — une invitation à agir
Créez votre premier projet pour voir apparaître ses métriques ici.
Graphiques & Timelines
Un moteur SVG sans dépendance, partagé par tous les projets qui affichent des données. Importer zl-charts.css + zl-charts.js après tokens.css, puis décrire le graphique en JSON. Règles imposées : un seul axe (jamais deux échelles), palette catégorielle à ordre fixe (jamais recyclée — validée CVD sur la surface ZLEF), légende dès 2 séries, étiquettes directes parcimonieuses, grille en cheveu, tooltips par défaut (survol au bureau, tap sur mobile), vue tableau jumelle de chaque graphique, formatage i18n des nombres/dates. Les séries temporelles denses gagnent un zoom + défilement interne (axe Y figé, ⌘/Ctrl+molette ou pinch). Tous les graphiques se redessinent à la largeur réelle du conteneur : le texte garde sa taille vraie, jamais d'étirement viewBox. Thème sombre uniquement.
Chiffres — stat tiles & jauge
Graphiques — barres, lignes, aire, nuage, heatmap
Timeline — événements & périodes
Données temporelles — zoom & défilement interne
Marquer une série x.type:"time" (ou un nombre de points élevé) active auto le zoom : boutons − ⤡ +, ⌘/Ctrl + molette ou pinch pour zoomer, glisser / balayer pour défiler. L'axe Y reste figé pendant qu'on parcourt toute la plage.
Palette catégorielle — ordre fixe, assignée 1→N, jamais recyclée
<!-- 1. importer (après tokens.css) --> <link rel="stylesheet" href="https://da.zlef.fr/zl-charts.css"> <script defer src="https://da.zlef.fr/zl-charts.js"></script> <!-- 2a. déclaratif (idéal en SSR) : le moteur hydrate au chargement --> <figure class="zlc" data-zlc> <script type="application/json" class="zlc-spec"> {"type":"bar","title":"Ventes", "x":{"values":["Nord","Sud"]}, "series":[{"name":"2025","data":[120,90]}]} </script> </figure> // 2b. impératif ZLC.mount(el, { type:'line', x:{ values:[2019,2020,2021] }, series:[{ name:'Paris', data:[12.1,12.8,12.4] }], valueFmt:'number' });
Header & footer — la gamme
Quatre humeurs de header et quatre de footer, sur le même markup (__brand / __nav / .zl-navlink ; colonnes et barre __bottom côté footer). Tous sont responsive par construction — flex-wrap, grid auto-fit, min() — ils s'adaptent à la largeur de leur conteneur et ne débordent jamais, même à 360 px ; les cadres « 375 px » ci-dessous le prouvent. (Le header sticky de cette page est l'exemple live du header App.)
Header · App — .zl-header
Le chrome d'application par défaut : translucide, flou, hairline, sticky (--zl-z-nav, opt-out --static). Pour tout produit connecté ou outil du quotidien.
Header · Hero — .zl-header--hero
L'humeur marketing/landing : transparent, sans flou ni bordure, marque un cran plus grande. Posé sur un hero ou le champ de particules ; non sticky — une barre transparente n'a rien sur quoi coller.
Header · Compact — .zl-header--compact
L'humeur outil/utilitaire : fin, marque réduite, fond surface-1 plein — pas de flou, économe sur les machines modestes. Sticky comme la base. Pour consoles, dashboards, pages denses.
Header · Éditorial — .zl-header--editorial
L'humeur contenu : une manchette de magazine. Marque centrée sur sa propre ligne, nav centré dessous, encadré de hairlines, padding vertical généreux. Non sticky — la manchette défile avec la page. Pour blogs, revues, longs formats.
Footer · Classique — .zl-footer
La ligne sobre par défaut : © à gauche, liens à droite, hairline en haut, liens qui virent à l'olive au survol. Passe à la ligne sur mobile.
Footer · Colonnes — .zl-footer--columns
L'humeur plan de site : une grille auto-fit de colonnes de liens (.zl-footer__col + __col-title) qui retombe de 4 à 2 puis 1 colonne selon la largeur du conteneur, et une barre __bottom pour le © et le légal. Pour les sites riches en pages.
Footer · Slim — .zl-footer--slim
Une seule ligne fine, centrée, en xs — le minimum vital. Pour outils plein-écran, jeux, pages où le footer doit disparaître.
Footer · CTA — .zl-footer--cta
L'humeur conversion : un bloc titre + bouton primaire (.zl-footer__cta) sur un lavis olive plat — jamais de gradient — au-dessus de la ligne classique. Pour landings et pages produit.
<!-- une humeur = la base + un modificateur, même markup --> <header class="zl-header zl-header--hero"> <div class="zl-header__brand">…</div> <nav class="zl-header__nav"><a class="zl-navlink">…</a></nav> </header> <footer class="zl-footer zl-footer--columns"> <div class="zl-footer__col"> <div class="zl-footer__col-title">Produit</div> <a href="…">Tarifs</a> </div> <div class="zl-footer__bottom">© 2026 …</div> </footer>
Mascotte — Sylve
Sylve, la sentinelle origami low-poly de ZLEF : des plans facettés pliés dans une feuille d'olivier, assise en silence, couronne entaillée comme le wordmark et regard vintage-grape parcimonieux. Le gardien d'un écran encore sans données — média par défaut des états vides, illustrations et 404. Deux fichiers : mascot.svg (détaillé) et mascot-mini.svg (simplifié, échelle icône). SVG autonome, fond transparent, viewBox 0 0 240 200, classes olive/grape pour re-styler. Si possible (et validé), chaque produit dérive sa propre variante — même construction origami low-poly, re-thémée via les classes — plutôt que Sylve stock.
Sylve garde l'écran vide — l'usage canonique de la mascotte : illustrer un état vide sans le rendre morne.
Usage — état vide
<!-- média d'état vide --> <div class="zl-empty"> <img src="https://da.zlef.fr/mascot-mini.svg" alt="Sylve" width="72"> <div class="zl-empty__title">Rien ici pour l'instant</div> </div>
À adopter / à bannir
La liste courte qui tranche les décisions de design ZLEF.
Adopter
- Fond #06060a + rampe de surfaces sombres.
- Olive leaf primaire, vintage grape secondaire — et rien d'autre de saturé.
- Arrondis légers (3–8px) ; composants plats, sans gradient ni halo.
- Corps ≥16px (idéal 17px), interlignage généreux, contrôles ≥44px.
- Stack de police système ; mono pour code et données.
- Survol sobre : décalage d'une teinte, jamais de saut.
- Mouvement subtil, eases doux, prefers-reduced-motion.
- Variables --zl-* de tokens.css pour chaque valeur.
Bannir
- Thème clair ou fonds blancs, sous toutes leurs formes.
- Le bleu en accent ou en lien — utiliser l'olive. (Les séries de données zl-charts, --zlc-cat-*, sont exemptées.)
- Le noir pur #000000 en surface.
- Pleines pilules et gros arrondis systématiques — arrondis légers (3–8px), les cercles restent des cercles.
- L'esthétique « générée par IA » : gradients sur boutons, halos colorés, zoom au survol. Plat, net, une teinte de décalage.
- Texte sous 16px, gris-sur-gris peu contrasté, hairline fin illisible.
- Dépendances web-font.
- Dégradés brillants, skeuomorphisme, mouvements rebondissants.
- Défauts bruts de framework (Bootstrap/Material) ignorant ces tokens.
Langues & sélecteur
Chaque projet est i18n : dictionnaires de chaînes par clé, anglais (défaut, fallback) + français au minimum. Le sélecteur de langue est conditionnel — ne l'affiche pas systématiquement.
Le sélecteur (3+ langues) — composant .zl-langpick, livré dans tokens.css
// résolution de la langue au chargement (ordre de priorité) const read = () => (document.cookie.match(/(?:^|; )zl-lang=([^;]+)/)||[])[1]; let lang = read() || navigator.language.slice(0,2) || 'en'; if (!LOCALES.includes(lang)) lang = 'en'; // fallback anglais // au changement : cookie partagé pour TOUS les sites zlef.fr function setLang(l) { document.cookie = `zl-lang=${l}; domain=.zlef.fr; path=/; max-age=31536000; samesite=lax`; apply(l); }
Ressources & intégration
Le design system est aussi exposé de façon programmatique pour les agents et les builds.
<!-- intégrer le design ZLEF dans n'importe quel projet --> <link rel="stylesheet" href="https://da.zlef.fr/tokens.css"> <script src="https://assets.zlef.fr/particles.js"></script> <!-- champ atmosphérique --> <body class="zl-body"> <button class="zl-btn zl-btn--primary">Go</button> </body>