Skip to content
Clone

feat(web): landing page, shared brand mark, code-split routes

/ becomes a real landing page (web/src/pages/landing) instead of a
redirect to /app, with its own zh-CN/en message catalogue, and
/agreements/:key serves the agreement text both pages link to. index.html
gained the theme-color and description meta the marketing surface needs.

Brand pieces move out of the auth feature: the kernel mark becomes
web/src/shared/brand (auth now imports it from there), and
ThemeProvider/useTheme are re-exported from shared/theme so non-app pages
do not have to reach into shared/ui.

Every route is now a lazy chunk behind a Suspense fallback
(app/routes/deferred-routes.tsx + workspace-route.tsx), which keeps the
workspace, its LiveKit vendoring and the board off the login/landing
first load; bootstrap no longer prefetches the workspace bundle it is not
showing. The api-error gate button became plain markup instead of pulling
shared/ui into that early chunk.
ZhenYi committed
parent 2f98fd0 commit 88a5ec8 Browse files

25 files changed

+3444 −64

File tree 25
Added web/src/app/routes/deferred-routes.tsx View web/src/app/routes/deferred-routes.tsx
@@ -0,0 +1,63 @@
1import { lazy, Suspense, type ReactNode } from 'react'
2import { Loader2 } from 'lucide-react'
3import { useT } from '@/shared/i18n'
4
5const WorkspaceRouteContent = lazy(() => import('./workspace-route').then(route => ({ default: route.WorkspaceRouteContent })))
6const AgreementPage = lazy(() => import('@/pages/agreement').then(page => ({ default: page.AgreementPage })))
7const EmailVerificationPage = lazy(() => import('@/pages/email-verification').then(page => ({ default: page.EmailVerificationPage })))
8const ForgotPasswordPage = lazy(() => import('@/pages/forgot-password').then(page => ({ default: page.ForgotPasswordPage })))
9const LoginPage = lazy(() => import('@/pages/login').then(page => ({ default: page.LoginPage })))
10const NotFoundPage = lazy(() => import('@/pages/not-found').then(page => ({ default: page.NotFoundPage })))
11const ResetPasswordPage = lazy(() => import('@/pages/reset-password').then(page => ({ default: page.ResetPasswordPage })))
12const SignUpPage = lazy(() => import('@/pages/sign-up').then(page => ({ default: page.SignUpPage })))
13const TwoFactorPage = lazy(() => import('@/pages/two-factor').then(page => ({ default: page.TwoFactorPage })))
14
15function RoutePending() {
16 const { t } = useT()
17 return (
18 <main className="grid min-h-dvh place-items-center bg-background text-foreground" role="status">
19 <Loader2 size={22} className="animate-spin text-primary" aria-hidden />
20 <span className="sr-only">{t('common.action.loading')}</span>
21 </main>
22 )
23}
24
25function DeferredRoute({ children }: { children: ReactNode }) {
26 return <Suspense fallback={<RoutePending />}>{children}</Suspense>
27}
28
29export function WorkspaceRoute() {
30 return <DeferredRoute><WorkspaceRouteContent /></DeferredRoute>
31}
32
33export function LoginRoute() {
34 return <DeferredRoute><LoginPage /></DeferredRoute>
35}
36
37export function SignUpRoute() {
38 return <DeferredRoute><SignUpPage /></DeferredRoute>
39}
40
41export function ForgotPasswordRoute() {
42 return <DeferredRoute><ForgotPasswordPage /></DeferredRoute>
43}
44
45export function ResetPasswordRoute() {
46 return <DeferredRoute><ResetPasswordPage /></DeferredRoute>
47}
48
49export function EmailVerificationRoute() {
50 return <DeferredRoute><EmailVerificationPage /></DeferredRoute>
51}
52
53export function TwoFactorRoute() {
54 return <DeferredRoute><TwoFactorPage /></DeferredRoute>
55}
56
57export function AgreementRoute() {
58 return <DeferredRoute><AgreementPage /></DeferredRoute>
59}
60
61export function NotFoundRoute() {
62 return <DeferredRoute><NotFoundPage /></DeferredRoute>
63}
Modified web/src/app/routes/router.tsx View web/src/app/routes/router.tsx
@@ -1,12 +1,16 @@
11import { Navigate, createBrowserRouter } from 'react-router-dom'
2import { WorkspaceShell } from '@/pages/workspace'
3import { EmailVerificationPage } from '@/pages/email-verification'
4import { ForgotPasswordPage } from '@/pages/forgot-password'
5import { LoginPage } from '@/pages/login'
6import { NotFoundPage } from '@/pages/not-found'
7import { ResetPasswordPage } from '@/pages/reset-password'
8import { SignUpPage } from '@/pages/sign-up'
9import { TwoFactorPage } from '@/pages/two-factor'
2import { LandingPage } from '@/pages/landing'
3import {
4 AgreementRoute,
5 EmailVerificationRoute,
6 ForgotPasswordRoute,
7 LoginRoute,
8 NotFoundRoute,
9 ResetPasswordRoute,
10 SignUpRoute,
11 TwoFactorRoute,
12 WorkspaceRoute,
13} from './deferred-routes'
1014
1115/**
1216 * Route configuration lives in the app layer: it is the only place allowed to
@@ -23,11 +27,11 @@
2327 * element: they exist purely to keep the URL set (and therefore 404s) exact.
2428 */
2529export const router = createBrowserRouter([
26 { path: '/', element: <Navigate to="/app" replace /> },
30 { path: '/', element: <LandingPage /> },
2731 { path: '/app', element: <Navigate to="/app/@me" replace /> },
2832 {
2933 path: '/app',
30 element: <WorkspaceShell />,
34 element: <WorkspaceRoute />,
3135 children: [
3236 { path: 'explore' },
3337 { path: '@me' },
@@ -42,11 +46,12 @@
4246 ],
4347 },
4448 { path: '/auth', element: <Navigate to="/auth/login" replace /> },
45 { path: '/auth/login', element: <LoginPage /> },
46 { path: '/auth/signup', element: <SignUpPage /> },
47 { path: '/auth/forgot-password', element: <ForgotPasswordPage /> },
48 { path: '/auth/reset-password', element: <ResetPasswordPage /> },
49 { path: '/auth/verify-email', element: <EmailVerificationPage /> },
50 { path: '/auth/2fa', element: <TwoFactorPage /> },
51 { path: '*', element: <NotFoundPage /> },
49 { path: '/auth/login', element: <LoginRoute /> },
50 { path: '/auth/signup', element: <SignUpRoute /> },
51 { path: '/auth/forgot-password', element: <ForgotPasswordRoute /> },
52 { path: '/auth/reset-password', element: <ResetPasswordRoute /> },
53 { path: '/auth/verify-email', element: <EmailVerificationRoute /> },
54 { path: '/auth/2fa', element: <TwoFactorRoute /> },
55 { path: '/agreements/:key', element: <AgreementRoute /> },
56 { path: '*', element: <NotFoundRoute /> },
5257])
Added web/src/app/routes/workspace-route.tsx View web/src/app/routes/workspace-route.tsx
@@ -0,0 +1,15 @@
1import { installVoiceSession } from '@/features/chat'
2import { WorkspaceShell, prefetchWorkspaceBootstrap } from '@/pages/workspace'
3import { VoiceHost } from '@/shared/voice'
4
5installVoiceSession()
6prefetchWorkspaceBootstrap()
7
8export function WorkspaceRouteContent() {
9 return (
10 <>
11 <VoiceHost />
12 <WorkspaceShell />
13 </>
14 )
15}
Modified web/src/app/api-error-gate.tsx View web/src/app/api-error-gate.tsx
@@ -2,7 +2,6 @@
22import { Loader2 } from 'lucide-react'
33import { API_PROBE_INTERVAL_MS, retryApiHealth, useApiHealth, useApiUnavailable } from '@/shared/api'
44import { useT } from '@/shared/i18n'
5import { Button } from '@/shared/ui'
65
76/**
87 * App-wide「api error」intermediate state.
@@ -75,10 +74,15 @@
7574 })}
7675 </p>
7776 <div className="justify-self-center">
78 <Button onClick={retryApiHealth} disabled={probing} className="min-w-32">
77 <button
78 type="button"
79 onClick={retryApiHealth}
80 disabled={probing}
81 className="inline-flex h-9 min-w-32 items-center justify-center gap-2 rounded-field bg-primary px-4 font-mono text-sm font-bold tracking-[0.4px] text-primary-foreground transition-[filter] hover:brightness-95 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50"
82 >
7983 {probing ? <Loader2 className="h-4 w-4 animate-spin" aria-hidden /> : null}
8084 {probing ? t('error.gate.connecting') : t('error.gate.retry')}
81 </Button>
85 </button>
8286 </div>
8387 <p className="text-[11px] text-shell-ink-muted">
8488 {t('error.gate.probeInterval', { seconds: Math.round(API_PROBE_INTERVAL_MS / 1000) })}
Modified web/src/app/bootstrap.tsx View web/src/app/bootstrap.tsx
@@ -1,8 +1,6 @@
11import type { ReactNode } from 'react'
22import { alovaInstance } from '@/shared/api'
3import { ThemeProvider } from '@/shared/ui'
4import { VoiceHost } from '@/shared/voice'
5import { installVoiceSession } from '@/features/chat'
3import { ThemeProvider } from '@/shared/theme'
64import { ApiErrorGate } from './api-error-gate'
75
86/**
@@ -22,15 +20,6 @@
2220 */
2321alovaInstance.options.cacheFor = { GET: 0 }
2422
25/**
26 * Voice needs two app-level hooks because the call outlives every route:
27 * the session gets its credential source (and its refresh restore) from the
28 * chat feature, and `VoiceHost` feeds the same Room to the vendored LiveKit
29 * components so remote audio and the connection toast keep working while the
30 * user browses a repository or a board.
31 */
32installVoiceSession()
33
3423export function AppProviders({ children }: { children: ReactNode }) {
3524 // The outage gate is deliberately above the router: it is a property of the
3625 // transport, not of any single route, and it must be able to swallow the
@@ -38,7 +27,6 @@
3827 return (
3928 <ApiErrorGate>
4029 <ThemeProvider>
41 <VoiceHost />
4230 {children}
4331 </ThemeProvider>
4432 </ApiErrorGate>
Modified web/src/app/index.tsx View web/src/app/index.tsx
@@ -3,7 +3,6 @@
33import { RouterProvider } from 'react-router-dom'
44import { bootLocale } from '@/shared/i18n'
55import { initPalette, initTheme } from '@/shared/lib'
6import { prefetchWorkspaceBootstrap } from '@/pages/workspace'
76import { AppProviders } from './bootstrap'
87import { router } from './routes/router'
98import '@/shared/livekit/styles/dist/general/index.css'
@@ -14,9 +13,6 @@
1413// Resolve the interface language before anything paints: the store is a
1514// module-level singleton, so this also sets `<html lang>` for a11y and fonts.
1615bootLocale()
17// Warm identity and the global lists before the router even mounts, so the
18// shell's first paint reads them from the cache instead of waiting on them.
19prefetchWorkspaceBootstrap()
2016createRoot(document.getElementById('root')!).render(
2117 <StrictMode>
2218 <AppProviders>
Modified web/src/features/auth/ui/auth-shell.tsx View web/src/features/auth/ui/auth-shell.tsx
@@ -3,7 +3,7 @@
33import { Moon, Sun } from 'lucide-react'
44import { useT } from '@/shared/i18n'
55import { getRegion, toggleTheme, useTheme } from '@/shared/lib'
6import { KernelMark } from './kernel-mark'
6import { KernelMark } from '@/shared/brand'
77
88export interface AuthShellProps {
99 /** Breadcrumb current segment, e.g. "SIGN IN". */
@@ -52,7 +52,7 @@
5252 className="absolute left-8 top-9 hidden items-center gap-2 font-mono text-[10px] tracking-[1.2px] sm:flex lg:left-[70px]"
5353 >
5454 <KernelMark size={14} radius={4} />
55 <Link to="/app/@me" className="font-bold text-foreground">
55 <Link to="/" className="font-bold text-foreground">
5656 GitDataAI
5757 </Link>
5858 <span className={crumbSep}>/</span>
Deleted web/src/features/auth/ui/kernel-mark.tsx
@@ -1,21 +0,0 @@
1const KERNEL_PATH =
2 'M27.10145 22.82898l0-7.26956 8.09855-4.65507-1.53044-2.67826-8.09854 4.46377-6.37682-3.76232 0-8.92754-3.1884 0 0 8.9913-6.37681 3.69856-7.84348-4.46377-1.78551 2.55072 7.97101 4.52754 0 7.46087-7.97101 4.5913 1.78551 2.93334 7.84348-4.52754 6.37681 3.57101 0 9.11884 3.1884 0 0-9.11884 6.37682-3.63478 8.09854 4.59131 1.65798-2.93334-8.22609-4.52754z'
3
4export interface KernelMarkProps {
5 size?: number
6 radius?: number
7}
8
9/** The Kernel glyph on an orange badge (logo mark from the auth design). */
10export function KernelMark({ size = 44, radius = 12 }: KernelMarkProps) {
11 return (
12 <span
13 className="inline-flex shrink-0 items-center justify-center bg-primary"
14 style={{ width: size, height: size, borderRadius: radius }}
15 >
16 <svg viewBox="0 0 35.33 38.45" style={{ width: size * 0.8 }} aria-hidden>
17 <path d={KERNEL_PATH} fill="#1C2127" />
18 </svg>
19 </span>
20 )
21}
Modified web/src/features/auth/index.ts View web/src/features/auth/index.ts
@@ -2,7 +2,7 @@
22export { AuthShell, type AuthShellProps } from './ui/auth-shell'
33export { AuthCard, type AuthCardProps } from './ui/auth-card'
44export { AuthFooter, type AuthFooterProps } from './ui/auth-footer'
5export { KernelMark, type KernelMarkProps } from './ui/kernel-mark'
5export { KernelMark, type KernelMarkProps } from '@/shared/brand'
66export { IconBadge, type IconBadgeProps } from './ui/icon-badge'
77export { Chip, Pill, type ChipProps, type PillProps } from './ui/chip'
88export { getPowAnswer } from './model/pow'
Added web/src/pages/agreement/ui/agreement-page.css View web/src/pages/agreement/ui/agreement-page.css
@@ -0,0 +1,37 @@
1.gitdata-markdown-agreement {
2 font-size: 0.9375rem;
3 line-height: 1.8;
4 color: var(--foreground);
5}
6
7.gitdata-markdown-agreement h1 {
8 font-size: 1.35rem;
9 font-weight: 700;
10 letter-spacing: -0.02em;
11}
12
13.gitdata-markdown-agreement h2 {
14 margin: 1.8em 0 0.6em;
15 font-size: 1.1rem;
16 font-weight: 700;
17 letter-spacing: -0.01em;
18}
19
20.gitdata-markdown-agreement h3 {
21 margin: 1.4em 0 0.5em;
22 font-size: 1rem;
23 font-weight: 700;
24}
25
26.gitdata-markdown-agreement p,
27.gitdata-markdown-agreement li {
28 color: var(--muted-foreground);
29}
30
31.gitdata-markdown-agreement li + li {
32 margin-top: 0.35em;
33}
34
35.gitdata-markdown-agreement a {
36 color: var(--foreground);
37}
No newline at end of file
Added web/src/pages/agreement/ui/agreement-page.tsx View web/src/pages/agreement/ui/agreement-page.tsx
@@ -0,0 +1,94 @@
1import { useEffect } from 'react'
2import { Link, useParams } from 'react-router-dom'
3import { ArrowLeft, FileText, Loader2, ShieldCheck } from 'lucide-react'
4import Apis from '@/shared/api'
5import { useT } from '@/shared/i18n'
6import { unwrapApi, useAsync } from '@/shared/lib'
7import { KernelMark } from '@/shared/brand'
8import { Markdown } from '@/shared/ui'
9import './agreement-page.css'
10
11interface AgreementDoc {
12 key: string
13 title: string
14 version: string
15 content_md: string
16}
17
18const AGREEMENTS = {
19 terms: { title: 'agreement.terms.title', summary: 'agreement.terms.summary', icon: FileText, meta: 'agreement.meta.terms' },
20 privacy: { title: 'agreement.privacy.title', summary: 'agreement.privacy.summary', icon: ShieldCheck, meta: 'agreement.meta.privacy' },
21} as const
22
23type AgreementKey = keyof typeof AGREEMENTS
24
25export function AgreementPage() {
26 const { t } = useT()
27 const { key = '' } = useParams()
28 const known = key in AGREEMENTS ? (key as AgreementKey) : null
29 const docs = useAsync(
30 () => unwrapApi<AgreementDoc[]>(Apis.account.list_agreements()),
31 [],
32 )
33 const doc = docs.data?.find(item => item.key === key)
34
35 useEffect(() => {
36 const title = known ? t(AGREEMENTS[known].meta) : t('agreement.meta.generic')
37 document.title = `${title} · ${t('common.app.name')}`
38 }, [known, t])
39
40 const heading = known ? t(AGREEMENTS[known].title) : (doc?.title ?? t('agreement.meta.generic'))
41 const summary = known ? t(AGREEMENTS[known].summary) : ''
42 const Icon = known ? AGREEMENTS[known].icon : FileText
43
44 return (
45 <main className="min-h-screen bg-background px-4 py-10 sm:px-8 sm:py-14">
46 <div className="mx-auto flex w-full max-w-3xl flex-col gap-8">
47 <header className="flex flex-col gap-5">
48 <Link
49 to="/"
50 className="inline-flex w-fit items-center gap-2 text-[13px] text-muted-foreground transition-colors hover:text-foreground"
51 >
52 <ArrowLeft size={15} aria-hidden />
53 {t('agreement.back')}
54 </Link>
55 <div className="flex items-start gap-4">
56 <KernelMark size={42} />
57 <div className="flex flex-col gap-1">
58 <span className="inline-flex items-center gap-2 font-mono text-[10px] font-bold uppercase tracking-[1.2px] text-muted-foreground">
59 <Icon size={13} aria-hidden />
60 GitDataAI
61 </span>
62 <h1 className="text-2xl font-bold tracking-tight text-foreground sm:text-3xl">{heading}</h1>
63 </div>
64 </div>
65 {summary && <p className="max-w-2xl text-sm leading-relaxed text-muted-foreground">{summary}</p>}
66 </header>
67
68 <section className="rounded-card border border-border bg-card p-6 sm:p-9">
69 {docs.loading ? (
70 <p className="flex items-center gap-2 text-sm text-muted-foreground" role="status">
71 <Loader2 size={15} className="animate-spin" aria-hidden />
72 {t('agreement.loading')}
73 </p>
74 ) : null}
75 {!docs.loading && (docs.error || !doc) ? (
76 <p className="text-sm text-muted-foreground" role={docs.error ? 'alert' : undefined}>
77 {t('agreement.error')}
78 </p>
79 ) : null}
80 {!docs.loading && doc ? (
81 <article className="flex flex-col gap-4">
82 <p className="font-mono text-[11px] uppercase tracking-wide text-muted-foreground">
83 {t('agreement.updated', { version: doc.version })}
84 </p>
85 <Markdown className="gitdata-markdown-agreement">{doc.content_md}</Markdown>
86 </article>
87 ) : null}
88 </section>
89
90 <p className="text-xs leading-relaxed text-muted-foreground">{t('agreement.footerNote')}</p>
91 </div>
92 </main>
93 )
94}
No newline at end of file
Added web/src/pages/agreement/index.ts View web/src/pages/agreement/index.ts
@@ -0,0 +1 @@
1export { AgreementPage } from './ui/agreement-page'
Added web/src/pages/landing/ui/landing-page.css View web/src/pages/landing/ui/landing-page.css
@@ -0,0 +1,2520 @@
1.landing-page {
2 --landing-bg: #090b0d;
3 --landing-surface: #111417;
4 --landing-surface-raised: #171a1e;
5 --landing-surface-soft: #0d1012;
6 --landing-line: #2a2e33;
7 --landing-line-soft: #1d2125;
8 --landing-text: #f7f5ef;
9 --landing-muted: #a8adb3;
10 --landing-muted-strong: #c5c8ca;
11 --landing-accent: #ff8400;
12 --landing-accent-soft: #ffb15e;
13 --landing-dark: #101214;
14 min-height: 100vh;
15 overflow: clip;
16 background:
17 radial-gradient(circle at 72% 7%, rgb(255 132 0 / 11%), transparent 29rem),
18 var(--landing-bg);
19 color: var(--landing-text);
20 isolation: isolate;
21}
22
23.landing-page *,
24.landing-page *::before,
25.landing-page *::after {
26 box-sizing: border-box;
27}
28
29.landing-page a {
30 color: inherit;
31 text-decoration: none;
32}
33
34.landing-container {
35 width: min(calc(100% - 48px), 1200px);
36 margin-inline: auto;
37}
38
39.landing-skip {
40 position: fixed;
41 top: 10px;
42 left: 10px;
43 z-index: 100;
44 transform: translateY(-160%);
45 border-radius: 8px;
46 background: var(--landing-accent);
47 padding: 10px 14px;
48 color: var(--landing-dark);
49 font-size: 14px;
50 font-weight: 700;
51 transition: transform 160ms ease;
52}
53
54.landing-skip:focus {
55 transform: translateY(0);
56}
57
58.landing-header {
59 position: fixed;
60 inset: 0 0 auto;
61 z-index: 40;
62 border-bottom: 1px solid rgb(255 255 255 / 7%);
63 background: rgb(9 11 13 / 80%);
64 backdrop-filter: blur(16px);
65}
66
67.landing-header-inner {
68 display: flex;
69 min-height: 72px;
70 align-items: center;
71 justify-content: space-between;
72 gap: 24px;
73}
74
75.landing-brand {
76 display: inline-flex;
77 align-items: center;
78 gap: 10px;
79 min-height: 44px;
80 color: var(--landing-text);
81 font-size: 17px;
82 font-weight: 730;
83 letter-spacing: -0.03em;
84}
85
86.landing-brand:focus-visible,
87.landing-header a:focus-visible,
88.landing-button:focus-visible,
89.landing-footer button:focus-visible,
90.landing-faq-trigger:focus-visible {
91 outline: 2px solid var(--landing-accent);
92 outline-offset: 4px;
93}
94
95.landing-nav {
96 display: flex;
97 align-items: center;
98 gap: 28px;
99}
100
101.landing-nav a,
102.landing-login-link {
103 display: inline-flex;
104 min-height: 44px;
105 align-items: center;
106 color: var(--landing-muted-strong);
107 font-size: 13px;
108 font-weight: 560;
109 transition: color 180ms ease;
110}
111
112.landing-nav a:hover,
113.landing-login-link:hover {
114 color: var(--landing-text);
115}
116
117.landing-header-actions {
118 display: flex;
119 align-items: center;
120 gap: 18px;
121}
122
123.landing-button {
124 display: inline-flex;
125 min-height: 44px;
126 align-items: center;
127 justify-content: center;
128 gap: 9px;
129 border: 1px solid transparent;
130 border-radius: 10px;
131 padding: 0 18px;
132 font-size: 13px;
133 font-weight: 690;
134 transition: background-color 180ms ease, border-color 180ms ease, color 180ms ease, transform 180ms ease;
135}
136
137.landing-button:hover {
138 transform: translateY(-1px);
139}
140
141.landing-button:active {
142 transform: translateY(0);
143}
144
145.landing-button-primary {
146 background: var(--landing-accent);
147 color: var(--landing-dark) !important;
148}
149
150.landing-button-primary:hover {
151 background: #ff9b32;
152}
153
154.landing-button-secondary {
155 border-color: var(--landing-line);
156 background: rgb(255 255 255 / 4%);
157 color: var(--landing-text);
158}
159
160.landing-button-secondary:hover {
161 border-color: #474d54;
162 background: rgb(255 255 255 / 7%);
163}
164
165.landing-button-dark {
166 background: var(--landing-dark);
167 color: var(--landing-text);
168}
169
170.landing-button-dark:hover {
171 background: #24282c;
172}
173
174.landing-button-large {
175 min-height: 52px;
176 border-radius: 12px;
177 padding-inline: 22px;
178 font-size: 14px;
179}
180
181.landing-hero {
182 position: relative;
183 padding: 160px 0 106px;
184}
185
186.landing-hero::before {
187 position: absolute;
188 inset: 72px 0 0;
189 z-index: -1;
190 background-image:
191 linear-gradient(rgb(255 255 255 / 3.2%) 1px, transparent 1px),
192 linear-gradient(90deg, rgb(255 255 255 / 3.2%) 1px, transparent 1px);
193 background-size: 64px 64px;
194 mask-image: linear-gradient(to bottom, black, transparent 82%);
195 content: '';
196}
197
198.landing-hero-grid {
199 display: grid;
200 grid-template-columns: minmax(0, 0.9fr) minmax(520px, 1.1fr);
201 align-items: center;
202 gap: clamp(48px, 6vw, 88px);
203}
204
205.landing-hero-copy {
206 position: relative;
207 z-index: 2;
208 min-width: 0;
209}
210
211.landing-eyebrow,
212.landing-kicker,
213.landing-feature-label,
214.landing-final-kicker {
215 font-family: var(--font-mono);
216 text-transform: uppercase;
217 letter-spacing: 0.11em;
218}
219
220.landing-eyebrow {
221 display: inline-flex;
222 align-items: center;
223 gap: 9px;
224 margin-bottom: 26px;
225 color: var(--landing-accent-soft);
226 font-size: 10px;
227 font-weight: 700;
228}
229
230.landing-status-dot {
231 position: relative;
232 width: 7px;
233 height: 7px;
234 border-radius: 999px;
235 background: var(--landing-accent);
236 box-shadow: 0 0 0 5px rgb(255 132 0 / 12%);
237}
238
239.landing-hero h1 {
240 max-width: 720px;
241 margin: 0;
242 color: var(--landing-text);
243 font-size: clamp(3.35rem, 5.75vw, 5.35rem);
244 font-weight: 650;
245 letter-spacing: -0.067em;
246 line-height: 0.97;
247 text-wrap: balance;
248}
249
250.landing-hero h1 > span {
251 display: block;
252}
253
254.landing-gradient-text {
255 margin-top: 8px;
256 background: linear-gradient(100deg, var(--landing-accent) 4%, #ffd0a1 78%);
257 background-clip: text;
258 color: transparent;
259}
260
261.landing-keep {
262 white-space: nowrap;
263}
264
265.landing-hero-body {
266 max-width: 600px;
267 margin: 28px 0 0;
268 color: var(--landing-muted);
269 font-size: clamp(16px, 1.35vw, 18px);
270 line-height: 1.72;
271}
272
273.landing-hero-actions {
274 display: flex;
275 flex-wrap: wrap;
276 gap: 12px;
277 margin-top: 34px;
278}
279
280.landing-hero-note {
281 display: flex;
282 align-items: center;
283 gap: 8px;
284 margin: 18px 0 0;
285 color: #888e94;
286 font-size: 12px;
287}
288
289.landing-hero-note svg {
290 color: var(--landing-accent);
291}
292
293.landing-demo-figure {
294 position: relative;
295 min-width: 0;
296 margin: 0;
297}
298
299.landing-demo-glow {
300 position: absolute;
301 inset: 16% 8% 2%;
302 border-radius: 999px;
303 background: rgb(255 132 0 / 19%);
304 filter: blur(80px);
305}
306
307.landing-demo {
308 position: relative;
309 min-height: 525px;
310 overflow: hidden;
311 border: 1px solid #353a40;
312 border-radius: 18px;
313 background: #0f1215;
314 box-shadow: 0 32px 80px rgb(0 0 0 / 45%), 0 0 0 1px rgb(255 255 255 / 3%) inset;
315}
316
317.landing-demo::after {
318 position: absolute;
319 inset: 0;
320 pointer-events: none;
321 background: linear-gradient(118deg, rgb(255 255 255 / 6%), transparent 19%, transparent 78%, rgb(255 132 0 / 4%));
322 content: '';
323}
324
325.landing-demo-topbar {
326 display: grid;
327 height: 44px;
328 grid-template-columns: 1fr auto 1fr;
329 align-items: center;
330 border-bottom: 1px solid var(--landing-line);
331 padding: 0 14px;
332 color: #899097;
333 font-family: var(--font-mono);
334 font-size: 9px;
335 text-transform: uppercase;
336 letter-spacing: 0.08em;
337}
338
339.landing-window-dots {
340 display: flex;
341 gap: 5px;
342}
343
344.landing-window-dots span {
345 width: 7px;
346 height: 7px;
347 border-radius: 99px;
348 background: #3b4046;
349}
350
351.landing-window-dots span:first-child {
352 background: var(--landing-accent);
353}
354
355.landing-demo-path,
356.landing-demo-live,
357.landing-branch {
358 display: inline-flex;
359 align-items: center;
360 gap: 6px;
361}
362
363.landing-demo-live {
364 justify-self: end;
365 color: #a6d8b4;
366}
367
368.landing-demo-live span {
369 width: 5px;
370 height: 5px;
371 border-radius: 50%;
372 background: #54be75;
373 box-shadow: 0 0 0 4px rgb(84 190 117 / 11%);
374}
375
376.landing-demo-body {
377 display: grid;
378 min-height: 481px;
379 grid-template-columns: 140px minmax(0, 1fr);
380}
381
382.landing-demo-sidebar {
383 border-right: 1px solid var(--landing-line-soft);
384 padding: 18px 10px;
385 background: #0c0f11;
386}
387
388.landing-demo-space {
389 display: grid;
390 width: 30px;
391 height: 30px;
392 margin: 0 0 20px 4px;
393 place-items: center;
394 border-radius: 8px;
395 background: var(--landing-accent);
396 color: #111;
397 font-family: var(--font-mono);
398 font-size: 11px;
399 font-weight: 800;
400}
401
402.landing-demo-nav {
403 display: flex;
404 min-height: 34px;
405 align-items: center;
406 gap: 8px;
407 border-radius: 7px;
408 padding: 0 8px;
409 color: #70777e;
410 font-family: var(--font-mono);
411 font-size: 8px;
412 text-transform: uppercase;
413 letter-spacing: 0.06em;
414}
415
416.landing-demo-nav + .landing-demo-nav {
417 margin-top: 4px;
418}
419
420.landing-demo-nav.is-active {
421 background: #1b1f23;
422 color: var(--landing-text);
423}
424
425.landing-demo-nav b {
426 margin-left: auto;
427 color: var(--landing-accent-soft);
428 font-size: 8px;
429}
430
431.landing-demo-canvas {
432 position: relative;
433 min-width: 0;
434 overflow: hidden;
435}
436
437.landing-demo-toolbar {
438 display: flex;
439 min-height: 60px;
440 align-items: center;
441 justify-content: space-between;
442 border-bottom: 1px solid var(--landing-line-soft);
443 padding: 0 18px;
444}
445
446.landing-demo-toolbar > div:first-child {
447 display: flex;
448 align-items: baseline;
449 gap: 6px;
450 color: #747b82;
451 font-size: 10px;
452}
453
454.landing-demo-toolbar strong {
455 color: var(--landing-text);
456 font-size: 12px;
457}
458
459.landing-mono,
460.landing-card-index,
461.landing-case-number {
462 font-family: var(--font-mono);
463}
464
465.landing-branch {
466 border: 1px solid #30353a;
467 border-radius: 7px;
468 padding: 6px 9px;
469 color: #9ca2a8;
470 font-family: var(--font-mono);
471 font-size: 8px;
472}
473
474.landing-demo-content {
475 padding: 20px 18px;
476}
477
478.landing-pr-heading {
479 display: flex;
480 align-items: flex-start;
481 justify-content: space-between;
482 gap: 16px;
483 margin-bottom: 18px;
484}
485
486.landing-pr-heading span {
487 color: var(--landing-accent-soft);
488 font-family: var(--font-mono);
489 font-size: 8px;
490 font-weight: 700;
491 letter-spacing: 0.08em;
492 text-transform: uppercase;
493}
494
495.landing-pr-heading h3 {
496 margin: 5px 0 0;
497 font-size: 15px;
498 font-weight: 590;
499 letter-spacing: -0.025em;
500}
501
502.landing-change-chip {
503 flex: 0 0 auto;
504 border: 1px solid #274532;
505 border-radius: 999px;
506 background: #13231a;
507 padding: 5px 8px;
508 color: #7fd29a !important;
509}
510
511.landing-file-row {
512 display: grid;
513 min-height: 35px;
514 grid-template-columns: 22px minmax(0, 1fr) auto;
515 align-items: center;
516 border-top: 1px solid var(--landing-line-soft);
517 color: #a8adb2;
518 font-family: var(--font-mono);
519 font-size: 8px;
520}
521
522.landing-file-row > span:last-child {
523 color: #5fbd7c;
524}
525
526.landing-file-status {
527 color: var(--landing-accent-soft);
528}
529
530.landing-file-status.is-new {
531 color: #68c786;
532}
533
534.landing-diff {
535 margin-top: 12px;
536 overflow: hidden;
537 border: 1px solid var(--landing-line-soft);
538 border-radius: 8px;
539 background: #0a0c0e;
540}
541
542.landing-diff-line {
543 display: flex;
544 min-height: 25px;
545 align-items: center;
546 color: #9aa0a6;
547 font-family: var(--font-mono);
548 font-size: 7.5px;
549}
550
551.landing-diff-line i {
552 width: 32px;
553 margin-right: 9px;
554 color: #4c5359;
555 font-style: normal;
556 text-align: right;
557}
558
559.landing-diff-line code {
560 font: inherit;
561}
562
563.landing-diff-line.is-removed {
564 background: rgb(141 53 47 / 18%);
565 color: #d8a29e;
566}
567
568.landing-diff-line.is-added {
569 background: rgb(46 121 70 / 18%);
570 color: #a8dbb8;
571}
572
573.landing-review-card {
574 position: absolute;
575 right: 16px;
576 bottom: 16px;
577 z-index: 3;
578 width: min(286px, calc(100% - 32px));
579 border: 1px solid #454a4f;
580 border-radius: 13px;
581 background: rgb(24 27 30 / 96%);
582 padding: 15px;
583 box-shadow: 0 18px 44px rgb(0 0 0 / 42%);
584}
585
586.landing-review-head {
587 display: flex;
588 align-items: center;
589 justify-content: space-between;
590 margin-bottom: 11px;
591}
592
593.landing-review-head > span:first-child {
594 display: inline-flex;
595 align-items: center;
596 gap: 7px;
597 color: var(--landing-accent-soft);
598 font-family: var(--font-mono);
599 font-size: 8px;
600 font-weight: 700;
601 letter-spacing: 0.08em;
602}
603
604.landing-review-score {
605 display: grid;
606 width: 23px;
607 height: 23px;
608 place-items: center;
609 border-radius: 7px;
610 background: var(--landing-accent);
611 color: #111;
612 font-family: var(--font-mono);
613 font-size: 8px;
614 font-weight: 800;
615}
616
617.landing-review-card > strong {
618 display: block;
619 margin-bottom: 12px;
620 font-size: 12px;
621 font-weight: 620;
622}
623
624.landing-review-item {
625 display: flex;
626 align-items: flex-start;
627 gap: 8px;
628 border-top: 1px solid #30353a;
629 padding: 8px 0;
630 color: #a8adb2;
631 font-size: 9px;
632 line-height: 1.45;
633}
634
635.landing-review-item > span {
636 color: var(--landing-accent-soft);
637 font-family: var(--font-mono);
638 font-size: 8px;
639}
640
641.landing-review-online {
642 display: flex;
643 align-items: center;
644 gap: 8px;
645 margin-top: 5px;
646 color: #7d848a;
647 font-size: 8px;
648}
649
650.landing-avatars {
651 display: flex;
652 padding-left: 4px;
653}
654
655.landing-avatars span {
656 display: grid;
657 width: 20px;
658 height: 20px;
659 margin-left: -4px;
660 place-items: center;
661 border: 2px solid #181b1e;
662 border-radius: 50%;
663 background: #363c42;
664 color: #ddd;
665 font-family: var(--font-mono);
666 font-size: 6px;
667}
668
669.landing-capabilities {
670 border-block: 1px solid var(--landing-line-soft);
671 background: rgb(255 255 255 / 1.5%);
672}
673
674.landing-capabilities-inner {
675 display: flex;
676 min-height: 82px;
677 align-items: center;
678 justify-content: space-between;
679 gap: 28px;
680}
681
682.landing-capabilities-label {
683 color: #7d8389;
684 font-family: var(--font-mono);
685 font-size: 9px;
686 font-weight: 700;
687 letter-spacing: 0.1em;
688 text-transform: uppercase;
689}
690
691.landing-capability-list {
692 display: flex;
693 flex-wrap: wrap;
694 align-items: center;
695 justify-content: flex-end;
696 gap: 16px;
697 color: #b9bdc1;
698 font-size: 11px;
699 font-weight: 560;
700}
701
702.landing-capability-list i {
703 width: 3px;
704 height: 3px;
705 border-radius: 50%;
706 background: #4b5157;
707}
708
709.landing-section {
710 padding: 128px 0;
711}
712
713.landing-section-soft {
714 border-block: 1px solid var(--landing-line-soft);
715 background: var(--landing-surface-soft);
716}
717
718.landing-section-heading {
719 max-width: 720px;
720 margin-bottom: 52px;
721}
722
723.landing-section-heading-wide {
724 max-width: 800px;
725}
726
727.landing-kicker {
728 display: block;
729 margin-bottom: 17px;
730 color: var(--landing-accent);
731 font-size: 10px;
732 font-weight: 700;
733}
734
735.landing-section-heading h2,
736.landing-final-card h2 {
737 margin: 0;
738 font-size: clamp(2.35rem, 4.2vw, 4.2rem);
739 font-weight: 620;
740 letter-spacing: -0.055em;
741 line-height: 1.04;
742 text-wrap: balance;
743}
744
745.landing-section-heading > p {
746 max-width: 650px;
747 margin: 21px 0 0;
748 color: var(--landing-muted);
749 font-size: 16px;
750 line-height: 1.72;
751}
752
753.landing-value-grid {
754 display: grid;
755 grid-template-columns: repeat(3, minmax(0, 1fr));
756 border-block: 1px solid var(--landing-line);
757}
758
759.landing-value-card {
760 position: relative;
761 min-height: 310px;
762 padding: 32px;
763}
764
765.landing-value-card + .landing-value-card {
766 border-left: 1px solid var(--landing-line);
767}
768
769.landing-value-card-top {
770 display: flex;
771 align-items: flex-start;
772 justify-content: space-between;
773}
774
775.landing-icon-box {
776 display: grid;
777 width: 42px;
778 height: 42px;
779 place-items: center;
780 border: 1px solid #343940;
781 border-radius: 11px;
782 background: #14171a;
783 color: var(--landing-accent-soft);
784}
785
786.landing-card-index {
787 color: #52585f;
788 font-size: 10px;
789 letter-spacing: 0.08em;
790}
791
792.landing-value-card h3 {
793 margin: 58px 0 14px;
794 font-size: 22px;
795 font-weight: 610;
796 letter-spacing: -0.035em;
797}
798
799.landing-value-card p,
800.landing-feature-copy p,
801.landing-case p {
802 margin: 0;
803 color: var(--landing-muted);
804 font-size: 14px;
805 line-height: 1.72;
806}
807
808.landing-feature-grid {
809 display: grid;
810 grid-template-columns: repeat(12, minmax(0, 1fr));
811 gap: 16px;
812}
813
814.landing-feature-card {
815 position: relative;
816 min-height: 450px;
817 overflow: hidden;
818 border: 1px solid var(--landing-line);
819 border-radius: 18px;
820 background: var(--landing-surface);
821 padding: 34px;
822 transition: border-color 200ms ease, transform 200ms ease;
823}
824
825.landing-feature-card:hover {
826 transform: translateY(-2px);
827 border-color: #42484f;
828}
829
830.landing-feature-repo,
831.landing-feature-team {
832 grid-column: span 7;
833}
834
835.landing-feature-ai,
836.landing-feature-board {
837 grid-column: span 5;
838}
839
840.landing-feature-label {
841 display: inline-flex;
842 align-items: center;
843 gap: 8px;
844 color: var(--landing-accent-soft);
845 font-size: 9px;
846 font-weight: 700;
847}
848
849.landing-feature-copy {
850 position: relative;
851 z-index: 2;
852 max-width: 470px;
853}
854
855.landing-feature-copy h3 {
856 margin: 18px 0 12px;
857 font-size: 27px;
858 font-weight: 610;
859 letter-spacing: -0.04em;
860}
861
862.landing-repo-visual,
863.landing-ai-visual,
864.landing-board-visual,
865.landing-team-visual {
866 position: absolute;
867 right: 30px;
868 bottom: -1px;
869 left: 30px;
870 border: 1px solid #2f3439;
871 border-bottom: 0;
872 border-radius: 13px 13px 0 0;
873 background: #0b0e10;
874 box-shadow: 0 -18px 50px rgb(0 0 0 / 18%);
875}
876
877.landing-repo-visual {
878 padding: 12px 16px 18px;
879}
880
881.landing-repo-visual > div {
882 display: grid;
883 min-height: 35px;
884 grid-template-columns: 18px minmax(0, 1fr) auto;
885 align-items: center;
886 border-bottom: 1px solid #202429;
887 color: #aeb3b8;
888 font-family: var(--font-mono);
889 font-size: 9px;
890}
891
892.landing-repo-visual > div em {
893 color: #686f76;
894 font-style: normal;
895}
896
897.landing-repo-visual .landing-repo-visual-head {
898 grid-template-columns: 1fr auto;
899 color: var(--landing-accent-soft);
900 font-size: 8px;
901 letter-spacing: 0.06em;
902 text-transform: uppercase;
903}
904
905.landing-file-dot {
906 padding-left: 3px;
907 color: var(--landing-accent);
908 font-size: 18px;
909}
910
911.landing-ai-visual {
912 padding: 20px;
913}
914
915.landing-ai-visual > div:first-child {
916 display: grid;
917 grid-template-columns: 10px 1fr auto;
918 align-items: center;
919 gap: 8px;
920 color: #d0d3d5;
921 font-family: var(--font-mono);
922 font-size: 9px;
923}
924
925.landing-ai-pulse {
926 width: 7px;
927 height: 7px;
928 border-radius: 50%;
929 background: var(--landing-accent);
930 box-shadow: 0 0 0 5px rgb(255 132 0 / 10%);
931}
932
933.landing-ai-visual > div:first-child > span:last-child {
934 color: var(--landing-accent-soft);
935}
936
937.landing-ai-bar {
938 height: 4px;
939 margin: 14px 0 18px;
940 overflow: hidden;
941 border-radius: 99px;
942 background: #292e33;
943}
944
945.landing-ai-bar span {
946 display: block;
947 width: 84%;
948 height: 100%;
949 border-radius: inherit;
950 background: var(--landing-accent);
951}
952
953.landing-ai-visual p {
954 display: flex;
955 align-items: center;
956 gap: 8px;
957 margin: 9px 0 0;
958 color: #858c92;
959 font-size: 9px;
960}
961
962.landing-ai-visual p svg {
963 color: #69c487;
964}
965
966.landing-board-visual {
967 display: grid;
968 grid-template-columns: repeat(3, 1fr);
969 gap: 8px;
970 border: 0;
971 background: transparent;
972 box-shadow: none;
973}
974
975.landing-board-visual > div {
976 min-height: 147px;
977 border: 1px solid #2c3136;
978 border-bottom: 0;
979 border-radius: 10px 10px 0 0;
980 background: #0b0e10;
981 padding: 12px 9px;
982}
983
984.landing-board-visual span {
985 display: block;
986 margin-bottom: 10px;
987 color: #777e85;
988 font-family: var(--font-mono);
989 font-size: 7px;
990 font-weight: 700;
991 letter-spacing: 0.06em;
992 text-transform: uppercase;
993}
994
995.landing-board-visual i {
996 display: block;
997 height: 34px;
998 margin-top: 7px;
999 border: 1px solid #30353a;
1000 border-radius: 7px;
1001 background: #171a1d;
1002}
1003
1004.landing-board-visual i::before {
1005 display: block;
1006 width: 46%;
1007 height: 3px;
1008 margin: 9px;
1009 border-radius: 9px;
1010 background: #555c63;
1011 content: '';
1012}
1013
1014.landing-board-visual i.is-orange::before {
1015 background: var(--landing-accent);
1016}
1017
1018.landing-board-visual i.is-done::before {
1019 background: #5cb778;
1020}
1021
1022.landing-team-visual {
1023 padding: 17px;
1024}
1025
1026.landing-team-message {
1027 display: grid;
1028 grid-template-columns: 28px 1fr;
1029 align-items: start;
1030 gap: 10px;
1031 padding: 9px 0;
1032}
1033
1034.landing-team-message + .landing-team-message {
1035 border-top: 1px solid #24282c;
1036}
1037
1038.landing-team-message > span {
1039 display: grid;
1040 width: 28px;
1041 height: 28px;
1042 place-items: center;
1043 border-radius: 8px;
1044 background: #2a2f34;
1045 color: #d5d8da;
1046 font-family: var(--font-mono);
1047 font-size: 7px;
1048}
1049
1050.landing-team-message p {
1051 margin: 4px 0 0;
1052 color: #989fa5;
1053 font-size: 9px;
1054 line-height: 1.55;
1055}
1056
1057.landing-team-message b {
1058 color: var(--landing-accent-soft);
1059 font-weight: 600;
1060}
1061
1062.landing-voice-pill {
1063 display: inline-flex;
1064 min-height: 30px;
1065 align-items: center;
1066 gap: 7px;
1067 margin-top: 8px;
1068 border: 1px solid #38453d;
1069 border-radius: 99px;
1070 background: #142018;
1071 padding: 0 10px;
1072 color: #92c7a2;
1073 font-size: 8px;
1074}
1075
1076.landing-voice-pill span {
1077 display: grid;
1078 width: 16px;
1079 height: 16px;
1080 place-items: center;
1081 border-radius: 50%;
1082 background: #2b4b35;
1083 font-family: var(--font-mono);
1084 font-size: 7px;
1085}
1086
1087.landing-cases-layout,
1088.landing-faq-layout {
1089 display: grid;
1090 grid-template-columns: minmax(260px, 0.74fr) minmax(0, 1.26fr);
1091 gap: clamp(56px, 8vw, 112px);
1092}
1093
1094.landing-cases-heading,
1095.landing-faq-heading {
1096 margin-bottom: 0;
1097}
1098
1099.landing-case-list {
1100 border-top: 1px solid var(--landing-line);
1101}
1102
1103.landing-case {
1104 position: relative;
1105 display: grid;
1106 grid-template-columns: 48px minmax(0, 1fr) 24px;
1107 gap: 20px;
1108 border-bottom: 1px solid var(--landing-line);
1109 padding: 30px 0;
1110}
1111
1112.landing-case-number {
1113 padding-top: 4px;
1114 color: var(--landing-accent);
1115 font-size: 9px;
1116 letter-spacing: 0.08em;
1117}
1118
1119.landing-case h3 {
1120 margin: 0 0 10px;
1121 font-size: 20px;
1122 font-weight: 610;
1123 letter-spacing: -0.03em;
1124}
1125
1126.landing-case-outcome {
1127 display: inline-flex;
1128 align-items: center;
1129 gap: 7px;
1130 margin-top: 18px;
1131 color: #c2c6c9;
1132 font-size: 11px;
1133 font-weight: 600;
1134}
1135
1136.landing-case-outcome svg {
1137 color: var(--landing-accent);
1138}
1139
1140.landing-case-arrow {
1141 color: #596067;
1142 transition: color 180ms ease, transform 180ms ease;
1143}
1144
1145.landing-case:hover .landing-case-arrow {
1146 transform: translate(2px, -2px);
1147 color: var(--landing-accent);
1148}
1149
1150.landing-faq-list {
1151 border-top: 1px solid var(--landing-line);
1152}
1153
1154.landing-faq-item {
1155 border-bottom: 1px solid var(--landing-line);
1156}
1157
1158.landing-faq-item-heading {
1159 margin: 0;
1160 font-size: inherit;
1161 font-weight: inherit;
1162}
1163
1164.landing-faq-trigger {
1165 display: grid;
1166 width: 100%;
1167 min-height: 76px;
1168 cursor: pointer;
1169 grid-template-columns: minmax(0, 1fr) 26px;
1170 align-items: center;
1171 gap: 24px;
1172 border: 0;
1173 background: transparent;
1174 padding: 0;
1175 color: var(--landing-text);
1176 font: inherit;
1177 font-size: 16px;
1178 font-weight: 590;
1179 line-height: 1.4;
1180 text-align: left;
1181 transition: color 200ms ease;
1182}
1183
1184.landing-faq-trigger:hover {
1185 color: var(--landing-accent-soft);
1186}
1187
1188.landing-faq-trigger:focus-visible {
1189 outline: 2px solid var(--landing-accent);
1190 outline-offset: 3px;
1191 border-radius: 4px;
1192}
1193
1194.landing-faq-trigger i {
1195 position: relative;
1196 width: 20px;
1197 height: 20px;
1198 justify-self: end;
1199}
1200
1201.landing-faq-trigger i::before,
1202.landing-faq-trigger i::after {
1203 position: absolute;
1204 top: 9px;
1205 left: 3px;
1206 width: 14px;
1207 height: 1px;
1208 background: var(--landing-accent);
1209 content: '';
1210 transition: transform 300ms cubic-bezier(0.22, 1, 0.36, 1);
1211}
1212
1213.landing-faq-trigger i::after {
1214 transform: rotate(90deg);
1215}
1216
1217.landing-faq-item.is-open .landing-faq-trigger i::after {
1218 transform: rotate(0deg);
1219}
1220
1221.landing-faq-panel {
1222 display: grid;
1223 grid-template-rows: 0fr;
1224 opacity: 0;
1225 transition:
1226 grid-template-rows 320ms cubic-bezier(0.22, 1, 0.36, 1),
1227 opacity 220ms ease;
1228}
1229
1230.landing-faq-item.is-open .landing-faq-panel {
1231 grid-template-rows: 1fr;
1232 opacity: 1;
1233}
1234
1235.landing-faq-panel-inner {
1236 min-height: 0;
1237 overflow: hidden;
1238}
1239
1240.landing-faq-panel p {
1241 max-width: 680px;
1242 margin: 0 50px 0 0;
1243 padding-bottom: 26px;
1244 color: var(--landing-muted);
1245 font-size: 14px;
1246 line-height: 1.75;
1247}
1248
1249.landing-final-section {
1250 padding: 96px 0;
1251}
1252
1253.landing-final-card {
1254 position: relative;
1255 display: grid;
1256 overflow: hidden;
1257 grid-template-columns: minmax(0, 1fr) auto;
1258 align-items: end;
1259 gap: 64px;
1260 border-radius: 22px;
1261 background: var(--landing-accent);
1262 padding: clamp(40px, 6vw, 72px);
1263 color: var(--landing-dark);
1264}
1265
1266.landing-final-grid {
1267 position: absolute;
1268 inset: 0;
1269 background-image:
1270 linear-gradient(rgb(17 18 20 / 9%) 1px, transparent 1px),
1271 linear-gradient(90deg, rgb(17 18 20 / 9%) 1px, transparent 1px);
1272 background-size: 44px 44px;
1273 mask-image: linear-gradient(110deg, transparent 25%, black);
1274}
1275
1276.landing-final-card > div:not(.landing-final-grid) {
1277 position: relative;
1278 z-index: 1;
1279}
1280
1281.landing-final-kicker {
1282 display: block;
1283 margin-bottom: 18px;
1284 font-size: 9px;
1285 font-weight: 800;
1286}
1287
1288.landing-final-card h2 {
1289 max-width: 700px;
1290}
1291
1292.landing-final-card p {
1293 max-width: 620px;
1294 margin: 18px 0 0;
1295 color: rgb(16 18 20 / 74%);
1296 font-size: 15px;
1297 line-height: 1.65;
1298}
1299
1300.landing-final-actions {
1301 display: flex;
1302 min-width: 190px;
1303 flex-direction: column;
1304 align-items: stretch;
1305 gap: 14px;
1306}
1307
1308.landing-final-login {
1309 min-height: 36px;
1310 text-align: center;
1311 font-size: 12px;
1312 font-weight: 700;
1313 text-decoration: underline !important;
1314 text-underline-offset: 4px;
1315}
1316
1317.landing-footer {
1318 border-top: 1px solid var(--landing-line-soft);
1319 padding: 46px 0 52px;
1320}
1321
1322.landing-footer-grid {
1323 display: grid;
1324 grid-template-columns: 1fr auto auto;
1325 align-items: start;
1326 gap: 64px;
1327}
1328
1329.landing-footer-grid > div:first-child > p {
1330 max-width: 310px;
1331 margin: 11px 0 0;
1332 color: #777e84;
1333 font-size: 12px;
1334 line-height: 1.6;
1335}
1336
1337.landing-footer-links {
1338 display: grid;
1339 grid-template-columns: repeat(2, auto);
1340 gap: 13px 32px;
1341}
1342
1343.landing-footer-links a,
1344.landing-footer-meta {
1345 color: #8d9399;
1346 font-size: 11px;
1347}
1348
1349.landing-footer-links a:hover {
1350 color: var(--landing-text);
1351}
1352
1353.landing-footer-meta {
1354 display: flex;
1355 flex-direction: column;
1356 align-items: flex-end;
1357 gap: 16px;
1358}
1359
1360.landing-footer-meta button {
1361 display: inline-flex;
1362 min-height: 40px;
1363 cursor: pointer;
1364 align-items: center;
1365 gap: 7px;
1366 border: 1px solid var(--landing-line);
1367 border-radius: 9px;
1368 background: transparent;
1369 padding: 0 11px;
1370 color: #a9aeb3;
1371 font: inherit;
1372 transition: border-color 180ms ease, color 180ms ease;
1373}
1374
1375.landing-footer-meta button:hover {
1376 border-color: #4a5056;
1377 color: var(--landing-text);
1378}
1379
1380@media (prefers-reduced-motion: no-preference) {
1381 .landing-hero-copy {
1382 animation: landing-rise 480ms cubic-bezier(0.22, 1, 0.36, 1) both;
1383 }
1384
1385 .landing-demo-figure {
1386 animation: landing-rise 560ms 80ms cubic-bezier(0.22, 1, 0.36, 1) both;
1387 }
1388
1389 .landing-status-dot,
1390 .landing-ai-pulse {
1391 animation: landing-pulse 2.6s ease-in-out infinite;
1392 }
1393}
1394
1395@keyframes landing-rise {
1396 from {
1397 opacity: 0;
1398 transform: translateY(14px);
1399 }
1400 to {
1401 opacity: 1;
1402 transform: translateY(0);
1403 }
1404}
1405
1406@keyframes landing-pulse {
1407 50% {
1408 box-shadow: 0 0 0 8px rgb(255 132 0 / 4%);
1409 }
1410}
1411
1412@media (max-width: 1040px) {
1413 .landing-nav {
1414 display: none;
1415 }
1416
1417 .landing-hero-grid {
1418 grid-template-columns: 1fr;
1419 }
1420
1421 .landing-hero-copy {
1422 max-width: 820px;
1423 }
1424
1425 .landing-demo-figure {
1426 width: min(100%, 820px);
1427 }
1428
1429 .landing-feature-card {
1430 min-height: 430px;
1431 }
1432
1433 .landing-feature-repo,
1434 .landing-feature-team,
1435 .landing-feature-ai,
1436 .landing-feature-board {
1437 grid-column: span 6;
1438 }
1439
1440 .landing-cases-layout,
1441 .landing-faq-layout {
1442 grid-template-columns: 1fr;
1443 gap: 48px;
1444 }
1445
1446 .landing-cases-heading,
1447 .landing-faq-heading {
1448 max-width: 680px;
1449 }
1450}
1451
1452@media (max-width: 760px) {
1453 .landing-container {
1454 width: min(calc(100% - 32px), 1200px);
1455 }
1456
1457 .landing-header-inner {
1458 min-height: 64px;
1459 gap: 10px;
1460 }
1461
1462 .landing-brand {
1463 gap: 8px;
1464 font-size: 15px;
1465 }
1466
1467 .landing-header-actions {
1468 gap: 7px;
1469 }
1470
1471 .landing-login-link {
1472 padding-inline: 8px;
1473 font-size: 12px;
1474 }
1475
1476 .landing-header-cta {
1477 min-height: 40px;
1478 padding-inline: 12px;
1479 font-size: 11px;
1480 }
1481
1482 .landing-header-cta svg {
1483 display: none;
1484 }
1485
1486 .landing-hero {
1487 padding: 122px 0 76px;
1488 }
1489
1490 .landing-hero::before {
1491 top: 64px;
1492 background-size: 42px 42px;
1493 }
1494
1495 .landing-hero-grid {
1496 gap: 52px;
1497 }
1498
1499 .landing-eyebrow {
1500 margin-bottom: 21px;
1501 }
1502
1503 .landing-hero h1 {
1504 font-size: clamp(2.72rem, 13vw, 4rem);
1505 letter-spacing: -0.062em;
1506 }
1507
1508 .landing-hero-body {
1509 margin-top: 23px;
1510 font-size: 16px;
1511 }
1512
1513 .landing-hero-actions {
1514 flex-direction: column;
1515 align-items: stretch;
1516 margin-top: 28px;
1517 }
1518
1519 .landing-demo {
1520 min-height: 455px;
1521 border-radius: 14px;
1522 }
1523
1524 .landing-demo-topbar {
1525 grid-template-columns: 1fr auto;
1526 }
1527
1528 .landing-demo-path {
1529 display: none;
1530 }
1531
1532 .landing-demo-sidebar {
1533 display: none;
1534 }
1535
1536 .landing-demo-body {
1537 min-height: 411px;
1538 grid-template-columns: 1fr;
1539 }
1540
1541 .landing-demo-toolbar,
1542 .landing-demo-content {
1543 padding-inline: 14px;
1544 }
1545
1546 .landing-diff-line code {
1547 overflow: hidden;
1548 text-overflow: ellipsis;
1549 white-space: nowrap;
1550 }
1551
1552 .landing-review-card {
1553 right: 12px;
1554 bottom: 12px;
1555 width: calc(100% - 24px);
1556 }
1557
1558 .landing-capabilities-inner {
1559 min-height: 112px;
1560 flex-direction: column;
1561 align-items: flex-start;
1562 justify-content: center;
1563 gap: 13px;
1564 }
1565
1566 .landing-capability-list {
1567 justify-content: flex-start;
1568 gap: 8px 12px;
1569 }
1570
1571 .landing-capability-list i {
1572 display: none;
1573 }
1574
1575 .landing-capability-list span {
1576 border: 1px solid #2d3237;
1577 border-radius: 99px;
1578 padding: 6px 9px;
1579 font-size: 9px;
1580 }
1581
1582 .landing-section {
1583 padding: 88px 0;
1584 }
1585
1586 .landing-section-heading {
1587 margin-bottom: 38px;
1588 }
1589
1590 .landing-section-heading h2,
1591 .landing-final-card h2 {
1592 font-size: clamp(2.15rem, 10vw, 3.25rem);
1593 }
1594
1595 .landing-section-heading > p {
1596 font-size: 15px;
1597 }
1598
1599 .landing-value-grid {
1600 grid-template-columns: 1fr;
1601 }
1602
1603 .landing-value-card {
1604 min-height: 260px;
1605 padding: 26px 4px;
1606 }
1607
1608 .landing-value-card + .landing-value-card {
1609 border-top: 1px solid var(--landing-line);
1610 border-left: 0;
1611 }
1612
1613 .landing-value-card h3 {
1614 margin-top: 42px;
1615 }
1616
1617 .landing-feature-grid {
1618 grid-template-columns: 1fr;
1619 }
1620
1621 .landing-feature-repo,
1622 .landing-feature-team,
1623 .landing-feature-ai,
1624 .landing-feature-board {
1625 grid-column: span 1;
1626 }
1627
1628 .landing-feature-card {
1629 min-height: 420px;
1630 padding: 26px;
1631 }
1632
1633 .landing-repo-visual,
1634 .landing-ai-visual,
1635 .landing-board-visual,
1636 .landing-team-visual {
1637 right: 20px;
1638 left: 20px;
1639 }
1640
1641 .landing-case {
1642 grid-template-columns: 34px minmax(0, 1fr);
1643 gap: 12px;
1644 }
1645
1646 .landing-case-arrow {
1647 display: none;
1648 }
1649
1650 .landing-final-section {
1651 padding: 64px 0;
1652 }
1653
1654 .landing-final-card {
1655 grid-template-columns: 1fr;
1656 align-items: start;
1657 gap: 30px;
1658 border-radius: 16px;
1659 padding: 36px 24px;
1660 }
1661
1662 .landing-final-actions {
1663 min-width: 0;
1664 }
1665
1666 .landing-footer {
1667 padding: 40px 0;
1668 }
1669
1670 .landing-footer-grid {
1671 grid-template-columns: 1fr;
1672 gap: 32px;
1673 }
1674
1675 .landing-footer-meta {
1676 align-items: flex-start;
1677 }
1678}
1679
1680@media (max-width: 390px) {
1681 .landing-header .landing-brand-name {
1682 display: none;
1683 }
1684
1685 .landing-hero h1 {
1686 font-size: 2.62rem;
1687 }
1688
1689 .landing-feature-card {
1690 padding: 22px;
1691 }
1692
1693 .landing-repo-visual,
1694 .landing-ai-visual,
1695 .landing-board-visual,
1696 .landing-team-visual {
1697 right: 14px;
1698 left: 14px;
1699 }
1700}
1701
1702html.landing-scroll {
1703 scroll-behavior: smooth;
1704 scroll-padding-top: 94px;
1705 scrollbar-gutter: stable;
1706}
1707
1708.landing-page {
1709 touch-action: pan-y pinch-zoom;
1710}
1711
1712.landing-header {
1713 backface-visibility: hidden;
1714 transform: translate3d(0, 0, 0);
1715 will-change: transform;
1716}
1717
1718.landing-demo-glow,
1719.landing-demo-figure::after {
1720 will-change: opacity, transform;
1721}
1722
1723@media (max-width: 760px) {
1724 html.landing-scroll {
1725 scroll-padding-top: 82px;
1726 }
1727}
1728
1729@media (prefers-reduced-motion: reduce) {
1730 html.landing-scroll {
1731 scroll-behavior: auto;
1732 }
1733
1734 .landing-page *,
1735 .landing-page *::before,
1736 .landing-page *::after {
1737 scroll-behavior: auto !important;
1738 animation-duration: 0.01ms !important;
1739 animation-iteration-count: 1 !important;
1740 transition-duration: 0.01ms !important;
1741 }
1742}
1743
1744@media (forced-colors: active) {
1745 .landing-gradient-text {
1746 color: CanvasText;
1747 }
1748
1749 .landing-status-dot,
1750 .landing-demo-live span,
1751 .landing-ai-pulse {
1752 border: 1px solid CanvasText;
1753 }
1754}
1755
1756.landing-page {
1757 --landing-bg: #f6f6f2;
1758 --landing-surface: #ffffff;
1759 --landing-surface-raised: #ffffff;
1760 --landing-surface-soft: #eeeeea;
1761 --landing-line: #d9d9d2;
1762 --landing-line-soft: #e8e8e2;
1763 --landing-text: #242421;
1764 --landing-muted: #666762;
1765 --landing-muted-strong: #444540;
1766 --landing-accent: #ff8400;
1767 --landing-accent-soft: #c95f00;
1768 --landing-dark: #232321;
1769 background:
1770 radial-gradient(circle at 8% 12%, rgb(255 132 0 / 8%), transparent 18rem),
1771 radial-gradient(circle at 91% 18%, rgb(88 168 255 / 10%), transparent 15rem),
1772 var(--landing-bg);
1773 color: var(--landing-text);
1774}
1775
1776.landing-container {
1777 width: min(calc(100% - 64px), 1176px);
1778}
1779
1780.landing-header {
1781 inset: 14px 16px auto;
1782 border: 1px solid rgb(35 35 33 / 7%);
1783 border-radius: 999px;
1784 background: rgb(255 255 255 / 88%);
1785 box-shadow: 0 14px 42px rgb(35 35 33 / 10%);
1786 backdrop-filter: blur(18px);
1787}
1788
1789.landing-header-inner {
1790 width: 100%;
1791 min-height: 58px;
1792 padding-inline: 20px;
1793}
1794
1795.landing-brand {
1796 color: var(--landing-text);
1797}
1798
1799.landing-nav a,
1800.landing-login-link {
1801 color: #5e5f5a;
1802}
1803
1804.landing-nav a:hover,
1805.landing-login-link:hover {
1806 color: #171715;
1807}
1808
1809.landing-button-primary,
1810.landing-header-cta {
1811 background: #242421;
1812 color: #ffffff !important;
1813 box-shadow: 0 6px 18px rgb(35 35 33 / 12%);
1814}
1815
1816.landing-button-primary:hover,
1817.landing-header-cta:hover {
1818 background: #050505;
1819}
1820
1821.landing-button-secondary {
1822 border-color: #cfcfc8;
1823 background: rgb(255 255 255 / 72%);
1824 color: #242421;
1825}
1826
1827.landing-button-secondary:hover {
1828 border-color: #a7a7a0;
1829 background: #ffffff;
1830}
1831
1832.landing-skip {
1833 background: #242421;
1834 color: #ffffff;
1835}
1836
1837.landing-hero {
1838 z-index: 0;
1839 padding: 132px 0 112px;
1840}
1841
1842.landing-hero::before {
1843 inset: 0;
1844 z-index: -2;
1845 background-image:
1846 linear-gradient(rgb(35 35 33 / 4%) 1px, transparent 1px),
1847 linear-gradient(90deg, rgb(35 35 33 / 4%) 1px, transparent 1px);
1848 background-size: 82px 82px;
1849 mask-image: linear-gradient(to bottom, black 8%, transparent 74%);
1850}
1851
1852.landing-hero::after {
1853 position: absolute;
1854 top: 230px;
1855 right: -48px;
1856 z-index: -1;
1857 width: 180px;
1858 height: 34px;
1859 border-radius: 999px;
1860 background: linear-gradient(90deg, #ffd199, #79b7ff);
1861 box-shadow: 0 18px 32px rgb(35 35 33 / 16%);
1862 content: '';
1863 filter: blur(3px);
1864 transform: rotate(9deg);
1865}
1866
1867.landing-hero-grid {
1868 grid-template-columns: minmax(0, 1fr);
1869 gap: 64px;
1870}
1871
1872.landing-hero-copy {
1873 display: flex;
1874 max-width: 1080px;
1875 flex-direction: column;
1876 align-items: center;
1877 margin-inline: auto;
1878 text-align: center;
1879}
1880
1881.landing-eyebrow {
1882 margin-bottom: 20px;
1883 border: 1px solid #d8d8d1;
1884 border-radius: 999px;
1885 background: rgb(255 255 255 / 70%);
1886 padding: 8px 12px;
1887 color: #555650;
1888 box-shadow: 0 4px 16px rgb(35 35 33 / 5%);
1889}
1890
1891.landing-status-dot {
1892 background: var(--landing-accent);
1893 box-shadow: 0 0 0 5px rgb(255 132 0 / 12%);
1894}
1895
1896.landing-hero h1 {
1897 position: relative;
1898 width: 100%;
1899 max-width: 1040px;
1900 margin: 0;
1901 border: 1px solid rgb(255 132 0 / 62%);
1902 background:
1903 linear-gradient(var(--landing-accent), var(--landing-accent)) left -1px top -1px / 9px 9px no-repeat,
1904 linear-gradient(var(--landing-accent), var(--landing-accent)) right -1px top -1px / 9px 9px no-repeat,
1905 linear-gradient(var(--landing-accent), var(--landing-accent)) left -1px bottom -1px / 9px 9px no-repeat,
1906 linear-gradient(var(--landing-accent), var(--landing-accent)) right -1px bottom -1px / 9px 9px no-repeat;
1907 padding: 27px 34px 31px;
1908 color: #242421;
1909 font-size: clamp(2.55rem, 4.1vw, 3.25rem);
1910 font-weight: 720;
1911 letter-spacing: -0.062em;
1912 line-height: 1.02;
1913 text-wrap: balance;
1914}
1915
1916.landing-gradient-text {
1917 width: fit-content;
1918 max-width: 100%;
1919 margin: 15px auto 0;
1920 background: linear-gradient(transparent 50%, #ffd9b1 50%, #ffd9b1 88%, transparent 88%);
1921 background-clip: border-box;
1922 color: #242421;
1923 font-size: 0.58em;
1924 font-weight: 670;
1925 letter-spacing: -0.035em;
1926 line-height: 1.28;
1927}
1928
1929.landing-keep {
1930 white-space: normal;
1931}
1932
1933.landing-hero-body {
1934 max-width: 720px;
1935 margin: 24px auto 0;
1936 color: #656660;
1937 font-size: 16px;
1938 line-height: 1.72;
1939}
1940
1941
1942.landing-hero-actions {
1943 justify-content: center;
1944 margin-top: 30px;
1945}
1946
1947.landing-hero-actions .landing-button-primary::before {
1948 display: grid;
1949 width: 28px;
1950 height: 28px;
1951 place-items: center;
1952 border-radius: 50%;
1953 background: var(--landing-accent);
1954 color: #111111;
1955 content: '↓';
1956 font-family: var(--font-mono);
1957 font-size: 14px;
1958}
1959
1960.landing-hero-actions .landing-button-primary > svg {
1961 display: none;
1962}
1963
1964.landing-hero-note {
1965 justify-content: center;
1966 margin-top: 17px;
1967 color: #777872;
1968}
1969
1970.landing-demo-figure {
1971 width: min(100%, 1080px);
1972 margin-inline: auto;
1973}
1974
1975.landing-demo-glow {
1976 inset: 14% 8% -5%;
1977 background: rgb(255 132 0 / 17%);
1978 filter: blur(90px);
1979}
1980
1981.landing-demo {
1982 border: 3px solid #242421;
1983 border-radius: 20px;
1984 box-shadow: 0 32px 70px rgb(35 35 33 / 20%);
1985}
1986
1987.landing-demo-figure::after {
1988 position: absolute;
1989 right: 3%;
1990 bottom: -20px;
1991 left: 3%;
1992 z-index: 4;
1993 height: 62px;
1994 background: linear-gradient(to bottom, transparent, var(--landing-bg) 72%);
1995 content: '';
1996 filter: blur(5px);
1997}
1998
1999.landing-capabilities {
2000 display: none;
2001}
2002
2003.landing-section {
2004 padding: 122px 0;
2005}
2006
2007.landing-section-soft {
2008 border-block-color: #e3e3dd;
2009 background: #ffffff;
2010}
2011
2012.landing-section-heading {
2013 max-width: 820px;
2014 margin: 0 auto 52px;
2015 text-align: center;
2016}
2017
2018.landing-section-heading-wide {
2019 max-width: 900px;
2020}
2021
2022.landing-kicker {
2023 width: fit-content;
2024 margin: 0 auto 17px;
2025 border-radius: 999px;
2026 background: #ffead4;
2027 padding: 8px 11px;
2028 color: #a94f00;
2029}
2030
2031.landing-section-heading h2,
2032.landing-final-card h2 {
2033 color: #242421;
2034 font-size: clamp(2.45rem, 4.5vw, 4.25rem);
2035 font-weight: 700;
2036 letter-spacing: -0.06em;
2037}
2038
2039.landing-section-heading > p {
2040 margin-inline: auto;
2041 color: #666762;
2042}
2043
2044.landing-value-grid {
2045 gap: 14px;
2046 border: 0;
2047}
2048
2049.landing-value-card {
2050 min-height: 328px;
2051 border: 1px solid #ddddD7;
2052 border-radius: 24px;
2053 background: #ffffff;
2054 box-shadow: 0 16px 40px rgb(35 35 33 / 6%);
2055}
2056
2057.landing-value-card + .landing-value-card {
2058 border-left: 1px solid #ddddD7;
2059}
2060
2061.landing-value-card:first-child {
2062 border-color: #242421;
2063 background: #242421;
2064 color: #ffffff;
2065}
2066
2067.landing-value-card:first-child .landing-icon-box {
2068 border-color: var(--landing-accent);
2069 background: var(--landing-accent);
2070 color: #161616;
2071}
2072
2073.landing-value-card:first-child .landing-card-index,
2074.landing-value-card:first-child p {
2075 color: #c8c8c2;
2076}
2077
2078.landing-icon-box {
2079 border-color: #e6c39f;
2080 background: #ffead4;
2081 color: #a94f00;
2082}
2083
2084.landing-card-index {
2085 color: #858680;
2086}
2087
2088.landing-value-card h3,
2089.landing-feature-copy h3,
2090.landing-case h3 {
2091 color: inherit;
2092}
2093
2094.landing-value-card p,
2095.landing-feature-copy p,
2096.landing-case p {
2097 color: #696a64;
2098}
2099
2100.landing-feature-grid {
2101 gap: 18px;
2102}
2103
2104.landing-feature-card {
2105 min-height: 470px;
2106 border-color: #ddddD7;
2107 border-radius: 26px;
2108 background: #f7f7f3;
2109 box-shadow: 0 18px 52px rgb(35 35 33 / 7%);
2110}
2111
2112.landing-feature-card:nth-child(2),
2113.landing-feature-card:nth-child(3) {
2114 background: #fff8f0;
2115}
2116
2117.landing-feature-card:hover {
2118 border-color: #ffb566;
2119 box-shadow: 0 24px 62px rgb(35 35 33 / 10%);
2120}
2121
2122.landing-feature-label {
2123 border-radius: 999px;
2124 background: #ffead4;
2125 padding: 7px 10px;
2126 color: #a94f00;
2127}
2128
2129.landing-feature-copy h3 {
2130 color: #242421;
2131}
2132
2133.landing-cases-layout {
2134 grid-template-columns: minmax(300px, 0.76fr) minmax(0, 1.24fr);
2135}
2136
2137.landing-cases-heading {
2138 margin: 0;
2139 text-align: left;
2140}
2141
2142.landing-cases-heading .landing-kicker {
2143 margin-left: 0;
2144}
2145
2146.landing-cases-heading > p {
2147 margin-left: 0;
2148}
2149
2150.landing-case-list {
2151 overflow: hidden;
2152 border: 1px solid #ddddD7;
2153 border-radius: 24px;
2154 background: #ffffff;
2155 padding-inline: 28px;
2156 box-shadow: 0 16px 44px rgb(35 35 33 / 5%);
2157}
2158
2159.landing-case {
2160 grid-template-columns: 44px minmax(0, 1fr) 24px;
2161 align-items: start;
2162 border-bottom-color: #e5e5df;
2163 padding: 30px 0;
2164}
2165
2166.landing-case-number {
2167 display: grid;
2168 width: 38px;
2169 height: 38px;
2170 place-items: center;
2171 border-radius: 999px;
2172 background: #242421;
2173 color: #ffffff;
2174 font-size: 9px;
2175}
2176
2177.landing-case:first-child .landing-case-number {
2178 background: var(--landing-accent);
2179 color: #161616;
2180}
2181
2182.landing-case-outcome {
2183 color: #383934;
2184}
2185
2186.landing-case-arrow {
2187 color: #92938c;
2188}
2189
2190.landing-faq-layout {
2191 width: min(100%, 920px);
2192 grid-template-columns: 1fr;
2193 gap: 44px;
2194 margin-inline: auto;
2195}
2196
2197.landing-faq-heading {
2198 max-width: 760px;
2199 margin: 0 auto;
2200 text-align: center;
2201}
2202
2203.landing-faq-list {
2204 border-top: 2px solid #242421;
2205}
2206
2207.landing-faq-item {
2208 border-bottom-color: #d8d8d2;
2209}
2210
2211.landing-faq-trigger {
2212 min-height: 90px;
2213 grid-template-columns: 48px minmax(0, 1fr) 26px;
2214 color: #242421;
2215 font-size: 17px;
2216 font-weight: 650;
2217}
2218
2219.landing-faq-number {
2220 color: #a5a69f;
2221 font-family: var(--font-mono);
2222 font-size: 10px;
2223 letter-spacing: 0.08em;
2224}
2225
2226.landing-faq-trigger i::before,
2227.landing-faq-trigger i::after {
2228 background: #242421;
2229}
2230
2231.landing-faq-item.is-open .landing-faq-trigger {
2232 color: #9d4900;
2233}
2234
2235.landing-faq-panel p {
2236 margin-left: 48px;
2237 color: #666762;
2238}
2239
2240.landing-final-section {
2241 background: #f6f6f2;
2242}
2243
2244.landing-final-card {
2245 background: #242421;
2246 color: #ffffff;
2247 box-shadow: 0 28px 70px rgb(35 35 33 / 18%);
2248}
2249
2250.landing-final-card h2 {
2251 color: #ffffff;
2252}
2253
2254.landing-final-card p {
2255 color: #bdbdb7;
2256}
2257
2258.landing-final-grid {
2259 background-image:
2260 linear-gradient(rgb(255 255 255 / 6%) 1px, transparent 1px),
2261 linear-gradient(90deg, rgb(255 255 255 / 6%) 1px, transparent 1px);
2262}
2263
2264.landing-final-kicker {
2265 color: var(--landing-accent);
2266}
2267
2268.landing-button-dark {
2269 background: var(--landing-accent);
2270 color: #141414;
2271}
2272
2273.landing-button-dark:hover {
2274 background: #ff9d39;
2275}
2276
2277.landing-final-login {
2278 color: #ffffff !important;
2279}
2280
2281.landing-footer {
2282 border-top-color: #deded8;
2283 background: #f6f6f2;
2284}
2285
2286.landing-footer-grid > div:first-child > p,
2287.landing-footer-links a,
2288.landing-footer-meta {
2289 color: #6f706a;
2290}
2291
2292.landing-footer-links a:hover {
2293 color: #171715;
2294}
2295
2296.landing-footer-meta button {
2297 border-color: #d3d3cc;
2298 background: #ffffff;
2299 color: #555650;
2300}
2301
2302.landing-footer-meta button:hover {
2303 border-color: #a9aaa3;
2304 color: #171715;
2305}
2306
2307@media (max-width: 1040px) {
2308 .landing-header {
2309 inset-inline: 14px;
2310 }
2311
2312 .landing-hero h1 {
2313 max-width: 900px;
2314 }
2315
2316 .landing-value-grid {
2317 gap: 12px;
2318 }
2319
2320 .landing-value-card {
2321 padding: 28px;
2322 }
2323
2324 .landing-cases-layout {
2325 grid-template-columns: 1fr;
2326 }
2327
2328 .landing-cases-heading {
2329 max-width: 720px;
2330 margin-inline: auto;
2331 text-align: center;
2332 }
2333
2334 .landing-cases-heading .landing-kicker,
2335 .landing-cases-heading > p {
2336 margin-inline: auto;
2337 }
2338}
2339
2340@media (max-width: 760px) {
2341 .landing-container {
2342 width: min(calc(100% - 28px), 1176px);
2343 }
2344
2345 .landing-header {
2346 inset: 10px 10px auto;
2347 }
2348
2349 .landing-header-inner {
2350 min-height: 56px;
2351 padding-inline: 12px;
2352 }
2353
2354 .landing-header .landing-brand-name {
2355 display: inline;
2356 }
2357
2358 .landing-brand {
2359 font-size: 14px;
2360 }
2361
2362 .landing-login-link {
2363 padding-inline: 5px;
2364 }
2365
2366 .landing-header-cta {
2367 border-radius: 999px;
2368 padding-inline: 13px;
2369 }
2370
2371 .landing-hero {
2372 padding: 108px 0 78px;
2373 }
2374
2375 .landing-hero::before {
2376 top: 0;
2377 background-size: 54px 54px;
2378 }
2379
2380 .landing-hero::after {
2381 top: 120px;
2382 right: -68px;
2383 width: 120px;
2384 height: 24px;
2385 }
2386
2387 .landing-hero-grid {
2388 gap: 46px;
2389 }
2390
2391 .landing-eyebrow {
2392 margin-bottom: 16px;
2393 padding: 7px 10px;
2394 font-size: 8px;
2395 }
2396
2397 .landing-hero h1 {
2398 padding: 20px 16px 23px;
2399 font-size: clamp(1.75rem, 8.4vw, 2.18rem);
2400 letter-spacing: -0.055em;
2401 line-height: 1.04;
2402 }
2403
2404 .landing-gradient-text {
2405 margin-top: 13px;
2406 font-size: 0.72em;
2407 line-height: 1.22;
2408 }
2409
2410 .landing-hero-body {
2411 margin-top: 20px;
2412 font-size: 14px;
2413 }
2414
2415
2416 .landing-hero-actions {
2417 width: min(100%, 270px);
2418 margin-top: 27px;
2419 }
2420
2421 .landing-hero-note {
2422 font-size: 10px;
2423 }
2424
2425 .landing-demo {
2426 border-width: 2px;
2427 }
2428
2429 .landing-section {
2430 padding: 92px 0;
2431 }
2432
2433 .landing-section-heading h2,
2434 .landing-final-card h2 {
2435 font-size: clamp(2.1rem, 10vw, 3rem);
2436 }
2437
2438 .landing-value-grid {
2439 gap: 10px;
2440 }
2441
2442 .landing-value-card,
2443 .landing-value-card + .landing-value-card {
2444 min-height: 250px;
2445 border: 1px solid #ddddD7;
2446 border-radius: 20px;
2447 padding: 26px;
2448 }
2449
2450 .landing-value-card:first-child {
2451 border-color: #242421;
2452 }
2453
2454 .landing-feature-card {
2455 min-height: 430px;
2456 border-radius: 22px;
2457 }
2458
2459 .landing-case-list {
2460 border-radius: 20px;
2461 padding-inline: 20px;
2462 }
2463
2464 .landing-case {
2465 grid-template-columns: 38px minmax(0, 1fr);
2466 }
2467
2468 .landing-case-number {
2469 width: 34px;
2470 height: 34px;
2471 }
2472
2473 .landing-faq-layout {
2474 gap: 36px;
2475 }
2476
2477 .landing-faq-trigger {
2478 min-height: 82px;
2479 grid-template-columns: 34px minmax(0, 1fr) 22px;
2480 gap: 10px;
2481 font-size: 15px;
2482 }
2483
2484 .landing-faq-panel p {
2485 margin: -2px 34px 24px 34px;
2486 font-size: 13px;
2487 }
2488
2489 .landing-final-card {
2490 border-radius: 20px;
2491 }
2492}
2493
2494@media (max-width: 390px) {
2495 .landing-header .landing-brand-name {
2496 display: inline;
2497 }
2498
2499 .landing-brand {
2500 gap: 6px;
2501 font-size: 13px;
2502 }
2503
2504 .landing-header-actions {
2505 gap: 4px;
2506 }
2507
2508 .landing-login-link {
2509 font-size: 11px;
2510 }
2511
2512 .landing-header-cta {
2513 padding-inline: 10px;
2514 font-size: 10px;
2515 }
2516
2517 .landing-feature-card {
2518 padding: 22px;
2519 }
2520}
Added web/src/pages/landing/ui/landing-page.tsx View web/src/pages/landing/ui/landing-page.tsx
@@ -0,0 +1,404 @@
1import { useEffect, useState } from 'react'
2import { Link } from 'react-router-dom'
3import {
4 ArrowRight,
5 ArrowUpRight,
6 Bot,
7 Boxes,
8 Check,
9 Code2,
10 GitBranch,
11 GitPullRequest,
12 Languages,
13 LayoutDashboard,
14 MessageSquareMore,
15 Mic2,
16 Sparkles,
17} from 'lucide-react'
18import { KernelMark } from '@/shared/brand'
19import { localeLabel, setLocale, useT } from '@/shared/i18n'
20import './landing-page.css'
21
22const NAV_ITEMS = [
23 { href: '#value', label: 'landing.nav.value' },
24 { href: '#features', label: 'landing.nav.features' },
25 { href: '#use-cases', label: 'landing.nav.useCases' },
26 { href: '#faq', label: 'landing.nav.faq' },
27] as const
28
29const VALUE_CARDS = [
30 { icon: Boxes, title: 'landing.value.context.title', body: 'landing.value.context.body' },
31 { icon: Sparkles, title: 'landing.value.ai.title', body: 'landing.value.ai.body' },
32 { icon: MessageSquareMore, title: 'landing.value.rhythm.title', body: 'landing.value.rhythm.body' },
33] as const
34
35const USE_CASES = [
36 { number: '01', title: 'landing.case.product.title', body: 'landing.case.product.body', outcome: 'landing.case.product.outcome' },
37 { number: '02', title: 'landing.case.open.title', body: 'landing.case.open.body', outcome: 'landing.case.open.outcome' },
38 { number: '03', title: 'landing.case.distributed.title', body: 'landing.case.distributed.body', outcome: 'landing.case.distributed.outcome' },
39] as const
40
41const FAQS = [
42 ['landing.faq.one.question', 'landing.faq.one.answer'],
43 ['landing.faq.two.question', 'landing.faq.two.answer'],
44 ['landing.faq.three.question', 'landing.faq.three.answer'],
45 ['landing.faq.four.question', 'landing.faq.four.answer'],
46 ['landing.faq.five.question', 'landing.faq.five.answer'],
47 ['landing.faq.six.question', 'landing.faq.six.answer'],
48] as const
49
50export function LandingPage() {
51 const { locale, t } = useT()
52 const [openFaq, setOpenFaq] = useState<number | null>(null)
53
54 useEffect(() => {
55 const previousTitle = document.title
56 const description = document.querySelector<HTMLMetaElement>('meta[name="description"]')
57 const previousDescription = description?.content
58 document.documentElement.classList.add('landing-scroll')
59 document.title = t('landing.meta.title')
60 if (description) description.content = t('landing.meta.description')
61 return () => {
62 document.documentElement.classList.remove('landing-scroll')
63 document.title = previousTitle
64 if (description && previousDescription) description.content = previousDescription
65 }
66 }, [t])
67
68 return (
69 <div className="landing-page">
70 <a className="landing-skip" href="#landing-main">
71 {t('landing.skip')}
72 </a>
73
74 <header className="landing-header">
75 <div className="landing-container landing-header-inner">
76 <Link to="/" className="landing-brand" aria-label={t('landing.brand.aria')}>
77 <KernelMark size={30} radius={8} />
78 <span className="landing-brand-name">GitDataAI</span>
79 </Link>
80
81 <nav className="landing-nav" aria-label={t('landing.nav.aria')}>
82 {NAV_ITEMS.map(item => (
83 <Link
84 key={item.href}
85 to={item.href}
86 onClick={() => requestAnimationFrame(() => document.querySelector(item.href)?.scrollIntoView())}
87 >
88 {t(item.label)}
89 </Link>
90 ))}
91 </nav>
92
93 <div className="landing-header-actions">
94 <Link to="/auth/login" className="landing-login-link">
95 {t('landing.nav.login')}
96 </Link>
97 <Link to="/auth/signup" className="landing-button landing-button-primary landing-header-cta">
98 {t('landing.nav.start')}
99 <ArrowUpRight size={15} aria-hidden />
100 </Link>
101 </div>
102 </div>
103 </header>
104
105 <main id="landing-main">
106 <section className="landing-hero">
107 <div className="landing-container landing-hero-grid">
108 <div className="landing-hero-copy">
109 <div className="landing-eyebrow">
110 <span className="landing-status-dot" aria-hidden />
111 {t('landing.hero.eyebrow')}
112 </div>
113 <h1>
114 <span>
115 {t('landing.hero.titleLeadStart')}
116 <span className={locale === 'zh-CN' ? 'landing-keep' : undefined}>{t('landing.hero.titleLeadEnd')}</span>
117 </span>
118 <span className="landing-gradient-text">
119 {t('landing.hero.titleAccentStart')}
120 <span className={locale === 'zh-CN' ? 'landing-keep' : undefined}>{t('landing.hero.titleAccentEnd')}</span>
121 </span>
122 </h1>
123 <p className="landing-hero-body">{t('landing.hero.body')}</p>
124 <div className="landing-hero-actions">
125 <Link to="/auth/signup" className="landing-button landing-button-primary landing-button-large">
126 {t('landing.hero.primary')}
127 <ArrowRight size={17} aria-hidden />
128 </Link>
129 <a href="#features" className="landing-button landing-button-secondary landing-button-large">
130 {t('landing.hero.secondary')}
131 </a>
132 </div>
133 <p className="landing-hero-note">
134 <Check size={14} aria-hidden />
135 {t('landing.hero.note')}
136 </p>
137 </div>
138
139 <figure className="landing-demo-figure">
140 <div className="landing-demo-glow" aria-hidden />
141 <div className="landing-demo" aria-hidden>
142 <div className="landing-demo-topbar">
143 <div className="landing-window-dots"><span /><span /><span /></div>
144 <div className="landing-demo-path"><Code2 size={13} /> acme / atlas</div>
145 <div className="landing-demo-live"><span />{t('landing.demo.status')}</div>
146 </div>
147 <div className="landing-demo-body">
148 <aside className="landing-demo-sidebar">
149 <div className="landing-demo-space">A</div>
150 <div className="landing-demo-nav is-active"><Code2 size={15} />{t('landing.demo.code')}</div>
151 <div className="landing-demo-nav"><LayoutDashboard size={15} />{t('landing.demo.issues')}<b>12</b></div>
152 <div className="landing-demo-nav"><GitPullRequest size={15} />{t('landing.demo.reviews')}<b>3</b></div>
153 <div className="landing-demo-nav"><MessageSquareMore size={15} />{t('landing.demo.team')}</div>
154 </aside>
155 <div className="landing-demo-canvas">
156 <div className="landing-demo-toolbar">
157 <div><span className="landing-mono">{t('landing.demo.repo')}</span><strong>atlas</strong></div>
158 <div className="landing-branch"><GitBranch size={12} />{t('landing.demo.branch')}</div>
159 </div>
160 <div className="landing-demo-content">
161 <div className="landing-pr-heading">
162 <div>
163 <span>{t('landing.demo.pr')}</span>
164 <h3>{t('landing.demo.prTitle')}</h3>
165 </div>
166 <span className="landing-change-chip">+184 · −47</span>
167 </div>
168 <div className="landing-file-row"><span className="landing-file-status">M</span><span>src/agent/context.rs</span><span>+72 −14</span></div>
169 <div className="landing-file-row"><span className="landing-file-status is-new">A</span><span>src/search/rerank.rs</span><span>+96 −0</span></div>
170 <div className="landing-file-row"><span className="landing-file-status">M</span><span>README.md</span><span>+16 −33</span></div>
171 <div className="landing-diff">
172 <div className="landing-diff-line is-muted"><i>41</i><code>pub async fn hydrate_context(</code></div>
173 <div className="landing-diff-line is-removed"><i>42</i><code>- cache.get(&amp;key).await?</code></div>
174 <div className="landing-diff-line is-added"><i>42</i><code>+ context.load(&amp;key, scope).await?</code></div>
175 <div className="landing-diff-line is-added"><i>43</i><code>+ tracing::info!(target: "agent", %scope);</code></div>
176 </div>
177 </div>
178 <div className="landing-review-card">
179 <div className="landing-review-head">
180 <span><Bot size={15} />{t('landing.demo.review')}</span>
181 <span className="landing-review-score">02</span>
182 </div>
183 <strong>{t('landing.demo.reviewTitle')}</strong>
184 <div className="landing-review-item"><span>01</span>{t('landing.demo.riskOne')}</div>
185 <div className="landing-review-item"><span>02</span>{t('landing.demo.riskTwo')}</div>
186 <div className="landing-review-online">
187 <div className="landing-avatars"><span>YL</span><span>AM</span><span>SK</span></div>
188 {t('landing.demo.online')}
189 </div>
190 </div>
191 </div>
192 </div>
193 </div>
194 <figcaption className="sr-only">{t('landing.demo.caption')}</figcaption>
195 </figure>
196 </div>
197 </section>
198
199 <section id="value" className="landing-section">
200 <div className="landing-container">
201 <div className="landing-section-heading">
202 <span className="landing-kicker">{t('landing.values.kicker')}</span>
203 <h2>{t('landing.values.title')}</h2>
204 <p>{t('landing.values.body')}</p>
205 </div>
206 <div className="landing-value-grid">
207 {VALUE_CARDS.map(({ icon: Icon, title, body }, index) => (
208 <article className="landing-value-card" key={title}>
209 <div className="landing-value-card-top">
210 <span className="landing-icon-box"><Icon size={19} aria-hidden /></span>
211 <span className="landing-card-index">0{index + 1}</span>
212 </div>
213 <h3>{t(title)}</h3>
214 <p>{t(body)}</p>
215 </article>
216 ))}
217 </div>
218 </div>
219 </section>
220
221 <section id="features" className="landing-section landing-section-soft">
222 <div className="landing-container">
223 <div className="landing-section-heading landing-section-heading-wide">
224 <span className="landing-kicker">{t('landing.features.kicker')}</span>
225 <h2>{t('landing.features.title')}</h2>
226 <p>{t('landing.features.body')}</p>
227 </div>
228
229 <div className="landing-feature-grid">
230 <article className="landing-feature-card landing-feature-repo">
231 <div className="landing-feature-copy">
232 <span className="landing-feature-label"><Code2 size={15} aria-hidden />{t('landing.feature.repo.label')}</span>
233 <h3>{t('landing.feature.repo.title')}</h3>
234 <p>{t('landing.feature.repo.body')}</p>
235 </div>
236 <div className="landing-repo-visual" aria-hidden>
237 <div className="landing-repo-visual-head"><span>atlas / main</span><span>8f31c2a</span></div>
238 <div><GitBranch size={13} /><span>src</span><em>{t('landing.feature.repo.updated')}</em></div>
239 <div><Code2 size={13} /><span>agent/context.rs</span><em>4m</em></div>
240 <div><Code2 size={13} /><span>search/rerank.rs</span><em>8m</em></div>
241 <div><span className="landing-file-dot">·</span><span>README.md</span><em>12m</em></div>
242 </div>
243 </article>
244
245 <article className="landing-feature-card landing-feature-ai">
246 <div className="landing-feature-copy">
247 <span className="landing-feature-label"><Sparkles size={15} aria-hidden />{t('landing.feature.ai.label')}</span>
248 <h3>{t('landing.feature.ai.title')}</h3>
249 <p>{t('landing.feature.ai.body')}</p>
250 </div>
251 <div className="landing-ai-visual" aria-hidden>
252 <div><span className="landing-ai-pulse" /><b>{t('landing.feature.ai.scanning')}</b><span>84%</span></div>
253 <div className="landing-ai-bar"><span /></div>
254 <p><Check size={13} />{t('landing.feature.ai.signalOne')}</p>
255 <p><Check size={13} />{t('landing.feature.ai.signalTwo')}</p>
256 </div>
257 </article>
258
259 <article className="landing-feature-card landing-feature-board">
260 <div className="landing-feature-copy">
261 <span className="landing-feature-label"><LayoutDashboard size={15} aria-hidden />{t('landing.feature.board.label')}</span>
262 <h3>{t('landing.feature.board.title')}</h3>
263 <p>{t('landing.feature.board.body')}</p>
264 </div>
265 <div className="landing-board-visual" aria-hidden>
266 <div><span>{t('landing.feature.board.todo')}</span><i /><i /></div>
267 <div><span>{t('landing.feature.board.progress')}</span><i /><i className="is-orange" /></div>
268 <div><span>{t('landing.feature.board.done')}</span><i className="is-done" /><i className="is-done" /></div>
269 </div>
270 </article>
271
272 <article className="landing-feature-card landing-feature-team">
273 <div className="landing-feature-copy">
274 <span className="landing-feature-label"><MessageSquareMore size={15} aria-hidden />{t('landing.feature.team.label')}</span>
275 <h3>{t('landing.feature.team.title')}</h3>
276 <p>{t('landing.feature.team.body')}</p>
277 </div>
278 <div className="landing-team-visual" aria-hidden>
279 <div className="landing-team-message"><span>AM</span><p><b>@team</b> {t('landing.feature.team.messageOne')}</p></div>
280 <div className="landing-team-message"><span>YL</span><p>{t('landing.feature.team.messageTwo')} <b>#142</b></p></div>
281 <div className="landing-voice-pill"><Mic2 size={13} />{t('landing.feature.team.voice')}<span>4</span></div>
282 </div>
283 </article>
284 </div>
285 </div>
286 </section>
287
288 <section id="use-cases" className="landing-section">
289 <div className="landing-container landing-cases-layout">
290 <div className="landing-section-heading landing-cases-heading">
291 <span className="landing-kicker">{t('landing.cases.kicker')}</span>
292 <h2>{t('landing.cases.title')}</h2>
293 <p>{t('landing.cases.body')}</p>
294 </div>
295 <div className="landing-case-list">
296 {USE_CASES.map(item => (
297 <article className="landing-case" key={item.number}>
298 <span className="landing-case-number">{item.number}</span>
299 <div>
300 <h3>{t(item.title)}</h3>
301 <p>{t(item.body)}</p>
302 <span className="landing-case-outcome"><Check size={14} aria-hidden />{t(item.outcome)}</span>
303 </div>
304 <ArrowUpRight size={18} className="landing-case-arrow" aria-hidden />
305 </article>
306 ))}
307 </div>
308 </div>
309 </section>
310
311 <section id="faq" className="landing-section landing-section-soft">
312 <div className="landing-container landing-faq-layout">
313 <div className="landing-section-heading landing-faq-heading">
314 <span className="landing-kicker">{t('landing.faq.kicker')}</span>
315 <h2>{t('landing.faq.title')}</h2>
316 <p>{t('landing.faq.body')}</p>
317 </div>
318 <div className="landing-faq-list">
319 {FAQS.map(([question, answer], index) => {
320 const expanded = openFaq === index
321 const answerId = `landing-faq-answer-${index}`
322 return (
323 <div className={`landing-faq-item${expanded ? ' is-open' : ''}`} key={question}>
324 <h3 className="landing-faq-item-heading">
325 <button
326 type="button"
327 className="landing-faq-trigger"
328 aria-expanded={expanded}
329 aria-controls={answerId}
330 onClick={() => setOpenFaq(expanded ? null : index)}
331 >
332 <span className="landing-faq-number">{String(index + 1).padStart(2, '0')}</span>
333 <span className="landing-faq-question">{t(question)}</span>
334 <i aria-hidden />
335 </button>
336 </h3>
337 <div className="landing-faq-panel" id={answerId} role="region" aria-hidden={!expanded}>
338 <div className="landing-faq-panel-inner">
339 <p>{t(answer)}</p>
340 </div>
341 </div>
342 </div>
343 )
344 })}
345 </div>
346 </div>
347 </section>
348
349 <section className="landing-final-section">
350 <div className="landing-container">
351 <div className="landing-final-card">
352 <div className="landing-final-grid" aria-hidden />
353 <div>
354 <span className="landing-final-kicker">{t('landing.cta.kicker')}</span>
355 <h2>{t('landing.cta.title')}</h2>
356 <p>{t('landing.cta.body')}</p>
357 </div>
358 <div className="landing-final-actions">
359 <Link to="/auth/signup" className="landing-button landing-button-dark landing-button-large">
360 {t('landing.cta.primary')}
361 <ArrowRight size={17} aria-hidden />
362 </Link>
363 <Link to="/auth/login" className="landing-final-login">
364 {t('landing.cta.secondary')}
365 </Link>
366 </div>
367 </div>
368 </div>
369 </section>
370 </main>
371
372 <footer className="landing-footer">
373 <div className="landing-container landing-footer-grid">
374 <div>
375 <Link to="/" className="landing-brand">
376 <KernelMark size={28} radius={7} />
377 <span className="landing-brand-name">GitDataAI</span>
378 </Link>
379 <p>{t('landing.footer.tagline')}</p>
380 </div>
381 <div className="landing-footer-links">
382 <a href="#features">{t('landing.nav.features')}</a>
383 <a href="#use-cases">{t('landing.nav.useCases')}</a>
384 <a href="#faq">{t('landing.nav.faq')}</a>
385 <Link to="/agreements/terms">{t('landing.footer.terms')}</Link>
386 <Link to="/agreements/privacy">{t('landing.footer.privacy')}</Link>
387 </div>
388 <div className="landing-footer-meta">
389 <button
390 type="button"
391 onClick={() => setLocale(locale === 'en' ? 'zh-CN' : 'en')}
392 aria-label={t('landing.footer.languageAria')}
393 >
394 <Languages size={15} aria-hidden />
395 {localeLabel(locale === 'en' ? 'zh-CN' : 'en')}
396 </button>
397 <span>© 2026 GitDataAI</span>
398 </div>
399 </div>
400 </footer>
401
402 </div>
403 )
404}
Added web/src/pages/landing/index.ts View web/src/pages/landing/index.ts
@@ -0,0 +1 @@
1export { LandingPage } from './ui/landing-page'
Modified web/src/pages/login/ui/login-page.tsx View web/src/pages/login/ui/login-page.tsx
@@ -38,7 +38,7 @@
3838 // Bind the local-first store to this account *before* the shell
3939 // hydrates: signing in as someone else wipes the previous cache.
4040 if (result?.user.user_id) await setCacheOwner(result.user.user_id)
41 navigate('/')
41 navigate('/app')
4242 }
4343 } catch (err) {
4444 setError(toUserError(err))
Modified web/src/pages/sign-up/ui/sign-up-page.tsx View web/src/pages/sign-up/ui/sign-up-page.tsx
@@ -220,7 +220,7 @@
220220 <span className="cursor-pointer" onClick={() => setTerms(v => !v)}>
221221 {t('auth.signup.termsPrefix')}{' '}
222222 <a
223 href="/pages/agreements/terms"
223 href="/agreements/terms"
224224 target="_blank"
225225 rel="noreferrer"
226226 className="font-bold underline-offset-4 hover:underline"
@@ -230,7 +230,7 @@
230230 </a>{' '}
231231 {t('auth.signup.and')}{' '}
232232 <a
233 href="/pages/agreements/privacy"
233 href="/agreements/privacy"
234234 target="_blank"
235235 rel="noreferrer"
236236 className="font-bold underline-offset-4 hover:underline"
Added web/src/shared/brand/index.ts View web/src/shared/brand/index.ts
@@ -0,0 +1 @@
1export { KernelMark, type KernelMarkProps } from './kernel-mark'
Added web/src/shared/brand/kernel-mark.tsx View web/src/shared/brand/kernel-mark.tsx
@@ -0,0 +1,25 @@
1export interface KernelMarkProps {
2 size?: number
3 radius?: number
4}
5
6/**
7 * The GitDataAI brand mark, served from `/favicon.png` so the header, footer,
8 * auth screens and the browser tab always show the same artwork.
9 *
10 * `radius` is kept in the signature because callers size the badge themselves;
11 * the image already carries its own transparent shape, so no clipping is
12 * applied by default.
13 */
14export function KernelMark({ size = 44 }: KernelMarkProps) {
15 return (
16 <img
17 src="/favicon.png"
18 alt=""
19 aria-hidden
20 draggable={false}
21 className="inline-block shrink-0 select-none object-contain"
22 style={{ width: size, height: size }}
23 />
24 )
25}
No newline at end of file
Modified web/src/shared/i18n/messages/en/index.ts View web/src/shared/i18n/messages/en/index.ts
@@ -6,6 +6,7 @@
66import { errors } from './errors'
77import { issueEvents, issues } from './issues'
88import { labels } from './labels'
9import { landing } from './landing'
910import { livekit } from './livekit'
1011import { misc } from './misc'
1112import { nav } from './nav'
@@ -27,6 +28,7 @@
2728 ...errors,
2829 ...shell,
2930 ...labels,
31 ...landing,
3032 ...livekit,
3133 ...settings,
3234 ...chat,
Added web/src/shared/i18n/messages/en/landing.ts View web/src/shared/i18n/messages/en/landing.ts
@@ -0,0 +1,118 @@
1export const landing = {
2 'landing.meta.title': 'GitDataAI — One context for code, collaboration, and AI',
3 'landing.meta.description': 'GitDataAI brings repositories, issues, pull requests, team communication, and AI code review into one engineering workspace.',
4 'landing.skip': 'Skip to main content',
5 'landing.brand.aria': 'GitDataAI home',
6 'landing.nav.aria': 'Primary navigation',
7 'landing.nav.value': 'Why GitDataAI',
8 'landing.nav.features': 'Product',
9 'landing.nav.useCases': 'Use cases',
10 'landing.nav.faq': 'FAQ',
11 'landing.nav.login': 'Sign in',
12 'landing.nav.start': 'Start free',
13 'landing.hero.eyebrow': 'The software workspace for the AI era',
14 'landing.hero.titleLeadStart': 'An AI engineering workspace ',
15 'landing.hero.titleLeadEnd': 'built for continuous delivery.',
16 'landing.hero.titleAccentStart': 'One shared context keeps ',
17 'landing.hero.titleAccentEnd': 'code, work, reviews, and conversations connected',
18 'landing.hero.body': 'GitDataAI connects the tools engineering teams use every day, so everyone works from the same context from the first commit to the final delivery.',
19 'landing.hero.primary': 'Start free',
20 'landing.hero.secondary': 'Explore the product',
21 'landing.hero.note': 'Container-ready deployment · Expand at your team’s pace',
22 'landing.demo.caption': 'A GitDataAI workspace showing code changes, AI review findings, and active collaborators together.',
23 'landing.demo.status': 'Live sync',
24 'landing.demo.repo': 'Repository',
25 'landing.demo.branch': 'main',
26 'landing.demo.code': 'Code',
27 'landing.demo.issues': 'Issues',
28 'landing.demo.reviews': 'Reviews',
29 'landing.demo.team': 'Team',
30 'landing.demo.pr': 'Pull Request #142',
31 'landing.demo.prTitle': 'Unify the retrieval context pipeline',
32 'landing.demo.review': 'AI review',
33 'landing.demo.reviewTitle': '2 risks need your attention',
34 'landing.demo.riskOne': 'The cache key is missing a space scope and may cross repositories.',
35 'landing.demo.riskTwo': 'The rerank request needs a timeout and fallback path.',
36 'landing.demo.online': '3 collaborators online',
37 'landing.values.kicker': 'Why GitDataAI',
38 'landing.values.title': 'Less tool switching. More forward motion.',
39 'landing.values.body': 'Reconnect the engineering context scattered across code hosting, task tracking, and team communication so everyone works from the same source of truth.',
40 'landing.value.context.title': 'Context that stays connected',
41 'landing.value.context.body': 'Code, issues, reviews, and discussions link together, so people stop reconstructing the story across disconnected systems.',
42 'landing.value.ai.title': 'AI inside the real workflow',
43 'landing.value.ai.body': 'From change summaries to review signals, AI works with the current repository and collaboration context instead of sitting in an isolated chat.',
44 'landing.value.rhythm.title': 'A continuous team rhythm',
45 'landing.value.rhythm.body': 'Boards, channels, threads, voice, and notifications organize around delivery goals so synchronous and asynchronous work flow together.',
46 'landing.features.kicker': 'Product capabilities',
47 'landing.features.title': 'From the first line of code to the final decision',
48 'landing.features.body': 'A complete work surface for engineering teams: manage code, move work forward, finish reviews, and keep every important discussion traceable.',
49 'landing.feature.repo.label': 'Code and repositories',
50 'landing.feature.repo.title': 'Familiar Git, with the full engineering view',
51 'landing.feature.repo.body': 'Browse branches, tags, file history, and diffs while managing code changes and repository collaboration in one interface.',
52 'landing.feature.repo.updated': 'Updated now',
53 'landing.feature.ai.label': 'AI-native review',
54 'landing.feature.ai.title': 'Understand the change before choosing the action',
55 'landing.feature.ai.body': 'Summarize commits and pull requests, surface risks worth human attention, and spend review time where judgment matters most.',
56 'landing.feature.ai.scanning': 'Analyzing change context',
57 'landing.feature.ai.signalOne': 'Cross-file impact connected',
58 'landing.feature.ai.signalTwo': 'Review signals prioritized',
59 'landing.feature.board.label': 'Issues and boards',
60 'landing.feature.board.title': 'Turn the plan into visible progress',
61 'landing.feature.board.body': 'Organize work with issues, labels, milestones, and boards while keeping every task connected to its code changes.',
62 'landing.feature.board.todo': 'To do',
63 'landing.feature.board.progress': 'In progress',
64 'landing.feature.board.done': 'Done',
65 'landing.feature.team.label': 'Team communication',
66 'landing.feature.team.title': 'Keep context where the conversation happens',
67 'landing.feature.team.body': 'Channels, threads, direct messages, and voice live around the workspace, reducing scattered updates and repeated explanations.',
68 'landing.feature.team.messageOne': 'The retrieval pipeline change is ready for review.',
69 'landing.feature.team.messageTwo': 'I added the edge cases to',
70 'landing.feature.team.voice': 'Architecture huddle in progress',
71 'landing.cases.kicker': 'Use cases',
72 'landing.cases.title': 'Different teams. One delivery path.',
73 'landing.cases.body': 'Whether you build products, maintain open source, or work across time zones, GitDataAI brings work together around code and decisions.',
74 'landing.case.product.title': 'Product engineering teams',
75 'landing.case.product.body': 'Move from requirement breakdown and board progress to pull request review with product, engineering, and delivery state in one space.',
76 'landing.case.product.outcome': 'Reduce handoff loss and turn decisions into code faster',
77 'landing.case.open.title': 'Open-source maintainers',
78 'landing.case.open.body': 'Manage repositories, contribution discussions, issues, and review signals together so outside contributors can understand the project faster.',
79 'landing.case.open.outcome': 'Create a clearer contribution path and a calmer maintenance flow',
80 'landing.case.distributed.title': 'Distributed engineering teams',
81 'landing.case.distributed.body': 'Use traceable discussions, threads, reminders, and voice across time zones without losing the background behind each decision.',
82 'landing.case.distributed.outcome': 'Run fewer sync meetings while complex work keeps moving',
83 'landing.faq.kicker': 'Frequently asked questions',
84 'landing.faq.title': 'What you may want to know before starting',
85 'landing.faq.body': 'Straight answers about product scope, migration, AI review, and deployment.',
86 'landing.faq.one.question': 'Is GitDataAI only a Git hosting platform?',
87 'landing.faq.one.answer': 'It goes beyond hosting. GitDataAI centers on Git repositories and connects issues, boards, pull requests, team communication, and AI review into a continuous engineering context.',
88 'landing.faq.two.question': 'Can we import existing repositories?',
89 'landing.faq.two.answer': 'Yes. The platform includes repository import capabilities, so teams can bring in existing projects and adopt issues, reviews, and collaboration features gradually.',
90 'landing.faq.three.question': 'Does AI code review replace human review?',
91 'landing.faq.three.answer': 'No. AI summarizes changes, connects context, and surfaces potential risks; your team still makes the final judgment. Think of it as a high-quality preparation layer for review.',
92 'landing.faq.four.question': 'Can teams self-host GitDataAI?',
93 'landing.faq.four.answer': 'Yes. GitDataAI supports containerized deployment and can be configured around your infrastructure and operating model. Enabled services and resource scale depend on the deployment environment.',
94 'landing.faq.five.question': 'Can it replace separate task and chat tools?',
95 'landing.faq.five.answer': 'GitDataAI includes issues, boards, channels, threads, direct messages, voice, and notifications. Teams can consolidate engineering-focused collaboration while keeping other systems where they still add value.',
96 'landing.faq.six.question': 'How do we start?',
97 'landing.faq.six.answer': 'Create an account to enter a workspace. Available features and quotas depend on the current deployment, or you can sign in to an existing account and continue working.',
98 'landing.cta.kicker': 'Start with the next delivery',
99 'landing.cta.title': 'Move good ideas toward done, faster',
100 'landing.cta.body': 'Put code, planning, communication, and AI on one work path so your team can spend its attention on the decisions that truly need it.',
101 'landing.cta.primary': 'Start free',
102 'landing.cta.secondary': 'Already have an account? Sign in',
103 'landing.footer.tagline': 'Build context around code and keep engineering teams shipping.',
104 'landing.footer.terms': 'Terms',
105 'landing.footer.privacy': 'Privacy',
106 'landing.footer.languageAria': 'Switch to Simplified Chinese',
107 'agreement.back': 'Back to home',
108 'agreement.loading': 'Loading…',
109 'agreement.error': 'This content cannot be loaded right now. Please try again later.',
110 'agreement.updated': 'Version {version}',
111 'agreement.footerNote': 'Questions about these terms? Reach us through the support channel.',
112 'agreement.terms.title': 'Terms of Service',
113 'agreement.terms.summary': 'Please read and accept these terms before using GitDataAI.',
114 'agreement.privacy.title': 'Privacy Policy',
115 'agreement.privacy.summary': 'Learn what data we collect, how it is used, and your rights.',
116 'agreement.meta.terms': 'Terms of Service',
117 'agreement.meta.privacy': 'Privacy Policy',
118 'agreement.meta.generic': 'Agreement',} as const
Modified web/src/shared/i18n/messages/zh-CN/index.ts View web/src/shared/i18n/messages/zh-CN/index.ts
@@ -5,6 +5,7 @@
55import { errors } from './errors'
66import { issueEvents, issues } from './issues'
77import { labels } from './labels'
8import { landing } from './landing'
89import { livekit } from './livekit'
910import { misc } from './misc'
1011import { nav } from './nav'
@@ -27,6 +28,7 @@
2728 ...errors,
2829 ...shell,
2930 ...labels,
31 ...landing,
3032 ...livekit,
3133 ...settings,
3234 ...chat,
Added web/src/shared/i18n/messages/zh-CN/landing.ts View web/src/shared/i18n/messages/zh-CN/landing.ts
@@ -0,0 +1,118 @@
1export const landing = {
2 'landing.meta.title': 'GitDataAI — 让代码、协作与 AI 共享同一上下文',
3 'landing.meta.description': 'GitDataAI 将代码仓库、Issue、Pull Request、团队沟通与 AI 代码审查放进同一个工程协作空间。',
4 'landing.skip': '跳到主要内容',
5 'landing.brand.aria': 'GitDataAI 首页',
6 'landing.nav.aria': '主要导航',
7 'landing.nav.value': '核心价值',
8 'landing.nav.features': '产品能力',
9 'landing.nav.useCases': '工作场景',
10 'landing.nav.faq': '常见问题',
11 'landing.nav.login': '登录',
12 'landing.nav.start': '免费开始',
13 'landing.hero.eyebrow': '面向 AI 时代的软件协作空间',
14 'landing.hero.titleLeadStart': '为持续交付而生的 ',
15 'landing.hero.titleLeadEnd': 'AI 工程工作空间。',
16 'landing.hero.titleAccentStart': '一份工程上下文,',
17 'landing.hero.titleAccentEnd': '让代码、任务、审查与沟通始终连贯',
18 'landing.hero.body': 'GitDataAI 把团队每天使用的工程工具连接起来,从第一次提交到最终交付,所有人始终基于同一份上下文协作。',
19 'landing.hero.primary': '免费开始',
20 'landing.hero.secondary': '查看产品能力',
21 'landing.hero.note': '支持容器化部署 · 可按团队节奏逐步扩展',
22 'landing.demo.caption': 'GitDataAI 工作空间示意:代码变更、AI 审查与团队在线状态集中显示。',
23 'landing.demo.status': '实时同步',
24 'landing.demo.repo': '仓库',
25 'landing.demo.branch': 'main',
26 'landing.demo.code': '代码',
27 'landing.demo.issues': '任务',
28 'landing.demo.reviews': '审查',
29 'landing.demo.team': '团队',
30 'landing.demo.pr': 'Pull Request #142',
31 'landing.demo.prTitle': '统一检索上下文链路',
32 'landing.demo.review': 'AI 审查',
33 'landing.demo.reviewTitle': '2 个风险需要确认',
34 'landing.demo.riskOne': '缓存键缺少空间作用域,可能串联不同仓库。',
35 'landing.demo.riskTwo': '重排请求需要设置超时与回退路径。',
36 'landing.demo.online': '3 位协作者在线',
37 'landing.values.kicker': '为什么选择 GitDataAI',
38 'landing.values.title': '少切换工具,多推进工作',
39 'landing.values.body': '把分散在代码托管、任务管理和即时沟通里的工程上下文重新连接,让团队围绕同一份事实协作。',
40 'landing.value.context.title': '上下文不再断层',
41 'landing.value.context.body': '代码、Issue、审查与讨论彼此关联,成员无需在多个系统之间反复还原事情的来龙去脉。',
42 'landing.value.ai.title': 'AI 进入真实工作流',
43 'landing.value.ai.body': '从变更摘要到代码审查线索,AI 基于当前仓库与协作上下文提供帮助,而不是停留在孤立对话里。',
44 'landing.value.rhythm.title': '协作节奏保持连贯',
45 'landing.value.rhythm.body': '看板、频道、线程、语音与通知围绕交付目标组织,让同步和异步协作自然衔接。',
46 'landing.features.kicker': '产品能力',
47 'landing.features.title': '从第一行代码,到最后一个共识',
48 'landing.features.body': '一套为工程团队设计的完整工作面:管理代码、推动任务、完成审查,并让关键讨论始终可追溯。',
49 'landing.feature.repo.label': '代码与仓库',
50 'landing.feature.repo.title': '熟悉的 Git,完整的工程视图',
51 'landing.feature.repo.body': '浏览分支、标签、文件历史与差异,在同一界面管理代码变更和仓库协作。',
52 'landing.feature.repo.updated': '刚刚更新',
53 'landing.feature.ai.label': 'AI 原生审查',
54 'landing.feature.ai.title': '先理解变化,再决定行动',
55 'landing.feature.ai.body': '自动提炼提交与 Pull Request 的重点,提示值得人工确认的风险,让审查时间花在真正重要的地方。',
56 'landing.feature.ai.scanning': '正在分析变更上下文',
57 'landing.feature.ai.signalOne': '跨文件影响已关联',
58 'landing.feature.ai.signalTwo': '审查线索已按优先级整理',
59 'landing.feature.board.label': 'Issue 与看板',
60 'landing.feature.board.title': '把计划变成看得见的进展',
61 'landing.feature.board.body': '用 Issue、标签、里程碑和看板组织工作,并让任务与代码变更保持连接。',
62 'landing.feature.board.todo': '待处理',
63 'landing.feature.board.progress': '进行中',
64 'landing.feature.board.done': '已完成',
65 'landing.feature.team.label': '团队沟通',
66 'landing.feature.team.title': '讨论发生在哪里,上下文就留在哪里',
67 'landing.feature.team.body': '频道、线程、私信与语音协作围绕空间展开,减少信息散落和重复同步。',
68 'landing.feature.team.messageOne': '检索链路的变更已经可以审查。',
69 'landing.feature.team.messageTwo': '我把边界条件补到了',
70 'landing.feature.team.voice': '架构讨论进行中',
71 'landing.cases.kicker': '工作场景',
72 'landing.cases.title': '适配不同团队,同一条交付链路',
73 'landing.cases.body': '无论是产品研发、开源维护还是远程协作,GitDataAI 都让工作围绕代码与决策自然汇合。',
74 'landing.case.product.title': '产品研发团队',
75 'landing.case.product.body': '从需求拆解、看板推进到 Pull Request 审查,把产品、研发与交付状态放在同一个空间里。',
76 'landing.case.product.outcome': '减少交接损耗,让决策更快落到代码上',
77 'landing.case.open.title': '开源项目维护者',
78 'landing.case.open.body': '集中管理仓库、贡献讨论、Issue 与审查线索,让外部贡献者更容易理解项目脉络。',
79 'landing.case.open.outcome': '让贡献路径更清楚,维护工作更有秩序',
80 'landing.case.distributed.title': '分布式工程团队',
81 'landing.case.distributed.body': '用可追溯的讨论、线程、提醒和语音协作跨越时区,让异步工作不丢失背景。',
82 'landing.case.distributed.outcome': '少开同步会议,也能持续推进复杂协作',
83 'landing.faq.kicker': '常见问题',
84 'landing.faq.title': '开始之前,你可能想知道',
85 'landing.faq.body': '关于产品定位、迁移、AI 审查与部署方式的简明说明。',
86 'landing.faq.one.question': 'GitDataAI 只是一个 Git 托管平台吗?',
87 'landing.faq.one.answer': '不止于此。GitDataAI 以 Git 仓库为核心,同时连接 Issue、看板、Pull Request、团队沟通与 AI 审查,让工程协作拥有连续上下文。',
88 'landing.faq.two.question': '可以导入已有仓库吗?',
89 'landing.faq.two.answer': '可以。平台提供仓库导入能力,团队可以把现有项目迁入工作空间,再逐步启用 Issue、审查与协作功能。',
90 'landing.faq.three.question': 'AI 代码审查会取代人工审查吗?',
91 'landing.faq.three.answer': '不会。AI 用于总结变化、关联上下文并提示潜在风险,最终判断仍由团队完成;它更像审查前的高质量准备层。',
92 'landing.faq.four.question': '支持团队自托管吗?',
93 'landing.faq.four.answer': '支持容器化部署,可按团队的基础设施与运维方式配置。具体启用的服务和资源规模由部署环境决定。',
94 'landing.faq.five.question': '它能替代独立的任务和聊天工具吗?',
95 'landing.faq.five.answer': 'GitDataAI 内置 Issue、看板、频道、线程、私信、语音与通知。团队可以把与研发强相关的协作收回一处,并按需要保留其他系统。',
96 'landing.faq.six.question': '如何开始体验?',
97 'landing.faq.six.answer': '注册账号后即可进入工作空间。可用功能与配额由当前部署配置决定,你也可以先登录已有账号继续工作。',
98 'landing.cta.kicker': '从下一次交付开始',
99 'landing.cta.title': '让好想法更快走到可交付',
100 'landing.cta.body': '把代码、计划、沟通与 AI 放在同一条工作链路里,让团队把精力留给真正需要判断的事情。',
101 'landing.cta.primary': '免费开始',
102 'landing.cta.secondary': '已有账号?登录',
103 'landing.footer.tagline': '围绕代码建立上下文,让工程团队持续交付。',
104 'landing.footer.terms': '服务条款',
105 'landing.footer.privacy': '隐私说明',
106 'landing.footer.languageAria': '切换为英文',
107 'agreement.back': '返回首页',
108 'agreement.loading': '正在加载内容…',
109 'agreement.error': '内容暂时无法加载,请稍后重试。',
110 'agreement.updated': '版本 {version}',
111 'agreement.footerNote': '如对条款内容有疑问,请通过支持渠道联系我们。',
112 'agreement.terms.title': '服务条款',
113 'agreement.terms.summary': '使用 GitDataAI 前请阅读并同意以下服务条款。',
114 'agreement.privacy.title': '隐私说明',
115 'agreement.privacy.summary': '了解我们收集哪些数据、如何使用以及你的权利。',
116 'agreement.meta.terms': '服务条款',
117 'agreement.meta.privacy': '隐私说明',
118 'agreement.meta.generic': '条款',} as const
Added web/src/shared/theme/index.ts View web/src/shared/theme/index.ts
@@ -0,0 +1,2 @@
1export { ThemeContext, useThemeContext, type ThemeContextValue } from '../ui/theme-context'
2export { ThemeProvider } from '../ui/theme-provider'
Modified web/index.html View web/index.html
@@ -4,6 +4,11 @@
44 <meta charset="UTF-8" />
55 <link rel="icon" type="image/svg+xml" href="/favicon.png" />
66 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7 <meta name="theme-color" content="#090b0d" />
8 <meta
9 name="description"
10 content="GitDataAI brings repositories, issues, pull requests, team communication, and AI code review into one engineering workspace."
11 />
712 <meta name="app:region" content="EU-CENTRAL-1" />
813 <meta name="app:min-age" content="13" />
914 <title>GitDataAI</title>