@layer components {
  /* Pull-to-refresh — anchored inside the list zone (below filters
   * / search), not as a fixed overlay over the page header. The
   * `.ptr-anchor` is a zero-height slot in the natural document flow;
   * the indicator absolutely-positions inside it and translates down
   * out of the slot when the user pulls. Result: the gesture's
   * affordance lives where the list begins, which is where the user's
   * eyes already are. */
  .ptr-anchor {
    position: relative;
    height: 0;
    z-index: 5;
    pointer-events: none;
  }

  .ptr {
    position: absolute;
    top: 0;
    left: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 48px;
    height: 48px;
    margin-left: -24px;
    border-radius: 50%;
    background: var(--color-surface);
    box-shadow: 0 6px 18px rgb(0 0 0 / 24%);
    transform: translateY(-72px) scale(0.85);
    opacity: 0;
    transition:
      transform 240ms cubic-bezier(0.32, 0.72, 0, 1),
      opacity 180ms ease-out,
      box-shadow 220ms cubic-bezier(0.34, 1.56, 0.64, 1);
  }

  /* While JS owns the transform (active drag), the easing is off so
   * the indicator tracks the finger 1:1. The class is cleared on
   * cancel/done so the snap-back uses the spring-feel above. */
  .ptr.is-active {
    opacity: 1;
    transition: box-shadow 220ms cubic-bezier(0.34, 1.56, 0.64, 1);
  }

  /* Crossed the threshold — accent halo + soft scale bump. The bubble
   * background stays neutral (surface) so the ring and ball read
   * cleanly against it; the halo is the calm "release to refresh"
   * cue. */
  .ptr.is-armed {
    box-shadow:
      0 6px 18px rgb(0 0 0 / 24%),
      0 0 0 4px color-mix(in oklab, var(--color-accent) 22%, transparent);
  }

  .ptr__spinner {
    width: 36px;
    height: 36px;
  }

  /* ── Outer progress ring ─────────────────────────────────────
   * Two concentric circles share the same geometry; the bg sits
   * permanently as a faint reference, the fg is drawn via
   * stroke-dasharray and grows clockwise from the top as the user
   * pulls. Circumference at r=17.25 is 2π·17.25 ≈ 108.4; we round
   * up to 109 so the offset never overshoots into a sliver. */
  .ptr__ring-bg {
    fill: none;
    stroke: var(--color-text-muted);
    opacity: 0.18;
  }

  .ptr__ring-fg {
    fill: none;
    stroke: var(--color-accent);
    stroke-dasharray: 109;
    stroke-dashoffset: calc(109 * (1 - var(--ptr-progress, 0)));
    transform: rotate(-90deg);
    transform-origin: 20px 20px;
    transition: stroke-dashoffset 80ms linear;
  }

  .ptr.is-active .ptr__ring-fg {
    transition: none;
  }

  /* Armed: the ring is fully drawn and starts spinning. The keyframe
   * rotates from the same -90° start position so the visible "head"
   * of the stroke (where dasharray gaps land) sweeps continuously
   * past the user's eye. */
  .ptr.is-armed .ptr__ring-fg {
    stroke-dashoffset: 0;
    animation: ptr-ring-spin 1.4s linear infinite;
  }

  @keyframes ptr-ring-spin {
    from {
      transform: rotate(-90deg);
    }

    to {
      transform: rotate(270deg);
    }
  }

  /* ── Ball (Telstar stylisation) ───────────────────────────────
   * A soccer ball is white-and-black across every theme, so the skin
   * and panels are hardcoded rather than theme-derived. The central
   * pentagon flips to brand accent green on the armed state — the
   * one signature touch that ties the ball back to the Feeberse
   * palette without making it loud. The ball rotates slowly in the
   * opposite direction to the ring while armed, so the layered
   * motion reads as a deliberate pair of speeds. */
  .ptr__ball {
    transform-origin: 20px 20px;
  }

  .ptr__ball-skin {
    fill: #f5f5f0;
    stroke: rgb(0 0 0 / 22%);
    stroke-width: 0.5;
  }

  .ptr__ball-panel {
    fill: #1a1a1a;
    transition: fill 220ms ease-out;
  }

  .ptr__ball-seam {
    stroke: #1a1a1a;
    stroke-width: 0.85;
    stroke-linecap: round;
  }

  .ptr.is-armed .ptr__ball-panel {
    fill: var(--color-accent);
  }

  .ptr.is-armed .ptr__ball {
    animation: ptr-ball-spin 3s linear infinite;
  }

  @keyframes ptr-ball-spin {
    from {
      transform: rotate(0deg);
    }

    to {
      transform: rotate(-360deg);
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .ptr {
      transition:
        transform 0ms,
        opacity 120ms;
    }

    .ptr.is-armed .ptr__ring-fg,
    .ptr.is-armed .ptr__ball {
      animation: none;
    }
  }

  /* Mouse / trackpad — hide entirely. The controller bails on the
   * touch listeners too, so this is just defence in depth. */
  @media (pointer: fine) {
    .ptr-anchor {
      display: none;
    }
  }
}
