Files
venture_studio-site/src/components/site/pages.tsx

1297 lines
51 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState, type CSSProperties, type ReactNode } from "react";
import { ArrowRight, ArrowUpRight, Check, Compass, Layers, Minus, Sparkles } from "lucide-react";
import { CONTACT_EMAIL } from "@/lib/contact";
import { SplitTitle, type TitlePart } from "@/components/site/motion";
import { IndustryIndex, MoltenMark } from "@/components/site/creative";
import { CropsphereStory } from "@/components/site/cropsphere";
import { DoorMileStory, LoyalyStory } from "@/components/site/venture-stories";
// Section photography (Unsplash License): see src/assets/sections/CREDITS.md.
import principleEvidence from "@/assets/sections/principle-evidence.webp";
import principleEngine from "@/assets/sections/principle-engine.webp";
import principleStandalone from "@/assets/sections/principle-standalone.webp";
import modelBuild from "@/assets/sections/model-build.webp";
import modelSpinoff from "@/assets/sections/model-spinoff.webp";
import modelCompound from "@/assets/sections/model-compound.webp";
import {
Accent,
DuoPhoto,
EASE_STAGE,
Eyebrow,
FRAMEWORK,
FillWords,
NAV,
STACK,
SectionHeader,
VERTICALS,
logoFull,
logoMark,
orbitPosition,
reveal,
rise,
type SitePageName,
} from "@/components/site/sections";
/** Position of a page in the nav, as "01".."07". */
function pageNumber(page: SitePageName) {
return String(NAV.findIndex((n) => n.href === `/${page}`) + 1).padStart(2, "0");
}
// ---------------------------------------------------------------------------------------------
// Page hero
// ---------------------------------------------------------------------------------------------
/**
* Full-height opener for inner pages: page number and label, a headline whose words rise in
* one by one, the lead paragraph and a page-specific visual. As it scrolls away the visual
* drifts and the copy lifts (hero-timeline in styles.css).
*/
function PageHero({
page,
label,
title,
intro,
visual,
children,
}: {
page: SitePageName;
label: string;
title: TitlePart[];
intro: ReactNode;
visual: ReactNode;
children?: ReactNode;
}) {
return (
<section
id="top"
className="hero-timeline relative flex min-h-[100svh] flex-col overflow-hidden bg-ink"
>
<div className="hero-media absolute inset-0">
<div className="mesh-gold absolute inset-0 opacity-25" aria-hidden />
<div className="dot-grid absolute inset-0" aria-hidden />
<div
aria-hidden
className="absolute inset-0 opacity-20 lg:left-auto lg:w-[48%] lg:opacity-100 xl:w-[46%]"
{...rise(500)}
>
<div className="rise-in h-full w-full">{visual}</div>
</div>
<div
aria-hidden
className="absolute inset-0 bg-gradient-to-r from-ink via-ink/75 to-transparent lg:via-ink/40"
/>
<div className="absolute inset-x-0 bottom-0 h-40 bg-gradient-to-t from-ink to-transparent" />
</div>
<div className="hero-copy container-x relative flex flex-1 flex-col justify-between gap-12 pb-10 pt-28 md:pb-12 md:pt-36">
<div
{...rise(100)}
className="rise-in flex items-center gap-4 font-display text-xs font-semibold uppercase tracking-[0.2em]"
>
<span data-scramble="in" className="tabular-nums text-primary">
({pageNumber(page)})
</span>
<span className="h-px w-12 bg-primary/50" aria-hidden />
<span data-scramble="in" className="text-muted-foreground">
{label}
</span>
</div>
<h1 className="max-w-[14ch] font-display text-[clamp(2.25rem,10vw,3.5rem)] md:text-[clamp(3.5rem,6.6vw,6.25rem)] font-extrabold leading-[0.93] tracking-tighter">
<SplitTitle parts={title} />
</h1>
<div className="grid items-end gap-8 md:grid-cols-12">
<div className="md:col-span-7 lg:col-span-6">
<p
{...rise(700)}
className="rise-in max-w-[52ch] text-lg leading-relaxed text-muted-foreground md:text-xl"
>
{intro}
</p>
{children ? (
<div {...rise(850)} className="rise-in mt-8">
{children}
</div>
) : null}
</div>
<a
href="#main-content"
{...rise(1000)}
className="rise-in hidden items-center gap-4 justify-self-end font-display text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground transition-colors hover:text-primary md:col-span-5 md:flex lg:col-span-6"
>
Scroll to explore
<span className="relative block h-12 w-px overflow-hidden bg-white/15" aria-hidden>
<span className="scroll-cue absolute inset-x-0 top-0 h-1/2 bg-primary" />
</span>
</a>
</div>
</div>
{/* Target for the scroll cue: the first section after the hero. */}
<span id="main-content" className="absolute bottom-0" aria-hidden />
</section>
);
}
// ---------------------------------------------------------------------------------------------
// Hero visuals (decorative)
// ---------------------------------------------------------------------------------------------
/** Place n items evenly on a circle of the given radius (% of the box), starting at 12 o'clock. */
function ring(i: number, n: number, radius: number): CSSProperties {
const a = ((-90 + (i * 360) / n) * Math.PI) / 180;
return {
left: `${(50 + radius * Math.cos(a)).toFixed(2)}%`,
top: `${(50 + radius * Math.sin(a)).toFixed(2)}%`,
};
}
/** Studio: the shared stack orbits the studio, the ventures circle closer in. */
function StudioVisual() {
return (
<div className="grid h-full place-items-center">
<div className="relative aspect-square w-[min(40rem,92%)]">
<div className="absolute inset-[3%] rounded-full border border-white/10" />
<div className="absolute inset-[23%] rounded-full border border-dashed border-primary/30" />
<div className="glow absolute inset-[20%]" />
<div className="spin-slow absolute inset-0">
{STACK.map((s, i) => (
<span
key={s.label}
style={ring(i, STACK.length, 47)}
className="absolute -translate-x-1/2 -translate-y-1/2"
>
<span className="upright block whitespace-nowrap rounded-full border border-white/10 bg-ink/80 px-3 py-1.5 font-display text-[11px] font-semibold uppercase tracking-wider text-muted-foreground backdrop-blur">
{s.label}
</span>
</span>
))}
</div>
<div className="spin-slower absolute inset-0">
{VERTICALS.map((v, i) => (
<span
key={v.n}
style={ring(i, VERTICALS.length, 27)}
className="absolute -translate-x-1/2 -translate-y-1/2"
>
<span className="upright flex items-center gap-2 whitespace-nowrap font-display text-[11px] font-bold uppercase tracking-wider text-primary">
<span className="h-2 w-2 rounded-full bg-primary shadow-[0_0_12px] shadow-primary" />
{v.short}
</span>
</span>
))}
</div>
<img
src={logoMark}
alt=""
width={201}
height={192}
className="absolute left-1/2 top-1/2 w-[18%] -translate-x-1/2 -translate-y-1/2 drop-shadow-[0_20px_40px_oklch(0.05_0.01_60/0.8)]"
/>
</div>
</div>
);
}
/** Framework: the six stages as a loop with a comet running round it. */
function FrameworkVisual() {
return (
<div className="grid h-full place-items-center">
<div className="relative aspect-square w-[min(38rem,90%)]">
<div className="absolute inset-[12.5%] rounded-full border border-white/10" />
<div
className="orbit-comet absolute inset-[12.5%] rounded-full"
style={{
background: "conic-gradient(from 0deg, transparent 0deg 250deg, var(--primary) 360deg)",
mask: "radial-gradient(farthest-side, transparent calc(100% - 3px), black calc(100% - 2px))",
}}
/>
<div className="glow absolute inset-[25%]" />
{FRAMEWORK.map((f, i) => {
const Icon = f.icon;
return (
<div
key={f.n}
style={orbitPosition(i, FRAMEWORK.length)}
className="absolute flex -translate-x-1/2 -translate-y-1/2 flex-col items-center gap-2"
>
<span className="grid h-14 w-14 place-items-center rounded-2xl border border-primary/30 bg-ink/90 text-primary shadow-lg shadow-black/40 backdrop-blur">
<Icon className="h-5 w-5" />
</span>
<span className="whitespace-nowrap font-display text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
<span className="text-primary">{f.n}</span> {f.title}
</span>
</div>
);
})}
<div className="absolute inset-0 grid place-items-center text-center">
<div>
<div className="font-display text-7xl font-extrabold tracking-tighter text-foreground md:text-8xl">
06
</div>
<div className="mt-1 font-display text-xs font-semibold uppercase tracking-[0.2em] text-primary">
Stages, one loop
</div>
</div>
</div>
</div>
</div>
);
}
/** Portfolio: columns of vertical photos drifting past each other. */
function PortfolioVisual() {
const columns = [
{ items: VERTICALS, cls: "column-up" },
{ items: [...VERTICALS.slice(3), ...VERTICALS.slice(0, 3)], cls: "column-down" },
{ items: [...VERTICALS.slice(5), ...VERTICALS.slice(0, 5)], cls: "column-up" },
];
return (
<div className="h-full overflow-hidden">
<div className="grid h-[140%] -translate-y-[12%] rotate-[-8deg] scale-110 grid-cols-2 gap-4 px-4 sm:grid-cols-3">
{columns.map((col, c) => (
<div key={c} className={`overflow-hidden ${c === 2 ? "hidden sm:block" : ""}`}>
<div className={col.cls}>
{[...col.items, ...col.items].map((v, i) => (
<figure
key={i}
className="relative mb-4 aspect-[4/5] overflow-hidden rounded-2xl border border-white/10"
>
<img
src={v.img}
alt=""
decoding="async"
className="absolute inset-0 h-full w-full object-cover"
/>
<div className="absolute inset-x-0 bottom-0 h-1/3 bg-gradient-to-t from-ink/80 to-transparent" />
<figcaption className="absolute bottom-3 left-3 rounded-full bg-ink/70 px-3 py-1 font-display text-[11px] font-semibold uppercase tracking-wider text-primary backdrop-blur">
{v.short}
</figcaption>
</figure>
))}
</div>
</div>
))}
</div>
</div>
);
}
// Network layout for the AI visual, in a 400×400 viewBox.
const AI_LAYERS = [3, 5, 5, 3].map((count, l, all) =>
Array.from({ length: count }, (_, i) => ({
x: 50 + (l * 300) / (all.length - 1),
y: 200 + (i - (count - 1) / 2) * 62,
})),
);
const AI_EDGES = AI_LAYERS.slice(0, -1).flatMap((layer, l) =>
layer.flatMap((a) => AI_LAYERS[l + 1].map((b) => ({ a, b }))),
);
/** AI: a small neural network with signals flowing through it. */
function AIVisual() {
return (
<div className="grid h-full place-items-center">
<svg viewBox="0 0 400 400" className="w-[min(40rem,94%)] overflow-visible">
{AI_EDGES.map(({ a, b }, i) => (
<line
key={`e${i}`}
x1={a.x}
y1={a.y}
x2={b.x}
y2={b.y}
stroke="white"
strokeOpacity={0.07}
/>
))}
{AI_EDGES.map(({ a, b }, i) => (
<line
key={`s${i}`}
x1={a.x}
y1={a.y}
x2={b.x}
y2={b.y}
className="signal"
stroke="var(--primary)"
strokeWidth={1.5}
strokeLinecap="round"
style={{ "--d": `${-((i * 37) % 29) / 9}s` } as CSSProperties}
/>
))}
{AI_LAYERS.flat().map((n, i) => (
<g key={`n${i}`}>
<circle cx={n.x} cy={n.y} r={14} fill="var(--primary)" opacity={0.08} />
<circle
cx={n.x}
cy={n.y}
r={4}
fill="var(--primary)"
opacity={0.55}
className="node-pulse"
style={{ "--d": `${-((i * 13) % 11) / 4}s` } as CSSProperties}
/>
</g>
))}
</svg>
</div>
);
}
/** Investors: a field of companies, lighting up in a wave across the ecosystem. */
function InvestorsVisual() {
const cols = 11;
const rows = 11;
return (
<div className="grid h-full place-items-center">
<div
className="grid w-[min(34rem,88%)] gap-[3.2%]"
style={{ gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` }}
>
{Array.from({ length: cols * rows }, (_, i) => {
const r = Math.floor(i / cols);
const c = i % cols;
const hub = r === 5 && c === 5;
return (
<span
key={i}
className={`dot-wave aspect-square rounded-full ${
hub ? "bg-foreground" : (r * 7 + c * 3) % 5 === 0 ? "bg-primary" : "bg-primary/60"
}`}
style={{ "--d": `${((r + c) * 0.14).toFixed(2)}s` } as CSSProperties}
/>
);
})}
</div>
</div>
);
}
function AboutVisual() {
return (
<div className="grid h-full place-items-center">
<div className="relative">
<div className="glow absolute -inset-16" />
<div className="spin-slower absolute -inset-10 rounded-full border border-dashed border-primary/25" />
<MoltenMark src={logoMark} className="aspect-square w-[min(28rem,70vw)]" />
</div>
</div>
);
}
const IST = new Intl.DateTimeFormat("en-GB", {
timeZone: "Asia/Kolkata",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
});
/** Contact: the studio's local time inside a rotating ring of text. */
function ContactVisual() {
const [time, setTime] = useState<string | null>(null);
useEffect(() => {
const tick = () => setTime(IST.format(new Date()));
tick();
const id = window.setInterval(tick, 1000);
return () => window.clearInterval(id);
}, []);
return (
<div className="grid h-full place-items-center">
<div className="relative aspect-square w-[min(34rem,86%)]">
<svg viewBox="0 0 400 400" className="spin-slow absolute inset-0 overflow-visible">
<defs>
<path
id="contact-ring"
d="M200,200 m-170,0 a170,170 0 1,1 340,0 a170,170 0 1,1 -340,0"
/>
</defs>
<text
className="font-display"
fill="var(--primary)"
fontSize="17"
fontWeight="700"
letterSpacing="5"
>
<textPath href="#contact-ring" textLength="1060">
BUILD WITH US · INVEST WITH US · PARTNER WITH US ·
</textPath>
</text>
</svg>
<div className="absolute inset-[16%] rounded-full border border-white/10" />
<div className="glow absolute inset-[10%]" />
<div className="absolute inset-0 grid place-items-center text-center">
<div>
<div className="font-display text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground">
Bangalore, IST
</div>
<div className="mt-3 font-display text-5xl font-extrabold tabular-nums tracking-tight md:text-7xl">
{time ?? "--:--:--"}
</div>
<div className="mt-3 inline-flex items-center gap-2 text-sm text-primary">
<span className="h-2 w-2 animate-pulse rounded-full bg-primary" />
Studio open to new partners
</div>
</div>
</div>
</div>
</div>
);
}
// ---------------------------------------------------------------------------------------------
// Heroes per page
// ---------------------------------------------------------------------------------------------
export function StudioHero() {
return (
<PageHero
page="studio"
label="The studio"
title={["One studio.", "Many companies.", { accent: "One engine." }]}
intro="We don't fund companies after they form. We find the problem, build the company and scale it on infrastructure every venture shares."
visual={<StudioVisual />}
/>
);
}
export function FrameworkHero() {
return (
<PageHero
page="framework"
label="The framework"
title={["Company building,", { accent: "made repeatable." }]}
intro="Every venture moves through the same six stages, so each new company starts with the lessons of the last."
visual={<FrameworkVisual />}
/>
);
}
export function PortfolioHero() {
return (
<PageHero
page="portfolio"
label="The portfolio"
title={["Big problems.", { accent: "Bold companies." }]}
intro="We build where problems are large, fragmented and overdue for change: seven industries, one AI engine underneath them all."
visual={<PortfolioVisual />}
/>
);
}
export function AIHero() {
return (
<PageHero
page="ai"
label="Artificial intelligence"
title={["Intelligence,", { accent: "built in from day one." }]}
intro="AI isn't bolted on later. It shapes how every KapFoundry company is designed, operated and scaled."
visual={<AIVisual />}
/>
);
}
export function InvestorsHero() {
return (
<PageHero
page="investors"
label="For investors"
title={["One investment.", { accent: "Many companies." }]}
intro="Exposure to a portfolio of AI-first ventures across industries: risk spread wide, upside shared across every company the studio builds."
visual={<InvestorsVisual />}
>
<a
href="/contact"
data-magnetic
className="press group inline-flex items-center gap-2 rounded-full bg-primary px-7 py-3.5 font-display text-sm font-semibold text-primary-foreground hover:brightness-110"
>
Speak with our team
<ArrowRight className="h-4 w-4 transition group-hover:translate-x-0.5" aria-hidden />
</a>
</PageHero>
);
}
export function AboutHero() {
return (
<PageHero
page="about"
label="About KapFoundry"
title={["We don't wait for the future.", { accent: "We build it." }]}
intro="A venture studio and technology holding company that engineers category-defining, AI-first companies, before they even exist."
visual={<AboutVisual />}
/>
);
}
export function ContactHero() {
return (
<PageHero
page="contact"
label="Contact"
title={["Let's build", { accent: "what comes next." }]}
intro="Whether you want to invest, partner or build with us, tell us a little about yourself and our team will be in touch."
visual={<ContactVisual />}
>
<a
href={`mailto:${CONTACT_EMAIL}`}
data-cursor="Write"
className="group inline-flex items-center gap-3 border-b border-primary/40 pb-2 font-display text-2xl font-bold transition-colors hover:border-primary hover:text-primary md:text-3xl"
>
{CONTACT_EMAIL}
<ArrowUpRight
className="h-6 w-6 text-primary transition-transform duration-300 group-hover:-translate-y-1 group-hover:translate-x-1"
aria-hidden
/>
</a>
</PageHero>
);
}
// ---------------------------------------------------------------------------------------------
// New content sections
// ---------------------------------------------------------------------------------------------
const VC_ROWS = [
{
topic: "When capital arrives",
vc: "After a company already exists",
kf: "Before the company exists, once research proves the problem",
},
{
topic: "Who builds",
vc: "Founders alone, starting from scratch",
kf: "Founders with a studio team behind them from day one",
},
{
topic: "Infrastructure",
vc: "Assembled again, company by company",
kf: "Engineering, design, legal, finance and growth, built once and shared",
},
{
topic: "Technology",
vc: "AI added later, if at all",
kf: "AI-first architecture from the first line of code",
},
{
topic: "Risk",
vc: "Concentrated in a single bet",
kf: "Spread across a portfolio of ventures",
},
];
/** Studio: how the studio model differs from traditional venture capital, row by row. */
export function StudioVsVC() {
return (
<section className="sheet-enter theme-light section-y bg-background text-foreground">
<div className="container-x">
<SectionHeader
eyebrow="A different model"
icon={Compass}
title={
<>
Venture capital waits. <Accent>We build.</Accent>
</>
}
>
<p>
Traditional venture capital invests in startups after they exist. KapFoundry creates
them, which changes almost everything about how a company begins.
</p>
</SectionHeader>
<div className="mt-16">
<div className="hidden grid-cols-12 gap-6 pb-4 font-display text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground lg:grid">
<span className="col-span-3" />
<span className="col-span-4">Traditional venture capital</span>
<span className="col-span-5 text-primary">KapFoundry venture studio</span>
</div>
<ol>
{VC_ROWS.map((r, i) => (
<li
key={r.topic}
{...reveal("up", i * 40)}
className="group relative grid gap-3 py-7 lg:grid-cols-12 lg:gap-6 lg:py-9"
>
<span aria-hidden className="absolute inset-x-0 top-0 h-px bg-border" />
<span
aria-hidden
className="line-draw absolute inset-x-0 top-0 h-px bg-primary/60"
/>
<h3 className="font-display text-xl font-bold lg:col-span-3 lg:text-2xl">
<span className="mr-3 text-sm tabular-nums text-primary">0{i + 1}</span>
{r.topic}
</h3>
<p className="flex gap-3 text-muted-foreground lg:col-span-4">
<Minus className="mt-1 h-4 w-4 shrink-0 opacity-60" aria-hidden />
<span>
<span className="sr-only">Traditional venture capital: </span>
{r.vc}
</span>
</p>
<p
className={`flex gap-3 font-display text-lg font-semibold transition-transform duration-500 group-hover:translate-x-1 lg:col-span-5 lg:text-xl ${EASE_STAGE}`}
>
<Check className="mt-1 h-5 w-5 shrink-0 text-primary" aria-hidden />
<span>
<span className="sr-only">KapFoundry: </span>
{r.kf}
</span>
</p>
</li>
))}
</ol>
</div>
</div>
</section>
);
}
const PRINCIPLES = [
{
t: "Evidence before capital",
img: principleEvidence,
d: "Every venture begins with market research, customer discovery and product validation, before a single rupee is deployed.",
},
{
t: "One engine, many companies",
img: principleEngine,
d: "Engineering, design, legal, finance and growth are built once and shared, so each venture launches with a mature company's stack.",
},
{
t: "Built to stand alone",
img: principleStandalone,
d: "Each venture is designed to spin off into an independent, high-growth company that strengthens the wider ecosystem.",
},
];
/** Framework: the statement and principles behind the six stages. */
export function FrameworkPrinciples() {
return (
<section className="section-y relative overflow-hidden bg-ink">
<div className="glow glow-drift absolute -left-48 top-10 h-[36rem] w-[36rem] opacity-40" />
<div className="container-x relative">
<p className="max-w-5xl font-display text-4xl font-bold leading-[1.06] tracking-tight md:text-6xl">
<FillWords text="Great ideas fail on avoidable mistakes." />
<FillWords
text="A repeatable system catches them before capital is spent."
className="text-primary"
/>
</p>
<ol className="mt-20 grid gap-6 md:grid-cols-3" {...reveal("stagger")}>
{PRINCIPLES.map((p, i) => (
<li
key={p.t}
tabIndex={0}
className="group relative overflow-hidden rounded-3xl border border-border bg-card/60 transition-colors duration-500 hover:border-primary/50"
>
<div className="relative h-56 overflow-hidden">
<DuoPhoto src={p.img} />
<div className="absolute inset-0 bg-gradient-to-t from-card via-card/20 to-transparent" />
<span className="absolute bottom-3 left-8 font-display text-6xl font-extrabold tabular-nums text-transparent transition-colors duration-500 [-webkit-text-stroke:1px_color-mix(in_oklab,var(--primary)_80%,transparent)] group-hover:text-primary">
0{i + 1}
</span>
</div>
<div className="p-8 pt-5">
<h3 className="font-display text-2xl font-bold">{p.t}</h3>
<p className="mt-3 leading-relaxed text-muted-foreground">{p.d}</p>
</div>
</li>
))}
</ol>
</div>
</section>
);
}
/** Ventures in the studio, from each vertical's example; each gets a page at /ventures/<slug>. */
export const VENTURES = VERTICALS.filter((v) => v.example).map((v) => {
const [name, ...rest] = (v.example ?? "").split(". ");
return {
name,
slug: name
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)/g, ""),
pitch: rest.join(". "),
vertical: v.name,
tag: v.tag,
problem: v.problem,
solution: v.solution,
img: v.img,
icon: v.icon,
};
});
/** Portfolio: ventures already taking shape in the studio. */
export function Ventures() {
return (
<section className="sheet-enter theme-light section-y bg-background text-foreground">
<div className="container-x">
<SectionHeader
eyebrow="In the studio"
icon={Sparkles}
title={
<>
Ventures <Accent>taking shape.</Accent>
</>
}
>
<p>A first look at companies being engineered on the KapFoundry engine.</p>
</SectionHeader>
<div className="mt-16 grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{VENTURES.map((v, i) => {
const Icon = v.icon;
return (
<article
key={v.name}
{...reveal("clip", i * 120)}
className="theme-dark group relative flex min-h-[30rem] flex-col justify-end overflow-hidden rounded-[2rem] bg-background p-8 text-foreground md:min-h-[36rem] md:p-10 md:[&:last-child:nth-child(odd)]:col-span-2 lg:[&:last-child:nth-child(odd)]:col-span-1"
>
<img
src={v.img}
alt=""
loading="lazy"
decoding="async"
className={`absolute inset-0 h-full w-full object-cover transition-transform duration-[1.6s] group-hover:scale-105 ${EASE_STAGE}`}
/>
<div className="absolute inset-0 bg-gradient-to-t from-ink via-ink/70 to-ink/10" />
<div className="relative">
<span className="inline-flex items-center gap-2 rounded-full bg-primary px-3 py-1.5 font-display text-[11px] font-semibold uppercase tracking-wider text-primary-foreground">
<Icon className="h-3.5 w-3.5" aria-hidden />
{v.vertical}
</span>
<h3 className="mt-6 font-display text-[2rem] font-extrabold tracking-tight min-[360px]:text-4xl xl:text-5xl">
{v.name}
</h3>
<p className="mt-4 max-w-[40ch] text-lg leading-relaxed text-muted-foreground">
{v.pitch}
</p>
{/* Visible label for the card-wide link below. */}
<span
aria-hidden
className="mt-8 inline-flex items-center gap-3 rounded-full border border-white/20 bg-white/10 py-2 pl-6 pr-2 font-display text-sm font-semibold text-foreground backdrop-blur-md transition-colors duration-500 group-hover:border-primary group-hover:bg-primary group-hover:text-primary-foreground"
>
Explore venture
<span className="grid h-9 w-9 place-items-center rounded-full bg-primary text-primary-foreground transition-[background-color,color,rotate] duration-500 group-hover:rotate-45 group-hover:bg-primary-foreground group-hover:text-primary">
<ArrowUpRight className="h-4 w-4" aria-hidden />
</span>
</span>
</div>
{/* One link over the whole card: click anywhere, one tab stop. */}
<a
href={`/ventures/${v.slug}`}
data-cursor="Explore"
aria-label={`Explore ${v.name}`}
className="absolute inset-0 z-10 rounded-[2rem]"
/>
</article>
);
})}
</div>
<a
href="/contact"
data-cursor="Talk"
{...reveal("up", 150)}
className="group mt-6 flex flex-col items-start justify-between gap-6 rounded-[2rem] border border-dashed border-foreground/25 p-8 transition-colors duration-500 hover:border-primary hover:bg-primary/5 md:flex-row md:items-center md:p-10"
>
<span>
<span className="block font-display text-xs font-semibold uppercase tracking-[0.16em] text-primary">
Next in the studio
</span>
<span className="mt-3 block font-display text-3xl font-bold tracking-tight md:text-4xl">
Build the next one with us.
</span>
</span>
<span className="grid h-16 w-16 shrink-0 place-items-center rounded-full bg-primary text-primary-foreground transition-transform duration-500 group-hover:rotate-45">
<ArrowUpRight className="h-6 w-6" aria-hidden />
</span>
</a>
</div>
</section>
);
}
/** AI: what the foundation does inside each vertical; the row at the centre of the screen is lit. */
export function AIAcrossVerticals() {
return (
<section className="section-y relative bg-ink">
<div className="container-x grid gap-12 lg:grid-cols-12 lg:gap-16">
<div className="lg:col-span-5">
<div className="lg:sticky lg:top-32" {...reveal("stagger")}>
<Eyebrow icon={Layers}>AI in every vertical</Eyebrow>
<h2 className="mt-6 font-display text-4xl font-bold leading-[1.04] md:text-5xl">
One foundation. <Accent>Seven industries.</Accent>
</h2>
<p className="mt-6 max-w-[48ch] text-lg leading-relaxed text-muted-foreground">
The same AI capabilities show up differently in every market we enter. Here is where
they do the heavy lifting.
</p>
</div>
</div>
<ol className="lg:col-span-7">
{VERTICALS.map((v) => {
const Icon = v.icon;
return (
<li
key={v.n}
className="row-focus border-t border-white/10 py-8 last:border-b md:py-10"
>
<div className="flex items-center gap-4">
<span className="relative h-14 w-14 shrink-0 overflow-hidden rounded-2xl md:h-16 md:w-16">
<img
src={v.img}
alt=""
loading="lazy"
decoding="async"
className="h-full w-full object-cover"
/>
<span className="absolute bottom-1 right-1 grid h-6 w-6 place-items-center rounded-lg bg-ink/80 text-primary backdrop-blur">
<Icon className="h-3.5 w-3.5" aria-hidden />
</span>
</span>
<h3 className="font-display text-2xl font-bold md:text-4xl">{v.name}</h3>
<span className="ml-auto font-display text-sm tabular-nums text-primary">
{v.n}
</span>
</div>
<ul className="mt-5 flex flex-wrap gap-2 md:pl-20">
{v.solution.slice(0, 3).map((s) => (
<li
key={s}
className="rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-sm text-foreground"
>
{s}
</li>
))}
</ul>
</li>
);
})}
</ol>
</div>
</section>
);
}
const MODEL = [
{
t: "The studio builds",
img: modelBuild,
d: "Ventures are researched, validated and built on KapFoundry's shared engine.",
},
{
t: "Ventures spin off",
img: modelSpinoff,
d: "Each company grows into an independent, high-growth business in its own market.",
},
{
t: "Value compounds",
img: modelCompound,
d: "Every success strengthens the ecosystem, and investors share in the portfolio, not a single bet.",
},
];
/** Investors: how value flows from the studio to the portfolio. */
export function InvestorModel() {
return (
<section className="sheet-enter theme-light section-y bg-background text-foreground">
<div className="container-x">
<SectionHeader
title={
<>
How value <Accent>compounds.</Accent>
</>
}
>
<p>A holding company at the centre, many independent companies around it.</p>
</SectionHeader>
<div className="relative mt-20">
<ol className="grid gap-12 md:grid-cols-3 md:gap-10" {...reveal("stagger")}>
{MODEL.map((m, i) => (
<li key={m.t} tabIndex={0} className="group relative">
<div className="theme-dark relative aspect-[4/3] overflow-hidden rounded-3xl bg-ink">
<DuoPhoto src={m.img} />
</div>
<span className="relative -mt-7 ml-6 grid h-14 w-14 place-items-center rounded-full bg-primary font-display text-lg font-bold tabular-nums text-primary-foreground shadow-lg shadow-primary/30 ring-4 ring-background">
0{i + 1}
</span>
<h3 className="mt-8 font-display text-3xl font-bold">{m.t}</h3>
<p className="mt-3 max-w-[36ch] text-lg leading-relaxed text-muted-foreground">
{m.d}
</p>
</li>
))}
</ol>
</div>
</div>
</section>
);
}
// ---------------------------------------------------------------------------------------------
// Next page handoff
// ---------------------------------------------------------------------------------------------
const NEXT_BLURB: Record<string, string> = {
"/studio": "How one shared engine builds many companies.",
"/framework": "Six stages from a billion-dollar problem to an independent company.",
"/portfolio": "The industries we're rebuilding, AI-first.",
"/ai": "The foundation under every venture we build.",
"/investors": "Exposure to a portfolio, not a single bet.",
"/about": "Our vision, our mission and the people behind them.",
"/contact": "Build with us, invest with us, or simply say hello.",
};
/** Giant link to the next page in nav order; grows in as it enters and fills gold on hover. */
export function NextPage({ page }: { page: SitePageName }) {
const i = NAV.findIndex((n) => n.href === `/${page}`);
const k = (i + 1) % NAV.length;
const next = NAV[k];
return (
<section
aria-label="Next page"
className="relative overflow-hidden border-t border-border bg-ink"
>
<div className="glow glow-drift absolute -right-40 top-0 h-[34rem] w-[34rem] opacity-30" />
<a href={next.href} data-cursor="Next" className="group relative block py-24 md:py-36">
<div className="container-x">
<div className="flex items-center justify-between font-display text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground">
<span data-scramble>Next page</span>
<span className="tabular-nums">
<span className="text-primary">{String(k + 1).padStart(2, "0")}</span> /{" "}
{String(NAV.length).padStart(2, "0")}
</span>
</div>
<div className="next-grow mt-8 flex flex-col items-start gap-6 md:flex-row md:items-center md:justify-between">
<span className="next-fill font-display text-[clamp(2.5rem,15vw,12rem)] md:text-[min(12vw,12rem)] font-extrabold leading-[0.9] tracking-tighter">
{next.label}
</span>
<span
className={`grid h-16 w-16 shrink-0 place-items-center rounded-full border border-primary/40 text-primary transition-all duration-700 group-hover:bg-primary group-hover:text-primary-foreground md:h-28 md:w-28 ${EASE_STAGE}`}
>
<ArrowUpRight
className={`h-7 w-7 transition-transform duration-700 group-hover:rotate-45 md:h-10 md:w-10 ${EASE_STAGE}`}
aria-hidden
/>
</span>
</div>
<div className="mt-10 h-px bg-white/10">
<div className="line-draw h-full bg-primary/70" />
</div>
<p className="mt-6 max-w-md text-lg text-muted-foreground">{NEXT_BLURB[next.href]}</p>
</div>
</a>
</section>
);
}
/** Portfolio: the seven industries as a typographic index with floating photos. */
export function PortfolioIndex() {
return (
<IndustryIndex
items={VERTICALS.map((v) => ({ n: v.n, name: v.short, tag: v.tag, img: v.img }))}
/>
);
}
// ---------------------------------------------------------------------------------------------
// Venture detail page (/ventures/$slug)
// ---------------------------------------------------------------------------------------------
/** Ventures with a full story of their own; the rest get the generic vertical sections. */
const VENTURE_STORIES: Record<string, () => ReactNode> = {
cropsphere: CropsphereStory,
"doormile-wings": DoorMileStory,
"loyaly-ai": LoyalyStory,
};
/** Looks a venture up by its URL slug. */
export function findVenture(slug: string) {
return VENTURES.find((v) => v.slug === slug);
}
/**
* Everything on this page comes from what the site already says about the venture and its
* vertical; nothing about stage, traction or team is implied.
*/
export function VentureDetail({ slug }: { slug: string }) {
const v = findVenture(slug);
if (!v) return null;
const i = VENTURES.indexOf(v);
const next = VENTURES[(i + 1) % VENTURES.length];
const Icon = v.icon;
const Story = VENTURE_STORIES[v.slug];
return (
<>
{/* Hero: the venture's own photo, full bleed */}
<section
id="top"
className="hero-timeline relative flex min-h-[100svh] flex-col overflow-hidden bg-ink"
>
<div className="hero-media absolute inset-0" aria-hidden>
{/* Pulls back into a rounded card as the hero scrolls away (.venture-photo). */}
<div className="venture-photo absolute inset-0 overflow-hidden">
<img
src={v.img}
alt=""
fetchPriority="high"
className="hero-zoom absolute inset-0 h-full w-full object-cover opacity-70"
/>
<div className="absolute inset-0 bg-gradient-to-r from-ink via-ink/75 to-ink/20" />
<div className="absolute inset-x-0 bottom-0 h-1/2 bg-gradient-to-t from-ink to-transparent" />
</div>
</div>
<div className="hero-copy container-x relative flex flex-1 flex-col justify-between gap-12 pb-12 pt-28 md:pt-36">
<nav
aria-label="Breadcrumb"
{...rise(100)}
className="rise-in flex flex-wrap items-center gap-3 font-display text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground"
>
<a href="/portfolio" className="transition-colors hover:text-primary">
Portfolio
</a>
<span className="h-px w-8 bg-primary/50" aria-hidden />
<span className="text-primary">{v.vertical}</span>
</nav>
<div>
<span
{...rise(250)}
className="rise-in inline-flex items-center gap-2 rounded-full bg-primary px-3.5 py-1.5 font-display text-[11px] font-semibold uppercase tracking-wider text-primary-foreground"
>
<Icon className="h-3.5 w-3.5" aria-hidden />
In the studio
</span>
<h1 className="venture-name mt-6 max-w-[14ch] font-display text-[clamp(2.75rem,11vw,4rem)] font-extrabold leading-[0.92] tracking-tighter md:text-[clamp(4rem,9vw,8.5rem)]">
<SplitTitle parts={[{ accent: v.name }]} />
</h1>
<p
{...rise(700)}
className="rise-in mt-8 max-w-[46ch] text-xl leading-relaxed text-white/80 md:text-2xl"
>
{v.pitch}
</p>
<div {...rise(850)} className="rise-in mt-10 flex flex-wrap gap-4">
<a
href="/contact"
data-magnetic
className="press group inline-flex items-center gap-2 rounded-full bg-primary px-7 py-3.5 font-display text-sm font-semibold text-primary-foreground hover:brightness-110"
>
Partner with us
<ArrowRight
className="h-4 w-4 transition group-hover:translate-x-0.5"
aria-hidden
/>
</a>
<a
href="/portfolio"
className="press inline-flex items-center gap-2 rounded-full border border-white/15 bg-white/5 px-7 py-3.5 font-display text-sm font-semibold text-foreground backdrop-blur-md hover:border-white/30 hover:bg-white/10"
>
Back to portfolio
</a>
</div>
</div>
</div>
</section>
{Story ? (
<Story />
) : (
<>
{/* The problem: the market this venture is built for */}
<section className="sheet-enter theme-light section-y bg-background text-foreground">
<div className="container-x grid gap-12 lg:grid-cols-12 lg:gap-16">
<div className="lg:col-span-5">
<div className="lg:sticky lg:top-32" {...reveal("stagger")}>
<span className="eyebrow">The problem</span>
<h2 className="mt-5 font-display text-4xl font-bold leading-[1.04] md:text-5xl">
{v.vertical}: <Accent>{v.tag.toLowerCase()}.</Accent>
</h2>
<p className="mt-6 max-w-[48ch] text-lg leading-relaxed text-muted-foreground">
{v.name} is being built for a market held back by the same problems, again and
again.
</p>
</div>
</div>
<ol className="lg:col-span-7">
{v.problem.map((p, k) => (
<li
key={p}
{...reveal("up", k * 40)}
className="relative flex items-baseline gap-6 py-6 md:py-7"
>
<span aria-hidden className="absolute inset-x-0 top-0 h-px bg-border" />
<span
aria-hidden
className="line-draw absolute inset-x-0 top-0 h-px bg-primary/60"
/>
<span className="w-10 shrink-0 font-display text-sm font-semibold tabular-nums text-primary">
0{k + 1}
</span>
<span className="font-display text-2xl font-bold tracking-tight md:text-3xl">
{p}
</span>
</li>
))}
</ol>
</div>
</section>
{/* What we build */}
<section className="section-y relative overflow-hidden bg-ink">
<div className="glow glow-drift absolute -right-48 top-0 h-[36rem] w-[36rem] opacity-40" />
<div className="container-x relative">
<SectionHeader
title={
<>
What we build, <Accent>AI-first.</Accent>
</>
}
>
<p>
Where KapFoundry focuses in {v.vertical.toLowerCase()}, and the ground {v.name} is
built on.
</p>
</SectionHeader>
<ul className="mt-14 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{v.solution.map((s) => (
<li
key={s}
className="tilt-in group flex items-start gap-4 rounded-3xl border border-border bg-card/70 p-6 transition-colors duration-500 hover:border-primary/50 md:p-7"
>
<span className="grid h-10 w-10 shrink-0 place-items-center rounded-xl bg-primary/15 text-primary transition-colors duration-500 group-hover:bg-primary group-hover:text-primary-foreground">
<Check className="h-4 w-4" aria-hidden />
</span>
<span className="pt-1.5 font-display text-lg font-semibold leading-snug">
{s}
</span>
</li>
))}
</ul>
</div>
</section>
</>
)}
{/* Built on the engine: the same framework and stack as every venture */}
<section className="sheet-enter theme-light section-y bg-background text-foreground">
<div className="container-x">
<SectionHeader
title={
<>
Built on the <Accent>KapFoundry engine.</Accent>
</>
}
>
<p>
Like every venture in the studio, {v.name} moves through the same six stages and
launches with a mature company's stack.
</p>
</SectionHeader>
<ol
className="mt-14 grid grid-cols-1 gap-3 min-[400px]:grid-cols-2 md:grid-cols-3 lg:grid-cols-6"
{...reveal("stagger")}
>
{FRAMEWORK.map((f) => {
const StageIcon = f.icon;
return (
<li key={f.n} className="rounded-3xl border border-border bg-card p-5">
<div className="flex items-center justify-between">
<span className="font-display text-sm font-semibold tabular-nums text-primary">
{f.n}
</span>
<StageIcon className="h-4 w-4 text-primary" aria-hidden />
</div>
<div className="mt-6 font-display text-xl font-bold">{f.title}</div>
<p className="mt-2 text-sm leading-snug text-muted-foreground">{f.body}</p>
</li>
);
})}
</ol>
<div className="mt-10 flex flex-wrap gap-2" {...reveal("up", 150)}>
{STACK.map((s) => (
<span
key={s.label}
className="rounded-full border border-border px-4 py-2 text-sm text-muted-foreground"
>
{s.label}
</span>
))}
</div>
</div>
</section>
{/* Call to action */}
<section className="section-y relative overflow-hidden bg-ink">
<div className="glow glow-drift absolute -left-40 top-0 h-[34rem] w-[34rem] opacity-40" />
<div className="container-x relative grid items-end gap-10 md:grid-cols-12">
<h2
{...reveal("mask")}
className="font-display text-4xl font-bold leading-[1.02] md:col-span-8 md:text-6xl"
>
Interested in {v.name}? <Accent>Let's talk.</Accent>
</h2>
<div className="md:col-span-4 md:justify-self-end" {...reveal("up", 150)}>
<p className="text-lg leading-relaxed text-muted-foreground">
Partners, investors and operators: we'd like to hear from you.
</p>
<a
href="/contact"
data-magnetic
className="press group mt-6 inline-flex items-center gap-2 rounded-full bg-primary px-7 py-3.5 font-display text-sm font-semibold text-primary-foreground hover:brightness-110"
>
Speak with our team
<ArrowRight className="h-4 w-4 transition group-hover:translate-x-0.5" aria-hidden />
</a>
</div>
</div>
</section>
{/* Next venture */}
{next !== v ? (
<section
aria-label="Next venture"
className="relative overflow-hidden border-t border-border bg-ink"
>
<a
href={`/ventures/${next.slug}`}
data-cursor="Next"
className="group relative block py-24 md:py-32"
>
<div className="container-x">
<div className="flex items-center justify-between font-display text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground">
<span data-scramble>Next venture</span>
<span>{next.vertical}</span>
</div>
<div className="next-grow mt-8 flex flex-col items-start gap-6 md:flex-row md:items-center md:justify-between">
<span className="next-fill font-display text-[clamp(2.5rem,13vw,10rem)] font-extrabold leading-[0.9] tracking-tighter md:text-[min(10vw,10rem)]">
{next.name}
</span>
<span
className={`grid h-16 w-16 shrink-0 place-items-center rounded-full border border-primary/40 text-primary transition-all duration-700 group-hover:bg-primary group-hover:text-primary-foreground md:h-28 md:w-28 ${EASE_STAGE}`}
>
<ArrowUpRight
className={`h-7 w-7 transition-transform duration-700 group-hover:rotate-45 md:h-10 md:w-10 ${EASE_STAGE}`}
aria-hidden
/>
</span>
</div>
<div className="mt-10 h-px bg-white/10">
<div className="line-draw h-full bg-primary/70" />
</div>
<p className="mt-6 max-w-md text-lg text-muted-foreground">{next.pitch}</p>
</div>
</a>
</section>
) : null}
</>
);
}