/* ============================================================================
   StellarKnot — the motion layer                                  (v1.5.0)

   All of the storefront's movement lives in this one file, so it can be read,
   tuned, or deleted without going near the design system in style.css.

   Three rules hold throughout:

     1. Only `opacity` and `transform` are animated. Animating layout
        properties — width, height, top, margin — forces the browser to
        re-lay-out the page on every frame. That is what makes an animated site
        feel heavy and why cheap animation is easy to spot.

     2. Every effect here is an enhancement, never a requirement. If JavaScript
        fails to load, or the visitor has asked their device to reduce motion,
        the page renders exactly as it did before — nothing is left hidden
        waiting for a script to come back.

     3. Restrained. Short distances, long easing, no bounce, no spin, no
        parallax you would notice on purpose. If a visitor can name the effect,
        it is too loud for this brand.

   The distances, durations and delays are all set below. Change them here.
   ========================================================================= */

:root {
  --m-rise:     22px;   /* how far a thing travels as it appears             */
  --m-lift:     -6px;   /* how far a product card lifts under the pointer    */
  --m-dur-s:    0.45s;  /* pointer feedback — must feel immediate           */
  --m-dur:      0.85s;  /* the standard reveal                               */
  --m-dur-l:    1.15s;  /* photographs — slow enough to feel like film       */
  --m-stagger:  90ms;   /* the gap between neighbours in a group             */
  --m-parallax: 24px;   /* total vertical drift of a photograph on scroll    */
}

/* ==========================================================================
   1. Scroll reveal

   functions.php marks the document `js` in the <head>, before anything is
   painted (sk_js_flag). That is what lets these elements be hidden here
   without a flash of content first. With JavaScript off, `.js` is never added,
   none of this applies, and the page is simply the page.

   `:where()` keeps this selector's specificity at zero, so the single
   `.is-in` rule below always wins without a pile of !important.

   `.reveal` is kept in the list: it is the hook the original theme used on the
   category tiles and the editorial band, and anything still carrying it keeps
   working.
   ========================================================================== */

.js :where(
  .reveal,
  .phead,                 /* shop / search / collection page headers          */
  .sec-head,              /* homepage section headers                         */
  .phead__title,
  .sec-head__title,
  .pgrid > *,             /* every product card, in every grid                */
  .duo > *,               /* the two-up FOCUS row                             */
  .rail > *,
  .catstrip__item,        /* the three collection tiles                       */
  .usp__item,             /* the four assurances                              */
  .shop__bar,             /* the sort bar on the shop page                    */
  .band,                  /* the editorial band                               */
  .ctaband,               /* the closing WhatsApp band                        */
  .empty,
  .pdp > *                /* the product page's own blocks                    */
) {
  opacity: 0;
  transform: translate3d(0, var(--m-rise), 0);
  transition:
    opacity var(--m-dur) var(--ease),
    transform var(--m-dur) var(--ease);
  transition-delay: var(--m-delay, 0ms);
}

.js .is-in {
  opacity: 1;
  transform: none;
}

/* Photographs take longer and travel a little further than type — the eye
   expects weight to move more slowly than ink. */
.js :where(.catstrip__item, .band, .pdp > *) {
  transition-duration: var(--m-dur-l);
}

/* The stagger delay is only ever wanted on the way in. motion.js clears
   --m-delay once an item has landed, so a card cannot sit there hesitating
   under the pointer because it was third in its group. */
.js :where(.pcard, .catstrip__item, .usp__item).is-in {
  transition-delay: var(--m-delay, 0ms);
}

/* ==========================================================================
   2. The page arriving, and the hero

   These are CSS animations rather than transitions on purpose: an animation
   always runs to completion and settles on its final state, so even if the
   script never arrives the hero is fully visible. Nothing here can strand the
   first screen in a half-faded state.
   ========================================================================== */

@keyframes m-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes m-rise-in {
  from { opacity: 0; transform: translate3d(0, 26px, 0); }
  to   { opacity: 1; transform: none; }
}

@keyframes m-settle {
  from { opacity: 0; transform: scale(1.045); }
  to   { opacity: 1; transform: none; }
}

/* A short settle on the whole page, so an internal link feels like one
   continuous place rather than a fresh document. */
.js #content {
  animation: m-in 0.34s var(--ease) both;
}

/* The hero's five pieces arrive in reading order. `backwards` rather than
   `both`: with `both`, the animation's final state keeps being applied for the
   life of the page, which leaves every one of these elements holding a
   transform of `matrix(1,0,0,1,0,0)` — harmless, but noise in the box model and
   a containing block it has no business creating. With `backwards` the
   pre-delay state is held, then the element simply returns to its own style. */
.js .hero__text > * {
  animation: m-rise-in 1s var(--ease) backwards;
}
.js .hero__text > :nth-child(1) { animation-delay: 0.06s; }  /* eyebrow   */
.js .hero__text > :nth-child(2) { animation-delay: 0.14s; }  /* headline  */
.js .hero__text > :nth-child(3) { animation-delay: 0.24s; }  /* lead      */
.js .hero__text > :nth-child(4) { animation-delay: 0.34s; }  /* buttons   */
.js .hero__text > :nth-child(5) { animation-delay: 0.44s; }  /* the stats */

.js .hero__media {
  animation: m-settle 1.3s var(--ease) backwards;
  animation-delay: 0.1s;
}

/* ==========================================================================
   3. Parallax

   Only the photograph inside a fixed frame drifts. motion.js writes --m-y on
   scroll; the frame itself never moves, so nothing below it is ever pushed
   around and no layout is recalculated while the visitor scrolls.

   The images are scaled up very slightly (their frames already clip with
   `overflow: hidden`), which is what gives the drift somewhere to travel to
   without exposing the edge of the picture.
   ========================================================================== */

.js .hero__media img,
.js .band__media img {
  transform: translate3d(0, var(--m-y, 0px), 0) scale(1.08);
  will-change: transform;
}

/* ==========================================================================
   4. Pointer feedback

   Guarded by `hover: hover` so a touch device never gets a style it cannot
   undo by moving the pointer away.
   ========================================================================== */

@media (hover: hover) and (pointer: fine) {
  .js .pcard,
  .js .catstrip__item {
    transition: transform var(--m-dur-s) var(--ease);
  }
  .js .pcard:hover,
  .js .catstrip__item:hover {
    transform: translate3d(0, var(--m-lift), 0);
  }

  /* The gold hairline under a product's name, drawn in from the left. */
  .js .pcard__name a {
    background-image: linear-gradient(var(--gold), var(--gold));
    background-repeat: no-repeat;
    background-position: 0 100%;
    background-size: 0% 1px;
    transition: background-size var(--m-dur-s) var(--ease), color var(--dur-fast) var(--ease);
  }
  .js .pcard:hover .pcard__name a {
    background-size: 100% 1px;
  }

  /* The ticker is decorative and `aria-hidden`; letting it pause while the
     pointer rests on it costs nothing and lets an offer be read. */
  .tickerwrap:hover .ticker {
    animation-play-state: paused;
  }
}

/* ==========================================================================
   5. Cart feedback

   motion.js watches the cart count and adds this when it changes, so a visitor
   who has just added something sees the badge acknowledge it.
   ========================================================================== */

@keyframes m-badge {
  0%   { transform: scale(1); }
  38%  { transform: scale(1.34); }
  100% { transform: scale(1); }
}

.badge.is-pulse {
  animation: m-badge 0.52s var(--ease);
}

/* ==========================================================================
   6. Reduce motion

   The visitor has said what they want. Everything above is switched off and
   every element is forced to its finished state — not merely sped up, which
   is what a global duration override alone would do.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  .js :where(
    .reveal, .phead, .sec-head, .phead__title, .sec-head__title,
    .pgrid > *, .duo > *, .rail > *, .catstrip__item, .usp__item,
    .shop__bar, .band, .ctaband, .empty, .pdp > *
  ) {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
    transition-delay: 0ms !important;
  }

  .js #content,
  .js .hero__text > *,
  .js .hero__media {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
    animation-delay: 0s !important;
  }

  .js .hero__media img,
  .js .band__media img {
    transform: none !important;
    will-change: auto;
  }

  .badge.is-pulse {
    animation: none !important;
  }

  /* The scrolled collection strip falls back to the ordinary reveal. */
  .js .catstrip--scrub .catstrip__item {
    --m-tile-y: 0px !important;
    --m-tile-op: 1 !important;
  }
}

/* ==========================================================================
   7. The scroll-scrubbed hero — home page only

   A thread that spins itself into the FOCUS tee as the visitor scrolls. The
   frames in assets/seq/focus are drawn to a canvas and the frame index is driven
   by scroll position, so the scrollbar becomes the timeline — the same mechanism
   as the reference the owner picked out.

   The section is deliberately tall and its contents are pinned for the duration.
   Every rule below is scoped to `.js`, so with JavaScript off none of it applies:
   the hero renders at its ordinary height with its ordinary photograph, and
   nothing is left as two and a half screens of empty space.
   ========================================================================== */

.js .hero-scrub {
  position: relative;
  min-height: 240vh;          /* how long the scrub lasts; tune to taste */
}

.js .hero-scrub__pin {
  position: sticky;
  top: 0;
  height: 100vh;
  display: flex;
  align-items: center;
  overflow: hidden;
}

/* The hero keeps its own grid and spacing rhythm; only the vertical padding is
   tightened so it fits the pinned viewport with room for the sticky header. */
.js .hero-scrub .hero {
  width: 100%;
  min-height: auto;
  padding-block: calc(var(--nav-h) + var(--s4)) var(--s5);
}

/* The canvas sits over the photograph. The photograph is never removed — it
   stays until the frames have genuinely arrived, so the panel is never blank
   and the largest-contentful-paint is never an empty box. */
.js .hero__media > img {
  transition: opacity 0.6s var(--ease);
}
.js .hero__seq {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.7s var(--ease);
}
.js .hero-scrub.is-ready .hero__seq { opacity: 1; }
.js .hero-scrub.is-ready .hero__media > img { opacity: 0; }

/* The canvas owns the stage during act 1: the photograph underneath stays
   dark until the frames cross-dissolve in, and the section's own ink
   background is the thread show's plate. */
.js .hero-scrub .hero__media { background: var(--ink); }
.js .hero-scrub.is-ready .hero__media > img { opacity: 0; }

/* Real progress: the bar tracks frames actually decoded, not a timer. */
.js .hero__seq-meter {
  position: absolute;
  top: var(--s4);
  left: var(--s4);
  right: var(--s4);
  height: 2px;
  background: var(--line-mid);
  overflow: hidden;
  transition: opacity 0.5s var(--ease);
}
.js .hero__seq-meter i {
  display: block;
  height: 100%;
  width: 0;
  background: var(--gold);
  transition: width 0.25s linear;
}
.js .hero-scrub.is-ready .hero__seq-meter { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  /* Collapse the pin. A visitor who asked for less motion should not have to
     scroll through two and a half screens of it. */
  .js .hero-scrub { min-height: auto; }
  .js .hero-scrub__pin { position: static; height: auto; overflow: visible; }
  .js .hero__seq,
  .js .hero__seq-meter { display: none !important; }
}

/* ==========================================================================
   8. The full-screen hero — landscape desktop only

   On a wide screen the sequence stops living inside the editorial column and
   takes the whole viewport: the pinned panel becomes a full-bleed stage, the
   wide frames fill it, and the text sits over the lower left on a soft scrim.
   On a narrow or portrait screen none of this applies and the ordinary
   two-column hero stands, running on the portrait frames.

   The condition here — 961px and landscape — is the same one motion.js uses
   to pick which frame set to load. Keep them identical; the layout and the
   footage always arrive together.

   Everything is scoped to `.js`: with JavaScript off this is the classic hero
   with its classic photograph, exactly as before.
   ========================================================================== */

/* The wide still only ever shows in the full-screen layout. */
.hero__wide-fallback { display: none; }

@media (min-width: 961px) and (orientation: landscape) {

  /* The stage. `.hero` is also `.shell`, which already clips nothing and
     carries the gutters; the media fills it, the text flows to the bottom. */
  /* The entrance settle (rule 2) scales the media panel by 4.5% and fades
     it in. On a column that reads as film; on a full-bleed stage it would
     crop the frame's edges for 1.3 s. The sequence arriving IS the entrance
     here, so the panel holds still. Specificity 0,2,1 beats the 0,1,1. */
  .js .hero-scrub--wide .hero__media {
    animation: none;
  }

  .js .hero-scrub--wide .hero {
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    min-height: 100vh;
    padding-block: calc(var(--nav-h) + var(--s9)) var(--s8);
  }

  /* The picture, the canvas and the wide still all fill the panel. */
  .js .hero-scrub--wide .hero__media {
    position: absolute;
    inset: 0;
    aspect-ratio: auto;
    border-radius: 0;
    background: var(--ink);
    z-index: 0;
  }
  .js .hero-scrub--wide .hero__wide-fallback {
    display: block;
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    opacity: 1;
    transition: opacity 0.6s var(--ease);
  }
  /* The ordinary portrait photograph never shows in this layout: the wide
     still is the picture underneath the sequence. Specificity 0,3,1 beats
     the parallax rule's 0,1,1, which would otherwise leave the drift's
     transform on a picture nobody sees. */
  .js .hero-scrub--wide .hero__media > img:not(.hero__wide-fallback) {
    opacity: 0;
    transform: none;
    will-change: auto;
  }

  /* The wide still must carry no transform either: the parallax rule (3)
     scales every hero image by 1.08 and drifts it, which would break the
     full-bleed inset here. It never parallaxes — the scrub is the movement. */
  .js .hero-scrub--wide .hero__media > img.hero__wide-fallback {
    transform: none;
    will-change: auto;
  }

  /* The caption belongs to the photograph in the column; on a full-screen
     stage the headline is the caption. */
  .js .hero-scrub--wide .hero__caption { display: none; }

  /* A scrim at the foot so type holds over any frame of the sequence. */
  .js .hero-scrub--wide .hero::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 62%;
    background: linear-gradient(to top,
      rgba(10, 9, 8, 0.80),
      rgba(10, 9, 8, 0.42) 42%,
      rgba(10, 9, 8, 0) 78%);
    pointer-events: none;
    z-index: 1;
  }

  /* The text rides above the scrim. */
  .js .hero-scrub--wide .hero__text {
    position: relative;
    z-index: 2;
  }

  /* The load meter rides just under the fixed header in this layout, so it
     is never hidden behind it while the frames arrive. */
  .js .hero-scrub--wide .hero__seq-meter {
    top: calc(var(--nav-h) + var(--s3));
  }

  /* Type on photography keeps its dark treatment in BOTH themes — the same
     rule the hero caption already follows. Redefining the palette variables
     on the text block is enough: everything inside resolves through them.
     Note `html.js[data-theme='light']` as ONE element: the `js` class and
     the theme attribute both live on <html>, so a descendant selector
     between them can never match. */
  html.js[data-theme='light'] .hero-scrub--wide .hero__text {
    --cream:      #F2EEE7;
    --cream-dim:  #C4BEB2;
    --muted:      #8B8579;
    --gold:       #C9A84C;
    --line:       rgba(242, 238, 231, 0.10);
    --line-strong: rgba(242, 238, 231, 0.26);
    color: var(--cream);
  }
  html.js[data-theme='light'] .hero-scrub--wide .hero__text .hero__title {
    color: var(--cream);
  }
  /* The outline button on the dark plate: cream letters, cream hairline. */
  html.js[data-theme='light'] .hero-scrub--wide .hero__text .btn:not(.btn--gold) {
    --btn-fg: #F2EEE7;
    --btn-bd: rgba(242, 238, 231, 0.26);
    --btn-bg-hover: #FFFFFF;
    --btn-fg-hover: #16140F;
    --btn-bd-hover: #FFFFFF;
  }
}

/* ==========================================================================
   9. The collection strip on the same rail

   The hero's scrub ends on the woven tee; the tiles directly below pick up
   the same instrument — motion.js drives --m-tile-y / --m-tile-op from the
   strip's own scroll progress (damped, like the hero) and these rules just
   read them. Without JS the variables are never written and the ordinary
   reveal stands.
   ========================================================================== */

.js .catstrip--scrub .catstrip__item {
  opacity: var(--m-tile-op, 1);
  transform: translate3d(0, var(--m-tile-y, 0px), 0);
  will-change: opacity, transform;
}
