Rodo Dendron Editorial Photography Portfolio AI website prompt
A dark, motion-led portfolio for editorial photographers. The hero pins for two full screens while the opening frame scales and tumbles away; galleries drift past each other at three speeds; a sticky panel inverts the page as six words fly in. Sixteen editable sections, a project grid, field notes and a contact form.
Now put it in WordPress
You have a site. Now make it yours.
An AI build hands you a front end and stops there. No CMS, no client login, no real ownership: you cannot hand it over, and you cannot change it without going back and re-prompting. UiChemy turns that build into native, editable WordPress you own, in the builder you already use.
Bolt to WordPressClaude to WordPressCodex to WordPressCursor to WordPressLovable to WordPressv0 to WordPressAI to WordPress
Get Started
More prompts
Showing 5 prompts
Frequently asked questions
What does the Rodo Dendron Editorial Photography Portfolio prompt build?
A dark, motion-led portfolio for editorial photographers. The hero pins for two full screens while the opening frame scales and tumbles away; galleries drift past each other at three speeds; a sticky panel inverts the page as six words fly in. Sixteen editable sections, a project grid, field notes and a contact form.
Which AI tools does the Rodo Dendron Editorial Photography Portfolio prompt work with?
It's written for Claude. Paste the whole prompt into Claude and it builds the complete site for you, then bring the result into WordPress with UiChemy. This prompt also has a WordPress version: with the UiChemy MCP connected, Claude builds the page straight into WordPress.
How do I turn the result into a WordPress site?
After the AI builds your site, UiChemy Composer rebuilds it as native, editable WordPress in Elementor or Gutenberg (Bricks coming soon) — you own it and edit it in your builder. See AI to WordPress.
Is the Rodo Dendron Editorial Photography Portfolio prompt free?
Yes — it's free to copy, no account needed.
Can I edit the website after it's built?
Yes. Once it's in WordPress with UiChemy, every section is native and editable — no re-prompting, no lock-in.
Copy prompt
Copy code
Coming soon
Copy SKILL.md
Coming soon
Rodo Dendron Editorial Photography Portfolio · Section prompt
# MASTER PROMPT — Rodo Dendron | A Photographer Based In New York
### Plain HTML, CSS and JavaScript. No WordPress, no page builder, no framework, no build step.
## ▶ HOW TO USE THIS PROMPT
**Paste this whole document into Claude. Nothing to attach.** It carries the complete source of the site plus the address of every image, so Claude can assemble a folder that owns its own assets and hand it back as a zip.
**INSTRUCTION TO CLAUDE — BUILD IT, DO NOT ASK.** Every decision is already made in this document. Do not stop to ask the user questions, do not offer options, and do not ask for confirmation before starting. Write the files, zip them, hand over the archive, then report what you built.
1. Create a folder named `rodo` and write out every file in the appendix below **exactly as written** — do not reformat, rename, shorten, reorder or "improve" anything.
2. **Download the 33 images into `assets/`.** The code already points at `assets/…` — nothing in it references a remote host. Run this from inside the folder; it reads the ASSET MANIFEST at the end of this document:
```bash
mkdir -p assets
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-about-tall-photo.jpg' -o 'assets/about-tall-photo.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-about-tall-warm.jpg' -o 'assets/about-tall-warm.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-contact-photo.jpg' -o 'assets/contact-photo.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-gallery-1-photo.jpg' -o 'assets/gallery-1-photo.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-gallery-1-warm.jpg' -o 'assets/gallery-1-warm.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-gallery-2-photo.jpg' -o 'assets/gallery-2-photo.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-gallery-2-warm.jpg' -o 'assets/gallery-2-warm.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-gallery-3-photo.jpg' -o 'assets/gallery-3-photo.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-gallery-3-warm.jpg' -o 'assets/gallery-3-warm.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-gallery-4-photo.jpg' -o 'assets/gallery-4-photo.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-gallery-4-warm.jpg' -o 'assets/gallery-4-warm.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-gallery-5-photo.jpg' -o 'assets/gallery-5-photo.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-gallery-5-warm.jpg' -o 'assets/gallery-5-warm.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-gallery-6-photo.jpg' -o 'assets/gallery-6-photo.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-gallery-6-warm.jpg' -o 'assets/gallery-6-warm.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-hero-photo.jpg' -o 'assets/hero-photo.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-hero-warm.jpg' -o 'assets/hero-warm.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-intro-1-photo.jpg' -o 'assets/intro-1-photo.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-intro-1-warm.jpg' -o 'assets/intro-1-warm.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-intro-2-photo.jpg' -o 'assets/intro-2-photo.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-intro-2-warm.jpg' -o 'assets/intro-2-warm.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-news-1-photo.jpg' -o 'assets/news-1-photo.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-news-2-photo.jpg' -o 'assets/news-2-photo.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-news-3-photo.jpg' -o 'assets/news-3-photo.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-news-3-warm.jpg' -o 'assets/news-3-warm.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-work-1-photo.jpg' -o 'assets/work-1-photo.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-work-1-warm.jpg' -o 'assets/work-1-warm.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-work-2-photo.jpg' -o 'assets/work-2-photo.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-work-3-photo.jpg' -o 'assets/work-3-photo.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-work-3-warm.jpg' -o 'assets/work-3-warm.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-work-4-photo.jpg' -o 'assets/work-4-photo.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-work-4-warm.jpg' -o 'assets/work-4-warm.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-work-5-photo.jpg' -o 'assets/work-5-photo.jpg'
```
Then check every file arrived and none is a zero-byte or HTML error page:
```bash
ls assets | wc -l # expect 33 files
find assets -size -1k # expect no output
```
If any download fails, retry it — the host is occasionally slow. Do **not** fall back to hotlinking the remote URL: the whole point is a folder that works offline.
3. **Zip that folder and give the user the archive.** Its contents:
```
rodo/
index.html
css/style.css
js/main.js
assets/
about-tall-photo.jpg
about-tall-warm.jpg
contact-photo.jpg
gallery-1-photo.jpg
… 29 more images
```
```bash
zip -r rodo.zip rodo -x '*.DS_Store'
```
Then hand over `rodo.zip` as a file the user can download, not as text in the chat.
4. Before handing it over, serve the folder and confirm it runs:
```bash
cd rodo && python3 -m http.server 8000 # then open http://localhost:8000
```
Check the page renders, the images load from `assets/` with the network throttled or offline, and scrolling drives the animations.
The finished folder needs no network for its images — they are in `assets/`.
Do not rebuild the design from the description below. The description is there so a person can understand and edit the code. The appendix is the deliverable.
---
## ▶ WHAT YOU GET
Rodo Dendron — freelance photographer based in New York. Natural-light portraits, fashion editorials and intimate visual stories.
**3 code files plus an `assets/` folder holding 33 images, delivered as `rodo.zip`.**
| File | What it is |
|---|---|
| `index.html` | a page, 19 KB |
| `css/style.css` | the stylesheet, 29 KB |
| `js/main.js` | the script, 19 KB |
| `assets/` | 33 images, downloaded at build time |
**Sections (9):** `#intro` · `#mam` · `#art-gallery` · `#short-about` · `#work` · `#newsroom` · `#work-with-me` · `#contact` · `#thank-you`.
---
## ▶ HOW THE PAGE MOVES
- **Hero.** A 200vh pinned scene. The still locks to the top of the screen and scales 1 → 1.4 → 0.5 across the first viewport, then tumbles away across the second — translateY −300, rotateX 30°, rotateY 10° — with a slow 26 s ken-burns drift running under it the whole time. The black panel with the nav rides over it.
- **Intro.** Every headline line slides up out of a clipped mask; the two photo cards tilt in 3-D as they cross the viewport; the ruled lines under "I work with clients around the world" draw left to right.
- **More about me.** A white panel sweeps across the black band while three words flip in on X, X-reverse and Y, 0.1 s apart.
- **Galleries.** Three columns parallax past each other at 200 / 700 / 1200 px amplitudes, scaled down proportionally below 1440 px so a phone does not get 1200 px of travel inside a 260 px image.
- **Text reveal.** A full-width panel pins and inverts the page beneath it with `mix-blend-mode: difference`; six words fly in from alternating sides, then the whole type block swings away on a hinge at its left edge.
- **Work.** The label pins beside five cards; each scales from 0.5 on hover and reveals its number and title.
- **See all my work.** Pins for a viewport while four rows slide in from alternating sides. The four empty spacers after it are what give the pin its scroll distance — keep them.
- **Contact and close.** The plate drops in, the form scales up, the heading rises out of its mask, and *Thank You / You Are Awesome* flips in at the end.
Smooth scrolling, the scrub scenes and the reveals are all hand-written in `js/main.js` — no GSAP, no ScrollTrigger, no Lenis, nothing to load. The wheel is intercepted and eased into the real scroll position, so `position: sticky`, anchors and find-in-page keep working. `prefers-reduced-motion` collapses every animation to its end state.
---
## ▶ WHERE THE MEDIA COMES FROM
All 33 files (33 images) live in `assets/` inside the folder, and every reference in the markup, the CSS and the script is a relative `assets/…` path. Once the folder is assembled it owns its own media: no CDN, no media library, no external image host. Open it on a laptop with the wifi off and it still renders.
They are fetched **once, at build time**, from the WordPress media library on `demo.uichemy.com` — the same `rodo__` files the WordPress build of this page uses. That is only how they travel into the folder; nothing in the delivered code points back at that host. Replacing an image means dropping a new file into `assets/` under the same name.
The manifest at the end of this document lists every source URL against the filename it is saved as, so the download step is reproducible and checkable.
---
## ▶ DESIGN TOKENS
Everything is driven from custom properties at the top of the stylesheet:
`--black` `--white` `--magenta` `--lime` `--cyan` `--grey` `--muted` `--gutter` `--pin` `--display` `--text` `--mono` `--fs-line` `--fs-line-sm` `--fs-nav` `--fs-word` `--fs-contact` `--ease` `--ease-io` `--dur`
Fonts load from `https://fonts.googleapis.com`, `https://fonts.gstatic.com`.
---
## ▶ WHAT THIS PROMPT DELIBERATELY DOES NOT CONTAIN
- No WordPress, no page builder, and none of the wrapper classes or compatibility CSS a builder needs. The WordPress build is a separate prompt.
- No framework, no TypeScript, no bundler, no `package.json`.
- No backend. Any form here validates and replies in place; it posts nowhere.
- No CMS. The content lives in the files.
- No external image host. The images ship inside the zip; nothing in the code calls out to a media library at runtime.
---
## ASSET MANIFEST — download these into `assets/`
Source is the media library on `demo.uichemy.com`. Save each one under the name in the second column; the code already refers to it by that name.
| Download from | Save as |
|---|---|
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-about-tall-photo.jpg` | `assets/about-tall-photo.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-about-tall-warm.jpg` | `assets/about-tall-warm.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-contact-photo.jpg` | `assets/contact-photo.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-gallery-1-photo.jpg` | `assets/gallery-1-photo.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-gallery-1-warm.jpg` | `assets/gallery-1-warm.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-gallery-2-photo.jpg` | `assets/gallery-2-photo.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-gallery-2-warm.jpg` | `assets/gallery-2-warm.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-gallery-3-photo.jpg` | `assets/gallery-3-photo.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-gallery-3-warm.jpg` | `assets/gallery-3-warm.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-gallery-4-photo.jpg` | `assets/gallery-4-photo.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-gallery-4-warm.jpg` | `assets/gallery-4-warm.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-gallery-5-photo.jpg` | `assets/gallery-5-photo.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-gallery-5-warm.jpg` | `assets/gallery-5-warm.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-gallery-6-photo.jpg` | `assets/gallery-6-photo.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-gallery-6-warm.jpg` | `assets/gallery-6-warm.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-hero-photo.jpg` | `assets/hero-photo.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-hero-warm.jpg` | `assets/hero-warm.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-intro-1-photo.jpg` | `assets/intro-1-photo.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-intro-1-warm.jpg` | `assets/intro-1-warm.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-intro-2-photo.jpg` | `assets/intro-2-photo.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-intro-2-warm.jpg` | `assets/intro-2-warm.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-news-1-photo.jpg` | `assets/news-1-photo.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-news-2-photo.jpg` | `assets/news-2-photo.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-news-3-photo.jpg` | `assets/news-3-photo.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-news-3-warm.jpg` | `assets/news-3-warm.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-work-1-photo.jpg` | `assets/work-1-photo.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-work-1-warm.jpg` | `assets/work-1-warm.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-work-2-photo.jpg` | `assets/work-2-photo.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-work-3-photo.jpg` | `assets/work-3-photo.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-work-3-warm.jpg` | `assets/work-3-warm.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-work-4-photo.jpg` | `assets/work-4-photo.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-work-4-warm.jpg` | `assets/work-4-warm.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/rodo__images-work-5-photo.jpg` | `assets/work-5-photo.jpg` |
---
# APPENDIX · VERBATIM SOURCE
The complete source of every file. **Write them out exactly** — do not change a value, a comment, a class name, or the order of a declaration. Recreate this file layout:
```
index.html
css/style.css
js/main.js
```
## `index.html`
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Rodo Dendron | A Photographer Based In New York</title>
<meta name="description" content="Rodo Dendron — freelance photographer based in New York. Natural-light portraits, fashion editorials and intimate visual stories.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@600;700;800&family=Inter+Tight:wght@400;500;600;700;800&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23000'/%3E%3Ctext x='16' y='23' font-family='Arial Black,Arial' font-size='20' font-weight='900' fill='%23fff' text-anchor='middle'%3ER%3C/text%3E%3C/svg%3E">
<link rel="stylesheet" href="css/style.css?v=responsive-5">
</head>
<body>
<!-- ============ FIXED CHROME ============ -->
<a class="chrome-logo" href="#top" aria-label="Rodo Dendron — home">
<span>Rodo</span><span>Dendron</span>
</a>
<button class="chrome-menu" type="button" aria-expanded="false" aria-controls="overlay-menu">menu</button>
<div class="menu-overlay" id="overlay-menu" hidden>
<nav class="menu-overlay__nav" aria-label="Main">
<a href="#top"><span>Home</span><span aria-hidden="true">Home</span></a>
<a href="#intro"><span>About me</span><span aria-hidden="true">About me</span></a>
<a href="#work"><span>Work</span><span aria-hidden="true">Work</span></a>
<a href="#newsroom"><span>Field Notes</span><span aria-hidden="true">Field Notes</span></a>
<a href="#contact"><span>Contact</span><span aria-hidden="true">Contact</span></a>
</nav>
</div>
<main id="top">
<!-- ============ 01 · HERO ============ -->
<section class="hero">
<svg class="hero__wordmark" viewBox="0 0 908 114" preserveAspectRatio="xMidYMid meet" aria-hidden="true" focusable="false">
<text x="0" y="111" textLength="908" lengthAdjust="spacingAndGlyphs">Rodo Dendron</text>
</svg>
<div class="hero__pin">
<div class="hero__bg"></div>
<div class="hero__media" data-hero-media>
<img src="assets/hero-warm.jpg" alt="Woman in a dark knit sweater beside a warmly lit studio window." data-kenburns>
</div>
</div>
<div class="hero__front">
<div class="hero__panel">
<div class="hero__panel-left">
<a class="hero__logo" href="#top" aria-label="Rodo Dendron — home"><span>Rodo</span><span>Dendron</span></a>
</div>
<nav class="hero__nav" aria-label="Primary">
<a href="#top"><span>Home</span><span aria-hidden="true">Home</span></a>
<a href="#intro"><span>About me</span><span aria-hidden="true">About me</span></a>
<a href="#work"><span>Work</span><span aria-hidden="true">Work</span></a>
<a href="#newsroom"><span>Field Notes</span><span aria-hidden="true">Field Notes</span></a>
<a href="#contact"><span>Contact</span><span aria-hidden="true">Contact</span></a>
</nav>
</div>
</div>
<div class="hero__tail"></div>
</section>
<!-- ============ 02 · INTRO ============ -->
<section class="intro" id="intro">
<div class="intro__inner">
<div class="line"><span class="line__mask"><span class="line__in">Hello there!</span></span></div>
<div class="line"><span class="line__mask"><span class="line__in">My name is</span></span></div>
<div class="line line--row">
<span class="line__mask"><span class="line__in c-magenta">Rodo</span></span>
<figure class="intro__card" data-tilt>
<img src="assets/intro-1-warm.jpg" alt="Curly-haired man in a linen shirt beside a studio wall.">
</figure>
</div>
<div class="line"><span class="line__mask"><span class="line__in">Dendron.</span></span></div>
<div class="line"><span class="line__mask"><span class="line__in">I am</span></span></div>
<div class="line"><span class="line__mask"><span class="line__in">Freelance</span></span></div>
<div class="line"><span class="line__mask"><span class="line__in">Editorial</span></span></div>
<div class="line"><span class="line__mask"><span class="line__in c-lime line__in--photographer">Photographer</span></span></div>
<div class="line line--row">
<span class="line__pair">
<span class="line__mask"><span class="line__in c-cyan">Based</span></span>
<span class="line__mask"><span class="line__in">In</span></span>
</span>
<figure class="intro__card" data-tilt>
<img src="assets/intro-2-warm.jpg" alt="Photographer and model talking beside a studio window.">
</figure>
</div>
<div class="line"><span class="line__mask"><span class="line__in c-outline">New York.</span></span></div>
</div>
<div class="intro__rules">
<div class="rule-line rule-line--end"><span class="line__mask"><span class="line__in">I work</span></span></div>
<div class="rule-line rule-line--end"><span class="line__mask"><span class="line__in">with</span></span></div>
<div class="rule-line rule-line--end"><span class="line__mask"><span class="line__in">clients</span></span></div>
<div class="rule-line rule-line--end"><span class="line__mask"><span class="line__in">around</span></span></div>
<div class="rule-line rule-line--end"><span class="line__mask"><span class="line__in">the</span></span></div>
<div class="rule-line rule-line--end"><span class="line__mask"><span class="line__in">world</span></span></div>
<div class="rule-line rule-line--start"><span class="line__mask"><span class="line__in">to create</span></span></div>
<div class="rule-line rule-line--end"><span class="line__mask"><span class="line__in">honest</span></span></div>
<div class="rule-line rule-line--end"><span class="line__mask"><span class="line__in">and</span></span></div>
<div class="rule-line rule-line--start"><span class="line__mask"><span class="line__in">timeless</span></span></div>
<div class="rule-line rule-line--end"><span class="line__mask"><span class="line__in">visual</span></span></div>
<div class="rule-line rule-line--end"><span class="line__mask"><span class="line__in">stories.</span></span></div>
</div>
</section>
<!-- ============ 03 · MORE ABOUT ME ============ -->
<section class="mam" id="mam">
<div class="mam__sweep" data-sweep></div>
<a class="mam__link" href="#short-about">
<span class="mam__w" data-flip="x">More</span>
<span class="mam__w" data-flip="x-rev">About</span>
<span class="mam__w mam__w--me" data-flip="y">Me
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h12M12 5l6 7-6 7" fill="none" stroke="currentColor" stroke-width="2.5"/></svg>
</span>
</a>
</section>
<!-- ============ 04 · GALLERY A ============ -->
<section class="gallery" id="art-gallery">
<div class="gallery__row">
<div class="gallery__cell" data-par="200"><img src="assets/gallery-1-warm.jpg" alt="Freckled woman in soft afternoon window light."></div>
<div class="gallery__cell" data-par="700"><img src="assets/gallery-2-warm.jpg" alt="Woman in a black dress in a warm daylight studio."></div>
<div class="gallery__cell" data-par="1200"><img src="assets/gallery-3-warm.jpg" alt="Seated man with salt-and-pepper hair in soft window light."></div>
</div>
</section>
<!-- ============ 05 · GALLERY B + TEXT REVEAL ============ -->
<section class="reveal-sec">
<div class="gallery gallery--flush" id="images-02">
<div class="gallery__row">
<div class="gallery__cell" data-par="1200"><img src="assets/gallery-4-warm.jpg" alt="Photographer working with a seated model in a daylight studio."></div>
<div class="gallery__cell" data-par="700"><img src="assets/gallery-5-warm.jpg" alt="Side profile of a woman with dark hair in warm sunlight."></div>
<div class="gallery__cell" data-par="200"><img src="assets/gallery-6-warm.jpg" alt="Silver-haired woman with a subtle smile beside a window."></div>
</div>
</div>
<div class="reveal-sec__track">
<div class="reveal-card" data-reveal-card>
<div class="reveal-card__invert">
<div class="reveal-card__inner">
<p class="reveal-word" data-from="600">timeless</p>
<p class="reveal-word" data-from="-600">images</p>
<p class="reveal-word" data-from="1056">with</p>
<p class="reveal-word" data-from="-900">feeling</p>
<p class="reveal-word" data-from="1386">and</p>
<p class="reveal-word" data-from="-1200">soul.</p>
</div>
</div>
</div>
<div class="reveal-sec__tail"></div>
</div>
</section>
<!-- ============ 06 · SHORT ABOUT ============ -->
<section class="about" id="short-about">
<div class="about__grid">
<div class="about__media">
<div class="about__media-mask"><img src="assets/about-tall-warm.jpg" alt="Photographer holding a camera beside a tall loft window." data-par-img="140"></div>
</div>
<div class="about__copy">
<p class="split" data-split>My work moves between intimate portraits, fashion editorials, and the quiet moments that unfold in the studio.</p>
<span></span>
<p class="split" data-split>I follow the light, look for the unexpected, and make space for the moments that feel honest and alive.</p>
<p class="split" data-split>From the first conversation to the final edit, I bring a thoughtful, personal approach to every shoot.</p>
</div>
</div>
</section>
<!-- ============ 07 · WORK ============ -->
<section class="work" id="work">
<div class="work__grid">
<div class="work__label">
<h2 class="flipchars" data-flipchars>My Work</h2>
</div>
<div class="work__list">
<a class="work-card" href="#contact">
<div class="work-card__frame">
<div class="work-card__img"><img src="assets/work-1-warm.jpg" alt="Two friends sharing a candid moment beside a brownstone window."></div>
<div class="work-card__scrim"></div>
<div class="work-card__meta">
<span class="work-card__view">View</span>
<span class="work-card__no">Work 01</span>
<span class="work-card__title">Quiet Connections — Portraits</span>
</div>
</div>
</a>
<a class="work-card" href="#contact">
<div class="work-card__frame">
<div class="work-card__img"><img src="assets/work-2-photo.jpg" alt="Burgundy chair in a sculptural concrete interior with orange light."></div>
<div class="work-card__scrim"></div>
<div class="work-card__meta">
<span class="work-card__view">View</span>
<span class="work-card__no">Work 02</span>
<span class="work-card__title">Quiet Spaces — Architecture</span>
</div>
</div>
</a>
<a class="work-card" href="#contact">
<div class="work-card__frame">
<div class="work-card__img"><img src="assets/work-3-warm.jpg" alt="Model in a black dress walking through a sunlit studio."></div>
<div class="work-card__scrim"></div>
<div class="work-card__meta">
<span class="work-card__view">View</span>
<span class="work-card__no">Work 03</span>
<span class="work-card__title">Soft Light — Fashion Editorial</span>
</div>
</div>
</a>
<a class="work-card" href="#contact">
<div class="work-card__frame">
<div class="work-card__img"><img src="assets/work-4-warm.jpg" alt="Photographer and assistant preparing a daylight studio shoot."></div>
<div class="work-card__scrim"></div>
<div class="work-card__meta">
<span class="work-card__view">View</span>
<span class="work-card__no">Work 04</span>
<span class="work-card__title">In the Studio — Behind the Scenes</span>
</div>
</div>
</a>
<a class="work-card" href="#contact">
<div class="work-card__frame">
<div class="work-card__img"><img src="assets/work-5-photo.jpg" alt="Black and white portrait of a ceramic artist in a sunlit studio."></div>
<div class="work-card__scrim"></div>
<div class="work-card__meta">
<span class="work-card__view">View</span>
<span class="work-card__no">Work 05</span>
<span class="work-card__title">Human Stories — Documentary Portraits</span>
</div>
</div>
</a>
</div>
</div>
</section>
<!-- ============ 08 · SEE ALL MY WORK (pinned) ============ -->
<section class="samw">
<div class="samw__stack">
<div class="samw__row" data-samw="1"><a href="#work"><span>See all my work</span><span aria-hidden="true">See all my work</span></a></div>
<div class="samw__row" data-samw="-1"><a href="#work"><span>See all my work</span><span aria-hidden="true">See all my work</span></a></div>
<div class="samw__row" data-samw="1"><a href="#work"><span>See all my work</span><span aria-hidden="true">See all my work</span></a></div>
<div class="samw__row" data-samw="-1"><a href="#work"><span>See all my work</span><span aria-hidden="true">See all my work</span></a></div>
</div>
</section>
<div class="vanish"></div>
<div class="vanish"></div>
<div class="vanish"></div>
<div class="vanish"></div>
<!-- ============ 09 · CLIENTS ============ -->
<section class="clients">
<div class="clients__title"><span class="line__mask"><span class="line__in">My Focus</span></span></div>
<div class="clients__list" data-clients></div>
</section>
<!-- ============ 10 · NEWSROOM ============ -->
<section class="news" id="newsroom">
<div class="news__inner">
<header class="news__head">
<h2>Field Notes</h2>
<a class="news__more" href="#newsroom">View more</a>
</header>
<div class="news__grid">
<a class="news-card" href="#newsroom">
<div class="news-card__img"><img src="assets/news-1-photo.jpg" alt="Window-lit portrait with warm highlights and deep shadows."></div>
<div class="news-card__body">
<span class="news-card__view">View</span>
<h3>Finding a Portrait in Natural Light</h3>
</div>
</a>
<a class="news-card" href="#newsroom">
<div class="news-card__img"><img src="assets/news-2-photo.jpg" alt="Photographer and model on a softly lit studio set."></div>
<div class="news-card__body">
<span class="news-card__view">View</span>
<h3>Behind the Scenes of an Editorial Shoot</h3>
</div>
</a>
<a class="news-card" href="#newsroom">
<div class="news-card__img"><img src="assets/news-3-warm.jpg" alt="Photographer and portrait subject reviewing prints in window light."></div>
<div class="news-card__body">
<span class="news-card__view">View</span>
<h3>Choosing the Frames That Tell Your Story</h3>
</div>
</a>
</div>
</div>
</section>
<!-- ============ 11 · WORK WITH ME ============ -->
<section class="wwm" id="work-with-me">
<div class="wwm__big">
<div class="line line--center"><span class="line__mask"><span class="line__in">If</span></span></div>
<div class="line line--center"><span class="line__mask"><span class="line__in">You</span></span></div>
<div class="line line--center"><span class="line__mask"><span class="line__in">Are</span></span></div>
<div class="line line--center"><span class="line__mask"><span class="line__in">Interested</span></span></div>
<div class="line line--center"><span class="line__mask"><span class="line__in">In</span></span></div>
<div class="line line--center"><span class="line__mask"><span class="line__in">Working</span></span></div>
<div class="line line--center"><span class="line__mask"><span class="line__in">With</span></span></div>
<div class="line line--center"><span class="line__mask"><span class="line__in">Me</span></span></div>
</div>
<div class="wwm__small">
<div class="line line--center line--sm"><span class="line__mask"><span class="line__in c-magenta">Reach</span></span><span class="line__mask"><span class="line__in c-magenta">Out</span></span></div>
<div class="line line--center line--sm"><span class="line__mask"><span class="line__in c-magenta">And</span></span></div>
<div class="line line--center line--sm"><span class="line__mask"><span class="line__in c-magenta">Let's</span></span><span class="line__mask"><span class="line__in c-magenta">Start</span></span></div>
<div class="line line--center line--sm"><span class="line__mask"><span class="line__in c-magenta">Planning</span></span></div>
<div class="line line--center line--sm"><span class="line__mask"><span class="line__in c-magenta">Your</span></span></div>
<div class="line line--center line--sm"><span class="line__mask"><span class="line__in c-magenta">Shoot</span></span></div>
<div class="line line--center line--sm"><span class="line__mask"><span class="line__in c-magenta">Together</span></span></div>
</div>
</section>
<!-- ============ 12 · CONTACT ============ -->
<section class="contact" id="contact">
<div class="contact__inner">
<div class="contact__lede">
<p class="split" data-split>I'm available for portraits, editorials, and commercial shoots — in New York and wherever your story takes us.</p>
</div>
<div class="contact__right">
<figure class="contact__photo" data-contact-photo>
<img src="assets/contact-photo.jpg" alt="Camera and photographic prints in warm window light.">
</figure>
<div class="contact__group">
<div class="contact__heading"><span class="line__mask"><span class="line__in">Book a shoot</span></span></div>
<form class="contact__form" data-form novalidate>
<label>
<span class="contact__caption">Your Name</span>
<span class="contact__field"><input type="text" name="name" required></span>
</label>
<label>
<span class="contact__caption">Your Email</span>
<span class="contact__field"><input type="email" name="email" required></span>
</label>
<label>
<span class="contact__caption">Tell me about your shoot</span>
<span class="contact__field contact__field--area"><textarea name="message" rows="3"></textarea></span>
</label>
<button class="contact__submit" type="submit">Send message</button>
<p class="contact__status" role="status" aria-live="polite"></p>
</form>
</div>
</div>
</div>
</section>
<!-- ============ 13 · NEWSLETTER ============ -->
<section class="newsletter">
<h2 class="newsletter__title" data-fade>Subscribe to my newsletter</h2>
<div class="newsletter__body" data-fade>
<form class="newsletter__form" data-form novalidate>
<label class="newsletter__field"><input type="email" name="email" placeholder="Your email" aria-label="Your email" required></label>
<button type="submit">Submit</button>
</form>
<p class="newsletter__fine">By submitting, you agree with <a href="#">Privacy Policy.</a></p>
<p class="newsletter__status" role="status" aria-live="polite"></p>
</div>
</section>
<!-- ============ 14 · THANK YOU ============ -->
<section class="thanks" id="thank-you">
<div class="thanks__inner">
<div class="thanks__line" data-thanks>Thank You</div>
<div class="thanks__line" data-thanks>You Are Awesome</div>
</div>
</section>
<footer class="foot">
<span>© <span data-year></span> Rodo Dendron</span>
<span>New York · Available for commissions</span>
</footer>
</main>
<script src="js/main.js?v=responsive-5"></script>
</body>
</html>
```
## `css/style.css`
```css
/* =========================================================================
Rodo Dendron — design tokens
Every colour, rhythm value and easing curve the page uses lives here.
========================================================================= */
:root{
--black:#000;
--white:#fff;
--magenta:#a85f48; /* "Rodo", see-all-my-work, submit button */
--lime:#73714f; /* Warm olive profession accent */
--cyan:#947254; /* Muted bronze location accent */
--grey:#141414; /* newsroom band */
--muted:#6b6b6b;
--gutter:20px; /* page inset, used everywhere */
--pin:100vh; /* height of a pinned scene */
--display:'Syne','Arial Black',Impact,sans-serif;
--text:'Inter Tight','Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
--mono:'Space Mono',ui-monospace,SFMono-Regular,Menlo,monospace;
/* Fluid display sizes. Syne ExtraBold sets ~1.63x wider than Anton at the
same size, so every one of these is retuned so the longest string in its
slot still fits its measure. Re-check these if you change the display face:
the binding constraints are "experiences." (--fs-line, 1385px column),
"Contact me" (--fs-contact, 693px column) and "Newsroom" (--fs-nav, 748px). */
--fs-line:7.5vw; /* 108px @1440 — "experiences." lands at 1369/1385 */
--fs-line-sm:3.75vw; /* 54px @1440 */
--fs-nav:4.5vw; /* 65px @1440 — "Newsroom" at 695/748 */
--fs-word:5.5vw; /* 79px @1440 — text-reveal words */
--fs-contact:4vw; /* 58px @1440 — "Contact me" at 663/693 */
--ease:cubic-bezier(.16,1,.3,1);
--ease-io:cubic-bezier(.65,.05,.36,1);
--dur:.9s;
}
/* ============================ reset ============================ */
/* While JS drives the scroll position, CSS must not also try to animate it. */
html.is-smooth{scroll-behavior:auto !important}
*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
margin:0;background:var(--white);color:var(--black);
font-family:var(--text);font-optical-sizing:auto;
overflow-x:hidden;
}
img,video{display:block;max-width:100%;height:auto}
a{color:inherit;text-decoration:none}
button,input,textarea{font:inherit;color:inherit}
h1,h2,h3,p,figure{margin:0}
main{position:relative;z-index:1;background:var(--white)}
.c-magenta{color:var(--magenta)}
.c-lime{color:var(--lime)}
.c-cyan{color:var(--cyan)}
.c-outline{
color:transparent;
-webkit-text-stroke:2px var(--black);
paint-order:stroke fill;
}
/* ============================ fixed chrome ============================ */
.chrome-logo,.chrome-menu{
position:fixed;z-index:60;mix-blend-mode:difference;color:var(--white);
opacity:0;pointer-events:none;transition:opacity .45s var(--ease);
}
.is-chrome .chrome-logo,.is-chrome .chrome-menu{opacity:1;pointer-events:auto}
.chrome-logo{
top:var(--gutter);left:var(--gutter);
display:flex;flex-direction:column;line-height:.92;
font-weight:700;font-size:23px;letter-spacing:-.05em;
}
.chrome-menu{
top:25px;right:var(--gutter);
background:none;border:0;padding:0;cursor:pointer;
font-weight:500;font-size:24px;letter-spacing:-.02em;line-height:1;
}
/* full-screen menu */
.menu-overlay{
position:fixed;inset:0;z-index:70;background:var(--black);
display:flex;align-items:center;justify-content:center;
clip-path:inset(0 0 100% 0);transition:clip-path .7s var(--ease);
}
.menu-overlay[hidden]{display:flex} /* keep it animatable */
.menu-overlay:not(.is-open){pointer-events:none;visibility:hidden}
.menu-overlay.is-open{clip-path:inset(0 0 0 0);visibility:visible}
.menu-overlay__nav{display:flex;flex-direction:column;gap:5px}
.menu-overlay__nav a{
position:relative;display:block;width:max-content;
height:var(--fs-nav);overflow:clip;
font-family:var(--display);font-weight:800;font-size:var(--fs-nav);line-height:1;
text-transform:uppercase;letter-spacing:-.02em;color:var(--white);
}
.menu-overlay__nav a span{display:block;transition:transform .5s var(--ease)}
/* the swap copy waits one line below, not on top of the original */
.menu-overlay__nav a span:last-child{
position:absolute;top:100%;left:0;color:transparent;
-webkit-text-stroke:.5px var(--white);
}
.menu-overlay__nav a:hover span{transform:translateY(-100%)}
/* ============================ 01 · HERO ============================ */
.hero{
position:relative;height:200vh;background:var(--white);overflow:clip;
display:flex;flex-direction:column;align-items:center;
}
/* The wordmark is SVG text with textLength, so it locks to the exact
908:114 proportion of the reference lockup at every viewport width —
something font-size alone cannot guarantee across font fallbacks. */
.hero__wordmark{
position:absolute;top:40px;left:40px;right:40px;z-index:3;
width:calc(100% - 80px);height:auto;display:block;
mix-blend-mode:overlay;pointer-events:none;overflow:visible;
}
.hero__wordmark text{
font-family:var(--text);font-weight:700;font-size:146px;
fill:var(--white);
}
.hero__pin{
position:sticky;top:0;z-index:1;
width:100%;height:var(--pin);
padding:var(--gutter) var(--gutter) 0;
display:flex;align-items:center;justify-content:center;overflow:clip;
}
.hero__bg{position:absolute;inset:0;background:var(--black);z-index:0}
.hero__media{
position:relative;z-index:1;
width:100%;height:calc(var(--pin) - var(--gutter));
transform-origin:30% 100%;
transform:perspective(1200px);
will-change:transform;
}
.hero__media img{width:100%;height:100%;object-fit:cover}
.hero__front{
position:absolute;top:0;left:0;width:100%;height:var(--pin);z-index:2;
display:flex;flex-direction:column;justify-content:flex-end;pointer-events:none;
}
.hero__panel{
position:relative;width:100%;height:470px;background:var(--black);
padding:80px var(--gutter) var(--gutter);
display:flex;align-items:flex-end;gap:var(--gutter);pointer-events:auto;
}
.hero__panel-left{flex:1;display:flex;align-items:flex-end}
.hero__logo{
display:flex;flex-direction:column;line-height:.9;color:var(--white);
font-weight:700;font-size:40px;letter-spacing:-.05em;
}
.hero__nav{
flex:1;display:flex;flex-direction:column;gap:5px;
mix-blend-mode:difference;
}
.hero__nav a{
position:relative;display:block;height:var(--fs-nav);overflow:clip;
font-family:var(--display);font-weight:800;font-size:var(--fs-nav);line-height:1;
letter-spacing:-.02em;text-transform:uppercase;color:var(--white);
width:max-content;
}
.hero__nav a span{display:block;transition:transform .5s var(--ease)}
.hero__nav a span:last-child{
position:absolute;top:100%;left:0;
color:transparent;-webkit-text-stroke:.5px var(--white);
}
.hero__nav a:hover span{transform:translateY(-100%)}
.hero__tail{position:relative;width:100%;height:var(--pin)}
/* ============================ line primitives ============================ */
/* Every big headline line is a clipped mask with the type sliding up into it. */
.line,.rule-line{
display:flex;align-items:flex-end;
height:var(--fs-line);width:100%;
}
.line--row{justify-content:space-between}
.line--center{justify-content:center}
.line--sm{height:var(--fs-line-sm)}
.line__pair{display:flex;align-items:flex-end;gap:50px}
.line__mask{display:block;overflow:clip;height:100%}
.line--sm .line__mask{height:100%}
.line__in{
display:block;
font-family:var(--display);font-weight:800;font-size:var(--fs-line);line-height:1;
text-transform:uppercase;white-space:pre;
transform:translateY(100%);
transition:transform 1s var(--ease);
}
.line--sm .line__in{font-size:var(--fs-line-sm)}
.is-in .line__in,.line.is-in .line__in{transform:translateY(0)}
/* ============================ 02 · INTRO ============================ */
.intro{
position:relative;z-index:1;background:var(--white);
padding:var(--gutter);overflow:clip;
}
.intro__inner{position:relative;width:100%;overflow:clip}
.intro__card{
position:relative;width:35.1vw;max-width:500px;aspect-ratio:500/360;
transform:perspective(1200px);transform-style:preserve-3d;
will-change:transform;
}
.intro__card img{width:100%;height:100%;object-fit:cover}
.intro__rules{position:relative;width:100%;margin-top:0}
.rule-line{position:relative;justify-content:flex-end}
.rule-line--start{justify-content:flex-start}
.rule-line::after{
content:"";position:absolute;left:0;right:0;bottom:0;height:1px;
background:var(--black);transform:scaleX(0);transform-origin:left;
transition:transform 1.1s var(--ease);
}
.rule-line.is-in::after{transform:scaleX(1)}
.rule-line .line__in{color:var(--white);mix-blend-mode:difference}
/* ============================ 03 · MORE ABOUT ME ============================ */
.mam{
position:relative;z-index:0;height:450px;background:var(--black);
display:flex;align-items:center;justify-content:center;overflow:clip;
}
.mam__sweep{
position:absolute;inset:0;background:var(--white);
transform:translateX(-100%);will-change:transform;
}
.mam__link{
position:relative;z-index:1;display:flex;align-items:center;gap:20px;
mix-blend-mode:difference;color:var(--white);
font-family:var(--display);font-weight:800;font-size:var(--fs-nav);line-height:1;
text-transform:uppercase;perspective:600px;
}
.mam__w{
display:block;transform-origin:50% 50%;
transition:transform .8s var(--ease),opacity .8s var(--ease);
}
.mam__w--me{display:flex;align-items:center;gap:5px}
.mam__w--me svg{width:.95em;height:.95em}
.mam [data-flip="x"]{transform:rotateX(90deg);opacity:0}
.mam [data-flip="x-rev"]{transform:rotateX(-90deg);opacity:0}
.mam [data-flip="y"]{transform:rotateY(90deg);opacity:0}
.mam.is-in [data-flip]{transform:none;opacity:1}
.mam.is-in [data-flip="x-rev"]{transition-delay:.1s}
.mam.is-in [data-flip="y"]{transition-delay:.2s}
/* ============================ 04/05 · GALLERIES ============================ */
.gallery{
position:relative;background:var(--white);
padding:100px 0;overflow:clip;
}
.gallery--flush{padding:100px 0 0;z-index:2}
.gallery__row{
display:flex;justify-content:center;align-items:center;
gap:var(--gutter);padding:var(--gutter);
max-width:1440px;margin:0 auto;
}
.gallery__cell{
position:relative;flex:1;max-width:440px;aspect-ratio:440/600;
overflow:clip;will-change:transform;
}
.gallery__cell img{width:100%;height:100%;object-fit:cover}
/* ============================ 05b · TEXT REVEAL ============================ */
.reveal-sec{
position:relative;background:var(--white);
display:flex;flex-direction:column;align-items:center;overflow:clip;
}
.reveal-sec__track{position:relative;width:100%;display:flex;flex-direction:column;align-items:center;overflow:clip}
/* The full-width panel stays put and simply inverts the page beneath it. */
.reveal-card{
position:sticky;top:0;z-index:2;
width:100%;height:var(--pin);
display:flex;align-items:center;justify-content:center;
background:var(--white);mix-blend-mode:difference;
}
/* The type block is the thing that swings away — hinged on its left edge,
which is why the reference sets transform-origin to 0 50%. A second
difference layer keeps the words readable inside the inverted panel. */
.reveal-card__invert{
width:395px;max-width:calc(100% - 40px);height:640px;
display:flex;align-items:center;justify-content:center;
mix-blend-mode:difference;
transform-origin:0 50%;
transform:perspective(1200px);
will-change:transform;
}
.reveal-card__inner{
display:flex;flex-direction:column;align-items:flex-start;justify-content:center;
width:355px;max-width:100%;height:600px;padding:20px;
opacity:0;transition:opacity .8s var(--ease);
}
.reveal-card.is-in .reveal-card__inner{opacity:1}
.reveal-word{
position:relative;display:block;width:100%;height:100px;
font-family:var(--text);font-weight:800;
font-size:var(--fs-word);line-height:100px;letter-spacing:-.03em;
color:var(--white);white-space:pre;
transform:translateX(var(--from,0));
transition:transform 1.1s var(--ease);
will-change:transform;
}
.reveal-card.is-in .reveal-word{transform:translateX(0)}
.reveal-sec__tail{width:100%;height:var(--pin)}
/* ============================ 06 · SHORT ABOUT ============================ */
.about{position:relative;z-index:1;background:var(--black);color:var(--white)}
.about__grid{
display:grid;grid-template-columns:1fr 1fr;gap:10px;
padding:var(--gutter);min-height:var(--pin);align-items:center;
max-width:1440px;margin:0 auto;
}
.about__media{display:flex;align-items:center;height:100%}
.about__media-mask{
position:relative;width:min(550px,80%);aspect-ratio:550/860;overflow:hidden;
}
.about__media-mask img{
position:absolute;top:-20%;left:0;width:100%;height:140%;object-fit:cover;
will-change:transform;
}
.about__copy{
display:grid;grid-template-columns:1fr 1fr;gap:10px;
align-content:space-between;height:100%;padding:60px 0;
}
.about__copy p{
font-weight:600;font-size:24px;line-height:1.2;letter-spacing:-.03em;
max-width:340px;
}
.about__copy p:nth-of-type(2),.about__copy p:nth-of-type(3){align-self:end}
/* per-character reveal */
.split .ch{
display:inline-block;white-space:pre;
opacity:.001;transform:translateY(80px);
transition:opacity .6s var(--ease),transform .8s var(--ease);
}
.split.is-in .ch{opacity:1;transform:none}
/* ============================ 07 · WORK ============================ */
.work{position:relative;z-index:1;background:var(--white);padding:100px 0 0}
.work__grid{
display:grid;grid-template-columns:1fr 2fr;gap:0 var(--gutter);
padding:var(--gutter);max-width:1440px;margin:0 auto;
}
.work__label{position:sticky;top:120px;align-self:start;height:450px}
.work__label h2{
font-family:var(--display);font-weight:800;font-size:var(--fs-nav);line-height:1;
text-transform:uppercase;perspective:500px;
}
.flipchars .ch{
display:inline-block;white-space:pre;transform-origin:50% 50%;
opacity:.001;transform:rotateX(-90deg) scaleX(.5);
transition:opacity .5s var(--ease),transform .7s var(--ease);
}
.flipchars.is-in .ch{opacity:1;transform:none}
.work__list{display:flex;flex-direction:column;gap:var(--gutter);overflow:clip}
.work-card{display:block;height:450px;display:flex;align-items:center;justify-content:flex-end}
.work-card__frame{
position:relative;width:100%;aspect-ratio:917/450;overflow:clip;
transform:scale(.5);transform-origin:100% 50%;
transition:transform .7s var(--ease);
will-change:transform;
}
.work-card:hover .work-card__frame,.work-card:focus-visible .work-card__frame{transform:scale(1)}
.work-card__img{position:absolute;inset:0;overflow:clip}
.work-card__img img{
width:100%;height:100%;object-fit:cover;
transform:scale(1.1);transition:transform 1s var(--ease);
}
.work-card:hover .work-card__img img{transform:scale(1)}
.work-card__scrim{
position:absolute;inset:0;z-index:1;
background:linear-gradient(rgba(84,84,84,0) 0%,rgba(0,0,0,.8) 100%);
}
.work-card__meta{
position:absolute;left:0;bottom:0;z-index:2;
display:flex;flex-direction:column;align-items:flex-start;gap:5px;
padding:40px;color:var(--white);
}
.work-card__meta>*{opacity:0;transform:translateY(12px);transition:opacity .5s var(--ease),transform .6s var(--ease)}
.work-card:hover .work-card__meta>*{opacity:1;transform:none}
.work-card:hover .work-card__no{transition-delay:.06s}
.work-card:hover .work-card__title{transition-delay:.12s}
.work-card__view{font-family:var(--mono);font-size:12px;letter-spacing:.08em;text-transform:uppercase}
.work-card__no{font-family:var(--display);font-weight:800;font-size:32px;line-height:1;text-transform:uppercase}
.work-card__title{font-weight:500;font-size:16px;letter-spacing:-.02em}
.work-card__no{font-family:var(--mono);font-weight:700;letter-spacing:.02em}
/* ============================ 08 · SEE ALL MY WORK ============================ */
.samw{
position:sticky;top:0;z-index:1;height:var(--pin);background:var(--black);
display:flex;align-items:center;justify-content:center;overflow:clip;
}
.samw__stack{display:flex;flex-direction:column;align-items:center}
.samw__row{
opacity:0;transform:translateX(calc(var(--dir,1) * 70vw));
transition:opacity .8s var(--ease),transform 1s var(--ease);
}
.samw.is-in .samw__row{opacity:1;transform:none}
.samw.is-in .samw__row:nth-child(2){transition-delay:.08s}
.samw.is-in .samw__row:nth-child(3){transition-delay:.16s}
.samw.is-in .samw__row:nth-child(4){transition-delay:.24s}
.samw__row a,.client-row a{
position:relative;display:block;height:var(--fs-nav);
font-family:var(--display);font-weight:800;font-size:var(--fs-nav);line-height:1;
text-transform:uppercase;transform-style:preserve-3d;
transition:transform .55s var(--ease);
}
.samw__row a{color:var(--magenta)}
.samw__row a span:last-child,.client-row a span:last-child{
position:absolute;inset:0;
transform:rotateX(180deg);backface-visibility:hidden;
}
.samw__row a span:first-child,.client-row a span:first-child{
display:block;backface-visibility:hidden;
}
.samw__row a span:last-child{color:var(--white)}
.samw__row a:hover,.client-row a:hover{transform:rotateX(180deg)}
.vanish{position:relative;z-index:0;height:140px}
/* ============================ 09 · CLIENTS ============================ */
.clients{
position:relative;z-index:1;background:var(--white);
padding:200px var(--gutter) 100px;
display:flex;flex-direction:column;align-items:center;gap:150px;overflow:hidden;
}
.clients__title{
width:100%;max-width:1440px;display:flex;justify-content:center;
height:var(--fs-line);overflow:clip;
}
.clients__title .line__in{font-size:var(--fs-line)}
.clients__list{width:100%;max-width:1440px;display:flex;flex-direction:column;align-items:center}
.client-row{
width:100%;display:flex;justify-content:center;
opacity:0;transform:translateX(calc(var(--dir,1) * 70vw));
transition:opacity .7s var(--ease),transform .9s var(--ease);
}
.client-row.is-in{opacity:1;transform:none}
.client-row a{color:var(--black);width:max-content}
.client-row a span:last-child{color:var(--black)}
/* ============================ 10 · NEWSROOM ============================ */
.news{position:relative;z-index:1;background:var(--grey);padding:100px 0;overflow:clip}
.news__inner{
display:grid;gap:50px var(--gutter);padding:var(--gutter);
max-width:1440px;margin:0 auto;
}
.news__head{display:flex;flex-direction:column;align-items:flex-start;gap:0}
.news__head h2{
font-family:var(--display);font-weight:800;font-size:var(--fs-nav);line-height:1;
text-transform:uppercase;color:var(--white);
}
.news__more{
font-weight:700;font-size:30px;line-height:1.2;letter-spacing:-.03em;color:var(--white);
border-bottom:2px solid transparent;transition:border-color .3s var(--ease);
}
.news__more:hover{border-color:var(--white)}
.news__grid{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--gutter)}
.news-card{
position:relative;aspect-ratio:448/500;background:var(--black);
overflow:clip;display:block;
}
.news-card__img{position:absolute;inset:0;overflow:clip}
.news-card__img img{
width:100%;height:100%;object-fit:cover;
transform:scale(1.04);transition:transform 1s var(--ease),opacity .6s var(--ease);
}
.news-card:hover .news-card__img img{transform:scale(1.12);opacity:.55}
.news-card__body::before{
content:"";position:absolute;inset:0;z-index:-1;
background:linear-gradient(rgba(0,0,0,.15) 0%,rgba(0,0,0,.05) 45%,rgba(0,0,0,.85) 100%);
}
.news-card__body{
position:absolute;inset:0;z-index:2;isolation:isolate;
display:flex;flex-direction:column;align-items:flex-start;gap:10px;
padding:var(--gutter);color:var(--white);
justify-content:flex-start;
}
.news-card__view{
font-family:var(--mono);font-size:12px;letter-spacing:.08em;text-transform:uppercase;
opacity:0;transform:translateY(-8px);transition:opacity .4s var(--ease),transform .5s var(--ease);
}
.news-card:hover .news-card__view{opacity:1;transform:none}
.news-card h3{
margin-top:auto;font-weight:600;font-size:24px;line-height:1.2;letter-spacing:-.03em;
}
/* ============================ 11 · WORK WITH ME ============================ */
.wwm{
position:relative;z-index:1;background:var(--white);
padding:200px var(--gutter);overflow:hidden;
display:flex;flex-direction:column;align-items:center;
}
.wwm__big,.wwm__small{width:100%;max-width:1440px;overflow:clip}
.wwm__small{margin-top:0}
/* ============================ 12 · CONTACT ============================ */
.contact{position:relative;z-index:1;background:var(--white);padding:100px 0;overflow:clip}
.contact__inner{
display:grid;grid-template-columns:1fr 1fr;gap:0;
padding:var(--gutter);max-width:1440px;margin:0 auto;align-items:start;
min-height:700px;
}
.contact__lede p{
max-width:500px;font-weight:600;font-size:30px;line-height:1.2;letter-spacing:-.03em;
}
.contact__right{
position:relative;display:flex;flex-direction:column;gap:20px;
height:660px;overflow:clip;
}
.contact__group{display:flex;flex-direction:column;justify-content:center;gap:50px;flex:1}
.contact__heading{height:var(--fs-contact);overflow:clip}
.contact__heading .line__in{font-size:var(--fs-contact)}
.contact__form{
display:flex;flex-direction:column;gap:20px;
opacity:0;transform:scale(.95);
transition:opacity .7s var(--ease),transform .8s var(--ease);
}
.contact__form.is-in{opacity:1;transform:none}
.contact__form label{position:relative;display:flex;flex-direction:column;align-items:flex-end}
.contact__caption{
position:absolute;top:11px;right:5px;z-index:1;
font-family:var(--mono);font-size:11px;letter-spacing:.06em;text-transform:uppercase;
line-height:1.2;color:var(--muted);pointer-events:none;
transition:opacity .25s var(--ease);
}
label:focus-within .contact__caption{opacity:0}
.contact__field{
display:flex;align-items:center;width:100%;padding:5px;
border-bottom:1px solid var(--black);overflow:hidden;
}
.contact__field--area{align-items:stretch}
.contact__field input,.contact__field textarea{
width:100%;background:none;border:0;outline:none;
font-size:16px;letter-spacing:-.01em;resize:none;
}
.contact__field input::placeholder,.contact__field textarea::placeholder{color:#b5b5b5}
.contact__field:focus-within+.contact__caption,
label:focus-within .contact__caption{opacity:0}
.contact__submit{
width:100%;padding:10px;background:var(--magenta);color:var(--white);border:0;
font-family:var(--display);font-weight:800;font-size:20px;letter-spacing:.04em;text-transform:uppercase;
cursor:pointer;transition:background .3s var(--ease);
}
.contact__submit:hover{background:var(--black)}
.contact__status,.newsletter__status{font-size:13px;color:var(--muted);min-height:16px}
.contact__photo{
position:relative;width:100%;height:200px;flex:0 0 200px;overflow:clip;
opacity:0;transform:translateY(-200px);
transition:opacity .8s var(--ease),transform 1s var(--ease);
}
.contact__photo.is-in{opacity:1;transform:none}
.contact__photo img{width:100%;height:400px;object-fit:cover;object-position:50% 35%}
/* ============================ 13 · NEWSLETTER ============================ */
.newsletter{
position:relative;z-index:1;background:var(--black);color:var(--white);
padding:100px var(--gutter);min-height:400px;
display:flex;flex-direction:column;align-items:center;justify-content:center;gap:20px;
}
.newsletter__title{
font-family:var(--display);font-weight:800;font-size:40px;line-height:1.1;text-align:center;
text-transform:uppercase;
}
.newsletter [data-fade]{opacity:0;transform:translateY(20px);transition:opacity .7s var(--ease),transform .8s var(--ease)}
.newsletter [data-fade].is-in{opacity:1;transform:none}
.newsletter__body{display:flex;flex-direction:column;align-items:center;gap:10px;width:min(452px,100%)}
.newsletter__form{display:flex;align-items:flex-end;gap:10px;width:100%}
.newsletter__field{
flex:1;display:flex;align-items:center;padding:5px;
border-bottom:1px solid var(--white);
}
.newsletter__field input{width:100%;background:none;border:0;outline:none;font-size:16px;color:var(--white)}
.newsletter__field input::placeholder{color:#7a7a7a}
.newsletter__form button{
width:140px;height:40px;background:var(--white);color:var(--black);border:0;cursor:pointer;
font-family:var(--display);font-weight:800;font-size:18px;letter-spacing:.04em;text-transform:uppercase;
transition:background .3s var(--ease),color .3s var(--ease);
}
.newsletter__form button:hover{background:var(--magenta);color:var(--white)}
.newsletter__fine{font-family:var(--mono);font-size:11px;letter-spacing:.04em;line-height:1.3;color:var(--muted);align-self:flex-start}
.newsletter__fine a{text-decoration:underline}
/* ============================ 14 · THANK YOU ============================ */
.thanks{
position:relative;z-index:1;background:var(--black);
padding:200px var(--gutter);overflow:clip;
display:flex;flex-direction:column;align-items:center;
}
.thanks__inner{
display:flex;flex-direction:column;align-items:center;gap:10px;
mix-blend-mode:difference;perspective:1200px;
}
.thanks__line{
font-family:var(--display);font-weight:800;font-size:var(--fs-nav);line-height:1;
text-transform:uppercase;color:var(--white);
transform:rotateX(180deg);opacity:0;transform-origin:50% 0;
transition:transform .9s var(--ease),opacity .9s var(--ease);
}
.thanks__line.is-in{transform:none;opacity:1}
.thanks__line:nth-child(2){transition-delay:.12s}
.foot{
background:var(--black);color:var(--muted);
padding:0 var(--gutter) 40px;
display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap;
font-family:var(--mono);font-size:11px;letter-spacing:.06em;text-transform:uppercase;
}
/* ============================ responsive ============================ */
@media (max-width:1100px){
.about__copy{grid-template-columns:1fr}
.work__grid{grid-template-columns:1fr}
.work__label{position:static;height:auto;margin-bottom:40px}
.news__grid{grid-template-columns:repeat(2,1fr)}
.contact__right{padding-top:120px}
}
@media (max-width:767px){
:root{
--gutter:16px;
--fs-line:clamp(22px,7.2vw,60px);
--fs-line-sm:clamp(17px,5vw,40px);
--fs-nav:clamp(18px,5.4vw,44px); /* "Oneeightoneone" is the binding string */
--fs-word:clamp(22px,6.6vw,48px);
--fs-contact:clamp(20px,6vw,44px);
}
.hero__wordmark{top:24px;left:16px;right:16px;width:calc(100% - 32px)}
.hero__panel{height:52vh;padding:28px var(--gutter) var(--gutter);flex-direction:column;align-items:flex-start;justify-content:flex-end;gap:18px}
.hero__panel-left{flex:0 0 auto;order:2}
.hero__nav{flex:0 0 auto;order:1;width:100%}
.hero__logo{font-size:26px}
.intro__card{width:46vw;margin-bottom:0}
.line--row{align-items:flex-end;gap:12px}
.line__pair{gap:16px}
.c-outline{-webkit-text-stroke-width:1.2px}
.gallery__row{gap:10px;padding:10px}
.reveal-card__invert{height:auto;max-width:calc(100% - 32px)}
.reveal-card__inner{width:100%;height:auto;padding:0}
.reveal-word{height:auto;line-height:1.05;margin-bottom:2px}
.about__grid{grid-template-columns:1fr;gap:40px;padding:60px var(--gutter)}
.about__media{justify-content:center}
.about__media-mask{width:min(420px,100%)}
.about__copy{padding:0;gap:28px}
.about__copy p{font-size:18px;max-width:none}
.work-card{height:auto}
.work-card__frame{transform:none}
.work-card__meta{padding:20px}
.work-card__meta>*{opacity:1;transform:none}
.work-card__no{font-size:24px}
.news__grid{grid-template-columns:1fr}
.news__more{font-size:20px}
.news-card h3{font-size:19px}
.clients{padding:100px var(--gutter) 60px;gap:60px}
.wwm{padding:100px var(--gutter)}
.wwm__small{margin-top:40px}
.contact__inner{grid-template-columns:1fr;gap:40px;min-height:0}
.contact__right{height:auto;gap:24px}
.contact__group{gap:24px}
.contact__lede p{font-size:20px}
.newsletter__title{font-size:28px}
.newsletter__form{flex-direction:column;align-items:stretch}
.newsletter__form button{width:100%}
.foot{flex-direction:column;gap:8px}
.thanks{padding:100px var(--gutter)}
}
/* ============================ reduced motion ============================ */
@media (prefers-reduced-motion:reduce){
*,*::before,*::after{
animation-duration:.001ms !important;animation-iteration-count:1 !important;
transition-duration:.001ms !important;scroll-behavior:auto !important;
}
.line__in,.reveal-word,.samw__row,.client-row,.contact__photo,.contact__form,
.newsletter [data-fade],.thanks__line,.split .ch,.flipchars .ch{
transform:none !important;opacity:1 !important;
}
.rule-line::after{transform:scaleX(1) !important}
.mam [data-flip]{transform:none !important;opacity:1 !important}
.mam__sweep{display:none}
.hero__media{transform:perspective(1200px) !important}
}
/* Fit the profession into the animated introductory line at every width. */
.line__in--photographer{font-size:min(var(--fs-line),10.1vw)}
/* Responsive portfolio typography and image rows. */
.line__in--photographer{font-size:min(var(--fs-line),5.7vw)}
.intro .line--row{height:auto;min-height:var(--fs-line);gap:24px;padding-block:20px}
.intro .line--row>.line__mask,.intro .line__pair .line__mask{height:auto}
.intro .line__pair{gap:2vw;flex-wrap:wrap}
.intro__card{flex-shrink:0;margin:0}
@media(max-width:767px){
.intro .line--row{display:flex;flex-wrap:wrap;align-items:center;gap:20px;padding-block:20px}
.intro .line--row>.line__mask,.intro .line__pair{width:100%}
.intro__card{width:70%;margin-left:auto}
.gallery__row{gap:12px;flex-wrap:wrap}
.gallery__cell{flex:1 1 calc(50% - 12px);max-width:calc(50% - 6px)}
.gallery__cell:last-child{margin-inline:auto}
.work-card__title{font-size:clamp(16px,4vw,24px);line-height:1.2}
.work-card__meta{padding:14px}
.contact__field input,.contact__field textarea,.newsletter__field input{font-size:16px}
}
@media(max-width:480px){
.newsletter__title{font-size:clamp(21px,6.3vw,28px);max-width:100%;overflow-wrap:anywhere}
.mam__link{gap:10px;font-size:clamp(22px,6.5vw,30px)}
.work-card__no{font-size:18px}
.work-card__meta{gap:8px}
}
@media(max-height:500px) and (orientation:landscape){
.hero__panel{height:auto;min-height:90svh}
.menu-overlay__nav{gap:6px}
.menu-overlay__nav a{font-size:clamp(20px,7svh,36px)}
}
```
## `js/main.js`
```js
/* =========================================================================
Rodo Dendron — one IIFE, numbered to match the stylesheet.
Three systems:
(A) SMOOTH SCROLL — the wheel drives a virtual target; the real scroll
position eases toward it each frame. This is what gives the page its
glide, and every scrubbed scene inherits that easing for free.
(B) SCRUB — values driven continuously by scroll position. Everything that
scrubs registers a scene {el, measure(), apply(progress)}. Scene offsets
are cached and re-measured on resize, on font load and on any document
height change, which is the classic desync bug in this pattern.
(C) ENTER — one-shot reveals that add a CSS class. Trigger positions are
precomputed at measure time, so the scroll loop never reads layout.
The hot loop only WRITES style. Every layout READ happens in measure().
Mixing the two per frame forces synchronous reflow and is the usual reason
a page like this feels sticky rather than smooth.
========================================================================= */
(function () {
'use strict';
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var clamp = function (v, a, b) { return v < a ? a : v > b ? b : v; };
var lerp = function (a, b, t) { return a + (b - a) * t; };
/* ------------------------------------------------------------------ 0
Small helpers
------------------------------------------------------------------ */
function docTop(el) {
var r = el.getBoundingClientRect();
return r.top + window.pageYOffset;
}
/* Piecewise-linear keyframe table: keys([[0,1],[0.4,1.4],[1,0.5]], p) */
function keys(table, p) {
if (p <= table[0][0]) return table[0][1];
for (var i = 1; i < table.length; i++) {
if (p <= table[i][0]) {
var a = table[i - 1], b = table[i];
var t = (p - a[0]) / (b[0] - a[0] || 1);
return lerp(a[1], b[1], t);
}
}
return table[table.length - 1][1];
}
/* ------------------------------------------------------------------ 1
Scrub engine
------------------------------------------------------------------ */
var scenes = [];
var vh = window.innerHeight;
/* The reference is authored at 1440px. Every pixel-valued motion amplitude
below is scaled by this so a 375px phone does not get 1200px of parallax
travel inside a 260px-tall image. */
var scale = 1;
function measureScale() { scale = Math.min(1, window.innerWidth / 1440); }
measureScale();
function addScene(el, measure, apply) {
var scene = { el: el, measure: measure, apply: apply, top: 0, len: 1 };
scenes.push(scene);
return scene;
}
function measureAll() {
vh = window.innerHeight;
measureScale();
for (var i = 0; i < scenes.length; i++) scenes[i].measure(scenes[i]);
measureReveals();
}
/* --------------------------------------------------------------- 1a
SMOOTH SCROLL
The wheel is intercepted and fed into `target`; the real scroll position
eases toward it with a frame-rate-independent exponential lerp, matching
the ~180ms half-life measured off the reference. Because it moves the
REAL scroll position, native `position: sticky`, anchors, find-in-page
and the scrollbar all keep working.
Off for touch (native momentum is better) and for reduced-motion.
--------------------------------------------------------------- */
var locked = false; /* true while the menu overlay is open */
var isTouch = window.matchMedia('(hover: none) and (pointer: coarse)').matches;
var smooth = !reduced && !isTouch;
var LERP = 0.062; /* per 60fps frame; ≈180ms half-life */
var target = window.pageYOffset;
var current = target;
var running = false;
var selfScroll = false;
var lastT = 0;
function maxScroll() {
return Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
}
function onWheel(e) {
if (!smooth || locked || e.ctrlKey) return; /* ctrl+wheel is zoom */
if (e.target && e.target.closest && e.target.closest('[data-native-scroll]')) return;
e.preventDefault();
var d = e.deltaY;
if (e.deltaMode === 1) d *= 16; /* lines */
else if (e.deltaMode === 2) d *= window.innerHeight; /* pages */
target = clamp(target + d, 0, maxScroll());
start();
}
function start() {
if (running) return;
running = true;
lastT = performance.now();
requestAnimationFrame(tick);
}
function tick(now) {
/* Scroll events are dispatched just BEFORE the next frame's rAF, so the
event caused by last frame's scrollTo has already been handled by the
time we get here. Clearing the guard now — not straight after
scrollTo — is what keeps the loop from cancelling its own glide. */
selfScroll = false;
var dt = Math.min((now - lastT) / 1000, 0.1);
lastT = now;
var a = 1 - Math.pow(1 - LERP, dt * 60); /* frame-rate independent */
current += (target - current) * a;
if (Math.abs(target - current) < 0.4) { current = target; running = false; }
selfScroll = true;
window.scrollTo(0, current);
frame(current);
if (running) requestAnimationFrame(tick);
else setTimeout(function () { selfScroll = false; }, 80);
}
/* Anything that moves the page by other means — scrollbar drag, keyboard,
find-in-page, a browser anchor jump — resyncs the virtual target. */
function onScroll() {
var y = window.pageYOffset;
if (selfScroll || Math.abs(y - current) < 1) { frame(y); return; }
target = current = y;
frame(y);
}
/* One write-only pass. No getBoundingClientRect, no offsetHeight. */
function frame(y) {
if (y === undefined) y = window.pageYOffset;
revealPass(y);
for (var i = 0; i < scenes.length; i++) {
var s = scenes[i];
var p = clamp((y - s.top) / s.len, 0, 1);
s.apply(p, y, s);
}
chromeCheck(y);
}
/* ------------------------------------------------------------------ 2
Enter reveals → CSS class.
Deliberately NOT IntersectionObserver. An observer only reports elements
that actually intersect, so anything the viewport *jumps over* — a deep
#anchor, a restored scroll position, a scrollbar drag — never fires and
stays hidden forever. A position test on each scroll frame has no such
hole, and the pending list drains as elements reveal, so the cost falls
to zero by the end of the page.
------------------------------------------------------------------ */
var pending = [];
function watch(el) {
if (!el) return;
if (reduced) { el.classList.add('is-in'); return; }
pending.push({ el: el, y: 0 });
}
/* Layout read — measure() only, never the scroll loop. */
function measureReveals() {
var trigger = vh * 0.88; /* fires in the lower 12% */
for (var i = 0; i < pending.length; i++) {
pending[i].y = docTop(pending[i].el) - trigger;
}
}
/* Pure arithmetic. The list drains as elements reveal, so this costs
nothing by the end of the page. */
function revealPass(y) {
if (!pending.length) return;
var keep = [];
for (var i = 0; i < pending.length; i++) {
if (y >= pending[i].y) pending[i].el.classList.add('is-in');
else keep.push(pending[i]);
}
pending = keep;
}
/* ------------------------------------------------------------------ 3
Text splitting — characters, preserving word boundaries for wrapping
------------------------------------------------------------------ */
function splitChars(el, stagger) {
var text = el.textContent;
el.textContent = '';
var words = text.split(/(\s+)/);
var idx = 0;
words.forEach(function (w) {
if (/^\s+$/.test(w)) { el.appendChild(document.createTextNode(w)); return; }
var wrap = document.createElement('span');
wrap.style.whiteSpace = 'nowrap';
for (var i = 0; i < w.length; i++) {
var s = document.createElement('span');
s.className = 'ch';
s.textContent = w[i];
s.style.transitionDelay = (idx * stagger) + 'ms';
wrap.appendChild(s);
idx++;
}
el.appendChild(wrap);
});
}
/* ------------------------------------------------------------------ 4
HERO — the pinned scene.
Phase 1 (first viewport of scroll): the media scales 1 → 1.4 → 0.5,
origin 30% 100%, matching the reference's keyframe table.
Phase 2 (second viewport): it holds 0.5 and tumbles away —
translateY 0→-300, rotateX 0→30deg, rotateY 0→10deg.
------------------------------------------------------------------ */
var heroMedia = document.querySelector('[data-hero-media]');
var hero = document.querySelector('.hero');
if (heroMedia && hero && !reduced) {
var SCALE = [[0, 1], [0.2, 1.4], [0.5, 0.5], [1, 0.5]];
addScene(hero,
function (s) { s.top = docTop(hero); s.len = Math.max(1, hero.offsetHeight); },
function (p) {
var sc = keys(SCALE, p);
var q = clamp((p - 0.5) / 0.5, 0, 1);
heroMedia.style.transform =
'perspective(1200px) translateY(' + (-300 * q * scale).toFixed(2) + 'px) scale(' +
sc.toFixed(4) + ') rotateX(' + (30 * q).toFixed(2) + 'deg) rotateY(' +
(10 * q).toFixed(2) + 'deg)';
});
}
/* Slow ken-burns drift on the hero still, standing in for the reference's
looping video clip. */
var kb = document.querySelector('[data-kenburns]');
if (kb && !reduced) {
kb.animate(
[{ transform: 'scale(1.06) translate3d(0,0,0)' },
{ transform: 'scale(1.14) translate3d(-1.5%,-1.5%,0)' },
{ transform: 'scale(1.06) translate3d(0,0,0)' }],
{ duration: 26000, iterations: Infinity, easing: 'ease-in-out' });
}
/* ------------------------------------------------------------------ 5
Fixed chrome — appears once the hero panel has passed.
------------------------------------------------------------------ */
var body = document.body;
var chromeOn = false;
function chromeCheck(y) {
var want = (y === undefined ? window.pageYOffset : y) > vh * 0.85;
if (want !== chromeOn) { chromeOn = want; body.classList.toggle('is-chrome', want); }
}
/* Menu overlay */
var menuBtn = document.querySelector('.chrome-menu');
var overlay = document.getElementById('overlay-menu');
if (menuBtn && overlay) {
overlay.removeAttribute('hidden');
var setOpen = function (open) {
overlay.classList.toggle('is-open', open);
menuBtn.setAttribute('aria-expanded', String(open));
menuBtn.textContent = open ? 'close' : 'menu';
document.documentElement.style.overflow = open ? 'hidden' : '';
locked = open; /* stop the wheel driving the page */
};
menuBtn.addEventListener('click', function () {
setOpen(!overlay.classList.contains('is-open'));
});
overlay.addEventListener('click', function (e) {
if (e.target.closest('a')) setOpen(false);
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && overlay.classList.contains('is-open')) setOpen(false);
});
}
/* ------------------------------------------------------------------ 6
INTRO — headline lines reveal; the two cards tilt with scroll.
------------------------------------------------------------------ */
document.querySelectorAll('.line, .rule-line, .clients__title, .contact__heading')
.forEach(function (l) { watch(l); });
document.querySelectorAll('[data-tilt]').forEach(function (card) {
if (reduced) return;
addScene(card,
function (s) { s.top = docTop(card) - vh; s.len = vh + card.offsetHeight; },
function (p) {
var t = (p - 0.5) * 2; /* -1 … 1 across the transit */
card.style.transform =
'perspective(1200px) translateY(' + (t * -40 * scale).toFixed(1) + 'px) rotateX(' +
(t * -7).toFixed(2) + 'deg) rotateY(' + (t * 4).toFixed(2) + 'deg)';
});
});
/* ------------------------------------------------------------------ 7
MORE ABOUT ME — white panel sweeps across, words flip in.
------------------------------------------------------------------ */
var mam = document.querySelector('.mam');
var sweep = document.querySelector('[data-sweep]');
if (mam) {
watch(mam);
if (sweep && !reduced) {
addScene(mam,
function (s) { s.top = docTop(mam) - vh; s.len = vh + mam.offsetHeight; },
function (p) {
/* enters from the left, parks across the middle, exits right */
var x = keys([[0, -100], [0.45, 0], [0.55, 0], [1, 100]], p);
sweep.style.transform = 'translateX(' + x.toFixed(2) + '%)';
});
}
}
/* ------------------------------------------------------------------ 8
GALLERIES — per-column parallax at 200 / 700 / 1200px amplitudes.
------------------------------------------------------------------ */
document.querySelectorAll('[data-par]').forEach(function (cell) {
if (reduced) return;
var amp = parseFloat(cell.getAttribute('data-par')) || 0;
addScene(cell,
function (s) { s.top = docTop(cell) - vh; s.len = vh + cell.offsetHeight; },
function (p) {
cell.style.transform = 'translate3d(0,' + ((1 - 2 * p) * amp * scale).toFixed(1) + 'px,0)';
});
});
/* Tall about image drifts inside its mask. */
document.querySelectorAll('[data-par-img]').forEach(function (img) {
if (reduced) return;
var amp = parseFloat(img.getAttribute('data-par-img')) || 100;
var host = img.parentElement;
addScene(img,
function (s) { s.top = docTop(host) - vh; s.len = vh + host.offsetHeight; },
function (p) { img.style.transform = 'translate3d(0,' + ((1 - 2 * p) * amp * 0.5 * scale).toFixed(1) + 'px,0)'; });
});
/* ------------------------------------------------------------------ 9
TEXT REVEAL — words fly in from alternating sides, then the whole
pinned card swings away on its Y axis (0 → -90deg).
------------------------------------------------------------------ */
var revealCard = document.querySelector('[data-reveal-card]');
if (revealCard) {
document.querySelectorAll('.reveal-word').forEach(function (w, i) {
var from = parseFloat(w.getAttribute('data-from')) || 0;
w.style.setProperty('--from', Math.round(from * scale) + 'px');
w.style.transitionDelay = (i * 60) + 'ms';
});
watch(revealCard);
var invert = revealCard.querySelector('.reveal-card__invert');
if (!reduced && invert) {
var track = revealCard.parentElement;
addScene(revealCard,
function (s) {
s.top = docTop(track);
s.len = Math.max(1, track.offsetHeight - vh);
},
function (p) {
/* hold flat while pinned, then swing the type block out on its hinge */
var q = clamp((p - 0.55) / 0.45, 0, 1);
invert.style.transform =
'perspective(1200px) rotateY(' + (-90 * q).toFixed(2) + 'deg)';
});
}
}
/* ------------------------------------------------------------------ 10
Per-character reveals
------------------------------------------------------------------ */
document.querySelectorAll('[data-split]').forEach(function (p) {
if (!reduced) splitChars(p, 12);
watch(p);
});
document.querySelectorAll('[data-flipchars]').forEach(function (h) {
if (!reduced) splitChars(h, 45);
watch(h);
});
/* ------------------------------------------------------------------ 11
SEE ALL MY WORK — alternating slide-in
------------------------------------------------------------------ */
var samw = document.querySelector('.samw');
if (samw) {
samw.querySelectorAll('[data-samw]').forEach(function (r) {
r.style.setProperty('--dir', r.getAttribute('data-samw'));
});
watch(samw);
}
/* ------------------------------------------------------------------ 12
CLIENTS — built from a list so the markup stays short
------------------------------------------------------------------ */
var CLIENTS = ['Portraits', 'Fashion', 'Editorial', 'Street', 'Architecture', 'Landscape', 'Documentary', 'Fine Art', 'Still Life', 'Movement'];
var clientsHost = document.querySelector('[data-clients]');
if (clientsHost) {
CLIENTS.forEach(function (name, i) {
var row = document.createElement('div');
row.className = 'client-row';
row.style.setProperty('--dir', i % 2 ? -1 : 1);
var a = document.createElement('a');
a.href = '#work';
var s1 = document.createElement('span'); s1.textContent = name;
var s2 = document.createElement('span'); s2.textContent = name; s2.setAttribute('aria-hidden', 'true');
a.appendChild(s1); a.appendChild(s2);
row.appendChild(a);
clientsHost.appendChild(row);
watch(row);
});
}
/* ------------------------------------------------------------------ 13
Remaining one-shot reveals
------------------------------------------------------------------ */
['[data-contact-photo]', '.contact__form', '[data-fade]', '[data-thanks]']
.forEach(function (sel) {
document.querySelectorAll(sel).forEach(watch);
});
/* ------------------------------------------------------------------ 14
Forms — no endpoint wired up; they acknowledge and reset.
------------------------------------------------------------------ */
document.querySelectorAll('[data-form]').forEach(function (form) {
form.addEventListener('submit', function (e) {
e.preventDefault();
var status = form.parentElement.querySelector('.contact__status, .newsletter__status');
if (!form.checkValidity()) {
if (status) status.textContent = 'Please complete the required fields.';
return;
}
if (status) status.textContent = 'Thanks — this demo form is not wired to a backend yet.';
form.reset();
});
});
/* ------------------------------------------------------------------ 15
Wiring. Every layout read lives in refresh(); the scroll loop only writes.
------------------------------------------------------------------ */
var yearEl = document.querySelector('[data-year]');
if (yearEl) yearEl.textContent = new Date().getFullYear();
function refresh() {
measureAll();
current = window.pageYOffset;
target = running ? clamp(target, 0, maxScroll()) : current;
frame(current);
}
/* Keep anchor navigation deterministic when the menu restores scrollbars. */
document.addEventListener('click', function (e) {
var a = e.target.closest && e.target.closest('a[href^="#"]');
if (!a) return;
var id = a.getAttribute('href').slice(1);
var el = id ? document.getElementById(id) : document.body;
if (!el) return;
e.preventDefault();
var y = clamp(docTop(el), 0, maxScroll());
target = current = y;
running = false;
window.scrollTo({ top: y, behavior: 'instant' });
frame(y);
if (history.replaceState) history.replaceState(null, '', a.getAttribute('href'));
});
if (smooth) {
document.documentElement.classList.add('is-smooth');
window.addEventListener('wheel', onWheel, { passive: false });
}
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', refresh);
window.addEventListener('orientationchange', refresh);
window.addEventListener('load', refresh);
if (document.fonts && document.fonts.ready) document.fonts.ready.then(refresh);
/* A late image, a lazy font or a sticky settling all change the document
height after first paint and would leave every cached offset stale.
ResizeObserver reports it without polling layout on a timer. */
if (window.ResizeObserver) {
var ro = new ResizeObserver(function () { refresh(); });
ro.observe(document.body);
} else {
var lastH = 0;
setInterval(function () {
var h = document.documentElement.scrollHeight;
if (h !== lastH) { lastH = h; refresh(); }
}, 500);
}
refresh();
})();
```