/* DM Sans is loaded via a <link> in each page's <head>, not an @import here
   — @import is discovered only after this file starts parsing (a serial
   round-trip), where a <link> is found by the browser's preloader and
   fetched in parallel from the very start of the page load. */

@font-face {
  font-family: 'Wamaki';
  src: url('fonts/Wamaki-Regular-Test1.ttf') format('truetype');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

:root {
  --background: #ffffff;
  --foreground: #0a0a0a;
  --muted-foreground: #737373;
  --muted-foreground-light: #b5b5b5;
  --dark-muted: #a3a3a3;
  --border: #e5e5e5;
}

/* Page transition (.page-transition / .page-transition__word) is styled via
   an inline <style> block in each page's <head>, not here — it has to paint
   before this stylesheet (and its Google Fonts @import above) is guaranteed
   to have loaded, or the curtain itself flashes unstyled for a moment on
   every navigation. Keep that inline block in sync across index.html,
   works.html, works2.html, about.html and 404.html if it's ever edited. */

* {
  box-sizing: border-box;
}

html,
body {
  height: 100%;
  margin: 0;
}

/* Reserves the scrollbar's width whether or not it's actually showing, so
   toggling overflow:hidden (the loading lock below) can't shift content
   by the scrollbar's width. */
html {
  scrollbar-gutter: stable;
}

/* Locks scrolling for a fixed 1s on load (research purposes only). */
html.is-loading,
html.is-loading body {
  overflow: hidden;
}

body {
  font-family: 'DM Sans', sans-serif;
  font-weight: 300;
  background: var(--background);
  color: var(--foreground);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

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

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

button {
  font: inherit;
  color: inherit;
  background: none;
  border: none;
  padding: 0;
}

.display {
  font-family: 'Wamaki', serif;
  font-weight: 400;
  margin: 0;
}

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

.small {
  font-size: 0.875rem;
}

.eyebrow {
  font-size: 0.75rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--muted-foreground);
  margin: 0;
}

.eyebrow--light {
  color: var(--muted-foreground-light);
}

.eyebrow--tight {
  letter-spacing: 0.12em;
}

.container {
  max-width: 1400px;
  margin: 0 auto;
  padding: 0 2rem;
}

/* Scrollbar */
::-webkit-scrollbar {
  width: 4px;
}
::-webkit-scrollbar-track {
  background: transparent;
}
::-webkit-scrollbar-thumb {
  background: #d4d4d4;
  border-radius: 0px;
}

/* ---------- Header ---------- */
.site-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 50;
  background: rgba(255, 255, 255, 0.95);
  backdrop-filter: blur(4px);
  border-bottom: 1px solid var(--border);
  opacity: 0;
  transform: translateY(-100%);
  transition: opacity 0.4s ease, transform 0.4s ease;
  pointer-events: none;
}

.site-header.is-visible {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}

.site-header__bar {
  max-width: 100%;
  margin: 0 auto;
  padding: 0 2rem;
  height: 3.5rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.brand {
  font-family: 'Wamaki', serif;
  font-size: 1.5rem;
  letter-spacing: -0.01em;
}

.nav-desktop {
  display: none;
  align-items: center;
  /* Grouped visually via spacing alone — no border/background box. */
  gap: 1.5rem;
}

.nav-desktop a {
  position: relative;
  display: inline-block;
  /* Centers the (narrower, lighter) resting text within the box the ghost
     below already reserves at full bold width — so bolding on hover grows
     it evenly from the center outward instead of only to the right. */
  text-align: center;
  font-size: 1rem;
  font-weight: 600;
  color: var(--muted-foreground);
  transition: color 0.2s ease;
}

/* Invisible weight-1000 "ghost" of the link's own text (nav-hover.js fills
   in data-text from the link's real textContent, so it's never hand-typed
   out of sync). height:0 keeps it out of flow vertically, but its width
   still counts toward this inline-block link's own shrink-to-fit width —
   so the link is already as wide as its boldest hover state at rest, and
   :hover below (which actually goes bold) never grows the box or shifts
   anything else in the row. */
.nav-desktop a::before {
  content: attr(data-text);
  display: block;
  height: 0;
  overflow: hidden;
  visibility: hidden;
  font-weight: 1000;
}

.nav-desktop a:hover,
.nav-desktop a:focus-visible {
  color: var(--foreground);
  font-weight: 1000;
}

/* Branding/Video/Design each hover to their own template color (same
   swatches as works-grid__item / work-preview__text) — About and Get in
   touch keep the plain hover above. */
.nav-desktop--middle a[data-color="red"]:hover {
  color: #ff3333;
}
.nav-desktop--middle a[data-color="blue"]:hover {
  color: #3386ff;
}
.nav-desktop--middle a[data-color="green"]:hover {
  color: #33cc70;
}

/* Get in touch — black button, sharp edges, no transition/hover animation
   (deliberately excluded from .nav-desktop a's color transition above). */
.nav-desktop a.nav-cta {
  background: #000;
  color: #fff;
  padding: 0.5rem 1.25rem;
  border-radius: 0;
  transition: none;
}

.nav-desktop a.nav-cta:hover {
  color: #fff;
}

.menu-toggle {
  display: flex;
  flex-direction: column;
  gap: 5px;
  padding: 0.25rem;
}

.menu-toggle span {
  display: block;
  height: 1px;
  width: 20px;
  background: currentColor;
  transition: transform 0.2s ease, opacity 0.2s ease;
}

.menu-toggle.is-open span:nth-child(1) {
  transform: translateY(6px) rotate(45deg);
}
.menu-toggle.is-open span:nth-child(2) {
  opacity: 0;
}
.menu-toggle.is-open span:nth-child(3) {
  transform: translateY(-6px) rotate(-45deg);
}

.nav-mobile {
  display: none;
  border-top: 1px solid var(--border);
  background: var(--background);
}

.nav-mobile.is-open {
  display: block;
}

.nav-mobile a {
  display: block;
  padding: 1rem 2rem;
  font-size: 1rem;
  font-weight: 600;
  color: var(--muted-foreground);
  border-bottom: 1px solid var(--border);
}

.nav-mobile a:hover,
.nav-mobile a:focus-visible {
  color: var(--foreground);
  /* No ghost-width trick needed here like .nav-desktop a above — each link
     is already a block that fills the full row width regardless of its
     text's weight, so going bold can't shift or resize anything. */
  font-weight: 1000;
}

.nav-mobile a[data-color="red"]:hover {
  color: #ff3333;
}
.nav-mobile a[data-color="blue"]:hover {
  color: #3386ff;
}
.nav-mobile a[data-color="green"]:hover {
  color: #33cc70;
}

@media (min-width: 768px) {
  /* Three-block header: brand left, work-category links centered, about +
     get-in-touch right — grid (not the base flex) so the middle block sits
     at the true center regardless of how wide the outer two blocks are. */
  .site-header__bar {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
  }
  .site-header__bar .brand {
    justify-self: start;
  }
  .nav-desktop {
    display: flex;
  }
  .nav-desktop--middle {
    justify-self: center;
  }
  .nav-desktop--right {
    justify-self: end;
  }
  .menu-toggle,
  .nav-mobile {
    display: none;
  }
}

/* ---------- Hero ---------- */
.hero {
  height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  position: relative;
  /* z-index establishes a stacking context here — without it, the -1 on
     .hero-bg/.hero-bg-loop below escapes past this element's own
     background instead of sitting behind its content within it. */
  z-index: 0;
  /* Shown while the transition video is still loading — matches its
     background so there's no color flash once it starts playing. */
  background: #000;
}

/* Opaque intro video (transition baked over the ambient backdrop). Plays
   once, then fades out to a hard cut onto the looping video underneath —
   the loop has its own cuts already, so the hand-off doesn't need to match
   frames. */
.hero-bg {
  position: absolute;
  inset: 0;
  z-index: -1;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  /* Hidden until JS starts playback (its first frame renders as soon as the
     video loads, well before that) — .hero's own black background shows
     through until then. */
  opacity: 0;
  transition: opacity 0.4s ease;
}

.hero-bg.is-playing {
  opacity: 1;
}

.hero-bg.is-done {
  opacity: 0;
}

/* Ambient looping backdrop video, behind the transition canvas. Starts
   hidden — revealed by JS once the transition reaches its 2nd frame, so
   it's ready underneath (no "starting" needed) well before the transition
   finishes. */
.hero-bg-loop {
  position: absolute;
  inset: 0;
  z-index: -2;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
}

.hero-bg-loop.is-visible {
  opacity: 1;
}

.hero__word {
  display: flex;
  align-items: flex-end;
  line-height: 1;
  user-select: none;
  font-family: 'Wamaki', serif;
  color: #ffffff;
  /* Capped at the size seen on normal/landscape viewports (~20rem, reached
     around 2000px wide); below that it scales down with viewport width so
     narrower or portrait aspect ratios shrink instead of clipping. */
  font-size: min(16vw, 20rem);
  will-change: transform;
}

.hero__clip {
  overflow: hidden;
  display: inline-block;
}

.hero__clip--up {
  /* the custom Wamaki font's "w" overshoots the tight glyph box slightly;
     pad the clip region and pull back with a matching negative margin so
     the extra room doesn't shift layout or alignment with "maki" */
  padding: 0.03em 0 0 0.08em;
  margin: -0.03em 0 0 -0.08em;
}

.hero__text {
  display: inline-block;
  transition: transform 0.9s cubic-bezier(0.16, 1, 0.3, 1);
}

.hero__clip--up .hero__text {
  transform: translateY(110%);
}
.hero__clip--down .hero__text {
  transform: translateY(-110%);
}

.hero.is-visible .hero__clip--up .hero__text,
.hero.is-visible .hero__clip--down .hero__text {
  transform: translateY(0);
}

.hero__scroll-hint {
  position: absolute;
  bottom: 2rem;
  left: 0;
  right: 0;
  display: flex;
  justify-content: center;
  opacity: 0;
  transition: opacity 0.6s ease 1.2s;
}

.hero__scroll-hint.is-visible {
  opacity: 1;
}

.hero__scroll-hint span {
  font-size: 10px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--muted-foreground-light);
}

/* ---------- Sections ---------- */
.section {
  border-top: 1px solid var(--border);
  padding: 6rem 0;
}

.section--tight {
  padding: 4rem 0;
}

.section--dark {
  background: #0a0a0a;
  color: #fff;
}

/* ---------- Slogan ---------- */
.slogan {
  border-top: 1px solid var(--border);
  /* More breathing room above (between the hero and this) than below. */
  padding: 20rem 0 8rem;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  /* Safety net: the sentence is sized to always fit one line (below), but
     this keeps a future longer/differently-worded slogan from ever
     breaking the page out to a horizontal scrollbar. */
  overflow-x: hidden;
}

.slogan__text {
  /* No max-width — a fixed ch-width is what was forcing the wrap. The
     font-size below is vw-driven specifically so the sentence keeps
     fitting one line as the viewport shrinks, rather than overflowing. */
  font-size: clamp(1rem, 5vw, 3.5rem);
  line-height: 1.25;
  white-space: nowrap;
  /* The cycling word's box (below) is reserved wide enough for the longest
     candidate but left-aligned, so shorter words leave invisible space on
     the right only — which pulls the centered line left of true visual
     center. Nudge the whole sentence right to compensate, without
     touching the left-alignment that keeps the words flush together. */
  transform: translateX(1.5ch);
}

/* Fade-in + rise (no clip mask like the hero) — revealed by JS once
   scrolled into view. Each word animates on its own, staggered so the
   whole sentence finishes showing in ~1.8s (6 words: 0.8s duration each,
   0.2s apart -> last word starts at 1.0s, ends at 1.8s). */
.slogan__word {
  display: inline-block;
  opacity: 0;
  transform: translateY(0.35em);
  transition: opacity 0.8s ease, transform 0.8s ease;
}

.slogan__text.is-visible .slogan__word {
  opacity: 1;
  transform: translateY(0);
}

.slogan__word:nth-child(1) {
  transition-delay: 0s;
}
.slogan__word:nth-child(2) {
  transition-delay: 0.2s;
}
.slogan__word:nth-child(3) {
  transition-delay: 0.4s;
}
.slogan__word:nth-child(4) {
  transition-delay: 0.6s;
}
.slogan__word:nth-child(5) {
  transition-delay: 0.8s;
}
.slogan__word:nth-child(6) {
  transition-delay: 1.0s;
}

/* Cycling word: three candidates stacked and absolutely positioned inside
   a fixed-size box (sized in `ch`, which scales with the fluid font-size
   above), so swapping between them doesn't reflow/resize the sentence.
   font/weight here match .slogan__cycle-item below so the `ch` sizing is
   computed against the actual rendered font (bold DM Sans), not the
   inherited serif — otherwise the box undersizes and clips the widest
   word. Deliberately no overflow clipping: the push (translateY) and the
   fade (opacity) run together, and since every non-active state already
   sits at opacity:0, there's nothing to hide by clipping — clipping was
   what cut words off before, not the transform itself. */
.slogan__cycle {
  position: relative;
  display: inline-block;
  font-family: 'DM Sans', sans-serif;
  font-weight: 800;
  min-width: 14ch;
  height: 1em;
  vertical-align: bottom;
  text-align: left;
}

.slogan__cycle-item {
  position: absolute;
  left: 0;
  bottom: 0;
  white-space: nowrap;
  font-family: 'DM Sans', sans-serif;
  font-weight: 800;
  opacity: 0;
  transform: translateY(100%); /* default: waiting below, ready to enter */
  transition: opacity 0.5s ease, transform 0.5s ease;
}

.slogan__cycle-item.is-active {
  opacity: 1;
  transform: translateY(0);
}

.slogan__cycle-item.is-leaving {
  opacity: 0;
  transform: translateY(-100%); /* pushed up and out, fading as it goes */
}

.slogan__cycle-item:nth-of-type(1) {
  color: #e94f37; /* authentic */
}
.slogan__cycle-item:nth-of-type(2) {
  color: #f6c445; /* approachable */
}
.slogan__cycle-item:nth-of-type(3) {
  color: #3f88c5; /* recognisable */
}

/* ---------- Works preview ---------- */
/* Each item is a card written directly in index.html — that's where you
   add its photo/video, link, title, tag, description, width and color.
   No .container, no padding — the grid touches both viewport edges — and
   no gap, so cells touch each other too. grid-auto-rows (not aspect-ratio
   per cell) keeps every row the same height regardless of how many
   columns a given cell spans. */
.works-preview {
  border-top: 1px solid var(--border);
}

.works-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: minmax(180px, 24vw);
  counter-reset: works-item;
}

/* Width: add data-span="2"/"3"/"4" to an item (defaults to 1). Spans in a
   run of items should add up to 4 to fill each row cleanly. */
.works-grid__item {
  counter-increment: works-item;
  grid-column: span 1;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background: #f0f0f0;
  color: inherit;
  text-decoration: none;
}

.works-grid__item[data-span="2"] {
  grid-column: span 2;
}
.works-grid__item[data-span="3"] {
  grid-column: span 3;
}
.works-grid__item[data-span="4"] {
  grid-column: span 4;
}

.works-grid__item:nth-child(even) {
  background: #e5e5e5;
}

/* Placeholder shown only until an item has a .works-grid__media (photo or
   video) — remove the media and this comes back automatically. */
.works-grid__item::before {
  content: counter(works-item);
  font-family: 'Wamaki', serif;
  font-size: 1.5rem;
  color: var(--muted-foreground-light);
}

.works-grid__item:has(.works-grid__media)::before {
  display: none;
}

.works-grid__media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Color: set data-color="red"/"blue"/"yellow"/"black"/"white" on the item —
   add more swatches here as needed, they're all driven off this one place. */
.works-grid__item[data-color="red"] {
  --overlay-color: #ff3333;
}
.works-grid__item[data-color="blue"] {
  --overlay-color: #3386ff;
}
.works-grid__item[data-color="yellow"] {
  --overlay-color: #ffc033;
}
.works-grid__item[data-color="black"] {
  --overlay-color: var(--foreground);
}
.works-grid__item[data-color="white"] {
  --overlay-color: var(--background);
}

/* The overlay's text is white by default (readable on every other swatch)
   — flip it dark specifically for the white swatch, or it'd disappear. */
.works-grid__item[data-color="white"] .works-grid__overlay {
  color: var(--foreground);
}

/* Hover overlay: slides up from the bottom to cover the bottom ~20% of the
   item. Title (bold) and a short tag sit on one line with a rule
   stretched between them; the description sits below. Both lines share
   the overlay's own 1em padding, so they line up on the same left/right
   edges — that's the only alignment mechanism needed here. */
.works-grid__overlay {
  position: absolute;
  inset: auto 0 0 0;
  height: 20%;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: 0.85em 1em;
  background: var(--overlay-color, rgba(10, 10, 10, 0.88));
  color: #fff;
  transform: translateY(100%);
  transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}

.works-grid__item:hover .works-grid__overlay {
  transform: translateY(0);
}

.works-grid__row {
  display: flex;
  align-items: center;
  gap: 0.75em;
  min-width: 0;
}

.works-grid__title {
  font-family: 'DM Sans', sans-serif;
  font-weight: 800;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.works-grid__rule {
  flex: 1;
  height: 2px;
  min-width: 1em;
  background: currentColor;
  opacity: 0.4;
}

.works-grid__tag {
  font-weight: 500;
  white-space: nowrap;
  opacity: 0.75;
}

.works-grid__desc {
  margin: 0;
  font-size: 0.85em;
  line-height: 1.3;
  opacity: 0.75;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
}

/* ---------- Services ---------- */
.services-layout {
  display: grid;
  grid-template-columns: 1fr;
  gap: 4rem;
}

.services-heading {
  font-size: 2.25rem;
  line-height: 1.2;
}

.services-list {
  display: flex;
  flex-direction: column;
}

.service {
  width: 100%;
  text-align: left;
  padding: 1.5rem 0;
  display: flex;
  align-items: flex-start;
  gap: 1.5rem;
  cursor: pointer;
  border-bottom: 1px solid var(--border);
}

.service:first-child {
  border-top: 1px solid var(--border);
}

.service__number {
  font-size: 0.75rem;
  color: var(--muted-foreground-light);
  padding-top: 0.25rem;
  min-width: 24px;
}

.service__body {
  flex: 1;
  display: block;
}

.service__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.service__title {
  font-size: 1.125rem;
  font-weight: 500;
  transition: color 0.2s ease;
}

.service:hover .service__title {
  color: var(--muted-foreground);
}

.service__toggle {
  font-size: 1.125rem;
  color: var(--muted-foreground);
  transition: transform 0.2s ease;
  display: inline-block;
}

.service.is-active .service__toggle {
  transform: rotate(45deg);
}

.service__desc {
  display: block;
  overflow: hidden;
  max-height: 0;
  opacity: 0;
  transition: max-height 0.3s ease, opacity 0.3s ease, margin-top 0.3s ease;
}

.service__desc span {
  display: block;
  font-size: 0.875rem;
  font-weight: 300;
  color: var(--muted-foreground);
  line-height: 1.6;
}

.service.is-active .service__desc {
  max-height: 6rem;
  margin-top: 0.75rem;
  opacity: 1;
}

@media (min-width: 1024px) {
  .services-layout {
    grid-template-columns: repeat(12, 1fr);
  }
  .services-intro {
    grid-column: span 4;
  }
  .services-list {
    grid-column: span 8;
  }
}

/* ---------- Studio ---------- */
.studio-layout {
  display: grid;
  grid-template-columns: 1fr;
  gap: 4rem;
  align-items: center;
}

.studio-image {
  order: 2;
  aspect-ratio: 3 / 4;
  overflow: hidden;
}

.studio-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.studio-copy {
  order: 1;
}

.studio-copy .eyebrow {
  margin-bottom: 2rem;
}

.studio-heading {
  font-size: clamp(2rem, 4vw, 3.5rem);
  line-height: 1.2;
  margin-bottom: 2rem;
}

.studio-paragraph {
  font-size: 0.875rem;
  font-weight: 300;
  color: var(--dark-muted);
  line-height: 1.6;
  margin: 0 0 1.5rem;
}

.studio-paragraph--last {
  margin-bottom: 0;
}

.studio-stats {
  margin-top: 3rem;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 2rem;
  padding-top: 3rem;
  border-top: 1px solid rgba(255, 255, 255, 0.1);
}

.studio-stat__number {
  font-size: 1.875rem;
}

.studio-stat__label {
  font-size: 0.75rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted-foreground);
  margin: 0.25rem 0 0;
}

@media (min-width: 1024px) {
  .studio-layout {
    grid-template-columns: repeat(12, 1fr);
  }
  .studio-image {
    grid-column: span 6;
    order: 1;
  }
  .studio-copy {
    grid-column: span 6;
    order: 2;
  }
}

/* ---------- Clients ---------- */
.eyebrow.eyebrow--light {
  margin-bottom: 2.5rem;
}

.clients-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 1px;
  background: var(--border);
}

.clients-grid__item {
  background: var(--background);
  padding: 2rem 1.5rem;
  display: flex;
  align-items: center;
  justify-content: center;
}

.clients-grid__item span {
  font-size: 1rem;
  color: var(--muted-foreground);
}

@media (min-width: 640px) {
  .clients-grid {
    grid-template-columns: repeat(4, 1fr);
  }
}

/* ---------- Contact ---------- */
.contact-layout {
  display: grid;
  grid-template-columns: 1fr;
  gap: 4rem;
}

.contact-heading {
  font-size: clamp(2.5rem, 5vw, 5rem);
  line-height: 1.05;
  margin: 1.5rem 0 2rem;
}

.contact-email {
  display: inline-flex;
  align-items: center;
  gap: 0.75rem;
  font-size: 0.875rem;
  font-weight: 500;
  border-bottom: 1px solid var(--foreground);
  padding-bottom: 1px;
  transition: color 0.2s ease, border-color 0.2s ease;
}

.contact-email:hover {
  color: var(--muted-foreground);
  border-color: var(--muted-foreground);
}

.contact-details {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
}

.contact-details__row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  padding: 1rem 0;
  border-bottom: 1px solid var(--border);
}

.contact-details__row:first-child {
  border-top: 1px solid var(--border);
}

.contact-details__value {
  font-size: 0.875rem;
  text-align: right;
}

@media (min-width: 1024px) {
  .contact-layout {
    grid-template-columns: repeat(12, 1fr);
  }
  .contact-layout > div {
    grid-column: span 6;
  }
}

/* ---------- Footer ---------- */
.site-footer {
  background: #0a0a0a;
  color: #ffffff;
  border-top: 1px solid var(--border);
  padding: 2rem 0;
}

/* .muted defaults to the light-page gray — override it here so the
   copyright line stays white on the footer's black background. */
.site-footer .muted {
  color: #ffffff;
}

.display-footer-logo {
  font-family: 'Wamaki', serif;
  font-weight: 400;
  line-height: 0.9;
  /* 128px cap; scales down on narrow viewports but the 5rem (80px) floor
     keeps it "huge" rather than shrinking away to normal text size. */
  font-size: clamp(5rem, 14vw, 8rem);
}

.site-footer__bar {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

.site-footer__social {
  display: flex;
  align-items: center;
  gap: 1.5rem;
}

.site-footer__social a {
  font-size: 0.75rem;
  color: #ffffff;
  transition: opacity 0.2s ease;
}

.site-footer__social a:hover {
  opacity: 0.6;
}

@media (min-width: 640px) {
  .site-footer__bar {
    flex-direction: row;
    /* The huge wamaki logo makes this row's items very different heights —
       align the shorter copyright/social text to its bottom edge instead
       of vertically centering them against it. */
    align-items: flex-end;
  }
}

/* ---------- Dev layout guides ---------- */
/* Temporary — outlines every div so you can see where they begin and end.
   Delete this block when you're done checking layout. */
div {
  outline: 1px solid #1f51ff;
  outline-offset: -1px;
}

/* ---------- Work list (works.html) ---------- */
.work-main {
  padding-top: 3.5rem; /* clears the fixed header, which is always visible on this page */
}

.work-section {
  min-height: calc(100vh - 3.5rem);
  display: flex;
  align-items: center;
}

.work-layout {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3.5rem;
  width: 100%;
  padding-top: 3rem;
  padding-bottom: 3rem;
}

/* List — vertically centered on the page via the flex row below (desktop),
   right-aligned; plain words, grayed out until hovered, clicked, or reached
   by the auto-cycle. Swap the inner <button> for an <a href="..."> once
   each project has a real page to link to.

   Each item is a flex row (dot, then text) with a fixed 3rem gap, right-
   aligned as a group — so the dot always sits ~3rem left of that item's
   own leftmost letter no matter how long its text is, with zero fixed
   offset to keep in sync. Because text length varies, dots don't land on
   one shared x — the SVG line in works.js draws an actual point-to-point
   segment between each pair of dots (their real, measured positions), so
   it reads as connecting them rather than a straight fixed-x rail. */
.work-list-wrap {
  position: relative;
  width: 100%;
}

.work-list__indicator {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}

.work-list__indicator line {
  stroke: var(--foreground);
  stroke-width: 1px;
}

.work-list {
  display: flex;
  flex-direction: column;
  gap: 2.5rem;
  width: 100%;
  list-style: none;
  margin: 0;
  padding: 0;
}

.work-item {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 3rem;
  cursor: pointer;
}

/* Fixed-size hit box — its center is what works.js measures and draws the
   line to. It never changes size itself (so that center point never moves
   as an item activates), it just centers .work-item__dot-core, which is
   the part that actually grows/shrinks and changes color. */
.work-item__dot {
  position: relative;
  flex-shrink: 0;
  width: 10px;
  height: 10px;
}

.work-item__dot-core {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--muted-foreground-light);
  opacity: 0.55;
  transform: translate(-50%, -50%);
  transition: width 0.35s ease, height 0.35s ease, background 0.35s ease, opacity 0.35s ease;
}

.work-item.is-active .work-item__dot-core {
  width: 10px;
  height: 10px;
  border-radius: 1;
  background: var(--foreground);
  opacity: 1;
}

.work-item__btn {
  display: block;
  width: auto;
  text-align: right;
  white-space: nowrap;
  cursor: pointer;
  font-family: 'Wamaki', serif;
  font-size: 1.375rem;
  letter-spacing: -0.01em;
  color: var(--muted-foreground-light);
  opacity: 0.55;
  transition: opacity 0.35s ease, color 0.35s ease;
}

.work-item:hover .work-item__btn,
.work-item.is-active .work-item__btn {
  opacity: 1;
  color: var(--foreground);
}

/* Preview — photo + description, revealed for whichever item is active.
   .work-preview__parallax is the element the mouse-follow effect in
   works.js moves; the crossfades below live one level deeper so the two
   transforms never fight over the same element. */
.work-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
}

/* Stacked (photo, then description) rather than side-by-side, with the
   description right-aligned so its edge lines up with the photo's own
   right border — align-items: flex-end works because this column shrinks
   to its widest child (the stage), so the narrower text block's flush-right
   edge lands exactly on the stage's right edge. */
.work-preview__parallax {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0;
  will-change: transform;
}

/* Height is fixed and width is left to follow each photo's own real
   aspect-ratio (set inline by works.js) — so switching items only ever
   changes the stage's WIDTH, never its height. Height is what the row's
   vertical centering (and therefore the list's position) depends on, so
   keeping it constant is what keeps the list still while still showing
   every photo at its true, uncropped ratio. object-fit: cover below is
   just a safety net for the instant before works.js applies the real
   ratio (or if a photo somehow doesn't load), so a mismatched box never
   shows letterboxing. */
.work-preview__stage {
  position: relative;
  height: min(640px, 70vh);
  width: auto;
  max-width: min(640px, 90vw);
  aspect-ratio: 4 / 5;
  flex-shrink: 0;
  background: var(--border);
}

.work-preview__photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
}

.work-preview__photo.is-active {
  opacity: 1;
}

/* No gap from the stage above (see .work-preview__parallax) — the padding
   here recreates that same buffer as space *inside* the box instead, so
   the text sits where it always did, but the colored box itself now butts
   right up against the photo. data-color is copied onto this element (in
   works.js) from whichever list item is active, same attribute/values the
   works-grid section uses — swatches below are shared with that pattern. */
.work-preview__text {
  position: relative;
  width: 100%; /* matches the stage's current rendered width, not a fixed column */
  min-height: 2rem;
  padding: 0.5rem;
  color: var(--muted-foreground);
  background: transparent;
  transition: background 0.15s ease-out, color 0.15s ease-out;
}

.work-preview__text[data-color="red"] {
  background: #ff3333;
  color: #fff;
}
.work-preview__text[data-color="blue"] {
  background: #3386ff;
  color: #fff;
}
.work-preview__text[data-color="yellow"] {
  background: #ffc033;
  color: #fff;
}
.work-preview__text[data-color="green"] {
  background: #33cc70;
  color: #fff;
}
.work-preview__text[data-color="black"] {
  background: var(--foreground);
  color: var(--background);
}
.work-preview__text[data-color="white"] {
  background: var(--background);
  color: var(--foreground);
}

/* Temporarily forcing every description back to plain white regardless of
   the active item's data-color swatch above — delete this rule to bring
   the swatches back. The #id selector's specificity is what lets it win
   over those .work-preview__text[data-color] rules without touching them. */
#work-preview-text {
  background: var(--background);
  color: var(--foreground);
}

.work-preview__desc {
  position: absolute;
  inset: 0 0 auto auto;
  width: 100%;
  margin: 0;
  text-align: right;
  white-space: nowrap; /* no auto-wrapping — manual <br> only, per content author */
  font-size: 0.8125rem;
  font-weight: 300;
  line-height: 1.6;
  color: inherit;
  opacity: 0;
  transition: opacity 0.15s ease-out;
}

.work-preview__desc.is-active {
  opacity: 1;
}

@media (min-width: 900px) {
  .work-layout {
    flex-direction: row;
    align-items: center;
    justify-content: flex-start;
    gap: clamp(3rem, 8vw, 7rem);
  }

  .work-list-wrap {
    flex: 0 0 auto;
    width: min(300px, 30vw);
  }

  .work-preview {
    flex: 1 1 auto;
  }

  .work-preview__stage {
    height: min(720px, 70vh);
    max-width: min(720px, 60vw);
  }
}

/* ---------- Scattered work field (works2.html) ---------- */
/* Masonry via CSS multi-column: each item is a fixed-width (~5.5rem) tile
   whose height falls out of its own photo's aspect ratio (portrait photos
   read taller, landscape ones shorter), stacked in neat columns instead of
   forced into uniform grid cells. column-fill:balance spreads items evenly
   across columns rather than filling one column before starting the next.
   No longer height-constrained to one viewport — this can scroll. */
.w2-scatter {
  width: 100%;
  padding: 3rem 2rem;
  columns: 5.5rem;
  column-gap: 3rem;
  column-fill: balance;
}

/* design.html only: a single non-wrapping row of 6 items instead of the
   masonry above — display:flex here overrides the multicol properties on
   .w2-scatter (columns only apply to a block/multicol container, not a
   flex one). overflow-x is just a safety net (see the img height formula
   below, which is built to never need it on desktop). */
.w2-scatter--row {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: 3rem;
  overflow-x: auto;
  overflow-y: hidden;
}

.w2-scatter--row .w2-scatter__item {
  flex: 0 0 auto;
  width: auto;
  margin: 0;
  padding: 0;
}

/* Fluid shared height (not a fixed rem) so all 6 items' combined width —
   each one's own width still follows from its own aspect ratio at
   whatever this resolves to — always equals the row's available width
   instead of only fitting it by luck at one viewport size:
     available width = 100vw - .w2-scatter's own 4rem of horizontal
       padding - 5 gaps of 3rem each (15rem) - 1rem safety margin
     height = available width / (sum of all 6 photos' own w/h ratios),
       so height * sum(ratios) again equals that same available width.
   6.02 is that ratio sum for this specific curated set of 6 photos (see
   design.html) — recompute it if that set ever changes. min() just caps
   how big they get on very wide/ultrawide screens; it can only make the
   row narrower than available space, never wider, so it can't reintroduce
   overflow. */
.w2-scatter--row .w2-scatter__item img {
  width: auto;
  height: min(calc((100vw - 20rem) / 6.02), 24rem);
  max-width: none;
}

.w2-scatter__item {
  display: block;
  /* Extends 1.5rem (half the 3rem column-gap) past each side of the photo
     into the gutter, and 3rem below it — all as padding, not margin, so it
     hovers/clicks like part of the item (margin sits outside an element's
     hit area, padding doesn't). The negative margin cancels the extra
     width/padding back out so the photo itself stays exactly where it was
     (same width, same column position) — only the invisible hit area grows.
     Bleeds into the container's own edge padding at the first/last column,
     which is harmless (still empty space there). */
  width: calc(100% + 3rem);
  margin: 0 -1.5rem 0;
  padding: 0 1.5rem 3rem;
  break-inside: avoid;
  border: none;
  background: none;
  cursor: pointer;
  line-height: 0;
}

.w2-scatter__item img {
  display: block;
  width: 100%;
  height: auto;
  /* No aspect-ratio override here — each <img>'s width/height attributes
     in the markup give it its own real photo's ratio. */
  object-fit: cover;
  transition: transform 0.25s ease;
}

.w2-scatter__item:hover img,
.w2-scatter__item:focus-visible img {
  transform: scale(1.05);
}

/* Locks scrolling while the lightbox (below) is open — same mechanism as
   html.is-loading above, kept separate since the two are unrelated. */
html.w2-scroll-lock,
html.w2-scroll-lock body {
  overflow: hidden;
}

/* Keeps the "wamaki" wordmark visible above the lightbox (z-index 9000+)
   while it's open, without bringing the rest of the header — bar
   background, nav links, menu toggle — along with it. */
html.w2-scroll-lock .site-header {
  z-index: 9010;
  background: transparent;
  backdrop-filter: none;
  border-bottom: none;
  pointer-events: none;
}

html.w2-scroll-lock .site-header .brand {
  pointer-events: auto;
}

html.w2-scroll-lock .site-header .nav-desktop,
html.w2-scroll-lock .site-header .menu-toggle {
  display: none;
}

/* ---------- Work lightbox (works2.html) ----------
   --main-top is the TOP edge (not center) of the main photo — every other
   position below (the scroll container, the prev/next slots, the close
   button) is anchored off that one edge plus --main-w/--main-h/--gap, so
   there's a single source of truth for the whole layout.
   --main-w/--main-h are set inline by works2-lightbox.js per work (see
   setMainBoxSize there) so the large photo always shows its own real
   aspect ratio instead of one shared fixed box — the values below are
   only the pre-JS fallback. Since --main-w/--main-h drive the prev/next
   slots' position (below), a transition on their top/left is what makes
   the whole layout glide to match the new photo's shape on navigation.
   State classes, applied by works2-lightbox.js:
     .is-open     root: mounts the lightbox (display none -> block)
     .is-visible  root: fades the scrim + close button in
     .is-landed   root: reveals the main photo (once its grow-in lands)
     .is-revealed on .w2-lightbox__prev / __next / the desc .w2-mask:
                  slides that piece in from behind its own mask, like the
                  hero wordmark on index.html */
.w2-lightbox {
  --main-w: clamp(285px, 31vw, 545px); /* ~30% wider than the original 220/24vw/420 clamp */
  --main-h: calc(var(--main-w) * 0.75);
  --main-top: 20vh;
  --side-h: 4rem;
  --side-w: calc(var(--side-h) * 4 / 3);
  --gap: 2.5rem;
  position: fixed;
  inset: 0;
  z-index: 9000;
  display: none;
}

.w2-lightbox.is-open {
  display: block;
}

.w2-lightbox__scrim {
  position: fixed;
  inset: 0;
  background: var(--background);
  opacity: 0;
  transition: opacity 0.45s ease;
}

.w2-lightbox.is-visible .w2-lightbox__scrim {
  opacity: 1;
}

/* Shown briefly at the cursor by works2-lightbox.js on a single click on
   the scrim (a no-op) — hints that double-click is what actually closes.
   Appears instantly, then fades out and drifts up over ~1s; top/left are
   set inline per-click. */
.w2-lightbox__hint {
  position: fixed;
  top: 0;
  left: 0;
  transform: translate(-50%, -50%) translateY(-10px);
  z-index: 9004;
  margin: 0;
  padding: 0.3em 0.65em;
  font-family: 'DM Sans', sans-serif;
  font-size: 0.75rem;
  font-weight: 500;
  white-space: nowrap;
  color: var(--muted-foreground-light);
  background: var(--background);
  border: none;
  pointer-events: none;
  opacity: 0;
  transition: opacity 1s ease, transform 1s ease;
}

.w2-lightbox__hint.is-visible {
  opacity: 1;
  transform: translate(-50%, -50%) translateY(0);
  transition: none;
}

.w2-lightbox__close {
  position: fixed;
  top: calc(var(--main-top) - 2.75rem);
  left: 50%;
  transform: translateX(-50%);
  width: 2rem;
  height: 2rem;
  padding: 0;
  border: none;
  background: none;
  color: var(--foreground);
  font-family: 'DM Sans', sans-serif;
  font-size: 1.75rem;
  font-weight: 300;
  line-height: 1;
  cursor: pointer;
  z-index: 9003;
  opacity: 0;
  transition: opacity 0.3s ease;
}

.w2-lightbox.is-visible .w2-lightbox__close {
  opacity: 1;
}

/* The button's own box (fixed position/size) always matches exactly the
   PHOTO's rect — unchanged from before adding labels — so the gap math
   above and the FLIP ghost targeting in works2-lightbox.js both still
   just work off getBoundingClientRect() of the button itself. The "next
   work"/"previous work" label and the work name are positioned outside
   that box (overflow: visible here) via the label/name rules below;
   .w2-lightbox__side-mask is the inner element that actually clips the
   photo's slide-in. */
.w2-lightbox__prev,
.w2-lightbox__next {
  position: fixed;
  height: var(--side-h);
  width: var(--side-w);
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
  overflow: visible;
  z-index: 9001;
  /* Deliberately no transition on top/left: main-w/main-h changing (a new
     work's own aspect ratio) moves these calc()'d values, and the slot
     should just land at the new, correct spot immediately — same as on
     open (works2-lightbox.js resizes via instantlyMany() there too). */
}

/* Both slots are clamped to always stay fully on screen — never purely
   reliant on the main photo's size. Vertical works can run up to ~90vh tall
   and horizontal ones up to ~50vw wide (see computeMainBoxSize() in
   works2-lightbox.js), so without these clamps an extreme one would push a
   slot off the top/bottom or side of the viewport, permanently out of
   reach. */
.w2-lightbox__prev {
  top: max(calc(var(--main-top) - var(--gap) - var(--side-h) + 0.5rem), 0.5rem);
  left: max(calc(50% - (var(--main-w) / 2) - var(--gap) - var(--side-w)), 0.5rem);
}

.w2-lightbox__next {
  /* The -4rem (not just -1rem) leaves room below the button's own box for
     its .side-name label (position:absolute, sits below via top:100%+0.2rem)
     plus a comfortable margin — without it, a tall enough photo clamps this
     right at the viewport edge and the label renders off-screen, clipped. */
  top: min(calc(var(--main-top) + var(--main-h) + var(--gap)), calc(100vh - var(--side-h) - 4rem));
  left: min(calc(50% + (var(--main-w) / 2) + var(--gap)), calc(100vw - var(--side-w) - 0.5rem));
}

.w2-lightbox__side-mask {
  display: block;
  position: absolute;
  inset: 0;
  overflow: hidden;
}

.w2-lightbox__prev img,
.w2-lightbox__next img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 1;
  transform: translateY(100%);
  transition: transform 0.8s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.3s ease;
}

.w2-lightbox__prev.is-revealed img,
.w2-lightbox__next.is-revealed img {
  transform: translateY(0);
}

/* Used only mid-navigation (works2-lightbox.js), for whichever side is
   about to disappear entirely rather than move to a new slot. */
.w2-lightbox__prev.is-fading img,
.w2-lightbox__next.is-fading img {
  opacity: 0;
}

.w2-lightbox__side-label {
  position: absolute;
  bottom: calc(100% + 0.2rem);
  left: 0;
  right: 0;
  margin: 0;
  font-family: 'DM Sans', sans-serif;
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--foreground);
  white-space: nowrap;
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 0.5s ease, transform 0.5s ease;
}

.w2-lightbox__side-name {
  position: absolute;
  top: calc(100% + 0.2rem);
  left: 0;
  right: 0;
  margin: 0;
  font-family: 'DM Sans', sans-serif;
  font-size: 0.6875rem;
  font-weight: 300;
  color: var(--muted-foreground);
  white-space: nowrap;
  opacity: 0;
  transform: translateY(-6px);
  transition: opacity 0.5s ease, transform 0.5s ease;
}

.w2-lightbox__prev.is-revealed .w2-lightbox__side-label,
.w2-lightbox__prev.is-revealed .w2-lightbox__side-name,
.w2-lightbox__next.is-revealed .w2-lightbox__side-label,
.w2-lightbox__next.is-revealed .w2-lightbox__side-name {
  opacity: 1;
  transform: translateY(0);
}

/* Wins over is-revealed above (same specificity, declared later) while a
   side is fading out during navigation. */
.w2-lightbox__prev.is-fading .w2-lightbox__side-label,
.w2-lightbox__prev.is-fading .w2-lightbox__side-name,
.w2-lightbox__next.is-fading .w2-lightbox__side-label,
.w2-lightbox__next.is-fading .w2-lightbox__side-name {
  opacity: 0;
  transition: opacity 0.3s ease;
}

.w2-lightbox__prev .w2-lightbox__side-label,
.w2-lightbox__prev .w2-lightbox__side-name {
  text-align: left;
}

.w2-lightbox__next .w2-lightbox__side-label,
.w2-lightbox__next .w2-lightbox__side-name {
  text-align: right;
}

.w2-lightbox__scroll {
  position: fixed;
  top: var(--main-top);
  left: 50%;
  transform: translateX(-50%);
  width: var(--main-w);
  max-height: calc(100vh - var(--main-top) - 2rem);
  overflow-y: auto;
  overflow-x: hidden;
  z-index: 9002;
  scrollbar-width: none; /* Firefox */
  -ms-overflow-style: none; /* old Edge/IE */
}

.w2-lightbox__scroll::-webkit-scrollbar {
  display: none; /* Chrome/Safari/new Edge */
}

/* Bottom scroll-hint: a white fade over the last ~2rem of the scroll
   viewport, implying there's more below. Last child of .w2-lightbox__scroll
   so its sticky top-of-stack position naturally lands at bottom:0 of the
   scroll viewport; height:0 + the ::before below (drawn upward from that
   same bottom edge) keeps it from adding any extra scrollable height of
   its own. Shown/hidden by works2-lightbox.js toggling .is-visible: only
   when the photo is actually tall enough to scroll, not once already
   scrolled to the bottom (nothing left below to hint at), and not before
   the photo's own landing reveal — opacity transition is what makes both
   the initial appearance and any later show/hide a fade, never a snap. */
.w2-lightbox__scroll-fade {
  position: sticky;
  bottom: 0;
  left: 0;
  right: 0;
  height: 0;
  overflow: visible;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.4s ease;
}

.w2-lightbox__scroll-fade::before {
  content: "";
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 2rem;
  background: linear-gradient(to top, var(--background), transparent);
}

.w2-lightbox__scroll-fade.is-visible {
  opacity: 1;
}

.w2-lightbox__photo {
  display: block;
  width: 100%;
  height: var(--main-h);
  /* contain, not cover: --main-w/--main-h are computed by
     setMainBoxSize() in works2-lightbox.js to already match this work's
     real aspect ratio, so the photo shows in its entirety — contain is
     just insurance against sub-pixel rounding in that calculation. */
  object-fit: contain;
  opacity: 0;
  transition: opacity 0.2s ease;
}

.w2-lightbox.is-landed .w2-lightbox__photo {
  opacity: 1;
}

/* Generic reveal mask (same technique as .hero__clip / .hero__text) — an
   overflow:hidden box with an inner element that slides up from fully
   below into place. Reused here for the description text. */
.w2-mask {
  overflow: hidden;
}

.w2-mask__inner {
  opacity: 1;
  transform: translateY(100%);
  transition: transform 0.8s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.25s ease;
}

.w2-mask.is-revealed .w2-mask__inner {
  transform: translateY(0);
}

/* Used only mid-navigation, to fade the description out in place before its
   content is swapped for the next/previous work's — see is-fading above. */
.w2-mask.is-fading .w2-mask__inner {
  opacity: 0;
}

/* Utility: temporarily kills transitions on an element and everything
   inside it (works2-lightbox.js's instantlyDeep()) — for suppressing a
   descendant's own transition (e.g. .w2-lightbox__side-label's opacity)
   when the element being mutated isn't the one the transition lives on,
   unlike instantly()/instantlyMany() which only suppress the mutated
   element's own transition. */
.w2-no-transition,
.w2-no-transition * {
  transition: none !important;
}

.w2-lightbox__eyebrow {
  margin: 1rem 0 0.35rem;
  font-family: 'Wamaki', serif;
  font-size: 1rem;
  color: var(--foreground);
}

.w2-lightbox__desc-text {
  margin: 0 0 2rem;
  font-size: 0.8125rem;
  font-weight: 300;
  line-height: 1.6;
  color: var(--muted-foreground);
}

.w2-lightbox__desc-text p {
  margin: 0 0 1em;
}

.w2-lightbox__desc-text p:last-child {
  margin-bottom: 0;
}

/* Ghosts (.w2-flip-ghost, below) fly as children of this — not <body>
   directly — so a ghost growing into/out of a tall vertical photo is
   clipped at the same bottom boundary .w2-lightbox__scroll's own overflow
   uses (100vh - 2rem, see there — --main-top always cancels out of that
   sum) instead of freely showing more of it than the real element ever
   will and popping shorter the instant it hands off. Full viewport width
   so it never clips a ghost that's currently over by the side prev/next
   slots (which stay well clear of this bottom line — see their own
   viewport clamps) — only the bottom edge is constrained.
   The transform is load-bearing, not decorative: overflow:hidden on a
   plain (non-transformed) element does NOT clip a position:fixed
   descendant — the fixed element's containing block stays the viewport,
   escaping the clip entirely. transform: translateZ(0) is a no-op visually
   here (this element already sits at the viewport's own origin) but makes
   it establish a containing block for fixed descendants, which is what
   actually makes the clip apply to the ghost. */
.w2-flip-ghost-clip {
  position: fixed;
  inset: 0 0 auto 0;
  height: calc(100vh - 2rem);
  overflow: hidden;
  pointer-events: none;
  z-index: 9005;
  transform: translateZ(0);
}

/* Flying clone used only during the grow/shrink transitions themselves
   (works2-lightbox.js creates and removes these) — never present at rest. */
.w2-flip-ghost {
  position: fixed;
  object-fit: cover;
  pointer-events: none;
  transition: top 0.65s cubic-bezier(0.76, 0, 0.24, 1), left 0.65s cubic-bezier(0.76, 0, 0.24, 1),
    width 0.65s cubic-bezier(0.76, 0, 0.24, 1), height 0.65s cubic-bezier(0.76, 0, 0.24, 1);
}

/* ---------- Not found (404.html) ---------- */
.not-found {
  min-height: calc(100vh - 3.5rem); /* .work-main above already clears the fixed header */
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: 4rem 2rem 2rem;
}

.not-found__content {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.5rem;
  max-width: 32rem;
}

.not-found__code {
  font-size: min(20vw, 14rem);
  line-height: 1;
}

.not-found__text {
  font-size: 0.9375rem;
  line-height: 1.6;
  color: var(--muted-foreground);
}

/* margin-top:auto pins the illustration to the bottom of .not-found
   regardless of how much text sits above it, keeping the 404 itself in the
   upper-middle of the screen instead of dead-centered. */
.not-found__image {
  width: min(100%, 40rem);
  margin-top: auto;
  padding-top: 4rem;
}
