/* ==========================================================================
   Motion layer.

   Reveal states, scroll-linked effects and keyframes. Keyframes and utilities
   that live in tailwind.css (ndi-float, marquee-track, twinkle, orbit-spin…)
   are not redefined here. Component *appearance* belongs in app.css; this file
   owns how things move and how they settle when motion is reduced.

   Rules of the house:
   - transform and opacity only, except the one-off clip-path image wipe;
   - 550–1000ms, cubic-bezier(0.22, 1, 0.36, 1), no bounce;
   - every effect resolves to its finished state under reduced motion and
     when scripting is off.
   ========================================================================== */

:root {
  --ease-out-expo: cubic-bezier(0.22, 1, 0.36, 1);
  --dur-reveal: 800ms;
}

/* --- 1. Reveal on scroll ---------------------------------------------------- */
[data-reveal] {
  opacity: 0;
  transform: translate3d(var(--reveal-x, 0), var(--reveal-y, 26px), 0);
  transition:
    opacity 700ms var(--ease-out-expo),
    transform 800ms var(--ease-out-expo);
  transition-delay: var(--reveal-delay, 0ms);
}
[data-reveal="left"]  { --reveal-x: -26px; --reveal-y: 0; }
[data-reveal="right"] { --reveal-x: 26px;  --reveal-y: 0; }
[data-reveal="up"]    { --reveal-x: 0;     --reveal-y: 26px; }
[data-reveal="scale"] { --reveal-y: 0; transform: scale(0.96); }
/* A card standing up from the table: tips back, then rises into place. */
[data-reveal="flip"] {
  transform: perspective(900px) translate3d(0, 40px, 0) rotateX(18deg) scale(0.96);
  transform-origin: 50% 100%;
  transition:
    opacity 800ms var(--ease-out-expo),
    transform 1000ms var(--ease-out-expo);
  transition-delay: var(--reveal-delay, 0ms);
}
[data-reveal].is-revealed { opacity: 1; transform: none; }

/* The class API: same states, for hand-written markup. */
.reveal, .reveal-up, .reveal-left, .reveal-right, .scale-reveal, .blur-reveal {
  opacity: 0;
  transition:
    opacity 700ms var(--ease-out-expo),
    transform 800ms var(--ease-out-expo);
  transition-delay: var(--reveal-delay, 0ms);
}
.reveal-up    { transform: translate3d(0, 26px, 0); }
.reveal-left  { transform: translate3d(-26px, 0, 0); }
.reveal-right { transform: translate3d(26px, 0, 0); }
.scale-reveal { transform: scale(0.95); }
.blur-reveal {
  transform: translate3d(0, 14px, 0);
  filter: blur(10px);
  transition:
    opacity 700ms var(--ease-out-expo),
    transform 800ms var(--ease-out-expo),
    filter 700ms ease;
}
.reveal.is-revealed, .reveal-up.is-revealed, .reveal-left.is-revealed,
.reveal-right.is-revealed, .scale-reveal.is-revealed, .blur-reveal.is-revealed {
  opacity: 1;
  transform: none;
}
.blur-reveal.is-revealed { filter: none; }

/* --- 2. Image reveal --------------------------------------------------------
   A wipe rather than a fade: the frame opens upward while the photograph
   settles from a slight zoom. Runs once. */
.image-reveal {
  clip-path: inset(0 0 100% 0 round var(--r-lg));
  transition: clip-path 1000ms var(--ease-out-expo);
  transition-delay: var(--reveal-delay, 0ms);
}
.image-reveal img {
  transform: scale(1.12);
  transition: transform 1400ms var(--ease-out-expo);
  transition-delay: var(--reveal-delay, 0ms);
}
.image-reveal.is-revealed { clip-path: inset(0 0 0 0 round var(--r-lg)); }
.image-reveal.is-revealed img { transform: none; }

/* --- 3. Headings: masked lines and word stagger ------------------------------ */
[data-reveal-line] {
  transform: translateY(110%);
  transition: transform 900ms var(--ease-out-expo);
  transition-delay: var(--line-delay, 0ms);
}
[data-reveal-text].is-revealed [data-reveal-line] { transform: translateY(0); }

/* Words are only hidden once animations.js has split them (.is-split), so a
   heading can never be left invisible by a script that did not run. */
.is-split .split-word {
  display: inline-block;
  opacity: 0;
  transform: translate3d(0, 0.45em, 0);
  transition:
    opacity 600ms var(--ease-out-expo),
    transform 800ms var(--ease-out-expo);
  transition-delay: calc(var(--reveal-delay, 0ms) + var(--w, 0) * 45ms);
}
.is-split.is-revealed .split-word { opacity: 1; transform: none; }

/* --- 4. Scroll-linked (--progress 0→1 written by animations.js) ------------- */
/* Campus panorama: settles from 1.08 to 1 as it rises through the viewport. */
[data-scroll-scale] {
  transform: scale(calc(1.08 - var(--progress, 0) * 0.08));
  transform-origin: 50% 40%;
}
/* Closing call to action: the panel converges toward the reader. */
[data-scroll-converge] {
  transform: scale(calc(0.94 + var(--progress, 1) * 0.06));
  opacity: calc(0.35 + var(--progress, 1) * 0.65);
}

/* Parallax layers. The rate lives in animations.js (capped at 30px). */
[data-parallax], .parallax-slow, .parallax-medium, .parallax-fast { backface-visibility: hidden; }

/* Top-of-page reading progress. */
.scroll-progress {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 70;
  height: 2px;
  pointer-events: none;
  transform-origin: 0 50%;
  transform: scaleX(var(--scroll, 0));
  background: linear-gradient(90deg, var(--color-primary), var(--color-primary-dark));
  box-shadow: 0 0 10px rgb(22 109 219 / 0.45);
}

/* --- 5. Pointer depth --------------------------------------------------------
   interactions.js writes --mx / --my (−0.5…0.5) on [data-scene]. Each layer
   declares its own depth; nothing here reads the DOM. */
[data-depth] {
  transform: translate3d(
    calc(var(--mx, 0) * var(--depth, 10) * -1px),
    calc(var(--my, 0) * var(--depth, 10) * -1px),
    0
  );
  transition: transform 120ms linear;
}
.hero-stage {
  transform:
    translate3d(calc(var(--mx, 0) * 8px), calc(var(--my, 0) * 6px), 0)
    rotateY(calc(var(--mx, 0) * 4deg))
    rotateX(calc(var(--my, 0) * -3deg));
  transform-style: preserve-3d;
  transition: transform 160ms linear;
}

/* --- 6. Tilt cards ---------------------------------------------------------- */
[data-tilt] {
  transition:
    transform 500ms var(--ease-out-expo),
    box-shadow 500ms ease;
}

/* --- 7. Ambient loops ------------------------------------------------------- */
.is-paused .float-slow,
.is-paused .float-mid,
.is-paused .mesh-drift,
.is-paused .orbit-dash,
.is-paused .hero-zoom { animation-play-state: paused; }

.float-slow  { animation: ndi-float 9s ease-in-out infinite; }
.float-mid   { animation: ndi-bob 6.5s ease-in-out infinite; }
.float-delay { animation-delay: -3s; }

@keyframes orbit-dash { to { stroke-dashoffset: -600; } }
.orbit-dash { animation: orbit-dash 60s linear infinite; }

@keyframes mesh-drift {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
  50%      { transform: translate3d(3%, -4%, 0) scale(1.06); }
}
.mesh-drift { animation: mesh-drift 22s ease-in-out infinite; }

/* A slow drift in scale on large photographic grounds. */
.hero-zoom {
  animation: hero-drift 26s ease-in-out infinite alternate;
  transform-origin: 60% 45%;
}
@keyframes hero-drift {
  from { transform: scale(1.02); }
  to   { transform: scale(1.1); }
}

/* The poster's entrance. Transform only, never opacity, so the image counts
   towards Largest Contentful Paint from its first frame. */
.hero-poster-enter { animation: hero-poster-in 1000ms var(--ease-out-expo) both; }
@keyframes hero-poster-in {
  from { transform: translate3d(0, 28px, 0) scale(0.985); }
  to   { transform: none; }
}

/* Hero slides: one visible at a time; waiting slides sit at an opacity no one
   can see rather than zero, so every slide is measured for LCP once, on load. */
.hero-slide {
  opacity: 0;
  visibility: hidden;
  transform: scale(1.06);
  transition:
    opacity 900ms var(--ease-out-expo),
    transform 1600ms var(--ease-out-expo);
  pointer-events: none;
}
.hero-slide[data-active="true"] {
  opacity: 1;
  visibility: visible;
  transform: none;
  pointer-events: auto;
}

.hero-scroll-dot { animation: hero-scroll-fall 2.2s ease-in-out infinite; }
@keyframes hero-scroll-fall {
  0%, 100% { transform: translateY(0); opacity: 1; }
  50%      { transform: translateY(0.55rem); opacity: 0.2; }
}

.marquee-mask {
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent);
}
.group\/marquee:hover .marquee-track,
.group\/marquee:focus-within .marquee-track { animation-play-state: paused; }

/* Skeleton shimmer behind image frames until the photograph arrives. */
@keyframes skel { to { background-position: -200% 0; } }
.media-skel { background-color: var(--color-bg-section); }
.js .media-skel:not(.is-loaded) {
  background: linear-gradient(100deg, var(--color-bg-section) 30%, var(--color-bg-soft) 50%, var(--color-bg-section) 70%);
  background-size: 200% 100%;
  animation: skel 1.6s linear infinite;
}

/* --- 8. Overlays: lightbox, modal, dropdowns, drawer, FAQ -------------------- */
.lightbox:not([hidden]),
.review-player:not([hidden]) { display: flex; }
.lightbox > [data-lightbox-close],
.lightbox .lightbox-figure,
.lightbox [data-lightbox-prev],
.lightbox [data-lightbox-next] { opacity: 0; transition: opacity 300ms ease; }
.lightbox .lightbox-figure {
  transform: scale(0.96);
  transition: opacity 320ms ease, transform 420ms var(--ease-out-expo);
}
.lightbox.is-open > [data-lightbox-close],
.lightbox.is-open [data-lightbox-prev],
.lightbox.is-open [data-lightbox-next] { opacity: 1; }
.lightbox.is-open .lightbox-figure { opacity: 1; transform: none; }

.lead-modal:not([hidden]) { display: flex; }
.lead-modal .lead-modal-backdrop { opacity: 0; transition: opacity 300ms ease; }
.lead-modal .lead-modal-panel {
  opacity: 0;
  transform: translateY(18px) scale(0.975);
  transition:
    opacity 420ms var(--ease-out-expo),
    transform 420ms var(--ease-out-expo);
}
.lead-modal.is-open .lead-modal-backdrop { opacity: 1; }
.lead-modal.is-open .lead-modal-panel { opacity: 1; transform: none; }
@media (max-width: 639px) {
  /* A phone gets a sheet: full width, from the bottom edge, scrolling inside
     itself so the on-screen keyboard can never push a field out of reach. */
  .lead-modal { align-items: flex-end; padding: 0; }
  .lead-modal .lead-modal-panel {
    max-height: 92vh;
    width: 100%;
    max-width: none;
    overflow-y: auto;
    border-radius: 1.75rem 1.75rem 0 0;
    transform: translateY(24px);
    padding-bottom: env(safe-area-inset-bottom);
  }
  .lead-modal.is-open .lead-modal-panel { transform: none; }
}
.lead-success-mark { animation: lead-success-pop 560ms var(--ease-out-expo) both; }
@keyframes lead-success-pop {
  0%   { transform: scale(0.6); opacity: 0; }
  100% { transform: scale(1); opacity: 1; }
}
.elig-answer { animation: elig-in 460ms var(--ease-out-expo) both; }
@keyframes elig-in {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

.dropdown-panel[hidden] { display: none !important; }
.dropdown-panel {
  opacity: 0;
  transform: translate(-50%, -8px) scale(0.97);
  transition: opacity 240ms var(--ease-out-expo), transform 300ms var(--ease-out-expo);
}
.dropdown-panel.is-open { opacity: 1; transform: translate(-50%, 0) scale(1); }
.dropdown-panel--wide { transform: translateY(-8px) scale(0.985); }
.dropdown-panel--wide.is-open { transform: translateY(0) scale(1); }

/* Mobile drawer: fades and settles from slightly above; links follow. */
.mobile-sheet {
  opacity: 0;
  transform: translateY(-12px);
  transition:
    opacity 320ms var(--ease-out-expo),
    transform 420ms var(--ease-out-expo);
}
.mobile-sheet.is-open { opacity: 1; transform: none; }
.mobile-sheet nav li,
.mobile-sheet nav > p {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 420ms ease, transform 520ms var(--ease-out-expo);
}
.mobile-sheet.is-open nav li,
.mobile-sheet.is-open nav > p {
  opacity: 1;
  transform: none;
  transition-delay: calc(80ms + var(--i, 0) * 32ms);
}

.faq-answer {
  display: grid;
  grid-template-rows: 1fr;
  transition: grid-template-rows 420ms var(--ease-out-expo), opacity 350ms ease;
}
.faq-answer > * { overflow: hidden; }
.faq-answer.is-collapsing { grid-template-rows: 0fr; opacity: 0; }

/* Admission journey rail: scaled, never resized, so its length is always a
   fraction of the track however wide the section is. */
.journey-fill {
  transform: scaleY(0);
  transform-origin: top center;
  transition: transform 800ms var(--ease-out-expo);
}
@media (min-width: 1024px) {
  .journey-fill { transform: scaleX(0); transform-origin: left center; }
}
.journey-marker {
  transition:
    border-color 400ms ease,
    background-color 400ms ease,
    color 400ms ease,
    box-shadow 400ms ease,
    transform 500ms var(--ease-out-expo);
}
.journey-index { transition: color 400ms ease; }
.routine-dot { transition: box-shadow 400ms ease, transform 400ms var(--ease-out-expo); }

/* Pinned horizontal scroll (residential story). */
[data-hscroll].is-pinned [data-hscroll-viewport] {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
}
[data-hscroll].is-pinned [data-hscroll-track] {
  width: max-content;
  overflow: visible;
  padding-left: 6vw;
  padding-right: 6vw;
  scroll-snap-type: none;
  will-change: transform;
}

/* --- 9. Page transitions ------------------------------------------------------
   Cross-document view transitions: the browser cross-fades the outgoing and
   incoming pages itself. Nothing waits on it — a browser without support
   simply navigates as before. */
@view-transition { navigation: auto; }
::view-transition-old(root) { animation: page-out 220ms var(--ease-soft, ease) both; }
::view-transition-new(root) { animation: page-in 420ms var(--ease-out-expo) both; }
@keyframes page-out { to { opacity: 0; transform: translateY(-6px); } }
@keyframes page-in  { from { opacity: 0; transform: translateY(10px); } }

/* ==========================================================================
   No JavaScript — reveal states start hidden and are cleared by the observer.
   `scripting: none` resolves them without an inline script (the CSP allows
   scripts only from this origin). Declared in Tailwind's `base` layer so
   !important here outranks Tailwind's own `[hidden]` rule.
   ========================================================================== */
@layer base {
@media (scripting: none), (scripting: initial-only) {
  [data-reveal], [data-reveal-text],
  .reveal, .reveal-up, .reveal-left, .reveal-right, .scale-reveal, .blur-reveal {
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
    transition: none !important;
  }
  .image-reveal { clip-path: none !important; }
  .image-reveal img { transform: none !important; }
  [data-reveal-line] { transform: none !important; }
  [data-cursor] { display: none !important; }
  .faq-answer { grid-template-rows: 1fr !important; opacity: 1 !important; }
  [data-accordion-panel][hidden] { display: block !important; }
  [data-story-marker] { background: var(--color-primary); color: #fff; }
}
}

/* ==========================================================================
   Reduced motion — everything resolves to its final state. Parallax, tilt,
   magnetic pull and pointer depth are also switched off in the scripts.
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  [data-reveal], [data-reveal].is-revealed,
  .reveal, .reveal-up, .reveal-left, .reveal-right, .scale-reveal, .blur-reveal {
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
    transition: none !important;
  }
  [data-reveal-line] { transform: none; transition: none; }
  .is-split .split-word { opacity: 1; transform: none; transition: none; }
  .image-reveal, .image-reveal.is-revealed { clip-path: none; transition: none; }
  .image-reveal img { transform: none; transition: none; }

  [data-scroll-scale], [data-scroll-converge] { transform: none; opacity: 1; }
  [data-parallax], .parallax-slow, .parallax-medium, .parallax-fast { transform: none !important; }
  [data-depth], .hero-stage, .hero-cursor-glow { transform: none !important; }
  .hero-swoosh path { stroke-dashoffset: 0; animation: none; }
  .hero .hero-accent::after { transform: scaleX(1); animation: none; }
  .hero-cube, .hero-gem-cube, .hero-gem-bob, .hero-back-frame { animation: none; }
  .hero-geo { animation: none !important; }
  [data-tilt] { transform: none !important; }

  .float-slow, .float-mid, .orbit-dash, .mesh-drift, .hero-zoom,
  .hero-poster-enter, .hero-scroll-dot, .marquee-track,
  .twinkle, .orbit-spin, .media-skel, .elig-answer, .lead-success-mark { animation: none !important; }
  .hero-slide { transition: opacity 200ms linear; transform: none; }

  .lightbox .lightbox-figure { transform: none; }
  .mobile-sheet, .mobile-sheet.is-open,
  .mobile-sheet nav li, .mobile-sheet nav > p { transition: none; transform: none; }

  [data-hscroll].is-pinned [data-hscroll-viewport] { position: static; height: auto; overflow: visible; }
  [data-hscroll].is-pinned [data-hscroll-track] { transform: none !important; }
  [data-cursor] { display: none !important; }

  ::view-transition-old(root), ::view-transition-new(root) { animation: none; }
}
