Luna Landing
Style direction: Reference landing study
$ npx cactus add luna
Copied!
Aa
Inter DisplayBody
Aa
DM SansDisplay
Aa
DM Serif TextDisplay Italic
EarlyBird Launch Recreation Spec
Use this as the exact implementation source of truth.
Updated Behavior Notes
- Do not render any fixed bottom-right maker badge/button.
- FAQ must be interactive accordion behavior (single item open at a time, click to toggle, animated reveal).
1) Design Values Vars
File: index.css
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,500;0,600;1,400&family=DM+Serif+Text:ital@1&family=Inter:wght@400;500;600;700&display=swap');
@import "tailwindcss";
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--background: #f9f9f9;
--foreground: #1c1c1c;
--card: #fcfcfc;
--card-foreground: #1c1c1c;
--popover: #ffffff;
--popover-foreground: #1c1c1c;
--primary: #1c1c1c;
--primary-foreground: #ededed;
--secondary: #f5f5f5;
--secondary-foreground: #1c1c1c;
--muted: #f5f5f5;
--muted-foreground: #575757;
--accent: #545454;
--accent-foreground: #f9f9f9;
--destructive: #dc2626;
--destructive-foreground: #ffffff;
--border: #ededed;
--input: #ededed;
--ring: #545454;
--chart-1: #1c1c1c;
--chart-2: #00c220;
--chart-3: #575757;
--chart-4: #787878;
--chart-5: #ededed;
--radius: 24px;
--sidebar: #fcfcfc;
--sidebar-foreground: #1c1c1c;
--sidebar-primary: #1c1c1c;
--sidebar-primary-foreground: #ededed;
--sidebar-accent: #f5f5f5;
--sidebar-accent-foreground: #1c1c1c;
--sidebar-border: #ededed;
--sidebar-ring: #545454;
--shadow-dark: inset 0px 0px 20px 1.64px rgb(255 255 255 / 0.15), 0px 0.8398px 0.5039px -0.3125px rgb(0 0 0 / 0.13), 0px 1.9905px 1.1943px -0.625px rgb(0 0 0 / 0.13), 0px 3.6308px 2.1785px -0.9375px rgb(0 0 0 / 0.13), 0px 6.0363px 3.6218px -1.25px rgb(0 0 0 / 0.13), 0px 9.7481px 5.8488px -1.5625px rgb(0 0 0 / 0.13), 0px 15.9566px 9.574px -1.875px rgb(0 0 0 / 0.13), 0px 27.4762px 16.4857px -2.1875px rgb(0 0 0 / 0.13), 0px 50px 30px -2.5px rgb(0 0 0 / 0.13);
--shadow-avatar: 0px 0.8398px 0.5039px -0.3125px rgb(77 77 77 / 0.13), 0px 1.9905px 1.1943px -0.625px rgb(77 77 77 / 0.13), 0px 3.6308px 2.1785px -0.9375px rgb(77 77 77 / 0.13), 0px 6.0363px 3.6218px -1.25px rgb(77 77 77 / 0.13), 0px 9.7481px 5.8488px -1.5625px rgb(77 77 77 / 0.13), 0px 15.9566px 9.574px -1.875px rgb(77 77 77 / 0.13), 0px 27.4762px 16.4857px -2.1875px rgb(77 77 77 / 0.13), 0px 50px 30px -2.5px rgb(77 77 77 / 0.13);
--shadow-badge: 0 1px 2px rgb(0 0 0 / 0.1);
font-family: 'Inter', sans-serif;
color: var(--foreground);
background: var(--background);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--background);
color: var(--foreground);
}
2) Shared Components
File: components/common.tsx
import type { ReactNode } from 'react'
export const darkShadow =
'var(--shadow-dark)'
export const avatarShadow =
'var(--shadow-avatar)'
type RoundIconProps = {
children: ReactNode
size?: number
className?: string
}
export function RoundIcon({ children, size = 38, className = '' }: RoundIconProps) {
return (
<div
className={`grid place-items-center rounded-full bg-primary text-primary-foreground ${className}`}
style={{ width: size, height: size, boxShadow: darkShadow }}
>
{children}
</div>
)
}
export function ArrowUpRightIcon({ className = '' }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M7 17L17 7M9 7H17V15"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
export function SparkIcon({ className = '' }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M12 3L14.4 8.6L20 11L14.4 13.4L12 19L9.6 13.4L4 11L9.6 8.6L12 3Z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinejoin="round"
/>
</svg>
)
}
export function InsightIcon({ className = '' }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M4 18H20M7 15V11M12 15V7M17 15V9"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
export function TeamIcon({ className = '' }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M8.3 12A3.3 3.3 0 108.3 5.4A3.3 3.3 0 008.3 12ZM15.8 10.6A2.6 2.6 0 1015.8 5.4A2.6 2.6 0 0015.8 10.6ZM2.7 18.5C3.4 15.9 5.6 14.2 8.3 14.2C10.9 14.2 13.2 15.9 13.9 18.5M13.7 18.5C14.2 16.8 15.8 15.6 17.6 15.6C19.5 15.6 21.1 16.8 21.6 18.5"
stroke="currentColor"
strokeWidth="1.4"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
export function PlusIcon({ className = '' }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M12 7V17M7 12H17"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
3) Page Composition
File: App.tsx
import { BackgroundSection } from './sections/BackgroundSection'
import { FaqSection } from './sections/FaqSection'
import { FeaturesSection } from './sections/FeaturesSection'
import { FooterSection } from './sections/FooterSection'
import { HeroSection } from './sections/HeroSection'
import { MissionSection } from './sections/MissionSection'
function App() {
return (
<div className="relative min-h-screen overflow-x-hidden bg-background text-foreground">
<BackgroundSection />
<div className="relative z-10 mx-auto flex w-full max-w-[1200px] flex-col items-center max-[809px]:pb-[3px]">
<main className="flex w-full flex-col items-center min-[810px]:origin-top min-[810px]:scale-[0.96] min-[810px]:translate-y-0 max-[809px]:origin-top max-[809px]:scale-[0.98] max-[809px]:translate-y-0">
<HeroSection />
<FeaturesSection />
<MissionSection />
<FaqSection />
</main>
<FooterSection />
</div>
</div>
)
}
export default App
4) Background Section
File: sections/BackgroundSection.tsx
export function BackgroundSection() {
return (
<figure className="pointer-events-none fixed left-0 top-0 z-0 h-[700px] w-full">
<img
srcSet="/media/cloud-512.png 512w, /media/cloud-1024.png 1024w"
src="/media/cloud-512.png"
sizes="100vw"
alt=""
className="h-full w-full object-cover object-center"
/>
</figure>
)
}
5) Hero Section
File: sections/HeroSection.tsx
import { avatarShadow, darkShadow } from '../components/common'
const avatars = [
'/media/avatar-1.png',
'/media/avatar-2.png',
'/media/avatar-3.png',
]
export function HeroSection() {
return (
<section className="flex w-full max-w-[800px] flex-col items-center px-0 pb-16 pt-[100px] max-[809px]:px-5 max-[809px]:pb-12">
<div className="flex w-full flex-col items-center gap-8">
<div className="grid h-[55px] w-[55px] place-items-center rounded-full bg-primary" style={{ boxShadow: darkShadow }}>
<img
src="/media/brand-logo.svg"
alt="EarlyBird logo"
className="h-[55px] w-[55px]"
/>
</div>
<div className="flex w-full flex-col items-center gap-12">
<div className="flex w-full flex-col items-center gap-3">
<div className="flex items-center gap-2 rounded-full bg-secondary px-3 py-1 text-[14px] font-medium text-secondary-foreground">
<span className="relative block h-[10px] w-[10px] rounded-full bg-chart-2">
<span className="absolute inset-0 animate-ping rounded-full bg-chart-2/35" />
</span>
Beta goes live soon
</div>
<h1 className="max-w-[450px] bg-gradient-to-r from-foreground to-accent bg-clip-text text-center font-['DM_Sans'] text-[48px] font-semibold leading-[1.2] tracking-[-0.06em] text-transparent max-[809px]:max-w-[350px] max-[809px]:text-[38px]">
Early Access to Future of AI{' '}
<span className="font-['DM_Serif_Text'] text-[1.04em] font-normal italic tracking-normal text-accent">
Growth
</span>
</h1>
<p className="max-w-[400px] text-center text-[16px] leading-[1.5] text-muted-foreground max-[809px]:text-[14px]">
Automate tasks, unlock insights, and scale your team's productivity — all in one place
</p>
</div>
<form className="w-full max-w-[450px]" onSubmit={(event) => event.preventDefault()}>
<div className="relative">
<input
type="email"
placeholder="Your Email"
className="h-16 w-full rounded-full border border-input bg-background px-5 pr-[170px] text-[16px] text-foreground outline-none placeholder:text-muted-foreground"
/>
<button
type="submit"
className="absolute bottom-2 right-2 rounded-full bg-primary px-6 py-3 text-[16px] font-semibold text-primary-foreground"
style={{ boxShadow: darkShadow }}
>
Join Waitlist
</button>
</div>
</form>
<div className="flex items-center gap-2 text-[16px] leading-[1.5] text-foreground max-[809px]:flex-wrap max-[809px]:justify-center">
<div className="relative h-7 w-[65px]">
{avatars.map((avatar, index) => (
<img
key={avatar}
src={avatar}
alt="Founder avatar"
className="absolute top-0 h-7 w-7 rounded-full border border-background object-cover"
style={{
left: index * 18,
boxShadow: avatarShadow,
}}
/>
))}
</div>
<span>Join</span>
<span>8,258</span>
<span>+</span>
<span>SaaS & AI founders</span>
</div>
</div>
</div>
</section>
)
}
6) Features Section
File: sections/FeaturesSection.tsx
import { InsightIcon, RoundIcon, SparkIcon, TeamIcon } from '../components/common'
const features = [
{
title: 'Smart Automation',
description: 'Automate everyday work fast',
icon: <SparkIcon className="h-[18px] w-[18px]" />,
},
{
title: 'AI Insights',
description: 'Unlock data-driven sharp clarity',
icon: <InsightIcon className="h-[18px] w-[18px]" />,
},
{
title: 'Team Collaboration',
description: 'Boost smart workflows with AI',
icon: <TeamIcon className="h-[18px] w-[18px]" />,
},
]
export function FeaturesSection() {
return (
<section className="flex w-full flex-row items-center justify-center gap-4 pt-8 max-[809px]:flex-col max-[809px]:gap-[42px]">
{features.map((feature) => (
<article key={feature.title} className="relative w-full max-w-[316px]">
<div className="absolute left-1/2 top-[-26px] -translate-x-1/2">
<RoundIcon size={42}>{feature.icon}</RoundIcon>
</div>
<div className="flex flex-col items-center gap-2 rounded-[24px] bg-card px-8 py-8 text-center">
<h3 className="font-['DM_Sans'] text-[18px] font-medium leading-[1.5] tracking-[-0.03em] text-card-foreground">
{feature.title}
</h3>
<p className="max-w-[150px] text-[14px] leading-[1.5] text-muted-foreground">{feature.description}</p>
</div>
</article>
))}
</section>
)
}
7) Mission Section
File: sections/MissionSection.tsx
import { ArrowUpRightIcon, RoundIcon } from '../components/common'
const missionStats = [
{ label: 'Launch Date:', value: 'November 2025' },
{ label: 'Key Benefit:', value: 'Save 10+ hours weekly' },
{ label: 'Built For:', value: 'SaaS & AI founders' },
]
export function MissionSection() {
return (
<section className="flex w-full max-w-[800px] flex-col items-center px-0 py-16 max-[809px]:px-5 max-[809px]:py-12">
<div className="w-full max-w-[550px] rounded-[24px] bg-card p-2">
<article className="relative rounded-[16px] bg-background px-[42px] py-[42px] max-[809px]:px-8 max-[809px]:py-8">
<div className="inline-flex rounded-full bg-secondary px-3 py-1.5 text-[14px] text-secondary-foreground">Mission</div>
<div className="absolute right-9 top-[-26px] max-[809px]:right-6">
<RoundIcon size={60}>
<ArrowUpRightIcon className="h-6 w-6" />
</RoundIcon>
</div>
<div className="mt-6 flex flex-col gap-6">
<h2 className="font-['DM_Sans'] text-[29px] font-medium leading-[1.2] tracking-[-0.05em] text-foreground">
The New Era of AI-Powered SaaS
</h2>
<div className="space-y-4 text-[16px] leading-[1.5] text-muted-foreground">
<p>
Our platform puts AI at the center of your workflow — helping teams automate repetitive tasks,
generate instant insights, and collaborate smarter.
</p>
<p>With faster decisions and seamless integration, you scale your SaaS product without limits.</p>
</div>
<dl className="flex flex-col gap-1">
{missionStats.map((item) => (
<div key={item.label} className="flex items-center gap-1.5 text-[16px] leading-[1.5]">
<dt className="font-semibold text-foreground">{item.label}</dt>
<dd className="text-muted-foreground">{item.value}</dd>
</div>
))}
</dl>
</div>
<div className="mt-8 flex items-center gap-3">
<img
src="/media/founder.png"
alt="Daniel Hayes"
className="h-[38px] w-[38px] rounded-full object-cover"
/>
<div className="min-w-0 flex-1">
<p className="text-[14px] font-medium leading-[1.5] tracking-[-0.02em] text-foreground">Daniel Hayes</p>
<p className="text-[12px] leading-[1.5] text-muted-foreground">Founder of EarlyBird</p>
</div>
</div>
</article>
</div>
</section>
)
}
8) FAQ Section
File: sections/FaqSection.tsx
import { useState } from 'react'
import { PlusIcon, RoundIcon } from '../components/common'
type FaqItemType = {
id: string
question: string
answer: string
}
const leftColumn: FaqItemType[] = [
{
id: 'left-0',
question: 'What's included in the beta?',
answer: 'You'll get access to the full platform, upcoming features, and priority support during the beta phase.',
},
{
id: 'left-1',
question: 'Do I need tech skills to use it?',
answer: 'No. EarlyBird is designed for SaaS and AI teams of all sizes — simple, intuitive, and ready to go.',
},
{
id: 'left-2',
question: 'When is the official launch?',
answer: 'We're aiming to launch in November 2025. Early access users will be the first to know.',
},
]
const rightColumn: FaqItemType[] = [
{
id: 'right-0',
question: 'Can I cancel anytime?',
answer: 'Yes, you can leave the beta program or unsubscribe from updates at any time.',
},
{
id: 'right-1',
question: 'How much does it cost?',
answer: 'The beta is free. Paid plans will be announced when we officially launch.',
},
{
id: 'right-2',
question: 'How secure is my data?',
answer: 'We take security seriously. All data is encrypted, stored safely, and never shared with third parties.',
},
]
function FaqItem({
id,
question,
answer,
isOpen,
onToggle,
}: FaqItemType & { isOpen: boolean; onToggle: () => void }) {
const buttonId = `faq-button-${id}`
const panelId = `faq-panel-${id}`
return (
<article className="w-full rounded-[24px] bg-background p-6">
<button
id={buttonId}
type="button"
aria-expanded={isOpen}
aria-controls={panelId}
onClick={onToggle}
className="flex w-full items-start gap-4 text-left"
>
<h3 className="flex-1 text-left text-[18px] font-medium leading-[1.4] tracking-[-0.02em] text-foreground">
{question}
</h3>
<RoundIcon size={28} className="shrink-0">
<PlusIcon className={`h-[14px] w-[14px] transition-transform duration-200 ${isOpen ? 'rotate-45' : ''}`} />
</RoundIcon>
</button>
<div
id={panelId}
role="region"
aria-labelledby={buttonId}
className={`grid transition-[grid-template-rows,opacity,margin] duration-300 ease-out ${isOpen ? 'mt-3 grid-rows-[1fr] opacity-100' : 'mt-0 grid-rows-[0fr] opacity-0'}`}
>
<p className="overflow-hidden text-[14px] leading-[1.6] text-muted-foreground">{answer}</p>
</div>
</article>
)
}
export function FaqSection() {
const [openId, setOpenId] = useState<string | null>(null)
const toggleItem = (id: string) => {
setOpenId((current) => (current === id ? null : id))
}
return (
<section className="flex w-full max-w-[800px] flex-col items-center gap-8 px-0 py-16 max-[809px]:px-5 max-[809px]:py-12">
<div className="flex w-full flex-col items-center gap-4">
<div className="inline-flex rounded-full bg-secondary px-3 py-1.5 text-[14px] text-secondary-foreground">FAQ</div>
<h2 className="max-w-[450px] bg-gradient-to-r from-foreground to-accent bg-clip-text text-center font-['DM_Sans'] text-[42px] font-semibold leading-[1.2] tracking-[-0.06em] text-transparent max-[809px]:text-[36px]">
Frequently Asked{' '}
<span className="font-['DM_Serif_Text'] text-[1.04em] font-normal italic tracking-normal text-accent">
Questions
</span>
</h2>
</div>
<div className="flex w-full flex-col items-center gap-6">
<div className="w-full rounded-[24px] bg-card p-2">
<div className="mx-auto hidden w-full max-w-[780px] gap-2 sm:flex">
<div className="flex flex-1 flex-col gap-[10px]">
{leftColumn.map((item) => (
<FaqItem key={item.id} {...item} isOpen={openId === item.id} onToggle={() => toggleItem(item.id)} />
))}
</div>
<div className="flex flex-1 flex-col gap-[10px]">
{rightColumn.map((item) => (
<FaqItem key={item.id} {...item} isOpen={openId === item.id} onToggle={() => toggleItem(item.id)} />
))}
</div>
</div>
<div className="flex flex-col gap-[10px] sm:hidden">
{[...leftColumn, ...rightColumn].map((item) => (
<FaqItem key={item.id} {...item} isOpen={openId === item.id} onToggle={() => toggleItem(item.id)} />
))}
</div>
</div>
<p className="max-w-[350px] text-center text-[14px] leading-[1.5] text-muted-foreground">
Contact us:{' '}
<a className="text-foreground underline" href="mailto:hello@earlybird.ai">
hello@earlybird.ai
</a>
</p>
</div>
</section>
)
}
9) Footer Section
File: sections/FooterSection.tsx
export function FooterSection() {
return (
<footer className="w-full border-t border-border py-[52px]">
<div className="mx-auto flex w-full max-w-[800px] items-center justify-between px-0 text-[14px] leading-[1.5] text-muted-foreground/85 max-[809px]:grid max-[809px]:max-w-[720px] max-[809px]:grid-cols-2 max-[809px]:gap-3 max-[809px]:px-5">
<a href="#" className="hover:text-foreground">
Use This Template
</a>
<p>Built with Studio</p>
<p className="justify-self-end max-[809px]:justify-self-start">
Created by{' '}
<a href="https://lunaui.co" target="_blank" rel="noreferrer" className="hover:text-foreground">
Luna UI
</a>
</p>
</div>
</footer>
)
}
10) Assets Required
Place these files in public/media:
avatar-1.pngavatar-2.pngavatar-3.pngbrand-logo.svgcloud-512.pngcloud-1024.pngfounder.png
11) Run
bun install
bun run dev