diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000..f1e47fa Binary files /dev/null and b/public/apple-touch-icon.png differ diff --git a/public/favicon.png b/public/favicon.png index 0ea6ef7..7caada3 100644 Binary files a/public/favicon.png and b/public/favicon.png differ diff --git a/src/assets/kapfoundry-logo-1.png b/src/assets/kapfoundry-logo-1.png new file mode 100644 index 0000000..0f84f79 Binary files /dev/null and b/src/assets/kapfoundry-logo-1.png differ diff --git a/src/assets/kapfoundry-logo-full.webp b/src/assets/kapfoundry-logo-full.webp new file mode 100644 index 0000000..f7e9b1c Binary files /dev/null and b/src/assets/kapfoundry-logo-full.webp differ diff --git a/src/assets/kapfoundry-mark.webp b/src/assets/kapfoundry-mark.webp new file mode 100644 index 0000000..e8d2035 Binary files /dev/null and b/src/assets/kapfoundry-mark.webp differ diff --git a/src/assets/kapfoundry-wordmark.webp b/src/assets/kapfoundry-wordmark.webp new file mode 100644 index 0000000..03f699f Binary files /dev/null and b/src/assets/kapfoundry-wordmark.webp differ diff --git a/src/components/site/sections.tsx b/src/components/site/sections.tsx new file mode 100644 index 0000000..5b36f65 --- /dev/null +++ b/src/components/site/sections.tsx @@ -0,0 +1,2569 @@ +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). +// PLACEHOLDER text: replace each role and blurb with the real team details. +const TEAM = [ + { + name: "", + role: "Role at KapFoundry", + blurb: "A short line on what this person leads at the studio.", + img: "/assets/4.jpeg", + }, + { + name: "", + role: "Role at KapFoundry", + blurb: "A short line on what this person leads at the studio.", + img: "/assets/1.png", + }, + { + name: "", + role: "Role at KapFoundry", + blurb: "A short line on what this person leads at the studio.", + img: "/assets/2.png", + }, + { + name: "", + role: "Role at KapFoundry", + blurb: "A short line on what this person leads at the studio.", + 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("[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 {children}; +} + +function Eyebrow({ icon: Icon, children }: { icon: LucideIcon; children: ReactNode }) { + return ( + + + {children} + + ); +} + +function IconTile({ icon: Icon, className = "" }: { icon: LucideIcon; className?: string }) { + return ( + + + + ); +} + +/** 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 ( +
+ {eyebrow && icon ? ( +
+ {eyebrow} +
+ ) : null} +

+ {title} +

+ {children ? ( +
+ {children} +
+ ) : null} +
+ ); +} + +function Nav({ page }: { page: SitePageName }) { + const scrolled = useScrolledPast(20); + const [scrollingDown, revealHeader] = useHideOnScroll(); + const active = useActiveSection(); + const [open, setOpen] = useState(false); + const toggleRef = useRef(null); + const overlayRef = useRef(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("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 ( + <> + + + {/* Full-screen menu for phones and tablets: a circle that opens from the menu button. */} +