/*
 * The site's loader (includes/loader.blade.php), in place of rolling_spinner.gif
 * (Javier, 2026-10-23). Every animation here moves only `transform` and
 * `opacity`: a phone keeps those running while the page's thread is busy
 * (learning the picture), where a GIF stops. Keep it that way:
 * tests/create/loader.cjs fails on any other animated property.
 * Which look shows is .file-loading's data-mode: none (the logo), "compress",
 * "send", "place" (the story of a video, below) or "photo".
 */
.file-loading .ml-stage { position: relative; width: 150px; height: 150px; margin: 0 auto; }
.file-loading .ml-mark { position: absolute; inset: 0; display: none; }
.file-loading:not([data-mode]) .ml-default,
.file-loading[data-mode="compress"] .ml-story,
.file-loading[data-mode="send"] .ml-story,
.file-loading[data-mode="place"] .ml-story,
.file-loading[data-mode="photo"] .ml-photo { display: block; }
.file-loading .ml-stage svg { display: block; }
.file-loading p#status2 { margin-top: 18px; }

/* Default: the round logo, the viewer's red brackets closing on it (#web-boot). */
.ml-default .ml-logo { position: absolute; left: 41px; top: 41px; width: 68px; height: 68px; }
.ml-corner { position: absolute; width: 24px; height: 24px; border: 0 solid #CC0000; animation: ml-close 1.4s ease-in-out infinite; will-change: transform; }
.ml-corner.tl { top: 19px; left: 19px; border-width: 3px 0 0 3px; border-radius: 7px 0 0 0; --x: 8px; --y: 8px; }
.ml-corner.tr { top: 19px; right: 19px; border-width: 3px 3px 0 0; border-radius: 0 7px 0 0; --x: -8px; --y: 8px; }
.ml-corner.bl { bottom: 19px; left: 19px; border-width: 0 0 3px 3px; border-radius: 0 0 0 7px; --x: 8px; --y: -8px; }
.ml-corner.br { bottom: 19px; right: 19px; border-width: 0 3px 3px 0; border-radius: 0 0 7px 0; --x: -8px; --y: -8px; }
@keyframes ml-close { 0%, 100% { transform: translate(0, 0); } 50% { transform: translate(var(--x), var(--y)); } }

/*
 * A video's story (Javier, 2026-10-28, from lab videos): one ball, the grey
 * circle with the red rim and the camera, as wide as the photo it ends on.
 * "compress": it shrinks in pulses as the video gets lighter; "send": it rises
 * past clouds with trails and red sparks into ours and pops out on top with a
 * burst; "place": ours lets it go and it drops onto the photo. Its size
 * (--size) and height (--lift) are set by custom-js.js (storyAt) from the
 * percentage; everything else is CSS. A 170 px box over the 150 px stage.
 */
.ml-story { inset: -10px; overflow: hidden; }
.ml-story > * { position: absolute; }
.ml-story [data-in] { opacity: 0; }
.file-loading[data-mode="compress"] .ml-story [data-in~="compress"],
.file-loading[data-mode="send"] .ml-story [data-in~="send"],
.file-loading[data-mode="place"] .ml-story [data-in~="place"] { opacity: 1; }
.file-loading[data-mode="send"] .ml-story .ms-home:not(.near) { opacity: 0; }

.ms-ball, .ms-drop { left: 40px; top: 40px; width: 90px; height: 90px; }
.ms-ball { transform: translateY(calc(var(--lift, 0) * 1px)) scale(var(--size, 1)); }
.ms-hop, .ms-pulse, .ms-orb { position: relative; width: 100%; height: 100%; }
.ms-orb .ms-ring { position: absolute; inset: 0; border-radius: 50%; background: #F5F5F5; border: 3px solid #CC0000; }
.ms-orb .ms-cam { position: absolute; left: 22px; top: 28px; width: 50px; height: 34px; }
.ms-orb svg, .ms-cloud svg, .ms-photo svg, .ms-burst svg { display: block; width: 100%; height: 100%; }
.file-loading[data-mode="compress"] .ms-ball .ms-pulse { animation: ms-squeeze .9s ease-in-out infinite; will-change: transform; }
.file-loading[data-mode="send"] .ms-ball .ms-pulse { animation: ms-wobble .6s ease-in-out infinite; will-change: transform; }
.ms-ball.pop .ms-hop { animation: ms-pop .75s cubic-bezier(.2, .8, .3, 1.2); }
@keyframes ms-squeeze { 0%, 100% { transform: scale(1); } 28% { transform: scale(.88, .9); } 42% { transform: scale(1.04, 1.03); } 56% { transform: scale(1); } }
@keyframes ms-wobble { 0%, 100% { transform: translateX(0) rotate(0deg); } 25% { transform: translateX(-4px) rotate(-5deg); } 75% { transform: translateX(4px) rotate(5deg); } }
@keyframes ms-pop { 0% { transform: translateY(26px) scale(.7); } 45% { transform: translateY(-10px) scale(1.08, .94); } 70% { transform: translateY(3px) scale(.96, 1.04); } 100% { transform: none; } }

/* Clouds it rises past, at the sides (they slide down), and ours. */
.ms-sky { left: 0; top: 0; width: 170px; height: 170px; }
.ms-pass { position: absolute; top: 0; animation: ms-pass var(--t) linear infinite; animation-delay: calc(var(--d) * -1); will-change: transform, opacity; }
@keyframes ms-pass { 0% { opacity: 0; transform: translateY(-50px); } 12% { opacity: 1; } 85% { opacity: 1; } 100% { opacity: 0; transform: translateY(190px); } }
.ms-home, .ms-leave { left: 35px; top: 32px; width: 100px; height: 60px; }
.file-loading[data-mode="send"] .ms-home.near { animation: ms-near .7s ease-out; }
@keyframes ms-near { 0% { opacity: 0; transform: translateY(-70px); } 100% { opacity: 1; transform: none; } }
.file-loading[data-mode="place"] .ms-leave { animation: ms-leave .8s ease-in forwards; }
@keyframes ms-leave { 100% { opacity: 0; transform: translateY(-60px) scale(.8); } }

/* The burst when it comes out of our cloud. */
.ms-burst { left: 85px; top: 30px; width: 0; height: 0; }
.ms-burst i { position: absolute; left: -10px; top: -10px; width: 20px; height: 20px; opacity: 0; }
.ms-burst i.s { left: -6px; top: -6px; width: 12px; height: 12px; }
.ms-burst path { fill: #CC0000; }
.file-loading[data-mode="send"] .ms-burst.pop i { animation: ms-burst 1.1s ease-out forwards; }
@keyframes ms-burst { 0% { opacity: 0; transform: translate(0, 0) scale(.2) rotate(0deg); } 15% { opacity: 1; } 60% { opacity: 1; } 100% { opacity: 0; transform: translate(var(--bx), var(--by)) scale(1) rotate(90deg); } }

/* Speed trails and red sparks under the rising ball; they follow its height. */
.ms-fly { left: 0; top: 0; width: 170px; height: 170px; transform: translateY(calc(var(--lift, 0) * 1px)); }
.ms-trail { position: absolute; top: 112px; width: 3px; height: 22px; border-radius: 2px; background: #B9C3D6; animation: ms-trail .5s linear infinite; animation-delay: calc(var(--d, 0s) * -1); will-change: transform, opacity; }
.ms-sparkle { position: absolute; top: 108px; width: 6px; height: 6px; border-radius: 50%; background: #CC0000; animation: ms-sparkle .7s ease-out infinite; animation-delay: calc(var(--d, 0s) * -1); will-change: transform, opacity; }
@keyframes ms-trail { 0% { opacity: 0; transform: translateY(-6px) scaleY(.6); } 30% { opacity: .9; } 100% { opacity: 0; transform: translateY(30px) scaleY(1.2); } }
@keyframes ms-sparkle { 0% { opacity: 1; transform: translate(0, 0) scale(1); } 100% { opacity: 0; transform: translate(var(--sx), 34px) scale(.3); } }

/* Placing: the photo, and the ball dropping onto it. */
.ms-photo { left: 40px; top: 72px; width: 90px; height: 90px; }
.ms-photo path { fill: #0F133A; }
.file-loading[data-mode="place"] .ms-drop .ms-pulse { animation: ms-drop 2.2s ease-in infinite; will-change: transform; }
@keyframes ms-drop {
    0% { transform: translateY(-52px) scale(.5); }
    38% { transform: translateY(32px) scale(.5); animation-timing-function: ease-out; }
    46% { transform: translateY(36px) scale(.6, .4); }
    58% { transform: translateY(24px) scale(.47, .55); }
    70%, 100% { transform: translateY(32px) scale(.5); }
}

/* Photo: Magoo's photo icon posing in perspective, thin red sparkles on it. */
.ml-photo { perspective: 420px; }
.ml-disc { position: absolute; inset: 15px; border-radius: 50%; background: #F5F5F5; }
.ml-shadow { position: absolute; left: 44px; right: 44px; bottom: 30px; height: 10px; border-radius: 50%; background: rgba(15, 19, 58, 0.18); filter: blur(3px); animation: ml-shadow 6s ease-in-out infinite; will-change: transform, opacity; }
.ml-model { position: absolute; left: 39px; top: 37px; width: 72px; height: 72px; transform-style: preserve-3d; animation: ml-pose 6s ease-in-out infinite; will-change: transform; }
.ml-photo-svg { width: 100%; height: 100%; }
.ml-photo-svg path { fill: #0F133A; }
@keyframes ml-pose {
    0%, 100% { transform: rotateY(0deg) rotateX(0deg) rotateZ(0deg) translateX(0); }
    20% { transform: rotateY(-22deg) rotateX(8deg) rotateZ(-5deg) translateX(-5px); }
    42% { transform: rotateY(-8deg) rotateX(-6deg) rotateZ(2deg) translateX(-1px); }
    64% { transform: rotateY(22deg) rotateX(6deg) rotateZ(5deg) translateX(5px); }
    84% { transform: rotateY(6deg) rotateX(-8deg) rotateZ(-2deg) translateX(1px); }
}
@keyframes ml-shadow {
    0%, 100% { transform: translateX(0) scaleX(1); opacity: 1; }
    20% { transform: translateX(-6px) scaleX(0.85); opacity: 0.75; }
    64% { transform: translateX(6px) scaleX(0.85); opacity: 0.75; }
}
.ml-spark { position: absolute; width: 20px; height: 20px; margin: -10px 0 0 -10px; opacity: 0; transform: scale(0); animation: ml-spark var(--d, 1.7s) ease-in-out infinite; animation-delay: calc(var(--w, 0s) * -1); will-change: transform, opacity; }
/* Started part way through their cycles (negative delay), not after a wait:
   on iPhone a sparkle waiting for its delay did not appear while the page was
   busy learning the picture; only the one without a delay did (Javier,
   2026-10-23: "una chispa en una esquina"). */
.ml-spark.s { width: 13px; height: 13px; margin: -6.5px 0 0 -6.5px; }
.ml-spark svg { width: 100%; height: 100%; }
.ml-spark path { fill: #CC0000; }
@keyframes ml-spark {
    0%, 100% { opacity: 0; transform: scale(0) rotate(0deg); }
    22% { opacity: 0.95; transform: scale(1) rotate(20deg); }
    42% { opacity: 0.8; transform: scale(0.75) rotate(40deg); }
    60% { opacity: 0; transform: scale(0) rotate(70deg); }
}

/* The story keeps the card's height between its steps (custom-js.js
   loaderLook): an empty note keeps one line. */
.file-loading[data-mode="send"] p#status2-note,
.file-loading[data-mode="place"] p#status2-note { min-height: 1.5em; }

/* The upload's share: a red bar, scaled (never resized) by the page. */
.file-loading .ml-track { width: 70%; max-width: 240px; height: 6px; margin: 16px auto 0; border-radius: 3px; background: #F1F1F1; overflow: hidden; }
.file-loading .ml-track[hidden] { display: none; }
.file-loading .ml-fill { width: 100%; height: 100%; border-radius: 3px; background: #CC0000; transform-origin: left center; transform: scaleX(0); }
/* Before learning starts (MindAR still downloading, 20 s on 3G), the bar
   slides so the wait never looks stuck (2026-10-28). Negative delay: an
   iPhone does not start a waiting animation while the page is busy. */
.file-loading .ml-track.is-waiting .ml-fill { width: 35%; transform: translateX(-100%); animation: ml-wait 1.4s ease-in-out -0.2s infinite; }
@keyframes ml-wait {
    0% { transform: translateX(-100%); }
    100% { transform: translateX(300%); }
}

@media (prefers-reduced-motion: reduce) {
    .ml-corner, .ml-shadow, .ml-model, .ml-spark, .file-loading .ml-track.is-waiting .ml-fill, .file-loading[data-mode="compress"] .ms-ball .ms-pulse, .file-loading[data-mode="send"] .ms-ball .ms-pulse, .ms-ball.pop .ms-hop, .ms-pass, .file-loading[data-mode="send"] .ms-home.near, .file-loading[data-mode="place"] .ms-leave, .file-loading[data-mode="send"] .ms-burst.pop i, .ms-trail, .ms-sparkle, .file-loading[data-mode="place"] .ms-drop .ms-pulse { animation: none; }
    .file-loading .ml-track.is-waiting .ml-fill { transform: none; opacity: 0.4; }
    .file-loading[data-mode="place"] .ms-drop .ms-pulse { transform: translateY(32px) scale(.5); }
    .file-loading[data-mode="place"] .ms-leave { opacity: 0; }
    .ml-spark.keep { opacity: 0.95; transform: scale(1); }
}
