Juno Vale Low Tide Radio AI website prompt
An alt-pop band site in six sections: a pinned hero that wipes away in vertical strips, a three-panel scroll statement, and a working 30-second preview player.
Now put it in WordPress
You have a site. Now make it yours.
An AI build hands you a front end and stops there. No CMS, no client login, no real ownership: you cannot hand it over, and you cannot change it without going back and re-prompting. UiChemy turns that build into native, editable WordPress you own, in the builder you already use.
Bolt to WordPressClaude to WordPressCodex to WordPressCursor to WordPressLovable to WordPressv0 to WordPressAI to WordPress
Get Started
More prompts
Showing 6 prompts
Frequently asked questions
What does the Juno Vale Low Tide Radio prompt build?
An alt-pop band site in six sections: a pinned hero that wipes away in vertical strips, a three-panel scroll statement, and a working 30-second preview player.
Which AI tools does the Juno Vale Low Tide Radio prompt work with?
It's written for Claude. Paste the whole prompt into Claude and it builds the complete site for you, then bring the result into WordPress with UiChemy. This prompt also has a WordPress version: with the UiChemy MCP connected, Claude builds the page straight into WordPress.
How do I turn the result into a WordPress site?
After the AI builds your site, UiChemy Composer rebuilds it as native, editable WordPress in Elementor or Gutenberg (Bricks coming soon) — you own it and edit it in your builder. See AI to WordPress.
Is the Juno Vale Low Tide Radio 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
Juno Vale Low Tide Radio · Section prompt
# MASTER PROMPT — Juno Vale — Low Tide Radio
### Plain HTML, CSS and JavaScript. No WordPress, no page builder, no framework, no build step.
## ▶ HOW TO USE THIS PROMPT
**Paste this whole document into Claude. Nothing to attach.** It carries the complete source of the site plus the address of every image, so Claude can assemble a folder that owns its own assets and hand it back as a zip.
**INSTRUCTION TO CLAUDE — BUILD IT, DO NOT ASK.** Every decision is already made in this document. Do not stop to ask the user questions, do not offer options, and do not ask for confirmation before starting. Write the files, zip them, hand over the archive, then report what you built.
1. Create a folder named `juno-vale` and write out every file in the appendix below **exactly as written** — do not reformat, rename, shorten, reorder or "improve" anything.
2. **Download the 7 images into `assets/`.** The code already points at `assets/…` — nothing in it references a remote host. Run this from inside the folder; it reads the ASSET MANIFEST at the end of this document:
```bash
mkdir -p assets
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/jv-crowd.jpg' -o 'assets/crowd.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/jv-hero.jpg' -o 'assets/hero.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/jv-live.jpg' -o 'assets/live.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/jv-sea.jpg' -o 'assets/sea.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/jv-studio.jpg' -o 'assets/studio.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/jv-tee.jpg' -o 'assets/tee.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/jv-vinyl.jpg' -o 'assets/vinyl.jpg'
```
Then check every file arrived and none is a zero-byte or HTML error page:
```bash
ls assets | wc -l # expect 7 files
find assets -size -1k # expect no output
```
If any download fails, retry it — hosts occasionally drop a request. Do **not** fall back to hotlinking the remote URL: the whole point is a folder that works offline.
3. **Zip that folder and give the user the archive.** Its contents:
```
juno-vale/
index.html
assets/
crowd.jpg
hero.jpg
live.jpg
sea.jpg
… 3 more
```
```bash
zip -r juno-vale.zip juno-vale -x '*.DS_Store'
```
Then hand over `juno-vale.zip` as a file the user can download, not as text in the chat.
4. Before handing it over, serve the folder and confirm it runs:
```bash
cd juno-vale && python3 -m http.server 8000 # then open http://localhost:8000
```
Check the page renders, the images load from `assets/` with the network throttled or offline, and scrolling drives the animations.
The finished folder needs no network for its images — they are in `assets/`; only the motion libraries load from a CDN.
Do not rebuild the design from the description below. The description is there so a person can understand and edit the code. The appendix is the deliverable.
---
## ▶ WHAT YOU GET
Juno Vale is an alt-pop singer-songwriter. New album Low Tide Radio out now — stream it, catch the 2026–27 tour, grab the merch.
**1 code file plus an `assets/` folder holding 7 images, delivered as `juno-vale.zip`.**
| File | What it is |
|---|---|
| `index.html` | a page, 138 KB |
| `assets/` | 7 images, downloaded at build time |
**Libraries, from a CDN, already referenced in the HTML:** `gsap.min.js`, `ScrollTrigger.min.js`, `lenis.min.js`. No npm install, no bundler, no framework.
**Sections (6):** `#top` · `#record` · `#album` · `#tour` · `#merch` · `#join`.
---
## ▶ WHERE THE MEDIA COMES FROM
All 7 files (7 images) live in `assets/` inside the folder, and every reference in the markup, the CSS and the script is a relative `assets/…` path. Once the folder is assembled it owns its own media: no CDN, no media library, no external image host. Open it on a laptop with the wifi off and it still renders.
They are fetched **once, at build time**, from the WordPress media library on `demo.uichemy.com` — the same `juno-vale__` files the WordPress build of this page uses. That is only how they travel into the folder; nothing in the delivered code points back at that host. Replacing an image means dropping a new file into `assets/` under the same name.
The manifest at the end of this document lists every source URL against the filename it is saved as, so the download step is reproducible and checkable.
---
## ▶ WHAT THIS PROMPT DELIBERATELY DOES NOT CONTAIN
- No WordPress, no page builder, and none of the wrapper classes or compatibility CSS a builder needs. The WordPress build is a separate prompt.
- No framework, no TypeScript, no bundler, no `package.json`.
- No backend. Any form here validates and replies in place; it posts nowhere.
- No CMS. The content lives in the files.
- No external image host. The images ship inside the zip; nothing in the code calls out to a media library at runtime.
---
## ASSET MANIFEST — download these into `assets/`
Source is the media library on `demo.uichemy.com`. Save each one under the name in the second column; the code already refers to it by that name.
| Download from | Save as |
|---|---|
| `https://demo.uichemy.com/wp-content/uploads/2026/09/jv-crowd.jpg` | `assets/crowd.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/jv-hero.jpg` | `assets/hero.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/jv-live.jpg` | `assets/live.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/jv-sea.jpg` | `assets/sea.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/jv-studio.jpg` | `assets/studio.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/jv-tee.jpg` | `assets/tee.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/jv-vinyl.jpg` | `assets/vinyl.jpg` |
---
# APPENDIX · VERBATIM SOURCE
The complete source of every file. **Write them out exactly** — do not change a value, a comment, a class name, or the order of a declaration. Recreate this file layout:
```
index.html
```
## `index.html`
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
<title>Juno Vale — Low Tide Radio</title>
<meta name="description" content="Juno Vale is an alt-pop singer-songwriter. New album Low Tide Radio out now — stream it, catch the 2026–27 tour, grab the merch.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@500;700;900&family=Manrope:wght@400;500;700;800&display=swap" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/lenis.min.js"></script>
<style>
/* ---------- JUNO VALE locked palette ---------- */
:root{
--bg:#EEF0FB; /* periwinkle-mist page */
--paper:#FFFFFF; /* cards */
--cobalt:#2438E8; /* primary (white text OK) */
--cobalt-deep:#1A29B8;/* primary hover */
--cobalt-soft:#D9DEFC;/* primary tint */
--mint:#3FE0B5; /* accent: tidal mint (ink text only, never white) */
--mint-deep:#22C499; /* accent hover */
--mint-soft:#D4F8EE;
--ink:#16123A; /* text only, never a background */
--mute:#5B5F86; /* secondary text */
--line:#CFD4F2; /* borders */
--shadow:0 28px 56px -30px rgba(36,56,232,.55);
--display:'Unbounded', system-ui, sans-serif;
--body:'Manrope', system-ui, sans-serif;
--gutter:clamp(16px, 4vw, 64px);
--ease:cubic-bezier(.2,.7,.1,1);
}
*,*::before,*::after{box-sizing:border-box}
html{scroll-behavior:smooth}
html.lenis,html.lenis body{height:auto}
.lenis.lenis-smooth{scroll-behavior:auto !important}
.lenis.lenis-stopped{overflow:hidden}
body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--body);font-size:16px;line-height:1.6;overflow-x:hidden;-webkit-font-smoothing:antialiased}
img{display:block;max-width:100%}
a{color:inherit;text-decoration:none}
button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
/* ---------- shared atoms (use these in every section) ---------- */
.wrap{max-width:1360px;margin-inline:auto;padding-inline:var(--gutter)}
.sec{padding-block:clamp(80px, 11vw, 150px)}
.eyebrow{font-size:12px;font-weight:800;letter-spacing:.2em;text-transform:uppercase;color:var(--cobalt)}
.display{font-family:var(--display);font-weight:900;line-height:.9;letter-spacing:-.04em;margin:0;text-transform:uppercase}
.display .hl{color:var(--cobalt)}
.lede{font-size:clamp(16px,1.3vw,19px);color:var(--mute);max-width:52ch}
.btn{display:inline-flex;align-items:center;gap:.6em;padding:14px 26px;border-radius:999px;font-weight:800;font-size:15px;
background:var(--cobalt);color:#fff;transition:background .25s var(--ease),transform .25s var(--ease)}
.btn:hover{background:var(--cobalt-deep);transform:translateY(-2px)}
.btn--mint{background:var(--mint);color:var(--ink)}
.btn--mint:hover{background:var(--mint-deep)}
.btn--ghost{background:transparent;color:var(--ink);box-shadow:inset 0 0 0 1.5px var(--ink)}
.btn--ghost:hover{background:var(--ink);color:#fff}
.card{background:var(--paper);border-radius:22px;box-shadow:var(--shadow)}
/* animated equaliser bars: <span class="eq"><i></i><i></i><i></i><i></i></span> */
.eq{display:inline-flex;align-items:flex-end;gap:3px;height:1em}
.eq i{width:3px;height:100%;background:currentColor;border-radius:2px;transform-origin:bottom;animation:eq 1s ease-in-out infinite}
.eq i:nth-child(2){animation-delay:-.3s}.eq i:nth-child(3){animation-delay:-.6s}.eq i:nth-child(4){animation-delay:-.15s}
@keyframes eq{0%,100%{transform:scaleY(.25)}50%{transform:scaleY(1)}}
/* sliced button hover: JV.sliceButtons() injects .jv-fx with 6 <i> slices that grow from the centre out */
.btn{position:relative;isolation:isolate;overflow:hidden}
.jv-fx{position:absolute;inset:0;display:flex;z-index:-1;pointer-events:none}
.jv-fx i{flex:1;height:100%;background:var(--jv-fx,var(--cobalt-deep));transform:scaleY(0);transition:transform .45s var(--ease)}
.btn:hover .jv-fx i{transform:scaleY(1)}
.jv-fx i:nth-child(3),.jv-fx i:nth-child(4){transition-delay:0s}
.jv-fx i:nth-child(2),.jv-fx i:nth-child(5){transition-delay:.05s}
.jv-fx i:nth-child(1),.jv-fx i:nth-child(6){transition-delay:.1s}
.btn:hover{background:var(--cobalt)}
.btn--mint{--jv-fx:var(--mint-deep)} .btn--mint:hover{background:var(--mint)}
.btn--ghost{--jv-fx:var(--ink)} .btn--ghost:hover{background:transparent}
[data-reveal]{opacity:0;transform:translateY(40px)}
@media (prefers-reduced-motion: reduce) and (update: none){
[data-reveal]{opacity:1;transform:none}
.eq i{animation:none;transform:scaleY(.6)}
.jv-fx{display:none}
}
</style>
</head>
<body>
<script>
/* ---------- shared motion helpers (window.JV) ---------- */
gsap.registerPlugin(ScrollTrigger);
window.JV = (function(){
const reduce = matchMedia('(prefers-reduced-motion: reduce) and (update: none)').matches;
let lenis = null;
if(!reduce && window.Lenis){
lenis = new Lenis({lerp:.1, smoothWheel:true});
lenis.on('scroll', ScrollTrigger.update);
gsap.ticker.add(t => lenis.raf(t*1000));
gsap.ticker.lagSmoothing(0);
document.addEventListener('click', e => {
const a = e.target.closest('a[href^="#"]'); if(!a) return;
const id = a.getAttribute('href'); if(id.length < 2) return;
const el = document.querySelector(id); if(!el) return;
e.preventDefault(); lenis.scrollTo(el, {offset:0, duration:1.4});
});
}
/* Sliced reveal: the element is cut into `slices` horizontal bands; each band's width
grows from 0 out of the horizontal centre, middle band first. The `corner` bands at the
top and bottom are stepped in so the finished shape has chunky pixel-rounded corners.
Returns a gsap timeline (paused unless scrollTrigger/auto given). */
function sliceReveal(el, o){
o = Object.assign({slices:10, corner:2, duration:1.2, ease:'expo.inOut', spread:.4, scrollTrigger:null, delay:0}, o||{});
if(reduce){ el.style.clipPath=''; return gsap.timeline(); }
const bands = [];
const n = o.slices;
for(let i=0;i<n;i++){
const inset = Math.max(i < n/2 ? o.corner - i : i - (n - o.corner - 1), 0);
bands.push({i, inset, w:0});
}
const draw = () => {
const W = el.offsetWidth, H = el.offsetHeight, bh = H/n, step = bh;
let d = '';
bands.forEach(b => {
const w = Math.max(0, (W - step*b.inset) * b.w);
const x = (W - w)/2, y = b.i*bh;
if(w > .5) d += `M${x.toFixed(1)} ${y.toFixed(1)}h${w.toFixed(1)}v${(bh+.6).toFixed(1)}h${(-w).toFixed(1)}Z`;
});
el.style.clipPath = d ? `path('${d}')` : 'inset(50%)';
};
draw();
const tl = gsap.timeline({scrollTrigger:o.scrollTrigger, delay:o.delay, onUpdate:draw,
onComplete(){ if(!o.keep) el.style.clipPath = ''; }});
tl.to(bands, {w:1, duration:o.duration, ease:o.ease, stagger:{amount:o.spread, from:Math.round(n/2)}, onUpdate:draw});
addEventListener('resize', draw);
return tl;
}
function sliceButtons(root){
(root||document).querySelectorAll('.btn:not([data-jv-fx])').forEach(b => {
b.dataset.jvFx = '';
const fx = document.createElement('span'); fx.className = 'jv-fx'; fx.setAttribute('aria-hidden','true');
fx.innerHTML = '<i></i><i></i><i></i><i></i><i></i><i></i>';
b.prepend(fx);
});
}
/* JV.later(fn): below-the-fold section setup waits until the first frames are painted,
then runs one section per idle slice, followed by a single ScrollTrigger.refresh(). */
const queue = [];
let flushing = false;
const idle = window.requestIdleCallback ? cb => requestIdleCallback(cb, {timeout:300}) : cb => setTimeout(cb, 16);
function flush(){
if(!queue.length){ draining = false; refresh(); return; }
const fn = queue.shift();
try { fn(); } catch(err){ console.error(err); }
idle(flush);
}
/* JV.refresh(): every section asks for a refresh through here, so a burst of requests
(fonts ready, images, deferred setup) collapses into one ScrollTrigger.refresh(). */
let rt = 0;
let draining = false;
function refresh(){
if(draining) return; // the deferred queue refreshes once when it finishes
clearTimeout(rt);
rt = setTimeout(() => { ScrollTrigger.sort(); ScrollTrigger.refresh(); }, 200);
}
function later(fn){
queue.push(fn);
if(flushing) return;
flushing = true;
let started = false;
draining = true;
const start = () => { if(started) return; started = true; idle(flush); };
requestAnimationFrame(() => requestAnimationFrame(start)); // after the first painted frame
setTimeout(start, 1200); // background tabs throttle rAF
}
return {reduce, lenis, sliceReveal, sliceButtons, later, refresh};
})();
</script>
<!-- ============ 01 · HERO (prefix jh-) ============ -->
<header class="jh-nav" id="jh-nav">
<div class="jh-nav__bar">
<a class="jh-logo" href="#top" aria-label="Juno Vale, back to top">JV<span class="jh-logo__dot" aria-hidden="true"></span></a>
<nav class="jh-nav__links" aria-label="Primary">
<a href="#album">Music</a>
<a href="#tour">Tour</a>
<a href="#merch">Merch</a>
<a href="#join">Join</a>
</nav>
<a class="btn btn--mint jh-nav__cta" href="#album">
<span class="eq" aria-hidden="true"><i></i><i></i><i></i><i></i></span>Listen now
</a>
<button class="jh-burger" type="button" aria-expanded="false" aria-controls="jh-panel" aria-label="Open menu">
<span></span><span></span>
</button>
</div>
<div class="jh-panel" id="jh-panel" hidden>
<a href="#album">Music</a>
<a href="#tour">Tour</a>
<a href="#merch">Merch</a>
<a href="#join">Join</a>
<a class="btn btn--mint jh-panel__cta" href="#album">
<span class="eq" aria-hidden="true"><i></i><i></i><i></i><i></i></span>Listen now
</a>
</div>
</header>
<!-- Contract: the next section (#record) uses margin-top:-100vh; position:relative; z-index:1
so it slides up UNDER this pinned hero during the last 100vh of the pin. -->
<section class="jh" id="top" aria-labelledby="jh-title">
<canvas class="jh-cv jh-cv--lines" aria-hidden="true"></canvas>
<canvas class="jh-cv jh-cv--trail" aria-hidden="true"></canvas>
<div class="jh-inner">
<div class="jh-meta">
<a class="jh-pill" href="#album">
<span class="jh-pill__eq eq" aria-hidden="true"><i></i><i></i><i></i><i></i></span>
<span>Now playing: <strong>Low Tide Radio</strong></span>
</a>
<p class="jh-meta__note">Album Nº2 · 12 tracks · tracked by the sea</p>
</div>
<div class="jh-stage">
<h1 class="jh-name" id="jh-title" aria-label="Juno Vale">
<span class="jh-line jh-line--1" aria-hidden="true">JUNO</span>
<span class="jh-line jh-line--2" aria-hidden="true">VALE</span>
</h1>
<div class="jh-badge" aria-hidden="true">
<svg viewBox="0 0 120 120">
<defs><path id="jh-circle" d="M60,60 m-44,0 a44,44 0 1,1 88,0 a44,44 0 1,1 -88,0"/></defs>
<text><textPath href="#jh-circle">OUT NOW • LOW TIDE RADIO • OUT NOW •</textPath></text>
</svg>
<span class="jh-badge__core">
<svg viewBox="0 0 24 24"><path d="M8 5.5v13l10.5-6.5z"/></svg>
</span>
</div>
<p class="jh-lede lede">Twelve songs cut in a salt-stung studio at the end of the pier — static, slow heartbreak and the hum of the tide, all tuned to one late-night frequency.</p>
</div>
<div class="jh-foot">
<div class="jh-ctas">
<a class="btn" href="#album">Stream the album
<svg class="jh-arrow" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg>
</a>
<a class="btn btn--ghost" href="#tour">Tour dates</a>
</div>
<a class="jh-scroll" href="#album" aria-label="Scroll to the album">
<span>Dive in</span>
<span class="jh-scroll__line" aria-hidden="true"></span>
</a>
</div>
</div>
<div class="jh-marquee" aria-label="Low Tide Radio out now. World tour 2026 to 2027.">
<div class="jh-marquee__track" aria-hidden="true">
<div class="jh-marquee__group">
<span>Low Tide Radio</span><i></i><span>Out now</span><i></i><span>World Tour 2026/27</span><i></i>
<span>Low Tide Radio</span><i></i><span>Out now</span><i></i><span>World Tour 2026/27</span><i></i>
</div>
<div class="jh-marquee__group">
<span>Low Tide Radio</span><i></i><span>Out now</span><i></i><span>World Tour 2026/27</span><i></i>
<span>Low Tide Radio</span><i></i><span>Out now</span><i></i><span>World Tour 2026/27</span><i></i>
</div>
</div>
</div>
</section>
<style>
/* ---------- NAV (fixed, outside the clipped hero) ---------- */
.jh-nav{position:fixed;inset:0 0 auto 0;z-index:100;transition:background .35s var(--ease),box-shadow .35s var(--ease),backdrop-filter .35s var(--ease)}
.jh-nav.is-stuck{background:rgba(238,240,251,.82);backdrop-filter:blur(14px) saturate(1.3);-webkit-backdrop-filter:blur(14px) saturate(1.3);box-shadow:0 1px 0 var(--line)}
.jh-nav__bar{display:flex;align-items:center;gap:24px;height:76px;padding-inline:var(--gutter);max-width:1680px;margin-inline:auto}
.jh-logo{font-family:var(--display);font-weight:900;font-size:24px;letter-spacing:-.06em;color:var(--ink);display:inline-flex;align-items:flex-end;gap:3px;line-height:1}
.jh-logo__dot{width:8px;height:8px;border-radius:50%;background:var(--mint);margin-bottom:2px}
.jh-nav__links{display:flex;gap:6px;margin-inline:auto;padding:6px;border-radius:999px;background:rgba(255,255,255,.55);box-shadow:inset 0 0 0 1px var(--line)}
.jh-nav__links a{font-weight:700;font-size:14px;padding:8px 16px;border-radius:999px;color:var(--ink);transition:background .25s var(--ease),color .25s var(--ease)}
.jh-nav__links a:hover{background:var(--cobalt);color:#fff}
.jh-nav__cta{padding:11px 20px;font-size:14px}
.jh-nav__cta .eq{height:12px}
.jh-burger{display:none;width:46px;height:46px;border-radius:50%;background:var(--paper);box-shadow:inset 0 0 0 1px var(--line);position:relative;margin-left:auto}
.jh-burger span{position:absolute;left:14px;right:14px;height:2px;border-radius:2px;background:var(--ink);transition:transform .35s var(--ease),top .35s var(--ease)}
.jh-burger span:first-child{top:19px}
.jh-burger span:last-child{top:25px}
.jh-burger[aria-expanded="true"] span:first-child{top:22px;transform:rotate(45deg)}
.jh-burger[aria-expanded="true"] span:last-child{top:22px;transform:rotate(-45deg)}
.jh-panel{position:absolute;top:72px;left:12px;right:12px;display:flex;flex-direction:column;gap:2px;padding:14px;border-radius:22px;background:var(--paper);box-shadow:var(--shadow),0 0 0 1px var(--line)}
.jh-panel[hidden]{display:none}
.jh-panel a:not(.btn){font-family:var(--display);font-weight:700;font-size:26px;letter-spacing:-.03em;text-transform:uppercase;padding:12px 10px;border-radius:14px;color:var(--ink)}
.jh-panel a:not(.btn):hover{background:var(--cobalt-soft)}
.jh-panel__cta{justify-content:center;margin-top:10px}
.jh-panel.is-in{animation:jh-drop .4s var(--ease)}
@keyframes jh-drop{from{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:none}}
/* ---------- HERO ---------- */
.jh{position:relative;z-index:2;overflow:hidden;overflow:clip;min-height:100vh;min-height:100svh;display:flex;flex-direction:column;
background:
radial-gradient(60vw 50vw at 88% 8%, rgba(63,224,181,.18), transparent 60%),
radial-gradient(70vw 60vw at 8% 92%, rgba(36,56,232,.14), transparent 62%),
var(--bg)}
.jh-cv{position:absolute;inset:0;width:100%;height:100%;display:block;pointer-events:none}
.jh-cv--lines{z-index:0}
.jh-cv--trail{z-index:1}
.jh-inner{position:relative;z-index:2;flex:1;display:flex;flex-direction:column;justify-content:center;padding:112px var(--gutter) clamp(32px,4vw,56px);max-width:1680px;width:100%;margin-inline:auto}
.jh-meta{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:clamp(20px,2.6vw,40px)}
.jh-pill{display:inline-flex;align-items:center;gap:10px;padding:9px 16px 9px 12px;border-radius:999px;background:var(--paper);box-shadow:inset 0 0 0 1px var(--line),0 10px 24px -18px rgba(36,56,232,.6);font-size:14px;font-weight:500;color:var(--ink);transition:transform .25s var(--ease)}
.jh-pill:hover{transform:translateY(-2px)}
.jh-pill strong{font-weight:800}
.jh-pill__eq{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:7px 6px;border-radius:50%;background:var(--mint);color:var(--ink)}
.jh-pill__eq i{width:2.5px}
.jh-meta__note{margin:0;font-size:12px;font-weight:800;letter-spacing:.18em;text-transform:uppercase;color:var(--mute)}
.jh-stage{--jh-fs:clamp(88px, min(14vw, 22vh), 300px);position:relative}
.jh-name{margin:0;font-family:var(--display);font-weight:900;font-size:var(--jh-fs);line-height:.8;letter-spacing:-.06em;text-transform:uppercase;color:var(--ink)}
/* each line masks its letters vertically only (for the rise-in), so words can still fly off sideways */
.jh-line{display:block;padding:.07em .04em .03em;margin-inline:-.04em;white-space:nowrap;clip-path:inset(-50% -300vw 0 -300vw)}
.jh-line--2{text-align:right;color:var(--cobalt);padding-right:clamp(0px,6vw,120px)}
.jh-ch{display:inline-block;will-change:transform}
.jh-badge{position:absolute;right:clamp(8px,4vw,80px);top:4%;width:clamp(88px,8vw,124px);aspect-ratio:1;border-radius:50%;background:var(--mint);color:var(--ink);display:grid;place-items:center;box-shadow:0 18px 34px -18px rgba(34,196,153,.9)}
.jh-badge > svg{position:absolute;inset:0;width:100%;height:100%;animation:jh-spin 16s linear infinite}
.jh-badge text{font-family:var(--display);font-weight:700;font-size:10.4px;letter-spacing:.14em;fill:var(--ink)}
.jh-badge__core{width:34%;aspect-ratio:1;border-radius:50%;background:var(--paper);display:grid;place-items:center}
.jh-badge__core svg{width:52%;fill:var(--cobalt);margin-left:8%}
@keyframes jh-spin{to{transform:rotate(360deg)}}
.jh-lede{position:absolute;left:0;bottom:calc(var(--jh-fs) * .1);margin:0;max-width:min(360px, 24vw);font-size:clamp(15px,1.15vw,18px);line-height:1.55;color:var(--mute)}
.jh-foot{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-top:clamp(28px,3.2vw,48px)}
.jh-ctas{display:flex;flex-wrap:wrap;gap:12px}
.jh-arrow{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round;transition:transform .3s var(--ease)}
.jh-ctas .btn:hover .jh-arrow{transform:translateX(4px)}
.jh-scroll{display:inline-flex;align-items:center;gap:14px;font-size:12px;font-weight:800;letter-spacing:.2em;text-transform:uppercase;color:var(--mute)}
.jh-scroll__line{position:relative;width:64px;height:2px;background:var(--line);overflow:hidden;border-radius:2px}
.jh-scroll__line::after{content:"";position:absolute;inset:0;background:var(--cobalt);transform:translateX(-100%);animation:jh-sweep 2.2s var(--ease) infinite}
@keyframes jh-sweep{60%,100%{transform:translateX(100%)}}
/* ---------- MARQUEE (inside the hero: it wipes away with it) ---------- */
.jh-marquee{position:relative;z-index:2;flex:none;background:var(--cobalt);color:#fff;overflow:hidden;padding:14px 0;border-block:1px solid var(--cobalt-deep)}
.jh-marquee__track{display:flex;width:max-content;animation:jh-marquee 34s linear infinite}
.jh-marquee__group{display:flex;align-items:center;flex:none}
.jh-marquee span{font-family:var(--display);font-weight:700;font-size:clamp(15px,1.5vw,20px);letter-spacing:.02em;text-transform:uppercase;white-space:nowrap;padding-inline:clamp(16px,1.8vw,28px)}
.jh-marquee i{flex:none;width:12px;height:12px;background:var(--mint);clip-path:polygon(50% 0,62% 38%,100% 50%,62% 62%,50% 100%,38% 62%,0 50%,38% 38%)}
.jh-marquee:hover .jh-marquee__track{animation-play-state:paused}
@keyframes jh-marquee{to{transform:translateX(-50%)}}
/* ---------- RESPONSIVE ---------- */
@media (max-width: 1180px){
.jh-meta__note{display:none}
.jh-lede{position:static;max-width:46ch;margin-top:24px}
}
@media (max-width: 860px){
.jh-nav__links,.jh-nav__cta{display:none}
.jh-burger{display:block}
.jh-nav__bar{height:68px}
.jh-panel{top:66px}
}
@media (min-width: 861px){
.jh-panel{display:none !important}
}
@media (max-width: 899px){
.jh-inner{padding-top:96px;justify-content:flex-start}
.jh-stage{--jh-fs:min(22vw, 15svh, 180px)}
.jh-line--2{padding-right:.04em}
/* badge rides the pill row, right-aligned, clear of the title */
.jh-badge{width:64px;right:0;top:calc(-32px - 23px - clamp(20px,2.6vw,40px))}
.jh-badge text{font-size:11px}
.jh-lede{position:static;max-width:46ch;margin-top:20px;font-size:15px}
.jh-foot{margin-top:22px}
.jh-scroll{display:none}
}
@media (max-width: 480px){
.jh-ctas{width:100%}
.jh-ctas .btn{flex:1 1 auto;justify-content:center;padding-inline:20px}
.jh-pill{font-size:13px}
}
@media (prefers-reduced-motion: reduce) and (update: none){
.jh-marquee__track,.jh-badge > svg,.jh-scroll__line::after{animation:none}
.jh-panel.is-in{animation:none}
.jh-cv--trail{display:none}
}
</style>
<script>
(function(){
var nav = document.getElementById('jh-nav');
var burger = nav.querySelector('.jh-burger');
var panel = document.getElementById('jh-panel');
var reduce = window.JV ? JV.reduce : matchMedia('(prefers-reduced-motion: reduce) and (update: none)').matches;
/* sticky nav state */
var stuck = null;
function onScroll(){ var s = window.scrollY > 16; if (s !== stuck){ stuck = s; nav.classList.toggle('is-stuck', s); } }
onScroll();
window.addEventListener('scroll', onScroll, {passive:true});
/* mobile menu */
function setMenu(open){
burger.setAttribute('aria-expanded', open ? 'true' : 'false');
burger.setAttribute('aria-label', open ? 'Close menu' : 'Open menu');
panel.hidden = !open;
panel.classList.toggle('is-in', open);
nav.classList.toggle('is-stuck', open || window.scrollY > 16);
}
burger.addEventListener('click', function(){ setMenu(burger.getAttribute('aria-expanded') !== 'true'); });
panel.addEventListener('click', function(e){ if (e.target.closest('a')) setMenu(false); });
document.addEventListener('keydown', function(e){ if (e.key === 'Escape' && !panel.hidden){ setMenu(false); burger.focus(); } });
window.matchMedia('(min-width: 861px)').addEventListener('change', function(m){ if (m.matches) setMenu(false); });
var hero = document.querySelector('.jh');
var linesCv = hero.querySelector('.jh-cv--lines');
var trailCv = hero.querySelector('.jh-cv--trail');
var lctx = linesCv.getContext('2d');
var tctx = trailCv.getContext('2d');
var W = 0, H = 0, DPR = 1;
/* ---------- background lines: half-circle arcs off the bottom-right + drop lines ---------- */
var state = {p:0, off:0}; // p: arc -> column collapse, off: 0..1 of hero height
var arcs = [];
function buildArcs(){
arcs = [];
var cx = 1.1 * W, cy = H, far = Math.hypot(cx, cy);
var base = Math.max(36, Math.min(100, W / 12));
var r0 = cx - 0.5 * W, list = [r0];
var r = r0, s = base;
while (r <= far + s){ r += s; s *= 1.1; list.push(r); }
r = r0; s = base * .9;
for (var i = 0; i < 4; i++){ r -= s; s *= .8; if (r > 4) list.push(r); }
list.sort(function(a, b){ return a - b; }); // inner (rightmost left-end) first
var n = list.length;
list.forEach(function(rad, i){
var k = 5 - Math.min(5, Math.floor(i * 6 / n)); // 6 columns, inner arcs -> right column
var colX = W * (k + .5) / 6;
arcs.push({r0:rad, rt:cx - colX});
});
}
function drawLines(){
if (!W) return;
lctx.setTransform(DPR, 0, 0, DPR, 0, 0);
lctx.clearRect(0, 0, W, H);
var cx = 1.1 * W, cy = H - state.off * H, p = state.p;
lctx.beginPath();
for (var i = 0; i < arcs.length; i++){
var r = arcs[i].r0 + (arcs[i].rt - arcs[i].r0) * p;
var x = cx - r;
lctx.moveTo(x, H);
lctx.lineTo(x, cy);
lctx.arc(cx, cy, r, Math.PI, Math.PI * 2);
}
lctx.lineWidth = 1;
lctx.strokeStyle = 'rgba(36,56,232,.22)';
lctx.stroke();
}
function resize(){
W = hero.offsetWidth; H = hero.offsetHeight;
DPR = Math.min(window.devicePixelRatio || 1, 1.5); // lines + trail are soft; 1.5x keeps canvas fill cost down on retina
[linesCv, trailCv].forEach(function(c){
c.width = Math.round(W * DPR); c.height = Math.round(H * DPR);
});
buildArcs(); drawLines(); drawClip();
}
/* ---------- exit strips: 6 bottom-anchored vertical rects ---------- */
var strips = [];
for (var si = 0; si < 6; si++) strips.push({h:1});
function drawClip(){
if (reduce) return;
var full = true, d = '';
var sw = W / 6;
strips.forEach(function(s, i){
if (s.h < 1) full = false;
var hh = s.h * H;
if (hh > .5){
var x1 = Math.floor(i * sw), x2 = Math.ceil((i + 1) * sw);
d += 'M' + x1 + ' ' + (H - hh).toFixed(1) + 'H' + x2 + 'V' + H + 'H' + x1 + 'Z';
}
});
hero.style.clipPath = full ? '' : (d ? "path('" + d + "')" : 'inset(50%)');
}
if ('ResizeObserver' in window) new ResizeObserver(resize).observe(hero);
else window.addEventListener('resize', resize);
resize();
/* reduced motion: static arcs, no trail, no pin, plain text */
if (reduce || !window.gsap || !window.ScrollTrigger) return;
gsap.registerPlugin(ScrollTrigger);
/* ---------- split the title into chars (aria-label stays on the h1) ---------- */
var lines = hero.querySelectorAll('.jh-line');
lines.forEach(function(line){
var txt = line.textContent.trim();
line.textContent = '';
txt.split('').forEach(function(c){
var s = document.createElement('span'); s.className = 'jh-ch'; s.textContent = c; line.appendChild(s);
});
});
var chars = hero.querySelectorAll('.jh-ch');
/* ---------- intro ---------- */
var intro = gsap.timeline({defaults:{ease:'expo.out'}});
intro.from('.jh-line--1 .jh-ch', {yPercent:115, rotate:6, duration:1.3, stagger:.07}, .1)
.from('.jh-line--2 .jh-ch', {yPercent:115, rotate:-6, duration:1.3, stagger:.07}, .3)
.from('.jh-badge', {scale:0, rotate:-120, duration:1, ease:'back.out(1.8)'}, .9)
.from(['.jh-meta', '.jh-lede', '.jh-foot'], {y:24, opacity:0, duration:1, stagger:.1, ease:'power3.out'}, .6)
.from('.jh-nav__bar', {y:-20, opacity:0, duration:.9, ease:'power3.out'}, .5)
.from(linesCv, {opacity:0, duration:1.6, ease:'power2.out'}, 0);
/* ---------- pinned scroll: arcs collapse, title flies, strips wipe ---------- */
var master = gsap.timeline({
defaults:{ease:'none'},
scrollTrigger:{trigger:hero, start:'top top', end:'+=200%', pin:true, pinSpacing:true, scrub:.25, invalidateOnRefresh:true},
onUpdate:function(){ drawLines(); drawClip(); }
});
master.to(state, {p:1, duration:1, ease:'power1.inOut'}, 0)
.to(state, {off:1, duration:2, ease:'none'}, 0)
.to(chars, {x:function(){ return -window.innerWidth; }, duration:.65, ease:'power3.in', stagger:.05}, 0)
.to(strips, {h:0, duration:.5, ease:'power3.inOut', stagger:{each:.1, from:'random'}}, 1);
/* ---------- cursor image trail ---------- */
var SRC = ['assets/hero.jpg','assets/live.jpg','assets/studio.jpg','assets/crowd.jpg','assets/vinyl.jpg','assets/sea.jpg','assets/tee.jpg'];
var pics = SRC.map(function(src){ var im = new Image(); im.decoding = 'async'; im.src = src; return im; });
var picIdx = 0, items = [], MAX = 14, BANDS = 10, CORNER = 2;
var hasFilter = typeof tctx.filter === 'string';
var coarse = matchMedia('(hover: none), (pointer: coarse)');
function autoOnly(){ return window.innerWidth < 900 || coarse.matches; }
function size(){ return .125 * Math.hypot(W, H); }
var ptr = {x:0, y:0, px:0, py:0, vx:0, vy:0, has:false, last:-1e9};
var lastEmit = {x:-1e9, y:-1e9};
var wasAuto = true;
function emit(x, y, vx, vy, auto){
if (items.length >= MAX) return;
var im = pics[picIdx++ % pics.length];
var bands = [];
for (var i = 0; i < BANDS; i++){
bands.push({i:i, w:0, inset:Math.max(i < BANDS / 2 ? CORNER - i : i - (BANDS - CORNER - 1), 0)});
}
var it = {im:im, x:x, y:y, vx:vx, vy:vy, sy:window.scrollY, bright:1, bands:bands, drift:auto};
var tl = gsap.timeline({onComplete:function(){ var k = items.indexOf(it); if (k > -1) items.splice(k, 1); }});
tl.to(bands, {w:1, duration:.8, ease:'power3.inOut', stagger:{amount:.4, from:'center'}}, 0)
.to(it, {bright:.25, duration:1.2, ease:'power2.out'}, .8)
.to(bands, {w:0, duration:.8, ease:'power3.inOut'}, 1.2);
if (auto) tl.timeScale(.75);
it.tl = tl;
items.push(it);
}
function clamp(v){ return Math.max(-10, Math.min(10, v)); }
hero.addEventListener('pointermove', function(e){
if (e.pointerType === 'touch' || autoOnly()) return;
var r = hero.getBoundingClientRect();
var x = e.clientX - r.left, y = e.clientY - r.top;
if (!ptr.has){ ptr.px = x; ptr.py = y; ptr.has = true; }
ptr.x = x; ptr.y = y; ptr.last = performance.now();
}, {passive:true});
var spriteCache = new Map();
function sprite(im, w, h){
var pw = Math.round(w * DPR), ph = Math.round(h * DPR), key = im.src + '|' + pw + 'x' + ph;
var c = spriteCache.get(key);
if (c) return c;
c = document.createElement('canvas'); c.width = pw; c.height = ph;
var ir = im.naturalWidth / im.naturalHeight, br = w / h, sx, sy, sw, sh;
if (ir > br){ sh = im.naturalHeight; sw = sh * br; sx = (im.naturalWidth - sw) / 2; sy = 0; }
else { sw = im.naturalWidth; sh = sw / br; sx = 0; sy = (im.naturalHeight - sh) / 2; }
c.getContext('2d').drawImage(im, sx, sy, sw, sh, 0, 0, pw, ph);
if (spriteCache.size > 24) spriteCache.clear();
spriteCache.set(key, c);
return c;
}
function drawItem(it){
var im = it.im;
if (!im.complete || !im.naturalWidth) return;
var s = size(), w = s, h = s * 1.1;
var L = it.x - w / 2, T = it.y - h / 2 - (window.scrollY - it.sy) * 1.5;
if (T > H || T + h < 0) return;
var bh = h / BANDS, any = false;
tctx.save();
tctx.beginPath();
for (var i = 0; i < BANDS; i++){
var b = it.bands[i];
var bw = Math.max(0, (w - 2 * bh * b.inset) * b.w);
if (bw < .5) continue;
any = true;
var x1 = Math.round(L + (w - bw) / 2), x2 = Math.round(L + (w + bw) / 2);
var y1 = Math.round(T + i * bh), y2 = Math.round(T + (i + 1) * bh);
tctx.rect(x1, y1, x2 - x1, y2 - y1);
}
if (!any){ tctx.restore(); return; }
tctx.clip();
/* pre-cropped, pre-scaled bitmap (built once per photo + size) instead of scaling the full photo every frame */
tctx.drawImage(sprite(im, w, h), L, T, w, h);
if (it.bright < 1){
tctx.fillStyle = 'rgba(22,18,58,' + (1 - it.bright).toFixed(3) + ')';
tctx.fillRect(L, T, w, h);
}
tctx.restore();
}
var raf = 0, running = false, t0 = performance.now();
function frame(now){
raf = requestAnimationFrame(frame);
var s = size(), spacing = s / 2;
var auto = autoOnly() || !ptr.has || now - ptr.last > 3000;
if (auto && now - t0 < 1400){ /* let the title intro land before the auto trail starts */ }
else if (auto){
var hx = W / 2 + Math.cos((now - t0) * .0005) * (W - s) / 2;
var hy = H / 2 + Math.sin((now - t0) * .001) * H * .1;
if (!wasAuto){ lastEmit.x = hx; lastEmit.y = hy; }
/* the idle trail is ambient: emit half as often and keep at most 7 alive */
if (items.length < 7 && Math.hypot(hx - lastEmit.x, hy - lastEmit.y) > spacing * 2){
lastEmit.x = hx; lastEmit.y = hy;
emit(hx, hy, .6, -.8, true);
}
} else {
var dx = ptr.x - ptr.px, dy = ptr.y - ptr.py;
ptr.vx += (dx - ptr.vx) * .2; ptr.vy += (dy - ptr.vy) * .2;
ptr.px = ptr.x; ptr.py = ptr.y;
if (wasAuto){ lastEmit.x = -1e9; }
if (Math.hypot(ptr.x - lastEmit.x, ptr.y - lastEmit.y) > spacing){
lastEmit.x = ptr.x; lastEmit.y = ptr.y;
emit(ptr.x, ptr.y, clamp(ptr.vx), clamp(ptr.vy), false);
}
}
wasAuto = auto;
tctx.setTransform(DPR, 0, 0, DPR, 0, 0);
tctx.clearRect(0, 0, W, H);
for (var i = 0; i < items.length; i++){
var it = items[i];
it.x += it.vx; it.y += it.vy;
if (it.drift){ it.vx *= .98; it.vy *= .98; } else { it.vx *= .95; it.vy *= .95; }
drawItem(it);
}
}
function start(){ if (!running){ running = true; raf = requestAnimationFrame(frame); } }
function stop(){ if (running){ running = false; cancelAnimationFrame(raf); } }
var inView = true;
if ('IntersectionObserver' in window){
new IntersectionObserver(function(en){
inView = en[0].isIntersecting;
(inView && !document.hidden) ? start() : stop();
}).observe(hero);
} else start();
document.addEventListener('visibilitychange', function(){ (document.hidden || !inView) ? stop() : start(); });
})();
</script>
<section class="js-stmt" id="record" aria-labelledby="js-stmt-title">
<style>
/* ---------- 02 · statement (prefix js-) ---------- */
/* Static / reduced-motion: three stacked colour panels — cobalt, mint, mist */
.js-stmt{position:relative;z-index:1;background:var(--cobalt);color:#fff;overflow:hidden;overflow:clip}
.js-stage{position:relative}
.js-canvas{display:none}
.js-hud{display:flex;align-items:center;justify-content:space-between;gap:16px;
padding:clamp(28px,4vw,48px) var(--gutter) 0;background:var(--cobalt);color:#fff;position:relative;z-index:4}
.js-hud .eyebrow{color:inherit;display:inline-flex;align-items:center;gap:12px;margin:0}
.js-hud .eq{height:14px}
.js-count{display:none;font-family:var(--display);font-weight:700;font-size:13px;letter-spacing:.12em;margin:0}
.js-count b{display:inline-block;min-width:2ch;font-weight:900}
.js-count span{opacity:.55}
.js-rail{display:none}
.js-panel{position:relative;z-index:2;padding:clamp(64px,9vw,130px) var(--gutter)}
.js-panel[data-step="1"]{background:var(--cobalt);color:#fff;--js-rule:rgba(255,255,255,.16)}
.js-panel[data-step="2"]{background:var(--mint);color:var(--ink);--js-rule:rgba(22,18,58,.12)}
.js-panel[data-step="3"]{background:var(--bg);color:var(--ink);--js-rule:rgba(22,18,58,.09)}
.js-panel::before{content:"";position:absolute;inset:0;pointer-events:none;z-index:-1;
background:repeating-linear-gradient(90deg,var(--js-rule) 0 1px,transparent 1px calc(100% / 6))}
.js-inner{max-width:1360px;margin-inline:auto;position:relative}
.js-num{display:block;font-family:var(--display);font-weight:700;font-size:13px;letter-spacing:.14em;margin-bottom:clamp(18px,2.4vw,32px);opacity:.7}
.js-say{font-family:var(--display);font-weight:900;text-transform:uppercase;letter-spacing:-.045em;line-height:.92;
font-size:clamp(30px,7.6vw,118px);margin:0;max-width:100%;overflow-wrap:break-word}
.js-line{display:block}
.js-say em{font-style:normal}
.js-panel[data-step="1"] em{color:var(--mint)}
.js-panel[data-step="2"] em,.js-panel[data-step="3"] em{color:var(--cobalt)}
.js-note{margin:clamp(20px,2.6vw,34px) 0 0;max-width:38ch;font-size:clamp(15px,1.2vw,18px);font-weight:500;line-height:1.55;opacity:.85}
.js-fit{position:relative}
.js-window{width:min(78%,420px);aspect-ratio:1;border-radius:50%;overflow:hidden;margin:clamp(36px,5vw,64px) 0 0 auto;
box-shadow:var(--shadow);position:relative;background:var(--cobalt-soft)}
.js-window img{width:100%;height:100%;object-fit:cover}
.js-window::after{content:"";position:absolute;inset:10px;border-radius:50%;border:1.5px solid rgba(255,255,255,.55);pointer-events:none}
/* ---------- motion mode (class added by the script) ---------- */
.js-stmt.js-on{margin-top:-200vh;height:100vh;height:100svh}
.pin-spacer:has(> .js-stmt){position:relative;z-index:1}
.js-on .js-stage{height:100%;overflow:hidden;background:var(--cobalt);color:#fff}
.js-on .js-canvas{display:block;position:absolute;inset:0;width:100%;height:100%;z-index:0;pointer-events:none}
.js-on .js-hud{position:absolute;left:0;right:0;bottom:clamp(34px,5vh,52px);padding:0 var(--gutter);background:transparent;color:inherit}
.js-on .js-count{display:inline-block}
.js-on .js-rail{display:block;position:absolute;left:var(--gutter);right:var(--gutter);bottom:clamp(18px,3vh,28px);height:2px;z-index:4}
.js-rail::before{content:"";position:absolute;inset:0;background:currentColor;opacity:.22}
.js-rail i{position:absolute;inset:0;background:currentColor;transform-origin:0 50%;transform:scaleX(0)}
.js-on .js-panel{position:absolute;inset:0;display:flex;align-items:center;background:transparent;
padding:clamp(72px,11vh,110px) var(--gutter) clamp(90px,14vh,130px)}
.js-on .js-panel::before,.js-on .js-num{display:none}
.js-on .js-inner{width:100%}
.js-on .js-fit{will-change:transform}
.js-w{display:inline-block}
/* statement 1: two lines fitted to the viewport width */
.js-on .js-fit{display:flex;flex-direction:column;align-items:center}
.js-on .js-say--fit{display:flex;flex-direction:column;align-items:center;white-space:nowrap;line-height:.9;max-width:none;overflow-wrap:normal}
.js-on .js-say--fit .js-line{display:block}
.js-on .js-fit .js-note{max-width:32ch;text-align:center;margin-top:clamp(18px,3vh,30px)}
.js-on .js-fit .js-note.is-side{position:absolute;margin:0;max-width:none;text-align:left}
.js-nl{display:block}
/* statements 2 + 3 */
.js-on .js-panel[data-step="2"] .js-say,.js-on .js-panel[data-step="3"] .js-say{font-size:clamp(32px,6.2vw,104px)}
.js-on .js-panel[data-step="2"] .js-inner > :not(.js-window){max-width:60%}
.js-on .js-window{position:absolute;right:0;top:50%;translate:0 -50%;margin:0;width:clamp(220px,30vw,440px);max-width:62vh}
@media (max-width:760px){
.js-window{width:min(78%,320px)}
.js-on .js-panel{align-items:flex-start;padding-top:clamp(84px,14vh,130px)}
.js-on .js-panel[data-step="1"]{align-items:center}
.js-on .js-panel[data-step="2"] .js-inner > :not(.js-window){max-width:100%}
.js-on .js-panel[data-step="2"] .js-inner{position:static}
.js-on .js-window{top:auto;translate:none;bottom:clamp(92px,15vh,140px);right:var(--gutter);width:min(56vw,260px)}
.js-note{max-width:32ch}
}
</style>
<div class="js-stage">
<canvas class="js-canvas" aria-hidden="true"></canvas>
<h2 id="js-stmt-title" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap">How Low Tide Radio was made</h2>
<div class="js-hud">
<p class="eyebrow"><span class="eq" aria-hidden="true"><i></i><i></i><i></i><i></i></span>The record</p>
<p class="js-count" aria-hidden="true"><b class="js-cur">01</b><span> / 03</span></p>
</div>
<article class="js-panel" data-step="1">
<div class="js-inner">
<span class="js-num">01 / 03</span>
<div class="js-fit">
<p class="js-say js-say--fit">
<span class="js-line">Written in a room</span>
<span class="js-line"><em>by the sea.</em></span>
</p>
<p class="js-note">One rented flat above a boarded-up arcade, a borrowed upright piano and the window left open all winter.</p>
</div>
</div>
</article>
<article class="js-panel" data-step="2">
<div class="js-inner">
<span class="js-num">02 / 03</span>
<p class="js-say">
<span class="js-line">Recorded</span>
<span class="js-line">at <em>low tide.</em></span>
</p>
<p class="js-note">Twelve songs tracked in the hours the water pulled back, when the harbour went quiet enough to hear the tape hiss.</p>
<figure class="js-window">
<img src="assets/sea.jpg" width="1880" height="1253" alt="Aerial view of white-capped waves rolling over deep blue water" loading="lazy" decoding="async">
</figure>
</div>
</article>
<article class="js-panel" data-step="3">
<div class="js-inner">
<span class="js-num">03 / 03</span>
<p class="js-say">
<span class="js-line">Made to be played</span>
<span class="js-line">loud at <em>3am.</em></span>
</p>
<p class="js-note">Low Tide Radio is out now. Headphones on, lights off, volume a little past sensible.</p>
</div>
</article>
<div class="js-rail" aria-hidden="true"><i></i></div>
</div>
<script>
(function(){
var sec = document.getElementById('record');
if(!sec || !window.gsap || !window.ScrollTrigger || !window.JV) return;
if(!JV.reduce){
gsap.registerPlugin(ScrollTrigger);
var css = getComputedStyle(document.documentElement);
var C = {
bg: css.getPropertyValue('--bg').trim() || '#EEF0FB',
cobalt: css.getPropertyValue('--cobalt').trim() || '#2438E8',
mint: css.getPropertyValue('--mint').trim() || '#3FE0B5',
ink: css.getPropertyValue('--ink').trim() || '#16123A'
};
var stage = sec.querySelector('.js-stage');
var canvas = sec.querySelector('.js-canvas');
var g = canvas.getContext('2d');
var cur = sec.querySelector('.js-cur');
var bar = sec.querySelector('.js-rail i');
var panels = sec.querySelectorAll('.js-panel');
var fit = panels[0].querySelector('.js-fit');
var say1 = fit.querySelector('.js-say');
var l1 = say1.querySelectorAll('.js-line');
var note1 = fit.querySelector('.js-note');
var note1Text = note1.textContent.trim();
var say2 = panels[1].querySelector('.js-say'), say3 = panels[2].querySelector('.js-say');
var win = panels[1].querySelector('.js-window'), img = win.querySelector('img');
img.loading = 'eager';
/* ---- split text: every word of every line into an inline-block span ---- */
function splitWords(line){
var out = [];
(function walk(node, into){
Array.prototype.slice.call(node.childNodes).forEach(function(n){
if(n.nodeType === 3){
var frag = document.createDocumentFragment();
n.textContent.split(/(\s+)/).forEach(function(part){
if(!part) return;
if(/^\s+$/.test(part)){ frag.appendChild(document.createTextNode(' ')); return; }
var w = document.createElement('span'); w.className = 'js-w'; w.textContent = part;
frag.appendChild(w); out.push(w);
});
node.replaceChild(frag, n);
} else if(n.nodeType === 1){ walk(n); }
});
})(line);
return out;
}
[say1, say2, say3].forEach(function(p){
p.setAttribute('aria-label', p.textContent.replace(/\s+/g, ' ').trim());
p.querySelectorAll('.js-line').forEach(function(l){ l.setAttribute('aria-hidden', 'true'); });
});
var words1 = [];
l1.forEach(function(l){ words1 = words1.concat(splitWords(l)); });
note1.setAttribute('aria-label', note1Text);
/* split the supporting paragraph into its rendered lines (redone on every build) */
function splitNoteLines(){
note1.innerHTML = note1Text.split(' ').map(function(w){ return '<span class="js-w" aria-hidden="true">' + w + '</span>'; }).join(' ');
var rows = [], lastTop = null;
note1.querySelectorAll('.js-w').forEach(function(w){
if(lastTop === null || Math.abs(w.offsetTop - lastTop) > 4){ rows.push([]); lastTop = w.offsetTop; }
rows[rows.length - 1].push(w.textContent);
});
note1.innerHTML = rows.map(function(r){ return '<span class="js-nl" aria-hidden="true">' + r.join(' ') + '</span>'; }).join('');
return note1.querySelectorAll('.js-nl');
}
/* ---- canvas: 7 vertical lines, spacing is tweenable, always centred ---- */
var S = {sp: 1}, W = 0, H = 0, dpr = 1;
function sizeCanvas(){
dpr = Math.min(window.devicePixelRatio || 1, 2);
W = stage.clientWidth; H = stage.clientHeight;
canvas.width = Math.round(W * dpr); canvas.height = Math.round(H * dpr);
}
function draw(){
if(!W) return;
var m = (getComputedStyle(stage).color.match(/[\d.]+/g) || [255, 255, 255]);
g.setTransform(dpr, 0, 0, dpr, 0, 0);
g.clearRect(0, 0, W, H);
g.strokeStyle = 'rgba(' + m[0] + ',' + m[1] + ',' + m[2] + ',.25)';
g.lineWidth = 1;
g.beginPath();
var gap = S.sp * W / 6;
for(var i = 0; i < 7; i++){
var x = Math.round(Math.min(Math.max(W / 2 + (i - 3) * gap, .5), W - .5)) + .5;
g.moveTo(x, 0); g.lineTo(x, H);
}
g.stroke();
}
/* ---- sea window: JV.sliceReveal, paused, its progress driven by the master timeline ---- */
var slice = JV.sliceReveal(win, {slices: 10, corner: 2, keep: true});
slice.pause(0);
var P = {p: 0};
sec.classList.add('js-on');
var ctx = null, lastW = 0, lastH = 0;
function build(){
sizeCanvas();
lastW = window.innerWidth; lastH = window.innerHeight;
ctx = gsap.context(function(){
gsap.set(stage, {backgroundColor: C.cobalt, color: '#fff'});
S.sp = 1; P.p = 0; slice.progress(0);
/* fit statement 1: widest line fills the viewport width */
gsap.set(say1, {fontSize: 100});
var wMax = Math.max(l1[0].offsetWidth, l1[1].offsetWidth);
var fs = Math.min(100 * (W * .96) / wMax, H * .24);
gsap.set(say1, {fontSize: fs});
var fr = fit.getBoundingClientRect();
function box(el){ var r = el.getBoundingClientRect(); return {x: r.left - fr.left, y: r.top - fr.top, w: r.width, h: r.height}; }
var b1 = box(l1[0]), b2 = box(l1[1]);
var w2 = b2.w;
wMax = Math.max(b1.w, b2.w);
var left = Math.min(b1.x, b2.x);
var gap = Math.max(18, fs * .22);
var side = window.innerWidth > 987 && (wMax - w2 - gap) >= 230;
if(side){
note1.classList.add('is-side');
gsap.set(note1, {left: left + w2 + gap, width: wMax - w2 - gap});
} else note1.classList.remove('is-side');
var nls = splitNoteLines();
if(side) gsap.set(note1, {top: b2.y + b2.h / 2, yPercent: -50});
/* each word scales from a point near the block's centre: origin = 0.9 * (blockCentre - wordCentre) */
var bcx = left + wMax / 2, bcy = (b1.y + b2.y + b2.h) / 2;
var origins = words1.map(function(w){
var b = box(w), wx = b.x + b.w / 2, wy = b.y + b.h / 2;
return (b.w / 2 + .9 * (bcx - wx)).toFixed(1) + 'px ' + (b.h / 2 + .9 * (bcy - wy)).toFixed(1) + 'px';
});
gsap.set(words1, {scale: 0, transformOrigin: function(i){ return origins[i]; }});
var in2 = gsap.utils.toArray(panels[1].querySelectorAll('.js-line, .js-note'));
var in3 = gsap.utils.toArray(panels[2].querySelectorAll('.js-line, .js-note'));
var blk2 = panels[1].querySelectorAll('.js-say, .js-note');
var blk3 = panels[2].querySelectorAll('.js-say, .js-note');
var sr = stage.getBoundingClientRect();
function exitY(el){ var r = el.getBoundingClientRect(); return -Math.max(r.height * 3, r.bottom - sr.top + 40); }
var out2 = Array.prototype.map.call(blk2, exitY), out3 = Array.prototype.map.call(blk3, exitY);
gsap.set(fit, {y: H});
gsap.set(in2.concat(in3), {y: H});
gsap.set(nls, {opacity: 0});
var T2 = 3.8, T3 = 6.9;
var tl = gsap.timeline({
defaults: {ease: 'none'},
onUpdate: draw,
scrollTrigger: {
trigger: sec, start: 'top top', end: '+=660%', pin: true, scrub: .25, anticipatePin: 1,
onUpdate: function(self){
var t = tl.time(), n = t < T2 ? '01' : t < T3 ? '02' : '03';
if(cur.textContent !== n) cur.textContent = n;
bar.style.transform = 'scaleX(' + self.progress.toFixed(4) + ')';
}
}
});
/* statement 1 — enter, burst, settle */
tl.fromTo(fit, {y: H}, {y: 0, duration: 1, ease: 'expo.out'}, 0)
.to(words1, {scale: 1, duration: 1.2, ease: 'expo.inOut', stagger: .05}, .1)
.to(S, {sp: .4, duration: 1.8, ease: 'expo.inOut'}, 0);
if(side){
var dx = left - b2.x;
tl.to(l1[1], {x: dx, duration: .9, ease: 'expo.inOut'}, 1.5)
.fromTo(nls, {x: 80, opacity: 0}, {x: 0, opacity: 1, duration: .9, ease: 'expo.out', stagger: .02}, 1.95);
} else {
tl.fromTo(nls, {yPercent: 100, opacity: 0}, {yPercent: 0, opacity: 1, duration: .9, ease: 'power3.out', stagger: .05}, 1.5);
}
/* exit up + cobalt → mint */
tl.to(fit, {y: -H, duration: 1, ease: 'expo.in'}, 3.2)
.to(stage, {backgroundColor: C.mint, color: C.ink, duration: 1, ease: 'power1.inOut'}, 3.2);
/* statement 2 — rise in, sea window slices open, exit, mint → mist */
tl.to(in2, {y: 0, duration: 1, ease: 'expo.out', stagger: .05}, 4.05)
.to(P, {p: 1, duration: 1.3, onUpdate: function(){ slice.progress(P.p); }}, 4.2)
.fromTo(win, {scale: .82}, {scale: 1.04, duration: 3, ease: 'sine.inOut'}, 4.05)
.fromTo(img, {scale: 1.3}, {scale: 1, duration: 3, ease: 'none'}, 4.05);
blk2.forEach(function(el, i){ tl.to(el, {y: out2[i], duration: .9, ease: 'power3.in'}, 6.4); });
tl.to(win, {y: -H * 1.2, duration: .9, ease: 'power3.in'}, 6.45)
.to(stage, {backgroundColor: C.bg, color: C.ink, duration: 1, ease: 'power1.inOut'}, 6.4);
/* statement 3 — rise in, lines squeeze harder, exit onto mist */
tl.to(in3, {y: 0, duration: 1, ease: 'expo.out', stagger: .05}, 7.2)
.to(S, {sp: .3, duration: 2.8, ease: 'power4.in'}, 7.2);
blk3.forEach(function(el, i){ tl.to(el, {y: out3[i], duration: .9, ease: 'power3.in'}, 9.3); });
tl.to({}, {duration: .3}, 10.2);
}, sec);
draw();
}
function rebuild(){
if(ctx) ctx.revert();
note1.textContent = note1Text;
build();
JV.refresh();
}
/* build once, after the webfonts are in (the fitted type depends on them) */
var built = false;
function first(){ if(built) return; built = true; build(); JV.refresh(); }
if(document.fonts && document.fonts.status !== 'loaded'){ document.fonts.ready.then(first); setTimeout(first, 1500); }
else first();
var rt;
window.addEventListener('resize', function(){
clearTimeout(rt);
rt = setTimeout(function(){
if(window.innerWidth === lastW && Math.abs(window.innerHeight - lastH) < 140) return;
rebuild();
}, 220);
});
if(!img.complete) img.addEventListener('load', function(){ JV.refresh(); }, {once: true});
}
})();
</script>
</section>
<section id="album" class="ja sec" aria-labelledby="ja-title">
<div class="wrap ja-grid">
<!-- LEFT: cover + vinyl -->
<div class="ja-art-col">
<div class="ja-art" data-playing="false">
<div class="ja-disc" aria-hidden="true">
<div class="ja-disc__px">
<div class="ja-disc__spin">
<div class="ja-disc__label"><span>JV</span></div>
</div>
</div>
</div>
<figure class="ja-cover">
<img src="assets/vinyl.jpg" width="868" height="1300" alt="" loading="lazy" decoding="async">
<div class="ja-cover__tint" aria-hidden="true"></div>
<figcaption class="ja-cover__type">
<span class="ja-cover__artist">Juno Vale</span>
<span class="ja-cover__title">Low<br>Tide<br>Radio</span>
<span class="ja-cover__freq" aria-hidden="true">FM 88.4 · LP</span>
</figcaption>
</figure>
</div>
<ul class="ja-facts" aria-label="Release facts">
<li><b>Released</b> 12 Sep 2026</li>
<li><b>12</b> tracks</li>
<li><b>41</b> min</li>
</ul>
</div>
<!-- RIGHT: copy + tracklist + player -->
<div class="ja-list-col">
<div class="ja-head">
<p class="eyebrow">The new album · out now</p>
<h2 id="ja-title" class="display ja-display">Low Tide <span class="hl">Radio</span></h2>
<p class="lede">Twelve songs tracked in a converted boathouse with the windows open, so the tide is on every take. Tap any track for a 30-second listen.</p>
</div>
<div class="ja-player card" aria-label="Preview player">
<div class="ja-player__top">
<div class="ja-player__now">
<span class="ja-player__kicker">Now previewing</span>
<span class="ja-player__name" aria-live="polite">01 · Low Tide Radio</span>
</div>
<div class="ja-player__ctrls">
<button type="button" class="ja-ctl ja-prev" aria-label="Previous track">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 5h2v14H6zM20 5v14L9 12z"/></svg>
</button>
<button type="button" class="ja-ctl ja-play" aria-pressed="false" aria-label="Play preview">
<svg class="ja-ico-play" viewBox="0 0 24 24" aria-hidden="true"><path d="M7 4.5v15L19.5 12z"/></svg>
<svg class="ja-ico-pause" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 4.5h4.2v15H6zM13.8 4.5H18v15h-4.2z"/></svg>
</button>
<button type="button" class="ja-ctl ja-next" aria-label="Next track">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M16 5h2v14h-2zM4 5v14l11-7z"/></svg>
</button>
</div>
</div>
<div class="ja-progress">
<span class="ja-time ja-elapsed">0:00</span>
<div class="ja-bar" role="progressbar" aria-label="Preview progress" aria-valuemin="0" aria-valuemax="30" aria-valuenow="0">
<span class="ja-bar__fill"></span>
</div>
<span class="ja-time ja-total">0:30</span>
</div>
</div>
<ol class="ja-tracks" aria-label="Tracklist"></ol>
<div class="ja-stream">
<span class="ja-stream__label">Listen on</span>
<a class="ja-pill" href="#">Spotify</a>
<a class="ja-pill" href="#">Apple Music</a>
<a class="ja-pill" href="#">Bandcamp</a>
<a class="ja-pill" href="#">YouTube</a>
</div>
</div>
</div>
</section>
<style>
.ja{position:relative;overflow:hidden}
.ja-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.05fr);gap:clamp(40px,6vw,96px);align-items:start}
/* ---------- art ---------- */
.ja-art-col{position:sticky;top:40px}
.ja-art{position:relative;width:100%;max-width:600px;aspect-ratio:1.36/1}
.ja-cover{position:absolute;left:0;top:0;width:73.5%;aspect-ratio:1/1;margin:0;border-radius:14px;overflow:hidden;
background:var(--cobalt);box-shadow:var(--shadow),0 2px 0 rgba(22,18,58,.06);z-index:2}
.ja-cover img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:50% 40%;
filter:grayscale(1) contrast(1.15) brightness(1.05)}
/* motion only: taller image so the scrubbed y:25% -> 0 drift never shows an edge */
.ja-cover.is-drift img{inset:auto 0;top:-34%;height:134%}
.ja-cover__tint{position:absolute;inset:0;background:var(--cobalt);mix-blend-mode:multiply}
.ja-cover__tint::after{content:"";position:absolute;inset:0;
background:linear-gradient(160deg,rgba(63,224,181,.0) 40%,rgba(63,224,181,.35) 100%);mix-blend-mode:screen}
.ja-cover__type{position:absolute;inset:0;display:flex;flex-direction:column;justify-content:space-between;
padding:clamp(16px,2.4vw,28px);color:#fff}
.ja-cover__artist{font-size:12px;font-weight:800;letter-spacing:.24em;text-transform:uppercase}
.ja-cover__title{font-family:var(--display);font-weight:900;text-transform:uppercase;line-height:.86;letter-spacing:-.04em;
font-size:clamp(34px,5.4vw,74px)}
.ja-cover__freq{align-self:flex-start;font-size:11px;font-weight:800;letter-spacing:.18em;text-transform:uppercase;
background:var(--mint);color:var(--ink);padding:5px 10px;border-radius:999px}
/* vinyl disc: an object, cobalt-deep face with ink/cobalt grooves */
.ja-disc{position:absolute;left:0;top:1.5%;width:70.5%;aspect-ratio:1/1;z-index:1;
transform:translateX(12%);transition:transform .9s var(--ease)}
.ja-art[data-playing="true"] .ja-disc,
.ja-art.is-out .ja-disc{transform:translateX(39%)}
.ja-disc__px{position:absolute;inset:0}
.ja-disc__spin{position:absolute;inset:0;border-radius:50%;
background:
conic-gradient(from 20deg,rgba(255,255,255,0) 0deg,rgba(217,222,252,.28) 30deg,rgba(255,255,255,0) 70deg,
rgba(255,255,255,0) 190deg,rgba(217,222,252,.22) 215deg,rgba(255,255,255,0) 250deg),
repeating-radial-gradient(circle at 50% 50%,var(--cobalt-deep) 0 2px,rgba(22,18,58,.9) 2px 3px,var(--cobalt-deep) 3px 5px),
var(--cobalt-deep);
box-shadow:0 24px 48px -26px rgba(22,18,58,.7),inset 0 0 0 6px rgba(22,18,58,.55);
animation:ja-spin 2.2s linear infinite;animation-play-state:paused}
.ja-art[data-playing="true"] .ja-disc__spin{animation-play-state:running}
.ja-disc__label{position:absolute;inset:33%;border-radius:50%;background:var(--mint);display:grid;place-items:center;
box-shadow:0 0 0 3px rgba(22,18,58,.35)}
.ja-disc__label::after{content:"";position:absolute;width:7%;aspect-ratio:1;border-radius:50%;background:var(--bg)}
.ja-disc__label span{font-family:var(--display);font-weight:900;font-size:clamp(11px,1.4vw,18px);color:var(--ink);
transform:translateY(-70%)}
@keyframes ja-spin{to{transform:rotate(360deg)}}
.ja-facts{list-style:none;margin:28px 0 0;padding:0;display:flex;flex-wrap:wrap;gap:10px}
.ja-facts li{background:var(--paper);border:1px solid var(--line);border-radius:999px;padding:8px 16px;font-size:14px;color:var(--mute)}
.ja-facts b{color:var(--ink);font-weight:800}
/* ---------- head ---------- */
.ja-display{font-size:clamp(40px,6vw,88px);margin-top:14px}
/* line masks (built by JS only when motion is on) */
.ja-line{display:block;overflow:hidden;padding-block:.08em;margin-block:-.08em}
.ja-line__in{display:block;will-change:transform}
.ja-head .lede{margin:20px 0 0}
/* ---------- player ---------- */
.ja-player{margin-top:32px;padding:18px 20px;border:1px solid var(--line)}
.ja-player__top{display:flex;align-items:center;justify-content:space-between;gap:16px}
.ja-player__now{display:flex;flex-direction:column;min-width:0}
.ja-player__kicker{font-size:11px;font-weight:800;letter-spacing:.2em;text-transform:uppercase;color:var(--cobalt)}
.ja-player__name{font-weight:800;font-size:17px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ja-player__ctrls{display:flex;align-items:center;gap:8px;flex:none}
.ja-ctl{display:grid;place-items:center;width:42px;height:42px;border-radius:50%;color:var(--ink);
transition:background .2s var(--ease),transform .2s var(--ease)}
.ja-ctl svg{width:18px;height:18px;fill:currentColor}
.ja-ctl:hover{background:var(--cobalt-soft)}
.ja-play{width:54px;height:54px;background:var(--cobalt);color:#fff}
.ja-play:hover{background:var(--cobalt-deep);transform:scale(1.05)}
.ja-play svg{width:22px;height:22px}
.ja-ico-pause{display:none}
.ja-play[aria-pressed="true"] .ja-ico-play{display:none}
.ja-play[aria-pressed="true"] .ja-ico-pause{display:block}
.ja-ctl:focus-visible,.ja-pill:focus-visible{outline:3px solid var(--mint-deep);outline-offset:3px}
.ja-row:focus-visible{outline:3px solid var(--mint-deep);outline-offset:-3px}
.ja-progress{display:flex;align-items:center;gap:12px;margin-top:14px}
.ja-time{font-size:12px;font-weight:700;color:var(--mute);font-variant-numeric:tabular-nums;min-width:32px}
.ja-total{text-align:right}
.ja-bar{position:relative;flex:1;height:6px;border-radius:999px;background:var(--cobalt-soft);overflow:hidden}
.ja-bar__fill{position:absolute;inset:0;background:linear-gradient(90deg,var(--cobalt),var(--mint));
transform-origin:left;transform:scaleX(0)}
/* ---------- tracklist ---------- */
.ja-tracks{list-style:none;margin:20px 0 0;padding:0;border-top:1px solid var(--line)}
.ja-tracks li{border-bottom:1px solid var(--line)}
.ja-row{width:100%;display:grid;grid-template-columns:34px minmax(0,1fr) auto auto;align-items:center;gap:14px;
padding:13px 14px;text-align:left;border-radius:12px;transition:background .2s var(--ease);
position:relative;isolation:isolate;overflow:hidden}
.ja-row:hover{background:rgba(217,222,252,.55)}
/* active highlight: 6 vertical slices that grow (scaleY) from the centre pair outward */
.ja-row__fx{position:absolute;inset:0;display:flex;z-index:-1;pointer-events:none}
.ja-row__fx i{flex:1;margin-inline:-.5px;background:var(--cobalt-soft);transform:scaleY(0);
transition:transform .5s var(--ease)}
.ja-row[aria-pressed="true"] .ja-row__fx i{transform:scaleY(1)}
.ja-row__fx i:nth-child(3),.ja-row__fx i:nth-child(4){transition-delay:0s}
.ja-row__fx i:nth-child(2),.ja-row__fx i:nth-child(5){transition-delay:.07s}
.ja-row__fx i:nth-child(1),.ja-row__fx i:nth-child(6){transition-delay:.14s}
.ja-tracks.is-masked li{overflow:hidden}
.ja-row__n{font-size:13px;font-weight:800;color:var(--mute);font-variant-numeric:tabular-nums}
.ja-row__t{font-weight:700;font-size:16px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ja-row__d{font-size:14px;color:var(--mute);font-variant-numeric:tabular-nums}
.ja-row .eq{color:var(--cobalt);visibility:hidden;width:21px}
.ja-row[aria-pressed="true"] .eq{visibility:visible}
.ja-row[aria-pressed="true"] .ja-row__n,.ja-row[aria-pressed="true"] .ja-row__t{color:var(--cobalt-deep)}
.ja-tracks:not(.is-playing) .eq i{animation-play-state:paused}
/* ---------- streaming ---------- */
.ja-stream{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-top:28px}
.ja-stream__label{font-size:12px;font-weight:800;letter-spacing:.2em;text-transform:uppercase;color:var(--mute);margin-right:4px}
.ja-pill{padding:10px 18px;border-radius:999px;font-weight:800;font-size:14px;background:var(--paper);
box-shadow:inset 0 0 0 1.5px var(--line);transition:background .2s var(--ease),box-shadow .2s var(--ease)}
.ja-pill:hover{background:var(--mint);box-shadow:inset 0 0 0 1.5px var(--mint-deep);color:var(--ink)}
/* ---------- responsive ---------- */
@media (max-width:960px){
.ja-grid{grid-template-columns:minmax(0,1fr)}
.ja-art-col{position:relative;top:auto}
.ja-art{margin-inline:auto}
}
@media (max-width:520px){
.ja-player{padding:14px}
.ja-player__top{flex-direction:column;align-items:stretch}
.ja-player__ctrls{justify-content:center}
.ja-row{grid-template-columns:26px minmax(0,1fr) auto auto;gap:10px;padding:12px 8px}
.ja-row__t{font-size:15px}
.ja-cover__type{padding:14px}
}
@media (prefers-reduced-motion: reduce) and (update: none){
.ja-disc{transition:none}
.ja-disc__spin{animation:none}
.ja-row__fx i{transition:none}
}
</style>
<script>
/* deferred until the page has painted — see JV.later in template.html */
JV.later(function(){
var root=document.getElementById('album'); if(!root) return;
var reduce=matchMedia('(prefers-reduced-motion: reduce) and (update: none)').matches;
var PREVIEW=30;
var TRACKS=[
['Low Tide Radio','3:24'],['Salt in the Speakers','3:51'],['Harbour Lights (Call Me Back)','2:58'],
['Undertow Tuesday','4:12'],['Glass Buoy','3:37'],['Static on the Pier','3:05'],
['Paper Moons','3:46'],['Seafoam Cassette','2:49'],["Lighthouse Keeper's Daughter",'3:33'],
['Tidepool Heart','4:05'],['Night Ferry','3:18'],['Out Past the Breakers','2:52']
];
/* gentle chord roots (Hz) per track for the ambient pad */
var ROOTS=[110,123.47,98,130.81,116.54,103.83,146.83,110,87.31,123.47,98,82.41];
var list=root.querySelector('.ja-tracks'), art=root.querySelector('.ja-art');
var nameEl=root.querySelector('.ja-player__name'), playBtn=root.querySelector('.ja-play');
var fill=root.querySelector('.ja-bar__fill'), bar=root.querySelector('.ja-bar');
var elEl=root.querySelector('.ja-elapsed');
function pad(n){return (n<10?'0':'')+n}
function fmt(s){s=Math.floor(s);return Math.floor(s/60)+':'+pad(s%60)}
var rows=TRACKS.map(function(t,i){
var li=document.createElement('li');
var b=document.createElement('button');
b.type='button';b.className='ja-row';b.setAttribute('aria-pressed','false');
b.setAttribute('aria-label','Track '+(i+1)+': '+t[0]+', '+t[1]);
b.innerHTML='<span class="ja-row__fx" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i></span><span class="ja-row__n">'+pad(i+1)+'</span><span class="ja-row__t"></span>'+
'<span class="eq" aria-hidden="true"><i></i><i></i><i></i><i></i></span><span class="ja-row__d">'+t[1]+'</span>';
b.querySelector('.ja-row__t').textContent=t[0];
b.addEventListener('click',function(){ if(i===cur){toggle()} else {select(i,true)} });
li.appendChild(b);list.appendChild(li);return b;
});
var cur=0, playing=false, elapsed=0, last=0, raf=0;
/* ---------- WebAudio ambient pad (created only after a user click) ---------- */
var ctx=null, master=null, voices=[];
function ensureCtx(){
if(ctx) return ctx;
var AC=window.AudioContext||window.webkitAudioContext; if(!AC) return null;
ctx=new AC(); master=ctx.createGain(); master.gain.value=0;
var lp=ctx.createBiquadFilter(); lp.type='lowpass'; lp.frequency.value=900; lp.Q.value=.4;
master.connect(lp); lp.connect(ctx.destination);
return ctx;
}
function startPad(){
if(!ensureCtx()) return;
if(ctx.state==='suspended') ctx.resume();
stopVoices(0.05);
var r=ROOTS[cur], ratios=[1,1.5,2,2.52], now=ctx.currentTime;
ratios.forEach(function(k,j){
var o=ctx.createOscillator(), g=ctx.createGain(), lfo=ctx.createOscillator(), lg=ctx.createGain();
o.type=j===0?'triangle':'sine'; o.frequency.value=r*k; o.detune.value=(j%2?4:-4);
g.gain.value=j===0?.5:.22;
lfo.frequency.value=.08+j*.05; lg.gain.value=.12; lfo.connect(lg); lg.connect(g.gain);
o.connect(g); g.connect(master); o.start(now); lfo.start(now);
voices.push(o,lfo);
});
master.gain.cancelScheduledValues(now);
master.gain.setValueAtTime(master.gain.value,now);
master.gain.linearRampToValueAtTime(.045,now+1.2);
}
function stopVoices(delay){
if(!ctx) return;
var t=ctx.currentTime+(delay||0);
voices.forEach(function(v){try{v.stop(t)}catch(e){}});
voices=[];
}
function stopPad(){
if(!ctx) return;
var now=ctx.currentTime;
master.gain.cancelScheduledValues(now);
master.gain.setValueAtTime(master.gain.value,now);
master.gain.linearRampToValueAtTime(0,now+.5);
stopVoices(.55);
}
/* ---------- UI ---------- */
function render(){
rows.forEach(function(b,i){b.setAttribute('aria-pressed',i===cur?'true':'false')});
nameEl.textContent=pad(cur+1)+' · '+TRACKS[cur][0];
playBtn.setAttribute('aria-pressed',playing?'true':'false');
playBtn.setAttribute('aria-label',playing?'Pause preview':'Play preview');
art.setAttribute('data-playing',playing?'true':'false');
list.classList.toggle('is-playing',playing);
paint();
}
function paint(){
fill.style.transform='scaleX('+(elapsed/PREVIEW)+')';
elEl.textContent=fmt(elapsed);
bar.setAttribute('aria-valuenow',Math.floor(elapsed));
}
function tick(t){
if(!playing) return;
elapsed+=(t-last)/1000; last=t;
if(elapsed>=PREVIEW){ elapsed=PREVIEW; paint(); select((cur+1)%TRACKS.length,true); return; }
paint(); raf=requestAnimationFrame(tick);
}
function play(){
playing=true; last=performance.now(); cancelAnimationFrame(raf); raf=requestAnimationFrame(tick);
startPad(); render();
}
function pause(){ playing=false; cancelAnimationFrame(raf); stopPad(); render(); }
function toggle(){ playing?pause():play() }
function select(i,autoplay){
cur=(i+TRACKS.length)%TRACKS.length; elapsed=0;
if(autoplay) play(); else render();
}
playBtn.addEventListener('click',toggle);
root.querySelector('.ja-prev').addEventListener('click',function(){ select(elapsed>3?cur:cur-1,playing) });
root.querySelector('.ja-next').addEventListener('click',function(){ select(cur+1,playing) });
document.addEventListener('visibilitychange',function(){ if(document.hidden&&playing) pause() });
render();
/* ---------- scroll motion (off under reduced motion; static layout stays intact) ---------- */
if(!JV.reduce && window.gsap && window.ScrollTrigger){
var cover=root.querySelector('.ja-cover'), coverImg=cover.querySelector('img');
var head=root.querySelector('.ja-head'), title=root.querySelector('.ja-display');
var player=root.querySelector('.ja-player'), stream=root.querySelector('.ja-stream');
/* cover: centre-out sliced reveal + scrubbed image drift */
JV.sliceReveal(cover,{slices:10,corner:2,duration:1.2,ease:'expo.inOut',
scrollTrigger:{trigger:cover,start:'top 75%'}});
cover.classList.add('is-drift');
gsap.fromTo(coverImg,{y:'25%'},{y:'0%',ease:'none',
scrollTrigger:{trigger:cover,start:'top 100%',end:'bottom 50%',scrub:.75}});
/* vinyl: small scrubbed slide-out parallax on its own wrapper, so the CSS play offset + spin keep working */
gsap.fromTo(root.querySelector('.ja-disc__px'),{xPercent:-6},{xPercent:8,ease:'none',
scrollTrigger:{trigger:root,start:'top bottom',end:'bottom top',scrub:.75}});
gsap.from(root.querySelectorAll('.ja-facts li'),{yPercent:100,opacity:0,ease:'power3.out',stagger:.05,
scrollTrigger:{trigger:root.querySelector('.ja-facts'),start:'top 100%',end:'top 80%',scrub:.75}});
/* tracklist rows rise out of their <li> masks */
list.classList.add('is-masked');
gsap.from(rows,{yPercent:175,ease:'power3.out',stagger:.05,
scrollTrigger:{trigger:list,start:'top 95%',end:'bottom 75%',scrub:.75}});
/* player card + streaming pills follow slightly behind */
gsap.from(player,{yPercent:100,opacity:0,ease:'power3.out',
scrollTrigger:{trigger:player,start:'top 90%',end:'top 60%',scrub:.75}});
gsap.from(stream.children,{yPercent:100,opacity:0,ease:'power3.out',stagger:.05,
scrollTrigger:{trigger:stream,start:'top 98%',end:'top 78%',scrub:.75}});
/* heading: split into measured lines, each wrapped in an overflow-hidden mask */
function splitLines(el){
var words=[];
Array.prototype.slice.call(el.childNodes).forEach(function(n){
var cls=n.nodeType===1?n.className:'';
(n.textContent||'').split(/\s+/).forEach(function(w){ if(w) words.push({w:w,cls:cls}); });
});
el.setAttribute('aria-label',el.textContent.replace(/\s+/g,' ').trim());
el.innerHTML='';
var spans=words.map(function(o,k){
var sp=document.createElement('span'); if(o.cls) sp.className=o.cls; sp.textContent=o.w;
el.appendChild(sp); if(k<words.length-1) el.appendChild(document.createTextNode(' '));
return sp;
});
var lines=[], top=null;
spans.forEach(function(sp){
if(top===null||Math.abs(sp.offsetTop-top)>2){ lines.push([]); top=sp.offsetTop; }
lines[lines.length-1].push(sp);
});
el.innerHTML='';
return lines.map(function(ln){
var m=document.createElement('span'); m.className='ja-line'; m.setAttribute('aria-hidden','true');
var inner=document.createElement('span'); inner.className='ja-line__in';
ln.forEach(function(sp,k){ if(k) inner.appendChild(document.createTextNode(' ')); inner.appendChild(sp); });
m.appendChild(inner); el.appendChild(m); return inner;
});
}
var ready=(document.fonts&&document.fonts.ready)?document.fonts.ready:Promise.resolve();
ready.then(function(){
var lines=splitLines(title);
var tl=gsap.timeline({scrollTrigger:{trigger:head,start:'top 100%',end:'bottom 50%',scrub:.75}});
tl.from(head.querySelector('.eyebrow'),{yPercent:100,opacity:0,ease:'power3.out'},0)
.from(lines,{yPercent:100,ease:'power3.out',stagger:.05},.05)
.from(head.querySelector('.lede'),{yPercent:40,opacity:0,ease:'power3.out'},.2);
JV.refresh();
});
}
});
</script>
<section id="tour" class="jt sec" aria-labelledby="jt-title">
<style>
.jt{position:relative;background:var(--bg);overflow:hidden}
.jt-head{position:relative;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:clamp(24px,4vw,64px);align-items:end;margin-bottom:clamp(36px,5vw,64px)}
.jt-head > div:not(.jt-tags){position:relative;z-index:1}
.jt-head .eyebrow{display:inline-flex;align-items:center;gap:10px;margin-bottom:18px}
.jt-title{font-size:clamp(40px,8.4vw,132px)}
.jt-title .jt-yr{display:inline-block;color:var(--cobalt);-webkit-text-stroke:0}
/* heading line masks */
.jt-ln{display:block;overflow:hidden;padding-block:.05em .08em;margin-block:-.05em -.08em}
.jt-lni{display:block}
/* floating sticker band behind the heading (clipped, decorative) */
.jt-tags{position:absolute;z-index:0;top:-18%;bottom:-18%;left:calc(-1 * var(--gutter));right:calc(-1 * var(--gutter));
overflow:hidden;pointer-events:none;opacity:.5}
.jt-tag{position:absolute;left:var(--x);top:var(--y);transform:rotate(var(--r));white-space:nowrap;
padding:8px 15px;border-radius:999px;font-size:12px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;line-height:1;
background:var(--paper);color:var(--ink);border:1.5px solid var(--line)}
.jt-tag--c{background:var(--cobalt);color:#fff;border-color:var(--cobalt-deep)}
.jt-tag--m{background:var(--mint);color:var(--ink);border-color:var(--mint-deep)}
.jt-tags.is-live .jt-tag{left:0;top:0;will-change:transform}
.jt-bar,.jt-list,.jt-foot{position:relative;z-index:1}
.jt-aside{display:flex;flex-direction:column;align-items:flex-start;gap:20px;max-width:340px}
.jt-aside .lede{margin:0}
.jt-bar{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:16px;padding-bottom:22px}
.jt-chips{display:flex;flex-wrap:wrap;gap:10px}
.jt-chip{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:999px;font-weight:800;font-size:14px;
color:var(--ink);background:var(--paper);box-shadow:inset 0 0 0 1.5px var(--line);
transition:background .25s var(--ease),color .25s var(--ease),box-shadow .25s var(--ease)}
.jt-chip small{font-size:11px;font-weight:800;min-width:22px;height:22px;display:inline-grid;place-items:center;border-radius:999px;
background:var(--cobalt-soft);color:var(--cobalt);transition:inherit}
.jt-chip:hover{box-shadow:inset 0 0 0 1.5px var(--cobalt)}
.jt-chip[aria-pressed="true"]{background:var(--cobalt);color:#fff;box-shadow:inset 0 0 0 1.5px var(--cobalt)}
.jt-chip[aria-pressed="true"] small{background:rgba(255,255,255,.2);color:#fff}
.jt-chip:focus-visible,.jt-btn:focus-visible{outline:2px solid var(--cobalt);outline-offset:3px}
.jt-count{font-size:13px;font-weight:700;color:var(--mute);letter-spacing:.04em}
.jt-count b{color:var(--ink)}
.jt-list{list-style:none;margin:0;padding:0;border-bottom:1.5px solid var(--line)}
.jt-item{display:grid;grid-template-rows:1fr;opacity:1;transition:grid-template-rows .55s var(--ease),opacity .4s var(--ease)}
.jt-item.is-out{grid-template-rows:0fr;opacity:0}
.jt-clip{min-height:0;overflow:hidden}
.jt-row{position:relative;display:grid;grid-template-columns:clamp(120px,13vw,190px) minmax(0,1.25fr) minmax(0,1fr) auto;
align-items:center;gap:clamp(16px,2.4vw,36px);padding:clamp(20px,2.4vw,30px) clamp(14px,1.6vw,26px);
border-top:1.5px solid var(--line);border-radius:0;isolation:isolate;
transition:background .35s var(--ease),border-color .35s var(--ease),border-radius .35s var(--ease),color .35s var(--ease)}
/* hover fill: 6 vertical cobalt slices grow from the centre outward (injected by script) */
.jt-fill{position:absolute;inset:-1.5px 0 0 0;z-index:-1;display:flex;border-radius:22px;overflow:hidden;pointer-events:none}
.jt-fill i{flex:1;height:100%;background:var(--cobalt);transform:scaleY(0);transition:transform .5s var(--ease)}
.jt-fill i:nth-child(3),.jt-fill i:nth-child(4){transition-delay:0s}
.jt-fill i:nth-child(2),.jt-fill i:nth-child(5){transition-delay:.06s}
.jt-fill i:nth-child(1),.jt-fill i:nth-child(6){transition-delay:.12s}
.jt-date{display:flex;align-items:center;gap:12px}
.jt-day{font-family:var(--display);font-weight:900;font-size:clamp(40px,5vw,72px);line-height:.85;letter-spacing:-.05em;color:var(--cobalt);transition:color .35s var(--ease)}
.jt-mon{display:flex;flex-direction:column;font-size:12px;font-weight:800;letter-spacing:.18em;text-transform:uppercase;line-height:1.35}
.jt-mon span:last-child{color:var(--mute);transition:color .35s var(--ease)}
.jt-city{margin:0;font-family:var(--display);font-weight:700;font-size:clamp(22px,2.7vw,40px);line-height:1;letter-spacing:-.03em;text-transform:uppercase;overflow-wrap:anywhere}
.jt-country{display:block;margin-top:8px;font-family:var(--body);font-size:12px;font-weight:800;letter-spacing:.18em;color:var(--mute);transition:color .35s var(--ease)}
.jt-venue{font-size:clamp(15px,1.2vw,17px);font-weight:500;color:var(--mute);transition:color .35s var(--ease)}
.jt-venue strong{display:block;color:var(--ink);font-weight:700;transition:color .35s var(--ease)}
.jt-act{display:flex;align-items:center;justify-content:flex-end;gap:14px}
.jt-status{display:inline-flex;align-items:center;gap:8px;white-space:nowrap;padding:7px 12px;border-radius:999px;font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;
transition:background .35s var(--ease),color .35s var(--ease)}
.jt-status::before{content:"";width:7px;height:7px;border-radius:50%;background:currentColor}
.jt-status--on{background:var(--mint-soft);color:var(--ink)}
.jt-status--on::before{background:var(--mint-deep)}
.jt-status--low{background:var(--cobalt-soft);color:var(--cobalt)}
.jt-status--out{background:transparent;color:var(--mute);box-shadow:inset 0 0 0 1.5px var(--line)}
.jt-btn{padding:12px 22px;font-size:14px;white-space:nowrap}
.jt-btn:disabled{background:transparent;color:var(--mute);box-shadow:inset 0 0 0 1.5px var(--line);cursor:not-allowed;transform:none;text-decoration:line-through}
/* sold out: dimmed */
.jt-row.is-sold > *{opacity:.5}
.jt-row.is-sold .jt-day{color:var(--mute)}
/* desktop pointer hover: cobalt fill + white type */
@media (hover:hover) and (pointer:fine){
.jt-list:hover .jt-row{cursor:default}
.jt-row:not(.is-sold):hover{border-color:transparent;border-radius:22px;color:#fff}
.jt-row:not(.is-sold):not(.has-fill):hover{background:var(--cobalt)}
.jt-row.has-fill:not(.is-sold):hover .jt-fill i{transform:scaleY(1)}
.jt-item:has(.jt-row:not(.is-sold):hover) + .jt-item .jt-row{border-top-color:transparent}
.jt-row:not(.is-sold):hover .jt-day,
.jt-row:not(.is-sold):hover .jt-venue strong{color:#fff}
.jt-row:not(.is-sold):hover .jt-mon span:last-child,
.jt-row:not(.is-sold):hover .jt-country,
.jt-row:not(.is-sold):hover .jt-venue{color:rgba(255,255,255,.78)}
.jt-row:not(.is-sold):hover .jt-status{background:rgba(255,255,255,.16);color:#fff;box-shadow:none}
.jt-row:not(.is-sold):hover .jt-status::before{background:var(--mint)}
.jt-row:not(.is-sold):hover .jt-btn{background:var(--mint);color:var(--ink)}
.jt-row:not(.is-sold):hover .jt-btn:hover{background:var(--mint-deep)}
.jt-row.is-sold:hover{background:rgba(217,222,252,.45)}
}
/* cursor-following preview (desktop pointer only, toggled by script) */
.jt-float{position:fixed;left:0;top:0;z-index:40;width:clamp(200px,17vw,260px);pointer-events:none;opacity:0;visibility:hidden;
border-radius:18px;overflow:hidden;box-shadow:var(--shadow);background:var(--cobalt-soft)}
.jt-float img{width:100%;height:auto;aspect-ratio:4/5;object-fit:cover;position:absolute;inset:0;opacity:0;transition:opacity .3s var(--ease)}
.jt-float img:first-child{position:relative}
.jt-float img.is-on{opacity:1}
.jt-float-tag{position:absolute;left:12px;bottom:12px;padding:6px 11px;border-radius:999px;background:var(--mint);color:var(--ink);font-size:11px;font-weight:800;letter-spacing:.14em;text-transform:uppercase}
.jt-foot{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:16px;margin-top:28px}
.jt-note{margin:0;font-size:14px;color:var(--mute);max-width:56ch}
@media (max-width:1080px){
.jt-row{grid-template-columns:clamp(110px,14vw,150px) minmax(0,1fr) auto}
.jt-date{grid-column:1;grid-row:1 / span 2}
.jt-city{grid-column:2;grid-row:1}
.jt-venue{grid-column:2;grid-row:2;margin-top:-6px}
.jt-act{grid-column:3;grid-row:1 / span 2}
}
@media (max-width:860px){
.jt-head{grid-template-columns:1fr}
.jt-aside{max-width:none}
}
@media (max-width:640px){
.jt-row{grid-template-columns:auto minmax(0,1fr);row-gap:10px;column-gap:18px;padding:22px 4px}
.jt-date{flex-direction:column;align-items:flex-start;gap:6px;grid-row:1 / span 2}
.jt-day{font-size:44px}
.jt-mon{flex-direction:row;gap:6px;font-size:11px;letter-spacing:.14em}
.jt-venue{grid-column:2;grid-row:2;margin-top:0;font-size:14px}
.jt-act{grid-column:1 / -1;grid-row:3;justify-content:space-between;margin-top:6px}
.jt-btn{padding:11px 20px}
.jt-chip{padding:9px 14px;font-size:13px}
.jt-foot .btn{width:100%;justify-content:center}
}
@media (prefers-reduced-motion: reduce) and (update: none){
.jt-item,.jt-row,.jt-chip,.jt-float img,.jt-fill i{transition:none}
}
</style>
<div class="wrap">
<header class="jt-head">
<div class="jt-tags" aria-hidden="true">
<span class="jt-tag jt-tag--c" style="--x:4%;--y:8%;--r:-14deg">Lisbon</span>
<span class="jt-tag" style="--x:30%;--y:2%;--r:9deg">Paris</span>
<span class="jt-tag jt-tag--m" style="--x:58%;--y:6%;--r:-22deg">Sold out</span>
<span class="jt-tag jt-tag--c" style="--x:82%;--y:12%;--r:18deg">Berlin</span>
<span class="jt-tag jt-tag--m" style="--x:14%;--y:34%;--r:24deg">London</span>
<span class="jt-tag" style="--x:46%;--y:30%;--r:-8deg">Low Tide Radio</span>
<span class="jt-tag jt-tag--c" style="--x:74%;--y:38%;--r:-26deg">Toronto</span>
<span class="jt-tag" style="--x:2%;--y:62%;--r:12deg">New York</span>
<span class="jt-tag jt-tag--m" style="--x:36%;--y:58%;--r:-18deg">New date</span>
<span class="jt-tag jt-tag--c" style="--x:62%;--y:66%;--r:28deg">Los Angeles</span>
<span class="jt-tag" style="--x:86%;--y:60%;--r:-12deg">Tokyo</span>
<span class="jt-tag jt-tag--m" style="--x:20%;--y:86%;--r:-4deg">Seoul</span>
<span class="jt-tag jt-tag--c" style="--x:50%;--y:88%;--r:16deg">Sydney</span>
<span class="jt-tag" style="--x:78%;--y:84%;--r:-20deg">Auckland</span>
</div>
<div>
<p class="eyebrow"><span class="eq" aria-hidden="true"><i></i><i></i><i></i><i></i></span> On the road</p>
<h2 class="display jt-title" id="jt-title"><span class="jt-ln"><span class="jt-lni">Low Tide Radio</span></span> <span class="jt-ln"><span class="jt-lni hl">World Tour</span></span> <span class="jt-ln"><span class="jt-lni"><span class="jt-yr">26/27</span></span></span></h2>
</div>
<div class="jt-aside">
<p class="lede">Eleven rooms, three continents, one long broadcast. Juno brings the whole record to the stage with a four-piece band and a lighthouse rig that sweeps the crowd.</p>
<a class="btn btn--ghost" href="#join">Get notified about new dates <span aria-hidden="true">→</span></a>
</div>
</header>
<div class="jt-bar" data-reveal>
<div class="jt-chips" role="group" aria-label="Filter shows by region">
<button class="jt-chip" type="button" data-filter="all" aria-pressed="true">All <small>11</small></button>
<button class="jt-chip" type="button" data-filter="eu" aria-pressed="false">Europe <small>4</small></button>
<button class="jt-chip" type="button" data-filter="na" aria-pressed="false">North America <small>3</small></button>
<button class="jt-chip" type="button" data-filter="ap" aria-pressed="false">Asia-Pacific <small>4</small></button>
</div>
<p class="jt-count" aria-live="polite"><b data-jt-count>11</b> shows · Oct 2026 – Mar 2027</p>
</div>
<ul class="jt-list">
<li class="jt-item" data-region="eu"><div class="jt-clip"><article class="jt-row">
<div class="jt-date"><span class="jt-day">09</span><span class="jt-mon"><span>Oct</span><span>2026</span></span></div>
<h3 class="jt-city">Lisbon<span class="jt-country">Portugal</span></h3>
<p class="jt-venue"><strong>Doca Velha Pavilion</strong>Opening night</p>
<div class="jt-act"><span class="jt-status jt-status--on">Tickets</span><button class="btn jt-btn" type="button" aria-label="Tickets for Lisbon, 9 October">Tickets</button></div>
</article></div></li>
<li class="jt-item" data-region="eu"><div class="jt-clip"><article class="jt-row is-sold">
<div class="jt-date"><span class="jt-day">14</span><span class="jt-mon"><span>Oct</span><span>2026</span></span></div>
<h3 class="jt-city">Paris<span class="jt-country">France</span></h3>
<p class="jt-venue"><strong>Salle des Marées</strong>With Oko Lane</p>
<div class="jt-act"><span class="jt-status jt-status--out">Sold out</span><button class="btn jt-btn" type="button" disabled aria-label="Paris, 14 October is sold out">Sold out</button></div>
</article></div></li>
<li class="jt-item" data-region="eu"><div class="jt-clip"><article class="jt-row">
<div class="jt-date"><span class="jt-day">19</span><span class="jt-mon"><span>Oct</span><span>2026</span></span></div>
<h3 class="jt-city">Berlin<span class="jt-country">Germany</span></h3>
<p class="jt-venue"><strong>Spreeufer Halle</strong>With Oko Lane</p>
<div class="jt-act"><span class="jt-status jt-status--low">Few left</span><button class="btn jt-btn" type="button" aria-label="Tickets for Berlin, 19 October">Tickets</button></div>
</article></div></li>
<li class="jt-item" data-region="eu"><div class="jt-clip"><article class="jt-row is-sold">
<div class="jt-date"><span class="jt-day">24</span><span class="jt-mon"><span>Oct</span><span>2026</span></span></div>
<h3 class="jt-city">London<span class="jt-country">United Kingdom</span></h3>
<p class="jt-venue"><strong>Tidewater Hall</strong>Two sets, full album</p>
<div class="jt-act"><span class="jt-status jt-status--out">Sold out</span><button class="btn jt-btn" type="button" disabled aria-label="London, 24 October is sold out">Sold out</button></div>
</article></div></li>
<li class="jt-item" data-region="na"><div class="jt-clip"><article class="jt-row">
<div class="jt-date"><span class="jt-day">13</span><span class="jt-mon"><span>Nov</span><span>2026</span></span></div>
<h3 class="jt-city">Toronto<span class="jt-country">Canada</span></h3>
<p class="jt-venue"><strong>Queens Quay Signal Room</strong>With Harbour Static</p>
<div class="jt-act"><span class="jt-status jt-status--low">Few left</span><button class="btn jt-btn" type="button" aria-label="Tickets for Toronto, 13 November">Tickets</button></div>
</article></div></li>
<li class="jt-item" data-region="na"><div class="jt-clip"><article class="jt-row is-sold">
<div class="jt-date"><span class="jt-day">18</span><span class="jt-mon"><span>Nov</span><span>2026</span></span></div>
<h3 class="jt-city">New York<span class="jt-country">United States</span></h3>
<p class="jt-venue"><strong>Low Water Ballroom</strong>Brooklyn</p>
<div class="jt-act"><span class="jt-status jt-status--out">Sold out</span><button class="btn jt-btn" type="button" disabled aria-label="New York, 18 November is sold out">Sold out</button></div>
</article></div></li>
<li class="jt-item" data-region="na"><div class="jt-clip"><article class="jt-row">
<div class="jt-date"><span class="jt-day">27</span><span class="jt-mon"><span>Nov</span><span>2026</span></span></div>
<h3 class="jt-city">Los Angeles<span class="jt-country">United States</span></h3>
<p class="jt-venue"><strong>Echo Basin Theatre</strong>With Harbour Static</p>
<div class="jt-act"><span class="jt-status jt-status--on">Tickets</span><button class="btn jt-btn" type="button" aria-label="Tickets for Los Angeles, 27 November">Tickets</button></div>
</article></div></li>
<li class="jt-item" data-region="ap"><div class="jt-clip"><article class="jt-row">
<div class="jt-date"><span class="jt-day">22</span><span class="jt-mon"><span>Jan</span><span>2027</span></span></div>
<h3 class="jt-city">Tokyo<span class="jt-country">Japan</span></h3>
<p class="jt-venue"><strong>Undertow Hall Shibuya</strong>Asia opener</p>
<div class="jt-act"><span class="jt-status jt-status--low">Few left</span><button class="btn jt-btn" type="button" aria-label="Tickets for Tokyo, 22 January">Tickets</button></div>
</article></div></li>
<li class="jt-item" data-region="ap"><div class="jt-clip"><article class="jt-row">
<div class="jt-date"><span class="jt-day">29</span><span class="jt-mon"><span>Jan</span><span>2027</span></span></div>
<h3 class="jt-city">Seoul<span class="jt-country">South Korea</span></h3>
<p class="jt-venue"><strong>Hangang Radio Stage</strong>With Mirae Sun</p>
<div class="jt-act"><span class="jt-status jt-status--on">Tickets</span><button class="btn jt-btn" type="button" aria-label="Tickets for Seoul, 29 January">Tickets</button></div>
</article></div></li>
<li class="jt-item" data-region="ap"><div class="jt-clip"><article class="jt-row is-sold">
<div class="jt-date"><span class="jt-day">19</span><span class="jt-mon"><span>Feb</span><span>2027</span></span></div>
<h3 class="jt-city">Sydney<span class="jt-country">Australia</span></h3>
<p class="jt-venue"><strong>Bayside Wireless Hall</strong>With Mirae Sun</p>
<div class="jt-act"><span class="jt-status jt-status--out">Sold out</span><button class="btn jt-btn" type="button" disabled aria-label="Sydney, 19 February is sold out">Sold out</button></div>
</article></div></li>
<li class="jt-item" data-region="ap"><div class="jt-clip"><article class="jt-row">
<div class="jt-date"><span class="jt-day">06</span><span class="jt-mon"><span>Mar</span><span>2027</span></span></div>
<h3 class="jt-city">Auckland<span class="jt-country">New Zealand</span></h3>
<p class="jt-venue"><strong>Wharf Lantern Room</strong>Final night</p>
<div class="jt-act"><span class="jt-status jt-status--on">Tickets</span><button class="btn jt-btn" type="button" aria-label="Tickets for Auckland, 6 March">Tickets</button></div>
</article></div></li>
</ul>
<div class="jt-foot">
<p class="jt-note">Sold-out nights sometimes open a small resale window the week of the show. Join the list and you hear first.</p>
<a class="btn btn--ghost" href="#join">Get notified about new dates <span aria-hidden="true">→</span></a>
</div>
</div>
<div class="jt-float" aria-hidden="true">
<img src="assets/crowd.jpg" alt="" width="1880" height="1253" loading="lazy" decoding="async" class="is-on">
<img src="assets/live.jpg" alt="" width="867" height="1300" loading="lazy" decoding="async">
<span class="jt-float-tag">Live</span>
</div>
<script>
/* deferred until the page has painted — see JV.later in template.html */
JV.later(function(){
var root = document.getElementById('tour');
if(!root) return;
var reduce = matchMedia('(prefers-reduced-motion: reduce) and (update: none)').matches;
var items = [].slice.call(root.querySelectorAll('.jt-item'));
var chips = [].slice.call(root.querySelectorAll('.jt-chip'));
var countEl = root.querySelector('[data-jt-count]');
/* ---- region filter: grid-rows collapse (CSS transition) ---- */
function setHidden(item, hide){
item.classList.toggle('is-out', hide);
if(hide){ item.setAttribute('aria-hidden','true'); item.inert = true; }
else { item.removeAttribute('aria-hidden'); item.inert = false; }
}
chips.forEach(function(chip){
chip.addEventListener('click', function(){
var f = chip.getAttribute('data-filter'), shown = 0;
chips.forEach(function(c){ c.setAttribute('aria-pressed', c === chip ? 'true' : 'false'); });
items.forEach(function(it){
var match = f === 'all' || it.getAttribute('data-region') === f;
setHidden(it, !match);
if(match) shown++;
});
if(countEl) countEl.textContent = shown;
if(window.JV) JV.refresh();
});
});
/* ---- row hover fill: inject 6 slices per row (CSS does the centre-out wipe) ---- */
var rows = [].slice.call(root.querySelectorAll('.jt-row'));
rows.forEach(function(row){
var fill = document.createElement('span');
fill.className = 'jt-fill'; fill.setAttribute('aria-hidden','true');
fill.innerHTML = '<i></i><i></i><i></i><i></i><i></i><i></i>';
row.prepend(fill); row.classList.add('has-fill');
});
var fine = matchMedia('(hover: hover) and (pointer: fine)');
var float = root.querySelector('.jt-float');
var imgs = float ? float.querySelectorAll('img') : [];
var list = root.querySelector('.jt-list');
var head = root.querySelector('.jt-head');
var band = root.querySelector('.jt-tags');
if(!float || !list || typeof gsap === 'undefined') return;
var canST = typeof ScrollTrigger !== 'undefined';
/* ---- scroll motion (scrubbed) ---- */
if(!reduce && canST){
/* heading lines rise out of their masks; eyebrow + aside follow */
gsap.timeline({scrollTrigger:{trigger:head, start:'top 90%', end:'top 40%', scrub:.75}})
.from(root.querySelectorAll('.jt-lni'), {yPercent:100, ease:'power2.out', stagger:.12, duration:1})
.from(root.querySelectorAll('.jt-head .eyebrow, .jt-aside'), {y:40, autoAlpha:0, ease:'power2.out', stagger:.1, duration:.8}, .1);
/* rows rise from 175% inside their .jt-clip masks */
gsap.from(rows, {yPercent:175, ease:'power2.out', stagger:.05,
scrollTrigger:{trigger:list, start:'top 92%', end:'top 35%', scrub:.75}});
/* whole sticker band: scrubbed parallax */
if(band) gsap.fromTo(band, {yPercent:150}, {yPercent:-150, ease:'none',
scrollTrigger:{trigger:head, start:'top 100%', end:'bottom 0%', scrub:.75}});
}
/* ---- floating stickers: one gsap.ticker loop, paused off-screen ---- */
if(!reduce && band){
var R = gsap.utils.random, W = 0, H = 0, running = false;
var tags = [].slice.call(band.querySelectorAll('.jt-tag')).map(function(el){
return {el:el, x:0, y:0, w:0, h:0,
vy:R(16, 40), /* px per second upward */
base:R(-30, 30), /* resting rotation */
drift:R(3, 9), rf:R(.15, .45), /* rotation drift */
amp:R(6, 18), sf:R(.25, .7), /* horizontal sway */
ph:R(0, 6.283)};
});
band.classList.add('is-live');
function measure(){
W = band.clientWidth; H = band.clientHeight;
tags.forEach(function(t){
t.w = t.el.offsetWidth; t.h = t.el.offsetHeight;
t.x = Math.min(t.x, Math.max(0, W - t.w));
});
}
measure();
tags.forEach(function(t){ t.x = Math.random() * Math.max(0, W - t.w); t.y = Math.random() * H; });
function tick(time, dt){
var d = Math.min(dt, 60) / 1000;
for(var i = 0; i < tags.length; i++){
var t = tags[i];
t.y -= t.vy * d;
if(t.y < -t.h - 60){ t.y = H + 20 + Math.random() * 40; t.x = Math.random() * Math.max(0, W - t.w); }
var sx = t.x + Math.sin(time * t.sf + t.ph) * t.amp;
var r = t.base + Math.sin(time * t.rf + t.ph) * t.drift;
t.el.style.transform = 'translate3d(' + sx.toFixed(1) + 'px,' + t.y.toFixed(1) + 'px,0) rotate(' + r.toFixed(2) + 'deg)';
}
}
tick(0, 0);
function run(on){
if(on === running) return; running = on;
if(on) gsap.ticker.add(tick); else gsap.ticker.remove(tick);
}
if('IntersectionObserver' in window){
new IntersectionObserver(function(en){ run(en[0].isIntersecting); }).observe(root);
} else run(true);
if('ResizeObserver' in window) new ResizeObserver(measure).observe(band);
else addEventListener('resize', measure);
}
/* ---- cursor-follow preview: desktop fine pointer only ---- */
gsap.set(float, {xPercent:-50, yPercent:-50, scale:.6, rotate:-6});
var moveX = reduce ? function(v){ gsap.set(float,{x:v}); } : gsap.quickTo(float, 'x', {duration:.55, ease:'power3.out'});
var moveY = reduce ? function(v){ gsap.set(float,{y:v}); } : gsap.quickTo(float, 'y', {duration:.55, ease:'power3.out'});
var visible = false, lastX = 0, lastRow = -1, revealTl = null;
/* banded pixel-corner reveal, built once then restarted on each new row */
function sliceIn(){
if(reduce || !window.JV || !JV.sliceReveal) return;
if(!revealTl) revealTl = JV.sliceReveal(float, {slices:10, corner:2, duration:.6, spread:.2});
else revealTl.restart();
}
function show(){
if(visible) return; visible = true;
if(reduce){ gsap.set(float, {autoAlpha:1, scale:1, rotate:0}); return; }
gsap.to(float, {autoAlpha:1, duration:.08, overwrite:'auto'});
gsap.to(float, {scale:1, rotate:0, duration:.5, ease:'power3.out', overwrite:'auto'});
}
function hide(){
if(!visible) return; visible = false; lastRow = -1;
gsap.to(float, {autoAlpha:0, scale:.6, rotate:-6, duration:reduce ? 0 : .3, ease:'power2.in', overwrite:'auto'});
}
rows.forEach(function(row, i){
row.addEventListener('mouseenter', function(e){
if(!fine.matches) return;
imgs[0].classList.toggle('is-on', i % 2 === 0);
imgs[1].classList.toggle('is-on', i % 2 === 1);
if(!visible){ gsap.set(float, {x:e.clientX, y:e.clientY}); moveX(e.clientX); moveY(e.clientY); }
show();
if(i !== lastRow){ lastRow = i; sliceIn(); }
});
});
list.addEventListener('mousemove', function(e){
if(!fine.matches) return;
moveX(e.clientX); moveY(e.clientY);
if(!reduce){
var dx = e.clientX - lastX; lastX = e.clientX;
gsap.to(float, {rotate:gsap.utils.clamp(-10, 10, dx * .6), duration:.5, ease:'power2.out', overwrite:'auto'});
}
});
list.addEventListener('mouseleave', hide);
window.addEventListener('scroll', function(){ if(visible && !list.matches(':hover')) hide(); }, {passive:true});
fine.addEventListener && fine.addEventListener('change', function(){ if(!fine.matches) hide(); });
});
</script>
</section>
<section id="merch" class="jm-merch sec" aria-labelledby="jm-title">
<style>
.jm-merch{position:relative;overflow:hidden}
.jm-merch .jm-inner{display:flex;flex-direction:column;gap:clamp(64px,8vw,120px)}
/* ---------- studio band ---------- */
.jm-studio{position:relative}
.jm-studio-top{display:flex;align-items:flex-end;justify-content:space-between;gap:24px;flex-wrap:wrap;margin-bottom:clamp(24px,3vw,40px)}
.jm-studio-top .display{font-size:clamp(30px,5vw,68px)}
.jm-frame{position:relative;border-radius:clamp(20px,2.4vw,32px);overflow:hidden;background:var(--paper);box-shadow:var(--shadow);aspect-ratio:1880/1000}
.jm-drift{position:absolute;inset:0}
.jm-frame img{width:100%;height:100%;object-fit:cover;object-position:50% 35%;transform:scale(1.001);transition:transform 1.2s var(--ease)}
.jm-frame:hover img{transform:scale(1.04)}
.jm-frame::after{content:"";position:absolute;inset:0;pointer-events:none;
background:linear-gradient(90deg,rgba(36,56,232,.18),rgba(36,56,232,0) 45%)}
.jm-quote{position:absolute;left:clamp(16px,3vw,40px);bottom:clamp(16px,3vw,40px);z-index:2;max-width:440px;
padding:clamp(20px,2.4vw,32px);border-radius:22px;background:rgba(255,255,255,.92);
-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);box-shadow:var(--shadow);margin:0}
.jm-quote blockquote{margin:0;font-family:var(--display);font-weight:700;font-size:clamp(16px,1.5vw,21px);line-height:1.3;letter-spacing:-.01em;color:var(--ink)}
.jm-quote blockquote::before{content:"\201C";display:block;font-size:2.6em;line-height:.6;height:.42em;color:var(--cobalt)}
.jm-quote figcaption{margin-top:14px;display:flex;align-items:center;gap:10px;font-size:13px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:var(--mute)}
.jm-quote figcaption .eq{color:var(--cobalt)}
.jm-play{position:absolute;right:clamp(16px,3vw,40px);top:clamp(16px,3vw,40px);z-index:2;
display:inline-flex;align-items:center;gap:12px;padding:8px 22px 8px 8px;border-radius:999px;
background:var(--cobalt);color:#fff;font-weight:800;font-size:15px;box-shadow:var(--shadow);
transition:background .25s var(--ease),transform .25s var(--ease)}
.jm-play:hover{background:var(--cobalt-deep);transform:translateY(-2px)}
.jm-play:focus-visible,.jm-add:focus-visible,.jm-bag:focus-visible{outline:3px solid var(--mint);outline-offset:3px}
.jm-play-ic{position:relative;width:44px;height:44px;border-radius:50%;background:var(--mint);display:grid;place-items:center;flex:none}
.jm-play-ic::before{content:"";width:0;height:0;margin-left:4px;border-style:solid;border-width:8px 0 8px 13px;border-color:transparent transparent transparent var(--ink)}
.jm-play-ic::after{content:"";position:absolute;inset:-4px;border-radius:50%;border:2px solid rgba(63,224,181,.7);animation:jm-pulse 2s var(--ease) infinite}
@keyframes jm-pulse{0%{transform:scale(.9);opacity:1}100%{transform:scale(1.35);opacity:0}}
/* ---------- store ---------- */
.jm-store-head{display:flex;align-items:flex-end;justify-content:space-between;gap:24px;flex-wrap:wrap;margin-bottom:clamp(28px,3.5vw,48px)}
.jm-store-head .display{font-size:clamp(34px,6vw,84px)}
.jm-store-head .lede{margin:16px 0 0}
.jm-bag{position:relative;display:inline-flex;align-items:center;gap:10px;padding:12px 20px 12px 16px;border-radius:999px;
background:var(--paper);color:var(--ink);font-weight:800;font-size:15px;box-shadow:inset 0 0 0 1.5px var(--line);flex:none}
.jm-bag svg{width:22px;height:22px;stroke:var(--cobalt);fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.jm-count{min-width:24px;height:24px;padding:0 7px;border-radius:999px;background:var(--mint);color:var(--ink);
display:inline-grid;place-items:center;font-size:12px;font-weight:800;line-height:1}
.jm-count[data-empty="true"]{background:var(--cobalt-soft);color:var(--mute)}
.jm-grid{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:clamp(16px,2vw,28px)}
.jm-card{display:flex;flex-direction:column;overflow:hidden;transition:transform .35s var(--ease)}
.jm-card:hover{transform:translateY(-4px)}
.jm-media{position:relative;aspect-ratio:4/5;overflow:hidden;background:var(--cobalt-soft)}
.jm-media > img,.jm-media > .jm-art{position:absolute;inset:0;width:100%;height:100%;transition:transform .8s var(--ease)}
.jm-media > img{object-fit:cover}
.jm-card:hover .jm-media > img,.jm-card:hover .jm-media > .jm-art{transform:scale(1.07)}
.jm-pill{position:absolute;top:14px;left:14px;z-index:2;padding:6px 12px;border-radius:999px;background:var(--mint);color:var(--ink);
font-size:11px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;line-height:1}
.jm-body{display:flex;flex-direction:column;gap:6px;padding:20px 20px 22px;flex:1}
.jm-name{margin:0;font-family:var(--display);font-weight:700;font-size:17px;line-height:1.2;letter-spacing:-.01em}
.jm-meta{margin:0;font-size:14px;color:var(--mute)}
.jm-row{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:auto;padding-top:14px}
.jm-price{font-family:var(--display);font-weight:900;font-size:20px;color:var(--cobalt)}
.jm-add{padding:11px 18px;font-size:14px}
.jm-add.jm-added{background:var(--mint);color:var(--ink)}
/* CSS-only CD jewel case */
.jm-cd{display:grid;place-items:center;background:
radial-gradient(circle at 20% 15%,rgba(63,224,181,.35),rgba(63,224,181,0) 45%),var(--cobalt-soft)}
.jm-case{position:relative;width:72%;aspect-ratio:1;border-radius:6px;background:var(--cobalt);
box-shadow:0 30px 50px -24px rgba(26,41,184,.8),inset 0 0 0 2px rgba(255,255,255,.25);overflow:hidden}
.jm-case::before{content:"";position:absolute;inset:0 auto 0 0;width:9%;background:linear-gradient(90deg,var(--cobalt-deep),rgba(26,41,184,.4));border-right:1px solid rgba(255,255,255,.3)}
.jm-case::after{content:"";position:absolute;inset:0;background:linear-gradient(135deg,rgba(255,255,255,.35),rgba(255,255,255,0) 40%);pointer-events:none}
.jm-disc{position:absolute;right:-22%;top:50%;width:78%;aspect-ratio:1;transform:translateY(-50%);border-radius:50%;
background:radial-gradient(circle,var(--paper) 0 9%,var(--mint-deep) 9.5% 11%,var(--mint) 11.5% 60%,var(--mint-soft) 61% 63%,var(--mint) 64% 100%)}
.jm-case-txt{position:absolute;left:16%;top:12%;right:30%;color:#fff;font-family:var(--display);font-weight:900;text-transform:uppercase;line-height:.95;letter-spacing:-.03em;font-size:clamp(14px,1.5vw,20px)}
.jm-case-txt small{display:block;margin-top:8px;font-family:var(--body);font-weight:800;font-size:9px;letter-spacing:.2em;opacity:.85}
.jm-sig{position:absolute;left:14%;bottom:12%;width:52%;height:18%;border-bottom:2px solid var(--mint);border-radius:0 0 60% 40%/0 0 100% 100%;transform:rotate(-8deg)}
.jm-sig::before{content:"Juno";position:absolute;left:4%;bottom:10%;color:var(--mint);font-family:'Brush Script MT',cursive;font-style:italic;font-size:clamp(18px,2vw,26px);line-height:1}
/* CSS-only tour poster */
.jm-poster-bg{display:grid;place-items:center;background:
repeating-linear-gradient(0deg,rgba(36,56,232,.07) 0 2px,transparent 2px 14px),var(--mint-soft)}
.jm-poster{position:relative;width:66%;aspect-ratio:2/3;background:var(--paper);transform:rotate(-3deg);
box-shadow:0 30px 50px -24px rgba(22,18,58,.45);padding:9% 8%;display:flex;flex-direction:column;overflow:hidden}
.jm-poster::before{content:"";position:absolute;left:-20%;right:-20%;top:44%;height:26%;background:var(--cobalt);transform:rotate(-8deg)}
.jm-poster::after{content:"";position:absolute;left:-20%;right:-20%;top:66%;height:6%;background:var(--mint);transform:rotate(-8deg)}
.jm-p-top{position:relative;z-index:1;font-family:var(--display);font-weight:900;text-transform:uppercase;color:var(--ink);font-size:clamp(18px,2vw,26px);line-height:.85;letter-spacing:-.05em}
.jm-p-top span{color:var(--cobalt)}
.jm-p-mid{position:relative;z-index:1;margin-top:auto;margin-bottom:auto;padding-top:22%;color:#fff;font-family:var(--display);font-weight:700;font-size:clamp(8px,.8vw,11px);letter-spacing:.14em;text-transform:uppercase;transform:rotate(-8deg)}
.jm-p-foot{position:relative;z-index:1;display:flex;justify-content:space-between;gap:4px;font-size:clamp(6px,.6vw,8px);font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:var(--mute);border-top:1px solid var(--line);padding-top:6px}
/* drawn strokes behind the grid */
.jm-store{position:relative}
.jm-store-head,.jm-grid{position:relative;z-index:1}
.jm-lines{position:absolute;z-index:0;top:-12%;bottom:-18%;left:calc(50% - 50vw);width:100vw;pointer-events:none;overflow:visible}
.jm-lines path{fill:none;stroke-width:1.5;stroke-linecap:round}
.jm-lines .jm-l-c{stroke:var(--cobalt);opacity:.32}
.jm-lines .jm-l-m{stroke:var(--mint-deep);opacity:.55}
/* 3D deal-in stage */
.jm-grid{perspective:1200px}
.jm-grid > li{transform-style:preserve-3d}
/* motion-only states (class added by JS when motion is allowed) */
.jm-motion .jm-drift{inset:auto;left:0;right:0;top:-40%;height:140%;will-change:transform}
.jm-line{display:block;overflow:hidden;padding-bottom:.06em;margin-bottom:-.06em}
.jm-line-in{display:block;will-change:transform}
/* toast */
.jm-toast{position:fixed;left:50%;bottom:24px;z-index:60;max-width:calc(100vw - 32px);transform:translate(-50%,24px);opacity:0;pointer-events:none;
display:flex;align-items:center;gap:10px;padding:14px 22px;border-radius:999px;background:var(--mint);color:var(--ink);
font-weight:800;font-size:15px;box-shadow:0 20px 40px -18px rgba(22,18,58,.45);transition:opacity .3s var(--ease),transform .3s var(--ease)}
.jm-toast.jm-show{opacity:1;transform:translate(-50%,0)}
.jm-toast svg{width:18px;height:18px;flex:none;stroke:var(--ink);fill:none;stroke-width:2.6;stroke-linecap:round;stroke-linejoin:round}
.jm-toast span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* ---------- responsive ---------- */
@media (max-width:1100px){
.jm-grid{grid-template-columns:repeat(2,minmax(0,1fr))}
}
@media (max-width:760px){
.jm-frame{aspect-ratio:4/3}
.jm-frame::after{background:linear-gradient(0deg,rgba(36,56,232,.12),rgba(36,56,232,0) 50%)}
.jm-quote{position:relative;left:auto;bottom:auto;max-width:none;margin:-40px 12px 0;background:var(--paper)}
.jm-play{top:auto;bottom:16px;right:auto;left:16px;position:absolute}
.jm-studio-media{position:relative}
}
@media (max-width:640px){
.jm-grid{perspective:900px;display:flex;overflow-x:auto;scroll-snap-type:x mandatory;gap:14px;padding:4px 2px 28px;
-webkit-overflow-scrolling:touch;scrollbar-width:thin;scrollbar-color:var(--cobalt) transparent}
.jm-grid > li{flex:0 0 80%;scroll-snap-align:start;display:flex}
.jm-grid > li > .jm-card{width:100%}
.jm-card:hover{transform:none}
.jm-bag{padding:10px 16px 10px 12px}
.jm-play{padding-right:18px;font-size:14px}
.jm-play-ic{width:38px;height:38px}
}
@media (prefers-reduced-motion: reduce) and (update: none){
.jm-play-ic::after{animation:none;opacity:0}
.jm-frame img,.jm-media > img,.jm-media > .jm-art,.jm-card,.jm-toast{transition:none}
.jm-frame:hover img,.jm-card:hover .jm-media > img,.jm-card:hover .jm-media > .jm-art{transform:none}
}
</style>
<div class="wrap jm-inner">
<!-- In the studio -->
<div class="jm-studio">
<div class="jm-studio-top" data-reveal>
<div>
<p class="eyebrow">In the studio</p>
<h2 class="display">Made after <span class="hl">midnight</span></h2>
</div>
</div>
<div class="jm-studio-media">
<div class="jm-frame">
<div class="jm-drift"><img src="assets/studio.jpg" width="1880" height="1253" loading="lazy" alt="Juno Vale in the studio wearing headphones against a white backdrop"></div>
<a class="jm-play" href="#" aria-label="Watch the making-of Low Tide Radio">
<span class="jm-play-ic" aria-hidden="true"></span>Watch the making-of
</a>
</div>
<figure class="jm-quote">
<blockquote>We tracked most of it at 3 a.m. with the window cracked open. If you listen close on track seven, that's the last ferry leaving the harbour.</blockquote>
<figcaption><span class="eq" aria-hidden="true"><i></i><i></i><i></i><i></i></span>Juno Vale</figcaption>
</figure>
</div>
</div>
<!-- Merch store -->
<div class="jm-store">
<div class="jm-store-head" data-reveal>
<div>
<p class="eyebrow">Official merch</p>
<h2 class="display" id="jm-title">Take the <span class="hl">tide</span> home</h2>
<p class="lede">Pressed, printed and signed in small runs. When a drop is gone, it's gone for good.</p>
</div>
<button class="jm-bag" type="button" aria-label="Bag, 0 items">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 8h14l-1.2 12H6.2L5 8z"/><path d="M9 8V6a3 3 0 0 1 6 0v2"/></svg>
Bag <span class="jm-count" data-empty="true">0</span>
</button>
</div>
<svg class="jm-lines" viewBox="0 0 1600 1000" preserveAspectRatio="xMidYMid slice" aria-hidden="true" focusable="false">
<path class="jm-l-c" d="M-60 930 C 380 820, 560 600, 860 560 S 1380 300, 1680 120"/>
<path class="jm-l-m" d="M-60 1010 C 300 900, 620 760, 900 700 S 1400 420, 1680 260"/>
<path class="jm-l-c" d="M-60 780 C 260 700, 520 520, 780 470 S 1300 200, 1680 -20"/>
<path class="jm-l-m" d="M-60 640 C 340 560, 600 400, 840 360 S 1360 140, 1680 -120"/>
<path class="jm-l-c" d="M200 1100 C 520 960, 760 860, 1040 780 S 1460 560, 1700 420"/>
</svg>
<ul class="jm-grid">
<li>
<article class="card jm-card">
<div class="jm-media">
<span class="jm-pill">Limited</span>
<img src="assets/vinyl.jpg" width="868" height="1300" loading="lazy" alt="Low Tide Radio vinyl record">
</div>
<div class="jm-body">
<h3 class="jm-name">Low Tide Radio — Vinyl</h3>
<p class="jm-meta">180g seafoam pressing, gatefold, lyric sheet</p>
<div class="jm-row">
<span class="jm-price">$34</span>
<button class="btn jm-add" type="button" data-name="Low Tide Radio Vinyl">Add to bag</button>
</div>
</div>
</article>
</li>
<li>
<article class="card jm-card">
<div class="jm-media">
<span class="jm-pill">New</span>
<img src="assets/tee.jpg" width="866" height="1300" loading="lazy" alt="Tide tee, heavyweight cotton t-shirt">
</div>
<div class="jm-body">
<h3 class="jm-name">Tide Tee</h3>
<p class="jm-meta">Heavyweight organic cotton, back print tracklist</p>
<div class="jm-row">
<span class="jm-price">$32</span>
<button class="btn jm-add" type="button" data-name="Tide Tee">Add to bag</button>
</div>
</div>
</article>
</li>
<li>
<article class="card jm-card">
<div class="jm-media">
<span class="jm-pill">Signed</span>
<div class="jm-art jm-cd" role="img" aria-label="Signed Low Tide Radio CD in a cobalt jewel case">
<div class="jm-case">
<span class="jm-disc"></span>
<span class="jm-case-txt">Low Tide Radio<small>Juno Vale</small></span>
<span class="jm-sig"></span>
</div>
</div>
</div>
<div class="jm-body">
<h3 class="jm-name">Signed CD</h3>
<p class="jm-meta">Hand-signed by Juno, numbered insert</p>
<div class="jm-row">
<span class="jm-price">$22</span>
<button class="btn jm-add" type="button" data-name="Signed CD">Add to bag</button>
</div>
</div>
</article>
</li>
<li>
<article class="card jm-card">
<div class="jm-media">
<span class="jm-pill">Tour 26–27</span>
<div class="jm-art jm-poster-bg" role="img" aria-label="Juno Vale Low Tide Radio tour poster">
<div class="jm-poster">
<div class="jm-p-top">Juno<br><span>Vale</span></div>
<div class="jm-p-mid">Low Tide Radio Tour</div>
<div class="jm-p-foot"><span>18 cities</span><span>26–27</span></div>
</div>
</div>
</div>
<div class="jm-body">
<h3 class="jm-name">Tour Poster</h3>
<p class="jm-meta">A2 screen print on 250gsm paper</p>
<div class="jm-row">
<span class="jm-price">$18</span>
<button class="btn jm-add" type="button" data-name="Tour Poster">Add to bag</button>
</div>
</div>
</article>
</li>
</ul>
</div>
</div>
<div class="jm-toast" role="status" aria-live="polite">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>
<span class="jm-toast-txt"></span>
</div>
<script>
/* deferred until the page has painted — see JV.later in template.html */
JV.later(function(){
var root = document.getElementById('merch');
if(!root) return;
var reduce = window.matchMedia('(prefers-reduced-motion: reduce) and (update: none)').matches;
var countEl = root.querySelector('.jm-count');
var bag = root.querySelector('.jm-bag');
var toast = root.querySelector('.jm-toast');
var toastTxt = root.querySelector('.jm-toast-txt');
var count = 0, timer = null;
root.querySelectorAll('.jm-add').forEach(function(btn){
btn.addEventListener('click', function(){
var name = btn.getAttribute('data-name');
count++;
countEl.textContent = count;
countEl.setAttribute('data-empty','false');
bag.setAttribute('aria-label','Bag, ' + count + (count === 1 ? ' item' : ' items'));
toastTxt.textContent = 'Added — ' + name;
toast.classList.add('jm-show');
clearTimeout(timer);
timer = setTimeout(function(){ toast.classList.remove('jm-show'); }, 2200);
btn.classList.add('jm-added');
btn.textContent = 'Added';
setTimeout(function(){ btn.classList.remove('jm-added'); btn.textContent = 'Add to bag'; }, 1400);
if(!reduce && window.gsap){
gsap.fromTo(countEl,{scale:1.6},{scale:1,duration:.5,ease:'back.out(3)'});
}
});
});
/* ---------- scroll motion ---------- */
if(!window.gsap || !window.JV || JV.reduce) return;
root.classList.add('jm-motion');
// studio photo: sliced reveal + scrubbed image drift
var frame = root.querySelector('.jm-frame');
JV.sliceReveal(frame, {slices:10, corner:2, duration:1.2, ease:'expo.inOut',
scrollTrigger:{trigger:frame, start:'top 75%'}});
gsap.fromTo(root.querySelector('.jm-drift'), {yPercent:25}, {yPercent:0, ease:'none',
scrollTrigger:{trigger:frame, start:'top bottom', end:'bottom 30%', scrub:true}});
// pull-quote: split into masked lines, lines rise scrubbed
var bq = root.querySelector('.jm-quote blockquote');
var quoteTween = null, lastW = 0;
function esc(t){ return t.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
function splitQuote(){
var w = bq.offsetWidth;
if(w === lastW) return; lastW = w;
if(quoteTween){ quoteTween.scrollTrigger && quoteTween.scrollTrigger.kill(); quoteTween.kill(); }
var text = bq.getAttribute('data-jm-text');
if(!text){ text = bq.textContent.trim().replace(/\s+/g,' '); bq.setAttribute('data-jm-text', text); }
bq.innerHTML = text.split(' ').map(function(x){ return '<span class="jm-w">'+esc(x)+'</span>'; }).join(' ');
var lines = [], top = null;
bq.querySelectorAll('.jm-w').forEach(function(sp){
var t = sp.offsetTop;
if(top === null || Math.abs(t - top) > 3){ lines.push([]); top = t; }
lines[lines.length-1].push(sp.textContent);
});
bq.innerHTML = lines.map(function(l){
return '<span class="jm-line"><span class="jm-line-in">'+esc(l.join(' '))+'</span></span>';
}).join(' ');
quoteTween = gsap.fromTo(bq.querySelectorAll('.jm-line-in'), {yPercent:100}, {yPercent:0, ease:'none', stagger:.05,
scrollTrigger:{trigger:root.querySelector('.jm-quote'), start:'top 92%', end:'top 55%', scrub:true}});
}
(document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve()).then(function(){
splitQuote(); JV.refresh();
});
var rT; addEventListener('resize', function(){ clearTimeout(rT); rT = setTimeout(splitQuote, 200); });
// drawn strokes + 3D card deal-in, one scrubbed timeline
var store = root.querySelector('.jm-store');
var paths = root.querySelectorAll('.jm-lines path');
paths.forEach(function(p){ var L = p.getTotalLength(); p.style.strokeDasharray = L; p.style.strokeDashoffset = L; });
var cards = gsap.utils.toArray(root.querySelectorAll('.jm-grid > li'));
var spins = [-20,-65,-30,-55];
function build(mobile){
var tl = gsap.timeline({scrollTrigger:{trigger:store, start:'top 125%', end:'bottom 25%', scrub:.5}});
tl.to(paths, {strokeDashoffset:0, ease:'none', duration:1, stagger:-.05}, 0);
if(mobile){
tl.fromTo(cards, {rotationX:60, transformOrigin:'50% 100%'},
{rotationX:0, ease:'power3.inOut', duration:.45, stagger:.05}, 0);
} else {
tl.fromTo(cards, {y:function(){ return innerHeight*.5; }, rotationX:60, rotationZ:function(i){ return spins[i % spins.length]; }, transformOrigin:'50% 100%'},
{y:0, rotationX:0, rotationZ:0, ease:'power3.inOut', duration:.45, stagger:.05}, 0);
}
return function(){ tl.scrollTrigger && tl.scrollTrigger.kill(); tl.kill(); gsap.set(cards, {clearProps:'transform'}); };
}
var mm = gsap.matchMedia();
mm.add('(max-width:640px)', function(){ return build(true); });
mm.add('(min-width:641px)', function(){ return build(false); });
});
</script>
</section>
<!-- ===================== 06 · JOIN THE TIDE + FOOTER (prefix jf-) ===================== -->
<style>
/* ---------- signup panel ---------- */
.jf{padding-block:clamp(56px,8vw,110px) clamp(40px,6vw,80px);overflow:hidden}
.jf-panel{position:relative;isolation:isolate;overflow:hidden;background:var(--cobalt);color:#fff;
border-radius:clamp(24px,3vw,40px);padding:clamp(36px,6vw,88px) clamp(22px,5vw,80px);box-shadow:var(--shadow)}
.jf-waves{position:absolute;inset:0;z-index:-1;pointer-events:none}
.jf-strip{position:absolute;left:0;width:200%;height:clamp(90px,14vw,180px);animation:jf-drift 26s linear infinite}
.jf-strip svg{display:block;width:100%;height:100%}
.jf-strip path{fill:none;stroke-width:2;vector-effect:non-scaling-stroke}
.jf-strip--1{top:6%;animation-duration:30s}
.jf-strip--1 path{stroke:rgba(255,255,255,.18)}
.jf-strip--2{top:38%;animation-duration:22s;animation-direction:reverse}
.jf-strip--2 path{stroke:rgba(63,224,181,.32)}
.jf-strip--3{top:62%;animation-duration:36s}
.jf-strip--3 path{stroke:rgba(255,255,255,.12)}
.jf-strip--4{bottom:-4%;animation-duration:18s;animation-direction:reverse}
.jf-strip--4 path{stroke:rgba(63,224,181,.2)}
@keyframes jf-drift{from{transform:translateX(0)}to{transform:translateX(-50%)}}
.jf-grid{display:grid;grid-template-columns:1.05fr 1fr;gap:clamp(32px,5vw,80px);align-items:center}
.jf-eyebrow{display:inline-flex;align-items:center;gap:10px;color:var(--mint)}
.jf-title{font-size:clamp(40px,6.4vw,96px);margin-top:18px;color:#fff}
.jf-title .jf-hl{color:var(--mint)}
.jf-lede{color:rgba(255,255,255,.82);margin:22px 0 0}
.jf-perks{list-style:none;padding:0;margin:26px 0 0;display:flex;flex-wrap:wrap;gap:10px}
.jf-perks li{font-size:13px;font-weight:700;padding:8px 14px;border-radius:999px;background:rgba(255,255,255,.1);
box-shadow:inset 0 0 0 1px rgba(255,255,255,.22)}
.jf-box{position:relative;background:rgba(255,255,255,.08);border-radius:26px;padding:clamp(22px,3vw,36px);
box-shadow:inset 0 0 0 1px rgba(255,255,255,.2);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px)}
.jf-form{display:grid;gap:18px}
.jf-form[hidden]{display:none}
.jf-field{display:grid;gap:8px;min-width:0}
.jf-label{font-size:12px;font-weight:800;letter-spacing:.16em;text-transform:uppercase;color:rgba(255,255,255,.85)}
.jf-label small{font-weight:600;letter-spacing:.04em;text-transform:none;color:rgba(255,255,255,.6)}
.jf-input,.jf-select{width:100%;min-width:0;font:inherit;font-size:16px;font-weight:600;color:#fff;
background:rgba(255,255,255,.1);border:1.5px solid rgba(255,255,255,.32);border-radius:16px;padding:15px 18px;
transition:border-color .2s var(--ease),background .2s var(--ease)}
.jf-input::placeholder{color:rgba(255,255,255,.55)}
.jf-input:focus,.jf-select:focus{outline:none;border-color:var(--mint);background:rgba(255,255,255,.16)}
.jf-input[aria-invalid="true"]{border-color:var(--mint);box-shadow:0 0 0 3px rgba(63,224,181,.35)}
.jf-select{appearance:none;-webkit-appearance:none;cursor:pointer;padding-right:46px;
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='9' viewBox='0 0 14 9'%3E%3Cpath d='M1 1l6 6 6-6' fill='none' stroke='%23FFFFFF' stroke-width='2'/%3E%3C/svg%3E");
background-repeat:no-repeat;background-position:right 18px center}
.jf-select option{color:var(--ink);background:var(--paper)}
.jf-error{min-height:0;margin:0;font-size:13px;font-weight:700}
.jf-error:not(:empty){display:inline-block;justify-self:start;background:var(--mint-soft);color:var(--ink);padding:6px 12px;border-radius:10px}
.jf-submit{justify-content:center;width:100%;padding-block:17px;font-size:16px}
.jf-submit svg{transition:transform .25s var(--ease)}
.jf-submit:hover svg{transform:translateX(4px)}
.jf-fine{margin:0;font-size:12.5px;color:rgba(255,255,255,.62)}
.jf-thanks{display:grid;gap:14px;text-align:left}
.jf-thanks[hidden]{display:none}
.jf-badge{width:56px;height:56px;border-radius:50%;background:var(--mint);color:var(--ink);display:grid;place-items:center}
.jf-thanks h3{font-family:var(--display);font-weight:900;text-transform:uppercase;letter-spacing:-.03em;
font-size:clamp(24px,2.6vw,34px);line-height:1;margin:6px 0 0;color:#fff}
.jf-thanks p{margin:0;color:rgba(255,255,255,.82)}
.jf-thanks p strong{color:var(--mint)}
.jf-again{justify-self:start;font-size:14px;font-weight:700;color:#fff;text-decoration:underline;text-underline-offset:4px}
.jf-again:hover{color:var(--mint)}
/* ---------- footer ---------- */
.jf-foot{background:var(--bg);color:var(--ink);padding-block:clamp(28px,4vw,56px) 28px;overflow:hidden}
.jf-word{font-family:var(--display);font-weight:900;text-transform:uppercase;color:var(--cobalt);
letter-spacing:-.05em;line-height:.82;margin:0;white-space:nowrap;font-size:clamp(34px,12.6vw,172px);
max-width:100%;overflow:hidden}
.jf-word span{display:inline-block}
.jf-cols{display:grid;grid-template-columns:1.4fr repeat(3,1fr);gap:clamp(28px,4vw,56px);
margin-top:clamp(36px,5vw,64px);padding-top:clamp(28px,3vw,40px);border-top:1.5px solid var(--line)}
.jf-about p{margin:0;color:var(--mute);max-width:34ch}
.jf-now{display:inline-flex;align-items:center;gap:10px;margin-top:16px;font-size:13px;font-weight:800;
padding:9px 16px;border-radius:999px;background:var(--mint);color:var(--ink)}
.jf-col h4{margin:0 0 14px;font-size:12px;font-weight:800;letter-spacing:.2em;text-transform:uppercase;color:var(--cobalt)}
.jf-col ul{list-style:none;margin:0;padding:0;display:grid;gap:9px}
.jf-col a{font-weight:600;color:var(--ink);background:linear-gradient(var(--cobalt),var(--cobalt)) 0 100%/0 2px no-repeat;
transition:background-size .3s var(--ease),color .2s;overflow-wrap:anywhere}
.jf-col a:hover{color:var(--cobalt);background-size:100% 2px}
.jf-col .jf-role{display:block;font-size:12px;color:var(--mute);font-weight:600}
.jf-bottom{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:14px;
margin-top:clamp(36px,5vw,64px);padding-top:22px;border-top:1.5px solid var(--line);font-size:13.5px;color:var(--mute)}
.jf-top{display:inline-flex;align-items:center;gap:8px;font-weight:800;color:var(--cobalt)}
.jf-top svg{transition:transform .25s var(--ease)}
.jf-top:hover svg{transform:translateY(-3px)}
/* vertical ticker band at the panel's right edge (moved by the scroll timeline) */
.jf-ticker{position:absolute;top:0;bottom:0;right:clamp(6px,1.6vw,26px);display:flex;align-items:center;pointer-events:none;z-index:0}
.jf-tick{writing-mode:vertical-rl;white-space:nowrap;font-size:11px;font-weight:800;letter-spacing:.3em;text-transform:uppercase;color:var(--mint);opacity:.75}
.jf-w{display:inline-block}
@media (max-width:960px){
.jf-grid{grid-template-columns:1fr}
.jf-cols{grid-template-columns:1fr 1fr}
.jf-about{grid-column:1/-1}
}
@media (max-width:520px){
.jf-cols{grid-template-columns:1fr}
.jf-bottom{flex-direction:column;align-items:flex-start}
.jf-ticker{right:4px}
.jf-tick{font-size:9px;letter-spacing:.24em}
}
@media (prefers-reduced-motion: reduce) and (update: none){
.jf-strip{animation:none}
}
</style>
<section id="join" class="jf" aria-labelledby="jf-title">
<div class="wrap">
<div class="jf-panel">
<div class="jf-waves" aria-hidden="true">
<!-- each strip = 200% wide, 4 identical 720-unit periods, drifts -50% for a seamless loop -->
<div class="jf-strip jf-strip--1"><svg viewBox="0 0 2880 200" preserveAspectRatio="none"><path d="M0 100 C180 40 360 40 540 100 S900 160 1080 100 1440 40 1620 100 1980 160 2160 100 2520 40 2700 100 2880 100 2880 100"/><path d="M0 130 C180 80 360 80 540 130 S900 180 1080 130 1440 80 1620 130 1980 180 2160 130 2520 80 2700 130 2880 130 2880 130"/></svg></div>
<div class="jf-strip jf-strip--2"><svg viewBox="0 0 2880 200" preserveAspectRatio="none"><path d="M0 100 C120 30 240 30 360 100 S600 170 720 100 960 30 1080 100 1320 170 1440 100 1680 30 1800 100 2040 170 2160 100 2400 30 2520 100 2760 170 2880 100"/></svg></div>
<div class="jf-strip jf-strip--3"><svg viewBox="0 0 2880 200" preserveAspectRatio="none"><path d="M0 90 C180 150 360 150 540 90 S900 30 1080 90 1440 150 1620 90 1980 30 2160 90 2520 150 2700 90 2880 90 2880 90"/><path d="M0 120 C180 180 360 180 540 120 S900 60 1080 120 1440 180 1620 120 1980 60 2160 120 2520 180 2700 120 2880 120 2880 120"/></svg></div>
<div class="jf-strip jf-strip--4"><svg viewBox="0 0 2880 200" preserveAspectRatio="none"><path d="M0 110 C120 60 240 60 360 110 S600 160 720 110 960 60 1080 110 1320 160 1440 110 1680 60 1800 110 2040 160 2160 110 2400 60 2520 110 2760 160 2880 110"/></svg></div>
</div>
<div class="jf-ticker" aria-hidden="true"><span class="jf-tick">Presale · Signed vinyl · First dibs ·</span></div>
<div class="jf-grid">
<div class="jf-copy">
<span class="eyebrow jf-eyebrow"><span class="eq"><i></i><i></i><i></i><i></i></span> The mailing list</span>
<h2 id="jf-title" class="display jf-title"><span class="jf-w">Join</span> <span class="jf-w">the</span> <span class="jf-w jf-hl">Tide</span></h2>
<p class="lede jf-lede">One letter when the water moves: new songs before they surface, tour dates before they sell out, and the odd voice memo recorded on the pier at 2am. No spam, no noise, just the signal.</p>
<ul class="jf-perks">
<li>Presale codes first</li>
<li>Unreleased demos</li>
<li>Tour in your city</li>
</ul>
</div>
<div class="jf-box">
<form class="jf-form" id="jf-form" action="#" method="post" novalidate>
<div class="jf-field">
<label class="jf-label" for="jf-email">Email address</label>
<input class="jf-input" id="jf-email" name="email" type="email" inputmode="email" autocomplete="email"
placeholder="[email protected]" required aria-describedby="jf-error">
</div>
<div class="jf-field">
<label class="jf-label" for="jf-city">Nearest city <small>(optional)</small></label>
<select class="jf-select" id="jf-city" name="city">
<option value="">Pick a stop on the tour</option>
<option>London</option>
<option>Manchester</option>
<option>Dublin</option>
<option>Paris</option>
<option>Berlin</option>
<option>Amsterdam</option>
<option>Lisbon</option>
<option>New York</option>
<option>Los Angeles</option>
<option>Toronto</option>
<option>Melbourne</option>
<option>Somewhere else</option>
</select>
</div>
<p class="jf-error" id="jf-error" role="alert" aria-live="polite"></p>
<button class="btn btn--mint jf-submit" type="submit">
Ride the tide
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg>
</button>
<p class="jf-fine">Roughly one email a month. Unsubscribe with a single click, whenever.</p>
</form>
<div class="jf-thanks" id="jf-thanks" hidden tabindex="-1">
<span class="jf-badge" aria-hidden="true">
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>
</span>
<h3>You're on the list.</h3>
<p>First dibs on presale. <span id="jf-thanks-city"></span>Keep an eye on <strong id="jf-thanks-email"></strong> for the next swell.</p>
<button class="jf-again" type="button" id="jf-again">Sign up another address</button>
</div>
</div>
</div>
</div>
</div>
</section>
<footer class="jf-foot">
<div class="wrap">
<p class="jf-word" id="jf-word" role="img" aria-label="Juno Vale"><span>Juno Vale</span></p>
<div class="jf-cols">
<div class="jf-about">
<p>Alt-pop from the edge of the shoreline. Songs about static, salt air and the people you call when the signal drops.</p>
<span class="jf-now"><span class="eq"><i></i><i></i><i></i><i></i></span> Low Tide Radio · out now</span>
</div>
<nav class="jf-col" aria-label="Listen">
<h4>Listen</h4>
<ul>
<li><a href="#">Spotify</a></li>
<li><a href="#">Apple Music</a></li>
<li><a href="#">YouTube Music</a></li>
<li><a href="#">Tidal</a></li>
<li><a href="#">Bandcamp</a></li>
</ul>
</nav>
<nav class="jf-col" aria-label="Follow">
<h4>Follow</h4>
<ul>
<li><a href="#">Instagram</a></li>
<li><a href="#">TikTok</a></li>
<li><a href="#">YouTube</a></li>
<li><a href="#">X</a></li>
</ul>
</nav>
<div class="jf-col">
<h4>Contact</h4>
<ul>
<li><span class="jf-role">Management</span><a href="mailto:[email protected]">[email protected]</a></li>
<li><span class="jf-role">Booking</span><a href="mailto:[email protected]">[email protected]</a></li>
<li><span class="jf-role">Press</span><a href="mailto:[email protected]">[email protected]</a></li>
</ul>
</div>
</div>
<div class="jf-bottom">
<span>© 2026 Juno Vale · Photos: Pexels</span>
<a class="jf-top" href="#" id="jf-top">Back to top
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M6 11l6-6 6 6"/></svg>
</a>
</div>
</div>
</footer>
<script>
/* deferred until the page has painted — see JV.later in template.html */
JV.later(function(){
var reduce = matchMedia('(prefers-reduced-motion: reduce) and (update: none)').matches;
var motion = !reduce && window.gsap && window.ScrollTrigger;
/* ---- signup form ---- */
var form = document.getElementById('jf-form');
var email = document.getElementById('jf-email');
var city = document.getElementById('jf-city');
var err = document.getElementById('jf-error');
var thanks = document.getElementById('jf-thanks');
var again = document.getElementById('jf-again');
var re = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
function setError(msg){
err.textContent = msg;
email.setAttribute('aria-invalid', msg ? 'true' : 'false');
}
email.addEventListener('input', function(){ if (err.textContent && re.test(email.value.trim())) setError(''); });
form.addEventListener('submit', function(e){
e.preventDefault();
var v = email.value.trim();
if (!v){ setError('Drop in your email so we know where to send it.'); email.focus(); return; }
if (!re.test(v)){ setError("That address doesn't look quite right. Mind checking it?"); email.focus(); return; }
setError('');
document.getElementById('jf-thanks-email').textContent = v;
var c = city.value;
document.getElementById('jf-thanks-city').textContent =
c && c !== 'Somewhere else' ? 'You’ll hear first when the tour lands in ' + c + '. ' : '';
form.hidden = true;
thanks.hidden = false;
thanks.focus({preventScroll:true});
if (!reduce && window.gsap){
gsap.fromTo(thanks.children, {opacity:0, y:18}, {opacity:1, y:0, duration:.6, stagger:.08, ease:'power3.out'});
gsap.fromTo(thanks.querySelector('.jf-badge'), {scale:.4}, {scale:1, duration:.7, ease:'back.out(2.2)'});
}
});
again.addEventListener('click', function(){
form.reset();
setError('');
thanks.hidden = true;
form.hidden = false;
email.focus();
});
/* ---- wordmark: fit exactly to the wrap width ---- */
var word = document.getElementById('jf-word');
var span = word.querySelector('span');
/* split into letters BEFORE fitting, so fit() measures the split line */
var letters = [];
if (motion){
var txt = span.textContent; span.textContent = '';
txt.split('').forEach(function(ch){
var l = document.createElement('span'); l.className = 'jf-l';
l.textContent = ch === ' ' ? ' ' : ch;
span.appendChild(l); letters.push(l);
});
}
function fit(){
word.style.fontSize = '';
var avail = word.clientWidth, w = span.getBoundingClientRect().width;
if (!avail || !w) return;
var fs = parseFloat(getComputedStyle(word).fontSize);
var next = Math.min(fs * (avail / w) * 0.995, 240);
word.style.fontSize = next + 'px';
}
fit();
if (document.fonts && document.fonts.ready) document.fonts.ready.then(fit);
var t; window.addEventListener('resize', function(){ clearTimeout(t); t = setTimeout(fit, 120); });
if (motion){
/* footer wordmark: letters rise out of the wordmark's own overflow mask, scrubbed */
gsap.fromTo(letters, {yPercent:100}, {yPercent:0, duration:.6, ease:'power3.out', stagger:.04,
scrollTrigger:{trigger:word, start:'top 100%', end:'bottom 75%', scrub:.5}});
/* ---- #join: one scrubbed timeline across the section ---- */
var join = document.getElementById('join');
var panel = join.querySelector('.jf-panel');
var words = panel.querySelectorAll('.jf-w');
var box = panel.querySelector('.jf-box');
var tick = panel.querySelector('.jf-tick');
var paths = [].slice.call(panel.querySelectorAll('.jf-strip path'));
/* strip wipe: 6 columns, each column's bg-cover height collapses 1 -> 0 (clip-path of 6 rects) */
var N = 6, cols = [];
for (var i = 0; i < N; i++) cols.push({h:1});
function wipe(){
var W = panel.offsetWidth, H = panel.offsetHeight, cw = W / N, d = '', open = true;
cols.forEach(function(c, i){
if (c.h > .001) open = false;
var r = (1 - c.h) * H;
if (r > .5) d += 'M' + (i*cw).toFixed(1) + ' 0h' + (cw+.8).toFixed(1) + 'v' + r.toFixed(1) + 'h' + (-(cw+.8)).toFixed(1) + 'Z';
});
panel.style.clipPath = open ? '' : (d ? "path('" + d + "')" : 'inset(50%)');
}
/* wave draw-in: dash length measured in screen px (strokes are non-scaling) */
var draw = {p:0};
function dash(){
paths.forEach(function(p){ p.style.strokeDashoffset = (p._len * (1 - draw.p)).toFixed(1); });
}
var measuredFor = '';
function measure(){
/* only re-measure when the strip size actually changed (refresh fires several times on load) */
var box0 = paths.length ? paths[0].ownerSVGElement.getBoundingClientRect() : null;
var key = box0 ? Math.round(box0.width) + 'x' + Math.round(box0.height) : '';
if (key === measuredFor) return;
measuredFor = key;
paths.forEach(function(p){
var r = p.ownerSVGElement.getBoundingClientRect(), sx = r.width / 2880, sy = r.height / 200;
var L = p.getTotalLength(), n = 24, len = 0, a = p.getPointAtLength(0);
for (var k = 1; k <= n; k++){ var b = p.getPointAtLength(L*k/n); len += Math.hypot((b.x-a.x)*sx, (b.y-a.y)*sy); a = b; }
p._len = Math.ceil(len) + 4;
p.style.strokeDasharray = p._len + ' ' + p._len;
});
dash();
}
measure();
wipe();
var tl = gsap.timeline({onUpdate:wipe, scrollTrigger:{trigger:join, start:'top 100%', end:'bottom 100%', scrub:.5, invalidateOnRefresh:true}});
tl.to(cols, {h:0, duration:2, ease:'power3.inOut', stagger:{each:.05, from:'random'}}, 0)
.fromTo(words, {y:function(){ return -panel.offsetHeight * .5; }},
{y:0, duration:1.4, ease:'power2.out', stagger:{each:.1, from:'end'}}, .7)
.fromTo(box, {y:160, opacity:0}, {y:0, opacity:1, duration:1.2, ease:'power3.out'}, 1.8);
var total = tl.duration();
tl.to(draw, {p:1, duration:total, ease:'none', onUpdate:dash}, 0)
.fromTo(tick, {yPercent:300}, {yPercent:-300, duration:total, ease:'none'}, 0);
ScrollTrigger.addEventListener('refresh', measure);
window.addEventListener('resize', wipe);
}
/* ---- back to top ---- */
document.getElementById('jf-top').addEventListener('click', function(e){
e.preventDefault();
if (window.JV && JV.lenis) JV.lenis.scrollTo(0, {duration:1.6}); else window.scrollTo({top:0, behavior: reduce ? 'auto' : 'smooth'});
});
});
</script>
<script>
JV.sliceButtons();
if(!matchMedia('(prefers-reduced-motion: reduce) and (update: none)').matches){
gsap.utils.toArray('[data-reveal]').forEach(el=>{
gsap.to(el,{opacity:1,y:0,duration:1,ease:'power3.out',scrollTrigger:{trigger:el,start:'top 88%'}});
});
}
</script>
</body>
</html>
```