/* ==========================================================================
   Workflow Studio — showreel
   intro.css
   The name-only curtain that opens the page, and its build.
   ========================================================================== */

/* --- Intro curtain --------------------------------------------------------
   An opaque plate over the whole viewport, holding the name alone: the mark,
   the word and its rule settle in, the plate is held for a beat, then it fades
   off the hero — whose own entrance is already running underneath, so the two
   cross-dissolve.

   Shown only while <html> carries .intro-playing, which an inline head script
   sets before first paint.

   The curtain holds for HOLD (intro.js) and the site arrives right after. Two
   things have to be finished by then: the four lockup pieces are staggered to
   land at 2 s, and the before/after under them has finished dissolving at
   3.2 s — so the hold is the longer of the two plus a beat spent on the
   finished plate, and the curtain then lifts over 700 ms. Shorten the hold
   without rescaling both and it goes out over a half-built name or a
   half-crossed plate.
   -------------------------------------------------------------------------- */

.intro {
  display: none;
}

.intro-playing {
  overflow: hidden;
}

.intro-playing .intro {
  position: fixed;
  inset: 0;
  z-index: 999;
  display: grid;
  place-items: center;
  background: var(--surface);
  /* Mirrored by FADE in intro.js, which removes the element once this is over:
     shorten one without the other and the curtain is either cut off mid-fade
     or left sitting in the document. */
  transition: opacity 0.7s cubic-bezier(0.33, 0, 0.2, 1);
}

/* --- The plate behind the name ---------------------------------------------
   The curtain used to be flat --surface, which is honest but reads as a
   loading screen. What it wants instead is the light of a room: a single warm
   pool sitting where the lockup is, still falling off when it runs out of
   screen, so nothing about it can be pointed at.

   Two layers, both out of flow. `position: absolute` matters here: .intro is a
   grid with place-items: center, and an in-flow pseudo-element would be
   centred as a grid item and size the plate with it.

   The stops are many and close because the ramp is only a few levels of an
   8-bit channel wide over a whole viewport — the same banding the grain exists
   to dither, and the reason ::after is here at all.

   There used to be a vignette painted over the pool to keep it off the
   corners. On a wide viewport its ellipse closed inside the screen, so the
   darkening had a visible outer arc — a halo around the plate, which is the
   one thing a pool of light must not have. It is gone: every layer here is now
   sized past the viewport so its last stop falls off-screen, and the plate is
   one uninterrupted ramp from the lockup to the edge.
   -------------------------------------------------------------------------- */

.intro-playing .intro::before,
.intro-playing .intro::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.intro-playing .intro::before {
  background-image:
    /* The pool, off-centre high: the lockup's optical centre is above the
       geometric one, and light landing under a name looks like a shadow.
       Sized well past the viewport (140% / 115%) and still carrying colour at
       its last stop: the ramp is cut by the edge of the screen instead of
       ending on it, which is what leaves no rim to see. */
    radial-gradient(
      ellipse 140% 115% at 50% 42%,
      rgba(216, 181, 111, 0.034) 0%,
      rgba(216, 181, 111, 0.028) 14%,
      rgba(216, 181, 111, 0.022) 28%,
      rgba(216, 181, 111, 0.015) 42%,
      rgba(216, 181, 111, 0.01) 56%,
      rgba(216, 181, 111, 0.006) 70%,
      rgba(216, 181, 111, 0.003) 84%,
      rgba(216, 181, 111, 0.001) 100%
    ),
    /* A cold rim under it, to keep the warm pool from reading as a stain: the
       eye needs something neutral nearby to call the gold gold. Its centre is
       below the fold, so only the top of the ramp is ever on screen. */
    radial-gradient(
      ellipse 120% 70% at 50% 112%,
      rgba(255, 255, 255, 0.016) 0%,
      rgba(255, 255, 255, 0.01) 30%,
      rgba(255, 255, 255, 0.006) 52%,
      rgba(255, 255, 255, 0.002) 74%,
      rgba(255, 255, 255, 0) 100%
    );
  /* Blooms with the lockup rather than being there when the plate opens: the
     delay clears the mark's own entrance, and the duration lands with the
     tagline at ~2 s, well inside the 3.5 s hold. Retime the pieces above and
     this follows them. */
  animation: intro-bloom 1.9s cubic-bezier(0.22, 1, 0.36, 1) 0.2s both;
}

/* The dither covers the plate flat, with no mask of its own. It used to be
   masked to an ellipse so it sat on the pool and faded past it — but any mask
   is another shape with an edge, and at this opacity the grain is invisible
   where there is nothing to dither anyway. Flat is both cleaner and one fewer
   ramp that can band. */
.intro-playing .intro::after {
  background-image: var(--grain);
  background-size: var(--grain-size) var(--grain-size);
  opacity: 0.06;
}

@keyframes intro-bloom {
  from {
    opacity: 0;
    transform: scale(1.08);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* The bloom is a light coming up, not a move: with motion off it is simply
   already on. The plate keeps its background either way — it is a colour, not
   an animation. */
@media (prefers-reduced-motion: reduce) {
  .intro-playing .intro::before {
    animation: none;
  }
}

/* --- The lift ---------------------------------------------------------------
   The name does not simply dim: it pulls away from the viewer and clears
   before the plate does, so what uncovers the hero is an empty black screen
   rather than a screen with a name still sitting on it. The plate follows a
   beat later, and the hero has been playing underneath since .intro-done went
   on, so the two cross-dissolve instead of the site waiting behind a curtain.

   Both properties are set on the lockup and not on its four pieces, which
   would have made for a nicer stagger: those pieces carry `both`-filled
   entrance animations, and a filling animation outranks a declarative value,
   so a per-piece exit would simply never apply. The lockup animates nothing of
   its own, which is exactly what leaves it free to move.
   -------------------------------------------------------------------------- */

.intro.is-out {
  opacity: 0;
  pointer-events: none;
}

.intro.is-out .intro-lockup {
  opacity: 0;
  transform: scale(1.06) translateY(-14px);
  /* The name is gone well before the plate is, which is the whole effect —
     retime the two together or it turns back into a single block fading. */
  transition:
    opacity 0.42s ease-in,
    transform 0.7s cubic-bezier(0.22, 1, 0.36, 1);
}

/* A width of its own, and it is not cosmetic. The word below animates its
   letter-spacing, which is a layout property, and with this block sized to its
   contents the widest child — the title — rewrites the block's width on every
   frame. place-items: center then recentres everything inside it a fraction of
   a pixel at a time. Text survives that; a photograph rasterised at a new
   sub-pixel offset sixty times a second does not, and flickers.

   So the block is measured and the tracking has nothing left to push. Anything
   dropped in here later inherits that protection rather than rediscovering the
   problem. */
.intro-lockup {
  position: relative;
  /* Above the plate drawn by .intro::before / ::after. */
  z-index: 1;
  width: min(760px, 100vw - 32px);
  padding: 0 24px;
  text-align: center;
}

.intro-mark {
  width: 46px;
  height: 46px;
  color: var(--accent);
  margin-inline: auto;
  filter: drop-shadow(0 0 14px rgba(216, 181, 111, 0.35));
  animation: intro-piece 1.22s cubic-bezier(0.22, 1, 0.36, 1) both;
}

.intro-word {
  margin: 26px 0 0;
  /* The floor has to clear the tracking below, not just the glyphs: the name is
     never wrapped, and intro-word-in opens it by 0.14em before closing to
     -0.02em, so the widest the block is ever asked to be is about a fifth
     past its resting width. A 34px floor was set for legibility and left the
     name clipped on any phone under ~480px for the first second of the
     animation. 6.4vw is within a pixel of the old 6.2vw everywhere the old
     floor was not binding, so the desktop lockup is unchanged. */
  font-size: clamp(20px, 6.4vw, 64px);
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -0.02em;
  color: var(--text);
  /* Never wrapped: the tracking below opens the title by about a fifth of its
     width, and a line break taken and given back mid-animation would move
     everything under it — the same problem as the width above, in the other
     axis. */
  white-space: nowrap;
  /* Tracking closing in as it fades up: the name assembles instead of merely
     appearing. Safe to animate a layout property only because the block that
     holds it is measured — see .intro-lockup. */
  animation: intro-word-in 1.5s cubic-bezier(0.22, 1, 0.36, 1) 0.14s both;
}

.intro-rule {
  display: block;
  width: min(280px, 60vw);
  height: 1px;
  margin: 30px auto 0;
  background: linear-gradient(90deg, transparent, var(--accent), transparent);
  animation: intro-wipe 1.22s cubic-bezier(0.22, 1, 0.36, 1) 0.57s both;
}

.intro-tagline {
  margin-top: 20px;
  font-size: 13px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-muted);
  animation: intro-piece 1.22s ease 0.78s both;
}

@keyframes intro-piece {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes intro-word-in {
  from {
    opacity: 0;
    letter-spacing: 0.14em;
    transform: translateY(12px);
  }
  to {
    opacity: 1;
    letter-spacing: -0.02em;
    transform: none;
  }
}

@keyframes intro-wipe {
  from {
    transform: scaleX(0.2);
    opacity: 0;
  }
  to {
    transform: none;
    opacity: 1;
  }
}

/* --- The before / after under the name -------------------------------------
   The curtain is holding the pitch as well as the name: the shot, then what
   the tools did to it. It arrives after the tagline, sits on the untreated
   plate long enough to be read as a photograph, and the seam then runs the
   whole way across and stays there. This one is watched rather than dragged,
   which is why it ends on the finished frame instead of falling back to the
   middle the way the hero's sweep does — there is nothing here to hand over
   to, the curtain is about to go.

   These timings and HOLD in intro.js are one clock: the wipe has to be over
   before the curtain lifts. Retime either and the other follows.
   -------------------------------------------------------------------------- */

.intro-compare {
  position: relative;
  /* Capped as a width and not a max-height, same trap as compare.css: a height
     cap takes the box off 3/2 and cover starts cropping, which slides the two
     plates out of register under the seam. */
  width: min(420px, 62vw, 48svh);
  margin: 30px auto 0;
  aspect-ratio: 3 / 2;
  overflow: hidden;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: var(--surface-raised);
  /* A fade, not intro-piece's rise: the plates are the one thing on this
     screen that must not move. */
  animation: intro-fade-in 0.7s ease 0.95s both;
}

.intro-compare img {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* --- The dissolve -----------------------------------------------------------
   The curtain does not wipe: it cross-dissolves. The site's own comparator, in
   the trust strip, is the one that wipes and is dragged — this is the tease,
   and it only has to make one point, which is that the plate changed.

   The reason it is a dissolve and not a small wipe is worth writing down: a
   wipe moves an edge, and an edge is either a one-pixel line or the boundary
   between two plates. Either way it lands on fractional device pixels frame
   after frame, is antialiased across two columns of them, and its brightness
   oscillates — which is seen as a flicker and cannot be composed away. It is
   what a moving hairline does. Opacity has no geometry, so there is nothing to
   round and nothing to shimmer.

   The frame is also static again, which is what lets the rounded corner come
   back: a border-radius clip over a child that moves has to be re-masked every
   frame, and nothing here moves any more.
   -------------------------------------------------------------------------- */

.intro-compare-after {
  position: absolute;
  inset: 0;
  animation: intro-dissolve 1.5s ease 1.7s both;
}

@keyframes intro-fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes intro-dissolve {
  /* Held, crossed, held. The pause on either side is what makes it read as one
     shot before and after rather than as two pictures taking turns. */
  0%,
  25% {
    opacity: 0;
  }
  70%,
  100% {
    opacity: 1;
  }
}

/* ...and the site settles in underneath. */
.intro-playing .nav {
  opacity: 0;
}

/* Keyed off .intro-done alone: the entrance keeps running after the scroll
   lock (.intro-playing) is lifted. */
.intro-done .nav {
  animation: intro-fade 0.5s ease 0.15s forwards;
}

@keyframes intro-fade {
  to {
    opacity: 1;
  }
}
