portable-agent-layer 0.38.0 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/assets/skills/consulting-report/SKILL.md +40 -1
  2. package/assets/skills/consulting-report/template/app/globals.css +51 -362
  3. package/assets/skills/consulting-report/template/app/layout.tsx +2 -8
  4. package/assets/skills/consulting-report/template/components/band-badge.tsx +72 -0
  5. package/assets/skills/consulting-report/template/components/callout.tsx +5 -3
  6. package/assets/skills/consulting-report/template/components/comparison-table.tsx +13 -7
  7. package/assets/skills/consulting-report/template/components/configuration-table.tsx +87 -0
  8. package/assets/skills/consulting-report/template/components/cover-page.tsx +18 -6
  9. package/assets/skills/consulting-report/template/components/exhibit.tsx +7 -5
  10. package/assets/skills/consulting-report/template/components/finding-card.tsx +7 -5
  11. package/assets/skills/consulting-report/template/components/quote-block.tsx +13 -3
  12. package/assets/skills/consulting-report/template/components/recommendation-card.tsx +6 -4
  13. package/assets/skills/consulting-report/template/components/rubric-table.tsx +104 -0
  14. package/assets/skills/consulting-report/template/components/score-badge.tsx +51 -0
  15. package/assets/skills/consulting-report/template/components/scorecard.tsx +154 -0
  16. package/assets/skills/consulting-report/template/components/section.tsx +16 -1
  17. package/assets/skills/consulting-report/template/components/severity-badge.tsx +20 -5
  18. package/assets/skills/consulting-report/template/components/stat-grid.tsx +11 -5
  19. package/assets/skills/consulting-report/template/components/table-of-contents.tsx +17 -7
  20. package/assets/skills/consulting-report/template/components/template-block.tsx +20 -0
  21. package/assets/skills/consulting-report/template/components/timeline.tsx +25 -6
  22. package/assets/skills/consulting-report/template/components/tuning-log.tsx +87 -0
  23. package/assets/skills/consulting-report/template/lib/report-data.ts +26 -74
  24. package/assets/skills/consulting-report/template/lib/types.ts +190 -0
  25. package/package.json +4 -5
@@ -6,7 +6,46 @@ argument-hint: scaffold <target-dir> | dev <report-dir> | <report-dir> (render P
6
6
 
7
7
  ## Overview
8
8
 
9
- Every report is a self-contained Next.js app: typed report data in `lib/report-data.ts`, layout composed from React components in `app/page.tsx`, fonts via `next/font/google` (Source Serif 4 + Inter), Tailwind v4 for styling. `bun run dev` gives a live preview while authoring; the PDF is rendered by Playwright against a static export.
9
+ Every report is a self-contained Next.js app: typed report data in `lib/report-data.ts`, layout composed from React components in `app/page.tsx`, font via `next/font/google` (Inter), Tailwind v4 for styling. `bun run dev` gives a live preview while authoring; the PDF is rendered by Playwright against a static export.
10
+
11
+ ## Authoring contract — what belongs where
12
+
13
+ The split is deliberate. Don't edit the template for one report; don't put report data in the template.
14
+
15
+ | File | Lives in | Edited by |
16
+ |------|----------|-----------|
17
+ | `lib/types.ts` | Template | Skill maintainer only (when adding new section types) |
18
+ | `lib/report-data.ts` | Project | **You.** Replace the scaffolded placeholder with your data. |
19
+ | `app/page.tsx` | Project | **You.** Compose your layout from the components in `@/components`. |
20
+ | `components/*.tsx` | Template | Skill maintainer only (when adding new primitives) |
21
+ | `app/globals.css` | Project (copied from template) | Brand colors and project-specific overrides only. |
22
+
23
+ **Tunable parameters** (e.g., the "TUNABLE" marker on configuration rows) take their label from the data:
24
+
25
+ ```ts
26
+ // In your project's report-data.ts:
27
+ { name: "Decision band thresholds", currentValue: "16–20 strong …",
28
+ tunable: true, tunableLabel: "Owner edit" }
29
+ ```
30
+
31
+ The component renders whatever `tunableLabel` you provide; the template never hardcodes a person's name.
32
+
33
+ **Custom band labels** (Scorecard / BandBadge): the `band` field is a free-form string. Pass `bandStyles` to `<Scorecard>` to map your band labels to colors:
34
+
35
+ ```tsx
36
+ <Scorecard
37
+ scorecard={...}
38
+ bandStyles={{
39
+ Greenlit: { color: "...", background: "...", borderColor: "..." },
40
+ Investigate: { ... },
41
+ Hold: { ... },
42
+ }}
43
+ />
44
+ ```
45
+
46
+ **Configurable columns** (ConfigurationTable, TuningLog): pass a `columns` prop to override the default schema. See each component's source for the column type.
47
+
48
+ **Rubric levels** (RubricTable): supports any number of levels — the colors sample the default palette evenly. Pass `palette` to override.
10
49
 
11
50
  ## Workflow
12
51
 
@@ -24,9 +24,8 @@
24
24
  --color-callout: #eff6ff;
25
25
 
26
26
  --font-sans: var(--font-inter), system-ui, sans-serif;
27
- --font-serif: var(--font-source-serif), Georgia, serif;
28
27
  --font-heading: var(--font-inter), system-ui, sans-serif;
29
- --font-body: var(--font-source-serif), Georgia, serif;
28
+ --font-body: var(--font-inter), system-ui, sans-serif;
30
29
  }
31
30
 
32
31
  @source "./**/*.{ts,tsx}";
@@ -41,320 +40,42 @@ body {
41
40
  -moz-osx-font-smoothing: grayscale;
42
41
  }
43
42
 
44
- /* McKinsey-style report layout */
43
+ /* Restore default list markers (Tailwind's preflight strips them).
44
+ Scoped to @layer base so Tailwind utilities (e.g., `list-none`, `m-0`)
45
+ on individual components still win. */
46
+ @layer base {
47
+ ol, ul {
48
+ padding-left: 1.6rem;
49
+ margin: 0.75rem 0;
50
+ }
51
+ ol { list-style: decimal outside; }
52
+ ul { list-style: disc outside; }
53
+ li { margin: 0.25rem 0; }
54
+ li::marker {
55
+ color: var(--color-muted);
56
+ font-weight: 600;
57
+ }
58
+ }
59
+
60
+ /* Page-level wrapper used by every report's app/page.tsx. */
45
61
  .report-container {
46
62
  max-width: 850px;
47
63
  margin: 0 auto;
48
64
  padding: 2rem;
49
65
  }
50
66
 
51
- .report-section {
52
- margin-bottom: 3rem;
53
- page-break-inside: avoid;
54
- }
55
-
56
- .report-section h2 {
57
- font-family: var(--font-heading);
58
- font-size: 1.75rem;
59
- font-weight: 600;
60
- color: var(--color-foreground);
61
- margin-bottom: 1.5rem;
62
- padding-bottom: 0.5rem;
63
- border-bottom: 2px solid var(--color-primary);
64
- }
65
-
66
- .report-section h3 {
67
- font-family: var(--font-heading);
68
- font-size: 1.25rem;
69
- font-weight: 600;
70
- color: var(--color-foreground);
71
- margin-bottom: 1rem;
72
- }
73
-
74
- /* Exhibit */
75
- .exhibit {
76
- background: var(--color-background-secondary);
77
- border: 1px solid var(--color-border);
78
- border-radius: 0.5rem;
79
- padding: 1.5rem;
80
- margin: 1.5rem 0;
81
- }
82
- .exhibit-header {
83
- display: flex;
84
- justify-content: space-between;
85
- align-items: baseline;
86
- margin-bottom: 1rem;
87
- padding-bottom: 0.5rem;
88
- border-bottom: 1px solid var(--color-border-subtle);
89
- }
90
- .exhibit-number {
91
- font-family: var(--font-sans);
92
- font-weight: 600;
93
- color: var(--color-primary);
94
- font-size: 0.875rem;
95
- text-transform: uppercase;
96
- letter-spacing: 0.1em;
97
- }
98
- .exhibit-title {
99
- font-family: var(--font-heading);
100
- font-weight: 600;
101
- color: var(--color-foreground);
102
- }
103
-
104
- /* Callout */
105
- .callout {
106
- background: var(--color-callout);
107
- border-left: 4px solid var(--color-primary);
108
- padding: 1.25rem 1.5rem;
109
- margin: 1.5rem 0;
110
- border-radius: 0 0.5rem 0.5rem 0;
111
- }
112
- .callout-label {
113
- font-family: var(--font-sans);
114
- font-weight: 600;
115
- color: var(--color-primary);
116
- font-size: 0.75rem;
117
- text-transform: uppercase;
118
- letter-spacing: 0.1em;
119
- margin-bottom: 0.5rem;
120
- }
121
- .callout-content {
122
- font-size: 1.125rem;
123
- font-weight: 500;
124
- color: var(--color-foreground);
125
- }
126
-
127
- /* Quote block */
128
- .quote-block {
129
- position: relative;
130
- padding: 1.5rem 2rem;
131
- margin: 1.5rem 0;
132
- background: var(--color-background-secondary);
133
- border-radius: 0.5rem;
134
- border: 1px solid var(--color-border-subtle);
135
- }
136
- .quote-block::before {
137
- content: "\201C";
138
- position: absolute;
139
- top: 0.5rem;
140
- left: 0.75rem;
141
- font-size: 3rem;
142
- color: var(--color-primary);
143
- opacity: 0.5;
144
- font-family: Georgia, serif;
145
- line-height: 1;
146
- }
147
- .quote-text {
148
- font-style: italic;
149
- color: var(--color-foreground);
150
- font-size: 1.0625rem;
151
- line-height: 1.7;
152
- }
153
- .quote-attribution {
154
- margin-top: 0.75rem;
155
- font-size: 0.875rem;
156
- color: var(--color-muted);
157
- }
158
-
159
- /* Severity badges */
160
- .severity-badge {
161
- display: inline-flex;
162
- align-items: center;
163
- padding: 0.25rem 0.75rem;
164
- border-radius: 9999px;
165
- font-family: var(--font-sans);
166
- font-size: 0.75rem;
167
- font-weight: 600;
168
- text-transform: uppercase;
169
- letter-spacing: 0.05em;
170
- }
171
- .severity-critical {
172
- background: rgba(220, 38, 38, 0.1);
173
- color: var(--color-destructive);
174
- border: 1px solid rgba(220, 38, 38, 0.3);
175
- }
176
- .severity-high {
177
- background: rgba(234, 88, 12, 0.1);
178
- color: #ea580c;
179
- border: 1px solid rgba(234, 88, 12, 0.3);
180
- }
181
- .severity-medium {
182
- background: rgba(217, 119, 6, 0.1);
183
- color: var(--color-warning);
184
- border: 1px solid rgba(217, 119, 6, 0.3);
185
- }
186
- .severity-low {
187
- background: rgba(22, 163, 74, 0.1);
188
- color: var(--color-success);
189
- border: 1px solid rgba(22, 163, 74, 0.3);
190
- }
191
-
192
- /* Finding card */
193
- .finding-card {
194
- background: var(--color-background-secondary);
195
- border: 1px solid var(--color-border);
196
- border-radius: 0.5rem;
197
- padding: 1.5rem;
198
- margin-bottom: 1rem;
199
- }
200
- .finding-header {
201
- display: flex;
202
- justify-content: space-between;
203
- align-items: flex-start;
204
- margin-bottom: 0.75rem;
205
- }
206
- .finding-title {
207
- font-family: var(--font-heading);
208
- font-weight: 600;
209
- color: var(--color-foreground);
210
- font-size: 1.0625rem;
211
- }
212
- .finding-evidence {
213
- font-size: 0.9375rem;
214
- color: var(--color-muted);
215
- margin-top: 0.5rem;
216
- }
217
-
218
- /* Timeline */
219
- .timeline {
220
- position: relative;
221
- padding-left: 2rem;
222
- }
223
- .timeline::before {
224
- content: "";
225
- position: absolute;
226
- left: 0.5rem;
227
- top: 0;
228
- bottom: 0;
229
- width: 2px;
230
- background: linear-gradient(180deg, var(--color-primary) 0%, var(--color-accent) 100%);
231
- }
232
- .timeline-item {
233
- position: relative;
234
- padding-bottom: 1.5rem;
235
- }
236
- .timeline-item::before {
237
- content: "";
238
- position: absolute;
239
- /* Center dot on the vertical line: line is at left:0.5rem (8px) +1px,
240
- timeline-item starts at padding 2rem (32px). Dot half-width 6px → place
241
- left at -1.8125rem so dot center lands at 9px from .timeline left. */
242
- left: -1.8125rem;
243
- top: 0.4rem;
244
- width: 0.75rem;
245
- height: 0.75rem;
246
- border-radius: 50%;
247
- background: var(--color-primary);
248
- }
249
- .timeline-phase {
250
- font-family: var(--font-sans);
251
- font-weight: 600;
252
- color: var(--color-primary);
253
- font-size: 0.875rem;
254
- text-transform: uppercase;
255
- letter-spacing: 0.1em;
256
- }
257
- .timeline-title {
258
- font-family: var(--font-heading);
259
- font-weight: 600;
260
- color: var(--color-foreground);
261
- margin-top: 0.25rem;
262
- }
263
- .timeline-description {
264
- color: var(--color-muted);
265
- font-size: 0.9375rem;
266
- margin-top: 0.25rem;
267
- }
268
-
269
- /* Table of Contents */
270
- .toc {
271
- page-break-after: always;
272
- margin-bottom: 3rem;
273
- }
274
- .toc h2 {
275
- font-family: var(--font-heading);
276
- font-size: 1.75rem;
277
- font-weight: 600;
278
- color: var(--color-foreground);
279
- margin-bottom: 1.5rem;
280
- padding-bottom: 0.5rem;
281
- border-bottom: 2px solid var(--color-primary);
282
- }
283
- .toc ol {
284
- list-style: none;
285
- padding-left: 0;
286
- margin: 0;
287
- }
288
- .toc li {
289
- margin: 0.75rem 0;
290
- border-bottom: 1px dotted var(--color-border-emphasis);
291
- padding-bottom: 0.5rem;
292
- }
293
- .toc a {
294
- display: flex;
295
- gap: 1rem;
296
- align-items: baseline;
297
- color: var(--color-foreground);
298
- text-decoration: none;
299
- }
300
- .toc-number {
301
- font-family: var(--font-sans);
302
- font-weight: 600;
303
- font-size: 0.875rem;
304
- color: var(--color-primary);
305
- width: 2rem;
306
- flex-shrink: 0;
307
- }
308
- .toc-title {
309
- font-family: var(--font-heading);
310
- font-weight: 500;
311
- font-size: 1rem;
312
- }
313
-
314
- /* Stat grid */
315
- .stat-grid {
316
- display: grid;
317
- gap: 1.5rem;
318
- margin: 1.5rem 0;
319
- padding: 1.5rem;
320
- background: var(--color-background-secondary);
321
- border-radius: 0.5rem;
322
- border: 1px solid var(--color-border);
323
- page-break-inside: avoid;
324
- }
325
- .stat {
326
- text-align: left;
327
- }
328
- .stat-value {
329
- font-family: var(--font-sans);
330
- font-size: 2.5rem;
331
- font-weight: 700;
332
- color: var(--color-primary);
333
- letter-spacing: -0.02em;
334
- line-height: 1;
335
- }
336
- .stat-label {
337
- margin-top: 0.5rem;
338
- font-family: var(--font-sans);
339
- font-size: 0.8125rem;
340
- font-weight: 600;
341
- color: var(--color-foreground);
342
- }
343
- .stat-caption {
344
- margin-top: 0.25rem;
345
- font-family: var(--font-body);
346
- font-size: 0.8125rem;
347
- color: var(--color-muted);
348
- }
349
-
350
- /* Comparison table */
67
+ /* Compatibility hooks for raw <table className="comparison-table"> markup
68
+ used directly in pages (not via the <ComparisonTable> component). Projects
69
+ that author tables inline use these. */
351
70
  .comparison-table {
352
71
  width: 100%;
353
72
  border-collapse: collapse;
354
73
  margin: 1.5rem 0;
355
74
  font-family: var(--font-body);
356
75
  font-size: 0.9375rem;
357
- page-break-inside: avoid;
76
+ /* No page-break-inside:avoid here — long tables must be allowed to break.
77
+ Short tables stay together naturally (browsers prefer not to break short
78
+ content). */
358
79
  }
359
80
  .comparison-table th {
360
81
  font-family: var(--font-sans);
@@ -381,53 +102,9 @@ body {
381
102
  border-bottom: none;
382
103
  }
383
104
 
384
- /* Cover page */
385
- .cover-page {
386
- min-height: 100vh;
387
- display: flex;
388
- flex-direction: column;
389
- justify-content: center;
390
- padding: 4rem;
391
- page-break-after: always;
392
- background: linear-gradient(180deg, var(--color-background) 0%, var(--color-background-secondary) 100%);
393
- }
394
- .cover-classification {
395
- font-family: var(--font-sans);
396
- font-size: 0.875rem;
397
- font-weight: 600;
398
- color: var(--color-destructive);
399
- text-transform: uppercase;
400
- letter-spacing: 0.15em;
401
- margin-bottom: 4rem;
402
- }
403
- .cover-title {
404
- font-family: var(--font-heading);
405
- font-size: 3rem;
406
- font-weight: 600;
407
- color: var(--color-foreground);
408
- line-height: 1.2;
409
- margin-bottom: 1rem;
410
- letter-spacing: -0.02em;
411
- }
412
- .cover-subtitle {
413
- font-family: var(--font-heading);
414
- font-size: 1.5rem;
415
- color: var(--color-muted);
416
- margin-bottom: 4rem;
417
- font-weight: 400;
418
- }
419
- .cover-meta {
420
- margin-top: auto;
421
- padding-top: 1.5rem;
422
- border-top: 1px solid var(--color-border);
423
- }
424
- .cover-date {
425
- font-family: var(--font-sans);
426
- font-size: 1rem;
427
- color: var(--color-muted);
428
- }
429
-
430
- /* Print styles — what Playwright sees */
105
+ /* Print rules — Playwright honors these when generating PDF. Atomic blocks
106
+ carry their own break-inside-avoid via Tailwind utilities on the component;
107
+ only page-level rules and cover-after live here. */
431
108
  @media print {
432
109
  body {
433
110
  font-size: 11pt;
@@ -436,23 +113,35 @@ body {
436
113
  max-width: none;
437
114
  padding: 0;
438
115
  }
439
- .report-section,
440
- .callout,
441
- .exhibit,
442
- .finding-card,
443
- .quote-block {
444
- break-inside: avoid;
445
- }
446
- .cover-page {
447
- page-break-after: always;
448
- }
449
116
  a {
450
117
  text-decoration: none;
451
118
  color: var(--color-foreground);
452
119
  }
120
+ /* No orphaned headings — if there isn't room for at least one line of
121
+ content after a heading, push the heading to the next page. */
122
+ h1, h2, h3, h4, h5, h6 {
123
+ break-after: avoid;
124
+ page-break-after: avoid;
125
+ }
126
+ /* Label-as-heading: a paragraph that contains only a <strong> child is
127
+ semantically a sub-heading (e.g., "Scoring guidance:"). Same orphan
128
+ treatment as real headings. */
129
+ p:has(> strong:only-child) {
130
+ break-after: avoid;
131
+ page-break-after: avoid;
132
+ }
133
+ /* Don't break paragraphs across pages.
134
+ Chromium's PDF engine doesn't reliably honor `orphans`/`widows`, so we
135
+ use the stronger `break-inside: avoid`. Most paragraphs are short
136
+ enough to stay whole; very long ones (>1 page) will break anyway
137
+ because the browser has no choice. */
138
+ p {
139
+ break-inside: avoid;
140
+ page-break-inside: avoid;
141
+ }
453
142
  }
454
143
 
455
- /* Page setup for PDF */
144
+ /* Page setup for PDF (A4, narrow margins). */
456
145
  @page {
457
146
  size: A4;
458
147
  margin: 18mm 16mm;
@@ -1,5 +1,5 @@
1
1
  import type { Metadata } from "next";
2
- import { Inter, Source_Serif_4 } from "next/font/google";
2
+ import { Inter } from "next/font/google";
3
3
  import "./globals.css";
4
4
 
5
5
  const inter = Inter({
@@ -8,12 +8,6 @@ const inter = Inter({
8
8
  display: "swap",
9
9
  });
10
10
 
11
- const sourceSerif = Source_Serif_4({
12
- subsets: ["latin"],
13
- variable: "--font-source-serif",
14
- display: "swap",
15
- });
16
-
17
11
  export const metadata: Metadata = {
18
12
  title: "Consulting Report",
19
13
  description: "Strategic assessment and recommendations.",
@@ -23,7 +17,7 @@ export default function RootLayout({
23
17
  children,
24
18
  }: Readonly<{ children: React.ReactNode }>) {
25
19
  return (
26
- <html lang="en" className={`${inter.variable} ${sourceSerif.variable}`}>
20
+ <html lang="en" className={inter.variable}>
27
21
  <body className="bg-background font-body text-foreground antialiased">
28
22
  {children}
29
23
  </body>
@@ -0,0 +1,72 @@
1
+ // Generic band badge. The set of band labels is not pinned by the template —
2
+ // projects pass any string. A default style map covers the strategic-bet
3
+ // "Strong/Promising/Park/Decline" vocabulary out of the box; projects can
4
+ // supply their own via `bandStyles`.
5
+
6
+ import type { CSSProperties } from "react";
7
+
8
+ export type BandStyle = {
9
+ color: string;
10
+ background: string;
11
+ borderColor: string;
12
+ };
13
+
14
+ const defaultBandStyles: Record<string, BandStyle> = {
15
+ Strong: {
16
+ color: "var(--color-success)",
17
+ background: "rgba(22,163,74,0.12)",
18
+ borderColor: "rgba(22,163,74,0.4)",
19
+ },
20
+ Promising: {
21
+ color: "var(--color-primary)",
22
+ background: "rgba(29,78,216,0.10)",
23
+ borderColor: "rgba(29,78,216,0.35)",
24
+ },
25
+ Park: {
26
+ color: "var(--color-muted)",
27
+ background: "rgba(100,116,139,0.12)",
28
+ borderColor: "rgba(100,116,139,0.35)",
29
+ },
30
+ Decline: {
31
+ color: "var(--color-destructive)",
32
+ background: "rgba(220,38,38,0.10)",
33
+ borderColor: "rgba(220,38,38,0.35)",
34
+ },
35
+ };
36
+
37
+ interface BandBadgeProps {
38
+ band: string;
39
+ /**
40
+ * Override or extend the default band styles. Keys are band labels; values
41
+ * are the visual treatment. Falls back to a neutral style when a band
42
+ * label is not found in either the override map or the defaults.
43
+ */
44
+ bandStyles?: Record<string, BandStyle>;
45
+ }
46
+
47
+ const fallbackStyle: BandStyle = {
48
+ color: "var(--color-muted)",
49
+ background: "rgba(100,116,139,0.10)",
50
+ borderColor: "rgba(100,116,139,0.30)",
51
+ };
52
+
53
+ function resolveStyle(band: string, override?: Record<string, BandStyle>): BandStyle {
54
+ return override?.[band] ?? defaultBandStyles[band] ?? fallbackStyle;
55
+ }
56
+
57
+ export function BandBadge({ band, bandStyles }: BandBadgeProps) {
58
+ const s = resolveStyle(band, bandStyles);
59
+ const style: CSSProperties = {
60
+ color: s.color,
61
+ background: s.background,
62
+ borderColor: s.borderColor,
63
+ };
64
+ return (
65
+ <span
66
+ className="inline-flex items-center px-2.5 py-0.5 rounded-full font-sans text-[0.7rem] font-bold uppercase tracking-widest border whitespace-nowrap"
67
+ style={style}
68
+ >
69
+ {band}
70
+ </span>
71
+ );
72
+ }
@@ -5,9 +5,11 @@ interface CalloutProps {
5
5
 
6
6
  export function Callout({ label = "Key Takeaway", children }: CalloutProps) {
7
7
  return (
8
- <div className="callout">
9
- <div className="callout-label">{label}</div>
10
- <div className="callout-content">{children}</div>
8
+ <div className="bg-callout border-l-4 border-primary px-6 py-5 my-6 rounded-r-lg break-inside-avoid">
9
+ <div className="font-sans font-semibold text-primary text-xs uppercase tracking-widest mb-2">
10
+ {label}
11
+ </div>
12
+ <div className="text-foreground">{children}</div>
11
13
  </div>
12
14
  );
13
15
  }
@@ -17,21 +17,27 @@ export function ComparisonTable({
17
17
  rows,
18
18
  metricLabel = "Metric",
19
19
  }: ComparisonTableProps) {
20
+ const thClass =
21
+ "font-sans text-[0.7rem] font-semibold uppercase tracking-widest text-primary px-4 py-3 border-b-2 border-primary text-left";
22
+ const tdClass =
23
+ "px-4 py-3 border-b border-border-subtle align-top last:[&:last-child]:border-b-0";
20
24
  return (
21
- <table className="comparison-table">
25
+ <table className="w-full border-collapse my-6 font-body text-[0.9375rem]">
22
26
  <thead>
23
27
  <tr>
24
- <th>{metricLabel}</th>
25
- <th>{leftLabel}</th>
26
- <th>{rightLabel}</th>
28
+ <th className={thClass}>{metricLabel}</th>
29
+ <th className={thClass}>{leftLabel}</th>
30
+ <th className={thClass}>{rightLabel}</th>
27
31
  </tr>
28
32
  </thead>
29
33
  <tbody>
30
34
  {rows.map((row) => (
31
35
  <tr key={row.metric}>
32
- <td className="metric">{row.metric}</td>
33
- <td>{row.left}</td>
34
- <td>{row.right}</td>
36
+ <td className={`${tdClass} font-sans font-semibold text-foreground`}>
37
+ {row.metric}
38
+ </td>
39
+ <td className={tdClass}>{row.left}</td>
40
+ <td className={tdClass}>{row.right}</td>
35
41
  </tr>
36
42
  ))}
37
43
  </tbody>