:root {
  --bg: #131114;
  --fg: #ffffff;
  --muted: #8a8a8b;
  --surface: #181818;
  --border: #303030;
  --radius: 14px;

  --grid-gap: 22px;
  --page-pad-x: clamp(18px, 3.6vw, 45px);
  --page-pad-y: clamp(25px, 4.95vh, 58px);

  --media-gap: 22px; /* space between media bottom and title */

  --font-body: "Figtree", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

*, *::before, *::after {
  box-sizing: border-box;
}

html, body {
  height: 100%;
}

/* The page is designed to fit one viewport exactly; anything that would
   overflow it (e.g. the hidden case-study buttons resting just past each
   card's visible edge) should clip rather than introduce a scrollbar.
   Below the mobile breakpoint the layout intentionally stacks and scrolls,
   so this only applies to the single-viewport desktop layout. */
@media (min-width: 901px) {
  html, body {
    overflow: hidden;
  }
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-body);
  /* Slightly smaller than it reads at first glance — at 15px, the longer
     card descriptions (BankFabric, IMDb) wrap to 3 lines on plenty of
     ordinary desktop widths, and the 2-line min-height reserve wasn't
     built to absorb that. 13px keeps everything reliably at 2 lines
     without needing to shorten the copy or floor the card width higher. */
  font-size: 13px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;

  /* Custom macOS-style arrow, inlined as base64 rather than linked to a
     file: cursor images don't block paint or count against preload the
     way other assets do, so a linked file left a beat of the system
     arrow showing on every full-page navigation before it loaded. Inline
     data is already in hand the moment this stylesheet itself parses, so
     there's no separate fetch to wait on. The plain url() line is the
     fallback for browsers that don't understand image-set() in `cursor`
     (older Firefox); browsers that do skip straight to the sharper 2x
     asset on high-DPI screens. Hotspot sits at the arrow's tip. */
  cursor: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABsAAAAqCAYAAABC8iBdAAAABmJLR0QA/wD/AP+gvaeTAAACz0lEQVRYhb3Xu0tbcRTA8XNzU1NaiIMYIUugs3+AQxEK4tKC0LqIg4OQTkIm6RLopH3YEPXGNm1aXyH2pRAoXYRCVqe2pMVVTAfFQkGR2Fa+HfRHf3nem5ibAxlyf49Pzi/JPecK8EDaFZzFw3ZiAI/aiQE8bicGMOM6VigUdPCJq9jIyAjr6+s6GHMNGx4exjRNMpmMDj4FDFcwEcE0TdLptA4+axlYjilwdXVVB5MtAathNcDngMcVTIErKys6+OJCYD1MgUtLSzqYahq0w0QEj8dTDr5sCnSCKXBxcVEHXzUMOsVEBMMwWFhY0MEMYLqCKTCRSOjgGuB1BVOgZVk6+NoR2AymwPn5eR18Yws2iylwbm5OB9/WBS+CKXB2dlYH3wGXXMEUGI/H7cFWYOo1PT2tg+8rwFZiIsLU1JQOfgB8ynL2/9Cip6dHIpGIGEbtirO/vy+BQEBE5KaIbAC3DcM4sc0sGAxWXMvlcjQYH4HLNTMzDENisZj4/X4ZHx8vGbMsS/r7+9XbbyJStDmQgIjcq1mpU6kUAMfHx3R1dZVk5vV62d3dVUvjNtD/KMdM02R5ebnkDCYnJyuOMhqNquFfwNWGsY6OjvJ27jvAzs4OpmmWYN3d3RSLRTUv3BA2OjpKNpvVoTRwDfgDMDQ0VJGd1oXlG8IODg50KMl5YQQ2ADY3Nyuwvr4+fc11x5gWCbS2DbihBnp7eyvAra0tNbzWKFb1OQ34AmBZVgU2Njam1v4Ggk6xmk+gQBjg8PCQzs7OEszn87G3t6f2iDrB6k4CrgA/ASYmJurdD39Qq7ycbxSp+2n+z5sB2N7exjCMEiwUCnF6eqrAO072s8NCwF+AwcFBRAS/3084HCafz+vf+6cLY+dgFiCXy5FMJjk6OqryY+YzTu8oNthAtd2BE876j4EW5FQCftWQAnAfCLQU0bC7nNWnWzTYfv8DAkt5iGd39JQAAAAASUVORK5CYII=") 1 1, auto;
  cursor:
    image-set(
      url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABsAAAAqCAYAAABC8iBdAAAABmJLR0QA/wD/AP+gvaeTAAACz0lEQVRYhb3Xu0tbcRTA8XNzU1NaiIMYIUugs3+AQxEK4tKC0LqIg4OQTkIm6RLopH3YEPXGNm1aXyH2pRAoXYRCVqe2pMVVTAfFQkGR2Fa+HfRHf3nem5ibAxlyf49Pzi/JPecK8EDaFZzFw3ZiAI/aiQE8bicGMOM6VigUdPCJq9jIyAjr6+s6GHMNGx4exjRNMpmMDj4FDFcwEcE0TdLptA4+axlYjilwdXVVB5MtAathNcDngMcVTIErKys6+OJCYD1MgUtLSzqYahq0w0QEj8dTDr5sCnSCKXBxcVEHXzUMOsVEBMMwWFhY0MEMYLqCKTCRSOjgGuB1BVOgZVk6+NoR2AymwPn5eR18Yws2iylwbm5OB9/WBS+CKXB2dlYH3wGXXMEUGI/H7cFWYOo1PT2tg+8rwFZiIsLU1JQOfgB8ynL2/9Cip6dHIpGIGEbtirO/vy+BQEBE5KaIbAC3DcM4sc0sGAxWXMvlcjQYH4HLNTMzDENisZj4/X4ZHx8vGbMsS/r7+9XbbyJStDmQgIjcq1mpU6kUAMfHx3R1dZVk5vV62d3dVUvjNtD/KMdM02R5ebnkDCYnJyuOMhqNquFfwNWGsY6OjvJ27jvAzs4OpmmWYN3d3RSLRTUv3BA2OjpKNpvVoTRwDfgDMDQ0VJGd1oXlG8IODg50KMl5YQQ2ADY3Nyuwvr4+fc11x5gWCbS2DbihBnp7eyvAra0tNbzWKFb1OQ34AmBZVgU2Njam1v4Ggk6xmk+gQBjg8PCQzs7OEszn87G3t6f2iDrB6k4CrgA/ASYmJurdD39Qq7ycbxSp+2n+z5sB2N7exjCMEiwUCnF6eqrAO072s8NCwF+AwcFBRAS/3084HCafz+vf+6cLY+dgFiCXy5FMJjk6OqryY+YzTu8oNthAtd2BE876j4EW5FQCftWQAnAfCLQU0bC7nNWnWzTYfv8DAkt5iGd39JQAAAAASUVORK5CYII=") 1x,
      url("data:image/png;base64,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") 2x
    )
    1 1,
    auto;
}

p, h1, h2, h3 {
  margin: 0;
}

a {
  color: inherit;
  text-decoration: none;
}

/* Custom pointing-hand cursor on every clickable link, and on the whole
   project card — hovering anywhere on a card is what reveals its
   "View Case Study" button, so the whole card should read as clickable. */
a,
.project,
.stack-edge,
.get-in-touch-btn {
  cursor: url("data:image/png;base64,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") 9 2, pointer;
  cursor:
    image-set(
      url("data:image/png;base64,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") 1x,
      url("data:image/png;base64,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") 2x
    )
    9 2,
    pointer;
}

@media (prefers-reduced-motion: no-preference) {
  a, .case-study-btn {
    transition: color 0.15s ease, border-color 0.15s ease, background-color 0.15s ease;
  }
}

a:focus-visible,
.case-study-btn:focus-visible {
  outline: 2px solid var(--fg);
  outline-offset: 3px;
  border-radius: 4px;
}

.page {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  padding: var(--page-pad-y) var(--page-pad-x);
  max-width: 1920px;
  margin-inline: auto;
}

.grid-row {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--grid-gap);
}

/* ---------- Header ---------- */

.site-header .col {
  display: flex;
  flex-direction: column;
}

.identity-name {
  margin: 0;
  font-weight: 700;
  color: var(--fg);
}

.muted-link,
.muted-text {
  color: var(--muted);
}

.muted-link:hover {
  color: var(--fg);
}

/* Bare icon at rest; hovering grows the label's max-width from 0, which
   the icon never moves for — the reveal always grows left-to-right, away
   from the icon, since that's the one edge a max-width transition can't
   ambiguously animate from. (A grid 0fr->1fr track was tried first, but
   browsers approximate that interpolation and it read as sweeping in
   from the right instead.) */
.social-link {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  height: 31px;
  padding: 0 7px;
  border: 1px solid transparent;
  border-radius: 9px;
  color: var(--muted);
  background: transparent;
}

.social-icon {
  flex: none;
  width: 16px;
  height: 16px;
}

@media (prefers-reduced-motion: no-preference) {
  .social-link {
    transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
  }
}

.social-link:hover,
.social-link:focus-visible {
  color: var(--fg);
  background: var(--surface);
  border-color: var(--border);
}

.social-label {
  max-width: 0;
  overflow: hidden;
}

@media (prefers-reduced-motion: no-preference) {
  .social-label {
    transition: max-width 0.5s ease;
  }
}

.social-link:hover .social-label,
.social-link:focus-visible .social-label {
  /* Generous enough to fit the email address, the longest label. */
  max-width: 220px;
}

.social-label-text {
  display: block;
  padding-left: 7px;
  font-size: 13px;
  font-weight: 600;
  white-space: nowrap;
}

.site-header .nav,
.site-header .socials {
  flex-direction: row;
  flex-wrap: wrap;
  gap: 18px;
}

/* Not wrap: the socials column has a fixed grid width, and the email pill
   expanding open on hover is wider than that column has spare room for.
   Wrapping the row down a line looked broken, so this lets the expanded
   pill overflow rightward instead — .social-link keeps its own natural
   size (flex-shrink:0) so nothing else in the row gets squeezed to make
   room for it. */
.site-header .socials {
  flex-wrap: nowrap;
}

.nav-link.is-active {
  font-weight: 700;
  color: var(--fg);
}

.availability {
  color: var(--muted);
}

.availability-row {
  display: inline-flex;
  align-items: center;
  gap: 18px;
}

.get-in-touch-btn {
  display: inline-flex;
  align-items: center;
  flex: none;
  height: 31px;
  padding: 0 14px;
  border: 1px solid var(--border);
  border-radius: 9px;
  background: var(--surface);
  color: var(--fg);
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
}

@media (prefers-reduced-motion: no-preference) {
  .get-in-touch-btn {
    transition: background-color 0.15s ease, border-color 0.15s ease;
  }
}

.get-in-touch-btn:hover,
.get-in-touch-btn:focus-visible {
  background: #202020;
  border-color: #3d3d3d;
}

/* Crossfades between "Get in touch" and the "Email copied" confirmation —
   text is swapped by JS while this is at opacity 0, so the change itself
   is never visible mid-transition. */
.get-in-touch-label {
  display: inline-block;
}

@media (prefers-reduced-motion: no-preference) {
  .get-in-touch-label {
    transition: opacity 0.15s ease;
  }
}

.get-in-touch-label.is-swapping {
  opacity: 0;
}

/* ---------- About page ---------- */

.about-content {
  margin-top: auto;
  margin-bottom: auto;
  max-width: 640px;
}

.about-text {
  margin: 0 0 14px;
  color: var(--muted);
  font-size: 15px;
  line-height: 1.6;
}

.about-text:last-child {
  margin-bottom: 0;
}

.mono {
  padding: 0.15em 0.45em;
  border-radius: 5px;
  background: rgba(255, 255, 255, 0.08);
  font-family: var(--font-mono);
  font-size: 0.85em;
  color: var(--muted);
}

/* ---------- Projects row ---------- */

.projects {
  display: flex;
  align-items: flex-end;
  gap: var(--grid-gap);
  margin-top: auto;
  min-height: 0;
}

.project {
  position: relative;
  min-width: 0;
  flex: 0 0 var(--col-width, calc((100% - 3 * var(--grid-gap)) / 4));
  width: var(--col-width, calc((100% - 3 * var(--grid-gap)) / 4));
}

/* Everything that makes up "the card" (image + title + description) lives
   here so it can be transformed as one rigid unit. The button is a
   descendant, absolutely positioned against this box, so lifting this
   element on hover carries the button up with it at a fixed distance. */
.project-inner {
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: 0;
}

@media (prefers-reduced-motion: no-preference) {
  .project-inner {
    transition: transform 0.28s cubic-bezier(0.22, 1, 0.36, 1);
  }
}

.project--has-button:hover .project-inner,
.project--has-button:focus-within .project-inner {
  transform: translateY(calc(-1 * var(--btn-lift, 0px)));
}

.project-media {
  width: 100%;
  border-radius: var(--radius);
  overflow: hidden;
  line-height: 0;
}

.project-media img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius);
}

.project-title {
  margin: var(--media-gap) 0 0;
  font-size: 16px;
  font-weight: 700;
  line-height: 1.3;
  color: var(--fg);
}

.project-title--concept {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}

.concept-badge {
  display: inline-flex;
  align-items: center;
  padding: 3px 8px;
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--muted);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

.project-desc {
  margin: 5px 0 0;
  color: var(--muted);
  /* Cards are bottom-aligned as a row, so a description short enough to
     fit on one line makes its whole card shorter than the others and
     pushes its image down to stay flush with the shared baseline.
     Reserving two lines' worth of height regardless of actual line count
     keeps every card's image top aligned no matter how long the copy is. */
  min-height: 3em;
}

/* Positioned just below project-inner's own bottom edge (its resting,
   untransformed position). At rest it sits past the shared card baseline
   and is invisible; when project-inner lifts by --btn-lift on hover, this
   button rides along and lands with its bottom flush on that baseline. */
.case-study-btn {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  margin-top: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  padding: 14px 20px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--fg);
  font-family: inherit;
  font-size: 14px;
  font-weight: 700;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.22s ease, background-color 0.15s ease, border-color 0.15s ease;
}

.project--has-button:hover .case-study-btn,
.project--has-button:focus-within .case-study-btn {
  opacity: 1;
  pointer-events: auto;
}

.case-study-btn:hover {
  background: #202020;
  border-color: #3d3d3d;
}

.case-study-btn svg {
  flex: none;
  width: 14px;
  height: 14px;
}

@media (prefers-reduced-motion: reduce) {
  .case-study-btn {
    transition: opacity 0.01ms;
    transform: none;
  }
}

/* ---------- Card page carousel (The Pulse, IMDb) ---------- */
/* Every page lives in the DOM at once, each tagged via data-slot
   (front/mid/back/hidden) by script.js based on how far it sits from the
   current front page. Cycling with the arrows just relabels the slots —
   every page smoothly tweens between its old and new geometry, which is
   what gives the "shuffle" its animation for free, no JS-driven frames.
   Shared by any card that wants this treatment — .stack--pulse/--imdb
   just set the card's native aspect ratio; everything else is generic. */

.stack {
  position: relative;
  /* Reserves the card's height (matching the native page aspect ratio)
     since every .stack-page is absolutely positioned and contributes no
     intrinsic height of its own. .project-media clips to rounded corners
     via overflow:hidden, but the mid/back pages peek out above the front
     page on purpose, so this needs to opt back out of that clipping. */
  overflow: visible;
  /* Hands vertical panning to the browser (so the page scrolls natively
     and smoothly) while reserving horizontal gestures here for the swipe
     handling in script.js — the browser decides which axis a touch means
     before either one gets to react, so this needs to be a CSS property,
     not something JS could arbitrate after the fact. */
  touch-action: pan-y;
}

.stack--pulse {
  aspect-ratio: 2650 / 4096;
}

.stack--imdb {
  aspect-ratio: 1 / 1;
}

/* Specificity note: `.project-media img` (a class + a type selector) would
   otherwise beat a bare `.stack-page` on width/height, so this is scoped
   through `.stack` to outrank it. */
.stack .stack-page {
  position: absolute;
  left: 50%;
  top: 0;
  width: 100%;
  height: 100%;
  transform: translateX(-50%);
  /* Pivoting from the bottom keeps every card's bottom edge flush with
     its neighbors (they're bottom-aligned as a row) — only the top swings
     out on lean, which is also right where the mid/back pages peek out. */
  transform-origin: bottom center;
  border-radius: var(--radius);
  object-fit: cover;
  /* Crop from the bottom, not the center, when a page's box shrinks to
     mid/back size — keeps each page's own top aligned with the others
     instead of centering on a random middle slice, which read as a
     misaligned, top-cropped mess. */
  object-position: top;
  opacity: 1;
  /* Stops iOS's native image drag-ghost and long-press save-image callout
     from hijacking the swipe gesture. */
  -webkit-user-drag: none;
  -webkit-touch-callout: none;
}

@media (prefers-reduced-motion: no-preference) {
  .stack-page {
    transition: width 0.42s cubic-bezier(0.22, 1, 0.36, 1),
      height 0.42s cubic-bezier(0.22, 1, 0.36, 1),
      top 0.42s cubic-bezier(0.22, 1, 0.36, 1),
      filter 0.42s ease,
      opacity 0.3s ease,
      transform 0.3s ease;
  }
}

.stack-page[data-slot="front"] {
  z-index: 3;
}

/* Nudging into either edge of the front page leans it toward that side,
   like tipping the top sheet of a stack to peek at what's underneath —
   the tilt itself is the "there's more here" cue, no arrow needed. */
.stack:has(.stack-edge--prev:hover) .stack-page[data-slot="front"],
.stack:has(.stack-edge--prev:focus-visible) .stack-page[data-slot="front"] {
  transform: translateX(-50%) rotate(-1.5deg);
}

.stack:has(.stack-edge--next:hover) .stack-page[data-slot="front"],
.stack:has(.stack-edge--next:focus-visible) .stack-page[data-slot="front"] {
  transform: translateX(-50%) rotate(1.5deg);
}

/* Washed out like a dimmer, unlit page rather than a translucent one —
   filter darkens the pixels themselves (still fully opaque), so the page
   underneath never bleeds through where the pages overlap. The page
   further back reads dimmer than the mid one, like it's further from
   the light. */
/* `top` is sized off the shared column width rather than a plain
   percentage — percentages here resolve against each card's own HEIGHT,
   so the same value produced a visibly bigger peek on the tall Pulse
   card than on the square IMDb one. --col-width is set by script.js and
   is the same for every card in the row, so this keeps the peek gap
   identical across cards regardless of their own aspect ratio. */
.stack-page[data-slot="mid"] {
  width: 91.3%;
  height: 78.9%;
  top: calc(var(--col-width, 360px) * -0.03);
  filter: brightness(0.7) saturate(0.85);
  z-index: 2;
}

.stack-page[data-slot="back"] {
  width: 81.2%;
  height: 70.5%;
  top: calc(var(--col-width, 360px) * -0.06);
  filter: brightness(0.5) saturate(0.75);
  z-index: 1;
}

.stack-page[data-slot="hidden"] {
  width: 71%;
  height: 62%;
  top: calc(var(--col-width, 360px) * -0.09);
  filter: brightness(0.5) saturate(0.75);
  opacity: 0;
  pointer-events: none;
  z-index: 0;
}

/* No visible buttons or icons at all — these are just invisible hit zones
   over the outer ~32% of either side of the image. The lean on the front
   page (above) is the only visual feedback. */
.stack-edge {
  position: absolute;
  top: 0;
  bottom: 0;
  z-index: 4;
  width: 32%;
  padding: 0;
  margin: 0;
  border: none;
  background: none;
}

.stack-edge--prev {
  left: 0;
}

.stack-edge--next {
  right: 0;
}

/* ---------- Responsive ---------- */

@media (max-width: 900px) {
  .site-header.grid-row {
    grid-template-columns: 1fr 1fr;
    row-gap: 29px;
  }

  .availability {
    grid-column: 1 / -1;
  }

  .projects.grid-row,
  .projects {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 50px;
    /* The stack's mid/back pages deliberately peek out above its own top
       edge (see the ".stack { overflow: visible }" note above) — on
       desktop there's always enough clearance above the row for that, but
       stacked full-width on mobile the first card sits close enough to
       the header that the peek could climb into the availability text.
       64px clears the worst case (~0.06 * the 414px mobile cap) with
       deliberate breathing room on top of that, not just the bare minimum. */
    padding-top: 64px;
  }

  .project {
    flex: none;
    width: 100%;
  }

  .project-media,
  .stack {
    max-width: 414px;
  }

  .case-study-btn {
    position: static;
    margin-top: 14px;
    opacity: 1;
    pointer-events: auto;
  }

  .project--has-button:hover .project-inner,
  .project--has-button:focus-within .project-inner {
    transform: none;
  }
}

@media (max-width: 520px) {
  .site-header.grid-row {
    grid-template-columns: 1fr;
    row-gap: 22px;
  }
}
