/* ==========================================================================
   PRODUCT SYSTEM PRIMITIVES (R376)
   ==========================================================================
   ADDITIVE ONLY. Every rule below is scoped to a `ps-` class that does not
   exist anywhere in the estate today, verified before writing. Nothing here
   overrides an existing selector, so linking this file cannot change how any
   current page renders. That constraint is deliberate: the R375 inventory
   found 16,265 inline styles across 179 pages, and a stylesheet that
   competed with them would break pages nobody can visually re-verify here.

   Token roles are declared with fallbacks so this file works on a page that
   defines none of them, and defers to the page where they exist.
   ========================================================================== */

:root {
  --ps-ground: var(--void, #05060B);
  --ps-surface: var(--field, #0A0D14);
  --ps-hairline: var(--edge, rgba(220, 230, 240, .14));
  --ps-ink: var(--mist, #DCE6F0);
  --ps-ink-muted: var(--haze, #7E8A9C);
  --ps-ink-faint: var(--dim, #5B657A);
  --ps-accent: var(--cyan, #7ADCEC);
  --ps-caution: #E0A552;
  --ps-positive: #2FBF7B;

  --ps-control-min: 44px;
  --ps-motion-fast: 120ms;
  --ps-motion-base: 200ms;
  --ps-ease: cubic-bezier(.2, 0, 0, 1);
  --ps-radius: 0;
}

/* --------------------------------------------------------------------------
   SKELETON
   Shape-matched, not a generic grey box. The shimmer is a background-position
   animation on a gradient, which compositors handle on the GPU; animating
   width or left would force layout on every frame.
   -------------------------------------------------------------------------- */
.ps-skel {
  background: linear-gradient(
    90deg,
    var(--ps-surface) 0%,
    rgba(220, 230, 240, .07) 40%,
    var(--ps-surface) 80%
  );
  background-size: 300% 100%;
  animation: ps-shimmer 1.4s var(--ps-ease) infinite;
  border-radius: var(--ps-radius);
  /* A skeleton must reserve the space its content will occupy, or it trades
     a spinner for a layout shift, which is the worse of the two. */
  display: block;
}

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

.ps-skel-line { height: 12px; margin: 0 0 8px; }
.ps-skel-line.is-short { width: 42%; }
.ps-skel-line.is-mid   { width: 68%; }
.ps-skel-title { height: 20px; width: 55%; margin: 0 0 14px; }

.ps-skel-card {
  border: 1px solid var(--ps-hairline);
  padding: 16px;
  min-height: 118px;
  background: var(--ps-ground);
}

.ps-skel-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 1px;
  background: var(--ps-hairline);
  border: 1px solid var(--ps-hairline);
}

.ps-skel-row {
  display: grid;
  grid-template-columns: 2fr 1fr 1fr;
  gap: 12px;
  padding: 12px 0;
  border-bottom: 1px solid var(--ps-hairline);
}

/* The skeleton is decorative; a screen reader should hear the status text,
   not a description of grey rectangles. Pair with aria-live on the region. */
.ps-skel-wrap[aria-busy="false"] { display: none; }

/* --------------------------------------------------------------------------
   BUSY STATE
   A control that is working must say so, and must refuse a second click.
   -------------------------------------------------------------------------- */
.ps-busy,
[aria-busy="true"].ps-btn {
  position: relative;
  pointer-events: none;
  opacity: .72;
  cursor: progress;
}

.ps-busy::after {
  content: "";
  position: absolute;
  inset-inline-end: 10px;
  inset-block-start: 50%;
  width: 12px;
  height: 12px;
  margin-block-start: -6px;
  border: 2px solid currentColor;
  border-inline-end-color: transparent;
  border-radius: 50%;
  animation: ps-spin 640ms linear infinite;
}

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

/* --------------------------------------------------------------------------
   STATE BLOCKS
   Empty and error states that say something specific. A block with no
   message is worse than no block, so the copy lives in the markup.
   -------------------------------------------------------------------------- */
.ps-state {
  border: 1px solid var(--ps-hairline);
  padding: 22px 18px;
  text-align: start;
  color: var(--ps-ink-muted);
  font-size: 13.5px;
  line-height: 1.6;
}
.ps-state strong { display: block; color: var(--ps-ink); margin-block-end: 6px; font-weight: 600; }
.ps-state.is-error { border-inline-start: 2px solid var(--ps-caution); }
.ps-state .ps-retry {
  margin-block-start: 12px;
  min-height: var(--ps-control-min);
  padding: 10px 16px;
  background: transparent;
  color: var(--ps-accent);
  border: 1px solid currentColor;
  cursor: pointer;
  font: inherit;
}

/* --------------------------------------------------------------------------
   TOUCH AND FOCUS
   -------------------------------------------------------------------------- */
.ps-tap { min-height: var(--ps-control-min); min-width: var(--ps-control-min); }

.ps-focus:focus-visible,
.ps-btn:focus-visible,
.ps-retry:focus-visible {
  outline: 2px solid var(--ps-accent);
  outline-offset: 2px;
}

.ps-sr {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* --------------------------------------------------------------------------
   REDUCED MOTION
   Motion that conveys state is replaced by a static equivalent rather than
   removed, so the state is still communicated.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .ps-skel {
    animation: none;
    background: rgba(220, 230, 240, .06);
  }
  .ps-busy::after { animation: none; opacity: .8; }
  .ps-state, .ps-skel-card { transition: none; }
}
