moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,58 @@
interface BrandMarkProps {
readonly size?: number;
readonly withWordmark?: boolean;
}
/**
* Waggle brand mark: a hive cell (pointy-top hexagon) holding a honey core —
* one memory node in the graph. Pure SVG so it stays crisp at every density
* and inherits no JPEG artifacts (replaces the legacy logo.jpeg raster).
*/
export default function BrandMark({ size = 28, withWordmark = false }: BrandMarkProps) {
const mark = (
<svg
width={size}
height={size}
viewBox="0 0 32 32"
fill="none"
role="img"
aria-label="Waggle"
>
<path
d="M16 3 L27.26 9.5 L27.26 22.5 L16 29 L4.74 22.5 L4.74 9.5 Z"
stroke="var(--honey-500, #e9a52c)"
strokeWidth="2.4"
strokeLinejoin="round"
fill="none"
/>
<path
d="M16 10.8 L20.5 13.4 L20.5 18.6 L16 21.2 L11.5 18.6 L11.5 13.4 Z"
fill="var(--honey-400, #f6c45a)"
/>
</svg>
);
if (!withWordmark) return mark;
return (
<span
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 10,
}}
>
{mark}
<span
style={{
fontSize: 17,
fontWeight: 700,
letterSpacing: '-0.02em',
color: 'var(--hive-50, #f6f1e4)',
}}
>
Waggle
</span>
</span>
);
}

View File

@@ -0,0 +1,537 @@
'use client';
import {
useCallback,
useState,
type CSSProperties,
type KeyboardEvent,
type ReactNode,
} from 'react';
import Image from 'next/image';
import { useTranslations } from 'next-intl';
import {
HEX_TEXTURE_PATH,
personas,
type Persona,
type PersonaSlug,
} from '../_data/personas';
/**
* 4x4 grid sequence in row-major order. `'filler'` slots occupy top-left,
* top-right, and bottom-right corners. Numbers reference `Persona.order`.
*/
const LANDING_GRID_SEQUENCE: ReadonlyArray<'filler' | number> = [
'filler', 1, 2, 'filler',
3, 4, 5, 6,
7, 8, 9, 10,
11, 12, 13, 'filler',
];
const personaByOrder = new Map<number, Persona>(
personas.map((p) => [p.order, p]),
);
/**
* Per-persona accent — a curated warm ramp (honey / amber / copper / bronze /
* terracotta family; no purple, no gradients). Round-6 palette discipline:
* exactly ONE muted cool note survives (sleeping → night blue, where the hue
* IS the meaning); everything else stays in the warm-Hive family. Each tile
* gets its hue on the title, top hairline, and hover border/glow so the grid
* reads as a cast of characters, not a spreadsheet. Hues are chosen so
* horizontally/vertically adjacent tiles (4-col landing grid) never repeat,
* and a few map to meaning (confused → terracotta flag, researcher/team →
* deep bronze, analyst/architect → copper).
*/
const PERSONA_ACCENTS: Readonly<Record<PersonaSlug, string>> = {
hunter: '#f6c45a',
researcher: '#c07e16',
analyst: '#d98a3d',
connector: '#f2b950',
architect: '#d98a3d',
builder: '#e9a52c',
writer: '#e0916f',
orchestrator: '#e9a52c',
marketer: '#f6c45a',
team: '#c07e16',
celebrating: '#f9d27e',
confused: '#db8068',
sleeping: '#86a9d1',
};
export interface BrandPersonasCardProps {
/** Optional uppercase kicker rendered above the heading (e.g. "Built for"). */
eyebrow?: string;
heading?: string;
subtitle?: string;
showFillerTiles?: boolean;
variant?: 'landing' | 'compact';
onTileHover?: (slug: PersonaSlug) => void;
onPersonaClick?: (slug: PersonaSlug) => void;
cta?: ReactNode;
}
/**
* Single-surface canon of the 13 Waggle bee personas.
*
* @remarks
* Copy is imported verbatim from `_data/personas.ts` — do not override in-place.
* Assets are loaded eagerly via `next/image` (optimizer serves ~256px
* AVIF/WebP of the 2048px source PNGs) with an `onError` fallback that flips
* the tile to a hex-texture placeholder. The placeholder auto-disables when an
* asset loads successfully, so shipping new PNGs requires no code change.
*
* @todo compact variant scaffolding — implement in future sprint
*/
export default function BrandPersonasCard({
eyebrow,
heading,
subtitle,
showFillerTiles = true,
variant = 'landing',
onTileHover,
onPersonaClick,
cta,
}: BrandPersonasCardProps) {
const t = useTranslations('landing.brand_personas');
const resolvedHeading = heading ?? t('default_heading');
const resolvedSubtitle = subtitle ?? t('default_subtitle');
const [erroredSlugs, setErroredSlugs] = useState<ReadonlySet<PersonaSlug>>(
() => new Set(),
);
const handleAssetError = useCallback((slug: PersonaSlug) => {
setErroredSlugs((prev) => {
if (prev.has(slug)) return prev;
const next = new Set(prev);
next.add(slug);
return next;
});
}, []);
if (variant === 'compact') {
return (
<div
data-testid="brand-personas-card-compact"
data-variant="compact"
aria-label={t('compact_aria')}
>
{/* Compact variant scaffolding — intentional stub.
TypeScript interface is stable; parent pages may wire props today
and receive a fuller layout in a future sprint without refactor. */}
</div>
);
}
return (
<section
data-testid="brand-personas-card"
data-variant="landing"
aria-labelledby="waggle-hive-heading"
style={sectionStyle}
>
<header style={headerStyle}>
{eyebrow ? <p style={eyebrowStyle}>{eyebrow}</p> : null}
<h2 id="waggle-hive-heading" style={headingStyle}>
{resolvedHeading}
</h2>
<p style={subtitleStyle}>{resolvedSubtitle}</p>
</header>
<ul
role="list"
data-testid="brand-personas-grid"
className="waggle-persona-grid"
>
{LANDING_GRID_SEQUENCE.map((entry, index) => {
if (entry === 'filler') {
if (!showFillerTiles) return null;
return (
<li
key={`filler-${index}`}
aria-hidden="true"
data-testid="brand-personas-filler"
className="waggle-persona-filler"
/>
);
}
const persona = personaByOrder.get(entry);
if (!persona) {
// Guard: should never happen — sequence mirrors canonical order.
return null;
}
return (
<PersonaTile
key={persona.slug}
persona={persona}
hasError={erroredSlugs.has(persona.slug)}
onAssetError={handleAssetError}
onPersonaClick={onPersonaClick}
onTileHover={onTileHover}
/>
);
})}
</ul>
{cta ? (
<div data-testid="brand-personas-cta" style={ctaWrapperStyle}>
{cta}
</div>
) : null}
<style>{scopedCss}</style>
</section>
);
}
interface PersonaTileProps {
persona: Persona;
hasError: boolean;
onAssetError: (slug: PersonaSlug) => void;
onPersonaClick?: (slug: PersonaSlug) => void;
onTileHover?: (slug: PersonaSlug) => void;
}
function PersonaTile({
persona,
hasError,
onAssetError,
onPersonaClick,
onTileHover,
}: PersonaTileProps) {
const handleClick = useCallback(() => {
onPersonaClick?.(persona.slug);
}, [onPersonaClick, persona.slug]);
const handleKey = useCallback(
(event: KeyboardEvent<HTMLElement>) => {
if (!onPersonaClick) return;
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
onPersonaClick(persona.slug);
}
},
[onPersonaClick, persona.slug],
);
const handleHover = useCallback(() => {
onTileHover?.(persona.slug);
}, [onTileHover, persona.slug]);
const handleFocus = useCallback(() => {
// Keyboard-only "hover" equivalent so a11y consumers get the same signal.
onTileHover?.(persona.slug);
}, [onTileHover, persona.slug]);
const isInteractive = Boolean(onPersonaClick);
const figure = (
<figure className="waggle-persona-figure">
<div className="waggle-persona-asset-frame">
{hasError ? (
<div
data-testid={`persona-placeholder-${persona.slug}`}
data-placeholder="true"
className="waggle-persona-placeholder"
aria-hidden="true"
>
<span className="waggle-persona-placeholder-dot" />
</div>
) : (
/* next/image (optimizer → ~256px AVIF/WebP) makes eager loading
affordable; the raw 2048px PNGs are ~2.5 MB each. Eager so a
tile never paints as an empty frame while lazy IO waits. */
<Image
src={persona.imagePath}
alt={persona.alt}
loading="eager"
width={256}
height={256}
className="waggle-persona-asset"
onError={() => onAssetError(persona.slug)}
/>
)}
</div>
<figcaption className="waggle-persona-caption">
<strong className="waggle-persona-title">{persona.title}</strong>
<span className="waggle-persona-role">{persona.role}</span>
</figcaption>
</figure>
);
const tileStyle = {
'--accent': PERSONA_ACCENTS[persona.slug],
} as CSSProperties;
return (
<li
data-testid={`persona-tile-${persona.slug}`}
data-slug={persona.slug}
data-placeholder={hasError ? 'true' : undefined}
className="waggle-persona-tile"
style={tileStyle}
onMouseEnter={handleHover}
onFocus={handleFocus}
>
{isInteractive ? (
<button
type="button"
className="waggle-persona-button"
aria-label={persona.alt}
onClick={handleClick}
onKeyDown={handleKey}
>
{figure}
</button>
) : (
figure
)}
</li>
);
}
/* ------------------------------------------------------------------ */
/* Inline styles (match existing apps/www convention — no Tailwind) */
/* ------------------------------------------------------------------ */
const sectionStyle: CSSProperties = {
background: 'var(--hive-950, #0e0c07)',
padding: '96px 24px',
width: '100%',
boxSizing: 'border-box',
};
const headerStyle: CSSProperties = {
maxWidth: 1200,
margin: '0 auto 48px',
textAlign: 'center',
};
const eyebrowStyle: CSSProperties = {
fontFamily: "var(--sans)",
fontSize: 11,
fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: '0.12em',
color: 'var(--honey-500, #e9a52c)',
margin: 0,
marginBottom: 12,
};
const headingStyle: CSSProperties = {
fontFamily: "var(--sans)",
fontSize: 'clamp(28px, 4vw, 32px)',
fontWeight: 700,
color: 'var(--hive-50, #f6f1e4)',
margin: 0,
marginBottom: 12,
};
const subtitleStyle: CSSProperties = {
fontFamily: "var(--sans)",
fontSize: 'clamp(16px, 2vw, 18px)',
fontWeight: 400,
color: 'var(--hive-300, #c8bfa9)',
margin: 0,
};
const ctaWrapperStyle: CSSProperties = {
maxWidth: 1200,
margin: '48px auto 0',
display: 'flex',
justifyContent: 'center',
};
/* ------------------------------------------------------------------ */
/* Scoped CSS — component-local selectors to avoid global collisions */
/* ------------------------------------------------------------------ */
const scopedCss = `
.waggle-persona-grid {
list-style: none;
padding: 0;
margin: 0 auto;
max-width: 1200px;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 16px;
}
@media (max-width: 1023px) {
.waggle-persona-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.waggle-persona-filler { display: none; }
}
@media (max-width: 640px) {
.waggle-persona-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.waggle-persona-tile {
position: relative;
list-style: none;
overflow: hidden;
background: linear-gradient(180deg, #14110b 0%, #0e0c07 100%);
border: 1px solid #1f1a12;
border-radius: 16px;
padding: 20px;
min-height: 260px;
transition: border-color 200ms ease-out, transform 200ms ease-out,
box-shadow 200ms ease-out;
}
/* Per-role accent hairline across the top edge of each tile. */
.waggle-persona-tile::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: var(--accent, #e9a52c);
opacity: 0.5;
transition: opacity 200ms ease-out;
}
.waggle-persona-tile:hover,
.waggle-persona-tile:focus-within {
border-color: var(--accent, #e9a52c);
transform: translateY(-3px);
box-shadow: 0 12px 34px -16px color-mix(in srgb, var(--accent, #e9a52c) 55%, transparent);
}
.waggle-persona-tile:hover::before,
.waggle-persona-tile:focus-within::before {
opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
.waggle-persona-tile,
.waggle-persona-tile:hover,
.waggle-persona-tile:focus-within {
transition: none;
transform: none;
}
}
.waggle-persona-button {
all: unset;
display: block;
width: 100%;
cursor: pointer;
border-radius: 12px;
}
.waggle-persona-button:focus-visible {
outline: 2px solid var(--accent, #e9a52c);
outline-offset: 2px;
}
.waggle-persona-figure {
margin: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.waggle-persona-asset-frame {
position: relative;
width: 100%;
aspect-ratio: 1 / 1;
max-width: 256px;
display: flex;
align-items: center;
justify-content: center;
}
.waggle-persona-asset {
position: relative;
width: 100%;
height: 100%;
object-fit: contain;
display: block;
/* The mascot PNGs are background-transparent (2026-07-06 flood-fill) —
they sit directly on the card gradient. The old dark vignette +
edge-fade masks compensated for baked-black squares and are gone:
with real alpha they READ as a dark box behind the art. */
filter: drop-shadow(0 10px 24px rgba(0, 0, 0, 0.35));
}
.waggle-persona-placeholder {
width: 100%;
height: 100%;
background-image: url("${HEX_TEXTURE_PATH}");
background-size: cover;
background-position: center;
background-color: #14110b;
opacity: 0.6;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.waggle-persona-placeholder-dot {
display: block;
width: 48px;
height: 48px;
border-radius: 50%;
background: #f6c45a;
box-shadow: 0 0 24px rgba(246, 196, 90, 0.4);
}
.waggle-persona-caption {
text-align: center;
display: flex;
flex-direction: column;
gap: 6px;
}
.waggle-persona-title {
font-family: var(--sans);
font-size: 16px;
font-weight: 600;
color: var(--accent, #f6c45a);
letter-spacing: 0.01em;
}
.waggle-persona-role {
font-family: var(--sans);
font-size: 13px;
font-weight: 400;
color: #c8bfa9;
line-height: 1.45;
}
/* Filler slots keep the staggered grid rhythm without reading as broken
cards: no border, no card surface — an outlined-but-hollow frame looks
like missing content. Instead: ambient comb texture that fades out
radially, so the corners read as intentional negative space.
Decorative only (aria-hidden on the element). */
.waggle-persona-filler {
position: relative;
list-style: none;
min-height: 260px;
border-radius: 16px;
overflow: hidden;
/* Round-7: complete the falloff — the texture dissolves toward the grid's
outer edge so the ghost reads as intentional ambience, never as an
unloaded card. */
-webkit-mask-image: linear-gradient(to right, rgba(0,0,0,0.8), rgba(0,0,0,0.15));
mask-image: linear-gradient(to right, rgba(0,0,0,0.8), rgba(0,0,0,0.15));
}
.waggle-persona-filler:nth-of-type(1),
li.waggle-persona-filler:first-child {
-webkit-mask-image: linear-gradient(to left, rgba(0,0,0,0.8), rgba(0,0,0,0.15));
mask-image: linear-gradient(to left, rgba(0,0,0,0.8), rgba(0,0,0,0.15));
}
/* Round-6: fainter + slightly shrunken so the ghosts can't be mistaken for
unloaded cards — clearly ambient texture, not content-in-waiting. */
.waggle-persona-filler::before {
content: "";
position: absolute;
inset: 0;
background:
radial-gradient(circle at 50% 46%, rgba(233, 165, 44, 0.10), rgba(233, 165, 44, 0) 62%),
url("${HEX_TEXTURE_PATH}") center / cover no-repeat;
opacity: 0.2;
transform: scale(0.88);
-webkit-mask-image: radial-gradient(circle at 50% 50%, #000 22%, transparent 74%);
mask-image: radial-gradient(circle at 50% 50%, #000 22%, transparent 74%);
}
.waggle-persona-filler::after {
content: "";
position: absolute;
left: 50%;
top: 50%;
width: 34px;
height: 38px;
transform: translate(-50%, -50%);
background: no-repeat center / contain
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='42' height='46' viewBox='0 0 42 46' fill='none'%3E%3Cpath d='M21 2 L39 12.5 V33.5 L21 44 L3 33.5 V12.5 Z' stroke='%23e9a52c' stroke-width='1.4' stroke-linejoin='round'/%3E%3C/svg%3E");
opacity: 0.18;
}
`;

View File

@@ -0,0 +1,71 @@
'use client';
import { useEffect, useState, type CSSProperties, type ReactNode } from 'react';
import { useTranslations } from 'next-intl';
import { detectOSFromUserAgent, type OSId } from '../_lib/os-detection';
import { emit, events } from '../_lib/event-taxonomy';
interface DownloadCTAProps {
readonly variant?: 'primary' | 'ghost';
readonly size?: 'default' | 'small';
readonly section: 'hero' | 'navbar' | 'solo-tier' | 'final-cta';
readonly children?: ReactNode;
readonly style?: CSSProperties;
}
const DOWNLOAD_URL = '/download';
/**
* OS-aware download CTA. Renders a generic "Download" label at SSR + first
* paint, then swaps to "Download for {os}" after hydration via
* `navigator.userAgent` detection.
*
* Styling comes from the shared `.btn` primitives in globals.css so every
* download button on the page is pixel-identical. Strings live in
* `messages/en.json` under `landing.download_cta.*` with an ICU placeholder
* for the OS name.
*/
export default function DownloadCTA({
variant = 'primary',
size = 'default',
section,
children,
style,
}: DownloadCTAProps) {
const t = useTranslations('landing.download_cta');
const [os, setOS] = useState<OSId | null>(null);
useEffect(() => {
if (typeof navigator !== 'undefined') {
setOS(detectOSFromUserAgent(navigator.userAgent));
}
}, []);
const label = children ?? (os ? t('with_os', { os }) : t('default'));
const className = [
'btn',
variant === 'primary' ? 'btn-primary' : 'btn-ghost',
size === 'small' ? 'btn-small' : '',
]
.filter(Boolean)
.join(' ');
const handleClick = () => {
emit({
name: events.ctaClick,
properties: { section, os: os ?? 'unknown' },
});
};
return (
<a
href={DOWNLOAD_URL}
className={className}
onClick={handleClick}
style={style}
>
{label}
</a>
);
}

View File

@@ -0,0 +1,67 @@
.header {
max-width: 720px;
margin: 0 auto 64px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.item {
padding: 28px 26px 30px;
border-radius: var(--r-lg);
background: var(--hive-900);
border: 1px solid var(--line-soft);
transition: border-color 0.2s ease, transform 0.2s ease,
box-shadow 0.2s ease;
}
.item:hover {
border-color: var(--honey-line);
box-shadow: var(--shadow-honey);
transform: translateY(-2px);
}
.icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 38px;
height: 38px;
border-radius: 11px;
border: 1px solid var(--honey-line);
background: var(--honey-wash);
color: var(--honey-400);
margin-bottom: 18px;
}
.title {
font-size: 16px;
font-weight: 600;
color: var(--hive-50);
margin-bottom: 10px;
}
.body {
font-size: var(--text-small);
line-height: 1.6;
color: var(--hive-300);
}
@media (max-width: 1023px) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 640px) {
.grid {
grid-template-columns: 1fr;
}
}

View File

@@ -0,0 +1,127 @@
import { getTranslations } from 'next-intl/server';
import Reveal from './Reveal';
import styles from './FeatureGrid.module.css';
type FeatureKey =
| 'personas'
| 'models'
| 'harvest'
| 'loops'
| 'skills'
| 'memory_center';
const FEATURES: readonly FeatureKey[] = [
'personas',
'models',
'harvest',
'loops',
'skills',
'memory_center',
];
/**
* Six feature cards, each naming a real subsystem (persona roster, LiteLLM
* routing, Harvest, Loops + approval queue, skills/connectors/MCP catalog,
* Memory Center). Strings under `landing.features.*`.
*/
export default async function FeatureGrid() {
const t = await getTranslations('landing.features');
return (
<section
id="features"
className="section"
aria-labelledby="features-heading"
>
<div className="container-wide">
<header className={styles.header}>
<p className="eyebrow">{t('eyebrow')}</p>
<h2 id="features-heading" className="section-headline">
{t('headline')}
</h2>
</header>
<div className={styles.grid}>
{FEATURES.map((key, i) => (
<Reveal key={key} delay={((i % 3) + 1) as 1 | 2 | 3}>
<div className={styles.item}>
<span className={styles.icon} aria-hidden="true">
<FeatureIcon feature={key} />
</span>
<h3 className={styles.title}>{t(`items.${key}.title`)}</h3>
<p className={styles.body}>{t(`items.${key}.body`)}</p>
</div>
</Reveal>
))}
</div>
</div>
</section>
);
}
/** Minimal 18px line icons — one per subsystem, stroke inherits currentColor. */
function FeatureIcon({ feature }: { readonly feature: FeatureKey }) {
const common = {
width: 18,
height: 18,
viewBox: '0 0 18 18',
fill: 'none',
stroke: 'currentColor',
strokeWidth: 1.5,
strokeLinecap: 'round',
strokeLinejoin: 'round',
} as const;
switch (feature) {
case 'personas':
return (
<svg {...common} aria-hidden="true">
<circle cx="6" cy="6.5" r="2.6" />
<path d="M1.8 14.8 C2.4 11.8 4.4 10.6 6 10.6 C7.6 10.6 9.6 11.8 10.2 14.8" />
<circle cx="12.8" cy="5.4" r="2" />
<path d="M10.9 9.4 C12 8.8 14.6 9 16.2 12.4" />
</svg>
);
case 'models':
return (
<svg {...common} aria-hidden="true">
<circle cx="9" cy="9" r="2.2" />
<path d="M9 6.8 V2.2 M9 11.2 V15.8 M6.8 9 H2.2 M11.2 9 H15.8" />
<circle cx="9" cy="2.2" r="1.2" />
<circle cx="9" cy="15.8" r="1.2" />
<circle cx="2.2" cy="9" r="1.2" />
<circle cx="15.8" cy="9" r="1.2" />
</svg>
);
case 'harvest':
return (
<svg {...common} aria-hidden="true">
<path d="M9 2 V11 M5.4 7.6 L9 11.2 L12.6 7.6" />
<path d="M2.5 12.5 V14.5 C2.5 15.3 3.2 16 4 16 H14 C14.8 16 15.5 15.3 15.5 14.5 V12.5" />
</svg>
);
case 'loops':
return (
<svg {...common} aria-hidden="true">
<path d="M14.5 7 A6 6 0 1 0 15.5 10.5" />
<path d="M15.8 3.4 L15.8 7.2 L12 7.2" />
</svg>
);
case 'skills':
return (
<svg {...common} aria-hidden="true">
<rect x="2.4" y="2.4" width="5.6" height="5.6" rx="1.4" />
<rect x="10" y="2.4" width="5.6" height="5.6" rx="1.4" />
<rect x="2.4" y="10" width="5.6" height="5.6" rx="1.4" />
<path d="M12.8 10.4 V15.2 M10.4 12.8 H15.2" />
</svg>
);
case 'memory_center':
return (
<svg {...common} aria-hidden="true">
<path d="M9 1.8 L15.2 5.4 L15.2 12.6 L9 16.2 L2.8 12.6 L2.8 5.4 Z" />
<circle cx="9" cy="9" r="2.4" />
</svg>
);
}
}

View File

@@ -0,0 +1,71 @@
.section {
position: relative;
padding: var(--section-pad) var(--gutter);
text-align: center;
overflow: hidden;
border-top: 1px solid var(--line-soft);
}
.glow {
position: absolute;
bottom: -300px;
left: 50%;
transform: translateX(-50%);
width: 900px;
height: 520px;
background: radial-gradient(
ellipse at center,
rgba(233, 165, 44, 0.1) 0%,
rgba(233, 165, 44, 0.03) 45%,
transparent 70%
);
pointer-events: none;
}
.inner {
position: relative;
max-width: 640px;
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: center;
}
.headline {
font-size: clamp(2.125rem, 4.5vw, 3.25rem);
font-weight: 800;
letter-spacing: -0.03em;
color: var(--hive-50);
margin-bottom: 18px;
}
.subhead {
font-size: var(--text-lead);
line-height: 1.6;
color: var(--hive-300);
margin-bottom: 36px;
}
.ctaRow {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 14px;
margin-bottom: 40px;
}
.kvark {
font-size: 13px;
color: var(--text-muted);
}
.kvarkLink {
color: var(--honey-400);
text-decoration: none;
font-weight: 600;
margin-left: 6px;
}
.kvarkLink:hover {
color: var(--honey-300);
}

View File

@@ -0,0 +1,51 @@
import { getTranslations } from 'next-intl/server';
import DownloadCTA from './DownloadCTA';
import styles from './FinalCTA.module.css';
const WAGGLE_REPO_URL = 'https://github.com/marolinik/waggle-os';
const KVARK_CONTACT =
'mailto:kvark@egzakta.com?subject=Waggle%20%E2%86%92%20KVARK%20sovereign%20deployment';
/**
* Closing CTA — echoes the hero promise, then routes to Download or GitHub,
* with the KVARK sovereign-deployment escape hatch underneath. Strings
* under `landing.final_cta.*`.
*/
export default async function FinalCTA() {
const t = await getTranslations('landing.final_cta');
return (
<section
id="final-cta"
className={`${styles.section} honeycomb-bg`}
aria-labelledby="final-heading"
>
<div className={styles.glow} aria-hidden="true" />
<div className={styles.inner}>
<h2 id="final-heading" className={styles.headline}>
{t('headline')}
</h2>
<p className={styles.subhead}>{t('subhead')}</p>
<div className={styles.ctaRow}>
<DownloadCTA section="final-cta" variant="primary" />
<a
href={WAGGLE_REPO_URL}
target="_blank"
rel="noopener noreferrer"
className="btn btn-ghost"
>
{t('cta_github')}
</a>
</div>
<p className={styles.kvark}>
{t('kvark_text')}
<a href={KVARK_CONTACT} className={styles.kvarkLink}>
{t('kvark_cta')}
</a>
</p>
</div>
</section>
);
}

View File

@@ -0,0 +1,95 @@
.footer {
padding: 72px var(--gutter) 32px;
background: var(--hive-950);
border-top: 1px solid var(--line-soft);
}
.grid {
max-width: var(--container-wide);
margin: 0 auto 56px;
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr 1fr;
gap: 48px;
}
.brandBlock {
max-width: 320px;
}
.brandDescription {
font-size: 13px;
line-height: 1.6;
color: var(--hive-300);
margin-top: 14px;
margin-bottom: 12px;
}
.attribution {
font-size: 12px;
color: var(--text-muted);
}
.columnTitle {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--hive-200);
margin-bottom: 16px;
}
.columnList {
list-style: none;
padding: 0;
margin: 0;
}
.columnList li {
margin-bottom: 10px;
}
.columnLink {
font-size: 13px;
color: var(--hive-300);
text-decoration: none;
transition: color 0.15s ease;
}
.columnLink:hover {
color: var(--hive-50);
}
.baseline {
max-width: var(--container-wide);
margin: 0 auto;
padding-top: 24px;
border-top: 1px solid var(--hive-800);
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
font-size: 12px;
color: var(--text-muted);
}
.baselineRight {
font-family: var(--mono);
}
@media (max-width: 1023px) {
.grid {
grid-template-columns: 1fr 1fr 1fr;
gap: 32px;
}
}
@media (max-width: 640px) {
.grid {
grid-template-columns: 1fr 1fr;
}
.baseline {
justify-content: flex-start;
flex-direction: column;
}
}

View File

@@ -0,0 +1,105 @@
import { getTranslations } from 'next-intl/server';
import BrandMark from './BrandMark';
import styles from './Footer.module.css';
interface FooterLink {
readonly key: string;
readonly href: string;
readonly external?: boolean;
}
/**
* Footer link map. Rule: every link must resolve to a real destination —
* no `#` placeholders. Columns whose content does not exist yet (blog,
* press, changelog) are omitted until they do.
*/
const PRODUCT_LINKS: readonly FooterLink[] = [
{
key: 'download',
href: '/download',
},
{ key: 'pricing', href: '/#pricing' },
{ key: 'how_it_works', href: '/#how-it-works' },
{ key: 'memory', href: '/#memory' },
];
const RESEARCH_LINKS: readonly FooterLink[] = [
{ key: 'methodology', href: '/docs/methodology' },
{
key: 'benchmarks',
href: 'https://github.com/marolinik/hive-mind',
external: true,
},
{
key: 'hive_mind',
href: 'https://github.com/marolinik/hive-mind',
external: true,
},
];
const COMPANY_LINKS: readonly FooterLink[] = [
{ key: 'about_egzakta', href: 'https://egzakta.com', external: true },
{ key: 'kvark', href: 'https://www.kvark.ai', external: true },
{ key: 'contact', href: 'mailto:hello@egzakta.com' },
];
const LEGAL_LINKS: readonly FooterLink[] = [
{ key: 'terms', href: '/terms' },
{ key: 'privacy', href: '/privacy' },
{ key: 'cookies', href: '/cookies' },
{ key: 'eu_ai_act', href: '/eu-ai-act' },
{
key: 'apache',
href: 'https://github.com/marolinik/hive-mind/blob/master/LICENSE',
external: true,
},
];
const COLUMN_DEFS = [
{ ns: 'product', links: PRODUCT_LINKS },
{ ns: 'research', links: RESEARCH_LINKS },
{ ns: 'company', links: COMPANY_LINKS },
{ ns: 'legal', links: LEGAL_LINKS },
] as const;
export default async function Footer() {
const t = await getTranslations('landing.footer');
return (
<footer id="footer" className={styles.footer}>
<div className={styles.grid}>
<div className={styles.brandBlock}>
<BrandMark withWordmark />
<p className={styles.brandDescription}>{t('brand.description')}</p>
<p className={styles.attribution}>{t('brand.attribution')}</p>
</div>
{COLUMN_DEFS.map((col) => (
<div key={col.ns}>
<h3 className={styles.columnTitle}>{t(`columns.${col.ns}.title`)}</h3>
<ul className={styles.columnList}>
{col.links.map((l) => (
<li key={l.key}>
<a
href={l.href}
{...(l.external
? { target: '_blank', rel: 'noopener noreferrer' }
: null)}
className={styles.columnLink}
>
{t(`columns.${col.ns}.links.${l.key}`)}
</a>
</li>
))}
</ul>
</div>
))}
</div>
<div className={styles.baseline}>
<span>{t('base_line.left')}</span>
<span className={styles.baselineRight}>{t('base_line.right')}</span>
</div>
</footer>
);
}

View File

@@ -0,0 +1,160 @@
.section {
position: relative;
padding: 168px var(--gutter) 104px;
overflow: hidden;
}
.glow {
position: absolute;
top: -240px;
left: 50%;
transform: translateX(-50%);
width: 900px;
height: 560px;
background: radial-gradient(
ellipse at center,
rgba(233, 165, 44, 0.09) 0%,
rgba(233, 165, 44, 0.03) 45%,
transparent 70%
);
pointer-events: none;
}
.grid {
position: relative;
max-width: var(--container-wide);
margin: 0 auto;
display: grid;
grid-template-columns: minmax(0, 10fr) minmax(0, 9fr);
gap: 64px;
align-items: center;
}
.copy {
max-width: 560px;
}
.headline {
font-size: var(--text-display);
font-weight: 800;
line-height: 1.05;
color: var(--hive-50);
margin-bottom: 24px;
}
.headlineEmphasis {
color: var(--honey-400);
}
.subhead {
font-size: var(--text-lead);
line-height: 1.6;
color: var(--hive-300);
margin-bottom: 36px;
max-width: 34em;
}
.ctaRow {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 14px;
margin-bottom: 28px;
}
.microcopy {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 18px;
row-gap: 8px;
font-family: var(--mono);
font-size: var(--text-xs);
color: var(--text-muted);
letter-spacing: 0.02em;
}
.microItem {
display: inline-flex;
align-items: center;
gap: 8px;
}
.microDot {
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--honey-500);
display: inline-block;
}
.visual {
display: flex;
align-items: center;
justify-content: center;
min-width: 0;
}
@media (max-width: 1023px) {
.section {
padding-top: 136px;
padding-bottom: 80px;
}
.grid {
grid-template-columns: 1fr;
gap: 56px;
}
.copy {
max-width: 640px;
}
}
/* Round-7: one benchmark strip in the hero's dead bottom quarter.
Wave R Lane E: the 86.49% number is lifted to a flagship stat chip
(larger mono, honey-wash lozenge) so the leaderboard claim carries weight
near the CTAs; the rest stays quiet supporting mono copy. */
.proofStrip {
display: inline-flex;
align-items: center;
flex-wrap: wrap;
gap: 12px;
margin-top: 28px;
max-width: 40em;
color: var(--honey-500);
text-decoration: none;
}
.proofStat {
flex-shrink: 0;
display: inline-flex;
align-items: baseline;
padding: 6px 12px;
border-radius: 10px;
background: var(--honey-wash);
border: 1px solid var(--honey-line);
font-family: var(--mono);
font-size: 1.5rem;
font-weight: 700;
line-height: 1;
letter-spacing: -0.02em;
color: var(--honey-400);
transition: border-color 0.15s ease, background 0.15s ease;
}
.proofLabel {
font-family: var(--mono);
font-size: var(--text-xs);
letter-spacing: 0.02em;
line-height: 1.5;
color: var(--text-muted);
}
.proofStrip:hover .proofStat {
border-color: var(--honey-500);
background: rgba(233, 165, 44, 0.16);
}
.proofStrip:hover .proofLabel {
color: var(--hive-300);
}

View File

@@ -0,0 +1,71 @@
import { getTranslations } from 'next-intl/server';
import DownloadCTA from './DownloadCTA';
import HeroVisual from './HeroVisual';
import styles from './Hero.module.css';
const MICROCOPY_KEYS = [
'microcopy_free',
'microcopy_platforms',
'microcopy_oss',
] as const;
/**
* Hero — single committed headline (the A/B variant infra was removed with
* the 2026-07 rebuild). Two columns: positioning copy left, the memory-core
* window visual right. All strings under `landing.hero.*`.
*/
export default async function Hero() {
const t = await getTranslations('landing.hero');
return (
<section id="hero" className={`${styles.section} honeycomb-bg`}>
<div className={styles.glow} aria-hidden="true" />
<div className={styles.grid}>
<div className={styles.copy}>
<p className="eyebrow">{t('eyebrow')}</p>
<h1 className={styles.headline}>
{t('headline_lead')}{' '}
<span className={styles.headlineEmphasis}>
{t('headline_emphasis')}
</span>
.
</h1>
<p className={styles.subhead}>{t('subhead')}</p>
<div className={styles.ctaRow}>
<DownloadCTA section="hero" variant="primary" />
<a href="#proof" className="btn btn-ghost">
{t('cta_secondary')}
</a>
</div>
<ul className={styles.microcopy}>
{MICROCOPY_KEYS.map((key) => (
<li key={key} className={styles.microItem}>
<span aria-hidden="true" className={styles.microDot} />
{t(key)}
</li>
))}
</ul>
{/* Round-7: the hero's dead bottom quarter carries the product's
strongest proof — one benchmark strip, linking to #proof.
Wave R Lane E: the 86.49% number is lifted to a flagship stat
(larger mono, honey) so the leaderboard claim carries visual
weight near the CTAs; the rest stays quiet supporting copy. */}
<a href="#proof" className={styles.proofStrip}>
<span className={styles.proofStat}>{t('proof_stat')}</span>
<span className={styles.proofLabel}>{t('proof_strip')}</span>
<span aria-hidden="true"></span>
</a>
</div>
<div className={styles.visual}>
<HeroVisual />
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,137 @@
.window {
width: 100%;
max-width: 560px;
border-radius: var(--r-lg);
border: 1px solid var(--line);
background: linear-gradient(180deg, var(--hive-900) 0%, var(--hive-950) 100%);
box-shadow: var(--shadow-elevated);
overflow: hidden;
}
.titleBar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 14px;
border-bottom: 1px solid var(--line-soft);
background: rgba(31, 26, 18, 0.6);
}
.trafficDots {
display: inline-flex;
gap: 6px;
}
.trafficDot {
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--hive-600);
opacity: 0.7;
}
.titleText {
font-family: var(--mono);
font-size: 11px;
color: var(--text-muted);
flex: 1;
text-align: center;
}
.titleBadge {
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.04em;
color: var(--honey-400);
border: 1px solid var(--honey-line);
background: var(--honey-wash);
border-radius: 999px;
padding: 2px 9px;
white-space: nowrap;
}
.svg {
display: block;
width: 100%;
height: auto;
}
.footerBar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 9px 14px;
border-top: 1px solid var(--line-soft);
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.03em;
color: var(--text-muted);
}
/* Edge pulse: honey energy flowing between memory and models. */
.edge {
stroke: var(--line-strong);
stroke-width: 1.2;
fill: none;
}
.edgePulse {
stroke: var(--honey-500);
stroke-width: 1.4;
fill: none;
stroke-dasharray: 10 110;
stroke-linecap: round;
opacity: 0.8;
animation: edge-flow 3.2s linear infinite;
}
.edgePulse2 {
animation-delay: 0.8s;
}
.edgePulse3 {
animation-delay: 1.6s;
}
.edgePulse4 {
animation-delay: 2.4s;
}
@keyframes edge-flow {
from {
stroke-dashoffset: 120;
}
to {
stroke-dashoffset: 0;
}
}
.coreGlow {
animation: core-breathe 4s ease-in-out infinite;
transform-origin: center;
transform-box: fill-box;
}
@keyframes core-breathe {
0%,
100% {
opacity: 0.5;
}
50% {
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.edgePulse {
animation: none;
stroke-dasharray: none;
opacity: 0.35;
}
.coreGlow {
animation: none;
}
}

View File

@@ -0,0 +1,160 @@
import { getTranslations } from 'next-intl/server';
import styles from './HeroVisual.module.css';
const CHIPS = [
{ key: 'claude', x: 28, y: 36, flow: 'out' },
{ key: 'gpt', x: 374, y: 36, flow: 'out' },
{ key: 'qwen', x: 28, y: 258, flow: 'in' },
{ key: 'gemini', x: 374, y: 258, flow: 'out' },
] as const;
const CHIP_W = 118;
const CHIP_H = 44;
/**
* Hero visualization — a desktop-app window (Waggle ships as a Tauri
* binary) framing the true architecture: one local memory core serving
* four model chips. Edges pulse honey; the `qwen · local` edge flows
* INTO the core (commit) while the others flow out (recall).
*
* Server component: the animation is pure CSS (HeroVisual.module.css),
* disabled under `prefers-reduced-motion`. No invented numbers anywhere —
* labels name real subsystems only.
*/
export default async function HeroVisual() {
const t = await getTranslations('landing.hero_visual');
return (
<figure className={styles.window} aria-label={t('aria_label')}>
<div className={styles.titleBar}>
<span className={styles.trafficDots} aria-hidden="true">
<span className={styles.trafficDot} />
<span className={styles.trafficDot} />
<span className={styles.trafficDot} />
</span>
<span className={styles.titleText}>{t('window_title')}</span>
<span className={styles.titleBadge}>{t('window_badge')}</span>
</div>
<svg
className={styles.svg}
viewBox="0 0 520 340"
role="img"
aria-hidden="true"
focusable="false"
>
<defs>
<radialGradient id="hv-core-glow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stopColor="rgba(233,165,44,0.22)" />
<stop offset="70%" stopColor="rgba(233,165,44,0.05)" />
<stop offset="100%" stopColor="rgba(233,165,44,0)" />
</radialGradient>
</defs>
{/* Edges: base line + honey pulse. Recall edges run core → chip;
the commit edge (qwen · local) runs chip → core. */}
<path className={styles.edge} d="M 232,140 C 200,112 182,84 150,62" />
<path
className={`${styles.edgePulse}`}
d="M 232,140 C 200,112 182,84 150,62"
/>
<path className={styles.edge} d="M 288,140 C 320,112 338,84 370,62" />
<path
className={`${styles.edgePulse} ${styles.edgePulse2}`}
d="M 288,140 C 320,112 338,84 370,62"
/>
<path className={styles.edge} d="M 150,278 C 182,256 200,224 232,196" />
<path
className={`${styles.edgePulse} ${styles.edgePulse3}`}
d="M 150,278 C 182,256 200,224 232,196"
/>
<path className={styles.edge} d="M 288,196 C 320,224 338,256 370,278" />
<path
className={`${styles.edgePulse} ${styles.edgePulse4}`}
d="M 288,196 C 320,224 338,256 370,278"
/>
{/* Memory core */}
<circle
className={styles.coreGlow}
cx="260"
cy="168"
r="78"
fill="url(#hv-core-glow)"
/>
<path
d="M260 112 L308.5 140 L308.5 196 L260 224 L211.5 196 L211.5 140 Z"
fill="rgba(31, 26, 18, 0.85)"
stroke="var(--honey-500)"
strokeWidth="1.6"
strokeLinejoin="round"
/>
<text
x="260"
y="165"
textAnchor="middle"
fill="var(--hive-50)"
fontSize="13.5"
fontWeight="600"
fontFamily="var(--sans)"
>
{t('center_label')}
</text>
<text
x="260"
y="184"
textAnchor="middle"
fill="var(--text-muted)"
fontSize="9"
fontFamily="var(--mono)"
>
{t('center_sublabel')}
</text>
{/* Model chips */}
{CHIPS.map((chip) => (
<g key={chip.key}>
<rect
x={chip.x}
y={chip.y}
width={CHIP_W}
height={CHIP_H}
rx="10"
fill="var(--surface)"
stroke="var(--line-strong)"
strokeWidth="1"
/>
<text
x={chip.x + 14}
y={chip.y + 19}
fill="var(--hive-100)"
fontSize="12"
fontWeight="600"
fontFamily="var(--mono)"
>
{t(`chips.${chip.key}_primary`)}
</text>
<text
x={chip.x + 14}
y={chip.y + 33}
fill={chip.flow === 'in' ? 'var(--honey-400)' : 'var(--text-muted)'}
fontSize="9"
fontFamily="var(--mono)"
>
{chip.flow === 'in' ? '↑ ' : '↓ '}
{t(`chips.${chip.key}_sub`)}
</text>
</g>
))}
</svg>
<div className={styles.footerBar}>
<span>{t('footer_left')}</span>
<span>{t('footer_right')}</span>
</div>
</figure>
);
}

View File

@@ -0,0 +1,93 @@
.header {
max-width: 720px;
margin: 0 auto 52px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
.steps {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
position: relative;
}
.step {
position: relative;
padding: 30px 28px 32px;
border-radius: var(--r-lg);
background: linear-gradient(180deg, var(--hive-900) 0%, var(--hive-950) 100%);
border: 1px solid var(--line-soft);
transition: border-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
}
.step:hover {
border-color: var(--honey-line);
transform: translateY(-3px);
box-shadow: var(--shadow-honey);
}
/* Flow connector: a honey chevron seated in the gap before every step after
the first, so the three cards read as one sequence (01 → 02 → 03).
Each step is wrapped in a <Reveal> div, so the chevron hangs off the reveal
wrapper (the actual grid cell), not the inner .step card. */
.steps > :global(.reveal) {
position: relative;
}
.steps > :global(.reveal) + :global(.reveal)::before {
content: '';
position: absolute;
top: 51px;
left: -22px;
width: 20px;
height: 20px;
background: no-repeat center / contain
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath d='M7 4l6 6-6 6' stroke='%23e9a52c' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
opacity: 0.75;
z-index: 2;
}
.stepNumber {
font-family: var(--mono);
font-size: 13px;
font-weight: 600;
color: var(--honey-500);
display: inline-flex;
align-items: center;
justify-content: center;
width: 42px;
height: 42px;
border-radius: 12px;
border: 1px solid var(--honey-line);
background: var(--honey-wash);
box-shadow: 0 0 20px rgba(233, 165, 44, 0.12);
margin-bottom: 22px;
}
.stepTitle {
font-size: var(--text-h3);
font-weight: 600;
color: var(--hive-50);
margin-bottom: 12px;
}
.stepBody {
font-size: var(--text-small);
line-height: 1.65;
color: var(--hive-300);
}
@media (max-width: 860px) {
.steps {
grid-template-columns: 1fr;
gap: 20px;
}
/* Chevrons point downward when the flow stacks vertically. */
.steps > :global(.reveal) + :global(.reveal)::before {
top: -21px;
left: 30px;
transform: rotate(90deg);
}
}

View File

@@ -0,0 +1,44 @@
import { getTranslations } from 'next-intl/server';
import Reveal from './Reveal';
import styles from './HowItWorks.module.css';
const STEPS = ['step_01', 'step_02', 'step_03'] as const;
/**
* Three-step product story: import history → work in workspaces → memory
* compounds. Strings under `landing.how_it_works.*`.
*/
export default async function HowItWorks() {
const t = await getTranslations('landing.how_it_works');
return (
<section
id="how-it-works"
className="section"
aria-labelledby="how-heading"
>
<div className="container">
<header className={styles.header}>
<p className="eyebrow">{t('eyebrow')}</p>
<h2 id="how-heading" className="section-headline">
{t('headline')}
</h2>
</header>
<div className={styles.steps}>
{STEPS.map((step, i) => (
<Reveal key={step} delay={(i + 1) as 1 | 2 | 3}>
<div className={styles.step}>
<span className={styles.stepNumber} aria-hidden="true">
{t(`${step}.number`)}
</span>
<h3 className={styles.stepTitle}>{t(`${step}.title`)}</h3>
<p className={styles.stepBody}>{t(`${step}.body`)}</p>
</div>
</Reveal>
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,89 @@
.grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 72px;
align-items: center;
}
.body {
font-size: var(--text-body);
line-height: 1.7;
color: var(--hive-300);
margin-top: 20px;
max-width: 34em;
}
.chips {
list-style: none;
padding: 0;
margin: 28px 0 0;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.chip {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.02em;
color: var(--hive-200);
border: 1px solid var(--line-strong);
background: var(--surface);
border-radius: 999px;
padding: 7px 14px;
}
/* ── Pipeline card ── */
.pipeline {
border-radius: var(--r-lg);
border: 1px solid var(--line);
background: linear-gradient(180deg, var(--hive-900) 0%, var(--hive-950) 100%);
box-shadow: var(--shadow-elevated);
padding: 32px 36px;
display: flex;
flex-direction: column;
align-items: stretch;
}
.node {
border-radius: var(--r);
border: 1px solid var(--line-strong);
background: var(--surface);
padding: 14px 18px;
text-align: center;
}
.nodeAccent {
border-color: var(--honey-line);
background: var(--honey-wash);
}
.nodeTitle {
font-size: 14px;
font-weight: 600;
color: var(--hive-50);
display: block;
}
.nodeSub {
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.03em;
color: var(--text-muted);
display: block;
margin-top: 4px;
}
.connector {
display: flex;
justify-content: center;
padding: 6px 0;
color: var(--honey-600);
}
@media (max-width: 1023px) {
.grid {
grid-template-columns: minmax(0, 1fr);
gap: 48px;
}
}

View File

@@ -0,0 +1,80 @@
import { getTranslations } from 'next-intl/server';
import Reveal from './Reveal';
import styles from './MemoryDiagram.module.css';
const CHIP_KEYS = ['local', 'provenance', 'erasure'] as const;
/**
* "Under the hood" — the real memory pipeline, named after the actual
* subsystems (frames → hybrid search → knowledge graph → any model).
* Copy under `landing.memory.*`; the diagram is semantic HTML so it
* stacks naturally and needs no JS.
*/
export default async function MemoryDiagram() {
const t = await getTranslations('landing.memory');
return (
<section id="memory" className="section" aria-labelledby="memory-heading">
<div className="container">
<div className={styles.grid}>
<div>
<p className="eyebrow">{t('eyebrow')}</p>
<h2 id="memory-heading" className="section-headline">
{t('headline')}
</h2>
<p className={styles.body}>{t('body')}</p>
<ul className={styles.chips}>
{CHIP_KEYS.map((key) => (
<li key={key} className={styles.chip}>
{t(`chips.${key}`)}
</li>
))}
</ul>
</div>
<Reveal>
<div className={styles.pipeline} role="img" aria-label={t('headline')}>
<div className={styles.node}>
<span className={styles.nodeTitle}>{t('diagram.input')}</span>
</div>
<Arrow />
<div className={styles.node}>
<span className={styles.nodeTitle}>{t('diagram.frames')}</span>
</div>
<Arrow />
<div className={styles.node}>
<span className={styles.nodeTitle}>{t('diagram.search')}</span>
<span className={styles.nodeSub}>{t('diagram.search_sub')}</span>
</div>
<Arrow />
<div className={styles.node}>
<span className={styles.nodeTitle}>{t('diagram.graph')}</span>
<span className={styles.nodeSub}>{t('diagram.graph_sub')}</span>
</div>
<Arrow />
<div className={`${styles.node} ${styles.nodeAccent}`}>
<span className={styles.nodeTitle}>{t('diagram.output')}</span>
</div>
</div>
</Reveal>
</div>
</div>
</section>
);
}
function Arrow() {
return (
<span className={styles.connector} aria-hidden="true">
<svg width="12" height="14" viewBox="0 0 12 14" fill="none">
<path
d="M6 0 V11 M2 8 L6 12 L10 8"
stroke="currentColor"
strokeWidth="1.4"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
);
}

View File

@@ -0,0 +1,145 @@
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
transition: background 0.25s ease, border-color 0.25s ease,
backdrop-filter 0.25s ease;
border-bottom: 1px solid transparent;
background: transparent;
}
.headerScrolled {
background: rgba(14, 12, 7, 0.82);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
border-bottom-color: var(--line-soft);
}
.inner {
max-width: var(--container-wide);
margin: 0 auto;
padding: 0 var(--gutter);
height: 64px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
}
.brand {
display: inline-flex;
align-items: center;
text-decoration: none;
flex-shrink: 0;
}
.nav {
display: flex;
align-items: center;
gap: 4px;
}
.navLink {
padding: 8px 14px;
border-radius: var(--r-sm);
font-size: var(--text-small);
font-weight: 500;
color: var(--hive-300);
text-decoration: none;
transition: color 0.15s ease, background 0.15s ease;
}
.navLink:hover {
color: var(--hive-50);
background: rgba(236, 227, 208, 0.05);
}
.actions {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.signIn {
padding: 8px 14px;
border-radius: var(--r-sm);
font-size: var(--text-small);
font-weight: 500;
color: var(--hive-200);
text-decoration: none;
transition: color 0.15s ease;
background: none;
border: none;
cursor: pointer;
font-family: var(--sans);
}
.signIn:hover {
color: var(--hive-50);
}
.menuButton {
display: none;
background: none;
border: 1px solid var(--line-strong);
border-radius: var(--r-sm);
color: var(--hive-100);
width: 40px;
height: 40px;
align-items: center;
justify-content: center;
cursor: pointer;
}
.mobilePanel {
display: none;
}
@media (max-width: 860px) {
.nav {
display: none;
}
.signIn {
display: none;
}
.menuButton {
display: inline-flex;
}
.mobilePanel {
display: flex;
flex-direction: column;
gap: 4px;
padding: 12px var(--gutter) 20px;
background: rgba(14, 12, 7, 0.96);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
border-bottom: 1px solid var(--line-soft);
}
.mobileLink {
padding: 12px 14px;
border-radius: var(--r-sm);
font-size: 15px;
font-weight: 500;
color: var(--hive-200);
text-decoration: none;
}
.mobileLink:hover {
color: var(--hive-50);
background: rgba(236, 227, 208, 0.05);
}
.mobileActions {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px 0;
}
}

View File

@@ -0,0 +1,153 @@
'use client';
import { useEffect, useState } from 'react';
import { useTranslations } from 'next-intl';
import { SignInButton, UserButton, Show } from '@clerk/nextjs';
import BrandMark from './BrandMark';
import DownloadCTA from './DownloadCTA';
import styles from './Navbar.module.css';
/* Absolute-path anchors so the navbar also works from /privacy, /terms,
and the other legal pages that render this chrome. */
const NAV_ITEMS = [
{ href: '/#how-it-works', key: 'how_it_works' },
{ href: '/#memory', key: 'memory' },
{ href: '/#proof', key: 'benchmark' },
{ href: '/#open-source', key: 'open_source' },
{ href: '/#pricing', key: 'pricing' },
] as const;
/**
* Fixed top navigation. Transparent over the hero, gains a blurred backdrop
* + hairline border after a small scroll. Collapses to a menu button below
* 860px; the mobile panel reuses the same anchor list.
*
* Stays a Client Component for scroll-aware backdrop + menu state. All
* strings under `landing.navbar.*`.
*/
export default function Navbar() {
const t = useTranslations('landing.navbar');
const [scrolled, setScrolled] = useState(false);
const [open, setOpen] = useState(false);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 8);
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);
const headerClass = [
styles.header,
scrolled || open ? styles.headerScrolled : '',
]
.filter(Boolean)
.join(' ');
return (
<header className={headerClass}>
<div className={styles.inner}>
<a href="/" className={styles.brand} aria-label={t('aria.home')}>
<BrandMark withWordmark />
</a>
<nav className={styles.nav} aria-label={t('aria.primary')}>
{NAV_ITEMS.map((item) => (
<a key={item.key} href={item.href} className={styles.navLink}>
{t(`links.${item.key}`)}
</a>
))}
</nav>
<div className={styles.actions}>
<Show when="signed-out">
<SignInButton mode="modal">
<button type="button" className={styles.signIn}>
{t('ctas.sign_in')}
</button>
</SignInButton>
</Show>
<Show when="signed-in">
<UserButton />
</Show>
<DownloadCTA section="navbar" size="small">
{t('ctas.download')}
</DownloadCTA>
<button
type="button"
className={styles.menuButton}
aria-expanded={open}
aria-controls="mobile-nav"
aria-label={t('aria.toggle_menu')}
onClick={() => setOpen((v) => !v)}
>
<MenuIcon open={open} />
</button>
</div>
</div>
{open ? (
<nav
id="mobile-nav"
className={styles.mobilePanel}
aria-label={t('aria.primary')}
>
{NAV_ITEMS.map((item) => (
<a
key={item.key}
href={item.href}
className={styles.mobileLink}
onClick={() => setOpen(false)}
>
{t(`links.${item.key}`)}
</a>
))}
<div className={styles.mobileActions}>
<Show when="signed-out">
<SignInButton mode="modal">
<button
type="button"
className={styles.signIn}
style={{ display: 'inline-flex' }}
>
{t('ctas.sign_in')}
</button>
</SignInButton>
</Show>
<Show when="signed-in">
<UserButton />
</Show>
</div>
</nav>
) : null}
</header>
);
}
function MenuIcon({ open }: { readonly open: boolean }) {
return (
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
aria-hidden="true"
>
{open ? (
<path
d="M4 4 L14 14 M14 4 L4 14"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
/>
) : (
<path
d="M2 5 H16 M2 9 H16 M2 13 H16"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
/>
)}
</svg>
);
}

View File

@@ -0,0 +1,87 @@
.grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 72px;
align-items: center;
}
.body {
font-size: var(--text-body);
line-height: 1.7;
color: var(--hive-300);
margin-top: 20px;
max-width: 34em;
}
.ctaRow {
display: flex;
flex-wrap: wrap;
gap: 14px;
margin-top: 32px;
}
.terminal {
border-radius: var(--r-lg);
border: 1px solid var(--line);
background: var(--hive-950);
box-shadow: var(--shadow-elevated);
overflow: hidden;
}
.terminalBar {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 14px;
border-bottom: 1px solid var(--line-soft);
background: rgba(31, 26, 18, 0.6);
}
.terminalDots {
display: inline-flex;
gap: 6px;
}
.terminalDot {
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--hive-600);
opacity: 0.7;
}
.terminalTitle {
font-family: var(--mono);
font-size: 11px;
color: var(--text-muted);
}
.terminalBody {
margin: 0;
padding: 22px 22px 26px;
font-family: var(--mono);
font-size: 12.5px;
line-height: 1.9;
color: var(--hive-200);
overflow-x: auto;
}
.prompt {
color: var(--honey-500);
user-select: none;
}
.comment {
color: var(--text-muted);
}
.output {
color: var(--healthy);
}
@media (max-width: 1023px) {
.grid {
grid-template-columns: minmax(0, 1fr);
gap: 48px;
}
}

View File

@@ -0,0 +1,88 @@
import { getTranslations } from 'next-intl/server';
import Reveal from './Reveal';
import styles from './OpenSource.module.css';
const HIVE_MIND_URL = 'https://github.com/marolinik/hive-mind';
const NPM_URL = 'https://www.npmjs.com/package/@hive-mind/core';
/**
* Open-source section: hive-mind (the memory substrate) with a terminal
* showing the offline benchmark reproduction path. Command + output are
* verbatim from the OSS repo (benchmarks/locomo/artifacts/w4-n1540/
* recount.mjs — verified against the local clone 2026-07-03); if that
* script moves, update this terminal. Strings under `landing.open_source.*`.
*/
export default async function OpenSource() {
const t = await getTranslations('landing.open_source');
return (
<section
id="open-source"
className="section"
aria-labelledby="oss-heading"
>
<div className="container">
<div className={styles.grid}>
<div>
<p className="eyebrow">{t('eyebrow')}</p>
<h2 id="oss-heading" className="section-headline">
{t('headline')}
</h2>
<p className={styles.body}>{t('body')}</p>
<div className={styles.ctaRow}>
<a
href={HIVE_MIND_URL}
target="_blank"
rel="noopener noreferrer"
className="btn btn-primary"
>
{t('cta_github')}
</a>
<a
href={NPM_URL}
target="_blank"
rel="noopener noreferrer"
className="btn btn-ghost"
>
{t('cta_npm')}
</a>
</div>
</div>
<Reveal>
<div className={styles.terminal} aria-label={t('terminal_aria')}>
<div className={styles.terminalBar}>
<span className={styles.terminalDots} aria-hidden="true">
<span className={styles.terminalDot} />
<span className={styles.terminalDot} />
<span className={styles.terminalDot} />
</span>
<span className={styles.terminalTitle}>
{t('terminal_title')}
</span>
</div>
<pre className={styles.terminalBody}>
<code>
<span className={styles.prompt}>$ </span>
git clone https://github.com/marolinik/hive-mind{'\n'}
<span className={styles.prompt}>$ </span>
cd hive-mind/benchmarks/locomo{'\n'}
<span className={styles.comment}>
# recount the committed judgments no API keys, no network
</span>
{'\n'}
<span className={styles.prompt}>$ </span>
node artifacts/w4-n1540/recount.mjs{'\n'}
<span className={styles.output}>
overall 1332/1540 = 86.49%{'\n'}
RECOUNT OK committed judgments reproduce 86.49%.
</span>
</code>
</pre>
</div>
</Reveal>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,190 @@
.header {
text-align: center;
max-width: 640px;
margin: 0 auto 40px;
display: flex;
flex-direction: column;
align-items: center;
}
.toggleRow {
display: flex;
justify-content: center;
gap: 4px;
padding: 4px;
background: var(--hive-900);
border: 1px solid var(--line-soft);
border-radius: 999px;
width: fit-content;
margin: 0 auto 56px;
}
.toggle {
font-size: 13px;
font-weight: 500;
padding: 8px 18px;
border-radius: 999px;
border: none;
cursor: pointer;
font-family: var(--sans);
background: transparent;
color: var(--hive-300);
transition: background 0.15s ease, color 0.15s ease;
}
.toggleActive {
background: var(--surface-2);
color: var(--hive-50);
}
.savePill {
color: var(--honey-400);
font-weight: 600;
margin-left: 6px;
}
.checkoutNotice {
display: flex;
align-items: center;
justify-content: center;
gap: 14px;
flex-wrap: wrap;
max-width: 680px;
margin: -28px auto 36px;
padding: 12px 16px;
border: 1px solid var(--honey-line);
border-radius: var(--r-md);
background: color-mix(in srgb, var(--honey-500) 10%, transparent);
color: var(--hive-100);
font-size: 13px;
line-height: 1.5;
text-align: center;
}
.noticeLink {
color: var(--honey-300);
font-weight: 700;
text-decoration: none;
}
.noticeLink:hover {
text-decoration: underline;
}
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
max-width: 1080px;
margin: 0 auto 40px;
align-items: stretch;
}
.tierCard {
position: relative;
border-radius: var(--r-lg);
padding: 30px 28px 28px;
background: var(--hive-900);
border: 1px solid var(--line-soft);
display: flex;
flex-direction: column;
}
.tierCardHighlighted {
background: var(--hive-850);
border-color: var(--honey-line);
box-shadow: var(--shadow-honey);
}
/* Enterprise/KVARK slot: present but visually subordinate to Solo/Team. */
.tierCardQuiet {
background: transparent;
}
.enterpriseBody {
font-size: 13px;
line-height: 1.6;
color: var(--hive-300);
margin: 0 0 26px;
flex-grow: 1;
}
.badge {
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 5px 12px;
border-radius: 999px;
background: var(--honey-500);
color: var(--hive-950);
white-space: nowrap;
}
.tierName {
font-size: 19px;
font-weight: 700;
margin-bottom: 6px;
color: var(--hive-50);
}
.tierTagline {
font-size: 13px;
color: var(--hive-300);
margin-bottom: 20px;
line-height: 1.5;
}
.price {
font-size: 26px;
font-weight: 700;
letter-spacing: -0.02em;
color: var(--hive-50);
}
.priceNote {
font-size: 12px;
font-weight: 400;
color: var(--text-muted);
margin-top: 4px;
margin-bottom: 22px;
min-height: 1em;
}
.bullets {
list-style: none;
padding: 0;
margin: 0 0 26px;
flex-grow: 1;
}
.bullet {
display: flex;
align-items: flex-start;
gap: 10px;
font-size: 13px;
color: var(--hive-200);
margin-bottom: 11px;
line-height: 1.5;
}
.bulletIcon {
flex-shrink: 0;
margin-top: 3px;
color: var(--honey-500);
}
.tierCta {
width: 100%;
}
@media (max-width: 1023px) {
.grid {
grid-template-columns: 1fr;
max-width: 460px;
}
}

View File

@@ -0,0 +1,253 @@
'use client';
import { useCallback, useEffect, useState } from 'react';
import { useTranslations } from 'next-intl';
import DownloadCTA from './DownloadCTA';
import { emit, events } from '../_lib/event-taxonomy';
import styles from './Pricing.module.css';
type BillingPeriod = 'monthly' | 'annual';
type TierId = 'SOLO' | 'TEAMS';
interface TierDef {
readonly id: TierId;
readonly nsKey: 'solo' | 'teams';
readonly highlighted: boolean;
readonly bulletKeys: readonly string[];
readonly ctaType: 'download' | 'stripe';
}
/**
* Tier content mirrors `packages/shared/src/tiers.ts` (the canonical tier
* system): SOLO is free forever with full memory + Harvest, unlimited
* workspaces, marketplace, all connectors, and BYO cloud models; TEAMS adds
* shared workspaces, WaggleDance, and governance. No bullets beyond what
* tiers.ts encodes. The 15-day Team trial lives in the section subhead —
* it applies to every install, so listing it as a Solo feature misreads.
*/
const TIER_DEFS: readonly TierDef[] = [
{
id: 'SOLO',
nsKey: 'solo',
highlighted: false,
bulletKeys: [
'bullet_memory',
'bullet_workspaces',
'bullet_marketplace',
'bullet_models',
'bullet_skills',
],
ctaType: 'download',
},
{
id: 'TEAMS',
nsKey: 'teams',
highlighted: true,
bulletKeys: [
'bullet_everything_solo',
'bullet_shared',
'bullet_dance',
'bullet_governance',
],
ctaType: 'stripe',
},
];
const STRIPE_ENDPOINT =
(process.env.NEXT_PUBLIC_API_URL ?? '').replace(/\/$/, '') +
'/api/stripe/checkout';
const KVARK_URL = 'https://www.kvark.ai';
export default function Pricing() {
const t = useTranslations('landing.pricing');
const [billing, setBilling] = useState<BillingPeriod>('monthly');
const [checkoutCancelled, setCheckoutCancelled] = useState(false);
useEffect(() => {
const params = new URLSearchParams(window.location.search);
setCheckoutCancelled(params.get('checkout') === 'cancelled');
}, []);
const handleBillingChange = useCallback((mode: BillingPeriod) => {
setBilling(mode);
emit({ name: events.pricingBillingToggle, properties: { mode } });
}, []);
const handleStripeCtaClick = useCallback(
(tier: TierId) => {
emit({
name: events.ctaClick,
properties: { section: 'pricing', tier, billing },
});
},
[billing],
);
return (
<section id="pricing" className="section" aria-labelledby="pricing-heading">
<div className="container-wide">
<header className={styles.header}>
<p className="eyebrow">{t('eyebrow')}</p>
<h2 id="pricing-heading" className="section-headline">
{t('headline')}
</h2>
<p className="section-lead">{t('subhead')}</p>
</header>
<div
role="group"
aria-label={t('toggle.aria_group')}
className={styles.toggleRow}
>
<button
type="button"
onClick={() => handleBillingChange('monthly')}
className={[
styles.toggle,
billing === 'monthly' ? styles.toggleActive : '',
]
.filter(Boolean)
.join(' ')}
aria-pressed={billing === 'monthly'}
>
{t('toggle.monthly')}
</button>
<button
type="button"
onClick={() => handleBillingChange('annual')}
className={[
styles.toggle,
billing === 'annual' ? styles.toggleActive : '',
]
.filter(Boolean)
.join(' ')}
aria-pressed={billing === 'annual'}
>
{t('toggle.annual')}
<span className={styles.savePill}>{t('toggle.save_pill')}</span>
</button>
</div>
{checkoutCancelled ? (
<div role="status" className={styles.checkoutNotice}>
<span>{t('notices.cancelled')}</span>
<a
href={`${STRIPE_ENDPOINT}?tier=teams&billing=${billing}`}
onClick={() => handleStripeCtaClick('TEAMS')}
className={styles.noticeLink}
>
{t('notices.retry')}
</a>
</div>
) : null}
<div className={styles.grid}>
{TIER_DEFS.map((tier) => {
const priceKey =
billing === 'monthly' ? 'price_monthly' : 'price_annual';
const cardClass = [
styles.tierCard,
tier.highlighted ? styles.tierCardHighlighted : '',
]
.filter(Boolean)
.join(' ');
return (
<div key={tier.id} className={cardClass}>
{tier.highlighted ? (
<span className={styles.badge}>{t('popular_badge')}</span>
) : null}
<h3 className={styles.tierName}>
{t(`tiers.${tier.nsKey}.name`)}
</h3>
<p className={styles.tierTagline}>
{t(`tiers.${tier.nsKey}.tagline`)}
</p>
<p className={styles.price}>
{t(`tiers.${tier.nsKey}.${priceKey}`)}
</p>
<p className={styles.priceNote}>
{t(`tiers.${tier.nsKey}.note`)}
</p>
<ul className={styles.bullets}>
{tier.bulletKeys.map((bk) => (
<li key={bk} className={styles.bullet}>
<CheckIcon />
<span>{t(`tiers.${tier.nsKey}.${bk}`)}</span>
</li>
))}
</ul>
{tier.ctaType === 'download' ? (
<DownloadCTA
section="solo-tier"
variant={tier.highlighted ? 'primary' : 'ghost'}
style={{ width: '100%' }}
/>
) : (
<a
href={`${STRIPE_ENDPOINT}?tier=${tier.id.toLowerCase()}&billing=${billing}`}
onClick={() => handleStripeCtaClick(tier.id)}
className={[
'btn',
tier.highlighted ? 'btn-primary' : 'btn-ghost',
styles.tierCta,
].join(' ')}
>
{t(`tiers.${tier.nsKey}.cta`)}
</a>
)}
</div>
);
})}
{/* Enterprise = KVARK sovereign deployment. A quieter third card
(description, no checklist) so the grid fills its row without
inventing a tier — pricing stays consultative. */}
<div className={[styles.tierCard, styles.tierCardQuiet].join(' ')}>
<h3 className={styles.tierName}>{t('enterprise.name')}</h3>
<p className={styles.tierTagline}>{t('enterprise.tagline')}</p>
<p className={styles.price}>{t('enterprise.price')}</p>
<p className={styles.priceNote}>{t('enterprise.note')}</p>
<p className={styles.enterpriseBody}>{t('enterprise.text')}</p>
<a
href={KVARK_URL}
target="_blank"
rel="noopener noreferrer"
className={['btn', 'btn-ghost', styles.tierCta].join(' ')}
>
{t('enterprise.cta')}
</a>
</div>
</div>
</div>
</section>
);
}
function CheckIcon() {
return (
<svg
width="15"
height="15"
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
className={styles.bulletIcon}
>
<path
d="M3 8.5 L6.5 12 L13 4.5"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}

View File

@@ -0,0 +1,58 @@
.header {
max-width: 720px;
margin-bottom: 56px;
}
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-bottom: 56px;
}
.beat {
padding: 26px 26px 28px;
border-radius: var(--r-lg);
background: var(--hive-900);
border: 1px solid var(--line-soft);
}
.beatIndex {
font-family: var(--mono);
font-size: 11px;
color: var(--text-muted);
display: block;
margin-bottom: 14px;
}
.beatTitle {
font-size: var(--text-h3);
font-weight: 600;
color: var(--hive-50);
margin-bottom: 10px;
}
.beatBody {
font-size: var(--text-small);
line-height: 1.6;
color: var(--hive-300);
}
.turn {
border-left: 2px solid var(--honey-500);
padding-left: 24px;
max-width: 640px;
}
.turnText {
font-size: clamp(1.125rem, 2vw, 1.375rem);
line-height: 1.55;
font-weight: 500;
color: var(--hive-100);
}
@media (max-width: 860px) {
.grid {
grid-template-columns: 1fr;
}
}

View File

@@ -0,0 +1,51 @@
import { getTranslations } from 'next-intl/server';
import Reveal from './Reveal';
import styles from './ProblemTurn.module.css';
const BEATS = ['reintroduce', 'tabs', 'compound'] as const;
/**
* The problem statement ("every AI session starts from zero") in three
* beats, then the turn — Waggle's opposite bet — as a pull-quote with a
* honey rule. Strings under `landing.problem.*`.
*/
export default async function ProblemTurn() {
const t = await getTranslations('landing.problem');
return (
<section
id="problem"
className="section"
aria-labelledby="problem-heading"
>
<div className="container">
<header className={styles.header}>
<p className="eyebrow">{t('eyebrow')}</p>
<h2 id="problem-heading" className="section-headline">
{t('headline')}
</h2>
</header>
<div className={styles.grid}>
{BEATS.map((beat, i) => (
<Reveal key={beat} delay={(i + 1) as 1 | 2 | 3}>
<div className={styles.beat}>
<span className={styles.beatIndex} aria-hidden="true">
{String(i + 1).padStart(2, '0')}
</span>
<h3 className={styles.beatTitle}>{t(`beats.${beat}.title`)}</h3>
<p className={styles.beatBody}>{t(`beats.${beat}.body`)}</p>
</div>
</Reveal>
))}
</div>
<Reveal>
<div className={styles.turn}>
<p className={styles.turnText}>{t('turn')}</p>
</div>
</Reveal>
</div>
</section>
);
}

View File

@@ -0,0 +1,111 @@
.band {
background: var(--hive-900);
border-top: 1px solid var(--line-soft);
border-bottom: 1px solid var(--line-soft);
}
.header {
max-width: 760px;
margin-bottom: 56px;
}
.chart {
display: flex;
flex-direction: column;
gap: 22px;
max-width: 820px;
margin-bottom: 28px;
}
.row {
display: grid;
grid-template-columns: 200px 1fr 72px;
align-items: center;
gap: 18px;
}
.system {
display: flex;
flex-direction: column;
gap: 2px;
}
.systemName {
font-size: 14px;
font-weight: 600;
color: var(--hive-100);
}
.systemDetail {
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.03em;
color: var(--text-muted);
}
.track {
display: block;
height: 30px;
border-radius: 7px;
background: var(--hive-800);
overflow: hidden;
}
.bar {
display: block;
height: 100%;
border-radius: 7px;
background: var(--hive-600);
transform-origin: left center;
}
.barHighlight {
background: linear-gradient(90deg, var(--honey-600) 0%, var(--honey-500) 100%);
box-shadow: var(--shadow-honey);
}
.score {
font-family: var(--mono);
font-size: 14px;
font-weight: 600;
color: var(--hive-100);
text-align: right;
}
.scoreHighlight {
color: var(--honey-400);
}
.footnote {
font-family: var(--mono);
font-size: 11px;
line-height: 1.7;
color: var(--text-muted);
max-width: 720px;
margin-bottom: 36px;
}
.ctaRow {
display: flex;
flex-wrap: wrap;
gap: 14px;
}
@media (max-width: 640px) {
.row {
grid-template-columns: 1fr 56px;
grid-template-rows: auto auto;
row-gap: 8px;
}
.system {
grid-column: 1 / -1;
flex-direction: row;
align-items: baseline;
gap: 10px;
}
.track {
height: 22px;
}
}

View File

@@ -0,0 +1,84 @@
import { getTranslations } from 'next-intl/server';
import Reveal from './Reveal';
import { LOCOMO_BARS } from '../_data/proof-points';
import styles from './ProofBand.module.css';
const HIVE_MIND_URL = 'https://github.com/marolinik/hive-mind';
/**
* Benchmark proof band. The chart is honest by construction: bar widths are
* raw scores on a 0100 axis (no truncated baseline), the protocol footnote
* names the judge, N, and significance, and both CTAs lead to verification
* paths (methodology page, reproducible repo). Data from `_data/proof-points`.
*/
export default async function ProofBand() {
const t = await getTranslations('landing.proof');
return (
<section
id="proof"
className={`section ${styles.band}`}
aria-labelledby="proof-heading"
>
<div className="container">
<header className={styles.header}>
<p className="eyebrow">{t('eyebrow')}</p>
<h2 id="proof-heading" className="section-headline">
{t('headline')}
</h2>
<p className="section-lead">{t('body')}</p>
</header>
<Reveal>
<div className={styles.chart} role="img" aria-label={t('chart_aria')}>
{LOCOMO_BARS.map((bar) => (
<div key={bar.id} className={styles.row}>
<span className={styles.system}>
<span className={styles.systemName}>{bar.system}</span>
<span className={styles.systemDetail}>{bar.detail}</span>
</span>
<span className={styles.track}>
<span
className={[
styles.bar,
bar.highlight ? styles.barHighlight : '',
]
.filter(Boolean)
.join(' ')}
style={{ width: `${bar.score}%` }}
/>
</span>
<span
className={[
styles.score,
bar.highlight ? styles.scoreHighlight : '',
]
.filter(Boolean)
.join(' ')}
>
{bar.score.toFixed(2)}%
</span>
</div>
))}
</div>
</Reveal>
<p className={styles.footnote}>{t('footnote')}</p>
<div className={styles.ctaRow}>
<a href="/docs/methodology" className="btn btn-ghost">
{t('cta_methodology')}
</a>
<a
href={HIVE_MIND_URL}
target="_blank"
rel="noopener noreferrer"
className="btn btn-ghost"
>
{t('cta_reproduce')}
</a>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,86 @@
'use client';
import {
useCallback,
useEffect,
useRef,
useState,
type CSSProperties,
type ReactNode,
} from 'react';
interface RevealProps {
readonly children: ReactNode;
/** Stagger slot 15 → transition-delay 60ms steps (see globals.css). */
readonly delay?: 1 | 2 | 3 | 4 | 5;
readonly as?: 'div' | 'section' | 'li' | 'span';
readonly className?: string;
readonly style?: CSSProperties;
}
/**
* Scroll-reveal wrapper: fades + lifts children in when they enter the
* viewport. Purely presentational — content is in the DOM at SSR (SEO-safe)
* and `prefers-reduced-motion` disables the effect entirely via globals.css.
*/
export default function Reveal({
children,
delay,
as: Tag = 'div',
className,
style,
}: RevealProps) {
const nodeRef = useRef<HTMLElement | null>(null);
const [visible, setVisible] = useState(false);
const setNode = useCallback((node: HTMLElement | null) => {
nodeRef.current = node;
}, []);
useEffect(() => {
const node = nodeRef.current;
if (!node) return;
if (typeof IntersectionObserver === 'undefined') {
setVisible(true);
return;
}
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
setVisible(true);
observer.disconnect();
}
}
},
{ rootMargin: '0px 0px -10% 0px', threshold: 0.1 },
);
observer.observe(node);
// Safety net: if a section is never scrolled into view — a crawler, a
// social-preview renderer, or a full-page screenshot that paints without
// scrolling — the observer never fires and the content would stay stuck at
// opacity:0. Reveal it anyway shortly after mount so no section is ever a
// headline floating in an empty void. Real users scrolling normally still
// trip the observer first and get the entrance animation per section.
const fallback = window.setTimeout(() => setVisible(true), 900);
return () => {
observer.disconnect();
window.clearTimeout(fallback);
};
}, []);
const classes = [
'reveal',
visible ? 'is-visible' : '',
delay ? `reveal-d${delay}` : '',
className ?? '',
]
.filter(Boolean)
.join(' ');
return (
<Tag ref={setNode} className={classes} style={style}>
{children}
</Tag>
);
}

View File

@@ -0,0 +1,47 @@
.band {
background: var(--hive-900);
border-top: 1px solid var(--line-soft);
border-bottom: 1px solid var(--line-soft);
}
.header {
max-width: 720px;
margin-bottom: 56px;
}
.grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 32px;
margin-bottom: 48px;
}
.item {
border-top: 2px solid var(--honey-600);
padding-top: 20px;
}
.title {
font-size: 15px;
font-weight: 600;
color: var(--hive-50);
margin-bottom: 8px;
}
.body {
font-size: 13px;
line-height: 1.6;
color: var(--hive-300);
}
@media (max-width: 1023px) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 560px) {
.grid {
grid-template-columns: 1fr;
}
}

View File

@@ -0,0 +1,46 @@
import { getTranslations } from 'next-intl/server';
import Reveal from './Reveal';
import styles from './SovereigntyBand.module.css';
const ITEMS = ['device', 'egress', 'erasure', 'injection'] as const;
/**
* Data-sovereignty band: four specific, verifiable statements about where
* data lives and what leaves the machine. Trust through specificity — no
* compliance badges we don't hold. Strings under `landing.sovereignty.*`.
*/
export default async function SovereigntyBand() {
const t = await getTranslations('landing.sovereignty');
return (
<section
id="trust"
className={`section ${styles.band}`}
aria-labelledby="trust-heading"
>
<div className="container">
<header className={styles.header}>
<p className="eyebrow">{t('eyebrow')}</p>
<h2 id="trust-heading" className="section-headline">
{t('headline')}
</h2>
</header>
<div className={styles.grid}>
{ITEMS.map((item, i) => (
<Reveal key={item} delay={((i % 4) + 1) as 1 | 2 | 3 | 4}>
<div className={styles.item}>
<h3 className={styles.title}>{t(`items.${item}.title`)}</h3>
<p className={styles.body}>{t(`items.${item}.body`)}</p>
</div>
</Reveal>
))}
</div>
<a href="/eu-ai-act" className="btn btn-ghost btn-small">
{t('cta')}
</a>
</div>
</section>
);
}