QC Signal — design system de QC Élection Forecast
Version 4 (2026-08-30). Direction : editorial intelligence + election command center + scientific forecasting + identité québécoise. La sensation recherchée : « je regarde un instrument de mesure de l'élection ». À proscrire : dashboard SaaS générique, admin panel, esthétique crypto.
Tout vit dans deux fichiers partagés :
frontend/assets/style.css— tokens + composants (source de vérité visuelle)frontend/assets/app.js— coquille (initShell), composants JS, thème ECharts « qc-signal »
1. Tokens
Couleurs de surface (palettes dédiées, le dark n'est PAS une inversion)
| Token | Dark | Light | Rôle |
|---|---|---|---|
--bg |
#0a0d14 |
#eef0f4 |
fond de page (bleu-noir profond / gris papier) |
--surface |
#0f131c |
#f5f6fa |
fonds encaissés (meter, pistes) |
--card |
#131824 |
#ffffff |
cartes |
--card-2 |
#1a2030 |
#edeff5 |
surfaces secondaires (champs, chips, hover) |
--ink |
#edeff5 |
#161b26 |
texte principal (blanc cassé / encre) |
--ink-2 / --muted |
#aab2c5 / #7d8699 |
#414d68 / #67718a |
texte secondaire / tertiaire |
--line / --border |
rgba(160,178,210,.10) |
rgba(38,54,92,.10) |
bordure discrète par défaut |
--border-strong |
rgba(170,188,218,.22) |
rgba(38,54,92,.22) |
bordure appuyée (hover, th) |
--accent / --accent-strong |
#94a9d3 / #5c7ec2 |
#3d5a94 / #35507e |
bleu institutionnel politiquement neutre |
--good / --warn / --danger |
états sémantiques SEULEMENT (statut pipeline, badges) — jamais pour un mouvement de parti |
Principe : espace / contraste / surface avant bordure. Les bordures sont des rgba
faibles; l'appui visuel vient des surfaces (--card vs --bg) et des ombres discrètes
--shadow-sm / --shadow-md (jamais de gros box-shadow).
Couleurs des partis — DONNÉES SEULEMENT
--caq #0097cf · --plq #e5484d · --pq #3f7ad6 · --qs #e06c1f · --pcq #7069d6 · --aut #ad8226
Validées daltonisme/contraste sur les deux surfaces. Interdiction de les réutiliser pour
le chrome du site, et interdiction de modifier ces valeurs. Toujours accompagnées du sigle
texte (jamais la couleur seule pour identifier un parti). Accès JS : partyColor(p).
Radius, espacement, motion
- Radius :
--r-8, --r-12, --r-16, --r-20, --r-24, --r-full(8/12/16/20/24/999).--radius= 16 (cartes),--radius-sm= 8 (champs, boutons). - Espacement : grille 4/8 px —
--s-1…--s-12(4, 8, 12, 16, 20, 24, 32, 40, 48). - Motion :
--t-fast130 ms,--t-med200 ms,--easecubic-bezier(.25,.6,.3,1). Aucune transition > 220 ms.prefers-reduced-motioncoupe tout (CSS global + ECharts viabaseChart).
2. Typographie
Inter uniquement (400–900). font-variant-numeric: tabular-nums sur tout chiffre
(kv, tables, metrics, deltas, strip, tooltips ECharts).
| Classe | Usage | Taille |
|---|---|---|
.t-display |
titre display | clamp ~27→40 px, 800 |
.hero h1 / .headline |
H1 de page / manchette éditoriale | clamp ~23→32 / ~24→40 px |
h2.section |
titre de section | 1.16 rem, 750 |
.card h3 / .label-up |
étiquette de carte | .76 rem, uppercase, letterspacing .09 em |
.metric-xl |
métrique héroïque | clamp → 64 px, 800, tabular |
.bignum / .pcard .p-main .v |
métrique de carte | 2.25 / 2.4 rem |
| corps | .sub, .kv, tables |
.84–.98 rem |
.caption / .note / .chart-note |
légendes | .76–.78 rem |
3. Coquille partagée (initShell(active))
Sur chaque page : <header class="site"> + <footer class="site"> vides, puis
initShell("/route") monte :
- Navbar — fleur-de-lys SVG (
FLEUR_SVG, courant =--accent-strong), wordmark « QC Élection Forecast », liens desktop, indicateur « Dernière MAJ hh:mm » (#hdr-updated, ≥1250 px), bascule de thème SVG (aria-label), hamburger ≤980 px (tiroir avec verrou de scroll, Escape, backdrop). - Forecast strip (
#fstrip) — fine barre sticky sous la nav, toutes pages. Alimentée par/api/today(repli/api/forecast/latest) :P(PREMIER) · PQ 78 ▼9.9 · CAQ 15 ▲9.7 …— sigle en couleur du parti, valeur = P(1er) publiée, flèche neutre = variation vs veille en points. Défilement horizontal en mobile,Scrutin 5 oct. · J−nà droite (desktop). - Footer — mission de neutralité + crédits.
Le thème est lu avant le premier paint par le one-liner en tête de chaque page
(localStorage["qce-theme"] → data-theme sur <html>). toggleTheme() recharge la
page (les canvas ECharts reprennent les couleurs).
4. Grammaire des données (langage + composants JS)
Vocabulaire uniforme : « Chance d'arriver premier » · « Sièges attendus » · « Vote projeté » · « Majorité ». Jamais « X va gagner ».
| Helper (app.js) | Rendu | Règles |
|---|---|---|
deltaHTML(d, {unit, digits, color, thresh}) |
▲ 4.1 pt / ▼ … / · stable |
JAMAIS vert/rouge : couleur du parti ou neutre (--muted). Une hausse d'un parti n'est pas « positive » universellement. |
rangeHTML({lo, hi, val, min, max, mark, color}) |
24 ───●─── 86 |
intervalle visuel; mark = repère (majorité 64); dot = valeur centrale; aria-label complet |
hemicycleSVG(seats, meta) |
hémicycle 127 sièges | arc demi-cercle 6 rangées (sièges ∝ rayon), allocation entière sur p50 (reste au plus fort), marqueur pointillé « majorité 64 » à l'angle du 64ᵉ siège, hover = parti/sièges/intervalle + dim des autres, légende sigle+valeur+IC |
emptyStateHTML(msg) |
état vide propre | « Données temporairement indisponibles. » — jamais de undefined/NaN à l'écran |
avatarHTML / initialsAvatar / partyLogo |
visuels secondaires | petits, jamais porteurs d'information seuls |
Autres composants CSS : .outcome (main outcome 2 colonnes), .outcome-bar (issues
segmentées — uniquement des probabilités du modèle : prob_majority,
prob_minority_win, complément), .pcard (carte parti compacte data-first),
.meter (baromètre sièges), .table-scroll (tables hautes, en-tête sticky —
table.data th { position: sticky }), .skel / .loading (squelettes shimmer),
.chip / .badge-aux, .map-layout (~75 % carte / 25 % panneau, empilé ≤980 px).
5. Conventions charts (thème « qc-signal », ECharts CDN uniquement)
Toujours partir des exports d'app.js — jamais de styles ad hoc :
baseChart(el)— Inter, animation 420 ms (0 si reduced motion), auto-resize.AXIS_STYLE— pas d'axisLine/tick; labelsMUTED11 px; grilleSPLIT_COLOR(recessive, plus claire que le texte).TOOLTIP— fond solide arrondi 12 px, bordure token, ombre portée, chiffres tabulaires, axisPointer en croix pointillée.LEGEND— puces rondes 10 px, texteINK2.- Séries :
lineGlow(color, 2–2.5)(trait arrondi + halo de sa propre couleur),areaGrad(color, .10–.28)(dégradé vertical, jamais d'aplat),hexA(hex, a). - Couleur de série = TOUJOURS
partyColor(p); séries neutres :#8fa3c8/GRID_COLOR. Repères (majorité 64, aujourd'hui) :markLinepointilléeMUTED. - Constantes de thème :
DARKMODE, GRID_COLOR, SPLIT_COLOR, INK2, MUTED, CHART_FONT.
6. Accessibilité & robustesse
- Focus visible global (
:focus-visible2 px--accent-strong). aria-labelsur burger, bascule de thème, hémicycle, ranges;aria-current="page"sur le lien actif; drawerrole="dialog".- Parti = sigle texte + couleur, jamais couleur seule; flèches de variation doublées du signe/valeur.
- Anti-zoom iOS : tout champ à 16 px min sous 640 px.
html, body { overflow-x: clip }(le tiroir hors-écran ne crée jamais de scroll) et.grid > * { min-width: 0 }(les canvas ECharts ne verrouillent jamais une colonne de grille).- Breakpoints : 1560 (nav compacte), 1250 (MAJ masquée), 1120 (nav dense), 980 (burger + colonnes empilées), 900, 640 (mobile), 400 (cartes 1 colonne).
7. Interdits
- Toucher aux couleurs des partis, ou les utiliser hors données.
- Vert/rouge pour un mouvement de parti.
- Nouvelle dépendance JS (ECharts CDN seulement), nouvelle police.
- Réordonner le DOM entre breakpoints (
order:,column-reverse). - Modifier un contrat d'API ou recalculer une probabilité côté client.