/* ============================================================================
   AURA — Relay design language
   ============================================================================
   Canonical source. Served at /aura.css (signaling.js resolves a bare filename
   against web/, so operator.html and admin/index.html both just link "/aura.css").
   Two deliberate exceptions:
     • desktop/renderer/aura.css — a COPY, because the Electron renderer loads
       from file:// and must work with no server and no network.
     • mobile/join.html — INLINED, because that page is intentionally a single
       request (see the comment at the top of its script block: a failed or
       stale second request would leave a guest staring at an unstyled page).
   Change this file first, then mirror it into those two.

   ---------------------------------------------------------------------------
   PERFORMANCE CONTRACT — read before adding anything here
   ---------------------------------------------------------------------------
   This app's real job is pushing 30 frames a second through a GPU compositor
   into a virtual camera. Decoration must never compete with that. So:

   1. Animate ONLY transform and opacity. Both are handled on the compositor
      thread; anything else (blur radius, background-position, width, top)
      forces paint or layout on every frame.
   2. NO animated filter: blur(). The soft glow comes from radial-gradient,
      which is soft by construction and costs one cheap paint that is then
      cached as a layer. A 100px blur filter on a 900px element re-rasterises
      continuously and is one of the most expensive things a page can do.
   3. Everything pauses under `body.is-live`. Once a feed is running, the GPU
      belongs to the video pipeline. See the .is-live block near the bottom.
   4. backdrop-filter is used sparingly, and it is the reason for rule 3: a
      blurred backdrop re-renders whenever the thing behind it moves, so glass
      over a moving aura costs every frame. Pausing the aura makes it free again.
   ========================================================================= */

:root {
  /* --- ground ------------------------------------------------------------ */
  --bg:            #080910;
  --bg-raised:     #0d0f1a;
  --surface:       rgba(255, 255, 255, 0.045);
  --surface-solid: #141728;
  --border:        rgba(255, 255, 255, 0.09);
  --border-strong: rgba(255, 255, 255, 0.16);

  /* --- ink --------------------------------------------------------------- */
  --text:   #eef0f8;
  --muted:  #8f93ad;
  --faint:  #5c607d;

  /* --- aura spectrum ----------------------------------------------------- */
  --aura-violet: #7c5cff;
  --aura-cyan:   #22d3ee;
  --aura-rose:   #f472b6;
  --aura-mint:   #34d399;

  --accent:      var(--aura-violet);
  --accent-2:    var(--aura-cyan);
  --accent-ink:  #ffffff;

  /* --- status ------------------------------------------------------------ */
  --live:   #34d399;
  --warn:   #fbbf24;
  --danger: #fb7185;

  /* --- form -------------------------------------------------------------- */
  --r-sm: 10px;
  --r-md: 14px;
  --r-lg: 20px;
  --r-pill: 999px;

  --shadow-sm: 0 1px 2px rgba(0,0,0,.4);
  --shadow-md: 0 8px 24px -8px rgba(0,0,0,.6);
  --shadow-lg: 0 24px 64px -16px rgba(0,0,0,.7);
  --glow: 0 0 0 1px rgba(124,92,255,.35), 0 8px 32px -8px rgba(124,92,255,.45);

  --ease: cubic-bezier(.22,.61,.36,1);
}

/* ============================================================================
   1. THE AURA LAYER
   ---------------------------------------------------------------------------
   Markup (put it as the first child of <body>):

     <div class="aura" aria-hidden="true">
       <span class="orb orb-1"></span>
       <span class="orb orb-2"></span>
       <span class="orb orb-3"></span>
       <span class="orb orb-4"></span>
       <span class="aura-veil"></span>
     </div>

   aria-hidden because it is pure atmosphere — there is nothing here for a
   screen reader to announce, and announcing it would just be noise.
   ========================================================================= */

.aura {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;          /* never eat a click meant for the UI */
  background:
    radial-gradient(1200px 800px at 15% -10%, rgba(124,92,255,.16), transparent 60%),
    radial-gradient(1000px 700px at 90% 110%, rgba(34,211,238,.13), transparent 60%),
    var(--bg);
}

/* Each orb is a soft radial gradient — no blur filter anywhere. Sizes are in
   vmax so the composition holds from a phone up to an ultrawide. */
.aura .orb {
  position: absolute;
  display: block;
  border-radius: 50%;
  /* Promote to its own compositor layer up front, so the browser never has to
     decide mid-animation. */
  will-change: transform, opacity;
  transform: translate3d(0, 0, 0);
}

.aura .orb-1 {
  width: 62vmax; height: 62vmax;
  top: -22vmax; left: -14vmax;
  background: radial-gradient(circle at 50% 50%, rgba(124,92,255,.42), rgba(124,92,255,.10) 45%, transparent 70%);
  animation: drift-a 34s var(--ease) infinite alternate;
}
.aura .orb-2 {
  width: 54vmax; height: 54vmax;
  top: 12vmax; right: -20vmax;
  background: radial-gradient(circle at 50% 50%, rgba(34,211,238,.34), rgba(34,211,238,.08) 45%, transparent 70%);
  animation: drift-b 46s var(--ease) infinite alternate;
}
.aura .orb-3 {
  width: 46vmax; height: 46vmax;
  bottom: -20vmax; left: 22vmax;
  background: radial-gradient(circle at 50% 50%, rgba(244,114,182,.26), rgba(244,114,182,.06) 45%, transparent 70%);
  animation: drift-c 52s var(--ease) infinite alternate;
}
.aura .orb-4 {
  width: 38vmax; height: 38vmax;
  bottom: 4vmax; right: 14vmax;
  background: radial-gradient(circle at 50% 50%, rgba(52,211,153,.20), rgba(52,211,153,.05) 45%, transparent 70%);
  animation: drift-d 40s var(--ease) infinite alternate;
}

/* The "breeze": one very slow, very wide sweep of light across everything.
   Opacity-only, so it is free. This is what stops the background reading as a
   static gradient — the orbs move slowly enough that the eye needs a second
   cue to register the scene as alive. */
.aura .aura-veil {
  position: absolute;
  inset: -30%;
  display: block;
  background: conic-gradient(from 0deg at 50% 50%,
    transparent 0deg,
    rgba(124,92,255,.09) 60deg,
    transparent 140deg,
    rgba(34,211,238,.07) 220deg,
    transparent 320deg);
  will-change: transform;
  animation: breeze 90s linear infinite;
}

@keyframes drift-a {
  0%   { transform: translate3d(0,0,0) scale(1); }
  50%  { transform: translate3d(8vmax, 5vmax, 0) scale(1.08); }
  100% { transform: translate3d(3vmax, 11vmax, 0) scale(.96); }
}
@keyframes drift-b {
  0%   { transform: translate3d(0,0,0) scale(1.04); }
  50%  { transform: translate3d(-10vmax, 7vmax, 0) scale(.94); }
  100% { transform: translate3d(-4vmax, -6vmax, 0) scale(1.1); }
}
@keyframes drift-c {
  0%   { transform: translate3d(0,0,0) scale(.98); }
  50%  { transform: translate3d(12vmax, -6vmax, 0) scale(1.12); }
  100% { transform: translate3d(-5vmax, -2vmax, 0) scale(1); }
}
@keyframes drift-d {
  0%   { transform: translate3d(0,0,0) scale(1.06); }
  50%  { transform: translate3d(-7vmax, -9vmax, 0) scale(.92); }
  100% { transform: translate3d(6vmax, -3vmax, 0) scale(1.04); }
}
@keyframes breeze {
  to { transform: rotate(360deg); }
}

/* Everything that is not the aura sits above it.
 *
 * :where() is load-bearing, not decoration. Written as `body > *:not(.aura)`
 * this selector scores (0,1,1) — higher than a plain class like `.overlay`
 * (0,1,0) — so it silently WON against every page's own rules and forced
 * `position: relative` onto elements that had declared `position: fixed`.
 * Full-screen login overlays stopped being overlays and dropped into normal
 * document flow at the bottom of the page, under the app they were supposed
 * to be covering.
 *
 * :where() contributes ZERO specificity, so this becomes (0,0,1) — a floor
 * that any page rule overrides simply by existing. A blanket layout rule in a
 * shared stylesheet should never outrank the page it is helping. */
body > :where(:not(.aura)) { position: relative; z-index: 1; }

/* ============================================================================
   2. GLASS
   ========================================================================= */

.glass {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  backdrop-filter: blur(14px) saturate(1.4);
  -webkit-backdrop-filter: blur(14px) saturate(1.4);
  box-shadow: var(--shadow-md), inset 0 1px 0 rgba(255,255,255,.07);
}

/* A hairline of light along the top edge — the single detail that most makes
   a flat translucent panel read as glass rather than as a grey box. */
.glass-lit { position: relative; overflow: hidden; }
.glass-lit::before {
  content: '';
  position: absolute; inset: 0 0 auto 0; height: 1px;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,.5), transparent);
  opacity: .6;
}

/* ============================================================================
   3. CONTROLS
   ========================================================================= */

.btn-aura {
  position: relative;
  border: none;
  border-radius: var(--r-md);
  background: linear-gradient(135deg, var(--aura-violet), #5b8cff 55%, var(--aura-cyan));
  color: var(--accent-ink);
  font: inherit; font-weight: 600;
  cursor: pointer;
  overflow: hidden;
  box-shadow: var(--glow);
  transition: transform .18s var(--ease), box-shadow .18s var(--ease), filter .18s var(--ease);
}
.btn-aura:hover:not(:disabled) {
  transform: translateY(-1px);
  filter: brightness(1.07);
  box-shadow: 0 0 0 1px rgba(124,92,255,.5), 0 14px 40px -10px rgba(124,92,255,.6);
}
.btn-aura:active:not(:disabled) { transform: translateY(0); }
.btn-aura:disabled { opacity: .45; cursor: default; box-shadow: none; }

/* Sheen that crosses the button on hover. Transform-only. */
.btn-aura::after {
  content: '';
  position: absolute; inset: 0;
  background: linear-gradient(105deg, transparent 35%, rgba(255,255,255,.28) 50%, transparent 65%);
  transform: translateX(-120%);
  transition: transform .7s var(--ease);
}
.btn-aura:hover:not(:disabled)::after { transform: translateX(120%); }

.btn-ghost {
  background: rgba(255,255,255,.03);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  color: var(--text);
  font: inherit;
  cursor: pointer;
  transition: background .18s var(--ease), border-color .18s var(--ease), transform .18s var(--ease);
}
.btn-ghost:hover:not(:disabled) {
  background: rgba(255,255,255,.07);
  border-color: var(--border-strong);
  transform: translateY(-1px);
}
.btn-ghost:disabled { opacity: .45; cursor: default; }

.btn-danger {
  background: rgba(251,113,133,.08);
  border: 1px solid rgba(251,113,133,.4);
  color: var(--danger);
}
.btn-danger:hover:not(:disabled) { background: rgba(251,113,133,.16); }

.field {
  font: inherit;
  width: 100%;
  padding: 12px 14px;
  color: var(--text);
  background: rgba(0,0,0,.28);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  transition: border-color .18s var(--ease), box-shadow .18s var(--ease);
}
.field:focus {
  outline: none;
  border-color: rgba(124,92,255,.6);
  box-shadow: 0 0 0 3px rgba(124,92,255,.16);
}
.field::placeholder { color: var(--faint); }

/* Visible keyboard focus everywhere, without adding a ring to mouse clicks. */
:focus-visible { outline: 2px solid var(--accent-2); outline-offset: 2px; }

/* ============================================================================
   4. STATUS
   ========================================================================= */

.pill {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 12px; font-weight: 500;
  color: var(--muted);
  background: rgba(255,255,255,.04);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  padding: 5px 13px;
}
.pill.on { color: var(--live); border-color: rgba(52,211,153,.42); background: rgba(52,211,153,.08); }
.pill.warn { color: var(--warn); border-color: rgba(251,191,36,.42); background: rgba(251,191,36,.08); }

.dot {
  display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  background: var(--faint); flex: none;
}
.dot.live { background: var(--live); animation: pulse 2.4s var(--ease) infinite; }
.dot.connecting { background: var(--accent-2); animation: pulse 1.1s var(--ease) infinite; }
.dot.ended { background: var(--danger); }

/* box-shadow spread animates on the compositor as a paint-free ring here
   because the element is 8px — the rasterised area is negligible. */
@keyframes pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(52,211,153,.45); }
  70%      { box-shadow: 0 0 0 7px rgba(52,211,153,0); }
}

/* ============================================================================
   5. LIVE MODE — the performance switch
   ---------------------------------------------------------------------------
   Toggle `document.body.classList.toggle('is-live', connected)` when the feed
   starts and stops.

   Everything decorative stops moving. The aura stays visible (the room should
   not go flat and dead) but becomes a STATIC gradient, which means:
     • zero compositor work per frame for the orbs and the veil
     • every backdrop-filter behind glass has a still backdrop, so it
       rasterises once and is then cached instead of re-blurring continuously
   That headroom goes to the thing the operator actually came for.
   ========================================================================= */

body.is-live .aura .orb,
body.is-live .aura .aura-veil {
  animation-play-state: paused;
}
body.is-live .aura .orb { opacity: .45; }
body.is-live .aura .aura-veil { opacity: .3; }
body.is-live .btn-aura::after { display: none; }

/* Keep the live indicator itself alive — it is information, not decoration. */
body.is-live .dot.live { animation-play-state: running; }

/* ============================================================================
   6. REDUCED MOTION
   ---------------------------------------------------------------------------
   A drifting, breathing background is exactly the kind of thing that triggers
   vestibular discomfort. When the OS asks for less motion, the aura becomes a
   still image — the palette and depth survive, the movement does not.
   ========================================================================= */

@media (prefers-reduced-motion: reduce) {
  .aura .orb, .aura .aura-veil { animation: none !important; }
  .dot.live, .dot.connecting { animation: none !important; }
  .btn-aura::after { display: none; }
  *, *::before, *::after {
    transition-duration: .01ms !important;
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
  }
}

/* Small screens: fewer, larger orbs. Four moving layers on a phone GPU while
   it is also decoding video is not a trade worth making. */
@media (max-width: 640px) {
  .aura .orb-3, .aura .orb-4 { display: none; }
  .aura .aura-veil { animation-duration: 140s; }
}
