/* =====================================================================
   MOTION LAYER

   Loaded after theme.css and pages.css, and deliberately kept apart from
   both: everything here is presentation polish that can be removed by
   dropping one <link> without touching a single layout rule.

   The rule this file follows, without exception:

     Animate transform and opacity. Nothing else.

   Those two are the only properties a browser can animate on the
   compositor, which means the work leaves the main thread and, on a
   laptop, leaves the CPU. Animating width, top, filter, box-shadow or
   background-position forces layout or paint on every single frame —
   which is exactly what makes a fan spin up. The scroll-driven sections
   below go one step further and run with no JavaScript at all.

   Three switches turn all of it off, and every one of them is honoured:

     prefers-reduced-motion   the visitor asked for stillness
     html.perf-lite           the probe in layout.php found a modest machine
     html.is-idle             app.js parked the tab because it is hidden
   ===================================================================== */

:root {
    /* One place to retune the whole layer. */
    --m-rise:      22px;     /* how far an entering element travels */
    --m-duration:  0.62s;
    --m-stagger:   70ms;     /* gap between siblings in a cascade */
    --m-ease-out:  cubic-bezier(0.16, 1, 0.3, 1);
}

/* =====================================================================
   1. SCROLL-DRIVEN PROGRESS BAR

   The reading-progress bar used to be written by a scroll listener in
   app.js: every scroll event scheduled a frame, read scrollY and
   scrollHeight — a forced layout read — and wrote a transform.

   A scroll-driven animation does the same thing on the compositor, with
   no listener, no layout read and no JavaScript. app.js detects support
   for this and skips its own handler entirely, so the two never both run.
   ===================================================================== */
@supports (animation-timeline: scroll()) {
    @keyframes m-progress {
        from { transform: scaleX(0); }
        to   { transform: scaleX(1); }
    }

    html.has-scroll-timeline .scroll-progress {
        animation: m-progress linear forwards;
        animation-timeline: scroll(root block);
    }
}

/* =====================================================================
   2. ENTRANCE

   theme.css gives .reveal its opacity-0 starting state and app.js adds
   .is-visible when the element scrolls into view. Only the feel is
   changed here — a decelerating curve instead of a symmetric one, so an
   element arrives quickly and settles rather than drifting in at a
   constant rate — and the travel is written as translate3d so the
   element is promoted to its own layer for the duration.

   The grids are deliberately left alone: their markup already staggers
   by column (data-delay="<?= $position % 3 + 1 ?>"), which sends a wave
   diagonally across the grid. A single running cascade would replace
   that with a queue, and the last card of a nine-card grid would be
   most of a second late.
   ===================================================================== */
.reveal {
    transition-duration: var(--m-duration), var(--m-duration);
    transition-timing-function: var(--m-ease-out), var(--m-ease-out);
    transform: translate3d(0, var(--m-rise), 0);
}
.reveal.is-visible {
    transform: translate3d(0, 0, 0);
}

/* Opt-in hook for a run of siblings that should arrive in order: set
   --m-index on each. Nothing in the stylesheet imposes it. */
.reveal[style*="--m-index"] {
    transition-delay: calc(var(--m-index) * var(--m-stagger));
}

/* =====================================================================
   3. HOVER

   Every one of these is a transform on an element that is already its
   own compositor layer while the pointer is over it. None of them
   animates a shadow or a filter, both of which repaint on each frame.
   ===================================================================== */

/* Cards lift a touch further and settle with a spring rather than a
   linear stop. theme.css already owns the colour and shadow change. */
.project-card {
    transition: transform 0.38s var(--m-ease-out),
                border-color var(--t-base),
                box-shadow var(--t-base);
}
.project-card:hover { transform: translate3d(0, -7px, 0); }
.project-card:active { transform: translate3d(0, -2px, 0); transition-duration: 0.1s; }

/* Buttons and links acknowledge the press. Scale, not padding: padding
   would reflow the line it sits on. */
.btn, .chip, .nav__link, .nav__action {
    transition: transform 0.22s var(--m-ease-out), background-color var(--t-fast),
                border-color var(--t-fast), color var(--t-fast), opacity var(--t-fast);
}
.btn:active, .chip:active, .nav__link:active, .nav__action:active { transform: scale(0.97); }

.to-top { transition: opacity var(--t-base), visibility var(--t-base), transform 0.3s var(--m-ease-out); }
.to-top:hover { transform: translate3d(0, -4px, 0); }
.to-top:active { transform: translate3d(0, -1px, 0) scale(0.94); }

/* =====================================================================
   4. FOCUS

   Keyboard focus deserves the same quality of feedback as the pointer
   gets, and outline-offset animates without repainting the element.
   ===================================================================== */
a:focus-visible, button:focus-visible, [tabindex]:focus-visible,
input:focus-visible, select:focus-visible, textarea:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 3px;
    transition: outline-offset 0.18s var(--m-ease-out);
}

/* =====================================================================
   5. CROSS-DOCUMENT VIEW TRANSITIONS

   A browser that supports this cross-fades between two page loads
   instead of flashing white, and does it entirely on the compositor
   using snapshots it already had to take. A browser that does not
   support it ignores the whole block and navigates as before — there is
   no JavaScript here to fall back from.

   The header and the brand are given stable names, so they stay put
   while the content beneath them changes rather than fading with it.
   ===================================================================== */
@view-transition {
    navigation: auto;
}

.site-header { view-transition-name: site-header; }
.site-header .brand { view-transition-name: site-brand; }
main#main { view-transition-name: page-content; }

::view-transition-old(page-content) {
    animation: m-page-out 0.22s var(--ease) both;
}
::view-transition-new(page-content) {
    animation: m-page-in 0.34s var(--m-ease-out) both;
}

@keyframes m-page-out {
    to { opacity: 0; transform: translate3d(0, -8px, 0); }
}
@keyframes m-page-in {
    from { opacity: 0; transform: translate3d(0, 12px, 0); }
}

/* The header is carried across rather than animated, so it does not
   appear to jump when the page beneath it changes height. */
::view-transition-group(site-header),
::view-transition-group(site-brand) {
    animation-duration: 0.28s;
}

/* =====================================================================
   6. THE SWITCHES

   Everything above is undone here, in the order of how strongly the
   visitor or their machine asked for it.
   ===================================================================== */

/* A modest machine keeps the layout and loses the choreography. The
   cascade is the first thing to go: it is the one effect that delays
   content rather than decorating it. */
html.perf-lite .reveal,
html.perf-lite .reveal[style*="--m-index"] {
    transition-delay: 0s !important;
    transition-duration: 0.3s !important;
}
html.perf-lite .project-card:hover { transform: translate3d(0, -4px, 0); }

/* A hidden tab paints nothing, so nothing here should be mid-transition
   when it comes back. */
html.is-idle .reveal,
html.is-idle .project-card,
html.is-idle .to-top {
    transition: none !important;
}

/* The visitor asked for stillness. Content still arrives — it simply
   arrives rather than travelling — and page transitions stop entirely. */
@media (prefers-reduced-motion: reduce) {
    .reveal,
    .reveal[style*="--m-index"] {
        opacity: 1 !important;
        transform: none !important;
        transition: none !important;
        transition-delay: 0s !important;
    }

    .project-card:hover,
    .project-card:active,
    .btn:active, .chip:active, .nav-pill:active,
    .to-top:hover, .to-top:active {
        transform: none;
    }

    html.has-scroll-timeline .scroll-progress { animation: none; }

    ::view-transition-old(page-content),
    ::view-transition-new(page-content) {
        animation: none !important;
    }
}
