updated pages

This commit is contained in:
R-Bharathraj
2026-09-29 15:24:35 +05:30
parent bb6386e34c
commit 7f49d2cd8a
55 changed files with 3308 additions and 350 deletions

59
package-lock.json generated
View File

@@ -44,6 +44,7 @@
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
"lenis": "^1.3.26",
"lucide-react": "^0.575.0", "lucide-react": "^0.575.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-day-picker": "^9.14.0", "react-day-picker": "^9.14.0",
@@ -2500,7 +2501,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2517,7 +2517,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2534,7 +2533,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2551,7 +2549,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2568,7 +2565,6 @@
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2585,7 +2581,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2602,7 +2597,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2619,7 +2613,6 @@
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2636,7 +2629,6 @@
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2653,7 +2645,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2670,7 +2661,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2687,7 +2677,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2704,7 +2693,6 @@
"cpu": [ "cpu": [
"wasm32" "wasm32"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@@ -2723,7 +2711,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2740,7 +2727,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -5480,6 +5466,37 @@
"shell-quote": "^1.8.4" "shell-quote": "^1.8.4"
} }
}, },
"node_modules/lenis": {
"version": "1.3.26",
"resolved": "https://registry.npmjs.org/lenis/-/lenis-1.3.26.tgz",
"integrity": "sha512-s/xTCZCxTFvHbAN1OzuhNaN5YPJH2ail0XAkctKW1b+RUAG4nUL5UHLXwNko1h8aEeT2jspBXegMgPJd8zcuag==",
"license": "MIT",
"workspaces": [
"packages/*",
"playground",
"playground/*"
],
"funding": {
"type": "github",
"url": "https://github.com/sponsors/darkroomengineering"
},
"peerDependencies": {
"@nuxt/kit": ">=3.0.0",
"react": ">=17.0.0",
"vue": ">=3.0.0"
},
"peerDependenciesMeta": {
"@nuxt/kit": {
"optional": true
},
"react": {
"optional": true
},
"vue": {
"optional": true
}
}
},
"node_modules/levn": { "node_modules/levn": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -5927,18 +5944,6 @@
} }
} }
}, },
"node_modules/nitro/node_modules/lru-cache": {
"version": "11.5.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz",
"integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==",
"dev": true,
"license": "BlueOak-1.0.0",
"optional": true,
"peer": true,
"engines": {
"node": "20 || >=22"
}
},
"node_modules/nitro/node_modules/unstorage": { "node_modules/nitro/node_modules/unstorage": {
"version": "2.0.0-alpha.7", "version": "2.0.0-alpha.7",
"resolved": "https://registry.npmjs.org/unstorage/-/unstorage-2.0.0-alpha.7.tgz", "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-2.0.0-alpha.7.tgz",

View File

@@ -50,6 +50,7 @@
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
"lenis": "^1.3.26",
"lucide-react": "^0.575.0", "lucide-react": "^0.575.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-day-picker": "^9.14.0", "react-day-picker": "^9.14.0",

View File

@@ -0,0 +1,19 @@
# Card photography
All photos are from [Unsplash](https://unsplash.com) under the
[Unsplash License](https://unsplash.com/license) (free for commercial use, no attribution
required; credited here anyway). Resized to 1200px WebP.
| File | Photographer | Unsplash photo |
| ---------------------- | ------------------ | --------------------------------------- |
| `studio-foundry.webp` | Morteza Mohammadi | https://unsplash.com/photos/ZLzkwU49BdY |
| `holding-tower.webp` | Verne Ho | https://unsplash.com/photos/0LAJfSNa-xQ |
| `compound-spiral.webp` | Jean-Luc Benazet | https://unsplash.com/photos/dLpP34ADQPI |
| `ai-vision.webp` | Yael Yañez | https://unsplash.com/photos/jw245ZR2lz4 |
| `ai-agentic.webp` | Gabriele Malaspina | https://unsplash.com/photos/CjWsslYVnPI |
| `ai-enterprise.webp` | Winston Chen | https://unsplash.com/photos/iZe21DzHnUg |
| `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-automation.webp` | Simon Kadula | https://unsplash.com/photos/8gr6bObQLOI |
| `ai-multiagent.webp` | James Wainscoat | https://unsplash.com/photos/b7MZ6iGIoSI |
| `ai-decision.webp` | Hassan Pasha | https://unsplash.com/photos/7SjEuEF06Zw |

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

View File

@@ -0,0 +1,32 @@
# Section photography
All photos are from [Unsplash](https://unsplash.com) under the
[Unsplash License](https://unsplash.com/license) (free for commercial use, no attribution
required; credited here anyway). Resized WebP.
| File | Photographer | Unsplash photo |
| --------------------------- | ------------------------- | --------------------------------------- |
| `stage-discover.webp` | Sergey Sokolov | https://unsplash.com/photos/-g657KN4Ah4 |
| `stage-validate.webp` | Nathália Rosa | https://unsplash.com/photos/P1SdQAhcJz8 |
| `stage-build.webp` | Abu Saeid | https://unsplash.com/photos/fdGTi4IcaJc |
| `stage-launch.webp` | SpaceX | https://unsplash.com/photos/TV2gg2kZD1o |
| `stage-scale.webp` | Mara F | https://unsplash.com/photos/tZdFQvqJuQQ |
| `stage-spinoff.webp` | Curioso Photography | https://unsplash.com/photos/OOiygaquXZs |
| `stack-engineering.webp` | Manuel | https://unsplash.com/photos/CANL3bzp6wU |
| `stack-design.webp` | Amélie Mourichon | https://unsplash.com/photos/sv8oOQaUb-o |
| `stack-cloud.webp` | Rebecca Campbell | https://unsplash.com/photos/5nN9NX6dJxc |
| `stack-legal.webp` | Sasun Bughdaryan | https://unsplash.com/photos/qVOJqYI37qE |
| `stack-finance.webp` | Kelly Sikkema | https://unsplash.com/photos/M98NRBuzbpc |
| `stack-marketing.webp` | Kayle Kaupanger | https://unsplash.com/photos/GVi0ijl8ExM |
| `stack-sales.webp` | Rock Staar | https://unsplash.com/photos/NzIV4vOBA7s |
| `stack-growth.webp` | Sable Flow | https://unsplash.com/photos/T74mVg__F_k |
| `stack-data.webp` | Luke Chesser | https://unsplash.com/photos/JKUTrJ4vK00 |
| `stack-governance.webp` | Danielle Cerullo | https://unsplash.com/photos/bIZJRVBLfOM |
| `vision-city.webp` | Pradeep Gopal (Bengaluru) | https://unsplash.com/photos/4J3CHmrFiIg |
| `investors-skyline.webp` | Himmel S | https://unsplash.com/photos/Io1lz5bZ0as |
| `principle-evidence.webp` | Mika Baumeister | https://unsplash.com/photos/Wpnoqo2plFA |
| `principle-engine.webp` | Nikita Turkovich | https://unsplash.com/photos/ioxS2dYRiJw |
| `principle-standalone.webp` | Nathan Jennings | https://unsplash.com/photos/VsPsf4F5Pi0 |
| `model-build.webp` | Art Institute of Chicago | https://unsplash.com/photos/sAlWjm2huck |
| `model-spinoff.webp` | Markus Spiske | https://unsplash.com/photos/vrbZVyX2k4I |
| `model-compound.webp` | Marek Studzinski | https://unsplash.com/photos/tnT54YSnlL0 |

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

View File

@@ -0,0 +1,540 @@
import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from "react";
import { Eye, Sparkles } from "lucide-react";
const REDUCED = "(prefers-reduced-motion: reduce)";
/** Milliseconds until the first-visit intro has lifted (0 when it isn't showing). */
function introRemaining() {
return document.documentElement.classList.contains("intro")
? Math.max(0, 1700 - performance.now())
: 0;
}
// ---------------------------------------------------------------------------------------------
// Molten mark: the brand mark rebuilt from gold particles
// ---------------------------------------------------------------------------------------------
type Particle = {
x: number;
y: number;
vx: number;
vy: number;
tx: number;
ty: number;
dx: number;
dy: number;
ph: number;
c: string;
};
/**
* The mark as ~1,500 particles sampled from the logo's own pixels. They fly in and cast the
* mark once the page is ready, scatter away from the cursor like sparks, and melt away as the
* section scrolls out. Reduced motion draws the finished mark once.
*/
export function MoltenMark({ src, className = "" }: { src: string; className?: string }) {
const wrapRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const wrap = wrapRef.current;
const canvas = canvasRef.current;
const ctx = canvas?.getContext("2d");
if (!wrap || !canvas || !ctx) return;
const reduce = window.matchMedia(REDUCED).matches;
const section = wrap.closest("section");
let W = 0;
let H = 0;
let size = 2;
let ps: Particle[] = [];
let raf = 0;
let visible = true;
let dead = false;
const pointer = { x: -1e4, y: -1e4 };
const castAt = performance.now() + introRemaining() + 250;
const img = new Image();
img.src = src;
const build = () => {
const r = wrap.getBoundingClientRect();
if (!r.width || !img.width) return;
const dpr = Math.min(2, window.devicePixelRatio || 1);
W = r.width;
H = r.height;
canvas.width = Math.round(W * dpr);
canvas.height = Math.round(H * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
// Sample the logo on a grid; each opaque cell becomes one particle in its own colour.
const fit = Math.min(W / img.width, H / img.height) * 0.86;
const sw = Math.round(img.width * fit);
const sh = Math.round(img.height * fit);
const gap = Math.max(3, Math.round(sw / 64));
size = gap * 0.62;
const off = document.createElement("canvas");
off.width = sw;
off.height = sh;
const o = off.getContext("2d", { willReadFrequently: true });
if (!o) return;
o.drawImage(img, 0, 0, sw, sh);
const data = o.getImageData(0, 0, sw, sh).data;
const ox = (W - sw) / 2;
const oy = (H - sh) / 2;
const old = ps;
ps = [];
for (let y = 0; y < sh; y += gap) {
for (let x = 0; x < sw; x += gap) {
const i = (y * sw + x) * 4;
if (data[i + 3] < 140) continue;
const prev = old[ps.length];
const a = Math.random() * Math.PI * 2;
const far = Math.max(W, H) * (0.6 + Math.random() * 0.8);
ps.push({
x: prev?.x ?? (reduce ? ox + x : W / 2 + Math.cos(a) * far),
y: prev?.y ?? (reduce ? oy + y : H / 2 + Math.sin(a) * far),
vx: 0,
vy: 0,
tx: ox + x,
ty: oy + y,
dx: Math.cos(a),
dy: Math.sin(a),
ph: Math.random() * Math.PI * 2,
c: `rgb(${data[i]},${data[i + 1]},${data[i + 2]})`,
});
}
}
};
const draw = (alpha: number) => {
ctx.clearRect(0, 0, W, H);
ctx.globalAlpha = alpha;
for (const p of ps) {
ctx.fillStyle = p.c;
ctx.fillRect(p.x, p.y, size, size);
}
};
const frame = (now: number) => {
raf = 0;
const t = now / 1000;
const cast = now >= castAt;
// Melt: 0 while the section fills the screen, 1 once it has scrolled away.
let melt = 0;
if (section) {
const r = section.getBoundingClientRect();
melt = Math.min(1, Math.max(0, -r.top / (r.height * 0.8)));
}
const R = 90;
for (const p of ps) {
let ax = 0;
let ay = 0;
if (cast) {
const gx = p.tx + p.dx * melt * W * 0.5 + Math.sin(t * 1.4 + p.ph) * 0.8;
const gy =
p.ty + p.dy * melt * H * 0.4 + melt * melt * H * 0.6 + Math.cos(t * 1.2 + p.ph) * 0.8;
ax = (gx - p.x) * 0.045;
ay = (gy - p.y) * 0.045;
}
const ddx = p.x - pointer.x;
const ddy = p.y - pointer.y;
const d2 = ddx * ddx + ddy * ddy;
if (d2 < R * R) {
const d = Math.sqrt(d2) || 1;
const f = (1 - d / R) * 7;
ax += (ddx / d) * f;
ay += (ddy / d) * f;
}
p.vx = (p.vx + ax) * 0.84;
p.vy = (p.vy + ay) * 0.84;
p.x += p.vx;
p.y += p.vy;
}
draw(1 - melt * 0.75);
if (visible && !dead) raf = requestAnimationFrame(frame);
};
const onMove = (e: PointerEvent) => {
const r = canvas.getBoundingClientRect();
pointer.x = e.clientX - r.left;
pointer.y = e.clientY - r.top;
};
const onLeave = () => {
pointer.x = pointer.y = -1e4;
};
const io = new IntersectionObserver(([entry]) => {
visible = entry.isIntersecting;
if (visible && !reduce && !raf) raf = requestAnimationFrame(frame);
});
const ro = new ResizeObserver(() => {
build();
if (reduce) draw(1);
});
img
.decode()
.catch(() => undefined)
.then(() => {
if (dead) return;
build();
if (reduce) {
draw(1);
} else {
window.addEventListener("pointermove", onMove, { passive: true });
document.documentElement.addEventListener("pointerleave", onLeave);
io.observe(wrap);
}
ro.observe(wrap);
});
return () => {
dead = true;
cancelAnimationFrame(raf);
io.disconnect();
ro.disconnect();
window.removeEventListener("pointermove", onMove);
document.documentElement.removeEventListener("pointerleave", onLeave);
};
}, [src]);
return (
<div ref={wrapRef} aria-hidden className={`relative ${className}`}>
<canvas ref={canvasRef} className="absolute inset-0 h-full w-full" />
</div>
);
}
// ---------------------------------------------------------------------------------------------
// Velocity ticker
// ---------------------------------------------------------------------------------------------
/**
* Two giant rows of words drifting in opposite directions. Scrolling speeds them up, leans
* them into the motion and flips their direction to follow the scroll.
*/
export function VelocityTicker({ words }: { words: string[] }) {
const rowRefs = useRef<(HTMLDivElement | null)[]>([]);
const wrapRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const wrap = wrapRef.current;
if (!wrap || window.matchMedia(REDUCED).matches) return;
const rows = rowRefs.current;
const x = [0, 0];
let last = window.scrollY;
let vel = 0;
let skew = 0;
let dir = 1;
let raf = 0;
let visible = false;
const loop = () => {
raf = 0;
const y = window.scrollY;
const d = y - last;
last = y;
if (d !== 0) dir = d > 0 ? 1 : -1;
vel += (d - vel) * 0.12;
skew += (Math.max(-12, Math.min(12, vel * 0.4)) - skew) * 0.12;
rows.forEach((el, i) => {
if (!el) return;
const half = el.scrollWidth / 2;
const speed = (0.7 + Math.min(Math.abs(vel), 60) * 0.3) * dir * (i ? -1 : 1);
x[i] = (x[i] - speed) % half;
if (x[i] > 0) x[i] -= half;
el.style.transform = `translate3d(${x[i].toFixed(1)}px,0,0) skewX(${(-skew).toFixed(2)}deg)`;
});
if (visible) raf = requestAnimationFrame(loop);
};
const io = new IntersectionObserver(([e]) => {
visible = e.isIntersecting;
last = window.scrollY;
if (visible && !raf) raf = requestAnimationFrame(loop);
});
io.observe(wrap);
return () => {
io.disconnect();
cancelAnimationFrame(raf);
};
}, []);
const row = (outline: boolean) =>
[0, 1].map((copy) => (
<div key={copy} className="flex shrink-0 items-center" aria-hidden={copy ? true : undefined}>
{words.map((w) => (
<span key={w} className="flex items-center">
<span
className={`px-6 font-display text-6xl font-extrabold uppercase leading-[1.05] tracking-tighter md:px-10 md:text-[9rem] ${
outline
? "text-transparent [-webkit-text-stroke:1.5px_color-mix(in_oklab,var(--primary)_70%,transparent)]"
: "text-foreground"
}`}
>
{w}
</span>
<Sparkles className="h-6 w-6 shrink-0 text-primary md:h-10 md:w-10" aria-hidden />
</span>
))}
</div>
));
return (
<div ref={wrapRef} className="overflow-hidden border-y border-border bg-ink py-8 md:py-14">
{[false, true].map((outline, i) => (
<div
key={i}
ref={(el) => {
rowRefs.current[i] = el;
}}
className="flex w-max will-change-transform"
>
{row(outline)}
</div>
))}
</div>
);
}
// ---------------------------------------------------------------------------------------------
// Look-closer lens
// ---------------------------------------------------------------------------------------------
/**
* One sentence you read, another you discover: a lens follows the cursor (or drifts on its own
* on touch screens and while idle) and swaps the text beneath it for the studio's answer.
*/
export function LensStatement({
before,
after,
children,
}: {
before: string;
after: string;
children?: ReactNode;
}) {
const stageRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const stage = stageRef.current;
if (!stage) return;
const reduce = window.matchMedia(REDUCED).matches;
let W = stage.offsetWidth;
let H = stage.offsetHeight;
let x = W * 0.3;
let y = H * 0.5;
let target: { x: number; y: number } | null = null;
let raf = 0;
let visible = false;
const t0 = performance.now();
const set = () => {
stage.style.setProperty("--lx", `${x.toFixed(1)}px`);
stage.style.setProperty("--ly", `${y.toFixed(1)}px`);
};
const loop = (now: number) => {
raf = 0;
const t = (now - t0) / 1000;
// Idle: a slow figure-of-eight across the text.
const gx = target ? target.x : W * (0.5 + 0.38 * Math.sin(t * 0.45));
const gy = target ? target.y : H * (0.5 + 0.3 * Math.sin(t * 0.9));
x += (gx - x) * (target ? 0.18 : 0.05);
y += (gy - y) * (target ? 0.18 : 0.05);
set();
if (visible) raf = requestAnimationFrame(loop);
};
if (reduce) {
x = W * 0.5;
set();
return;
}
const onMove = (e: PointerEvent) => {
if (e.pointerType !== "mouse") return;
const r = stage.getBoundingClientRect();
target = { x: e.clientX - r.left, y: e.clientY - r.top };
};
const onLeave = () => {
target = null;
};
const ro = new ResizeObserver(() => {
W = stage.offsetWidth;
H = stage.offsetHeight;
});
const io = new IntersectionObserver(([e]) => {
visible = e.isIntersecting;
if (visible && !raf) raf = requestAnimationFrame(loop);
});
stage.addEventListener("pointermove", onMove);
stage.addEventListener("pointerleave", onLeave);
ro.observe(stage);
io.observe(stage);
return () => {
stage.removeEventListener("pointermove", onMove);
stage.removeEventListener("pointerleave", onLeave);
ro.disconnect();
io.disconnect();
cancelAnimationFrame(raf);
};
}, []);
return (
<section className="section-y relative overflow-hidden bg-ink">
<div className="glow glow-drift absolute -left-40 top-0 h-[34rem] w-[34rem] opacity-30" />
<div className="container-x relative">
<div className="flex items-center justify-between gap-6">
<span className="eyebrow">
<Eye className="h-3.5 w-3.5" aria-hidden />
Look closer
</span>
<span className="hidden font-display text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground md:block">
Move over the text
</span>
</div>
<div
ref={stageRef}
data-cursor-hide
className="lens-stage relative mt-10 grid cursor-none py-6"
>
<p className="lens-base font-display text-4xl font-bold leading-[1.06] tracking-tight text-foreground/25 [grid-area:1/1] md:text-7xl">
{before}
</p>
<p
aria-hidden
className="lens-top font-display text-4xl font-bold leading-[1.06] tracking-tight text-primary [grid-area:1/1] md:text-7xl"
>
{after}
</p>
<span aria-hidden className="lens-ring" />
<p className="sr-only">{after}</p>
</div>
{children ? (
<div className="mt-12 max-w-[65ch] text-lg leading-relaxed text-muted-foreground">
{children}
</div>
) : null}
</div>
</section>
);
}
// ---------------------------------------------------------------------------------------------
// Industry index with a floating photo
// ---------------------------------------------------------------------------------------------
type IndexItem = { n: string; name: string; tag: string; img: string };
/**
* A typographic index: each industry is a giant line of type. Hovering one floats its photo
* under the cursor, leaning with the pointer's speed; the other lines recede. Touch screens
* get an inline thumbnail instead.
*/
export function IndustryIndex({ items }: { items: IndexItem[] }) {
const floatRef = useRef<HTMLDivElement>(null);
const [active, setActive] = useState<number | null>(null);
useEffect(() => {
const el = floatRef.current;
if (!el || !window.matchMedia("(pointer: fine)").matches) return;
let x = window.innerWidth / 2;
let y = window.innerHeight / 2;
let tx = x;
let ty = y;
let rot = 0;
let raf = 0;
const tick = () => {
raf = 0;
const dx = tx - x;
x += dx * 0.14;
y += (ty - y) * 0.14;
rot += (Math.max(-14, Math.min(14, dx * 0.12)) - rot) * 0.15;
el.style.transform = `translate3d(${x.toFixed(1)}px, ${y.toFixed(1)}px, 0) translate(-50%, -50%) rotate(${rot.toFixed(2)}deg)`;
if (Math.abs(dx) > 0.2 || Math.abs(ty - y) > 0.2 || Math.abs(rot) > 0.05)
raf = requestAnimationFrame(tick);
};
const onMove = (e: PointerEvent) => {
tx = e.clientX;
ty = e.clientY;
if (!raf) raf = requestAnimationFrame(tick);
};
window.addEventListener("pointermove", onMove, { passive: true });
return () => {
window.removeEventListener("pointermove", onMove);
cancelAnimationFrame(raf);
};
}, []);
return (
<section className="section-y relative bg-ink">
<div className="container-x">
<div className="flex items-end justify-between gap-6">
<h2 className="font-display text-4xl font-bold leading-[1.04] md:text-5xl">The index.</h2>
<span className="font-display text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground">
{String(items.length).padStart(2, "0")} industries
</span>
</div>
<ol className="group/list mt-12" onPointerLeave={() => setActive(null)}>
{items.map((it, i) => (
<li
key={it.n}
onPointerEnter={() => setActive(i)}
className="relative grid grid-cols-12 items-center gap-4 border-t border-white/10 py-5 transition-opacity duration-500 last:border-b group-hover/list:opacity-30 hover:opacity-100! md:py-7"
>
<span className="col-span-2 font-display text-sm tabular-nums text-primary md:col-span-1">
{it.n}
</span>
<img
src={it.img}
alt=""
loading="lazy"
className="col-span-3 aspect-square w-full max-w-16 rounded-xl object-cover [@media(pointer:fine)]:hidden"
/>
<span
className={`col-span-7 font-display text-3xl font-extrabold uppercase leading-none tracking-tighter transition-[transform,color] duration-700 ease-[cubic-bezier(0.16,1,0.3,1)] sm:text-5xl md:col-span-8 md:text-6xl lg:text-7xl [@media(pointer:fine)]:col-span-10 md:[@media(pointer:fine)]:col-span-8 ${
active === i ? "translate-x-4 text-primary md:translate-x-8" : ""
}`}
>
{it.name}
</span>
<span className="col-span-3 hidden text-right text-sm text-muted-foreground md:block">
{it.tag}
</span>
</li>
))}
</ol>
</div>
{/* Floating photo, fine pointers only. */}
<div
ref={floatRef}
aria-hidden
className="pointer-events-none fixed left-0 top-0 z-40 hidden [@media(pointer:fine)]:block"
>
<div
className={`relative aspect-[4/5] w-72 overflow-hidden rounded-2xl shadow-2xl shadow-black/60 transition-[opacity,scale] duration-500 ease-[cubic-bezier(0.16,1,0.3,1)] ${
active === null ? "scale-50 opacity-0" : "scale-100 opacity-100"
}`}
>
{items.map((it, i) => (
<img
key={it.n}
src={it.img}
alt=""
loading="lazy"
className={`absolute inset-0 h-full w-full object-cover transition-[clip-path,scale] duration-700 ease-[cubic-bezier(0.16,1,0.3,1)] ${
active === i
? "scale-100 [clip-path:inset(0)]"
: "scale-125 [clip-path:inset(100%_0_0_0)]"
}`}
style={{ zIndex: active === i ? 2 : 1 } as CSSProperties}
/>
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,243 @@
import { useEffect, useRef, type CSSProperties } from "react";
import Lenis from "lenis";
const MOTION_OK = "(prefers-reduced-motion: no-preference)";
/**
* Inertial smooth scrolling for wheel and trackpad (touch keeps native scrolling). Pauses while
* the mobile menu holds the page (html.menu-open) and hands anchor links a smooth glide.
*/
export function useSmoothScroll() {
useEffect(() => {
if (!window.matchMedia(MOTION_OK).matches) return;
const lenis = new Lenis({ lerp: 0.1, anchors: { offset: -80 }, autoRaf: true });
const root = document.documentElement;
const sync = () => (root.classList.contains("menu-open") ? lenis.stop() : lenis.start());
const watch = new MutationObserver(sync);
watch.observe(root, { attributes: true, attributeFilter: ["class"] });
return () => {
watch.disconnect();
lenis.destroy();
};
}, []);
}
const LINKISH = "a, button, [role='button'], summary, [data-cursor]";
const TEXTISH = "input, textarea, select, [contenteditable='true']";
/**
* A ring that trails the pointer: swells over links, shows the data-cursor label (e.g. "Next")
* and steps aside over text fields. Also drives [data-magnetic] buttons, which lean toward the
* pointer while it is over them. Fine pointers only.
*/
export function Cursor() {
const ref = useRef<HTMLDivElement>(null);
const labelRef = useRef<HTMLSpanElement>(null);
useEffect(() => {
const el = ref.current;
const label = labelRef.current;
if (!el || !label) return;
if (!window.matchMedia(`(pointer: fine) and ${MOTION_OK}`).matches) return;
let x = -100;
let y = -100;
let tx = -100;
let ty = -100;
let frame = 0;
let magnet: HTMLElement | null = null;
const tick = () => {
frame = 0;
x += (tx - x) * 0.22;
y += (ty - y) * 0.22;
el.style.transform = `translate3d(${x.toFixed(1)}px, ${y.toFixed(1)}px, 0)`;
if (Math.abs(tx - x) > 0.1 || Math.abs(ty - y) > 0.1) frame = requestAnimationFrame(tick);
};
const release = () => {
if (magnet) magnet.style.translate = "";
magnet = null;
};
const onMove = (e: PointerEvent) => {
if (e.pointerType !== "mouse") return;
tx = e.clientX;
ty = e.clientY;
el.dataset.on = "true";
if (!frame) frame = requestAnimationFrame(tick);
const target = e.target instanceof Element ? e.target : null;
const labelled = target?.closest<HTMLElement>("[data-cursor]");
if (target?.closest(`${TEXTISH}, [data-cursor-hide]`)) el.dataset.state = "text";
else if (labelled) {
el.dataset.state = "label";
label.textContent = labelled.dataset.cursor ?? "";
} else if (target?.closest(LINKISH)) el.dataset.state = "link";
else el.dataset.state = "";
const m = target?.closest<HTMLElement>("[data-magnetic]") ?? null;
if (m !== magnet) release();
if (m) {
magnet = m;
const r = m.getBoundingClientRect();
const dx = (e.clientX - (r.left + r.width / 2)) * 0.3;
const dy = (e.clientY - (r.top + r.height / 2)) * 0.4;
m.style.translate = `${dx.toFixed(1)}px ${dy.toFixed(1)}px`;
}
};
const onLeave = () => {
el.dataset.on = "false";
release();
};
window.addEventListener("pointermove", onMove, { passive: true });
document.documentElement.addEventListener("pointerleave", onLeave);
return () => {
window.removeEventListener("pointermove", onMove);
document.documentElement.removeEventListener("pointerleave", onLeave);
cancelAnimationFrame(frame);
release();
};
}, []);
return (
<div ref={ref} aria-hidden className="cursor">
<div className="cursor-ring">
<span ref={labelRef} className="cursor-label" />
</div>
</div>
);
}
const GLYPHS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#%&*+/";
/**
* Decodes an element's text from random glyphs, left to right. Only touches a lone text node
* (never React's structure), and holds the element's width so nothing around it shifts.
*/
export function scramble(el: HTMLElement) {
const node = el.firstChild;
if (
el.dataset.scrambling ||
!node ||
node.nodeType !== Node.TEXT_NODE ||
el.childNodes.length > 1
)
return;
const text = el.dataset.text ?? node.nodeValue ?? "";
el.dataset.text = text;
el.dataset.scrambling = "1";
el.style.minWidth = `${el.getBoundingClientRect().width}px`;
const steps = Math.min(22, text.length + 8);
let step = 0;
const id = window.setInterval(() => {
step++;
const shown = Math.floor((step / steps) * text.length);
node.nodeValue = Array.from(text, (ch, i) =>
i < shown || ch === " " ? ch : GLYPHS[(Math.random() * GLYPHS.length) | 0],
).join("");
if (step >= steps) {
window.clearInterval(id);
node.nodeValue = text;
delete el.dataset.scrambling;
el.style.minWidth = "";
}
}, 32);
}
/**
* [data-scramble] elements decode on hover; [data-scramble="in"] ones also decode once on
* arrival (after the intro, when it is showing).
*/
export function useScramble() {
useEffect(() => {
if (!window.matchMedia(MOTION_OK).matches) return;
const onOver = (e: PointerEvent) => {
const el =
e.target instanceof Element ? e.target.closest<HTMLElement>("[data-scramble]") : null;
if (el && !(e.relatedTarget instanceof Node && el.contains(e.relatedTarget))) scramble(el);
};
document.addEventListener("pointerover", onOver);
const wait = document.documentElement.classList.contains("intro") ? 1800 : 350;
const timer = window.setTimeout(() => {
document.querySelectorAll<HTMLElement>('[data-scramble="in"]').forEach(scramble);
}, wait);
return () => {
document.removeEventListener("pointerover", onOver);
window.clearTimeout(timer);
};
}, []);
}
/** A title segment: plain words, or words picked out in the accent colour. */
export type TitlePart = string | { accent: string };
/**
* Headline whose words rise one after another through their own masks (see .word).
* Screen readers get the sentence once, from a visually hidden copy.
*/
export function SplitTitle({ parts, start = 0 }: { parts: TitlePart[]; start?: number }) {
let w = start;
const text = parts.map((p) => (typeof p === "string" ? p : p.accent)).join(" ");
return (
<>
<span className="sr-only">{text}</span>
{parts.map((p, pi) => {
const accent = typeof p !== "string";
const words = (accent ? p.accent : p).split(" ").filter(Boolean);
return (
<span key={pi} aria-hidden className={accent ? "text-primary" : undefined}>
{words.map((word, i) => (
<span key={i}>
<span className="word-mask">
<span className="word" style={{ "--w": w++ } as CSSProperties}>
{word}
</span>
</span>{" "}
</span>
))}
</span>
);
})}
</>
);
}
/**
* First-visit intro. Always in the server HTML but only displayed when the head script
* (INTRO_SCRIPT) has put html.intro on the page, so it never flashes for returning visitors.
*/
export function Intro({ mark }: { mark: string }) {
return (
<div
aria-hidden
className="intro-overlay fixed inset-0 z-[80] flex-col justify-between bg-ink p-6 text-foreground md:p-10"
>
<div className="flex items-center justify-between font-display text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground">
<span>Venture studio</span>
<span>Bangalore, India</span>
</div>
<div className="flex flex-col items-center gap-6">
<img src={mark} alt="" width={201} height={192} className="h-16 w-auto md:h-20" />
<div className="font-display text-4xl font-extrabold tracking-tighter md:text-7xl">
<SplitTitle parts={["Build", { accent: "Bold." }]} />
</div>
</div>
<div>
<div className="flex items-end justify-between">
<span className="font-display text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground">
KapFoundry
</span>
<span className="intro-count font-display text-6xl font-extrabold tabular-nums leading-none text-primary md:text-8xl" />
</div>
<div className="mt-4 h-px bg-white/10">
<div className="intro-bar h-full bg-primary" />
</div>
</div>
</div>
);
}
/** Runs before first paint: turns the intro on once per session unless motion is reduced. */
export const INTRO_SCRIPT = `try{if(!sessionStorage.getItem("kf-intro")&&matchMedia("${MOTION_OK}").matches){document.documentElement.classList.add("intro");sessionStorage.setItem("kf-intro","1")}}catch(e){}`;

View File

@@ -0,0 +1,965 @@
import { useEffect, useState, type CSSProperties, type ReactNode } from "react";
import { ArrowRight, ArrowUpRight, Check, Compass, Layers, Minus, Sparkles } from "lucide-react";
import { CONTACT_EMAIL } from "@/lib/contact";
import { SplitTitle, type TitlePart } from "@/components/site/motion";
import { IndustryIndex, MoltenMark } from "@/components/site/creative";
// Section photography (Unsplash License): see src/assets/sections/CREDITS.md.
import principleEvidence from "@/assets/sections/principle-evidence.webp";
import principleEngine from "@/assets/sections/principle-engine.webp";
import principleStandalone from "@/assets/sections/principle-standalone.webp";
import modelBuild from "@/assets/sections/model-build.webp";
import modelSpinoff from "@/assets/sections/model-spinoff.webp";
import modelCompound from "@/assets/sections/model-compound.webp";
import {
Accent,
DuoPhoto,
EASE_STAGE,
Eyebrow,
FRAMEWORK,
FillWords,
NAV,
STACK,
SectionHeader,
VERTICALS,
logoFull,
logoMark,
orbitPosition,
reveal,
rise,
type SitePageName,
} from "@/components/site/sections";
/** Position of a page in the nav, as "01".."07". */
function pageNumber(page: SitePageName) {
return String(NAV.findIndex((n) => n.href === `/${page}`) + 1).padStart(2, "0");
}
// ---------------------------------------------------------------------------------------------
// Page hero
// ---------------------------------------------------------------------------------------------
/**
* Full-height opener for inner pages: page number and label, a headline whose words rise in
* one by one, the lead paragraph and a page-specific visual. As it scrolls away the visual
* drifts and the copy lifts (hero-timeline in styles.css).
*/
function PageHero({
page,
label,
title,
intro,
visual,
children,
}: {
page: SitePageName;
label: string;
title: TitlePart[];
intro: ReactNode;
visual: ReactNode;
children?: ReactNode;
}) {
return (
<section
id="top"
className="hero-timeline relative flex min-h-[100svh] flex-col overflow-hidden bg-ink"
>
<div className="hero-media absolute inset-0">
<div className="mesh-gold absolute inset-0 opacity-25" aria-hidden />
<div className="dot-grid absolute inset-0" aria-hidden />
<div
aria-hidden
className="absolute inset-0 opacity-20 lg:left-auto lg:w-[48%] lg:opacity-100 xl:w-[46%]"
{...rise(500)}
>
<div className="rise-in h-full w-full">{visual}</div>
</div>
<div
aria-hidden
className="absolute inset-0 bg-gradient-to-r from-ink via-ink/75 to-transparent lg:via-ink/40"
/>
<div className="absolute inset-x-0 bottom-0 h-40 bg-gradient-to-t from-ink to-transparent" />
</div>
<div className="hero-copy container-x relative flex flex-1 flex-col justify-between gap-12 pb-10 pt-28 md:pb-12 md:pt-36">
<div
{...rise(100)}
className="rise-in flex items-center gap-4 font-display text-xs font-semibold uppercase tracking-[0.2em]"
>
<span data-scramble="in" className="tabular-nums text-primary">
({pageNumber(page)})
</span>
<span className="h-px w-12 bg-primary/50" aria-hidden />
<span data-scramble="in" className="text-muted-foreground">
{label}
</span>
</div>
<h1 className="max-w-[14ch] font-display text-[clamp(2.25rem,10vw,3.5rem)] md:text-[clamp(3.5rem,6.6vw,6.25rem)] font-extrabold leading-[0.93] tracking-tighter">
<SplitTitle parts={title} />
</h1>
<div className="grid items-end gap-8 md:grid-cols-12">
<div className="md:col-span-7 lg:col-span-6">
<p
{...rise(700)}
className="rise-in max-w-[52ch] text-lg leading-relaxed text-muted-foreground md:text-xl"
>
{intro}
</p>
{children ? (
<div {...rise(850)} className="rise-in mt-8">
{children}
</div>
) : null}
</div>
<a
href="#main-content"
{...rise(1000)}
className="rise-in hidden items-center gap-4 justify-self-end font-display text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground transition-colors hover:text-primary md:col-span-5 md:flex lg:col-span-6"
>
Scroll to explore
<span className="relative block h-12 w-px overflow-hidden bg-white/15" aria-hidden>
<span className="scroll-cue absolute inset-x-0 top-0 h-1/2 bg-primary" />
</span>
</a>
</div>
</div>
{/* Target for the scroll cue: the first section after the hero. */}
<span id="main-content" className="absolute bottom-0" aria-hidden />
</section>
);
}
// ---------------------------------------------------------------------------------------------
// Hero visuals (decorative)
// ---------------------------------------------------------------------------------------------
/** Place n items evenly on a circle of the given radius (% of the box), starting at 12 o'clock. */
function ring(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)}%`,
};
}
/** Studio: the shared stack orbits the studio, the ventures circle closer in. */
function StudioVisual() {
return (
<div className="grid h-full place-items-center">
<div className="relative aspect-square w-[min(40rem,92%)]">
<div className="absolute inset-[3%] rounded-full border border-white/10" />
<div className="absolute inset-[23%] rounded-full border border-dashed border-primary/30" />
<div className="glow absolute inset-[20%]" />
<div className="spin-slow absolute inset-0">
{STACK.map((s, i) => (
<span
key={s.label}
style={ring(i, STACK.length, 47)}
className="absolute -translate-x-1/2 -translate-y-1/2"
>
<span className="upright block whitespace-nowrap rounded-full border border-white/10 bg-ink/80 px-3 py-1.5 font-display text-[11px] font-semibold uppercase tracking-wider text-muted-foreground backdrop-blur">
{s.label}
</span>
</span>
))}
</div>
<div className="spin-slower absolute inset-0">
{VERTICALS.map((v, i) => (
<span
key={v.n}
style={ring(i, VERTICALS.length, 27)}
className="absolute -translate-x-1/2 -translate-y-1/2"
>
<span className="upright flex items-center gap-2 whitespace-nowrap font-display text-[11px] font-bold uppercase tracking-wider text-primary">
<span className="h-2 w-2 rounded-full bg-primary shadow-[0_0_12px] shadow-primary" />
{v.short}
</span>
</span>
))}
</div>
<img
src={logoMark}
alt=""
width={201}
height={192}
className="absolute left-1/2 top-1/2 w-[18%] -translate-x-1/2 -translate-y-1/2 drop-shadow-[0_20px_40px_oklch(0.05_0.01_60/0.8)]"
/>
</div>
</div>
);
}
/** Framework: the six stages as a loop with a comet running round it. */
function FrameworkVisual() {
return (
<div className="grid h-full place-items-center">
<div className="relative aspect-square w-[min(38rem,90%)]">
<div className="absolute inset-[12.5%] rounded-full border border-white/10" />
<div
className="orbit-comet absolute inset-[12.5%] rounded-full"
style={{
background: "conic-gradient(from 0deg, transparent 0deg 250deg, var(--primary) 360deg)",
mask: "radial-gradient(farthest-side, transparent calc(100% - 3px), black calc(100% - 2px))",
}}
/>
<div className="glow absolute inset-[25%]" />
{FRAMEWORK.map((f, i) => {
const Icon = f.icon;
return (
<div
key={f.n}
style={orbitPosition(i, FRAMEWORK.length)}
className="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 border-primary/30 bg-ink/90 text-primary shadow-lg shadow-black/40 backdrop-blur">
<Icon className="h-5 w-5" />
</span>
<span className="whitespace-nowrap font-display text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
<span className="text-primary">{f.n}</span> {f.title}
</span>
</div>
);
})}
<div className="absolute inset-0 grid place-items-center text-center">
<div>
<div className="font-display text-7xl font-extrabold tracking-tighter text-foreground md:text-8xl">
06
</div>
<div className="mt-1 font-display text-xs font-semibold uppercase tracking-[0.2em] text-primary">
Stages, one loop
</div>
</div>
</div>
</div>
</div>
);
}
/** Portfolio: columns of vertical photos drifting past each other. */
function PortfolioVisual() {
const columns = [
{ items: VERTICALS, cls: "column-up" },
{ items: [...VERTICALS.slice(3), ...VERTICALS.slice(0, 3)], cls: "column-down" },
{ items: [...VERTICALS.slice(5), ...VERTICALS.slice(0, 5)], cls: "column-up" },
];
return (
<div className="h-full overflow-hidden">
<div className="grid h-[140%] -translate-y-[12%] rotate-[-8deg] scale-110 grid-cols-2 gap-4 px-4 sm:grid-cols-3">
{columns.map((col, c) => (
<div key={c} className={`overflow-hidden ${c === 2 ? "hidden sm:block" : ""}`}>
<div className={col.cls}>
{[...col.items, ...col.items].map((v, i) => (
<figure
key={i}
className="relative mb-4 aspect-[4/5] overflow-hidden rounded-2xl border border-white/10"
>
<img
src={v.img}
alt=""
decoding="async"
className="absolute inset-0 h-full w-full object-cover"
/>
<div className="absolute inset-x-0 bottom-0 h-1/3 bg-gradient-to-t from-ink/80 to-transparent" />
<figcaption className="absolute bottom-3 left-3 rounded-full bg-ink/70 px-3 py-1 font-display text-[11px] font-semibold uppercase tracking-wider text-primary backdrop-blur">
{v.short}
</figcaption>
</figure>
))}
</div>
</div>
))}
</div>
</div>
);
}
// Network layout for the AI visual, in a 400×400 viewBox.
const AI_LAYERS = [3, 5, 5, 3].map((count, l, all) =>
Array.from({ length: count }, (_, i) => ({
x: 50 + (l * 300) / (all.length - 1),
y: 200 + (i - (count - 1) / 2) * 62,
})),
);
const AI_EDGES = AI_LAYERS.slice(0, -1).flatMap((layer, l) =>
layer.flatMap((a) => AI_LAYERS[l + 1].map((b) => ({ a, b }))),
);
/** AI: a small neural network with signals flowing through it. */
function AIVisual() {
return (
<div className="grid h-full place-items-center">
<svg viewBox="0 0 400 400" className="w-[min(40rem,94%)] overflow-visible">
{AI_EDGES.map(({ a, b }, i) => (
<line
key={`e${i}`}
x1={a.x}
y1={a.y}
x2={b.x}
y2={b.y}
stroke="white"
strokeOpacity={0.07}
/>
))}
{AI_EDGES.map(({ a, b }, i) => (
<line
key={`s${i}`}
x1={a.x}
y1={a.y}
x2={b.x}
y2={b.y}
className="signal"
stroke="var(--primary)"
strokeWidth={1.5}
strokeLinecap="round"
style={{ "--d": `${-((i * 37) % 29) / 9}s` } as CSSProperties}
/>
))}
{AI_LAYERS.flat().map((n, i) => (
<g key={`n${i}`}>
<circle cx={n.x} cy={n.y} r={14} fill="var(--primary)" opacity={0.08} />
<circle
cx={n.x}
cy={n.y}
r={4}
fill="var(--primary)"
opacity={0.55}
className="node-pulse"
style={{ "--d": `${-((i * 13) % 11) / 4}s` } as CSSProperties}
/>
</g>
))}
</svg>
</div>
);
}
/** Investors: a field of companies, lighting up in a wave across the ecosystem. */
function InvestorsVisual() {
const cols = 11;
const rows = 11;
return (
<div className="grid h-full place-items-center">
<div
className="grid w-[min(34rem,88%)] gap-[3.2%]"
style={{ gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` }}
>
{Array.from({ length: cols * rows }, (_, i) => {
const r = Math.floor(i / cols);
const c = i % cols;
const hub = r === 5 && c === 5;
return (
<span
key={i}
className={`dot-wave aspect-square rounded-full ${
hub ? "bg-foreground" : (r * 7 + c * 3) % 5 === 0 ? "bg-primary" : "bg-primary/60"
}`}
style={{ "--d": `${((r + c) * 0.14).toFixed(2)}s` } as CSSProperties}
/>
);
})}
</div>
</div>
);
}
function AboutVisual() {
return (
<div className="grid h-full place-items-center">
<div className="relative">
<div className="glow absolute -inset-16" />
<div className="spin-slower absolute -inset-10 rounded-full border border-dashed border-primary/25" />
<MoltenMark src={logoMark} className="aspect-square w-[min(28rem,70vw)]" />
</div>
</div>
);
}
const IST = new Intl.DateTimeFormat("en-GB", {
timeZone: "Asia/Kolkata",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
});
/** Contact: the studio's local time inside a rotating ring of text. */
function ContactVisual() {
const [time, setTime] = useState<string | null>(null);
useEffect(() => {
const tick = () => setTime(IST.format(new Date()));
tick();
const id = window.setInterval(tick, 1000);
return () => window.clearInterval(id);
}, []);
return (
<div className="grid h-full place-items-center">
<div className="relative aspect-square w-[min(34rem,86%)]">
<svg viewBox="0 0 400 400" className="spin-slow absolute inset-0 overflow-visible">
<defs>
<path
id="contact-ring"
d="M200,200 m-170,0 a170,170 0 1,1 340,0 a170,170 0 1,1 -340,0"
/>
</defs>
<text
className="font-display"
fill="var(--primary)"
fontSize="17"
fontWeight="700"
letterSpacing="5"
>
<textPath href="#contact-ring" textLength="1060">
BUILD WITH US · INVEST WITH US · PARTNER WITH US ·
</textPath>
</text>
</svg>
<div className="absolute inset-[16%] rounded-full border border-white/10" />
<div className="glow absolute inset-[10%]" />
<div className="absolute inset-0 grid place-items-center text-center">
<div>
<div className="font-display text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground">
Bangalore, IST
</div>
<div className="mt-3 font-display text-5xl font-extrabold tabular-nums tracking-tight md:text-7xl">
{time ?? "--:--:--"}
</div>
<div className="mt-3 inline-flex items-center gap-2 text-sm text-primary">
<span className="h-2 w-2 animate-pulse rounded-full bg-primary" />
Studio open to new partners
</div>
</div>
</div>
</div>
</div>
);
}
// ---------------------------------------------------------------------------------------------
// Heroes per page
// ---------------------------------------------------------------------------------------------
export function StudioHero() {
return (
<PageHero
page="studio"
label="The studio"
title={["One studio.", "Many companies.", { accent: "One engine." }]}
intro="We don't fund companies after they form. We find the problem, build the company and scale it on infrastructure every venture shares."
visual={<StudioVisual />}
/>
);
}
export function FrameworkHero() {
return (
<PageHero
page="framework"
label="The framework"
title={["Company building,", { accent: "made repeatable." }]}
intro="Every venture moves through the same six stages, so each new company starts with the lessons of the last."
visual={<FrameworkVisual />}
/>
);
}
export function PortfolioHero() {
return (
<PageHero
page="portfolio"
label="The portfolio"
title={["Big problems.", { accent: "Bold companies." }]}
intro="We build where problems are large, fragmented and overdue for change: seven industries, one AI engine underneath them all."
visual={<PortfolioVisual />}
/>
);
}
export function AIHero() {
return (
<PageHero
page="ai"
label="Artificial intelligence"
title={["Intelligence,", { accent: "built in from day one." }]}
intro="AI isn't bolted on later. It shapes how every KapFoundry company is designed, operated and scaled."
visual={<AIVisual />}
/>
);
}
export function InvestorsHero() {
return (
<PageHero
page="investors"
label="For investors"
title={["One investment.", { accent: "Many companies." }]}
intro="Exposure to a portfolio of AI-first ventures across industries: risk spread wide, upside shared across every company the studio builds."
visual={<InvestorsVisual />}
>
<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"
>
Speak with our team
<ArrowRight className="h-4 w-4 transition group-hover:translate-x-0.5" aria-hidden />
</a>
</PageHero>
);
}
export function AboutHero() {
return (
<PageHero
page="about"
label="About KapFoundry"
title={["We don't wait for the future.", { accent: "We build it." }]}
intro="A venture studio and technology holding company that engineers category-defining, AI-first companies, before they even exist."
visual={<AboutVisual />}
/>
);
}
export function ContactHero() {
return (
<PageHero
page="contact"
label="Contact"
title={["Let's build", { accent: "what comes next." }]}
intro="Whether you want to invest, partner or build with us, tell us a little about yourself and our team will be in touch."
visual={<ContactVisual />}
>
<a
href={`mailto:${CONTACT_EMAIL}`}
data-cursor="Write"
className="group inline-flex items-center gap-3 border-b border-primary/40 pb-2 font-display text-2xl font-bold transition-colors hover:border-primary hover:text-primary md:text-3xl"
>
{CONTACT_EMAIL}
<ArrowUpRight
className="h-6 w-6 text-primary transition-transform duration-300 group-hover:-translate-y-1 group-hover:translate-x-1"
aria-hidden
/>
</a>
</PageHero>
);
}
// ---------------------------------------------------------------------------------------------
// New content sections
// ---------------------------------------------------------------------------------------------
const VC_ROWS = [
{
topic: "When capital arrives",
vc: "After a company already exists",
kf: "Before the company exists, once research proves the problem",
},
{
topic: "Who builds",
vc: "Founders alone, starting from scratch",
kf: "Founders with a studio team behind them from day one",
},
{
topic: "Infrastructure",
vc: "Assembled again, company by company",
kf: "Engineering, design, legal, finance and growth, built once and shared",
},
{
topic: "Technology",
vc: "AI added later, if at all",
kf: "AI-first architecture from the first line of code",
},
{
topic: "Risk",
vc: "Concentrated in a single bet",
kf: "Spread across a portfolio of ventures",
},
];
/** Studio: how the studio model differs from traditional venture capital, row by row. */
export function StudioVsVC() {
return (
<section className="sheet-enter theme-light section-y bg-background text-foreground">
<div className="container-x">
<SectionHeader
eyebrow="A different model"
icon={Compass}
title={
<>
Venture capital waits. <Accent>We build.</Accent>
</>
}
>
<p>
Traditional venture capital invests in startups after they exist. KapFoundry creates
them, which changes almost everything about how a company begins.
</p>
</SectionHeader>
<div className="mt-16">
<div className="hidden grid-cols-12 gap-6 pb-4 font-display text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground lg:grid">
<span className="col-span-3" />
<span className="col-span-4">Traditional venture capital</span>
<span className="col-span-5 text-primary">KapFoundry venture studio</span>
</div>
<ol>
{VC_ROWS.map((r, i) => (
<li
key={r.topic}
{...reveal("up", i * 40)}
className="group relative grid gap-3 py-7 lg:grid-cols-12 lg:gap-6 lg:py-9"
>
<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"
/>
<h3 className="font-display text-xl font-bold lg:col-span-3 lg:text-2xl">
<span className="mr-3 text-sm tabular-nums text-primary">0{i + 1}</span>
{r.topic}
</h3>
<p className="flex gap-3 text-muted-foreground lg:col-span-4">
<Minus className="mt-1 h-4 w-4 shrink-0 opacity-60" aria-hidden />
<span>
<span className="sr-only">Traditional venture capital: </span>
{r.vc}
</span>
</p>
<p
className={`flex gap-3 font-display text-lg font-semibold transition-transform duration-500 group-hover:translate-x-1 lg:col-span-5 lg:text-xl ${EASE_STAGE}`}
>
<Check className="mt-1 h-5 w-5 shrink-0 text-primary" aria-hidden />
<span>
<span className="sr-only">KapFoundry: </span>
{r.kf}
</span>
</p>
</li>
))}
</ol>
</div>
</div>
</section>
);
}
const PRINCIPLES = [
{
t: "Evidence before capital",
img: principleEvidence,
d: "Every venture begins with market research, customer discovery and product validation, before a single rupee is deployed.",
},
{
t: "One engine, many companies",
img: principleEngine,
d: "Engineering, design, legal, finance and growth are built once and shared, so each venture launches with a mature company's stack.",
},
{
t: "Built to stand alone",
img: principleStandalone,
d: "Each venture is designed to spin off into an independent, high-growth company that strengthens the wider ecosystem.",
},
];
/** Framework: the statement and principles behind the six stages. */
export function FrameworkPrinciples() {
return (
<section className="section-y relative overflow-hidden bg-ink">
<div className="glow glow-drift absolute -left-48 top-10 h-[36rem] w-[36rem] opacity-40" />
<div className="container-x relative">
<p className="max-w-5xl font-display text-4xl font-bold leading-[1.06] tracking-tight md:text-6xl">
<FillWords text="Great ideas fail on avoidable mistakes." />
<FillWords
text="A repeatable system catches them before capital is spent."
className="text-primary"
/>
</p>
<ol className="mt-20 grid gap-6 md:grid-cols-3" {...reveal("stagger")}>
{PRINCIPLES.map((p, i) => (
<li
key={p.t}
tabIndex={0}
className="group relative overflow-hidden rounded-3xl border border-border bg-card/60 transition-colors duration-500 hover:border-primary/50"
>
<div className="relative h-56 overflow-hidden">
<DuoPhoto src={p.img} />
<div className="absolute inset-0 bg-gradient-to-t from-card via-card/20 to-transparent" />
<span className="absolute bottom-3 left-8 font-display text-6xl font-extrabold tabular-nums text-transparent transition-colors duration-500 [-webkit-text-stroke:1px_color-mix(in_oklab,var(--primary)_80%,transparent)] group-hover:text-primary">
0{i + 1}
</span>
</div>
<div className="p-8 pt-5">
<h3 className="font-display text-2xl font-bold">{p.t}</h3>
<p className="mt-3 leading-relaxed text-muted-foreground">{p.d}</p>
</div>
</li>
))}
</ol>
</div>
</section>
);
}
const VENTURES = VERTICALS.filter((v) => v.example).map((v) => {
const [name, ...rest] = (v.example ?? "").split(". ");
return { name, pitch: rest.join(". "), vertical: v.name, img: v.img, icon: v.icon };
});
/** Portfolio: ventures already taking shape in the studio. */
export function Ventures() {
return (
<section className="sheet-enter theme-light section-y bg-background text-foreground">
<div className="container-x">
<SectionHeader
eyebrow="In the studio"
icon={Sparkles}
title={
<>
Ventures <Accent>taking shape.</Accent>
</>
}
>
<p>A first look at companies being engineered on the KapFoundry engine.</p>
</SectionHeader>
<div className="mt-16 grid gap-6 md:grid-cols-2">
{VENTURES.map((v, i) => {
const Icon = v.icon;
return (
<article
key={v.name}
{...reveal("clip", i * 120)}
className="theme-dark group relative flex min-h-[30rem] flex-col justify-end overflow-hidden rounded-[2rem] bg-background p-8 text-foreground md:min-h-[36rem] md:p-10"
>
<img
src={v.img}
alt=""
loading="lazy"
decoding="async"
className={`absolute inset-0 h-full w-full object-cover transition-transform duration-[1.6s] group-hover:scale-105 ${EASE_STAGE}`}
/>
<div className="absolute inset-0 bg-gradient-to-t from-ink via-ink/70 to-ink/10" />
<div className="relative">
<span className="inline-flex items-center gap-2 rounded-full bg-primary px-3 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 />
{v.vertical}
</span>
<h3 className="mt-6 font-display text-4xl font-extrabold tracking-tight lg:text-6xl">
{v.name}
</h3>
<p className="mt-4 max-w-[40ch] text-lg leading-relaxed text-muted-foreground">
{v.pitch}
</p>
</div>
</article>
);
})}
</div>
<a
href="/contact"
data-cursor="Talk"
{...reveal("up", 150)}
className="group mt-6 flex flex-col items-start justify-between gap-6 rounded-[2rem] border border-dashed border-foreground/25 p-8 transition-colors duration-500 hover:border-primary hover:bg-primary/5 md:flex-row md:items-center md:p-10"
>
<span>
<span className="block font-display text-xs font-semibold uppercase tracking-[0.16em] text-primary">
Next in the studio
</span>
<span className="mt-3 block font-display text-3xl font-bold tracking-tight md:text-4xl">
Build the next one with us.
</span>
</span>
<span className="grid h-16 w-16 shrink-0 place-items-center rounded-full bg-primary text-primary-foreground transition-transform duration-500 group-hover:rotate-45">
<ArrowUpRight className="h-6 w-6" aria-hidden />
</span>
</a>
</div>
</section>
);
}
/** AI: what the foundation does inside each vertical; the row at the centre of the screen is lit. */
export function AIAcrossVerticals() {
return (
<section className="section-y relative bg-ink">
<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")}>
<Eyebrow icon={Layers}>AI in every vertical</Eyebrow>
<h2 className="mt-6 font-display text-4xl font-bold leading-[1.04] md:text-5xl">
One foundation. <Accent>Seven industries.</Accent>
</h2>
<p className="mt-6 max-w-[48ch] text-lg leading-relaxed text-muted-foreground">
The same AI capabilities show up differently in every market we enter. Here is where
they do the heavy lifting.
</p>
</div>
</div>
<ol className="lg:col-span-7">
{VERTICALS.map((v) => {
const Icon = v.icon;
return (
<li
key={v.n}
className="row-focus border-t border-white/10 py-8 last:border-b md:py-10"
>
<div className="flex items-center gap-4">
<span className="relative h-14 w-14 shrink-0 overflow-hidden rounded-2xl md:h-16 md:w-16">
<img
src={v.img}
alt=""
loading="lazy"
decoding="async"
className="h-full w-full object-cover"
/>
<span className="absolute bottom-1 right-1 grid h-6 w-6 place-items-center rounded-lg bg-ink/80 text-primary backdrop-blur">
<Icon className="h-3.5 w-3.5" aria-hidden />
</span>
</span>
<h3 className="font-display text-2xl font-bold md:text-4xl">{v.name}</h3>
<span className="ml-auto font-display text-sm tabular-nums text-primary">
{v.n}
</span>
</div>
<ul className="mt-5 flex flex-wrap gap-2 md:pl-20">
{v.solution.slice(0, 3).map((s) => (
<li
key={s}
className="rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-sm text-foreground"
>
{s}
</li>
))}
</ul>
</li>
);
})}
</ol>
</div>
</section>
);
}
const MODEL = [
{
t: "The studio builds",
img: modelBuild,
d: "Ventures are researched, validated and built on KapFoundry's shared engine.",
},
{
t: "Ventures spin off",
img: modelSpinoff,
d: "Each company grows into an independent, high-growth business in its own market.",
},
{
t: "Value compounds",
img: modelCompound,
d: "Every success strengthens the ecosystem, and investors share in the portfolio, not a single bet.",
},
];
/** Investors: how value flows from the studio to the portfolio. */
export function InvestorModel() {
return (
<section className="sheet-enter theme-light section-y bg-background text-foreground">
<div className="container-x">
<SectionHeader
title={
<>
How value <Accent>compounds.</Accent>
</>
}
>
<p>A holding company at the centre, many independent companies around it.</p>
</SectionHeader>
<div className="relative mt-20">
<ol className="grid gap-12 md:grid-cols-3 md:gap-10" {...reveal("stagger")}>
{MODEL.map((m, i) => (
<li key={m.t} tabIndex={0} className="group relative">
<div className="theme-dark relative aspect-[4/3] overflow-hidden rounded-3xl bg-ink">
<DuoPhoto src={m.img} />
</div>
<span className="relative -mt-7 ml-6 grid h-14 w-14 place-items-center rounded-full bg-primary font-display text-lg font-bold tabular-nums text-primary-foreground shadow-lg shadow-primary/30 ring-4 ring-background">
0{i + 1}
</span>
<h3 className="mt-8 font-display text-3xl font-bold">{m.t}</h3>
<p className="mt-3 max-w-[36ch] text-lg leading-relaxed text-muted-foreground">
{m.d}
</p>
</li>
))}
</ol>
</div>
</div>
</section>
);
}
// ---------------------------------------------------------------------------------------------
// Next page handoff
// ---------------------------------------------------------------------------------------------
const NEXT_BLURB: Record<string, string> = {
"/studio": "How one shared engine builds many companies.",
"/framework": "Six stages from a billion-dollar problem to an independent company.",
"/portfolio": "The industries we're rebuilding, AI-first.",
"/ai": "The foundation under every venture we build.",
"/investors": "Exposure to a portfolio, not a single bet.",
"/about": "Our vision, our mission and the people behind them.",
"/contact": "Build with us, invest with us, or simply say hello.",
};
/** Giant link to the next page in nav order; grows in as it enters and fills gold on hover. */
export function NextPage({ page }: { page: SitePageName }) {
const i = NAV.findIndex((n) => n.href === `/${page}`);
const k = (i + 1) % NAV.length;
const next = NAV[k];
return (
<section
aria-label="Next page"
className="relative overflow-hidden border-t border-border bg-ink"
>
<div className="glow glow-drift absolute -right-40 top-0 h-[34rem] w-[34rem] opacity-30" />
<a href={next.href} data-cursor="Next" className="group relative block py-24 md:py-36">
<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 page</span>
<span className="tabular-nums">
<span className="text-primary">{String(k + 1).padStart(2, "0")}</span> /{" "}
{String(NAV.length).padStart(2, "0")}
</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,15vw,12rem)] md:text-[min(12vw,12rem)] font-extrabold leading-[0.9] tracking-tighter">
{next.label}
</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_BLURB[next.href]}</p>
</div>
</a>
</section>
);
}
/** Portfolio: the seven industries as a typographic index with floating photos. */
export function PortfolioIndex() {
return (
<IndustryIndex
items={VERTICALS.map((v) => ({ n: v.n, name: v.short, tag: v.tag, img: v.img }))}
/>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -9,15 +9,51 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root' import { Route as rootRouteImport } from './routes/__root'
import { Route as StudioRouteImport } from './routes/studio'
import { Route as SitemapDotxmlRouteImport } from './routes/sitemap[.]xml' import { Route as SitemapDotxmlRouteImport } from './routes/sitemap[.]xml'
import { Route as PortfolioRouteImport } from './routes/portfolio'
import { Route as InvestorsRouteImport } from './routes/investors'
import { Route as FrameworkRouteImport } from './routes/framework'
import { Route as ContactRouteImport } from './routes/contact'
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'
const StudioRoute = StudioRouteImport.update({
id: '/studio',
path: '/studio',
getParentRoute: () => rootRouteImport,
} as any)
const SitemapDotxmlRoute = SitemapDotxmlRouteImport.update({ const SitemapDotxmlRoute = SitemapDotxmlRouteImport.update({
id: '/sitemap.xml', id: '/sitemap.xml',
path: '/sitemap.xml', path: '/sitemap.xml',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const PortfolioRoute = PortfolioRouteImport.update({
id: '/portfolio',
path: '/portfolio',
getParentRoute: () => rootRouteImport,
} as any)
const InvestorsRoute = InvestorsRouteImport.update({
id: '/investors',
path: '/investors',
getParentRoute: () => rootRouteImport,
} as any)
const FrameworkRoute = FrameworkRouteImport.update({
id: '/framework',
path: '/framework',
getParentRoute: () => rootRouteImport,
} as any)
const ContactRoute = ContactRouteImport.update({
id: '/contact',
path: '/contact',
getParentRoute: () => rootRouteImport,
} as any)
const AiRoute = AiRouteImport.update({
id: '/ai',
path: '/ai',
getParentRoute: () => rootRouteImport,
} as any)
const AboutRoute = AboutRouteImport.update({ const AboutRoute = AboutRouteImport.update({
id: '/about', id: '/about',
path: '/about', path: '/about',
@@ -32,35 +68,94 @@ const IndexRoute = IndexRouteImport.update({
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/about': typeof AboutRoute '/about': typeof AboutRoute
'/ai': typeof AiRoute
'/contact': typeof ContactRoute
'/framework': typeof FrameworkRoute
'/investors': typeof InvestorsRoute
'/portfolio': typeof PortfolioRoute
'/sitemap.xml': typeof SitemapDotxmlRoute '/sitemap.xml': typeof SitemapDotxmlRoute
'/studio': typeof StudioRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/about': typeof AboutRoute '/about': typeof AboutRoute
'/ai': typeof AiRoute
'/contact': typeof ContactRoute
'/framework': typeof FrameworkRoute
'/investors': typeof InvestorsRoute
'/portfolio': typeof PortfolioRoute
'/sitemap.xml': typeof SitemapDotxmlRoute '/sitemap.xml': typeof SitemapDotxmlRoute
'/studio': typeof StudioRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
__root__: typeof rootRouteImport __root__: typeof rootRouteImport
'/': typeof IndexRoute '/': typeof IndexRoute
'/about': typeof AboutRoute '/about': typeof AboutRoute
'/ai': typeof AiRoute
'/contact': typeof ContactRoute
'/framework': typeof FrameworkRoute
'/investors': typeof InvestorsRoute
'/portfolio': typeof PortfolioRoute
'/sitemap.xml': typeof SitemapDotxmlRoute '/sitemap.xml': typeof SitemapDotxmlRoute
'/studio': typeof StudioRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/about' | '/sitemap.xml' fullPaths:
| '/'
| '/about'
| '/ai'
| '/contact'
| '/framework'
| '/investors'
| '/portfolio'
| '/sitemap.xml'
| '/studio'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: '/' | '/about' | '/sitemap.xml' to:
id: '__root__' | '/' | '/about' | '/sitemap.xml' | '/'
| '/about'
| '/ai'
| '/contact'
| '/framework'
| '/investors'
| '/portfolio'
| '/sitemap.xml'
| '/studio'
id:
| '__root__'
| '/'
| '/about'
| '/ai'
| '/contact'
| '/framework'
| '/investors'
| '/portfolio'
| '/sitemap.xml'
| '/studio'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
export interface RootRouteChildren { export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute
AboutRoute: typeof AboutRoute AboutRoute: typeof AboutRoute
AiRoute: typeof AiRoute
ContactRoute: typeof ContactRoute
FrameworkRoute: typeof FrameworkRoute
InvestorsRoute: typeof InvestorsRoute
PortfolioRoute: typeof PortfolioRoute
SitemapDotxmlRoute: typeof SitemapDotxmlRoute SitemapDotxmlRoute: typeof SitemapDotxmlRoute
StudioRoute: typeof StudioRoute
} }
declare module '@tanstack/react-router' { declare module '@tanstack/react-router' {
interface FileRoutesByPath { interface FileRoutesByPath {
'/studio': {
id: '/studio'
path: '/studio'
fullPath: '/studio'
preLoaderRoute: typeof StudioRouteImport
parentRoute: typeof rootRouteImport
}
'/sitemap.xml': { '/sitemap.xml': {
id: '/sitemap.xml' id: '/sitemap.xml'
path: '/sitemap.xml' path: '/sitemap.xml'
@@ -68,6 +163,41 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof SitemapDotxmlRouteImport preLoaderRoute: typeof SitemapDotxmlRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/portfolio': {
id: '/portfolio'
path: '/portfolio'
fullPath: '/portfolio'
preLoaderRoute: typeof PortfolioRouteImport
parentRoute: typeof rootRouteImport
}
'/investors': {
id: '/investors'
path: '/investors'
fullPath: '/investors'
preLoaderRoute: typeof InvestorsRouteImport
parentRoute: typeof rootRouteImport
}
'/framework': {
id: '/framework'
path: '/framework'
fullPath: '/framework'
preLoaderRoute: typeof FrameworkRouteImport
parentRoute: typeof rootRouteImport
}
'/contact': {
id: '/contact'
path: '/contact'
fullPath: '/contact'
preLoaderRoute: typeof ContactRouteImport
parentRoute: typeof rootRouteImport
}
'/ai': {
id: '/ai'
path: '/ai'
fullPath: '/ai'
preLoaderRoute: typeof AiRouteImport
parentRoute: typeof rootRouteImport
}
'/about': { '/about': {
id: '/about' id: '/about'
path: '/about' path: '/about'
@@ -88,7 +218,13 @@ declare module '@tanstack/react-router' {
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
AboutRoute: AboutRoute, AboutRoute: AboutRoute,
AiRoute: AiRoute,
ContactRoute: ContactRoute,
FrameworkRoute: FrameworkRoute,
InvestorsRoute: InvestorsRoute,
PortfolioRoute: PortfolioRoute,
SitemapDotxmlRoute: SitemapDotxmlRoute, SitemapDotxmlRoute: SitemapDotxmlRoute,
StudioRoute: StudioRoute,
} }
export const routeTree = rootRouteImport export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren) ._addFileChildren(rootRouteChildren)

View File

@@ -10,6 +10,8 @@ import {
import { useEffect, type ReactNode } from "react"; import { useEffect, type ReactNode } from "react";
import appCss from "../styles.css?url"; import appCss from "../styles.css?url";
import logoMark from "../assets/kapfoundry-mark.webp";
import { INTRO_SCRIPT, Intro } from "../components/site/motion";
import { reportLovableError } from "../lib/lovable-error-reporting"; import { reportLovableError } from "../lib/lovable-error-reporting";
function NotFoundComponent() { function NotFoundComponent() {
@@ -112,11 +114,14 @@ export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()(
function RootShell({ children }: { children: ReactNode }) { function RootShell({ children }: { children: ReactNode }) {
return ( return (
<html lang="en"> // The intro script may add a class to <html> before React hydrates.
<html lang="en" suppressHydrationWarning>
<head> <head>
<HeadContent /> <HeadContent />
<script dangerouslySetInnerHTML={{ __html: INTRO_SCRIPT }} />
</head> </head>
<body> <body>
<Intro mark={logoMark} />
{children} {children}
<Scripts /> <Scripts />
</body> </body>

View File

@@ -1,12 +1,6 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { import { Different, SitePage, Team, VisionMission } from "@/components/site/sections";
AboutHero, import { AboutHero, NextPage } from "@/components/site/pages";
Contact,
Different,
SitePage,
Team,
VisionMission,
} from "@/components/site/sections";
export const Route = createFileRoute("/about")({ export const Route = createFileRoute("/about")({
head: () => ({ head: () => ({
@@ -30,7 +24,7 @@ function About() {
<Different /> <Different />
<VisionMission /> <VisionMission />
<Team /> <Team />
<Contact /> <NextPage page="about" />
</SitePage> </SitePage>
); );
} }

29
src/routes/ai.tsx Normal file
View File

@@ -0,0 +1,29 @@
import { createFileRoute } from "@tanstack/react-router";
import { AISection, SitePage } from "@/components/site/sections";
import { AIAcrossVerticals, AIHero, NextPage } from "@/components/site/pages";
export const Route = createFileRoute("/ai")({
head: () => ({
meta: [
{ title: "AI | KapFoundry" },
{
name: "description",
content:
"Artificial intelligence is the foundation beneath every KapFoundry venture, not a vertical of its own.",
},
{ property: "og:title", content: "KapFoundry AI" },
],
}),
component: AIPage,
});
function AIPage() {
return (
<SitePage page="ai">
<AIHero />
<AISection />
<AIAcrossVerticals />
<NextPage page="ai" />
</SitePage>
);
}

27
src/routes/contact.tsx Normal file
View File

@@ -0,0 +1,27 @@
import { createFileRoute } from "@tanstack/react-router";
import { Contact, SitePage } from "@/components/site/sections";
import { ContactHero, NextPage } from "@/components/site/pages";
export const Route = createFileRoute("/contact")({
head: () => ({
meta: [
{ title: "Contact | KapFoundry" },
{
name: "description",
content: "Get in touch with KapFoundry to build, partner or invest.",
},
{ property: "og:title", content: "KapFoundry Contact" },
],
}),
component: ContactPage,
});
function ContactPage() {
return (
<SitePage page="contact">
<ContactHero />
<Contact />
<NextPage page="contact" />
</SitePage>
);
}

29
src/routes/framework.tsx Normal file
View File

@@ -0,0 +1,29 @@
import { createFileRoute } from "@tanstack/react-router";
import { Framework, SitePage } from "@/components/site/sections";
import { FrameworkHero, FrameworkPrinciples, NextPage } from "@/components/site/pages";
export const Route = createFileRoute("/framework")({
head: () => ({
meta: [
{ title: "Framework | KapFoundry" },
{
name: "description",
content:
"KapFoundry's repeatable framework for taking a venture from problem discovery to scale.",
},
{ property: "og:title", content: "KapFoundry Framework" },
],
}),
component: FrameworkPage,
});
function FrameworkPage() {
return (
<SitePage page="framework">
<FrameworkHero />
<Framework />
<FrameworkPrinciples />
<NextPage page="framework" />
</SitePage>
);
}

View File

@@ -2,6 +2,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { import {
AISection, AISection,
Contact, Contact,
Different,
Framework, Framework,
Hero, Hero,
Investors, Investors,
@@ -21,6 +22,7 @@ function Home() {
<SitePage page="home"> <SitePage page="home">
<Hero /> <Hero />
<Ticker /> <Ticker />
<Different />
<WhoWeAre /> <WhoWeAre />
<StudioAdvantage /> <StudioAdvantage />
<Framework /> <Framework />

29
src/routes/investors.tsx Normal file
View File

@@ -0,0 +1,29 @@
import { createFileRoute } from "@tanstack/react-router";
import { Investors, SitePage } from "@/components/site/sections";
import { InvestorModel, InvestorsHero, NextPage } from "@/components/site/pages";
export const Route = createFileRoute("/investors")({
head: () => ({
meta: [
{ title: "Investors | KapFoundry" },
{
name: "description",
content:
"Invest in a diversified portfolio of AI-first ventures across multiple industries with KapFoundry.",
},
{ property: "og:title", content: "KapFoundry Investors" },
],
}),
component: InvestorsPage,
});
function InvestorsPage() {
return (
<SitePage page="investors">
<InvestorsHero />
<Investors />
<InvestorModel />
<NextPage page="investors" />
</SitePage>
);
}

30
src/routes/portfolio.tsx Normal file
View File

@@ -0,0 +1,30 @@
import { createFileRoute } from "@tanstack/react-router";
import { Portfolio, SitePage } from "@/components/site/sections";
import { NextPage, PortfolioHero, PortfolioIndex, Ventures } from "@/components/site/pages";
export const Route = createFileRoute("/portfolio")({
head: () => ({
meta: [
{ title: "Portfolio | KapFoundry" },
{
name: "description",
content:
"The industries KapFoundry builds in: logistics, retail, fintech, agritech, foodtech, carbon and healthtech.",
},
{ property: "og:title", content: "KapFoundry Portfolio" },
],
}),
component: PortfolioPage,
});
function PortfolioPage() {
return (
<SitePage page="portfolio">
<PortfolioHero />
<PortfolioIndex />
<Portfolio />
<Ventures />
<NextPage page="portfolio" />
</SitePage>
);
}

View File

@@ -9,7 +9,9 @@ export const Route = createFileRoute("/sitemap.xml")({
GET: async () => { GET: async () => {
const entries = [ const entries = [
{ path: "/", changefreq: "weekly", priority: "1.0" }, { path: "/", changefreq: "weekly", priority: "1.0" },
{ path: "/about", changefreq: "monthly", priority: "0.8" }, ...["/studio", "/framework", "/portfolio", "/ai", "/investors", "/about", "/contact"].map(
(path) => ({ path, changefreq: "monthly", priority: "0.8" }),
),
]; ];
const urls = entries.map( const urls = entries.map(
(e) => (e) =>

30
src/routes/studio.tsx Normal file
View File

@@ -0,0 +1,30 @@
import { createFileRoute } from "@tanstack/react-router";
import { SitePage, StudioAdvantage, WhoWeAre } from "@/components/site/sections";
import { NextPage, StudioHero, StudioVsVC } from "@/components/site/pages";
export const Route = createFileRoute("/studio")({
head: () => ({
meta: [
{ title: "Studio | KapFoundry" },
{
name: "description",
content:
"How KapFoundry works: a venture studio and technology holding company that builds AI-first companies on one shared engine.",
},
{ property: "og:title", content: "KapFoundry Studio" },
],
}),
component: Studio,
});
function Studio() {
return (
<SitePage page="studio">
<StudioHero />
<WhoWeAre />
<StudioAdvantage />
<StudioVsVC />
<NextPage page="studio" />
</SitePage>
);
}

View File

@@ -291,7 +291,7 @@
} }
.rise-in { .rise-in {
animation: rise-in 0.9s cubic-bezier(0.22, 1, 0.36, 1) both; animation: rise-in 0.9s cubic-bezier(0.22, 1, 0.36, 1) both;
animation-delay: var(--rise-delay, 0ms); animation-delay: calc(var(--rise-delay, 0ms) + var(--intro-delay, 0ms));
} }
.hero-zoom { .hero-zoom {
animation: hero-zoom 2.4s cubic-bezier(0.22, 1, 0.36, 1) both; animation: hero-zoom 2.4s cubic-bezier(0.22, 1, 0.36, 1) both;
@@ -537,6 +537,532 @@ html.menu-open {
} }
} }
/* ------------------------------------------------------------------------------------------
Headline split: each word rises through its own mask, one after another (--w is its index).
------------------------------------------------------------------------------------------ */
@keyframes word-rise {
from {
transform: translate3d(0, 110%, 0) rotate(4deg);
}
}
.word-mask {
display: inline-block;
overflow: clip;
overflow-clip-margin: 0.12em;
vertical-align: top;
padding-bottom: 0.08em;
margin-bottom: -0.08em;
}
.word {
display: inline-block;
transform-origin: 0 100%;
animation: word-rise 1.15s var(--ease-out-expo) both;
animation-delay: calc(120ms + var(--w, 0) * 65ms + var(--intro-delay, 0ms));
}
/* Smooth scrolling (Lenis) takes over from CSS smooth scrolling while it runs. */
html.lenis,
html.lenis body {
height: auto;
}
.lenis.lenis-smooth {
scroll-behavior: auto !important;
}
.lenis.lenis-stopped {
overflow: clip;
}
/* ------------------------------------------------------------------------------------------
Page transitions between routes (cross-document view transitions). The leaving page sinks
and dims; the arriving page wipes up over it. Browsers without support navigate normally.
------------------------------------------------------------------------------------------ */
@view-transition {
navigation: auto;
}
@keyframes vt-leave {
to {
transform: translate3d(0, -12vh, 0) scale(0.94);
opacity: 0.35;
filter: blur(2px);
}
}
@keyframes vt-enter {
from {
clip-path: inset(100% 0 0 0 round 3rem 3rem 0 0);
transform: translate3d(0, 10vh, 0);
}
}
::view-transition-old(root) {
animation: vt-leave 0.9s cubic-bezier(0.7, 0, 0.2, 1) both;
}
::view-transition-new(root) {
animation: vt-enter 1s cubic-bezier(0.7, 0, 0.2, 1) both;
}
::view-transition-group(site-header) {
animation-duration: 0.5s;
}
.vt-header {
view-transition-name: site-header;
}
/* ------------------------------------------------------------------------------------------
First-visit intro: a counter runs to 100 while the wordmark rises, then the curtain lifts.
Only shown when the head script adds html.intro (once per session, never with reduced
motion). Everything behind it waits via --intro-delay.
------------------------------------------------------------------------------------------ */
@property --pct {
syntax: "<integer>";
initial-value: 0;
inherits: false;
}
@keyframes intro-count {
to {
--pct: 100;
}
}
@keyframes intro-lift {
to {
clip-path: inset(0 0 100% 0);
visibility: hidden;
}
}
@keyframes intro-bar {
from {
transform: scaleX(0);
}
}
.intro-overlay {
display: none;
}
html.intro {
--intro-delay: 1.75s;
}
html.intro .intro-overlay {
display: flex;
animation: intro-lift 0.95s cubic-bezier(0.76, 0, 0.24, 1) 1.6s forwards;
}
.intro-count {
counter-reset: pct var(--pct);
animation: intro-count 1.4s cubic-bezier(0.65, 0, 0.35, 1) 0.1s forwards;
}
.intro-count::after {
content: counter(pct);
}
.intro-bar {
transform-origin: left;
animation: intro-bar 1.4s cubic-bezier(0.65, 0, 0.35, 1) 0.1s both;
}
.intro-overlay .word {
animation-delay: calc(80ms + var(--w, 0) * 45ms);
}
/* ------------------------------------------------------------------------------------------
Cursor: a gold ring that trails the pointer, swells over links and shows a label over
elements with data-cursor. Fine pointers only; the native cursor stays visible.
------------------------------------------------------------------------------------------ */
.cursor {
position: fixed;
left: 0;
top: 0;
z-index: 70;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s ease;
}
.cursor[data-on="true"] {
opacity: 1;
}
.cursor-ring {
display: grid;
place-items: center;
width: 2.25rem;
height: 2.25rem;
margin: -1.125rem 0 0 -1.125rem;
border-radius: 999px;
border: 1px solid color-mix(in oklab, var(--gold) 70%, transparent);
transition:
width 0.5s var(--ease-out-expo),
height 0.5s var(--ease-out-expo),
margin 0.5s var(--ease-out-expo),
background-color 0.4s ease,
border-color 0.4s ease,
opacity 0.3s ease;
}
.cursor[data-state="link"] .cursor-ring {
width: 3.75rem;
height: 3.75rem;
margin: -1.875rem 0 0 -1.875rem;
background: color-mix(in oklab, var(--gold) 16%, transparent);
border-color: transparent;
}
.cursor[data-state="label"] .cursor-ring {
width: 5.5rem;
height: 5.5rem;
margin: -2.75rem 0 0 -2.75rem;
background: var(--gold);
border-color: transparent;
}
.cursor[data-state="text"] .cursor-ring {
opacity: 0;
}
.cursor-label {
font-family: var(--font-display);
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
color: oklch(0.12 0.005 60);
opacity: 0;
transform: scale(0.6);
transition:
opacity 0.3s ease,
transform 0.5s var(--ease-out-expo);
}
.cursor[data-state="label"] .cursor-label {
opacity: 1;
transform: none;
}
/* ------------------------------------------------------------------------------------------
Look-closer lens: --lx/--ly (set from JS) place a circle that hides the first sentence and
shows the second inside it. The ring marks the edge of the lens.
------------------------------------------------------------------------------------------ */
.lens-stage {
--lx: 30%;
--ly: 50%;
--lr: 5.5rem;
}
@media (min-width: 768px) {
.lens-stage {
--lr: 9rem;
}
}
.lens-base {
mask-image: radial-gradient(circle var(--lr) at var(--lx) var(--ly), transparent 96%, black 100%);
}
.lens-top {
mask-image: radial-gradient(circle var(--lr) at var(--lx) var(--ly), black 96%, transparent 100%);
}
.lens-ring {
position: absolute;
left: var(--lx);
top: var(--ly);
width: calc(var(--lr) * 2);
height: calc(var(--lr) * 2);
translate: -50% -50%;
border-radius: 999px;
border: 1px solid color-mix(in oklab, var(--primary) 55%, transparent);
background: radial-gradient(
closest-side,
color-mix(in oklab, var(--primary) 9%, transparent),
transparent
);
box-shadow: 0 0 60px -10px color-mix(in oklab, var(--primary) 45%, transparent);
pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
.lens-base,
.lens-top {
mask-image: none;
}
.lens-top {
grid-area: auto !important;
margin-top: 1.5rem;
}
.lens-ring {
display: none;
}
}
/* Film grain over the whole site: an SVG noise tile that jitters a few times a second. */
@keyframes grain {
0%,
100% {
transform: translate3d(0, 0, 0);
}
20% {
transform: translate3d(-4%, 3%, 0);
}
40% {
transform: translate3d(3%, -5%, 0);
}
60% {
transform: translate3d(-6%, -2%, 0);
}
80% {
transform: translate3d(5%, 4%, 0);
}
}
.grain {
position: fixed;
inset: -25%;
z-index: 65;
pointer-events: none;
opacity: 0.075;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1.4 -0.2'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
animation: grain 0.9s steps(5) infinite;
}
@media (prefers-reduced-motion: reduce) {
.grain {
animation: none;
}
}
/* ------------------------------------------------------------------------------------------
Gold duotone photos: every photo reads in the brand's gold until its card is hovered or
focused (or is the open filmstrip panel), when it blooms into full colour.
------------------------------------------------------------------------------------------ */
.duo-img {
filter: grayscale(1) contrast(1.15) brightness(0.72);
transition:
filter 1.1s var(--ease-out-expo),
scale 1.6s var(--ease-out-expo);
}
.duo-tint {
background: linear-gradient(
150deg,
oklch(0.8 0.13 82 / 0.9),
oklch(0.55 0.11 60 / 0.9) 55%,
oklch(0.3 0.05 50 / 0.9)
);
mix-blend-mode: color;
transition: opacity 1.1s var(--ease-out-expo);
}
.group:hover .duo-img,
.group:focus-visible .duo-img,
.group:focus-within .duo-img,
.is-open .duo-img {
filter: none;
scale: 1.06;
}
.group:hover .duo-tint,
.group:focus-visible .duo-tint,
.group:focus-within .duo-tint,
.is-open .duo-tint {
opacity: 0;
}
/* Filmstrip: panels share the row; the open one takes most of it. */
.film-panel {
flex: 1 1 0%;
transition: flex-grow 0.9s var(--ease-out-expo);
}
.film-panel.is-open {
flex-grow: 5.5;
}
.film-label {
writing-mode: vertical-rl;
rotate: 180deg;
}
/* Magnetic buttons: JS sets `translate`; this eases them back when the pointer leaves. */
[data-magnetic] {
transition: translate 0.6s var(--ease-out-expo);
}
/* ------------------------------------------------------------------------------------------
Inner-page hero visuals
------------------------------------------------------------------------------------------ */
@keyframes spin {
to {
rotate: 360deg;
}
}
@keyframes spin-back {
to {
rotate: -360deg;
}
}
.spin-slow {
animation: spin 70s linear infinite;
}
.spin-slower {
animation: spin-back 110s linear infinite;
}
/* Labels on a spinning ring counter-rotate so they always read upright. */
.spin-slow .upright {
animation: spin-back 70s linear infinite;
}
.spin-slower .upright {
animation: spin 110s linear infinite;
}
/* Photo columns drifting past each other (portfolio hero). Tracks hold two copies. */
@keyframes column-up {
to {
transform: translate3d(0, -50%, 0);
}
}
@keyframes column-down {
from {
transform: translate3d(0, -50%, 0);
}
}
.column-up {
animation: column-up 60s linear infinite;
}
.column-down {
animation: column-down 70s linear infinite;
}
/* Signals travelling along network edges (AI hero). */
@keyframes signal {
from {
stroke-dashoffset: 240;
}
to {
stroke-dashoffset: 0;
}
}
.signal {
stroke-dasharray: 22 218;
animation: signal 3.2s linear infinite;
animation-delay: var(--d, 0s);
}
@keyframes node-pulse {
50% {
opacity: 1;
r: 6;
}
}
.node-pulse {
animation: node-pulse 2.8s ease-in-out infinite;
animation-delay: var(--d, 0s);
}
/* Ecosystem field (investors hero): dots light up in a slow diagonal wave. */
@keyframes dot-wave {
0%,
100% {
opacity: 0.12;
transform: scale(0.7);
}
45% {
opacity: 1;
transform: scale(1);
}
}
.dot-wave {
animation: dot-wave 4.8s ease-in-out infinite;
animation-delay: var(--d, 0s);
}
/* Travelling comet on the framework loop. */
.orbit-comet {
animation: spin 9s cubic-bezier(0.45, 0, 0.55, 1) infinite;
}
/* Scroll cue line: a highlight slides down the track. */
@keyframes cue {
from {
transform: translateY(-100%);
}
to {
transform: translateY(200%);
}
}
.scroll-cue {
animation: cue 2s cubic-bezier(0.65, 0, 0.35, 1) infinite;
}
/* "Next page" handoff: the giant label fills with gold from the left on hover. */
.next-fill {
background: linear-gradient(
90deg,
var(--primary) 50%,
color-mix(in oklab, var(--foreground) 14%, transparent) 50%
);
background-size: 200% 100%;
background-position: 100% 0;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
transition: background-position 1s var(--ease-out-expo);
}
.group:hover .next-fill,
.group:focus-visible .next-fill {
background-position: 0 0;
}
@keyframes next-grow {
from {
transform: scale(0.72) translate3d(0, 12%, 0);
opacity: 0.2;
}
}
@keyframes line-draw {
from {
transform: scaleX(0);
}
}
@keyframes row-focus {
from {
opacity: 0.22;
}
45% {
opacity: 1;
}
55% {
opacity: 1;
}
to {
opacity: 0.22;
}
}
@keyframes rise-scroll {
from {
transform: translate3d(0, 18%, 0);
opacity: 0;
}
}
@supports (animation-timeline: view()) {
@media (prefers-reduced-motion: no-preference) {
.next-grow {
animation: next-grow linear both;
animation-timeline: view();
animation-range: entry 0% cover 45%;
}
.line-draw {
transform-origin: left;
animation: line-draw linear both;
animation-timeline: view();
animation-range: entry 30% cover 50%;
}
/* A list where the row crossing the middle of the screen is lit and the rest recede. */
.row-focus {
animation: row-focus linear both;
animation-timeline: view();
animation-range: cover 0% cover 100%;
}
.rise-scroll {
animation: rise-scroll linear both;
animation-timeline: view();
animation-range: entry 0% entry 80%;
}
}
}
@media (prefers-reduced-motion: reduce) {
.word,
.spin-slow,
.spin-slower,
.spin-slow .upright,
.spin-slower .upright,
.column-up,
.column-down,
.signal,
.node-pulse,
.dot-wave,
.orbit-comet,
.scroll-cue {
animation: none;
}
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
}
}
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.menu-overlay, .menu-overlay,
.menu-overlay.is-open, .menu-overlay.is-open,