SPB Git

spb/airiskindex Public

The most methodologically rigorous, fully transparent AI job-exposure index.

TypeScript 88% Python 6.1% SQL 2.7% CSS 1.2% JavaScript 0.9% Shell 0.8%

feat(web): design overhaul — sharper modern light design, fully mobile-responsive

Sticky blurred header, hero wash, pill kicker, card idiom with hover
elevation, refined footer (3-column), responsive paddings/typography,
scrollable audit tables on small screens, explicit viewport/themeColor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
simon-pierre boucher committed 6 days ago (Aug 5, 2026) parent efb3283

Showing 10 changed files with +195 and −78

modified apps/web/app/globals.css +45 −0
@@ -18,6 +18,7 @@
18 18 --seq-deep: #1c5cab;
19 19 --seq-track: #cde2fb; /* lighter step of the same ramp (meter/bar track) */
20 20 --wash: rgba(42, 120, 214, 0.1); /* series hue at ~10% — area wash */
21 + --header: rgba(252, 252, 251, 0.82);
21 22 }
22 23
23 24 @media (prefers-color-scheme: dark) {
@@ -35,10 +36,54 @@
35 36 --seq-deep: #6da7ec;
36 37 --seq-track: #0d366b; /* recessive step of the same ramp on dark */
37 38 --wash: rgba(57, 135, 229, 0.12);
39 + --header: rgba(13, 13, 13, 0.82);
38 40 }
39 41 }
40 42
43 +html {
44 + scroll-behavior: smooth;
45 +}
46 +
41 47 body {
42 48 background: var(--page);
43 49 color: var(--ink);
50 + text-rendering: optimizeLegibility;
51 + -webkit-font-smoothing: antialiased;
52 +}
53 +
54 +::selection {
55 + background: var(--seq);
56 + color: #ffffff;
57 +}
58 +
59 +:focus-visible {
60 + outline: 2px solid var(--seq);
61 + outline-offset: 2px;
62 + border-radius: 4px;
63 +}
64 +
65 +/* Hero backdrop — two soft washes of the series hue, light and airy. */
66 +.hero-wash {
67 + background:
68 + radial-gradient(56rem 26rem at 85% -8%, var(--wash), transparent 62%),
69 + radial-gradient(40rem 22rem at 8% -14%, var(--wash), transparent 55%);
70 +}
71 +
72 +/* Card idiom used across the site. */
73 +.card {
74 + border: 1px solid var(--border);
75 + background: var(--surface-1);
76 + border-radius: 1rem;
77 +}
78 +
79 +.card-hover {
80 + transition:
81 + border-color 160ms ease,
82 + box-shadow 160ms ease,
83 + transform 160ms ease;
84 +}
85 +
86 +.card-hover:hover {
87 + border-color: var(--seq);
88 + box-shadow: 0 4px 20px -8px rgba(42, 120, 214, 0.25);
44 89 }
modified apps/web/app/layout.tsx +121 −50
@@ -1,10 +1,33 @@
1 import type { Metadata } from "next";
1 +import type { Metadata, Viewport } from "next";
2 2 import Link from "next/link";
3 3 import type { ReactNode } from "react";
4 4 import { prisma } from "@airiskindex/db";
5 5 import { INDEX_VERSION } from "@airiskindex/scoring";
6 6 import "./globals.css";
7 7
8 +export const metadata: Metadata = {
9 + metadataBase: new URL("https://www.airiskindex.io"),
10 + title: "AI Risk Index — task-based AI exposure scores for every occupation",
11 + description:
12 + "Transparent, versioned, task-based scores of how occupations are exposed to AI — with separate exposure, substitution and augmentation sub-scores and confidence intervals. Adaptation guidance, not doom.",
13 + openGraph: {
14 + title: "AI Risk Index",
15 + description:
16 + "How is your occupation exposed to AI — task by task? Three sub-scores, confidence intervals, fully open methodology.",
17 + url: "https://www.airiskindex.io",
18 + siteName: "AI Risk Index",
19 + },
20 +};
21 +
22 +export const viewport: Viewport = {
23 + width: "device-width",
24 + initialScale: 1,
25 + themeColor: [
26 + { media: "(prefers-color-scheme: light)", color: "#f9f9f7" },
27 + { media: "(prefers-color-scheme: dark)", color: "#0d0d0d" },
28 + ],
29 +};
30 +
8 31 async function isDemoRun(): Promise<boolean> {
9 32 try {
10 33 const run = await prisma.scoreRun.findFirst({ orderBy: { createdAt: "desc" } });
@@ -14,11 +37,12 @@ async function isDemoRun(): Promise<boolean> {
14 37 }
15 38 }
16 39
17 export const metadata: Metadata = {
18 title: "AI Risk Index — task-based AI exposure scores for every occupation",
19 description:
20 "Transparent, versioned, task-based scores of how occupations are exposed to AI — with separate exposure, substitution and augmentation sub-scores and confidence intervals. Adaptation guidance, not doom.",
21 };
40 +const NAV = [
41 + ["/#ranking", "Ranking"],
42 + ["/occupations", "Browse"],
43 + ["/methodology", "Methodology"],
44 + ["/api/v1/methodology", "API"],
45 +] as const;
22 46
23 47 export default async function RootLayout({
24 48 children,
@@ -30,58 +54,105 @@ export default async function RootLayout({
30 54 <html lang="en">
31 55 <body className="min-h-screen antialiased">
32 56 {demo && (
33 <div className="border-b border-[var(--border)] bg-[var(--surface-1)] px-6 py-2 text-center text-xs text-[var(--ink-2)]">
34 Preview — scores below are computed from a demonstration dataset while the first
35 published index run ({INDEX_VERSION.replace("-draft.1", "")}) is in progress.
57 + <div className="bg-[var(--seq)] px-4 py-2 text-center text-xs font-medium text-white">
58 + Preview — scores are computed from a demonstration dataset while the first published
59 + index run ({INDEX_VERSION.replace("-draft.1", "")}) is in progress.
36 60 </div>
37 61 )}
38 <header className="border-b border-[var(--border)] bg-[var(--surface-1)]">
39 <nav className="mx-auto flex max-w-4xl items-center justify-between px-6 py-4">
40 <Link href="/" className="font-semibold tracking-tight">
62 + <header className="sticky top-0 z-40 border-b border-[var(--border)] backdrop-blur-md [background:var(--header)]">
63 + <nav className="mx-auto flex max-w-5xl items-center justify-between gap-3 px-4 py-3 sm:px-6">
64 + <Link href="/" className="flex shrink-0 items-center gap-2 font-semibold tracking-tight">
65 + <span
66 + aria-hidden="true"
67 + className="inline-block h-2.5 w-2.5 rounded-full bg-[var(--seq)]"
68 + />
41 69 AI Risk Index
42 70 </Link>
43 <div className="flex items-center gap-5 text-sm text-[var(--ink-2)]">
44 <Link href="/#ranking" className="hover:text-[var(--ink)]">
45 Ranking
46 </Link>
47 <Link href="/occupations" className="hover:text-[var(--ink)]">
48 Browse
49 </Link>
50 <Link href="/methodology" className="hover:text-[var(--ink)]">
51 Methodology
52 </Link>
53 <Link href="/api/v1/methodology" className="hover:text-[var(--ink)]">
54 API
55 </Link>
71 + <div className="flex items-center gap-4 overflow-x-auto whitespace-nowrap text-sm text-[var(--ink-2)] sm:gap-6">
72 + {NAV.map(([href, label]) => (
73 + <Link
74 + key={href}
75 + href={href}
76 + className="transition-colors hover:text-[var(--ink)]"
77 + >
78 + {label}
79 + </Link>
80 + ))}
56 81 </div>
57 82 </nav>
58 83 </header>
59 84 {children}
60 <footer className="mt-16 border-t border-[var(--border)] bg-[var(--surface-1)]">
61 <div className="mx-auto max-w-4xl px-6 py-8 text-xs leading-relaxed text-[var(--muted)]">
62 <p>
63 Methodology {INDEX_VERSION} · every score traces to a stored, immutable computation
64 run · weights, prompts and formulas are public. Occupation and task data from O*NET
65 (CC BY 4.0, U.S. Department of Labor). This index measures task-level pressure, not
66 certainty of job loss — read it as adaptation guidance.
67 </p>
68 <p className="mt-3 flex flex-wrap gap-x-4 gap-y-1">
69 <Link href="/methodology" className="hover:text-[var(--ink)] hover:underline">
70 Methodology
71 </Link>
72 <Link href="/terms" className="hover:text-[var(--ink)] hover:underline">
73 Terms of Service
74 </Link>
75 <Link href="/privacy" className="hover:text-[var(--ink)] hover:underline">
76 Privacy Policy
77 </Link>
78 <a
79 href="mailto:contact@spboucher.ai"
80 className="hover:text-[var(--ink)] hover:underline"
81 >
82 contact@spboucher.ai
83 </a>
84 <span>© 2026 Simon-Pierre Boucher</span>
85 + <footer className="mt-20 border-t border-[var(--border)] bg-[var(--surface-1)]">
86 + <div className="mx-auto grid max-w-5xl gap-8 px-4 py-10 sm:grid-cols-3 sm:px-6">
87 + <div>
88 + <p className="flex items-center gap-2 font-semibold tracking-tight">
89 + <span
90 + aria-hidden="true"
91 + className="inline-block h-2 w-2 rounded-full bg-[var(--seq)]"
92 + />
93 + AI Risk Index
94 + </p>
95 + <p className="mt-2 text-xs leading-relaxed text-[var(--muted)]">
96 + The transparent, task-based AI job-exposure index. Methodology {INDEX_VERSION} —
97 + every score traces to a stored, immutable computation run.
98 + </p>
99 + </div>
100 + <div className="text-sm">
101 + <p className="text-xs font-semibold uppercase tracking-wide text-[var(--muted)]">
102 + Explore
103 + </p>
104 + <ul className="mt-2 space-y-1.5 text-[var(--ink-2)]">
105 + <li>
106 + <Link href="/occupations" className="hover:text-[var(--ink)] hover:underline">
107 + All occupations
108 + </Link>
109 + </li>
110 + <li>
111 + <Link href="/methodology" className="hover:text-[var(--ink)] hover:underline">
112 + Methodology
113 + </Link>
114 + </li>
115 + <li>
116 + <Link
117 + href="/api/v1/methodology"
118 + className="hover:text-[var(--ink)] hover:underline"
119 + >
120 + Public API
121 + </Link>
122 + </li>
123 + </ul>
124 + </div>
125 + <div className="text-sm">
126 + <p className="text-xs font-semibold uppercase tracking-wide text-[var(--muted)]">
127 + Legal & contact
128 + </p>
129 + <ul className="mt-2 space-y-1.5 text-[var(--ink-2)]">
130 + <li>
131 + <Link href="/terms" className="hover:text-[var(--ink)] hover:underline">
132 + Terms of Service
133 + </Link>
134 + </li>
135 + <li>
136 + <Link href="/privacy" className="hover:text-[var(--ink)] hover:underline">
137 + Privacy Policy
138 + </Link>
139 + </li>
140 + <li>
141 + <a
142 + href="mailto:contact@spboucher.ai"
143 + className="hover:text-[var(--ink)] hover:underline"
144 + >
145 + contact@spboucher.ai
146 + </a>
147 + </li>
148 + </ul>
149 + </div>
150 + </div>
151 + <div className="border-t border-[var(--grid)]">
152 + <p className="mx-auto max-w-5xl px-4 py-4 text-xs leading-relaxed text-[var(--muted)] sm:px-6">
153 + Occupation and task data from O*NET (CC BY 4.0, U.S. Department of Labor) · wages
154 + from BLS OEWS. This index measures task-level pressure, not certainty of job loss —
155 + read it as adaptation guidance. © 2026 Simon-Pierre Boucher.
85 156 </p>
86 157 </div>
87 158 </footer>
modified apps/web/app/methodology/page.tsx +5 −5
@@ -82,7 +82,7 @@ function Section({
82 82
83 83 export default function MethodologyPage(): JSX.Element {
84 84 return (
85 <main className="mx-auto max-w-3xl px-6 py-14">
85 + <main className="mx-auto max-w-3xl px-4 py-10 sm:px-6 sm:py-14">
86 86 <p className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--muted)]">
87 87 Methodology · version {INDEX_VERSION}
88 88 </p>
@@ -99,7 +99,7 @@ export default function MethodologyPage(): JSX.Element {
99 99 .
100 100 </p>
101 101
102 <nav className="mt-8 rounded-xl border border-[var(--border)] bg-[var(--surface-1)] p-5 text-sm">
102 + <nav className="mt-8 card p-5 text-sm">
103 103 <p className="font-medium text-[var(--ink)]">Contents</p>
104 104 <ol className="mt-2 list-inside list-decimal space-y-1 text-[var(--ink-2)]">
105 105 {[
@@ -181,7 +181,7 @@ export default function MethodologyPage(): JSX.Element {
181 181 </Section>
182 182
183 183 <Section id="data" title="3. Data sources">
184 <div className="overflow-x-auto rounded-xl border border-[var(--border)] bg-[var(--surface-1)]">
184 + <div className="overflow-x-auto card">
185 185 <table className="w-full text-left text-sm">
186 186 <thead className="border-b border-[var(--grid)] text-xs uppercase tracking-wide text-[var(--muted)]">
187 187 <tr>
@@ -265,7 +265,7 @@ export default function MethodologyPage(): JSX.Element {
265 265 </Section>
266 266
267 267 <Section id="dimensions" title="5. The five dimensions and their weights">
268 <div className="overflow-x-auto rounded-xl border border-[var(--border)] bg-[var(--surface-1)]">
268 + <div className="overflow-x-auto card">
269 269 <table className="w-full text-left text-sm">
270 270 <thead className="border-b border-[var(--grid)] text-xs uppercase tracking-wide text-[var(--muted)]">
271 271 <tr>
@@ -319,7 +319,7 @@ export default function MethodologyPage(): JSX.Element {
319 319 <p>
320 320 A rating <em>r</em> ∈ [1, 5] normalizes to substitution pressure <em>p</em> ∈ [0, 1]:
321 321 </p>
322 <pre className="overflow-x-auto rounded-xl border border-[var(--border)] bg-[var(--surface-1)] p-4 text-sm text-[var(--ink)]">
322 + <pre className="overflow-x-auto card p-4 text-sm text-[var(--ink)]">
323 323 {`p = (r − 1) / 4 direct dimensions
324 324 p = 1 − (r − 1) / 4 inverted dimensions (barriers)
325 325
modified apps/web/app/occupations/[code]/page.tsx +9 −9
@@ -137,7 +137,7 @@ export default async function OccupationPage({
137 137 const wage = formatWage(occupation.medianWageCents);
138 138
139 139 return (
140 <main className="mx-auto max-w-4xl px-6 py-12">
140 + <main className="mx-auto max-w-4xl px-4 py-10 sm:px-6 sm:py-12">
141 141 <nav className="text-sm text-[var(--ink-2)]">
142 142 <Link href="/#ranking" className="hover:text-[var(--ink)]">
143 143 Ranking
@@ -176,7 +176,7 @@ export default async function OccupationPage({
176 176 )}
177 177
178 178 {!score ? (
179 <div className="mt-10 rounded-xl border border-[var(--border)] bg-[var(--surface-1)] p-8 text-sm text-[var(--ink-2)]">
179 + <div className="mt-10 card p-8 text-sm text-[var(--ink-2)]">
180 180 This occupation has not been scored yet — its {occupation.tasks.length} tasks are queued
181 181 for the multi-model rater panel. Task statements are listed below.
182 182 <ul className="mt-4 list-inside list-disc space-y-1">
@@ -236,7 +236,7 @@ export default async function OccupationPage({
236 236 </Link>
237 237 .
238 238 </p>
239 <div className="mt-5 overflow-hidden rounded-xl border border-[var(--border)] bg-[var(--surface-1)]">
239 + <div className="mt-5 overflow-hidden card">
240 240 {dimensions.map((dim) => (
241 241 <div key={dim.dimension} className="border-b border-[var(--grid)] px-5 py-3.5 last:border-b-0">
242 242 <div className="flex items-baseline gap-2">
@@ -282,7 +282,7 @@ export default async function OccupationPage({
282 282 Expand a task for the full rater audit trail — every rating, every model, every
283 283 rationale.
284 284 </p>
285 <div className="mt-5 overflow-hidden rounded-xl border border-[var(--border)] bg-[var(--surface-1)]">
285 + <div className="mt-5 overflow-hidden card">
286 286 {rankedTasks.map((task) => {
287 287 const ts = byTask.get(task.id);
288 288 const taskRatings = ratingsByTask.get(task.id) ?? [];
@@ -320,8 +320,8 @@ export default async function OccupationPage({
320 320 )}
321 321 </summary>
322 322 {taskRatings.length > 0 && (
323 <div className="border-t border-[var(--grid)] bg-[var(--page)] px-5 py-4">
324 <table className="w-full text-left text-xs">
323 + <div className="overflow-x-auto border-t border-[var(--grid)] bg-[var(--page)] px-5 py-4">
324 + <table className="w-full min-w-[480px] text-left text-xs">
325 325 <caption className="sr-only">
326 326 Panel ratings per dimension for this task
327 327 </caption>
@@ -366,7 +366,7 @@ export default async function OccupationPage({
366 366 <h2 className="text-xl font-semibold tracking-tight">
367 367 Related occupations — {socGroupName(occupation.code)}
368 368 </h2>
369 <ul className="mt-4 overflow-hidden rounded-xl border border-[var(--border)] bg-[var(--surface-1)]">
369 + <ul className="mt-4 overflow-hidden card">
370 370 {related.map((row) => (
371 371 <li key={row.occupationCode} className="border-b border-[var(--grid)] last:border-b-0">
372 372 <Link
@@ -385,7 +385,7 @@ export default async function OccupationPage({
385 385 )}
386 386
387 387 <section className="mt-12 grid gap-4 sm:grid-cols-2">
388 <div className="rounded-xl border border-[var(--border)] bg-[var(--surface-1)] p-6">
388 + <div className="card p-6">
389 389 <h2 className="font-semibold">How to read this</h2>
390 390 <p className="mt-2 text-sm leading-relaxed text-[var(--ink-2)]">
391 391 A high substitution score does not mean this job disappears — it means a large
@@ -395,7 +395,7 @@ export default async function OccupationPage({
395 395 the rater panel disagreed: treat those scores as open questions, not verdicts.
396 396 </p>
397 397 </div>
398 <div className="rounded-xl border border-[var(--border)] bg-[var(--surface-1)] p-6">
398 + <div className="card p-6">
399 399 <h2 className="font-semibold">What would change this score</h2>
400 400 <p className="mt-2 text-sm leading-relaxed text-[var(--ink-2)]">
401 401 New model capabilities (automatability, feasibility), falling inference costs
modified apps/web/app/occupations/page.tsx +1 −1
@@ -33,7 +33,7 @@ export default async function OccupationsPage(): Promise<JSX.Element> {
33 33 }
34 34
35 35 return (
36 <main className="mx-auto max-w-5xl px-6 py-12">
36 + <main className="mx-auto max-w-5xl px-4 py-10 sm:px-6 sm:py-12">
37 37 <h1 className="text-3xl font-bold tracking-tight">All occupations</h1>
38 38 <p className="mt-2 text-[var(--ink-2)]">
39 39 {occupations.length.toLocaleString("en-US")} O*NET occupations across{" "}
modified apps/web/app/page.tsx +8 −7
@@ -76,7 +76,7 @@ async function loadHome() {
76 76
77 77 function RankList({ rows, startRank }: { rows: RankRow[]; startRank?: number }): JSX.Element {
78 78 return (
79 <ol className="overflow-hidden rounded-xl border border-[var(--border)] bg-[var(--surface-1)]">
79 + <ol className="overflow-hidden card">
80 80 {rows.map((row, index) => (
81 81 <li key={row.occupationCode} className="border-b border-[var(--grid)] last:border-b-0">
82 82 <Link
@@ -119,11 +119,12 @@ export default async function HomePage(): Promise<JSX.Element> {
119 119 const { occupations, tasks, run, scored, top, bottom, groups } = await loadHome();
120 120
121 121 return (
122 <main className="mx-auto max-w-5xl px-6 py-14">
123 <p className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--muted)]">
122 + <main className="hero-wash mx-auto max-w-5xl px-4 py-10 sm:px-6 sm:py-16">
123 + <p className="inline-flex items-center gap-2 rounded-full border border-[var(--border)] bg-[var(--surface-1)] px-3 py-1 text-xs font-medium text-[var(--ink-2)]">
124 + <span aria-hidden="true" className="h-1.5 w-1.5 rounded-full bg-[var(--seq)]" />
124 125 The transparent, task-based AI job-exposure index
125 126 </p>
126 <h1 className="mt-3 max-w-2xl text-4xl font-bold tracking-tight sm:text-5xl">
127 + <h1 className="mt-5 max-w-2xl text-4xl font-bold leading-[1.08] tracking-tight sm:text-6xl">
127 128 How is your occupation exposed to AI — task by task?
128 129 </h1>
129 130 <p className="mt-4 max-w-2xl text-lg leading-relaxed text-[var(--ink-2)]">
@@ -145,7 +146,7 @@ export default async function HomePage(): Promise<JSX.Element> {
145 146 ].map(([value, label]) => (
146 147 <div
147 148 key={label as string}
148 className="rounded-xl border border-[var(--border)] bg-[var(--surface-1)] p-4"
149 + className="card card-hover p-4"
149 150 >
150 151 <p className="text-2xl font-semibold">{value}</p>
151 152 <p className="mt-1 text-xs text-[var(--muted)]">{label}</p>
@@ -187,7 +188,7 @@ export default async function HomePage(): Promise<JSX.Element> {
187 188 <p className="mt-1 text-sm text-[var(--ink-2)]">
188 189 Mean substitution score across scored occupations in each SOC major group.
189 190 </p>
190 <div className="mt-5 overflow-hidden rounded-xl border border-[var(--border)] bg-[var(--surface-1)]">
191 + <div className="mt-5 overflow-hidden card">
191 192 {groups.map((group) => (
192 193 <Link
193 194 key={group.prefix}
@@ -230,7 +231,7 @@ export default async function HomePage(): Promise<JSX.Element> {
230 231 {CONCEPTS.map((concept) => (
231 232 <div
232 233 key={concept.name}
233 className="rounded-xl border border-[var(--border)] bg-[var(--surface-1)] p-5"
234 + className="card card-hover p-5"
234 235 >
235 236 <h3 className="font-semibold">{concept.name}</h3>
236 237 <p className="mt-2 text-sm leading-relaxed text-[var(--ink-2)]">
modified apps/web/app/privacy/page.tsx +1 −1
@@ -13,7 +13,7 @@ function Section({ title, children }: { title: string; children: React.ReactNode
13 13
14 14 export default function PrivacyPage(): JSX.Element {
15 15 return (
16 <main className="mx-auto max-w-3xl px-6 py-14">
16 + <main className="mx-auto max-w-3xl px-4 py-10 sm:px-6 sm:py-14">
17 17 <h1 className="text-4xl font-bold tracking-tight">Privacy Policy</h1>
18 18 <p className="mt-3 text-sm text-[var(--muted)]">Effective date: August 5, 2026</p>
19 19 <p className="mt-4 leading-relaxed text-[var(--ink-2)]">
modified apps/web/app/terms/page.tsx +1 −1
@@ -13,7 +13,7 @@ function Section({ title, children }: { title: string; children: React.ReactNode
13 13
14 14 export default function TermsPage(): JSX.Element {
15 15 return (
16 <main className="mx-auto max-w-3xl px-6 py-14">
16 + <main className="mx-auto max-w-3xl px-4 py-10 sm:px-6 sm:py-14">
17 17 <h1 className="text-4xl font-bold tracking-tight">Terms of Service</h1>
18 18 <p className="mt-3 text-sm text-[var(--muted)]">Effective date: August 5, 2026</p>
19 19 <p className="mt-4 leading-relaxed text-[var(--ink-2)]">
modified apps/web/components/occupation-search.tsx +2 −2
@@ -59,10 +59,10 @@ export function OccupationSearch(): JSX.Element {
59 59 onFocus={() => items.length > 0 && setOpen(true)}
60 60 placeholder="Search 1,000+ occupations — e.g. paralegal, radiologist, roofer…"
61 61 aria-label="Search occupations"
62 className="w-full rounded-xl border border-[var(--border)] bg-[var(--surface-1)] px-4 py-3 text-base outline-none placeholder:text-[var(--muted)] focus:border-[var(--seq)]"
62 + className="w-full card px-4 py-3 text-base outline-none placeholder:text-[var(--muted)] focus:border-[var(--seq)]"
63 63 />
64 64 {open && items.length > 0 && (
65 <ul className="absolute z-20 mt-2 w-full overflow-hidden rounded-xl border border-[var(--border)] bg-[var(--surface-1)] shadow-lg">
65 + <ul className="absolute z-20 mt-2 w-full overflow-hidden card shadow-lg">
66 66 {items.map((item) => (
67 67 <li key={item.code} className="border-b border-[var(--grid)] last:border-b-0">
68 68 <Link
modified apps/web/components/score-marks.tsx +2 −2
@@ -70,7 +70,7 @@ export function ScoreTile({
70 70 hint: string;
71 71 }): JSX.Element {
72 72 return (
73 <div className="rounded-xl border border-[var(--border)] bg-[var(--surface-1)] p-5">
73 + <div className="card p-5">
74 74 <p className="text-sm font-medium text-[var(--ink-2)]">{label}</p>
75 75 <p className="mt-1 text-4xl font-semibold">
76 76 {band.score.toFixed(0)}
@@ -90,7 +90,7 @@ export function ScoreTile({
90 90 export function ShareMeter({ share, label }: { share: number; label: string }): JSX.Element {
91 91 const value = Math.round(share * 100);
92 92 return (
93 <div className="rounded-xl border border-[var(--border)] bg-[var(--surface-1)] p-5">
93 + <div className="card p-5">
94 94 <div className="flex items-baseline justify-between">
95 95 <p className="text-sm font-medium text-[var(--ink-2)]">{label}</p>
96 96 <p className="text-2xl font-semibold">{value}%</p>
97 97