updated version

This commit is contained in:
R-Bharathraj
2026-09-28 11:52:24 +05:30
parent be013ba380
commit d266a08543
17 changed files with 2651 additions and 596 deletions

19
.dockerignore Normal file
View File

@@ -0,0 +1,19 @@
# Keep host build artefacts and platform-specific binaries out of the image;
# dependencies are installed fresh inside the container by `npm ci`.
node_modules
.output
.nitro
.tanstack
.wrangler
dist
.git
.claude
.lovable
.vscode
.idea
*.log
.env
.env.*
!.env.example
Dockerfile
.dockerignore

7
.env.example Normal file
View File

@@ -0,0 +1,7 @@
# Contact form email delivery (https://resend.com). Without RESEND_API_KEY the
# form falls back to opening the visitor's email app.
RESEND_API_KEY=
# Where enquiries are delivered (defaults to hello@kapfoundry.com).
CONTACT_TO_EMAIL=
# Sender; must be on a domain verified in Resend, e.g. "KapFoundry <website@kapfoundry.com>".
CONTACT_FROM_EMAIL=

6
.gitignore vendored
View File

@@ -24,9 +24,15 @@ dist-ssr
.vscode/*
!.vscode/extensions.json
.idea
.claude
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Local secrets
.env
.env.*
!.env.example

View File

@@ -2,5 +2,6 @@
"printWidth": 100,
"semi": true,
"singleQuote": false,
"trailingComma": "all"
"trailingComma": "all",
"endOfLine": "auto"
}

View File

@@ -1,4 +1,5 @@
<!-- LOVABLE:BEGIN -->
> [!IMPORTANT]
> This project is connected to [Lovable](https://lovable.dev). Avoid rewriting
> published git history — force pushing, or rebasing/amending/squashing commits
@@ -7,4 +8,5 @@
>
> Commits you push to the connected branch sync back to Lovable and show up in
> the editor, so keep the branch in a working state.
<!-- LOVABLE:END -->

View File

@@ -25,6 +25,7 @@ COPY --from=build /app/.output ./.output
EXPOSE 3000
ENV NODE_ENV=production
ENV PORT=3000
ENV HOST=0.0.0.0

View File

@@ -1,18 +0,0 @@
events {}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
server {
listen 3000;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
# This line forces Nginx to pass routing back to React Router
try_files $uri $uri/ /index.html;
}
}
}

10
package-lock.json generated
View File

@@ -5978,16 +5978,6 @@
}
}
},
"node_modules/nitro/node_modules/lru-cache": {
"version": "11.5.2",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
"integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
"extraneous": true,
"license": "BlueOak-1.0.0",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/nitro/node_modules/unstorage": {
"version": "2.0.0-alpha.7",
"resolved": "https://registry.npmjs.org/unstorage/-/unstorage-2.0.0-alpha.7.tgz",

BIN
public/assets/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 KiB

BIN
public/assets/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 KiB

BIN
public/assets/3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
public/assets/4.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

65
src/lib/contact.ts Normal file
View File

@@ -0,0 +1,65 @@
import { createServerFn } from "@tanstack/react-start";
import { z } from "zod";
export const CONTACT_EMAIL = "hello@kapfoundry.com";
export const INTERESTS = [
"Investing in the studio",
"Co-founding a venture",
"Partnership or collaboration",
"General enquiry",
] as const;
const contactSchema = z.object({
name: z.string().trim().min(1).max(200),
organization: z.string().trim().max(200).default(""),
email: z.string().trim().email().max(320),
interest: z.enum(INTERESTS),
message: z.string().trim().max(5000).default(""),
// Honeypot: hidden from people, so only bots fill it in.
website: z.string().default(""),
});
export type ContactInput = z.input<typeof contactSchema>;
export type ContactResult = { ok: true } | { ok: false; reason: "not-configured" | "send-failed" };
/**
* Emails an enquiry to the studio via Resend (https://resend.com).
* Env: RESEND_API_KEY (required to send), CONTACT_TO_EMAIL, CONTACT_FROM_EMAIL.
* Without an API key it reports "not-configured" so the page can fall back to mailto.
*/
export const sendContactEnquiry = createServerFn({ method: "POST" })
.validator((data: ContactInput) => contactSchema.parse(data))
.handler(async ({ data }): Promise<ContactResult> => {
if (data.website) return { ok: true };
const apiKey = process.env.RESEND_API_KEY;
if (!apiKey) return { ok: false, reason: "not-configured" };
const text = [
`Name: ${data.name}`,
`Organization: ${data.organization || "-"}`,
`Email: ${data.email}`,
`Interested in: ${data.interest}`,
"",
data.message || "(no message)",
].join("\n");
const res = await fetch("https://api.resend.com/emails", {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
body: JSON.stringify({
from: process.env.CONTACT_FROM_EMAIL ?? "KapFoundry Website <onboarding@resend.dev>",
to: [process.env.CONTACT_TO_EMAIL ?? CONTACT_EMAIL],
reply_to: data.email,
subject: `Website enquiry: ${data.interest} (${data.name})`,
text,
}),
});
if (!res.ok) {
console.error("Contact email failed", res.status, await res.text());
return { ok: false, reason: "send-failed" };
}
return { ok: true };
});

View File

@@ -7,15 +7,15 @@ is `src/routes/__root.tsx`.
## Conventions
| File | URL |
| --- | --- |
| `index.tsx` | `/` |
| `about.tsx` | `/about` |
| `users/index.tsx` | `/users` |
| `users/$id.tsx` | `/users/:id` (dynamic — bare `$`, no curly braces) |
| `posts/{-$category}.tsx` | `/posts/:category?` (optional segment) |
| `files/$.tsx` | `/files/*` (splat — read via `_splat` param, never `*`) |
| `_layout.tsx` | layout route (renders children via `<Outlet />`) |
| `__root.tsx` | app shell — wraps every page; preserve `<Outlet />` |
| File | URL |
| ------------------------ | ------------------------------------------------------- |
| `index.tsx` | `/` |
| `about.tsx` | `/about` |
| `users/index.tsx` | `/users` |
| `users/$id.tsx` | `/users/:id` (dynamic — bare `$`, no curly braces) |
| `posts/{-$category}.tsx` | `/posts/:category?` (optional segment) |
| `files/$.tsx` | `/files/*` (splat — read via `_splat` param, never `*`) |
| `_layout.tsx` | layout route (renders children via `<Outlet />`) |
| `__root.tsx` | app shell — wraps every page; preserve `<Outlet />` |
`routeTree.gen.ts` is auto-generated. Don't edit it by hand.

View File

@@ -47,12 +47,13 @@ function ErrorComponent({ error, reset }: { error: Error; reset: () => void }) {
<h1 className="text-xl font-semibold tracking-tight text-foreground">
This page didn't load
</h1>
<p className="mt-2 text-sm text-muted-foreground">
Please refresh or head back home.
</p>
<p className="mt-2 text-sm text-muted-foreground">Please refresh or head back home.</p>
<div className="mt-6 flex flex-wrap justify-center gap-2">
<button
onClick={() => { router.invalidate(); reset(); }}
onClick={() => {
router.invalidate();
reset();
}}
className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
>
Try again
@@ -74,14 +75,14 @@ export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()(
meta: [
{ charSet: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" },
{ title: "KapFoundry — Venture Studio & Technology Holding Company" },
{ title: "KapFoundry | Venture Studio & Technology Holding Company" },
{
name: "description",
content:
"KapFoundry is a Hyderabad-based venture studio and technology holding company engineering AI-first, category-defining companies across logistics, retail, fintech, agritech, cuisine, and carbon.",
},
{ name: "author", content: "KapFoundry" },
{ property: "og:title", content: "KapFoundry — Build Bold. Build the Future." },
{ property: "og:title", content: "KapFoundry: Build Bold. Build the Future." },
{
property: "og:description",
content:
@@ -98,7 +99,7 @@ export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()(
{ rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" },
{
rel: "stylesheet",
href: "https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,500;9..144,600;9..144,700&family=Inter:wght@300;400;500;600;700&display=swap",
href: "https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700;800&family=Geist:wght@400;500;600&display=swap",
},
],
}),

File diff suppressed because it is too large Load Diff

View File

@@ -32,18 +32,22 @@
--color-ink: var(--ink);
--color-cream: var(--cream);
--font-display: "Fraunces", ui-serif, Georgia, serif;
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, monospace;
--font-display: "Sora", ui-sans-serif, system-ui, sans-serif;
--font-sans: "Geist", ui-sans-serif, system-ui, sans-serif;
--font-mono: ui-monospace, "SF Mono", Consolas, monospace;
}
:root {
--radius: 0.5rem;
}
/* Dark is the default palette; .theme-dark re-applies it inside a light section. */
:root,
.theme-dark {
/* Etihad-inspired: deep charcoal/black with warm gold */
--background: oklch(0.14 0.005 60); /* near-black warm */
--background: oklch(0.14 0.005 60); /* near-black warm */
--foreground: oklch(0.96 0.015 85);
--ink: oklch(0.10 0.005 60);
--ink: oklch(0.1 0.005 60);
--cream: oklch(0.95 0.03 85);
--card: oklch(0.18 0.008 65);
@@ -51,13 +55,13 @@
--popover: oklch(0.18 0.008 65);
--popover-foreground: oklch(0.96 0.015 85);
--primary: oklch(0.78 0.13 82); /* warm gold */
--primary: oklch(0.78 0.13 82); /* warm gold */
--primary-foreground: oklch(0.12 0.005 60);
--gold: oklch(0.82 0.14 85); /* champagne gold accent */
--gold: oklch(0.82 0.14 85); /* champagne gold accent */
--secondary: oklch(0.22 0.01 70);
--secondary-foreground: oklch(0.96 0.015 85);
--muted: oklch(0.20 0.008 65);
--muted: oklch(0.2 0.008 65);
--muted-foreground: oklch(0.72 0.03 80);
--accent: oklch(0.28 0.03 75);
--accent-foreground: oklch(0.96 0.015 85);
@@ -68,20 +72,63 @@
--ring: oklch(0.78 0.13 82);
}
/* Warm cream palette for alternating light sections. */
.theme-light {
--background: oklch(0.975 0.012 85);
--foreground: oklch(0.17 0.01 60);
--card: oklch(1 0 0);
--card-foreground: oklch(0.17 0.01 60);
--primary: oklch(0.56 0.12 70);
--primary-foreground: oklch(0.99 0.005 85);
--secondary: oklch(0.93 0.02 85);
--secondary-foreground: oklch(0.17 0.01 60);
--muted: oklch(0.94 0.015 85);
--muted-foreground: oklch(0.44 0.02 70);
--accent: oklch(0.92 0.03 85);
--accent-foreground: oklch(0.17 0.01 60);
--border: oklch(0.17 0.01 60 / 12%);
--input: oklch(0.17 0.01 60 / 18%);
--ring: oklch(0.56 0.12 70);
}
.dark { color-scheme: dark; }
.dark {
color-scheme: dark;
}
@layer base {
* { border-color: var(--color-border); }
html { scroll-behavior: smooth; }
* {
border-color: var(--border);
}
html {
scroll-padding-top: 5rem;
}
@media (prefers-reduced-motion: no-preference) {
html {
scroll-behavior: smooth;
}
}
body {
background-color: var(--color-background);
color: var(--color-foreground);
background-color: var(--background);
color: var(--foreground);
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
font-feature-settings: "ss01", "cv11";
}
h1, h2, h3, h4 { font-family: var(--font-display); letter-spacing: -0.02em; }
h1,
h2,
h3,
h4 {
font-family: var(--font-display);
letter-spacing: -0.035em;
text-wrap: balance;
}
p {
text-wrap: pretty;
}
/* One visible keyboard focus style for every interactive element. */
:focus-visible {
outline: 2px solid var(--primary);
outline-offset: 3px;
}
}
@utility container-x {
@@ -89,25 +136,22 @@
margin-inline: auto;
padding-inline: 1.5rem;
max-width: 1280px;
@media (min-width: 768px) { padding-inline: 2.5rem; }
@media (min-width: 768px) {
padding-inline: 2.5rem;
}
}
/* Plain section label (used sparingly: roughly one per three sections). */
@utility eyebrow {
font-family: var(--font-sans);
font-size: 0.72rem;
letter-spacing: 0.22em;
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-family: var(--font-display);
font-size: 0.75rem;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--color-primary);
font-weight: 500;
}
@utility hairline {
display: inline-block;
width: 2.5rem;
height: 1px;
background: var(--color-primary);
vertical-align: middle;
margin-right: 0.75rem;
color: var(--primary);
font-weight: 600;
}
@utility grid-lines {
@@ -116,3 +160,428 @@
linear-gradient(to bottom, oklch(1 0 0 / 4%) 1px, transparent 1px);
background-size: 80px 80px;
}
@utility section-y {
padding-block: 6rem;
@media (min-width: 768px) {
padding-block: 8rem;
}
}
/* Infinite ticker: the track holds two copies of its content and slides by half. */
@keyframes marquee {
to {
transform: translateX(-50%);
}
}
.marquee-track {
display: flex;
width: max-content;
animation: marquee 40s linear infinite;
}
.marquee:hover .marquee-track {
animation-play-state: paused;
}
/* Scroll reveal. Only elements below the fold at hydration get .reveal-pending,
so server-rendered content never flashes or stays hidden without JS. The JS removes
both classes once an element has settled, handing it back to its own transitions. */
:root {
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
}
[data-reveal] {
--reveal-wait: calc(var(--reveal-delay, 0ms) + var(--batch-delay, 0ms));
}
[data-reveal].reveal-pending {
opacity: 0;
transform: translate3d(0, 40px, 0);
}
[data-reveal].reveal-pending.is-visible {
opacity: 1;
transform: none;
transition:
opacity 1s var(--ease-out-expo),
transform 1.2s var(--ease-out-expo),
clip-path 1.3s var(--ease-out-expo);
transition-delay: var(--reveal-wait);
}
/* Headlines: the text rises through a mask that opens from the top. */
[data-reveal="mask"].reveal-pending {
opacity: 1;
transform: translate3d(0, 0.45em, 0);
clip-path: inset(0 0 100% 0);
}
[data-reveal="mask"].reveal-pending.is-visible {
transform: none;
clip-path: inset(-0.3em -0.3em -0.35em -0.3em);
}
/* Media: the frame opens out from an inset while the photo settles from a slight zoom. */
[data-reveal="clip"].reveal-pending {
opacity: 1;
transform: none;
clip-path: inset(12% 10% 12% 10% round 1.5rem);
}
[data-reveal="clip"].reveal-pending.is-visible {
clip-path: inset(0 0 0 0 round 0);
}
[data-reveal="clip"].reveal-pending img {
transform: scale(1.22);
}
[data-reveal="clip"].reveal-pending.is-visible img {
transform: none;
transition: transform 1.8s var(--ease-out-expo) var(--reveal-wait);
}
/* Groups: the container stays put while its children rise in sequence. */
[data-reveal="stagger"].reveal-pending {
opacity: 1;
transform: none;
}
[data-reveal="stagger"].reveal-pending > * {
opacity: 0;
transform: translate3d(0, 28px, 0);
}
[data-reveal="stagger"].reveal-pending.is-visible > * {
opacity: 1;
transform: none;
transition:
opacity 0.9s var(--ease-out-expo),
transform 1.1s var(--ease-out-expo);
transition-delay: calc(var(--reveal-wait) + var(--i, 0) * 80ms);
}
/* Small pill labels need more contrast than large headings on cream. */
.theme-light .eyebrow {
color: oklch(0.47 0.1 65);
}
/* Soft gold glow drawn with a gradient: much cheaper to paint than a blurred element. */
@utility glow {
background: radial-gradient(
closest-side,
color-mix(in oklab, var(--primary) 28%, transparent),
transparent
);
pointer-events: none;
}
/* Tactile press feedback for buttons and button-like links. */
@utility press {
transition-property: transform, filter, background-color, color, border-color, box-shadow;
transition-duration: 250ms;
transition-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
&:active {
transform: scale(0.97);
}
}
/* Hero entrance: staggered rise on first paint. Pure CSS, so it also runs before hydration. */
@keyframes rise-in {
from {
opacity: 0;
transform: translateY(24px);
}
}
@keyframes hero-zoom {
from {
transform: scale(1.08);
}
}
.rise-in {
animation: rise-in 0.9s cubic-bezier(0.22, 1, 0.36, 1) both;
animation-delay: var(--rise-delay, 0ms);
}
.hero-zoom {
animation: hero-zoom 2.4s cubic-bezier(0.22, 1, 0.36, 1) both;
}
/* Reading progress bar driven by the scroll position itself (no JS). */
@keyframes grow-x {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
.scroll-progress {
display: none;
}
@supports (animation-timeline: scroll()) {
.scroll-progress {
display: block;
transform-origin: left;
animation: grow-x linear both;
animation-timeline: scroll(root);
}
}
/* Gentle sideways nudge for "scroll to explore" hints. */
@keyframes nudge-x {
50% {
transform: translateX(6px);
}
}
.nudge-x {
animation: nudge-x 1.6s ease-in-out infinite;
}
@keyframes nudge-y {
50% {
transform: translateY(4px);
}
}
.nudge-y {
animation: nudge-y 1.6s ease-in-out infinite;
}
/* Mission rules: the gold line draws across once its list is revealed. Full by default. */
[data-reveal].reveal-pending [data-draw] {
transform: scaleX(0);
}
[data-reveal].reveal-pending.is-visible [data-draw] {
transform: scaleX(1);
transition: transform 1.4s var(--ease-out-expo);
transition-delay: calc(var(--reveal-wait) + var(--i, 0) * 80ms + 150ms);
}
/* Cursor spotlight inside a tile; --mx/--my are set from pointermove. */
.spotlight::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s ease;
background: radial-gradient(
280px circle at var(--mx, 50%) var(--my, 50%),
color-mix(in oklab, var(--primary) 18%, transparent),
transparent 70%
);
}
.spotlight:hover::after {
opacity: 1;
}
/* Scroll-driven effects. Browsers without view timelines simply show the final state. */
@keyframes word-fill {
from {
opacity: 0.16;
}
}
@keyframes hero-media-drift {
to {
transform: translate3d(0, 28%, 0) scale(1.08);
}
}
@keyframes hero-copy-lift {
to {
transform: translate3d(0, 16vh, 0);
opacity: 0;
}
}
@keyframes wordmark-fill {
to {
color: color-mix(in oklab, var(--primary) 26%, transparent);
}
}
/* Brand mesh: layered gold, amber and bronze light on ink. Drifts on hover. */
.mesh-gold {
background:
radial-gradient(42% 48% at 82% 18%, oklch(0.8 0.13 82 / 0.55), transparent 70%),
radial-gradient(38% 42% at 18% 32%, oklch(0.62 0.12 62 / 0.4), transparent 72%),
radial-gradient(55% 45% at 58% 96%, oklch(0.42 0.08 50 / 0.5), transparent 70%),
radial-gradient(30% 30% at 45% 55%, oklch(0.7 0.1 85 / 0.18), transparent 70%), var(--ink);
background-size: 140% 140%;
background-position: 0% 0%;
transition: background-position 1.6s var(--ease-out-expo);
}
.group:hover .mesh-gold {
background-position: 100% 60%;
}
/* Fine dot grid, strongest near the light and fading toward the edges. */
.dot-grid {
background-image: radial-gradient(rgb(255 255 255 / 0.14) 1px, transparent 1.2px);
background-size: 18px 18px;
mask-image: radial-gradient(ellipse 70% 60% at 75% 25%, black, transparent 75%);
opacity: 0.7;
}
/* Mobile / tablet menu: a circle that grows out of the menu button and shrinks back into it.
--menu-x / --menu-y are set to the button's centre just before toggling. */
html.menu-open {
overflow: hidden;
}
.menu-overlay {
--menu-x: calc(100% - 3rem);
--menu-y: 2.25rem;
clip-path: circle(0px at var(--menu-x) var(--menu-y));
visibility: hidden;
transition:
clip-path 0.6s cubic-bezier(0.7, 0, 0.84, 0),
visibility 0s linear 0.6s;
}
.menu-overlay.is-open {
clip-path: circle(150vmax at var(--menu-x) var(--menu-y));
visibility: visible;
transition:
clip-path 0.9s var(--ease-out-expo),
visibility 0s;
}
/* Links rise through their row's mask one after another; they drop away quickly on close. */
.menu-link {
transform: translate3d(0, 105%, 0);
opacity: 0;
transition:
transform 0.35s ease-in,
opacity 0.25s ease;
}
.menu-overlay.is-open .menu-link {
transform: none;
opacity: 1;
transition:
transform 0.9s var(--ease-out-expo),
opacity 0.6s ease;
transition-delay: calc(0.18s + var(--i, 0) * 0.055s);
}
.menu-foot {
opacity: 0;
transform: translate3d(0, 18px, 0);
transition:
opacity 0.25s ease,
transform 0.35s ease;
}
.menu-overlay.is-open .menu-foot {
opacity: 1;
transform: none;
transition:
opacity 0.7s ease 0.55s,
transform 0.9s var(--ease-out-expo) 0.55s;
}
/* Hero brand mark: tilt comes from --rx/--ry set on pointermove. */
.hero-mark {
transform: perspective(900px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
transition: transform 0.7s var(--ease-out-expo);
}
@keyframes media-parallax {
from {
transform: translateY(-7%) scale(1.16);
}
to {
transform: translateY(7%) scale(1.16);
}
}
/* Cream sections open from an inset rounded sheet to full width as they enter. */
@keyframes sheet-open {
from {
clip-path: inset(0 3% round 3rem);
}
to {
clip-path: inset(0 0 round 0);
}
}
@keyframes panel-rise {
from {
opacity: 0;
transform: translate3d(0, 72px, 0);
}
}
@keyframes glow-drift {
from {
translate: 0 -18%;
}
to {
translate: 0 18%;
}
}
@supports (animation-timeline: view()) {
@media (prefers-reduced-motion: no-preference) {
.sheet-enter {
animation: sheet-open linear both;
animation-timeline: view();
animation-range: entry 0% entry 55%;
}
.panel-rise {
animation: panel-rise linear both;
animation-timeline: view();
animation-range: entry 5% cover 30%;
}
/* Side-by-side on desktop: offset each panel's range so they rise left to right. */
@media (min-width: 1024px) {
.panel-rise {
animation-range: entry calc(var(--i, 0) * 7%) cover calc(24% + var(--i, 0) * 3%);
}
}
.glow-drift {
animation: glow-drift linear both;
animation-timeline: view();
animation-range: cover 0% cover 100%;
}
.media-parallax {
animation: media-parallax linear both;
animation-timeline: view();
animation-range: cover 0% cover 100%;
}
/* Statement words brighten line by line as they rise through the viewport. */
.fill-word {
animation: word-fill linear both;
animation-timeline: view();
animation-range: cover 8% cover 42%;
}
.hero-timeline {
view-timeline-name: --hero;
}
.hero-media {
animation: hero-media-drift linear both;
animation-timeline: --hero;
animation-range: exit 0% exit 100%;
}
.hero-copy {
animation: hero-copy-lift linear both;
animation-timeline: --hero;
animation-range: exit 0% exit 70%;
}
.wordmark-fill {
animation: wordmark-fill linear both;
animation-timeline: view();
animation-range: entry 20% cover 55%;
}
}
}
@media (prefers-reduced-motion: reduce) {
.menu-overlay,
.menu-overlay.is-open,
.menu-link,
.menu-overlay.is-open .menu-link,
.menu-foot,
.menu-overlay.is-open .menu-foot {
transition: none;
transform: none;
}
.hero-mark {
transform: none;
}
[data-reveal].reveal-pending [data-draw] {
transform: none;
}
.nudge-y,
.nudge-x,
.marquee-track,
.rise-in,
.hero-zoom {
animation: none;
}
[data-reveal].reveal-pending,
[data-reveal].reveal-pending > *,
[data-reveal].reveal-pending img {
opacity: 1;
transform: none;
clip-path: none;
}
}