# VeriWorkly Design System 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. ## How to use this document | If you want to… | Go to | | :---------------------------------- | :------------------------------------------ | | Copy a colour value | [Colour](#colour) | | Set a heading correctly | [Typography](#typography) | | Use the logo | [Logo](#logo) | | Build a page layout | [Spacing & layout](#spacing--layout) | | Pick a component | [Component library](#component-library) | | Animate something | [Motion](#motion) | | Write UI or marketing copy | [Voice](#voice) | | Change a token | [Changing the system](#changing-the-system) | | Know what is currently inconsistent | [Known deviations](#known-deviations) | ### Sources of truth 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: ```bash cd apps/site && npm run check:design ``` compares 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 components - `https://veriworkly.com/brand-kit` — the outward-facing press kit --- ## Brand foundation **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. --- ## Logo 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. ### Geometry 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 ``` ### Facet palette 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` | ### Variants | 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. ### Usage rules | 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. --- ## Colour 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`. ### Core palette | 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. | ### Status palette 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` | `#B91C1C` | `#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. | ### Internal tokens | 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. | ### Tailwind mapping Tokens become utilities through an `@theme inline` block in each app's `globals.css`: ```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](#portfolio). **A token that is not mapped produces no utility.** `apps/studio` and `apps/blog-platform` map `--color-muted-foreground` to `--muted`; `apps/site` and `apps/docs-platform` do not, so `text-muted-foreground` is inert there. Shared components therefore use `text-muted`, never `text-muted-foreground`. If you add a token to `themes.css` and intend to use it as a class, map it in all four apps. **`color-scheme`** is declared in `themes.css` (`light` on `:root`, `dark` under `.dark`), so native scrollbars, ``, 44 px, `rounded-2xl`, same focus and error treatment. **`Checkbox`** — 20 px, `rounded-md`, `border-2`, 40 px pointer target; checked and indeterminate both fill with `--accent`. Controlled (`checked`) or uncontrolled (`defaultChecked`). Tick and dash cross-fade over 150 ms. **`Switch`** — `role="switch"`, 24 × 44 px track, 20 px knob, `bg-accent` on / `bg-muted/80` off, 200 ms. Focus ring on keyboard focus only. Has no visible text: name it with `aria-label` or `aria-labelledby`. **`Badge`** — `rounded-full border px-3 py-1 text-xs font-medium`. `variant` is `neutral` (default), `accent`, `success`, `warning`, or `destructive`, each the tinted-fill triplet. ### Surfaces & overlays **`Card`** — `bg-card border-border/50 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. Pass `as="article"` for a self-contained piece of content. **`Modal`** — portaled to `body` at `z-200`, above the navbar and below toasts. `bg-black/50 backdrop-blur-sm` overlay; a bottom sheet on mobile (`items-end`, `rounded-t-3xl`, swipe-down handle) that becomes a centred `max-w-lg rounded-3xl` dialog from `md`. `Modal.Title` and `Modal.Description` label the dialog automatically. Escape and the focus trap act on the topmost modal only. **`Menu`** — absolutely positioned, `z-30`, `rounded-xl` / `rounded-2xl` by size, `shadow-xl ring-1 ring-black/5`, scales in from its trigger's corner over 150 ms; items are `rounded-xl` with `hover:bg-accent/10`; separators are `bg-border h-px`. Follows the APG menu-button pattern (arrow keys, Home/End, typeahead, Escape returns focus). **`Accordion`** — `rounded-2xl border-border/70 bg-card/90`; animates `grid-template-rows` over 300 ms; the chevron rotates 180°. Collapsed panels are `hidden="until-found"`, so find-in-page reaches every answer and opens it. **`Tooltip`** — `rounded-2xl`, `max-w-70` (280 px), `bg-foreground text-background`: deliberately **inverted** against the page, near-black in light mode and near-white in dark, so it reads as separate from the surface beneath it. Rendered in the top layer with the Popover API; linked by `aria-describedby`; hoverable and dismissible with Escape (WCAG 1.4.13). ### Icons **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`. --- ## Share cards (Open Graph) 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: 1. **Satori has no CSS custom properties.** The theme values are repeated as literals in a `t` object at the top of the route. They must stay equal to `themes.css`. 2. **No network dependencies.** The mark is drawn as an inline ``, 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 a `clipPath` and 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. --- ## Surfaces & sub-brands 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** | ### Portfolio 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. --- ## Voice 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. ### UI copy - 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. ### Product terms | 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. | ### Boilerplate - **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.description` in `apps/site/config/site.ts`. Press and brand enquiries: `info@veriworkly.com`. --- ## Changing the system ### Adding or changing a colour token 1. Edit `packages/ui/src/styles/themes.css` — **both** `:root` and `.dark`. 2. Mirror the change in `brandColors` in `apps/site/config/brand.ts`, with a description and a `core` flag (`true` puts it on `/brand-kit`; every token appears on `/style-guide`). 3. If it needs a Tailwind utility, add `--color-: var(--)` to the `@theme inline` block in `apps/site`, `apps/studio`, `apps/blog-platform`, and `apps/docs-platform`. Skipping this leaves a dead class. 4. Check contrast against `--background` and `--card` in both themes. 5. Run `npm run build:brand-kit` from `apps/site` to regenerate the download. ### Adding a logo asset 1. Drop the file in `apps/site/public/brand/logo/`. 2. Add it to `logoAssets` in `apps/site/config/brand.ts` with its size, format, and a one-line usage note. 3. Run `npm run build:brand-kit`. The script fails loudly if a referenced file is missing, so the manifest and the folder cannot drift. ### Rebuilding the brand kit ```bash cd apps/site && npm run build:brand-kit ``` Writes `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. ### Verifying the system agrees with itself ```bash cd apps/site && npm run check:design ``` Compares 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](#known-deviations). ### Review checklist 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-visible` ring. - [ ] Spacing comes from layout `gap`, not ad-hoc margins. - [ ] Wide content scrolls in its own container. - [ ] Any new token is mapped in all four `@theme` blocks. - [ ] `npm run check:design` passes. --- ## Known deviations 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. **`--muted-foreground` is a trap.** It is `#171717` in both themes (text on a muted fill), so as a text colour on the dark ground it is unreadable. `apps/studio` and `apps/blog-platform` sidestep it by mapping the utility to `--muted` instead. Shared components no longer use it. **The wordmark is set in Geist Mono.** [Logo](#logo) says "Geist Sans Semibold beside the mark", but every live surface (the site's `LogoPill`, `MarketingNavLogo`, `MarketingFooter`) sets it in `font-mono`. Either the rule or the surfaces should change; that is a brand decision, not a code fix. **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. **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.