/* =========================================================================
   NextEdge — Portfolio (renders.html)
   Light, image-first gallery in the spirit of the reference Ralph picked
   (bloom3d.studio/projects): near-full-bleed tiles with hairline gaps,
   alternating rows of two and three, small captions overlaid on the image,
   and quiet, expensive-feeling motion. No hierarchy, no dark room.

   Scoped under .projects-page. Loaded only by renders.html.
   ========================================================================= */

.projects-page {
  --p-gap: 4px;                     /* hairline gutters, like the reference */
  --p-ink: var(--c-text);
  --p-ink-dim: rgba(29, 29, 29, .55);
  --p-ink-faint: rgba(29, 29, 29, .34);
  --p-rule: rgba(29, 29, 29, .12);
  --p-ease: cubic-bezier(.22, .8, .3, 1);

  background: var(--c-bg);
  color: var(--p-ink);
}

/* Small uppercase utility type used for counts, chips and modal metadata. */
.projects-page .p-spec {
  font-size: .72rem;
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--p-ink-dim);
}

/* ---------- Page head ------------------------------------------------------ */

.p-head {
  padding: clamp(2.5rem, 7vh, 5.5rem) var(--gutter) clamp(1.5rem, 3vh, 2.5rem);
}

.p-head__row {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 2rem;
  flex-wrap: wrap;
}

.p-head__h {
  font-family: var(--f-display);
  text-transform: uppercase;
  font-size: clamp(3rem, 7.5vw, 6.5rem);
  line-height: .92;
  letter-spacing: .01em;
  /* load reveal: rises out of its own clip box */
  overflow: hidden;
}

.p-head__h span {
  display: inline-block;
  transform: translateY(115%);
  animation: p-rise 1s var(--p-ease) .95s forwards;
}

@keyframes p-rise { to { transform: translateY(0); } }

@keyframes p-fade-up {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}

/* ---------- Filter rail ---------------------------------------------------- */

.p-rail {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.25rem;
  flex-wrap: wrap;
  padding: 1rem var(--gutter);
  opacity: 0;
  animation: p-fade-up .9s var(--p-ease) 1.12s forwards;
}

.p-rail__filters {
  display: flex;
  gap: clamp(1rem, 2.4vw, 2rem);
  flex-wrap: wrap;
}

.p-filter {
  appearance: none;
  background: none;
  border: 0;
  padding: .35rem 0;
  cursor: pointer;
  font-size: .9rem;
  font-weight: 500;
  color: var(--p-ink-dim);
  position: relative;
  transition: color .3s ease;
}

.p-filter sup {
  font-size: .62rem;
  font-weight: 400;
  margin-left: .3em;
  opacity: .65;
}

.p-filter::after {
  content: "";
  position: absolute;
  left: 0; right: 100%; bottom: 0;
  height: 1.5px;
  background: currentColor;
  transition: right .45s var(--p-ease);
}

.p-filter:hover { color: var(--p-ink); }
.p-filter.is-active { color: var(--p-ink); }
.p-filter.is-active::after { right: 0; }

/* ---------- The grid -------------------------------------------------------
   Justified rows: every image keeps its native aspect ratio, uncropped.
   Within a row each tile's flex-grow AND aspect-ratio are the same number
   (--r = width/height), which makes all tiles in a row share one height
   while the row fills the page edge-to-edge. Rows of two read larger than
   rows of three, so the 2/3 rhythm survives without any cropping.          */

.p-grid {
  display: flex;
  flex-direction: column;
  gap: var(--p-gap);
  padding: 0 var(--p-gap) var(--p-gap);
}

.p-row {
  display: flex;
  gap: var(--p-gap);
}

.p-tile {
  flex: var(--r, 1.5) 1 0;
  aspect-ratio: var(--r, 1.5);
  min-width: 0;
  position: relative;
  display: block;
  padding: 0;
  border: 0;
  background: #e7e6e4;
  cursor: pointer;
  overflow: hidden;
  text-align: left;
  color: #fff;
  /* scroll reveal */
  opacity: 0;
  transform: translateY(26px);
  transition: opacity .9s var(--p-ease), transform .9s var(--p-ease);
  transition-delay: var(--d, 0s);
}

.p-tile.is-in { opacity: 1; transform: none; }

.p-tile__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* The box ratio equals the image ratio, so cover crops nothing — it only
     absorbs sub-pixel rounding that would otherwise leave hairline gaps. */
  object-fit: cover;
  transform: scale(1.12);
  transition: transform 1.3s var(--p-ease);
}

.p-tile.is-in .p-tile__img { transform: scale(1); }

@media (hover: hover) {
  .p-tile:hover .p-tile__img,
  .p-tile:focus-visible .p-tile__img { transform: scale(1.045); }
}

.p-tile:focus-visible { outline: 2px solid var(--p-ink); outline-offset: 3px; }

/* Caption overlaid bottom-left over a soft legibility scrim. */
.p-tile__scrim {
  position: absolute;
  inset: 55% 0 0;
  background: linear-gradient(to top, rgba(0, 0, 0, .48), rgba(0, 0, 0, 0));
  opacity: .85;
  transition: opacity .5s ease;
  pointer-events: none;
}

.p-tile:hover .p-tile__scrim { opacity: 1; }

.p-tile__cap {
  position: absolute;
  left: clamp(.9rem, 1.6vw, 1.4rem);
  right: clamp(.9rem, 1.6vw, 1.4rem);
  bottom: clamp(.8rem, 1.5vw, 1.25rem);
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  text-shadow: 0 1px 14px rgba(0, 0, 0, .35);
}

.p-tile__title {
  font-size: clamp(.95rem, 1.15vw, 1.1rem);
  font-weight: 500;
  letter-spacing: .01em;
}

.p-tile__meta {
  font-size: clamp(.74rem, .9vw, .84rem);
  font-weight: 400;
  color: rgba(255, 255, 255, .78);
  white-space: nowrap;
}

.p-grid__empty {
  grid-column: 1 / -1;
  padding: clamp(3rem, 8vw, 6rem) 0;
  text-align: center;
  color: var(--p-ink-dim);
}

.p-grid__empty.is-hidden { display: none; }

@media (max-width: 900px) {
  /* Stack: one full-width image per row, still at its native ratio. */
  .p-row { flex-direction: column; }
  .p-tile { flex: none; width: 100%; }
  .p-head__row { align-items: flex-start; flex-direction: column; gap: 1rem; }
}

/* ---------- Case-study modal (light) --------------------------------------- */

.p-modal {
  position: fixed;
  inset: 0;
  z-index: 220;
  display: none;
  background: var(--c-bg);
  color: var(--p-ink);
}

.p-modal.is-open { display: block; animation: p-modal-in .45s var(--p-ease) both; }

@keyframes p-modal-in {
  from { opacity: 0; transform: translateY(1.25%); }
  to   { opacity: 1; transform: none; }
}

.p-modal__bar {
  position: absolute;
  top: 0; left: 0; right: 0;
  z-index: 3;
  height: 64px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 var(--gutter);
  background: linear-gradient(to bottom, rgba(245, 245, 245, .96) 30%, rgba(245, 245, 245, 0));
  pointer-events: none;
}

.p-modal__bar > * { pointer-events: auto; }

.p-modal__nav { display: flex; gap: .4rem; }

.p-modal__btn {
  appearance: none;
  background: transparent;
  border: 1px solid var(--p-rule);
  color: var(--p-ink-dim);
  font-size: .72rem;
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  padding: .55rem .9rem;
  cursor: pointer;
  transition: color .25s ease, border-color .25s ease;
}

.p-modal__btn:hover:not(:disabled) { color: var(--p-ink); border-color: var(--p-ink-faint); }
.p-modal__btn:disabled { opacity: .3; cursor: default; }

.p-modal__body {
  height: 100%;
  display: grid;
  grid-template-columns: minmax(0, 1fr) clamp(258px, 20vw, 320px);
  grid-template-rows: auto minmax(0, 1fr);
}

.p-modal__head {
  grid-column: 2;
  grid-row: 1;
  border-left: 1px solid var(--p-rule);
  padding: 76px clamp(1.5rem, 2.2vw, 2.25rem) 1.2rem;
  display: grid;
  gap: .55rem;
}

.p-modal__plate { color: var(--p-ink-faint); }

.p-modal__title {
  font-family: var(--f-display);
  text-transform: uppercase;
  font-size: clamp(1.6rem, 2.4vw, 2.3rem);
  line-height: .98;
  letter-spacing: .015em;
}

.p-modal__detail {
  grid-column: 2;
  grid-row: 2;
  border-left: 1px solid var(--p-rule);
  padding: 0 clamp(1.5rem, 2.2vw, 2.25rem) clamp(1.75rem, 3.5vh, 2.5rem);
  overflow-y: auto;
  overscroll-behavior: contain;
  display: flex;
  flex-direction: column;
  gap: clamp(1.25rem, 2.6vh, 1.85rem);
}

.p-modal__desc {
  color: var(--p-ink-dim);
  font-size: .92rem;
  line-height: 1.65;
}

.p-modal__dl { display: grid; gap: 1.2rem; }

.p-modal__dl h4 {
  font-size: .68rem;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--p-ink-faint);
  padding-bottom: .55rem;
  margin-bottom: .6rem;
  border-bottom: 1px solid var(--p-rule);
}

.p-modal__dl ul { display: grid; gap: .35rem; }

.p-modal__dl li {
  font-size: .88rem;
  color: var(--p-ink-dim);
  padding-left: 1rem;
  position: relative;
}

.p-modal__dl li::before {
  content: "";
  position: absolute;
  left: 0;
  top: .62em;
  width: 6px;
  height: 1px;
  background: var(--p-ink-faint);
}

/* Sits directly under the title block — with the prose gone there is nothing
   to push it to the bottom of the column against. */
.p-modal__cta {
  display: block;
  width: 100%;
  appearance: none;
  border: 0;
  cursor: pointer;
  background: var(--c-text);
  color: var(--c-text-light);
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  padding: 1.05rem .75rem;
  text-align: center;
  text-wrap: balance;
  transition: opacity .25s ease, transform .25s ease;
}

.p-modal__cta:hover { opacity: .88; transform: translateY(-1px); }

/* Keyboard-shortcut hint. Hidden wherever there is no hover-capable pointer —
   a touch device has no Esc key, and the ← → Close buttons are already on
   screen, so the line would be both wrong and redundant. */
.p-modal__hint { color: var(--p-ink-faint); text-align: center; }

@media (hover: none) {
  .p-modal__hint { display: none; }
}

.p-modal__stream {
  grid-column: 1;
  grid-row: 1 / 3;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 64px 0 clamp(3rem, 8vh, 6rem);
  scroll-behavior: smooth;
}

.p-modal__fig {
  margin: 0;
  opacity: 0;
  animation: p-plate-in .8s var(--p-ease) both;
}

.p-modal__fig + .p-modal__fig { margin-top: clamp(1.25rem, 3vh, 2.25rem); }

/* Every plate fits the screen whole — the frame carries the image's exact
   aspect ratio (--ar, set inline by JS) and is sized so it can never be
   taller than the viewport nor wider than the column. Because the box has
   a definite size before the image loads, scroll offsets are stable and
   the image can simply fill it with no distortion. */
.p-modal__frame {
  display: block;
  aspect-ratio: var(--ar, 1.5);
  width: min(100%, calc((100svh - 150px) * var(--ar, 1.5)));
  margin: 0 auto;
  background: #e7e6e4;
}

.p-modal__frame img {
  display: block;
  width: 100%;
  height: 100%;
}

.p-modal__fig figcaption {
  text-align: center;
  padding: .65rem var(--gutter) 0;
}

@keyframes p-plate-in {
  from { opacity: 0; transform: translateY(18px); }
  to   { opacity: 1; transform: none; }
}

@media (max-width: 900px) {
  /* Flex column so DOM order rules: title → renders → specification. */
  .p-modal__body {
    display: flex;
    flex-direction: column;
    height: 100%;
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  .p-modal__head {
    border-left: 0;
    padding: 76px var(--gutter) .9rem;
  }
  .p-modal__stream {
    overflow: visible;
    padding: 0 0 1.5rem;
    height: auto;
  }
  .p-modal__detail {
    border-left: 0;
    border-top: 1px solid var(--p-rule);
    padding: 1.5rem var(--gutter) clamp(2.25rem, 7vh, 3.5rem);
    overflow: visible;
  }
  .p-modal__cta { margin-top: .4rem; }
  .p-modal__frame { width: min(100%, calc(80svh * var(--ar, 1.5))); }
  /* Second trigger alongside (hover: none): at this width the ← → Close
     buttons are the only sane way to navigate anyway. */
  .p-modal__hint { display: none; }
}

body.modal-open { overflow: hidden; }

/* ---------- Motion preferences --------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  .projects-page *,
  .projects-page *::before,
  .projects-page *::after {
    animation-duration: .01ms !important;
    animation-delay: 0s !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
  .p-tile { opacity: 1; transform: none; }
  .p-tile__img { transform: none; }
  .p-head__h span { transform: none; }
  .p-rail { opacity: 1; }
}
