/* =========================================================================
   DreamPack Studio — the transformation section
   -------------------------------------------------------------------------
   A tall section with a pinned stage. The canvas inside it is painted by
   js/scrub.js; everything here is the frame around it — the vignette that
   stops it reading as a rectangle pasted onto the page, the captions that
   name each state, and the rail that says how far through you are.

   The section owns 400vh. That is three viewports of scroll spent on one
   idea, which is only justified because it IS the idea: the product turns
   a description into a document, and this is the page saying so without a
   paragraph of copy.
   ========================================================================= */

/* --span is how many stage-heights of scroll the section owns. Four means
   three viewports of pinned scrubbing (four, minus the one the stage itself
   occupies). The number lives here so the breakpoint below can change it,
   but scrub.js converts it to an exact pixel height at boot — see measure().

   WHY NOT JUST LEAVE IT AT `height:400vh`
   Pin distance is (section height − stage height), so the two have to be in
   the same unit. They are not: the stage is 100dvh and this is vh, and on
   iOS Safari those differ by the height of the browser chrome — vh is
   measured with the toolbars retracted, dvh with them as they are. The
   section would be sized against the tall number while the pin consumes the
   short one, and the sequence would scrub faster than it was tuned for by
   however much chrome is showing. Desktop never sees it; a phone always
   does. The calc below is the no-JS fallback; JS makes the two exact. */
.scrub{
  position:relative;
  --span:4;
  --p:0;                        /* scroll progress through the section; scrub.js writes it */
  height:calc(var(--span) * 100vh);
  background:var(--void);
  z-index:1;
}

/* =========================================================================
   Carrying the light down
   -------------------------------------------------------------------------
   The room above is lit by a row of filament bulbs; this section is a dark
   studio. Cutting straight from one to the other left a band of flat black
   across the join long enough to read as a gap in the page rather than as a
   change of scene.

   So the bulbs spill into the top of this section and fade out as you go
   further in — by the time the code garment is the subject, the room it came
   from is gone. It is the same trick as the hero's own exit: overlap the two
   states rather than butting them together.
   ========================================================================= */
.scrub__stage::before{
  content:"";
  position:absolute;left:0;right:0;top:0;height:58%;
  z-index:1;pointer-events:none;

  /* THE TOP STOP IS TRANSPARENT, AND THAT IS THE WHOLE POINT.

     This spill used to put its maximum exactly where the section begins —
     `rgba(40,50,64,.5)` at 0%, plus a warm radial centred at `50% -6%`, which
     is brightest at that same edge. Composited, the first row of this section
     came out #363031 against a hero ending on #090D11: a channel-sum
     difference of 112, which is not a blend, it is a stripe. The gradient
     meant to hide the join was the thing drawing it.

     So it starts at nothing, matching the void above it exactly, and swells
     in below the edge where there is no boundary for it to outline.

     The warmth is now stops in THIS gradient rather than a second radial
     layered over it. A radial cannot be pinned to zero at a straight edge —
     its falloff is measured from a centre, so wherever you put that centre
     some of the top row keeps colour. One vertical gradient can be exactly
     transparent at exactly 0%, which is the only value that guarantees no
     step. Same light, no line under it. */
  background:linear-gradient(to bottom,
    transparent 0%,
    rgba(44,42,48,.26) 16%,
    rgba(38,40,50,.30) 34%,
    rgba(26,31,41,.12) 62%,
    transparent 92%);

  /* Gone by a fifth of the way in. Any longer and it stops being a spill
     from the room above and starts looking like this section is fogged. */
  opacity:clamp(0,calc(1 - var(--p,0) * 5),1);
}

/* =========================================================================
   The hero's exit
   -------------------------------------------------------------------------
   `--exit` is written by the rAF loop in site.js and runs 0 → 1 over the
   first ~85% of a viewport. Spent here, so the hero dissolves into the
   section below rather than sliding off the top of it.

   The two moves are deliberately different speeds: the WORDS leave first
   and fastest, because they have been read by then; the RENDER stays a
   beat longer and settles back a little, so the eye has something
   continuous to hold while the transformation underneath takes over.

   Only opacity and transform, so this stays on the compositor — the same
   rule the rest of the page's scroll work obeys.
   ========================================================================= */
.hero__in{
  opacity:calc(1 - var(--exit,0) * 1.25);
  transform:translate3d(0,calc(var(--exit,0) * -72px),0);
  will-change:transform,opacity;
}
.hero__scene img{
  transform:scale(calc(1 + var(--exit,0) * .07));
  transform-origin:50% 42%;   /* matches object-position, so it grows about the subject */
}
/* The cue is an invitation. Once the scroll has started it has been taken. */
.hero__scroll,
.hero__badge{opacity:calc(1 - var(--exit,0) * 2.4);}

/* The join itself. The hero's scrim reaches full void only at its very last
   pixel, so the lit floor of the render is still showing a few pixels above
   the boundary and the eye lands straight on the edge. Bringing the black up
   to 84% gives the hero and the section under it a shared band of nothing to
   meet in — the two are the same colour for ~100px before either one ends,
   so there is no line left to find. Only the last stop moves; the radial
   pool behind the headline is untouched. */
/* The scrim was doing as much of the darkening as the render was. With the
   room's own exposure lifted, most of it can come off — but not the last two
   stops: the bottom still has to reach --void so the section below has no
   seam to find. The radial behind the headline is reduced rather than
   removed, because a brighter room is a busier one and the display type still
   has to sit on something quiet. */
/* WHY THE LAST STOPS ARE AT 74/82 AND NOT 88/100.

   This gradient is measured against `.hero__scene`, NOT against the hero —
   the scene is `inset:-8% 0` so the parallax has somewhere to travel, which
   makes it ~16% taller than the section and overhang it top and bottom. The
   hero's real bottom edge therefore lands around 84% of this gradient, and
   `overflow:hidden` throws the rest away.

   With void at 100% that meant the hero ended on roughly 77% alpha —
   measured, at one scroll position, with the room still 23% visible — while
   the section below opens on pure void. That difference IS the colour
   separation: a lit band, then a hard line, then black.

   So void now arrives at 82% of the scene, which is above the hero's bottom
   edge with enough margin left for the parallax to slide the scene ±8% and
   still land on black. Both sides of the join are now the same colour, which
   is the only thing that actually removes a seam.

   Nothing above 60% moves, so the room and the headline are untouched; what
   goes dark is the floor, which is the least interesting part of the frame. */
.hero__scrim{
  background:
    radial-gradient(58% 52% at 50% 46%,rgba(4,7,11,.70),rgba(4,7,11,.24) 62%,transparent 82%),
    linear-gradient(180deg,
      rgba(7,11,16,.50) 0%,
      rgba(7,11,16,.06) 26%,
      rgba(7,11,16,.30) 60%,
      rgba(7,11,16,.88) 74%,
      var(--void) 82%);
}

@media (prefers-reduced-motion:reduce){
  .hero__in{opacity:1;transform:none;}
  .hero__scene img{transform:none;}
  .hero__scroll,.hero__badge{opacity:1;}
}

.scrub__stage{
  position:sticky;
  top:0;
  height:100vh;
  height:100dvh;                /* iOS measures 100vh with the toolbars retracted */
  overflow:hidden;
  display:grid;
  place-items:center;
}

.scrub__canvas{
  position:absolute;
  inset:0;
  width:100%;
  height:100%;
  /* The canvas is sized in device pixels by JS. Without this the browser
     smooths the backing store on top of our own cover-fit maths and the
     line art in the last frames goes soft. */
  image-rendering:auto;
}

/* -------------------------------------------------------------------------
   Vignette. Two jobs: pull the eye to the centre of frame where the object
   is, and fade the top and bottom edges into the page colour so the pinned
   stage has no seam against the sections either side of it.
   ------------------------------------------------------------------------- */
.scrub__veil{
  position:absolute;inset:0;pointer-events:none;
  background:
    radial-gradient(120% 88% at 50% 46%,transparent 34%,rgba(7,11,16,.66) 100%),
    linear-gradient(to bottom,var(--void) 0%,transparent 16%,transparent 78%,rgba(7,11,16,.94) 100%);
}

/* A single sweep of vermilion that tracks the beat. It is the same colour
   the rest of the page uses for "this commits", and here it marks the
   moment the thing stops being a picture and becomes a spec. */
.scrub__veil::after{
  content:"";position:absolute;inset:0;
  background:radial-gradient(70% 50% at 50% 58%,rgba(214,59,47,.11),transparent 68%);
  opacity:calc(var(--beat,0) * .5);
  transition:opacity .8s var(--ease);
}

/* -------------------------------------------------------------------------
   Captions. One per state. They cross-fade rather than slide, because the
   picture behind them is already moving and two motions competing for the
   same 200ms reads as jitter.
   ------------------------------------------------------------------------- */
.scrub__caps{
  position:absolute;
  left:0;right:0;bottom:clamp(38px,7vh,86px);
  display:grid;
  place-items:center;
  pointer-events:none;
}
.scrub__cap{
  grid-area:1/1;                /* stacked, so no reflow as they swap */
  text-align:center;
  max-width:min(620px,86vw);
  opacity:0;
  transform:translateY(12px);
  transition:opacity .55s var(--ease),transform .55s var(--ease);
}
.scrub__cap.is-on{opacity:1;transform:none;}

.scrub__cap b{
  display:block;
  font-family:var(--display);
  font-weight:700;
  font-size:clamp(24px,3.2vw,42px);
  letter-spacing:-.03em;
  line-height:1.04;
  font-variation-settings:"wdth" 96;
}
.scrub__cap span{
  display:block;
  margin-top:12px;
  font-size:clamp(14px,1.35vw,17px);
  line-height:1.55;
  color:var(--ink-soft);
}
.scrub__cap .k{
  display:inline-block;
  margin-bottom:16px;
  font-family:var(--mono);
  font-size:9.5px;
  letter-spacing:.18em;
  text-transform:uppercase;
  color:var(--ink-faint);
  padding:5px 11px;
  border:1px solid var(--line);
  border-radius:var(--r-pill);
  background:rgba(7,11,16,.55);
  backdrop-filter:blur(8px);
}
.scrub__cap.is-on .k{color:var(--red);border-color:rgba(214,59,47,.34);}

/* The type sits over the picture, and the picture is a lit object on black
   — so the one place it can lose contrast is where the garment's own
   highlight passes behind a line of copy. A soft scrim under the caption
   block only, never a full-width bar, which would read as a caption plate
   rather than as lighting. */
.scrub__caps::before{
  content:"";position:absolute;left:50%;top:-30px;bottom:-44px;
  width:min(760px,94vw);transform:translateX(-50%);
  background:radial-gradient(60% 70% at 50% 55%,rgba(7,11,16,.6),transparent 72%);
  pointer-events:none;
}

/* -------------------------------------------------------------------------
   Rail. Three ticks, one per state, filled by scroll progress. Same
   language as the steps rail on the rest of the page.
   ------------------------------------------------------------------------- */
.scrub__rail{
  position:absolute;
  top:50%;right:clamp(16px,3vw,44px);
  transform:translateY(-50%);
  display:grid;
  gap:14px;
  pointer-events:none;
}
.scrub__rail i{
  display:block;width:2px;height:34px;border-radius:2px;
  background:var(--line-strong);
  transition:background .5s var(--ease),height .5s var(--ease);
}
.scrub__rail i:nth-child(1){background:var(--red);}
.scrub[style*="--beat:1"] .scrub__rail i:nth-child(2),
.scrub[style*="--beat:2"] .scrub__rail i:nth-child(2){background:var(--red);}
.scrub[style*="--beat:2"] .scrub__rail i:nth-child(3){background:var(--red);}

/* -------------------------------------------------------------------------
   The typed request
   -------------------------------------------------------------------------
   Sits inside the input box in the first frame. scrub.js gives it left, top
   and font-size in pixels, mapped through the same cover-fit transform the
   frames are drawn with, so it stays welded to the box at every size.

   `translateY(-50%)` because the measured anchor is the box's optical
   CENTRE — the one landmark that does not move when the font does.
   ------------------------------------------------------------------------- */
.scrub__type{
  position:absolute;
  margin:0;
  transform:translateY(-50%);
  white-space:pre;                 /* trailing spaces mid-word must hold the cursor out */
  line-height:1;
  font-family:var(--body);
  font-weight:500;
  letter-spacing:-.005em;
  color:#F7FAFD;
  opacity:0;
  transition:opacity .25s var(--ease);
  pointer-events:none;
  z-index:2;
  text-shadow:0 2px 18px rgba(0,0,0,.55);
}
.scrub__type.is-on{opacity:1;}

/* The caret. Matches the vermilion bar rendered into the empty box, and
   takes over from it the moment the first character covers it. It does not
   blink: this one is driven by the scrollbar, and a caret that blinks on its
   own clock next to text that only moves when you scroll reads as two
   different things happening. */
.scrub__type i{
  display:inline-block;
  width:.075em;height:1.05em;
  margin-left:.06em;
  vertical-align:-.16em;
  background:var(--red);
  box-shadow:0 0 12px rgba(214,59,47,.8);
  border-radius:1px;
}
/* Empty box: the frame's own caret is showing, so ours would be a duplicate. */
.scrub__type.is-empty i{opacity:0;}

@media (prefers-reduced-motion:reduce){
  .scrub__type{display:none;}
}

/* -------------------------------------------------------------------------
   Loading. The first frame paints immediately and the rest arrive behind
   it, so there is no spinner — but until the sequence is complete the
   scrub is coarse, and saying so is more honest than letting it look
   broken. It removes itself the moment the last frame lands.
   ------------------------------------------------------------------------- */
.scrub__load{
  position:absolute;
  top:clamp(16px,3vh,32px);left:50%;transform:translateX(-50%);
  font-family:var(--mono);font-size:9.5px;letter-spacing:.16em;text-transform:uppercase;
  color:var(--ink-faint);
  opacity:.85;transition:opacity .6s var(--ease);
}
.scrub.is-ready .scrub__load{opacity:0;}

/* -------------------------------------------------------------------------
   Reduced motion. No pin, no sequence, one frame, all three captions
   readable as a normal stacked list. The section still makes its point.
   ------------------------------------------------------------------------- */
.scrub.is-static{height:auto;}
.scrub.is-static .scrub__stage{position:relative;height:min(76vh,700px);}
.scrub.is-static .scrub__caps{
  position:relative;bottom:auto;
  padding:clamp(32px,6vw,64px) var(--gutter);
  gap:34px;
}
.scrub.is-static .scrub__cap{grid-area:auto;opacity:1;transform:none;}
.scrub.is-static .scrub__rail,
.scrub.is-static .scrub__load{display:none;}

@media (prefers-reduced-motion:reduce){
  .scrub{height:auto;}
  .scrub__stage{position:relative;height:min(76vh,700px);}
}

/* -------------------------------------------------------------------------
   Narrow. The rail is the first thing to go — it is orientation, not
   content, and on a phone the captions already carry that. The section
   also shortens: 400vh of thumb-scrolling is a lot longer on a phone than
   the same number is on a trackpad.
   ------------------------------------------------------------------------- */
@media (max-width:860px){
  .scrub{--span:3.2;}
  .scrub__rail{display:none;}
  .scrub__caps{bottom:clamp(30px,6vh,60px);}
  .scrub__veil{
    background:
      radial-gradient(140% 80% at 50% 42%,transparent 28%,rgba(7,11,16,.72) 100%),
      linear-gradient(to bottom,var(--void) 0%,transparent 14%,transparent 62%,rgba(7,11,16,.97) 100%);
  }
}
