Single source of truth for frontend design decisions. Status: v1.0 · Documentation only — no components have been modified to produce this file.
Before writing a single guideline, the existing codebase was audited directly (not assumed). Findings referenced throughout this doc are grounded in that audit:
- 21 distinct
font-sizevalues in use across components/pages, with no shared type scale (e.g.12px,12.5px,13px,13.5px,14px,14.5pxall present as separate ad-hoc values). - No spacing scale exists. Padding is hardcoded per-component (
14px,20px,24px,28px,30px,32px,40px,44px,48px...) with no shared increments. - 9 different responsive breakpoints (
380,420,460,480,520,560,720,860,900,940px) instead of a consistent 3-tier system. - 18 separate CSS files independently redefine the same "card" pattern (
border: 1px solid var(--line)+border-radius+background: var(--surface)) instead of extending one shared primitive. - Input styling is duplicated 5 times — Wardrobe's edit form, Upload's item form, Profile's edit form, and both Login/Signup forms each redefine their own
input/selectrules rather than sharing one. - Icons are Unicode glyphs (
⌂ ⇧ ⌗ ↻ ☀ ☾ ⌘ ⚑ ✂ ♥ ✓ × ⋯ ☰ ▦ ⌕), not an icon library. These render inconsistently across OS/browser font stacks and can't be sized, stroked, or aligned reliably. Button.cssdefines aprimaryvariant that is never used anywhere in the app — every call site usesaccent,outline,ghost, ordanger. Dead code.AuthForm.cssexists twice, byte-identical, inpages/Login/andpages/Signup/.- An unused, unreferenced
hero.png(16KB) sits insrc/assets/alongside the actual SVG-based hero graphic. - A stray malformed directory (
src/{assets,components,pages,styles,context}) was left behind by an earlier shell command — harmless but noted for cleanliness.
None of this is a criticism of the visual direction — the burgundy/brass "atelier" identity (Fraunces + Public Sans, garment-tag motif) is a deliberate, distinctive choice and should be preserved, not replaced. This document formalizes it into enforceable tokens so it stays consistent as the app grows.
Selvedge reads as a boutique atelier, not a SaaS dashboard: warm paper tones, a serif display face, and a garment-tag/hanger visual motif. Every guideline below exists to protect that identity at scale — not to genericize it.
| Token | Light | Dark | Usage |
|---|---|---|---|
--paper |
#EFEBE2 |
#131215 |
App background |
--surface |
#FFFFFF |
#1C1B1F |
Cards, modals, inputs |
--surface-raised |
#FAF8F3 |
#232128 |
Nested/hover surfaces, skeletons |
--ink |
#19171B |
#EFEAE3 |
Primary text, primary buttons |
--ink-soft |
#58534E |
#A79E93 |
Secondary text, captions |
--line |
#DCD5C7 |
#322F35 |
Borders, dividers |
--accent |
#6B2737 |
#C77E8C |
Brand accent, links, active states |
--accent-soft |
#F4E4E6 |
#2E1E22 |
Accent tints (icon chips, badges) |
--brass |
#B8935F |
#D4AF7A |
Secondary accent (tags, chart fills) |
--brass-soft |
#F1E6D3 |
#2B2519 |
Brass tints |
--success |
#3E6B4A |
same | Positive states |
--danger |
#A23B2E |
same | Destructive states, errors |
No --warning token exists. Add one for future use (form warnings, low-stock-style states):
--warning: #9C6B1F;
--warning-soft: #F6EAD2;- Never hardcode a hex value in component CSS. If a color isn't in the token list, that's a signal to extend the token list, not to inline a one-off.
--accentis used for exactly three things: interactive affordances (links, active nav, focused inputs), the brand accent chip, and primary CTA hover. It should never be used as a large background fill (reserve that for--ink).
Add to variables.css:
--text-xs: 12px; /* meta, timestamps, mono captions */
--text-sm: 13px; /* secondary body, form hints */
--text-base: 14px; /* default body copy, buttons */
--text-md: 15px; /* comfortable body (hero lede) */
--text-lg: 16px; /* card titles, emphasized body */
--text-xl: 18px; /* section sub-headings, modal titles */
--text-2xl: 22px; /* card display numbers, small H2 */
--text-3xl: 26px; /* page H1 (compact pages) */
--text-4xl: 32px; /* page H1 (standard) */
--text-5xl: 40px; /* section H2 */
--text-6xl: 46px; /* hero H1 */Migration is additive: existing near-matches (12.5px→12, 13.5px→13, 14.5px→14/15, 24px→22, 30px→32) round to the nearest token with no visible regression.
- Display —
Fraunces— page titles, hero headline, card display numbers. Use weight 600–700; italic for the one-line editorial accent (as in the hero's "wear on cue"). - Body —
Public Sans— everything else. Weights 400 (body) / 600 (labels, buttons, nav links). - Mono —
IBM Plex Mono— eyebrows, tag pills, chart labels, timestamps. Always uppercase + letter-spacing0.06–0.14emwhen used as a label.
- Headings never take
font-weightabove 700 or below 500. - Body text line-height:
1.5default,1.6–1.65for lede/paragraph copy,1.1–1.25for display headings. - Letter-spacing on headings:
-0.01em(already applied globally — keep).
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
--space-12: 48px;
--space-16: 64px;
--space-20: 80px;
--space-24: 96px;- Component internal padding:
--space-4(compact: chips, tags) to--space-6(cards, modals). - Section vertical rhythm:
--space-16to--space-24between major page sections (matches existing.section { padding: 72px 0 }closely — round to--space-16+ a bit, or introduce--space-18: 72pxif exact preservation matters). - Grid gaps:
--space-5(20px) is the default for card grids — this is already the de facto standard across.wardrobe-grid,.outfit-grid,.favorites-grid. Formalize it, don't change it. - Never use an arbitrary px value for padding/gap/margin once this scale exists. If nothing fits, that's a sign the layout needs reconsideration, not a new one-off value.
Keep the existing 3-tier scale, but name the two currently-unnamed values that recur everywhere:
--radius-sm: 6px; /* inputs, tags, small chips */
--radius-md: 12px; /* cards, buttons-as-blocks, dropdowns */
--radius-lg: 20px; /* modals, hero panels, empty states */
--radius-pill: 999px; /* buttons, search bar, filter chips, toggles */
--radius-full: 50%; /* avatars, icon circles, dots */--radius-pill and --radius-full are currently written as raw 999px / 50% in 12+ places — tokenizing them costs nothing and makes intent explicit in a diff.
Keep both existing shadows, they're well-tuned (soft, warm-tinted, dark-mode-aware):
--shadow-tag: 0 1px 2px rgba(25,23,27,0.06), 0 8px 20px rgba(25,23,27,0.06); /* floating/rotated elements (garment tags) */
--shadow-raised: 0 4px 14px rgba(25,23,27,0.10), 0 1px 3px rgba(25,23,27,0.08); /* hover elevation, modals, toasts */Add one for pressed/inset states (currently missing, would help form focus states feel more tactile):
--shadow-inset: inset 0 1px 2px rgba(25,23,27,0.06);Rule: exactly two elevation levels exist by design (resting = border only, raised = --shadow-raised). Do not introduce a third — flatness is part of the brand's restraint.
| Variant | Use for | Status |
|---|---|---|
accent |
Primary CTA (one per view) | ✅ keep |
outline |
Secondary action | ✅ keep |
ghost |
Tertiary / low-emphasis (cancel, menu triggers) | ✅ keep |
danger |
Destructive confirm | ✅ keep |
primary |
— | ❌ remove — defined in Button.css but called zero times across the app. Delete to avoid a confusing 5th option next to accent. |
sm (14px pill, most-used — chips, table-row actions) / md (default) / lg (hero, form submit). This 3-tier system is already correctly used everywhere — no changes needed.
- One
accentbutton per screen/section maximum. Everything else isoutlineorghost. - Icon-only buttons (avatar, theme toggle, modal close) are a separate pattern from
Button.jsx— that's correct as-is, don't force them into the Button component.
Five different files currently define their own input styling with near-identical but not-quite-matching rules (border color, padding, radius, focus treatment). This is the single highest-value consolidation target in the whole codebase.
Introduce one canonical rule set — either as a new Input component or, at minimum, a single .field utility class in global.css that every form imports instead of redefining:
.field {
border: 1px solid var(--line);
border-radius: var(--radius-sm);
padding: var(--space-3) var(--space-4); /* 12px 16px, was 10–12px inconsistently */
font-size: var(--text-base);
color: var(--ink);
background: var(--surface);
transition: border-color var(--duration) var(--ease), box-shadow var(--duration) var(--ease);
}
.field:focus { border-color: var(--accent); outline: none; box-shadow: 0 0 0 3px var(--accent-soft); }Note the one real UX gap found: current inputs only change border-color on focus, with no visible focus ring beyond that — weaker than the rest of the app's :focus-visible accent-outline standard defined globally. Adding the box-shadow ring here brings inputs in line with buttons/links.
Standardize the label wrapper (currently repeated 5×: display:flex; flex-direction:column; gap:6px; font-size:12.5px; color:var(--ink-soft)) into .field-label alongside .field.
18 files independently write background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-*). None conflict destructively, but every one is a copy-paste of the same 3 lines.
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius-md);
transition: transform var(--duration) var(--ease), box-shadow var(--duration) var(--ease);
}
.card--hoverable:hover { transform: translateY(-3px); box-shadow: var(--shadow-raised); }Existing card-like components (ClothingCard, OutfitCard, StatsCard, .feature-card, .step-card, .chart-card, .profile-card, .settings-section) keep their own class for content-specific layout, but that class should extend .card (add card to the className string) rather than re-declare the base visual rules. This is a pure subtractive change — remove ~3 duplicate lines from each of the 18 files, add nothing new to markup logic.
Unicode glyphs (⌂ ⇧ ⌗ ↻ ☀ ☾ ⌘ ⚑ ✂ ♥ ✓ × ⋯ ☰ ▦ ⌕) are used throughout for icons. Problems:
- Rendering varies by OS font (emoji-style on some systems, dotted-box "tofu" on others for less common glyphs).
- Cannot be sized, stroked, or color-transitioned independently of text — currently coupled to
font-size+color, which is fragile. - Not accessible by default (need
aria-hidden+ separate label everywhere, currently inconsistent).
Adopt lucide-react (already available in this project's toolchain) as the single icon source. It matches the brand's restrained, linear aesthetic far better than default emoji glyphs, and every icon shares consistent stroke width/sizing props.
Suggested 1:1 replacements:
| Current glyph | Meaning | Lucide icon |
|---|---|---|
♥ |
Favorite | Heart |
✓ |
Confirm/selected | Check |
× |
Close/remove | X |
⋯ |
More menu | MoreHorizontal |
☰ / ▦ |
List/grid view | List / LayoutGrid |
⌕ |
Search | Search |
☀ / ☾ |
Theme toggle | Sun / Moon |
⇧ |
Upload | Upload |
↻ |
Wear count / refresh | RotateCw |
⌗ |
Tags | Hash or Tags |
⚑ |
Flag/unworn | Flag |
The hand-drawn hero hanger SVG and garment-tag motif are the one deliberate signature illustration — that stays hand-crafted SVG, not swapped for a library icon. Icon library replacement applies only to functional UI icons, never to the brand's signature visual.
--ease: cubic-bezier(0.22, 1, 0.36, 1);
--duration: 220ms;Already respects prefers-reduced-motion globally — this is correctly implemented and should not be touched.
- 220ms +
--easeis the only transition timing for interactive state changes (hover, focus, toggle). Don't introduce a second duration for the same category of interaction. - Ambient/looping animation (the hero's floating garment tags) is the one place continuous motion is allowed. It should not spread to cards, buttons, or list items — those get discrete transitions only, triggered by user action.
- Skeleton shimmer (
Loader.css) is the only other continuous animation permitted, since it communicates active loading state.
- Container:
max-width: 1180px,padding: 0 32px— already global via.container, correct, keep as the only container width in the app. - Card grids:
grid-template-columns: repeat(auto-fill, minmax(Npx, 1fr))where N is210px(dense wardrobe grid) or220–240px(outfit/favorites grids). Standardize on220pxas the single minimum card width across all three grids — the 210/220/240px split found in the audit is cosmetic drift, not intentional. - Two-column layouts (Upload form, Auth split-screen):
1fr 1fror0.85fr 1fr, always collapsing to single column at themdbreakpoint (see §13).
9 distinct breakpoint values currently in use (380, 420, 460, 480, 520, 560, 720, 860, 900, 940px) with no naming or shared logic — each component picked whatever number made its own layout stop breaking.
--bp-sm: 480px; /* phone */
--bp-md: 768px; /* tablet / stacked two-column layouts */
--bp-lg: 1024px; /* nav collapse, sidebar/grid breakpoints */Since CSS custom properties can't be used inside @media directly, treat these as documented constants and hardcode the matching px value at each of the 3 tiers only — never a 4th number. The one justified exception is the navbar's own collapse point, which can stay wherever the link count actually wraps (currently 900px) — document it as --bp-nav-collapse: 900px in comments so it reads as intentional, not drift.
This is the lowest-risk, highest-clarity fix in this document: every existing breakpoint rounds to its nearest tier (380/420/460/480→480, 520/560→768 or stays a deliberate in-between only for nav, 720/768→768, 860/900/940→1024). Visually near-identical, dramatically easier to maintain.
- Product/item photography (clothing images): keep the current
3:4aspect ratio andobject-fit: covertreatment used inClothingCard— correct for a fashion catalog and should propagate to any new photo-bearing component. - Placeholder images: currently sourced from
loremflickr.comwith keyword+lock params so demo content visually matches item names. This is correct practice for a portfolio demo — keep, but note in code comments that production would replace this with real user-uploaded + background-removed images. - Illustration (hero hanger, garment tags): hand-built inline SVG using CSS variables for stroke/fill so it themes correctly in dark mode — this is the right pattern and should be the template for any future custom illustration. Never hardcode a color inside an SVG that should adapt to theme.
- Cleanup:
src/assets/hero.png(16KB) is unused and unreferenced anywhere in the codebase — safe to delete, it's dead weight from initial scaffolding.
- File pairing: every component ships as
ComponentName.jsx+ComponentName.cssin its own folder — already consistent across all 13 components, keep this pattern for anything new. - CSS class naming: BEM-lite, already the de facto standard in this codebase (
.clothing-card__media,.clothing-card__fav,.clothing-card.is-selected). Formalize it:- Block:
.component-name - Element:
.component-name__part - State:
.is-state(e.g..is-active,.is-selected,.is-open) — modifies a block or element, never a standalone class.
- Block:
- No inline
style={{ }}for anything expressible in CSS. Current exceptions (Hero.jsx's per-tag rotation/depth,Statistics.jsx's dynamic bar heights,Footer.jsx's icon color override) are legitimate — they're driven by runtime data (loop index, computed percentage), which CSS classes can't express. Anything else should move to a class. - Shared primitives get a shared home. Once
.cardand.fieldexist (§8, §9), they belong inglobal.cssor a newsrc/styles/primitives.css— not duplicated into whichever component needs them first. - De-duplicate
AuthForm.css. It's currently byte-identical inpages/Login/andpages/Signup/. Move it tosrc/styles/auth-form.css(or make itAuthLayout.css's responsibility, since both pages already shareAuthLayout.jsx) and delete the second copy.
Everything below is additive to variables.css — nothing existing is removed or renamed:
/* type scale */
--text-xs: 12px; --text-sm: 13px; --text-base: 14px; --text-md: 15px;
--text-lg: 16px; --text-xl: 18px; --text-2xl: 22px; --text-3xl: 26px;
--text-4xl: 32px; --text-5xl: 40px; --text-6xl: 46px;
/* spacing */
--space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px;
--space-5: 20px; --space-6: 24px; --space-8: 32px; --space-10: 40px;
--space-12: 48px; --space-16: 64px; --space-20: 80px; --space-24: 96px;
/* radius additions */
--radius-pill: 999px; --radius-full: 50%;
/* shadow addition */
--shadow-inset: inset 0 1px 2px rgba(25,23,27,0.06);
/* color addition */
--warning: #9C6B1F; --warning-soft: #F6EAD2;This document reflects the state of the codebase as audited. It should be updated whenever a new pattern is deliberately introduced — if a component needs something not covered here, that's a signal to extend this file first, then build.