/* Base element styles */

/* Enables native cross-document View Transitions for full-page navigation
   site-wide — a smooth default cross-fade everywhere out of the box,
   customized below into a directional carousel slide specifically for the
   project prev/next navigation (see .project-nav in css/pages/project.css,
   which sets a direction hint that js/main.js passes to the next page via
   sessionStorage, applied as a class on <html> before first paint). Browsers
   without support just keep their normal, instant navigation — this is
   pure progressive enhancement, nothing degrades. */
@view-transition {
  navigation: auto;
}

@media (prefers-reduced-motion: no-preference) {
  /* Targets the whole-document "root" group rather than <main> itself.
     Naming <main> directly seems like the obvious way to exclude the
     header from the slide, but its group would then be sized to <main>'s
     own content-box — which varies a lot page to page (some galleries
     have 5 images, some have 2) — and the browser tweens the group's
     width/height between those two real sizes over the animation, visibly
     stretching/squashing the content mid-slide. "root" doesn't have that
     problem: per spec its group is always sized to the snapshot viewport,
     never the document's actual (scrollable, content-dependent) height.
     The header is excluded from the slide instead by giving *it* the
     constant-size group (view-transition-name: site-header, in
     css/layout.css) — its own box is the same size on every page, so it
     can't morph either, and it's left with no animation override here, so
     it just quietly stays in place. */
  ::view-transition-group(root) {
    animation-duration: 300ms;
  }

  /* Asymmetric easing (Material Design's "emphasized" pair) reads smoother
     and snappier than one shared curve for both halves: the outgoing page
     accelerates away quickly so it doesn't linger, while the incoming page
     decelerates into place gently instead of slamming to a stop. */
  .vt-next::view-transition-old(root),
  .vt-prev::view-transition-old(root) {
    animation-timing-function: cubic-bezier(0.3, 0, 0.8, 0.15);
  }

  .vt-next::view-transition-new(root),
  .vt-prev::view-transition-new(root) {
    animation-timing-function: cubic-bezier(0.05, 0.7, 0.1, 1);
  }

  .vt-next::view-transition-old(root) {
    animation-name: pf-slide-out-left;
  }

  .vt-next::view-transition-new(root) {
    animation-name: pf-slide-in-right;
  }

  .vt-prev::view-transition-old(root) {
    animation-name: pf-slide-out-right;
  }

  .vt-prev::view-transition-new(root) {
    animation-name: pf-slide-in-left;
  }

  /* Horizontal-only — no translateY. .page's own "Fade Content" entrance
     (css/layout.css) is separately suppressed for these directional
     arrivals so it can't add a vertical component on top of this and read
     as a diagonal, from-the-bottom-corner motion. */
  @keyframes pf-slide-out-left {
    to {
      transform: translateX(-8%);
      opacity: 0;
    }
  }

  @keyframes pf-slide-in-right {
    from {
      transform: translateX(8%);
      opacity: 0;
    }
  }

  @keyframes pf-slide-out-right {
    to {
      transform: translateX(8%);
      opacity: 0;
    }
  }

  @keyframes pf-slide-in-left {
    from {
      transform: translateX(-8%);
      opacity: 0;
    }
  }
}

/* Custom cursor: a circle matching the favicon's shape, rendered as a DOM
   element (js/main.js) rather than a native `cursor: url()` image, because
   only a painted element can use mix-blend-mode. The blend mode inverts the
   circle against whatever is underneath, so it stays visible over light or
   dark artwork alike. The native cursor is only hidden once JS confirms the
   custom one is actually running, so nothing disappears if JS is off. */
.has-custom-cursor,
.has-custom-cursor * {
  cursor: none;
}

.custom-cursor {
  position: fixed;
  top: 0;
  left: 0;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: #fff;
  mix-blend-mode: difference;
  pointer-events: none;
  z-index: 9999;
  opacity: 0;
  will-change: transform;
}

@media (hover: none), (pointer: coarse) {
  .has-custom-cursor,
  .has-custom-cursor * {
    cursor: auto;
  }

  .custom-cursor {
    display: none;
  }
}

/* Click Spark: a burst of lines from the cursor on press (js/main.js).
   Same white + difference blend as the cursor, so the sparks read as an
   extension of it rather than a separately colored effect. */
.click-spark-canvas {
  position: fixed;
  inset: 0;
  pointer-events: none;
  mix-blend-mode: difference;
  z-index: 9998;
}

/* Text selection highlight — grey instead of the browser-default blue,
   with dark text forced so it stays readable on both light and dark pages */
::selection {
  background-color: #f2f0f0;
  color: var(--color-ink);
}

::-moz-selection {
  background-color: #f2f0f0;
  color: var(--color-ink);
}

body {
  font-family: var(--font-body);
  font-weight: 300;
  font-size: var(--fs-body);
  line-height: var(--lh-normal);
  color: var(--color-ink);
  background: var(--color-paper);
  letter-spacing: var(--ls-tight);
  /* Paired so macOS renders the same lighter, thinner antialiasing in both
     engines — WebKit/Blink read the -webkit- property; Gecko (Firefox) only
     responds to its own -moz-osx- one. Without the second, Firefox on
     macOS falls back to its default (visibly heavier/bolder) text
     rendering while Chrome/Safari right next to it look lighter, for the
     exact same font-weight. */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

body.is-dark {
  color: var(--color-paper);
  background: var(--color-ink);
}

h1,
h2,
h3,
h4 {
  font-family: var(--font-display);
  font-weight: 300;
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-tight);
}

p {
  max-width: 65ch;
}

a {
  transition: opacity var(--duration-fast) var(--ease);
}

a:focus-visible,
button:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 3px;
}

/* Underline hover — signature interaction from the original site. The two
   colors come from --link-a/--link-b, set inline by initLinkHoverColors()
   in js/main.js: a fresh random neon pair from that function's palette
   list is assigned every time a link is entered, so the accent doesn't
   repeat the same combo hover after hover. The var() fallbacks (first
   palette entry) only matter before JS has attached its listener. */
.link-hover {
  position: relative;
  display: inline-block;
}

.link-hover::after {
  content: "";
  position: absolute;
  left: 0;
  right: 100%;
  bottom: -0.15em;
  height: 2px;
  background: linear-gradient(to right, var(--link-a, #3494e6), var(--link-b, #f014e0));
  transition: right var(--duration-base) var(--ease);
}

.link-hover:hover::after,
.link-hover:focus-visible::after,
.link-hover.is-active::after,
/* :hover doesn't reliably fire from touch, which is why the logo's
   long-press-and-drag gesture (initTextPressure in js/main.js) drives
   this underline directly via a class instead of leaning on :hover —
   otherwise the underline and the per-character scale/weight effect
   would fall out of sync on touch: one triggered by an inconsistent
   browser hover quirk, the other by our own explicit gesture state. */
.link-hover.is-pressing::after {
  right: 0;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Skip-to-content link: visually-hidden by default, revealed on keyboard focus */
.skip-link:focus {
  position: fixed;
  top: var(--space-sm);
  left: var(--gutter);
  z-index: 1000;
  width: auto;
  height: auto;
  padding: 0.6rem 1rem;
  margin: 0;
  overflow: visible;
  clip: auto;
  white-space: normal;
  background: var(--color-ink);
  color: var(--color-paper);
  border-radius: var(--radius-sm);
}

.is-dark .skip-link:focus {
  background: var(--color-paper);
  color: var(--color-ink);
}
