/* Loading indicators — one spinner, every context.
 *
 * The app used to ship four unrelated ones: a 34px ring in ui.css, a 40px ring
 * in base.css, a 5px-thick grey ring at 2s in status_pictures.css that
 * silently overrode both on status pages, and a conic-gradient preloader that
 * matched none of them. Three files each declared their own @keyframes spin.
 *
 * There is now a single .spinner, and it is a comet: a faint full track with a
 * rotating arc that fades into its own tail and ends in a rounded head. A
 * two-tone ring (one lit border side, three unlit) is the older pattern and it
 * breaks down at the sizes this app actually needs — below ~20px the lit
 * quarter reads as a flickering dash rather than something turning. A tail
 * that fades has luminance everywhere, so the same component works at 14px
 * beside a button label and at 52px on the app preloader.
 *
 * Geometry, colour and speed are all custom properties, so a context retunes
 * the spinner without redeclaring it:
 *
 *   <span class="spinner spinner--sm" role="status" aria-label="Loading"></span>
 *   <span class="spinner spinner--lg spinner--on-dark" aria-hidden="true"></span>
 *   .my-panel .spinner { --spinner-size: 18px; }
 *
 * Loaded from base.html on every page (and from the model admin's Media), so
 * page stylesheets should never redefine a spinner — retune the properties.
 */

.spinner {
  --spinner-size: 24px;
  --spinner-thickness: 2.5px;
  --spinner-ink: var(--oa-accent);
  --spinner-track: var(--oa-accent-track);
  --spinner-speed: 0.85s;

  position: relative;
  display: inline-block;
  flex: 0 0 auto;
  width: var(--spinner-size);
  height: var(--spinner-size);
  border-radius: 50%;
  background: var(--spinner-track);
  /* Punch the middle out of the disc. The track, the arc and the arc's rounded
     head are then all the same band by construction — nothing has to be
     positioned or sized against anything else when --spinner-thickness moves. */
  -webkit-mask: radial-gradient(farthest-side, #0000 calc(100% - var(--spinner-thickness)), #000 calc(100% - var(--spinner-thickness)));
  mask: radial-gradient(farthest-side, #0000 calc(100% - var(--spinner-thickness)), #000 calc(100% - var(--spinner-thickness)));
}

.spinner::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background:
    /* The head: a dot of exactly band width at 12 o'clock, which is what gives
       the arc a round cap instead of the sheared edge a bare conic leaves. */
    radial-gradient(circle calc(var(--spinner-thickness) / 2) at 50% calc(var(--spinner-thickness) / 2), var(--spinner-ink) 98%, #0000),
    /* The tail: full ink at the head, fading to nothing by the time it has
       wrapped all the way back round to it. */
    conic-gradient(#0000, var(--spinner-ink));
  animation: oa-spin var(--spinner-speed) linear infinite;
}

@keyframes oa-spin {
  to { transform: rotate(1turn); }
}

/* --- sizes ---------------------------------------------------------------
   Thickness climbs more slowly than diameter on purpose: a large spinner with
   proportional stroke reads as a heavy donut rather than a light indicator. */

.spinner--sm { --spinner-size: 18px; --spinner-thickness: 2px; }
.spinner--lg { --spinner-size: 36px; --spinner-thickness: 3px; }
.spinner--xl { --spinner-size: 52px; --spinner-thickness: 3.5px; }

/* --- contexts ----------------------------------------------------------- */

/* Over a radiograph or the 3D stage, where the brand indigo is close enough to
   the dark ground to disappear. */
.spinner--on-dark {
  --spinner-ink: #ffffff;
  --spinner-track: rgba(255, 255, 255, 0.2);
}

/* --- labelled loading state ----------------------------------------------
   A bare spinner says "wait" but not what for. This pairs it with a label,
   which is also what carries the state to a screen reader — put role="status"
   on the wrapper and aria-hidden on the spinner, not the other way round.
   Used beside a control: form submits, inline actions.

   Panel-filling variants stay with their panel (.timeline-loading in
   timeline.css); only the spinner itself is shared. */
.loading-inline {
  display: inline-flex;
  align-items: center;
  gap: var(--oa-space-2);
  color: var(--oa-muted);
  font-size: 0.875rem;
  font-weight: 500;
}

.loading-inline .spinner {
  --spinner-size: 18px;
  --spinner-thickness: 2px;
}

/* [hidden] is how these get toggled, but it only carries UA specificity and
   would lose to the display rules above. */
.spinner[hidden],
.loading-inline[hidden] {
  display: none;
}

/* --- viewer overlay ------------------------------------------------------
   Covers the 3D canvas while a model streams in. Built here rather than as
   inline styles in model3d.js so the viewer and the rest of the app can not
   drift apart again. Non-interactive: orbit controls stay live underneath. */

.viewer-loading {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: grid;
  place-items: center;
  pointer-events: none;
  /* Darkest at the edges, so the model stays readable through the middle
     while it loads instead of being flattened under an even wash. */
  background: radial-gradient(circle at center, rgba(15, 18, 24, 0.45), rgba(15, 18, 24, 0.75));
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  opacity: 1;
  transition: opacity 0.25s ease;
}

.viewer-loading.hidden {
  opacity: 0;
  visibility: hidden;
}

.viewer-loading__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--oa-space-3);
}

.viewer-loading-text {
  color: rgba(255, 255, 255, 0.92);
  font-size: 0.8125rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  /* Progress text counts up through varying digit widths; without a floor the
     label jitters horizontally under a centred spinner. */
  min-width: 6ch;
  text-align: center;
}

/* --- app preloader -------------------------------------------------------
   The full-page hold on app navigation. The ring turns around a static brand
   mark — spinning the mark too would blur it into an unreadable smear, and the
   point of a branded preloader is that the brand is legible. */

.preloader-spinner {
  position: relative;
  display: grid;
  place-items: center;
  width: 52px;
  height: 52px;
}

.preloader-spinner .spinner {
  position: absolute;
  inset: 0;
}

.preloader-spinner__mark {
  width: 22px;
  height: 22px;
  opacity: 0.9;
}

/* --- reduced motion ------------------------------------------------------
   Rotation is the specific thing that provokes vestibular symptoms, so slowing
   it down is not a fix. The arc is dropped for an even ring that breathes:
   still unmistakably "working", with nothing travelling across the screen. */

@media (prefers-reduced-motion: reduce) {
  .spinner::after {
    background: var(--spinner-ink);
    animation: oa-spin-pulse 1.6s ease-in-out infinite;
  }

  .viewer-loading {
    transition: none;
  }
}

@keyframes oa-spin-pulse {
  0%, 100% { opacity: 0.3; }
  50% { opacity: 1; }
}
