/* ============================================
   Andrei Ulazovskii — shared styles (every page)
   base, header, mobile menu, footer, text styles.
   Page-specific styles stay inside each page.
   ============================================ */

@import url('https://fonts.googleapis.com/css2?family=Onest:wght@400;500;600&display=swap');

:root {
  --bg: #FFFFFF;
  --ink: #000000;

  --font-display: 'Onest', -apple-system, BlinkMacSystemFont, sans-serif;
  --font-body: 'Onest', -apple-system, BlinkMacSystemFont, sans-serif;

  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  --dur: 0.7s;

  --gutter: 24px;
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html {
  scroll-behavior: smooth;
  overscroll-behavior-y: none;
}

body {
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1;
  -webkit-font-smoothing: antialiased;
  overscroll-behavior-y: none;
}


/* Page fade between pages: a white veil over the page that fades out on
   entry and back in before leaving. Not opacity on main/footer/bg-layer:
   an ancestor with opacity < 1 isolates everything inside it, so elements
   with mix-blend-mode (the outlined pill buttons, blended text) lost the
   white page behind them and stayed invisible until the fade had ended.
   The veil sits below the header (50) and the mobile menu (45), so the
   header stays put and never disappears on navigation, and above the
   footer (40) and everything in the page. */
body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 44;
  background: var(--bg);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.4s var(--ease);
}

body.is-leaving::after,
body.is-entering::after {
  opacity: 1;
}

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

img {
  display: block;
  max-width: 100%;
}

button {
  font: inherit;
  background: none;
  border: none;
  cursor: pointer;
  color: inherit;
}

/* ---------- Layout shell ---------- */

.shell {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.wrap {
  width: 100%;
  padding-left: var(--gutter);
  padding-right: var(--gutter);
}


@media (max-width: 1023px) {
  :root {
    --gutter: 16px; /* .wrap's own side padding, and everything else keyed to var(--gutter) */
  }
}

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

.site-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 50;
  padding-top: calc(24px + env(safe-area-inset-top, 0px));
  padding-bottom: 24px;
  mix-blend-mode: difference;
}

.shell {
  padding-top: calc(110px + env(safe-area-inset-top, 0px));
}

.site-header .wrap {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  column-gap: 24px;
  align-items: start;
}

.brand {
  grid-column: 1 / 4;
  justify-self: start;
  display: inline-block;
  overflow: hidden;
  height: 18px;
  font-family: var(--font-body);
  font-size: 18px;
  font-weight: 400;
  letter-spacing: -0.01em;
  white-space: nowrap;
  color: #fff;
}

.brand .nav-link-mask {
  display: flex;
  flex-direction: column;
  transition: transform 0.45s var(--ease);
}

.brand .nav-link-mask span {
  display: block;
  height: 18px;
  line-height: 1;
}

.brand:hover .nav-link-mask {
  transform: translateY(-18px);
}

.tagline {
  grid-column: 4 / 7;
  font-size: 18px;
  color: #fff;
  line-height: 1;
  height: 36px; /* two lines at line-height: 1, clips the intro mask */
  overflow: hidden;
}

.tagline-mask {
  display: block;
  transition: transform 0.45s var(--ease);
  transition-delay: 0.07s; /* always a beat after the brand/Back next to it */
}

.header-right {
  grid-column: 11 / 13;
  justify-self: end;
  display: flex;
  gap: 28px;
  font-size: 18px;
}

.header-right a {
  position: relative;
  display: inline-block;
  overflow: hidden;
  height: 18px;
  color: #fff;
}

.nav-link-mask {
  display: flex;
  flex-direction: column;
  transition: transform 0.45s var(--ease);
}

.nav-link-mask span {
  display: block;
  height: 18px;
  line-height: 1;
}

.header-right a:hover .nav-link-mask {
  transform: translateY(-18px);
}

/* Header hand-off between the home page and project pages: leaving, the
   name (or "Back") and the tagline go up into their masks — the preloader's
   drop-in played backwards; on the next page what belongs there drops in.
   The tagline is not shown on project pages at all. After the hover rules,
   so a hovered link still goes up and out. */
.is-inner .tagline-mask,
.is-header-out .tagline-mask,
.is-header-out .brand .nav-link-mask {
  transform: translateY(-100%);
}

/* Icon+text variant for header-right links (currently just "Get in touch").
   A <div> rather than a <span> for the row itself, same reason as .btn-row:
   ".nav-link-mask span" above is a descendant selector and would otherwise
   force display:block/height:18px onto it too. */
.link-row {
  display: flex;
  align-items: center;
  gap: 12px;
  height: 18px;
  line-height: 1;
  white-space: nowrap;
}

.link-icon {
  width: 10px;
  height: 10px;
  flex-shrink: 0;
  display: block;
}

/* "Back" arrow on project pages (brand spot) — kept at its own 15x14 size */
.link-icon-back {
  width: 15px;
  height: 14px;
}

@media (max-width: 1023px) {
  .shell {
    padding-top: calc(64px + env(safe-area-inset-top, 0px));
  }

  .site-header {
    padding-top: calc(16px + env(safe-area-inset-top, 0px));
    padding-bottom: 16px;
  }

  .site-header .wrap {
    display: flex;
    align-items: center;
    justify-content: space-between;
  }

  .tagline,
  .header-right {
    display: none;
  }
}

/* ---------- Mobile menu toggle (burger / close) ---------- */

.menu-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px; /* 48x48 touch target — the visible icon stays 16x5, centered inside via .menu-toggle-icon */
  flex-shrink: 0;
  opacity: 1;
  transition: opacity 0.45s var(--ease);
}

.is-intro .menu-toggle {
  opacity: 0;
  transition: none;
}

.menu-toggle-icon {
  position: relative;
  width: 16px;
  height: 6px; /* whole px, so it sits on a whole-px position inside the 48px
                  button; the bars themselves span 5px of it (1.5 + 2 + 1.5) */
}

/* The bars are 3px boxes on whole-px positions, drawn at 1.5px by
   scaleY(0.5) and nudged by ±0.25px with translateY. Browsers round the
   size and position of a box to whole pixels, but not a transform — with
   plain 1.5px boxes on half-px positions the two bars were rounded
   differently (one to 1px, the other to 2px). Transforms are drawn at the
   screen's real resolution, so both bars come out identical.
   Visible result (relative to the icon box): bar 1 at 0.5–2px, bar 2 at
   4–5.5px — i.e. 1.5px bars with a 2px gap, centered in the button. */
.menu-toggle-bar {
  position: absolute;
  left: 0;
  width: 16px;
  height: 3px;
  background: #fff;
  transform-origin: 50% 50%;
  transition: transform 0.35s var(--ease);
}

.menu-toggle-bar:nth-child(1) {
  top: 0;
  transform: translateY(-0.25px) rotate(0deg) scaleY(0.5);
}

.menu-toggle-bar:nth-child(2) {
  top: 3px;
  transform: translateY(0.25px) rotate(0deg) scaleY(0.5);
}

/* Open: both bars meet at the icon's centre (3px) and cross */
.menu-toggle.is-open .menu-toggle-bar:nth-child(1) {
  transform: translateY(1.5px) rotate(45deg) scaleY(0.5);
}

.menu-toggle.is-open .menu-toggle-bar:nth-child(2) {
  transform: translateY(-1.5px) rotate(-45deg) scaleY(0.5);
}

@media (max-width: 1023px) {
  .menu-toggle {
    display: flex;
    /* The 48x48 hit area is bigger than the visible 16x5 icon, so letting it
       sit in the normal flex flow (like the brand) pushed the icon inward,
       away from the 16px edge every other header element sits at. Anchoring
       it to the header's own top-right corner instead keeps the icon at the
       same visual 16px inset — the extra hit area bleeds outward into the
       header's own padding/edge instead of pushing inward. */
    position: absolute;
    top: 0;
    right: 0;
  }
}

/* ---------- Mobile menu drawer ---------- */

.mobile-menu {
  display: none;
  position: fixed;
  top: -6px;
  left: -6px;
  right: -6px;
  bottom: -6px; /* overscan — some viewports/zoom levels round 100vw/100vh a
                   fraction short of the real viewport, leaving a hairline
                   sliver of the page showing at the top/side; this hides it */
  z-index: 45; /* below .site-header (50) so the brand + toggle stay visible/usable above it */
  overflow: hidden; /* clips the counter-sliding graphics below to whatever
                        portion of the panel is currently on screen — the
                        same mask-window principle as .nav-link-mask, just
                        used here as a 2D travelling reveal instead of a
                        vertical one */
  background: #000; /* solid — deliberately NOT blended itself, so it's a reliable black
                        backdrop for the difference-blended content sitting on top of it */
  transform: translateX(var(--slide-distance));
  transition: transform 0.5s var(--ease);
  /* local 5-column grid, viewport-based like --col-vw — percentages don't
     shrink for absolutely positioned children the way they do for gallery-wrap's
     in-flow ones, so this is measured directly off 100vw instead. Declared here
     (not on .mobile-menu-inner) so both the text and the graphics below share it. */
  --col: calc((100vw - 96px) / 5); /* 96px = 2*16 margin + 4*16 gutter */
  /* exact pixel distance the panel travels — matches its own overscanned
     width precisely (100vw + 12px, the 6px overscan doubled), so the AU/star
     counter-slide below can cancel it exactly rather than approximate it
     with a percentage taken from a different-sized element */
  --slide-distance: calc(100vw + 12px);
}

.mobile-menu.is-open {
  transform: translateX(0);
}

/* A local copy of the AU + star artwork, positioned with the exact same
   formulas as the real fixed .bg-layer. It has to be a real (small) copy
   rather than the same element reused: mix-blend-mode can't cross between
   two separate position:fixed layers (each is its own isolated stacking
   context), so the real .bg-layer sitting behind the opaque panel could
   only ever flatly cover this drawer's text, never interact with it. Kept
   inside .mobile-menu, in the same stacking context as the text below, the
   difference blend actually works both ways — same convention the header/
   hero text already uses against this artwork elsewhere on the site.

   Each one also carries a counter-slide: the exact opposite of the panel's
   own translateX, so as the black panel travels across the screen, these
   stay visually anchored at their real on-screen position the whole time
   — like the panel is a mask/curtain sliding across artwork that was
   already sitting there, rather than something riding along with it. */
.mobile-menu-graphics {
  position: absolute;
  inset: 6px; /* the panel itself is overscanned by 6px on every edge (see
                 .mobile-menu) to hide a rendering-rounding gap; without this
                 correction that overscan would become part of this wrapper's
                 own box and throw off every left/top below by 6px versus
                 the real, non-overscanned .bg-layer on the page */
  pointer-events: none;
}

.menu-au {
  position: absolute;
  width: 80px;
  top: calc(16px + env(safe-area-inset-top, 0px));
  left: calc(3.5 * var(--col) + 64px); /* centered on column 4 of 5 */
  transform: translateX(-50%) translateX(calc(-1 * var(--slide-distance)));
  transition: transform 0.5s var(--ease);
  display: flex;
  flex-direction: column;
  gap: 245px;
  filter: invert(1);
  mix-blend-mode: difference;
}

.mobile-menu.is-open .menu-au {
  transform: translateX(-50%) translateX(0);
}

.menu-au-a,
.menu-au-u {
  width: 80px;
}

.menu-au-a img,
.menu-au-u img {
  width: 100%;
  display: block;
}

.menu-star {
  position: absolute;
  width: 300px;
  height: 300px;
  left: calc(var(--col) - 118px); /* own center on the left edge of column 2 of 5 */
  top: 72%;
  transform: translateX(calc(-1 * var(--slide-distance)));
  transition: transform 0.5s var(--ease);
  filter: invert(1);
  mix-blend-mode: difference;
}

/* Safari on iPhone (iOS 26): nothing fixed is drawn below the edge of its
   floating bottom toolbar — the star was cut off there in a hard line, with
   the page showing underneath. So there the star sits right on that edge
   (its bottom = the bottom of the fixed area) instead of running off the
   screen. Same for its copy in the menu, so the two still line up.
   Other browsers keep the original position. */
@media (max-width: 1023px) {
  .is-ios-safari .bg-star,
  .is-ios-safari .menu-star {
    top: calc(100% - 300px); /* 300px = the mobile star's height */
  }
}

.mobile-menu.is-open .menu-star {
  transform: translateX(0);
}

.menu-star img {
  width: 100%;
  height: 100%;
  display: block;
}

.mobile-menu-inner {
  position: absolute;
  inset: 6px; /* same correction .mobile-menu-graphics already applies —
                 .mobile-menu is overscanned by 6px on every edge (see its
                 own comment), and without this every left/top/bottom below
                 (16px, column 1 start, etc.) was measured from 6px inside
                 the true viewport edge instead of from the edge itself */
  color: #fff;
  /* no mix-blend-mode here — it's applied per element below instead, so two
     overlapping white elements don't get flattened into one unit first
     (which was reading as merged/indistinct) */
}

.mobile-menu-tagline {
  position: absolute;
  top: 18%;
  left: calc(1 * var(--col) + 32px); /* column 2 start */
  font-size: 18px;
  line-height: 1.3;
  mix-blend-mode: difference;
}

.mobile-menu-links,
.mobile-menu-social {
  position: absolute;
  top: 58%;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.mobile-menu-links {
  left: 16px; /* column 1 start */
  font-size: 18px;
  mix-blend-mode: difference;
}

.mobile-menu-social {
  left: calc(2 * var(--col) + 48px); /* column 3 start */
}

.mobile-menu-email {
  position: absolute;
  top: 75%;
  left: calc(2 * var(--col) + 48px); /* column 3 start */
}

.mobile-menu-copyright {
  position: absolute;
  left: 16px; /* column 1 start */
  bottom: calc(12px + env(safe-area-inset-bottom, 0px));
  font-size: 18px;
  mix-blend-mode: difference;
}

.mobile-menu-links a,
.mobile-menu-social a,
.mobile-menu-email a {
  position: relative;
  display: inline-block;
  overflow: hidden;
  height: 24px; /* taller than the 18px font so descenders in "Telegram" /
                   "Instagram" aren't clipped — same fix as .footer-social */
  font-size: 18px;
  mix-blend-mode: difference;
}

.mobile-menu-links a .nav-link-mask span,
.mobile-menu-social a .nav-link-mask span,
.mobile-menu-email a .nav-link-mask span {
  height: 24px;
  line-height: 24px;
}

.mobile-menu-links a:hover .nav-link-mask,
.mobile-menu-social a:hover .nav-link-mask,
.mobile-menu-email a:hover .nav-link-mask {
  transform: translateY(-24px);
}

/* Text-only reveal mask (tagline lines, copyright — no hover-flip needed) */
.menu-text-mask {
  display: block;
  overflow: hidden;
  height: 24px; /* same descender fix as the links above */
  line-height: 24px;
}

.menu-text-mask-inner {
  display: block;
  transition: transform 0.45s var(--ease);
}

/* Everything starts pulled up out of its mask; opening the drawer drops each
   line into place in sequence (staggered via JS, same technique as the page
   preloader's header masks), closing just lets it recede together. */
.mobile-menu .nav-link-mask,
.mobile-menu .menu-text-mask-inner {
  transform: translateY(-100%);
}

.mobile-menu.is-open .nav-link-mask,
.mobile-menu.is-open .menu-text-mask-inner {
  transform: translateY(0);
}

body.menu-open {
  overflow: hidden;
}
@media (max-width: 1023px) {
  .mobile-menu {
    display: block;
  }
}

/* ---------- Footer ---------- */

.site-footer {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 40;
  padding: 28px 0 calc(24px + env(safe-area-inset-bottom, 0px));
  mix-blend-mode: difference;
  /* Only the individual links fade in (opacity, below) — that never touches
     hit-testing, so this fixed, full-width layer stayed clickable at all
     times, even pre-reveal, silently stealing taps/hover from whatever
     page content happened to sit underneath it (cards, buttons). Switched
     on only once the footer is actually shown. */
  pointer-events: none;
}

body.is-footer-visible .site-footer {
  pointer-events: auto;
}

.footer-grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  column-gap: 24px;
  align-items: end;
}

.footer-copy {
  grid-column: 1 / 4;
  font-size: 18px;
  line-height: 24px;
  color: #fff;
}

.footer-email {
  grid-column: 4 / 7;
  font-size: 18px;
}

/* Desktop: the email shares one row with the copyright / socials / back to top.
   Without flex the link is an inline-block inside a line box, and the strut's
   descender lifts it a few px above the other items. (On mobile it has its own
   row, so it's left as is.) */
@media (min-width: 1024px) {
  .footer-email {
    display: flex;
  }
}

.footer-social {
  grid-column: 7 / 10;
  display: flex;
  flex-direction: column;
  gap: 8px;
  font-size: 18px;
}

.footer-social a {
  align-self: flex-start;
}

.footer-backtotop {
  grid-column: 12 / 13;
  justify-self: end;
  font-size: 18px;
}

/* Reveal lives here, on the outer link wrapper — opacity + a small rise,
   triggered once the last project has scrolled past (see
   body.is-footer-visible, toggled in the script at the bottom of the page).
   Kept off the inner .nav-link-mask on purpose: that element also carries
   the hover flip, and having both effects compete for the same transform
   property there caused jitter and, worse, silently blocked hover once
   the footer was visible (the reveal rule's higher specificity always won). */
.footer-email a,
.footer-social a,
.footer-backtotop,
.footer-copy {
  position: relative;
  display: inline-block;
  overflow: hidden;
  height: 24px; /* taller than the header's 18px so descenders in "Telegram"/"Instagram" aren't clipped */
  color: #fff;
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.6s var(--ease), transform 0.6s var(--ease);
}

body.is-footer-visible .footer-email a,
body.is-footer-visible .footer-social a,
body.is-footer-visible .footer-backtotop,
body.is-footer-visible .footer-copy {
  opacity: 1;
  transform: translateY(0);
}

.footer-email .nav-link-mask span,
.footer-social a .nav-link-mask span,
.footer-backtotop .nav-link-mask span {
  height: 24px;
  line-height: 24px;
}

.footer-email a:hover .nav-link-mask,
.footer-social a:hover .nav-link-mask,
.footer-backtotop:hover .nav-link-mask {
  transform: translateY(-24px);
}

@media (max-width: 1023px) {
  .site-footer {
    padding: 0;
  }

  .footer-grid {
    display: grid;
    grid-template-columns: repeat(5, minmax(0, 1fr));
    column-gap: 16px;
    padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px)); /* fixed gap from the copyright row to the bottom of the page */
  }

  .footer-social {
    grid-column: 3 / 4;
    grid-row: 1;
    justify-self: start; /* content (Telegram/Instagram/Linkedin) is wider than one equal column — size to content instead of stretching/clipping to the cell, still flush against its left edge */
    margin-top: 200px; /* fixed gap from the last project card to Telegram */
  }

  .footer-email {
    grid-column: 3 / 6;
    grid-row: 2;
    margin-top: 80px; /* fixed gap from Linkedin to the email */
  }

  .footer-copy {
    grid-column: 1 / 4;
    grid-row: 3;
    margin-top: 166px; /* fixed gap from the email to the copyright row */
    align-self: end;
  }

  .footer-backtotop {
    grid-column: 4 / 6;
    grid-row: 3;
    margin-top: 166px; /* fixed gap from the email to the copyright row */
    justify-self: end;
    align-self: end;
  }
}

/* ---------- Background star (shared) ----------
   Fixed layer holding the star. On the home page it sits next to the AU
   letters and is always visible; on inner pages (<body class="is-inner">)
   it fades in together with the footer. */

.bg-layer {
  position: fixed;
  inset: 0;
  height: 100vh;
  height: 100dvh; /* iOS Safari sizes a plain fixed+inset:0 box against the
                     browser-chrome-collapsed viewport, not what's actually
                     visible — this pinned the star (positioned by % of this
                     box) partly behind/below the real visible area as the
                     address bar showed and hid. dvh keeps it matched to
                     what's actually on screen. */
  z-index: -1;
  pointer-events: none;
  /* Clipped sideways only (the star overhangs the left edge). */
  overflow: hidden;
  overflow-x: clip;
  overflow-y: visible;
  --col-vw: calc((100vw - 312px) / 12); /* 312px = 2*24 margin + 11*24 gutter */
}

.bg-star {
  position: absolute;
  left: calc(3 * var(--col-vw) - 104px); /* star's own center lands on column 4's left edge */
  top: calc(68.8% - 24px); /* nudged 24px higher than the original resting position */
  width: 400px;
  height: 400px;
  opacity: 1;
  filter: blur(0);
  transform: none;
  transition: top 0.9s var(--ease), left 0.9s var(--ease), transform 0.9s var(--ease);
}

.bg-star svg {
  width: 100%;
  height: 100%;
  display: block;
}

/* Smaller inner star, rotated independently on scroll (desktop only —
   see initStarRotation). Shares the same center as the outer star
   (200,200 in the 400x400 viewBox), so it spins in place with no drift. */
.bg-star-inner {
  transform-origin: 200px 200px;
  will-change: transform;
}

/* Small upward nudge, synced with the footer's own reveal
   (body.is-footer-visible) — reverses automatically on scroll back up
   since it's the same class toggle. */
body.is-footer-visible .bg-star {
  transform: translateY(-40px);
}

.is-inner .bg-star {
  opacity: 0;
  transition: opacity 0.6s var(--ease), transform 0.9s var(--ease);
}

.is-inner.is-footer-visible .bg-star {
  opacity: 1;
}

/* Inner pages: instead of the scroll-driven spin of the home page, the small
   star makes one smooth quarter turn as the footer appears, and turns back
   as it goes away (a quarter turn looks the same as the start, so it reads
   as one clean move). */
.is-inner .bg-star-inner {
  transform: rotate(0deg);
  transition: transform 1.2s var(--ease);
}

.is-inner.is-footer-visible .bg-star-inner {
  transform: rotate(90deg);
}

@media (max-width: 1023px) {
  .bg-layer {
    /* 5-column mobile grid: 16px outer margins, 16px gutters
       (96px = 2*16 margin + 4*16 gutter) */
    --col-vw: calc((100vw - 96px) / 5);
  }

  .bg-star {
    width: 300px;
    height: 300px;
    /* star's own center on the left edge of column 2 of 5 */
    left: calc(var(--col-vw) - 118px);
    top: 72%;
  }
}

/* ---------- Pill buttons (shared) ----------
   Home hero ("Get in touch" / "Download CV") and project pages (site link).
   Same mask hover-flip as the nav links, just at the button's 48px height. */

.btn {
  position: relative;
  display: inline-block;
  height: 48px;
  padding: 0 24px;
  border-radius: 24px;
  overflow: hidden;
  font-family: var(--font-body);
  font-size: 18px;
  line-height: 1;
  font-weight: 400;
  white-space: nowrap;
}

.btn-primary {
  background: var(--ink);
  color: #fff;
}

.btn-secondary {
  background: transparent;
  color: #fff;
  border: 1px solid #fff;
  mix-blend-mode: difference;
  /* Own compositing layer: Safari only applies mix-blend-mode correctly to
     elements the compositor draws as separate layers (like the fixed
     header). Painted into the page's shared layer — which Safari keeps
     transparent because of the negative z-index background (.bg-layer) —
     the white pill had nothing to blend with and stayed white on white.
     On the element itself, never on an ancestor (that would isolate it). */
  transform: translateZ(0);
}

.btn .nav-link-mask {
  display: flex;
  flex-direction: column;
  transition: transform 0.45s var(--ease);
}

.btn:hover .nav-link-mask {
  transform: translateY(-48px);
}

.btn-row {
  display: flex;
  align-items: center;
  gap: 12px;
  height: 48px;
}

.btn-icon {
  width: 10px;
  height: 10px;
  flex-shrink: 0;
  display: block;
}

/* ---------- Text styles ---------- */

/* Big display text: hero heading on the home page, project title and
   section headings on project pages. */
.text-display {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 48px;
  line-height: 1;
  letter-spacing: -0.02em;
}

@media (max-width: 1023px) {
  .text-display {
    font-size: 24px;
  }
}

/* ---------- Reduced motion ---------- */

@media (prefers-reduced-motion: reduce) {
  * {
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ---------- Focus visibility ---------- */

a:focus-visible,
button:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
}
