/* ==========================================================================
   animations.css — EditingTools.pro
   Every animation on the site lives here, and every one of them is wrapped in
   @media (prefers-reduced-motion: no-preference). The default state of the
   page — outside that query — is the FINAL state: fully visible, no
   transforms. If a user prefers reduced motion they simply get the finished
   page, never a half-faded one.

   Only transform and opacity are animated. Never width/height/top/left.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Default (reduced motion, and pre-JS): everything is already in place.
   -------------------------------------------------------------------------- */
[data-reveal],
[data-hero-step] {
  opacity: 1;
  transform: none;
}
.hero__grid { transform: none; }
.site-footer__mesh { transform: none; }

@media (prefers-reduced-motion: no-preference) {

  /* ------------------------------------------------------------------------
     Keyframes
     ------------------------------------------------------------------------ */
  @keyframes rise-in {
    from { opacity: 0; transform: translate3d(0, 12px, 0); }
    to   { opacity: 1; transform: none; }
  }
  @keyframes fade-in {
    from { opacity: 0; }
    to   { opacity: 1; }
  }
  @keyframes word-rise {
    from { opacity: 0; transform: translate3d(0, 0.5em, 0); }
    to   { opacity: 1; transform: none; }
  }
  @keyframes zone-bloom {
    from { opacity: 0; transform: scale(0.97); box-shadow: 0 0 0 rgb(0 0 0 / 0); }
    to   { opacity: 1; transform: none; box-shadow: var(--elev); }
  }
  @keyframes draw-border {
    from { stroke-dashoffset: var(--dash-len, 2000); }
    to   { stroke-dashoffset: 0; }
  }
  /* Calibration grid drifts exactly one 40px cell over 30s, so the loop is
     seamless. */
  @keyframes grid-drift {
    from { transform: translate3d(0, 0, 0); }
    to   { transform: translate3d(-40px, -40px, 0); }
  }
  @keyframes mesh-drift {
    0%   { transform: translate3d(0, 0, 0) scale(1); }
    50%  { transform: translate3d(3%, -2%, 0) scale(1.06); }
    100% { transform: translate3d(0, 0, 0) scale(1); }
  }
  @keyframes shimmer {
    from { transform: translateX(-100%); }
    to   { transform: translateX(100%); }
  }

  /* ------------------------------------------------------------------------
     Scroll reveal — once, 12px rise + fade. main.js adds .is-in when the
     element intersects, then unobserves it.

     The hidden start state is gated on [data-reveal-ready], which main.js
     sets ONLY after it has confirmed IntersectionObserver exists and it is
     about to start observing. If the script fails, is blocked, or never runs,
     the attribute is never set and the content is simply visible — no
     mechanism on this site may be the reason a paragraph cannot be read.
     ------------------------------------------------------------------------ */
  :root[data-reveal-ready] [data-reveal] {
    opacity: 0;
    transform: translate3d(0, 12px, 0);
    transition: opacity 500ms var(--ease-out), transform 500ms var(--ease-out);
    transition-delay: var(--reveal-delay, 0ms);
    will-change: opacity, transform;
  }
  :root[data-reveal-ready] [data-reveal].is-in { opacity: 1; transform: none; }
  /* Stagger children of a revealed group without per-element JS. */
  [data-reveal-group] > * { --reveal-delay: 0ms; }
  [data-reveal-group] > *:nth-child(2) { --reveal-delay: 60ms; }
  [data-reveal-group] > *:nth-child(3) { --reveal-delay: 120ms; }
  [data-reveal-group] > *:nth-child(4) { --reveal-delay: 180ms; }
  [data-reveal-group] > *:nth-child(5) { --reveal-delay: 240ms; }
  [data-reveal-group] > *:nth-child(6) { --reveal-delay: 300ms; }
  [data-reveal-group] > *:nth-child(7) { --reveal-delay: 360ms; }
  [data-reveal-group] > *:nth-child(8) { --reveal-delay: 420ms; }

  /* ------------------------------------------------------------------------
     Hero load sequence — orchestrated, not scattered.
       0ms    hero container fades in
       60ms   H1 words rise, 40ms stagger per word
       180ms  subhead fades
       240ms  the search field rises — the first thing anyone can act on, so
              it lands before the ornament rather than after it
       260ms  drop zone scales from 0.97 with a shadow bloom
       380ms  the drop zone border draws itself around the perimeter
       460ms  quick-action chips fade in, 40ms stagger
       520ms  trust row fades
     ------------------------------------------------------------------------ */
  .hero.is-ready { animation: fade-in 300ms var(--ease-out) both; }

  .hero.is-ready .word {
    display: inline-block;
    animation: word-rise 480ms var(--ease-out) both;
    animation-delay: calc(60ms + var(--i, 0) * 40ms);
  }
  .hero.is-ready [data-hero-step="sub"]   { animation: rise-in 420ms var(--ease-out) 180ms both; }
  .hero.is-ready [data-hero-step="search"]{ animation: rise-in 420ms var(--ease-out) 240ms both; }
  .hero.is-ready [data-hero-step="zone"]  { animation: zone-bloom 520ms var(--ease-out) 260ms both; }
  .hero.is-ready [data-hero-step="chips"] > * {
    animation: rise-in 400ms var(--ease-out) both;
    animation-delay: calc(460ms + var(--i, 0) * 40ms);
  }
  .hero.is-ready [data-hero-step="trust"] { animation: fade-in 420ms var(--ease-out) 520ms both; }

  /* Dashed perimeter draws itself. --dash-len is measured in JS from the
     rect's real geometry so it works at any container size. */
  .hero.is-ready .dropzone__border rect {
    stroke-dasharray: var(--dash-len, 2000);
    animation: draw-border 700ms var(--ease-out) 380ms both;
  }
  /* After the draw completes JS swaps in .is-drawn to restore the dashes. */
  .dropzone__border rect.is-drawn {
    stroke-dasharray: 8 6;
    animation: none;
  }

  /* ------------------------------------------------------------------------
     Ambient loops
     ------------------------------------------------------------------------ */
  .hero__grid { animation: grid-drift 30s linear infinite; }
  .site-footer__mesh { animation: mesh-drift 20s ease-in-out infinite; }

  /* The drop zone breathes while it is waiting.

     Half a percent of scale over four seconds. That is deliberately below the
     level at which anyone would describe the page as animated — nobody looks
     at this and thinks "that's moving" — and above the level at which the eye
     ignores it. Peripheral vision is far more sensitive to motion than to
     colour or contrast, which is why a barely-moving element wins attention
     that a merely brighter one does not.

     It stops the moment there is something better to look at: during the hero
     entrance (so it cannot fight the bloom animation), while the pointer is
     over it (hover has its own feedback), and while a file is being dragged
     in (is-over says something specific and should not be diluted). */
  @keyframes zone-breathe {
    0%, 100% { transform: scale(1); }
    50%      { transform: scale(1.005); }
  }
  .dropzone {
    animation: zone-breathe 4s var(--ease-out) 1.4s infinite;
  }
  .hero.is-ready [data-hero-step="zone"] .dropzone { animation-delay: 2.2s; }
  .dropzone:hover,
  .dropzone.is-over,
  .dropzone:focus-within { animation: none; }

  /* ------------------------------------------------------------------------
     Theme crossfade.

     Flipping data-theme repaints every colour on the page in one frame, which
     reads as a flash rather than as a change. A short transition on the four
     properties that actually differ turns it into a wipe.

     Scoped to [data-theme-shift], which theme.js sets for 240ms around the
     toggle and then removes. It is NOT a permanent transition on * — that
     would put a 200ms lag on every hover state on the site, and the reason it
     looks fine here is precisely that it is not running the rest of the time.
     ------------------------------------------------------------------------ */
  :root[data-theme-shift] * {
    transition: background-color 220ms var(--ease-out),
                border-color 220ms var(--ease-out),
                color 220ms var(--ease-out),
                fill 220ms var(--ease-out) !important;
  }

  /* ------------------------------------------------------------------------
     Loading skeleton — shown while a heavy library lazy-loads (pdf-lib,
     pdf.js, tesseract.js, Chart.js). Defined now so every phase uses one.
     ------------------------------------------------------------------------ */
  .skeleton { position: relative; overflow: hidden; background: var(--surface-inset); border-radius: var(--r-input); }
  .skeleton::after {
    content: ""; position: absolute; inset: 0;
    background: linear-gradient(90deg, transparent, var(--hairline-faint), transparent);
    animation: shimmer 1.4s linear infinite;
  }
}

/* --------------------------------------------------------------------------
   Reduced motion: kill everything, including anything a component author
   adds later. countUp() in utils.js also checks this and jumps straight to
   the final value.
   -------------------------------------------------------------------------- */
/* Printing must never lose content to an animation that has not run. */
@media print {
  [data-reveal], [data-hero-step], .word {
    opacity: 1 !important;
    transform: none !important;
    animation: none !important;
  }
  .site-header, .site-footer__mesh, .hero__grid, .slot { display: none !important; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  .site-footer__mesh { display: none; }
}
