/* ============================================================
   jeo m. — black & white, gritty, minimal.

   Every page is a composition on a 16:9 stage, with each piece
   placed in percent OF THAT STAGE and every type size in `cqw`,
   so the whole thing scales as one rigid picture at any width.
   ============================================================ */

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

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

  --f:'Helvetica Neue',Helvetica,Arial,sans-serif;
  --f-vcr:'VT323','Helvetica Neue',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;

  /* Tracking-overlay ink. The overlay composites with `difference`, so one
     neutral grey reads the same over black paper, over white paper and over
     the artwork itself — no per-paper theming. */
  --trk:rgba(146,146,146,.42);
  --trk-hi:rgba(190,190,190,.52);
}

/* 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}

/* One fixed backdrop behind everything, its tone interpolated on scroll so
   sections bleed into each other 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}

/* The cut-outs are black artwork on transparency. The rule is always 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 a rectangle sitting
   on top of the type. */
.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}

.el{filter:invert(var(--el-inv));user-select:none;-webkit-user-drag:none}
/* max-width:none is load-bearing: the base img rule would clamp any cut-out
   authored wider than its stage, and clamping one axis while the other is an
   explicit length breaks the aspect ratio. These are meant to bleed. */
.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);
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1.6rem;
  transition:opacity .5s var(--ease),visibility .5s;
}
.boot.is-done{opacity:0;visibility:hidden}
.boot__mark{font-size:clamp(2.6rem,9vw,5rem);font-style:italic;line-height:1;letter-spacing:-.03em}
.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:1.05rem;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__row{
  display:flex;flex-direction:row;align-items:center;gap:.5rem;
}
.ctl__transport{
  display:flex;flex-direction:row;align-items:center;gap:.3rem;
}
.ctl__i{
  background:none;border:0;padding:.2rem;cursor:pointer;
  color:#fff;display:flex;align-items:center;justify-content:center;
  width:clamp(1rem,1.2vw,1.5rem);height:clamp(1rem,1.2vw,1.5rem);
  transition:opacity .2s;
}
.ctl__i:hover:not(:disabled){opacity:0.7}
.ctl__i:disabled{opacity:0.4;cursor:not-allowed}
.ctl__i svg{width:100%;height:100%;display:block}
/* the title only exists once there is a track to name */
.ctl__track{
  margin:0 0 .1rem;font-family:var(--f-vcr);font-size:clamp(.78rem,1vw,.95rem);
  letter-spacing:.1em;text-transform:uppercase;color:#fff;
  max-width:min(48vw,26ch);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  opacity:0;transition:opacity .5s var(--ease);
}
.ctl__track.is-on{opacity:.55}
.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}

/* ============ 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. The stage also
   carries the paper the cut-outs blend against, because container-type creates
   a stacking context and isolates the blend from anything outside it. The mask
   is anchored to the VIEWPORT, not the stage box, so the seam fade stays on
   screen however much of the stage is cropped away. */
.stage{
  position:absolute;inset:0;margin:auto;   /* centred WITHOUT transform — the
     data-rise reveal owns `transform` here 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 */
.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 ----------------------------------------------------------
   The two halves of the name are butted against the centre line rather than
   positioned by eye: `jeo` ends at 34%, `metelitsa.` starts at 66%, so they
   frame the rosette exactly whatever the font does. */
.hero{height:100svh;display:grid;place-items:center}
.el--star-a{left:66%;top:-13%;width:31%}
.el--star-b{left:-11%;top:26%;width:54%}
/* the stars turn, very slowly and against each other — one revolution takes
   over two minutes, so it reads as drift rather than as an animation */
.el--star-a,.el--star-b,.el--star-c{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--bloom{left:33%;top:15%;width:34%;animation:spin-cw 96s linear infinite;
           transform:translateY(var(--ty,0)) rotate(var(--spin))}
.lay--jeo {right:66.5%;top:43%;font-size:4.2cqw;font-style:italic;text-align:right;white-space:nowrap}
.lay--last{left:66.5%;top:43%;font-size:4.2cqw;font-style:italic;white-space:nowrap}

.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 genuinely
   transparent. Being an ordinary cut-out is the point: it keys off the paper
   like every other one, and it takes the rotation and the tracker for free. */
.machine .stage{z-index:1}
/* Sized against the VIEWPORT, not the stage — the stage overflows on a cover
   crop and a stage-relative size would bleed the petals off screen. Never
   centred with a transform: the tracker reads offsetLeft/offsetTop plus the
   computed transform, and a centring transform would desync the contour. */
.el--flower{
  height:104svh;width:104svh;
  left:calc(50cqw - 52svh);
  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(2rem,4.4vw,3.4rem);font-weight:400;line-height:1;font-variant-numeric:tabular-nums}
.rail i{font-style:normal;font-size:1.05rem;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;font-family:var(--f);
  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}}
/* the title of whatever is on the glass, set beside the counter */
.rail__name{
  position:absolute;left:var(--gutter);bottom:calc(clamp(1.6rem,5vh,3.2rem) + clamp(2.4rem,5vw,4rem));
  margin:0;z-index:6;font-family:var(--f-vcr);font-size:clamp(.95rem,1.5vw,1.3rem);
  letter-spacing:.08em;text-transform:uppercase;opacity:.5;max-width:min(46vw,30ch);
  transition:opacity .3s var(--ease);
}

.keys{position:absolute;right:var(--gutter);bottom:clamp(1.6rem,5vh,3.2rem);z-index:6;display:flex;gap:.3rem;color:var(--on-dark)}
kbd{font-family:var(--f-vcr);font-size:.95rem;border:1px solid currentColor;opacity:.45;padding:.1rem .35rem;min-width:24px;text-align:center}

/* ---- 03 about ---- */
.about{height:100svh;display:grid;place-items:center}
.el--cd-a{left:-7%;top:-24%;width:41%;animation:spin-cw 190s linear infinite;
          transform:translateY(var(--ty,0)) rotate(var(--spin))}
.el--cd-b{left:81%;top:44%;width:30%;animation:spin-ccw 151s linear infinite;
          transform:translateY(var(--ty,0)) rotate(var(--spin))}
.lay--abouth{left:50%;top:22%;transform:translate(-50%,var(--ty,0));font-family:var(--f-vcr);font-size:3.6cqw;letter-spacing:.06em}
.lay--bio{
  left:50%;top:33%;width:46%;margin-left:-23%;
  font-size:1.72cqw;line-height:1.35;letter-spacing:0;
  text-align:justify;text-justify:inter-word;hyphens:auto;
}
.lay--bio p{margin:0}
.lay--bio p+p{margin-top:.6em}
/* the stack itself, set as one line of small caps furniture */
.lay--skills{
  display:flex;flex-wrap:wrap;gap:.35em .9em;margin-top:1.4em !important;
  font-family:var(--f-vcr);font-size:.86em;letter-spacing:.1em;text-transform:uppercase;opacity:.55;
}

/* ---- 04 prints ---- */
.prints{height:100svh;display:grid;place-items:center}
.el--star-c{left:-13%;top:38%;width:36%;animation:spin-cw 240s linear infinite}
.lay--printh{left:50%;top:14%;transform:translate(-50%,var(--ty,0));font-family:var(--f-vcr);font-size:3.6cqw;letter-spacing:.06em;white-space:nowrap}
.lay--printlead{left:50%;top:23%;transform:translate(-50%,var(--ty,0));font-size:1.5cqw;opacity:.65;white-space:nowrap}
/* A single row that never stops moving — and that you can also take hold of
   and push. The drift is real scrolling, driven from js/cms.js rather than by
   a transform, which is the whole reason a trackpad, a swipe or a drag can
   join in mid-motion instead of fighting an animation for the same pixels.
   The list is duplicated so the loop is seamless in both directions, and the
   strip is masked at both ends so photographs fade in and out of the frame
   instead of being chopped by the viewport. */
.strip{
  position:absolute;left:0;right:0;top:38%;z-index:6;
  overflow-x:auto;overflow-y:hidden;
  cursor:grab;touch-action:pan-x;overscroll-behavior-x:contain;
  scrollbar-width:none;-ms-overflow-style:none;
  -webkit-mask-image:linear-gradient(to right,transparent,#000 12%,#000 88%,transparent);
          mask-image:linear-gradient(to right,transparent,#000 12%,#000 88%,transparent);
}
/* the bar would cut straight across the bottom photographs, and the strip is
   self-evidently draggable without it */
.strip::-webkit-scrollbar{display:none}
.strip.is-drag{cursor:grabbing}
.strip__row{display:flex;gap:clamp(.8rem,1.6vw,1.6rem);width:max-content}
.strip figure{margin:0;position:relative}
.strip img{
  height:clamp(150px,30svh,300px);width:auto;object-fit:cover;
  filter:grayscale(1) contrast(1.25) brightness(.85);
  opacity:.72;transition:opacity .5s var(--ease),filter .5s var(--ease),transform .5s var(--ease);
}
/* the one place colour is allowed back into the page: hovering a frame
   develops it — the grayscale comes off and the photograph is itself */
.strip a:hover img,.strip a:focus-visible img{opacity:1;filter:none;transform:translateY(-6px) scale(1.02)}

/* ---- 05 contact ---- */
.contact{height:100svh;display:grid;place-items:center}
/* Bottom-aligned, not centred: the marks sit low in the comp and centring the
   cover crop would eat them. Cropping off the top costs nothing. */
.contact .stage{top:auto;bottom:0;margin-block:0;--vtop:calc(100% - 100svh);--vbot:100%}
.el--petal{left:27%;top:-6%;width:46%;animation:spin-ccw 168s linear infinite;
           transform:translateY(var(--ty,0)) rotate(var(--spin))}
.lay--contact{right:66.5%;top:40%;font-size:3.9cqw;text-align:right;white-space:nowrap}
.lay--me     {left:66.5%;top:40%;font-size:3.9cqw;white-space:nowrap}
.lay--icons{left:0;width:100%;top:86%;display:flex;align-items:center;justify-content:center;gap:2.6cqw}
.lay--icons a{width:2.7cqw;display:block;transition:transform .4s var(--ease)}
.lay--icons svg{width:100%;height:auto;display:block}
.lay--icons a:hover{transform:translateY(-5px)}
.lay--colophon{left:0;width:100%;top:95%;text-align:center;font-family:var(--f-vcr);
               font-size:1.15cqw;letter-spacing:.12em;text-transform:uppercase;opacity:.4}

/* ============ 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)}

/* ============ 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 ============
   ONE coordinate system, same as desktop — only the aspect changes. The stage
   becomes the viewport, keeps a definite size so container units still
   resolve, and every position below is authored against it. It stays ABSOLUTE:
   as a flow item it would claim the full height inside .machine, collapse the
   canvas to nothing and the PSP would never render. */
@media (max-width:820px){
  .keys{display:none}
  .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 */
  .el{height:auto}

  /* 01 front */
  .el--star-a{left:46%;top:1%;width:62%}
  .el--star-b{left:-22%;top:64%;width:92%}
  /* 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 */
  .el--bloom{width:auto;height:34cqh;left:calc(50cqw - 17cqh);top:19cqh}
  .lay--jeo,.lay--last{top:58cqh;font-size:9.5cqw}
  .lay--jeo {right:50%;padding-right:.12em}
  .lay--last{left:50%}

  /* 02 work */
  .el--flower{width:auto;height:74cqh;left:calc(50cqw - 37cqh);top:13cqh}
  .rail__name{max-width:70vw}

  /* 03 about */
  .lay--abouth{top:12%;font-size:8cqw}
  .lay--bio{left:7%;top:22%;width:86%;margin-left:0;font-size:4.1cqw;text-align:left}
  .el--cd-a{left:-20%;top:-2%;width:64%}
  .el--cd-b{left:60%;top:76%;width:56%}

  /* 04 prints */
  .lay--printh{top:11%;font-size:8cqw}
  .lay--printlead{top:19%;font-size:3.6cqw;white-space:normal;width:80%;text-align:center;left:10%;transform:translateY(var(--ty,0))}
  .strip{top:34%}
  .el--star-c{left:-30%;top:60%;width:80%}

  /* 05 contact */
  .el--petal{width:auto;height:44cqh;left:calc(50cqw - 22cqh);top:24cqh}
  .lay--contact,.lay--me{font-size:12cqw;left:0;right:auto;width:100%;text-align:center}
  .lay--contact{top:13cqh}
  .lay--me     {top:72cqh}
  .lay--icons{top:84%;gap:8cqw}
  .lay--icons a{width:8.5cqw}
  .lay--colophon{top:93%;font-size:3cqw}
}

/* ============ REDUCED MOTION ============ */
@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  /* the strip stops drifting on its own — js/cms.js checks the same query —
     but it stays scrollable by hand */
  .cue i::after,.rail.is-swap b{animation:none}
  .el--star-a,.el--star-b,.el--star-c,.el--bloom,.el--flower,.el--cd-a,.el--cd-b,.el--petal{
    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}
  .strip img,.rail__open,.ctl__b::after,.lay--icons a{transition:none}
}

/* visually hidden: readable by search engines and screen readers, invisible on screen */
.u-sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}
