diff --git a/src/app/globals.css b/src/app/globals.css index f27f489..dc6b946 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -19,6 +19,8 @@ /* Keep in sync with BRAND_GRADIENT in components/logo.tsx — same colors, same stops, same angle. */ --gradient-sunset: linear-gradient(135deg, var(--violet) 0%, var(--magenta) 55%, var(--amber) 100%); + /* The darker stops keep white action text at or above 4.5:1 across the gradient. */ + --gradient-action: linear-gradient(135deg, #5b45a5 0%, #a43b72 55%, #8a6200 100%); --gradient-glow: radial-gradient(60% 60% at 50% 0%, rgba(167, 139, 250, 0.35), transparent 70%); } @@ -56,6 +58,10 @@ body { background-image: var(--gradient-sunset); } +.bg-gradient-action { + background-image: var(--gradient-action); +} + .border-gradient { position: relative; border: 1px solid transparent; diff --git a/src/app/page.tsx b/src/app/page.tsx index 4b13da3..4704412 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -260,11 +260,11 @@ export default function Home() {
-
+

Ready to issue your first ticket?

-

+

For events, travel, entertainment, and transportation — one platform, open source top to bottom.

@@ -277,7 +277,7 @@ export default function Home() { Learn more diff --git a/src/components/audience-toggle.tsx b/src/components/audience-toggle.tsx index 6f50ee9..8d38548 100644 --- a/src/components/audience-toggle.tsx +++ b/src/components/audience-toggle.tsx @@ -39,7 +39,7 @@ export function AudienceToggle() {
diff --git a/src/components/button.test.ts b/src/components/button.test.ts index d2798c2..82dd619 100644 --- a/src/components/button.test.ts +++ b/src/components/button.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest'; +import { readFileSync } from 'node:fs'; import { createElement } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { Button, buttonClassName } from './button'; @@ -12,7 +13,7 @@ function render(props: Parameters[0]): HTMLButtonElement { describe('buttonClassName', () => { it('defaults to the primary md style', () => { const cls = buttonClassName(); - expect(cls).toContain('bg-gradient-sunset'); + expect(cls).toContain('bg-gradient-action'); expect(cls).toContain('rounded-md px-4 py-2'); }); @@ -28,6 +29,44 @@ describe('buttonClassName', () => { }); }); +describe('action gradient contrast', () => { + it('keeps white text at WCAG AA contrast across the CSS gradient', () => { + const css = readFileSync('src/app/globals.css', 'utf8'); + const declaration = css.match(/--gradient-action:\s*linear-gradient\([^;]+\);/i)?.[0]; + expect(declaration).toBeDefined(); + + const stops = [...declaration!.matchAll(/(#[\da-f]{6})\s+([\d.]+)%/gi)].map((match) => ({ + position: Number(match[2]) / 100, + color: [1, 3, 5].map((offset) => Number.parseInt(match[1].slice(offset, offset + 2), 16)), + })); + expect(stops.length).toBeGreaterThanOrEqual(2); + expect(stops[0].position).toBe(0); + expect(stops.at(-1)?.position).toBe(1); + + const linearise = (channel: number) => { + const srgb = channel / 255; + return srgb <= 0.04045 ? srgb / 12.92 : ((srgb + 0.055) / 1.055) ** 2.4; + }; + const luminance = (color: number[]) => + 0.2126 * linearise(color[0]) + 0.7152 * linearise(color[1]) + 0.0722 * linearise(color[2]); + + let minimumContrast = Number.POSITIVE_INFINITY; + for (let index = 0; index < stops.length - 1; index += 1) { + const start = stops[index]; + const end = stops[index + 1]; + for (let sample = 0; sample <= 1000; sample += 1) { + const progress = sample / 1000; + const color = start.color.map((channel, component) => + channel + (end.color[component] - channel) * progress, + ); + minimumContrast = Math.min(minimumContrast, 1.05 / (luminance(color) + 0.05)); + } + } + + expect(minimumContrast).toBeGreaterThanOrEqual(4.5); + }); +}); + describe('Button', () => { it('defaults to type="button" and is not busy', () => { const btn = render({ children: 'Go' }); diff --git a/src/components/button.tsx b/src/components/button.tsx index d142875..74d59eb 100644 --- a/src/components/button.tsx +++ b/src/components/button.tsx @@ -4,7 +4,7 @@ export type ButtonVariant = 'primary' | 'secondary' | 'danger'; export type ButtonSize = 'sm' | 'md' | 'lg'; const VARIANT_CLASSES: Record = { - primary: 'bg-gradient-sunset text-white shadow-lg shadow-violet/20 hover:opacity-90', + primary: 'bg-gradient-action text-white shadow-lg shadow-violet/20 hover:opacity-90', secondary: 'border border-border hover:bg-surface', danger: 'border border-red-900/50 text-red-300 hover:bg-red-950/30', }; diff --git a/src/components/navbar.tsx b/src/components/navbar.tsx index 0e207ac..fc1fbf2 100644 --- a/src/components/navbar.tsx +++ b/src/components/navbar.tsx @@ -106,7 +106,7 @@ export function Navbar() { Get started @@ -160,7 +160,7 @@ export function Navbar() { Get started diff --git a/src/components/pill-button.tsx b/src/components/pill-button.tsx index 4bbd359..237b506 100644 --- a/src/components/pill-button.tsx +++ b/src/components/pill-button.tsx @@ -16,12 +16,12 @@ export function PillButton({ href={href} className={`group relative inline-flex items-center overflow-hidden rounded-full border px-6 py-3 font-semibold ${ variant === 'solid' - ? 'border-transparent bg-gradient-sunset text-white shadow-lg shadow-violet/20' + ? 'border-transparent bg-gradient-action text-white shadow-lg shadow-violet/20' : 'border-border-bright text-foreground' }`} > {variant === 'outline' && ( - + )} {children}