From 2f6adf4382bbe9f812c1ab2f5ce36d248cc78412 Mon Sep 17 00:00:00 2001 From: Randall Stillwell Date: Fri, 10 Jul 2026 22:50:05 -0500 Subject: [PATCH] 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 --- apps/web/app/(marketing)/landing/page.tsx | 29 ++++ .../app/(marketing)/landing/pricing/page.tsx | 125 ++++++++++++++ apps/web/app/(marketing)/layout.tsx | 47 +++++ apps/web/app/page.tsx | 25 +-- .../components/marketing/border-glow-card.tsx | 22 +++ .../marketing/comparison-section.tsx | 78 +++++++++ apps/web/components/marketing/cta-section.tsx | 35 ++++ apps/web/components/marketing/dot-field.tsx | 21 +++ .../components/marketing/features-section.tsx | 110 ++++++++++++ .../web/components/marketing/hero-section.tsx | 75 ++++++++ .../marketing/how-it-works-section.tsx | 72 ++++++++ apps/web/components/marketing/logo.tsx | 61 +++++++ .../web/components/marketing/product-demo.tsx | 116 +++++++++++++ apps/web/components/marketing/reveal.tsx | 90 ++++++++++ apps/web/components/marketing/site-footer.tsx | 44 +++++ apps/web/components/marketing/site-header.tsx | 52 ++++++ apps/web/middleware.ts | 1 + apps/web/package.json | 1 + apps/web/styles/globals.css | 5 + apps/web/styles/marketing.css | 160 ++++++++++++++++++ apps/web/tailwind.config.ts | 8 + pnpm-lock.yaml | 60 +++++++ 22 files changed, 1215 insertions(+), 22 deletions(-) create mode 100644 apps/web/app/(marketing)/landing/page.tsx create mode 100644 apps/web/app/(marketing)/landing/pricing/page.tsx create mode 100644 apps/web/app/(marketing)/layout.tsx create mode 100644 apps/web/components/marketing/border-glow-card.tsx create mode 100644 apps/web/components/marketing/comparison-section.tsx create mode 100644 apps/web/components/marketing/cta-section.tsx create mode 100644 apps/web/components/marketing/dot-field.tsx create mode 100644 apps/web/components/marketing/features-section.tsx create mode 100644 apps/web/components/marketing/hero-section.tsx create mode 100644 apps/web/components/marketing/how-it-works-section.tsx create mode 100644 apps/web/components/marketing/logo.tsx create mode 100644 apps/web/components/marketing/product-demo.tsx create mode 100644 apps/web/components/marketing/reveal.tsx create mode 100644 apps/web/components/marketing/site-footer.tsx create mode 100644 apps/web/components/marketing/site-header.tsx create mode 100644 apps/web/styles/marketing.css diff --git a/apps/web/app/(marketing)/landing/page.tsx b/apps/web/app/(marketing)/landing/page.tsx new file mode 100644 index 0000000..07ec591 --- /dev/null +++ b/apps/web/app/(marketing)/landing/page.tsx @@ -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 ( + <> + +
+ + + + + +
+ + + ); +} diff --git a/apps/web/app/(marketing)/landing/pricing/page.tsx b/apps/web/app/(marketing)/landing/pricing/page.tsx new file mode 100644 index 0000000..6c37734 --- /dev/null +++ b/apps/web/app/(marketing)/landing/pricing/page.tsx @@ -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 ( + <> + +
+ +

Pricing

+

+ Predictable seats. +
+ No per-token surprise. +

+

+ Beta pricing for feedback. BYOK for model providers always. +

+
+ + + {tiers.map((tier) => ( + + + {tier.highlighted ? ( + + popular + + ) : ( + + )} +

{tier.name}

+

+ {tier.price} + {tier.period} +

+

{tier.description}

+
    + {tier.features.map((feature) => ( +
  • + + {feature} +
  • + ))} +
+ + {tier.cta} + +
+
+ ))} +
+ +

+ kind: pricing · status: draft · priority: P2 +

+
+ + + ); +} diff --git a/apps/web/app/(marketing)/layout.tsx b/apps/web/app/(marketing)/layout.tsx new file mode 100644 index 0000000..b60bd3d --- /dev/null +++ b/apps/web/app/(marketing)/layout.tsx @@ -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 ( +
+ {children} +
+ ); +} diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index f974339..1b7e6dc 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -6,27 +6,14 @@ import { db } from "@tasks/database/client"; import { workspaceMembers, workspaces } from "@tasks/database/schema"; /** - * Root landing route. Decides where an authenticated user goes when they - * hit `/`. The post-sign-in `callbackUrl` defaults here, so this is the - * 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. + * Root landing route. Unauthenticated visitors go to the public marketing site; + * signed-in users land on their oldest workspace home (`/{slug}/`). */ export default async function HomePage() { const session = await auth(); if (!session?.user?.id) { - redirect("/sign-in"); + redirect("/landing"); } const [membership] = await db @@ -41,11 +28,5 @@ export default async function HomePage() { 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"); } diff --git a/apps/web/components/marketing/border-glow-card.tsx b/apps/web/components/marketing/border-glow-card.tsx new file mode 100644 index 0000000..70bb3bc --- /dev/null +++ b/apps/web/components/marketing/border-glow-card.tsx @@ -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 ( + + {children} + + ); +} diff --git a/apps/web/components/marketing/comparison-section.tsx b/apps/web/components/marketing/comparison-section.tsx new file mode 100644 index 0000000..f3f52b9 --- /dev/null +++ b/apps/web/components/marketing/comparison-section.tsx @@ -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 yes; + if (value === false) return —; + return {value}; +} + +export function ComparisonSection() { + return ( +
+
+ +

Compare

+

+ The agent-native lane +

+
+ + +
+ + + + + + + + + + + + {rows.map((row) => ( + + + + + + + + ))} + +
+ Capability + + SBMTR + + Linear + + Notion + + Convoy tools +
{row.label} + + + + + + + +
+
+
+
+
+ ); +} diff --git a/apps/web/components/marketing/cta-section.tsx b/apps/web/components/marketing/cta-section.tsx new file mode 100644 index 0000000..5407791 --- /dev/null +++ b/apps/web/components/marketing/cta-section.tsx @@ -0,0 +1,35 @@ +import Link from "next/link"; +import { ArrowRight } from "lucide-react"; + +import { Reveal } from "@/components/marketing/reveal"; + +export function CtaSection() { + return ( +
+
+ +

---

+

SBMTR

+

+ Subagents. Frontmatter. One workspace. Open Monday morning in Submatter — not a demo shell. +

+
+ + Start free + + + + View pricing + +
+
+
+
+ ); +} diff --git a/apps/web/components/marketing/dot-field.tsx b/apps/web/components/marketing/dot-field.tsx new file mode 100644 index 0000000..ccc3373 --- /dev/null +++ b/apps/web/components/marketing/dot-field.tsx @@ -0,0 +1,21 @@ +import { cn } from "@/lib/utils"; + +type DotFieldProps = { + className?: string; + variant?: "hero" | "light"; +}; + +export function DotField({ className, variant = "hero" }: DotFieldProps) { + return ( +
+ ); +} diff --git a/apps/web/components/marketing/features-section.tsx b/apps/web/components/marketing/features-section.tsx new file mode 100644 index 0000000..65b8c8b --- /dev/null +++ b/apps/web/components/marketing/features-section.tsx @@ -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 ( +
+ + +
+ +

Capabilities

+

+ Not a chat wrapper. +
+ A coordination plane. +

+
+ + + {features.map((feature) => ( + + +
+ + + {feature.tag} + +
+

{feature.title}

+

+ {feature.description} +

+
+
+ ))} + + + +
+ +
+

Per-role model routing

+

+ Fast implementers, premium architects — declared in convoy frontmatter and enforced by your + agent pipeline. +

+
+
+

+ model_policy: composer-fast +
+ tier: fast | premium +

+
+
+
+
+
+ ); +} diff --git a/apps/web/components/marketing/hero-section.tsx b/apps/web/components/marketing/hero-section.tsx new file mode 100644 index 0000000..d6e5616 --- /dev/null +++ b/apps/web/components/marketing/hero-section.tsx @@ -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 ( +
+ +
+ +
+
+ +

+ Agent-native workspace +

+
+ + +

+ The matter beneath{" "} + + SBMTR + +

+
+ + +

+ Submatter syncs your plans/{" "} + backlog to a real workspace, then lets Cursor subagents claim tasks through MCP — with convoy gates + humans actually approve. +

+
+ + +
+ + Start free + + + + See the pipeline + +
+
+ + +

+ subagents · frontmatter · one workspace +

+
+
+ + + + +
+ +
+
+ ); +} diff --git a/apps/web/components/marketing/how-it-works-section.tsx b/apps/web/components/marketing/how-it-works-section.tsx new file mode 100644 index 0000000..88a2ea2 --- /dev/null +++ b/apps/web/components/marketing/how-it-works-section.tsx @@ -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 ( +
+
+ +
+

+ How it works +

+

+ From frontmatter to shipped +

+
+

+ Interactive product demo pattern — show the file, show the pipeline, show the gate. +

+
+ + +
+ {steps.map((step) => ( + +
+
+ + {step.n} + + +
+

{step.title}

+

{step.body}

+

{step.code}

+
+
+ ))} + +
+
+ ); +} diff --git a/apps/web/components/marketing/logo.tsx b/apps/web/components/marketing/logo.tsx new file mode 100644 index 0000000..8aff656 --- /dev/null +++ b/apps/web/components/marketing/logo.tsx @@ -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 ( + + + + SBMTR + + {showTagline ? ( + + Submatter + + ) : null} + + ); +} diff --git a/apps/web/components/marketing/product-demo.tsx b/apps/web/components/marketing/product-demo.tsx new file mode 100644 index 0000000..6424fc3 --- /dev/null +++ b/apps/web/components/marketing/product-demo.tsx @@ -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 ( +
+
+
+ + + +
+

+ submatter — convoy · mcp-claim-task-tool +

+
+ +
+
+
+ + plans/ +
+
+            ---{"\n"}
+            kind
+            : task{"\n"}
+            slug
+            : mcp-claim-task-tool{"\n"}
+            status
+            : 
+            in_progress
+            {"\n"}
+            priority
+            : P1{"\n"}
+            model_policy
+            : composer-fast{"\n"}
+            ---
+            ▍
+          
+
+ +
+
+
+ + + Convoy pipeline + +
+ + + MCP live + +
+ +
    + {pipelineSteps.map((step, index) => ( + +
    + {step.status === "done" ? ( + + ) : ( + + )} + + role-{step.role} + +
    + {step.model} +
    + ))} +
+ +
+ claim_task · workspace_id scoped · run ar_8f2a +
+
+
+
+ ); +} diff --git a/apps/web/components/marketing/reveal.tsx b/apps/web/components/marketing/reveal.tsx new file mode 100644 index 0000000..103ccce --- /dev/null +++ b/apps/web/components/marketing/reveal.tsx @@ -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
{children}
; + } + + return ( + + {children} + + ); +} + +type StaggerProps = { + children: ReactNode; + className?: string; + stagger?: number; +}; + +export function Stagger({ children, className, stagger = 0.08 }: StaggerProps) { + const reduceMotion = useReducedMotion(); + + if (reduceMotion) { + return
{children}
; + } + + return ( + + {children} + + ); +} + +export function StaggerItem({ + children, + className, +}: { + children: ReactNode; + className?: string; +}) { + const reduceMotion = useReducedMotion(); + + if (reduceMotion) { + return
{children}
; + } + + return ( + + {children} + + ); +} diff --git a/apps/web/components/marketing/site-footer.tsx b/apps/web/components/marketing/site-footer.tsx new file mode 100644 index 0000000..563e958 --- /dev/null +++ b/apps/web/components/marketing/site-footer.tsx @@ -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 ( +
+
+
+ +

+ Subagents. Frontmatter. One workspace. +

+
+ + +
+ +
+
+

© {new Date().getFullYear()} Submatter

+

sbmtr.io · built for agent pipelines

+
+
+
+ ); +} diff --git a/apps/web/components/marketing/site-header.tsx b/apps/web/components/marketing/site-header.tsx new file mode 100644 index 0000000..8c285be --- /dev/null +++ b/apps/web/components/marketing/site-header.tsx @@ -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 ( +
+
+ + + + +
+ + Sign in + + + Start free + +
+
+
+ ); +} diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index 2503fb6..0a02f53 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -6,6 +6,7 @@ export default auth((req) => { const isPublic = pathname === "/" || + pathname.startsWith("/landing") || pathname.startsWith("/sign-in") || pathname.startsWith("/sign-up") || pathname.startsWith("/api/auth"); diff --git a/apps/web/package.json b/apps/web/package.json index 6ea6ee9..fcc5482 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -63,6 +63,7 @@ "clsx": "^2.1.1", "drizzle-orm": "^0.38.0", "lucide-react": "^0.468.0", + "motion": "^12.42.2", "next": "^15.1.0", "next-auth": "^5.0.0-beta.25", "next-themes": "^0.4.4", diff --git a/apps/web/styles/globals.css b/apps/web/styles/globals.css index 5c03f9e..a31ca06 100644 --- a/apps/web/styles/globals.css +++ b/apps/web/styles/globals.css @@ -19,6 +19,11 @@ --teal: 168 76% 42%; --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-foreground: 222 47% 11%; diff --git a/apps/web/styles/marketing.css b/apps/web/styles/marketing.css new file mode 100644 index 0000000..52df5bb --- /dev/null +++ b/apps/web/styles/marketing.css @@ -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; + } +} diff --git a/apps/web/tailwind.config.ts b/apps/web/tailwind.config.ts index 38c64eb..3106b54 100644 --- a/apps/web/tailwind.config.ts +++ b/apps/web/tailwind.config.ts @@ -55,6 +55,14 @@ const config: Config = { DEFAULT: "hsl(var(--teal))", 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: { lg: "var(--radius)", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd546d5..2d06962 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -290,6 +290,9 @@ importers: lucide-react: specifier: ^0.468.0 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: 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) @@ -3835,6 +3838,20 @@ packages: resolution: {integrity: sha512-PcOxmqwYCW7O2ovKRU8OoQQj2yqTfEB/yeTYk4gPid6dN5ODRfU1hXd9tTVZzax/0NkO7AxpHykvZnT1aYp/BQ==} 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: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} @@ -4362,6 +4379,26 @@ packages: mlly@1.8.2: 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: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -8719,6 +8756,15 @@ snapshots: 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: {} fsevents@2.3.3: @@ -9250,6 +9296,20 @@ snapshots: pkg-types: 1.3.1 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: {} mz@2.7.0: