June 18

Скиллы / промпты / ссылки из видео

▸Наш канал — https://t.me/crypto_sgx ▸Выйти на новый уровень вайб-кодинга — https://t.me/crypto_sgx/1727

Начну с скиллов которые юзал для создания проекта Визуал скиллы (фронтенд):
▸Делает визуал дороже - https://github.com/nextlevelbuilder/ui-ux-pro-max-skill
▸То же самое - https://github.com/leonxlnx/taste-skill

Оптимизация работы / оркестрация:
▸Автоматизирует управление репозиториями и проектами с помощью роя ИИ-агентов - https://github.com/ruvnet/ruflo
▸несколько Claude/агентов параллельно в tmux, каждый своя задача - https://github.com/smtg-ai/claude-squad
▸большая коллекция субагентов по доменам - https://github.com/VoltAgent/awesome-claude-code-subagents

Под оптимизацию могу отметить:
▸claude-context - https://github.com/zilliztech/claude-context
▸ccusage - https://github.com/ccusage/ccusage
▸SuperClaude_Framework - https://github.com/SuperClaude-Org/SuperClaude_Framework
▸caveman - https://github.com/juliusbrussee/caveman

Под воркфлоу:
▸GSD - https://github.com/gsd-build

Ну и парочку приватных из - SGX +VIBE (https://t.me/crypto_sgx/1727) Как установить эти скиллы?
Просто просите клауд код установить и все) он их поставит. Потом вызывайте через слеш в нужный момент и все. Например: "/designtaste"

Отс чаты (основные - откуда было много трафика): ▸https://t.me/tvrn_otc ▸https://t.me/otc_service_ban ▸https://t.me/+dgxKFHm7dOo0MzVi ▸Ссылка на таблицу в фигме - https://www.figma.com/board/YQTdd6nuwtAG3iUPu90gRU/%D0%92%D0%B0%D0%B9%D0%B1%D0%BA%D0%BE%D0%B4%D0%B8%D0%BC?t=hm0qIrzJwmJm7lcO-1

Cообщение для рассылки:

WTS
Ищу проекты по веб и мобайл разработке. Делаю под ключ:
— Лендинги
— Мобильные приложения
— Сайты-визитки
— Интернет-магазины
Есть опыт с реальными клиентами, небольшие сроки и цены. Есть задача? Расскажите — предложу решение. Есть портфолио.

Теперь как создать себе сочные кейсы в портфолио?

▸Ответ: Просто украдите мои промпты, уникализируйте (меняйте под себя) и запускайте на Opus 4.8 - на нем все четко получается. Ниже будет еще два сайта с промптами для создания с таким же вкусным визуалом. ▸Первый сайтец с таким визуалом (я показывал его в видео):

Промпт:
# Recreate this site as a single HTML file: Lumora — Design & Engineering Studio
You are an expert creative front-end developer. Produce a **single self-contained `index.html`** that reproduces the project below **exactly** — same layout, sections, visuals, motion, and interaction. Pure HTML/CSS/JS in one file: no build step, no framework, no bundler. Use ES modules with a CDN importmap for the one library actually used (Lenis smooth-scroll). Hardcode every value given here as a fixed constant. All the CSS below lives in one `<style>` block in `<head>`; the JS in one `<script type="module">` block before `</body>`. The spring / text-reveal animations from the original (react-spring + spring-text-engine) must be reproduced with **plain JS** — a tiny rAF spring helper and/or CSS transitions — achieving the same feel.
## What it is
A single-page, light-palette landing site for **"Lumora — Independent Design & Engineering Studio"**. The page is built on a rem-based adaptive grid (the root font-size scales with the viewport), uses Google **Onest** as the only typeface, and reads as near-white surfaces (`#ffffff` page, `#f1f0ee` light fills) punctuated by deep near-black ink cards (`#0a0a0a`) and a single burnt-orange accent (`#b15f2c`). It opens with a **full-screen dark intro loader** that counts `000 → 100` and slides up; only then do the above-the-fold reveals play. The hero is a **full-bleed before/after photo with a "liquid" cursor-reveal** (moving the pointer paints a soft brush trail of a second image over the first), with a giant `LUMORA` watermark, a line-by-line headline, a carousel card, and a partner grid. Below: an About statement, a four-pill "We / Build / → / Better" band, a 4-card Portfolio on black cards, a 4-row Services list with hover-fill rows, a black Stats panel with scroll-driven count-up numbers, and a black footer with a CTA, link columns and a watermark. A header (Menu button + live local clock) overlays the hero; the Menu opens a full-screen dark overlay; every "Contact / Let's Talk / Start a project" CTA opens a **request modal** (stubbed submit). All smooth-scrolling is driven by **Lenis**.
Sections in DOM order: **PageLoader** → **Header** (fixed overlay) → `main`{ **Hero** → **About** → **CreateBand** → **Portfolio** → **Services** → **Stats** } → **Footer** → **NavMenu** (overlay) → **RequestModal** (overlay).
## Page shell & libraries
### `<head>`
```html
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lumora — Independent Design & Engineering Studio</title>
<meta name="description" content="Lumora is an independent studio crafting brands, products, and the systems that connect them — bold ideas, shipped with quiet precision." />
<meta name="theme-color" content="#0a0a0a" />
<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=Onest:wght@400;500;600;700&display=swap" rel="stylesheet">
```
### Importmap + module entry (before `</body>`)
```html
<script type="importmap">
{ "imports": { "lenis": "https://unpkg.com/lenis@1.3.23/dist/lenis.mjs" } }
</script>
<script type="module"> /* all JS below */ </script>
```
Lenis import + raf loop (the original instantiates Lenis with smoothWheel and runs a manual raf loop; it also resets scroll to top on load):
```js
import Lenis from 'lenis';
window.scrollTo(0, 0);
const lenis = new Lenis({ smoothWheel: true });
function raf(t){ lenis.raf(t); requestAnimationFrame(raf); }
requestAnimationFrame(raf);
```
**Scroll lock model.** A single boolean `scrollEnabled` gates everything. When something needs to lock scroll (loader, nav overlay, request modal) call a `stopScroll()` that does `lenis.stop()` and sets `html { position:relative; overflow:hidden; height:100% }`; `startScroll()` does `lenis.start()` and removes those three inline styles. The loader stops scroll on mount and starts it when its exit finishes.
**`scrollTo(id)` helper.** Smooth-scroll to an element by id: temporarily disable scroll-state, then after `50ms` `window.scrollTo({ top: <element top + pageYOffset>, behavior: 'smooth' })`, re-enable after `100ms`. Used by the logo, nav links (non-contact), and the hero "View Work" button.
### Global CSS reset / base
```css
*{ box-sizing:border-box; margin:0; padding:0 }
html{ font-size:16px; -webkit-font-smoothing:antialiased }
body{ background:#ffffff; color:#111111; font-family:'Onest',sans-serif; overflow-x:hidden }
a{ color:inherit; text-decoration:none }
button{ font:inherit; color:inherit; background:none; border:none; cursor:pointer }
ul{ list-style:none }
img{ display:block }
.sr-only{ position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0 }
:focus-visible{ outline:2px solid #b15f2c; outline-offset:2px }
@media (prefers-reduced-motion: reduce){ *{ animation:none !important; transition:none !important } }
```
### The rem-based adaptive grid (CRITICAL — bake exactly)
The whole layout is sized in **rem**; the root font-size scales with the viewport so the design stays proportional. The design base is `FONT_BASE = 16px`. Reproduce the original's media queries verbatim (each = `16 * 100 / baseWidth` vw):
```css
@media (max-width:1920px){ html{ font-size:0.833333vw } } /* base 1920 */
@media (max-width:1440px){ html{ font-size:1.111111vw } } /* base 1440 */
@media (max-width:1024px){ html{ font-size:1.5625vw } } /* base 1024 */
@media (max-width:640px){ html{ font-size:4.444444vw } } /* base 360 */
```
**Scale-UP above 1920px** (runtime JS, mirrors the original `AdaptiveGrid`): on load + resize, set `html.style.fontSize` to an interpolated value so the layout keeps growing on large displays. Formula with damping `coef = 0.6666`:
```js
function applyAdaptiveGrid(){
const FONT_BASE = 16, baseWidth = 1920, coef = 0.6666;
const w = window.innerWidth;
const widthReduction = ((baseWidth - w) / baseWidth) * 100; // negative when w > baseWidth
const size = FONT_BASE - (FONT_BASE * (widthReduction * coef)) / 100;
if (size > FONT_BASE) document.documentElement.style.fontSize = size + 'px';
else document.documentElement.style.removeProperty('font-size'); // let media queries drive
}
applyAdaptiveGrid(); addEventListener('resize', applyAdaptiveGrid);
```
Because everything is rem-based, **all sizes below are given in rem/px exactly as Tailwind emitted them** (Tailwind's default scale: `text-sm`=0.875rem, `text-base`=1rem, `text-lg`=1.125rem, `text-xl`=1.25rem, `text-2xl`=1.5rem, `text-3xl`=1.875rem, `text-4xl`=2.25rem, `text-5xl`=3rem, `text-6xl`=3.75rem, `text-7xl`=4.5rem; spacing unit `0.25rem`; `gap-3`=0.75rem etc.). Keep them in rem so the adaptive grid works.
### Shared spring helper (replace react-spring)
Implement one tiny critically-ish-damped spring stepper used for entrance reveals and hovers. react-spring configs are given as `{ tension, friction }`; map them to a stiffness/damping rAF integrator (mass = 1): `accel = tension*(target - x) - friction*v`, integrate at `dt≈1/60`, settle when `|target-x|<0.001 && |v|<0.001`. For entrance reveals you can instead use CSS transitions with the equivalent feel:
- `{ tension:210, friction:26 }` ≈ `cubic-bezier(.22,1,.36,1)` ~0.7s
- `{ tension:200, friction:24 }` / `{180,26}` ≈ `cubic-bezier(.16,1,.3,1)` ~0.8s
- `{ tension:320, friction:18 }` (hovers) ≈ snappy ~0.35s `cubic-bezier(.2,.8,.2,1)`
Either approach is acceptable as long as motion reads springy. Hovers are pointer-driven and disabled on touch (the original disables `Hover` on mobile).
### Text reveal helper (replace spring-text-engine)
Two reveal modes are used; both play **once** when the element scrolls into view (IntersectionObserver, `mode:"once"`), gated additionally on the intro loader being finished for hero text.
- **Line reveal** (`overflow` clip): split the heading into lines (wrap each line in a `span` with `overflow:hidden`; inner span translates `Y 100% → 0%` and `opacity 0 → 1`). Per-line stagger when given (`lineStagger`). Spring/curve ≈ `duration 900ms, easeOutCubic` → `cubic-bezier(0.215,0.61,0.355,1)`.
- **Word reveal** (About statement): split into words; each word `translateY(24px→0)` + `opacity(0→1)`, stagger `35ms` per word, `duration 700ms, easeOutQuart` → `cubic-bezier(0.165,0.84,0.44,1)`.
For simplicity you may split by spaces and treat visual "lines" as the natural wrap; the important part is the staggered slide-up-from-clip feel.
## Fixed palette & tokens (bake these in)
```
--background:#ffffff --foreground:#111111
--ink:#0a0a0a (black cards / pills / overlays)
--muted:#8d8d8d --subtle:#b6b6b6
--line:#e6e5e2 (hairline borders)
--surface:#f1f0ee --surface-2:#e3e2df
--accent:#b15f2c --accent-from:#cf8047 --accent-to:#97501f
--hero-from:#ecebe9 --hero-to:#c9c9c9 (hero section bg = hero-to #c9c9c9)
```
Radii: `--radius-pill:9999px`, `--radius-card:2rem`, `--radius-card-sm:1.25rem`, `--radius-control:0.875rem`.
Watermark size: `--text-watermark:13rem`.
Page shell max-width: `--container-shell:88rem` (use `max-width:88rem; margin-inline:auto` for `.shell`).
Font weights used: 400/500/600/700.
### SVG assets to inline (sized `1em`, `fill`/`stroke` = `currentColor`)
- **LogoMark** (brand 4-point spark), `viewBox="0 0 48 48"`, filled:
`M24 2c2.2 13.8 7.9 19.6 22 22-14.1 2.4-19.8 8.2-22 22-2.2-13.8-7.9-19.6-22-22 14.1-2.4 19.8-8.2 22-22Z`
- **ArrowRight** `viewBox 0 0 24 24` stroke 2 round: `M5 12h14M13 6l6 6-6 6`
- **ArrowUpRight** stroke 2 round: `M7 17 17 7M8 7h9v9`
- **Star** filled: `M12 2.5l2.9 5.88 6.49.94-4.7 4.58 1.11 6.46L12 17.9l-5.8 3.05 1.1-6.46-4.69-4.58 6.49-.94L12 2.5z`
- **Globe** stroke 1.4: `<circle cx=12 cy=12 r=9.25/>` + `M12 2.75c2.6 2.3 4 5.8 4 9.25s-1.4 6.95-4 9.25c-2.6-2.3-4-5.8-4-9.25s1.4-6.95 4-9.25zM2.75 12h18.5`
- **X / close** stroke 2 round: `M4 4l16 16M20 4 4 20`
- **CircleDot** stroke 1.6: `<circle cx12 cy12 r9/>` + filled `<circle cx12 cy12 r3.2/>`
- **Grid / menu** (three lines) stroke 2 round: `M4 6h16M4 12h16M4 18h16`
---
## The loader / reveal (PageLoader)
A fixed full-screen panel on top of everything from first paint. `position:fixed; inset:0; z-index:120; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:2rem; background:#0a0a0a; color:#fff; border-radius:0 0 2rem 2rem` (rounded bottom corners — `rounded-b-card`).
- Locks scroll on mount (`stopScroll()`).
- **Center content** (`gap:1.25rem`, centered text, `opacity 1`): a row `font-weight:600; font-size:1.5rem` (sm: 1.875rem) reading `[LogoMark, 1.875rem, color:#cf8047] Lumora`; below it a `max-width:24ch; font-size:0.875rem; color:rgba(255,255,255,.55)` paragraph: **"Bold ideas, shipped with quiet precision."**
- **Progress block** `width:min(22rem,72vw); gap:0.75rem`: a `height:1px` track `background:rgba(255,255,255,.15)` with an inner fill `height:100%; background:#cf8047; width:<progress>%; transition:width .1s ease-out`. Below it a row `justify-content:space-between; font-size:0.75rem; font-weight:500; text-transform:uppercase; letter-spacing:.05em; color:rgba(255,255,255,.45)` → left `Loading`, right a tabular-nums counter `color:rgba(255,255,255,.8)` showing the number **zero-padded to 3 digits** (`000`…`100`).
- **Count animation:** drive progress `0 → 100` over `FILL_MS = 1300ms`, eased with **easeInOutCubic** (`t<.5 ? 4t³ : 1-((-2t+2)³)/2`), `progress = round(ease(t)*100)`.
- **Exit:** when the count reaches 100, slide the whole panel up: `transform: translateY(0%) → translateY(-100%)` with spring feel `{tension:220,friction:30}` (~`cubic-bezier(.22,1,.36,1)`, ~0.7s); simultaneously fade the center content `opacity 1→0, translateY 0→-12px`. When the slide finishes: set the global **intro `ready` flag true**, `startScroll()`, and remove the loader from the DOM. All hero reveals are gated on `ready` (they only begin after the loader has left).
---
## Layout & sections (in order)
A `.shell` wrapper = `max-width:88rem; margin-inline:auto`. Default horizontal padding `px-5`=1.25rem, `sm:px-8`=2rem (sm breakpoint = 640px). `lg` = 1024px.
### A skip link (first focusable)
`<a href="#main">Skip to content</a>` — visually hidden until focused, then `position:fixed; left:1rem; top:1rem; z-index:60; border-radius:.875rem; background:#0a0a0a; padding:.5rem 1rem; font-size:.875rem; color:#fff`.
---
### 1) Header (fixed overlay, entrance gated on `ready`)
`position:absolute; inset-inline:0; top:0; z-index:50`. Entrance: `opacity 0→1, translateY(-14px→0)`, spring `{210,26}`, delay `150ms` after `ready`.
Inner: `.shell` flex row, `align-items:center; justify-content:space-between; gap:1.5rem; padding:1.25rem` (sm: `2rem 2rem` → `py-6 px-8`).
- **Left — brand button** (`onClick → scrollTo('home')`): a hover-spring `span` (`scale 1→1.04`, `{320,18}`) `display:flex; align-items:center; gap:.5rem; font-size:1.125rem; font-weight:600; letter-spacing:-.01em` → `[LogoMark 1.25rem color:#b15f2c] Lumora`.
- **Center — primary nav** (`display:none` below `lg`, then `flex`): `ul` `gap:2rem; font-size:.875rem; font-weight:500`. Items (each a button; hover lifts the label `translateY(0→-2px)` + `opacity .8→1`, `{320,22}`): **Home** (current, `aria-current=page`), **Work**, **Services** (with a `▾` dropdown caret, `font-size:.75rem opacity:.6`), **Studio**, **Careers**, **Contact**. Click routing: `Home→#home`, `Work→#works`, `Services→#services`, `Studio→#about`, `Careers→#careers`, `Contact→opens request modal`. Non-contact links call `scrollTo(id)`.
- **Right — clock chip + Menu**:
- Clock chip (`display:none` below `md`=768px, then `flex`): `border:1px solid rgba(230,229,226,.8); background:rgba(255,255,255,.4); backdrop-filter:blur(4px); border-radius:.875rem; padding:.5rem .75rem; gap:.75rem; font-size:.75rem; color:rgba(17,17,17,.7)`. Contents: muted label **"Local time"** (`color:rgba(17,17,17,.45)`), then a `min-width:3.5rem; tabular-nums; font-weight:500; color:#111` **live time** (e.g. `9:41am`), a `•` separator (`color:rgba(17,17,17,.3)`), then a `font-weight:500` **live date** (e.g. `12 March, 2025`).
- **Live clock JS:** update every 1s. Time = `H:MM` + lowercase meridiem, no leading zero on hour (`hours%12||12`, minutes padded to 2). Date = `D Month, YYYY` (full month name). Until first tick, show fallbacks `9:41am` / `12 March, 2025`.
- Menu button (`onClick → open NavMenu`): `border:1px solid rgba(230,229,226,.8); background:rgba(255,255,255,.4); backdrop-filter:blur(4px); border-radius:.875rem; hover bg:rgba(255,255,255,.7)`. Inner hover-spring span (`scale 1→1.05`): `padding:.5rem 1rem; font-size:.75rem; font-weight:500; text-transform:uppercase; letter-spacing:.05em` → `[GridIcon .875rem] Menu` (the word "Menu" hidden below `sm`).
---
### 2) Hero (`#home`) — full-bleed before/after liquid reveal
`section#home`: `position:relative; isolation:isolate; overflow:hidden; border-radius:0 0 2rem 2rem; background:#c9c9c9` (hero-to).
**A) LiquidReveal full-bleed background** (`position:absolute; inset:0; z-index:0`). This is the signature effect — reproduce its mechanics exactly:
- A positioned container holds: (1) a `<img>` of the **before** image, `object-fit:cover`, `position:absolute; inset:0; width/height:100%` — always visible, the LCP image; (2) a `<canvas aria-hidden>` `position:absolute; inset:0; width/height:100%; pointer-events:none` that paints the **after** image along the cursor trail.
- **IMPORTANT image mapping (preserve exactly):** `beforeSrc = .../hero/after.jpg` (the always-shown base image) and `afterSrc = .../hero/before.jpg` (the brush-revealed image). I.e. the file named `after.jpg` is shown by default and the file named `before.jpg` is painted on the cursor trail. Do not swap them.
- **Params:** `brushRadius = 143` (CSS px), `decay = 0.016` per frame, `dpr = min(devicePixelRatio, 2)`.
- **Canvas sizing:** size the main canvas to the container rect × dpr; keep CSS size = rect size. On resize (ResizeObserver on the container) re-measure. Build an offscreen **cover** canvas at canvas resolution and draw the `after` image into it with `object-fit:cover` math (scale to fill, center). `radius = brushRadius*dpr`; a **brush** offscreen canvas is `diameter = ceil(radius*2)` square.
- **Pointer trail:** listen to `pointermove` on `window`. Convert client coords to canvas space (×dpr). Ignore points more than `radius` outside the canvas (and reset `last`). Interpolate between the last point and the new point: `step = max(radius*0.3, 1)`, `n = min(ceil(dist/step), 60)` intermediate points pushed into a `points` array.
- **Per-frame tick (rAF):**
- If there are queued points → `idle = 0`; else increment `idle` and bail once `idle > 120`.
- Compute `fade = drawing ? decay : min(decay + idle*0.004, 0.5)`. Apply `globalCompositeOperation:'destination-out'; fillStyle = rgba(0,0,0,fade); fillRect(full)` so the existing trail decays.
- If drawing: for each queued point **stamp** it, then clear the queue. If idle reaches 120 frames: `clearRect(full)` (hard clear so no residue lingers).
- **stamp(x,y):** on the brush canvas, clear, `source-over`, draw a radial gradient centered (`addColorStop 0 → rgba(255,255,255,1)`, `0.55 → rgba(255,255,255,.82)`, `1 → rgba(255,255,255,0)`), fill the square. Then `source-in`, draw the matching region of the **cover** canvas (`drawImage(cover, x-c,y-c,diam,diam, 0,0,diam,diam)`) so only the after-pixels under the soft brush remain. Finally on the main canvas `source-over`, `drawImage(brush, x-c, y-c)`.
- Honor `prefers-reduced-motion: reduce` → skip the canvas entirely, leave only the static base image.
**B) Legibility vignette** `position:absolute; inset:0; z-index:1; pointer-events:none; background:linear-gradient(to bottom, rgba(255,255,255,.35), transparent, rgba(255,255,255,.35))`.
**C) Brand watermark** (`pointer-events:none; position:absolute; inset-inline:0; bottom:7rem; z-index:1; text-align:center; user-select:none; font-weight:700; line-height:1; font-size:13rem; color:rgba(255,255,255,.4)`) text **LUMORA**. Reveal (gated on `ready`): `opacity 0→0.4, translateY(20px→0)`, `{120,30}`, delay `300ms`.
**D) Content grid** `.shell` `position:relative; z-index:20; display:flex; flex-direction:column; gap:2rem; padding:7rem 1.25rem 5rem` (sm px 2rem; lg: `display:grid; min-height:100lvh; grid-template-columns:repeat(12,1fr); gap:2.5rem; padding:9rem 2rem 7rem`).
- **Left column** (`lg:grid-column: span 7`), `display:flex; flex-direction:column; gap:1.75rem`:
- **Eyebrow** (reveal `opacity/translateY(10px)`, delay 200ms): a `font-size:.875rem; font-weight:500; color:rgba(17,17,17,.7); display:inline-flex; align-items:center; gap:.5rem` with a leading `0.375rem` dot (`background:rgba(17,17,17,.5); border-radius:9999px`) → **"Independent Studio"**.
- **H1** (line reveal, gated on `ready`, delay 250ms, `lineStagger 120ms`, easeOutCubic 900ms, `overflow` clip): `max-width:18ch; font-size:2.25rem; font-weight:600; line-height:.98; letter-spacing:-.02em` (sm 3rem, md 3.75rem). Lines: **"Bold ideas,"** / **"shipped with"** / **"quiet precision"**.
- **Rating row** (reveal, delay 650ms): `display:flex; align-items:center; gap:.75rem`. A `color:#b15f2c` span with **5** Star icons (`font-size:1rem`), then `font-size:.875rem; font-weight:500; color:rgba(17,17,17,.7)` text **"200+ brands shipped"**.
- **CTA row** (reveal, delay 750ms): `display:flex; flex-wrap:wrap; gap:.75rem`. Two pill buttons: **"Let's Talk"** (variant `dark`, with arrow → opens request modal) and **"View Work"** (variant `outline` → `scrollTo('works')`).
- **Right column** (`lg:span 5`), `display:flex; flex-direction:column; align-items:flex-start; gap:2rem` (lg: `align-items:flex-end`):
- **HeroCard** (carousel) — reveal `opacity/translateY(16px) scale(.96→1)`, `{200,24}`, delay 400ms. Card: `width:100%; max-width:24rem (lg width:19rem); border-radius:1.25rem; background:rgba(255,255,255,.7); padding:.5rem; box-shadow: sm; box-shadow ring 1px rgba(230,229,226,.7); backdrop-filter:blur(12px)`. Inside a clickable row (`display:flex; gap:.5rem; cursor:pointer; border-radius:.875rem`):
- Left tile: `aspect-ratio:1; width:6rem; display:grid; place-items:center; border-radius:.875rem; background:#0a0a0a; font-size:1.875rem; color:#fff` containing a `LogoMark` in `color:#cf8047`.
- Right panel: `flex:1; border-radius:.875rem; background:rgba(241,240,238,.7); padding:.75rem; display:flex; flex-direction:column; justify-content:space-between`. Top = a `position:relative; min-height:3.25rem` slot showing the active item: a `font-size:.65rem; font-weight:500; uppercase; letter-spacing:.05em; color:rgba(17,17,17,.45)` **caption** over a `max-width:8rem; font-size:.875rem; font-weight:500; line-height:1.35` **title**. Items (cycle): `{caption:"Conversion design", title:"Crafted to convert."}`, `{caption:"Engineering", title:"Built to scale."}`, `{caption:"Brand systems", title:"Designed to last."}`. Bottom row: dots on the left (`h:1px*`… actually 3 dashes `height:.25rem; border-radius:9999px`; active = `width:1rem; background:rgba(17,17,17,.7)`, inactive = `width:.375rem; background:rgba(17,17,17,.2)`, `transition:all .3s`) and prev/next buttons on the right (`size:1.75rem; display:grid; place-items:center; border-radius:9999px; background:#fff; color:rgba(17,17,17,.7); ring 1px #e6e5e2; hover color:#111`, prev = ArrowRight rotated 180°, next = ArrowRight). **Behavior:** clicking the card or Next advances; Prev goes back; wraps around `(i+step+n)%n`. Swap animation: outgoing item fades/slides `translateY(±14px)`, incoming enters from `translateY(∓14px) → 0`, `{300,28}`.
- **Partners** (reveal `translateY(14px)`, `{200,24}`, delay 550ms): `width:100%; max-width:24rem (lg 19rem)`. A label `margin-bottom:.75rem; font-size:.75rem; font-weight:500; color:rgba(17,17,17,.45); text-align:left` (lg right) **"Trusted by"**. Then a `display:grid; grid-template-columns:repeat(4,1fr); column-gap:1rem; row-gap:.75rem` list; each item is a hover-spring span (`translateY(0→-2px)`, `opacity .7→1`, `{320,20}`) `display:flex; align-items:center; gap:.375rem; font-size:.75rem; color:rgba(17,17,17,.7)` → `[CircleDot .875rem color:rgba(17,17,17,.4)] <name>`. Names: **Kaido, Northpeak, Vellum, Orbit, Brightline, Cobalt, Mesa**.
**E) Bottom status bar** (reveal opacity only, delay 900ms): `.shell` `display:flex; align-items:center; justify-content:space-between; gap:.75rem; border-top:1px solid rgba(17,17,17,.1); padding:1.25rem; font-size:.75rem; font-weight:500; text-transform:uppercase; letter-spacing:.025em; color:rgba(17,17,17,.6)` (sm px 2rem). Left **"Working since 2014"**; center **"Remote-first, worldwide"** (hidden below sm); right `inline-flex; gap:.5rem` **"Scroll to explore"** + a `↓`.
---
### 3) About (`#about`)
`section#about` `background:#fff`. `.shell` `display:grid; grid-template-columns:1fr; align-items:center; gap:3rem; padding:5rem 1.25rem` (sm px 2rem; lg: `grid-template-columns:1fr 1fr; padding-block:7rem`).
- **Left — globe block** `position:relative; min-height:14rem` (lg 20rem): a big background `Globe` icon `position:absolute; left:-1rem; top:50%; transform:translateY(-50%); font-size:12rem; color:rgba(17,17,17,.1)` (sm 16rem, lg `left:-1.5rem; 20rem`). Eyebrow **"The Studio"** (`position:relative`). Bottom-left a reveal block (`translateY(12px)`) `display:flex; align-items:center; gap:.75rem; font-size:.875rem; color:rgba(17,17,17,.7)` → `[Globe 1.5rem color:#111] <span max-width:14rem>` **"A distributed team building across every time zone."**
- **Right — statement** `display:flex; flex-direction:column; gap:2.5rem`:
- **H2** (word reveal, `wordStagger 35ms`, easeOutQuart 700ms, `translateY 24px`): `font-size:1.5rem; font-weight:500; line-height:1.35; letter-spacing:-.01em` (sm 1.875rem). Text: **"We partner with ambitious teams to ship "** then in `color:#8d8d8d`: **"digital products, brand systems, and the strategy that holds them together."**
- **Footer row** (reveal `translateY(12px)`, delay 200ms): `display:flex; flex-wrap:wrap; align-items:flex-end; justify-content:space-between; gap:1.5rem; border-top:1px solid #e6e5e2; padding-top:1.5rem`. Left: a `font-size:.875rem; color:rgba(17,17,17,.45)` label **"Find us online"** above a `display:flex; gap:.5rem` row of 3 social chips (`size:2.25rem; display:grid; place-items:center; border-radius:9999px; font-size:.875rem`; each inner icon hover-springs `scale 1→1.18`, `{320,16}`): **X / Twitter** = `background:#b15f2c; color:#fff` with the X icon; **Behance** and **Dribbble** = `background:#f1f0ee; color:rgba(17,17,17,.7)` with a CircleDot icon. Right: a pill button **"About Us"** (variant `outline`, withArrow, href `#about`).
---
### 4) CreateBand ("We Build → Better")
`section` `background:#fff`. `ul` `.shell` `display:flex; flex-direction:column; gap:.75rem; padding:2.5rem 1.25rem` (sm: `flex-direction:row; gap:1rem; px 2rem`). Four `li flex:1`, each a reveal (`translateY(28px)`, `{200,22}`, delay `index*120ms`) wrapping a hover-spring tile (`scale 1→1.03`, `{300,18}`) `display:grid; place-items:center; height:6rem; border-radius:9999px; font-size:1.875rem; font-weight:500` (sm: `height:10rem; font-size:2.25rem`). Words/variants:
1. **"We"** — variant `light`: `background:#f1f0ee; color:#111`.
2. **"Build"** — variant `accent`: `background:linear-gradient(to bottom right,#cf8047,#97501f); color:#fff`.
3. (arrow) — variant `dark`: `background:#0a0a0a; color:#fff`, content = ArrowRight icon `font-size:2.25rem` (sm 3rem).
4. **"Better"** — variant `ghost`: `background:rgba(241,240,238,.6); color:rgba(17,17,17,.35)`.
---
### 5) Portfolio (`#works`)
`section#works` `background:#fff`. `.shell` `padding:2.5rem 1.25rem 5rem` (sm px 2rem; lg pb 7rem).
- **Eyebrow** (reveal, centered): an Eyebrow with extra `border:1px solid #e6e5e2; border-radius:9999px; padding:.375rem 1rem` → **"Portfolio"**.
- **H2** (line reveal, easeOutCubic 900ms, delay 120ms, `overflow`, centered, `width:fit-content`): `font-size:2.25rem; font-weight:600; letter-spacing:-.02em` (sm 3rem) → **"Selected Work"**.
- **Cards** `display:grid; grid-template-columns:1fr; gap:1.5rem` (md=768px: 2 cols). Each card is a reveal `li` (`translateY(48px)`, `{180,26}`, delay `index*90ms`) wrapping a link → hover-spring `article` (`translateY(0→-8px) scale(1→1.012)`, `{260,22}`): `position:relative; min-height:22rem; overflow:hidden; border-radius:2rem; background:#0a0a0a; padding:1.5rem; color:#fff; box-shadow ring 1px rgba(255,255,255,.05)` (sm `min-height:26rem; padding:2rem`).
- Top meta row `display:flex; justify-content:space-between; font-size:.75rem; text-transform:uppercase; letter-spacing:.025em; color:rgba(255,255,255,.45)`: left `<category> — <year>`; right a hover-spring badge **triggered by the whole card** (`rotate 0→45deg, scale 1→1.08`, `{280,18}`) `size:2.75rem; display:grid; place-items:center; border-radius:9999px; background:rgba(255,255,255,.1); color:#fff; ring 1px rgba(255,255,255,.15)` containing ArrowUpRight.
- Centered watermark: `position:absolute; inset:0; display:grid; place-items:center; pointer-events:none` → `[LogoMark font-size:4.5rem color:rgba(255,255,255,.9)]` with a tiny `® font-size:.75rem color:rgba(255,255,255,.6)`.
- Bottom block `position:absolute; inset-inline:1.5rem; bottom:1.5rem` (sm 2rem): `h3 font-size:1.5rem; font-weight:500; letter-spacing:-.01em` (sm 1.875rem) = name; `p margin-top:.5rem; max-width:28rem; font-size:.875rem; color:rgba(255,255,255,.55)` = description; then a `margin-top:1.25rem; display:flex; flex-wrap:wrap; gap:.5rem` of tag chips (`display:inline-flex; border:1px solid rgba(255,255,255,.25); color:#fff; border-radius:9999px; padding:.5rem 1rem; font-size:.875rem`).
- **Items:**
1. **Aster Labs** — Branding — 2025 — "A complete identity and go-to-market system for a fast-moving research startup." — tags: Branding, Strategy, Design.
2. **Nova Finance** — Product — 2024 — "A finance platform reimagined — clear data, calm interfaces, and effortless flows." — tags: Product Design, Web App, QA.
3. **Helio Studio** — Identity — 2023 — "A bold visual identity and art direction system built to scale across every surface." — tags: Brand Identity, Art Direction.
4. **Pulse Health** — Mobile — 2023 — "A wellness app grounded in research, shipped end to end from concept to release." — tags: Mobile App, UX Research, Development.
---
### 6) Services (`#services`)
`section#services` `background:#fff`. `.shell` `padding:5rem 1.25rem` (sm px 2rem; lg py 7rem).
- **Eyebrow** (reveal) **"Services"**.
- **H2** (line reveal, easeOutCubic 900ms, delay 120ms, overflow): `margin:1.25rem 0 3rem; max-width:16ch; font-size:2.25rem; font-weight:600; letter-spacing:-.02em` (sm `margin-bottom:3.5rem; font-size:3rem`) → **"What we do best"**.
- **Rows** `ul`. Each row is a reveal `li` (`translateY(24px)`, `{200,24}`, delay `index*80ms`) `border-top:1px solid #e6e5e2` (first row no top border) wrapping a link → a **hover-spring row** that fills on hover: `from { background:rgba(241,240,238,0); padding-left:1.5rem; padding-right:1.5rem }` → `to { background:rgba(241,240,238,1); padding-left:2rem; padding-right:1.25rem }`, `{240,26}`; base `display:flex; align-items:center; gap:1rem; border-radius:1.25rem; padding-block:1.5rem` (sm `gap:1.5rem; padding-block:2rem`). Cells: index `width:1.75rem; font-size:.875rem; font-weight:500; color:rgba(17,17,17,.4)` (sm `width:2.5rem`); `h3 flex:1; font-size:1.5rem; font-weight:500; letter-spacing:-.01em` (sm 1.875rem, md 2.25rem) = title; a description `p` (hidden below lg) `max-width:20rem; font-size:.875rem; color:rgba(17,17,17,.55)`; a trailing badge — hover-spring **triggered by the row** (`translateX 0→5px`, `{300,18}`) `size:2.5rem; display:grid; place-items:center; border-radius:9999px; background:#0a0a0a; color:#fff` (sm `size:3rem`) with ArrowUpRight.
- **01 Software Development** — "Scalable web & mobile products built to last."
- **02 Product Design** — "Interfaces that feel effortless and look sharp."
- **03 Quality Assurance** — "Rigorous testing for flawless, confident releases."
- **04 Consulting** — "Strategy and direction for ambitious teams."
---
### 7) Stats (black panel, scroll count-up)
`section` `background:#fff`. `.shell` `padding:0 1.25rem 5rem` (sm px 2rem; lg pb 7rem). Inside, a reveal panel (`translateY(40px) scale(.99→1)`, `{180,26}`): `border-radius:2rem; background:#0a0a0a; padding:3rem 1.5rem; color:#fff` (sm `padding:4rem 2rem`, md `padding-inline:4rem`).
- **Eyebrow** tone `light` (`color:rgba(255,255,255,.7)`, dot `rgba(255,255,255,.6)`) **"By the numbers"**.
- **H2** (line reveal, easeOutCubic 900ms, delay 120ms, overflow): `margin-top:1rem; max-width:20ch; font-size:1.875rem; font-weight:500; letter-spacing:-.01em` (md 2.25rem) → **"Proof in the work, not the words."**
- **Grid** `margin-top:3.5rem; display:grid; grid-template-columns:repeat(2,1fr); column-gap:2rem; row-gap:3rem` (lg 4 cols). Each stat is a reveal `li` (`translateY(20px)`, `{200,24}`, delay `index*90ms`): a `font-size:3rem; font-weight:600; letter-spacing:-.02em` number (sm 3.75rem, md 4.5rem) followed by `<suffix>`, then `margin-top:.75rem; font-size:.875rem; color:rgba(255,255,255,.55)` label.
- **Count-up:** drive each number with a scroll-progress trigger from **`start:"top bottom"` → `end:"center center"`** (progress 0 when the element's top hits viewport bottom, 1 when its center hits viewport center); display `round(progress * value)`. Throttle ~30ms.
- Stats: **150+** "Projects delivered", **98%** "Client retention", **12** "Years of craft" (no suffix), **40+** "Team members".
---
### 8) Footer (black, CTA + columns + watermark)
`footer` `position:relative; overflow:hidden; border-radius:2rem 2rem 0 0; background:#0a0a0a; color:#fff`. Inner `.shell` `position:relative; z-index:10; padding:5rem 1.25rem 2.5rem` (sm px 2rem; lg pt 6rem).
- **CTA row** `display:flex; flex-direction:column; gap:2rem; border-bottom:1px solid rgba(255,255,255,.1); padding-bottom:4rem` (lg: `flex-direction:row; align-items:flex-end; justify-content:space-between`):
- **H2** (line reveal, `lineStagger 100ms`, easeOutCubic 900ms, overflow): `max-width:16ch; font-size:2.25rem; font-weight:600; letter-spacing:-.02em` (sm 3rem, md 3.75rem) → **"Have a project in mind? Let's get to work."**
- Pill button **"Start a project"** (variant `light`, withArrow, arrow `up-right` → opens request modal).
- **Columns** `display:grid; grid-template-columns:1fr; gap:3rem; padding-block:4rem` (md 2 cols, lg 4 cols):
- Brand col: `[LogoMark 1.25rem] Lumora` (`font-size:1.125rem; font-weight:600`) over `max-width:20rem; font-size:.875rem; color:rgba(255,255,255,.55)` tagline **"An independent studio crafting brands, products, and the systems that connect them."**
- **Company:** About (#about), Careers (#careers), Partners (#partners), Contact (#contact).
- **Services:** Development (#development), Design (#design), Quality Assurance (#qa), Consulting (#consulting).
- **Social:** X / Twitter, Behance, Dribbble, LinkedIn.
- Column titles: `font-size:.75rem; text-transform:uppercase; letter-spacing:.025em; color:rgba(255,255,255,.4)`. Links: `font-size:.875rem`, each an **AnimatedLink** = hover-spring span (`translateX 0→4px, opacity .65→1`, `{320,22}`).
- **Legal bar** `display:flex; flex-direction:column; align-items:center; justify-content:space-between; gap:1rem; border-top:1px solid rgba(255,255,255,.1); padding-top:2rem; font-size:.75rem; color:rgba(255,255,255,.45)` (sm row): left **"© 2025 Lumora Studio. All rights reserved."**; right a `gap:1.5rem` row of AnimatedLinks (`translateX 0→3px, opacity .7→1`) **Privacy** (#privacy), **Terms** (#terms).
- **Watermark** `position:absolute; inset-inline:0; bottom:-1.5rem; z-index:0; text-align:center; pointer-events:none; user-select:none; font-weight:700; line-height:1; font-size:13rem; color:rgba(255,255,255,.05)` → **LUMORA**.
---
### 9) NavMenu (full-screen overlay)
Opened by the header Menu button (and not present until opened). `position:fixed; inset:0; z-index:115; display:flex; flex-direction:column; background:#0a0a0a; color:#fff`. Open/close = fade `opacity 0↔1`, `{280,32}`. On open: `stopScroll()`, listen for `Escape` to close; on close: `startScroll()`.
- **Top bar** `.shell` `display:flex; align-items:center; justify-content:space-between; padding:1.25rem` (sm `1.5rem 2rem`): left `[LogoMark 1.25rem color:#cf8047] Lumora` (`font-size:1.125rem; font-weight:600`); right a **Close** button `display:inline-flex; gap:.5rem; border:1px solid rgba(255,255,255,.15); border-radius:.875rem; padding:.5rem 1rem; font-size:.75rem; font-weight:500; text-transform:uppercase; letter-spacing:.05em; color:rgba(255,255,255,.7); hover border:rgba(255,255,255,.4) color:#fff` → `[X .875rem] Close`.
- **Nav** `.shell` `flex:1; display:flex; flex-direction:column; justify-content:center`. `ul gap:.25rem`. Each item a full-width button (`display:flex; gap:1rem; padding-block:.5rem; text-align:left; font-size:2.25rem; font-weight:600; letter-spacing:-.02em`, sm 3.75rem) that **staggers in** on open: `transition:all .5s ease-out; transition-delay:<index*45 + 80>ms`; entered state = `translateY(0) opacity 1`, initial = `translateY(1rem) opacity 0`. Each contains a small index `0<n>` (`font-size:1rem; font-weight:400; color:rgba(255,255,255,.3)`, group-hover → `color:#cf8047`) and the label (`color:rgba(255,255,255,.7)`, group-hover → `#fff`, `transition .3s`). Items: **Home, Work, Services, Studio, Careers, Contact** (same routing as header; Contact opens the request modal; clicking any item closes the menu first).
- **Bottom bar** `.shell` `display:flex; flex-direction:column; gap:.75rem; border-top:1px solid rgba(255,255,255,.1); padding:1.5rem 1.25rem; font-size:.75rem; text-transform:uppercase; letter-spacing:.025em; color:rgba(255,255,255,.45)` (sm row, justify-between, px 2rem): left **"Local time — <live time>"** (fallback just "Local time"); right a button **"Start a project →"** (`color:rgba(255,255,255,.7); hover underline + #fff`) → closes menu + opens request modal.
---
### 10) RequestModal (stubbed submit)
Opened by any "Contact / Let's Talk / Start a project / Send request" CTA. Backdrop: `position:fixed; inset:0; z-index:110; display:flex; align-items:flex-end; justify-content:center; padding:1rem; background:rgba(17,17,17,.3); backdrop-filter:blur(16px)` (sm `align-items:center`), `role=dialog aria-modal=true`. Click the backdrop to close; `Escape` closes; on open `stopScroll()`, on close `startScroll()`. Enter/exit: panel `opacity 0↔1` + `translateY(28px → 0 → 18px)`, `{260,30}`.
Panel: `position:relative; width:100%; max-width:32rem; overflow:hidden; border-radius:2rem; background:#fff; padding:1.5rem; box-shadow:2xl; ring 1px #e6e5e2` (sm padding 2rem). `stopPropagation` on panel click. Top-right Close button `position:absolute; right:1rem; top:1rem; size:2.25rem; display:grid; place-items:center; border-radius:9999px; background:#f1f0ee; color:rgba(17,17,17,.6); hover bg:#e3e2df color:#111` with X icon.
**Default (form) state:**
- Heading block `margin-bottom:1.5rem; gap:.375rem`: a `inline-flex; gap:.5rem; font-size:.875rem; font-weight:500; color:rgba(17,17,17,.6)` row with a `.375rem` accent dot (`background:#b15f2c`) + **"Start a project"**; then `h2 font-size:1.5rem; font-weight:600; letter-spacing:-.01em` (sm 1.875rem) → **"Tell us what you're building."**
- Form `display:flex; flex-direction:column; gap:1rem`. Three fields, each a `label` with a `font-size:.75rem; font-weight:500; uppercase; letter-spacing:.025em; color:rgba(17,17,17,.5)` caption above the control. Controls: `width:100%; border:1px solid #e6e5e2; background:rgba(241,240,238,.5); border-radius:.875rem; padding:.75rem 1rem; font-size:.875rem; outline:none; transition; focus → border:rgba(17,17,17,.3) background:#fff`.
- **Name** (text, required, placeholder "Your name")
- **Email** (email, required, placeholder "you@company.com")
- **Project** (textarea, 4 rows, required, `resize:none`, placeholder "A few words about your project, timeline, and budget.")
- Bottom row `margin-top:.5rem; display:flex; align-items:center; justify-content:space-between; gap:1rem`: a `font-size:.75rem; color:rgba(17,17,17,.45)` note **"We reply within one business day."** and a pill button **"Send request"** (variant `dark`, withArrow, arrow `up-right`, `type=submit`; while submitting label = "Sending…").
- **Submit is a no-op/stub:** prevent default, briefly show "Sending…", then switch to the **success** state (no network call).
**Success state** (centered, `padding-block:2rem; gap:1rem`): a `size:3.5rem; display:grid; place-items:center; border-radius:9999px; background:#0a0a0a; color:#cf8047; font-size:1.5rem` LogoMark badge; `h2 font-size:1.5rem; font-weight:600` **"Request received"**; `max-width:32ch; font-size:.875rem; color:rgba(17,17,17,.6)` **"Thanks for reaching out — we'll get back to you within one business day."**; a pill button **"Close"** (variant `dark`) that closes the modal. Reset the form ~300ms after the modal closes.
---
## Shared component recipes
**PillButton** — `inline-block`, wraps a hover-spring scale (`scale 1→1.04`, `{320,18}`, triggered by the root). Inner pill = `inline-flex; align-items:center; gap:.75rem; border-radius:9999px; font-size:.875rem; font-weight:500`. Variants: `dark` = `background:#0a0a0a; color:#fff`; `light` = `background:#f1f0ee; color:#111`; `outline` = `border:1px solid #e6e5e2; background:transparent; color:#111`. Padding: with arrow = `.375rem .375rem .375rem 1.5rem` (`py-1.5 pl-6 pr-1.5`); without arrow = `.875rem 1.75rem` (`py-3.5 px-7`). With arrow: append a `size:2.25rem; display:grid; place-items:center; border-radius:9999px; font-size:1rem` badge (dark variant badge = `background:#fff; color:#0a0a0a`; other variants = `background:#0a0a0a; color:#fff`) whose icon hover-springs (triggered by the button root): `right` arrow shifts `translate(3px,0)`, `up-right` arrow shifts `translate(2px,-2px)`, `{320,18}`. Icon = ArrowRight or ArrowUpRight per `arrow`.
**Eyebrow** — `inline-flex; align-items:center; gap:.5rem; font-size:.875rem; font-weight:500`. Leading `.375rem` round dot. `dark` tone: text `rgba(17,17,17,.7)`, dot `rgba(17,17,17,.5)`. `light` tone: text `rgba(255,255,255,.7)`, dot `rgba(255,255,255,.6)`.
**TagChip** — `inline-flex; border:1px solid; border-radius:9999px; padding:.5rem 1rem; font-size:.875rem`. `light` tone (on dark cards) = `border:rgba(255,255,255,.25); color:#fff`.
**AnimatedLink** — `inline-flex` link wrapping a hover-spring span (default `translateX 0→4px, opacity .65→1`, `{320,22}`; footer legal links override to `translateX 0→3px, opacity .7→1`).
**Hover behavior note** — all hover springs are disabled on touch/mobile (no pointer:hover). On desktop they spring to the `to` state on `mouseenter` and back to `from` on `mouseleave` (a few are triggered by a parent element rather than self — noted inline above).
---
## Assets
`ASSET_BASE_URL = https://api.getlayers.ai/storage/v1/object/public/public/assets/lumora-e8b711fc68`
Load these from the public bucket (NOT relative paths):
| Original path | Full URL | Role in hero |
|---|---|---|
| `/assets/hero/before.jpg` | `https://api.getlayers.ai/storage/v1/object/public/public/assets/lumora-e8b711fc68/hero/before.jpg` | painted on cursor trail (`afterSrc`) |
| `/assets/hero/after.jpg` | `https://api.getlayers.ai/storage/v1/object/public/public/assets/lumora-e8b711fc68/hero/after.jpg` | always-shown base image (`beforeSrc`) |
**Mapping note (preserve exactly):** in the LiquidReveal, `beforeSrc` (the always-visible base / LCP image) = `.../hero/after.jpg`, and `afterSrc` (the brush-revealed image) = `.../hero/before.jpg`. The file named `after.jpg` shows by default; the file named `before.jpg` is revealed under the cursor. Do not swap which file sits on which layer.
---
## Fixed parameters (bake these in)
- **Colors:** `#ffffff #111111 #0a0a0a #8d8d8d #b6b6b6 #e6e5e2 #f1f0ee #e3e2df #b15f2c #cf8047 #97501f #ecebe9 #c9c9c9`. theme-color `#0a0a0a`.
- **Font:** Google **Onest**, weights 400/500/600/700. Fallback `sans-serif`.
- **Radii:** pill `9999px`, card `2rem`, card-sm `1.25rem`, control `.875rem`. Watermark font-size `13rem`. Shell max-width `88rem`.
- **Breakpoints:** sm `640px`, md `768px`, lg `1024px`; adaptive-grid base widths `1920/1440/1024/360`, scale-up base `1920`, coef `0.6666`, FONT_BASE `16`.
- **Loader:** `FILL_MS = 1300ms`, easeInOutCubic count, counter zero-padded to 3 digits, exit slide `translateY 0→-100%` `{220,30}`, content fade `{260,26}`, progress track `1px`, fill `#cf8047`.
- **LiquidReveal:** `brushRadius 143`, `decay 0.016`, dpr `min(dpr,2)`, fadeFrames `120`, idle fade ramp `+idle*0.004` capped `0.5`, brush gradient stops `1 / 0.82 / 0` at `0 / 0.55 / 1`, step `max(radius*0.3,1)`, max interp points `60`.
- **Reveal easings:** lines `easeOutCubic`≈`cubic-bezier(.215,.61,.355,1)` 900ms; words `easeOutQuart`≈`cubic-bezier(.165,.84,.44,1)` 700ms (`wordStagger 35`); hero line `lineStagger 120`, footer CTA `lineStagger 100`.
- **Entrance delays (ms after `ready`):** header 150, hero eyebrow 200, hero H1 250, hero card 400, partners 550, rating 650, CTAs 750, status bar 900, watermark 300. CreateBand `index*120`, Portfolio cards `index*90`, Services rows `index*80`, Stats `index*90`.
- **Spring configs (tension/friction):** header `210/26`; hovers (logo/menu/badges/pills) `320/18`; nav-item & link lifts `320/22`; social icon `320/16`; partner `320/20`; create tile `300/18`; service row fill `240/26`, service arrow `300/18`; portfolio card `260/22` + badge `280/18`; portfolio reveal `180/26`; create/partners/heroCard/stats-item reveal `200/24`; stats panel `180/26`; hero card carousel `300/28`; nav overlay fade `280/32`; modal `260/30`.
- **Stats count-up trigger:** `start "top bottom"`, `end "center center"`, value = `round(progress*target)`, throttle ~30ms.
- **Clock:** updates every 1s; time `H:MMam/pm` (no leading hour zero, minutes padded), date `D Month, YYYY`; fallback `9:41am` / `12 March, 2025`.
- **Copy / labels (verbatim):** brand "Lumora"; loader tagline "Bold ideas, shipped with quiet precision."; status bar "Local time" / time / date; nav Home/Work/Services/Studio/Careers/Contact; hero eyebrow "Independent Studio", watermark "LUMORA", heading "Bold ideas," / "shipped with" / "quiet precision", rating "200+ brands shipped", CTAs "Let's Talk" / "View Work", card items + partners + hero footer ("Working since 2014" / "Remote-first, worldwide" / "Scroll to explore") as listed; About all copy as listed; CreateBand We/Build/→/Better; Portfolio "Portfolio" / "Selected Work" + 4 items; Services "Services" / "What we do best" + 4 rows; Stats "By the numbers" / "Proof in the work, not the words." + 4 stats; Footer CTA "Have a project in mind? Let's get to work." + "Start a project", tagline, 3 link columns, legal "© 2025 Lumora Studio. All rights reserved." + Privacy/Terms; modal "Start a project" / "Tell us what you're building." / fields / "We reply within one business day." / "Send request" / success "Request received" / "Thanks for reaching out — we'll get back to you within one business day."


▸Еще один вкусный кейс

# Recreate this site as a single HTML file: Laocoön — Bronze and Time
You are an expert creative front-end developer. Produce a **single self-contained `index.html`**
that reproduces the project below **exactly** — same layout, sections, visuals, motion, and
interaction. Pure HTML/CSS/JS in one file: no build step, no framework, no bundler. Use ES modules
with a CDN importmap for Three.js (r0.160 from unpkg, with the matching `GLTFLoader` addon).
Hardcode every value given here as a fixed constant. Rebuild each component described below as a
section of the one file.
## What it is
A cinematic, scroll-driven landing page titled **"Bronze and Time"** centered on a glowing,
semi-polished **bronze horse** sculpture rendered in WebGL on a deep-black stage. The camera flies
a full 360° orbit around the statue as you scroll through a very tall page (900vh); molten **forge
sparks** rise around it; a slow **liquid-bronze wave shader** breathes in the background, shifting
its palette from molten bronze at the top to deep sapphire blue at the bottom. Over the canvas sits
an editorial overlay: a fixed header (brand + nav + Contact button), four text "slides" that fade in
at scroll milestones with per-letter blur-up animation, a five-line vertical grid with drifting
dots, a stories-style scroll progress bar, and a custom double-ring cursor. Fonts are *Italiana*
(display headings) and *Outfit* (body). The whole thing is dark, slow, and luxurious.
## Page shell & libraries
- `<!DOCTYPE html>`, `lang="en"`, `<title>The Statue of Laocoön</title>`, UTF-8, responsive viewport.
- Load fonts in CSS:
`@import url('https://fonts.googleapis.com/css2?family=Italiana&family=Outfit:wght@300;400;600&family=Playfair+Display:ital,wght@1,500&display=swap');`
- Importmap + module script for Three.js:
```html
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/examples/jsm/loaders/GLTFLoader.js": "https://unpkg.com/three@0.160.0/examples/jsm/loaders/GLTFLoader.js"
}
}
</script>
<script type="module"> /* all JS below */ </script>
```
- The page scroll model: `<body>` has `min-height: 900vh` so the page is very tall; the canvas and
all overlay UI are `position: fixed`. Scroll position drives everything (camera orbit, shader,
sparks turbulence, slide reveals, grid dots, progress bar). No smooth-scroll library — smoothing is
done in JS via lerp (see animation loop). All scrollbars are fully hidden (WebKit, Firefox, IE).
- The native cursor is hidden everywhere (`cursor: none` on body and `cursor: none !important` on
`*`), replaced by a custom two-element cursor.
## Global CSS (reset + base)
```css
* { margin: 0; padding: 0; box-sizing: border-box; user-select: none; }
html, body {
width: 100%;
margin: 0;
padding: 0;
background-color: #000000;
-ms-overflow-style: none;
scrollbar-width: none;
}
body {
min-height: 900vh;
font-family: 'Outfit', sans-serif;
color: #ffffff;
overflow-x: hidden;
cursor: none;
}
* { cursor: none !important; }
::-webkit-scrollbar { display: none; width: 0; }
#webgl {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
z-index: 1;
outline: none;
}
```
## Layout & sections (in order)
The body markup, in order: two cursor divs, the `.cinematic-container` (header + slides), an image
mask, the grid-lines overlay, and the `<canvas id="webgl">`. Build them all.
### Custom cursor (two divs, top of `<body>`)
```html
<div class="cursor-inner"></div>
<div class="cursor-outer"></div>
```
```css
.cursor-inner {
position: fixed; top: 0; left: 0;
width: 6px; height: 6px;
border: 2px solid rgba(255, 255, 255, 1);
border-radius: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
z-index: 9999;
}
.cursor-outer {
position: fixed; top: 0; left: 0;
width: 40px; height: 40px;
border: 1.5px solid rgba(255, 255, 255, 0.7);
border-radius: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
z-index: 9998;
transition: width 0.3s, height 0.3s, background 0.3s;
}
```
The inner ring snaps to the pointer instantly; the outer ring lerps toward it (see JS).
### `.cinematic-container` (the overlay layer)
```css
.cinematic-container {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
z-index: 10;
pointer-events: none; /* clicks pass through to canvas */
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 0 60px 40px 60px;
box-sizing: border-box;
}
```
### Header `.main-header`
```html
<div class="main-header">
<div class="brand">Laocoön</div>
<nav class="header-nav">
<a href="#slide-1" class="nav-link">Bronze</a>
<span class="nav-dot"></span>
<a href="#slide-2" class="nav-link">Marble</a>
<span class="nav-dot"></span>
<a href="#slide-3" class="nav-link">Fluid</a>
<span class="nav-dot"></span>
<a href="#slide-4" class="nav-link">Digital</a>
</nav>
<div class="header-actions">
<a href="#" class="contact-btn">Contact <span class="btn-circle"></span></a>
</div>
</div>
```
```css
.main-header {
display: flex; justify-content: space-between; align-items: center;
width: 100%; z-index: 20;
pointer-events: auto;
padding: 20px 0;
}
.brand { font-size: 14px; font-weight: 600; letter-spacing: 5px; text-transform: uppercase; }
.header-nav { display: flex; align-items: center; gap: 24px; }
.nav-link {
color: #ffffff; text-decoration: none;
font-size: 10px; text-transform: uppercase; letter-spacing: 2px;
font-weight: 500; transition: color 0.3s;
}
.nav-link:hover { color: #ffffff; }
.nav-dot { width: 3px; height: 3px; background: rgba(255, 255, 255, 0.2); border-radius: 50%; }
.contact-btn {
display: inline-flex; align-items: center; gap: 8px;
padding: 10px 24px;
background: #ffffff; color: #000000;
text-decoration: none;
font-size: 10px; text-transform: uppercase; letter-spacing: 1px; font-weight: 600;
border-radius: 20px;
transition: transform 0.3s, background 0.3s;
}
.btn-circle {
display: inline-block; width: 6px; height: 6px;
border: 1px solid #000000; border-radius: 50%; background: transparent;
}
.contact-btn:hover { transform: scale(1.05); background: #f0f0f0; }
```
### The four slides + the image mask
```html
<div class="slide" id="slide-1">
<h2 class="slide-title">Bronze <br>and Time</h2>
<div class="desc-row">
<p class="slide-desc col-1">A timeless material holding centuries of human history. Fluid in hot flames, eternal in its form. Each curve captures a tense, dramatic moment.</p>
<p class="slide-desc col-2">Born of molten fire and creative will, it stands to bridge our ancient memory and modern vision. A fluid energy frozen in still, heavy bronze.</p>
</div>
</div>
<div class="slide-image-mask" id="slide-2-img">
<img src="ASSET_BASE_URL/1.png" alt="Editorial Concept">
</div>
<div class="slide" id="slide-2">
<h2 class="slide-title">Marble <br>Emotion</h2>
<p class="slide-desc">A sculpture frozen at the peak of human suffering and heroic struggle. Laocoön and his sons, bound by ruthless fate.</p>
</div>
<div class="slide" id="slide-3">
<h2 class="slide-title">Liquid Metal</h2>
<p class="slide-desc">Art that breathes. Mesmerizing waves of liquid bronze flow through space, reflecting every contour and tensed muscle of the ancient masterpiece.</p>
</div>
<div class="slide" id="slide-4">
<h2 class="slide-title">Eternal <br>Moment</h2>
<p class="slide-desc">Contemplating antique form through the lens of new dimensions. A classic masterpiece reborn in the currents of radiant digital matter.</p>
</div>
```
(Use the exact verbatim copy above. The `1.png` image uses the bucket URL — see Assets.)
```css
.slide { position: absolute; bottom: 12%; pointer-events: none; }
#slide-1 { left: 0; width: 100%; }
#slide-1 .slide-title { margin-left: 60px; }
#slide-1 .desc-row { position: relative; display: flex; width: 100%; }
#slide-1 .col-1 {
margin-left: 60px;
width: calc(25vw - 60px);
max-width: calc(25vw - 60px);
}
#slide-1 .col-2 {
position: absolute;
left: calc(25vw + 40px);
width: calc(25vw - 60px);
max-width: calc(25vw - 60px);
}
#slide-2 { left: 0; width: 100%; }
#slide-2-img {
position: fixed;
top: 90px;
left: 60px;
width: calc(25vw - 60px);
aspect-ratio: 1 / 1;
overflow: hidden;
z-index: 2;
pointer-events: none;
clip-path: inset(0 0 100% 0);
transition: clip-path 1.8s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.8s ease;
opacity: 0;
}
#slide-2-img img {
width: 100%; height: 100%;
object-fit: cover;
transform: scale(1.15);
transition: transform 1.8s cubic-bezier(0.16, 1, 0.3, 1);
}
#slide-2-img.active { clip-path: inset(0 0 0 0); opacity: 1; }
#slide-2-img.active img { transform: scale(1); }
#slide-2 .slide-title { margin-left: calc(25vw + 40px); }
#slide-2 .slide-desc {
margin-left: calc(50vw + 20px);
width: calc(25vw - 60px);
max-width: calc(25vw - 60px);
}
#slide-3 {
left: calc(50vw + 20px);
width: calc(25vw - 60px);
max-width: calc(25vw - 60px);
}
#slide-3 .slide-title { white-space: nowrap; }
#slide-4 { left: calc(25% + 40px); max-width: 600px; }
.slide-title {
font-family: 'Italiana', serif;
font-size: 116px;
line-height: 1.0;
font-weight: 400;
letter-spacing: 2px;
margin-bottom: 20px;
color: #fff6ed;
}
.slide-desc {
font-size: 16px; line-height: 1.7; font-weight: 300;
color: #d1d5db; letter-spacing: 0.5px;
opacity: 0;
transform: translateY(30px);
transition: opacity 0.8s cubic-bezier(0.25, 1, 0.5, 1),
transform 0.8s cubic-bezier(0.25, 1, 0.5, 1);
transition-delay: 0.4s;
}
.slide.active .slide-desc { opacity: 1; transform: translateY(0); }
.slide-title .char {
display: inline-block;
opacity: 0;
transform: translateY(50px);
filter: blur(12px);
transition: opacity 0.8s cubic-bezier(0.25, 1, 0.5, 1),
transform 0.8s cubic-bezier(0.25, 1, 0.5, 1),
filter 0.8s cubic-bezier(0.25, 1, 0.5, 1);
}
.slide.active .slide-title .char { opacity: 1; transform: translateY(0); filter: blur(0); }
.slide.active { pointer-events: auto; }
```
### Grid overlay (horizontal divider + 5 vertical lines with dots + stories progress)
```html
<div class="grid-horizontal-line"></div>
<div class="grid-lines">
<div class="grid-line"><div class="grid-dot top"></div><div class="grid-dot bottom"></div></div>
<div class="grid-line"><div class="grid-dot top"></div><div class="grid-dot bottom"></div></div>
<div class="grid-line"><div class="grid-dot top"></div><div class="grid-dot bottom"></div></div>
<div class="grid-line"><div class="grid-dot top"></div><div class="grid-dot bottom"></div></div>
<div class="grid-line story-progress-container">
<div class="grid-dot top"></div>
<div class="grid-dot bottom"></div>
<div class="story-dashes">
<div class="story-dash"><div class="story-dash-fill" id="dash-fill-1"></div></div>
<div class="story-dash"><div class="story-dash-fill" id="dash-fill-2"></div></div>
<div class="story-dash"><div class="story-dash-fill" id="dash-fill-3"></div></div>
<div class="story-dash"><div class="story-dash-fill" id="dash-fill-4"></div></div>
</div>
</div>
</div>
```
```css
.story-dashes {
position: absolute; top: 50%; left: 50%;
transform: translate(-50%, -50%);
display: flex; flex-direction: column; gap: 12px;
}
.story-dash { width: 2px; height: 40px; background: rgba(255, 255, 255, 0.15); border-radius: 2px; }
.story-dash-fill { width: 100%; height: 0%; background: #ffffff; border-radius: 2px; }
.grid-horizontal-line {
position: fixed; top: 70px; left: 0; right: 0;
width: 100%; height: 1px;
background: rgba(255, 255, 255, 0.12);
z-index: 5; pointer-events: none;
}
.grid-lines {
position: fixed; top: 0; left: 40px; right: 40px;
width: calc(100% - 80px); height: 100vh;
display: flex; justify-content: space-between;
z-index: 5; pointer-events: none;
}
.grid-line {
position: relative; width: 1px; height: 100%;
background: rgba(255, 255, 255, 0.12);
}
.grid-line:nth-child(3) { margin-top: 70px; height: calc(100vh - 70px); }
.grid-dot {
position: absolute; left: 50%;
transform: translate(-50%, -50%);
width: 6px; height: 6px; border-radius: 50%;
background: rgba(255, 255, 255, 0.35);
box-shadow: 0 0 4px rgba(255, 255, 255, 0.15);
}
```
### Canvas (last element)
```html
<canvas id="webgl"></canvas>
```
## The WebGL scene
All of the following runs inside the module script. Globals to declare at top:
```js
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
const canvas = document.querySelector('#webgl');
let scene, camera, renderer;
let gltfModel; // the statue
let modelPivot; // pivot group for perfect center-rotation
let mixer;
const clock = new THREE.Clock();
let currentScroll = 0; // smoothed accumulated scroll for lerping
let mouseX = 0, mouseY = 0, targetMouseX = 0, targetMouseY = 0;
let cursorX = window.innerWidth / 2, cursorY = window.innerHeight / 2;
let outerCursorX = window.innerWidth / 2, outerCursorY = window.innerHeight / 2;
let bgMaterial;
const shaderUniforms = {
uTime: { value: 0 },
uResolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) },
uMouse: { value: new THREE.Vector2(0, 0) },
uScroll: { value: 0 }
};
let sparkParticles;
const sparkCount = 450;
const sparkData = [];
const sizes = { width: window.innerWidth, height: window.innerHeight };
```
### Renderer, scene, camera (verbatim setup)
```js
scene = new THREE.Scene();
scene.background = new THREE.Color('#000000');
scene.fog = new THREE.FogExp2('#000000', 0.01);
camera = new THREE.PerspectiveCamera(50, sizes.width / sizes.height, 0.1, 100);
camera.position.set(0, 0.2, 3.0);
scene.add(camera);
createBackgroundShader(); // adds the wave plane as a child of the camera
renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true,
alpha: false,
powerPreference: "high-performance"
});
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 2.2;
```
### Dramatic lighting (chiaroscuro) — verbatim
```js
const ambientLight = new THREE.AmbientLight('#ffffff', 0.1);
scene.add(ambientLight);
// Key light: super-bright white from upper-right, casts shadows
const keyLight = new THREE.SpotLight('#ffffff', 18.0);
keyLight.position.set(4, 6, 3);
keyLight.angle = Math.PI / 4;
keyLight.penumbra = 0.9;
keyLight.castShadow = true;
keyLight.shadow.mapSize.width = 2048;
keyLight.shadow.mapSize.height = 2048;
keyLight.shadow.camera.near = 1.0;
keyLight.shadow.camera.far = 15;
keyLight.shadow.bias = -0.001;
scene.add(keyLight);
// Rim light: cool blue from behind-left, defines the silhouette
const rimLight = new THREE.DirectionalLight('#e3f2ff', 10.0);
rimLight.position.set(-5, 3, -4);
scene.add(rimLight);
// Fill light: very faint warm cream from below-front
const fillLight = new THREE.DirectionalLight('#fff3e6', 0.8);
fillLight.position.set(-2, -4, 2);
scene.add(fillLight);
```
### Forge sparks (450 additive point particles) — verbatim
Procedural radial-gradient texture (white core → transparent edge):
```js
function createSparkTexture() {
const canvas = document.createElement('canvas');
canvas.width = 16; canvas.height = 16;
const ctx = canvas.getContext('2d');
const gradient = ctx.createRadialGradient(8, 8, 0, 8, 8, 8);
gradient.addColorStop(0, 'rgba(255, 255, 255, 1)');
gradient.addColorStop(0.25, 'rgba(255, 255, 255, 0.85)');
gradient.addColorStop(0.6, 'rgba(255, 255, 255, 0.3)');
gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 16, 16);
return new THREE.CanvasTexture(canvas);
}
```
Build the particle cloud — random positions in a box around the model, colors split 60% molten
orange / 40% icy blue, and per-particle physics stored in `sparkData`:
```js
function createSparks() {
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(sparkCount * 3);
const colors = new Float32Array(sparkCount * 3);
for (let i = 0; i < sparkCount; i++) {
const x = (Math.random() - 0.5) * 6.5;
const y = (Math.random() - 0.5) * 5.0 - 0.5;
const z = (Math.random() - 0.5) * 6.5;
positions[i * 3] = x;
positions[i * 3 + 1] = y;
positions[i * 3 + 2] = z;
if (Math.random() < 0.6) {
// saturated fiery orange
colors[i * 3] = 1.0;
colors[i * 3 + 1] = 0.4 + Math.random() * 0.15;
colors[i * 3 + 2] = 0.05 + Math.random() * 0.1;
} else {
// cosmic icy light-blue (matches rim light)
colors[i * 3] = 0.55 + Math.random() * 0.15;
colors[i * 3 + 1] = 0.82 + Math.random() * 0.12;
colors[i * 3 + 2] = 1.0;
}
sparkData.push({
speedX: (Math.random() - 0.5) * 0.4,
speedY: 0.15 + Math.random() * 0.3,
speedZ: (Math.random() - 0.5) * 0.4,
swaySpeed: 0.5 + Math.random() * 1.5,
swayRadius: 0.05 + Math.random() * 0.15,
phase: Math.random() * Math.PI * 2
});
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const material = new THREE.PointsMaterial({
size: 0.025,
vertexColors: true,
transparent: true,
opacity: 0.85,
blending: THREE.AdditiveBlending,
depthWrite: false,
map: createSparkTexture()
});
sparkParticles = new THREE.Points(geometry, material);
scene.add(sparkParticles);
}
```
### Model load + centering/scaling logic — verbatim
Load the GLB from the bucket (see Assets). Wrap it in a pivot group, set the bronze material
(roughness **0.42**, metalness **0.92**, smooth shading), then auto-scale so the largest dimension
is 3.5 units, recompute the scaled bounding box, recenter the model inside the pivot, and drop the
pivot to `y = -0.4`:
```js
function loadModel() {
const loader = new GLTFLoader();
loader.load(
ASSET_BASE_URL + '/bronze_horse.glb',
(gltf) => {
gltfModel = gltf.scene;
modelPivot = new THREE.Group();
scene.add(modelPivot);
modelPivot.add(gltfModel);
gltfModel.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
if (child.material) {
child.material.roughness = 0.42; // semi-matte, noble highlights
child.material.metalness = 0.92; // high satin-bronze metallic sheen
child.material.flatShading = false;
if (child.material.map) {
child.material.map.anisotropy = 16;
}
}
}
});
if (gltf.animations && gltf.animations.length > 0) {
mixer = new THREE.AnimationMixer(gltfModel);
gltf.animations.forEach((clip) => { mixer.clipAction(clip).play(); });
}
// 1. scale so max dimension = 3.5
const boxInitial = new THREE.Box3().setFromObject(gltfModel);
const sizeInitial = boxInitial.getSize(new THREE.Vector3());
const maxDim = Math.max(sizeInitial.x, sizeInitial.y, sizeInitial.z);
const targetScale = 3.5 / (maxDim > 0.0001 ? maxDim : 1);
gltfModel.scale.setScalar(targetScale);
// 2. update world matrix so the box accounts for scale
gltfModel.updateMatrixWorld(true);
// 3. exact geometric center of the scaled model
const boxScaled = new THREE.Box3().setFromObject(gltfModel);
const centerScaled = boxScaled.getCenter(new THREE.Vector3());
// 4. recenter model so its center sits at the pivot origin
gltfModel.position.sub(centerScaled);
// 5. lower the pivot for a grounded stance
modelPivot.position.y = -0.4;
},
undefined,
(error) => { console.error('Error loading bronze horse model:', error); }
);
}
```
### Background liquid-bronze wave shader (full-screen plane, child of camera) — VERBATIM
```js
function createBackgroundShader() {
const vertexShader = `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShader = `
varying vec2 vUv;
uniform float uTime;
uniform vec2 uResolution;
uniform vec2 uMouse;
uniform float uScroll;
// 3D Noise function for organic warping (high performance hash-based noise)
float hash(float n) { return fract(sin(n) * 43758.5453123); }
float noise(in vec3 x) {
vec3 p = floor(x);
vec3 f = fract(x);
f = f*f*(3.0-2.0*f);
float n = p.x + p.y*57.0 + 113.0*p.z;
return mix(mix(mix(hash(n+ 0.0), hash(n+ 1.0), f.x),
mix(hash(n+ 57.0), hash(n+ 58.0), f.x), f.y),
mix(mix(hash(n+113.0), hash(n+114.0), f.x),
mix(hash(n+170.0), hash(n+171.0), f.x), f.y), f.z);
}
void main() {
// Normalize UV coordinates
vec2 uv = (gl_FragCoord.xy - 0.5 * uResolution.xy) / uResolution.y;
float aspect = uResolution.x / uResolution.y;

float time = uTime * 0.08; // Extremely slow, calm breathing speed
float scroll = uScroll;

// --- 1. Constant Wave Parameters (Perfect Scroll Stability) ---
// Keep frequencies and angles constant to prevent compressing/speeding waves on scroll
float angle1 = 0.6;
float angle2 = -0.7;
float angle3 = 1.2;

float freq1 = 2.4; // Higher frequency = significantly more lines on screen
float freq2 = 3.2;
float freq3 = 4.0;

// --- 2. Coordinates Distortion (Smooth Rounded Arcs & Scroll Deformation) ---
vec2 warpedUv = uv;

// Scroll directly drives the deformation of the wave shapes!
// As you scroll, the phase of the distortion shifts, smoothly bending the rounded lines.
float scrollDeform = scroll * 5.0; // 5 radians of smooth sweeping deformation

// Large scale, perfectly smooth sine-wave distortions for clean, even, rounded arcs
warpedUv.x += sin(uv.y * 2.5 + time * 0.2 + scrollDeform) * 0.35;
warpedUv.y += cos(uv.x * 2.5 - time * 0.15 - scrollDeform * 0.8) * 0.35;

// Secondary clean curve to create elegant, topographical intersections
warpedUv.x += sin(uv.y * 1.2 - time * 0.1 - scrollDeform * 1.5) * 0.25;
warpedUv.y += cos(uv.x * 1.2 + time * 0.18 + scrollDeform * 1.2) * 0.25;

// Soft global sliding shift based on scroll parallax and mouse movement
// Keep drift multipliers tiny so scroll motion remains extremely gentle
vec2 scrollDrift = vec2(scroll * 0.04, -scroll * 0.02);
vec2 mouseShift = vec2(uMouse.x * aspect * 0.05, uMouse.y * 0.05);
warpedUv += scrollDrift + mouseShift;

// --- 3. Layered Wave Computations ---
// Wave directions
vec2 dir1 = vec2(cos(angle1), sin(angle1));
vec2 dir2 = vec2(cos(angle2), sin(angle2));
vec2 dir3 = vec2(cos(angle3), sin(angle3));

// Layered sine wave calculations with self-warping
float w1 = sin(dot(warpedUv, dir1) * freq1 + time * 1.0);
float w2 = cos(dot(warpedUv, dir2) * freq2 - time * 1.4 + w1 * 0.4);
float w3 = sin(dot(warpedUv, dir3) * freq3 + time * 1.8 + w2 * 0.5);

// Combine layers into a seamless wave scalar field in [-1.0, 1.0]
float waveField = w1 * 0.50 + w2 * 0.35 + w3 * 0.15;

// --- 4. Crisp Metallic Lustre ---
// Wide satin sheen (gives defined body to the folds without haze)
float wideSheen = pow(max(0.0, 1.0 - abs(waveField - 0.1)), 2.5);

// Crisp, thicker specular reflection (defined metallic shine, ensuring thickness with more lines)
float crispSpecular = pow(max(0.0, 1.0 - abs(waveField - 0.15)), 8.0);

// Highlight blend: crisp, defined glowing folds
float crest = wideSheen * 0.5 + crispSpecular * 0.9;

// --- 5. Scroll-Driven Color Palettes: Pure Bronze to Pure Blue ---
// Top Palette (Scroll = 0.0): Rich Molten Bronze
vec3 c0_shadow = vec3(0.0010, 0.0006, 0.0004); // Deepest bronze shadow
vec3 c0_wave1 = vec3(0.085, 0.040, 0.015); // Rich copper-bronze
vec3 c0_wave2 = vec3(0.050, 0.022, 0.008); // Deep bronze body
vec3 c0_crest = vec3(0.45, 0.30, 0.18); // Polished bronze/gold sheen

// Bottom Palette (Scroll = 1.0): Deep Sapphire Blue
vec3 c1_shadow = vec3(0.0004, 0.0006, 0.0012); // Deepest sapphire shadow
vec3 c1_wave1 = vec3(0.015, 0.035, 0.065); // Rich cobalt/teal blue
vec3 c1_wave2 = vec3(0.008, 0.020, 0.045); // Deep sapphire body
vec3 c1_crest = vec3(0.18, 0.35, 0.55); // Polished blue-steel sheen

// Interpolate colors smoothly based on scroll progress
float t = smoothstep(0.0, 1.0, scroll);
vec3 colShadow = mix(c0_shadow, c1_shadow, t);
vec3 colWave1 = mix(c0_wave1, c1_wave1, t);
vec3 colWave2 = mix(c0_wave2, c1_wave2, t);
vec3 colCrest = mix(c0_crest, c1_crest, t);

// --- 6. Final Composition ---
// Blend wave details and wave bodies with deep shadow background
vec3 color = colShadow;
color = mix(color, colWave2, smoothstep(-0.6, 0.2, waveField));
color = mix(color, colWave1, smoothstep(0.0, 0.8, waveField));

// Add the soft satinated crest highlights with rich metallic multiplier
color += colCrest * crest * 1.4;

// Apply a gentle vignette to match cinematic design
float vignette = 1.0 - dot(uv, uv) * 0.12;
color *= vignette;

gl_FragColor = vec4(color, 1.0);
}
`;
bgMaterial = new THREE.ShaderMaterial({
vertexShader: vertexShader,
fragmentShader: fragmentShader,
uniforms: shaderUniforms,
depthWrite: false,
depthTest: false
});
const bgGeometry = new THREE.PlaneGeometry(30, 30);
const bgMesh = new THREE.Mesh(bgGeometry, bgMaterial);
bgMesh.position.set(0.0, 0.0, -8.0); // local camera space, far behind
bgMesh.renderOrder = -10;
camera.add(bgMesh); // attach to camera so it always fills view
}
```
### Resize handler — verbatim
```js
function onWindowResize() {
sizes.width = window.innerWidth;
sizes.height = window.innerHeight;
camera.aspect = sizes.width / sizes.height;
camera.updateProjectionMatrix();
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
if (shaderUniforms) shaderUniforms.uResolution.value.set(sizes.width, sizes.height);
}
```
## Interaction & animation loop
### Pointer handling
On `mousemove`: snap the inner cursor to `event.clientX/Y` (set its `left`/`top`); store normalized
mouse target `targetMouseX = clientX/innerWidth*2-1`, `targetMouseY = clientY/innerHeight*2-1`.
```js
window.addEventListener('mousemove', (event) => {
cursorX = event.clientX;
cursorY = event.clientY;
const cursorInner = document.querySelector('.cursor-inner');
if (cursorInner) { cursorInner.style.left = `${cursorX}px`; cursorInner.style.top = `${cursorY}px`; }
targetMouseX = (event.clientX / window.innerWidth) * 2 - 1;
targetMouseY = (event.clientY / window.innerHeight) * 2 - 1;
});
window.addEventListener('resize', onWindowResize);
```
### Per-letter title split (call before first frame)
Split each `.slide-title` into per-character `<span class="char">` with a staggered
`transition-delay` of `index * 0.035s`, preserving `<br>` tags and spaces:
```js
function splitTitlesIntoChars() {
const titles = document.querySelectorAll('.slide-title');
titles.forEach(title => {
const text = title.innerHTML;
let newHTML = '';
let delayCounter = 0;
const parts = text.split(/(<br\s*\/?>)/i);
parts.forEach(part => {
if (part.toLowerCase().startsWith('<br')) {
newHTML += part;
} else {
for (let i = 0; i < part.length; i++) {
if (part[i] === ' ') {
newHTML += ' ';
} else {
newHTML += `<span class="char" style="transition-delay: ${delayCounter * 0.035}s">${part[i]}</span>`;
delayCounter++;
}
}
}
});
title.innerHTML = newHTML;
});
}
```
### The `animate()` loop (verbatim logic)
```js
function animate() {
requestAnimationFrame(animate);
const deltaTime = clock.getDelta();
if (mixer) mixer.update(deltaTime);
// 1. target scroll [0..1], cross-browser
const maxScroll = document.documentElement.scrollHeight - window.innerHeight;
const scrollTop = window.scrollY !== undefined ? window.scrollY
: (window.pageYOffset !== undefined ? window.pageYOffset : document.documentElement.scrollTop);
const targetScroll = maxScroll > 0 ? scrollTop / maxScroll : 0;
// smooth physical lerp (inertia)
currentScroll += (targetScroll - currentScroll) * 0.025;
// smooth model rotation lerp
mouseX += (targetMouseX - mouseX) * 0.05;
mouseY += (targetMouseY - mouseY) * 0.05;
// outer cursor ring lerps toward inner
outerCursorX += (cursorX - outerCursorX) * 0.2;
outerCursorY += (cursorY - outerCursorY) * 0.2;
const cursorOuter = document.querySelector('.cursor-outer');
if (cursorOuter) { cursorOuter.style.left = `${outerCursorX}px`; cursorOuter.style.top = `${outerCursorY}px`; }
// gentle interactive model tilt from mouse
if (modelPivot) {
modelPivot.rotation.y = mouseX * 0.25;
modelPivot.rotation.x = mouseY * 0.15;
}
// 2. sparks physics, accelerated/turbulent on fast scroll
if (sparkParticles) {
const positions = sparkParticles.geometry.attributes.position.array;
const time = clock.getElapsedTime();
const scrollVelocity = Math.abs(targetScroll - currentScroll);
const speedMultiplier = 1.0 + scrollVelocity * 9.0;
const turbulence = scrollVelocity * 0.8;
for (let i = 0; i < sparkCount; i++) {
const idx = i * 3;
const data = sparkData[i];
positions[idx] += data.speedX * deltaTime * speedMultiplier;
positions[idx + 1] += data.speedY * deltaTime * speedMultiplier;
positions[idx + 2] += data.speedZ * deltaTime * speedMultiplier;
const currentSway = data.swayRadius * (1.0 + turbulence * 4.0);
positions[idx] += Math.sin(time * data.swaySpeed + data.phase) * currentSway * deltaTime;
positions[idx + 2] += Math.cos(time * data.swaySpeed + data.phase) * currentSway * deltaTime;
// recycle when out of bounds
if (positions[idx + 1] > 3.0 || Math.abs(positions[idx]) > 3.5 || Math.abs(positions[idx + 2]) > 3.5) {
positions[idx + 1] = -2.5;
positions[idx] = (Math.random() - 0.5) * 3.0;
positions[idx + 2] = (Math.random() - 0.5) * 3.0;
}
}
sparkParticles.geometry.attributes.position.needsUpdate = true;
}
// 3. camera orbits the model 360° driven by scroll
const phi = currentScroll * Math.PI * 2.0;
const y = 0.35 + Math.sin(currentScroll * Math.PI) * 0.8;
const radius = 4.2 - Math.sin(currentScroll * Math.PI) * 0.6;
const x = radius * Math.sin(phi);
const z = radius * Math.cos(phi);
// shift the framing left on the first screen to clear room for the text columns
let transitionProgress = Math.min(1.0, currentScroll / 0.28);
let easeFactor = (Math.cos(transitionProgress * Math.PI) + 1.0) * 0.5; // 1.0 → 0.0
const lookAtXOffset = -0.9 * easeFactor;
const targetLookAt = new THREE.Vector3(lookAtXOffset, -0.15, 0);
const targetPos = new THREE.Vector3(x, y, z);
camera.position.lerp(targetPos, 0.025);
camera.lookAt(targetLookAt);
// background shader uniforms
if (shaderUniforms) {
shaderUniforms.uTime.value = clock.getElapsedTime();
shaderUniforms.uMouse.value.set(mouseX, -mouseY);
shaderUniforms.uScroll.value = currentScroll;
}
updateSlides(currentScroll);
updateGridDots(currentScroll);
renderer.render(scene, camera);
}
```
### Grid dots drift — verbatim
```js
function updateGridDots(scroll) {
const dots = document.querySelectorAll('.grid-dot');
dots.forEach((dot, i) => {
const startY = (i * 17) % 80 + 10;
let speed = 90 + (i * 55) % 180;
if (i % 2 === 0) speed = -speed;
let y = startY + scroll * speed;
y = ((y % 100) + 100) % 100;
dot.style.top = `${y}%`;
});
}
```
### Slides + stories progress — verbatim
```js
function updateSlides(scroll) {
const slide1 = document.getElementById('slide-1');
const slide2 = document.getElementById('slide-2');
const slide3 = document.getElementById('slide-3');
const slide4 = document.getElementById('slide-4');
for (let i = 1; i <= 4; i++) {
const fill = document.getElementById(`dash-fill-${i}`);
if (fill) {
const start = (i - 1) * 0.25;
const end = i * 0.25;
let progress = (scroll - start) / (end - start);
progress = Math.max(0, Math.min(1, progress));
fill.style.height = `${progress * 100}%`;
}
}
function isActive(val, start, end) { return val >= start && val <= end; }
if (slide1) slide1.classList.toggle('active', isActive(scroll, -0.10, 0.12));
if (slide2) {
const active2 = isActive(scroll, 0.28, 0.40);
slide2.classList.toggle('active', active2);
const slide2Img = document.getElementById('slide-2-img');
if (slide2Img) slide2Img.classList.toggle('active', active2);
}
if (slide3) slide3.classList.toggle('active', isActive(scroll, 0.56, 0.68));
if (slide4) slide4.classList.toggle('active', isActive(scroll, 0.84, 1.05));
}
```
### Nav click → smooth scroll-to — verbatim
```js
function setupNavigation() {
const navLinks = document.querySelectorAll('.nav-link');
const targetScrolls = [0.0, 0.34, 0.62, 0.94];
navLinks.forEach((link, index) => {
link.addEventListener('click', (e) => {
e.preventDefault();
const maxScroll = document.documentElement.scrollHeight - window.innerHeight;
const targetY = maxScroll * targetScrolls[index];
window.scrollTo({ top: targetY, behavior: 'smooth' });
});
});
}
```
### Bootstrap
```js
window.addEventListener('DOMContentLoaded', () => {
splitTitlesIntoChars(); // (or call inside init before animate)
// ... build scene: createScene/camera/shader, renderer, lighting, sparks, loadModel ...
onWindowResize(); // optional initial sync
animate();
setupNavigation();
});
```
Order matters: split titles, create scene + camera, `createBackgroundShader()`, renderer + tone
mapping, lighting, sparks, then `loadModel()`, then start `animate()` and wire navigation.
## The loader / reveal
There is no separate loading screen. The reveal is the natural sequence: the black stage + breathing
bronze wave shader render immediately; the bronze horse fades into presence as the GLB resolves
(centered, scaled, lit by the key/rim/fill rig); slide-1 ("Bronze and Time") is active at the top —
its title animates in per-letter from below with a 12px→0 blur and 50px→0 rise (0.035s stagger per
char), and its two description columns float up (translateY 30px→0, 0.4s delay). As you scroll, each
subsequent slide repeats the blur-up reveal at its milestone, the editorial image (slide-2) wipes
open top-to-bottom via `clip-path` with a 1.15→1 parallax zoom, sparks swirl faster, and the shader
palette migrates bronze→sapphire.
## Fixed parameters (bake these in)
- Background: pure black `#000000`; `FogExp2('#000000', 0.01)`.
- Camera: `PerspectiveCamera(50, aspect, 0.1, 100)`, start `(0, 0.2, 3.0)`.
- Renderer: antialias on, pixelRatio `min(dpr, 2)`, `PCFSoftShadowMap`, `ACESFilmicToneMapping`,
exposure `2.2`.
- Lights: ambient `#ffffff @ 0.1`; key SpotLight `#ffffff @ 18.0` at `(4,6,3)`, angle `PI/4`,
penumbra `0.9`, shadow map 2048², near 1.0 far 15, bias -0.001; rim DirectionalLight
`#e3f2ff @ 10.0` at `(-5,3,-4)`; fill DirectionalLight `#fff3e6 @ 0.8` at `(-2,-4,2)`.
- Bronze material: `roughness 0.42`, `metalness 0.92`, `flatShading false`, map anisotropy 16.
- Model: auto-scale max dim → `3.5`; recenter via scaled bounding box; pivot `y = -0.4`.
- Sparks: `sparkCount = 450`, `PointsMaterial` size `0.025`, opacity `0.85`, additive,
`depthWrite:false`, vertexColors; 60% orange / 40% icy-blue; recycle bounds `y>3.0` or
`|x|>3.5`/`|z|>3.5` → respawn at `y=-2.5`, `x,z = ±1.5`.
- Camera orbit: `phi = scroll * 2π`; `y = 0.35 + sin(scroll·π)*0.8`;
`radius = 4.2 - sin(scroll·π)*0.6`; lookAt `(lookAtXOffset, -0.15, 0)`,
`lookAtXOffset = -0.9 * easeFactor`, ease over `scroll/0.28`; `camera.position.lerp(target, 0.025)`.
- Smoothing lerps: scroll `0.025`, mouse `0.05`, outer cursor `0.2`.
- Model tilt from mouse: `rotation.y = mouseX*0.25`, `rotation.x = mouseY*0.15`.
- Slide active ranges: s1 `[-0.10, 0.12]`, s2 `[0.28, 0.40]`, s3 `[0.56, 0.68]`, s4 `[0.84, 1.05]`.
- Nav target scrolls: `[0.0, 0.34, 0.62, 0.94]`.
- Title font 116px Italiana; body 16px Outfit weight 300; title color `#fff6ed`; desc `#d1d5db`.
- Page height `min-height: 900vh`. Layout padding `0 60px 40px 60px`; columns based on `25vw` grid.
- Shader: time scale `0.08`; freqs `2.4 / 3.2 / 4.0`; angles `0.6 / -0.7 / 1.2`; wave weights
`0.50 / 0.35 / 0.15`; `scrollDeform = scroll*5.0`; palette bronze→sapphire (values in shader);
vignette `1.0 - dot(uv,uv)*0.12`; crest multiplier `1.4`; plane `PlaneGeometry(30,30)` at
camera-local `z = -8.0`, `renderOrder -10`, `depthWrite/depthTest false`.
## Assets
Define once near the top of the script:
```js
const ASSET_BASE_URL = "https://api.getlayers.ai/storage/v1/object/public/public/assets/laocoon-59f84455c6";
```
| Asset | Usage | URL |
|-------|-------|-----|
| `bronze_horse.glb` | The bronze statue loaded via `GLTFLoader` (material tweaks + scale 3.5 + recenter) | `https://api.getlayers.ai/storage/v1/object/public/public/assets/laocoon-59f84455c6/bronze_horse.glb` |
| `1.png` | Editorial image inside slide-2 mask (`#slide-2-img img` src) | `https://api.getlayers.ai/storage/v1/object/public/public/assets/laocoon-59f84455c6/1.png` |
Point the `GLTFLoader` at `ASSET_BASE_URL + '/bronze_horse.glb'` and set the `<img>` src to
`ASSET_BASE_URL + '/1.png'` (or hardcode the full URL). The spark texture is generated procedurally
in code (no file). Fonts come from Google Fonts (Italiana, Outfit) via the CSS `@import`.


▸Вкусность еще одна

# Recreate this site as a single HTML file: Loopstack
You are an expert creative front-end developer. Produce a **single self-contained `index.html`** that reproduces the project below **exactly** — same layout, sections, visuals, motion, and interaction. Pure HTML/CSS/JS in one file: no build step, no framework, no bundler. You may inline all CSS in a `<style>` tag and all JS in a `<script>` tag at the end of `<body>`. Load fonts from their CDNs. Hardcode every value given here as a fixed constant. Rebuild each component described below as a section of the one file.
## What it is
A full-viewport, single-screen "footer hero" landing moment on a pure black background. A looping flower video fills the bottom 90% of the screen, with a soft black radial gradient bleeding in from the top to fade the video into the page. Centered near the top is a serif headline and a pill "Book a demo" button with a pulsing neon-green status dot. A fixed footer block sits centered at the vertical middle of the screen ("Stay in Touch" / "Think. Build. Repeat." over a thin divider, with social icons, nav links, and a copyright). A massive "Loopstack" wordmark is pinned across the very bottom. On load: the headline reveals word-by-word (slide up + un-blur from a mask) and the giant wordmark reveals letter-by-letter (slide in from the left + un-blur). A custom cursor follows the pointer — an outlined ring that tracks instantly plus a lagging glassmorphism pill reading "SAY HELLO!"; hovering the button hides the pill and expands the ring. The page does not scroll (it is one fixed screen). Everything is white text on black with a single neon-green accent `#39FF14`.
## Page shell & libraries
- No JS libraries. Everything is hand-rolled vanilla JS + CSS.
- `<html lang="ru">`, `<head>` with `<meta charset="UTF-8">` and `<meta name="viewport" content="width=device-width, initial-scale=1.0">`, `<title>Loopstack</title>`.
- Fonts, loaded via two `@import` rules at the very top of the CSS:
```css
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600&family=Playfair+Display:ital,wght@0,400..900;1,400..900&display=swap');
@import url('https://api.fontshare.com/v2/css?f[]=general-sans@400,500,600&display=swap');
```
Three families are used: **Playfair Display** (serif headline), **Outfit** (button, links, copyright), **General Sans** (footer titles, big wordmark, cursor pill).
- Global reset and page model:
```css
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background-color: #000000;
min-height: 100vh;
overflow: hidden;
position: relative;
}
```
Note `overflow: hidden` — the page is a single non-scrolling screen.
## Layout & sections (in order)
The DOM body order is exactly: top gradient image, hero `<main>`, footer `<footer>`, big logo wordmark wrapper, video container, cursor ring, glass cursor card.
### 1. Top gradient overlay
A soft, blurred black blob SVG pinned to the top, fixed, full width, overlaying the video to fade it into the black page. Source it from the Assets bucket (see Assets).
```html
<img src="ASSET_BASE_URL/black_gradient.svg" alt="Top Gradient" id="top-gradient">
```
```css
#top-gradient {
position: fixed;
top: -30vh; /* pushed well above the top edge */
left: 0;
width: 100vw;
height: auto;
display: block;
z-index: 0; /* above the video container (z-index: -1) */
pointer-events: none; /* clicks pass through */
}
```
### 2. Hero content (headline + button)
```html
<main class="hero-content">
<h1 class="hero-title">Apply Now to be part <br> of the closed beta</h1>
<button class="hero-btn">
<span class="btn-text">Book a demo</span>
<span class="blinking-dot"></span>
</button>
</main>
```
Copy is verbatim: headline `Apply Now to be part` / (line break) / `of the closed beta`; button label `Book a demo`.
```css
.hero-content {
position: relative;
z-index: 2; /* above video and top gradient */
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding-top: 6vh;
width: 95%;
max-width: 1100px;
margin: 0 auto;
}
.hero-title {
font-family: 'Playfair Display', Georgia, serif;
font-size: 2.8rem;
font-weight: 400;
color: #ffffff;
line-height: 1.15;
margin-bottom: 2.2rem;
letter-spacing: -0.015em;
text-shadow: 0 4px 30px rgba(0, 0, 0, 0.5);
}
.hero-btn {
font-family: 'Outfit', sans-serif;
font-size: 1.1rem;
font-weight: 500;
letter-spacing: 0.02em;
color: #ffffff;
background-color: #000000;
border: 1px solid rgba(255, 255, 255, 0.18);
padding: 1.3rem 2.5rem;
border-radius: 9999px;
display: inline-flex;
align-items: center;
gap: 0.9rem;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
box-shadow: 0 4px 25px rgba(0, 0, 0, 0.4);
outline: none;
}
.hero-btn:hover {
background-color: #ffffff;
color: #000000;
border-color: #ffffff;
transform: translateY(-2px);
box-shadow: 0 6px 30px rgba(0, 0, 0, 0.3);
}
.hero-btn:active {
transform: translateY(0);
}
```
The blinking neon dot (inside the button, after the label) with a pulsing glow and an expanding outer wave:
```css
.blinking-dot {
width: 10px;
height: 10px;
background-color: #39FF14;
border-radius: 50%;
position: relative;
display: inline-block;
animation: pulse-glow 2s infinite ease-in-out;
}
.blinking-dot::after {
content: '';
position: absolute;
top: -5px; left: -5px; right: -5px; bottom: -5px;
background-color: rgba(57, 255, 20, 0.45);
border-radius: 50%;
animation: wave-expand 2s infinite ease-in-out;
}
@keyframes pulse-glow {
0%, 100% {
opacity: 0.5;
transform: scale(0.85);
box-shadow: 0 0 4px rgba(57, 255, 20, 0.3);
}
50% {
opacity: 1;
transform: scale(1.1);
box-shadow: 0 0 12px rgba(57, 255, 20, 0.9);
}
}
@keyframes wave-expand {
0% { transform: scale(0.6); opacity: 0.9; }
100% { transform: scale(2.3); opacity: 0; }
}
```
### 3. Footer block (fixed, vertically centered)
```html
<footer class="footer-container">
<div class="footer-top">
<h2 class="footer-title">Stay in Touch</h2>
<h2 class="footer-title quote">Think. Build. Repeat.</h2>
</div>
<hr class="footer-divider">
<div class="footer-bottom">
<div class="footer-socials">
<a href="#" aria-label="LinkedIn" class="social-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path><rect x="2" y="9" width="4" height="12"></rect><circle cx="4" cy="4" r="2"></circle></svg>
</a>
<a href="#" aria-label="X" class="social-icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4l11.733 16h4.267l-11.733 -16z"></path><path d="M4 20l6.768 -6.768m2.46 -2.46l6.772 -6.772"></path></svg>
</a>
<a href="#" aria-label="Instagram" class="social-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="20" rx="5" ry="5"></rect><path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path><line x1="17.5" y1="6.5" x2="17.51" y2="6.5"></line></svg>
</a>
</div>
<nav class="footer-links">
<a href="#about" class="footer-link">About</a>
<a href="#features" class="footer-link">Features</a>
<a href="#pricing" class="footer-link">Pricing</a>
<a href="#contact" class="footer-link">Contact</a>
</nav>
<div class="footer-copyright">
© 2026 Loopstack
</div>
</div>
</footer>
```
Copy verbatim: footer titles `Stay in Touch` and `Think. Build. Repeat.`; nav links `About`, `Features`, `Pricing`, `Contact`; copyright `© 2026 Loopstack`. Three social icons in order LinkedIn, X, Instagram with the exact inline SVG paths above.
```css
.footer-container {
position: fixed;
top: 50vh;
left: 20px;
right: 20px;
width: calc(100vw - 40px);
transform: translateY(-50%); /* exactly screen-centered vertically */
z-index: 3; /* above every other layer */
display: flex;
flex-direction: column;
}
.footer-top {
display: flex;
justify-content: space-between;
align-items: flex-end;
width: 100%;
padding: 0 5px;
}
.footer-title {
font-family: 'General Sans', -apple-system, sans-serif;
font-size: 1.4rem;
font-weight: 400;
color: #ffffff;
letter-spacing: -0.015em;
margin: 0;
}
.footer-title.quote {
text-transform: none;
}
.footer-divider {
border: none;
height: 1px;
background-color: rgba(255, 255, 255, 0.2);
margin: 1.6rem 0;
width: 100%;
}
.footer-bottom {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding: 0 5px;
}
.footer-socials {
display: flex;
gap: 1.25rem;
align-items: center;
flex: 1;
}
.social-icon {
color: rgba(255, 255, 255, 0.55);
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
display: inline-flex;
align-items: center;
justify-content: center;
text-decoration: none;
}
.social-icon:hover {
color: #ffffff;
transform: translateY(-2px);
}
.footer-links {
display: flex;
gap: 2.8rem;
justify-content: center;
align-items: center;
flex: 2;
}
.footer-link {
font-family: 'Outfit', sans-serif;
font-size: 0.95rem;
font-weight: 400;
color: #ffffff;
text-decoration: none;
letter-spacing: 0.03em;
transition: opacity 0.3s ease, transform 0.3s ease;
}
.footer-link:hover {
opacity: 0.75;
transform: translateY(-1px);
}
.footer-copyright {
font-family: 'Outfit', sans-serif;
font-size: 0.95rem;
font-weight: 400;
color: rgba(255, 255, 255, 0.45);
text-align: right;
flex: 1;
letter-spacing: 0.02em;
}
```
### 4. Giant "Loopstack" wordmark (pinned to bottom)
```html
<div class="footer-logo-wrap">
<h2 class="footer-logo-text">Loopstack</h2>
</div>
```
Text verbatim: `Loopstack` (9 letters).
```css
.footer-logo-wrap {
position: fixed;
bottom: 20px;
left: 0;
right: 0;
width: 100%;
padding: 0 20px;
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
z-index: 3;
margin: 0;
}
.footer-logo-text {
font-family: 'General Sans', -apple-system, sans-serif;
font-size: 21.9vw; /* enormous, fills the width */
font-weight: 400;
color: #ffffff;
letter-spacing: -0.03em;
margin-right: -0.03em;
transform: translateX(-20px);
line-height: 0.8;
margin-top: 0;
margin-bottom: 0;
margin-left: 0;
text-align: center;
width: 100%;
pointer-events: none;
opacity: 0.95;
text-shadow: none;
white-space: nowrap;
}
```
### 5. Background flower video
```html
<div class="video-container">
<video autoplay muted playsinline id="bg-video">
<source src="ASSET_BASE_URL/flower.mp4" type="video/mp4">
Ваш браузер не поддерживает видео.
</video>
</div>
```
```css
.video-container {
position: fixed;
bottom: 0;
left: 0;
width: 100vw;
height: 90vh; /* occupies bottom 90% of viewport */
overflow: hidden;
z-index: -1; /* sits behind everything */
}
#bg-video {
width: 100%;
height: 110%;
object-fit: cover;
display: block;
transform: translateY(0%);
}
```
Add `loop` so it cycles seamlessly (autoplay/muted/playsinline are present for mobile autoplay).
### 6. Custom cursor elements
```html
<div id="cursor-ring" class="cursor-ring-outline"></div>
<div id="glass-card" class="glass-cursor-card">
<span class="cursor-card-text"><span class="text-white">Say</span> Hello!</span>
</div>
```
Pill copy verbatim: `Say` (white) + ` Hello!` (neon green) — rendered uppercase by CSS so it reads "SAY HELLO!".
```css
.glass-cursor-card {
position: fixed;
top: 0;
left: 0;
z-index: 99999;
pointer-events: none;
padding: 0.75rem 1.5rem;
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 9999px;
box-shadow: inset 0 1px 0 0 rgba(255, 255, 255, 0.15);
transform: translate(-50%, -50%) scale(0);
opacity: 0;
will-change: transform, opacity;
transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1),
background 0.35s cubic-bezier(0.16, 1, 0.3, 1),
border-color 0.35s cubic-bezier(0.16, 1, 0.3, 1),
box-shadow 0.35s cubic-bezier(0.16, 1, 0.3, 1);
}
.glass-cursor-card.active { opacity: 1; }
.glass-cursor-card.dark-mode {
background: rgba(0, 0, 0, 0.82);
border-color: rgba(255, 255, 255, 0.08);
box-shadow: inset 0 1px 0 0 rgba(255, 255, 255, 0.05);
}
.cursor-card-text {
font-family: 'General Sans', -apple-system, sans-serif;
font-size: 0.85rem;
font-weight: 500;
letter-spacing: 0.06em;
color: #39FF14;
text-transform: uppercase;
white-space: nowrap;
text-shadow: 0 0 8px rgba(57, 255, 20, 0.45);
}
.cursor-card-text .text-white {
color: #ffffff;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
}
.cursor-ring-outline {
position: fixed;
top: 0;
left: 0;
width: 48px;
height: 48px;
border: 1.5px solid rgba(255, 255, 255, 0.45);
border-radius: 50%;
z-index: 99998;
pointer-events: none;
transform: translate(-50%, -50%) scale(0);
opacity: 0;
will-change: transform, opacity;
transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1),
border-color 0.4s ease;
}
.cursor-ring-outline.active { opacity: 1; }
.cursor-ring-outline.expanded {
border-color: rgba(255, 255, 255, 0.15);
}
```
## The loader / reveal (entrance animations)
Two coordinated masked reveals run on load. Both use the same dramatic easing `cubic-bezier(0.05, 0.9, 0.1, 1)` and start blurred at `blur(20px)`.
### Hero headline — word-by-word slide-up reveal
Each word is wrapped in a mask (`overflow: hidden`) and an inner span that starts pushed down and blurred, then slides up into place and un-blurs. `<br>` and spaces are preserved. Stagger: `0.1s` per word.
```css
.hero-title .word-wrapper {
display: inline-block;
overflow: hidden;
vertical-align: bottom;
padding-bottom: 0.15em; /* room so descenders (y, p, g) aren't clipped */
margin-bottom: -0.15em;
}
.hero-title .word-inner {
display: inline-block;
opacity: 0;
transform: translateY(105%);
filter: blur(20px);
animation: word-reveal-mask 1.3s cubic-bezier(0.05, 0.9, 0.1, 1) forwards;
}
@keyframes word-reveal-mask {
0% { opacity: 0; transform: translateY(105%); filter: blur(20px); }
30% { opacity: 1; }
100% { opacity: 1; transform: translateY(0); filter: blur(0); }
}
```
### Big wordmark — letter-by-letter slide-in-from-left reveal
Each letter is wrapped in a mask and an inner span that starts pushed left (`translateX(-105%)`) and blurred, then slides right into place and un-blurs. Spaces become ` `. Stagger: `0.09s` per letter (with 9 letters, the last letter finishes at `0.72s` delay + `1.2s` animation = `1.92s`).
```css
.footer-logo-text .letter-wrapper {
display: inline-block;
overflow: hidden;
vertical-align: bottom;
line-height: 0.8;
}
.footer-logo-text .letter-inner {
display: inline-block;
opacity: 0;
transform: translateX(-105%);
filter: blur(20px);
animation: letter-reveal-mask 1.2s cubic-bezier(0.05, 0.9, 0.1, 1) forwards;
}
@keyframes letter-reveal-mask {
0% { opacity: 0; transform: translateX(-105%); filter: blur(20px); }
25% { opacity: 1; }
100% { opacity: 0.95; transform: translateX(0); filter: blur(0); }
}
```
## Interaction logic (custom cursor + reveal splitting) — copy this JS VERBATIM
```js
document.addEventListener('DOMContentLoaded', () => {
const logoText = document.querySelector('.footer-logo-text');
if (logoText) {
const text = logoText.textContent.trim();
logoText.innerHTML = ''; // Очищаем контейнер
// Разделяем слово на отдельные буквы и оборачиваем каждую в двойной span для маски
[...text].forEach((char, index) => {
// Внешний span выступает в роли маски (overflow: hidden)
const wrapper = document.createElement('span');
wrapper.className = 'letter-wrapper';
// Внутренний span содержит букву и анимируется слева направо
const inner = document.createElement('span');
inner.textContent = char === ' ' ? ' ' : char; // Обрабатываем пробелы
inner.className = 'letter-inner';
// Задаем задержку анимации для каждой буквы:
// 9 букв, задержка шага 0.09с, длительность анимации 1.2с.
// Последняя буква закончит выдвигаться ровно на отметке: 0.72с (задержка) + 1.2с (анимация) = 1.92 секунды!
inner.style.animationDelay = `${index * 0.09}s`;
wrapper.appendChild(inner);
logoText.appendChild(wrapper);
});
}
// Разделение верхнего заголовка на слова для анимации появления снизу вверх из маски с блюром
const heroTitle = document.querySelector('.hero-title');
if (heroTitle) {
const text = heroTitle.innerHTML;
// Разделяем по пробелам, сохраняя теги <br>
const parts = text.split(/(\s+|<br\s*\/?>)/i);
heroTitle.innerHTML = '';
let wordIndex = 0;
parts.forEach(part => {
if (part.trim() === '') {
// Обычный пробел между словами
heroTitle.appendChild(document.createTextNode(' '));
} else if (part.toLowerCase().startsWith('<br')) {
// Сохраняем перенос строки
heroTitle.appendChild(document.createElement('br'));
} else {
// Обертка-маска (overflow: hidden)
const wrapper = document.createElement('span');
wrapper.className = 'word-wrapper';
// Внутреннее слово, сдвинутое вниз и заблюренное
const inner = document.createElement('span');
inner.className = 'word-inner';
inner.textContent = part;
// Задержка анимации: шаг 0.1с для размеренного, солидного появления
inner.style.animationDelay = `${wordIndex * 0.1}s`;
wordIndex++;
wrapper.appendChild(inner);
heroTitle.appendChild(wrapper);
}
});
}
// Летающий оутлайновый белый кружок и стеклянная плашка за курсором (LERP-физика)
const glassCard = document.getElementById('glass-card');
const cursorRing = document.getElementById('cursor-ring');
if (glassCard && cursorRing) {
let mouseX = window.innerWidth / 2;
let mouseY = window.innerHeight / 2;
// Координаты для плашки (медленное движение)
let cardX = mouseX;
let cardY = mouseY;
// Координаты для кружка (более быстрое движение)
let ringX = mouseX;
let ringY = mouseY;
let isFirstMove = true;
let scale = 0;
let targetScale = 0;
let isHoveringBtn = false; // Флаг наведения на кнопку Book a Demo
// Отслеживаем координаты курсора
document.addEventListener('mousemove', (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
if (isFirstMove) {
// Мгновенно позиционируем при первом движении, чтобы избежать "прыжка"
cardX = mouseX;
cardY = mouseY;
ringX = mouseX;
ringY = mouseY;
isFirstMove = false;
glassCard.classList.add('active');
cursorRing.classList.add('active');
}
// Показываем элементы только если курсор не находится на кнопке Book a demo
if (!isHoveringBtn) {
targetScale = 1;
}
});
// Плавное скрытие при выходе курсора за пределы экрана
document.addEventListener('mouseleave', () => {
targetScale = 0;
});
// Плавное появление при возвращении курсора в окно
document.addEventListener('mouseenter', () => {
if (!isHoveringBtn) {
targetScale = 1;
}
});
// Слушатели наведения на кнопку "Book a demo" для переключения режимов
const heroBtn = document.querySelector('.hero-btn');
if (heroBtn) {
heroBtn.addEventListener('mouseenter', () => {
isHoveringBtn = true;
targetScale = 0; // Плавное скрытие плашки (scale 0)
cursorRing.classList.add('expanded'); // Добавляем класс расширения кружка в CSS
});
heroBtn.addEventListener('mouseleave', () => {
isHoveringBtn = false;
targetScale = 1; // Плавное возвращение плашки
cursorRing.classList.remove('expanded'); // Сброс расширения кружка
});
}
// Системный цикл анимации (60-120 кадров/сек) для сглаживания
const updatePhysics = () => {
// Мягкое отставание плашки: коэффициент LERP 0.08
cardX += (mouseX - cardX) * 0.08;
cardY += (mouseY - cardY) * 0.08;
// Кружок следует за курсором мгновенно и без задержки
ringX = mouseX;
ringY = mouseY;
// Интерполируем масштаб для плавного появления/исчезновения
scale += (targetScale - scale) * 0.15;
// Масштаб кружка (если навели на кнопку, он увеличивается до 1.6 через класс expanded)
// Но мы всё еще перемножаем его на общий scale, чтобы кружок сжался в 0 при выходе с экрана
const currentRingScale = cursorRing.classList.contains('expanded') ? 1.6 * scale : scale;
// Применяем аппаратную 3D-трансформацию
glassCard.style.transform = `translate3d(${cardX}px, ${cardY}px, 0) translate(-50%, -50%) scale(${scale})`;
cursorRing.style.transform = `translate3d(${ringX}px, ${ringY}px, 0) translate(-50%, -50%) scale(${currentRingScale})`;
requestAnimationFrame(updatePhysics);
};
updatePhysics();
}
});
```
## Fixed parameters (bake these in)
- Background: `#000000`. Body `overflow: hidden` (single non-scrolling screen).
- Accent neon green: `#39FF14` (and `rgba(57, 255, 20, …)` variants).
- Text white: `#ffffff`; muted whites: `rgba(255,255,255,0.55)` (social), `rgba(255,255,255,0.45)` (copyright), `rgba(255,255,255,0.2)` (divider), `rgba(255,255,255,0.18)` (button & pill border).
- Fonts: Playfair Display (headline `2.8rem`/400), Outfit (button `1.1rem`/500, links & copyright `0.95rem`/400), General Sans (footer titles `1.4rem`/400, wordmark `21.9vw`/400, pill `0.85rem`/500).
- Video container height `90vh`; video height `110%`, `object-fit: cover`.
- Top gradient `top: -30vh`, `z-index: 0`; video container `z-index: -1`; hero `z-index: 2`; footer & logo wrap `z-index: 3`; cursor ring `z-index: 99998`; glass pill `z-index: 99999`.
- Footer container centered: `top: 50vh; transform: translateY(-50%)`, `left/right: 20px`, `width: calc(100vw - 40px)`.
- Wordmark: `21.9vw`, `letter-spacing: -0.03em`, `line-height: 0.8`, `translateX(-20px)`, `opacity: 0.95`, pinned `bottom: 20px`, `white-space: nowrap`.
- Button: `border-radius: 9999px`, `padding: 1.3rem 2.5rem`, `gap: 0.9rem`, hover inverts to white bg/black text and lifts `translateY(-2px)`.
- Blinking dot: `10px`, `pulse-glow` and `wave-expand` both `2s infinite ease-in-out`; outer wave color `rgba(57,255,20,0.45)` scaling `0.6 → 2.3`.
- Cursor ring: `48px`, `1.5px solid rgba(255,255,255,0.45)`, tracks pointer instantly; expands to `1.6×` on button hover (`expanded` fades border to `rgba(255,255,255,0.15)`).
- Glass pill: lags pointer with LERP `0.08`; scale interpolates toward target with LERP `0.15`; hidden (`scale 0`) while hovering the button, shown (`scale 1`) otherwise; `backdrop-filter: blur(12px)`.
- Reveal easing `cubic-bezier(0.05, 0.9, 0.1, 1)`; both reveals start at `blur(20px)`. Headline words: `1.3s`, stagger `0.1s`, slide up `translateY(105%)→0`. Wordmark letters: `1.2s`, stagger `0.09s`, slide right `translateX(-105%)→0`.
## Assets
Route every local asset through:
```
ASSET_BASE_URL = https://api.getlayers.ai/storage/v1/object/public/public/assets/loopstack-f8c64439bf
```
- `flower.mp4` → `https://api.getlayers.ai/storage/v1/object/public/public/assets/loopstack-f8c64439bf/flower.mp4` (the background `<video>` `<source src>`).
- `black_gradient.svg` → `https://api.getlayers.ai/storage/v1/object/public/public/assets/loopstack-f8c64439bf/black_gradient.svg` (the `#top-gradient` `<img src>`).