The complete specification for how VeriWorkly looks, reads, and behaves — across the marketing site, the studio app, the docs and blog platforms, portfolio templates, and every share card and brand asset we publish.
| If you want to… | Go to |
|---|---|
| Copy a colour value | Colour |
| Set a heading correctly | Typography |
| Use the logo | Logo |
| Build a page layout | Spacing & layout |
| Pick a component | Component library |
| Animate something | Motion |
| Write UI or marketing copy | Voice |
| Change a token | Changing the system |
| Know what is currently inconsistent | Known deviations |
Three files define the system. Everything else consumes them.
| File | Owns |
|---|---|
packages/ui/src/styles/themes.css |
Every colour token, in both themes. What the product actually renders. |
packages/ui/src/styles/globals.css |
Base element styles, .surface-grid, the page gradient, reduced-motion. |
apps/site/config/brand.ts |
The published description of the system — palette, type scale, logo manifest, motion tokens, press facts. |
apps/site/config/brand.ts is what /style-guide, /brand-kit, and the downloadable
.zip all render from. It mirrors themes.css by hand, because CSS custom properties
cannot be imported into a React tree or a Node build script. Change the two together
in the same commit. A token that disagrees between them is a published lie.
This document is the third description of the same palette, so it can drift too. It does not have to be taken on trust:
cd apps/site && npm run check:designcompares all three, token by token, and fails if any of them disagree.
Live, interactive versions of everything below:
https://veriworkly.com/style-guide— the full system, with working componentshttps://veriworkly.com/brand-kit— the outward-facing press kit
Product. VeriWorkly is a free-to-use, open-core, privacy-first AI career workspace: resumes, cover letters, and web portfolios, with no login required to start.
Core metaphor. The Master Profile is one verified record of a career. Every document is a view of it — never re-typed, never out of sync.
Essence line. One profile. Every document.
Design stance. Warm ivory rather than stark white. One blue accent doing all the work. Generous negative space. Type carries the page; ornament does not. Motion confirms an action and then gets out of the way.
Name. VeriWorkly — one word, capital V and W. Never Veriworkly, veriworkly,
Veri Workly, or VeriWorkly.ai.
Licence. MIT. Copyright © 2026 Gautam Raj.
The mark is a faceted W with a period below its centre vertex — a wordmark abbreviation reduced to a single glyph. There is no separate wordmark lockup; when a text label is needed, set "VeriWorkly" in Geist Sans Semibold beside the mark.
Authored on a 512 × 512 canvas, exactly symmetric about x = 256.
| Property | Value |
|---|---|
| Canvas | viewBox="0 0 512 512" |
| Mark bounds | x 66 → 446, y 117 → 395 |
| Occupies | 74% of canvas width, 54% of height |
| Silhouette | One closed path, 13 vertices |
| Period | Circle at (256, 381.8), r = 12.3 |
| Construction | Facet polygons are clipped to the silhouette, so the outline stays crisp independent of facet edits |
The silhouette path, for reference:
M66 117H156l57.2 137.6L236.1 200h39.8l22.9 54.6L356 117h90L322 395h-35.6L256 324l-30.4 71H190Z
Five planes, lightest to darkest. These are the mark's own colours and are not theme tokens — the logo does not recolour with the theme.
| Plane | Value |
|---|---|
| Highlight | #58C2F0 |
| Centre peak | #2DAFEB |
| Base | #0299DA |
| Outer descending | #0D6EB5 |
| Core / period | #055390 / #065898 |
| File | Use |
|---|---|
veriworkly-logo-256.png |
Primary mark. For websites, headers, and media placements. |
veriworkly-icon-512.png |
Store listings, app launchers, and high-DPI displays. |
veriworkly-icon-192.png |
Android home screen, manifest, and favicons. |
veriworkly-icon-apple-touch.png |
iOS home screen and web clip. |
All live in apps/site/public/brand/logo/. The manifest is logoAssets in
apps/site/config/brand.ts; the .zip is built from that list, so adding a file there
and re-running the build is the whole process.
| Rule | Value |
|---|---|
| Clear space | 25% of the mark's width, on every side |
| Minimum display size | 16 px |
Nothing enters the clear-space box — not type, rules, other logos, or the edge of a photograph.
Never recolour the mark, distort its proportions, rotate it, add shadows, glows, gradients, or outlines, or place it on a ground that breaks its contrast.
Every token carries two values. The light column applies on pale grounds, the dark column
under .dark. Values below are authoritative and match
packages/ui/src/styles/themes.css.
| Token | Light | Dark | Role |
|---|---|---|---|
--background |
#F5F4EF |
#0D1117 |
Page ground. Warm ivory, not white. |
--foreground |
#171717 |
#F3F4F6 |
Primary text. |
--card |
#FFFFFF |
#121924 |
Raised surfaces, one step off the page. |
--muted |
#5F5C54 |
#94A3B8 |
Secondary text, metadata, captions. |
--border |
rgba(23, 23, 23, 0.12) |
rgba(148, 163, 184, 0.25) |
Dividers and outlines. Always alpha, never a solid grey. |
--accent |
#2563EB |
#60A5FA |
Links, CTAs, focus rings. Lifted in dark to hold contrast. |
--accent-foreground |
#F8FBFF |
#0F172A |
Text and icons on an accent fill. |
Semantic status is deliberately separate from --accent: accent means "this is
VeriWorkly", status means "this is the state of your data". There are four states and four
tokens. Informational states reuse --accent rather than introducing a fifth hue.
| Token | Light | Dark | Role |
|---|---|---|---|
--destructive |
#DC2626 |
#EF4444 |
Errors, destructive actions, failed states. |
--destructive-foreground |
#FFFFFF |
#FFFFFF |
Text on a destructive fill. |
--success |
#047857 |
#34D399 |
Completed, valid, passing. |
--success-foreground |
#FFFFFF |
#052E1F |
Text on a success fill. |
--warning |
#B45309 |
#FBBF24 |
Needs attention but has not failed. |
--warning-foreground |
#FFFFFF |
#271A02 |
Text on a warning fill. |
| Token | Light | Dark | Role |
|---|---|---|---|
--muted-foreground |
#171717 |
#171717 |
Text on a muted fill. Identical in both themes by design. |
--fd-accent |
rgba(96, 165, 250, 0.2) |
rgba(130, 139, 2, 0.8) |
Fumadocs search/menu highlight. Docs platform only. |
Tokens become utilities through an @theme inline block in each app's globals.css:
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-muted: var(--muted);
--color-border: var(--border);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
}Mapped in apps/site, apps/studio, apps/blog-platform, and apps/docs-platform.
apps/portfolio runs a parallel system — see Portfolio.
A token that is not mapped produces no utility. text-muted-foreground currently
resolves to nothing anywhere, because --color-muted-foreground is absent from every
@theme block. If you add a token to themes.css and intend to use it as a class, map it
in all four apps.
- Tinted fills. A status or accent surface is a
10%fill with a25%border of the same token:bg-success/10 border-success/25 text-success. That combination reads correctly on both--backgroundand--cardwithout a second variant. - Alpha borders.
--borderis always an alpha value so it composites over whatever sits behind it. Never substitute a solid grey. - The dark theme is not an inversion.
--accentlightens from#2563EBto#60A5FAspecifically so it keeps contrast on near-black. Do not assume a token can be derived from its counterpart. - No raw palette colours in product code.
emerald-500,blue-600,zinc-200and friends do not follow the theme and appear in no palette, which makes them invisible to this document.
Measured WCAG 2.1 ratios for the pairs that matter. Normal text needs 4.5:1; large text and UI boundaries need 3:1.
| Pair | Light | Dark |
|---|---|---|
| Foreground on Background | 16.28 — AAA | 17.20 — AAA |
| Foreground on Card | 17.93 — AAA | 16.03 — AAA |
| Muted on Background | 6.06 — AA | 7.38 — AAA |
| Muted on Card | 6.68 — AA | 6.88 — AA |
| Accent on Background | 4.69 — AA | 7.44 — AAA |
| Accent-foreground on Accent | 4.98 — AA | 7.02 — AAA |
| Success on Background | 4.98 — AA | 9.84 — AAA |
| Warning on Background | 4.56 — AA | 11.34 — AAA |
| Destructive on Background | 4.39 — fails AA for normal text | 5.03 — AA |
--destructive at #DC2626 on #F5F4EF is 4.39:1 — under the 4.5:1 threshold. It is
safe for icons, borders, and text at 18px+ (or 14px+ bold). For error text at body size on
a light ground, darken to roughly #B91C1C (6.0:1), or pair the colour with an icon and
explicit wording so colour is not the only signal.
| Role | Family | Variable | Loaded by |
|---|---|---|---|
| Interface | Geist Sans | --font-geist-sans |
packages/ui/src/lib/fonts.ts via next/font/google |
| Mono | Geist Mono | --font-geist-mono |
same |
Consumed by apps/site, apps/studio, apps/blog-platform, and apps/docs-platform —
each spreads globalFontVariables onto its root element. apps/portfolio uses Outfit
instead; see Portfolio.
Both Geist faces are released by Vercel under the SIL Open Font License 1.1 and are
available from Google Fonts. next/font self-hosts them at build time, so there is no
runtime request to Google and nothing to license to typeset the VeriWorkly name.
Geist Sans carries interface and body copy. Geist Mono carries labels, metadata, file paths, dimensions, token names, and code — anything the reader should recognise as a machine value rather than prose.
Seven steps. Everything on a VeriWorkly surface should be one of them.
| Step | Tailwind | Rendered | Weight | Tracking | Line height | Use |
|---|---|---|---|---|---|---|
| Display | text-4xl sm:text-5xl md:text-6xl font-semibold tracking-tight |
36 → 48 → 60 px | 600 | −0.025em | 1.0 | Page-opening headline. One per page. |
| Section | text-3xl font-semibold tracking-tight |
30 px | 600 | −0.025em | 36 px | Section headings. |
| Component | text-xl font-semibold tracking-tight |
20 px | 600 | −0.025em | 28 px | Card and panel titles. |
| Body Large | text-base leading-8 md:text-lg |
16 → 18 px | 400 | 0 | 32 px | Lead paragraphs, marketing copy. |
| Body | text-sm leading-relaxed |
14 px | 400 | 0 | 1.625 | Descriptions, list items, dense UI. |
| Eyebrow | text-xs font-semibold tracking-[0.24em] uppercase |
12 px | 600 | 0.24em | 16 px | Label above a heading. Always uppercase. |
| Mono Meta | font-mono text-xs |
12 px | 400 | 0 | 16 px | Token names, paths, dimensions, code. |
The canonical copy of this table is typeScale in apps/site/config/brand.ts.
- Weights. 400 for prose, 600 for every heading and label. 500 appears on some interactive text; 700 and above are reserved for the share-card title, which is optically compensating for downscaling in social feeds.
- Tracking. Headings tighten to
−0.025em. Uppercase eyebrows open to0.24em— uppercase without added tracking reads as shouting. - Measure. Keep running prose near 65 characters.
max-w-2xlon lead paragraphs,max-w-3xlon explanatory blocks. - Balance. Multi-line headings get
text-wrap: balance. - Numerals. Any column of digits gets
tabular-nums.
<Container>…</Container> // packages/ui/src/components/layout/Container.tsxRenders mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8 — 1280 px maximum, with 16 px of
gutter on mobile, 24 px from sm, 32 px from lg. Accepts as to change the element. Do
not hand-roll a container; reach for this one so gutters stay identical across every page
and app.
Tailwind v4 defaults, unmodified.
| Prefix | Min width |
|---|---|
sm |
640 px (40rem) |
md |
768 px (48rem) |
lg |
1024 px (64rem) |
xl |
1280 px (80rem) |
2xl |
1536 px (96rem) |
- Marketing page shell:
pt-28 pb-20 lg:pt-36on the outer wrapper. The top padding clears the fixed navbar. - Between sections:
space-y-16 md:space-y-24. - Inside a section:
space-y-8, withspace-y-4tospace-y-6inside a card. - Anchored sections:
scroll-mt-24, so a deep link does not land under the navbar. - Sibling groups are spaced with flex or grid
gap, not per-element margins — margins collapse and double in ways that are hard to see and harder to fix.
| Token | Value | Applied to |
|---|---|---|
rounded-md |
6 px | Checkbox |
rounded-lg |
8 px | Small inputs |
rounded-xl |
12 px | Medium inputs, menus, small tiles |
rounded-2xl |
16 px | Large inputs, select, textarea, accordion, tooltip, nested panels |
rounded-3xl |
24 px | Card, modal |
rounded-4xl |
32 px | Major landing sections |
rounded-full |
pill | Buttons, badges, switch, chips |
Radius tracks size: the larger the surface, the larger the corner. Controls that accept text are pills or 12–16 px; containers that hold other things are 24–32 px.
The signature background. 28 px, drawn from --border at 60% so it follows the theme.
.surface-grid {
background-image:
linear-gradient(
to right,
color-mix(in srgb, var(--border) 60%, transparent) 1px,
transparent 1px
),
linear-gradient(
to bottom,
color-mix(in srgb, var(--border) 60%, transparent) 1px,
transparent 1px
);
background-position: center;
background-size: 28px 28px;
}28 px is the system's grid unit. Share cards use it too — if you draw a grid anywhere, it is 28 px.
Applied globally to body in packages/ui/src/styles/globals.css. Two accent radials in
the top corners over the page ground.
background:
radial-gradient(circle at top left, rgba(37, 99, 235, 0.12), transparent 28%),
radial-gradient(circle at top right, rgba(96, 165, 250, 0.08), transparent 22%), var(--background);When reproducing it in a component, compose from tokens rather than baking the hexes, so the swatch follows the theme:
background:
radial-gradient(
circle at top left,
color-mix(in srgb, var(--accent) 12%, transparent),
transparent 28%
),
radial-gradient(
circle at top right,
color-mix(in srgb, var(--accent) 8%, transparent),
transparent 22%
),
var(--background);A 145° linear gradient gives card surfaces a soft, paper-like fall-off.
background: linear-gradient(
145deg,
var(--card),
color-mix(in oklab, var(--card) 88%, var(--foreground))
);| Name | Value | Use |
|---|---|---|
| Card default | shadow-sm |
Every Card |
| Premium / float | shadow-[0_30px_90px_-50px_rgba(0,0,0,0.45)] |
Landing sections, floating panels |
| Accordion | shadow-[0_12px_35px_-28px_rgba(15,23,42,0.65)] |
Accordion shell |
| Menu | shadow-xl ring-1 ring-black/5 |
Dropdowns |
| Modal | shadow-2xl |
Dialog |
The premium shadow is deliberately large-blur and heavily negative-spread: it reads as depth rather than as a drop shadow.
::selection {
background: color-mix(in srgb, var(--accent) 28%, transparent);
}Motion confirms an action; it never announces itself. Anything longer than 250 ms on an interaction reads as latency.
| Token | Duration | Easing | Use |
|---|---|---|---|
| Micro transition | 200 ms | ease (default) |
Hover and focus colour changes |
| Enter | 200 ms | cubic-bezier(0.16, 1, 0.3, 1) |
Popovers, dropdowns, dialogs (animate-scale-in) |
| Shimmer | 2.2 s loop | ease-in-out |
Skeleton placeholders (animate-shimmer) |
| Pulse ring | 2.4 s loop | cubic-bezier(0.4, 0, 0.6, 1) |
Live / recording indicators (animate-pulse-ring) |
Registered in each app's @theme block. Canonical list: motionTokens in
apps/site/config/brand.ts.
Component-level transitions run at duration-200, occasionally duration-300 for
accordion height. Transform-based hover affordances signal direction of travel — links
move right (translate-x-0.5), downloads move down (translate-y-0.5), external links
move up-right.
A global rule in packages/ui/src/styles/globals.css collapses every animation and
transition to 0.01ms and disables smooth scrolling:
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}You do not need to handle this per component. But never encode meaning in motion alone — for some readers there will be none.
Every interactive element needs a visible keyboard focus state, on :focus-visible only
so a mouse click stays quiet.
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2
Button uses focus-visible:ring-accent/40; form controls use ring-accent/10 to
ring-accent/20 paired with focus-visible:border-accent/40, because a filled control
already carries a border to strengthen.
Checklist for anything interactive:
- Reachable by keyboard, in a sensible tab order.
- Visible focus ring with an offset — never
outline: nonealone. - Decorative icons get
aria-hidden="true"; meaningful ones get a label. - Icon-only controls get
aria-label. - Colour is never the only carrier of state — pair it with an icon, a label, or wording.
- Touch targets are at least 44 px tall (
size="md"is 44 px,size="lg"is 48 px). - Wide content (tables, code, diagrams) scrolls inside its own
overflow-x: autocontainer so the page body never scrolls sideways.
Exported from @veriworkly/ui (packages/ui/src/index.ts). Reach for these before
writing a new one.
| Component | Notes |
|---|---|
AppShell |
flex min-h-screen flex-col with navbar / footer slots and a flex-1 main. |
Container |
1280 px max, responsive gutters. See Container. |
MarketingNavbar |
Fixed marketing nav; desktop nav, mobile sheet, actions, and logo as sub-parts. |
MarketingFooter |
Shared marketing footer. |
ThemeToggle |
Light / dark switch. Toggles .dark on the root element. |
SocialIcons |
Brand social icons, sourced from apps/site/public/icons/socials/. |
Button — rounded-full, font-medium, transition, focus ring accent/40,
disabled:opacity-50.
| Variant | Style |
|---|---|
primary |
bg-accent text-accent-foreground shadow-sm hover:opacity-90 |
secondary |
bg-card text-foreground ring-1 ring-inset ring-border hover:bg-background |
ghost |
bg-transparent text-foreground hover:bg-card |
| Size | Height | Padding | Text |
|---|---|---|---|
sm |
36 px | 12 px | 14 px |
md |
44 px | 16 px | 14 px |
lg |
48 px | 20 px | 16 px |
Supports asChild (clones the child — e.g. a Link — with the button classes), loading,
and loadingText. One primary button per view.
Input — variants outline (default), filled, ghost; sizes sm (36 px,
rounded-lg), md (44 px, rounded-xl), lg (48 px, rounded-2xl). Takes error to
swap the border and ring to --destructive. Note the prop is inputSize, not size,
because size collides with the native attribute.
TextArea — min-h-30 (120 px), rounded-2xl, non-resizable, same error treatment.
Select — 44 px, rounded-2xl, focus:ring-accent/20.
Checkbox — 20 px, rounded-md, border-2; checked and indeterminate both fill with
--accent. Tick animates in over 200 ms.
Switch — 24 × 44 px track, 20 px knob, bg-accent on / bg-muted/80 off, 200 ms,
focus ring on focus-within.
Badge — rounded-full border px-3 py-1 text-xs font-medium, neutral by default. For
status, add the tinted-fill triplet: bg-success/10 border-success/25 text-success.
Card — bg-card rounded-3xl p-5 shadow-sm overflow-hidden. The default padding is
routinely overridden (p-6, p-8), and p-0 is used when children own their own padding
— a divided list, for instance.
Modal — bg-black/50 backdrop-blur-sm overlay; a bottom sheet on mobile
(items-end, rounded-t-3xl, 6 × 48 px drag handle) that becomes a centred
max-w-lg rounded-3xl dialog from md.
Menu — absolutely positioned, z-30, rounded-xl / rounded-2xl by size,
shadow-xl ring-1 ring-black/5; items are rounded-xl with hover:bg-accent/10;
separators are bg-border h-px.
Accordion — rounded-2xl border-border/70 bg-card/90; animates grid-template-rows
over 300 ms; the chevron rotates 180°.
Tooltip — rounded-2xl, max-w-[280px], backdrop-blur-2xl. Deliberately
inverted against the page: near-black in light mode, near-white in dark. A tooltip is
a temporary overlay, so it should read as separate from the surface beneath it.
Lucide React (lucide-react ^1.27), 1.5 px stroke, sized in 4 px steps from 16 to
24 px (size-4, size-5, size-6). Icons inherit currentColor.
Generated on request by apps/site/app/api/og/route.tsx (edge runtime, Satori) rather
than designed one at a time, so no page ships without one.
| Property | Value |
|---|---|
| Canvas | 1200 × 630 (1.91:1) |
| Composition | Centred: pill badge, title, description, with the domain locked to the bottom |
| Ground | --background, two accent radials in the top corners, 28 px grid |
| Badge | Inline mono mark at 34 px + "VeriWorkly" in mono 700, on a token-tinted pill |
| Title | 84 px / weight 900 / −0.05em, dropping to 60 px past 40 characters, filled with a Foreground → Muted gradient |
| Description | 32 px / weight 500 / --muted, max-width 850 px |
| Footer | veriworkly.com, 18 px, 0.2em tracking, uppercase, 50% opacity |
| Cache | public, immutable, max-age=31536000 |
| Parameter | Behaviour |
|---|---|
title |
Up to 120 characters. Defaults to "VeriWorkly". |
description |
Up to 250 characters. |
theme |
light (default) or dark. |
showDesc |
false for a title-only card. |
Two constraints worth knowing before editing this route:
- Satori has no CSS custom properties. The theme values are repeated as literals in a
tobject at the top of the route. They must stay equal tothemes.css. - No network dependencies. The mark is drawn as an inline
<path>, not fetched as a PNG. A remote image that fails to resolve does not fail the render — it silently produces a card with no logo. The faceted mark needs aclipPathand seven gradients that Satori does not support reliably, which is why the badge uses the single-colour mark.
Text is stripped of control, zero-width, and bidi-override codepoints and length-capped, so the endpoint cannot be used to mint a convincing card that says something we did not write.
One system, five surfaces. None of them introduces a separate brand.
| Surface | App | Font | Tokens |
|---|---|---|---|
| Marketing | apps/site |
Geist | Shared |
| Studio (app) | apps/studio |
Geist | Shared |
| Docs | apps/docs-platform |
Geist | Shared + --fd-* (Fumadocs) |
| Blog | apps/blog-platform |
Geist | Shared |
| Portfolio | apps/portfolio |
Outfit | Parallel — see below |
The portfolio product is a system-managed app surface that extends the platform without introducing a separate brand.
- Marketing pages — asymmetric editorial product tour with a structured template gallery.
- App pages — workbench layout: grouped editing cards, contextual help, publish readiness, and a persistent private preview.
- Public templates — may use distinct local palettes and typography, so portfolio
owners get a real creative choice. Template palettes must be declared as named CSS
tokens in each template stylesheet (
apps/portfolio/template-library/*/). - Motion — CSS-first reveal, hover lift, and state transitions. Spatial motion
collapses under
prefers-reduced-motion. - Editor stance — section-aware inputs, plain-language guidance, compatible snapshot parsing, visible focus, and no destructive action without a label.
Portfolio declares its own token layer in apps/portfolio/app/tokens.css under a different
vocabulary — paper, panel, ink, line instead of background, card, foreground,
border — plus its own spacing scale (--space-xs … --space-4xl, 4 → 112 px), radius
scale (--radius-xs … --radius-xl, 6 → 20 px), easings, and durations (--dur-fast
160 ms, --dur-medium 320 ms, --dur-slow 700 ms).
The underlying colour values are the platform palette:
| Portfolio | Platform equivalent |
|---|---|
--color-paper |
--background (#F5F4EF / #0D1117) |
--color-panel |
--card (#FFFFFF / #121924) |
--color-ink |
--foreground (#171717 / #F3F4F6) |
--color-muted |
--muted (#5F5C54 / #94A3B8) |
--color-line |
--border (12% / 24% alpha) |
--color-accent |
--accent (#2563EB / #60A5FA) |
--color-accent-ink |
--accent-foreground (#F8FBFF / #0F172A) |
Portfolio's --color-success / --color-warning / --color-danger are authored in oklch
and are not the platform's --success / --warning / --destructive. Treat them as
template-layer colours, not platform status.
How we write about VeriWorkly, in product and in public.
Direct, not hypey. Lead with what the product does, not adjectives. "No login required to start" beats "revolutionary, game-changing career platform."
Privacy-forward. Be specific about what stays local, what syncs, and why. Vague privacy claims read as marketing; specifics read as true.
Built by the same people who use it. Written by people who build resumes, not a brand team. Plain language over jargon; explain ATS, JSON Resume, and similar on first use.
Confident about being free and open. "Free-to-use and open-core" is a real differentiator, not an apology. State it plainly rather than hedging.
- Write from the reader's side of the screen. A person manages notifications, not webhook config.
- Active voice. A control says exactly what happens — "Publish", then a toast that says "Published".
- Errors explain what went wrong and how to fix it. No apologies, no vagueness.
- Specific beats clever.
| Term | Meaning |
|---|---|
| VeriWorkly | The company and product. One word, capital V and W. |
| VeriWorkly Resume | Only when disambiguating from other VeriWorkly products (e.g. the repo name). |
| Master Profile | The canonical career-facts record that seeds resumes, cover letters, and portfolios. Capitalised as a product term. |
- One-liner — Free AI resumes, cover letters & web portfolios. No login required.
- Short — VeriWorkly is a free, open-core, privacy-first AI career workspace — a resume, cover letter, and portfolio builder that requires no login to start and keeps data local-first with optional cloud sync.
- Long — see
siteConfig.descriptioninapps/site/config/site.ts.
Press and brand enquiries: info@veriworkly.com.
- Edit
packages/ui/src/styles/themes.css— both:rootand.dark. - Mirror the change in
brandColorsinapps/site/config/brand.ts, with a description and acoreflag (trueputs it on/brand-kit; every token appears on/style-guide). - If it needs a Tailwind utility, add
--color-<name>: var(--<name>)to the@theme inlineblock inapps/site,apps/studio,apps/blog-platform, andapps/docs-platform. Skipping this leaves a dead class. - Check contrast against
--backgroundand--cardin both themes. - Run
npm run build:brand-kitfromapps/siteto regenerate the download.
- Drop the file in
apps/site/public/brand/logo/. - Add it to
logoAssetsinapps/site/config/brand.tswith its size, format, and a one-line usage note. - Run
npm run build:brand-kit. The script fails loudly if a referenced file is missing, so the manifest and the folder cannot drift.
cd apps/site && npm run build:brand-kitWrites public/brand/veriworkly-brand-kit.zip — logos, palette.json, and a generated
README.txt — from config/brand.ts. Output is byte-identical across runs (timestamps are
pinned), so a rebuild with no content change produces no diff. The ZIP writer is
dependency-free and lives in apps/site/scripts/build-brand-kit.mts.
Never hand-edit anything inside the archive. The README says so too.
cd apps/site && npm run check:designCompares all three descriptions of the palette — themes.css, brand.ts, and the tables
in this document — token by token, and reports any token that is defined but never mapped
into a Tailwind @theme block. Exits non-zero on drift, so it belongs in CI.
Two tokens are expected in the "not mapped" list: --fd-accent, which Fumadocs reads as a
raw CSS variable rather than a utility, and --muted-foreground, which is a real gap — see
Known deviations.
Before merging anything visual:
- Renders correctly in both themes — not just the one you were working in.
- No raw Tailwind palette colours (
emerald-500,blue-600,zinc-200). - Type is one of the seven scale steps.
- Interactive elements have a visible
:focus-visiblering. - Spacing comes from layout
gap, not ad-hoc margins. - Wide content scrolls in its own container.
- Any new token is mapped in all four
@themeblocks. -
npm run check:designpasses.
An honest list of places the codebase does not yet match this document. Each is real and verified; none is a reason to copy the pattern.
Card border is not themed. packages/ui/src/components/ui/Card.tsx uses
border-zinc-200/50 where border-border exists. Because zinc-200 does not respond to
the theme, every card in dark mode carries a light-grey outline instead of the
rgba(148, 163, 184, 0.25) the system specifies. It is a one-line fix, but it changes the
look of every card surface in every app, so it wants a deliberate visual review.
Two dead token references.
--border-hoveris used byInputandTextArea(hover:border-border-hover) but is defined in no theme or@themeblock. The class generates nothing, so those controls have no hover border state.--muted-foregroundexists inthemes.cssbut is never mapped to--color-muted-foreground, sotext-muted-foregroundinCheckboxandplaceholder:text-muted-foreground/40inInputandTextAreaare inert. Input placeholders fall back to the browser default rather than a brand colour.
Raw blue utilities in the shared chrome. Roughly 406 occurrences of text-blue-600,
dark:text-blue-400, border-blue-500/*, and bg-blue-500/* across 30+ files in
apps/site hand-roll the accent token at approximately its own values. Converting them to
text-accent is mechanical but touches a lot of surface, so it wants to be its own
reviewed change.
Raw zinc in two components. Input's filled variant uses bg-zinc-500/10, and
Tooltip uses zinc-950/90 and white/95. The tooltip inversion is intentional; the
input fill is not, and should become an alpha of --foreground or --muted.
--destructive fails AA on light. 4.39:1 against --background. See
Contrast for the workaround until the token is darkened.
Portfolio duplicates the scale. apps/portfolio/app/tokens.css restates the palette
under different names and adds parallel spacing, radius, easing, and duration scales. This
is defensible — templates need their own vocabulary — but it means a platform token change
has to be applied in two places, and portfolio's oklch status colours do not match the
platform's.