/* ============================================================================
   Sauna voice orb — shared component
   ----------------------------------------------------------------------------
   Single source of truth for the floating, morphing, audio-reactive orb used by
   the three voice apps (sauna-interview, sauna-voiceplay, sauna-voicetest). This
   file owns the orb's *machinery*: geometry, the morph/float/drift/halo
   animations, the audio-reactive (`--level`) transforms, and the hover
   interaction. Everything that differs between apps is exposed as a CSS custom
   property ("argument") with a sensible default below — an app themes the orb
   purely by overriding these in its own stylesheet (loaded after this one).

   Markup the apps render (identical in all three):

     <button class="orb-float" id="orbBtn">
       <span class="orb-core" id="orbCore">
         <span class="orb-glow" id="orbGlow"></span>
         <span class="orb" id="orb"></span>
       </span>
       <span class="orb-label" id="orbLabel">tap me</span>
     </button>

   JS drives liveness by setting two things from the app:
     - `--level` (0..1 audio amplitude) on #orb and #orbGlow, every animation frame
     - `data-speaker` / `data-state` / `.is-open` / `.is-paused` on <body>
   The per-state size/colour choreography (which state maps to which size, hue,
   etc.) stays in each app — it's product behaviour, not orb machinery — and it
   composes with this file by setting `--birth` and the theme vars.

   See README.md for the full list of arguments.

   Note: this file is symlinked into each app's public dir as orb.css, and the
   deploy workflow maps a change here to a redeploy of every app that links it.
   ========================================================================== */

:root {
  /* live audio amplitude 0..1, written from JS each frame onto #orb/#orbGlow */
  --level: 0;

  /* ── Geometry ─────────────────────────────────────────── */
  --orb-float-size: 280px;     /* tap target / float frame */
  --orb-float-margin: 0;       /* nudge the orb within its frame */
  --orb-float-dur: 7s;         /* float bob period */
  --orb-bob: -16px;            /* float bob distance (peak) */
  --orb-size: 188px;           /* the blob itself */
  --orb-glow-size: 280px;      /* the halo */
  --orb-glow-blur: 30px;

  /* ── Audio reactivity ─────────────────────────────────── */
  --orb-level-scale: 0.42;     /* blob scale gain per unit --level */
  --orb-glow-level: 0.5;       /* halo scale gain per unit --level */
  --orb-glow-op-base: 0.55;    /* halo opacity floor */
  --orb-glow-op-gain: 0.6;     /* halo opacity gain per unit --level */

  /* ── Birth (base size before the --level pulse rides on top) ── */
  --orb-birth: 0.62;

  /* ── Colour / theme ───────────────────────────────────── */
  --glow: #96aaff;             /* single tint the colour slots are derived from */
  --orb-surface:
    radial-gradient(circle at 34% 28%, #ffffff 0%,
      color-mix(in srgb, var(--glow) 12%, #ffffff) 22%,
      color-mix(in srgb, var(--glow) 55%, #ffffff) 50%,
      var(--glow) 82%,
      color-mix(in srgb, var(--glow) 80%, #000) 100%);
  --orb-shadow:
    inset 0 0 46px rgba(255, 255, 255, 0.55),
    inset -16px -20px 56px color-mix(in srgb, var(--glow) 50%, #000),
    0 0 calc(46px + var(--level) * 80px) color-mix(in srgb, var(--glow) 60%, transparent),
    0 0 calc(110px + var(--level) * 130px) color-mix(in srgb, var(--glow) 38%, transparent);
  --orb-glow-bg:
    radial-gradient(circle,
      color-mix(in srgb, var(--glow) 55%, transparent),
      color-mix(in srgb, var(--glow) 22%, transparent) 45%, transparent 70%);
  --orb-glow-anim: halo 6s ease-in-out infinite;
  --orb-sheen-1: radial-gradient(circle at 65% 70%, rgba(255, 255, 255, 0.7), transparent 55%);
  --orb-sheen-2: radial-gradient(circle at 30% 75%, color-mix(in srgb, var(--glow) 70%, #fff), transparent 55%);

  /* ── Label ────────────────────────────────────────────── */
  --orb-label-color: #1d1030;
  --orb-label-size: 18px;
  --orb-label-weight: 700;
  --orb-label-spacing: 0.04em;
  --orb-label-line: 1.12;
  --orb-label-shadow: 0 1px 8px rgba(255, 255, 255, 0.45);
  --orb-label-anim: labelBreathe 3.2s ease-in-out infinite;

  /* ── Hover (the subtle "alive on hover" interaction) ──────
     Both compose with --level and the current speaker state rather than
     overriding them. Apps can re-tune per speaker (see the bottom of this file
     for the data-speaker defaults). */
  --orb-hover-lift: 0.08;       /* extra birth scale added to the core on hover */
  --orb-hover-glow: 0.22;       /* extra halo opacity on hover */
  --orb-hover-glow-scale: 0.1;  /* extra halo scale on hover */
  --orb-hover-bright: 1.08;     /* brightness applied to the blob on hover */
  --orb-hover-sat: 1.1;         /* saturation applied to the blob on hover */
}

/* ── Float frame / tap target ──────────────────────────── */
.orb-float {
  appearance: none;
  border: none;
  background: none;
  cursor: pointer;
  pointer-events: auto;
  display: grid;
  place-items: center;
  width: var(--orb-float-size);
  height: var(--orb-float-size);
  margin: var(--orb-float-margin);
  animation: float var(--orb-float-dur) ease-in-out infinite;
  -webkit-tap-highlight-color: transparent;
}
@keyframes float {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(var(--orb-bob)); }
}

/* Birth scale lives here so it composes cleanly with the orb's audio-reactive
   scale; --orb-hover rides on top of --birth so hover never clobbers the
   per-speaker filters set on .orb-core elsewhere. */
.orb-core {
  grid-area: 1 / 1;
  display: grid;
  place-items: center;
  --birth: var(--orb-birth);
  --orb-hover: 0;
  transform: scale(calc(var(--birth) + var(--orb-hover)));
  transition: transform 0.55s cubic-bezier(0.2, 0.9, 0.25, 1.2), filter 0.45s ease;
}

/* Halo — soft bloom that swells with the voice. */
.orb-glow {
  grid-area: 1 / 1;
  width: var(--orb-glow-size);
  height: var(--orb-glow-size);
  border-radius: 50%;
  background: var(--orb-glow-bg);
  filter: blur(var(--orb-glow-blur));
  --orb-glow-hover: 0;
  --orb-glow-hover-scale: 0;
  opacity: calc(var(--orb-glow-op-base) + var(--level) * var(--orb-glow-op-gain) + var(--orb-glow-hover));
  transform: scale(calc(1 + var(--level) * var(--orb-glow-level) + var(--orb-glow-hover-scale)));
  transition: opacity 0.12s ease-out, transform 0.1s ease-out, background 0.4s ease;
  animation: var(--orb-glow-anim);
  pointer-events: none;
}
@keyframes halo {
  0%, 100% { filter: blur(var(--orb-glow-blur)); }
  50% { filter: blur(calc(var(--orb-glow-blur) + 10px)); }
}

/* The body of the orb — liquid morphing blob. */
.orb {
  grid-area: 1 / 1;
  position: relative;
  width: var(--orb-size);
  height: var(--orb-size);
  border-radius: 60% 40% 55% 45% / 55% 50% 50% 45%;
  background: var(--orb-surface);
  box-shadow: var(--orb-shadow);
  transform: scale(calc(1 + var(--level) * var(--orb-level-scale)));
  transition: transform 0.08s ease-out, box-shadow 0.12s ease-out, background 0.4s ease, filter 0.3s ease;
  animation: morph 9s ease-in-out infinite;
  will-change: transform, border-radius;
}
@keyframes morph {
  0%, 100% { border-radius: 60% 40% 55% 45% / 55% 50% 50% 45%; }
  25% { border-radius: 50% 50% 42% 58% / 48% 56% 44% 52%; }
  50% { border-radius: 44% 56% 58% 42% / 56% 46% 54% 44%; }
  75% { border-radius: 56% 44% 48% 52% / 44% 52% 48% 56%; }
}

/* "Thinking" (tapped, before the agent's first word): the small orb churns its
   shape faster and pulses, so it reads as actively thinking hard rather than
   sitting frozen. The pulse rides on .orb (its --level transform is ~0 while no
   one is speaking yet); the per-state birth on .orb-core still sets the size.
   Stops the moment the agent speaks (is-open), back to the calm 9s morph. */
@keyframes thinkPulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.1); } }
body[data-state="connecting"] .orb,
body[data-state="call"]:not(.is-open) .orb {
  animation: morph 2.6s ease-in-out infinite, thinkPulse 1.1s ease-in-out infinite;
}

/* Two slow liquid sheens drifting over the surface. Set --orb-sheen-1/2 to
   `none` to disable (sauna-voicetest renders a cleaner, sheen-less orb). */
.orb::before,
.orb::after {
  content: "";
  position: absolute;
  inset: -8%;
  border-radius: inherit;
  mix-blend-mode: screen;
  pointer-events: none;
}
.orb::before {
  background: var(--orb-sheen-1);
  animation: drift 8s ease-in-out infinite;
}
.orb::after {
  background: var(--orb-sheen-2);
  animation: drift 12s ease-in-out infinite reverse;
}
@keyframes drift {
  0%, 100% { transform: rotate(0deg) scale(1); }
  50% { transform: rotate(180deg) scale(1.08); }
}

/* Label centred over the blob — sits outside .orb-core so it stays readable
   while the orb is small. Apps override font/colour via the --orb-label-* vars;
   per-state labels (paused text, the sealed checkmark, etc.) are set directly
   by each app and win on specificity. */
.orb-label {
  grid-area: 1 / 1;
  z-index: 1;
  color: var(--orb-label-color);
  font-size: var(--orb-label-size);
  font-weight: var(--orb-label-weight);
  line-height: var(--orb-label-line);
  letter-spacing: var(--orb-label-spacing);
  text-align: center;
  white-space: pre-line;        /* render "\n" in labels as a line break */
  text-shadow: var(--orb-label-shadow);
  pointer-events: none;
  animation: var(--orb-label-anim);
}
@keyframes labelBreathe {
  0%, 100% { opacity: 0.55; transform: translateY(0.5px); }
  50% { opacity: 0.95; transform: translateY(-1px); }
}

/* ── Hover ───────────────────────────────────────────────
   A subtle "the orb notices you" lift: the core swells a hair and the halo
   blooms a little. Scoped to real hover-capable pointers so touch devices never
   get a stuck hover state. The magnitude is re-tuned per speaker below, so the
   feel of the hover shifts with who's talking. */
@media (hover: hover) and (pointer: fine) {
  .orb-float:hover .orb-core { --orb-hover: var(--orb-hover-lift); }
  .orb-float:hover .orb-glow {
    --orb-glow-hover: var(--orb-hover-glow);
    --orb-glow-hover-scale: var(--orb-hover-glow-scale);
  }
  /* A directly-transitioned cue on the blob itself so the hover always reads,
     even where the custom-property-driven scale animates subtly. Speaker filters
     live on .orb-core, so this composes rather than clobbers them. */
  .orb-float:hover .orb { filter: brightness(var(--orb-hover-bright)) saturate(var(--orb-hover-sat)); }
}

/* Per-speaker hover character. Set on .orb-float so the :hover rules above pick
   up the right --orb-hover-* values. Apps can override any of these.
   - listening / idle: a bigger, more inviting swell ("tap me")
   - the agent speaking (sauna / model): smaller lift, but the halo blooms more
   - you speaking (user): a lively, balanced nudge */
body[data-speaker="idle"] .orb-float,
body[data-state="idle"] .orb-float,
body[data-speaker="listening"] .orb-float { --orb-hover-lift: 0.12; --orb-hover-glow: 0.26; --orb-hover-glow-scale: 0.12; }
body[data-speaker="thinking"] .orb-float  { --orb-hover-lift: -0.03; --orb-hover-glow: 0.04; --orb-hover-glow-scale: 0; }
body[data-speaker="sauna"] .orb-float,
body[data-speaker="model"] .orb-float     { --orb-hover-lift: 0.07; --orb-hover-glow: 0.30; --orb-hover-glow-scale: 0.14; }
body[data-speaker="user"] .orb-float      { --orb-hover-lift: 0.10; --orb-hover-glow: 0.24; }

/* Pre-first-word "thinking" (tapped, waiting on the agent): a hover here should
   settle the orb a touch SMALLER, never lift it — otherwise hovering on click
   fights the shrink and the orb won't drop. Placed after the idle rule so it
   wins when data-speaker is still "idle" during the connecting window. */
body[data-state="connecting"] .orb-float,
body[data-state="call"]:not(.is-open) .orb-float { --orb-hover-lift: -0.03; --orb-hover-glow: 0.04; --orb-hover-glow-scale: 0; }
