From 489d849f1eee134b258e065a468aa2b37e2b2386 Mon Sep 17 00:00:00 2001 From: Kev Date: Fri, 17 Jul 2026 00:48:56 -0400 Subject: [PATCH] =?UTF-8?q?P0-1=20fix:=20ledger/u=20grade=20badges=20are?= =?UTF-8?q?=20token-derived=20=E2=80=94=20kill=20blue-B=20/=20amber-C=20/?= =?UTF-8?q?=20non-A=20glow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phone audit found LEDGER READ CARDS rendering B badges with BLUE borders + C with AMBER right now in prod — GradePill (components/GradeCard.tsx) hardcoded the OLD palette (rgba(74,158,255) blue-B, rgba(255,179,71) amber-C) for bg/border while the text used the migrated token. Migrated bg/border to color-mix on the grade token, so B renders neutral-white and C grey (matching the board). - globals.css .grade-*-bg → token-derived color-mix (was raw blue/amber rgba). - DELETED glow from .grade-glow-b/c/d (glow is A-tier ONLY, by law) — B/C/D keep their token color, no text-shadow. - Purged the last dead grade-blue #4A9EFF fallbacks (SoccerGradeResult, the intelligence INFO dot). - REGRESSION LOCK: vyndrParityQA fails if #4a9eff / rgba(74,158,255) reappears anywhere in web/src, if GradePill hardcodes blue/amber rgba, or if grade-glow B/C/D grow a text-shadow again. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/unit/vyndrParityQA.test.js | 41 ++++++++++++++++++++++++ web/src/app/globals.css | 17 ++++++---- web/src/app/intelligence/page.tsx | 2 +- web/src/components/GradeCard.tsx | 13 +++++--- web/src/components/SoccerGradeResult.tsx | 6 ++-- 5 files changed, 64 insertions(+), 15 deletions(-) diff --git a/tests/unit/vyndrParityQA.test.js b/tests/unit/vyndrParityQA.test.js index 8a20b49..8eb0dc1 100644 --- a/tests/unit/vyndrParityQA.test.js +++ b/tests/unit/vyndrParityQA.test.js @@ -205,3 +205,44 @@ describe('M4 — mobile structural rules locked (source, not visual)', () => { expect(css).toMatch(/@media\s*\(max-width:\s*767px\)\s*\{[^]*?html,\s*body\s*\{[^}]*overflow-x:\s*hidden/); }); }); + +// ── P0-1 regression — blue-B / amber-C grade colors must never return ───── +describe('P0-1 — grade badges are token-derived; blue-B / amber-C banished', () => { + const css = read('app/globals.css'); + + it('the dead grade-blue #4a9eff / rgba(74,158,255) appears NOWHERE in web/src', () => { + const banned = /#4a9eff|rgba\(\s*74\s*,\s*158\s*,\s*255/i; + const offenders = []; + const walk = (d) => { + for (const e of fs.readdirSync(d, { withFileTypes: true })) { + const fp = path.join(d, e.name); + if (e.isDirectory()) { walk(fp); continue; } + if (!/\.(tsx?|css|js)$/.test(e.name)) continue; + if (banned.test(fs.readFileSync(fp, 'utf8'))) offenders.push(path.relative(WEB, fp)); + } + }; + walk(WEB); + expect(offenders).toEqual([]); + }); + + it('GradePill (ledger/u cards) derives bg/border from the grade token, no old-palette rgba', () => { + const gc = read('components/GradeCard.tsx'); + // the B/C tiers must NOT hardcode blue/amber rgba; must go through color-mix on the token + expect(gc).not.toMatch(/rgba\(\s*74\s*,\s*158\s*,\s*255/); // blue-B + expect(gc).not.toMatch(/bg:\s*'rgba\(\s*255\s*,\s*179\s*,\s*71/); // amber-C badge bg + expect(gc).toMatch(/color-mix\(in srgb, \$\{tok\}/); + }); + + it('GLOW is A-tier only — grade-glow-b/c/d carry NO text-shadow', () => { + const glowBlock = css.slice(css.indexOf('.grade-glow-aplus'), css.indexOf('.grade-glow-d') + 60); + expect(glowBlock).toMatch(/\.grade-glow-b\s*\{\s*color:[^}]*\}/); + expect(glowBlock).not.toMatch(/\.grade-glow-b\s*\{[^}]*text-shadow/); + expect(glowBlock).not.toMatch(/\.grade-glow-c\s*\{[^}]*text-shadow/); + expect(glowBlock).not.toMatch(/\.grade-glow-d\s*\{[^}]*text-shadow/); + }); + + it('grade-*-bg utility classes are token-derived (color-mix), not raw rgba', () => { + expect(css).toMatch(/\.grade-b-bg\s*\{[^}]*color-mix\(in srgb, var\(--grade-b\)/); + expect(css).toMatch(/\.grade-c-bg\s*\{[^}]*color-mix\(in srgb, var\(--grade-c\)/); + }); +}); diff --git a/web/src/app/globals.css b/web/src/app/globals.css index 624431d..d98cb24 100644 --- a/web/src/app/globals.css +++ b/web/src/app/globals.css @@ -374,10 +374,11 @@ h1, h2, h3, h4, h5, h6 { .grade-c-tier { color: var(--grade-c); } .grade-d-tier { color: var(--grade-d); } -.grade-a-bg { background: rgba(0, 200, 150, 0.10); border-color: rgba(0, 200, 150, 0.30); } -.grade-b-bg { background: rgba(74, 158, 255, 0.10); border-color: rgba(74, 158, 255, 0.30); } -.grade-c-bg { background: rgba(255, 179, 71, 0.10); border-color: rgba(255, 179, 71, 0.30); } -.grade-d-bg { background: rgba(255, 107, 107, 0.10); border-color: rgba(255, 107, 107, 0.30); } +/* P0-1 — token-derived (was hardcoded blue-B / amber-C old palette). */ +.grade-a-bg { background: color-mix(in srgb, var(--grade-a) 10%, transparent); border-color: color-mix(in srgb, var(--grade-a) 30%, transparent); } +.grade-b-bg { background: color-mix(in srgb, var(--grade-b) 10%, transparent); border-color: color-mix(in srgb, var(--grade-b) 30%, transparent); } +.grade-c-bg { background: color-mix(in srgb, var(--grade-c) 10%, transparent); border-color: color-mix(in srgb, var(--grade-c) 30%, transparent); } +.grade-d-bg { background: color-mix(in srgb, var(--grade-d) 10%, transparent); border-color: color-mix(in srgb, var(--grade-d) 30%, transparent); } /* ───────────────────────────────────────────────────────── Animations @@ -817,11 +818,13 @@ body.tex-grain::before { } /* CRT phosphor glow utility for grade letters */ +/* GLOW = A-TIER ONLY (law). B/C/D glow classes DELETED (P0-1) — a glowing B/C/D + devalues the cue. B/C/D letters take their token color with no text-shadow. */ .grade-glow-aplus { color: var(--grade-aplus); text-shadow: 0 0 18px var(--grade-aplus), 0 0 36px rgba(0, 255, 184, 0.40); } .grade-glow-a { color: var(--grade-a); text-shadow: 0 0 14px rgba(0, 212, 160, 0.70), 0 0 30px rgba(0, 212, 160, 0.30); } -.grade-glow-b { color: var(--grade-b); text-shadow: 0 0 12px rgba(74, 158, 255, 0.60), 0 0 24px rgba(74, 158, 255, 0.20); } -.grade-glow-c { color: var(--grade-c); text-shadow: 0 0 12px rgba(255, 179, 71, 0.55); } -.grade-glow-d { color: var(--grade-d); text-shadow: 0 0 12px rgba(255, 82, 82, 0.55); } +.grade-glow-b { color: var(--grade-b); } +.grade-glow-c { color: var(--grade-c); } +.grade-glow-d { color: var(--grade-d); } @media (prefers-reduced-motion: reduce) { *, *::before, *::after { diff --git a/web/src/app/intelligence/page.tsx b/web/src/app/intelligence/page.tsx index a3b8ba2..be5111b 100644 --- a/web/src/app/intelligence/page.tsx +++ b/web/src/app/intelligence/page.tsx @@ -18,7 +18,7 @@ type Signal = { const SEVERITY: Record = { critical: { dot: '#FF4757', label: 'CRITICAL' }, notable: { dot: '#FFB347', label: 'NOTABLE' }, - info: { dot: '#4A9EFF', label: 'INFO' }, + info: { dot: '#6C8CFF', label: 'INFO' }, }; const SPORT_COLOR: Record = { diff --git a/web/src/components/GradeCard.tsx b/web/src/components/GradeCard.tsx index da46499..51d5f06 100644 --- a/web/src/components/GradeCard.tsx +++ b/web/src/components/GradeCard.tsx @@ -76,10 +76,15 @@ const SPORTSBOOKS = [ function gradeTierClass(grade: string): { color: string; bg: string; border: string } { const g = (grade || '').trim().toUpperCase().charAt(0); - if (g === 'A') return { color: 'var(--grade-a)', bg: 'rgba(0,200,150,0.10)', border: 'rgba(0,200,150,0.40)' }; - if (g === 'B') return { color: 'var(--grade-b)', bg: 'rgba(74,158,255,0.10)', border: 'rgba(74,158,255,0.40)' }; - if (g === 'C') return { color: 'var(--grade-c)', bg: 'rgba(255,179,71,0.10)', border: 'rgba(255,179,71,0.40)' }; - return { color: 'var(--grade-d)', bg: 'rgba(255,107,107,0.10)', border: 'rgba(255,107,107,0.40)' }; + // P0-1 — TOKEN-DERIVED (was hardcoded old palette: blue-B / amber-C). + // bg/border follow the grade token via color-mix, + // so B renders neutral-white and C grey, matching the board-row badges. + const tok = g === 'A' ? 'var(--grade-a)' : g === 'B' ? 'var(--grade-b)' : g === 'C' ? 'var(--grade-c)' : 'var(--grade-d)'; + return { + color: tok, + bg: `color-mix(in srgb, ${tok} 12%, transparent)`, + border: `color-mix(in srgb, ${tok} 42%, transparent)`, + }; } function confidenceLabel(sample?: number): { label: string; tone: 'high' | 'moderate' | 'limited' } { diff --git a/web/src/components/SoccerGradeResult.tsx b/web/src/components/SoccerGradeResult.tsx index 68156ee..29e170f 100644 --- a/web/src/components/SoccerGradeResult.tsx +++ b/web/src/components/SoccerGradeResult.tsx @@ -56,7 +56,7 @@ interface ParsedSignal { const SIGNAL_TONE_STYLE: Record = { positive: { color: 'var(--grade-a)', bg: 'rgba(0,200,150,0.08)', border: 'rgba(0,200,150,0.40)' }, - caution: { color: 'var(--grade-c, #FFB347)', bg: 'rgba(255,179,71,0.08)', border: 'rgba(255,179,71,0.40)' }, + caution: { color: 'var(--grade-c, #B8BCC8)', bg: 'rgba(255,179,71,0.08)', border: 'rgba(255,179,71,0.40)' }, warning: { color: 'var(--grade-d, #ff5a5a)', bg: 'rgba(255,90,90,0.08)', border: 'rgba(255,90,90,0.40)' }, neutral: { color: 'var(--text-secondary)', bg: 'transparent', border: 'var(--border)' }, }; @@ -163,8 +163,8 @@ function parseSignals(summary: string | undefined): ParsedSignal[] { function gradeColor(grade: string): string { const g = (grade || '').trim().toUpperCase().charAt(0); if (g === 'A') return 'var(--grade-a)'; - if (g === 'B') return 'var(--grade-b, #4A9EFF)'; - if (g === 'C') return 'var(--grade-c, #FFB347)'; + if (g === 'B') return 'var(--grade-b, #F0F0F0)'; + if (g === 'C') return 'var(--grade-c, #B8BCC8)'; return 'var(--grade-d, #ff5a5a)'; }