/* ============================================================
   Taurifer — motion discipline pass
   Reference: Emil Kowalski's emil-design-eng skill.

   This layer narrows motion in the high-frequency training loop, preserves
   expressive beats for rarer milestones, and prevents pointer-only hover
   treatments from becoming sticky touch states.
   ============================================================ */

/* ---- Frequent navigation and press feedback ---- */
.view{
  animation-duration:.14s;
  animation-timing-function:cubic-bezier(.2,.7,.2,1);
}
.btn{transition-duration:.1s,.18s,.18s,.18s}
.btn:active{transform:translateY(1px) scale(.975)}
nav button:active{transform:scale(.975)}

/* ---- Set completion: acknowledgement, not ceremony ----
   A lifter can trigger this dozens of times in one session. Keep the state
   change legible, but finish the whole beat inside ~160ms and never grow an
   element from a tiny scale. Exercise completion is the shelf's actions rising
   in (.motion-rise below), not a larger celebration. The ledger row's own beat is
   `.ledgerline.is-fresh` (styles.css, setland-row); the rules that named the retired
   ledger and set-count markup are gone. */

/* ---- Effort explainer: emerge from the value without growing from nothing ---- */
.effortpop{
  transform:translateY(6px) scale(.96);
  transform-origin:50% 100%;
}
.effortpop.is-open{
  animation:taurifer-effort-in .18s cubic-bezier(.2,.8,.2,1) both;
}
.effortpop.is-closing{
  animation:taurifer-effort-out .13s cubic-bezier(.4,0,.8,.2) both;
}
@keyframes taurifer-effort-in{
  0%{opacity:0;transform:translateY(6px) scale(.96);filter:blur(1px)}
  100%{opacity:1;transform:none;filter:blur(0)}
}
@keyframes taurifer-effort-out{
  from{opacity:1;transform:none}
  to{opacity:0;transform:translateY(4px) scale(.97)}
}
.effortpop.is-open .effortpop__arrow{
  animation:taurifer-effort-arrow .16s .02s cubic-bezier(.2,.8,.2,1) both;
}
@keyframes taurifer-effort-arrow{
  from{opacity:0;transform:rotate(45deg) scale(.9)}
  to{opacity:1;transform:rotate(45deg) scale(1)}
}
.effortpop.is-bump .effortpop__hint{
  animation:taurifer-effort-swap .18s cubic-bezier(.2,.8,.2,1) both;
}
@keyframes taurifer-effort-swap{
  0%{opacity:.45;transform:translateX(-50%) translateY(-3px) scale(.98)}
  60%{opacity:1;transform:translateX(-50%) scale(1.02)}
  100%{opacity:1;transform:translateX(-50%) scale(1)}
}

/* ---- Transient runtime UI: interruptible state transitions ----
   Keep these nodes mounted while hidden so both entry and exit can transition.
   Visibility changes after the exit; pointer input is disabled immediately. */
.toast{
  animation:none;
  opacity:1;
  visibility:visible;
  transition:opacity .16s cubic-bezier(.2,.7,.2,1),
             transform .16s cubic-bezier(.2,.7,.2,1),
             visibility 0s linear 0s;
}
.toast.hidden{
  display:block!important;
  opacity:0;
  visibility:hidden;
  pointer-events:none;
  transform:translateX(-50%) translateY(8px);
  transition:opacity .13s cubic-bezier(.4,0,.8,.2),
             transform .13s cubic-bezier(.4,0,.8,.2),
             visibility 0s linear .13s;
}
.tour{
  opacity:1;
  visibility:visible;
  transform:translateY(0);
  transition:opacity .16s cubic-bezier(.2,.7,.2,1),
             transform .16s cubic-bezier(.2,.7,.2,1),
             visibility 0s linear 0s;
}
.tour.hidden{
  display:flex!important;
  opacity:0;
  visibility:hidden;
  pointer-events:none;
  transform:translateY(8px);
  transition:opacity .13s cubic-bezier(.4,0,.8,.2),
             transform .13s cubic-bezier(.4,0,.8,.2),
             visibility 0s linear .13s;
}
.installbanner{
  animation:none;
  opacity:1;
  visibility:visible;
  transition:opacity .16s cubic-bezier(.2,.7,.2,1),
             transform .16s cubic-bezier(.2,.7,.2,1),
             visibility 0s linear 0s;
}
.installbanner.hidden{
  display:grid!important;
  opacity:0;
  visibility:hidden;
  pointer-events:none;
  transform:translateX(-50%) translateY(8px);
  transition:opacity .13s cubic-bezier(.4,0,.8,.2),
             transform .13s cubic-bezier(.4,0,.8,.2),
             visibility 0s linear .13s;
}
@starting-style{
  .toast:not(.hidden){opacity:0;transform:translateX(-50%) translateY(8px)}
  .tour:not(.hidden){opacity:0;transform:translateY(8px)}
  .installbanner:not(.hidden){opacity:0;transform:translateX(-50%) translateY(8px)}
}

/* ---- Toggle: transform-only tactile stretch ---- */
.toggle::after{
  width:27px;
  transform-origin:right center;
  transition:transform .24s cubic-bezier(.32,.72,0,1);
}
.toggle:active::after{
  width:27px;
  transform:scaleX(1.12);
  transform-origin:right center;
}
.toggle[aria-checked="true"]::after,
.toggle[aria-pressed="true"]::after,
.toggle.is-on::after{
  transform:translateX(20px);
  transform-origin:left center;
}
.toggle[aria-checked="true"]:active::after,
.toggle[aria-pressed="true"]:active::after,
.toggle.is-on:active::after{
  width:27px;
  transform:translateX(20px) scaleX(1.12);
  transform-origin:left center;
}

/* ---- Short fixed beats (Plan 064 section 8, rule 11) ----
   Each consumer adds the class from its own render and is named in the
   interaction runtime audit. The Focus shelf's rise and value beats are
   consumed by app.js (L4, M1), the slot crossfade by the inline rest (L3) and
   the Progress chart (the clip wipe for C1, the same crossfade for a metric
   change and for the axes of a scope change); the rest wait for their slice. Every beat is
   transform, opacity or clip, finishes inside the budget named here, and has a
   reduced-motion entry below that leaves the end state on the first frame. */

/* The shelf changing job, and the completion actions arriving: a rise of at
   most 12px while fading in, inside 160ms. */
.motion-rise{
  animation:taurifer-rise .16s cubic-bezier(.2,.7,.2,1) both;
}
@keyframes taurifer-rise{
  from{opacity:0;transform:translateY(12px)}
  to{opacity:1;transform:none}
}

/* The selected field's value changing: the new value arrives 6px from the side
   it moved toward, inside 120ms. Where that reads as busy at logging frequency
   the crossfade class replaces it (80ms, no travel). */
.motion-value-up{
  animation:taurifer-value-up .12s cubic-bezier(.2,.7,.2,1) both;
}
.motion-value-down{
  animation:taurifer-value-down .12s cubic-bezier(.2,.7,.2,1) both;
}
.motion-value-fade{
  animation:taurifer-value-fade .08s linear both;
}
@keyframes taurifer-value-up{
  from{opacity:0;transform:translateY(6px)}
  to{opacity:1;transform:none}
}
@keyframes taurifer-value-down{
  from{opacity:0;transform:translateY(-6px)}
  to{opacity:1;transform:none}
}
@keyframes taurifer-value-fade{
  from{opacity:0}
  to{opacity:1}
}

/* The Focus cue slot and the shelf's pad row trading one job for another (L3,
   inline rest): the incoming content fades in while the outgoing copy, held
   over it as an inert layer, fades out, inside 160ms. The slot's own height run
   is `animateSlot`; this is only the crossfade. */
.motion-fade-in{
  animation:taurifer-fade-in .16s linear both;
}
.motion-fade-out{
  position:absolute;
  top:0;
  left:0;
  right:0;
  pointer-events:none;
  animation:taurifer-fade-out .16s linear both;
}
@keyframes taurifer-fade-in{
  from{opacity:0}
  to{opacity:1}
}
@keyframes taurifer-fade-out{
  from{opacity:1}
  to{opacity:0}
}

/* A chart line drawing in from the left, or only the segment that was added (C1, the Progress
   exercise chart: `.ch-trace` in `app.js`, which takes the class off when the beat ends). It is
   a clip on the trace's own stroke box and nothing else; the axes, the cursor and the table are
   never covered by it. */
.motion-clip-reveal{
  animation:taurifer-clip-reveal .36s cubic-bezier(.2,.7,.2,1) both;
}
@keyframes taurifer-clip-reveal{
  from{clip-path:inset(0 100% 0 0)}
  to{clip-path:inset(0 0 0 0)}
}

/* A stepped reveal on entry. The hidden state only exists once a script has
   marked the group ready, so without JavaScript every step is simply shown. */
.motion-steps-ready .motion-step{
  opacity:0;
  transform:translateY(8px);
  transition:opacity .2s cubic-bezier(.2,.7,.2,1),
             transform .2s cubic-bezier(.2,.7,.2,1);
}
.motion-steps-ready .motion-step.is-in{
  opacity:1;
  transform:none;
}

/* A generated list arriving in reading order, one item every 55ms. The caller
   sets --build-i on each item, counting from zero, once per generation. */
.motion-build > .motion-build-item{
  animation:taurifer-build-in .2s cubic-bezier(.2,.7,.2,1) both;
  animation-delay:calc(var(--build-i,0) * 55ms);
}
@keyframes taurifer-build-in{
  from{opacity:0;transform:translateY(8px)}
  to{opacity:1;transform:none}
}

/* An indeterminate hairline: a sweep along a 1px rule that runs only while
   .is-pending is on the host, so it is never a loop at rest. Under reduced
   motion the sweep is gone and the label states the same thing. */
.motion-hairline{
  position:relative;
  overflow:hidden;
}
.motion-hairline__label{display:none}
.motion-hairline.is-pending::after{
  content:"";
  position:absolute;
  left:0;
  right:0;
  bottom:0;
  height:1px;
  background:linear-gradient(90deg,transparent,var(--ink-faint),transparent);
  transform:translateX(-100%);
  animation:taurifer-hairline 1.1s linear infinite;
}
@keyframes taurifer-hairline{
  from{transform:translateX(-100%)}
  to{transform:translateX(100%)}
}

/* Reduced motion is a semantic alternate state, not a slower animation. */
@media (prefers-reduced-motion:reduce){
  .view,
  .effortpop.is-open,
  .effortpop.is-closing,
  .effortpop.is-open .effortpop__arrow,
  .effortpop.is-bump .effortpop__hint,
  .toast,.tour,.installbanner,.toggle::after{transition:none}
  .motion-rise,
  .motion-value-up,
  .motion-value-down,
  .motion-value-fade,
  .motion-fade-in,
  .motion-fade-out,
  .motion-clip-reveal,
  .motion-build > .motion-build-item{animation:none}
  .motion-steps-ready .motion-step{opacity:1;transform:none;transition:none}
  .motion-hairline.is-pending::after{animation:none;content:none}
  .motion-hairline.is-pending .motion-hairline__label{display:block}
  .toggle:active::after{transform:none}
  .toggle[aria-checked="true"]:active::after,
  .toggle[aria-pressed="true"]:active::after,
  .toggle.is-on:active::after{transform:translateX(20px)}
}

/* ---- Page push (N4 drill-downs, N5 Today to Focus) ----
   For the length of a push both pages are mounted: the page being entered or
   left rides above (`is-push-over`, the one that moves) and the other sits under
   it, still (`is-push-under`). Both are pinned to the screen so they overlap
   instead of stacking, and the page under is held at the scroll offset it had
   (`--push-top`). Transform only; no new elevation, so the edge is the page's own.
   The class names are written and removed by `app.js` (`pushRoute`) and never
   exist under reduced motion, where the route simply changes. */
.is-push-under,.is-push-over{
  position:fixed;left:max(0px,calc(50% - var(--maxw) / 2));right:max(0px,calc(50% - var(--maxw) / 2));
  box-sizing:border-box;overflow:hidden;animation:none;
  padding-top:calc(12px + env(safe-area-inset-top));
}
.view.is-push-under,.view.is-push-over{padding-left:max(16px,env(safe-area-inset-left));padding-right:max(16px,env(safe-area-inset-right))}
.view.is-push-under,.view.is-push-over{display:block}
.hidden.is-push-under,.hidden.is-push-over{display:block!important}
.is-push-under{top:var(--push-top,0px);z-index:1;pointer-events:none}
.is-push-over{top:0;bottom:0;z-index:2;background:var(--bg)}
/* B's one-question entry pages share the onboarding scroll surface. Grid them
   in one row so the old, inert page can sit beneath an incoming page or behind
   a page being carried out without making duplicate IDs live. */
#onbBody.entry-motion-active{
  display:grid;grid-template-columns:minmax(0,1fr);position:relative;overflow:hidden;isolation:isolate
}
#onbBody.entry-motion-active>.entry-motion-page{
  grid-area:1/1;box-sizing:border-box;width:100%;min-width:0;margin:0;overflow:hidden;animation:none
}
#onbBody.entry-motion-active>.entry-motion-page.is-push-under,
#onbBody.entry-motion-active>.entry-motion-page.is-push-over,
#onbBody.entry-motion-active>.entry-motion-page.is-edge-swiping{
  position:relative;inset:auto;padding-top:0
}
#onbBody.entry-motion-active>.entry-motion-page.is-push-under{z-index:1;pointer-events:none}
#onbBody.entry-motion-active>.entry-motion-page.is-push-over,
#onbBody.entry-motion-active>.entry-motion-page.is-edge-swiping{z-index:2;background:var(--bg)}
/* The left-edge pointer can land on this fixed scroller's padding, outside the
   page. Restrict horizontal browser panning only while standalone B is active;
   the weight ruler remains its own nested horizontal scroller. */
#onboarding.entry-edge-swipe-active{touch-action:pan-y pinch-zoom}
/* Focus sits in a main with no side padding and the same top inset, so it rides in the same box it will rest in. */
#workoutShell.is-push-over{top:calc(12px + env(safe-area-inset-top));bottom:env(safe-area-inset-bottom);padding:0}
/* The pages a push can leave by an edge swipe take the horizontal drag themselves
   (RepForgeMotion.registerEdgeSwipeBack); the browser keeps the vertical pan. */
#exercise,#library,#exercisePreview,#importReview,#history.is-session-page{touch-action:pan-y pinch-zoom}
/* Focus lays itself out from the body classes `is-workout is-focus-wo`, which are
   gone by the time Back carries it off. While it rides out it keeps its own layout. */
#workoutShell.is-push-over,
#workoutShell.is-push-over #logForm,
#workoutShell.is-push-over #workout{display:flex;flex-direction:column;flex:1;min-height:0}
#workoutShell.is-push-over > :not(#logForm),
#workoutShell.is-push-over #logForm > :not(#workout),
#workoutShell.is-push-over #workout > :not(.deck){
  margin-left:max(var(--gut),env(safe-area-inset-left));margin-right:max(var(--gut),env(safe-area-inset-right));
}
#workoutShell.is-push-over .deck{flex:1;min-height:0}
#workoutShell.is-push-over #dayTabs,
#workoutShell.is-push-over #logForm > .field,
#workoutShell.is-push-over #logForm > .btn--save{display:none!important}
#workoutShell.hidden.is-push-over{display:flex!important}
