/* ============================================================
   eClinic — Motion system
   ------------------------------------------------------------
   Loaded AFTER tailwindv4/output.css and main.css, unlayered,
   so these rules win over Tailwind's @layer components/utilities.

   Naming:
     anim-*      one-shot entrance / attention animations
     stagger-*   cascade the children of a container
     hover-*     pointer affordances
     is-*        JS-driven state flags (modal, drawer, progress)

   Everything degrades to "no motion" under
   prefers-reduced-motion (see the block at the bottom).
   ============================================================ */

/* Always reserve the scrollbar gutter. Flowbite parks `overflow: hidden` on
   <body> whenever a modal or drawer opens; without a reserved gutter the
   scrollbar vanishes, every page shifts sideways by its width, and shifts
   back on close — the "nudge". */
/* Opening a modal/drawer hides the body overflow, which takes the viewport
   scrollbar with it and shifts the whole page sideways — then back again on
   close. scroll-lock.js measures the scrollbar before it disappears and puts
   the width back as padding, so nothing moves.

   Body covers everything in flow; the navbar and the progress rail are fixed
   and pinned to right:0, so they need the same padding of their own. */
html.scroll-locked body,
html.scroll-locked .app-navbar,
html.scroll-locked #hx-progress {
    padding-right: var(--scrollbar-comp, 0px);
}

:root {
    /* Durations */
    --mo-fast: 180ms;
    --mo-base: 380ms;
    --mo-slow: 620ms;

    /* Easings — "spring" overshoots slightly, that's on purpose */
    --mo-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
    --mo-out: cubic-bezier(0.22, 1, 0.36, 1);
    --mo-in: cubic-bezier(0.55, 0, 1, 0.45);
    --mo-inout: cubic-bezier(0.65, 0, 0.35, 1);

    /* Default step between staggered children */
    --stagger-step: 60ms;
    --stagger-start: 0ms;
}

/* ============================================================
   1. Keyframe library
   ============================================================ */

@keyframes anim-rise {
    from {
        opacity: 0;
        transform: translate3d(0, 28px, 0);
    }

    to {
        opacity: 1;
        transform: translate3d(0, 0, 0);
    }
}

@keyframes anim-drop {
    from {
        opacity: 0;
        transform: translate3d(0, -26px, 0);
    }

    to {
        opacity: 1;
        transform: translate3d(0, 0, 0);
    }
}

@keyframes anim-slide-left {
    from {
        opacity: 0;
        transform: translate3d(-34px, 0, 0);
    }

    to {
        opacity: 1;
        transform: translate3d(0, 0, 0);
    }
}

@keyframes anim-slide-right {
    from {
        opacity: 0;
        transform: translate3d(34px, 0, 0);
    }

    to {
        opacity: 1;
        transform: translate3d(0, 0, 0);
    }
}

@keyframes anim-pop {
    0% {
        opacity: 0;
        transform: scale(0.82) translate3d(0, 12px, 0);
    }

    60% {
        opacity: 1;
    }

    100% {
        opacity: 1;
        transform: scale(1) translate3d(0, 0, 0);
    }
}

@keyframes anim-zoom {
    from {
        opacity: 0;
        transform: scale(1.06);
    }

    to {
        opacity: 1;
        transform: scale(1);
    }
}

@keyframes anim-blur {
    from {
        opacity: 0;
        filter: blur(10px);
        transform: translate3d(0, 16px, 0);
    }

    to {
        opacity: 1;
        filter: blur(0);
        transform: translate3d(0, 0, 0);
    }
}

@keyframes anim-fade {
    from {
        opacity: 0;
    }

    to {
        opacity: 1;
    }
}

@keyframes anim-unfold {
    from {
        opacity: 0;
        transform: perspective(900px) rotateX(-14deg) translate3d(0, 22px, 0);
        transform-origin: top center;
    }

    to {
        opacity: 1;
        transform: perspective(900px) rotateX(0) translate3d(0, 0, 0);
    }
}

@keyframes anim-sweep-in {
    from {
        opacity: 0;
        clip-path: inset(0 100% 0 0);
        transform: translate3d(-14px, 0, 0);
    }

    to {
        opacity: 1;
        clip-path: inset(0 0 0 0);
        transform: translate3d(0, 0, 0);
    }
}

/* Attention / ambient loops */

@keyframes anim-float {

    0%,
    100% {
        transform: translate3d(0, 0, 0);
    }

    50% {
        transform: translate3d(0, -10px, 0);
    }
}

@keyframes anim-drift {

    0%,
    100% {
        transform: translate3d(0, 0, 0) scale(1);
    }

    33% {
        transform: translate3d(3%, -4%, 0) scale(1.06);
    }

    66% {
        transform: translate3d(-3%, 3%, 0) scale(0.96);
    }
}

@keyframes anim-ping-ring {
    0% {
        opacity: 0.75;
        transform: scale(1);
    }

    80%,
    100% {
        opacity: 0;
        transform: scale(2.6);
    }
}

@keyframes anim-shimmer {
    from {
        background-position: -220% 0;
    }

    to {
        background-position: 220% 0;
    }
}

@keyframes anim-wiggle {

    0%,
    100% {
        transform: rotate(0);
    }

    25% {
        transform: rotate(-12deg);
    }

    75% {
        transform: rotate(12deg);
    }
}

@keyframes anim-spin {
    to {
        transform: rotate(360deg);
    }
}

@keyframes anim-heartbeat {

    0%,
    100% {
        transform: scale(1);
    }

    14% {
        transform: scale(1.22);
    }

    28% {
        transform: scale(1);
    }

    42% {
        transform: scale(1.16);
    }

    56% {
        transform: scale(1);
    }
}

/* Scale-only entrance. Because it never translates, it adds nothing to a
   scroll container's scrollable area — safe inside overflow:auto boxes. */
@keyframes anim-settle {
    from {
        opacity: 0;
        transform: scale(0.9);
    }

    to {
        opacity: 1;
        transform: scale(1);
    }
}

@keyframes anim-bounce-in-dot {
    0% {
        transform: scale(0);
    }

    70% {
        transform: scale(1.35);
    }

    100% {
        transform: scale(1);
    }
}

/* ============================================================
   2. Entrance utilities
   ============================================================ */

/* `backwards`, never `both`: a finished `both` animation keeps its last
   keyframe's transform applied forever, which turns the element into a
   containing block for any position:fixed descendant. `backwards` holds
   the *first* keyframe during the delay and then hands control back to
   the element's own styles — same look, no lingering transform. */
.anim-rise,
.anim-drop,
.anim-slide-left,
.anim-slide-right,
.anim-pop,
.anim-zoom,
.anim-blur,
.anim-fade,
.anim-unfold,
.anim-sweep {
    animation-duration: var(--mo-base);
    animation-timing-function: var(--mo-out);
    animation-fill-mode: backwards;
}

.anim-rise {
    animation-name: anim-rise;
}

.anim-drop {
    animation-name: anim-drop;
}

.anim-slide-left {
    animation-name: anim-slide-left;
}

.anim-slide-right {
    animation-name: anim-slide-right;
}

.anim-pop {
    animation-name: anim-pop;
    animation-timing-function: var(--mo-spring);
    animation-duration: 520ms;
}

.anim-zoom {
    animation-name: anim-zoom;
}

.anim-blur {
    animation-name: anim-blur;
    animation-duration: 560ms;
}

.anim-fade {
    animation-name: anim-fade;
}

.anim-unfold {
    animation-name: anim-unfold;
    animation-duration: 560ms;
}

.anim-sweep {
    animation-name: anim-sweep-in;
    animation-duration: 520ms;
}

/* Duration modifiers */
.anim-slow {
    animation-duration: var(--mo-slow) !important;
}

.anim-fastest {
    animation-duration: var(--mo-fast) !important;
}

/* Ambient loops */
.anim-float {
    animation: anim-float 5s ease-in-out infinite;
}

.anim-drift {
    animation: anim-drift 22s ease-in-out infinite;
}

.anim-heartbeat {
    animation: anim-heartbeat 2.4s ease-in-out infinite;
}

.anim-spin-slow {
    animation: anim-spin 2.4s linear infinite;
}

/* Kept for backwards compatibility with the old class names, but
   upgraded so existing markup gets the louder motion for free. */
.rise-in {
    animation: anim-rise var(--mo-base) var(--mo-out) backwards;
}

.fade-in {
    animation: anim-fade var(--mo-base) var(--mo-out) backwards;
}

/* A live "pulse" that reads at a glance — the old one only dipped opacity */
.soft-pulse {
    position: relative;
    animation: anim-heartbeat 2.4s ease-in-out infinite;
}

.soft-pulse::after {
    content: "";
    position: absolute;
    inset: -2px;
    border-radius: 9999px;
    background: currentColor;
    animation: anim-ping-ring 2.4s cubic-bezier(0, 0, 0.2, 1) infinite;
    pointer-events: none;
}

/* ============================================================
   3. Stagger cascade
   Put `stagger` on the PARENT — its direct children animate in
   one after another. The variant classes pick which keyframes
   the children use; the parent itself never animates.

   Custom properties (not `animation-name: inherit`) carry the
   choice down, so the parent stays completely still.
   ============================================================ */

.stagger>* {
    animation-name: var(--stagger-anim, anim-rise);
    animation-duration: var(--stagger-duration, 520ms);
    animation-timing-function: var(--stagger-ease, cubic-bezier(0.22, 1, 0.36, 1));
    animation-fill-mode: backwards;
}

.stagger-pop {
    --stagger-anim: anim-pop;
    --stagger-ease: cubic-bezier(0.34, 1.56, 0.64, 1);
    --stagger-duration: 560ms;
}

.stagger-left {
    --stagger-anim: anim-slide-left;
}

.stagger-right {
    --stagger-anim: anim-slide-right;
}

.stagger-drop {
    --stagger-anim: anim-drop;
}

.stagger-zoom {
    --stagger-anim: anim-zoom;
}

.stagger-blur {
    --stagger-anim: anim-blur;
    --stagger-duration: 620ms;
}

.stagger-sweep {
    --stagger-anim: anim-sweep-in;
}

/* Overflow-safe variant: scales up from slightly smaller and never
   translates, so it can be used on children of a scrolling container
   (the Today queue columns) without pushing a scrollbar into view. */
.stagger-settle {
    --stagger-anim: anim-settle;
    --stagger-ease: cubic-bezier(0.34, 1.56, 0.64, 1);
    --stagger-duration: 520ms;
}

.stagger-tight {
    --stagger-step: 38ms;
}

.stagger-loose {
    --stagger-step: 95ms;
}

.stagger-delayed {
    --stagger-start: 140ms;
}

.stagger>*:nth-child(1) {
    animation-delay: calc(var(--stagger-start) + 0 * var(--stagger-step));
}

.stagger>*:nth-child(2) {
    animation-delay: calc(var(--stagger-start) + 1 * var(--stagger-step));
}

.stagger>*:nth-child(3) {
    animation-delay: calc(var(--stagger-start) + 2 * var(--stagger-step));
}

.stagger>*:nth-child(4) {
    animation-delay: calc(var(--stagger-start) + 3 * var(--stagger-step));
}

.stagger>*:nth-child(5) {
    animation-delay: calc(var(--stagger-start) + 4 * var(--stagger-step));
}

.stagger>*:nth-child(6) {
    animation-delay: calc(var(--stagger-start) + 5 * var(--stagger-step));
}

.stagger>*:nth-child(7) {
    animation-delay: calc(var(--stagger-start) + 6 * var(--stagger-step));
}

.stagger>*:nth-child(8) {
    animation-delay: calc(var(--stagger-start) + 7 * var(--stagger-step));
}

.stagger>*:nth-child(9) {
    animation-delay: calc(var(--stagger-start) + 8 * var(--stagger-step));
}

.stagger>*:nth-child(10) {
    animation-delay: calc(var(--stagger-start) + 9 * var(--stagger-step));
}

.stagger>*:nth-child(11) {
    animation-delay: calc(var(--stagger-start) + 10 * var(--stagger-step));
}

.stagger>*:nth-child(12) {
    animation-delay: calc(var(--stagger-start) + 11 * var(--stagger-step));
}

.stagger>*:nth-child(13) {
    animation-delay: calc(var(--stagger-start) + 12 * var(--stagger-step));
}

.stagger>*:nth-child(14) {
    animation-delay: calc(var(--stagger-start) + 13 * var(--stagger-step));
}

.stagger>*:nth-child(15) {
    animation-delay: calc(var(--stagger-start) + 14 * var(--stagger-step));
}

.stagger>*:nth-child(16) {
    animation-delay: calc(var(--stagger-start) + 15 * var(--stagger-step));
}

.stagger>*:nth-child(17) {
    animation-delay: calc(var(--stagger-start) + 16 * var(--stagger-step));
}

.stagger>*:nth-child(18) {
    animation-delay: calc(var(--stagger-start) + 17 * var(--stagger-step));
}

.stagger>*:nth-child(19) {
    animation-delay: calc(var(--stagger-start) + 18 * var(--stagger-step));
}

.stagger>*:nth-child(20) {
    animation-delay: calc(var(--stagger-start) + 19 * var(--stagger-step));
}

/* Past 20 rows the cascade would feel like lag — hold them together */
.stagger>*:nth-child(n + 21) {
    animation-delay: calc(var(--stagger-start) + 20 * var(--stagger-step));
}

/* ============================================================
   4. Pointer affordances
   ============================================================ */

.hover-lift {
    transition: transform 260ms var(--mo-out), box-shadow 260ms var(--mo-out);
}

.hover-lift:hover {
    transform: translate3d(0, -5px, 0);
    box-shadow: 0 12px 28px -8px rgb(28 25 23 / 0.22);
}

.dark .hover-lift:hover {
    box-shadow: 0 12px 28px -8px rgb(0 0 0 / 0.55);
}

.hover-grow {
    transition: transform 260ms var(--mo-spring);
}

.hover-grow:hover {
    transform: scale(1.035);
}

/* Icon inside a hovered row/card reacts on its own */
.hover-icon svg {
    transition: transform 320ms var(--mo-spring);
}

.hover-icon:hover svg {
    transform: scale(1.22) rotate(-8deg);
}

/* Every button gets a tactile press + a light sweep on hover */
.btn {
    position: relative;
    overflow: hidden;
    /* `overflow: hidden` (needed to clip the sweep and the ripple) sets a
       flex item's automatic minimum size to 0 instead of its content size,
       so buttons in a flex row silently squeeze down and clip their own
       label — that's what was truncating "Clear all". Put the content-based
       floor back. */
    min-width: fit-content;
    transition: background-color 160ms ease, color 160ms ease,
        border-color 160ms ease, box-shadow 240ms var(--mo-out),
        transform 200ms var(--mo-spring);
}

.btn:hover:not(:disabled) {
    transform: translate3d(0, -2px, 0);
}

.btn:active:not(:disabled) {
    transform: translate3d(0, 0, 0) scale(0.955);
    transition-duration: 90ms;
}

.btn::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(110deg,
            transparent 20%,
            rgb(255 255 255 / 0.28) 45%,
            transparent 70%);
    transform: translateX(-130%);
    transition: transform 620ms var(--mo-out);
    pointer-events: none;
}

.btn:hover:not(:disabled)::after {
    transform: translateX(130%);
}

/* Ripple injected by animations.js on pointerdown */
.ripple {
    position: absolute;
    border-radius: 9999px;
    transform: scale(0);
    background: rgb(255 255 255 / 0.45);
    animation: ripple-out 620ms var(--mo-out) forwards;
    pointer-events: none;
}

.btn-secondary .ripple,
.btn-ghost .ripple,
.btn-primary-soft .ripple,
.btn-danger-soft .ripple,
.btn-success-soft .ripple {
    background: currentColor;
    opacity: 0.22;
}

@keyframes ripple-out {
    to {
        transform: scale(2.6);
        opacity: 0;
    }
}

/* Inputs bloom on focus */
.input-base {
    transition: border-color 200ms ease, box-shadow 260ms var(--mo-out),
        background-color 200ms ease, transform 260ms var(--mo-out);
}

.input-base:focus {
    transform: translate3d(0, -2px, 0);
}

/* Badges land with a pop the first time they render */
.badge {
    transition: transform 220ms var(--mo-spring);
}

.badge:hover {
    transform: scale(1.06);
}

/* Skip the entry bounce when the dot is also a live indicator — both
   rules set the `animation` shorthand, and the last one would silently
   cancel the pulse. */
.badge-dot:not(.soft-pulse) {
    animation: anim-bounce-in-dot 520ms var(--mo-spring) backwards;
}

/* Pagination + table rows */
.pagination-btn {
    transition: background-color 160ms ease, color 160ms ease,
        transform 220ms var(--mo-spring);
}

.pagination-btn:hover {
    transform: translate3d(0, -2px, 0);
    z-index: 1;
}

/* ============================================================
   5. Table rows
   ============================================================ */

tbody.stagger {
    --stagger-step: 45ms;
    --stagger-duration: 460ms;
}

/* The table card scrolls sideways for wide tables but must never scroll
   vertically — it has no height cap. Left at `auto`, the rows' entrance
   animation briefly makes the content taller than the box and a vertical
   scrollbar flashes in and out on every page load. */
#table-container {
    overflow-y: hidden;
}

/* Rows slide aside on hover. No box-shadow rail on the <tr> itself —
   Tailwind's preflight sets border-collapse:collapse, under which a row
   shadow never paints, so the rail lives on the first cell. */
#object-list>tr {
    cursor: pointer;
    transition: background-color 180ms ease, transform 260ms var(--mo-out);
}

#object-list>tr:hover {
    transform: translate3d(22px, 0, 0);
}

#object-list>tr:hover>td:first-child {
    box-shadow: inset 4px 0 0 0 var(--color-primary-500, #14b8a6);
}

/* ============================================================
   6. Sidebar & navbar
   ============================================================ */

.app-navbar {
    animation: anim-drop 520ms var(--mo-out) backwards;
}

/* Only animate the docked desktop sidebar. Below xl it sits off-canvas on
   `-translate-x-full`, and an animation would out-rank that transform and
   flash the whole panel across the screen on load. Same for the saved
   collapsed state, which parks it at translateX(-100%). */
@media (min-width: 1280px) {
    html:not(.sidebar-collapsed) .app-sidenav {
        animation: anim-slide-left 560ms var(--mo-out) backwards;
    }
}

/* Nav links: icon slides + a growing rail on the left edge */
.nav-link {
    position: relative;
    transition: background-color 200ms ease, color 200ms ease,
        padding-left 260ms var(--mo-out);
}

.nav-link::before {
    content: "";
    position: absolute;
    left: 0;
    top: 50%;
    height: 0;
    width: 3px;
    border-radius: 9999px;
    background: var(--color-primary-500, #14b8a6);
    transform: translateY(-50%);
    transition: height 300ms var(--mo-spring);
}

.nav-link:hover {
    padding-left: 1.15rem;
}

.nav-link:hover::before,
.nav-link.active::before {
    height: 62%;
}

.nav-link svg {
    transition: transform 320ms var(--mo-spring), color 200ms ease;
}

.nav-link:hover svg {
    transform: scale(1.2) rotate(-6deg);
}

.nav-link.active svg {
    color: var(--color-primary-600, #0d9488);
}

/* Sidebar profile card */
.nav-profile {
    transition: transform 300ms var(--mo-out), border-color 240ms ease,
        box-shadow 300ms var(--mo-out);
}

.nav-profile:hover {
    transform: translate3d(0, -3px, 0);
    box-shadow: 0 10px 24px -10px rgb(28 25 23 / 0.28);
}

.nav-profile img {
    transition: transform 400ms var(--mo-spring);
}

.nav-profile:hover img {
    transform: scale(1.12) rotate(6deg);
}

/* Reports submenu unfolds instead of blinking on */
.nav-submenu:not(.hidden) {
    animation: anim-unfold 420ms var(--mo-out) backwards;
    transform-origin: top center;
}

/* Brand mark in the navbar */
.brand-mark {
    transition: transform 480ms var(--mo-spring), box-shadow 300ms var(--mo-out);
}

.brand-link:hover .brand-mark {
    transform: rotate(-10deg) scale(1.12);
}

.theme-toggle svg {
    transition: transform 520ms var(--mo-spring);
}

.theme-toggle:hover svg {
    transform: rotate(70deg) scale(1.18);
}

/* ============================================================
   7. Page shell entrance
   ============================================================ */

.page-header {
    animation: anim-drop 520ms var(--mo-out) backwards;
}

.page-toolbar {
    animation: anim-rise 520ms var(--mo-out) 90ms backwards;
}

.page-content {
    animation: anim-rise 620ms var(--mo-out) 160ms backwards;
}

/* Cards revealed as they scroll into view (JS adds .in-view).
   Scoped to `html.js` so the hidden start state only exists when
   something is actually around to un-hide it. */
.js .reveal {
    opacity: 0;
    transform: translate3d(0, 40px, 0) scale(0.97);
    transition: opacity 620ms var(--mo-out), transform 620ms var(--mo-out);
}

/* `none`, not a zeroed translate — a revealed card must stop being a
   containing block so Plotly's overlays keep positioning correctly. */
.reveal.in-view {
    opacity: 1;
    transform: none;
}

/* ============================================================
   8. Modal — open / close / content morph
   ------------------------------------------------------------
   Flowbite only toggles `hidden`/`flex`; initialize-modal.js adds
   `is-open` on the next frame and `is-closing` before it really
   hides, which is what drives everything below.
   ============================================================ */

.modal-root {
    perspective: 1400px;
}

.modal-panel {
    opacity: 0;
    transform: translate3d(0, 44px, 0) scale(0.9) rotateX(8deg);
    transform-origin: center top;
    transition: opacity 300ms var(--mo-out),
        transform 460ms var(--mo-spring);
}

.modal-root.is-open .modal-panel {
    opacity: 1;
    transform: translate3d(0, 0, 0) scale(1) rotateX(0);
}

.modal-root.is-closing .modal-panel {
    opacity: 0;
    transform: translate3d(0, 26px, 0) scale(0.94);
    transition: opacity 200ms var(--mo-in), transform 220ms var(--mo-in);
}

/* Height morph between the loader and the real content */
.modal-panel.is-morphing {
    overflow: hidden;
    transition: opacity 300ms var(--mo-out),
        transform 460ms var(--mo-spring),
        height 420ms var(--mo-out);
}

/* While the panel is still growing, the form is taller than the box it sits
   in, so .modal-body would flash a scrollbar for the length of the morph and
   then drop it again. Hold the overflow until the panel has reached its final
   height — initialize-modal.js adds .is-settled at that point. Gating on the
   settled state rather than on .is-morphing also covers the frames on either
   side of the morph, which is where the flicker actually showed up. */
.modal-panel:not(.is-settled) .modal-body {
    overflow: hidden;
}

/* Backdrop (created by Flowbite with these classes) */
.modal-backdrop {
    opacity: 0;
    backdrop-filter: blur(0px);
    transition: opacity 300ms var(--mo-out), backdrop-filter 420ms var(--mo-out);
}

.modal-backdrop.is-in {
    opacity: 1;
    backdrop-filter: blur(6px);
}

.modal-backdrop.is-out {
    opacity: 0;
    backdrop-filter: blur(0px);
    transition: opacity 200ms var(--mo-in), backdrop-filter 200ms var(--mo-in);
}

/* Content that just arrived from htmx */
.modal-content-enter {
    animation: anim-rise 420ms var(--mo-out) backwards;
}

/* In-modal loader (replaces the old skeleton, which resized the
   dialog twice on every open) */
.modal-loader {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 1rem;
    min-height: 9.5rem;
    animation: anim-fade 240ms ease backwards;
}

.modal-loader__ring {
    position: relative;
    width: 2.75rem;
    height: 2.75rem;
}

.modal-loader__ring::before,
.modal-loader__ring::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 9999px;
    border: 3px solid transparent;
}

.modal-loader__ring::before {
    border-color: var(--color-gray-200, #e7e5e4);
}

.dark .modal-loader__ring::before {
    border-color: var(--color-gray-700, #44403c);
}

.modal-loader__ring::after {
    border-top-color: var(--color-primary-500, #14b8a6);
    border-right-color: var(--color-primary-500, #14b8a6);
    animation: anim-spin 800ms linear infinite;
}

.modal-loader__dots {
    display: flex;
    gap: 0.35rem;
}

.modal-loader__dots span {
    width: 0.4rem;
    height: 0.4rem;
    border-radius: 9999px;
    background: var(--color-primary-400, #2dd4bf);
    animation: loader-dot 1.1s ease-in-out infinite;
}

.modal-loader__dots span:nth-child(2) {
    animation-delay: 0.14s;
}

.modal-loader__dots span:nth-child(3) {
    animation-delay: 0.28s;
}

@keyframes loader-dot {

    0%,
    100% {
        transform: translateY(0) scale(0.7);
        opacity: 0.45;
    }

    50% {
        transform: translateY(-8px) scale(1);
        opacity: 1;
    }
}

/* Header spinner while the title is still loading */
.modal-header-loading {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    color: var(--color-gray-400, #a8a29e);
    font-weight: 600;
    animation: anim-fade 240ms ease backwards;
}

.modal-header-loading i {
    width: 1rem;
    height: 1rem;
    border-radius: 9999px;
    border: 2px solid currentColor;
    border-top-color: transparent;
    animation: anim-spin 700ms linear infinite;
}

/* ============================================================
   9. Drawer
   ============================================================ */

#defaultDrawer {
    transition: transform 460ms var(--mo-out), box-shadow 460ms var(--mo-out);
    /* #drawer-body owns horizontal scrolling for wide content; the panel
       itself must not, or the content's slide-in flashes a scrollbar. */
    overflow-x: hidden;
}

[drawer-backdrop] {
    animation: anim-fade 280ms var(--mo-out) backwards;
    backdrop-filter: blur(5px);
}

.backdrop-ghost-out {
    animation: backdrop-out 260ms var(--mo-in) forwards;
    backdrop-filter: blur(5px);
    pointer-events: none;
}

@keyframes backdrop-out {
    from {
        opacity: 1;
    }

    to {
        opacity: 0;
    }
}

.drawer-content-enter {
    animation: anim-slide-right 480ms var(--mo-out) backwards;
}

/* ============================================================
   10. Toast
   ============================================================ */

@keyframes toast-in {
    0% {
        opacity: 0;
        transform: translate3d(120%, 0, 0) scale(0.85);
    }

    70% {
        opacity: 1;
        transform: translate3d(-8px, 0, 0) scale(1.03);
    }

    100% {
        opacity: 1;
        transform: translate3d(0, 0, 0) scale(1);
    }
}

@keyframes toast-out {
    0% {
        opacity: 1;
        transform: translate3d(0, 0, 0) scale(1);
    }

    30% {
        transform: translate3d(-10px, 0, 0) scale(1.02);
    }

    100% {
        opacity: 0;
        transform: translate3d(120%, 0, 0) scale(0.9);
    }
}

.toast-slide-in {
    animation: toast-in 620ms var(--mo-spring) both;
}

.toast-slide-out {
    animation: toast-out 420ms var(--mo-in) both;
}

/* Countdown rail so the auto-dismiss is visible */
#defaultToast.toast-slide-in::after {
    content: "";
    position: absolute;
    left: 1rem;
    right: 1rem;
    bottom: 0.5rem;
    height: 3px;
    border-radius: 9999px;
    background: var(--color-primary-500, #14b8a6);
    transform-origin: left center;
    animation: toast-countdown 5s linear both;
}

@keyframes toast-countdown {
    from {
        transform: scaleX(1);
    }

    to {
        transform: scaleX(0);
    }
}

#toast-content>div:first-child {
    animation: anim-pop 620ms var(--mo-spring) 120ms both;
}

/* ============================================================
   11. htmx — request progress bar + swapped content
   ============================================================ */

#hx-progress {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: 3px;
    z-index: 80;
    pointer-events: none;
    opacity: 0;
    transition: opacity 240ms ease;
}

#hx-progress.is-active {
    opacity: 1;
}

#hx-progress span {
    display: block;
    height: 100%;
    width: 0;
    border-radius: 0 9999px 9999px 0;
    background: linear-gradient(90deg,
            var(--color-primary-400, #2dd4bf),
            var(--color-primary-600, #0d9488),
            var(--color-sky-400, #38bdf8));
    box-shadow: 0 0 12px 1px rgb(20 184 166 / 0.65);
    transition: width 240ms var(--mo-out);
}

/* Buttons mid-request read as busy */
.htmx-request.btn {
    opacity: 0.75;
    pointer-events: none;
}

/* ============================================================
   12. Entrance pages (login / password / logout)
   ============================================================ */

.entrance-orb {
    animation: anim-drift 22s ease-in-out infinite;
}

.entrance-orb--alt {
    animation: anim-drift 28s ease-in-out infinite reverse;
}

.entrance-brand {
    animation: anim-pop 720ms var(--mo-spring) backwards;
}

.entrance-card {
    animation: anim-blur 760ms var(--mo-out) 120ms backwards;
}

.entrance-card form>*,
.entrance-card h1,
.entrance-card p {
    animation: anim-rise 520ms var(--mo-out) backwards;
}

.entrance-card h1 {
    animation-delay: 240ms;
}

.entrance-card p {
    animation-delay: 300ms;
}

.entrance-card form>*:nth-child(1) {
    animation-delay: 360ms;
}

.entrance-card form>*:nth-child(2) {
    animation-delay: 430ms;
}

.entrance-card form>*:nth-child(3) {
    animation-delay: 500ms;
}

.entrance-card form>*:nth-child(n + 4) {
    animation-delay: 560ms;
}

.entrance-footer {
    animation: anim-fade 620ms ease 700ms backwards;
}

/* --- Showcase panel beside the sign-in card ------------------- */

.showcase-headline {
    animation: anim-rise 720ms var(--mo-out) 120ms backwards;
}

.showcase-sub {
    animation: anim-rise 720ms var(--mo-out) 240ms backwards;
}

/* Slow drift across the brand ramp so the headline never sits still */
.showcase-gradient {
    background: linear-gradient(100deg,
            var(--color-primary-500, #14b8a6),
            var(--color-sky-500, #0ea5e9),
            var(--color-primary-400, #2dd4bf),
            var(--color-primary-600, #0d9488));
    background-size: 300% 100%;
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    animation: gradient-drift 7s ease-in-out infinite;
}

@keyframes gradient-drift {

    0%,
    100% {
        background-position: 0% 50%;
    }

    50% {
        background-position: 100% 50%;
    }
}

/* Heartbeat trace: draws itself, holds, then repeats */
.ecg-line path {
    stroke-dasharray: 420;
    stroke-dashoffset: 420;
    animation: ecg-draw 3.4s cubic-bezier(0.65, 0, 0.35, 1) 400ms infinite;
}

@keyframes ecg-draw {
    0% {
        stroke-dashoffset: 420;
        opacity: 0.25;
    }

    45% {
        stroke-dashoffset: 0;
        opacity: 1;
    }

    75% {
        stroke-dashoffset: 0;
        opacity: 1;
    }

    100% {
        stroke-dashoffset: -420;
        opacity: 0.25;
    }
}

.showcase-board {
    animation: anim-pop 760ms var(--mo-spring) 380ms backwards,
        anim-float 6s ease-in-out 1.2s infinite;
}

.showcase-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    border-radius: 9999px;
    padding: 0.4rem 0.85rem;
    font-size: 0.75rem;
    font-weight: 700;
    color: var(--color-gray-600, #57534e);
    background: rgb(255 255 255 / 0.7);
    box-shadow: inset 0 0 0 1px var(--color-gray-200, #e7e5e4);
    transition: transform 260ms var(--mo-spring), color 200ms ease;
}

.dark .showcase-chip {
    color: var(--color-gray-300, #d6d3d1);
    background: rgb(41 37 36 / 0.6);
    box-shadow: inset 0 0 0 1px var(--color-gray-700, #44403c);
}

.showcase-chip:hover {
    transform: translate3d(0, -3px, 0);
    color: var(--color-primary-600, #0d9488);
}

/* ============================================================
   13. Command palette
   ============================================================ */

.palette-panel {
    animation: anim-pop 460ms var(--mo-spring) backwards;
}

.palette-backdrop {
    animation: anim-fade 260ms ease backwards;
}

/* ============================================================
   12b. Timeline (visit history)
   ------------------------------------------------------------
   The rail dots are position:absolute against the <ol>. A transform
   on the <li> makes that <li> their containing block instead, so
   every dot jumped sideways for the length of the entrance and then
   snapped back once the transform was released.

   So the <li> only fades — the card inside does the moving, picking
   up the same staggered delay via `animation-delay: inherit`.
   ============================================================ */

.timeline>li {
    --stagger-anim: anim-fade;
}

.timeline>li>.card {
    animation: anim-slide-right 520ms var(--mo-out) backwards;
    animation-delay: inherit;
}

/* Pure scale — a translate here would drag the dot off the rail. */
.timeline-dot {
    animation: anim-bounce-in-dot 520ms var(--mo-spring) backwards;
    animation-delay: inherit;
}

/* ============================================================
   13b. Waiting-room screen
   That board re-fetches itself every 10s, so its cascade is kept
   short — it should read as a refresh pulse, not a full replay.
   ============================================================ */

.board-refresh {
    --stagger-step: 45ms;
    --stagger-duration: 360ms;
}

@keyframes serving-glow {

    0%,
    100% {
        box-shadow: 0 0 24px -8px rgb(16 185 129 / 0.75);
    }

    50% {
        box-shadow: 0 0 48px -2px rgb(16 185 129 / 1);
    }
}

.now-serving {
    animation: serving-glow 2.2s ease-in-out infinite;
}

/* ============================================================
   14. Charts & stat cards
   ============================================================ */

.stat-card {
    transition: transform 300ms var(--mo-out), box-shadow 300ms var(--mo-out);
}

.stat-card:hover {
    transform: translate3d(0, -6px, 0);
}

.stat-card:hover .stat-icon {
    animation: anim-wiggle 620ms ease-in-out;
}

.chart-card {
    animation: anim-rise 620ms var(--mo-out) backwards;
}

/* Skeleton shimmer (kept for the <c-skeleton> component) */
.skeleton {
    background-image: linear-gradient(100deg,
            transparent 20%,
            rgb(255 255 255 / 0.55) 45%,
            transparent 70%);
    background-size: 220% 100%;
    background-repeat: no-repeat;
    animation: anim-shimmer 1.6s ease-in-out infinite;
}

.dark .skeleton {
    background-image: linear-gradient(100deg,
            transparent 20%,
            rgb(255 255 255 / 0.09) 45%,
            transparent 70%);
}

/* ============================================================
   15. Cross-document view transitions (Chromium)
   The shell keeps its identity while the page content swaps.
   ============================================================ */

@view-transition {
    navigation: auto;
}

/* Deliberately a plain crossfade: the incoming page runs its own
   .page-header / .page-toolbar / .page-content entrance on top of this,
   and translating both at once reads as lag rather than polish. */
@keyframes vt-old {
    to {
        opacity: 0;
        transform: scale(0.99);
    }
}

@keyframes vt-new {
    from {
        opacity: 0;
        transform: scale(0.99);
    }
}

::view-transition-old(root) {
    animation: vt-old 180ms var(--mo-in) both;
}

::view-transition-new(root) {
    animation: vt-new 260ms var(--mo-out) both;
}

.app-navbar {
    view-transition-name: app-navbar;
}

.app-sidenav {
    view-transition-name: app-sidenav;
}

/* ============================================================
   16. Reduced motion — everything above collapses to a fade
   ============================================================ */

@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;
    }

    .js .reveal {
        opacity: 1;
        transform: none;
    }

    .btn:hover:not(:disabled),
    .hover-lift:hover,
    .hover-grow:hover,
    .stat-card:hover,
    #object-list>tr:hover {
        transform: none;
    }

    ::view-transition-old(root),
    ::view-transition-new(root) {
        animation: none;
    }
}
