# MASTER PROMPT — Oval — zero-proof botanicals
### 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 `oval` and write out every file in the appendix below **exactly as written** — do not reformat, rename, shorten, reorder or "improve" anything.
2. **Download the 11 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/oval__images-cta.jpg' -o 'assets/cta.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/oval__images-n1.jpg' -o 'assets/n1.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/oval__images-n2.jpg' -o 'assets/n2.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/oval__images-n3.jpg' -o 'assets/n3.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/oval__images-range-grapefruit.jpg' -o 'assets/range-grapefruit.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/oval__images-range-juniper.jpg' -o 'assets/range-juniper.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/oval__images-range-rosemary.jpg' -o 'assets/range-rosemary.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/oval__images-reveal.jpg' -o 'assets/reveal.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/oval__images-ritual-1.jpg' -o 'assets/ritual-1.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/oval__images-ritual-2.jpg' -o 'assets/ritual-2.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/oval__images-ritual-3.jpg' -o 'assets/ritual-3.jpg'
```
Then check every file arrived and none is a zero-byte or HTML error page:
```bash
ls assets | wc -l # expect 11 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:
```
oval/
index.html
sections/01-hero.html
sections/02-benefits.html
sections/03-range.html
sections/04-ritual.html
sections/05-reviews.html
sections/06-cta.html
sections/07-footer.html
template.html
assets/
cta.jpg
n1.jpg
n2.jpg
n3.jpg
… 7 more
```
```bash
zip -r oval.zip oval -x '*.DS_Store'
```
Then hand over `oval.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 oval && 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
Oval makes zero-proof botanical spirits: rosemary, pink grapefruit and wild juniper at 0.0% ABV.
**9 code files plus an `assets/` folder holding 11 images, delivered as `oval.zip`.**
| File | What it is |
|---|---|
| `index.html` | a page, 88 KB |
| `sections/01-hero.html` | a page, 9 KB |
| `sections/02-benefits.html` | a page, 9 KB |
| `sections/03-range.html` | a page, 14 KB |
| `sections/04-ritual.html` | a page, 14 KB |
| `sections/05-reviews.html` | a page, 15 KB |
| `sections/06-cta.html` | a page, 13 KB |
| `sections/07-footer.html` | a page, 5 KB |
| `template.html` | a page, 5 KB |
| `assets/` | 11 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` · `#benefits` · `#range` · `#ritual` · `#reviews` · `#stockists`.
---
## ▶ WHERE THE MEDIA COMES FROM
All 11 files (11 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 `oval__` 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/oval__images-cta.jpg` | `assets/cta.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/oval__images-n1.jpg` | `assets/n1.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/oval__images-n2.jpg` | `assets/n2.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/oval__images-n3.jpg` | `assets/n3.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/oval__images-range-grapefruit.jpg` | `assets/range-grapefruit.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/oval__images-range-juniper.jpg` | `assets/range-juniper.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/oval__images-range-rosemary.jpg` | `assets/range-rosemary.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/oval__images-reveal.jpg` | `assets/reveal.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/oval__images-ritual-1.jpg` | `assets/ritual-1.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/oval__images-ritual-2.jpg` | `assets/ritual-2.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/oval__images-ritual-3.jpg` | `assets/ritual-3.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
sections/01-hero.html
sections/02-benefits.html
sections/03-range.html
sections/04-ritual.html
sections/05-reviews.html
sections/06-cta.html
sections/07-footer.html
template.html
```
## `index.html`
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Oval — zero-proof botanicals</title>
<meta name="description" content="Oval makes zero-proof botanical spirits: rosemary, pink grapefruit and wild juniper at 0.0% ABV.">
<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=Inter+Tight:wght@400;500;600&family=Unbounded:wght@500;700;800&display=swap" rel="stylesheet">
<style>
:root{
--lime:#E4F76B;
--forest:#245C3E;
--tang:#FF8A3D;
--soft:#F4FBD0;
--ink:#1F3B2C;
--white:#FFFFFF;
--forest-12:rgba(36,92,62,.12);
--forest-24:rgba(36,92,62,.24);
--soft-94:rgba(244,251,208,.94);
--tang-20:rgba(255,138,61,.2);
--shadow:0 20px 50px -24px rgba(31,59,44,.35);
--display:"Unbounded",system-ui,sans-serif;
--body:"Inter Tight",system-ui,sans-serif;
--gutter:clamp(16px,4vw,56px);
--section-y:clamp(80px,13vh,150px);
--radius:22px;
}
*,*::before,*::after{box-sizing:border-box}
html,body{margin:0}
html.lenis,html.lenis body{height:auto}
.lenis.lenis-smooth{scroll-behavior:auto!important}
body{
background:var(--lime);color:var(--ink);
font-family:var(--body);font-size:17px;line-height:1.55;
overflow-x:hidden;-webkit-font-smoothing:antialiased;
}
img{display:block;max-width:100%}
a{color:inherit}
button{font:inherit}
/* ---------- shared atoms (used by every section) ---------- */
.ov-pill{
display:inline-flex;align-items:center;gap:8px;text-decoration:none;border:0;cursor:pointer;
background:var(--forest);color:var(--soft);font-weight:600;font-size:15px;
padding:11px 20px;border-radius:999px;transition:background .25s,color .25s,transform .25s;
}
.ov-pill:hover{background:var(--tang);color:var(--ink);transform:translateY(-1px)}
.ov-pill--ghost{background:transparent;color:var(--forest);box-shadow:inset 0 0 0 1.5px var(--forest-24)}
.ov-pill--ghost:hover{background:var(--forest);color:var(--soft)}
.ov-tag{
display:inline-block;font-size:13px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;
color:var(--forest);border:1.5px solid var(--forest-24);border-radius:999px;padding:6px 12px;
}
.ov-eyebrow{font-size:13px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--tang);margin:0 0 14px}
.ov-h2{
margin:0;font-family:var(--display);font-weight:700;color:var(--forest);
font-size:clamp(30px,4.6vw,64px);line-height:1;letter-spacing:-.035em;
}
.ov-h2 em{font-style:normal;color:var(--tang)}
.ov-more{
display:inline-flex;align-items:center;gap:8px;font-weight:600;text-decoration:none;color:var(--forest);
border-bottom:2px solid var(--tang);padding-bottom:2px;transition:color .2s,gap .25s;
}
.ov-more span{color:var(--tang);font-size:20px;line-height:1}
.ov-more:hover{color:var(--tang);gap:12px}
/* line mask used by OV.lines() */
.ov-line{display:block;overflow:hidden;padding-bottom:.08em;margin-bottom:-.08em}
.ov-line-in{display:block;will-change:transform}
</style>
<script src="https://cdn.jsdelivr.net/npm/
[email protected]/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/
[email protected]/dist/ScrollTrigger.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/
[email protected]/dist/lenis.min.js"></script>
<script>
/* ---------- shared runtime: window.OV ---------- */
(function(){
var OV = window.OV = {};
gsap.registerPlugin(ScrollTrigger);
// ?motion=1 forces animation on; otherwise honour the OS setting.
OV.reduce = /[?&]motion=1\b/.test(location.search) ? false
: matchMedia('(prefers-reduced-motion: reduce) and (update: none)').matches;
if (!OV.reduce) document.documentElement.classList.add('ov-motion');
// One Lenis for the whole page, driven by the GSAP ticker.
OV.lenis = null;
if (!OV.reduce){
OV.lenis = new Lenis({ duration: 1.15, smoothWheel: true });
OV.lenis.on('scroll', ScrollTrigger.update);
gsap.ticker.add(function(t){ OV.lenis.raf(t * 1000); });
gsap.ticker.lagSmoothing(0);
}
// Smooth in-page anchor links.
document.addEventListener('click', function(e){
var a = e.target.closest('a[href^="#"]');
if (!a || a.getAttribute('href').length < 2) return;
var target = document.querySelector(a.getAttribute('href'));
if (!target) return;
e.preventDefault();
if (OV.lenis) OV.lenis.scrollTo(target, { offset: -10 });
else target.scrollIntoView();
});
// Wrap each direct text line of el in a mask: el's children become .ov-line > .ov-line-in.
// Pass an element whose content is plain text / inline tags; lines are measured after fonts load.
OV.lines = function(el){
if (el._ovLines) return el._ovLines;
var html = el.innerHTML.trim();
var tokens = html.split(/(\s+)/);
el.innerHTML = tokens.map(function(t){ return /^\s+$/.test(t) ? ' ' : '<span class="ov-w" style="display:inline-block">' + t + '</span>'; }).join('');
var rows = [], top = null;
el.querySelectorAll('.ov-w').forEach(function(w){
if (w.offsetTop !== top){ rows.push([]); top = w.offsetTop; }
rows[rows.length - 1].push(w.innerHTML);
});
el.innerHTML = rows.map(function(r){ return '<span class="ov-line"><span class="ov-line-in">' + r.join(' ') + '</span></span>'; }).join('');
el._ovLines = el.querySelectorAll('.ov-line-in');
return el._ovLines;
};
window.addEventListener('load', function(){ ScrollTrigger.refresh(); });
if (document.fonts) document.fonts.ready.then(function(){ ScrollTrigger.refresh(); });
})();
</script>
</head>
<body>
<!-- ============ 01 HERO: nav + intro + the dive through the zero ============ -->
<style>
.oh-nav{
position:fixed;top:0;left:0;right:0;z-index:50;
display:flex;align-items:center;justify-content:space-between;gap:16px;
padding:18px var(--gutter);transition:background .35s,box-shadow .35s,padding .35s;
}
.oh-nav.is-scrolled{background:var(--soft-94);box-shadow:0 1px 0 var(--forest-12);padding-top:12px;padding-bottom:12px;backdrop-filter:blur(10px)}
.oh-logo{
display:inline-flex;align-items:center;gap:10px;text-decoration:none;
font-family:var(--display);font-weight:800;font-size:22px;color:var(--forest);letter-spacing:-.02em;
}
.oh-logo-mark{width:18px;height:24px;border:5px solid var(--forest);border-radius:50%}
.oh-links{display:flex;gap:30px;list-style:none;margin:0;padding:0}
.oh-links a{text-decoration:none;font-weight:500;color:var(--ink);transition:color .2s}
.oh-links a:hover{color:var(--tang)}
.oh-intro{
padding:clamp(120px,18vh,180px) var(--gutter) clamp(28px,5vh,56px);
display:grid;grid-template-columns:1.4fr 1fr;gap:32px;align-items:end;
}
.oh-intro h1{
margin:0;font-family:var(--display);font-weight:700;color:var(--forest);
font-size:clamp(34px,6vw,86px);line-height:.98;letter-spacing:-.035em;
}
.oh-intro h1 em{font-style:normal;color:var(--tang)}
.oh-side{max-width:360px;justify-self:end}
.oh-side p{margin:0 0 18px}
/* the dive */
.od{position:relative;height:100vh;height:100svh;overflow:hidden;background:var(--soft)}
.od-photo{position:absolute;inset:0;overflow:hidden}
.od-photo img{width:100%;height:100%;object-fit:cover;object-position:32% 50%;transform-origin:50% 50%}
.od-color{position:absolute;inset:0;display:none;pointer-events:none}
.od-color svg{position:absolute;inset:0;width:100%;height:100%;display:block}
.od-marks{position:absolute;inset:0;transform-origin:50% 50%}
.od-mark{position:absolute;font-size:13px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--forest)}
.od-mark--tl{top:90px;left:var(--gutter)}
.od-mark--tr{top:90px;right:var(--gutter);text-align:right}
.od-mark--bl{bottom:28px;left:var(--gutter)}
.od-mark--br{bottom:28px;right:var(--gutter);text-align:right}
.od-mark b{color:var(--tang);font-weight:600}
.od-copy{
position:absolute;right:var(--gutter);bottom:clamp(20px,7vh,72px);
width:min(460px,calc(100% - 2*var(--gutter)));
background:var(--soft-94);border-radius:var(--radius);padding:clamp(22px,3vw,36px);
box-shadow:var(--shadow);
}
.od-copy h2{
margin:0 0 16px;font-family:var(--display);font-weight:800;color:var(--forest);
font-size:clamp(30px,3.6vw,50px);line-height:1;letter-spacing:-.035em;
}
.od-copy p.od-body{margin:0 0 20px}
html.ov-motion .od-color{display:block}
@media (max-width:860px){
.oh-links{display:none}
.oh-intro{grid-template-columns:1fr}
.oh-side{justify-self:start}
.od-photo img{object-position:36% 30%}
.od-copy{left:var(--gutter);right:var(--gutter);width:auto;bottom:16px}
.od-mark--tr,.od-mark--bl{display:none}
}
@media (max-width:420px){
.oh-nav .ov-pill{padding:10px 16px;font-size:14px}
.od-copy p.od-body{font-size:15.5px}
}
</style>
<header class="oh-nav">
<a class="oh-logo" href="#top"><span class="oh-logo-mark" aria-hidden="true"></span>Oval</a>
<ul class="oh-links">
<li><a href="#benefits">Why zero</a></li>
<li><a href="#range">The range</a></li>
<li><a href="#ritual">Ritual</a></li>
<li><a href="#stockists">Stockists</a></li>
</ul>
<a class="ov-pill" href="#range">Shop the range</a>
</header>
<section class="oh-intro" id="top">
<h1 class="oh-title">Everything the evening wants. <em>Nothing</em> it doesn't.</h1>
<div class="oh-side">
<p>Garden-grown botanical spirits, distilled for long dinners, early mornings and every hour between.</p>
<span class="ov-tag">0.0% ABV</span>
</div>
</section>
<section class="od" aria-label="Zero proof, full flavour">
<div class="od-photo">
<img src="assets/reveal.jpg" alt="A pale botanical drink with a mint sprig in a coupe glass, set on a sunlit white wall beside rosemary bushes">
</div>
<div class="od-color" aria-hidden="true">
<svg class="od-svg" preserveAspectRatio="none">
<path class="od-field" fill="#E4F76B" fill-rule="evenodd"/>
<path class="od-ring" fill="#245C3E" fill-rule="evenodd"/>
</svg>
<div class="od-marks">
<span class="od-mark od-mark--tl">N°<b>0</b> — the zero-proof edition</span>
<span class="od-mark od-mark--tr">Distilled in small batches</span>
<span class="od-mark od-mark--bl">Rosemary · Grapefruit · Juniper</span>
<span class="od-mark od-mark--br">Scroll to step inside ↓</span>
</div>
</div>
<div class="od-copy">
<p class="ov-eyebrow"><span class="ov-line"><span class="ov-line-in">Distilled, never diluted</span></span></p>
<h2>
<span class="ov-line"><span class="ov-line-in">Zero proof.</span></span>
<span class="ov-line"><span class="ov-line-in">Full flavour.</span></span>
</h2>
<p class="od-body">
<span class="ov-line"><span class="ov-line-in">Every bottle starts in the garden:</span></span>
<span class="ov-line"><span class="ov-line-in">rosemary, pink grapefruit and wild</span></span>
<span class="ov-line"><span class="ov-line-in">juniper, each distilled on its own</span></span>
<span class="ov-line"><span class="ov-line-in">for all the bite, none of the buzz.</span></span>
</p>
<span class="ov-line"><span class="ov-line-in"><a class="ov-more" href="#benefits">Find out more <span>+</span></a></span></span>
</div>
</section>
<script>
(function () {
// nav background once the page moves
var nav = document.querySelector('.oh-nav');
function navState(){ nav.classList.toggle('is-scrolled', window.scrollY > 40); }
window.addEventListener('scroll', navState, { passive: true }); navState();
if (OV.reduce) return;
// intro headline rises in on load
var title = document.querySelector('.oh-title');
(document.fonts ? document.fonts.ready : Promise.resolve()).then(function(){
var lines = OV.lines(title);
gsap.from(lines, { yPercent: 110, duration: 1.1, ease: 'power4.out', stagger: .1, delay: .15 });
gsap.from('.oh-side > *', { y: 24, autoAlpha: 0, duration: .9, ease: 'power3.out', stagger: .1, delay: .55 });
});
var dive = document.querySelector('.od');
var svg = dive.querySelector('.od-svg');
var field = svg.querySelector('.od-field');
var ring = svg.querySelector('.od-ring');
var colorLayer = dive.querySelector('.od-color');
var marks = dive.querySelector('.od-marks');
var photo = dive.querySelector('.od-photo img');
var copyLines = dive.querySelectorAll('.od-copy .ov-line-in');
var copyCard = dive.querySelector('.od-copy');
// Numeral geometry, measured from the stage.
var g = { W: 0, H: 0, cx: 0, cy: 0, orx: 0, ory: 0, irx: 0, iry: 0, sEnd: 1 };
var state = { z: 0 };
function ellipse(cx, cy, rx, ry) {
return 'M' + (cx - rx) + ' ' + cy +
'a' + rx + ' ' + ry + ' 0 1 0 ' + (2 * rx) + ' 0' +
'a' + rx + ' ' + ry + ' 0 1 0 ' + (-2 * rx) + ' 0Z';
}
function measure() {
var W = dive.clientWidth, H = dive.clientHeight;
g.W = W; g.H = H; g.cx = W / 2; g.cy = H / 2;
// A heavy oval zero: tall, with thick sides and a narrow counter.
var ory = Math.min(H * 0.43, (W * 0.44) / 0.74);
g.ory = ory; g.orx = ory * 0.74;
g.irx = g.orx * 0.44; g.iry = ory * 0.7;
// Scale at which the counter swallows the stage corners, plus margin.
g.sEnd = Math.sqrt(Math.pow(g.cx / g.irx, 2) + Math.pow(g.cy / g.iry, 2)) * 1.15;
svg.setAttribute('viewBox', '0 0 ' + W + ' ' + H);
}
// Redraw the vector at scale s; geometry is rebuilt each frame so edges stay crisp.
function draw() {
var s = Math.exp(state.z * Math.log(g.sEnd));
var cx = g.cx, cy = g.cy, W = g.W, H = g.H;
var pad = Math.max(W, H) * s;
field.setAttribute('d',
'M' + (cx - pad) + ' ' + (cy - pad) + 'H' + (cx + pad) + 'V' + (cy + pad) + 'H' + (cx - pad) + 'Z' +
ellipse(cx, cy, g.orx * s, g.ory * s));
ring.setAttribute('d',
ellipse(cx, cy, g.orx * s + 1, g.ory * s + 1) + ellipse(cx, cy, g.irx * s, g.iry * s));
gsap.set(marks, { scale: s, autoAlpha: Math.max(0, 1 - state.z * 5) });
}
measure();
gsap.set(colorLayer, { autoAlpha: 1 });
gsap.set(photo, { scale: 1.4 });
gsap.set(copyCard, { autoAlpha: 0, y: 40 });
gsap.set(copyLines, { yPercent: 115 });
ScrollTrigger.addEventListener('refreshInit', function(){ measure(); draw(); });
var tl = gsap.timeline({
defaults: { ease: 'none' },
scrollTrigger: {
trigger: dive, start: 'top top', end: '+=340%',
pin: true, scrub: true, anticipatePin: 1, invalidateOnRefresh: true
}
});
tl.to(state, { z: 1, duration: 6, onUpdate: draw }, 0) // dive through the counter
.to(photo, { scale: 1.15, duration: 6 }, 0)
.set(colorLayer, { autoAlpha: 0 }, 6)
.to(photo, { scale: 1, duration: 3 }, 6) // photo settles
.to(copyCard, { autoAlpha: 1, y: 0, duration: 1.2 }, 6.4) // copy rises in
.to(copyLines, { yPercent: 0, duration: 1.1, stagger: 0.28 }, 6.8)
.to({}, { duration: 1.2 }); // hold
draw();
})();
</script>
<!-- ============ 02 BENEFITS: why zero — sticky stat + four benefit rows ============ -->
<style>
.ob{
position:relative;background:var(--soft);border-radius:40px 40px 0 0;
padding:var(--section-y) var(--gutter);
}
.ob-grid{
display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.15fr);
gap:clamp(32px,6vw,110px);max-width:1320px;margin:0 auto;align-items:start;
}
.ob-side{position:sticky;top:clamp(96px,14vh,130px)}
.ob-lede{margin:22px 0 0;max-width:400px}
/* the big rolling stat */
.ob-stat{margin-top:clamp(28px,5vh,52px);padding-top:22px;border-top:1.5px solid var(--forest-24)}
.ob-stat-win{
position:relative;overflow:hidden;
height:clamp(78px,9.5vw,138px);
}
.ob-stat-val{
position:absolute;left:0;top:0;white-space:nowrap;
font-family:var(--display);font-weight:800;color:var(--forest);
font-size:clamp(68px,8.4vw,122px);line-height:1.1;letter-spacing:-.05em;
will-change:transform;
}
.ob-stat-val small{font-size:.36em;letter-spacing:-.02em;color:var(--tang);margin-left:.14em;font-weight:700}
.ob-stat-label{margin:8px 0 0;font-size:13px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--ink)}
.ob-stat-dots{display:flex;gap:6px;margin-top:18px}
.ob-stat-dots i{width:22px;height:4px;border-radius:4px;background:var(--forest-24);transition:background .35s,width .35s}
.ob-stat-dots i.is-on{background:var(--tang);width:40px}
/* rows */
.ob-list{list-style:none;margin:0;padding:0}
.ob-row{position:relative;padding:clamp(28px,5vh,48px) 0 clamp(36px,7vh,72px)}
.ob-rule{position:absolute;left:0;right:0;top:0;height:1.5px;background:var(--forest);transform-origin:0 50%}
.ob-row-head{display:flex;align-items:baseline;gap:clamp(14px,2vw,26px)}
.ob-idx{
flex:none;font-family:var(--display);font-weight:800;color:var(--tang);
font-size:clamp(40px,5vw,72px);line-height:1;letter-spacing:-.05em;
}
.ob-row h3{
margin:0;font-family:var(--display);font-weight:700;color:var(--forest);
font-size:clamp(22px,2.3vw,32px);line-height:1.08;letter-spacing:-.03em;
}
.ob-row p{margin:18px 0 0;max-width:480px;padding-left:calc(clamp(40px,5vw,72px)*1.25 + clamp(14px,2vw,26px))}
.ob-rstat{
display:flex;align-items:baseline;gap:10px;margin-top:20px;
padding-left:calc(clamp(40px,5vw,72px)*1.25 + clamp(14px,2vw,26px));
}
.ob-rstat b{font-family:var(--display);font-weight:800;color:var(--forest);font-size:clamp(34px,4vw,48px);line-height:1;letter-spacing:-.04em}
.ob-rstat b small{font-size:.45em;color:var(--tang);margin-left:.12em}
.ob-rstat span{font-size:13px;font-weight:600;letter-spacing:.08em;text-transform:uppercase}
/* motion on, desktop: stat lives in the sticky column, inactive rows fade */
html.ov-motion .ob-row{transition:opacity .5s ease}
@media (min-width:861px){
html.ov-motion .ob-row{opacity:.35}
html.ov-motion .ob-row.is-active{opacity:1}
html.ov-motion .ob-rstat{display:none}
}
/* no motion: the sticky stat is dropped, each row carries its own */
html:not(.ov-motion) .ob-stat{display:none}
@media (max-width:860px){
.ob{border-radius:28px 28px 0 0}
.ob-grid{grid-template-columns:minmax(0,1fr);gap:28px}
.ob-side{position:static}
.ob-stat{display:none}
.ob-row p,.ob-rstat{padding-left:0}
.ob-row{padding:26px 0 34px}
}
</style>
<section id="benefits" class="ob" aria-labelledby="ob-title">
<div class="ob-grid">
<div class="ob-side">
<p class="ov-eyebrow">Why zero</p>
<h2 class="ov-h2 ob-title" id="ob-title"><span class="ov-line"><span class="ov-line-in">Keep the toast.</span></span><span class="ov-line"><span class="ov-line-in"><em>Lose</em> the fog.</span></span></h2>
<p class="ob-lede">Oval pours like a proper nightcap: bitter, aromatic, built to be sipped slowly. It just leaves tomorrow exactly as you planned it.</p>
<div class="ob-stat" aria-hidden="true">
<div class="ob-stat-win"><span class="ob-stat-val">0.0<small>%</small></span></div>
<p class="ob-stat-label">alcohol by volume</p>
<div class="ob-stat-dots"><i class="is-on"></i><i></i><i></i><i></i></div>
</div>
</div>
<ol class="ob-list">
<li class="ob-row is-active" data-num="0.0" data-dec="1" data-unit="%" data-label="alcohol by volume">
<span class="ob-rule" aria-hidden="true"></span>
<div class="ob-row-head"><span class="ob-idx">01</span><h3>Wake up on the right side of Sunday</h3></div>
<p>No haze, no half-day lost to the sofa. A second glass costs you nothing but another ice cube, so the early run, the market or the long lie-in stays yours.</p>
<div class="ob-rstat"><b>0.0<small>%</small></b><span>alcohol by volume</span></div>
</li>
<li class="ob-row" data-num="3" data-dec="0" data-unit="" data-label="botanicals, distilled one by one">
<span class="ob-rule" aria-hidden="true"></span>
<div class="ob-row-head"><span class="ob-idx">02</span><h3>All of the ritual, none of the regret</h3></div>
<p>The heavy glass, the long pour, the twist of peel. Rosemary, pink grapefruit and wild juniper give it the bite and the slow finish you reach for at the end of a day.</p>
<div class="ob-rstat"><b>3</b><span>botanicals, distilled one by one</span></div>
</li>
<li class="ob-row" data-num="30" data-dec="0" data-unit="kcal" data-label="in a 50 ml serve">
<span class="ob-rule" aria-hidden="true"></span>
<div class="ob-row-head"><span class="ob-idx">03</span><h3>Light enough for a Tuesday</h3></div>
<p>Thirty calories a serve and no added sugar. Top it with soda and a wedge and you have a weeknight drink that doesn't need a weekend to recover from.</p>
<div class="ob-rstat"><b>30<small>kcal</small></b><span>in a 50 ml serve</span></div>
</li>
<li class="ob-row" data-num="1" data-dec="0" data-unit="farm" data-label="where it's grown and stilled">
<span class="ob-rule" aria-hidden="true"></span>
<div class="ob-row-head"><span class="ob-idx">04</span><h3>Grown and stilled in one place</h3></div>
<p>Every leaf and peel is picked a short walk from the copper still, then distilled the same week. You can taste the hillside, not the supply chain.</p>
<div class="ob-rstat"><b>1<small>farm</small></b><span>where it's grown and stilled</span></div>
</li>
</ol>
</div>
</section>
<script>
(function () {
if (OV.reduce) return;
var sec = document.getElementById('benefits');
var rows = [].slice.call(sec.querySelectorAll('.ob-row'));
var win = sec.querySelector('.ob-stat-win');
var label = sec.querySelector('.ob-stat-label');
var dots = [].slice.call(sec.querySelectorAll('.ob-stat-dots i'));
var title = sec.querySelector('.ob-title');
var current = 0;
function fmt(n, dec, unit){
return n.toFixed(dec) + (unit ? '<small>' + unit + '</small>' : '');
}
// Swap the big stat: old value slides up and out, new one slides up in and counts to its target.
function show(i){
if (i === current) return;
var dir = i > current ? 1 : -1;
current = i;
rows.forEach(function(r, k){ r.classList.toggle('is-active', k === i); });
dots.forEach(function(d, k){ d.classList.toggle('is-on', k === i); });
var d = rows[i].dataset, target = parseFloat(d.num), dec = +d.dec;
var old = win.querySelectorAll('.ob-stat-val');
old.forEach(function(o){
gsap.killTweensOf(o);
gsap.to(o, { yPercent: -110 * dir, duration: .55, ease: 'power3.in', onComplete: function(){ o.remove(); } });
});
var el = document.createElement('span');
el.className = 'ob-stat-val';
el.innerHTML = fmt(0, dec, d.unit);
win.appendChild(el);
var c = { v: 0 };
gsap.fromTo(el, { yPercent: 110 * dir }, { yPercent: 0, duration: .75, ease: 'power3.out', delay: .12 });
gsap.to(c, { v: target, duration: .9, ease: 'power2.out', delay: .12,
onUpdate: function(){ el.innerHTML = fmt(c.v, dec, d.unit); } });
gsap.fromTo(label, { autoAlpha: 0, y: 8 }, { autoAlpha: 1, y: 0, duration: .5, delay: .3, overwrite: true,
onStart: function(){ label.textContent = d.label; } });
}
rows.forEach(function(row, i){
ScrollTrigger.create({
trigger: row, start: 'top 55%', end: 'bottom 55%',
onToggle: function(self){ if (self.isActive) show(i); }
});
// hairline draws in as the row arrives
gsap.fromTo(row.querySelector('.ob-rule'), { scaleX: 0 }, {
scaleX: 1, duration: 1.2, ease: 'power3.inOut',
scrollTrigger: { trigger: row, start: 'top 85%', toggleActions: 'play none none reverse' }
});
gsap.from(row.querySelectorAll('.ob-row-head, p, .ob-rstat'), {
y: 26, autoAlpha: 0, duration: .9, ease: 'power3.out', stagger: .08,
scrollTrigger: { trigger: row, start: 'top 80%', toggleActions: 'play none none reverse' }
});
});
// heading + lede rise in once
(document.fonts ? document.fonts.ready : Promise.resolve()).then(function(){
var lines = title.querySelectorAll('.ov-line-in');
var st = { trigger: sec, start: 'top 75%', toggleActions: 'play none none reverse' };
gsap.from(lines, { yPercent: 110, duration: 1, ease: 'power4.out', stagger: .09, scrollTrigger: st });
gsap.from(sec.querySelectorAll('.ob-side .ov-eyebrow, .ob-lede, .ob-stat'), {
y: 24, autoAlpha: 0, duration: .9, ease: 'power3.out', stagger: .1, delay: .15,
scrollTrigger: { trigger: sec, start: 'top 75%', toggleActions: 'play none none reverse' }
});
ScrollTrigger.refresh();
});
})();
</script>
<!-- ============ 03 RANGE: three bottles + discovery set ============ -->
<style>
.or{background:var(--soft);padding:var(--section-y) var(--gutter);position:relative}
.or-head{display:grid;grid-template-columns:1.3fr 1fr;gap:32px;align-items:end;margin-bottom:clamp(36px,6vh,64px)}
.or-head p.or-lede{margin:0;max-width:420px;justify-self:end}
.or-track{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(3,1fr);gap:clamp(16px,2vw,28px)}
.or-item{display:flex}
.or-card{
position:relative;display:flex;flex-direction:column;width:100%;overflow:hidden;
background:var(--white);border-radius:var(--radius);padding:12px;
box-shadow:0 1px 0 var(--forest-12);transition:transform .45s cubic-bezier(.2,.8,.2,1),box-shadow .45s;
}
html.ov-motion .or-card:hover,html.ov-motion .or-card:focus-within{transform:translateY(-10px);box-shadow:var(--shadow)}
/* photo frame: botanical by default, the serve on hover */
.or-frame{position:relative;border-radius:16px;overflow:hidden;aspect-ratio:1/1.02;background:var(--forest-12)}
.or-media,.or-zoom{position:absolute;inset:0}
.or-zoom img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;transition:opacity .6s ease,transform 1.1s cubic-bezier(.2,.8,.2,1)}
.or-zoom .or-img-serve{opacity:0}
.or-card:hover .or-img-serve,.or-card:focus-within .or-img-serve{opacity:1}
.or-card:hover .or-img-bot,.or-card:focus-within .or-img-bot{opacity:0}
html.ov-motion .or-zoom .or-img-serve{transform:scale(1.12)}
html.ov-motion .or-card:hover .or-img-serve,html.ov-motion .or-card:focus-within .or-img-serve{transform:scale(1)}
html.ov-motion .or-card:hover .or-img-bot,html.ov-motion .or-card:focus-within .or-img-bot{transform:scale(1.08)}
.or-flag{
position:absolute;left:12px;top:12px;z-index:2;display:inline-flex;align-items:center;gap:8px;
background:var(--soft-94);color:var(--forest);font-size:12px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;
padding:6px 11px;border-radius:999px;
}
.or-dot{width:9px;height:9px;border-radius:50%;background:var(--forest)}
.or-dot--tang{background:var(--tang)}
.or-dot--lime{background:var(--lime);box-shadow:inset 0 0 0 1.5px var(--forest)}
.or-flag-alt{display:none}
.or-card:hover .or-flag-def,.or-card:focus-within .or-flag-def{display:none}
.or-card:hover .or-flag-alt,.or-card:focus-within .or-flag-alt{display:inline}
/* text body with the outlined zero behind it */
.or-body{position:relative;flex:1;display:flex;flex-direction:column;padding:22px 10px 8px}
.or-zero{
position:absolute;right:-14px;bottom:-40px;z-index:0;pointer-events:none;user-select:none;
font-family:var(--display);font-weight:800;font-size:clamp(200px,20vw,300px);line-height:.8;
color:transparent;-webkit-text-stroke:2px var(--forest-12);
}
.or-body > *:not(.or-zero){position:relative;z-index:1}
.or-meta{display:flex;justify-content:space-between;gap:12px;font-size:13px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--forest);opacity:.75}
.or-name{margin:10px 0 8px;font-family:var(--display);font-weight:700;color:var(--forest);font-size:clamp(26px,2.4vw,34px);line-height:1;letter-spacing:-.03em}
.or-desc{margin:0 0 16px;font-size:16px}
.or-chips{list-style:none;margin:0 0 16px;padding:0;display:flex;flex-wrap:wrap;gap:6px}
.or-chips li{font-size:13px;font-weight:600;color:var(--forest);background:var(--soft);border:1.5px solid var(--forest-12);border-radius:999px;padding:5px 11px}
.or-best{margin:0 0 22px;font-size:15px;color:var(--ink)}
.or-best b{color:var(--tang);font-weight:600}
.or-buy{margin-top:auto;display:flex;align-items:center;justify-content:space-between;gap:12px;padding-top:16px;border-top:1.5px solid var(--forest-12)}
.or-price{font-family:var(--display);font-weight:700;font-size:26px;color:var(--forest);letter-spacing:-.02em}
.or-price small{display:block;font-family:var(--body);font-size:12px;font-weight:500;letter-spacing:.04em;color:var(--ink);opacity:.7}
/* discovery set */
.or-set{
position:relative;margin-top:clamp(28px,4vw,44px);display:grid;grid-template-columns:auto 1fr auto;gap:clamp(20px,3vw,44px);align-items:center;
background:var(--soft);border:1.5px solid var(--forest-24);border-radius:var(--radius);padding:clamp(22px,3vw,36px);color:var(--forest);
}
.or-trio{display:flex}
.or-trio img{width:clamp(64px,7vw,92px);height:clamp(64px,7vw,92px);border-radius:50%;object-fit:cover;border:4px solid var(--soft)}
.or-trio img + img{margin-left:-22px}
.or-set h3{margin:0 0 8px;font-family:var(--display);font-weight:700;font-size:clamp(22px,2.4vw,32px);line-height:1.05;letter-spacing:-.03em}
.or-set p{margin:0;color:var(--ink);max-width:520px}
.or-set .ov-eyebrow{margin-bottom:8px}
.or-set-buy{display:flex;align-items:center;gap:18px}
.or-badge{
width:92px;height:92px;border-radius:50%;background:var(--tang);color:var(--ink);flex:none;
display:grid;place-items:center;text-align:center;transform:rotate(-8deg);
font-family:var(--display);font-weight:800;font-size:26px;line-height:1;letter-spacing:-.03em;
}
.or-badge small{display:block;font-family:var(--body);font-weight:600;font-size:11px;letter-spacing:.08em;text-transform:uppercase;margin-top:3px}
/* toast */
.or-toast{
position:fixed;left:50%;bottom:22px;z-index:60;max-width:calc(100vw - 32px);
display:flex;align-items:center;gap:10px;background:var(--forest);color:var(--soft);
font-weight:600;font-size:15px;padding:12px 20px;border-radius:999px;box-shadow:var(--shadow);
transform:translate(-50%,0);opacity:0;visibility:hidden;pointer-events:none;
transition:opacity .35s ease,visibility 0s linear .35s;
}
.or-toast::before{content:"";width:9px;height:9px;border-radius:50%;background:var(--lime);flex:none}
.or-toast.is-on{opacity:1;visibility:visible;transition:opacity .35s ease}
html.ov-motion .or-toast{transform:translate(-50%,160%);transition:opacity .35s ease,transform .5s cubic-bezier(.2,.8,.2,1),visibility 0s linear .5s}
html.ov-motion .or-toast.is-on{transform:translate(-50%,0);transition:opacity .35s ease,transform .5s cubic-bezier(.2,1.3,.4,1)}
@media (max-width:1100px){
.or-set{grid-template-columns:1fr auto}
.or-trio{grid-column:1/-1}
}
@media (max-width:860px){
.or-head{grid-template-columns:1fr;gap:18px}
.or-head p.or-lede{justify-self:start}
.or-track{
display:flex;overflow-x:auto;overscroll-behavior-x:contain;scroll-snap-type:x mandatory;
margin:0 calc(-1 * var(--gutter));padding:14px var(--gutter) 20px;scroll-padding:0 var(--gutter);
scrollbar-width:none;
}
.or-track::-webkit-scrollbar{display:none}
.or-item{flex:0 0 min(82%,340px);scroll-snap-align:start}
.or-zero{font-size:220px}
.or-set{grid-template-columns:1fr}
.or-set-buy{justify-content:space-between}
}
</style>
<section id="range" class="or" aria-labelledby="or-title">
<div class="or-head">
<div>
<p class="ov-eyebrow">The range</p>
<h2 class="ov-h2 or-title" id="or-title">Three plants. Choose your <em>zero.</em></h2>
</div>
<p class="or-lede">Each bottle is distilled from a single botanical, so what you taste is the plant we picked. Pour it over ice, lengthen it with tonic, or build it into something slower.</p>
</div>
<ul class="or-track">
<li class="or-item">
<article class="or-card" data-name="N°01 Rosemary">
<div class="or-frame">
<span class="or-flag"><span class="or-dot"></span><span class="or-flag-def">Resin · green</span><span class="or-flag-alt">The serve</span></span>
<div class="or-media"><div class="or-zoom">
<img class="or-img-bot" src="assets/n1.jpg" alt="Upright stems of fresh rosemary growing in a garden bed" loading="lazy">
<img class="or-img-serve" src="assets/range-rosemary.jpg" alt="A clear long drink with ice and a rosemary sprig in a stemmed glass, outdoors" loading="lazy">
</div></div>
</div>
<div class="or-body">
<span class="or-zero" aria-hidden="true">0</span>
<div class="or-meta"><span>N°01</span><span>50cl · 0.0%</span></div>
<h3 class="or-name">Rosemary</h3>
<p class="or-desc">Resinous and green, like brushing past the hedge on the way home.</p>
<ul class="or-chips" aria-label="Tasting notes"><li>Pine resin</li><li>Bay leaf</li><li>Cracked pepper</li></ul>
<p class="or-best"><b>Best with</b> — light tonic, a bruised sprig and a long table.</p>
<div class="or-buy">
<span class="or-price">£28<small>50cl bottle</small></span>
<button class="ov-pill or-add" type="button">Add to basket</button>
</div>
</div>
</article>
</li>
<li class="or-item">
<article class="or-card" data-name="N°02 Grapefruit">
<div class="or-frame">
<span class="or-flag"><span class="or-dot or-dot--tang"></span><span class="or-flag-def">Zest · bright</span><span class="or-flag-alt">The serve</span></span>
<div class="or-media"><div class="or-zoom">
<img class="or-img-bot" src="assets/n2.jpg" alt="Halved pink grapefruit showing its ruby flesh" loading="lazy">
<img class="or-img-serve" src="assets/range-grapefruit.jpg" alt="A short glass over ice with a wedge of citrus on the rim, in bright sunlight" loading="lazy">
</div></div>
</div>
<div class="or-body">
<span class="or-zero" aria-hidden="true">0</span>
<div class="or-meta"><span>N°02</span><span>50cl · 0.0%</span></div>
<h3 class="or-name">Grapefruit</h3>
<p class="or-desc">Pink zest up front, a clean bitter snap of pith at the finish.</p>
<ul class="or-chips" aria-label="Tasting notes"><li>Pink zest</li><li>Bitter pith</li><li>Sea salt</li></ul>
<p class="or-best"><b>Best with</b> — soda, a salted rim and a brunch that runs late.</p>
<div class="or-buy">
<span class="or-price">£28<small>50cl bottle</small></span>
<button class="ov-pill or-add" type="button">Add to basket</button>
</div>
</div>
</article>
</li>
<li class="or-item">
<article class="or-card" data-name="N°03 Juniper">
<div class="or-frame">
<span class="or-flag"><span class="or-dot or-dot--lime"></span><span class="or-flag-def">Spice · dry</span><span class="or-flag-alt">The serve</span></span>
<div class="or-media"><div class="or-zoom">
<img class="or-img-bot" src="assets/n3.jpg" alt="A juniper branch heavy with dusty blue berries" loading="lazy">
<img class="or-img-serve" src="assets/range-juniper.jpg" alt="A clear glass bottle with a blank label beside an empty tumbler" loading="lazy">
</div></div>
</div>
<div class="or-body">
<span class="or-zero" aria-hidden="true">0</span>
<div class="or-meta"><span>N°03</span><span>50cl · 0.0%</span></div>
<h3 class="or-name">Juniper</h3>
<p class="or-desc">Dry and peppery, with cold forest air on the back of the throat.</p>
<ul class="or-chips" aria-label="Tasting notes"><li>Wild juniper</li><li>Cardamom</li><li>Cedar</li></ul>
<p class="or-best"><b>Best with</b> — a bone-dry tonic and a wide twist of lemon peel.</p>
<div class="or-buy">
<span class="or-price">£30<small>50cl bottle</small></span>
<button class="ov-pill or-add" type="button">Add to basket</button>
</div>
</div>
</article>
</li>
</ul>
<div class="or-set" data-name="Discovery set">
<div class="or-trio" aria-hidden="true">
<img src="assets/n1.jpg" alt="" loading="lazy">
<img src="assets/n2.jpg" alt="" loading="lazy">
<img src="assets/n3.jpg" alt="" loading="lazy">
</div>
<div>
<p class="ov-eyebrow">Discovery set</p>
<h3>Can't pick one? Take all three.</h3>
<p>Rosemary, Grapefruit and Juniper in 20cl bottles — enough for a dozen long drinks and one firm favourite.</p>
</div>
<div class="or-set-buy">
<span class="or-badge">£36<small>3 × 20cl</small></span>
<button class="ov-pill or-add" type="button">Add the set</button>
</div>
</div>
<div class="or-toast" role="status" aria-live="polite"></div>
</section>
<script>
(function () {
var sec = document.getElementById('range');
// pure front-end "basket": a toast that rises, holds 2s, then fades
var toast = sec.querySelector('.or-toast'), timer;
sec.querySelectorAll('.or-add').forEach(function (btn) {
btn.addEventListener('click', function () {
var host = btn.closest('[data-name]');
toast.textContent = 'Added — ' + host.getAttribute('data-name');
toast.classList.remove('is-on');
void toast.offsetWidth;
toast.classList.add('is-on');
clearTimeout(timer);
timer = setTimeout(function () { toast.classList.remove('is-on'); }, 2000);
});
});
if (OV.reduce) return;
// heading lines
var title = sec.querySelector('.or-title');
(document.fonts ? document.fonts.ready : Promise.resolve()).then(function () {
var lines = OV.lines(title);
gsap.from(lines, { yPercent: 110, duration: 1, ease: 'power4.out', stagger: .1,
scrollTrigger: { trigger: sec, start: 'top 80%', once: true } });
});
gsap.from(['.or-head .ov-eyebrow', '.or-lede'], { y: 24, autoAlpha: 0, duration: .9, ease: 'power3.out', stagger: .1,
scrollTrigger: { trigger: sec, start: 'top 80%', once: true } });
// cards rise with a slight turn; photos unmask from the bottom
var items = sec.querySelectorAll('.or-item');
var medias = sec.querySelectorAll('.or-media');
var zooms = sec.querySelectorAll('.or-zoom');
gsap.set(items, { y: 110, rotation: -3, autoAlpha: 0, transformOrigin: '0% 100%' });
gsap.set(medias, { clipPath: 'inset(100% 0% 0% 0%)' });
gsap.set(zooms, { scale: 1.25 });
gsap.timeline({ scrollTrigger: { trigger: sec.querySelector('.or-track'), start: 'top 80%', once: true } })
.to(items, { y: 0, rotation: 0, autoAlpha: 1, duration: 1.1, ease: 'power3.out', stagger: .14 }, 0)
.to(medias, { clipPath: 'inset(0% 0% 0% 0%)', duration: 1.2, ease: 'power3.inOut', stagger: .14 }, .2)
.to(zooms, { scale: 1, duration: 1.6, ease: 'power3.out', stagger: .14 }, .2);
// outlined zero drifts against the scroll
sec.querySelectorAll('.or-zero').forEach(function (z) {
gsap.fromTo(z, { yPercent: 16 }, { yPercent: -16, ease: 'none',
scrollTrigger: { trigger: z.closest('.or-card'), start: 'top bottom', end: 'bottom top', scrub: true } });
});
// discovery set + price badge pop
var set = sec.querySelector('.or-set');
gsap.timeline({ scrollTrigger: { trigger: set, start: 'top 85%', once: true } })
.from(set, { y: 40, autoAlpha: 0, duration: .9, ease: 'power3.out' })
.from(set.querySelectorAll('.or-trio img'), { scale: .6, autoAlpha: 0, duration: .6, ease: 'back.out(2)', stagger: .08 }, .2)
.fromTo(set.querySelector('.or-badge'), { scale: 0, rotation: -40 }, { scale: 1, rotation: -8, duration: .8, ease: 'back.out(2.4)' }, .45);
})();
</script>
<!-- ============ 04 RITUAL: the Oval Hour, three signature serves (pinned, stepped) ============ -->
<style>
.ot{position:relative;background:var(--lime);padding:var(--section-y) var(--gutter)}
.ot-head{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(0,1fr);gap:24px 40px;align-items:end;margin-bottom:clamp(32px,5vw,56px)}
.ot-head .ov-h2{max-width:14ch}
.ot-intro{margin:0;max-width:340px;justify-self:end;color:var(--ink)}
/* stacked (default, mobile, reduced motion) */
.ot-frame,.ot-rail{display:none}
.ot-panel{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:20px}
.ot-serve{background:var(--white);border-radius:28px;overflow:hidden;box-shadow:var(--shadow);display:flex;flex-direction:column}
.ot-serve-photo{aspect-ratio:4/5;overflow:hidden}
.ot-serve-photo img{width:100%;height:100%;object-fit:cover}
.ot-body{padding:clamp(22px,2.6vw,34px);display:flex;flex-direction:column;gap:18px;flex:1}
.ot-count{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:13px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--forest)}
.ot-count b{color:var(--tang);font-weight:600}
.ot-name{margin:0;font-family:var(--display);font-weight:700;color:var(--forest);font-size:clamp(26px,2.6vw,40px);line-height:1.02;letter-spacing:-.03em}
.ot-steps{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:10px}
.ot-steps li{display:grid;grid-template-columns:34px minmax(0,1fr);gap:10px;align-items:start;font-size:16px;line-height:1.45}
.ot-num{font-family:var(--display);font-weight:700;font-size:13px;color:var(--forest);background:var(--lime);border-radius:999px;height:26px;display:grid;place-items:center;margin-top:-1px}
.ot-meta{display:flex;flex-wrap:wrap;gap:8px;margin:auto 0 0;padding:16px 0 0;border-top:1.5px solid var(--forest-12)}
.ot-meta .ov-line{width:100%}
.ot-meta .ov-line-in{display:flex;flex-wrap:wrap;gap:8px}
.ot-meta .ov-line-in > span{font-size:14px;font-weight:600;color:var(--forest);background:var(--soft);border-radius:999px;padding:7px 13px}
.ot-meta .ov-line-in > span b{color:var(--tang);font-weight:600}
/* pinned stepped layout: desktop + motion only */
@media (min-width:861px){
html.ov-motion .ot{height:100vh;height:100svh;display:flex;flex-direction:column;padding-top:clamp(84px,12vh,120px);padding-bottom:clamp(24px,5vh,56px);overflow:hidden}
html.ov-motion .ot-head{margin-bottom:clamp(18px,3.5vh,40px);flex:none}
html.ov-motion .ot-head .ov-h2{font-size:clamp(28px,min(3.8vw,5.4vh),56px);max-width:none}
html.ov-motion .ot-stage{flex:1;min-height:0;display:grid;grid-template-columns:auto 64px minmax(0,560px);grid-template-rows:100%;gap:clamp(20px,3vw,48px);justify-content:center}
html.ov-motion .ot-frame{display:block;position:relative;height:100%;aspect-ratio:4/5;border-radius:28px;overflow:hidden;background:var(--soft);box-shadow:var(--shadow)}
html.ov-motion .ot-frame img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;will-change:clip-path,transform}
html.ov-motion .ot-frame img + img{clip-path:inset(100% 0 0 0)}
html.ov-motion .ot-rail{display:flex;position:relative;flex-direction:column;justify-content:space-between;align-items:center;padding:6px 0}
html.ov-motion .ot-rail-track,html.ov-motion .ot-rail-fill{position:absolute;left:50%;top:24px;bottom:24px;width:2px;margin-left:-1px;border-radius:2px}
html.ov-motion .ot-rail-track{background:var(--forest-24)}
html.ov-motion .ot-rail-fill{background:var(--forest);transform-origin:50% 0;transform:scaleY(0)}
html.ov-motion .ot-panel{display:grid;grid-template-columns:minmax(0,1fr);align-self:center;background:var(--white);border-radius:28px;box-shadow:var(--shadow);max-height:100%}
html.ov-motion .ot-serve{grid-area:1/1;background:none;box-shadow:none;border-radius:0}
html.ov-motion .ot-serve-photo{display:none}
html.ov-motion .ot-body{padding:clamp(24px,3.6vh,44px) clamp(24px,2.8vw,44px);gap:clamp(12px,2.2vh,22px)}
html.ov-motion .ot-name{font-size:clamp(28px,min(3.2vw,5.4vh),48px)}
html.ov-motion .ot-steps{gap:clamp(6px,1.3vh,12px)}
html.ov-motion .ot-steps li{font-size:clamp(15px,2vh,17px)}
html.ov-motion .ot-serve:not(:first-child){pointer-events:none}
}
.ot-dot{
position:relative;z-index:1;width:46px;height:46px;border-radius:50%;border:0;cursor:pointer;padding:0;
background:var(--soft);color:var(--forest);font-family:var(--display);font-weight:700;font-size:13px;
box-shadow:inset 0 0 0 1.5px var(--forest-24);transition:background .3s,color .3s,box-shadow .3s,transform .3s;
}
.ot-dot:hover{transform:scale(1.06)}
.ot-dot.is-on{background:var(--forest);color:var(--lime);box-shadow:none}
.ot-dot.is-now{background:var(--tang);color:var(--ink)}
.ot-dot:focus-visible{outline:2px solid var(--forest);outline-offset:3px}
@media (max-width:1100px){ .ot-panel{grid-template-columns:repeat(2,minmax(0,1fr))} }
@media (max-width:860px){
.ot-head{grid-template-columns:1fr}
.ot-intro{justify-self:start}
.ot-panel{grid-template-columns:1fr;gap:18px}
.ot-serve-photo{aspect-ratio:4/4.4}
}
@media (max-width:420px){
.ot-steps li{font-size:15.5px;grid-template-columns:30px minmax(0,1fr)}
}
</style>
<section id="ritual" class="ot" aria-labelledby="ot-title">
<div class="ot-head">
<div>
<p class="ov-eyebrow">The Oval Hour</p>
<h2 class="ov-h2" id="ot-title">Three glasses, one hour, <em>nothing</em> to sleep off.</h2>
</div>
<p class="ot-intro">Our signature serves need a glass, some ice and about two minutes. No shaker skills, no bar cart, no morning after.</p>
</div>
<div class="ot-stage">
<div class="ot-frame" aria-hidden="true">
<img src="assets/ritual-1.jpg" alt="">
<img src="assets/ritual-2.jpg" alt="">
<img src="assets/ritual-3.jpg" alt="">
</div>
<div class="ot-rail" role="group" aria-label="Choose a serve">
<span class="ot-rail-track"></span><span class="ot-rail-fill"></span>
<button class="ot-dot is-on is-now" type="button" data-step="0" aria-label="Serve 01, Garden Tonic">01</button>
<button class="ot-dot" type="button" data-step="1" aria-label="Serve 02, Pink Hour Spritz">02</button>
<button class="ot-dot" type="button" data-step="2" aria-label="Serve 03, Juniper Sour">03</button>
</div>
<div class="ot-panel">
<article class="ot-serve">
<div class="ot-serve-photo"><img src="assets/ritual-1.jpg" alt="A tall copa glass of sparkling tonic with lemon wheels and a rosemary sprig on a wooden bar" loading="lazy"></div>
<div class="ot-body">
<div class="ot-count ot-t"><span class="ov-line"><span class="ov-line-in">Serve <b>01</b> / 03 · Oval Rosemary</span></span></div>
<h3 class="ot-name ot-t"><span class="ov-line"><span class="ov-line-in">Garden Tonic</span></span></h3>
<ol class="ot-steps">
<li><span class="ot-num">1</span><span class="ov-line ot-t"><span class="ov-line-in">Pack a copa glass to the brim with large, clear ice.</span></span></li>
<li><span class="ot-num">2</span><span class="ov-line ot-t"><span class="ov-line-in">Pour in 50 ml of Oval Rosemary.</span></span></li>
<li><span class="ot-num">3</span><span class="ov-line ot-t"><span class="ov-line-in">Top with 120 ml of cold tonic, poured slowly down a bar spoon.</span></span></li>
<li><span class="ot-num">4</span><span class="ov-line ot-t"><span class="ov-line-in">Twist a strip of lemon peel over the glass, then drop it in.</span></span></li>
</ol>
<p class="ot-meta ot-t"><span class="ov-line"><span class="ov-line-in"><span>Glass: <b>Copa</b></span> <span>Time: <b>2 min</b></span> <span>0.0% ABV</span></span></span></p>
</div>
</article>
<article class="ot-serve">
<div class="ot-serve-photo"><img src="assets/ritual-2.jpg" alt="A pale orange spritz in a stemmed glass with a straw, on a sunny patio table beside lavender" loading="lazy"></div>
<div class="ot-body">
<div class="ot-count ot-t"><span class="ov-line"><span class="ov-line-in">Serve <b>02</b> / 03 · Oval Grapefruit</span></span></div>
<h3 class="ot-name ot-t"><span class="ov-line"><span class="ov-line-in">Pink Hour Spritz</span></span></h3>
<ol class="ot-steps">
<li><span class="ot-num">1</span><span class="ov-line ot-t"><span class="ov-line-in">Fill a large wine glass two-thirds with ice.</span></span></li>
<li><span class="ot-num">2</span><span class="ov-line ot-t"><span class="ov-line-in">Add 50 ml of Oval Grapefruit and a squeeze of fresh pink grapefruit.</span></span></li>
<li><span class="ot-num">3</span><span class="ov-line ot-t"><span class="ov-line-in">Lengthen with 100 ml of soda water and stir once.</span></span></li>
<li><span class="ot-num">4</span><span class="ov-line ot-t"><span class="ov-line-in">Clap a sprig of thyme between your palms and tuck it in.</span></span></li>
</ol>
<p class="ot-meta ot-t"><span class="ov-line"><span class="ov-line-in"><span>Glass: <b>Wine glass</b></span> <span>Time: <b>2 min</b></span> <span>0.0% ABV</span></span></span></p>
</div>
</article>
<article class="ot-serve">
<div class="ot-serve-photo"><img src="assets/ritual-3.jpg" alt="A frothy amber sour in a coupe with an orange wheel, standing on a pale stone table" loading="lazy"></div>
<div class="ot-body">
<div class="ot-count ot-t"><span class="ov-line"><span class="ov-line-in">Serve <b>03</b> / 03 · Oval Juniper</span></span></div>
<h3 class="ot-name ot-t"><span class="ov-line"><span class="ov-line-in">Juniper Sour</span></span></h3>
<ol class="ot-steps">
<li><span class="ot-num">1</span><span class="ov-line ot-t"><span class="ov-line-in">Add 50 ml Oval Juniper, 25 ml lemon juice and 15 ml syrup to a shaker.</span></span></li>
<li><span class="ot-num">2</span><span class="ov-line ot-t"><span class="ov-line-in">Add 25 ml of aquafaba and shake hard, without ice, for 15 seconds.</span></span></li>
<li><span class="ot-num">3</span><span class="ov-line ot-t"><span class="ov-line-in">Add ice and shake again until the tin feels frosty.</span></span></li>
<li><span class="ot-num">4</span><span class="ov-line ot-t"><span class="ov-line-in">Double-strain into a chilled coupe and let the foam settle.</span></span></li>
</ol>
<p class="ot-meta ot-t"><span class="ov-line"><span class="ov-line-in"><span>Glass: <b>Coupe</b></span> <span>Time: <b>2 min</b></span> <span>0.0% ABV</span></span></span></p>
</div>
</article>
</div>
</div>
</section>
<script>
(function () {
if (OV.reduce) return;
var sec = document.getElementById('ritual');
var frameImgs = sec.querySelectorAll('.ot-frame img');
var serves = sec.querySelectorAll('.ot-serve');
var dots = sec.querySelectorAll('.ot-dot');
var fill = sec.querySelector('.ot-rail-fill');
var title = sec.querySelector('.ov-h2');
var mm = gsap.matchMedia();
// the heading rises in once, at every size
(document.fonts ? document.fonts.ready : Promise.resolve()).then(function(){
var lines = OV.lines(title);
gsap.from(lines, { yPercent: 110, duration: 1, ease: 'power4.out', stagger: .1,
scrollTrigger: { trigger: sec, start: 'top 75%', once: true } });
gsap.from(sec.querySelectorAll('.ot-head .ov-eyebrow, .ot-intro'), { y: 20, autoAlpha: 0, duration: .8, ease: 'power3.out', stagger: .12,
scrollTrigger: { trigger: sec, start: 'top 75%', once: true } });
});
// desktop: pinned, three stepped serves
mm.add('(min-width: 861px)', function(){
var groups = [].map.call(serves, function(s){ return s.querySelectorAll('.ot-t .ov-line-in, .ot-t.ov-line > .ov-line-in'); });
groups.forEach(function(g, i){ if (i) gsap.set(g, { yPercent: 115 }); });
// step badges sit outside the line masks, so they fade/pop instead of sliding
var nums = [].map.call(serves, function(s){ return s.querySelectorAll('.ot-num'); });
nums.forEach(function(n, i){ if (i) gsap.set(n, { autoAlpha: 0, scale: .4 }); });
gsap.set([].slice.call(frameImgs, 1), { clipPath: 'inset(100% 0% 0% 0%)', scale: 1.15 });
gsap.set(fill, { scaleY: 0 });
var current = 0;
function mark(i){
if (i === current) return;
current = i;
dots.forEach(function(d, k){ d.classList.toggle('is-on', k <= i); d.classList.toggle('is-now', k === i); });
serves.forEach(function(s, k){ s.style.pointerEvents = k === i ? 'auto' : 'none'; });
}
var tl = gsap.timeline({
defaults: { ease: 'none' },
scrollTrigger: {
trigger: sec, start: 'top top', end: '+=250%',
pin: true, scrub: .8, anticipatePin: 1, invalidateOnRefresh: true,
snap: { snapTo: 'labels', duration: .6, delay: .05, ease: 'power2.inOut' },
onUpdate: function(self){ mark(Math.round(self.progress * 2)); }
}
});
tl.addLabel('s0', 0);
[1, 2].forEach(function(i){
var at = i - 1 + .15;
tl.to(groups[i - 1], { yPercent: -115, duration: .35, stagger: .03, ease: 'power2.in' }, at)
.fromTo(frameImgs[i], { clipPath: 'inset(100% 0% 0% 0%)', scale: 1.15 },
{ clipPath: 'inset(0% 0% 0% 0%)', scale: 1, duration: .7, ease: 'power2.inOut' }, at)
.fromTo(frameImgs[i - 1], { scale: 1 }, { scale: 1.06, duration: .7 }, at)
.to(nums[i - 1], { autoAlpha: 0, scale: .4, duration: .25, stagger: .03, ease: 'power2.in' }, at)
.fromTo(nums[i], { autoAlpha: 0, scale: .4 }, { autoAlpha: 1, scale: 1, duration: .35, stagger: .04, ease: 'back.out(2)', immediateRender: false }, at + .35)
.fromTo(groups[i], { yPercent: 115 }, { yPercent: 0, duration: .4, stagger: .03, ease: 'power3.out', immediateRender: false }, at + .3)
.addLabel('s' + i, i);
});
tl.to(fill, { scaleY: 1, duration: 2 }, 0);
tl.set({}, {}, 2);
function go(e){
var st = tl.scrollTrigger, i = +e.currentTarget.getAttribute('data-step');
var y = st.start + (st.end - st.start) * (i / 2);
if (OV.lenis) OV.lenis.scrollTo(y, { duration: 1.1 }); else window.scrollTo(0, y);
}
dots.forEach(function(d){ d.addEventListener('click', go); });
return function(){
dots.forEach(function(d){ d.removeEventListener('click', go); });
serves.forEach(function(s){ s.style.pointerEvents = ''; });
};
});
// mobile: stacked cards, simple fade-ups
mm.add('(max-width: 860px)', function(){
serves.forEach(function(s){
gsap.from(s, { y: 48, autoAlpha: 0, duration: .9, ease: 'power3.out',
scrollTrigger: { trigger: s, start: 'top 88%', once: true } });
});
});
})();
</script>
<!-- ============ 05 REVIEWS: press marquee + aggregate + customer cards ============ -->
<style>
.ov5{
position:relative;background:var(--white);color:var(--ink);
border-radius:40px 40px 0 0;padding:clamp(56px,9vh,110px) 0 var(--section-y);
overflow:hidden;
}
/* press marquee */
.ov5-marquee{overflow:hidden;width:100%;padding:6px 0 10px;border-bottom:1.5px solid var(--forest-12)}
.ov5-track{display:flex;width:max-content;will-change:transform}
.ov5-group{display:flex;align-items:center;flex:none}
.ov5-name{
font-family:var(--display);font-weight:700;color:var(--forest);white-space:nowrap;
font-size:clamp(40px,7vw,96px);line-height:1.1;letter-spacing:-.04em;padding:0 .32em;
}
.ov5-sep{color:var(--tang);font-size:clamp(28px,4.2vw,60px);line-height:1;flex:none}
/* pull quotes */
.ov5-quotes{
list-style:none;margin:0;padding:clamp(26px,4vh,40px) var(--gutter) 0;
display:grid;grid-template-columns:repeat(5,1fr);gap:clamp(16px,2.4vw,32px);
}
.ov5-quotes li{border-top:2px solid var(--tang);padding-top:14px}
.ov5-quotes q{display:block;font-size:15.5px;line-height:1.45;color:var(--ink);quotes:"\201C" "\201D"}
.ov5-quotes cite{display:block;margin-top:10px;font-style:normal;font-size:12px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--forest)}
/* heading + aggregate */
.ov5-head{
padding:clamp(64px,10vh,120px) var(--gutter) clamp(36px,5vh,56px);
display:grid;grid-template-columns:1.3fr 1fr;gap:32px;align-items:end;
}
.ov5-score{
justify-self:end;display:flex;align-items:center;gap:18px;
background:var(--soft);border-radius:var(--radius);padding:18px 24px;
}
.ov5-num{font-family:var(--display);font-weight:800;color:var(--forest);font-size:clamp(44px,5vw,68px);line-height:1;letter-spacing:-.04em}
.ov5-num small{font-size:.42em;color:var(--tang);letter-spacing:0;margin-left:4px}
.ov5-score p{margin:6px 0 0;font-size:15px;line-height:1.35}
.ov5-score b{color:var(--forest)}
/* stars */
.ov5-stars{position:relative;display:inline-block;height:18px;line-height:0}
.ov5-stars svg{display:block;height:18px;width:auto}
.ov5-stars-base{color:var(--forest-24)}
.ov5-stars-fill{position:absolute;inset:0 auto 0 0;width:100%;overflow:hidden;color:var(--tang)}
.ov5-stars-fill svg{width:calc(18px * 122 / 22);max-width:none}
.ov5-stars--lg,.ov5-stars--lg svg{height:22px}
.ov5-stars--lg .ov5-stars-fill svg{width:122px}
/* review cards */
.ov5-grid{
padding:0 var(--gutter);display:grid;grid-template-columns:repeat(3,1fr);
gap:clamp(16px,2vw,26px);align-items:start;
}
.ov5-col{display:flex;flex-direction:column;gap:clamp(16px,2vw,26px)}
.ov5-col:nth-child(2){margin-top:clamp(40px,6vw,90px)}
.ov5-col:nth-child(3){margin-top:clamp(16px,2.5vw,36px)}
.ov5-card{
background:var(--soft);border-radius:var(--radius);padding:clamp(22px,2.4vw,32px);
display:flex;flex-direction:column;gap:14px;
}
.ov5-card blockquote{margin:0;font-size:17px;line-height:1.5}
.ov5-card blockquote strong{font-family:var(--display);font-weight:700;color:var(--forest);font-size:19px;letter-spacing:-.02em;display:block;margin-bottom:8px;line-height:1.2}
.ov5-meta{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;padding-top:14px;border-top:1.5px solid var(--forest-12)}
.ov5-who{font-weight:600;color:var(--forest);font-size:15px}
.ov5-who span{display:block;font-weight:400;color:var(--ink);font-size:13.5px;opacity:.8}
.ov5-bottle{font-size:12px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--forest);background:var(--white);border-radius:999px;padding:6px 11px}
.ov5-bottle::before{content:"";display:inline-block;width:7px;height:9px;border-radius:50%;background:var(--tang);margin-right:7px;vertical-align:0}
.ov5-foot{padding:clamp(36px,5vh,56px) var(--gutter) 0;text-align:center}
/* reduced motion: static marquee that wraps */
html:not(.ov-motion) .ov5-track{width:auto;flex-wrap:wrap;justify-content:center}
html:not(.ov-motion) .ov5-group{flex-wrap:wrap;justify-content:center}
html:not(.ov-motion) .ov5-group[aria-hidden]{display:none}
html:not(.ov-motion) .ov5-name{white-space:normal;text-align:center}
@media (max-width:1100px){
.ov5-quotes{grid-template-columns:repeat(3,1fr)}
}
@media (max-width:860px){
.ov5{border-radius:28px 28px 0 0}
.ov5-name{font-size:clamp(34px,10vw,52px)}
.ov5-sep{font-size:clamp(24px,7vw,36px)}
.ov5-quotes{grid-template-columns:1fr 1fr}
.ov5-head{grid-template-columns:1fr}
.ov5-score{justify-self:start}
.ov5-grid{grid-template-columns:1fr}
.ov5-col:nth-child(2),.ov5-col:nth-child(3){margin-top:0}
}
@media (max-width:520px){
.ov5-quotes{grid-template-columns:1fr}
}
</style>
<section id="reviews" class="ov5" aria-labelledby="ov5-title">
<div class="ov5-marquee" aria-label="As featured in">
<div class="ov5-track">
<div class="ov5-group">
<span class="ov5-name">The Sober Table</span><span class="ov5-sep" aria-hidden="true">✺</span>
<span class="ov5-name">Field & Glass</span><span class="ov5-sep" aria-hidden="true">✺</span>
<span class="ov5-name">Weekend Pour</span><span class="ov5-sep" aria-hidden="true">✺</span>
<span class="ov5-name">Kitchen Notes</span><span class="ov5-sep" aria-hidden="true">✺</span>
<span class="ov5-name">The Long Lunch</span><span class="ov5-sep" aria-hidden="true">✺</span>
</div>
<div class="ov5-group" aria-hidden="true">
<span class="ov5-name">The Sober Table</span><span class="ov5-sep">✺</span>
<span class="ov5-name">Field & Glass</span><span class="ov5-sep">✺</span>
<span class="ov5-name">Weekend Pour</span><span class="ov5-sep">✺</span>
<span class="ov5-name">Kitchen Notes</span><span class="ov5-sep">✺</span>
<span class="ov5-name">The Long Lunch</span><span class="ov5-sep">✺</span>
</div>
</div>
</div>
<ul class="ov5-quotes">
<li><q>The first alcohol-free bottle we'd happily leave out on the table.</q><cite>The Sober Table</cite></li>
<li><q>Rosemary that tastes picked, not pressed into a flavouring lab.</q><cite>Field & Glass</cite></li>
<li><q>Our new house pour for Saturdays that start before noon.</q><cite>Weekend Pour</cite></li>
<li><q>Bitter, bright and properly grown-up. Pairs with anything salty.</q><cite>Kitchen Notes</cite></li>
<li><q>Three-hour lunch, zero regret. The juniper does the heavy lifting.</q><cite>The Long Lunch</cite></li>
</ul>
<div class="ov5-head">
<div>
<p class="ov-eyebrow">Word from the table</p>
<h2 class="ov-h2" id="ov5-title">Poured once. <em>Reordered</em> twice.</h2>
</div>
<div class="ov5-score">
<div class="ov5-num"><span class="ov5-count" data-to="4.9">4.9</span><small>/ 5</small></div>
<div>
<span class="ov5-stars ov5-stars--lg" role="img" aria-label="4.9 out of 5 stars">
<svg class="ov5-stars-base" viewBox="0 0 122 22" aria-hidden="true"><g fill="currentColor"><path id="ov5-s" d="M11 1.2l2.9 6.3 6.9.7-5.2 4.6 1.5 6.8L11 16.1l-6.1 3.5 1.5-6.8L1.2 8.2l6.9-.7z"/><use href="#ov5-s" x="25"/><use href="#ov5-s" x="50"/><use href="#ov5-s" x="75"/><use href="#ov5-s" x="100"/></g></svg>
<span class="ov5-stars-fill" style="width:98%"><svg viewBox="0 0 122 22" aria-hidden="true"><g fill="currentColor"><use href="#ov5-s"/><use href="#ov5-s" x="25"/><use href="#ov5-s" x="50"/><use href="#ov5-s" x="75"/><use href="#ov5-s" x="100"/></g></svg></span>
</span>
<p>from <b>2,300+ reviews</b><br>across all three bottles</p>
</div>
</div>
</div>
<div class="ov5-grid">
<div class="ov5-col">
<article class="ov5-card">
<span class="ov5-stars" role="img" aria-label="5 out of 5 stars"><svg class="ov5-stars-base" viewBox="0 0 122 22" aria-hidden="true"><g fill="currentColor"><use href="#ov5-s"/><use href="#ov5-s" x="25"/><use href="#ov5-s" x="50"/><use href="#ov5-s" x="75"/><use href="#ov5-s" x="100"/></g></svg><span class="ov5-stars-fill"><svg viewBox="0 0 122 22" aria-hidden="true"><g fill="currentColor"><use href="#ov5-s"/><use href="#ov5-s" x="25"/><use href="#ov5-s" x="50"/><use href="#ov5-s" x="75"/><use href="#ov5-s" x="100"/></g></svg></span></span>
<blockquote><strong>Finally, a proper aperitivo.</strong>Over ice with a strip of peel and nobody at dinner clocked it was zero-proof. I've stopped buying anything else for Friday nights.</blockquote>
<div class="ov5-meta"><div class="ov5-who">Priya S.<span>Bristol</span></div><span class="ov5-bottle">N°02 Grapefruit</span></div>
</article>
<article class="ov5-card">
<span class="ov5-stars" role="img" aria-label="5 out of 5 stars"><svg class="ov5-stars-base" viewBox="0 0 122 22" aria-hidden="true"><g fill="currentColor"><use href="#ov5-s"/><use href="#ov5-s" x="25"/><use href="#ov5-s" x="50"/><use href="#ov5-s" x="75"/><use href="#ov5-s" x="100"/></g></svg><span class="ov5-stars-fill"><svg viewBox="0 0 122 22" aria-hidden="true"><g fill="currentColor"><use href="#ov5-s"/><use href="#ov5-s" x="25"/><use href="#ov5-s" x="50"/><use href="#ov5-s" x="75"/><use href="#ov5-s" x="100"/></g></svg></span></span>
<blockquote><strong>Tastes like a walk.</strong>Piney, a bit resinous, dry finish. Great with tonic.</blockquote>
<div class="ov5-meta"><div class="ov5-who">Tomás R.<span>Valencia</span></div><span class="ov5-bottle">N°03 Juniper</span></div>
</article>
</div>
<div class="ov5-col">
<article class="ov5-card">
<span class="ov5-stars" role="img" aria-label="5 out of 5 stars"><svg class="ov5-stars-base" viewBox="0 0 122 22" aria-hidden="true"><g fill="currentColor"><use href="#ov5-s"/><use href="#ov5-s" x="25"/><use href="#ov5-s" x="50"/><use href="#ov5-s" x="75"/><use href="#ov5-s" x="100"/></g></svg><span class="ov5-stars-fill"><svg viewBox="0 0 122 22" aria-hidden="true"><g fill="currentColor"><use href="#ov5-s"/><use href="#ov5-s" x="25"/><use href="#ov5-s" x="50"/><use href="#ov5-s" x="75"/><use href="#ov5-s" x="100"/></g></svg></span></span>
<blockquote><strong>My pregnancy lifesaver.</strong>Nine months of lime soda was wearing thin. This has actual bite and a herby backbone, and it made the evenings feel like mine again.</blockquote>
<div class="ov5-meta"><div class="ov5-who">Hannah K.<span>Leeds</span></div><span class="ov5-bottle">N°01 Rosemary</span></div>
</article>
<article class="ov5-card">
<span class="ov5-stars" role="img" aria-label="5 out of 5 stars"><svg class="ov5-stars-base" viewBox="0 0 122 22" aria-hidden="true"><g fill="currentColor"><use href="#ov5-s"/><use href="#ov5-s" x="25"/><use href="#ov5-s" x="50"/><use href="#ov5-s" x="75"/><use href="#ov5-s" x="100"/></g></svg><span class="ov5-stars-fill"><svg viewBox="0 0 122 22" aria-hidden="true"><g fill="currentColor"><use href="#ov5-s"/><use href="#ov5-s" x="25"/><use href="#ov5-s" x="50"/><use href="#ov5-s" x="75"/><use href="#ov5-s" x="100"/></g></svg></span></span>
<blockquote><strong>Bought one. Now a subscriber.</strong>The grapefruit spritz is my go-to when friends come round.</blockquote>
<div class="ov5-meta"><div class="ov5-who">Marcus O.<span>Manchester</span></div><span class="ov5-bottle">Discovery set</span></div>
</article>
</div>
<div class="ov5-col">
<article class="ov5-card">
<span class="ov5-stars" role="img" aria-label="5 out of 5 stars"><svg class="ov5-stars-base" viewBox="0 0 122 22" aria-hidden="true"><g fill="currentColor"><use href="#ov5-s"/><use href="#ov5-s" x="25"/><use href="#ov5-s" x="50"/><use href="#ov5-s" x="75"/><use href="#ov5-s" x="100"/></g></svg><span class="ov5-stars-fill"><svg viewBox="0 0 122 22" aria-hidden="true"><g fill="currentColor"><use href="#ov5-s"/><use href="#ov5-s" x="25"/><use href="#ov5-s" x="50"/><use href="#ov5-s" x="75"/><use href="#ov5-s" x="100"/></g></svg></span></span>
<blockquote><strong>Not sweet. Thank you.</strong>Every other alcohol-free bottle I've tried tastes like cordial. This one tastes like someone actually cooked with rosemary.</blockquote>
<div class="ov5-meta"><div class="ov5-who">Elena V.<span>Lisbon</span></div><span class="ov5-bottle">N°01 Rosemary</span></div>
</article>
<article class="ov5-card">
<span class="ov5-stars" role="img" aria-label="5 out of 5 stars"><svg class="ov5-stars-base" viewBox="0 0 122 22" aria-hidden="true"><g fill="currentColor"><use href="#ov5-s"/><use href="#ov5-s" x="25"/><use href="#ov5-s" x="50"/><use href="#ov5-s" x="75"/><use href="#ov5-s" x="100"/></g></svg><span class="ov5-stars-fill"><svg viewBox="0 0 122 22" aria-hidden="true"><g fill="currentColor"><use href="#ov5-s"/><use href="#ov5-s" x="25"/><use href="#ov5-s" x="50"/><use href="#ov5-s" x="75"/><use href="#ov5-s" x="100"/></g></svg></span></span>
<blockquote><strong>Dry January, extended.</strong>Started as a one-month thing. It's September. The juniper with soda and a sprig of thyme is unbeatable after a long run.</blockquote>
<div class="ov5-meta"><div class="ov5-who">Callum J.<span>Edinburgh</span></div><span class="ov5-bottle">N°03 Juniper</span></div>
</article>
</div>
</div>
<div class="ov5-foot"><a class="ov-more" href="#range">Find your bottle <span>+</span></a></div>
</section>
<script>
(function () {
if (OV.reduce) return;
var sec = document.getElementById('reviews');
/* ---- press marquee: loop whose speed + direction follow scroll velocity ---- */
var track = sec.querySelector('.ov5-track');
var loop = gsap.to(track, { xPercent: -50, duration: 38, ease: 'none', repeat: -1 });
var dir = 1;
var proxy = { ts: 1 };
function applyTs(){ loop.timeScale(proxy.ts); }
ScrollTrigger.create({
trigger: sec, start: 'top bottom', end: 'bottom top',
onUpdate: function (self) {
var v = self.getVelocity();
if (Math.abs(v) < 5) return;
dir = v < 0 ? -1 : 1;
var boost = dir * Math.min(1 + Math.abs(v) / 260, 7);
proxy.ts = boost; applyTs();
gsap.to(proxy, { ts: dir, duration: 1.2, ease: 'power3.out', overwrite: true, onUpdate: applyTs });
}
});
// negative timeScale on a repeating tween needs time > 0 to run backwards forever
loop.totalTime(loop.duration() * 100);
/* ---- heading + aggregate ---- */
var count = sec.querySelector('.ov5-count');
var bigFill = sec.querySelector('.ov5-score .ov5-stars-fill');
var n = { v: 0 };
count.textContent = '0.0';
gsap.set(bigFill, { width: '0%' });
gsap.from('.ov5-head > div > *', {
y: 30, autoAlpha: 0, duration: .9, ease: 'power3.out', stagger: .1,
scrollTrigger: { trigger: '.ov5-head', start: 'top 82%', once: true }
});
ScrollTrigger.create({
trigger: '.ov5-score', start: 'top 88%', once: true,
onEnter: function () {
gsap.to(n, { v: 4.9, duration: 1.8, ease: 'power2.out', onUpdate: function(){ count.textContent = n.v.toFixed(1); } });
gsap.to(bigFill, { width: '98%', duration: 1.8, ease: 'power2.out' });
}
});
/* ---- press quotes ---- */
gsap.from(sec.querySelectorAll('.ov5-quotes li'), {
y: 24, autoAlpha: 0, duration: .8, ease: 'power3.out', stagger: .08,
scrollTrigger: { trigger: '.ov5-quotes', start: 'top 88%', once: true }
});
/* ---- review cards: rise + unrotate, then stars fill left to right ---- */
var cards = gsap.utils.toArray(sec.querySelectorAll('.ov5-card'));
cards.forEach(function (card) {
gsap.set(card, { y: 60, autoAlpha: 0, rotation: gsap.utils.random(-2, 2, .1) });
gsap.set(card.querySelector('.ov5-stars-fill'), { width: '0%' });
});
ScrollTrigger.batch(cards, {
start: 'top 90%', once: true,
onEnter: function (batch) {
var tl = gsap.timeline();
tl.to(batch, { y: 0, rotation: 0, autoAlpha: 1, duration: 1, ease: 'power3.out', stagger: .12 });
batch.forEach(function (card, i) {
tl.to(card.querySelector('.ov5-stars-fill'), { width: '100%', duration: .9, ease: 'power2.inOut' }, .35 + i * .12);
});
}
});
})();
</script>
<!-- ============ 06 CTA: raise a zero (oval photo reveal) + stockists + newsletter ============ -->
<style>
.oc{background:var(--lime);padding:var(--section-y) 0 calc(var(--section-y) * .8);overflow:hidden}
/* ---- the closing moment ---- */
.oc-stage{
height:100vh;height:100svh;min-height:560px;
display:flex;flex-direction:column;
padding:clamp(72px,11vh,110px) var(--gutter) clamp(20px,4vh,40px);
}
.oc-head{display:flex;align-items:flex-end;justify-content:space-between;gap:24px 40px;flex-wrap:wrap;margin-bottom:clamp(18px,3vh,32px)}
.oc-title{
margin:0;font-family:var(--display);font-weight:800;color:var(--forest);
font-size:clamp(40px,8.4vw,128px);line-height:.95;letter-spacing:-.045em;
}
.oc-title em{font-style:normal;color:var(--tang)}
.oc-w{display:inline-block;overflow:hidden;vertical-align:top;padding-bottom:.1em;margin-bottom:-.1em}
.oc-w > span{display:inline-block;will-change:transform}
.oc-sub{margin:0;max-width:330px;font-size:17px}
.oc-oval{position:relative;flex:1;min-height:0;overflow:hidden;border-radius:clamp(24px,4vw,56px)}
.oc-photo{
position:absolute;inset:0;overflow:hidden;background:var(--soft);border-radius:clamp(24px,4vw,56px);
-webkit-clip-path:ellipse(75% 80% at 50% 50%);clip-path:ellipse(75% 80% at 50% 50%);
}
.oc-photo img{width:100%;height:100%;object-fit:cover;object-position:50% 45%;transform-origin:50% 50%}
.oc-ring{
position:absolute;left:50%;top:50%;width:36%;height:60%;
transform:translate(-50%,-50%);border:clamp(8px,1vw,14px) solid var(--forest);border-radius:50%;
pointer-events:none;opacity:0;
}
.oc-badge{
position:absolute;right:clamp(12px,3vw,40px);bottom:clamp(12px,3vh,30px);
background:var(--soft-94);color:var(--forest);border-radius:999px;padding:10px 16px;
font-size:13px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;box-shadow:var(--shadow);
}
.oc-badge b{color:var(--tang);font-weight:600}
/* ---- cards ---- */
.oc-cards{
display:grid;grid-template-columns:1.15fr 1fr;gap:clamp(16px,2vw,28px);
padding:clamp(40px,7vh,80px) var(--gutter) 0;
}
.oc-card{
background:var(--soft);border-radius:var(--radius);box-shadow:var(--shadow);
padding:clamp(24px,3.4vw,48px);display:flex;flex-direction:column;
}
.oc-card h3{
margin:0 0 10px;font-family:var(--display);font-weight:700;color:var(--forest);
font-size:clamp(24px,2.4vw,34px);line-height:1.05;letter-spacing:-.03em;
}
.oc-card h3 em{font-style:normal;color:var(--tang)}
.oc-card > p{margin:0 0 22px}
.oc-shops{
list-style:none;margin:0 0 26px;padding:0;
display:grid;grid-template-columns:1fr 1fr;column-gap:28px;
}
.oc-shops li{
display:flex;justify-content:space-between;align-items:baseline;gap:10px;
padding:13px 0;border-bottom:1.5px solid var(--forest-12);transition:color .25s;
}
.oc-shops strong{font-weight:600;color:var(--ink)}
.oc-shops span{font-size:14px;color:var(--forest);white-space:nowrap}
.oc-shops li.is-near strong{color:var(--forest)}
.oc-shops li.is-near span{color:var(--ink);background:var(--tang-20);border-radius:999px;padding:2px 9px}
.oc-form{margin-top:auto}
.oc-label{display:block;font-size:13px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--forest);margin-bottom:8px}
.oc-row{display:flex;gap:10px}
.oc-input{
flex:1;min-width:0;font:inherit;font-size:16px;color:var(--ink);background:var(--white);
border:1.5px solid var(--forest-24);border-radius:999px;padding:11px 18px;
transition:border-color .2s,box-shadow .2s;
}
.oc-input::placeholder{color:rgba(31,59,44,.5)}
.oc-input:focus{outline:none;border-color:var(--tang);box-shadow:0 0 0 3px var(--tang-20),0 0 0 1.5px var(--tang)}
.oc-input[aria-invalid="true"]{border-color:var(--tang)}
.oc .ov-pill:focus-visible{outline:3px solid var(--tang);outline-offset:3px}
.oc-msg{min-height:1.55em;margin:10px 0 0;font-size:15px;color:var(--ink)}
.oc-msg.is-error::before{content:"";display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--tang);margin-right:8px;vertical-align:1px}
.oc-msg.is-ok{font-weight:600;color:var(--forest)}
.oc-card--join{background:var(--forest);color:var(--soft)}
.oc-card--join h3{color:var(--lime)}
.oc-card--join .oc-label{color:var(--lime)}
.oc-card--join .oc-input{border-color:transparent}
.oc-card--join .oc-input:focus,.oc-card--join .oc-input[aria-invalid="true"]{border-color:var(--tang)}
.oc-card--join .ov-pill{background:var(--lime);color:var(--forest)}
.oc-card--join .ov-pill:hover{background:var(--tang);color:var(--ink)}
.oc-card--join .oc-msg{color:var(--soft)}
.oc-card--join .oc-msg.is-ok{color:var(--lime)}
.oc-perks{list-style:none;margin:0 0 26px;padding:0;display:flex;flex-wrap:wrap;gap:8px}
.oc-perks li{font-size:14px;border:1.5px solid rgba(244,251,208,.3);border-radius:999px;padding:6px 12px}
.oc-done{display:none;align-items:center;gap:14px;margin-top:4px}
.oc-done-mark{flex:none;width:30px;height:40px;border:7px solid var(--lime);border-radius:50%}
.oc-form.is-sent .oc-row,.oc-form.is-sent .oc-label{display:none}
.oc-form.is-sent .oc-done{display:flex}
.oc-form.is-sent .oc-msg{display:none}
.oc-done p{margin:0;font-weight:600;color:var(--lime)}
@media (max-width:860px){
.oc-stage{height:88vh;height:88svh;min-height:520px}
.oc-head{margin-bottom:16px}
.oc-sub{max-width:none;font-size:16px}
.oc-cards{grid-template-columns:1fr}
.oc-ring{width:60%;height:52%}
}
@media (max-width:520px){
.oc-shops{grid-template-columns:1fr}
.oc-row{flex-direction:column}
.oc-row .ov-pill{justify-content:center}
.oc-badge{font-size:12px;padding:8px 13px}
}
</style>
<section id="stockists" class="oc" aria-labelledby="oc-title">
<div class="oc-stage">
<div class="oc-head">
<h2 class="oc-title" id="oc-title">
<span class="oc-w"><span>Raise</span></span>
<span class="oc-w"><span>a</span></span>
<span class="oc-w"><span><em>zero.</em></span></span>
</h2>
<p class="oc-sub">Long tables, loud laughs, clear heads in the morning. Pour Oval wherever the good company gathers.</p>
</div>
<div class="oc-oval">
<div class="oc-photo">
<img src="assets/cta.jpg" alt="Friends round a garden table in daylight, clinking glasses over salads, bread and a carafe of juice" loading="lazy">
</div>
<div class="oc-ring" aria-hidden="true"></div>
<span class="oc-badge">Cheers at <b>0.0%</b></span>
</div>
</div>
<div class="oc-cards">
<div class="oc-card oc-card--shops">
<p class="ov-eyebrow">Stockists</p>
<h3>Find a bottle <em>nearby</em></h3>
<p>Our favourite delis, bottle shops and grocers already have Oval on the shelf.</p>
<ul class="oc-shops">
<li data-pc="BS"><strong>Greengage Deli</strong><span>Bristol</span></li>
<li data-pc="M"><strong>The Pantry Row</strong><span>Manchester</span></li>
<li data-pc="EH"><strong>Fennel & Fig</strong><span>Edinburgh</span></li>
<li data-pc="BN"><strong>Low Tide Grocer</strong><span>Brighton</span></li>
<li data-pc="LS"><strong>Quince Cellar</strong><span>Leeds</span></li>
<li data-pc="E"><strong>Hedgerow Market</strong><span>London</span></li>
</ul>
<form class="oc-form oc-find" novalidate>
<label class="oc-label" for="oc-postcode">Find near you</label>
<div class="oc-row">
<input class="oc-input" id="oc-postcode" name="postcode" type="text" inputmode="text" autocomplete="postal-code" placeholder="Postcode, e.g. BS1 4DJ" aria-describedby="oc-find-msg">
<button class="ov-pill" type="submit">Search</button>
</div>
<p class="oc-msg" id="oc-find-msg" role="status" aria-live="polite"></p>
</form>
</div>
<div class="oc-card oc-card--join" id="join">
<p class="ov-eyebrow">Newsletter</p>
<h3>Join the Zero Club</h3>
<p>New botanicals, seasonal serves and first dibs on small batches, about twice a month.</p>
<ul class="oc-perks">
<li>Early batch access</li>
<li>Serve cards</li>
<li>Members' tastings</li>
</ul>
<form class="oc-form oc-join" novalidate>
<label class="oc-label" for="oc-email">Email address</label>
<div class="oc-row">
<input class="oc-input" id="oc-email" name="email" type="email" autocomplete="email" placeholder="
[email protected]" required aria-describedby="oc-join-msg">
<button class="ov-pill" type="submit">Join</button>
</div>
<p class="oc-msg" id="oc-join-msg" role="status" aria-live="polite"></p>
<div class="oc-done" role="status" aria-live="polite">
<span class="oc-done-mark" aria-hidden="true"></span>
<p class="oc-done-text"></p>
</div>
</form>
</div>
</div>
</section>
<script>
(function () {
var root = document.getElementById('stockists');
/* ---- newsletter: front-end only ---- */
var join = root.querySelector('.oc-join');
var email = join.querySelector('#oc-email');
var joinMsg = join.querySelector('#oc-join-msg');
function setMsg(el, text, kind){ el.textContent = text; el.className = 'oc-msg' + (kind ? ' is-' + kind : ''); }
join.addEventListener('submit', function (e) {
e.preventDefault();
var v = email.value.trim();
if (!v) { email.setAttribute('aria-invalid', 'true'); setMsg(joinMsg, 'Pop your email in first and we will save you a seat.', 'error'); email.focus(); return; }
if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v)) { email.setAttribute('aria-invalid', 'true'); setMsg(joinMsg, 'That email looks a little off. Mind checking it?', 'error'); email.focus(); return; }
email.removeAttribute('aria-invalid');
setMsg(joinMsg, '', '');
join.querySelector('.oc-done-text').textContent = "You're in. First pour's on us — check your inbox.";
join.classList.add('is-sent');
});
email.addEventListener('input', function(){ if (email.hasAttribute('aria-invalid')) { email.removeAttribute('aria-invalid'); setMsg(joinMsg, '', ''); } });
/* ---- stockist finder: matches the postcode area to a shop ---- */
var find = root.querySelector('.oc-find');
var pc = find.querySelector('#oc-postcode');
var findMsg = find.querySelector('#oc-find-msg');
var shops = root.querySelectorAll('.oc-shops li');
find.addEventListener('submit', function (e) {
e.preventDefault();
var v = pc.value.trim().toUpperCase().replace(/\s+/g, ' ');
shops.forEach(function(li){ li.classList.remove('is-near'); });
if (!/^[A-Z]{1,2}\d[A-Z\d]?( ?\d[A-Z]{2})?$/.test(v)) { pc.setAttribute('aria-invalid', 'true'); setMsg(findMsg, 'Try a postcode like BS1 4DJ or just the first half, like M4.', 'error'); pc.focus(); return; }
pc.removeAttribute('aria-invalid');
var area = v.match(/^[A-Z]+/)[0], hit = null;
shops.forEach(function(li){ if (!hit && li.dataset.pc === area) hit = li; });
if (hit) { hit.classList.add('is-near'); setMsg(findMsg, hit.querySelector('strong').textContent + ' is right on your doorstep.', 'ok'); }
else setMsg(findMsg, 'Nothing in ' + area + ' just yet. Order online and we will post it to you.', '');
});
if (OV.reduce) return;
/* ---- the oval reveal ---- */
var stage = root.querySelector('.oc-stage');
var photo = root.querySelector('.oc-photo');
var img = photo.querySelector('img');
var ring = root.querySelector('.oc-ring');
var words = root.querySelectorAll('.oc-w > span');
var sub = root.querySelector('.oc-sub');
var badge = root.querySelector('.oc-badge');
var mobile = matchMedia('(max-width:860px)');
// ellipse radii in % of the frame: small tall oval -> wide soft-cornered frame
var shape = { rx: 18, ry: 30 };
function from(){ return mobile.matches ? { rx: 28, ry: 34 } : { rx: 18, ry: 30 }; }
function paint(){
var c = 'ellipse(' + shape.rx + '% ' + shape.ry + '% at 50% 50%)';
photo.style.clipPath = c; photo.style.webkitClipPath = c;
// the ring hugs the clip edge (radius% of width/height -> diameter%)
ring.style.width = (shape.rx * 2) + '%';
ring.style.height = (shape.ry * 2) + '%';
}
gsap.set(words, { yPercent: 110 });
gsap.set(sub, { autoAlpha: 0, y: 20 });
gsap.set(badge, { autoAlpha: 0, y: 16 });
gsap.set(ring, { opacity: 1 });
gsap.set(img, { scale: 1.3 });
var s0 = from(); shape.rx = s0.rx; shape.ry = s0.ry; paint();
// headline rises as the stage arrives
gsap.to(words, {
yPercent: 0, duration: 1.1, ease: 'power4.out', stagger: .09,
scrollTrigger: { trigger: stage, start: 'top 70%', toggleActions: 'play none none reverse' }
});
gsap.to(sub, {
autoAlpha: 1, y: 0, duration: .9, ease: 'power3.out', delay: .3,
scrollTrigger: { trigger: stage, start: 'top 70%', toggleActions: 'play none none reverse' }
});
var tl = gsap.timeline({
defaults: { ease: 'none' },
scrollTrigger: {
trigger: stage, start: 'top top', end: '+=110%',
pin: true, scrub: true, anticipatePin: 1, invalidateOnRefresh: true
}
});
tl.to(shape, { rx: 75, ry: 80, duration: 5, ease: 'power1.inOut', onUpdate: paint }, 0)
.to(img, { scale: 1, duration: 5, ease: 'power1.out' }, 0)
.to(ring, { opacity: 0, duration: 1.3 }, 1.8)
.to(badge, { autoAlpha: 1, y: 0, duration: 1 }, 4.4)
.to({}, { duration: .8 });
mobile.addEventListener('change', function(){ ScrollTrigger.refresh(); });
ScrollTrigger.addEventListener('refreshInit', function(){
if (tl.progress() === 0) { var s = from(); shape.rx = s.rx; shape.ry = s.ry; paint(); }
});
/* ---- cards rise in ---- */
gsap.from(root.querySelectorAll('.oc-card'), {
y: 70, autoAlpha: 0, duration: 1.1, ease: 'power3.out', stagger: .14,
scrollTrigger: { trigger: root.querySelector('.oc-cards'), start: 'top 85%', toggleActions: 'play none none reverse' }
});
})();
</script>
<!-- ============ 07 FOOTER: link columns + the big oval wordmark ============ -->
<style>
.of{
background:var(--soft);color:var(--ink);border-radius:40px 40px 0 0;
padding:clamp(56px,9vh,96px) var(--gutter) 22px;overflow:hidden;
}
.of-top{display:grid;grid-template-columns:1.2fr repeat(4,1fr);gap:32px}
.of-intro p{margin:0 0 18px;max-width:280px;font-family:var(--display);font-weight:700;color:var(--forest);font-size:clamp(20px,1.8vw,26px);line-height:1.12;letter-spacing:-.025em}
.of-intro p em{font-style:normal;color:var(--tang)}
.of-col h3{margin:0 0 14px;font-size:13px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--tang)}
.of-col ul{list-style:none;margin:0;padding:0;display:grid;gap:9px}
.of-col a{text-decoration:none;font-weight:500;color:var(--ink);background:linear-gradient(var(--tang),var(--tang)) 0 100%/0 2px no-repeat;transition:color .2s,background-size .3s}
.of-col a:hover{color:var(--forest);background-size:100% 2px}
.of a:focus-visible,.of-top-btn:focus-visible{outline:3px solid var(--tang);outline-offset:3px;border-radius:4px}
.of-mark{
margin:clamp(40px,7vh,80px) 0 0;display:flex;justify-content:center;align-items:flex-end;
font-family:var(--display);font-weight:800;color:var(--forest);
font-size:22vw;line-height:.86;letter-spacing:-.05em;white-space:nowrap;
}
.of-l{display:inline-block;overflow:hidden;padding:0 .02em .04em}
.of-l > span{display:inline-block;will-change:transform}
/* the O is the logo mark: a tall oval ring sitting on the baseline */
.of-o{
display:inline-block;width:.6em;height:.72em;margin-right:.04em;
border:.15em solid var(--forest);border-radius:50%;
}
.of-base{
display:flex;justify-content:space-between;align-items:center;gap:14px;flex-wrap:wrap;
border-top:1.5px solid var(--forest-12);margin-top:clamp(14px,2vh,24px);padding-top:18px;font-size:14px;
}
.of-base p{margin:0}
.of-top-btn{
display:inline-flex;align-items:center;gap:8px;background:transparent;border:0;cursor:pointer;
color:var(--forest);font-weight:600;font-size:14px;padding:6px 0;
}
.of-top-btn span{
display:inline-grid;place-items:center;width:32px;height:32px;border-radius:50%;
background:var(--forest);color:var(--lime);transition:background .25s,color .25s,transform .25s;
}
.of-top-btn:hover span{background:var(--tang);color:var(--ink);transform:translateY(-3px)}
@media (max-width:860px){
.of-top{grid-template-columns:1fr 1fr;gap:32px 20px}
.of-intro{grid-column:1 / -1}
.of{border-radius:28px 28px 0 0}
}
</style>
<footer class="of">
<div class="of-top">
<div class="of-intro">
<p>All of the ritual, <em>none</em> of the proof.</p>
<a class="ov-pill" href="#join">Join the Zero Club</a>
</div>
<nav class="of-col" aria-label="Shop">
<h3>Shop</h3>
<ul>
<li><a href="#range">The range</a></li>
<li><a href="#range">N°01 Rosemary</a></li>
<li><a href="#range">N°02 Grapefruit</a></li>
<li><a href="#range">N°03 Juniper</a></li>
<li><a href="#stockists">Discovery set</a></li>
</ul>
</nav>
<nav class="of-col" aria-label="Learn">
<h3>Learn</h3>
<ul>
<li><a href="#benefits">Why zero</a></li>
<li><a href="#ritual">Serve rituals</a></li>
<li><a href="#ritual">Botanical notes</a></li>
<li><a href="#benefits">How we distil</a></li>
</ul>
</nav>
<nav class="of-col" aria-label="Company">
<h3>Company</h3>
<ul>
<li><a href="#top">Our garden</a></li>
<li><a href="#stockists">Stockists</a></li>
<li><a href="#stockists">Wholesale</a></li>
<li><a href="#join">Contact</a></li>
</ul>
</nav>
<nav class="of-col" aria-label="Follow">
<h3>Follow</h3>
<ul>
<li><a href="#join">Instagram</a></li>
<li><a href="#join">Pinterest</a></li>
<li><a href="#join">TikTok</a></li>
<li><a href="#join">Newsletter</a></li>
</ul>
</nav>
</div>
<p class="of-mark" aria-label="Oval">
<span class="of-l" aria-hidden="true"><span><i class="of-o"></i></span></span><span class="of-l" aria-hidden="true"><span>v</span></span><span class="of-l" aria-hidden="true"><span>a</span></span><span class="of-l" aria-hidden="true"><span>l</span></span>
</p>
<div class="of-base">
<p>© 2026 Oval Botanicals · 0.0% ABV · Drink curious.</p>
<button class="of-top-btn" type="button">Back to top <span aria-hidden="true">↑</span></button>
</div>
</footer>
<script>
(function () {
var foot = document.querySelector('.of');
foot.querySelector('.of-top-btn').addEventListener('click', function(){
if (window.OV && OV.lenis) OV.lenis.scrollTo(0, { duration: 1.6 });
else window.scrollTo({ top: 0, behavior: 'smooth' });
});
if (OV.reduce) return;
// wordmark letters climb out of their masks over the last stretch of the page
gsap.fromTo(foot.querySelectorAll('.of-l > span'), { yPercent: 100 }, {
yPercent: 0, ease: 'none', stagger: .15,
scrollTrigger: { trigger: foot, start: 'top bottom', end: 'bottom bottom', scrub: .6 }
});
gsap.from(foot.querySelectorAll('.of-intro, .of-col'), {
y: 36, autoAlpha: 0, duration: .9, ease: 'power3.out', stagger: .08,
scrollTrigger: { trigger: foot, start: 'top 85%', toggleActions: 'play none none reverse' }
});
})();
</script>
</body>
</html>
```
## `sections/01-hero.html`
```html
<!-- ============ 01 HERO: nav + intro + the dive through the zero ============ -->
<style>
.oh-nav{
position:fixed;top:0;left:0;right:0;z-index:50;
display:flex;align-items:center;justify-content:space-between;gap:16px;
padding:18px var(--gutter);transition:background .35s,box-shadow .35s,padding .35s;
}
.oh-nav.is-scrolled{background:var(--soft-94);box-shadow:0 1px 0 var(--forest-12);padding-top:12px;padding-bottom:12px;backdrop-filter:blur(10px)}
.oh-logo{
display:inline-flex;align-items:center;gap:10px;text-decoration:none;
font-family:var(--display);font-weight:800;font-size:22px;color:var(--forest);letter-spacing:-.02em;
}
.oh-logo-mark{width:18px;height:24px;border:5px solid var(--forest);border-radius:50%}
.oh-links{display:flex;gap:30px;list-style:none;margin:0;padding:0}
.oh-links a{text-decoration:none;font-weight:500;color:var(--ink);transition:color .2s}
.oh-links a:hover{color:var(--tang)}
.oh-intro{
padding:clamp(120px,18vh,180px) var(--gutter) clamp(28px,5vh,56px);
display:grid;grid-template-columns:1.4fr 1fr;gap:32px;align-items:end;
}
.oh-intro h1{
margin:0;font-family:var(--display);font-weight:700;color:var(--forest);
font-size:clamp(34px,6vw,86px);line-height:.98;letter-spacing:-.035em;
}
.oh-intro h1 em{font-style:normal;color:var(--tang)}
.oh-side{max-width:360px;justify-self:end}
.oh-side p{margin:0 0 18px}
/* the dive */
.od{position:relative;height:100vh;height:100svh;overflow:hidden;background:var(--soft)}
.od-photo{position:absolute;inset:0;overflow:hidden}
.od-photo img{width:100%;height:100%;object-fit:cover;object-position:32% 50%;transform-origin:50% 50%}
.od-color{position:absolute;inset:0;display:none;pointer-events:none}
.od-color svg{position:absolute;inset:0;width:100%;height:100%;display:block}
.od-marks{position:absolute;inset:0;transform-origin:50% 50%}
.od-mark{position:absolute;font-size:13px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--forest)}
.od-mark--tl{top:90px;left:var(--gutter)}
.od-mark--tr{top:90px;right:var(--gutter);text-align:right}
.od-mark--bl{bottom:28px;left:var(--gutter)}
.od-mark--br{bottom:28px;right:var(--gutter);text-align:right}
.od-mark b{color:var(--tang);font-weight:600}
.od-copy{
position:absolute;right:var(--gutter);bottom:clamp(20px,7vh,72px);
width:min(460px,calc(100% - 2*var(--gutter)));
background:var(--soft-94);border-radius:var(--radius);padding:clamp(22px,3vw,36px);
box-shadow:var(--shadow);
}
.od-copy h2{
margin:0 0 16px;font-family:var(--display);font-weight:800;color:var(--forest);
font-size:clamp(30px,3.6vw,50px);line-height:1;letter-spacing:-.035em;
}
.od-copy p.od-body{margin:0 0 20px}
html.ov-motion .od-color{display:block}
@media (max-width:860px){
.oh-links{display:none}
.oh-intro{grid-template-columns:1fr}
.oh-side{justify-self:start}
.od-photo img{object-position:36% 30%}
.od-copy{left:var(--gutter);right:var(--gutter);width:auto;bottom:16px}
.od-mark--tr,.od-mark--bl{display:none}
}
@media (max-width:420px){
.oh-nav .ov-pill{padding:10px 16px;font-size:14px}
.od-copy p.od-body{font-size:15.5px}
}
</style>
<header class="oh-nav">
<a class="oh-logo" href="#top"><span class="oh-logo-mark" aria-hidden="true"></span>Oval</a>
<ul class="oh-links">
<li><a href="#benefits">Why zero</a></li>
<li><a href="#range">The range</a></li>
<li><a href="#ritual">Ritual</a></li>
<li><a href="#stockists">Stockists</a></li>
</ul>
<a class="ov-pill" href="#range">Shop the range</a>
</header>
<section class="oh-intro" id="top">
<h1 class="oh-title">Everything the evening wants. <em>Nothing</em> it doesn't.</h1>
<div class="oh-side">
<p>Garden-grown botanical spirits, distilled for long dinners, early mornings and every hour between.</p>
<span class="ov-tag">0.0% ABV</span>
</div>
</section>
<section class="od" aria-label="Zero proof, full flavour">
<div class="od-photo">
<img src="assets/reveal.jpg" alt="A pale botanical drink with a mint sprig in a coupe glass, set on a sunlit white wall beside rosemary bushes">
</div>
<div class="od-color" aria-hidden="true">
<svg class="od-svg" preserveAspectRatio="none">
<path class="od-field" fill="#E4F76B" fill-rule="evenodd"/>
<path class="od-ring" fill="#245C3E" fill-rule="evenodd"/>
</svg>
<div class="od-marks">
<span class="od-mark od-mark--tl">N°<b>0</b> — the zero-proof edition</span>
<span class="od-mark od-mark--tr">Distilled in small batches</span>
<span class="od-mark od-mark--bl">Rosemary · Grapefruit · Juniper</span>
<span class="od-mark od-mark--br">Scroll to step inside ↓</span>
</div>
</div>
<div class="od-copy">
<p class="ov-eyebrow"><span class="ov-line"><span class="ov-line-in">Distilled, never diluted</span></span></p>
<h2>
<span class="ov-line"><span class="ov-line-in">Zero proof.</span></span>
<span class="ov-line"><span class="ov-line-in">Full flavour.</span></span>
</h2>
<p class="od-body">
<span class="ov-line"><span class="ov-line-in">Every bottle starts in the garden:</span></span>
<span class="ov-line"><span class="ov-line-in">rosemary, pink grapefruit and wild</span></span>
<span class="ov-line"><span class="ov-line-in">juniper, each distilled on its own</span></span>
<span class="ov-line"><span class="ov-line-in">for all the bite, none of the buzz.</span></span>
</p>
<span class="ov-line"><span class="ov-line-in"><a class="ov-more" href="#benefits">Find out more <span>+</span></a></span></span>
</div>
</section>
<script>
(function () {
// nav background once the page moves
var nav = document.querySelector('.oh-nav');
function navState(){ nav.classList.toggle('is-scrolled', window.scrollY > 40); }
window.addEventListener('scroll', navState, { passive: true }); navState();
if (OV.reduce) return;
// intro headline rises in on load
var title = document.querySelector('.oh-title');
(document.fonts ? document.fonts.ready : Promise.resolve()).then(function(){
var lines = OV.lines(title);
gsap.from(lines, { yPercent: 110, duration: 1.1, ease: 'power4.out', stagger: .1, delay: .15 });
gsap.from('.oh-side > *', { y: 24, autoAlpha: 0, duration: .9, ease: 'power3.out', stagger: .1, delay: .55 });
});
var dive = document.querySelector('.od');
var svg = dive.querySelector('.od-svg');
var field = svg.querySelector('.od-field');
var ring = svg.querySelector('.od-ring');
var colorLayer = dive.querySelector('.od-color');
var marks = dive.querySelector('.od-marks');
var photo = dive.querySelector('.od-photo img');
var copyLines = dive.querySelectorAll('.od-copy .ov-line-in');
var copyCard = dive.querySelector('.od-copy');
// Numeral geometry, measured from the stage.
var g = { W: 0, H: 0, cx: 0, cy: 0, orx: 0, ory: 0, irx: 0, iry: 0, sEnd: 1 };
var state = { z: 0 };
function ellipse(cx, cy, rx, ry) {
return 'M' + (cx - rx) + ' ' + cy +
'a' + rx + ' ' + ry + ' 0 1 0 ' + (2 * rx) + ' 0' +
'a' + rx + ' ' + ry + ' 0 1 0 ' + (-2 * rx) + ' 0Z';
}
function measure() {
var W = dive.clientWidth, H = dive.clientHeight;
g.W = W; g.H = H; g.cx = W / 2; g.cy = H / 2;
// A heavy oval zero: tall, with thick sides and a narrow counter.
var ory = Math.min(H * 0.43, (W * 0.44) / 0.74);
g.ory = ory; g.orx = ory * 0.74;
g.irx = g.orx * 0.44; g.iry = ory * 0.7;
// Scale at which the counter swallows the stage corners, plus margin.
g.sEnd = Math.sqrt(Math.pow(g.cx / g.irx, 2) + Math.pow(g.cy / g.iry, 2)) * 1.15;
svg.setAttribute('viewBox', '0 0 ' + W + ' ' + H);
}
// Redraw the vector at scale s; geometry is rebuilt each frame so edges stay crisp.
function draw() {
var s = Math.exp(state.z * Math.log(g.sEnd));
var cx = g.cx, cy = g.cy, W = g.W, H = g.H;
var pad = Math.max(W, H) * s;
field.setAttribute('d',
'M' + (cx - pad) + ' ' + (cy - pad) + 'H' + (cx + pad) + 'V' + (cy + pad) + 'H' + (cx - pad) + 'Z' +
ellipse(cx, cy, g.orx * s, g.ory * s));
ring.setAttribute('d',
ellipse(cx, cy, g.orx * s + 1, g.ory * s + 1) + ellipse(cx, cy, g.irx * s, g.iry * s));
gsap.set(marks, { scale: s, autoAlpha: Math.max(0, 1 - state.z * 5) });
}
measure();
gsap.set(colorLayer, { autoAlpha: 1 });
gsap.set(photo, { scale: 1.4 });
gsap.set(copyCard, { autoAlpha: 0, y: 40 });
gsap.set(copyLines, { yPercent: 115 });
ScrollTrigger.addEventListener('refreshInit', function(){ measure(); draw(); });
var tl = gsap.timeline({
defaults: { ease: 'none' },
scrollTrigger: {
trigger: dive, start: 'top top', end: '+=340%',
pin: true, scrub: true, anticipatePin: 1, invalidateOnRefresh: true
}
});
tl.to(state, { z: 1, duration: 6, onUpdate: draw }, 0) // dive through the counter
.to(photo, { scale: 1.15, duration: 6 }, 0)
.set(colorLayer, { autoAlpha: 0 }, 6)
.to(photo, { scale: 1, duration: 3 }, 6) // photo settles
.to(copyCard, { autoAlpha: 1, y: 0, duration: 1.2 }, 6.4) // copy rises in
.to(copyLines, { yPercent: 0, duration: 1.1, stagger: 0.28 }, 6.8)
.to({}, { duration: 1.2 }); // hold
draw();
})();
</script>
```
## `sections/02-benefits.html`
```html
<!-- ============ 02 BENEFITS: why zero — sticky stat + four benefit rows ============ -->
<style>
.ob{
position:relative;background:var(--soft);border-radius:40px 40px 0 0;
padding:var(--section-y) var(--gutter);
}
.ob-grid{
display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.15fr);
gap:clamp(32px,6vw,110px);max-width:1320px;margin:0 auto;align-items:start;
}
.ob-side{position:sticky;top:clamp(96px,14vh,130px)}
.ob-lede{margin:22px 0 0;max-width:400px}
/* the big rolling stat */
.ob-stat{margin-top:clamp(28px,5vh,52px);padding-top:22px;border-top:1.5px solid var(--forest-24)}
.ob-stat-win{
position:relative;overflow:hidden;
height:clamp(78px,9.5vw,138px);
}
.ob-stat-val{
position:absolute;left:0;top:0;white-space:nowrap;
font-family:var(--display);font-weight:800;color:var(--forest);
font-size:clamp(68px,8.4vw,122px);line-height:1.1;letter-spacing:-.05em;
will-change:transform;
}
.ob-stat-val small{font-size:.36em;letter-spacing:-.02em;color:var(--tang);margin-left:.14em;font-weight:700}
.ob-stat-label{margin:8px 0 0;font-size:13px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--ink)}
.ob-stat-dots{display:flex;gap:6px;margin-top:18px}
.ob-stat-dots i{width:22px;height:4px;border-radius:4px;background:var(--forest-24);transition:background .35s,width .35s}
.ob-stat-dots i.is-on{background:var(--tang);width:40px}
/* rows */
.ob-list{list-style:none;margin:0;padding:0}
.ob-row{position:relative;padding:clamp(28px,5vh,48px) 0 clamp(36px,7vh,72px)}
.ob-rule{position:absolute;left:0;right:0;top:0;height:1.5px;background:var(--forest);transform-origin:0 50%}
.ob-row-head{display:flex;align-items:baseline;gap:clamp(14px,2vw,26px)}
.ob-idx{
flex:none;font-family:var(--display);font-weight:800;color:var(--tang);
font-size:clamp(40px,5vw,72px);line-height:1;letter-spacing:-.05em;
}
.ob-row h3{
margin:0;font-family:var(--display);font-weight:700;color:var(--forest);
font-size:clamp(22px,2.3vw,32px);line-height:1.08;letter-spacing:-.03em;
}
.ob-row p{margin:18px 0 0;max-width:480px;padding-left:calc(clamp(40px,5vw,72px)*1.25 + clamp(14px,2vw,26px))}
.ob-rstat{
display:flex;align-items:baseline;gap:10px;margin-top:20px;
padding-left:calc(clamp(40px,5vw,72px)*1.25 + clamp(14px,2vw,26px));
}
.ob-rstat b{font-family:var(--display);font-weight:800;color:var(--forest);font-size:clamp(34px,4vw,48px);line-height:1;letter-spacing:-.04em}
.ob-rstat b small{font-size:.45em;color:var(--tang);margin-left:.12em}
.ob-rstat span{font-size:13px;font-weight:600;letter-spacing:.08em;text-transform:uppercase}
/* motion on, desktop: stat lives in the sticky column, inactive rows fade */
html.ov-motion .ob-row{transition:opacity .5s ease}
@media (min-width:861px){
html.ov-motion .ob-row{opacity:.35}
html.ov-motion .ob-row.is-active{opacity:1}
html.ov-motion .ob-rstat{display:none}
}
/* no motion: the sticky stat is dropped, each row carries its own */
html:not(.ov-motion) .ob-stat{display:none}
@media (max-width:860px){
.ob{border-radius:28px 28px 0 0}
.ob-grid{grid-template-columns:minmax(0,1fr);gap:28px}
.ob-side{position:static}
.ob-stat{display:none}
.ob-row p,.ob-rstat{padding-left:0}
.ob-row{padding:26px 0 34px}
}
</style>
<section id="benefits" class="ob" aria-labelledby="ob-title">
<div class="ob-grid">
<div class="ob-side">
<p class="ov-eyebrow">Why zero</p>
<h2 class="ov-h2 ob-title" id="ob-title"><span class="ov-line"><span class="ov-line-in">Keep the toast.</span></span><span class="ov-line"><span class="ov-line-in"><em>Lose</em> the fog.</span></span></h2>
<p class="ob-lede">Oval pours like a proper nightcap: bitter, aromatic, built to be sipped slowly. It just leaves tomorrow exactly as you planned it.</p>
<div class="ob-stat" aria-hidden="true">
<div class="ob-stat-win"><span class="ob-stat-val">0.0<small>%</small></span></div>
<p class="ob-stat-label">alcohol by volume</p>
<div class="ob-stat-dots"><i class="is-on"></i><i></i><i></i><i></i></div>
</div>
</div>
<ol class="ob-list">
<li class="ob-row is-active" data-num="0.0" data-dec="1" data-unit="%" data-label="alcohol by volume">
<span class="ob-rule" aria-hidden="true"></span>
<div class="ob-row-head"><span class="ob-idx">01</span><h3>Wake up on the right side of Sunday</h3></div>
<p>No haze, no half-day lost to the sofa. A second glass costs you nothing but another ice cube, so the early run, the market or the long lie-in stays yours.</p>
<div class="ob-rstat"><b>0.0<small>%</small></b><span>alcohol by volume</span></div>
</li>
<li class="ob-row" data-num="3" data-dec="0" data-unit="" data-label="botanicals, distilled one by one">
<span class="ob-rule" aria-hidden="true"></span>
<div class="ob-row-head"><span class="ob-idx">02</span><h3>All of the ritual, none of the regret</h3></div>
<p>The heavy glass, the long pour, the twist of peel. Rosemary, pink grapefruit and wild juniper give it the bite and the slow finish you reach for at the end of a day.</p>
<div class="ob-rstat"><b>3</b><span>botanicals, distilled one by one</span></div>
</li>
<li class="ob-row" data-num="30" data-dec="0" data-unit="kcal" data-label="in a 50 ml serve">
<span class="ob-rule" aria-hidden="true"></span>
<div class="ob-row-head"><span class="ob-idx">03</span><h3>Light enough for a Tuesday</h3></div>
<p>Thirty calories a serve and no added sugar. Top it with soda and a wedge and you have a weeknight drink that doesn't need a weekend to recover from.</p>
<div class="ob-rstat"><b>30<small>kcal</small></b><span>in a 50 ml serve</span></div>
</li>
<li class="ob-row" data-num="1" data-dec="0" data-unit="farm" data-label="where it's grown and stilled">
<span class="ob-rule" aria-hidden="true"></span>
<div class="ob-row-head"><span class="ob-idx">04</span><h3>Grown and stilled in one place</h3></div>
<p>Every leaf and peel is picked a short walk from the copper still, then distilled the same week. You can taste the hillside, not the supply chain.</p>
<div class="ob-rstat"><b>1<small>farm</small></b><span>where it's grown and stilled</span></div>
</li>
</ol>
</div>
</section>
<script>
(function () {
if (OV.reduce) return;
var sec = document.getElementById('benefits');
var rows = [].slice.call(sec.querySelectorAll('.ob-row'));
var win = sec.querySelector('.ob-stat-win');
var label = sec.querySelector('.ob-stat-label');
var dots = [].slice.call(sec.querySelectorAll('.ob-stat-dots i'));
var title = sec.querySelector('.ob-title');
var current = 0;
function fmt(n, dec, unit){
return n.toFixed(dec) + (unit ? '<small>' + unit + '</small>' : '');
}
// Swap the big stat: old value slides up and out, new one slides up in and counts to its target.
function show(i){
if (i === current) return;
var dir = i > current ? 1 : -1;
current = i;
rows.forEach(function(r, k){ r.classList.toggle('is-active', k === i); });
dots.forEach(function(d, k){ d.classList.toggle('is-on', k === i); });
var d = rows[i].dataset, target = parseFloat(d.num), dec = +d.dec;
var old = win.querySelectorAll('.ob-stat-val');
old.forEach(function(o){
gsap.killTweensOf(o);
gsap.to(o, { yPercent: -110 * dir, duration: .55, ease: 'power3.in', onComplete: function(){ o.remove(); } });
});
var el = document.createElement('span');
el.className = 'ob-stat-val';
el.innerHTML = fmt(0, dec, d.unit);
win.appendChild(el);
var c = { v: 0 };
gsap.fromTo(el, { yPercent: 110 * dir }, { yPercent: 0, duration: .75, ease: 'power3.out', delay: .12 });
gsap.to(c, { v: target, duration: .9, ease: 'power2.out', delay: .12,
onUpdate: function(){ el.innerHTML = fmt(c.v, dec, d.unit); } });
gsap.fromTo(label, { autoAlpha: 0, y: 8 }, { autoAlpha: 1, y: 0, duration: .5, delay: .3, overwrite: true,
onStart: function(){ label.textContent = d.label; } });
}
rows.forEach(function(row, i){
ScrollTrigger.create({
trigger: row, start: 'top 55%', end: 'bottom 55%',
onToggle: function(self){ if (self.isActive) show(i); }
});
// hairline draws in as the row arrives
gsap.fromTo(row.querySelector('.ob-rule'), { scaleX: 0 }, {
scaleX: 1, duration: 1.2, ease: 'power3.inOut',
scrollTrigger: { trigger: row, start: 'top 85%', toggleActions: 'play none none reverse' }
});
gsap.from(row.querySelectorAll('.ob-row-head, p, .ob-rstat'), {
y: 26, autoAlpha: 0, duration: .9, ease: 'power3.out', stagger: .08,
scrollTrigger: { trigger: row, start: 'top 80%', toggleActions: 'play none none reverse' }
});
});
// heading + lede rise in once
(document.fonts ? document.fonts.ready : Promise.resolve()).then(function(){
var lines = title.querySelectorAll('.ov-line-in');
var st = { trigger: sec, start: 'top 75%', toggleActions: 'play none none reverse' };
gsap.from(lines, { yPercent: 110, duration: 1, ease: 'power4.out', stagger: .09, scrollTrigger: st });
gsap.from(sec.querySelectorAll('.ob-side .ov-eyebrow, .ob-lede, .ob-stat'), {
y: 24, autoAlpha: 0, duration: .9, ease: 'power3.out', stagger: .1, delay: .15,
scrollTrigger: { trigger: sec, start: 'top 75%', toggleActions: 'play none none reverse' }
});
ScrollTrigger.refresh();
});
})();
</script>
```
## `sections/03-range.html`
```html
<!-- ============ 03 RANGE: three bottles + discovery set ============ -->
<style>
.or{background:var(--soft);padding:var(--section-y) var(--gutter);position:relative}
.or-head{display:grid;grid-template-columns:1.3fr 1fr;gap:32px;align-items:end;margin-bottom:clamp(36px,6vh,64px)}
.or-head p.or-lede{margin:0;max-width:420px;justify-self:end}
.or-track{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(3,1fr);gap:clamp(16px,2vw,28px)}
.or-item{display:flex}
.or-card{
position:relative;display:flex;flex-direction:column;width:100%;overflow:hidden;
background:var(--white);border-radius:var(--radius);padding:12px;
box-shadow:0 1px 0 var(--forest-12);transition:transform .45s cubic-bezier(.2,.8,.2,1),box-shadow .45s;
}
html.ov-motion .or-card:hover,html.ov-motion .or-card:focus-within{transform:translateY(-10px);box-shadow:var(--shadow)}
/* photo frame: botanical by default, the serve on hover */
.or-frame{position:relative;border-radius:16px;overflow:hidden;aspect-ratio:1/1.02;background:var(--forest-12)}
.or-media,.or-zoom{position:absolute;inset:0}
.or-zoom img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;transition:opacity .6s ease,transform 1.1s cubic-bezier(.2,.8,.2,1)}
.or-zoom .or-img-serve{opacity:0}
.or-card:hover .or-img-serve,.or-card:focus-within .or-img-serve{opacity:1}
.or-card:hover .or-img-bot,.or-card:focus-within .or-img-bot{opacity:0}
html.ov-motion .or-zoom .or-img-serve{transform:scale(1.12)}
html.ov-motion .or-card:hover .or-img-serve,html.ov-motion .or-card:focus-within .or-img-serve{transform:scale(1)}
html.ov-motion .or-card:hover .or-img-bot,html.ov-motion .or-card:focus-within .or-img-bot{transform:scale(1.08)}
.or-flag{
position:absolute;left:12px;top:12px;z-index:2;display:inline-flex;align-items:center;gap:8px;
background:var(--soft-94);color:var(--forest);font-size:12px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;
padding:6px 11px;border-radius:999px;
}
.or-dot{width:9px;height:9px;border-radius:50%;background:var(--forest)}
.or-dot--tang{background:var(--tang)}
.or-dot--lime{background:var(--lime);box-shadow:inset 0 0 0 1.5px var(--forest)}
.or-flag-alt{display:none}
.or-card:hover .or-flag-def,.or-card:focus-within .or-flag-def{display:none}
.or-card:hover .or-flag-alt,.or-card:focus-within .or-flag-alt{display:inline}
/* text body with the outlined zero behind it */
.or-body{position:relative;flex:1;display:flex;flex-direction:column;padding:22px 10px 8px}
.or-zero{
position:absolute;right:-14px;bottom:-40px;z-index:0;pointer-events:none;user-select:none;
font-family:var(--display);font-weight:800;font-size:clamp(200px,20vw,300px);line-height:.8;
color:transparent;-webkit-text-stroke:2px var(--forest-12);
}
.or-body > *:not(.or-zero){position:relative;z-index:1}
.or-meta{display:flex;justify-content:space-between;gap:12px;font-size:13px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--forest);opacity:.75}
.or-name{margin:10px 0 8px;font-family:var(--display);font-weight:700;color:var(--forest);font-size:clamp(26px,2.4vw,34px);line-height:1;letter-spacing:-.03em}
.or-desc{margin:0 0 16px;font-size:16px}
.or-chips{list-style:none;margin:0 0 16px;padding:0;display:flex;flex-wrap:wrap;gap:6px}
.or-chips li{font-size:13px;font-weight:600;color:var(--forest);background:var(--soft);border:1.5px solid var(--forest-12);border-radius:999px;padding:5px 11px}
.or-best{margin:0 0 22px;font-size:15px;color:var(--ink)}
.or-best b{color:var(--tang);font-weight:600}
.or-buy{margin-top:auto;display:flex;align-items:center;justify-content:space-between;gap:12px;padding-top:16px;border-top:1.5px solid var(--forest-12)}
.or-price{font-family:var(--display);font-weight:700;font-size:26px;color:var(--forest);letter-spacing:-.02em}
.or-price small{display:block;font-family:var(--body);font-size:12px;font-weight:500;letter-spacing:.04em;color:var(--ink);opacity:.7}
/* discovery set */
.or-set{
position:relative;margin-top:clamp(28px,4vw,44px);display:grid;grid-template-columns:auto 1fr auto;gap:clamp(20px,3vw,44px);align-items:center;
background:var(--soft);border:1.5px solid var(--forest-24);border-radius:var(--radius);padding:clamp(22px,3vw,36px);color:var(--forest);
}
.or-trio{display:flex}
.or-trio img{width:clamp(64px,7vw,92px);height:clamp(64px,7vw,92px);border-radius:50%;object-fit:cover;border:4px solid var(--soft)}
.or-trio img + img{margin-left:-22px}
.or-set h3{margin:0 0 8px;font-family:var(--display);font-weight:700;font-size:clamp(22px,2.4vw,32px);line-height:1.05;letter-spacing:-.03em}
.or-set p{margin:0;color:var(--ink);max-width:520px}
.or-set .ov-eyebrow{margin-bottom:8px}
.or-set-buy{display:flex;align-items:center;gap:18px}
.or-badge{
width:92px;height:92px;border-radius:50%;background:var(--tang);color:var(--ink);flex:none;
display:grid;place-items:center;text-align:center;transform:rotate(-8deg);
font-family:var(--display);font-weight:800;font-size:26px;line-height:1;letter-spacing:-.03em;
}
.or-badge small{display:block;font-family:var(--body);font-weight:600;font-size:11px;letter-spacing:.08em;text-transform:uppercase;margin-top:3px}
/* toast */
.or-toast{
position:fixed;left:50%;bottom:22px;z-index:60;max-width:calc(100vw - 32px);
display:flex;align-items:center;gap:10px;background:var(--forest);color:var(--soft);
font-weight:600;font-size:15px;padding:12px 20px;border-radius:999px;box-shadow:var(--shadow);
transform:translate(-50%,0);opacity:0;visibility:hidden;pointer-events:none;
transition:opacity .35s ease,visibility 0s linear .35s;
}
.or-toast::before{content:"";width:9px;height:9px;border-radius:50%;background:var(--lime);flex:none}
.or-toast.is-on{opacity:1;visibility:visible;transition:opacity .35s ease}
html.ov-motion .or-toast{transform:translate(-50%,160%);transition:opacity .35s ease,transform .5s cubic-bezier(.2,.8,.2,1),visibility 0s linear .5s}
html.ov-motion .or-toast.is-on{transform:translate(-50%,0);transition:opacity .35s ease,transform .5s cubic-bezier(.2,1.3,.4,1)}
@media (max-width:1100px){
.or-set{grid-template-columns:1fr auto}
.or-trio{grid-column:1/-1}
}
@media (max-width:860px){
.or-head{grid-template-columns:1fr;gap:18px}
.or-head p.or-lede{justify-self:start}
.or-track{
display:flex;overflow-x:auto;overscroll-behavior-x:contain;scroll-snap-type:x mandatory;
margin:0 calc(-1 * var(--gutter));padding:14px var(--gutter) 20px;scroll-padding:0 var(--gutter);
scrollbar-width:none;
}
.or-track::-webkit-scrollbar{display:none}
.or-item{flex:0 0 min(82%,340px);scroll-snap-align:start}
.or-zero{font-size:220px}
.or-set{grid-template-columns:1fr}
.or-set-buy{justify-content:space-between}
}
</style>
<section id="range" class="or" aria-labelledby="or-title">
<div class="or-head">
<div>
<p class="ov-eyebrow">The range</p>
<h2 class="ov-h2 or-title" id="or-title">Three plants. Choose your <em>zero.</em></h2>
</div>
<p class="or-lede">Each bottle is distilled from a single botanical, so what you taste is the plant we picked. Pour it over ice, lengthen it with tonic, or build it into something slower.</p>
</div>
<ul class="or-track">
<li class="or-item">
<article class="or-card" data-name="N°01 Rosemary">
<div class="or-frame">
<span class="or-flag"><span class="or-dot"></span><span class="or-flag-def">Resin · green</span><span class="or-flag-alt">The serve</span></span>
<div class="or-media"><div class="or-zoom">
<img class="or-img-bot" src="assets/n1.jpg" alt="Upright stems of fresh rosemary growing in a garden bed" loading="lazy">
<img class="or-img-serve" src="assets/range-rosemary.jpg" alt="A clear long drink with ice and a rosemary sprig in a stemmed glass, outdoors" loading="lazy">
</div></div>
</div>
<div class="or-body">
<span class="or-zero" aria-hidden="true">0</span>
<div class="or-meta"><span>N°01</span><span>50cl · 0.0%</span></div>
<h3 class="or-name">Rosemary</h3>
<p class="or-desc">Resinous and green, like brushing past the hedge on the way home.</p>
<ul class="or-chips" aria-label="Tasting notes"><li>Pine resin</li><li>Bay leaf</li><li>Cracked pepper</li></ul>
<p class="or-best"><b>Best with</b> — light tonic, a bruised sprig and a long table.</p>
<div class="or-buy">
<span class="or-price">£28<small>50cl bottle</small></span>
<button class="ov-pill or-add" type="button">Add to basket</button>
</div>
</div>
</article>
</li>
<li class="or-item">
<article class="or-card" data-name="N°02 Grapefruit">
<div class="or-frame">
<span class="or-flag"><span class="or-dot or-dot--tang"></span><span class="or-flag-def">Zest · bright</span><span class="or-flag-alt">The serve</span></span>
<div class="or-media"><div class="or-zoom">
<img class="or-img-bot" src="assets/n2.jpg" alt="Halved pink grapefruit showing its ruby flesh" loading="lazy">
<img class="or-img-serve" src="assets/range-grapefruit.jpg" alt="A short glass over ice with a wedge of citrus on the rim, in bright sunlight" loading="lazy">
</div></div>
</div>
<div class="or-body">
<span class="or-zero" aria-hidden="true">0</span>
<div class="or-meta"><span>N°02</span><span>50cl · 0.0%</span></div>
<h3 class="or-name">Grapefruit</h3>
<p class="or-desc">Pink zest up front, a clean bitter snap of pith at the finish.</p>
<ul class="or-chips" aria-label="Tasting notes"><li>Pink zest</li><li>Bitter pith</li><li>Sea salt</li></ul>
<p class="or-best"><b>Best with</b> — soda, a salted rim and a brunch that runs late.</p>
<div class="or-buy">
<span class="or-price">£28<small>50cl bottle</small></span>
<button class="ov-pill or-add" type="button">Add to basket</button>
</div>
</div>
</article>
</li>
<li class="or-item">
<article class="or-card" data-name="N°03 Juniper">
<div class="or-frame">
<span class="or-flag"><span class="or-dot or-dot--lime"></span><span class="or-flag-def">Spice · dry</span><span class="or-flag-alt">The serve</span></span>
<div class="or-media"><div class="or-zoom">
<img class="or-img-bot" src="assets/n3.jpg" alt="A juniper branch heavy with dusty blue berries" loading="lazy">
<img class="or-img-serve" src="assets/range-juniper.jpg" alt="A clear glass bottle with a blank label beside an empty tumbler" loading="lazy">
</div></div>
</div>
<div class="or-body">
<span class="or-zero" aria-hidden="true">0</span>
<div class="or-meta"><span>N°03</span><span>50cl · 0.0%</span></div>
<h3 class="or-name">Juniper</h3>
<p class="or-desc">Dry and peppery, with cold forest air on the back of the throat.</p>
<ul class="or-chips" aria-label="Tasting notes"><li>Wild juniper</li><li>Cardamom</li><li>Cedar</li></ul>
<p class="or-best"><b>Best with</b> — a bone-dry tonic and a wide twist of lemon peel.</p>
<div class="or-buy">
<span class="or-price">£30<small>50cl bottle</small></span>
<button class="ov-pill or-add" type="button">Add to basket</button>
</div>
</div>
</article>
</li>
</ul>
<div class="or-set" data-name="Discovery set">
<div class="or-trio" aria-hidden="true">
<img src="assets/n1.jpg" alt="" loading="lazy">
<img src="assets/n2.jpg" alt="" loading="lazy">
<img src="assets/n3.jpg" alt="" loading="lazy">
</div>
<div>
<p class="ov-eyebrow">Discovery set</p>
<h3>Can't pick one? Take all three.</h3>
<p>Rosemary, Grapefruit and Juniper in 20cl bottles — enough for a dozen long drinks and one firm favourite.</p>
</div>
<div class="or-set-buy">
<span class="or-badge">£36<small>3 × 20cl</small></span>
<button class="ov-pill or-add" type="button">Add the set</button>
</div>
</div>
<div class="or-toast" role="status" aria-live="polite"></div>
</section>
<script>
(function () {
var sec = document.getElementById('range');
// pure front-end "basket": a toast that rises, holds 2s, then fades
var toast = sec.querySelector('.or-toast'), timer;
sec.querySelectorAll('.or-add').forEach(function (btn) {
btn.addEventListener('click', function () {
var host = btn.closest('[data-name]');
toast.textContent = 'Added — ' + host.getAttribute('data-name');
toast.classList.remove('is-on');
void toast.offsetWidth;
toast.classList.add('is-on');
clearTimeout(timer);
timer = setTimeout(function () { toast.classList.remove('is-on'); }, 2000);
});
});
if (OV.reduce) return;
// heading lines
var title = sec.querySelector('.or-title');
(document.fonts ? document.fonts.ready : Promise.resolve()).then(function () {
var lines = OV.lines(title);
gsap.from(lines, { yPercent: 110, duration: 1, ease: 'power4.out', stagger: .1,
scrollTrigger: { trigger: sec, start: 'top 80%', once: true } });
});
gsap.from(['.or-head .ov-eyebrow', '.or-lede'], { y: 24, autoAlpha: 0, duration: .9, ease: 'power3.out', stagger: .1,
scrollTrigger: { trigger: sec, start: 'top 80%', once: true } });
// cards rise with a slight turn; photos unmask from the bottom
var items = sec.querySelectorAll('.or-item');
var medias = sec.querySelectorAll('.or-media');
var zooms = sec.querySelectorAll('.or-zoom');
gsap.set(items, { y: 110, rotation: -3, autoAlpha: 0, transformOrigin: '0% 100%' });
gsap.set(medias, { clipPath: 'inset(100% 0% 0% 0%)' });
gsap.set(zooms, { scale: 1.25 });
gsap.timeline({ scrollTrigger: { trigger: sec.querySelector('.or-track'), start: 'top 80%', once: true } })
.to(items, { y: 0, rotation: 0, autoAlpha: 1, duration: 1.1, ease: 'power3.out', stagger: .14 }, 0)
.to(medias, { clipPath: 'inset(0% 0% 0% 0%)', duration: 1.2, ease: 'power3.inOut', stagger: .14 }, .2)
.to(zooms, { scale: 1, duration: 1.6, ease: 'power3.out', stagger: .14 }, .2);
// outlined zero drifts against the scroll
sec.querySelectorAll('.or-zero').forEach(function (z) {
gsap.fromTo(z, { yPercent: 16 }, { yPercent: -16, ease: 'none',
scrollTrigger: { trigger: z.closest('.or-card'), start: 'top bottom', end: 'bottom top', scrub: true } });
});
// discovery set + price badge pop
var set = sec.querySelector('.or-set');
gsap.timeline({ scrollTrigger: { trigger: set, start: 'top 85%', once: true } })
.from(set, { y: 40, autoAlpha: 0, duration: .9, ease: 'power3.out' })
.from(set.querySelectorAll('.or-trio img'), { scale: .6, autoAlpha: 0, duration: .6, ease: 'back.out(2)', stagger: .08 }, .2)
.fromTo(set.querySelector('.or-badge'), { scale: 0, rotation: -40 }, { scale: 1, rotation: -8, duration: .8, ease: 'back.out(2.4)' }, .45);
})();
</script>
```
## `sections/04-ritual.html`
```html
<!-- ============ 04 RITUAL: the Oval Hour, three signature serves (pinned, stepped) ============ -->
<style>
.ot{position:relative;background:var(--lime);padding:var(--section-y) var(--gutter)}
.ot-head{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(0,1fr);gap:24px 40px;align-items:end;margin-bottom:clamp(32px,5vw,56px)}
.ot-head .ov-h2{max-width:14ch}
.ot-intro{margin:0;max-width:340px;justify-self:end;color:var(--ink)}
/* stacked (default, mobile, reduced motion) */
.ot-frame,.ot-rail{display:none}
.ot-panel{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:20px}
.ot-serve{background:var(--white);border-radius:28px;overflow:hidden;box-shadow:var(--shadow);display:flex;flex-direction:column}
.ot-serve-photo{aspect-ratio:4/5;overflow:hidden}
.ot-serve-photo img{width:100%;height:100%;object-fit:cover}
.ot-body{padding:clamp(22px,2.6vw,34px);display:flex;flex-direction:column;gap:18px;flex:1}
.ot-count{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:13px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--forest)}
.ot-count b{color:var(--tang);font-weight:600}
.ot-name{margin:0;font-family:var(--display);font-weight:700;color:var(--forest);font-size:clamp(26px,2.6vw,40px);line-height:1.02;letter-spacing:-.03em}
.ot-steps{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:10px}
.ot-steps li{display:grid;grid-template-columns:34px minmax(0,1fr);gap:10px;align-items:start;font-size:16px;line-height:1.45}
.ot-num{font-family:var(--display);font-weight:700;font-size:13px;color:var(--forest);background:var(--lime);border-radius:999px;height:26px;display:grid;place-items:center;margin-top:-1px}
.ot-meta{display:flex;flex-wrap:wrap;gap:8px;margin:auto 0 0;padding:16px 0 0;border-top:1.5px solid var(--forest-12)}
.ot-meta .ov-line{width:100%}
.ot-meta .ov-line-in{display:flex;flex-wrap:wrap;gap:8px}
.ot-meta .ov-line-in > span{font-size:14px;font-weight:600;color:var(--forest);background:var(--soft);border-radius:999px;padding:7px 13px}
.ot-meta .ov-line-in > span b{color:var(--tang);font-weight:600}
/* pinned stepped layout: desktop + motion only */
@media (min-width:861px){
html.ov-motion .ot{height:100vh;height:100svh;display:flex;flex-direction:column;padding-top:clamp(84px,12vh,120px);padding-bottom:clamp(24px,5vh,56px);overflow:hidden}
html.ov-motion .ot-head{margin-bottom:clamp(18px,3.5vh,40px);flex:none}
html.ov-motion .ot-head .ov-h2{font-size:clamp(28px,min(3.8vw,5.4vh),56px);max-width:none}
html.ov-motion .ot-stage{flex:1;min-height:0;display:grid;grid-template-columns:auto 64px minmax(0,560px);grid-template-rows:100%;gap:clamp(20px,3vw,48px);justify-content:center}
html.ov-motion .ot-frame{display:block;position:relative;height:100%;aspect-ratio:4/5;border-radius:28px;overflow:hidden;background:var(--soft);box-shadow:var(--shadow)}
html.ov-motion .ot-frame img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;will-change:clip-path,transform}
html.ov-motion .ot-frame img + img{clip-path:inset(100% 0 0 0)}
html.ov-motion .ot-rail{display:flex;position:relative;flex-direction:column;justify-content:space-between;align-items:center;padding:6px 0}
html.ov-motion .ot-rail-track,html.ov-motion .ot-rail-fill{position:absolute;left:50%;top:24px;bottom:24px;width:2px;margin-left:-1px;border-radius:2px}
html.ov-motion .ot-rail-track{background:var(--forest-24)}
html.ov-motion .ot-rail-fill{background:var(--forest);transform-origin:50% 0;transform:scaleY(0)}
html.ov-motion .ot-panel{display:grid;grid-template-columns:minmax(0,1fr);align-self:center;background:var(--white);border-radius:28px;box-shadow:var(--shadow);max-height:100%}
html.ov-motion .ot-serve{grid-area:1/1;background:none;box-shadow:none;border-radius:0}
html.ov-motion .ot-serve-photo{display:none}
html.ov-motion .ot-body{padding:clamp(24px,3.6vh,44px) clamp(24px,2.8vw,44px);gap:clamp(12px,2.2vh,22px)}
html.ov-motion .ot-name{font-size:clamp(28px,min(3.2vw,5.4vh),48px)}
html.ov-motion .ot-steps{gap:clamp(6px,1.3vh,12px)}
html.ov-motion .ot-steps li{font-size:clamp(15px,2vh,17px)}
html.ov-motion .ot-serve:not(:first-child){pointer-events:none}
}
.ot-dot{
position:relative;z-index:1;width:46px;height:46px;border-radius:50%;border:0;cursor:pointer;padding:0;
background:var(--soft);color:var(--forest);font-family:var(--display);font-weight:700;font-size:13px;
box-shadow:inset 0 0 0 1.5px var(--forest-24);transition:background .3s,color .3s,box-shadow .3s,transform .3s;
}
.ot-dot:hover{transform:scale(1.06)}
.ot-dot.is-on{background:var(--forest);color:var(--lime);box-shadow:none}
.ot-dot.is-now{background:var(--tang);color:var(--ink)}
.ot-dot:focus-visible{outline:2px solid var(--forest);outline-offset:3px}
@media (max-width:1100px){ .ot-panel{grid-template-columns:repeat(2,minmax(0,1fr))} }
@media (max-width:860px){
.ot-head{grid-template-columns:1fr}
.ot-intro{justify-self:start}
.ot-panel{grid-template-columns:1fr;gap:18px}
.ot-serve-photo{aspect-ratio:4/4.4}
}
@media (max-width:420px){
.ot-steps li{font-size:15.5px;grid-template-columns:30px minmax(0,1fr)}
}
</style>
<section id="ritual" class="ot" aria-labelledby="ot-title">
<div class="ot-head">
<div>
<p class="ov-eyebrow">The Oval Hour</p>
<h2 class="ov-h2" id="ot-title">Three glasses, one hour, <em>nothing</em> to sleep off.</h2>
</div>
<p class="ot-intro">Our signature serves need a glass, some ice and about two minutes. No shaker skills, no bar cart, no morning after.</p>
</div>
<div class="ot-stage">
<div class="ot-frame" aria-hidden="true">
<img src="assets/ritual-1.jpg" alt="">
<img src="assets/ritual-2.jpg" alt="">
<img src="assets/ritual-3.jpg" alt="">
</div>
<div class="ot-rail" role="group" aria-label="Choose a serve">
<span class="ot-rail-track"></span><span class="ot-rail-fill"></span>
<button class="ot-dot is-on is-now" type="button" data-step="0" aria-label="Serve 01, Garden Tonic">01</button>
<button class="ot-dot" type="button" data-step="1" aria-label="Serve 02, Pink Hour Spritz">02</button>
<button class="ot-dot" type="button" data-step="2" aria-label="Serve 03, Juniper Sour">03</button>
</div>
<div class="ot-panel">
<article class="ot-serve">
<div class="ot-serve-photo"><img src="assets/ritual-1.jpg" alt="A tall copa glass of sparkling tonic with lemon wheels and a rosemary sprig on a wooden bar" loading="lazy"></div>
<div class="ot-body">
<div class="ot-count ot-t"><span class="ov-line"><span class="ov-line-in">Serve <b>01</b> / 03 · Oval Rosemary</span></span></div>
<h3 class="ot-name ot-t"><span class="ov-line"><span class="ov-line-in">Garden Tonic</span></span></h3>
<ol class="ot-steps">
<li><span class="ot-num">1</span><span class="ov-line ot-t"><span class="ov-line-in">Pack a copa glass to the brim with large, clear ice.</span></span></li>
<li><span class="ot-num">2</span><span class="ov-line ot-t"><span class="ov-line-in">Pour in 50 ml of Oval Rosemary.</span></span></li>
<li><span class="ot-num">3</span><span class="ov-line ot-t"><span class="ov-line-in">Top with 120 ml of cold tonic, poured slowly down a bar spoon.</span></span></li>
<li><span class="ot-num">4</span><span class="ov-line ot-t"><span class="ov-line-in">Twist a strip of lemon peel over the glass, then drop it in.</span></span></li>
</ol>
<p class="ot-meta ot-t"><span class="ov-line"><span class="ov-line-in"><span>Glass: <b>Copa</b></span> <span>Time: <b>2 min</b></span> <span>0.0% ABV</span></span></span></p>
</div>
</article>
<article class="ot-serve">
<div class="ot-serve-photo"><img src="assets/ritual-2.jpg" alt="A pale orange spritz in a stemmed glass with a straw, on a sunny patio table beside lavender" loading="lazy"></div>
<div class="ot-body">
<div class="ot-count ot-t"><span class="ov-line"><span class="ov-line-in">Serve <b>02</b> / 03 · Oval Grapefruit</span></span></div>
<h3 class="ot-name ot-t"><span class="ov-line"><span class="ov-line-in">Pink Hour Spritz</span></span></h3>
<ol class="ot-steps">
<li><span class="ot-num">1</span><span class="ov-line ot-t"><span class="ov-line-in">Fill a large wine glass two-thirds with ice.</span></span></li>
<li><span class="ot-num">2</span><span class="ov-line ot-t"><span class="ov-line-in">Add 50 ml of Oval Grapefruit and a squeeze of fresh pink grapefruit.</span></span></li>
<li><span class="ot-num">3</span><span class="ov-line ot-t"><span class="ov-line-in">Lengthen with 100 ml of soda water and stir once.</span></span></li>
<li><span class="ot-num">4</span><span class="ov-line ot-t"><span class="ov-line-in">Clap a sprig of thyme between your palms and tuck it in.</span></span></li>
</ol>
<p class="ot-meta ot-t"><span class="ov-line"><span class="ov-line-in"><span>Glass: <b>Wine glass</b></span> <span>Time: <b>2 min</b></span> <span>0.0% ABV</span></span></span></p>
</div>
</article>
<article class="ot-serve">
<div class="ot-serve-photo"><img src="assets/ritual-3.jpg" alt="A frothy amber sour in a coupe with an orange wheel, standing on a pale stone table" loading="lazy"></div>
<div class="ot-body">
<div class="ot-count ot-t"><span class="ov-line"><span class="ov-line-in">Serve <b>03</b> / 03 · Oval Juniper</span></span></div>
<h3 class="ot-name ot-t"><span class="ov-line"><span class="ov-line-in">Juniper Sour</span></span></h3>
<ol class="ot-steps">
<li><span class="ot-num">1</span><span class="ov-line ot-t"><span class="ov-line-in">Add 50 ml Oval Juniper, 25 ml lemon juice and 15 ml syrup to a shaker.</span></span></li>
<li><span class="ot-num">2</span><span class="ov-line ot-t"><span class="ov-line-in">Add 25 ml of aquafaba and shake hard, without ice, for 15 seconds.</span></span></li>
<li><span class="ot-num">3</span><span class="ov-line ot-t"><span class="ov-line-in">Add ice and shake again until the tin feels frosty.</span></span></li>
<li><span class="ot-num">4</span><span class="ov-line ot-t"><span class="ov-line-in">Double-strain into a chilled coupe and let the foam settle.</span></span></li>
</ol>
<p class="ot-meta ot-t"><span class="ov-line"><span class="ov-line-in"><span>Glass: <b>Coupe</b></span> <span>Time: <b>2 min</b></span> <span>0.0% ABV</span></span></span></p>
</div>
</article>
</div>
</div>
</section>
<script>
(function () {
if (OV.reduce) return;
var sec = document.getElementById('ritual');
var frameImgs = sec.querySelectorAll('.ot-frame img');
var serves = sec.querySelectorAll('.ot-serve');
var dots = sec.querySelectorAll('.ot-dot');
var fill = sec.querySelector('.ot-rail-fill');
var title = sec.querySelector('.ov-h2');
var mm = gsap.matchMedia();
// the heading rises in once, at every size
(document.fonts ? document.fonts.ready : Promise.resolve()).then(function(){
var lines = OV.lines(title);
gsap.from(lines, { yPercent: 110, duration: 1, ease: 'power4.out', stagger: .1,
scrollTrigger: { trigger: sec, start: 'top 75%', once: true } });
gsap.from(sec.querySelectorAll('.ot-head .ov-eyebrow, .ot-intro'), { y: 20, autoAlpha: 0, duration: .8, ease: 'power3.out', stagger: .12,
scrollTrigger: { trigger: sec, start: 'top 75%', once: true } });
});
// desktop: pinned, three stepped serves
mm.add('(min-width: 861px)', function(){
var groups = [].map.call(serves, function(s){ return s.querySelectorAll('.ot-t .ov-line-in, .ot-t.ov-line > .ov-line-in'); });
groups.forEach(function(g, i){ if (i) gsap.set(g, { yPercent: 115 }); });
// step badges sit outside the line masks, so they fade/pop instead of sliding
var nums = [].map.call(serves, function(s){ return s.querySelectorAll('.ot-num'); });
nums.forEach(function(n, i){ if (i) gsap.set(n, { autoAlpha: 0, scale: .4 }); });
gsap.set([].slice.call(frameImgs, 1), { clipPath: 'inset(100% 0% 0% 0%)', scale: 1.15 });
gsap.set(fill, { scaleY: 0 });
var current = 0;
function mark(i){
if (i === current) return;
current = i;
dots.forEach(function(d, k){ d.classList.toggle('is-on', k <= i); d.classList.toggle('is-now', k === i); });
serves.forEach(function(s, k){ s.style.pointerEvents = k === i ? 'auto' : 'none'; });
}
var tl = gsap.timeline({
defaults: { ease: 'none' },
scrollTrigger: {
trigger: sec, start: 'top top', end: '+=250%',
pin: true, scrub: .8, anticipatePin: 1, invalidateOnRefresh: true,
snap: { snapTo: 'labels', duration: .6, delay: .05, ease: 'power2.inOut' },
onUpdate: function(self){ mark(Math.round(self.progress * 2)); }
}
});
tl.addLabel('s0', 0);
[1, 2].forEach(function(i){
var at = i - 1 + .15;
tl.to(groups[i - 1], { yPercent: -115, duration: .35, stagger: .03, ease: 'power2.in' }, at)
.fromTo(frameImgs[i], { clipPath: 'inset(100% 0% 0% 0%)', scale: 1.15 },
{ clipPath: 'inset(0% 0% 0% 0%)', scale: 1, duration: .7, ease: 'power2.inOut' }, at)
.fromTo(frameImgs[i - 1], { scale: 1 }, { scale: 1.06, duration: .7 }, at)
.to(nums[i - 1], { autoAlpha: 0, scale: .4, duration: .25, stagger: .03, ease: 'power2.in' }, at)
.fromTo(nums[i], { autoAlpha: 0, scale: .4 }, { autoAlpha: 1, scale: 1, duration: .35, stagger: .04, ease: 'back.out(2)', immediateRender: false }, at + .35)
.fromTo(groups[i], { yPercent: 115 }, { yPercent: 0, duration: .4, stagger: .03, ease: 'power3.out', immediateRender: false }, at + .3)
.addLabel('s' + i, i);
});
tl.to(fill, { scaleY: 1, duration: 2 }, 0);
tl.set({}, {}, 2);
function go(e){
var st = tl.scrollTrigger, i = +e.currentTarget.getAttribute('data-step');
var y = st.start + (st.end - st.start) * (i / 2);
if (OV.lenis) OV.lenis.scrollTo(y, { duration: 1.1 }); else window.scrollTo(0, y);
}
dots.forEach(function(d){ d.addEventListener('click', go); });
return function(){
dots.forEach(function(d){ d.removeEventListener('click', go); });
serves.forEach(function(s){ s.style.pointerEvents = ''; });
};
});
// mobile: stacked cards, simple fade-ups
mm.add('(max-width: 860px)', function(){
serves.forEach(function(s){
gsap.from(s, { y: 48, autoAlpha: 0, duration: .9, ease: 'power3.out',
scrollTrigger: { trigger: s, start: 'top 88%', once: true } });
});
});
})();
</script>
```
## `sections/05-reviews.html`
```html
<!-- ============ 05 REVIEWS: press marquee + aggregate + customer cards ============ -->
<style>
.ov5{
position:relative;background:var(--white);color:var(--ink);
border-radius:40px 40px 0 0;padding:clamp(56px,9vh,110px) 0 var(--section-y);
overflow:hidden;
}
/* press marquee */
.ov5-marquee{overflow:hidden;width:100%;padding:6px 0 10px;border-bottom:1.5px solid var(--forest-12)}
.ov5-track{display:flex;width:max-content;will-change:transform}
.ov5-group{display:flex;align-items:center;flex:none}
.ov5-name{
font-family:var(--display);font-weight:700;color:var(--forest);white-space:nowrap;
font-size:clamp(40px,7vw,96px);line-height:1.1;letter-spacing:-.04em;padding:0 .32em;
}
.ov5-sep{color:var(--tang);font-size:clamp(28px,4.2vw,60px);line-height:1;flex:none}
/* pull quotes */
.ov5-quotes{
list-style:none;margin:0;padding:clamp(26px,4vh,40px) var(--gutter) 0;
display:grid;grid-template-columns:repeat(5,1fr);gap:clamp(16px,2.4vw,32px);
}
.ov5-quotes li{border-top:2px solid var(--tang);padding-top:14px}
.ov5-quotes q{display:block;font-size:15.5px;line-height:1.45;color:var(--ink);quotes:"\201C" "\201D"}
.ov5-quotes cite{display:block;margin-top:10px;font-style:normal;font-size:12px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--forest)}
/* heading + aggregate */
.ov5-head{
padding:clamp(64px,10vh,120px) var(--gutter) clamp(36px,5vh,56px);
display:grid;grid-template-columns:1.3fr 1fr;gap:32px;align-items:end;
}
.ov5-score{
justify-self:end;display:flex;align-items:center;gap:18px;
background:var(--soft);border-radius:var(--radius);padding:18px 24px;
}
.ov5-num{font-family:var(--display);font-weight:800;color:var(--forest);font-size:clamp(44px,5vw,68px);line-height:1;letter-spacing:-.04em}
.ov5-num small{font-size:.42em;color:var(--tang);letter-spacing:0;margin-left:4px}
.ov5-score p{margin:6px 0 0;font-size:15px;line-height:1.35}
.ov5-score b{color:var(--forest)}
/* stars */
.ov5-stars{position:relative;display:inline-block;height:18px;line-height:0}
.ov5-stars svg{display:block;height:18px;width:auto}
.ov5-stars-base{color:var(--forest-24)}
.ov5-stars-fill{position:absolute;inset:0 auto 0 0;width:100%;overflow:hidden;color:var(--tang)}
.ov5-stars-fill svg{width:calc(18px * 122 / 22);max-width:none}
.ov5-stars--lg,.ov5-stars--lg svg{height:22px}
.ov5-stars--lg .ov5-stars-fill svg{width:122px}
/* review cards */
.ov5-grid{
padding:0 var(--gutter);display:grid;grid-template-columns:repeat(3,1fr);
gap:clamp(16px,2vw,26px);align-items:start;
}
.ov5-col{display:flex;flex-direction:column;gap:clamp(16px,2vw,26px)}
.ov5-col:nth-child(2){margin-top:clamp(40px,6vw,90px)}
.ov5-col:nth-child(3){margin-top:clamp(16px,2.5vw,36px)}
.ov5-card{
background:var(--soft);border-radius:var(--radius);padding:clamp(22px,2.4vw,32px);
display:flex;flex-direction:column;gap:14px;
}
.ov5-card blockquote{margin:0;font-size:17px;line-height:1.5}
.ov5-card blockquote strong{font-family:var(--display);font-weight:700;color:var(--forest);font-size:19px;letter-spacing:-.02em;display:block;margin-bottom:8px;line-height:1.2}
.ov5-meta{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;padding-top:14px;border-top:1.5px solid var(--forest-12)}
.ov5-who{font-weight:600;color:var(--forest);font-size:15px}
.ov5-who span{display:block;font-weight:400;color:var(--ink);font-size:13.5px;opacity:.8}
.ov5-bottle{font-size:12px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--forest);background:var(--white);border-radius:999px;padding:6px 11px}
.ov5-bottle::before{content:"";display:inline-block;width:7px;height:9px;border-radius:50%;background:var(--tang);margin-right:7px;vertical-align:0}
.ov5-foot{padding:clamp(36px,5vh,56px) var(--gutter) 0;text-align:center}
/* reduced motion: static marquee that wraps */
html:not(.ov-motion) .ov5-track{width:auto;flex-wrap:wrap;justify-content:center}
html:not(.ov-motion) .ov5-group{flex-wrap:wrap;justify-content:center}
html:not(.ov-motion) .ov5-group[aria-hidden]{display:none}
html:not(.ov-motion) .ov5-name{white-space:normal;text-align:center}
@media (max-width:1100px){
.ov5-quotes{grid-template-columns:repeat(3,1fr)}
}
@media (max-width:860px){
.ov5{border-radius:28px 28px 0 0}
.ov5-name{font-size:clamp(34px,10vw,52px)}
.ov5-sep{font-size:clamp(24px,7vw,36px)}
.ov5-quotes{grid-template-columns:1fr 1fr}
.ov5-head{grid-template-columns:1fr}
.ov5-score{justify-self:start}
.ov5-grid{grid-template-columns:1fr}
.ov5-col:nth-child(2),.ov5-col:nth-child(3){margin-top:0}
}
@media (max-width:520px){
.ov5-quotes{grid-template-columns:1fr}
}
</style>
<section id="reviews" class="ov5" aria-labelledby="ov5-title">
<div class="ov5-marquee" aria-label="As featured in">
<div class="ov5-track">
<div class="ov5-group">
<span class="ov5-name">The Sober Table</span><span class="ov5-sep" aria-hidden="true">✺</span>
<span class="ov5-name">Field & Glass</span><span class="ov5-sep" aria-hidden="true">✺</span>
<span class="ov5-name">Weekend Pour</span><span class="ov5-sep" aria-hidden="true">✺</span>
<span class="ov5-name">Kitchen Notes</span><span class="ov5-sep" aria-hidden="true">✺</span>
<span class="ov5-name">The Long Lunch</span><span class="ov5-sep" aria-hidden="true">✺</span>
</div>
<div class="ov5-group" aria-hidden="true">
<span class="ov5-name">The Sober Table</span><span class="ov5-sep">✺</span>
<span class="ov5-name">Field & Glass</span><span class="ov5-sep">✺</span>
<span class="ov5-name">Weekend Pour</span><span class="ov5-sep">✺</span>
<span class="ov5-name">Kitchen Notes</span><span class="ov5-sep">✺</span>
<span class="ov5-name">The Long Lunch</span><span class="ov5-sep">✺</span>
</div>
</div>
</div>
<ul class="ov5-quotes">
<li><q>The first alcohol-free bottle we'd happily leave out on the table.</q><cite>The Sober Table</cite></li>
<li><q>Rosemary that tastes picked, not pressed into a flavouring lab.</q><cite>Field & Glass</cite></li>
<li><q>Our new house pour for Saturdays that start before noon.</q><cite>Weekend Pour</cite></li>
<li><q>Bitter, bright and properly grown-up. Pairs with anything salty.</q><cite>Kitchen Notes</cite></li>
<li><q>Three-hour lunch, zero regret. The juniper does the heavy lifting.</q><cite>The Long Lunch</cite></li>
</ul>
<div class="ov5-head">
<div>
<p class="ov-eyebrow">Word from the table</p>
<h2 class="ov-h2" id="ov5-title">Poured once. <em>Reordered</em> twice.</h2>
</div>
<div class="ov5-score">
<div class="ov5-num"><span class="ov5-count" data-to="4.9">4.9</span><small>/ 5</small></div>
<div>
<span class="ov5-stars ov5-stars--lg" role="img" aria-label="4.9 out of 5 stars">
<svg class="ov5-stars-base" viewBox="0 0 122 22" aria-hidden="true"><g fill="currentColor"><path id="ov5-s" d="M11 1.2l2.9 6.3 6.9.7-5.2 4.6 1.5 6.8L11 16.1l-6.1 3.5 1.5-6.8L1.2 8.2l6.9-.7z"/><use href="#ov5-s" x="25"/><use href="#ov5-s" x="50"/><use href="#ov5-s" x="75"/><use href="#ov5-s" x="100"/></g></svg>
<span class="ov5-stars-fill" style="width:98%"><svg viewBox="0 0 122 22" aria-hidden="true"><g fill="currentColor"><use href="#ov5-s"/><use href="#ov5-s" x="25"/><use href="#ov5-s" x="50"/><use href="#ov5-s" x="75"/><use href="#ov5-s" x="100"/></g></svg></span>
</span>
<p>from <b>2,300+ reviews</b><br>across all three bottles</p>
</div>
</div>
</div>
<div class="ov5-grid">
<div class="ov5-col">
<article class="ov5-card">
<span class="ov5-stars" role="img" aria-label="5 out of 5 stars"><svg class="ov5-stars-base" viewBox="0 0 122 22" aria-hidden="true"><g fill="currentColor"><use href="#ov5-s"/><use href="#ov5-s" x="25"/><use href="#ov5-s" x="50"/><use href="#ov5-s" x="75"/><use href="#ov5-s" x="100"/></g></svg><span class="ov5-stars-fill"><svg viewBox="0 0 122 22" aria-hidden="true"><g fill="currentColor"><use href="#ov5-s"/><use href="#ov5-s" x="25"/><use href="#ov5-s" x="50"/><use href="#ov5-s" x="75"/><use href="#ov5-s" x="100"/></g></svg></span></span>
<blockquote><strong>Finally, a proper aperitivo.</strong>Over ice with a strip of peel and nobody at dinner clocked it was zero-proof. I've stopped buying anything else for Friday nights.</blockquote>
<div class="ov5-meta"><div class="ov5-who">Priya S.<span>Bristol</span></div><span class="ov5-bottle">N°02 Grapefruit</span></div>
</article>
<article class="ov5-card">
<span class="ov5-stars" role="img" aria-label="5 out of 5 stars"><svg class="ov5-stars-base" viewBox="0 0 122 22" aria-hidden="true"><g fill="currentColor"><use href="#ov5-s"/><use href="#ov5-s" x="25"/><use href="#ov5-s" x="50"/><use href="#ov5-s" x="75"/><use href="#ov5-s" x="100"/></g></svg><span class="ov5-stars-fill"><svg viewBox="0 0 122 22" aria-hidden="true"><g fill="currentColor"><use href="#ov5-s"/><use href="#ov5-s" x="25"/><use href="#ov5-s" x="50"/><use href="#ov5-s" x="75"/><use href="#ov5-s" x="100"/></g></svg></span></span>
<blockquote><strong>Tastes like a walk.</strong>Piney, a bit resinous, dry finish. Great with tonic.</blockquote>
<div class="ov5-meta"><div class="ov5-who">Tomás R.<span>Valencia</span></div><span class="ov5-bottle">N°03 Juniper</span></div>
</article>
</div>
<div class="ov5-col">
<article class="ov5-card">
<span class="ov5-stars" role="img" aria-label="5 out of 5 stars"><svg class="ov5-stars-base" viewBox="0 0 122 22" aria-hidden="true"><g fill="currentColor"><use href="#ov5-s"/><use href="#ov5-s" x="25"/><use href="#ov5-s" x="50"/><use href="#ov5-s" x="75"/><use href="#ov5-s" x="100"/></g></svg><span class="ov5-stars-fill"><svg viewBox="0 0 122 22" aria-hidden="true"><g fill="currentColor"><use href="#ov5-s"/><use href="#ov5-s" x="25"/><use href="#ov5-s" x="50"/><use href="#ov5-s" x="75"/><use href="#ov5-s" x="100"/></g></svg></span></span>
<blockquote><strong>My pregnancy lifesaver.</strong>Nine months of lime soda was wearing thin. This has actual bite and a herby backbone, and it made the evenings feel like mine again.</blockquote>
<div class="ov5-meta"><div class="ov5-who">Hannah K.<span>Leeds</span></div><span class="ov5-bottle">N°01 Rosemary</span></div>
</article>
<article class="ov5-card">
<span class="ov5-stars" role="img" aria-label="5 out of 5 stars"><svg class="ov5-stars-base" viewBox="0 0 122 22" aria-hidden="true"><g fill="currentColor"><use href="#ov5-s"/><use href="#ov5-s" x="25"/><use href="#ov5-s" x="50"/><use href="#ov5-s" x="75"/><use href="#ov5-s" x="100"/></g></svg><span class="ov5-stars-fill"><svg viewBox="0 0 122 22" aria-hidden="true"><g fill="currentColor"><use href="#ov5-s"/><use href="#ov5-s" x="25"/><use href="#ov5-s" x="50"/><use href="#ov5-s" x="75"/><use href="#ov5-s" x="100"/></g></svg></span></span>
<blockquote><strong>Bought one. Now a subscriber.</strong>The grapefruit spritz is my go-to when friends come round.</blockquote>
<div class="ov5-meta"><div class="ov5-who">Marcus O.<span>Manchester</span></div><span class="ov5-bottle">Discovery set</span></div>
</article>
</div>
<div class="ov5-col">
<article class="ov5-card">
<span class="ov5-stars" role="img" aria-label="5 out of 5 stars"><svg class="ov5-stars-base" viewBox="0 0 122 22" aria-hidden="true"><g fill="currentColor"><use href="#ov5-s"/><use href="#ov5-s" x="25"/><use href="#ov5-s" x="50"/><use href="#ov5-s" x="75"/><use href="#ov5-s" x="100"/></g></svg><span class="ov5-stars-fill"><svg viewBox="0 0 122 22" aria-hidden="true"><g fill="currentColor"><use href="#ov5-s"/><use href="#ov5-s" x="25"/><use href="#ov5-s" x="50"/><use href="#ov5-s" x="75"/><use href="#ov5-s" x="100"/></g></svg></span></span>
<blockquote><strong>Not sweet. Thank you.</strong>Every other alcohol-free bottle I've tried tastes like cordial. This one tastes like someone actually cooked with rosemary.</blockquote>
<div class="ov5-meta"><div class="ov5-who">Elena V.<span>Lisbon</span></div><span class="ov5-bottle">N°01 Rosemary</span></div>
</article>
<article class="ov5-card">
<span class="ov5-stars" role="img" aria-label="5 out of 5 stars"><svg class="ov5-stars-base" viewBox="0 0 122 22" aria-hidden="true"><g fill="currentColor"><use href="#ov5-s"/><use href="#ov5-s" x="25"/><use href="#ov5-s" x="50"/><use href="#ov5-s" x="75"/><use href="#ov5-s" x="100"/></g></svg><span class="ov5-stars-fill"><svg viewBox="0 0 122 22" aria-hidden="true"><g fill="currentColor"><use href="#ov5-s"/><use href="#ov5-s" x="25"/><use href="#ov5-s" x="50"/><use href="#ov5-s" x="75"/><use href="#ov5-s" x="100"/></g></svg></span></span>
<blockquote><strong>Dry January, extended.</strong>Started as a one-month thing. It's September. The juniper with soda and a sprig of thyme is unbeatable after a long run.</blockquote>
<div class="ov5-meta"><div class="ov5-who">Callum J.<span>Edinburgh</span></div><span class="ov5-bottle">N°03 Juniper</span></div>
</article>
</div>
</div>
<div class="ov5-foot"><a class="ov-more" href="#range">Find your bottle <span>+</span></a></div>
</section>
<script>
(function () {
if (OV.reduce) return;
var sec = document.getElementById('reviews');
/* ---- press marquee: loop whose speed + direction follow scroll velocity ---- */
var track = sec.querySelector('.ov5-track');
var loop = gsap.to(track, { xPercent: -50, duration: 38, ease: 'none', repeat: -1 });
var dir = 1;
var proxy = { ts: 1 };
function applyTs(){ loop.timeScale(proxy.ts); }
ScrollTrigger.create({
trigger: sec, start: 'top bottom', end: 'bottom top',
onUpdate: function (self) {
var v = self.getVelocity();
if (Math.abs(v) < 5) return;
dir = v < 0 ? -1 : 1;
var boost = dir * Math.min(1 + Math.abs(v) / 260, 7);
proxy.ts = boost; applyTs();
gsap.to(proxy, { ts: dir, duration: 1.2, ease: 'power3.out', overwrite: true, onUpdate: applyTs });
}
});
// negative timeScale on a repeating tween needs time > 0 to run backwards forever
loop.totalTime(loop.duration() * 100);
/* ---- heading + aggregate ---- */
var count = sec.querySelector('.ov5-count');
var bigFill = sec.querySelector('.ov5-score .ov5-stars-fill');
var n = { v: 0 };
count.textContent = '0.0';
gsap.set(bigFill, { width: '0%' });
gsap.from('.ov5-head > div > *', {
y: 30, autoAlpha: 0, duration: .9, ease: 'power3.out', stagger: .1,
scrollTrigger: { trigger: '.ov5-head', start: 'top 82%', once: true }
});
ScrollTrigger.create({
trigger: '.ov5-score', start: 'top 88%', once: true,
onEnter: function () {
gsap.to(n, { v: 4.9, duration: 1.8, ease: 'power2.out', onUpdate: function(){ count.textContent = n.v.toFixed(1); } });
gsap.to(bigFill, { width: '98%', duration: 1.8, ease: 'power2.out' });
}
});
/* ---- press quotes ---- */
gsap.from(sec.querySelectorAll('.ov5-quotes li'), {
y: 24, autoAlpha: 0, duration: .8, ease: 'power3.out', stagger: .08,
scrollTrigger: { trigger: '.ov5-quotes', start: 'top 88%', once: true }
});
/* ---- review cards: rise + unrotate, then stars fill left to right ---- */
var cards = gsap.utils.toArray(sec.querySelectorAll('.ov5-card'));
cards.forEach(function (card) {
gsap.set(card, { y: 60, autoAlpha: 0, rotation: gsap.utils.random(-2, 2, .1) });
gsap.set(card.querySelector('.ov5-stars-fill'), { width: '0%' });
});
ScrollTrigger.batch(cards, {
start: 'top 90%', once: true,
onEnter: function (batch) {
var tl = gsap.timeline();
tl.to(batch, { y: 0, rotation: 0, autoAlpha: 1, duration: 1, ease: 'power3.out', stagger: .12 });
batch.forEach(function (card, i) {
tl.to(card.querySelector('.ov5-stars-fill'), { width: '100%', duration: .9, ease: 'power2.inOut' }, .35 + i * .12);
});
}
});
})();
</script>
```
## `sections/06-cta.html`
```html
<!-- ============ 06 CTA: raise a zero (oval photo reveal) + stockists + newsletter ============ -->
<style>
.oc{background:var(--lime);padding:var(--section-y) 0 calc(var(--section-y) * .8);overflow:hidden}
/* ---- the closing moment ---- */
.oc-stage{
height:100vh;height:100svh;min-height:560px;
display:flex;flex-direction:column;
padding:clamp(72px,11vh,110px) var(--gutter) clamp(20px,4vh,40px);
}
.oc-head{display:flex;align-items:flex-end;justify-content:space-between;gap:24px 40px;flex-wrap:wrap;margin-bottom:clamp(18px,3vh,32px)}
.oc-title{
margin:0;font-family:var(--display);font-weight:800;color:var(--forest);
font-size:clamp(40px,8.4vw,128px);line-height:.95;letter-spacing:-.045em;
}
.oc-title em{font-style:normal;color:var(--tang)}
.oc-w{display:inline-block;overflow:hidden;vertical-align:top;padding-bottom:.1em;margin-bottom:-.1em}
.oc-w > span{display:inline-block;will-change:transform}
.oc-sub{margin:0;max-width:330px;font-size:17px}
.oc-oval{position:relative;flex:1;min-height:0;overflow:hidden;border-radius:clamp(24px,4vw,56px)}
.oc-photo{
position:absolute;inset:0;overflow:hidden;background:var(--soft);border-radius:clamp(24px,4vw,56px);
-webkit-clip-path:ellipse(75% 80% at 50% 50%);clip-path:ellipse(75% 80% at 50% 50%);
}
.oc-photo img{width:100%;height:100%;object-fit:cover;object-position:50% 45%;transform-origin:50% 50%}
.oc-ring{
position:absolute;left:50%;top:50%;width:36%;height:60%;
transform:translate(-50%,-50%);border:clamp(8px,1vw,14px) solid var(--forest);border-radius:50%;
pointer-events:none;opacity:0;
}
.oc-badge{
position:absolute;right:clamp(12px,3vw,40px);bottom:clamp(12px,3vh,30px);
background:var(--soft-94);color:var(--forest);border-radius:999px;padding:10px 16px;
font-size:13px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;box-shadow:var(--shadow);
}
.oc-badge b{color:var(--tang);font-weight:600}
/* ---- cards ---- */
.oc-cards{
display:grid;grid-template-columns:1.15fr 1fr;gap:clamp(16px,2vw,28px);
padding:clamp(40px,7vh,80px) var(--gutter) 0;
}
.oc-card{
background:var(--soft);border-radius:var(--radius);box-shadow:var(--shadow);
padding:clamp(24px,3.4vw,48px);display:flex;flex-direction:column;
}
.oc-card h3{
margin:0 0 10px;font-family:var(--display);font-weight:700;color:var(--forest);
font-size:clamp(24px,2.4vw,34px);line-height:1.05;letter-spacing:-.03em;
}
.oc-card h3 em{font-style:normal;color:var(--tang)}
.oc-card > p{margin:0 0 22px}
.oc-shops{
list-style:none;margin:0 0 26px;padding:0;
display:grid;grid-template-columns:1fr 1fr;column-gap:28px;
}
.oc-shops li{
display:flex;justify-content:space-between;align-items:baseline;gap:10px;
padding:13px 0;border-bottom:1.5px solid var(--forest-12);transition:color .25s;
}
.oc-shops strong{font-weight:600;color:var(--ink)}
.oc-shops span{font-size:14px;color:var(--forest);white-space:nowrap}
.oc-shops li.is-near strong{color:var(--forest)}
.oc-shops li.is-near span{color:var(--ink);background:var(--tang-20);border-radius:999px;padding:2px 9px}
.oc-form{margin-top:auto}
.oc-label{display:block;font-size:13px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--forest);margin-bottom:8px}
.oc-row{display:flex;gap:10px}
.oc-input{
flex:1;min-width:0;font:inherit;font-size:16px;color:var(--ink);background:var(--white);
border:1.5px solid var(--forest-24);border-radius:999px;padding:11px 18px;
transition:border-color .2s,box-shadow .2s;
}
.oc-input::placeholder{color:rgba(31,59,44,.5)}
.oc-input:focus{outline:none;border-color:var(--tang);box-shadow:0 0 0 3px var(--tang-20),0 0 0 1.5px var(--tang)}
.oc-input[aria-invalid="true"]{border-color:var(--tang)}
.oc .ov-pill:focus-visible{outline:3px solid var(--tang);outline-offset:3px}
.oc-msg{min-height:1.55em;margin:10px 0 0;font-size:15px;color:var(--ink)}
.oc-msg.is-error::before{content:"";display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--tang);margin-right:8px;vertical-align:1px}
.oc-msg.is-ok{font-weight:600;color:var(--forest)}
.oc-card--join{background:var(--forest);color:var(--soft)}
.oc-card--join h3{color:var(--lime)}
.oc-card--join .oc-label{color:var(--lime)}
.oc-card--join .oc-input{border-color:transparent}
.oc-card--join .oc-input:focus,.oc-card--join .oc-input[aria-invalid="true"]{border-color:var(--tang)}
.oc-card--join .ov-pill{background:var(--lime);color:var(--forest)}
.oc-card--join .ov-pill:hover{background:var(--tang);color:var(--ink)}
.oc-card--join .oc-msg{color:var(--soft)}
.oc-card--join .oc-msg.is-ok{color:var(--lime)}
.oc-perks{list-style:none;margin:0 0 26px;padding:0;display:flex;flex-wrap:wrap;gap:8px}
.oc-perks li{font-size:14px;border:1.5px solid rgba(244,251,208,.3);border-radius:999px;padding:6px 12px}
.oc-done{display:none;align-items:center;gap:14px;margin-top:4px}
.oc-done-mark{flex:none;width:30px;height:40px;border:7px solid var(--lime);border-radius:50%}
.oc-form.is-sent .oc-row,.oc-form.is-sent .oc-label{display:none}
.oc-form.is-sent .oc-done{display:flex}
.oc-form.is-sent .oc-msg{display:none}
.oc-done p{margin:0;font-weight:600;color:var(--lime)}
@media (max-width:860px){
.oc-stage{height:88vh;height:88svh;min-height:520px}
.oc-head{margin-bottom:16px}
.oc-sub{max-width:none;font-size:16px}
.oc-cards{grid-template-columns:1fr}
.oc-ring{width:60%;height:52%}
}
@media (max-width:520px){
.oc-shops{grid-template-columns:1fr}
.oc-row{flex-direction:column}
.oc-row .ov-pill{justify-content:center}
.oc-badge{font-size:12px;padding:8px 13px}
}
</style>
<section id="stockists" class="oc" aria-labelledby="oc-title">
<div class="oc-stage">
<div class="oc-head">
<h2 class="oc-title" id="oc-title">
<span class="oc-w"><span>Raise</span></span>
<span class="oc-w"><span>a</span></span>
<span class="oc-w"><span><em>zero.</em></span></span>
</h2>
<p class="oc-sub">Long tables, loud laughs, clear heads in the morning. Pour Oval wherever the good company gathers.</p>
</div>
<div class="oc-oval">
<div class="oc-photo">
<img src="assets/cta.jpg" alt="Friends round a garden table in daylight, clinking glasses over salads, bread and a carafe of juice" loading="lazy">
</div>
<div class="oc-ring" aria-hidden="true"></div>
<span class="oc-badge">Cheers at <b>0.0%</b></span>
</div>
</div>
<div class="oc-cards">
<div class="oc-card oc-card--shops">
<p class="ov-eyebrow">Stockists</p>
<h3>Find a bottle <em>nearby</em></h3>
<p>Our favourite delis, bottle shops and grocers already have Oval on the shelf.</p>
<ul class="oc-shops">
<li data-pc="BS"><strong>Greengage Deli</strong><span>Bristol</span></li>
<li data-pc="M"><strong>The Pantry Row</strong><span>Manchester</span></li>
<li data-pc="EH"><strong>Fennel & Fig</strong><span>Edinburgh</span></li>
<li data-pc="BN"><strong>Low Tide Grocer</strong><span>Brighton</span></li>
<li data-pc="LS"><strong>Quince Cellar</strong><span>Leeds</span></li>
<li data-pc="E"><strong>Hedgerow Market</strong><span>London</span></li>
</ul>
<form class="oc-form oc-find" novalidate>
<label class="oc-label" for="oc-postcode">Find near you</label>
<div class="oc-row">
<input class="oc-input" id="oc-postcode" name="postcode" type="text" inputmode="text" autocomplete="postal-code" placeholder="Postcode, e.g. BS1 4DJ" aria-describedby="oc-find-msg">
<button class="ov-pill" type="submit">Search</button>
</div>
<p class="oc-msg" id="oc-find-msg" role="status" aria-live="polite"></p>
</form>
</div>
<div class="oc-card oc-card--join" id="join">
<p class="ov-eyebrow">Newsletter</p>
<h3>Join the Zero Club</h3>
<p>New botanicals, seasonal serves and first dibs on small batches, about twice a month.</p>
<ul class="oc-perks">
<li>Early batch access</li>
<li>Serve cards</li>
<li>Members' tastings</li>
</ul>
<form class="oc-form oc-join" novalidate>
<label class="oc-label" for="oc-email">Email address</label>
<div class="oc-row">
<input class="oc-input" id="oc-email" name="email" type="email" autocomplete="email" placeholder="
[email protected]" required aria-describedby="oc-join-msg">
<button class="ov-pill" type="submit">Join</button>
</div>
<p class="oc-msg" id="oc-join-msg" role="status" aria-live="polite"></p>
<div class="oc-done" role="status" aria-live="polite">
<span class="oc-done-mark" aria-hidden="true"></span>
<p class="oc-done-text"></p>
</div>
</form>
</div>
</div>
</section>
<script>
(function () {
var root = document.getElementById('stockists');
/* ---- newsletter: front-end only ---- */
var join = root.querySelector('.oc-join');
var email = join.querySelector('#oc-email');
var joinMsg = join.querySelector('#oc-join-msg');
function setMsg(el, text, kind){ el.textContent = text; el.className = 'oc-msg' + (kind ? ' is-' + kind : ''); }
join.addEventListener('submit', function (e) {
e.preventDefault();
var v = email.value.trim();
if (!v) { email.setAttribute('aria-invalid', 'true'); setMsg(joinMsg, 'Pop your email in first and we will save you a seat.', 'error'); email.focus(); return; }
if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v)) { email.setAttribute('aria-invalid', 'true'); setMsg(joinMsg, 'That email looks a little off. Mind checking it?', 'error'); email.focus(); return; }
email.removeAttribute('aria-invalid');
setMsg(joinMsg, '', '');
join.querySelector('.oc-done-text').textContent = "You're in. First pour's on us — check your inbox.";
join.classList.add('is-sent');
});
email.addEventListener('input', function(){ if (email.hasAttribute('aria-invalid')) { email.removeAttribute('aria-invalid'); setMsg(joinMsg, '', ''); } });
/* ---- stockist finder: matches the postcode area to a shop ---- */
var find = root.querySelector('.oc-find');
var pc = find.querySelector('#oc-postcode');
var findMsg = find.querySelector('#oc-find-msg');
var shops = root.querySelectorAll('.oc-shops li');
find.addEventListener('submit', function (e) {
e.preventDefault();
var v = pc.value.trim().toUpperCase().replace(/\s+/g, ' ');
shops.forEach(function(li){ li.classList.remove('is-near'); });
if (!/^[A-Z]{1,2}\d[A-Z\d]?( ?\d[A-Z]{2})?$/.test(v)) { pc.setAttribute('aria-invalid', 'true'); setMsg(findMsg, 'Try a postcode like BS1 4DJ or just the first half, like M4.', 'error'); pc.focus(); return; }
pc.removeAttribute('aria-invalid');
var area = v.match(/^[A-Z]+/)[0], hit = null;
shops.forEach(function(li){ if (!hit && li.dataset.pc === area) hit = li; });
if (hit) { hit.classList.add('is-near'); setMsg(findMsg, hit.querySelector('strong').textContent + ' is right on your doorstep.', 'ok'); }
else setMsg(findMsg, 'Nothing in ' + area + ' just yet. Order online and we will post it to you.', '');
});
if (OV.reduce) return;
/* ---- the oval reveal ---- */
var stage = root.querySelector('.oc-stage');
var photo = root.querySelector('.oc-photo');
var img = photo.querySelector('img');
var ring = root.querySelector('.oc-ring');
var words = root.querySelectorAll('.oc-w > span');
var sub = root.querySelector('.oc-sub');
var badge = root.querySelector('.oc-badge');
var mobile = matchMedia('(max-width:860px)');
// ellipse radii in % of the frame: small tall oval -> wide soft-cornered frame
var shape = { rx: 18, ry: 30 };
function from(){ return mobile.matches ? { rx: 28, ry: 34 } : { rx: 18, ry: 30 }; }
function paint(){
var c = 'ellipse(' + shape.rx + '% ' + shape.ry + '% at 50% 50%)';
photo.style.clipPath = c; photo.style.webkitClipPath = c;
// the ring hugs the clip edge (radius% of width/height -> diameter%)
ring.style.width = (shape.rx * 2) + '%';
ring.style.height = (shape.ry * 2) + '%';
}
gsap.set(words, { yPercent: 110 });
gsap.set(sub, { autoAlpha: 0, y: 20 });
gsap.set(badge, { autoAlpha: 0, y: 16 });
gsap.set(ring, { opacity: 1 });
gsap.set(img, { scale: 1.3 });
var s0 = from(); shape.rx = s0.rx; shape.ry = s0.ry; paint();
// headline rises as the stage arrives
gsap.to(words, {
yPercent: 0, duration: 1.1, ease: 'power4.out', stagger: .09,
scrollTrigger: { trigger: stage, start: 'top 70%', toggleActions: 'play none none reverse' }
});
gsap.to(sub, {
autoAlpha: 1, y: 0, duration: .9, ease: 'power3.out', delay: .3,
scrollTrigger: { trigger: stage, start: 'top 70%', toggleActions: 'play none none reverse' }
});
var tl = gsap.timeline({
defaults: { ease: 'none' },
scrollTrigger: {
trigger: stage, start: 'top top', end: '+=110%',
pin: true, scrub: true, anticipatePin: 1, invalidateOnRefresh: true
}
});
tl.to(shape, { rx: 75, ry: 80, duration: 5, ease: 'power1.inOut', onUpdate: paint }, 0)
.to(img, { scale: 1, duration: 5, ease: 'power1.out' }, 0)
.to(ring, { opacity: 0, duration: 1.3 }, 1.8)
.to(badge, { autoAlpha: 1, y: 0, duration: 1 }, 4.4)
.to({}, { duration: .8 });
mobile.addEventListener('change', function(){ ScrollTrigger.refresh(); });
ScrollTrigger.addEventListener('refreshInit', function(){
if (tl.progress() === 0) { var s = from(); shape.rx = s.rx; shape.ry = s.ry; paint(); }
});
/* ---- cards rise in ---- */
gsap.from(root.querySelectorAll('.oc-card'), {
y: 70, autoAlpha: 0, duration: 1.1, ease: 'power3.out', stagger: .14,
scrollTrigger: { trigger: root.querySelector('.oc-cards'), start: 'top 85%', toggleActions: 'play none none reverse' }
});
})();
</script>
```
## `sections/07-footer.html`
```html
<!-- ============ 07 FOOTER: link columns + the big oval wordmark ============ -->
<style>
.of{
background:var(--soft);color:var(--ink);border-radius:40px 40px 0 0;
padding:clamp(56px,9vh,96px) var(--gutter) 22px;overflow:hidden;
}
.of-top{display:grid;grid-template-columns:1.2fr repeat(4,1fr);gap:32px}
.of-intro p{margin:0 0 18px;max-width:280px;font-family:var(--display);font-weight:700;color:var(--forest);font-size:clamp(20px,1.8vw,26px);line-height:1.12;letter-spacing:-.025em}
.of-intro p em{font-style:normal;color:var(--tang)}
.of-col h3{margin:0 0 14px;font-size:13px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--tang)}
.of-col ul{list-style:none;margin:0;padding:0;display:grid;gap:9px}
.of-col a{text-decoration:none;font-weight:500;color:var(--ink);background:linear-gradient(var(--tang),var(--tang)) 0 100%/0 2px no-repeat;transition:color .2s,background-size .3s}
.of-col a:hover{color:var(--forest);background-size:100% 2px}
.of a:focus-visible,.of-top-btn:focus-visible{outline:3px solid var(--tang);outline-offset:3px;border-radius:4px}
.of-mark{
margin:clamp(40px,7vh,80px) 0 0;display:flex;justify-content:center;align-items:flex-end;
font-family:var(--display);font-weight:800;color:var(--forest);
font-size:22vw;line-height:.86;letter-spacing:-.05em;white-space:nowrap;
}
.of-l{display:inline-block;overflow:hidden;padding:0 .02em .04em}
.of-l > span{display:inline-block;will-change:transform}
/* the O is the logo mark: a tall oval ring sitting on the baseline */
.of-o{
display:inline-block;width:.6em;height:.72em;margin-right:.04em;
border:.15em solid var(--forest);border-radius:50%;
}
.of-base{
display:flex;justify-content:space-between;align-items:center;gap:14px;flex-wrap:wrap;
border-top:1.5px solid var(--forest-12);margin-top:clamp(14px,2vh,24px);padding-top:18px;font-size:14px;
}
.of-base p{margin:0}
.of-top-btn{
display:inline-flex;align-items:center;gap:8px;background:transparent;border:0;cursor:pointer;
color:var(--forest);font-weight:600;font-size:14px;padding:6px 0;
}
.of-top-btn span{
display:inline-grid;place-items:center;width:32px;height:32px;border-radius:50%;
background:var(--forest);color:var(--lime);transition:background .25s,color .25s,transform .25s;
}
.of-top-btn:hover span{background:var(--tang);color:var(--ink);transform:translateY(-3px)}
@media (max-width:860px){
.of-top{grid-template-columns:1fr 1fr;gap:32px 20px}
.of-intro{grid-column:1 / -1}
.of{border-radius:28px 28px 0 0}
}
</style>
<footer class="of">
<div class="of-top">
<div class="of-intro">
<p>All of the ritual, <em>none</em> of the proof.</p>
<a class="ov-pill" href="#join">Join the Zero Club</a>
</div>
<nav class="of-col" aria-label="Shop">
<h3>Shop</h3>
<ul>
<li><a href="#range">The range</a></li>
<li><a href="#range">N°01 Rosemary</a></li>
<li><a href="#range">N°02 Grapefruit</a></li>
<li><a href="#range">N°03 Juniper</a></li>
<li><a href="#stockists">Discovery set</a></li>
</ul>
</nav>
<nav class="of-col" aria-label="Learn">
<h3>Learn</h3>
<ul>
<li><a href="#benefits">Why zero</a></li>
<li><a href="#ritual">Serve rituals</a></li>
<li><a href="#ritual">Botanical notes</a></li>
<li><a href="#benefits">How we distil</a></li>
</ul>
</nav>
<nav class="of-col" aria-label="Company">
<h3>Company</h3>
<ul>
<li><a href="#top">Our garden</a></li>
<li><a href="#stockists">Stockists</a></li>
<li><a href="#stockists">Wholesale</a></li>
<li><a href="#join">Contact</a></li>
</ul>
</nav>
<nav class="of-col" aria-label="Follow">
<h3>Follow</h3>
<ul>
<li><a href="#join">Instagram</a></li>
<li><a href="#join">Pinterest</a></li>
<li><a href="#join">TikTok</a></li>
<li><a href="#join">Newsletter</a></li>
</ul>
</nav>
</div>
<p class="of-mark" aria-label="Oval">
<span class="of-l" aria-hidden="true"><span><i class="of-o"></i></span></span><span class="of-l" aria-hidden="true"><span>v</span></span><span class="of-l" aria-hidden="true"><span>a</span></span><span class="of-l" aria-hidden="true"><span>l</span></span>
</p>
<div class="of-base">
<p>© 2026 Oval Botanicals · 0.0% ABV · Drink curious.</p>
<button class="of-top-btn" type="button">Back to top <span aria-hidden="true">↑</span></button>
</div>
</footer>
<script>
(function () {
var foot = document.querySelector('.of');
foot.querySelector('.of-top-btn').addEventListener('click', function(){
if (window.OV && OV.lenis) OV.lenis.scrollTo(0, { duration: 1.6 });
else window.scrollTo({ top: 0, behavior: 'smooth' });
});
if (OV.reduce) return;
// wordmark letters climb out of their masks over the last stretch of the page
gsap.fromTo(foot.querySelectorAll('.of-l > span'), { yPercent: 100 }, {
yPercent: 0, ease: 'none', stagger: .15,
scrollTrigger: { trigger: foot, start: 'top bottom', end: 'bottom bottom', scrub: .6 }
});
gsap.from(foot.querySelectorAll('.of-intro, .of-col'), {
y: 36, autoAlpha: 0, duration: .9, ease: 'power3.out', stagger: .08,
scrollTrigger: { trigger: foot, start: 'top 85%', toggleActions: 'play none none reverse' }
});
})();
</script>
```
## `template.html`
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Oval — zero-proof botanicals</title>
<meta name="description" content="Oval makes zero-proof botanical spirits: rosemary, pink grapefruit and wild juniper at 0.0% ABV.">
<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=Inter+Tight:wght@400;500;600&family=Unbounded:wght@500;700;800&display=swap" rel="stylesheet">
<style>
:root{
--lime:#E4F76B;
--forest:#245C3E;
--tang:#FF8A3D;
--soft:#F4FBD0;
--ink:#1F3B2C;
--white:#FFFFFF;
--forest-12:rgba(36,92,62,.12);
--forest-24:rgba(36,92,62,.24);
--soft-94:rgba(244,251,208,.94);
--tang-20:rgba(255,138,61,.2);
--shadow:0 20px 50px -24px rgba(31,59,44,.35);
--display:"Unbounded",system-ui,sans-serif;
--body:"Inter Tight",system-ui,sans-serif;
--gutter:clamp(16px,4vw,56px);
--section-y:clamp(80px,13vh,150px);
--radius:22px;
}
*,*::before,*::after{box-sizing:border-box}
html,body{margin:0}
html.lenis,html.lenis body{height:auto}
.lenis.lenis-smooth{scroll-behavior:auto!important}
body{
background:var(--lime);color:var(--ink);
font-family:var(--body);font-size:17px;line-height:1.55;
overflow-x:hidden;-webkit-font-smoothing:antialiased;
}
img{display:block;max-width:100%}
a{color:inherit}
button{font:inherit}
/* ---------- shared atoms (used by every section) ---------- */
.ov-pill{
display:inline-flex;align-items:center;gap:8px;text-decoration:none;border:0;cursor:pointer;
background:var(--forest);color:var(--soft);font-weight:600;font-size:15px;
padding:11px 20px;border-radius:999px;transition:background .25s,color .25s,transform .25s;
}
.ov-pill:hover{background:var(--tang);color:var(--ink);transform:translateY(-1px)}
.ov-pill--ghost{background:transparent;color:var(--forest);box-shadow:inset 0 0 0 1.5px var(--forest-24)}
.ov-pill--ghost:hover{background:var(--forest);color:var(--soft)}
.ov-tag{
display:inline-block;font-size:13px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;
color:var(--forest);border:1.5px solid var(--forest-24);border-radius:999px;padding:6px 12px;
}
.ov-eyebrow{font-size:13px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--tang);margin:0 0 14px}
.ov-h2{
margin:0;font-family:var(--display);font-weight:700;color:var(--forest);
font-size:clamp(30px,4.6vw,64px);line-height:1;letter-spacing:-.035em;
}
.ov-h2 em{font-style:normal;color:var(--tang)}
.ov-more{
display:inline-flex;align-items:center;gap:8px;font-weight:600;text-decoration:none;color:var(--forest);
border-bottom:2px solid var(--tang);padding-bottom:2px;transition:color .2s,gap .25s;
}
.ov-more span{color:var(--tang);font-size:20px;line-height:1}
.ov-more:hover{color:var(--tang);gap:12px}
/* line mask used by OV.lines() */
.ov-line{display:block;overflow:hidden;padding-bottom:.08em;margin-bottom:-.08em}
.ov-line-in{display:block;will-change:transform}
</style>
<script src="https://cdn.jsdelivr.net/npm/
[email protected]/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/
[email protected]/dist/ScrollTrigger.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/
[email protected]/dist/lenis.min.js"></script>
<script>
/* ---------- shared runtime: window.OV ---------- */
(function(){
var OV = window.OV = {};
gsap.registerPlugin(ScrollTrigger);
// ?motion=1 forces animation on; otherwise honour the OS setting.
OV.reduce = /[?&]motion=1\b/.test(location.search) ? false
: matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!OV.reduce) document.documentElement.classList.add('ov-motion');
// One Lenis for the whole page, driven by the GSAP ticker.
OV.lenis = null;
if (!OV.reduce){
OV.lenis = new Lenis({ duration: 1.15, smoothWheel: true });
OV.lenis.on('scroll', ScrollTrigger.update);
gsap.ticker.add(function(t){ OV.lenis.raf(t * 1000); });
gsap.ticker.lagSmoothing(0);
}
// Smooth in-page anchor links.
document.addEventListener('click', function(e){
var a = e.target.closest('a[href^="#"]');
if (!a || a.getAttribute('href').length < 2) return;
var target = document.querySelector(a.getAttribute('href'));
if (!target) return;
e.preventDefault();
if (OV.lenis) OV.lenis.scrollTo(target, { offset: -10 });
else target.scrollIntoView();
});
// Wrap each direct text line of el in a mask: el's children become .ov-line > .ov-line-in.
// Pass an element whose content is plain text / inline tags; lines are measured after fonts load.
OV.lines = function(el){
if (el._ovLines) return el._ovLines;
var html = el.innerHTML.trim();
var tokens = html.split(/(\s+)/);
el.innerHTML = tokens.map(function(t){ return /^\s+$/.test(t) ? ' ' : '<span class="ov-w" style="display:inline-block">' + t + '</span>'; }).join('');
var rows = [], top = null;
el.querySelectorAll('.ov-w').forEach(function(w){
if (w.offsetTop !== top){ rows.push([]); top = w.offsetTop; }
rows[rows.length - 1].push(w.innerHTML);
});
el.innerHTML = rows.map(function(r){ return '<span class="ov-line"><span class="ov-line-in">' + r.join(' ') + '</span></span>'; }).join('');
el._ovLines = el.querySelectorAll('.ov-line-in');
return el._ovLines;
};
window.addEventListener('load', function(){ ScrollTrigger.refresh(); });
if (document.fonts) document.fonts.ready.then(function(){ ScrollTrigger.refresh(); });
})();
</script>
</head>
<body>
<!-- @include sections/01-hero.html -->
<!-- @include sections/02-benefits.html -->
<!-- @include sections/03-range.html -->
<!-- @include sections/04-ritual.html -->
<!-- @include sections/05-reviews.html -->
<!-- @include sections/06-cta.html -->
<!-- @include sections/07-footer.html -->
</body>
</html>
```