/* =========================================================================
   MOTION LAYER

   Loaded after style.css, so equal-specificity rules here win. Two rules of
   the house:

   1. Nothing in this file hides content. Every "start" state (opacity 0,
      offsets, clips) is written by motion.js at runtime. If the GSAP CDN is
      blocked or the script throws, the page renders exactly as it did before
      the motion layer existed.
   2. Anything motion.js tweens must not depend on a CSS transform for its
      position — GSAP owns the transform property and would wipe it out. The
      overrides below re-centre those elements with auto margins instead.
   ========================================================================= */

:root {
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}

/* ===== Transform ownership handoffs =====

   .project-card__desc and .project-card__thumb centred themselves with
   translateX(-50%). GSAP writes the whole transform matrix when it animates
   y/scale/clip, which would drop that -50% and shove them right by half their
   width. left/right/auto-margins centre them identically with no transform. */

.project-card__desc,
.project-card__thumb {
  left: 0;
  right: 0;
  margin-left: auto;
  margin-right: auto;
  transform: none;
}

/* Stack icons were click-through, which also made them hover-through. They
   need pointer events for the scale + cursor-label treatment. */
.design-stack__icon {
  pointer-events: auto;
}

/* Marquee tracks are driven frame by frame in JS once motion.js is alive, so
   the CSS keyframes have to get out of the way. The class is added by JS,
   which means the CSS animation stays in charge if the script never runs. */
.js-marquee {
  animation: none !important;
}

/* Every marquee clips to its viewport, which would shear the tops off items
   that scale or lift on hover. Padding buys the headroom and an equal
   negative margin gives the space straight back, so the flow is unchanged. */
.animate__carousel,
.design-stack__scroll,
.scribbles__viewport {
  padding: 30px 0;
  margin: -30px 0;
}

/* ===== Nav =====

   style.css owns the nav's position, z-index and background — it is already
   sticky at z-index 210 and carries the scroll progress bar. The motion layer
   deliberately adds no hide-on-scroll here: retreating the bar would take the
   progress indicator with it, which is the opposite of what it is for.

   Nav link word swap: the label rides up out of a clipped box while its
   duplicate rides in from below. */
.swap {
  position: relative;
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
}

.swap span {
  display: block;
  transition: transform 0.5s var(--ease-out);
}

.swap__b {
  position: absolute;
  left: 0;
  top: 100%;
  color: var(--purple);
}

.nav__link:hover .swap span {
  transform: translateY(-100%);
}

.nav__link--active .swap__b {
  color: var(--white);
}

/* ===== Split text ===== */

.split {
  perspective: 600px;
}

/* Keeps a split heading breaking at spaces only. Without it the per-character
   inline-blocks let the browser wrap mid-word. */
.word {
  display: inline-block;
  white-space: nowrap;
}

.char {
  display: inline-block;
  overflow: hidden;
  vertical-align: top;
  /* Descenders would be clipped by the overflow that makes the rise work, so
     the box is grown downward and pulled back up by the same amount. */
  padding-bottom: 0.14em;
  margin-bottom: -0.14em;
}

.char__inner {
  display: inline-block;
}

/* ===== Custom cursor ===== */

body.has-cursor,
body.has-cursor * {
  cursor: none !important;
}

.cursor__dot,
.cursor__ring,
.cursor__label {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 10000;
  pointer-events: none;
}

.cursor__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--purple);
}

.cursor__ring {
  width: 42px;
  height: 42px;
  border-radius: 50%;
  border: 1.5px solid rgba(210, 192, 249, 0.55);
}

/* Dark, because a label only ever appears on top of the filled disc the ring
   becomes for labelled targets. Purple-on-purple was unreadable over the
   lighter project cards.

   Impact rather than the site's display face: at 13px inside a disc the
   handwritten Hardshock lost its counters and read as a smudge, and this is
   the one piece of type that has to survive being small. */
.cursor__label {
  font-family: Impact, Haettenschweiler, "Arial Narrow Bold", sans-serif;
  font-size: 17px;
  line-height: 1;
  letter-spacing: 0.05em;
  color: var(--bg);
  white-space: nowrap;
  text-transform: uppercase;
}

/* ===== Intro overlay and page wipe ===== */

/* The pre-paint cover. The inline script in each page's <head> adds
   `is-booting` before the first frame is painted; motion.js drops it the
   moment the real .intro overlay is in the DOM, and the script itself drops it
   on `load` and on a 2.5s timer regardless. That belt-and-braces is what keeps
   this honest against the rule that nothing here may leave content hidden: no
   path through a blocked CDN or a thrown script ends with the class still on.

   ::before rather than a real element because it paints from the first frame
   without waiting for <body> to be parsed — which is also why the about page's
   colour is keyed off an attribute on <html> and not body.about-page. */
html.is-booting::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 9998;
  background: var(--bg);
}

html[data-page="about"].is-booting::before {
  background: #5c4c7f;
}

.intro,
.pagewipe {
  position: fixed;
  inset: 0;
  z-index: 9998;
  background: var(--bg);
  display: flex;
  align-items: center;
  justify-content: center;
}

body.about-page .intro,
body.about-page .pagewipe {
  background: #5c4c7f;
}

.pagewipe {
  z-index: 9999;
  /* Parked off-screen until a navigation sweeps it up. */
  clip-path: inset(100% 0 0 0);
}

/* The logo sits in its own fixed layer, deliberately NOT inside .intro. The
   overlay reveals by animating clip-path, and clip-path on a parent clips its
   children — with the logo inside, the wipe sliced through it mid-flight and
   the travel to the header read as a cut rather than a move. */
.intro__layer {
  position: fixed;
  inset: 0;
  z-index: 9999;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}

.intro__logo {
  display: block;
  width: 80px;
  height: auto;
}

html.is-locked,
html.is-locked body {
  overflow: hidden;
}

/* ===== Section-level hover treatments ===== */

/* Cards in the "I Animate" marquee: the hovered one lifts, its neighbours
   step back so the row reads as depth rather than a flat strip. */
.animate__clip {
  transition: transform 0.5s var(--ease-out), opacity 0.4s ease;
}

/* Only where a pointer can genuinely hover. On a touch screen a tap leaves a
   sticky :hover on the carousel, which dropped every clip in the row to 45%
   and left them there — the "opacity keeps reducing" bug. */
@media (hover: hover) and (pointer: fine) {
  .animate__carousel:hover .animate__clip {
    opacity: 0.45;
  }

  .animate__carousel .animate__clip:hover {
    opacity: 1;
    transform: translateY(-10px) scale(1.05);
  }
}

/* Project card content leans on a perspective while the card art stays put,
   so the tilt reads as parallax rather than the whole card rocking. No
   translateZ on the thumb: pushing it forward would visually enlarge it and
   the thumb sizes are matched to Figma. */
.project-card {
  perspective: 1400px;
}

.brainstorms__stack:active {
  cursor: grabbing;
}

.works__arrow {
  transition: opacity 0.3s ease;
}

/* Gives the screenshot's pointer lean somewhere to lean into. */
.works__content {
  perspective: 1400px;
}

/* About: hovering one photo pulls it forward and mutes the rest of the grid.
   motion.js clears its inline transform after the reveal so these rules can
   take over — inline styles would otherwise outrank them. */
.about-intro__photo {
  transition: transform 0.5s var(--ease-out), filter 0.4s ease;
}

/* Hover-capable pointers only, the same guard the "I Animate" strip carries
   above. A tap on a touch screen leaves a sticky :hover on the grid, which
   dropped every other photo to grayscale and left them there until the next
   tap landed somewhere else — on a phone the grid is meant to read as
   photographs, nothing more. */
@media (hover: hover) and (pointer: fine) {
  .about-intro__grid:hover .about-intro__photo {
    filter: grayscale(0.65) brightness(0.75);
  }

  .about-intro__grid .about-intro__photo:hover {
    filter: none;
    transform: scale(1.05);
    position: relative;
    z-index: 2;
  }
}

/* Scribbles read as pinned polaroids — a small lift and tilt on hover, and
   the rest of the row drops to black and white so the one you are looking at
   is the only thing in colour. Same shape as the "I Animate" strip on the work
   page, which dims its neighbours instead. */
.scribbles__item {
  transition: transform 0.5s var(--ease-out), filter 0.4s ease;
}

/* Same guard as the photo grid: on touch this row is a marquee to watch, not
   something to pick one item out of. */
@media (hover: hover) and (pointer: fine) {
  .scribbles__viewport:hover .scribbles__item {
    filter: grayscale(1) brightness(0.72);
  }

  .scribbles__viewport .scribbles__item:hover {
    filter: none;
    transform: translateY(-10px) rotate(-2.5deg) scale(1.04);
    position: relative;
    z-index: 2;
  }
}

.connect__contact-icon img {
  transition: filter 0.4s ease;
}

/* ===== Reduced motion =====

   motion.js bails out entirely under this preference, but the CSS-only
   transitions above are declared here too so nothing is left moving. */
@media (prefers-reduced-motion: reduce) {

  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}
