/* Ghost orb + voice call-to-action.
 *
 * The voice state lives on <html data-voice="..."> (set by voice.js) and every
 * orb on the page reacts to it -- hero, login and the small chat indicator --
 * so state is conveyed by light and motion, never by a spinner:
 *
 *   idle        slow breathing, drifting rings
 *   connecting  rings pull in and turn faster (short, then it resolves)
 *   listening   two soft ripples leave the orb
 *   working     rings counter-rotate, tighter and deeper (focus, not a spinner)
 *   speaking    the whole orb pulses in an irregular, speech-like rhythm
 *   approval    steady warm light, one calm pulse -- a decision is waiting
 *   error       dimmed and cool, no blinking
 *
 * Motion is CSS-only (transform / opacity), no canvas, no WebGL. State is
 * driven by the app's state, not by audio amplitude. Reduced motion keeps
 * every state distinguishable through static colour and brightness.
 *
 * The Ghost character is a placed asset, never redrawn. `.orb__figure` is the
 * one media slot: presence.js mounts the current state's media into it (still
 * image today, motion clips later) and sets data-presence-source on the orb
 * (motion | still | none). With `none` the CSS ring orb (the orb of the
 * desktop/mobile references) is shown instead. See docs/CHARACTER_ASSETS.md.
 *
 * The light is one animatable colour: --orb-color / --orb-color-2 are
 * registered <color> properties, so halo, horizon, glass edges and the ambient
 * wash all cross-fade together when the state changes instead of snapping.
 */

@property --orb-color {
  syntax: '<color>';
  inherits: true;
  initial-value: #3d8bff;
}

@property --orb-color-2 {
  syntax: '<color>';
  inherits: true;
  initial-value: #5fd4ff;
}

:root {
  --orb-color: var(--light-blue);
  --orb-color-2: var(--light-cyan);
  --light-indigo-rgb: 111, 125, 255;
  --ring-speed: 40s;
  --halo-speed: 7s;
  transition: --orb-color 800ms var(--ease-out), --orb-color-2 800ms var(--ease-out);
}

:root[data-voice='connecting'] {
  --orb-color-2: var(--light-cyan);
  --ring-speed: 3.4s;
  --halo-speed: 1.3s;
}

:root[data-voice='listening'] {
  --orb-color: var(--light-cyan);
  --ring-speed: 28s;
  --halo-speed: 3.6s;
}

:root[data-voice='working'] {
  --orb-color: var(--light-indigo);
  --orb-color-2: var(--light-violet);
  --ring-speed: 7s;
  --halo-speed: 2.8s;
}

:root[data-voice='speaking'] {
  --orb-color: var(--light-cyan);
  --ring-speed: 14s;
  --halo-speed: 1.6s;
}

:root[data-voice='approval_required'] {
  --orb-color: var(--warn);
  --orb-color-2: var(--warn);
  --ring-speed: 110s;
  --halo-speed: 4.6s;
}

:root[data-voice='error'] {
  --orb-color: var(--error);
  --orb-color-2: var(--error);
  --ring-speed: 140s;
  --halo-speed: 10s;
}

/* --- The orb ----------------------------------------------------------- */

/* Size comes from the surrounding scene (--orb-size on .hero); the hero, the
   mini indicator and the login orb each set their own. */
.orb {
  position: relative;
  display: grid;
  place-items: center;
  flex: none;
  width: var(--orb-size, min(76vw, 32dvh, 20rem));
  aspect-ratio: 1;
  cursor: pointer;
  -webkit-user-select: none;
  user-select: none;
}

.orb > * {
  grid-area: 1 / 1;
}

.orb__halo {
  width: 178%;
  height: 178%;
  border-radius: 50%;
  pointer-events: none;
  background: radial-gradient(
    closest-side,
    color-mix(in srgb, var(--orb-color) 40%, transparent),
    color-mix(in srgb, var(--orb-color) 14%, transparent) 46%,
    color-mix(in srgb, var(--orb-color) 4%, transparent) 70%,
    transparent 100%
  );
  animation: orb-breathe var(--halo-speed) var(--ease-ambient) infinite alternate;
}

.orb__ripple {
  width: 84%;
  height: 84%;
  border-radius: 50%;
  pointer-events: none;
  opacity: 0;
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--orb-color-2) 32%, transparent), 0 0 34px color-mix(in srgb, var(--orb-color) 30%, transparent), inset 0 0 30px color-mix(in srgb, var(--orb-color) 12%, transparent);
}

.orb__ripple--late {
  animation-delay: 1.4s;
}

/* CSS ring orb -- two tilted, lit rings. The orb of the references. */
.orb__rings {
  position: relative;
  width: 74%;
  height: 74%;
  pointer-events: none;
}

.orb__rings::before {
  content: '';
  position: absolute;
  inset: -8%;
  border-radius: 50%;
  background: radial-gradient(closest-side, color-mix(in srgb, var(--orb-color) 20%, transparent), color-mix(in srgb, var(--orb-color) 8%, transparent) 62%, transparent);
}

.orb__ring {
  position: absolute;
  inset: 0;
}

.orb__ring i {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: conic-gradient(
    from 0deg,
    color-mix(in srgb, var(--orb-color-2) 95%, transparent),
    color-mix(in srgb, var(--orb-color) 32%, transparent) 24%,
    rgba(var(--light-violet-rgb), 0.72) 47%,
    color-mix(in srgb, var(--orb-color) 28%, transparent) 72%,
    color-mix(in srgb, var(--orb-color-2) 95%, transparent)
  );
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - var(--ring-w)), #000 calc(100% - var(--ring-w) + 1px));
  mask: radial-gradient(farthest-side, transparent calc(100% - var(--ring-w)), #000 calc(100% - var(--ring-w) + 1px));
  animation: orb-spin var(--ring-speed) linear infinite;
}

.orb__ring--a {
  --ring-w: 15%;
  transform: translate(7%, -6%) rotate(-24deg) scale(1, 0.84);
}

/* A little glass inside the ring, so it reads as a lens rather than a wire. */
.orb__ring--a::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: radial-gradient(closest-side, transparent 58%, color-mix(in srgb, var(--orb-color) 16%, transparent) 88%, color-mix(in srgb, var(--orb-color-2) 22%, transparent));
}

.orb__ring--b {
  --ring-w: 9%;
  inset: 8%;
  opacity: 0.85;
  transform: translate(-7%, 7%) rotate(32deg) scale(1, 0.9);
}

.orb__ring--b i {
  animation-direction: reverse;
}

/* The Ghost character slot: placed, masked and lit -- never redrawn. */
.orb__figure {
  position: relative;
  width: 100%;
  height: 100%;
  mix-blend-mode: screen;
  pointer-events: none;
  transition: filter var(--dur-3) var(--ease-out);
}

/* A still on a near-black ground is blended into the night and edge-faded;
   media with a real alpha channel (blend: normal in the manifest) is shown as is. */
.orb__figure[data-slot] {
  -webkit-mask-image: radial-gradient(closest-side, #000 74%, transparent 100%);
  mask-image: radial-gradient(closest-side, #000 74%, transparent 100%);
}

:root[data-presence-blend='normal'] .orb__figure {
  mix-blend-mode: normal;
  -webkit-mask-image: none;
  mask-image: none;
}

.orb__media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  opacity: 0;
  transition: opacity 260ms var(--ease-out);
  -webkit-user-drag: none;
  user-select: none;
}

.orb__media[data-state='ready'],
.orb__media[data-state='covered'] {
  opacity: 1;
}

/* A replaced clip stays fully opaque under the incoming one until it is removed:
   crossfading both would dim the figure halfway through. */
.orb__media[data-state='covered'] {
  transition: none;
}

/* A still is animated procedurally (float / talk); real motion media brings its own. */
.orb[data-presence-source='still'] .orb__media {
  animation: orb-float 6.5s var(--ease-ambient) infinite alternate;
}

.orb[data-presence-source='motion'] .orb__media {
  animation: none;
}

/* The CSS ring orb is the fallback: it steps aside as soon as any media is present. */
.orb[data-presence-source='still'] .orb__rings,
.orb[data-presence-source='motion'] .orb__rings {
  display: none;
}

/* --- State motion ------------------------------------------------------ */

:root[data-voice='listening'] .orb__ripple,
:root[data-voice='speaking'] .orb__ripple {
  animation: orb-ripple 2.8s var(--ease-out) infinite;
}

:root[data-voice='speaking'] .orb__ripple {
  animation-duration: 1.9s;
}

:root[data-voice='speaking'] .orb__ripple--late {
  animation-delay: 0.95s;
}

:root[data-voice='speaking'] .orb[data-presence-source='still'] .orb__media,
:root[data-voice='speaking'] .orb__rings {
  animation: orb-talk 1.7s var(--ease-ambient) infinite;
}

:root[data-voice='connecting'] .orb__rings,
:root[data-voice='connecting'] .orb[data-presence-source='still'] .orb__media {
  animation: orb-gather 1.3s var(--ease-ambient) infinite alternate;
}

:root[data-voice='working'] .orb__rings {
  transform: scale(0.93);
  transition: transform var(--dur-3) var(--ease-out);
}

:root[data-voice='working'] .orb[data-presence-source='still'] .orb__figure {
  filter: hue-rotate(14deg) brightness(0.92);
}

:root[data-voice='approval_required'] .orb[data-presence-source='still'] .orb__figure {
  filter: sepia(0.55) saturate(1.5) hue-rotate(-10deg) brightness(0.95);
}

:root[data-voice='approval_required'] .orb__ripple {
  animation: orb-ripple 4.6s var(--ease-out) infinite;
}

:root[data-voice='approval_required'] .orb__ripple--late {
  display: none;
}

/* Motion clips bring their own look; the dimming is for the ring orb and the still. */
:root[data-voice='error'] .orb:not([data-presence-source='motion']) {
  filter: saturate(0.55) brightness(0.78);
}

:root[data-voice='error'] .orb__media,
:root[data-voice='error'] .orb__halo {
  animation-play-state: paused;
}

@keyframes orb-breathe {
  from { transform: scale(0.94); opacity: 0.78; }
  to { transform: scale(1.06); opacity: 1; }
}

@keyframes orb-float {
  from { transform: translateY(1.5%) scale(0.985); }
  to { transform: translateY(-1.5%) scale(1.01); }
}

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

@keyframes orb-ripple {
  0% { transform: scale(0.72); opacity: 0; }
  18% { opacity: 0.4; }
  100% { transform: scale(1.4); opacity: 0; }
}

@keyframes orb-talk {
  0%, 100% { transform: scale(1); }
  16% { transform: scale(1.045); }
  30% { transform: scale(1.005); }
  52% { transform: scale(1.06); }
  70% { transform: scale(1.015); }
  84% { transform: scale(1.035); }
}

@keyframes orb-gather {
  from { transform: scale(0.94); }
  to { transform: scale(1.02); }
}

/* Small orb: the chat header indicator. Rings + halo only, no figure. */
.orb--mini {
  --orb-size: 2.5rem;
  cursor: inherit;
}

.orb--mini .orb__halo {
  width: 220%;
  height: 220%;
}

.orb--mini .orb__rings {
  width: 78%;
  height: 78%;
}

.orb--mini .orb__ring--a {
  --ring-w: 20%;
  transform: none;
}

.orb--login {
  --orb-size: min(52vw, 15rem);
  cursor: default;
}

/* --- Voice status line + call to action -------------------------------- */

.voice-panel {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-4);
  width: 100%;
}

.voice-status {
  min-height: 1.5em;
  max-width: 24rem;
  text-align: center;
  color: var(--ink-2);
  font-size: var(--fs-body);
  letter-spacing: 0.02em;
  text-wrap: balance;
}

.voice-status[data-state='error'] {
  color: var(--error);
  letter-spacing: 0.01em;
}

.voice-status[data-state='approval_required'] {
  color: var(--warn);
  letter-spacing: 0.01em;
}

.voice-cta {
  --cta-rgb: var(--light-blue-rgb);
  position: relative;
  display: grid;
  grid-template-columns: 3.25rem 1fr 3.25rem;
  align-items: center;
  width: min(100%, 23rem);
  min-height: 4.5rem;
  padding: 0.5rem;
  overflow: hidden;
  border-radius: var(--r-pill);
  background: linear-gradient(180deg, rgba(var(--cta-rgb), 0.2), rgba(var(--cta-rgb), 0.08)), var(--night-2);
  box-shadow:
    inset 0 0 0 1px rgba(var(--cta-rgb), 0.42),
    inset 0 1px 0 var(--highlight-top),
    0 0 28px rgba(var(--cta-rgb), 0.14),
    var(--shadow-1);
  color: var(--ink-1);
  font-size: var(--fs-lead);
  font-weight: 400;
  letter-spacing: 0.01em;
  transition: box-shadow var(--dur-3) var(--ease-out), background var(--dur-3) var(--ease-out), transform var(--dur-1) var(--ease-out);
}

/* A large control presses less than a small one. */
.voice-cta:active {
  transform: scale(0.985);
}

.voice-cta__label {
  text-align: center;
}

.voice-cta__bars {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 3px;
  height: 1.75rem;
  color: var(--light-cyan);
}

.voice-cta__bars i {
  width: 3px;
  height: 100%;
  border-radius: 2px;
  background: currentColor;
  box-shadow: 0 0 8px rgba(var(--light-cyan-rgb), 0.55);
  transform: scaleY(var(--h, 0.4));
}

.voice-cta__bars i:nth-child(1) { --h: 0.3; }
.voice-cta__bars i:nth-child(2) { --h: 0.6; }
.voice-cta__bars i:nth-child(3) { --h: 1; }
.voice-cta__bars i:nth-child(4) { --h: 0.55; }
.voice-cta__bars i:nth-child(5) { --h: 0.28; }

.voice-cta__mic {
  display: grid;
  place-items: center;
  width: 3.25rem;
  height: 3.25rem;
  border-radius: 50%;
  background: rgba(112, 164, 255, 0.14);
  box-shadow: inset 0 1px 0 var(--highlight-top);
  transition: background var(--dur-2) var(--ease-out), color var(--dur-2) var(--ease-out), box-shadow var(--dur-3) var(--ease-out);
}

.voice-cta__mic .icon {
  grid-area: 1 / 1;
  width: 1.4rem;
  height: 1.4rem;
}

.voice-cta .icon--stop {
  display: none;
}

/* A running session: the mic becomes a lit stop button. */
.voice-cta:is([data-state='connecting'], [data-state='listening'], [data-state='working'], [data-state='speaking'], [data-state='approval_required']) .icon--mic {
  display: none;
}

.voice-cta:is([data-state='connecting'], [data-state='listening'], [data-state='working'], [data-state='speaking'], [data-state='approval_required']) .icon--stop {
  display: block;
}

.voice-cta:is([data-state='connecting'], [data-state='listening'], [data-state='working'], [data-state='speaking']) {
  --cta-rgb: var(--light-cyan-rgb);
}

.voice-cta:is([data-state='connecting'], [data-state='listening'], [data-state='working'], [data-state='speaking']) .voice-cta__mic {
  background: var(--light-cyan);
  color: var(--ink-on-light);
  box-shadow: 0 0 22px rgba(var(--light-cyan-rgb), 0.6);
}

.voice-cta[data-state='working'] {
  --cta-rgb: var(--light-indigo-rgb);
}

.voice-cta[data-state='approval_required'] {
  --cta-rgb: var(--warn-rgb);
}

.voice-cta[data-state='approval_required'] .voice-cta__bars {
  color: var(--warn);
}

.voice-cta[data-state='approval_required'] .voice-cta__mic {
  background: rgba(var(--warn-rgb), 0.2);
  color: var(--warn);
}

.voice-cta[data-state='error'] {
  --cta-rgb: var(--error-rgb);
}

.voice-cta[data-state='error'] .voice-cta__bars {
  color: var(--error);
  opacity: 0.7;
}

/* Bars: the CTA's own meter. Listening/speaking move like a voice; connecting
   and working sweep left to right, slower and more focused. */
.voice-cta[data-state='listening'] .voice-cta__bars i,
.voice-cta[data-state='speaking'] .voice-cta__bars i {
  animation: cta-bar 0.9s var(--ease-ambient) infinite alternate;
}

.voice-cta[data-state='speaking'] .voice-cta__bars i {
  animation-duration: 0.55s;
}

.voice-cta[data-state='connecting'] .voice-cta__bars i,
.voice-cta[data-state='working'] .voice-cta__bars i {
  animation: cta-sweep 1.6s var(--ease-ambient) infinite;
}

.voice-cta__bars i:nth-child(2) { animation-delay: 0.12s !important; }
.voice-cta__bars i:nth-child(3) { animation-delay: 0.24s !important; }
.voice-cta__bars i:nth-child(4) { animation-delay: 0.36s !important; }
.voice-cta__bars i:nth-child(5) { animation-delay: 0.48s !important; }

@keyframes cta-bar {
  from { transform: scaleY(0.25); }
  to { transform: scaleY(1); }
}

@keyframes cta-sweep {
  0%, 100% { transform: scaleY(0.3); opacity: 0.55; }
  40% { transform: scaleY(1); opacity: 1; }
}

/* --- Reduced motion: static, but every state stays distinguishable ------ */

@media (prefers-reduced-motion: reduce) {
  :root[data-voice='listening'] .orb__ripple,
  :root[data-voice='speaking'] .orb__ripple {
    animation: none;
    opacity: 0.4;
    transform: scale(1.1);
  }

  :root[data-voice='approval_required'] .orb__ripple {
    animation: none;
    opacity: 0.35;
    transform: scale(1.08);
  }
}

/* --- Hover: only on devices that really hover (touch taps must not stick) --- */

@media (hover: hover) and (pointer: fine) {
  .voice-cta:hover {
    background: linear-gradient(180deg, rgba(var(--cta-rgb), 0.28), rgba(var(--cta-rgb), 0.12)), var(--night-2);
  }
}
