/* ════════════════════════════════════════════════════════════════════════
   css/components/contactsheet.css — THE EXPOSURE PASS
   service.tnaado.ca · reusable · pairs with js/contactsheet.js

   PORTED from the old live site's .pkit exposure rules in
   css/prod-showcase.css, generalised off that page's class names onto data
   attributes so it can wrap any photograph on any page.

   WHAT IT IS. A red edge sweeps across a photograph and the frame is
   uncovered behind it, like a pass over a contact sheet, while the picture
   settles from a slight overscan to rest.

   ── IT ADDS NO HEIGHT AND MOVES NO PHOTOGRAPH ──────────────────────────
   The only structural requirement is that the frame is a positioned,
   overflow-hidden box around the picture — which a <figure> already is once
   these two lines land on it. The sweeping edge is a pseudo-element,
   absolutely positioned, so it contributes nothing to layout. Measure the
   section before and after: it should be identical to the pixel. If it is
   not, something in the page's own CSS is fighting the overflow, and that is
   the thing to fix, not this file.

   ── NOTHING HERE MAY HIDE A PHOTOGRAPH ─────────────────────────────────
   There is no opacity and no cover in the pre-reveal state, deliberately and
   permanently. See the header of js/contactsheet.js for the production
   failure that rule comes from. The un-armed state is a fully visible
   photograph at a 1.04 overscan; the armed state is the same photograph at
   rest. Every state in this file is a finished, readable state.

   ── EVERY PRE-REVEAL STATE IS SCOPED UNDER .is-live ────────────────────
   which js/contactsheet.js adds only after confirming script,
   IntersectionObserver and no reduced-motion preference. No script, no
   observer, or reduced motion → the class never lands, none of these
   selectors match, and the section renders finished and static.
   ════════════════════════════════════════════════════════════════════════ */

[data-sheet] {
  --cs-red:  var(--red, #c8102e);
  /* The iOS drawer curve. Long, decisive, and it settles rather than stops —
     which is what makes the picture read as landing instead of sliding. */
  --cs-ease: cubic-bezier(0.32, 0.72, 0, 1);
  --cs-step: 70ms;
}

/* Present with or without motion: it is the picture's clipping box, and the
   overscan it clips only exists while the reveal is running. Declared
   unscoped so the box is correct on first paint, before .is-live lands —
   otherwise the first armed frame would briefly overflow its own figure. */
[data-sheet-frame] {
  position: relative;
  overflow: hidden;
  min-width: 0;
}

/* Stagger across the frames, on static selectors. Not written from JS: a
   custom property set on a parent recalculates every descendant, and these
   never change after parse. Six is enough for any strip that should be a
   strip; a seventh frame simply shares the sixth's delay, which is correct —
   past about half a second the stagger stops reading as a sequence anyway. */
[data-sheet-frame] { --cs-d: 0ms; }
[data-sheet-frame]:nth-child(2) { --cs-d: calc(var(--cs-step) * 1); }
[data-sheet-frame]:nth-child(3) { --cs-d: calc(var(--cs-step) * 2); }
[data-sheet-frame]:nth-child(4) { --cs-d: calc(var(--cs-step) * 3); }
[data-sheet-frame]:nth-child(5) { --cs-d: calc(var(--cs-step) * 4); }
[data-sheet-frame]:nth-child(n + 6) { --cs-d: calc(var(--cs-step) * 5); }

/* ── the sweeping edge ────────────────────────────────────────────────────
   A 2px red border on the leading side of a full-bleed transparent overlay,
   translated across the frame by its own width. Transform only — no width
   animation, no left offset — so it composites off the main thread and never
   triggers layout. */
[data-sheet].is-live [data-sheet-frame]::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  border-inline-start: 2px solid var(--cs-red);
  transform: translateX(0);
  transition: transform .9s var(--cs-ease) var(--cs-d);
}

/* The picture settles from overscan. 1.3s against the edge's 0.9s on purpose:
   the edge finishes its pass and the frame is still coming to rest behind it,
   which is what stops the two reading as one rigid animation. 1.04, not the
   original's 1.05 — the frames in this build are tighter crops and 1.05 was
   visibly clipping subject at the edges of the portrait frames. */
[data-sheet].is-live [data-sheet-frame] img {
  transform: scale(1.04);
  transition: transform 1.3s var(--cs-ease) var(--cs-d);
  will-change: transform;
}

[data-sheet].is-live [data-sheet-frame].is-armed::after { transform: translateX(100%); }
[data-sheet].is-live [data-sheet-frame].is-armed img    { transform: scale(1); }

/* Released once the frame is at rest so a long page is not holding a
   compositor layer per photograph for the rest of the session. */
[data-sheet].is-live [data-sheet-frame].is-armed img { will-change: auto; }

/* Explicit kill. js/contactsheet.js already returns before adding .is-live
   under reduced motion, so in practice none of the selectors above can match.
   This is the belt to that braces, and it is what the next person greps for. */
@media (prefers-reduced-motion: reduce) {
  [data-sheet].is-live [data-sheet-frame]::after { display: none; }
  [data-sheet].is-live [data-sheet-frame] img {
    transform: none;
    transition: none;
    will-change: auto;
  }
}
