diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 05ed9df..941df82 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -37,6 +37,13 @@ jobs: run: pnpm install --frozen-lockfile - name: Typecheck (tsc over src/ + tests/ -- blocking) run: pnpm run typecheck + # UI-09: 0pt carried 82 axe color-contrast violations (impact=serious), the + # estate's largest single a11y defect, and every one came from a Tailwind + # alpha modifier on a text colour. The unmodified token measures 4.73:1 + # light / 7.63:1 dark and passes AA; every faded variant fails. Blocking, + # because a contrast regression is invisible in review. + - name: Text contrast shape (WCAG 2.2 AA 1.4.3 -- blocking) + run: pnpm run check:contrast - name: Test (vitest -- blocking) run: pnpm run test - name: Lint (eslint -- advisory; legacy tree has pre-existing noise) diff --git a/app/admin/verktyg/page.tsx b/app/admin/verktyg/page.tsx index db0e2a2..cddfa8e 100644 --- a/app/admin/verktyg/page.tsx +++ b/app/admin/verktyg/page.tsx @@ -88,7 +88,7 @@ function ScreenTool() { ))} )} -

{t({ en: "Requires verified OpenSanctions terms/key for full matches.", sv: "Kräver verifierade OpenSanctions-villkor/nyckel för fullständiga träffar." })}

+

{t({ en: "Requires verified OpenSanctions terms/key for full matches.", sv: "Kräver verifierade OpenSanctions-villkor/nyckel för fullständiga träffar." })}

); diff --git a/app/bestall/beskriv/page.tsx b/app/bestall/beskriv/page.tsx index f688ff7..29440d9 100644 --- a/app/bestall/beskriv/page.tsx +++ b/app/bestall/beskriv/page.tsx @@ -79,7 +79,7 @@ export default function DescribePage() { ))} - {svc ?

{t(svc.covers)} → {t(svc.deliverable)}

: null} + {svc ?

{t(svc.covers)} → {t(svc.deliverable)}

: null}
diff --git a/app/kallor/page.tsx b/app/kallor/page.tsx index 48c4929..4de3b0d 100644 --- a/app/kallor/page.tsx +++ b/app/kallor/page.tsx @@ -273,7 +273,7 @@ export default function SourcesPage() { > {s.name} - + {s.source_id} @@ -301,7 +301,7 @@ export default function SourcesPage() {

{rows.length} {t({ en: "sources · every field carries its provenance", sv: "källor · varje fält bär sin härkomst" })}

-

+

{t({ en: "Licence terms verified before use", sv: "Licensvillkor verifieras innan användning" })}

diff --git a/app/portal/page.tsx b/app/portal/page.tsx index 7e5bb8c..e850326 100644 --- a/app/portal/page.tsx +++ b/app/portal/page.tsx @@ -286,7 +286,7 @@ function CopyId({ id }: { id: string }) { setTimeout(() => setCopied(false), 2000); }); }} - className="text-foreground/20 transition-colors hover:text-foreground/60" + className="text-muted-foreground transition-colors hover:text-foreground" > {copied ? "✓" : "⧉"} diff --git a/app/tjanster/page.tsx b/app/tjanster/page.tsx index d65e1f6..bd6c884 100644 --- a/app/tjanster/page.tsx +++ b/app/tjanster/page.tsx @@ -40,7 +40,7 @@ export default function ServicesPage() {

{t(FLOW_LABEL[flow])}

- {String(items.length).padStart(2, "0")} + {String(items.length).padStart(2, "0")}
{items.map((s, i) => { @@ -50,7 +50,7 @@ export default function ServicesPage() {

{t(s.name)}

{t(s.tagline)}

{t(s.covers)}

-

→ {t(s.deliverable)}

+

→ {t(s.deliverable)}

{t(cta.label)} → diff --git a/components/header.tsx b/components/header.tsx index 65831b2..2ad979d 100644 --- a/components/header.tsx +++ b/components/header.tsx @@ -123,7 +123,7 @@ export function Header() { className="flex items-baseline justify-between border-b border-border py-4 font-mono text-[12px] uppercase tracking-[0.12em] text-muted-foreground transition-colors hover:text-foreground" > {t(n.label)} - {String(i + 1).padStart(2, "0")} + {String(i + 1).padStart(2, "0")} ))}
diff --git a/components/sections/collection-section.tsx b/components/sections/collection-section.tsx index 13bd99d..ae22c15 100644 --- a/components/sections/collection-section.tsx +++ b/components/sections/collection-section.tsx @@ -44,7 +44,7 @@ export function CollectionSection() { >

{m.code}

- {String(i + 1).padStart(2, "0")} + {String(i + 1).padStart(2, "0")}

{m.live ? `${m.coverage}%` : t({ en: "coming", sv: "kommande" })} diff --git a/components/sections/featured-products-section.tsx b/components/sections/featured-products-section.tsx index 47807f9..2f3cdd3 100644 --- a/components/sections/featured-products-section.tsx +++ b/components/sections/featured-products-section.tsx @@ -91,7 +91,7 @@ export function FeaturedProductsSection() {

{d.markets}

- {String(i + 1).padStart(2, "0")} + {String(i + 1).padStart(2, "0")}

{t(d.title)}

{t(d.desc)}

diff --git a/components/sections/philosophy-section.tsx b/components/sections/philosophy-section.tsx index a04be34..525ef26 100644 --- a/components/sections/philosophy-section.tsx +++ b/components/sections/philosophy-section.tsx @@ -33,7 +33,7 @@ export function PhilosophySection() { className={`reveal delay-${i + 1} group relative grid grid-cols-[3.5rem_1fr] items-baseline gap-8 border-b border-border py-8 transition-[padding] duration-300 ease-out hover:pl-2`} > - + {n}
diff --git a/components/sections/pricing-section.tsx b/components/sections/pricing-section.tsx index 648b760..65f7747 100644 --- a/components/sections/pricing-section.tsx +++ b/components/sections/pricing-section.tsx @@ -64,7 +64,7 @@ export function PricingSection() { {MODELS.map((m, i) => (
- {m.index} + {m.index} {m.featured ? : null}
diff --git a/components/sections/testimonials-section.tsx b/components/sections/testimonials-section.tsx index 759046c..21b2d63 100644 --- a/components/sections/testimonials-section.tsx +++ b/components/sections/testimonials-section.tsx @@ -32,7 +32,7 @@ export function TestimonialsSection() { key={l.sv} className={`reveal delay-${i + 2} group relative border-b border-border py-12 sm:border-b-0 sm:border-l sm:py-0 sm:pl-8 sm:pr-4 ${i === 0 ? "sm:border-l-0 sm:pl-0" : ""}`} > -

0{i + 1}

+

0{i + 1}

{ + for (const m of line.matchAll(PATTERN)) { + // Decorative text is exempt when the element declares itself hidden to AT. + // The attribute may sit on an earlier line of the same JSX element. + const window = lines.slice(Math.max(0, i - 6), i + 1).join('\n'); + if (/\baria-hidden\b/.test(window)) continue; + violations.push({ file: relative(ROOT, file), line: i + 1, token: m[0], text: line.trim() }); + } + }); +} + +if (violations.length) { + console.error(`\n✗ ${violations.length} alpha-faded text colour(s) found — WCAG 2.2 AA 1.4.3 (finding UI-09).\n`); + for (const v of violations) { + console.error(` ${v.file}:${v.line} ${v.token}`); + console.error(` ${v.text.slice(0, 160)}`); + } + console.error('\n Use the unmodified token (text-muted-foreground = 4.73:1 light / 7.63:1 dark).'); + console.error(' If the text really is decoration, put aria-hidden on the element.\n'); + process.exit(1); +} +console.log('✓ no alpha-faded text colours — WCAG 2.2 AA 1.4.3 contrast shape is clean (UI-09)');