Design System
Vantage Circle v2 — 26 tokens · 8 font sizes · 4 weights · 2 families · 2 line heights · 2 letter-spacings · 8 spacing steps · 15 z-index layers · 5 border widths · 6 opacity steps · 5 icon + 5 avatar sizes · 57 active + 5,108 available icons (HugeIcons)
Colors
7 semantic palettes + 1 runtime brand token. Primary → slate, Secondary → violet, Info → skyblue, Success → green, Warning → orange, Error → red, Neutral → gray.
Shadows
6 levels — all use rgba(16, 24, 40, …) as base. Token:
--shadow-{xs|sm|md|lg|xl|2xl}.
rgba(…, 0.05)
+ 1px 2px
+ 2px 4px
+ 4px 6px
+ 8px 8px
rgba(…, 0.18)
Frosted Glass
Translucent blurred surface that content sits on — cards, panels and composers floating over busy content (used throughout the AskVee modal). Apply the .frosted-glass class, or copy the CSS below. Tokens:
--frosted-glass-fill, --frosted-glass-filter,
--frosted-glass-border, --frosted-glass-shadow.
.frosted-glass {
background: rgba(255, 255, 255, 0.4);
-webkit-backdrop-filter: saturate(1.3) blur(16px);
backdrop-filter: saturate(1.3) blur(16px);
border: 1px solid rgba(255, 255, 255, 0.75);
box-shadow: 0 8px 32px rgba(16, 24, 40, .10),
0 2px 8px rgba(16, 24, 40, .06);
}
Blurry Background
The AskVee modal backdrop — a soft coral / magenta / violet aurora glowing from the top, fading into a near-white ground. Apply the .blurry-background class, or copy the CSS below. Token: --blurry-background.
.blurry-background {
background:
radial-gradient(440px 320px at 6% -12%, rgba(255,136,93,.30), transparent 70%),
radial-gradient(440px 320px at 94% -10%, rgba(208,65,93,.26), transparent 70%),
radial-gradient(380px 300px at 50% -16%, rgba(106,75,219,.26), transparent 72%),
#faf7fd;
}
Corner Radii
Token: --radius-{step}. 9 steps from none to pill.
Border Widths
5-step scale matching actual codebase usage frequency. --border-width-thin (1px) is the default; --border-width-md (2px) emphasises a focal element. --border-width-hairline (0.5px) is reserved for sub-pixel separators that need to stay visually lighter than 1px on high-DPI screens.
Opacity
6-step opacity scale for visual hierarchy and interaction states. Apply via the opacity property on an existing element. For colour-with-alpha tints (e.g. tint a backdrop), prefer rgba(var(--color-rgb), α) instead.
Write opacity: 0 and opacity: 1 as literals — they're not tokens. They represent "hidden" and "fully visible" reset states.
Icon & Avatar Sizes
Square dimensions for icons and avatars. The two scales intentionally overlap (24px and 32px appear in both) — use the --avatar-* tier when the element represents a person or profile so the intent is clear at the call site.
The clean scale above is the intended standard. Older components still use off-scale dimensions like 27px, 30px, 35px, 37px, 45px, and 90px for avatars. These predate the design-token system and should be migrated to the nearest token (e.g. 27px → --icon-size-lg 24px, 45px → --avatar-md 40px) once a designer has signed off on the small visual shifts.
Icons
Icons live in the dedicated VC Icon Library — ~3,500 icons (Lucide strokes + Tabler fills) with search, categories, stroke-width & colour controls, and an AI “create / find an icon” tool. In-app they render through the <vc-icon> component.
Semantic Tokens
Assign a name to a meaning, not just a colour. Every UI state must use one of
these — never a raw hex value. Source: color-tokens.md.
--text-primary--text-secondary--color-white
--color-success--color-success-bg--color-error--color-error-bg--color-warning--color-warning-bg--color-link--color-focus--color-disabled-bg--color-disabled-text--overlay-modal--overlay-subtle--color-available--color-available-darkButtons
Uses brand, semantic, and neutral tokens. Border-radius: --radius-pill.
Badges
Semantic status indicators. Background uses -50 tint, text uses
-600.
Alerts
Background uses -25, border uses -200, text uses
-600.
Inputs
Border: --neutral-300. Focus ring: --brand-500 at 12% opacity.
Radius: --radius (8px).
Typography
8 sizes · 4 weights · 2 families · 2 line heights · 2 letter-spacings. Always use the token — never a raw value.
$font-size-xs0.6875rem
$font-size-sm0.75rem
$font-size-dense0.8125rem
$font-size-md
DEFAULT
0.875rem
$font-size-lg1rem
$font-size-xl1.125rem
$font-size-2xl1.25rem
$font-size-3xl1.625rem
$font-family-base$font-family-monojohn.doe@company.com
This Week
Spacing
8 steps — applies to padding, margin, and gap. Based on a 4px base with 5px-grid
influence. Write 0 as a literal, never a token.
Z-Index
15-step stacking hierarchy. Mid-tier values (1000-1090) follow Bootstrap's conventions so existing utility consumers stay aligned. Reach for the top tier (1100+) only for full-screen system overlays that must sit above modals. Never invent ad-hoc z-index: 9999 values — pick the right tier.
Cards
Dropdown / Menu
Pattern used by the profile widget and Explore More subheader menu. 16px radius, neutral-600 shadow at 10% opacity, 36px compact rows, brand-50 hover. Two variants: text-only and icon + label.
| Panel radius | 16px |
| Panel shadow | 0 12px 32px rgba(var(--shadow-color-rgb), 0.10) |
| Item height | 36px · margin 0 12px · radius --radius-sm |
| Item label | 13px / 500 · --text-primary |
| Hover | bg --brand-50 · text --company-color-hex · weight 600 |
| Section label | 9px / 700 / uppercase · --neutral-400 |
| Icon chip | 26 × 26 · radius 50% · bg --neutral-50 |
| Glyph | Lucide line · 14×14 · stroke 2px · --company-color-hex |
| Divider | 1px · --neutral-100 · margin 4px 20px |
Components — Cards
Reward & recognition surfaces built during the design-system rollout. Static replicas; in-app these are Angular components.
<vc-leaderboard>Brand-purple panel, bottom-aligned 2·1·3 columns (winner centred & larger), white avatar rings, medal rank seals, and a white laurel framing #1. Tokens: --company-color-hex, --radius-xl, --space-8; medal #dba91e/#a6b1c0/#bd7339.
White card: header with a confetti burst (top-right), a company-hex-ringed avatar carrying the medal seal, the recipient name in --company-color-hex (clickable) + role, and a round chevron “view details” button; below, a white Awards · Badges stats strip split by a --neutral-200 divider. Tokens: --shadow-sm, --radius-xl, avatar ring --border-width-thin --company-color-hex.
White card, confetti header (avatar + purple name + dept), divider, white body with recent-award medal + chevron. Tokens: --company-color-hex ring/name, --neutral-200 divider, --radius-xl.
Leaderboard sidebar: brand-tint card, ringed avatar with medal seal (top-3 only), “You”, and a Current-Rank stat tile. Tokens: color-mix(--company-color-hex) tint, --radius-xl.
Components — Modals
Dialog surfaces. All share the soft gradient header, generous body padding, and the Lucide-X close button.
Centred header stack over a centred 2-up award grid; each award is a split card (neutral medal top, white title/points body) — mirrors the “By Awards” layout. Tokens: gradient header, --neutral-50 card top, --radius-lg.
Anurag’s Awards
Marketing (R&R) · 12 awards
Avatar hugged by a pill card; the AI-style sparkle sits on the avatar’s bottom-right edge (left/top:84%). Primary “Upload” + destructive text-link “Delete Photo”. Tokens: --radius-full, --error-600.
Edit Profile Photo
Avatar → name rows with a fixed gap (the global p-reset excludes this dialog so the name keeps its spacing from the avatar). Tokens: --radius-full avatars, --neutral-200 header rule.
Components — Feed preview card
Recognition “preview your post” card. Mirrors the vc-feed v9 post layout: header (recipient + “appreciated by” + time + menu), optional 2-line-clamped caption, award/photo body, hashtags, Like/Comment footer.
Tokens: --radius-xl, --shadow-md, caption font-size:var(--font-size-md)/line-height:1.5, award block --neutral-50, hashtags & action icons --company-color-hex.
Heather Tuffnail was appreciated by Anurag Barkataki
Just now
Star Performer
Heather Tuffnail
Components — Badges & pills
Small shared elements used across the recognition surfaces.
Scalloped “seal” badge for top-3 ranks. Built from a SVG mask (centre circle + 8 perimeter bumps) layered as pseudo-elements — an unmasked base carries the drop-shadow, a masked gradient fills the seal, and a slightly-larger masked white layer draws the stroke (a mask clips drop-shadow, so the stroke must be a backing layer, not a shadow). Medal palette: gold #dba91e, silver #a6b1c0, bronze #bd7339.
Capsule in the global header: wallet icon + a vertically-centred “Vantage Points / value” stack, with the profile avatar alongside. Value is a <span> (never a nested <a> — that produced invalid HTML and broke centring). Tokens: --radius-pill, --shadow-xs, brand-tint bg.
Foundation Principles
The non-negotiables. Every rule later in this document is downstream of one of these.
- Tokens, not literals. Never write a raw hex, px, rem, or rgba in a component. If a token doesn't exist for the value you need, the value is wrong — pick the nearest token instead of inventing a new one.
- Semantic over primitive. Reach for
--text-primarybefore--neutral-600,--color-errorbefore--error-500. Primitives are for defining tokens; semantics are for using them. - One way to do each thing. A button is a
.btn-*class, not a hand-rolled border-radius + padding. A card is.card, not bespoke shadow + radius. Variants exist; reinventions don't. - No
!important. It is the symptom of a specificity problem, not a fix. If you need it, you're styling at the wrong layer — fix the cascade or scope the selector instead. - Brand colour is one ramp, not seven shades of purple. All purple comes from the
--brand-*ramp. Never hand-pick a "slightly different purple" — use the next step on the ramp. - Light by default, dark by intention. Surfaces are white or
--neutral-50/100. Dark sections are reserved for trust strips, testimonials, gradient CTAs, and the footer — and never two in a row. - Accent colour is loud — use it like punctuation. Status colours (
success,error,warning,info) communicate state. They never decorate. They never live alongside other status colours in the same component. - Mobile-first responsive. Every layout is designed for a 320–375 px viewport first, then upscaled at
sm,md,lg,xl. If it doesn't work on mobile, it isn't done.
Section Layout Patterns
Every content section is built from one of these skeletons. Copy-paste, don't reinvent.
--brand-25 or --brand-50 as section background.Page Anatomy
A typical page rhythm. Not every section is required — but light/dark alternation is.
Rhythm Rules
- Alternate light ↔ dark — never stack two darks
- White is the default surface
- One CTA per section; outline by default; only the hero gets the filled
.btn-primary - Section headings are centred with
max-width: 56chto prevent long lines - Every section is self-contained —
<section class="section">with its own.container
Responsive Behavior
Mobile-first. Designed for 320–375 px viewport, then upscaled.
Grid Collapse
| Desktop | Mobile fallback |
|---|---|
grid-cols-3 | Single-column stack |
grid-cols-2 | Single-column stack |
grid-cols-5 (stat grid) | grid-cols-2, last card spans 2 |
| Side-by-side card+image | Image below text |
Mobile-Specific Treatments
- Section padding:
--space-5horizontal mobile →--space-8desktop. Vertical scales similarly. - Headings: Step down one size token on mobile, e.g.
--font-size-2xldesktop →--font-size-xlmobile. - Card padding:
--space-5mobile →--space-7/8desktop. - Text alignment: Centre on mobile, left-align ≥
lg. Single rule: hero is always centre on mobile. - CTA stacking: Vertical stack ≤
sm, horizontal ≥md. Primary always first in DOM and visually.
Mobile Carousel Pattern
For 3+ items that don't fit a stacked layout, use a snap carousel on mobile, grid on desktop.
Image & Media Treatment
Rules for loading, sizing, and responsive image patterns.
Loading & Format
- All non-hero images:
loading="lazy" - All Cloudinary URLs use
f_auto,q_auto - Width hint:
w_400for card images,w_1000for hero - Decorative images:
alt=""androle="presentation"
Image Sizing
| Context | Pattern |
|---|---|
| Logos in cards | Fixed width, no explicit height |
| Card feature images | max-width: 100%; max-height: 100%; object-fit: contain |
| Avatars | border-radius: var(--radius-pill), fixed width = height |
| Award badges | max-width: 130px, object-fit: contain, height: auto |
| Trust logos | Fixed height 40px, auto width, horizontal scroll on mobile |
Dark Section Rules
Dark surfaces are reserved for trust strips, testimonials, gradient CTAs, and the footer. Light by default, dark by intention.
Live example — dark section
Body text uses rgba(255,255,255,0.7), never --text-primary (which is invisible on dark).
When to Go Dark
| Surface | Use |
|---|---|
--brand-600 | Compact info bars, trust strips, single-row content |
--neutral-600 | Footer, mature content sections |
Brand gradient (--brand-500 → --brand-600) | Feature showcases, CTA banners |
What's Allowed on Dark
| Element | Treatment |
|---|---|
| Headings | --color-white |
| Body | rgba(255,255,255,0.7) |
| Muted | rgba(255,255,255,0.5) |
| Buttons | .btn-neutral (white-fill) or .btn-outline with white border + white text |
| Links | --color-white, no underline by default — text-decoration: underline on hover |
| Icons / badges | rgba(255,255,255,0.08) container background |
Don'ts on Dark
- No
--text-primary(#33475b is invisible on dark) - No
.btn-primary(brand purple on brand-dark = no contrast). Use.btn-neutralor outline-white instead - No
--neutral-300borders. Usergba(255,255,255,0.08)for hairlines - Never two dark sections back-to-back. Separate with a light section or trust strip
Breadcrumbs
Shared component. Sits between the hero and any tab/filter navigation.
| Property | Token / Value |
|---|---|
| Font-size | --font-size-sm desktop · --font-size-xs mobile |
| Link colour | --text-secondary |
| Link hover | --brand-500 |
| Current colour | --text-primary, --font-weight-medium |
| Separator | / in --neutral-300 |
| Gap | --space-2 |
| Margin-bottom | --space-6 desktop · --space-3 mobile |
Motion & Transitions
100+ uses of all 0.3s ease-in-out showed up in the audit with no token. Standardised below. Hover the demo boxes to feel each duration.
Duration
| Token | Value | Usage |
|---|---|---|
--duration-fast | 150 ms | Hover colour shifts, focus rings, tooltip fade |
--duration-base | 200 ms | Default — buttons, inputs, dropdowns |
--duration-slow | 300 ms | Card hover lift, modal open, drawer slide |
--duration-slower | 500 ms | Carousel slide, hero fade-in |
Easing
| Token | Value | Usage |
|---|---|---|
--ease-out | cubic-bezier(0.16, 1, 0.3, 1) | Entrance — modals, dropdowns, toasts |
--ease-in-out | cubic-bezier(0.65, 0, 0.35, 1) | Default — most state changes |
--ease-bounce | cubic-bezier(0.34, 1.56, 0.64, 1) | Optional pop — chip select, success checkmark |
transition: all. Always enumerate properties — the audit shows all is the single biggest cause of unintended re-renders and layout flicker.
Design Rules
The five rules everyone breaks first.
- Brand colour is
--brand-500. Period. Don't mix in raw#5b3b97even though it's "the same". Use the token, so the runtime override works. - No pure grey. All neutrals have a slight blue cast. Replacing
#999or#666with the right--neutral-*step is almost always correct. - One
<h1>per page. Heading levels are semantic — never skip from<h2>to<h4>for visual reasons. If you need a smaller heading, use a smaller font-size token. - Status colours don't decorate.
--color-successis for state, not for "this is good news". A green-bordered card is wrong unless the card represents a successful state. - Section padding is
--space-8vertical, container--space-5/8horizontal. No bespoke values. The rhythm is the rhythm.
Common Mistakes & Fixes
Highest-frequency violations from the codebase audit. Use this as a code review checklist.
| ❌ Mistake | ✅ Fix |
|---|---|
#fff or #ffffff written inline | Use var(--color-white) |
#33475b or any hex matching a token | Use matching --text-* / --neutral-* token |
| Multiple values for same role (4 different success greens) | Collapse to var(--color-success) (#28a745) |
Pure grey (#999, #666, #ccc, #000) | Replace with bluish-grey via --neutral-* ramp |
!important to override a parent style | Fix the cascade — scope the selector or move the rule into the right layer |
Hardcoded 14px font size | Use var(--font-size-md) |
Hardcoded 0.7rem font size | Use var(--font-size-sm) (12) — drop 0.7rem entirely |
| Missing font-weight | Always set explicitly with var(--font-weight-*) |
font-family: Inter (unquoted) | Always var(--font-family-base) |
border-radius: 16px literal | Use var(--radius-xl) |
border-radius: 25px literal (button) | Use var(--radius-pill) |
border-radius: 8px literal (input) | Use var(--radius) |
Custom shadow (0px 3px 6px #00000029) | Snap to nearest --shadow-* token |
transition: all 0.3s ease-in-out | Enumerate properties; use --duration-slow + --ease-in-out |
Square buttons (--radius-md/lg) | All buttons are --radius-pill |
| Buttons with no focus ring | Add box-shadow: 0 0 0 4px var(--color-focus) on :focus-visible |
Heading text in --brand-500 | Headings stay --text-primary. Brand colour is for CTAs and links |
Body paragraph in --brand-* or status colour | Body is always --text-primary (or --text-secondary) |
text-transform: uppercase on buttons | Sentence case only |
Multiple <h1> on one page | Exactly one <h1> per page, in the hero |
Image without loading="lazy" (below fold) | Add lazy loading to all non-hero images |
| Decorative image with descriptive alt | alt="" and role="presentation" |
Section without .container wrapper | Wrap content in <div class="container"> |
| Two dark sections back-to-back | Insert a light section or trust strip between them |
.btn-primary on a dark surface | Use .btn-neutral (white fill) or outline-white instead |
CSS Variable Quick-Reference
Copy-paste root for any new project or audit baseline. Click each block to expand.
Brand (runtime override-able)
--brand-25: #f6f4fb; --brand-50: #f1edf8; --brand-100: #e2daf1; --brand-200: #c5b5e2; --brand-300: #a086d0; --brand-400: #7a56bd; --brand-500: #5b3b97; --brand-600: #472e75;
Neutrals
--neutral-50: #f5f5f7; --neutral-100: #eff2f5; --neutral-200: #eef0f4; --neutral-300: #d9dde7; --neutral-400: #637281; --neutral-500: #5c6c7c; --neutral-600: #475467;
Semantic text + status
--text-primary: #33475b; --text-secondary: var(--neutral-500); --color-white: #ffffff; --color-success: #28a745; --color-error: #dc3545; --color-warning: #ffc107; --color-link: var(--brand-500); --color-focus: rgba(var(--company-color), 0.25); --color-disabled-bg: #eef0f4; --color-disabled-text: #637281;
Spacing & layout
--space-1: 4px; --space-2: 8px; --space-3: 10px; --space-4: 12px; --space-5: 16px; --space-6: 20px; --space-7: 24px; --space-8: 32px;
Border radius & widths
--radius-none: 0; --radius-2xs: 2px; --radius-xs: 4px; --radius-sm: 6px; --radius: 8px; --radius-md: 10px; --radius-lg: 12px; --radius-xl: 16px; --radius-pill: 50px; --border-width-hairline: 0.5px; --border-width-thin: 1px; --border-width-md: 2px; --border-width-thick: 3px; --border-width-heavy: 5px;
Typography
--font-family-base: 'Inter', system-ui, sans-serif; --font-size-xs: 11px; --font-size-sm: 12px; --font-size-dense: 13px; --font-size-md: 14px; --font-size-lg: 16px; --font-size-xl: 18px; --font-size-2xl: 20px; --font-size-3xl: 26px; --font-weight-regular: 400; --font-weight-medium: 500; --font-weight-semibold: 600; --font-weight-bold: 700;
Shadows (uniform 0.10 alpha on --shadow-color-rgb)
--shadow-color-rgb: 71, 84, 103; --shadow-xs: 0px 1px 2px rgba(var(--shadow-color-rgb), 0.10); --shadow-sm: 0px 1px 3px rgba(…,0.10), 0px 1px 2px rgba(…,0.10); --shadow-md: 0px 4px 8px rgba(…,0.10), 0px 2px 4px rgba(…,0.10); --shadow-lg: 0px 12px 16px rgba(…,0.10), 0px 4px 6px rgba(…,0.10); --shadow-xl: 0px 20px 24px rgba(…,0.10), 0px 8px 8px rgba(…,0.10); --shadow-2xl: 0px 24px 48px rgba(var(--shadow-color-rgb), 0.10);
Motion & opacity
--duration-fast: 150ms; --duration-base: 200ms; --duration-slow: 300ms; --duration-slower: 500ms; --ease-out: cubic-bezier(0.16, 1, 0.3, 1); --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); --ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1); --opacity-faint: 0.3; --opacity-disabled: 0.4; --opacity-muted: 0.6; --opacity-subtle: 0.7; --opacity-soft: 0.8; --opacity-strong: 0.9;
Z-index (Bootstrap-compatible)
--z-behind: -1; --z-base: 0; --z-raised: 1; --z-elevated: 10; --z-dropdown: 1000; --z-sticky: 1020; --z-fixed: 1030; --z-overlay: 1040; --z-offcanvas: 1050; --z-modal: 1060; --z-popover: 1070; --z-tooltip: 1080; --z-toast: 1090; --z-tour: 1100; --z-max: 1200;
Token Migration Cheatsheet
Use when refactoring existing SCSS or replacing literals during code review.
Color
| Old literal | New token |
|---|---|
#fff, #ffffff, white | var(--color-white) |
#33475b, $text-primary | var(--text-primary) |
#5c6c7c, $text_secondary | var(--text-secondary) |
#d9dde7, $border-color | var(--neutral-300) |
#eff2f5, $base_color, $sec-bg | var(--neutral-100) |
#eef0f4, $border-bottom | var(--neutral-200) |
#637281, rgb(99,114,129), $icon-color | var(--neutral-400) |
#5b3b97, --company-color-hex, rgba(91,59,151,1) | var(--brand-500) |
#28a745, #389704, #6DD400, #459D14 | var(--color-success) |
#dc3545, red | var(--color-error) |
#ffc107 | var(--color-warning) |
#17a2b8 | var(--info-500) |
#6c757d, #adb5bd, #ced4da | var(--color-disabled-text) |
rgba(0,0,0,0.5) | var(--overlay-modal) |
rgba(0,0,0,0.1) | var(--overlay-subtle) |
Typography
| Old literal | New token |
|---|---|
14px, $normal | var(--font-size-md) |
12px, 0.7rem | var(--font-size-sm) |
13px, 0.8rem | var(--font-size-dense) |
15px, 16px, 1rem | var(--font-size-lg) |
18px | var(--font-size-xl) |
20px | var(--font-size-2xl) |
Inter, "Inter", Roboto | var(--font-family-base) |
font-weight: 500 | var(--font-weight-medium) |
font-weight: 700 | var(--font-weight-bold) |
Radius
| Old literal | New token |
|---|---|
2px | var(--radius-2xs) |
4px | var(--radius-xs) |
5px, 6px | var(--radius-sm) |
8px | var(--radius) |
10px | var(--radius-md) |
12px | var(--radius-lg) |
15px, 16px | var(--radius-xl) |
25px, 9999px, 4.4375rem (button) | var(--radius-pill) |
50% (true circle) | var(--radius-pill) with equal width = height |
Shadow
| Old literal | New token |
|---|---|
0px 3px 6px #00000029, $box-shadow | var(--shadow-sm) |
0px 3px 5px 0px rgb(0 0 0 / 21%) | var(--shadow-md) |
0px 3px 20px 0px rgb(0 0 0 / 10%) | var(--shadow-lg) |
rgba(0, 0, 0, 0.15) 0px 5px 15px 0px | var(--shadow-xl) |
0 4px 6px 0 rgba(120,135,150,0.20) | var(--shadow-md) |
Transition
| Old literal | New tokens |
|---|---|
all 0.3s ease-in-out | Enumerate props + var(--duration-slow) var(--ease-in-out) |
all 0.2s ease-in-out | Enumerate props + var(--duration-base) var(--ease-in-out) |
all 0.5s cubic-bezier(...) | Pick var(--duration-slower) + var(--ease-out) or --ease-bounce |