/* ==========================================================
   ht-law / effects.css

   ht-kit: FX/RISE on section entry.
   PANELS family · Quiet tier · SCROLL input.

   ht-kit: FX/THRESHOLD on first load.
   PAGE family · Considered tier · LOAD input.

   ht-kit: FX/FACET on the home hero.
   PAGE family · Signature tier · AMBIENT input.

   THE SIGNATURE SLOT IS NOW SPENT, on FX/FACET. It was held
   open deliberately while this was a template, on the grounds
   that the signature is a per client decision. Putting the
   faceted field on the home hero spends it, and that was the
   call. Two consequences for anyone cloning this:

     1  no second Signature effect ships alongside it, and that
        includes all four page transitions, per kit rule 8
     2  a clone that wants its own signature replaces this one
        rather than adding to it

   FX/RISE is Quiet and FX/THRESHOLD is Considered, so neither
   conflicts. No page transition, so no transition base and no
   --dur-page-* tokens.
   ========================================================== */

/* ─ FX/RISE ─────────────────────────────────────────────────
   Staggered fade and rise on entry. Replaces the source's
   .reveal pattern with the same distance, duration and curve.

   Three ways this stays visible when the effect cannot run:

     1. Script disabled  <noscript> in every <head> unhides
                         instantly, and (scripting: none) below
                         covers the same case in CSS.
     2. Module missing   the safety keyframe reveals at 3s,
                         because js/modules/reveal.js never
                         stamped .rise-js on the root.
     3. Reduced motion   the block at the end of this file.

   The source failed all three. With script disabled it painted
   0 of 111 elements.
   ────────────────────────────────────────────────────────── */

.rise {
  opacity: 0;
  transform: translateY(var(--rise-y));
  transition:
    opacity   var(--dur-base) var(--ease),
    transform var(--dur-base) var(--ease);
  transition-delay: calc(var(--rise-i, 0) * var(--stagger));
}

.rise.is-in {
  opacity: 1;
  transform: translateY(0);
}

/* No will-change. The source set it on all 111 elements per
   page and never released it, holding a compositor layer for
   the whole session. Opacity and transform are composited
   anyway, so it bought nothing. */

/* ─ stagger ─────────────────────────────────────────────────
   Every delay in this composition is a whole multiple of
   --stagger, so these carry a multiplier rather than a
   duration. 2 = 40ms through 16 = 320ms.
   ────────────────────────────────────────────────────────── */

.rise-2  { --rise-i: 2;  }
.rise-3  { --rise-i: 3;  }
.rise-4  { --rise-i: 4;  }
.rise-6  { --rise-i: 6;  }
.rise-8  { --rise-i: 8;  }
.rise-9  { --rise-i: 9;  }
.rise-10 { --rise-i: 10; }
.rise-11 { --rise-i: 11; }
.rise-12 { --rise-i: 12; }
.rise-13 { --rise-i: 13; }
.rise-14 { --rise-i: 14; }
.rise-15 { --rise-i: 15; }
.rise-16 { --rise-i: 16; }

/* ─ fallbacks ─────────────────────────────────────────────── */

/* Module never ran. Reveal rather than strand the page. */
@keyframes rise-safety {
  to { opacity: 1; transform: translateY(0); }
}

html:not(.rise-js) .rise {
  animation: rise-safety 1ms linear 3s forwards;
}

@media (scripting: none) {
  .rise {
    opacity: 1;
    transform: none;
    transition: none;
    animation: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .rise,
  html:not(.rise-js) .rise {
    opacity: 1;
    transform: none;
    transition: none;
    animation: none;
  }
}

/* ─ FX/FACET ────────────────────────────────────────────────
   The home hero's field. PAGE family · Signature · AMBIENT.

   Overlapping sheets of tinted glass, drifting. Built from
   skewed panes, no image, no canvas, no build step.

   Vertical strips under skewY, so the left and right edges stay
   vertical and only the horizontals slant. That is the geometry
   doing the work: the boundaries between planes run straight
   down the field and the tops run diagonally across it.

   The top edge of every pane has to land INSIDE the viewport or
   the shear is invisible and the whole thing reads as vertical
   stripes. So each pane is far taller than it needs to be,
   hangs off the bottom, and is placed by its top edge alone.
   Those staggered top edges are the diagonals you see.

   Panes overlap horizontally on purpose. multiply is what turns
   an overlap into a facet: where two tints cross the tint
   compounds and an edge appears for free. The --lift panes work
   the other way, laying paper back over the field to carve out
   the planes that catch the light.

   Every tint is a color-mix percentage of --accent, so the
   whole field follows the client's brand colour and there is
   nothing to keep in step when it changes.
   ────────────────────────────────────────────────────────── */

.facet {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
}

.facet__wash {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(105deg,
      color-mix(in srgb, var(--accent) 10%, transparent) 0%,
      color-mix(in srgb, var(--accent) 4%,  transparent) 26%,
      color-mix(in srgb, var(--accent) 12%, transparent) 54%,
      color-mix(in srgb, var(--accent) 19%, transparent) 78%,
      color-mix(in srgb, var(--accent) 9%,  transparent) 100%);
}

.facet__pane {
  position: absolute;
  height: 190%;
  transform: skewY(-19deg) translate3d(0, 0, 0);
  mix-blend-mode: multiply;
  animation: facet-drift var(--drift-dur) var(--ease-drift)
             var(--drift-delay) infinite alternate;
}

/* Lays paper back over the field instead of tint, so the plane
   reads as catching the light rather than casting a shadow. */
.facet__pane--lift {
  mix-blend-mode: normal;
  background: var(--paper);
}

/* The eight panes. Seconds-scale durations are literal here for
   the same reason the 3s safety reveal above is: the token
   contract stops at --dur-slow, 520ms, and ambient motion is an
   order of magnitude slower than anything else on the site. */

.facet__pane--1 {
  left: -12%; width: 46%; top: -6%;
  background: color-mix(in srgb, var(--accent) 7%, transparent);
  --drift: 0.62%; --drift-dur: 19s; --drift-delay: -3s;
}

.facet__pane--2 {
  left: 17%; width: 39%; top: 15%;
  background: color-mix(in srgb, var(--accent) 4%, transparent);
  --drift: 0.44%; --drift-dur: 24s; --drift-delay: -11s;
}

.facet__pane--3 {
  left: 39%; width: 31%; top: -9%;
  background: color-mix(in srgb, var(--accent) 9%, transparent);
  --drift: 0.78%; --drift-dur: 16s; --drift-delay: -7s;
}

.facet__pane--4 {
  left: 54%; width: 35%; top: 25%;
  background: color-mix(in srgb, var(--accent) 5%, transparent);
  --drift: 0.5%;  --drift-dur: 27s; --drift-delay: -19s;
}

.facet__pane--5 {
  left: 69%; width: 43%; top: 3%;
  background: color-mix(in srgb, var(--accent) 11%, transparent);
  --drift: 0.68%; --drift-dur: 21s; --drift-delay: -14s;
}

.facet__pane--6 {
  left: 28%; width: 24%; top: 35%;
  opacity: 0.55;
  --drift: 0.55%; --drift-dur: 23s; --drift-delay: -5s;
}

.facet__pane--7 {
  left: 82%; width: 30%; top: 17%;
  opacity: 0.4;
  --drift: 0.72%; --drift-dur: 18s; --drift-delay: -9s;
}

.facet__pane--8 {
  left: 4%; width: 15%; top: 28%;
  opacity: 0.5;
  --drift: 0.4%;  --drift-dur: 29s; --drift-delay: -22s;
}

/* ─ the drift ───────────────────────────────────────────────
   The panes breathe, up and down, a few pixels each.

   Every period is awkward against its neighbours, 16 to 29
   seconds, and every pane starts partway through its own cycle
   on a negative delay. Both are there for one reason: if two
   panes share a period they eventually sync, and the moment
   they do the field stops reading as separate sheets and
   becomes one sheet moving. Nothing should ever repeat at a
   spacing a visitor could notice.

   The distance is a percentage of the pane's own height rather
   than a fixed length, so it stays proportional on a short
   laptop and a tall monitor, and so no pixel value has to live
   outside tokens.css. At 900px tall it is 6px to 13px.

   alternate, not a loop, so it eases into each turn instead of
   jumping back to the start. Transform only, per kit rule 4,
   and the skew is restated in the keyframes because a transform
   list replaces rather than merges.

   Measured: 60fps, 1.1% of pixels changing over twelve seconds,
   and zero change below the clear line, so nothing moves under
   anything anyone is reading.
   ────────────────────────────────────────────────────────── */

@keyframes facet-drift {
  from { transform: skewY(-19deg) translate3d(0, calc(var(--drift) * -1), 0); }
  to   { transform: skewY(-19deg) translate3d(0, var(--drift), 0); }
}

/* Clears the field back to paper before the type starts, so the
   headline and the baseline row sit on a flat page. This single
   gradient is what lets 120px of near-black type read over a
   busy background. */
.facet__clear {
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom,
    transparent 28%,
    color-mix(in srgb, var(--paper) 45%, transparent) 44%,
    color-mix(in srgb, var(--paper) 88%, transparent) 56%,
    var(--paper) 64%);
}

/* The drift stops dead rather than slowing down. It is ambient
   and continuous, which is the exact category of motion this
   preference exists to switch off, and a slower version of
   something that never stops still never stops. The panes hold
   at their resting position, which is identical to mid-cycle,
   so the field is unchanged and only the movement goes.

   The field itself stays. It is a background, not motion, and
   removing it would take the hero's design away from someone
   who asked for less movement rather than less design. */
@media (prefers-reduced-motion: reduce) {
  .facet__pane { animation: none; }
}

/* ─ FX/THRESHOLD ────────────────────────────────────────────
   First-load preloader. PAGE family · Considered tier · LOAD.

   Paper panel covers the view with the client's monogram on it,
   holds briefly, then lifts away upward. First page of a session
   only, gated on the ht-seen key the kit reserves for exactly
   this. Total is --dur-preload-hold plus --dur-preload-out, which
   must stay under 400ms.

   Not a page transition, and it does not take the Signature slot.
   A transition runs on every navigation, which is what makes it
   the most memorable thing on a site. This runs once per session.

   The arming direction is the whole safety story. CSS shows the
   panel only when BOTH .js and .is-preloading are on the root.
   Nothing on the page is ever hidden waiting to be revealed, so
   with script off, or the module missing, or storage unavailable,
   the site is simply the site. There is no state in which content
   exists but cannot be seen.

   The panel is a pseudo-element on the root, so it needs no markup
   in any page and can be painted before <body> is parsed. Its
   content is only declared inside the armed rule, so when the
   effect is not running the pseudo-element does not exist at all,
   rather than sitting there invisible over the whole viewport.
   ────────────────────────────────────────────────────────── */

html.js.is-preloading { overflow: hidden; }

html.js.is-preloading::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 9000;
  background: var(--accent);
  pointer-events: none;
}

/* The mark arrives rather than appearing. It rises a little, fades
   up and settles out of a slight under-scale, which is what stops
   the panel reading as a blank flash with a letter stamped on it. */
html.js.is-preloading::after {
  content: var(--mark);
  position: fixed;
  inset: 0;
  z-index: 9001;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-size: var(--step-mark);
  font-weight: var(--weight-medium);
  letter-spacing: var(--track-display);
  line-height: var(--lh-tight);
  color: var(--accent-ink);
  pointer-events: none;
  animation: ht-threshold-in var(--dur-preload-in) var(--ease-preload) both;
}

/* Transform and opacity only, per the kit's rule 4. */
html.js.is-preloading.is-clearing::before {
  animation: ht-threshold-out var(--dur-preload-out) var(--ease-preload) both;
}

/* The mark leaves slightly ahead of the panel, so the panel reads as
   lifting away from underneath it rather than the two sliding as one
   flat sheet. */
html.js.is-preloading.is-clearing::after {
  animation: ht-threshold-mark calc(var(--dur-preload-out) * 0.7)
             var(--ease-preload) both;
}

/* ─ the progress bar ────────────────────────────────────────
   Full width along the bottom edge, which is where a browser puts
   a loading bar and so needs no explaining. White fill on a white
   hairline track, both already the site's own steps for the accent
   field. Runs left to right across the hold and finishes exactly as
   the panel starts to lift. Only scaleX moves, so it stays inside
   the kit's rule 4.

   It lives on body rather than the root because the root's two
   pseudo-elements are already the panel and the mark. body is
   parsed a moment after the script stamps the root, which is why
   the panel is on the root and can cover from the first frame,
   while the bar arriving a frame later costs nothing.

   Pinned with left and right rather than a width, so the fill's
   transform is free to be nothing but scaleX. */

html.js.is-preloading body::before,
html.js.is-preloading body::after {
  content: "";
  position: fixed;
  z-index: 9002;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--bar-height);
  pointer-events: none;
}

html.js.is-preloading body::before { background: var(--accent-rule); }

html.js.is-preloading body::after {
  background: var(--accent-ink);
  transform-origin: 0 50%;
  animation: ht-threshold-bar var(--dur-preload-hold) var(--ease-bar) both;
}

/* The bar goes before the panel does, so the last thing you see
   lifting is the panel itself. */
html.js.is-preloading.is-clearing body::before,
html.js.is-preloading.is-clearing body::after {
  animation: ht-threshold-bar-out calc(var(--dur-preload-out) * 0.45)
             var(--ease) both;
}

@keyframes ht-threshold-bar {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

@keyframes ht-threshold-bar-out {
  from { opacity: 1; }
  to   { opacity: 0; }
}

@keyframes ht-threshold-in {
  from { opacity: 0; transform: translate3d(0, var(--rise-y), 0) scale(0.96); }
  to   { opacity: 1; transform: translate3d(0, 0, 0) scale(1); }
}

@keyframes ht-threshold-out {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(0, -100%, 0); }
}

@keyframes ht-threshold-mark {
  from { opacity: 1; transform: translate3d(0, 0, 0) scale(1); }
  to   { opacity: 0; transform: translate3d(0, calc(var(--rise-y) * -1.6), 0) scale(0.98); }
}

/* Skipped entirely, not merely shortened. The module returns
   before arming, and this is the belt behind that brace. */
@media (prefers-reduced-motion: reduce) {
  html.js.is-preloading,
  html.js.is-preloading::before,
  html.js.is-preloading::after,
  html.js.is-preloading body::before,
  html.js.is-preloading body::after {
    display: revert;
    animation: none !important;
    content: none !important;
    overflow: revert;
  }
}
