/* motion.css — every duration in the app, and the switch that turns them off.

   Split out of design.css so there is exactly one file to open when something
   moves and should not. Nothing else sets a transition or a keyframe.

   Two rules the whole file obeys. Everything is --fast or --mid, so nothing
   ever runs past 180ms: this is a tool somebody walks through in three
   minutes, and past about a fifth of a second a transition stops reading as
   polish and starts reading as the app thinking. And nothing moves the frame.
   The top bar, the bottom bar and the edges of the stage are the one fixed
   thing on screen, so only the content inside them animates. A frame that
   slides makes the app feel like a slideshow and makes it much harder to see
   what actually changed.

   The last block turns all of it into an instant change for anyone who has
   asked their device for less movement. That is not a downgrade. For a person
   who gets motion sick from it, an instant swap is the correct behaviour, and
   every state the app can be in is still readable without a single frame of
   animation. */

/* ---- one step to the next --------------------------------------------------- */

/* A step is swapped by putting the hidden attribute on one and taking it off
   another, so the outgoing one cannot be faded: it is already gone. What can
   be done, and what reads the same, is the incoming one arriving. An element
   that goes from display none to shown starts its animations again from the
   top, which is what makes this work with no javascript at all.

   The 8px is what tells back from next. Going forward the new step comes in
   from the right, going back it comes from the left, so the two directions
   feel like directions rather than like the same flicker twice. The stage
   carries data-dir and everything else follows from it. */
@keyframes step-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes step-in-fwd { from { opacity: 0; transform: translateX(8px); } to { opacity: 1; transform: none; } }
@keyframes step-in-back { from { opacity: 0; transform: translateX(-8px); } to { opacity: 1; transform: none; } }

.step { animation: step-in var(--mid) var(--ease) both; }
.stage[data-dir='fwd'] .step { animation-name: step-in-fwd; }
.stage[data-dir='back'] .step { animation-name: step-in-back; }

/* The bar under the current stop is one element for all three, placed from
   --u-left and --u-width measured off the current word, so moving it is a
   transition and not a swap. It travels from the word you left to the word
   you are on, which is the only part of the top bar allowed to move. */
.stops::after { transition: left var(--mid) var(--ease), width var(--mid) var(--ease); }
.stops li { transition: color var(--fast) var(--ease); }
/* The count hung under a number is faded rather than switched, because it
   appears and disappears under the pointer and a hard swap there reads as a
   flicker. */
.wordmark, #settings { transition: color var(--fast) var(--ease); }

/* ---- files arriving and leaving --------------------------------------------- */

/* Six files dropped at once used to appear as one block, which reads as a
   redraw rather than as six things arriving. 40ms apart is enough to see them
   land one after another and short enough that the last one is on screen
   inside a quarter of a second. The index comes in on --i from screens.js. */
@keyframes card-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.stagger { animation: card-in var(--mid) var(--ease) both; animation-delay: calc(var(--i, 0) * 40ms); }

/* A card that vanishes leaves the eye hunting for what changed. Fading it up
   and out first means the list closing up underneath is followed rather than
   discovered. The class goes on, the animation runs, then the element is
   removed. */
@keyframes card-out { to { opacity: 0; transform: translateY(-4px); } }
.leaving { animation: card-out var(--fast) var(--ease) both; pointer-events: none; }

/* ---- the drop zone ----------------------------------------------------------- */

/* A colour change alone is easy to miss on a dashed hairline, especially while
   a file is under the cursor and the cursor is what you are watching. A couple
   of pixels of growth is felt rather than read. */
.drop { transition: border-color var(--fast) var(--ease), transform var(--fast) var(--ease); }
.drop:hover { transform: scale(1.002); }
.drop.over { transform: scale(1.006); }

/* ---- controls ---------------------------------------------------------------- */

.btn, .chip, .cell, .usage-option, input, .link { transition: border-color var(--fast) var(--ease), color var(--fast) var(--ease), background-color var(--fast) var(--ease), text-decoration-color var(--fast) var(--ease); }

/* Nothing in the app moved when it was clicked, which is the difference
   between a button and a picture of a button. One pixel down is the whole
   effect and it is enough. */
.btn:active:not(:disabled), .chip:active, .usage-option:active,
.link:active, .stops button:active, #settings:active { transform: translateY(1px); }
.cell:active:not(:disabled) { transform: scale(0.97); }

/* A matrix cell has to render a real logo on a real background before it can
   show it is selected, and on a slow machine that is long enough to tap twice.
   The ring grows from the middle of the cell the moment the tap lands, so the
   tap is answered before the artwork is. It is drawn on the cell's own edge,
   exactly over the selected border, so the two land as one line. */
.cell::after {
  content: '';
  position: absolute;
  inset: 0;
  border: 1px solid var(--ink);
  border-radius: var(--r);
  opacity: 0;
  transform: scale(0.7);
  transition: opacity var(--fast) var(--ease), transform var(--fast) var(--ease);
  pointer-events: none;
}
.cell[aria-pressed='true']::after { opacity: 1; transform: none; }

/* ---- building and finishing --------------------------------------------------- */

/* The bar used to jump from one estimate to the next. Easing it makes a build
   look like it is working rather than stalling and then lurching. Going
   backwards is the one thing easing cannot fix, because that is a number
   problem and not a drawing one, so the caller has to hand this a figure that
   only ever climbs. */
.bar span { transition: width var(--mid) var(--ease); }

/* The folder name is the first line of the tree and the thing worth reading
   first, so the reveal starts at the top and runs down rather than fading the
   whole block in at once. */
@keyframes tree-wipe { from { clip-path: inset(0 0 100% 0); } to { clip-path: inset(0 0 0 0); } }
#doneWrap .tree-card { animation: tree-wipe var(--mid) var(--ease) both; }

/* ---- panels ------------------------------------------------------------------- */

@keyframes panel-in { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
@keyframes sheet-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes toast-in { from { opacity: 0; transform: translate(-50%, 8px); } to { opacity: 1; transform: translate(-50%, 0); } }
#settingsPanel { animation: panel-in var(--fast) var(--ease) both; }
#shortcuts { animation: sheet-in var(--fast) var(--ease) both; }
#toast { animation: toast-in var(--mid) var(--ease) both; }

/* ---- the top bar hairline ------------------------------------------------------ */

/* The line under the top bar is drawn only once the stage has been scrolled,
   so the bar reads as a fixed frame with content passing under it rather than
   as a line that was always there. It is driven by the scroll position itself
   rather than by a class, which means no scroll listener and no work on the
   main thread while a build is running.

   The stage is not an ancestor of the top bar, so the timeline has to be named
   on the stage and brought into scope on the app around both. Wrapped in
   @supports because a browser without scroll driven animation would otherwise
   run this once on load and leave the line permanently on, which is the wrong
   way round. Without support there is simply no hairline. */
@keyframes topbar-hairline { from { border-bottom-color: transparent; } to { border-bottom-color: var(--line); } }

@supports (timeline-scope: --stage-scroll) {
  .app { timeline-scope: --stage-scroll; }
  .stage { scroll-timeline: --stage-scroll block; }
  .topbar {
    animation: topbar-hairline linear both;
    animation-timeline: --stage-scroll;
    animation-range: 0 16px;
  }
}

/* ---- less movement, on request -------------------------------------------------- */

/* Everything above becomes an instant change. The durations are cut to 1ms
   rather than set to none, because a zero length animation still fires its end
   event and still applies its final state, and several of the rules above rely
   on that final state being applied.

   The top bar is left out on purpose. Its hairline is a readout of the scroll
   position rather than movement, it is drawn by the scroll and not by time,
   and switching it off would remove information rather than remove motion.

   scroll-behavior is in here too. A smooth scroll is motion whether it came
   from a stylesheet or from a script, and the step change scrolls the stage
   back to the top every time. */
@media (prefers-reduced-motion: reduce) {
  *:not(.topbar), *::before, *::after {
    animation-duration: 1ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    transition-delay: 0ms !important;
  }
  html, body, .stage, .matrix-wrap, .tree-card { scroll-behavior: auto !important; }
  .drop:hover, .drop.over, .cell:active:not(:disabled) { transform: none !important; }
}
