/* ============================================================
   skeleton.css — Apple-feel skeleton system
   Load order: global.css → components.css → skeleton.css → [page].css
   Depends on tokens from global.css. Adds skeleton tokens of its own.

   Principles enforced here, not in JS:
   - Spatial consistency (skeletons occupy the final box exactly)
   - Subtle shimmer (low-contrast luminance drift, no white sweep)
   - Crossfade handoff (no hard swap)
   - Blur-to-focus for images (the blur IS the skeleton)
   - Reduced-motion respect (opacity fade only)
   ============================================================ */

:root {
  /* Light-mode skeleton tokens (kept for future themed surfaces).
     The site lives in dark, so the :root values below resolve to the
     dark variant — see the [data-theme="light"] block at the bottom. */
  --skeleton-base:        rgba(255, 255, 255, 0.05);
  --skeleton-highlight:   rgba(255, 255, 255, 0.09);

  --skeleton-radius-sm:   4px;                 /* matches text line radius */
  --skeleton-radius-md:   0px;                 /* matches existing card edges (sharp) */
  --skeleton-radius-lg:   0px;                 /* matches hero/media edges */

  --shimmer-duration:     3.6s;
  --shimmer-angle:        110deg;

  --crossfade-duration:   280ms;
  --spring-settle:        cubic-bezier(0.2, 0.8, 0.2, 1.05);
  --blur-up-duration:     480ms;

  /* Ambient drift — for surfaces that must show liveness without
     suggesting "loading bar". Used by charts and the diagnosis scan. */
  --ambient-drift-duration: 4.2s;
}

/* Light-theme override token block. Triggered by adding
   data-theme="light" on <html> (or any ancestor). */
[data-theme="light"] {
  --skeleton-base:      rgba(0, 0, 0, 0.04);
  --skeleton-highlight: rgba(0, 0, 0, 0.07);
}

/* ============================================================
   Primitive — .skl
   Every variant is a class composed on top of .skl. The base
   class owns the shimmer paint; variants own the silhouette.
   ============================================================ */
.skl {
  position: relative;
  display: block;
  background-color: var(--skeleton-base);
  background-image: linear-gradient(
    var(--shimmer-angle),
    var(--skeleton-base) 0%,
    var(--skeleton-highlight) 50%,
    var(--skeleton-base) 100%
  );
  background-size: 200% 100%;
  background-position: 200% 0%;
  background-repeat: no-repeat;
  animation: skl-shimmer var(--shimmer-duration) ease-in-out infinite;
  border-radius: var(--skeleton-radius-md);
  overflow: hidden;
  /* Spatial reservation — every variant must lock its own height
     or aspect-ratio. The base class refuses to collapse. */
  min-height: 1px;
  /* Skeletons are decorative — the visually-hidden aria-label on
     the root status node handles the announcement. */
  user-select: none;
  pointer-events: none;
}

@keyframes skl-shimmer {
  0%   { background-position:  200% 0%; }
  100% { background-position: -100% 0%; }
}

/* ============================================================
   Variant — .skl-text
   Single or multi-line. Configurable via inline style:
     --skl-lines      (number of lines, default 1)
     --skl-line-h     (line-height in px, default 18)
     --skl-last-w     (last-line width %, default 100%)
   Total height = lines * line-h, plus gap between lines.
   ============================================================ */
.skl-text {
  --skl-lines:  1;
  --skl-line-h: 18px;
  --skl-last-w: 100%;
  display: flex;
  flex-direction: column;
  gap: calc(var(--skl-line-h) * 0.45);
  background: none;
  animation: none;
  border-radius: 0;
  overflow: visible;
  min-height: 0;
}
.skl-text > .skl-line {
  height: var(--skl-line-h);
  border-radius: var(--skeleton-radius-sm);
  background-color: var(--skeleton-base);
  background-image: linear-gradient(
    var(--shimmer-angle),
    var(--skeleton-base) 0%,
    var(--skeleton-highlight) 50%,
    var(--skeleton-base) 100%
  );
  background-size: 200% 100%;
  background-position: 200% 0%;
  background-repeat: no-repeat;
  animation: skl-shimmer var(--shimmer-duration) ease-in-out infinite;
}
.skl-text > .skl-line:last-child { width: var(--skl-last-w); }

/* ============================================================
   Variant — .skl-box
   Generic rectangle for cards, buttons, badges. Caller sets
   width/height inline so the box matches the final element.
   ============================================================ */
.skl-box { /* uses .skl defaults; caller dimensions via style */ }

/* ============================================================
   Variant — .skl-image
   Aspect-ratio-locked. Accepts:
     --skl-ar     (aspect-ratio, default 16/9)
   Pair with <img> wrapper for blur-up — see .blur-up below.
   ============================================================ */
.skl-image {
  --skl-ar: 16 / 9;
  width: 100%;
  aspect-ratio: var(--skl-ar);
  border-radius: var(--skeleton-radius-lg);
}

/* ============================================================
   Variant — .skl-avatar
   Circle. Caller sets --skl-size inline.
   ============================================================ */
.skl-avatar {
  --skl-size: 40px;
  width: var(--skl-size);
  height: var(--skl-size);
  border-radius: 50%;
}

/* ============================================================
   Variant — .skl-card
   Composes .skl-image + .skl-text + .skl-box at the same layout
   as a real card. Caller supplies padding via inline style.
   ============================================================ */
.skl-card {
  background: none;
  animation: none;
  border: 1px solid var(--border);
  padding: 18px 16px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  border-radius: var(--skeleton-radius-md);
  overflow: visible;
  min-height: 0;
}

/* ============================================================
   Variant — .skl-list
   Repeats a row skeleton at the real row height. Caller sets
   --skl-row-h and the row count via the number of children.
   ============================================================ */
.skl-list {
  --skl-row-h: 60px;
  background: none;
  animation: none;
  display: flex;
  flex-direction: column;
  gap: 8px;
  border-radius: 0;
  overflow: visible;
  min-height: 0;
}
.skl-list > .skl-row {
  height: var(--skl-row-h);
  border-radius: var(--skeleton-radius-md);
  background-color: var(--skeleton-base);
  background-image: linear-gradient(
    var(--shimmer-angle),
    var(--skeleton-base) 0%,
    var(--skeleton-highlight) 50%,
    var(--skeleton-base) 100%
  );
  background-size: 200% 100%;
  background-position: 200% 0%;
  background-repeat: no-repeat;
  animation: skl-shimmer var(--shimmer-duration) ease-in-out infinite;
}

/* ============================================================
   Variant — .skl-hero
   Full-bleed media + headline + sub. Caller supplies height
   via --skl-hero-h or aspect-ratio.
   ============================================================ */
.skl-hero {
  --skl-hero-h: 60dvh;
  background: none;
  animation: none;
  width: 100%;
  min-height: var(--skl-hero-h);
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 18px;
  padding: 0;
  border-radius: 0;
  overflow: visible;
}

/* ============================================================
   Crossfade handoff
   Real content and skeleton occupy the same grid cell. Two
   stacked layers, not siblings — no layout shift.

   Markup:
     <div class="skl-stage" data-skeleton-stage>
       <div class="skl-layer skl-layer--skeleton" data-skeleton-layer>
         ...skeleton variants...
       </div>
       <div class="skl-layer skl-layer--content" data-content-layer hidden>
         ...real content...
       </div>
     </div>

   skeleton.js flips the [data-state] attribute on the stage:
     pending  → skeleton visible, content hidden
     ready    → crossfade in progress
     resolved → skeleton unmounted, content live
   ============================================================ */
.skl-stage {
  position: relative;
  display: grid;
  grid-template-areas: "stack";
  isolation: isolate;
}
.skl-stage > .skl-layer {
  grid-area: stack;
  min-width: 0;
}
.skl-stage > .skl-layer--skeleton {
  opacity: 1;
  transition: opacity var(--crossfade-duration) ease-out;
  z-index: 2;
}
.skl-stage > .skl-layer--content {
  opacity: 0;
  transform: translateY(2px);
  filter: blur(2px);
  transition:
    opacity   var(--crossfade-duration) var(--spring-settle),
    transform var(--crossfade-duration) var(--spring-settle),
    filter    var(--crossfade-duration) var(--spring-settle);
  z-index: 1;
}

.skl-stage[data-state="ready"] > .skl-layer--skeleton {
  opacity: 0;
  pointer-events: none;
}
.skl-stage[data-state="ready"] > .skl-layer--content {
  opacity: 1;
  transform: translateY(0);
  filter: blur(0);
}
.skl-stage[data-state="resolved"] > .skl-layer--skeleton {
  display: none;
}
.skl-stage[data-state="resolved"] > .skl-layer--content {
  opacity: 1;
  transform: none;
  filter: none;
}

/* ============================================================
   Blur-to-focus images — <img class="blur-up">
   Render the BlurHash / LQIP first via background-image on the
   wrapper, then crossfade the real <img> in on load.

   Markup:
     <div class="blur-up" style="--skl-ar: 16/9;
                                  background-image: url('lqip.jpg');">
       <img class="blur-up__img" data-src="full.jpg" alt="">
     </div>
   ============================================================ */
.blur-up {
  position: relative;
  width: 100%;
  aspect-ratio: var(--skl-ar, 16/9);
  overflow: hidden;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  border-radius: var(--skeleton-radius-lg);
  filter: blur(14px);
  transform: scale(1.04); /* hide the soft edge of the blur */
  transition:
    filter    var(--blur-up-duration) var(--spring-settle),
    transform var(--blur-up-duration) var(--spring-settle);
}
.blur-up__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity var(--blur-up-duration) var(--spring-settle);
}
.blur-up.is-loaded {
  filter: blur(0);
  transform: scale(1);
}
.blur-up.is-loaded .blur-up__img { opacity: 1; }

/* ============================================================
   Ambient drift — used for surfaces that need to feel alive
   without suggesting a loading bar (charts, the diagnosis scan).
   Slow soft luminance pulse across a gradient. No sweep.
   ============================================================ */
.ambient-drift {
  position: relative;
  background-color: var(--skeleton-base);
  background-image: linear-gradient(
    var(--shimmer-angle),
    var(--skeleton-base)      0%,
    var(--skeleton-highlight) 50%,
    var(--skeleton-base)      100%
  );
  background-size: 250% 100%;
  background-position: 0% 0%;
  background-repeat: no-repeat;
  animation: ambient-drift var(--ambient-drift-duration) ease-in-out infinite alternate;
}
@keyframes ambient-drift {
  0%   { background-position: 0%   0%; }
  100% { background-position: 100% 0%; }
}

/* ============================================================
   Stalled affordance — appears via skeleton.js after ~1200ms.
   Keeps the spatial box but switches the message to honest
   "still loading" copy. No endless shimmer.
   ============================================================ */
.skl-stalled {
  font-size: 11px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text3);
  text-align: center;
  padding: 12px 0;
  opacity: 0;
  transition: opacity 200ms ease-out;
}
.skl-stage[data-stalled="true"] .skl-stalled { opacity: 1; }

/* ============================================================
   Reduced motion — disable shimmer, disable spring, keep a
   plain 180ms opacity fade.
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  .skl,
  .skl-text > .skl-line,
  .skl-list > .skl-row,
  .ambient-drift {
    animation: none !important;
    background-image: none !important;
    background-color: var(--skeleton-base);
  }
  .skl-stage > .skl-layer--skeleton,
  .skl-stage > .skl-layer--content {
    transition: opacity 180ms ease !important;
    transform: none !important;
    filter: none !important;
  }
  .blur-up {
    transition: opacity 180ms ease !important;
    filter: none !important;
    transform: none !important;
  }
  .blur-up__img {
    transition: opacity 180ms ease !important;
  }
}
