/* Landing motion layer — /v2 only. Loaded after site.css, never by /pt.

   Three rules govern every line in here:

   · Every resting state is site.css's own. What this file adds are *starting* states, and they all
     hang off .is-motion on <html>, which only landing-v2.js writes. No script, a script that threw,
     or prefers-reduced-motion: reduce, and the page is the /pt exactly.
   · Progress arrives as a plain 0..1 number in a --m-* custom property. The script measures; this
     file decides what the number means. Only parallax and the mouse write a transform directly,
     because those two are the ones that need sub-pixel values every frame.
   · Geometry only, and only where the geometry *is* the effect. Nothing here changes a colour, a
     type size, a spacing scale or an order. Each override below names the effect it serves.

   Hand-written CSS on purpose: tailwind.config.js only ever compiles tailwind/input.css into
   site.css, so @apply and theme() here would be shipped to the browser verbatim. Literal values
   throughout — #0a0b0d is colors.ink.DEFAULT, #f6f5f1 is colors.paper.DEFAULT, and the brand is
   read through the white-label channels the way site.css reads them.

   Namespace: --m-*. Never --tw-* (the Tailwind transform utilities read those out of the universal
   selector and would compose whatever we wrote into their own transform), never --gts-/--hero-/
   --seam-/--nav-/--rail-. */


/* ─────────────────────────────────────────────────────────── 0 · Invariants */

/* The whole stacking deck is position: sticky and nothing else. Sticky resolves against the nearest
   scroll container, so a single ancestor with overflow other than visible turns the deck back into
   normal flow — silently, with no error anywhere. .page-body is `relative z-10 bg-paper` today and
   every box between it and <html> is overflow: visible, so this rule changes nothing; it is here to
   state the dependency out loud and to outrank a stylesheet that later reaches for overflow on the
   wrapper. The same invariant forbids transform, filter, perspective, backdrop-filter and
   contain: paint|layout on these boxes: any one of them would make the wrapper the containing block
   for the fixed header and for the shutter stage, and would re-parent both into the page.

   The paper sections are named for the same reason one level down — #como-funciona pins a panel of
   its own inside itself. What is *not* named is deliberate: .living-block and .bookend are
   overflow: clip, which clips the paint without becoming a scroll container, and their sticky
   children (.living-bg, and the bookend's media) depend on exactly that. */
.is-motion .page-body,
.is-motion .page-body > .section-light {
  overflow: visible;
}

/* Preventive, not current: no element rendered by this page carries data-reveal today — the Index
   markup does, and every occurrence was dropped when it was copied here (see the note in
   V2.cshtml). site.css leaves [data-reveal] at opacity: 0 waiting for a class only landing.js
   writes, and landing.js is not on this page, so a shared partial that one day grows one back would
   go dark and stay dark. That is what this rule is here to make impossible; it is not fixing
   anything the page has. Deliberately not under .is-motion — it has to hold with the script gone as
   well. */
[data-reveal] {
  opacity: 1;
  transform: none;
  transition: none;
}

/* Same lock the mobile menu uses, under our own name: Lenis.stop() only stands down its own virtual
   scroll — the keyboard, the scrollbar and iOS momentum still move the page. Both elements, as
   tailwind/input.css locks html.menu-locked, because on iOS overflow on the root alone is ignored.

   Two things ride on that, and neither is obvious. The gutter: on a platform with classic
   scrollbars, taking the overflow away takes the scrollbar with it and the whole page steps
   sideways — and this lock is held for the loader's two seconds on *every* load, so it would step
   back again on the one frame the layer is meant to look composed. `stable` reserves the same width
   the scrollbar had, so neither edge of the lock moves anything. The sticky: with the root hidden,
   the body stops propagating its own overflow and becomes the scroll container, which is what the
   deck's position: sticky then resolves against. Harmless while it is held — nothing scrolls behind
   a curtain, and the menu that takes the same lock only exists below 768px where there is no deck —
   but anything that lengthens the lock has to deal with it. */
html.m-locked,
html.m-locked body {
  overflow: hidden;
}
html.m-locked {
  scrollbar-gutter: stable;
}


/* ───────────────────────────────────────────── 1 · Split text — #6, lines */

/* The split is scaffolding: a block wrapper the reader never hears (split.js pairs it with an
   sr-only copy of the original sentence), one mask per measured line, one moving box inside it. */
.m-split {
  display: inline;
}
/* Flex, and not block, for one reason: the masks below carry negative vertical margins, and the
   margins of adjacent block boxes collapse — two negative ones to the *more negative* of the pair
   rather than to their sum. Each gap between two lines would keep 0.16em of the padding that the
   collapsed margin no longer pays back, and the block would grow by that much per line while
   claiming to be net-zero. Flex items never collapse their margins, so every mask is worth exactly
   the line it holds and a split heading is the same height as the heading it replaced. */
.is-motion [data-m-lines] .m-split {
  display: flex;
  flex-direction: column;
}

.is-motion .m-line-mask {
  display: block;
  overflow: hidden;
  /* leading-[0.88] on the display titles leaves the line box shorter than the glyphs it holds, so a
     clip on the line box alone would shave ascenders and descenders at rest — the one thing a
     motion layer may never do. The padding buys that ink back and the negative margin hands the
     space straight to the layout, so the block occupies exactly what it did before. */
  padding-block: 0.16em 0.2em;
  margin-block: -0.16em -0.2em;
}

/* 30px, 1.6s, power4.out, 70ms apart — the inventory's numbers for a title. The rise is small
   against the mask's reach on purpose: the first frames are where opacity is still near zero, so
   what little of the line clears the padded clip is invisible while it does. */
.is-motion .m-line {
  display: block;
  transform: translateY(30px);
  opacity: 0;
  transition:
    transform 1.6s cubic-bezier(0.165, 0.84, 0.44, 1),
    opacity 1.6s cubic-bezier(0.165, 0.84, 0.44, 1);
  transition-delay: calc(var(--m-d, 0) * 70ms);
}
/* A paragraph travels further, on expo.out, one tenth of a second apart. */
.is-motion [data-m-lines="para"] .m-line {
  transform: translateY(40px);
  transition-timing-function: cubic-bezier(0.19, 1, 0.22, 1);
  transition-delay: calc(var(--m-d, 0) * 100ms);
}
.is-motion [data-m-lines].is-in .m-line {
  transform: none;
  opacity: 1;
}


/* ───────────────────────────────────────────── 2 · Split text — #6, chars */

/* Opacity only, sine.out, 20ms apart: a short label has no room to travel and a label that moves
   reads as a mistake next to a title that does. */
.is-motion [data-m-chars] .m-c {
  display: inline-block;
  opacity: 0;
  transition: opacity 0.6s cubic-bezier(0.39, 0.575, 0.565, 1);
  transition-delay: calc(var(--m-d, 0) * 20ms);
}
.is-motion [data-m-chars].is-in .m-c {
  opacity: 1;
}


/* ──────────────────────────────────────────────────── 3 · data-m-pop — #6 */

/* Direct children only, 70ms apart, expo.out. The two numbers are custom properties so a container
   whose construction cannot take them can hand back one without losing the rest. */
.is-motion [data-m-pop] > * {
  --m-pop-y: 16px;
  --m-pop-s: 0.96;
  transform: translateY(var(--m-pop-y)) scale(var(--m-pop-s));
  opacity: 0;
  transition:
    transform 1.1s cubic-bezier(0.19, 1, 0.22, 1),
    opacity 0.9s cubic-bezier(0.19, 1, 0.22, 1);
  transition-delay: calc(var(--m-d, 0) * 70ms);
}
.is-motion [data-m-pop].is-in > * {
  transform: none;
  opacity: 1;
}

/* Two grids on this page draw their hairlines as a 1px gap in the parent's own fill and then clip
   the result to a rounded box. A cell that shrinks opens that fill into a visible band, and a cell
   that slides has its edge cut off by the clip — so on these two the pop is the fade alone. The
   geometry is doing the drawing here; the motion has to leave it alone.

   These are also the only pop children site.css already transitions: .feature-card-dark and .door
   both hover from bg-ink/50 to bg-ink/25 over 300ms, and the shorthand above — being a shorthand —
   resets transition-property to transform and opacity and takes that hover away for good. Restated
   here, summed rather than replaced: a motion layer may add to the page, never subtract from it.
   transform is not in the list because on these two there is none left to run. */
.is-motion .feature-grid-dark > *,
.is-motion .doors > * {
  --m-pop-y: 0px;
  --m-pop-s: 1;
  /* The stagger is written per property rather than as the transition-delay longhand the rule above
     uses, and that is the whole reason this list is spelled out twice: one delay for both would put
     the sixth card's hover 350ms after the pointer landed on it. The arrival is staggered, the
     hover is immediate. */
  transition:
    opacity 0.9s cubic-bezier(0.19, 1, 0.22, 1) calc(var(--m-d, 0) * 70ms),
    background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

/* The one frame that has to be silent. .is-motion is written by a deferred module, which can land
   after the page has already painted — and installing a starting state on an element the reader is
   looking at is a transition *into* it: every pop child would fade out over nine tenths of a second
   before it was allowed to fade back in. So the layer arrives disarmed, and the script hands the
   transitions back a frame later, once the starting states are the resolved styles rather than a
   destination. Split lines never had the problem — they are created after the class — but they are
   listed anyway so the rule is about the moment and not about one effect.

   Last in the section on purpose: it weighs the same as the rule above, which also declares a
   transition on some of the same elements, so only source order can decide between them and this
   one has to win. */
.m-arming .m-line,
.m-arming [data-m-chars] .m-c,
.m-arming [data-m-pop] > * {
  transition: none;
}


/* ─────────────────────────────────────────────────── 4 · data-m-fill — #4 */

/* The reference fills type with background-clip: text, which means owning the colour. A mask reads
   the same without touching it — but it *multiplies* the alpha the element already has, and there
   is no arithmetic that undoes that from here. Which fixes where the attribute may go: only on type
   whose own colour is opaque. On .living-close-line, text-white, 0.55 is 0.55 and the statement
   comes up through the dark at 6.2:1 from its first frame. On something already translucent it
   compounds — text-ink/50 would start the sweep at 0.275 of ink on paper, which is 1.9:1, unreadable
   body copy for the whole length of the scroll that fills it, on every load. That is why 02's note
   arrives in lines like every other note on the page and not in colour.

   0.55 is the inventory's starting point. --m-fill runs 0..1; the stop is pushed past both ends so
   the sweep starts fully unfilled and finishes fully filled instead of clipping a band at either
   edge. */
.is-motion [data-m-fill] {
  --m-fill: 0;
  --m-fill-x: calc(var(--m-fill) * 122% - 22%);
  -webkit-mask-image: linear-gradient(
    95deg,
    #000 var(--m-fill-x),
    rgb(0 0 0 / 0.55) calc(var(--m-fill-x) + 14%)
  );
  mask-image: linear-gradient(
    95deg,
    #000 var(--m-fill-x),
    rgb(0 0 0 / 0.55) calc(var(--m-fill-x) + 14%)
  );
}
/* Dropped the moment it is full. A mask is a compositor surface and it costs the text its subpixel
   antialiasing for as long as it is there; once the sweep is done it has nothing left to say. */
.is-motion [data-m-fill].m-full {
  -webkit-mask-image: none;
  mask-image: none;
}


/* ─────────────────────────────────────────────── 5 · data-m-parallax — #5 */

/* Both neutralisers exist for the same reason and only where the parallax runs: the script writes
   style.transform on these elements every frame, and something in site.css is already animating or
   transitioning that same property. */
@media (min-width: 992px) {
  /* .hero-media carries `animation: media-in 1.2s both`, whose last keyframe sets a transform of its
     own. An animation outranks the style attribute for as long as it runs, so every frame scrubbed
     inside that window is swallowed, and whether the fill still wins afterwards is a question about
     when the engine drops a finished animation. The entry fade is the loader's job on this page. */
  .is-motion .hero-media {
    animation: none;
  }
  /* .race-card-media has `transition-transform 700ms`, which would put a 0.7s lag on a per-frame
     scrub and leave the picture dragging behind the page. The hover lift that transition served is
     folded into the scrub itself — see the hover term in scrub.js — because an inline transform
     outranks the :hover rule and would otherwise kill it silently. */
  .is-motion .race-card-media {
    transition: none;
  }
}


/* ────────────────────────────────────────────────── 6 · data-m-stack — #2 */

/* The inventory's `pin: true, pinSpacing: false, start "bottom bottom" → end "top top"`, as sticky
   and nothing else. Everything after the pinned section keeps moving and rides over the top,
   because every one of these sections is opaque and later siblings paint over earlier ones.

   The offset is negative and it is the whole trick. `bottom: 0` is the obvious reading of "bottom
   bottom" and it is wrong: it drags the section up the moment its foot would fall below the fold —
   all the way to the top of .page-body, which is the containing block — so the deck lands on the
   hero. `top: 100svh - own height` is the correct one: the section travels normally and stops at
   the exact frame its foot reaches the foot of the screen. Only stack.js knows that height, which
   is why it is the one number this effect asks a script for.

   Not a transform, and never a transform: these sections carry sticky children of their own
   (.howto-stage, .living-bg, the bookend's media), and a transform on the ancestor would become
   their containing block and freeze all three. Same reason there is no will-change here. */
@media (min-width: 992px) {
  .is-motion [data-m-stack] {
    position: sticky;
    top: var(--m-stack-top, 0px);
    /* A card shorter than the screen would pin with a strip of the page showing over its head. */
    min-height: 100svh;
  }
}


/* ─────────────────────────────────────────────────── 7 · data-m-open — #1 */

/* The contained card that opens to full bleed, without Flip.

   The media becomes a sticky box that takes exactly one screen of flow and holds the top of the
   viewport for the whole section, so the section's own height is the travel. --m-open runs 0..1 on
   the section and the card's inset, height and radius are read straight off it: at 0 a rounded card
   floating in the ink, at 1 the screen.

   The arithmetic that matters: the card's margin box stays exactly 100svh whatever --m-open is
   doing (height loses 2y, margin-bottom gives 2y back), so nothing below it shifts while it opens.
   The veil and the grain come after it in flow, so each pulls itself back to the section's top edge
   with a -100svh margin and hands the same screen back — the same "take a viewport and return it"
   trick .living-bg already uses in 03. They carry the card's geometry too, so the wash and the
   noise sit on the picture rather than over the whole two-screen block.

   The radius is on the video itself, not on a container that clips it. That is the lesson written
   into .hero-stage and .howto-shot: a rounded clip around a separate compositor surface flickers
   the page colour through its edge on the frames where the mask has moved and the surface has not.
   .bookend-media has no filter and no transform of its own, so rounding it is one surface being
   painted with rounded corners, which is safe. */
@media (min-width: 992px) {
  .is-motion [data-m-open] {
    --m-open: 0;
    /* The flex column centred a body that no longer has the section to itself. */
    display: block;
    /* One screen for the opening, one for what the opening reveals. Declared after the stack block
       on purpose: both selectors weigh the same and this section wears both attributes. */
    min-height: 200svh;
  }
  .is-motion [data-m-open] .bookend-media,
  .is-motion [data-m-open] .bookend-veil,
  .is-motion [data-m-open] .grain {
    /* Left and right no longer travel together. At rest the card is the right half of the spread —
       50% is the shell's midpoint at every width, because the shell is centred — with the statement
       in the half it leaves. Opening is therefore a sweep leftwards across the page rather than a
       box growing evenly out of its own middle, which is the difference between a card resizing and
       a picture taking the screen. */
    --m-open-l: calc((1 - var(--m-open)) * 50%);
    --m-open-r: calc((1 - var(--m-open)) * clamp(1.5rem, 4vw, 4rem));
    --m-open-y: calc((1 - var(--m-open)) * clamp(1.5rem, 7vh, 6rem));
    /* inset: auto first — all three arrive from `absolute inset-0`, and a sticky box with a bottom
       offset as well as a top one is one more thing for the engine to arbitrate. display: block
       because two of the three are spans, and an inline box does not stick. */
    inset: auto;
    display: block;
    position: sticky;
    top: var(--m-open-y);
    /* Spelled out rather than left to `auto`: one of the three is a <video>, and auto on a replaced
       element resolves to its intrinsic width — the card would open to the size of the footage. */
    width: calc(100% - var(--m-open-l) - var(--m-open-r));
    height: calc(100svh - 2 * var(--m-open-y));
    margin-left: var(--m-open-l);
    margin-right: var(--m-open-r);
    border-radius: calc((1 - var(--m-open)) * 28px);
  }
  /* First child: its flow box starts at the section's top already, and the bottom margin is what
     keeps the two screens of choreography from breathing as the card grows. */
  .is-motion [data-m-open] .bookend-media {
    margin-top: 0;
    margin-bottom: calc(2 * var(--m-open-y));
  }
  /* Everything after it: pulled back over the card, net contribution zero. */
  .is-motion [data-m-open] .bookend-veil,
  .is-motion [data-m-open] .grain {
    margin-top: -100svh;
    margin-bottom: calc(2 * var(--m-open-y));
  }
  /* The second screen. The doors arrive on it, which is what "and only then the doors appear"
     means in the page map: they are simply below the screen the card is opening on. */
  .is-motion [data-m-open] .bookend-body {
    display: flex;
    flex-direction: column;
    justify-content: center;
    min-height: 100svh;
  }
  /* A blur that re-samples a video opening underneath it is a read-back every frame, which is the
     one thing .living-veil and .site-header.solid.tone-dark are both written to avoid. The doors
     take their blur back once the section has stopped moving. */
  .is-motion [data-m-open] .door {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
  .is-motion [data-m-open].m-open-done .door {
    -webkit-backdrop-filter: blur(4px);
    backdrop-filter: blur(4px);
  }
}


/* ──────────────────────────── 7b · 04's first act — the statement column */

/* Geometry only, per this file's rule: the type comes from .section-title, .hero-lead,
   .section-index and .btn-primary, which the base sheet already dresses for a dark block
   (input.css:1219). What is added here is where the column sits and how it leaves. */
.bookend-intro-shell {
  margin-inline: auto;
  width: 100%;
  max-width: 72rem;
  padding-inline: 1.5rem;
}
.bookend-intro-col {
  max-width: 32rem;
}
/* Larger than a section head because this is the page's last statement and it has a screen to
   itself — but it stops short of the hero's size, which stays the page's loudest voice. */
.bookend-intro-title {
  margin-top: 0.9rem;
  font-size: clamp(2.1rem, 4.6vw, 3.3rem);
}
.bookend-intro-go {
  margin-top: 2.5rem;
}

/* Below the line data-m-open does not run, so the column is simply the copy above the doors and
   needs the air the body's own padding would have given it. */
@media (max-width: 991.98px) {
  .bookend-intro {
    padding-top: 6rem;
  }
}

@media (min-width: 992px) {
  /* The column takes the same screen the card opens on: sticky for its length, then pulled back over
     it with the -100svh margin .bookend-veil and .grain already use, so it costs the flow nothing and
     the two-screen arithmetic above still holds. */
  .is-motion [data-m-open] .bookend-intro {
    position: sticky;
    top: 0;
    z-index: 1;
    display: flex;
    align-items: center;
    height: 100svh;
    margin-top: -100svh;
    /* Gone by a third of the opening, rising as it goes. Leaving early is the point: the statement
       and the full-bleed picture must never share the frame at half strength each. */
    opacity: calc(1 - clamp(0, (var(--m-open) - 0.06) / 0.34, 1));
    transform: translateY(calc(clamp(0, (var(--m-open) - 0.06) / 0.34, 1) * -2.5rem));
  }
  /* Faded is not gone. A link at opacity 0 still takes the click, still answers the keyboard and is
     still read aloud; scrub.js writes this class once the card is past halfway, and visibility is
     what actually takes the column out of the page. */
  .is-motion [data-m-open].m-open-past .bookend-intro {
    visibility: hidden;
  }
  /* Stops clear of the card's resting edge at 50vw rather than running up to it. */
  .is-motion [data-m-open] .bookend-intro-col {
    padding-right: 3.5rem;
    max-width: min(32rem, calc(50vw - 4rem));
  }
}


/* ──────────────────────────────────────────────── 8 · data-m-shutter — #3 */

/* A band of its own, not a curtain over the viewport. The first cut fixed the stage to the screen,
   which meant that for the 1.8 screens the wipe stays live it painted over whatever was behind it —
   and between two sections of the same paper that read as the page being eaten in stripes rather
   than as a transition. The reference does not do that either: its transition block is a short strip
   in the flow. Bounded to its own box, the effect cannot occlude a neighbour however badly it is
   timed, which is the property worth having.
   The marker keeps its own height and the stage is absolute inside it, so the band scrolls away with
   the page instead of following the reader down. */
.is-motion [data-m-shutter] {
  display: block;
  position: relative;
  overflow: hidden;
  height: 10rem;
}
.is-motion [data-m-shutter] .m-shutter {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-rows: 1fr 1fr;
  pointer-events: none;
  /* Hidden whenever the wipe is at either end: two rows of composited blocks are not worth keeping
     alive for a boundary nobody is near. */
  visibility: hidden;
}
.is-motion [data-m-shutter] .m-shutter.m-live {
  visibility: visible;
}
.m-shutter-row {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
}
/* 1.05 is the inventory's overshoot, and it is also what stops a seam showing between two blocks
   that are meant to read as one sheet. */
.m-shutter-b {
  background: var(--m-tone, #0a0b0d);
  transform: scaleX(calc(var(--m-b, 0) * 1.05));
  transform-origin: left;
}
.m-shutter-row:nth-child(2) .m-shutter-b {
  transform-origin: right;
}
/* Past the halfway point the sheet is opening, not closing, so each block has to retreat to the
   edge it did not come from — otherwise the wipe rewinds instead of passing through. */
.m-shutter.m-back .m-shutter-b {
  transform-origin: right;
}
.m-shutter.m-back .m-shutter-row:nth-child(2) .m-shutter-b {
  transform-origin: left;
}
/* The colour comes from the attribute as a token the script matches against this list, never as a
   value it passes through: style-src is 'self' and data must not reach a stylesheet. */
.m-tone-ink {
  --m-tone: #0a0b0d;
}
.m-tone-paper {
  --m-tone: #f6f5f1;
}
.m-tone-primary {
  --m-tone: rgb(var(--gts-primary));
}
.m-tone-accent {
  --m-tone: rgb(var(--gts-accent));
}


/* ──────────────────────────────────────────────────── 9 · data-m-nav — #9 */

/* The bar itself moves, not the header that carries the attribute: .site-menu is position: fixed
   *inside* that header, and a transform on the header would become its containing block and
   collapse the panel into the height of the bar. Above 768px only, which is exactly where the
   burger and that panel do not exist — so the two can never be on screen together. */
@media (min-width: 768px) {
  /* Summed, not replaced. This is a shorthand and it outranks .site-nav, which transitions the
     104px→84px it shrinks to when the bar goes solid; declaring only the transform here would make
     that a jump, and a bar that snaps on the first wheel click is a difference between the two
     pages that serves no effect. */
  .is-motion [data-m-nav] > nav {
    transition:
      height 0.3s ease,
      transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
  }
  .is-motion [data-m-nav].m-away > nav {
    transform: translateY(-105%);
  }
}


/* ──────────────────────────────────────────────────── 10 · Loader — #9 */

/* Built by the script and removed by it, so there is no loader at all without JavaScript — the one
   arrangement in which an overlay can never strand the page. */
.m-loader {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  pointer-events: none;
  /* 0.35s, and chrome.js starts it at 650 of its 1100ms ceiling: the overlay reaches zero on the
     frame it is removed, instead of being torn out of the document part way through a fade. See the
     timeline at the top of chrome.js for the rest of the arithmetic. */
  transition: opacity 0.35s ease-out;
}
.m-loader.m-gone {
  opacity: 0;
}
.m-loader-col {
  background: #0a0b0d;
  transform-origin: left;
  transition: transform 0.4s cubic-bezier(0.455, 0.03, 0.515, 0.955);
  transition-delay: calc(var(--m-d, 0) * 24ms);
}
.m-loader-col:nth-child(even) {
  transform-origin: right;
}
.m-loader.m-open .m-loader-col {
  transform: scaleX(0);
}
.m-loader-brand {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
}
/* The mark arrives from above its own edge and leaves below it, so the clip has to be the mark and
   nothing more. */
.m-loader-clip {
  display: block;
  overflow: hidden;
}
.m-loader-clip img {
  display: block;
  width: auto;
  height: 44px;
  transform: translateY(-105%);
  transition: transform 0.45s cubic-bezier(0.19, 1, 0.22, 1);
  filter: brightness(0) invert(1);
}
.m-loader.m-in .m-loader-clip img {
  transform: none;
}
.m-loader.m-out .m-loader-clip img {
  transform: translateY(105%);
  transition-duration: 0.25s;
  transition-timing-function: cubic-bezier(0.95, 0.05, 0.795, 0.035);
}


/* ─────────────────────────────────────────────────────── 11 · Degradation */

/* site.css already kills every animation and transition under reduced motion with !important, so
   nothing declared above could run even if it were applied. The script never writes .is-motion in
   that case, which is the belt to that brace: no initial state is ever installed, and the page is
   the resting one. Repeated here only for the states that are not transitions — the ones a
   stylesheet would otherwise leave switched on.

   Written without .is-motion on purpose. Prefixed with it these selectors could not match anything:
   the class exists precisely when this query does not, so the net would be hanging in a room the
   fall can never reach. Unprefixed they are a real net, and they cost nothing when they are not
   needed — every one of them declares the resting state the rest of the page already has. */
@media (prefers-reduced-motion: reduce) {
  .m-line,
  [data-m-chars] .m-c,
  [data-m-pop] > * {
    transform: none;
    opacity: 1;
  }
  [data-m-fill] {
    -webkit-mask-image: none;
    mask-image: none;
  }
  .m-loader {
    display: none;
  }
}
