Add venture detail pages, CropSphere showcase, and update Dockerfile
26
Dockerfile
@@ -1,5 +1,8 @@
|
|||||||
|
# Node 24 to match local development: package-lock.json written by a local `npm install`
|
||||||
|
# (npm 11) must also pass `npm ci` here, or the build fails with "lock file out of sync".
|
||||||
|
|
||||||
# 1. Build Stage
|
# 1. Build Stage
|
||||||
FROM node:22-alpine AS build
|
FROM node:24-alpine AS build
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -7,7 +10,7 @@ WORKDIR /app
|
|||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN npm ci
|
RUN npm ci --no-audit --no-fund
|
||||||
|
|
||||||
# Copy source code
|
# Copy source code
|
||||||
COPY . .
|
COPY . .
|
||||||
@@ -16,17 +19,24 @@ COPY . .
|
|||||||
RUN NITRO_PRESET=node-server npm run build
|
RUN NITRO_PRESET=node-server npm run build
|
||||||
|
|
||||||
# 2. Production Stage
|
# 2. Production Stage
|
||||||
FROM node:22-alpine
|
FROM node:24-alpine
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy built output from build stage
|
|
||||||
COPY --from=build /app/.output ./.output
|
|
||||||
|
|
||||||
EXPOSE 3000
|
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENV PORT=3000
|
ENV PORT=3000
|
||||||
ENV HOST=0.0.0.0
|
ENV HOST=0.0.0.0
|
||||||
|
|
||||||
|
# The Nitro output is self-contained (dependencies are bundled), so nothing else is copied.
|
||||||
|
COPY --from=build --chown=node:node /app/.output ./.output
|
||||||
|
|
||||||
|
# Run as the image's unprivileged user rather than root.
|
||||||
|
USER node
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Lets Dokploy/Docker see when the server is actually answering requests.
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||||
|
CMD wget -qO- http://127.0.0.1:3000/ >/dev/null || exit 1
|
||||||
|
|
||||||
CMD ["node", ".output/server/index.mjs"]
|
CMD ["node", ".output/server/index.mjs"]
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ All photos are from [Unsplash](https://unsplash.com) under the
|
|||||||
[Unsplash License](https://unsplash.com/license) (free for commercial use, no attribution
|
[Unsplash License](https://unsplash.com/license) (free for commercial use, no attribution
|
||||||
required; credited here anyway). Resized to 1200px WebP.
|
required; credited here anyway). Resized to 1200px WebP.
|
||||||
|
|
||||||
| File | Photographer | Unsplash photo |
|
| File | Photographer | Unsplash photo |
|
||||||
| ---------------------- | ------------------ | --------------------------------------- |
|
| ----------------------------- | ------------------ | --------------------------------------- |
|
||||||
| `studio-foundry.webp` | Morteza Mohammadi | https://unsplash.com/photos/ZLzkwU49BdY |
|
| `pillar-studio-team.webp` | Annie Spratt | https://unsplash.com/photos/MChSQHxGZrQ |
|
||||||
| `holding-tower.webp` | Verne Ho | https://unsplash.com/photos/0LAJfSNa-xQ |
|
| `pillar-holding-hq.webp` | Juzzepo | https://unsplash.com/photos/vcee2hRsjl0 |
|
||||||
| `compound-spiral.webp` | Jean-Luc Benazet | https://unsplash.com/photos/dLpP34ADQPI |
|
| `pillar-compound-growth.webp` | Carlos Muza | https://unsplash.com/photos/hpjSkU2UYSU |
|
||||||
| `ai-vision.webp` | Yael Yañez | https://unsplash.com/photos/jw245ZR2lz4 |
|
| `ai-vision.webp` | Yael Yañez | https://unsplash.com/photos/jw245ZR2lz4 |
|
||||||
| `ai-agentic.webp` | Gabriele Malaspina | https://unsplash.com/photos/CjWsslYVnPI |
|
| `ai-agentic.webp` | Gabriele Malaspina | https://unsplash.com/photos/CjWsslYVnPI |
|
||||||
| `ai-enterprise.webp` | Winston Chen | https://unsplash.com/photos/iZe21DzHnUg |
|
| `ai-enterprise.webp` | Winston Chen | https://unsplash.com/photos/iZe21DzHnUg |
|
||||||
| `ai-predictive.webp` | Tötös Ádám | https://unsplash.com/photos/3r8rcSy0Ffg |
|
| `ai-predictive.webp` | Tötös Ádám | https://unsplash.com/photos/3r8rcSy0Ffg |
|
||||||
| `ai-llm.webp` | Andrew Seaman | https://unsplash.com/photos/-m88z7ily-w |
|
| `ai-llm.webp` | Andrew Seaman | https://unsplash.com/photos/-m88z7ily-w |
|
||||||
| `ai-automation.webp` | Simon Kadula | https://unsplash.com/photos/8gr6bObQLOI |
|
| `ai-automation.webp` | Simon Kadula | https://unsplash.com/photos/8gr6bObQLOI |
|
||||||
| `ai-multiagent.webp` | James Wainscoat | https://unsplash.com/photos/b7MZ6iGIoSI |
|
| `ai-multiagent.webp` | James Wainscoat | https://unsplash.com/photos/b7MZ6iGIoSI |
|
||||||
| `ai-decision.webp` | Hassan Pasha | https://unsplash.com/photos/7SjEuEF06Zw |
|
| `ai-decision.webp` | Hassan Pasha | https://unsplash.com/photos/7SjEuEF06Zw |
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 223 KiB |
|
Before Width: | Height: | Size: 81 KiB |
BIN
src/assets/cards/pillar-compound-growth.webp
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
src/assets/cards/pillar-holding-hq.webp
Normal file
|
After Width: | Height: | Size: 213 KiB |
BIN
src/assets/cards/pillar-studio-team.webp
Normal file
|
After Width: | Height: | Size: 321 KiB |
|
Before Width: | Height: | Size: 155 KiB |
@@ -12,21 +12,21 @@ required; credited here anyway). Resized WebP.
|
|||||||
| `stage-launch.webp` | SpaceX | https://unsplash.com/photos/TV2gg2kZD1o |
|
| `stage-launch.webp` | SpaceX | https://unsplash.com/photos/TV2gg2kZD1o |
|
||||||
| `stage-scale.webp` | Mara F | https://unsplash.com/photos/tZdFQvqJuQQ |
|
| `stage-scale.webp` | Mara F | https://unsplash.com/photos/tZdFQvqJuQQ |
|
||||||
| `stage-spinoff.webp` | Curioso Photography | https://unsplash.com/photos/OOiygaquXZs |
|
| `stage-spinoff.webp` | Curioso Photography | https://unsplash.com/photos/OOiygaquXZs |
|
||||||
| `stack-engineering.webp` | Manuel | https://unsplash.com/photos/CANL3bzp6wU |
|
| `stack-engineering.webp` | Peaky Frames | https://unsplash.com/photos/GAoGLvYLt3c |
|
||||||
| `stack-design.webp` | Amélie Mourichon | https://unsplash.com/photos/sv8oOQaUb-o |
|
| `stack-design.webp` | Amélie Mourichon | https://unsplash.com/photos/sv8oOQaUb-o |
|
||||||
| `stack-cloud.webp` | Rebecca Campbell | https://unsplash.com/photos/5nN9NX6dJxc |
|
| `stack-cloud.webp` | Eric Stoynov | https://unsplash.com/photos/UfR_yqIcd4g |
|
||||||
| `stack-legal.webp` | Sasun Bughdaryan | https://unsplash.com/photos/qVOJqYI37qE |
|
| `stack-legal.webp` | Sasun Bughdaryan | https://unsplash.com/photos/qVOJqYI37qE |
|
||||||
| `stack-finance.webp` | Kelly Sikkema | https://unsplash.com/photos/M98NRBuzbpc |
|
| `stack-finance.webp` | Kelly Sikkema | https://unsplash.com/photos/M98NRBuzbpc |
|
||||||
| `stack-marketing.webp` | Kayle Kaupanger | https://unsplash.com/photos/GVi0ijl8ExM |
|
| `stack-marketing.webp` | Kayle Kaupanger | https://unsplash.com/photos/GVi0ijl8ExM |
|
||||||
| `stack-sales.webp` | Rock Staar | https://unsplash.com/photos/NzIV4vOBA7s |
|
| `stack-sales.webp` | Rock Staar | https://unsplash.com/photos/NzIV4vOBA7s |
|
||||||
| `stack-growth.webp` | Sable Flow | https://unsplash.com/photos/T74mVg__F_k |
|
| `stack-growth.webp` | Sable Flow | https://unsplash.com/photos/T74mVg__F_k |
|
||||||
| `stack-data.webp` | Luke Chesser | https://unsplash.com/photos/JKUTrJ4vK00 |
|
| `stack-data.webp` | Resource Database | https://unsplash.com/photos/TIUyoNGM0no |
|
||||||
| `stack-governance.webp` | Danielle Cerullo | https://unsplash.com/photos/bIZJRVBLfOM |
|
| `stack-governance.webp` | Danielle Cerullo | https://unsplash.com/photos/bIZJRVBLfOM |
|
||||||
| `vision-city.webp` | Pradeep Gopal (Bengaluru) | https://unsplash.com/photos/4J3CHmrFiIg |
|
| `vision-city.webp` | Pradeep Gopal (Bengaluru) | https://unsplash.com/photos/4J3CHmrFiIg |
|
||||||
| `investors-skyline.webp` | Himmel S | https://unsplash.com/photos/Io1lz5bZ0as |
|
| `investors-skyline.webp` | Himmel S | https://unsplash.com/photos/Io1lz5bZ0as |
|
||||||
| `principle-evidence.webp` | Mika Baumeister | https://unsplash.com/photos/Wpnoqo2plFA |
|
| `principle-evidence.webp` | Mika Baumeister | https://unsplash.com/photos/Wpnoqo2plFA |
|
||||||
| `principle-engine.webp` | Nikita Turkovich | https://unsplash.com/photos/ioxS2dYRiJw |
|
| `principle-engine.webp` | Nikita Turkovich | https://unsplash.com/photos/ioxS2dYRiJw |
|
||||||
| `principle-standalone.webp` | Nathan Jennings | https://unsplash.com/photos/VsPsf4F5Pi0 |
|
| `principle-standalone.webp` | Nathan Jennings | https://unsplash.com/photos/VsPsf4F5Pi0 |
|
||||||
| `model-build.webp` | Art Institute of Chicago | https://unsplash.com/photos/sAlWjm2huck |
|
| `model-build.webp` | Lucas Kepner | https://unsplash.com/photos/Yn8D5B8C-eY |
|
||||||
| `model-spinoff.webp` | Markus Spiske | https://unsplash.com/photos/vrbZVyX2k4I |
|
| `model-spinoff.webp` | Markus Spiske | https://unsplash.com/photos/vrbZVyX2k4I |
|
||||||
| `model-compound.webp` | Marek Studzinski | https://unsplash.com/photos/tnT54YSnlL0 |
|
| `model-compound.webp` | Marek Studzinski | https://unsplash.com/photos/tnT54YSnlL0 |
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 194 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 285 KiB After Width: | Height: | Size: 118 KiB |
766
src/components/site/cropsphere.tsx
Normal file
@@ -0,0 +1,766 @@
|
|||||||
|
import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from "react";
|
||||||
|
import {
|
||||||
|
ArrowDown,
|
||||||
|
ArrowRight,
|
||||||
|
BrainCircuit,
|
||||||
|
Coins,
|
||||||
|
Database,
|
||||||
|
Globe,
|
||||||
|
Recycle,
|
||||||
|
ShoppingBasket,
|
||||||
|
Sprout,
|
||||||
|
Store,
|
||||||
|
User,
|
||||||
|
Wheat,
|
||||||
|
type LucideIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
import {
|
||||||
|
Accent,
|
||||||
|
EASE_STAGE,
|
||||||
|
FillWords,
|
||||||
|
SectionHeader,
|
||||||
|
reveal,
|
||||||
|
usePinnedLayout,
|
||||||
|
} from "@/components/site/sections";
|
||||||
|
|
||||||
|
// Cropsphere's story, adapted from the founding team's own description. Plans and potential
|
||||||
|
// revenue are worded as such; nothing here claims traction.
|
||||||
|
|
||||||
|
const CHAIN = ["Farm", "Food", "Consumer", "Regeneration"];
|
||||||
|
|
||||||
|
const LAYERS: { layer: string; product: string; role: string; icon: LucideIcon }[] = [
|
||||||
|
{
|
||||||
|
layer: "Agriculture",
|
||||||
|
product: "HarvestHQ",
|
||||||
|
role: "An AI + IoT agricultural operating system.",
|
||||||
|
icon: Sprout,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
layer: "Commerce",
|
||||||
|
product: "Cropsphere Produce",
|
||||||
|
role: "A traceable farm-to-consumer marketplace.",
|
||||||
|
icon: ShoppingBasket,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
layer: "Regeneration",
|
||||||
|
product: "O-Points",
|
||||||
|
role: "Rewards and measures regenerative actions.",
|
||||||
|
icon: Recycle,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
layer: "Consumer",
|
||||||
|
product: "Living",
|
||||||
|
role: "The physical consumer and retail experience layer.",
|
||||||
|
icon: Store,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Section 1: what Cropsphere is, and the four layers it connects. */
|
||||||
|
function Overview() {
|
||||||
|
return (
|
||||||
|
<section className="sheet-enter theme-light section-y bg-background text-foreground">
|
||||||
|
<div className="container-x">
|
||||||
|
<SectionHeader
|
||||||
|
eyebrow="What is Cropsphere"
|
||||||
|
icon={Sprout}
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
Not an app. Not a marketplace.{" "}
|
||||||
|
<Accent>An operating ecosystem for regenerative agriculture.</Accent>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
compact
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Cropsphere is an integrated regenerative-agriculture operating ecosystem. It connects
|
||||||
|
four things that operate separately today:
|
||||||
|
</p>
|
||||||
|
</SectionHeader>
|
||||||
|
|
||||||
|
{/* Farm → Food → Consumer → Regeneration */}
|
||||||
|
{/* Each step lights up in turn as the row crosses the screen (.chain-step). */}
|
||||||
|
<ol className="mt-10 flex flex-wrap items-center gap-3 font-display text-xl font-bold md:text-3xl">
|
||||||
|
{CHAIN.map((c, i) => (
|
||||||
|
<li
|
||||||
|
key={c}
|
||||||
|
className="chain-step flex items-center gap-3"
|
||||||
|
style={{ "--i": i } as CSSProperties}
|
||||||
|
>
|
||||||
|
<span className="rounded-full border border-primary/40 bg-primary/10 px-5 py-2.5 text-primary">
|
||||||
|
{c}
|
||||||
|
</span>
|
||||||
|
{i < CHAIN.length - 1 ? (
|
||||||
|
<ArrowRight className="h-5 w-5 text-foreground/40" aria-hidden />
|
||||||
|
) : null}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<blockquote
|
||||||
|
{...reveal("up", 120)}
|
||||||
|
className="mt-14 max-w-4xl border-l-2 border-primary pl-6 font-display text-2xl font-semibold leading-snug tracking-tight md:pl-8 md:text-4xl"
|
||||||
|
>
|
||||||
|
Use technology to make farming more intelligent, make food more traceable, and make
|
||||||
|
regenerative actions economically valuable.
|
||||||
|
</blockquote>
|
||||||
|
|
||||||
|
<div className="mt-20">
|
||||||
|
<div className="flex flex-wrap items-end justify-between gap-4">
|
||||||
|
<h3 className="font-display text-3xl font-bold md:text-4xl">
|
||||||
|
Four layers, one system.
|
||||||
|
</h3>
|
||||||
|
<span className="font-display text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
||||||
|
Regenerative Agri Operating Ecosystem (RAOE)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/* The layers stack on top of each other as you scroll (.layer-stack). */}
|
||||||
|
<ul className="layer-stack mt-8 flex flex-col gap-5 pb-[10vh]">
|
||||||
|
{LAYERS.map((l, i) => {
|
||||||
|
const Icon = l.icon;
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={l.product}
|
||||||
|
style={{ "--i": i } as CSSProperties}
|
||||||
|
className="theme-dark group relative flex min-h-[13rem] flex-col justify-between gap-6 overflow-hidden rounded-3xl border border-white/10 bg-background p-7 text-foreground shadow-[0_-24px_60px_-30px_oklch(0.05_0.01_60/0.8)] md:flex-row md:items-center md:p-10"
|
||||||
|
>
|
||||||
|
<div className="glow absolute -right-16 -top-16 h-56 w-56 opacity-40 transition-opacity duration-500 group-hover:opacity-90" />
|
||||||
|
<div className="relative flex items-center gap-5">
|
||||||
|
<span className="grid h-14 w-14 shrink-0 place-items-center rounded-2xl bg-primary text-primary-foreground">
|
||||||
|
<Icon className="h-6 w-6" aria-hidden />
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<div className="font-display text-xs font-semibold uppercase tracking-[0.16em] text-primary">
|
||||||
|
0{i + 1} · {l.layer}
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 font-display text-3xl font-bold tracking-tight md:text-5xl">
|
||||||
|
{l.product}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="relative text-lg leading-relaxed text-muted-foreground md:max-w-sm md:text-right">
|
||||||
|
{l.role}
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A small left-to-right (wrapping) chain of steps. */
|
||||||
|
function Chain({ items, tone = "dark" }: { items: string[]; tone?: "dark" | "gold" }) {
|
||||||
|
return (
|
||||||
|
<ol className="mt-5 flex flex-wrap items-center gap-2">
|
||||||
|
{items.map((t, i) => (
|
||||||
|
<li key={t} className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className={`rounded-full px-3.5 py-1.5 text-sm ${
|
||||||
|
tone === "gold"
|
||||||
|
? "bg-primary text-primary-foreground"
|
||||||
|
: "border border-white/10 bg-white/[0.05] text-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{t}
|
||||||
|
</span>
|
||||||
|
{i < items.length - 1 ? (
|
||||||
|
<ArrowRight className="h-3.5 w-3.5 text-muted-foreground" aria-hidden />
|
||||||
|
) : null}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const STEPS: { n: string; title: string; product: string; body: ReactNode }[] = [
|
||||||
|
{
|
||||||
|
n: "01",
|
||||||
|
title: "Connect farms",
|
||||||
|
product: "Network",
|
||||||
|
body: (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
Farmers and landowners join the Cropsphere network. HarvestHQ gathers what each farm needs
|
||||||
|
to be understood, combining AI, IoT, satellite and farm-level data to support agricultural
|
||||||
|
decisions.
|
||||||
|
</p>
|
||||||
|
<Chain
|
||||||
|
items={[
|
||||||
|
"Land and crop information",
|
||||||
|
"Soil and environmental data",
|
||||||
|
"Weather",
|
||||||
|
"Farm activity",
|
||||||
|
"Crop images",
|
||||||
|
"Market demand",
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
n: "02",
|
||||||
|
title: "HarvestHQ helps run the farm",
|
||||||
|
product: "HarvestHQ",
|
||||||
|
body: (
|
||||||
|
<>
|
||||||
|
<p>The intelligence layer, across the whole season:</p>
|
||||||
|
<Chain
|
||||||
|
tone="gold"
|
||||||
|
items={[
|
||||||
|
"Crop planning",
|
||||||
|
"Irrigation",
|
||||||
|
"Disease monitoring",
|
||||||
|
"Input optimisation",
|
||||||
|
"Yield prediction",
|
||||||
|
"Market planning",
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<div className="mt-6 rounded-2xl border border-white/10 bg-white/[0.04] p-5">
|
||||||
|
<div className="font-display text-xs font-semibold uppercase tracking-[0.16em] text-primary">
|
||||||
|
For example
|
||||||
|
</div>
|
||||||
|
<p className="mt-2">
|
||||||
|
A farmer has two acres. Instead of only asking “What should I grow?”, HarvestHQ can
|
||||||
|
analyse the farm's information and help work out{" "}
|
||||||
|
<span className="font-semibold text-foreground">
|
||||||
|
what to grow, how to grow it, how much input to use, the expected yield and the likely
|
||||||
|
market demand.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p className="mt-6">
|
||||||
|
The long-term vision is to move from advice alone to an operating system that connects
|
||||||
|
recommendations with execution and automation.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
n: "03",
|
||||||
|
title: "Cropsphere Produce takes it to market",
|
||||||
|
product: "Cropsphere Produce",
|
||||||
|
body: (
|
||||||
|
<>
|
||||||
|
<p>Once production is connected, the next question is where the produce goes.</p>
|
||||||
|
<div className="mt-6 grid gap-4 md:grid-cols-2">
|
||||||
|
<div className="rounded-2xl border border-white/10 p-5">
|
||||||
|
<div className="font-display text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
||||||
|
Today
|
||||||
|
</div>
|
||||||
|
<Chain items={["Farmer", "Many intermediaries", "Retailer", "Consumer"]} />
|
||||||
|
</div>
|
||||||
|
<div className="rounded-2xl border border-primary/40 bg-primary/10 p-5">
|
||||||
|
<div className="font-display text-xs font-semibold uppercase tracking-[0.16em] text-primary">
|
||||||
|
With Cropsphere
|
||||||
|
</div>
|
||||||
|
<Chain tone="gold" items={["Farm", "Cropsphere", "Consumer"]} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="mt-6">
|
||||||
|
With digital traceability, a consumer could see which farm, what was grown and how, right
|
||||||
|
through to purchase. The marketplace can support subscriptions, quick commerce, standard
|
||||||
|
e-commerce, farm experiences and institutional or B2B supply.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
n: "04",
|
||||||
|
title: "O-Points rewards regeneration",
|
||||||
|
product: "O-Points",
|
||||||
|
body: (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
Whenever people take part in regenerative activity, they can earn O-Points, turning
|
||||||
|
otherwise invisible environmental actions into a measurable, digital layer of
|
||||||
|
participation.
|
||||||
|
</p>
|
||||||
|
<ul className="mt-6 grid gap-3 sm:grid-cols-2">
|
||||||
|
{[
|
||||||
|
"Buy produce",
|
||||||
|
"Plant a sapling or seedball",
|
||||||
|
"Register and verify an existing tree",
|
||||||
|
"Join regenerative activities",
|
||||||
|
].map((a) => (
|
||||||
|
<li
|
||||||
|
key={a}
|
||||||
|
className="flex items-center justify-between gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-5 py-4"
|
||||||
|
>
|
||||||
|
<span className="text-foreground">{a}</span>
|
||||||
|
<span className="shrink-0 rounded-full bg-primary/15 px-3 py-1 text-xs font-semibold text-primary">
|
||||||
|
+ O-Points
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<p className="mt-6">
|
||||||
|
Behind it: reward logic, verification, a ledger and wallet, and over time a mechanism for
|
||||||
|
aggregating carbon value.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
n: "05",
|
||||||
|
title: "Living brings it to the consumer",
|
||||||
|
product: "Living",
|
||||||
|
body: (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
Living is Cropsphere's physical, offline layer: hubs, product sampling, farm experiences,
|
||||||
|
wellness and consumer engagement. Its first touchpoint is a vending-machine network in
|
||||||
|
tier-1 and tier-2 cities.
|
||||||
|
</p>
|
||||||
|
<Chain
|
||||||
|
items={[
|
||||||
|
"A consumer buys a smoothie or produce",
|
||||||
|
"The sale earns revenue",
|
||||||
|
"They discover Cropsphere",
|
||||||
|
"They join O-Points",
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<p className="mt-6 font-display text-xl font-semibold text-foreground">
|
||||||
|
Not merely a vending machine: revenue, brand, customer acquisition and an entry point to
|
||||||
|
the ecosystem, in one.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Section 2: the five steps, each pinned beside its number as you scroll. */
|
||||||
|
function HowItWorks() {
|
||||||
|
return (
|
||||||
|
<section className="section-y relative bg-ink">
|
||||||
|
<div className="container-x">
|
||||||
|
<SectionHeader
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
How it <Accent>actually works.</Accent>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p>Four products, one flywheel. Here is the path from a farm to a consumer and back.</p>
|
||||||
|
</SectionHeader>
|
||||||
|
|
||||||
|
{/* A stem grows down the steps as you read; each step's node lights as it passes. */}
|
||||||
|
<ol className="relative mt-16 pl-10 md:pl-16">
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className="absolute bottom-0 left-[11px] top-0 w-px bg-white/10 md:left-[19px]"
|
||||||
|
>
|
||||||
|
<span className="stem-grow block h-full w-full bg-gradient-to-b from-primary via-primary to-primary/30" />
|
||||||
|
</span>
|
||||||
|
{STEPS.map((s) => (
|
||||||
|
<li
|
||||||
|
key={s.n}
|
||||||
|
className="relative grid gap-6 border-t border-white/10 py-12 md:py-16 lg:grid-cols-12 lg:gap-12"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className="stem-node absolute -left-10 top-14 grid h-6 w-6 place-items-center rounded-full border-2 border-primary bg-primary md:-left-16 md:top-[4.5rem] md:h-10 md:w-10"
|
||||||
|
>
|
||||||
|
<Sprout className="h-3 w-3 text-ink md:h-4 md:w-4" />
|
||||||
|
</span>
|
||||||
|
<div className="lg:col-span-4">
|
||||||
|
<div className="lg:sticky lg:top-32" {...reveal("stagger")}>
|
||||||
|
<span className="font-display text-6xl font-extrabold tabular-nums text-transparent [-webkit-text-stroke:1px_color-mix(in_oklab,var(--primary)_75%,transparent)] md:text-7xl">
|
||||||
|
{s.n}
|
||||||
|
</span>
|
||||||
|
<h3 className="mt-4 font-display text-3xl font-bold leading-tight md:text-4xl">
|
||||||
|
{s.title}
|
||||||
|
</h3>
|
||||||
|
<span className="mt-4 inline-block rounded-full bg-primary/15 px-3 py-1 font-display text-xs font-semibold uppercase tracking-[0.14em] text-primary">
|
||||||
|
{s.product}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
{...reveal("up", 100)}
|
||||||
|
className="space-y-0 text-lg leading-relaxed text-muted-foreground lg:col-span-8"
|
||||||
|
>
|
||||||
|
{s.body}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const FLYWHEEL: { label: string; icon: LucideIcon; desc: string }[] = [
|
||||||
|
{ label: "Farm", icon: Sprout, desc: "HarvestHQ improves agricultural intelligence." },
|
||||||
|
{ label: "Food", icon: Wheat, desc: "Cropsphere Produce brings verified production to market." },
|
||||||
|
{ label: "Consumer", icon: User, desc: "Consumers purchase and take part." },
|
||||||
|
{
|
||||||
|
label: "O-Points",
|
||||||
|
icon: Recycle,
|
||||||
|
desc: "Their regenerative actions are recorded and rewarded.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Impact",
|
||||||
|
icon: Globe,
|
||||||
|
desc: "Regenerative activity creates measurable environmental data.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Value",
|
||||||
|
icon: Coins,
|
||||||
|
desc: "That data and activity can support future carbon and ESG monetisation.",
|
||||||
|
},
|
||||||
|
{ label: "Data", icon: Database, desc: "More farms, transactions and users create more data." },
|
||||||
|
{
|
||||||
|
label: "Better intelligence",
|
||||||
|
icon: BrainCircuit,
|
||||||
|
desc: "More data sharpens how the platform understands farms, demand and regeneration. More farms join, and the cycle repeats.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const SPIN_MS = 2800;
|
||||||
|
|
||||||
|
/** Place n items evenly on a circle (radius in % of the box), starting at 12 o'clock. */
|
||||||
|
function onRing(i: number, n: number, radius: number): CSSProperties {
|
||||||
|
const a = ((-90 + (i * 360) / n) * Math.PI) / 180;
|
||||||
|
return {
|
||||||
|
left: `${(50 + radius * Math.cos(a)).toFixed(2)}%`,
|
||||||
|
top: `${(50 + radius * Math.sin(a)).toFixed(2)}%`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll length per flywheel stage while the section is pinned.
|
||||||
|
const FLYWHEEL_STEP_SVH = 32;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Section 3: the flywheel. Large screens: the section pins and scrolling turns the wheel one
|
||||||
|
* stage at a time (pointer and keyboard can also pick a stage); the centre explains it. Short
|
||||||
|
* screens and reduced motion: the wheel advances on its own. Small screens: a looping list.
|
||||||
|
*/
|
||||||
|
function Flywheel() {
|
||||||
|
const [active, setActive] = useState(0);
|
||||||
|
const [held, setHeld] = useState(false);
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const sectionRef = useRef<HTMLElement>(null);
|
||||||
|
const ringRef = useRef<SVGGElement>(null);
|
||||||
|
const n = FLYWHEEL.length;
|
||||||
|
const pinned = usePinnedLayout("(min-width: 1024px)");
|
||||||
|
|
||||||
|
// Pinned: scroll position picks the stage and spins the outer ring.
|
||||||
|
useEffect(() => {
|
||||||
|
const section = sectionRef.current;
|
||||||
|
if (!pinned || !section) return;
|
||||||
|
let frame = 0;
|
||||||
|
const update = () => {
|
||||||
|
frame = 0;
|
||||||
|
const distance = section.offsetHeight - window.innerHeight;
|
||||||
|
const p =
|
||||||
|
distance > 0
|
||||||
|
? Math.min(1, Math.max(0, -section.getBoundingClientRect().top / distance))
|
||||||
|
: 0;
|
||||||
|
setActive(Math.min(n - 1, Math.floor(p * n)));
|
||||||
|
if (ringRef.current) ringRef.current.style.transform = `rotate(${(p * 120).toFixed(2)}deg)`;
|
||||||
|
};
|
||||||
|
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, n]);
|
||||||
|
|
||||||
|
// Not pinned: advance on a timer while in view.
|
||||||
|
useEffect(() => {
|
||||||
|
const el = ref.current;
|
||||||
|
if (!el || pinned || held || window.matchMedia("(prefers-reduced-motion: reduce)").matches)
|
||||||
|
return;
|
||||||
|
let timer = 0;
|
||||||
|
const io = new IntersectionObserver(([e]) => {
|
||||||
|
window.clearInterval(timer);
|
||||||
|
if (e.isIntersecting)
|
||||||
|
timer = window.setInterval(() => setActive((a) => (a + 1) % n), SPIN_MS);
|
||||||
|
});
|
||||||
|
io.observe(el);
|
||||||
|
return () => {
|
||||||
|
io.disconnect();
|
||||||
|
window.clearInterval(timer);
|
||||||
|
};
|
||||||
|
}, [pinned, held, n]);
|
||||||
|
|
||||||
|
const current = FLYWHEEL[active];
|
||||||
|
const CurrentIcon = current.icon;
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
ref={sectionRef}
|
||||||
|
className={`sheet-enter theme-light relative bg-background text-foreground ${pinned ? "" : "section-y"}`}
|
||||||
|
style={pinned ? { height: `${100 + n * FLYWHEEL_STEP_SVH}svh` } : undefined}
|
||||||
|
>
|
||||||
|
<div className={pinned ? "sticky top-0 flex h-[100svh] items-center pt-16" : ""}>
|
||||||
|
<div className="container-x grid w-full items-center gap-14 lg:grid-cols-12">
|
||||||
|
<div className="lg:col-span-5" {...reveal("stagger")}>
|
||||||
|
<span className="eyebrow">The flywheel</span>
|
||||||
|
<h2 className="mt-5 font-display text-4xl font-bold leading-[1.04] md:text-5xl">
|
||||||
|
Every turn makes the <Accent>next one stronger.</Accent>
|
||||||
|
</h2>
|
||||||
|
<p className="mt-6 max-w-[48ch] text-lg leading-relaxed text-muted-foreground">
|
||||||
|
Cropsphere is a flywheel, not four separate products. Better farming creates better
|
||||||
|
food; engaged consumers create data and revenue; regenerative activity creates
|
||||||
|
measurable environmental value, and all of it brings more farms in.
|
||||||
|
</p>
|
||||||
|
{pinned ? (
|
||||||
|
<div className="mt-10 flex items-center gap-5">
|
||||||
|
<span className="font-display text-sm font-semibold tabular-nums">
|
||||||
|
<span className="text-primary">{String(active + 1).padStart(2, "0")}</span>
|
||||||
|
<span className="text-muted-foreground"> / {String(n).padStart(2, "0")}</span>
|
||||||
|
</span>
|
||||||
|
<span className="h-px flex-1 overflow-hidden bg-foreground/10">
|
||||||
|
<span
|
||||||
|
className="block h-full origin-left bg-primary transition-transform duration-500"
|
||||||
|
style={{ transform: `scaleX(${(active + 1) / n})` }}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className="font-display text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
||||||
|
Scroll to turn
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Wheel (large screens) */}
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className="relative mx-auto hidden aspect-square w-full max-w-[34rem] lg:col-span-7 lg:block"
|
||||||
|
onPointerEnter={() => setHeld(true)}
|
||||||
|
onPointerLeave={() => setHeld(false)}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 400 400"
|
||||||
|
className="absolute inset-0 h-full w-full overflow-visible"
|
||||||
|
aria-hidden
|
||||||
|
>
|
||||||
|
{/* Outer dashed ring: spins as the flywheel is scrolled. */}
|
||||||
|
<g ref={ringRef} style={{ transformOrigin: "200px 200px" }}>
|
||||||
|
<circle
|
||||||
|
cx="200"
|
||||||
|
cy="200"
|
||||||
|
r="194"
|
||||||
|
fill="none"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeDasharray="2 12"
|
||||||
|
className="stroke-primary/50"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<circle
|
||||||
|
cx="200"
|
||||||
|
cy="200"
|
||||||
|
r="168"
|
||||||
|
fill="none"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
className="stroke-foreground/10"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="200"
|
||||||
|
cy="200"
|
||||||
|
r="168"
|
||||||
|
fill="none"
|
||||||
|
strokeWidth="3"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeDasharray={2 * Math.PI * 168}
|
||||||
|
strokeDashoffset={2 * Math.PI * 168 * (1 - (active + 1) / n)}
|
||||||
|
transform="rotate(-90 200 200)"
|
||||||
|
className="stroke-primary transition-[stroke-dashoffset] duration-700"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{FLYWHEEL.map((f, i) => {
|
||||||
|
const Icon = f.icon;
|
||||||
|
const on = i === active;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={f.label}
|
||||||
|
type="button"
|
||||||
|
style={onRing(i, n, 42)}
|
||||||
|
onPointerEnter={() => setActive(i)}
|
||||||
|
onFocus={() => {
|
||||||
|
setHeld(true);
|
||||||
|
setActive(i);
|
||||||
|
}}
|
||||||
|
onBlur={() => setHeld(false)}
|
||||||
|
aria-label={`${f.label}: ${f.desc}`}
|
||||||
|
aria-pressed={on}
|
||||||
|
className="group absolute flex -translate-x-1/2 -translate-y-1/2 flex-col items-center gap-2"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`grid h-14 w-14 place-items-center rounded-2xl border transition-all duration-500 ${EASE_STAGE} ${
|
||||||
|
on
|
||||||
|
? "scale-110 border-primary bg-primary text-primary-foreground shadow-lg shadow-primary/30"
|
||||||
|
: "border-border bg-card text-primary group-hover:border-primary/60"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon className="h-5 w-5" aria-hidden />
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`whitespace-nowrap font-display text-xs font-semibold uppercase tracking-wider transition-colors ${
|
||||||
|
on ? "text-foreground" : "text-muted-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{f.label}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<div className="absolute inset-[24%] grid place-items-center text-center">
|
||||||
|
<div key={active} className="duration-700 animate-in fade-in zoom-in-95">
|
||||||
|
<CurrentIcon className="mx-auto h-8 w-8 text-primary" aria-hidden />
|
||||||
|
<div className="mt-3 font-display text-2xl font-bold">{current.label}</div>
|
||||||
|
<p className="mx-auto mt-2 max-w-[22ch] text-sm leading-snug text-muted-foreground">
|
||||||
|
{current.desc}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Loop list (small screens) */}
|
||||||
|
<ol className="space-y-2 lg:hidden" {...reveal("stagger")}>
|
||||||
|
{FLYWHEEL.map((f, i) => {
|
||||||
|
const Icon = f.icon;
|
||||||
|
return (
|
||||||
|
<li key={f.label}>
|
||||||
|
<div className="flex items-start gap-4 rounded-2xl border border-border bg-card p-4">
|
||||||
|
<span className="grid h-11 w-11 shrink-0 place-items-center rounded-xl bg-primary text-primary-foreground">
|
||||||
|
<Icon className="h-5 w-5" aria-hidden />
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<span className="block font-display text-lg font-bold">{f.label}</span>
|
||||||
|
<span className="block text-sm leading-snug text-muted-foreground">
|
||||||
|
{f.desc}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{i < FLYWHEEL.length - 1 ? (
|
||||||
|
<ArrowDown className="mx-auto my-1 h-4 w-4 text-primary/60" aria-hidden />
|
||||||
|
) : null}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<li className="pt-2 text-center font-display text-sm font-semibold text-primary">
|
||||||
|
↺ More farms join, and the cycle repeats
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const REVENUE: { product: string; items: string[] }[] = [
|
||||||
|
{
|
||||||
|
product: "HarvestHQ",
|
||||||
|
items: ["AI and platform subscriptions", "Farm management and technology services"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
product: "Cropsphere Produce",
|
||||||
|
items: ["Produce margins", "Subscriptions", "Marketplace commissions", "Value-added products"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
product: "Living",
|
||||||
|
items: ["Vending-machine sales", "Retail", "Experiences", "Wellness and consumer offerings"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
product: "O-Points and regeneration",
|
||||||
|
items: [
|
||||||
|
"Corporate sustainability programmes",
|
||||||
|
"Carbon-value realisation",
|
||||||
|
"ESG and impact data services",
|
||||||
|
"Partnerships",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Section 4: where revenue can come from, layer by layer. */
|
||||||
|
function Revenue() {
|
||||||
|
return (
|
||||||
|
<section className="section-y relative overflow-hidden bg-ink">
|
||||||
|
<div className="glow glow-drift absolute -right-48 top-0 h-[36rem] w-[36rem] opacity-40" />
|
||||||
|
<div className="container-x relative">
|
||||||
|
<SectionHeader
|
||||||
|
eyebrow="Business model"
|
||||||
|
icon={Coins}
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
Revenue from <Accent>every layer.</Accent>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
A multi-layer revenue architecture rather than a business that depends on selling
|
||||||
|
software alone. Potential revenue streams:
|
||||||
|
</p>
|
||||||
|
</SectionHeader>
|
||||||
|
{/* Columns rise at different speeds, like bars growing (.rise-staggered). */}
|
||||||
|
<div className="mt-14 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{REVENUE.map((r, i) => (
|
||||||
|
<div
|
||||||
|
key={r.product}
|
||||||
|
style={{ "--i": i } as CSSProperties}
|
||||||
|
className="rise-staggered rounded-3xl border border-border bg-card/70 p-7"
|
||||||
|
>
|
||||||
|
<div className="font-display text-xl font-bold text-primary">{r.product}</div>
|
||||||
|
<ul className="mt-5 space-y-3">
|
||||||
|
{r.items.map((it) => (
|
||||||
|
<li key={it} className="flex gap-3 text-foreground/90">
|
||||||
|
<span className="mt-2.5 h-1.5 w-1.5 shrink-0 rounded-full bg-primary" />
|
||||||
|
{it}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Section 5: the whole story in one paragraph, brightening word by word as it scrolls. */
|
||||||
|
function InOneParagraph() {
|
||||||
|
return (
|
||||||
|
<section className="sheet-enter theme-light section-y bg-background text-foreground">
|
||||||
|
<div className="container-x">
|
||||||
|
<span className="eyebrow">In one paragraph</span>
|
||||||
|
<p className="mt-8 max-w-5xl font-display text-3xl font-bold leading-[1.12] tracking-tight md:text-5xl">
|
||||||
|
<FillWords
|
||||||
|
text="Cropsphere is building the operating infrastructure for regenerative agriculture."
|
||||||
|
className="text-primary"
|
||||||
|
/>
|
||||||
|
<FillWords text="We connect farms through AI, move their produce through a traceable marketplace, reward consumers and farmers for regenerative actions through O-Points, and bring the ecosystem to consumers through Living." />
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
{...reveal("up", 150)}
|
||||||
|
className="mt-10 max-w-[65ch] text-lg leading-relaxed text-muted-foreground"
|
||||||
|
>
|
||||||
|
The result is a closed loop: better farming creates better food, better consumer
|
||||||
|
engagement creates more data and revenue, and regenerative activity creates measurable
|
||||||
|
environmental value.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cropsphere's full story, shown on its venture page in place of the generic vertical lists. */
|
||||||
|
export function CropsphereStory() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Overview />
|
||||||
|
<HowItWorks />
|
||||||
|
<Flywheel />
|
||||||
|
<Revenue />
|
||||||
|
<InOneParagraph />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -22,6 +22,23 @@ export function useSmoothScroll() {
|
|||||||
}, []);
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks page sections that are well off screen with data-offscreen, which pauses their looping
|
||||||
|
* CSS animations (see styles.css) so hidden decoration never competes with the scroll.
|
||||||
|
*/
|
||||||
|
export function usePauseOffscreen() {
|
||||||
|
useEffect(() => {
|
||||||
|
const io = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
for (const e of entries) e.target.toggleAttribute("data-offscreen", !e.isIntersecting);
|
||||||
|
},
|
||||||
|
{ rootMargin: "200px 0px" },
|
||||||
|
);
|
||||||
|
document.querySelectorAll("main > section, main > div").forEach((el) => io.observe(el));
|
||||||
|
return () => io.disconnect();
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
const LINKISH = "a, button, [role='button'], summary, [data-cursor]";
|
const LINKISH = "a, button, [role='button'], summary, [data-cursor]";
|
||||||
const TEXTISH = "input, textarea, select, [contenteditable='true']";
|
const TEXTISH = "input, textarea, select, [contenteditable='true']";
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { ArrowRight, ArrowUpRight, Check, Compass, Layers, Minus, Sparkles } fro
|
|||||||
import { CONTACT_EMAIL } from "@/lib/contact";
|
import { CONTACT_EMAIL } from "@/lib/contact";
|
||||||
import { SplitTitle, type TitlePart } from "@/components/site/motion";
|
import { SplitTitle, type TitlePart } from "@/components/site/motion";
|
||||||
import { IndustryIndex, MoltenMark } from "@/components/site/creative";
|
import { IndustryIndex, MoltenMark } from "@/components/site/creative";
|
||||||
|
import { CropsphereStory } from "@/components/site/cropsphere";
|
||||||
|
import { DoorMileStory, LoyalyStory } from "@/components/site/venture-stories";
|
||||||
// Section photography (Unsplash License): see src/assets/sections/CREDITS.md.
|
// Section photography (Unsplash License): see src/assets/sections/CREDITS.md.
|
||||||
import principleEvidence from "@/assets/sections/principle-evidence.webp";
|
import principleEvidence from "@/assets/sections/principle-evidence.webp";
|
||||||
import principleEngine from "@/assets/sections/principle-engine.webp";
|
import principleEngine from "@/assets/sections/principle-engine.webp";
|
||||||
@@ -701,9 +703,23 @@ export function FrameworkPrinciples() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const VENTURES = VERTICALS.filter((v) => v.example).map((v) => {
|
/** Ventures in the studio, from each vertical's example; each gets a page at /ventures/<slug>. */
|
||||||
|
export const VENTURES = VERTICALS.filter((v) => v.example).map((v) => {
|
||||||
const [name, ...rest] = (v.example ?? "").split(". ");
|
const [name, ...rest] = (v.example ?? "").split(". ");
|
||||||
return { name, pitch: rest.join(". "), vertical: v.name, img: v.img, icon: v.icon };
|
return {
|
||||||
|
name,
|
||||||
|
slug: name
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, "-")
|
||||||
|
.replace(/(^-|-$)/g, ""),
|
||||||
|
pitch: rest.join(". "),
|
||||||
|
vertical: v.name,
|
||||||
|
tag: v.tag,
|
||||||
|
problem: v.problem,
|
||||||
|
solution: v.solution,
|
||||||
|
img: v.img,
|
||||||
|
icon: v.icon,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Portfolio: ventures already taking shape in the studio. */
|
/** Portfolio: ventures already taking shape in the studio. */
|
||||||
@@ -723,14 +739,14 @@ export function Ventures() {
|
|||||||
<p>A first look at companies being engineered on the KapFoundry engine.</p>
|
<p>A first look at companies being engineered on the KapFoundry engine.</p>
|
||||||
</SectionHeader>
|
</SectionHeader>
|
||||||
|
|
||||||
<div className="mt-16 grid gap-6 md:grid-cols-2">
|
<div className="mt-16 grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{VENTURES.map((v, i) => {
|
{VENTURES.map((v, i) => {
|
||||||
const Icon = v.icon;
|
const Icon = v.icon;
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
key={v.name}
|
key={v.name}
|
||||||
{...reveal("clip", i * 120)}
|
{...reveal("clip", i * 120)}
|
||||||
className="theme-dark group relative flex min-h-[30rem] flex-col justify-end overflow-hidden rounded-[2rem] bg-background p-8 text-foreground md:min-h-[36rem] md:p-10"
|
className="theme-dark group relative flex min-h-[30rem] flex-col justify-end overflow-hidden rounded-[2rem] bg-background p-8 text-foreground md:min-h-[36rem] md:p-10 md:[&:last-child:nth-child(odd)]:col-span-2 lg:[&:last-child:nth-child(odd)]:col-span-1"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={v.img}
|
src={v.img}
|
||||||
@@ -745,13 +761,30 @@ export function Ventures() {
|
|||||||
<Icon className="h-3.5 w-3.5" aria-hidden />
|
<Icon className="h-3.5 w-3.5" aria-hidden />
|
||||||
{v.vertical}
|
{v.vertical}
|
||||||
</span>
|
</span>
|
||||||
<h3 className="mt-6 font-display text-4xl font-extrabold tracking-tight lg:text-6xl">
|
<h3 className="mt-6 font-display text-[2rem] font-extrabold tracking-tight min-[360px]:text-4xl xl:text-5xl">
|
||||||
{v.name}
|
{v.name}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="mt-4 max-w-[40ch] text-lg leading-relaxed text-muted-foreground">
|
<p className="mt-4 max-w-[40ch] text-lg leading-relaxed text-muted-foreground">
|
||||||
{v.pitch}
|
{v.pitch}
|
||||||
</p>
|
</p>
|
||||||
|
{/* Visible label for the card-wide link below. */}
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className="mt-8 inline-flex items-center gap-3 rounded-full border border-white/20 bg-white/10 py-2 pl-6 pr-2 font-display text-sm font-semibold text-foreground backdrop-blur-md transition-colors duration-500 group-hover:border-primary group-hover:bg-primary group-hover:text-primary-foreground"
|
||||||
|
>
|
||||||
|
Explore venture
|
||||||
|
<span className="grid h-9 w-9 place-items-center rounded-full bg-primary text-primary-foreground transition-[background-color,color,rotate] duration-500 group-hover:rotate-45 group-hover:bg-primary-foreground group-hover:text-primary">
|
||||||
|
<ArrowUpRight className="h-4 w-4" aria-hidden />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{/* One link over the whole card: click anywhere, one tab stop. */}
|
||||||
|
<a
|
||||||
|
href={`/ventures/${v.slug}`}
|
||||||
|
data-cursor="Explore"
|
||||||
|
aria-label={`Explore ${v.name}`}
|
||||||
|
className="absolute inset-0 z-10 rounded-[2rem]"
|
||||||
|
/>
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -963,3 +996,301 @@ export function PortfolioIndex() {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
// Venture detail page (/ventures/$slug)
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Ventures with a full story of their own; the rest get the generic vertical sections. */
|
||||||
|
const VENTURE_STORIES: Record<string, () => ReactNode> = {
|
||||||
|
cropsphere: CropsphereStory,
|
||||||
|
"doormile-wings": DoorMileStory,
|
||||||
|
"loyaly-ai": LoyalyStory,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Looks a venture up by its URL slug. */
|
||||||
|
export function findVenture(slug: string) {
|
||||||
|
return VENTURES.find((v) => v.slug === slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Everything on this page comes from what the site already says about the venture and its
|
||||||
|
* vertical; nothing about stage, traction or team is implied.
|
||||||
|
*/
|
||||||
|
export function VentureDetail({ slug }: { slug: string }) {
|
||||||
|
const v = findVenture(slug);
|
||||||
|
if (!v) return null;
|
||||||
|
const i = VENTURES.indexOf(v);
|
||||||
|
const next = VENTURES[(i + 1) % VENTURES.length];
|
||||||
|
const Icon = v.icon;
|
||||||
|
const Story = VENTURE_STORIES[v.slug];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Hero: the venture's own photo, full bleed */}
|
||||||
|
<section
|
||||||
|
id="top"
|
||||||
|
className="hero-timeline relative flex min-h-[100svh] flex-col overflow-hidden bg-ink"
|
||||||
|
>
|
||||||
|
<div className="hero-media absolute inset-0" aria-hidden>
|
||||||
|
{/* Pulls back into a rounded card as the hero scrolls away (.venture-photo). */}
|
||||||
|
<div className="venture-photo absolute inset-0 overflow-hidden">
|
||||||
|
<img
|
||||||
|
src={v.img}
|
||||||
|
alt=""
|
||||||
|
fetchPriority="high"
|
||||||
|
className="hero-zoom absolute inset-0 h-full w-full object-cover opacity-70"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-ink via-ink/75 to-ink/20" />
|
||||||
|
<div className="absolute inset-x-0 bottom-0 h-1/2 bg-gradient-to-t from-ink to-transparent" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hero-copy container-x relative flex flex-1 flex-col justify-between gap-12 pb-12 pt-28 md:pt-36">
|
||||||
|
<nav
|
||||||
|
aria-label="Breadcrumb"
|
||||||
|
{...rise(100)}
|
||||||
|
className="rise-in flex flex-wrap items-center gap-3 font-display text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground"
|
||||||
|
>
|
||||||
|
<a href="/portfolio" className="transition-colors hover:text-primary">
|
||||||
|
Portfolio
|
||||||
|
</a>
|
||||||
|
<span className="h-px w-8 bg-primary/50" aria-hidden />
|
||||||
|
<span className="text-primary">{v.vertical}</span>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
{...rise(250)}
|
||||||
|
className="rise-in inline-flex items-center gap-2 rounded-full bg-primary px-3.5 py-1.5 font-display text-[11px] font-semibold uppercase tracking-wider text-primary-foreground"
|
||||||
|
>
|
||||||
|
<Icon className="h-3.5 w-3.5" aria-hidden />
|
||||||
|
In the studio
|
||||||
|
</span>
|
||||||
|
<h1 className="venture-name mt-6 max-w-[14ch] font-display text-[clamp(2.75rem,11vw,4rem)] font-extrabold leading-[0.92] tracking-tighter md:text-[clamp(4rem,9vw,8.5rem)]">
|
||||||
|
<SplitTitle parts={[{ accent: v.name }]} />
|
||||||
|
</h1>
|
||||||
|
<p
|
||||||
|
{...rise(700)}
|
||||||
|
className="rise-in mt-8 max-w-[46ch] text-xl leading-relaxed text-white/80 md:text-2xl"
|
||||||
|
>
|
||||||
|
{v.pitch}
|
||||||
|
</p>
|
||||||
|
<div {...rise(850)} className="rise-in mt-10 flex flex-wrap gap-4">
|
||||||
|
<a
|
||||||
|
href="/contact"
|
||||||
|
data-magnetic
|
||||||
|
className="press group inline-flex items-center gap-2 rounded-full bg-primary px-7 py-3.5 font-display text-sm font-semibold text-primary-foreground hover:brightness-110"
|
||||||
|
>
|
||||||
|
Partner with us
|
||||||
|
<ArrowRight
|
||||||
|
className="h-4 w-4 transition group-hover:translate-x-0.5"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/portfolio"
|
||||||
|
className="press inline-flex items-center gap-2 rounded-full border border-white/15 bg-white/5 px-7 py-3.5 font-display text-sm font-semibold text-foreground backdrop-blur-md hover:border-white/30 hover:bg-white/10"
|
||||||
|
>
|
||||||
|
Back to portfolio
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{Story ? (
|
||||||
|
<Story />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* The problem: the market this venture is built for */}
|
||||||
|
<section className="sheet-enter theme-light section-y bg-background text-foreground">
|
||||||
|
<div className="container-x grid gap-12 lg:grid-cols-12 lg:gap-16">
|
||||||
|
<div className="lg:col-span-5">
|
||||||
|
<div className="lg:sticky lg:top-32" {...reveal("stagger")}>
|
||||||
|
<span className="eyebrow">The problem</span>
|
||||||
|
<h2 className="mt-5 font-display text-4xl font-bold leading-[1.04] md:text-5xl">
|
||||||
|
{v.vertical}: <Accent>{v.tag.toLowerCase()}.</Accent>
|
||||||
|
</h2>
|
||||||
|
<p className="mt-6 max-w-[48ch] text-lg leading-relaxed text-muted-foreground">
|
||||||
|
{v.name} is being built for a market held back by the same problems, again and
|
||||||
|
again.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ol className="lg:col-span-7">
|
||||||
|
{v.problem.map((p, k) => (
|
||||||
|
<li
|
||||||
|
key={p}
|
||||||
|
{...reveal("up", k * 40)}
|
||||||
|
className="relative flex items-baseline gap-6 py-6 md:py-7"
|
||||||
|
>
|
||||||
|
<span aria-hidden className="absolute inset-x-0 top-0 h-px bg-border" />
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className="line-draw absolute inset-x-0 top-0 h-px bg-primary/60"
|
||||||
|
/>
|
||||||
|
<span className="w-10 shrink-0 font-display text-sm font-semibold tabular-nums text-primary">
|
||||||
|
0{k + 1}
|
||||||
|
</span>
|
||||||
|
<span className="font-display text-2xl font-bold tracking-tight md:text-3xl">
|
||||||
|
{p}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* What we build */}
|
||||||
|
<section className="section-y relative overflow-hidden bg-ink">
|
||||||
|
<div className="glow glow-drift absolute -right-48 top-0 h-[36rem] w-[36rem] opacity-40" />
|
||||||
|
<div className="container-x relative">
|
||||||
|
<SectionHeader
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
What we build, <Accent>AI-first.</Accent>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Where KapFoundry focuses in {v.vertical.toLowerCase()}, and the ground {v.name} is
|
||||||
|
built on.
|
||||||
|
</p>
|
||||||
|
</SectionHeader>
|
||||||
|
<ul className="mt-14 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{v.solution.map((s) => (
|
||||||
|
<li
|
||||||
|
key={s}
|
||||||
|
className="tilt-in group flex items-start gap-4 rounded-3xl border border-border bg-card/70 p-6 transition-colors duration-500 hover:border-primary/50 md:p-7"
|
||||||
|
>
|
||||||
|
<span className="grid h-10 w-10 shrink-0 place-items-center rounded-xl bg-primary/15 text-primary transition-colors duration-500 group-hover:bg-primary group-hover:text-primary-foreground">
|
||||||
|
<Check className="h-4 w-4" aria-hidden />
|
||||||
|
</span>
|
||||||
|
<span className="pt-1.5 font-display text-lg font-semibold leading-snug">
|
||||||
|
{s}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Built on the engine: the same framework and stack as every venture */}
|
||||||
|
<section className="sheet-enter theme-light section-y bg-background text-foreground">
|
||||||
|
<div className="container-x">
|
||||||
|
<SectionHeader
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
Built on the <Accent>KapFoundry engine.</Accent>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Like every venture in the studio, {v.name} moves through the same six stages and
|
||||||
|
launches with a mature company's stack.
|
||||||
|
</p>
|
||||||
|
</SectionHeader>
|
||||||
|
|
||||||
|
<ol
|
||||||
|
className="mt-14 grid grid-cols-1 gap-3 min-[400px]:grid-cols-2 md:grid-cols-3 lg:grid-cols-6"
|
||||||
|
{...reveal("stagger")}
|
||||||
|
>
|
||||||
|
{FRAMEWORK.map((f) => {
|
||||||
|
const StageIcon = f.icon;
|
||||||
|
return (
|
||||||
|
<li key={f.n} className="rounded-3xl border border-border bg-card p-5">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="font-display text-sm font-semibold tabular-nums text-primary">
|
||||||
|
{f.n}
|
||||||
|
</span>
|
||||||
|
<StageIcon className="h-4 w-4 text-primary" aria-hidden />
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 font-display text-xl font-bold">{f.title}</div>
|
||||||
|
<p className="mt-2 text-sm leading-snug text-muted-foreground">{f.body}</p>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<div className="mt-10 flex flex-wrap gap-2" {...reveal("up", 150)}>
|
||||||
|
{STACK.map((s) => (
|
||||||
|
<span
|
||||||
|
key={s.label}
|
||||||
|
className="rounded-full border border-border px-4 py-2 text-sm text-muted-foreground"
|
||||||
|
>
|
||||||
|
{s.label}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Call to action */}
|
||||||
|
<section className="section-y relative overflow-hidden bg-ink">
|
||||||
|
<div className="glow glow-drift absolute -left-40 top-0 h-[34rem] w-[34rem] opacity-40" />
|
||||||
|
<div className="container-x relative grid items-end gap-10 md:grid-cols-12">
|
||||||
|
<h2
|
||||||
|
{...reveal("mask")}
|
||||||
|
className="font-display text-4xl font-bold leading-[1.02] md:col-span-8 md:text-6xl"
|
||||||
|
>
|
||||||
|
Interested in {v.name}? <Accent>Let's talk.</Accent>
|
||||||
|
</h2>
|
||||||
|
<div className="md:col-span-4 md:justify-self-end" {...reveal("up", 150)}>
|
||||||
|
<p className="text-lg leading-relaxed text-muted-foreground">
|
||||||
|
Partners, investors and operators: we'd like to hear from you.
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="/contact"
|
||||||
|
data-magnetic
|
||||||
|
className="press group mt-6 inline-flex items-center gap-2 rounded-full bg-primary px-7 py-3.5 font-display text-sm font-semibold text-primary-foreground hover:brightness-110"
|
||||||
|
>
|
||||||
|
Speak with our team
|
||||||
|
<ArrowRight className="h-4 w-4 transition group-hover:translate-x-0.5" aria-hidden />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Next venture */}
|
||||||
|
{next !== v ? (
|
||||||
|
<section
|
||||||
|
aria-label="Next venture"
|
||||||
|
className="relative overflow-hidden border-t border-border bg-ink"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={`/ventures/${next.slug}`}
|
||||||
|
data-cursor="Next"
|
||||||
|
className="group relative block py-24 md:py-32"
|
||||||
|
>
|
||||||
|
<div className="container-x">
|
||||||
|
<div className="flex items-center justify-between font-display text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground">
|
||||||
|
<span data-scramble>Next venture</span>
|
||||||
|
<span>{next.vertical}</span>
|
||||||
|
</div>
|
||||||
|
<div className="next-grow mt-8 flex flex-col items-start gap-6 md:flex-row md:items-center md:justify-between">
|
||||||
|
<span className="next-fill font-display text-[clamp(2.5rem,13vw,10rem)] font-extrabold leading-[0.9] tracking-tighter md:text-[min(10vw,10rem)]">
|
||||||
|
{next.name}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`grid h-16 w-16 shrink-0 place-items-center rounded-full border border-primary/40 text-primary transition-all duration-700 group-hover:bg-primary group-hover:text-primary-foreground md:h-28 md:w-28 ${EASE_STAGE}`}
|
||||||
|
>
|
||||||
|
<ArrowUpRight
|
||||||
|
className={`h-7 w-7 transition-transform duration-700 group-hover:rotate-45 md:h-10 md:w-10 ${EASE_STAGE}`}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-10 h-px bg-white/10">
|
||||||
|
<div className="line-draw h-full bg-primary/70" />
|
||||||
|
</div>
|
||||||
|
<p className="mt-6 max-w-md text-lg text-muted-foreground">{next.pitch}</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
memo,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useRef,
|
useRef,
|
||||||
@@ -50,7 +51,13 @@ import {
|
|||||||
Workflow,
|
Workflow,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Cursor, SplitTitle, useScramble, useSmoothScroll } from "@/components/site/motion";
|
import {
|
||||||
|
Cursor,
|
||||||
|
SplitTitle,
|
||||||
|
usePauseOffscreen,
|
||||||
|
useScramble,
|
||||||
|
useSmoothScroll,
|
||||||
|
} from "@/components/site/motion";
|
||||||
import { LensStatement, MoltenMark, VelocityTicker } from "@/components/site/creative";
|
import { LensStatement, MoltenMark, VelocityTicker } from "@/components/site/creative";
|
||||||
import { CONTACT_EMAIL, INTERESTS, sendContactEnquiry, type ContactInput } from "@/lib/contact";
|
import { CONTACT_EMAIL, INTERESTS, sendContactEnquiry, type ContactInput } from "@/lib/contact";
|
||||||
// Brand assets cut from kapfoundry-logo-1.png (emblem, wordmark, full lockup).
|
// Brand assets cut from kapfoundry-logo-1.png (emblem, wordmark, full lockup).
|
||||||
@@ -66,9 +73,9 @@ import vFoodtech from "@/assets/vertical-foodtech.jpg";
|
|||||||
import vCarbon from "@/assets/vertical-carbon.jpg";
|
import vCarbon from "@/assets/vertical-carbon.jpg";
|
||||||
import vHealthtech from "@/assets/vertical-healthtech.jpg";
|
import vHealthtech from "@/assets/vertical-healthtech.jpg";
|
||||||
// Card photography (Unsplash License): see src/assets/cards/CREDITS.md.
|
// Card photography (Unsplash License): see src/assets/cards/CREDITS.md.
|
||||||
import imgFoundry from "@/assets/cards/studio-foundry.webp";
|
import imgStudioTeam from "@/assets/cards/pillar-studio-team.webp";
|
||||||
import imgTower from "@/assets/cards/holding-tower.webp";
|
import imgHoldingHq from "@/assets/cards/pillar-holding-hq.webp";
|
||||||
import imgSpiral from "@/assets/cards/compound-spiral.webp";
|
import imgCompoundGrowth from "@/assets/cards/pillar-compound-growth.webp";
|
||||||
import imgVision from "@/assets/cards/ai-vision.webp";
|
import imgVision from "@/assets/cards/ai-vision.webp";
|
||||||
import imgAgentic from "@/assets/cards/ai-agentic.webp";
|
import imgAgentic from "@/assets/cards/ai-agentic.webp";
|
||||||
import imgEnterprise from "@/assets/cards/ai-enterprise.webp";
|
import imgEnterprise from "@/assets/cards/ai-enterprise.webp";
|
||||||
@@ -109,14 +116,7 @@ const NAV = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export type SitePageName =
|
export type SitePageName =
|
||||||
| "home"
|
"home" | "studio" | "framework" | "portfolio" | "ai" | "investors" | "about" | "contact";
|
||||||
| "studio"
|
|
||||||
| "framework"
|
|
||||||
| "portfolio"
|
|
||||||
| "ai"
|
|
||||||
| "investors"
|
|
||||||
| "about"
|
|
||||||
| "contact";
|
|
||||||
|
|
||||||
const VERTICALS = [
|
const VERTICALS = [
|
||||||
{
|
{
|
||||||
@@ -210,6 +210,7 @@ const VERTICALS = [
|
|||||||
"Supply-chain digitization",
|
"Supply-chain digitization",
|
||||||
"Yield prediction and farm analytics",
|
"Yield prediction and farm analytics",
|
||||||
],
|
],
|
||||||
|
example: "Cropsphere. The operating infrastructure for regenerative agriculture.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
n: "05",
|
n: "05",
|
||||||
@@ -462,22 +463,19 @@ function useScrollReveal() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
||||||
const timers: number[] = [];
|
const timers: number[] = [];
|
||||||
const io = new IntersectionObserver(
|
const pending = new Set<HTMLElement>();
|
||||||
(entries) => {
|
|
||||||
// Elements that arrive together (a grid row, a fast scroll) cascade in reading order.
|
// Reveal a group of elements, cascading them in reading order.
|
||||||
const arriving = entries
|
const revealAll = (els: HTMLElement[]) => {
|
||||||
.filter((e) => e.isIntersecting)
|
els
|
||||||
.sort(
|
.map((el) => ({ el, r: el.getBoundingClientRect() }))
|
||||||
(a, b) =>
|
.sort((a, b) => a.r.top - b.r.top || a.r.left - b.r.left)
|
||||||
a.boundingClientRect.top - b.boundingClientRect.top ||
|
.forEach(({ el }, i) => {
|
||||||
a.boundingClientRect.left - b.boundingClientRect.left,
|
if (!pending.delete(el)) return;
|
||||||
);
|
io.unobserve(el);
|
||||||
arriving.forEach((entry, i) => {
|
|
||||||
const el = entry.target as HTMLElement;
|
|
||||||
const batchDelay = Math.min(i, 5) * REVEAL_BATCH_STEP;
|
const batchDelay = Math.min(i, 5) * REVEAL_BATCH_STEP;
|
||||||
el.style.setProperty("--batch-delay", `${batchDelay}ms`);
|
el.style.setProperty("--batch-delay", `${batchDelay}ms`);
|
||||||
el.classList.add("is-visible");
|
el.classList.add("is-visible");
|
||||||
io.unobserve(el);
|
|
||||||
// Once settled, drop the reveal classes so the element's own hover transitions return.
|
// Once settled, drop the reveal classes so the element's own hover transitions return.
|
||||||
const own = parseFloat(el.style.getPropertyValue("--reveal-delay")) || 0;
|
const own = parseFloat(el.style.getPropertyValue("--reveal-delay")) || 0;
|
||||||
const stagger = el.dataset.reveal === "stagger" ? el.children.length * 80 : 0;
|
const stagger = el.dataset.reveal === "stagger" ? el.children.length * 80 : 0;
|
||||||
@@ -488,9 +486,29 @@ function useScrollReveal() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
};
|
||||||
|
|
||||||
|
const io = new IntersectionObserver(
|
||||||
|
(entries) =>
|
||||||
|
revealAll(entries.filter((e) => e.isIntersecting).map((e) => e.target as HTMLElement)),
|
||||||
{ rootMargin: "0px 0px -10% 0px" },
|
{ rootMargin: "0px 0px -10% 0px" },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// A fast fling, an anchor jump or the End key can carry an element past the screen between
|
||||||
|
// two frames, so the observer never sees it on screen and it would stay hidden. Catch those:
|
||||||
|
// anything whose top is already above the reveal line is revealed straight away.
|
||||||
|
let frame = 0;
|
||||||
|
const sweep = () => {
|
||||||
|
frame = 0;
|
||||||
|
const line = window.innerHeight * 0.9;
|
||||||
|
const passed = [...pending].filter((el) => el.getBoundingClientRect().top < line);
|
||||||
|
if (passed.length) revealAll(passed);
|
||||||
|
if (!pending.size) window.removeEventListener("scroll", onScroll);
|
||||||
|
};
|
||||||
|
const onScroll = () => {
|
||||||
|
if (!frame) frame = requestAnimationFrame(sweep);
|
||||||
|
};
|
||||||
|
|
||||||
for (const el of document.querySelectorAll<HTMLElement>("[data-reveal]")) {
|
for (const el of document.querySelectorAll<HTMLElement>("[data-reveal]")) {
|
||||||
// Leave anything already on screen alone so it never flickers after hydration.
|
// Leave anything already on screen alone so it never flickers after hydration.
|
||||||
if (el.getBoundingClientRect().top < window.innerHeight) continue;
|
if (el.getBoundingClientRect().top < window.innerHeight) continue;
|
||||||
@@ -500,10 +518,14 @@ function useScrollReveal() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
el.classList.add("reveal-pending");
|
el.classList.add("reveal-pending");
|
||||||
|
pending.add(el);
|
||||||
io.observe(el);
|
io.observe(el);
|
||||||
}
|
}
|
||||||
|
window.addEventListener("scroll", onScroll, { passive: true });
|
||||||
return () => {
|
return () => {
|
||||||
io.disconnect();
|
io.disconnect();
|
||||||
|
window.removeEventListener("scroll", onScroll);
|
||||||
|
cancelAnimationFrame(frame);
|
||||||
timers.forEach(clearTimeout);
|
timers.forEach(clearTimeout);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
@@ -692,96 +714,98 @@ function Nav({ page }: { page: SitePageName }) {
|
|||||||
const isCurrent = (href: string) => href === `/${page}`;
|
const isCurrent = (href: string) => href === `/${page}`;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{/* Loyaly-style header: transparent over the hero with a dark nav capsule; once scrolled
|
||||||
|
it becomes a floating cream island with rounded lower corners. */}
|
||||||
<header
|
<header
|
||||||
className={`vt-header 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} ${
|
className={`vt-header fixed inset-x-0 top-0 z-50 transition-transform duration-500 focus-within:translate-y-0 motion-reduce:transition-none ${EASE_STAGE} ${
|
||||||
hidden ? "-translate-y-full" : "translate-y-0"
|
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
|
<div className={`header-island relative overflow-hidden ${solid ? "is-solid" : ""}`}>
|
||||||
aria-hidden
|
<div
|
||||||
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 ${
|
aria-hidden
|
||||||
open ? "opacity-0" : ""
|
className={`scroll-progress absolute inset-x-0 bottom-0 h-0.5 bg-gradient-to-r from-primary/40 via-primary to-primary/40 transition-opacity duration-300 ${
|
||||||
}`}
|
solid ? "" : "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) => {
|
<div className="container-x flex h-18 items-center justify-between gap-4">
|
||||||
const current = isCurrent(n.href);
|
<a
|
||||||
return (
|
href={page === "home" ? "#top" : "/"}
|
||||||
<a
|
aria-label="KapFoundry home"
|
||||||
key={n.href}
|
className="flex shrink-0 items-center gap-2.5"
|
||||||
href={n.href}
|
onClick={() => setOpen(false)}
|
||||||
data-scramble
|
>
|
||||||
aria-current={current ? "true" : undefined}
|
<img src={logoMark} alt="" width={201} height={192} className="h-10 w-auto sm:h-11" />
|
||||||
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 ${
|
<img
|
||||||
current ? "bg-primary/15 text-primary" : "text-muted-foreground"
|
src={logoWordmark}
|
||||||
}`}
|
alt=""
|
||||||
>
|
width={1041}
|
||||||
{n.label}
|
height={72}
|
||||||
</a>
|
className="hidden h-3.5 w-auto min-[400px]:block sm:h-4 lg:hidden xl:block"
|
||||||
);
|
/>
|
||||||
})}
|
</a>
|
||||||
</nav>
|
<nav
|
||||||
<a
|
aria-label="Primary"
|
||||||
href="/investors"
|
className="nav-capsule hidden items-center gap-1 rounded-full p-1.5 lg:flex"
|
||||||
data-magnetic
|
>
|
||||||
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"
|
{NAV.map((n) => {
|
||||||
>
|
const current = isCurrent(n.href);
|
||||||
Invest with us
|
return (
|
||||||
</a>
|
<a
|
||||||
|
key={n.href}
|
||||||
|
href={n.href}
|
||||||
|
data-scramble
|
||||||
|
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 xl:px-4 ${
|
||||||
|
current
|
||||||
|
? "bg-white/10 text-primary"
|
||||||
|
: "text-white/70 hover:bg-white/10 hover:text-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{n.label}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
<a
|
||||||
|
href="/investors"
|
||||||
|
data-magnetic
|
||||||
|
className={`press header-cta hidden items-center gap-2 rounded-full px-5 py-2.5 font-display text-xs font-semibold uppercase tracking-wider lg:inline-flex ${
|
||||||
|
solid ? "is-outline" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Invest with us
|
||||||
|
</a>
|
||||||
|
|
||||||
{/* Two bars that cross into an X; the label swaps on tablets. */}
|
{/* Two bars that cross into an X; the label swaps on tablets. */}
|
||||||
<button
|
<button
|
||||||
ref={toggleRef}
|
ref={toggleRef}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
aria-controls="site-menu"
|
aria-controls="site-menu"
|
||||||
aria-label={open ? "Close menu" : "Open 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 ${
|
className={`press nav-capsule group flex h-11 items-center gap-3 rounded-full pl-4 pr-3 text-white transition-colors duration-300 lg:hidden ${
|
||||||
open ? "border-primary/50 bg-primary/10" : "border-border bg-ink/40 backdrop-blur-md"
|
open ? "ring-1 ring-primary/60" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="hidden font-display text-xs font-semibold uppercase tracking-[0.16em] sm:block">
|
<span className="hidden font-display text-xs font-semibold uppercase tracking-[0.16em] sm:block">
|
||||||
{open ? "Close" : "Menu"}
|
{open ? "Close" : "Menu"}
|
||||||
</span>
|
</span>
|
||||||
<span className="relative block h-5 w-5" aria-hidden>
|
<span className="relative block h-5 w-5" aria-hidden>
|
||||||
<span
|
<span
|
||||||
className={`absolute left-0 top-1/2 h-[1.5px] w-5 rounded-full bg-current transition-transform duration-500 ${EASE_STAGE} ${
|
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]"
|
open ? "rotate-45" : "-translate-y-[4px]"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className={`absolute left-0 top-1/2 h-[1.5px] rounded-full bg-current transition-all duration-500 ${EASE_STAGE} ${
|
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"
|
open ? "w-5 -rotate-45" : "w-3.5 translate-y-[4px] group-hover:w-5"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -968,19 +992,19 @@ const PILLARS = [
|
|||||||
{
|
{
|
||||||
t: "Venture Studio",
|
t: "Venture Studio",
|
||||||
icon: Building2,
|
icon: Building2,
|
||||||
img: imgFoundry,
|
img: imgStudioTeam,
|
||||||
d: "Companies are systematically created, validated and scaled on shared infrastructure, not simply funded after formation.",
|
d: "Companies are systematically created, validated and scaled on shared infrastructure, not simply funded after formation.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
t: "Holding Company",
|
t: "Holding Company",
|
||||||
icon: Layers,
|
icon: Layers,
|
||||||
img: imgTower,
|
img: imgHoldingHq,
|
||||||
d: "Every venture strengthens the ecosystem while remaining capable of becoming an independent, high-growth business.",
|
d: "Every venture strengthens the ecosystem while remaining capable of becoming an independent, high-growth business.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
t: "Built to Compound",
|
t: "Built to Compound",
|
||||||
icon: TrendingUp,
|
icon: TrendingUp,
|
||||||
img: imgSpiral,
|
img: imgCompoundGrowth,
|
||||||
d: "Build faster, reduce risk, and maximize long-term value with one shared engine behind many companies.",
|
d: "Build faster, reduce risk, and maximize long-term value with one shared engine behind many companies.",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -1703,8 +1727,14 @@ const PORTFOLIO_STEP_SVH = 70;
|
|||||||
const DECK_FLY_START = 0.5;
|
const DECK_FLY_START = 0.5;
|
||||||
const DECK_PEEK_PX = 22;
|
const DECK_PEEK_PX = 22;
|
||||||
|
|
||||||
/** One vertical as a deck card: photo side and details side. */
|
// Portfolio cards show only the top problems and solutions of each vertical, to stay scannable.
|
||||||
function VerticalCard({ v }: { v: (typeof VERTICALS)[number] }) {
|
const CARD_POINTS = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One vertical as a deck card: photo side and details side. Memoised: the deck re-renders on
|
||||||
|
* every card change, and rebuilding all seven cards each time stalled the scroll.
|
||||||
|
*/
|
||||||
|
const VerticalCard = memo(function VerticalCard({ v }: { v: (typeof VERTICALS)[number] }) {
|
||||||
const Icon = v.icon;
|
const Icon = v.icon;
|
||||||
return (
|
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="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">
|
||||||
@@ -1735,13 +1765,13 @@ function VerticalCard({ v }: { v: (typeof VERTICALS)[number] }) {
|
|||||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-1 md:gap-6 xl:grid-cols-2">
|
<div className="grid grid-cols-2 gap-4 md:grid-cols-1 md:gap-6 xl:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-2.5 text-sm font-medium text-muted-foreground">The problem</div>
|
<div className="mb-2.5 text-sm font-medium text-muted-foreground">The problem</div>
|
||||||
<ul className="space-y-1.5">
|
<ul className="space-y-2">
|
||||||
{v.problem.map((p) => (
|
{v.problem.slice(0, CARD_POINTS).map((p) => (
|
||||||
<li
|
<li
|
||||||
key={p}
|
key={p}
|
||||||
className="flex gap-2 text-[13px] leading-snug text-muted-foreground md:text-sm"
|
className="flex gap-2 text-[13px] leading-snug text-muted-foreground min-[360px]:text-sm md:text-base"
|
||||||
>
|
>
|
||||||
<span className="mt-[7px] h-1 w-1 shrink-0 rounded-full bg-muted-foreground/60" />
|
<span className="mt-2 h-1 w-1 shrink-0 rounded-full bg-muted-foreground/60" />
|
||||||
{p}
|
{p}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@@ -1749,11 +1779,11 @@ function VerticalCard({ v }: { v: (typeof VERTICALS)[number] }) {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-2.5 text-sm font-medium text-primary">Our solution</div>
|
<div className="mb-2.5 text-sm font-medium text-primary">Our solution</div>
|
||||||
<ul className="space-y-1.5">
|
<ul className="space-y-2">
|
||||||
{v.solution.map((p) => (
|
{v.solution.slice(0, CARD_POINTS).map((p) => (
|
||||||
<li
|
<li
|
||||||
key={p}
|
key={p}
|
||||||
className="flex gap-2 text-[13px] leading-snug text-foreground md:text-sm"
|
className="flex gap-2 text-[13px] leading-snug text-foreground min-[360px]:text-sm md:text-base"
|
||||||
>
|
>
|
||||||
<Check className="mt-0.5 h-3.5 w-3.5 shrink-0 text-primary" aria-hidden />
|
<Check className="mt-0.5 h-3.5 w-3.5 shrink-0 text-primary" aria-hidden />
|
||||||
{p}
|
{p}
|
||||||
@@ -1774,7 +1804,7 @@ function VerticalCard({ v }: { v: (typeof VERTICALS)[number] }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Card deck. Pinned: the verticals sit stacked like a deck; scrolling flies the top card away
|
* Card deck. Pinned: the verticals sit stacked like a deck; scrolling flies the top card away
|
||||||
@@ -2813,6 +2843,7 @@ function SitePage({ page, children }: { page: SitePageName; children: ReactNode
|
|||||||
useScrollReveal();
|
useScrollReveal();
|
||||||
useSmoothScroll();
|
useSmoothScroll();
|
||||||
useScramble();
|
useScramble();
|
||||||
|
usePauseOffscreen();
|
||||||
|
|
||||||
// Arriving from another page with a hash (e.g. /#framework): the router restores scroll to
|
// 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.
|
// the top after hydration, so jump to the requested section once it has settled.
|
||||||
@@ -2847,6 +2878,7 @@ function SitePage({ page, children }: { page: SitePageName; children: ReactNode
|
|||||||
export {
|
export {
|
||||||
NAV,
|
NAV,
|
||||||
DuoPhoto,
|
DuoPhoto,
|
||||||
|
usePinnedLayout,
|
||||||
VERTICALS,
|
VERTICALS,
|
||||||
FRAMEWORK,
|
FRAMEWORK,
|
||||||
STACK,
|
STACK,
|
||||||
|
|||||||
472
src/components/site/venture-stories.tsx
Normal file
@@ -0,0 +1,472 @@
|
|||||||
|
import type { CSSProperties, ReactNode } from "react";
|
||||||
|
import {
|
||||||
|
BarChart3,
|
||||||
|
Boxes,
|
||||||
|
Brain,
|
||||||
|
Eye,
|
||||||
|
Gamepad2,
|
||||||
|
Gift,
|
||||||
|
Package,
|
||||||
|
PackageCheck,
|
||||||
|
Pill,
|
||||||
|
Plane,
|
||||||
|
ScanLine,
|
||||||
|
Snowflake,
|
||||||
|
Store,
|
||||||
|
Tag,
|
||||||
|
Truck,
|
||||||
|
Users,
|
||||||
|
Warehouse,
|
||||||
|
Zap,
|
||||||
|
type LucideIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { Accent, SectionHeader } from "@/components/site/sections";
|
||||||
|
|
||||||
|
// Short venture stories, summarised from each venture's own website (doormile.com,
|
||||||
|
// loyaly.ai). Plans are worded as plans; marketing outcome figures are left out.
|
||||||
|
|
||||||
|
type Item = { title: string; body: string; icon: LucideIcon };
|
||||||
|
|
||||||
|
/** Cards that stack on top of one another as you scroll (.layer-stack). */
|
||||||
|
function StackedCards({ items }: { items: Item[] }) {
|
||||||
|
return (
|
||||||
|
<ul className="layer-stack mt-12 flex flex-col gap-5 pb-[10vh]">
|
||||||
|
{items.map((it, i) => {
|
||||||
|
const Icon = it.icon;
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={it.title}
|
||||||
|
style={{ "--i": i } as CSSProperties}
|
||||||
|
className="theme-dark group relative flex min-h-[11rem] flex-col justify-between gap-5 overflow-hidden rounded-3xl border border-white/10 bg-background p-7 text-foreground shadow-[0_-24px_60px_-30px_oklch(0.05_0.01_60/0.8)] md:flex-row md:items-center md:p-10"
|
||||||
|
>
|
||||||
|
<div className="glow absolute -right-16 -top-16 h-56 w-56 opacity-40 transition-opacity duration-500 group-hover:opacity-90" />
|
||||||
|
<div className="relative flex items-center gap-5">
|
||||||
|
<span className="grid h-14 w-14 shrink-0 place-items-center rounded-2xl bg-primary text-primary-foreground">
|
||||||
|
<Icon className="h-6 w-6" aria-hidden />
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<div className="font-display text-xs font-semibold uppercase tracking-[0.16em] text-primary">
|
||||||
|
0{i + 1}
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 font-display text-3xl font-bold tracking-tight md:text-4xl">
|
||||||
|
{it.title}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="relative text-lg leading-relaxed text-muted-foreground md:max-w-md md:text-right">
|
||||||
|
{it.body}
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Steps in a row that light up one after another as they cross the screen (.chain-step). */
|
||||||
|
function StepRow({ items }: { items: Item[] }) {
|
||||||
|
return (
|
||||||
|
<ol className="mt-12 grid gap-3 sm:grid-cols-2 lg:grid-flow-col lg:auto-cols-fr lg:grid-cols-none">
|
||||||
|
{items.map((it, i) => {
|
||||||
|
const Icon = it.icon;
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={it.title}
|
||||||
|
style={{ "--i": i } as CSSProperties}
|
||||||
|
className="chain-step relative rounded-3xl border border-border bg-card p-6"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="grid h-11 w-11 place-items-center rounded-xl bg-primary text-primary-foreground">
|
||||||
|
<Icon className="h-5 w-5" aria-hidden />
|
||||||
|
</span>
|
||||||
|
<span className="font-display text-sm font-semibold tabular-nums text-primary">
|
||||||
|
0{i + 1}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 font-display text-xl font-bold">{it.title}</div>
|
||||||
|
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">{it.body}</p>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ol>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A vertical timeline: a stem grows down it and each node lights as it passes. */
|
||||||
|
function Timeline({ items }: { items: { label: string; title: string; body: string }[] }) {
|
||||||
|
return (
|
||||||
|
<ol className="relative mt-12 pl-10 md:pl-16">
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className="absolute bottom-0 left-[11px] top-0 w-px bg-white/10 md:left-[19px]"
|
||||||
|
>
|
||||||
|
<span className="stem-grow block h-full w-full bg-gradient-to-b from-primary via-primary to-primary/30" />
|
||||||
|
</span>
|
||||||
|
{items.map((it) => (
|
||||||
|
<li
|
||||||
|
key={it.label}
|
||||||
|
className="relative grid gap-2 border-t border-white/10 py-8 md:grid-cols-12 md:gap-8 md:py-10"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className="stem-node absolute -left-10 top-9 h-6 w-6 rounded-full border-2 border-primary bg-primary md:-left-16 md:top-11 md:h-10 md:w-10"
|
||||||
|
/>
|
||||||
|
<div className="font-display text-sm font-semibold uppercase tracking-[0.16em] text-primary md:col-span-3">
|
||||||
|
{it.label}
|
||||||
|
</div>
|
||||||
|
<div className="md:col-span-9">
|
||||||
|
<div className="font-display text-2xl font-bold md:text-3xl">{it.title}</div>
|
||||||
|
<p className="mt-2 text-lg leading-relaxed text-muted-foreground">{it.body}</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Light (cream) or dark section wrapper. */
|
||||||
|
function Band({ light, children }: { light?: boolean; children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={
|
||||||
|
light
|
||||||
|
? "sheet-enter theme-light section-y bg-background text-foreground"
|
||||||
|
: "section-y relative overflow-hidden bg-ink"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="container-x relative">{children}</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
// DoorMile Wings (from doormile.com/doormile-wings)
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function DoorMileStory() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Band light>
|
||||||
|
<SectionHeader
|
||||||
|
eyebrow="What is DoorMile Wings"
|
||||||
|
icon={Plane}
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
Where quick commerce meets <Accent>quick logistics.</Accent>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
DoorMile Wings is a dedicated domestic air-cargo network: same-day logistics between
|
||||||
|
India's major regional capitals, with AI coordinating every handoff from runway to
|
||||||
|
doorstep.
|
||||||
|
</p>
|
||||||
|
</SectionHeader>
|
||||||
|
<StackedCards
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
title: "Same-day air",
|
||||||
|
body: "A maximum of three hours door to door between major regional capitals.",
|
||||||
|
icon: Plane,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "MileTruth™ AI",
|
||||||
|
body: "Forecasts demand and optimises every route and load in real time.",
|
||||||
|
icon: Brain,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Dedicated freighters",
|
||||||
|
body: "Scheduled cargo capacity, not passenger-belly space competing with baggage.",
|
||||||
|
icon: Boxes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "EV last mile",
|
||||||
|
body: "Electric vehicles complete every first- and final-mile leg.",
|
||||||
|
icon: Zap,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Band>
|
||||||
|
|
||||||
|
<Band>
|
||||||
|
<SectionHeader
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
Pickup to arrival, <Accent>one connected flow.</Accent>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StepRow
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
title: "Local pickup",
|
||||||
|
body: "On-demand first mile by electric vehicle.",
|
||||||
|
icon: Truck,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Airport clearance",
|
||||||
|
body: "Automated screening and rapid cross-docking at the hub.",
|
||||||
|
icon: ScanLine,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Air transit",
|
||||||
|
body: "Regional cargo flights bypass surface congestion.",
|
||||||
|
icon: Plane,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Destination hub",
|
||||||
|
body: "Immediate de-consolidation and screening on arrival.",
|
||||||
|
icon: Warehouse,
|
||||||
|
},
|
||||||
|
{ title: "EV last mile", body: "Zero-emission delivery to the door.", icon: Zap },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Band>
|
||||||
|
|
||||||
|
<Band light>
|
||||||
|
<SectionHeader
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
Built for shipments that <Accent>can't wait.</Accent>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ul className="mt-12 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{[
|
||||||
|
{ t: "Quick commerce", d: "Same-day air capacity from metros to dark stores.", i: Zap },
|
||||||
|
{ t: "Express B2B cargo", d: "Time-critical parts, airport to airport.", i: Package },
|
||||||
|
{ t: "Healthcare & pharma", d: "Cold-chain packaging with chain of custody.", i: Pill },
|
||||||
|
{ t: "Cold chain", d: "Perishables tracked at airline speed.", i: Snowflake },
|
||||||
|
{
|
||||||
|
t: "High-value cargo",
|
||||||
|
d: "Secure handling with electronic custody.",
|
||||||
|
i: PackageCheck,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
t: "E-commerce",
|
||||||
|
d: "Same- and next-day reach into tier-1 and tier-2 cities.",
|
||||||
|
i: Store,
|
||||||
|
},
|
||||||
|
].map(({ t, d, i: Icon }) => (
|
||||||
|
<li
|
||||||
|
key={t}
|
||||||
|
className="tilt-in flex items-start gap-4 rounded-3xl border border-border bg-card p-6"
|
||||||
|
>
|
||||||
|
<span className="grid h-11 w-11 shrink-0 place-items-center rounded-xl bg-primary/15 text-primary">
|
||||||
|
<Icon className="h-5 w-5" aria-hidden />
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<span className="block font-display text-lg font-bold">{t}</span>
|
||||||
|
<span className="mt-1 block text-sm leading-relaxed text-muted-foreground">
|
||||||
|
{d}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</Band>
|
||||||
|
|
||||||
|
<Band>
|
||||||
|
<SectionHeader
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
A ten-year path to <Accent>national scale.</Accent>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p>The roadmap DoorMile has set out for Wings.</p>
|
||||||
|
</SectionHeader>
|
||||||
|
<Timeline
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
label: "Year 1",
|
||||||
|
title: "Launch",
|
||||||
|
body: "Clearance hubs and cargo services across 5–6 key commercial cities.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Year 2",
|
||||||
|
title: "Network growth",
|
||||||
|
body: "Serving 15 regional and secondary cargo airports.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Year 3",
|
||||||
|
title: "Cross-border",
|
||||||
|
body: "International regional routes across South Asian freight corridors.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Year 5",
|
||||||
|
title: "Scale",
|
||||||
|
body: "A fleet of 25 leased cargo aircraft serving 50+ regional airports.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Year 10",
|
||||||
|
title: "Global",
|
||||||
|
body: "The asset-light, AI-powered model replicated across Asia, Africa and the Middle East.",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Band>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
// Loyaly.ai (from loyaly.ai)
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function LoyalyStory() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Band light>
|
||||||
|
<div className="grid gap-12 lg:grid-cols-12 lg:gap-16">
|
||||||
|
<div className="lg:col-span-5">
|
||||||
|
<SectionHeader
|
||||||
|
eyebrow="The problem"
|
||||||
|
icon={Eye}
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
Retail is busy. <Accent>But blind.</Accent>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Stores struggle to win new customers and keep existing ones without giving away
|
||||||
|
margin on generic discounts.
|
||||||
|
</p>
|
||||||
|
</SectionHeader>
|
||||||
|
<div className="mt-10 font-display text-7xl font-extrabold tracking-tighter text-primary md:text-8xl">
|
||||||
|
68%+
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 font-display text-lg font-semibold">
|
||||||
|
of shoppers walk in once and never return.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ol className="lg:col-span-7">
|
||||||
|
{[
|
||||||
|
"No compelling reason to visit the store",
|
||||||
|
"Discounts erode profit margins",
|
||||||
|
"Digital ads rarely turn into footfall",
|
||||||
|
"No real-time link between online and offline",
|
||||||
|
"Customers forget brands between visits",
|
||||||
|
].map((p, k) => (
|
||||||
|
<li key={p} className="relative flex items-baseline gap-6 py-6">
|
||||||
|
<span aria-hidden className="absolute inset-x-0 top-0 h-px bg-border" />
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className="line-draw absolute inset-x-0 top-0 h-px bg-primary/60"
|
||||||
|
/>
|
||||||
|
<span className="w-10 shrink-0 font-display text-sm font-semibold tabular-nums text-primary">
|
||||||
|
0{k + 1}
|
||||||
|
</span>
|
||||||
|
<span className="font-display text-2xl font-bold tracking-tight">{p}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</Band>
|
||||||
|
|
||||||
|
<Band>
|
||||||
|
<SectionHeader
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
Every visit, <Accent>a loyal customer.</Accent>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Loyaly bridges digital engagement and offline shopping in one loop that resets with
|
||||||
|
every visit.
|
||||||
|
</p>
|
||||||
|
</SectionHeader>
|
||||||
|
<StepRow
|
||||||
|
items={[
|
||||||
|
{ title: "Understand", body: "See who walks in, when, and why they buy.", icon: Eye },
|
||||||
|
{
|
||||||
|
title: "Engage",
|
||||||
|
body: "Daily streaks, challenges and mini games build a habit.",
|
||||||
|
icon: Gamepad2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Convert",
|
||||||
|
body: "Shoppers play to unlock a deal, then buy on the spot.",
|
||||||
|
icon: Tag,
|
||||||
|
},
|
||||||
|
{ title: "Retain", body: "Every redemption sets up the next visit.", icon: Users },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Band>
|
||||||
|
|
||||||
|
<Band light>
|
||||||
|
<SectionHeader
|
||||||
|
eyebrow="The platform"
|
||||||
|
icon={Brain}
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
Four agents. <Accent>One AI.</Accent>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StackedCards
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
title: "Behavision",
|
||||||
|
body: "Footfall analytics: how customers move, dwell and engage in store.",
|
||||||
|
icon: Eye,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "SpendSense",
|
||||||
|
body: "Spending intelligence: high-value segments and buying behaviour.",
|
||||||
|
icon: BarChart3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Dyscount",
|
||||||
|
body: "Smart discounts: the right offer, to the right shopper, without killing margin.",
|
||||||
|
icon: Tag,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Identiq",
|
||||||
|
body: "Consumer loyalty: earn LYTs online, redeem them in real stores.",
|
||||||
|
icon: Gift,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Band>
|
||||||
|
|
||||||
|
<Band>
|
||||||
|
<SectionHeader
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
For shoppers: <Accent>earn as you live.</Accent>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p>Through the LytsUp app, shoppers earn LYTs online and spend them in store.</p>
|
||||||
|
</SectionHeader>
|
||||||
|
<Timeline
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
label: "Step 1",
|
||||||
|
title: "Earn online",
|
||||||
|
body: "Daily logins, step challenges and games like Maze and Spin Wheel earn LYTs and surprise coupons.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Step 2",
|
||||||
|
title: "Unlock deals",
|
||||||
|
body: "Merchants run LYT campaigns; shoppers play once to reveal their reward.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Step 3",
|
||||||
|
title: "Discover and visit",
|
||||||
|
body: "Find participating stores nearby, then walk in and shop in person.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Step 4",
|
||||||
|
title: "Redeem and repeat",
|
||||||
|
body: "Use LYTs and coupons at checkout, then come back tomorrow to earn more.",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Band>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ import { Route as ContactRouteImport } from './routes/contact'
|
|||||||
import { Route as AiRouteImport } from './routes/ai'
|
import { Route as AiRouteImport } from './routes/ai'
|
||||||
import { Route as AboutRouteImport } from './routes/about'
|
import { Route as AboutRouteImport } from './routes/about'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
|
import { Route as VenturesSlugRouteImport } from './routes/ventures/$slug'
|
||||||
|
|
||||||
const StudioRoute = StudioRouteImport.update({
|
const StudioRoute = StudioRouteImport.update({
|
||||||
id: '/studio',
|
id: '/studio',
|
||||||
@@ -64,6 +65,11 @@ const IndexRoute = IndexRouteImport.update({
|
|||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const VenturesSlugRoute = VenturesSlugRouteImport.update({
|
||||||
|
id: '/ventures/$slug',
|
||||||
|
path: '/ventures/$slug',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
@@ -75,6 +81,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/portfolio': typeof PortfolioRoute
|
'/portfolio': typeof PortfolioRoute
|
||||||
'/sitemap.xml': typeof SitemapDotxmlRoute
|
'/sitemap.xml': typeof SitemapDotxmlRoute
|
||||||
'/studio': typeof StudioRoute
|
'/studio': typeof StudioRoute
|
||||||
|
'/ventures/$slug': typeof VenturesSlugRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
@@ -86,6 +93,7 @@ export interface FileRoutesByTo {
|
|||||||
'/portfolio': typeof PortfolioRoute
|
'/portfolio': typeof PortfolioRoute
|
||||||
'/sitemap.xml': typeof SitemapDotxmlRoute
|
'/sitemap.xml': typeof SitemapDotxmlRoute
|
||||||
'/studio': typeof StudioRoute
|
'/studio': typeof StudioRoute
|
||||||
|
'/ventures/$slug': typeof VenturesSlugRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
@@ -98,6 +106,7 @@ export interface FileRoutesById {
|
|||||||
'/portfolio': typeof PortfolioRoute
|
'/portfolio': typeof PortfolioRoute
|
||||||
'/sitemap.xml': typeof SitemapDotxmlRoute
|
'/sitemap.xml': typeof SitemapDotxmlRoute
|
||||||
'/studio': typeof StudioRoute
|
'/studio': typeof StudioRoute
|
||||||
|
'/ventures/$slug': typeof VenturesSlugRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
@@ -111,6 +120,7 @@ export interface FileRouteTypes {
|
|||||||
| '/portfolio'
|
| '/portfolio'
|
||||||
| '/sitemap.xml'
|
| '/sitemap.xml'
|
||||||
| '/studio'
|
| '/studio'
|
||||||
|
| '/ventures/$slug'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
| '/'
|
| '/'
|
||||||
@@ -122,6 +132,7 @@ export interface FileRouteTypes {
|
|||||||
| '/portfolio'
|
| '/portfolio'
|
||||||
| '/sitemap.xml'
|
| '/sitemap.xml'
|
||||||
| '/studio'
|
| '/studio'
|
||||||
|
| '/ventures/$slug'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
@@ -133,6 +144,7 @@ export interface FileRouteTypes {
|
|||||||
| '/portfolio'
|
| '/portfolio'
|
||||||
| '/sitemap.xml'
|
| '/sitemap.xml'
|
||||||
| '/studio'
|
| '/studio'
|
||||||
|
| '/ventures/$slug'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
@@ -145,6 +157,7 @@ export interface RootRouteChildren {
|
|||||||
PortfolioRoute: typeof PortfolioRoute
|
PortfolioRoute: typeof PortfolioRoute
|
||||||
SitemapDotxmlRoute: typeof SitemapDotxmlRoute
|
SitemapDotxmlRoute: typeof SitemapDotxmlRoute
|
||||||
StudioRoute: typeof StudioRoute
|
StudioRoute: typeof StudioRoute
|
||||||
|
VenturesSlugRoute: typeof VenturesSlugRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
@@ -212,6 +225,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof IndexRouteImport
|
preLoaderRoute: typeof IndexRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/ventures/$slug': {
|
||||||
|
id: '/ventures/$slug'
|
||||||
|
path: '/ventures/$slug'
|
||||||
|
fullPath: '/ventures/$slug'
|
||||||
|
preLoaderRoute: typeof VenturesSlugRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,6 +245,7 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
PortfolioRoute: PortfolioRoute,
|
PortfolioRoute: PortfolioRoute,
|
||||||
SitemapDotxmlRoute: SitemapDotxmlRoute,
|
SitemapDotxmlRoute: SitemapDotxmlRoute,
|
||||||
StudioRoute: StudioRoute,
|
StudioRoute: StudioRoute,
|
||||||
|
VenturesSlugRoute: VenturesSlugRoute,
|
||||||
}
|
}
|
||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
._addFileChildren(rootRouteChildren)
|
._addFileChildren(rootRouteChildren)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import type {} from "@tanstack/react-start";
|
import type {} from "@tanstack/react-start";
|
||||||
|
import { VENTURES } from "@/components/site/pages";
|
||||||
|
|
||||||
const BASE_URL = "";
|
const BASE_URL = "";
|
||||||
|
|
||||||
@@ -12,6 +13,11 @@ export const Route = createFileRoute("/sitemap.xml")({
|
|||||||
...["/studio", "/framework", "/portfolio", "/ai", "/investors", "/about", "/contact"].map(
|
...["/studio", "/framework", "/portfolio", "/ai", "/investors", "/about", "/contact"].map(
|
||||||
(path) => ({ path, changefreq: "monthly", priority: "0.8" }),
|
(path) => ({ path, changefreq: "monthly", priority: "0.8" }),
|
||||||
),
|
),
|
||||||
|
...VENTURES.map((v) => ({
|
||||||
|
path: `/ventures/${v.slug}`,
|
||||||
|
changefreq: "monthly",
|
||||||
|
priority: "0.7",
|
||||||
|
})),
|
||||||
];
|
];
|
||||||
const urls = entries.map(
|
const urls = entries.map(
|
||||||
(e) =>
|
(e) =>
|
||||||
|
|||||||
30
src/routes/ventures/$slug.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { createFileRoute, notFound } from "@tanstack/react-router";
|
||||||
|
import { SitePage } from "@/components/site/sections";
|
||||||
|
import { VentureDetail, findVenture } from "@/components/site/pages";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/ventures/$slug")({
|
||||||
|
loader: ({ params }) => {
|
||||||
|
const v = findVenture(params.slug);
|
||||||
|
if (!v) throw notFound();
|
||||||
|
return { name: v.name, pitch: v.pitch };
|
||||||
|
},
|
||||||
|
head: ({ loaderData }) => ({
|
||||||
|
meta: loaderData
|
||||||
|
? [
|
||||||
|
{ title: `${loaderData.name} | KapFoundry` },
|
||||||
|
{ name: "description", content: loaderData.pitch },
|
||||||
|
{ property: "og:title", content: `${loaderData.name}, a KapFoundry venture` },
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
}),
|
||||||
|
component: VenturePage,
|
||||||
|
});
|
||||||
|
|
||||||
|
function VenturePage() {
|
||||||
|
const { slug } = Route.useParams();
|
||||||
|
return (
|
||||||
|
<SitePage page="portfolio">
|
||||||
|
<VentureDetail slug={slug} />
|
||||||
|
</SitePage>
|
||||||
|
);
|
||||||
|
}
|
||||||
188
src/styles.css
@@ -855,6 +855,194 @@ html.intro .intro-overlay {
|
|||||||
rotate: 180deg;
|
rotate: 180deg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------------------------
|
||||||
|
Venture pages: scroll-driven motion that acts out each section. Browsers without scroll
|
||||||
|
timelines, and reduced motion, simply show the finished state.
|
||||||
|
------------------------------------------------------------------------------------------ */
|
||||||
|
@keyframes venture-photo-card {
|
||||||
|
to {
|
||||||
|
clip-path: inset(10% 6% 16% 6% round 3rem);
|
||||||
|
scale: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes venture-name-spread {
|
||||||
|
to {
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
opacity: 0;
|
||||||
|
filter: blur(8px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes chain-lit {
|
||||||
|
from {
|
||||||
|
opacity: 0.2;
|
||||||
|
transform: translate3d(0, 14px, 0) scale(0.92);
|
||||||
|
filter: grayscale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes grow-y {
|
||||||
|
from {
|
||||||
|
transform: scaleY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes node-lit {
|
||||||
|
from {
|
||||||
|
background-color: var(--background);
|
||||||
|
border-color: color-mix(in oklab, var(--foreground) 20%, transparent);
|
||||||
|
scale: 0.7;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
background-color: var(--primary);
|
||||||
|
border-color: var(--primary);
|
||||||
|
scale: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes rise-staggered {
|
||||||
|
from {
|
||||||
|
transform: translate3d(0, calc(70px + var(--i, 0) * 55px), 0);
|
||||||
|
opacity: 0.15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes tilt-in {
|
||||||
|
from {
|
||||||
|
transform: perspective(900px) rotateX(32deg) translate3d(0, 50px, 0);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layer cards stack on top of one another as the list scrolls (pure position: sticky). */
|
||||||
|
.layer-stack > li {
|
||||||
|
position: sticky;
|
||||||
|
top: calc(5.5rem + var(--i, 0) * 1.25rem);
|
||||||
|
}
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.layer-stack > li {
|
||||||
|
top: calc(7rem + var(--i, 0) * 1.75rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports (animation-timeline: view()) {
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
/* Hero: the photo pulls back into a card and the name spreads apart as you leave. */
|
||||||
|
.venture-photo {
|
||||||
|
animation: venture-photo-card linear both;
|
||||||
|
animation-timeline: --hero;
|
||||||
|
animation-range: exit 0% exit 75%;
|
||||||
|
}
|
||||||
|
.venture-name {
|
||||||
|
animation: venture-name-spread linear both;
|
||||||
|
animation-timeline: --hero;
|
||||||
|
animation-range: exit 0% exit 55%;
|
||||||
|
}
|
||||||
|
/* Farm → Food → Consumer → Regeneration: each step lights up in turn. */
|
||||||
|
.chain-step {
|
||||||
|
animation: chain-lit linear both;
|
||||||
|
animation-timeline: view();
|
||||||
|
animation-range: entry calc(30% + var(--i, 0) * 12%) entry calc(60% + var(--i, 0) * 12%);
|
||||||
|
}
|
||||||
|
/* How it works: a stem grows down the steps, tracking the middle of the screen. */
|
||||||
|
.stem-grow {
|
||||||
|
transform-origin: top;
|
||||||
|
animation: grow-y linear both;
|
||||||
|
animation-timeline: view();
|
||||||
|
animation-range: cover 50vh cover calc(100% - 50vh);
|
||||||
|
}
|
||||||
|
.stem-node {
|
||||||
|
animation: node-lit linear both;
|
||||||
|
animation-timeline: view();
|
||||||
|
animation-range: cover 46% cover 50%;
|
||||||
|
}
|
||||||
|
/* Revenue columns rise at different speeds, like bars growing. */
|
||||||
|
.rise-staggered {
|
||||||
|
animation: rise-staggered linear both;
|
||||||
|
animation-timeline: view();
|
||||||
|
animation-range: entry 0% cover 45%;
|
||||||
|
}
|
||||||
|
/* Cards tilt up into place from a 3D angle. */
|
||||||
|
.tilt-in {
|
||||||
|
transform-origin: 50% 100%;
|
||||||
|
animation: tilt-in linear both;
|
||||||
|
animation-timeline: view();
|
||||||
|
animation-range: entry 0% entry 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Looping decoration pauses while its section is off screen (data-offscreen, set from JS). */
|
||||||
|
[data-offscreen]
|
||||||
|
:is(
|
||||||
|
.column-up,
|
||||||
|
.column-down,
|
||||||
|
.spin-slow,
|
||||||
|
.spin-slower,
|
||||||
|
.upright,
|
||||||
|
.signal,
|
||||||
|
.node-pulse,
|
||||||
|
.dot-wave,
|
||||||
|
.orbit-comet,
|
||||||
|
.scroll-cue,
|
||||||
|
.nudge-x,
|
||||||
|
.nudge-y,
|
||||||
|
.animate-ping,
|
||||||
|
.animate-pulse
|
||||||
|
) {
|
||||||
|
animation-play-state: paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------------------------
|
||||||
|
Header (Loyaly-style). Over the hero it is transparent; once scrolled it becomes a floating
|
||||||
|
cream island with rounded lower corners. The nav sits in a dark capsule either way.
|
||||||
|
------------------------------------------------------------------------------------------ */
|
||||||
|
.header-island {
|
||||||
|
margin-inline: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
transition:
|
||||||
|
margin 0.5s var(--ease-out-expo),
|
||||||
|
border-radius 0.5s var(--ease-out-expo),
|
||||||
|
background-color 0.4s ease,
|
||||||
|
box-shadow 0.4s ease;
|
||||||
|
}
|
||||||
|
.header-island.is-solid {
|
||||||
|
margin-inline: 0.5rem;
|
||||||
|
border-radius: 0 0 1.5rem 1.5rem;
|
||||||
|
background-color: oklch(0.975 0.012 85 / 0.97);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
box-shadow: 0 14px 40px -20px oklch(0.05 0.01 60 / 0.55);
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.header-island.is-solid {
|
||||||
|
margin-inline: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.nav-capsule {
|
||||||
|
background-color: oklch(0.13 0.008 60);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 1px oklch(1 0 0 / 0.08),
|
||||||
|
0 8px 24px -12px oklch(0.05 0.01 60 / 0.8);
|
||||||
|
}
|
||||||
|
/* Call to action: a cream pill over the hero, a gold outline on the cream island. */
|
||||||
|
.header-cta {
|
||||||
|
background-color: oklch(0.975 0.012 85);
|
||||||
|
color: oklch(0.17 0.01 60);
|
||||||
|
box-shadow: inset 0 0 0 1px transparent;
|
||||||
|
transition:
|
||||||
|
background-color 0.3s ease,
|
||||||
|
color 0.3s ease,
|
||||||
|
box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
.header-cta:hover {
|
||||||
|
background-color: var(--primary);
|
||||||
|
color: var(--primary-foreground);
|
||||||
|
}
|
||||||
|
.header-cta.is-outline {
|
||||||
|
background-color: transparent;
|
||||||
|
box-shadow: inset 0 0 0 1.5px oklch(0.62 0.12 68);
|
||||||
|
}
|
||||||
|
.header-cta.is-outline:hover {
|
||||||
|
background-color: oklch(0.62 0.12 68);
|
||||||
|
color: oklch(0.99 0.005 85);
|
||||||
|
}
|
||||||
|
|
||||||
/* Magnetic buttons: JS sets `translate`; this eases them back when the pointer leaves. */
|
/* Magnetic buttons: JS sets `translate`; this eases them back when the pointer leaves. */
|
||||||
[data-magnetic] {
|
[data-magnetic] {
|
||||||
transition: translate 0.6s var(--ease-out-expo);
|
transition: translate 0.6s var(--ease-out-expo);
|
||||||
|
|||||||