/* ── SELF-HOSTED FACES ──────────────────────────────────────────────────
   Merged in here rather than shipped as fonts/fonts.css. MEASURED: as its own
   file it was a SECOND render-blocking request that finished at 3,232ms on
   Slow 4G and pushed First Contentful Paint from 1,664ms to 3,412ms - it was
   queued behind the preloaded opening frame on the same connection. Living
   inside a stylesheet the page already blocks on, it costs no request at all.
   Only latin + latin-ext ship; unicode-range keeps the browser honest. */
/* latin-ext */
@font-face {
  font-family: 'Cormorant Garamond';
  font-style: italic;
  font-weight: 300;
  font-display: swap;
  src: url(/fonts/cormorantgaramond-300-italic-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Cormorant Garamond';
  font-style: italic;
  font-weight: 300;
  font-display: swap;
  src: url(/fonts/cormorantgaramond-300-italic-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
  font-family: 'Cormorant Garamond';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url(/fonts/cormorantgaramond-300-normal-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Cormorant Garamond';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url(/fonts/cormorantgaramond-300-normal-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
  font-family: 'Cormorant Garamond';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/fonts/cormorantgaramond-400-normal-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Cormorant Garamond';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/fonts/cormorantgaramond-400-normal-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 200;
  font-display: swap;
  src: url(/fonts/inter-200-normal-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 200;
  font-display: swap;
  src: url(/fonts/inter-200-normal-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url(/fonts/inter-300-normal-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url(/fonts/inter-300-normal-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/fonts/inter-400-normal-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/fonts/inter-400-normal-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* CINQ TERRES — five worlds, one day.

   Design rules held throughout:
   - the world is desaturated, the perfume is the only saturated thing
   - glass is earned: the product is a glass vessel, so the UI is glass
   - low blur, low opacity, edges by refraction — never bordered cards
   - nothing bounces, nothing scales on hover, no visible scrollbar
   - THE LAW: nothing on this page ever comes fully to rest
*/

:root{
  --ink:#0d0f11;
  --paper:#f4f2ee;
  --ease:cubic-bezier(.16,1,.3,1);      /* easeOutExpo — the house curve */
  --slow:1200ms;
  --mid:760ms;
  --fast:420ms;

  --accent:#7fb2c7;                      /* set per world by JS */
  --gutter:clamp(1.4rem,4vw,4.5rem);

  /* driven by engine/atmosphere.js — all lagging, none exact */
  --par-x:0;      /* parallax, -1..1, already inverted against the pointer */
  --par-y:0;
  --lit:0;        /* 0 pointer away, 1 pointer present */
  --px:50vw;      /* raw pointer position */
  --py:50vh;

  /* THE LIFE. These never settle and never repeat — three sine waves per axis
     at periods with no common multiple. This is what stops the page becoming a
     photograph the moment a hand stops moving. Defaults here so the sibling
     test page renders correctly without the module. */
  --float-x:0;    /* -1..1, a camera that cannot quite hold still */
  --float-y:0;
  --sx:50vw;      /* where the light is — the pointer, or wandering on its own */
  --sy:42vh;

  /* scroll choreography — written by app.js, meaning assigned here */
  --sp:0;         /* 0..1 through the WHOLE film */
  --dwell:0;      /* 0..1 how arrived at a bottle you are */

  /* ── the drift ──────────────────────────────────────────────────────────
     How far each plane travels with the pointer. The film is the far plane
     and moves most; the numeral sits in front of it and moves less; the copy
     is nearest and moves the opposite way. Three rates = depth.

     --overscan is what makes it possible: the canvas hangs this far past every
     edge, so there is real image to slide into. Without it the film would pull
     a black margin in behind itself.

     It is applied as SIZE, not as scale(). Transform-scaling the canvas
     resampled a raster that was already at its native resolution and cost
     visible sharpness across the whole film. Making the element genuinely
     larger means each frame is still drawn 1:1 — it just has more canvas to
     be drawn into.

     THE INVARIANT: pointer drift + idle float must both fit inside the margin.
       x: --par-x arrives pre-scaled by 1.15 → 1.15 * 3.2vw = 3.68vw
          plus --float-x at full swing      → 1.00 * 2.4vw = 2.40vw
          = 6.08vw of travel against 8vw of margin
       y: 0.85 * 2.2vh + 1.00 * 1.9vh = 3.77vh against 8vh
     engine/drift.test.mjs asserts this and reads the multipliers straight out
     of atmosphere.js. Change any one number, run the test. */
  --overscan:8%;
  --drift-x:3.2vw;
  --drift-y:2.2vh;
  --float-x-amp:2.4vw;
  --float-y-amp:1.9vh;
}

*{box-sizing:border-box;margin:0;padding:0}

html,body{height:100%}
html{scrollbar-width:none}
html::-webkit-scrollbar{display:none}

body{
  background:var(--ink);
  color:var(--paper);
  font-family:'Inter',system-ui,sans-serif;
  font-weight:300;
  overflow:hidden;
  -webkit-font-smoothing:antialiased;
}

/* ── the film ─────────────────────────────────────────────────────────────
   One canvas, full bleed. No photograph underneath it: an earlier build
   cross-faded a still into the film and the mismatch in scale and position
   read as a glitch. There is nothing to mismatch now.

   The canvas fades in when its first real frame paints, so the page opens on
   ink rather than on a placeholder. */
#stage{position:fixed;inset:0;z-index:0}

/* The canvas is genuinely larger than the viewport — not scaled up to look it.
   Explicit width/height rather than relying on the insets: a <canvas> is a
   replaced element, and `width:auto` on one silently becomes 300x150. That has
   already cost this project once. */
#film{
  position:absolute;
  left:calc(-1 * var(--overscan));
  top:calc(-1 * var(--overscan));
  width:calc(100% + 2 * var(--overscan));
  height:calc(100% + 2 * var(--overscan));
  display:block;
  opacity:0;
  transition:opacity 900ms var(--ease);

  /* THE WORLD MOVES. Two motions summed on one compositor property:

     the DRIFT answers the pointer. atmosphere.js had always computed
     --par-x/--par-y and documented them as "the plate moves AGAINST the
     pointer" — but no rule ever applied them to the plate, so only the copy
     moved and the film stood still. That was the missing line.

     the FLOAT answers nothing. It runs whether or not anyone is here. Three
     sines per axis at periods that never realign, so it does not visibly loop.

     TRANSLATE ONLY. A slow scale breath was tried alongside these and removed:
     even at 1.8% it read as the frame pulsing in and out, which looks cheap on
     a photograph however slowly it happens. Shifting the camera's weight is
     life; changing how much of the world is visible is a wobble.

     No transition on any of it. Both are already smoothed in JS at frame rate;
     a CSS transition on top would be a second, slower spring fighting the
     first. */
  transform:
    translate3d(
      calc(var(--par-x) * var(--drift-x) + var(--float-x) * var(--float-x-amp)),
      calc(var(--par-y) * var(--drift-y) + var(--float-y) * var(--float-y-amp)), 0);
  will-change:transform;
}
#film.is-live{opacity:1}

/* a grade, not a scrim: darkens the edges so type holds without flattening
   the image. Weighted to the right, where the copy lives. */
.grade{
  position:absolute;inset:0;
  pointer-events:none;
  background:
    linear-gradient(to left, rgba(8,10,12,.80) 0%, rgba(8,10,12,.38) 36%, rgba(8,10,12,0) 64%),
    linear-gradient(to top,  rgba(8,10,12,.66) 0%, rgba(8,10,12,.14) 40%, rgba(8,10,12,0) 68%),
    linear-gradient(to bottom, rgba(8,10,12,.42) 0%, rgba(8,10,12,0) 28%);
}

/* ── atmosphere ───────────────────────────────────────────────────────────
   THE SHEEN. A soft light that tracks the pointer. A static specular reads as
   paint; only moving light reads as glass. `soft-light` brightens what is
   already bright and leaves the shadows alone, so it finds the bottle and the
   ice and ignores the black sand. */
/* The light never goes out. It used to fade to nothing the moment the pointer
   left, which made the frame die on you — a light that switches off when you
   look away is a lamp, not a sun. It now keeps a floor and goes wandering on
   its own path (--sx/--sy), crossing back under the pointer whenever there is
   one. No transition on the opacity: --lit is already spring-smoothed every
   frame, and a 500ms CSS transition on top of it lagged a full half second
   behind the hand. */
/* THE FLOW. A field of light the pointer pours into, which then spreads and
   thins on its own — engine/flow.js. It sits above the film and below the
   sheen, so it lights the world without ever washing over the copy.

   The canvas is a third of the screen and stretched back up: that upscale is
   the blur, and it is free because the compositor was going to scale this
   layer anyway. A real blur of the same radius, every frame, would not be.

   `screen` never darkens — it only ever adds light, which is what this is. */
.flow{
  position:fixed;inset:0;z-index:2;
  width:100%;height:100%;
  pointer-events:none;
  mix-blend-mode:screen;
  opacity:.66;
}

.sheen{
  position:fixed;inset:0;z-index:3;
  pointer-events:none;
  mix-blend-mode:soft-light;
  /* pulled back from .26/.6 — the flow now carries the light that follows the
     hand, and two full-strength light systems over one photograph read muddy */
  opacity:calc(.16 + var(--lit) * .32);
  background:radial-gradient(
    38vmax 38vmax at var(--sx) var(--sy),
    rgba(255,255,255,.55) 0%,
    rgba(255,255,255,.18) 34%,
    rgba(255,255,255,0)   68%);
}

/* FILM GRAIN. Photographic images on a screen look too clean; a little noise
   is what stops a large flat gradient banding and reads as film rather than
   render. Inline SVG turbulence, so no request and nothing to fail. */
.grain{
  position:fixed;inset:-120px;z-index:4;
  pointer-events:none;
  opacity:.16;
  background-image:url("data:image/svg+xml;utf8,\
<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'>\
<filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/>\
<feColorMatrix type='saturate' values='0'/></filter>\
<rect width='180' height='180' filter='url(%23n)' opacity='0.55'/></svg>");
  /* jumps between discrete offsets rather than sliding — sliding grain reads
     as a moving texture, jumping reads as successive frames of film */
  animation:grainShift 900ms steps(1) infinite;
}
@keyframes grainShift{
  0%  {transform:translate(0,0)}
  20% {transform:translate(-34px,18px)}
  40% {transform:translate(22px,-28px)}
  60% {transform:translate(-16px,-14px)}
  80% {transform:translate(30px,24px)}
  100%{transform:translate(0,0)}
}

/* ── the name of the shared object ───────────────────────────────────────
   The portal is no longer drawn over the film — it IS the film, a match cut
   generated with both ends locked. All that remains for the interface to do
   is name the thing the two worlds have in common, while you are inside it.
   The rule draws itself first and the words arrive on it. */
#gate-label{
  position:fixed;z-index:9;
  left:50%;bottom:16vh;
  transform:translateX(-50%);
  pointer-events:none;
  display:none;
  opacity:0;
  text-align:center;
  white-space:nowrap;
}
.gl-rule{
  display:block;
  width:5.5rem;height:1px;
  margin:0 auto .7rem;
  background:linear-gradient(to right,transparent,var(--paper),transparent);
  transform:scaleX(var(--gl,0));
  transform-origin:center;
}
.gl-text{
  display:block;
  font-family:'Cormorant Garamond',serif;
  font-style:italic;
  font-size:clamp(.95rem,1.6vw,1.4rem);
  letter-spacing:.04em;
  color:var(--paper);
  text-shadow:0 2px 22px rgba(0,0,0,.8);
}

/* ── masthead ─────────────────────────────────────────────────────────── */
/* No blend mode here. `difference` looked elegant against the dark plates and
   then disappeared completely against the glacier's mid-grey sky — difference
   against mid-grey returns mid-grey. A wordmark that survives only on some of
   the five worlds is not a wordmark. Solid ink with a soft carry shadow reads
   on all five. */
.masthead{
  position:fixed;z-index:40;
  top:var(--gutter);left:var(--gutter);
  display:flex;align-items:center;gap:.9rem;
  color:var(--paper);
  text-shadow:0 1px 16px rgba(0,0,0,.55), 0 0 2px rgba(0,0,0,.35);
}
.wordmark{
  font-family:'Cormorant Garamond',serif;
  font-size:.95rem;letter-spacing:.42em;font-weight:400;
}
.rule{width:2.2rem;height:1px;background:currentColor;opacity:.5}
.sub{font-size:.66rem;letter-spacing:.24em;text-transform:lowercase;opacity:.62}

/* ── the numeral: a watermark, not a label ────────────────────────────── */
.numeral{
  position:fixed;z-index:10;
  right:calc(var(--gutter) - .06em);
  top:50%;
  transform:translateY(-58%);
  font-family:'Cormorant Garamond',serif;
  font-weight:300;
  font-size:clamp(11rem,26vw,26rem);
  line-height:.72;
  color:var(--accent);
  /* it must sit BEHIND the copy without dimming it, so it is drawn as light
     rather than as a tinted shape */
  mix-blend-mode:soft-light;
  pointer-events:none;
  user-select:none;
  /* scroll-linked: the world grows around you as you go into it */
  opacity:calc(var(--dwell) * .22);
  scale:calc(1 + var(--dwell) * .10);
  /* the middle plane. It drifts the same way as the film — both are behind
     the copy — but at roughly a third of the distance, which is what stops
     the two of them reading as one flat sheet sliding about.
     Its float is NEGATED, so while the film breathes one way this breathes
     the other. Two planes floating in lockstep is one plane. */
  translate:calc(var(--par-x) * .9vw  - var(--float-x) * .75vw)
            calc(var(--par-y) * .6vh  - var(--float-y) * .60vh);
}

/* ── the copy: right column, clear of the bottle ──────────────────────── */
/* The five plates are not equally bright — the beach is a pale gold sky where
   the glacier is near black, and paper-white type simply vanished on it. A
   grade tuned for the dark plates cannot also serve the light one, so the type
   carries its own floor and stops depending on what is behind it. */
.copy{
  position:fixed;z-index:40;
  right:var(--gutter);
  bottom:calc(var(--gutter) + 4.4rem);
  width:min(38ch,46vw);
  text-align:right;
  text-shadow:0 1px 20px rgba(6,8,10,.72), 0 0 3px rgba(6,8,10,.42);
  /* Two motions composed on one element:
     - the type drifts WITH the pointer while the plate drifts against it, so
       the two planes separate (sign inverted because --par-x is already
       negated for the plate)
     - and it rises through the section as you scroll, fading as it goes,
       because by then you have read it */
  /* Two motions composed:
     - the type drifts WITH the pointer while the film drifts against it, so
       the planes separate (sign inverted because --par-x is already negated)
     - and it rises into place as you ARRIVE at the bottle, driven by --dwell,
       so the words appear because you got somewhere rather than on a timer */
  /* and a third float rate, smallest of the three — the words are nearest, so
     they move least. Nearest-moves-least is backwards for parallax against a
     pointer and correct for a camera that is breathing: everything in the
     frame shifts together, by less the closer it is to the lens. */
  translate:calc(var(--par-x) * -0.45% + var(--float-x) * .50vw)
            calc(var(--par-y) * -0.3%  + var(--float-y) * .40vh
                 + (1 - var(--dwell)) * 2.2vh);
  opacity:var(--dwell);
}

/* the name never animates. one still element gives the moving ones
   something to move against. */
.name{
  font-family:'Inter',sans-serif;
  font-weight:200;
  font-size:clamp(.74rem,1.1vw,.9rem);
  letter-spacing:.44em;
  text-transform:uppercase;
  opacity:.92;
  margin-bottom:1.25rem;
}
.name::after{
  content:'';display:inline-block;
  width:1.7rem;height:1px;
  background:var(--accent);
  vertical-align:middle;
  margin-left:1rem;
  transform-origin:right;
  transition:transform var(--mid) var(--ease) 240ms;
}
.can-animate .copy:not(.is-in) .name::after{transform:scaleX(0)}

/* line-mask reveal — split by LINE, never by character.
   Character animation reads as a startup; lines rising from behind a mask
   read as editorial. */
.line{
  font-family:'Cormorant Garamond',serif;
  font-style:italic;font-weight:300;
  font-size:clamp(1.35rem,3vw,2.55rem);
  line-height:1.26;
  letter-spacing:.005em;
  margin-bottom:1.9rem;
  text-wrap:balance;
}
.mask{display:block;overflow:hidden;padding-bottom:.08em}
.inner{
  display:block;
  transition:transform var(--slow) var(--ease), opacity var(--slow) var(--ease);
}

/* The copy is VISIBLE by default and the entrance is opt-in — `.can-animate`
   is set by JS once it is actually running.
   The obvious way round (hide in CSS, reveal with a class) means the page's
   main line lives at translateY(110%) behind an overflow:hidden mask, so any
   failure to complete that transition — JS error, stalled paint, a slow
   device — hides the headline permanently with no way back. Content must
   never depend on an animation finishing. */
.can-animate .copy:not(.is-in) .inner{transform:translateY(110%);opacity:0}

/* ── the glass panel ──────────────────────────────────────────────────────
   The product is a glass vessel, so the one solid interface surface on the
   page is glass too. Its edge is drawn by refraction and a light-catching
   hairline — the way a real chamfer catches a window — never by a border.  */
.notes{
  display:inline-block;
  text-align:right;
  padding:1.05rem 1.35rem 1.15rem;
  border-radius:2px;

  /* Real glass brightens what is behind it and carries the colour of what it
     holds. A flat white wash at low alpha just reads as a grey box — which is
     exactly what it did over the glacier's dark water — so the panel is tinted
     with the world's own accent and lifted at the top edge. */
  background:linear-gradient(150deg,
    color-mix(in oklab, var(--accent) 16%, rgba(255,255,255,.14)) 0%,
    rgba(255,255,255,.035) 46%,
    color-mix(in oklab, var(--accent) 10%, rgba(255,255,255,.06)) 100%);

  backdrop-filter:blur(16px) saturate(1.6) brightness(1.12);
  -webkit-backdrop-filter:blur(16px) saturate(1.6) brightness(1.12);

  /* the hairline is brighter where light would strike a bevel and fades
     around the form; a uniform 1px border is the tell of a template */
  box-shadow:
    inset 1px 1px 0 rgba(255,255,255,.28),
    inset -1px -1px 0 rgba(255,255,255,.06),
    0 18px 40px -22px rgba(0,0,0,.85);

  transition:opacity var(--mid) var(--ease) 420ms,
             transform var(--mid) var(--ease) 420ms;
}
.can-animate .copy:not(.is-in) .notes{opacity:0;transform:translateY(12px)}

/* .5 opacity measured ~3.2:1 on ALL five worlds, not just the bright ones —
   the tinted panel and the type dim together, so it is structural rather than
   photographic. .82 clears 4.5:1 and still reads as a quiet label. */
.notes-label{
  display:block;
  font-size:.55rem;letter-spacing:.34em;text-transform:uppercase;
  opacity:.82;
  margin-bottom:.6rem;
}
.notes-list{list-style:none;display:flex;flex-wrap:wrap;justify-content:flex-end;gap:.45rem .8rem}
.notes-list li{
  font-size:.74rem;letter-spacing:.06em;
  opacity:.88;
  position:relative;
}
.notes-list li+li::before{
  content:'';position:absolute;left:-.46rem;top:50%;
  width:2px;height:2px;border-radius:50%;
  background:var(--accent);
  transform:translateY(-50%);
}
/* The accents are chosen to sit in a photograph, which makes them too dark to
   be type. Lifted toward white for this one use — it still reads as the
   world's colour, and it clears 4.5:1 (desert measured 2.64 before). */
.notes-hour{
  display:block;
  margin-top:.85rem;
  font-size:.56rem;letter-spacing:.3em;text-transform:uppercase;
  color:color-mix(in oklab, var(--accent) 45%, white);
}

/* ── navigation: five colour marks ────────────────────────────────────── */
.marks{
  position:fixed;z-index:50;
  left:var(--gutter);bottom:var(--gutter);
  display:flex;align-items:center;gap:1.05rem;
}
.mark{
  position:relative;
  width:2.4rem;height:2.4rem;
  display:grid;place-items:center;
  background:none;border:0;padding:0;cursor:pointer;
  -webkit-tap-highlight-color:transparent;
  /* real links, so they work before JS loads — but they must not look like
     links */
  text-decoration:none;color:inherit;
}
.mark .dot{
  width:.52rem;height:.52rem;border-radius:50%;
  background:var(--c);
  opacity:.34;
  filter:saturate(.25);
  transition:opacity var(--mid) var(--ease),
             filter var(--mid) var(--ease),
             transform var(--mid) var(--ease);
}
.mark:hover .dot{opacity:.7;filter:saturate(.7)}
.mark.is-on .dot{opacity:1;filter:saturate(1);transform:scale(1.55)}
/* the ring is drawn by refraction, not by a border */
.mark.is-on::after{
  content:'';position:absolute;inset:.32rem;
  border-radius:50%;
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.22);
  backdrop-filter:blur(2px) saturate(1.1);
  -webkit-backdrop-filter:blur(2px) saturate(1.1);
}

/* The active mark IS the progress readout. A separate bar somewhere else on
   the page would be a second thing to look at; drawing it on the dot you are
   already watching means the navigation and the scroll position are one
   object. It fills as the liquid settles and empties as you leave. */
.mark.is-on::before{
  content:'';position:absolute;inset:.06rem;
  border-radius:50%;
  background:conic-gradient(from -90deg,
    var(--c) calc(var(--sp) * 360deg),
    transparent calc(var(--sp) * 360deg));
  -webkit-mask:radial-gradient(circle, transparent 58%, #000 62%);
          mask:radial-gradient(circle, transparent 58%, #000 62%);
  opacity:.85;
}
.mark .tip{
  position:absolute;bottom:100%;left:50%;
  transform:translateX(-50%) translateY(.2rem);
  font-size:.56rem;letter-spacing:.24em;text-transform:uppercase;
  white-space:nowrap;
  opacity:0;pointer-events:none;
  transition:opacity var(--fast) var(--ease), transform var(--fast) var(--ease);
}
.mark:hover .tip,
.mark:focus-visible .tip{opacity:.75;transform:translateX(-50%) translateY(-.15rem)}
/* A 1px ring at 40% white measured 2.6:1 against the beach plate — below the
   3:1 WCAG needs, on four of the five dots. It has to survive a pale gold sky
   AND a black lagoon, so it is drawn twice: a solid white ring with a dark
   halo outside it. One of the two always has contrast. */
.mark:focus-visible{
  outline:2px solid #fff;
  outline-offset:2px;
  border-radius:50%;
  box-shadow:0 0 0 5px rgba(6,8,10,.62);
}

/* ── scroll hint ──────────────────────────────────────────────────────── */
.hint{
  position:fixed;z-index:40;
  left:50%;transform:translateX(-50%);
  bottom:calc(var(--gutter) + .5rem);
  font-size:.58rem;letter-spacing:.34em;text-transform:uppercase;
  opacity:.4;
  transition:opacity var(--mid) var(--ease);
}
.hint.is-gone{opacity:0}

/* ── responsive ───────────────────────────────────────────────────────── */
@media (max-width:900px){
  .copy{width:min(34ch,64vw)}
  .numeral{font-size:clamp(9rem,34vw,18rem);opacity:.07}
}
@media (max-width:640px){

  /* the bottle sits low-left on a phone, so the copy takes the top half */
  .copy{
    right:var(--gutter);left:var(--gutter);
    width:auto;
    top:calc(var(--gutter) + 3.4rem);
    bottom:auto;
  }
  .masthead .sub{display:none}
  .marks{gap:.7rem}

  /* The five marks reach 259px from the left at this size and the centred
     hint spans 223-273px, so "scroll" sat inside the fifth dot's hit area and
     its focus ring. Send it to the opposite corner. */
  .hint{left:auto;right:var(--gutter);transform:none}
  .grade{
    background:
      linear-gradient(to bottom, rgba(8,10,12,.80) 0%, rgba(8,10,12,.20) 46%, rgba(8,10,12,0) 70%),
      linear-gradient(to top,    rgba(8,10,12,.62) 0%, rgba(8,10,12,0) 34%);
  }
}

@media (prefers-reduced-motion:reduce){
  *{animation-duration:.01ms !important;animation-iteration-count:1 !important;
    transition-duration:.01ms !important}
  /* No drift and no float. THE LAW does not apply here: a visitor who has
     asked their operating system to stop moving things has overruled the
     house. atmosphere.js also refuses to start its loop under this setting,
     so the variables stay at their :root zeros and nothing is left running. */
  #film{transition:none;transform:none}
  .numeral{translate:0 0}
  .copy{translate:0 0;opacity:1}
  .grain{animation:none}
  .sheen{display:none}
  /* flow.js also refuses to construct itself under this setting */
  .flow{display:none}
  /* the pointer goes back to being the operating system's. cursor.js also
     refuses to start under this setting, so nothing is left running. */
  .cursor-lens,.cursor-core{display:none}
  html.cursor-live,html.cursor-live *{cursor:auto}
}


/* ── the rail ─────────────────────────────────────────────────────────────
   One hairline across the foot of the frame showing where you are in the
   whole film. The film is 55 seconds of continuous travel; without this there
   is no way to know whether you are near the second fragrance or the fifth.
   Drawn as a scale on a single element, so it costs one compositor property. */
.rail{
  position:fixed;z-index:45;
  left:0;right:0;bottom:0;height:1px;
  background:rgba(255,255,255,.10);
  pointer-events:none;
}
.rail-fill{
  display:block;height:100%;
  background:var(--accent);
  transform-origin:left center;
  scale:0 1;
  transition:background var(--slow) var(--ease);
}

/* ── the cursor ───────────────────────────────────────────────────────────
   Built as a lens and a point, moving at two different speeds — see
   engine/cursor.js for why. Everything here is the house glass recipe from
   .notes and .mark.is-on::after: refraction, a light-catching hairline, no
   border anywhere.

   The native pointer is hidden by `html.cursor-live`, a class JS adds on the
   first real pointer movement — the same opt-in pattern as `.can-animate`.
   If this module never runs, the class is never set and the visitor simply
   keeps their own cursor. A page must never be able to lose the pointer
   because a script failed. */
.cursor-lens,
.cursor-core{
  position:fixed;top:0;left:0;z-index:9999;
  border-radius:50%;
  pointer-events:none;
  opacity:0;
  transition:opacity var(--fast) var(--ease),
             width 300ms var(--ease),
             height 300ms var(--ease);
}
html.cursor-live .cursor-lens,
html.cursor-live .cursor-core{opacity:1}

/* hide the real one — including over .mark, which sets cursor:pointer */
html.cursor-live,
html.cursor-live *{cursor:none}

.cursor-lens{
  width:30px;height:30px;
  /* the same brightening glass as the notes panel, at a size where only the
     refraction is legible — a blur this small does not smear the film, it
     just bends it enough that the disc has an edge */
  backdrop-filter:blur(2.5px) saturate(1.55) brightness(1.07);
  -webkit-backdrop-filter:blur(2.5px) saturate(1.55) brightness(1.07);
  box-shadow:
    inset 0 0 0 1px rgba(255,255,255,.26),
    inset 0 1px 0 rgba(255,255,255,.34),
    0 6px 18px -8px rgba(0,0,0,.7);
}
/* over something clickable the glass opens. It widens on the house curve
   rather than popping — "nothing bounces" holds for the pointer too. */
.cursor-lens.is-hot{
  width:48px;height:48px;
  backdrop-filter:blur(3.5px) saturate(1.8) brightness(1.12);
  -webkit-backdrop-filter:blur(3.5px) saturate(1.8) brightness(1.12);
  box-shadow:
    inset 0 0 0 1px rgba(255,255,255,.42),
    inset 0 1px 0 rgba(255,255,255,.5),
    0 8px 24px -8px rgba(0,0,0,.75);
}
/* pressed: the glass closes on the point */
.cursor-lens.is-down{width:22px;height:22px}

/* the point carries the world's colour, so the pointer is glacier-blue in the
   first fragrance and oud-violet in the fifth without anything being told to
   change it — --accent is already rewritten per world by app.js */
.cursor-core{
  width:4.5px;height:4.5px;
  background:var(--accent);
  box-shadow:0 0 10px -1px var(--accent), 0 0 2px rgba(0,0,0,.5);
}
/* it dims as the lens opens, so the two never compete for the same moment */
.cursor-lens.is-hot ~ .cursor-core{opacity:.55}
