/* CSS Custom Properties */
/* ============================================
   DESIGN TOKENS
   Surfaces, elevation, radius and motion. Every value below was a hardcoded
   literal repeated across this file (22 hex values, one shadow, four durations).
   ============================================ */
:root {
  color-scheme: light;

  --panel-height: 37vh;
  --expanded-bottom: calc(37vh + 20px);

  /* Metrics — the bespoke type system is untouched */
  --font-ui: 21px;
  --font-nav: 14px;    /* every capsule shares one size */
  --capsule-lh: 18px;  /* line box inside a capsule */
  --capsule-h: 37px;   /* 9px + --capsule-lh + 10px, for square capsules */
  --hit-min: 45px;     /* minimum touch target; extends hit area, not the visual.
                          45 rather than 44 so (--hit-min − --capsule-h) stays
                          even at both breakpoints (8px and 14px) and the
                          halved border lands on whole device pixels. */

  /* Spacing scale */
  --space-xs: 4px;
  --space-sm: 8px;
  --space-md: 16px;
  --space-lg: 24px;
  --space-xl: 48px;
  --space-2xl: 96px;

  /* Tracking is size-specific — a single letter-spacing is wrong somewhere.
     Large text reads too loose as it grows and wants negative tracking; small
     UI labels want a touch positive. Leading moves inversely to size. */
  --track-lg: -0.024em;   /* 21px body and headings */
  --track-sm: -0.004em;   /* 14px running text */
  --track-ui: 0.006em;    /* 14px capsule labels */
  --lead-lg: 1.38;
  --lead-sm: 1.45;

  /* Media corners. Applied to every element that can hold artwork — tile,
     list image, carousel item and FLIP clone — so the radius never pops
     mid-transition. */
  --radius-media: 10px;
  --radius-card: 18px;

  /* Materials */
  --surface-raised-glass: rgba(228, 228, 228, 0.68);
  --surface-card-glass: rgba(255, 255, 255, 0.72);
  --blur-chrome: blur(20px) saturate(180%);
  --blur-card: blur(30px) saturate(180%);

  /* Surfaces */
  --surface-ground: #f3f3f3;   /* page + view containers */
  --surface-chrome: #f3f3f3;   /* top band + bottom bar. Split from ground so
                                  translucency can target chrome alone. */
  --surface-chrome-glass: rgba(243, 243, 243, 0.72);
  --surface-raised: #e4e4e4;   /* every capsule at rest */
  --surface-active: #ffffff;   /* every capsule hovered / open */
  --text-primary: #000000;
  --hairline: rgba(0, 0, 0, 0.06);

  /* Elevation */
  --elev-0: none;
  --elev-1: 0 0 0 1px rgba(0, 0, 0, 0.05), 0 2px 6px rgba(0, 0, 0, 0.10);
  --elev-2: 0 0 0 1px rgba(0, 0, 0, 0.05), 0 12px 40px rgba(0, 0, 0, 0.14);
  /* Bigger surfaces read as thicker: stronger blur, deeper shadow */
  --elev-card: 0 0 0 1px rgba(0, 0, 0, 0.05), 0 4px 24px rgba(0, 0, 0, 0.10);

  /* Radius */
  --radius-pill: 9999px;
  --radius-flat: 0;

  /* Motion. --dur/--ease keep their names so the FLIP signature is unchanged. */
  --ease: cubic-bezier(0.51, 0.00, 0.50, 1.00);
  --ease-out: cubic-bezier(0.25, 0.1, 0.25, 1);
  --dur: 800ms;         /* hero / FLIP — deliberate, kept */
  --dur-fast: 400ms;
  --dur-ui: 300ms;      /* drawers and sheets (Apple response 0.3) */
  --dur-hover: 200ms;
  --dur-press: 100ms;   /* pointer-down feedback */

  /* The nav pill overshoots (0.34, 1.5) — bounce on a hover-driven object.
     Apple reserves overshoot for momentum gestures. Swap this one token back
     to cubic-bezier(0.34, 1.5, 0.5, 1) to restore the original bounce. */
  --ease-pill: cubic-bezier(0.25, 0.1, 0.25, 1);

  --focus-ring: 0 0 0 2px var(--surface-ground), 0 0 0 4px rgba(0, 0, 0, 0.55);
}

/* Dark palette — kept, but NO LONGER APPLIED AUTOMATICALLY.
   The portfolio imagery carries light backdrops baked into the artwork, so an
   auto dark ground made the work read as bright cutouts floating in void. The
   page keeps its original #f3f3f3 ground.
     • To preview dark:  document.documentElement.dataset.theme = 'dark'
     • To restore auto:  swap the selector below back to
                         @media (prefers-color-scheme: dark) { :root { … } }
   Shadows do not read on a dark ground, so elevation becomes a light hairline
   rather than a darker shadow. */
:root[data-theme="dark"] {
  --surface-ground: #1d1d1f;
  --surface-chrome: #1d1d1f;
  --surface-chrome-glass: rgba(29, 29, 31, 0.72);
  --surface-raised: #2c2c2e;
  --surface-active: #48484a;
  --surface-raised-glass: rgba(44, 44, 46, 0.68);
  --surface-card-glass: rgba(58, 58, 60, 0.72);
  --text-primary: #f5f5f7;
  --hairline: rgba(255, 255, 255, 0.12);
  --elev-1: 0 0 0 1px rgba(255, 255, 255, 0.12);
  --elev-2: 0 0 0 1px rgba(255, 255, 255, 0.14), 0 12px 40px rgba(0, 0, 0, 0.55);
  --elev-card: 0 0 0 1px rgba(255, 255, 255, 0.12), 0 4px 24px rgba(0, 0, 0, 0.5);
  --focus-ring: 0 0 0 2px var(--surface-ground), 0 0 0 4px rgba(255, 255, 255, 0.65);
}

/* The toggle sets data-theme explicitly, so color-scheme has to follow it or
   the browser keeps painting scrollbars and form controls for the wrong one. */
:root[data-theme="dark"] { color-scheme: dark; }
:root[data-theme="light"] { color-scheme: light; }

/* Near-solid surfaces with a defined border */
@media (prefers-contrast: more) {
  :root {
    --hairline: rgba(0, 0, 0, 0.35);
    --elev-1: 0 0 0 1px rgba(0, 0, 0, 0.45);
    --surface-chrome-glass: var(--surface-chrome);
    --surface-raised-glass: var(--surface-raised);
    --surface-card-glass: var(--surface-active);
  }
}

/* Custom Font */
@font-face {
  font-family: 'ABC Favorit';
  src: url('fonts/ABCFavorit-Medium.otf') format('opentype');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'ABC Favorit';
  src: url('fonts/ABCFavorit-Light.otf') format('opentype');
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}

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

body {
  width: 100vw;
  height: 100vh;
  overflow: hidden;
  font-family: 'ABC Favorit', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  background: var(--surface-ground);
  color: var(--text-primary);
  transition: background-color var(--dur-ui) var(--ease-out),
              color var(--dur-ui) var(--ease-out);
  touch-action: none;
  /* iOS Safari honours the prefixed form; without it a long press still runs
     a selection across the tiles, and the selection then sticks because
     nothing on the page will clear it. */
  -webkit-user-select: none;
  user-select: none;
  /* Separate property, separate behaviour: this is the long-press callout
     (Copy / Look Up / Share, or Save Image over a picture). */
  -webkit-touch-callout: none;
  overscroll-behavior: none;
  font-size: var(--font-ui);
}

html {
  overflow: hidden;
  overscroll-behavior: none;
  touch-action: none;
}

/* Suppress tap highlight, selection and the long-press callout everywhere.
   Inheriting from body is not enough: the UA stylesheet sets its own
   -webkit-user-select on some elements, images among them. */
*, *::before, *::after {
  -webkit-tap-highlight-color: transparent;
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

/* Artwork must not be draggable or long-pressable — a long press over an
   image otherwise offers Save Image, and a desktop drag lifts a ghost of it
   off a grid that is itself dragged to pan. */
img,
video {
  -webkit-user-drag: none;
  user-drag: none;
  -webkit-touch-callout: none;
}

/* Keyboard focus. Every one of these used to be `outline: none` with no
   replacement, which removed the indicator from the only people who see it.
   The inner ring is the page ground, so the outer ring separates cleanly
   without needing a border.

   Gated on an explicit keyboard flag rather than :focus-visible. The browser
   heuristic is not dependable — Safari matches :focus-visible after a tap on
   a button, which left a dark ring latched around the close button with no
   way to dismiss it. The flag is set only by Tab and cleared by any pointer,
   so the ring follows the keyboard and nothing else. */
.tab-button:focus,
.nav-link:focus,
.detail-close:focus,
.more-info-button:focus,
.project-title:focus,
.theme-toggle:focus,
.tile:focus,
.project-image-item:focus {
  outline: none;
}

html.using-keyboard .tab-button:focus,
html.using-keyboard .nav-link:focus,
html.using-keyboard .detail-close:focus,
html.using-keyboard .more-info-button:focus,
html.using-keyboard .project-title:focus,
html.using-keyboard .theme-toggle:focus {
  outline: none;
  box-shadow: var(--focus-ring);
}

/* Artwork gets an offset ring, so the indicator never sits on the image */
html.using-keyboard .tile:focus,
html.using-keyboard .project-image-item:focus {
  outline: 2px solid var(--text-primary);
  outline-offset: 3px;
}

#grid-container {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  cursor: grab;
  will-change: transform;
  overflow: visible;
  transform-origin: center center;
}

#grid-container.dragging {
  cursor: grabbing;
}

/* Grid effects when zoomed into detail mode */
#grid-container.grid--zoomed {
  pointer-events: none;
}

.tile {
  position: absolute;
  width: 290px;
  height: 220px;
  overflow: hidden;
  will-change: transform;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Only enable tile clicks when grid container is active */
#grid-container.active .tile {
  pointer-events: auto;
}

/* Tile being animated during FLIP */
.tile--animating {
  pointer-events: none;
}

/* Tiles being scattered away */
.tile--scattering {
  transition: transform var(--dur) var(--ease);
}

.tile--scattering.scatter-out {
  transition: transform var(--dur) var(--ease),
              opacity 0.3s var(--ease) 0.5s;
  opacity: 0;
}

.tile img {
  max-width: 100%;
  max-height: 100%;
  display: block;
  object-fit: contain;
  object-position: center;
}

/* Both were cursor:pointer with no visual response — and in list view the
   heading highlighted on hover while the images under it did not. The dim
   sits on the <img>, not the tile, so the FLIP code's opacity writes on
   .tile are unaffected. Opacity rather than scale or shadow: scale would
   fight the FLIP measurement, shadow on artwork breaks deference. */
.tile img,
.project-image-item img {
  transition: opacity var(--dur-hover) var(--ease-out);
}

/* Corners go on every element that can hold artwork — grid tile, list image,
   carousel item, FLIP clone and video — so the radius never changes
   mid-transition. */
.tile img,
.project-image-item img,
.carousel-item img,
.flip-clone img,
video.carousel-video-iframe {
  border-radius: var(--radius-media);
}

@media (hover: hover) {
  #grid-container.active .tile:hover img,
  .project-image-item:hover img {
    opacity: 0.86;
  }
}

/* ============================================
   DETAIL VIEW OVERLAY
   ============================================ */
#detail-view {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100dvh; /* dvh = dynamic viewport height, accounts for mobile URL bar */
  z-index: 1000;
  opacity: 0;
  pointer-events: none;
  background: var(--surface-ground);
  overflow: hidden;
}

#detail-view.active {
  opacity: 1;
  pointer-events: auto;
}

/* Top bar — mirrors the bottom bar, solid background behind × and tabs */
/* Translucent rather than opaque: carousel imagery now passes under the band
   instead of vanishing at a hard edge. This is the ONLY glass surface on the
   site, because it is the only piece of chrome that never transforms. The
   bottom bar stays opaque precisely because it slides — a translating
   backdrop-filter over a preserve-3d scene re-rasterises its backdrop every
   frame, which is the worst compositing case there is. */
#detail-view::before {
  content: '';
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 70px;
  background: var(--surface-chrome);
  z-index: 1003;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
}

@media (prefers-reduced-transparency: reduce) {
  #detail-view::before,
  .nav-group,
  .info-slide {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
  #detail-view::before { background: var(--surface-chrome); }
  .nav-group { background: var(--surface-raised); }
  .info-slide { background: var(--surface-ground); }
}

#detail-view.active::before {
  opacity: 1;
}

.detail-content {
  width: 100%;
  height: 100%;
  position: relative;
  padding: 0;
  overflow: visible;  /* Must be visible for 3D perspective to work */
  perspective: 1200px;
  perspective-origin: center center;
  /* An elastic pinch scale is applied inline here while pinching, then
     cleared. Scaling the element that owns the perspective zooms the
     projected scene without disturbing the carousel's 3D positioning. It is
     deliberately not set by default: any transform would make this a
     containing block for the position:fixed .nav-area children. */
}

/* Round capsule, centred on the nav group's centre line beside it
   (nav: top 16px + 4px padding → centre 38.5px; a --capsule-h box → top 20px) */
.detail-close {
  position: fixed;
  top: 20px;
  left: 24px;
  width: var(--capsule-h);
  height: var(--capsule-h);
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-raised-glass);
  backdrop-filter: var(--blur-chrome);
  -webkit-backdrop-filter: var(--blur-chrome);
  box-shadow: var(--elev-1);
  border-radius: var(--radius-pill);
  padding: 0;
  margin: 0;
  border: none;
  color: var(--text-primary);
  cursor: pointer;
  z-index: 1004;
  transition: transform var(--dur) var(--ease),
              opacity var(--dur) var(--ease),
              background-color var(--dur-hover) var(--ease-out),
              box-shadow var(--dur-hover) var(--ease-out),
              filter var(--dur-press) var(--ease-out);
  transform: translateX(-100px);
  opacity: 0;
  pointer-events: auto;
}

#detail-view.active .detail-close {
  transform: translateX(0);
  opacity: 1;
}

/* Hover is gated behind a real pointer. On touch, :hover latches after a tap
   and leaves the capsule stuck white; touch gets :active instead. */
@media (hover: hover) {
  .detail-close:hover {
    background: var(--surface-active);
    box-shadow: var(--elev-1);
  }
}

/* Feedback on pointer-down, not on release. transform is already spoken for by
   the entrance animation, so the press reads as a brightness dip. */
.detail-close:active {
  background: var(--surface-active);
  box-shadow: var(--elev-1);
  filter: brightness(0.94);
}

.detail-close:focus {
  outline: none;
}

/* A plus rotated 45°, drawn as bars rather than set as a glyph: the × and +
   characters both sit on the font's math axis, which left the icon ~2px below
   the circle's centre. Bars centre on the box exactly, and whole-pixel
   dimensions keep them crisp on non-retina screens.
   Both bars now live on ::before as two gradients, which frees ::after to
   carry the touch target. */
.detail-close::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 16px;
  height: 16px;
  background:
    linear-gradient(currentColor, currentColor) center / 16px 2px no-repeat,
    linear-gradient(currentColor, currentColor) center / 2px 16px no-repeat;
  transform: translate(-50%, -50%) rotate(45deg);
}

/* 44px of reach without changing the 37px visual */
.detail-close::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: max(100%, var(--hit-min));
  height: max(100%, var(--hit-min));
  transform: translate(-50%, -50%);
}


/* ============================================
   3D CAROUSEL
   ============================================ */

/* Carousel container with 3D transforms */
/* NOTE: Must be position:absolute (not fixed) to inherit perspective from #detail-view */
.carousel-container {
  position: absolute;
  width: 100%;
  height: 100%;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  transform-style: preserve-3d;
  pointer-events: none;
}

/* Base carousel item - size set by JavaScript based on viewport */
.carousel-item {
  position: absolute;
  top: 50%;
  left: 50%;
  overflow: hidden;
  transition: transform var(--dur-fast) var(--ease);
  pointer-events: none;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Only enable pointer events when detail view is active */
#detail-view.active .carousel-item {
  pointer-events: auto;
}

.carousel-item img {
  /* width (not just max-width) so small sources — e.g. the 480px grid webp
     shown under a still-loading video — upscale to fill like the FLIP clone */
  width: 100%;
  max-width: 100%;
  height: auto;
  display: block;
  object-fit: contain;
  object-position: center;
}

/* Portrait carousel items - height-constrained instead of width-constrained */
.carousel-item--portrait img {
  max-width: none;
  width: auto;
  max-height: 100%;
  height: 100%;
}

/* Exit animations when switching to Work or Grid view */
.detail-view--exiting-up .carousel-item {
  transition: transform var(--dur) var(--ease);
}

/* Center item exits up, side items exit to sides */
.detail-view--exiting-up .carousel-item[data-position="0"] {
  transform: translate(-50%, -50%) translate3d(0, -150vh, 0) !important;
}

.detail-view--exiting-up .carousel-item[data-position="-1"],
.detail-view--exiting-up .carousel-item[data-position="-2"] {
  transform: translate(-50%, -50%) translate3d(-150vw, 0, 0) !important;
}

.detail-view--exiting-up .carousel-item[data-position="1"],
.detail-view--exiting-up .carousel-item[data-position="2"] {
  transform: translate(-50%, -50%) translate3d(150vw, 0, 0) !important;
}

/* FLIP animation element - temporary clone for smooth transition */
.flip-clone {
  position: fixed;
  pointer-events: none;
  z-index: 999;
  overflow: hidden;
  will-change: transform;
  display: flex;
  align-items: center;
  justify-content: center;
}

.flip-clone img {
  /* width (not just max-width) so small sources — e.g. 480px grid webps on
     video tiles — upscale to fill the clone and match the carousel item */
  width: 100%;
  max-width: 100%;
  height: auto;
  display: block;
  object-fit: contain;
  object-position: center;
}

/* Portrait FLIP clone styling */
.flip-clone.portrait img {
  max-width: none;
  width: auto;
  max-height: 100%;
  height: 100%;
  object-fit: contain;
}

/* Self-hosted <video> overlay: JS sets aspect-ratio from the video's own
   dimensions on loadedmetadata, so with height:auto the element hugs the
   content exactly — no letterbox region for browsers to paint black */
video.carousel-video-iframe {
  object-fit: contain;
  background: transparent;
  height: auto;
}

/* Video iframe overlay on carousel item */
.carousel-video-iframe {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  border: none;
  z-index: 1;
  pointer-events: auto;
  width: 100%;
  height: 100%;
  opacity: 0;
  transition: opacity 0.15s ease-in;
}

/* Navigation click areas */
.nav-area {
  position: fixed;
  top: 0;
  width: 50%;
  height: 100%;
  z-index: 998;
  cursor: pointer;
  user-select: none;
  border: none;
  background: none;
  padding: 0;
  margin: 0;
  font: inherit;
  color: inherit;
}

.nav-area-left {
  left: 0;
  cursor: w-resize;
}

.nav-area-right {
  right: 0;
  cursor: e-resize;
}

/* Accessibility hint */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}

/* ============================================
   BOTTOM BAR: Wrapper for controls + info panel
   Moves as ONE unit for expand/collapse.
   Layout (top to bottom): controls → gap → panel.
   Default: translated down so panel hides below viewport.
   Expanded: translated to 0 so panel is visible.
   ============================================ */
.detail-bottom-bar {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 1001;
  pointer-events: none;
  /* A drawer, so it takes the UI response (300ms) rather than the hero
     duration. Still a CSS transition, so it cannot yet be grabbed and
     reversed mid-slide — that needs the JS spring (see endCarouselDrag for
     the pattern). */
  transition: transform var(--dur-ui) var(--ease-out);
  /* Default: push down by panel height so only controls + gap are in viewport */
  transform: translateY(var(--panel-height));
  background: var(--surface-chrome);
}

.detail-view--expanded .detail-bottom-bar {
  transform: translateY(0);
}

/* Controls row: button, title, counter */
.bottom-bar-controls {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 24px;
  /* Gap = distance from viewport bottom when collapsed (matches top nav) */
  margin-bottom: 16px;
  pointer-events: auto;
}

/* The bottom bar wears the same recessed capsules as the top nav */
.image-counter,
.detail-project-title,
.more-info-button {
  padding: 9px 16px 10px;
  border-radius: var(--radius-pill);
  background: var(--surface-raised);
  color: var(--text-primary);
  border: none;
  font-family: 'ABC Favorit', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  font-size: var(--font-nav);
  font-weight: 500;
  letter-spacing: var(--track-ui);
  line-height: var(--capsule-lh);
  white-space: nowrap;
}

/* Image counter - right side of controls row */
.image-counter {
  pointer-events: none;
  transform: translateX(100px);
  opacity: 0;
  transition: transform var(--dur) var(--ease),
              opacity var(--dur) var(--ease);
  flex-shrink: 0;
  font-variant-numeric: tabular-nums;
}

#detail-view.active .image-counter {
  transform: translateX(0);
  opacity: 1;
}

/* Reduced motion: simpler transitions.
   This block used to cover only #grid-container and .flip-clone, leaving the
   carousel, the bottom bar and both scatter animations moving at full travel. */
@media (prefers-reduced-motion: reduce) {
  #grid-container {
    transition: opacity var(--dur) var(--ease);
  }

  .flip-clone {
    transition: opacity var(--dur) var(--ease) !important;
  }

  .carousel-item,
  .detail-bottom-bar,
  .project-image-item {
    transition: opacity var(--dur-hover) var(--ease-out);
  }

  .tile--scattering,
  .list-item--scattering,
  .tile--scattering.scatter-out,
  .list-item--scattering.scatter-out {
    transition: opacity var(--dur-hover) var(--ease-out);
  }

  /* Keep the state change, drop the travel */
  .tab-button:active,
  .nav-link:active {
    transform: none;
  }

  .more-info-icon {
    transition: none;
  }

  .detail-info-content {
    transform: none;
    filter: none;
    transition: opacity var(--dur-hover) var(--ease-out);
  }
}

/* ============================================
   MORE INFO FEATURE STYLES
   ============================================ */

/* Project title - centred capsule, hugging its text */
.detail-project-title {
  position: absolute;
  left: 50%;
  bottom: 0;
  margin: 0;
  /* Keep clear of the two flanking capsules */
  max-width: calc(100% - 420px);
  transform: translateX(-50%) translateY(100px);
  transition: transform var(--dur) var(--ease),
              opacity var(--dur) var(--ease);
  opacity: 0;
  text-align: center;
  box-sizing: border-box;
  overflow: hidden;
  text-overflow: ellipsis;
  hyphens: none;
  pointer-events: none;
}

#detail-view.active .detail-project-title {
  transform: translateX(-50%) translateY(0);
  opacity: 1;
}

/* Narrow screens: centring reserves the same width on both sides, but the
   flanking capsules aren't the same width, so a centred title has to be far
   narrower than the space that actually exists. Below here it sits in the
   flow and takes what's left. */
@media (max-width: 685px) {
  .detail-project-title {
    position: static;
    max-width: none;
    min-width: 0;  /* let it shrink (and ellipsis) as a flex item */
    margin: 0 8px;
    transform: translateY(100px);
  }

  #detail-view.active .detail-project-title {
    transform: translateY(0);
  }
}

/* More Info button - left side of controls row */
.more-info-button {
  position: relative;
  transform: translateX(-100px);
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 8px;
  transition: transform var(--dur) var(--ease),
              opacity var(--dur) var(--ease),
              background-color var(--dur-hover) var(--ease-out),
              box-shadow var(--dur-hover) var(--ease-out),
              filter var(--dur-press) var(--ease-out);
  opacity: 0;
  flex-shrink: 0;
}

/* 44px of vertical reach without changing the capsule's visual height */
.more-info-button::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  height: max(100%, var(--hit-min));
  transform: translateY(-50%);
}

#detail-view.active .more-info-button {
  transform: translateX(0);
  opacity: 1;
}

/* Fills white when open — the same "this one is active" cue as the nav pill,
   and now carries the same elevation as it too. */
.detail-view--expanded .more-info-button {
  background: var(--surface-active);
  box-shadow: var(--elev-1);
}

@media (hover: hover) {
  .more-info-button:hover {
    background: var(--surface-active);
    box-shadow: var(--elev-1);
  }
}

.more-info-button:active {
  background: var(--surface-active);
  box-shadow: var(--elev-1);
  filter: brightness(0.94);
}

.more-info-icon {
  display: inline-block;
  font-size: var(--font-nav);
  transition: transform var(--dur-ui) var(--ease-out);
}

/* Info panel - sits below controls in the bottom bar */
.detail-info-panel {
  width: 100%;
  height: var(--panel-height);
  background: transparent;
  /* No divider here on purpose: the panel content materialises in from the
     button instead, which separates the two without drawing a line. */
  padding: 33px 80px 80px;
  overflow: hidden;
  pointer-events: auto;
}

.detail-info-content {
  max-width: 400px;
  margin: 0 auto;
  text-align: center;
  /* Anchored to the trigger: More Info sits bottom-left of the bar */
  transform-origin: left bottom;
  transform: scale(0.96);
  filter: blur(6px);
  opacity: 0;
  transition: transform var(--dur-ui) var(--ease-out),
              filter var(--dur-ui) var(--ease-out),
              opacity var(--dur-ui) var(--ease-out);
}

.detail-view--expanded .detail-info-content {
  transform: scale(1);
  filter: blur(0);
  opacity: 1;
}

.info-section {
  margin-bottom: 0;
}

/* Tags were empty on every project, so the panel only ever rendered one
   section and the sections never needed separating. Now that they render,
   consecutive sections are spaced on the house scale. A hidden section
   (credits, when empty) contributes nothing, so the gap stays correct in
   both states. */
.info-section + .info-section {
  margin-top: var(--space-md);
}

.info-section-text {
  font-size: 14px;
  font-weight: 400;
  /* Looser leading and near-zero tracking at small sizes */
  line-height: var(--lead-sm);
  letter-spacing: var(--track-sm);
  color: var(--text-primary);
  margin: 0;
}

.info-credits-list {
  list-style: none;
  padding: 0;
  margin: 0;
  text-align: center;
}

.info-credits-list li {
  font-size: 14px;
  font-weight: 400;
  line-height: var(--lead-sm);
  letter-spacing: var(--track-sm);
  color: var(--text-primary);
}

.info-tags-container {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  justify-content: center;
}

/* Was padding:0 / transparent / radius:0 — structurally a chip row (flex with
   an 8px gap) rendering as bare text, while every other small label on the
   site is a capsule. */
.info-tag {
  padding: 5px 12px 6px;
  background: var(--surface-raised);
  border-radius: var(--radius-pill);
  font-size: 14px;
  font-weight: 400;
  letter-spacing: -0.02em;
  color: var(--text-primary);
}

/* Expanded state - the entire bottom bar slides up as one unit (handled by .detail-bottom-bar rule above) */

/* ============================================
   VIEW TABS NAVIGATION
   ============================================ */
/* Pill-shaped nav groups: subtle recessed capsules on the page background.
   One shared white pill per group slides (left + width) to the hovered item
   and springs back to the active one — the pill signals state, text never
   changes. */
.nav-group {
  position: fixed;
  top: 16px;
  display: flex;
  align-items: center;
  padding: 4px;
  border-radius: var(--radius-pill);
  background: var(--surface-raised-glass);
  backdrop-filter: var(--blur-chrome);
  -webkit-backdrop-filter: var(--blur-chrome);
  box-shadow: var(--elev-1);
  z-index: 1004;
  font-family: 'ABC Favorit', sans-serif;
}

.view-tabs {
  left: 50%;
  transform: translateX(-50%);
}

.nav-group--secondary {
  right: 24px;
}

/* Yields to the centred group before the two would touch (measured: 671px) */
@media (max-width: 690px) {
  .nav-group--secondary {
    display: none;
  }
}

.tab-button,
.nav-link {
  position: relative;
  z-index: 1;
  padding: 9px 16px 10px;
  border-radius: var(--radius-pill);
  font-family: inherit;
  font-size: var(--font-nav);
  font-weight: 500;
  letter-spacing: var(--track-ui);
  line-height: var(--capsule-lh);
  color: var(--text-primary);
  text-decoration: none;
  white-space: nowrap;
  transition: transform var(--dur-press) var(--ease-out);
}

/* The 4px of group padding above and below each label was dead space to a
   finger. Reaching 44px costs no layout. */
.tab-button::after,
.nav-link::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  height: max(100%, var(--hit-min));
  transform: translateY(-50%);
}

.tab-button:active,
.nav-link:active {
  transform: scale(0.97);
}

.tab-button {
  background: transparent;
  border: none;
  cursor: pointer;
}

.nav-pill {
  position: absolute;
  top: 4px;
  bottom: 4px;
  left: 0;
  width: 0;
  border-radius: var(--radius-pill);
  background: var(--surface-active);
  box-shadow: var(--elev-1);
  opacity: 0;
  /* Was a 1.5 overshoot — bounce on a hover-driven object. Overshoot belongs
     to gestures that carried momentum. Swap --ease-pill back to restore it. */
  transition: left 420ms var(--ease-pill),
              width 420ms var(--ease-pill),
              opacity 180ms var(--ease-out);
  pointer-events: none;
}

.nav-pill.is-visible {
  opacity: 1;
}

.nav-pill.no-transition {
  transition: opacity 150ms ease;
}

@media (prefers-reduced-motion: reduce) {
  .nav-pill {
    transition: opacity 150ms ease;
  }
}

/* ============================================
   APPEARANCE TOGGLE
   ============================================ */
/* Same circle as the detail close button, wearing the nav's translucent
   material. Sits top-left; when the detail view opens, the close button takes
   that corner and this steps to its right on the same timing. */
.theme-toggle {
  position: fixed;
  top: 20px;
  left: 24px;
  width: var(--capsule-h);
  height: var(--capsule-h);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  margin: 0;
  border: none;
  border-radius: var(--radius-pill);
  background: var(--surface-raised-glass);
  backdrop-filter: var(--blur-chrome);
  -webkit-backdrop-filter: var(--blur-chrome);
  box-shadow: var(--elev-1);
  color: var(--text-primary);
  cursor: pointer;
  z-index: 1004;
  transition: transform var(--dur) var(--ease),
              background-color var(--dur-hover) var(--ease-out),
              box-shadow var(--dur-hover) var(--ease-out),
              filter var(--dur-press) var(--ease-out);
}

/* Half-filled circle — the standard "appearance" mark. Whole-pixel geometry
   (16px box, 2px border, split at 8px) so the divide stays crisp. */
.theme-toggle::before {
  content: '';
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 2px solid currentColor;
  background: linear-gradient(to right, currentColor 50%, transparent 50%);
}

.theme-toggle::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: max(100%, var(--hit-min));
  height: max(100%, var(--hit-min));
  transform: translate(-50%, -50%);
}

@media (hover: hover) {
  .theme-toggle:hover {
    background: var(--surface-active);
    box-shadow: var(--elev-1);
  }
}

.theme-toggle:active {
  background: var(--surface-active);
  filter: brightness(0.94);
}

/* Focus ring lives in the keyboard-gated block near the top of this file */

/* Detail view open: yield the corner to the close button */
#detail-view.active ~ .theme-toggle {
  transform: translateX(calc(var(--capsule-h) + var(--space-sm)));
}

/* ============================================
   VIEW CONTAINER MANAGEMENT
   ============================================ */
.view-container {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur) var(--ease);
  overflow: hidden;
}

.view-container.active {
  opacity: 1;
  pointer-events: auto;
}

/* ============================================
   BIO (Top Left - Global Element)
   ============================================ */

/* ============================================
   INFO VIEW CAROUSEL
   ============================================ */
#info-container {
  background: var(--surface-ground);
}

.info-carousel {
  width: 100%;
  height: 100%;
  position: relative;
  overflow: hidden;
  cursor: grab;
  perspective: 1200px;
  perspective-origin: 50% 50%;
  transform-style: preserve-3d;
}

.info-carousel.dragging {
  cursor: grabbing;
}

.info-slide {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 60vw;
  margin-left: -30vw;
  /* Capped so the card always clears the top nav by roughly 32px. A flat 80vh
     was fine while the slide was the same colour as the page, but it crowds
     the nav now the slide reads as a card — and the shorter the screen, the
     tighter it got (18px on a 667px phone). The margin is derived from the
     same value so the card stays exactly centred at any height; it cannot use
     a transform for that, because JS owns transform for the 3D carousel.
     dvh rather than vh so iOS's URL bar is accounted for. */
  --info-h: min(80dvh, calc(100dvh - 186px));
  height: var(--info-h);
  margin-top: calc(var(--info-h) / -2);
  /* A floating material rather than a flat block of page colour. The slides
     sit beside each other in a 3D carousel, so the blur catches its
     neighbours as you drag between them. */
  background: var(--surface-card-glass);
  backdrop-filter: var(--blur-card);
  -webkit-backdrop-filter: var(--blur-card);
  border-radius: var(--radius-card);
  box-shadow: var(--elev-card);
  color: var(--text-primary);
  display: flex;
  align-items: center;
  justify-content: center;
  will-change: transform;
  pointer-events: none;
  padding: var(--space-xl);
}

.info-slide-content {
  width: 100%;
  max-width: 380px;
  text-align: left;
}

.info-slide-content h2 {
  font-size: var(--font-ui);
  font-weight: 500;
  margin: 0 0 var(--space-lg) 0;
  text-transform: none;
  /* Tight leading and negative tracking as the size grows */
  line-height: 1.15;
  letter-spacing: var(--track-lg);
}

.info-slide-content p {
  font-size: var(--font-ui);
  font-weight: 400;
  line-height: var(--lead-lg);
  margin: 0 0 var(--space-sm) 0;
  letter-spacing: var(--track-lg);
  text-align: left;
}

.info-slide-content p:last-child {
  margin-bottom: 0;
}

.info-slide-content ul {
  list-style: none;
  padding: 0;
  margin: 0;
}

.info-slide-content ul li {
  font-size: var(--font-ui);
  font-weight: 400;
  line-height: var(--lead-lg);
  letter-spacing: var(--track-lg);
  margin: 0;
}

.info-slide-content .brands-intro {
  margin-bottom: 10px;
}

.info-slide-content .brands-list {
  columns: 1;
}

.info-slide-content .brands-list li {
  margin-bottom: 5px;
}

/* Same capsule as the detail view's counter */
.info-counter {
  position: fixed;
  bottom: 16px;
  right: 24px;
  padding: 9px 16px 10px;
  border-radius: var(--radius-pill);
  background: var(--surface-raised);
  color: var(--text-primary);
  font-size: var(--font-nav);
  font-weight: 500;
  letter-spacing: var(--track-ui);
  line-height: var(--capsule-lh);
  font-variant-numeric: tabular-nums;
  z-index: 100;
  pointer-events: none;
  opacity: 0;
  transform: translateX(100px);
  transition: transform var(--dur) var(--ease),
              opacity var(--dur) var(--ease);
}

#info-container.active .info-counter {
  opacity: 1;
  transform: translateX(0);
}

/* ============================================
   LIST VIEW
   ============================================ */
#list-container {
  width: 100%;
  height: 100vh;
  overflow: hidden; /* No scrollbars, use panning instead */
  background: var(--surface-ground);
  /* Fades content into the floating chrome rather than letting it slide
     under a hard edge. Lifted while scattering so flung items aren't clipped. */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 96px);
  mask-image: linear-gradient(to bottom, transparent 0, #000 96px);
  position: relative;
  cursor: grab;
}

#list-container-inner {
  position: absolute;
  padding: 160px 60px 40px 60px;
  width: 100%;
  will-change: transform;
  display: flex;
  flex-direction: column;
  align-items: center;
  box-sizing: border-box;
  transform-origin: center top;
}

.project-section {
  margin-bottom: 80px;
  display: flex;
  flex-direction: column;
  align-items: center;
  max-width: 1200px;
  width: 100%;
}

.project-section:last-child {
  margin-bottom: 0;  /* Remove margin from last section to prevent excess scroll */
}

/* Same capsule as the detail view's project title — the section heading and
   the title you land on after clicking it are the same object */
.project-title {
  padding: 9px 16px 10px;
  border-radius: var(--radius-pill);
  background: var(--surface-raised);
  font-size: var(--font-nav);
  font-weight: 500;
  letter-spacing: var(--track-ui);
  line-height: var(--capsule-lh);
  /* The capsule's fill extends below the text, so it needs more clearance
     from the grid than the old plain-text heading did */
  margin-top: calc((var(--hit-min) - var(--capsule-h)) / -2);
  margin-bottom: calc(50px - (var(--hit-min) - var(--capsule-h)) / 2);
  color: var(--text-primary);
  transform: translate3d(0, 0, 0); /* Initial transform for animation */
  text-align: center;
  max-width: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: pointer;
  /* This capsule needs overflow:hidden for its ellipsis, which would clip a
     pseudo-element hit area — so the 44px comes from a transparent border
     instead, with background-clip keeping the visible fill at 37px. The 7px
     is taken back out of margin-bottom, so layout is unchanged. */
  border-top: calc((var(--hit-min) - var(--capsule-h)) / 2) solid transparent;
  border-bottom: calc((var(--hit-min) - var(--capsule-h)) / 2) solid transparent;
  background-clip: padding-box;
  transition: background-color var(--dur-hover) var(--ease-out),
              box-shadow var(--dur-hover) var(--ease-out),
              filter var(--dur-press) var(--ease-out);
}

@media (hover: hover) {
  .project-title:hover {
    background: var(--surface-active);
    box-shadow: var(--elev-1);
  }
}

.project-title:active {
  background: var(--surface-active);
  box-shadow: var(--elev-1);
  filter: brightness(0.94);
}

.project-images-grid {
  display: grid;
  grid-template-columns: repeat(5, 220px);
  grid-auto-flow: row;
  gap: 30px;
  max-width: fit-content;
  width: 100%;
  justify-content: center;
  align-items: start;
  position: relative;
  margin: 0 auto;
}

.project-image-item {
  width: 220px;
  height: auto;
  overflow: hidden;
  cursor: pointer;
  transform: translate3d(0, 0, 0);
  transition: width var(--dur) var(--ease),
              height var(--dur) var(--ease),
              transform var(--dur) var(--ease);
  display: flex;
  align-items: center;
  justify-content: center;
}

.project-image-item img {
  max-width: 100%;
  height: auto;
  object-fit: contain;
  object-position: center;
  display: block;
}

/* List view scatter animation */
.list-item--scattering {
  transition: transform var(--dur) var(--ease);
}

.list-item--scattering.scatter-out {
  transition: transform var(--dur) var(--ease),
              opacity 0.3s var(--ease) 0.5s;
  opacity: 0;
}

/* Allow scattered items to fly beyond the list container */
#list-container.scattering {
  overflow: visible;
  -webkit-mask-image: none;
  mask-image: none;
}

/* Portrait video/image placeholder in list view */
.project-image-item--video {
  width: auto;
  height: 100%;
  overflow: hidden;
}

.project-image-item--video img {
  width: auto;
  height: 100%;
  max-width: none;
}

/* Landscape video placeholder in list view */
.project-image-item--video-landscape {
  width: 220px;
  height: auto;
}

.project-image-item--video-landscape img {
  max-width: 100%;
  height: auto;
}

/* ============================================
   MOBILE: Info view slide sizing
   ============================================ */
@media (max-width: 574px) {
  .info-slide {
    width: 85vw;
    margin-left: -42.5vw;
    padding: 30px;
  }
}

/* ============================================
   MOBILE: UI chrome — page furniture
   ============================================ */
@media (max-width: 430px) {
  :root {
    --font-ui: 14px;
    --font-nav: 13px;
    --capsule-lh: 16px;
    --capsule-h: 31px;  /* 7px + --capsule-lh + 8px */
  }

  /* Navigation tabs — tighter capsule */
  .nav-group {
    top: 12px;
    padding: 3px;
  }

  /* The nav is shorter here (49px rather than 61px to its underside), so the
     card can be correspondingly taller and keep the same clearance */
  .info-slide {
    --info-h: min(80dvh, calc(100dvh - 162px));
  }

  .tab-button,
  .nav-link,
  .image-counter,
  .detail-project-title,
  .more-info-button,
  .project-title,
  .info-counter {
    padding: 7px 12px 8px;
  }

  .info-tag {
    padding: 4px 10px 5px;
  }

  .nav-pill {
    top: 3px;
    bottom: 3px;
  }

  /* Top bar — shorter on mobile */
  #detail-view::before {
    height: 50px;
  }

  /* Detail view close button — align with the tighter mobile nav
     (top 12px + 3px padding → centre 30.5px; a --capsule-h box → top 15px) */
  .detail-close,
  .theme-toggle {
    top: 15px;
    left: 16px;
    width: var(--capsule-h);
    height: var(--capsule-h);
  }

  /* Bottom bar controls — tighter padding on mobile */
  .bottom-bar-controls {
    padding: 0 16px;
  }

  /* More info panel */
  .detail-info-panel {
    padding: 20px 20px 30px;
  }

  /* Info view counter */
  .info-counter {
    right: 20px;
    bottom: 16px;
  }

  /* Info slide text — left-align */
  .info-slide-content h2,
  .info-slide-content p,
  .info-slide-content ul li {
    text-align: left;
  }

}

/* Narrow phones (e.g. iPhone SE 375px, Galaxy S8+ 360px) — scale down to fit */
@media (max-width: 379px) {
  .detail-close,
  .theme-toggle {
    left: 12px;
  }

  /* Tighter than the 16px above, not wider — every pixel here is title room */
  .bottom-bar-controls {
    padding: 0 12px;
  }

  .info-counter {
    right: 12px;
  }
}
