Deploy Kairas landing page
This commit is contained in:
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
||||
.git
|
||||
.agents
|
||||
.codex
|
||||
.playwright-mcp
|
||||
node_modules
|
||||
npm-debug.log*
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
32
.gitignore
vendored
Normal file
32
.gitignore
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# next.js
|
||||
/.next
|
||||
/out
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
.playwright-mcp/
|
||||
*.png
|
||||
|
||||
# env
|
||||
.env*
|
||||
!.env.example
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
39
Dockerfile
Normal file
39
Dockerfile
Normal file
@@ -0,0 +1,39 @@
|
||||
FROM node:22-alpine AS deps
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
FROM node:22-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM node:22-alpine AS runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
ENV PORT=3000
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
COPY --from=builder /app/.next ./.next
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/src/app/icon.svg ./src/app/icon.svg
|
||||
COPY --from=builder /app/next.config.ts ./next.config.ts
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["npm", "run", "start"]
|
||||
12
README.md
Normal file
12
README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Website
|
||||
|
||||
Next.js project using the App Router, TypeScript, ESLint, and Tailwind CSS.
|
||||
|
||||
## Scripts
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
npm run build
|
||||
npm run start
|
||||
npm run lint
|
||||
```
|
||||
17
eslint.config.mjs
Normal file
17
eslint.config.mjs
Normal file
@@ -0,0 +1,17 @@
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTypescript from "eslint-config-next/typescript";
|
||||
|
||||
const eslintConfig = [
|
||||
...nextVitals,
|
||||
...nextTypescript,
|
||||
{
|
||||
ignores: [
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
5
next.config.ts
Normal file
5
next.config.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {};
|
||||
|
||||
export default nextConfig;
|
||||
6596
package-lock.json
generated
Normal file
6596
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
package.json
Normal file
27
package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "website",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"animejs": "^4.4.1",
|
||||
"next": "latest",
|
||||
"react": "latest",
|
||||
"react-dom": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "latest",
|
||||
"@types/node": "latest",
|
||||
"@types/react": "latest",
|
||||
"@types/react-dom": "latest",
|
||||
"eslint": "latest",
|
||||
"eslint-config-next": "latest",
|
||||
"tailwindcss": "latest",
|
||||
"typescript": "latest"
|
||||
}
|
||||
}
|
||||
7
postcss.config.mjs
Normal file
7
postcss.config.mjs
Normal file
@@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
1
public/.gitkeep
Normal file
1
public/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
81
src/app/globals.css
Normal file
81
src/app/globals.css
Normal 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
4
src/app/icon.svg
Normal 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
21
src/app/layout.tsx
Normal 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
467
src/app/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
33
tsconfig.json
Normal file
33
tsconfig.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user