Passe de mutualisation, d’accessibilité et de SEO, pour une stack HTML / CSS / JS natifs : classes CSS pour la présentation, éléments personnalisés vzr- pour le comportement, et le natif d’abord (<details>, <dialog>, popover, <table>). Chaque entrée porte sa forme HTML, son API d’attributs et son contrat d’accessibilité. Toutes les valeurs viennent de tokens.css.
vzr- (comportement, en light DOM).Aucun composant ne référence un hexadécimal. Jetons complets : --surface-2 (squelettes, encarts inline), --accent-hover / --accent-active, --disabled-* (contraste ≥ 3:1), --success, --overlay.
[data-theme="light"][data-theme="dark"]:focus-visible → outline 2px var(--accent), offset 2px. Sur un aplat d’accent, double anneau --bg + --accent via la classe vzr-focus-inset. Jamais outline:none sans remplacement.
--tap: 44pxLe glyphe peut mesurer 14px, la zone cliquable jamais moins de 44×44 — y compris les onglets de la barre basse et les cellules de jour du calendrier.
Classe .sr-only (définie une fois dans tokens.css). Elle préfixe les tags de confiance/résultat pour que la couleur ne porte jamais seule le sens.
display/40h1, une seule par pageheading/24h2heading/18h3body/16pbody/14p, spanlabel/12h2/h3 si c’est un titre de section, sinon spannum/tabularfont-variant-numeric: tabular-numsPrincipe non négociable : la hiérarchie h1 → h2 → h3 doit être réelle et sans saut de niveau — c’est le premier levier de navigation au lecteur d’écran. Les étiquettes en capitales qui ouvrent une section (« R1 · Vincennes », « Niveau de confiance ») sont des titres, pas des décorations.
--col-fav 44 · flex · --col-odds 64 · --col-gap 16 · --col-tag 150. En-tête et lignes lisent les mêmes jetons : un seul endroit à changer. Points de rupture : mobile 4 col. (90% de large), tablette 8 col. (85%), desktop 12 col. (75%, plafond 1600px), bascule des lignes en grille sous 480px.
css/components/<button class="btn btn--primary"> · modificateurs : --primary --secondary --tertiary --danger --sm --block · états : [disabled] [aria-disabled] [aria-busy] · navigation : <a class="btn">
md = 14px / 12-24 (bordure : 11-23), sm = 13px / 10-20 (bordure : 9-19). Tout CTA de page est md, tout CTA d’en-tête ou de modale est sm.<button type> ou, si l’action navigue, un <a class="btn"> (jamais un div cliquable). Ne pas créer de <vzr-button> : envelopper un bouton casse la soumission de formulaire et la sémantique. disabled retire l’élément du parcours clavier : préférer aria-disabled="true" + onClick neutralisé quand l’utilisateur doit pouvoir comprendre pourquoi c’est bloqué. loading ⇒ aria-busy="true" et libellé qui change (« Envoi… »), pas seulement un spinner.
<button class="icon-btn"> · [aria-label] obligatoire · --sm (36px) --accent --danger · états : [aria-pressed] [aria-expanded] + [aria-controls] · enfant .icon-btn__badge
.icon-btn__badge), menu, fermeture de feuille/modale, flèches de semaine, œil du mot de passe. Les croix de fermeture de feuille/modale et la bascule de thème doivent aussi être de vrais boutons, jamais une icône seule.aria-label est obligatoire (pas de bouton sans nom accessible), le glyphe porte aria-hidden. Bouton à deux états (favori) ⇒ aria-pressed et libellé qui décrit l’action à venir. Bouton qui ouvre un panneau ⇒ aria-expanded + aria-controls. Le compteur de la cloche est aria-hidden : le nombre est déjà dans le libellé.
<span class="tag" data-tone="faible|moyen|eleve|won|lost|np"> · --sm · préfixe .sr-only obligatoire
--conf-nul-* reste employé pour l’état de résultat « Non partant ».size="sm" (usage en ligne de conseil)data-tone — trois de confiance (faible, moyen, eleve) et trois de résultat (won, lost, np) —, deux tailles : md 13px/10-16 en démonstration et filtre, sm 12px/8-12 en ligne de liste. Ne jamais confondre avec .chip (contour, sélectionnable) ni avec le .label de catégorie d’article.span). Le préfixe .sr-only injecte « Confiance : » / « Résultat : » pour que l’information ne dépende ni de la couleur ni de la colonne. Le tag doit appartenir à la ligne du cheval dans l’ordre de lecture du DOM.
<button class="chip" aria-pressed> dans <vzr-chip-group> · émet vzr:change
.label
class="label" · --sm --accent · balise h2/h3 quand l’étiquette ouvre une section
.label remplace sept déclinaisons manuelles de la même étiquette Archivo capitales : titre de section, nom de réunion, en-tête de colonne, titre de groupe de filtres, catégorie d’article, libellé de ROI, en-tête de colonne de pied de page. .label--accent couvre la catégorie d’article. .chip est toujours un vrai bouton à bascule, jamais un div..chip : <button aria-pressed> dans un role="group" nommé, 44px de haut minimum, sélection marquée d’une coche en plus de la couleur ; <vzr-chip-group> gère la bascule et émet vzr:change. .label : mettre la classe sur un <h2>/<h3> quand l’étiquette ouvre une section — sinon la page n’a plus de structure de titres.
.field > label.field__label[for] + contrôle + .field__hint + .field__error
input · select · textarea · checkbox (natifs) + vzr-form
class="input|select|textarea" · [aria-invalid] [aria-describedby] [autocomplete] · vzr-form : validation, focus, résumé role="alert"
Format invalide — 5 chiffres attendus.
.field (libellé + aide + erreur + liaison des id) et les quatre contrôles natifs. Le champ date et le champ mot de passe ne sont qu’un .input suivi d’un .icon-btn. Le « sélecteur ouvert » disparaît : un <select> natif — et si une recherche devient nécessaire, un <vzr-combobox> bâti sur un <input list>, jamais une liste en div role="option" sans gestion clavier.label for ⇄ id écrit dans le HTML (pas généré en JS, il doit exister sans JavaScript). Erreur : aria-invalid="true" + aria-describedby pointant sur le message, bordure portée à 2px et icône d’alerte — la couleur seule ne suffit pas. Aide et erreur peuvent coexister dans aria-describedby. À la soumission, focus sur le premier champ invalide et résumé d’erreurs en role="alert". Cases à cocher : zone cliquable de 44px via le label.
<vzr-tabs> (role=tablist + tabpanel) ou <vzr-radio-group name value> (role=radiogroup) · gabarit .segmented · --icon --grid · tabindex tournant + flèches
role="tablist" n’est légitime que si le contrôle change un panneau de contenu dans la même page : il faut alors aria-controls vers un role="tabpanel" identifié, un seul onglet dans l’ordre de tabulation (tabindex 0 / -1) et la navigation par flèches ← →, Début/Fin. Quand le contrôle est un choix de formulaire (périodicité, moyen de paiement), c’est role="radiogroup" + role="radio" aria-checked. Quand ce n’est ni l’un ni l’autre (filtre cumulable), utiliser Chip. Icône seule ⇒ nom accessible en .sr-only, jamais un aria-label qui remplacerait un texte visible.hidden). Choix de formulaire : préférer de vrais <input type="radio"> stylés en .segmented — recommandé pour Paiement et Formules, où la robustesse prime sur le rendu.
class="avatar" · --sm (36px) · initiales aria-hidden
.stat
.stat > .label + .stat__value + .stat__meta · info : .icon-btn[popovertarget]
.skeleton
class="skeleton" · conteneur [aria-busy] + annonce .sr-only unique
.stat pour le profil du pronostiqueur ci-contre et pour le bandeau de performance sous le calendrier des pages Turf et Sports : un seul .icon-btn d’info partagé par la paire 30j/7j (jamais un par valeur, sinon les deux colonnes désalignent), .vzr-roi-row empile les deux valeurs en colonne avec un faible écart (gap:12px) et passe en ligne dès 480px (gap:20px, pas de grande marge), et le libellé porte la discipline ou le type de pari sélectionné (« ROI · Football »). Sur Turf et Sports, .vzr-cal-roi-row regroupe ce bandeau avec la navigation par date juste au-dessus : empilés en mobile (comportement inchangé), côte à côte dès 900px (calendrier 2 fr, bandeau 1 fr). La valeur (signe + nombre) porte white-space:nowrap pour ne jamais se rompre entre le signe et le chiffre quand la colonne se resserre..avatar : les initiales sont décoratives (aria-hidden), le nom lisible est porté par la ligne voisine ; avec src, alt="" et non le nom répété. .stat : le signe est déjà dans la valeur (« −4,2 % »), aucune couleur n’encode la polarité — conserver ce choix, il est bon. Le glyphe de tendance (ph-trend-up/down, ou ph-minus si stable) code une donnée distincte du signe — la trajectoire récente, pas la polarité : --success en hausse, --danger en baisse, --accent stable. La couleur renforce une forme déjà différente par état (jamais seule porteuse de sens) et reste toujours accompagnée d’un .sr-only (« tendance en hausse/baisse/stable ») pour les lecteurs d’écran et le mode sans couleur. Quand la sélection qui détermine la valeur (discipline, type de pari) peut changer sans rechargement de page, .stat__value porte aria-live="polite" pour annoncer la mise à jour. .skeleton : conteneur aria-busy="true", barres aria-hidden, une annonce .sr-only unique — pas une barre lue N fois.
<details class="roi-detail"> > table par type de pari > th colspan (période) > lignes global + 3 niveaux de confiance
| Période / confiance | Réussite | ROI |
|---|---|---|
| Veille | ||
| Tous niveaux de confiance | 36,4 % | +35,2 % |
| Confiance faible | 36,8 % | +54,2 % |
| Confiance moyenne | 66,7 % | +80,0 % |
| Confiance élevée | 27,3 % | -10,0 % |
| 30 derniers jours | ||
| Tous niveaux de confiance | 31,4 % | +3,1 % |
Reprend .stat pour l’en-tête chiffré (30j / veille) et ajoute ce panneau repliable — en superposition (position:absolute, ombre) plutôt que de repousser le contenu — pour le détail par niveau de confiance et par période (veille, 30 jours, depuis le début). Une seule table à la fois : celle du type de pari (ou marché) actif dans l’onglet au-dessus ; changer d’onglet change la table affichée. Se referme sur clic extérieur ou Échap. Contenu vertical plafonné en .vzr-scroll-y si la liste dépasse la hauteur du panneau.
INDICATEURS change de contenu par discipline. Une table par type de pari (deux tables : gagnant/placé, ou 1X2/plus pour le foot), une ligne <th colspan="3"> par période à l’intérieur du <tbody>, sans imbriquer de sous-table.<table> avec <caption> (.sr-only), en-têtes de colonnes scope="col", en-tête de période scope="colgroup", et libellé de ligne en <th scope="row"> — jamais un <td>. <details>/<summary> natif : replié par défaut, clavier et annonce d’expansion gratuits, zéro JavaScript pour l’ouverture/fermeture. Le signe du ROI est déjà porté par la valeur (+/-) ; la couleur (--success/--danger) ne fait que renforcer, jamais seule porteuse de sens.
scrollbar-width:thin + scrollbar-color:var(--rule) transparent · ::-webkit-scrollbar assorti
html ci-dessous) : .vzr-scroll-x pour un défilement horizontal (barre de dates, rangées de chips qui débordent), .vzr-scroll-y pour un défilement vertical (panneau .roi-detail, tout contenu à hauteur plafonnée). Même piste (var(--rule)), même épaisseur fine, même éclaircissement au survol (var(--text-2)) que l’ascenseur de la page — jamais l’ascenseur par défaut du navigateur. La fenêtre html applique la même palette via scrollbar-color et ::-webkit-scrollbar pour rester cohérente au niveau document.scrollbar-width/scrollbar-color (Firefox) et ::-webkit-scrollbar-* (Chromium/Safari) sont purement visuels — le comportement clavier et molette du défilement natif reste intact, aucun ascenseur maison en JavaScript.
class="msg msg--info|--success|--danger" · --toast · <vzr-toasts> dans la coque · API : toast(texte, {tone})
variant. inline se pose dans le flux, --toast se pose en haut centré dans l’élément <vzr-toasts> présent dans la coque de chaque page.role="status" (aria-live="polite") ; erreur ⇒ role="alert". La zone d’annonce doit exister dans le DOM avant l’apparition du message, sinon rien n’est lu. Disparition auto à ~3s : uniquement pour les toasts non critiques ; une erreur reste jusqu’à action. Respecter prefers-reduced-motion pour l’entrée.
js/components/<details class="disclosure disclosure--label|--row|--footer" [open]> > <summary><h3> · <vzr-accordion> optionnel (un seul panneau ouvert)
<details> / <summary> natifs. Un <vzr-accordion> optionnel n’est nécessaire que si un seul panneau doit rester ouvert. Le pied de page bascule accordéon ⇄ colonnes en CSS seul.<details> fournit l’état, le clavier et l’annonce d’expansion gratuitement, et fonctionne sans JavaScript — c’est la raison de le préférer à un accordéon maison. Le titre (h2/h3) se place dans le <summary>. Marqueur natif masqué, chevron dont le glyphe change (caret-right → caret-down) plutôt qu’une rotation, qui disparaît sous prefers-reduced-motion. Ne jamais simuler le repli en height:0 : les liens resteraient focalisables.
<ul class="picks"> · <li class="pick" data-discipline="turf|football|tennis" data-mode="tip|result"> · une colonne par conseil
vzr-fav-toggle
<vzr-fav-toggle pick="id"> autour du .icon-btn[aria-pressed] · repli : <form method="post">
<li> à trois zones — favori (44px), identité, valeurs. L’identité s’empile toujours verticalement : horaire (sport) ou numéro (turf), nom, puis ligne de conseil. Les valeurs (cote + confiance/résultat) passent à droite au-delà de 760px, sous l’identité en dessous : aucun réalignement de colonnes, donc aucun chevauchement en étroit.aria-pressed + libellé nominatif (« Ajouter Aston Villa – Everton aux favoris »), pleine et accentuée quand actif. Sur la page Favoris elle sert de retrait.data-discipline) et pour les états conseil / résultat (data-mode). Ce qui varie : le contenu des trois lignes d’identité et la pastille de droite. Turf : libellé « Jockey » (Plat/Monté/Steeple/Haies) ou « Driver » (Attelé), trophée = top 3 gagnant, médaille = top 3 placé, légende avant la liste.<table> : <ul> de conseils, chaque valeur préfixée de son libellé — « Cote » visible, « Confiance : » / « Résultat : » en sr-only dans la pastille. Titre de groupe (compétition ou course) en amont de la liste. Cibles tactiles 44px pour l’étoile et le bouton d’info.
<ul class="list"><li><a class="list-row" data-state="default|unread|pinned|closed"> · un seul lien par ligne
.list-row ; l’emplacement de tête reçoit un .avatar ou une icône..sr-only ou méta visible « Clos »), et l’état clos utilise --text-2 plutôt qu’une opacité — l’opacité fait tomber le contraste sous 3:1 et ne peut pas être compensée. Liste = ul>li, ligne entièrement cliquable via un seul <a> (jamais deux liens imbriqués), hauteur minimale 44px.
class="card" · --highlighted --locked · titre porteur du lien
.card--plan · .card--space · .card--article
— trois compositions de la même coque .card
29 €/mois
Réunions du jour, analyses de partants, retours d’expérience entre abonnés Turf.
Décimale, fractionnaire, américaine : ce que chaque format révèle sur la probabilité implicite.
highlighted = filet 2px accent. Trois compositions sur une coque, pas trois composants indépendants.aria-hidden : sinon le lecteur d’écran annonce deux liens vers la même cible. État locked : jamais d’opacité — couleur --text-2, filet décoratif, et une étiquette « Verrouillé » lisible ; le CTA reste un vrai lien focalisable. La liste de bénéfices d’une formule est un ul, pas des br.
<vzr-date-strip value="2026-10-14" week-of="2026-10-12"> · cellules .day en role=radio · émet vzr:change
vzr-date-picker
grille de .day dans [popover] (desktop) ou dialog.sheet (mobile), clonée depuis <template id="tpl-month">
Anneau = aujourd’hui (aria-current="date") · soulignement = date sélectionnée (aria-checked) · jours passés en --text-2.
.day (états passé / aujourd’hui / sélectionné). Le calendrier n’est pas un composant à part : c’est une grille de .day dans un [popover] (desktop) ou un <dialog class="sheet"> (mobile), clonée depuis un <template>. Sur Turf et Sports, cette section vit dans .vzr-cal-roi-row avec le bandeau ROI voisin (voir plus bas) : colonne empilée en mobile, ligne dès 900px, le calendrier prenant deux fois la largeur du bandeau.role="radiogroup" + role="radio" aria-checked, qui décrit un choix exclusif. Aujourd’hui = aria-current="date", distinct de la sélection. Un seul jour dans l’ordre de tabulation, flèches ← → pour changer de jour, ↑ ↓ pour changer de semaine. Les cellules du calendrier sont des boutons de 44px minimum, jamais des span. Fermeture du calendrier : Échap, clic extérieur, focus restitué au déclencheur.
déclencheur [popovertarget] + <div popover class="popover" role="dialog" aria-labelledby> · natif : couche supérieure, Échap, clic extérieur
dialog.sheet
<dialog class="sheet sheet--bottom|--right"> ouvert par showModal() · <vzr-dialog> n’ajoute que le glissement et le verrou de défilement
dialog.modal
<dialog class="modal modal--danger" aria-labelledby aria-describedby> · actions dans <form method="dialog"> · autofocus sur Annuler
Ne signifie pas donnée manquante : le modèle identifie un risque élevé sur ce pari, la probabilité estimée reste faible.
Lire l’article de méthodologie →Ne signifie pas donnée manquante : le modèle identifie un risque élevé sur ce pari.
Lire l’article de méthodologie →3 notifications non lues
popover + popovertarget pour le non modal, <dialog> + showModal() pour les feuilles et modales. Il n’y a plus d'Overlay maison à écrire : voile ::backdrop, Échap, clic extérieur, couche supérieure, piège de focus et inert sont fournis par le navigateur. Popover ⇄ Sheet : même contenu, choix par point de rupture — un seul contenu, la bascule se fait par matchMedia dans le composant, pas dans les pages.<dialog> ouvert par showModal() apporte le rôle, la modalité, le piège de focus, Échap, inert et la restitution du focus ; open posé à la main ne les apporte pas. aria-labelledby obligatoire, aria-describedby pour la modale destructive. Focus initial sur le premier élément focalisable — jamais sur le bouton destructeur (autofocus sur « Annuler »). Voile ::backdrop en --overlay : ferme au clic, mais toujours doublé d’un bouton « Fermer » réel de 44px, focalisable. Le titre de la modale destructive pose la question, le bouton nomme l’action (« Supprimer »), pas « OK ».sheet--right que le panneau de menu, ouverte par la cloche : le déclencheur porte aria-haspopup="dialog", aria-expanded, aria-controls="notif-sheet" et un libellé qui contient le compte (« Notifications, 3 non lues ») — la bulle de compteur reste aria-hidden. Chaque ligne non lue est doublée d’un .sr-only « Non lue. » en plus de la pastille d’accent, « Tout marquer comme lu » annonce son effet dans une région role="status", et l’état « non lue » n’est jamais porté par la seule couleur.
<section class="share" aria-labelledby> · .label + ul.share__list > li > a|button.icon-btn.icon-btn--outlined
vzr-share
<vzr-share> autour du bloc · lit l’URL dans <link rel="canonical"> · presse-papiers → toast(…, {tone:'success'}) · repli : liens réels
Ordre invariable : lien, Facebook, X, puis le bouton système (affiché uniquement si navigator.share existe). Le bouton « lien » copie l’adresse canonique : pas de champ URL à afficher.
.msg--toastAucun composant propre à .share : la copie appelle toast('Lien de l’article copié.', { tone: 'success' }) et s’affiche dans le <vzr-toasts> de la coque, ton danger si le presse-papiers refuse (le message porte alors l’adresse en clair).
.share pour l’article, et demain pour un sujet de forum ou une fiche de conseil : seuls l’URL et le titre changent, et ils ne sont écrits qu’une fois — <vzr-share> lit <link rel="canonical"> et document.title, jamais une URL recopiée dans le gabarit (source unique, et donc pas de partage d’une adresse avec paramètres de filtre). Les boutons réutilisent .icon-btn avec le modificateur --outlined (filet 1px --border-fn, 44px, survol en accent) : aucune nouvelle forme de bouton. Les logos viennent de Phosphor (facebook-logo, x-logo, link, share-network), à la même taille de 20px que le reste de l’interface — aucun logo redessiné, aucun aplat de couleur de marque. La confirmation réutilise .msg--toast et <vzr-toasts> : rien de spécifique au partage.sharer.php) et X (intent/post). Instagram n’en expose aucun — pas d’icône Instagram, plutôt qu’un bouton qui copierait l’adresse en promettant un partage. Le partage vers Instagram passe par le bouton système sur mobile, où la feuille de partage de l’appareil propose la vraie entrée Instagram. Même règle pour tout réseau ajouté plus tard : pas de point de partage documenté, pas d’icône.<section aria-labelledby> dont le titre visible (« Partager cet article ») sert d’étiquette, et la <ul> annonce le nombre d’options. Chaque commande sans texte porte un aria-label qui nomme le réseau et l’effet (« (nouvelle fenêtre) », « copier le lien de l’article »), le glyphe étant aria-hidden. La copie est annoncée par le toast, dans la région role="status" présente avant l’action — jamais un simple changement d’icône ou de couleur ; auto-effacement à ~3s. Cibles de 44px, focus visible :focus-visible. Liens externes en target="_blank" rel="noopener". Le bouton système n’est rendu que si l’API existe : pas de bouton mort.<a href> vers leur point de partage : ils fonctionnent sans JavaScript. Seuls « Copier le lien » et « Autre application » dépendent du script, et <vzr-share> les retire (hidden) si le presse-papiers ou navigator.share n’est pas disponible — l’adresse reste alors accessible par la barre du navigateur.
<head> : og:* + twitter:card · rendu attendu chez Facebook, X et les messageries · gabarit d’image 1200 × 630
og:type=articleGrande image imposée par og:image ≥ 1200 × 630 (sous 600px de large, Facebook rétrograde en vignette carrée). Domaine en capitales : ajouté par la plateforme, pas par nous. Titre coupé vers 88 caractères, description vers 200 — et souvent masquée sur mobile : jamais d’information indispensable dans la description.
summary_large_imageMême image que Facebook — un seul fichier à produire. X arrondit la carte et place le domaine sous le texte : le rayon et cet ordre appartiennent à la plateforme, on ne les imite nulle part dans le site. Titre coupé vers 70 caractères. Sans twitter:card, X retombe sur la vignette carrée summary.
Le lien copié par le bouton « lien » ne contrôle rien du rendu : chaque messagerie recadre l’image en vignette et tronque plus court. D’où deux exigences sur l’URL elle-même — lisible (slug en clair, pas d’identifiant ni de paramètre de suivi) et suffisamment parlante nue, puisque certains contextes (SMS, e-mail texte) n’affichent jamais de carte.
<head><link rel="canonical" href="https://vizir.bet/blog/lire-une-cote-de-bookmaker"> <title>Comment lire une cote de bookmaker avant un match — vizir.bet</title> <meta name="description" content="Décimale, fractionnaire, américaine : …"> <meta property="og:type" content="article"> <meta property="og:site_name" content="vizir.bet"> <meta property="og:locale" content="fr_FR"> <meta property="og:url" content="https://vizir.bet/blog/lire-une-cote-de-bookmaker"> <meta property="og:title" content="Comment lire une cote de bookmaker avant un match"> <meta property="og:description" content="Décimale, fractionnaire, américaine : …"> <meta property="og:image" content="https://vizir.bet/og/lire-une-cote-de-bookmaker.jpg"> <meta property="og:image:width" content="1200"> <meta property="og:image:height" content="630"> <meta property="og:image:alt" content="Trois formats de cote pour un même match, côte à côte."> <meta name="twitter:card" content="summary_large_image"> <meta name="twitter:site" content="@vizirbet">
og:* en repli : on n’écrit pas de twitter:title, twitter:description ni twitter:image — seuls twitter:card et twitter:site, qui n’ont pas d’équivalent Open Graph. Une image, un titre, une description, une URL : les mêmes valeurs que <title>, le h1 et le chapeau de l’article, générées par le gabarit, jamais ressaisies à la main. <vzr-share> lit ce même canonical (§ .share) : une seule adresse pour le partage, les cartes et le référencement.og:image:alt est obligatoire : c’est le seul texte alternatif que les lecteurs d’écran obtiennent quand la carte est rendue par la plateforme, hors de notre HTML. Il décrit l’image, pas l’article, et ne redit pas le titre.= .skip-link + header.app-header + main#content + nav.bottom-nav (< 768px) + .compliance + footer.app-footer + <vzr-toasts> · vrai HTML dans chaque page (inclusion au build), jamais généré en JS
Icône réversible (soleil / lune), légère rotation au clic, thème persisté en localStorage et appliqué via data-theme.
Mockups Page Accueil : les 4 exports fournis avaient une large marge transparente autour de l’appareil — recadrés à leur contenu visible pour retrouver la taille d’affichage d’origine dans le hero.
header.app-header, pas deux gabarits. .nav-item est partagé par l’en-tête et la barre basse (icône + libellé + état actif). footer.app-footer rend les mêmes données en colonnes ≥ 768px et en <details class="disclosure--footer"> en dessous, par CSS seul. Le bandeau légal et <vzr-toasts> font partie de la coque incluse dans chaque page — <vzr-toasts> doit exister avant toute annonce, sinon rien n’est lu..skip-link : premier élément focalisable, visible au focus, cible <main id="content">. Navigations dans <nav aria-label> distinct (« Navigation principale », « Pied de page ») et listes en ul>li, jamais en span non cliquables. Page courante = aria-current="page", jamais la couleur seule. Barre basse : 48px de haut, libellé texte conservé sous l’icône. Le bandeau de conformité est du contenu, pas une région ARIA. Ordre invariable des actions d’en-tête : cloche, menu, CTA.
.state-screen : un gabarit pour sept écrans<section class="state-screen" data-tone="neutral|danger"> · --center · .state-screen__eyebrow · rendu côté serveur avec le bon code HTTP
Les conseils turf, football et tennis, avec taux de confiance et cote, sont accessibles avec un abonnement vizir.bet.
Votre formule Turf ne couvre pas les conseils Sports. Passez à la formule Tout pour y accéder.
Votre accès au forum a été suspendu le 14 octobre pour non-respect des règles. Vos accès aux conseils restent actifs.
Cette page est réservée aux utilisateurs disposant d’un compte.
Le lien est peut-être erroné, ou la page a été déplacée. Les conseils et les résultats, eux, sont toujours là.
Les conseils Sports reprennent demain à partir de 9h.
| Cas d’usage | icon | tone / align | actions |
|---|---|---|---|
| Réservé aux abonnés | lock-simple | neutral / start | Voir les formules |
| Formule partielle | lock-simple | neutral / start | Changer + Comparer |
| Compte suspendu | prohibit | danger / start | Contacter le support |
| Abonnement expiré | clock-counter-clockwise | neutral / start | Réactiver + Formules |
| Erreur 403 | lock-key | neutral / center + eyebrow | Connexion + Inscription |
| Liste vide | calendar-x | neutral / center | — |
| Erreur 404 | compass | neutral / center + eyebrow | Accueil + Conseils du jour |
Message variant="inline" : c’est un incident transitoire, pas un état de page.h1 de la page quand l’écran remplace tout le contenu (403, 404, compte suspendu), un h2 quand il remplace une section. tone="danger" ne colore que l’icône : le rouge reste réservé à la suspension de compte et aux actions destructrices. L’écran qui remplace une page doit aussi mettre à jour le <title> du document et être annoncé au changement de route.
div / span / <i> : croix de fermeture, pastilles de filtre, entrées de menu et de navigation, cellules de calendrier, bascule de thème → vrais button / a.<table> avec caption et th scope.role="tablist" détourné pour des choix de formulaire → radiogroup, avec tabindex tournant et navigation aux flèches.aria-pressed → radiogroup + aria-current="date" pour aujourd’hui.:focus-visible) au lieu d’un seul état de bouton documenté..sr-only.aria-invalid, aria-describedby, icône, bordure 2px.role="status" / role="alert", toujours annoncés.role="dialog", aria-modal, piège de focus, Échap, restitution du focus, focus initial jamais sur l’action destructrice.<main id="content">, nav aria-label, aria-current="page".h1 → h3 ; libellés d’icônes seules ; cibles 44px partout (barre basse à 48px).prefers-reduced-motion respecté (chevrons, toasts, transitions).<html lang="fr"> et <title> unique par route ; annonce du changement de page au routage client.dense, elle ne défile pas horizontalement.--text-2 ≥ 4,5:1 sur --bg et --surface dans les deux thèmes.placeholder seul), autocomplete renseigné, erreurs regroupées en role="alert" à la soumission.aria-live="polite" unique, jamais une annonce par ligne..state-screen remplace le contenu côté serveur.Le détail complet — jetons, API d’attributs, arborescence de fichiers, classe de base des éléments personnalisés, carte page → composants, plan SEO et ordre de construction — est dans DESIGN-SYSTEM.md et tokens.css à la racine du projet.
Le choix d’une stack HTML classique est un avantage SEO décisif à condition de ne pas le gâcher : ce qui rend le site indexable est exactement ce qui le rend accessible et utilisable sans JavaScript. Une seule discipline à tenir pour les trois.
vzr- n’injectent jamais de contenu textuel indexable : ils enrichissent du HTML déjà correct.span reste invisible pour un robot.<a href> réel.curl la page — le h1 et le contenu doivent y être ; JS désactivé ne doit pas vider la page.vzr-…:not(:defined) plutôt que les masquer : zéro CLS, zéro contenu invisible.<html lang="fr">, title unique (≤ 60 car.) et description (≤ 155) rédigés à la main pour chaque page.canonical absolue sur toutes les pages, y compris les variantes filtrées.twitter:card, theme-color par schéma de couleurs (clair et sombre).h1, hiérarchie sans saut, repères header / nav / main / footer./turf/2026-10-14, filtres en paramètres de requête avec canonical vers la page mère.| Accueil | Organization + WebSite |
|---|---|
| Article | BlogPosting |
| Formules | Service + Offer |
| FAQ | FAQPage si visible dans la page |
| Sujet de forum | DiscussionForumPosting |
| Pages profondes | BreadcrumbList |
sitemap.xml généré au build avec un lastmod réel, pages noindex exclues ; robots.txt fermant /paiement et /profil.
.state-screen remplace le contenu côté serveur, avec le bon code HTTP.isAccessibleForFree: false + hasPart avec cssSelector: ".paywalled". Le début lisible doit être réellement lisible.noindex, follow, plus une page vitrine publique et indexable par discipline qui explique la méthodologie./turf/2026-10-14, avec le ROI constaté : meilleure preuve de crédibilité et première source de trafic organique. Le jour J reste réservé.woff2 en preload, font-display: swap, sous-ensemble latin. Pas d’appel à Google Fonts en production.width et height systématiques (zéro CLS), loading="lazy" sauf la première image visible, AVIF/WebP avec repli.type="module" (différé par nature). Budget indicatif : < 30 ko compressés pour tous les composants — atteignable puisque <details>, <dialog> et popover sont natifs. Seule exception synchrone : les six lignes anti-flash du thème.rel="nofollow sponsored noopener". Pages légales et jeu responsable réelles, indexables, liées depuis chaque pied de page.Ressources de marque fournies par Apple et Google, non modifiables : ni recoloration, ni recadrage, ni ajout d’ombre ou de contour, ni traduction du libellé. Version française, badge noir pour l’App Store et badge couleur pour Google Play, alignés sur une même hauteur de 46px. Ils s’emploient toujours par paire, dans l’ordre App Store puis Google Play.
Hauteur de référence 46px, jamais sous 40px. Écart de 12px entre les deux badges, marge libre d’au moins un quart de la hauteur du badge tout autour.
Les deux badges portent leur propre fond : ils restent lisibles sur l’accent des deux thèmes, sans variante à produire.
/assets/img/badge-app-store-fr.svg et /assets/img/badge-google-play-fr.svg.<a> vers la fiche de l’application, avec width et height sur l’<img> (zéro CLS).aria-label redondant sur l’image.Règle non négociable : toute la ponctuation est française. Aucun texte livré — libellé d’interface, corps d’article, message système, gabarit de données — ne doit porter de ponctuation anglophone ni de tic de rédaction reconnaissable comme généré par une IA. À chaque génération de texte, relire les caractères un par un : un seul symbole fautif suffit à trahir la source.
« … » pour toute citation ou terme rapporté, avec espace insécable après « et avant ». Jamais " … ".' dans tout mot élidé (l’article, d’un, aujourd’hui) — jamais l’apostrophe droite '.: ; ! ?, et comme séparateur de milliers (10 000).4,5:1, 2,10), jamais le point décimal anglo-saxon.— espacé pour une incise réelle ; tiret demi-cadratin – collé pour un intervalle ou un séparateur de champs.…, jamais trois points bruts." " ou droits " ", apostrophe droite '.✓, étoile ★, flèche → en fil de texte courant (hors CTA de navigation déjà établi type « Lire plus → »).* pour l’emphase : utiliser <strong> / <em>.• en prose, hors véritables listes <ul>.