From d63a52ec507b93f902c40654e6052bde3084f94d Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Sun, 5 Jul 2026 19:59:45 +0200 Subject: [PATCH 1/3] chore(clean): dedup card CSS via grouped selectors (CLN-3) Shared chrome/hover/head/title/tag blocks for stack/project/theme cards + methode items; per-class blocks keep only specifics. Zero HTML change, cascade-order verified (no interfering rules between shared and specific blocks). Net -60 lines; the audit's ~421 estimate was overstated. --- index.html | 83 +++++++++++++----------------------------------------- 1 file changed, 19 insertions(+), 64 deletions(-) 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; } From c0632aefa8f5e9f13fd2547424fb37299530e5d3 Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Sun, 5 Jul 2026 19:59:45 +0200 Subject: [PATCH 2/3] fix(security): pin script-src to the inline script's sha256 hash (SEC-7) unsafe-inline dropped for scripts (index has zero style/script attributes; the single inline script is hash-pinned). style-src keeps unsafe-inline (CV carries 2 style attributes + single-file convention). Verified in hardened container: served-script hash == policy hash, JS executes. --- nginx-security-headers.conf | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 ',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