_             
  ___ __ _  ___| |_ _   _ ___ 
 / __/ _` |/ __| __| | | / __|
| (_| (_| | (__| |_| |_| \__ \
 \___\__,_|\___|\__|\__,_|___/
                              

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 &amp; 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>
  )
}

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.png
  • avatar-2.png
  • avatar-3.png
  • brand-logo.svg
  • cloud-512.png
  • cloud-1024.png
  • founder.png

11) Run

bun install
bun run dev