/* <hero-bridge> — the styles the component's markup contract depends on. See
   assets/hero-bridge.js for the contract itself.

   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 --hb-pin to a fixed
   header's height and --hb-entry-clear to however much room its own copy needs.

   Everything geometric lives here rather than in JS, including the six things JS reads
   back: --hb-variant tells the element which cut of the plate to load, --hb-budget how
   much decoded image to hold, --hb-entry-span how much scroll the approach costs,
   --hb-entry-clear how much room the host's copy needs, and --hb-entry-sky / --hb-entry-keep
   describe the artwork the entry is composed against. So the breakpoint that sizes the
   stage is also the one that picks the file, sizes the memory and composes the entry, and
   there is no second copy of any of it in the script. */

/* THE PLATE IS NEVER CLIPPED BY A BOX. THAT IS THE WHOLE ARRANGEMENT.

   The plate is 2048x1432, or 1.4302:1, and it runs edge to edge — so on any desk screen it
   is taller than the screen under the header: 1007px against 828px at 1440x900, 1342px
   against 1008px at 1920x1080. Something has to give, and there are only two ways to give.

   The previous rig cropped it: overflow:hidden on the stage, with --hb-anchor placing the
   window. That looks right while the stage is pinned, because the cut sits exactly on the
   bottom edge of the screen where nobody can see it. It stops looking right the instant the
   stage releases — the clip rectangle then walks up the screen, and a hard horizontal line
   crosses the ground shadow on the way out. Cropping by the VIEWPORT is invisible; cropping
   by a BOX is an edge, and only one of those is acceptable here.

   So the stage takes the plate's own height instead of the screen's. The plate is in flow
   and gives the stage that height, nothing anywhere clips, and the bottom of the picture is
   simply below the fold while the arch builds — 18% of it at 1440x900, 25% at 1920x1080.
   Scrolling on reveals the feet and the shadow, in one piece, as the section leaves.

   What that costs is the desk's feet and the rack's base during the scrub: content below
   y 0.82 of the plate is off-screen while pinned at 1440x900. That is deliberate and was
   chosen over side margins — the alternative, fitting the whole plate into the screen,
   leaves the artwork 1184px wide inside a 1440px viewport with 128px of page colour each
   side, and this hero is meant to run edge to edge.

   Below about 5/4 the plate is shorter than the screen and none of this applies: it is
   simply centred in a stage a screen tall, with no crop of any kind to reconcile. There is
   no media query for that case any more, because there is nothing left for it to say. */

/* The lengths here have to be readable from script as PIXELS, and a plain custom
   property computes to its token text — "calc(100svh - 4.5rem)" — not to a length.
   Registering them is what makes getComputedStyle resolve the calc, so the entry can be
   written here against the heights it is derived from rather than as constants in the
   script that would be free to drift from them. Every engine that supports
   mix-blend-mode: plus-lighter, which this component already requires for its cross-fade,
   supports @property; hero-bridge.js has a fallback for each where it is missing.

   --hb-max is registered for the same reason and it is newly load-bearing: settle() now
   reads it, because the entry may scale the plate UP and the ceiling on how wide the
   artwork may be drawn has to apply to the drawn width and not only to the box. --hb-max
   is a plain length today, so parseFloat would cope — registering it means a host may
   write it as a calc, like everything else here, without silently breaking the entry. */
@property --hb-entry-span { syntax: "<length>"; inherits: true; initial-value: 0px; }
@property --hb-entry-clear { syntax: "<length>"; inherits: true; initial-value: 0px; }
@property --hb-entry-zoom { syntax: "<number>"; inherits: true; initial-value: 1; }
@property --hb-max { syntax: "<length>"; inherits: true; initial-value: 2880px; }
@property --hb-scrub { syntax: "<length>"; inherits: true; initial-value: 0px; }
@property --hb-hold { syntax: "<length>"; inherits: true; initial-value: 0px; }
@property --hb-exit { syntax: "<length>"; inherits: true; initial-value: 0px; }

hero-bridge {
  /* Where the stage pins. One number drives the sticky offset, the stage's height, the
     approach's length and the element's padding, and the element reads it back off the
     stage — so a host with a taller header, or none, sets this and nothing else. */
  --hb-pin: 0px;
  --hb-variant: "";

  /* THE WIDTH CEILING, and it is now two ceilings written as one number. The box runs edge
     to edge below this and stops growing above it, which is what keeps a very wide screen
     from stretching the artwork across a metre of desk. It is above every common width on
     purpose — 2560 and 3440 panels both take the full viewport — so as a BOX width it only
     bites on a 5K.

     It also caps the width the plate is DRAWN at, which is a different thing since
     --hb-entry-zoom below: settle() reads this back and will not scale the plate past it,
     so on a 1920 screen the entry stops at 1.50 rather than the 1.81 the geometry would
     allow. That is a resolution limit rather than a compositional one. The shipped cut is
     1600px, so this ceiling is a 1.8x upscale — the figure docs/hero-bridge-render.md
     already names as the point where a 2048 cut starts to be worth cutting. Raising this
     without raising the cut just makes the entry soft, and this render has grille mesh,
     cable bundles and LED rows in it. Note also that the sequence masters are NOT in this
     repository's history, only nine beat plates at 98d0243, so a bigger cut means new
     renders rather than a re-encode. */
  --hb-max: 2880px;

  /* HOW MUCH ROOM THE HOST'S COPY NEEDS, measured from under the header. It is the one
     geometric value a host is expected to set against its own layout rather than leave at
     the default, and it says what it means: the plate's CONTENT will not start above this
     line. A length and not a percentage, because what has to fit there is a block of text
     whose height is in pixels and barely moves with the viewport — a share of the screen
     would be far too small on a short one and wasteful on a tall one.

     Note it is the plate's content that clears this, not the plate's top edge. The plate's
     own top --hb-entry-sky is empty at the frame the approach holds on, so the picture is
     placed to put its first pixel of artwork below this line and its empty top is allowed
     to sit behind the words. That is worth a surprising amount: at 1440x900 it is what
     lets the plate enter at its FULL edge-to-edge width rather than scaled to 45% of it.

     40svh is a placeholder that keeps a bare mount sensible. This site sets it from the
     measured height of its own hero copy — see the note beside --hb-pin in index.html. */
  --hb-entry-clear: 40svh;

  /* The two artwork constants the entry is composed against, both measured off the frame
     the approach holds on (the first played frame, 276) rather than chosen.

     --hb-entry-sky is the share of the plate's height that is empty page colour at the top.
     Content begins at y 0.238 on the desk side and y 0.270 on the rack side; 0.226 is
     inside both, so the words can overlap that band and nothing shows through.

     --hb-entry-keep is the share that must stay above the fold, and 0.4 is not a round
     number either — it is the lower edge of the two TOP SURFACES, which are the things
     this composition exists to show. Classified by colour rather than by alpha (wood is
     alpha>=200 with R>=110, R-G>=14 and G-B>=14; the cubby's interior is alpha>=200 with
     R+G+B<=170), across the desk's x-range on the 1600-wide cut:

       y 0.363   the desktop plane ends — the bright specular crease along its far edge
       y 0.380   the slab's front face ends and the dark cubby begins  (row 425 of 1119,
                 and it is genuinely horizontal: 450 of 486 columns land on that row)
       y 0.396   the rack's top plate ends  (row 443)

     0.4 clears both. Cutting at 0.363 would slice the desk slab through its own 18px
     thickness, which reads as damage rather than as a crop; 0.4 keeps the desktop whole
     with its front edge, keeps the rack's lid, and spends the cubby and the legs below —
     which is the largest the picture can be drawn while still showing the thing it is
     there to show.

     THESE NUMBERS DESCRIBE FRAME 276 AND ONLY FRAME 276, and that is correct rather than
     sloppy: --hb-entry-keep governs the approach, and the approach holds on the first
     played frame throughout — head does not move until the scrub starts, by which point
     the plate is at full size and carries no transform at all. Re-measuring on a later
     frame would give a different and irrelevant answer, because the camera moves: the same
     desk edge is a flat 0.380 at frame 276 but slopes 0.386 to 0.404 by frame 417.

     Between them these two set how big the plate is at entry: it is scaled so that
     --hb-entry-keep of it fits between --hb-entry-clear and the fold. What it may NOT
     exceed is --hb-entry-zoom below. */
  --hb-entry-sky: 0.226;
  --hb-entry-keep: 0.4;

  /* HOW FAR PAST EDGE TO EDGE THE ENTRY MAY GROW, and 1 — do not — is the component's
     default because it is the only value that costs nothing.

     This used to be a hard cap in the script with a comment saying it was load-bearing,
     and the argument was sound as far as it went: content spans x 0.000-0.999, so any
     scale above 1 crops the rack's left edge and the desk's right, and edge to edge is as
     big as this artwork goes. What that misses is that a host laying copy over the plate
     is already spending most of the screen on words, and the plate is sized from what is
     left — so "edge to edge" is a ceiling it often cannot reach anyway, and a host that
     would rather crop the sides than shrink the picture has no way to say so.

     What it costs is the sides, and how much depends on the screen rather than on this
     number, because the geometry usually binds first. Measured on this site: 1.07 at
     1512x780 shows plate x 0.03-0.97 and only kisses the rack's left edge; 1.35 at
     1920x955 shows x 0.13-0.87 and takes about a quarter off each object; 1.50 at
     1920x1080 shows x 0.17-0.83 and takes rather more. It also costs sharpness, which is
     what --hb-max above bounds.

     It does not cost the scrub anything. The scale runs down to exactly 1 across the
     approach and the box carries no transform at all by the time the first frame is
     wanted, so the arch assembles at the plate's own size however this is set. */
  --hb-entry-zoom: 1;

  /* The floor under that scale, and it exists because the alternative is not a small plate
     but a broken one. On a viewport shorter than the copy needs there is no room below the
     copy at all, and an unclamped solve goes NEGATIVE — a negative scale reflects the box
     about its transform origin, so the plate is drawn mirrored and entirely off-screen for
     the whole hold. Landscape phones are the case: 667x375 leaves 303px under the header
     against a copy that wants more than that. Here the plate simply keeps a quarter of its
     size, drops below the fold further than --hb-entry-keep would like, and still reads as
     a picture rising into place. */
  --hb-entry-min: 0.25;

  /* How much of the scroll budget the approach spends before the scrub starts, and the
     window the plate's rise is drawn across. One screen is the default because it is a
     sane one for a bare mount; a host that lays copy over the plate will usually want it
     to be however far that copy has to travel, which is what this site sets it to. Read
     back as a length by hero-bridge.js — see the @property above. */
  --hb-entry-span: calc(100vh - var(--hb-pin));
  --hb-entry-span: calc(100svh - var(--hb-pin));

  /* The two lengths everything geometric below is built out of, named here rather than
     spelled out at each use. --hb-screen-h carries the svh/vh fallback pair for all of
     them, which is why it is worth extracting: it used to be written out three times and
     each copy needed its own fallback line.

     100vw and not 100% in the plate's height because a percentage would resolve against
     this element's own height, which is what is being defined. 100vw counts the classic
     scrollbar, so on an engine that has one this comes out a few pixels TALLER than the
     plate rather than shorter — the safe direction, costing a hairline of page colour
     rather than a clip. */
  --hb-plate-h: calc(min(100vw, var(--hb-max)) * 1432 / 2048);
  --hb-screen-h: calc(100vh - var(--hb-pin));
  --hb-screen-h: calc(100svh - var(--hb-pin));

  /* Where the plate rests inside a stage taller than it, which is every screen below about
     5/4 — phones and tablets in portrait. place-items: center used to do this and cannot
     any more, because there is now a second offset to add to it and a grid can only centre
     or not. Written out, the two simply compose in the box's margin below. */
  --hb-slack: max(0px, calc((var(--hb-screen-h) - var(--hb-plate-h)) / 2));

  /* WHERE THE TOP OF THE ASSEMBLED BRIDGE SITS, as a share of the plate's height, and the
     third artwork constant beside --hb-entry-sky and --hb-entry-keep. Measured on frame
     417 — the last frame the page plays, and the one the scrub's tail holds on, so it is
     the FINISHED bridge and not a moment during assembly.

     0.1001, and it is a line rather than a peak: the deck is flat at y 0.100-0.105 across
     x 0.30-0.68. Note it sits ABOVE both the rack's top at y 0.257 and the desk's at
     y 0.245, because the arch rises over the gap between them — which is the whole reason
     this constant is needed. Anything that clears the arch clears the furniture by a wide
     margin, and nothing that clears the furniture clears the arch.

     Blocks fly in from the top for the whole run, so DURING assembly the picture reaches
     y 0.000 and no offset can hold them clear of a host's copy. Only the end state is
     addressable, and that is what this describes. The last few frames of settling come
     about 9px closer than the final position — topmost content is y 0.092 at frame 411
     against 0.1001 at 417 — so a host asking for 30px of clearance gets 21 at the tightest
     moment. */
  --hb-arch: 0.1001;

  /* HOW FAR BELOW THE HEADER THAT LINE HAS TO LAND, and the host's number rather than the
     component's, exactly as --hb-entry-clear is: nothing here can know how tall a host's
     headline is or whether it even holds over the plate.

     0 is the default and means "leave the plate where it rests". A host that pins copy
     over the picture sets this to where its copy ends, and the plate moves down to put
     the finished bridge under it. This site sets the gap above its headline, plus the
     headline, plus 30px. */
  --hb-arch-clear: 0px;

  /* WHAT THAT COSTS, SOLVED, and it is a static offset rather than anything the script
     does: it is not a function of scroll, so it belongs here. hero-bridge.js picks it up
     for free — settle() reads the plate's resting position out of offsetTop, and a margin
     is in offsetTop.

     The middle term is the ask: how far the arch is above where the host needs it, given
     where the plate rests (--hb-slack) and where the arch sits inside it. Floored at 0 so
     a host whose copy already clears the bridge moves nothing.

     THE CEILING IS THE PROMISE --hb-entry-keep MAKES, and it is not decoration. Pushing
     the plate down spends exactly the thing the entry exists to protect: the desk's top
     edge staying above the fold. Uncapped, a short screen would take a 240px drop and put
     the desk under the fold for the whole hold. It does not bind on a laptop — 1512x780
     wants 239px against a ceiling of 285 — but a landscape phone would hit it. clamp()
     resolves to its minimum where the ceiling falls below it, so the degenerate case
     comes out as no drop rather than as a negative one. */
  --hb-arch-drop: clamp(
    0px,
    calc(var(--hb-arch-clear) - var(--hb-slack) - var(--hb-arch) * var(--hb-plate-h)),
    max(0px, calc(var(--hb-screen-h) - var(--hb-slack)
                  - var(--hb-entry-keep) * var(--hb-plate-h))));

  display: block;
  position: relative;
  box-sizing: border-box;

  /* THE STAGE'S HEIGHT, written here rather than left to the plate, and the reason is
     position: sticky. A sticky box is constrained to its containing block, which is its
     parent's CONTENT box — padding on this element does not extend it. Give the stage its
     height from its content and put the scroll budget in padding and the sticky range
     works out to zero: the stage never pins and the whole hero simply scrolls past. So
     both heights are declared, and their difference is the budget.

     The plate is min(100%, --hb-max) wide at 1.4302:1, and this has to be at least that
     tall or the stage clips it — which is the one thing this arrangement must never do.
     100vw and not 100% here because a percentage height would resolve against this
     element's own height, which is what is being defined. 100vw counts the classic
     scrollbar, so on an engine that has one this comes out a few pixels TALLER than the
     plate rather than shorter — the safe direction, costing a hairline of page colour
     above and below a plate that is centred in it anyway.

     max() and not min(): below about 5/4 the plate is shorter than the screen, and the
     stage stays a full screen with the plate centred in it.

     AND IT HAS TO GROW WITH --hb-arch-drop, which is easy to leave out and does not fail
     until the very end of the hero. On a desk screen the stage is exactly the plate's
     height, so a plate pushed down 244px overhangs it by 244px — invisible while the stage
     is pinned, and then painted over whatever follows the hero the moment the sticky stage
     bottoms out against this element. The host's section padding is nowhere near that:
     --hero-tail is about 100px on this site. */
  --hb-stage-h: max(var(--hb-screen-h),
                    calc(var(--hb-plate-h) + var(--hb-slack) + var(--hb-arch-drop)));

  /* HOW LONG THE SEQUENCE PLAYS, and the number to reach for when it reads as too fast.
     48 render frames across it, of which TAIL holds the last for the final 15%, so the rate
     is budget x 0.85 / 47 — 11.4px of scroll a frame at 1440x900 on the 70svh this
     defaults to. (An earlier note here said 13px; that divided by the whole budget and
     forgot the tail.) If the full stride-1 delivery lands (142 frames, see
     docs/hero-bridge-render.md) this has to rise with it or the whole assembly plays three
     times faster: frames and budget are one setting in two files.

     It is a host property rather than the hard 70svh it used to be, and that is not a
     promotion on principle — the host needs the number. Where a host lays pinned copy over
     the plate, the point at which its copy stops being pinned has to line up with the point
     at which this element stops being pinned, and that lands `--hb-entry-span + --hb-scrub
     + --hb-hold + --hb-exit` from the bottom of the element. A host that cannot name all
     four cannot compute it, and one that keeps its own copy of them beside a component that
     changed them has a hero that comes apart at the seam. See §5a of the WordPress
     handoff. */
  --hb-scrub: 70vh;
  --hb-scrub: 70svh;

  /* THE BEAT ON THE FINISHED PICTURE, held still before anything starts moving again. The
     scrub's own TAIL already holds the last frame for 15% of --hb-scrub — about 95px at
     1440x900 — which is enough to stop the sequence but not enough to read as an arrival.
     This is on top of it. Nothing at all happens here: no frame changes, no transform.

     Separate from --hb-exit below, and it used to be one number split in half by a `/2` in
     leave(). Two properties that each mean one thing beat one that means two, and they were
     wanted at different sizes the first time either was tuned. */
  --hb-hold: 12vh;
  --hb-hold: 12svh;

  /* THE RAMP'S HALF-WINDOW, which is the other thing the release needed, and it exists
     because that release used to be a cliff. While the stage is pinned the picture does not
     move at all; the instant it is not, the picture moves at the speed of the page. Measured
     at 1440x900 that step is 0 to 1 in a single frame, and it reads as the whole hero being
     yanked off the screen — position is continuous, velocity is not, and the eye is reading
     velocity.

     leave() blends the two across a window this long on EITHER side of the release: this
     much is spent before it, held in the element's height, and the same distance after it is
     paid for by the scroll that was always there. See leave() in hero-bridge.js for the
     curve and why the halves have to match.

     BIGGER IS GENTLER, AND THE RELATIONSHIP IS WORTH KNOWING because "smooth" is not a
     yes-or-no. The velocity is continuous at any size above zero, but its peak rate of
     change is 0.75 / this — so at 180px a reader moving 60px a frame sees the picture's
     step change by 15px between frames, and at 405px by 7px. Doubling this halves that.

     24svh keeps a bare mount sensible; 0 turns the ramp off and restores the cliff. */
  --hb-exit: 24vh;
  --hb-exit: 24svh;

  /* THE SCROLL BUDGET is the difference between the two heights, which is what progress()
     measures as offsetHeight minus the stage's, and it is the approach plus the sequence
     plus the exit written as exactly that. --hb-entry-span used to be spelled out here as a
     hard 100svh, which was fine while it was always one screen and wrong the moment a host
     shortened it: the element kept the screen it no longer spent and handed the difference
     to nobody, so the hero simply stopped moving for a third of a screen. Naming all three
     means a host that retimes any of them retimes the element with it — and means
     progress() can divide the budget by subtraction rather than by shares.

     The plain-vh lines are 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 scrub. */
  height: calc(var(--hb-stage-h) + var(--hb-entry-span) + var(--hb-scrub)
               + var(--hb-hold) + var(--hb-exit));
}
hero-bridge *, hero-bridge *::before, hero-bridge *::after { box-sizing: border-box; }

/* The stage pins under the header and is AT LEAST a screen tall — taller wherever the
   plate is, because --hb-stage-h above is the larger of the two. That is what makes the
   picture unclippable, so:

   NO overflow: hidden, and no overflow: clip either. The stage is sized to hold the plate
   at rest, and a clip rectangle here is the exact artefact this arrangement exists to
   avoid: it sits on the bottom edge of the screen while the stage is pinned, where nobody
   can see it, and then walks up the page as the stage releases. Putting one back as a
   "safety net" reintroduces the edge.

   Which matters more now than it did, because with --hb-entry-zoom above the entry
   transform can scale UP as well as down and the plate genuinely overhangs this box during
   the approach. Overhanging is the intent — the viewport is what crops it, and cropping by
   the viewport is invisible. Sideways that overhang would extend the page's scrollable
   width, so the HOST has to be the one that clips it, at the full width of the page where
   the cut lands off-screen: this site does it with overflow-x: clip on its page wrapper,
   which is not a scroll container and so leaves the stage's sticky intact.

   place-items rather than an absolutely-positioned child, which is the reverse of what
   this file used to do. The old note was right that a browser clamps `center` to `start`
   for an item larger than its alignment container, so that overflow cannot become
   unreachable — but that only applies to a scroll container, and without overflow:hidden
   the stage is not one. Nor is the item ever larger than it.

   START and not centre on the block axis, which is a change: the plate's vertical position
   is now --hb-slack plus --hb-arch-drop, written on the box below. --hb-slack is exactly
   what centring used to produce, so nothing moves where the drop is zero — but a grid can
   only centre or not, and there is now a second offset to add to it. Doing it in one
   margin is also what lets settle() read the whole thing back out of offsetTop. */
hero-bridge [data-hb-stage] {
  position: sticky;
  top: var(--hb-pin);
  height: var(--hb-stage-h);
  display: grid;
  place-items: start center;
}

/* Edge to edge at the plate's own aspect. aspect-ratio rather than a height, so the box is
   the plate whatever the viewport does and there is no second number to keep in step with
   the render.

   min(100%, …) and not min(100vw, …): 100vw counts the classic scrollbar, so it is a
   handful of pixels wider than the content box, and in a stage that no longer clips those
   pixels would push the plate sideways instead of being quietly swallowed. 100% is the
   element's own width and cannot do that.

   transform-origin at the TOP edge is what the entry is built on: hero-bridge.js places
   this box's top below the host's copy and scales it about that same top — so the picture
   hangs from a line under the words and settles into the stage, rather than growing out of
   its own middle. It resolves to no transform at all by the time the scrub starts.

   Which direction it scales depends on --hb-entry-zoom. At the default 1 it only ever
   scales down and the approach is a pure rise. Above it the plate can start LARGER than
   the stage and settle back, and the top origin is what keeps that legible: the extra
   height goes off the bottom of the screen where the viewport swallows it, not into the
   copy above.

   The top edge and not the bottom, which is what this was first built with. Anchoring the
   bottom to the fold forces the WHOLE plate to fit between the copy and the fold, and at
   1440x900 that is 458px for a 1007px picture — 45% scale, a postage stamp. Anchoring the
   top instead lets the plate run off the bottom of the screen, which costs nothing because
   the viewport cropping it there is invisible, and buys the full edge-to-edge width.

   will-change is set, and the note that used to sit here saying this never animates is why
   it is worth flagging: it does now, once, over the approach. It is left on rather than
   toggled because the element already promotes two canvas layers for their opacity
   cross-fade, so this adds no layer that is not there for the length of the section anyway.

   Content spans x 0.000-0.999 and y 0.000-0.999 of this plate, measured over all 48 played
   frames at the lossless-alpha encode: the ground shadow reaches the bottom edge and blocks
   fly in from the top and the right for the whole run. There is no transparent margin — so
   there is nothing to trim off before the picture starts, and every edge of this box is
   artwork. Which is the argument for not clipping any of them.

   isolation is load-bearing: it makes the two canvases their own blending group, and
   without it plus-lighter blends against the page and blows out to white. An ancestor
   with a filter, an opacity below 1, or its own mix-blend-mode creates a competing
   stacking context — the hero's [data-reveal] wrapper animates opacity, so it is one of
   these, and this is what contains the group against it.

   THE MASK IS THE LAST FOUR PERCENT, AND IT IS WHAT KEEPS THE BOTTOM EDGE INVISIBLE.
   Not clipping the plate is only half of "no edge": the render itself ends in one. Measured
   on the shipped frames, the last row of the plate carries a black wash at alpha 2.4/255,
   still 2.3 nine rows in — the far tail of the ground-shadow plane, which the frame simply
   stops in the middle of. Composited on this site's page colour that is a step from 250.6
   to 253.0 across the full width of the screen, and it walks up the viewport as the hero
   scrolls away. Small, but it is a perfectly straight line the width of the page, which is
   the kind of small the eye is best at.

   Four percent is chosen against the artwork, not picked: the desk's feet rest at y 0.94
   and everything below y 0.96 is shadow tail, so the ramp touches no object. Across 40px
   at 1440x900 it takes 2.4 levels of alpha to zero — a gradient of 0.06 levels per pixel,
   which no display resolves.

   Only the bottom. The top row measures alpha 0.01 and needs nothing, and the sides carry
   real artwork — the rack's reflection reaches the left edge at alpha 59 — so a feather
   there would erode the picture rather than an artefact. Their edges also sit off-screen
   at every width below --hb-max.

   The mask goes on this element and not on the layers because it has to apply AFTER the
   two canvases have blended: masking them separately would fade each one's contribution
   before plus-lighter added them, which is a different and wrong picture during a
   cross-fade. */
hero-bridge [data-hb-box] {
  position: relative;
  width: min(100%, var(--hb-max));
  aspect-ratio: 2048 / 1432;
  /* Where the plate rests. --hb-slack is the centring the stage used to do; --hb-arch-drop
     is how far the host's copy needs the finished bridge pushed down. A MARGIN and not a
     transform or a `top`, for two reasons: it is in offsetTop, which is where settle()
     reads the resting position from and how the entry learns about this without a line of
     script; and it contributes layout, so the stage's height above genuinely accounts for
     it rather than the plate overhanging into the next section. */
  margin-top: calc(var(--hb-slack) + var(--hb-arch-drop));
  transform-origin: 50% 0%;
  will-change: transform;
  isolation: isolate;
  mask-image: linear-gradient(to bottom, #000 96%, transparent 100%);
}

/* One box, three stacked pictures: the two scrub layers and the still they replace. They
   register 1:1 because they are the same plate at three sizes, so nothing here can drift
   out of alignment as the browser picks a srcset candidate. Absolute against a box that
   carries its own aspect-ratio, so none of the three contributes layout and the box's
   height is the render's shape rather than whichever file arrived. */
hero-bridge [data-hb-box] > canvas,
hero-bridge [data-hb-box] > img {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
}

/* Two canvases, not one. They hold the frames either side of the current position and are
   cross-faded by opacity under plus-lighter. Drawing both into a single context at partial
   alpha squares the outgoing frame's contribution, so everything the two frames share —
   here the rack, the desk and most of the arch — sags to three-quarters opacity halfway
   through every transition. plus-lighter adds to exactly the frame in between.

   It earns its place at this stride. The frames are every third one of a 30fps render, so
   two adjacent frames are 0.1s of animation apart and a hard cut between them reads as a
   judder; the fade is what makes 48 frames play like a sequence rather than a flipbook. */
hero-bridge [data-hb-layer] { will-change: opacity; }
hero-bridge [data-hb-layer="1"] { opacity: 0; mix-blend-mode: plus-lighter; }

/* The still holds the hero until a frame has actually been drawn over it — see paint().
   visibility rather than display, so the swap cannot be mistaken for a layout change by
   anything watching; the image is absolutely positioned and contributes no layout either
   way. */
hero-bridge[data-hb-motion="on"][data-hb-ready] [data-hb-box] > img { visibility: hidden; }

/* Static presentation, and the default: the attribute is absent until the element decides
   it can animate, so a page whose script never runs, or has not run yet, renders the hero
   as a plain full-width still in flow, with no pin and no extra screens to scroll past.

   Reduced motion lands here too, but is decided in JS rather than by a media query in this
   file. It is one of three signals — a preference, save-data, and whether the engine can
   decode a bitmap at all — and only the element sees the other two, so all three resolve
   in one place.

   The screen of padding is the important line and is easy to mistake for decoration. The
   host lays a screen of copy OVER the pinned plate, and with no pin there is nothing to
   lay it over — so the still drops below that screen instead of underneath the words. It
   is also what the first paint looks like on every load, before boot decides anything,
   which is why the copy never jumps: the arrangement is legible in both states and the
   transition between them changes only the plate's position.

   PADDING and not margin, which is not interchangeable here. A top margin on this element
   collapses up through the host's wrapper and out of the section, taking the section's own
   top with it — and the copy overlay is positioned against that section, so it travels the
   full screen downwards too and lands back on top of the still. Padding cannot collapse,
   and with height:auto it adds to the element exactly as intended. */
hero-bridge:not([data-hb-motion="on"]) {
  height: auto;
  padding-top: calc(100vh - var(--hb-pin));
  padding-top: calc(100svh - var(--hb-pin));
}
hero-bridge:not([data-hb-motion="on"]) [data-hb-stage] { position: static; height: auto; display: block; }
/* margin-top goes with the rest of it, and it is easy to miss because the property that
   drives it is guarded in the HOST's stylesheet rather than here. --hb-arch-clear buys room
   for a headline pinned OVER the plate; a host can only guard it on what CSS can see, which
   is @supports and prefers-reduced-motion — and neither of those is "the script never ran".
   With scripting off the copy is in flow above a still that is not pinned at all, so the
   drop is 240px of blank page between the words and the picture. --hb-slack goes with it:
   there is nothing to centre in once the stage is display:block and height:auto. */
hero-bridge:not([data-hb-motion="on"]) [data-hb-box] { transform: none; will-change: auto; margin-top: 0; }
hero-bridge:not([data-hb-motion="on"]) [data-hb-box] > canvas { display: none; }

/* The cut follows the box, and the box is the page's full width up to --hb-max. Wide
   viewports take the 1600 cut: a 1440x900 laptop wants 1440 device pixels at 1x, and the
   1200 cut there would be a 1.2x upscale of the artwork the site is built around.

   That costs what it costs, and the number to watch is not memory. Decoded size is
   width x height x 4 whatever the file weighs, so the 1600 cut is 7.2 MiB a frame against
   the 1200 cut's 4.0 and the byte budget below holds proportionally fewer — but the
   measured bottleneck is arrival, not residency. Raising the ceiling does not help and
   actively hurts the substitutions: at 192 MB the window doubles but the frames the scrub
   falls back on land 6.7 positions away instead of 3.0, because the same four in-flight
   slots are spread over twice the requests. If ticks-waiting-on-a-frame needs improving
   the levers are the encoded width or the shadow's share of the bytes, which
   docs/hero-bridge-render.md costs out — not this number.

   Note that the approach moved those numbers, and they were re-measured for it: over the
   paints where the scrub is advancing, 19.5% wanted a frame that was not resident under
   the old rig against 4.2% now, and the substitutions land 1.29 frames away rather than
   4.03. Nothing about the budget or the cut changed — what changed is that there is a
   screen of scroll before the first frame is wanted, so the head-first fetch order in
   plan() has somewhere to work. */

/* Below the design system's own breakpoint, the smaller cut. A 991px box at 1x wants
   991 device pixels and the 1200 cut covers it; phones at 2-3x are undersampled by both
   cuts and take the cheaper one, which is the compromise the render doc already records.

   The budget travels with the cut, and is smaller for a reason that is easy to get
   backwards: a byte ceiling alone buys MORE frames when each one gets cheaper. The mobile
   cut decodes to 4.0 MiB against the full cut's 7.2, so the desktop's 96 MB would hold 24
   frames here against 14 there — the wider window on the device least able to afford it.
   48 MB holds 12, which is the same coverage at half the cost. */
@media (max-width: 991px) {
  hero-bridge { --hb-variant: "m"; --hb-budget: 48; }
}
