/* ============================================================================
   FOLDER CARD STACK — styles
   ----------------------------------------------------------------------------
   Depth is a flat-2D stack: cards are scaled down + shifted up as they recede
   (translate + scale). Each card's own shape (a folder silhouette with a tab
   notch) is cut with `clip-path: path(...)`, generated in JS. See js/main.js
   for the full state machine. The page-load entrance is a true-Z move on the
   NATIVE Web Animations API (perspective + translateZ + a spacing accordion;
   no opacity) — see runIntroAnimation().

   Shadow note: cards use `filter: drop-shadow()` rather than `box-shadow`.
   box-shadow is a paint property — when a card changes apparent size (the
   Z intro, or any scale), the browser re-rasterizes the whole shadowed layer
   every frame, which tanked FPS. `filter` is compositor-friendly AND the
   drop-shadow follows the folder clip-path silhouette for free.
   ========================================================================== */

:root {
  /* ----- mockup geometry (tunable) ----- */
  --card-w: min(1200px, 92vw);      /* nearest/active card width; height derived by ratio */
  --card-ratio: (1200 / 836);       /* keep the mockup proportion — parenthesized so it divides
                                        correctly wherever it's substituted into another calc() */
  --card-h: calc(var(--card-w) / var(--card-ratio));
  --card-radius: 21px;              /* corner radius, flat px (not scaled by viewport width) */
  --stack-scale-min: 0.78;          /* farthest card scale; ladder interpolates up to 1.0 */
  --stack-peek: 28px;               /* base tab height; also seeds the ascending peek/gap pattern */
  --hover-lift: 60px;               /* how far the hovered card rises */
  --hover-spread: 12px;             /* how far neighbours part in depth */
  --card-drop-shadow: drop-shadow(0 14px 26px rgba(0, 0, 0, .20)); /* compositor-friendly; see header note */
  --unfold-center-y: 50%;           /* where a lifted card settles vertically */

  /* ----- derived tunables (not in the spec's minimum list, but kept here so
     the whole feel stays adjustable from one place) ----- */
  --tab-width-frac: 0.3;            /* tab width as a fraction of card width (measured from mockup) */
  --stack-rest-visible: 220px;      /* px of the nearest card visible above the viewport bottom at rest */
  --hover-duration: 190ms;
  --hover-scroll-fade: 0.024;       /* fraction of the scroll over which a hovered folder's lift fades into the unfold (~110px on a 900px screen) */
  --stage-duration: 800ms;          /* click-to-expand: folder raising (open) / folder+stack returning (close) — easy-out, see easeOut() in main.js */
  --stack-hide-duration: 800ms;     /* click-to-expand: stack disappearing (open only), independent of the above — easy-in, see easeIn() in main.js */
  --expand-duration: 480ms;         /* click-to-expand: center-stage / fullscreen morph step (easy-out too) */
  --intro-duration: 880ms;          /* page-load Z entrance: the whole stack moves in together */
  --intro-z: 420px;                 /* how far toward the viewer the stack starts (with --intro-perspective) */
  --intro-perspective: 1100px;      /* perspective depth for the Z entrance (smaller = stronger foreshortening) */
  --intro-spread: 1.7;              /* inter-card gap multiplier mid-move; back folders gather toward the front on settle */
  --intro-rise: 260px;              /* how far below rest the stack starts — it rises up from the bottom edge */
  --card-bg-top: #D9D9D9;           /* empty-face gradient, top */
  --card-bg-bottom: #737373;        /* empty-face gradient, bottom */
  --card-bg-active: #D9D9D9;        /* nearest / active card: flat, no gradient */
  --hero-name-fade-span: 0.12;      /* fraction of the scroll-unfold over which the intro text dissolves (fade+blur) once scrolling starts */
  --hero-name-blur-max: 14px;       /* blur amount reached at the end of that dissolve */
  --avatar-flight: 0.32;            /* viewport heights of scroll for the intro avatar to reach the header (pinned stack) */
  --intro-line-delay: 170ms;        /* intro text on load: gap between the starts of two lines */
  --intro-char-delay: 11ms;         /* … and between two letters in a line */
  --intro-char-duration: 560ms;     /* one letter, from full blur at zero opacity to sharp */
  --intro-char-blur: 14px;          /* where each letter starts (same as the scroll dissolve) */
  --lang-spread-y: 520ms;           /* language switch cascade: top → bottom of the screen (js/langswap.js) */
  --lang-spread-x: 240ms;           /* … and left → right within a line */
  --lang-swap-duration: 520ms;      /* one piece: the old blurs away while the new sharpens in, mirrored curves, same length */
  --convoy-lead-k: 130;             /* finale ↔ header buttons (js/convoy.js): the lead car's spring stiffness … */
  --convoy-lead-c: 20;              /* … and damping (more = brakes softer, less bounce) */
  --convoy-follow-k: 320;           /* the rest chase the car in front: stiffness … */
  --convoy-follow-c: 23;            /* … and damping — lower = they run up further and roll back more */
  --convoy-gap: 150ms;              /* header pills leave first, the finale row sets off this much later */
  --footer-delay: 700ms;            /* the footer comes up this long after the finale row starts driving in */
  --contact-line: 0.25;             /* scrolling down, the finale row drives in once the last folder's bottom edge is above this share of the screen height … */
  --contact-back: 30px;             /* … scrolling back up, it drives off as soon as that edge is this far into the screen */
  --contact-slot: 0.6;              /* scroll left after the last folder, in folder slots (1 = a screen); keep ≥ 0.325 */
  --card-stroke: rgba(0, 0, 0, .05); /* inner border of the folders, 1px */
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  background: var(--surface-page);
  color: var(--text-primary);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
}

/* ---- load curtain: an opaque page-coloured layer painted over everything on
   first load, so the brief base-layer flash before the JS module runs (and the
   intro's first frame) is hidden. js/main.js adds `is-hidden` once the first
   intro frame has painted → it fades out over ~0.28s. The `animation` is a
   failsafe: if JS never runs (no-JS / load error), the curtain still fades so
   the page is never stuck white. ---------------------------------------- */
.page-curtain {
  position: fixed;
  inset: 0;
  background: var(--surface-page);
  z-index: 9999;
  opacity: 1;
  pointer-events: none;
  transition: opacity 1s ease;
  animation: page-curtain-failsafe 0.4s ease 1.2s forwards;
}
.page-curtain.is-hidden {
  opacity: 0;
  animation: none; /* JS revealed it — cancel the failsafe */
}
@keyframes page-curtain-failsafe { to { opacity: 0; } }

/* ---- finale + footer (js/main.js → renderContact): the header's three
   buttons come back bigger, «Написати» split into glass orbs with the brand
   marks under the glass; the row drives in from the left on springs
   (js/convoy.js) while the header pills drive off, and every button's glass
   lifts into a magnifier on hover (js/lens.js). ------------------------ */
:root {
  --cx-size: clamp(56px, 4.5vw, 75px); /* ≈ 1.5 × the header pill */
  --cx-gap: clamp(8px, .7vw, 12px);
}
.contact {
  position: relative;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100svh;
  padding: var(--header-height) 16px 0;
  background: var(--surface-page);
  overflow: hidden; /* the parked row sits off-screen to the left */
}
.contact__body {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(36px, 4vw, 72px);
  padding: 48px 0;
}
.contact__title {
  max-width: 24em;
  margin: 0;
  font: 400 clamp(24px, 1.5vw + 13px, 42px) / 1.4 "Literaturnaya", Georgia, serif;
  letter-spacing: -0.01em;
  text-align: center;
  color: var(--text-primary);
  transition: opacity 700ms ease, filter 700ms ease;
}
.contact__title em { font-style: italic; letter-spacing: -0.06em; }
/* the line gathers out of a blur as the row arrives, like the intro */
.contact--live:not(.is-on) .contact__title { opacity: 0; filter: blur(var(--intro-char-blur)); }
@media (max-width: 767px) { .contact__title br { display: none; } }

.contact__row, .contact__orbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--cx-gap); /* every button the same distance apart, the orbs included */
  margin: 0;
  padding: 0;
  list-style: none;
}
.contact__orbs { flex-wrap: nowrap; }
.contact__item { display: block; will-change: transform; }
/* the magnifying glass over the row (js/lens.js): once the row has settled
   it is drawn on two canvases under the real buttons, which go see-through
   (they keep the pointer, the focus ring and the screen reader) */
.contact__stage { position: relative; }
.cx-lens-canvas { position: absolute; z-index: 0; pointer-events: none; display: none; }
.contact__stage.is-lensing .cx-lens-canvas { display: block; }
.contact__stage.is-lensing .contact__row { position: relative; z-index: 1; }
.contact__stage.is-lensing :is(.cx-orb__glass, .cx-orb__icon) { visibility: hidden; }
.contact__stage.is-lensing .cx-pill { color: transparent; background: none; border-color: transparent; box-shadow: none; }

/* pinned stack: the finale overlaps the last viewport of the track, so the
   page ends as the contact slot settles; the row is fixed mid-screen (where
   a fifth folder would stop) and the footer scrolls up under it */
html.enhanced .contact {
  margin-top: -100vh; /* the track's last screen */
  background: transparent;
  pointer-events: none;
}
html.enhanced .contact__body {
  position: fixed;
  inset: 0;
  z-index: 0; /* under the folders (the stack viewport is z 1): a folder coming back down covers the row while it drives off */
  padding: 0;
  pointer-events: none;
}
html.enhanced .contact__item { pointer-events: auto; } /* parked items are hidden, so the empty row never blocks the folders */
/* the footer waits at the bottom of the screen and comes up as soon as the
   row has driven in (the convoy's own pace), not at the end of the scroll */
html.enhanced .site-footer {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 2;
  margin: 0;
  opacity: 0;
  visibility: hidden;
  transform: translateY(16px);
  pointer-events: auto;
  transition: opacity 220ms ease, transform 220ms ease, visibility 0s linear 220ms;
}
html.enhanced.contact-on .site-footer {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition: opacity 500ms ease var(--footer-delay), transform 600ms cubic-bezier(.2, .7, .2, 1) var(--footer-delay), visibility 0s linear var(--footer-delay);
}
html.case-expanded .site-footer { visibility: hidden; }
html.case-expanded .contact__body { visibility: hidden; }

/* every button: a white disc (or pill) with a fine edge and a soft shadow
   below; the same look the lens row draws (js/lens.js), so the swap between
   the two never shows. An orb is its coloured mark on its disc. */
.cx-orb {
  position: relative;
  display: block;
  width: var(--cx-size);
  height: var(--cx-size);
  border-radius: 50%;
}
.cx-orb:focus-visible, .cx-pill:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 4px; }
.cx-orb__icon {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.cx-orb__icon svg { width: 41.6%; height: 41.6%; display: block; }
.cx-orb__at {
  font: 600 calc(var(--cx-size) * .489) / 1 "Fixel Display", "Fixel Text", system-ui, sans-serif;
  color: #000;
  transform: translateY(-.04em);
}
.cx-orb__glass, .cx-pill {
  background: #fbfbfb;
  border: 1px solid rgba(0, 0, 0, .06);
  box-shadow: 0 6px 14px rgba(0, 0, 0, .10);
}
.cx-orb__glass {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  pointer-events: none;
}

/* «Забукати міт» and CV at the orbs' height, in the same look */
.cx-pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: var(--cx-size);
  padding: 0 calc(var(--cx-size) * .3);
  border-radius: 999px;
  font: 500 calc(var(--cx-size) * .36) / 1 "Fixel Text", system-ui, sans-serif;
  color: var(--text-primary);
  text-decoration: none;
  white-space: nowrap;
}
.cx-pill--round { width: var(--cx-size); padding: 0; }

/* footer: the time in Kharkiv (handy across time zones) in the buttons'
   Fixel, the name in the middle as the header sets it on load, and the
   case pages' back-to-top button */
.site-footer {
  display: grid;
  grid-template-columns: 1fr auto 1fr; /* the name sits dead centre whatever the sides measure */
  align-items: center;
  gap: 12px 24px;
  margin: auto -16px 0; /* at the bottom (on the pinned stack the row above is fixed, out of the flow); out of .contact's side padding */
  padding: 0 var(--header-padding-x) calc(var(--header-padding-top) + env(safe-area-inset-bottom));
}
.site-footer__copy {
  font: 700 calc(20px / 1.5) / calc(29px / 1.5) "Literaturnaya", Georgia, serif; /* the header name on load, at two thirds */
  letter-spacing: .18em;
  text-transform: uppercase;
  color: rgba(0, 0, 0, .55);
}
.site-footer__time {
  justify-self: start;
  font: 500 calc(var(--header-pill-font) / 1.5 + 2px) / 1.4 "Fixel Text", system-ui, sans-serif;
  color: var(--text-secondary);
}
.site-footer__time time { color: var(--text-primary); } /* Fixel's own digits: its tabular ones read as another typeface */
/* the colon ticks like a desk clock's: on for a second, off for a second */
.site-footer__colon { animation: clock-colon 2s steps(1, end) infinite; }
@keyframes clock-colon { 50% { opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .site-footer__colon { animation: none; } }
.site-footer__top { justify-self: end; }
@media (max-width: 767px) {
  .site-footer { grid-template-columns: 1fr; justify-items: center; text-align: center; }
  .site-footer__time, .site-footer__top { justify-self: center; }
}

/* ---- measurement ruler: invisible, untransformed, gives JS the resolved
   px size of var(--card-w/--card-h) without reading a card mid-transform ---- */
.size-ruler {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--card-w);
  height: var(--card-h);
  visibility: hidden;
  pointer-events: none;
}

/* ---- intro (Figma «Card Stack - Default»): avatar + a few lines about me
   above the stack. On the pinned stack it sits behind every card (z -1)
   inside the sticky viewport, centred in the space between the header and
   the back folder (--stack-top comes from js/main.js), and dissolves as the
   unfold starts. On the plain list it's simply the first block. The avatar
   here is only a placeholder: the visible one is .site-avatar, which flies
   into the header. ------------------------------------------------------ */
.intro {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(20px, 2.2vw, 43px);
  padding: calc(var(--header-height) + 24px) 16px 8px;
  pointer-events: none;
}
.intro__avatar {
  display: block;
  width: clamp(72px, 4vw + 42px, 100px);
  aspect-ratio: 1;
  visibility: hidden;
}
.intro__avatar img { display: block; width: 100%; height: 100%; }
.intro__text {
  width: 21.4em; /* Figma 934px at 42px, minus the extra л of «іллюстрацію» so «та» wraps as in the layout */
  max-width: 100%;
  margin: 0;
  font: 400 clamp(24px, 1.5vw + 13px, 42px) / 1.4 "Literaturnaya", Georgia, serif;
  letter-spacing: -0.01em;
  color: var(--text-primary);
}
.intro__text em { font-style: italic; letter-spacing: -0.03em; }
.intro__text u { text-decoration-thickness: .04em; text-underline-offset: .14em; }
.intro__text .intro__ferrari { letter-spacing: -0.06em; }
@media (max-width: 767px) { .intro__text br { display: none; } }
/* language switch (js/langswap.js): a text-only copy of the old page sits on
   top while its letters blur away, the new letters assemble underneath */
.lang-ghost { pointer-events: none !important; }
.lang-ghost, .lang-ghost * {
  background: transparent !important;
  border-color: transparent !important;
  box-shadow: none !important;
  outline: none !important;
  transition: none !important;
}
.lang-ghost *:not(.cs-ink-on) { -webkit-backdrop-filter: none !important; backdrop-filter: none !important; }
.lang-ghost :is(img, video, canvas, picture, svg, .cs-lang__thumb, .site-avatar__ring, .cs-avatar) { visibility: hidden !important; }
.lang-ghost .cs-chart svg, .lang-ghost .cs-chart svg * { visibility: visible !important; } /* a chart on screen cross-blurs as a whole */
.lang-ghost .cs-ink-on { visibility: visible; } /* glass labels are letter-shaped windows */
/* old and new run the same curve mirrored, so at any moment their opacities
   add up to one: each word turns into the other, no gap, no double */
.ls-out { animation: lang-out var(--lang-swap-duration) cubic-bezier(.45, 0, .55, 1) var(--d, 0ms) both; }
.ls-in { animation: lang-in var(--lang-swap-duration) cubic-bezier(.45, 0, .55, 1) var(--d, 0ms) both; }
/* each moving piece (a text block, or an intro word) on its own layer: the
   blur runs on the GPU instead of re-painting the page every frame */
.ls-out, .ls-in { will-change: filter, opacity; }
/* new text is hidden from the very first frame, before its animation has
   started, and the new glass labels too until their turn comes — otherwise
   the new words flash in before the cascade reaches them */
.ls-in { opacity: 0; }
html.lang-swapping .is-ink:not(.lang-ghost) .cs-ink-on.cs-ink:not(.ls-in) { opacity: 0; }
@keyframes lang-out {
  from { opacity: 1; filter: blur(0); }
  to { opacity: 0; filter: blur(var(--intro-char-blur)); }
}
@keyframes lang-in {
  from { opacity: 0; filter: blur(var(--intro-char-blur)); }
  to { opacity: 1; filter: blur(0); }
}

/* letters assembling on load (js/main.js → assembleIntroText) */
.intro__text.is-waiting { visibility: hidden; }
.intro__text.is-assembling .intro__ch {
  animation: intro-ch var(--intro-char-duration) cubic-bezier(.2, .65, .25, 1) var(--d, 0ms) both;
}
@keyframes intro-ch {
  from { opacity: 0; filter: blur(var(--intro-char-blur)); }
  to { opacity: 1; filter: blur(0); }
}

html.enhanced .intro {
  position: absolute;
  left: 50%;
  top: calc((var(--header-height) + var(--stack-top, 60vh)) / 2);
  width: max-content;
  max-width: calc(100vw - 48px);
  padding: 0;
  transform: translate(-50%, -50%);
  z-index: -1;
  will-change: opacity, filter;
}

/* ---- the flying avatar (js/main.js → updateHeaderFlight): fixed, sized and
   placed every scroll frame; --ring (0…1) fades in the glass ring of the
   docked state (Figma component avatar_float: 56 photo in a 62 ring). ---- */
.site-avatar {
  --ring: 0;
  position: fixed;
  top: 0;
  left: 0;
  z-index: 25;
  width: 100px;
  height: 100px;
  border-radius: 50%;
  pointer-events: none;
  transform-origin: 50% 50%;
  transition: opacity 200ms ease;
}
.site-avatar__ring {
  position: absolute;
  inset: -3px;
  border-radius: 50%;
  background: var(--glass-fill);
  border: 1px solid var(--glass-stroke);
  box-shadow: 0 2px 2px rgba(0, 0, 0, .12);
  -webkit-backdrop-filter: blur(6px) saturate(180%);
  backdrop-filter: blur(6px) saturate(180%);
  opacity: var(--ring);
}
.site-avatar img { position: relative; display: block; width: 100%; height: 100%; border-radius: 50%; }

/* ---- site header: same markup and tokens as the case header; the name is
   55% ink here and squeezes away (inline transform from JS) ---------------- */
.site-header { z-index: 24; transition: opacity 200ms ease; }
.site-header.is-ink { z-index: 26; } /* inverted labels sit above the glass and the flying avatar's ring */
.site-header .cs-avatar { display: none !important; }
.site-header .cs-name { color: rgba(0, 0, 0, .55); transform-origin: 50% 50%; will-change: transform, opacity, filter; }
html.case-expanded .site-header,
html.case-expanded .site-avatar { opacity: 0; pointer-events: none; }
html.case-expanded .site-header * { pointer-events: none !important; }

/* ---- BASE LAYER: plain vertical list. This is what mobile, reduced-motion,
   and no-JS all see — the enhancement layer below only activates once JS
   confirms the capability gate (min-width + hover + fine pointer + motion-OK)
   and adds `enhanced` to <html>. ----------------------------------------- */
.stack-track { position: relative; }

.stack-viewport { display: block; }

.stack-list {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 24px;
  margin: 0;
  padding: 32px 16px 64px;
  list-style: none;
}

.card {
  position: relative;
  width: var(--card-w);
  height: var(--card-h);
  max-width: 100%;
  margin: 0;
  padding: 0;
  border: none;
  display: block;
  background: linear-gradient(180deg, var(--card-bg-top), var(--card-bg-bottom));
  border-radius: var(--card-radius);
  filter: var(--card-drop-shadow);
  cursor: pointer;
  font: inherit;
  text-align: left;
  -webkit-tap-highlight-color: transparent;
}

/* Live "lit" amount (0 = full gradient, 1 = flat/active) is applied as an
   inline background from JS — see setCardLit() — so it can react to scroll
   continuously instead of snapping between two fixed states. */

/* inner border on the folders: on the pinned stack an SVG copy of the
   folder silhouette (js/main.js → setCardStroke), clipped to 1px inside; on
   the plain list a rounded 1px border */
.card-stroke { display: none; }
html.enhanced .card .card-stroke {
  display: block;
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}
.card-stroke path { fill: none; stroke: var(--card-stroke); stroke-width: 2px; }
html:not(.enhanced) .card::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  border: 1px solid var(--card-stroke);
  border-radius: inherit;
  pointer-events: none;
}

.card-media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--media-pos, 50% 50%);
  border-radius: inherit;
  display: block;
  pointer-events: none;
}

/* ---- Click-to-expand: the clicked CARD ITSELF grows to fill the viewport
   (the morph), then the case-study content (#expandContent) fades in on top
   of it. Close reverses: content out, then card morphs back. See
   openExpand()/closeExpand() in js/main.js. ----------------------------- */
.expand-close {
  position: fixed;
  top: 24px;
  right: 24px;
  width: 44px;
  height: 44px;
  border: none;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.06);
  color: #444;
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  z-index: 3100;
  transition: opacity 150ms ease, background-color 150ms ease;
}
.expand-close:hover { background: rgba(0, 0, 0, 0.12); }
.expand-close.is-visible {
  opacity: 1;
  pointer-events: auto;
}

/* ---- Case-study content surface: a full-viewport scrollable panel laid
   OVER the morphed-fullscreen card (z above the card at 3000, below the
   close button at 3100). Hidden until the morph finishes, then its blocks
   animate in (Motion stagger). It carries its own opaque background so the
   text is readable regardless of the card colour beneath it. ----------- */
.expand-content {
  position: fixed;
  inset: 0;
  z-index: 3050;
  overflow-y: auto;
  overflow-x: hidden;
  background: var(--surface-page);
  opacity: 0;
  pointer-events: none;
  transition: opacity 240ms ease;
  -webkit-overflow-scrolling: touch;
}
.expand-content.is-open { opacity: 1; pointer-events: auto; }

.case {
  max-width: 920px;
  margin: 0 auto;
  padding: 104px 32px 128px;
}
.case-eyebrow {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-secondary);
  margin: 0 0 14px;
}
.case-title {
  font-size: clamp(34px, 5vw, 64px);
  line-height: 1.05;
  font-weight: 700;
  margin: 0 0 18px;
  letter-spacing: -0.02em;
}
.case-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 28px;
  margin: 0 0 40px;
  font-size: 14px;
  color: var(--text-secondary);
}
.case-meta b { color: var(--text-primary); font-weight: 600; }
.case-hero {
  width: 100%;
  aspect-ratio: 1200 / 720;
  object-fit: cover;
  border-radius: 18px;
  display: block;
  margin: 0 0 48px;
  background: #eee;
}
.case-body { max-width: 660px; }
.case-body h2 {
  font-size: clamp(20px, 2.4vw, 26px);
  margin: 40px 0 12px;
  letter-spacing: -0.01em;
}
.case-body p {
  font-size: 17px;
  line-height: 1.7;
  color: #333;
  margin: 0 0 18px;
}
.case-gallery {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
  margin: 40px 0 8px;
}
.case-gallery img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: 12px;
  display: block;
  background: #eee;
}
/* blocks the content-entrance animation targets; start hidden so there's no
   flash of final-position content before Motion takes over (JS clears these). */
.expand-content [data-reveal] { opacity: 0; }

/* ---- ENHANCEMENT LAYER: pinned scroll-driven deck. Gated by JS adding
   `enhanced` to <html>. Mirrored by a matching media query below. ------- */
html.enhanced .stack-track {
  height: 700vh; /* 6 cards + 1 extra viewport of settle room; set again from JS to track card count */
}

html.enhanced .stack-viewport {
  position: sticky;
  top: 0;
  z-index: 1;            /* above the finale's row (js/main.js → updateContact) … */
  height: 100vh;
  overflow: hidden;
  pointer-events: none;  /* … without blocking it or the footer: only the folders take the pointer */
}
html.enhanced .stack-viewport .card { pointer-events: auto; }

html.enhanced .stack-list {
  position: absolute;
  inset: 0;
  display: block;
  margin: 0;
  padding: 0;
  gap: 0;
}

html.enhanced .card {
  position: absolute;
  top: 0;
  left: 50%;
  max-width: none;
  transform-origin: 50% 0%;   /* top-center: translateY places the TOP edge, scale shrinks downward from it */
  will-change: transform, opacity; /* opacity now animates during the Z intro */
}

@media (prefers-reduced-motion: reduce) {
  html.enhanced .stack-track,
  html.enhanced .stack-viewport,
  html.enhanced .stack-list,
  html.enhanced .card {
    /* belt-and-suspenders: JS already refuses to enhance under reduced-motion,
       this just guarantees the static list wins even if that check is ever bypassed */
    position: static !important;
    height: auto !important;
    inset: auto !important;
    transform: none !important;
  }
  html.enhanced .stack-list {
    display: flex !important;
    flex-direction: column;
    align-items: center;
    gap: 24px;
    padding: 32px 16px 64px;
  }
  html.enhanced .intro {
    position: static !important;
    transform: none !important;
    padding: calc(var(--header-height) + 24px) 16px 8px !important;
  }
}
