/*
  The motion layer. Every keyframe and every animation in the product lives here, so "what moves,
  and why" is one file rather than a hunt through component styles.

  This product does not use motion to hold attention (spec 4, 22.2). Everything below is one of:

    Arrival   something new is on screen, and the movement says where it came from
    Feedback  the child touched it, and it responded
    State     a value changed, and the change is shown rather than swapped
    Ambient   scenery, long-cycle, decorative only

  Nothing pulses to be noticed. Nothing bounces to be tapped. There is no streak that throbs and
  no reward that demands a look — those are the mechanics the specification rules out, and the
  audience is four to twelve years old.

  All of it stops. The global switch in tokens.css collapses every duration to 0.01ms for both
  the OS preference and the child's in-app setting, so animations here are written to end in
  their finished state and never to leave an element hidden.
*/

/* ------------------------------------------------------- arrival keyframes */

@keyframes sj-rise {
  from {
    opacity: 0;
    transform: translateY(0.75rem) scale(0.985);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes sj-fade {
  from {
    opacity: 0;
  }

  to {
    opacity: 1;
  }
}

@keyframes sj-pop {
  from {
    opacity: 0;
    transform: scale(0.82);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes sj-slide-up {
  from {
    transform: translateY(100%);
  }

  to {
    transform: none;
  }
}

/* --------------------------------------------------------- state keyframes */

/*
  The progress ring draws itself in from empty on first paint. No fill-mode: once the animation
  ends the element falls back to its own stroke-dashoffset attribute, which is the same value, so
  later progress changes are still handled by the transition in app.css rather than frozen here.
*/
@keyframes sj-draw-ring {
  from {
    stroke-dashoffset: var(--sj-ring-length);
  }

  to {
    stroke-dashoffset: var(--sj-ring-offset);
  }
}

@keyframes sj-shimmer {
  from {
    background-position: 200% 0;
  }

  to {
    background-position: -200% 0;
  }
}

/* ------------------------------------------------------- ambient keyframes */

/*
  A story scene, breathing sideways.

  The picture drifts from right to left across a very long cycle and comes back the same way, so a
  child looking at a page that has no clip and no animated layer is still looking at something that
  is alive rather than at a printed sheet. It is written in the standalone `translate` property
  rather than in `transform` for the reason `player.css` gives at the parallax rule: a scene layer's
  preset animation and the pointer follow are both transforms, and a third one would have to fight
  them. The three properties compose instead.
*/
@keyframes sj-scene-pan {
  from {
    translate: 1.6% 0;
  }

  to {
    translate: -1.6% 0;
  }
}

@keyframes sj-scene-drift {
  from {
    transform: scale(1.04);
  }

  to {
    transform: scale(1.1) translate(-1.5%, -1%);
  }
}

@keyframes sj-float {
  from {
    transform: translate3d(0, 0, 0);
  }

  to {
    transform: translate3d(2%, -3%, 0);
  }
}

@keyframes sj-sway {
  from {
    transform: rotate(-0.9deg);
  }

  to {
    transform: rotate(0.9deg);
  }
}

@keyframes sj-glow {
  from {
    opacity: 0.28;
    transform: scale(1);
  }

  to {
    opacity: 0.44;
    transform: scale(1.06);
  }
}

@keyframes sj-twinkle {
  0%,
  100% {
    opacity: 0.35;
  }

  50% {
    opacity: 1;
  }
}

/* ---------------------------------------------------------- arrival: pages */

/*
  A route change remounts .sj-page, so the entrance runs on navigation without any router hook.
  It is short and travels a small distance: this is orientation, not a transition effect.
*/
.sj-page {
  animation: sj-rise var(--sj-duration-normal) var(--sj-ease-spring) both;
}

.sj-hero {
  animation: sj-pop var(--sj-duration-slow) var(--sj-ease-spring) both;
}

/*
  The page turn between scenes, the same in every story.

  SceneViewport keys the picture stack on the scene id, so pressing Next replaces this element and
  the animation runs again: a short beat of the frame's own quiet surface (the delay, with `both`
  holding the `from` frame), then the new picture fading in. The beat is deliberate — a scene that
  snaps instantly into the next reads as an interruption, and a breath between pages is how a book
  is read to a child.

  Opacity only, never a transform: the plate, the planes and the presets already own `transform`,
  `translate` and `scale` between them, and a fourth hand on any of those would fight the three
  rules player.css documents. Under either reduced-motion switch the duration collapses and
  tokens.css zeroes the delay, so the swap is instant — a pause a child asked not to have is not
  smuggled back in as blankness.
*/
.sj-scene-viewport__stage {
  animation: sj-fade var(--sj-duration-slow) var(--sj-ease-standard) both;
  animation-delay: var(--sj-duration-normal);
}

/*
  The support snackbar arriving from the bottom edge. Arrival, not attention-seeking: one short
  rise, ending visible, so under reduced motion it simply appears. It animates the fixed element
  itself and has no fixed descendants, so the transform-makes-a-containing-block trap does not
  apply.
*/
.sj-snackbar {
  animation: sj-rise var(--sj-duration-slow) var(--sj-ease-spring) both;
}

/* --------------------------------------------------------- arrival: lists */

/*
  Staggered reveal. Items arrive in reading order so the eye is led through the set once, rather
  than the whole grid appearing at once. Capped at twelve: beyond that the last item would wait
  long enough to feel broken, so everything after it arrives with the twelfth.
*/
.sj-stagger > * {
  animation: sj-rise var(--sj-duration-normal) var(--sj-ease-spring) both;
  animation-delay: calc(var(--sj-stagger-step) * 12);
}

.sj-stagger > *:nth-child(1) {
  animation-delay: 0ms;
}

.sj-stagger > *:nth-child(2) {
  animation-delay: calc(var(--sj-stagger-step) * 1);
}

.sj-stagger > *:nth-child(3) {
  animation-delay: calc(var(--sj-stagger-step) * 2);
}

.sj-stagger > *:nth-child(4) {
  animation-delay: calc(var(--sj-stagger-step) * 3);
}

.sj-stagger > *:nth-child(5) {
  animation-delay: calc(var(--sj-stagger-step) * 4);
}

.sj-stagger > *:nth-child(6) {
  animation-delay: calc(var(--sj-stagger-step) * 5);
}

.sj-stagger > *:nth-child(7) {
  animation-delay: calc(var(--sj-stagger-step) * 6);
}

.sj-stagger > *:nth-child(8) {
  animation-delay: calc(var(--sj-stagger-step) * 7);
}

.sj-stagger > *:nth-child(9) {
  animation-delay: calc(var(--sj-stagger-step) * 8);
}

.sj-stagger > *:nth-child(10) {
  animation-delay: calc(var(--sj-stagger-step) * 9);
}

.sj-stagger > *:nth-child(11) {
  animation-delay: calc(var(--sj-stagger-step) * 10);
}

.sj-stagger > *:nth-child(12) {
  animation-delay: calc(var(--sj-stagger-step) * 11);
}

/* ------------------------------------------------------- arrival: overlays */

.sj-modal {
  animation: sj-fade var(--sj-duration-fast) var(--sj-ease-standard) both;
}

.sj-modal__panel {
  animation: sj-pop var(--sj-duration-normal) var(--sj-ease-spring) both;
}

.sj-state__scene,
.sj-state__icon {
  animation: sj-pop var(--sj-duration-slow) var(--sj-ease-spring) both;
}

/* -------------------------------------------------------------- state */

.sj-progress-ring__indicator {
  animation: sj-draw-ring var(--sj-duration-slow) var(--sj-ease-decelerate);
}

.sj-skeleton {
  animation: sj-shimmer 1.6s linear infinite;
}

/* ------------------------------------------------------------- ambient */

/*
  Scenery. The only motion in the product that repeats without being asked for, kept to the
  welcome hero and its artwork, and slow enough to read as light changing rather than as
  something happening.
*/
.sj-hero__scene .sj-scene__art {
  animation: sj-scene-drift var(--sj-duration-ambient) ease-in-out infinite alternate;
}

/*
  The scene a child is reading, drifting.

  Every published story is a still picture, and a still picture on a screen a child is meant to
  stay with reads as a page that has stopped. This is the smallest amount of motion that fixes
  that: about 3% of the frame's width, right to left and back, over half a minute — slow enough
  that nothing appears to be happening and the picture is simply never quite where it was. On the
  slow ambient token the travel came to a fifth of a pixel a second, which is a promise of motion
  rather than motion; this is the faster of the two and it is still under a pixel and a half.

  The 6% scale is what makes the travel possible rather than decoration. A picture that exactly
  fills its frame walks an empty edge into view the moment it is moved, so it is held slightly
  oversize and the movement crops instead — which is what `object-fit: cover` was already doing.
  The parallax follow borrows the same headroom and deliberately declares none of its own.

  Everything moves together, background and layers and the hero alike, because this is the camera
  and not the scenery: a background that panned while the characters on it stood still would come
  apart. Depth is the parallax's business, and it has its own property to say it in.

  Under either reduced-motion switch the animation collapses to a single 0.01ms pass and settles on
  the `to` frame — a picture 1.6% off centre inside 6% of headroom, which is to say a still picture
  in a frame that is still full.
*/
.sj-scene-viewport__background,
.sj-scene-viewport__drawn,
.sj-scene-viewport__plane,
.sj-scene-viewport .sj-hero__image,
.sj-scene-viewport .sj-hero__video {
  scale: 1.06;
  animation: sj-scene-pan var(--sj-duration-ambient) ease-in-out infinite alternate;
}

/*
  transform-box: fill-box makes a percentage transform-origin resolve against the element's own
  bounding box rather than the SVG viewport, which is what lets the tree pivot at its trunk
  instead of swinging around the corner of the picture.
*/
.sj-scene__sun,
.sj-scene__cloud,
.sj-scene__canopy {
  transform-box: fill-box;
}

.sj-scene__sun {
  transform-origin: center;
  animation: sj-glow calc(var(--sj-duration-ambient) * 0.5) ease-in-out infinite alternate;
}

.sj-scene__cloud {
  animation: sj-float var(--sj-duration-ambient-slow) ease-in-out infinite alternate;
}

.sj-scene__cloud--far {
  animation-duration: calc(var(--sj-duration-ambient-slow) * 1.6);
  animation-direction: alternate-reverse;
}

.sj-scene__canopy {
  transform-origin: 50% 100%;
  animation: sj-sway calc(var(--sj-duration-ambient) * 0.7) ease-in-out infinite alternate;
}

.sj-scene__sparkle {
  animation: sj-twinkle calc(var(--sj-duration-ambient) * 0.25) ease-in-out infinite;
}

.sj-scene__sparkle--offset {
  animation-delay: calc(var(--sj-duration-ambient) * 0.12);
}

/*
  The colour fields behind the app breathe on a very long cycle. At this size and softness the
  movement is below the threshold of noticing directly; it stops the background reading as a flat
  printed sheet without ever becoming something to look at.
*/
.sj-aurora__field {
  animation: sj-float var(--sj-duration-ambient-slow) ease-in-out infinite alternate;
}

.sj-aurora__field--two {
  animation-duration: calc(var(--sj-duration-ambient-slow) * 1.35);
  animation-direction: alternate-reverse;
}

.sj-aurora__field--three {
  animation-duration: calc(var(--sj-duration-ambient-slow) * 1.7);
}

/* ------------------------------------------------------------- feedback */

/*
  A light sweep across a primary button on hover. Pointer-only: it says "this is the live one" to
  someone moving a cursor, and adds nothing on a touch screen, where the press itself is the
  feedback.
*/
@media (hover: hover) and (pointer: fine) {
  .sj-button--primary {
    position: relative;
    overflow: hidden;
  }

  .sj-button--primary::after {
    content: "";
    position: absolute;
    inset-block: 0;
    inset-inline-start: -60%;
    width: 40%;
    background: linear-gradient(
      100deg,
      transparent,
      rgba(255, 255, 255, 0.28),
      transparent
    );
    transform: skewX(-18deg);
    opacity: 0;
    transition:
      inset-inline-start var(--sj-duration-slow) var(--sj-ease-standard),
      opacity var(--sj-duration-fast) var(--sj-ease-standard);
  }

  .sj-button--primary:not(:disabled):hover::after {
    inset-inline-start: 110%;
    opacity: 1;
  }
}

/*
  The bottom-nav pill grows into place behind the current icon. This is the one indicator a child
  navigates by, so its change of position is shown rather than jumped.
*/
.sj-bottom-nav__icon {
  transition:
    background-color var(--sj-duration-fast) var(--sj-ease-standard),
    transform var(--sj-duration-normal) var(--sj-ease-bounce);
}

.sj-avatar {
  transition: transform var(--sj-duration-normal) var(--sj-ease-bounce);
}

.sj-child-card:hover .sj-avatar,
.sj-avatar-choice:hover .sj-avatar {
  transform: scale(1.05) rotate(-2deg);
}

/* ------------------------------------------ feedback: the child got it right */

/*
  What happens when something goes well, and the limits it is written inside.

  This is the "feedback" job and only that job: it fires because a child did something, it fires
  once, and it is finished before they have finished reading the sentence beside it. Nothing here
  loops, nothing waits to be noticed, and nothing is on the screen because attention is valuable
  (spec 4, 22.2). A reward that kept moving would be exactly the mechanic this product refuses.

  The burst is the one animation in this file that ends invisible, and it is allowed to because it
  is never rendered when motion is unwanted — `SJCelebrate` draws nothing at all rather than relying
  on the duration collapsing. Sparkles frozen permanently over the screen is what the "ends in its
  visible state" rule exists to prevent, and drawing nothing is a stronger answer than ending
  visible would be.
*/
@keyframes sj-spark {
  from {
    opacity: 1;
    transform: rotate(var(--sj-spark-angle, 0deg)) translateY(0) scale(0.35);
  }

  to {
    opacity: 0;
    transform: rotate(var(--sj-spark-angle, 0deg)) translateY(-2.5rem) scale(1);
  }
}

.sj-celebrate__spark {
  animation: sj-spark 640ms var(--sj-ease-decelerate) both;
  animation-delay: var(--sj-spark-delay, 0ms);
}

/*
  Something earned, arriving. Ends fully visible, so a star that was won stays won when every
  duration collapses to nothing.
*/
@keyframes sj-award {
  from {
    opacity: 0;
    transform: scale(0.4) rotate(-14deg);
  }

  60% {
    opacity: 1;
    transform: scale(1.14) rotate(5deg);
  }

  to {
    opacity: 1;
    transform: scale(1) rotate(0deg);
  }
}

/*
  Stars land one after another rather than all at once, which is the difference between a screen
  that celebrates and a screen that reports. The delay per star is written by the player.
*/
.sj-star--earned {
  animation: sj-award var(--sj-duration-slow) var(--sj-ease-bounce) both;
  animation-delay: var(--sj-star-delay, 0ms);
}

.sj-player__badge-art {
  animation: sj-award var(--sj-duration-slow) var(--sj-ease-bounce) both;
  animation-delay: 420ms;
}

/* Said back, then gone. Arrival only: the message removes itself on a timer, not on a transform. */
.sj-player__flash-pill {
  animation: sj-pop var(--sj-duration-normal) var(--sj-ease-bounce) both;
}

/*
  A result the child got right, and a step of an activity being ticked off. Both are the same job as
  the burst and both end where they started, so a collapsed duration leaves the screen correct.
*/
.sj-interaction__result--correct,
.sj-interaction__complete,
.sj-quiz__feedback--correct {
  animation: sj-pop var(--sj-duration-normal) var(--sj-ease-bounce) both;
}

.sj-interaction__pip--done,
.sj-quiz__pip--done,
.sj-progress-dots__dot--done {
  transition:
    background-color var(--sj-duration-normal) var(--sj-ease-standard),
    transform var(--sj-duration-normal) var(--sj-ease-bounce);
}

/* A hotspot answering the tap that opened it. */
.sj-hotspot__dot {
  transition:
    background-color var(--sj-duration-fast) var(--sj-ease-standard),
    transform var(--sj-duration-normal) var(--sj-ease-bounce);
}

.sj-hotspot--revealed .sj-hotspot__dot {
  transform: scale(1.2);
}

/* A switch answering the press. */
.sj-toggle-chip {
  transition:
    background-color var(--sj-duration-fast) var(--sj-ease-standard),
    border-color var(--sj-duration-fast) var(--sj-ease-standard),
    transform var(--sj-duration-fast) var(--sj-ease-bounce);
}

.sj-toggle-chip:active {
  transform: scale(0.96);
}

/*
  Reduced motion is enforced globally in tokens.css rather than repeated per rule here. Both the
  OS preference and html[data-sj-reduced-motion="true"] collapse every duration above, and every
  animation in this file finishes in its visible state, so nothing is left hidden when they do.
*/

/* --------------------------------------------------- scene layer presets */

/*
  The animation vocabulary a manifest may draw on (spec 12.5), one class per preset. A content
  editor picks a preset and an intensity; nothing in a manifest is ever evaluated, so this is the
  only place a preset becomes motion.

  Duration, iteration count, and intensity arrive as custom properties written by SceneViewport
  from the manifest. Intensity is normalized 0-1 and scales the distance travelled, never the
  speed — a louder animation moves further, it does not move faster.
*/

.sj-anim {
  animation-duration: var(--sj-anim-duration, 8s);
  animation-iteration-count: var(--sj-anim-iterations, 1);
  animation-timing-function: ease-in-out;
  animation-direction: alternate;
  animation-fill-mode: both;
  transform-box: fill-box;
}

@keyframes sj-anim-fade {
  from { opacity: calc(1 - var(--sj-anim-intensity, 0.3)); }
  to { opacity: 1; }
}

/*
  Ken Burns, at a size a person can actually see.

  These were 6% and 8% at full intensity, spread over twenty seconds and running `alternate`, so a
  complete cycle took forty. Measured on a real scene the picture moved from scale 1.0138 to 1.0331
  across three seconds — running the whole time and below the threshold of noticing, which is the
  worst of both: the cost of compositing every frame and none of the benefit.

  A slow zoom in film is a fifth of the frame, not a thirtieth. Raised twice: the first attempt
  measured 5.4% of travel across three seconds and was still read as too subtle on a real screen,
  which is worth trusting over the arithmetic. Reduced motion still collapses these
  to 0.01ms, and every one of them ends where it began, so nothing disappears for a child who asked
  motion to stop.
*/
@keyframes sj-anim-pan {
  from { transform: translateX(0) scale(1.28); }
  to { transform: translateX(calc(var(--sj-anim-intensity, 0.3) * -24%)) scale(1.28); }
}

@keyframes sj-anim-zoom {
  from { transform: scale(1); }
  to { transform: scale(calc(1 + var(--sj-anim-intensity, 0.3) * 0.38)); }
}

@keyframes sj-anim-drift {
  from { transform: translate3d(0, 0, 0) scale(1.28); }
  to { transform: translate3d(calc(var(--sj-anim-intensity, 0.3) * 26%), calc(var(--sj-anim-intensity, 0.3) * -8%), 0) scale(1.28); }
}

@keyframes sj-anim-ripple {
  from { transform: translateY(0) scaleY(1); }
  to { transform: translateY(calc(var(--sj-anim-intensity, 0.3) * 1%)) scaleY(calc(1 + var(--sj-anim-intensity, 0.3) * 0.02)); }
}

@keyframes sj-anim-flicker {
  from { opacity: calc(1 - var(--sj-anim-intensity, 0.3) * 0.35); }
  to { opacity: 1; }
}

@keyframes sj-anim-sway {
  from { transform: rotate(calc(var(--sj-anim-intensity, 0.3) * -2deg)); }
  to { transform: rotate(calc(var(--sj-anim-intensity, 0.3) * 2deg)); }
}

@keyframes sj-anim-path {
  from { transform: translate3d(calc(var(--sj-anim-intensity, 0.3) * -10%), 0, 0); }
  to { transform: translate3d(calc(var(--sj-anim-intensity, 0.3) * 10%), calc(var(--sj-anim-intensity, 0.3) * -4%), 0); }
}

@keyframes sj-anim-blink {
  0%, 92%, 100% { transform: scaleY(1); }
  96% { transform: scaleY(0.1); }
}

@keyframes sj-anim-breathe {
  from { transform: scale(1); }
  to { transform: scale(calc(1 + var(--sj-anim-intensity, 0.3) * 0.02)); }
}

@keyframes sj-anim-gesture {
  from { transform: rotate(0deg); }
  to { transform: rotate(calc(var(--sj-anim-intensity, 0.3) * 6deg)); }
}

@keyframes sj-anim-pulse {
  from { filter: brightness(1); }
  to { filter: brightness(calc(1 + var(--sj-anim-intensity, 0.3) * 0.25)); }
}

.sj-anim--fade { animation-name: sj-anim-fade; }
.sj-anim--slow-pan { animation-name: sj-anim-pan; }
.sj-anim--slow-zoom { animation-name: sj-anim-zoom; }
.sj-anim--parallax { animation-name: sj-anim-pan; }
.sj-anim--cloud-drift { animation-name: sj-anim-drift; }
.sj-anim--water-ripple { animation-name: sj-anim-ripple; }
.sj-anim--fire-flicker { animation-name: sj-anim-flicker; }
.sj-anim--light-shimmer { animation-name: sj-anim-flicker; }
.sj-anim--leaf-sway { animation-name: sj-anim-sway; }
.sj-anim--bird-path { animation-name: sj-anim-path; }
.sj-anim--character-blink { animation-name: sj-anim-blink; animation-direction: normal; }
.sj-anim--subtle-breathing { animation-name: sj-anim-breathe; }
.sj-anim--hand-gesture { animation-name: sj-anim-gesture; }
.sj-anim--object-highlight-pulse { animation-name: sj-anim-pulse; }
