Add venture detail pages, CropSphere showcase, and update Dockerfile

This commit is contained in:
R-Bharathraj
2026-10-06 18:38:03 +05:30
parent 1582052c98
commit bebf78cefd
22 changed files with 2026 additions and 153 deletions

View File

@@ -3,6 +3,8 @@ import { ArrowRight, ArrowUpRight, Check, Compass, Layers, Minus, Sparkles } fro
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";
@@ -701,9 +703,23 @@ export function FrameworkPrinciples() {
);
}
const VENTURES = VERTICALS.filter((v) => v.example).map((v) => {
/** 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, pitch: rest.join(". "), vertical: v.name, img: v.img, icon: v.icon };
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. */
@@ -723,14 +739,14 @@ export function Ventures() {
<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">
<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"
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}
@@ -745,13 +761,30 @@ export function Ventures() {
<Icon className="h-3.5 w-3.5" aria-hidden />
{v.vertical}
</span>
<h3 className="mt-6 font-display text-4xl font-extrabold tracking-tight lg:text-6xl">
<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>
);
})}
@@ -963,3 +996,301 @@ export function PortfolioIndex() {
/>
);
}
// ---------------------------------------------------------------------------------------------
// 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}
</>
);
}