/* Shared motion primitives.
   Every page links this after tokens.css. Anything here is app-wide: the one
   spin keyframe, the skeleton sweep, the indeterminate progress bar, and the
   single reduced-motion escape hatch.

   Before this file existed the same rotation was declared four times under
   four names (spin / bld-spin / it-spin / stage-stepper-spin) in four files,
   and adspy.css owned a keyframe called `shimmer` that was really an opacity
   pulse — on a name generic enough to collide with anything added later. */

/* ── spinner ────────────────────────────────────────────────────────────
   The single rotation keyframe. builder.css, item-tree.css, panels.css and
   stage-stepper.css all reference this one. */

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

.spinner {
    display: inline-block;
    width: 14px;
    height: 14px;
    border: 2px solid var(--border3);
    border-top-color: var(--blue-text);
    border-radius: 50%;
    animation: spin 700ms linear infinite;
    flex-shrink: 0;
}

.spinner-sm {
    width: 10px;
    height: 10px;
    border-width: 1.5px;
}

.spinner-lg {
    width: 22px;
    height: 22px;
    border-width: 2.5px;
}

/* Legacy helper kept for the dashboard refresh glyph, which rotates an
   existing character rather than drawing a ring. */
.spinning {
    display: inline-block;
    animation: spin 700ms linear infinite;
}

/* ── skeletons ──────────────────────────────────────────────────────────
   A highlight sweeping across, not the whole element blinking. Skeletons must
   match the geometry of the content they stand in for — a skeleton that is a
   different size than the real thing trades a spinner for a layout jump. */

@keyframes sk-sweep {
    from {
        background-position: 100% 0;
    }
    to {
        background-position: -100% 0;
    }
}

.sk {
    background-color: var(--bg3);
    background-image: linear-gradient(90deg,
    transparent 0%,
    rgba(255, 255, 255, 0.055) 50%,
    transparent 100%);
    background-size: 200% 100%;
    background-repeat: no-repeat;
    animation: sk-sweep 1.2s ease-in-out infinite;
    border-radius: 3px;
}

.sk-line {
    height: 10px;
}

/* A control-shaped placeholder: same 30px as .ctrl, so a skeleton form holds
   the exact height the real one will occupy and the swap moves nothing. */
.sk-field {
    height: 30px;
    border-radius: var(--r);
}

/* ── skeleton composition ───────────────────────────────────────────────
   Enough vocabulary to lay out a placeholder in a template without any of
   them hand-rolling their own geometry — every page that renders its content
   from JS lands on one of these instead of on an empty container.

   Nothing here is page-specific: .sk-card matches the shared card surface
   (--bg2 + hairline + --r-lg) used by the review list and the service tiles,
   so a placeholder card is the real card minus its content. */

.sk-stack {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.sk-inline {
    display: flex;
    align-items: center;
    gap: 10px;
}

/* Equal shares rather than leaving it to each child's intrinsic width, so a
   row of placeholders divides its space predictably. */
.sk-inline > * {
    flex: 1 1 0;
    min-width: 0;
}

.sk-card {
    display: flex;
    flex-direction: column;
    gap: 9px;
    padding: 14px 18px;
    border: 1px solid var(--border);
    border-radius: var(--r-lg);
    background: var(--bg2);
}

.sk-w20 { width: 20%; }
.sk-w30 { width: 30%; }
.sk-w40 { width: 40%; }
.sk-w50 { width: 50%; }
.sk-w60 { width: 60%; }
.sk-w70 { width: 70%; }
.sk-w100 { width: 100%; }

/* Placeholders are decoration, not content: they carry aria-hidden in the
   markup, and they must never outlive the data they stand in for. */
.sk-pad {
    padding: 14px 18px;
}

/* ── indeterminate progress bar ─────────────────────────────────────────
   The "something is happening" signal for a background fetch. Sits above the
   content region so it reads from anywhere on the page, unlike a spinner
   tucked into a toolbar corner. Toggle with the [hidden] attribute. */

.prog-bar {
    position: relative;
    height: 2px;
    background: var(--bg3);
    overflow: hidden;
    flex-shrink: 0;
}

.prog-bar::after {
    content: '';
    position: absolute;
    inset: 0 auto 0 0;
    width: 40%;
    background: linear-gradient(90deg, transparent, var(--blue-text), transparent);
    animation: prog-slide 1.1s var(--e-in-out) infinite;
}

@keyframes prog-slide {
    from {
        transform: translateX(-100%);
    }
    to {
        transform: translateX(350%);
    }
}

/* ── list / row entrances ───────────────────────────────────────────────
   Capped stagger. Rows 1–9 step 10ms apart; row 10 and everything after it
   share a 90ms delay, so a 200-row page costs exactly what a 10-row page
   costs. Uncapped stagger is the most common way animation makes a data
   table feel slow.

   Ten explicit nth-child rules rather than an inline --i custom property,
   because CLAUDE.md rules out inline styles in markup. */

@keyframes row-in {
    from {
        opacity: 0;
        transform: translateY(-3px);
    }
    to {
        opacity: 1;
        transform: none;
    }
}

.rows-entering > tr,
.rows-entering > .row-in {
    animation: row-in var(--t-base) var(--e-out) both;
    animation-delay: 90ms;
}

.rows-entering > tr:nth-child(1), .rows-entering > .row-in:nth-child(1) { animation-delay: 0ms; }
.rows-entering > tr:nth-child(2), .rows-entering > .row-in:nth-child(2) { animation-delay: 10ms; }
.rows-entering > tr:nth-child(3), .rows-entering > .row-in:nth-child(3) { animation-delay: 20ms; }
.rows-entering > tr:nth-child(4), .rows-entering > .row-in:nth-child(4) { animation-delay: 30ms; }
.rows-entering > tr:nth-child(5), .rows-entering > .row-in:nth-child(5) { animation-delay: 40ms; }
.rows-entering > tr:nth-child(6), .rows-entering > .row-in:nth-child(6) { animation-delay: 50ms; }
.rows-entering > tr:nth-child(7), .rows-entering > .row-in:nth-child(7) { animation-delay: 60ms; }
.rows-entering > tr:nth-child(8), .rows-entering > .row-in:nth-child(8) { animation-delay: 70ms; }
.rows-entering > tr:nth-child(9), .rows-entering > .row-in:nth-child(9) { animation-delay: 80ms; }

/* ── value change flash ─────────────────────────────────────────────────
   Two-step, no keyframes and no cleanup timer: add .flash to snap the
   highlight on (transition:none), then remove it on the next frame and the
   token transition carries it back down. */

.flash-target {
    border-radius: 3px;
    transition: background-color var(--t-slow) var(--e-out);
}

.flash-target.flash {
    background-color: rgba(255, 255, 255, 0.10);
    transition: none;
}

/* ── reduced motion ─────────────────────────────────────────────────────
   Because every duration is a token, honouring the preference is one block
   here instead of an audit of every feature that ever added a transition.
   base.css separately opts the cross-document view transitions out. */

@media (prefers-reduced-motion: reduce) {
    :root {
        --t-instant: 0s;
        --t-fast: 0s;
        --t-base: 0s;
        --t-slow: 0s;
    }

    .sk,
    .spinner,
    .spinning,
    .prog-bar::after,
    .rows-entering > tr,
    .rows-entering > .row-in {
        animation: none;
    }
}
