Add venture detail pages, CropSphere showcase, and update Dockerfile
This commit is contained in:
@@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user