/* =========================================================================
   Motion
   -------------------------------------------------------------------------
   Everything that moves, in one place, so the site has a motion system rather
   than a transition bolted onto whichever rule needed one.

   Three ideas run through it:

   1. Motion is split by job, following the Material 3 split. State layers
      (hover, press, focus) answer in about 120ms with standard easing.
      Components move in about 240ms. Whole-view changes take about 420ms with
      emphasized easing, which starts and ends slowly so a page change reads as
      one object moving rather than two things swapping. The tokens live in
      site.css beside the colours.

   2. Entrances are scroll-driven, not scripted. animation-timeline: view()
      hands the browser the job the IntersectionObserver used to do, and runs
      it on the compositor instead of the main thread, so it stays smooth while
      the page is busy. The JavaScript reveal stays as the fallback for
      browsers without it; this file never hides anything those browsers would
      then fail to show, because every rule that starts at opacity 0 sits
      inside @supports (animation-timeline: view()).

   3. Nothing here runs for a visitor who has asked for less motion. Each block
      is inside prefers-reduced-motion: no-preference rather than relying on
      the global reduce override, so an animation can never be left half
      applied.

   Deliberately not here: a scroll progress bar, a loading skeleton, and
   anything that moves on its own without being asked. The audience is mostly
   reading a date and an address on a phone in a bar parking lot.
   ========================================================================= */

/* ------------------------------------------------------- page transitions */

/*
 * Cross-document view transitions. Both the outgoing and the incoming page
 * have to opt in, which they do because every public page loads this file.
 *
 * The header, the announcement bar and the footer are named, which takes them
 * out of the root snapshot: they hold still while the content between them
 * moves, so navigating reads as the page changing rather than the whole window
 * being replaced. Each name appears exactly once per page, which is the rule.
 *
 * If the next page is not ready within four seconds the browser drops the
 * transition and navigates normally, so a slow connection loses the effect and
 * nothing else.
 */
@view-transition {
    navigation: auto;
}

@media (prefers-reduced-motion: reduce) {
    @view-transition {
        navigation: none;
    }
}

.site-header {
    view-transition-name: site-header;
}

.announce {
    view-transition-name: announce;
}

.site-footer {
    view-transition-name: site-footer;
}

@media (prefers-reduced-motion: no-preference) {
    ::view-transition-old(root) {
        animation: vt-leave var(--dur-view) var(--ease-emph-accel) both;
    }

    ::view-transition-new(root) {
        animation: vt-enter var(--dur-view) var(--ease-emph-decel) both;
    }
}

@keyframes vt-leave {
    to {
        opacity: 0;
        transform: translateY(-6px);
    }
}

@keyframes vt-enter {
    from {
        opacity: 0;
        transform: translateY(12px);
    }
}

/* ------------------------------------------------------ scroll-driven entrances */

/*
 * Entrances move things. They do not fade them.
 *
 * The first cut faded text in, and axe caught it: a heading parked halfway
 * through its range renders at 1.02 to 1 against the page, because a contrast
 * check measures what is actually on the screen and does not care that it will
 * be readable in another 200ms. A scroll-driven animation makes that worse
 * than a timed one, because the visitor decides where it stops.
 *
 * So nothing here animates opacity. Position and scale carry the motion, text
 * is at full contrast the whole way, and the effect is the same to look at.
 */
@keyframes rise-in {
    from {
        transform: translateY(16px);
    }
}

@keyframes photo-in {
    from {
        transform: scale(1.035);
    }
}

@supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
        /*
         * The stagger is free. Each row enters the viewport a moment after the
         * one above it, so tying the animation to the element's own progress
         * through the scrollport stages them without a single delay value, and
         * without the whole list replaying every time it is scrolled past.
         *
         * animation-range ends at entry 60% so a row has finished settling
         * well before it reaches the middle of the screen, where it is being
         * read. Both fill modes are required: backwards keeps a row below the
         * fold hidden, forwards keeps one above it shown.
         */
        .gh-route > li,
        .press-list > li,
        .post-list > li,
        .hand > li,
        .ways > li,
        .event-list > li,
        .ch-gallery > a,
        .story-rail > figure,
        .chart__row,
        .numcards > li,
        .gh-table tbody tr {
            animation: rise-in linear both;
            animation-timeline: view();
            animation-range: entry 0% entry 60%;
        }

        /* Bigger blocks move less: a full-width figure sliding 16px reads as
           the layout settling badly rather than as the picture arriving. */
        .band,
        .person,
        .poster,
        .figure-bleed,
        .story-lead__fig,
        .album-card {
            animation: photo-in linear both;
            animation-timeline: view();
            animation-range: entry 0% entry 50%;
        }

        .spec--wide > div {
            animation: rise-in linear both;
            animation-timeline: view();
            animation-range: entry 0% entry 60%;
        }

        .section__head,
        .ch-open,
        .banner,
        .gh-strip__inner > div {
            animation: rise-in linear both;
            animation-timeline: view();
            animation-range: entry 5% entry 45%;
        }
    }
}

/* ------------------------------------------------- the hero and the card */

/*
 * The mark drops onto the photograph, and that is the whole of the load
 * animation now.
 *
 * There used to be a deal here: the card of details came in from the right at
 * a couple of degrees off square and turned flat as it landed. The card was
 * dissolved into the section under it, so there is nothing left to deal, and a
 * deal is the wrong gesture for a heading and a ruled table anyway. What is
 * left of the opening enters on scroll with the rest of the sheet, which is
 * what the entrances at the top of this file are for.
 *
 * The mark fades, and it is the only thing on the site that does. It carries
 * no text, so it cannot leave a heading part way through an opacity ramp
 * failing a contrast check, and it cannot delay the largest contentful paint.
 *
 * 1081px and up. On a phone the mark is the entire first screen, and somebody
 * arriving from a Facebook link should find it already there.
 */
@media (prefers-reduced-motion: no-preference) and (min-width: 1081px) {
    .ch-hero__badge {
        animation: badge-in 680ms var(--ease-emph-decel) 80ms both;
    }
}

/*
 * Lands on the resting tilt the mark carries, not on square. It starts further
 * off than it used to, because at 720px across the drop has a lot more
 * artwork to move and the same 14px reads as a twitch.
 */
@keyframes badge-in {
    from {
        opacity: 0;
        transform: translateY(20px) rotate(-7deg);
    }
}

/*
 * The photograph drifts.
 *
 * Tied to the scroll position rather than to a clock, so it only ever moves
 * because the visitor moved it: the background of this hero is a slideshow,
 * and a picture that pans on its own behind a panel of text is exactly the
 * motion that makes people feel sick. Scrolled, it is depth.
 *
 * The picture is scaled 1.1 first so the 4% of travel comes out of the
 * overhang and never uncovers an edge of the stage. That 10% is also the whole
 * cost of the effect and the reason it is not 20%: the hero photograph is
 * 958px wide and already being stretched across a 1440px window, so every
 * extra percent of scale is spent on softness.
 *
 * scroll(root block) rather than view(): the hero starts at the top of the
 * document, so a view timeline would already be part-way through its range on
 * the first frame and the photograph would load off its own framing.
 */
@supports (animation-timeline: scroll()) {
    @media (prefers-reduced-motion: no-preference) {
        .ch-hero__bg {
            animation: hero-drift linear both;
            animation-timeline: scroll(root block);
            animation-range: 0 100vh;
        }
    }
}

@keyframes hero-drift {
    from {
        transform: scale(1.1) translateY(0);
    }
    to {
        transform: scale(1.1) translateY(4%);
    }
}

/* --------------------------------------------------------- state layers */

/*
 * Press feedback. A button that only changes colour on hover gives a touch
 * visitor nothing at all, because there is no hover on a phone: the first
 * feedback they get is the next page loading. A press that moves the control
 * a pixel is felt.
 */
@media (prefers-reduced-motion: no-preference) {
    .btn,
    .nav-toggle,
    .header-social,
    .lightbox__nav,
    .lightbox__close {
        transition: background-color var(--dur-state) var(--ease-standard),
            color var(--dur-state) var(--ease-standard),
            border-color var(--dur-state) var(--ease-standard),
            transform var(--dur-state) var(--ease-standard),
            box-shadow var(--dur-state) var(--ease-standard);
    }

    .btn:active,
    .nav-toggle:active,
    .header-social:active {
        transform: translateY(1px);
    }

    /* The whole row is the target, so the whole row answers. */
    .post,
    .way,
    .event-row,
    .press-list a,
    .album-card,
    .card-stop {
        transition: transform var(--dur-comp) var(--ease-standard),
            box-shadow var(--dur-comp) var(--ease-standard),
            background-color var(--dur-comp) var(--ease-standard);
    }

    .post:hover,
    .way:hover,
    .event-row:hover {
        transform: translateX(3px);
    }

    .album-card:hover,
    .card-stop:hover {
        transform: translateY(-3px);
    }

    .post:active,
    .way:active,
    .album-card:active,
    .press-list a:active {
        transform: translateY(1px);
    }
}

/*
 * The arrow on a text link leans the way the link goes. It is the smallest
 * thing on this list and the one people notice, because it answers the
 * question the arrow is already asking.
 */
@media (prefers-reduced-motion: no-preference) {
    .textlink .icon,
    .press-list__arrow,
    .btn .icon {
        transition: transform var(--dur-comp) var(--ease-emph-decel);
    }

    .textlink:hover .icon,
    .press-list a:hover .press-list__arrow {
        transform: translateX(4px);
    }

    .btn--donate:hover .icon {
        transform: scale(1.12);
    }
}

/*
 * The nav underline grows from the left rather than appearing whole, so the
 * current page and a hovered one are told apart by more than colour.
 */
@media (prefers-reduced-motion: no-preference) {
    .site-nav__list a {
        position: relative;
        border-bottom-color: transparent;
    }

    .site-nav__list a::after {
        content: "";
        position: absolute;
        left: 0.65rem;
        right: 0.65rem;
        bottom: 0;
        height: 3px;
        background: var(--green);
        transform: scaleX(0);
        transform-origin: left center;
        transition: transform var(--dur-comp) var(--ease-emph-decel);
    }

    .site-nav__list a:hover::after,
    .site-nav__list a:focus-visible::after,
    .site-nav__list a.is-current::after {
        transform: scaleX(1);
    }
}

/* ------------------------------------------------------------- components */

/*
 * The drawer comes down from under the header rather than appearing. 240ms
 * with decelerate easing: long enough to see where it came from, short enough
 * that a second tap to close never feels blocked.
 */
@media (prefers-reduced-motion: no-preference) {
    .mobile-nav:not([hidden]) {
        animation: drawer-in var(--dur-comp) var(--ease-emph-decel) both;
    }

    /* The rows inside arrive just behind the panel, which is what stops a full
       screen of links from landing as one slab. */
    .mobile-nav:not([hidden]) .mobile-nav__list li,
    .mobile-nav:not([hidden]) .mobile-nav__cta > * {
        animation: rise-in 260ms var(--ease-emph-decel) both;
    }

    .mobile-nav__list li:nth-child(1) { animation-delay: 40ms; }
    .mobile-nav__list li:nth-child(2) { animation-delay: 60ms; }
    .mobile-nav__list li:nth-child(3) { animation-delay: 80ms; }
    .mobile-nav__list li:nth-child(4) { animation-delay: 100ms; }
    .mobile-nav__list li:nth-child(5) { animation-delay: 120ms; }
    .mobile-nav__list li:nth-child(6) { animation-delay: 140ms; }
    .mobile-nav__list li:nth-child(7) { animation-delay: 160ms; }
    .mobile-nav__list li:nth-child(8) { animation-delay: 180ms; }
    .mobile-nav__list li:nth-child(9) { animation-delay: 200ms; }
    .mobile-nav__list li:nth-child(n + 10) { animation-delay: 220ms; }
    .mobile-nav__cta > * { animation-delay: 240ms; }
}

@keyframes drawer-in {
    from {
        opacity: 0;
        transform: translateY(-10px);
    }
}

/*
 * The FAQ marker. details/summary has no transition of its own between open
 * and closed, so the chevron is the only thing that can show the state
 * changing.
 */
.faq summary::after {
    content: "";
    flex: none;
    width: 0.62em;
    height: 0.62em;
    border-right: 2px solid var(--green-ink);
    border-bottom: 2px solid var(--green-ink);
    transform: rotate(45deg) translate(-0.1em, -0.1em);
    transform-origin: center;
}

@media (prefers-reduced-motion: no-preference) {
    .faq summary::after {
        transition: transform var(--dur-comp) var(--ease-emph-decel);
    }
}

.faq details[open] > summary::after {
    transform: rotate(-135deg) translate(-0.16em, -0.16em);
}

@media (prefers-reduced-motion: no-preference) {
    .faq details[open] > .faq__body {
        animation: rise-in var(--dur-comp) var(--ease-emph-decel) both;
    }
}

/*
 * The countdown. Only the seconds cell is animated, and only by a hair: a
 * digit that jumps every second in the corner of the eye is a distraction on
 * a page somebody is reading an address off.
 */
@media (prefers-reduced-motion: no-preference) {
    .numcards__num[data-unit="seconds"] {
        animation: tick 1s steps(1, end) infinite;
    }
}

@keyframes tick {
    0% {
        opacity: 0.72;
    }
    8% {
        opacity: 1;
    }
}

/* ---------------------------------------------------------- back to top */

/*
 * Appears once the visitor is far enough down that scrolling back is real
 * work, and gets out of the way otherwise. It sits in the bottom right, where
 * the boxed sheet has a margin on a desktop and where a right thumb reaches on
 * a phone, and it is 48px so it is a comfortable target either way.
 *
 * It floats over the content, which is only acceptable because it is small,
 * temporary, and never over the start of anything: at the top of the page,
 * where the ride details are, it is not on screen at all.
 */
.to-top {
    position: fixed;
    right: clamp(0.85rem, 2vw, 1.5rem);
    bottom: clamp(0.85rem, 2vw, 1.5rem);
    z-index: var(--z-sticky);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 48px;
    height: 48px;
    padding: 0;
    border: 2px solid var(--ink);
    border-radius: 50%;
    background: var(--paper);
    color: var(--ink);
    cursor: pointer;
    box-shadow: 0 2px 6px rgba(20, 24, 29, 0.12), 0 10px 26px rgba(20, 24, 29, 0.18);
}

.to-top:hover {
    background: var(--ink);
    color: var(--paper);
}

/* Out of the layout and out of the tab order while it is away, so a keyboard
   visitor never lands on an invisible control. */
.to-top[hidden] {
    display: none;
}

@media (prefers-reduced-motion: no-preference) {
    .to-top {
        transition: opacity var(--dur-comp) var(--ease-standard),
            transform var(--dur-comp) var(--ease-emph-decel),
            background-color var(--dur-state) var(--ease-standard),
            color var(--dur-state) var(--ease-standard);
    }

    .to-top:not(.is-in) {
        opacity: 0;
        transform: translateY(12px) scale(0.9);
    }

    .to-top.is-in {
        opacity: 1;
        transform: none;
    }

    .to-top:active {
        transform: translateY(1px);
    }

    .to-top .icon {
        transition: transform var(--dur-comp) var(--ease-emph-decel);
    }

    .to-top:hover .icon {
        transform: translateY(-2px);
    }
}

/* Focus rings arrive rather than snap, which makes keyboard travel readable
   across a long page of links. */
@media (prefers-reduced-motion: no-preference) {
    a,
    button,
    input,
    select,
    textarea,
    summary,
    [tabindex] {
        transition: outline-offset var(--dur-state) var(--ease-standard);
    }

    :focus-visible {
        outline-offset: 3px;
    }
}
