Verance 3D Luxury Watch AI website prompt
A luxury watch landing page where a gold 3D watch reshapes itself as you scroll. It opens with a molten gold loader, then shows an exploded anatomy view, a live configurator that reforms the watch from gold dust, and a collectors slider.
Now put it in WordPress
You have a site. Now make it yours.
An AI build hands you a front end and stops there. No CMS, no client login, no real ownership: you cannot hand it over, and you cannot change it without going back and re-prompting. UiChemy turns that build into native, editable WordPress you own, in the builder you already use.
Bolt to WordPressClaude to WordPressCodex to WordPressCursor to WordPressLovable to WordPressv0 to WordPressAI to WordPress
Get started free
More prompts
Showing 2 prompts
Frequently asked questions
What does the Verance 3D Luxury Watch prompt build?
A luxury watch landing page where a gold 3D watch reshapes itself as you scroll. It opens with a molten gold loader, then shows an exploded anatomy view, a live configurator that reforms the watch from gold dust, and a collectors slider.
Which AI tools does the Verance 3D Luxury Watch prompt work with?
It's written to work with . Paste it into your AI tool and it builds the site, then move the result into WordPress with UiChemy.
How do I turn the result into a WordPress site?
After the AI builds your site, UiChemy Composer rebuilds it as native, editable WordPress in Elementor or Gutenberg (Bricks coming soon) — you own it and edit it in your builder. See AI to WordPress.
Is the Verance 3D Luxury Watch prompt free?
Yes — it's free to copy, no account needed.
Can I edit the website after it's built?
Yes. Once it's in WordPress with UiChemy, every section is native and editable — no re-prompting, no lock-in.
Copy prompt
Copy code
Coming soon
Copy SKILL.md
Coming soon
Verance 3D Luxury Watch · Section prompt
# MASTER PROMPT — Verance Nocturne · 3D luxury watch scroll landing · **WP-hosted assets build**
## ▶ HOW TO USE THIS PROMPT (do this first)
**Paste this entire document into Claude — nothing to attach.** Every asset this page needs is already hosted on WordPress at `demo.uichemy.com` (fonts in the media library, libraries and the page engine as UiChemy code files); there are no CDN dependencies and no local files.
This document has **two halves:**
- **PART A — the specification**: describes the page as documentation.
- **PART B — the appendix** (`APPENDIX · VERBATIM SOURCE`): the **complete source of every payload**, plus the exact UiChemy ability calls that put them on the site. **This is the half that guarantees the identical page.**
**If you want the exact same result: go to the appendix and use those payloads verbatim.** Read Part A as documentation, not as instructions to re-invent the page — generating from Part A alone gets you close, not identical.
**INSTRUCTION TO CLAUDE:** This is a **WordPress / UiChemy Composer** build on an **Elementor page**, not a standalone HTML file. Read the `code-to-wordpress` skill (`uichemy-composer/read-skill`), call `uichemy-composer/describe-site`, **read "THE WRITE-PATH RULES" below before your first write**, then run the calls in **B7** in order, sending the payloads from B1–B6 **exactly as written** (do not reformat, rename, shorten, minify, expand, or omit anything). Keep every CSS value and unit as written (px stays px). If you are rebuilding on a **different** site, do B8 first to re-host the assets there and then substitute your own URLs throughout. Do not reproduce from Part A when the appendix is present.
## ⚠ THE WRITE-PATH RULES (read before writing anything)
Verified on `demo.uichemy.com` on 2026-09-21 while building page 575. Earlier builds on this site hit a sanitizing write path (JS dropped, `<canvas>`/`<svg>` stripped, all reported as success); by this build it was fixed. **Still verify every write — do not trust a success response.**
| Lane | What happened on this build | Rule |
|---|---|---|
| `append-section` / `insert-section` → `html` | `<svg>` with `defs`/`clipPath`/gradients/`pathLength`, `<canvas>`, `<form>`/`<input>`, inline `style="--c1:…"`, `data-*`, `aria-*` all persisted. | After writing, fetch the published URL and check every `id` and `class` from B3 is present. |
| `append-section` → `js` | Persisted. | `grep` (field `js`) for `requestRefresh` on the page → expect 11 matches. |
| `set-page-code` → `page_before_head` / `page_before_body` | `<link rel=stylesheet>` and `<script src>` stored and printed exactly as written. | Read back with `get-page-code`; check the tags in the page source. |
| Code files (`code-file request-upload`) | Byte-identical. | `curl` each URL and `cmp` against the local file. |
| MCP calls | Intermittent 10 s Cloudflare connect timeouts. | After a timeout, run `get-structure` **before retrying**, so a section is never duplicated; put a missing one back in place with `insert-section`. |
| Cloudflare cache | `/wp-content/uichemy-composer/*` is cached for 30 days, keyed by query string. | Whenever a code file changes, bump its `?v=` in **every** reference (page code, `verance-boot.js`, all 11 stubs). |
**Never use `platform set-site-code`.** This build uses **page-level** code only, so site-wide head/body code and other pages are untouched.
Verify on the published URL, not the Elementor canvas.
---
## OVERVIEW
- **What it is:** a full landing page for "Verance Nocturne", a fictional Genève maison's limited 18k gold automatic watch. A single fixed WebGL canvas renders a procedurally built watch behind the page; every section re-poses that one watch as you scroll.
- **Stack:** 11 UiChemy Composer widgets on one Elementor page (page 575, slug `verance-nocturne`), plus one shared page engine and one shared stylesheet as UiChemy code files, wired in with **page-level** code. Libraries are self-hosted at the exact versions the source pins: three.js **r169** (+ `RoomEnvironment`, `RoundedBoxGeometry`, `MeshSurfaceSampler`), GSAP **3.12.5** + ScrollTrigger, Lenis **1.1.13**. **No importmap, no ES modules, no CDN.**
- **Why one engine file:** the source's scroll choreography is one program across every section (one watch, one pose per section, one ScrollTrigger timeline). Splitting it per widget would change behaviour, so each widget's JS is a small **stub** that feeds its panel controls to the engine and asks it to rebuild.
- **Fonts:** Cormorant Garamond (normal + italic) and Jost, **self-hosted in the WP media library**, declared per weight with `@font-face` inside `verance-app.css`.
- **Works in both the frontend and the Elementor editor** (see "EDITOR BEHAVIOUR").
## SECTION MAP (live element ids on page 575)
| # | Widget label | Element id | Content |
|---|---|---|---|
| 1 | Header & Loader | `o8k5tvv` | loader, grain, glow, fixed `canvas#webgl`, cursor, progress bar, 8 callouts, clasp chip, glass nav, mobile menu |
| 2 | Hero | `3hyizvg` | `#hero` giant "Nocturne", lead, live clock |
| 3 | La Maison | `chee6wa` | `#maison` heading, signature, three stat cards |
| 4 | Anatomie | `yxo4tej` | `#anatomie` pinned exploded view |
| 5 | Calibre | `f8rrwvi` | `#calibre` specs grid |
| 6 | Configure | `67gwfmm` | `#collection` configurator (ivory chapter) |
| 7 | Marquee | `w4ztxtq` | velocity-reactive marquee |
| 8 | Quote | `ttv4thm` | `#quote` scroll-lit quote |
| 9 | Reserve | `djg2mbj` | `#reserve` private-viewing form |
| 10 | Collectors | `mrtxgdf` | `#voices` testimonial slider |
| 11 | Footer | `ieqemul` | `#footer` link columns + VERANCE wordmark |
Each widget's root is `<div class="verance-nocturne uichemy-verance-{slug}-{n}">`. Every source `id` and `class` is kept, so the engine's selectors are the source's.
## ASSET MANIFEST (already hosted — reference these URLs directly)
| Asset | Kind | URL | Notes |
|---|---|---|---|
| Cormorant Garamond, normal (latin) | WP media | `https://demo.uichemy.com/wp-content/uploads/2026/09/verance-cormorant-garamond.woff2` | variable file; declared for 300/400/500/600 |
| Cormorant Garamond, italic (latin) | WP media | `https://demo.uichemy.com/wp-content/uploads/2026/09/verance-cormorant-garamond-italic.woff2` | declared for 300/400/500 |
| Jost (latin) | WP media | `https://demo.uichemy.com/wp-content/uploads/2026/09/verance-jost.woff2` | declared for 300/400/500 |
| Page stylesheet | UiChemy code file | `/wp-content/uichemy-composer/verance-app.css?v=2` | the source CSS, scoped (see B1); fonts inside |
| Page engine | UiChemy code file | `/wp-content/uichemy-composer/verance-app.js?v=5` | source JS ported to a re-entrant classic script (B5) |
| Boot | UiChemy code file | `/wp-content/uichemy-composer/verance-boot.js?v=5` | loads the engine only when `.verance-nocturne` is on the page (B4) |
| three.js r169 + 3 addons | UiChemy code file | `/wp-content/uichemy-composer/verance-three.js?v=1` | 671 KB esbuild IIFE → `window.VERANCE_THREE = { THREE, RoomEnvironment, RoundedBoxGeometry, MeshSurfaceSampler }` (B8) |
| Lenis 1.1.13 | UiChemy code file | `/wp-content/uichemy-composer/verance-lenis.js?v=1` | `lenis.min.js` wrapped so an AMD/CommonJS loader cannot divert it from `window.Lenis` (B8) |
| GSAP 3.12.5 | UiChemy code file (pre-existing) | `/wp-content/uichemy-composer/uichemy-gsap.js` | already on the site |
| ScrollTrigger 3.12.5 | UiChemy code file (pre-existing) | `/wp-content/uichemy-composer/uichemy-scrolltrigger.js` | already on the site |
No images: the watch, the dial/bezel/caseback engravings (canvas textures) and the loader watch (inline SVG) are all generated in code.
## PART A · SPECIFICATION (documentation — the appendix is authoritative)
**Concept.** Luxury black-and-gold art direction with one warm-ivory chapter (the configurator). A single fixed WebGL canvas renders the watch behind the page; every section moves, rotates and re-poses that one watch as you scroll.
**Design system.** Colours: noir `#0a0908`, ivory `#f5efe3`, gold `#c9a45c`, highlight `#f1d9a0`, low `#8a6a2e`. Type: Cormorant Garamond (display, italic gold emphasis) + Jost (UI, uppercase tracked eyebrows). Film-grain overlay, gold radial glow that follows the watch, custom gold cursor, magnetic buttons, pearl-white star particles (custom shader) drifting with scroll velocity.
**Loader.** A faint line-drawn watch draws itself behind the VERANCE wordmark (live hands). Molten gold with rolling waves sweeps diagonally left→right through the letters (the liquid is rotated 60°). A glint crosses the word, then the screen splits open horizontally like a caseback while the hero intro plays.
**Sections, in order.**
1. **Hero** — giant italic "Nocturne" (letters rise from blur, lean away from the cursor, fan out in 3D on scroll); watch front-facing with the bracelet open and flat; live local-time clock.
2. **01 La Maison** — the bracelet curls link by link around a wrist and a butterfly clasp snaps shut ("Papillon clasp · Locked" chip + light flash); heading, signature, and three glass stat cards that fly in from depth, trace a gold border, draw their icons and count up.
3. **02 Anatomie** — pinned exploded view in 8 layers; the bracelet retracts; projected callouts in two columns with angled leader lines.
4. **03 Calibre V-88** — specs grid with counters.
5. **04 Configure** — warm-ivory chapter (`body.is-light`); case and dial swatches; each change dissolves the watch into ~9,000 surface-sampled gold particles in a left→right wave and re-forms it in the new option; drag to rotate; animated price.
6. **Marquee + quote** — velocity-reactive marquee; quote words light up on scroll.
7. **05 Private viewing** — reservation form (front-end only).
8. **06 Collectors** — testimonial slider; each collector's configuration re-forms the watch from gold dust; autoplay with a progress bar, arrows, pause on hover.
9. **Footer** — four link columns, legal row, cropped full-width VERANCE wordmark rising letter by letter.
**Navigation.** Fixed glass-pill nav, gold highlight on the current section. Phones and portrait tablets: round menu button → full-screen menu. "Narrow" layout (≤900px wide or portrait) puts the watch in a top band with copy on solid panels.
**Parity checked on the live page at 1440×900:** top offset, size, font-size, family, weight, letter-spacing, line-height and colour of the nav, hero lead, eyebrows, every section heading, the `.btn`, and the footer match the standalone original exactly; document height is 10937px in both; 59 ScrollTriggers in both.
## PANEL CONTROLS (the `uichemy_controller_bridge` in each stub)
| Widget | Key | Default | Range | Drives |
|---|---|---|---|---|
| Header & Loader | `lenis_lerp` | 0.085 | 0.02–0.3 | smooth-scroll easing |
| Header & Loader | `tone_exposure` | 1.05 | 0.5–2 | watch exposure |
| Header & Loader | `env_intensity` | 0.95 | 0–2 | reflections |
| Hero | `intro_duration` | 2.8 s | 0.5–6 | watch intro |
| Hero | `hero_fan` | 16 deg | 0–40 | letter fan on scroll |
| Hero | `hero_depth` | 60 px | 0–200 | letter depth on scroll |
| Anatomie | `pin_length` | 220 % | 100–400 | exploded-view pin length |
| Marquee | `marquee_duration` | 28 s | 8–80 | loop duration |
| Collectors | `autoplay_seconds` | 7 s | 3–20 | testimonial autoplay |
Defaults are the source's own values. The stubs write them to `window.VERANCE_MOTION`; the engine merges them over its `DEFAULTS`.
## EDITOR BEHAVIOUR (Elementor)
- **Detection:** `?elementor-preview=` in the URL, `body.elementor-editor-active`, or `elementorFrontend.isEditMode()` → adds `html.verance-edit`.
- **Re-entrant engine:** the Composer re-runs a widget's JS on every edit, and each stub calls `VeranceEngine.requestRefresh()` (debounced 300 ms). Each rebuild tears everything down first: listeners, intervals, ticker callbacks, ScrollTriggers, the `gsap.context`, Lenis, split-text snapshots, and the WebGL renderer (disposed + `forceContextLoss`, canvas replaced). Rebuilding leaves one canvas and 59 ScrollTriggers every time.
- **A MutationObserver** notices when Elementor re-renders a `.verance-nocturne` root. A re-render during boot sets a dirty flag so the engine rebinds to the live DOM once booted.
- **Loader in the editor:** plays when the editor opens and again whenever the Header & Loader widget is re-rendered. It plays on whichever loader element is currently on the canvas; a copy that Elementor removes mid-play is stopped. It lifts on its own (7 s safety limit). The 3D scene is built only after it lifts, and edits made while it plays are applied right after.
- **Off in the editor:** custom cursor (CSS), Lenis (native-scroll stub with the same interface), anchor hijack, scroll-to-top, scroll lock. Renderer pixel ratio capped at 1.5 (frontend 2).
## PERFORMANCE (both frontend and editor)
- The 3D scene is **not drawn while the loader covers the screen** (`window.__vnCover`); drawing resumes the moment the loader splits open.
- Shaders compile with `renderer.compileAsync` (non-blocking `KHR_parallel_shader_compile`); the render loop waits for it.
- Boot yields between heavy steps (fonts → build watch → scene setup) so the gold pour keeps its frames.
## DELIBERATE DIFFERENCES FROM THE STANDALONE ORIGINAL
- **Self-hosted everything:** Google Fonts → WP media `@font-face`; jsDelivr importmap → own three r169 IIFE bundle; Lenis and GSAP from code files. UiChemy ships its own three (`UiChemyThree.mount()`), but it is not r169 and lacks the three addons the source uses, so this build bundles its own.
- **CSS in one code file, scoped:** every selector is prefixed with `html:has(.verance-nocturne)`, a uniform +(0,1,1) specificity, so the source's cascade order is unchanged. The source's resets are wrapped in `:where(.verance-nocturne)`, adding zero specificity, so they never touch the theme. `body{margin:0}` is re-added because the scoped reset no longer reaches `<body>`.
- **Source vars bridged to globals:** e.g. `--gold: var(--vn-gold, #c9a45c)`; with no global set, the source value applies.
- **JS ported to a classic script** (B5): imports removed, values made controllable (table above), everything tracked for teardown, and the editor behaviour above. The source's choreography, numbers and easing are unchanged.
- **Page-level wiring:** stylesheet in `page_before_head`, boot script in `page_before_body`. If a context drops the stylesheet, the engine links it itself.
---
# APPENDIX · VERBATIM SOURCE
Send each payload exactly as written. B7 is the call order.
## B1 · Page code (`set-page-code`)
`page_before_head`:
```html
<link rel="stylesheet" id="verance-app-css" href="/wp-content/uichemy-composer/verance-app.css?v=2">
```
`page_before_body`:
```html
<script src="/wp-content/uichemy-composer/verance-boot.js?v=5"></script>
```
## B2 · Sections — HTML + JS stubs (no widget CSS)
One `append-section` per section, in this order, `upload_images: false`, label as shown. Leave `css` empty: all styles live in B3.
### B2.1 · Header & Loader
label: `Header & Loader`
**html**
```html
<div class="verance-nocturne uichemy-verance-header-1">
<!-- Preloader: liquid gold sweeps diagonally through the wordmark over a faint line-drawn
watch, then the screen splits open like a caseback -->
<div class="loader" aria-hidden="true">
<svg class="loader__watch" viewBox="-200 -310 400 620">
<g class="lw-strap">
<path pathLength="1" d="M-68 -300 L-68 -178 M68 -300 L68 -178 M-68 178 L-68 300 M68 178 L68 300" />
<g class="lw-links"></g>
</g>
<path class="lw-lugs" pathLength="1" d="M-96 -118 L-80 -178 L-56 -178 L-62 -140 M96 -118 L80 -178 L56 -178 L62 -140 M-96 118 L-80 178 L-56 178 L-62 140 M96 118 L80 178 L56 178 L62 140" />
<circle class="lw-ring" pathLength="1" r="150" />
<circle class="lw-ring" pathLength="1" r="138" />
<circle class="lw-ring lw-ring--dial" pathLength="1" r="116" />
<rect class="lw-crown" pathLength="1" x="150" y="-16" width="22" height="32" rx="4" />
<g class="lw-ticks"></g>
<g class="lw-hands">
<line class="lw-hour" x1="0" y1="12" x2="0" y2="-62" />
<line class="lw-min" x1="0" y1="16" x2="0" y2="-96" />
<line class="lw-sec" x1="0" y1="26" x2="0" y2="-106" />
<circle r="4" />
</g>
</svg>
<svg class="loader__word" viewBox="0 0 1000 200" preserveAspectRatio="xMidYMid meet">
<defs>
<clipPath id="ldClip"><text x="500" y="150" text-anchor="middle" class="ld-text">VERANCE</text></clipPath>
<linearGradient id="ldGold" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#fff1c9" /><stop offset="0.35" stop-color="#e2bd72" />
<stop offset="0.7" stop-color="#b48b43" /><stop offset="1" stop-color="#7a5c29" />
</linearGradient>
<linearGradient id="ldGlint" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#fff" stop-opacity="0" /><stop offset="0.5" stop-color="#fff" stop-opacity="0.85" /><stop offset="1" stop-color="#fff" stop-opacity="0" />
</linearGradient>
</defs>
<g clip-path="url(#ldClip)">
<rect width="1000" height="200" fill="rgba(201,164,92,0.06)" />
<!-- the liquid is rotated 60°, so "rising" becomes a diagonal sweep from left to right -->
<g transform="translate(500 100) rotate(60)">
<g class="ld-liquid">
<path class="ld-wave ld-wave--back" d="" fill="#8a6a2e" opacity="0.75" />
<path class="ld-wave ld-wave--front" d="" fill="url(#ldGold)" stroke="rgba(255,241,201,0.7)" stroke-width="1.4" />
</g>
</g>
<g class="ld-glint"><rect x="-260" y="0" width="200" height="200" fill="url(#ldGlint)" transform="skewX(-20)" /></g>
</g>
<text x="500" y="150" text-anchor="middle" class="ld-text ld-outline">VERANCE</text>
</svg>
<div class="loader__meta">
<span class="loader__line"><i></i></span>
<span class="loader__tag">Haute Horlogerie — Genève — Est. 1874</span>
</div>
</div>
<div class="grain" aria-hidden="true"></div>
<div class="glow" aria-hidden="true"></div>
<canvas id="webgl" aria-hidden="true"></canvas>
<div class="cursor" aria-hidden="true">
<div class="cursor__dot"></div>
<div class="cursor__ring"><span>Drag</span></div>
</div>
<div class="progress" aria-hidden="true"><i></i></div>
<!-- Callouts projected from the 3D exploded view -->
<div class="callouts" aria-hidden="true">
<div class="callout" data-part="crystal" data-side="right"><span class="callout__line"></span><div class="callout__text"><em>I</em><b>Sapphire Crystal</b><small>Double-domed, 9 coats anti-reflective</small></div></div>
<div class="callout" data-part="bezel" data-side="left"><span class="callout__line"></span><div class="callout__text"><em>II</em><b>Ceramic Bezel</b><small>Unidirectional, gold-filled numerals</small></div></div>
<div class="callout" data-part="hands" data-side="right"><span class="callout__line"></span><div class="callout__text"><em>III</em><b>Dauphine Hands</b><small>Hand-polished 18k gold, three levels</small></div></div>
<div class="callout" data-part="dial" data-side="left"><span class="callout__line"></span><div class="callout__text"><em>IV</em><b>Sunray Dial</b><small>Guilloché brass, applied indices</small></div></div>
<div class="callout" data-part="movement" data-side="right"><span class="callout__line"></span><div class="callout__text"><em>V</em><b>Calibre V-88</b><small>Bridges, gear train & balance at 4 Hz</small></div></div>
<div class="callout" data-part="case" data-side="left"><span class="callout__line"></span><div class="callout__text"><em>VI</em><b>18k Gold Case</b><small>41 mm, forged & mirror-finished</small></div></div>
<div class="callout" data-part="rotor" data-side="right"><span class="callout__line"></span><div class="callout__text"><em>VII</em><b>Oscillating Rotor</b><small>22k gold, winds on every movement</small></div></div>
<div class="callout" data-part="caseback" data-side="left"><span class="callout__line"></span><div class="callout__text"><em>VIII</em><b>Exhibition Caseback</b><small>Sapphire window, engraved Nº /188</small></div></div>
</div>
<!-- Clasp lock indicator (projected from 3D) -->
<div class="lock-chip" aria-hidden="true">
<span class="lock-chip__icon"><svg viewBox="0 0 24 24" fill="none"><path class="lock-chip__shackle" d="M8 11V8a4 4 0 0 1 8 0v3"/><rect x="5" y="11" width="14" height="10" rx="2"/></svg></span>
<span class="lock-chip__text"><b>Papillon clasp</b><small class="lock-chip__state">Unlocked</small></span>
</div>
<header class="nav">
<a href="#hero" class="nav__logo" data-hover>VERANCE</a>
<nav class="nav__links">
<a href="#maison" data-hover>Maison</a>
<a href="#anatomie" data-hover>Anatomie</a>
<a href="#calibre" data-hover>Calibre</a>
<a href="#collection" data-hover>Collection</a>
</nav>
<a href="#reserve" class="btn btn--ghost magnetic" data-hover><span>Reserve</span></a>
<button class="nav__menu" aria-label="Open menu" aria-expanded="false"><i></i><i></i></button>
</header>
<nav class="menu" aria-label="Mobile">
<a href="#maison" class="menu__link"><em>01</em> La Maison</a>
<a href="#anatomie" class="menu__link"><em>02</em> Anatomie</a>
<a href="#calibre" class="menu__link"><em>03</em> Calibre</a>
<a href="#collection" class="menu__link"><em>04</em> Configure</a>
<a href="#voices" class="menu__link"><em>05</em> Collectors</a>
<a href="#reserve" class="menu__link"><em>06</em> Reserve</a>
<p class="menu__foot">Quai du Mont-Blanc 19 · Genève</p>
</nav>
</div>
```
**js**
```js
const uichemy_controller_bridge = {
"lenis_lerp": {
"value": 0.085,
"label": "Smooth-scroll easing",
"min": 0.02,
"max": 0.3,
"step": 0.005,
"group": "Page"
},
"tone_exposure": {
"value": 1.05,
"label": "Watch exposure",
"min": 0.5,
"max": 2,
"step": 0.01,
"group": "Watch"
},
"env_intensity": {
"value": 0.95,
"label": "Reflections",
"min": 0,
"max": 2,
"step": 0.01,
"group": "Watch"
}
};
(function () {
'use strict';
var val = function (v) { return v && typeof v === 'object' && 'value' in v ? v.value : v; };
var M = window.VERANCE_MOTION = window.VERANCE_MOTION || {};
M.lenis_lerp = val(uichemy_controller_bridge.lenis_lerp);
M.tone_exposure = val(uichemy_controller_bridge.tone_exposure);
M.env_intensity = val(uichemy_controller_bridge.env_intensity);
// The engine lives in /wp-content/uichemy-composer/verance-app.js (shared by every section of
// this page). Ask it to rebuild: the Composer re-runs this script on every edit in the editor.
// If the site-level boot has not loaded it (e.g. inside the editor), load it from here.
if (window.VeranceEngine) { window.VeranceEngine.requestRefresh(); return; }
if (document.querySelector('script[src*="/uichemy-composer/verance-app.js"]')) return;
var s = document.createElement('script');
s.src = '/wp-content/uichemy-composer/verance-app.js?v=5';
s.async = false;
document.head.appendChild(s);
})();
```
### B2.2 · Hero
label: `Hero`
**html**
```html
<div class="verance-nocturne uichemy-verance-hero-2">
<!-- HERO -->
<section id="hero" class="hero">
<div class="hero__title" aria-hidden="true">
<span class="hero__word" data-split="chars">Nocturne</span>
</div>
<div class="hero__top content">
<p class="eyebrow hero__eyebrow"><span class="line"></span> The 2026 Collection · Limited to 188 pieces</p>
</div>
<div class="hero__bottom content">
<div class="hero__intro">
<h1 class="hero__lead">Time, <em>cast in gold.</em></h1>
<p>An 18k automatic chronometer, finished by hand in Genève over four hundred hours.</p>
</div>
<a href="#maison" class="scroll-cue" data-hover>
<span class="scroll-cue__track"><i></i></span>
<span>Scroll to discover</span>
</a>
<div class="hero__time">
<span class="eyebrow">Your local time</span>
<span class="hero__clock" id="clock">00:00:00</span>
<small>The hands you see are live.</small>
</div>
</div>
</section>
</div>
```
**js**
```js
const uichemy_controller_bridge = {
"intro_duration": {
"value": 2.8,
"unit": "s",
"label": "Watch intro",
"target": ".hero__title",
"min": 0.5,
"max": 6,
"step": 0.1
},
"hero_fan": {
"value": 16,
"unit": "deg",
"label": "Letter fan on scroll",
"target": ".hero__word",
"min": 0,
"max": 40,
"step": 1
},
"hero_depth": {
"value": 60,
"unit": "px",
"label": "Letter depth on scroll",
"target": ".hero__word",
"min": 0,
"max": 200,
"step": 5
}
};
(function () {
'use strict';
var val = function (v) { return v && typeof v === 'object' && 'value' in v ? v.value : v; };
var M = window.VERANCE_MOTION = window.VERANCE_MOTION || {};
M.intro_duration = val(uichemy_controller_bridge.intro_duration);
M.hero_fan = val(uichemy_controller_bridge.hero_fan);
M.hero_depth = val(uichemy_controller_bridge.hero_depth);
// The engine lives in /wp-content/uichemy-composer/verance-app.js (shared by every section of
// this page). Ask it to rebuild: the Composer re-runs this script on every edit in the editor.
// If the site-level boot has not loaded it (e.g. inside the editor), load it from here.
if (window.VeranceEngine) { window.VeranceEngine.requestRefresh(); return; }
if (document.querySelector('script[src*="/uichemy-composer/verance-app.js"]')) return;
var s = document.createElement('script');
s.src = '/wp-content/uichemy-composer/verance-app.js?v=5';
s.async = false;
document.head.appendChild(s);
})();
```
### B2.3 · La Maison
label: `La Maison`
**html**
```html
<div class="verance-nocturne uichemy-verance-maison-3">
<!-- MAISON -->
<section id="maison" class="maison">
<div class="maison__copy content">
<p class="eyebrow" data-reveal="fade"><span class="num">01</span> La Maison</p>
<h2 class="display display--md" data-reveal="words">A century and a half of measuring the <em>immeasurable.</em></h2>
<div class="maison__row" data-reveal="fade">
<p class="body">Since 1874, seven generations of Verance watchmakers have worked from the same atelier above Lake Geneva. Every Nocturne is assembled, regulated and cased by a single master.</p>
<div class="signature">
<span class="signature__mark">É.V.</span>
<small>Élise Varenne<br />7th-generation master</small>
</div>
</div>
<div class="cards">
<article class="card" data-hover>
<span class="card__shine"></span>
<div class="card__top">
<svg class="card__icon" viewBox="0 0 40 40" fill="none"><path pathLength="1" d="M5 16 20 6l15 10M8 16v16m8-16v16m8-16v16m8-16v16M4 34h32"/></svg>
<span class="card__idx">I</span>
</div>
<b class="card__num" data-count="1874" data-plain>1874</b>
<span class="card__label">Founded in Genève</span>
<p class="card__desc">One atelier, one lake view, seven generations.</p>
<i class="card__line"></i>
</article>
<article class="card" data-hover>
<span class="card__shine"></span>
<div class="card__top">
<svg class="card__icon" viewBox="0 0 40 40" fill="none"><path pathLength="1" d="M10 5h20M10 35h20M12 5c0 9 16 9 16 15s-16 6-16 15M28 5c0 9-16 9-16 15s16 6 16 15"/></svg>
<span class="card__idx">II</span>
</div>
<b class="card__num"><span data-count="400">400</span><sup>h</sup></b>
<span class="card__label">Hours per piece</span>
<p class="card__desc">Each bevel and anglage finished by hand.</p>
<i class="card__line"></i>
</article>
<article class="card" data-hover>
<span class="card__shine"></span>
<div class="card__top">
<svg class="card__icon" viewBox="0 0 40 40" fill="none"><path pathLength="1" d="M17 6a11 11 0 1 1 0 22 11 11 0 0 1 0-22Zm8 19 10 10M12 17a5 5 0 0 1 5-5"/></svg>
<span class="card__idx">III</span>
</div>
<b class="card__num" data-count="7">7</b>
<span class="card__label">Master watchmakers</span>
<p class="card__desc">Their initials, engraved beneath the rotor.</p>
<i class="card__line"></i>
</article>
</div>
</div>
</section>
</div>
```
**js**
```js
(function () {
'use strict';
// The engine lives in /wp-content/uichemy-composer/verance-app.js (shared by every section of
// this page). Ask it to rebuild: the Composer re-runs this script on every edit in the editor.
// If the site-level boot has not loaded it (e.g. inside the editor), load it from here.
if (window.VeranceEngine) { window.VeranceEngine.requestRefresh(); return; }
if (document.querySelector('script[src*="/uichemy-composer/verance-app.js"]')) return;
var s = document.createElement('script');
s.src = '/wp-content/uichemy-composer/verance-app.js?v=5';
s.async = false;
document.head.appendChild(s);
})();
```
### B2.4 · Anatomie
label: `Anatomie`
**html**
```html
<div class="verance-nocturne uichemy-verance-anatomie-4">
<!-- ANATOMY (pinned, exploded 3D view) -->
<section id="anatomie" class="anatomy">
<div class="anatomy__head content">
<p class="eyebrow"><span class="num">02</span> Anatomie</p>
<h2 class="display display--sm" data-reveal="words">212 components. <em>One heartbeat.</em></h2>
</div>
<div class="anatomy__foot content">
<span class="eyebrow">Scroll to disassemble</span>
<span class="anatomy__meter"><i></i></span>
</div>
</section>
</div>
```
**js**
```js
const uichemy_controller_bridge = {
"pin_length": {
"value": 220,
"unit": "%",
"label": "Exploded-view pin length",
"target": ".anatomy",
"min": 100,
"max": 400,
"step": 10
}
};
(function () {
'use strict';
var val = function (v) { return v && typeof v === 'object' && 'value' in v ? v.value : v; };
var M = window.VERANCE_MOTION = window.VERANCE_MOTION || {};
M.pin_length = val(uichemy_controller_bridge.pin_length);
// The engine lives in /wp-content/uichemy-composer/verance-app.js (shared by every section of
// this page). Ask it to rebuild: the Composer re-runs this script on every edit in the editor.
// If the site-level boot has not loaded it (e.g. inside the editor), load it from here.
if (window.VeranceEngine) { window.VeranceEngine.requestRefresh(); return; }
if (document.querySelector('script[src*="/uichemy-composer/verance-app.js"]')) return;
var s = document.createElement('script');
s.src = '/wp-content/uichemy-composer/verance-app.js?v=5';
s.async = false;
document.head.appendChild(s);
})();
```
### B2.5 · Calibre
label: `Calibre`
**html**
```html
<div class="verance-nocturne uichemy-verance-calibre-5">
<!-- CALIBRE -->
<section id="calibre" class="calibre">
<div class="calibre__copy content">
<p class="eyebrow" data-reveal="fade"><span class="num">03</span> Calibre V-88</p>
<h2 class="display" data-reveal="words">Engineered for the <em>long night.</em></h2>
<p class="body" data-reveal="fade">An in-house automatic movement beating at four hertz, with twin barrels that carry three full days of reserve. Certified to −2/+2 seconds a day — twice as strict as chronometer standard.</p>
<div class="specs" data-reveal="fade">
<div class="spec"><b><span data-count="72">72</span>h</b><span>Power reserve</span></div>
<div class="spec"><b><span data-count="28800">28,800</span></b><span>Vibrations / hour</span></div>
<div class="spec"><b><span data-count="42">42</span></b><span>Ruby jewels</span></div>
<div class="spec"><b><span data-count="212">212</span></b><span>Components</span></div>
<div class="spec"><b><span data-count="100">100</span>m</b><span>Water resistance</span></div>
<div class="spec"><b><span data-count="41">41</span>mm</b><span>Case diameter</span></div>
</div>
</div>
</section>
</div>
```
**js**
```js
(function () {
'use strict';
// The engine lives in /wp-content/uichemy-composer/verance-app.js (shared by every section of
// this page). Ask it to rebuild: the Composer re-runs this script on every edit in the editor.
// If the site-level boot has not loaded it (e.g. inside the editor), load it from here.
if (window.VeranceEngine) { window.VeranceEngine.requestRefresh(); return; }
if (document.querySelector('script[src*="/uichemy-composer/verance-app.js"]')) return;
var s = document.createElement('script');
s.src = '/wp-content/uichemy-composer/verance-app.js?v=5';
s.async = false;
document.head.appendChild(s);
})();
```
### B2.6 · Configure
label: `Configure`
**html**
```html
<div class="verance-nocturne uichemy-verance-configure-6">
<!-- COLLECTION / CONFIGURATOR -->
<section id="collection" class="collection">
<div class="collection__stage" data-drag aria-label="Drag to rotate the watch"></div>
<div class="collection__panel content">
<p class="eyebrow" data-reveal="fade"><span class="num">04</span> Configure</p>
<h2 class="display display--sm" data-reveal="words">Make it unmistakably <em>yours.</em></h2>
<div class="opt">
<div class="opt__label"><span>Case</span><b id="caseName">Or Jaune 18k</b></div>
<div class="opt__row">
<button class="swatch is-active" data-case="yellow" data-name="Or Jaune 18k" data-price="48500" style="--c1:#f6dc9a;--c2:#b8862f" data-hover aria-label="Yellow gold"></button>
<button class="swatch" data-case="rose" data-name="Or Rose 18k" data-price="51200" style="--c1:#f5c7b1;--c2:#b56e52" data-hover aria-label="Rose gold"></button>
<button class="swatch" data-case="noir" data-name="Platine Noir DLC" data-price="62800" style="--c1:#5b5b60;--c2:#111113" data-hover aria-label="Black platinum"></button>
</div>
</div>
<div class="opt">
<div class="opt__label"><span>Dial</span><b id="dialName">Noir Abysse</b></div>
<div class="opt__row">
<button class="swatch is-active" data-dial="noir" data-name="Noir Abysse" style="--c1:#3a3632;--c2:#050505" data-hover aria-label="Black dial"></button>
<button class="swatch" data-dial="ivoire" data-name="Ivoire Grand Feu" style="--c1:#fffaf0;--c2:#d6c8a8" data-hover aria-label="Ivory dial"></button>
<button class="swatch" data-dial="emeraude" data-name="Vert Émeraude" style="--c1:#2a8a69;--c2:#03140d" data-hover aria-label="Emerald dial"></button>
<button class="swatch" data-dial="nuit" data-name="Bleu Nuit" style="--c1:#3b64a8;--c2:#040a18" data-hover aria-label="Midnight blue dial"></button>
</div>
</div>
<div class="price">
<div>
<span class="eyebrow">From</span>
<b class="price__val">CHF <span id="price">48,500</span></b>
</div>
<a href="#reserve" class="btn btn--gold magnetic" data-hover><span>Reserve this piece</span></a>
</div>
<p class="hint">← Drag the watch to turn it in the light</p>
</div>
</section>
</div>
```
**js**
```js
(function () {
'use strict';
// The engine lives in /wp-content/uichemy-composer/verance-app.js (shared by every section of
// this page). Ask it to rebuild: the Composer re-runs this script on every edit in the editor.
// If the site-level boot has not loaded it (e.g. inside the editor), load it from here.
if (window.VeranceEngine) { window.VeranceEngine.requestRefresh(); return; }
if (document.querySelector('script[src*="/uichemy-composer/verance-app.js"]')) return;
var s = document.createElement('script');
s.src = '/wp-content/uichemy-composer/verance-app.js?v=5';
s.async = false;
document.head.appendChild(s);
})();
```
### B2.7 · Marquee
label: `Marquee`
**html**
```html
<div class="verance-nocturne uichemy-verance-marquee-7">
<!-- MARQUEE -->
<section class="marquee" aria-hidden="true">
<div class="marquee__track">
<span>Nocturne</span><i>✦</i><span class="outline">Aurore</span><i>✦</i><span>Solstice</span><i>✦</i><span class="outline">Équinoxe</span><i>✦</i>
<span>Nocturne</span><i>✦</i><span class="outline">Aurore</span><i>✦</i><span>Solstice</span><i>✦</i><span class="outline">Équinoxe</span><i>✦</i>
</div>
</section>
</div>
```
**js**
```js
const uichemy_controller_bridge = {
"marquee_duration": {
"value": 28,
"unit": "s",
"label": "Loop duration",
"target": ".marquee__track",
"min": 8,
"max": 80,
"step": 1
}
};
(function () {
'use strict';
var val = function (v) { return v && typeof v === 'object' && 'value' in v ? v.value : v; };
var M = window.VERANCE_MOTION = window.VERANCE_MOTION || {};
M.marquee_duration = val(uichemy_controller_bridge.marquee_duration);
// The engine lives in /wp-content/uichemy-composer/verance-app.js (shared by every section of
// this page). Ask it to rebuild: the Composer re-runs this script on every edit in the editor.
// If the site-level boot has not loaded it (e.g. inside the editor), load it from here.
if (window.VeranceEngine) { window.VeranceEngine.requestRefresh(); return; }
if (document.querySelector('script[src*="/uichemy-composer/verance-app.js"]')) return;
var s = document.createElement('script');
s.src = '/wp-content/uichemy-composer/verance-app.js?v=5';
s.async = false;
document.head.appendChild(s);
})();
```
### B2.8 · Quote
label: `Quote`
**html**
```html
<div class="verance-nocturne uichemy-verance-quote-8">
<!-- QUOTE -->
<section id="quote" class="quote">
<blockquote class="content">
<p class="quote__text" data-split="scrub">“We do not make watches. We make heirlooms that happen to tell the time.”</p>
<footer><span class="line"></span> Élise Varenne, Master Watchmaker</footer>
</blockquote>
</section>
</div>
```
**js**
```js
(function () {
'use strict';
// The engine lives in /wp-content/uichemy-composer/verance-app.js (shared by every section of
// this page). Ask it to rebuild: the Composer re-runs this script on every edit in the editor.
// If the site-level boot has not loaded it (e.g. inside the editor), load it from here.
if (window.VeranceEngine) { window.VeranceEngine.requestRefresh(); return; }
if (document.querySelector('script[src*="/uichemy-composer/verance-app.js"]')) return;
var s = document.createElement('script');
s.src = '/wp-content/uichemy-composer/verance-app.js?v=5';
s.async = false;
document.head.appendChild(s);
})();
```
### B2.9 · Reserve
label: `Reserve`
**html**
```html
<div class="verance-nocturne uichemy-verance-reserve-9">
<!-- RESERVE -->
<section id="reserve" class="reserve">
<div class="reserve__copy content">
<p class="eyebrow" data-reveal="fade"><span class="num">05</span> Private Viewing</p>
<h2 class="display" data-reveal="words">Only 188 pieces. <em>Each numbered, never repeated.</em></h2>
<p class="body" data-reveal="fade">Request an appointment at our Genève salon, or have a Verance concierge bring the Nocturne to you.</p>
<form class="reserve__form" data-reveal="fade" novalidate>
<label class="field">
<input type="email" required placeholder=" " id="email" autocomplete="email" />
<span>Your email address</span>
</label>
<button type="submit" class="btn btn--gold magnetic" data-hover><span>Request viewing</span></button>
</form>
<p class="reserve__msg" role="status"></p>
</div>
</section>
</div>
```
**js**
```js
(function () {
'use strict';
// The engine lives in /wp-content/uichemy-composer/verance-app.js (shared by every section of
// this page). Ask it to rebuild: the Composer re-runs this script on every edit in the editor.
// If the site-level boot has not loaded it (e.g. inside the editor), load it from here.
if (window.VeranceEngine) { window.VeranceEngine.requestRefresh(); return; }
if (document.querySelector('script[src*="/uichemy-composer/verance-app.js"]')) return;
var s = document.createElement('script');
s.src = '/wp-content/uichemy-composer/verance-app.js?v=5';
s.async = false;
document.head.appendChild(s);
})();
```
### B2.10 · Collectors
label: `Collectors`
**html**
```html
<div class="verance-nocturne uichemy-verance-collectors-10">
<!-- COLLECTORS / TESTIMONIALS -->
<section id="voices" class="voices">
<div class="voices__stage" aria-hidden="true">
<span class="voices__ring"></span>
<span class="voices__caption">Their piece · <span id="voiceConfig">Or Jaune 18k · Noir Abysse</span></span>
</div>
<div class="voices__panel content">
<p class="eyebrow" data-reveal="fade"><span class="num">06</span> Collectors</p>
<h2 class="display display--sm" data-reveal="words">Worn by those who <em>wait for nothing.</em></h2>
<div class="voices__slides" aria-live="polite">
<figure class="voice is-active" data-case="yellow" data-dial="noir">
<blockquote class="voice__quote">“I have owned watches for forty years. The Nocturne is the first one I check the time on simply to look at it.”</blockquote>
<figcaption class="voice__by">
<span class="voice__mono">HL</span>
<span class="voice__who"><b>Henri Laurent</b><small>Paris · Collector since 1998</small></span>
<span class="voice__piece">Nº 007 / 188</span>
</figcaption>
</figure>
<figure class="voice" data-case="rose" data-dial="nuit">
<blockquote class="voice__quote">“The dial changes with the light of the room. By evening it turns almost violet, and I find myself turning my wrist just to watch it.”</blockquote>
<figcaption class="voice__by">
<span class="voice__mono">AO</span>
<span class="voice__who"><b>Amara Okafor</b><small>Lagos · Architect</small></span>
<span class="voice__piece">Nº 042 / 188</span>
</figcaption>
</figure>
<figure class="voice" data-case="noir" data-dial="emeraude">
<blockquote class="voice__quote">“Élise wound it in front of me at the salon. Two years later it has not lost a second I could ever notice.”</blockquote>
<figcaption class="voice__by">
<span class="voice__mono">KT</span>
<span class="voice__who"><b>Kenji Tanaka</b><small>Kyoto · Tea master</small></span>
<span class="voice__piece">Nº 113 / 188</span>
</figcaption>
</figure>
<figure class="voice" data-case="yellow" data-dial="ivoire">
<blockquote class="voice__quote">“My daughter already asks for it. That is how I know it was the right choice: it was bought for two lifetimes.”</blockquote>
<figcaption class="voice__by">
<span class="voice__mono">SM</span>
<span class="voice__who"><b>Sofia Marchetti</b><small>Milano · Collector since 2011</small></span>
<span class="voice__piece">Nº 161 / 188</span>
</figcaption>
</figure>
</div>
<div class="voices__nav">
<button class="voices__btn magnetic" data-dir="-1" data-hover aria-label="Previous testimonial"><svg viewBox="0 0 24 24" fill="none"><path d="M19 12H5M11 6l-6 6 6 6" /></svg></button>
<button class="voices__btn magnetic" data-dir="1" data-hover aria-label="Next testimonial"><svg viewBox="0 0 24 24" fill="none"><path d="M5 12h14M13 6l6 6-6 6" /></svg></button>
<span class="voices__count"><b id="voiceIdx">01</b> / 04</span>
<span class="voices__bar"><i></i></span>
</div>
</div>
</section>
</div>
```
**js**
```js
const uichemy_controller_bridge = {
"autoplay_seconds": {
"value": 7,
"unit": "s",
"label": "Autoplay interval",
"target": ".voices__slides",
"min": 3,
"max": 20,
"step": 0.5
}
};
(function () {
'use strict';
var val = function (v) { return v && typeof v === 'object' && 'value' in v ? v.value : v; };
var M = window.VERANCE_MOTION = window.VERANCE_MOTION || {};
M.autoplay_seconds = val(uichemy_controller_bridge.autoplay_seconds);
// The engine lives in /wp-content/uichemy-composer/verance-app.js (shared by every section of
// this page). Ask it to rebuild: the Composer re-runs this script on every edit in the editor.
// If the site-level boot has not loaded it (e.g. inside the editor), load it from here.
if (window.VeranceEngine) { window.VeranceEngine.requestRefresh(); return; }
if (document.querySelector('script[src*="/uichemy-composer/verance-app.js"]')) return;
var s = document.createElement('script');
s.src = '/wp-content/uichemy-composer/verance-app.js?v=5';
s.async = false;
document.head.appendChild(s);
})();
```
### B2.11 · Footer
label: `Footer`
**html**
```html
<div class="verance-nocturne uichemy-verance-footer-11">
<footer class="footer" id="footer">
<div class="footer__grid content">
<div class="footer__col">
<span class="eyebrow">Salon</span>
<p>Quai du Mont-Blanc 19<br />1201 Genève, Suisse</p>
<a href="#reserve" class="footer__link" data-hover>Book a viewing</a>
</div>
<div class="footer__col">
<span class="eyebrow">Atelier</span>
<p>Visits by appointment<br />Monday – Saturday, 10–18h</p>
<a href="#" class="footer__link" data-hover>+41 22 000 18 74</a>
</div>
<div class="footer__col">
<span class="eyebrow">Explore</span>
<nav>
<a href="#maison" class="footer__link" data-hover>La Maison</a>
<a href="#anatomie" class="footer__link" data-hover>Anatomie</a>
<a href="#calibre" class="footer__link" data-hover>Calibre V-88</a>
<a href="#collection" class="footer__link" data-hover>Configure</a>
</nav>
</div>
<div class="footer__col">
<span class="eyebrow">Follow</span>
<nav>
<a href="#" class="footer__link" data-hover>Instagram</a>
<a href="#" class="footer__link" data-hover>Journal</a>
<a href="#" class="footer__link" data-hover>YouTube</a>
</nav>
</div>
</div>
<div class="footer__legal content">
<span>© 2026 Verance SA — a fictional maison</span>
<span class="footer__badge">Swiss Made · Genève</span>
<a href="#hero" class="footer__top-link" data-hover>Back to top <svg viewBox="0 0 24 24" fill="none"><path d="M12 19V5M6 11l6-6 6 6" /></svg></a>
</div>
<div class="footer__brand" aria-hidden="true"><span data-split="brand">VERANCE</span></div>
</footer>
</div>
```
**js**
```js
(function () {
'use strict';
// The engine lives in /wp-content/uichemy-composer/verance-app.js (shared by every section of
// this page). Ask it to rebuild: the Composer re-runs this script on every edit in the editor.
// If the site-level boot has not loaded it (e.g. inside the editor), load it from here.
if (window.VeranceEngine) { window.VeranceEngine.requestRefresh(); return; }
if (document.querySelector('script[src*="/uichemy-composer/verance-app.js"]')) return;
var s = document.createElement('script');
s.src = '/wp-content/uichemy-composer/verance-app.js?v=5';
s.async = false;
document.head.appendChild(s);
})();
```
## B3 · `verance-app.css` (code file, served as `?v=2`)
```css
/* Verance Nocturne — the source's css/style.css, scoped to this page.
*
* Every selector carries the same `html:has(.verance-nocturne)` prefix, which adds one identical
* specificity step to every rule, so any two rules still compare exactly as they did in
* the original. Rules for the page's own markup also carry `:where(.verance-nocturne)`, which adds
* nothing to specificity but stops them reaching Elementor's editor chrome or the theme.
* The values themselves are untouched.
*
* Fonts: the Google files, self-hosted in the WordPress media library, declared per weight
* exactly as Google's stylesheet declares them.
*/
@font-face { font-family: 'Cormorant Garamond'; font-style: normal; font-weight: 300; font-display: swap; src: url(https://demo.uichemy.com/wp-content/uploads/2026/09/verance-cormorant-garamond.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: 'Cormorant Garamond'; font-style: normal; font-weight: 400; font-display: swap; src: url(https://demo.uichemy.com/wp-content/uploads/2026/09/verance-cormorant-garamond.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: 'Cormorant Garamond'; font-style: normal; font-weight: 500; font-display: swap; src: url(https://demo.uichemy.com/wp-content/uploads/2026/09/verance-cormorant-garamond.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: 'Cormorant Garamond'; font-style: normal; font-weight: 600; font-display: swap; src: url(https://demo.uichemy.com/wp-content/uploads/2026/09/verance-cormorant-garamond.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: 'Cormorant Garamond'; font-style: italic; font-weight: 300; font-display: swap; src: url(https://demo.uichemy.com/wp-content/uploads/2026/09/verance-cormorant-garamond-italic.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: 'Cormorant Garamond'; font-style: italic; font-weight: 400; font-display: swap; src: url(https://demo.uichemy.com/wp-content/uploads/2026/09/verance-cormorant-garamond-italic.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: 'Cormorant Garamond'; font-style: italic; font-weight: 500; font-display: swap; src: url(https://demo.uichemy.com/wp-content/uploads/2026/09/verance-cormorant-garamond-italic.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: 'Jost'; font-style: normal; font-weight: 300; font-display: swap; src: url(https://demo.uichemy.com/wp-content/uploads/2026/09/verance-jost.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: 'Jost'; font-style: normal; font-weight: 400; font-display: swap; src: url(https://demo.uichemy.com/wp-content/uploads/2026/09/verance-jost.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: 'Jost'; font-style: normal; font-weight: 500; font-display: swap; src: url(https://demo.uichemy.com/wp-content/uploads/2026/09/verance-jost.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
html:has(.verance-nocturne) {
--noir: var(--vn-noir, #0a0908);
--noir-2: var(--vn-noir-2, #13100c);
--ivory: var(--vn-ivory, #f5efe3);
--ivory-2: var(--vn-ivory-2, #ebe2d0);
--gold: var(--vn-gold, #c9a45c);
--gold-hi: var(--vn-gold-hi, #f1d9a0);
--gold-lo: var(--vn-gold-lo, #8a6a2e);
--bg: var(--noir);
--fg: #efe7d8;
--muted: rgba(239, 231, 216, 0.58);
--hair: rgba(201, 164, 92, 0.28);
--serif: var(--vn-serif, "Cormorant Garamond", "Times New Roman", serif);
--sans: var(--vn-sans, "Jost", "Helvetica Neue", Arial, sans-serif);
--ease: cubic-bezier(0.19, 1, 0.22, 1);
--pad: clamp(20px, 4vw, 64px);
}
html:has(.verance-nocturne) body.is-light {
--bg: var(--ivory);
--fg: #1e1810;
--muted: rgba(30, 24, 16, 0.62);
--hair: rgba(138, 106, 46, 0.35);
}
html:has(.verance-nocturne) :where(.verance-nocturne) *,
html:has(.verance-nocturne) :where(.verance-nocturne),
html:has(.verance-nocturne) :where(.verance-nocturne) *::before,
html:has(.verance-nocturne) :where(.verance-nocturne) *::after { box-sizing: border-box; margin: 0; padding: 0; }
html:has(.verance-nocturne) { -webkit-font-smoothing: antialiased; overflow-x: clip; }
html.lenis:has(.verance-nocturne),
html.lenis:has(.verance-nocturne) body { height: auto; }
html.lenis.lenis-smooth:has(.verance-nocturne) { scroll-behavior: auto !important; }
html:has(.verance-nocturne) body {
background: var(--bg);
color: var(--fg);
font-family: var(--sans);
font-weight: 300;
font-size: 16px;
line-height: 1.6;
overflow-x: clip;
transition: background-color 1s var(--ease), color 1s var(--ease);
}
html:has(.verance-nocturne) body.is-loading { overflow: hidden; }
html:has(.verance-nocturne) :where(.verance-nocturne) a { color: inherit; text-decoration: none; }
html:has(.verance-nocturne) :where(.verance-nocturne) button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
html:has(.verance-nocturne) :where(.verance-nocturne) em { font-style: italic; color: var(--gold); }
html:has(.verance-nocturne) body.is-light :where(.verance-nocturne) em { color: var(--gold-lo); }
@media (hover: hover) and (pointer: fine) {
html:has(.verance-nocturne) body:not(.elementor-editor-active),
html:has(.verance-nocturne) body:not(.elementor-editor-active) :where(.verance-nocturne) a,
html:has(.verance-nocturne) body:not(.elementor-editor-active) :where(.verance-nocturne) button,
html:has(.verance-nocturne) body:not(.elementor-editor-active) :where(.verance-nocturne) input { cursor: none; }
}
/* ---------- Layers ---------- */
html:has(.verance-nocturne) :where(.verance-nocturne) #webgl {
position: fixed; inset: 0;
width: 100vw; height: 100vh;
z-index: 1;
pointer-events: none;
}
html:has(.verance-nocturne) :where(.verance-nocturne) .glow {
position: fixed; inset: 0; z-index: 0; pointer-events: none;
background:
radial-gradient(38vmax 38vmax at var(--gx, 50%) var(--gy, 50%), rgba(201, 164, 92, 0.18), transparent 70%),
radial-gradient(80vmax 60vmax at 50% 120%, rgba(201, 164, 92, 0.08), transparent 70%);
transition: opacity 1s var(--ease);
}
html:has(.verance-nocturne) body.is-light :where(.verance-nocturne) .glow { opacity: 0.55; }
html:has(.verance-nocturne) :where(.verance-nocturne) .grain {
position: fixed; inset: 0; z-index: 50; pointer-events: none; opacity: 0.07;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.9 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
animation: grain 1.2s steps(6) infinite;
}
/* animate the texture, not the element, so it never widens the page */
@keyframes grain {
0% { background-position: 0 0; } 20% { background-position: -37px 23px; } 40% { background-position: 29px -51px; }
60% { background-position: -53px -11px; } 80% { background-position: 41px 37px; } 100% { background-position: 0 0; }
}
html:has(.verance-nocturne) :where(.verance-nocturne) main { position: relative; }
html:has(.verance-nocturne) :where(.verance-nocturne) .content { position: relative; z-index: 2; }
/* ---------- Loader ---------- */
html:has(.verance-nocturne) :where(.verance-nocturne) .loader {
position: fixed; inset: 0; z-index: 100; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 22px;
background: radial-gradient(circle at 50% 50%, #17130d, var(--noir) 65%);
/* --g opens a horizontal gap through the middle: the screen splits like a caseback */
--g: 0vh;
-webkit-mask: linear-gradient(180deg, #000 calc(50% - var(--g)), transparent calc(50% - var(--g)), transparent calc(50% + var(--g)), #000 calc(50% + var(--g)));
mask: linear-gradient(180deg, #000 calc(50% - var(--g)), transparent calc(50% - var(--g)), transparent calc(50% + var(--g)), #000 calc(50% + var(--g)));
}
html:has(.verance-nocturne) :where(.verance-nocturne) .loader__word { position: relative; z-index: 1; width: min(86vw, 900px); height: auto; overflow: visible; }
/* faint line-drawn watch sitting behind the wordmark */
html:has(.verance-nocturne) :where(.verance-nocturne) .loader__watch {
position: absolute; left: 50%; top: 50%; width: auto; height: min(92vh, 150vw);
opacity: 0.16; overflow: visible; pointer-events: none; /* centred with GSAP xPercent/yPercent */
}
html:has(.verance-nocturne) :where(.verance-nocturne) .loader__watch * { fill: none; stroke: var(--gold-hi); stroke-width: 1.2; stroke-linecap: round; }
html:has(.verance-nocturne) :where(.verance-nocturne) .loader__watch [pathLength] { stroke-dasharray: 1; stroke-dashoffset: 1; }
html:has(.verance-nocturne) :where(.verance-nocturne) .loader__watch .lw-ring--dial { stroke-width: 0.8; }
html:has(.verance-nocturne) :where(.verance-nocturne) .loader__watch .lw-links line { stroke-width: 0.9; }
html:has(.verance-nocturne) :where(.verance-nocturne) .loader__watch .lw-hour { stroke-width: 4; }
html:has(.verance-nocturne) :where(.verance-nocturne) .loader__watch .lw-min { stroke-width: 2.4; }
html:has(.verance-nocturne) :where(.verance-nocturne) .loader__watch .lw-sec { stroke-width: 1; }
html:has(.verance-nocturne) :where(.verance-nocturne) .loader__watch .lw-hands circle { fill: var(--gold-hi); }
html:has(.verance-nocturne) :where(.verance-nocturne) .ld-text { font-family: var(--serif); font-weight: 400; font-size: 168px; letter-spacing: 22px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .ld-outline { fill: none; stroke: rgba(241, 217, 160, 0.4); stroke-width: 0.8; }
html:has(.verance-nocturne) :where(.verance-nocturne) .loader__meta { display: flex; flex-direction: column; align-items: center; gap: 16px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .loader__line { width: 160px; height: 1px; background: rgba(201, 164, 92, 0.2); overflow: hidden; }
html:has(.verance-nocturne) :where(.verance-nocturne) .loader__line i { display: block; height: 100%; background: var(--gold-hi); transform-origin: left; transform: scaleX(0); }
html:has(.verance-nocturne) :where(.verance-nocturne) .loader__tag { font-size: 10px; letter-spacing: 0.34em; text-transform: uppercase; color: var(--muted); }
/* ---------- Cursor ---------- */
html:has(.verance-nocturne) :where(.verance-nocturne) .cursor { position: fixed; left: 0; top: 0; z-index: 90; pointer-events: none; }
html:has(.verance-nocturne) :where(.verance-nocturne) .cursor__dot,
html:has(.verance-nocturne) :where(.verance-nocturne) .cursor__ring { position: fixed; left: 0; top: 0; border-radius: 50%; transform: translate(-50%, -50%); }
html:has(.verance-nocturne) :where(.verance-nocturne) .cursor__dot { width: 6px; height: 6px; background: var(--gold-hi); }
html:has(.verance-nocturne) :where(.verance-nocturne) .cursor__ring {
width: 38px; height: 38px; border: 1px solid rgba(201, 164, 92, 0.6);
display: grid; place-items: center;
transition: width 0.5s var(--ease), height 0.5s var(--ease), background-color 0.5s var(--ease), border-color 0.5s;
}
html:has(.verance-nocturne) :where(.verance-nocturne) .cursor__ring span { font-size: 10px; letter-spacing: 0.25em; text-transform: uppercase; opacity: 0; color: var(--noir); transition: opacity 0.3s; }
html:has(.verance-nocturne) :where(.verance-nocturne) .cursor.is-hover .cursor__ring { width: 64px; height: 64px; background: rgba(201, 164, 92, 0.12); }
html:has(.verance-nocturne) :where(.verance-nocturne) .cursor.is-drag .cursor__ring { width: 84px; height: 84px; background: var(--gold); border-color: var(--gold); }
html:has(.verance-nocturne) :where(.verance-nocturne) .cursor.is-drag .cursor__ring span { opacity: 1; }
@media (hover: none), (pointer: coarse) {
html:has(.verance-nocturne) :where(.verance-nocturne) .cursor { display: none; } }
/* ---------- Progress + nav ---------- */
html:has(.verance-nocturne) :where(.verance-nocturne) .progress { position: fixed; left: 0; right: 0; top: 0; height: 2px; z-index: 60; }
html:has(.verance-nocturne) :where(.verance-nocturne) .progress i { display: block; height: 100%; background: linear-gradient(90deg, var(--gold-lo), var(--gold-hi)); transform-origin: left; transform: scaleX(0); }
html:has(.verance-nocturne) :where(.verance-nocturne) .nav {
position: fixed; top: 0; left: 0; right: 0; z-index: 40;
display: flex; align-items: center; justify-content: space-between;
padding: 18px var(--pad);
transition: transform 0.8s var(--ease);
--nav-glass: rgba(12, 10, 8, 0.58);
--nav-ink: #f5ecda;
}
/* soft shade behind the bar so it reads over the watch and the stars */
html:has(.verance-nocturne) :where(.verance-nocturne) .nav::before {
content: ""; position: absolute; inset: 0 0 -30px; z-index: -1; pointer-events: none;
background: linear-gradient(180deg, rgba(10, 9, 8, 0.78), rgba(10, 9, 8, 0.35) 60%, transparent);
transition: opacity 0.8s var(--ease);
}
html:has(.verance-nocturne) body.is-light :where(.verance-nocturne) .nav { --nav-glass: rgba(245, 239, 227, 0.72); --nav-ink: #1e1810; }
html:has(.verance-nocturne) body.is-light :where(.verance-nocturne) .nav::before { background: linear-gradient(180deg, rgba(245, 239, 227, 0.85), rgba(245, 239, 227, 0.35) 60%, transparent); }
html:has(.verance-nocturne) :where(.verance-nocturne) .nav.is-scrolled { padding-top: 10px; padding-bottom: 10px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .nav { transition: transform 0.8s var(--ease), padding 0.6s var(--ease); }
html:has(.verance-nocturne) :where(.verance-nocturne) .nav__logo {
font-family: var(--serif); font-size: 24px; font-weight: 600; letter-spacing: 0.4em; color: var(--gold-hi);
text-shadow: 0 1px 12px rgba(0, 0, 0, 0.5);
}
html:has(.verance-nocturne) body.is-light :where(.verance-nocturne) .nav__logo { color: var(--gold-lo); text-shadow: none; }
html:has(.verance-nocturne) :where(.verance-nocturne) .nav__links {
display: flex; gap: 6px; padding: 6px;
border-radius: 999px; border: 1px solid rgba(201, 164, 92, 0.35);
background: var(--nav-glass);
-webkit-backdrop-filter: blur(16px) saturate(1.2); backdrop-filter: blur(16px) saturate(1.2);
box-shadow: 0 10px 30px -12px rgba(0, 0, 0, 0.6);
}
html:has(.verance-nocturne) :where(.verance-nocturne) .nav__links a {
position: relative; padding: 10px 18px; border-radius: 999px;
font-size: 12.5px; font-weight: 500; letter-spacing: 0.2em; text-transform: uppercase; color: var(--nav-ink);
transition: color 0.4s var(--ease), background-color 0.4s var(--ease);
}
html:has(.verance-nocturne) :where(.verance-nocturne) .nav__links a:hover { color: var(--gold-hi); background: rgba(201, 164, 92, 0.12); }
html:has(.verance-nocturne) :where(.verance-nocturne) .nav__links a.is-current { color: #1a1208; background: linear-gradient(120deg, #c9a45c, #f1d9a0); }
html:has(.verance-nocturne) :where(.verance-nocturne) .nav > .btn--ghost {
height: 46px; padding: 0 26px; font-weight: 500; color: var(--nav-ink);
border-color: rgba(201, 164, 92, 0.55); background: var(--nav-glass);
-webkit-backdrop-filter: blur(16px); backdrop-filter: blur(16px);
}
/* ---------- Buttons ---------- */
html:has(.verance-nocturne) :where(.verance-nocturne) .btn {
position: relative; display: inline-flex; align-items: center; justify-content: center;
height: 52px; padding: 0 30px; border-radius: 999px; overflow: hidden;
font-size: 12px; letter-spacing: 0.24em; text-transform: uppercase; font-weight: 400;
white-space: nowrap; isolation: isolate;
}
html:has(.verance-nocturne) :where(.verance-nocturne) .btn span { position: relative; z-index: 1; transition: color 0.5s var(--ease); }
html:has(.verance-nocturne) :where(.verance-nocturne) .btn::before {
content: ""; position: absolute; inset: 0; z-index: 0; border-radius: inherit;
transform: translateY(101%); transition: transform 0.6s var(--ease);
}
html:has(.verance-nocturne) :where(.verance-nocturne) .btn:hover::before { transform: translateY(0); }
html:has(.verance-nocturne) :where(.verance-nocturne) .btn--ghost { border: 1px solid var(--hair); height: 44px; padding: 0 24px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .btn--ghost::before { background: var(--gold); }
html:has(.verance-nocturne) :where(.verance-nocturne) .btn--ghost:hover span { color: var(--noir); }
html:has(.verance-nocturne) :where(.verance-nocturne) .btn--gold { background: linear-gradient(120deg, #b98c3e, #f1d9a0 45%, #c9a45c 60%, #8a6a2e); background-size: 220% 100%; color: #1a1208; animation: sheen 6s linear infinite; }
html:has(.verance-nocturne) :where(.verance-nocturne) .btn--gold::before { background: var(--noir); }
html:has(.verance-nocturne) :where(.verance-nocturne) .btn--gold:hover span { color: var(--gold-hi); }
@keyframes sheen { to { background-position: -220% 0; } }
/* ---------- Type ---------- */
html:has(.verance-nocturne) :where(.verance-nocturne) .eyebrow {
display: inline-flex; align-items: center; gap: 12px;
font-size: 11px; letter-spacing: 0.3em; text-transform: uppercase; color: var(--muted); font-weight: 400;
}
html:has(.verance-nocturne) :where(.verance-nocturne) .eyebrow .num { color: var(--gold); font-family: var(--serif); font-size: 14px; letter-spacing: 0.1em; }
html:has(.verance-nocturne) :where(.verance-nocturne) .line { display: inline-block; width: 44px; height: 1px; background: var(--gold); }
html:has(.verance-nocturne) :where(.verance-nocturne) .display {
font-family: var(--serif); font-weight: 300; line-height: 1.02; letter-spacing: -0.01em;
font-size: clamp(44px, 6.2vw, 104px); margin: 22px 0 28px;
}
html:has(.verance-nocturne) :where(.verance-nocturne) .display--sm { font-size: clamp(38px, 4.6vw, 76px); }
html:has(.verance-nocturne) :where(.verance-nocturne) .body { max-width: 460px; color: var(--muted); font-size: 16px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .word { display: inline-block; overflow: hidden; vertical-align: top; padding-bottom: 0.08em; margin-bottom: -0.08em; }
html:has(.verance-nocturne) :where(.verance-nocturne) .word__in { display: inline-block; will-change: transform; }
/* ---------- Hero ---------- */
html:has(.verance-nocturne) :where(.verance-nocturne) .hero { position: relative; height: 100vh; min-height: 640px; overflow: hidden; }
html:has(.verance-nocturne) :where(.verance-nocturne) .hero__title {
position: absolute; inset: 0; z-index: 0;
display: grid; place-items: center; pointer-events: none;
}
html:has(.verance-nocturne) :where(.verance-nocturne) .hero__word {
font-family: var(--serif); font-weight: 300; font-style: italic;
font-size: clamp(90px, 23vw, 420px); line-height: 1; letter-spacing: -0.03em;
white-space: nowrap; transform: translateY(-2vh); perspective: 900px; color: #d9b36c;
}
html:has(.verance-nocturne) :where(.verance-nocturne) .hero__word .char {
display: inline-block; color: transparent; padding: 0 0.04em; margin: 0 -0.04em;
background: linear-gradient(100deg, #6b5226 0%, #f5dfa8 30%, #b48b43 50%, #fff1c9 62%, #7a5c29 100%);
background-size: 300% 100%;
-webkit-background-clip: text; background-clip: text;
animation: goldflow 7s ease-in-out infinite alternate;
transform-origin: 50% 100%;
}
html:has(.verance-nocturne) :where(.verance-nocturne) .hero__word.is-lit .char { animation: goldflow 7s ease-in-out infinite alternate, litpulse 1.8s ease-out; }
@keyframes litpulse { 0% { text-shadow: none; filter: brightness(1); } 35% { filter: brightness(1.6) drop-shadow(0 0 24px rgba(241, 217, 160, 0.55)); } 100% { filter: brightness(1); } }
html:has(.verance-nocturne) :where(.verance-nocturne) em.is-shining .word__in { animation: emshine 1.8s ease-out; }
@keyframes emshine { 0% { text-shadow: 0 0 0 rgba(241, 217, 160, 0); } 40% { text-shadow: 0 0 22px rgba(241, 217, 160, 0.75); } 100% { text-shadow: 0 0 0 rgba(241, 217, 160, 0); } }
@keyframes goldflow { from { background-position: 0% 0; } to { background-position: 100% 0; } }
html:has(.verance-nocturne) :where(.verance-nocturne) .hero__top { position: absolute; top: 110px; left: var(--pad); }
html:has(.verance-nocturne) :where(.verance-nocturne) .hero__bottom {
position: absolute; left: var(--pad); right: var(--pad); bottom: 44px;
display: grid; grid-template-columns: 1fr auto 1fr; align-items: end; gap: 24px;
}
html:has(.verance-nocturne) :where(.verance-nocturne) .hero__lead { font-family: var(--serif); font-weight: 300; font-size: clamp(30px, 3vw, 46px); line-height: 1.05; margin-bottom: 10px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .hero__intro p { max-width: 330px; color: var(--muted); font-size: 14px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .hero__time { justify-self: end; text-align: right; display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .hero__clock { font-family: var(--serif); font-size: 34px; letter-spacing: 0.06em; font-variant-numeric: tabular-nums; color: var(--gold-hi); line-height: 1.1; }
html:has(.verance-nocturne) :where(.verance-nocturne) .hero__time small { color: var(--muted); font-size: 12px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .scroll-cue { display: flex; flex-direction: column; align-items: center; gap: 14px; font-size: 10px; letter-spacing: 0.3em; text-transform: uppercase; color: var(--muted); }
html:has(.verance-nocturne) :where(.verance-nocturne) .scroll-cue__track { width: 1px; height: 64px; background: var(--hair); overflow: hidden; }
html:has(.verance-nocturne) :where(.verance-nocturne) .scroll-cue__track i { display: block; width: 100%; height: 40%; background: var(--gold-hi); animation: drip 2.2s var(--ease) infinite; }
@keyframes drip { from { transform: translateY(-100%); } to { transform: translateY(260%); } }
/* ---------- Maison ---------- */
html:has(.verance-nocturne) :where(.verance-nocturne) .maison { min-height: 130vh; display: flex; align-items: center; padding: 18vh var(--pad); }
html:has(.verance-nocturne) :where(.verance-nocturne) .maison__copy { width: min(700px, 50vw); }
html:has(.verance-nocturne) :where(.verance-nocturne) .display--md { font-size: clamp(40px, 4.9vw, 84px); max-width: 12ch; }
html:has(.verance-nocturne) :where(.verance-nocturne) .maison__row { display: grid; grid-template-columns: 1fr auto; gap: 40px; align-items: end; }
html:has(.verance-nocturne) :where(.verance-nocturne) .signature { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; padding-left: 26px; border-left: 1px solid var(--hair); }
html:has(.verance-nocturne) :where(.verance-nocturne) .signature__mark { font-family: var(--serif); font-style: italic; font-size: 38px; line-height: 1; color: var(--gold-hi); }
html:has(.verance-nocturne) :where(.verance-nocturne) .signature small { font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); line-height: 1.6; }
html:has(.verance-nocturne) :where(.verance-nocturne) .cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin-top: 56px; perspective: 1200px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .card {
position: relative; overflow: hidden; border-radius: 6px;
padding: 22px 22px 24px; min-height: 230px;
display: flex; flex-direction: column;
border: 1px solid var(--hair);
background: linear-gradient(160deg, rgba(201, 164, 92, 0.12), rgba(20, 17, 12, 0.4) 55%, rgba(10, 9, 8, 0.6));
backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
transform-style: preserve-3d; will-change: transform;
transition: border-color 0.6s var(--ease), box-shadow 0.6s var(--ease);
}
html:has(.verance-nocturne) :where(.verance-nocturne) .card:hover { border-color: rgba(241, 217, 160, 0.6); box-shadow: 0 24px 60px -20px rgba(201, 164, 92, 0.35); }
html:has(.verance-nocturne) :where(.verance-nocturne) .card__shine {
position: absolute; inset: 0; pointer-events: none; opacity: 0; transition: opacity 0.5s;
background: radial-gradient(260px circle at var(--mx, 50%) var(--my, 0%), rgba(241, 217, 160, 0.2), transparent 60%);
}
html:has(.verance-nocturne) :where(.verance-nocturne) .card:hover .card__shine { opacity: 1; }
html:has(.verance-nocturne) :where(.verance-nocturne) .card__top { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: auto; }
html:has(.verance-nocturne) :where(.verance-nocturne) .card__icon { width: 34px; height: 34px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .card__icon path { stroke: var(--gold); stroke-width: 1.2; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 1; stroke-dashoffset: 0; }
html:has(.verance-nocturne) :where(.verance-nocturne) .card__idx { font-family: var(--serif); font-style: italic; font-size: 15px; color: var(--muted); }
html:has(.verance-nocturne) :where(.verance-nocturne) .card__num { display: block; margin-top: 34px; font-family: var(--serif); font-weight: 400; font-size: clamp(40px, 3.4vw, 54px); line-height: 1; color: var(--gold-hi); font-variant-numeric: tabular-nums; }
html:has(.verance-nocturne) :where(.verance-nocturne) .card__num sup { font-size: 0.45em; margin-left: 2px; vertical-align: super; color: var(--gold); }
html:has(.verance-nocturne) :where(.verance-nocturne) .card__label { margin-top: 10px; font-size: 11px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--fg); }
html:has(.verance-nocturne) :where(.verance-nocturne) .card__desc { margin-top: 8px; font-size: 13px; line-height: 1.5; color: var(--muted); }
html:has(.verance-nocturne) :where(.verance-nocturne) .card__line { position: absolute; left: 0; right: 0; bottom: 0; height: 2px; background: linear-gradient(90deg, var(--gold-lo), var(--gold-hi), var(--gold-lo)); transform-origin: left; }
/* Card entrance extras */
html:has(.verance-nocturne) :where(.verance-nocturne) .card { clip-path: inset(0% 0% 0% 0% round 6px); }
html:has(.verance-nocturne) :where(.verance-nocturne) .card__border { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }
html:has(.verance-nocturne) :where(.verance-nocturne) .card__border rect { fill: none; stroke: var(--gold-hi); stroke-width: 1; stroke-dasharray: 1; }
html:has(.verance-nocturne) :where(.verance-nocturne) .card__sweep {
position: absolute; top: -10%; bottom: -10%; left: 0; width: 45%; pointer-events: none; opacity: 0;
background: linear-gradient(100deg, transparent, rgba(241, 217, 160, 0.22), transparent);
}
/* Clasp lock indicator */
html:has(.verance-nocturne) :where(.verance-nocturne) .lock-chip {
position: fixed; left: 0; top: 0; z-index: 3; pointer-events: none; opacity: 0;
display: flex; align-items: center; gap: 12px; padding: 10px 18px 10px 10px;
border: 1px solid var(--hair); border-radius: 999px;
background: rgba(10, 9, 8, 0.55); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
will-change: transform, opacity;
}
html:has(.verance-nocturne) :where(.verance-nocturne) .lock-chip__icon { position: relative; width: 36px; height: 36px; border-radius: 50%; display: grid; place-items: center; background: rgba(201, 164, 92, 0.14); }
html:has(.verance-nocturne) :where(.verance-nocturne) .lock-chip__icon::after { content: ""; position: absolute; inset: 0; border-radius: 50%; border: 1px solid var(--gold-hi); opacity: 0; }
html:has(.verance-nocturne) :where(.verance-nocturne) .lock-chip__icon svg { width: 18px; height: 18px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .lock-chip__icon svg * { stroke: var(--gold-hi); stroke-width: 1.5; stroke-linecap: round; }
html:has(.verance-nocturne) :where(.verance-nocturne) .lock-chip__shackle { transform: translateY(-3px) rotate(-18deg); transform-origin: 16px 11px; transition: transform 0.5s cubic-bezier(0.5, 2.2, 0.4, 1); }
html:has(.verance-nocturne) :where(.verance-nocturne) .lock-chip__text { display: flex; flex-direction: column; line-height: 1.25; }
html:has(.verance-nocturne) :where(.verance-nocturne) .lock-chip__text b { font-family: var(--serif); font-weight: 500; font-size: 17px; color: var(--fg); }
html:has(.verance-nocturne) :where(.verance-nocturne) .lock-chip__state { font-size: 10px; letter-spacing: 0.24em; text-transform: uppercase; color: var(--muted); transition: color 0.4s; }
html:has(.verance-nocturne) :where(.verance-nocturne) .lock-chip.is-locked .lock-chip__shackle { transform: none; }
html:has(.verance-nocturne) :where(.verance-nocturne) .lock-chip.is-locked .lock-chip__state { color: var(--gold-hi); }
html:has(.verance-nocturne) :where(.verance-nocturne) .lock-chip.is-locked .lock-chip__icon::after { animation: lockpulse 1.2s var(--ease) forwards; }
@keyframes lockpulse { 0% { opacity: 0.9; transform: scale(1); } 100% { opacity: 0; transform: scale(2.4); } }
/* ---------- Anatomy ---------- */
html:has(.verance-nocturne) :where(.verance-nocturne) .anatomy { position: relative; height: 100vh; }
html:has(.verance-nocturne) :where(.verance-nocturne) .anatomy__head { position: absolute; top: 120px; left: var(--pad); max-width: 340px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .anatomy__head .display--sm { font-size: clamp(34px, 3.3vw, 58px); }
html:has(.verance-nocturne) :where(.verance-nocturne) .anatomy__foot { position: absolute; bottom: 48px; left: var(--pad); display: flex; align-items: center; gap: 20px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .anatomy__meter { width: 180px; height: 1px; background: var(--hair); }
html:has(.verance-nocturne) :where(.verance-nocturne) .anatomy__meter i { display: block; height: 100%; background: var(--gold); transform-origin: left; transform: scaleX(0); }
html:has(.verance-nocturne) :where(.verance-nocturne) .callouts { position: fixed; inset: 0; z-index: 3; pointer-events: none; }
html:has(.verance-nocturne) :where(.verance-nocturne) .callout { position: absolute; left: 0; top: 0; opacity: 0; will-change: transform, opacity; --len: 90px; --ang: 0deg; --dx: 90px; --dy: 0px; --draw: 1; }
html:has(.verance-nocturne) :where(.verance-nocturne) .callout__line {
position: absolute; left: 0; top: 0; width: var(--len); height: 1px; transform-origin: 0 0;
transform: rotate(var(--ang)) scaleX(var(--draw));
background: linear-gradient(90deg, var(--gold-hi), rgba(201, 164, 92, 0.35));
}
html:has(.verance-nocturne) :where(.verance-nocturne) .callout::before { content: ""; position: absolute; left: -4px; top: -4px; width: 9px; height: 9px; border-radius: 50%; border: 1px solid var(--gold-hi); background: rgba(10, 9, 8, 0.6); box-shadow: 0 0 12px rgba(241, 217, 160, 0.6); }
html:has(.verance-nocturne) :where(.verance-nocturne) .callout__text {
position: absolute; left: calc(var(--dx) + 14px); top: calc(var(--dy) - 19px); white-space: nowrap;
display: grid; grid-template-columns: auto auto; column-gap: 10px;
}
html:has(.verance-nocturne) :where(.verance-nocturne) .callout__text em { grid-row: span 2; font-family: var(--serif); font-size: 13px; color: var(--gold); padding-top: 3px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .callout__text b { font-family: var(--serif); font-weight: 500; font-size: 19px; line-height: 1.1; color: var(--fg); }
html:has(.verance-nocturne) :where(.verance-nocturne) .callout__text small { font-size: 11px; letter-spacing: 0.08em; color: var(--muted); }
html:has(.verance-nocturne) :where(.verance-nocturne) .callout.is-left .callout__text { left: calc(var(--dx) - 14px); transform: translateX(-100%); text-align: right; }
html:has(.verance-nocturne) :where(.verance-nocturne) .callout.is-left .callout__text em { grid-column: 2; grid-row: 1 / span 2; }
html:has(.verance-nocturne) :where(.verance-nocturne) .callout.is-left .callout__text b,
html:has(.verance-nocturne) :where(.verance-nocturne) .callout.is-left .callout__text small { grid-column: 1; }
/* ---------- Calibre ---------- */
html:has(.verance-nocturne) :where(.verance-nocturne) .calibre { min-height: 120vh; display: flex; align-items: center; justify-content: flex-end; padding: 20vh var(--pad); }
html:has(.verance-nocturne) :where(.verance-nocturne) .calibre__copy { width: min(620px, 46vw); }
html:has(.verance-nocturne) :where(.verance-nocturne) .specs { display: grid; grid-template-columns: repeat(3, 1fr); margin-top: 48px; border-top: 1px solid var(--hair); }
html:has(.verance-nocturne) :where(.verance-nocturne) .spec { padding: 26px 18px 26px 0; border-bottom: 1px solid var(--hair); }
html:has(.verance-nocturne) :where(.verance-nocturne) .spec b { display: block; font-family: var(--serif); font-weight: 400; font-size: clamp(32px, 3vw, 46px); line-height: 1; color: var(--gold-hi); font-variant-numeric: tabular-nums; }
html:has(.verance-nocturne) :where(.verance-nocturne) .spec > span { font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--muted); }
/* ---------- Collection ---------- */
html:has(.verance-nocturne) :where(.verance-nocturne) .collection { position: relative; min-height: 110vh; display: flex; align-items: center; justify-content: flex-end; padding: 16vh var(--pad); }
html:has(.verance-nocturne) :where(.verance-nocturne) .collection__stage { position: absolute; left: 0; top: 0; bottom: 0; width: 55%; z-index: 2; touch-action: pan-y; }
html:has(.verance-nocturne) :where(.verance-nocturne) .collection__panel { width: min(520px, 42vw); }
html:has(.verance-nocturne) :where(.verance-nocturne) .opt { margin-top: 30px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .opt__label { display: flex; justify-content: space-between; align-items: baseline; padding-bottom: 14px; border-bottom: 1px solid var(--hair); margin-bottom: 18px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .opt__label span { font-size: 11px; letter-spacing: 0.3em; text-transform: uppercase; color: var(--muted); }
html:has(.verance-nocturne) :where(.verance-nocturne) .opt__label b { font-family: var(--serif); font-weight: 500; font-size: 22px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .opt__row { display: flex; gap: 16px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .swatch {
width: 50px; height: 50px; border-radius: 50%; position: relative;
background: radial-gradient(circle at 32% 28%, var(--c1), var(--c2) 72%);
box-shadow: inset 0 -4px 10px rgba(0, 0, 0, 0.35), 0 6px 18px rgba(0, 0, 0, 0.18);
transition: transform 0.6s var(--ease);
}
html:has(.verance-nocturne) :where(.verance-nocturne) .swatch::after { content: ""; position: absolute; inset: -7px; border-radius: 50%; border: 1px solid var(--gold); opacity: 0; transform: scale(0.8); transition: 0.6s var(--ease); }
html:has(.verance-nocturne) :where(.verance-nocturne) .swatch:hover { transform: translateY(-3px); }
html:has(.verance-nocturne) :where(.verance-nocturne) .swatch.is-active::after { opacity: 1; transform: scale(1); }
html:has(.verance-nocturne) :where(.verance-nocturne) .price { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; margin-top: 44px; padding-top: 26px; border-top: 1px solid var(--hair); flex-wrap: wrap; }
html:has(.verance-nocturne) :where(.verance-nocturne) .price__val { display: block; font-family: var(--serif); font-weight: 400; font-size: 40px; line-height: 1.1; }
html:has(.verance-nocturne) :where(.verance-nocturne) .hint { margin-top: 26px; font-size: 12px; letter-spacing: 0.12em; color: var(--muted); }
/* ---------- Marquee ---------- */
html:has(.verance-nocturne) :where(.verance-nocturne) .marquee { position: relative; z-index: 2; padding: 10vh 0; overflow: hidden; border-top: 1px solid var(--hair); border-bottom: 1px solid var(--hair); }
html:has(.verance-nocturne) :where(.verance-nocturne) .marquee__track { display: flex; align-items: center; gap: 4vw; width: max-content; will-change: transform; }
html:has(.verance-nocturne) :where(.verance-nocturne) .marquee span { font-family: var(--serif); font-weight: 300; font-style: italic; font-size: clamp(70px, 11vw, 190px); line-height: 1; white-space: nowrap; }
html:has(.verance-nocturne) :where(.verance-nocturne) .marquee .outline { color: transparent; -webkit-text-stroke: 1px var(--gold); font-style: normal; }
html:has(.verance-nocturne) :where(.verance-nocturne) .marquee i { font-style: normal; color: var(--gold); font-size: clamp(24px, 3vw, 46px); }
/* ---------- Quote ---------- */
html:has(.verance-nocturne) :where(.verance-nocturne) .quote { min-height: 110vh; display: grid; place-items: center; padding: 20vh var(--pad); text-align: center; }
html:has(.verance-nocturne) :where(.verance-nocturne) .quote blockquote { max-width: 1100px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .quote__text { font-family: var(--serif); font-weight: 300; font-size: clamp(34px, 4.6vw, 78px); line-height: 1.12; }
html:has(.verance-nocturne) :where(.verance-nocturne) .quote__text .w { opacity: 0.12; transition: none; }
html:has(.verance-nocturne) :where(.verance-nocturne) .quote footer { margin-top: 44px; display: inline-flex; align-items: center; gap: 16px; font-size: 12px; letter-spacing: 0.26em; text-transform: uppercase; color: var(--muted); }
/* ---------- Reserve ---------- */
html:has(.verance-nocturne) :where(.verance-nocturne) .reserve { min-height: 110vh; display: flex; align-items: center; padding: 20vh var(--pad); }
html:has(.verance-nocturne) :where(.verance-nocturne) .reserve__copy { width: min(660px, 50vw); }
html:has(.verance-nocturne) :where(.verance-nocturne) .reserve__form { display: flex; gap: 14px; align-items: flex-end; margin-top: 44px; max-width: 580px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .field { position: relative; flex: 1; display: block; }
html:has(.verance-nocturne) :where(.verance-nocturne) .field input {
width: 100%; height: 52px; background: transparent; border: 0; border-bottom: 1px solid var(--hair);
color: var(--fg); font: inherit; font-size: 17px; outline: none; padding-top: 12px;
transition: border-color 0.4s;
}
html:has(.verance-nocturne) :where(.verance-nocturne) .field span { position: absolute; left: 0; top: 22px; font-size: 13px; letter-spacing: 0.12em; color: var(--muted); pointer-events: none; transition: 0.5s var(--ease); }
html:has(.verance-nocturne) :where(.verance-nocturne) .field input:focus { border-color: var(--gold); }
html:has(.verance-nocturne) :where(.verance-nocturne) .field input:focus + span,
html:has(.verance-nocturne) :where(.verance-nocturne) .field input:not(:placeholder-shown) + span { top: -4px; font-size: 10px; letter-spacing: 0.26em; text-transform: uppercase; color: var(--gold); }
html:has(.verance-nocturne) :where(.verance-nocturne) .reserve__msg { margin-top: 18px; min-height: 24px; font-size: 14px; color: var(--gold-hi); }
/* ---------- Footer ---------- */
/* ---------- Collectors (testimonials) ---------- */
html:has(.verance-nocturne) :where(.verance-nocturne) .voices { position: relative; min-height: 110vh; display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 4vw; align-items: center; padding: 14vh var(--pad); }
html:has(.verance-nocturne) :where(.verance-nocturne) .voices__stage { position: relative; height: 76vh; display: grid; place-items: center; }
html:has(.verance-nocturne) :where(.verance-nocturne) .voices__ring {
position: absolute; width: min(36vw, 66vh); aspect-ratio: 1; border-radius: 50%;
border: 1px solid var(--hair); background: radial-gradient(circle, rgba(201, 164, 92, 0.1), transparent 65%);
}
html:has(.verance-nocturne) :where(.verance-nocturne) .voices__ring::before { content: ""; position: absolute; inset: 7%; border-radius: 50%; border: 1px dashed rgba(201, 164, 92, 0.18); animation: sweep 60s linear infinite; }
html:has(.verance-nocturne) :where(.verance-nocturne) .voices__caption { position: absolute; bottom: 0; font-size: 10px; letter-spacing: 0.28em; text-transform: uppercase; color: var(--muted); white-space: nowrap; }
html:has(.verance-nocturne) :where(.verance-nocturne) .voices__caption span { color: var(--gold-hi); }
html:has(.verance-nocturne) :where(.verance-nocturne) .voices__panel { max-width: 620px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .voices__slides { display: grid; margin-top: 12px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .voice { grid-area: 1 / 1; opacity: 0; visibility: hidden; }
html:has(.verance-nocturne) :where(.verance-nocturne) .voice.is-active { opacity: 1; visibility: visible; }
html:has(.verance-nocturne) :where(.verance-nocturne) .voice__quote { font-family: var(--serif); font-style: italic; font-weight: 300; font-size: clamp(26px, 2.3vw, 38px); line-height: 1.28; color: var(--fg); }
html:has(.verance-nocturne) :where(.verance-nocturne) .voice__quote .word { padding-bottom: 0.12em; margin-bottom: -0.12em; }
html:has(.verance-nocturne) :where(.verance-nocturne) .voice__by { display: flex; align-items: center; gap: 16px; margin-top: 34px; padding-top: 24px; border-top: 1px solid var(--hair); }
html:has(.verance-nocturne) :where(.verance-nocturne) .voice__mono {
flex: none; width: 52px; height: 52px; border-radius: 50%; display: grid; place-items: center;
font-family: var(--serif); font-size: 18px; letter-spacing: 0.06em; color: var(--noir);
background: radial-gradient(circle at 32% 28%, #f6dc9a, #b8862f 75%);
}
html:has(.verance-nocturne) :where(.verance-nocturne) .voice__who { display: flex; flex-direction: column; line-height: 1.3; }
html:has(.verance-nocturne) :where(.verance-nocturne) .voice__who b { font-family: var(--serif); font-weight: 500; font-size: 20px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .voice__who small { font-size: 12px; letter-spacing: 0.08em; color: var(--muted); }
html:has(.verance-nocturne) :where(.verance-nocturne) .voice__piece { margin-left: auto; font-size: 10px; letter-spacing: 0.26em; text-transform: uppercase; color: var(--gold); border: 1px solid var(--hair); border-radius: 999px; padding: 8px 14px; white-space: nowrap; }
html:has(.verance-nocturne) :where(.verance-nocturne) .voices__nav { display: flex; align-items: center; gap: 12px; margin-top: 40px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .voices__btn { width: 52px; height: 52px; border-radius: 50%; border: 1px solid var(--hair); display: grid; place-items: center; transition: background-color 0.5s var(--ease), border-color 0.5s; }
html:has(.verance-nocturne) :where(.verance-nocturne) .voices__btn svg { width: 20px; height: 20px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .voices__btn path { stroke: var(--gold-hi); stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; transition: stroke 0.4s; }
html:has(.verance-nocturne) :where(.verance-nocturne) .voices__btn:hover { background: var(--gold); border-color: var(--gold); }
html:has(.verance-nocturne) :where(.verance-nocturne) .voices__btn:hover path { stroke: var(--noir); }
html:has(.verance-nocturne) :where(.verance-nocturne) .voices__count { margin-left: 14px; font-family: var(--serif); font-size: 18px; color: var(--muted); font-variant-numeric: tabular-nums; }
html:has(.verance-nocturne) :where(.verance-nocturne) .voices__count b { font-weight: 400; color: var(--gold-hi); }
html:has(.verance-nocturne) :where(.verance-nocturne) .voices__bar { flex: 1; height: 1px; background: var(--hair); max-width: 200px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .voices__bar i { display: block; height: 100%; background: var(--gold); transform-origin: left; transform: scaleX(0); }
/* ---------- Footer ---------- */
html:has(.verance-nocturne) :where(.verance-nocturne) .footer { position: relative; padding: 8vh var(--pad) 0; overflow: hidden; }
html:has(.verance-nocturne) :where(.verance-nocturne) .footer__grid {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 32px;
padding: 48px 0; border-top: 1px solid var(--hair); border-bottom: 1px solid var(--hair);
}
html:has(.verance-nocturne) :where(.verance-nocturne) .footer__col p { margin: 14px 0 12px; color: var(--muted); font-size: 14px; line-height: 1.7; }
html:has(.verance-nocturne) :where(.verance-nocturne) .footer__col nav { display: flex; flex-direction: column; gap: 8px; margin-top: 14px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .footer__link { position: relative; display: inline-block; width: fit-content; font-family: var(--serif); font-size: 19px; color: var(--fg); transition: color 0.4s; }
html:has(.verance-nocturne) :where(.verance-nocturne) .footer__link::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 1px; background: var(--gold); transform: scaleX(0); transform-origin: right; transition: transform 0.6s var(--ease); }
html:has(.verance-nocturne) :where(.verance-nocturne) .footer__link:hover { color: var(--gold-hi); }
html:has(.verance-nocturne) :where(.verance-nocturne) .footer__link:hover::after { transform: scaleX(1); transform-origin: left; }
html:has(.verance-nocturne) :where(.verance-nocturne) .footer__legal {
display: flex; justify-content: space-between; align-items: center; gap: 20px; padding: 26px 0;
font-size: 11px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--muted);
}
html:has(.verance-nocturne) :where(.verance-nocturne) .footer__badge { color: var(--gold); }
html:has(.verance-nocturne) :where(.verance-nocturne) .footer__top-link { display: inline-flex; align-items: center; gap: 10px; color: var(--fg); }
html:has(.verance-nocturne) :where(.verance-nocturne) .footer__top-link svg { width: 16px; height: 16px; transition: transform 0.5s var(--ease); }
html:has(.verance-nocturne) :where(.verance-nocturne) .footer__top-link path { stroke: var(--gold-hi); stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
html:has(.verance-nocturne) :where(.verance-nocturne) .footer__top-link:hover svg { transform: translateY(-4px); }
html:has(.verance-nocturne) :where(.verance-nocturne) .footer__brand {
position: relative; z-index: 2; display: flex; justify-content: center;
height: 12.6vw; overflow: hidden; margin: 0 calc(var(--pad) * -1);
}
html:has(.verance-nocturne) :where(.verance-nocturne) .footer__brand > span {
font-family: var(--serif); font-weight: 300; line-height: 0.82; letter-spacing: 0.02em; white-space: nowrap;
font-size: 18.5vw; display: flex;
}
html:has(.verance-nocturne) :where(.verance-nocturne) .footer__brand .char {
display: inline-block; color: transparent;
background: linear-gradient(180deg, var(--gold-hi) 0%, rgba(201, 164, 92, 0.55) 45%, rgba(201, 164, 92, 0.04) 80%);
-webkit-background-clip: text; background-clip: text;
}
/* ---------- Mobile menu ---------- */
html:has(.verance-nocturne) :where(.verance-nocturne) .nav__menu { display: none; width: 44px; height: 44px; border-radius: 50%; border: 1px solid var(--hair); position: relative; z-index: 2; }
html:has(.verance-nocturne) :where(.verance-nocturne) .nav__menu i { position: absolute; left: 13px; right: 13px; height: 1px; background: var(--gold-hi); transition: transform 0.6s var(--ease), top 0.6s var(--ease); }
html:has(.verance-nocturne) :where(.verance-nocturne) .nav__menu i:nth-child(1) { top: 18px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .nav__menu i:nth-child(2) { top: 25px; }
html:has(.verance-nocturne) body.menu-open :where(.verance-nocturne) .nav__menu i:nth-child(1) { top: 21.5px; transform: rotate(45deg); }
html:has(.verance-nocturne) body.menu-open :where(.verance-nocturne) .nav__menu i:nth-child(2) { top: 21.5px; transform: rotate(-45deg); }
html:has(.verance-nocturne) :where(.verance-nocturne) .menu {
position: fixed; inset: 0; z-index: 35; display: flex; flex-direction: column; justify-content: center; gap: 6px;
padding: 100px var(--pad) 48px; background: rgba(10, 9, 8, 0.96);
-webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
clip-path: circle(0% at calc(100% - 44px) 44px); pointer-events: none; transition: clip-path 0.9s var(--ease);
}
html:has(.verance-nocturne) body.menu-open :where(.verance-nocturne) .menu { clip-path: circle(150% at calc(100% - 44px) 44px); pointer-events: auto; }
html:has(.verance-nocturne) :where(.verance-nocturne) .menu a.menu__link { display: flex; align-items: baseline; gap: 16px; font-family: var(--serif); font-weight: 300; font-size: clamp(40px, 11vw, 72px); line-height: 1.15; color: #efe7d8; }
html:has(.verance-nocturne) :where(.verance-nocturne) .menu a.menu__link:active { color: var(--gold-hi); }
html:has(.verance-nocturne) :where(.verance-nocturne) .menu a.menu__link em { font-size: 14px; font-style: normal; color: var(--gold); font-family: var(--sans); letter-spacing: 0.2em; }
html:has(.verance-nocturne) :where(.verance-nocturne) .menu__foot { margin-top: 36px; font-size: 11px; letter-spacing: 0.24em; text-transform: uppercase; color: rgba(239, 231, 216, 0.58); }
html:has(.verance-nocturne) body.menu-open :where(.verance-nocturne) .nav__logo { color: var(--gold-hi); }
/* ---------- Responsive ----------
"Narrow" = phones and portrait tablets (also tall screens such as iPad Pro portrait).
The JS uses the same rule, so the watch switches to its top "stage band" in sync:
section copy starts below the band and scrolls up over it on a solid panel. */
@media (max-width: 900px), (max-aspect-ratio: 9/10) {
html:has(.verance-nocturne) :where(.verance-nocturne) .nav { padding: 16px var(--pad); }
html:has(.verance-nocturne) :where(.verance-nocturne) .nav__links { display: none; }
html:has(.verance-nocturne) :where(.verance-nocturne) .nav > .btn--ghost { display: none; }
html:has(.verance-nocturne) :where(.verance-nocturne) .nav__menu { display: block; }
html:has(.verance-nocturne) :where(.verance-nocturne) .nav__logo { font-size: 18px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .hero__bottom { grid-template-columns: 1fr auto; bottom: 28px; align-items: end; }
html:has(.verance-nocturne) :where(.verance-nocturne) .scroll-cue { display: none; }
html:has(.verance-nocturne) :where(.verance-nocturne) .hero__time { gap: 2px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .hero__clock { font-size: 26px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .hero__top { top: 86px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .hero__eyebrow { flex-wrap: wrap; max-width: calc(100vw - 2 * var(--pad)); row-gap: 6px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .maison,
html:has(.verance-nocturne) :where(.verance-nocturne) .calibre,
html:has(.verance-nocturne) :where(.verance-nocturne) .reserve { display: block; min-height: 0; padding: 50vh var(--pad) 10vh; }
html:has(.verance-nocturne) :where(.verance-nocturne) .maison__copy,
html:has(.verance-nocturne) :where(.verance-nocturne) .calibre__copy,
html:has(.verance-nocturne) :where(.verance-nocturne) .reserve__copy,
html:has(.verance-nocturne) :where(.verance-nocturne) .collection__panel,
html:has(.verance-nocturne) :where(.verance-nocturne) .voices__panel {
width: 100%; max-width: 720px; margin: 0 auto;
background: linear-gradient(180deg, transparent 0, var(--bg) 90px);
padding-top: 90px; margin-top: -90px;
}
html:has(.verance-nocturne) :where(.verance-nocturne) .display { font-size: clamp(40px, 8.4vw, 72px); }
html:has(.verance-nocturne) :where(.verance-nocturne) .display--md,
html:has(.verance-nocturne) :where(.verance-nocturne) .display--sm { font-size: clamp(36px, 7.4vw, 64px); max-width: none; }
html:has(.verance-nocturne) :where(.verance-nocturne) .body { max-width: none; }
html:has(.verance-nocturne) :where(.verance-nocturne) .maison__row { grid-template-columns: 1fr auto; gap: 28px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .cards { grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 40px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .card { min-height: 0; padding: 18px 16px 20px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .card__num { margin-top: 20px; font-size: clamp(34px, 6vw, 46px); }
html:has(.verance-nocturne) :where(.verance-nocturne) .card__desc { font-size: 12px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .anatomy__head { top: 84px; max-width: none; right: var(--pad); }
html:has(.verance-nocturne) :where(.verance-nocturne) .anatomy__head .display--sm { font-size: clamp(30px, 6vw, 48px); }
html:has(.verance-nocturne) :where(.verance-nocturne) .callout__text { white-space: normal; width: 118px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .callout__text small { display: none; }
html:has(.verance-nocturne) :where(.verance-nocturne) .callout__text b { font-size: 14px; line-height: 1.15; }
html:has(.verance-nocturne) :where(.verance-nocturne) .callout__text em { font-size: 11px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .callout::before { width: 7px; height: 7px; left: -3px; top: -3px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .specs { grid-template-columns: repeat(3, 1fr); }
html:has(.verance-nocturne) :where(.verance-nocturne) .spec { padding: 20px 12px 20px 0; }
html:has(.verance-nocturne) :where(.verance-nocturne) .collection { display: block; min-height: 0; padding: 0 var(--pad) 10vh; }
/* the watch stays visible above the swatches while you configure it */
html:has(.verance-nocturne) :where(.verance-nocturne) .collection__stage { position: sticky; top: 0; width: auto; height: 46vh; margin: 0 calc(var(--pad) * -1); }
html:has(.verance-nocturne) :where(.verance-nocturne) .collection__panel { position: relative; z-index: 3; background: none; margin-top: 0; padding-top: 8px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .collection__panel .display--sm { font-size: clamp(30px, 6.4vw, 56px); margin: 12px 0 8px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .opt { margin-top: 18px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .opt__label { padding-bottom: 10px; margin-bottom: 12px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .price { margin-top: 24px; padding-top: 18px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .hint { display: none; }
html:has(.verance-nocturne) :where(.verance-nocturne) .opt__row { flex-wrap: wrap; }
html:has(.verance-nocturne) :where(.verance-nocturne) .quote { min-height: 80vh; padding: 16vh var(--pad); }
html:has(.verance-nocturne) :where(.verance-nocturne) .reserve__form { flex-direction: column; align-items: stretch; }
html:has(.verance-nocturne) :where(.verance-nocturne) .reserve__form .btn { width: 100%; }
html:has(.verance-nocturne) :where(.verance-nocturne) .voices { grid-template-columns: 1fr; gap: 0; padding: 8vh var(--pad) 10vh; min-height: 0; }
html:has(.verance-nocturne) :where(.verance-nocturne) .voices__stage { height: 56vh; }
html:has(.verance-nocturne) :where(.verance-nocturne) .voices__ring { width: min(72vw, 48vh); }
html:has(.verance-nocturne) :where(.verance-nocturne) .voices__panel { padding-top: 40px; margin-top: 0; background: none; }
html:has(.verance-nocturne) :where(.verance-nocturne) .voice__by { flex-wrap: wrap; }
html:has(.verance-nocturne) :where(.verance-nocturne) .footer__grid { grid-template-columns: 1fr 1fr; gap: 32px 20px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .footer__legal { flex-wrap: wrap; }
html:has(.verance-nocturne) :where(.verance-nocturne) .lock-chip { display: none; }
}
/* Phones */
@media (max-width: 600px) {
html:has(.verance-nocturne) { --pad: 20px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .hero__bottom { grid-template-columns: 1fr; }
html:has(.verance-nocturne) :where(.verance-nocturne) .hero__time { justify-self: start; align-items: flex-start; text-align: left; }
html:has(.verance-nocturne) :where(.verance-nocturne) .hero__intro p { max-width: 280px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .maison__row { grid-template-columns: 1fr; gap: 20px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .signature { padding-left: 16px; }
/* cards stack vertically on phones */
html:has(.verance-nocturne) :where(.verance-nocturne) .cards { grid-template-columns: 1fr; gap: 12px; margin-top: 36px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .card { min-height: 0; }
html:has(.verance-nocturne) :where(.verance-nocturne) .card__num { margin-top: 16px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .specs { grid-template-columns: repeat(2, 1fr); }
html:has(.verance-nocturne) :where(.verance-nocturne) .stats { gap: 28px; flex-wrap: wrap; }
html:has(.verance-nocturne) :where(.verance-nocturne) .callout__text { width: 92px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .callout__text b { font-size: 12.5px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .voice__piece { margin-left: 0; }
html:has(.verance-nocturne) :where(.verance-nocturne) .voices__nav { flex-wrap: wrap; }
html:has(.verance-nocturne) :where(.verance-nocturne) .voices__bar { flex-basis: 100%; max-width: none; order: 5; }
html:has(.verance-nocturne) :where(.verance-nocturne) .price { flex-direction: column; align-items: stretch; }
html:has(.verance-nocturne) :where(.verance-nocturne) .price .btn { width: 100%; }
html:has(.verance-nocturne) :where(.verance-nocturne) .footer__legal { flex-direction: column; align-items: flex-start; gap: 10px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .marquee { padding: 7vh 0; }
}
/* Tablet landscape / small laptops: desktop layout, tightened */
@media (min-width: 901px) and (max-width: 1200px) and (min-aspect-ratio: 9/10) {
html:has(.verance-nocturne) :where(.verance-nocturne) .nav__links { gap: 26px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .maison__copy { width: min(640px, 52vw); }
html:has(.verance-nocturne) :where(.verance-nocturne) .cards { gap: 10px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .card { padding: 18px 16px 20px; min-height: 200px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .card__desc { font-size: 12px; }
html:has(.verance-nocturne) :where(.verance-nocturne) .calibre__copy { width: min(560px, 50vw); }
html:has(.verance-nocturne) :where(.verance-nocturne) .collection__panel { width: min(480px, 44vw); }
html:has(.verance-nocturne) :where(.verance-nocturne) .reserve__copy { width: min(560px, 52vw); }
}
@media (prefers-reduced-motion: reduce) {
html:has(.verance-nocturne) :where(.verance-nocturne) .grain,
html:has(.verance-nocturne) :where(.verance-nocturne) .hero__word .char,
html:has(.verance-nocturne) :where(.verance-nocturne) .btn--gold,
html:has(.verance-nocturne) :where(.verance-nocturne) .scroll-cue__track i { animation: none; }
}
/* ---------- WordPress additions (not in the source) ---------- */
/* The source's universal reset zeroed <body>'s margin; the scoped reset no longer reaches it. */
html:has(.verance-nocturne) body { margin: 0; }
/* The Elementor editor: no custom cursor, so every element stays selectable. The loader plays
there as on the site (when the editor opens and after each edit of its widget), then lifts. */
html:has(.verance-nocturne) body.elementor-editor-active :where(.verance-nocturne) .cursor,
html.verance-edit :where(.verance-nocturne) .cursor { display: none !important; }
```
## B4 · `verance-boot.js` (code file, served as `?v=5`)
```js
/* Verance Nocturne boot. Loads the page engine only on a page that contains its sections, so the
tag can sit in site-wide code without costing any other page more than this file. */
(function () {
if (window.VeranceEngine || !document.querySelector('.verance-nocturne')) return;
if (document.querySelector('script[src*="/uichemy-composer/verance-app.js"]')) return;
var s = document.createElement('script');
s.src = '/wp-content/uichemy-composer/verance-app.js?v=5';
s.async = false;
document.body.appendChild(s);
})();
```
## B5 · `verance-app.js` — the page engine (code file, served as `?v=5`)
```js
/* Verance Nocturne — page engine.
*
* The source's loader.js, watch.js, morph.js and main.js, ported from ES modules into one
* classic script so it can run on a WordPress page and inside the Elementor editor.
*
* Why this is a code file and not widget JS: the page is one experience. A single fixed WebGL
* watch sits behind every section, and its pose is computed each frame from every section's
* ScrollTrigger progress. No single section owns that. Each widget carries a small stub with
* its Animation-panel values; this file is the engine they all share.
*
* Built to be torn down and rebuilt. Elementor re-renders a widget on every edit and the
* Composer re-runs its JS, so the engine tracks everything it creates: listeners, intervals,
* ticker callbacks, tweens, ScrollTriggers, the text it splits and the WebGL context. On a
* rebuild it removes all of that and starts clean.
*
* In the editor it skips the loader, the custom cursor and Lenis, and never jumps the scroll.
* Everything else runs exactly as on the live page.
*/
(function () {
'use strict';
if (window.VeranceEngine) return;
var BASE = '/wp-content/uichemy-composer/';
var LIB = {
gsap: BASE + 'uichemy-gsap.js', // GSAP 3.12.5, the version the source pins
st: BASE + 'uichemy-scrolltrigger.js', // ScrollTrigger 3.12.5
lenis: BASE + 'verance-lenis.js?v=1', // Lenis 1.1.13
three: BASE + 'verance-three.js?v=1', // three.js r169 + RoomEnvironment, RoundedBoxGeometry, MeshSurfaceSampler
css: BASE + 'verance-app.css?v=2' // every style of the page, scoped to .verance-nocturne
};
// Defaults are the source's own values; each widget's stub can override them from the panel.
var DEFAULTS = {
lenis_lerp: 0.085, tone_exposure: 1.05, env_intensity: 0.95,
intro_duration: 2.8, hero_fan: 16, hero_depth: 60,
pin_length: 220, marquee_duration: 28, autoplay_seconds: 7
};
// Markup the engine drives. If a section is missing (deleted in the editor), the engine stays
// off rather than throwing halfway through, and the page stays readable.
var REQUIRED = ['#webgl', '.glow', '.lock-chip', '.callout', '.cursor', '.progress i', '.nav', '.nav__menu',
'#hero', '[data-split="chars"]', '.hero__lead', '#clock', '#maison', '.maison__copy', '.cards',
'#anatomie', '.anatomy__head', '.anatomy__meter i', '#calibre', '.calibre__copy', '#collection',
'.collection__stage', '#caseName', '#dialName', '#price', '.marquee__track', '#quote', '[data-split="scrub"]',
'#reserve', '.reserve__copy', '.reserve__form', '#voices', '.voices__stage', '.voices__panel', '#voiceConfig',
'#voiceIdx', '.voices__bar i', '[data-split="brand"]'];
var isEditor = /[?&]elementor-preview=/.test(location.search)
|| document.body.classList.contains('elementor-editor-active')
|| !!(window.elementorFrontend && elementorFrontend.isEditMode && elementorFrontend.isEditMode());
var state = { gen: 0, current: null, booted: false, timer: 0, dirty: false };
/* ---------------- watch.js (verbatim) ---------------- */
function createWatchModule(THREE, RoundedBoxGeometry) {
// Procedural 3D watch — every part is generated in code (no external model).
// Inner coordinate system: dial faces +Y. The outer group rotates it so the dial faces +Z (the camera).
const TAU = Math.PI * 2;
const CASES = {
yellow: { color: 0xe6b964, roughness: 0.18 },
rose: { color: 0xe7a688, roughness: 0.2 },
noir: { color: 0x2b2a2e, roughness: 0.32 },
};
const DIALS = {
noir: { light: '#2e2a25', dark: '#040404', text: '#e2bd72', sheen: 0.14 },
ivoire: { light: '#fffaf0', dark: '#d8cbae', text: '#3b2c16', sheen: 0.1 },
emeraude: { light: '#1f7a5c', dark: '#02120b', text: '#ecc97e', sheen: 0.16 },
nuit: { light: '#2d559a', dark: '#030916', text: '#ecc97e', sheen: 0.16 },
};
/* ---------- Canvas textures ---------- */
function drawDial(canvas, d) {
const ctx = canvas.getContext('2d');
const S = canvas.width, c = S / 2;
ctx.clearRect(0, 0, S, S);
ctx.save();
ctx.beginPath(); ctx.arc(c, c, c, 0, TAU); ctx.clip();
const g = ctx.createRadialGradient(c, c * 0.9, 0, c, c, c);
g.addColorStop(0, d.light); g.addColorStop(1, d.dark);
ctx.fillStyle = g; ctx.fillRect(0, 0, S, S);
// sunray reflection
if (ctx.createConicGradient) {
const cg = ctx.createConicGradient(-Math.PI / 4, c, c);
for (let i = 0; i <= 8; i++) cg.addColorStop(i / 8, i % 2 ? `rgba(255,255,255,${d.sheen})` : 'rgba(0,0,0,0.18)');
ctx.globalCompositeOperation = 'soft-light';
ctx.fillStyle = cg; ctx.fillRect(0, 0, S, S);
ctx.globalCompositeOperation = 'source-over';
}
// fine brushed rays
ctx.translate(c, c);
for (let i = 0; i < 720; i++) {
ctx.rotate(TAU / 720);
ctx.strokeStyle = i % 2 ? 'rgba(255,255,255,0.035)' : 'rgba(0,0,0,0.05)';
ctx.lineWidth = 1.2;
ctx.beginPath(); ctx.moveTo(0, c * 0.06); ctx.lineTo(0, c); ctx.stroke();
}
ctx.setTransform(1, 0, 0, 1, 0, 0);
// guilloché centre rings
ctx.strokeStyle = 'rgba(255,255,255,0.05)';
for (let r = 0.04; r < 0.2; r += 0.012) { ctx.beginPath(); ctx.arc(c, c, c * r, 0, TAU); ctx.stroke(); }
// chapter ring + minute track
ctx.strokeStyle = d.text; ctx.globalAlpha = 0.75; ctx.lineWidth = S * 0.0016;
ctx.beginPath(); ctx.arc(c, c, c * 0.955, 0, TAU); ctx.stroke();
ctx.beginPath(); ctx.arc(c, c, c * 0.905, 0, TAU); ctx.stroke();
for (let i = 0; i < 300; i++) {
const a = (i / 300) * TAU;
const major = i % 25 === 0, mid = i % 5 === 0;
if (!mid && !major) continue;
ctx.lineWidth = major ? S * 0.004 : S * 0.0018;
const r0 = major ? 0.905 : 0.918;
ctx.beginPath();
ctx.moveTo(c + Math.sin(a) * c * r0, c - Math.cos(a) * c * r0);
ctx.lineTo(c + Math.sin(a) * c * 0.955, c - Math.cos(a) * c * 0.955);
ctx.stroke();
}
ctx.globalAlpha = 1;
// typography
ctx.fillStyle = d.text; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.font = `500 ${S * 0.062}px "Cormorant Garamond", serif`;
if ('letterSpacing' in ctx) ctx.letterSpacing = `${S * 0.012}px`;
ctx.fillText('VERANCE', c + S * 0.006, c * 0.6);
ctx.font = `400 ${S * 0.019}px "Jost", sans-serif`;
if ('letterSpacing' in ctx) ctx.letterSpacing = `${S * 0.008}px`;
ctx.globalAlpha = 0.8;
ctx.fillText('GENÈVE', c + S * 0.004, c * 0.69);
ctx.globalAlpha = 1;
ctx.font = `italic 400 ${S * 0.05}px "Cormorant Garamond", serif`;
if ('letterSpacing' in ctx) ctx.letterSpacing = `${S * 0.004}px`;
ctx.fillText('Nocturne', c, c * 1.36);
ctx.font = `400 ${S * 0.016}px "Jost", sans-serif`;
if ('letterSpacing' in ctx) ctx.letterSpacing = `${S * 0.007}px`;
ctx.globalAlpha = 0.75;
ctx.fillText('AUTOMATIQUE · CHRONOMÈTRE', c, c * 1.445);
ctx.font = `400 ${S * 0.013}px "Jost", sans-serif`;
ctx.fillText('SWISS MADE', c, c * 1.86);
ctx.globalAlpha = 1;
ctx.restore();
}
function makeInsertTexture() {
const S = 1024, c = S / 2;
const canvas = document.createElement('canvas'); canvas.width = canvas.height = S;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#070708'; ctx.fillRect(0, 0, S, S);
const g = ctx.createRadialGradient(c, c, c * 0.88, c, c, c);
g.addColorStop(0, '#141416'); g.addColorStop(0.5, '#060607'); g.addColorStop(1, '#121214');
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(c, c, c, 0, TAU); ctx.fill();
ctx.fillStyle = '#e9c77f'; ctx.strokeStyle = '#e9c77f';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.font = `500 ${S * 0.04}px "Jost", sans-serif`;
for (let i = 0; i < 60; i++) {
const a = (i / 60) * TAU;
ctx.save(); ctx.translate(c, c); ctx.rotate(a);
if (i === 0) {
ctx.beginPath(); ctx.moveTo(0, -c * 0.905); ctx.lineTo(-S * 0.018, -c * 0.975); ctx.lineTo(S * 0.018, -c * 0.975); ctx.closePath(); ctx.fill();
} else if (i % 10 === 0) {
ctx.fillText(String(i), 0, -c * 0.943);
} else if (i % 5 === 0) {
ctx.fillRect(-S * 0.004, -c * 0.975, S * 0.008, c * 0.06);
} else if (i < 15) {
ctx.fillRect(-S * 0.0018, -c * 0.97, S * 0.0036, c * 0.045);
} else {
ctx.beginPath(); ctx.arc(0, -c * 0.945, S * 0.004, 0, TAU); ctx.fill();
}
ctx.restore();
}
const tex = new THREE.CanvasTexture(canvas);
tex.colorSpace = THREE.SRGBColorSpace; tex.anisotropy = 8;
return tex;
}
function makePlateTexture() {
const S = 512;
const canvas = document.createElement('canvas'); canvas.width = canvas.height = S;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#8d8a84'; ctx.fillRect(0, 0, S, S);
// Côtes de Genève stripes
for (let x = -S; x < S * 2; x += 36) {
const g = ctx.createLinearGradient(x, 0, x + 36, 0);
g.addColorStop(0, '#6f6b65'); g.addColorStop(0.5, '#cfcac0'); g.addColorStop(1, '#6f6b65');
ctx.save(); ctx.translate(S / 2, S / 2); ctx.rotate(-0.5); ctx.translate(-S / 2, -S / 2);
ctx.fillStyle = g; ctx.fillRect(x, -S, 36, S * 3); ctx.restore();
}
const tex = new THREE.CanvasTexture(canvas);
tex.colorSpace = THREE.SRGBColorSpace;
return tex;
}
function makeCasebackTexture() {
const S = 1024, c = S / 2;
const canvas = document.createElement('canvas'); canvas.width = canvas.height = S;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, S, S);
// fine circular brushing
for (let r = c * 0.7; r < c; r += 3) { ctx.strokeStyle = `rgba(0,0,0,${0.03 + Math.random() * 0.04})`; ctx.beginPath(); ctx.arc(c, c, r, 0, TAU); ctx.stroke(); }
ctx.fillStyle = 'rgba(40,30,15,0.75)'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.font = `500 ${S * 0.03}px "Jost", sans-serif`;
const text = 'VERANCE GENÈVE · NOCTURNE · Nº 001/188 · 18K 750 · 100M · SAPPHIRE · ';
const r = c * 0.87;
[...text].forEach((ch, i) => {
const a = (i / text.length) * TAU;
ctx.save(); ctx.translate(c + Math.sin(a) * r, c - Math.cos(a) * r); ctx.rotate(a); ctx.fillText(ch, 0, 0); ctx.restore();
});
const tex = new THREE.CanvasTexture(canvas);
tex.colorSpace = THREE.SRGBColorSpace; tex.anisotropy = 8;
return tex;
}
/* ---------- Geometry helpers ---------- */
function gearGeometry(teeth, r, depth) {
const shape = new THREE.Shape();
const rIn = r * 0.9, step = TAU / teeth;
for (let i = 0; i < teeth; i++) {
const a = i * step;
const pts = [[a, rIn], [a + step * 0.2, r], [a + step * 0.5, r], [a + step * 0.7, rIn]];
pts.forEach(([ang, rad], j) => {
const x = Math.cos(ang) * rad, y = Math.sin(ang) * rad;
i === 0 && j === 0 ? shape.moveTo(x, y) : shape.lineTo(x, y);
});
}
shape.closePath();
const hub = new THREE.Path(); hub.absarc(0, 0, r * 0.12, 0, TAU, true); shape.holes.push(hub);
const spokes = r > 0.12 ? 5 : 4;
for (let i = 0; i < spokes; i++) {
const a = (i / spokes) * TAU, h = new THREE.Path();
h.absarc(Math.cos(a) * r * 0.52, Math.sin(a) * r * 0.52, r * 0.24, 0, TAU, true);
shape.holes.push(h);
}
const geo = new THREE.ExtrudeGeometry(shape, { depth, bevelEnabled: false, curveSegments: 18 });
geo.rotateX(-Math.PI / 2);
return geo;
}
function handGeometry(pts, depth = 0.012) {
const shape = new THREE.Shape();
pts.forEach(([x, y], i) => (i ? shape.lineTo(x, y) : shape.moveTo(x, y)));
shape.closePath();
const geo = new THREE.ExtrudeGeometry(shape, {
depth, bevelEnabled: true, bevelThickness: 0.004, bevelSize: 0.004, bevelSegments: 2,
});
geo.rotateX(-Math.PI / 2);
return geo;
}
function lathe(points, segs = 160) {
return new THREE.LatheGeometry(points.map(([r, y]) => new THREE.Vector2(r, y)), segs);
}
/* ---------- Build ---------- */
async function buildWatch() {
const group = new THREE.Group();
const inner = new THREE.Group();
inner.rotation.x = Math.PI / 2;
group.add(inner);
// Materials
const caseMat = new THREE.MeshPhysicalMaterial({
color: CASES.yellow.color, metalness: 1, roughness: CASES.yellow.roughness,
clearcoat: 0.5, clearcoatRoughness: 0.12, side: THREE.DoubleSide,
});
const brushedMat = new THREE.MeshPhysicalMaterial({ color: 0xd9ae5e, metalness: 1, roughness: 0.38 });
const handMat = new THREE.MeshPhysicalMaterial({ color: 0xf0cc7e, metalness: 1, roughness: 0.12, clearcoat: 1 });
const ceramic = new THREE.MeshPhysicalMaterial({ color: 0x0b0b0d, metalness: 0.15, roughness: 0.22, clearcoat: 1, clearcoatRoughness: 0.05 });
const steel = new THREE.MeshStandardMaterial({ color: 0xb8b4ad, metalness: 1, roughness: 0.28 });
const ruby = new THREE.MeshPhysicalMaterial({ color: 0xb0122e, emissive: 0x2a0008, roughness: 0.05, clearcoat: 1 });
const parts = {};
const mk = (name) => { const g = new THREE.Group(); g.name = name; inner.add(g); parts[name] = g; return g; };
/* Case */
const caseG = mk('case');
caseG.add(new THREE.Mesh(lathe([
[0.82, -0.188], [0.93, -0.15], [1.01, -0.09], [1.06, -0.02],
[1.07, 0.05], [1.055, 0.1], [1.025, 0.13], [0.87, 0.13],
]), caseMat));
/* Caseback: screwed ring with an exhibition sapphire and engraving */
const casebackG = mk('caseback');
casebackG.add(new THREE.Mesh(lathe([
[0.56, -0.18], [0.82, -0.18], [0.848, -0.19], [0.84, -0.212], [0.8, -0.228], [0.56, -0.232],
]), caseMat));
const engraveMat = new THREE.MeshPhysicalMaterial({ map: makeCasebackTexture(), metalness: 1, roughness: 0.3 });
engraveMat.color = caseMat.color; // follows the chosen case metal
const engrave = new THREE.Mesh(new THREE.RingGeometry(0.58, 0.8, 128), engraveMat);
engrave.rotation.x = Math.PI / 2; engrave.position.y = -0.2335; casebackG.add(engrave);
const window_ = new THREE.Mesh(new THREE.CylinderGeometry(0.565, 0.565, 0.02, 96), new THREE.MeshPhysicalMaterial({
color: 0xffffff, transparent: true, opacity: 0.16, roughness: 0, metalness: 0, clearcoat: 1, depthWrite: false, envMapIntensity: 2,
}));
window_.position.y = -0.21; casebackG.add(window_);
const screwGeo = new THREE.CylinderGeometry(0.028, 0.028, 0.012, 20);
for (let i = 0; i < 6; i++) {
const a = (i / 6) * TAU + 0.26;
const sc = new THREE.Mesh(screwGeo, new THREE.MeshStandardMaterial({ color: 0xb8b4ad, metalness: 1, roughness: 0.28 }));
sc.position.set(Math.sin(a) * 0.815, -0.236, -Math.cos(a) * 0.815); casebackG.add(sc);
}
const gasketMat = new THREE.MeshStandardMaterial({ color: 0x141414, roughness: 0.8, metalness: 0 });
const gasketLow = new THREE.Mesh(new THREE.TorusGeometry(0.83, 0.012, 8, 96), gasketMat);
gasketLow.rotation.x = Math.PI / 2; gasketLow.position.y = -0.178; casebackG.add(gasketLow);
const lugGeo = new RoundedBoxGeometry(0.2, 0.13, 0.55, 4, 0.05);
for (const sx of [-1, 1]) for (const sz of [-1, 1]) {
const lug = new THREE.Mesh(lugGeo, caseMat);
lug.position.set(sx * 0.52, 0.0, sz * 1.04);
lug.rotation.x = sz * 0.2;
caseG.add(lug);
}
const barGeo = new THREE.CylinderGeometry(0.03, 0.03, 0.9, 16); barGeo.rotateZ(Math.PI / 2);
for (const sz of [-1, 1]) { const b = new THREE.Mesh(barGeo, steel); b.position.set(0, -0.04, sz * 1.2); caseG.add(b); }
// Crown + guards
const crown = new THREE.Group();
const crownBody = new THREE.Mesh(new THREE.CylinderGeometry(0.115, 0.115, 0.15, 48), caseMat);
crownBody.rotation.z = Math.PI / 2; crownBody.position.x = 1.17; crown.add(crownBody);
const ridgeGeo = new THREE.BoxGeometry(0.13, 0.016, 0.016);
for (let i = 0; i < 28; i++) {
const a = (i / 28) * TAU, r = new THREE.Mesh(ridgeGeo, caseMat);
r.position.set(1.17, Math.sin(a) * 0.118, Math.cos(a) * 0.118); r.rotation.x = -a; crown.add(r);
}
const cap = new THREE.Mesh(new THREE.CylinderGeometry(0.08, 0.08, 0.02, 48), ceramic);
cap.rotation.z = Math.PI / 2; cap.position.x = 1.25; crown.add(cap);
const stem = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.05, 0.1, 24), steel);
stem.rotation.z = Math.PI / 2; stem.position.x = 1.08; crown.add(stem);
const guardGeo = new RoundedBoxGeometry(0.14, 0.12, 0.08, 3, 0.03);
for (const sz of [-1, 1]) { const gd = new THREE.Mesh(guardGeo, caseMat); gd.position.set(1.09, 0.0, sz * 0.17); crown.add(gd); }
crown.position.y = 0.0;
caseG.add(crown);
/* Movement: main plate → gear train → bridges & balance, plus the oscillating rotor */
const movementG = mk('movement');
const plateL = new THREE.Group(), trainL = new THREE.Group(), bridgeL = new THREE.Group();
movementG.add(plateL, trainL, bridgeL);
const plate = new THREE.Mesh(new THREE.CylinderGeometry(0.82, 0.82, 0.03, 96),
new THREE.MeshStandardMaterial({ map: makePlateTexture(), metalness: 0.9, roughness: 0.35 }));
plate.material.userData.pColor = 0x9a968f;
plate.position.y = -0.08; plateL.add(plate);
const roseMat = new THREE.MeshPhysicalMaterial({ color: 0xe4a07e, metalness: 1, roughness: 0.25 });
const gearDefs = [
{ t: 48, r: 0.3, x: -0.18, z: 0.12, y: -0.055, m: brushedMat, s: 0.25 },
{ t: 14, r: 0.1, x: 0.17, z: 0.24, y: -0.04, m: roseMat, s: -1.2 },
{ t: 36, r: 0.22, x: 0.28, z: -0.22, y: -0.05, m: brushedMat, s: 0.5 },
{ t: 12, r: 0.08, x: 0.05, z: -0.1, y: -0.03, m: roseMat, s: -1.9 },
{ t: 30, r: 0.18, x: -0.34, z: -0.38, y: -0.045, m: roseMat, s: -0.7 },
{ t: 20, r: 0.12, x: -0.5, z: 0.28, y: -0.04, m: brushedMat, s: 1.1 },
];
const gears = gearDefs.map((d) => {
const m = new THREE.Mesh(gearGeometry(d.t, d.r, 0.018), d.m);
m.position.set(d.x, d.y, d.z); m.userData.speed = d.s; trainL.add(m);
const arbor = new THREE.Mesh(new THREE.CylinderGeometry(0.012, 0.012, 0.09, 12), steel);
arbor.position.set(d.x, d.y + 0.01, d.z); trainL.add(arbor);
const j = new THREE.Mesh(new THREE.SphereGeometry(0.022, 16, 12), ruby);
j.position.set(d.x, d.y + 0.03, d.z); bridgeL.add(j);
return m;
});
// Balance wheel
const balance = new THREE.Group();
const rim = new THREE.Mesh(new THREE.TorusGeometry(0.15, 0.014, 12, 64), brushedMat); rim.rotation.x = Math.PI / 2; balance.add(rim);
for (let i = 0; i < 3; i++) {
const sp = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.01, 0.018), brushedMat); sp.rotation.y = (i / 3) * Math.PI; balance.add(sp);
}
const spring = new THREE.Mesh(new THREE.TorusGeometry(0.06, 0.004, 6, 48), steel); spring.rotation.x = Math.PI / 2; spring.position.y = 0.012; balance.add(spring);
balance.position.set(0.42, -0.01, 0.34); bridgeL.add(balance);
const cock = new THREE.Mesh(new RoundedBoxGeometry(0.42, 0.025, 0.1, 3, 0.01), brushedMat);
cock.position.set(0.3, 0.03, 0.42); cock.rotation.y = -0.5; bridgeL.add(cock);
// Bridges
const bridgeGeo = new RoundedBoxGeometry(0.7, 0.03, 0.14, 3, 0.012);
[[-0.1, 0.0, -0.1, 0.6], [0.3, 0.005, 0.1, -0.9]].forEach(([x, y, z, ry]) => {
const b = new THREE.Mesh(bridgeGeo, brushedMat); b.position.set(x, y, z); b.rotation.y = ry; bridgeL.add(b);
});
[[0.05, -0.18], [0.45, 0.02], [-0.35, 0.2]].forEach(([x, z]) => {
const s = new THREE.Mesh(new THREE.CylinderGeometry(0.025, 0.025, 0.02, 16), steel); s.position.set(x, 0.025, z); bridgeL.add(s);
});
const rotorG = mk('rotor');
const rotor = new THREE.Group();
const rShape = new THREE.Shape();
rShape.absarc(0, 0, 0.76, 0, Math.PI, false); rShape.absarc(0, 0, 0.12, Math.PI, 0, true);
const rGeo = new THREE.ExtrudeGeometry(rShape, { depth: 0.022, bevelEnabled: true, bevelThickness: 0.004, bevelSize: 0.006, bevelSegments: 2, curveSegments: 48 });
rGeo.rotateX(-Math.PI / 2);
rotor.add(new THREE.Mesh(rGeo, brushedMat));
const weight = new THREE.Mesh(new THREE.TorusGeometry(0.72, 0.035, 10, 64, Math.PI), roseMat);
weight.rotation.x = -Math.PI / 2; weight.position.y = 0.012; rotor.add(weight);
const rHub = new THREE.Mesh(new THREE.CylinderGeometry(0.1, 0.1, 0.04, 32), steel); rotor.add(rHub);
const rJewel = new THREE.Mesh(new THREE.SphereGeometry(0.03, 16, 12), ruby); rJewel.position.y = 0.02; rotor.add(rJewel);
rotor.position.y = -0.155; rotorG.add(rotor);
/* Dial */
const dialG = mk('dial');
const dialCanvas = document.createElement('canvas'); dialCanvas.width = dialCanvas.height = 2048;
drawDial(dialCanvas, DIALS.noir);
const dialTex = new THREE.CanvasTexture(dialCanvas);
dialTex.colorSpace = THREE.SRGBColorSpace; dialTex.anisotropy = 8;
const dialMat = new THREE.MeshPhysicalMaterial({ map: dialTex, metalness: 0.35, roughness: 0.38, clearcoat: 0.6, clearcoatRoughness: 0.2 });
dialMat.userData.isDial = true;
let dialKey = 'noir';
const dial = new THREE.Mesh(new THREE.CircleGeometry(0.87, 128), dialMat);
dial.rotation.x = -Math.PI / 2; dial.position.y = 0.1; dialG.add(dial);
const flange = new THREE.Mesh(new THREE.CylinderGeometry(0.875, 0.87, 0.04, 128, 1, true), ceramic);
flange.position.y = 0.12; dialG.add(flange);
const idx = new THREE.Group();
for (let i = 0; i < 12; i++) {
const a = (i / 12) * TAU;
const long = i % 3 === 0;
const geo = new RoundedBoxGeometry(i === 0 ? 0.034 : 0.045, 0.03, long ? 0.17 : 0.12, 2, 0.01);
const offsets = i === 0 ? [-0.03, 0.03] : [0];
offsets.forEach((o) => {
const m = new THREE.Mesh(geo, handMat);
const r = long ? 0.66 : 0.685;
m.position.set(Math.sin(a) * r + Math.cos(a) * o, 0.115, -Math.cos(a) * r + Math.sin(a) * o);
m.rotation.y = -a;
idx.add(m);
});
}
dialG.add(idx); idx.userData.rel = 0.3;
/* Hands */
const handsG = mk('hands');
const hourG = new THREE.Group(), minG = new THREE.Group(), secG = new THREE.Group();
hourG.position.y = 0.125; minG.position.y = 0.14; secG.position.y = 0.155;
hourG.add(new THREE.Mesh(handGeometry([[0, -0.08], [0.048, 0.02], [0, 0.46], [-0.048, 0.02]]), handMat));
minG.add(new THREE.Mesh(handGeometry([[0, -0.1], [0.034, 0.02], [0, 0.72], [-0.034, 0.02]]), handMat));
const secMat = new THREE.MeshPhysicalMaterial({ color: 0xf6dca0, metalness: 1, roughness: 0.15 });
secG.add(new THREE.Mesh(handGeometry([[-0.006, -0.2], [0.006, -0.2], [0.004, 0.8], [-0.004, 0.8]], 0.006), secMat));
const cw = new THREE.Mesh(new THREE.CylinderGeometry(0.035, 0.035, 0.01, 32), secMat); cw.position.z = 0.16; secG.add(cw);
const hub = new THREE.Mesh(new THREE.CylinderGeometry(0.035, 0.04, 0.05, 32), handMat); hub.position.y = 0.145;
handsG.add(hourG, minG, secG, hub);
/* Bezel */
const bezelG = mk('bezel');
bezelG.add(new THREE.Mesh(lathe([
[0.87, 0.13], [0.875, 0.2], [0.9, 0.224], [1.0, 0.222], [1.035, 0.19], [1.048, 0.15], [1.035, 0.128],
]), caseMat));
const insert = new THREE.Mesh(new THREE.RingGeometry(0.89, 0.995, 160), new THREE.MeshPhysicalMaterial({
map: makeInsertTexture(), metalness: 0.25, roughness: 0.2, clearcoat: 1, clearcoatRoughness: 0.04,
}));
insert.material.userData.pColor = 0x141416;
insert.rotation.x = -Math.PI / 2; insert.position.y = 0.227;
const insertL = new THREE.Group(); insertL.add(insert); bezelG.add(insertL); insertL.userData.rel = 0.3;
const gasketTop = new THREE.Mesh(new THREE.TorusGeometry(0.93, 0.012, 8, 96), gasketMat);
gasketTop.rotation.x = Math.PI / 2; gasketTop.position.y = 0.132;
const gasketL = new THREE.Group(); gasketL.add(gasketTop); bezelG.add(gasketL); gasketL.userData.rel = -0.3;
const knurl = new THREE.InstancedMesh(new THREE.BoxGeometry(0.018, 0.045, 0.02), caseMat, 90);
const d = new THREE.Object3D();
for (let i = 0; i < 90; i++) {
const a = (i / 90) * TAU;
d.position.set(Math.sin(a) * 1.045, 0.168, -Math.cos(a) * 1.045); d.rotation.set(0, -a, 0); d.updateMatrix();
knurl.setMatrixAt(i, d.matrix);
}
bezelG.add(knurl);
/* Crystal */
const crystalG = mk('crystal');
const crystal = new THREE.Mesh(new THREE.CylinderGeometry(0.885, 0.885, 0.03, 128), new THREE.MeshPhysicalMaterial({
color: 0xffffff, metalness: 0, roughness: 0, transparent: true, opacity: 0.14,
depthWrite: false, clearcoat: 1, clearcoatRoughness: 0, envMapIntensity: 2.2, specularIntensity: 1,
}));
crystal.position.y = 0.228; crystalG.add(crystal);
/* Bracelet — a jointed chain that curls from open (flat) to closed and locks with a clasp.
Each link stores a joint angle; blending the angles keeps the chain connected while it wraps. */
const STEP = 0.205;
const centerGeo = new RoundedBoxGeometry(0.3, 0.1, 0.19, 3, 0.035);
const sideGeo = new RoundedBoxGeometry(0.25, 0.09, 0.19, 3, 0.035);
const START = { z: 1.2, y: -0.04 }; // z is measured outward from the case on each side
const closedCurve = new THREE.CatmullRomCurve3([
[1.2, -0.04], [1.62, -0.14], [2.02, -0.55], [2.2, -1.25], [1.95, -1.98], [1.28, -2.48], [0.62, -2.68], [0.25, -2.72],
].map(([z, y]) => new THREE.Vector3(0, y, z)));
const curveLen = closedCurve.getLength();
const LINKS = Math.floor(curveLen / STEP);
const closedA = [], openA = [];
let pz = START.z, py = START.y, prevA = 0;
for (let i = 1; i <= LINKS; i++) {
const p = closedCurve.getPointAt(Math.min(1, (i * STEP) / curveLen));
let a = Math.atan2(p.y - py, p.z - pz);
while (a - prevA > Math.PI) a -= Math.PI * 2;
while (a - prevA < -Math.PI) a += Math.PI * 2;
closedA.push(a); openA.push(-0.03 - 0.012 * (i - 1));
prevA = a; pz = p.z; py = p.y;
}
const smooth = (x) => x * x * (3 - 2 * x);
const clamp01 = (x) => Math.min(1, Math.max(0, x));
const makeStrap = (dir) => {
const g = new THREE.Group();
const links = [];
for (let i = 0; i < LINKS; i++) {
const link = new THREE.Group();
link.add(new THREE.Mesh(centerGeo, caseMat));
for (const sx of [-1, 1]) { const m = new THREE.Mesh(sideGeo, ceramic); m.position.x = sx * 0.285; link.add(m); }
g.add(link); links.push(link);
}
inner.add(g);
return { g, links, dir };
};
function layoutStrap(strap, c) {
let z = START.z, y = START.y;
strap.links.forEach((link, i) => {
const u = i / (LINKS - 1);
const k = smooth(clamp01((c - u * 0.45) / 0.55)); // links nearest the case curl first
const a = openA[i] + (closedA[i] - openA[i]) * k;
const dz = Math.cos(a) * STEP, dy = Math.sin(a) * STEP;
link.position.set(0, y + dy / 2, strap.dir * (z + dz / 2));
link.rotation.x = strap.dir > 0 ? -a : Math.PI + a;
z += dz; y += dy;
});
return { z, y };
}
const strapA = makeStrap(1), strapB = makeStrap(-1);
parts.strapA = strapA.g; parts.strapB = strapB.g;
const lockEnd = layoutStrap(strapA, 1);
// Butterfly (papillon) clasp sitting where the two ends meet
const claspG = new THREE.Group(); inner.add(claspG); parts.clasp = claspG;
const clasp = new THREE.Group();
clasp.position.set(0, lockEnd.y, 0);
const claspBody = new THREE.Mesh(new RoundedBoxGeometry(0.84, 0.1, Math.max(0.4, lockEnd.z * 2 + 0.06), 3, 0.035), caseMat);
clasp.add(claspBody);
const lidPivot = new THREE.Group(); lidPivot.position.set(0, -0.06, lockEnd.z + 0.02); clasp.add(lidPivot);
const lidLen = lockEnd.z * 2 + 0.04;
const lid = new THREE.Mesh(new RoundedBoxGeometry(0.62, 0.05, lidLen, 3, 0.02), caseMat);
lid.position.set(0, -0.02, -lidLen / 2); lidPivot.add(lid);
const inlay = new THREE.Mesh(new RoundedBoxGeometry(0.34, 0.02, lidLen * 0.6, 2, 0.008), ceramic);
inlay.position.set(0, -0.05, -lidLen / 2); lidPivot.add(inlay);
const emblem = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.05, 0.02, 32), handMat);
emblem.position.set(0, -0.065, -lidLen / 2); lidPivot.add(emblem);
claspG.add(clasp);
let closeAmt = -1;
function setClose(c) {
if (Math.abs(c - closeAmt) < 1e-4) return;
closeAmt = c;
layoutStrap(strapA, c); layoutStrap(strapB, c);
const appear = smooth(clamp01((c - 0.8) / 0.12));
clasp.scale.setScalar(Math.max(0.001, appear));
lidPivot.rotation.x = -1.35 * (1 - smooth(clamp01((c - 0.9) / 0.1)));
}
setClose(0);
/* Anchors for DOM callouts (placed on the edge facing the label's side) */
const anchors = {};
const addAnchor = (name, parent, x, y, z = 0) => { const o = new THREE.Object3D(); o.position.set(x, y, z); parent.add(o); anchors[name] = o; };
addAnchor('crystal', crystalG, 0.86, 0.228);
addAnchor('bezel', insertL, -0.96, 0.227);
addAnchor('hands', handsG, 0.12, 0.15, -0.3);
addAnchor('dial', dialG, -0.86, 0.1);
addAnchor('movement', bridgeL, 0.8, 0.0);
addAnchor('case', caseG, -1.07, 0.0);
addAnchor('rotor', rotorG, 0.72, -0.155);
addAnchor('caseback', casebackG, -0.84, -0.205);
addAnchor('clasp', claspG, 0.42, lockEnd.y - 0.05, 0);
addAnchor('crownTip', crown, 1.3, 0, 0); // keeps the right label column clear of the crown
/* Explode: each layer lifts along the dial axis by (offset × e), sub-layers spread
within their group, the crown is drawn out and the bracelet retracts. */
const layers = [
[casebackG, -1.4], [rotorG, -0.78], [caseG, 0], [movementG, 0.55], [dialG, 1.6],
[handsG, 2.2], [bezelG, 2.95], [crystalG, 3.6],
];
const subLayers = [
[plateL, 0], [trainL, 0.28], [bridgeL, 0.56], [idx, 0.3],
[hourG, 0], [minG, 0.17], [secG, 0.34], [insertL, 0.3], [gasketL, -0.3],
];
subLayers.forEach(([o]) => { o.userData.baseY = o.position.y; });
const smoothE = (x) => { const c = Math.min(1, Math.max(0, x)); return c * c * (3 - 2 * c); };
function setExplode(e) {
const shift = -1.1 * e;
layers.forEach(([g, off]) => { g.position.y = off * e + shift; });
subLayers.forEach(([o, rel]) => { o.position.y = o.userData.baseY + rel * e; });
const gone = 1 - smoothE(e * 1.8); // bracelet slides away and retracts
const bs = Math.max(0.001, gone);
parts.strapA.position.set(0, shift, 1.2 * e); parts.strapA.scale.setScalar(bs);
parts.strapB.position.set(0, shift, -1.2 * e); parts.strapB.scale.setScalar(bs);
claspG.position.set(0, shift, 0); claspG.scale.setScalar(bs);
crown.position.x = 0.3 * e;
bezelG.rotation.y = -e * 0.5;
movementG.rotation.y = e * 0.2;
rotorG.rotation.y = e * 0.9;
}
/* Time */
function update(t) {
const now = new Date();
const ms = now.getMilliseconds();
const s = now.getSeconds() + Math.floor(ms / 125) * 0.125; // 8 beats per second sweep
const m = now.getMinutes() + s / 60;
const h = (now.getHours() % 12) + m / 60;
secG.rotation.y = -(s / 60) * TAU;
minG.rotation.y = -(m / 60) * TAU;
hourG.rotation.y = -(h / 12) * TAU;
gears.forEach((g) => { g.rotation.y = t * g.userData.speed; });
balance.rotation.y = Math.sin(t * TAU * 2) * 1.4;
rotor.rotation.y = Math.sin(t * 0.5) * 2.2;
}
/* Configurator */
function setCase(key) {
const c = CASES[key];
caseMat.color.set(c.color);
caseMat.roughness = c.roughness;
}
function setDial(key) {
dialKey = key;
drawDial(dialCanvas, DIALS[key]);
dialTex.needsUpdate = true;
}
return { group, inner, parts, anchors, setExplode, setClose, update, setCase, setDial, getDialColor: () => DIALS[dialKey].light };
}
return { buildWatch, CASES, DIALS };
}
/* ---------------- morph.js (verbatim) ---------------- */
function createMorphModule(THREE, MeshSurfaceSampler) {
// "Gold dust" morph for the configurator: the watch dissolves into particles sampled
// from its own surfaces, the option is swapped while it is apart, then the dust
// swirls back together into the new watch.
const COUNT = 9000;
function createMorph(watch, parent, renderer) {
const geo = new THREE.BufferGeometry();
const pos = new Float32Array(COUNT * 3);
const col = new Float32Array(COUNT * 3);
const dir = new Float32Array(COUNT * 3);
const rnd = new Float32Array(COUNT);
const del = new Float32Array(COUNT);
for (let i = 0; i < COUNT; i++) rnd[i] = Math.random();
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
geo.setAttribute('aColor', new THREE.BufferAttribute(col, 3));
geo.setAttribute('aDir', new THREE.BufferAttribute(dir, 3));
geo.setAttribute('aRand', new THREE.BufferAttribute(rnd, 1));
geo.setAttribute('aDelay', new THREE.BufferAttribute(del, 1));
const mat = new THREE.ShaderMaterial({
transparent: true, depthWrite: false, depthTest: false,
uniforms: {
uProgress: { value: 0 }, uOpacity: { value: 0 }, uTime: { value: 0 },
uSize: { value: 32 * renderer.getPixelRatio() },
uTint: { value: new THREE.Color(0xd9ae5e) }, uTintAmt: { value: 0 },
},
vertexShader: /* glsl */`
attribute vec3 aColor; attribute vec3 aDir; attribute float aRand; attribute float aDelay;
uniform float uProgress, uTime, uSize, uTintAmt; uniform vec3 uTint;
varying vec3 vColor; varying float vFade;
void main() {
// a wave travels across the watch: each grain lifts off in turn (no rotation)
float p = clamp((uProgress - aDelay * 0.45) / 0.55, 0.0, 1.0);
float e = p * p * (3.0 - 2.0 * p);
vec3 pos = position;
pos += aDir * e * (0.25 + aRand * 0.75); // drift apart
pos.y += e * (0.08 + aRand * 0.22); // a little lift, like dust in warm air
float n = uTime * 1.4 + aRand * 40.0;
pos += vec3(sin(n), cos(n * 1.3), sin(n * 0.7)) * 0.05 * e; // gentle shimmer while apart
vec4 mv = modelViewMatrix * vec4(pos, 1.0);
gl_Position = projectionMatrix * mv;
gl_PointSize = uSize * (0.55 + aRand * 0.9) * (1.0 - e * 0.3) * (1.0 / -mv.z);
vColor = mix(aColor, uTint, uTintAmt * e);
vFade = 1.0 - e * 0.2;
}`,
fragmentShader: /* glsl */`
uniform float uOpacity;
varying vec3 vColor; varying float vFade;
void main() {
float d = length(gl_PointCoord - 0.5);
float a = smoothstep(0.5, 0.15, d);
gl_FragColor = vec4(vColor * 1.15, a * uOpacity * vFade);
#include <tonemapping_fragment>
#include <colorspace_fragment>
}`,
});
const points = new THREE.Points(geo, mat);
points.visible = false;
points.frustumCulled = false;
parent.add(points);
const gold = new THREE.Color(0xd9ae5e);
const inv = new THREE.Matrix4(), m = new THREE.Matrix4();
const p = new THREE.Vector3(), c = new THREE.Color();
// Sample points across every visible opaque surface, weighted by area.
function sample() {
watch.group.updateMatrixWorld(true);
inv.copy(watch.group.matrixWorld).invert();
const items = [];
watch.group.traverse((o) => {
if (!o.isMesh || o.isInstancedMesh || o.material.transparent) return;
let vis = true; o.traverseAncestors((a) => { if (!a.visible && a !== watch.group) vis = false; });
if (!vis) return;
if (!o.userData.sampler) o.userData.sampler = new MeshSurfaceSampler(o).build();
const s = o.userData.sampler;
const mm = new THREE.Matrix4().multiplyMatrices(inv, o.matrixWorld);
const sc = mm.getMaxScaleOnAxis();
const area = s.distribution[s.distribution.length - 1] * sc * sc;
const ud = o.material.userData;
const mc = ud.isDial ? new THREE.Color(watch.getDialColor()) : ud.pColor ? new THREE.Color(ud.pColor) : o.material.color;
items.push({ s, mm, area, color: mc });
});
const total = items.reduce((a, it) => a + it.area, 0);
let i = 0;
items.forEach((it, idx) => {
const n = idx === items.length - 1 ? COUNT - i : Math.round((COUNT * it.area) / total);
for (let k = 0; k < n && i < COUNT; k++, i++) {
it.s.sample(p); p.applyMatrix4(it.mm);
pos[i * 3] = p.x; pos[i * 3 + 1] = p.y; pos[i * 3 + 2] = p.z;
c.copy(it.color || gold);
col[i * 3] = c.r; col[i * 3 + 1] = c.g; col[i * 3 + 2] = c.b;
const r = Math.random();
const d = new THREE.Vector3(p.x, p.y, p.z * 2.5).normalize()
.add(new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).multiplyScalar(1.2))
.normalize().multiplyScalar(0.6 + r);
dir[i * 3] = d.x; dir[i * 3 + 1] = d.y; dir[i * 3 + 2] = d.z;
del[i] = Math.min(1, Math.max(0, (p.x + 1.3) / 2.6)) * 0.75 + Math.random() * 0.25; // left → right wave
}
});
geo.attributes.position.needsUpdate = true;
geo.attributes.aColor.needsUpdate = true;
geo.attributes.aDir.needsUpdate = true;
geo.attributes.aDelay.needsUpdate = true;
}
let tl = null;
const u = mat.uniforms;
function play(onSwap, tint) {
if (tl) tl.progress(1).kill();
sample();
u.uTint.value.set(tint);
u.uProgress.value = 0; u.uOpacity.value = 0; u.uTintAmt.value = 0;
points.visible = true;
tl = gsap.timeline({ onComplete: () => { points.visible = false; tl = null; } })
.to(u.uOpacity, { value: 1, duration: 0.18, ease: 'power1.out' })
.add(() => { watch.group.visible = false; })
.to(u.uProgress, { value: 1, duration: 1.1, ease: 'power2.inOut' })
.to(u.uTintAmt, { value: 1, duration: 1.1, ease: 'power2.inOut' }, '<')
.add(() => onSwap && onSwap())
.to({}, { duration: 0.15 })
.to(u.uProgress, { value: 0, duration: 1.3, ease: 'power3.inOut' })
.add(() => { watch.group.visible = true; })
.to(u.uOpacity, { value: 0, duration: 0.4, ease: 'power1.in' });
return tl;
}
return { play, update: (t) => { u.uTime.value = t; } };
}
return { createMorph };
}
/* ---------------- loader.js (verbatim, started on demand) ---------------- */
// Preloader: a faint line-drawn watch draws itself behind the VERANCE wordmark while
// molten gold sweeps diagonally through the letters, left to right; a glint follows, then the screen
// splits open through the middle of the word (like a caseback) to reveal the page.
// Classic script (not a module) so it starts before Three.js has downloaded.
function startLoader(el) {
const NS = 'http://www.w3.org/2000/svg';
const root = el || document.querySelector('.loader');
const meta = root.querySelector('.loader__meta');
const word = root.querySelector('.loader__word');
const outline = word.querySelector('.ld-outline');
const glint = word.querySelector('.ld-glint');
const line = root.querySelector('.loader__line i');
const mk = (tag, attrs) => { const el = document.createElementNS(NS, tag); Object.entries(attrs).forEach(([k, v]) => el.setAttribute(k, v)); return el; };
/* ---- The watch behind the word ---- */
const watchSvg = root.querySelector('.loader__watch');
gsap.set(watchSvg, { xPercent: -50, yPercent: -50 }); // centred via GSAP so later scaling keeps it centred
const ticks = watchSvg.querySelector('.lw-ticks');
for (let i = 0; i < 60; i++) {
const a = (i / 60) * Math.PI * 2, major = i % 5 === 0;
const r0 = major ? 96 : 104, r1 = 112;
ticks.appendChild(mk('line', { x1: Math.sin(a) * r0, y1: -Math.cos(a) * r0, x2: Math.sin(a) * r1, y2: -Math.cos(a) * r1, 'stroke-width': major ? 2.4 : 0.9 }));
}
const links = watchSvg.querySelector('.lw-links');
for (let y = -290; y <= 290; y += 22) {
if (Math.abs(y) < 180) continue;
links.appendChild(mk('line', { x1: -68, y1: y, x2: 68, y2: y }));
links.appendChild(mk('line', { x1: -22, y1: y, x2: -22, y2: y + 22 }));
links.appendChild(mk('line', { x1: 22, y1: y, x2: 22, y2: y + 22 }));
}
const [hHand, mHand, sHand] = ['.lw-hour', '.lw-min', '.lw-sec'].map((q) => watchSvg.querySelector(q));
const now = new Date();
gsap.set(hHand, { svgOrigin: '0 0', rotation: ((now.getHours() % 12) + now.getMinutes() / 60) * 30 });
gsap.set(mHand, { svgOrigin: '0 0', rotation: now.getMinutes() * 6 });
gsap.set(sHand, { svgOrigin: '0 0', rotation: now.getSeconds() * 6 });
gsap.to(sHand, { rotation: '+=360', duration: 60, ease: 'none', repeat: -1 });
gsap.to(mHand, { rotation: '+=6', duration: 60, ease: 'none', repeat: -1 });
gsap.timeline()
.to(watchSvg.querySelectorAll('[pathLength]'), { strokeDashoffset: 0, duration: 1.8, stagger: 0.08, ease: 'power2.inOut' })
.from([...ticks.children, ...links.children], { opacity: 0, duration: 0.6, stagger: 0.006, ease: 'none' }, 0.3)
.from(watchSvg.querySelector('.lw-hands'), { opacity: 0, duration: 1, ease: 'expo.out' }, 0.8);
/* ---- Diagonal gold pour ---- */
// The liquid group is rotated 60° about the word's centre, so moving its surface "up"
// sweeps a slanted, wavy gold front across the letters from left to right.
const liquid = word.querySelector('.ld-liquid');
const back = word.querySelector('.ld-wave--back');
const front = word.querySelector('.ld-wave--front');
const wavePath = (amp, len) => {
let d = `M-1500 ${amp}`;
for (let x = -1500; x < 1500; x += len / 2) {
const up = Math.round((x + 1500) / (len / 2)) % 2 === 0;
d += ` Q${x + len / 4} ${up ? -amp : amp * 3} ${x + len / 2} ${amp}`;
}
return `${d} L1500 1600 L-1500 1600 Z`;
};
back.setAttribute('d', wavePath(11, 150));
front.setAttribute('d', wavePath(8, 120));
gsap.fromTo(back, { x: 0 }, { x: -300, duration: 3.2, ease: 'none', repeat: -1 }); // two periods → seamless loop
gsap.fromTo(front, { x: -240 }, { x: 0, duration: 2.2, ease: 'none', repeat: -1 });
// Surface offset along the sweep direction: 520 = nothing filled, -545 = word fully gold
// (the word's corners project to ±483 on this axis, plus room for the wave crests).
const state = { p: 0 };
const render = () => {
const e = state.p * state.p * (3 - 2 * state.p);
gsap.set(liquid, { y: 520 - e * 1065 });
line.style.transform = `scaleX(${state.p})`;
};
render();
gsap.set(root, { '--g': '0vh' });
window.__vnCover = root; // WP: the engine skips drawing the 3D scene while this covers the screen
const uncover = () => { if (window.__vnCover === root) window.__vnCover = null; };
const started = performance.now();
let fill = null;
const ready = document.fonts
? Promise.race([document.fonts.load('400 168px "Cormorant Garamond"'), new Promise((r) => setTimeout(r, 600))])
: Promise.resolve();
ready.then(() => {
render();
gsap.from(word, { opacity: 0, scale: 0.95, duration: 0.9, ease: 'expo.out' });
gsap.from(meta, { opacity: 0, y: 10, duration: 0.8, delay: 0.2, ease: 'expo.out' });
gsap.ticker.add(render);
fill = gsap.to(state, { p: 0.86, duration: 3.2, ease: 'sine.inOut' }); // slow, steady sweep
});
let fin = null, stopped = false;
const api = {
// onReveal fires as the split starts, so the page intro plays through the opening.
// WP: onDone fires once the loader has lifted.
finish(onReveal, onDone) {
ready.then(() => {
if (stopped) return;
// let most of the pour play even when everything is cached
const wait = Math.max(0, 3.0 - (performance.now() - started) / 1000);
fin = gsap.timeline({ delay: wait, onStart: () => fill && fill.kill() })
.to(state, { p: 1, duration: 0.7, ease: 'sine.out' })
.fromTo(glint, { x: -60 }, { x: 1250, duration: 0.6, ease: 'power2.inOut' }, '-=0.1')
.add(() => { uncover(); if (onReveal) onReveal(); }, '-=0.15')
.to(root, { '--g': '51vh', duration: 1.1, ease: 'expo.inOut' }, '<')
.to([word, watchSvg], { scale: 1.08, duration: 1.1, ease: 'expo.inOut' }, '<')
.to(meta, { opacity: 0, duration: 0.3 }, '<')
.add(() => {
root.style.display = 'none';
gsap.ticker.remove(render);
gsap.killTweensOf([sHand, mHand, back, front]);
if (onDone) onDone();
});
});
},
// WP: stop every tween of this loader (Elementor replaced it, or it is being lifted early).
stop() {
stopped = true;
uncover();
if (fin) fin.kill();
if (fill) fill.kill();
gsap.ticker.remove(render);
gsap.killTweensOf([sHand, mHand, back, front, state, word, watchSvg, glint, root, meta, liquid, line]);
gsap.killTweensOf([...watchSvg.querySelectorAll('[pathLength]'), ...ticks.children, ...links.children, watchSvg.querySelector('.lw-hands')]);
},
};
window.__loader = api;
return api;
}
async function veranceMain(env, veTrack) {
const { THREE, RoomEnvironment, buildWatch, CASES, DIALS, createMorph } = env;
const M = env.M;
const on = (t, type, fn, o) => { if (!t) return; t.addEventListener(type, fn, o); veTrack.listeners.push([t, type, fn, o]); };
const tick = (fn) => { gsap.ticker.add(fn); veTrack.tickers.push(fn); };
const every = (fn, ms) => { veTrack.intervals.push(setInterval(fn, ms)); };
const stOn = (type, fn) => { ScrollTrigger.addEventListener(type, fn); veTrack.stEvents.push([type, fn]); };
const snap = (el) => { if (el && !veTrack.snaps.some((s) => s[0] === el)) veTrack.snaps.push([el, el.innerHTML]); };
// Lenis' interface over native scrolling, for the editor
const nativeScroller = () => ({ velocity: 0, on() {}, off() {}, stop() {}, start() {}, raf() {}, destroy() {},
scrollTo(y) { window.scrollTo({ top: typeof y === 'number' ? y : 0, behavior: 'smooth' }); } });
await Promise.all([
document.fonts.load('500 40px "Cormorant Garamond"'),
document.fonts.load('italic 400 40px "Cormorant Garamond"'),
document.fonts.load('400 20px "Jost"'),
]).catch(() => {});
await new Promise((r) => setTimeout(r, 16)); // WP: let the loader draw a frame between heavy steps
const watch = await buildWatch();
await new Promise((r) => setTimeout(r, 16));
if (env.gen !== state.gen) { disposeObject(watch.group); return null; } // superseded while building
veTrack.ctx = gsap.context(() => {
gsap.registerPlugin(ScrollTrigger);
const lerp = (a, b, t) => a + (b - a) * t;
const clamp = (v, a = 0, b = 1) => Math.min(b, Math.max(a, v));
const isTouch = matchMedia('(hover: none), (pointer: coarse)').matches;
/* ================= Smooth scroll ================= */
const lenis = env.isEditor ? nativeScroller() : new Lenis({ lerp: M.lenis_lerp, smoothWheel: true }); // WP: the editor keeps native scroll
lenis.stop();
lenis.on('scroll', ScrollTrigger.update);
tick((time) => lenis.raf(time * 1000));
if (!env.isEditor) gsap.ticker.lagSmoothing(0);
// Mobile menu
const menuBtn = document.querySelector('.nav__menu');
const setMenu = (open) => {
document.body.classList.toggle('menu-open', open);
menuBtn.setAttribute('aria-expanded', open);
menuBtn.setAttribute('aria-label', open ? 'Close menu' : 'Open menu');
if (open) gsap.fromTo('.menu__link', { yPercent: 60, opacity: 0 }, { yPercent: 0, opacity: 1, duration: 0.9, stagger: 0.06, ease: 'expo.out', delay: 0.15 });
};
on(menuBtn, 'click', () => setMenu(!document.body.classList.contains('menu-open')));
document.querySelectorAll('.menu__link').forEach((l) => on(l, 'click', () => setMenu(false)));
// Some links land mid-section (e.g. Anatomie opens on the fully exploded view).
const scrollTargets = {};
if (!env.isEditor) document.querySelectorAll('a[href^="#"]').forEach((a) => on(a, 'click', (e) => {
const id = a.getAttribute('href');
if (id.length < 2) return;
e.preventDefault();
const custom = scrollTargets[id];
const el = document.querySelector(id);
const y = custom ? custom() : el ? el.getBoundingClientRect().top + scrollY : 0;
// Lenis only smooths wheel input; on touch devices use the browser's native smooth scroll
if (isTouch) window.scrollTo({ top: y, behavior: 'smooth' });
else lenis.scrollTo(y, { duration: 2, easing: (t) => 1 - Math.pow(1 - t, 4) });
}));
/* ================= Three.js ================= */
const canvas = document.getElementById('webgl');
let compiled = false; // WP: set once the shaders are ready
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true, powerPreference: 'high-performance' });
renderer.setPixelRatio(Math.min(devicePixelRatio, env.isEditor ? 1.5 : 2)); // WP: lighter in the editor
renderer.setSize(innerWidth, innerHeight);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = M.tone_exposure;
renderer.outputColorSpace = THREE.SRGBColorSpace;
const scene = new THREE.Scene();
const pmrem = new THREE.PMREMGenerator(renderer);
scene.environment = pmrem.fromScene(new RoomEnvironment(), 0.04).texture;
scene.environmentIntensity = M.env_intensity;
const camera = new THREE.PerspectiveCamera(30, innerWidth / innerHeight, 0.1, 100);
camera.position.set(0, 0, 7);
const key = new THREE.DirectionalLight(0xffe0ae, 2.4); key.position.set(3, 4, 5); scene.add(key);
const rim = new THREE.DirectionalLight(0xfff1d6, 1.6); rim.position.set(-4, 2, -3); scene.add(rim);
const mouseLight = new THREE.PointLight(0xffd79a, 14, 14, 2); mouseLight.position.set(0, 0, 4); scene.add(mouseLight);
const pivot = new THREE.Group(); // driven by scroll
const hover = new THREE.Group(); // driven by mouse + drag
pivot.add(hover); hover.add(watch.group); scene.add(pivot);
const morph = createMorph(watch, hover, renderer);
/* Gold dust particles (custom shader) */
const COUNT = isTouch ? 700 : 1600;
const pGeo = new THREE.BufferGeometry();
const pos = new Float32Array(COUNT * 3), scl = new Float32Array(COUNT), phs = new Float32Array(COUNT), col = new Float32Array(COUNT * 3);
// Starlight palette: bright white with the faintest warm tints
const STAR_COLORS = [[0xffffff, 0.55], [0xfffaf0, 0.3], [0xfff1e2, 0.15]].map(([hex, w]) => [new THREE.Color(hex), w]);
const pickStar = () => { let r = Math.random(); for (const [c, w] of STAR_COLORS) { if ((r -= w) <= 0) return c; } return STAR_COLORS[0][0]; };
for (let i = 0; i < COUNT; i++) {
pos[i * 3] = (Math.random() - 0.5) * 16;
pos[i * 3 + 1] = (Math.random() - 0.5) * 12;
pos[i * 3 + 2] = (Math.random() - 0.5) * 8 - 1;
scl[i] = Math.random() * 0.9 + 0.1; phs[i] = Math.random();
const sc = pickStar(); col[i * 3] = sc.r; col[i * 3 + 1] = sc.g; col[i * 3 + 2] = sc.b;
}
pGeo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
pGeo.setAttribute('aScale', new THREE.BufferAttribute(scl, 1));
pGeo.setAttribute('aPhase', new THREE.BufferAttribute(phs, 1));
pGeo.setAttribute('aColor', new THREE.BufferAttribute(col, 3));
const pMat = new THREE.ShaderMaterial({
transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,
uniforms: {
uTime: { value: 0 }, uSize: { value: 70 * renderer.getPixelRatio() },
uOpacity: { value: 0 }, uVel: { value: 0 },
},
vertexShader: /* glsl */`
attribute float aScale; attribute float aPhase; attribute vec3 aColor;
uniform float uTime; uniform float uSize; uniform float uVel;
varying float vAlpha; varying vec3 vColor;
void main() {
vec3 p = position;
p.y = mod(p.y + uTime * 0.12 * (0.4 + aScale) + uVel * aScale + 6.0, 12.0) - 6.0;
p.x += sin(uTime * 0.35 + aPhase * 6.2831) * 0.25;
p.z += cos(uTime * 0.25 + aPhase * 6.2831) * 0.2;
vec4 mv = modelViewMatrix * vec4(p, 1.0);
gl_Position = projectionMatrix * mv;
gl_PointSize = uSize * aScale * (1.0 / -mv.z);
vAlpha = 0.35 + 0.65 * (0.5 + 0.5 * sin(uTime * 1.6 + aPhase * 40.0));
vColor = aColor;
}`,
fragmentShader: /* glsl */`
uniform float uOpacity;
varying float vAlpha; varying vec3 vColor;
void main() {
float d = length(gl_PointCoord - 0.5);
float a = smoothstep(0.5, 0.0, d);
a = pow(a, 2.2);
gl_FragColor = vec4(vColor, a * vAlpha * uOpacity);
}`,
});
const dust = new THREE.Points(pGeo, pMat);
scene.add(dust);
/* ================= Scroll choreography ================= */
// Each pose sits in a layout "zone": the empty space beside that section's copy, measured
// from the DOM so the watch is centred in it and scaled to fit at any viewport size.
// y is a fraction of the visible half-height. r* in radians. s = base scale. e = explode.
// rx is kept <= 0 so the dial always faces the viewer or tilts upward, never down.
const STATES = {
hero: { zone: null, y: -0.03, rx: -0.1, ry: -0.22, rz: 0.06, s: 0.7, e: 0 },
maison: { zone: ['.maison__copy', 'right'], y: 0.0, rx: -0.16, ry: -0.62, rz: 0.08, s: 0.74, e: 0 },
anatA: { zone: ['.anatomy__head', 'right'], y: 0.0, rx: -0.85, ry: 0.5, rz: 0.0, s: 0.64, e: 0 },
anatB: { zone: ['.anatomy__head', 'right'], y: -0.06, rx: -0.62, ry: -0.32, rz: 0.04, s: 0.52, e: 1 },
calibre: { zone: ['.calibre__copy', 'left'], y: 0, rx: -0.14, ry: 0.42, rz: -0.06, s: 0.8, e: 0 },
coll: { zone: ['.collection__stage', 'in'], y: 0, rx: -0.1, ry: 0.12, rz: 0.0, s: 0.78, e: 0 },
quote: { zone: null, y: 1.5, rx: -0.35, ry: 1.2, rz: 0.2, s: 0.6, e: 0 },
voices: { zone: ['.voices__stage', 'in'], track: '.voices__stage', y: 0, rx: -0.1, ry: 0.3, rz: -0.05, s: 0.9, e: 0 },
reserve: { zone: ['.reserve__copy', 'right'], y: 0, rx: -0.14, ry: -0.42, rz: 0.08, s: 0.8, e: 0 },
};
const WATCH_W = 2.5; // world width of the watch incl. bracelet at scale 1
function viewHalf() {
const halfH = camera.position.z * Math.tan(THREE.MathUtils.degToRad(camera.fov / 2));
return { halfH, halfW: halfH * camera.aspect };
}
// Phones and portrait tablets (must match the CSS "narrow" media query).
const isNarrow = () => innerWidth <= 900 || innerWidth / innerHeight < 0.9;
function layoutZones() {
const { halfW, halfH } = viewHalf();
const narrow = isNarrow();
const fit = Math.min(1, halfW / 1.75);
const toWorld = (px) => (px / innerWidth) * 2 * halfW;
Object.entries(STATES).forEach(([name, st]) => {
st.xw = 0; st.se = st.s * fit;
if (narrow) {
// the watch lives in a band across the top of the screen; fit it to that band
if (!name.startsWith('anat')) {
const band = name === 'hero' ? 0.6 : innerWidth > 600 ? 0.47 : 0.36; // tablets have room for a larger watch
st.se = Math.min(st.se, (band * 2 * halfH * 0.95) / 4.4);
}
if (st.track) { const r = document.querySelector(st.zone[0]).getBoundingClientRect(); st.se = Math.min(st.se, ((r.height / innerHeight) * 2 * halfH * 0.84) / 4.4); }
return;
}
if (!st.zone) return;
const r = document.querySelector(st.zone[0]).getBoundingClientRect();
const [a, b] = st.zone[1] === 'right' ? [r.right, innerWidth] : st.zone[1] === 'left' ? [0, r.left] : [r.left, r.right];
st.xw = toWorld((a + b) / 2) - halfW;
st.se = Math.min(st.se, (toWorld(b - a) * 0.84) / WATCH_W);
if (st.zone[1] === 'in') st.se = Math.min(st.se, ((r.height / innerHeight) * 2 * viewHalf().halfH * 0.84) / 4.4);
});
}
layoutZones();
stOn('refresh', layoutZones);
const S = { ...STATES.hero, c: 0 };
const intro = { s: 0.001, spin: -Math.PI * 1.4, y: -0.6 };
const spin = { v: 0 }; // reserved for programmatic spins
// Each segment blends two poses by its ScrollTrigger progress. The target pose is
// recomputed every frame (no competing tweens), then S eases towards it.
const SEGS = [];
const ease = (t) => (t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2);
function segment(trigger, from, to, opts = {}, map = (p) => p) {
const st = ScrollTrigger.create({ trigger, start: 'top bottom', end: 'top top', ...opts });
SEGS.push({ st, from: STATES[from], to: STATES[to], map });
return st;
}
segment('#maison', 'hero', 'maison');
segment('#anatomie', 'maison', 'anatA');
const pinST = segment('#anatomie', 'anatA', 'anatB', { start: 'top top', end: '+=' + M.pin_length + '%', pin: true }, (p) => ease(Math.min(1, p / 0.8)));
// the explode completes at 80% of the pin (see map above); land just after it
scrollTargets['#anatomie'] = () => pinST.start + (pinST.end - pinST.start) * 0.86;
segment('#calibre', 'anatB', 'calibre');
segment('#collection', 'calibre', 'coll');
segment('#quote', 'coll', 'quote');
segment('#reserve', 'quote', 'reserve');
segment('#voices', 'reserve', 'voices', { end: 'top 10%' });
// Bracelet wraps and locks while the Maison section scrolls in
const claspST = ScrollTrigger.create({ trigger: '#maison', start: 'top 85%', end: 'top 5%' });
const meter = document.querySelector('.anatomy__meter i');
const KEYS = ['xw', 'y', 'rx', 'ry', 'rz', 'se', 'e'];
const target = { ...STATES.hero };
let tracking = 0;
function trackedY(sel) {
const r = document.querySelector(sel).getBoundingClientRect();
return -(((r.top + r.height / 2) / innerHeight) * 2 - 1); // stage centre as a fraction of half-height
}
function updatePose() {
let seg = null;
for (const sg of SEGS) if (sg.st.scroll() >= sg.st.start) seg = sg;
tracking = 0;
if (seg) {
const k = seg.map(seg.st.progress);
const to = seg.to.track ? { ...seg.to, y: trackedY(seg.to.track) } : seg.to;
for (const key of KEYS) target[key] = seg.from[key] + (to[key] - seg.from[key]) * k;
if (seg.to.track) tracking = k;
} else Object.assign(target, STATES.hero);
target.c = claspST.progress;
const follow = lerp(0.075, 0.35, tracking); // stick to the stage while it scrolls
for (const key of KEYS) S[key] = lerp(S[key], target[key], follow);
S.c = lerp(S.c, target.c, 0.09);
meter.style.transform = `scaleX(${pinST.progress})`;
}
// Warm-white chapter for the configurator
ScrollTrigger.create({
trigger: '#collection', start: 'top 55%', end: 'bottom 45%',
toggleClass: { targets: document.body, className: 'is-light' },
});
let inConfig = false;
let inMaison = false;
ScrollTrigger.create({ trigger: '#maison', start: 'top 70%', end: 'bottom 45%', onToggle: (self) => { inMaison = self.isActive; } });
ScrollTrigger.create({ trigger: '#collection', start: 'top 60%', end: 'bottom 40%', onToggle: (self) => { inConfig = self.isActive; } });
/* ================= Interaction state ================= */
const mouse = { x: 0, y: 0, sx: 0, sy: 0 };
on(window, 'pointermove', (e) => {
mouse.x = (e.clientX / innerWidth) * 2 - 1;
mouse.y = -((e.clientY / innerHeight) * 2 - 1);
});
const drag = { active: false, target: 0, cur: 0, vel: 0, lastX: 0, tilt: 0 };
const stage = document.querySelector('[data-drag]');
on(stage, 'pointerdown', (e) => {
drag.active = true; drag.lastX = e.clientX; drag.vel = 0;
stage.setPointerCapture(e.pointerId);
});
on(stage, 'pointermove', (e) => {
if (!drag.active) return;
const dx = (e.clientX - drag.lastX) * 0.01;
drag.lastX = e.clientX; drag.target += dx; drag.vel = dx;
});
const endDrag = () => { drag.active = false; };
on(stage, 'pointerup', endDrag);
on(stage, 'pointercancel', endDrag);
/* ================= Callouts ================= */
const callouts = [...document.querySelectorAll('.callout')].map((el) => ({ el, side: el.dataset.side, anchor: watch.anchors[el.dataset.part] }));
const v3 = new THREE.Vector3(), center = new THREE.Vector3();
const glowEl = document.querySelector('.glow');
const lockChip = document.querySelector('.lock-chip');
const lockState = lockChip.querySelector('.lock-chip__state');
const kick = { v: 0 };
let wasLocked = false;
/* ================= Render loop ================= */
const clock = new THREE.Clock();
let velocity = 0;
lenis.on('scroll', (l) => { velocity = l.velocity; });
function resize() {
const w = innerWidth, h = innerHeight;
renderer.setSize(w, h); camera.aspect = w / h; camera.updateProjectionMatrix();
pMat.uniforms.uSize.value = 70 * renderer.getPixelRatio();
layoutZones();
}
on(window, 'resize', resize);
function render() {
const t = clock.getElapsedTime();
const aspect = camera.aspect;
const halfH = camera.position.z * Math.tan(THREE.MathUtils.degToRad(camera.fov / 2));
const halfW = halfH * aspect;
const narrow = isNarrow();
// Scroll-driven pose
pivot.position.x = S.xw;
pivot.position.y = (S.y + intro.y) * halfH + (narrow ? 0.48 * halfH * (1 - S.e) * (1 - tracking) : 0) + Math.sin(t * 0.8) * 0.035;
pivot.rotation.set(S.rx, S.ry + intro.spin + spin.v, S.rz);
pivot.scale.setScalar(S.se * intro.s);
// Mouse parallax + drag inertia
mouse.sx = lerp(mouse.sx, mouse.x, 0.06); mouse.sy = lerp(mouse.sy, mouse.y, 0.06);
if (!drag.active) { drag.target += drag.vel; drag.vel *= 0.94; }
if (!inConfig && !drag.active) drag.target = lerp(drag.target, Math.round(drag.target / (Math.PI * 2)) * Math.PI * 2, 0.04);
drag.cur = lerp(drag.cur, drag.target, 0.12);
hover.rotation.x = lerp(hover.rotation.x, -mouse.sy * 0.16, 0.08);
hover.rotation.y = mouse.sx * 0.28 + drag.cur;
hover.rotation.z = kick.v;
mouseLight.position.set(mouse.sx * 4.5, mouse.sy * 3.2, 3.5);
updatePose();
watch.setExplode(S.e);
watch.setClose(S.c);
watch.update(t);
morph.update(t);
// Particles
pMat.uniforms.uTime.value = t;
pMat.uniforms.uVel.value = lerp(pMat.uniforms.uVel.value, pMat.uniforms.uVel.value + velocity * 0.004, 0.5);
const lightMode = document.body.classList.contains('is-light');
pMat.uniforms.uOpacity.value = lerp(pMat.uniforms.uOpacity.value, lightMode ? 0.15 : 0.9 * Math.min(1, intro.s + 0.2), 0.05);
dust.rotation.y = mouse.sx * 0.08; dust.rotation.x = -mouse.sy * 0.05;
if (compiled && !window.__vnCover) renderer.render(scene, camera); // WP: skip under the loader / before compile
// Glow follows the watch
center.setFromMatrixPosition(pivot.matrixWorld).project(camera);
glowEl.style.setProperty('--gx', `${(center.x * 0.5 + 0.5) * 100}%`);
glowEl.style.setProperty('--gy', `${(-center.y * 0.5 + 0.5) * 100}%`);
// Clasp lock moment
const locked = S.c > 0.985;
if (locked !== wasLocked) {
wasLocked = locked;
lockChip.classList.toggle('is-locked', locked);
lockState.textContent = locked ? 'Locked' : 'Unlocked';
if (locked) {
gsap.fromTo(mouseLight, { intensity: 70 }, { intensity: 14, duration: 1.2, ease: 'expo.out' });
gsap.fromTo(kick, { v: 0.07 }, { v: 0, duration: 1.4, ease: 'elastic.out(1, 0.3)' });
}
}
const chipOp = inMaison ? clamp((S.c - 0.55) / 0.3) : 0;
lockChip.style.opacity = chipOp;
if (chipOp > 0) {
watch.anchors.clasp.getWorldPosition(v3).project(camera);
const lx = clamp((v3.x * 0.5 + 0.5) * innerWidth + 24, 0, innerWidth - 230);
const ly = clamp((-v3.y * 0.5 + 0.5) * innerHeight, 90, innerHeight - 80);
lockChip.style.transform = `translate3d(${lx}px, ${ly - 28}px, 0)`;
}
// Project callouts into two tidy columns beside the exploded stack
const op = clamp((S.e - 0.5) / 0.35);
if (op > 0.001) {
const pts = callouts.map((c) => {
c.anchor.getWorldPosition(v3).project(camera);
return { c, x: (v3.x * 0.5 + 0.5) * innerWidth, y: (-v3.y * 0.5 + 0.5) * innerHeight };
});
const narrowView = isNarrow();
const labelW = narrowView ? (innerWidth <= 600 ? 104 : 130) : 250;
['left', 'right'].forEach((side) => {
const list = pts.filter((p) => p.c.side === side).sort((a, b) => a.y - b.y);
if (!list.length) return;
let edge = side === 'right' ? Math.max(...list.map((p) => p.x)) + 60 : Math.min(...list.map((p) => p.x)) - 60;
if (side === 'right') { watch.anchors.crownTip.getWorldPosition(v3).project(camera); edge = Math.max(edge, (v3.x * 0.5 + 0.5) * innerWidth + 40); }
const col = side === 'right' ? Math.min(edge, innerWidth - labelW - 24) : Math.max(edge, labelW + 24);
let prevY = -Infinity;
list.forEach((p) => { p.ty = Math.max(p.y, prevY + (narrowView ? 38 : 52)); prevY = p.ty; });
const overflow = prevY - (innerHeight - 60); // keep the column on screen
if (overflow > 0) list.forEach((p) => { p.ty -= overflow; });
list.forEach((p) => { p.col = col; });
});
pts.forEach((p, i) => {
const { c } = p;
const dx = p.col - p.x, dy = p.ty - p.y;
const local = clamp(op * 1.8 - i * 0.1);
c.el.classList.toggle('is-left', c.side === 'left');
c.el.style.opacity = local;
c.el.style.transform = `translate3d(${p.x}px, ${p.y}px, 0)`;
c.el.style.setProperty('--len', `${Math.hypot(dx, dy)}px`);
c.el.style.setProperty('--ang', `${Math.atan2(dy, dx)}rad`);
c.el.style.setProperty('--dx', `${dx}px`);
c.el.style.setProperty('--dy', `${dy}px`);
c.el.style.setProperty('--draw', local);
});
} else if (callouts[0].el.style.opacity !== '0') {
callouts.forEach((c) => { c.el.style.opacity = 0; });
}
}
tick(render);
/* ================= Text utilities ================= */
function splitChars(el) {
snap(el);
const text = el.textContent; el.textContent = '';
return [...text].map((ch) => {
const s = document.createElement('span'); s.className = 'char'; s.textContent = ch === ' ' ? ' ' : ch;
el.appendChild(s); return s;
});
}
function splitWords(el) {
snap(el);
const out = [];
const walk = (node, parent) => {
[...node.childNodes].forEach((n) => {
if (n.nodeType === 3) {
n.textContent.split(/(\s+)/).forEach((w) => {
if (!w) return;
if (/^\s+$/.test(w)) { parent.appendChild(document.createTextNode(' ')); return; }
const wrap = document.createElement('span'); wrap.className = 'word';
const inner = document.createElement('span'); inner.className = 'word__in'; inner.textContent = w;
wrap.appendChild(inner); parent.appendChild(wrap); out.push(inner);
});
} else {
const clone = n.cloneNode(false); parent.appendChild(clone); walk(n, clone);
}
});
};
const frag = document.createDocumentFragment();
walk(el, frag); el.textContent = ''; el.appendChild(frag);
return out;
}
const heroWord = document.querySelector('[data-split="chars"]');
const heroChars = splitChars(heroWord);
gsap.set(heroChars, { yPercent: 120, rotateX: -95, opacity: 0, scale: 1.35, filter: 'blur(14px)' });
const heroLead = splitWords(document.querySelector('.hero__lead'));
gsap.set(heroLead, { yPercent: 120 });
// Hero title: letters lean away from the cursor, and fan out in 3D as you scroll away
if (!isTouch) {
const movers = heroChars.map((ch) => ({ ch, x: gsap.quickTo(ch, 'x', { duration: 0.9, ease: 'power3' }), y: gsap.quickTo(ch, 'y', { duration: 0.9, ease: 'power3' }) }));
on(document.querySelector('.hero'), 'pointermove', (e) => {
movers.forEach((m) => {
const r = m.ch.getBoundingClientRect();
const dx = r.left + r.width / 2 - e.clientX, dy = r.top + r.height / 2 - e.clientY;
const f = Math.max(0, 1 - Math.hypot(dx, dy) / 420);
m.x(dx * f * 0.14); m.y(dy * f * 0.12);
});
});
on(document.querySelector('.hero'), 'pointerleave', () => movers.forEach((m) => { m.x(0); m.y(0); }));
}
const mid = (heroChars.length - 1) / 2;
gsap.timeline({ scrollTrigger: { trigger: '#hero', start: 'top top', end: 'bottom top', scrub: 0.8 } })
.to(heroChars, { rotationY: (i) => (i - mid) * M.hero_fan, z: (i) => -Math.abs(i - mid) * M.hero_depth, ease: 'none' }, 0)
.to(heroWord, { letterSpacing: '0.06em', yPercent: -35, opacity: 0.08, ease: 'none' }, 0)
.to('.hero__bottom, .hero__top', { y: -80, opacity: 0, ease: 'none' }, 0);
// Section headings: words rise out of a mask with a soft blur
document.querySelectorAll('[data-reveal="words"]').forEach((el) => {
const words = splitWords(el);
gsap.from(words, {
yPercent: 120, rotateX: -70, opacity: 0, filter: 'blur(6px)', transformOrigin: '50% 100%',
duration: 1.4, ease: 'expo.out', stagger: 0.06,
scrollTrigger: { trigger: el, start: 'top 82%' },
onComplete: () => el.querySelectorAll('em').forEach((em) => em.classList.add('is-shining')),
});
});
// Body copy: words fade in from a blur, like ink settling
document.querySelectorAll('.body').forEach((el) => {
el.removeAttribute('data-reveal');
const words = splitWords(el);
gsap.from(words, {
opacity: 0, filter: 'blur(8px)', y: 10, duration: 1.1, ease: 'power3.out', stagger: 0.012,
scrollTrigger: { trigger: el, start: 'top 88%' },
});
});
document.querySelectorAll('[data-reveal="fade"]').forEach((el) => {
gsap.from(el, { y: 40, opacity: 0, duration: 1.4, ease: 'expo.out', scrollTrigger: { trigger: el, start: 'top 88%' } });
});
// Eyebrow rules draw in
gsap.utils.toArray('.eyebrow .num').forEach((n) => {
gsap.from(n, { opacity: 0, x: -14, duration: 1, ease: 'expo.out', scrollTrigger: { trigger: n, start: 'top 90%' } });
});
// Maison cards: fly up out of depth one by one, unmask, trace their gold border,
// draw their icons, then a light sweep passes over each.
const cards = gsap.utils.toArray('.card');
cards.forEach((card) => {
snap(card);
card.insertAdjacentHTML('beforeend',
'<svg class="card__border" preserveAspectRatio="none"><rect x="0.5" y="0.5" rx="6" ry="6" pathLength="1" /></svg><span class="card__sweep"></span>');
const rect = card.querySelector('.card__border rect');
const size = () => { rect.setAttribute('width', card.offsetWidth - 1); rect.setAttribute('height', card.offsetHeight - 1); };
size(); on(window, 'resize', size);
});
gsap.set('.card__line', { scaleX: 0 });
gsap.set('.card__icon path', { strokeDashoffset: 1 });
gsap.set('.card__border rect', { strokeDashoffset: 1 });
gsap.timeline({ scrollTrigger: { trigger: '.cards', start: 'top 85%', toggleActions: 'play none none reverse' } })
.fromTo(cards, {
y: 180, z: -320, rotateX: -65, rotateY: (i) => (i - 1) * -22, opacity: 0, filter: 'blur(14px)',
clipPath: 'inset(45% 0% 45% 0% round 6px)',
}, {
y: 0, z: 0, rotateX: 0, rotateY: 0, opacity: 1, filter: 'blur(0px)', clipPath: 'inset(0% 0% 0% 0% round 6px)',
transformOrigin: '50% 100%', duration: 1.6, stagger: 0.18, ease: 'expo.out', clearProps: 'filter',
})
.to('.card__border rect', { strokeDashoffset: 0, duration: 1.8, stagger: 0.18, ease: 'power2.inOut' }, 0.25)
.to('.card__icon path', { strokeDashoffset: 0, duration: 1.6, stagger: 0.18, ease: 'power2.inOut' }, 0.4)
.to('.card__line', { scaleX: 1, duration: 1.2, stagger: 0.18, ease: 'expo.inOut' }, 0.6)
.from('.card__num, .card__label, .card__desc', { y: 22, opacity: 0, duration: 1, stagger: 0.05, ease: 'expo.out' }, 0.5)
.fromTo('.card__sweep', { xPercent: -120, opacity: 1 }, { xPercent: 260, duration: 1.3, stagger: 0.18, ease: 'power2.inOut' }, 1.1)
.to('.card__border', { opacity: 0.45, duration: 1 }, 2.2);
if (!isTouch) cards.forEach((card) => {
const rx = gsap.quickTo(card, 'rotateX', { duration: 0.6, ease: 'power3' });
const ry = gsap.quickTo(card, 'rotateY', { duration: 0.6, ease: 'power3' });
on(card, 'pointermove', (e) => {
const r = card.getBoundingClientRect();
const px = (e.clientX - r.left) / r.width, py = (e.clientY - r.top) / r.height;
card.style.setProperty('--mx', `${px * 100}%`); card.style.setProperty('--my', `${py * 100}%`);
rx((0.5 - py) * 12); ry((px - 0.5) * 14);
});
on(card, 'pointerleave', () => { rx(0); ry(0); });
});
// Quote: words light up as you scroll
const quoteEl = document.querySelector('[data-split="scrub"]');
snap(quoteEl);
const qWords = quoteEl.textContent.split(' ').map((w) => `<span class="w">${w}</span>`);
quoteEl.innerHTML = qWords.join(' ');
gsap.to(quoteEl.querySelectorAll('.w'), {
opacity: 1, stagger: 0.1, ease: 'none',
scrollTrigger: { trigger: '#quote', start: 'top 70%', end: 'center 40%', scrub: true },
});
// Counters
document.querySelectorAll('[data-count]').forEach((el) => {
const end = +el.dataset.count, plain = el.hasAttribute('data-plain');
const o = { v: plain ? end - 150 : 0 };
const fmt = (v) => (plain ? String(Math.round(v)) : Math.round(v).toLocaleString('en-US'));
el.textContent = fmt(o.v);
gsap.to(o, {
v: end, duration: 2.4, ease: 'expo.out', onUpdate: () => { el.textContent = fmt(o.v); },
scrollTrigger: { trigger: el.closest('.cards, .specs, .stats') || el, start: 'top 85%' },
});
});
// Marquee reacting to scroll velocity
const track = document.querySelector('.marquee__track');
const marquee = gsap.to(track, { xPercent: -50, duration: M.marquee_duration, ease: 'none', repeat: -1 });
lenis.on('scroll', (l) => {
const boost = 1 + Math.min(6, Math.abs(l.velocity) * 0.25);
gsap.to(marquee, { timeScale: l.velocity < 0 ? -boost : boost, duration: 0.3, overwrite: true });
});
ScrollTrigger.create({
trigger: '.marquee', start: 'top bottom', end: 'bottom top',
onUpdate: (self) => gsap.set(track, { x: -self.progress * 200 }),
});
// Nav: highlight the link for the section in view
document.querySelectorAll('.nav__links a').forEach((link) => {
const id = link.getAttribute('href');
const sec = document.querySelector(id);
if (!sec) return;
const range = id === '#anatomie' // the pinned section stays on screen for the whole pin
? { start: () => pinST.start - innerHeight / 2, end: () => pinST.end + innerHeight / 2 }
: { trigger: sec, start: 'top 50%', end: 'bottom 50%' };
ScrollTrigger.create({ ...range, onToggle: (self) => link.classList.toggle('is-current', self.isActive) });
});
// Nav hide/show + progress bar
const nav = document.querySelector('.nav');
ScrollTrigger.create({
start: 0, end: 'max',
onUpdate: (self) => nav.classList.toggle('is-scrolled', self.scroll() > 80), // always visible; compacts once you scroll
});
gsap.to('.progress i', { scaleX: 1, ease: 'none', scrollTrigger: { start: 0, end: 'max', scrub: true } });
// Live clock
const clockEl = document.getElementById('clock');
const tickClock = () => { clockEl.textContent = new Date().toLocaleTimeString('en-GB', { hour12: false }); };
tickClock(); every(tickClock, 1000);
/* ================= Configurator ================= */
const priceEl = document.getElementById('price');
const priceObj = { v: 48500 };
function flipText(el, text) {
gsap.timeline()
.to(el, { yPercent: -60, opacity: 0, duration: 0.3, ease: 'power2.in' })
.add(() => { el.textContent = text; })
.fromTo(el, { yPercent: 60, opacity: 0 }, { yPercent: 0, opacity: 1, duration: 0.6, ease: 'expo.out' });
}
// One entry point for changing the watch, shared by the swatches and the testimonials.
const caseBtns = [...document.querySelectorAll('.swatch[data-case]')];
const dialBtns = [...document.querySelectorAll('.swatch[data-dial]')];
const config = { case: 'yellow', dial: 'noir' };
const nameOf = (btns, key, attr) => btns.find((x) => x.dataset[attr] === key).dataset.name;
function applyConfig(next, speed = 1) {
const c = next.case || config.case, d = next.dial || config.dial;
if (c === config.case && d === config.dial) return;
const caseChanged = c !== config.case;
caseBtns.forEach((x) => x.classList.toggle('is-active', x.dataset.case === c));
dialBtns.forEach((x) => x.classList.toggle('is-active', x.dataset.dial === d));
if (caseChanged) {
flipText(document.getElementById('caseName'), nameOf(caseBtns, c, 'case'));
const price = +caseBtns.find((x) => x.dataset.case === c).dataset.price;
gsap.to(priceObj, { v: price, duration: 1.4, ease: 'expo.out', onUpdate: () => { priceEl.textContent = Math.round(priceObj.v).toLocaleString('en-US'); } });
}
if (d !== config.dial) flipText(document.getElementById('dialName'), nameOf(dialBtns, d, 'dial'));
flipText(document.getElementById('voiceConfig'), `${nameOf(caseBtns, c, 'case')} · ${nameOf(dialBtns, d, 'dial')}`);
morph.play(() => { watch.setCase(c); watch.setDial(d); }, caseChanged ? CASES[c].color : DIALS[d].light).timeScale(speed);
config.case = c; config.dial = d;
}
caseBtns.forEach((b) => on(b, 'click', () => applyConfig({ case: b.dataset.case })));
dialBtns.forEach((b) => on(b, 'click', () => applyConfig({ dial: b.dataset.dial })));
/* ================= Collectors (testimonial slider) ================= */
// Each testimonial carries the collector's configuration; changing slide re-forms the
// watch from gold dust into their piece.
const voices = [...document.querySelectorAll('.voice')];
const voiceWords = voices.map((v) => splitWords(v.querySelector('.voice__quote')));
const voiceIdx = document.getElementById('voiceIdx');
const voiceBar = document.querySelector('.voices__bar i');
let vCur = 0, vTarget = 0, vBusy = false, inVoices = false; // vTarget updates on click, vCur once the text swaps
const autoplay = gsap.fromTo(voiceBar, { scaleX: 0 }, { scaleX: 1, duration: M.autoplay_seconds, ease: 'none', paused: true, onComplete: () => goVoice(vCur + 1) });
function goVoice(n) {
if (vBusy) return;
const next = (n + voices.length) % voices.length;
if (next === vCur) return;
vBusy = true; vTarget = next;
const from = voices[vCur], to = voices[next];
applyConfig({ case: to.dataset.case, dial: to.dataset.dial }, 1.45); // watch and text change together
gsap.timeline({ onComplete: () => { vBusy = false; autoplay.restart(); if (!inVoices) autoplay.pause(); } })
.to(voiceWords[vCur], { yPercent: -110, opacity: 0, filter: 'blur(6px)', duration: 0.5, stagger: 0.008, ease: 'power3.in' })
.to(from.querySelector('.voice__by'), { y: -14, opacity: 0, duration: 0.4, ease: 'power2.in' }, '<0.1')
.add(() => {
from.classList.remove('is-active'); to.classList.add('is-active');
gsap.set(from.querySelector('.voice__by'), { clearProps: 'all' });
gsap.set(voiceWords[vCur], { clearProps: 'all' });
voiceIdx.textContent = String(next + 1).padStart(2, '0');
vCur = next;
})
.fromTo(voiceWords[next], { yPercent: 110, opacity: 0, filter: 'blur(6px)' }, { yPercent: 0, opacity: 1, filter: 'blur(0px)', duration: 1, stagger: 0.018, ease: 'expo.out' })
.fromTo(to.querySelector('.voice__by'), { y: 18, opacity: 0 }, { y: 0, opacity: 1, duration: 0.9, ease: 'expo.out' }, '<0.25')
.fromTo(to.querySelector('.voice__mono'), { scale: 0.4, rotate: -90 }, { scale: 1, rotate: 0, duration: 1, ease: 'back.out(2)' }, '<');
autoplay.pause();
}
document.querySelectorAll('.voices__btn').forEach((b) => on(b, 'click', () => goVoice(vCur + +b.dataset.dir)));
ScrollTrigger.create({
trigger: '#voices', start: 'top 45%', end: 'bottom 55%',
onToggle: (self) => {
inVoices = self.isActive;
if (inVoices) {
const v = voices[vTarget];
applyConfig({ case: v.dataset.case, dial: v.dataset.dial }); // show this collector's piece
if (!vBusy) autoplay.play();
} else autoplay.pause();
},
});
const voicesPanel = document.querySelector('.voices__panel');
on(voicesPanel, 'pointerenter', () => autoplay.pause());
on(voicesPanel, 'pointerleave', () => { if (inVoices && !vBusy) autoplay.play(); });
gsap.from('.voices__ring', { scale: 0.6, opacity: 0, duration: 1.8, ease: 'expo.out', scrollTrigger: { trigger: '.voices__stage', start: 'top 75%' } });
gsap.from(['.voices__slides', '.voices__nav'], { y: 40, opacity: 0, duration: 1.3, stagger: 0.12, ease: 'expo.out', scrollTrigger: { trigger: '.voices__slides', start: 'top 85%' } });
// Footer: wordmark letters rise out of the bottom edge, columns follow
const brandChars = splitChars(document.querySelector('[data-split="brand"]'));
gsap.from(brandChars, {
yPercent: 100, duration: 1.6, ease: 'expo.out', stagger: { each: 0.07, from: 'center' },
scrollTrigger: { trigger: '.footer__brand', start: 'top 98%' },
});
gsap.from('.footer__col', { y: 40, opacity: 0, duration: 1.2, stagger: 0.1, ease: 'expo.out', scrollTrigger: { trigger: '.footer__grid', start: 'top 88%' } });
// Form
const form = document.querySelector('.reserve__form');
const msg = document.querySelector('.reserve__msg');
on(form, 'submit', (e) => {
e.preventDefault();
const email = form.querySelector('input');
if (!email.checkValidity() || !email.value) { msg.textContent = 'Please enter a valid email address.'; gsap.fromTo(form, { x: -8 }, { x: 0, duration: 0.6, ease: 'elastic.out(1, 0.3)' }); return; }
msg.textContent = 'Thank you. A Verance concierge will be in touch within 24 hours.';
gsap.from(msg, { y: 12, opacity: 0, duration: 0.8, ease: 'expo.out' });
form.reset();
});
/* ================= Cursor & magnetic ================= */
if (!isTouch && !env.isEditor) {
const cursor = document.querySelector('.cursor');
const dot = cursor.querySelector('.cursor__dot'), ring = cursor.querySelector('.cursor__ring');
const dx = gsap.quickTo(dot, 'x', { duration: 0.1 }), dy = gsap.quickTo(dot, 'y', { duration: 0.1 });
const rx = gsap.quickTo(ring, 'x', { duration: 0.55, ease: 'power3' }), ry = gsap.quickTo(ring, 'y', { duration: 0.55, ease: 'power3' });
on(window, 'pointermove', (e) => { dx(e.clientX); dy(e.clientY); rx(e.clientX); ry(e.clientY); });
document.querySelectorAll('[data-hover]').forEach((el) => {
on(el, 'pointerenter', () => cursor.classList.add('is-hover'));
on(el, 'pointerleave', () => cursor.classList.remove('is-hover'));
});
on(stage, 'pointerenter', () => cursor.classList.add('is-drag'));
on(stage, 'pointerleave', () => cursor.classList.remove('is-drag'));
document.querySelectorAll('.magnetic').forEach((el) => {
on(el, 'pointermove', (e) => {
const r = el.getBoundingClientRect();
gsap.to(el, { x: (e.clientX - r.left - r.width / 2) * 0.35, y: (e.clientY - r.top - r.height / 2) * 0.45, duration: 0.6, ease: 'power3.out' });
});
on(el, 'pointerleave', () => gsap.to(el, { x: 0, y: 0, duration: 1, ease: 'elastic.out(1, 0.35)' }));
});
}
/* ================= Intro ================= */
const revealPage = () => gsap.timeline()
.to(intro, { s: 1, spin: 0, y: 0, duration: M.intro_duration, ease: 'expo.out' }, 0.1)
.to(heroChars, { yPercent: 0, rotateX: 0, opacity: 1, scale: 1, filter: 'blur(0px)', duration: 2, stagger: { each: 0.08, from: 'center' }, ease: 'expo.out' }, 0.45)
.add(() => heroWord.classList.add('is-lit'), 1.6)
.to(heroLead, { yPercent: 0, duration: 1.4, stagger: 0.08, ease: 'expo.out' }, 1.0)
.from(['.nav', '.hero__eyebrow', '.hero__intro p', '.scroll-cue', '.hero__time'], { y: 30, opacity: 0, duration: 1.4, stagger: 0.1, ease: 'expo.out', clearProps: 'transform' }, 0.9)
.add(() => {
document.body.classList.remove('is-loading');
lenis.start();
ScrollTrigger.refresh();
}, 1.3);
// WP: compile the shaders without blocking the loader (KHR_parallel_shader_compile), then start
const startIntro = () => {
if (env.gen !== state.gen) return;
compiled = true;
render();
if (env.playIntro && window.__loader) window.__loader.finish(() => veTrack.ctx.add(revealPage));
else veTrack.ctx.add(revealInstant);
};
(renderer.compileAsync ? renderer.compileAsync(scene, camera) : Promise.resolve(renderer.compile(scene, camera))).then(startIntro, startIntro);
function revealInstant() { // WP: editor rebuilds settle straight to the post-intro state
intro.s = 1; intro.spin = 0; intro.y = 0;
gsap.set(heroChars, { yPercent: 0, rotateX: 0, opacity: 1, scale: 1, filter: 'blur(0px)' });
heroWord.classList.add('is-lit');
gsap.set(heroLead, { yPercent: 0 });
document.body.classList.remove('is-loading');
lenis.start();
ScrollTrigger.refresh();
}
veTrack.three = { renderer, scene, pmrem, canvas };
veTrack.lenis = lenis;
});
return veTrack;
}
/* ---------------- lifecycle ---------------- */
function makeTrack() { return { listeners: [], intervals: [], tickers: [], stEvents: [], snaps: [], ctx: null, three: null, lenis: null }; }
function disposeObject(root) {
root.traverse(function (o) {
if (o.geometry) o.geometry.dispose();
[].concat(o.material || []).forEach(function (m) {
Object.keys(m).forEach(function (k) { if (m[k] && m[k].isTexture) m[k].dispose(); });
m.dispose();
});
});
}
function teardown(t) {
if (!t) return;
t.tickers.forEach(function (fn) { gsap.ticker.remove(fn); });
t.listeners.forEach(function (l) { l[0].removeEventListener(l[1], l[2], l[3]); });
t.intervals.forEach(clearInterval);
t.stEvents.forEach(function (e) { ScrollTrigger.removeEventListener(e[0], e[1]); });
try { if (t.ctx) t.ctx.revert(); } catch (e) { console.warn('[Verance] revert', e); }
try { if (t.lenis && t.lenis.destroy) t.lenis.destroy(); } catch (e) {}
for (var i = t.snaps.length - 1; i >= 0; i--) t.snaps[i][0].innerHTML = t.snaps[i][1];
document.body.classList.remove('is-loading', 'is-light', 'menu-open');
document.documentElement.classList.remove('lenis', 'lenis-smooth', 'lenis-stopped', 'lenis-scrolling');
if (t.three) {
var th = t.three;
disposeObject(th.scene);
if (th.scene.environment) th.scene.environment.dispose();
th.pmrem.dispose();
th.renderer.dispose();
th.renderer.forceContextLoss(); // dispose() alone does not return the context
th.canvas.replaceWith(th.canvas.cloneNode(false)); // a lost context cannot be revived on the same canvas
}
}
function missing() { return REQUIRED.filter(function (s) { return !document.querySelector(s); }); }
function failsafe() { // never leave a visitor behind a loader that will not lift
window.__vnCover = null;
var l = document.querySelector('.verance-nocturne .loader');
if (l) l.style.display = 'none';
document.body.classList.remove('is-loading');
}
function rebuild() {
var gen = ++state.gen;
teardown(state.current); state.current = null;
var gone = missing();
if (gone.length) { console.info('[Verance] engine off, missing:', gone.join(', ')); failsafe(); return; }
var t = makeTrack();
var V = window.VERANCE_THREE;
var W = createWatchModule(V.THREE, V.RoundedBoxGeometry);
var env = {
gen: gen, isEditor: isEditor, playIntro: !state.booted && !isEditor,
M: Object.assign({}, DEFAULTS, window.VERANCE_MOTION || {}),
THREE: V.THREE, RoomEnvironment: V.RoomEnvironment,
buildWatch: W.buildWatch, CASES: W.CASES, DIALS: W.DIALS,
createMorph: createMorphModule(V.THREE, V.MeshSurfaceSampler).createMorph
};
veranceMain(env, t).then(function (res) {
if (gen !== state.gen) { teardown(t); return; }
if (res) {
state.current = t; state.booted = true;
// Elementor re-rendered a section while the engine was booting: bind to the live DOM.
if (state.dirty) { state.dirty = false; requestRefresh(); }
}
}).catch(function (err) {
console.error('[Verance]', err); teardown(t); failsafe();
});
}
// Asked for by a widget's stub when the Composer re-runs it, or by the DOM watcher when
// Elementor re-renders a section. Only meaningful in the editor: on the live page the
// stubs run once, before boot, and their values are read when the engine starts.
function requestRefresh() {
if (!isEditor) return;
if (!state.booted) { state.dirty = true; return; }
if (edLoader.busy) { afterLoader(requestRefresh); return; } // never rebuild the 3D scene under a playing loader
clearTimeout(state.timer);
state.timer = setTimeout(rebuild, 300);
}
/* Editor loader. It plays on whichever loader element is on the canvas, on its own, apart from
the engine's rebuilds: Elementor re-renders widgets after the editor opens and on every edit of
the Header & Loader widget, and each render puts a fresh loader there. Each one plays once and
lifts; one that Elementor removes mid-play is stopped. */
var edLoader = { el: null, api: null, busy: false, queue: [] };
function afterLoader(fn) {
if (!edLoader.busy) { fn(); return; }
if (edLoader.queue.indexOf(fn) < 0) edLoader.queue.push(fn);
}
function playEditorLoader() {
if (!window.gsap) return;
var el = document.querySelector('.verance-nocturne .loader');
if (!el || el.__vnPlayed) return;
el.__vnPlayed = true;
if (edLoader.api) edLoader.api.stop();
var done = false;
var lift = function () {
if (done) return;
done = true;
el.style.display = 'none';
api.stop();
if (edLoader.el !== el) return;
edLoader.busy = false; edLoader.api = null;
var q = edLoader.queue; edLoader.queue = [];
q.forEach(function (fn) { fn(); });
};
var api = startLoader(el);
edLoader.el = el; edLoader.api = api; edLoader.busy = true;
api.finish(null, lift);
setTimeout(lift, 7000); // never leave the canvas covered
}
/* ---------------- boot ---------------- */
function loadScript(src, present) {
return new Promise(function (resolve, reject) {
if (present()) return resolve();
var s = document.createElement('script');
s.src = src; s.async = false;
s.onload = function () { present() ? resolve() : reject(new Error('loaded but not initialised: ' + src)); };
s.onerror = function () { reject(new Error('failed to load ' + src)); };
document.head.appendChild(s);
});
}
function watchEditorDom() {
new MutationObserver(function (muts) {
for (var i = 0; i < muts.length; i++) {
var nodes = [].concat([].slice.call(muts[i].addedNodes), [].slice.call(muts[i].removedNodes));
for (var j = 0; j < nodes.length; j++) {
var n = nodes[j];
if (n.nodeType === 1 && (n.matches('.verance-nocturne') || n.querySelector('.verance-nocturne'))) {
if (!started) { if (!missing().length) start(); return; }
playEditorLoader(); // a re-rendered Header & Loader widget brings a fresh loader
requestRefresh();
return;
}
}
}
}).observe(document.body, { childList: true, subtree: true });
}
var started = false;
function start() {
if (started) return;
started = true;
// The page code links the stylesheet; if a context drops it (e.g. the editor), link it from here.
if (!document.querySelector('link[href*="/uichemy-composer/verance-app.css"]')) {
var css = document.createElement('link');
css.rel = 'stylesheet'; css.href = LIB.css;
document.head.appendChild(css);
}
if (isEditor) document.documentElement.classList.add('verance-edit');
else { // the source does this at module start: a first visit always begins at the top, behind the loader
if ('scrollRestoration' in history) history.scrollRestoration = 'manual';
window.scrollTo(0, 0);
document.body.classList.add('is-loading');
}
setTimeout(function () { if (!state.booted) { console.warn('[Verance] boot timed out'); failsafe(); } }, 20000);
var core = loadScript(LIB.gsap, function () { return !!window.gsap; })
.then(function () { return loadScript(LIB.st, function () { return !!window.ScrollTrigger; }); })
.then(function () {
gsap.registerPlugin(ScrollTrigger);
if (isEditor) playEditorLoader();
else if (document.querySelector('.verance-nocturne .loader')) startLoader(); // the loader needs only GSAP
});
var three = loadScript(LIB.three, function () { return !!window.VERANCE_THREE; });
var lenis = isEditor ? Promise.resolve() : loadScript(LIB.lenis, function () { return !!window.Lenis; });
Promise.all([core, three, lenis])
.then(function () { if (isEditor) afterLoader(rebuild); else rebuild(); }) // editor: build the scene once the loader lifts
.catch(function (err) { console.error('[Verance]', err); failsafe(); });
}
window.VeranceEngine = { requestRefresh: requestRefresh, rebuild: rebuild, version: 1 };
if (isEditor) watchEditorDom();
if (document.querySelector('.verance-nocturne')) start();
})();
```
## B6 · Library code files (built in B8, not reproduced here)
- `verance-three.js` — three.js r169 IIFE, 686,981 bytes (B8 reproduces it byte-for-byte), `THREE.REVISION === "169"`.
- `verance-lenis.js` — Lenis 1.1.13, 13,673 bytes.
- `uichemy-gsap.js`, `uichemy-scrolltrigger.js` — GSAP / ScrollTrigger 3.12.5, already on the site (`code-file list` to confirm).
## B7 · The exact call sequence
1. `uichemy-composer/code-file` `request-upload` → **one** slot, then upload in a single shell loop (bytes disk→disk, never read into context):
```bash
for f in verance-app.css verance-app.js verance-boot.js verance-lenis.js verance-three.js; do
curl -s -X PUT -H 'Uich-Token: <token>' --data-binary @"$f" \
'<slot_url>'"&filename=$f"
done
```
Verify each: `curl -s "https://<site>/wp-content/uichemy-composer/$f?v=<n>" | cmp - $f`.
2. The fonts must already be in the media library (manifest). On another site, see B8.
3. `uichemy-composer/page` `create` the page with the first section (B2.1), `title: "Verance Nocturne"`, `status: "draft"`. Or `append-section` to an existing page.
4. `append-section` × 10 for B2.2 … B2.11, **one call at a time**, `upload_images: false`. After any timeout, `get-structure` before retrying.
5. `get-structure` → 11 `uichemy-composer` widgets in order; `grep` `requestRefresh` (field `js`) → 11 matches.
6. `set-page-code` with B1.
7. `update` → `status: "publish"`, `excerpt: "Verance Nocturne: a scroll-built 3D luxury watch experience with an exploded-view anatomy, live configurator and gold-dust morph."`.
8. Verify on the published URL: 11 `.verance-nocturne` roots; every id and class present; one canvas; `VERANCE_THREE.THREE.REVISION === "169"`; `gsap.version === "3.12.5"`; 59 ScrollTriggers; loader lifts; no console errors; at 375px no horizontal scroll.
9. Verify the editor path: open the page URL with `?elementor-preview=<id>` → `html.verance-edit`, loader plays and lifts, no custom cursor, native scroll, pinned Anatomie works. Then open the real Elementor editor.
## B8 · Rebuilding the hosted assets on another site
```bash
mkdir vendor && cd vendor
npm init -y >/dev/null
npm i [email protected] [email protected] esbuild
cat > entry.mjs <<'EOF'
import * as THREE from './node_modules/three/build/three.module.js';
import { RoomEnvironment } from './node_modules/three/examples/jsm/environments/RoomEnvironment.js';
import { RoundedBoxGeometry } from './node_modules/three/examples/jsm/geometries/RoundedBoxGeometry.js';
import { MeshSurfaceSampler } from './node_modules/three/examples/jsm/math/MeshSurfaceSampler.js';
export { THREE, RoomEnvironment, RoundedBoxGeometry, MeshSurfaceSampler };
EOF
npx esbuild entry.mjs --bundle --format=iife --global-name=VERANCE_THREE --minify --legal-comments=none --target=es2019 --outfile=verance-three.js
{ echo '/* Lenis 1.1.13 (the version the source pins). Wrapped so an AMD/CommonJS loader on the page cannot divert it from window.Lenis. */'
echo '(function (define, exports, module) {'
cat node_modules/lenis/dist/lenis.min.js
echo; echo '}).call(window);'; } > verance-lenis.js
```
Fonts: fetch `https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;1,300;1,400;1,500&family=Jost:wght@300;400;500&display=swap` with a modern browser User-Agent. Take the `/* latin */` block's `url(...)` for each family and style (one variable woff2 each), and save them as `verance-cormorant-garamond.woff2`, `verance-cormorant-garamond-italic.woff2` and `verance-jost.woff2`. Upload them with `uichemy-composer/media` `request-upload`, then replace the three font URLs at the top of B3 with the returned URLs.
If the site does not already have GSAP 3.12.5, upload `gsap.min.js` and `ScrollTrigger.min.js` from `[email protected]` as `uichemy-gsap.js` and `uichemy-scrolltrigger.js`; B5's `LIB` expects those names.