/* Motion ------------------------------------------------------------------
   Entrance, scroll and hover motion layered on top of styles.css.
   Content is only hidden before it animates when html.js is set (added in
   <head>), and reduced-motion visitors get the static page. */

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

@media (prefers-reduced-motion: no-preference) {
  html {
    scroll-behavior: smooth;
  }
}

/* Hero headline ----------------------------------------------------------- */

/* The headline stays pinned where it sits while the page scrolls; motion.js
   fades it out as Work comes up, and Work draws over it */
.js .hero__title {
  position: sticky;
  top: var(--hero-top);
}

.work {
  position: relative;
}

/* /scripts/motion.js wraps each word; hold the headline back until it has */
.js .hero__title:not(.is-split) {
  opacity: 0;
}

.word--text {
  display: inline-block;
}

.is-split .word {
  animation: word-rise 1s var(--ease-out) backwards;
  animation-delay: calc(150ms + var(--i) * 55ms);
}

.is-split .name-pill.word,
.is-split .xp-badge.word {
  animation-name: sticker-pop;
  animation-duration: 0.8s;
  animation-timing-function: var(--ease-spring);
}

@keyframes word-rise {
  from {
    opacity: 0;
    transform: translateY(0.45em);
    filter: blur(8px);
  }
}

@keyframes sticker-pop {
  from {
    opacity: 0;
    transform: scale(0.3) rotate(-14deg);
  }
}

.xp-badge img {
  animation: star-spin var(--xp-spin, 16s) linear infinite;
}

@keyframes star-spin {
  to { rotate: 360deg; }
}

/* Name pill hover (Figma 128:241): the photo rises out of the pill and the
   name is scribbled above with an arrow. All offsets are Figma px / 48 so
   they scale with the headline; the label divides by its own 24px size. */

.name-pill__photo img {
  transition: transform 0.55s var(--ease-spring);
}

.name-pill__label,
.name-pill__arrow,
.name-pill__arrowhead {
  position: absolute;
  pointer-events: none;
}

.name-pill__label {
  top: -3.5917em;
  left: 3.4em;
  color: var(--ink);
  font-family: var(--font-hand);
  font-size: 0.46em;
  font-weight: 400;
  line-height: normal;
  white-space: nowrap;
  opacity: 0;
  transform: translate(-50%, -30%) rotate(-12deg) scale(0.9);
  transition: opacity 0.25s ease, transform 0.5s var(--ease-spring);
}

.name-pill__arrow {
  top: -1.2891em;
  left: 1.407em;
  width: 1.3542em;
  max-width: none;
  height: 0.4063em;
  rotate: -8.19deg;
  clip-path: inset(0 0 0 100%);
  transition: clip-path 0.45s var(--ease-out);
}

.name-pill__arrowhead {
  top: -0.9464em;
  left: 1.3802em;
  width: 0.2083em;
  max-width: none;
  height: 0.1635em;
  rotate: -8.19deg;
  opacity: 0;
  transition: opacity 0.2s ease;
}

/* ...and an "Anees" cursor glides in and says hi (motion.js swaps the label
   for a greeting, a different one each hover). Scoped to the pill so it
   outranks the page cursor's fixed positioning in .fig-cursor below. */
.name-pill .anees {
  position: absolute;
  /* /scripts/motion.js sets --anees-top so the cursor stands 80px below the nav,
     clear of the pill and photo */
  top: var(--anees-top, 78%);
  left: 72%;
  z-index: 3;
  opacity: 0;
  transform: translate(90px, 70px);
  transition: opacity 0.25s ease, transform 0.7s var(--ease-out);
}

.name-pill .anees.is-here {
  opacity: 1;
  transform: none;
}

/* XP badge hover: six pills fly out to the corners of a hexagon and orbit the
   star clockwise. Three nested layers, one job each — .xp-orbit turns, .xp-spoke
   places a pill at its corner, and .xp-tag turns back so the words stay upright.
   The hexagon itself is never drawn; it is only where the pills sit. */

.xp-tags {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.xp-orbit {
  position: absolute;
  inset: 0;
  animation: xp-orbit var(--xp-spin, 16s) linear infinite;
  animation-play-state: paused;
}

/* A zero-size arm from the badge's centre out to one corner */
.xp-spoke {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 0;
  height: 0;
  transform: rotate(calc(var(--i) * 60deg)) translateY(0);
  transition: transform 0.55s var(--ease-spring);
}

.xp-tag {
  position: absolute;
  left: 50%;
  top: 50%;
  padding: 0.42em 0.72em;
  border-radius: 999px;
  background: #fff;
  box-shadow:
    0 2px 6px -1px rgba(20, 22, 28, 0.16),
    0 6px 14px -8px rgba(20, 22, 28, 0.3);
  color: var(--ink);
  font-size: 0.23em;
  font-weight: 500;
  line-height: 1.25;
  /* Small text should not carry the page's display tracking */
  letter-spacing: 0.005em;
  text-transform: none;
  white-space: nowrap;
  /* Centring uses the `translate` property, which composes outside `transform`,
     so the shift is never itself rotated — that is what pushed the wider pills
     off their spoke and broke the even spacing. */
  translate: -50% -50%;
  /* Undo the spoke's angle so the pill sits level at its corner */
  transform: rotate(calc(var(--i) * -60deg));
  opacity: 0;
  scale: 0.4;
  /* Cancels the orbit, so the words never turn upside down */
  animation: xp-upright var(--xp-spin, 16s) linear infinite;
  animation-play-state: paused;
  transition: opacity 0.25s ease, scale 0.5s var(--ease-spring);
}

@keyframes xp-orbit {
  to { transform: rotate(360deg); }
}

@keyframes xp-upright {
  to { rotate: -360deg; }
}

@media (hover: hover) {
  .name-pill:hover {
    z-index: 2;
  }

  .name-pill:hover .name-pill__photo img {
    transform: none;
  }

  .name-pill:hover .name-pill__label {
    opacity: 1;
    transform: translate(-50%, -50%) rotate(-5.14deg);
    transition-delay: 0.12s;
  }

  .name-pill:hover .name-pill__arrow {
    clip-path: inset(0 0 0 0);
    transition-delay: 0.22s;
  }

  .name-pill:hover .name-pill__arrowhead {
    opacity: 1;
    transition-delay: 0.6s;
  }

  /* --badge-scale spreads the ring with the badge (/styles/styles.css) */
  .xp-badge:hover .xp-spoke {
    transform:
      rotate(calc(var(--i) * 60deg))
      translateY(calc(var(--xp-r, 1.6em) * -1 * var(--badge-scale, 1)));
    transition-delay: calc(var(--i) * 45ms);
  }

  .xp-badge:hover .xp-tag {
    opacity: 1;
    scale: 1;
    transition-delay: calc(var(--i) * 45ms);
  }

  .xp-badge:hover .xp-orbit,
  .xp-badge:hover .xp-tag {
    animation-play-state: running;
  }
}

/* Touch has no hover, so the same three flourishes answer to a tap.
   /scripts/motion.js adds .is-on and clears it after 4s. These mirror the
   :hover rules above — change one, change the other. */

.name-pill.is-on {
  z-index: 2;
}

.name-pill.is-on .name-pill__photo img {
  transform: none;
}

.name-pill.is-on .name-pill__label {
  opacity: 1;
  transform: translate(-50%, -50%) rotate(-5.14deg);
  transition-delay: 0.12s;
}

.name-pill.is-on .name-pill__arrow {
  clip-path: inset(0 0 0 0);
  transition-delay: 0.22s;
}

.name-pill.is-on .name-pill__arrowhead {
  opacity: 1;
  transition-delay: 0.6s;
}

.xp-badge.is-on .xp-spoke {
  transform:
    rotate(calc(var(--i) * 60deg))
    translateY(calc(var(--xp-r, 1.6em) * -1 * var(--badge-scale, 1)));
  transition-delay: calc(var(--i) * 45ms);
}

.xp-badge.is-on .xp-tag {
  opacity: 1;
  scale: 1;
  transition-delay: calc(var(--i) * 45ms);
}

.xp-badge.is-on .xp-orbit,
.xp-badge.is-on .xp-tag {
  animation-play-state: running;
}

/* Touch: the chips do not orbit ------------------------------------------
   The badge sits near the end of its line, so on a phone a ring around it
   runs off the right edge. There the six chips become a centred cluster
   under the hero instead. /scripts/motion.js measures --xp-cluster-dx and
   --xp-cluster-dy, so the cluster clears the headline however it wraps. */

@media (hover: none) {
  .xp-badge .xp-tags {
    position: absolute;
    top: calc(100% + var(--xp-cluster-dy, 0.5em));
    left: 50%;
    width: min(92vw, 420px);
    translate: calc(-50% + var(--xp-cluster-dx, 0px)) 0;
  }

  .xp-badge .xp-orbit {
    position: static;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.14em;
    animation: none;
  }

  .xp-badge .xp-spoke,
  .xp-badge.is-on .xp-spoke {
    position: static;
    width: auto;
    height: auto;
    transform: none;
    transition: none;
  }

  .xp-badge .xp-tag {
    position: static;
    translate: none;
    transform: none;
    animation: none;
  }
}

/* Sticky notes ------------------------------------------------------------
   All note motion is stop motion, like the stack icons. Each note has an
   off-screen spot (--ex/--ey: out past its side, and above or below):
   - on load it hops in from there through held poses (`translate`/`rotate`)
   - hovering "disappear." sends it back out there in held steps (a steps()
     transition on `translate`), and back in when the hover ends
   - on scroll /scripts/motion.js hops it out through held poses (`transform`)
   The base tilt lives in `transform` too, so none of these fight. */

/* Pinned with the headline; /scripts/motion.js hops the notes out as Work comes up */
.js .notes {
  position: sticky;
  top: 0;
}

.notes {
  z-index: 0;
}

.note[data-side="l"] { --dir: -1; }
.note[data-side="r"] { --dir: 1; }

.note {
  --ex: calc(var(--dir) * 560);
  --ey: calc((var(--y) - 300) * 1.3);
  transition: translate 0.7s steps(7, jump-end);
  animation:
    note-enter 1.1s steps(1, end) backwards,
    note-boil 1.8s steps(1, end) infinite;
  animation-delay: calc(var(--y) * 1.2ms), calc(var(--y) * 1.2ms + 1.1s);
}

@keyframes note-enter {
  0% {
    opacity: 0;
    translate: calc(var(--ex) * 1px) calc(var(--ey) * 1px);
    rotate: calc(var(--dir) * 24deg);
  }
  12% {
    opacity: 1;
    translate: calc(var(--ex) * 0.8px) calc(var(--ey) * 0.8px);
    rotate: calc(var(--dir) * -14deg);
  }
  25% {
    opacity: 1;
    translate: calc(var(--ex) * 0.6px) calc(var(--ey) * 0.6px);
    rotate: calc(var(--dir) * 10deg);
  }
  37% {
    opacity: 1;
    translate: calc(var(--ex) * 0.42px) calc(var(--ey) * 0.42px);
    rotate: calc(var(--dir) * -7deg);
  }
  50% {
    opacity: 1;
    translate: calc(var(--ex) * 0.26px) calc(var(--ey) * 0.26px);
    rotate: calc(var(--dir) * 5deg);
  }
  62% {
    opacity: 1;
    translate: calc(var(--ex) * 0.13px) calc(var(--ey) * 0.13px);
    rotate: calc(var(--dir) * -3deg);
  }
  75% {
    opacity: 1;
    translate: calc(var(--ex) * 0.04px) -8px;
    rotate: 2deg;
  }
  87% {
    opacity: 1;
    translate: 0 3px;
    rotate: calc(var(--dir) * -1deg);
  }
  100% {
    opacity: 1;
    translate: 0 0;
    rotate: 0deg;
  }
}

/* A tiny held-pose jitter while resting, like a stop-motion "boil" */
@keyframes note-boil {
  0%, 100% { rotate: 0deg; }
  33% { rotate: 0.8deg; }
  66% { rotate: -0.6deg; }
}

/* Hovering "disappear." makes the notes hop out (the word itself stays) */
@media (hover: hover) {
  .hero:has(.accent:hover) .note {
    translate: calc(var(--ex) * 1px) calc(var(--ey) * 1px);
    transition-delay: calc(var(--y) * 0.4ms);
  }
}

/* Same hop, on a tap */
.hero:has(.accent.is-on) .note {
  translate: calc(var(--ex) * 1px) calc(var(--ey) * 1px);
  transition-delay: calc(var(--y) * 0.4ms);
}

/* Nav: one white pill that slides to the active item ---------------------- */

.nav a {
  position: relative;
  transition:
    color 0.2s ease,
    background-color 0.2s ease,
    scale 0.2s var(--ease-out);
}

.nav a:active {
  scale: 0.94;
}

.nav__indicator {
  position: absolute;
  top: 4px;
  left: 0;
  height: 40px;
  border-radius: 20px;
  background: var(--accent);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s var(--ease-out);
}

/* Nothing is active until the first section arrives */
.nav--has-active .nav__indicator {
  opacity: 1;
}

/* The pill keeps the widest item's width; /scripts/main.js clips it down to the
   active item, so the move animates translate and clip-path, never layout */
.nav--pill-ready .nav__indicator {
  transition: translate 0.55s var(--ease-spring), clip-path 0.55s var(--ease-spring);
}

.nav--pill .is-active {
  background: transparent;
}

/* Selected works: the cards stack -----------------------------------------
   The title pins under the nav, and each card pins a little lower than the
   one before while the next slides over it. /scripts/motion.js shrinks a card as it
   gets covered, and lets the title travel with the stack once the last card
   has landed. */

.work {
  --works-title-top: calc(var(--nav-top) + var(--nav-height) + 28px);
  --works-card-top: calc(var(--works-title-top) + 30px + 28px);
  --works-step: 18px;
}

.js .work .section-title {
  position: sticky;
  top: var(--works-title-top);
  z-index: 2;
}

.js .work-list > li {
  position: sticky;
  top: calc(var(--works-card-top) + var(--stack-i, 0) * var(--works-step));
  /* Pad every item down to where the last card's bottom sits when stacked,
     so they all let go together; the next item's negative margin cancels it,
     so the layout does not move. /scripts/motion.js refines these by measurement. */
  padding-bottom: calc((2 - var(--stack-i, 0)) * var(--works-step));
}

.js .work-list > li:nth-child(2) {
  --stack-i: 1;
  margin-top: calc(-2 * var(--works-step));
}

.js .work-list > li:nth-child(3) {
  --stack-i: 2;
  margin-top: calc(-1 * var(--works-step));
}

.work-card {
  transform-origin: 50% 0;
}

@media (max-width: 719px) {
  .work {
    --works-title-top: calc(var(--nav-top) + var(--nav-height) + 16px);
    --works-card-top: calc(var(--works-title-top) + 30px + 16px);
    --works-step: 12px;
  }
}

/* Selected works ----------------------------------------------------------
   Layout lives in styles.css; what is left here is the sheen over an empty
   image slot, until the real images go in. */

.work-card__media:empty {
  position: relative;
  overflow: hidden;
}

.work-card__media:empty::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(105deg, transparent 35%, rgba(255, 255, 255, 0.55) 50%, transparent 65%);
  transform: translateX(-100%);
  animation: media-sheen 2.8s ease-in-out infinite;
}

@keyframes media-sheen {
  0% { transform: translateX(-100%); }
  60%, 100% { transform: translateX(100%); }
}

/* Scroll reveals (motion.js adds .is-in as each block enters the viewport) - */

/* Titles are anchor targets, so they fade from a blur without moving */
.js .section-title {
  transition: opacity 0.8s var(--ease-out), filter 0.8s var(--ease-out);
}

.js .section-title:not(.is-in) {
  opacity: 0;
  filter: blur(10px);
}

.js :is(.enjoy__title, .contact__text) {
  transition: opacity 0.9s var(--ease-out), transform 0.9s var(--ease-out);
}

.js :is(.enjoy__title, .contact__text):not(.is-in) {
  opacity: 0;
  transform: translateY(40px);
}

.js .paper {
  transition: opacity 1s var(--ease-out), transform 1.1s var(--ease-out);
}

.js .paper:not(.is-in) {
  opacity: 0;
  transform: translateY(80px) rotate(-2deg);
}

/* Groups cascade their children; /scripts/motion.js sets --d per child */
.js :is(.enjoy__list, .footer) > * {
  transition:
    opacity 0.7s var(--ease-out) var(--d, 0ms),
    transform 0.7s var(--ease-out) var(--d, 0ms);
}

.js :is(.enjoy__list, .footer):not(.is-in) > * {
  opacity: 0;
  transform: translateY(16px);
}

/* About copy ink: grey until the reading line passes (/scripts/motion.js) --------- */

.about-text.is-inking .ink {
  color: #aaa;
  transition: color 0.25s ease;
}

.about-text.is-inking .ink.is-inked {
  color: var(--ink);
}

/* Clientele: one row, running right to left ------------------------------- */

.clients__track {
  animation: client-marquee 46s linear infinite;
}

@media (hover: hover) {
  .clients:hover .clients__track {
    animation-play-state: paused;
  }
}

@keyframes client-marquee {
  to {
    transform: translateX(-50%);
  }
}


/* Resume link and footer -------------------------------------------------- */

/* Hand-drawn loop around the resume CTA and contact links (svg added by
   motion.js), drawn in on hover or keyboard focus */
.mark {
  position: absolute;
  top: -0.3em;
  left: -0.35em;
  width: calc(100% + 0.7em);
  height: calc(100% + 0.5em);
  overflow: visible;
  pointer-events: none;
  rotate: -3deg;
  /* Hidden outright until hover/focus, not just by the stroke dash */
  opacity: 0;
  transition: opacity 0.2s ease;
}

/* The resume words are small; give their loop more room */
.about-text .accent .mark {
  top: -0.6em;
  left: -0.7em;
  width: calc(100% + 1.4em);
  height: calc(100% + 1.2em);
}

.contact__text .mark {
  --mark: var(--accent);
}

.mark path {
  fill: none;
  stroke: var(--mark, currentColor);
  stroke-width: 2.5px;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  transition: stroke-dashoffset 0.3s var(--ease-out);
}

.contact__text .mark path {
  stroke-width: 3px;
}

.has-mark:focus-visible .mark {
  opacity: 1;
  transition-duration: 0s;
}

.has-mark:focus-visible .mark path {
  stroke-dashoffset: 0;
  transition-duration: 0.6s;
}

@media (hover: hover) {
  .has-mark:hover .mark {
    opacity: 1;
    transition-duration: 0s;
  }

  .has-mark:hover .mark path {
    stroke-dashoffset: 0;
    transition-duration: 0.6s;
  }
}

/* "Made with ..." — five reasons, one on screen at a time. Each takes a
   2.2s turn of the 11s loop, so the whole strip runs on one keyframe set. */
.made-emoji {
  opacity: 0;
  visibility: hidden;
  animation: made-emoji 11s linear infinite;
  animation-delay: calc(var(--i) * 2.2s);
}

@keyframes made-emoji {
  0% { opacity: 0; visibility: visible; }
  4%, 16% { opacity: 1; visibility: visible; }
  20% { opacity: 0; visibility: visible; }
  20.01%, 100% { opacity: 0; visibility: hidden; }
}

/* Figma-style multiplayer cursor (Figma 136:569) --------------------------
   /scripts/motion.js replaces the pointer with a "Me" cursor on mouse devices; over
   [data-cursor] elements the label becomes that text in a chat bubble. The
   element's origin is the pointer tip (the arrow's tip sits 2.7px into its
   24px box; the label sits 14px right and 13px down from the tip). */

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

.fig-cursor {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 1000;
  color: #fff;
  font-family: var(--font-sans);
  font-size: 14px;
  font-style: normal;
  font-weight: 500;
  line-height: normal;
  letter-spacing: -0.04em;
  text-align: left;
  text-transform: none;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.2s ease;
}

.fig-cursor--me.is-visible {
  opacity: 1;
}

.fig-cursor__pointer {
  position: absolute;
  top: -2.7px;
  left: -2.7px;
  width: 24px;
  max-width: none;
  height: 24px;
  transform-origin: 2.7px 2.7px;
  transition: scale 0.15s var(--ease-out);
}

.fig-cursor__label {
  position: absolute;
  top: 13px;
  left: 14px;
  padding: 4px 8px;
  border-radius: 8px;
  background: var(--cursor);
  white-space: nowrap;
  transform-origin: 0 0;
  transition:
    padding 0.25s var(--ease-out),
    border-radius 0.25s var(--ease-out),
    scale 0.15s var(--ease-out);
}

.fig-cursor.is-chat .fig-cursor__label {
  padding: 8px;
  border-radius: 4px 16px 16px 16px;
}

.fig-cursor.is-pressed .fig-cursor__pointer,
.fig-cursor.is-pressed .fig-cursor__label {
  scale: 0.9;
}

.fig-cursor--anees .fig-cursor__label {
  background: var(--accent);
}

/* Reduced motion: everything visible, nothing moving on its own ----------- */

@media (prefers-reduced-motion: reduce) {
  .js :is(
    .hero__title,
    .section-title,
    .paper,
    .enjoy__title,
    .contact__text,
    .enjoy__list > *,
    .work-list > *,
    .footer > *
  ) {
    opacity: 1 !important;
    filter: none !important;
    transform: none !important;
    transition: none !important;
  }

  .note,
  .is-split .word,
  .xp-badge img,
  .xp-orbit,
  .xp-tag,
  .work-card__media::after,
  .clients__track {
    animation: none !important;
  }

  /* No marquee: the logos just wrap */
  .clients {
    -webkit-mask-image: none;
    mask-image: none;
  }

  .clients__track {
    width: 100%;
    transform: none;
  }

  .clients__row {
    flex-wrap: wrap;
    justify-content: center;
    gap: 32px;
    padding-right: 0;
  }

  .clients__row[aria-hidden="true"] {
    display: none;
  }



  /* No cycling: the first reason just stays */
  .made-emoji {
    animation: none !important;
  }

  .made-emoji:first-of-type {
    opacity: 1;
    visibility: visible;
  }

  .note {
    transition: none;
  }

  .nav--pill-ready .nav__indicator {
    transition: none;
  }

  /* No fades without motion, so let the pinned hero scroll normally */
  .js .hero__title,
  .js .notes {
    position: relative;
  }
}
