.project-card {
  position: relative;
  display: flex;
  flex-direction: column;
  height: 100%;
  padding: 20px;
  border-radius: var(--radius-xl);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  overflow: hidden;
  cursor: pointer;
  opacity: 0;
  /*
    The reveal moves the card with `translate` and the interaction scales it
    with `transform`, deliberately — they are separate properties that
    compose, so neither has to win. Sharing one `transform` meant the reveal
    rule (three classes) outranked `:hover` (two) and silently killed the
    hover scale the moment the grid came into view.
  */
  translate: 0 var(--motion-shift);
  /*
    Two different jobs, so two different responses: the reveal is slow and
    staggered because it's ambient, the hover/press reaction is fast because
    a hand is waiting on it. `--ease-exit` is the cubic-bezier mirror of
    `--ease-enter`, so a card that grew on hover shrinks back along exactly
    the path it grew on.
  */
  transition:
    transform var(--response) var(--ease-exit),
    box-shadow var(--response) var(--ease-exit),
    border-color var(--response) var(--ease-exit),
    opacity var(--response-slow) var(--ease-enter),
    translate var(--response-slow) var(--ease-enter);
  transition-delay: 0ms, 0ms, 0ms,
    calc(var(--i, 0) * 90ms), calc(var(--i, 0) * 90ms);
}

.projects-grid.in-view .project-card {
  opacity: 1;
  translate: 0 0;
}

.project-card:hover,
.project-card:focus-within,
.project-card.is-open {
  transform: scale(1.035);
  z-index: 2;
  border-color: rgba(var(--color-brand-rgb), 0.35);
  box-shadow: 0 24px 48px rgba(var(--color-black-rgb), 0.12);
  transition:
    transform var(--response-fast) var(--ease-enter),
    box-shadow var(--response-fast) var(--ease-enter),
    border-color var(--response-fast) var(--ease-enter);
}

/*
  The press reads as the card being pushed *in* from wherever it already is,
  not jumping to some unrelated size — so it compounds with the hover scale
  rather than replacing it. It also outranks the hover rules, because the
  most recent thing the user did is the thing that should be visible.
*/
.project-card.is-pressed,
.project-card.is-pressed:hover,
.project-card.is-pressed:focus-within {
  transform: scale(calc(1.035 * var(--press-scale)));
  transition: transform var(--response-instant) var(--ease-enter);
}

.project-card:focus-within,
.project-card.is-open {
  outline: none;
}

.project-card-image {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--color-project-image-bg);
}

.project-card-placeholder {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-brand);
  opacity: 0.55;
  transition: transform var(--response) var(--ease-exit);
}

.project-card-placeholder img {
  width: 100%;
  height: 100%;
  object-fit:cover;
  object-position: center;
}

.project-card-placeholder:has(img) {
  opacity: 1;
}

.project-card:hover .project-card-placeholder,
.project-card:focus-within .project-card-placeholder,
.project-card.is-open .project-card-placeholder {
  transform: scale(1.08);
  transition: transform var(--response) var(--ease-enter);
}

.project-card-body {
  flex-shrink: 0;
  padding-top: 16px;
}

.project-badge {
  display: inline-block;
  margin-bottom: 10px;
  padding: 0.3rem 0.75rem;
  border-radius: var(--radius-pill);
  background: rgba(var(--color-brand-rgb), 0.1);
  color: var(--color-brand);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}

.project-card-title {
  margin: 0;
  font-size: 1.4rem;
  font-weight: 700;
  line-height: 1.25;
  color: var(--color-heading);
}

/*
  The repo link. It lives on the title so the accessible name is the project
  name rather than a wall of identical "GitHub" links, and it inherits the
  heading's type — the anchor is the title, not a control bolted next to it.
*/
.project-card-link {
  display: inline-flex;
  align-items: baseline;
  gap: 0.4em;
  color: inherit;
  text-decoration: none;
}

/*
  Stretched link: an invisible overlay makes the whole tile the hit target,
  which is what a card that already scales and lifts on hover is promising.

  Guarded to fine, hover-capable pointers on purpose. On touch there is no
  hover, so the *tap* is what opens the description (see project-cards.js) —
  a full-card link there would navigate away before the card ever gets to
  say what the project is. Coarse pointers keep the title itself as the
  target, small but unambiguous, and the tap-to-expand survives.
*/
@media (hover: hover) and (pointer: fine) {
  .project-card-link::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
  }
}

.project-card-link-icon {
  width: 0.7em;
  height: 0.7em;
  flex-shrink: 0;
  color: var(--color-text-faint);
  transition: color var(--response) var(--ease-exit);
}

.project-card:hover .project-card-link-icon,
.project-card:focus-within .project-card-link-icon,
.project-card.is-open .project-card-link-icon {
  color: var(--color-brand);
  transition: color var(--response-fast) var(--ease-enter);
}

/* The card kills its own focus ring (`:focus-within { outline: none }`), so
   the ring belongs to the link — and has to sit inside the card's `overflow:
   hidden`, hence a small offset rather than a large one. */
.project-card-link:focus-visible {
  outline: 2px solid var(--color-brand);
  outline-offset: 3px;
  border-radius: 4px;
}

/* The two feature cards in each group of six carry a larger title. Keyed to
   the same nth-child positions css/sections/projects.css gives the feature
   shapes, so the type follows the layout instead of naming fixed cards. */
.project-card:nth-child(6n + 1) .project-card-title,
.project-card:nth-child(6n + 2) .project-card-title {
  font-size: 1.55rem;
}

.project-card-desc-wrap {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--response) var(--ease-exit);
}

.project-card:hover .project-card-desc-wrap,
.project-card:focus-within .project-card-desc-wrap,
.project-card.is-open .project-card-desc-wrap {
  grid-template-rows: 1fr;
  transition: grid-template-rows var(--response) var(--ease-enter);
}

.project-card-desc {
  margin: 0;
  overflow: hidden;
  font-size: 0.92rem;
  line-height: 1.55;
  color: var(--color-text-muted);
  opacity: 0;
  transition: opacity var(--response-fast) var(--ease-exit);
}

.project-card:hover .project-card-desc,
.project-card:focus-within .project-card-desc,
.project-card.is-open .project-card-desc {
  padding-top: 10px;
  opacity: 1;
  /* Text arrives just behind the space that opened for it, so the panel
     reads as making room rather than the two racing each other. */
  transition: opacity var(--response) var(--ease-enter) 60ms;
}

/*
  Reduced motion keeps the opacity cross-fades — they carry meaning and
  aren't vestibular — and drops only the travel and the scale. `--motion-shift`
  and `--press-scale` already zero themselves in css/base/motion.css, so the
  reveal and press become fades in place with no rules repeated here.
*/
@media (prefers-reduced-motion: reduce) {
  .project-card:hover,
  .project-card:focus-within,
  .project-card.is-open,
  .project-card.is-pressed,
  .project-card.is-pressed:hover,
  .project-card.is-pressed:focus-within,
  .project-card-placeholder,
  .project-card:hover .project-card-placeholder,
  .project-card:focus-within .project-card-placeholder,
  .project-card.is-open .project-card-placeholder {
    transform: none;
  }
}

@media (max-width: 640px) {
  .project-card {
    min-height: 300px;
    padding: 16px;
  }

  .project-card-title {
    font-size: 1.25rem;
  }

  .project-card:nth-child(6n + 1) .project-card-title,
  .project-card:nth-child(6n + 2) .project-card-title {
    font-size: 1.35rem;
  }
}
