/* ==========================================================================
   Nubuilt - motion system

   Rules, enforced throughout:
   1. Only transform, opacity, clip-path and filter. Never width, height,
      top or margin - those trigger layout and drop frames on a mid-range
      phone, which is most of the audience.
   2. Nothing animates the LCP element in a way that delays or shifts it.
   3. Every reveal is additive. With JavaScript off, content is simply there.
   4. Nothing runs longer than 900ms. Slow is not the same as premium.
   5. Reduced motion is honoured completely, not partially.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Scroll reveal
   Elements start hidden ONLY when JS is present to reveal them again.
   -------------------------------------------------------------------------- */

.js [data-reveal] {
  opacity: 0;
  transform: translateY(24px);
  transition:
    opacity   var(--dur-slow) var(--ease-out-arch),
    transform var(--dur-slow) var(--ease-out-arch);
  transition-delay: var(--reveal-delay, 0ms);
  will-change: transform, opacity;
}

.js [data-reveal].is-revealed {
  opacity: 1;
  transform: none;
  will-change: auto;
}

/* Ultimate fallback. If the script sets the .js class but then fails before
   revealing (a thrown error, an aborted load), this self-reveals every
   element after a beat so content can never be permanently stranded. The
   observer and the load-time failsafe both fire long before this, so it is
   invisible in normal use. */
.js [data-reveal]:not(.is-revealed) {
  animation: reveal-failsafe 0.01ms linear 3s forwards;
}

@keyframes reveal-failsafe {
  to { opacity: 1; transform: none; }
}

/* --------------------------------------------------------------------------
   2. Line-mask heading reveal
   Each line rises from behind its own overflow mask. This one effect does
   more for an editorial, premium register than anything else here.
   -------------------------------------------------------------------------- */

/* overflow:hidden is what makes each line rise from behind a mask, but it also
   clips descenders (g, y, p) on the final resting text. The padding adds room
   for them below the baseline; the equal negative margin keeps the line box the
   same height, so surrounding spacing is unchanged. */
.line-mask { display: block; overflow: hidden; padding-bottom: 0.15em; margin-bottom: -0.15em; }

.line-mask__inner {
  display: block;
  transform: translateY(105%);
  transition: transform var(--dur-hero) var(--ease-out-arch);
  transition-delay: var(--line-delay, 0ms);
  will-change: transform;
}

.is-revealed .line-mask__inner,
.line-mask.is-revealed .line-mask__inner { transform: none; will-change: auto; }

/* --------------------------------------------------------------------------
   3. Image reveal
   A clip-path wipe with a counter-scale, so the photograph feels uncovered
   rather than faded in. Architectural rather than cinematic.
   -------------------------------------------------------------------------- */

.reveal-image { overflow: hidden; position: relative; }

.js .reveal-image img,
.js .reveal-image picture {
  clip-path: inset(0 0 100% 0);
  transform: scale(1.06);
  transition:
    clip-path var(--dur-hero) var(--ease-out-arch),
    transform 1200ms var(--ease-out-arch);
  transition-delay: var(--reveal-delay, 0ms);
}

.js .reveal-image.is-revealed img,
.js .reveal-image.is-revealed picture {
  clip-path: inset(0 0 0 0);
  transform: scale(1);
}

/* A reveal-image nested inside a reveal target (e.g. a card whose parent link
   carries data-reveal) has no data-reveal of its own, so it never receives
   is-revealed directly. Uncover it when its ancestor reveals, otherwise the
   image stays clipped to nothing and never appears. */
.js [data-reveal].is-revealed .reveal-image img,
.js [data-reveal].is-revealed .reveal-image picture {
  clip-path: inset(0 0 0 0);
  transform: scale(1);
}

/* --------------------------------------------------------------------------
   4. The signature: the mark settles in
   The supplied mark is a filled brush form, not a stroked outline, so it
   fades up and settles rather than drawing itself. Runs once per session,
   so a returning visitor sees a static logo. That restraint is what keeps
   it a signature rather than a gimmick.
   -------------------------------------------------------------------------- */

.mark { display: block; }

.mark-draw {
  animation: mark-settle 900ms var(--ease-out-arch) both;
}

@keyframes mark-settle {
  from { opacity: 0; transform: translateY(6px) scale(0.96); }
  to   { opacity: 1; transform: none; }
}

/* --------------------------------------------------------------------------
   5. Hover micro-interactions
   Four small moves that read as one gesture.
   -------------------------------------------------------------------------- */

.hover-media { overflow: hidden; }

.hover-media img {
  transition: transform var(--dur-slow) var(--ease-out-arch);
}

.hover-parent:hover .hover-media img,
.hover-parent:focus-visible .hover-media img { transform: scale(1.04); }

.hover-rule {
  position: relative;
  display: inline-block;
}

.hover-rule::after {
  content: "";
  position: absolute;
  inset-inline-start: 0; bottom: -0.35em;
  width: 100%; height: 1px;
  background: var(--stone-dark);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur-base) var(--ease-out-arch);
}

.hover-parent:hover .hover-rule::after,
.hover-rule:hover::after,
.hover-parent:focus-visible .hover-rule::after { transform: scaleX(1); }

.hover-shift {
  transition: transform var(--dur-base) var(--ease-out-arch);
}
.hover-parent:hover .hover-shift { transform: translateX(4px); }

.arrow-nudge { transition: transform var(--dur-fast) var(--ease-out-arch); }
.hover-parent:hover .arrow-nudge,
.btn:hover .arrow-nudge { transform: translateX(3px); }

/* --------------------------------------------------------------------------
   6. Page transitions
   Progressive enhancement: browsers without support just navigate.
   -------------------------------------------------------------------------- */

@view-transition { navigation: auto; }

::view-transition-old(root) {
  animation: fade-out 220ms var(--ease-in-out) both;
}
::view-transition-new(root) {
  animation: fade-in 380ms var(--ease-out-arch) both;
}

/* Shared element: a portfolio cover morphs into the case study header. */
::view-transition-group(project-cover) {
  animation-duration: var(--dur-slow);
  animation-timing-function: var(--ease-out-arch);
}

@keyframes fade-out { to { opacity: 0; } }
@keyframes fade-in  { from { opacity: 0; transform: translateY(8px); } }

/* --------------------------------------------------------------------------
   7. Header condense
   -------------------------------------------------------------------------- */

.header {
  transition:
    background-color var(--dur-base) var(--ease-out-arch),
    box-shadow       var(--dur-base) var(--ease-out-arch),
    backdrop-filter  var(--dur-base) var(--ease-out-arch);
}

.header__inner {
  transition: height var(--dur-base) var(--ease-out-arch);
}

.header__logo {
  transform-origin: left center;
  transition: transform var(--dur-base) var(--ease-out-arch);
}

.header.is-condensed .header__logo { transform: scale(0.88); }

/* --------------------------------------------------------------------------
   8. Mobile menu
   -------------------------------------------------------------------------- */

.menu {
  clip-path: inset(0 0 100% 0);
  transition: clip-path var(--dur-slow) var(--ease-out-arch);
}
.menu.is-open { clip-path: inset(0 0 0 0); }

.menu__item {
  opacity: 0;
  transform: translateY(16px);
  transition:
    opacity   var(--dur-base) var(--ease-out-arch),
    transform var(--dur-base) var(--ease-out-arch);
}
.menu.is-open .menu__item { opacity: 1; transform: none; }

/* --------------------------------------------------------------------------
   9. Reduced motion - complete, not cosmetic
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  .js [data-reveal],
  .line-mask__inner,
  .js .reveal-image img,
  .js .reveal-image picture,
  .menu__item {
    opacity: 1 !important;
    transform: none !important;
    clip-path: none !important;
  }

  .mark-draw { animation: none !important; opacity: 1 !important; transform: none !important; }

  @view-transition { navigation: none; }
}
