.project-card {
  display: block;
}

/* The `hidden` attribute (used by assets/js/project-filter.js to remove
   non-matching cards/rows from layout) doesn't work for free here: the
   browser's UA stylesheet supplies `[hidden] { display: none }`, but
   author-origin `display` declarations — `.project-card`'s `display:
   block` above, and `.project-row`/`.project-row-2`…`-5`'s `display: grid`
   in project-grid.css — always beat a UA-origin rule regardless of
   selector specificity. Without this override, setting `.hidden = true`
   on a card or row would silently do nothing. */
.project-card[hidden],
[data-project-row][hidden] {
  display: none;
}

.project-card__media {
  position: relative;
  aspect-ratio: 4 / 5;
  overflow: hidden;
  background-color: var(--color-bg-alt);
  /* The mask — fixed size, clips its content, and never itself carries a
     transform. Hover zoom lives one level deeper, on
     .project-card__media-zoom, not on the img/video itself: that's where
     scroll parallax (assets/js/parallax.js) sets its own inline transform
     every scroll tick, and GSAP inline styles always beat a stylesheet
     rule, so a hover transform on the media element itself would silently
     never show. It also can't live on this outer wrapper — scaling this
     element would grow the box itself and bleed over the grid cell/
     neighbouring cards, not stay masked within a fixed frame. The zoom
     wrapper is the one element free to carry it: no GSAP transform of its
     own, and its parent's overflow: hidden clips whatever it scales to. */
}

.project-card__media-zoom {
  width: 100%;
  height: 100%;
  transition: transform var(--duration-slow) var(--ease-standard);
}

.project-card__media img,
.project-card__media video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.project-card:hover .project-card__media-zoom,
.project-card:focus-visible .project-card__media-zoom {
  transform: scale(1.06);
}

@media (prefers-reduced-motion: reduce) {
  .project-card__media-zoom {
    transition: none;
  }
}

.project-card__meta {
  margin-top: var(--space-8);
}

/* Sized up a step from .caption's 11px default — at that size the sand
   accent colour reads too faint to be legible. */
.project-card__category {
  margin-top: var(--space-3);
  font-size: var(--font-size-4);
  color: var(--color-accent);
}

/* Landscape crop — used on any card that shouldn't take the default
   portrait treatment: smaller cards sharing a row with a taller sibling
   (equal width + equal aspect-ratio gives equal rendered height without a
   fixed pixel value), and any card spanning the full 6-column half-width,
   which reads as portrait-cropped-too-tight at that width. */
.project-card--landscape .project-card__media {
  aspect-ratio: 4 / 3;
}

/* ---- Fade-in reveal (assets/js/project-fade.js) ----
   Unlike the footer's fade (footer.css), the hidden state here is
   unconditional — it applies straight from the bare attribute
   (data-project-fade="") with no gating class required. This project
   listing sits close enough to the fold that it can be partially visible
   on first paint; main.js is a deferred module script that runs *after*
   that first paint, so gating the hidden state behind a JS-added class
   (the footer's approach) would let the browser paint cards fully visible
   for a frame and then genuinely *transition* them to hidden once the
   class lands — a visible flash-then-fade right at page load. Applying
   the hidden state unconditionally from the raw HTML avoids that: there's
   nothing to transition from, since it's the very first computed style.
   The <noscript> block in projects.html's <head> is what guarantees full
   visibility with JS disabled (see reduced-motion override below for that
   case). JS flips the attribute to "visible" once a card's row rises into
   view. Each card's stagger is a transition-delay set inline by the JS
   (per its position within the row) rather than CSS nth-child — some rows
   nest cards one level deeper than others (Row 1's secondary group), so a
   flat selector can't reliably order every row's cards. */

[data-project-fade] {
  transition:
    opacity calc(var(--duration-slow) * 2) var(--ease-standard),
    transform calc(var(--duration-slow) * 2) var(--ease-standard),
    filter calc(var(--duration-slow) * 2) var(--ease-standard);
}

/* Blur amount matches the panel text entrance (TEXT_ANIMATION in
   panel-text.js) and the footer CTA columns that mirror it — keep in sync
   if that value is retuned. */
[data-project-fade=""] {
  opacity: 0;
  transform: translateY(var(--space-4));
  filter: blur(6px);
}

[data-project-fade="visible"] {
  filter: blur(0);
}

@media (prefers-reduced-motion: reduce) {
  [data-project-fade] {
    opacity: 1;
    transform: none;
    filter: none;
    transition: none;
  }
}
