Factory Design System
A rigorous, pixel-level reproduction of the Factory.ai aesthetic. This skill defines every measurement, variable, class combination, and animation that must be used. When building UI in this system, copy these values verbatim — do not round, re-interpret, or "modernize" them.
Core Philosophy
Factory's look is "agent-native developer tooling". Five non-negotiable rules define the vibe:
- Monospace is the voice.
Geist Monois the default typeface for ~90% of text (nav, labels, body paragraphs, buttons, badges, code).Geist Sansappears only for display/marketing headings and section titles — never for UI chrome. - Dark-first, tokenized. Always author in dark mode first. Never hardcode colors — always
var(--color-...). Light theme is the mirror, not a rewrite. - Micro-typography. Tracking is tight and negative almost everywhere (
tracking-[-0.015rem],tracking-[-0.0175rem],tracking-[-0.04em]on headlines). Font sizes are specific pixels (text-[12px],text-[14px],text-[60px]), not Tailwind presets. - Dashed borders and diagonal stripes. Containers use
border-dashed border-[var(--color-base-700)]. Buttons and hoverable cards reveal a 45° conveyor-belt stripe pattern on hover. - Motion is earned. Three signature animations: character reveal on the hero headline, scramble on section badges, typewriter inside the CLI install box. Everything else is
duration-150/duration-200color and opacity transitions.
If a design decision isn't covered below, err toward: mono, dashed, 8-12px radius, var(--color-base-XXX), tight tracking, stripe on hover.
1. Design Tokens — copy these exactly
All tokens live in app/globals.css under :root, html / [data-theme="dark"], and [data-theme="light"]. Never inline a hex/oklch. Reference via var(--token-name).
1.1 Accent (orange — shared across themes)
--color-accent-100: #ef6f2e; /* primary orange — links, hero word, dots, "Copied" check */
--color-accent-200: #ee6018; /* hover/active */
--color-accent-300: #d15010; /* badge numerals, emphatic accents */
Usage rules:
- Exactly one accent word per headline. Example:
<span class="text-[var(--color-accent-100)]">Next-Gen</span> Platform. - Accent dot (
w-2 h-2 rounded-full bg-[var(--color-accent-100)]) precedes "Trusted by" / section intros. ::selectionis always accent-100 on dark-primary.- Nav links hover to
text-[var(--color-accent-100)]viatransition-colors duration-200.
1.2 Base Scale — inverts between themes
The base scale runs 100 (near-foreground) → 1000 (near-background). Dark theme: 100 is almost white, 1000 is near-black. Light theme: inverted. This means every component works in both themes without conditionals — you just reference the scale.
/* DARK (default) */
--background: #020202; /* page bg */
--foreground: #eee; /* primary text */
--color-base-100: #d6d3d2; /* near-foreground */
--color-base-200: #ccc9c7;
--color-base-300: #b8b3b0;
--color-base-400: #a49d9a; /* nav links, subheading body text */
--color-base-500: #8a8380; /* muted body text, "Trusted by" copy, placeholders */
--color-base-600: #5c5855; /* terminal dots, social link separators */
--color-base-700: #4d4947; /* dashed borders, dropdown borders, theme-toggle border */
--color-base-800: #3d3a39; /* solid borders on cards, tab dividers */
--color-base-900: #2e2c2b; /* card backgrounds (CTA block, footer, terminal mockup) */
--color-base-1000: #1f1d1c; /* deepest fill (command box inside terminal) */
/* LIGHT */
--background: #eee;
--foreground: #020202;
--color-base-100: #1f1d1c; /* (inverted) */
--color-base-1000: #d6d3d2;
/* ...the whole scale mirrors */
Semantic aliases (prefer these when the role is semantic, not shade-specific):
--color-dark-base-primary: #020202 / #eee; /* background surface */
--color-dark-base-secondary: #101010 / #fafafa;
--color-light-base-primary: #eee / #020202; /* readable text surface */
--color-light-base-secondary:#fafafa / #101010;
Button-specific tokens (never hardcode button colors):
/* dark */
--btn-primary-bg: #1f1d1c; --btn-primary-text: #fafafa;
--btn-secondary-bg: #fafafa; --btn-secondary-text: #1f1d1c;
--btn-secondary-border: var(--color-base-700);
/* light (inverted + transparent secondary border) */
--btn-primary-bg: #eeeeee; --btn-primary-text: #101010;
--btn-secondary-bg: #101010; --btn-secondary-text: #eeeeee;
--btn-secondary-border: transparent;
1.3 Typography
--font-sans: var(--font-geist-sans), system-ui, sans-serif;
--font-mono: var(--font-geist-mono), monospace;
Load both Geists from next/font/google in app/layout.tsx:
import { Geist, Geist_Mono } from "next/font/google";
const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] });
const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] });
Body default: font-family: var(--font-sans) + antialiased. All UI chrome explicitly overrides with font-mono.
1.4 Radius, Motion, Spacing
--spacing: 0.25rem; /* 4px base unit */
--radius-xs: 0.125rem; /* 2px */
--radius-sm: 0.25rem; /* 4px — buttons */
--radius-md: 0.375rem; /* 6px */
--radius-lg: 0.5rem; /* 8px — cards, CLI box, terminal mockup, CTA */
--radius-xl: 0.75rem; /* 12px — product dropdown panel */
--radius-2xl: 1rem;
--radius-3xl: 1.5rem;
--ease-reveal: cubic-bezier(0.645, 0.045, 0.355, 1); /* char reveal, fade-in-up */
--ease-default: cubic-bezier(0.4, 0, 0.2, 1);
--duration-fast: 0.15s; /* transition-colors default */
--duration-normal: 0.3s;
--duration-slow: 0.5s;
2. Layout Grid — the 4/12 column rule
Every <section> and the header use this container:
<div className="mx-auto grid grid-cols-4 lg:grid-cols-12 gap-x-4 lg:gap-x-6 px-4 lg:px-9">
- Mobile: 4 columns,
px-4(16px),gap-x-4. - Desktop (
lg≥ 1024px): 12 columns,px-9(36px),gap-x-6. - Never
containerormax-w-7xl. Horizontal centering comes frommx-auto+ the grid itself.
Column allocations (memorize these):
| Block | Mobile | Desktop |
|---|---|---|
| Hero left (copy + CLI box) | col-span-4 |
col-span-6 |
| Hero right (graphic) | hidden | col-span-6 |
| Product copy | col-span-4 |
col-span-5 |
| Product mockup | col-span-4 mt-8 |
col-span-7 mt-0 |
| CLI install box | max-width max-w-[480px] inside col |
same |
| CTA card | max-w-[520px] |
max-w-[520px] |
Vertical rhythm:
- Hero margin:
mt-4 lg:mt-20 mb-20 lg:mb-30. - Section padding:
py-20 lg:py-30. - Header padding:
py-5. - Footer outer:
mx-4 lg:mx-9 mb-4 lg:mb-9(the footer itself is a rounded card, not a full-bleed bar). - Stack gaps inside columns:
gap-y-6 lg:gap-y-8.
3. Typography Scale — exact sizes per role
Rule: always use text-[Npx] bracket values for UI chrome. Tailwind's text-sm / text-base are only acceptable in the <Text> / <Heading> CVA variants.
3.1 Hero headline (marketing display)
<h1
ref={heroTitleRef}
className="text-[40px] lg:text-[60px] 2xl:text-[72px]
font-mono font-normal leading-[100%] tracking-[-0.04em]"
>
<span className="text-[var(--color-accent-100)]">Next-Gen</span>
<span className="text-[var(--foreground)]"> Platform</span>
<br />
<span className="text-[var(--foreground)]">for Developers</span>
</h1>
Hero headline is font-mono, font-normal (not bold) with leading-[100%]. useCharReveal(heroTitleRef) splits it into char spans and fades them in on intersection.
3.2 Section headings (h2)
<h2 className="text-[32px] lg:text-[48px] font-normal leading-[100%]
tracking-[-0.04em] text-[var(--foreground)]">
Droids meet you<br />wherever you work.
</h2>
Section h2 uses default sans (not mono), font-normal, forced line breaks with <br />.
3.3 CTA headings
<h2 className="text-[28px] lg:text-[36px] font-normal leading-[110%]
tracking-[-0.02em]">
3.4 Subheadings / body (mono)
Paragraph body is font-mono text-[14px] lg:text-[16px] leading-[140%] tracking-[-0.0175rem] colored by role:
| Role | Class |
|---|---|
| Primary body | text-[var(--color-base-400)] |
| Muted body | text-[var(--color-base-500)] |
Always use <br /> to enforce specific line breaks in marketing copy — never let the browser wrap.
3.5 Labels (micro-UI)
className="font-mono text-[12px] uppercase tracking-[-0.015rem]
text-[var(--color-base-400)]
hover:text-[var(--foreground)]
transition-colors duration-200"
Use for: nav links, tab labels, section eyebrow labels, terminal window titles ("01 — TERMINAL / IDE" at text-[12px]).
3.6 Tab pills inside CLI box
className="px-2 py-0.5 rounded text-[11px] font-mono
uppercase tracking-[-0.01rem] transition-colors"
/* active */ "border border-[var(--color-base-800)] bg-[var(--color-base-1000)] text-[var(--foreground)]"
/* inactive */ "border border-transparent text-[var(--color-base-500)] hover:text-[var(--foreground)]"
3.7 Code / command strings
<code className="font-mono text-[14px] text-[var(--foreground)]">...</code>
Prefix with an accent caret: <span className="text-[var(--color-accent-100)] font-mono text-[14px]">></span>.
3.8 <Heading> and <Text> CVA variants (for reusable cases)
components/ui/heading.tsx exposes seven variants — use them when a reusable heading is appropriate:
| variant | sizes | weight |
|---|---|---|
display |
40 / 56 / 72 | semibold |
heading-1 |
30 / 36 / 48 | semibold |
heading-2 |
24 / 28 / 36 | semibold |
subheading-1 |
18 / 20 / 24 | medium |
subheading-2 |
16 / 18 / 20 | medium |
subheading-3 |
14 / 16 | medium |
metrics |
48 / 64 / 80 | bold |
color prop: "default" | "secondary" | "muted" | "accent".
<Text> variants: paragraph / paragraph-lg / paragraph-sm / label-1 / label-1-mono / label-2 / label-2-mono / caption / code.
Note the conflict: display and heading-1 in CVA use font-sans font-semibold. The hero in page.tsx overrides to font-mono font-normal because the marketing hero has its own look. When in doubt for the home hero: mono, normal, tracking -0.04em. For dashboard/product page headings: use the CVA display/heading-1 directly.
4. Components — exact shapes
4.1 <Button> (components/ui/button.tsx)
Built with cva + forwardRef + optional Radix Slot (asChild).
Base classes (always applied):
group relative inline-flex w-max cursor-pointer items-center justify-center
border font-mono uppercase transition-colors duration-150
disabled:cursor-not-allowed disabled:opacity-50
[&_*]:transition-colors [&_*]:duration-150
focus-visible:outline focus-visible:outline-offset-4
Variants (exact classes):
| variant | classes |
|---|---|
primary (default) |
bg-[var(--btn-primary-bg)] text-[var(--btn-primary-text)] border-[var(--color-base-700)] hover:opacity-80 overflow-hidden rounded-[4px] |
secondary |
bg-[var(--btn-secondary-bg)] text-[var(--btn-secondary-text)] border-transparent hover:opacity-80 overflow-hidden rounded-[4px] |
ghost |
bg-transparent text-[var(--foreground)] hover:bg-[var(--color-base-900)] hover:text-[var(--color-accent-100)] |
link |
bg-transparent text-[var(--foreground)] hover:text-[var(--color-accent-100)] underline-offset-4 hover:underline |
outline |
border-[var(--color-base-700)] text-[var(--foreground)] hover:border-[var(--color-accent-100)] hover:text-[var(--color-accent-100)] |
Sizes (exact heights):
| size | classes |
|---|---|
sm |
h-[25px] px-3 text-[12px] tracking-[-0.015rem] |
default |
h-[31px] px-[14px] text-[12px] tracking-[-0.015rem] |
lg |
h-[40px] px-6 text-[14px] tracking-[-0.0175rem] |
icon |
h-[31px] w-[31px] p-0 |
The stripe-hover overlay (required for primary/secondary):
Every primary/secondary button renders an absolute overlay that contains an animated 45° stripe pattern, visible only on hover / focus-visible. Copy this verbatim inside the button body:
{showPattern && isPrimaryOrSecondary && (
<div className="pointer-events-none absolute inset-0 opacity-0 will-change-transform
group-hover:opacity-100 group-focus-visible:opacity-100
transition-opacity duration-100 delay-75">
<div
className="btn-stripe-pattern absolute inset-0"
style={{ "--lines-color": linesColor } as React.CSSProperties}
/>
</div>
)}
<span className="relative z-10 flex items-center gap-1">{children}</span>
linesColor rule:
variant === "secondary"→var(--color-base-600)(darker lines on light bg)- else →
var(--color-base-500)
The .btn-stripe-pattern CSS lives in globals.css and must not be modified:
.btn-stripe-pattern {
background-image: repeating-linear-gradient(
45deg,
transparent 0px, transparent 2px,
var(--lines-color, var(--color-base-400)) 2px,
var(--lines-color, var(--color-base-400)) 3px,
transparent 3px, transparent 5px
);
background-size: 7.07px 7.07px;
animation: slidePattern 2s linear infinite;
}
@keyframes slidePattern {
0% { background-position: 0 0; }
100% { background-position: 28.28px -28.28px; }
}
Children inside a button that should animate (e.g. arrow icons) go after the text with gap-1 spacing: <Button>Learn More <ArrowIcon className="w-3 h-3" /></Button>.
4.2 <Badge> (components/ui/badge.tsx)
An all-caps mono label with either (a) an accent dot or (b) a numbered prefix. Accompanied by a scramble-text animation on the label.
<Badge text="PLATFORM" /> // dot + scrambled text
<Badge text="PRODUCT" variant="numbered" number={1} />
Base:
inline-flex items-center uppercase font-mono text-xs tracking-wider
Variants:
default:gap-3 text-[var(--color-base-500)]+ prefixed by<span class="w-2 h-2 rounded-full bg-[var(--color-accent-300)]">.numbered:gap-2 text-[var(--color-base-500)]+ prefixed by the number rendered intext-[var(--color-accent-300)] font-semibold.off: identical to default but typically used on inactive states (still dot-prefixed).
Use one Badge at the top of every major section (PLATFORM, PRODUCT, FOOTER, etc.). Always uppercase, ≤ 12 characters.
4.3 CLI Install Box (hero right of copy)
<div className="flex flex-col max-w-[480px] rounded-lg
border border-[var(--color-base-800)] bg-[var(--background)]
overflow-hidden">
{/* tabs row */}
<div className="flex gap-1 px-3 py-2">
<button className={tabClass(active)}>macOS / Linux</button>
<button className={tabClass(active)}>Windows</button>
</div>
<div className="border-t border-[var(--color-base-800)]" />
{/* command row */}
<div className="flex items-center justify-between px-4 py-3 gap-4">
<div className="flex items-center gap-2 min-w-0">
<span className="text-[var(--color-accent-100)] font-mono text-[14px]">></span>
<code className="font-mono text-[14px] text-[var(--foreground)]">
{displayedText}
{isTyping && (
<span className="inline-block w-[8px] h-[16px] bg-[var(--foreground)]
ml-[1px] align-middle translate-y-[1px]" />
)}
</code>
</div>
<button
className="flex-shrink-0 p-1.5 rounded border border-transparent
hover:border-[var(--color-base-700)] transition-all">
{copied ? <CheckIcon className="w-4 h-4 text-[var(--color-accent-100)]" />
: <CopyIcon className="w-4 h-4 text-[var(--color-base-500)]" />}
</button>
</div>
</div>
Typewriter interval is exactly 25 ms / char (see §6.3). Reset + retype on tab switch. Clipboard copy → swap to accent check icon for 2000 ms, then revert.
4.4 Terminal Mockup (product section)
Mac-style traffic-light dots with a text title on the right:
<div className="rounded-lg border border-[var(--color-base-800)] bg-[var(--color-base-900)] overflow-hidden">
<div className="flex items-center justify-between px-4 py-3 border-b border-[var(--color-base-800)]">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-[var(--color-base-600)]" />
<div className="w-3 h-3 rounded-full bg-[var(--color-base-600)]" />
<div className="w-3 h-3 rounded-full bg-[var(--color-base-600)]" />
</div>
<span className="font-mono text-[12px] text-[var(--color-base-500)]">
01 — TERMINAL / IDE
</span>
</div>
<div className="p-6 min-h-[300px]">{/* content */}</div>
</div>
Traffic-light dots are monochromatic grey (never red/yellow/green — that's macOS, not Factory). Title format: NN — CATEGORY.
4.5 Navigation Dropdown ("Product" menu)
Two-column grid of icon-tiles inside a rounded-xl dashed-border panel:
<div className="w-[520px] p-3 rounded-xl border border-dashed
border-[var(--color-base-700)] bg-[var(--background)] shadow-2xl">
<div className="grid grid-cols-2 gap-1">
{/* each tile: */}
<a className="group relative flex items-start gap-3 p-2.5 rounded-lg
border border-dashed border-transparent
hover:border-[var(--color-base-700)]
transition-colors overflow-hidden">
{/* stripe overlay on hover */}
<div className="pointer-events-none absolute inset-0 opacity-0
group-hover:opacity-30 transition-opacity duration-100">
<div className="btn-stripe-pattern absolute inset-0"
style={{ "--lines-color": "var(--color-base-600)" } as React.CSSProperties} />
</div>
{/* icon square */}
<div className="relative z-10 w-11 h-11 flex-shrink-0 rounded-lg
border border-[var(--color-base-700)] bg-[var(--background)]
flex items-center justify-center text-[var(--color-base-500)]">
<TerminalIcon className="w-[18px] h-[18px]" />
</div>
{/* labels */}
<div className="relative z-10">
<div className="font-mono text-[13px] text-[var(--foreground)]">Terminal / IDE</div>
<div className="font-mono text-[11px] text-[var(--color-base-500)]">Code where you work</div>
</div>
</a>
</div>
</div>
Wrapper that toggles visibility:
className={`absolute top-full left-1/2 -translate-x-1/2 pt-3 z-50
transition-all duration-200 origin-top ${
open ? "opacity-100 scale-100 translate-y-0"
: "opacity-0 scale-95 -translate-y-2 pointer-events-none"
}`}
4.6 CTA Card
<div className="rounded-lg border border-[var(--color-base-800)]
bg-[var(--color-base-900)] p-8 lg:p-12 max-w-[520px]">
<LogoMark className="w-12 h-12 text-[var(--color-base-400)] mb-6" />
<h2 className="text-[28px] lg:text-[36px] font-normal leading-[110%]
tracking-[-0.02em] text-[var(--foreground)] mb-6">
Ready to build the<br />software of the future?
</h2>
<Button size="sm">Start Building <ArrowIcon className="w-3 h-3" /></Button>
</div>
4.7 Footer (inset rounded card, not full-bleed)
<footer className="mx-4 lg:mx-9 mb-4 lg:mb-9 rounded-lg
border border-[var(--color-base-800)] bg-[var(--color-base-900)]
p-8 lg:p-12 min-h-[360px] flex flex-col">
Footer layout (top → bottom):
<Badge text="FOOTER" />- Link grid (2-3 columns) with section titles
text-[var(--foreground)] font-normal text-[14px] mb-4, itemsfont-mono text-[14px] text-[var(--color-base-500)] hover:text-[var(--foreground)] transition-colors. - Theme toggle (right-aligned pill).
- Bottom bar:
border-t border-[var(--color-base-800)] pt-8withLogoMarkleft, comma-separated social links center, copyright right — allfont-mono text-[14px].
4.8 Theme Toggle
<div className="flex items-center gap-1 rounded-full
border border-[var(--color-base-700)] p-1">
{/* three circular buttons */}
<button className={`p-2 rounded-full transition-colors ${
theme === mode
? "bg-[var(--color-base-800)] text-[var(--foreground)]"
: "text-[var(--color-base-500)] hover:text-[var(--foreground)]"
}`}>
<MoonIcon className="w-4 h-4" />
</button>
{/* SunIcon, MonitorIcon similarly */}
</div>
The system button pairs the monitor icon with a font-mono text-[10px] uppercase "System" label.
Theme switching logic (verbatim):
useEffect(() => {
const root = document.documentElement;
root.classList.add("disable-transitions");
if (theme === "system") {
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
root.setAttribute("data-theme", prefersDark ? "dark" : "light");
} else {
root.setAttribute("data-theme", theme);
}
localStorage.setItem("theme", theme);
setTimeout(() => root.classList.remove("disable-transitions"), 100);
}, [theme]);
The .disable-transitions class freezes all transitions during the swap to avoid 300ms color animations on every token.
4.9 Hover Underline (nav links)
Apply hover-underline to nav links for the sliding-underline effect. Do not use Tailwind's hover:underline.
.hover-underline { position: relative; }
.hover-underline::after {
content: ''; position: absolute; bottom: -1px; left: 0;
width: 0; height: 1px; background-color: currentColor;
transition: width 0.3s ease-in-out;
}
.hover-underline:hover::after { width: 100%; }
4.10 Logo & LogoMark
Logo is word-based (ACME in Geist Sans, fontWeight 600, letter-spacing -0.02em) inside an SVG. LogoMark is a rounded square with a plus sign. Both accept className and use currentColor — drive color through parent text-[var(--color-base-XXX)].
5. Patterns & Surfaces
5.1 Dashed vs solid borders
- Dashed
border-dashed border-[var(--color-base-700)]→ interactive containers (dropdown panel, hoverable tiles, diagonal accent areas). - Solid
border-[var(--color-base-800)]→ structural surfaces (cards, CTA, footer, terminal chrome, CLI box, tab dividers).
Never mix: a single container uses one or the other.
5.2 Stripe pattern — three uses
- Animated
.btn-stripe-pattern→ buttons + nav tiles (hover-revealed overlay). - Static
.btn-pattern(currentColorlines, no animation) → decorative backgrounds when the parent color should drive the lines. - Inline SVG URL for entire section backgrounds:
<section className="relative overflow-hidden bg-[#fafafa]">
<div
className="absolute inset-0 opacity-[0.11]"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg width='7' height='7' viewBox='0 0 7 7' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0L7 7' stroke='%23888' stroke-width='1.2' fill='none'/%3E%3C/svg%3E")`,
backgroundSize: '7px 7px'
}}
/>
<div className="relative ...grid...">{/* content */}</div>
</section>
The product section inverts to a near-white (#fafafa) surface with 11%-opacity diagonal lines. Always wrap content in a relative child so it sits above the absolute inset-0 pattern.
5.3 Marquee (brand strip)
animate-marquee (30s linear infinite, translateX(0) → translateX(-50%)). To loop seamlessly, render the brand list twice inside an overflow-hidden flex row, then apply animate-marquee. .animate-marquee-reverse flips direction.
5.4 Scrollbar
Custom 8px webkit scrollbar: track var(--color-base-900), thumb var(--color-base-600) (hover -500). Do not restyle.
6. Motion & Animation — signatures
All motion respects prefers-reduced-motion via the rule at the bottom of globals.css which forces animation-duration: 0.01ms and transition-duration: 0.01ms.
6.1 Character Reveal — hero headline
Use the useCharReveal(heroRef) hook. It:
- Splits text into
<span class="char">spans viasplitTextIntoChars. - Sets
opacity:0, y:initialY, color:initialColoron all chars. - On
IntersectionObserverhit, GSAPs them toopacity:1, y:0, color:finalColorwith stagger +ease-reveal(cubic-bezier(0.645, 0.045, 0.355, 1)).
Defaults live in ANIMATION_CONFIG.charReveal. Only the hero <h1> gets this. Section h2s stay static.
6.2 Scramble Text — badges
useScrambleText(textRef, iconRef, { text, triggerOnScroll: true, infinite: true }). Characters cycle through XO01 before resolving. Duration 800ms, reveal begins at 50% progress. Every <Badge> uses this — if you want it static, pass animated={false}.
6.3 Typewriter — CLI command
Interval 25ms/char. Cursor is an 8×16px filled <span> block, visible only while isTyping. Triggered on tab change by clearing state and restarting the interval. Keep the Unix command exactly: curl -fsSL https://app.example.com/cli | sh. Windows: irm https://app.example.com/cli/windows | iex.
6.4 Blink — static cursor (alternative)
.animate-blink { animation: blink 1s step-end infinite; }
.typewriter-cursor {
display: inline-block; width: 2px; height: 1em;
background-color: currentColor; margin-left: 2px;
animation: blink 1.06s step-end infinite;
}
6.5 Scale / Slide (menus, modals, nav content)
All 250ms eased. Use the ready-made utilities:
.animate-scale-in/.animate-scale-out.animate-enter-from-left/-right,.animate-exit-to-left/-right.animate-accordion-down/-up(bind to--radix-accordion-content-height).animate-fade-in-up(600ms,--ease-reveal)
Radix NavigationMenu content auto-binds via .nav-menu-content[data-state|data-motion] selectors in globals.css.
6.6 Transitions (the default)
For all non-signature motion: transition-colors duration-150 or transition-all duration-200. Never duration-500 for hovers.
6.7 GPU / perf helpers
.gpu-accelerated→will-change: transform; transform: translateZ(0); backface-visibility: hidden;— apply to elements under GSAP control..invisible-animated→ hides pre-animated elements without layout shift (for GSAPautoAlpha)..paused/.running→ controlanimation-play-state.
7. Iconography
Style: stroke-based <svg viewBox="0 0 24 24">, fill="none", stroke="currentColor", strokeWidth="2", strokeLinecap="round", strokeLinejoin="round". Never fill icons. Size with w-3 h-3 (button), w-4 h-4 (theme toggle, copy), w-[18px] h-[18px] (nav tile icons), w-12 h-12 (section LogoMarks), w-16 h-16 (empty-state LogoMarks).
Icons are inline React components in the page, not imported from lucide-react — keep them inline, named exactly: ChevronIcon, ArrowIcon, CopyIcon, CheckIcon, MoonIcon, SunIcon, MonitorIcon, TerminalIcon, SlackIcon, WebIcon, ProjectIcon, CLIIcon. If a new icon is needed, follow the same 24×24 stroke-2 pattern.
8. File Structure
When scaffolding or extending a Factory-style project, mirror this layout:
app/
globals.css ← tokens, keyframes, utility classes (authoritative)
layout.tsx ← Geist fonts, data-theme="dark"
page.tsx ← landing page with inline icons
{route}/page.tsx
components/
ui/
button.tsx badge.tsx heading.tsx text.tsx index.ts
shared/
logo.tsx theme-toggle.tsx
home/
animated-chart.tsx brands-marquee.tsx
hooks/
useCharReveal.ts useScrambleText.ts useReveal.ts
useReducedMotion.ts useTheme.ts index.ts
lib/
utils.ts ← cn(), splitTextIntoChars, scrambleText, typewriterText, debounce, throttle
gsap.ts ← ANIMATION_CONFIG, revealEase, registered plugins
Tooling:
- Next.js App Router (≥ 16), React 19, TypeScript strict.
- Tailwind CSS 4 via
@tailwindcss/postcssand@import "tailwindcss"(notailwind.config.js; the@theme inlineblock inglobals.csswires tokens). - class-variance-authority for every reusable component.
- clsx + tailwind-merge behind
cn(). - @radix-ui/react-slot for
asChild, plus@radix-ui/react-accordion/-navigation-menu/-select. - gsap (core only — paid plugins are replaced by
splitTextIntoChars+scrambleTextinlib/utils.ts). - geist package +
next/font/google.
cn utility (copy verbatim)
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }
HTML root attributes
<html lang="en" data-theme="dark" suppressHydrationWarning> — always.
9. Authoring Rules (enforcement)
When generating or editing UI in a Factory-style codebase, do:
- Reference colors via
var(--token)— never#xxxxxxoroklch(...)inline except insideglobals.css. - Use
font-monoby default; only drop to sans for display/section headings. - Prefer pixel-exact
text-[Npx]values from §3. - Set
tracking-[-0.015rem]on small mono labels,tracking-[-0.0175rem]on body,tracking-[-0.04em]on display headlines. - Wrap every section in the 4/12 grid.
- Place one
<Badge>at the top of every major section. - Use dashed borders for interactive containers, solid for structural.
- Use the
btn-stripe-patternoverlay on any hoverable surface that's "primary" in feel. - Always provide dark and light via token inversion — don't ship single-theme UI.
- Always guard motion with
useReducedMotion/ the global media query. - Keep icons inline, 24×24, stroke-2,
currentColor. - On route changes or tab swaps that re-trigger typewriters: reset state then restart intervals (don't just mutate text).
Do not:
- Introduce
bg-gray-900,text-slate-500, arbitrary shadcn tokens, ordark:variants — the theme comes fromdata-theme+ CSS vars, not Tailwind'sdarkclass. - Use
rounded-fullon buttons orrounded-2xlon cards — the system is 4px button / 8px card / 12px panel. - Add drop shadows (except
shadow-2xlon the product-menu dropdown). The look is flat with borders, not elevated. - Import
lucide-react/heroicons. Icons are handcrafted inline SVGs. - Use Tailwind's
containerclass ormax-w-7xl. Grid +mx-autoonly. - Use emoji in UI copy.
- Use bold weights on marketing headlines — they are font-normal. Bold is reserved for the
metricsCVA variant. - Animate anything longer than 300ms except the signature reveals.
- Use red, green, yellow, blue, or purple for UI state. Success/confirm is
--color-accent-100(orange check). Error states, if needed, desaturate to--color-base-400or use a new token you add to:root.
10. Quick-Reference Cheat Sheet
BG page: bg-[var(--background)]
Text primary: text-[var(--foreground)]
Text body: text-[var(--color-base-400)]
Text muted: text-[var(--color-base-500)]
Accent: text-[var(--color-accent-100)]
Card bg: bg-[var(--color-base-900)]
Deep fill: bg-[var(--color-base-1000)]
Solid border: border-[var(--color-base-800)]
Dashed border: border-dashed border-[var(--color-base-700)]
Hero h1: text-[40px] lg:text-[60px] 2xl:text-[72px] font-mono font-normal leading-[100%] tracking-[-0.04em]
Section h2:text-[32px] lg:text-[48px] font-normal leading-[100%] tracking-[-0.04em]
CTA h2: text-[28px] lg:text-[36px] font-normal leading-[110%] tracking-[-0.02em]
Body: font-mono text-[14px] lg:text-[16px] leading-[140%] tracking-[-0.0175rem]
Label: font-mono text-[12px] uppercase tracking-[-0.015rem]
Tab pill: px-2 py-0.5 rounded text-[11px] font-mono uppercase tracking-[-0.01rem]
Button sm: h-[25px] px-3 text-[12px] rounded-[4px]
Button md: h-[31px] px-[14px] text-[12px] rounded-[4px]
Button lg: h-[40px] px-6 text-[14px] rounded-[4px]
Card radius: rounded-lg (8px)
Panel radius:rounded-xl (12px)
Icon stroke: stroke="currentColor" strokeWidth="2" fill="none"
Transition: transition-colors duration-150 (hovers)
transition-all duration-200 (menus)
When in doubt, open app/globals.css, app/page.tsx, components/ui/button.tsx, and components/ui/badge.tsx — those four files are the design law. Copy their patterns verbatim before inventing new ones.