/* ========================================================
   MOTION & MICRO-INTERACTIONS  (v2.3.0)
   ========================================================
   Every animation in the app lives here or is built from the tokens here.
   That is not tidiness. Motion in a UI is judged COMPARATIVELY: two things
   moving at once at 200ms and 300ms read as broken even though each on its
   own is fine, and that mismatch is exactly what you get when each component
   picks its own duration at the point of use. The durations and curves are
   in base/variables.css; this file is the only place they are spent.

   FOUR THINGS ARE ANIMATED, AND NOTHING ELSE IS:

     1  PRESS      something the operator tapped acknowledges the tap
     2  HOVER      something the operator is pointing at lifts slightly
     3  ARRIVE     something new (a page, a panel, a toast, a row) settles in
     4  CELEBRATE  a target met or a streak milestone, once, briefly

   WHAT THIS FILE IS ALLOWED TO ANIMATE
   `transform` and `opacity`, and nothing else. Both are composited on the
   GPU and cost no layout or paint. Animating `width`, `height`, `top`,
   `margin` or `box-shadow` forces a layout or paint pass per frame, and this
   app is read on mid-range Android tablets in a mill yard — the same devices
   the backdrop-filter blurs were removed for. The two exceptions are
   deliberate and bounded: the progress bars animate `width` (once, on load,
   and there is no transform that fills a bar from the left) and buttons
   transition `box-shadow` (paint only, on a handful of elements, and the
   hover lift is meaningless without it).

   CSP: THERE ARE NO INLINE STYLES OR HANDLERS ANYWHERE IN THIS FEATURE.
   The policy carries a per-request nonce and no 'unsafe-inline' for either
   script-src or style-src, so an `onclick=` or a `style=` is not a style
   choice here, it is a broken feature with nothing in the server log. Every
   trigger below is a class added from main.core.js, and the one place a
   value genuinely has to vary per element (the angle of a celebration spark)
   is set through `element.style.setProperty` — the CSSOM, which CSP does not
   govern, and which is the same mechanism DataTables and Chart.js already
   rely on.
   ======================================================== */


/* ========================================================
   1 — PRESS  ("tap-to-like")
   ========================================================
   The bounce. An element scales DOWN under the finger and overshoots
   slightly on the way back up, which is the whole trick: the compression
   says "received" and the overshoot says "done". A press animation without
   the overshoot reads as a lag rather than as a response.

   Two mechanisms, and they do different jobs:

     :active            free, instant, works with scripts off, but it only
                        lasts as long as the finger is down. Every tappable
                        thing gets this.
     .is-tapped         added by main.core.js on pointerdown and removed when
                        the animation ends. This is the one that survives the
                        finger lifting, so the bounce completes even on a
                        30ms tap. Only on things worth celebrating a tap on:
                        submits, approvals, mark-read, quick actions.

   WHY SCALE AND NOT TRANSLATE
   A button that moves under a gloved thumb on a shop-floor tablet is a
   mis-tap. Scaling about the centre keeps the tap target where the finger
   already is. The same reasoning removed the old translateY(-2px) hover. */

.btn, .btn-primary, .btn-secondary, .btn-submit, .btn-danger, .btn-action,
.btn-edit, .btn-import, .btn-export, .pill-btn, .icon-action,
.tab-btn, .quick-action, .notif-item, .kpi-card, .inv-item {
    /* transform is listed here rather than in `transition: all` so a width
       or colour change elsewhere on the element cannot accidentally inherit
       the bounce curve. */
    transition: transform var(--dur-fast) var(--ease-out),
                box-shadow var(--dur-fast) var(--ease-out),
                background-color var(--dur-fast) var(--ease-out),
                border-color var(--dur-fast) var(--ease-out),
                color var(--dur-fast) var(--ease-out);
}

.btn:active, .btn-primary:active, .btn-secondary:active, .btn-submit:active,
.btn-danger:active, .btn-action:active, .btn-edit:active, .btn-import:active,
.btn-export:active, .pill-btn:active, .icon-action:active, .tab-btn:active,
.quick-action:active {
    transform: scale(0.96);
    transition-duration: var(--dur-instant);
}

/* Cards are bigger, so the same 4% compression reads as a much larger
   movement. 1.5% is the equivalent felt amount on a 300px tile. */
.kpi-card:active, .inv-item:active, .notif-item:active {
    transform: scale(0.985);
    transition-duration: var(--dur-instant);
}

@keyframes tap-bounce {
    0%   { transform: scale(1); }
    35%  { transform: scale(0.94); }
    100% { transform: scale(1); }
}

.is-tapped {
    animation: tap-bounce var(--dur-base) var(--ease-bounce);
}

/* The larger surfaces get their own, gentler keyframe for the reason above. */
.kpi-card.is-tapped, .inv-item.is-tapped, .notif-item.is-tapped {
    animation-name: tap-bounce-soft;
}
@keyframes tap-bounce-soft {
    0%   { transform: scale(1); }
    35%  { transform: scale(0.978); }
    100% { transform: scale(1); }
}


/* ========================================================
   2 — HOVER
   ========================================================
   Guarded by `(hover: hover) and (pointer: fine)` throughout. On a
   touchscreen a :hover rule sticks after a tap until something else is
   tapped, so an unguarded hover lift leaves a button visibly raised for no
   reason — which is how a "sticky pressed button" bug gets reported.

   The lift is 1px and the shadow does the rest of the work. --elev-hover is
   wider and softer than --elev-2 rather than darker, so the element reads as
   rising off the page instead of as the shadow being turned up. */
@media (hover: hover) and (pointer: fine) {
    .btn:hover, .btn-primary:hover, .btn-submit:hover, .btn-danger:hover,
    .btn-secondary:hover, .btn-edit:hover, .btn-import:hover, .btn-export:hover,
    .pill-btn:hover {
        transform: translateY(-1px);
    }
    .btn:active, .btn-primary:active, .btn-submit:active, .btn-danger:active,
    .btn-secondary:active, .btn-edit:active, .btn-import:active,
    .btn-export:active, .pill-btn:active {
        /* Cancel the lift while pressed, or the button appears to hover and
           compress at the same time and does neither convincingly. */
        transform: translateY(0) scale(0.96);
    }
}


/* ========================================================
   3 — ARRIVE
   ========================================================
   Page loads, dashboard panel swaps, table rows, toasts, drawers.

   All of these are the same movement at different sizes: come up a few
   pixels while fading in, on --ease-out, which decelerates. Nothing uses
   --ease-bounce; an overshooting panel reads as a rendering fault rather
   than as personality. */

@keyframes arrive {
    from { opacity: 0; transform: translateY(8px); }
    to   { opacity: 1; transform: translateY(0); }
}
@keyframes arrive-fade {
    from { opacity: 0; }
    to   { opacity: 1; }
}

/* --- The page itself -------------------------------------------------
   Applied to <main> by base.html. This is a plain CSS animation on load
   rather than a View Transition: the API is not in the WebViews this app is
   installed into as a PWA, and the fallback for an unsupported View
   Transition is no transition at all, which is the case we are trying to
   improve. An 8px rise on --dur-base is under a fifth of a second and is
   felt rather than watched.

   IT MUST NOT DELAY FIRST PAINT. The animation starts at opacity 0, so if
   the stylesheet were to load late the page would be briefly invisible —
   which is why this rule lives in the bundle that is already render
   blocking, and why the duration is short enough that a slow frame is
   indistinguishable from a fast one. */
.page-enter {
    animation: arrive var(--dur-base) var(--ease-out) both;
}

/* --- Dashboard panel swaps -------------------------------------------
   main.dashboard.js replaces the panel's innerHTML when a tab is clicked.
   Without this the new content appears instantly in place, which at a
   glance is indistinguishable from nothing having happened — the commonest
   reason someone clicks a tab twice. */
.panel-enter {
    animation: arrive var(--dur-base) var(--ease-out) both;
}
/* The outgoing half. main.dashboard.js adds this, waits --dur-fast, then
   swaps. Fade only, no movement: the panel is about to be replaced by
   something of a different height and moving it first makes the page jump. */
.panel-leave {
    opacity: 0;
    transition: opacity var(--dur-fast) var(--ease-in-out);
}

/* --- Cards and rows, staggered --------------------------------------
   A stagger is the difference between "the list appeared" and "the list is
   arriving". --stagger-i is set from main.core.js as an index; the delay is
   capped at 8 steps so a 200-row table does not take four seconds to finish
   arriving. Rows past the cap simply arrive with the eighth. */
.arrive-item {
    animation: arrive var(--dur-base) var(--ease-out) both;
    animation-delay: calc(var(--stagger-i, 0) * 40ms);
}


/* ========================================================
   4 — CELEBRATE
   ========================================================
   Two moments, and they are deliberately the only two: a dashboard target
   card reaching 100%, and a daily-entry streak hitting a milestone. If a
   third is ever added, the bar it has to clear is "would an operator be
   pleased to see this at 6am", not "would this look good in a demo".

   The burst is built by main.core.js as a <span class="celebrate-burst">
   holding N <i class="celebrate-spark"> children, each given --spark-angle
   and --spark-dist through the CSSOM. It is pointer-events: none and
   aria-hidden, so it can never intercept a tap or reach a screen reader —
   the information is in the number, not in the confetti. */

.celebrate-burst {
    position: absolute;
    top: 50%;
    left: 50%;
    width: 0;
    height: 0;
    pointer-events: none;
    z-index: var(--z-celebrate);
}
.celebrate-spark {
    position: absolute;
    width: 7px;
    height: 7px;
    margin: -3.5px 0 0 -3.5px;
    border-radius: var(--radius-full);
    background: var(--spark-colour, var(--brand));
    opacity: 0;
    animation: spark-fly var(--dur-celebrate) var(--ease-out) both;
    animation-delay: var(--spark-delay, 0ms);
}
/* rotate() then translate() then rotate() back: the first rotation aims the
   spark, the translate throws it that far, and the third keeps the square
   from spinning as it goes. Doing it as a single translate(x, y) would mean
   computing the sine and cosine in JavaScript for every spark on every
   celebration; this way the only per-spark values are an angle and a
   distance, which is what a burst actually is. */
@keyframes spark-fly {
    0%   { opacity: 0; transform: rotate(var(--spark-angle, 0deg)) translateX(0) scale(0.4); }
    15%  { opacity: 1; }
    70%  { opacity: 1; }
    100% { opacity: 0;
           transform: rotate(var(--spark-angle, 0deg))
                      translateX(var(--spark-dist, 60px))
                      scale(0.9); }
}

/* The element being congratulated swells once. Applied alongside the burst
   and removed on animationend. */
@keyframes celebrate-pop {
    0%   { transform: scale(1); }
    30%  { transform: scale(1.045); }
    60%  { transform: scale(0.995); }
    100% { transform: scale(1); }
}
.is-celebrating {
    animation: celebrate-pop var(--dur-celebrate) var(--ease-bounce);
    position: relative;   /* the burst is absolutely positioned inside it */
}

/* A target bar that has reached 100% keeps a slow shimmer. This is the one
   piece of motion in the app that repeats, and it is deliberate: the burst
   is gone within a second, and the shimmer is what tells someone arriving at
   the screen an hour later that the target was met. `background-position` on
   a gradient is a paint, not a layout, and it is at most six elements. */
@keyframes target-shimmer {
    0%   { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}
.progress-bar-fill.is-met {
    background-image: linear-gradient(100deg,
        transparent 30%,
        rgba(255, 255, 255, 0.45) 50%,
        transparent 70%);
    background-size: 200% 100%;
    animation: target-shimmer 3.2s linear infinite;
}

/* The streak flame, when the count goes up. */
@keyframes streak-flare {
    0%   { transform: scale(1) rotate(0deg); }
    25%  { transform: scale(1.25) rotate(-7deg); }
    55%  { transform: scale(1.12) rotate(6deg); }
    100% { transform: scale(1) rotate(0deg); }
}
.streak-flame.is-flaring {
    animation: streak-flare var(--dur-celebrate) var(--ease-bounce);
    display: inline-block;
}


/* ========================================================
   REDUCED MOTION
   ========================================================
   Not a nicety. Vestibular disorders are common, the operating-system
   setting is how someone tells us they have one, and this is a screen
   somebody looks at for an entire shift.

   Everything is turned OFF rather than shortened. A 1ms animation still
   fires animationend, so every JS handler that cleans up a class still runs
   and no element is left stuck mid-state — which is why the durations go to
   0.01ms rather than the rules being removed.

   The celebration sparks are hidden outright: a burst is decorative by
   definition, and a static ring of dots appearing and vanishing is worse
   than nothing. The swell, the shimmer and the flare all stop too. What
   survives is the colour change and the number, which is the actual
   information. */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
    .celebrate-burst { display: none !important; }
    .progress-bar-fill.is-met { background-image: none !important; }
    .is-celebrating, .is-tapped, .streak-flame.is-flaring { animation: none !important; }
    /* Hover and press feedback still has to EXIST — someone who cannot
       tolerate movement still needs to know the button registered the tap.
       It becomes a colour and shadow change with no transform. */
    .btn:active, .btn-primary:active, .btn-submit:active,
    .kpi-card:active, .inv-item:active { transform: none !important; }
}
