factory-design/SKILL.md
Apply the Factory.ai visual design system to any UI. Use whenever the user asks for Factory-style, "factory.ai look", agent-native developer tooling aesthetics, or is building landing pages, dashboards, marketing sections, CLI install boxes, terminal mockups, or developer-tool UIs that should feel like Factory.ai / Vercel / Linear-adjacent dev brands. Triggers on phrases like "factory design", "factory style", "droid look", "make it look like factory.ai", or any task in a project using the color tokens `--color-accent-100`, `--color-base-*`, Geist Mono + orange `#ef6f2e`. Produces exact class strings, CSS variables, component shapes, motion, and enforcement rules — never guesses values.
npx skillsauth add sssemil/skills factory-designInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
Factory's look is "agent-native developer tooling". Five non-negotiable rules define the vibe:
Geist Mono is the default typeface for ~90% of text (nav, labels, body paragraphs, buttons, badges, code). Geist Sans appears only for display/marketing headings and section titles — never for UI chrome.var(--color-...). Light theme is the mirror, not a rewrite.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.border-dashed border-[var(--color-base-700)]. Buttons and hoverable cards reveal a 45° conveyor-belt stripe pattern on hover.duration-150 / duration-200 color 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.
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).
--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:
<span class="text-[var(--color-accent-100)]">Next-Gen</span> Platform.w-2 h-2 rounded-full bg-[var(--color-accent-100)]) precedes "Trusted by" / section intros.::selection is always accent-100 on dark-primary.text-[var(--color-accent-100)] via transition-colors duration-200.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;
--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.
--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;
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">
px-4 (16px), gap-x-4.lg ≥ 1024px): 12 columns, px-9 (36px), gap-x-6.container or max-w-7xl. Horizontal centering comes from mx-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:
mt-4 lg:mt-20 mb-20 lg:mb-30.py-20 lg:py-30.py-5.mx-4 lg:mx-9 mb-4 lg:mb-9 (the footer itself is a rounded card, not a full-bleed bar).gap-y-6 lg:gap-y-8.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.
<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.
<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 />.
<h2 className="text-[28px] lg:text-[36px] font-normal leading-[110%]
tracking-[-0.02em]">
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.
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]).
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)]"
<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>.
<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.
<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)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>.
<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 in text-[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.
<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 onClick={handleCopy}
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.
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.
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"
}`}
<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>
<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" />text-[var(--foreground)] font-normal text-[14px] mb-4, items font-mono text-[14px] text-[var(--color-base-500)] hover:text-[var(--foreground)] transition-colors.border-t border-[var(--color-base-800)] pt-8 with LogoMark left, comma-separated social links center, copyright right — all font-mono text-[14px].<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.
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%; }
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)].
border-dashed border-[var(--color-base-700)] → interactive containers (dropdown panel, hoverable tiles, diagonal accent areas).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.
.btn-stripe-pattern → buttons + nav tiles (hover-revealed overlay)..btn-pattern (currentColor lines, no animation) → decorative backgrounds when the parent color should drive the lines.<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.
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.
Custom 8px webkit scrollbar: track var(--color-base-900), thumb var(--color-base-600) (hover -500). Do not restyle.
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.
Use the useCharReveal(heroRef) hook. It:
<span class="char"> spans via splitTextIntoChars.opacity:0, y:initialY, color:initialColor on all chars.IntersectionObserver hit, GSAPs them to opacity:1, y:0, color:finalColor with 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.
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}.
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.
.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;
}
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.
For all non-signature motion: transition-colors duration-150 or transition-all duration-200. Never duration-500 for hovers.
.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 GSAP autoAlpha)..paused / .running → control animation-play-state.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.
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:
@tailwindcss/postcss and @import "tailwindcss" (no tailwind.config.js; the @theme inline block in globals.css wires tokens).cn().asChild, plus @radix-ui/react-accordion / -navigation-menu / -select.splitTextIntoChars + scrambleText in lib/utils.ts).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 lang="en" data-theme="dark" suppressHydrationWarning> — always.
When generating or editing UI in a Factory-style codebase, do:
var(--token) — never #xxxxxx or oklch(...) inline except inside globals.css.font-mono by default; only drop to sans for display/section headings.text-[Npx] values from §3.tracking-[-0.015rem] on small mono labels, tracking-[-0.0175rem] on body, tracking-[-0.04em] on display headlines.<Badge> at the top of every major section.btn-stripe-pattern overlay on any hoverable surface that's "primary" in feel.useReducedMotion / the global media query.currentColor.Do not:
bg-gray-900, text-slate-500, arbitrary shadcn tokens, or dark: variants — the theme comes from data-theme + CSS vars, not Tailwind's dark class.rounded-full on buttons or rounded-2xl on cards — the system is 4px button / 8px card / 12px panel.shadow-2xl on the product-menu dropdown). The look is flat with borders, not elevated.lucide-react / heroicons. Icons are handcrafted inline SVGs.container class or max-w-7xl. Grid + mx-auto only.metrics CVA variant.--color-accent-100 (orange check). Error states, if needed, desaturate to --color-base-400 or use a new token you add to :root.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.
tools
Autonomous Linear task worker that selects Linear issues, implements them with TDD, self-reviews, commits, pushes, and moves finished work to In Review.
tools
Systematically reviews a project subsystem-by-subsystem with resumable .brutal-workspace state and creates Linear review finding issues for CRITICAL and MAJOR problems.
development
Collaborative, multi-perspective feature planning with rigorous requirements interrogation. Creates Linear project documents and Linear issues instead of local workspace plan/task files.
documentation
Compact the current conversation into a handoff document for another agent to pick up.