Deploy Kairas landing page

This commit is contained in:
Codex
2026-05-04 21:56:58 +01:00
commit 102967242e
15 changed files with 7350 additions and 0 deletions

81
src/app/globals.css Normal file
View File

@@ -0,0 +1,81 @@
@import "tailwindcss";
:root {
--background: #eee4d4;
--foreground: #081b33;
--navy: #081b33;
--navy-soft: #17314d;
--beige: #eee4d4;
--sand: #d9ccb8;
--paper: #f7f1e7;
--ink-muted: #58616a;
--line: rgba(8, 27, 51, 0.18);
--accent: #b46d3a;
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
margin: 0;
}
body {
background: var(--background);
color: var(--foreground);
font-family: Georgia, "Times New Roman", Times, serif;
text-rendering: optimizeLegibility;
}
* {
box-sizing: border-box;
}
a {
color: inherit;
text-decoration: none;
}
.motion-hidden {
opacity: 0;
}
.hero-word {
display: block;
transform-origin: left bottom;
}
.reveal-line {
transform-origin: left center;
}
.marquee {
overflow: hidden;
}
.marquee-track {
display: flex;
width: max-content;
will-change: transform;
}
.grain {
pointer-events: none;
background-image:
linear-gradient(rgba(8, 27, 51, 0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(8, 27, 51, 0.035) 1px, transparent 1px);
background-size: 42px 42px;
mask-image: linear-gradient(to bottom, black, transparent 72%);
}
::selection {
background: var(--navy);
color: var(--paper);
}
button,
input,
textarea,
select {
font: inherit;
}

4
src/app/icon.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" fill="#eee4d4"/>
<path d="M15 12h10v18l17-18h12L34 33l22 19H43L25 36v16H15V12Z" fill="#081b33"/>
</svg>

After

Width:  |  Height:  |  Size: 198 B

21
src/app/layout.tsx Normal file
View File

@@ -0,0 +1,21 @@
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
metadataBase: new URL("https://kairas.io"),
title: "Kairas | Web design studio",
description:
"Kairas is a web design firm crafting refined websites, brand systems, and digital products.",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}

467
src/app/page.tsx Normal file
View File

@@ -0,0 +1,467 @@
"use client";
import { useEffect, useRef } from "react";
import { animate, createTimeline, stagger } from "animejs";
const services = [
{
number: "01",
title: "Brand-led websites",
text: "Identity, UX, interface design, and front-end systems for companies that need their site to carry the weight of the brand.",
},
{
number: "02",
title: "Editorial product pages",
text: "Launch pages, case studies, and content structures that make complex offers feel considered, useful, and easy to move through.",
},
{
number: "03",
title: "Design systems",
text: "Reusable components, visual rules, and interaction patterns that help teams ship new pages without losing quality.",
},
];
const work = [
["Atelier North", "Architecture portfolio", "2026"],
["Vellum Labs", "SaaS website", "2025"],
["Morrow House", "Hospitality booking", "2025"],
["Plainform", "Brand system", "2024"],
];
const notes = [
["2026.04.18", "Essay", "Designing quieter conversion paths for premium service brands"],
["2026.03.02", "Studio", "Kairas opens a focused website sprint for early-stage teams"],
["2026.01.14", "Guide", "What belongs above the fold when the work is the proof"],
];
export default function Home() {
const rootRef = useRef<HTMLElement>(null);
useEffect(() => {
const root = rootRef.current;
if (!root) {
return;
}
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
if (reduceMotion.matches) {
root.querySelectorAll(".motion-hidden").forEach((element) => {
element.classList.remove("motion-hidden");
});
return;
}
const animations = [
createTimeline({
defaults: {
ease: "outExpo",
},
})
.add(root.querySelectorAll(".intro-mask"), {
y: ["0%", "-101%"],
duration: 1250,
delay: stagger(120),
})
.add(
root.querySelectorAll(".nav-item"),
{
y: [-18, 0],
opacity: [0, 1],
duration: 900,
delay: stagger(55),
},
"-=780",
)
.add(
root.querySelectorAll(".hero-kicker"),
{
y: [18, 0],
opacity: [0, 1],
duration: 800,
},
"-=600",
)
.add(
root.querySelectorAll(".hero-word"),
{
y: [130, 0],
rotate: [5, 0],
opacity: [0, 1],
duration: 1450,
delay: stagger(145),
},
"-=520",
)
.add(
root.querySelectorAll(".hero-card"),
{
x: [64, 0],
rotate: [2, 0],
opacity: [0, 1],
duration: 1150,
},
"-=980",
)
.add(
root.querySelectorAll(".hero-copy"),
{
y: [22, 0],
opacity: [0, 1],
duration: 900,
},
"-=720",
),
animate(root.querySelectorAll(".float-piece"), {
y: [-10, 12],
rotate: [-1.5, 1.5],
duration: 3200,
loop: true,
alternate: true,
ease: "inOutSine",
delay: stagger(260),
}),
animate(root.querySelectorAll(".interface-line"), {
scaleX: [0.18, 1],
duration: 1500,
loop: true,
alternate: true,
ease: "inOutQuart",
delay: stagger(220),
}),
animate(root.querySelectorAll(".ticker-item"), {
opacity: [0.35, 1],
y: [4, -4],
duration: 1600,
loop: true,
alternate: true,
ease: "inOutSine",
delay: stagger(180),
}),
animate(root.querySelectorAll(".marquee-track"), {
x: "-50%",
duration: 26000,
loop: true,
ease: "linear",
}),
animate(root.querySelectorAll(".spin-mark"), {
rotate: "360deg",
duration: 18000,
loop: true,
ease: "linear",
}),
];
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) {
return;
}
const target = entry.target;
target.classList.remove("motion-hidden");
animate(target, {
y: [44, 0],
opacity: [0, 1],
duration: 1050,
ease: "outExpo",
});
target.querySelectorAll(".reveal-line").forEach((line, index) => {
animate(line, {
scaleX: [0, 1],
duration: 1100,
delay: index * 90,
ease: "outExpo",
});
});
observer.unobserve(target);
});
},
{ threshold: 0.18 },
);
root.querySelectorAll("[data-reveal]").forEach((element) => {
observer.observe(element);
});
root.querySelectorAll(".work-link").forEach((link) => {
link.addEventListener("pointerenter", () => {
animate(link, {
x: 18,
duration: 450,
ease: "outExpo",
});
});
link.addEventListener("pointerleave", () => {
animate(link, {
x: 0,
duration: 520,
ease: "outExpo",
});
});
});
return () => {
observer.disconnect();
animations.forEach((animation) => animation.revert());
};
}, []);
return (
<main
ref={rootRef}
className="relative min-h-screen bg-[var(--beige)] text-[var(--navy)]"
>
<div className="grain fixed inset-0 z-0" />
<div className="pointer-events-none fixed inset-0 z-50 grid grid-cols-3">
<div className="intro-mask bg-[var(--navy)]" />
<div className="intro-mask bg-[var(--navy)]" />
<div className="intro-mask bg-[var(--navy)]" />
</div>
<header className="fixed left-0 right-0 top-0 z-20 border-b border-[var(--line)] bg-[rgba(238,228,212,0.86)] backdrop-blur">
<nav className="mx-auto flex max-w-[1500px] items-center justify-between px-5 py-4 text-[13px] uppercase leading-none tracking-[0.12em] sm:px-8 lg:px-12">
<a href="#" className="nav-item text-lg font-semibold tracking-[0.18em]">
Kairas
</a>
<div className="hidden items-center gap-8 md:flex">
<a className="nav-item" href="#studio">Studio</a>
<a className="nav-item" href="#services">Services</a>
<a className="nav-item" href="#work">Work</a>
<a className="nav-item" href="#notes">Notes</a>
</div>
<a
href="mailto:hello@kairas.io"
className="nav-item border border-[var(--navy)] px-4 py-3 transition hover:bg-[var(--navy)] hover:text-[var(--paper)]"
>
hello@kairas.io
</a>
</nav>
</header>
<section className="relative z-10 mx-auto grid min-h-screen max-w-[1500px] content-end gap-12 px-5 pb-12 pt-28 sm:px-8 md:pt-36 lg:grid-cols-[1.05fr_0.95fr] lg:px-12">
<div className="flex flex-col justify-end gap-8">
<div className="hero-kicker flex max-w-sm flex-wrap items-baseline gap-x-3 gap-y-1 text-sm leading-6 text-[var(--navy-soft)]">
<span className="uppercase tracking-[0.14em]">Web design firm</span>
<a
href="https://kairas.io"
className="font-semibold italic tracking-normal"
>
kairas.io
</a>
</div>
<h1 className="max-w-5xl overflow-hidden text-7xl font-semibold leading-[0.86] tracking-normal sm:text-8xl md:text-9xl lg:text-[10rem] xl:text-[12rem]">
<span className="hero-word motion-hidden">Websites</span>
<span className="hero-word motion-hidden">with quiet</span>
<span className="hero-word motion-hidden">force.</span>
</h1>
</div>
<div className="grid content-end gap-8">
<div className="hero-card motion-hidden relative min-h-[420px] overflow-hidden border border-[var(--line)] bg-[var(--paper)] p-4 shadow-[0_18px_60px_rgba(8,27,51,0.08)]">
<div className="grid h-full min-h-[388px] grid-rows-[auto_1fr_auto] border border-[var(--line)] bg-[var(--beige)]">
<div className="flex items-center justify-between border-b border-[var(--line)] px-4 py-3 text-xs uppercase tracking-[0.16em]">
<span>Selected interface</span>
<span>01 / 04</span>
</div>
<div className="grid grid-cols-[1fr_0.72fr] gap-3 p-4">
<div className="float-piece flex flex-col justify-between bg-[var(--navy)] p-5 text-[var(--paper)]">
<span className="text-xs uppercase tracking-[0.18em] text-[#d9ccb8]">
Brand system
</span>
<div>
<p className="spin-mark inline-block text-5xl font-semibold leading-none">KA</p>
<p className="mt-3 max-w-[14rem] text-sm leading-6 text-[#d9ccb8]">
Layouts tuned for calm reading and decisive action.
</p>
</div>
</div>
<div className="grid grid-rows-[1fr_0.7fr] gap-3">
<div className="float-piece bg-[var(--sand)] p-4">
<div className="h-16 w-full border-b border-[var(--navy)]" />
<div className="interface-line mt-4 h-3 w-3/4 origin-left bg-[var(--navy)]" />
<div className="interface-line mt-3 h-3 w-1/2 origin-left bg-[var(--navy)]" />
</div>
<div className="grid grid-cols-2 gap-3">
<div className="float-piece bg-[#c5d1cf]" />
<div className="float-piece bg-[var(--accent)]" />
</div>
</div>
</div>
<div className="flex items-center justify-between border-t border-[var(--line)] px-4 py-3 text-xs uppercase tracking-[0.16em]">
<span className="ticker-item">Strategy</span>
<span className="ticker-item">Design</span>
<span className="ticker-item">Build</span>
</div>
</div>
</div>
<p className="hero-copy motion-hidden max-w-xl text-xl leading-9 text-[var(--navy-soft)]">
Kairas designs and builds refined websites for founders, studios,
and service brands that care about taste, clarity, and commercial
performance.
</p>
</div>
</section>
<div className="marquee relative z-10 border-y border-[var(--line)] bg-[var(--navy)] py-5 text-[var(--paper)]">
<div className="marquee-track text-4xl italic leading-none sm:text-6xl">
{Array.from({ length: 2 }).map((_, group) => (
<div key={group} className="flex shrink-0 items-center gap-8 pr-8">
<span>Strategy</span>
<span>/</span>
<span>Design</span>
<span>/</span>
<span>Development</span>
<span>/</span>
<span>Systems</span>
<span>/</span>
<span>Launch</span>
<span>/</span>
</div>
))}
</div>
</div>
<section
id="studio"
data-reveal
className="motion-hidden relative z-10 border-y border-[var(--line)] bg-[var(--paper)]"
>
<div className="mx-auto grid max-w-[1500px] gap-10 px-5 py-20 sm:px-8 lg:grid-cols-[0.7fr_1.3fr] lg:px-12 lg:py-28">
<p className="text-sm uppercase tracking-[0.16em]">About us</p>
<div>
<h2 className="max-w-4xl text-4xl font-semibold leading-tight sm:text-6xl">
We shape digital homes for brands that need to feel exact,
intentional, and easy to trust.
</h2>
<p className="mt-8 max-w-2xl text-lg leading-8 text-[var(--ink-muted)]">
Our work sits between design studio and front-end craft. We use
strong typography, generous pacing, and disciplined systems to
make every page feel composed without becoming static.
</p>
</div>
</div>
</section>
<section id="services" data-reveal className="motion-hidden relative z-10 mx-auto max-w-[1500px] px-5 py-20 sm:px-8 lg:px-12 lg:py-28">
<div className="mb-12 flex items-end justify-between border-b border-[var(--line)] pb-6">
<h2 className="text-5xl font-semibold sm:text-7xl">Services</h2>
<span className="text-sm uppercase tracking-[0.16em]">03</span>
</div>
<div className="grid gap-0 border-t border-[var(--line)]">
{services.map((service) => (
<article
key={service.title}
className="grid gap-6 border-b border-[var(--line)] py-9 md:grid-cols-[0.22fr_0.78fr] lg:grid-cols-[0.18fr_0.38fr_0.44fr]"
>
<span className="text-sm tracking-[0.16em]">{service.number}</span>
<h3 className="text-3xl font-semibold">{service.title}</h3>
<p className="max-w-2xl text-lg leading-8 text-[var(--ink-muted)]">
{service.text}
</p>
<div className="reveal-line col-span-full h-px bg-[var(--navy)] opacity-40" />
</article>
))}
</div>
</section>
<section id="work" data-reveal className="motion-hidden relative z-10 bg-[var(--navy)] text-[var(--paper)]">
<div className="mx-auto grid max-w-[1500px] gap-12 px-5 py-20 sm:px-8 lg:grid-cols-[0.42fr_0.58fr] lg:px-12 lg:py-28">
<div>
<p className="text-sm uppercase tracking-[0.16em] text-[#d9ccb8]">
Selected work
</p>
<h2 className="mt-6 max-w-lg text-5xl font-semibold leading-none sm:text-7xl">
Places where the brand can breathe.
</h2>
</div>
<div className="grid content-start border-t border-[rgba(247,241,231,0.24)]">
{work.map(([name, type, year], index) => (
<a
href="#contact"
key={name}
className="work-link grid gap-3 border-b border-[rgba(247,241,231,0.24)] py-7 transition hover:bg-[rgba(247,241,231,0.06)] sm:grid-cols-[auto_1fr_auto] sm:items-center"
>
<span className="text-sm text-[#d9ccb8]">
{String(index + 1).padStart(2, "0")}
</span>
<span className="text-3xl font-semibold">{name}</span>
<span className="text-sm uppercase tracking-[0.14em] text-[#d9ccb8]">
{type} / {year}
</span>
</a>
))}
</div>
</div>
</section>
<section data-reveal className="motion-hidden relative z-10 mx-auto grid max-w-[1500px] gap-10 px-5 py-20 sm:px-8 lg:grid-cols-[0.35fr_0.65fr] lg:px-12 lg:py-28">
<h2 className="text-5xl font-semibold sm:text-7xl">Process</h2>
<div className="grid gap-8 md:grid-cols-3">
{["Read", "Compose", "Ship"].map((step, index) => (
<div key={step} className="border-t border-[var(--line)] pt-5">
<span className="text-sm tracking-[0.16em]">
{String(index + 1).padStart(2, "0")}
</span>
<h3 className="mt-8 text-3xl font-semibold">{step}</h3>
<p className="mt-4 leading-7 text-[var(--ink-muted)]">
{index === 0 &&
"We clarify the offer, audience, proof, and moments where a visitor needs confidence."}
{index === 1 &&
"We design the system: type, pacing, components, motion notes, and content hierarchy."}
{index === 2 &&
"We build responsive pages with practical handoff, analytics, and room to grow."}
</p>
</div>
))}
</div>
</section>
<section id="notes" data-reveal className="motion-hidden relative z-10 border-y border-[var(--line)] bg-[var(--paper)]">
<div className="mx-auto grid max-w-[1500px] gap-10 px-5 py-20 sm:px-8 lg:grid-cols-[0.35fr_0.65fr] lg:px-12 lg:py-28">
<div>
<h2 className="text-5xl font-semibold sm:text-7xl">Notes</h2>
<p className="mt-5 text-sm uppercase tracking-[0.16em] text-[var(--ink-muted)]">
Studio journal
</p>
</div>
<div className="border-t border-[var(--line)]">
{notes.map(([date, label, title]) => (
<a
href="#contact"
key={title}
className="grid gap-3 border-b border-[var(--line)] py-6 sm:grid-cols-[7rem_7rem_1fr]"
>
<span className="text-sm text-[var(--ink-muted)]">{date}</span>
<span className="text-sm uppercase tracking-[0.14em]">
{label}
</span>
<span className="text-xl font-semibold">{title}</span>
</a>
))}
</div>
</div>
</section>
<footer
id="contact"
data-reveal
className="motion-hidden relative z-10 mx-auto grid max-w-[1500px] gap-12 px-5 py-16 sm:px-8 lg:grid-cols-[1fr_auto] lg:px-12"
>
<div>
<p className="text-sm uppercase tracking-[0.16em]">Kairas</p>
<h2 className="mt-5 max-w-4xl text-5xl font-semibold leading-none sm:text-8xl">
Build the site your brand has been waiting for.
</h2>
</div>
<div className="grid content-between gap-10 text-sm uppercase tracking-[0.14em] lg:text-right">
<a href="https://kairas.io">kairas.io</a>
<a href="mailto:hello@kairas.io">hello@kairas.io</a>
<p>Strategy / Design / Development</p>
<p>© 2026 Kairas</p>
</div>
</footer>
</main>
);
}