/* =========================================================================
   Africa Fund Tracker — UI module styles
   Web Component: <africa-fund-tracker>
   See /research/africa-fund-tracker.md for design rationale.
   Design tokens reflect the Figma source (file jqP0qsjY6PQp2eBcK42Wsb).
   ========================================================================= */

/* Perf tier — see main.css §0 for rationale. Repeated here because this
   stylesheet ships independently and the global override must cover the
   tracker's own animation surface. */
html.perf-low africa-fund-tracker,
html.perf-low africa-fund-tracker *,
html.perf-low africa-fund-tracker *::before,
html.perf-low africa-fund-tracker *::after {
  animation-duration: 0.001ms !important;
  animation-delay: 0s !important;
  animation-iteration-count: 1 !important;
  transition-duration: 0.001ms !important;
  transition-delay: 0s !important;
}


/* Design tokens — shared between the in-tree custom element and the
   portaled modal. The modal lives at document.body (so position:fixed
   anchors to the viewport instead of the custom element's bounding
   box), which means it's outside `africa-fund-tracker`'s scope and
   needs its own copy of these vars — otherwise `var(--aft-outer-bg)`
   etc. resolve to invalid and properties fall back to initial values
   (transparent backgrounds, missing shadows). */
africa-fund-tracker,
.aft-donors-modal {
  /* Light-theme tokens. The `var(--foo, fb)` fallbacks keep the
     standalone embed demo working without main.css. Brand colors come
     from main.css (--color-cream-*, --color-ink-*, --color-blue-*,
     --color-orange-*). The lime progress-bar gradient is preserved per
     the Figma — the bar reads "live, growing donations" with a lime
     fill regardless of theme. */

  /* Surfaces — Figma uses a "tray + card" pattern: cream-50 outer
     wrapper with a cream-300 hairline, holding a white inner card
     with a cream-200 hairline. Creates a visible nested-card feel. */
  --aft-outer-bg:        var(--color-cream-50, #f6f6f0);
  --aft-outer-border:    var(--color-cream-300, #d6d6c3);
  --aft-inner-bg:        var(--color-white, #ffffff);
  --aft-inner-border:    var(--color-cream-200, #e7e7d9);
  --aft-track-bg:        #e8e8e8;
  --aft-track-border:    rgba(0,0,0,0.07);

  /* Text */
  --aft-text:            var(--color-ink-900, #171717);
  --aft-text-50:         rgba(0,0,0,0.50);
  --aft-text-79:         rgba(0,0,0,0.79);
  --aft-text-muted:      #868686;
  --aft-text-faint:      var(--color-cream-400, #a5a593);
  --aft-text-dim:        rgba(0,0,0,0.40);

  /* Lime (default donor + bar fill) — bar gradient stays lime per Figma */
  --aft-lime:            #a9f42f;
  --aft-lime-soft:       #d8e46e;
  --aft-lime-50:         rgba(169,244,47,0.50);

  /* Peach / gold / milestone — Figma values pulled directly from the
     light tracker spec. Slightly warmer than the gray/orange semantic
     tokens so the milestone cards read with the right amber heat. */
  --aft-peach-border:    #ffaa2b;
  --aft-peach-name:      var(--color-orange-700, #92400e);   /* whale donor name */
  --aft-peach-amount:    #b45309;                              /* whale donor amount (orange-600) */
  --aft-peach-amount-50: #d97706;                              /* whale donor $ prefix (orange-500) */
  --aft-peach-bg-1:      rgba(255,201,120,0.12);
  --aft-peach-bg-2:      rgba(255,146,57,0.12);
  --aft-milestone-bg-1:  rgba(255, 152, 35, 0.12);
  --aft-milestone-bg-2:  rgba(213,110,0,0.06);
  --aft-milestone-label: #be6600;                              /* "Next milestone" label */
  --aft-milestone-amt:   #cf6f00;                              /* "$50,000" amount */
  --aft-milestone-amt-50: #d68956;                             /* $ prefix on amount */
  --aft-milestone-amt-90: #be6600;
  --aft-comet-orange:    #f18100;
  --aft-comet-orange-2:  #ffcf33;
  --aft-comet-border:    #fe3700;
  --aft-comet-glow:      rgba(255, 156, 26, 0.29);
  --aft-comet-glow-soft: rgba(255, 162, 39, 0.13);

  /* CTA — blue pill with white text, matching the hero primary button. */
  --aft-cta-bg:          var(--color-blue-600, #0052e9);
  --aft-cta-text:        var(--color-white, #ffffff);

  /* Geometry */
  --aft-radius-outer:   10px;
  --aft-radius-inner:   8px;
  --aft-radius-pill:    48px;
  --aft-radius-cta:     40px;
  --aft-pad-outer:      3px;
  --aft-pad-inner:      24px;

  --aft-shadow-card:        0 3px 44px rgba(0,0,0,0.12);
  --aft-shadow-chip-inset:  inset 0 0.5px 3px rgba(255,255,255,0.6);
  --aft-shadow-inner-inset: inset 0 -1px 5px rgba(0,0,0,0.03);

  --aft-font:           var(--font-body, "Plus Jakarta Sans", "Inter", -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, sans-serif);

  /* Back-compat aliases used elsewhere in this file */
  --aft-bg:             var(--aft-outer-bg);
  --aft-card-bg:        var(--aft-inner-bg);
  --aft-card-inset:     var(--color-cream-50, #f6f6f0);
  --aft-fill-1:         var(--aft-lime-soft);
  --aft-fill-2:         var(--aft-lime);
  --aft-fill-hot-1:     #e8ff7a;
  --aft-fill-hot-2:     #a4ff1a;
  --aft-gold-1:         var(--aft-peach-name);
  --aft-gold-2:         var(--aft-peach-amount);

  --aft-easing-spring: cubic-bezier(0.34, 1.4, 0.64, 1);
  --aft-easing-out:    cubic-bezier(0.22, 1, 0.36, 1);
  --aft-easing-in:     cubic-bezier(0.55, 0, 0.66, 0.6);

  --aft-dur-fast:   180ms;
  --aft-dur-med:    320ms;
  --aft-dur-slow:   600ms;
}

/* Host-element layout — only applies to the in-tree custom element,
   not the portaled modal (the modal has its own positioning further
   down). Split from the token block above so the modal can inherit
   tokens without inheriting the host's centered/contained layout. */
africa-fund-tracker {
  display: block;
  position: relative;
  width: 100%;
  max-width: 480px;
  margin-inline: auto;
  font-family: var(--aft-font);
  color: var(--aft-text);
  font-feature-settings: "ss01", "tnum", "lnum";
  -webkit-font-smoothing: antialiased;
  contain: layout;
}

/* -------------------------------------------------------------------------
   Card
   ------------------------------------------------------------------------- */

.aft-card {
  position: relative;
  background: var(--aft-outer-bg);
  border: 0.5px solid var(--aft-outer-border);
  border-radius: var(--aft-radius-outer);
  padding: var(--aft-pad-outer);
  display: flex;
  flex-direction: column;
  gap: 2px;
  box-shadow: var(--aft-shadow-card);
  isolation: isolate;
  transform: translateY(24px);
  opacity: 0;
  transition: transform 720ms var(--aft-easing-spring), opacity 480ms var(--aft-easing-out);
}

.aft-card[data-mounted="true"] {
  transform: translateY(0);
  opacity: 1;
}

/* Animated auto-resize. The intro slide-up uses its own transition above;
   resize-ready is flipped on after intro settles, layering a height
   transition on top so the card can glide between content sizes (e.g. a
   milestone card flipping to completed shrinks its slot). */
.aft-card[data-resize-ready="true"] {
  transition:
    height 640ms var(--aft-easing-out),
    transform 720ms var(--aft-easing-spring),
    opacity 480ms var(--aft-easing-out);
}

/* While the FLIP transition is in flight the card holds an explicit pixel
   height; clip overflow so the inner sections don't visually leak past
   the rounded outer border on the way to the new size. */
.aft-card[data-resizing="true"] {
  overflow: hidden;
}

/* -------------------------------------------------------------------------
   Intro reveal cascade

   Sections live in the template (or are rendered before _playIntro)
   with data-intro="pending". JS flips each one to "entered" at the
   times defined in INTRO_TIMING in africa-fund-tracker.js. CSS does the
   actual animation — opacity + a small upward translate, with a soft
   ease-out so the cascade reads as elegant rather than mechanical.

   The leaderboard rows have an `animation: aft-leaderboard-pop` rule
   for the post-intro "row added on donation" pop. We disable that
   keyframe while the intro pattern is in control of the row so the
   transition + keyframe don't fight each other.

   To retune timing, change INTRO_TIMING in the JS. To retune the
   look of each item's individual reveal (how far it nudges, how soft
   the ease), change the values in this block.
   ------------------------------------------------------------------------- */

[data-intro] {
  transition:
    opacity 980ms var(--aft-easing-out),
    transform 1320ms cubic-bezier(0.22, 1, 0.36, 1);
}

[data-intro="pending"] {
  opacity: 0;
  transform: translateY(8px);
}

[data-intro="entered"] {
  opacity: 1;
  transform: translateY(0);
}

/* Suppress the leaderboard pop keyframe while the row is under intro
   control — the keyframe and the intro transition would otherwise both
   try to animate opacity/translate and produce a visual glitch. */
.aft-leaderboard-item[data-intro] {
  animation: none;
}

/* Milestones have their own .aft-milestone transition rule (opacity +
   transform default to 320ms for the state-flip cross-fade). That's
   more specific than the generic [data-intro] rule above and would
   otherwise win — making milestones reveal noticeably faster than the
   rest of the cascade. Re-assert the intro timing here so milestones
   animate in alongside the other sections. The other transitioned
   properties (background, padding, gap) come from the base rule and
   stay at their state-flip durations. */
.aft-milestone[data-intro] {
  transition:
    opacity 380ms var(--aft-easing-out),
    transform 520ms cubic-bezier(0.22, 1, 0.36, 1);
}

/* -------------------------------------------------------------------------
   Header — bolt + title + status
   ------------------------------------------------------------------------- */

.aft-header {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 10px 14px;
  font-size: 16px;
  letter-spacing: -0.16px;
  font-weight: 500;
  color: var(--aft-text);
}

.aft-bolt {
  display: inline-flex;
  width: 16px;
  height: 16px;
  /* Blue accent on light tracker card per Figma. */
  color: var(--color-blue-600, #0052e9);
  filter: drop-shadow(0 0 6px rgba(0, 82, 233, 0.25));
  animation: aft-bolt-pulse 6s var(--aft-easing-out) infinite;
  flex-shrink: 0;
}

.aft-title {
  flex: 1;
  min-width: 0;
}

.aft-bolt svg { width: 100%; height: 100%; }

@keyframes aft-bolt-pulse {
  0%, 92%, 100% { transform: scale(1); filter: drop-shadow(0 0 6px rgba(0, 82, 233, 0.40)); }
  94% { transform: scale(1.18); filter: drop-shadow(0 0 14px rgba(0, 82, 233, 0.70)); }
  96% { transform: scale(0.95); filter: drop-shadow(0 0 8px rgba(0, 82, 233, 0.55)); }
}

.aft-bolt[data-strike="true"] {
  animation: aft-bolt-strike 700ms var(--aft-easing-spring);
}

@keyframes aft-bolt-strike {
  0%   { transform: scale(0.4) rotate(-20deg); opacity: 0; }
  40%  { transform: scale(1.4) rotate(8deg); opacity: 1; filter: drop-shadow(0 0 18px rgba(0, 82, 233, 0.85)); }
  100% { transform: scale(1) rotate(0); opacity: 1; }
}

.aft-status {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  font-weight: 600;
  color: var(--aft-text-faint);
  letter-spacing: 0.72px;
  text-transform: uppercase;
  flex-shrink: 0;
}

.aft-status-dot {
  width: 4px;
  height: 4px;
  border-radius: 50%;
  /* "Live" dot uses solid green (--color-green-live) per Figma — distinct
     from the lime progress-bar gradient. */
  background: var(--color-green-live, #48ac00);
  box-shadow: 0 0 6px rgba(72, 172, 0, 0.7);
  animation: aft-status-pulse 1.8s var(--aft-easing-out) infinite;
}

.aft-status[data-state="reconnecting"] .aft-status-dot { background: #ffb732; box-shadow: 0 0 8px rgba(255, 183, 50, 0.8); }
.aft-status[data-state="offline"]      .aft-status-dot { background: #555; box-shadow: none; animation: none; }

@keyframes aft-status-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50%      { opacity: 0.5; transform: scale(0.85); }
}

/* -------------------------------------------------------------------------
   Inset (the dark inner panel where the numbers + bar live)
   ------------------------------------------------------------------------- */

.aft-inset {
  background: var(--aft-inner-bg);
  border: 0.5px solid var(--aft-inner-border);
  border-radius: var(--aft-radius-inner);
  padding: 32px 0 0;
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 24px;
  overflow: hidden;
  box-shadow: var(--aft-shadow-inner-inset);
  transition:
    background var(--aft-dur-slow) var(--aft-easing-out),
    box-shadow var(--aft-dur-slow) var(--aft-easing-out);
}

/* Sections that need the horizontal padding */
.aft-counter-section,
.aft-cta-section,
.aft-donors-section,
.aft-leaderboard,
.aft-milestone-section {
  padding-inline: var(--aft-pad-inner);
}

/* Per-donation tick — soft inner glow only, no border. */
.aft-inset[data-tick="true"] {
  animation: aft-inset-tick 600ms var(--aft-easing-out);
}

@keyframes aft-inset-tick {
  0%   { box-shadow: inset 0 0 0 0 transparent; }
  25%  { box-shadow: inset 0 0 28px rgba(214, 248, 107, 0.06); }
  100% { box-shadow: inset 0 0 0 0 transparent; }
}

/* Sustained breathing during burst — overrides the tick */
.aft-card[data-burst="true"] .aft-inset {
  animation: aft-inset-breathe 3.6s ease-in-out infinite;
}

@keyframes aft-inset-breathe {
  0%, 100% { box-shadow: inset 0 0 14px rgba(214, 248, 107, 0.02); }
  50%      { box-shadow: inset 0 0 36px rgba(214, 248, 107, 0.07); }
}

/* -------------------------------------------------------------------------
   Counter
   ------------------------------------------------------------------------- */

.aft-counter-section {
  display: flex;
  flex-direction: column;
  gap: 12px;
  align-items: stretch;
}

.aft-counter-wrap {
  display: flex;
  align-items: baseline;
  line-height: 1;
}

.aft-counter {
  display: inline-flex;
  align-items: baseline;
  font-weight: 600;
  font-size: 52px;
  line-height: 1;
  letter-spacing: -2.08px;
  font-variant-numeric: tabular-nums;
}

.aft-counter-currency {
  /* Recessed "$" — softer than the number per Figma (rgba(0,0,0,0.2)). */
  color: rgba(0, 0, 0, 0.2);
  font-weight: 600;
}

.aft-counter-int {
  display: inline-flex;
  position: relative;
  color: var(--aft-text);
  transition: color 240ms var(--aft-easing-out);
}

.aft-counter[data-flash="true"] .aft-counter-int {
  /* Forest green — close to ink-900 in weight, still reads as "live
     donation" without going pale lime against the white card. */
  color: #087a0d;
}

.aft-goal {
  font-size: 16px;
  font-weight: 500;
  line-height: 1.3;
  letter-spacing: -0.08px;
  color: var(--aft-text-muted);
  display: flex;
  align-items: center;
  gap: 8px;
}

.aft-overflow {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px;
  border-radius: var(--aft-radius-pill);
  background: linear-gradient(135deg, #ffd382, #ffe9d4);
  color: #804800;
  font-weight: 700;
  font-size: 11px;
  letter-spacing: 0.02em;
  opacity: 0;
  transform: scale(0.7);
  transition: opacity 320ms var(--aft-easing-out), transform 480ms var(--aft-easing-spring);
}

.aft-overflow[data-active="true"] {
  opacity: 1;
  transform: scale(1);
}

/* -------------------------------------------------------------------------
   Progress bar
   ------------------------------------------------------------------------- */

.aft-bar-wrap {
  margin-top: 18px;
  display: block;
}

/* Two-column bar layout. Pre-goal the goal column takes 100% width and
   the over column collapses to 0. Once the goal is hit, barWrap flips
   data-over="true" and the columns animate to their split proportions
   (goal ≈ 1/stretchMultiplier of total, over takes the rest). */
.aft-bar-row {
  display: flex;
  align-items: stretch;
  gap: 4px;
}

.aft-bar-column {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
}

.aft-bar-column--goal {
  flex: 1 1 100%;
  transition: flex-basis 760ms var(--aft-easing-spring);
}

.aft-bar-column--over {
  flex: 0 0 0%;
  opacity: 0;
  pointer-events: none;
  overflow: hidden;
  transition:
    flex-basis 760ms var(--aft-easing-spring),
    opacity 480ms var(--aft-easing-out);
}

/* Post-goal split: goal column shrinks to ~1/stretchMultiplier (≈ 62%
   with the default 1.5×) and the over column reveals. Once the over
   fill climbs past 80%, JS overrides these flex-basis values inline to
   grow the over column and shrink the goal column dynamically — see
   _advanceBar. */
.aft-bar-wrap[data-over="true"] .aft-bar-column--goal { flex-basis: 62%; }
.aft-bar-wrap[data-over="true"] .aft-bar-column--over {
  flex: 1 1 38%;
  opacity: 1;
  pointer-events: auto;
}

/* The goal bar is fully filled once the goal is hit, so its leading-edge
   marker has nothing meaningful to mark — fade it out. The over bar's
   own marker carries the live position from here on. */
.aft-bar-wrap[data-over="true"] .aft-bar--goal .aft-bar-edge {
  opacity: 0;
  transition: opacity 480ms var(--aft-easing-out);
}

.aft-bar {
  position: relative;
  width: 100%;
  height: 42px;
  /* Hardcoded color + !important to defeat whatever override was wiping
     the var()-driven version. We previously confirmed via dev tools
     that getComputedStyle shows backgroundColor: rgba(0,0,0,0) here —
     no other rule in this stylesheet sets bar bg to transparent, so
     either there's a stage we can't see or the var chain resolves
     invalid at runtime. Literal color rules both out. Light-theme
     value matches the Figma empty-track gray. */
  background-color: #F4F4F1 !important;
  background-image: none;
  /* Rounded rectangle (was pill). The inner fill keeps its own matching
     radius so its rounded corners stay visible at any width — see below. */
  border-radius: 8px;
  /* overflow stays visible so the inner fill's rounded corners aren't
     clipped flat by the track. The fill itself clips its own shine. */
  isolation: isolate;
  transition: background-color 600ms var(--aft-easing-out);
}

.aft-bar-fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: 0%;
  background: linear-gradient(90deg, var(--aft-fill-1), var(--aft-fill-2));
  /* Explicit radius (was `inherit`) so the fill carries its own rounded
     corners on the right side at any width — the track no longer clips
     them flat. */
  border-radius: 8px;
  /* Clip the traveling ::before shine within the fill's rounded shape
     now that the track's overflow:hidden is gone. */
  overflow: hidden;
  transition: width 720ms var(--aft-easing-spring), background 600ms var(--aft-easing-out);
}

.aft-bar-wrap[data-hot="true"] .aft-bar--goal .aft-bar-fill {
  background: linear-gradient(90deg, var(--aft-fill-hot-1), var(--aft-fill-hot-2));
}

/* Over-goal bar: solid peach fill, no shine — keeps the post-goal moment
   feeling like a separate "bonus" surface rather than a noisier extension
   of the lime bar. */
.aft-bar--over .aft-bar-fill {
  background:  #ffb445;
}
.aft-bar--over .aft-bar-fill::before { display: none; }

/* "Over Goal" subtitle under the over-goal bar — same scale + muted
   colour as the milestone amount labels so the two bar columns balance. */
.aft-bar-label {
  font-size: 12px;
  font-weight: 500;
  line-height: 1.3;
  color: var(--aft-text-50);
  text-align: right;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Traveling shine — drifts left to right across the filled portion */
.aft-bar-fill::before {
  content: "";
  position: absolute;
  top: 0; bottom: 0;
  left: -40%;
  width: 40%;
  background: linear-gradient(
    90deg,
    transparent 0%,
    rgba(255,255,255,0.0) 20%,
    rgba(255,255,255,0.35) 50%,
    rgba(255,255,255,0.0) 80%,
    transparent 100%
  );
  animation: aft-shine 4.5s linear infinite;
  pointer-events: none;
}

@keyframes aft-shine {
  0%   { transform: translateX(0%); opacity: 0; }
  10%  { opacity: 1; }
  90%  { opacity: 1; }
  100% { transform: translateX(350%); opacity: 0; }
}

/* Heartbeat / leading edge — one steady animation regardless of burst state.
   Bar fill gradient (data-hot) already conveys the heat-up.
   JS positions the marker at `calc(fill-pct + 6px)` so there's visible
   padding between the fill's rounded right edge and the marker. When
   fill is at 0 the data-visible flag flips false and the marker fades
   out so it doesn't hover past the bar's left edge. */
.aft-bar-edge {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 4px;
  border-radius: 2px;
  background: rgb(194, 194, 191);
  transform: translateX(-50%);
  transition:
    left 720ms var(--aft-easing-spring),
    opacity 320ms var(--aft-easing-out);
  pointer-events: none;
}

.aft-bar-edge[data-visible="false"] {
  opacity: 0;
}

.aft-bar-edge[data-pulsing="true"] {
  animation: aft-edge-pulse 1.6s ease-in-out infinite;
}

@keyframes aft-edge-pulse {
  0%, 100% {
    box-shadow: 0 0 16px rgba(214, 248, 107, 0.7), 0 0 4px rgba(255,255,255,0.45);
    transform: translateX(-50%) scaleY(0.95);
  }
  50% {
    box-shadow: 0 0 24px rgba(214, 248, 107, 0.95), 0 0 6px rgba(255,255,255,0.75);
    transform: translateX(-50%) scaleY(1.04);
  }
}

/* Notches (milestone ticks) on the bar */
.aft-notch {
  position: absolute;
  top: 50%;
  width: 2px;
  height: 28px;
  /* Dark notch reads on both the lime fill and the cream empty track. */
  background: rgba(0,0,0,0.08);
  transform: translate(-50%, -50%);
  transition: background 320ms var(--aft-easing-out), box-shadow 320ms var(--aft-easing-out);
  pointer-events: none;
}

.aft-notch[data-hit="true"] {
  background: rgba(0, 0, 0, 0.05);
  box-shadow: 0 0 4px rgba(0,0,0,0.03);
}

/* Pre-goal: goal flag is a tall neutral notch, no gold halo. */
.aft-notch[data-flag="true"] {
  width: 2px;
  height: 32px;
  background: rgba(0,0,0,0.32);
}

/* Goal reached → light up gold */
.aft-notch[data-flag="true"][data-hit="true"] {
  width: 3px;
  height: 40px;
  background: var(--aft-gold-1);
  box-shadow: 0 0 18px var(--aft-gold-1), 0 0 6px rgba(0,0,0,0.15);
}

/* Milestone labels under the goal bar. Now lives inside the goal column
   (rather than the wrap directly) so its width tracks the column's
   flex-basis as the bar splits post-goal. */
.aft-notch-labels {
  position: relative;
  height: 14px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--aft-text-faint);
  font-variant-numeric: tabular-nums;
  text-transform: uppercase;
}

.aft-notch-label {
  position: absolute;
  top: 0;
  transform: translateX(-50%);
  white-space: nowrap;
  transition:
    color 320ms var(--aft-easing-out),
    left 800ms var(--aft-easing-out),
    opacity 480ms var(--aft-easing-out);
  pointer-events: none;
}

/* Post-goal the goal bar shrinks; the lower milestone labels (25K, 50K,
   75K, …) would crowd each other as the column narrows. Fade them out
   and keep only the goal (data-flag) label visible — it sits at the
   bar's right edge and marks the boundary into the over-goal zone. */
.aft-bar-wrap[data-over="true"] .aft-notch-label:not([data-flag="true"]) {
  opacity: 0;
}

/* Labels at the very ends of the bar get edge-aligned so they don't overflow
   the track when center-anchored on 0% / 100%. */
.aft-notch-label[data-align="right"] { left: auto; transform: none; }
.aft-notch-label[data-align="left"]  { transform: none; }

.aft-notch-label[data-hit="true"] {
  color: var(--aft-text);
}

/* The goal-flag label intentionally matches the other milestone labels —
   the gold flag notch itself carries the celebration, the text stays neutral. */

/* Containment for the ripple expansion. The bar's overflow is `visible`
   so the fill's rounded corners aren't clipped, but ripples still need
   to be clipped to the bar's shape — they expand to 6× size. */
.aft-ripple-layer {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  overflow: hidden;
  pointer-events: none;
}

/* Bursts: ripple emitted from leading edge on donation */
.aft-bar-ripple {
  position: absolute;
  top: 50%;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: rgba(255,255,255,0.85);
  transform: translate(-50%, -50%) scale(0.4);
  pointer-events: none;
  animation: aft-ripple 700ms var(--aft-easing-out) forwards;
}

@keyframes aft-ripple {
  0%   { transform: translate(-50%, -50%) scale(0.4); opacity: 0.9; }
  100% { transform: translate(-50%, -50%) scale(6);   opacity: 0; }
}

/* -------------------------------------------------------------------------
   Milestone slot — peach gradient card with comet-trail progress
   ------------------------------------------------------------------------- */

.aft-milestone-section {
  /* No horizontal padding — each milestone card has its own 4px inner pad.
     Vertical stack with a thin gap so completed cards stack cleanly above
     the current card.
     column-reverse: DOM order is ascending by amount (required for the
     bar notches + nextIdx / prev-leg math in _reflectMilestones); we
     reverse only the visual stack so the highest-amount milestone reads
     first and the goal sits at the bottom. */
  padding-inline: 0;
  padding: 4px;
  display: flex;
  flex-direction: column-reverse;
  gap: 4px;
}

/* Base card. Three states drive the look:
   • upcoming  — hidden from the stack
   • current   — peach gradient + comet + progress (in-flight)
   • completed — green gradient + check + compact row (earned) */
.aft-milestone {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 12px 16px;
  /* Current/upcoming milestone hairline — soft black-alpha per Figma. */
  border: 0.5px solid rgba(0, 0, 0, 0.08);
  border-radius: 6px;
  background: linear-gradient(to top, var(--aft-milestone-bg-1) 11.111%, var(--aft-milestone-bg-2) 109.03%);
  box-shadow: inset 0 -0.4px 8px rgba(255,255,255,0.03);
  overflow: hidden;
  /* Cross-fade the gradient AND collapse-friendly transitions for the
     state flip. Height isn't animated here — the outer card's auto-resize
     observer animates the section's contribution to overall card height. */
  transition:
    background 720ms var(--aft-easing-out),
    padding 480ms var(--aft-easing-out),
    gap 480ms var(--aft-easing-out),
    opacity 320ms var(--aft-easing-out),
    transform 320ms var(--aft-easing-out);
}

/* Self-contained entrance — fires on render. Mirrors the
   aft-leaderboard-pop pattern so milestones animate in even when
   they're rendered asynchronously (production setSnapshot after the
   intro scan), not just during the synchronous initial cascade.

   The rule is scoped to the VISIBLE states only (not the base
   .aft-milestone selector) for a critical timing reason:
     • Every milestone card is created with data-state="upcoming"
       (display: none) and only then has _reflectMilestones flip it
       to "completed" or "current".
     • If the animation lived on the base selector, the timeline
       would start while the card was display:none — by the time the
       state flips and the card becomes visible, the animation has
       already elapsed and the card just "appears."
     • Scoping the animation to data-state="completed"/"current" means
       the `animation` property is ADDED the moment the card becomes
       visible, which kicks off a fresh play from frame 0.

   Per-card stagger comes from inline --enter-index set in
   _syncMilestoneCards (i.e., the milestone's sort index). */
.aft-milestone[data-state="completed"],
.aft-milestone[data-state="current"] {
  animation: aft-milestone-enter 540ms cubic-bezier(0.22, 1, 0.36, 1) both;
  animation-delay: calc(var(--enter-index, 0) * 75ms);
}

/* When the intro pattern owns this card (mounted in the initial render
   batch before the microtask scan), the keyframe is suppressed — the
   intro transition handles the reveal instead. They'd otherwise both
   try to animate opacity/transform and produce a glitch. */
.aft-milestone[data-intro] {
  animation: none;
}

@keyframes aft-milestone-enter {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Upcoming cards are not yet relevant — removed from layout so the stack
   only contains "what you've earned" + "what's next". */
.aft-milestone[data-state="upcoming"] {
  display: none;
}

/* Completed — compact single-line card. Just check + label, no chip,
   no comet, no progress row. Whisper-soft green wash per the updated
   Figma: barely-there gradient over the white card, neutral hairline.
   The label color does the real signaling — see further below. */
.aft-milestone[data-state="completed"] {
  gap: 0;
  padding: 10px 16px;
  background: linear-gradient(to top, rgba(53,255,35,0.03) 11.111%, rgba(11,213,0,0.02) 109.03%);
  border-color: rgba(0, 0, 0, 0.10);
}

/* Top row — icon + label only. Target chip lives in the bottom progress
   row now (current state only); see .aft-milestone-progress below. */
.aft-milestone-row {
  display: flex;
  align-items: center;
  gap: 4px;
}

.aft-milestone-label-wrap {
  display: flex;
  align-items: center;
  gap: 4px;
  min-width: 0;
}

/* Icon swap by state. Both icons live in the DOM so we can animate the
   pop-in on the check when the card flips to completed. */
.aft-milestone-icon {
  display: none;
  width: 12px;
  height: 12px;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
}
.aft-milestone-icon svg { display: block; }
/* Active-milestone marker: a small flickering flame with embers rising
   off the tip. Body is a single gradient-filled element clipped to a
   flame outline; the inner brightness comes from a radial gradient
   placed near the base (the hot core). A handful of dot embers loop
   from the flame tip upward, fading out as they go. Performance: only
   transform/opacity is animated (GPU-composited); all elements are
   <= 12px; blur is sub-pixel. */
.aft-milestone-icon--flag {
  position: relative;
  width: 12px;
  height: 12px;
}

/* Three-layer flame, mirroring the comet trail's far/mid/near blur
   stack. Each layer flickers at its own period so the layers drift
   in/out of sync — gives the flame a sense of depth and "alive" motion
   that a single layer can't carry. */

/* Far layer — wide, heavily blurred warm halo. Has no clip-path; just a
   radial gradient that fades to transparent at the edges, then a 3.5px
   blur smudges everything into ambient glow. Reads as the light the
   flame is casting on its surroundings, not the flame itself. */
.aft-flame-aura {
  position: absolute;
  left: 50%;
  bottom: -2px;
  width: 14px;
  height: 15px;
  margin-left: -7px;
  background: radial-gradient(ellipse 55% 75% at 50% 60%,
    rgba(255, 165, 55, 0.55)  0%,
    rgba(255, 100, 30, 0.30)  45%,
    rgba(255, 145, 35, 0)     80%);
  filter: blur(5px);
  pointer-events: none;
  transform-origin: 50% 100%;
  animation: aft-flame-flicker 1.5s ease-in-out infinite;
}

/* Mid layer — main flame body. Gradient-filled rect clipped to a flame
   outline. The blur softens the clip-path edge so it reads as
   ember-light rather than a hard cutout. */
.aft-flame {
  position: absolute;
  left: 50%;
  bottom: 0;
  width: 9px;
  height: 12px;
  margin-left: -4.5px;
  background: radial-gradient(ellipse 70% 90% at 50% 78%,
    rgba(255, 250, 220, 1)             0%,
    var(--aft-comet-orange-2, #ffcf4c) 22%,
    var(--aft-comet-orange,   #ff9123) 58%,
    var(--aft-comet-border,   #fe3700) 100%);
  /* Coords are in CSS pixels matching the 9×12 element box. */
  clip-path: path("M 4.5 0 C 7 2, 9 6, 8 10 C 7.5 11.5, 5 12, 4.5 12 C 4 12, 1.5 11.5, 1 10 C 0 6, 2 2, 4.5 0 Z");
  filter: blur(1.7px);
  transform-origin: 50% 100%;
  animation: aft-flame-flicker 2.2s ease-in-out infinite;
}

/* Near layer — bright hot core. Sits on top, centered near the base.
   Mid-level blur keeps it diffuse so it reads as a soft glow
   inside the flame rather than a hard inner shape. */
.aft-flame-core {
  position: absolute;
  left: 50%;
  bottom: 0.5px;
  width: 5px;
  height: 8px;
  margin-left: -2.5px;
  background: radial-gradient(ellipse 70% 85% at 50% 75%,
    rgba(255, 250, 220, 0.95) 0%,
    rgba(255, 215, 130, 0.65) 45%,
    rgba(255, 145, 35, 0)     100%);
  filter: blur(4.7px);
  pointer-events: none;
  transform-origin: 50% 100%;
  animation: aft-flame-flicker 1.0s ease-in-out infinite;
}

/* Sub-pixel scale + sway gives the burning-flame motion. transform-origin
   is the base (50% 100%) so the flicker only nudges the tip, not the
   anchor — feels like fire feeding from the bottom. Shared by all three
   flame layers; mismatched durations keep them slightly out of phase. */
@keyframes aft-flame-flicker {
  0%, 100% { transform: scale(1, 1)         translateX(0); }
  22%      { transform: scale(0.96, 1.06)   translateX(0.2px); }
  44%      { transform: scale(1.03, 0.96)   translateX(-0.2px); }
  66%      { transform: scale(0.98, 1.05)   translateX(0.15px); }
  82%      { transform: scale(1.04, 0.97)   translateX(-0.15px); }
}

/* Embers — small dots starting at the flame tip and floating upward
   while fading. Each has its own --x (start offset) and --drift
   (horizontal sway as it rises) so they don't move as a uniform column. */
.aft-ember {
  position: absolute;
  bottom: 80%;
  left: calc(50% + var(--x, 0px));
  width: 1.5px;
  height: 1.5px;
  margin-left: -0.75px;
  border-radius: 50%;
  background: var(--aft-comet-orange-2, #ffcf4c);
  box-shadow: 0 0 3px rgba(255, 200, 110, 0.85);
  filter: blur(0.25px);
  opacity: 0;
  animation: aft-ember-rise 1.8s ease-out infinite;
  pointer-events: none;
}
/* Stagger so the embers don't all bloom together — gives the sense of
   a steady stream rising off the flame rather than a synchronized burst.
   Per-ember blur varies too: sharper specks read as fresh, blurrier
   ones as dying-out — adds the same depth the comet trail gets from
   its layered blur stack. */
.aft-ember:nth-of-type(1) { animation-delay:  0s;    filter: blur(0.6px); }
.aft-ember:nth-of-type(2) { animation-delay: -0.36s; filter: blur(1.2px); }
.aft-ember:nth-of-type(3) { animation-delay: -0.72s; filter: blur(0.8px); }
.aft-ember:nth-of-type(4) { animation-delay: -1.08s; filter: blur(1.6px); }
.aft-ember:nth-of-type(5) { animation-delay: -1.44s; filter: blur(0.9px); }

@keyframes aft-ember-rise {
  0%   { transform: translate(0, 3px)                              scale(1);   opacity: 0; }
  18%  {                                                                       opacity: 1; }
  100% { transform: translate(var(--drift, 0px), -9px)             scale(0.3); opacity: 0; }
}

.aft-milestone-icon--check {
  color: #7EDE83;
  filter: drop-shadow(0 0 4px rgba(150, 255, 159, 0.4));
}
.aft-milestone[data-state="current"]   .aft-milestone-icon--flag,
.aft-milestone[data-state="completed"] .aft-milestone-icon--check {
  display: inline-flex;
}

.aft-milestone-label {
  font-size: 12px;
  font-weight: 600;
  line-height: 1.3;
  text-transform: uppercase;
  letter-spacing: 0.72px;
  white-space: nowrap;
  /* Current/upcoming milestone — deep amber-brown per the updated
     Figma. The completed state overrides this to forest green below. */
  color: #5c4722;
  transition: color 480ms var(--aft-easing-out);
}
.aft-milestone[data-state="completed"] .aft-milestone-label {
  /* Deep forest green per the updated Figma — reads as "earned" on
     the whisper-green wash without competing with the live ticker. */
  color: #153e17;
}

.aft-milestone-target {
  display: inline-flex;
  align-items: center;
  padding: 4px 8px;
  /* Peach-hairline pill over near-white with a faint warm wash —
     reads as a distinct amount badge against the milestone card's
     orange ground. Updated to match the latest Figma exactly. */
  border: 0.5px solid #ffaa2b;
  border-radius: var(--aft-radius-pill);
  background:
    linear-gradient(189.44deg, rgba(255, 201, 120, 0.12) 1.29%, rgba(255, 146, 57, 0.12) 102.94%),
    rgba(255, 255, 255, 0.98);
  box-shadow: inset 0 -0.5px 7px #ffffff;
  font-size: 14px;
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: 0.12px;
  color: #a35b0e;
  white-space: nowrap;
  flex-shrink: 0;
  transition: background 480ms var(--aft-easing-out), color 480ms var(--aft-easing-out);
}

.aft-milestone-target::before {
  content: "$";
  color: rgba(163, 91, 14, 0.55);
  margin-right: 0;
  transition: color 480ms var(--aft-easing-out);
}

/* Completed cards embed the amount in the label ("$25K MILESTONE") so the
   target pill is no longer needed on the right side — drop it from the
   layout. Comet trail + raised subtitle also only belong to the in-flight
   card. */
.aft-milestone[data-state="completed"] .aft-milestone-target,
.aft-milestone[data-state="completed"] .aft-comet-wrap,
.aft-milestone[data-state="completed"] .aft-milestone-progress {
  display: none;
}

/* -------------------------------------------------------------------------
   Milestone fanfare — fired one-shot when a card flips to completed.
   Pure CSS: JS toggles data-just-hit, the keyframes do the rest.
   ------------------------------------------------------------------------- */

.aft-milestone-burst {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
}

/* Anchor for the ring + sparks. Roughly matches the icon position on the
   left of the row (padding 16px + icon center ~6px). */
.aft-milestone-ring,
.aft-milestone-spark {
  position: absolute;
  top: 50%;
  left: 22px;
  transform: translate(-50%, -50%);
  opacity: 0;
}

.aft-milestone-ring {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  border: 1.5px solid rgba(150, 255, 159, 0.85);
  box-shadow: 0 0 14px rgba(150, 255, 159, 0.5);
}

.aft-milestone-spark {
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: linear-gradient(to bottom, #ffffff, #d8ff8c);
  box-shadow: 0 0 6px rgba(216, 255, 140, 0.9);
}

.aft-milestone[data-just-hit="true"] .aft-milestone-burst { opacity: 1; }
.aft-milestone[data-just-hit="true"] {
  animation: aft-milestone-card-pop 720ms var(--aft-easing-spring);
}
.aft-milestone[data-just-hit="true"] .aft-milestone-ring {
  animation: aft-milestone-ring 900ms var(--aft-easing-out) forwards;
}
.aft-milestone[data-just-hit="true"] .aft-milestone-spark {
  animation: aft-milestone-spark 850ms var(--aft-easing-out) forwards;
}
.aft-milestone[data-just-hit="true"] .aft-milestone-icon--check {
  animation: aft-milestone-check-pop 700ms var(--aft-easing-spring);
}

@keyframes aft-milestone-card-pop {
  0%   { box-shadow: inset 0 -0.4px 8px rgba(255,255,255,0.03), 0 0 0 rgba(150,255,159,0); }
  18%  { transform: scale(1.012); box-shadow: inset 0 -0.4px 8px rgba(255,255,255,0.05), 0 0 24px rgba(150,255,159,0.35); }
  100% { transform: scale(1);     box-shadow: inset 0 -0.4px 8px rgba(255,255,255,0.03), 0 0 0 rgba(150,255,159,0); }
}

@keyframes aft-milestone-ring {
  0%   { transform: translate(-50%, -50%) scale(0.4); opacity: 1; }
  60%  { transform: translate(-50%, -50%) scale(2.6); opacity: 0.55; }
  100% { transform: translate(-50%, -50%) scale(4);   opacity: 0; }
}

/* Each spark gets its own angle via the inline --i (0..5 × 60deg). */
@keyframes aft-milestone-spark {
  0% {
    transform: translate(-50%, -50%) rotate(calc(var(--i) * 60deg)) translateX(2px) scale(1);
    opacity: 1;
  }
  100% {
    transform: translate(-50%, -50%) rotate(calc(var(--i) * 60deg)) translateX(44px) scale(0.3);
    opacity: 0;
  }
}

@keyframes aft-milestone-check-pop {
  0%   { transform: scale(0.4) rotate(-12deg); opacity: 0; }
  55%  { transform: scale(1.25) rotate(4deg);  opacity: 1; }
  100% { transform: scale(1) rotate(0);        opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .aft-milestone[data-just-hit="true"],
  .aft-milestone[data-just-hit="true"] .aft-milestone-ring,
  .aft-milestone[data-just-hit="true"] .aft-milestone-spark,
  .aft-milestone[data-just-hit="true"] .aft-milestone-icon--check {
    animation: none;
  }
  .aft-milestone[data-just-hit="true"] .aft-milestone-burst { opacity: 0; }
  /* Suppress the per-card entrance keyframe too — items appear at
     final opacity/position with no motion. */
  .aft-milestone { animation: none; }
  /* Active milestone flame — kill the flicker on every layer so the
     body sits still, and hide the embers since they only read as
     motion. The flame layers (aura + body + core) remain as the static
     visual marker. */
  .aft-flame-aura,
  .aft-flame,
  .aft-flame-core { animation: none; }
  .aft-ember      { display: none; }
  /* Comet dust trail — same reasoning: it reads as motion, so drop it. */
  .aft-comet-dust-particle { display: none; }
}

/* Comet — layered trail with progressive blur, bright nucleus at the head */
.aft-comet-wrap {
  position: relative;
  height: 28px;
  padding: 14px 0;
}

.aft-comet-track {
  position: relative;
  width: 100%;
  height: 6px;
}

/* Segmented dots along the track (faint specks) */
.aft-comet-track::before {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(circle at 12% 50%, rgba(212, 129, 12, 0.363) 0 1px, transparent 1.5px),
    radial-gradient(circle at 28% 50%, rgba(212, 129, 12, 0.363) 0 1px, transparent 1.5px),
    radial-gradient(circle at 44% 50%, rgba(212, 129, 12, 0.363) 0 1px, transparent 1.5px),
    radial-gradient(circle at 60% 50%, rgba(212, 129, 12, 0.363) 0 1px, transparent 1.5px),
    radial-gradient(circle at 76% 50%, rgba(212, 129, 12, 0.363) 0 1px, transparent 1.5px),
    radial-gradient(circle at 92% 50%, rgba(212, 129, 12, 0.363) 0 1px, transparent 1.5px);
}

/* The "track" the comet is running along — a long, soft, blurred bar that
   stretches the full width of the card. Static; the comet sits on top of it
   and visually fills it as it progresses. Vertically centered with the dots
   and comet head/trail. */
.aft-comet-ground {
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  transform: translateY(-50%);
  height: 4px;
  border-radius: 100px;
  background: rgba(217, 119, 6, 0.52);
  filter: blur(6px);
  pointer-events: none;
}

/* All trail layers anchor at the LEFT edge of the card and extend rightward
   to the head position — so the trail visually starts at the far left of the
   card and grows longer as the comet progresses. JS sets --aft-comet-head-left. */
.aft-comet-trail {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  left: 0;
  width: var(--aft-comet-head-left, 0%);
  min-width: 8px;
  border-radius: 100px;
  pointer-events: none;
  transition: width 600ms var(--aft-easing-out);
}

/* Far layer — tallest, most blurred, warm orange-to-amber halo.
   Bright orange-400 (#fb923c) instead of deep orange-600 so the
   blurred composite doesn't darken into a brown smudge near the head. */
.aft-comet-trail--far {
  height: 18px;
  background: linear-gradient(to right,
    rgba(251, 146, 60, 0.00) 0%,
    rgba(251, 146, 60, 0.30) 40%,
    rgba(252, 211, 77, 0.75) 80%,
    rgba(255, 247, 210, 0.85) 100%
  );
  filter: blur(10px);
}

/* Mid layer — main visible trail: bright orange → amber → white.
   No deep orange-600 stops — high-saturation/high-luminance only so
   the trail stays colorful right up to the head, no darkening band. */
.aft-comet-trail--mid {
  height: 9px;
  background: linear-gradient(to right,
    rgba(251, 146, 60, 0.00) 0%,
    rgba(251, 146, 60, 0.55) 25%,
    rgba(253, 186, 116, 0.80) 50%,
    rgb(252, 197, 77) 80%,
    rgb(255, 235, 143) 98%,
    #ffffff 100%
  );
  filter: blur(4px);
}

/* Near layer — bright pre-head zone, occupies the rightmost ~30% of the trail */
.aft-comet-trail--near {
  height: 6px;
  background: linear-gradient(to right,
    rgba(252, 211, 77, 0.00) 0%,
    rgba(252, 211, 77, 0.00) 50%,
    rgba(252, 211, 77, 0.90) 80%,
    rgba(255, 255, 255, 1.00) 100%
  );
  filter: blur(1.5px);
}

/* Head — bright white nucleus with multi-stop halo + subtle ambient pulse */
.aft-comet-head {
  position: absolute;
  top: 50%;
  left: var(--aft-comet-head-left, 0%);
  transform: translate(-50%, -50%);
  width: 12px;
  height: 8px;
  border-radius: 100px;
  background: #ffffff;
  filter: blur(0.4px);
  transition: left 600ms var(--aft-easing-out);
  pointer-events: none;
  z-index: 1;
  animation: aft-comet-head-pulse 2.6s ease-in-out infinite;
}

/* Continuous subtle glow oscillation — outer halo intensified for the
   light tracker so the comet head still pops on the white card. */
@keyframes aft-comet-head-pulse {
  0%, 100% {
    box-shadow:
      0 0 5px  rgba(255, 255, 255, 0.95),
      0 0 12px rgba(252, 211, 77, 0.70),
      0 0 22px rgba(217, 119, 6, 0.55);
  }
  50% {
    box-shadow:
      0 0 7px  rgba(255, 255, 255, 1),
      0 0 17px rgba(252, 211, 77, 0.90),
      0 0 30px rgba(217, 119, 6, 0.70);
  }
}

/* Donation flare — head briefly intensifies, overrides the ambient pulse */
.aft-comet-head[data-flare="true"] {
  animation: aft-comet-flare 800ms ease-out, aft-comet-head-pulse 2.6s ease-in-out infinite 800ms;
}

@keyframes aft-comet-flare {
  0%   { box-shadow: 0 0 6px rgba(255,255,255,1), 0 0 14px rgba(252,211,77,0.85), 0 0 26px rgba(217,119,6,0.65); }
  30%  { box-shadow: 0 0 12px rgba(255,255,255,1), 0 0 28px rgba(252,211,77,1), 0 0 50px rgba(217,119,6,0.85); }
  100% { box-shadow: 0 0 6px rgba(255,255,255,1), 0 0 14px rgba(252,211,77,0.85), 0 0 26px rgba(217,119,6,0.65); }
}

/* Dust trail — small embers cast off the comet head, drifting leftward
   as if shed in the comet's wake. The container is anchored at the head
   position (same --aft-comet-head-left variable) so as the head moves
   the dust origin moves with it; each particle then animates
   independently leftward from that anchor.

   ▶ SPEED KNOB — change --aft-comet-dust-duration only. The six
   per-particle delays are derived from --aft-comet-dust-step (=
   duration / -6) below, so they stay evenly distributed across the
   cycle no matter what duration you pick. */
.aft-comet-dust {
  --aft-comet-dust-duration: 3.6s;
  --aft-comet-dust-step: calc(var(--aft-comet-dust-duration) / -6);

  position: absolute;
  top: 50%;
  left: var(--aft-comet-head-left, 0%);
  width: 0;
  height: 0;
  transform: translateY(-50%);
  transition: left 600ms var(--aft-easing-out);
  pointer-events: none;
}

.aft-comet-dust-particle {
  position: absolute;
  top: 0;
  left: 0;
  width: 2px;
  height: 2px;
  margin-top: -1px;
  margin-left: -1px;
  border-radius: 50%;
  background: rgb(255, 233, 193);
  box-shadow: 0 0 4px rgba(255, 200, 110, 0.85);
  opacity: 0;
  animation: aft-comet-dust-drift var(--aft-comet-dust-duration) linear infinite;
  will-change: transform, opacity;
}

/* Stagger so the particles emit continuously rather than in a burst.
   Each delay = step × index (step is negative, so the multiplications
   walk the cycle backward by 1/6 each). Per-particle blur varies
   (sharp → soft) like the comet's far/mid/near trail stack — sharper
   specks read as freshly cast off the head, blurrier ones as
   further-decayed dust. */
.aft-comet-dust-particle:nth-of-type(1) { animation-delay: calc(var(--aft-comet-dust-step) * 0); filter: blur(0.3px); }
.aft-comet-dust-particle:nth-of-type(2) { animation-delay: calc(var(--aft-comet-dust-step) * 1); filter: blur(0.9px); }
.aft-comet-dust-particle:nth-of-type(3) { animation-delay: calc(var(--aft-comet-dust-step) * 2); filter: blur(0.5px); }
.aft-comet-dust-particle:nth-of-type(4) { animation-delay: calc(var(--aft-comet-dust-step) * 3); filter: blur(1.3px); }
.aft-comet-dust-particle:nth-of-type(5) { animation-delay: calc(var(--aft-comet-dust-step) * 4); filter: blur(0.7px); }
.aft-comet-dust-particle:nth-of-type(6) { animation-delay: calc(var(--aft-comet-dust-step) * 5); filter: blur(1.1px); }

@keyframes aft-comet-dust-drift {
  0%   { transform: translate(0, 0)                                            scale(1.2); opacity: 0; }
  10%  {                                                                                   opacity: 1; }
  100% { transform: translate(calc(-1 * var(--travel, 36px)), var(--dy, 0px)) scale(0.3); opacity: 0; }
}

/* Bottom row — raised / of / target chip / "milestone reached".
   All four spans sit in a single centered row. Gap is 6px around the
   target chip (Figma) but tighter (2px) on either side of the "of"
   word so the chip feels visually paired with both numbers. */
.aft-milestone-progress {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font-size: 14px;
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: 0.12px;
  text-align: center;
  white-space: nowrap;
  flex-wrap: wrap;
}

.aft-milestone-raised {
  /* Updated Figma: muted amber-brown for the raised amount. */
  color: rgba(124, 95, 42, 0.9);
}

/* "of" sits between the raised amount and the chip — same dimmed
   amber as the trailing context so the phrase reads as one unit. */
.aft-milestone-progress-of,
.aft-milestone-context {
  color: rgba(102, 76, 27, 0.6);
}

/* Tightens the gap immediately around the "of" word so the chip
   doesn't feel orphaned from its neighbors. */
.aft-milestone-raised + .aft-milestone-progress-of,
.aft-milestone-progress-of + .aft-milestone-target {
  margin-left: -2px;
  margin-right: 1px;
}

/* HUD removed in the Figma redesign — "$X to goal" is now the subtitle
   directly under the counter. The hidden element is kept in the DOM for
   any external callers that still reference it, but visually hidden. */
.aft-hud { display: none; }

/* -------------------------------------------------------------------------
   Recent Donors marquee — glassy pills, whale chips animate
   ------------------------------------------------------------------------- */

.aft-donors-section {
  display: flex;
  flex-direction: column;
  gap: 10px;
  align-items: center;
  /* Donors section spans the full width for the marquee mask */
  padding-inline: 0;
}

/* -------------------------------------------------------------------------
   Top donors leaderboard
   3 visible by default; expand button reveals 4–10.
   Top 3 ranked rows get medal treatment (gold / silver / bronze gradients).
   ------------------------------------------------------------------------- */

.aft-leaderboard {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.aft-leaderboard-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}

.aft-leaderboard-label {
  margin: 0;
  font-size: 12px;
  font-weight: 500;
  line-height: 1.3;
  color: var(--aft-text-dim);
}

.aft-leaderboard-toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 8px;
  border: 0;
  background: transparent;
  color: var(--aft-text-faint);
  font: inherit;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  cursor: pointer;
  border-radius: 999px;
  transition: color 180ms var(--aft-easing-out), background-color 180ms var(--aft-easing-out);
}
.aft-leaderboard-toggle:hover { color: var(--aft-text); background: var(--color-cream-100, #f0f0e8); }
.aft-leaderboard-toggle:focus-visible {
  outline: 2px solid var(--aft-lime);
  outline-offset: 2px;
  color: var(--aft-text);
}
.aft-leaderboard-chev {
  transition: transform 240ms var(--aft-easing-out);
}
.aft-leaderboard[data-expanded="true"] .aft-leaderboard-chev {
  transform: rotate(180deg);
}

.aft-leaderboard-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-variant-numeric: tabular-nums;
}

.aft-leaderboard-item {
  display: grid;
  /* 18px medallion (sized to match the rank pip below). */
  grid-template-columns: 18px 1fr auto;
  align-items: center;
  gap: 8px;
  padding: 12px;
  border: 0.5px solid var(--color-cream-200, #e7e7d9);
  border-radius: 8px;
  background: #ffffff;
  /* Insanely subtle drop shadow per the new Figma — barely there, just
     enough to lift the row off the page bg. */
  box-shadow: 0 1px 7px rgba(0, 0, 0, 0.02);
  color: var(--aft-text);
  font-size: 14px;
  font-weight: 600;
  letter-spacing: -0.12px;
  /* Reveal animation when items appear after a leaderboard refresh.
     Duration matches the milestone entrance (540ms) so both lists
     read at the same pace.

     Two variables drive the timing, both set on the parent list in JS
     from INTRO_TIMING (single source of truth):
       • --row-start   — initial delay before the FIRST row starts
         (only set on first render; refreshes get 0 so they don't lag).
       • --row-stagger — per-row offset.
     Per-row index comes from inline --enter-index (each row's
     position in the list). A simultaneous fire-all-at-once entrance
     reads as a single flash; the stagger turns it into a cascade. */
  animation: aft-leaderboard-pop 540ms cubic-bezier(0.22, 1, 0.36, 1) both;
  animation-delay: calc(var(--row-start, 0ms) + var(--enter-index, 0) * var(--row-stagger, 75ms));
}

@keyframes aft-leaderboard-pop {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Items past rank 3 are hidden until the section is expanded. */
.aft-leaderboard:not([data-expanded="true"]) .aft-leaderboard-item--overflow {
  display: none;
}

/* When the leaderboard is expanded, the overflow rows reveal as
   their own event (user-initiated), not part of the intro cascade.
   Override the keyframe-path animation-delay to a tight 30ms stagger
   with no initial offset so the expansion feels snappy. This rule
   applies to the post-refresh path where overflow rows render without
   data-intro and use the .aft-leaderboard-item keyframe. The
   data-intro path (initial render, pre-expand) is handled in JS via
   staggered setTimeouts in the toggle's click handler.
   --enter-index for overflow rows is 3..N-1; subtract 3 so the first
   overflow row starts at 0. */
.aft-leaderboard[data-expanded="true"] .aft-leaderboard-item--overflow {
  animation-delay: calc((var(--enter-index, 3) - 3) * 30ms);
}

/* Rank medallion — 18×18 rounded-square pip. Top 3 get metallic
   gradient treatments (see below); ranks 4+ fall back to a plain
   cream-tinted pip with the number in muted ink. The number reads
   as a small graphic element rather than a text label. */
.aft-leaderboard-rank {
  /* position: relative establishes the containing block for the
     absolutely-positioned .aft-rank-spark children on top-3 medallions.
     Without this, sparks would anchor to whatever the next positioned
     ancestor is (the card, way up the tree) and never appear near the
     pip they're supposed to fly off of. */
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  border-radius: 8px;
  /* Stroke is an inset box-shadow rather than a `border` so it renders
     as one continuous ring along the curve. A `border` at a fractional
     width with a transparent color + rounded corners lets the gradient
     under it bleed through unevenly — darker amber sits at the corners
     vs. the straight edges. Inset shadow doesn't have that artifact. */
  background: var(--color-cream-50, #f6f6f0);
  box-shadow: inset 0 0 0 0.5px var(--color-cream-300, #d6d6c3);
  font-family: "Inter", "Plus Jakarta Sans", sans-serif;
  font-size: 10px;
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.1px;
  color: var(--aft-text-faint);
  text-shadow:
    0 0 17.5px rgba(255, 255, 255, 0.31),
    0 0 5px    rgba(255, 255, 255, 0.91);
}

.aft-leaderboard-name {
  font-weight: 500;
  color: var(--color-ink-800, #272621);
  letter-spacing: -0.12px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.aft-leaderboard-count {
  font-weight: 500;
  color: var(--aft-text-faint);
  font-size: 11px;
  margin-left: 4px;
}

.aft-leaderboard-amount {
  font-weight: 500;
  color: var(--color-ink-800, #272621);
  letter-spacing: -0.12px;
  font-variant-numeric: tabular-nums lining-nums;
}
.aft-leaderboard-amount::first-letter {
  /* Recessed $ to match the donor-chip amount style. */
  color: rgba(39, 38, 33, 0.31);
}

/* --- Medallion treatments for the top 3 ---------------------------------
   Each is a rounded gradient pip with a warm bottom-inset glow + a top
   white sheen, sized to read as a metallic badge at 16×16. Border + drop
   shadow tints follow the metal's hue. The row itself stays white — only
   the medallion changes between ranks. */

/* Glow pulse — animatable custom properties for the outer-halo box-shadow.
   @property registers them so CSS can interpolate between numeric
   values; the inset shadows in the box-shadow stack stay literal,
   only this one shadow's blur + alpha breathe. */
@property --rank-glow-blur  { syntax: '<length>'; initial-value: 3.25px; inherits: false; }
@property --rank-glow-alpha { syntax: '<number>'; initial-value: 0.3;   inherits: false; }

/* Gold — #1. The stroke is layered as the FIRST inset shadow so it
   draws on top of the gradient (no corner-bleed artifact a `border`
   would have at sub-pixel widths + low alpha). Outer-glow + warm
   bottom sheen + top white sheen follow.

   Two ambient effects layer on top:
     • Shimmer — diagonal white band drifts across as a second background
       layer (animated via background-position, see keyframes below).
     • Glow pulse — the outer halo shadow's blur + alpha breathe in/out
       via animated --rank-glow-* custom properties.
   Each rank tints the halo from its own --rank-glow-color triple. */
.aft-leaderboard-item--rank-1 .aft-leaderboard-rank {
  --rank-glow-color: 255, 196, 119;
  background:
    linear-gradient(105deg,
      transparent 25%,
      rgba(255, 255, 255, 0.45) 50%,
      transparent 75%) -150% 0 / 200% 100% no-repeat,
    linear-gradient(188.53deg, rgb(255, 233, 191) 2.45%, rgb(255, 167, 100) 108.7%);
  box-shadow:
    inset 0 0 0 0.5px rgba(252, 137, 0, 0.23),
    0 0 var(--rank-glow-blur) rgba(var(--rank-glow-color), var(--rank-glow-alpha)),
    inset 0 -3.75px 5px rgba(255, 227, 144, 0.87),
    inset 0  2.5px  5px rgba(255, 255, 255, 0.67);
  color: #a55300;
  animation:
    aft-medallion-shimmer 10s ease-in-out infinite,
    aft-medallion-glow     6s ease-in-out infinite;
}

/* Silver — #2. Cool slate-blue tones (not literal silver — Figma uses a
   blue-leaning highlight). */
.aft-leaderboard-item--rank-2 .aft-leaderboard-rank {
  --rank-glow-color: 160, 194, 217;
  background:
    linear-gradient(105deg,
      transparent 25%,
      rgba(255, 255, 255, 0.40) 50%,
      transparent 75%) -150% 0 / 200% 100% no-repeat,
    linear-gradient(188.53deg, rgb(218, 237, 255) 2.45%, rgb(174, 194, 210) 108.7%);
  box-shadow:
    inset 0 0 0 0.5px rgba(0, 71, 252, 0.15),
    0 0 var(--rank-glow-blur) rgba(var(--rank-glow-color), var(--rank-glow-alpha)),
    inset 0 -3.75px 5px rgba(213, 213, 238, 0.87),
    inset 0  2.5px  5px rgba(255, 255, 255, 0.67);
  color: #3f5669;
  animation:
    aft-medallion-shimmer 10s ease-in-out infinite,
    aft-medallion-glow     6s ease-in-out infinite;
  animation-delay: -3.3s, -2s;
}

/* Bronze — #3. Warmer red-brown, distinct from the gold's amber. */
.aft-leaderboard-item--rank-3 .aft-leaderboard-rank {
  --rank-glow-color: 255, 164, 119;
  background:
    linear-gradient(105deg,
      transparent 25%,
      rgba(255, 255, 255, 0.40) 50%,
      transparent 75%) -150% 0 / 200% 100% no-repeat,
    linear-gradient(188.53deg, rgb(237, 210, 191) 2.45%, rgb(224, 149, 126) 108.7%);
  box-shadow:
    inset 0 0 0 0.5px rgba(252, 50, 0, 0.18),
    0 0 var(--rank-glow-blur) rgba(var(--rank-glow-color), var(--rank-glow-alpha)),
    inset 0 -3.75px 5px rgba(235, 182, 162, 0.87),
    inset 0  2.5px  5px rgba(255, 255, 255, 0.67);
  color: #a54200;
  animation:
    aft-medallion-shimmer 10s ease-in-out infinite,
    aft-medallion-glow     6s ease-in-out infinite;
  animation-delay: -6.6s, -4s;
}

/* Slow shimmer drift. The shimmer layer sits off-screen for the first
   60% of the cycle, glides across over the next 35%, and rests
   off-screen on the right for the remainder. Eased so entry/exit feels
   soft rather than mechanical. All three ranks share this keyframe but
   each has its own animation-delay so they don't shimmer in unison
   (which would read as "blinking" instead of "subtle"). The main
   metallic gradient (second background layer) stays at 0,0 throughout. */
@keyframes aft-medallion-shimmer {
  0%, 60%   { background-position: -150% 0, 0 0; }
  95%, 100% { background-position:  150% 0, 0 0; }
}

/* Outer-halo breath: blur expands from 1.25 → 5px and alpha from 0.13
   → 0.32 over half the cycle, then drops back. Continuous (no
   off-period) because a slow opacity breath reads as ambient warmth
   rather than blinking. Per-rank tint comes from --rank-glow-color. */
@keyframes aft-medallion-glow {
  0%, 100% { --rank-glow-blur: 1.25px; --rank-glow-alpha: 0.13; }
  50%      { --rank-glow-blur: 5px;    --rank-glow-alpha: 0.32; }
}

/* Spark particles — small glints that drift off the top 3 medallions
   and fade. Same family as the comet's dust trail, scaled down for
   the 18px pip. Two per medallion (top 3 only — injected via JS).
   Per-rank tint comes from --rank-glow-color so all three particle
   sets stay color-matched to their metal. */
.aft-rank-spark {
  position: absolute;
  top: 0;
  left: calc(50% + var(--x, 0px));
  width: 1.4px;
  height: 1.4px;
  margin: -0.7px;
  border-radius: 50%;
  background: rgba(var(--rank-glow-color, 255, 220, 180), 1);
  box-shadow: 0 0 4px rgba(var(--rank-glow-color, 255, 200, 110), 0.7);
  filter: blur(0.65px);
  opacity: 0;
  animation: aft-rank-spark-rise 6s ease-out infinite;
  pointer-events: none;
}
/* Staggered evenly across the 5s cycle so the medallion emits a
   continuous trickle rather than five sparks blooming together. */
.aft-rank-spark:nth-of-type(2) { animation-delay: -1s; }
.aft-rank-spark:nth-of-type(3) { animation-delay: -2s; }
.aft-rank-spark:nth-of-type(4) { animation-delay: -3s; }
.aft-rank-spark:nth-of-type(5) { animation-delay: -4s; }

@keyframes aft-rank-spark-rise {
  0%   { transform: translate(0, 15px)                       scale(1.9); opacity: 0; }
  15%  {                                                                opacity: 1; }
  100% { transform: translate(var(--drift, 0px), -2px)      scale(0.8); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .aft-leaderboard-item--rank-1 .aft-leaderboard-rank,
  .aft-leaderboard-item--rank-2 .aft-leaderboard-rank,
  .aft-leaderboard-item--rank-3 .aft-leaderboard-rank {
    animation: none;
    /* Restore static background — drop the shimmer layer entirely
       and keep only the metallic gradient. */
  }
  .aft-leaderboard-item--rank-1 .aft-leaderboard-rank { background: linear-gradient(188.53deg, rgb(255, 233, 191) 2.45%, rgb(255, 167, 100) 108.7%); }
  .aft-leaderboard-item--rank-2 .aft-leaderboard-rank { background: linear-gradient(188.53deg, rgb(218, 237, 255) 2.45%, rgb(174, 194, 210) 108.7%); }
  .aft-leaderboard-item--rank-3 .aft-leaderboard-rank { background: linear-gradient(188.53deg, rgb(237, 210, 191) 2.45%, rgb(224, 149, 126) 108.7%); }
  /* Sparks hidden — they only read as motion. */
  .aft-rank-spark { display: none; }
}

/* Two-end layout: "Recent Donors" on the left, "Show all" on the right.
   Padding lines the head up with the rest of the inset content; the
   marquee below intentionally spans full width. */
.aft-donors-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding-inline: var(--aft-pad-inner);
}
.aft-donors-toggle {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 8px;
  border: 0;
  background: transparent;
  color: var(--aft-text-faint);
  font: inherit;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  cursor: pointer;
  border-radius: 999px;
  transition: color 180ms var(--aft-easing-out), background-color 180ms var(--aft-easing-out);
}
.aft-donors-toggle:hover { color: var(--aft-text); background: var(--color-cream-100, #f0f0e8); }
.aft-donors-toggle:focus-visible {
  outline: 2px solid var(--aft-lime);
  outline-offset: 2px;
  color: var(--aft-text);
}
.aft-donors-toggle-icon { transition: transform 180ms var(--aft-easing-out); }
.aft-donors-toggle:hover .aft-donors-toggle-icon { transform: translateX(1px); }

.aft-donors-label {
  font-size: 12px;
  font-weight: 500;
  line-height: 1.3;
  color: var(--aft-text-dim);
}

/* -------------------------------------------------------------------------
   Recent-donors modal
   Fixed full-viewport overlay with a centered card. Rendered as a
   sibling of .aft-card (not a descendant) so the card's intro
   transform doesn't establish the containing block for `position: fixed`.
   ------------------------------------------------------------------------- */

.aft-donors-modal {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  visibility: hidden;
  opacity: 0;
  /* Modal is portaled to <body>, so it doesn't inherit typography from
     the custom element — set it explicitly here. */
  font-family: var(--aft-font);
  color: var(--aft-text);
  font-feature-settings: "ss01", "tnum", "lnum";
  -webkit-font-smoothing: antialiased;
  /* Delay visibility flip to coincide with the opacity fade ending,
     so screen readers don't see it while transparent. */
  transition: opacity 240ms var(--aft-easing-out), visibility 0s linear 240ms;
}
.aft-donors-modal[data-open="true"] {
  visibility: visible;
  opacity: 1;
  transition: opacity 240ms var(--aft-easing-out);
}

.aft-donors-modal-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.72);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  cursor: pointer;
}

.aft-donors-modal-card {
  position: relative;
  width: min(100%, 440px);
  max-height: min(78vh, 720px);
  display: flex;
  flex-direction: column;
  background: var(--aft-outer-bg);
  border: 0.5px solid var(--aft-outer-border);
  border-radius: var(--aft-radius-outer);
  /* Same shadow stack as the main tracker .aft-card — drop shadow for
     elevation + inset top-edge highlight that defines the container
     against the blurred backdrop. Without the inset highlight the card
     felt indistinct from the dimmed page behind it. */
  box-shadow: var(--aft-shadow-card), inset 0 2px 4px rgba(255, 255, 255, 0.03);
  overflow: hidden;
  /* Slide-up entrance complementing the backdrop fade */
  transform: translateY(12px);
  transition: transform 320ms var(--aft-easing-spring);
}
.aft-donors-modal[data-open="true"] .aft-donors-modal-card {
  transform: translateY(0);
}

.aft-donors-modal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 20px;
  border-bottom: 0.5px solid rgba(255, 255, 255, 0.06);
}

.aft-donors-modal-title {
  margin: 0;
  font-size: 14px;
  font-weight: 600;
  letter-spacing: -0.14px;
  color: var(--aft-text);
}

.aft-donors-modal-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--aft-text-faint);
  cursor: pointer;
  border-radius: 50%;
  transition: color 180ms, background 180ms;
}
.aft-donors-modal-close:hover { color: var(--aft-text); background: var(--color-cream-100, #f0f0e8); }
.aft-donors-modal-close:focus-visible {
  outline: 2px solid var(--aft-lime);
  outline-offset: 2px;
}

/* Filter bar — sits between the head and the scrollable body. Not
   sticky because the list itself owns the scroll; keeping search
   fixed at the top of the modal card (above the scroll container)
   means it never disappears regardless of how far the user scrolls. */
.aft-donors-modal-filters {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 16px;
  border-bottom: 0.5px solid rgba(255, 255, 255, 0.04);
}

/* Wrapper hosts the absolutely-positioned magnifier icon. Input gets
   left padding to clear it. Wrapper itself stretches to fill the
   filters row. */
.aft-donors-modal-search-wrap {
  position: relative;
  flex: 1;
  min-width: 0;
}

.aft-donors-modal-search-icon {
  position: absolute;
  top: 50%;
  left: 12px;
  transform: translateY(-50%);
  width: 14px;
  height: 14px;
  color: var(--aft-text-faint);
  pointer-events: none;
  transition: color 180ms;
}
/* Icon brightens in lockstep with the input when the field is focused. */
.aft-donors-modal-search-wrap:focus-within .aft-donors-modal-search-icon {
  color: var(--aft-text);
}

.aft-donors-modal-search {
  width: 100%;
  height: 36px;
  /* Left padding clears the absolute-positioned magnifier icon. */
  padding: 0 12px 0 34px;
  border: 0.5px solid rgba(0, 0, 0, 0.123);
  border-radius: 999px;
  background: rgb(255, 255, 255);
  color: var(--aft-text);
  font: inherit;
  font-size: 13px;
  outline: none;
  transition: border-color 180ms, background-color 180ms;
}
.aft-donors-modal-search::placeholder { color: var(--aft-text-faint); }
.aft-donors-modal-search:hover {
  border: 0.5px solid rgba(0, 0, 0, 0.23);
  background: rgb(255, 255, 255);
}
/* Active/focused state — white border with low opacity + slightly
   brighter inner fill. No lime accent here; this is a neutral utility
   input, the brand-accent ring would compete with the tier colors in
   the list below. */
.aft-donors-modal-search:focus-visible {
  border: 0.5px solid rgba(0, 0, 0, 0.23);
  background: rgb(255, 255, 255);
}
/* Hide the native browser search-clear button — we have no custom
   styling for it and it varies across browsers. */
.aft-donors-modal-search::-webkit-search-cancel-button { display: none; }

.aft-donors-modal-no-results {
  padding: 32px 16px;
  text-align: center;
  color: var(--aft-text-faint);
  font-size: 13px;
}

.aft-donors-modal-body {
  overflow-y: auto;
  flex: 1;
  padding: 8px;
  /* Inset darker panel — same trick as .aft-inset on the main tracker.
     The slightly recessed body visually distinguishes scrolling content
     from the head + filter bar, and gives the row backgrounds more
     contrast to stand on. */
  background: var(--aft-inner-bg);
  box-shadow: var(--aft-shadow-inner-inset);
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
  /* Reserve scrollbar gutter on BOTH sides so the row inset stays
     symmetric whether or not the scrollbar is visible. Without this,
     the right gutter consumes 8px (custom scrollbar width) while the
     left has none — visible as an off-center inset on tinted rows
     where the asymmetry is most noticeable. */
  scrollbar-gutter: stable both-edges;
}
.aft-donors-modal-body::-webkit-scrollbar { width: 8px; }
.aft-donors-modal-body::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.14);
  border-radius: 8px;
}

.aft-donors-modal-loading,
.aft-donors-modal-empty {
  padding: 32px 16px;
  text-align: center;
  color: var(--aft-text-faint);
  font-size: 13px;
}

.aft-donors-modal-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  font-variant-numeric: tabular-nums;
}

.aft-donors-modal-item {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 12px;
  padding: 10px 12px;
  border-radius: 8px;
  border: 0.5px solid transparent;
  font-size: 14px;
  transition: background-color 180ms, border-color 180ms;
  /* Anchor for the straight divider pseudo-element below. */
  position: relative;
}
.aft-donors-modal-item + .aft-donors-modal-item::before {
  /* Hairline divider between rows so the list reads as discrete
     entries even before any hover state lights one up. Drawn as a
     pseudo-element (not border-top) so it doesn't inherit the row's
     border-radius — a bordered row produced curved tips where the
     top edge bent into the rounded corners. The row stays rounded so
     hover/active backgrounds still clip nicely; the divider is just
     a flat hairline across the full width. */
  content: "";
  position: absolute;
  top: -0.5px;
  left: 0;
  right: 0;
  height: 0.5px;
  background: var(--aft-inner-border);
  pointer-events: none;
}
/* Suppress the divider when either neighbour is a tinted (big/whale)
   card. Those rows carry their own margin-block, so a hairline pinned
   to the next row's top edge sits in the middle of that gap and reads
   as visual noise. The margin alone is the separation. */
:is(.aft-donors-modal-item--big, .aft-donors-modal-item--whale) + .aft-donors-modal-item::before,
.aft-donors-modal-item + :is(.aft-donors-modal-item--big, .aft-donors-modal-item--whale)::before {
  display: none;
}
.aft-donors-modal-item:hover { background-color: rgba(255, 255, 255, 0.025); }

.aft-donors-modal-item-meta {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.aft-donors-modal-item-name {
  font-weight: 600;
  color: var(--aft-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.aft-donors-modal-item-time {
  font-size: 12px;
  color: var(--aft-text-faint);
}

.aft-donors-modal-item-amount {
  font-weight: 600;
  color: var(--aft-text);
  font-variant-numeric: tabular-nums lining-nums;
}
/* Dollar sign rendered as a pseudo-element (same pattern as ticker
   chips) so it can be dimmed to 70% independent of the amount glyphs.
   Reads as a softer prefix while the number stays at full weight. */
.aft-donors-modal-item-amount::before {
  content: "$";
  opacity: 0.4;
}

/* --- Tier color treatments ----------------------------------------------
   tiny     ($1–24)   : default — neutral row, white amount
   standard ($25–99)  : lime amount, no background tint
   big      ($100–499): subtle peach row + lighter peach amount
   whale    ($500+)   : full peach gradient row + bordered + glowing amount
   Mirrors the ticker-chip + leaderboard tier hierarchy so a viewer
   scanning the modal recognises whales/big donors instantly. */

.aft-donors-modal-item--standard .aft-donors-modal-item-amount {
  color: var(--aft-lime);
}

.aft-donors-modal-item--big {
  background: linear-gradient(135deg, rgba(255, 201, 120, 0.06), rgba(255, 146, 57, 0.02));
  border-color: rgba(255, 180, 69, 0.16);
  /* Breathing room around the tinted card so it reads as its own
     visual block rather than blurring into adjacent rows. */
  margin-block: 6px;
}
.aft-donors-modal-item--big .aft-donors-modal-item-name {
  color: var(--aft-peach-name);
}
.aft-donors-modal-item--big .aft-donors-modal-item-amount {
  color: var(--aft-peach-amount);
}

.aft-donors-modal-item--whale {
  background: linear-gradient(135deg, var(--aft-peach-bg-1), var(--aft-peach-bg-2));
  border-color: var(--aft-peach-border);
  box-shadow: inset 0 0.5px 0 rgba(255, 221, 169, 0.18), 0 0 24px rgba(255, 180, 69, 0.06);
  margin-block: 6px;
}

/* No hover state on the tiered (peach) cards — they're visually
   prominent enough on their own. background-color: transparent
   overrides the base .aft-donors-modal-item:hover background-color,
   which would otherwise bleed white through the tier's translucent
   gradient. */
.aft-donors-modal-item--big:hover,
.aft-donors-modal-item--whale:hover {
  background-color: transparent;
}
.aft-donors-modal-item--whale .aft-donors-modal-item-name {
  color: var(--aft-peach-name);
}
.aft-donors-modal-item--whale .aft-donors-modal-item-amount {
  color: var(--aft-peach-amount);
  text-shadow: 0 0 12px rgba(255, 180, 69, 0.4);
}

.aft-ticker {
  width: 100%;
  height: 36px;
  position: relative;
  /* Horizontal clip for the marquee scroll, vertical visible so the
     chip drop-shadows are not cropped. */
  overflow: hidden visible;
  font-variant-numeric: tabular-nums;
  mask-image: linear-gradient(90deg, transparent 0%, #000 8%, #000 92%, transparent 100%);
  -webkit-mask-image: linear-gradient(90deg, transparent 0%, #000 8%, #000 92%, transparent 100%);
}

.aft-ticker-rail {
  position: absolute;
  top: 50%;
  left: 0;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
  will-change: transform;
}

/* Default donor chip — white bg, soft gray hairline, gentle drop
   shadow (per the updated Figma light tracker). */
.aft-ticker-item {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 5px 10px;
  border: 0.5px solid var(--color-cream-200, #e7e7d9);
  border-radius: var(--aft-radius-pill);
  background: #ffffff;
  box-shadow: 0 1px 6px rgba(0, 0, 0, 0.04);
  flex-shrink: 0;
  font-size: 14px;
  font-weight: 500;
  letter-spacing: -0.12px;
  line-height: 1.3;
  position: relative;
  overflow: hidden;
}

.aft-ticker-item-name {
  color: var(--color-ink-800, #272621);
}

.aft-ticker-item-amount {
  /* Near-black per the Figma — the amount is a value, not a status.
     The $ glyph is recessed to gray. */
  color: #292929;
  font-weight: 500;
}

.aft-ticker-item-amount::before {
  content: "$";
  color: #a1a1a1;
  margin-right: 0;
}

/* Whale chip — peach hairline + barely-tinted white bg + warm soft
   drop shadow. Color of the amount stays in the orange family. */
.aft-ticker-item--whale {
  border-color: var(--aft-peach-border);
  /* Layer a faint peach gradient over white so the chip reads as
     "premium" without darkening the bg. Matches the Figma exactly. */
  background:
    linear-gradient(185.6deg, rgba(255, 201, 120, 0.05) 1.3%, rgba(255, 146, 57, 0.05) 102.9%),
    #ffffff;
  box-shadow: 0 2px 4px rgba(255, 160, 120, 0.22);
}

.aft-ticker-item--whale .aft-ticker-item-name {
  color: var(--aft-peach-name);
}

.aft-ticker-item--whale .aft-ticker-item-amount {
  color: var(--aft-peach-amount);
  font-weight: 600;
}

.aft-ticker-item--whale .aft-ticker-item-amount::before {
  color: var(--aft-peach-amount-50);
}

/* Sheen that travels across the whale chip */
.aft-ticker-item--whale::after {
  content: "";
  position: absolute;
  top: 0;
  left: -60%;
  width: 50%;
  height: 100%;
  background: linear-gradient(
    105deg,
    transparent 0%,
    rgba(255, 221, 169, 0.0) 30%,
    rgba(255, 221, 169, 0.55) 50%,
    rgba(255, 221, 169, 0.0) 70%,
    transparent 100%
  );
  animation: aft-whale-sheen 3.2s ease-in-out infinite;
  pointer-events: none;
  mix-blend-mode: screen;
}

@keyframes aft-whale-sheen {
  0%   { transform: translateX(0%); }
  60%  { transform: translateX(360%); }
  100% { transform: translateX(360%); }
}

.aft-ticker-item--ghost { opacity: 0.4; }

/* Flourish when a new donation chip first enters the visible marquee area */
.aft-ticker-item[data-flourish="true"] {
  animation: aft-ticker-flourish 900ms var(--aft-easing-spring);
  z-index: 1;
}

@keyframes aft-ticker-flourish {
  0%   { transform: scale(0.7);  filter: brightness(1.5); }
  35%  { transform: scale(1.08); filter: brightness(1.6); }
  100% { transform: scale(1);    filter: brightness(1); }
}

/* -------------------------------------------------------------------------
   Notifications — float up from center, below the CTA
   ------------------------------------------------------------------------- */

/* Notifications stack at the top-right of the inset. They live as a direct
   child of .aft-inset so the visible column extends all the way from just
   above the bar up to the very top edge of the black container. */
.aft-notifications {
  position: absolute;
  top: 0px;
  right: 0px;
  /* Height covers the area from just above the bar up to the inset's top */
  height: 149px;
  width: 55%;
  max-width: 240px;
  overflow: hidden;
  pointer-events: none;
  z-index: 3;
  /* Tiny top fade so chips dissolve gracefully at the container edge */
  mask-image: linear-gradient(180deg, transparent 0%, #000 8%, #000 100%);
  -webkit-mask-image: linear-gradient(180deg, transparent 0%, #000 8%, #000 100%);
}

.aft-notif {
  position: absolute;
  right: 20px;
  bottom: 0;
  margin-bottom: 8px;
  padding: 7px 12px;
  border-radius: var(--aft-radius-pill);
  /* Light toast: white pill with a cream hairline + subtle gray shadow,
     matching the rest of the tracker card. */
  background: var(--color-white, #fff);
  border: 0.5px solid var(--color-cream-200, #e7e7d9);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
  font-size: 12px;
  line-height: 1.3;
  color: var(--color-ink-900, #171717);
  display: inline-flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
  opacity: 0;
  transform: translateY(14px) scale(0.85);
  will-change: transform, opacity, bottom;
  transition:
    bottom    420ms var(--aft-easing-spring),
    opacity   260ms ease-out,
    transform 360ms var(--aft-easing-spring);
}

.aft-notif[data-entering="true"] {
  opacity: 1;
  transform: translateY(0) scale(1);
}

.aft-notif[data-exiting="true"] {
  opacity: 0;
  transform: translateY(-8px) scale(0.94);
}

.aft-notif-name {
  color: var(--aft-text);
  font-weight: 600;
  letter-spacing: -0.005em;
}

.aft-notif-amount {
  /* Forest green for amounts — matches the donor-pill amount color
     and reads on the white toast background. */
  color: #3ea408;
  font-weight: 800;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
}

.aft-notif-amount::before { content: "+$"; color: rgba(62, 164, 8, 0.5); margin-right: 1px; font-weight: 600; }

/* Standard — default styling above */

/* Big tier — same white toast, slightly stronger green border halo */
.aft-notif--big {
  padding: 9px 14px;
  background: var(--color-white, #fff);
  border-color: rgba(62, 164, 8, 0.45);
  box-shadow: 0 2px 10px rgba(62, 164, 8, 0.10), 0 1px 4px rgba(0, 0, 0, 0.06);
  font-size: 12px;
}

.aft-notif--big .aft-notif-amount { color: #3ea408; font-size: 13px; }

/* Whale tier — peach toast */
.aft-notif--whale {
  padding: 10px 14px;
  background: var(--color-white, #fff);
  border-color: var(--aft-peach-border);
  box-shadow: 0 2px 12px rgba(255, 170, 43, 0.22), 0 1px 4px rgba(0, 0, 0, 0.06);
  font-size: 12px;
  gap: 8px;
}

.aft-notif--whale .aft-notif-name { font-size: 13px; font-weight: 600; color: var(--aft-peach-name); }
.aft-notif--whale .aft-notif-amount { font-size: 14px; color: var(--aft-peach-amount); }
.aft-notif--whale .aft-notif-amount::before { color: var(--aft-peach-amount-50); }

/* Aggregate / chaos card — warm orange toast */
.aft-notif--aggregate {
  background: var(--color-white, #fff);
  border-color: rgba(217, 119, 6, 0.45);
  box-shadow: 0 2px 12px rgba(217, 119, 6, 0.15), 0 1px 4px rgba(0, 0, 0, 0.06);
}

.aft-notif--aggregate .aft-notif-amount { color: #b45309; }
.aft-notif--aggregate .aft-notif-amount::before { color: #d97706; }

/* -------------------------------------------------------------------------
   Donate CTA
   ------------------------------------------------------------------------- */

.aft-cta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: 16px 24px;
  border: none;
  border-radius: var(--aft-radius-cta);
  background: var(--aft-cta-bg);
  color: var(--aft-cta-text);
  font-family: inherit;
  font-weight: 700;
  font-size: 16px;
  line-height: 1.25;
  letter-spacing: -0.0px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  position: relative;
  z-index: 2;
  /* Single subtle gray drop shadow — no lime glow, no inset highlight,
     no breathing animation. The button reads as a solid blue affordance. */
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08);
  transition:
    transform 320ms var(--aft-easing-spring),
    box-shadow 200ms var(--aft-easing-out);
}

.aft-cta:hover {
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12);
}

.aft-cta:active {
  transform: scale(0.975);
  transition:
    transform 90ms ease-out,
    box-shadow 90ms ease-out;
}

.aft-cta:focus-visible { outline: 2px solid var(--color-blue-600, #0052e9); outline-offset: 4px; }

.aft-cta-arrow { transition: transform var(--aft-dur-fast) var(--aft-easing-out); }
.aft-cta:hover .aft-cta-arrow { transform: translateX(4px); }

/* -------------------------------------------------------------------------
   Goal-hit celebration overlay
   ------------------------------------------------------------------------- */

.aft-celebration {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 10;
  opacity: 0;
  transition: opacity 240ms var(--aft-easing-out);
}

.aft-celebration[data-active="true"] { opacity: 1; }

.aft-celebration-flash {
  position: absolute;
  inset: 0;
  background: radial-gradient(circle at center, rgba(214, 248, 107, 0.4), transparent 70%);
  animation: aft-flash 700ms var(--aft-easing-out) forwards;
}

@keyframes aft-flash {
  0%   { opacity: 0; transform: scale(0.6); }
  35%  { opacity: 1; transform: scale(1.05); }
  100% { opacity: 0; transform: scale(1.4); }
}

.aft-confetti-piece {
  position: absolute;
  width: 8px;
  height: 14px;
  border-radius: 2px;
  top: 50%;
  left: 50%;
  pointer-events: none;
  will-change: transform, opacity;
}

/* -------------------------------------------------------------------------
   Burst mode — slow soft outer glow that breathes during high-traffic moments
   ------------------------------------------------------------------------- */

.aft-card {
  /* Smooth fall-off when burst state clears */
  transition: box-shadow 900ms var(--aft-easing-out);
}

.aft-card[data-burst="true"] {
  animation: aft-burst-pulse 3.6s ease-in-out infinite;
}

@keyframes aft-burst-pulse {
  0%, 100% { box-shadow: var(--aft-shadow-card), 0 0 28px -10px rgba(214, 248, 107, 0.14); }
  50%      { box-shadow: var(--aft-shadow-card), 0 0 44px -6px  rgba(214, 248, 107, 0.26); }
}

/* -------------------------------------------------------------------------
   Mobile (< 480px)
   ------------------------------------------------------------------------- */

@media (max-width: 480px) {
  africa-fund-tracker { --aft-pad-card: 12px; --aft-pad-inset: 18px; }
  .aft-bar { height: 48px; }
  .aft-notifications { height: 90px; }
}

/* -------------------------------------------------------------------------
   Reduced motion
   ------------------------------------------------------------------------- */

@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;
  }
}
