랜딩 페이지: D3RO 브랜드 프로모션 사이트 + GitHub Pages 배포

- Vite + React 19 + Tailwind CSS
- 4가지 디자인 레퍼런스 통합 (CRT 스크린, 크로스헤어, 노이즈, 인스트루먼트)
- 섹션 9개: Hero, Features, HowItWorks, Privacy, Pricing, FAQ, CTA, Header, Footer
- 재사용 컴포넌트 8개 (Badge, Container, InstrumentCard, GlowButton 등)
- i18n 10개 국어 (en/ko/ja/zh/es/fr/de/pt/ru/vi)
- GitHub Pages 자동 배포 워크플로우
This commit is contained in:
Yun Chan 2026-04-05 23:55:13 +09:00
parent eb83682269
commit 5892125740
45 changed files with 6225 additions and 0 deletions

40
site/src/sections/CTA.tsx Normal file
View file

@ -0,0 +1,40 @@
import { Container } from '../components/Container'
import { Crosshair } from '../components/Crosshair'
import { WaveBars } from '../components/WaveBars'
import { GlowButton } from '../components/GlowButton'
import { useI18n } from '../i18n'
export function CTA() {
const { t } = useI18n()
return (
<section className="relative py-24 md:py-32">
{/* Background glow */}
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[500px] h-[400px] bg-brand-amber/[0.03] rounded-full blur-[120px] pointer-events-none" />
<Container className="relative text-center">
<Crosshair className="inline-block mb-8">
<WaveBars className="h-8 px-2" />
</Crosshair>
<h2 className="font-display text-display font-bold text-neutral-50 mb-6 max-w-3xl mx-auto">
{t.cta.title1} <span className="text-brand-amber">{t.cta.title2}</span>
</h2>
<p className="text-base md:text-lg text-neutral-400 max-w-xl mx-auto mb-10 leading-relaxed">
{t.cta.subtitle1}
<br />
{t.cta.subtitle2}
</p>
<GlowButton href="https://github.com/user/D3ROVoice/releases" variant="primary" size="lg">
{t.cta.downloadBtn}
</GlowButton>
<p className="font-mono text-nano text-neutral-600 mt-8 uppercase tracking-widest">
{t.cta.systemReq}
</p>
</Container>
</section>
)
}

68
site/src/sections/FAQ.tsx Normal file
View file

@ -0,0 +1,68 @@
import { useState } from 'react'
import { SectionHeader } from '../components/SectionHeader'
import { Container } from '../components/Container'
import { useI18n } from '../i18n'
export function FAQ() {
const { t } = useI18n()
const [openIndex, setOpenIndex] = useState<number | null>(null)
return (
<section id="faq" className="relative py-24 md:py-32">
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-surface-800/30 to-transparent pointer-events-none" />
<Container className="relative max-w-3xl">
<SectionHeader
index={t.faq.index}
title={t.faq.title}
/>
<div className="space-y-px">
{t.faq.items.map((faq, i) => {
const isOpen = openIndex === i
return (
<div
key={i}
className="border-b border-white/[0.04]"
>
<button
onClick={() => setOpenIndex(isOpen ? null : i)}
className="w-full flex items-start justify-between gap-4 py-5 text-left group"
>
<div className="flex items-start gap-4">
<span className="font-mono text-nano text-neutral-600 mt-1 flex-shrink-0">
{String(i + 1).padStart(2, '0')}
</span>
<span className="text-sm font-medium text-neutral-200 group-hover:text-white transition-colors">
{faq.q}
</span>
</div>
<svg
className={`w-4 h-4 text-neutral-500 flex-shrink-0 mt-1 transition-transform duration-200 ${
isOpen ? 'rotate-45' : ''
}`}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
>
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
</button>
<div className={`accordion-content ${isOpen ? 'open' : ''}`}>
<div>
<p className="pl-10 pb-5 text-sm text-neutral-400 leading-relaxed">
{faq.a}
</p>
</div>
</div>
</div>
)
})}
</div>
</Container>
</section>
)
}

View file

@ -0,0 +1,166 @@
import type { ReactNode } from 'react'
import { SectionHeader } from '../components/SectionHeader'
import { InstrumentCard } from '../components/InstrumentCard'
import { Container } from '../components/Container'
import { useI18n } from '../i18n'
interface FeatureMeta {
icon: ReactNode
tag: 'FREE' | 'PRO' | 'PRO+'
}
const featureMeta: FeatureMeta[] = [
{ icon: <MicIcon />, tag: 'FREE' },
{ icon: <SparklesIcon />, tag: 'FREE' },
{ icon: <LanguageIcon />, tag: 'FREE' },
{ icon: <CaptionIcon />, tag: 'PRO' },
{ icon: <ConversationIcon />, tag: 'PRO+' },
{ icon: <FileIcon />, tag: 'PRO+' },
{ icon: <ChainIcon />, tag: 'PRO' },
{ icon: <ContextIcon />, tag: 'PRO' },
{ icon: <MemoIcon />, tag: 'PRO' },
]
const tagStyles = {
FREE: 'text-emerald-400 bg-emerald-500/10 border-emerald-500/20',
PRO: 'text-brand-amber bg-brand-amber/10 border-brand-amber/20',
'PRO+': 'text-violet-400 bg-violet-500/10 border-violet-500/20',
} as const
export function Features() {
const { t } = useI18n()
return (
<section id="features" className="relative py-24 md:py-32">
<Container>
<SectionHeader
index={t.features.index}
title={t.features.title}
subtitle={t.features.subtitle}
/>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{t.features.items.map((item, i) => {
const meta = featureMeta[i]
return (
<InstrumentCard key={i}>
{/* Top row: icon + tag */}
<div className="flex items-start justify-between mb-6">
<div className="w-10 h-10 rounded-card bg-surface-400 border border-white/[0.06] flex items-center justify-center text-brand-amber">
{meta.icon}
</div>
<span className={`text-nano font-mono font-semibold tracking-widest uppercase px-2 py-0.5 border rounded-sm ${tagStyles[meta.tag]}`}>
{meta.tag}
</span>
</div>
{/* Content */}
<h3 className="font-display text-lg font-semibold text-neutral-100 mb-2">
{item.title}
</h3>
<p className="text-sm text-neutral-400 leading-relaxed">
{item.description}
</p>
{/* Bottom arrow */}
<div className="mt-5 flex justify-end">
<div className="w-7 h-7 rounded-full border border-white/[0.06] flex items-center justify-center text-neutral-500 group-hover:text-brand-amber group-hover:border-brand-amber/20 transition-colors">
<svg className="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<line x1="7" y1="17" x2="17" y2="7" />
<polyline points="7 7 17 7 17 17" />
</svg>
</div>
</div>
</InstrumentCard>
)
})}
</div>
</Container>
</section>
)
}
/* ── Inline SVG Icons ─────────────────────────────────── */
function MicIcon() {
return (
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="9" y="2" width="6" height="11" rx="3" />
<path d="M5 10a7 7 0 0 0 14 0" />
<line x1="12" y1="17" x2="12" y2="21" />
</svg>
)
}
function SparklesIcon() {
return (
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 2l2.4 7.2L22 12l-7.6 2.8L12 22l-2.4-7.2L2 12l7.6-2.8z" />
</svg>
)
}
function LanguageIcon() {
return (
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M5 8l6 6" /><path d="M4 14l6-6 2-3" /><path d="M2 5h12" /><path d="M7 2h1" />
<path d="M22 22l-5-10-5 10" /><path d="M14 18h6" />
</svg>
)
}
function CaptionIcon() {
return (
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="2" y="4" width="20" height="16" rx="2" />
<path d="M7 15h4" /><path d="M13 15h4" /><path d="M7 11h10" />
</svg>
)
}
function ConversationIcon() {
return (
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
</svg>
)
}
function FileIcon() {
return (
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
</svg>
)
}
function ChainIcon() {
return (
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
)
}
function ContextIcon() {
return (
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="2" y="3" width="20" height="14" rx="2" />
<line x1="8" y1="21" x2="16" y2="21" />
<line x1="12" y1="17" x2="12" y2="21" />
</svg>
)
}
function MemoIcon() {
return (
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
<line x1="16" y1="13" x2="8" y2="13" />
<line x1="16" y1="17" x2="8" y2="17" />
</svg>
)
}

View file

@ -0,0 +1,80 @@
import { Container } from '../components/Container'
import { Led } from '../components/Led'
import { useI18n } from '../i18n'
const techStack = [
'Electron', 'React 19', 'faster-whisper', 'Ollama',
'better-sqlite3', 'TypeScript', 'uiohook-napi',
]
export function Footer() {
const { t } = useI18n()
return (
<footer className="border-t border-white/[0.04] py-12 md:py-16">
<Container>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mb-12">
{/* Left: brand */}
<div>
<div className="flex items-center gap-2 mb-3">
<Led color="amber" size="sm" />
<span className="font-mono text-sm font-semibold tracking-tight text-neutral-300">
D3RO&middot;VOICE
</span>
</div>
<p className="text-sm text-neutral-500 leading-relaxed max-w-sm">
{t.footer.description}
</p>
</div>
{/* Right: links */}
<div className="flex flex-wrap gap-x-8 gap-y-3 md:justify-end">
<a href="https://github.com/user/D3ROVoice" className="font-mono text-nano uppercase tracking-widest text-neutral-500 hover:text-neutral-300 transition-colors flex items-center gap-1.5">
<GithubIcon />
GitHub
</a>
<a href="#features" className="font-mono text-nano uppercase tracking-widest text-neutral-500 hover:text-neutral-300 transition-colors">
{t.nav.features}
</a>
<a href="#pricing" className="font-mono text-nano uppercase tracking-widest text-neutral-500 hover:text-neutral-300 transition-colors">
{t.nav.pricing}
</a>
<a href="#faq" className="font-mono text-nano uppercase tracking-widest text-neutral-500 hover:text-neutral-300 transition-colors">
{t.nav.faq}
</a>
</div>
</div>
{/* Tech stack bar */}
<div className="flex flex-wrap items-center gap-2 mb-8">
{techStack.map((tech) => (
<span
key={tech}
className="font-mono text-nano uppercase tracking-widest text-neutral-600 bg-surface-700/50 px-2.5 py-1 rounded-sm border border-white/[0.03]"
>
{tech}
</span>
))}
</div>
{/* Bottom bar */}
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 pt-6 border-t border-white/[0.04]">
<span className="font-mono text-nano text-neutral-600 uppercase tracking-widest">
{t.footer.copyright.replace('{year}', String(new Date().getFullYear()))}
</span>
<span className="font-mono text-nano text-neutral-600 uppercase tracking-widest">
{t.footer.builtWith}
</span>
</div>
</Container>
</footer>
)
}
function GithubIcon() {
return (
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
)
}

View file

@ -0,0 +1,96 @@
import { useState, useEffect } from 'react'
import { Led } from '../components/Led'
import { LanguageSwitcher } from '../components/LanguageSwitcher'
import { navItems } from '../tokens'
import { useI18n } from '../i18n'
export function Header() {
const { t } = useI18n()
const [scrolled, setScrolled] = useState(false)
const [mobileOpen, setMobileOpen] = useState(false)
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 40)
window.addEventListener('scroll', onScroll, { passive: true })
return () => window.removeEventListener('scroll', onScroll)
}, [])
return (
<header
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 mix-blend-difference ${
scrolled ? 'backdrop-blur-xl' : ''
}`}
>
<div className="mx-auto max-w-7xl px-5 md:px-8 lg:px-12 h-16 flex items-center justify-between">
{/* Logo */}
<a href="#" className="flex items-center gap-2 group">
<Led color="amber" pulse size="md" />
<span className="font-mono text-sm font-semibold tracking-tight text-white">
D3RO<span className="text-neutral-400">&middot;</span>VOICE
</span>
</a>
{/* Desktop Nav */}
<nav className="hidden md:flex items-center gap-8">
{navItems.map((link) => (
<a
key={link.href}
href={link.href}
className="font-mono text-nano uppercase tracking-widest text-neutral-400 hover:text-white transition-colors"
>
{t.nav[link.key]}
</a>
))}
</nav>
{/* Right side: language + CTA */}
<div className="hidden md:flex items-center gap-3">
<LanguageSwitcher />
<a
href="https://github.com/user/D3ROVoice/releases"
className="inline-flex items-center gap-2 px-4 py-2 font-mono text-xs uppercase tracking-wider text-white bg-brand-amber/90 rounded-panel hover:bg-brand-amber transition-colors"
>
{t.nav.download}
</a>
</div>
{/* Mobile menu button */}
<button
onClick={() => setMobileOpen(!mobileOpen)}
className="md:hidden w-9 h-9 flex items-center justify-center"
aria-label="Menu"
>
<div className="space-y-1.5">
<span className={`block w-5 h-px bg-white transition-all ${mobileOpen ? 'rotate-45 translate-y-[3.5px]' : ''}`} />
<span className={`block w-5 h-px bg-white transition-all ${mobileOpen ? '-rotate-45 -translate-y-[3.5px]' : ''}`} />
</div>
</button>
</div>
{/* Mobile Nav */}
{mobileOpen && (
<div className="md:hidden bg-surface-900/95 backdrop-blur-xl border-t border-white/[0.06] px-5 py-6 space-y-1">
{navItems.map((link) => (
<a
key={link.href}
href={link.href}
onClick={() => setMobileOpen(false)}
className="block font-mono text-xs uppercase tracking-widest text-neutral-400 hover:text-brand-amber py-3 border-b border-white/[0.03]"
>
{t.nav[link.key]}
</a>
))}
<div className="flex items-center justify-between mt-4 gap-3">
<LanguageSwitcher />
<a
href="https://github.com/user/D3ROVoice/releases"
className="flex-1 text-center px-5 py-2.5 font-mono text-xs uppercase tracking-wider text-white bg-brand-amber rounded-panel"
>
{t.nav.download}
</a>
</div>
</div>
)}
</header>
)
}

100
site/src/sections/Hero.tsx Normal file
View file

@ -0,0 +1,100 @@
import { Badge } from '../components/Badge'
import { Crosshair } from '../components/Crosshair'
import { DataPoint } from '../components/DataPoint'
import { Container } from '../components/Container'
import { useI18n } from '../i18n'
const CJK_LOCALES = new Set(['ko', 'ja', 'zh'])
export function Hero() {
const { t, locale } = useI18n()
const isCJK = CJK_LOCALES.has(locale)
return (
<section className="relative min-h-screen flex flex-col justify-center overflow-hidden bg-grid pt-16 pb-12">
{/* Background effects */}
<div className="absolute inset-0 pointer-events-none">
<div className="absolute top-1/3 left-1/2 -translate-x-1/2 w-[700px] h-[700px] rounded-full bg-brand-amber/[0.025] blur-[140px]" />
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-brand-amber/15 to-transparent" />
</div>
<Container className="relative flex-1 flex flex-col justify-center">
{/* Meta corners */}
<div className="hidden lg:block absolute top-8 left-5 md:left-8 lg:left-12">
<Badge led ledColor="green">{t.hero.systemStatus}</Badge>
</div>
<div className="hidden lg:block absolute top-8 right-5 md:right-8 lg:right-12 font-mono text-nano uppercase tracking-widest text-neutral-500">
{t.hero.protocol}
</div>
{/* Main hero content */}
<div className="max-w-5xl pt-12 md:pt-0">
<Crosshair className="inline-block mb-8">
<Badge led>{t.hero.badge}</Badge>
</Crosshair>
<h1
className="font-display text-hero font-bold text-neutral-50 mb-6 max-w-4xl"
style={isCJK ? { lineHeight: 1.15 } : undefined}
>
<span className="block">{t.hero.title1}</span>
<span className="block">{t.hero.title2}</span>
<span className="block text-brand-amber">{t.hero.title3}</span>
</h1>
<p className="max-w-xl text-base md:text-lg text-neutral-400 leading-relaxed mb-10">
{t.hero.subtitle}
</p>
<div className="flex flex-col sm:flex-row items-start gap-4 mb-16">
<a
href="https://github.com/user/D3ROVoice/releases"
className="glow-btn inline-flex items-center gap-2 px-8 py-4 font-mono text-sm font-medium uppercase tracking-wider text-white bg-brand-amber rounded-panel"
>
<span className="relative z-10 flex items-center gap-2">
<DownloadIcon />
{t.hero.downloadBtn}
</span>
</a>
<a
href="#features"
className="inline-flex items-center gap-2 px-8 py-4 font-mono text-sm uppercase tracking-wider text-neutral-400 border border-white/[0.08] rounded-panel hover:border-white/[0.15] hover:text-neutral-200 transition-all"
>
{t.hero.viewFeatures}
<ArrowIcon />
</a>
</div>
</div>
{/* Data strip */}
<div className="border-t border-white/[0.06] pt-8 mt-auto">
<div className="grid grid-cols-2 md:grid-cols-4 gap-8">
<DataPoint label={t.hero.dataProcessing} value="100%" unit="local" />
<DataPoint label={t.hero.dataCloudTraffic} value="0" unit="bytes" />
<DataPoint label={t.hero.dataLatency} value="<2s" />
<DataPoint label={t.hero.dataPrivacy} value="10/10" />
</div>
</div>
</Container>
</section>
)
}
function DownloadIcon() {
return (
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7 10 12 15 17 10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
)
}
function ArrowIcon() {
return (
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="7" y1="17" x2="17" y2="7" />
<polyline points="7 7 17 7 17 17" />
</svg>
)
}

View file

@ -0,0 +1,112 @@
import { SectionHeader } from '../components/SectionHeader'
import { Crosshair } from '../components/Crosshair'
import { Container } from '../components/Container'
import { Led } from '../components/Led'
import { WaveBars } from '../components/WaveBars'
import { useI18n } from '../i18n'
export function HowItWorks() {
const { t } = useI18n()
return (
<section id="pipeline" className="relative py-24 md:py-32">
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-surface-800/40 to-transparent pointer-events-none" />
<Container className="relative">
<SectionHeader
index={t.pipeline.index}
title={t.pipeline.title}
subtitle={t.pipeline.subtitle}
/>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-16 items-start">
{/* Left: visual panel */}
<Crosshair className="order-2 lg:order-1">
<div className="crt-screen rounded-card p-6 md:p-8 aspect-[4/3] flex flex-col justify-between">
<div className="relative z-10 flex items-center justify-between mb-6">
<span className="font-mono text-nano uppercase tracking-widest text-brand-amber/60">
{t.pipeline.panelTitle}
</span>
<div className="flex items-center gap-2">
<Led color="green" />
<span className="font-mono text-nano uppercase tracking-widest text-neutral-500">{t.pipeline.panelActive}</span>
</div>
</div>
<div className="relative z-10 flex-1 flex flex-col justify-center space-y-4">
{/* Simulated terminal output */}
<div className="font-mono text-xs text-neutral-500 leading-relaxed space-y-2">
<div className="flex items-center gap-2">
<Led color="green" size="sm" />
<span className="text-emerald-400/70">{t.pipeline.sttReady}</span>
<span className="text-neutral-600">faster-whisper base</span>
</div>
<div className="flex items-center gap-2">
<Led color="green" size="sm" />
<span className="text-emerald-400/70">{t.pipeline.llmConnected}</span>
<span className="text-neutral-600">qwen3:4b @ localhost:11434</span>
</div>
<div className="h-px bg-white/[0.04] my-3" />
<div className="flex items-start gap-2">
<Led color="amber" size="sm" pulse className="mt-1" />
<div>
<span className="text-brand-amber/50 block mb-1">{t.pipeline.transcription}</span>
<span className="text-neutral-300">&quot;{t.pipeline.sampleInput}&quot;</span>
</div>
</div>
<div className="flex items-start gap-2">
<Led color="green" size="sm" className="mt-1" />
<div>
<span className="text-emerald-400/50 block mb-1">{t.pipeline.aiPolish}</span>
<span className="text-neutral-300">&quot;{t.pipeline.sampleOutput}&quot;</span>
</div>
</div>
</div>
</div>
<div className="relative z-10 flex items-center justify-between pt-4 border-t border-white/[0.04]">
<WaveBars className="h-6" />
<span className="font-mono text-nano text-neutral-600">{t.pipeline.panelLatency}</span>
</div>
</div>
</Crosshair>
{/* Right: numbered steps */}
<div className="order-1 lg:order-2 space-y-1">
{t.pipeline.steps.map((step, i) => (
<div
key={i}
className="group relative flex gap-5 p-5 rounded-card border border-transparent hover:border-white/[0.04] hover:bg-surface-700/30 transition-all"
>
{/* Number */}
<div className="flex-shrink-0 w-12 h-12 rounded-card bg-surface-600 border border-white/[0.06] flex items-center justify-center">
<span className="font-mono text-lg font-bold text-brand-amber">{String(i + 1).padStart(2, '0')}</span>
</div>
<div className="flex-1 min-w-0">
<span className="font-mono text-nano uppercase tracking-widest text-brand-amber/60 block mb-1">
{step.label}
</span>
<h3 className="font-display text-lg font-semibold text-neutral-100 mb-1.5">
{step.title}
</h3>
<p className="text-sm text-neutral-400 leading-relaxed mb-2">
{step.description}
</p>
<span className="inline-block font-mono text-nano text-neutral-600 bg-surface-600/60 px-2.5 py-1 rounded-sm border border-white/[0.03]">
{step.detail}
</span>
</div>
{/* Connector */}
{i < t.pipeline.steps.length - 1 && (
<div className="absolute left-[42px] top-[72px] w-px h-5 bg-gradient-to-b from-surface-300 to-transparent hidden lg:block" />
)}
</div>
))}
</div>
</div>
</Container>
</section>
)
}

View file

@ -0,0 +1,120 @@
import { SectionHeader } from '../components/SectionHeader'
import { Container } from '../components/Container'
import { GlowButton } from '../components/GlowButton'
import { useI18n } from '../i18n'
function CellValue({ value }: { value: string | boolean }) {
if (value === true) {
return (
<span className="flex items-center justify-center">
<span className="w-2 h-2 rounded-full bg-emerald-400 shadow-[0_0_4px_rgba(52,211,153,0.6)]" />
</span>
)
}
if (value === false) {
return (
<span className="flex items-center justify-center">
<span className="w-2 h-2 rounded-full bg-neutral-600" />
</span>
)
}
return <span className="font-mono text-xs text-neutral-300">{value}</span>
}
export function Pricing() {
const { t } = useI18n()
return (
<section id="pricing" className="relative py-24 md:py-32">
<Container>
<SectionHeader
index={t.pricing.index}
title={t.pricing.title}
subtitle={t.pricing.subtitle}
/>
{/* Pricing table */}
<div className="overflow-x-auto -mx-5 md:mx-0">
<table className="w-full min-w-[640px] border-collapse">
{/* Header */}
<thead>
<tr className="border-b border-white/[0.08]">
<th className="text-left py-4 px-4 font-mono text-nano uppercase tracking-widest text-neutral-500 w-[40%]">
{t.pricing.featureLabel}
</th>
<th className="text-center py-4 px-4 w-[20%]">
<div className="font-mono text-nano uppercase tracking-widest text-neutral-500 mb-1">{t.pricing.free}</div>
<div className="font-display text-xl font-bold text-neutral-300">$0</div>
<div className="font-mono text-nano text-neutral-600">{t.pricing.forever}</div>
</th>
<th className="text-center py-4 px-4 w-[20%] relative">
<div className="absolute inset-x-0 -top-3 flex justify-center">
<span className="px-3 py-0.5 rounded-full bg-brand-amber text-white text-nano font-mono font-semibold uppercase tracking-widest">
{t.pricing.popular}
</span>
</div>
<div className="font-mono text-nano uppercase tracking-widest text-brand-amber mt-4 mb-1">{t.pricing.pro}</div>
<div className="font-display text-xl font-bold text-neutral-100">$29</div>
<div className="font-mono text-nano text-neutral-600">{t.pricing.oneTime}</div>
</th>
<th className="text-center py-4 px-4 w-[20%]">
<div className="font-mono text-nano uppercase tracking-widest text-neutral-500 mb-1">{t.pricing.proPlus}</div>
<div className="font-display text-xl font-bold text-neutral-300">$49</div>
<div className="font-mono text-nano text-neutral-600">{t.pricing.oneTime}</div>
</th>
</tr>
</thead>
{/* Body */}
<tbody>
{t.pricing.rows.map((row, i) => (
<tr
key={i}
className={`border-b border-white/[0.03] hover:bg-surface-700/30 transition-colors ${
i % 2 === 0 ? 'bg-transparent' : 'bg-surface-800/20'
}`}
>
<td className="py-3 px-4 font-mono text-xs text-neutral-400">
{row.feature}
</td>
<td className="py-3 px-4 text-center">
<CellValue value={row.free} />
</td>
<td className="py-3 px-4 text-center bg-brand-amber-muted/30">
<CellValue value={row.pro} />
</td>
<td className="py-3 px-4 text-center">
<CellValue value={row.proPlus} />
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* CTA row */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-8">
<div className="flex justify-center">
<GlowButton href="https://github.com/user/D3ROVoice/releases" variant="secondary" size="md">
{t.pricing.downloadFree}
</GlowButton>
</div>
<div className="flex justify-center">
<GlowButton href="#" variant="primary" size="md">
{t.pricing.getPro}
</GlowButton>
</div>
<div className="flex justify-center">
<GlowButton href="#" variant="secondary" size="md">
{t.pricing.getProPlus}
</GlowButton>
</div>
</div>
<p className="text-center font-mono text-nano text-neutral-600 mt-6 uppercase tracking-widest">
{t.pricing.taxNote}
</p>
</Container>
</section>
)
}

View file

@ -0,0 +1,76 @@
import { SectionHeader } from '../components/SectionHeader'
import { Container } from '../components/Container'
import { Led } from '../components/Led'
import { useI18n } from '../i18n'
const ledColors: Array<'amber' | 'green' | 'red'> = [
'green', 'green', 'green', 'green', 'green', 'green',
]
export function Privacy() {
const { t } = useI18n()
return (
<section id="privacy" className="relative py-24 md:py-32 overflow-hidden">
<Container>
<SectionHeader
index={t.privacy.index}
title={t.privacy.title}
subtitle={t.privacy.subtitle}
/>
{/* CRT instrument panel */}
<div className="crt-screen rounded-card p-6 md:p-10 noise-texture mb-12">
<div className="relative z-10">
{/* Panel header */}
<div className="flex items-center justify-between mb-8 pb-4 border-b border-white/[0.04]">
<span className="font-mono text-nano uppercase tracking-widest text-brand-amber/60">
{t.privacy.monitorTitle}
</span>
<div className="flex items-center gap-2">
<Led color="green" pulse />
<span className="font-mono text-nano uppercase tracking-widest text-emerald-400/60">
{t.privacy.allClear}
</span>
</div>
</div>
{/* Metric grid */}
<div className="grid grid-cols-2 md:grid-cols-3 gap-6 md:gap-8">
{t.privacy.metrics.map((metric, i) => (
<div key={i} className="flex flex-col gap-2">
<span className="font-mono text-nano uppercase tracking-widest text-neutral-500">
{metric.label}
</span>
<div className="flex items-center gap-2">
<Led color={ledColors[i]} size="sm" />
<span className="font-mono text-sm font-semibold text-neutral-200 tracking-wide">
{metric.value}
</span>
</div>
</div>
))}
</div>
</div>
</div>
{/* Guarantees list */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{t.privacy.guarantees.map((item, i) => (
<div
key={i}
className="flex items-start gap-3 p-4 rounded-card bg-surface-700/30 border border-white/[0.03]"
>
<div className="w-5 h-5 rounded-full bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center flex-shrink-0 mt-0.5">
<svg className="w-3 h-3 text-emerald-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12" />
</svg>
</div>
<span className="text-sm text-neutral-300 leading-relaxed">{item}</span>
</div>
))}
</div>
</Container>
</section>
)
}