/* ==========================================================================
   Fluentbyte — motion layer
   Scroll-driven scenes, reveals, pinned sequences.

   Every scrubbed animation reads a `--p` (0 → 1) custom property that
   js/motion.js writes on each frame. CSS owns the look; JS owns the timing.
   ========================================================================== */

/* ------------------------------------------------------------- reveals -- */

[data-reveal] {
  opacity: 0;
  transform: translate3d(0, 28px, 0);
  filter: blur(8px);
  transition:
    opacity 900ms var(--ease-out),
    transform 900ms var(--ease-out),
    filter 900ms var(--ease-out);
  transition-delay: var(--delay, 0ms);
  will-change: opacity, transform, filter;
}

/* A reveal fires once. Holding the hint afterwards pins a composited layer
   per element for the life of the page — on the home page that is ~30 of
   them, blurred, which is what made scrolling it on a phone feel syrupy. */
[data-reveal].is-in {
  will-change: auto;
}

[data-reveal='scale'] {
  transform: translate3d(0, 40px, 0) scale(0.94);
}
[data-reveal='left'] {
  transform: translate3d(-36px, 0, 0);
}
[data-reveal='right'] {
  transform: translate3d(36px, 0, 0);
}
[data-reveal='fade'] {
  transform: none;
  filter: blur(4px);
}

[data-reveal].is-in {
  opacity: 1;
  transform: none;
  filter: blur(0);
}

/* Staggered children: motion.js stamps --delay per child. */
[data-stagger] > * {
  opacity: 0;
  transform: translate3d(0, 22px, 0);
  transition:
    opacity 760ms var(--ease-out),
    transform 760ms var(--ease-out);
  transition-delay: var(--delay, 0ms);
}
[data-stagger].is-in > * {
  opacity: 1;
  transform: none;
}

/* Word-by-word headline reveal. */
.split-word {
  display: inline-block;
  opacity: 0;
  transform: translate3d(0, 0.5em, 0) rotateX(-40deg);
  transform-origin: 50% 100%;
  transition:
    opacity 700ms var(--ease-out),
    transform 700ms var(--ease-out);
  transition-delay: var(--delay, 0ms);
}
.is-in .split-word {
  opacity: 1;
  transform: none;
}

/* ---------------------------------------------------------------- hero -- */

.hero {
  position: relative;
  min-height: 100svh;
  display: grid;
  place-items: center;
  text-align: center;
  padding: calc(var(--nav-h) + 3rem) 0 4rem;
  overflow: clip;
}

/* Slow aurora wash behind the hero. Pure CSS, no canvas, cheap to composite. */
.hero-aura {
  position: absolute;
  inset: -25%;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(38% 42% at 22% 30%, rgba(255, 106, 61, 0.3), transparent 62%),
    radial-gradient(34% 40% at 78% 26%, rgba(247, 223, 30, 0.2), transparent 60%),
    radial-gradient(46% 48% at 62% 74%, rgba(56, 161, 255, 0.3), transparent 64%),
    radial-gradient(40% 40% at 28% 78%, rgba(75, 201, 138, 0.24), transparent 62%);
  filter: blur(38px) saturate(140%);
  /* The drift loop is gone. It communicated nothing and ran for as long as
     the page was open, in the corner of the eye of anyone reading. */
  transform: translate3d(-2%, -1%, 0);
  opacity: 0.9;
}

@keyframes aura-drift {
  0% {
    transform: translate3d(-2%, -1%, 0) scale(1) rotate(0deg);
  }
  50% {
    transform: translate3d(3%, 2%, 0) scale(1.08) rotate(4deg);
  }
  100% {
    transform: translate3d(-3%, 3%, 0) scale(1.03) rotate(-3deg);
  }
}

.hero-inner {
  position: relative;
  z-index: 1;
  /* Scrubbed by motion.js: the hero recedes as the page scrolls past it. */
  transform: translate3d(0, calc(var(--p, 0) * -14vh), 0) scale(calc(1 - var(--p, 0) * 0.14));
  opacity: calc(1 - var(--p, 0) * 1.25);
  filter: blur(calc(var(--p, 0) * 14px));
  will-change: transform, opacity, filter;
}

/* The hero headline is one <h1> rendered as two independently animated
   lines, so the document keeps a single top-level heading. */
.hero-line {
  display: block;
  opacity: 0;
  transform: translate3d(0, 0.24em, 0);
  animation: hero-line-in 900ms var(--ease-out) 80ms both;
  will-change: opacity, transform;
}
.hero-line + .hero-line {
  animation-delay: 210ms;
}
@keyframes hero-line-in {
  0% {
    opacity: 0;
    transform: translate3d(0, 0.24em, 0);
  }
  55% {
    opacity: 1;
  }
  100% {
    opacity: 1;
    transform: translate3d(0, 0, 0);
  }
}

.hero-cta {
  justify-content: center;
  margin-top: 2rem;
}

@media (max-width: 560px) {
  .hero {
    padding-top: calc(var(--nav-h) + 2rem);
  }
  .hero-inner .lede {
    font-size: 1.05rem;
  }
  .hero-cta {
    display: grid;
    grid-template-columns: 1fr;
    width: 100%;
    gap: 0.65rem;
  }
  .hero-cta .btn {
    width: 100%;
    justify-content: center;
  }
}

/* ------------------------------------------------------------ sections -- */

.section {
  position: relative;
  padding: clamp(5rem, 14vh, 9rem) 0;
}
.section-tight {
  padding: clamp(3rem, 8vh, 5rem) 0;
}

.section-head {
  max-width: 44rem;
  margin-bottom: 3rem;
}
.section-head.center {
  margin-inline: auto;
}
.section-head .lede {
  max-width: 42ch;
  margin-top: 1rem;
}

/* --------------------------------------------------------- path builder -- */

.path-builder {
  display: grid;
  grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr);
  gap: clamp(1.5rem, 5vw, 4rem);
  padding: clamp(1.5rem, 4vw, 3rem);
  border-radius: var(--radius-lg);
  background: var(--bg-card);
  box-shadow: inset 0 0 0 1px var(--hairline);
}
.path-intro .display {
  margin: 0.55rem 0 1rem;
  font-size: clamp(1.8rem, 4vw, 3rem);
}
.path-controls {
  display: grid;
  gap: 1.35rem;
}
.path-controls fieldset {
  border: 0;
  padding: 0;
  margin: 0;
  min-width: 0;
}
.path-controls legend {
  margin-bottom: 0.65rem;
  font-size: var(--step-sm);
  font-weight: 650;
}
.path-controls legend span {
  display: inline-grid;
  place-items: center;
  width: 1.6rem;
  height: 1.6rem;
  margin-right: 0.45rem;
  border-radius: 50%;
  background: var(--hairline);
  font-family: var(--mono);
  font-size: var(--step-xs);
}
.path-choices {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}
.path-choices button {
  min-height: 44px;
  padding: 0.65rem 0.9rem;
  border: 0;
  border-radius: 999px;
  cursor: pointer;
  background: var(--hairline-soft);
  box-shadow: inset 0 0 0 1px var(--hairline);
  color: var(--text-dim);
  font: inherit;
  font-size: var(--step-sm);
  transition:
    transform var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast),
    color var(--dur-fast),
    box-shadow var(--dur-fast);
}
.path-choices button[aria-pressed='true'] {
  color: var(--bg);
  background: var(--text);
  box-shadow: none;
}
.path-result {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 1rem;
  min-height: 9rem;
  padding: 1.25rem;
  border-radius: 18px;
  background: var(--bg-raise);
  box-shadow: inset 0 0 0 1px var(--hairline-soft);
}
.path-result .title {
  margin: 0.25rem 0;
}
.path-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 0.6rem;
}
.diagnostic {
  width: 100%;
}
.diagnostic-head {
  justify-content: space-between;
  margin-bottom: 0.8rem;
}
.diagnostic-options {
  display: grid;
  gap: 0.5rem;
  margin: 1rem 0;
}
.diagnostic-options button {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  min-height: 48px;
  padding: 0.7rem 0.85rem;
  border: 0;
  border-radius: 12px;
  background: var(--hairline-soft);
  color: var(--text);
  text-align: left;
  font: inherit;
  cursor: pointer;
  box-shadow: inset 0 0 0 1px var(--hairline);
  transition:
    transform var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast);
}
.diagnostic-options button span {
  display: grid;
  place-items: center;
  width: 1.7rem;
  height: 1.7rem;
  flex: none;
  border-radius: 50%;
  background: var(--hairline);
  font-family: var(--mono);
  font-size: var(--step-xs);
}
.link-button {
  border: 0;
  padding: 0.35rem 0;
  background: none;
  color: var(--link);
  font: inherit;
  font-size: var(--step-sm);
  cursor: pointer;
}
.path-tracks {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 0.8rem;
}
.path-result[data-ready='true'] {
  animation: path-ready 480ms var(--ease-out);
}
@keyframes path-ready {
  from {
    opacity: 0.5;
    transform: translateY(8px) scale(0.99);
  }
}

@media (hover: hover) and (pointer: fine) {
  .path-choices button:hover {
    transform: translateY(-2px);
    color: var(--text);
    background: var(--hairline);
  }
  .path-choices button[aria-pressed='true']:hover {
    color: var(--bg);
    background: var(--text);
  }
}
@media (max-width: 760px) {
  .path-builder {
    grid-template-columns: 1fr;
  }
  .path-result {
    grid-template-columns: 1fr;
  }
  .path-result .btn {
    justify-content: center;
    width: 100%;
  }
  .path-actions {
    display: grid;
  }
}

/* ------------------------------------------------- pinned scene (Apple) -- */
/* A tall spacer holds scroll distance; the inner panel sticks to the
   viewport and cross-fades its frames as --p sweeps 0 → 1.               */

.pin {
  position: relative;
  /* height set inline per scene, e.g. height: 340vh */
}

.pin-stage {
  position: sticky;
  top: 0;
  height: 100svh;
  display: grid;
  place-items: center;
  overflow: clip;
}

.pin-frame {
  grid-area: 1 / 1;
  /* Arithmetic inside min() must be wrapped in calc(). Without it the whole
     declaration is invalid, so code blocks gave the frame an intrinsic width
     wider than an iPhone viewport while body overflow merely hid the damage. */
  width: min(calc(100% - var(--gutter) * 2), 1000px);
  min-width: 0;
  opacity: 0;
  transform: translate3d(0, 30px, 0) scale(0.97);
  filter: blur(10px);
  transition:
    opacity 520ms var(--ease-out),
    transform 620ms var(--ease-out),
    filter 520ms var(--ease-out);
  pointer-events: none;
  will-change: opacity, transform, filter;
}
.pin-frame.is-active {
  opacity: 1;
  transform: none;
  filter: blur(0);
  pointer-events: auto;
}
.pin-frame.is-past {
  opacity: 0;
  transform: translate3d(0, -30px, 0) scale(1.03);
  filter: blur(10px);
}

/* Frame counter dots on the right edge of a pinned scene. */
/* Two-column frames.

   The first pinned scene fills its 1000px frame with a full-width code block,
   so the composition reads as a column. The second scene's frames carry a
   small demo card instead — 460px against a 1000px frame — and stacking it
   under the heading left the right half of the frame, plus the page margin,
   empty: about 800px of nothing beside a headline. Side by side at desktop
   widths uses the frame the same way scene one does.

   Below 900px this rule does not apply and the frame is a plain stack, which
   is also what the flat/reduced-motion layout wants. */
@media (min-width: 900px) {
  .pin-frame.split {
    display: grid;
    grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
    align-items: center;
    column-gap: clamp(2rem, 5vw, 4rem);
  }
  /* The card sizes itself to its column now, so the per-frame max-widths that
     used to hold it in are gone from the markup. */
  .pin-frame.split > .card {
    width: 100%;
  }
}

/* Stacked, the card needs the breathing room the inline margin used to give. */
@media (max-width: 899px) {
  .pin-frame.split > .card {
    margin-top: 2rem;
  }
}

.pin-dots {
  position: absolute;
  right: clamp(0.75rem, 3vw, 2rem);
  top: 50%;
  translate: 0 -50%;
  display: grid;
  gap: 0.6rem;
  z-index: 2;
}
.pin-dots b {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--hairline);
  transition:
    background-color var(--dur) var(--ease-soft),
    transform var(--dur) var(--ease-out);
}
.pin-dots b.on {
  background: var(--text);
  transform: scale(1.6);
}

/* --------------------------------------------------------- scrubbed bits */

/* Progress-driven code typing: a clip-path sweeps the block as you scroll. */
.type-scrub {
  position: relative;
  --tp: var(--p, 0);
}
.type-scrub .type-mask {
  clip-path: inset(0 calc((1 - var(--tp, 0)) * 100%) 0 0);
}
.type-scrub .caret {
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(var(--tp, 0) * 100%);
  width: 2px;
  background: var(--js);
  opacity: calc(1 - var(--tp, 0) * var(--tp, 0));
}
@keyframes caret-blink {
  50% {
    opacity: 0.15;
  }
}

/* Parallax layers: depth is a data attribute, motion.js does the math. */
[data-parallax] {
  transform: translate3d(0, calc(var(--shift, 0) * 1px), 0);
  will-change: transform;
}

/* Counters that roll up when scrolled into view. */
.stat-num {
  font-variant-numeric: tabular-nums;
  font-size: var(--step-xl);
  font-weight: 700;
  letter-spacing: -0.04em;
  line-height: 1;
}

/* ------------------------------------------------------- track showcase -- */

.track-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 1rem;
}

.track-card {
  position: relative;
  display: grid;
  align-content: start;
  gap: 0.6rem;
  padding: 1.5rem 1.4rem 1.7rem;
  border-radius: var(--radius-lg);
  background: var(--bg-card);
  box-shadow: inset 0 0 0 1px var(--hairline-soft);
  overflow: hidden;
  isolation: isolate;
  transition:
    transform var(--dur) var(--ease-out),
    box-shadow var(--dur) var(--ease-soft);
  will-change: transform;
}
.track-card::before {
  content: '';
  position: absolute;
  inset: -40% -20% auto -20%;
  height: 70%;
  z-index: -1;
  background: radial-gradient(60% 100% at 50% 100%, var(--tint), transparent 70%);
  opacity: 0.55;
  transition:
    opacity var(--dur) var(--ease-soft),
    transform var(--dur) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .track-card:hover {
    transform: translateY(-6px);
    box-shadow:
      inset 0 0 0 1px var(--hairline),
      var(--shadow);
  }
  .track-card:hover::before {
    opacity: 0.95;
    transform: translateY(8px) scale(1.06);
  }
}
@media (hover: none) {
  .track-card:active {
    transform: scale(0.985);
    transition-duration: 0ms;
  }
}

.track-card .glyph {
  width: 46px;
  height: 46px;
  display: grid;
  place-items: center;
  border-radius: 13px;
  background: var(--hairline-soft);
  box-shadow: inset 0 0 0 1px var(--hairline);
  font-family: var(--mono);
  font-size: 15px;
  font-weight: 700;
  color: var(--tint);
  margin-bottom: 0.4rem;
}

.track-card[data-track='html'] {
  --tint: var(--html);
}
.track-card[data-track='css'] {
  --tint: var(--css);
}
.track-card[data-track='js'] {
  --tint: var(--js);
}
.track-card[data-track='python'] {
  --tint: var(--python);
}

.meter {
  height: 4px;
  border-radius: 999px;
  background: var(--hairline);
  overflow: hidden;
}
.meter i {
  display: block;
  height: 100%;
  width: 100%;
  border-radius: inherit;
  background: var(--tint, var(--text));
  transform: scaleX(var(--v, 0));
  transform-origin: 0 50%;
  transition: transform 1s var(--ease-out);
}

/* ---------------------------------------------------------- device frame */

.device {
  position: relative;
  border-radius: 22px;
  background: var(--bg-card);
  box-shadow:
    inset 0 0 0 1px var(--hairline),
    var(--shadow);
  overflow: hidden;
}
.device-bar {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.7rem 0.9rem;
  box-shadow: inset 0 -1px 0 var(--hairline-soft);
}
.device-bar s {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--hairline);
}
.device-bar em {
  margin-left: 0.6rem;
  font-style: normal;
  font-size: var(--step-xs);
  color: var(--text-mute);
  font-family: var(--mono);
}

/* --------------------------------------------------------- marquee strip */

.marquee {
  overflow: hidden;
  mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}
.marquee > div {
  display: flex;
  gap: 2.5rem;
  width: max-content;
  /* Was a 42s loop. A strip of words sliding past forever is motion with
     nothing to say, and it sat in the periphery of the whole page. */
}
@keyframes slide {
  to {
    transform: translateX(-50%);
  }
}

.marquee span {
  font-family: var(--mono);
  font-size: var(--step-sm);
  color: var(--text-mute);
  white-space: nowrap;
}

/* ------------------------------------------------------- reduced motion -- */

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

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

  [data-reveal],
  [data-stagger] > *,
  .split-word,
  .pin-frame {
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
    will-change: auto !important;
  }
  .hero-line {
    opacity: 1;
    transform: none;
    animation: none;
    will-change: auto;
  }

  .hero-inner {
    transform: none !important;
    opacity: 1 !important;
    filter: none !important;
  }

  .type-scrub .type-mask {
    clip-path: none;
  }
  .type-scrub .caret {
    display: none;
  }
}

/* --------------------------------------------------- flat (unpinned) scenes

   Three conditions want the same result, so they share one rule set rather
   than drifting apart:

     · a reader who asked for less motion;
     · a phone, where the frames stack and animate on entry instead of using
       a screen-height sticky stage;
     · any short viewport — which is what a phone in landscape is. That case
       used to fall through to the pinned layout and break outright: the
       stage is `height: 100svh; overflow: clip`, so at 390px of height the
       heading, the lede and the code frame were simply cut off, with no
       scroll available to reach them. Width alone could not see it.

   js/motion.js applies the same OR and skips registering pins, so the rAF
   loop is not driving elements that no longer move.                       */

@media (prefers-reduced-motion: reduce), (max-width: 560px), (max-height: 640px) {
  .pin {
    height: auto !important;
  }
  .pin-stage {
    position: static;
    height: auto;
    display: grid;
    gap: 3rem;
    padding: 2rem 0;
  }
  .pin-frame {
    grid-area: auto;
    width: calc(100% - var(--gutter) * 2);
    max-width: 1000px;
    min-width: 0;
    margin-inline: auto;
    pointer-events: auto;
    will-change: opacity, transform;
  }
  .pin-frame > *,
  .pin-frame .frame-copy,
  .pin-frame .device,
  .pin-frame .code {
    min-width: 0;
    max-width: 100%;
  }
  .pin-frame .lede {
    overflow-wrap: anywhere;
  }
  .pin-dots {
    display: none;
  }
}

@media (max-width: 560px), (max-height: 640px) {
  .pin-frame {
    opacity: 0;
    transform: translate3d(0, 24px, 0) scale(0.985);
    filter: none;
    transition:
      opacity 680ms var(--ease-out),
      transform 680ms var(--ease-out);
  }
  .pin-frame.is-in {
    opacity: 1;
    transform: none;
    will-change: auto;
  }
}

/* ------------------------------------------------------- mobile budget --

   Scroll-linked `filter: blur()` is the one effect here that cannot be
   composited: the layer is re-rasterised on every frame it changes, and on a
   phone GPU that alone accounted for most of the dropped frames scrolling
   past the hero. Transform and opacity carry the same idea for free, so the
   blur is what goes. The aura keeps its slow composited transform so mobile
   still receives the full motion language without per-frame raster work. */

@media (hover: none) and (pointer: coarse), (max-width: 560px) {
  .hero-inner {
    filter: none;
  }

  .hero-aura {
    filter: blur(30px) saturate(130%);
    opacity: 0.7;
  }

  /* Same reasoning one level down: 30-odd elements each transitioning a blur
     on entry, while the page is being flung. Fade and slide read identically
     at phone scale. */
  [data-reveal] {
    filter: none;
    transition:
      opacity 620ms var(--ease-out),
      transform 620ms var(--ease-out);
    will-change: opacity, transform;
  }
  [data-reveal='fade'] {
    filter: none;
  }
  [data-reveal].is-in {
    filter: none;
  }
  .pin-frame {
    filter: none;
  }
}
