/* ============================================================
   shutter kif. — black & white, gritty, minimal
   Composed sections sit on a 16:9 stage carrying the exact PSD
   coordinates in percent.
   ============================================================ */

@font-face{font-family:'Helv';src:url('../assets/fonts/Helvetica.woff') format('woff'),url('../assets/fonts/Helvetica.ttf') format('truetype');font-weight:400;font-style:normal;font-display:swap}
@font-face{font-family:'Helv';src:url('../assets/fonts/Helvetica-Oblique.woff') format('woff'),url('../assets/fonts/Helvetica-Oblique.ttf') format('truetype');font-weight:400;font-style:italic;font-display:swap}
@font-face{font-family:'Helv';src:url('../assets/fonts/Helvetica-Bold.woff') format('woff'),url('../assets/fonts/Helvetica-Bold.ttf') format('truetype');font-weight:700;font-style:normal;font-display:swap}
@font-face{font-family:'VCR';src:url('../assets/fonts/VCR.woff') format('woff'),url('../assets/fonts/VCR.ttf') format('truetype');font-weight:400;font-display:swap}

:root{
  --light:#ffffff;
  --dark:#000000;
  --on-light:#000000;
  --on-dark:#ffffff;

  /* paper = the tone actually painted behind the cut-outs.
     --el-inv / --el-blend are derived from it, never from a section name. */
  --paper:var(--dark);
  --el-inv:1;
  --el-blend:screen;

  --f:'Helv','Helvetica Neue',Helvetica,Arial,sans-serif;
  --f-vcr:'VCR','Helv',monospace;
  --ease:cubic-bezier(.16,1,.3,1);
  --gutter:clamp(1rem,3.5vw,3rem);
  --fade-t:7svh;        /* seam bleed, per side — 0 where there is no seam */
  --fade-b:7svh;
}

/* the angle is its own animatable property so the parallax translate on the
   same element stays live instead of being frozen into the keyframes */
@property --spin{syntax:'<angle>';inherits:false;initial-value:0deg}

/* invert — every surface and every cut-out flips together */
body.is-invert{
  --light:#000000;
  --dark:#ffffff;
  --on-light:#ffffff;
  --on-dark:#000000;
}

*,*::before,*::after{box-sizing:border-box}
html{scroll-behavior:smooth;-webkit-text-size-adjust:100%}
body{
  margin:0;background:var(--dark);color:var(--on-dark);
  font-family:var(--f);line-height:1.5;
  overflow-x:hidden;-webkit-font-smoothing:antialiased;
}
body.is-booting{overflow:hidden}
img{max-width:100%;display:block}
a{color:inherit;text-decoration:none}
::selection{background:currentColor;color:var(--light)}
:focus-visible{outline:2px solid currentColor;outline-offset:4px}

/* A single fixed backdrop behind everything has its tone interpolated on
   scroll, so where two sections differ the tones bleed instead of meeting at a
   hard seam. Each stage paints its own opaque paper over it and fades that
   paper out at the viewport edges, which is where the bleed shows through. */
.backdrop{position:fixed;inset:0;z-index:-1;background:var(--dark)}
.sec{position:relative;overflow:hidden}

/* ---- paper, and everything derived from it -------------------------------
   The cut-outs are dark artwork printed on an opaque white rectangle (star-a,
   star-b, dice, cards) or dark artwork on transparency (cd-a, cd-b, ic-mail).
   Either way the rule is the same: the artwork must end up LIGHTER than the
   paper and screen onto it, or DARKER than the paper and multiply into it.
   Anything else leaves the source rectangle sitting on top of the type — that
   was the black-box bug on the first and last pages.                        */
.sec--light{--paper:var(--light);color:var(--on-light);--el-blend:multiply;--el-inv:0}
.sec--dark {--paper:var(--dark); color:var(--on-dark); --el-blend:screen;  --el-inv:1}
body.is-invert .sec--light{--el-blend:screen;  --el-inv:1}
body.is-invert .sec--dark {--el-blend:multiply;--el-inv:0}

/* Tracking-overlay ink. The overlay composites with `difference` (see .trk),
   so one neutral grey reads the same faint grey over black paper, over white
   paper and over the cut-outs themselves — no per-paper theming, and it can
   never disappear against the white artwork the way a plain white hairline did.
   Same trick the controls use. */
:root{--trk:rgba(146,146,146,.42);--trk-hi:rgba(190,190,190,.52)}

.el,.boot__img,.lay--icons img{filter:invert(var(--el-inv));user-select:none;-webkit-user-drag:none}
/* max-width:none is load-bearing. The base `img{max-width:100%}` clamps any
   cut-out authored wider than its stage — and when only ONE axis is clamped
   while the other is an explicit length, the aspect ratio breaks and the
   artwork renders stretched. These are art-directed cut-outs that are meant to
   bleed past the frame; let them. */
.el{position:absolute;pointer-events:none;z-index:1;max-width:none;
    mix-blend-mode:var(--el-blend);transform:translateY(var(--ty,0))}

/* ============ LOADER ============ */
.boot{
  position:fixed;inset:0;z-index:200;background:var(--light);color:var(--on-light);
  --el-inv:0;
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1.6rem;
  transition:opacity .5s var(--ease),visibility .5s;
}
body.is-invert .boot{--el-inv:1}
.boot.is-done{opacity:0;visibility:hidden}
.boot__img{width:min(230px,44vw);image-rendering:pixelated}
.boot__bar{width:min(230px,44vw);height:2px;background:currentColor;opacity:.18;position:relative;overflow:hidden}
.boot__bar span{position:absolute;inset:0 100% 0 0;background:var(--on-light);transition:right .16s linear}
.boot__pct{font-family:var(--f-vcr);font-size:.95rem;letter-spacing:.14em;color:var(--on-light);opacity:.55}

/* ============ FIXED CONTROLS ============ */
.ctl{
  position:fixed;right:var(--gutter);bottom:calc(var(--gutter) - .3rem);z-index:95;
  display:flex;flex-direction:column;align-items:flex-end;gap:.25rem;
  mix-blend-mode:difference;
  opacity:0;transition:opacity .8s var(--ease) .2s;
}
.is-lit .ctl{opacity:1}
.ctl__b{
  background:none;border:0;padding:.18rem 0;cursor:pointer;
  font-family:var(--f);font-size:clamp(.72rem,1vw,.86rem);color:#fff;
  line-height:1.35;position:relative;
}
.ctl__b::after{content:'';position:absolute;left:0;right:100%;bottom:.1rem;height:1px;background:currentColor;transition:right .35s var(--ease)}
.ctl__b:hover::after,.ctl__b[aria-pressed="true"]::after{right:0}
.ctl__link{display:block}

/* ============ COMP STAGE ============
   COVER, not contain: the comps are full-bleed, so on a window wider than 16:9
   the stage grows past the viewport and the section crops it. Contain left bare
   backdrop down both sides — that was the "zoomed out" bug.
   The stage also carries the paper the cut-outs blend against, because
   container-type creates a stacking context and isolates the blend from
   anything painted outside it. The mask is anchored to the VIEWPORT, not to the
   stage box, so the seam fade stays on screen no matter how much of the stage
   is cropped away — --vtop/--vbot are where the viewport edges land inside the
   stage, and they change with how the stage is aligned in its section. */
/* Absolutely placed, not grid-aligned: once the stage overflows its section,
   Chrome falls back to `start` for align-self and the crop anchor silently
   moves to the top — which is what cropped the contact marks off the bottom. */
.stage{
  position:absolute;inset:0;margin:auto;   /* centred WITHOUT transform — the
     data-rise reveal owns `transform` on this element and would cancel it */
  width:max(100vw, calc(100svh * 16 / 9));
  height:max(100svh, calc(100vw * 9 / 16));
  container-type:size;
  background:var(--paper);
  --vtop:calc(50% - 50svh);
  --vbot:calc(50% + 50svh);
  -webkit-mask-image:linear-gradient(to bottom,
      transparent var(--vtop),
      #000 calc(var(--vtop) + var(--fade-t)),
      #000 calc(var(--vbot) - var(--fade-b)),
      transparent var(--vbot));
          mask-image:linear-gradient(to bottom,
      transparent var(--vtop),
      #000 calc(var(--vtop) + var(--fade-t)),
      #000 calc(var(--vbot) - var(--fade-b)),
      transparent var(--vbot));
}
/* First and last pages have no seam above / below them, so they get the full
   frame instead of fading into nothing. */
.hero .stage{--fade-t:0}
.contact .stage{--fade-b:0}
.lay{position:absolute;margin:0;z-index:5;line-height:1;letter-spacing:-.02em;font-weight:400;transform:translateY(var(--ty,0))}

/* tracking overlay — above the cut-outs, under the type, never interactive */
.trk{
  position:absolute;inset:0;width:100%;height:100%;z-index:3;
  pointer-events:none;mix-blend-mode:difference;
  opacity:0;transition:opacity 1.6s var(--ease) .25s;
}
.trk.is-on{opacity:1}

/* ---- 01 front ---- */
.hero{height:100svh;display:grid;place-items:center}
.el--star-a{left:66.30%;top:-12.41%;width:31.46%}
.el--star-b{left:-10.78%;top:12.13%;width:60.78%}
/* the stars turn, very slowly and against each other — one revolution is over
   two minutes, so it reads as drift rather than as an animation */
.el--star-a,.el--star-b{transform:translateY(var(--ty,0)) rotate(var(--spin))}
.el--star-a{animation:spin-cw 128s linear infinite}
.el--star-b{animation:spin-ccw 173s linear infinite}
@keyframes spin-cw {to{--spin:360deg}}
@keyframes spin-ccw{to{--spin:-360deg}}
.el--dice  {left:32.45%;top:19.26%;width:35.10%}
.lay--shutter{left:auto;right:calc(67.55% - 6%);top:43.0%;font-size:4cqw;font-style:italic}
.lay--kif    {left:59.84%;top:43.0%;font-size:4cqw;font-style:italic}

.cue{position:absolute;bottom:1.5rem;left:50%;transform:translateX(-50%);z-index:6;padding:.6rem}
.cue i{display:block;width:1px;height:34px;background:currentColor;opacity:.3;position:relative;overflow:hidden}
.cue i::after{content:'';position:absolute;inset:0;background:currentColor;animation:sd 2s cubic-bezier(.65,0,.35,1) infinite}
@keyframes sd{0%{transform:translateY(-100%)}60%,100%{transform:translateY(100%)}}

/* ---- 02 work ---- */
.machine{height:100svh;display:flex;flex-direction:column;position:relative}
/* The flower sits in its own stage BEHIND the WebGL canvas, which is now
   genuinely transparent. Being an ordinary cut-out is the whole point: it keys
   off the paper like every other one — dark on white paper, light on black,
   never a white box on black — and it takes the rotation and the tracker for
   free. It replaced a leopard photograph that could do none of that. */
.machine .stage{z-index:1}
/* Sized and centred in container units, never with a centring transform or the
   independent `translate` property — the tracker reads offsetLeft/offsetTop
   plus the computed `transform`, and either of those would desync the contour
   from the artwork. Source is 1248x1152, so height = width / 1.0833. */
/* Sized against the VIEWPORT, not the stage: the stage overflows on a cover
   crop, so a stage-relative size would bleed the petals off screen. Source is
   1248x1152, so width = height * 1.0833 and half of that is 0.5417.
   Never centred with a transform or the independent `translate` property — the
   tracker reads offsetLeft/offsetTop plus the computed `transform`, and either
   would desync the contour from the artwork. */
.el--flower{
  height:104svh;width:auto;
  left:calc(50cqw - 56.3svh);
  top:calc(50cqh - 52svh);
  transform:translateY(var(--ty,0)) rotate(var(--spin));
  animation:spin-cw 214s linear infinite;
}
.machine__stage{position:relative;flex:1;min-height:0;z-index:2}
#pspCanvas{position:absolute;inset:0;width:100%;height:100%;display:block;touch-action:pan-y}
/* deliberately NOT inverted: the bloom and the screen are emitted light,
   and inverting turns the highlights into black smears */
.machine__loading{position:absolute;inset:auto 0 50% 0;text-align:center;z-index:3;transition:opacity .5s}
.machine__loading.is-off{opacity:0;pointer-events:none}
.spin{display:inline-block;width:11px;height:11px;border:1px solid currentColor;border-top-color:transparent;border-radius:50%;opacity:.5;animation:spin .9s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}

.rail{
  position:absolute;left:var(--gutter);bottom:clamp(1.6rem,5vh,3.2rem);z-index:6;
  display:flex;align-items:baseline;gap:.45rem;font-family:var(--f-vcr);color:var(--on-dark);
}
.rail b{font-size:clamp(1.8rem,4vw,3rem);font-weight:400;line-height:1;font-variant-numeric:tabular-nums}
.rail i{font-style:normal;font-size:.9rem;opacity:.45}
.rail__open{
  margin-left:.7rem;align-self:center;width:36px;height:36px;display:grid;place-items:center;
  border:1px solid currentColor;font-size:1rem;line-height:1;
  transition:background .25s,color .25s,transform .4s var(--ease);
}
.rail__open:hover{background:var(--on-dark);color:var(--dark);transform:translate(2px,-2px)}
.rail.is-swap b{animation:flick .3s steps(2) both}
@keyframes flick{0%{opacity:0}50%,100%{opacity:1}}

.keys{position:absolute;right:var(--gutter);bottom:clamp(1.6rem,5vh,3.2rem);z-index:6;display:flex;gap:.3rem;color:var(--on-dark)}
.keys button{font-family:var(--f-vcr);font-size:.8rem;border:1px solid currentColor;opacity:.45;padding:.1rem .35rem;min-width:30px;height:30px;text-align:center;background:none;color:inherit;cursor:pointer;transition:opacity .25s,background .25s,color .25s,transform .3s var(--ease)}
.keys button:hover,.keys button:focus-visible{opacity:1;background:var(--on-dark);color:var(--dark);transform:translateY(-2px)}

/* ---- 03 about ---- */
.about{height:100svh;display:grid;place-items:center}
.el--cd-a  {left:-5.99%;top:-23.61%;width:41.35%}
.el--cd-b  {left:82.08%;top:45.83%;width:29.90%}
.lay--abouth{left:0;width:100%;top:26%;transform:translateY(var(--ty,0));font-family:var(--f-vcr);font-size:1.65cqw;letter-spacing:.06em;text-align:center;white-space:nowrap}
.lay--bio{
  left:50%;top:38%;width:44%;margin-left:-22%;
  font-size:1.78cqw;line-height:1.3;letter-spacing:0;
  text-align:justify;text-justify:inter-word;hyphens:auto;
}
.lay--bio p{margin:0}
.lay--bio p+p{margin-top:.15em}
.bio__lead{margin-bottom:.3em!important}
.bio__skills{display:flex;flex-wrap:wrap;gap:.28em .38em;margin-top:.55em;font-size:.72em;line-height:1.15}
.bio__skills span{border:1px solid currentColor;border-radius:999px;padding:.24em .62em;opacity:.72;white-space:nowrap}

/* ---- 04 contact ---- */
/* Bottom-aligned, not centred: the comp puts the mail and Instagram marks at
   89.7% of a 1080-tall canvas, so centring the cover crop ate them. Cropping
   off the top instead costs nothing — the cards already start above the frame. */
.contact{height:100svh;display:grid;place-items:center}
.contact .stage{
  top:auto;bottom:0;margin-block:0;
  --vtop:calc(100% - 100svh);--vbot:100%;
}
.el--cards{left:26.67%;top:-3.52%;width:46.67%}
.lay--contact{left:auto;right:calc(73.33% - 7%);top:40.0%;font-size:3.78cqw}
.lay--me     {left:64.27%;top:40.0%;font-size:3.78cqw}
.lay--icons{left:45.94%;top:89.7%;display:flex;align-items:center;gap:1.1%}
.lay--icons img{width:100%;transition:transform .4s var(--ease)}
.lay--icons a:first-child{width:3.70cqw}
.lay--icons a:not(:first-child){width:3.02cqw}
.lay--icons .social__logo{width:100%;height:auto}
.lay--icons a:hover img{transform:translateY(-4px)}
.icons__ig,body.is-invert .icons__ig{filter:none}   /* the IG mark keeps its colour */
.music-link,.blogs-link{
   position:absolute;left:64.27%;top:51%;
   font-family:var(--f-vcr);font-size:.72rem;letter-spacing:.08em;text-transform:uppercase;
   opacity:.68;transition:opacity .25s var(--ease),transform .3s var(--ease)
}
.blogs-link{top:55%}
.music-link span,.blogs-link span{display:inline-block;margin-left:.4rem;transition:transform .3s var(--ease)}
.music-link:hover,.blogs-link:hover{opacity:1;transform:translateY(-2px)}
.music-link:hover span,.blogs-link:hover span{transform:translate(3px,-3px)}

/* ============ MOTION ============
   Nothing here is a page transition — each piece drifts at its own rate as you
   scroll, so a section reads as a moving composition rather than a flat still. */
.el,.lay{will-change:transform}
.rev{opacity:0;filter:blur(6px)}
.rev.is-shown{opacity:1;filter:none;visibility:visible;
  transition:opacity 1.1s var(--ease),filter 1.1s var(--ease)}
/* cut-outs keep their invert while revealing — a bare `filter:none` here would
   drop the polarity and bring the black boxes straight back */
.el.rev{filter:invert(var(--el-inv)) blur(9px)}
.el.rev.is-shown{filter:invert(var(--el-inv));
  transition:opacity 1.1s var(--ease),filter 1.1s var(--ease)}
.rev.is-shown.d1{transition-delay:.08s}
.rev.is-shown.d2{transition-delay:.16s}

/* ============ REVEAL ============ */
[data-rise]{opacity:0;transform:translateY(18px);filter:blur(6px)}
.is-lit [data-rise]{opacity:1;transform:none;filter:none;transition:opacity .9s var(--ease) .1s,transform 1s var(--ease) .1s,filter .9s var(--ease) .1s}

/* ============ NARROW SCREENS ============
   The 16:9 stage would shrink the type past legibility, so let it fill the
   width and grow taller instead. The section grows with it, and the
   viewport-anchored mask no longer makes sense, so it comes off. */
@media (max-width:820px){
  .keys{display:none}

  /* ---- ONE coordinate system, same as desktop, only the aspect changes ----
     The desktop composition is rigid because there is a stage of definite
     size, every element is placed in percent OF THAT STAGE, and every type
     size is in `cqw` — so the whole thing scales as a single piece.

     The old mobile block threw all of that away. It made the stage
     variable-height and `container-type:normal`, then placed elements in
     percent against that moving box while sizing type in `vw` against the
     *viewport*. Two coordinate systems that disagree the moment a phone's
     aspect ratio differs from whatever it was eyeballed at — which is exactly
     what "everything is all over the place" looks like.

     So: keep the machinery, drop the second system. The stage is the viewport,
     with a definite size so container units resolve, and every position and
     type size below is authored against it.

     It also stays ABSOLUTE. Making it a flow item turned it into a flex item
     inside .machine, where it claimed the full 100svh, collapsed
     .machine__stage to zero height, and the PSP never rendered at all —
     resize() bails on a zero-size canvas. */
  .hero,.about,.contact{height:100svh;min-height:0}
  .stage{
    position:absolute;inset:0;margin:0;
    width:100%;height:100%;
    container-type:size;
    -webkit-mask-image:none;mask-image:none;
  }
  /* No cut-out may inherit a height from the desktop rules. This is what
     stretched the flower: the desktop rule sets `height:104svh; width:auto`
     and the mobile rule only overrode the width, so it rendered at the phone's
     width by the desktop's height — squashed into a completely wrong aspect. */
  .el{height:auto}

  /* Every value below is percent of the stage, or cqw — one system.
     `cqw` and not `vw`: if the stage ever stops being exactly the viewport,
     the type still scales with the composition instead of drifting out of it. */

  /* 01 front — one word, "shutterkif.", sitting under the dice.
     The two halves are separate elements and always will be (the copy is
     frozen), so they are butted against the stage's centre line instead:
     `shutter` ends exactly at 50%, `kif.` starts exactly at 50%. They meet
     perfectly as one centred word without anything needing to know how wide
     the text renders — no font metrics, no magic numbers, no drift when the
     webfont swaps in. */
  .el--star-a{left:44%;top:2%;width:64%;height:auto}
  .el--star-b{left:-24%;top:66%;width:98%;height:auto}
  /* the dice is sized by HEIGHT so the band it occupies is the same fraction
     of the screen on every phone — width-sized art moves vertically as the
     aspect changes, which is what put the type on top of it */
  .el--dice{width:auto;height:32cqh;left:calc(50cqw - 16.24cqh);top:20cqh}
  .lay--shutter,.lay--kif{top:57cqh;font-size:11cqw;white-space:nowrap}
   .lay--shutter{left:auto;right:calc(50% + 6px);text-align:right}
  .lay--kif    {left:50%;right:auto;text-align:left}

  /* 03 about */
   .lay--abouth{left:0;width:100%;top:14%;transform:translateY(var(--ty,0));font-size:4.5cqw}
   .lay--bio{left:7%;top:24%;width:86%;margin-left:0;font-size:4.4cqw;text-align:left}
   .bio__skills{font-size:.74em}
  .el--cd-a{left:-18%;top:-2%;width:62%;height:auto}
  .el--cd-b{left:58%;top:74%;width:54%;height:auto}

  /* 04 contact — `contact` above the card, `me.` below it, both centred.
     Card sized by HEIGHT for the same reason as the dice: it then occupies a
     fixed 26%–70% band whatever the phone's aspect, so the two words can sit
     just outside it without ever landing on top of it. Source is 896x1184, so
     width = height * 0.757 and half of that is 0.3785. */
  .el--cards{width:auto;height:44cqh;left:calc(50cqw - 16.65cqh);top:26cqh}
  .lay--contact,.lay--me{
    left:0;right:0;width:100%;text-align:center;
    font-size:12cqw;white-space:nowrap;
  }
  .lay--contact{top:15cqh}
  .lay--me     {top:73cqh}
  /* centred by the flex box itself, not by a left offset that only happens to
     look centred at one particular width */
  .lay--icons{left:0;width:100%;justify-content:center;top:82%;gap:5%}
  .lay--icons a:first-child{width:12cqw}
  .lay--icons a:last-child {width:9.8cqw}

  /* 02 work — behind the machine, aspect preserved, centred in container units
     so it stays centred at any phone aspect. Source is 1248x1152, so
     width = height * 1.0833 and half of that is 0.5417. */
  .el--flower{width:auto;height:74cqh;left:calc(50cqw - 40.1cqh);top:13cqh}
}

/* ============ REDUCED MOTION ============ */
@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  .cue i::after,.rail.is-swap b{animation:none}
  .el--star-a,.el--star-b,.el--flower{animation:none;transform:translateY(var(--ty,0))}
  .rev{opacity:1;filter:none;visibility:visible;transition:none}
  .el.rev,.el.rev.is-shown{opacity:1;filter:invert(var(--el-inv));transition:none}
  .el,.lay{--ty:0 !important}
  [data-rise]{opacity:1;transform:none;filter:none;transition:none}
  .lay--icons img,.rail__open,.ctl__b::after{transition:none}
}
