Merge branch 'mai/brunel/issue-12-kontrast-fix': Kontrast-Fix für 33 Dark-BG Sites (#12)
This commit is contained in:
@@ -21,8 +21,8 @@
|
||||
--bg: #1a1d2e;
|
||||
--bg-light: #1e2235;
|
||||
--text: #c8cad7;
|
||||
--text-dim: #6b6e82;
|
||||
--text-faint: #3d4056;
|
||||
--text-dim: #a6aaca;
|
||||
--text-faint: #8086b5;
|
||||
--ai: #7b8aad;
|
||||
--ai-glow: rgba(123, 138, 173, 0.12);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
--border: #1e1e26;
|
||||
--text: #f0f0f5;
|
||||
--text-dim: #8a8a99;
|
||||
--text-muted: #55555f;
|
||||
--text-muted: #7b7b8a;
|
||||
--gold: #d4af37;
|
||||
--gold-light: #e8c84a;
|
||||
--gold-glow: rgba(212, 175, 55, 0.15);
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
:root {
|
||||
--bg: #0a0a0a;
|
||||
--text: #e8e8e8;
|
||||
--text-dim: #777;
|
||||
--text-muted: #444;
|
||||
--text-dim: #9b9b9b;
|
||||
--text-muted: #7a7a7a;
|
||||
--red: #cc2222;
|
||||
--red-glow: rgba(204, 34, 34, 0.15);
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
--bg-card: #16161b;
|
||||
--border: #1e1e26;
|
||||
--text: #e8e8ed;
|
||||
--text-dim: #6e6e7a;
|
||||
--text-muted: #44444f;
|
||||
--text-dim: #9a9aab;
|
||||
--text-muted: #7a7a8e;
|
||||
--accent: #6366f1;
|
||||
--accent-glow: rgba(99, 102, 241, 0.2);
|
||||
--accent-subtle: rgba(99, 102, 241, 0.08);
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
--bg-card: #16161b;
|
||||
--border: #1e1e26;
|
||||
--text: #e8e8ed;
|
||||
--text-dim: #6e6e7a;
|
||||
--text-muted: #44444f;
|
||||
--text-dim: #9a9aab;
|
||||
--text-muted: #7a7a8e;
|
||||
--accent: #a78bfa;
|
||||
--accent-glow: rgba(167, 139, 250, 0.15);
|
||||
--accent-subtle: rgba(167, 139, 250, 0.08);
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
:root {
|
||||
--bg: #0a0a0c;
|
||||
--text: #e8e8ed;
|
||||
--text-dim: #6e6e7a;
|
||||
--text-muted: #44444f;
|
||||
--text-dim: #9a9aab;
|
||||
--text-muted: #7a7a8e;
|
||||
--accent: #2dd4a8;
|
||||
--accent-glow: rgba(45, 212, 168, 0.15);
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
--bg-card: #16161b;
|
||||
--border: #1e1e26;
|
||||
--text: #e8e8ed;
|
||||
--text-dim: #6e6e7a;
|
||||
--text-muted: #44444f;
|
||||
--text-dim: #9a9aab;
|
||||
--text-muted: #7a7a8e;
|
||||
--accent: #d4a843;
|
||||
--accent-glow: rgba(212, 168, 67, 0.15);
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
--bg-card: #0e0e1a;
|
||||
--border: #1a1a2e;
|
||||
--text: #e8e8f0;
|
||||
--text-dim: #7070a0;
|
||||
--text-muted: #404068;
|
||||
--text-dim: #9797d8;
|
||||
--text-muted: #7373bb;
|
||||
--octopus: #e85040;
|
||||
--octopus-glow: rgba(232, 80, 64, 0.2);
|
||||
--octopus-subtle: rgba(232, 80, 64, 0.06);
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
:root {
|
||||
--bg: #0a0a0c; --text: #e8e8ed; --text-dim: #6e6e7a; --text-muted: #44444f;
|
||||
--bg: #0a0a0c; --text: #e8e8ed; --text-dim: #9a9aab; --text-muted: #7a7a8e;
|
||||
--accent: #e04848; --accent-glow: rgba(224, 72, 72, 0.15);
|
||||
}
|
||||
body {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
--border: #2a1a3e;
|
||||
--text: #f0e8f8;
|
||||
--text-dim: #8a7a9e;
|
||||
--text-muted: #4a3d5e;
|
||||
--text-muted: #8971ae;
|
||||
--pink: #ff2d78;
|
||||
--pink-glow: rgba(255, 45, 120, 0.25);
|
||||
--pink-soft: rgba(255, 45, 120, 0.08);
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
--border-hover: #252830;
|
||||
--text: #e4e5ea;
|
||||
--text-secondary: #9496a1;
|
||||
--text-muted: #505264;
|
||||
--text-muted: #787b96;
|
||||
--accent: #2dd4bf;
|
||||
--accent-light: #5eead4;
|
||||
--accent-glow: rgba(45, 212, 191, 0.15);
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
--bg-card: #12141b;
|
||||
--border: #1a1d28;
|
||||
--text: #e4e6ed;
|
||||
--text-dim: #6b7084;
|
||||
--text-muted: #3d4155;
|
||||
--text-dim: #969db9;
|
||||
--text-muted: #71789d;
|
||||
--accent: #06b6d4;
|
||||
--accent-glow: rgba(6, 182, 212, 0.15);
|
||||
--accent-subtle: rgba(6, 182, 212, 0.06);
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
--bg-card: #16161b;
|
||||
--border: #1e1e26;
|
||||
--text: #e8e8ed;
|
||||
--text-dim: #6e6e7a;
|
||||
--text-muted: #44444f;
|
||||
--text-dim: #9a9aab;
|
||||
--text-muted: #7a7a8e;
|
||||
--accent: #10b981;
|
||||
--accent-glow: rgba(16, 185, 129, 0.15);
|
||||
--accent-subtle: rgba(16, 185, 129, 0.08);
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
--bg-card: #151519;
|
||||
--bg-card-hover: #1a1a20;
|
||||
--text: #e8e6e3;
|
||||
--text-dim: #7a7880;
|
||||
--text-dimmer: #4a4850;
|
||||
--text-dim: #9f9ca6;
|
||||
--text-dimmer: #7e7a88;
|
||||
--accent: #8b5cf6;
|
||||
--accent-dim: #6d3fd4;
|
||||
--accent-dim: #9355ff;
|
||||
--accent-glow: rgba(139, 92, 246, 0.15);
|
||||
--warm: #f59e0b;
|
||||
--border: #222228;
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
--bg-card: #141a10;
|
||||
--border: #1e2818;
|
||||
--text: #c8d0b8;
|
||||
--text-dim: #707860;
|
||||
--text-muted: #404830;
|
||||
--text-dim: #97a282;
|
||||
--text-muted: #738256;
|
||||
--olive: #6b8e23;
|
||||
--olive-light: #8fbc3c;
|
||||
--olive-glow: rgba(107, 142, 35, 0.15);
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
--border: #2a2a1e;
|
||||
--text: #e8e8d8;
|
||||
--text-dim: #808070;
|
||||
--text-muted: #505040;
|
||||
--text-muted: #7c7c63;
|
||||
--yellow: #eab308;
|
||||
--yellow-dark: #a17b06;
|
||||
--black: #1a1a14;
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
--border: #2a2418;
|
||||
--text: #f5f0e0;
|
||||
--text-dim: #a09070;
|
||||
--text-muted: #605040;
|
||||
--text-muted: #907860;
|
||||
--gold: #d4a017;
|
||||
--gold-light: #f0c040;
|
||||
--gold-glow: rgba(212, 160, 23, 0.2);
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
--bg-card: #12121a;
|
||||
--border: #1a1a28;
|
||||
--text: #e0e0f0;
|
||||
--text-dim: #7070a0;
|
||||
--text-muted: #404060;
|
||||
--text-dim: #9797d8;
|
||||
--text-muted: #7676b2;
|
||||
--accent: #6366f1;
|
||||
--accent-light: #818cf8;
|
||||
--accent-glow: rgba(99, 102, 241, 0.15);
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
--bg-card: #0e0e18;
|
||||
--border: #181830;
|
||||
--text: #d0d0e8;
|
||||
--text-dim: #606088;
|
||||
--text-muted: #383858;
|
||||
--text-dim: #9595d3;
|
||||
--text-muted: #7373b4;
|
||||
--accent: #7c3aed;
|
||||
--accent-light: #a78bfa;
|
||||
--accent-glow: rgba(124, 58, 237, 0.2);
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
--bg-card: #121216;
|
||||
--border: #1c1c24;
|
||||
--text: #e0e0e8;
|
||||
--text-dim: #707080;
|
||||
--text-muted: #404050;
|
||||
--text-dim: #9d9db3;
|
||||
--text-muted: #7a7a98;
|
||||
--pink: #ec4899;
|
||||
--cyan: #06b6d4;
|
||||
--purple: #8b5cf6;
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
--bg-card: #16161b;
|
||||
--border: #1e1e26;
|
||||
--text: #e8e8ed;
|
||||
--text-dim: #6e6e7a;
|
||||
--text-muted: #44444f;
|
||||
--text-dim: #9a9aab;
|
||||
--text-muted: #7a7a8e;
|
||||
--accent: #f97316;
|
||||
--accent-glow: rgba(249, 115, 22, 0.15);
|
||||
--accent-subtle: rgba(249, 115, 22, 0.08);
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
:root {
|
||||
--bg: #0a0a0c; --text: #e8e8ed; --text-dim: #6e6e7a; --text-muted: #44444f;
|
||||
--bg: #0a0a0c; --text: #e8e8ed; --text-dim: #9a9aab; --text-muted: #7a7a8e;
|
||||
--accent: #9b7ed8; --accent-glow: rgba(155, 126, 216, 0.15);
|
||||
}
|
||||
body {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
:root {
|
||||
--bg: #0a0a0c; --text: #e8e8ed; --text-dim: #6e6e7a; --text-muted: #44444f;
|
||||
--bg: #0a0a0c; --text: #e8e8ed; --text-dim: #9a9aab; --text-muted: #7a7a8e;
|
||||
--accent: #6b8cce; --accent-glow: rgba(107, 140, 206, 0.15);
|
||||
}
|
||||
body {
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
--bg-card: #16161b;
|
||||
--border: #1e1e26;
|
||||
--text: #e8e8ed;
|
||||
--text-dim: #6e6e7a;
|
||||
--text-muted: #44444f;
|
||||
--text-dim: #9a9aab;
|
||||
--text-muted: #7a7a8e;
|
||||
--accent: #3b82f6;
|
||||
--accent-glow: rgba(59, 130, 246, 0.15);
|
||||
--accent-subtle: rgba(59, 130, 246, 0.08);
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
--bg-card: #0e0e1a;
|
||||
--border: #1a1a2e;
|
||||
--text: #e8e8f0;
|
||||
--text-dim: #7070a0;
|
||||
--text-muted: #404068;
|
||||
--text-dim: #9797d8;
|
||||
--text-muted: #7373bb;
|
||||
--octopus: #e85040;
|
||||
--octopus-glow: rgba(232, 80, 64, 0.2);
|
||||
--octopus-subtle: rgba(232, 80, 64, 0.06);
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
--bg-card: #16161b;
|
||||
--border: #1e1e26;
|
||||
--text: #e8e8ed;
|
||||
--text-dim: #6e6e7a;
|
||||
--text-muted: #44444f;
|
||||
--text-dim: #9a9aab;
|
||||
--text-muted: #7a7a8e;
|
||||
--accent: #3b82f6;
|
||||
--accent-glow: rgba(59, 130, 246, 0.15);
|
||||
--accent-subtle: rgba(59, 130, 246, 0.08);
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
--violet-glow: rgba(107, 76, 154, 0.2);
|
||||
--violet-mist: rgba(107, 76, 154, 0.08);
|
||||
--text: #d4cbb8;
|
||||
--text-dim: #7a7060;
|
||||
--text-muted: #3d3830;
|
||||
--text-dim: #a59782;
|
||||
--text-muted: #837867;
|
||||
}
|
||||
|
||||
html { scroll-behavior: smooth; }
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
:root {
|
||||
--gold: #c9a84c;
|
||||
--gold-light: #e8d48b;
|
||||
--gold-dim: #8a7235;
|
||||
--gold-dim: #c1a04a;
|
||||
--dark: #0a0a0f;
|
||||
--dark-surface: #12121a;
|
||||
--dark-card: #1a1a26;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
:root {
|
||||
--bg: #0a0a0c; --text: #e8e8ed; --text-dim: #6e6e7a; --text-muted: #44444f;
|
||||
--bg: #0a0a0c; --text: #e8e8ed; --text-dim: #9a9aab; --text-muted: #7a7a8e;
|
||||
--accent: #e8a838; --accent-glow: rgba(232, 168, 56, 0.15);
|
||||
}
|
||||
body {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
--red-glow: #ef4444;
|
||||
--bg: #0a0a0a;
|
||||
--text: #e5e5e5;
|
||||
--text-dim: #737373;
|
||||
--text-dim: #9b9b9b;
|
||||
--white: #fafafa;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
--bg-card: #16161b;
|
||||
--border: #1e1e26;
|
||||
--text: #e8e8ed;
|
||||
--text-dim: #6e6e7a;
|
||||
--text-muted: #44444f;
|
||||
--text-dim: #9a9aab;
|
||||
--text-muted: #7a7a8e;
|
||||
--orange: #f97316;
|
||||
--orange-glow: rgba(249, 115, 22, 0.2);
|
||||
--orange-subtle: rgba(249, 115, 22, 0.08);
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
:root {
|
||||
--bg: #0a0a0c; --text: #e8e8ed; --text-dim: #6e6e7a; --text-muted: #44444f;
|
||||
--bg: #0a0a0c; --text: #e8e8ed; --text-dim: #9a9aab; --text-muted: #7a7a8e;
|
||||
--accent: #7c9885; --accent-glow: rgba(124, 152, 133, 0.15);
|
||||
}
|
||||
body {
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
--bg-card: #16161b;
|
||||
--border: #1e1e26;
|
||||
--text: #e8e8ed;
|
||||
--text-dim: #6e6e7a;
|
||||
--text-muted: #44444f;
|
||||
--text-dim: #9a9aab;
|
||||
--text-muted: #7a7a8e;
|
||||
--accent: #c9a84c;
|
||||
--accent-glow: rgba(201, 168, 76, 0.15);
|
||||
--accent-subtle: rgba(201, 168, 76, 0.08);
|
||||
|
||||
125
tools/contrast-audit.py
Executable file
125
tools/contrast-audit.py
Executable file
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Quick contrast audit across all onepager sites.
|
||||
|
||||
Extracts CSS custom properties for backgrounds and text colors,
|
||||
computes WCAG contrast ratio for likely text-on-bg pairs, flags
|
||||
violations.
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SITES_DIR = Path(__file__).resolve().parent.parent / "sites"
|
||||
|
||||
HEX_RE = re.compile(r"#([0-9a-fA-F]{3,8})\b")
|
||||
VAR_DECL_RE = re.compile(r"--([\w-]+)\s*:\s*([^;]+);")
|
||||
|
||||
def hex_to_rgb(h):
|
||||
h = h.lstrip("#")
|
||||
if len(h) == 3:
|
||||
h = "".join(c * 2 for c in h)
|
||||
if len(h) == 8:
|
||||
h = h[:6]
|
||||
if len(h) != 6:
|
||||
return None
|
||||
try:
|
||||
return tuple(int(h[i:i+2], 16) for i in (0, 2, 4))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def relative_luminance(rgb):
|
||||
def channel(c):
|
||||
c /= 255
|
||||
return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4
|
||||
r, g, b = (channel(c) for c in rgb)
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
||||
|
||||
def contrast_ratio(rgb1, rgb2):
|
||||
l1 = relative_luminance(rgb1)
|
||||
l2 = relative_luminance(rgb2)
|
||||
lighter, darker = max(l1, l2), min(l1, l2)
|
||||
return (lighter + 0.05) / (darker + 0.05)
|
||||
|
||||
# Variable names that suggest "background" or "text"
|
||||
BG_KEYS = ("bg", "background", "surface")
|
||||
TEXT_KEYS = ("text", "fg", "foreground", "color", "muted", "dim", "subtle", "secondary")
|
||||
|
||||
def is_bg_var(name):
|
||||
n = name.lower()
|
||||
return any(k in n for k in BG_KEYS) and "border" not in n
|
||||
|
||||
def is_text_var(name):
|
||||
n = name.lower()
|
||||
return any(k in n for k in TEXT_KEYS) and "border" not in n and "bg" not in n.split("-")[0]
|
||||
|
||||
def audit(site):
|
||||
html = (SITES_DIR / site / "index.html")
|
||||
if not html.exists():
|
||||
return None
|
||||
css = html.read_text(errors="ignore")
|
||||
# Only look at the <style> block(s)
|
||||
style_blocks = re.findall(r"<style.*?>(.*?)</style>", css, re.DOTALL | re.IGNORECASE)
|
||||
if not style_blocks:
|
||||
return None
|
||||
css_only = "\n".join(style_blocks)
|
||||
|
||||
vars_ = {}
|
||||
for m in VAR_DECL_RE.finditer(css_only):
|
||||
name, val = m.group(1), m.group(2).strip()
|
||||
# only resolve to hex
|
||||
hm = HEX_RE.search(val)
|
||||
if hm:
|
||||
rgb = hex_to_rgb(hm.group(0))
|
||||
if rgb:
|
||||
vars_[name] = rgb
|
||||
|
||||
bg_vars = {n: c for n, c in vars_.items() if is_bg_var(n)}
|
||||
text_vars = {n: c for n, c in vars_.items() if is_text_var(n)}
|
||||
|
||||
if not bg_vars or not text_vars:
|
||||
return None
|
||||
|
||||
# Find primary bg (the darkest one is usually --bg)
|
||||
primary_bg_name = min(bg_vars, key=lambda n: relative_luminance(bg_vars[n]))
|
||||
bg_rgb = bg_vars[primary_bg_name]
|
||||
bg_lum = relative_luminance(bg_rgb)
|
||||
|
||||
# Only audit dark backgrounds (lum < 0.05 = near-black)
|
||||
if bg_lum > 0.1:
|
||||
return None # not a dark site
|
||||
|
||||
findings = []
|
||||
for tname, trgb in text_vars.items():
|
||||
ratio = contrast_ratio(trgb, bg_rgb)
|
||||
if ratio < 4.5: # WCAG AA for body text
|
||||
findings.append((tname, trgb, ratio))
|
||||
|
||||
if not findings:
|
||||
return None
|
||||
return {
|
||||
"site": site,
|
||||
"bg_name": primary_bg_name,
|
||||
"bg_rgb": bg_rgb,
|
||||
"findings": findings,
|
||||
}
|
||||
|
||||
results = []
|
||||
for site in sorted(p.name for p in SITES_DIR.iterdir() if p.is_dir()):
|
||||
r = audit(site)
|
||||
if r:
|
||||
results.append(r)
|
||||
|
||||
print(f"Sites with sub-AA text on dark bg: {len(results)}/59\n")
|
||||
for r in results:
|
||||
bg = r["bg_rgb"]
|
||||
print(f"{r['site']} (bg --{r['bg_name']} = #{bg[0]:02x}{bg[1]:02x}{bg[2]:02x})")
|
||||
for name, rgb, ratio in sorted(r["findings"], key=lambda x: x[2]):
|
||||
flag = "FAIL" if ratio < 3.0 else ("WEAK" if ratio < 4.5 else "OK")
|
||||
print(f" {flag:4} ratio {ratio:5.2f} --{name:24} #{rgb[0]:02x}{rgb[1]:02x}{rgb[2]:02x}")
|
||||
print()
|
||||
|
||||
# Summary: how many distinct sites have FAIL (< 3.0) somewhere
|
||||
fails = [r for r in results if any(ratio < 3.0 for _, _, ratio in r["findings"])]
|
||||
print(f"\n=== SUMMARY ===")
|
||||
print(f"Dark-bg sites with at least one FAIL (<3:1): {len(fails)}")
|
||||
print(f"Dark-bg sites with WEAK (<4.5 but >=3): {len(results) - len(fails)}")
|
||||
Reference in New Issue
Block a user