diff --git a/.claude/audits/TOUR.md b/.claude/audits/TOUR.md
index f039810..0381333 100644
--- a/.claude/audits/TOUR.md
+++ b/.claude/audits/TOUR.md
@@ -92,3 +92,12 @@ container is redeployed: merge → VPS `git pull && docker compose up -d
Commits: 4 (fix/clean/docs + this report). BREAKING: 1 (SEC-1, container
port — compose covered). Branch left UNMERGED — `gitflow finish` on GO.
+
+## Follow-up 2026-07-05 — residuals closed (chore/tour-residuals, user GO)
+
+| ID | Resolution |
+|----|-----------|
+| CLN-3 | Card CSS deduplicated via grouped selectors (shared chrome/hover/head/title/tag blocks + per-class specifics), zero HTML change, cascade-order verified (no interfering same-specificity rules between shared and specific blocks), braces 195/195. Honest correction: the audited "~421 redundant lines" was overstated — real net dedup ≈ 60 lines. |
+| CLN-4 | Norm aligned with reality: the 8 functional neutrals (inks, rule/tag, 2 green intermediates) are now DOCUMENTED as allowed in CLAUDE.md (+ README pointer). "Any color outside the two lists is a violation" keeps the norm enforceable. |
+| SEC-7 | script-src hardened: `unsafe-inline` replaced by the sha256 hash of the single inline script (index has zero style/script attributes). style-src keeps `unsafe-inline` (CV carries 2 style attributes + single-file convention) — documented. NEW INVARIANT in CLAUDE.md: recompute the hash after any inline-JS edit (stale hash = JS silently blocked in prod). |
+| INF-2 | CORRECTION: false positive in the 2026-07-05 report-only run — `.gitignore` exists (549B) and covers the expected classes. No action was ever needed. |
diff --git a/CLAUDE.md b/CLAUDE.md
index cae6427..75d6790 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -66,7 +66,7 @@ The PDF must match the latest HTML before pushing or sending.
## Design system (non-negotiable)
-Palette — exact hex:
+Palette — exact hex (brand colors):
- `#0d1b12` — dark forest (nav, dark sections, footer)
- `#1b5e3b` — green primary (links, section titles on light bg)
- `#2d7a4f` — green accent (borders, dots, separators)
@@ -74,6 +74,13 @@ Palette — exact hex:
- `#dff0e7` — green tint (pill bg)
- `#f5f3ec` — parchment (page bg)
+Functional neutrals (allowed, intentional — layering + text, NOT brand):
+- `#183325` (`--dark-mid`), `#0e3320` (`--g900`), `#eef7f1` (`--g050`) —
+ green-scale intermediates for dark layering and light block bg
+- `#111111` / `#1e1e1e` / `#636363` (`--ink-1/2/3`) — text hierarchy
+- `#d8d4c8` (`--rule`), `#e6e2d8` (`--tag`) — separators, generic tags
+Any color outside these two lists is a violation.
+
Typography:
- `Fraunces` (serif) — display: hero name, section titles, role headings
- `JetBrains Mono` (mono) — eyebrows, badges, tech pills, nav, contact rows
@@ -124,6 +131,12 @@ None — global rules apply.
- Edits to `index.html` or `CV_Bastien_Chanot.html` must preserve the
palette + typography + structure unless explicitly asked to change them.
- After editing `CV_Bastien_Chanot.html`, regenerate the PDF.
+- After editing index.html's inline `',open('index.html',encoding='utf-8').read(),re.S).group(1).encode()).digest()).decode();print('sha256-'+h)"
+ ```
- Never add external dependencies beyond Google Fonts.
- Never add tracking, analytics, cookie banners or third-party scripts.
- Always test in mobile width (375px) and desktop (1440px) before claiming done.
diff --git a/README.md b/README.md
index a23462c..4d79c21 100644
--- a/README.md
+++ b/README.md
@@ -73,6 +73,9 @@ Strict palette (non-negotiable):
| `#dff0e7` | Green tint — pill background |
| `#f5f3ec` | Parchment — page background |
+Plus a documented set of functional neutrals (text inks, rules/tags, two
+green-scale intermediates) — the exhaustive list lives in `CLAUDE.md`.
+
Typography:
- `Fraunces` — display (names, titles)
- `JetBrains Mono` — technical labels, badges, pills, nav, contact
diff --git a/index.html b/index.html
index c82c303..6e277bb 100644
--- a/index.html
+++ b/index.html
@@ -419,39 +419,49 @@
gap: 20px;
margin-top: 40px;
}
- .stack-card {
+ /* ── Shared card chrome (stack / project / theme cards + méthode items).
+ Per-class blocks below keep only their specifics; the cascade resolves
+ identically to the previous duplicated declarations. ── */
+ .stack-card, .project-card, .theme-card, .methode-item {
background: var(--page);
border: 1px solid var(--rule);
border-radius: var(--r-md);
- padding: 24px;
transition: border-color .25s ease, transform .25s ease, box-shadow .25s ease;
}
- .stack-card:hover {
+ .stack-card:hover, .project-card:hover, .theme-card:hover, .methode-item:hover {
border-color: var(--g300);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
- .stack-card-head {
+ .stack-card-head, .project-card-head, .theme-card-head {
display: flex;
align-items: baseline;
justify-content: space-between;
- margin-bottom: 16px;
- padding-bottom: 12px;
border-bottom: 1px dashed var(--rule);
}
- .stack-card h3 {
+ .stack-card h3, .project-card h3, .theme-card h4, .methode-body h3 {
font-family: var(--serif);
font-weight: 600;
- font-size: 19px;
color: var(--ink-1);
letter-spacing: -0.01em;
}
- .stack-card-tag {
+ .stack-card-tag, .project-card-tag, .theme-card-tag {
font-family: var(--mono);
font-size: 11px;
color: var(--g500);
letter-spacing: 0.1em;
}
+
+ .stack-card {
+ padding: 24px;
+ }
+ .stack-card-head {
+ margin-bottom: 16px;
+ padding-bottom: 12px;
+ }
+ .stack-card h3 {
+ font-size: 19px;
+ }
.pills {
display: flex;
flex-wrap: wrap;
@@ -641,40 +651,19 @@
}
@media (min-width: 768px) { .projects-grid { grid-template-columns: repeat(2, 1fr); } }
.project-card {
- background: var(--page);
- border: 1px solid var(--rule);
- border-radius: var(--r-md);
padding: 24px;
- transition: border-color .25s ease, transform .25s ease, box-shadow .25s ease;
display: flex;
flex-direction: column;
}
- .project-card:hover {
- border-color: var(--g300);
- transform: translateY(-2px);
- box-shadow: var(--shadow-md);
- }
.project-card-head {
- display: flex;
- align-items: baseline;
- justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
padding-bottom: 10px;
- border-bottom: 1px dashed var(--rule);
}
.project-card h3 {
- font-family: var(--serif);
- font-weight: 600;
font-size: 20px;
- color: var(--ink-1);
- letter-spacing: -0.01em;
}
.project-card-tag {
- font-family: var(--mono);
- font-size: 11px;
- color: var(--g500);
- letter-spacing: 0.1em;
flex-shrink: 0;
white-space: nowrap;
}
@@ -732,41 +721,20 @@
@media (min-width: 1200px) { .formation-themes { grid-template-columns: repeat(3, 1fr); } }
.theme-card {
- background: var(--page);
- border: 1px solid var(--rule);
- border-radius: var(--r-md);
padding: 22px;
- transition: border-color .25s ease, transform .25s ease, box-shadow .25s ease;
display: flex;
flex-direction: column;
}
- .theme-card:hover {
- border-color: var(--g300);
- transform: translateY(-2px);
- box-shadow: var(--shadow-md);
- }
.theme-card-head {
- display: flex;
- align-items: baseline;
- justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
padding-bottom: 10px;
- border-bottom: 1px dashed var(--rule);
}
.theme-card h4 {
- font-family: var(--serif);
- font-weight: 600;
font-size: 18px;
- color: var(--ink-1);
- letter-spacing: -0.01em;
line-height: 1.2;
}
.theme-card-tag {
- font-family: var(--mono);
- font-size: 11px;
- color: var(--g500);
- letter-spacing: 0.1em;
flex-shrink: 0;
}
.theme-quote {
@@ -861,16 +829,7 @@
grid-template-columns: 56px 1fr;
gap: 20px;
align-items: start;
- background: var(--page);
- border: 1px solid var(--rule);
- border-radius: var(--r-md);
padding: 22px 24px;
- transition: border-color .25s ease, transform .25s ease, box-shadow .25s ease;
- }
- .methode-item:hover {
- border-color: var(--g300);
- transform: translateY(-2px);
- box-shadow: var(--shadow-md);
}
.methode-num {
font-family: var(--mono);
@@ -882,11 +841,7 @@
padding-top: 4px;
}
.methode-body h3 {
- font-family: var(--serif);
- font-weight: 600;
font-size: 19px;
- color: var(--ink-1);
- letter-spacing: -0.01em;
margin-bottom: 6px;
line-height: 1.25;
}
diff --git a/nginx-security-headers.conf b/nginx-security-headers.conf
index 73c1c53..182a335 100644
--- a/nginx-security-headers.conf
+++ b/nginx-security-headers.conf
@@ -8,5 +8,8 @@ add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=()" always;
-# CSP: inline CSS + JS are allowed (project convention), fonts from Google.
-add_header Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; script-src 'self' 'unsafe-inline'; img-src 'self' data:; base-uri 'self'; form-action 'self'; frame-ancestors 'self'" always;
+# CSP: inline CSS allowed (style attributes in the CV + single-file convention);
+# the inline script is HASH-pinned (no script unsafe-inline). INVARIANT: after
+# ANY edit to index.html's inline