/* <falling-blocks> — see assets/falling-blocks.js for the markup contract.

   Host-agnostic: no design tokens, no project class names, no assumption about the page
   around it. A host supplies the frame directory, and may set --fb-sticky-top to a fixed
   header's height and this element's height to change how long the hero pins. */

falling-blocks {
  --fb-sticky-top: 0px;
  /* The colour of the halo around the body copy (see [data-fb-front] below). A host
     sets it to its page colour; unset, there is no halo. */
  --fb-halo: transparent;
  /* Which encoded width this viewport scrubs. The element reads this back at boot and
     on every media-query flip, so the breakpoint that changes the page's layout is
     also the one that picks the file — the same arrangement approach-scrub uses for
     its cuts, and for the same reason: a second copy of the breakpoint in JS is free
     to drift from this one.

     The values name directories that exist under the element's base
     (w1440/, w720/ — see assets/falling-blocks/ and tools/encode-falling-blocks.mjs),
     so this file is coupled to what the encoder shipped. The phone tier exists because
     the arithmetic forces it: a 1440 frame pair decodes to 23.7 MiB however small the
     WebP is, so the default 128 MB budget holds five frames of forty-eight — measured
     on a throttled phone, every single draw during a scroll wanted a frame that was
     not decoded yet. At 720 the same budget holds twenty-one and each decode costs a
     quarter, which is the difference between the tumble playing and it juddering
     between stale frames. */
  --fb-tier: 1440;
  display: block;
  position: relative;
  /* Taller than the stage, and the difference is the pin: the copy is held still while
     the blocks rise through it, and the hero only starts scrolling away once they have
     all left. The element reads that difference as its scroll budget, so this one
     number sets both how long the hero holds and how fast the blocks move — there is
     no second place to keep in step.

     The plain-vh line is the fallback for engines without svh; svh rather than dvh
     because dvh changes as mobile chrome hides, and a budget that moved mid-gesture
     would snap the animation. */
  height: 290vh;
  height: 290svh;
}

/* Static presentation, which is also what a page with no JavaScript gets: one screen
   and no pin, so nobody scrolls past two empty ones to reach the next section. */
falling-blocks:not([data-fb-motion="on"]) {
  height: 100vh;
  height: 100svh;
}

/* Phones scrub the small tier. 900px rather than the design system's 991 because the
   element's own former still-image cutoff was 901, and keeping the same line means
   this change swaps what those viewports get without moving where anything changes.
   The short-viewport arm catches large phones rotated: an iPhone Pro Max is 926 CSS px
   wide in landscape, over the width line, and was measured scrubbing the 1440 tier
   there.

   The budget travels with the tier, and is smaller for a reason that is easy to get
   backwards: a byte ceiling alone buys MORE frames when each one gets cheaper. At the
   attribute's 128 MB the phone held 17 pairs — 100.9 MiB of decoded bitmaps, more
   than the desktop's 94.9 — on the device least able to afford it. 48 MB holds 8
   pairs, ~47 MiB, still a wider window than the desktop's five. */
@media (max-width: 900px), (max-height: 500px) {
  falling-blocks { --fb-tier: 720; --fb-budget: 48; }
}

falling-blocks [data-fb-stage] {
  position: sticky;
  top: var(--fb-sticky-top);
  /* A floor, not a height. The copy fits a screen at every width at the page's own
     spacing, so this is exactly the old fixed height — until a reader raises line or
     letter spacing (WCAG 1.4.12). Then the copy outgrows a screen, and a fixed height
     with overflow: hidden cut the paragraph and buttons off at 375px (measured). Now
     the stage grows with its copy and only the plates are clipped to it. */
  min-height: calc(100vh - var(--fb-sticky-top));
  min-height: calc(100svh - var(--fb-sticky-top));
  overflow: hidden;
  display: grid;
  place-items: center;
  /* The plates are absolutely positioned inside this box and never affect layout, so
     nothing here can shift the page as frames arrive — the section's height is fixed
     by the rule above before any script runs. contain:paint also makes this the
     stacking context the z-indexes below are resolved against. */
  contain: layout paint;
}

/* Both layers read the same two properties, written once per tick by the element.
   There is one box, so the two depth planes cannot drift out of registration. */
falling-blocks [data-fb-layer] {
  position: absolute;
  left: 50%;
  top: 0;
  width: var(--fb-plate-w, 100%);
  height: var(--fb-plate-h, 150%);
  margin-left: calc(var(--fb-plate-w, 100%) / -2);
  will-change: transform;
  /* The near plate covers the copy, including any button in it. */
  pointer-events: none;
}

falling-blocks [data-fb-layer] > canvas,
falling-blocks [data-fb-layer] > img {
  display: block;
  width: 100%;
  height: 100%;
}

/* The sandwich, and it cuts through the copy rather than around it: the near plate
   passes in front of the heading but behind the body and the buttons. Those are the
   two things that have to stay readable and clickable, and the heading is large enough
   to read through a block crossing it.

   [data-fb-copy] is deliberately positioned but has no z-index of its own — that makes
   it a containing block without making it a stacking context, so the heading and
   [data-fb-front] inside it resolve against the stage and can sit on either side of a
   plate. Give this element a z-index and the whole copy collapses to one layer again.

   Ordering is by z-index rather than DOM order so the element never has to move
   authored nodes around; a framework that re-renders the copy has nothing of ours to
   reconcile away. */
falling-blocks [data-fb-layer="bottom"] { z-index: 1; }
falling-blocks [data-fb-copy]           { position: relative; }
falling-blocks [data-fb-copy] h1        { position: relative; z-index: 2; }
falling-blocks [data-fb-layer="top"]    { z-index: 3; }
falling-blocks [data-fb-front]          { position: relative; z-index: 4; }

/* The body copy stays in front of every plate, but in front is not the same as
   readable: where dark text crosses the blue block, the pixels around its glyphs
   measured 1.46:1 at 1440 and 3.18:1 at 375 against the 4.5:1 it needs (1.4.3). A halo in the page colour, stacked tight around each glyph, gives the
   text its own background wherever a block passes behind it and is invisible
   everywhere else. On the paragraph only — the buttons carry their own fill. */
falling-blocks [data-fb-front] p {
  text-shadow:
    0 0 1px var(--fb-halo), 0 0 1px var(--fb-halo), 0 0 2px var(--fb-halo),
    0 0 2px var(--fb-halo), 0 0 3px var(--fb-halo), 0 0 4px var(--fb-halo);
}

/* Frame 1 is the designed composition, so the still is a deliberate state rather than
   a degraded one. It is a plain <img>, which means it also survives with scripting
   off and gives the static path a real LCP element instead of a canvas. */
falling-blocks [data-fb-layer] > canvas { display: none; }
falling-blocks[data-fb-motion="on"] [data-fb-layer] > canvas { display: block; }
falling-blocks[data-fb-motion="on"] [data-fb-layer] > img { display: none; }

/* Without the element running there is no cover-fit to apply, so the layer fills the
   stage and the image covers it. Biased above centre because both plates carry their
   blocks in the upper-middle band of the plate. */
falling-blocks:not([data-fb-motion="on"]) [data-fb-layer] {
  left: 0;
  width: 100%;
  height: 100%;
  margin-left: 0;
  transform: none;
}
falling-blocks:not([data-fb-motion="on"]) [data-fb-layer] > img {
  object-fit: cover;
  object-position: 50% 38%;
}
