/* =============================================================================
   Anakgym Motion System
   -----------------------------------------------------------------------------
   Every movement in the app is defined here: how modals arrive and leave, how
   sheets slide, how a page pushes in, how a list staggers, what a tap feels
   like, what loading looks like.

   Why one file. Before this, motion was scattered across four stylesheets: 28
   keyframes in four places, 18 spellings of a duration, six easings of which
   two were the same curve written differently, and 17 separate
   prefers-reduced-motion blocks. Nothing said what the app's movement WAS, so
   every new screen invented its own.

   How to use it. Reach for a ROLE class (section 4) — .ank-m-rise-in,
   .ank-m-sheet-in, .ank-m-pop-in — never a raw @keyframes name and never a
   hand-written duration. To retune how the whole app feels, change the tokens
   in section 1; nothing else should need touching.

   What deliberately stays OUT of this file: one-off choreography that belongs
   to exactly one component and means nothing elsewhere — the body-weight chart
   drawing its line, the login screen's ambient drift, the splash pop (which
   lives inline in index.html because it must paint before any stylesheet
   loads). Those stay next to the thing they animate. This file holds the
   shared vocabulary, not every movement in the codebase.
   ============================================================================= */

/* ---------------------------------------------------------------------------
   1. TOKENS — the single source for how fast and with what curve.
   --------------------------------------------------------------------------- */
:root {
  /* Curves. --ank-ease is the workhorse: a decelerate that starts fast and
     settles, which is what makes an interface feel responsive rather than
     floaty. --ank-ease-pop overshoots slightly and is for arrivals only. */
  --ank-ease:      cubic-bezier(.22, 1, .36, 1);
  --ank-ease-out:  cubic-bezier(.2, .8, .2, 1);
  --ank-ease-pop:  cubic-bezier(.34, 1.56, .64, 1);
  --ank-ease-in:   cubic-bezier(.4, 0, 1, 1);      /* exits: start slow, leave fast */

  /* Durations. These are the values the app already moved at; the scale names
     them, it does not retune them. Exits are deliberately shorter than
     entrances — a thing arriving deserves to be watched, a thing leaving does
     not, and a slow dismissal is the single most common way an interface
     starts to feel sluggish. */
  --ank-dur-press: .08s;   /* tap/press feedback — must stay under ~.1s */
  --ank-dur-quick: .12s;
  --ank-dur-snap:  .15s;   /* the workhorse: hover, colour, opacity */
  --ank-dur-fast:  .18s;
  --ank-dur-mid:   .2s;
  --ank-dur:       .26s;   /* default enter */
  --ank-dur-slow:  .3s;
  --ank-dur-exit:  .18s;   /* default leave — shorter than the enter on purpose */

  /* Travel distances. Naming these stops "10px here, 26px there, 12px in the
     other place" from creeping back in. */
  --ank-travel-sm: 10px;   /* a dialog rising into place */
  --ank-travel-md: 26px;   /* a page pushing in from the side */
}

/* ---------------------------------------------------------------------------
   2. REDUCED MOTION — ONE global guard.
   Replaces 17 hand-maintained blocks that each listed their own selectors and
   only ever covered animations somebody remembered to add. A blanket rule
   cannot slip, so new animations are covered by default.

   `animation: none` rather than the more modern `animation-duration: .01ms`:
   .01ms lets an animation RUN and land on its 100% frame, while `none` leaves
   the element at its natural CSS state. Those differ for any keyframe whose
   end state is not the resting state, and this app has plenty. Every block
   this replaced used `none`, so `none` keeps behaviour identical.

   The !important is load-bearing, not laziness: the app sets ~2050 inline
   styles from index.html and js/, and an accessibility override has to beat
   those. Deliberate exceptions re-enable one specific, non-vestibular
   transition; they live next to their component in a later stylesheet and
   carry !important so they win over this block.
   --------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation: none !important;
    transition: none !important;
    scroll-behavior: auto !important;
  }
}

/* ---------------------------------------------------------------------------
   3. KEYFRAMES — the shared vocabulary. Paired in/out so every arrival has a
   matching departure; before this, nothing in the app had an exit animation at
   all, which is why closing anything felt abrupt next to opening it.
   --------------------------------------------------------------------------- */

/* fade — the neutral one, for scrims and tab panels */
@keyframes ank-m-fade-in  { from { opacity: 0; } to { opacity: 1; } }
@keyframes ank-m-fade-out { from { opacity: 1; } to { opacity: 0; } }

/* rise — a centred dialog arriving. Small travel: a dialog is already where
   you are looking, so it only needs to settle, not to fly. */
@keyframes ank-m-rise-in {
  from { opacity: 0; transform: translateY(var(--ank-travel-sm)); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes ank-m-rise-out {
  from { opacity: 1; transform: translateY(0); }
  to   { opacity: 0; transform: translateY(var(--ank-travel-sm)); }
}

/* pop — grows from where you tapped. The origin is supplied per-open by
   js/ui/motion.js, which measures the button you pressed; without it the
   fallback is the centre, so the animation still works if the JS never runs. */
@keyframes ank-m-pop-in {
  from { opacity: 0; transform: scale(.86); }
  to   { opacity: 1; transform: scale(1); }
}
/* The exit does not just shrink — it TRAVELS back toward the button that
   opened the modal, then shrinks into it. js/ui/motion.js measures that
   direction and writes it as --ank-m-dx/--ank-m-dy.
   The fallback is `0, 28px`: straight down. That is the right default because
   the buttons that open most modals live in the bottom nav, and it means a
   modal opened without a known source still reads as being put away rather
   than blinking out. */
@keyframes ank-m-pop-out {
  from { opacity: 1; transform: translate(0, 0) scale(1); }
  to   { opacity: 0;
         transform: translate(var(--ank-m-dx, 0px), var(--ank-m-dy, 28px)) scale(.86); }
}

/* sheet — a panel owned by the bottom edge, so it travels its own full height
   rather than a fixed distance.
   PLUS whatever gap sits between it and that edge. 100% alone is only correct
   for a sheet flush to the bottom; a sheet that floats ABOVE the nav ends its
   travel still poking onto the screen by exactly that gap, and then vanishes.
   Measured when this was found: the Profil "Buka sebagai" sheet is 190px tall
   and floats clear of the nav, so nearly HALF of it was still visible when it
   was hidden — it read as not sliding at all. The "+" sheet has the same gap but
   is 322px tall, so the same defect only cost it 29% and passed unnoticed.
   --ank-m-sheet-gap is 0 by default, so a flush sheet is unchanged. */
@keyframes ank-m-sheet-in {
  from { transform: translateY(calc(100% + var(--ank-m-sheet-gap, 0px))); }
  to   { transform: translateY(0); }
}
@keyframes ank-m-sheet-out {
  from { transform: translateY(0); }
  to   { transform: translateY(calc(100% + var(--ank-m-sheet-gap, 0px))); }
}

/* page — a full-screen view pushed on from the right, iOS navigation style.
   Reversing it on the way out is what makes back feel like back. */
@keyframes ank-m-page-in {
  from { opacity: 0; transform: translateX(var(--ank-travel-md)); }
  to   { opacity: 1; transform: translateX(0); }
}
@keyframes ank-m-page-out {
  from { opacity: 1; transform: translateX(0); }
  to   { opacity: 0; transform: translateX(var(--ank-travel-md)); }
}

/* list — one row arriving. Stagger it with --ank-m-i (section 4). */
@keyframes ank-m-list-in {
  from { opacity: 0; transform: translateY(var(--ank-travel-sm)); }
  to   { opacity: 1; transform: translateY(0); }
}

/* feedback + loading */
@keyframes ank-m-tap    { 0% { transform: scale(1); } 40% { transform: scale(1.22); } 70% { transform: scale(.94); } 100% { transform: scale(1); } }
@keyframes ank-m-spin   { to { transform: rotate(360deg); } }
@keyframes ank-m-shimmer{ 100% { transform: translateX(100%); } }

/* ---------------------------------------------------------------------------
   4. ROLES — what you actually put on an element.
   --------------------------------------------------------------------------- */
.ank-m-fade-in   { animation: ank-m-fade-in  var(--ank-dur-fast) var(--ank-ease) both; }
.ank-m-fade-out  { animation: ank-m-fade-out var(--ank-dur-exit) var(--ank-ease-in) both; }

.ank-m-rise-in   { animation: ank-m-rise-in  var(--ank-dur) var(--ank-ease) both; }
.ank-m-rise-out  { animation: ank-m-rise-out var(--ank-dur-exit) var(--ank-ease-in) both; }

.ank-m-pop-in    { animation: ank-m-pop-in  var(--ank-dur) var(--ank-ease-pop) both; }
.ank-m-pop-out   { animation: ank-m-pop-out var(--ank-dur-exit) var(--ank-ease-in) both; }

.ank-m-sheet-in  { animation: ank-m-sheet-in  var(--ank-dur) var(--ank-ease) both; }
.ank-m-sheet-out { animation: ank-m-sheet-out var(--ank-dur-exit) var(--ank-ease-in) both; }

.ank-m-page-in   { animation: ank-m-page-in  var(--ank-dur) var(--ank-ease) both; }
.ank-m-page-out  { animation: ank-m-page-out var(--ank-dur-exit) var(--ank-ease-in) both; }

.ank-m-tap       { animation: ank-m-tap .32s var(--ank-ease); }
.ank-m-spin      { animation: ank-m-spin 1s linear infinite; }

/* Anything animated from a tap grows out of the tap. js/ui/motion.js writes
   --ank-m-ox / --ank-m-oy as percentages of the element's own box; the 50%
   fallback means a missing value degrades to a plain centred pop rather than
   to nothing. */
.ank-m-pop-in,
.ank-m-pop-out { transform-origin: var(--ank-m-ox, 50%) var(--ank-m-oy, 50%); }

/* Staggered list entrance. Set --ank-m-i on each row (0, 1, 2 …) and the delay
   follows; capped so a long list never leaves the reader waiting on row 30. */
.ank-m-list-in {
  animation: ank-m-list-in var(--ank-dur) var(--ank-ease) backwards;
  animation-delay: calc(min(var(--ank-m-i, 0), 7) * 40ms);
}

/* ---------------------------------------------------------------------------
   5. WIRED IN — the roles above, attached to the structures that already exist
   so the whole app picks up the system without every call site changing.
   The .is-closing class is added by js/ui/motion.js just before a modal hides.
   --------------------------------------------------------------------------- */

/* A modal's scrim fades; its inner box pops from wherever you tapped. */
.wt-modal                        { animation: ank-m-fade-in var(--ank-dur-fast) var(--ank-ease) both; }
.wt-modal .wt-modal-inner        { transform-origin: var(--ank-m-ox, 50%) var(--ank-m-oy, 50%);
                                   animation: ank-m-pop-in var(--ank-dur) var(--ank-ease-pop) both; }
/* Held on screen for exactly one exit animation. `display` here is CSS-side
   only — the inline `display:none` the app wrote stays untouched, so every
   read of el.style.display still reports the modal as closed the instant it
   was closed. pointer-events is off so the dying modal cannot swallow a tap
   meant for whatever is behind it. */
.wt-modal.is-closing,
.wt-modal.is-closing *           { pointer-events: none !important; }
.wt-modal.is-closing             { display: flex !important;
                                   animation: ank-m-fade-out var(--ank-dur-exit) var(--ank-ease-in) both; }
/* The !important display is what lets closeModal() set the app's own
   `display:none` immediately — so every read of el.style.display is honest the
   instant the modal closes — while the pixels stay for one animation. */
.wt-modal.is-closing .wt-modal-inner { animation: ank-m-pop-out var(--ank-dur-exit) var(--ank-ease-in) both; }

/* Sheets and pages animate differently, and BOTH read the same data-modal the
   surface reads in design-system.css. This replaced two separate guesses: the
   sheet rule matched on component classes, the page rule on a hand-written
   list of four ids — which had already missed upgradeModal, tutorialModal and
   createPostModal. One declared role cannot drift from itself. */
[data-modal="sheet"] .wt-modal-inner,
[data-modal="sheet"] .wt-share-sheet,
[data-modal="sheet"] .wt-paywall-sheet {
  animation: ank-m-sheet-in var(--ank-dur) var(--ank-ease) both;
}
.is-closing[data-modal="sheet"] .wt-modal-inner,
.is-closing[data-modal="sheet"] .wt-share-sheet,
.is-closing[data-modal="sheet"] .wt-paywall-sheet {
  animation: ank-m-sheet-out var(--ank-dur-exit) var(--ank-ease-in) both;
}
/* The -cx keyframe pair that used to live here is gone: it existed only to
   compensate for a translateX(-50%) on the Profil sheet, and that sheet now
   centres with auto margins instead. One pair of sheet keyframes serves every
   sheet in the app. */

/* The promo poster view is a page (no sheet surface) but moves like a sheet: the picture slides up from below
   the screen and back down (owner 2026-10-02). It is centred, so the travel adds half the screen to clear it. */
.gym-poster-view .gym-poster-frame {
  --ank-m-sheet-gap: 50vh;
  animation: ank-m-sheet-in var(--ank-dur) var(--ank-ease) both;
}
.is-closing.gym-poster-view .gym-poster-frame {
  animation: ank-m-sheet-out var(--ank-dur-exit) var(--ank-ease-in) both;
}

/* Full-screen views push in from the side, iOS navigation style. Reversing it
   on the way out is what makes "back" feel like back. */
[data-modal="page"] .wt-modal-inner,
[data-modal="page"] > div:first-child {
  animation: ank-m-page-in var(--ank-dur) var(--ank-ease) both;
}
.is-closing[data-modal="page"] .wt-modal-inner,
.is-closing[data-modal="page"] > div:first-child {
  animation: ank-m-page-out var(--ank-dur-exit) var(--ank-ease-in) both;
}

/* --- Bottom-sheet menus that are NOT .wt-modal ---------------------------
   The "+" menu and the Profil kebab menu behave exactly like sheets but never
   used the modal markup, so neither the role system nor the observer reached
   them. The result was visible: both slid up on open and then vanished on
   close with no animation at all — the + menu because its exit was never
   WRITTEN (the old .ank-anim-* family had entrances only), the Profil menu
   because its exit existed but nothing ever added .is-closing.

   They keep their own markup; what they gain here is the same in/out pair the
   sheet role gives a modal. js/ui/motion.js exports playExit() for the close
   paths, which already set display themselves.

   The display value has to be restated per element because it differs (the +
   menu is a flex scrim, the Profil menu a block sheet) — there is no generic
   "whatever it was" in CSS. */
/* Both of these sheets are held clear of the bottom nav by the scrim's
   padding-bottom (redesign.css). The travel has to cover that gap too, or they
   stop short of the edge and disappear mid-air — the value is the SAME
   expression as that padding, and must be changed with it. */
#ankQuickMenu .ank-quick,
#ankProfilSheet .ank-quick {
  /* MUST equal the sheets' own padding-bottom, or the sheet stops short of the
     screen edge and vanishes mid-air (§11.1 #14). One token now holds both. */
  --ank-m-sheet-gap: var(--ank-sheet-lift);
}
#ankQuickMenu .ank-quick {
  animation: ank-m-sheet-in var(--ank-dur) var(--ank-ease) both;
}
#ankQuickMenu.is-closing {
  display: flex !important;
  pointer-events: none;
  animation: ank-m-fade-out var(--ank-dur-exit) var(--ank-ease-in) both;
}
#ankQuickMenu.is-closing .ank-quick {
  animation: ank-m-sheet-out var(--ank-dur-exit) var(--ank-ease-in) both;
}

/* The Profil sheet (coach accounts: "Buka sebagai" → Profil / Coach Mode) is the
   SAME component: same .ank-quick surface, same bottom scrim. It must not be
   listed separately from the "+" menu — both of them carried the old
   .ank-anim-up together, and wiring only one of them back is exactly how this
   sheet ended up appearing and vanishing with no animation at all. */
#ankProfilSheet .ank-quick {
  animation: ank-m-sheet-in var(--ank-dur) var(--ank-ease) both;
}
#ankProfilSheet.is-closing {
  display: flex !important;
  pointer-events: none;
  animation: ank-m-fade-out var(--ank-dur-exit) var(--ank-ease-in) both;
}
#ankProfilSheet.is-closing .ank-quick {
  animation: ank-m-sheet-out var(--ank-dur-exit) var(--ank-ease-in) both;
}

#forumProfileMenu.is-closing      { display: block !important; pointer-events: none; }
#forumProfileMenuScrim.is-closing { display: block !important; pointer-events: none;
                                    animation: ank-m-fade-out var(--ank-dur-exit) var(--ank-ease-in) both; }

/* Switching tabs fades the new panel in. Opacity ONLY, never transform: a
   transform here would create a containing block for position:fixed children
   and detach the sticky headers inside. */
.wt-tab-panel { animation: ank-m-fade-in .22s var(--ank-ease-out); }
