/* ═══════════════════ MOTION SYSTEM ═══════════════════════════════════════
   Phase 0 (tokens) + Phase 1 (primitives).

   Purely additive: this file defines tokens and opt-in utility classes only.
   It overrides nothing in styles.css and no element animates until a class is
   applied. Deleting this file leaves the app functionally identical — that is
   the property that keeps a motion pass from turning into a rewrite.

   Three rules everything here obeys:
     1. transform + opacity ONLY. Never width/height/top/left/box-shadow —
        anything else drops frames on the mid-range Androids our students use.
     2. Direction is expressed through var(--mo-dir), never a hard-coded sign,
        so every travelling animation mirrors correctly in Arabic.
     3. prefers-reduced-motion is handled once, at the token level, so it can
        never be forgotten at an individual call site.
   ───────────────────────────────────────────────────────────────────────── */

:root {
  /* Durations and easings DERIVE from the scale styles.css already defines
     (--dur-0..5, --ease*). They are deliberately not redeclared with literal
     values: styles.css owns the design scale, and retuning --dur-3 there must
     move this layer with it. Only motion-specific concepts are new below. */
  --mo-dur-fast: var(--dur-1);   /* 120ms — micro-feedback */
  --mo-dur-base: var(--dur-3);   /* 250ms — panels, cards, entrances */
  --mo-dur-slow: var(--dur-4);   /* 350ms — overlays, screen changes */

  --mo-ease: var(--ease);
  --mo-ease-out: var(--ease-out);
  --mo-ease-in: var(--ease-in);
  --mo-ease-in-out: var(--ease-smooth);
  /* Clinical resolves --mo-spring to a NON-overshooting curve on purpose. */
  --mo-spring: var(--ease-out);

  /* Travel distances. Kept small: restraint is what reads as "premium". */
  --mo-rise: 8px;
  --mo-slide: 16px;
  --mo-lift: 2px;

  --mo-stagger-step: 40ms;

  /* Writing-direction multiplier. Every translateX is multiplied by this. */
  --mo-dir: 1;
}

[dir="rtl"] { --mo-dir: -1; }

/* Competition surfaces: same class names, more expressive resolution. Scope it
   by putting data-motion="competition" on a container. */
[data-motion="competition"] {
  --mo-dur-base: var(--dur-4);
  --mo-dur-slow: var(--dur-5);
  --mo-spring: var(--ease-spring);   /* the existing subtle-overshoot curve */
  --mo-rise: 14px;
  --mo-lift: 6px;
  --mo-stagger-step: 55ms;
}

/* Clinical surfaces may opt in explicitly where a competition container would
   otherwise cascade into them (e.g. a screening result inside a student view). */
[data-motion="clinical"] {
  --mo-dur-base: var(--dur-3);
  --mo-dur-slow: var(--dur-4);
  --mo-spring: var(--ease-out);                   /* no overshoot, ever */
  --mo-rise: 8px;
  --mo-lift: 2px;
  --mo-stagger-step: 40ms;
}

/* Collapse every duration at the source. styles.css already ships a global
   !important reduced-motion rule; this additionally neutralises any animation
   driven from these tokens (including future Web Animations calls that read
   them), rather than relying on that one blanket rule. */
@media (prefers-reduced-motion: reduce) {
  :root,
  [data-motion="competition"],
  [data-motion="clinical"] {
    --mo-dur-fast: .01ms;
    --mo-dur-base: .01ms;
    --mo-dur-slow: .01ms;
    --mo-rise: 0px;
    --mo-slide: 0px;
    --mo-lift: 0px;
    --mo-stagger-step: 0ms;
  }
}


/* ──── PRIMITIVES — entrances ──── */

@keyframes mo-fade-in { from { opacity: 0; } to { opacity: 1; } }

@keyframes mo-rise-in {
  from { opacity: 0; transform: translate3d(0, var(--mo-rise), 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}

@keyframes mo-slide-in {
  from { opacity: 0; transform: translate3d(calc(var(--mo-slide) * var(--mo-dir)), 0, 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}

@keyframes mo-scale-in {
  from { opacity: 0; transform: scale(.96); }
  to   { opacity: 1; transform: scale(1); }
}

.mo-fade-in  { animation: mo-fade-in  var(--mo-dur-base) var(--mo-ease-out) both; }
.mo-rise-in  { animation: mo-rise-in  var(--mo-dur-base) var(--mo-ease-out) both; }
.mo-slide-in { animation: mo-slide-in var(--mo-dur-base) var(--mo-ease-out) both; }
.mo-scale-in { animation: mo-scale-in var(--mo-dur-base) var(--mo-spring)   both; }


/* ──── PRIMITIVES — stagger ────
   Children animate in sequence. nth-child rather than a JS-set custom property
   so this stays CSS-only; 12 covers our longest visible list before scroll. */
.mo-stagger > *      { animation: mo-rise-in var(--mo-dur-base) var(--mo-ease-out) both; }
.mo-stagger > *:nth-child(1)  { animation-delay: calc(var(--mo-stagger-step) * 0); }
.mo-stagger > *:nth-child(2)  { animation-delay: calc(var(--mo-stagger-step) * 1); }
.mo-stagger > *:nth-child(3)  { animation-delay: calc(var(--mo-stagger-step) * 2); }
.mo-stagger > *:nth-child(4)  { animation-delay: calc(var(--mo-stagger-step) * 3); }
.mo-stagger > *:nth-child(5)  { animation-delay: calc(var(--mo-stagger-step) * 4); }
.mo-stagger > *:nth-child(6)  { animation-delay: calc(var(--mo-stagger-step) * 5); }
.mo-stagger > *:nth-child(7)  { animation-delay: calc(var(--mo-stagger-step) * 6); }
.mo-stagger > *:nth-child(8)  { animation-delay: calc(var(--mo-stagger-step) * 7); }
.mo-stagger > *:nth-child(9)  { animation-delay: calc(var(--mo-stagger-step) * 8); }
.mo-stagger > *:nth-child(10) { animation-delay: calc(var(--mo-stagger-step) * 9); }
.mo-stagger > *:nth-child(11) { animation-delay: calc(var(--mo-stagger-step) * 10); }
.mo-stagger > *:nth-child(12) { animation-delay: calc(var(--mo-stagger-step) * 11); }
.mo-stagger > *:nth-child(n+13) { animation-delay: calc(var(--mo-stagger-step) * 12); }


/* ──── PRIMITIVES — interaction feedback ──── */

/* Tactile press. Pointer-coarse only for :hover lift so phones don't get a
   sticky hover state after tap. */
.mo-press {
  transition: transform var(--mo-dur-fast) var(--mo-ease);
  will-change: transform;
}
.mo-press:active { transform: scale(.97); }

@media (hover: hover) {
  .mo-lift { transition: transform var(--mo-dur-fast) var(--mo-ease); }
  .mo-lift:hover { transform: translate3d(0, calc(var(--mo-lift) * -1), 0); }
}


/* ──── PRIMITIVES — pending states ──── */

/* Skeleton shimmer. Animates background-position (compositor-friendly here
   because it is a gradient on a fixed-size box, not a layout property). */
@keyframes mo-shimmer { to { background-position-x: -200%; } }

.mo-skeleton {
  background: linear-gradient(90deg,
              var(--gray-200, #e5e7eb) 25%,
              var(--gray-100, #f3f4f6) 37%,
              var(--gray-200, #e5e7eb) 63%);
  background-size: 200% 100%;
  animation: mo-shimmer 1.4s linear infinite;
  border-radius: 6px;
}

@keyframes mo-spin { to { transform: rotate(360deg); } }
.mo-spin { animation: mo-spin .9s linear infinite; }

/* Indeterminate progress bar — for "analysing…" where we cannot honestly show
   percent complete. Travels with the writing direction. */
@keyframes mo-indeterminate {
  from { transform: translate3d(calc(-100% * var(--mo-dir)), 0, 0); }
  to   { transform: translate3d(calc(200% * var(--mo-dir)), 0, 0); }
}
.mo-indeterminate { overflow: hidden; position: relative; }
.mo-indeterminate::after {
  content: ""; position: absolute; inset: 0;
  width: 50%;
  background: currentColor;
  opacity: .28;
  animation: mo-indeterminate 1.1s var(--mo-ease-in-out) infinite;
}


/* ──── LOW-END DEVICE TIER (phase 2) ────
   app.js sets data-motion-tier="low" from navigator.deviceMemory /
   hardwareConcurrency. Our student cohort is largely on mid/low-end Android;
   there, a shorter animation that lands immediately beats a graceful one that
   drops frames. Responsiveness > smoothness. Distances shrink but do not go to
   zero — that is reserved for prefers-reduced-motion, which is a stated user
   preference rather than a hardware guess. */
[data-motion-tier="low"] {
  --mo-dur-fast: var(--dur-0);
  --mo-dur-base: var(--dur-2);
  --mo-dur-slow: var(--dur-3);
  --mo-rise: 4px;
  --mo-slide: 8px;
  --mo-stagger-step: 20ms;
}
[data-motion-tier="low"] .mo-skeleton { animation-duration: 2s; }


/* ──── SCREEN TRANSITIONS (phase 2) ────
   Toggling [hidden] flips display, which restarts CSS animations — so this
   needs no JS and cannot desynchronise from the screen controller. Deliberately
   NOT the View Transitions API: startViewTransition defers the DOM mutation to
   a later frame, and screen-state.activate() is synchronous with callers across
   app.js that read the DOM immediately after it returns. */
/* OPACITY ONLY — no transform, ever, on .screen.
   A transform on an element makes it the containing block for every
   position: fixed descendant. The 6px rise this used to have re-anchored the
   bottom tab bar to the screen instead of the viewport: on a phone the bar
   landed at the end of the page, so changing tabs meant scrolling all the way
   down first. `both` fill made it permanent, and translate3d(0, 0, 0) is still
   a transform, so finishing the animation never released it.

   Ending at `transform: none` is NOT a sufficient fix either — the containing
   block still exists while the animation runs. The same trap applies to
   filter, backdrop-filter, perspective, contain and will-change, and .screen
   wraps the whole authenticated app including every fixed overlay. */
@keyframes mo-screen-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
.screen:not([hidden]) {
  animation: mo-screen-in var(--mo-dur-slow) var(--mo-ease-out) both;
}
/* The boot screen is the first paint. Fading it in delays the moment the user
   sees anything at all, which reads as a slower app rather than a smoother one
   — so it is exempt and appears instantly. */
.screen.boot-screen:not([hidden]) { animation: none; }


/* ──── MODAL / OVERLAY ENTER + EXIT (phase 5) ────
   @starting-style gives a real entry transition without a JS "is-open" class.
   Chrome 121+/Safari 17.5+/Firefox 129+; older browsers simply get the end
   state immediately, which is a correct, non-broken fallback. */
.mo-overlay {
  opacity: 1;
  transition: opacity var(--mo-dur-base) var(--mo-ease-out);
}
@starting-style { .mo-overlay { opacity: 0; } }

.mo-overlay-card {
  opacity: 1;
  transform: scale(1);
  transition: opacity var(--mo-dur-base) var(--mo-ease-out),
              transform var(--mo-dur-base) var(--mo-spring);
}
@starting-style {
  .mo-overlay-card { opacity: 0; transform: scale(.97); }
}


/* ──── REVEAL-ON-SCROLL (CSS half) ────
   The IntersectionObserver that adds .is-revealed lands in Phase 2. Until then
   this is inert: without the observer nothing carries .mo-reveal. */
.mo-reveal { opacity: 0; transform: translate3d(0, var(--mo-rise), 0); }
.mo-reveal.is-revealed {
  opacity: 1;
  transform: translate3d(0, 0, 0);
  transition: opacity var(--mo-dur-slow) var(--mo-ease-out),
              transform var(--mo-dur-slow) var(--mo-ease-out);
}
