feat(web): Submatter marketing redesign with Signal Editorial system

Introduce public /landing and /pricing pages for the SBMTR brand with
bento features, convoy product demo, motion reveals, and scoped marketing
tokens. Unauthenticated visitors route to the landing page.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Randall Stillwell 2026-07-10 22:50:05 -05:00
parent b513b3b04f
commit 2f6adf4382
22 changed files with 1215 additions and 22 deletions

View file

@ -0,0 +1,29 @@
import type { Metadata } from "next";
import { ComparisonSection } from "@/components/marketing/comparison-section";
import { CtaSection } from "@/components/marketing/cta-section";
import { FeaturesSection } from "@/components/marketing/features-section";
import { HeroSection } from "@/components/marketing/hero-section";
import { HowItWorksSection } from "@/components/marketing/how-it-works-section";
import { SiteFooter } from "@/components/marketing/site-footer";
import { SiteHeader } from "@/components/marketing/site-header";
export const metadata: Metadata = {
title: "Subagents. Frontmatter. One workspace.",
};
export default function LandingPage() {
return (
<>
<SiteHeader />
<main>
<HeroSection />
<FeaturesSection />
<HowItWorksSection />
<ComparisonSection />
<CtaSection />
</main>
<SiteFooter />
</>
);
}

View file

@ -0,0 +1,125 @@
import type { Metadata } from "next";
import Link from "next/link";
import { Check } from "lucide-react";
import { BorderGlowCard } from "@/components/marketing/border-glow-card";
import { Reveal, Stagger, StaggerItem } from "@/components/marketing/reveal";
import { SiteFooter } from "@/components/marketing/site-footer";
import { SiteHeader } from "@/components/marketing/site-header";
import { cn } from "@/lib/utils";
export const metadata: Metadata = {
title: "Pricing",
};
const tiers = [
{
name: "Solo",
price: "Free",
period: "during beta",
description: "Personal workspace for testing agent workflows and markdown sync.",
features: ["1 workspace", "Markdown backlog sync", "MCP claim / complete", "Docs & planner"],
cta: "Start free",
href: "/sign-up",
highlighted: false,
},
{
name: "Team",
price: "$12",
period: "per seat / mo",
description: "Shared convoys, run history, and model routing for small builder teams.",
features: [
"Unlimited workspaces",
"Agent run history",
"Role-based routing",
"Audit log",
"Priority sync",
],
cta: "Start trial",
href: "/sign-up",
highlighted: true,
},
{
name: "Fleet",
price: "Custom",
period: "contact us",
description: "Mission control for multiple repos, tenants, and orchestrated pipelines.",
features: ["Connected repos", "Fleet dashboard", "Webhook gates", "SSO", "Self-hosted option"],
cta: "Contact",
href: "mailto:hello@submatter.dev",
highlighted: false,
},
] as const;
export default function PricingPage() {
return (
<>
<SiteHeader />
<main className="mx-auto max-w-6xl px-4 py-16 sm:px-6 sm:py-24">
<Reveal className="mx-auto max-w-2xl text-center">
<p className="font-mono-brand text-[11px] uppercase tracking-[0.2em] text-muted-foreground">Pricing</p>
<h1 className="mt-4 font-marketing text-4xl font-semibold tracking-[-0.02em] text-foreground sm:text-5xl">
Predictable seats.
<br />
No per-token surprise.
</h1>
<p className="mt-4 font-marketing text-muted-foreground">
Beta pricing for feedback. BYOK for model providers always.
</p>
</Reveal>
<Stagger className="mt-16 grid gap-4 lg:grid-cols-3">
{tiers.map((tier) => (
<StaggerItem key={tier.name}>
<BorderGlowCard
as="article"
className={cn(
"flex h-full flex-col p-6",
tier.highlighted && "ring-1 ring-matter/30",
)}
>
{tier.highlighted ? (
<span className="mb-4 inline-flex w-fit font-mono-brand text-[10px] uppercase tracking-widest text-matter">
popular
</span>
) : (
<span className="mb-4 h-4" aria-hidden />
)}
<h2 className="font-marketing text-lg font-semibold text-foreground">{tier.name}</h2>
<p className="mt-2 flex items-baseline gap-2">
<span className="font-marketing text-3xl font-semibold text-foreground">{tier.price}</span>
<span className="font-marketing text-sm text-muted-foreground">{tier.period}</span>
</p>
<p className="mt-3 font-marketing text-sm text-muted-foreground">{tier.description}</p>
<ul className="mt-6 flex-1 space-y-2.5">
{tier.features.map((feature) => (
<li key={feature} className="flex gap-2 font-marketing text-sm text-foreground">
<Check className="mt-0.5 size-4 shrink-0 text-agent" aria-hidden />
{feature}
</li>
))}
</ul>
<Link
href={tier.href}
className={cn(
"mt-8 inline-flex h-10 items-center justify-center font-marketing text-sm font-medium transition-colors",
tier.highlighted
? "bg-foreground text-background hover:bg-foreground/90"
: "border border-border hover:bg-muted",
)}
>
{tier.cta}
</Link>
</BorderGlowCard>
</StaggerItem>
))}
</Stagger>
<p className="mt-14 text-center font-mono-brand text-[11px] text-muted-foreground">
kind: pricing · status: draft · priority: P2
</p>
</main>
<SiteFooter />
</>
);
}

View file

@ -0,0 +1,47 @@
import type { Metadata } from "next";
import type { ReactNode } from "react";
import { Barlow_Condensed, IBM_Plex_Sans, JetBrains_Mono } from "next/font/google";
import "@/styles/marketing.css";
const barlowCondensed = Barlow_Condensed({
subsets: ["latin"],
weight: ["700", "800"],
variable: "--font-display",
});
const ibmPlexSans = IBM_Plex_Sans({
subsets: ["latin"],
weight: ["400", "500", "600"],
variable: "--font-marketing-sans",
});
const jetbrainsMono = JetBrains_Mono({
subsets: ["latin"],
weight: ["400", "500"],
variable: "--font-mono-brand",
});
export const metadata: Metadata = {
title: {
default: "Submatter — Subagents. Frontmatter. One workspace.",
template: "%s · Submatter",
},
description:
"Agent-native workspace with markdown backlog sync, MCP task lifecycle, and convoy pipelines for Cursor subagents.",
openGraph: {
title: "SBMTR — Submatter",
description: "Subagents. Frontmatter. One workspace.",
siteName: "Submatter",
},
};
export default function MarketingLayout({ children }: { children: ReactNode }) {
return (
<div
className={`marketing-theme min-h-screen bg-background font-marketing text-foreground antialiased ${barlowCondensed.variable} ${ibmPlexSans.variable} ${jetbrainsMono.variable}`}
>
{children}
</div>
);
}

View file

@ -6,27 +6,14 @@ import { db } from "@tasks/database/client";
import { workspaceMembers, workspaces } from "@tasks/database/schema"; import { workspaceMembers, workspaces } from "@tasks/database/schema";
/** /**
* Root landing route. Decides where an authenticated user goes when they * Root landing route. Unauthenticated visitors go to the public marketing site;
* hit `/`. The post-sign-in `callbackUrl` defaults here, so this is the * signed-in users land on their oldest workspace home (`/{slug}/`).
* single source of truth for "where does a user actually land?"
*
* Decision: land on the user's oldest workspace home (`/{slug}/`). Why oldest:
* we don't have a "last visited" column yet (deliberately deferred — see
* Task-pick-workspace-landing-route) and oldest-membership is stable across
* page loads. The workspace home is no longer a mockup; it now shows real
* stats and recent activity via `objects.stats` / `objects.listRecent`.
*
* The auth callback (`ensureUserHasWorkspace` in `apps/web/lib/auth.ts`)
* provisions a personal workspace on first sign-in, so the zero-workspace
* branch below should be effectively unreachable for fresh sign-ins. It
* survives only as a defensive fallback for sessions that were minted
* before that provisioning logic existed.
*/ */
export default async function HomePage() { export default async function HomePage() {
const session = await auth(); const session = await auth();
if (!session?.user?.id) { if (!session?.user?.id) {
redirect("/sign-in"); redirect("/landing");
} }
const [membership] = await db const [membership] = await db
@ -41,11 +28,5 @@ export default async function HomePage() {
redirect(`/${membership.slug}`); redirect(`/${membership.slug}`);
} }
// Zero-workspace fallback. See header doc — this should be unreachable
// post-`ensureUserHasWorkspace`. If we hit it anyway, bounce through
// sign-in so the auth callback re-runs and provisions a workspace.
// Track follow-up: a proper onboarding flow for users in this state
// is captured in Plan-daily-driver-finish/Epic-shipping-the-shell/
// Task-onboarding-zero-workspace-flow.md.
redirect("/sign-in?error=no_workspace"); redirect("/sign-in?error=no_workspace");
} }

View file

@ -0,0 +1,22 @@
import type { ReactNode } from "react";
import { cn } from "@/lib/utils";
type BorderGlowCardProps = {
children: ReactNode;
className?: string;
as?: "div" | "article" | "li";
};
export function BorderGlowCard({ children, className, as: Tag = "div" }: BorderGlowCardProps) {
return (
<Tag
className={cn(
"marketing-glow-card rounded-md border border-border",
className,
)}
>
{children}
</Tag>
);
}

View file

@ -0,0 +1,78 @@
import { Reveal } from "@/components/marketing/reveal";
const rows = [
{ label: "Markdown backlog in git", sbmtr: true, linear: false, notion: "partial", convoy: false },
{ label: "MCP claim / complete", sbmtr: true, linear: false, notion: false, convoy: true },
{ label: "Full workspace surface", sbmtr: true, linear: "partial", notion: true, convoy: false },
{ label: "Role-based convoys", sbmtr: true, linear: false, notion: false, convoy: true },
{ label: "Human approval gates", sbmtr: true, linear: false, notion: false, convoy: "partial" },
] as const;
function Cell({ value }: { value: boolean | string }) {
if (value === true) return <span className="font-mono-brand text-agent">yes</span>;
if (value === false) return <span className="text-muted-foreground/50">—</span>;
return <span className="font-mono-brand text-matter">{value}</span>;
}
export function ComparisonSection() {
return (
<section className="border-b border-border bg-background py-20 sm:py-28">
<div className="mx-auto max-w-6xl px-4 sm:px-6">
<Reveal className="mb-10 max-w-2xl">
<p className="font-mono-brand text-[11px] uppercase tracking-[0.2em] text-muted-foreground">Compare</p>
<h2 className="mt-4 font-marketing text-3xl font-semibold tracking-[-0.02em] text-foreground">
The agent-native lane
</h2>
</Reveal>
<Reveal delay={0.08}>
<div className="overflow-x-auto">
<table className="w-full min-w-[640px] border-collapse text-left text-sm">
<thead>
<tr className="border-b-2 border-foreground">
<th className="py-3 pr-4 font-marketing font-medium text-muted-foreground" scope="col">
Capability
</th>
<th
className="px-4 py-3 font-display text-xs font-extrabold tracking-[0.2em] text-foreground"
scope="col"
>
SBMTR
</th>
<th className="px-4 py-3 font-marketing font-medium text-muted-foreground" scope="col">
Linear
</th>
<th className="px-4 py-3 font-marketing font-medium text-muted-foreground" scope="col">
Notion
</th>
<th className="px-4 py-3 font-marketing font-medium text-muted-foreground" scope="col">
Convoy tools
</th>
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr key={row.label} className="border-b border-border">
<td className="py-4 pr-4 font-marketing text-foreground">{row.label}</td>
<td className="px-4 py-4">
<Cell value={row.sbmtr} />
</td>
<td className="px-4 py-4">
<Cell value={row.linear} />
</td>
<td className="px-4 py-4">
<Cell value={row.notion} />
</td>
<td className="px-4 py-4">
<Cell value={row.convoy} />
</td>
</tr>
))}
</tbody>
</table>
</div>
</Reveal>
</div>
</section>
);
}

View file

@ -0,0 +1,35 @@
import Link from "next/link";
import { ArrowRight } from "lucide-react";
import { Reveal } from "@/components/marketing/reveal";
export function CtaSection() {
return (
<section className="border-b border-border bg-[hsl(var(--hero))] py-20 text-[hsl(var(--hero-foreground))] sm:py-28">
<div className="mx-auto max-w-6xl px-4 sm:px-6">
<Reveal className="mx-auto max-w-3xl text-center">
<p className="font-mono-brand text-[11px] uppercase tracking-[0.25em] text-[hsl(var(--hero-muted))]">---</p>
<h2 className="mt-6 font-display text-5xl font-extrabold tracking-[0.12em] sm:text-7xl">SBMTR</h2>
<p className="mx-auto mt-6 max-w-lg font-marketing text-lg text-[hsl(var(--hero-muted))]">
Subagents. Frontmatter. One workspace. Open Monday morning in Submatter — not a demo shell.
</p>
<div className="mt-10 flex flex-wrap items-center justify-center gap-6">
<Link
href="/sign-up"
className="group inline-flex items-center gap-2 border-b-2 border-[hsl(var(--matter))] pb-1 font-marketing text-sm font-medium transition-colors hover:text-[hsl(var(--matter))]"
>
Start free
<ArrowRight className="size-4 transition-transform group-hover:translate-x-0.5" aria-hidden />
</Link>
<Link
href="/landing/pricing"
className="font-marketing text-sm text-[hsl(var(--hero-muted))] transition-colors hover:text-[hsl(var(--hero-foreground))]"
>
View pricing
</Link>
</div>
</Reveal>
</div>
</section>
);
}

View file

@ -0,0 +1,21 @@
import { cn } from "@/lib/utils";
type DotFieldProps = {
className?: string;
variant?: "hero" | "light";
};
export function DotField({ className, variant = "hero" }: DotFieldProps) {
return (
<div
className={cn(
"pointer-events-none absolute inset-0",
variant === "hero" ? "marketing-dot-field marketing-scanline" : "opacity-30",
variant === "light" &&
"bg-[radial-gradient(circle_at_center,hsl(var(--foreground)/0.07)_1px,transparent_1px)] [background-size:24px_24px]",
className,
)}
aria-hidden
/>
);
}

View file

@ -0,0 +1,110 @@
import { Bot, FileText, GitBranch, Layers, Route, Shield } from "lucide-react";
import type { LucideIcon } from "lucide-react";
import { BorderGlowCard } from "@/components/marketing/border-glow-card";
import { DotField } from "@/components/marketing/dot-field";
import { Reveal, Stagger, StaggerItem } from "@/components/marketing/reveal";
type Feature = {
icon: LucideIcon;
title: string;
description: string;
span: string;
tag: string;
};
const features: Feature[] = [
{
icon: FileText,
title: "Markdown source of truth",
description: "Plan / Epic / Task files in git. Frontmatter fields drive status, priority, and agent routing.",
span: "lg:col-span-7",
tag: "sync",
},
{
icon: Bot,
title: "MCP lifecycle",
description: "claim_task → work → complete_task. Auditable runs, not ad-hoc object patches.",
span: "lg:col-span-5",
tag: "mcp",
},
{
icon: GitBranch,
title: "Convoy pipelines",
description: "Architect, implementer, reviewer roles with human gates between stages.",
span: "lg:col-span-4",
tag: "convoy",
},
{
icon: Layers,
title: "Full workspace",
description: "Docs, boards, forms, whiteboards — same object model humans and agents share.",
span: "lg:col-span-4",
tag: "workspace",
},
{
icon: Shield,
title: "Multitenant",
description: "workspace_id on every row. Built for teams, not a single homelab tenant.",
span: "lg:col-span-4",
tag: "tenant",
},
];
export function FeaturesSection() {
return (
<section id="features" className="relative border-b border-border bg-background py-20 sm:py-28">
<DotField variant="light" className="opacity-40" />
<div className="relative mx-auto max-w-6xl px-4 sm:px-6">
<Reveal className="mb-14 max-w-2xl">
<p className="font-mono-brand text-[11px] uppercase tracking-[0.2em] text-muted-foreground">Capabilities</p>
<h2 className="mt-4 font-marketing text-3xl font-semibold tracking-[-0.02em] text-foreground sm:text-4xl">
Not a chat wrapper.
<br />
A coordination plane.
</h2>
</Reveal>
<Stagger className="grid gap-4 lg:grid-cols-12">
{features.map((feature) => (
<StaggerItem key={feature.title} className={feature.span}>
<BorderGlowCard className="flex h-full flex-col p-6">
<div className="mb-5 flex items-center justify-between">
<feature.icon className="size-5 text-primary" strokeWidth={1.75} aria-hidden />
<span className="font-mono-brand text-[10px] uppercase tracking-widest text-muted-foreground">
{feature.tag}
</span>
</div>
<h3 className="font-marketing text-lg font-semibold text-foreground">{feature.title}</h3>
<p className="mt-2 flex-1 font-marketing text-sm leading-relaxed text-muted-foreground">
{feature.description}
</p>
</BorderGlowCard>
</StaggerItem>
))}
<StaggerItem className="lg:col-span-12">
<BorderGlowCard className="flex flex-col gap-4 p-6 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-start gap-4">
<Route className="mt-0.5 size-5 shrink-0 text-matter" strokeWidth={1.75} aria-hidden />
<div>
<h3 className="font-marketing text-lg font-semibold text-foreground">Per-role model routing</h3>
<p className="mt-1 max-w-2xl font-marketing text-sm text-muted-foreground">
Fast implementers, premium architects — declared in convoy frontmatter and enforced by your
agent pipeline.
</p>
</div>
</div>
<p className="font-mono-brand text-xs text-muted-foreground sm:text-right">
model_policy: composer-fast
<br />
tier: fast | premium
</p>
</BorderGlowCard>
</StaggerItem>
</Stagger>
</div>
</section>
);
}

View file

@ -0,0 +1,75 @@
import Link from "next/link";
import { ArrowRight } from "lucide-react";
import { DotField } from "@/components/marketing/dot-field";
import { ProductDemo } from "@/components/marketing/product-demo";
import { Reveal } from "@/components/marketing/reveal";
export function HeroSection() {
return (
<section className="relative overflow-hidden bg-[hsl(var(--hero))] text-[hsl(var(--hero-foreground))]">
<DotField />
<div
className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_60%_50%_at_70%_20%,hsl(var(--matter)/0.08),transparent_60%)]"
aria-hidden
/>
<div className="relative mx-auto grid max-w-6xl gap-12 px-4 pb-20 pt-12 sm:px-6 lg:grid-cols-2 lg:items-center lg:gap-16 lg:pb-28 lg:pt-16">
<div className="space-y-8">
<Reveal>
<p className="font-mono-brand text-[11px] uppercase tracking-[0.2em] text-[hsl(var(--hero-muted))]">
Agent-native workspace
</p>
</Reveal>
<Reveal delay={0.05}>
<h1 className="max-w-xl font-marketing text-[2.75rem] font-semibold leading-[1.02] tracking-[-0.03em] sm:text-6xl lg:text-[4.25rem]">
The matter beneath{" "}
<span className="font-display font-extrabold uppercase tracking-[0.08em] text-[hsl(var(--matter))]">
SBMTR
</span>
</h1>
</Reveal>
<Reveal delay={0.1}>
<p className="max-w-lg font-marketing text-lg leading-relaxed text-[hsl(var(--hero-muted))]">
Submatter syncs your <span className="font-mono-brand text-[hsl(var(--hero-foreground))]">plans/</span>{" "}
backlog to a real workspace, then lets Cursor subagents claim tasks through MCP — with convoy gates
humans actually approve.
</p>
</Reveal>
<Reveal delay={0.15}>
<div className="flex flex-wrap items-center gap-3">
<Link
href="/sign-up"
className="group inline-flex h-12 items-center gap-2 border-b-2 border-[hsl(var(--matter))] bg-transparent pb-0.5 font-marketing text-sm font-medium text-[hsl(var(--hero-foreground))] transition-colors hover:text-[hsl(var(--matter))]"
>
Start free
<ArrowRight className="size-4 transition-transform group-hover:translate-x-0.5" aria-hidden />
</Link>
<Link
href="/landing#pipeline"
className="inline-flex h-12 items-center px-1 font-marketing text-sm text-[hsl(var(--hero-muted))] transition-colors hover:text-[hsl(var(--hero-foreground))]"
>
See the pipeline
</Link>
</div>
</Reveal>
<Reveal delay={0.2}>
<p className="font-mono-brand text-[11px] text-[hsl(var(--hero-muted))]">
subagents · frontmatter · one workspace
</p>
</Reveal>
</div>
<Reveal delay={0.12} y={32} className="lg:translate-y-4">
<ProductDemo />
</Reveal>
</div>
<div className="h-1 bg-[hsl(var(--matter))]" aria-hidden />
</section>
);
}

View file

@ -0,0 +1,72 @@
import { Reveal, Stagger, StaggerItem } from "@/components/marketing/reveal";
const steps = [
{
n: "01",
title: "Write in plans/",
body: "YAML frontmatter on every Plan, Epic, and Task. Versioned in git like the rest of your codebase.",
code: "plans/Plan-*/Epic-*/Task-*.md",
},
{
n: "02",
title: "Sync to Submatter",
body: "Importer mirrors frontmatter into Postgres. Cursor mappings keep agent ids aligned with slugs.",
code: "watch:markdown-backlog",
},
{
n: "03",
title: "Agents claim work",
body: "Subagents call MCP tools against a scoped workspace_id. Runs appear in fleet history.",
code: "claim_task → complete_task",
},
{
n: "04",
title: "Humans gate convoys",
body: "Reviewer and architect roles pause for approval. Status flows back to markdown on merge.",
code: "ready → in_progress → done",
},
] as const;
export function HowItWorksSection() {
return (
<section id="pipeline" className="border-b border-border bg-card py-20 sm:py-28">
<div className="mx-auto max-w-6xl px-4 sm:px-6">
<Reveal className="mb-14 flex flex-col gap-6 lg:flex-row lg:items-end lg:justify-between">
<div className="max-w-xl">
<p className="font-mono-brand text-[11px] uppercase tracking-[0.2em] text-muted-foreground">
How it works
</p>
<h2 className="mt-4 font-marketing text-3xl font-semibold tracking-[-0.02em] text-foreground sm:text-4xl">
From frontmatter to shipped
</h2>
</div>
<p className="max-w-sm font-marketing text-sm text-muted-foreground lg:text-right">
Interactive product demo pattern — show the file, show the pipeline, show the gate.
</p>
</Reveal>
<Stagger className="relative grid gap-0 md:grid-cols-4">
<div
className="absolute left-0 right-0 top-8 hidden h-px bg-border md:block"
aria-hidden
/>
{steps.map((step) => (
<StaggerItem key={step.n} className="relative border-t border-border pt-8 md:border-t-0 md:pt-0">
<div className="md:px-4 md:first:pl-0 md:last:pr-0">
<div className="mb-6 flex items-center gap-3">
<span className="flex size-8 items-center justify-center rounded-full border border-border bg-background font-mono-brand text-xs text-foreground">
{step.n}
</span>
<span className="hidden h-px flex-1 bg-border md:block" aria-hidden />
</div>
<h3 className="font-marketing text-base font-semibold text-foreground">{step.title}</h3>
<p className="mt-2 font-marketing text-sm leading-relaxed text-muted-foreground">{step.body}</p>
<p className="mt-4 font-mono-brand text-[11px] text-primary">{step.code}</p>
</div>
</StaggerItem>
))}
</Stagger>
</div>
</section>
);
}

View file

@ -0,0 +1,61 @@
import Link from "next/link";
import { cn } from "@/lib/utils";
type LogoProps = {
className?: string;
showTagline?: boolean;
size?: "sm" | "md" | "lg";
variant?: "dark" | "light";
};
const sizeClasses = {
sm: { rule: "w-8", word: "text-xl tracking-[0.22em]", tag: "text-[10px]" },
md: { rule: "w-10", word: "text-2xl tracking-[0.24em]", tag: "text-xs" },
lg: { rule: "w-14", word: "text-4xl tracking-[0.26em]", tag: "text-sm" },
};
export function Logo({
className,
showTagline = false,
size = "md",
variant = "light",
}: LogoProps) {
const s = sizeClasses[size];
const isDark = variant === "dark";
return (
<Link href="/landing" className={cn("group inline-flex flex-col gap-1", className)}>
<span
className={cn(
"h-0.5 rounded-full transition-colors",
s.rule,
isDark
? "bg-[hsl(var(--hero-foreground))] group-hover:bg-[hsl(var(--matter))]"
: "bg-foreground group-hover:bg-primary",
)}
aria-hidden
/>
<span
className={cn(
"font-display font-extrabold leading-none",
s.word,
isDark ? "text-[hsl(var(--hero-foreground))]" : "text-foreground",
)}
>
SBMTR
</span>
{showTagline ? (
<span
className={cn(
"font-marketing",
s.tag,
isDark ? "text-[hsl(var(--hero-muted))]" : "text-muted-foreground",
)}
>
Submatter
</span>
) : null}
</Link>
);
}

View file

@ -0,0 +1,116 @@
"use client";
import { motion, useReducedMotion } from "motion/react";
import { Bot, CheckCircle2, GitBranch, Terminal } from "lucide-react";
import { cn } from "@/lib/utils";
const pipelineSteps = [
{ role: "architect", model: "opus", status: "done" as const },
{ role: "implementer", model: "fast", status: "active" as const },
{ role: "reviewer", model: "fast", status: "queued" as const },
];
export function ProductDemo({ className }: { className?: string }) {
const reduceMotion = useReducedMotion();
return (
<div
className={cn(
"relative overflow-hidden rounded-md border border-[hsl(var(--hero-border))] bg-[hsl(240_6%_6%)] shadow-2xl shadow-black/40",
className,
)}
>
<div className="flex items-center gap-2 border-b border-[hsl(var(--hero-border))] px-4 py-3">
<div className="flex gap-1.5" aria-hidden>
<span className="size-2.5 rounded-full bg-[#FF5F57]" />
<span className="size-2.5 rounded-full bg-[#FEBC2E]" />
<span className="size-2.5 rounded-full bg-[#28C840]" />
</div>
<p className="flex-1 text-center font-mono-brand text-[11px] text-[hsl(var(--hero-muted))]">
submatter — convoy · mcp-claim-task-tool
</p>
</div>
<div className="grid gap-0 lg:grid-cols-5">
<div className="border-b border-[hsl(var(--hero-border))] p-4 lg:col-span-2 lg:border-b-0 lg:border-r">
<div className="mb-3 flex items-center gap-2 text-[hsl(var(--hero-muted))]">
<Terminal className="size-3.5" aria-hidden />
<span className="font-mono-brand text-[10px] uppercase tracking-widest">plans/</span>
</div>
<pre className="font-mono-brand text-[11px] leading-relaxed text-[hsl(var(--hero-foreground))]">
<span className="text-[hsl(var(--hero-muted))]">---{"\n"}</span>
<span className="text-[hsl(var(--agent))]">kind</span>
<span className="text-[hsl(var(--hero-muted))]">: </span>task{"\n"}
<span className="text-[hsl(var(--agent))]">slug</span>
<span className="text-[hsl(var(--hero-muted))]">: </span>mcp-claim-task-tool{"\n"}
<span className="text-[hsl(var(--agent))]">status</span>
<span className="text-[hsl(var(--hero-muted))]">: </span>
<span className="text-[hsl(var(--matter))]">in_progress</span>
{"\n"}
<span className="text-[hsl(var(--agent))]">priority</span>
<span className="text-[hsl(var(--hero-muted))]">: </span>P1{"\n"}
<span className="text-[hsl(var(--agent))]">model_policy</span>
<span className="text-[hsl(var(--hero-muted))]">: </span>composer-fast{"\n"}
<span className="text-[hsl(var(--hero-muted))]">---</span>
<span className="marketing-cursor-blink text-[hsl(var(--matter))]">▍</span>
</pre>
</div>
<div className="p-4 lg:col-span-3">
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-2">
<GitBranch className="size-3.5 text-[hsl(var(--hero-muted))]" aria-hidden />
<span className="font-mono-brand text-[10px] uppercase tracking-widest text-[hsl(var(--hero-muted))]">
Convoy pipeline
</span>
</div>
<span className="inline-flex items-center gap-1.5 rounded-full border border-[hsl(var(--hero-border))] px-2 py-0.5 font-mono-brand text-[10px] text-[hsl(var(--agent))]">
<span className="marketing-pulse-dot size-1.5 rounded-full bg-[hsl(var(--agent))]" />
MCP live
</span>
</div>
<ol className="space-y-2">
{pipelineSteps.map((step, index) => (
<motion.li
key={step.role}
className={cn(
"flex items-center justify-between rounded border px-3 py-2.5",
step.status === "active"
? "border-[hsl(var(--matter)/0.45)] bg-[hsl(var(--matter)/0.08)]"
: "border-[hsl(var(--hero-border))] bg-[hsl(240_5%_8%)]",
)}
initial={reduceMotion ? false : { opacity: 0, x: 12 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.15 + index * 0.12, duration: 0.45 }}
>
<div className="flex items-center gap-2.5">
{step.status === "done" ? (
<CheckCircle2 className="size-3.5 text-[hsl(var(--agent))]" aria-hidden />
) : (
<Bot
className={cn(
"size-3.5",
step.status === "active" ? "text-[hsl(var(--matter))]" : "text-[hsl(var(--hero-muted))]",
)}
aria-hidden
/>
)}
<span className="font-mono-brand text-xs text-[hsl(var(--hero-foreground))]">
role-{step.role}
</span>
</div>
<span className="font-mono-brand text-[10px] text-[hsl(var(--hero-muted))]">{step.model}</span>
</motion.li>
))}
</ol>
<div className="mt-4 rounded border border-dashed border-[hsl(var(--hero-border))] px-3 py-2 font-mono-brand text-[10px] text-[hsl(var(--hero-muted))]">
claim_task · workspace_id scoped · run ar_8f2a
</div>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,90 @@
"use client";
import type { ReactNode } from "react";
import { motion, useReducedMotion } from "motion/react";
type RevealProps = {
children: ReactNode;
className?: string;
delay?: number;
y?: number;
};
export function Reveal({ children, className, delay = 0, y = 24 }: RevealProps) {
const reduceMotion = useReducedMotion();
if (reduceMotion) {
return <div className={className}>{children}</div>;
}
return (
<motion.div
className={className}
initial={{ opacity: 0, y }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-60px" }}
transition={{ duration: 0.55, delay, ease: [0.22, 1, 0.36, 1] }}
>
{children}
</motion.div>
);
}
type StaggerProps = {
children: ReactNode;
className?: string;
stagger?: number;
};
export function Stagger({ children, className, stagger = 0.08 }: StaggerProps) {
const reduceMotion = useReducedMotion();
if (reduceMotion) {
return <div className={className}>{children}</div>;
}
return (
<motion.div
className={className}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-40px" }}
variants={{
hidden: {},
visible: { transition: { staggerChildren: stagger } },
}}
>
{children}
</motion.div>
);
}
export function StaggerItem({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
const reduceMotion = useReducedMotion();
if (reduceMotion) {
return <div className={className}>{children}</div>;
}
return (
<motion.div
className={className}
variants={{
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.5, ease: [0.22, 1, 0.36, 1] },
},
}}
>
{children}
</motion.div>
);
}

View file

@ -0,0 +1,44 @@
import Link from "next/link";
import { Logo } from "@/components/marketing/logo";
const footerLinks = [
{ href: "/landing#features", label: "Features" },
{ href: "/landing#pipeline", label: "Pipeline" },
{ href: "/landing/pricing", label: "Pricing" },
{ href: "/sign-in", label: "Sign in" },
] as const;
export function SiteFooter() {
return (
<footer className="bg-card">
<div className="mx-auto flex max-w-6xl flex-col gap-10 px-4 py-14 sm:px-6 md:flex-row md:items-start md:justify-between">
<div className="space-y-3">
<Logo size="sm" showTagline variant="light" />
<p className="max-w-xs font-marketing text-sm text-muted-foreground">
Subagents. Frontmatter. One workspace.
</p>
</div>
<nav className="flex flex-wrap gap-x-10 gap-y-3" aria-label="Footer">
{footerLinks.map((item) => (
<Link
key={item.href}
href={item.href}
className="font-marketing text-sm text-muted-foreground transition-colors hover:text-foreground"
>
{item.label}
</Link>
))}
</nav>
</div>
<div className="border-t-2 border-foreground">
<div className="mx-auto flex max-w-6xl flex-col gap-2 px-4 py-5 sm:flex-row sm:items-center sm:justify-between sm:px-6">
<p className="font-mono-brand text-[11px] text-muted-foreground">© {new Date().getFullYear()} Submatter</p>
<p className="font-mono-brand text-[11px] text-muted-foreground">sbmtr.io · built for agent pipelines</p>
</div>
</div>
</footer>
);
}

View file

@ -0,0 +1,52 @@
import Link from "next/link";
import { Logo } from "@/components/marketing/logo";
import { cn } from "@/lib/utils";
const nav = [
{ href: "/landing#features", label: "Features" },
{ href: "/landing#pipeline", label: "Pipeline" },
{ href: "/landing/pricing", label: "Pricing" },
] as const;
export function SiteHeader({ className }: { className?: string }) {
return (
<header
className={cn(
"sticky top-0 z-50 border-b border-[hsl(var(--hero-border))] bg-[hsl(var(--hero)/0.85)] text-[hsl(var(--hero-foreground))] backdrop-blur-md",
className,
)}
>
<div className="mx-auto flex h-14 max-w-6xl items-center justify-between gap-6 px-4 sm:px-6">
<Logo size="sm" variant="dark" />
<nav className="hidden items-center gap-8 md:flex" aria-label="Main">
{nav.map((item) => (
<Link
key={item.href}
href={item.href}
className="font-marketing text-sm text-[hsl(var(--hero-muted))] transition-colors hover:text-[hsl(var(--hero-foreground))]"
>
{item.label}
</Link>
))}
</nav>
<div className="flex items-center gap-2 sm:gap-3">
<Link
href="/sign-in"
className="hidden font-marketing text-sm text-[hsl(var(--hero-muted))] transition-colors hover:text-[hsl(var(--hero-foreground))] sm:inline"
>
Sign in
</Link>
<Link
href="/sign-up"
className="inline-flex h-9 items-center border border-[hsl(var(--matter))] px-4 font-marketing text-sm font-medium text-[hsl(var(--matter))] transition-colors hover:bg-[hsl(var(--matter)/0.1)]"
>
Start free
</Link>
</div>
</div>
</header>
);
}

View file

@ -6,6 +6,7 @@ export default auth((req) => {
const isPublic = const isPublic =
pathname === "/" || pathname === "/" ||
pathname.startsWith("/landing") ||
pathname.startsWith("/sign-in") || pathname.startsWith("/sign-in") ||
pathname.startsWith("/sign-up") || pathname.startsWith("/sign-up") ||
pathname.startsWith("/api/auth"); pathname.startsWith("/api/auth");

View file

@ -63,6 +63,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"drizzle-orm": "^0.38.0", "drizzle-orm": "^0.38.0",
"lucide-react": "^0.468.0", "lucide-react": "^0.468.0",
"motion": "^12.42.2",
"next": "^15.1.0", "next": "^15.1.0",
"next-auth": "^5.0.0-beta.25", "next-auth": "^5.0.0-beta.25",
"next-themes": "^0.4.4", "next-themes": "^0.4.4",

View file

@ -19,6 +19,11 @@
--teal: 168 76% 42%; --teal: 168 76% 42%;
--teal-foreground: 0 0% 100%; --teal-foreground: 0 0% 100%;
--matter: 32 92% 50%;
--matter-foreground: 220 25% 12%;
--agent: 168 55% 36%;
--agent-foreground: 0 0% 100%;
--secondary: 210 40% 96%; --secondary: 210 40% 96%;
--secondary-foreground: 222 47% 11%; --secondary-foreground: 222 47% 11%;

View file

@ -0,0 +1,160 @@
/* Submatter marketing — Signal Editorial (ui-ux-pro-max: Swiss + interactive demo + bold type) */
.marketing-theme {
--background: 40 18% 96%;
--foreground: 220 20% 8%;
--card: 0 0% 100%;
--card-foreground: 220 20% 8%;
--primary: 228 45% 22%;
--primary-foreground: 40 20% 98%;
--matter: 32 95% 52%;
--matter-foreground: 220 20% 8%;
--agent: 162 72% 38%;
--agent-foreground: 0 0% 100%;
--secondary: 40 12% 92%;
--secondary-foreground: 220 20% 8%;
--muted: 40 10% 93%;
--muted-foreground: 220 8% 42%;
--accent: 228 30% 94%;
--accent-foreground: 228 45% 22%;
--border: 40 8% 86%;
--input: 40 8% 86%;
--ring: 32 95% 52%;
--hero: 240 6% 4%;
--hero-foreground: 40 15% 96%;
--hero-muted: 240 4% 55%;
--hero-border: 240 4% 14%;
--radius: 0.375rem;
color-scheme: light;
}
.marketing-theme .font-display {
font-family: var(--font-display), system-ui, sans-serif;
}
.marketing-theme .font-marketing {
font-family: var(--font-marketing-sans), system-ui, sans-serif;
}
.marketing-theme .font-mono-brand {
font-family: var(--font-mono-brand), ui-monospace, monospace;
}
/* Dot field (React Bits–style ambient grid) */
.marketing-dot-field {
background-image: radial-gradient(
circle at center,
hsl(var(--hero-foreground) / 0.12) 1px,
transparent 1px
);
background-size: 28px 28px;
mask-image: radial-gradient(ellipse 80% 70% at 50% 40%, black 20%, transparent 75%);
}
.marketing-scanline {
background: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
hsl(var(--hero-foreground) / 0.02) 2px,
hsl(var(--hero-foreground) / 0.02) 4px
);
pointer-events: none;
}
/* Border glow card (React Bits Border Glow–inspired) */
.marketing-glow-card {
position: relative;
background: hsl(var(--card));
transition: box-shadow 200ms ease, border-color 200ms ease;
}
.marketing-glow-card::before {
content: "";
position: absolute;
inset: -1px;
border-radius: inherit;
padding: 1px;
background: linear-gradient(
135deg,
hsl(var(--matter) / 0),
hsl(var(--matter) / 0.35),
hsl(var(--agent) / 0.2),
hsl(var(--matter) / 0)
);
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
opacity: 0;
transition: opacity 200ms ease;
pointer-events: none;
}
.marketing-glow-card:hover::before,
.marketing-glow-card:focus-within::before {
opacity: 1;
}
.marketing-glow-card:hover {
box-shadow:
0 0 0 1px hsl(var(--border)),
0 12px 40px -12px hsl(var(--foreground) / 0.12);
}
@keyframes marketing-blink {
0%,
49% {
opacity: 1;
}
50%,
100% {
opacity: 0;
}
}
.marketing-cursor-blink {
animation: marketing-blink 1.1s step-end infinite;
}
@keyframes marketing-pulse-dot {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.55;
transform: scale(0.92);
}
}
.marketing-pulse-dot {
animation: marketing-pulse-dot 2s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.marketing-cursor-blink,
.marketing-pulse-dot {
animation: none;
}
.marketing-glow-card::before {
transition: none;
}
}

View file

@ -55,6 +55,14 @@ const config: Config = {
DEFAULT: "hsl(var(--teal))", DEFAULT: "hsl(var(--teal))",
foreground: "hsl(var(--teal-foreground))", foreground: "hsl(var(--teal-foreground))",
}, },
matter: {
DEFAULT: "hsl(var(--matter))",
foreground: "hsl(var(--matter-foreground, 0 0% 100%))",
},
agent: {
DEFAULT: "hsl(var(--agent))",
foreground: "hsl(var(--agent-foreground, 0 0% 100%))",
},
}, },
borderRadius: { borderRadius: {
lg: "var(--radius)", lg: "var(--radius)",

View file

@ -290,6 +290,9 @@ importers:
lucide-react: lucide-react:
specifier: ^0.468.0 specifier: ^0.468.0
version: 0.468.0(react@19.2.4) version: 0.468.0(react@19.2.4)
motion:
specifier: ^12.42.2
version: 12.42.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
next: next:
specifier: ^15.1.0 specifier: ^15.1.0
version: 15.5.14(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) version: 15.5.14(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@ -3835,6 +3838,20 @@ packages:
resolution: {integrity: sha512-PcOxmqwYCW7O2ovKRU8OoQQj2yqTfEB/yeTYk4gPid6dN5ODRfU1hXd9tTVZzax/0NkO7AxpHykvZnT1aYp/BQ==} resolution: {integrity: sha512-PcOxmqwYCW7O2ovKRU8OoQQj2yqTfEB/yeTYk4gPid6dN5ODRfU1hXd9tTVZzax/0NkO7AxpHykvZnT1aYp/BQ==}
engines: {node: ^14.13.1 || >=16.0.0} engines: {node: ^14.13.1 || >=16.0.0}
framer-motion@12.42.2:
resolution: {integrity: sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
fresh@2.0.0: fresh@2.0.0:
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@ -4362,6 +4379,26 @@ packages:
mlly@1.8.2: mlly@1.8.2:
resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
motion-dom@12.42.2:
resolution: {integrity: sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA==}
motion-utils@12.39.0:
resolution: {integrity: sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==}
motion@12.42.2:
resolution: {integrity: sha512-Atvv11yUKIid41cVrRBDVX5m8tF8kNpExRSlbpt6APClhDjtwQssgFHhQzejxw7/7YYbjHSPKBVbHo05BuJT5Q==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
ms@2.1.3: ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@ -8719,6 +8756,15 @@ snapshots:
fractional-indexing@3.2.0: {} fractional-indexing@3.2.0: {}
framer-motion@12.42.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
motion-dom: 12.42.2
motion-utils: 12.39.0
tslib: 2.8.1
optionalDependencies:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
fresh@2.0.0: {} fresh@2.0.0: {}
fsevents@2.3.3: fsevents@2.3.3:
@ -9250,6 +9296,20 @@ snapshots:
pkg-types: 1.3.1 pkg-types: 1.3.1
ufo: 1.6.3 ufo: 1.6.3
motion-dom@12.42.2:
dependencies:
motion-utils: 12.39.0
motion-utils@12.39.0: {}
motion@12.42.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
framer-motion: 12.42.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
tslib: 2.8.1
optionalDependencies:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
ms@2.1.3: {} ms@2.1.3: {}
mz@2.7.0: mz@2.7.0: