/* =============================================================================
   animations.css - THE FEW ANIMATIONS THAT ARE NOT IN main.css
   =============================================================================

   WHAT THIS FILE IS
     A small, deliberately short addition to site/assets/css/main.css. It is
     loaded AFTER main.min.css by templates/_layout.html, so where a selector
     here has the same weight as one there, this file wins. That ordering is
     load-bearing for the two press states at the end and is stated again
     beside them.

   WHY IT IS A SEPARATE FILE
     main.css already owns the site's motion - section 16 (the entrance rule
     and the brand gradient sweep) and section 17 (reduced motion, forced
     colours, print). Three agents were editing this repository at once, so a
     new file was the only way to add motion without racing another edit to a
     155 KB stylesheet. Nothing here contradicts main.css; it fills three gaps
     in it, and the gaps are named below.

     If the two files are ever merged, this whole file belongs at the end of
     main.css section 16, and the reduced-motion block at the end of section 17.

   THE THREE GAPS IT FILLS, AND NOTHING ELSE
     1. main.css has an entrance animation, .motion-rise, and MEASURED IN THE
        BUILT PAGES IT IS DEAD CODE: `grep -c motion-rise site/index.html` is 0
        and no page generator emits the class. The entrance the brief asks for
        therefore did not exist on the wire. It does now, driven by scroll
        position rather than by a class, so no markup change is needed and no
        script is involved.
     2. .service-card is an <a> (verified in the built page: tagName "A",
        tabIndex 0). A mouse user gets a 4px lift on hover; a keyboard user got
        nothing but the focus ring. That is a parity gap, not a decoration.
     3. .nav-cta and .btn-gradient-border both LIFT on hover and have no
        :active state at all, so a press does nothing. Every other control on
        the site has one (.btn--primary, .btn--secondary, .btn-ghost,
        .qty-chip, .qty-btn all handle :active in main.css).

   THE RULES THIS FILE OBEYS - ALL FOUR, EVERY TIME
     a. TRANSFORM ONLY. Not one declaration in this file animates or
        transitions anything except `transform`. No opacity (see the
        measurement below), no geometry, no colour, no filter. Transform is
        composited, so none of this runs layout on a scroll frame.
     b. NOTHING HAS A HIDDEN RESTING STATE. There is no opacity:0,
        visibility:hidden, display:none, zero size or off-screen transform
        anywhere in this file, in any state - not in a rule, not in a keyframe,
        not under any media query. PRD Rule 9 is the reason main.css section 16
        carries a 60-line banner about this; read it before adding anything.
     c. NO SCRIPT GATES ANY OF IT. No class a script adds appears in any
        selector here. With JavaScript switched off this file behaves
        identically.
     d. EVERY ANIMATION IS OFF UNDER prefers-reduced-motion: reduce, and the
        way it is switched off never hides anything. See the last block.

   NO COLOUR IS INTRODUCED HERE. There is not a single colour value in this
   file, so it cannot change any contrast ratio on the site. The one place
   contrast DID drive a decision is the opacity question below, and that is
   settled with measured numbers rather than judgement.

   ---------------------------------------------------------------------------
   WHAT THE BUZZOID RE-SKIN CHANGED IN THIS FILE (2026-08-20)
   ---------------------------------------------------------------------------
     1. The opacity working in section 1 was RE-MEASURED, because both tokens
        it names moved. Same conclusion, new numbers - see there.
     2. Section 3 gained .cta-gradient, the new signature button from PRD 4.4.
        It had the same gap .nav-cta had: a lift on hover and no press at all.
     3. Section 2 gained the two new card-shaped components, .pack-tile and
        .tier-compare-card, for the same keyboard-parity reason .service-card
        is there.
     4. Rule (a) below still holds: every declaration added is a `transform`.
        The gradient SHIFT on CTA hover that PRD 4.7 asks for is NOT here - it
        transitions background-position, which is a paint property, so it lives
        in main.css section 16 beside the arrival sweep that already owns that
        property. Putting it here would have broken this file's one hard rule
        for no reason other than proximity.
   ============================================================================= */


/* =============================================================================
   1. THE SECTION ENTRANCE - A RISE, AND DELIBERATELY NOT A FADE
   =============================================================================

   WHY THERE IS NO FADE, WITH THE NUMBERS THAT DECIDED IT
     The brief asked for "fade + rise". The fade is not here, and this is the
     measurement that removed it.

     A scroll-driven animation is not a one-shot. Its progress IS the scroll
     position, so the reader parks it wherever they stop scrolling. A partial
     frame is therefore a STATIC, TESTABLE state, not a transient one - which
     means every frame of it has to pass the contrast bar, not just the end.

     RE-MEASURED FOR THE BUZZOID PALETTE, 2026-08-20. Both tokens in the
     original working moved in the re-skin - --text-dim from #64748B to
     #5F6875, and --bg-alt from #F9FAFB to the lavender-tinted #F7F8FE - so the
     numbers below were recomputed rather than carried over. THE CONCLUSION IS
     UNCHANGED, which is worth stating plainly: this rule survived a full
     repaint of the palette because the argument is about the SHAPE of the
     maths, not about two particular colours.

     Measured from the tokens in main.css, using the WCAG relative-luminance
     formula, the tightest text pair this site ships is:

         --text-dim   #5F6875  on  --surface-hi  #E9EBF3  =  4.74:1

     That is 0.24 above the 4.5:1 minimum. Compositing that same pair at a
     reduced opacity over its own band gives:

         opacity 1.00 -> 4.74:1   passes
         opacity 0.95 -> 4.28:1   FAILS
         opacity 0.90 -> 3.91:1   FAILS
         opacity 0.80 -> 3.25:1   FAILS
         opacity 0.55 -> 2.13:1   FAILS

     There is no usable floor - not even 95%. ANY opacity below 1 puts this
     site's smallest text under 4.5:1, and a reader who stops mid-scroll holds
     it there. So the entrance moves and does not fade, and this file contains
     no opacity at all.

     A rise has no such problem. A section sitting 8px low is not less legible
     than a section sitting in place; transform does not affect layout, so it
     cannot overlap, reflow or clip anything either.

   WHY IT IS SAFE EVEN IF EVERYTHING GOES WRONG (PRD Rule 9)
     The failure this codebase is shaped around is an element parked at
     opacity:0 waiting for a script. Walk every state of the rule below:

       - browser has no scroll-driven animations  -> @supports is false, the
         rule never applies, sections render normally;
       - the animation has not started            -> fill-mode:none, so the
         base style applies: no transform;
       - it is halfway through                    -> the section is drawn up to
         8px low. Fully readable, fully clickable;
       - it finished, or was interrupted          -> fill-mode:none again, base
         style, no transform;
       - CSS is disabled entirely                 -> no animation exists.

     Not one of those states hides, dims or moves anything out of reach. That
     is the whole safety argument, and it holds because the only property
     touched is transform.

   WHY .section, AND WHY THE :not(:has()) GUARD IS NOT PARANOIA
     Scanned across the built pages, the order form lives inside a plain
     <section class="section"> on two of them:

         site/buy-followers-paypal/index.html   .section > form.order-card
         site/cheap-followers-uk/index.html     .section > form.order-card

     (elsewhere it sits in .hero or .order-section, neither of which is matched
     here). A transform on an ancestor is also the containing block for
     anything absolutely positioned inside it, and .order-card--sticky is
     `position: sticky` from 64rem. Nothing in the list above could hide the
     form - but the money element is the one place on this site where "it
     probably behaves" is not good enough, so it is excluded outright and the
     reasoning is written down instead of being rediscovered later.

     :has() is also a second safety net by accident: a browser that does not
     support it treats the whole selector as invalid and drops the rule, which
     fails to "no animation" rather than to "animation on the order form".

   THE RANGE, AND WHY IT IS A LENGTH RATHER THAN A PERCENTAGE
     `entry 0%` is the moment the section's top edge reaches the bottom of the
     viewport. `entry 18rem` ends the movement 288px of scrolling later.

     A percentage would be a share of the section's own height, and the
     sections here are tall - measured on the built home page: #services
     1805px, #faq 1090px, #reviews 951px, #trusted-stats 536px, against a
     730px viewport. `entry 100%` on the 1805px one would still be settling
     long after it filled the screen and the reader had started reading it. A
     fixed 288px settles every section at the same speed no matter how tall it
     is, which is also what makes the page feel consistent rather than random.

   MEASURED, NOT ASSUMED
     Chrome 148 reports, via CSS.supports():
         animation-timeline: view()          true
         animation-duration: auto            true
         animation-range: entry 0% entry 18rem   true
         selector(.a:not(:has(.b)))          true
     and on the built home page at localhost:4173 this rule attaches exactly
     one animation to each of the four .section elements, playState "running",
     range name "entry", with the order-card sections correctly unmatched.

     NOT VERIFIED: the movement was never seen with human eyes. The browser
     pane was not displayed during this work, so the page composited no frames
     and the scroll position could not be driven. Worth a look on a real
     screen. It cannot fail unsafely - see the state walk above - but "it is
     safe" and "it looks right" are different claims and only the first is
     proven.
   ============================================================================= */

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .section:not(:has(.order-card)) {
      animation-name: section-rise;

      /* `auto` is what maps the animation onto the whole of the range named by
         animation-range. A time value here would be ignored for a
         progress-based timeline in some engines and honoured in others, which
         is exactly the kind of difference nobody notices until it ships. */
      animation-duration: auto;
      animation-timing-function: var(--ease-out);
      animation-iteration-count: 1;

      /* MUST stay none, for the same reason main.css section 16 says so about
         .motion-rise: "backwards" or "both" pins the element at a keyframe
         whenever the animation is not running, and a resting state that comes
         from a keyframe is the PRD Rule 9 bug wearing a new hat. With `none`
         the resting state is the section's own style and nothing else. */
      animation-fill-mode: none;

      /* Progress comes from where this section sits in the viewport, not from
         a clock and not from a script. */
      animation-timeline: view();
      animation-range: entry 0% entry 18rem;
    }
  }
}

/* 0.5rem is 8px. Small enough that it reads as the section settling into
   place rather than as an element flying in, which is the point: the eye
   should end up on the page, not on the movement. */
@keyframes section-rise {
  from { transform: translateY(0.5rem); }
  to   { transform: none; }
}


/* =============================================================================
   2. THE KEYBOARD GETS THE SAME LIFT THE MOUSE ALREADY HAD
   =============================================================================

   .service-card is an <a> - measured in the built page, tagName "A" with
   tabIndex 0 - so it is reached by Tab. main.css lifts it 4px on :hover and
   leaves :focus-visible with only the focus ring. The ring says "you are
   here"; the lift says "this is one object you can press". A keyboard user
   was getting half the affordance for no reason.

   -4px is the same distance main.css uses for .service-card:hover, on purpose:
   two different distances for the same object would read as two different
   controls. If that value changes there, change it here in the same commit.

   :focus-visible rather than :focus, matching main.css section 9 - a mouse
   click must not leave a card standing up after the pointer has gone.

   The transition is already on .service-card in main.css (transform,
   border-color, background-color, box-shadow - confirmed in the built page's
   computed style), so nothing new is declared here. This is a transform and
   nothing else.
   ============================================================================= */

@media (prefers-reduced-motion: no-preference) {
  .service-card:focus-visible { transform: translateY(-4px); }

  /* THE TWO NEW CARD-SHAPED COMPONENTS FROM THE RE-SKIN, same argument.

     .pack-tile is the quantity tile in the order widget - the densest set of
     touch targets on the site and the one a buyer actually chooses with. It is
     a <button> (or an <a> on a no-script page), so it is reached by Tab.

     -2px rather than the -4px .service-card uses, and that is deliberate
     rather than sloppy: eight tiles sit in a grid 8px apart, and a 4px lift on
     one of them visibly breaks the row it is in. A service card is alone in
     its column and can afford the bigger move.

     THE LIFT IS NOT THE ONLY SIGNAL ON THESE, WHICH IS WHY IT CAN BE SMALL.
     The focus ring from main.css section 7 is doing the work; this adds the
     "one object you can press" affordance the mouse already gets. */
  .pack-tile:hover,
  .pack-tile:focus-visible { transform: translateY(-2px); }

  .tier-compare-card:hover { transform: translateY(-4px); }
}


/* =============================================================================
   3. A PRESS THAT CAN BE FELT, ON THE TWO CONTROLS THAT HAD NONE
   =============================================================================

   main.css gives every other control a press: .btn--primary and .btn-primary
   drop back to 0 from their hover lift, .btn--secondary, .btn-ghost, .qty-chip
   and .qty-btn all go to translateY(1px). Two were missed:

       .nav-cta              lifts -1px on hover, no :active at all
       .btn-gradient-border  lifts -2px on hover, no :active at all

   .nav-cta is the call to action in the header of all 23 pages, so this is the
   control a visitor is most likely to press and the one least likely to
   confirm it was pressed. 1px down, matching what main.css already uses
   everywhere else - the point of a press state is that it is felt rather than
   seen.

   ORDER MATTERS HERE, AND IT IS THE ONLY PLACE IN THIS FILE THAT IT DOES.
   `.nav-cta:active` and `.nav-cta:hover` have identical specificity (0,2,0),
   and on a mouse both match at once during a press. This file is linked after
   main.min.css, so :active wins and the button moves down instead of staying
   lifted. Move the <link> in templates/_layout.html above main.min.css and
   this stops working - silently, because nothing errors.

   Both elements already transition transform in main.css (confirmed in the
   built page's computed style: .nav-cta "transform, box-shadow";
   .btn-gradient-border "background-color, border-color, color, box-shadow,
   transform"), so again nothing new is declared.

   .btn--quiet is deliberately NOT here. It has no :active either, but it does
   not appear in any built page - styling something that does not exist is how
   a stylesheet grows rules nobody can test.
   ============================================================================= */

@media (prefers-reduced-motion: no-preference) {
  .nav-cta:active,
  .btn-gradient-border:active,
  /* .cta-gradient is the re-skin's signature button (PRD 4.4) and it arrived
     with exactly the same gap: main.css gives it a hover shadow and a gradient
     shift, and no :active at all. It is the control a buyer presses to spend
     money, so it is the last one that should feel dead under the thumb.
     1px down, matching every other control on the site.

     THE SAME SOURCE-ORDER DEPENDENCY APPLIES. `.cta-gradient:active` and
     `.cta-gradient:hover` weigh the same (0,2,0) and both match during a mouse
     press; :active only wins because this file is parsed after main.min.css.
     See the note above - moving the <link> breaks this silently. */
  .cta-gradient:active { transform: translateY(1px); }
}


/* =============================================================================
   4. REDUCED MOTION - AND WHY SECTION 17 IS NOT ENOUGH ON ITS OWN
   =============================================================================

   Everything above is already inside @media (prefers-reduced-motion:
   no-preference), which is the strongest form of "off": `no-preference` and
   `reduce` are mutually exclusive, so a visitor who asked for less motion
   never matches those rules in the first place. This block is the second net,
   for the same reason main.css keeps both.

   AND IT IS NOT REDUNDANT, BECAUSE OF ONE REAL HOLE. main.css section 17
   switches motion off across the whole site with:

       * { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }

   That works for every animation on the site today, because they are all
   driven by a clock. It does NOT work for the section entrance above: its
   progress comes from the scroll position, so its duration is `auto` and
   setting a duration of 0.01ms changes nothing at all. Cancelling that
   animation therefore has to be done by name, and it is done here.

   NOTHING IN THIS BLOCK HIDES ANYTHING. Every declaration either stops an
   animation or returns a transform to none. Reduced motion means the content
   arrives at once and complete - never that there is less of it. A block here
   that hid an element would be caught by finding F14's test, and it would
   deserve to be.
   ============================================================================= */

@media (prefers-reduced-motion: reduce) {
  /* Named, not duration-limited - see the note above. With no animation-name
     there is no animation, whatever the timeline says; animation-timeline is
     reset alongside it so nothing is left half-configured for the next person
     who adds a rule here. */
  .section:not(:has(.order-card)) {
    animation-name: none;
    animation-timeline: auto;
  }

  /* Every feedback transform in this file. The focus ring, the hover colours
     and the :active colours all stay exactly as they are - only the movement
     goes. Nothing in this list hides anything: each declaration returns a
     transform to `none`, which is the element's own resting state. */
  .service-card:focus-visible,
  .pack-tile:hover,
  .pack-tile:focus-visible,
  .tier-compare-card:hover,
  .nav-cta:active,
  .btn-gradient-border:active,
  .cta-gradient:active { transform: none; }
}
