/* ============================================================
   09 — CINEMA
   The motion layer. Where 08 is about surfaces, this is about
   TIME: staged entrances, kinetic type, camera moves, and the
   handful of tricks that make a page feel directed rather than
   merely animated.

   The organising idea is a BEAT. One beat is 80ms. Everything on
   the opening sequence lands on a beat, so the whole thing reads
   as one choreographed move instead of twelve separate ones.

   Same rules as every other layer here:
     · transform, opacity, filter and clip-path only
     · nothing is load-bearing — delete the file, the page works
     · the entire layer stops under prefers-reduced-motion
   ============================================================ */

:root{
  --beat:80ms;
  /* easing curves, named for what they feel like rather than their numbers */
  --ease-out-expo:cubic-bezier(.16,1,.3,1);
  --ease-out-back:cubic-bezier(.34,1.56,.64,1);
  --ease-in-out-quint:cubic-bezier(.83,0,.17,1);
  --ease-spring:linear(0,.28 7%,.78 16%,.98 22%,1.06 26%,1.04 33%,.995 45%,1);
}

/* ---------- 01. SEQUENCE : everything lands on a beat ---------- */
.seq [data-beat]{
  opacity:0;
  animation:beat-in .85s var(--ease-out-expo) both;
  animation-delay:calc(var(--beat) * var(--b, 0));
}
.seq [data-beat="1"]{--b:1}   .seq [data-beat="2"]{--b:2}
.seq [data-beat="3"]{--b:3}   .seq [data-beat="4"]{--b:4}
.seq [data-beat="5"]{--b:5}   .seq [data-beat="6"]{--b:6}
.seq [data-beat="7"]{--b:7}   .seq [data-beat="8"]{--b:8}
.seq [data-beat="9"]{--b:9}   .seq [data-beat="10"]{--b:10}
.seq [data-beat="12"]{--b:12} .seq [data-beat="14"]{--b:14}
@keyframes beat-in{
  from{opacity:0;transform:translateY(22px) scale(.985);filter:blur(6px)}
  to  {opacity:1;transform:none;filter:blur(0)}
}

/* entrance variants — pick the one that suits the element ------- */
.in-mask{
  animation-name:mask-in;
  clip-path:inset(0 0 100% 0);
}
@keyframes mask-in{
  from{opacity:0;clip-path:inset(0 0 100% 0);transform:translateY(14px)}
  to  {opacity:1;clip-path:inset(0 0 -12% 0);transform:none}
}
.in-slice{animation-name:slice-in}
@keyframes slice-in{
  0%  {opacity:0;clip-path:polygon(0 0,100% 0,100% 0,0 0)}
  55% {opacity:1}
  100%{opacity:1;clip-path:polygon(0 0,100% 0,100% 100%,0 100%)}
}
.in-scale{animation-name:scale-in;animation-timing-function:var(--ease-out-back)}
@keyframes scale-in{
  from{opacity:0;transform:scale(.86)}
  to  {opacity:1;transform:none}
}
.in-blur{animation-name:blur-in}
@keyframes blur-in{
  from{opacity:0;filter:blur(16px);transform:scale(1.04)}
  to  {opacity:1;filter:blur(0);transform:none}
}

/* ---------- 02. KINETIC TYPE : word by word, on a hinge -------- */
.kinetic{perspective:800px}
/*  When a kinetic headline is also .holo, the gradient has to live on the WORD.
    Left on the parent it is lost the moment a child gets its own filter, and
    the headline renders invisible. */
.kinetic.holo{background:none;-webkit-text-fill-color:currentColor}
.kinetic.holo .kw{
  background:var(--spectrum);
  -webkit-background-clip:text;background-clip:text;
  -webkit-text-fill-color:transparent;color:transparent;
}
.kinetic .kw{
  display:inline-block;transform-origin:50% 100%;
  opacity:0;animation:kw-in .78s var(--ease-out-expo) both;
}
@keyframes kw-in{
  from{opacity:0;transform:translateY(.55em) rotateX(-72deg);filter:blur(5px)}
  to  {opacity:1;transform:none;filter:blur(0)}
}
/* the line the whole page hangs on gets its own hinge per line */
.kinetic .kline{display:block;overflow:hidden;padding-bottom:.06em}

/* ---------- 03. CAMERA : push-in and rack focus on scroll ------ */
.camera{transform-origin:50% 40%;will-change:transform,opacity}
@supports (animation-timeline: view()){
  .camera{
    animation:camera-push linear both;
    animation-timeline:view();
    animation-range:exit 0% exit 90%;
  }
  /*  Blurring the entire hero subtree on every scroll frame is the most
      expensive effect on the page and the least noticed. Scale and fade give
      the same "camera leaves" read for a fraction of the cost. */
  @keyframes camera-push{
    from{transform:scale(1);opacity:1}
    to  {transform:scale(1.1);opacity:.2}
  }
}

/* ---------- 04. FLARE : anamorphic streak across the hero ------ */
.flare{
  position:absolute;top:22%;left:-40%;width:180%;height:2px;pointer-events:none;z-index:1;
  background:linear-gradient(90deg,transparent,rgba(34,224,255,.0) 18%,
    rgba(34,224,255,.85) 48%,rgba(255,255,255,.95) 50%,rgba(155,107,255,.85) 52%,
    rgba(155,107,255,0) 82%,transparent);
  filter:blur(1px);opacity:0;
  animation:flare-run 11s var(--ease-in-out-quint) infinite;
}
.flare::after{
  content:"";position:absolute;left:48%;top:50%;width:190px;height:190px;
  transform:translate(-50%,-50%);border-radius:50%;
  background:radial-gradient(circle,rgba(34,224,255,.30),transparent 62%);
  filter:blur(22px);
}
@keyframes flare-run{
  0%,58%  {transform:translateX(-16%) rotate(-12deg);opacity:0}
  63%     {opacity:.9}
  86%,100%{transform:translateX(28%) rotate(-12deg);opacity:0}
}

/* ---------- 05. TRAIL : motion-blur ghosts behind a moving thing */
.trail{position:relative}
.trail::before,.trail::after{
  content:attr(data-trail);position:absolute;inset:0;pointer-events:none;
  color:inherit;opacity:.35;filter:blur(4px);
}
.trail::before{transform:translateX(-6px);color:var(--cyan)}
.trail::after {transform:translateX(6px);color:var(--violet)}

/* ---------- 06. BURST : particles thrown out on entrance ------- */
.burst{position:relative}
.burst i{
  position:absolute;left:50%;top:50%;width:4px;height:4px;border-radius:50%;
  background:var(--cyan);opacity:0;pointer-events:none;
  animation:burst-out 1.15s var(--ease-out-expo) both;
}
.burst i:nth-child(1){--a:0deg}    .burst i:nth-child(2){--a:30deg;background:var(--blue)}
.burst i:nth-child(3){--a:65deg}   .burst i:nth-child(4){--a:100deg;background:var(--violet)}
.burst i:nth-child(5){--a:140deg}  .burst i:nth-child(6){--a:175deg;background:var(--blue)}
.burst i:nth-child(7){--a:205deg}  .burst i:nth-child(8){--a:240deg;background:var(--violet)}
.burst i:nth-child(9){--a:275deg}  .burst i:nth-child(10){--a:310deg;background:var(--blue)}
.burst i:nth-child(11){--a:335deg} .burst i:nth-child(12){--a:355deg}
@keyframes burst-out{
  0%  {opacity:0;transform:rotate(var(--a)) translateX(0) scale(.4)}
  22% {opacity:1}
  100%{opacity:0;transform:rotate(var(--a)) translateX(150px) scale(.2)}
}

/* ---------- 07. GLITCH : rare, brief, never decorative ---------- */
.glitch{position:relative;isolation:isolate}
.glitch::before,.glitch::after{
  content:attr(data-glitch);position:absolute;inset:0;pointer-events:none;
  background:inherit;-webkit-background-clip:text;background-clip:text;
}
.glitch::before{
  color:var(--rose);clip-path:inset(12% 0 62% 0);
  animation:glitch-a 7s steps(1) infinite;
}
.glitch::after{
  color:var(--cyan);clip-path:inset(58% 0 18% 0);
  animation:glitch-b 7s steps(1) infinite;
}
@keyframes glitch-a{
  0%,96%{transform:none;opacity:0}
  96.5% {transform:translate(-4px,1px);opacity:.9}
  97.2% {transform:translate(3px,-2px);opacity:.9}
  98%   {transform:none;opacity:0}
}
@keyframes glitch-b{
  0%,96%{transform:none;opacity:0}
  96.8% {transform:translate(4px,-1px);opacity:.9}
  97.6% {transform:translate(-3px,2px);opacity:.9}
  98.4% {transform:none;opacity:0}
}

/* ---------- 08. VIGNETTE + FILM FLICKER ------------------------ */
.vignette{
  position:fixed;inset:0;pointer-events:none;z-index:3;
  background:radial-gradient(130% 100% at 50% 42%,transparent 42%,rgba(2,4,10,.55) 100%);
}
html[data-theme="day"] .vignette{background:radial-gradient(130% 100% at 50% 42%,transparent 52%,rgba(12,20,44,.12) 100%)}
.flicker{animation:flick 6s steps(1) infinite}
@keyframes flick{
  0%,100%{opacity:.035} 47%{opacity:.05} 48%{opacity:.02} 49%{opacity:.045}
}

/* ---------- 09. KEN BURNS : the background never sits still ---- */
/*  Drift only. Scaling this layer means re-rasterising four blurred sources
    every frame for the whole time the page is open. */
.kenburns{animation:kb 40s ease-in-out infinite alternate;will-change:transform}
@keyframes kb{
  from{transform:translate3d(0,0,0)}
  to  {transform:translate3d(-3%,-4%,0)}
}

/* ---------- 10. SCENE : scroll-linked wipe between sections ---- */
.scene{position:relative;overflow:hidden}
.scene::before{
  content:"";position:absolute;inset:0;pointer-events:none;z-index:1;
  background:linear-gradient(180deg,var(--void),transparent 22%,transparent 78%,var(--void));
  opacity:.85;
}
html[data-theme="day"] .scene::before{background:linear-gradient(180deg,var(--void),transparent 22%,transparent 78%,var(--void))}

/* ---------- 11. VELOCITY SKEW : the page leans as you scroll --- */
.velskew{
  transform:skewY(calc(var(--vel,0) * .32deg));
  transition:transform .22s var(--ease-out-expo);
  will-change:transform;
}

/* ---------- 12. (removed) ------------------------------------
   There was a cursor ring here that followed the pointer. Removed at
   request. Nothing else depended on it, so the whole effect is gone
   rather than left switched off — a dead rule is a rule someone
   re-enables by accident later.
   ------------------------------------------------------------ */

/* ---------- 13. COUNT-IN : numbers arrive out of focus --------- */
.countin{animation:count-in .9s var(--ease-out-expo) both}
@keyframes count-in{
  from{opacity:0;filter:blur(12px);transform:translateY(12px) scale(1.06)}
  to  {opacity:1;filter:blur(0);transform:none}
}

/* ---------- 14. PULSE : a bloom that breathes ------------------ */
.pulse-glow{animation:pulseglow 3.4s ease-in-out infinite}
@keyframes pulseglow{
  0%,100%{box-shadow:0 0 0 0 rgba(46,107,255,.35),0 18px 60px -30px rgba(46,107,255,.7)}
  50%    {box-shadow:0 0 0 14px rgba(46,107,255,0),0 26px 80px -30px rgba(46,107,255,.95)}
}

/* ---------- 15. TICKER LEAN : marquee reacts to scroll --------- */
.marquee.lean .marquee-track{
  transform:skewX(calc(var(--vel,0) * -.5deg));
  transition:transform .3s var(--ease-out-expo);
}

/* ---------- 16. UNDERLINE DRAW -------------------------------- */
/*  Must stay `inline`. As an inline-block it cannot break across lines, and a
    long phrase then becomes one unbreakable box that overflows a phone. The
    underline is therefore a background, not a positioned pseudo-element. */
.draw{
  display:inline;
  background-image:var(--spectrum);
  background-repeat:no-repeat;
  background-position:0 100%;
  background-size:0% 2px;
  padding-bottom:.1em;
  animation:draw-line .7s var(--ease-out-expo) both;
  animation-delay:.55s;
}
@keyframes draw-line{to{background-size:100% 2px}}

/* ---------- 17. HOLD FRAME : first paint has no flash ---------- */
.seq{opacity:1}
html:not(.js) .seq [data-beat]{opacity:1;animation:none}

/* ---------- 18. ARRIVE : sections fill in as you reach them ----
   The settle used to fire on page load, so anything below the fold
   had already finished by the time it was scrolled to — you arrived
   at a section that was simply there, with no sense of it landing.

   It now waits for the element to be reached, plays once, and stays.
   The starting state is deliberately legible: 55% opacity and 14px
   of offset, not invisible. If the script never runs, or the browser
   has no IntersectionObserver, the content is fully readable anyway.
   -------------------------------------------------------------- */
.arrive{
  opacity:1;transform:none;
  transition:opacity .85s cubic-bezier(.2,.7,.3,1),transform .85s cubic-bezier(.2,.7,.3,1);
}
html.js .arrive:not(.seen){opacity:.55;transform:translateY(14px)}
html.js .arrive.seen{opacity:1;transform:none}
@media(prefers-reduced-motion:reduce){
  html.js .arrive:not(.seen){opacity:1;transform:none}
}
html[data-fx-off] .arrive{opacity:1 !important;transform:none !important}

/* ============================================================
   Cost control and accessibility
   ============================================================ */
@media(max-width:900px){
  .flare{display:none}
  .kenburns{animation-duration:48s}
  .camera{animation:none !important;filter:none !important;transform:none !important}
  /* the scene wash reads as a dark stripe once the hero is tall and narrow */
  .scene::before{opacity:.45}
}
@media(max-width:700px){
  /* on a phone the vignette darkens the sides of the content rather than the
     corners of a wide frame, which just looks murky */
  .vignette{background:radial-gradient(150% 110% at 50% 40%,transparent 68%,rgba(2,4,10,.30) 100%)}
  .scene::before{display:none}
  .burst i{display:none}
  .flicker{animation:none}
  /* one beat is too slow to sit through on a small screen */
  :root{--beat:56ms}
}
@media(prefers-reduced-motion:reduce){
  .seq [data-beat],.kinetic .kw,.burst i,.countin,.draw::after{
    opacity:1 !important;transform:none !important;filter:none !important;
    clip-path:none !important;animation:none !important;
  }
  .flare,.glitch::before,.glitch::after,.kenburns,.flicker,.pulse-glow,
  .camera{animation:none !important}
  .flare{display:none}
  .velskew,.marquee.lean .marquee-track{transform:none !important}
  .draw::after{transform:scaleX(1) !important}
}
@media (update: slow), (prefers-reduced-data: reduce){
  .flare,.burst,.kenburns{display:none}
}

/* the graphics kill switch owns this layer too */
html[data-fx-off] .flare,
html[data-fx-off] .vignette,
html[data-fx-off] .burst,
html[data-fx-off] .seq [data-beat],
html[data-fx-off] .kinetic .kw{opacity:1 !important;transform:none !important;filter:none !important;clip-path:none !important}
