/* ── 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}

/* ── THE WAY INTO THE SHOP ───────────────────────────────────────────────────
   This page had no route to /shop.html at all. The film is the entire sales
   pitch and it ended nowhere: a visitor could watch all five worlds and never
   learn the perfume was for sale.

   Top RIGHT, opposite the wordmark, because that is where a century of shops
   have put the way to buy and nobody should have to hunt for it. Fixed, so it
   is reachable at any point in the film rather than only at the end — most
   people decide early, and a buy route you must finish a five-minute film to
   reach is one most people never see.

   Styled as a quiet hairline rather than a button: this is a luxury film, and
   a loud CTA over it would cheapen the thing it is selling. It earns attention
   on hover instead of demanding it. */
.to-shop{
  position:fixed;z-index:41;
  top:var(--gutter);right:var(--gutter);
  display:inline-flex;align-items:center;gap:.6rem;
  padding:.62rem 1.05rem;
  color:var(--paper);text-decoration:none;
  font-size:.62rem;letter-spacing:.26em;text-transform:uppercase;
  border:1px solid rgba(244,242,238,.28);
  background:rgba(13,15,17,.28);
  backdrop-filter:blur(10px);
  text-shadow:0 1px 16px rgba(0,0,0,.55);
  transition:border-color var(--mid) var(--ease), background var(--mid) var(--ease),
             letter-spacing var(--mid) var(--ease), gap var(--mid) var(--ease);
}
.to-shop svg{width:.85rem;height:.85rem;flex:none;opacity:.75;transition:transform var(--mid) var(--ease)}
.to-shop:hover,
.to-shop:focus-visible{
  border-color:var(--accent);
  background:rgba(13,15,17,.5);
  letter-spacing:.3em;gap:.85rem;
}
.to-shop:hover svg,
.to-shop:focus-visible svg{transform:translateX(3px);opacity:1}
.to-shop:focus-visible{outline:2px solid var(--accent);outline-offset:3px}

/* On a phone the film is the whole screen and the gutter is tight, so the
   label is dropped to the arrow alone rather than wrapping over the frame. */
@media (max-width:34rem){
  .to-shop{padding:.55rem .7rem;gap:.4rem}
  .to-shop span{
    /* visually hidden, still read aloud — the link must keep its name */
    position:absolute;width:1px;height:1px;margin:-1px;padding:0;
    overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;
  }
}

/* Someone who prefers less motion gets no letter-spacing shuffle. */
@media (prefers-reduced-motion:reduce){
  .to-shop,.to-shop svg{transition:none}
  .to-shop:hover,.to-shop:focus-visible{letter-spacing:.26em;gap:.6rem}
  .to-shop:hover svg{transform:none}
}

/* ── BUYING FROM INSIDE THE FILM ─────────────────────────────────────────────
   BOTTOM LEFT, above the world marks — opposite the copy, which has been
   right-aligned since the first build. Two hairline buttons, deliberately NOT
   a filled call-to-action: this is a luxury film, and a loud button over it
   would cheapen the thing it is selling.

   Fixed rather than in the copy block, because inside <article class="copy">
   these inherited the copy's rise-and-fade-on-scroll. That is right for prose
   and wrong for a control: the buy option faded out from under the pointer.

   Prices are written by engine/filmShop.js from the live catalogue — never
   hardcoded here, and the block stays hidden until real numbers arrive, so it
   can never show a stale or invented figure. */
.acquire{
  position:fixed;z-index:45;
  left:var(--gutter);
  bottom:calc(var(--gutter) + 2.9rem);   /* clears the marks row beneath it */
  display:flex;gap:.55rem;flex-wrap:wrap;
}
.acquire[hidden]{display:none}

/* ── THE PRODUCT ARRIVES WITH THE WORLD, NOT WITH A TIMER ────────────────────
   The prices rise into place on --dwell, the same number the copy fades on and
   the same number the film computes from scroll position. So the panel is not
   animated ALONGSIDE the arrival — it IS the arrival, sampled from the identical
   clock. A flick and a crawl travel the same curve; there is no second timeline
   to fall out of step with the film.

   There is deliberately NO timed exit. locate() reaches dwell 0 at every one of
   the nine world boundaries — proved by filmShop.invariant.test.mjs, which fails
   if anyone retunes the ramps — so the visitor's own scroll has already taken
   the panel to nothing before the next world exists. Every repaint therefore
   lands in the dark, and one perfume's price can never be seen in another's
   world. Adding a fade-out here would replace a guarantee with a race.

   STAGGERED BY DISTANCE, NOT BY DELAY. The 100ml button travels further, so it
   lags the 50ml button as a function of scroll POSITION. A transition-delay
   would be a second clock, and at speed the two buttons would invert. */
.acquire{ --acq-swap:1 }
.acq{
  display:inline-flex;align-items:baseline;gap:.6rem;
  padding:.6rem 1rem;
  background:rgba(13,15,17,.34);
  border:1px solid rgba(244,242,238,.26);
  color:var(--paper);cursor:pointer;
  font-family:inherit;font-size:.58rem;letter-spacing:.22em;text-transform:uppercase;
  backdrop-filter:blur(10px);
  text-shadow:0 1px 16px rgba(0,0,0,.5);
  opacity:calc(var(--dwell,0) * var(--acq-swap,1));
  translate:0 calc((1 - var(--dwell,0)) * 1.1rem);
  transition:border-color var(--mid) var(--ease), background var(--mid) var(--ease),
             opacity 240ms var(--ease);
}
.acq:nth-of-type(2){ translate:0 calc((1 - var(--dwell,0)) * 1.55rem) }

/* Interactivity follows visibility. An invisible panel that is still focusable
   lets a keyboard visitor buy from a world they are not standing in. Only the
   BUTTONS are withheld — the currency control lives inside #acquire and must
   never be hidden under the hand that just clicked it, which is also why the
   swap fades .acq and never the container. */
.acquire:not(.is-live) .acq{ pointer-events:none;visibility:hidden }

/* THE PRICE MUST BE FULLY READABLE, NOT 82% OF ONE.
   Under reduced motion the film seeks to anchorOf() — journey local 0.92 —
   which yields dwell 0.822 and parks there. Coupling opacity to dwell would
   leave the buy control permanently dimmed for the visitors least able to
   tolerate it, and the price is the one thing on screen that must never be
   approximate. So the scroll coupling is cut entirely here: the panel is either
   fully present or absent, decided by .is-live, exactly as .copy does above.
   The reducer is untouched — every step still runs, just with zero-length
   fades, so the ordering guarantee is unaffected.

   IT MUST SIT HERE, BELOW THE RULES IT OVERRIDES. It was first written up with
   the other reduced-motion rules, forty lines ABOVE .acq — same specificity,
   earlier in the file, so the cascade discarded it silently and the browser
   still measured 0.821. And :nth-of-type(2) has to be named explicitly: it is
   (0,2,0) and a bare .acq can never outrank it, so the 100ml button alone
   would have kept its 1.55rem offset. */
@media (prefers-reduced-motion:reduce){
  .acq,
  .acq:nth-of-type(2){
    opacity:calc(1 * var(--acq-swap,1));
    translate:none;
    transition:opacity .01ms linear;
  }
  .acquire:not(.is-live) .acq{ opacity:0 }
}

.acq .acq-price{
  font-family:'Cormorant Garamond',serif;
  font-size:.92rem;letter-spacing:.06em;text-transform:none;opacity:.92;
}
.acq:hover:not(:disabled),
.acq:focus-visible:not(:disabled){border-color:var(--accent);background:rgba(13,15,17,.56)}
.acq:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
.acq:disabled{opacity:.4;cursor:not-allowed}

/* ── THE BUCKET ──────────────────────────────────────────────────────────────
   Top right, beside the way into the collection.

   This replaced a full basket panel — every line, a subtotal, a checkout
   button. That is an ORDER SUMMARY, and an order summary parked over a film is
   the film interrupted to do paperwork. At the moment of adding, the only
   questions are "did that work" and "how many have I got". Everything else is
   one click away.

   transform and opacity only — both compositor properties, so the bucket
   arriving costs the film no layout and no paint. Animating width or right
   here would make the basket the reason the film stutters. */
.bucket{
  position:fixed;z-index:42;
  top:var(--gutter);
  /* sits to the LEFT of "The collection", which owns the corner */
  right:calc(var(--gutter) + 11.5rem);
  display:inline-flex;align-items:center;gap:.55rem;
  padding:.62rem .95rem;
  color:var(--paper);text-decoration:none;
  font-size:.58rem;letter-spacing:.2em;text-transform:uppercase;
  border:1px solid rgba(244,242,238,.28);
  background:rgba(13,15,17,.38);
  backdrop-filter:blur(10px);
  text-shadow:0 1px 16px rgba(0,0,0,.55);
  transform:translateY(-.5rem);opacity:0;
  transition:transform 420ms var(--ease), opacity 300ms var(--ease), border-color var(--mid) var(--ease);
  will-change:transform,opacity;
}
.bucket.is-in{transform:none;opacity:1}
.bucket[hidden]{display:none}
.bucket:hover,.bucket:focus-visible{border-color:var(--accent);background:rgba(13,15,17,.6)}
.bucket:focus-visible{outline:2px solid var(--accent);outline-offset:3px}

.bucket-icon{width:1rem;height:1rem;flex:none;opacity:.85}
.bucket-count{
  font-family:'Cormorant Garamond',serif;
  font-size:1.05rem;letter-spacing:0;text-transform:none;
  min-width:.7rem;text-align:center;
}
.bucket-label{opacity:.72}

/* A refusal turns it amber and drops the count — nothing was added, so a
   number would be a lie. */
.bucket[data-state="problem"]{border-color:rgba(216,180,140,.6);color:#e6c9a6}
.bucket[data-state="problem"] .bucket-count{display:none}

/* The pop on add. Runs on the whole chip so the count and icon move together;
   scale is compositor-only, so it is free. */
@keyframes bucket-pop{
  0%  {transform:scale(1)}
  38% {transform:scale(1.075)}
  100%{transform:scale(1)}
}
.bucket.is-in.is-pop{animation:bucket-pop 420ms var(--ease)}

/* On a phone the film is the whole screen. The bucket keeps the corner and the
   words go, because "2" and a basket icon say it without covering the frame —
   and "The collection" already sits under it as the arrow alone. */
@media (max-width:34rem){
  .bucket{right:calc(var(--gutter) + 3.6rem);padding:.5rem .7rem;gap:.4rem}
  .bucket-label{
    position:absolute;width:1px;height:1px;margin:-1px;padding:0;
    overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;
  }
  .acquire{bottom:calc(var(--gutter) + 2.6rem);gap:.4rem}
  .acq{padding:.5rem .75rem;gap:.45rem}
}

/* Reduced motion: it appears, it does not travel or pop. */
@media (prefers-reduced-motion:reduce){
  .bucket{transition:opacity 200ms linear;transform:none}
  .bucket.is-in{transform:none}
  .bucket.is-in.is-pop{animation:none}
}

/* A HARD WIDTH CEILING, as well as short text.
   The label is already shortened in filmShop.js, but a chip that can be
   widened by a server message is a chip that will be, one message from now.
   This is the guarantee: it can never span the film again. */
.bucket{max-width:min(22rem,52vw)}
.bucket-label{
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;
}

/* ── CURRENCY, BESIDE THE PRICES IT CHANGES ──────────────────────────────────
   In the masthead it would be separated from its effect, and a customer would
   have to look in two places to understand one number. */
.acq-currency{
  display:inline-flex;align-self:stretch;
  border:1px solid rgba(244,242,238,.22);
  background:rgba(13,15,17,.34);
  backdrop-filter:blur(10px);
}
.cur{
  background:none;border:0;cursor:pointer;
  padding:.6rem .7rem;
  color:var(--paper);opacity:.5;
  font-family:inherit;font-size:.55rem;letter-spacing:.2em;text-transform:uppercase;
  transition:opacity var(--mid) var(--ease), background var(--mid) var(--ease);
}
.cur:hover{opacity:.85}
.cur.is-on{opacity:1;background:rgba(244,242,238,.1)}
.cur:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}

/* The add button while the SERVER is deciding. Nothing says "added" here —
   this is the wait, and the card only appears once the shop has answered. */
.acq.is-working{opacity:.6;cursor:progress}
.acq.is-working .acq-price{visibility:hidden}
.acq.is-working::after{
  content:'';width:.7rem;height:.7rem;margin-left:-.4rem;
  border:1px solid currentColor;border-top-color:transparent;border-radius:50%;
  animation:acq-spin 620ms linear infinite;
}
@keyframes acq-spin{to{transform:rotate(360deg)}}

/* ── THE ADD-CARD STACK ──────────────────────────────────────────────────────
   One card per addition, under the bucket, each with its own lifespan. A single
   notification element would be OVERWRITTEN by the next click — which is
   precisely the collision this stack exists to prevent. Newest at the bottom,
   because that is where the eye already is after the previous one appeared. */
.basket-hud{
  position:fixed;z-index:42;
  top:var(--gutter);
  right:calc(var(--gutter) + 11.5rem);
  display:flex;flex-direction:column;align-items:flex-end;gap:.5rem;
  pointer-events:none;          /* the film stays scrollable through the gaps */
}
.basket-hud > *{pointer-events:auto}

.adds{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.45rem;align-items:flex-end}

.add-card{
  min-width:11rem;max-width:min(20rem,54vw);
  padding:.6rem .85rem;
  border:1px solid rgba(244,242,238,.2);
  background:rgba(13,15,17,.82);
  backdrop-filter:blur(16px);
  color:var(--paper);
  box-shadow:0 18px 44px rgba(0,0,0,.45);
  text-align:right;
  /* transform + opacity only: both compositor properties, so a card arriving
     costs the film no layout and no paint. */
  transform:translateX(.9rem);opacity:0;
  transition:transform 420ms var(--ease), opacity 320ms var(--ease);
  will-change:transform,opacity;
}
.add-card.is-in{transform:none;opacity:1}

.add-name{font-family:'Cormorant Garamond',serif;font-size:1.05rem;margin:0;line-height:1.2}
.add-meta{font-size:.55rem;letter-spacing:.18em;text-transform:uppercase;opacity:.62;margin:.2rem 0 0}
.add-price{font-family:'Cormorant Garamond',serif;font-size:.98rem;margin:.3rem 0 0;color:var(--accent)}
.add-card.is-problem{border-color:rgba(216,180,140,.55)}
.add-note{font-size:.74rem;line-height:1.5;margin:0;color:#e6c9a6;text-align:right}

/* The bucket sits inside the HUD now, so its own fixed position is dropped. */
.bucket{position:relative;top:auto;right:auto}

/* ── MOBILE: THE NOTIFICATION MOVES TO THE BOTTOM ────────────────────────────
   Top right is correct on a desktop, where the copy sits low on the right and
   there is clear air above it. On a phone the copy is FULL WIDTH from 95px to
   310px — measured, not assumed — so any card in the top corner necessarily
   covers the perfume's own description. There is no space to be had up there.

   So on a phone the whole head-up display sits just above the buy buttons it
   belongs to: same information, same lifecycle, in the one place on the screen
   that is already about buying. The stack grows upward from there, away from
   everything else. */
@media (max-width:34rem){
  .basket-hud{
    top:auto;
    bottom:calc(var(--gutter) + 6rem);
    right:var(--gutter);
    gap:.4rem;
  }
  .add-card{min-width:9rem;max-width:min(16rem,66vw);padding:.5rem .7rem}
  .add-name{font-size:.95rem}
  .acq-currency{align-self:auto}
  .cur{padding:.5rem .55rem}
}

@media (prefers-reduced-motion:reduce){
  .add-card{transition:opacity 200ms linear;transform:none}
  .add-card.is-in{transform:none}
  .acq.is-working::after{animation:none}
}
