Viktor Oddy — Creative Studio Landing Page
Landing page vendored from the MIT-licensed
pulkitxm/claude-directorygallery.
Single-page landing for the fictional creative design studio Viktor Oddy, built with React + TypeScript + Vite + Tailwind CSS and lucide-react icons. The design is minimal white-background throughout, pairing PP Neue Montreal (body, Webflow CDN) with PP Mondwest (serif accent, served locally from /PPMondwest-Regular.woff2). Staggered IntersectionObserver scroll reveals, an infinite image marquee, a parallax testimonial portrait, pricing cards, an auto-scrolling testimonial carousel, a project showcase, and a cursor-trail GIF CTA make up the full interactive experience.
Workflow
- Clone
example.htmlinto the user's workspace as the working file. - Replace placeholder content with the user's real brand name, headlines, body copy, numbers, and imagery. Match existing image dimensions when swapping assets.
- Preserve the design system. The palette, type scale, spacing rhythm, and motion in the build spec below are the identity — do not substitute fonts, recolour the palette, or strip decorative elements.
- Extend by duplicating sections, never by importing a layout from another template. If a section is missing, design it from scratch in this template's own vocabulary.
- Keep motion accessible. Every animation must stay behind
prefers-reduced-motion, as the build spec requires.
Output contract
Emit between <artifact> tags:
<artifact identifier="viktor-oddy-landing" type="text/html" title="Viktor Oddy — Creative Studio Landing Page">
<!doctype html>
<html>...</html>
</artifact>
Build spec
The upstream prompt that produced this design, verbatim.
Viktor Oddy — Creative Studio Landing Page
Overview
Build a single-page marketing landing page for a creative design studio called "Viktor Oddy". The page has a white background throughout, uses two custom fonts, and is composed of a centered hero, an infinite image marquee, a parallax testimonial, a pricing grid, an auto-scrolling testimonial carousel, a project showcase, an interactive partner CTA, a footer, a copyright bar, and a fixed floating bottom nav. Scroll-triggered fade-in animations are used throughout.
Tech Stack
- Framework: React 18 (
react^18.3.1,react-dom^18.3.1) with TypeScript (typescript^5.7.3). - Build tool: Vite (
vite^5.4.14) with@vitejs/plugin-react(^4.3.4). Build script:tsc -b && vite build. - Styling: Tailwind CSS (
tailwindcss^3.4.17) withpostcss(^8.5.1) andautoprefixer(^10.4.20). - Icons:
lucide-react(^0.469.0) —Quote,Star,ChevronLeft,ChevronRight,ArrowUpRight. - Fonts: PP Neue Montreal (body, loaded from Webflow CDN) and PP Mondwest (serif accent, loaded from a local
/PPMondwest-Regular.woff2file). The body default font is PP Neue Montreal with system fallbacks (font-sans). - Notable techniques:
IntersectionObserver-driven scroll reveals, a CSS marquee, arequestAnimationFrameparallax image, an auto-playing infinite carousel with snap-back, and a cursor-trail effect that spawns image thumbnails on mouse move.
Global Setup
index.html
lang="en", charset UTF-8, viewport meta.- Description meta:
Viktor Oddy — the creative studio of Viktor Oddy. Build the next wave, the bold way. - Title:
Viktor Oddy — Creative Studio. - Inline SVG favicon (data URI): a rounded square
fill='%23051A24'with a serif "V" infill='%23F6FCFF'. - Preload the font:
<link rel="preload" href="/PPMondwest-Regular.woff2" as="font" type="font/woff2" crossorigin />. - Root element
<div id="root"></div>mounted by/src/main.tsx.
src/main.tsx
Standard React 18 entry. Renders <App /> inside <StrictMode> and imports ./index.css.
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);
src/index.css — fonts and global base
Includes the three @tailwind directives, the @font-face declarations, a base layer, and the keyframe animations.
@tailwind base;
@tailwind components;
@tailwind utilities;
@font-face {
font-family: 'PP Neue Montreal';
src: url('https://assets.website-files.com/6009ec8cda7f305645c9d91b/60176f9bb43e36419997ecfe_PPNeueMontreal-Book.otf')
format('opentype');
font-weight: 400;
font-display: swap;
}
@font-face {
font-family: 'PP Neue Montreal';
src: url('https://assets.website-files.com/6009ec8cda7f305645c9d91b/60176f9b39c5673e51a86f5a_PPNeueMontreal-Medium.otf')
format('opentype');
font-weight: 500;
font-display: swap;
}
@font-face {
font-family: 'PP Mondwest';
src: url('/PPMondwest-Regular.woff2') format('woff2');
font-weight: 400;
font-display: swap;
}
@layer base {
html {
scroll-behavior: smooth;
}
body {
@apply bg-white font-sans text-[#051A24] antialiased;
}
}
Configure Tailwind so that font-serif maps to PP Mondwest and font-sans to PP Neue Montreal (with system fallbacks).
Layout (src/App.tsx)
The root <div id="top"> uses min-h-screen bg-white pb-32. Sections render in this order:
HeroMarqueeTestimonialSectionPricingSectionTestimonialCarouselProjectsSectionPartnerSection(receivesimages={MARQUEE_IMAGES})FooterCopyrightBarBottomNav
Shared constants
BOOK_URL = 'https://halaskastudio.com/./book'— used by primary/secondary CTAs throughout.MARQUEE_IMAGES— eight GIF paths (also reused byPartnerSection):
const MARQUEE_IMAGES = [
'/assets/hero-space-voyage-preview-eECLH3Yc.gif',
'/assets/hero-portfolio-cosmic-preview-BpvWJ3Nc.gif',
'/assets/hero-velorah-preview-CJNTtbpd.gif',
'/assets/hero-asme-preview-B_nGDnTP.gif',
'/assets/hero-transform-data-preview-Cx5OU29N.gif',
'/assets/hero-aethera-preview-DknSlcTa.gif',
'/assets/hero-orbit-web3-preview-BXt4OttD.gif',
'/assets/hero-nexora-preview-cx5HmUgo.gif',
];
Asset note: These GIFs (and the avatar/parallax images below) are vendored locally under
public/assets/. They originate fromhttps://motionsites.ai/assets/...(e.g.https://motionsites.ai/assets/hero-space-voyage-preview-eECLH3Yc.gif), with case-sensitive filenames matching the local paths.
Hero (Hero in src/App.tsx)
Centered, narrow column: <section id="about"> with mx-auto w-full max-w-[440px] px-6 pt-12 md:pt-16. Uses the useInViewAnimation hook for staggered reveals.
- Logo text:
Viktor Oddyin an<h1>, classesmb-4 font-serif text-[32px] font-semibold tracking-tight text-[#051A24] md:text-[40px] lg:text-[44px]. Animation delay0.1s. - Tagline:
The creative studio of Viktor Oddyin a<p>, classesmb-2 font-mono text-xs text-[#051A24] md:text-sm. Animation delay0.2s. - Main heading (
<h2>): two lines —Build the next wave,/the bold way.— wherenext waveandbold way.are wrapped in<span className="font-serif">. Classeswhitespace-nowrap text-[32px] leading-[1.1] tracking-tight text-[#0D212C] md:text-[40px] lg:text-[44px]. A<br />separates the lines. Animation delay0.3s. - Description: a
flex flex-col gap-6container with classesmt-5 ... text-sm leading-relaxed text-[#051A24] md:mt-6 md:text-base, three paragraphs. Animation delay0.4s:- Paragraph 1:
I spent seven years at Apple crafting products used by over a billion people. I founded Vortex Studio to bring that same level of thinking to innovators shaping what comes next. - Paragraph 2:
The studio is deliberately small. I guide the creative vision on every project, backed by a veteran design crew that moves fast without cutting corners. - Paragraph 3:
Projects start at $5,000 per month.
- Paragraph 1:
- Buttons: a
flex flex-col gap-3 sm:flex-row md:mt-6 md:gap-4container,mt-5, animation delay0.5s:Start a chat—variant="primary",href={BOOK_URL}.View projects—variant="secondary",href="#work".
Infinite Marquee (Marquee in src/App.tsx)
Full-width horizontally scrolling image strip.
- Wrapper:
mb-16 mt-16 w-full overflow-hidden md:mt-20. - Track:
animate-marquee flex w-max. - The eight
MARQUEE_IMAGESare duplicated ([...MARQUEE_IMAGES, ...MARQUEE_IMAGES], 16 total). Each<img>haskey={i},alt="",aria-hidden={i >= MARQUEE_IMAGES.length}(the duplicate copy is hidden from assistive tech), and classesmx-3 h-[280px] w-auto rounded-2xl object-cover shadow-lg md:h-[500px]. - Animation (CSS):
translateX(0)totranslateX(-50%),30s linear infiniteon desktop,10son mobile.
Testimonial / Parallax Section (src/components/TestimonialSection.tsx)
<section> with mx-auto max-w-2xl px-6 py-12 text-center. Uses useInViewAnimation plus a custom useParallax hook.
- Quote icon: Lucide
Quote, classesmx-auto h-6 w-6 text-slate-900, wrapped in a<div>with animation delay0.1s. - Large quote (
<blockquote>):‘I left+<span className="font-serif">Apple</span>+to build the studio I always wanted to work with’(uses‘/’curly quotes). Classesmt-6 text-[32px] leading-[1.1] tracking-tight text-[#0D212C] md:text-[40px] lg:text-[44px]. Animation delay0.2s. - Author:
Viktor Oddyin a<p>, classesmt-6 text-sm italic text-[#273C46]. Animation delay0.3s. - Company logos (as text): a
mt-12 flex items-center justify-center gap-8row, animation delay0.4s. Each is a<span>with classestext-[24px] font-medium text-slate-900and an inlinewidth:Apple—80pxIDEO—83pxPolygon—110px
- Parallax image: a
<div ref={parallaxRef}>withmt-16and animation delay0.5s, containing an<img>:src=/assets/hf_20260330_103804_7aa5494f-4d5b-432e-9dc7-20715275f143.webpalt="Chris Halaska",loading="lazy"- Classes
mx-auto w-full max-w-xs rounded-2xl shadow-lg will-change-transform - Inline
style={{ transform: \translateY(${offset.toFixed(2)}px)` }}`
Asset note: The parallax image is vendored locally as the
.webpabove. It originally came fromhttps://images.higgs.ai/?default=1&output=webp&url=https%3A%2F%2Fd8j0ntlcm91z4.cloudfront.net%2Fuser_38xzzbokvigwjottwixh07lwa1p%2Fhf_20260330_103804_7aa5494f-4d5b-432e-9dc7-20715275f143.png&w=1280&q=85.
useParallax hook
const PARALLAX_MAX_OFFSET = 200;
- Tracks an element with an
IntersectionObserver(sets avisibleflag) plus passivescrollandresizelisteners. - On update, computes progress
(viewportHeight - rect.top) / (viewportHeight + rect.height)clamped to[0, 1], thennext = (clamped - 0.5) * 2 * PARALLAX_MAX_OFFSET, clamped to[-200, 200]px. - Updates are scheduled via
requestAnimationFrameand only run while the element is visible; the frame and listeners are cleaned up on unmount.
Pricing Section (src/components/PricingSection.tsx)
<section id="services"> with w-full px-6 py-12. Inner wrapper md:flex md:justify-end; the grid is grid grid-cols-1 gap-8 md:max-w-4xl md:grid-cols-2 (right-aligned on desktop). Uses useInViewAnimation. BOOK_URL = 'https://halaskastudio.com/./book'.
Card 1 — Monthly Partnership (dark)
<article> classes: rounded-[40px] bg-[#051A24] pb-10 pl-10 pr-10 pt-3 shadow-[inset_0_2px_16px_0_rgba(246,252,255,0.12)] md:pr-24. Animation delay 0.1s.
- Title (
<h3>):Monthly Partnership—mt-7 text-[22px] font-medium text-[#F6FCFF]. - Description (
<p>):A dedicated creative design team.<br />You work directly with Viktor.—mt-3 text-sm leading-relaxed text-[#E0EBF0]. - Price:
$5,000inmt-8 text-2xl text-[#F6FCFF], thenMonthlyinmt-1 text-sm text-[#E0EBF0]. - Buttons (
mt-8 flex flex-col gap-3 sm:flex-row):Start a chat(variant="primary") andHow it works(variant="secondary"), bothhref={BOOK_URL}.
Card 2 — Custom Project (light)
<article> classes: rounded-[40px] bg-white pb-10 pl-10 pr-10 pt-3 shadow-[0_4px_16px_rgba(0,0,0,0.08)] md:pr-24. Animation delay 0.2s.
- Title (
<h3>):Custom Project—mt-7 text-[22px] font-medium text-[#0D212C]. - Description (
<p>):Fixed scope, fixed timeline.<br />Same team, same standards.—mt-3 text-sm leading-relaxed text-[#273C46]. - Price:
$5,000inmt-8 text-2xl text-[#0D212C], thenMinimuminmt-1 text-sm text-[#273C46]. - Button (
mt-8 flex):Start a chat(variant="tertiary"),href={BOOK_URL}.
Testimonial Carousel (src/components/TestimonialCarousel.tsx)
<section> with w-full overflow-hidden py-20. Uses useInViewAnimation.
Header row
A flex flex-col gap-8 md:ml-auto md:max-w-4xl md:flex-row md:items-end md:justify-between container (animation delay 0.1s):
- Title (
<h2>):What+<span className="font-serif">builders</span>+say, classestext-[32px] leading-[1.1] tracking-tight text-[#0D212C] md:text-[40px] lg:text-[44px]. - Right cluster (
flex flex-col items-start gap-4 md:items-end):- Five Lucide
Staricons (h-5 w-5 fill-black text-black) inside aflexwrapper labeledaria-label="5 out of 5 stars", followed byClutch 5/5intext-sm text-[#051A24]. - Prev/next buttons in a
flex gap-3: eachflex h-12 w-12 items-center justify-center rounded-full border border-[#0D212C]/20 text-[#0D212C] transition-colors hover:bg-[#0D212C]/5, containing LucideChevronLeft/ChevronRight(h-5 w-5) witharia-label"Previous testimonial" / "Next testimonial".
- Five Lucide
Carousel mechanics
const COUNT = TESTIMONIALS.length;
const TRIPLED = [...TESTIMONIALS, ...TESTIMONIALS, ...TESTIMONIALS];
const GAP = 24; // gap-6
const DESKTOP_CARD_WIDTH = 427.5;
const AUTOPLAY_MS = 3000;
- State:
index(starts atCOUNT),animate,hovered,cardWidth. - Card width updates on resize:
window.innerWidth >= 768 ? DESKTOP_CARD_WIDTH : window.innerWidth - 48(i.e. 427.5px on desktop, full width minus 48px on mobile). - Auto-advance every
3000msviasetInterval; pauses whilehovered(onMouseEnter/onMouseLeaveon the track wrapper). - The testimonials are tripled for an infinite-scroll illusion.
handleTransitionEndsnaps the index back into the middle copy without animation when it drifts pastCOUNT * 2or belowCOUNT(re-enablinganimateafter a doublerequestAnimationFrame). - Track container:
mt-12 pl-6(animation delay0.2s). Inner track:flex gap-6 will-change-transformwith inlinetransform: translateX(-offset px)whereoffset = index * (cardWidth + GAP), andtransition: 'transform 0.8s cubic-bezier(0.4, 0, 0.2, 1)'when animating ('none'otherwise).
Cards
Each <article>: shrink-0 rounded-[32px] bg-white px-6 py-8 shadow-[0_4px_16px_rgba(0,0,0,0.08)] md:rounded-[40px] md:pl-10 md:pr-24, inline width: ${cardWidth}px. Exited cards (i < index) fade and scale down: opacity: 0 and transform: scale(0.9) with transition: opacity 0.8s cubic-bezier(0.4, 0, 0.2, 1), transform 0.8s cubic-bezier(0.4, 0, 0.2, 1).
Card content:
QuoteMarkSVG — a custom28x22(viewBox="0 0 28 22") double-quote glyph filled#0D212C:
<svg width="28" height="22" viewBox="0 0 28 22" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path
d="M0 22V14.3C0 11.5 0.56 9 1.68 6.8C2.84 4.56 4.72 2.3 7.32 0L11.6 3.1C10 4.62 8.82 6.04 8.06 7.36C7.34 8.68 6.94 10.06 6.86 11.5H12.2V22H0ZM15.8 22V14.3C15.8 11.5 16.36 9 17.48 6.8C18.64 4.56 20.52 2.3 23.12 0L27.4 3.1C25.8 4.62 24.62 6.04 23.86 7.36C23.14 8.68 22.74 10.06 22.66 11.5H28V22H15.8Z"
fill="#0D212C"
/>
</svg>
- Quote text (
<p>):mt-5 text-base leading-relaxed text-[#0D212C]. - Author row (
mt-7 flex items-center gap-4): circular avatar<img>(h-12 w-12 rounded-full object-cover,loading="lazy",alt={name}), name intext-sm font-semibold text-[#051A24], role/company intext-sm text-[#273C46]prefixed with→(→):→ {role}, {company}.
Testimonials data
| Name | Role | Company | Avatar |
|---|---|---|---|
Marcus Anderson | CEO | Data.storage | /assets/pexels-2379004.jpg |
alexwu | Founder | Nexgate | /assets/pexels-220453.jpg |
James Mitchell | VP Product | LaunchPad | /assets/pexels-614810.jpg |
Rachel Foster | Co-founder | Nexus Labs | /assets/pexels-774909.jpg |
David Zhang | Head of Design | Paradigm Labs | /assets/pexels-1222271.jpg |
Quotes (verbatim):
With very little guidance team delivered designs that were consistently spot on. They understood our brand from day one, and every iteration landed closer than anything we could have briefed ourselves.Viktor led the creation of our best fundraising deck to date! Investors kept commenting on the design before we even got to the numbers. Worth every dollar and then some.Working with Viktor transformed our product vision into an interface our users genuinely love. Every screen feels considered — nothing shipped until it earned its place.The design quality exceeded our expectations at every single milestone. It felt like having a world-class in-house design team without the overhead of building one.Incredible work from start to finish. Fast, opinionated in the best way, and relentlessly polished. The crew moves like a team three times its size.
Asset note: Avatars are vendored locally from Pexels (
pexels-*.jpg).
Projects Section (src/components/ProjectsSection.tsx)
<section id="work"> with mx-auto max-w-[1200px] px-6 py-12. Inner stack: flex flex-col gap-16 md:gap-20. Each ProjectItem independently triggers a fade-in via its own useInViewAnimation.
Each ProjectItem (animation delay 0.1s):
- Text block offset left:
<div className="ml-20 mb-6 md:ml-28">with the project name (<h3>,font-serif text-2xl font-semibold text-[#051A24] md:text-3xl) and description (<p>,mt-2 text-sm text-[#051A24]/70 md:text-base). - Full-width
<img>below:w-full rounded-2xl object-cover shadow-lg,loading="lazy",alt={\${name} — ${description}`}`.
Projects data:
| Name | Description | Image |
|---|---|---|
evr | From idea to millions raised for a web3 AI product | /assets/hero-evr-ventures-preview-DZxeVFEX.gif |
Automation Machines | Streamlining industrial automation processes | /assets/hero-automation-machines-preview-DlTveRIN.gif |
xPortfolio | Modern portfolio management platform | /assets/hero-xportfolio-preview-D4A8maiC.gif |
Asset note: Project GIFs are vendored locally; original origin
https://motionsites.ai/assets/....
Partner Section (src/components/PartnerSection.tsx)
<section> with w-full px-6 py-12. Receives images: string[] (the MARQUEE_IMAGES). Uses useInViewAnimation.
const SPAWN_INTERVAL_MS = 80;
const TRAIL_LIFETIME_MS = 1000;
const AVATAR = '/assets/pexels-415829.jpg';
- Container (
ref={containerRef},onMouseMove={handleMouseMove}):relative mx-auto max-w-7xl overflow-hidden rounded-[40px] bg-white py-48 text-center shadow-[0_0_0_0.5px_rgba(0,0,0,0.05),0_4px_30px_rgba(0,0,0,0.06)]. Animation delay0.1s. - Cursor trail: on
mousemove, no more often than every80ms(performance.now()throttle), aTrailItemis spawned at the cursor position (event.clientX/Yminus the container rect) with a randomrotationofMath.random() * 20 - 10(i.e. −10° to +10°), cycling throughimages. Each trail<img>isanimate-trail-fade pointer-events-none absolute z-0 w-36 rounded-lg shadow-lg, positioned via inlineleft/top, and sets the CSS custom property--trail-rotation. Items are removed after1000msviasetTimeout; all timeouts are tracked in aSetand cleared on unmount. - Foreground (
pointer-events-none relative z-10):- Heading (
<h2>):Partner with us, classesmb-12 font-serif text-[48px] leading-[1.05] text-[#0D212C] md:text-[64px] lg:text-[80px]. - CTA
Button variant="primary",href="https://halaskastudio.com/./book", extra classespointer-events-auto py-2 pl-2, containing a circular avatar<img src={AVATAR}>(h-10 w-10 rounded-full object-cover,aria-hidden) and the labelStart chat with Viktor.
- Heading (
Asset note: The avatar (
pexels-415829.jpg) is vendored locally from Pexels.
Footer (src/components/Footer.tsx)
<footer> with mx-auto w-full max-w-[1200px] px-6 py-12, animation delay 0.1s (via useInViewAnimation).
- Inner
flex flex-col justify-between gap-12 md:flex-row. - Left:
Start a chat(variant="primary",href="https://halaskastudio.com/./book"). - Right (
flex items-start gap-8): LucideArrowUpRight(mt-1 h-5 w-5 text-[#051A24]), then two<nav>columns of links (flex flex-col gap-3):- Anchor links (
aria-label="Site"):Services→#services,Work→#work,About→#about. - External links (
aria-label="Social",target="_blank",rel="noopener noreferrer"):x.com→https://x.com,LinkedIn→https://www.linkedin.com.
- Anchor links (
- All links:
text-base text-[#051A24] transition-opacity hover:opacity-70.
Copyright Bar (src/components/CopyrightBar.tsx)
A <div> with mx-auto flex w-full max-w-[1200px] items-center justify-between px-6 py-4 text-sm text-[#051A24]. Left <span>: Vortex Studio Limited. Right <span>: Austin, USA.
Fixed Bottom Nav (src/components/BottomNav.tsx)
A floating pill <nav aria-label="Quick actions"> fixed to the bottom and horizontally centered: fixed bottom-6 left-1/2 z-50 flex -translate-x-1/2 items-center gap-6 rounded-full bg-white px-8 py-2, with a layered shadow:
shadow-[0_0_0_0.5px_rgba(0,0,0,0.05),0_1px_2px_0_rgba(5,26,36,0.1),0_4px_4px_0_rgba(5,26,36,0.09),0_9px_6px_0_rgba(5,26,36,0.05),0_17px_7px_0_rgba(5,26,36,0.01),0_4px_30px_rgba(0,0,0,0.08)]
Contents:
- A
Vlink (href="#top",aria-label="Back to top") infont-serif text-2xl font-semibold text-[#051A24]. - A
Start a chatbutton (variant="primary",href="https://halaskastudio.com/./book", extra classespx-5 py-2.5).
Button Component (src/components/Button.tsx)
A reusable anchor-based button. Props extend AnchorHTMLAttributes<HTMLAnchorElement> with variant?: 'primary' | 'secondary' | 'tertiary' (default 'primary') and children.
Base classes:
inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-full px-7 py-3 text-sm font-medium transition-transform duration-200 ease-out hover:-translate-y-0.5 active:translate-y-0
Variant classes:
- primary:
bg-[#051A24] text-white+PRIMARY_SHADOW - secondary:
bg-white text-[#051A24]+SECONDARY_SHADOW - tertiary:
bg-white text-[#051A24]+TERTIARY_SHADOW
Shadow tokens (critical for the design feel):
PRIMARY_SHADOW =
'shadow-[0_1px_2px_0_rgba(5,26,36,0.1),0_4px_4px_0_rgba(5,26,36,0.09),0_9px_6px_0_rgba(5,26,36,0.05),0_17px_7px_0_rgba(5,26,36,0.01),0_26px_7px_0_rgba(5,26,36,0),inset_0_2px_8px_0_rgba(255,255,255,0.5)]'
SECONDARY_SHADOW =
'shadow-[0_0_0_0.5px_rgba(0,0,0,0.05),0_4px_30px_rgba(0,0,0,0.08)]'
TERTIARY_SHADOW =
'shadow-[0_0_0_0.5px_rgba(0,0,0,0.05),0_4px_30px_rgba(0,0,0,0.08),0_1px_2px_0_rgba(5,26,36,0.1),0_4px_4px_0_rgba(5,26,36,0.09),0_9px_6px_0_rgba(5,26,36,0.05)]'
Animations
useInViewAnimation hook (src/hooks/useInViewAnimation.ts)
A generic scroll-trigger hook. It observes a refd element with an IntersectionObserver (threshold: 0.1), flips inView to true once when at least 10% enters the viewport (then disconnects, so it triggers only once), and returns { ref, inView, animationClass } where animationClass is 'animate-fade-in-up' when in view and 'opacity-0' otherwise.
import { useEffect, useRef, useState } from 'react';
export function useInViewAnimation<T extends HTMLElement = HTMLDivElement>() {
const ref = useRef<T | null>(null);
const [inView, setInView] = useState(false);
useEffect(() => {
const element = ref.current;
if (!element) return;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0]?.isIntersecting) {
setInView(true);
observer.disconnect();
}
},
{ threshold: 0.1 },
);
observer.observe(element);
return () => observer.disconnect();
}, []);
const animationClass = inView ? 'animate-fade-in-up' : 'opacity-0';
return { ref, inView, animationClass };
}
Each element within a section uses staggered animationDelay values (0.1s, 0.2s, 0.3s, etc.).
CSS keyframes (in src/index.css)
/* ---- Infinite marquee ---- */
@keyframes marquee {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-50%);
}
}
.animate-marquee {
animation: marquee 30s linear infinite;
}
@media (max-width: 767px) {
.animate-marquee {
animation-duration: 10s;
}
}
/* ---- Scroll-triggered fade in ---- */
@keyframes fadeInUp {
0% {
opacity: 0;
transform: translateY(30px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in-up {
animation: fadeInUp 0.8s ease-out forwards;
opacity: 0;
}
/* ---- Cursor trail thumbnails (PartnerSection) ---- */
@keyframes trail-fade {
0% {
opacity: 1;
transform: translate(-50%, -50%) rotate(var(--trail-rotation, 0deg)) scale(1);
}
100% {
opacity: 0;
transform: translate(-50%, -50%) rotate(var(--trail-rotation, 0deg)) scale(0.6);
}
}
.animate-trail-fade {
animation: trail-fade 1000ms ease-out forwards;
}
@media (prefers-reduced-motion: reduce) {
.animate-marquee,
.animate-fade-in-up,
.animate-trail-fade {
animation-duration: 0.01ms;
animation-iteration-count: 1;
}
}
Note: the trail keyframe scales to
0.6and the.animate-fade-in-uprule keepsopacity: 0so elements stay hidden until their delayed animation runs. Aprefers-reduced-motion: reduceblock neutralizes all three animations.
Color Palette
- Primary dark:
#051A24(also body text) - Secondary dark:
#0D212C - Light text on dark:
#F6FCFF,#E0EBF0 - Muted text:
#273C46 - Background: white throughout
File Structure
index.html— favicon, font preload, root mount.src/main.tsx— React entry (StrictMode, importsindex.css).src/App.tsx— main layout withHero,Marquee, and section composition; definesMARQUEE_IMAGESandBOOK_URL.src/components/Button.tsx— reusable button (primary/secondary/tertiary variants).src/components/TestimonialSection.tsx— quote with parallax image (useParallax).src/components/PricingSection.tsx— two pricing cards.src/components/TestimonialCarousel.tsx— auto-scrolling testimonial cards.src/components/ProjectsSection.tsx— project showcase items.src/components/PartnerSection.tsx— interactive mouse-trail CTA section.src/components/Footer.tsx— footer with links.src/components/CopyrightBar.tsx— copyright line.src/components/BottomNav.tsx— fixed floating bottom nav.src/hooks/useInViewAnimation.ts—IntersectionObserverscroll-trigger hook.src/index.css— font faces, marquee animation, fade-in-up animation, cursor-trail animation.public/PPMondwest-Regular.woff2— local serif accent font.public/assets/— vendored GIF previews, Pexels avatars, and the parallax.webp.
Source & license
Vendored from MIT-licensed
pulkitxm/claude-directory.
The upstream MIT licence text ships in this template at LICENSE and
must be redistributed alongside any copy of example.html or assets/.
Webfonts and full-resolution imagery are not vendored: local @font-face
rules are served from the Google Fonts CDN and referenced images are
downscaled. Fetch the upstream folder for the original assets.