/* =====================================================================
   perf.css — Core Web Vitals fixes. No visual change at any width.

   Everything here either reserves space that JavaScript is about to fill
   (so the page stops reflowing after load) or reduces needless compositor
   work. Nothing changes a colour, size, position or font.

   Load AFTER chrome.css and BEFORE mobile.css.
   ===================================================================== */

/* ---------------------------------------------------------------------
   1. CLS — reserve height for sections that start empty.

   #photoGrid, #siteFooter and the project page body are empty elements in
   the HTML that JS fills after load, so the page rendered short and then
   jumped. These floors are set at the *minimum* the finished section
   occupies, so they never stretch anything: once the real content is in,
   it is taller than the floor and the floor stops mattering.
   --------------------------------------------------------------------- */
#siteFooter { min-height: 320px; }
#photoGrid { min-height: 300px; }
#editorProjectPage,
#projectBody { min-height: 70vh; }
#projectsGrid { min-height: 380px; }
#tScroll { min-height: 260px; }
#logosTrack { min-height: 56px; }

@media (max-width: 760px) {
  #siteFooter { min-height: 400px; }
  #photoGrid { min-height: 520px; }
  #projectsGrid { min-height: 300px; }
  #tScroll { min-height: 300px; }
}

/* ---------------------------------------------------------------------
   2. CLS — images that know their own shape before they load.

   Every <img> the site injects now carries width/height attributes taken
   from the real file (see the imageSizes map in the editor). The browser
   derives an aspect ratio from those and reserves the right box up front.
   These rules make sure the attributes never override the CSS sizing that
   was already there — the picture still fills its frame exactly as before.
   --------------------------------------------------------------------- */
.photo-img img,
.work-thumb img,
.work-cta-media img,
.case-slot img,
.blog-card-cover img,
.blog-hero-cover img,
.contact-scene img,
.logo img,
.t-avatar {
  max-width: 100%;
}
.photo-img img,
.work-thumb img,
.work-cta-media img,
.case-slot img,
.blog-card-cover img,
.blog-hero-cover img {
  height: 100%;
}
.contact-scene img { height: auto; }

/* The sofa is a fixed piece of art; pin its ratio so the scene reserves
   its box before the PNG arrives. */
.contact-scene { aspect-ratio: 1920 / 900; }
@media (max-width: 760px) {
  .contact-scene { aspect-ratio: auto; }
}

/* Decorative clouds are absolutely positioned, so they cannot shift the
   page — but giving them a ratio stops each one resizing itself mid-drift. */
.cloud-img { aspect-ratio: 1 / 1; height: auto; }

/* ---------------------------------------------------------------------
   3. INP — stop promoting elements that are not animating.

   `will-change` was set permanently on every polaroid and every cloud.
   That forces a GPU layer per element for the whole session, and with 40
   hero tiles also compositing, a tap took over a second to produce its
   next frame. Promote on hover only, which is when the transform runs.
   --------------------------------------------------------------------- */
.photo { will-change: auto; }
.photo:hover,
.photo:focus-within { will-change: transform; }

/* Tiles only need a layer while a flip is actually running.

   Promotion has to happen BEFORE the transform starts, not at the same moment
   — a layer created on the first animated frame is exactly what produced the
   hitch on the opening ripple. Each diagonal band is armed one frame ahead of
   its own flip and disarmed when that flip is done, so only the handful of
   tiles actually in motion is ever promoted. */
.tile { will-change: auto; }
.tile.flip-armed,
.tile:hover { will-change: transform; }

/* The sprite carries a drop-shadow filter. A filter inside a 3D-rotating
   parent has to be re-blurred on every frame, and with a whole diagonal band
   turning at once that is what dropped frames. The shadow is invisible while a
   tile is edge-on anyway, so armed tiles go without it for the length of their
   flip. Hover flips are one tile at a time and keep it throughout. */
.tile.flip-armed .tile-sprite { filter: none; }

/* Same problem on the artboard: 50 cards flying in, each with a drop-shadow
   being reblurred every frame. The shadow arrives as the card lands. */
.bx--enter { filter: none; }

/* Isolate each card's painting so one tile's flip cannot invalidate the
   whole grid. */
/* NOT on .tile: the tiles use transform-style: preserve-3d, and
   `contain: paint` forces a stacking context that flattens 3D, which both
   breaks the flip and forces a re-rasterise every frame. */
.photo,
.bx,
.t-card {
  contain: layout paint;
}

/* Faces are decoration; let clicks fall straight through to the tile so
   hit-testing does not walk the subtree. */
.tile .face,
.tile .face * {
  pointer-events: none;
}

/* ---------------------------------------------------------------------
   4. Skip rendering work for sections far below the fold. The
      contain-intrinsic-size values match the min-heights above, so
      scrollbar length stays correct.
   --------------------------------------------------------------------- */
.testimonials,
.logos {
  content-visibility: auto;
  contain-intrinsic-size: auto 400px;
}

/* ---------------------------------------------------------------------
   5. Older / low-powered devices.

   perf.js sets data-low-power on <html> when the device reports few cores,
   little memory, a slow connection or data saver. Only MOTION is reduced —
   every image is still served at full quality and full resolution.
   --------------------------------------------------------------------- */
[data-low-power="true"] .cloud-cover {
  animation-duration: 1ms;
  animation-delay: 0ms;
}
[data-low-power="true"] .bx--enter {
  animation: none;
  opacity: 1;
}
[data-low-power="true"] .tile {
  transition-duration: 420ms;
}
[data-low-power="true"] .tile .front::after { display: none; }
[data-low-power="true"] .photo,
[data-low-power="true"] .bx {
  filter: none;
  box-shadow: 0 8px 16px -6px rgba(10, 30, 60, 0.42);
}
[data-low-power="true"] .contact-scene,
[data-low-power="true"] .pixel-side {
  display: none;
}

/* Anyone who has asked their system to reduce motion gets the same calm
   treatment for the two heaviest sequences. */
@media (prefers-reduced-motion: reduce) {
  .bx--enter { animation: none; opacity: 1; }
  .tile.flip-armed { will-change: auto; }
}
