/* ==========================================================================
   PROJECTS / SOFTWARE GRID
   ==========================================================================

   Replaces 290 lines of CSS that were inlined in the Blade template, so it
   shipped in the HTML of every homepage load and could not be cached.

   The old grid had three problems beyond that:

   1. It rendered <img> unconditionally, so nine of twelve cards showed a
      broken image with the alt text sitting on a grey box.
   2. Cards carried a title, a category chip and a button, but no description
      — nothing to tell a visitor what the product actually does.
   3. Card heights were set by whether the screenshot loaded, so a row could
      be twice the height of the one above it.
   ========================================================================== */

.wm-projects {
  --azure-100: #cfedff;
  --azure-300: #5fcdfb;
  --azure-400: #29b6f0;
  --azure-600: #0079b8;
  --azure-700: #005f8f;
  --ink-950: #080c12;
  --ink-700: #22303f;
  --ink-600: #35485c;
  --ink-500: #4e6479;
  --ink-100: #e4ebf1;
  --ink-50: #f2f6f9;

  font-family: 'Roboto', sans-serif;
}

.wm-projects *, .wm-projects *::before, .wm-projects *::after { box-sizing: border-box; }

/* Three per row. Wider cards give the screenshot window enough height for
   the hover pan to reveal something, which four-up did not. */
.wm-projects__grid {
  display: grid;
  gap: 1.75rem;
  grid-template-columns: 1fr;
}

@media (width >= 48rem) {
  .wm-projects__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (width >= 68rem) {
  .wm-projects__grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* --- Card ------------------------------------------------------------------
   Flex column with the footer pushed down, so every card in a row ends at the
   same place regardless of how long its description is. */

.wm-project {
  position: relative;
  display: flex;
  flex-direction: column;
  background-color: #fff;
  border: 1px solid rgb(8 12 18 / .09);
  border-radius: 10px;
  overflow: hidden;
  transition: border-color .25s ease, transform .25s ease, box-shadow .25s ease;
}

.wm-project:hover {
  border-color: rgb(41 182 240 / .5);
  transform: translateY(-2px);
  box-shadow: 0 12px 32px rgb(8 12 18 / .08);
}

/* --- Media -----------------------------------------------------------------
   Fixed aspect ratio, so a missing screenshot leaves a designed panel rather
   than collapsing the card or stretching it. */

.wm-project__media {
  position: relative;
  /* A fixed window rather than an aspect ratio: the tall screenshots are
     panned through it on hover, and a fixed height is what gives the
     animation a known distance to travel. */
  height: 260px;
  overflow: hidden;
  background-color: var(--ink-50);
  border-bottom: 1px solid rgb(8 12 18 / .07);
}

/* Default: a short screenshot simply fills the window. */
.wm-project__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top center;
  display: block;
  transition: transform .7s cubic-bezier(.16, 1, .3, 1);
}

.wm-project:hover .wm-project__media img { transform: scale(1.04); }

/* --- Hover pan --------------------------------------------------------------
   Only applied where the image is genuinely taller than the window. The class
   is set server-side from the file's real dimensions, so a wide screenshot
   never gets an animation with nowhere to go. */

.wm-project__media--scroll img {
  width: 100%;
  height: auto;
  object-fit: unset;
  position: absolute;
  top: 0;
  left: 0;
  transition: none;
}

.wm-project:hover .wm-project__media--scroll img {
  transform: none;
  /* Down to the foot of the capture and back. Slow, because the point is to
     let someone read the interface, not to show off the animation. */
  animation: wmProjectPan 9s ease-in-out infinite;
}

@keyframes wmProjectPan {
  0%, 8% { transform: translateY(0); }
  55%, 63% { transform: translateY(calc(-100% + 260px)); }
  100% { transform: translateY(0); }
}

/* Hint that the panel is worth hovering, since the cue is otherwise invisible
   until the pointer is already there. */
.wm-project__scroll-hint {
  position: absolute;
  inset-block-end: .625rem;
  inset-inline-end: .625rem;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: .3125rem;
  padding: .25rem .5rem;
  border-radius: 999px;
  font-family: 'Mulish', sans-serif;
  font-size: .5625rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: #fff;
  background-color: rgb(8 12 18 / .72);
  backdrop-filter: blur(6px);
  opacity: 0;
  transform: translateY(4px);
  transition: opacity .25s ease, transform .25s ease;
}

.wm-project:hover .wm-project__scroll-hint { opacity: 1; transform: none; }

.wm-project__scroll-hint svg { width: .625rem; height: .625rem; }

@media (prefers-reduced-motion: reduce) {
  .wm-project:hover .wm-project__media--scroll img { animation: none; }
}

/* Placeholder for products without a screenshot yet. A monogram on a tinted
   panel reads as deliberate; a broken image reads as neglect. */
.wm-project__placeholder {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  background:
    radial-gradient(circle at 30% 25%, rgb(41 182 240 / .16) 0%, transparent 60%),
    linear-gradient(160deg, #0e141c 0%, #080c12 100%);
}

.wm-project__placeholder span {
  font-family: 'Mulish', sans-serif;
  font-weight: 800;
  font-size: 3.5rem;
  line-height: 1;
  color: var(--azure-300);
  opacity: .9;
}

/* Faint grid, matching the hero, so the panel is not a flat rectangle. */
.wm-project__placeholder::before {
  content: '';
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(to right, rgb(255 255 255 / .05) 1px, transparent 1px),
    linear-gradient(to bottom, rgb(255 255 255 / .05) 1px, transparent 1px);
  background-size: 34px 34px;
  mask-image: radial-gradient(ellipse 70% 70% at 50% 40%, #000 20%, transparent 75%);
}

.wm-project__category {
  position: absolute;
  inset-block-start: .75rem;
  inset-inline-start: .75rem;
  z-index: 2;
  padding: .25rem .5rem;
  border-radius: 3px;
  font-family: 'Mulish', sans-serif;
  font-size: .625rem;
  font-weight: 500;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-950);
  background-color: rgb(255 255 255 / .92);
  backdrop-filter: blur(6px);
}

/* --- Body ------------------------------------------------------------------- */

.wm-project__body {
  display: flex;
  flex-direction: column;
  gap: .5rem;
  padding: 1.25rem;
  flex: 1;
}

.wm-project__title {
  margin: 0;
  font-size: 1.125rem !important;
  font-weight: 600 !important;
  letter-spacing: -.011em;
  line-height: 1.3;
  color: var(--ink-950) !important;
}

.wm-project__title a { color: inherit !important; text-decoration: none; }

.wm-project__desc {
  margin: 0;
  font-size: .875rem;
  line-height: 1.6;
  color: var(--ink-600) !important;
  /* Clamped so a 48-word summary and a 9-word one produce the same card. */
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.wm-project__footer {
  margin-block-start: auto;
  padding-block-start: 1rem;
}

/* Arrow link rather than a gradient pill, matching the rest of the site. */
.wm-project__link {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  font-size: .875rem;
  font-weight: 560;
  text-decoration: none;
  color: var(--ink-950) !important;
  transition: color .15s ease;
}

.wm-project__link svg {
  width: 1em;
  height: 1em;
  transition: transform .25s cubic-bezier(.16, 1, .3, 1);
}

.wm-project__link:hover { color: var(--azure-700) !important; }
.wm-project__link:hover svg { transform: translateX(.25rem); }

/* One anchor covers the card; the visible link stays for screen readers. */
.wm-project__link::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
}

@media (prefers-reduced-motion: reduce) {
  .wm-projects *, .wm-projects *::before, .wm-projects *::after {
    transition-duration: .01ms !important;
  }
}
