/* ══════════════════════════════════════════════════════════════════════════
   TNAADO index-full — "THE JOURNEY"  (T-1039, 2026-08-13)

   Built from zero to Ethan's own dictated choreography after seven days of
   iteration on the storm hero were rejected outright ("delete this page
   start over 7 days nothing but worse"). Nothing in this file derives from
   css/storm.css, css/storm-page.css or js/storm-hero.js; that hero's JS is
   deleted and neither stylesheet is linked from this page any more.

   THE SHAPE
   ---------
   ONE pinned viewport holds the whole page. Behind everything, the office-
   building video Ethan named — the same clip the page's second section
   already used (page-portfolio-hero.mp4) — plays continuously and never
   restarts between moments, which is the tnaado.com FAQ backdrop pattern he
   pointed at. In front of it, the real TNAADO emblem travels
   right → left → right → left → right → centre across six moments (the
   open, four practice beats, and the close), growing large at the end so
   the closing copy sits inside the object. Copy alternates to the opposite
   side of the emblem at every beat, with a red glow behind it, and a scroll
   rail on the right reports real progress.

   THE MECHANIC THAT KEPT FAILING, AND WHY IT IS DIFFERENT HERE
   -----------------------------------------------------------
   Every previous round drove the emblem from one source and the text from
   another, so the two drifted and text landed nowhere near the object. Here
   there is exactly ONE scrubbed value: GSAP ScrollTrigger interpolates a
   single progress number 0→1 across the track (scrub, so backward scroll
   interpolates instead of jumping), and js/index-journey.js computes the
   emblem pose AND every moment's opacity from that same number in the same
   function on the same frame. They cannot drift; there is nothing to drift
   against.

   DEGRADATION — this file's default state is the STATIC one. Without
   .is-live (no JS, no GSAP, or prefers-reduced-motion) every moment lays
   out in normal document flow as a readable stacked page: no pin, no
   absolute positioning, no zero opacity. .is-live is added by JS only when
   the journey can actually run, and only then does anything become an
   overlay. A visitor who never gets JS gets the whole page's content, not
   an empty screen — the failure mode a pinned-and-faded default would give.

   TOKENS — ink #0a0a0a, on-black #f5f1e8, red #c8102e / #e8455a. Nothing
   invented; same values as every other page on this site.
   ══════════════════════════════════════════════════════════════════════════ */

.ij-journey {
  position: relative;
  background: #0a0a0a;
  isolation: isolate;

  /* Pinned dark in BOTH themes, same convention as .page-hero / .stk-hero
     elsewhere on this site: the light theme flips --ink to a cream and
     --on-black to a near-black, which would paint this copy invisible. */
  --ink: #0a0a0a;
  --on-black: #f5f1e8;
  --on-black-mute: rgba(245,241,232,.62);
  --red-text: #e8455a;
}
/* The [data-theme="light"] twin of the block above is gone with the sitewide
   toggle removal (T-1040). There is one colour mode now — light header, dark
   body, light footer — so the token block above is simply unconditional and
   nothing needs re-declaring for a second theme. */

/* ── THE TRACK ────────────────────────────────────────────────────────────
   600vh = six moments at roughly one screen of scroll each. Only applied in
   journey mode; statically the section is just as tall as its content. */
.ij-journey.is-live { height: 600vh; }

.ij-pin {
  position: relative;
  overflow: hidden;
  background: #0a0a0a;
}
.ij-journey.is-live .ij-pin {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;
}

/* ── THE PERSISTENT BACKDROP ──────────────────────────────────────────────
   One video, never restarted, never swapped — it is the constant the whole
   journey happens in front of. object-fit:cover so it fills any viewport
   without letterboxing. */
.ij-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 0;
  /* Not decoration-only: this is the page's ground. Kept dim enough that
     cream type clears contrast against its brightest frame. */
  /* T-1144 - was .38, far too faded. The photograph is the page ground
     and should read as a photograph. */
  opacity: .68;
  filter: saturate(.95) contrast(1.04);
}
.ij-journey:not(.is-live) .ij-pin { min-height: 100svh; }

/* Scrim stack: a letterbox top/bottom, one red radial low-right, and a side
   anchor so copy always has a dark ground under it whichever side it is on.
   Designed for the worst frame — legible with the video at full brightness. */
.ij-veil {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    radial-gradient(ellipse 60% 55% at 78% 72%, rgba(200,16,46,.20) 0%, rgba(200,16,46,.06) 46%, transparent 72%),
    linear-gradient(to bottom, rgba(10,10,10,.88) 0%, rgba(10,10,10,.42) 26%, rgba(10,10,10,.42) 62%, rgba(10,10,10,.92) 100%),
    linear-gradient(100deg, rgba(10,10,10,.72) 0%, rgba(10,10,10,.30) 52%, rgba(10,10,10,.62) 100%);
}

/* ── CN TOWER, RIGHT ──────────────────────────────────────────────────────
   Ethan: "the animation needs to start to the right with the tower to the
   right so it's not interfering with the logo and text on the left."
   Present in the opening frame and faded out by the first beat (opacity is
   driven from the same scrubbed progress as everything else). */
.ij-tower {
  position: absolute;
  z-index: 2;
  right: -2%;
  bottom: 0;
  width: min(46%, 620px);
  height: 100%;
  pointer-events: none;
  opacity: 0;
}
.ij-journey:not(.is-live) .ij-tower { display: none; }
.ij-tower img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 62% 40%;
  filter: saturate(.72) contrast(1.1) brightness(.82);
  /* Feathered on every inward edge so the photograph never shows a hard
     rectangle against the video behind it. */
  -webkit-mask-image:
    linear-gradient(to right, transparent 0%, #000 34%),
    linear-gradient(to bottom, transparent 0%, #000 16%, #000 78%, transparent 100%);
  -webkit-mask-composite: source-in;
  mask-image:
    linear-gradient(to right, transparent 0%, #000 34%),
    linear-gradient(to bottom, transparent 0%, #000 16%, #000 78%, transparent 100%);
  mask-composite: intersect;
}

/* ══════════════════════════════════════════════════════════════════════
   THE EMBLEM
   Opaque, always — Ethan: "make the 3D object pure not transparent." No
   opacity below 1 on the mark itself and no mix-blend-mode anywhere in this
   subtree. The PNG's own alpha channel is what gives it a silhouette; the
   pixels that are there are fully there.

   Two nested layers on purpose:
     .ij-emblem       position + scale, written by JS from scroll progress
     .ij-emblem__spin a perpetual CSS rotation, independent of scroll
   An emblem that only turns while you scroll reads as a dead sprite the
   moment you stop. The base spin runs regardless; JS adds an extra hard
   turn on top of it across the beat 2 → beat 3 leg, which is Ethan's "spins
   back to the left".
   ══════════════════════════════════════════════════════════════════════ */
.ij-emblem {
  position: absolute;
  z-index: 3;
  left: 50%;
  top: 50%;
  width: clamp(180px, 26vw, 380px);
  aspect-ratio: 1;
  margin: 0;
  pointer-events: none;
  /* JS overwrites this every frame in journey mode. This value is what a
     static visitor would see if the element were shown at all (it is not —
     see the :not(.is-live) rule below), and it is also the pose the first
     JS frame writes, so there is never a visible jump on boot. */
  transform: translate(-50%, -50%);
}
.ij-journey:not(.is-live) .ij-emblem { display: none; }

.ij-emblem__spin {
  width: 100%;
  height: 100%;
  animation: ijSpin 24s linear infinite;
  will-change: transform;
}
.ij-emblem__spin img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}
@keyframes ijSpin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }

/* The emblem's own light on the scene. Sits BEHIND the mark (negative
   z-index inside the emblem's stacking context) so it never washes over the
   opaque silhouette — it is the glow the object casts, not a veil on it. */
.ij-emblem__glow {
  position: absolute;
  z-index: -1;
  inset: -42%;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 50%,
    rgba(200,16,46,.34) 0%,
    rgba(200,16,46,.15) 38%,
    rgba(200,16,46,.04) 62%,
    transparent 76%);
  animation: ijGlowBreath 11s cubic-bezier(.45,0,.55,1) infinite alternate;
}
@keyframes ijGlowBreath {
  from { opacity: .62; transform: scale(.96); }
  to   { opacity: 1;   transform: scale(1.07); }
}

/* ── SCROLL RAIL, RIGHT ───────────────────────────────────────────────────
   Ethan: "add an indicator on the right side stating to scroll for more."
   Reports real progress off the same value that drives the emblem, and
   retires at the end so it informs rather than nags. */
.ij-rail {
  position: absolute;
  z-index: 5;
  right: clamp(10px, 1.5vw, 22px);
  top: 50%;
  transform: translateY(-50%);
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  pointer-events: none;
  opacity: 0;
  transition: opacity .45s ease;
}
.ij-journey:not(.is-live) .ij-rail { display: none; }
.ij-rail[data-visible="1"] { opacity: 1; }
.ij-rail__label {
  writing-mode: vertical-rl;
  font-family: var(--font-mono);
  font-size: .58rem;
  letter-spacing: .30em;
  text-transform: uppercase;
  color: rgba(245,241,232,.62);
  text-shadow: 0 1px 10px rgba(0,0,0,.9);
  white-space: nowrap;
}
.ij-rail__track {
  position: relative;
  width: 2px;
  height: clamp(70px, 12vh, 120px);
  border-radius: 2px;
  background: rgba(245,241,232,.16);
  overflow: hidden;
}
.ij-rail__fill {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: calc(var(--rail, 0) * 100%);
  background: linear-gradient(to bottom, var(--red-text), var(--red, #c8102e));
  box-shadow: 0 0 10px rgba(200,16,46,.65);
}

/* ══════════════════════════════════════════════════════════════════════
   THE MOMENTS
   Static default = normal document flow, fully visible, stacked. Journey
   mode = absolutely positioned overlays whose opacity JS drives.
   ══════════════════════════════════════════════════════════════════════ */
.ij-content { position: relative; z-index: 4; }

/* THE CONTAINING BLOCK, AND IT IS LOAD-BEARING — this one line was the
   "timing and placement are SUPER OFF" defect, measured rather than
   guessed. In journey mode every .ij-moment is position:absolute with
   top:50%, and percentage `top` resolves against the nearest POSITIONED
   ancestor's height. That ancestor was .ij-content, which is
   position:relative and — with every one of its children absolute — has
   no in-flow content left, so it collapses to height 0. top:50% of 0 is
   0, and all six moments centred on the very top edge of the viewport
   while the emblem (a child of .ij-pin, which is sticky and therefore
   correctly sized) centred properly at the middle.

   Measured across seven viewports before the fix: the emblem/copy centre
   drift was exactly half the viewport height every time — 370px at
   1440x740, 450px at 1440x900, 540px at 1920x1080, 650px at 2560x1300 —
   which is the signature of this bug, not of an easing or timing error.
   Making .ij-content fill the pin gives top:50% a real height to resolve
   against, and the copy lands on the emblem's own centre line. */
.ij-journey.is-live .ij-content { position: absolute; inset: 0; }

.ij-journey:not(.is-live) .ij-content {
  display: flex;
  flex-direction: column;
  gap: clamp(3.5rem, 9vw, 7rem);
  padding: calc(var(--header-h, 72px) + clamp(3rem, 8vw, 5rem)) clamp(1.25rem, 5vw, 3rem) clamp(4rem, 9vw, 7rem);
  max-width: 1140px;
  margin: 0 auto;
}

.ij-journey.is-live .ij-moment {
  position: absolute;
  z-index: 4;
  top: 50%;
  width: min(42%, 520px);
  opacity: 0;
  /* JS writes opacity and the translate every frame. will-change is on the
     two properties it actually touches, nothing else. */
  will-change: opacity, transform;
}

/* DEFENSIVE: the journey owns its own opacity, at the .ij-moment level and
   nowhere else. js/home-scroll.js's ladder() used to stamp an inline
   opacity:0 on the first h2 inside any <section> and only clear it once it
   saw 50% of that section on screen — unreachable on a 600vh track, so
   beat 1's headline was permanently invisible on the deployed preview
   while measuring as otherwise perfectly correct. That script is no longer
   loaded here (see the note in index-full.html), but this rule means any
   future script doing the same thing cannot silently blank a beat: only
   .ij-moment fades, its contents never do. !important beats an inline
   style, which is exactly what has to be overridden. */
.ij-journey.is-live .ij-moment > *,
.ij-journey.is-live .ij-moment > * > * {
  opacity: 1 !important;
}
/* The one child that legitimately has its own opacity: the beat's glow
   slab is part of the art, not the copy. */
.ij-journey.is-live .ij-beat__glow { opacity: 1 !important; }
/* Which side of the emblem the copy sits on. data-side is authored in the
   markup next to the beat it belongs to, so the alternation is readable in
   one place rather than split between two files. */
.ij-journey.is-live .ij-moment[data-side="left"]  { left: clamp(1.5rem, 6vw, 6rem); }
.ij-journey.is-live .ij-moment[data-side="right"] { right: clamp(1.5rem, 6vw, 6rem); }
.ij-journey.is-live .ij-moment[data-side="center"] {
  left: 50%;
  width: min(72%, 640px);
  text-align: center;
}

/* ── OPEN ─────────────────────────────────────────────────────────────── */
.ij-open__eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 14px;
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: 11px;
  line-height: 1;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--red-text);
  margin: 0 0 1.35rem;
}
.ij-open__eyebrow::before {
  content: "";
  width: 36px;
  height: 1px;
  background: var(--red, #c8102e);
  display: inline-block;
}
/* The real wordmark asset, not the word set in a display face. LIGHT
   version, not the crimson one: measured against this ground the crimson
   mark is 2.64:1 (fails the 3:1 large-text floor) and the cream is 13.80:1.
   The body is dark sitewide, so the cream mark is simply the right one and
   there is no theme swap to get wrong. */
.ij-open__wordmark { margin: 0 0 1.5rem; line-height: 0; }
.ij-open__wordmark img { width: min(86%, 560px); height: auto; display: block; }
.ij-journey.is-live .ij-moment[data-side="center"] .ij-open__wordmark img { margin: 0 auto; }

.ij-open__sub {
  font-family: var(--font-display);
  font-size: clamp(1.05rem, 1.7vw, 1.32rem);
  line-height: 1.55;
  color: var(--on-black-mute);
  max-width: 32em;
  margin: 0 0 2.1rem;
  text-shadow: 0 2px 20px rgba(10,10,10,.75);
}
.ij-open__ctas { display: flex; flex-wrap: wrap; gap: .8rem 1rem; align-items: center; }

/* ── BEATS ────────────────────────────────────────────────────────────── */
/* The red glow behind the copy Ethan asked for. A slab, not a halo: it is
   what guarantees the type clears contrast over whatever video frame
   happens to be behind it at that scroll position. */
.ij-beat__glow {
  position: absolute;
  z-index: -1;
  inset: -12% -16%;
  pointer-events: none;
  background:
    radial-gradient(ellipse 62% 58% at 40% 46%, rgba(200,16,46,.26) 0%, rgba(200,16,46,.08) 46%, transparent 72%),
    radial-gradient(ellipse 78% 70% at 46% 50%, rgba(10,10,10,.86) 0%, rgba(10,10,10,.62) 50%, transparent 78%);
}
.ij-beat { position: relative; }
.ij-beat--right .ij-beat__glow { background-position: right center; }

.ij-beat__eyebrow {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: 11px;
  line-height: 1;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--red-text);
  margin: 0 0 1rem;
}
.ij-beat__h2 {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2rem, 4.4vw, 3.4rem);
  line-height: 1.05;
  letter-spacing: -.02em;
  color: var(--on-black);
  margin: 0 0 1.15rem;
  text-shadow: 0 2px 28px rgba(10,10,10,.8);
}
.ij-beat__h2 em { font-style: italic; color: var(--red-text); }
.ij-beat__body {
  font-family: var(--font-display);
  font-size: clamp(1rem, 1.5vw, 1.18rem);
  line-height: 1.6;
  color: var(--on-black-mute);
  margin: 0;
  max-width: 34em;
  text-shadow: 0 1px 16px rgba(10,10,10,.8);
}
.ij-beat__list {
  list-style: none;
  margin: 1.4rem 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: .5rem .6rem;
}
.ij-beat__list li {
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: rgba(245,241,232,.82);
  border: 1px solid rgba(245,241,232,.22);
  border-radius: 999px;
  padding: .5rem .85rem;
  background: rgba(10,10,10,.5);
}

/* ── CLOSE ────────────────────────────────────────────────────────────────
   The emblem grows large here and this copy sits INSIDE it, so the type is
   reading against brand red rather than against the video. A dark halo is
   what carries cream over #c8102e; the red glow that helps the beats would
   only lower contrast here, so it is deliberately not repeated. */
.ij-close__eyebrow {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: 11px;
  line-height: 1;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--on-black);
  margin: 0 0 1.1rem;
  text-shadow: 0 2px 12px rgba(10,10,10,.95);
}
.ij-close__h2 {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2rem, 4.6vw, 3.5rem);
  line-height: 1.06;
  letter-spacing: -.02em;
  color: #fff;
  margin: 0 0 1.9rem;
  text-shadow: 0 2px 22px rgba(10,10,10,.95), 0 0 60px rgba(10,10,10,.8);
}
.ij-close__h2 em { font-style: italic; color: #fff; }

/* ══════════════════════════════════════════════════════════════════════
   RESPONSIVE
   The journey still runs on a phone — it is the whole page, so disabling it
   would leave nothing. What changes: the emblem centres and shrinks (JS
   uses a separate keyframe set, see MOBILE in js/index-journey.js), copy
   goes full width and sits over the mark rather than beside it, and the
   rail is dropped because it competes with thumbs.
   ══════════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  .ij-journey.is-live .ij-moment,
  .ij-journey.is-live .ij-moment[data-side="left"],
  .ij-journey.is-live .ij-moment[data-side="right"],
  .ij-journey.is-live .ij-moment[data-side="center"] {
    left: 50%;
    right: auto;
    width: min(88%, 560px);
  }
  .ij-emblem { width: clamp(150px, 52vw, 300px); }
  .ij-rail { display: none; }
  .ij-video { opacity: .3; }
  .ij-tower { width: 64%; right: -8%; }
  .ij-beat__glow { inset: -14% -20%; }
  .ij-open__wordmark img { width: min(78vw, 430px); }
}

@media (max-width: 560px) {
  .ij-beat__h2 { font-size: clamp(1.75rem, 8.4vw, 2.3rem); }
  .ij-close__h2 { font-size: clamp(1.7rem, 8vw, 2.2rem); }
  .ij-beat__body { font-size: 1rem; }
}

/* ══════════════════════════════════════════════════════════════════════
   REDUCED MOTION — explicit per-layer kill (ART-GUIDE §2 rule 5).
   js/index-journey.js never adds .is-live under reduced motion, so the
   journey itself never exists to be stopped; what remains is the ambient
   CSS on the emblem and glow, killed here, plus the video, which is the one
   thing that would still be moving.
   ══════════════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: reduce) {
  .ij-emblem__spin,
  .ij-emblem__glow,
  .ij-rail {
    animation: none !important;
    transition: none !important;
    will-change: auto !important;
  }
  .ij-video { display: none; }
  .ij-journey { height: auto !important; }
  .ij-pin { position: relative !important; height: auto !important; }
}

/* The footer must paint above the pinned journey. This rule used to come
   from css/storm.css, which this page no longer links — re-declared here so
   the dependency is owned by the file that actually needs it rather than
   inherited from a stylesheet kept around for a different page. */
.tnaado-footer { position: relative; z-index: 8; }

/* The <h1> on this page is the wordmark image, so its only text is the
   alt attribute, "TNAADO". That names the firm but says nothing about what
   it does. This gives the heading a real sentence for search engines and
   screen readers without putting a second line of type next to a logo that
   is deliberately alone on the frame. Clipped rather than display:none,
   which would remove it from the accessibility tree along with everything
   else. */
.ij-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
