/* ============================================================================
   Grosso University v2 - icon animation layer
   ----------------------------------------------------------------------------
   ONE stylesheet for the whole 30-icon set. Pairs with src/runtime/icons.js.

   THE INVARIANT THIS FILE IS BUILT AROUND, and the reason it looks the way it
   does: an icon is `<svg class="gu-icon"><use href="#i-name"></use></svg>`, and
   the referenced <symbol> lives in a SHADOW TREE. No selector in this file can
   reach the paths inside it - `.gu-icon path` matches nothing. The ONLY channel
   into that tree is CSS INHERITANCE, and in SVG the inherited properties that
   matter are `stroke`, `fill`, `stroke-width`, `stroke-dasharray` and
   `stroke-dashoffset`.

   So: whole-element `transform`/`opacity` for movement, INHERITED
   `stroke-dashoffset` for the draw-on. There is no third option short of
   inlining 30 copies of every glyph on every page, which is what the sprite
   exists to avoid.

   THREE ANIMATIONS, all of them decoration:
     1. hover/focus  - :hover AND :focus-visible on the icon or its control
     2. in-view      - CSS view() timeline where supported, WAAPI + IO otherwise
     3. state/active - a loop on [data-state="active"], for live things

   THE HARD RULES, enforced at the bottom of this file:
     - prefers-reduced-motion: reduce turns EVERY one of them off, and pins the
       icon at its final state. Not slower. Off.
     - Only transform, opacity and stroke-* are animated, so nothing here can
       move a neighbor. No width, height, margin, or inset is touched.
     - The icon is complete before any animation runs and stays complete if none
       ever does. With CSS off it is a plain SVG; with JS off it is a plain SVG.
   ============================================================================ */

/* ---------------------------------------------------------------------------
   1. The element
   ---------------------------------------------------------------------------
   Sized by --gu-icon-size so a caller sets ONE value and never touches width or
   height separately - two properties that can disagree are two properties that
   will. Default 1em ties the icon to the text it sits beside.

   The declarations below ARE the final state. Every animation in this file
   starts somewhere else and ends here, so an interrupted, blocked, cancelled or
   never-started animation leaves a correct icon.                             */

.gu-icon {
  --gu-icon-size: 1em;
  --gu-icon-dash: 120;      /* see the note on gu-draw before changing this */

  display: inline-block;
  inline-size: var(--gu-icon-size);
  block-size: var(--gu-icon-size);
  vertical-align: -0.125em;
  flex: none;

  color: inherit;           /* the symbol's stroke="currentColor" resolves here */
  opacity: 1;
  transform: none;
  stroke-dasharray: none;
  stroke-dashoffset: 0;

  transform-origin: 50% 50%;
  transition: transform var(--gu-dur-base, 240ms) var(--gu-ease-out, cubic-bezier(0.16, 1, 0.3, 1));
}

/* A control that wraps an icon opts its icon into hover/focus by carrying this
   class - or by being a real link/button, which is the common case. */
.gu-ico-host { display: inline-flex; align-items: center; gap: 0.5em; }

/* The armed (pre-reveal) state for the WAAPI path in section 4. src/runtime/icons.js
   adds this class; the declaration lives here so the reduced-motion block at the
   bottom can neutralize it in one place.

   IT IS DECLARED HERE, ABOVE THE HOVER RULES, DELIBERATELY. Several hover
   variants are single-class selectors of the same specificity as this one, so
   whichever came LAST in the file would win - and with this rule last, hovering
   an armed icon painted the armed transform instead of the hover transform. It
   only shows up on an icon that is armed and interactive at the same time
   (keyboard focus reaching a below-fold link before its reveal has run), which
   is exactly the combination that is easy to never test. Source order is the
   fix; raising specificity on seven hover rules to beat one pre-state is not. */
.gu-icon.is-armed {
  opacity: 0;
  transform: translateY(5px) scale(0.92);
  stroke-dasharray: var(--gu-icon-dash);
  stroke-dashoffset: var(--gu-icon-dash);
}

/* ---------------------------------------------------------------------------
   2. Keyframes
   ---------------------------------------------------------------------------
   Every one of these ends on the base state above, and NONE of them uses
   `forwards`. A finished animation therefore hands the element back to the
   cascade rather than pinning a computed value on it - which is what lets the
   reduced-motion block below win by simply existing.                         */

/* The draw-on. One dash pattern for every subpath, because `<use>` gives no way
   to measure them individually - `getTotalLength()` cannot cross the shadow
   boundary. --gu-icon-dash: 120 is longer than the longest subpath in the set
   (the certified-closer shield, ~58 user units), so at offset 0 the pattern is
   one dash covering the whole path and the icon is indistinguishable from
   dasharray:none. That equivalence is what makes it safe to drop the dash
   entirely when the animation ends. An icon added later with a subpath longer
   than 120 would show a gap at rest - raise the value, do not remove it. */
@keyframes gu-draw {
  from { stroke-dasharray: var(--gu-icon-dash); stroke-dashoffset: var(--gu-icon-dash); }
  to   { stroke-dasharray: var(--gu-icon-dash); stroke-dashoffset: 0; }
}

@keyframes gu-pulse {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.13); }
}

@keyframes gu-shake {
  0%, 100%      { transform: rotate(0deg); }
  20%, 60%      { transform: rotate(-11deg); }
  40%, 80%      { transform: rotate(11deg); }
}

@keyframes gu-spin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}

/* The in-view reveal. Movement is 5px of translate and a hair of scale - large
   enough to read as an arrival, small enough that thirty of them firing down a
   page is not a disco. */
@keyframes gu-reveal {
  from {
    opacity: 0;
    transform: translateY(5px) scale(0.92);
    stroke-dasharray: var(--gu-icon-dash);
    stroke-dashoffset: var(--gu-icon-dash);
  }
  to {
    opacity: 1;
    transform: none;
    stroke-dasharray: var(--gu-icon-dash);
    stroke-dashoffset: 0;
  }
}

/* ---------------------------------------------------------------------------
   3. Animation 1 - hover and focus
   ---------------------------------------------------------------------------
   :focus-visible carries the same weight as :hover here, deliberately. A
   keyboard user tabbing onto a link gets the identical affordance a mouse user
   gets; an icon that only reacts to a pointer is an icon that tells half the
   audience nothing.

   Three trigger surfaces:
     - the icon itself (:hover, and :focus-visible when it is given a tabindex)
     - the interactive ancestor that owns it, which is the real case
     - .demo-force-hover, so the contact sheet can hold every icon in its hover
       state for a screenshot. Nothing on the site sets that class.

   :where() keeps the ancestor selector at zero specificity so a page can
   override any of this with a single class.                                  */

/* -- transform variants: one transition, no keyframes, nothing to retrigger -- */
.gu-icon[data-hover="lift"]:is(:hover, :focus-visible),
:where(a, button, summary, label, [role="button"], .gu-ico-host):is(:hover, :focus-visible) .gu-icon[data-hover="lift"],
.demo-force-hover .gu-icon[data-hover="lift"] { transform: translateY(-2.5px); }

.gu-icon[data-hover="nudge"]:is(:hover, :focus-visible),
:where(a, button, summary, label, [role="button"], .gu-ico-host):is(:hover, :focus-visible) .gu-icon[data-hover="nudge"],
.demo-force-hover .gu-icon[data-hover="nudge"] { transform: translateX(2px) scale(1.06); }

.gu-icon[data-hover="pop"]:is(:hover, :focus-visible),
:where(a, button, summary, label, [role="button"], .gu-ico-host):is(:hover, :focus-visible) .gu-icon[data-hover="pop"],
.demo-force-hover .gu-icon[data-hover="pop"] { transform: scale(1.16); }

.gu-icon[data-hover="spin"]:is(:hover, :focus-visible),
:where(a, button, summary, label, [role="button"], .gu-ico-host):is(:hover, :focus-visible) .gu-icon[data-hover="spin"],
.demo-force-hover .gu-icon[data-hover="spin"] { transform: rotate(30deg); }

/* -- keyframe variants -- */
.gu-icon[data-hover="draw"]:is(:hover, :focus-visible),
:where(a, button, summary, label, [role="button"], .gu-ico-host):is(:hover, :focus-visible) .gu-icon[data-hover="draw"],
.demo-force-hover .gu-icon[data-hover="draw"] {
  animation: gu-draw var(--gu-dur-slow, 420ms) var(--gu-ease-out, cubic-bezier(0.16, 1, 0.3, 1)) 1;
}

.gu-icon[data-hover="pulse"]:is(:hover, :focus-visible),
:where(a, button, summary, label, [role="button"], .gu-ico-host):is(:hover, :focus-visible) .gu-icon[data-hover="pulse"],
.demo-force-hover .gu-icon[data-hover="pulse"] {
  animation: gu-pulse var(--gu-dur-slow, 420ms) var(--gu-ease, cubic-bezier(0.22, 0.61, 0.36, 1)) 1;
}

.gu-icon[data-hover="shake"]:is(:hover, :focus-visible),
:where(a, button, summary, label, [role="button"], .gu-ico-host):is(:hover, :focus-visible) .gu-icon[data-hover="shake"],
.demo-force-hover .gu-icon[data-hover="shake"] {
  animation: gu-shake 520ms var(--gu-ease, cubic-bezier(0.22, 0.61, 0.36, 1)) 1;
}

/* ---------------------------------------------------------------------------
   4. Animation 2 - the in-view reveal
   ---------------------------------------------------------------------------
   TWO implementations, and exactly one of them runs per element.

   (a) CSS scroll-driven, for `data-reveal="scroll"`. Zero JS, the timeline is
       the element's own progress through the scrollport. Behind an @supports
       guard because `animation-timeline` is Chromium 115+ / Safari 26+ and
       Firefox has it in preview only (MDN BCD, css.properties.animation-timeline,
       read 2026-08-19). Where it is unsupported the whole block is dropped and
       the icon simply renders - which is the ADR-001 degradation rule.

   (b) WAAPI driven by IntersectionObserver, for plain `data-reveal`. That is
       the default, because IntersectionObserver and Element.animate() are
       universal (IO: Chrome 51 / Firefox 55 / Safari 12.1; Element.animate:
       Chrome 36 / Firefox 48 / Safari 13.1) and this is the path that has to
       work in Firefox today. src/runtime/icons.js owns it, and it deliberately
       skips [data-reveal="scroll"] whenever (a) is supported so the two can
       never both drive the same element.

   Neither one sets a starting opacity in CSS. The armed state is applied by JS
   only to elements it has confirmed are below the fold, so an icon is never
   invisible waiting for a script that failed to load.                        */

@supports (animation-timeline: view()) {
  .gu-icon[data-reveal="scroll"] {
    animation: gu-reveal linear both;
    animation-timeline: view();
    animation-range: entry 5% cover 30%;
  }
}

/* ---------------------------------------------------------------------------
   5. Animation 3 - the live state
   ---------------------------------------------------------------------------
   For the icons that label something happening NOW: a video playing, a webinar
   live, a countdown counting, a phone ringing, a search running, the platform
   syncing. `data-loop` names the loop; `data-state="active"` runs it. An icon
   with no data-loop has no live state and never loops - most of the set.

   The loop is on the outer element, so it is a compositor transform and costs
   nothing per frame. It also means the loop and the hover animation are the
   same property, and hover therefore replaces the loop for its duration rather
   than fighting it.                                                          */

.gu-icon[data-loop="pulse"][data-state="active"],
.demo-force-active .gu-icon[data-loop="pulse"] {
  animation: gu-pulse 1.9s var(--gu-ease, cubic-bezier(0.22, 0.61, 0.36, 1)) infinite;
}

.gu-icon[data-loop="spin"][data-state="active"],
.demo-force-active .gu-icon[data-loop="spin"] {
  animation: gu-spin 2.6s linear infinite;
}

.gu-icon[data-loop="shake"][data-state="active"],
.demo-force-active .gu-icon[data-loop="shake"] {
  animation: gu-shake 1.15s var(--gu-ease, cubic-bezier(0.22, 0.61, 0.36, 1)) infinite;
}

/* ---------------------------------------------------------------------------
   6. Forced colors
   ---------------------------------------------------------------------------
   Windows High Contrast replaces the palette; currentColor still resolves, so
   the icons keep working. The one thing to say explicitly is that a decorative
   scale must not survive into a mode built for legibility.                   */
@media (forced-colors: active) {
  .gu-icon { forced-color-adjust: auto; }
}

/* ============================================================================
   7. REDUCED MOTION - the whole point
   ----------------------------------------------------------------------------
   Every animation in this file is off here, and the icon is pinned at its final
   state. Not a shorter duration, not a fade instead of a slide: off.

   Why `!important` on properties rather than only `animation: none`:
   the CSS cascade puts IMPORTANT AUTHOR declarations ABOVE animation
   declarations, and above the Web Animations API animations that
   src/runtime/icons.js creates. So this block beats a running WAAPI animation
   even if the user flips the OS preference mid-animation, without the script
   having to notice. The script cancels them anyway - but the stylesheet does
   not depend on the script, and that is the arrangement that survives a script
   that failed to load.

   `transition` is the exception the cascade does not cover: TRANSITION
   declarations sit ABOVE important author declarations, so a transform can only
   be stopped by removing the transition itself. Hence `transition: none`.

   `animation-timeline: auto` is here as well as `animation: none`, because
   `animation: none` resets the animation shorthand and `animation-timeline` is
   NOT part of that shorthand - a scroll-driven animation left with a view()
   timeline would keep a live timeline attached.

   Compat: prefers-reduced-motion is Chrome 74 / Firefox 63 / Safari 10.1
   (MDN BCD, css.at-rules.media.prefers-reduced-motion, read 2026-08-19). There
   is no browser that runs these animations and cannot read this query.
   ============================================================================ */

@media (prefers-reduced-motion: reduce) {
  .gu-icon,
  .gu-icon.is-armed,
  .gu-icon[data-reveal],
  .gu-icon[data-reveal="scroll"],
  .gu-icon[data-hover],
  .gu-icon[data-loop],
  .gu-icon *,
  .demo-force-hover .gu-icon,
  .demo-force-active .gu-icon {
    animation: none !important;
    animation-timeline: auto !important;
    transition: none !important;
    transform: none !important;
    opacity: 1 !important;
    stroke-dasharray: none !important;
    stroke-dashoffset: 0 !important;
  }
}
