/* ==========================================================================
   Animation

   Two rules govern everything here:

   1. Motion carries meaning or it doesn't ship. Markers cascade because the
      map is filling with real places; report pins pulse because they are the
      live, human part of the data; charts grow from zero because that is what
      a count does.

   2. Every effect has an honest resting state. Elements animate *from* an
      offset *to* their real position, so if the animation never runs — reduced
      motion, JS disabled, an old browser — the page is still correct. Nothing
      is hidden waiting for a script to reveal it, except where JS explicitly
      opts in with .reveal, which motion.js always resolves.

   The reduced-motion block at the foot of this file is the master switch.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Keyframes
   -------------------------------------------------------------------------- */

@keyframes fade-up {
  from {
    opacity: 0;
    transform: translateY(18px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

/* Headlines wipe up from behind a mask, the way type is set. */
@keyframes type-set {
  from {
    opacity: 0;
    transform: translateY(0.5em);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* Map markers arrive by dropping onto the map and settling. */
@keyframes marker-drop {
  0% {
    opacity: 0;
    transform: translateY(-14px) scale(0.4);
  }
  60% {
    opacity: 1;
    transform: translateY(0) scale(1.15);
  }
  100% {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

/* Community reports are the living data — a slow ring breathes out of them. */
@keyframes pulse-ring {
  0% {
    transform: scale(0.75);
    opacity: 0.55;
  }
  70% {
    transform: scale(2.4);
    opacity: 0;
  }
  100% {
    transform: scale(2.4);
    opacity: 0;
  }
}

@keyframes marker-bounce {
  0%,
  100% {
    transform: translateY(0);
  }
  35% {
    transform: translateY(-9px);
  }
  70% {
    transform: translateY(-3px);
  }
}

@keyframes popup-in {
  from {
    opacity: 0;
    transform: translateY(6px) scale(0.94);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* The county outline draws itself. stroke-dasharray is set inline by
   hero-map.js to the measured path length. */
@keyframes draw-path {
  to {
    stroke-dashoffset: 0;
  }
}

/* Facility dots land on the hero county map. */
@keyframes dot-land {
  0% {
    opacity: 0;
    transform: scale(0);
  }
  70% {
    opacity: 1;
    transform: scale(1.5);
  }
  100% {
    opacity: 1;
    transform: scale(1);
  }
}

@keyframes wash-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes bar-grow {
  from {
    transform: scaleX(0);
  }
  to {
    transform: scaleX(1);
  }
}

@keyframes bar-rise {
  from {
    transform: scaleY(0);
  }
  to {
    transform: scaleY(1);
  }
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

/* --------------------------------------------------------------------------
   Scroll reveal

   motion.js adds .is-visible when the element enters the viewport. The
   .reveal class is only ever applied BY JavaScript (never in the HTML), so a
   page with scripting disabled shows everything immediately.
   -------------------------------------------------------------------------- */

.reveal {
  opacity: 0;
  transform: translateY(20px);
}

.reveal.is-visible {
  animation: fade-up var(--duration-slow) var(--ease-out) both;
  animation-delay: calc(var(--stagger-index, 0) * var(--stagger-step));
}

/* Children cascade rather than arriving as a block. */
.reveal-group.is-visible > * {
  animation: fade-up var(--duration-slow) var(--ease-out) both;
  animation-delay: calc(var(--stagger-index, 0) * var(--stagger-step));
}

.reveal-group > * {
  opacity: 0;
}

.reveal-group.is-visible > * {
  opacity: 1;
}

/* --------------------------------------------------------------------------
   Hero
   -------------------------------------------------------------------------- */

.hero__eyebrow,
.hero__title,
.hero__lead,
.hero__actions {
  animation: type-set var(--duration-slow) var(--ease-out) both;
}

.hero__eyebrow {
  animation-delay: 60ms;
}
.hero__title {
  animation-delay: 140ms;
}
.hero__lead {
  animation-delay: 300ms;
}
.hero__actions {
  animation-delay: 420ms;
}

/* The county figure. Paths get their dasharray set inline by hero-map.js. */
.hero-figure__outline {
  animation: draw-path var(--duration-hero) var(--ease-in-out) both;
}

.hero-figure__district {
  animation: wash-in var(--duration-base) var(--ease-out) both;
  animation-delay: calc(760ms + var(--stagger-index, 0) * 60ms);
}

.hero-figure__dot {
  transform-box: fill-box;
  transform-origin: center;
  animation: dot-land 420ms var(--ease-spring) both;
  /* Ordered east-to-west by hero-map.js, so the viewer watches Data Center
     Alley fill in before the rest of the county. */
  animation-delay: calc(1150ms + var(--stagger-index, 0) * 7ms);
}

.hero-figure__label {
  animation: fade-in var(--duration-base) var(--ease-out) both;
  animation-delay: calc(1000ms + var(--stagger-index, 0) * 50ms);
}

/* --------------------------------------------------------------------------
   Map
   -------------------------------------------------------------------------- */

/* Applied to the marker's inner span so Leaflet's own transform on the parent
   is never disturbed. */
.pin--enter {
  animation: marker-drop 460ms var(--ease-spring) both;
  animation-delay: calc(var(--stagger-index, 0) * 6ms);
}

.pin--bounce {
  animation: marker-bounce 620ms var(--ease-out);
}

/* The pulsing ring behind a community report pin. */
.pin--report {
  position: relative;
}

.pin--report::after {
  content: "";
  position: absolute;
  inset: -3px;
  border-radius: inherit;
  border: 2px solid var(--status-reported);
  animation: pulse-ring 2600ms var(--ease-out) infinite;
  pointer-events: none;
}

.leaflet-popup {
  animation: popup-in var(--duration-base) var(--ease-out) both;
}

/* Layers fade rather than snapping when toggled off. */
.leaflet-marker-pane .leaflet-marker-icon,
.leaflet-marker-pane .leaflet-cluster-anim {
  transition: opacity var(--duration-base) var(--ease-out);
}

.marker-cluster-custom span {
  transition: transform var(--duration-fast) var(--ease-spring),
    box-shadow var(--duration-fast) var(--ease-out);
}

.marker-cluster-custom:hover span {
  transform: scale(1.14);
}

/* --------------------------------------------------------------------------
   Charts — bars grow from nothing when scrolled into view
   -------------------------------------------------------------------------- */

.chart__bar {
  transform-origin: left center;
}

.is-visible .chart__bar {
  animation: bar-grow var(--duration-slow) var(--ease-out) both;
  animation-delay: calc(var(--stagger-index, 0) * 55ms);
}

.timeline-chart__bar {
  transform-origin: bottom center;
}

.is-visible .timeline-chart__bar {
  animation: bar-rise var(--duration-slow) var(--ease-out) both;
  animation-delay: calc(var(--stagger-index, 0) * 45ms);
}

/* --------------------------------------------------------------------------
   Interaction
   -------------------------------------------------------------------------- */

.card--link {
  transition: transform var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out),
    background var(--duration-fast) var(--ease-out);
}

.card--link:hover {
  transform: translateY(-3px);
}

.report-card {
  transition: background var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out);
}

.report-card:hover {
  background: var(--bg-tint);
}

.btn {
  transition: background var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out),
    transform var(--duration-fast) var(--ease-out);
}

.btn:active:not(:disabled) {
  transform: translateY(1px);
}

.stat {
  transition: background var(--duration-fast) var(--ease-out);
}

/* Numbers are animated by motion.js countUp(); this keeps the glyph box from
   reflowing as the digits change. */
.stat__value {
  font-variant-numeric: tabular-nums;
}

.spinner {
  animation: spin 0.7s linear infinite;
}

/* Faint county watermark behind section breaks. Parallax offset is written to
   --parallax by motion.js. */
.watermark {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
  opacity: 0.035;
  display: flex;
  align-items: center;
  justify-content: flex-end;
}

.watermark svg {
  width: min(60%, 520px);
  height: auto;
  color: var(--text);
  transform: translateY(var(--parallax, 0px));
  will-change: transform;
}

.watermark + * {
  position: relative;
  z-index: 1;
}

/* --------------------------------------------------------------------------
   Reduced motion — the master switch

   Everything above collapses to its final state. Note `animation: none` rather
   than a near-zero duration: several of the effects above use `both` fill
   mode, and a 0.01ms animation would still apply the from-state for a frame.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation: none !important;
    transition: none !important;
    scroll-behavior: auto !important;
  }

  .reveal,
  .reveal-group > * {
    opacity: 1;
    transform: none;
  }

  .hero-figure__outline {
    stroke-dashoffset: 0 !important;
  }

  .chart__bar,
  .timeline-chart__bar {
    transform: none;
  }

  .pin--report::after {
    display: none;
  }

  .watermark svg {
    transform: none;
  }
}
