:root {
  --spacing-unit: 12px;
  --black: #111;
  --white: #fff;
  --header-height: 41px;

  /* Homepage-only theme surface — deliberately separate from --black/--white
     above, which the project overlay pages use as absolute constants (plus
     each project's own inline color). Redefining --black/--white directly
     for dark mode would leak into the overlay; these are scoped so it can't.
     Colors match portorocha.com's own dark mode exactly, per request. */
  --page-bg: var(--white);
  --page-text: var(--black);
  --page-text-muted: rgba(17, 17, 17, 0.5);
  --page-surface: rgba(0, 0, 0, 0.04);
  --toggle-track: rgba(0, 0, 0, 0.08);
  --toggle-track-hover: rgba(0, 0, 0, 0.12);
}

body.dark-mode {
  --page-bg: #000;
  --page-text: #e2e6e3;
  --page-text-muted: rgba(226, 230, 227, 0.5);
  --page-surface: rgba(255, 255, 255, 0.1);
  --toggle-track: rgba(255, 255, 255, 0.12);
  --toggle-track-hover: rgba(255, 255, 255, 0.24);
}

/* Medium mapped to 400 (and Bold to 700) so every existing font-weight:400
   rule on the site picks up Monument Grotesk with no other CSS changes.
   The metric overrides below matter: this font's native ascent/descent are
   over 2x the system font stack's, which without correction pushes every
   line box (and anything vertically centered against it) visibly downward. */
@font-face {
  font-family: "Monument Grotesk";
  src: url("fonts/MonumentGrotesk-Medium.woff") format("woff");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  ascent-override: 96.5%;
  descent-override: 21%;
  line-gap-override: 0%;
}

@font-face {
  font-family: "Monument Grotesk";
  src: url("fonts/MonumentGrotesk-MediumItalic.woff") format("woff");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
  ascent-override: 96.5%;
  descent-override: 21%;
  line-gap-override: 0%;
}

@font-face {
  font-family: "Monument Grotesk";
  src: url("fonts/MonumentGrotesk-Bold.woff") format("woff");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  ascent-override: 96.5%;
  descent-override: 21%;
  line-gap-override: 0%;
}

@font-face {
  font-family: "Monument Grotesk";
  src: url("fonts/MonumentGrotesk-BoldItalic.woff") format("woff");
  font-weight: 700;
  font-style: italic;
  font-display: swap;
  ascent-override: 96.5%;
  descent-override: 21%;
  line-gap-override: 0%;
}

* {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
  overflow-anchor: none;
}

body {
  margin: 0;
  background: var(--page-bg);
  color: var(--page-text);
  font-family: "Monument Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  line-height: 1.3;
  transition: background-color 0.3s ease, color 0.3s ease;
}

a {
  color: inherit;
}

.page {
  padding: var(--spacing-unit);
  padding-top: calc(var(--header-height) + var(--spacing-unit));
}

/* ---------- header row ---------- */

/* Header row and info panel share ONE frosted-glass surface (.nav-shell)
   instead of each having their own backdrop-filter — two adjacent blurred
   boxes each sample a different slice of the scrolled content behind them,
   which reads as a visible seam. One shared box blurs one continuous slice,
   so it grows/shrinks as a single smooth unit with no seam. */
.nav-shell {
  position: fixed;
  top: var(--spacing-unit);
  left: var(--spacing-unit);
  right: var(--spacing-unit);
  z-index: 110;
  background: var(--page-surface);
  backdrop-filter: blur(50px);
  -webkit-backdrop-filter: blur(50px);
  border-radius: 6px;
  overflow: hidden;
  transition: transform 0.3s ease, background-color 0.3s ease;
}

/* Hidden on scroll-down past the first row, revealed again on any scroll-up
   — the reveal check happens continuously, not just near the page top. */
.nav-shell--hidden {
  transform: translateY(-150%);
}

.nav-row {
  box-sizing: border-box;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: var(--spacing-unit);
}

.nav-row-right {
  display: flex;
  align-items: center;
  gap: var(--spacing-unit);
}

.home-link {
  font-size: 13px;
  font-weight: 400;
  color: var(--page-text);
  text-decoration: none;
}

/* Circle + icon sizing/colors matched to portorocha.com/all's own close
   button — same background/hover as .dark-mode-toggle (not a coincidence:
   portorocha uses those exact rgba values for both components), 24px tall
   to match the toggle's own height. */
.info-trigger {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--toggle-track);
  border: none;
  padding: 0;
  color: var(--page-text);
  cursor: pointer;
  transition: background-color 0.3s ease;
}

.info-trigger:hover {
  background: var(--toggle-track-hover);
}

/* Homepage-only dark mode toggle — mechanics and colors matched to
   portorocha.com's own switch (track/knob background + travel distance). */
.dark-mode-toggle {
  flex-shrink: 0;
  display: block;
  width: 40px;
  height: 24px;
  padding: 2px;
  border: none;
  border-radius: 24px;
  background: var(--toggle-track);
  cursor: pointer;
  transition: background-color 0.3s ease;
}

.dark-mode-toggle:hover {
  background: var(--toggle-track-hover);
}

.dark-mode-toggle-knob {
  display: block;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  /* Tied to the same color as the +/x icon (--page-text) rather than its
     own separate value, so the two always stay consistent. */
  background: var(--page-text);
  transition: transform 0.3s ease, background-color 0.3s ease;
}

body.dark-mode .dark-mode-toggle-knob {
  transform: translateX(16px);
}

.plus-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
  transition: transform 0.2s ease;
}

[aria-expanded="true"] .plus-icon {
  transform: rotate(45deg);
}

/* ---------- info panel ---------- */

.info-panel {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.35s ease;
}

.info-panel-inner {
  padding: 0 var(--spacing-unit) var(--spacing-unit);
}

/* Thumbnail strip at the bottom of the info panel. No autoplay — the
   track is wider than its container and is scrubbed directly by cursor
   x-position (see setupInfoMarqueeMagnify). The item nearest the cursor
   grows in height (width follows automatically, preserving its natural
   aspect ratio — never distorted/cropped); every other item shrinks by a
   small fraction to compensate, so the gap between items never changes. */
.info-marquee {
  overflow: hidden;
  padding: 0 var(--spacing-unit) var(--spacing-unit);
  cursor: pointer;
}

/* Fixed height (set in JS to match the max grown size) so an item growing
   taller never resizes this flex container itself. align-items: flex-start
   (not flex-end) so the resting thumbnails sit flush against the top of
   that fixed box — right below the bio photo, matching the panel's normal
   spacing-unit gap everywhere else — and hovering grows an item DOWNWARD
   into the reserved extra height instead of upward away from the photo;
   flex-end would leave that reserved height as visible empty space above
   the resting row instead. */
.info-marquee-track {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  width: max-content;
}

.info-marquee-item {
  flex: 0 0 auto;
  display: block;
  height: 56px;
  width: auto;
  transition: height 0.15s ease;
}

.info-marquee-media {
  display: block;
  height: 100%;
  width: auto;
  border-radius: 4px;
}

.info-columns {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  column-gap: var(--spacing-unit);
}

.info-title {
  font-size: 13px;
  margin: 0;
}

.info-photo {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 6px;
}

.info-contact {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0;
  margin-top: var(--spacing-unit);
}

.info-contact a {
  font-size: 13px;
  text-decoration: none;
}

.info-group {
  margin-bottom: var(--spacing-unit);
}

.info-group:last-child {
  margin-bottom: 0;
}

.info-group-label {
  font-size: 13px;
  margin: 0;
}

.info-entry {
  margin-bottom: 8px;
}

.info-entry:last-child {
  margin-bottom: 0;
}

.info-entry-org {
  font-size: 13px;
  margin: 0;
}

.info-entry-role,
.info-entry-years {
  font-size: 13px;
  margin: 0;
}

/* ---------- project grid ---------- */

.project-grid {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-unit);
}

.project-row {
  display: flex;
  align-items: flex-start;
  gap: var(--spacing-unit);
}

.project-item {
  display: block;
  flex: 1 1 0%;
  min-width: 0;
  text-decoration: none;
  color: inherit;
  transition: flex-grow 0.35s ease;
}

.thumb {
  display: block;
  width: 100%;
  height: auto;
  margin-bottom: var(--spacing-unit);
  border-radius: 6px;
  overflow: hidden;
  background-size: cover;
  background-position: center;
}

.thumb-square { aspect-ratio: 1 / 1; }
.thumb-16-9   { aspect-ratio: 16 / 9; }
.thumb-4-3    { aspect-ratio: 4 / 3; }

.caption-title {
  font-size: 13px;
}

.caption-desc {
  font-size: 13px;
  color: var(--page-text-muted);
}

@media (max-width: 640px) {
  /* Bio+contact and Currently/Previously stay side by side as two columns
     even on mobile — only the photo (3rd column) drops to its own full-width
     row below, spanning both via grid-column, rather than every column
     stacking into one on top of the other. */
  .info-columns {
    grid-template-columns: 1fr 1fr;
  }

  .info-col:nth-child(3) {
    grid-column: 1 / -1;
    margin-top: calc(var(--spacing-unit) * 2);
  }

  .info-marquee {
    display: none;
  }
}

/* ---------- project overlay ---------- */

.project-overlay {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  top: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  padding-top: 0;
  transform: translateY(100%);
  transition: transform 0.35s ease, padding-top 0.3s ease;
  z-index: 100;
}

/* Reserves room at the top for the main header when a scroll-up gesture
   brings it back over the (now full-height) project page — the gallery
   below genuinely shrinks to make room rather than being covered. */
.project-overlay.header-visible {
  padding-top: var(--header-height);
}

.project-overlay.is-open {
  transform: translateY(0);
}

.project-overlay-head {
  flex-shrink: 0;
  box-sizing: border-box;
  /* position: relative anchors the subtitle below, which is pulled out of
     this row entirely (position: absolute) and centered on this box's own
     midpoint — see .project-overlay-subtitle for why: a grid/flex layout
     with title and close as siblings can only center the subtitle between
     their own edges, which drifts off true-center whenever their widths
     differ from each other (title is rarely the same length as "Close").
     Taking it out of flow decouples its position from their asymmetry
     entirely. */
  position: relative;
  display: flex;
  justify-content: space-between;
  align-items: center;
  /* --header-height is the floating homepage header's distance from the
     viewport top (its own gap + height) — right for reserving space when
     that header slides back over a project (.header-visible below), wrong
     as this bar's own height: this bar is flush to the viewport, a totally
     different shape, and borrowing that value made it far taller than a
     single line of text needs, stealing space the gallery could use.
     Padding (not an explicit height) sizes the box to its content, and
     calc(spacing-unit * 2) on all sides both lands the title/close at the
     same x-position as "Kitron Neuschatz" and the "+", AND puts the title
     the same ~27px from the true viewport top as "Kitron Neuschatz" sits
     from the top of the homepage — the gallery's own gap below is a
     separate, independently-tuned value (see .project-overlay-gallery). */
  padding: calc(var(--spacing-unit) * 2);
}

.project-overlay-title {
  /* flex-grow: 0 (not 1) deliberately — this box must hug its own text
     width, not stretch to fill the row, since updateOverlaySubtitlePosition()
     in script.js reads this element's actual rendered right edge to figure
     out how much room the (absolutely positioned, out-of-flow) subtitle can
     use without overlapping it. A grown/stretched box would report a right
     edge way past the real text, starving the subtitle of a measured
     max-width down to nothing. */
  flex: 0 1 auto;
  font-size: 13px;
  font-weight: 400;
  color: var(--overlay-text-color, var(--page-text));
  margin: 0;
  /* Never wraps to a second line, however tight it gets — allowed to
     shrink/ellipsize itself only in the extreme case where it and "Close"
     alone (the subtitle no longer competes for this row's space at all)
     can't both fit. */
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.project-overlay-subtitle {
  /* Centered on .project-overlay-head's own midpoint, independent of
     title/close's widths — see that rule's comment. max-width is set
     inline by updateOverlaySubtitlePosition() in script.js: the widest
     symmetric band around this center point that still clears both
     title's and close's actual rendered edges (recomputed on open/resize,
     since title's width varies per project and close never moves). */
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  font-size: 13px;
  font-weight: 400;
  color: var(--overlay-text-color, var(--page-text));
  margin: 0;
  overflow: hidden;
}

.project-overlay-subtitle-track {
  display: inline-flex;
  white-space: nowrap;
}

/* Static (non-marquee) state: a single centered, ellipsis-truncated copy —
   see updateOverlaySubtitleMarquee() for when/why the second copy and the
   scrolling animation turn on instead. */
.project-overlay-subtitle-copy:first-child {
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}

.project-overlay-subtitle-copy + .project-overlay-subtitle-copy {
  display: none;
  padding-left: var(--overlay-subtitle-gap, 0px);
}

.project-overlay-subtitle.is-marquee {
  text-align: left;
}

.project-overlay-subtitle.is-marquee .project-overlay-subtitle-copy:first-child {
  overflow: visible;
  text-overflow: clip;
}

.project-overlay-subtitle.is-marquee .project-overlay-subtitle-copy + .project-overlay-subtitle-copy {
  display: block;
}

.project-overlay-subtitle.is-marquee .project-overlay-subtitle-track {
  animation: overlay-subtitle-marquee var(--overlay-subtitle-duration, 12s) linear infinite;
}

@keyframes overlay-subtitle-marquee {
  from { transform: translateX(0); }
  to { transform: translateX(calc(-1 * var(--overlay-subtitle-distance, 0px))); }
}

.project-overlay-close {
  flex-shrink: 0;
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  font-size: 13px;
  font-weight: 400;
  color: var(--overlay-text-color, var(--page-text));
  cursor: pointer;
}

.project-overlay-gallery {
  position: relative;
  flex: 1;
  min-height: 0;
  display: flex;
  align-items: center;
  overflow: hidden;
  /* Explicit, guaranteed bottom breathing room before the thumb strip —
     .gallery-slide-img fills 100% of whatever height is left, so without
     this an image could end up touching the thumb strip depending on
     aspect ratio and viewport. No top padding here: .project-overlay-head
     already has its own 24px bottom padding, and this padding would stack
     on top of that rather than replace it, making the gap below the title
     50% bigger than the gap above it. */
  padding-bottom: var(--spacing-unit);
  /* The gallery-hover-label is the cursor now — the native pointer would
     just be a redundant second indicator sitting on top of it. */
  cursor: none;
}

/* Follows the cursor across the whole gallery — see setupGalleryHoverLabel().
   mix-blend-mode: difference against a white color (rather than reading
   --overlay-text-color) is what makes it invert and stay legible over any
   image/background it happens to be sitting on, matching the reference
   site's cursor label. Centered on the cursor via transform, and never
   intercepts the click that's meant for whatever's underneath it. */
.gallery-hover-label {
  position: absolute;
  top: 0;
  left: 0;
  transform: translate(-50%, -50%);
  font-size: 13px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: #fff;
  mix-blend-mode: difference;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.15s ease;
}

.gallery-hover-label.is-visible {
  opacity: 1;
}

.gallery-track {
  display: flex;
  align-items: center;
  gap: var(--spacing-unit);
  height: 100%;
  width: 100%;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  /* 50% side padding guarantees any slide — regardless of its own width —
     can reach a fully centered scroll position, even as the first/last
     slide. Anything less clamps at the scroll boundary for narrow slides. */
  padding: 0 50%;
  scrollbar-width: none;
}

.gallery-track::-webkit-scrollbar {
  display: none;
}

.gallery-slide {
  flex: 0 0 auto;
  height: 100%;
  max-width: 70vw;
  aspect-ratio: 16 / 9;
  border-radius: 6px;
  scroll-snap-align: center;
  background-size: cover;
  background-position: center;
  cursor: none;
}

/* Every slide — real image or video, any native resolution — targets the
   SAME frame height, min(100%, 75vw): height is a resolved value (not
   auto), so a replaced element with an intrinsic ratio scales its width to
   match it automatically, in EITHER direction — a small/low-res source
   scales up to reach it just as readily as a large one scales down, which
   max-width/max-height (the previous approach) never did: those only ever
   shrink, so anything already smaller than the cap just sat at its native
   pixel size instead of filling the frame like its neighbors. max-width
   still exists to stop extreme panoramic shots from overflowing narrow
   mobile viewports, but since height no longer shrinks to match when it
   triggers, object-fit: contain is back to letterbox the visible content
   within the (uniformly tall) box for that one case — a rare tradeoff
   against the common case of every slide actually reaching a consistent
   height. */
.gallery-slide-img {
  flex: 0 0 auto;
  display: block;
  width: auto;
  height: min(100%, 75vw);
  max-width: calc(100vw - var(--spacing-unit) * 4);
  /* Before a video's metadata has loaded (real, non-local network
     conditions can leave this pending for a moment, especially for the
     peeking/inactive neighbor slides, which only preload="metadata" rather
     than eagerly loading), it has no known intrinsic ratio yet — width:
     auto has nothing to scale proportionally from in that window, so
     without this it can default to some indeterminate/oversized box
     instead of respecting the frame height. "auto 16/9" prefers the real
     intrinsic ratio the instant it's known, but gives it 16/9 as a sane
     ratio to size against meanwhile. */
  aspect-ratio: auto 16 / 9;
  border-radius: 6px;
  scroll-snap-align: center;
  object-fit: contain;
  cursor: none;
}

.project-overlay-thumbs {
  flex-shrink: 0;
  display: flex;
  /* Fixed height (matches the active/grown item size) so the active thumb
     growing taller never resizes this row itself — see .info-marquee-track
     for why that's what causes the whole row to visibly shift. Bottom
     anchored so growth only ever extends upward — gap/spacing between
     items and the row's own position never change.
     Everything here uses box-sizing: border-box (global rule), so height
     has to account for the padding too — 56px alone was swallowing the
     12px top/bottom padding into the box, leaving only 32px of real
     content room and clipping the tops of every thumbnail. */
  align-items: flex-end;
  justify-content: center;
  gap: 8px;
  height: calc(56px + var(--spacing-unit) * 2);
  padding: var(--spacing-unit);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

.overlay-thumb {
  flex: 0 0 64px;
  height: 44px;
  border-radius: 4px;
  border: none;
  padding: 0;
  cursor: pointer;
  background-size: cover;
  background-position: center;
  transition: height 0.15s ease;
}

/* real images: fixed strip height, natural width, never cropped */
.overlay-thumb.has-image {
  flex: 0 0 auto;
  background: none;
}

.overlay-thumb-img {
  display: block;
  height: 100%;
  width: auto;
  border-radius: 4px;
}

/* ---------- project overlay prev/next nav ---------- */
/* Fixed to the true viewport (siblings of .project-overlay, not descendants)
   so the hover preview isn't clipped by the overlay's own overflow:hidden or
   re-parented into its containing block by its transform. */

/* A modest fixed-size hit zone in the corner (not just the tiny arrow
   glyph itself), so hovering "the corner area" in general is enough to
   trigger both the preview and the bounce — the arrow stays pinned to the
   exact same spot it always sat at via absolute positioning inside this
   now-larger box, so its own visual position is unchanged. */
.overlay-nav {
  position: fixed;
  bottom: 0;
  z-index: 120;
  display: none;
  width: 120px;
  height: 120px;
  text-decoration: none;
  color: var(--page-text);
}

.project-overlay.is-open ~ .overlay-nav {
  display: block;
}

.overlay-nav-prev { left: 0; }
.overlay-nav-next { right: 0; }

.overlay-nav-arrow {
  position: absolute;
  /* Matches .project-overlay-head's own padding (calc(spacing-unit * 2)),
     so the arrows sit the same distance from the bottom/side of the
     viewport as the title and Close button do from the top/side. */
  bottom: calc(var(--spacing-unit) * 2);
  display: inline-block;
  font-size: 13px;
  line-height: 1;
}

.overlay-nav-prev .overlay-nav-arrow { left: calc(var(--spacing-unit) * 2); }
.overlay-nav-next .overlay-nav-arrow { right: calc(var(--spacing-unit) * 2); }

.overlay-nav-prev:hover .overlay-nav-arrow {
  animation: overlay-arrow-bounce-left 0.5s ease-in-out infinite;
}

.overlay-nav-next:hover .overlay-nav-arrow {
  animation: overlay-arrow-bounce-right 0.5s ease-in-out infinite;
}

@keyframes overlay-arrow-bounce-left {
  0%, 100% { transform: translateX(0); }
  50% { transform: translateX(6px); }
}

@keyframes overlay-arrow-bounce-right {
  0%, 100% { transform: translateX(0); }
  50% { transform: translateX(-6px); }
}

.overlay-nav-preview {
  position: absolute;
  /* Anchored to the arrow's own position (its bottom inset + its ~13px
     line height), not 100% of .overlay-nav's box — that box is now a fixed
     120px hit zone, not shrink-wrapped to the arrow, so a plain 100%
     would float the preview way higher than intended. */
  bottom: calc(var(--spacing-unit) * 2 + 13px + 2vw);
  z-index: 120;
  display: block;
  width: min(176px, 21.6vw);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
}

.overlay-nav-prev .overlay-nav-preview {
  /* Same inset as the arrow/title/close, instead of flush with the true
     viewport edge. */
  left: calc(var(--spacing-unit) * 2);
  transform-origin: left bottom;
  transform: rotateZ(-20deg);
}

.overlay-nav-next .overlay-nav-preview {
  right: calc(var(--spacing-unit) * 2);
  transform-origin: right bottom;
  transform: rotateZ(20deg);
}

.overlay-nav:hover .overlay-nav-preview {
  opacity: 1;
  transform: rotateZ(0deg);
}

.overlay-nav-preview-media {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 6px;
  background: var(--white);
}

/* ---------- mobile prev/next-project row ---------- */
/* On mobile the fixed corner hover-preview nav (.overlay-nav above) doesn't
   really work at all — it's keyed on :hover, which touch never triggers, so
   the preview never appears — and its 120x120 hit zone overlaps the thumb
   strip besides (a real conflict: that corner box sits at z-index 120,
   above .project-overlay's own 100, so nothing inside the overlay, thumbs
   included, can ever out-stack it and receive the tap instead). Rather than
   patch the fixed corner box to dodge the thumbs, mobile gets its own
   always-visible row: a real flex child inside .project-overlay, directly
   after the thumb strip, with small thumbnail-sized (not the big rotated
   card) previews for the adjacent projects — no hover required, no
   z-index/stacking fight, since it isn't position:fixed at all. */
.project-overlay-mobile-nav {
  display: none;
}

.mobile-nav-link {
  display: flex;
  align-items: center;
  gap: var(--spacing-unit);
  text-decoration: none;
  color: var(--page-text);
  font-size: 13px;
}

.mobile-nav-arrow {
  display: inline-block;
  line-height: 1;
}

/* Same bounce as the desktop arrows (.overlay-nav-*:hover above), just
   always running instead of hover-gated — mobile has no hover state, but
   the arrows are always on screen here (unlike the desktop corner box,
   which only appears on hover in the first place), so a constant bounce is
   the equivalent "this is interactive" cue. */
.mobile-nav-prev .mobile-nav-arrow {
  animation: overlay-arrow-bounce-left 0.5s ease-in-out infinite;
}

.mobile-nav-next .mobile-nav-arrow {
  animation: overlay-arrow-bounce-right 0.5s ease-in-out infinite;
}

/* Fixed height, natural width — same idea as .overlay-thumb-img (the
   gallery's own thumb strip): never force-crop a project's thumbnail into a
   square, just cap how tall it renders and let its own aspect ratio decide
   the width. */
.mobile-nav-thumb {
  display: block;
  flex: 0 0 auto;
  height: 72px;
  border-radius: 6px;
  overflow: hidden;
}

.mobile-nav-thumb-media {
  display: block;
  height: 100%;
  width: auto;
}

@media (max-width: 640px) {
  /* The gallery's own image thumb strip is redundant with swipe-through-
     the-gallery on mobile, and forcing those thumbnails into a small fixed
     square previously distorted their aspect ratio — simplest fix is to
     just not show it here, leaving the actual gallery images (unchanged
     dimensions) as the only preview, plus prev/next-PROJECT arrows below. */
  .project-overlay-thumbs {
    display: none;
  }

  /* Same reasoning as .info-columns above: this is a real flex child of
     .project-overlay (flex-direction: column), so switching it on adds a
     row's worth of height to that column — the gallery above it (flex: 1)
     is what actually shrinks to make room, same as it already did for the
     (now-hidden) thumb strip. */
  .project-overlay-mobile-nav {
    display: flex;
    flex-shrink: 0;
    justify-content: space-between;
    align-items: center;
    padding: 0 calc(var(--spacing-unit) * 2) calc(var(--spacing-unit) * 2);
  }

  /* Matches the base rule's own selector/specificity (.project-overlay.is-open
     ~ .overlay-nav) — a plain .overlay-nav { display: none } here would lose
     to that more specific rule and never actually hide anything while the
     overlay is open, which is the only time this would be visible at all. */
  .project-overlay.is-open ~ .overlay-nav {
    display: none;
  }
}

/* ---------- site footer clock ---------- */

/* Normal document flow (not fixed) — sits at the end of the page's
   content, below the grid, so it's only visible once you scroll all the
   way down. Lives inside .page so it inherits the same side padding as
   the grid, keeping both edges aligned with the thumbnail columns. */
.site-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: var(--spacing-unit);
  background: var(--page-bg);
}

.footer-date,
.footer-time {
  margin: 0;
  font-size: 13px;
  color: var(--page-text);
}

