/* ==========================================================================
   Emi Marie — motion layer
   Purely additive: opacity / transform / transition / animation only.
   No layout, position, size, color, or content rules are changed here.
   ========================================================================== */

/* ---------- mobile fix: real viewport height, not the address-bar-jumpy vh ----------
   Browsers that don't understand dvh simply drop this line, leaving the
   original vh rule (from the main stylesheet) in place — safe either way. */
.hero { min-height: 88dvh; }
@media (max-width: 640px) {
  .hero { min-height: 82dvh; }
}

/* ---------- hero banner framing ---------- */
.hero-media img { object-position: center 18%; }
.hero-media { overflow: hidden; }
.hero-media img {
  transform: translate3d(0, var(--em-hero-shift, 0px), 0) scale(1.08);
  will-change: transform;
}
@media (min-width: 641px) {
  .hero-media img {
    object-position: 58% 30%;
    transform: translate3d(0, var(--em-hero-shift, 0px), 0) scale(1.02);
  }
}
.hero-content {
  transform: translate3d(0, var(--em-content-shift, 0px), 0);
  opacity: var(--em-content-opacity, 1);
  will-change: transform, opacity;
}
html { scroll-behavior: smooth; }

html.em-page-exit .page-shell,
html.em-page-exit .portfolio-page {
  animation: emPageExit .24s ease forwards;
}

/* ---------- base entrance keyframes ---------- */
@keyframes emFadeIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes emPageExit {
  from { opacity: 1; transform: translateY(0); }
  to { opacity: 0; transform: translateY(-8px); }
}
@keyframes emFadeUp {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes emHeroTextIn {
  from { opacity: 0; transform: translate3d(0, 22px, 0); filter: blur(5px); }
  to { opacity: 1; transform: translate3d(0, 0, 0); filter: blur(0); }
}
@keyframes emShimmer {
  0%   { opacity: .35; }
  50%  { opacity: .75; }
  100% { opacity: .35; }
}
@keyframes emSwap {
  from { opacity: 0; transform: scale(.985); }
  to   { opacity: 1; transform: scale(1); }
}
@keyframes emLinkIn {
  from { opacity: 0; transform: translate(-50%, -50%) scale(.6); }
  to   { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}
@keyframes emImgSettle {
  from { opacity: 0; transform: scale(1.015); }
  to   { opacity: 1; transform: scale(1); }
}
@keyframes emSkeleton {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
@keyframes emPulseRing {
  0%   { box-shadow: 0 0 0 0 rgba(247,241,232,.32); }
  70%  { box-shadow: 0 0 0 9px rgba(247,241,232,0); }
  100% { box-shadow: 0 0 0 0 rgba(247,241,232,0); }
}

/* ---------- whole-page settle-in ---------- */
.page-shell,
.portfolio-page {
  animation: emFadeIn .6s ease both;
}

/* ---------- hero: staggered cascade on load ---------- */
.hero-header   { animation: emFadeUp .7s cubic-bezier(.16,.84,.44,1) both; }
.eyebrow       { animation: emFadeUp .7s cubic-bezier(.16,.84,.44,1) .06s both; }
.hero-title    { animation: emHeroTextIn .95s cubic-bezier(.16,.84,.44,1) .14s both; }
.hero-text     { animation: emHeroTextIn .95s cubic-bezier(.16,.84,.44,1) .22s both; }
.hero-actions  { animation: emHeroTextIn .95s cubic-bezier(.16,.84,.44,1) .3s both; }
.hero-quote    { animation: emHeroTextIn .95s cubic-bezier(.16,.84,.44,1) .36s both; }
.hero-meta     { animation: emHeroTextIn .95s cubic-bezier(.16,.84,.44,1) .42s both; }

.portfolio-title  { animation: emFadeUp .8s cubic-bezier(.16,.84,.44,1) both; }
.portfolio-copy   { animation: emFadeUp .8s cubic-bezier(.16,.84,.44,1) .1s both; }
.portfolio-tags   { animation: emFadeUp .8s cubic-bezier(.16,.84,.44,1) .18s both; }

/* ---------- images: a calm skeleton while loading, a soft settle on arrival ----------
   enhance.js adds .em-img-loading the moment an image is scanned and swaps
   it for .em-img-loaded once the browser actually has bytes (or removes
   both, untouched, if an image was already cached — no need to animate
   something that's already there). Default state with no JS is simply
   "visible, un-animated", so nothing ever depends on this to be seen. */
img.em-img-loading {
  background-image: linear-gradient(100deg,
    #ffffff07 30%, #ffffff16 45%, #ffffff16 55%, #ffffff07 70%);
  background-size: 250% 100%;
  animation: emSkeleton 1.6s ease-in-out infinite;
}
img.em-img-loaded {
  animation: emImgSettle .45s cubic-bezier(.16,.84,.44,1) both;
}

/* ---------- scroll reveal, progressively enhanced ----------
   Default state = fully visible. Only once enhance.js confirms
   IntersectionObserver support does it add html.js-reveal-ready,
   which is the only thing that ever hides content — and a safety
   timer in the script force-reveals anything left hidden either way. */
html.js-reveal-ready .comp-card,
html.js-reveal-ready .portfolio-hero-card,
html.js-reveal-ready .instagram-embed-wrap,
html.js-reveal-ready .about-copy > p,
html.js-reveal-ready .card,
html.js-reveal-ready .grid2 .card {
  opacity: 0;
  transform: translateY(22px);
  transition: opacity .6s cubic-bezier(.16,.84,.44,1),
              transform .6s cubic-bezier(.16,.84,.44,1);
}
html.js-reveal-ready .in-view {
  opacity: 1 !important;
  transform: none !important;
}
html.js-reveal-ready .instagram-grid > .instagram-embed-wrap:nth-child(2) { transition-delay: .05s; }
html.js-reveal-ready .instagram-grid > .instagram-embed-wrap:nth-child(3) { transition-delay: .1s; }
html.js-reveal-ready .grid2 .card:nth-child(2n) { transition-delay: .04s; }
html.js-reveal-ready .grid2 .card:nth-child(3n) { transition-delay: .08s; }

/* ---------- marquee ("Selected Work"): one settle-in on load, then just drift ---------- */
.marquee { animation: emFadeIn .8s ease both; }
.marquee:hover .marquee-track { animation-play-state: paused; }
/* Held by enhance.js for a beat on load so the drift doesn't start while
   ~40 hot-linked images are simultaneously requesting/decoding — that
   overlap was a big part of the mobile jank. */
.marquee-track.is-priming { animation-play-state: paused; }

/* ---------- micro-interactions ---------- */
.work-card {
  /* Override the base stylesheet's per-card translateZ(0). Promoting
     every one of the ~44 marquee cards to its own compositing layer
     underneath a parent that's already a single moving layer buys
     nothing and costs real GPU/raster budget on mobile — this was
     part of what made the strip stutter instead of drifting smoothly. */
  transform: none;
  will-change: auto;
  transition: transform .35s cubic-bezier(.2,.8,.2,1),
              box-shadow .35s ease,
              border-color .25s ease;
}
.work-card:hover {
  transform: translateZ(0) translateY(-6px);
  box-shadow: 0 20px 48px rgba(0,0,0,.5);
}

.button-link {
  transition: transform .3s cubic-bezier(.2,.8,.2,1), box-shadow .3s ease;
}
.button-link:hover {
  transform: translateY(-2px);
  box-shadow: 0 10px 26px rgba(0,0,0,.35);
}
.button-link:active { transform: translateY(0) scale(.97); }

.hero-audio-button { transition: opacity .25s ease, transform .25s ease; }
.hero-audio-button:hover { opacity: .8; transform: translateY(-1px); }
.hero-audio-button:active { transform: scale(.96); }
/* A small sign of life while the track is actually playing — set by
   enhance.js listening to the real <audio> element's play/pause events,
   so it always matches actual playback state. */
.hero-audio-button.is-playing {
  animation: emPulseRing 2.4s cubic-bezier(.4,0,.6,1) infinite;
}

.nav-links a { position: relative; }
.nav-links a::after {
  content: "";
  position: absolute;
  left: 0;
  right: 100%;
  bottom: -3px;
  height: 1px;
  background: currentColor;
  transition: right .3s cubic-bezier(.2,.8,.2,1);
}
.nav-links a:hover::after,
.nav-links a:focus-visible::after {
  right: 0;
}

.instagram-nav-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
  padding: .2rem .25rem;
}
.instagram-nav-link svg { display: block; width: 1.05rem; height: 1.05rem; }

.instagram-embed-wrap iframe {
  display: block;
  width: 100%;
  min-height: 620px;
  border: 0;
  background: #fff;
}
@media (max-width: 640px) {
  .instagram-embed-wrap iframe { min-height: 560px; }
}

.portfolio-hero-card { transition: box-shadow .35s ease; }
.portfolio-hero-card:hover { box-shadow: 0 20px 48px rgba(0,0,0,.45); }

/* ==========================================================================
   Contact circle (orbit) — one calm entrance, responsive on hover.
   No continuous/infinite animation here anymore — running several looping
   animations across the rings at once was what made this feel flashy/buggy.
   Only opacity/transform/box-shadow are ever touched; every element keeps
   its original top/left/inset positioning untouched.
   ========================================================================== */
.orbit-ring {
  animation: emFadeIn .6s ease both;
  transform-origin: center;
}
.ring-two   { animation-delay: .08s; }
.ring-three { animation-delay: .16s; }
.ring-four  { animation-delay: .24s; }

.orbit-center {
  animation: emFadeIn .6s ease both;
  transition: box-shadow .3s ease, transform .3s ease;
}
.orbit-wrap:hover .orbit-center {
  transform: scale(1.02);
  box-shadow: inset 0 0 0 1px #ffffff14, 0 0 0 1px rgba(255,255,255,.06);
}

.orbit-link {
  animation: emLinkIn .5s cubic-bezier(.2,.8,.2,1) both;
  transition: border-color .3s ease, color .3s ease, transform .3s cubic-bezier(.2,.8,.2,1), background .3s ease;
}
.orbit-link:nth-of-type(1) { animation-delay: .05s; }
.orbit-link:nth-of-type(2) { animation-delay: .1s; }
.orbit-link:nth-of-type(3) { animation-delay: .15s; }
.orbit-link:nth-of-type(4) { animation-delay: .2s; }
.orbit-link:nth-of-type(5) { animation-delay: .25s; }
.orbit-link:nth-of-type(6) { animation-delay: .3s; }
.orbit-link:nth-of-type(7) { animation-delay: .35s; }
.orbit-link:nth-of-type(8) { animation-delay: .4s; }
.orbit-link:hover {
  transform: translate(-50%,-50%) scale(1.08);
  background: #17161488;
}
.orbit-link:active {
  transform: translate(-50%,-50%) scale(.96);
}

/* ==========================================================================
   Comp card — animated swap on prev/next click.
   enhance.js triggers this class each time the displayed comp card image
   changes; it's removed automatically once the animation ends so it can
   replay on every click, including rapid clicking.
   ========================================================================== */
.comp-card-visual,
.comp-card-heading,
.comp-details {
  animation: none;
}
.comp-card.is-swapping .comp-card-visual   { animation: emSwap .38s cubic-bezier(.16,.84,.44,1); }
.comp-card.is-swapping .comp-card-heading  { animation: emSwap .38s cubic-bezier(.16,.84,.44,1) .03s both; }
.comp-card.is-swapping .comp-details       { animation: emSwap .38s cubic-bezier(.16,.84,.44,1) .06s both; }

.comp-card-nav button {
  transition: transform .25s cubic-bezier(.2,.8,.2,1),
              border-color .25s ease,
              background .25s ease;
}
.comp-card-nav button:hover {
  transform: scale(1.08);
  border-color: rgba(255,255,255,.4);
}
.comp-card-nav button:active {
  transform: scale(.9);
}

/* ---------- gallery pages ---------- */
.status { animation: emShimmer 1.8s ease-in-out infinite; }
.card {
  transition: transform .35s cubic-bezier(.2,.8,.2,1),
              box-shadow .35s ease,
              border-color .25s ease;
}
.card:hover {
  transform: translateY(-6px);
  box-shadow: 0 20px 48px rgba(0,0,0,.45);
}
.lightbox img { animation: emFadeUp .35s ease both; }

/* ---------- offscreen render cost: skip layout/paint for rows the
   viewport can't see yet, without changing anything about how they
   look once they're actually on screen. Meaningful on the galleries
   page and the marquee, where dozens of image cards exist at once —
   this is what keeps scrolling smooth on lower-powered phones instead
   of the browser paying paint cost for everything simultaneously. ---------- */
.grid img,
.grid2 .card,
.work-card {
  content-visibility: auto;
}
.grid img { contain-intrinsic-size: 320px 320px; }
.grid2 .card { contain-intrinsic-size: 280px 400px; }
.work-card { contain-intrinsic-size: 280px 380px; }

/* ---------- gentle focus polish ---------- */
a:focus-visible,
button:focus-visible {
  outline: 2px solid #f7f1e8;
  outline-offset: 3px;
  border-radius: 4px;
}

/* ---------- respect reduced-motion preference ---------- */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
  html.js-reveal-ready .comp-card,
  html.js-reveal-ready .portfolio-hero-card,
  html.js-reveal-ready .instagram-embed-wrap,
  html.js-reveal-ready .about-copy > p,
  html.js-reveal-ready .card,
  html.js-reveal-ready .grid2 .card {
    opacity: 1 !important;
    transform: none !important;
  }
  img.em-img-loading { animation: none; background: #ffffff0a; }
  .hero-audio-button.is-playing { animation: none; }
  html { scroll-behavior: auto; }
  .hero-media img,
  .hero-content { transform: none; opacity: 1; will-change: auto; }
  html.em-page-exit .page-shell,
  html.em-page-exit .portfolio-page { animation: none; }
}
