/* the stripe-wipe's open share of each 9px pitch, 0..1: a number, not a length, because Safari rescales a registered length by the devices' zoom but not the pitch, leaving frames striped */
@property --o { syntax: "<number>"; inherits: false; initial-value: 0; }

/* fonts, self-hosted (SIL OFL 1.1): latin subsets from Google Fonts. Anybody is variable on weight and width; Hanken Grotesk on weight */
@font-face { font-family: "Anybody"; font-style: normal; font-weight: 100 900; font-stretch: 50% 150%; font-display: swap; src: url(assets/fonts/anybody.woff2) format("woff2"); }
@font-face { font-family: "Hanken Grotesk"; font-style: normal; font-weight: 400 600; font-display: swap; src: url(assets/fonts/hanken-grotesk.woff2) format("woff2"); }

.device-homepod{height:395px;width:320px}
.device-homepod .device-frame{background:linear-gradient(to right,#36373a 0,#8d8f95 25%,#1d1e20 60%,#111112 90%,#1d1e20 100%);border-radius:80px;box-shadow:inset 0 0 0 2px #36373a,inset 0 0 40px 1px #1d1e20,inset 0 12px 40px 1px #fff,inset 0 -24px 40px 1px #000;height:385px;margin:5px auto;overflow:hidden;position:relative;width:320px}
.device-homepod .device-stripe{background:repeating-linear-gradient(45deg,rgba(141,143,149,.35) 0,rgba(141,143,149,.35) 2px,transparent 2px,transparent 6px),repeating-linear-gradient(135deg,rgba(141,143,149,.35) 0,rgba(141,143,149,.35) 2px,transparent 2px,transparent 6px),repeating-linear-gradient(45deg,transparent 0,transparent 2px,rgba(29,30,32,.35) 2px,rgba(29,30,32,.35) 4px,transparent 4px,transparent 6px),repeating-linear-gradient(135deg,transparent 0,transparent 2px,rgba(29,30,32,.35) 2px,rgba(29,30,32,.35) 4px,transparent 4px,transparent 6px);border-radius:80px;box-shadow:inset 0 0 0 2px #36373a,inset 0 0 40px 1px #1d1e20,inset 0 12px 24px 1px #9b9ca1,inset 0 -12px 24px 1px #050505;height:385px;left:0;overflow:hidden;position:absolute;top:5px;width:320px;z-index:9}
.device-homepod .device-stripe::before{top:0;transform:translateY(-34%) perspective(500px) rotateX(51deg)}
.device-homepod .device-stripe::after{bottom:0;transform:translateY(34%) perspective(500px) rotateX(-51deg)}
.device-homepod .device-home::after,.device-homepod .device-home::before{background:#36373a;content:"";left:50%;position:absolute}
.device-homepod .device-home::before{border-radius:50% 50% 8px 8px;height:10px;margin-left:-85px;top:0;width:170px}
.device-homepod .device-home::after{border-radius:0 0 10% 10%/0 0 8px 8px;bottom:0;box-shadow:inset 0 0 4px 0 #111112;height:5px;margin-left:-85px;width:170px}

:root {
  --ink: #1F1E36;
  --ink-2: #4A4868;
  --ground: #F1EFF6;
  --paper: #FBFAFD;
  /* each project's own accent, exact from its repo: ChemAlliance app primary, Lifert logo green, Corner black, KickPick graphite, Palladion bronze button */
  --utk: #2563EB;
  --lifert: #138A42;
  --corner: #000000;
  --kickpick: #2B2B2F;
  --palladion: #D0A375;
  --p: 9px; /* stripe pitch: the only unit */
  --display: "Anybody", "Arial Narrow", sans-serif;
  --text: "Hanken Grotesk", system-ui, sans-serif;
  --ease: cubic-bezier(.16, 1, .3, 1);
  color-scheme: light;
  scrollbar-color: var(--ink) var(--ground);
}

* { box-sizing: border-box; }
html { scroll-behavior: smooth; overscroll-behavior-y: none; } /* no rubber-band pull above the hero */
body { overscroll-behavior-y: none; }
body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font: 400 17px/calc(var(--p) * 3) var(--text);
  -webkit-font-smoothing: antialiased;
}
::selection { background: var(--ink); color: var(--ground); }
:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }
a { color: inherit; text-underline-offset: 5px; text-decoration-thickness: 1px; }
a:hover { text-decoration-thickness: 2px; }
h1, h2, p, ul, figure { margin: 0; }

#field { position: fixed; inset: 0; width: 100%; height: 100%; z-index: -1; }

/* fixed chrome: one left rail (CV slab, contacts, project index) that exists only while projects run */
.rail {
  position: fixed; top: calc(var(--p) * 3); left: calc(var(--p) * 2); z-index: 10;
  width: calc(var(--p) * 14);
  display: flex; flex-direction: column;
}
/* contacts and the project map share one ruled slot under the CV slab */
.links {
  display: grid; padding: var(--p) calc(var(--p) * 2);
  background: var(--ground); border: 2px solid var(--ink); border-bottom: 0;
  font-weight: 500; font-size: 15px;
}
.cv {
  display: inline-flex; align-items: center; gap: var(--p);
  padding: calc(var(--p) * 2) calc(var(--p) * 3);
  background: var(--ink); color: var(--ground); text-decoration: none;
  font: 700 24px/1 var(--display); font-stretch: 120%;
  transition: background .3s var(--ease);
}
.cv:hover { background: #34325A; }
.cv svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 2.4; transition: transform .4s var(--ease); }
.cv:hover svg { transform: translate(2px, -2px); }
.rail .cv { justify-content: space-between; margin-bottom: var(--p); padding: calc(var(--p) * 2); font-size: 22px; }

.index {
  display: flex; flex-direction: column;
  padding: var(--p) calc(var(--p) * 2);
  background: var(--ground); border: 2px solid var(--ink);
}
.index a {
  font: 600 14px/calc(var(--p) * 4) var(--display); font-stretch: 72%;
  color: var(--ink-2); text-decoration: none;
  display: flex; align-items: center; justify-content: space-between; gap: var(--p);
  transition: color .3s var(--ease);
}
/* the marker keeps its slot and scales, so the box never changes width */
.index a::after { content: ""; width: calc(var(--p) * 2); height: 6px; background: var(--tone, var(--ink)); border-bottom: 2px solid var(--ink); transform: scaleX(0); transform-origin: left; transition: transform .5s var(--ease); }
.index a[aria-current="true"], .index a:is(:hover, :focus-visible) { color: var(--ink); }
.index a[aria-current="true"]::after { transform: scaleX(1); }
/* hover previews the jump: the name darkens and its marker wipes in halfway, so the current one still reads as current */
.index a:is(:hover, :focus-visible):not([aria-current="true"])::after { transform: scaleX(.5); }

/* each rail piece wipes in on the stripe pitch a beat after the last while projects run, and out at the hero and the close */
.rail > * {
  --o: 1;
  -webkit-mask: repeating-linear-gradient(#000 0 calc(var(--o) * (var(--p) + 1px)), transparent calc(var(--o) * (var(--p) + 1px)) var(--p));
  mask: repeating-linear-gradient(#000 0 calc(var(--o) * (var(--p) + 1px)), transparent calc(var(--o) * (var(--p) + 1px)) var(--p));
  transition: --o .9s var(--ease) var(--beat, 0s), visibility 0s;
}
.rail > .links { --beat: .08s; }
.rail > .index { --beat: .16s; }
body:not([data-zone="projects"]) .rail > * { --o: 0; visibility: hidden; transition: --o .9s var(--ease), visibility 0s .9s; }

/* sections: calm column + field stage, swapping sides */
.sec {
  position: relative; min-height: 100svh;
  display: grid; grid-template-columns: 42fr 58fr; align-items: center;
  gap: calc(var(--p) * 6);
  padding: calc(var(--p) * 14) calc(var(--p) * 8) calc(var(--p) * 10) calc(var(--p) * 18);
}
.sec[data-side="left"] { grid-template-columns: 58fr 42fr; }
.hero { padding-left: calc(var(--p) * 8); --name: clamp(56px, 9vw - 34px, 96px); } /* no index in the hero, so the name takes the gutter; full size at 1440, giving way faster below so the field clears it */
@media (min-width: 901px) { .hero .meta { max-width: calc(var(--name) * 4.9); } } /* no wider than "Maksym": the hero's words end on one line the field keeps clear of */
.calm { display: grid; gap: calc(var(--p) * 3); align-content: center; justify-items: start; }
/* the side frame hugs the field edge, staggered below the lens */
.calm:has(.side) { justify-items: end; }
.sec[data-side="left"] .calm { justify-items: start; }
/* projects: screen + side frame as one centred composition, clear of the rail */
@media (min-width: 901px) {
  .sec:is([data-side="left"], [data-side="right"]):not(.hero) {
    grid-template-columns: minmax(0, 680px) minmax(0, 300px); justify-content: center;
    gap: calc(var(--p) * 6); padding-left: calc(var(--p) * 20);
  }
  .sec[data-side="right"]:not(.hero) { grid-template-columns: minmax(0, 300px) minmax(0, 680px); }
  #kickpick { grid-template-columns: minmax(0, 460px) minmax(0, 560px); }
  #corner { grid-template-columns: minmax(0, 560px) minmax(0, 460px); } /* phone-only projects: narrower stage, two side phones */
}

.name {
  font: 800 var(--name)/.88 var(--display); font-stretch: 108%;
  letter-spacing: -.02em;
}
.name span { display: block; font-weight: 300; font-stretch: 62%; letter-spacing: -.01em; }
.line { font: 600 clamp(22px, 2vw, 30px)/1.15 var(--display); font-stretch: 110%; }
.role { font: 600 clamp(20px, 1.9vw, 28px)/1.1 var(--display); font-stretch: 110%; }
.role + .meta { margin-top: calc(var(--p) * -2); } /* role and base read as one group */
.meta { color: var(--ink-2); font-size: 16px; text-wrap: pretty; }

/* hero: actions under the name, about + stack in a slot on the field */
.actions { display: grid; gap: calc(var(--p) * 2); justify-items: start; justify-self: stretch; margin-top: var(--p); container-type: inline-size; }
.btn {
  display: inline-flex; align-items: center; gap: var(--p);
  padding: calc(var(--p) * 1.5) calc(var(--p) * 1.25); /* the icon took the room: four still fit one row at 1440 */
  background: var(--ground); border: 2px solid var(--ink); text-decoration: none;
  font: 700 16px/1 var(--display); font-stretch: 105%;
  transition: background .3s var(--ease), color .3s var(--ease);
}
.btn:hover { background: var(--ink); color: var(--ground); }
.btn svg, .links svg { width: 1.1em; height: 1.1em; flex: none; fill: currentColor; }
.links a { display: inline-flex; align-items: center; gap: var(--p); justify-self: start; }
.reach { display: flex; flex-wrap: wrap; gap: var(--p); }
/* when four contacts can't share one row, they square up 2×2 instead of stranding one */
@container (max-width: 520px) { .reach { display: grid; grid-template-columns: repeat(2, max-content); } }
/* the about card is no box: it is a void the field parts around, its outline drawn by where the stripes end (main.js) */
/* the hero pair: card and projects share one lens; three whole stripes run between their voids */
.pair { width: min(100%, 600px); display: grid; gap: calc(var(--p) * 11); }
.about { display: grid; gap: calc(var(--p) * 2); }
.hero .stage { justify-items: end; } /* the pair sits right so the field shows a band of stripes before the voids */
/* one big "Always" spans the height of its three continuations, set off by a 2px ink rule */
.about .line { --c: clamp(22px, 2.1vw, 30px); display: flex; align-items: center; gap: calc(var(--p) * 2); }
.always {
  align-self: stretch; display: flex; align-items: center;
  padding-right: calc(var(--p) * 2); border-right: 2px solid var(--ink);
  font-size: calc(var(--c) * 3.45); font-weight: 300; font-stretch: 50%; line-height: .8; /* light, like "Shupeikin": it frames the lines, not outshouts them */
}
.cont { font-size: var(--c); line-height: 1.15; font-stretch: 100%; white-space: nowrap; }
.bio { color: var(--ink-2); }
.bio span { display: block; text-wrap: balance; } /* one sentence per line, each wrapping evenly on phones */
.stack {
  list-style: none; padding: 0; margin: var(--p) 0 0;
  display: grid; grid-template-columns: repeat(6, 1fr);
  border: 2px solid var(--ink); background: var(--ground);
}
.stack li {
  display: grid; justify-items: center; align-content: center; gap: var(--p);
  height: calc(var(--p) * 9); padding: 0 var(--p);
  font: 600 14px/1 var(--display); font-stretch: 85%;
  border-right: 2px solid var(--ink);
  transition: background .3s var(--ease), color .3s var(--ease), opacity .3s var(--ease);
}
.stack li:last-child { border-right: 0; }
.stack li.hit { background: var(--ink); color: var(--ground); }
.stack li.hit svg { fill: var(--ground); }
.stack svg { width: 30px; height: 30px; fill: var(--ink); transition: fill .3s var(--ease); }

/* projects at a glance: a second void under the card, each app's own icon over its name, its tone as a base band */
.works {
  display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); /* long names never widen the grid */
  border: 2px solid var(--ink); background: var(--ground);
  container-type: inline-size;
}
.works a {
  position: relative; display: grid; justify-items: center; gap: var(--p);
  padding: calc(var(--p) * 2) calc(var(--p) / 2) calc(var(--p) * 2.5);
  font: 700 15px/1 var(--display); font-size: min(15px, 4cqi - 3px); font-stretch: 72%; text-decoration: none; /* the rail's condensed name voice, sized so ChemAlliance fits a fifth */
  border-right: 2px solid var(--ink);
  transition: background .3s var(--ease), opacity .3s var(--ease);
}
.works a:last-child { border-right: 0; }
.works a::after { content: ""; position: absolute; inset: auto 0 0; height: 6px; background: var(--tone); }
.works img { width: 56px; height: 56px; object-fit: contain; border: 2px solid var(--ink); background: var(--paper); }
.works a.hit, .works a:hover { background: var(--tone); color: #fff; } /* white on the dark brand tones: 5.2:1 blue, 4.4:1 green, 14:1+ graphite and black */
.works a[data-p="palladion"].hit, .works a[data-p="palladion"]:hover { color: var(--ink); } /* bronze is light: ink, 7:1 */
.pair.hl :is(.stack li, .works a):not(.hit) { opacity: .3; }

.stage { display: grid; justify-items: center; }
/* real device bodies (devices.css) hold the screenshots; main.js zooms each to its slot, a soft shadow gives volume */
/* box-shadow on the bodies, not filter: Chrome clips a filter's region on zoomed, translated elements; insets repeat devices.css */
.device { --lift: 0 22px 26px rgb(31 30 54 / .24), 0 4px 6px rgb(31 30 54 / .16); }
.device-iphone-14-pro .device-frame { box-shadow: inset 0 0 4px 2px #c0b7cd, inset 0 0 0 6px #342c3f, var(--lift); }
.device-macbook-pro .device-frame { box-shadow: inset 0 0 0 2px #c8cacb, var(--lift); }
.device-macbook-pro .device-power { box-shadow: inset 0 -2px 8px 0 #6c7074, var(--lift); }
.device-screen { position: relative; overflow: hidden; background: var(--paper); }
.device-iphone-14-pro .device-screen { isolation: isolate; } /* keeps the rounded screen corners clipping the frames */
/* safe areas: the notch sits in a black menu bar like a full-screen app; web shots get a status bar under the island */
.device-macbook-pro .device-screen { background: #0d0d0d; }
.device-macbook-pro .frame { top: 14px; height: calc(100% - 14px); }
.device.web .device-screen { background: #111; }
.device.web .frame { top: 50px; height: calc(100% - 50px); }
.side { translate: 0 var(--drop, 0); margin-right: calc(var(--p) * -4); }
.sec[data-side="left"] .side { margin: 0 0 0 calc(var(--p) * -4); }
/* two side phones step like a staircase beside the lens */
.sides { display: flex; gap: calc(var(--p) * 3); align-items: flex-start; translate: 0 var(--drop, 0); }
.sec .sides .side { margin: 0; translate: 0 calc(var(--p) * -6); } /* the pair hugs the field edge as one, not each phone */
.sec[data-side="left"] .sides { margin-left: calc(var(--p) * -4); }
.sec[data-side="right"] .sides { margin-right: calc(var(--p) * -4); }
.sec .sides .side + .side { translate: 0 calc(var(--p) * 6); }

/* frames change by stripe-wipe on the 9px pitch */
.frame {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: top; display: block; /* crop the bottom, never a header */
  --o: 0;
  -webkit-mask: repeating-linear-gradient(#000 0 calc(var(--o) * (var(--p) + 1px)), transparent calc(var(--o) * (var(--p) + 1px)) var(--p));
  mask: repeating-linear-gradient(#000 0 calc(var(--o) * (var(--p) + 1px)), transparent calc(var(--o) * (var(--p) + 1px)) var(--p));
  transition: --o 1.1s var(--ease);
}
.frame.on { --o: 1; }
/* at rest a frame carries no mask: some iOS WebKit builds draw the wipe's stripes misaligned under the devices' zoom, so they must never outlive the 1.1s wipe */
.frame.shown { -webkit-mask: none; mask: none; }

/* placeholder until a real screenshot lands at the path */
.ph {
  display: grid; align-content: center; justify-items: center; gap: var(--p);
  padding: calc(var(--p) * 3); text-align: center;
  background:
    repeating-linear-gradient(transparent 0 calc(var(--p) - 1px), rgb(31 30 54 / .14) calc(var(--p) - 1px) var(--p)),
    var(--tone);
}
.ph b { font: 600 20px/1.2 var(--display); font-stretch: 90%; max-width: 22em; }
.side .ph b { font-size: 15px; }

.slot {
  width: min(100%, 680px);
  display: grid; gap: var(--p);
  padding: calc(var(--p) * 1.5) calc(var(--p) * 3) calc(var(--p) * 2);
  background: var(--ground); border: 2px solid var(--ink);
}
/* project captions are no boxes: like the hero card, each is a void the field parts around (main.js) */
.caption { width: min(100%, 600px); margin-top: calc(var(--p) * 6); display: grid; gap: calc(var(--p) * 1.5); }
.caption h2 { display: flex; align-items: center; gap: calc(var(--p) * 2); font: 800 30px/1.1 var(--display); font-stretch: 135%; letter-spacing: -.01em; }
/* the app's own icon beside its name, framed as in the hero's projects block */
.caption h2 img { width: 48px; height: 48px; flex: none; object-fit: contain; border: 2px solid var(--ink); background: var(--paper); }
.caption .desc { color: var(--ink-2); max-width: 56ch; text-wrap: pretty; }
.foot { display: flex; flex-wrap: wrap; align-items: center; gap: calc(var(--p) * 2) calc(var(--p) * 3); }
/* facts are a ruled strip like the hero's stack row: the ink shows through 2px gaps between ground cells, the project's tone runs along the base */
.facts {
  flex: 1 1 100%; list-style: none; padding: 0 0 6px; display: flex; flex-wrap: wrap; gap: 2px;
  border: 2px solid var(--ink);
  background: linear-gradient(var(--tone) 0 0) bottom / 100% 6px no-repeat, var(--ink);
  font: 600 15px/1.2 var(--display); font-stretch: 85%;
}
.facts li { flex: 1 1 auto; padding: calc(var(--p) * 1.25) calc(var(--p) * 1.5); background: var(--ground); }
.out { font-weight: 600; font-size: 15px; }

.ticks { display: flex; gap: var(--p); }
.ticks button {
  width: calc(var(--p) * 4); height: calc(var(--p) * 3); padding: 0; border: 0; cursor: pointer;
  background: linear-gradient(var(--ink), var(--ink)) center / 100% 2px no-repeat;
  transition: background-size .4s var(--ease);
}
.ticks button[aria-pressed="true"] { background-size: 100% 6px; }
.ticks button:hover { background-size: 100% 4px; }

/* the close: field resolves flat around the CV */
.end { grid-template-columns: 1fr; justify-items: center; }
.close { width: min(100%, 820px); justify-items: center; text-align: center; gap: calc(var(--p) * 3); padding: calc(var(--p) * 6) calc(var(--p) * 4); }
.close .meta { max-width: none; }

.jump { display: none; }
.cv.big { font-size: clamp(36px, 5vw, 64px); padding: calc(var(--p) * 3) calc(var(--p) * 5); }
.cv.big svg { width: .7em; height: .7em; }
.close .reach { justify-content: center; }
.close .btn { font-size: clamp(17px, 1.6vw, 22px); padding: calc(var(--p) * 2) calc(var(--p) * 2); }

@media (max-width: 900px) {
  .rail { top: calc(var(--p) * 2); left: auto; right: calc(var(--p) * 2); width: auto; flex-direction: row; gap: 0; }
  .links { display: none; }
  .jump {
    display: inline-flex; align-items: center; order: -1; padding: 0 calc(var(--p) * 2);
    background: var(--ground); border: 2px solid var(--ink); border-right: 0;
    font-weight: 600; font-size: 15px; text-decoration: none;
  }
  .cv, .rail .cv { font-size: 20px; padding: calc(var(--p) * 1.5) calc(var(--p) * 2); }
  .rail .cv { margin-bottom: 0; } /* the desktop gap under the slab would stretch the Contact tab 9px taller */
  .index {
    position: fixed; bottom: 0; left: 0; right: 0;
    flex-direction: row; justify-content: space-between;
    padding: 0 calc(var(--p) * 2); border-width: 2px 0 0;
  }
  .index a { flex-direction: column; gap: 0; line-height: calc(var(--p) * 4); }
  .index a::after { width: 100%; height: 5px; margin-top: -7px; }
  .name { font-size: min(15vw, 64px); }
  .sec, .sec[data-side="left"] {
    grid-template-columns: minmax(0, 1fr); gap: calc(var(--p) * 3); min-height: 0;
    padding: calc(var(--p) * 10) calc(var(--p) * 2) calc(var(--p) * 6);
  }
  .stage { order: -1; }
  .hero .stage { order: 0; }
  .hero .calm { justify-items: start; background: var(--ground); padding: calc(var(--p) * 2); margin: 0 calc(var(--p) * -2); }
  .sec:not(.hero) .calm { display: none; } /* one preview per project on phones: the main device, then its caption; side phones stay on desktop */
  .slot { padding: calc(var(--p) * 2); }
  .slot h2, .caption h2 { font-size: 26px; }
  .actions { justify-items: stretch; width: 100%; }
  .actions .cv { justify-content: center; }
  .reach { display: grid; grid-template-columns: 1fr 1fr; width: 100%; }
  .reach .btn { justify-content: center; }
  .about .line { --c: min(4.4vw, 18px); gap: calc(var(--p) * 1.5); }
  .always { padding-right: calc(var(--p) * 1.5); }
  .cont { font-stretch: 90%; }
  .pair { gap: calc(var(--p) * 3); } /* one merged calm band on phones: no stripes between the blocks */
  .stack { grid-template-columns: repeat(3, 1fr); }
  .stack li:nth-child(-n+3) { border-bottom: 2px solid var(--ink); }
  .stack li:nth-child(3n) { border-right: 0; }
  .works a { font-size: min(13px, 3.4vw); font-stretch: 72%; padding-inline: 0; }
  .ticks { gap: calc(var(--p) * .75); }
  .ticks button { width: calc(var(--p) * 3); } /* seven frames still fit a 360px caption */
  .works img { width: 44px; height: 44px; }
  .end { padding-bottom: calc(var(--p) * 12); }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .frame, .index a::after, .cv, .cv svg, .ticks button, .btn, .stack li, .stack svg, .works a, body .rail > *, body:not([data-zone="projects"]) .rail > * { transition: none; }
}
