/* Educircle Website V3 — Immersive homepage
   =========================================
   The homepage as one continuous scroll-driven story: possibility → overwhelm →
   understanding → plan → discovery → comparison → application → arrival →
   connected ecosystem. Depends on tokens.css + components.css.

   ARCHITECTURE — read this before editing
   ---------------------------------------
   1. PINNING IS NATIVE `position: sticky`, never a JS pin. GSAP/ScrollTrigger only
      MEASURES progress and writes transforms; it never takes an element out of flow.
      This is the same decision sage-journey.js documents, and it is why resizing,
      orientation changes, back/forward restore and keyboard scrolling stay stable —
      there is no pin-spacer to get out of sync.

   2. EVERY HIDDEN STATE IS GATED ON `html.js-anim`, the class website-v2.js adds only
      after it has confirmed GSAP is present and motion is allowed. If the script is
      blocked, fails to parse or throws, NONE of these rules match and every scene is
      a plain, fully readable section. Content is never hidden by a script that might
      not run. (components.css and motion.css state the same rule; this file obeys it.)

   3. THE SCROLL RUNWAYS ARE ALSO GATED. A pinned scene needs several viewports of
      scroll to play through, but without JS that runway would just be dead space, so
      the tall `min-height` only exists under `html.js-anim` too.

   4. REDUCED MOTION resolves everything to its final, readable state — see the block
      at the end of this file. Nothing is hidden, nothing scrubs, nothing floats.
*/

/* ============================================================================
   MOTION TOKENS — one place, so no scene invents its own curve
   ============================================================================
   Duration/easing primitives already live in tokens.css (--dur-*, --ease-*) and are
   reused as-is. What is added here is what only the immersive layer needs: depth,
   parallax strength and scene runway lengths. Scoped to this file because only the
   homepage loads it.
*/
:root {
  /* Scene runway: how many viewport-heights of scroll a pinned scene occupies.
     A value of 200 means the scene is 2 viewports tall, so the sticky panel has
     exactly ONE viewport of scrolling to play through — roughly two or three wheel
     turns. That is the measured sweet spot between "too fast to read" and "why is
     this page so long".

     These numbers are load-bearing for total page length. Measured at 1440x900:
     the eight scenes come to ~13 viewports and the whole page to ~17, against
     ~14 for the classic section-stack homepage it replaces. Raising them is the
     fastest way to make the page feel endless — the brief's words — so change them
     with a measurement, not a hunch. */
  --imm-runway-short: 160;
  --imm-runway: 200;
  --imm-runway-long: 240;

  /* Depth planes. Foreground moves most, background least — the parallax ratio
     is applied by immersive.js, these are the design values it reads. */
  --imm-depth-fg: 1;
  --imm-depth-mid: 0.55;
  --imm-depth-bg: 0.22;

  /* Pointer-response strength. Deliberately small: the brief asks for a subtle
     camera response, not elements chasing the cursor. */
  --imm-pointer: 10px;

  /* The immersive world's palette — brand navy, deepened for atmosphere. */
  --imm-void: #04162B;
  --imm-deep: #041F3D;
  --imm-navy: #022C54;
  --imm-glow: rgba(13, 202, 210, 0.55);   /* Sage teal, from --grad-sage */
  --imm-glow-soft: rgba(0, 129, 250, 0.28);
  --imm-line: rgba(255, 255, 255, 0.12);
  --imm-line-strong: rgba(255, 255, 255, 0.22);
}

/* ============================================================================
   THE RUN — one continuous world behind the dark scenes
   ============================================================================ */
/* The navy world sits on the RUN itself, not on each dark scene.
   ---------------------------------------------------------------
   This is the layering that lets the WebGL object be seen at all:

     .imm            navy backdrop, in normal flow  (bottom)
     .imm__stage     the canvas, position: fixed, z-index 1
     .imm-scene      content, z-index 2             (top)

   The dark scenes are therefore TRANSPARENT — an opaque background on them would
   paint straight over the canvas, which is exactly what happened at first: the
   renderer compiled, sized, ran its loop and drew a perfect orb into a layer that
   nothing could see.

   Keeping the backdrop here rather than on a second fixed layer also bounds it to
   the run's own box, so it cannot bleed behind the editorial sections that follow. */
.imm { position: relative; background: var(--imm-deep); }

/* The persistent canvas layer. FIXED so a single WebGL scene carries across every
   dark scene (the object that travels with you is what makes it one story rather
   than eight sections). It is decorative: aria-hidden, never focusable, and it must
   never intercept a click on a CTA. */
.imm__stage {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dur-cinematic) var(--ease-out);
  contain: strict;      /* the canvas can never trigger layout work on the page */

  /* Composite the neon INTO the hero photograph rather than on top of it. The
     renderer already blends additively within itself; without this the result is
     still a flat sticker pasted over the harbour, which is exactly how it read.
     `screen` lets the bright sky push back through the particles, so the object
     sits in the scene and its edges dissolve instead of stopping dead.

     It works because .imm is not a stacking context (position: relative, z-index:
     auto), so the blend group is the root and the backdrop is the hero photo. The
     `contain: strict` above isolates this element's CHILDREN, not the element
     itself, so the two do not fight. */
  mix-blend-mode: screen;
}
.imm.is-stage-on .imm__stage { opacity: 1; }
.imm__stage canvas { display: block; width: 100%; height: 100%; }

/* The object is grabbable, and the cursor should say so — but only where saying so
   is safe. `is-orb-hot` is set by immersive.js when the cursor is over the object
   AND over bare background, never over copy or a link, because `cursor` inherits
   and would otherwise turn a paragraph into something that looks clickable.
   `is-orb-drag` uses !important because it must beat every descendant's own
   cursor, including links, for as long as the drag lasts. */
.imm.is-orb-hot { cursor: grab; }
.imm.is-orb-drag, .imm.is-orb-drag * { cursor: grabbing !important; user-select: none; }

/* No WebGL / no JS: a still, brand-correct atmosphere instead of a blank canvas.
   Painted with gradients so it costs nothing and can never fail to load. */
.imm__stage--fallback {
  background:
    radial-gradient(60% 50% at 68% 38%, rgba(13, 202, 210, 0.16), transparent 70%),
    radial-gradient(50% 45% at 22% 72%, rgba(0, 129, 250, 0.13), transparent 70%);
}

/* Scenes sit above the canvas. z-index 2 (canvas is 1) keeps every link and button
   clickable no matter what the renderer is doing.

   `overflow-x: clip` is the page's outermost horizontal guarantee, and it belongs
   HERE rather than only on .imm-pin. The decision-field tokens are absolutely
   positioned, and their containing block is the nearest positioned ancestor: that is
   .imm-pin while a scene is pinned, but .imm-scene once the unpin guard makes the
   panel static. Clipping only the panel therefore left a hole — tokens escaped and
   produced 55px of horizontal scroll at 1280x800. Clipping the scene closes it for
   every pin state. Vertical overflow stays visible so the seam gradient (which is
   drawn at inset -80px) still renders. */
.imm-scene { position: relative; z-index: 2; overflow-x: clip; overflow-y: visible; }

/* Dark scenes are a semi-transparent vignette over the run's navy, NOT an opaque
   fill: the canvas lives between the two and has to be visible through them. The
   edges stay dark enough for text contrast while the centre lets the Sage object
   glow through. */
.imm-scene--dark {
  background: linear-gradient(180deg,
    rgba(4, 22, 43, 0.72) 0%,
    rgba(2, 44, 84, 0.30) 45%,
    rgba(4, 22, 43, 0.72) 100%);
  color: var(--c-on-dark);
}
.imm-scene--dark h1, .imm-scene--dark h2, .imm-scene--dark h3 { color: #fff; }
.imm-scene--dark .lead { color: var(--c-on-dark-muted); }
.imm-scene--dark .eyebrow { color: #6FD0FF; }

/* Light scenes are the resolutions in the story (a plan, a comparison, people).
   Their opaque background is what hides the canvas — no JS needed to swap worlds. */
.imm-scene--light { background: var(--c-surface); color: var(--c-ink); }
.imm-scene--tint { background: var(--c-surface-2); color: var(--c-ink); }

/* Seam between a dark scene and the light scene that follows: a soft horizon rather
   than a hard edge, so the story flows instead of cutting. */
.imm-scene--dark + .imm-scene--light::before,
.imm-scene--dark + .imm-scene--tint::before {
  content: "";
  position: absolute; inset: -80px 0 auto 0; height: 80px;
  background: linear-gradient(180deg, transparent, var(--c-surface));
  pointer-events: none;
}
.imm-scene--dark + .imm-scene--tint::before { background: linear-gradient(180deg, transparent, var(--c-surface-2)); }

/* ============================================================================
   PINNED SCENE MECHANICS (native sticky)
   ============================================================================ */
/* Without JS these are ordinary sections. With JS the runway appears and the inner
   panel sticks, giving the scene room to play. */
html.js-anim .imm-scene--pin { min-height: calc(var(--imm-runway) * 1svh); }
html.js-anim .imm-scene--pin-short { min-height: calc(var(--imm-runway-short) * 1svh); }
html.js-anim .imm-scene--pin-long { min-height: calc(var(--imm-runway-long) * 1svh); }

/* ── The unpin guard ───────────────────────────────────────────────────────
   A sticky panel only works when its content FITS the viewport. When it does not,
   the panel grows past the runway, sticky silently never engages, and the scene
   becomes a very tall section that the reader meets in fragments — the worst of
   both worlds, and invisible in code review because nothing errors.

   immersive.js measures every panel against the real viewport and adds
   `.is-unpinned` to any scene that does not fit. That covers the cases no
   breakpoint can predict: short laptop viewports, large browser font settings,
   long translated strings, and browser chrome eating height on mobile.

   An unpinned scene is a normal, well-spaced section. Its scrub still runs, so it
   still reveals as it passes — it simply is not held in place. */
html.js-anim .imm-scene.is-unpinned { min-height: 0; }
html.js-anim .imm-scene.is-unpinned > .imm-pin {
  position: static; min-height: 0;
  padding-block: var(--section-y);
  /* Vertical overflow is released (that is the whole point of unpinning), but the
     HORIZONTAL clip stays. The decision-field tokens are absolutely positioned and
     drift up to 200px during the cull; without this they escape the viewport and
     create horizontal scroll — which is exactly what happened at 1280x800, where
     the guard unpinned the scene and took the containment with it.
     `overflow-x: clip` is the one value that lets the other axis stay `visible`. */
  overflow-x: clip;
  overflow-y: visible;
}

/* Scenes that are NOT pinned still need the section rhythm — they are ordinary
   sections that happen to reveal. Keeping them out of .imm-pin means they never
   get a sticky position or a forced viewport height. */
.imm-scene--flow { padding-block: var(--section-y); }

.imm-pin { position: relative; }
html.js-anim .imm-pin {
  position: sticky;
  top: 0;
  min-height: 100svh;
  display: grid;
  align-content: center;
  /* Clear the fixed header so nothing sits under it while pinned.
     KEEP THIS TIGHT. A pinned panel is exactly one viewport tall and it clips, so
     every pixel of padding is a pixel of content budget. The compare table's eyebrow
     was being sliced off the top at 900px viewports until this came down. If a scene
     still does not fit, shorten the SCENE — do not grow the panel. */
  padding-block: calc(var(--header-h) + var(--sp-3)) var(--sp-5);
  overflow: clip;               /* a drifting token can never widen the page */
}

/* min-width: 0 is load-bearing, not tidying.
   A grid item's automatic minimum size is min-content, so a child that refuses to
   shrink — the comparison table has `min-width: 640px` — stretches this container
   past the viewport instead of scrolling inside it. On a 412px phone the wrap
   measured 640px wide, the scene's `overflow-x: clip` hid the excess, and the table
   was cut off with no way to reach the rest of it. */
.imm-pin > * { min-width: 0; }

/* Scene progress rail — the "you are here" spine. Desktop only; decorative. */
.imm-rail {
  position: fixed; right: max(var(--sp-4), env(safe-area-inset-right)); top: 50%;
  transform: translateY(-50%);
  z-index: 3; display: none; gap: 14px; flex-direction: column;
  pointer-events: none; opacity: 0;
  transition: opacity var(--dur-slow) var(--ease-out);
}
.imm.is-stage-on .imm-rail { opacity: 1; }
.imm-rail__dot {
  width: 7px; height: 7px; border-radius: var(--r-pill);
  background: rgba(255, 255, 255, 0.28);
  transition: background var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out);
}
.imm-rail__dot.is-on { background: #6FD0FF; transform: scale(1.7); }
@media (min-width: 1200px) { html.js-anim .imm-rail { display: flex; } }

/* ============================================================================
   SCENE 1 — HERO
   ============================================================================ */
/* z-index: auto is NOT redundant — it undoes the `z-index: 2` that .imm-scene sets
   on every scene, and that matters more here than anywhere else.
   ------------------------------------------------------------------------------
   The other dark scenes can carry a stacking context because their backgrounds are
   semi-transparent, so the canvas beneath still shows through them. The hero's
   background is an opaque PHOTOGRAPH, so nothing shows through it — the canvas has
   to paint ABOVE the photo and below the copy, which is only possible if the hero
   does not establish a stacking context at all.

   Painting order with z-index: auto, all in the root stacking context:
     .imm             navy backdrop
     .imm-hero        vignette (non-positioned background)
     .imm-hero__bg    photo,  z-index 0
     .imm__stage      canvas, z-index 1  <- the Sage object, over the harbour
     .imm-hero__content  copy, z-index 2

   Inheriting z-index: 2 put the entire hero, photo included, above the canvas, and
   the orb rendered every frame into a layer nothing could see. */
.imm-hero { min-height: 100svh; display: grid; align-items: center; color: #fff; z-index: auto; }
.imm-hero__bg { position: absolute; inset: 0; z-index: 0; background: var(--imm-deep); }
.imm-hero__bg picture { display: contents; }
.imm-hero__bg img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.imm-hero__bg::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(180deg, rgba(4, 22, 43, 0.74) 0%, rgba(2, 44, 84, 0.58) 42%, rgba(4, 22, 43, 0.92) 100%);
}
.imm-hero__content { position: relative; z-index: 2; padding-block: 8rem 5rem; max-width: 62ch; }
.imm-hero h1 { color: #fff; max-width: 15ch; }
.imm-hero .lead { color: #DCE9FB; margin-top: var(--sp-5); max-width: 46ch; }
.imm-hero__actions { display: flex; flex-wrap: wrap; gap: var(--sp-4); margin-top: var(--sp-7); }
.imm-hero__returning { margin-top: var(--sp-4); color: #DCE9FB; font-size: var(--fs-small); }
.imm-hero__returning a { color: #6FD0FF; font-weight: var(--fw-semibold); }
.imm-hero__trust {
  margin-top: var(--sp-8); display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--sp-3) var(--sp-6); font-size: var(--fs-small); color: #BBD0EC;
}

/* Scroll cue */
.imm-hero__cue {
  position: absolute; left: 50%; bottom: 22px; transform: translateX(-50%); z-index: 2;
  font-size: 12px; letter-spacing: .12em; text-transform: uppercase; color: #BBD0EC;
  display: grid; justify-items: center; gap: 6px;
}
.imm-hero__cue .line { width: 1px; height: 34px; background: linear-gradient(#BBD0EC, transparent); }
@media (prefers-reduced-motion: no-preference) {
  html.js-anim .imm-hero__cue .line { animation: immCue 1.8s var(--ease-in-out) infinite; transform-origin: top; }
}
@keyframes immCue { 0%, 100% { transform: scaleY(.4); opacity: .5 } 50% { transform: scaleY(1); opacity: 1 } }

/* ---- Text reveal: line-by-line clipping mask ----
   Applied to the H1 only. The words are real text in the DOM at all times (SEO +
   screen readers + no-JS); the mask animates a wrapper, never the character stream.
   Restrained on purpose — §24 asks for a system, not per-sentence novelty. */
html.js-anim .imm-reveal-line { display: block; overflow: hidden; }
html.js-anim .imm-reveal-line > span { display: block; transform: translateY(105%); }

/* Body copy and CTAs: small, confident rise. Transform-only where the element is
   part of the LCP block, so the text paints immediately. */
html.js-anim [data-imm-rise] { opacity: 0; transform: translateY(22px); }
html.js-anim [data-imm-rise].is-in {
  opacity: 1; transform: none;
  transition: opacity var(--dur-slow) var(--ease-out), transform var(--dur-slow) var(--ease-out);
  transition-delay: var(--imm-delay, 0ms);
}

/* ============================================================================
   SCENE 2 — TOO MANY CHOICES
   ============================================================================ */
.imm-choices__copy { max-width: 46ch; position: relative; z-index: 3; }
.imm-choices__copy .title { text-wrap: balance; }

/* The decision field. A real, readable list in the DOM (crawlable, and the whole
   point of the scene is that these are REAL Australian options) that JS lifts into
   a depth-layered cloud. Without JS it is a plain tag list. */
.imm-field {
  list-style: none; padding: 0; margin: var(--sp-6) 0 0;
  display: flex; flex-wrap: wrap; gap: var(--sp-2);
}
.imm-field__item {
  font-size: 13px; font-weight: var(--fw-semibold); line-height: 1.4;
  padding: .42rem .85rem; border-radius: var(--r-pill);
  border: 1px solid var(--imm-line); background: rgba(255, 255, 255, 0.05);
  color: #CFE0F5; white-space: nowrap;
}
.imm-field__item--university { border-color: rgba(111, 208, 255, 0.32); color: #A9D8FF; }
.imm-field__item--course     { border-color: rgba(13, 202, 210, 0.30); color: #93E4E8; }
.imm-field__item--area       { border-color: rgba(112, 163, 68, 0.36); color: #B9D89A; }
.imm-field__item--level      { border-color: rgba(255, 255, 255, 0.20); color: #D6E4F5; }
.imm-field__item--city       { border-color: rgba(255, 189, 0, 0.30); color: #F0D48A; }
.imm-field__item--worry      { border-color: rgba(255, 255, 255, 0.14); color: #93A9C4; font-weight: var(--fw-medium); }

/* Enhanced: the tags leave flow and become a 3-plane cloud around the copy.
   `position: absolute` inside a clipped scene means a stray token can never create
   horizontal scroll. Positions are written by immersive.js.

   `.is-cloud` IS THE CONTRACT, and it is deliberately a class the script adds only
   after it has actually written every --x/--y. Never key this off a media query or
   a `:not()` of some other state.

   The previous version used `:not(.is-unpinned)`, and that shipped a real defect.
   `:not()` adds specificity, so the rule outranked the mobile override inside
   `@media (max-width: 899px)` — and on any phone tall enough for the scene to
   "fit" (412x915, 430x932), `.is-unpinned` was absent, the cloud layout switched
   on, and the mobile code path never wrote a single coordinate. All 27 tokens
   resolved to left:0/top:0 and piled into one unreadable smear of overlapping text
   at the top of the scene. Every automated check passed: they were opaque, on
   screen, and caused no overflow.

   Tying the styling to the class that means "the coordinates exist" makes that
   state unrepresentable. */
html.js-anim .imm-choices.is-cloud .imm-field {
  position: absolute; inset: 0; display: block; margin: 0;
  pointer-events: none;
}
/* left/top are written once at init; GSAP owns transform + opacity from there
   (it writes a single composited matrix per frame, which is cheaper than making
   the browser re-resolve several custom properties per element per frame). */
html.js-anim .imm-choices.is-cloud .imm-field__item {
  position: absolute;
  left: var(--x); top: var(--y);
  will-change: transform, opacity;
}

/* The four that survive — the resolution beat. */
.imm-resolved {
  position: relative; z-index: 3; margin-top: var(--sp-6);
  display: grid; gap: var(--sp-3);
}
@media (min-width: 640px) { .imm-resolved { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.imm-resolved__item {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-4); border-radius: var(--r-lg);
  background: rgba(255, 255, 255, 0.06); border: 1px solid var(--imm-line-strong);
  backdrop-filter: blur(6px);
}
.imm-resolved__tick {
  flex: none; width: 26px; height: 26px; border-radius: var(--r-pill);
  background: var(--grad-sage); color: #fff; display: grid; place-items: center;
  font-size: 13px; font-weight: var(--fw-extrabold);
}
.imm-resolved__label { font-size: var(--fs-small); font-weight: var(--fw-semibold); color: #EAF2FF; }
.imm-resolved__meta { display: block; font-weight: var(--fw-regular); color: var(--c-on-dark-muted); font-size: 13px; }
html.js-anim .imm-choices.is-live .imm-resolved { opacity: 0; }

/* ============================================================================
   SCENE 3 — BUILD YOUR AUSTRALIA PLAN
   ============================================================================ */
.imm-plan__grid { display: grid; gap: var(--sp-8); align-items: center; }
@media (min-width: 980px) { .imm-plan__grid { grid-template-columns: .9fr 1.1fr; gap: var(--sp-12); } }

/* A production-authentic plan surface — the same chrome language as the real
   student dashboard, not a screenshot. */
.imm-plancard {
  background: #fff; border: 1px solid var(--c-line); border-radius: var(--r-2xl);
  box-shadow: var(--sh-lg); overflow: hidden;
}
.imm-plancard__bar {
  display: flex; align-items: center; gap: 8px;
  padding: var(--sp-4) var(--sp-5); border-bottom: 1px solid var(--c-line);
  background: var(--c-surface-2);
}
.imm-plancard__dot { width: 11px; height: 11px; border-radius: 50%; background: #d7e0ea; }
.imm-plancard__name { margin-left: auto; font-size: 12px; font-weight: var(--fw-bold); color: var(--c-muted); }
.imm-plancard__body { padding: var(--sp-5); display: grid; gap: var(--sp-3); }

/* Five rows plus the meter and the completion beat have to fit inside one pinned
   viewport, next to (or under) the copy column. The vertical padding is the lever:
   at --sp-4 the card ran ~30px past the panel at every width below 1440. */
.imm-planrow {
  display: grid; grid-template-columns: auto 1fr auto; align-items: center;
  gap: var(--sp-3); padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--c-line); border-radius: var(--r-md); background: #fff;
}
.imm-planrow__n {
  flex: none; width: 26px; height: 26px; border-radius: var(--r-pill);
  background: var(--c-surface-3); color: var(--c-primary);
  display: grid; place-items: center; font-size: 12px; font-weight: var(--fw-extrabold);
}
.imm-planrow__label { font-size: var(--fs-small); font-weight: var(--fw-semibold); color: var(--c-navy); }
.imm-planrow__q { display: block; font-weight: var(--fw-regular); color: var(--c-muted); font-size: 12.5px; margin-top: 2px; }
.imm-planrow__answer {
  font-size: 13px; font-weight: var(--fw-bold); color: var(--c-primary-700);
  background: var(--c-surface-3); padding: .3rem .7rem; border-radius: var(--r-pill);
  white-space: nowrap;
  justify-self: end;
}

/* Narrow screens: the answer moves under the question instead of fighting it for
   width. "Already taken (IELTS / PTE / TOEFL)" cannot wrap and cannot fit beside a
   label on a 360px screen — as a third grid column it forced the row to 454px and
   pushed the whole page into horizontal scroll. Two columns, answer on its own
   line, nothing to overflow. */
@media (max-width: 560px) {
  .imm-planrow { grid-template-columns: auto 1fr; row-gap: var(--sp-2); }
  .imm-planrow__answer {
    grid-column: 2; justify-self: start;
    white-space: normal; overflow-wrap: anywhere;
  }
}

/* Enhanced: rows activate in sequence as the scene scrubs. */
html.js-anim .imm-plan.is-live .imm-planrow {
  opacity: .3; transform: translateY(10px);
  transition: opacity var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out),
              border-color var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out);
}
html.js-anim .imm-plan.is-live .imm-planrow.is-on {
  opacity: 1; transform: none; border-color: rgba(0, 74, 173, 0.35); box-shadow: var(--sh-xs);
}
html.js-anim .imm-plan.is-live .imm-planrow__answer { opacity: 0; transform: scale(.9); transition: opacity var(--dur-base) var(--ease-spring), transform var(--dur-base) var(--ease-spring); }
html.js-anim .imm-plan.is-live .imm-planrow.is-on .imm-planrow__answer { opacity: 1; transform: none; }

/* Completion beat */
.imm-plancard__done {
  display: flex; align-items: center; gap: var(--sp-3);
  margin-top: var(--sp-2); padding: var(--sp-4) var(--sp-5);
  border-radius: var(--r-lg); background: rgba(112, 163, 68, 0.10);
  border: 1px solid rgba(112, 163, 68, 0.34);
}
.imm-plancard__done b { color: var(--c-green-text); font-size: var(--fs-small); }
.imm-plancard__done span { color: var(--c-muted); font-size: 13px; display: block; font-weight: var(--fw-regular); }
html.js-anim .imm-plan.is-live .imm-plancard__done { opacity: 0; transform: translateY(8px); transition: opacity var(--dur-slow) var(--ease-out), transform var(--dur-slow) var(--ease-out); }
html.js-anim .imm-plan.is-live .imm-plancard__done.is-on { opacity: 1; transform: none; }

/* Progress meter across the five questions */
.imm-planmeter { height: 4px; border-radius: 4px; background: var(--c-surface-3); overflow: hidden; margin-top: var(--sp-2); }
.imm-planmeter i { display: block; height: 100%; width: 100%; background: var(--grad-primary); transform-origin: left; transform: scaleX(var(--fill, 1)); }

/* ============================================================================
   SCENE 4 — DISCOVER AUSTRALIA
   ============================================================================ */
.imm-discover__grid { display: grid; gap: var(--sp-8); align-items: center; }
@media (min-width: 980px) { .imm-discover__grid { grid-template-columns: 1.05fr .95fr; gap: var(--sp-10); } }

/* Australia as an SVG silhouette with constellation points — elegant and cheap,
   exactly as the brief prefers over a photoreal 3D model. */
.imm-au { position: relative; width: 100%; max-width: 580px; margin-inline: auto; }
/* Everything is inside the viewBox, so the SVG can clip — that is a hard guarantee
   that a label can never widen the page. */
.imm-au svg { width: 100%; height: auto; display: block; }
.imm-au__shape {
  fill: rgba(111, 208, 255, 0.07);
  stroke: rgba(111, 208, 255, 0.55);
  stroke-width: 1.2; stroke-linejoin: round;
}
.imm-au__node { fill: #6FD0FF; }
.imm-au__ring { fill: none; stroke: rgba(111, 208, 255, 0.5); stroke-width: 1; }
.imm-au__label { fill: #CFE0F5; font-size: 11px; font-weight: 700; letter-spacing: .04em; }
html.js-anim .imm-discover.is-live .imm-au__pt { opacity: 0; transform-box: fill-box; transform-origin: center; }

/* The three discovery paths */
.imm-paths { display: grid; gap: var(--sp-4); margin-top: var(--sp-7); }
@media (min-width: 640px) { .imm-paths { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.imm-path {
  display: flex; flex-direction: column; gap: 6px;
  padding: var(--sp-5); border-radius: var(--r-lg);
  background: rgba(255, 255, 255, 0.05); border: 1px solid var(--imm-line-strong);
  color: var(--c-on-dark); text-decoration: none;
  transition: transform var(--dur-base) var(--ease-out), background var(--dur-base) var(--ease-out),
              border-color var(--dur-base) var(--ease-out);
}
.imm-path:hover, .imm-path:focus-visible { transform: translateY(-4px); background: rgba(255, 255, 255, 0.09); border-color: #6FD0FF; }
.imm-path__k { font-size: 11px; font-weight: var(--fw-bold); letter-spacing: .14em; text-transform: uppercase; color: #6FD0FF; }
.imm-path__t { font-size: var(--fs-h4); font-weight: var(--fw-extrabold); color: #fff; }
.imm-path__d { font-size: 13px; color: var(--c-on-dark-muted); }
.imm-path__go { margin-top: auto; padding-top: var(--sp-3); font-size: 13px; font-weight: var(--fw-bold); color: #6FD0FF; }
.imm-path:hover .imm-path__go { text-decoration: underline; text-underline-offset: 3px; }

/* ============================================================================
   SCENE 5 — COMPARE
   ============================================================================ */
/* Three real offerings that start as independent cards and align into rows. The
   markup is ONE table, always — a comparison is tabular data, and a screen reader
   should get a table, not a set of divs pretending to be one. The "cards" beat is
   a visual treatment of the same table's columns. */
/* The table is wider than a phone and scrolls horizontally inside its own box.
   That was true before too — but with no affordance it just looked cut off, which
   is what it was reported as. Three fixes, all cheap:
     · a fade on the right edge that says "there is more this way";
     · scroll-snap, so a swipe lands cleanly on the next course rather than halfway;
     · an explicit swipe hint under the table on touch devices.
   The fade is a mask so it works over any background and needs no overlay element. */
.imm-compare__wrap {
  margin-top: var(--sp-7);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scroll-snap-type: x proximity;
  scrollbar-width: thin;
}
.imm-compare__wrap thead th[data-imm-col],
.imm-compare__wrap tbody td { scroll-snap-align: start; }

/* Base state first, media override second — the other way round the base
   `display: none` simply won on equal specificity and the hint never appeared. */
.imm-compare__hint {
  display: none;
  align-items: center; gap: 8px; margin-top: var(--sp-3);
  font-size: 13px; font-weight: var(--fw-semibold); color: var(--c-muted);
}
@media (max-width: 899px) {
  .imm-compare__wrap {
    -webkit-mask-image: linear-gradient(90deg, #000 82%, rgba(0, 0, 0, 0.15) 100%);
            mask-image: linear-gradient(90deg, #000 82%, rgba(0, 0, 0, 0.15) 100%);
  }
  .imm-compare__hint { display: flex; }
}
.imm-compare__hint::after {
  content: "→"; font-size: 15px;
  animation: immSwipe 1.8s var(--ease-in-out) infinite;
}
@keyframes immSwipe { 0%, 100% { transform: translateX(0); opacity: .55 } 50% { transform: translateX(5px); opacity: 1 } }
@media (prefers-reduced-motion: reduce) { .imm-compare__hint::after { animation: none; } }
.imm-compare {
  width: 100%; min-width: 640px; border-collapse: separate; border-spacing: 0;
  background: #fff; border: 1px solid var(--c-line); border-radius: var(--r-xl); overflow: hidden;
}
/* Compact rows: six criteria plus a header have to fit inside one pinned viewport
   alongside the heading, the CTAs and the data caveat. */
.imm-compare th, .imm-compare td {
  padding: var(--sp-3) var(--sp-4); text-align: left; vertical-align: top;
  border-bottom: 1px solid var(--c-line); font-size: 13px;
}
.imm-compare thead th {
  background: var(--c-surface-2); color: var(--c-navy);
  font-size: var(--fs-small); font-weight: var(--fw-extrabold); line-height: 1.35;
}
.imm-compare thead th:first-child { width: 9.5rem; }
.imm-compare th[scope="row"] {
  color: var(--c-muted); font-weight: var(--fw-bold); white-space: nowrap;
  text-transform: uppercase; letter-spacing: .06em; font-size: 11.5px;
}
.imm-compare td { color: var(--c-ink); font-weight: var(--fw-semibold); }
.imm-compare tbody tr:last-child th, .imm-compare tbody tr:last-child td { border-bottom: 0; }
.imm-compare__uni { display: block; color: var(--c-muted); font-weight: var(--fw-regular); margin-top: 2px; }

/* Enhanced: columns arrive as separate "cards", then rows align one at a time.

   Un-aligned rows are DIMMED, not hidden. At opacity 0 they still occupied their
   height, so a reader arriving mid-scene met a table with two rows and a large
   blank block — which reads as broken rather than as progressive. Dimming keeps the
   table whole and makes the reveal an emphasis, which is what the scene is actually
   about: the rows becoming comparable, not appearing. */
html.js-anim .imm-compare-scene.is-live .imm-compare tbody tr {
  opacity: 0.24;
  transition: opacity var(--dur-base) var(--ease-out);
}
html.js-anim .imm-compare-scene.is-live .imm-compare tbody tr.is-on { opacity: 1; }
html.js-anim .imm-compare-scene.is-live .imm-compare thead th[data-imm-col] {
  transform: translateY(var(--lift, 0));
  transition: transform var(--dur-cinematic) var(--ease-out);
}

/* ============================================================================
   SCENE 6 — APPLICATION JOURNEY
   ============================================================================ */
/* An <ol> because the four states are genuinely ordered — but the numbers are shown
   as designed badges, so the browser's own markers must go. */
.imm-apply__track {
  list-style: none; padding: 0; margin: var(--sp-8) 0 0;
  display: grid; gap: var(--sp-4);
}
@media (min-width: 900px) { .imm-apply__track { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--sp-5); } }
.imm-stage-card {
  position: relative; padding: var(--sp-5);
  border-radius: var(--r-lg); background: rgba(255, 255, 255, 0.05);
  border: 1px solid var(--imm-line);
}
/* These are <span>s (inline by default), and inline boxes ignore vertical margins —
   which is why the badge, title and description all ran together on one line until
   they were made block. */
.imm-stage-card__n {
  width: 28px; height: 28px; border-radius: var(--r-pill);
  background: rgba(255, 255, 255, 0.10); color: #9DB4D0;
  display: grid; place-items: center; font-size: 12px; font-weight: var(--fw-extrabold);
  transition: background var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out);
}
.imm-stage-card__t { display: block; margin-top: var(--sp-3); font-size: var(--fs-small); font-weight: var(--fw-extrabold); color: #fff; }
.imm-stage-card__d { display: block; margin-top: 6px; font-size: 13px; color: var(--c-on-dark-muted); line-height: 1.5; }
html.js-anim .imm-apply.is-live .imm-stage-card {
  opacity: .38;
  transition: opacity var(--dur-base) var(--ease-out), border-color var(--dur-base) var(--ease-out),
              background var(--dur-base) var(--ease-out);
}
html.js-anim .imm-apply.is-live .imm-stage-card.is-on {
  opacity: 1; border-color: rgba(111, 208, 255, 0.42); background: rgba(111, 208, 255, 0.07);
}
html.js-anim .imm-apply.is-live .imm-stage-card.is-on .imm-stage-card__n { background: var(--grad-sage); color: #fff; }

/* The connecting line the selected course travels along. Decorative. */
.imm-apply__line { position: relative; height: 2px; margin: var(--sp-6) 0 0; background: var(--imm-line); border-radius: 2px; }
.imm-apply__line i { position: absolute; inset: 0 auto 0 0; width: 100%; background: var(--grad-sage); border-radius: 2px; transform-origin: left; transform: scaleX(var(--fill, 1)); }
.imm-apply__note { margin-top: var(--sp-5); font-size: 13px; color: var(--c-on-dark-muted); max-width: 68ch; }

/* ============================================================================
   SCENE 7 — ARRIVAL + STUDENT AMBASSADORS
   ============================================================================ */
.imm-arrive__head { max-width: 60ch; }
.imm-arrive__grid { margin-top: var(--sp-8); }
/* Cards emerge from depth. Small, tasteful tilt on pointer — never a 3D flip. */
html.js-anim .imm-arrive.is-live [data-imm-amb] {
  opacity: 0; transform: translateY(38px) scale(.965);
  transition: opacity var(--dur-cinematic) var(--ease-out), transform var(--dur-cinematic) var(--ease-out);
  transition-delay: var(--imm-delay, 0ms);
}
html.js-anim .imm-arrive.is-live [data-imm-amb].is-in { opacity: 1; transform: none; }

/* ============================================================================
   SCENE 8 — THE EDUCIRCLE ECOSYSTEM (finale)
   ============================================================================ */
.imm-eco { text-align: center; }
.imm-eco__head { max-width: 58ch; margin-inline: auto; }
.imm-eco__system { position: relative; margin: var(--sp-10) auto 0; max-width: 940px; }

/* Connector lines are drawn in SVG behind the nodes so they scale cleanly and cost
   nothing to animate (stroke-dashoffset only). Decorative. */
.imm-eco__links { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.imm-eco__links line { stroke: rgba(111, 208, 255, 0.34); stroke-width: 1.2; }

/* The student at the centre. */
.imm-eco__core {
  position: relative; z-index: 2; width: 132px; height: 132px; margin-inline: auto;
  border-radius: var(--r-pill); display: grid; place-items: center; text-align: center;
  background: radial-gradient(circle at 40% 35%, rgba(13, 202, 210, .30), rgba(2, 44, 84, .85));
  border: 1px solid rgba(13, 202, 210, .55);
  box-shadow: 0 0 60px rgba(13, 202, 210, .22);
}
.imm-eco__core b { display: block; color: #fff; font-size: var(--fs-small); }
.imm-eco__core span { display: block; color: #9DB4D0; font-size: 11px; margin-top: 2px; }

/* Node list. A real list in the DOM (crawlable, keyboard-navigable, and each node
   that has a destination is a real link); JS places them on a ring. */
.imm-eco__nodes {
  list-style: none; padding: 0; margin: var(--sp-8) 0 0;
  display: grid; gap: var(--sp-3); grid-template-columns: repeat(2, minmax(0, 1fr));
  text-align: left; position: relative; z-index: 2;
}
@media (min-width: 900px) { .imm-eco__nodes { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.imm-eco__node {
  display: flex; flex-direction: column; gap: 4px; height: 100%;
  padding: var(--sp-4); border-radius: var(--r-lg);
  background: rgba(255, 255, 255, 0.05); border: 1px solid var(--imm-line-strong);
  color: var(--c-on-dark); text-decoration: none;
  transition: transform var(--dur-base) var(--ease-out), background var(--dur-base) var(--ease-out),
              border-color var(--dur-base) var(--ease-out);
}
a.imm-eco__node:hover, a.imm-eco__node:focus-visible { transform: translateY(-4px); background: rgba(255, 255, 255, 0.10); border-color: #6FD0FF; }
.imm-eco__node b { color: #fff; font-size: var(--fs-small); }
.imm-eco__node span { color: var(--c-on-dark-muted); font-size: 12.5px; line-height: 1.45; }
.imm-eco__cta { margin-top: var(--sp-6); display: flex; flex-wrap: wrap; gap: var(--sp-4); justify-content: center; }
/* Its own beat below the pinned system, not part of the panel — see ecosystem.blade.php. */
.imm-eco__close { padding-block: var(--section-y); text-align: center; }
/* The closing line is a <p>, so it needs the display treatment stated explicitly —
   the h1–h4 rules in components.css do not reach it. */
.imm-eco__close .title {
  color: #fff; max-width: 20ch; margin-inline: auto; text-wrap: balance;
  font-size: var(--fs-h2); font-weight: var(--fw-extrabold);
  line-height: var(--lh-snug); letter-spacing: var(--ls-tight);
}

/* Visually hidden, but read by assistive technology. Used for the comparison
   table's caption and its empty top-left header cell, both of which a screen
   reader needs and a sighted user does not. NOT .skip-link — that one is a
   focusable navigation control that deliberately becomes visible. */
.imm-vh {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* ============================================================================
   MICRO-INTERACTIONS
   ============================================================================
   The button/card/nav polish that applies to EVERY page lives in components.css,
   not here — this file is only loaded on the homepage, so anything defined here
   would silently be a homepage-only inconsistency. What stays below is the pointer
   response, which only the immersive scenes opt into. */

/* Pointer-reactive depth. Applied ONLY to elements that opt in via [data-imm-tilt],
   and only on devices with a real pointer — never a global cursor takeover. */
@media (hover: hover) and (pointer: fine) {
  html.js-anim [data-imm-tilt] {
    transform: perspective(900px)
               rotateX(calc(var(--ty, 0) * -2.2deg))
               rotateY(calc(var(--tx, 0) * 2.2deg))
               translate3d(0, 0, 0);
    transition: transform var(--dur-slow) var(--ease-out);
    transform-style: preserve-3d;
  }
}

/* ============================================================================
   MOBILE — deliberately designed, not "desktop with animation off"
   ============================================================================
   Shorter runways, fewer simultaneous elements, no pointer response, no floating
   token cloud (it needs width to be legible). The story and its beats survive;
   the staging is simpler. */
@media (max-width: 899px) {
  /* NO PINNING ON PHONES — a deliberate decision, not a fallback.
     Measured on a 390x844 viewport, every scene's content is 1.6–2.1 viewports
     tall, so a sticky panel could never engage: the section grew to fit the panel
     and the "pinned" scene silently became a very tall ordinary section. Rather
     than ship an accident, phones get the designed flow version — scenes at their
     natural height, revealing as they enter, with shorter timelines and no token
     cloud. That is what §28 of the brief asks for: a mobile experience that is
     designed, not a desktop one with the motion switched off. */
  html.js-anim .imm-scene--pin,
  html.js-anim .imm-scene--pin-short,
  html.js-anim .imm-scene--pin-long { min-height: 0; }
  html.js-anim .imm-pin {
    position: static; min-height: 0;
    padding-block: var(--section-y);
    overflow-x: clip; overflow-y: visible;   /* same reasoning as .is-unpinned */
  }

  /* The decision field stays a readable, wrapped tag list — a depth cloud on a
     360px screen is unreadable noise, which is the opposite of the point.
     No override is needed to achieve that any more: the cloud layout is opt-in via
     `.is-cloud`, which the script only adds on the pinned desktop path. */
  html.js-anim .imm-choices .imm-field { margin-top: var(--sp-5); }
  .imm-hero__content { padding-block: 7rem 4.5rem; }

  /* The core carries the finale on its own here: there is no WebGL object behind it
     and no connector lines beside it, so as a thin outlined ring it read as an
     unfinished placeholder. Give it real presence — a denser core, a brighter rim
     and a wider glow — so "You" is clearly the centre of the system. */
  .imm-eco__core {
    width: 132px; height: 132px;
    background:
      radial-gradient(circle at 42% 34%, rgba(13, 202, 210, .55), rgba(2, 44, 84, .92) 68%),
      radial-gradient(circle at 50% 50%, rgba(0, 129, 250, .30), transparent 70%);
    border-color: rgba(13, 202, 210, .75);
    box-shadow: 0 0 46px rgba(13, 202, 210, .34), inset 0 0 26px rgba(13, 202, 210, .22);
  }
  .imm-eco__core b { font-size: var(--fs-body); }

  /* The ecosystem connectors only read as a radial system in the four-column
     desktop layout. Against a two-column stack they run diagonally through the
     cards and look like stray lines, so they are dropped — the grouping already
     says "one connected system" on its own. */
  .imm-eco__links { display: none; }
}

/* Very small phones: trim the tallest runways further so the page does not feel
   endless on a device that scrolls slowest. */
@media (max-width: 480px) {
  :root { --imm-runway-short: 120; --imm-runway: 140; --imm-runway-long: 155; }
}

/* Landscape phones have almost no vertical room — a pinned scene would trap the
   user in a viewport too short to read. Release the pins entirely. */
@media (max-height: 520px) and (orientation: landscape) {
  html.js-anim .imm-scene--pin,
  html.js-anim .imm-scene--pin-short,
  html.js-anim .imm-scene--pin-long { min-height: 0; }
  html.js-anim .imm-pin { position: static; min-height: 0; padding-block: var(--sp-8); }
}

/* ============================================================================
   REDUCED MOTION — mandatory
   ============================================================================
   Everything resolves at its final value: revealed, in place, no scrub, no pin, no
   float, no parallax, no pointer response. The page carries exactly the same
   message, it simply does not move. */
@media (prefers-reduced-motion: reduce) {
  html.js-anim .imm-scene--pin,
  html.js-anim .imm-scene--pin-short,
  html.js-anim .imm-scene--pin-long { min-height: 0; }
  html.js-anim .imm-pin { position: static; min-height: 0; overflow: visible; padding-block: var(--sp-8); }

  .imm__stage { display: none; }
  .imm-rail { display: none !important; }

  html.js-anim .imm-reveal-line { overflow: visible; }
  html.js-anim .imm-reveal-line > span { transform: none !important; }

  html.js-anim [data-imm-rise],
  html.js-anim [data-imm-rise].is-in,
  html.js-anim .imm-arrive.is-live [data-imm-amb],
  html.js-anim .imm-plan.is-live .imm-planrow,
  html.js-anim .imm-plan.is-live .imm-planrow__answer,
  html.js-anim .imm-plan.is-live .imm-plancard__done,
  html.js-anim .imm-apply.is-live .imm-stage-card,
  html.js-anim .imm-compare-scene.is-live .imm-compare tbody tr,
  html.js-anim .imm-choices.is-live .imm-resolved,
  html.js-anim .imm-discover.is-live .imm-au__pt {
    opacity: 1 !important; transform: none !important; transition: none !important;
  }

  /* The token cloud returns to a plain wrapped list. Kept as a belt-and-braces
     reset: immersive.js bails out entirely under reduced motion and never adds
     `.is-cloud`, but if that ever changed, reduced motion must still get a
     readable list rather than a pile. */
  html.js-anim .imm-choices .imm-field { position: static !important; display: flex !important; }
  html.js-anim .imm-choices .imm-field__item {
    position: static !important; transform: none !important; opacity: 1 !important;
    left: auto !important; top: auto !important;
  }

  html.js-anim [data-imm-tilt] { transform: none !important; transition: none !important; }
  .imm-path:hover, a.imm-eco__node:hover, .btn:active { transform: none !important; }
  html.js-anim .imm-hero__cue .line { animation: none; }
}
