+ Annexe du dossier projet. Chaque carte ci-dessous est rendue avec les
+ couleurs de sa propre palette (fond, texte et bordure réels) ; chaque
+ jeton indique son ratio de contraste WCAG calculé contre son fond de
+ référence, avec le verdict AA (texte : 4,5:1 ; composant d'interface :
+ 3:1). « décor » signale les jetons de surface sans exigence de contraste
+ propre. Les valeurs proviennent directement des feuilles de style du
+ thème ; ce fichier est généré par
+ scripts/generate_palette_diagrams.py --html.
+
+
Catppuccin
+
+
+
Catppuccin Latte (light)
+
+
bg
#eff1f5
décor
+
surface
#e6e9ef
décor
+
surface-deep
#dce0e8
décor
+
border
#ccd0da
décor
+
text
#4c4f69
7.06:1 sur bg
AA ✓
+
text-muted
#5c5f77
5.53:1 sur bg
AA ✓
+
primary
#8839ef
4.79:1 sur bg
AA ✓
+
on-primary
#ffffff
5.41:1 sur primary
AA ✓
+
link
#1a5cd7
5.22:1 sur bg
AA ✓
+
success
#2f7a1f
4.73:1 sur bg
AA ✓
+
warning
#8f5b08
5.06:1 sur bg
AA ✓
+
error
#d20f39
4.80:1 sur bg
AA ✓
+
focus
#1e66f5
4.34:1 sur bg
UI 3:1 ✓
+
primary-soft
#eadcfd
décor
+
code-bg
#1e1e2e
décor
+
code-text
#cdd6f4
11.34:1 sur code-bg
AA ✓
+
code-keyword
#cba6f7
8.07:1 sur code-bg
AA ✓
+
code-string
#a6e3a1
11.03:1 sur code-bg
AA ✓
+
code-function
#89b4fa
7.79:1 sur code-bg
AA ✓
+
code-comment
#9399b2
5.81:1 sur code-bg
AA ✓
+
+
+
+
Catppuccin Mocha (dark)
+
+
bg
#1e1e2e
décor
+
surface
#181825
décor
+
surface-deep
#11111b
décor
+
border
#313244
décor
+
text
#cdd6f4
11.34:1 sur bg
AA ✓
+
text-muted
#bac2de
9.26:1 sur bg
AA ✓
+
primary
#cba6f7
8.07:1 sur bg
AA ✓
+
on-primary
#11111b
9.23:1 sur primary
AA ✓
+
link
#89b4fa
7.79:1 sur bg
AA ✓
+
success
#a6e3a1
11.03:1 sur bg
AA ✓
+
warning
#f9e2af
12.91:1 sur bg
AA ✓
+
error
#f38ba8
7.08:1 sur bg
AA ✓
+
focus
#89b4fa
7.79:1 sur bg
UI 3:1 ✓
+
primary-soft
#2b2440
décor
+
code-bg
#11111b
décor
+
code-text
#cdd6f4
12.97:1 sur code-bg
AA ✓
+
code-keyword
#cba6f7
9.23:1 sur code-bg
AA ✓
+
code-string
#a6e3a1
12.61:1 sur code-bg
AA ✓
+
code-function
#89b4fa
8.91:1 sur code-bg
AA ✓
+
code-comment
#9399b2
6.64:1 sur code-bg
AA ✓
+
+
+
+
Soft Paper
+
+
+
Soft Paper light
+
+
bg
#eee6dd
décor
+
surface
#e6dbd1
décor
+
surface-deep
#ddd0c6
décor
+
border
#dcd3cb
décor
+
text
#575279
5.89:1 sur bg
AA ✓
+
text-muted
#525252
6.32:1 sur bg
AA ✓
+
primary
#286983
4.94:1 sur bg
AA ✓
+
on-primary
#ffffff
6.11:1 sur primary
AA ✓
+
link
#286983
4.94:1 sur bg
AA ✓
+
success
#2f6a4a
5.18:1 sur bg
AA ✓
+
warning
#7d570c
5.25:1 sur bg
AA ✓
+
error
#94425a
5.34:1 sur bg
AA ✓
+
focus
#286983
4.94:1 sur bg
UI 3:1 ✓
+
primary-soft
#dfe9ec
décor
+
code-bg
#303446
décor
+
code-text
#c6ceef
7.90:1 sur code-bg
AA ✓
+
code-keyword
#bb93d6
4.84:1 sur code-bg
AA ✓
+
code-string
#67c48f
5.79:1 sur code-bg
AA ✓
+
code-function
#8caaee
5.34:1 sur code-bg
AA ✓
+
code-comment
#838ba7
3.65:1 sur code-bg
AA ✗
+
+
+
+
Soft Paper dark
+
+
bg
#303446
décor
+
surface
#292c3c
décor
+
surface-deep
#232634
décor
+
border
#414459
décor
+
text
#c6ceef
7.90:1 sur bg
AA ✓
+
text-muted
#b5bddc
6.61:1 sur bg
AA ✓
+
primary
#8caaee
5.34:1 sur bg
AA ✓
+
on-primary
#232634
6.51:1 sur primary
AA ✓
+
link
#8caaee
5.34:1 sur bg
AA ✓
+
success
#67c48f
5.79:1 sur bg
AA ✓
+
warning
#c9be3e
6.40:1 sur bg
AA ✓
+
error
#e78284
4.65:1 sur bg
AA ✓
+
focus
#8caaee
5.34:1 sur bg
UI 3:1 ✓
+
primary-soft
#3b415c
décor
+
code-bg
#232634
décor
+
code-text
#c6ceef
9.64:1 sur code-bg
AA ✓
+
code-keyword
#bb93d6
5.91:1 sur code-bg
AA ✓
+
code-string
#67c48f
7.06:1 sur code-bg
AA ✓
+
code-function
#8caaee
6.51:1 sur code-bg
AA ✓
+
code-comment
#838ba7
4.45:1 sur code-bg
AA ✗
+
+
+
+
+
+
diff --git a/docs/design/theme-exploration.md b/docs/design/theme-exploration.md
index 045087e..180b7df 100644
--- a/docs/design/theme-exploration.md
+++ b/docs/design/theme-exploration.md
@@ -79,6 +79,139 @@ semantic colors are darkened to pass 4.5:1.
Character: calm, warm, reflective; reads like paper. Lower-key than
Catppuccin, closer to a reading environment than an interactive app.
+## Color palettes
+
+
+
+Every swatch below comes straight from the theme stylesheets;
+GitHub renders these Mermaid blocks with the real colors, so no
+image is committed. Values are the shipped (contrast-adjusted)
+tokens, not the upstream palettes. A standalone HTML version for
+the dossier, with each token's computed contrast ratio and AA
+verdict, lives in [palettes.html](palettes.html) (same generator,
+`--html` flag).
+
+### Catppuccin Latte (light)
+
+```mermaid
+flowchart LR
+ t0["bg #eff1f5"] ~~~ t1["surface #e6e9ef"] ~~~ t2["surface-deep #dce0e8"] ~~~ t3["border #ccd0da"] ~~~ t4["text #4c4f69"]
+ t5["text-muted #5c5f77"] ~~~ t6["primary #8839ef"] ~~~ t7["on-primary #ffffff"] ~~~ t8["link #1a5cd7"] ~~~ t9["success #2f7a1f"]
+ t10["warning #8f5b08"] ~~~ t11["error #d20f39"] ~~~ t12["focus #1e66f5"] ~~~ t13["primary-soft #eadcfd"] ~~~ t14["code-bg #1e1e2e"]
+ t15["code-text #cdd6f4"] ~~~ t16["code-keyword #cba6f7"] ~~~ t17["code-string #a6e3a1"] ~~~ t18["code-function #89b4fa"] ~~~ t19["code-comment #9399b2"]
+ style t0 fill:#eff1f5,stroke:#7f7f7f,color:#11111b
+ style t1 fill:#e6e9ef,stroke:#7f7f7f,color:#11111b
+ style t2 fill:#dce0e8,stroke:#7f7f7f,color:#11111b
+ style t3 fill:#ccd0da,stroke:#7f7f7f,color:#11111b
+ style t4 fill:#4c4f69,stroke:#7f7f7f,color:#f8f8f8
+ style t5 fill:#5c5f77,stroke:#7f7f7f,color:#f8f8f8
+ style t6 fill:#8839ef,stroke:#7f7f7f,color:#f8f8f8
+ style t7 fill:#ffffff,stroke:#7f7f7f,color:#11111b
+ style t8 fill:#1a5cd7,stroke:#7f7f7f,color:#f8f8f8
+ style t9 fill:#2f7a1f,stroke:#7f7f7f,color:#f8f8f8
+ style t10 fill:#8f5b08,stroke:#7f7f7f,color:#f8f8f8
+ style t11 fill:#d20f39,stroke:#7f7f7f,color:#f8f8f8
+ style t12 fill:#1e66f5,stroke:#7f7f7f,color:#f8f8f8
+ style t13 fill:#eadcfd,stroke:#7f7f7f,color:#11111b
+ style t14 fill:#1e1e2e,stroke:#7f7f7f,color:#f8f8f8
+ style t15 fill:#cdd6f4,stroke:#7f7f7f,color:#11111b
+ style t16 fill:#cba6f7,stroke:#7f7f7f,color:#11111b
+ style t17 fill:#a6e3a1,stroke:#7f7f7f,color:#11111b
+ style t18 fill:#89b4fa,stroke:#7f7f7f,color:#11111b
+ style t19 fill:#9399b2,stroke:#7f7f7f,color:#f8f8f8
+```
+
+### Catppuccin Mocha (dark)
+
+```mermaid
+flowchart LR
+ t0["bg #1e1e2e"] ~~~ t1["surface #181825"] ~~~ t2["surface-deep #11111b"] ~~~ t3["border #313244"] ~~~ t4["text #cdd6f4"]
+ t5["text-muted #bac2de"] ~~~ t6["primary #cba6f7"] ~~~ t7["on-primary #11111b"] ~~~ t8["link #89b4fa"] ~~~ t9["success #a6e3a1"]
+ t10["warning #f9e2af"] ~~~ t11["error #f38ba8"] ~~~ t12["focus #89b4fa"] ~~~ t13["primary-soft #2b2440"] ~~~ t14["code-bg #11111b"]
+ t15["code-text #cdd6f4"] ~~~ t16["code-keyword #cba6f7"] ~~~ t17["code-string #a6e3a1"] ~~~ t18["code-function #89b4fa"] ~~~ t19["code-comment #9399b2"]
+ style t0 fill:#1e1e2e,stroke:#7f7f7f,color:#f8f8f8
+ style t1 fill:#181825,stroke:#7f7f7f,color:#f8f8f8
+ style t2 fill:#11111b,stroke:#7f7f7f,color:#f8f8f8
+ style t3 fill:#313244,stroke:#7f7f7f,color:#f8f8f8
+ style t4 fill:#cdd6f4,stroke:#7f7f7f,color:#11111b
+ style t5 fill:#bac2de,stroke:#7f7f7f,color:#11111b
+ style t6 fill:#cba6f7,stroke:#7f7f7f,color:#11111b
+ style t7 fill:#11111b,stroke:#7f7f7f,color:#f8f8f8
+ style t8 fill:#89b4fa,stroke:#7f7f7f,color:#11111b
+ style t9 fill:#a6e3a1,stroke:#7f7f7f,color:#11111b
+ style t10 fill:#f9e2af,stroke:#7f7f7f,color:#11111b
+ style t11 fill:#f38ba8,stroke:#7f7f7f,color:#11111b
+ style t12 fill:#89b4fa,stroke:#7f7f7f,color:#11111b
+ style t13 fill:#2b2440,stroke:#7f7f7f,color:#f8f8f8
+ style t14 fill:#11111b,stroke:#7f7f7f,color:#f8f8f8
+ style t15 fill:#cdd6f4,stroke:#7f7f7f,color:#11111b
+ style t16 fill:#cba6f7,stroke:#7f7f7f,color:#11111b
+ style t17 fill:#a6e3a1,stroke:#7f7f7f,color:#11111b
+ style t18 fill:#89b4fa,stroke:#7f7f7f,color:#11111b
+ style t19 fill:#9399b2,stroke:#7f7f7f,color:#f8f8f8
+```
+
+### Soft Paper light
+
+```mermaid
+flowchart LR
+ t0["bg #eee6dd"] ~~~ t1["surface #e6dbd1"] ~~~ t2["surface-deep #ddd0c6"] ~~~ t3["border #dcd3cb"] ~~~ t4["text #575279"]
+ t5["text-muted #525252"] ~~~ t6["primary #286983"] ~~~ t7["on-primary #ffffff"] ~~~ t8["link #286983"] ~~~ t9["success #2f6a4a"]
+ t10["warning #7d570c"] ~~~ t11["error #94425a"] ~~~ t12["focus #286983"] ~~~ t13["primary-soft #dfe9ec"] ~~~ t14["code-bg #303446"]
+ t15["code-text #c6ceef"] ~~~ t16["code-keyword #bb93d6"] ~~~ t17["code-string #67c48f"] ~~~ t18["code-function #8caaee"] ~~~ t19["code-comment #838ba7"]
+ style t0 fill:#eee6dd,stroke:#7f7f7f,color:#11111b
+ style t1 fill:#e6dbd1,stroke:#7f7f7f,color:#11111b
+ style t2 fill:#ddd0c6,stroke:#7f7f7f,color:#11111b
+ style t3 fill:#dcd3cb,stroke:#7f7f7f,color:#11111b
+ style t4 fill:#575279,stroke:#7f7f7f,color:#f8f8f8
+ style t5 fill:#525252,stroke:#7f7f7f,color:#f8f8f8
+ style t6 fill:#286983,stroke:#7f7f7f,color:#f8f8f8
+ style t7 fill:#ffffff,stroke:#7f7f7f,color:#11111b
+ style t8 fill:#286983,stroke:#7f7f7f,color:#f8f8f8
+ style t9 fill:#2f6a4a,stroke:#7f7f7f,color:#f8f8f8
+ style t10 fill:#7d570c,stroke:#7f7f7f,color:#f8f8f8
+ style t11 fill:#94425a,stroke:#7f7f7f,color:#f8f8f8
+ style t12 fill:#286983,stroke:#7f7f7f,color:#f8f8f8
+ style t13 fill:#dfe9ec,stroke:#7f7f7f,color:#11111b
+ style t14 fill:#303446,stroke:#7f7f7f,color:#f8f8f8
+ style t15 fill:#c6ceef,stroke:#7f7f7f,color:#11111b
+ style t16 fill:#bb93d6,stroke:#7f7f7f,color:#11111b
+ style t17 fill:#67c48f,stroke:#7f7f7f,color:#11111b
+ style t18 fill:#8caaee,stroke:#7f7f7f,color:#11111b
+ style t19 fill:#838ba7,stroke:#7f7f7f,color:#f8f8f8
+```
+
+### Soft Paper dark
+
+```mermaid
+flowchart LR
+ t0["bg #303446"] ~~~ t1["surface #292c3c"] ~~~ t2["surface-deep #232634"] ~~~ t3["border #414459"] ~~~ t4["text #c6ceef"]
+ t5["text-muted #b5bddc"] ~~~ t6["primary #8caaee"] ~~~ t7["on-primary #232634"] ~~~ t8["link #8caaee"] ~~~ t9["success #67c48f"]
+ t10["warning #c9be3e"] ~~~ t11["error #e78284"] ~~~ t12["focus #8caaee"] ~~~ t13["primary-soft #3b415c"] ~~~ t14["code-bg #232634"]
+ t15["code-text #c6ceef"] ~~~ t16["code-keyword #bb93d6"] ~~~ t17["code-string #67c48f"] ~~~ t18["code-function #8caaee"] ~~~ t19["code-comment #838ba7"]
+ style t0 fill:#303446,stroke:#7f7f7f,color:#f8f8f8
+ style t1 fill:#292c3c,stroke:#7f7f7f,color:#f8f8f8
+ style t2 fill:#232634,stroke:#7f7f7f,color:#f8f8f8
+ style t3 fill:#414459,stroke:#7f7f7f,color:#f8f8f8
+ style t4 fill:#c6ceef,stroke:#7f7f7f,color:#11111b
+ style t5 fill:#b5bddc,stroke:#7f7f7f,color:#11111b
+ style t6 fill:#8caaee,stroke:#7f7f7f,color:#11111b
+ style t7 fill:#232634,stroke:#7f7f7f,color:#f8f8f8
+ style t8 fill:#8caaee,stroke:#7f7f7f,color:#11111b
+ style t9 fill:#67c48f,stroke:#7f7f7f,color:#11111b
+ style t10 fill:#c9be3e,stroke:#7f7f7f,color:#11111b
+ style t11 fill:#e78284,stroke:#7f7f7f,color:#f8f8f8
+ style t12 fill:#8caaee,stroke:#7f7f7f,color:#11111b
+ style t13 fill:#3b415c,stroke:#7f7f7f,color:#f8f8f8
+ style t14 fill:#232634,stroke:#7f7f7f,color:#f8f8f8
+ style t15 fill:#c6ceef,stroke:#7f7f7f,color:#11111b
+ style t16 fill:#bb93d6,stroke:#7f7f7f,color:#11111b
+ style t17 fill:#67c48f,stroke:#7f7f7f,color:#11111b
+ style t18 fill:#8caaee,stroke:#7f7f7f,color:#11111b
+ style t19 fill:#838ba7,stroke:#7f7f7f,color:#f8f8f8
+```
+
## Structural tokens (theme-independent)
Defined once in `base.css`, identical for both themes:
diff --git a/scripts/generate_palette_diagrams.py b/scripts/generate_palette_diagrams.py
new file mode 100644
index 0000000..602c083
--- /dev/null
+++ b/scripts/generate_palette_diagrams.py
@@ -0,0 +1,261 @@
+#!/usr/bin/env python3
+"""Generate the "Color palettes" Mermaid diagrams from the theme CSS.
+
+Reads the design tokens straight from the two theme stylesheets (the source
+of truth) and prints a Markdown section containing one Mermaid `block-beta`
+swatch grid per theme and mode. GitHub renders Mermaid natively, so the
+palette is visible in the browser without committing any image.
+
+Each swatch is filled with the token's color; the label color (dark or
+light) is chosen by WCAG relative luminance so the token name stays
+readable on its own swatch.
+
+Usage:
+ python3 scripts/generate_palette_diagrams.py
+ Print the Markdown section; paste it over the "Color palettes"
+ section of docs/design/theme-exploration.md.
+ python3 scripts/generate_palette_diagrams.py --html
+ Write docs/design/palettes.html, the standalone French palette page
+ for the dossier: same data, plus the contrast ratio of every token
+ against its reference background and the AA verdict.
+
+Run both after any token change.
+"""
+
+import re
+import sys
+
+THEMES = [
+ ("Catppuccin", "src/main/resources/static/css/theme-catppuccin.css",
+ "Latte (light)", "Mocha (dark)"),
+ ("Soft Paper", "src/main/resources/static/css/theme-soft-paper.css",
+ "light", "dark"),
+]
+
+COLUMNS = 5
+
+TOKEN_RE = re.compile(r"^\s*(--[a-z-]+)\s*:\s*(#[0-9a-fA-F]{3,6})\s*;")
+
+
+def parse_blocks(path: str) -> tuple[dict[str, str], dict[str, str]]:
+ """Return (light, dark) ordered token->hex maps from a theme stylesheet.
+
+ The light palette is the first `:root` block; the dark palette is the
+ `:root` inside `@media (prefers-color-scheme: dark)`.
+ """
+ light: dict[str, str] = {}
+ dark: dict[str, str] = {}
+ in_dark = False
+ for line in open(path):
+ if "prefers-color-scheme: dark" in line:
+ in_dark = True
+ match = TOKEN_RE.match(line)
+ if match:
+ (dark if in_dark else light)[match.group(1)] = match.group(2).lower()
+ return light, dark
+
+
+def luminance(hex_color: str) -> float:
+ """WCAG relative luminance of a #rrggbb color."""
+ hex_color = hex_color.lstrip("#")
+ if len(hex_color) == 3:
+ hex_color = "".join(c * 2 for c in hex_color)
+ channels = []
+ for i in (0, 2, 4):
+ c = int(hex_color[i:i + 2], 16) / 255
+ channels.append(c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4)
+ r, g, b = channels
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b
+
+
+def label_color(background: str) -> str:
+ """Dark label on light swatches, light label on dark ones."""
+ return "#11111b" if luminance(background) > 0.35 else "#f8f8f8"
+
+
+def diagram(tokens: dict[str, str]) -> str:
+ """A flowchart whose disconnected rows stack vertically: each row is a
+ chain of COLUMNS nodes joined by invisible links (~~~). block-beta would
+ be the natural fit but it ignores the label color, which makes dark
+ swatches unreadable; flowchart honors it."""
+ lines = ["```mermaid", "flowchart LR"]
+ ids = []
+ row: list[str] = []
+ for index, (token, value) in enumerate(tokens.items()):
+ short = token.removeprefix("--color-").removeprefix("--")
+ block_id = f"t{index}"
+ ids.append((block_id, value))
+ row.append(f'{block_id}["{short} {value}"]')
+ if len(row) == COLUMNS:
+ lines.append(" " + " ~~~ ".join(row))
+ row = []
+ if row:
+ lines.append(" " + " ~~~ ".join(row))
+ for block_id, value in ids:
+ lines.append(
+ f" style {block_id} fill:{value},stroke:#7f7f7f,"
+ f"color:{label_color(value)}")
+ lines.append("```")
+ return "\n".join(lines)
+
+
+def contrast(color_a: str, color_b: str) -> float:
+ """WCAG contrast ratio between two #rrggbb colors."""
+ lum_a, lum_b = luminance(color_a), luminance(color_b)
+ lighter, darker = max(lum_a, lum_b), min(lum_a, lum_b)
+ return (lighter + 0.05) / (darker + 0.05)
+
+
+# How each token is judged: (reference token or None, threshold or None).
+# None reference means decorative/surface: no contrast requirement of its own.
+JUDGMENTS = {
+ "--color-text": ("--color-bg", 4.5),
+ "--color-text-muted": ("--color-bg", 4.5),
+ "--color-primary": ("--color-bg", 4.5),
+ "--color-on-primary": ("--color-primary", 4.5),
+ "--color-link": ("--color-bg", 4.5),
+ "--color-success": ("--color-bg", 4.5),
+ "--color-warning": ("--color-bg", 4.5),
+ "--color-error": ("--color-bg", 4.5),
+ "--color-focus": ("--color-bg", 3.0), # UI component (RGAA/WCAG 1.4.11)
+ "--code-text": ("--code-bg", 4.5),
+ "--code-keyword": ("--code-bg", 4.5),
+ "--code-string": ("--code-bg", 4.5),
+ "--code-function": ("--code-bg", 4.5),
+ "--code-comment": ("--code-bg", 4.5),
+}
+
+
+def html_rows(tokens: dict[str, str]) -> str:
+ rows = []
+ for token, value in tokens.items():
+ short = token.removeprefix("--color-").removeprefix("--")
+ reference, threshold = JUDGMENTS.get(token, (None, None))
+ if reference:
+ ratio = contrast(value, tokens[reference])
+ verdict = "pass" if ratio >= threshold else "fail"
+ badge = (f'
{ratio:.2f}:1 sur '
+ f'{reference.removeprefix("--color-").removeprefix("--")}
'
+ f'
'
+ f'{"AA" if threshold == 4.5 else "UI 3:1"}'
+ f'{" ✓" if verdict == "pass" else " ✗"}
+ Annexe du dossier projet. Chaque carte ci-dessous est rendue avec les
+ couleurs de sa propre palette (fond, texte et bordure réels) ; chaque
+ jeton indique son ratio de contraste WCAG calculé contre son fond de
+ référence, avec le verdict AA (texte : 4,5:1 ; composant d'interface :
+ 3:1). « décor » signale les jetons de surface sans exigence de contraste
+ propre. Les valeurs proviennent directement des feuilles de style du
+ thème ; ce fichier est généré par
+ scripts/generate_palette_diagrams.py --html.
+