/* =========================================================================
   loading.css — loading indicators (pure CSS, no packages, no JS)
   Three layers:
   1. `.fps-loading-body` — profile-card shells: the tab inserts instantly
      with an empty body uiOutput; while it is `:empty` it shows a centred
      spinner + label (from `data-loading-label`). The first render replaces
      the content, so the spinner clears itself.
   2. Empty plot / htmlwidget outputs — small centred spinner until the first
      render lands. A validate()/error message also fills the node, so it
      replaces the spinner automatically. Recalculating outputs keep Shiny's
      standard dimming (old content stays visible) — this only covers the
      first, blank render.
   3. `html.shiny-busy` — thin sweeping bar pinned to the top of the viewport
      whenever the R process is busy; fade-in is delayed so routine
      sub-second flushes never flicker.
   ========================================================================= */

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

/* --- 1. card shells ----------------------------------------------------- */
.fps-loading-body:empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 55vh;
}
.fps-loading-body:empty::before {
  content: "";
  width: 42px;
  height: 42px;
  border-radius: 50%;
  border: 4px solid rgba(0, 0, 0, 0.10);
  border-top-color: var(--bs-primary, #449183);
  animation: fps-spin 0.8s linear infinite;
}
.fps-loading-body:empty::after {
  content: attr(data-loading-label);
  margin-top: 14px;
  font-size: 14px;
  color: rgba(0, 0, 0, 0.55);
}

/* --- 2. plots / widgets --------------------------------------------------- */
.shiny-plot-output:empty,
.html-widget-output:empty {
  position: relative;
  min-height: 90px; /* an unrendered widget output is 0px tall — give the
                       spinner room without touching fixed plot heights */
}
.shiny-plot-output:empty::after,
.html-widget-output:empty::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 28px;
  height: 28px;
  margin: -14px 0 0 -14px;
  border-radius: 50%;
  border: 3px solid rgba(0, 0, 0, 0.10);
  border-top-color: var(--bs-primary, #449183);
  animation: fps-spin 0.8s linear infinite;
}

/* --- 3. global busy bar --------------------------------------------------- */
html.shiny-busy::after {
  content: "";
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  z-index: 2000;
  pointer-events: none;
  background: linear-gradient(90deg, transparent,
              var(--bs-primary, #449183), transparent);
  background-size: 40% 100%;
  background-repeat: no-repeat;
  animation: fps-busy-sweep 1.2s ease-in-out infinite,
             fps-busy-fade 0.2s 0.35s both;
}
@keyframes fps-busy-sweep {
  from { background-position: -40% 0; }
  to   { background-position: 140% 0; }
}
/* Delayed `both` fill: the bar only fades in once the app has been busy for
   >0.35s, so quick flushes (picker changes, tab switches) don't strobe it. */
@keyframes fps-busy-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}
