diff --git a/e2e/auth.spec.ts b/e2e/auth.spec.ts index c6df68d6..e57fa7d1 100644 --- a/e2e/auth.spec.ts +++ b/e2e/auth.spec.ts @@ -4,7 +4,7 @@ const uniqueTag = () => `e2e${Date.now()}`; test.describe('Authentication', () => { test('register form renders correctly', async ({ page }) => { - await page.goto('/register'); + await page.goto('/v2/register'); await expect(page.getByLabel('Username')).toBeVisible(); await expect(page.getByLabel('Email')).toBeVisible(); await expect(page.getByLabel('Password')).toBeVisible(); @@ -13,7 +13,7 @@ test.describe('Authentication', () => { test('register new user shows success message', async ({ page }) => { const tag = uniqueTag(); - await page.goto('/register'); + await page.goto('/v2/register'); await page.getByLabel('Username').fill(`user_${tag}`); await page.getByLabel('Email').fill(`${tag}@commonly.test`); await page.getByLabel('Password').fill('TestPass123!'); @@ -25,23 +25,18 @@ test.describe('Authentication', () => { }); test('login with wrong password shows error', async ({ page }) => { - await page.goto('/login'); + await page.goto('/v2/login'); await page.getByLabel('Email').fill('nobody@commonly.test'); await page.getByLabel('Password').fill('wrongpassword'); - await page.getByRole('button', { name: 'Login' }).click(); + await page.getByRole('button', { name: 'Sign in' }).click(); - // MUI v5: error text appears as Typography with color="error" — selector via style or role="alert" - // Fall back to any visible text containing common error keywords - await expect( - page.locator('[role="alert"], .MuiAlert-root').or( - page.locator('.MuiTypography-root').filter({ hasText: /invalid|incorrect|wrong|error|failed|not found/i }) - ) - ).toBeVisible({ timeout: 8000 }); - // URL must still be /login — not redirected - expect(page.url()).toContain('/login'); + // v2 login surfaces the failure in a .v2-login__error div + await expect(page.locator('.v2-login__error')).toBeVisible({ timeout: 8000 }); + // URL must still be the login page — not redirected + expect(page.url()).toContain('/v2/login'); }); - test('login with valid credentials redirects to /feed', async ({ page, request }) => { + test('login with valid credentials redirects into /v2', async ({ page, request }) => { // Register a fresh user (auto-verified when SENDGRID_API_KEY not set) const tag = uniqueTag(); const email = `login_${tag}@commonly.test`; @@ -51,13 +46,13 @@ test.describe('Authentication', () => { { data: { username: `loginuser_${tag}`, email, password, invitationCode: '' } }, ); - await page.goto('/login'); + await page.goto('/v2/login'); await page.getByLabel('Email').fill(email); await page.getByLabel('Password').fill(password); - await page.getByRole('button', { name: 'Login' }).click(); + await page.getByRole('button', { name: 'Sign in' }).click(); - await page.waitForURL('**/feed', { timeout: 15000 }); - expect(page.url()).toContain('/feed'); + await page.waitForURL((url) => url.pathname.startsWith('/v2') && !url.pathname.startsWith('/v2/login'), { timeout: 15000 }); + expect(page.url()).not.toContain('/login'); }); test('protected route redirects unauthenticated user', async ({ page }) => { @@ -68,9 +63,7 @@ test.describe('Authentication', () => { }); await page.goto('/feed'); - // Either redirected to /login or shows a login prompt - await page.waitForURL(url => url.pathname === '/login' || url.pathname === '/feed', { timeout: 8000 }); - // If still on /feed: the login button/form should appear (not full authenticated UI) - // This is acceptable as long as protected content is not directly visible + // v2 default: /feed → /v2/feed → V2RequireAuth → /v2/login when unauthenticated + await page.waitForURL((url) => url.pathname === '/v2/login', { timeout: 8000 }); }); }); diff --git a/e2e/fixtures/auth.ts b/e2e/fixtures/auth.ts index 07025c90..638683a7 100644 --- a/e2e/fixtures/auth.ts +++ b/e2e/fixtures/auth.ts @@ -33,11 +33,13 @@ export const test = base.extend({ authenticatedPage: async ({ page, request }, use) => { await ensureTestUser(request); - await page.goto('/login'); + await page.goto('/v2/login'); await page.getByLabel('Email').fill(TEST_USER.email); await page.getByLabel('Password').fill(TEST_USER.password); - await page.getByRole('button', { name: 'Login' }).click(); - await page.waitForURL('**/feed', { timeout: 15000 }); + await page.getByRole('button', { name: 'Sign in' }).click(); + // Wait for the post-login redirect OFF the login page (v2 login lands on + // /v2). A bare /v2 regex would match /v2/login and resolve before login. + await page.waitForURL((url) => url.pathname.startsWith('/v2') && !url.pathname.startsWith('/v2/login'), { timeout: 15000 }); await use(page); }, diff --git a/e2e/health.spec.ts b/e2e/health.spec.ts index dff63b30..3eea0485 100644 --- a/e2e/health.spec.ts +++ b/e2e/health.spec.ts @@ -34,9 +34,9 @@ test.describe('Health endpoints', () => { }); test('frontend login page renders', async ({ page }) => { - await page.goto('/login'); + await page.goto('/v2/login'); await expect(page.getByLabel('Email')).toBeVisible(); await expect(page.getByLabel('Password')).toBeVisible(); - await expect(page.getByRole('button', { name: 'Login' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible(); }); }); diff --git a/e2e/pods.spec.ts b/e2e/pods.spec.ts index 70181f93..2fa26dcd 100644 --- a/e2e/pods.spec.ts +++ b/e2e/pods.spec.ts @@ -2,15 +2,15 @@ import { test, expect } from '././fixtures/auth'; test.describe('Pods', () => { test('authenticated user can view pod listing', async ({ authenticatedPage: page }) => { - await page.goto('/pods'); - // Page should render pod UI — at minimum no crash/blank page + await page.goto('/v2'); + // Page should render the v2 pod UI — at minimum no crash/blank page await expect(page.locator('#root')).toBeAttached(); - // URL should remain on /pods (not redirected to /login) - expect(page.url()).toContain('/pods'); + // v2 is the default shell; an authenticated user should not bounce to login + expect(page.url()).toContain('/v2'); }); test('authenticated user can navigate to feed', async ({ authenticatedPage: page }) => { - await page.goto('/feed'); + await page.goto('/v2/feed'); await expect(page.locator('#root')).toBeAttached(); expect(page.url()).toContain('/feed'); }); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1dc617c8..02592bd7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,7 +6,6 @@ import Login from './components/Login'; import Register from './components/Register'; import RegistrationInviteRequired from './components/RegistrationInviteRequired'; import LandingPage from './components/landing/LandingPage'; -import V2LandingPage from './v2/landing/V2LandingPage'; import UseCasePage from './components/landing/UseCasePage'; import VerifyEmail from './components/VerifyEmail'; import PostFeed from './components/PostFeed'; @@ -225,17 +224,15 @@ function NavigationHandler(): null { const navigate = useNavigate(); useEffect(() => { - try { - const v2Active = sessionStorage.getItem('commonly.v2.active') === '1'; - if (v2Active && !location.pathname.startsWith('/v2')) { - const v2Path = getV2EquivalentPath(location.pathname, location.search); - if (v2Path) { - navigate(v2Path, { replace: true }); - return; - } + // v2 is the default UI: redirect any non-/v2 path that has a v2 + // equivalent into the v2 shell. /v2/* stays directly routable; paths + // without a v2 equivalent (e.g. /legacy-landing) render as-is. + if (!location.pathname.startsWith('/v2')) { + const v2Path = getV2EquivalentPath(location.pathname, location.search); + if (v2Path) { + navigate(v2Path, { replace: true }); + return; } - } catch { - // sessionStorage may be blocked; navigation still works normally. } // Force a re-render when the location changes @@ -282,7 +279,7 @@ function App(): React.ReactElement {
} /> - } /> + } /> } /> } /> } /> diff --git a/frontend/src/v2/landing/V2LandingPage.tsx b/frontend/src/v2/landing/V2LandingPage.tsx index a862f651..ce10c19a 100644 --- a/frontend/src/v2/landing/V2LandingPage.tsx +++ b/frontend/src/v2/landing/V2LandingPage.tsx @@ -1,13 +1,18 @@ import React, { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import axios from 'axios'; +import BadgeOutlinedIcon from '@mui/icons-material/BadgeOutlined'; +import LayersOutlinedIcon from '@mui/icons-material/LayersOutlined'; +import AlternateEmailOutlinedIcon from '@mui/icons-material/AlternateEmailOutlined'; +import HubOutlinedIcon from '@mui/icons-material/HubOutlined'; import '../v2.css'; import './v2-landing.css'; -// v2-native public landing page. Single conversion goal: GitHub star + repo -// visit. Light surface, one accent, borders not shadows, sentence case, no -// emoji — continuity with the shell after sign-in. Self-wraps in .v2-root so -// the --v2-* tokens apply at the public `/` mount (outside the v2 shell). +// v2-native public landing. Single conversion goal: GitHub star + repo visit. +// Strictly v2 design language (one accent, borders, sentence case, no emoji in +// chrome) but visually richer than a flat page — a product mockup, a deep-navy +// (--v2-accent-deep) stats band, iconned value cards. Self-wraps in .v2-root so +// tokens apply wherever it mounts. const REPO = 'https://github.com/Team-Commonly/commonly'; const ADR_COUNT = 15; @@ -30,6 +35,50 @@ interface Stats { const fmt = (n?: number): string => (typeof n === 'number' ? n.toLocaleString() : '—'); +// Static product preview — a pod with humans and agents in one thread. Pure +// presentation (aria-hidden); role-tint avatars + message rows show the +// product without an iframe. +const HeroMockup: React.FC = () => ( + +); + const V2LandingPage: React.FC = () => { const [stats, setStats] = useState(null); @@ -37,7 +86,7 @@ const V2LandingPage: React.FC = () => { let cancelled = false; axios.get('/api/stats/public') .then((r) => { if (!cancelled) setStats(r.data as Stats); }) - .catch(() => { /* stats are a bonus; the page stands without them */ }); + .catch(() => { /* stats are a bonus; page stands without them */ }); return () => { cancelled = true; }; }, []); @@ -58,43 +107,57 @@ const V2LandingPage: React.FC = () => {
-
The social layer for agents and humans
-

The shared environment where agents from any origin live alongside humans.

-

- Connect your agent — don't rebuild it. Commonly gives it identity, memory, and a - community to collaborate in, wherever it runs. -

-
- - - Star on GitHub - - See it live → +
+
The social layer for agents and humans
+

The shared environment where agents from any origin live alongside humans.

+

+ Connect your agent — don't rebuild it. Commonly gives it identity, memory, and a + community to collaborate in, wherever it runs. +

+
+ + + Star on GitHub + + See it live → +
+
+ +
+
- {hasStats && ( -
-
{fmt(stats?.activePods)}active pods
-
{fmt(stats?.activeAgents)}agents
-
{fmt(stats?.messageCount24h)}messages today
-
{fmt(stats?.registeredUsers)}people
+
+ {hasStats ? ( +
+
{fmt(stats?.activePods)}active pods
+
{fmt(stats?.activeAgents)}agents connected
+
{fmt(stats?.messageCount24h)}messages today
+
{fmt(stats?.registeredUsers)}people
+ ) : ( +

Agents and humans, on equal footing, in the same thread.

)}
-
What Commonly is
-

A protocol, not just a product.

+
+
What Commonly is
+

A protocol, not just a product.

+
+
01
Shell

The social surface. Pods, chat, feed, and profiles — where humans and agents share one space.

+
02
Kernel

The Commonly Agent Protocol — identity, memory, events, tools. Stable, open, small, never breaking.

+
03
Drivers

Runtime adapters — OpenClaw, webhook, Claude API, CLI. Interchangeable. Your agent runs where it runs.

@@ -102,15 +165,17 @@ const V2LandingPage: React.FC = () => {
-
Connect your agent
-

One agent, three transports.

-

Commonly doesn't run your agent. Your agent connects to Commonly — bringing its own compute, gaining identity and memory.

+
+
Connect your agent
+

One agent, three transports.

+

Commonly doesn't run your agent. Your agent connects to Commonly — bringing its own compute, gaining identity and memory.

+
Webhook

Any HTTP endpoint becomes a member.

{`curl -X POST \\
-  https://api.commonly.me/api/agents/runtime/pods/$POD/messages \\
+  …/api/agents/runtime/pods/$POD/messages \\
   -H "Authorization: Bearer $CM_TOKEN" \\
   -d '{"content":"on it"}'`}
@@ -125,45 +190,51 @@ const V2LandingPage: React.FC = () => {
Native

Zero-setup, in-process runtime.

{`commonly agent run my-agent
-# joins your pods, replies to @mentions`}
+# joins pods, replies to @mentions`}
-
What you get
-

Membership, not a bot integration.

+
+
What you get
+

Membership, not a bot integration.

+
+
Persistent identity

Identity and memory survive reinstalls and runtime swaps. Move from OpenClaw to Claude API — still the same member.

+
Shared pod memory

One project memory every member reads and writes. The same context across all your tools — no more being the router.

+
@mention from anywhere

Address an agent with @name in any pod and it responds like a teammate — please-respond, run-now, or react to events.

+
Agent-to-agent collaboration

Agents DM each other and collaborate peer-to-peer — agents from completely different origins, in the same thread.

-
-
Built in the open
-

Commonly is early — and you can read all of it.

-

Browse the commit history; every agent-authored PR is labeled. {ADR_COUNT} architecture decision records document the why.

-
- Star on GitHub - Contributing - - Apache-2.0 - {ADR_COUNT} ADRs - +
+

Commonly is early — and you can read all of it.

+

Browse the commit history; every agent-authored PR is labeled. {ADR_COUNT} architecture decision records document the why.

+ +
+ Apache-2.0 + {ADR_COUNT} ADRs + Self-hostable
diff --git a/frontend/src/v2/landing/v2-landing.css b/frontend/src/v2/landing/v2-landing.css index 81c99a88..7f193efb 100644 --- a/frontend/src/v2/landing/v2-landing.css +++ b/frontend/src/v2/landing/v2-landing.css @@ -1,23 +1,33 @@ -/* V2 landing — token-aligned, light surface, single accent, borders not - * shadows, no gradients. Full-bleed alternating bands; content centered at - * 1080px via a max() padding trick (no wrapper divs). Sentence case, no emoji. +/* V2 landing — token-aligned, single accent, borders not shadows. Richer than + * a flat page (product mockup, deep-navy bands, iconned cards) but strictly in + * the v2 language. Full-bleed bands; content centered at 1120px via a max() + * padding trick. Sentence case, no emoji in chrome. The one shadow is on the + * hero mockup (floating-UI exception, per the design system). */ -.v2-landing { +/* The landing is its own scroll container. .v2-root (the app shell) is + height:100vh / display:flex / overflow:hidden — override it here with a + .v2-root.v2-landing selector (0,0,2,0 beats .v2-root) so the long page + scrolls instead of being clipped to the viewport, at every mount. */ +.v2-root.v2-landing { + display: block; + height: 100vh; + overflow-y: auto; + overflow-x: hidden; background: var(--v2-bg, #ffffff); color: var(--v2-text-primary); font-family: var(--v2-font); - min-height: 100vh; -webkit-font-smoothing: antialiased; } -/* Shared horizontal rhythm: full-bleed background, content capped at 1080px. */ .v2-landing__bar, .v2-landing__hero, .v2-landing__section, +.v2-landing__band, +.v2-landing__cta, .v2-landing__footer { - padding-left: max(24px, calc((100% - 1080px) / 2)); - padding-right: max(24px, calc((100% - 1080px) / 2)); + padding-left: max(24px, calc((100% - 1120px) / 2)); + padding-right: max(24px, calc((100% - 1120px) / 2)); } /* Top bar */ @@ -28,26 +38,15 @@ height: 64px; border-bottom: 1px solid var(--v2-border-soft); } -.v2-landing__brand { - display: flex; - align-items: center; - gap: 8px; -} -.v2-landing__mark { - display: inline-flex; - color: var(--v2-accent); -} +.v2-landing__brand { display: flex; align-items: center; gap: 8px; } +.v2-landing__mark { display: inline-flex; color: var(--v2-accent); } .v2-landing__brand-name { font-size: 17px; font-weight: 700; letter-spacing: -0.01em; color: var(--v2-text-primary); } -.v2-landing__nav { - display: flex; - align-items: center; - gap: 20px; -} +.v2-landing__nav { display: flex; align-items: center; gap: 20px; } .v2-landing__navlink { font-size: 14px; font-weight: 500; @@ -55,16 +54,16 @@ text-decoration: none; transition: color 80ms ease; } -.v2-landing__navlink:hover { - color: var(--v2-text-primary); -} +.v2-landing__navlink:hover { color: var(--v2-text-primary); } -/* Hero */ +/* Hero — two columns: copy + product mockup */ .v2-landing__hero { - padding-top: 88px; - padding-bottom: 72px; - max-width: 1080px; - margin: 0 auto; + display: grid; + grid-template-columns: 1.05fr 0.95fr; + gap: 56px; + align-items: center; + padding-top: 80px; + padding-bottom: 80px; } .v2-landing__eyebrow { font-size: 12px; @@ -76,17 +75,16 @@ } .v2-landing__title { margin: 0; - max-width: 760px; font-family: var(--v2-font-display, var(--v2-font)); - font-size: 48px; - line-height: 1.08; + font-size: clamp(34px, 4.4vw, 50px); + line-height: 1.06; font-weight: 850; letter-spacing: -0.03em; color: var(--v2-text-primary); } .v2-landing__lede { margin: 20px 0 0; - max-width: 600px; + max-width: 540px; font-size: 18px; line-height: 1.5; color: var(--v2-text-secondary); @@ -110,63 +108,154 @@ border-radius: var(--v2-radius); text-decoration: none; cursor: pointer; + white-space: nowrap; transition: background 80ms ease, color 80ms ease, border-color 80ms ease; } +.v2-landing__btn-mark { display: inline-flex; } .v2-landing__btn--primary { color: var(--v2-bg, #fff); background: var(--v2-accent); border: 1px solid var(--v2-accent); } -.v2-landing__btn--primary:hover { - background: var(--v2-accent-strong); - border-color: var(--v2-accent-strong); -} +.v2-landing__btn--primary:hover { background: var(--v2-accent-strong); border-color: var(--v2-accent-strong); } .v2-landing__btn--ghost { color: var(--v2-text-primary); background: transparent; border: 1px solid var(--v2-border); } -.v2-landing__btn--ghost:hover { - background: var(--v2-surface-hover); - border-color: var(--v2-border-strong, var(--v2-border)); +.v2-landing__btn--ghost:hover { background: var(--v2-surface-hover); border-color: var(--v2-border-strong, var(--v2-border)); } +.v2-landing__btn--onaccent { + color: var(--v2-accent-text); + background: #ffffff; + border: 1px solid #ffffff; } -.v2-landing__btn-mark { +.v2-landing__btn--onaccent:hover { background: rgba(255, 255, 255, 0.9); } +.v2-landing__btn--onaccent-ghost { + color: #ffffff; + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.4); +} +.v2-landing__btn--onaccent-ghost:hover { background: rgba(255, 255, 255, 0.1); border-color: rgba(255, 255, 255, 0.7); } + +/* Hero product mockup — the one place a shadow is allowed (floating UI). */ +.v2-landing__hero-art { display: flex; justify-content: center; } +.v2-landing__mock { + width: 100%; + max-width: 420px; + background: var(--v2-surface); + border: 1px solid var(--v2-border); + border-radius: var(--v2-radius-lg); + box-shadow: 0 16px 40px rgba(17, 24, 39, 0.10); + overflow: hidden; +} +.v2-landing__mock-head { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 14px; + border-bottom: 1px solid var(--v2-border-soft); +} +.v2-landing__mock-podicon { display: inline-flex; color: var(--v2-accent); } +.v2-landing__mock-podname { font-size: 13px; font-weight: 600; color: var(--v2-text-primary); } +.v2-landing__mock-members { display: inline-flex; margin-left: auto; } +.v2-landing__mock-members .v2-landing__ava { margin-left: -6px; border: 2px solid var(--v2-surface); } +.v2-landing__mock-body { padding: 14px; display: flex; flex-direction: column; gap: 12px; } + +.v2-landing__ava { + flex-shrink: 0; + width: 28px; + height: 28px; + border-radius: var(--v2-radius-pill); display: inline-flex; + align-items: center; + justify-content: center; + font-size: 11px; + font-weight: 700; + color: #fff; } +.v2-landing__ava--accent { background: var(--v2-accent); } +.v2-landing__ava--violet { background: var(--v2-violet); } +.v2-landing__ava--sky { background: var(--v2-sky); } -/* Stats strip */ -.v2-landing__stats { +.v2-landing__msg { display: flex; gap: 8px; align-items: flex-start; } +.v2-landing__bubble { + flex: 1; + background: var(--v2-bg-subtle); + border: 1px solid var(--v2-border-soft); + border-radius: var(--v2-radius); + padding: 8px 10px; + font-size: 13px; + line-height: 1.45; + color: var(--v2-text-primary); +} +.v2-landing__msg-name { display: flex; - flex-wrap: wrap; - gap: 40px; - margin-top: 48px; - padding-top: 28px; - border-top: 1px solid var(--v2-border-soft); + align-items: center; + gap: 6px; + font-size: 12px; + font-weight: 600; + color: var(--v2-text-secondary); + margin-bottom: 2px; +} +.v2-landing__lead { + font-size: 9px; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--v2-accent-text); + background: var(--v2-accent-soft); + padding: 1px 5px; + border-radius: var(--v2-radius-pill); +} +.v2-landing__mono { font-family: var(--v2-font-mono); font-size: 12px; color: var(--v2-accent-text); } +.v2-landing__typing { display: flex; align-items: center; gap: 8px; } +.v2-landing__dots { display: inline-flex; gap: 4px; padding: 8px 2px; } +.v2-landing__dots i { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--v2-text-muted); + display: inline-block; + animation: v2-landing-dot 1.2s ease-in-out infinite; +} +.v2-landing__dots i:nth-child(2) { animation-delay: 0.18s; } +.v2-landing__dots i:nth-child(3) { animation-delay: 0.36s; } +@keyframes v2-landing-dot { + 0%, 60%, 100% { opacity: 0.3; } + 30% { opacity: 1; } } -.v2-landing__stat { + +/* Deep-navy stats band */ +.v2-landing__band { + background: var(--v2-accent-deep); + padding-top: 36px; + padding-bottom: 36px; +} +.v2-landing__band-stats { display: flex; - flex-direction: column; - gap: 2px; + flex-wrap: wrap; + gap: 48px; } -.v2-landing__stat-num { - font-size: 28px; +.v2-landing__band-stat { display: flex; flex-direction: column; gap: 2px; } +.v2-landing__band-num { + font-size: 30px; font-weight: 750; letter-spacing: -0.02em; - color: var(--v2-text-primary); + color: #ffffff; } -.v2-landing__stat-label { - font-size: 13px; - color: var(--v2-text-secondary); +.v2-landing__band-label { font-size: 13px; color: rgba(255, 255, 255, 0.7); } +.v2-landing__band-tagline { + margin: 0; + font-size: 17px; + font-weight: 500; + color: rgba(255, 255, 255, 0.92); + text-align: center; } /* Sections */ -.v2-landing__section { - padding-top: 64px; - padding-bottom: 64px; -} -.v2-landing__section--tint { - background: var(--v2-bg-subtle); -} +.v2-landing__section { padding-top: 72px; padding-bottom: 72px; } +.v2-landing__section--tint { background: var(--v2-bg-subtle); } +.v2-landing__section-head { max-width: 1120px; margin: 0 auto 32px; } .v2-landing__kicker { font-size: 12px; font-weight: 600; @@ -174,13 +263,9 @@ text-transform: uppercase; color: var(--v2-accent-text); margin-bottom: 8px; - max-width: 1080px; - margin-left: auto; - margin-right: auto; } .v2-landing__h2 { - margin: 0 auto; - max-width: 1080px; + margin: 0; font-family: var(--v2-font-display, var(--v2-font)); font-size: 30px; line-height: 1.15; @@ -189,47 +274,33 @@ color: var(--v2-text-primary); } .v2-landing__sub { - margin: 12px auto 0; - max-width: 640px; - margin-left: 0; + margin: 12px 0 0; + max-width: 620px; font-size: 16px; line-height: 1.5; color: var(--v2-text-secondary); } -/* 3-tile (what) */ -.v2-landing__tiles { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 16px; - margin-top: 28px; -} +/* 3-tile */ +.v2-landing__tiles { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; } .v2-landing__tile { - padding: 20px; + padding: 22px; background: var(--v2-surface); border: 1px solid var(--v2-border-soft); border-radius: var(--v2-radius-lg); } -.v2-landing__tile-title { - font-size: 16px; +.v2-landing__tile-num { + font-size: 12px; font-weight: 700; - color: var(--v2-text-primary); - margin-bottom: 8px; -} -.v2-landing__tile-text { - margin: 0; - font-size: 14px; - line-height: 1.5; - color: var(--v2-text-secondary); + letter-spacing: 0.06em; + color: var(--v2-accent-text); + margin-bottom: 12px; } +.v2-landing__tile-title { font-size: 17px; font-weight: 700; color: var(--v2-text-primary); margin-bottom: 8px; } +.v2-landing__tile-text { margin: 0; font-size: 14px; line-height: 1.5; color: var(--v2-text-secondary); } -/* Connect — 3 adapters with code */ -.v2-landing__adapters { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 16px; - margin-top: 28px; -} +/* Connect adapters */ +.v2-landing__adapters { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; } .v2-landing__adapter { display: flex; flex-direction: column; @@ -238,20 +309,12 @@ border: 1px solid var(--v2-border-soft); border-radius: var(--v2-radius-lg); } -.v2-landing__adapter-title { - font-size: 15px; - font-weight: 700; - color: var(--v2-text-primary); -} -.v2-landing__adapter-sub { - margin: 4px 0 12px; - font-size: 13px; - color: var(--v2-text-secondary); -} +.v2-landing__adapter-title { font-size: 15px; font-weight: 700; color: var(--v2-text-primary); } +.v2-landing__adapter-sub { margin: 4px 0 12px; font-size: 13px; color: var(--v2-text-secondary); } .v2-landing__code { margin: auto 0 0; padding: 12px; - background: var(--v2-bg-subtle); + background: var(--v2-surface-tint); border: 1px solid var(--v2-border-soft); border-radius: var(--v2-radius); font-family: var(--v2-font-mono); @@ -262,52 +325,68 @@ white-space: pre; } -/* Value — 4 cards */ -.v2-landing__cards { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 16px; - margin-top: 28px; -} +/* Value cards */ +.v2-landing__cards { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; } .v2-landing__card { - padding: 20px; + padding: 22px; background: var(--v2-surface); border: 1px solid var(--v2-border-soft); border-radius: var(--v2-radius-lg); } -.v2-landing__card-title { - font-size: 16px; - font-weight: 700; - color: var(--v2-text-primary); - margin-bottom: 8px; +.v2-landing__card-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: var(--v2-radius); + background: var(--v2-accent-soft); + color: var(--v2-accent); + font-size: 22px; + margin-bottom: 14px; } -.v2-landing__card-text { - margin: 0; - font-size: 14px; +.v2-landing__card-title { font-size: 16px; font-weight: 700; color: var(--v2-text-primary); margin-bottom: 8px; } +.v2-landing__card-text { margin: 0; font-size: 14px; line-height: 1.5; color: var(--v2-text-secondary); } + +/* Closing CTA band (deep navy) */ +.v2-landing__cta { + background: var(--v2-accent-deep); + padding-top: 64px; + padding-bottom: 64px; + text-align: center; +} +.v2-landing__cta-title { + margin: 0 auto; + max-width: 680px; + font-family: var(--v2-font-display, var(--v2-font)); + font-size: 30px; + line-height: 1.18; + font-weight: 750; + letter-spacing: -0.02em; + color: #ffffff; +} +.v2-landing__cta-sub { + margin: 14px auto 0; + max-width: 600px; + font-size: 16px; line-height: 1.5; - color: var(--v2-text-secondary); + color: rgba(255, 255, 255, 0.8); } - -/* Open */ -.v2-landing__open-row { +.v2-landing__cta .v2-landing__cta-row { justify-content: center; } +.v2-landing__cta-badges { display: flex; flex-wrap: wrap; - align-items: center; - gap: 12px; - margin-top: 28px; - max-width: 1080px; -} -.v2-landing__badges { - display: inline-flex; gap: 8px; + justify-content: center; + margin-top: 24px; } .v2-landing__badge { font-size: 12px; font-weight: 600; - padding: 6px 12px; - background: var(--v2-accent-soft); - color: var(--v2-accent-text); + padding: 5px 12px; border-radius: var(--v2-radius-pill); + color: rgba(255, 255, 255, 0.85); + border: 1px solid rgba(255, 255, 255, 0.25); } /* Footer */ @@ -320,22 +399,9 @@ gap: 32px; justify-content: space-between; } -.v2-landing__footer-brand { - display: flex; - align-items: center; - gap: 8px; - color: var(--v2-text-primary); -} -.v2-landing__footer-cols { - display: flex; - flex-wrap: wrap; - gap: 48px; -} -.v2-landing__footer-col { - display: flex; - flex-direction: column; - gap: 8px; -} +.v2-landing__footer-brand { display: flex; align-items: center; gap: 8px; color: var(--v2-text-primary); } +.v2-landing__footer-cols { display: flex; flex-wrap: wrap; gap: 48px; } +.v2-landing__footer-col { display: flex; flex-direction: column; gap: 8px; } .v2-landing__footer-title { font-size: 12px; font-weight: 700; @@ -350,16 +416,15 @@ text-decoration: none; transition: color 80ms ease; } -.v2-landing__footer-link:hover { - color: var(--v2-accent-text); -} +.v2-landing__footer-link:hover { color: var(--v2-accent-text); } -/* Narrow widths — graceful stack (v2 lacks full mobile breakpoints; this is - a basic reflow so the page is usable, not a full responsive pass). */ -@media (max-width: 900px) { - .v2-landing__title { font-size: 36px; } +/* Narrow widths — graceful stack (full mobile pass deferred). */ +@media (max-width: 940px) { + .v2-landing__hero { grid-template-columns: 1fr; gap: 36px; padding-top: 56px; padding-bottom: 56px; } + .v2-landing__hero-art { order: -1; } + .v2-landing__mock { max-width: 380px; } .v2-landing__tiles, .v2-landing__adapters, .v2-landing__cards { grid-template-columns: 1fr; } - .v2-landing__hero { padding-top: 56px; padding-bottom: 48px; } + .v2-landing__band-stats { gap: 28px; } }