/* ===========================================================================
   VELVET EMBRACE — MOTION
   ---------------------------------------------------------------------------
   The fourth sheet, and the only optional one. Everything in here is finish:
   entrances, sheen, drift, the way a card answers a cursor. Delete the one
   <link> that loads it and the site is exactly the site it was — plain,
   complete, and correct. Nothing below is load-bearing.

   That is the rule this file lives by, and it is worth stating because it is
   easy to break: no layout is defined here, no colour is invented here, no
   content depends on an animation finishing. If a phone drops every frame,
   the page still reads.

   THREE THINGS IT WILL NOT DO
   ---------------------------------------------------------------------------
   1. IT WILL NOT MOVE WHEN SOMEBODY HAS ASKED IT NOT TO.
      styles.css already carries a global prefers-reduced-motion block that
      flattens every animation and transition to .01ms and every loop to a
      single iteration. Everything here is written so that the LAST FRAME is
      the correct frame — `animation-fill-mode: both` throughout — so when
      that block bites, the result is the finished state, instantly, not a
      half-drawn one.

   2. IT WILL NOT HIDE ANYTHING PERMANENTLY.
      An entrance that starts at opacity 0 is a promise that something will
      turn it back on. The hero's is keyed on `.ve-ready`, which site.js adds
      — and there is a fail-safe below that shows it anyway if that never
      happens. See the note there; this is the one genuinely dangerous
      pattern in the file.

   3. IT WILL NOT REACH OUT. One data: URI for the paper grain, generated in
      this file, no request. Same rule as the fonts.
   ========================================================================= */

:root {
  /* One overshoot-free easing for entrances, a softer one for hovers, and a
     long slow one for anything that loops. Named rather than repeated so a
     change of feel is one line. */
  --ease-enter: cubic-bezier(.16, 1, .3, 1);
  --ease-drift: cubic-bezier(.45, 0, .55, 1);

  --dur-enter: .95s;
  --dur-hover: .32s;

  /* The stagger between siblings arriving. Under about 60ms a group reads as
     one block; over about 140ms it reads as a queue and the last card feels
     late. */
  --stagger: 85ms;
}

/* =========================================================================
   THE PAPER GRAIN
   -------------------------------------------------------------------------
   A single fixed layer of fractal noise over the whole page at 3%. It is the
   difference between "a flat green rectangle" and "a printed page", and it
   costs one data: URI and no requests.

   ⚠ pointer-events: none, and it sits ABOVE the skip link on purpose — the
     grain must never be the thing a click lands on, and at 3% it cannot be
     seen over anything.
   ========================================================================= */

.ve-grain {
  position: fixed; inset: 0;
  z-index: 400;
  pointer-events: none;
  opacity: .038;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 160px 160px;
}
@media print { .ve-grain { display: none; } }

/* =========================================================================
   THE HERO ENTRANCE
   -------------------------------------------------------------------------
   The one piece of choreography on the site. Five things arrive in the order
   somebody reads them — eyebrow, first line, second line, sentence, buttons
   — with the picture coming up underneath from a fraction behind.

   ⚠  THE FAIL-SAFE MATTERS MORE THAN THE ANIMATION.
      These elements start invisible, and they are turned on by a class that
      site.js adds. If site.js never arrives — bad wifi, a proxy, a syntax
      error introduced next year — the hero would be a blank green band with
      no way back. So each one ALSO carries a second animation that simply
      shows it after 2.6 seconds no matter what. It never fires in practice,
      because .ve-ready lands in tens of milliseconds and replaces it. It is
      there for the day something goes wrong.

      (`html.no-js` never gets .ve-ready at all and is handled by styles.css,
      which is why nothing here touches it.)
   ========================================================================= */

.hero__title > span { display: block; }

.hero .eyebrow,
.hero__title > span,
.hero__intro,
.hero__actions,
.hero__figure {
  animation: ve-failsafe 1ms 2.6s both;
}

@keyframes ve-failsafe {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.ve-ready .hero .eyebrow   { animation: ve-rise var(--dur-enter) var(--ease-enter) .05s both; }
.ve-ready .hero__title > span:first-child { animation: ve-line var(--dur-enter) var(--ease-enter) .14s both; }
.ve-ready .hero__title > span + span      { animation: ve-line var(--dur-enter) var(--ease-enter) .26s both; }
.ve-ready .hero__intro     { animation: ve-rise var(--dur-enter) var(--ease-enter) .40s both; }
.ve-ready .hero__actions   { animation: ve-rise var(--dur-enter) var(--ease-enter) .52s both; }
/* .hero__figure is declared once, with its float, further down. */

@keyframes ve-rise {
  from { opacity: 0; transform: translateY(18px); }
  to   { opacity: 1; transform: none; }
}

/* The headline rises out from behind its own baseline. clip-path rather than
   a wrapper with overflow:hidden, because there is no wrapper to add — and
   the end state uses NEGATIVE insets so that Fraunces' italic swash on
   "wearing" is not shaved off once the animation has finished. */
@keyframes ve-line {
  from { opacity: 0; transform: translateY(.42em); clip-path: inset(0 0 108% 0); }
  to   { opacity: 1; transform: none;              clip-path: inset(-30% -14% -30% -14%); }
}

@keyframes ve-figure {
  from { opacity: 0; transform: perspective(1200px) translateY(26px) scale(.955); }
  to   { opacity: 1; transform: perspective(1200px) translateY(0) scale(1); }
}

/* The buttons inside the hero arrive one after the other, a beat behind the
   row they live in. */
.ve-ready .hero__actions > * { animation: ve-rise .8s var(--ease-enter) both; }
.ve-ready .hero__actions > :nth-child(1) { animation-delay: .58s; }
.ve-ready .hero__actions > :nth-child(2) { animation-delay: .68s; }

/* =========================================================================
   THE HERO ATMOSPHERE
   -------------------------------------------------------------------------
   Two soft blooms drifting behind the type, on top of the static pair that
   styles.css already paints. They move slowly enough that you never catch
   them moving — the hero simply feels alive rather than printed.

   ⚠ inset: 0 AND NOTHING BIGGER. An oversized absolutely-positioned box
     inside the hero extends its scrollable overflow even under overflow:
     hidden, which leaves a section that can be scrolled inside itself with
     nothing to see. That bug has been fixed here once already; the bleed
     comes from the gradients' own geometry and from `scale`, which does not
     affect scroll extent.
   ========================================================================= */

/* The clipped box every decorative layer in the hero lives inside. See the
   long note on aura() in motion.js: without it, a layer that scales or is
   dragged by the pointer extends the hero's own scrollable overflow. */
.hero__fx {
  position: absolute; inset: 0;
  overflow: hidden;
  z-index: -1;
  pointer-events: none;
}

.hero__aura {
  position: absolute; inset: 0;
  opacity: .85;
  background:
    radial-gradient(38% 42% at 22% 30%, color-mix(in srgb, var(--sage) 40%, transparent), transparent 72%),
    radial-gradient(34% 38% at 78% 68%, color-mix(in srgb, var(--silver-pale) 60%, transparent), transparent 70%);
  animation: ve-aura 26s var(--ease-drift) infinite alternate;
}

/* ---- the light that follows the cursor ---------------------------------
   A soft warm bloom under the pointer while it is over the hero. It is the
   one place on the site that answers a mouse directly, and it is the right
   place: the hero is what a fragrance shop cannot photograph, so the hero is
   where the invisible things happen.

   ⚠  IT LIVES INSIDE .hero__fx, and it has to. An absolutely positioned box
      this large, dragged around by a pointer, extends the hero's SCROLLABLE
      OVERFLOW every time it goes past an edge — leaving a band that can be
      scrolled inside itself, which is the bug the note above describes. */
.hero__glow {
  position: absolute; left: 0; top: 0;
  width: 30rem; aspect-ratio: 1;
  translate: calc(var(--gx, 50%) - 15rem) calc(var(--gy, 50%) - 15rem);
  background: radial-gradient(circle,
    color-mix(in srgb, var(--sage) 30%, transparent) 0%,
    color-mix(in srgb, var(--sage-soft) 16%, transparent) 38%,
    transparent 68%);
  opacity: 0;
  transition: opacity .7s var(--ease-soft);
}
.hero:hover .hero__glow { opacity: 1; }

@keyframes ve-aura {
  from { transform: scale(1)    translate3d(0, 0, 0); }
  to   { transform: scale(1.18) translate3d(2%, -3%, 0); }
}

/* =========================================================================
   THE DRAWN PANEL — tilt, float, and the light that crosses it
   -------------------------------------------------------------------------
   Three separate movements that must not fight over the same property:

     transform  — the tilt (set by motion.js from the pointer) and the
                  scroll parallax, both fed in as custom properties.
     translate  — the idle float. A SEPARATE CSS property, which is exactly
                  why it can loop forever without stamping on the transform.
     ::after    — a specular sweep, every twelve seconds, that reads as light
                  moving across glass.
   ========================================================================= */

.hero__figure {
  transform:
    perspective(1200px)
    translate3d(0, var(--py, 0px), 0)
    rotateX(var(--rx, 0deg))
    rotateY(var(--ry, 0deg));
  transform-style: preserve-3d;
  will-change: transform;
  transition: box-shadow .5s var(--ease-soft);
}
/* ⚠ THE FLOAT LIVES ON `translate`, NOT INSIDE THE TRANSFORM ABOVE.
     They are two separate CSS properties, which is the whole trick: the
     pointer tilt can keep rewriting `transform` sixty times a second while
     the idle float loops forever on `translate`, and neither cancels the
     other. Written as one animation shorthand so the entrance and the loop
     cannot drift out of step. */
.ve-ready .hero__figure {
  animation:
    ve-figure 1.25s var(--ease-enter) .18s both,
    ve-hover 12s var(--ease-drift) 1.4s infinite alternate;
}
@keyframes ve-hover {
  from { translate: 0 0; }
  to   { translate: 0 -9px; }
}

.hero__figure::after {
  content: '';
  position: absolute; inset: 0;
  pointer-events: none;
  background: linear-gradient(104deg,
    transparent 34%,
    color-mix(in srgb, var(--white) 62%, transparent) 48%,
    transparent 62%);
  transform: translateX(-130%);
  animation: ve-sweep 12s var(--ease-drift) 3s infinite;
}
@keyframes ve-sweep {
  0%        { transform: translateX(-130%); }
  22%, 100% { transform: translateX(130%); }
}

/* ---- and the illustration itself breathes -------------------------------
   Classes are on the groups inside the inline SVG. transform-box: fill-box
   is not optional: without it an SVG child rotates around the origin of the
   whole viewBox and flies off the canvas. */
.hero__panel [class^="ve-"] { transform-box: fill-box; transform-origin: center; }

.ve-halo   { animation: ve-breathe 9s var(--ease-drift) infinite alternate; }
.ve-disc   { animation: ve-breathe 13s var(--ease-drift) 1s infinite alternate; }
.ve-btl-1  { animation: ve-bob 7.5s var(--ease-drift) infinite alternate; }
.ve-btl-2  { animation: ve-bob 9.5s var(--ease-drift) .6s infinite alternate; }
.ve-btl-3  { animation: ve-bob 8.2s var(--ease-drift) 1.2s infinite alternate; }
.ve-ribbon { animation: ve-sway 6.5s var(--ease-drift) infinite alternate; }
.ve-shine  { animation: ve-glint 12s var(--ease-drift) 3s infinite; }
.ve-motes  { animation: ve-motes 18s var(--ease-drift) infinite alternate; }

@keyframes ve-breathe { from { transform: scale(1); opacity: 1; } to { transform: scale(1.05); opacity: .82; } }
@keyframes ve-bob     { from { transform: translateY(0); } to { transform: translateY(-2.5px); } }
@keyframes ve-sway    { from { transform: rotate(-2.2deg); } to { transform: rotate(2.2deg); } }
@keyframes ve-glint   { 0%, 84%, 100% { opacity: .7; } 92% { opacity: 1; } }
@keyframes ve-motes   { from { transform: translateY(3px); opacity: .75; } to { transform: translateY(-5px); opacity: 1; } }

/* =========================================================================
   REVEAL ON SCROLL — the upgrade
   -------------------------------------------------------------------------
   styles.css fades and lifts. This adds the two things that make it read as
   deliberate rather than as a wipe: a hair of scale, and a blur that resolves
   as the element settles — the eye reads that as coming INTO focus, which is
   a different sensation from sliding in.

   The blur is on text and small cards only. A blur filter on a very large
   box is a full-size offscreen buffer every frame, and on a phone that is the
   one effect here that could actually cost a frame.
   ========================================================================= */

/* ⚠ transform-origin AT THE TOP EDGE, and it is not cosmetic. A scale
     applied about the centre moves the top of the element as well as the
     bottom, so a tall card grows upwards into whatever sits above it while
     it settles. The browser suite measures exactly that on the scent finder
     — the card's top must not move — and the centre origin moved it by four
     pixels. From the top edge, a reveal only ever grows downwards. */
.reveal { transform: translateY(22px) scale(.992); transform-origin: 50% 0%; }
.reveal.is-in {
  transition:
    opacity .85s var(--ease-soft),
    transform .8s var(--ease-enter),
    filter .85s var(--ease-soft);
}

.section-head.reveal,
.route.reveal,
.cat.reveal,
.finder.reveal { filter: blur(7px); }
.section-head.reveal.is-in,
.route.reveal.is-in,
.cat.reveal.is-in,
.finder.reveal.is-in { filter: blur(0); }

@media (prefers-reduced-motion: reduce) {
  .reveal { transform: none; filter: none; }
}

/* The rows INSIDE a revealed card arrive in reading order. site.js already
   sorts and staggers the cards themselves; this is for their contents, which
   are revealed as one unit and would otherwise land all at once.
   --i is written by motion.js. */
.is-in .steps .step,
.is-in .route__points .route__point {
  animation: ve-rise .7s var(--ease-enter) both;
  animation-delay: calc(var(--i, 0) * var(--stagger));
}

/* =========================================================================
   THE SCROLL PROGRESS LINE
   -------------------------------------------------------------------------
   Two pixels along the bottom edge of the header. It answers "how much of
   this is there" — the question every long single page raises and almost
   none of them answer — and it is her two brand colours, so it reads as
   trim rather than as a browser control.
   ========================================================================= */

.ve-progress {
  position: absolute; left: 0; right: 0; bottom: -1px;
  height: 2px;
  transform: scaleX(var(--p, 0));
  transform-origin: left;
  background: linear-gradient(90deg, var(--sage-soft), var(--sage-mid) 45%, var(--sage-deep));
  pointer-events: none;
  opacity: 0;
  transition: opacity .4s var(--ease-soft);
}
.header.is-stuck .ve-progress { opacity: 1; }

/* The bar tightens as you leave the top of the page. --header-h is measured
   while the header is at its TALLEST, so shrinking is the safe direction:
   every anchor keeps more clearance than it needs rather than less. */
.header__inner { transition: padding-block .4s var(--ease-soft); }
.header.is-stuck .header__inner { padding-block: .62rem; }
.brand__lockup, .brand__emblem { transition: height .4s var(--ease-soft); }

/* =========================================================================
   BUTTONS
   -------------------------------------------------------------------------
   A sheen that crosses on hover, a press that dips, and — on the two buttons
   that leave the site — a lean from the arrow that styles.css already draws.

   ⚠ The transform is rebuilt here around two custom properties so that the
     magnetic pull in motion.js can write to them without destroying the
     hover lift. An inline `style="transform:..."` would have done exactly
     that.
   ========================================================================= */

.btn {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  transform: translate3d(var(--bx, 0px), var(--by, 0px), 0);
}
.btn:hover  { transform: translate3d(var(--bx, 0px), calc(var(--by, 0px) - 2px), 0); }
.btn:active { transform: translate3d(var(--bx, 0px), 1px, 0); }

.btn::before {
  content: '';
  position: absolute; inset: 0;
  z-index: -1;
  background: linear-gradient(100deg,
    transparent 24%,
    color-mix(in srgb, var(--white) 34%, transparent) 46%,
    transparent 68%);
  transform: translateX(-120%);
  transition: transform .85s var(--ease-enter);
}
.btn:hover::before,
.btn:focus-visible::before { transform: translateX(120%); }

/* The ghost button's sheen has to be darker than its ground, not lighter. */
.btn--ghost::before {
  background: linear-gradient(100deg,
    transparent 24%,
    color-mix(in srgb, var(--sage) 26%, transparent) 46%,
    transparent 68%);
}

/* =========================================================================
   NAVIGATION
   ========================================================================= */

.nav__link { position: relative; }
.nav__link::after {
  content: '';
  position: absolute; left: 0; right: 0; bottom: .55rem;
  height: 1px; background: currentColor;
  transform: scaleX(0); transform-origin: right;
  transition: transform .4s var(--ease-enter);
}
.nav__link:hover::after,
.nav__link:focus-visible::after { transform: scaleX(1); transform-origin: left; }

/* The mobile panel deals its links out as it slides in. */
@media (max-width: 860px) {
  .nav.is-open > * { animation: ve-deal .5s var(--ease-enter) both; }
  .nav.is-open > :nth-child(1) { animation-delay: .10s; }
  .nav.is-open > :nth-child(2) { animation-delay: .16s; }
  .nav.is-open > :nth-child(3) { animation-delay: .22s; }
  .nav.is-open > :nth-child(4) { animation-delay: .28s; }
  .nav.is-open > :nth-child(5) { animation-delay: .34s; }
  .nav__link::after { display: none; }
}
@keyframes ve-deal {
  from { opacity: 0; transform: translateX(22px); }
  to   { opacity: 1; transform: none; }
}

/* =========================================================================
   CARDS THAT ANSWER THE CURSOR
   -------------------------------------------------------------------------
   A soft light follows the pointer across a card. motion.js writes --mx and
   --my; the card does the rest. Only where there IS a pointer — on a touch
   screen this is dead weight, and the media query is what stops it being
   painted at all.
   ========================================================================= */

@media (hover: hover) and (pointer: fine) {
  .route, .cat, .finder, .order__panel, .join { position: relative; }

  .route::after, .cat::after {
    content: '';
    position: absolute; inset: 0;
    border-radius: inherit;
    pointer-events: none;
    background: radial-gradient(18rem 18rem at var(--mx, 50%) var(--my, 50%),
      color-mix(in srgb, var(--sage) 28%, transparent), transparent 68%);
    opacity: 0;
    transition: opacity .45s var(--ease-soft);
  }
  .route:hover::after, .cat:hover::after { opacity: 1; }

  .route:hover, .cat:hover { transform: translateY(-4px); }
}

/* The little rule above each category title draws itself out on hover. */
.cat__rule { transition: width .45s var(--ease-enter), background-color .45s var(--ease-soft); }
.cat:hover .cat__rule { width: 3.4rem; background: var(--sage-mid); }

/* =========================================================================
   THE COMPARISON TABLE
   Rows light up under the cursor, so the eye can hold one line across four
   columns without a ruler.
   ========================================================================= */

.compare__table tbody tr { transition: background-color .3s var(--ease-soft); }
.compare__table tbody tr:hover { background: color-mix(in srgb, var(--sage-wash) 55%, transparent); }

/* =========================================================================
   THE RIBBON DIVIDER
   A slow shine passes along it, the way light crosses foil.
   ========================================================================= */

.divider__mark {
  overflow: hidden;
  animation: ve-ribbon-lift 7s var(--ease-drift) infinite alternate;
}
.divider__mark::after,
.divider__mark::before { z-index: 1; }

@keyframes ve-ribbon-lift {
  from { transform: translateY(0) rotate(0); }
  to   { transform: translateY(-2px) rotate(-1.5deg); }
}

/* =========================================================================
   THE SCENT FINDER
   -------------------------------------------------------------------------
   ⚠  NOTHING HERE MAY SCALE A CHIP OR PUSH IT DOWNWARDS.
      Two reasons, both learned the hard way. The browser suite measures every
      option's height to prove it is a 44px thumb target, and it measures
      shortly after the question is drawn — an entrance that scales from .96
      is measured mid-flight and fails. And the stage animates its own height
      to whatever the new question needs, measured from scrollHeight the
      instant the chips are written: a chip translated DOWN adds to that and
      the card ends a few pixels too tall.

      So the chips fade and drift UPWARDS by four pixels, which is clipped by
      the stage and counts for nothing.
   ========================================================================= */

.finder__opts .chip { animation: ve-chip .55s var(--ease-enter) both; }
.finder__opts .chip:nth-child(1) { animation-delay: .02s; }
.finder__opts .chip:nth-child(2) { animation-delay: .07s; }
.finder__opts .chip:nth-child(3) { animation-delay: .12s; }
.finder__opts .chip:nth-child(4) { animation-delay: .17s; }
.finder__opts .chip:nth-child(5) { animation-delay: .22s; }
.finder__opts .chip:nth-child(6) { animation-delay: .27s; }
.finder__opts .chip:nth-child(7) { animation-delay: .32s; }
.finder__opts .chip:nth-child(8) { animation-delay: .37s; }

@keyframes ve-chip {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}

.finder__q, .finder__count, .finder__hint {
  animation: ve-rise .6s var(--ease-enter) both;
}

/* The answer arrives as a sentence being written rather than a box appearing. */
.finder__result-label { animation: ve-rise .6s var(--ease-enter) .05s both; }
.finder__quote {
  animation: ve-quote .9s var(--ease-enter) .12s both;
  position: relative;
  overflow: hidden;
}
.finder__quote::after {
  content: '';
  position: absolute; inset: 0;
  background: linear-gradient(100deg, transparent 30%,
    color-mix(in srgb, var(--white) 70%, transparent) 50%, transparent 70%);
  transform: translateX(-130%);
  animation: ve-sweep-once 1.5s var(--ease-enter) .5s both;
}
.finder__result-actions { animation: ve-rise .7s var(--ease-enter) .3s both; }

@keyframes ve-quote {
  from { opacity: 0; transform: translateY(12px); clip-path: inset(0 100% 0 0); }
  to   { opacity: 1; transform: none;             clip-path: inset(0 0 0 0); }
}
@keyframes ve-sweep-once {
  from { transform: translateX(-130%); }
  to   { transform: translateX(130%); }
}

/* The progress dots gain a soft glow on the one you are on, so the row reads
   as a position rather than as four identical marks. */
.finder__dot.is-now::after { box-shadow: 0 0 10px color-mix(in srgb, var(--sage-mid) 70%, transparent); }

/* =========================================================================
   THE ORDER FORM
   ========================================================================= */

.field input, .field select, .field textarea {
  transition: border-color .25s var(--ease-soft), background-color .25s var(--ease-soft),
              box-shadow .3s var(--ease-soft);
}
.field input:focus, .field select:focus, .field textarea:focus {
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--sage) 22%, transparent);
}

/* A label lifts a fraction while its own field has focus — the smallest
   possible way of saying "this one". */
.field:focus-within > label { color: var(--sage-deep); }
.field > label { transition: color .25s var(--ease-soft); }

/* The live preview pulses when it changes, so it is obvious that the thing
   on the left is the thing being typed on the right. Driven by motion.js
   watching the bubble, not by the form — the form should not have to know. */
.preview__bubble { transition: box-shadow .5s var(--ease-soft), background-color .5s var(--ease-soft); }
.preview__bubble.is-live {
  background: color-mix(in srgb, var(--sage-soft) 55%, var(--sage-wash));
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--sage) 25%, transparent);
}

.step__n { transition: transform .35s var(--ease-enter), box-shadow .35s var(--ease-soft); }
.steps:hover .step__n { box-shadow: 0 0 0 4px color-mix(in srgb, var(--sage) 16%, transparent); }

/* =========================================================================
   JOIN — THE REGISTRATION PANEL
   -------------------------------------------------------------------------
   This is the loudest thing on the page after the hero, and that is a
   deliberate change: it is the one route a customer cannot find any other
   way, and in the first build it was a flat green box that people scrolled
   straight past.

   ⚠  IT IS STILL NOT AN ORDER ROUTE, AND IT MUST NOT LOOK LIKE ONE.
      It stays well below the two order options and its own copy says, in
      plain words, that nobody has to do it. Loud is fine. Compulsory is not,
      because it is not true — and a shop that seems to want you signed up
      before you may buy loses the sale.
   ========================================================================= */

/* ⚠ The plain sheet's big white disc is dialled back rather than removed.
     It was drawn to keep a flat panel from being a flat rectangle; now that
     two lights drift behind the same panel, the disc's hard edge cuts
     visibly across the glass card — so it stays as a wash and the aura does
     the work. Delete this sheet and the disc comes back, which is correct:
     without the aura the panel needs it. */
.join::after { opacity: .5; filter: blur(34px); }

.join {
  isolation: isolate;
  background:
    radial-gradient(120% 140% at 12% 0%, color-mix(in srgb, var(--sage-mid) 55%, transparent), transparent 60%),
    var(--sage-deep);
}

/* Two slow lights moving behind the panel. Contained by the panel's own
   overflow: hidden, and sized in percentages so they cannot widen the page. */
.join__aura {
  position: absolute; inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(30% 40% at 20% 30%, color-mix(in srgb, var(--sage-soft) 40%, transparent), transparent 70%),
    radial-gradient(26% 34% at 82% 72%, color-mix(in srgb, var(--silver-pale) 34%, transparent), transparent 70%);
  animation: ve-aura 22s var(--ease-drift) infinite alternate;
}

/* A hairline of light along the top edge, travelling. It is the detail that
   makes the panel read as an object rather than as a coloured rectangle. */
.join::before {
  content: '';
  position: absolute; top: 0; left: 0; right: 0; height: 1px;
  background: linear-gradient(90deg,
    transparent, color-mix(in srgb, var(--white) 75%, transparent), transparent);
  background-size: 40% 100%;
  background-repeat: no-repeat;
  animation: ve-edge 9s linear infinite;
  pointer-events: none;
}
@keyframes ve-edge {
  from { background-position: -45% 0; }
  to   { background-position: 145% 0; }
}

/* The card lifts a little towards a cursor, and the seal on it turns very
   slowly. Everything else about this card — its layout, its border, its type
   — is in styles.css, because a panel carrying her only registration link
   must not fall apart if this sheet is deleted. */
.join__card { transition: transform .5s var(--ease-enter), border-color .4s var(--ease-soft); }
.join__card:hover {
  transform: translateY(-4px);
  border-color: color-mix(in srgb, var(--white) 48%, transparent);
}
.join__seal { animation: ve-seal 8s var(--ease-drift) infinite alternate; }
@keyframes ve-seal {
  from { transform: rotate(-3deg) scale(1); }
  to   { transform: rotate(3deg) scale(1.04); }
}


/* The one button on the page that catches the eye by itself. Once every ten
   seconds, briefly — enough to be noticed on a slow scroll and not enough to
   be a flashing advert. Reduced motion clamps it to a single pass. */
.join .btn--lg::before {
  animation: ve-pulse 10s var(--ease-drift) 4s infinite;
}
@keyframes ve-pulse {
  0%, 74%, 100% { transform: translateX(-120%); }
  86%           { transform: translateX(120%); }
}

/* =========================================================================
   FAQ
   -------------------------------------------------------------------------
   The answer opens by growing rather than appearing. site.js does the
   measuring (it has to — `hidden` and `display:none` cannot be transitioned);
   this is only the transition it drives.
   ========================================================================= */

.faq__item { transition: background-color .35s var(--ease-soft); }
.faq__q { transition: color .3s var(--ease-soft); }
.faq__q:hover { color: var(--sage-deep); }
.faq__q[aria-expanded="true"] { color: var(--sage-deep); }

.faq__wrap { overflow: hidden; }
.faq__wrap.is-animating { transition: height .38s var(--ease-enter); }
@media (prefers-reduced-motion: reduce) {
  .faq__wrap.is-animating { transition: none; }
}
.faq__q[aria-expanded="true"] + .faq__wrap .faq__a { animation: ve-rise .5s var(--ease-enter) .04s both; }

/* =========================================================================
   BACK TO TOP — with the page's progress drawn round it
   ========================================================================= */

/* ⚠ FLEX, NOT GRID, AND ONLY BECAUSE OF THE RING.
     styles.css centres the arrow with `display: grid; place-items: center`,
     which is right — but an absolutely positioned child of a GRID CONTAINER
     is laid out against its GRID AREA, not against the padding box. The area
     here is one implicit cell sized to a 17px arrow, so `width: 100% + 8px`
     resolved against seventeen pixels and drew the ring as a bead in the
     corner. Flex centring is identical to look at and gives the ring the
     containing block it expects. */
.to-top {
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: visible;
}
/* ⚠ SIZED EXPLICITLY. An <svg> is a REPLACED element: give it four insets
     and `width: auto` and it does not stretch to them the way a div would,
     it falls back to its intrinsic size — which drew the ring as a thumbnail
     in the corner of the button rather than around it. */
/* ⚠ `.to-top .to-top__ring`, NOT `.to-top__ring`. styles.css already says
     `.to-top svg { width: 1.1rem }` for the arrow, and that selector matches
     this ring too — with one class and one type it outweighs a single class,
     so the ring was being drawn at the size of the arrow. */
.to-top .to-top__ring {
  position: absolute;
  left: -4px; top: -4px;
  width: calc(100% + 8px); height: calc(100% + 8px);
  pointer-events: none;
  transform: rotate(-90deg);
  overflow: visible;
}
.to-top .to-top__ring circle {
  fill: none;
  stroke: var(--sage-mid);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-dasharray: var(--c, 100);
  stroke-dashoffset: calc(var(--c, 100) - var(--c, 100) * var(--p, 0));
  transition: stroke-dashoffset .15s linear;
}

/* =========================================================================
   SECTION SEAMS
   -------------------------------------------------------------------------
   Where two grounds meet, a one-pixel gradient rather than a hard edge. It
   is the difference between stacked blocks and a page that flows.
   ========================================================================= */

.bg-white, .bg-sage { position: relative; }
.bg-white::before, .bg-sage::before {
  content: '';
  position: absolute; left: 0; right: 0; top: 0; height: 1px;
  background: linear-gradient(90deg, transparent, var(--sage-soft) 30%, var(--sage-soft) 70%, transparent);
  pointer-events: none;
}

/* =========================================================================
   FOCUS, STILL VISIBLE THROUGH ALL OF IT
   -------------------------------------------------------------------------
   Several things above set overflow: hidden or a transform, both of which
   can clip an outline drawn outside the box. The ring is redrawn as a shadow
   for exactly those elements, so a keyboard user never loses their place.
   ========================================================================= */

.btn:focus-visible {
  outline: 2px solid var(--sage-deep);
  outline-offset: 3px;
  box-shadow: 0 0 0 5px color-mix(in srgb, var(--sage) 25%, transparent);
}
.join .btn:focus-visible { outline-color: var(--white); }

/* =========================================================================
   PRINT AND NO-JAVASCRIPT
   Nothing here should survive into either. The entrances in particular start
   at opacity 0, and a printed page cannot wait for an animation.
   ========================================================================= */

@media print {
  .ve-grain, .hero__aura, .join__aura, .ve-progress, .to-top__ring { display: none !important; }
  .hero .eyebrow, .hero__title > span, .hero__intro, .hero__actions, .hero__figure,
  .finder__quote, .finder__q, .reveal {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
    clip-path: none !important;
  }
}

.no-js .hero .eyebrow,
.no-js .hero__title > span,
.no-js .hero__intro,
.no-js .hero__actions,
.no-js .hero__figure,
.no-js .finder__opts .chip,
.no-js .faq__a {
  animation: none !important;
  opacity: 1 !important;
  transform: none !important;
  clip-path: none !important;
}
.no-js .reveal { filter: none !important; }
