/* ==========================================================================
   MUSAFIR CAFE — effects.css
   Keyframes, transitions and scroll behaviour ONLY. Every colour, size,
   font and layout decision lives in style.css; every media query lives in
   responsive.css. Nothing here declares a design token or positions a box.
   --------------------------------------------------------------------------

   THE ONE CURVE
   cubic-bezier(.22, .61, .36, 1) — a calm ease-out, and the ONLY curve in
   this file. The sibling cafe (Chaska) runs an overshoot curve; this site
   must NEVER overshoot. Nothing here may pass its target value and come
   back. The restraint IS the identity: a hill station at 2,005 m does not
   bounce.

   THE FOUR THINGS THAT MOVE — and there is no fifth
     1. THE RISE       One scroll entrance: a 16px lift and fade on .reveal,
                       staggered between siblings by --i. Armed only once JS
                       is present (html.is-js), so a scripting-off visitor
                       gets a finished page, never a blank one.
     2. THE MIST       (First-timer) The fog wash over the ridge thins as the
                       hero scrolls away. animations.js writes ONE custom
                       property — --mist-density, the one style.css already
                       turns into .mist's opacity — and the transition below
                       glides between the observer's steps.
     3. THE VIEW DRIFT The valley photograph shifts a few pixels behind the
                       mullions, which never move: they belong to the
                       building, not to the landscape. Written as a pure CSS
                       scroll-driven animation on --ws-parallax, the property
                       style.css reserved for exactly this. No scroll
                       listener exists on this site.
     4. THE ANSWER     Pointer and press feedback on cards, links, buttons,
                       .board-item and .walk-item. 2px and 3.5%, never more,
                       and never a downward shadow — this site owns only the
                       inverted lift, which belongs to the sill and the bar.

   THE SEAM WITH style.css (do not duplicate these — they are already there)
     * .mist declares --mist-density: .16 as its DEFAULT and reads it as
       opacity. That default IS the cleared, final state. This file only
       transitions the property; it never re-declares the opacity, except in
       the reduced-motion block, where doing so is the whole point.
     * .ws-view img declares --ws-parallax: 0px and consumes it inside its
       transform, together with scale(1.08). That 8% is the bleed the drift
       eats into, so no edge can ever be exposed.
     * .walk-strip already carries scroll-snap-type and overscroll-behavior.
       Only scroll-behavior — which is motion, and must switch off under
       reduced motion — is added here.

   RULES HELD
     * transform, opacity and two registered custom properties. Nothing here
       animates width, height, padding, margin or a box-shadow.
     * no infinite animation, no marquee — that belongs to the other cafe.
     * no will-change beyond the one style.css already sets on .ws-view img.
     * no !important, anywhere, for any reason.
     * prefers-reduced-motion: reduce removes EVERY animation by name and
       pins the mist with a plain opacity declaration, which beats any
       custom property animations.js may have written as an inline style.
   ========================================================================== */


/* ==========================================================================
   1. MOTION-ONLY VARIABLES
      Namespaced --fx-* so they can never be mistaken for design tokens.
      --fx-mist-clear / --fx-mist-full MIRROR style.css: .mist's default
      (.16) and .mist.is-thick (.88). animations.js reads both from here, so
      the fog has exactly one source of truth and no magic number in the JS.
   ========================================================================== */

:root {
  --fx-ease:       cubic-bezier(.22,.61,.36,1);  /* THE one curve */

  --fx-rise:       16px;   /* how far a revealed block travels        */
  --fx-in:         .55s;   /* entrance duration                       */
  --fx-step:       80ms;   /* stagger between revealed siblings       */

  --fx-quick:      .2s;    /* press and colour feedback               */
  --fx-hover:      .45s;   /* pointer response on media               */

  --fx-mist:       .32s;   /* glide between mist observer steps       */
  --fx-mist-clear: .16;    /* RESTING value — matches .mist default   */
  --fx-mist-full:  .88;    /* thickest — matches .mist.is-thick       */

  --fx-drift:      14px;   /* half the view's total parallax travel   */
}


/* ==========================================================================
   2. THE RISE — the one scroll entrance
      animations.js observes every .reveal, adds .is-in, and sets --i on
      revealed siblings so the stagger is data the stylesheet cannot know.

      THE GATE IS .is-js, AND THAT NAME IS LOAD-BEARING. header.php puts
      `is-js` on <html> from an inline script in <head>, before first paint,
      and responsive.css keys the no-JS navigation off the same class. One
      name, one meaning across all three files: "scripting is running".

      With scripting off the class never lands, none of these selectors
      match, and every .reveal renders normally — which is why style.css
      deliberately declares nothing for .reveal and left the hidden state
      here. Two independent failsafes cover the case where the class lands
      but the motion code does not: animations.js reveals everything on any
      bail-out or thrown error, and main.js settles anything still hidden
      after load if animations.js never reported in.
   ========================================================================== */

.is-js .reveal {
  --fx-delay: calc(var(--i, 0) * var(--fx-step));
  opacity: 0;
  transform: translate3d(0, var(--fx-rise), 0);
  transition:
    opacity   var(--fx-in) var(--fx-ease) var(--fx-delay),
    transform var(--fx-in) var(--fx-ease) var(--fx-delay);
}

.is-js .reveal.is-in {
  opacity: 1;
  transform: translate3d(0, 0, 0);
}


/* ==========================================================================
   3. THE MIST — First-timer: the scroll-thinning fog
      ONE property carries the whole effect, and style.css already reads it
      as .mist's opacity. All this file adds is the glide.

      @property is what makes it animatable: an unregistered custom property
      is an unparsed string and cannot be transitioned. Where @property is
      unsupported the block is dropped, the transition is ignored, and
      animations.js still writes the value — the fog then steps between
      observer thresholds instead of gliding. Nothing breaks; it is only
      less smooth.

      initial-value must stay in step with .mist's own default in style.css
      (@property will not accept a var() there). Cleared is the resting
      state, so with no script at all the hero shows the faint standing haze
      and stays readable — never fogged out.
   ========================================================================== */

@property --mist-density {
  syntax: "<number>";
  inherits: false;
  initial-value: 0.16;
}

.mist,
.window-seat .mist {
  transition: --mist-density var(--fx-mist) var(--fx-ease);
}


/* ==========================================================================
   4. THE VIEW DRIFT — the photograph moves, the window does not
      A scroll-driven animation, so it costs the main thread nothing and
      there is no scroll listener anywhere on this site. It is mapped to the
      hero's EXIT: still at rest while the hero is the page, drifting only
      as it leaves.

      Double-gated on @supports and on reduced motion. An unsupporting
      browser gets a still photograph, which is the correct fallback for a
      window.
   ========================================================================== */

@property --ws-parallax {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}

@keyframes ws-drift {
  from { --ws-parallax: calc(var(--fx-drift) * -1); }
  to   { --ws-parallax: var(--fx-drift); }
}

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .window-seat .ws-view img {
      animation: ws-drift linear both;
      animation-timeline: view();
      animation-range: exit 0% exit 100%;
    }
  }
}


/* ==========================================================================
   5. THE ANSWER — pointer and press feedback
      Quiet by design: a 2px nudge or a 3.5% scale, and nothing else. Every
      hover is paired with :focus-within so a keyboard reaches the same
      state, and the transforms are guarded by (hover: hover) so a touch
      screen never sticks in a hover state after a tap.
   ========================================================================== */

/* Media settles inside its frame; the frame itself holds still.
   .card-media and .card already clip in style.css. */
.card-media img,
.card-media picture,
.walk-item img,
.board-item img {
  transition: transform var(--fx-hover) var(--fx-ease);
}

/* Whole-element nudges, plus the border answer style.css defines on .card. */
.card,
.board-item,
.walk-item {
  transition:
    transform    var(--fx-hover) var(--fx-ease),
    border-color var(--fx-quick) var(--fx-ease);
}

@media (hover: hover) and (pointer: fine) {
  .card:hover .card-media img,
  .card:focus-within .card-media img,
  .card:hover .card-media picture,
  .card:focus-within .card-media picture {
    transform: scale(1.035);
  }

  .board-item:hover,
  .board-item:focus-within,
  .walk-item:hover,
  .walk-item:focus-within {
    transform: translate3d(0, -2px, 0);
  }

  .board-item:hover img,
  .board-item:focus-within img,
  .walk-item:hover img,
  .walk-item:focus-within img {
    transform: scale(1.035);
  }
}

/* Links, buttons, chips and inputs: colour, and a 1px press. */
.brand,
.nav-link,
.nav-toggle,
.btn,
.chip,
.status-chip,
.card-title,
.footer-col a,
.milestone-row a,
.mobile-bar a,
.field-input {
  transition:
    color            var(--fx-quick) var(--fx-ease),
    background-color var(--fx-quick) var(--fx-ease),
    border-color     var(--fx-quick) var(--fx-ease),
    opacity          var(--fx-quick) var(--fx-ease),
    transform        var(--fx-quick) var(--fx-ease);
}

.btn:active,
.nav-toggle:active,
a.chip:active,
button.chip:active {
  transform: translate3d(0, 1px, 0);
}

/* Prose links: the underline thickens rather than the colour flashing. */
p a,
li a,
address a,
dd a {
  transition:
    color                       var(--fx-quick) var(--fx-ease),
    text-decoration-thickness   var(--fx-quick) var(--fx-ease),
    text-underline-offset       var(--fx-quick) var(--fx-ease);
}

p a:hover,
li a:hover,
address a:hover,
dd a:hover {
  text-decoration-thickness: 2px;
  text-underline-offset: .22em;
}

/* The milestone only answers a pointer where it is inside a link. */
a .milestone {
  transition: transform var(--fx-hover) var(--fx-ease);
}

@media (hover: hover) and (pointer: fine) {
  a:hover .milestone,
  a:focus-visible .milestone {
    transform: translate3d(0, -2px, 0);
  }
}


/* ==========================================================================
   6. SCROLL BEHAVIOUR
      Motion, therefore this file. scroll-padding-top keeps an in-page
      anchor from landing underneath the sticky header. scroll-snap itself
      is layout and already lives in style.css — only the glide is added.
   ========================================================================== */

html {
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--header-h, 4rem) + var(--space-sm, 1rem));
}

.walk-strip {
  scroll-behavior: smooth;
}


/* ==========================================================================
   7. REDUCED MOTION — mandatory, and switched off by name
      No wildcard selector, no !important. Two things matter more than the
      rest:

        * .reveal lands FINISHED and visible. Content is never left hidden
          because someone asked for less motion.
        * .mist is pinned with a plain `opacity` declaration rather than
          through --mist-density. That is deliberate: animations.js may have
          written the property as an inline style, and an inline custom
          property beats a stylesheet one — but it cannot beat an opacity
          that no longer reads the property at all. Under reduced motion the
          mist therefore CANNOT animate by construction, not by good
          behaviour, and it rests at exactly its cleared value.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {

  html,
  .walk-strip {
    scroll-behavior: auto;
  }

  .is-js .reveal,
  .is-js .reveal.is-in {
    opacity: 1;
    transform: none;
    transition: none;
    animation: none;
  }

  .mist,
  .window-seat .mist {
    opacity: var(--fx-mist-clear);
    transition: none;
    animation: none;
  }

  .window-seat .ws-view img {
    --ws-parallax: 0px;
    animation: none;
  }

  .card,
  .card-media img,
  .card-media picture,
  .board-item,
  .board-item img,
  .walk-item,
  .walk-item img,
  .brand,
  .nav-link,
  .nav-toggle,
  .btn,
  .chip,
  .status-chip,
  .card-title,
  .footer-col a,
  .milestone-row a,
  .mobile-bar a,
  .field-input,
  p a,
  li a,
  address a,
  dd a,
  a .milestone {
    transition: none;
    animation: none;
  }

  .card:hover .card-media img,
  .card:focus-within .card-media img,
  .card:hover .card-media picture,
  .card:focus-within .card-media picture,
  .board-item:hover,
  .board-item:focus-within,
  .board-item:hover img,
  .board-item:focus-within img,
  .walk-item:hover,
  .walk-item:focus-within,
  .walk-item:hover img,
  .walk-item:focus-within img,
  .btn:active,
  .nav-toggle:active,
  a.chip:active,
  button.chip:active,
  a:hover .milestone,
  a:focus-visible .milestone {
    transform: none;
  }
}
