/* Don't show overflow if a table row is swapping */
table:has(tr.htmx-request) {
    overflow: hidden !important;
}

/*
Inline CRUD tables (common/htmx/inline_crud.py).

A row swaps between a read-only cell and a form input of the same column, so the
table's geometry must not depend on cell contents. Fixed layout locks column
widths to the <thead> row: columns with an explicit Col.width keep it, and the
rest divide the remaining space evenly. Without this, entering edit mode
re-solves the grid and every row shifts sideways.
*/
.inline-crud-table {
    table-layout: fixed;
}

/*
Match the read row's height to the edit row's so swapping doesn't push the rows
below it up and down. The value is the .form-control height plus the vertical
padding Bootstrap puts on a table cell.
*/
.inline-crud-table > tbody > tr > td {
    height: calc(1.5em + 0.75rem + 2px + 1.5rem);
    vertical-align: middle;
    /* Columns can no longer widen to fit, so long values must wrap. */
    overflow-wrap: break-word;
}

/* Fade a deleted row out instead of snapping the table closed. */
.inline-crud-table tr.htmx-swapping {
    opacity: 0;
    transform: translateX(1rem);
    transition: opacity 200ms ease, transform 200ms ease;
}

@media (prefers-reduced-motion: reduce) {
    .inline-crud-table tr.htmx-swapping {
        transition: none;
    }
}

/*
Spinner over an icon in a button:
    <button>
        <span class="htmx-spinner-icon">
            <span class="htmx-indicator"></span>
            <i class="fa fa-link text-secondary"></i>
        </span>
    </button
*/

.htmx-spinner-icon {
    position: relative;
    display: inline-flex;
}
.htmx-spinner-icon .htmx-indicator {
    width: 14px;
    height: 14px;
    color: #4cfd4c;
    position: absolute;
    display: inline-block;
    vertical-align: -.125em;
    border: .25em solid currentcolor;
    border-right-color: transparent;
    border-radius: 50%;
    -webkit-animation: .75s linear infinite spinner-border;
    animation: .75s linear infinite spinner-border;
}


/* HTMX Loading: Pulse Blur Effect */
.htmx-loading-pulse-blur.htmx-request {
    animation: pulse-blur 1s ease-in-out infinite;
}
@keyframes pulse-blur {
    0%, 100% {
        opacity: 1;
        filter: blur(0px);
    }
    50% {
        opacity: 0.4;
        filter: blur(1px);
    }
}

/* HTMX Loading: Skeleton Shimmer Effect
   A sweep of "not the background" across content that is being replaced.
   --ink-rgb is white on dark and slate on light, so one gradient themes both.

   The alphas are low on purpose. The surfaces underneath are flat and opaque,
   so a sweep no longer has a translucent wash to blend into and any real
   brightness reads as a glare bar crossing live content rather than as a
   sheen on a pending one. */
.htmx-loading-skeleton-shimmer.htmx-request {
    position: relative;
    pointer-events: none;
    overflow: hidden;
}

/* Content backs off while the swap is in flight, so the panel reads as
   pending. Without this the sweep is the only signal, and a bright band over
   fully-lit content looks like a rendering fault rather than a loading state. */
.htmx-loading-skeleton-shimmer.htmx-request > * {
    opacity: 0.55;
    transition: opacity 120ms ease;
}

.htmx-loading-skeleton-shimmer.htmx-request::after {
    content: "";
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    /* Symmetric, and narrow: the lit band is about a third of the width, so it
       crosses as a highlight with falloff either side. The old ramp peaked at
       60% and cut to zero at the edge, which led with a hard vertical edge. */
    background-image: linear-gradient(
            90deg,
            rgba(var(--ink-rgb), 0) 0%,
            rgba(var(--ink-rgb), 0) 30%,
            rgba(var(--ink-rgb), 0.07) 50%,
            rgba(var(--ink-rgb), 0) 70%,
            rgba(var(--ink-rgb), 0) 100%
    );
    /* The band is swept by moving the BACKGROUND across a box that stays put, not
       by translating the box itself. A translated overlay sits a full width outside
       its host for most of the cycle and is only invisible because the host clips
       it — so any host that must not clip leaks a bright band across the page
       beside it. An overlay hosting a select2 is exactly that host: it declares
       `overflow: visible` so the select2 menu can escape, and the shimmer escaped
       with it. Nothing here depends on the host's overflow now. */
    background-repeat: no-repeat;
    background-size: 300% 100%;
    animation: shimmer 2.1s linear infinite;
    will-change: background-position;
}

/* `linear` rather than the default `ease`, which slows at both ends and makes
   the band hang at the edges before snapping back. The pass takes 1.3s of the
   2.1s cycle and the remainder is dead air, so repeated sweeps read as a pulse
   with a rest instead of a belt that never stops moving. */
@keyframes shimmer {
    0% {
        background-position: 150% 0;
    }
    62%, 100% {
        background-position: -150% 0;
    }
}

@media (prefers-reduced-motion: reduce) {
    .htmx-loading-skeleton-shimmer.htmx-request::after {
        animation: none;
        background: rgba(var(--ink-rgb), 0.04);
    }
}

/* HTMX Loading: Breathing Scale Effect */
.htmx-loading-breathing-scale.htmx-request {
    animation: scale-fade 0.8s ease-in-out infinite alternate;
    transform-origin: center;
}
@keyframes scale-fade {
    0% {
        opacity: 1;
        transform: scale(1);
    }
    100% {
        opacity: 0.3;
        transform: scale(0.98);
    }
}

/* HTMX Loading: Centered Dots Overlay Effect */
.htmx-loading-dots-overlay.htmx-request {
    position: relative;
    opacity: 0.6;
    pointer-events: none;
}
.htmx-loading-dots-overlay.htmx-request::after {
    content: "●●●";
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    font-size: 24px;
    letter-spacing: 8px;
    color: #3b82f6;
    animation: dots 1.5s steps(4, end) infinite;
    background: rgba(255, 255, 255, 0.95);
    padding: 10px 20px;
    border-radius: 20px;
    box-shadow: var(--dots-overlay-shadow);
}
@keyframes dots {
    0%, 20% { content: "●"; }
    40% { content: "●●"; }
    60%, 100% { content: "●●●"; }
}

/* HTMX Loading: Sliding Line Progress - Fixed for no overflow */
.htmx-loading-sliding-line.htmx-request {
    position: relative;
    opacity: 0.6;
    overflow: hidden !important;
}
.htmx-loading-sliding-line.htmx-request::after {
    content: "";
    position: absolute;
    top: 0;
    left: -30%;
    height: 4px;
    width: 30%;
    background: linear-gradient(90deg, transparent, #3b82f6, transparent);
    animation: slide-line 1.5s linear infinite;
}
@keyframes slide-line {
    0% {
        left: -30%;
    }
    100% {
        left: 100%;
    }
}

/* HTMX Loading: Ripple Effect */
.htmx-loading-ripple.htmx-request {
    transition: opacity 0.15s ease-out;
    opacity: 0.55;
}
.htmx-loading-ripple.htmx-request::before,
.htmx-loading-ripple.htmx-request::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 160px;
    height: 160px;
    border-radius: 50%;
    border: 3px solid #3b82f6;
    transform: translate(-50%, -50%);
    animation: ripple 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
    z-index: 9999999;
}
.htmx-loading-ripple.htmx-request::after {
    animation-delay: -0.5s;
}
@keyframes ripple {
    0% {
        width: 0;
        height: 0;
        opacity: 1;
    }
    100% {
        width: 320px;
        height: 320px;
        opacity: 0;
    }
}

/* Modifier: Center ripple vertically in viewport */
.htmx-loading-ripple.ripple-viewport-center.htmx-request::before,
.htmx-loading-ripple.ripple-viewport-center.htmx-request::after {
    position: fixed;
    top: 50vh;
    left: 50%
}

/* HTMX Loading: Glitch Effect */
.htmx-loading-glitch.htmx-request {
    position: relative;
    animation: glitch 2s steps(20) infinite;
}
@keyframes glitch {
    0%, 100% {
        filter: hue-rotate(0deg) saturate(100%);
        transform: translateX(0);
    }
    20% {
        filter: hue-rotate(90deg) saturate(120%);
        transform: translateX(-2px);
    }
    40% {
        filter: hue-rotate(180deg) saturate(110%);
        transform: translateX(2px);
    }
    60% {
        filter: hue-rotate(270deg) saturate(130%);
        transform: translateX(-1px);
    }
    80% {
        filter: hue-rotate(360deg) saturate(100%);
        transform: translateX(1px);
    }
}

/* HTMX Loading: Scanner Effect */
.htmx-loading-scanner.htmx-request {
    position: relative;
    opacity: 0.85;
    overflow: hidden;
}
.htmx-loading-scanner.htmx-request::after {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 2px;
    background: linear-gradient(90deg, transparent, #10b981, transparent);
    box-shadow: 0 0 10px #10b981;
    animation: scan 2s linear infinite;
}
@keyframes scan {
    0% {
        top: 0;
    }
    50% {
        top: 100%;
    }
    51% {
        top: 100%;
    }
    100% {
        top: 0;
    }
}

/* Disable click events on the hx-indicator element */
body .htmx-request, body.htmx-request, body[disabled] {
    pointer-events: none;
    cursor: wait;
}

/* Disable all interaction when modal dialog is loading */
html:has(#htmx-modal-dialog:is(.htmx-request, .htmx-modal-loading)),
html:has(#htmx-modal-dialog:is(.htmx-request, .htmx-modal-loading)) * {
    cursor: wait !important;
}
body:has(#htmx-modal-dialog:is(.htmx-request, .htmx-modal-loading)) * {
    pointer-events: none !important;
}

/* HTMX Modal Spinner - centered in viewport by default */
.htmx-modal-spinner {
    display: none;
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    z-index: 1060; /* Above modal backdrop (1050) */
    pointer-events: none;
    color: rgba(180, 180, 180, 0.7);
}

/* Show outer spinner when modal is loading (fallback, viewport centered) */
#htmx-modal:has(#htmx-modal-dialog:is(.htmx-request, .htmx-modal-loading)) > .htmx-modal-spinner {
    display: block;
}

/* Hide outer spinner if inner spinner exists (to avoid duplicates) */
#htmx-modal:has(#htmx-modal-dialog .htmx-modal-spinner) > .htmx-modal-spinner {
    display: none;
}

/* Inner spinner (added by JS when modal has content) - centered in the dialog */
#htmx-modal-dialog .htmx-modal-spinner {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    z-index: 1060;
    display: block;
}

