Files
venture_studio-site/src/components/site/sections.tsx
2026-09-28 21:48:09 +05:30

2566 lines
95 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 {
useCallback,
useEffect,
useRef,
useState,
type CSSProperties,
type FormEvent,
type PointerEvent as ReactPointerEvent,
type ReactNode,
} from "react";
import {
ArrowDown,
ArrowRight,
ArrowUp,
ArrowUpRight,
Bot,
Brain,
Building2,
ChartLine,
Check,
Cloud,
Cpu,
Database,
Eye,
FlaskConical,
GitBranch,
Hammer,
Handshake,
HeartPulse,
Landmark,
Layers,
Leaf,
Mail,
MapPin,
Megaphone,
Network,
Palette,
Rocket,
Scale,
Search,
ShoppingBag,
Sparkles,
Sprout,
Target,
TrendingUp,
Truck,
Users,
UtensilsCrossed,
Wallet,
Workflow,
type LucideIcon,
} from "lucide-react";
import { CONTACT_EMAIL, INTERESTS, sendContactEnquiry, type ContactInput } from "@/lib/contact";
// Brand assets cut from kapfoundry-logo-1.png (emblem, wordmark, full lockup).
import logoMark from "@/assets/kapfoundry-mark.webp";
import logoWordmark from "@/assets/kapfoundry-wordmark.webp";
import logoFull from "@/assets/kapfoundry-logo-full.webp";
import heroBg from "@/assets/hero-bg.jpg";
import vLogistics from "@/assets/vertical-logistics.jpg";
import vRetail from "@/assets/vertical-retail.jpg";
import vFintech from "@/assets/vertical-fintech.jpg";
import vAgri from "@/assets/vertical-agri.jpg";
import vFoodtech from "@/assets/vertical-foodtech.jpg";
import vCarbon from "@/assets/vertical-carbon.jpg";
import vHealthtech from "@/assets/vertical-healthtech.jpg";
// Home-page sections are "/#id" so they work from any page; About is its own route.
const NAV = [
{ label: "Studio", href: "/#studio" },
{ label: "Framework", href: "/#framework" },
{ label: "Portfolio", href: "/#portfolio" },
{ label: "AI", href: "/#ai" },
{ label: "Investors", href: "/#investors" },
{ label: "About", href: "/about" },
{ label: "Contact", href: "/#contact" },
];
export type SitePageName = "home" | "about";
/** Section id a nav link points at ("" for page links). */
function sectionOf(href: string) {
return href.split("#")[1] ?? "";
}
const VERTICALS = [
{
n: "01",
name: "Logistics Technology",
short: "Logistics",
icon: Truck,
tag: "Fragmented, expensive, inefficient",
img: vLogistics,
problem: [
"Last-mile delivery",
"Fleet optimization",
"Air cargo coordination",
"Route planning",
"Warehouse visibility",
"Freight pricing and supply-chain transparency",
],
solution: [
"Route optimization",
"Real-time fleet intelligence",
"Air cargo platforms",
"Warehouse automation and delivery intelligence",
"Predictive logistics",
"Carbon-aware route planning",
],
example: "DoorMile Wings. An AI-powered domestic air cargo and express logistics network.",
},
{
n: "02",
name: "Retail Technology",
short: "Retail",
icon: ShoppingBag,
tag: "Help merchants compete",
img: vRetail,
problem: [
"Poor loyalty programs",
"Lack of customer insights",
"No digital engagement",
"Ineffective promotions",
"Limited analytics",
],
solution: [
"Customer loyalty and rewards platforms",
"AI marketing",
"Consumer and merchant analytics",
"Store performance analytics",
"Digital commerce",
],
example: "Loyaly.ai. An AI loyalty and engagement platform for independent merchants.",
},
{
n: "03",
name: "Financial Technology",
short: "Fintech",
icon: Wallet,
tag: "Millions underserved",
img: vFintech,
problem: [
"Lending access",
"Payments",
"Credit visibility",
"Collections",
"Merchant financing",
],
solution: [
"Embedded finance",
"Lending infrastructure and merchant credit",
"Payment platforms",
"Expense intelligence and financial automation",
"Risk analytics",
],
},
{
n: "04",
name: "Agriculture Technology",
short: "Agritech",
icon: Sprout,
tag: "Connect farms to markets",
img: vAgri,
problem: [
"Low productivity",
"Market inefficiencies",
"Price uncertainty",
"Limited technology adoption",
"Climate risks",
],
solution: [
"Precision farming",
"AI crop monitoring",
"Marketplace platforms",
"Supply-chain digitization",
"Yield prediction and farm analytics",
],
},
{
n: "05",
name: "FoodTech",
short: "FoodTech",
icon: UtensilsCrossed,
tag: "Powering the future of food",
img: vFoodtech,
problem: [
"Customer retention",
"Food waste",
"Inventory management",
"Delivery inefficiencies",
"Demand forecasting",
],
solution: [
"Smart POS",
"AI kitchen analytics",
"Restaurant CRM and customer rewards",
"Food-delivery optimization",
"Inventory intelligence",
],
},
{
n: "06",
name: "Carbon Technology",
short: "Carbon",
icon: Leaf,
tag: "Sustainability, made measurable",
img: vCarbon,
problem: [
"Accurate emission measurement",
"Meeting sustainability goals",
"Regulatory reporting",
"Green supply-chain visibility",
"Reliable ESG tooling",
],
solution: [
"Carbon accounting",
"ESG platforms",
"Emission monitoring",
"Carbon credit marketplaces",
"Sustainability dashboards",
"Green supply-chain intelligence",
],
},
{
n: "07",
name: "HealthTech",
short: "HealthTech",
icon: HeartPulse,
tag: "Care that reaches everyone",
img: vHealthtech,
problem: [
"Access to quality care",
"Fragmented patient records",
"Rising treatment costs",
"Clinic and hospital efficiency",
"Preventive health at scale",
],
solution: [
"Digital health platforms",
"AI diagnostics and triage",
"Clinic operations software",
"Remote care and telemedicine",
"Health data intelligence",
"Patient engagement tools",
],
},
];
const FRAMEWORK = [
{
n: "01",
title: "Discover",
icon: Search,
body: "Identify billion-dollar industry problems.",
},
{
n: "02",
title: "Validate",
icon: FlaskConical,
body: "Customer interviews, market research, prototype testing.",
},
{ n: "03", title: "Build", icon: Hammer, body: "Scalable products on AI-first architecture." },
{
n: "04",
title: "Launch",
icon: Rocket,
body: "Acquire customers and reach product-market fit.",
},
{ n: "05", title: "Scale", icon: TrendingUp, body: "Expand nationally and internationally." },
{
n: "06",
title: "Spin-Off",
icon: GitBranch,
body: "Grow into independent portfolio companies.",
},
];
const STACK: { label: string; icon: LucideIcon }[] = [
{ label: "AI and Engineering", icon: Cpu },
{ label: "Product Design", icon: Palette },
{ label: "Cloud Infrastructure", icon: Cloud },
{ label: "Legal and Compliance", icon: Scale },
{ label: "Finance", icon: Wallet },
{ label: "Marketing", icon: Megaphone },
{ label: "Sales Enablement", icon: Handshake },
{ label: "Growth Teams", icon: Users },
{ label: "Data Science", icon: Database },
{ label: "Corporate Governance", icon: Landmark },
];
const AI_CAPS: { label: string; icon: LucideIcon }[] = [
{ label: "Computer Vision", icon: Eye },
{ label: "Agentic AI", icon: Bot },
{ label: "Enterprise AI", icon: Building2 },
{ label: "Predictive Analytics", icon: ChartLine },
{ label: "LLM Applications", icon: Sparkles },
{ label: "AI Automation", icon: Workflow },
{ label: "Multi-Agent Systems", icon: Network },
{ label: "Decision Intelligence", icon: Brain },
];
const MISSION = [
"Identify large market inefficiencies.",
"Build AI-first technology companies.",
"Scale them with shared resources.",
"Create long-term enterprise value.",
"Deliver diversified returns for investors.",
"Empower entrepreneurs with world-class execution.",
];
const INVESTOR_REASONS = [
"Diversified technology portfolio",
"Reduced single-company risk",
"AI-first businesses",
"Experienced venture-building team",
"Multiple growth opportunities",
"Shared engineering and operations",
"Long-term enterprise value",
"Repeatable creation framework",
];
// Team portraits live in public/assets, in card order: 4.jpeg, 1.png, 2.png, 3.jpg.
// Add each person's name when ready (cards hide the name while it is empty).
// Every card is labelled with its role and numbered 1-4 in card order.
const TEAM = [
{
name: "",
role: "Venture Builder",
img: "/assets/4.jpeg",
},
{
name: "",
role: "Venture Builder",
img: "/assets/1.png",
},
{
name: "",
role: "Venture Builder",
img: "/assets/2.png",
},
{
name: "",
role: "Venture Builder",
img: "/assets/3.jpg",
},
];
const TICKER = [...VERTICALS.map((v) => v.short), "AI-First", "Build Bold"];
/**
* Scroll-reveal styles (see styles.css):
* - "up": fade and rise (default)
* - "mask": text wipes up into view, for headlines
* - "clip": image frame opens from an inset while the photo settles, for media
* - "stagger": the element's direct children rise one after another
*/
type RevealVariant = "up" | "mask" | "clip" | "stagger";
/** Props that opt an element into the scroll-reveal animation. */
function reveal(variant: RevealVariant = "up", delay = 0) {
return {
"data-reveal": variant,
style: { "--reveal-delay": `${delay}ms` } as CSSProperties,
};
}
const REVEAL_BATCH_STEP = 90;
const REVEAL_SETTLE_MS = 1700;
function useScrollReveal() {
useEffect(() => {
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
const timers: number[] = [];
const io = new IntersectionObserver(
(entries) => {
// Elements that arrive together (a grid row, a fast scroll) cascade in reading order.
const arriving = entries
.filter((e) => e.isIntersecting)
.sort(
(a, b) =>
a.boundingClientRect.top - b.boundingClientRect.top ||
a.boundingClientRect.left - b.boundingClientRect.left,
);
arriving.forEach((entry, i) => {
const el = entry.target as HTMLElement;
const batchDelay = Math.min(i, 5) * REVEAL_BATCH_STEP;
el.style.setProperty("--batch-delay", `${batchDelay}ms`);
el.classList.add("is-visible");
io.unobserve(el);
// Once settled, drop the reveal classes so the element's own hover transitions return.
const own = parseFloat(el.style.getPropertyValue("--reveal-delay")) || 0;
const stagger = el.dataset.reveal === "stagger" ? el.children.length * 80 : 0;
timers.push(
window.setTimeout(
() => el.classList.remove("reveal-pending", "is-visible"),
own + batchDelay + stagger + REVEAL_SETTLE_MS,
),
);
});
},
{ rootMargin: "0px 0px -10% 0px" },
);
for (const el of document.querySelectorAll<HTMLElement>("[data-reveal]")) {
// Leave anything already on screen alone so it never flickers after hydration.
if (el.getBoundingClientRect().top < window.innerHeight) continue;
if (el.dataset.reveal === "stagger") {
Array.from(el.children).forEach((child, i) =>
(child as HTMLElement).style.setProperty("--i", String(i)),
);
}
el.classList.add("reveal-pending");
io.observe(el);
}
return () => {
io.disconnect();
timers.forEach(clearTimeout);
};
}, []);
}
function useScrolledPast(offset: number) {
const [past, setPast] = useState(false);
useEffect(() => {
const on = () => setPast(window.scrollY > offset);
on();
window.addEventListener("scroll", on, { passive: true });
return () => window.removeEventListener("scroll", on);
}, [offset]);
return past;
}
/**
* True while the visitor is scrolling down past the top of the page; false as soon as they
* scroll back up. Small movements are ignored so the header does not flicker.
*/
function useHideOnScroll(topZone = 120, tolerance = 6) {
const [hidden, setHidden] = useState(false);
useEffect(() => {
let last = window.scrollY;
let frame = 0;
const update = () => {
frame = 0;
const y = window.scrollY;
const delta = y - last;
if (y < topZone) {
setHidden(false);
last = y;
} else if (Math.abs(delta) > tolerance) {
setHidden(delta > 0);
last = y;
}
};
const onScroll = () => {
if (!frame) frame = requestAnimationFrame(update);
};
window.addEventListener("scroll", onScroll, { passive: true });
return () => {
window.removeEventListener("scroll", onScroll);
cancelAnimationFrame(frame);
};
}, [topZone, tolerance]);
/** Bring the header back until the next downward scroll. */
const reveal = useCallback(() => setHidden(false), []);
return [hidden, reveal] as const;
}
const SECTION_IDS = ["top", ...NAV.map((n) => sectionOf(n.href)).filter(Boolean)];
/** Id of the section crossing the middle of the viewport, for highlighting the nav. */
function useActiveSection() {
const [active, setActive] = useState("top");
useEffect(() => {
const io = new IntersectionObserver(
(entries) => {
for (const entry of entries) if (entry.isIntersecting) setActive(entry.target.id);
},
{ rootMargin: "-45% 0px -50% 0px" },
);
for (const id of SECTION_IDS) {
const el = document.getElementById(id);
if (el) io.observe(el);
}
return () => io.disconnect();
}, []);
return active;
}
/** Staggered entrance for hero content (pairs with the `rise-in` class). */
function rise(delay: number) {
return { style: { "--rise-delay": `${delay}ms` } as CSSProperties };
}
function Accent({ children }: { children: ReactNode }) {
return <span className="text-primary">{children}</span>;
}
function Eyebrow({ icon: Icon, children }: { icon: LucideIcon; children: ReactNode }) {
return (
<span className="eyebrow">
<Icon className="h-3.5 w-3.5" aria-hidden />
{children}
</span>
);
}
function IconTile({ icon: Icon, className = "" }: { icon: LucideIcon; className?: string }) {
return (
<span
className={`inline-flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl bg-primary/15 text-primary ${className}`}
>
<Icon className="h-5 w-5" aria-hidden />
</span>
);
}
/** Section heading: headline with an optional body line stacked beneath it. Labels are rationed. */
function SectionHeader({
eyebrow,
icon,
title,
children,
compact = false,
}: {
eyebrow?: string;
icon?: LucideIcon;
title: ReactNode;
children?: ReactNode;
/** Smaller heading for long titles. */
compact?: boolean;
}) {
return (
<div className="max-w-3xl">
{eyebrow && icon ? (
<div {...reveal()} className="mb-5">
<Eyebrow icon={icon}>{eyebrow}</Eyebrow>
</div>
) : null}
<h2
{...reveal("mask", 120)}
className={`font-display font-bold leading-[1.04] ${
compact ? "text-3xl md:text-4xl lg:text-5xl" : "text-4xl md:text-5xl lg:text-6xl"
}`}
>
{title}
</h2>
{children ? (
<div
{...reveal("up", 260)}
className="mt-6 max-w-[65ch] text-lg leading-relaxed text-muted-foreground"
>
{children}
</div>
) : null}
</div>
);
}
function Nav({ page }: { page: SitePageName }) {
const scrolled = useScrolledPast(20);
const [scrollingDown, revealHeader] = useHideOnScroll();
const active = useActiveSection();
const [open, setOpen] = useState(false);
const toggleRef = useRef<HTMLButtonElement>(null);
const overlayRef = useRef<HTMLDivElement>(null);
// The overlay grows out of (and shrinks back into) the menu button.
const setOrigin = () => {
const btn = toggleRef.current;
const overlay = overlayRef.current;
if (!btn || !overlay) return;
const r = btn.getBoundingClientRect();
overlay.style.setProperty("--menu-x", `${r.left + r.width / 2}px`);
overlay.style.setProperty("--menu-y", `${r.top + r.height / 2}px`);
};
const toggle = () => {
setOrigin();
setOpen((o) => !o);
};
useEffect(() => {
if (!open) return;
document.documentElement.classList.add("menu-open");
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") {
setOpen(false);
toggleRef.current?.focus();
return;
}
if (e.key !== "Tab" || !overlayRef.current || !toggleRef.current) return;
// Keep keyboard focus inside the open menu (the toggle button plus the menu's links).
const items = [
toggleRef.current,
...overlayRef.current.querySelectorAll<HTMLElement>("a[href], button"),
];
const first = items[0];
const last = items[items.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
};
window.addEventListener("keydown", onKey);
// Close if the viewport grows to desktop, where the menu does not exist.
const desktop = window.matchMedia("(min-width: 1024px)");
const onDesktop = () => desktop.matches && setOpen(false);
desktop.addEventListener("change", onDesktop);
return () => {
document.documentElement.classList.remove("menu-open");
window.removeEventListener("keydown", onKey);
desktop.removeEventListener("change", onDesktop);
};
}, [open]);
// After the menu closes, keep the header (and its menu button) in view.
useEffect(() => {
if (!open) revealHeader();
}, [open, revealHeader]);
const solid = scrolled && !open;
const hidden = scrollingDown && !open;
// Home highlights the section in view; other pages highlight their own link.
const isCurrent = (href: string) =>
page === "home" ? active === sectionOf(href) : href === `/${page}`;
return (
<>
<header
className={`fixed inset-x-0 top-0 z-50 transition-[transform,background-color,border-color] duration-500 focus-within:translate-y-0 motion-reduce:transition-none ${EASE_STAGE} ${
hidden ? "-translate-y-full" : "translate-y-0"
} ${
solid
? "border-b border-border bg-background/85 backdrop-blur-md"
: "border-b border-transparent bg-transparent"
}`}
>
<div
aria-hidden
className={`scroll-progress absolute inset-x-0 bottom-0 h-px bg-gradient-to-r from-primary/40 via-primary to-primary/40 transition-opacity duration-300 ${
open ? "opacity-0" : ""
}`}
/>
<div className="container-x flex h-18 items-center justify-between gap-4">
<a
href={page === "home" ? "#top" : "/"}
aria-label="KapFoundry home"
className="flex shrink-0 items-center gap-2.5"
onClick={() => setOpen(false)}
>
<img src={logoMark} alt="" width={201} height={192} className="h-10 w-auto sm:h-11" />
<img
src={logoWordmark}
alt=""
width={1041}
height={72}
className="hidden h-3.5 w-auto min-[400px]:block sm:h-4 lg:hidden xl:block"
/>
</a>
<nav
aria-label="Primary"
className={`hidden items-center gap-1 rounded-full border border-white/10 p-1.5 transition-colors duration-300 lg:flex ${
solid ? "bg-white/[0.03]" : "bg-white/5 backdrop-blur-md"
}`}
>
{NAV.map((n) => {
const current = isCurrent(n.href);
return (
<a
key={n.href}
href={n.href}
aria-current={current ? "true" : undefined}
className={`rounded-full px-3 py-2 font-display text-xs font-semibold uppercase tracking-wider transition-colors duration-300 hover:bg-white/10 hover:text-foreground xl:px-4 ${
current ? "bg-primary/15 text-primary" : "text-muted-foreground"
}`}
>
{n.label}
</a>
);
})}
</nav>
<a
href="#investors"
className="press hidden items-center gap-2 rounded-full bg-primary px-5 py-2.5 font-display text-xs font-semibold uppercase tracking-wider text-primary-foreground hover:brightness-110 lg:inline-flex"
>
Invest with us
</a>
{/* Two bars that cross into an X; the label swaps on tablets. */}
<button
ref={toggleRef}
type="button"
onClick={toggle}
aria-expanded={open}
aria-controls="site-menu"
aria-label={open ? "Close menu" : "Open menu"}
className={`press group flex h-11 items-center gap-3 rounded-full border pl-4 pr-3 text-foreground transition-colors duration-300 lg:hidden ${
open ? "border-primary/50 bg-primary/10" : "border-border bg-ink/40 backdrop-blur-md"
}`}
>
<span className="hidden font-display text-xs font-semibold uppercase tracking-[0.16em] sm:block">
{open ? "Close" : "Menu"}
</span>
<span className="relative block h-5 w-5" aria-hidden>
<span
className={`absolute left-0 top-1/2 h-[1.5px] w-5 rounded-full bg-current transition-transform duration-500 ${EASE_STAGE} ${
open ? "rotate-45" : "-translate-y-[4px]"
}`}
/>
<span
className={`absolute left-0 top-1/2 h-[1.5px] rounded-full bg-current transition-all duration-500 ${EASE_STAGE} ${
open ? "w-5 -rotate-45" : "w-3.5 translate-y-[4px] group-hover:w-5"
}`}
/>
</span>
</button>
</div>
</header>
{/* Full-screen menu for phones and tablets: a circle that opens from the menu button. */}
<div
id="site-menu"
ref={overlayRef}
role="dialog"
aria-modal="true"
aria-label="Site menu"
inert={!open}
className={`menu-overlay fixed inset-0 z-40 overflow-y-auto bg-ink lg:hidden ${open ? "is-open" : ""}`}
>
<div className="mesh-gold absolute inset-0 opacity-40" aria-hidden />
<div className="dot-grid absolute inset-0" aria-hidden />
<div className="container-x relative flex min-h-full flex-col gap-10 pb-10 pt-28 md:grid md:grid-cols-12 md:items-end md:gap-12 md:pb-14">
<nav aria-label="Mobile" className="md:col-span-8">
<ul>
{NAV.map((n, i) => {
const current = isCurrent(n.href);
return (
<li key={n.href} className="overflow-hidden border-b border-white/10">
<a
href={n.href}
onClick={() => setOpen(false)}
aria-current={current ? "true" : undefined}
style={{ "--i": i } as CSSProperties}
className="menu-link group flex items-center gap-4 py-3 sm:py-4"
>
<span className="w-7 font-display text-xs font-semibold tabular-nums text-primary/70">
{String(i + 1).padStart(2, "0")}
</span>
<span
className={`font-display text-3xl font-bold tracking-tight transition-[color,transform] duration-500 group-hover:translate-x-2 sm:text-5xl md:text-6xl ${EASE_STAGE} ${
current ? "text-primary" : "text-foreground"
}`}
>
{n.label}
</span>
<ArrowUpRight
aria-hidden
className="ml-auto h-6 w-6 text-primary opacity-0 transition-all duration-500 group-hover:translate-x-0 group-hover:opacity-100 group-focus-visible:opacity-100 sm:-translate-x-2"
/>
</a>
</li>
);
})}
</ul>
</nav>
<div className="menu-foot space-y-6 md:col-span-4 md:pb-3">
<a
href="#investors"
onClick={() => setOpen(false)}
className="press group inline-flex w-full items-center justify-center gap-2 rounded-full bg-primary px-7 py-4 font-display text-sm font-semibold text-primary-foreground hover:brightness-110 md:w-auto"
>
Invest with us
<ArrowRight className="h-4 w-4 transition group-hover:translate-x-0.5" aria-hidden />
</a>
<div className="space-y-1 text-sm text-muted-foreground">
<a
href={`mailto:${CONTACT_EMAIL}`}
className="block text-foreground transition-colors hover:text-primary"
>
{CONTACT_EMAIL}
</a>
<div>Bangalore, India</div>
</div>
</div>
</div>
</div>
</>
);
}
function Hero() {
const markRef = useRef<HTMLDivElement>(null);
// The brand mark leans toward the cursor: quiet feedback, written to CSS vars (no re-render).
const tilt = (e: ReactPointerEvent<HTMLElement>) => {
const mark = markRef.current;
if (!mark || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
const r = e.currentTarget.getBoundingClientRect();
const x = (e.clientX - r.left) / r.width - 0.5;
const y = (e.clientY - r.top) / r.height - 0.5;
mark.style.setProperty("--rx", `${(-y * 10).toFixed(2)}deg`);
mark.style.setProperty("--ry", `${(x * 16).toFixed(2)}deg`);
};
const resetTilt = () => {
markRef.current?.style.setProperty("--rx", "0deg");
markRef.current?.style.setProperty("--ry", "0deg");
};
return (
// .hero-timeline drives .hero-media / .hero-copy as the hero scrolls away (styles.css).
<section
id="top"
onPointerMove={tilt}
onPointerLeave={resetTilt}
className="hero-timeline relative flex min-h-[100svh] items-center overflow-hidden"
>
<div className="hero-media absolute inset-0">
<img
src={heroBg}
alt=""
aria-hidden
fetchPriority="high"
className="hero-zoom absolute inset-0 h-full w-full object-cover opacity-60"
/>
</div>
<div className="absolute inset-0 bg-gradient-to-r from-ink/90 via-ink/60 to-ink/30" />
<div className="absolute inset-x-0 bottom-0 h-1/3 bg-gradient-to-t from-background to-transparent" />
<div className="hero-copy container-x relative grid w-full items-center gap-12 pb-20 pt-28 lg:grid-cols-12">
<div className="lg:col-span-8">
<h1
{...rise(150)}
className="rise-in font-display text-5xl font-extrabold leading-[0.98] sm:text-6xl lg:text-[3.5rem] xl:text-7xl"
>
Build Bold.
<br />
<Accent>Build the Future.</Accent>
</h1>
<p
{...rise(400)}
className="rise-in mt-8 max-w-xl text-lg leading-relaxed text-muted-foreground md:text-xl"
>
A venture studio and technology holding company that engineers category-defining,
AI-first companies, before they even exist.
</p>
<div {...rise(550)} className="rise-in mt-10 flex flex-wrap gap-4">
<a
href="#investors"
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 shadow-[0_10px_40px_-10px] shadow-primary/60 hover:-translate-y-0.5 hover:brightness-110"
>
Invest 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:-translate-y-0.5 hover:border-white/30 hover:bg-white/10"
>
Explore the portfolio
</a>
</div>
</div>
<div {...rise(700)} className="rise-in relative hidden justify-end lg:col-span-4 lg:flex">
<div ref={markRef} className="hero-mark relative">
<div className="glow absolute -inset-20" />
<img
src={logoFull}
alt=""
aria-hidden
width={900}
height={697}
className="relative w-[min(28rem,100%)] drop-shadow-[0_40px_60px_oklch(0.05_0.01_60/0.7)]"
/>
</div>
</div>
</div>
</section>
);
}
function Ticker() {
const items = (hidden: boolean) => (
<div className="flex shrink-0 items-center" aria-hidden={hidden || undefined}>
{TICKER.map((t) => (
<span key={t} className="flex items-center">
<span className="px-8 font-display text-2xl font-bold uppercase tracking-tight md:text-4xl">
{t}
</span>
<Sparkles className="h-5 w-5 text-primary" aria-hidden />
</span>
))}
</div>
);
return (
<div className="marquee overflow-hidden border-y border-border bg-ink py-6 md:py-8">
<div className="marquee-track">
{items(false)}
{items(true)}
</div>
</div>
);
}
function WhoWeAre() {
const cards = [
{
t: "Venture Studio",
icon: Building2,
d: "Companies are systematically created, validated and scaled on shared infrastructure, not simply funded after formation.",
},
{
t: "Holding Company",
icon: Layers,
d: "Every venture strengthens the ecosystem while remaining capable of becoming an independent, high-growth business.",
},
{
t: "Built to Compound",
icon: TrendingUp,
d: "Build faster, reduce risk, and maximize long-term value with one shared engine behind many companies.",
},
];
return (
<section
id="studio"
className="sheet-enter theme-light section-y bg-background text-foreground"
>
<div className="container-x">
<SectionHeader
title={
<>
A venture studio and <Accent>technology holding company.</Accent>
</>
}
>
<p>
We identify large-scale industry problems, validate market opportunities, create
technology-first companies, and scale them into category leaders, designed as a
portfolio, not a single startup.
</p>
</SectionHeader>
{/* One tall feature tile beside two stacked tiles, each with its own surface. */}
<div className="mt-16 grid gap-6 md:grid-cols-2 md:grid-rows-2">
{cards.map((c, i) => {
const feature = i === 0;
const gold = i === 1;
return (
<div
key={c.t}
{...reveal()}
className={`theme-dark group relative flex flex-col overflow-hidden rounded-3xl p-8 transition-transform duration-300 [clip-path:inset(0_round_1.5rem)] hover:-translate-y-1 md:p-10 ${
gold ? "bg-primary text-primary-foreground" : "bg-background text-foreground"
} ${feature ? "min-h-[360px] justify-end md:row-span-2 md:p-12" : ""}`}
>
{feature ? (
<>
<div aria-hidden className="mesh-gold media-parallax absolute inset-0" />
<div aria-hidden className="dot-grid absolute inset-0" />
<div
aria-hidden
className="absolute inset-0 bg-gradient-to-t from-ink via-ink/40 to-transparent"
/>
</>
) : gold ? null : (
<div className="glow absolute -right-20 -top-20 h-64 w-64 opacity-50 transition-opacity duration-500 group-hover:opacity-100" />
)}
<div className="relative">
<IconTile
icon={c.icon}
className={gold ? "bg-primary-foreground/10 text-primary-foreground" : ""}
/>
<h3
className={`font-display font-bold ${
feature ? "mt-8 text-3xl md:text-4xl" : "mt-8 text-2xl"
}`}
>
{c.t}
</h3>
<p
className={`mt-4 max-w-[45ch] leading-relaxed ${
gold ? "text-primary-foreground/80" : "text-muted-foreground"
} ${feature ? "text-lg" : ""}`}
>
{c.d}
</p>
</div>
</div>
);
})}
</div>
</div>
</section>
);
}
function VisionMission() {
return (
<section className="section-y relative overflow-hidden bg-ink">
<div className="glow glow-drift absolute -right-48 top-0 h-[38rem] w-[38rem] opacity-40" />
<div className="container-x relative">
<div className="max-w-4xl" {...reveal("stagger")}>
<Eyebrow icon={Eye}>Vision</Eyebrow>
<p className="mt-6 font-display text-3xl font-bold leading-[1.1] tracking-tight md:text-5xl">
To become India's most <Accent>influential venture studio</Accent>, building
category-defining companies that solve real-world problems for billions.
</p>
<p className="mt-8 max-w-[65ch] text-lg leading-relaxed text-muted-foreground">
The next generation of global companies will not emerge by chance. They will be
engineered through research, technology, and repeatable company-building.
</p>
</div>
<div className="mt-20">
<div {...reveal()}>
<Eyebrow icon={Target}>Our mission</Eyebrow>
</div>
{/* Each line's gold rule draws in turn, so the six commitments read as a sequence. */}
<ol
className="mt-8 grid gap-x-12 md:grid-cols-2 lg:grid-cols-3"
{...reveal("stagger", 150)}
>
{MISSION.map((m, i) => (
<li key={m} className="group relative pb-8 pt-6">
<span aria-hidden className="absolute inset-x-0 top-0 h-px bg-border" />
<span
aria-hidden
data-draw
className="absolute inset-x-0 top-0 h-px origin-left bg-primary/70"
/>
<span className="font-display text-3xl font-bold tabular-nums text-primary/40 transition-colors duration-500 group-hover:text-primary">
{String(i + 1).padStart(2, "0")}
</span>
<p className="mt-3 max-w-[32ch] text-lg leading-snug">{m}</p>
</li>
))}
</ol>
</div>
</div>
</section>
);
}
/** Splits a sentence into words that brighten as they cross the viewport (see .fill-word). */
function FillWords({ text, className = "" }: { text: string; className?: string }) {
return (
<>
{text.split(" ").map((word, i) => (
<span key={i} className={`fill-word ${className}`}>
{word}{" "}
</span>
))}
</>
);
}
function Different() {
return (
<section className="sheet-enter theme-light section-y bg-background text-foreground">
<div className="container-x">
<p className="max-w-5xl font-display text-4xl font-bold leading-[1.06] tracking-tight md:text-6xl">
<FillWords text="Traditional venture capital invests in startups after they exist." />
<FillWords
text="KapFoundry creates startups before they exist."
className="text-primary"
/>
</p>
<p
{...reveal("up", 150)}
className="mt-10 max-w-[65ch] text-lg leading-relaxed text-muted-foreground"
>
Every company begins with extensive market research, customer discovery, product
validation, and AI-driven technology development,{" "}
<span className="font-semibold text-foreground">
before a single rupee of capital is deployed.
</span>
</p>
</div>
</section>
);
}
function StudioAdvantage() {
return (
<section className="section-y bg-background">
<div className="container-x">
<SectionHeader
title={
<>
Every venture launches with a <Accent>mature company's stack.</Accent>
</>
}
>
<p>
<span className="font-semibold text-foreground">The result:</span> dramatically reduced
startup risk, with meaningfully faster execution.
</p>
</SectionHeader>
{/* Index list. The hovered row lifts into a glass card: gold edge bar, filled number
badge, icon tile and a cursor spotlight. Fills down each column (01-05, 06-10). */}
<ol className="mt-16 grid gap-x-10 md:grid-flow-col md:grid-cols-2 md:grid-rows-5">
{STACK.map((s, i) => {
const Icon = s.icon;
return (
<li
key={s.label}
{...reveal()}
onPointerMove={trackSpotlight}
className={`spotlight group relative -mx-4 flex items-center gap-5 overflow-hidden rounded-2xl px-4 py-5 transition-[background-color,box-shadow] duration-500 before:absolute before:inset-x-4 before:top-0 before:h-px before:bg-border before:transition-opacity before:duration-300 hover:bg-white/[0.04] hover:shadow-[inset_0_0_0_1px_rgb(255_255_255/0.08),inset_0_1px_0_rgb(255_255_255/0.08),0_24px_48px_-28px_oklch(0.05_0.01_60/0.9)] hover:before:opacity-0 [li:hover+&]:before:opacity-0 ${EASE_STAGE}`}
>
<span
aria-hidden
className={`absolute left-0 top-1/2 h-8 w-[3px] -translate-y-1/2 scale-y-0 rounded-full bg-primary transition-transform duration-500 group-hover:scale-y-100 ${EASE_STAGE}`}
/>
<span className="relative grid h-9 w-9 shrink-0 place-items-center rounded-full border border-border font-display text-xs font-semibold tabular-nums text-muted-foreground transition-all duration-500 group-hover:border-primary group-hover:bg-primary group-hover:text-primary-foreground group-hover:shadow-lg group-hover:shadow-primary/25">
{String(i + 1).padStart(2, "0")}
</span>
<span
className={`relative flex-1 font-display text-xl font-semibold transition-transform duration-500 group-hover:translate-x-1 md:text-2xl ${EASE_STAGE}`}
>
{s.label}
</span>
<span
className={`relative grid h-10 w-10 shrink-0 place-items-center rounded-xl bg-primary/10 text-primary ring-1 ring-inset ring-primary/20 transition-all duration-500 md:-rotate-12 md:scale-75 md:opacity-0 md:group-hover:rotate-0 md:group-hover:scale-100 md:group-hover:opacity-100 ${EASE_STAGE}`}
>
<Icon className="h-[18px] w-[18px]" aria-hidden />
</span>
</li>
);
})}
</ol>
</div>
</section>
);
}
/**
* Whether a section may pin itself and drive its content from scroll. Short screens
* (e.g. landscape phones) and reduced-motion users get the static layout instead.
*/
function usePinnedLayout(extraQuery?: string) {
const [pinned, setPinned] = useState(false);
useEffect(() => {
const base = "(min-height: 600px) and (prefers-reduced-motion: no-preference)";
const mq = window.matchMedia(extraQuery ? `${base} and ${extraQuery}` : base);
const sync = () => setPinned(mq.matches);
sync();
mq.addEventListener("change", sync);
window.addEventListener("resize", sync);
return () => {
mq.removeEventListener("change", sync);
window.removeEventListener("resize", sync);
};
}, [extraQuery]);
return pinned;
}
const STEP_SCROLL_SVH = 75;
// Orbit geometry in a 400×400 viewBox; nodes sit on the ring at ORBIT_R.
const ORBIT_R = 150;
const ORBIT_C = 2 * Math.PI * ORBIT_R;
const EASE_STAGE = "ease-[cubic-bezier(0.16,1,0.3,1)]";
function orbitPosition(i: number, count: number) {
const angle = ((-90 + (i * 360) / count) * Math.PI) / 180;
const pct = (ORBIT_R / 400) * 100;
return {
left: `${(50 + pct * Math.cos(angle)).toFixed(3)}%`,
top: `${(50 + pct * Math.sin(angle)).toFixed(3)}%`,
};
}
function Framework() {
const pinned = usePinnedLayout();
const sectionRef = useRef<HTMLElement>(null);
const arcRef = useRef<SVGCircleElement>(null);
const segmentRefs = useRef<(HTMLSpanElement | null)[]>([]);
const [active, setActive] = useState(0);
const steps = FRAMEWORK.length;
useEffect(() => {
const section = sectionRef.current;
if (!pinned || !section) return;
const segments = segmentRefs.current;
let frame = 0;
let shown = -1;
const update = () => {
frame = 0;
const distance = section.offsetHeight - window.innerHeight;
const progress =
distance > 0
? Math.min(1, Math.max(0, -section.getBoundingClientRect().top / distance))
: 0;
const scaled = progress * steps;
// Each stage owns an equal slice of the scroll, so one wheel tick never skips a stage.
const index = Math.min(steps - 1, Math.floor(scaled));
segments.forEach((el, i) => {
if (el) el.style.transform = `scaleX(${Math.min(1, Math.max(0, scaled - i))})`;
});
// The arc reaches node i as stage i begins and closes the loop after the last stage.
if (arcRef.current) arcRef.current.style.strokeDashoffset = String(ORBIT_C * (1 - progress));
if (index !== shown) {
shown = index;
setActive(index);
}
};
const onScroll = () => {
if (!frame) frame = requestAnimationFrame(update);
};
update();
window.addEventListener("scroll", onScroll, { passive: true });
window.addEventListener("resize", onScroll);
return () => {
window.removeEventListener("scroll", onScroll);
window.removeEventListener("resize", onScroll);
cancelAnimationFrame(frame);
};
}, [pinned, steps]);
const goTo = (i: number) => {
const section = sectionRef.current;
if (!section) return;
const distance = section.offsetHeight - window.innerHeight;
window.scrollTo({ top: section.offsetTop + ((i + 0.5) / steps) * distance });
};
const title = (
<>
Our <Accent>venture-building</Accent> framework.
</>
);
const intro =
"Six stages take every company from a billion-dollar problem to an independent business.";
if (!pinned) {
return (
<section
id="framework"
className="sheet-enter theme-light section-y bg-background text-foreground"
>
<div className="container-x">
<SectionHeader eyebrow="Repeatable by design" icon={Workflow} title={title}>
<p>{intro}</p>
</SectionHeader>
<ol className="mt-16 grid gap-4 md:grid-cols-2">
{FRAMEWORK.map((f) => (
<li
key={f.n}
{...reveal()}
className="flex gap-5 rounded-3xl border border-border bg-card p-6"
>
<IconTile icon={f.icon} />
<div>
<h3 className="font-display text-2xl font-bold">
<span className="mr-2 text-primary tabular-nums">{f.n}</span>
{f.title}
</h3>
<p className="mt-2 text-muted-foreground">{f.body}</p>
</div>
</li>
))}
</ol>
</div>
</section>
);
}
const next = FRAMEWORK[active + 1];
return (
<section
id="framework"
ref={sectionRef}
className="sheet-enter theme-light relative bg-background text-foreground"
style={{ height: `${100 + steps * STEP_SCROLL_SVH}svh` }}
>
<div className="sticky top-0 flex h-[100svh] flex-col overflow-hidden pb-6 pt-20 md:pt-24">
<div className="container-x flex min-h-0 w-full flex-1 flex-col">
{/* Heading row */}
<div {...reveal("stagger")}>
<Eyebrow icon={Workflow}>Repeatable by design</Eyebrow>
<h2 className="mt-4 font-display text-3xl font-bold leading-[1.04] md:text-5xl">
{title}
</h2>
<p className="mt-3 hidden max-w-[65ch] leading-relaxed text-muted-foreground md:block [@media(max-height:820px)]:!hidden">
{intro}
</p>
</div>
{/* Segmented progress: one bar per stage, filling as you scroll through it */}
<div className="mt-6 grid grid-cols-6 gap-2 md:mt-8 md:gap-3">
{FRAMEWORK.map((f, i) => (
<button
key={f.n}
type="button"
onClick={() => goTo(i)}
aria-label={`Stage ${f.n}: ${f.title}`}
aria-current={i === active ? "step" : undefined}
className="group py-2 text-left"
>
<span className="block h-1 overflow-hidden rounded-full bg-foreground/10">
<span
ref={(el) => {
segmentRefs.current[i] = el;
}}
className="block h-full origin-left scale-x-0 rounded-full bg-primary"
/>
</span>
<span
className={`mt-3 flex items-center gap-2 font-display text-xs font-semibold transition-colors duration-500 ${
i === active
? "text-foreground"
: "text-muted-foreground group-hover:text-foreground"
}`}
>
<span className={i <= active ? "text-primary" : ""}>{f.n}</span>
<span className="hidden truncate md:inline">{f.title}</span>
</span>
</button>
))}
</div>
{/* Stage panel */}
<div
{...reveal("up", 150)}
className="theme-dark relative mt-5 min-h-0 flex-1 overflow-hidden rounded-[2rem] bg-background text-foreground shadow-2xl shadow-black/25 md:mt-6"
>
<div className="glow absolute -left-40 -top-40 h-[32rem] w-[32rem] opacity-60" />
<div className="absolute inset-0 grid-lines opacity-30" />
<div className="relative grid h-full md:grid-cols-2">
{/* Copy */}
<div className="flex min-h-0 flex-col justify-center p-6 md:p-10 lg:p-12">
<div className="flex items-end gap-3">
{/* Odometer: the stage numbers roll past like a counter */}
<div
aria-hidden
className="h-[1em] overflow-hidden font-display text-6xl font-extrabold leading-none tracking-tighter md:text-7xl lg:text-8xl"
>
<div
className={`transition-transform duration-700 ${EASE_STAGE}`}
style={{ transform: `translateY(-${active}em)` }}
>
{FRAMEWORK.map((f) => (
<div
key={f.n}
className="h-[1em] bg-gradient-to-b from-primary to-primary/35 bg-clip-text text-transparent"
>
{f.n}
</div>
))}
</div>
</div>
<span className="mb-1.5 font-display text-sm font-semibold text-muted-foreground">
/ {String(steps).padStart(2, "0")}
</span>
</div>
<div className="relative mt-6 min-h-[8.5rem] md:mt-8 md:min-h-[10rem]">
{FRAMEWORK.map((f, i) => {
const offset = i - active;
return (
<div
key={f.n}
aria-hidden={offset !== 0}
className={`absolute inset-x-0 top-0 transition-all duration-700 ${EASE_STAGE} ${
offset === 0
? "translate-y-0 opacity-100 blur-0"
: offset < 0
? "pointer-events-none -translate-y-6 opacity-0 blur-sm"
: "pointer-events-none translate-y-6 opacity-0 blur-sm"
}`}
>
<h3 className="font-display text-4xl font-extrabold md:text-5xl lg:text-6xl">
{f.title}
</h3>
<p className="mt-3 max-w-md text-base leading-relaxed text-muted-foreground md:text-lg">
{f.body}
</p>
</div>
);
})}
</div>
<div
key={active}
className="mt-4 flex items-center gap-3 font-display text-xs font-semibold uppercase tracking-[0.16em] text-primary duration-700 animate-in fade-in slide-in-from-left-2"
>
{next ? (
<>
Next: {next.title}
<ArrowDown className="nudge-y h-4 w-4" aria-hidden />
</>
) : (
<>
<Check className="h-4 w-4" aria-hidden />
An independent portfolio company
</>
)}
</div>
</div>
{/* Orbit: the whole journey at a glance */}
<div className="relative hidden min-h-0 items-center justify-center p-8 md:flex">
<div className="relative aspect-square h-full max-h-[440px]">
<svg viewBox="0 0 400 400" className="absolute inset-0 h-full w-full" aria-hidden>
<circle
cx="200"
cy="200"
r="188"
fill="none"
strokeWidth="1"
strokeDasharray="2 10"
className="origin-center animate-[spin_80s_linear_infinite] stroke-white/15 [transform-box:fill-box] motion-reduce:animate-none"
/>
<circle
cx="200"
cy="200"
r={ORBIT_R}
fill="none"
strokeWidth="1.5"
className="stroke-white/10"
/>
<circle
ref={arcRef}
cx="200"
cy="200"
r={ORBIT_R}
fill="none"
strokeWidth="2.5"
strokeLinecap="round"
strokeDasharray={ORBIT_C}
strokeDashoffset={ORBIT_C}
transform="rotate(-90 200 200)"
className="stroke-primary transition-[stroke-dashoffset] duration-150 ease-linear"
/>
</svg>
{FRAMEWORK.map((f, i) => {
const state = i === active ? "active" : i < active ? "done" : "next";
return (
<button
key={f.n}
type="button"
onClick={() => goTo(i)}
aria-label={`Stage ${f.n}: ${f.title}`}
style={orbitPosition(i, steps)}
className="group absolute -translate-x-1/2 -translate-y-1/2"
>
{state === "active" ? (
<span className="absolute inset-0 animate-ping rounded-full bg-primary/30 motion-reduce:animate-none" />
) : null}
<span
className={`relative flex h-11 w-11 items-center justify-center rounded-full border font-display text-xs font-bold transition-all duration-500 ${
state === "active"
? "scale-110 border-primary bg-primary text-primary-foreground shadow-lg shadow-primary/40"
: state === "done"
? "border-primary bg-background text-primary"
: "border-white/15 bg-background text-muted-foreground group-hover:border-primary/60 group-hover:text-foreground"
}`}
>
{state === "done" ? <Check className="h-4 w-4" aria-hidden /> : f.n}
</span>
</button>
);
})}
{/* Centre: the active stage's icon */}
<div className="absolute left-1/2 top-1/2 flex h-28 w-28 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-[2rem] border border-white/10 bg-white/[0.04] backdrop-blur-sm">
<div className="glow absolute -inset-10 opacity-80" />
{FRAMEWORK.map((f, i) => {
const Icon = f.icon;
return (
<Icon
key={f.n}
aria-hidden
className={`absolute h-10 w-10 text-primary transition-all duration-700 ${EASE_STAGE} ${
i === active
? "rotate-0 scale-100 opacity-100"
: "-rotate-45 scale-50 opacity-0"
}`}
/>
);
})}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
}
const PORTFOLIO_STEP_SVH = 70;
// A card starts flying off once this share of its scroll slice has passed.
const DECK_FLY_START = 0.5;
const DECK_PEEK_PX = 22;
/** One vertical as a deck card: photo side and details side. */
function VerticalCard({ v }: { v: (typeof VERTICALS)[number] }) {
const Icon = v.icon;
return (
<div className="grid h-full grid-rows-[minmax(130px,28%)_1fr] overflow-hidden rounded-[2rem] border border-white/10 bg-card shadow-[0_40px_80px_-40px_oklch(0.05_0.01_60/0.9)] md:grid-cols-2 md:grid-rows-1">
<div className="relative min-h-[150px] overflow-hidden md:min-h-0">
<img
src={v.img}
alt=""
aria-hidden
loading="lazy"
decoding="async"
className="absolute inset-0 h-full w-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-ink via-ink/40 to-ink/5" />
<span className="absolute left-5 top-5 inline-flex h-12 w-12 items-center justify-center rounded-2xl bg-primary text-primary-foreground shadow-lg shadow-black/30 md:left-7 md:top-7">
<Icon className="h-5 w-5" aria-hidden />
</span>
<div className="absolute bottom-5 left-5 right-5 md:bottom-8 md:left-8 md:right-8">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-primary">
{v.tag}
</div>
<h3 className="mt-2 font-display text-2xl font-bold leading-tight text-white md:text-4xl">
{v.name}
</h3>
</div>
</div>
<div className="flex min-h-0 flex-col justify-center gap-4 p-4 sm:p-5 md:gap-5 md:p-8 lg:p-10">
<div className="grid grid-cols-2 gap-4 md:grid-cols-1 md:gap-6 xl:grid-cols-2">
<div>
<div className="mb-2.5 text-sm font-medium text-muted-foreground">The problem</div>
<ul className="space-y-1.5">
{v.problem.map((p) => (
<li
key={p}
className="flex gap-2 text-[13px] leading-snug text-muted-foreground md:text-sm"
>
<span className="mt-[7px] h-1 w-1 shrink-0 rounded-full bg-muted-foreground/60" />
{p}
</li>
))}
</ul>
</div>
<div>
<div className="mb-2.5 text-sm font-medium text-primary">Our solution</div>
<ul className="space-y-1.5">
{v.solution.map((p) => (
<li
key={p}
className="flex gap-2 text-[13px] leading-snug text-foreground md:text-sm"
>
<Check className="mt-0.5 h-3.5 w-3.5 shrink-0 text-primary" aria-hidden />
{p}
</li>
))}
</ul>
</div>
</div>
{v.example ? (
<p className="flex items-start gap-3 rounded-2xl border border-primary/25 bg-primary/10 p-4 text-sm text-foreground">
<ArrowUpRight className="mt-0.5 h-4 w-4 shrink-0 text-primary" aria-hidden />
<span>
<span className="text-primary">Example venture: </span>
{v.example}
</span>
</p>
) : null}
</div>
</div>
);
}
/**
* Card deck. Pinned: the verticals sit stacked like a deck; scrolling flies the top card away
* and the next one rises into place. Otherwise (short screens, reduced motion) the cards
* simply stack down the page.
*/
function Portfolio() {
// Phones need more height than tablets/desktops to fit a whole card while pinned.
const pinned = usePinnedLayout(
"(((min-width: 768px) and (min-height: 700px)) or (min-height: 800px))",
);
const sectionRef = useRef<HTMLElement>(null);
const cardRefs = useRef<(HTMLDivElement | null)[]>([]);
const fillRefs = useRef<(HTMLSpanElement | null)[]>([]);
const [active, setActive] = useState(0);
const count = VERTICALS.length;
useEffect(() => {
const section = sectionRef.current;
if (!pinned || !section) return;
const cards = cardRefs.current.filter((c): c is HTMLDivElement => c !== null);
const fills = fillRefs.current;
let frame = 0;
let shown = -1;
let eased = -1;
let last = 0;
const target = () => {
const distance = section.offsetHeight - window.innerHeight;
return distance > 0
? Math.min(1, Math.max(0, -section.getBoundingClientRect().top / distance))
: 0;
};
const render = (progress: number) => {
const scaled = progress * count;
const current = Math.min(count - 1, Math.floor(scaled));
// How far the top card has flown off (the last card never leaves).
const fly =
current < count - 1
? Math.min(1, Math.max(0, (scaled - current - DECK_FLY_START) / (1 - DECK_FLY_START)))
: 0;
cards.forEach((card, k) => {
const veil = card.querySelector<HTMLElement>("[data-veil]");
if (k < current) {
card.style.transform = "translate3d(0, -115%, 0) rotate(-7deg)";
card.style.opacity = "0";
return;
}
if (k === current) {
// Ease in (slow start, fast exit) and fade early so the two cards never read as one.
const lift = fly * fly;
card.style.transform = `translate3d(0, ${-lift * 115}%, 0) rotate(${-fly * 7}deg) scale(${1 - fly * 0.06})`;
card.style.opacity = String(Math.max(0, 1 - fly * 2));
if (veil) veil.style.opacity = "0";
return;
}
// Waiting in the deck: each layer sits a little lower and smaller, and rises as the
// card above it flies away.
const depth = k - current - fly;
card.style.transform = `translate3d(0, ${depth * DECK_PEEK_PX}px, 0) scale(${1 - depth * 0.045})`;
card.style.opacity = depth > 2.6 ? "0" : "1";
if (veil) veil.style.opacity = String(Math.min(0.75, depth * 0.28));
});
fills.forEach((fill, k) => {
if (fill) fill.style.transform = `scaleX(${Math.min(1, Math.max(0, scaled - k))})`;
});
if (current !== shown) {
shown = current;
setActive(current);
}
};
// Glide toward the scroll position rather than snapping, so wheel steps feel smooth.
const tick = (now: number) => {
frame = 0;
const goal = target();
const dt = last ? Math.min(now - last, 64) : 16;
last = now;
eased = eased < 0 ? goal : eased + (goal - eased) * (1 - Math.exp(-dt / 110));
if (Math.abs(goal - eased) < 0.0005) eased = goal;
render(eased);
if (eased !== goal) frame = requestAnimationFrame(tick);
else last = 0;
};
const onScroll = () => {
if (!frame) frame = requestAnimationFrame(tick);
};
eased = target();
render(eased);
window.addEventListener("scroll", onScroll, { passive: true });
window.addEventListener("resize", onScroll);
return () => {
window.removeEventListener("scroll", onScroll);
window.removeEventListener("resize", onScroll);
cancelAnimationFrame(frame);
for (const card of cards) {
card.style.transform = "";
card.style.opacity = "";
}
};
}, [pinned, count]);
const goTo = (i: number) => {
const section = sectionRef.current;
if (!section) return;
const distance = section.offsetHeight - window.innerHeight;
window.scrollTo({ top: section.offsetTop + ((i + 0.25) / count) * distance });
};
const title = (
<>
Seven industries, <Accent>one AI engine.</Accent>
</>
);
const intro = (
<>
<span className="font-semibold text-foreground">Artificial Intelligence</span> is the
foundation beneath every venture, not a vertical of its own.
</>
);
if (!pinned) {
return (
<section id="portfolio" className="section-y relative bg-ink">
<div className="container-x">
<SectionHeader title={title}>
<p>{intro}</p>
</SectionHeader>
<div className="mt-14 space-y-6">
{VERTICALS.map((v) => (
<div key={v.n} {...reveal()} className="md:h-[520px]">
<VerticalCard v={v} />
</div>
))}
</div>
</div>
</section>
);
}
return (
<section
id="portfolio"
ref={sectionRef}
className="relative bg-ink"
style={{ height: `${100 + count * PORTFOLIO_STEP_SVH}svh` }}
>
<div className="sticky top-0 flex h-[100svh] flex-col overflow-hidden pb-6 pt-20 lg:pb-10 lg:pt-24">
<div className="glow glow-drift absolute -right-40 top-1/4 h-[36rem] w-[36rem] opacity-40" />
<div className="container-x relative grid min-h-0 w-full flex-1 grid-rows-[auto_minmax(0,1fr)] gap-5 lg:grid-cols-12 lg:grid-rows-1 lg:gap-12">
{/* Title and index */}
<div className="flex flex-col lg:col-span-4" {...reveal("stagger")}>
<h2 className="font-display text-3xl font-bold leading-[1.04] md:text-4xl xl:text-5xl">
{title}
</h2>
<p className="mt-3 hidden max-w-[40ch] text-muted-foreground lg:block [@media(max-height:760px)]:!hidden">
{intro}
</p>
{/* Desktop: named index with a fill line per vertical */}
<ol className="mt-8 hidden space-y-1 lg:block">
{VERTICALS.map((v, i) => (
<li key={v.n}>
<button
type="button"
onClick={() => goTo(i)}
aria-current={i === active ? "step" : undefined}
className="group flex min-h-10 w-full items-center gap-4 text-left"
>
<span
className={`w-6 font-display text-xs font-semibold tabular-nums transition-colors duration-500 ${
i <= active ? "text-primary" : "text-muted-foreground"
}`}
>
{v.n}
</span>
<span className="relative flex-1">
<span
className={`font-display text-base font-semibold transition-colors duration-500 xl:text-lg ${
i === active
? "text-foreground"
: "text-muted-foreground group-hover:text-foreground"
}`}
>
{v.short}
</span>
<span className="absolute inset-x-0 -bottom-1.5 h-px bg-white/10">
<span
ref={(el) => {
fillRefs.current[i] = el;
}}
className="block h-full origin-left scale-x-0 bg-primary"
/>
</span>
</span>
</button>
</li>
))}
</ol>
{/* Phones and tablets: dots */}
<div className="mt-4 flex items-center gap-1 lg:hidden">
{VERTICALS.map((v, i) => (
<button
key={v.n}
type="button"
onClick={() => goTo(i)}
aria-label={v.name}
aria-current={i === active ? "step" : undefined}
className="flex h-10 min-w-8 items-center justify-center"
>
<span
className={`block h-1.5 rounded-full transition-all duration-500 ${
i === active
? "w-8 bg-primary"
: i < active
? "w-3 bg-primary/60"
: "w-3 bg-white/15"
}`}
/>
</button>
))}
</div>
</div>
{/* The deck */}
<div
// Bottom padding leaves room for the deck layers peeking below the top card.
className="relative min-h-0 pb-11 lg:col-span-8"
{...reveal("up", 150)}
>
<div className="relative h-full">
{VERTICALS.map((v, k) => (
<div
key={v.n}
ref={(el) => {
cardRefs.current[k] = el;
}}
aria-hidden={k !== active}
className="absolute inset-0 origin-top will-change-transform"
style={{ zIndex: count - k }}
>
<VerticalCard v={v} />
<div
data-veil
aria-hidden
className="pointer-events-none absolute inset-0 rounded-[2rem] bg-ink opacity-0"
/>
</div>
))}
</div>
</div>
</div>
</div>
</section>
);
}
/** Moves a cell's cursor spotlight (see .spotlight) without re-rendering React. */
function trackSpotlight(e: ReactPointerEvent<HTMLElement>) {
const r = e.currentTarget.getBoundingClientRect();
e.currentTarget.style.setProperty("--mx", `${e.clientX - r.left}px`);
e.currentTarget.style.setProperty("--my", `${e.clientY - r.top}px`);
}
// Bento placement for the eight capabilities: one feature tile, one wide tile, six singles.
const AI_FEATURE = 0;
const AI_WIDE = 5;
function AISection() {
return (
<section id="ai" className="sheet-enter theme-light section-y bg-background text-foreground">
<div className="container-x">
<SectionHeader
compact
title={
<>
AI is not a vertical.{" "}
<Accent>It's the foundation under every company we build.</Accent>
</>
}
/>
<div className="mt-16 grid auto-rows-[minmax(150px,auto)] grid-cols-2 gap-4 lg:grid-cols-4">
{AI_CAPS.map((a, i) => {
const Icon = a.icon;
if (i === AI_FEATURE) {
return (
<div
key={a.label}
{...reveal()}
onPointerMove={trackSpotlight}
className="spotlight theme-dark group relative col-span-2 row-span-2 flex flex-col justify-between overflow-hidden rounded-3xl [clip-path:inset(0_round_1.5rem)] bg-background p-8 text-foreground md:p-10"
>
<div className="glow absolute -right-24 -top-24 h-[26rem] w-[26rem] opacity-70" />
<span className="relative inline-flex h-16 w-16 items-center justify-center rounded-2xl bg-primary/15 text-primary">
<Icon className="h-7 w-7" aria-hidden />
</span>
<div className="relative mt-16">
<h3 className="font-display text-3xl font-bold md:text-4xl">{a.label}</h3>
<p className="mt-3 max-w-[40ch] text-lg leading-relaxed text-muted-foreground">
Every venture is designed{" "}
<span className="font-semibold text-foreground">AI-first</span>, not just
AI-enabled.
</p>
</div>
</div>
);
}
return (
<div
key={a.label}
{...reveal()}
onPointerMove={trackSpotlight}
className={`spotlight group relative flex overflow-hidden rounded-3xl p-6 transition-colors duration-300 ${
i === AI_WIDE
? "col-span-2 items-center gap-5 bg-primary/10"
: "flex-col justify-between border border-border bg-card hover:border-primary/40"
}`}
>
<IconTile
icon={Icon}
className="relative transition-colors duration-300 group-hover:bg-primary group-hover:text-primary-foreground"
/>
<span
className={`relative font-display font-semibold ${
i === AI_WIDE ? "text-xl md:text-2xl" : "mt-6 text-base md:text-lg"
}`}
>
{a.label}
</span>
</div>
);
})}
</div>
</div>
</section>
);
}
function Team() {
const sectionRef = useRef<HTMLElement>(null);
const trackRef = useRef<HTMLDivElement>(null);
const barRef = useRef<HTMLDivElement>(null);
const backdropRef = useRef<HTMLDivElement>(null);
const cardRefs = useRef<(HTMLElement | null)[]>([]);
// Starts as a plain swipeable row (server render, reduced motion, short screens) and
// upgrades to the pinned horizontal scroll once mounted.
const pinned = usePinnedLayout();
const [current, setCurrent] = useState(0);
useEffect(() => {
const section = sectionRef.current;
const track = trackRef.current;
const backdrop = backdropRef.current;
const cards = cardRefs.current.filter((c): c is HTMLElement => c !== null);
if (!pinned || !section || !track) return;
let distance = 0;
let frame = 0;
let shown = -1;
let eased = -1;
let last = 0;
const target = () =>
distance ? Math.min(1, Math.max(0, -section.getBoundingClientRect().top / distance)) : 0;
// The track glides toward the scroll position instead of snapping to it, which smooths
// out coarse mouse-wheel steps. Time-based, so it feels the same at 60 Hz and 120 Hz.
const tick = (now: number) => {
frame = 0;
const goal = target();
const dt = last ? Math.min(now - last, 64) : 16;
last = now;
eased = eased < 0 ? goal : eased + (goal - eased) * (1 - Math.exp(-dt / 110));
if (Math.abs(goal - eased) < 0.0005) eased = goal;
render(eased);
if (eased !== goal) frame = requestAnimationFrame(tick);
else last = 0;
};
const render = (progress: number) => {
const offset = progress * distance;
track.style.transform = `translate3d(${-offset}px, 0, 0)`;
if (barRef.current) barRef.current.style.transform = `scaleX(${progress})`;
if (backdrop) backdrop.style.transform = `translate3d(${-progress * 30}%, 0, 0)`;
// Cards grow toward the centre of the screen; photos drift against the motion.
const mid = window.innerWidth / 2;
let nearest = 0;
let best = Infinity;
cards.forEach((card, i) => {
const d = (card.offsetLeft + card.offsetWidth / 2 - offset - mid) / window.innerWidth;
const a = Math.min(Math.abs(d), 1);
card.style.transform = `scale(${1 - a * 0.12})`;
card.style.opacity = String(1 - a * 0.5);
const img = card.querySelector<HTMLElement>("[data-parallax]");
if (img) img.style.transform = `translate3d(${d * -8}%, 0, 0) scale(1.18)`;
if (a < best) {
best = a;
nearest = i;
}
});
if (nearest !== shown) {
shown = nearest;
setCurrent(nearest);
}
};
const measure = () => {
distance = Math.max(0, track.scrollWidth - window.innerWidth);
// Vertical scroll length equals the horizontal travel, so the motion feels 1:1.
section.style.height = `${distance + window.innerHeight}px`;
eased = target();
render(eased);
};
const onScroll = () => {
if (!frame) frame = requestAnimationFrame(tick);
};
measure();
window.addEventListener("scroll", onScroll, { passive: true });
window.addEventListener("resize", measure);
return () => {
window.removeEventListener("scroll", onScroll);
window.removeEventListener("resize", measure);
cancelAnimationFrame(frame);
section.style.height = "";
track.style.transform = "";
if (backdrop) backdrop.style.transform = "";
for (const card of cards) {
card.style.transform = "";
card.style.opacity = "";
card.querySelector<HTMLElement>("[data-parallax]")?.style.removeProperty("transform");
}
};
}, [pinned]);
return (
<section
id="team"
ref={sectionRef}
aria-label="Our team"
className={`relative bg-background ${pinned ? "" : "section-y"}`}
>
<div
className={
pinned
? "sticky top-0 flex h-[100svh] flex-col justify-center overflow-hidden pt-16"
: "relative overflow-hidden"
}
>
<div
aria-hidden
className="pointer-events-none absolute inset-x-0 top-1/2 -translate-y-1/2"
>
<div
ref={backdropRef}
className="whitespace-nowrap pl-[10vw] font-display text-[26vw] font-extrabold leading-none tracking-tighter text-white/[0.035] will-change-transform"
>
TEAM · TEAM · TEAM
</div>
</div>
<div className="glow glow-drift absolute -right-40 top-1/4 h-[36rem] w-[36rem] opacity-40" />
<div
ref={trackRef}
className={`relative flex items-stretch gap-6 px-6 md:gap-10 md:px-10 lg:px-[max(2.5rem,calc((100vw-1280px)/2+2.5rem))] ${
pinned
? "w-max will-change-transform"
: "snap-x snap-mandatory overflow-x-auto pb-6 [scrollbar-width:none]"
}`}
>
<div
{...reveal("stagger")}
className="flex w-[72vw] shrink-0 snap-start flex-col justify-center sm:w-[55vw] lg:w-[34vw]"
>
<Eyebrow icon={Users}>Our team</Eyebrow>
<h2 className="mt-6 font-display text-4xl font-bold leading-[1.02] md:text-5xl lg:text-6xl">
The people behind <Accent>every venture.</Accent>
</h2>
<p className="mt-6 max-w-md text-lg leading-relaxed text-muted-foreground">
The operators and builders who turn research into companies, and companies into
category leaders.
</p>
</div>
{TEAM.map((p, i) => (
<article
key={p.img}
ref={(el) => {
cardRefs.current[i] = el;
}}
className="group relative aspect-[3/4] w-[68vw] shrink-0 self-center snap-center overflow-hidden rounded-[2rem] border border-border bg-card transition-colors duration-500 will-change-transform [clip-path:inset(0_round_2rem)] hover:border-primary/50 sm:h-[62svh] sm:max-h-[640px] sm:min-h-[420px] sm:w-auto"
>
<img
data-parallax
src={p.img}
alt={p.name ? `Portrait of ${p.name}` : "KapFoundry team member"}
loading="lazy"
decoding="async"
width={680}
height={680}
className="absolute inset-0 h-full w-full scale-[1.18] object-cover object-[50%_30%] will-change-transform"
/>
<div className="absolute inset-x-0 bottom-0 h-2/5 bg-gradient-to-t from-ink via-ink/60 to-transparent" />
<span className="absolute right-5 top-5 grid h-11 w-11 place-items-center rounded-full bg-ink/60 font-display text-base font-bold tabular-nums text-primary ring-1 ring-inset ring-white/15 backdrop-blur md:right-7 md:top-7">
{i + 1}
</span>
<div className="absolute bottom-5 left-5 right-5 md:bottom-8 md:left-8 md:right-8">
<div className="font-display text-sm font-semibold uppercase tracking-[0.18em] text-primary md:text-base">
{p.role}
</div>
{p.name ? (
<h3 className="mt-1.5 font-display text-2xl font-bold leading-tight text-white md:text-3xl">
{p.name}
</h3>
) : null}
</div>
</article>
))}
<div className="flex w-[70vw] shrink-0 snap-center flex-col justify-center sm:w-[45vw] lg:w-[28vw]">
<p className="font-display text-3xl font-bold leading-tight md:text-4xl">
Your company could be <Accent>next.</Accent>
</p>
<a
href="#contact"
className="press group mt-8 inline-flex w-fit 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"
>
Become a partner
<ArrowUpRight
className="h-4 w-4 transition group-hover:-translate-y-0.5 group-hover:translate-x-0.5"
aria-hidden
/>
</a>
</div>
</div>
{pinned ? (
<div className="container-x relative mt-8 flex items-center gap-6">
<span className="font-display text-sm font-semibold tabular-nums" aria-live="polite">
<span className="text-primary">{String(current + 1).padStart(2, "0")}</span>
<span className="text-muted-foreground">
{" "}
/ {String(TEAM.length).padStart(2, "0")}
</span>
</span>
<div className="h-px flex-1 overflow-hidden bg-white/10">
<div ref={barRef} className="h-full origin-left scale-x-0 bg-primary" />
</div>
</div>
) : null}
</div>
</section>
);
}
function Investors() {
return (
// overflow-x-clip (not overflow-hidden) so the pinned column can stick.
<section id="investors" className="section-y relative overflow-x-clip bg-ink">
<div className="glow glow-drift absolute -left-60 top-0 h-[40rem] w-[40rem] opacity-40" />
<div className="container-x relative grid gap-12 lg:grid-cols-12 lg:gap-16">
{/* The argument stays pinned while the reasons scroll past it. */}
<div className="lg:col-span-5">
<div className="lg:sticky lg:top-28" {...reveal("stagger")}>
<h2 className="font-display text-4xl font-bold leading-[1.02] md:text-5xl">
A diversified portfolio, <Accent>not a single bet.</Accent>
</h2>
<p className="mt-6 max-w-[65ch] text-lg leading-relaxed text-muted-foreground">
Rather than depending on one company, investors gain exposure to a portfolio of
ventures across multiple industries, spreading risk while multiplying the chances to
participate in high-growth markets.
</p>
<p className="mt-6 font-display text-xl font-semibold leading-snug md:text-2xl">
Instead of asking{" "}
<span className="text-muted-foreground">"Which startup will succeed?"</span>{" "}
<Accent>We build an ecosystem where many companies can succeed.</Accent>
</p>
<a
href="#contact"
className="press group mt-10 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>
<ol className="space-y-4 lg:col-span-7">
{INVESTOR_REASONS.map((r, i) => (
<li
key={r}
{...reveal()}
className="group relative flex items-center gap-6 overflow-hidden rounded-3xl border border-border bg-card/70 p-6 transition-colors duration-500 hover:border-primary/40 md:gap-8 md:p-8"
>
<span
aria-hidden
className="w-16 shrink-0 font-display text-4xl font-extrabold tabular-nums text-transparent transition-colors duration-500 [-webkit-text-stroke:1px_color-mix(in_oklab,var(--primary)_65%,transparent)] group-hover:text-primary md:w-20 md:text-5xl"
>
{String(i + 1).padStart(2, "0")}
</span>
<span className="font-display text-lg font-semibold md:text-2xl">{r}</span>
</li>
))}
</ol>
</div>
</section>
);
}
const INPUT_CLASS =
"mt-2 w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-base text-foreground placeholder:text-muted-foreground/60 transition focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/30";
type ContactStatus = "idle" | "sending" | "sent" | "mailto" | "error";
const CONTACT_STATUS_TEXT: Record<ContactStatus, string> = {
idle: "We respond to investor enquiries within 48 hours.",
sending: "Sending…",
sent: "Thank you. Your message is with our team and we'll be in touch within 48 hours.",
mailto: `Your email app should open with the message ready. If it doesn't, write to ${CONTACT_EMAIL}.`,
error: `Something went wrong. Please check your details, or write to ${CONTACT_EMAIL}.`,
};
function openMailto(data: ContactInput) {
const body = [
`Name: ${data.name}`,
`Organization: ${data.organization || "-"}`,
`Email: ${data.email}`,
`Interested in: ${data.interest}`,
"",
data.message ?? "",
].join("\n");
window.location.href = `mailto:${CONTACT_EMAIL}?subject=${encodeURIComponent(
`Enquiry: ${data.interest}`,
)}&body=${encodeURIComponent(body)}`;
}
function Contact() {
const [status, setStatus] = useState<ContactStatus>("idle");
async function onSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
const form = e.currentTarget;
const f = new FormData(form);
const data: ContactInput = {
name: String(f.get("name") ?? ""),
organization: String(f.get("organization") ?? ""),
email: String(f.get("email") ?? ""),
interest: String(f.get("interest")) as ContactInput["interest"],
message: String(f.get("message") ?? ""),
website: String(f.get("website") ?? ""),
};
setStatus("sending");
try {
const result = await sendContactEnquiry({ data });
if (result.ok) {
form.reset();
setStatus("sent");
} else if (result.reason === "not-configured") {
// No email service set up on the server yet, so the enquiry goes through the visitor's email app.
openMailto(data);
setStatus("mailto");
} else {
setStatus("error");
}
} catch (err) {
console.error(err);
setStatus("error");
}
}
return (
<section
id="contact"
className="sheet-enter theme-light section-y bg-background text-foreground"
>
<div className="container-x">
<div className="grid gap-12 md:grid-cols-12">
<div className="md:col-span-5" {...reveal("stagger")}>
<h2 className="font-display text-4xl font-bold leading-[1.05] md:text-5xl">
Build with us.
<br />
<Accent>Invest with us.</Accent>
</h2>
<p className="mt-6 text-lg leading-relaxed text-muted-foreground">
We work with institutional investors, family offices, and operators who want exposure
to India's next generation of AI-first companies.
</p>
<ul className="mt-10 divide-y divide-border border-y border-border">
<li>
<a href={`mailto:${CONTACT_EMAIL}`} className="group flex items-center gap-4 py-5">
<IconTile icon={Mail} />
<span className="flex-1">
<span className="block text-sm text-muted-foreground">Email</span>
<span className="font-display text-lg font-semibold transition-colors duration-300 group-hover:text-primary md:text-xl">
{CONTACT_EMAIL}
</span>
</span>
<ArrowUpRight
aria-hidden
className="h-5 w-5 text-primary transition-transform duration-300 group-hover:-translate-y-1 group-hover:translate-x-1"
/>
</a>
</li>
<li className="flex items-center gap-4 py-5">
<IconTile icon={MapPin} />
<span>
<span className="block text-sm text-muted-foreground">Studio</span>
<span className="font-display text-lg font-semibold md:text-xl">
Bangalore, India
</span>
</span>
</li>
</ul>
</div>
<div className="md:col-span-7" {...reveal("up", 200)}>
<form
onSubmit={onSubmit}
{...reveal("stagger", 300)}
className="theme-dark space-y-5 rounded-3xl bg-background p-8 text-foreground shadow-2xl md:p-10"
>
<h3 className="font-display text-2xl font-bold">Send us a message</h3>
<div className="grid gap-5 sm:grid-cols-2">
<label className="block text-sm font-medium">
Full name
<input
required
name="name"
type="text"
autoComplete="name"
className={INPUT_CLASS}
/>
</label>
<label className="block text-sm font-medium">
Organization
<input
name="organization"
type="text"
autoComplete="organization"
className={INPUT_CLASS}
/>
</label>
</div>
<label className="block text-sm font-medium">
Email
<input
required
name="email"
type="email"
autoComplete="email"
placeholder="you@company.com"
className={INPUT_CLASS}
/>
</label>
<label className="block text-sm font-medium">
I'm interested in
<select name="interest" className={INPUT_CLASS}>
{INTERESTS.map((i) => (
<option key={i} className="bg-card">
{i}
</option>
))}
</select>
</label>
<label className="block text-sm font-medium">
Message
<textarea
name="message"
rows={4}
placeholder="Tell us about what you're looking for…"
className={`${INPUT_CLASS} resize-none`}
/>
</label>
<input
type="text"
name="website"
tabIndex={-1}
autoComplete="off"
aria-hidden
className="absolute -left-[9999px] h-px w-px opacity-0"
/>
<button
type="submit"
disabled={status === "sending"}
className="group flex w-full items-center justify-center gap-2 rounded-full bg-primary px-6 py-4 font-display text-sm font-semibold text-primary-foreground press hover:brightness-110 disabled:cursor-wait disabled:opacity-60"
>
{status === "sending" ? "Sending…" : "Send message"}
<ArrowRight
className="h-4 w-4 transition group-hover:translate-x-0.5"
aria-hidden
/>
</button>
<p
className={`text-center text-xs ${
status === "error" ? "text-destructive" : "text-muted-foreground"
} ${status === "sent" ? "font-medium text-primary" : ""}`}
aria-live="polite"
>
<span
key={status}
className="inline-block duration-500 animate-in fade-in slide-in-from-bottom-1"
>
{CONTACT_STATUS_TEXT[status]}
</span>
</p>
</form>
</div>
</div>
</div>
</section>
);
}
function Footer() {
return (
<footer className="relative overflow-hidden bg-ink pt-20">
<div className="container-x">
<div className="grid gap-12 md:grid-cols-12" {...reveal("stagger")}>
<div className="md:col-span-6">
<img
src={logoFull}
alt="KapFoundry, Build Bold"
width={900}
height={697}
loading="lazy"
className="h-28 w-auto md:h-32"
/>
<p className="mt-6 max-w-md text-sm leading-relaxed text-muted-foreground">
We identify the problems, engineer the companies, and scale them with a shared engine,
building an ecosystem where many companies can succeed.
</p>
</div>
<nav aria-label="Footer" className="md:col-span-3">
<div className="mb-4 font-display text-xs font-semibold uppercase tracking-[0.16em] text-primary">
Explore
</div>
<ul className="grid grid-cols-2 gap-x-6 text-sm text-muted-foreground">
{NAV.map((n) => (
<li key={n.href}>
<a
href={n.href}
className="inline-flex min-h-10 min-w-10 items-center transition duration-300 hover:translate-x-1 hover:text-foreground"
>
{n.label}
</a>
</li>
))}
</ul>
</nav>
<div className="space-y-2 text-sm text-muted-foreground md:col-span-3">
<div className="mb-4 font-display text-xs font-semibold uppercase tracking-[0.16em] text-primary">
Studio
</div>
<div>Bangalore, India</div>
<a
href={`mailto:${CONTACT_EMAIL}`}
className="inline-flex min-h-10 items-center transition hover:text-primary"
>
{CONTACT_EMAIL}
</a>
</div>
</div>
</div>
<div
aria-hidden
{...reveal("mask")}
className="wordmark-fill mt-16 select-none text-center font-display text-[18vw] font-extrabold leading-[0.8] tracking-tighter text-transparent [-webkit-text-stroke:1px_rgb(255_255_255/0.12)]"
>
KAPFOUNDRY
</div>
<div className="border-t border-border py-6">
<div className="container-x text-xs text-muted-foreground">
© {new Date().getFullYear()} KapFoundry. All rights reserved.
</div>
</div>
</footer>
);
}
function BackToTop() {
const visible = useScrolledPast(800);
return (
<a
href="#top"
aria-label="Back to top"
className={`fixed bottom-6 right-6 z-40 inline-flex h-12 w-12 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg shadow-black/40 transition-all duration-300 hover:-translate-y-0.5 active:scale-95 ${
visible ? "opacity-100" : "pointer-events-none translate-y-4 opacity-0"
}`}
>
<ArrowUp className="h-5 w-5" aria-hidden />
</a>
);
}
/** Intro band for the About page. */
function AboutHero() {
return (
<section id="top" className="relative overflow-hidden bg-ink pb-20 pt-32 md:pb-28 md:pt-40">
<div className="mesh-gold absolute inset-0 opacity-30" aria-hidden />
<div className="dot-grid absolute inset-0" aria-hidden />
<div className="container-x relative grid items-end gap-12 lg:grid-cols-12">
<div className="lg:col-span-8">
<a
{...rise(80)}
href="/"
className="rise-in inline-flex min-h-10 items-center gap-2 text-sm text-muted-foreground transition-colors hover:text-primary"
>
<ArrowRight className="h-4 w-4 rotate-180" aria-hidden />
Back to home
</a>
<h1
{...rise(180)}
className="rise-in mt-6 font-display text-5xl font-extrabold leading-[0.98] sm:text-6xl lg:text-7xl"
>
We don't wait for the future. <Accent>We build it.</Accent>
</h1>
<p
{...rise(380)}
className="rise-in mt-8 max-w-[60ch] text-lg leading-relaxed text-muted-foreground md:text-xl"
>
A venture studio and technology holding company that engineers category-defining,
AI-first companies, before they even exist.
</p>
</div>
<div {...rise(520)} className="rise-in hidden justify-end lg:col-span-4 lg:flex">
<img
src={logoFull}
alt=""
aria-hidden
width={900}
height={697}
className="w-[min(22rem,100%)] drop-shadow-[0_40px_60px_oklch(0.05_0.01_60/0.7)]"
/>
</div>
</div>
</section>
);
}
/** Shared page shell: skip link, header, main content, footer and back-to-top. */
function SitePage({ page, children }: { page: SitePageName; children: ReactNode }) {
useScrollReveal();
// Arriving from another page with a hash (e.g. /#framework): the router restores scroll to
// the top after hydration, so jump to the requested section once it has settled.
useEffect(() => {
const id = decodeURIComponent(window.location.hash.slice(1));
if (!id) return;
const timer = window.setTimeout(() => {
document.getElementById(id)?.scrollIntoView({ behavior: "instant", block: "start" });
}, 60);
return () => window.clearTimeout(timer);
}, []);
return (
<>
<a
href="#main"
className="sr-only rounded-full bg-primary px-5 py-3 font-display text-sm font-semibold text-primary-foreground focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-[60]"
>
Skip to content
</a>
<Nav page={page} />
<main id="main" className="min-h-screen bg-background text-foreground">
{children}
</main>
<Footer />
<BackToTop />
</>
);
}
export {
SitePage,
Hero,
AboutHero,
Ticker,
WhoWeAre,
VisionMission,
Different,
StudioAdvantage,
Framework,
Portfolio,
AISection,
Team,
Investors,
Contact,
};