/* ============================================================
   RAW & HUNGRY — first-load intro
   ------------------------------------------------------------
   A five-second curtain over the first page view of a visit: the
   wordmark composites in at display size, the NULL-bot climbs out
   of a lit gap at four times its usual size, greets the visitor,
   loads the logo onto its own CRT, then shrinks and flies to the
   spot the ambient mascot lives on — the centre of the hero's
   innermost orbit ring.

   The timeline lives in assets/js/rh-null-bot-intro.js. Everything
   here is either a start state or a named beat the script toggles,
   so the choreography reads in one file instead of being split
   between keyframes and code.

   Painting rules, in order of how badly they bite:

   1. The overlay must be opaque on the FIRST paint, or the visitor
      sees the page and then has it covered up. So the markup is
      printed in the body by the theme (see header.php) and this
      file is a blocking stylesheet — not a class JS adds later.
   2. It must not paint at all on a repeat view. `html.rh-intro-off`
      is set by an inline script before the body renders; the
      <noscript> block in header.php covers JS being off.
   3. Nothing may clip: the bot leaves the stage's layout box on the
      last beat. Hence `contain: layout style` and no `paint`, and
      no overflow on any ancestor. The one intentional clip is the
      inset() on __lift, which is what hides the half of the bot
      that is still inside the hole.
   ============================================================ */

.rh-intro {
  position: fixed;
  inset: 0;
  /* Above the route-transition curtain (1000) and the header (100). The
     difference-blended .cursor-dot at 9999 goes under too: during the intro the
     visitor has not moved the pointer yet, and a blend-mode dot appearing over
     the wordmark reads as a rendering fault rather than as a cursor. */
  z-index: 10000;
  pointer-events: none;
  /* No `paint` — the bot's closing flight leaves this box. */
  contain: layout style;
}

/* Repeat view in the same tab, or the run has finished. Both are display:none
   rather than opacity:0 so a stale overlay can never eat a pointer event or
   hold a compositor layer for the rest of the session. */
html.rh-intro-off .rh-intro,
html.rh-intro-done .rh-intro {
  display: none;
}

/* Scroll lock. `overflow: hidden` on the root is also how the theme stops Lenis
   (.lenis-stopped), so this needs no cooperation from the prebuilt motion
   bundle — with nothing to overflow, its scrollTo writes go nowhere. Any wheel,
   touch, key or click releases it by skipping the intro, so it is never a trap. */
html.rh-intro-active,
html.rh-intro-active body {
  overflow: hidden;
}

/* ---------- Stage ---------- */

.rh-intro__veil {
  position: absolute;
  inset: 0;
  background-color: var(--bg);
  /* One warm pool low in the frame, sitting under the gap the bot climbs out of.
     The stage would otherwise be flat black behind a bright ellipse, and the
     ellipse would read as a sticker rather than as a hole in something. */
  background-image: radial-gradient(122% 92% at 50% 74%,
                    rgba(255, 74, 28, 0.13) 0%,
                    rgba(255, 74, 28, 0.04) 38%,
                    transparent 66%);
  opacity: 1;
  transition: opacity .66s var(--ease);
}

.rh-intro.is-out .rh-intro__veil {
  opacity: 0;
}

.rh-intro__stage {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(14px, 3.2vh, 40px);
  padding-inline: var(--gut);
  /* The bot is sized off the viewport so the composition survives a short
     laptop window as well as a tall phone. 88x136 is the sprite's CSS box in the
     follower, so the ratio here is the follower's ratio — which is what lets the
     closing flight be a pure scale with no reflow. */
  --rh-intro-bot-h: clamp(164px, 30vh, 340px);
  --rh-intro-bot-w: calc(var(--rh-intro-bot-h) * 88 / 136);
}

/* ---------- Wordmark ---------- */

.rh-intro__markwrap {
  position: relative;
  display: inline-block;
  white-space: nowrap;
  /* Both layers are the same string at the same size; the ghost is laid over the
     main one and has to measure identically or the registration effect below
     turns into a double image. */

  /* Hidden until the timeline has split the string into letters.
     The start state below lives on .rh-intro__ch, and those spans do not exist
     until splitMark() runs — which can be up to 700ms after first paint, because
     the intro waits on document.fonts.ready. Until then the plates hold plain
     text with nothing hiding it, so the wordmark painted fully opaque, snapped to
     invisible the moment JS wrapped the characters, and then faded back in. That
     was the second half of the reported first-second blink.
     visibility, not opacity: it must not be animatable, and it must still let the
     browser lay the text out so the box is measured and the composition does not
     jump when it appears. */
  visibility: hidden;
}

.rh-intro.is-armed .rh-intro__markwrap {
  visibility: visible;
}

.rh-intro__mark {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(2.4rem, 8.4vw, 6.4rem);
  line-height: 0.92;
  letter-spacing: -0.035em;
  color: var(--text);
  white-space: nowrap;
}

/* The composite: a second ember plate, offset and blurred, converging onto the
   black one as the letters land. It is the registration of two printing plates,
   which is where the identity comes from — and it does more for the "assembled
   from parts" reading than any per-letter easing curve on its own would. */
.rh-intro__mark--ghost {
  position: absolute;
  inset: 0;
  color: var(--ember);
  mix-blend-mode: screen;
  opacity: 0;
  pointer-events: none;
  transition: opacity .9s var(--ease) .18s;
}

.rh-intro.is-mark-in .rh-intro__mark--ghost {
  opacity: 0.4;
}

.rh-intro.is-mark-out .rh-intro__mark--ghost {
  opacity: 0;
  transition-duration: .5s;
  transition-delay: 0s;
}

.rh-intro__amp {
  color: var(--ember);
  padding-inline: 0.02em;
}

/* Per-letter start state. JS splits the string and stamps --rh-ch-delay on each
   span; the transition only exists in the .is-mark-in rule, so the letters sit
   in their start pose with no animation running until the beat fires. */
.rh-intro__ch {
  display: inline-block;
  opacity: 0;
  transform: translate3d(0, 0.4em, 0) scale(1.07);
  filter: blur(13px);
}

.rh-intro__mark--ghost .rh-intro__ch {
  transform: translate3d(-0.055em, -0.3em, 0) scale(1.13);
}

.rh-intro.is-mark-in .rh-intro__ch {
  opacity: 1;
  transform: none;
  filter: blur(0);
  transition: opacity .8s var(--ease), transform .8s var(--ease), filter .8s var(--ease);
  transition-delay: var(--rh-ch-delay, 0ms);
}

.rh-intro.is-mark-out .rh-intro__mark {
  opacity: 0;
  transform: translate3d(0, -0.1em, 0) scale(0.985);
  filter: blur(6px);
  transition: opacity .52s var(--ease), transform .52s var(--ease), filter .52s var(--ease);
}

/* Whole-plate fade on the way out, so the letters leave together. Re-asserting
   the start-state properties per letter here would fight the plate transition
   and stagger the exit, which reads as the logo falling apart. */
.rh-intro.is-mark-out .rh-intro__ch {
  transition: none;
}

/* ---------- Speech bubble ----------
   A flex item rather than an absolutely positioned balloon beside the head. The
   head is 30 of the sprite's 44 columns wide, so at this scale a side-anchored
   bubble either covers the bezel or runs off a phone; in the column it always
   fits, and the tail below it does the pointing. */

.rh-intro__say {
  margin: 0;
  max-width: min(30ch, 82vw);
  position: relative;
  padding: 10px 18px;
  border: 1px solid rgba(255, 74, 28, 0.4);
  border-radius: 14px;
  background: rgba(11, 10, 9, 0.94);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  box-shadow: 0 12px 34px rgba(0, 0, 0, 0.6), inset 0 1px 1px rgba(255, 255, 255, 0.14);
  color: var(--text);
  font-family: var(--font-mono);
  font-size: clamp(0.72rem, 1.5vw, 0.95rem);
  font-weight: 500;
  letter-spacing: 0.04em;
  line-height: 1.45;
  text-align: center;
  /* Held out of the layout's way but still measured: the row keeps its height
     from the first frame, so the bubble arriving never nudges the wordmark or
     the bot. visibility rather than display for the same reason. */
  opacity: 0;
  visibility: hidden;
  transform: translate3d(0, 8px, 0) scale(0.96);
  transition: opacity .34s var(--ease), transform .34s var(--ease), visibility .34s;
}

.rh-intro.is-say .rh-intro__say {
  opacity: 1;
  visibility: visible;
  transform: none;
}

.rh-intro__say::after {
  content: '';
  position: absolute;
  left: 50%;
  bottom: -6px;
  width: 10px;
  height: 10px;
  transform: translateX(-50%) rotate(45deg);
  background: rgba(11, 10, 9, 0.94);
  border-right: 1px solid rgba(255, 74, 28, 0.4);
  border-bottom: 1px solid rgba(255, 74, 28, 0.4);
}

/* The caret only exists while the line is still arriving. */
.rh-intro__caret {
  display: inline-block;
  width: 0.55em;
  margin-left: 0.12em;
  border-bottom: 2px solid var(--ember);
  vertical-align: baseline;
  animation: rh-intro-caret 0.9s steps(2, jump-none) infinite;
}

@keyframes rh-intro-caret {
  0%, 49% { opacity: 1; }
  50%, 100% { opacity: 0.15; }
}

/* ---------- The gap, and the bot in it ---------- */

.rh-intro__rise {
  position: relative;
  width: var(--rh-intro-bot-w);
  height: var(--rh-intro-bot-h);
  flex: 0 0 auto;
}

/* Wide, soft, and behind everything: the light the hole throws into the room.
   Screen-blended so it lifts the veil's warm pool instead of painting over it. */
.rh-intro__halo {
  position: absolute;
  left: 50%;
  bottom: 0;
  width: calc(var(--rh-intro-bot-w) * 5.2);
  height: calc(var(--rh-intro-bot-w) * 2.4);
  transform: translate(-50%, 50%) scale(0.4);
  border-radius: 50%;
  background: radial-gradient(closest-side,
              rgba(255, 122, 61, 0.5) 0%,
              rgba(255, 74, 28, 0.24) 42%,
              rgba(255, 74, 28, 0) 78%);
  mix-blend-mode: screen;
  opacity: 0;
  filter: blur(14px);
  transition: opacity .9s var(--ease), transform 1.1s var(--ease);
}

/* The hole itself. A flat ellipse, not a circle — it is an opening in a ground
   plane, and the same shape drawn round reads as a ball of light in mid air. */
.rh-intro__core {
  position: absolute;
  left: 50%;
  bottom: 0;
  width: calc(var(--rh-intro-bot-w) * 1.95);
  height: calc(var(--rh-intro-bot-w) * 0.42);
  transform: translate(-50%, 50%) scaleX(0.06);
  border-radius: 50%;
  background: radial-gradient(closest-side,
              #FFF3EA 0%,
              var(--o-light) 16%,
              var(--o) 34%,
              var(--ember) 58%,
              rgba(194, 57, 15, 0.55) 82%,
              rgba(194, 57, 15, 0) 100%);
  /* Lit near rim. An inset shadow on a 50% radius follows the ellipse, which is
     what gives the opening an edge to be in front of the bot's feet. */
  box-shadow: inset 0 3px 7px -2px rgba(255, 243, 234, 0.95),
              0 0 26px rgba(255, 74, 28, 0.55);
  opacity: 0;
  transition: opacity .5s var(--ease), transform .82s var(--ease);
}

/* The specular streak — the "shiny" of a shiny hole. Kept as its own element so
   it can stay sharp while the core stays soft. */
.rh-intro__shine {
  position: absolute;
  left: 50%;
  bottom: 0;
  width: calc(var(--rh-intro-bot-w) * 0.72);
  height: 3px;
  transform: translate(-50%, calc(50% - 2px)) scaleX(0.1);
  border-radius: 999px;
  background: linear-gradient(90deg, transparent, #FFF7F0 44%, #FFFFFF 50%, #FFF7F0 56%, transparent);
  opacity: 0;
  filter: blur(0.4px);
  transition: opacity .4s var(--ease), transform .8s var(--ease);
}

.rh-intro.is-stage .rh-intro__halo {
  opacity: 1;
  transform: translate(-50%, 50%) scale(1);
}

.rh-intro.is-stage .rh-intro__core {
  opacity: 1;
  transform: translate(-50%, 50%) scaleX(1);
}

.rh-intro.is-stage .rh-intro__shine {
  opacity: 0.9;
  transform: translate(-50%, calc(50% - 2px)) scaleX(1);
}

/* Closing: the gap shuts before the veil lifts, so the page is never revealed
   with a light source on it that does not belong to the page. */
.rh-intro.is-portal-out .rh-intro__halo {
  opacity: 0;
  transform: translate(-50%, 50%) scale(0.55);
  transition-duration: .5s, .5s;
}

.rh-intro.is-portal-out .rh-intro__core {
  opacity: 0;
  transform: translate(-50%, 50%) scaleX(0.04);
  transition-duration: .42s, .46s;
}

.rh-intro.is-portal-out .rh-intro__shine {
  opacity: 0;
  transform: translate(-50%, calc(50% - 2px)) scaleX(0.06);
  transition-duration: .3s, .4s;
}

/* Feet anchor, matching .rh-null-bot: the closing flight is a translate plus a
   scale in one transform, and with the origin at the feet the anchor point stays
   put at any size. JS owns this transform outright — nothing here may set one,
   which is why the horizontal centring is done by sizing __rise to the sprite
   instead of by translateX(-50%). */
.rh-intro__bot {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 100%;
  transform-origin: 50% 100%;
  opacity: 0;
  transition: opacity .28s linear;
}

.rh-intro.is-stage .rh-intro__bot {
  opacity: 1;
}

/* Handed over: the real follower is now standing in this exact spot at this
   exact size, so this copy cross-fades out underneath it. */
.rh-intro.is-handed .rh-intro__bot {
  opacity: 0;
}

/* Promoted one beat before the closing flight rather than for the whole run:
   is-portal-out lands 120ms before the transform starts, which is enough for the
   layer to exist by the time it is needed and short enough that the compositor is
   not holding it through the four seconds before. */
.rh-intro.is-portal-out .rh-intro__bot {
  will-change: transform;
}

/* The gap's mouth. A static box that clips whatever is pushed below it, which is
   all the rise needs: __lift is translated down inside this box, so the part of
   the sprite that is still under the rim is simply outside it.

   JS used to write a `clip-path: inset(0 0 n 0)` on __lift alongside the
   translate every frame. Same rows in the same place — clip-path resolves in the
   element's own box before its transform — but clip-path cannot be animated on
   the compositor, so all ~57 frames of the rise repainted the sprite layer
   instead of moving it.

   Inside __bot rather than around it, so the closing flight's scale applies to
   the clip box too. By then __lift is back at translate 0 and exactly fills this
   box, so nothing is clipped — and .rh-intro__sprite carries no filter or shadow
   (see below), so nothing legitimately reaches outside it either. */
.rh-intro__clip {
  width: 100%;
  height: 100%;
  overflow: hidden;
}

/* The rise. JS writes the translate; the clip above does the hiding. */
.rh-intro__lift {
  width: 100%;
  height: 100%;
  will-change: transform;
}

.rh-intro__sprite {
  display: block;
  width: 100%;
  height: 100%;
  /* Integer-scaled nearest neighbour, as everywhere else the mascot is drawn.
     No drop-shadow here, unlike .rh-null-bot__sprite: clip-path clips filter
     output, so a shadow would be sliced off along the rim of the hole. The
     portal's own glow grounds the bot instead. */
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  /* Sheet fallback, used when the runtime sprite engine or canvas is
     unavailable. background-position is stepped by JS, as in rh-null-bot.css. */
  background-repeat: no-repeat;
  background-size: auto 100%;
}

/* ---------- Skipped ----------
   The visitor pressed a key, scrolled or clicked, so the curtain leaves at a pace
   that reads as a dismissal rather than as the end of a performance. Shortened
   here rather than in JS because the durations that matter are the ones the
   overlay is removed in the middle of: the veil is torn down at 340ms, and a
   .66s fade cut off at half opacity is a visible snap. */
.rh-intro.is-skip .rh-intro__veil {
  transition-duration: .3s;
}

.rh-intro.is-skip .rh-intro__bot {
  transition-duration: .2s;
}

.rh-intro.is-skip .rh-intro__mark,
.rh-intro.is-skip .rh-intro__say {
  transition-duration: .22s;
}

/* ---------- Narrow viewports ----------
   Below the tablet break the wordmark is most of the width, so the bot has to
   give up height rather than the type give up size: at a phone width the logo
   IS the intro, and a 5rem wordmark over a small mascot reads better than the
   reverse. */
@media (max-width: 640px) {
  .rh-intro__stage {
    --rh-intro-bot-h: clamp(150px, 26vh, 240px);
    gap: clamp(12px, 2.4vh, 26px);
  }

  .rh-intro__say {
    max-width: 88vw;
    padding: 9px 14px;
  }
}

/* Short and wide — a laptop window in landscape, where 30vh of bot plus a
   display wordmark plus a bubble does not fit vertically. */
@media (max-height: 620px) {
  .rh-intro__stage {
    --rh-intro-bot-h: clamp(132px, 34vh, 210px);
    gap: clamp(10px, 2vh, 18px);
  }

  .rh-intro__mark {
    font-size: clamp(1.9rem, 6.4vw, 3.4rem);
  }
}

/* ---------- Reduced motion ----------
   Belt to the braces in the JS: the script bails before it ever adds a class,
   and this makes sure a stylesheet cached from before that check still cannot
   paint a curtain over the page. */
@media (prefers-reduced-motion: reduce) {
  .rh-intro {
    display: none;
  }
}
