Summary
The app is substantially on-brand. The palette, the type system and the component vocabulary all match the marketing site — the two properties genuinely share one visual identity, which is more than most companies at this stage can say.
The problems are the ones a fast-moving codebase always accumulates: a token layer that stops at the edge of the newest feature, a few accessibility gaps that no one would catch by eye, and one colour decision that is wrong by a margin too small to notice in review.
Counted at df8eb7e:
| Measure | Value |
|---|---|
Hard-coded hex literals in src/ | 183 occurrences, 52 distinct values |
text-terra used as a text colour | 62 occurrences — all below AA |
Pages hand-rolling a title instead of PageHeader | 11 |
Pages whose title is an h2 with no h1 on the page | 11 |
<table> elements / <caption> elements | 16 / 0 |
prefers-reduced-motion handling | 0 |
| Dark theme support | none (the marketing site has one) |
Nothing here is urgent in the sense of being broken. Three items are accessibility defects and should be scheduled.
P1 — Accessibility defects
A1 · Terracotta text and buttons fail WCAG AA
62 occurrences of text-terra and 16 of bg-terra.
#C4602A on white is 4.16:1; AA for normal text is 4.5:1. That covers every
link in the product ("Forgot password?", "View all", "Browse all apps"), the
active sidebar row, and every primary button label at 14px.
The fix is not to abandon the brand orange — it is to use the darker terracotta
that sincelabs.com already defines as --accent-ink.
| Use | Now | Should be | Then |
|---|---|---|---|
| Link / text | #C4602A (4.16:1) | #A34A1F | 5.90:1 ✅ |
| Primary button fill | #C4602A (4.16:1) | #A34A1F | 5.90:1 ✅ |
Icons, dots, rings, charts, the // mark | #C4602A | unchanged | non-text, 3:1 ✅ |
Fix: add --color-terraink to the theme, then replace text-terra →
text-terraink for text and bg-terra → bg-terraink on Button variant
primary and Avatar. Leave every non-text use alone.
Reference: tokens/tailwind-theme.css, components/ui/Button.tsx.
A2 · Eleven pages have no h1
Every admin index and detail page renders its title as
<h2 className="font-lora text-[32px] …">, and no h1 exists anywhere on the
page — not in app/admin/layout.tsx either.
A screen-reader user landing on /admin/users is told the page has no heading.
Affected: admin/users, admin/users/[id], admin/organizations,
admin/organizations/[id], admin/products, admin/products/[id],
admin/requests/[id], admin/deletions, admin/audit-log, admin/staff,
admin/dashboard.
Fix: replace each with <PageHeader title=… />. This closes A2 and A3 at
once.
A3 · Sixteen tables, zero captions
No <table> in the app has a <caption>. A screen-reader user arrowing into a
table gets no announcement of what it contains.
Fix: adopt components/ui/DataTable.tsx, which requires a caption prop
and renders it sr-only.
A4 · InfoTooltip removes the focus ring and is keyboard-inaccessible
components/ui/InfoTooltip.tsx:9 is the one place in the app that sets
focus:outline-none without an equivalent replacement — it substitutes a colour
change, which is not a focus indicator.
Separately, the tooltip content is delivered by the native title attribute,
which never appears for keyboard or touch users, and the visible tooltip is
pointer-events-none and hover-driven.
Fix: restore the focus ring; move the text into an aria-describedby target
that appears on :focus-visible as well as :hover.
A5 · No reduced-motion handling
prefers-reduced-motion appears zero times. There are ~100 transitions in
the codebase.
Fix: one block, already written in tokens/since-brand.css.
P2 — Token drift
D1 · 183 hard-coded hex literals across 52 distinct values
The token layer covers the platform shell well. It stops at the Sora surfaces and the operator console built on top of them.
Warm-palette drift — right family, wrong value:
| Literal | Where | Should be |
|---|---|---|
#A95022 ×9 | dashboard charts, Sora admin | #A34A1F (accent-ink) |
#FBFAF8 ×9 | Sora customer surfaces | #F7F5F2 (paper) |
#FFF8F2, #FFF8EE, #FBF7EE, #F0DCC0, #E5DCC8 | scattered | paper / surface-cream |
Off-brand drift — the Tailwind default palette, entirely inside the four
Sora operator files (ClientReview, ClientReviewQueue, ClientIngestPanel,
runs/page):
| Literal | Tailwind name | Should be |
|---|---|---|
#B91C1C ×15, #FEF2F2 ×11, #FCA5A5 | red-700 / red-50 | --sl-error-* |
#FAFAFA ×8, #F4F4F5 ×5, #52525B ×4 | zinc-50 / zinc-100 / zinc-600 | paper / mist / ink-muted |
#EEF2FF ×5, #3730A3 ×5 | indigo-50 / indigo-800 | --sl-info-* |
#F0FDF4 ×4, #15803D ×4 | green-50 / green-700 | --sl-success-* |
These are cool-toned. Next to the warm palette they read as a different product — which, visually, is what the Sora back office currently is.
Fix: these four files are the highest-value cleanup in the repo. One pass converts them and closes most of the count.
Legitimate literals to promote, not remove: #B25C00 / #FFF3E0 (the
warning triple) and #3A4A60 / #EBF0F7 (the info triple) are correct brand
values that were never given token names. They now have them.
D2 · Button is 48px everywhere
Button.tsx hard-codes h-12 with no size prop, so a button in a table row is
as tall as one in a sign-in form. Call sites work around it by passing
className="h-9", which then also has to re-specify the text size.
Fix: the size prop in components/ui/Button.tsx — sm 32 / md 38 /
lg 48.
D3 · Three shadow definitions for one concept
shadow-sm (×16), shadow-[0_1px_2px_rgba(26,25,23,0.04)] (×6),
shadow-[0_2px_8px_rgba(0,0,0,0.04)] (×4). The third is neutral black on a
warm ground, which goes grey.
Fix: shadow-raised / shadow-lifted / shadow-overlay from the theme.
D4 · StatusChip duplicates the status palette
StatusChip.tsx re-declares the six status colours as literals in an if-chain,
independently of the custom properties in globals.css. Two sources for one
palette.
Fix: components/ui/StatusChip.tsx, which reads the tokens and maps status
strings in one place.
P3 — Consistency
C1 · PageHeader adoption
The platform surfaces use PageHeader; the eleven admin pages in A2 do not, and
render their titles at 32px against the component's 28px. Two page-title
sizes in one product.
C2 · Type scale has drifted wide
Twenty-four distinct arbitrary font sizes, including 10.5px, 14.5px, 19px
and one 8px — below the 11px floor. The top five sizes cover 87% of uses; the
tail is noise.
Fix: adopt the nine named steps in typography § 5.3.
The 8px is a defect regardless.
C3 · transition-all in four places
InfoTooltip, admin/products, ClientIngestPanel ×2. Animates properties
nobody intended.
C4 · Email template colours
src/lib/email/templates.ts uses #C4602A for the CTA fill — same AA failure as
A1, and email has no fallback — and #666666 for helper text, which is not a
brand grey.
Fix: #A34A1F and #6B6762.
C5 · Stale marketing copy
AuthLayout.tsx reads "We partner with Nordic SMBs to build and deploy custom
AI agents that drive real operational leverage."
Two problems. It predates the current positioning (the site now leads with the command center and the infrastructure beachhead), and "leverage" as a verb is on the banned-words list in verbal identity § 2.3.
Fix: align with the live site. Suggested:
"We test every AI tool on our own company first. Only the ones that work reach yours."
P4 — Gaps
G1 · No dark theme
The marketing site ships one; the app does not. A customer moving from
sincelabs.com to the portal at night gets a full-brightness page.
Not a defect — a deliberate scope decision worth recording. The dark palette is
specified in colour § 4.5 and shipped in
tokens/since-brand.css, so adopting it is a matter of using the tokens rather
than designing anything.
G2 · No visual regression testing
Playwright is configured but there are no screenshot tests, so drift like D1 is invisible until someone reads the diff.
G3 · public/ is empty
No favicon, no app icons, no OG image. src/app/icon.svg carries the mark alone.
Fix: copy from assets/logo/. Note the app's icon.svg is drawn on an
off-centre grid — 6.5 units of left margin against 10.5 on the right. The
assets in this repo correct that.
Suggested order
| # | Item | Effort | Value |
|---|---|---|---|
| 1 | A1 — terracotta text and buttons | S | Fixes 78 AA failures |
| 2 | A2 + C1 — PageHeader on 11 admin pages | S | Fixes the h1 gap and title sizes together |
| 3 | A5 — reduced motion | XS | One CSS block |
| 4 | A3 — table captions via DataTable | M | 16 tables |
| 5 | A4 — InfoTooltip | XS | |
| 6 | D1 — the four Sora operator files | M | Removes most of the 183 literals |
| 7 | C4 — email colours | XS | |
| 8 | D2, D3, D4 — adopt reference components | M | |
| 9 | C5 — auth copy | XS | Needs Talha's sign-off |
| 10 | G3 — favicons and OG image | S | |
| 11 | C2 — type scale | L | Mechanical but wide |
| 12 | G1 — dark theme | L | Tokens are ready |
Target end state: src/app/globals.css contains only the two imports from
@sincelabs/brand, and src/ contains no hex literals at all.
Method: static analysis of sincelabs/app at df8eb7e; contrast computed with
lib/color.ts against WCAG 2.1. Counts are reproducible with grep over
src/.