/* Puentech motion layer.

   Everything here is opt-in through a class that motion.js adds at runtime.
   That ordering matters: if scripting never runs, no element is ever hidden,
   so the page is fully readable with JavaScript off and there is no noscript
   rule to keep in sync. Only transform and opacity are animated, which are
   the two properties the compositor can handle without laying the page out
   again on every frame.

   Anyone who has asked their operating system to reduce motion gets the
   finished state immediately, not a shorter animation. */

@media (prefers-reduced-motion: no-preference) {

  /* ---- Reveal on scroll ----
     --i is the element's position within its group, set by motion.js, so
     siblings in a grid arrive one after another instead of together. The
     delay is capped in JS at six steps: past that a slow scroller is just
     waiting for text that is already on screen. */
  .rv {
    opacity: 0;
    transform: translateY(14px);
    transition:
      opacity 0.62s cubic-bezier(0.22, 0.75, 0.3, 1),
      transform 0.62s cubic-bezier(0.22, 0.75, 0.3, 1);
    transition-delay: calc(var(--i, 0) * 65ms);
    will-change: opacity, transform;
  }
  .rv.in {
    opacity: 1;
    transform: none;
    will-change: auto;
  }

  /* ---- Hero portrait ----
     A slow drift inward. It starts fractionally enlarged and settles, which
     reads as the photograph arriving rather than being pasted in. The
     duration is long enough that no frame of it looks like motion. */
  .portrait img {
    transform: scale(1.045);
    transition: transform 2.4s cubic-bezier(0.16, 0.8, 0.24, 1), opacity 0.8s ease;
  }
  .portrait.in img { transform: none; }

  /* ---- Images fade in once decoded ----
     .m-loaded is set by motion.js on the load event, so a photograph that
     arrives late resolves gently instead of snapping into place. */
  img[data-fade] { opacity: 0; transition: opacity 0.7s ease; }
  img[data-fade].m-loaded { opacity: 1; }

  /* ---- Hover and focus lift ----
     Small enough to feel like a response rather than an effect. Focus gets
     the same treatment so keyboard users see what the mouse user sees.
     The selectors name the ITEMS, not their containers: .offer and .work are
     grids, so lifting them would raise the whole block at once. */
  .offer > article, .work > article, .caps > article,
  .org, .ev > li {
    transition: transform 0.28s cubic-bezier(0.2, 0.7, 0.3, 1),
                border-color 0.28s ease,
                box-shadow 0.28s ease;
  }
  .offer > article:hover, .work > article:hover, .caps > article:hover,
  .org:hover, .ev > li:hover,
  .offer > article:focus-within, .work > article:focus-within,
  .caps > article:focus-within, .org:focus-within, .ev > li:focus-within {
    transform: translateY(-3px);
    border-color: var(--accent);
    /* Tinted rather than black: a pure black shadow on warm paper reads as
       grime. This one carries the page's own ink colour at low opacity. */
    box-shadow: 0 10px 26px -14px rgba(11, 91, 99, 0.4);
  }

  /* Buttons take a physical push on press. */
  .btn { transition: background 0.2s, border-color 0.2s, color 0.2s, transform 0.12s; }
  .btn:active { transform: translateY(1px); }
}

/* ---- The portrait itself, motion aside ----
   Outside the motion query because these are layout facts, not decoration.
   The width and height attributes on the tag reserve the space; height:auto
   lets the image keep its own proportions once CSS constrains the width,
   without which a 1050px-tall attribute would stretch it. */
.portrait img {
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  /* The photograph was taken at night, so it carries its own dark mass. A
     hair of extra contrast keeps it from going muddy against warm paper. */
  filter: saturate(1.03) contrast(1.02);
}
