/* 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='ready'] {
  /* live, but at rest between turns: the listening colour, the calm tempo */
  --orb-color: var(--light-cyan);
  --ring-speed: 40s;
  --halo-speed: 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 ------------------------------------------------------ */

/* Speaking gives: its ripples leave the orb. Listening receives: its ripples
   contract toward Ghost (the same direction as its inflowing particles). */
:root[data-voice='speaking'] .orb__ripple {
  animation: orb-ripple 1.9s var(--ease-out) infinite;
}

:root[data-voice='listening'] .orb__ripple {
  animation: orb-ripple-in 3.2s var(--ease-ambient) infinite;
}

: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 and the rig dims itself (opacity, light);
   the filter is for the ring orb and the still only. */
:root[data-voice='error'] .orb:not([data-presence-source='motion']):not([data-presence-source='rig']) {
  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-ripple-in {
  0% { transform: scale(1.38); opacity: 0; }
  30% { opacity: 0.32; }
  100% { transform: scale(0.8); 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='working'] {
  color: var(--light-indigo);
}

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

/* Round voice launch control (Beta feedback 2026-09-29). One tap starts or ends a
   session -- nothing slides, so it is a disc, not a pill: a glass-dark circle with
   the mic in it and Ghost's cyan / blue / violet light in a ring around it, the
   state word underneath. The whole button (disc + word) is the tap target.
   State by geometry AND light, never by colour alone:
     idle        calm ring, no motion
     connecting  the ring "opens": two soft waves leave the disc
     listening   full cyan ring that breathes with what Ghost hears (--vc-level)
     ready       the ring at rest, dimmer -- live, nothing happening
     working     a violet arc circles the disc -- processing, not listening
     speaking    outward waves scaled by Ghost's voice (--vc-level)
     approval    steady amber ring; error a muted rose ring
   Only transform/opacity animate. --vc-level is written by voiceControlLevel.js
   (<= 20 Hz, only while listening/speaking, off under reduced motion). */
.voice-cta {
  --cta-rgb: var(--light-blue-rgb);
  --cta-rgb-2: var(--light-violet-rgb);
  --vc-level: 0;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-3);
  min-width: 11rem;
  padding: var(--s-2) var(--s-4) var(--s-1);
  border-radius: 1.5rem;
  background: none;
  box-shadow: none; /* the global button rim would outline the whole hit area */
  color: var(--ink-1);
  font-size: var(--fs-lead);
  font-weight: 400;
  letter-spacing: 0.01em;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

.voice-cta__disc {
  position: relative;
  display: grid;
  place-items: center;
  width: 5.25rem;
  height: 5.25rem;
  border-radius: 50%;
  /* dark glass body: a faint top light, an inner rim in the state colour */
  background:
    radial-gradient(circle at 50% 28%, rgba(var(--cta-rgb), 0.3), transparent 62%),
    linear-gradient(180deg, rgba(var(--cta-rgb), 0.16), rgba(var(--cta-rgb), 0.05)),
    var(--night-2);
  box-shadow:
    inset 0 0 0 1px rgba(var(--cta-rgb), 0.5),
    inset 0 1px 0 var(--highlight-top),
    0 0 26px rgba(var(--cta-rgb), 0.22),
    var(--shadow-1);
  transition: box-shadow var(--dur-3) var(--ease-out), background var(--dur-3) var(--ease-out), transform var(--dur-1) var(--ease-out);
}

.voice-cta:active .voice-cta__disc {
  transform: scale(0.96);
}

/* The light ring: a conic cyan -> blue -> violet edge, masked to a thin band. */
.voice-cta__ring {
  position: absolute;
  inset: -0.4rem;
  border-radius: 50%;
  pointer-events: none;
  background: conic-gradient(from 210deg, rgba(var(--light-cyan-rgb), 0.95), rgba(var(--light-blue-rgb), 0.85), rgba(var(--light-violet-rgb), 0.95), rgba(var(--light-cyan-rgb), 0.95));
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - var(--vc-ring-w, 3px) - 1px), #000 calc(100% - var(--vc-ring-w, 3px)));
  mask: radial-gradient(farthest-side, transparent calc(100% - var(--vc-ring-w, 3px) - 1px), #000 calc(100% - var(--vc-ring-w, 3px)));
  opacity: 0.55;
  transition: opacity var(--dur-3) var(--ease-out), transform var(--dur-2) var(--ease-out);
}

/* Waves: only shown while connecting / speaking. */
.voice-cta__wave {
  position: absolute;
  inset: -0.4rem;
  border-radius: 50%;
  pointer-events: none;
  border: 1.5px solid rgba(var(--cta-rgb), 0.7);
  opacity: 0;
}

.voice-cta__label {
  min-height: 1.5em;
  text-align: center;
  line-height: 1.4;
}

.voice-cta__mic {
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: 50%;
  color: var(--ink-1);
  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.6rem;
  height: 1.6rem;
}

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

/* A running session: the mic becomes a small lit stop button (a tap ends it). */
.voice-cta:is([data-state='connecting'], [data-state='listening'], [data-state='ready'], [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='ready'], [data-state='working'], [data-state='speaking'], [data-state='approval_required']) .icon--stop {
  display: block;
  width: 1.15rem;
  height: 1.15rem;
}

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

.voice-cta:is([data-state='connecting'], [data-state='listening'], [data-state='ready'], [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);
}

/* --- per state --- */

.voice-cta[data-state='connecting'] .voice-cta__ring {
  opacity: 0.8;
}

/* connecting: a hollow core -- "opening", not yet live */
.voice-cta[data-state='connecting'] .voice-cta__mic {
  background: transparent;
  color: var(--light-cyan);
  box-shadow: inset 0 0 0 2px rgba(var(--light-cyan-rgb), 0.85);
}

.voice-cta[data-state='connecting'] .voice-cta__wave {
  animation: cta-wave 1.6s var(--ease-out) infinite;
}

.voice-cta[data-state='listening'] .voice-cta__ring {
  opacity: calc(0.7 + var(--vc-level) * 0.3);
  transform: scale(calc(1 + var(--vc-level) * 0.1));
}

.voice-cta[data-state='listening'] .voice-cta__disc {
  box-shadow:
    inset 0 0 0 1px rgba(var(--cta-rgb), 0.6),
    inset 0 1px 0 var(--highlight-top),
    0 0 calc(24px + var(--vc-level) * 22px) rgba(var(--cta-rgb), calc(0.26 + var(--vc-level) * 0.3)),
    var(--shadow-1);
}

.voice-cta[data-state='ready'] .voice-cta__ring {
  opacity: 0.32;
}

.voice-cta[data-state='ready'] .voice-cta__mic {
  background: rgba(var(--light-cyan-rgb), 0.75);
  box-shadow: none;
}

/* working: the ring is a violet arc that circles -- clearly not listening's breath */
.voice-cta[data-state='working'] {
  --cta-rgb: var(--light-indigo-rgb);
}

.voice-cta[data-state='working'] .voice-cta__ring {
  background: conic-gradient(from 0deg, transparent 0 40%, rgba(var(--light-blue-rgb), 0.55) 55%, rgba(var(--light-violet-rgb), 1) 78%, transparent 82% 100%);
  --vc-ring-w: 4px;
  opacity: 1;
  animation: cta-orbit 2.4s linear infinite;
}

.voice-cta[data-state='working'] .voice-cta__mic {
  background: var(--light-indigo);
  color: #fff;
  box-shadow: 0 0 22px rgba(var(--light-indigo-rgb), 0.65);
}

.voice-cta[data-state='speaking'] .voice-cta__ring {
  opacity: 0.9;
  transform: scale(calc(1 + var(--vc-level) * 0.08));
}

/* speaking: concentric halos (static, so it reads in a still frame; the waves add the motion) */
.voice-cta[data-state='speaking'] .voice-cta__disc {
  box-shadow:
    inset 0 0 0 1px rgba(var(--cta-rgb), 0.6),
    inset 0 1px 0 var(--highlight-top),
    0 0 0 0.5rem rgba(var(--cta-rgb), 0.11),
    0 0 0 0.95rem rgba(var(--cta-rgb), 0.055),
    0 0 30px rgba(var(--cta-rgb), 0.3),
    var(--shadow-1);
}

.voice-cta[data-state='speaking'] .voice-cta__wave {
  opacity: calc(0.25 + var(--vc-level) * 0.55);
  animation: cta-wave 1.3s var(--ease-out) infinite;
}

.voice-cta[data-state='speaking'] .voice-cta__wave--late,
.voice-cta[data-state='connecting'] .voice-cta__wave--late {
  animation-delay: 0.65s;
}

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

.voice-cta[data-state='approval_required'] .voice-cta__ring {
  background: rgba(var(--warn-rgb), 0.9);
  opacity: 0.9;
}

.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__ring {
  background: rgba(var(--error-rgb), 0.7);
  opacity: 0.6;
}

@keyframes cta-wave {
  0% { opacity: 0.55; transform: scale(1); }
  100% { opacity: 0; transform: scale(1.55); }
}

@keyframes cta-orbit {
  to { transform: rotate(360deg); }
}

/* --- Ghost rig (motion PoC) ----------------------------------------------
 * The body is the approved idle loop without its face; the face is a live
 * canvas that ghostRig.js keeps on the body. JS writes exactly two transforms
 * per frame (.rig = body pose, .rig__face = the face on the body); everything
 * below is per-state light that CSS cross-fades. Deliberately no `filter` on
 * the playing video: the state light is the body's opacity and a rim in the
 * orb's own colour -- both compositor-cheap on a phone. */

.orb[data-presence-source='rig'] .orb__rings {
  display: none;
}

.orb[data-figure-state] {
  --rig-body: 0.96;
  --rig-rim: 0;
}

/* Per-state light (owner feedback 2026-09-23): ready is the base; the states
   differ by light, colour and particle flow (rigParticles.js), and the negative
   states are energetically reduced -- dimmer, paler, quieter. */
.orb[data-figure-state='idle'] { --rig-body: 0.7; }
.orb[data-figure-state='connecting'] { --rig-body: 1; --rig-rim: 0.35; }
.orb[data-figure-state='listening'] { --rig-body: 1; --rig-rim: 0.62; }
/* Working Presence 2.0 (D32): thinking's rig light raised ~15% over the base
   (owner: WORKING must read as clearly more active than listening). --rig-body
   is already near its 0-1 ceiling, so it is raised to that ceiling; --rig-rim
   (the light with the most headroom) carries the rest of the ~15%. */
.orb[data-figure-state='thinking'] { --rig-body: 1; --rig-rim: 0.68; } /* beta 2026-09-29: rim +~17 % over D32's 0.58 */
.orb[data-figure-state='working'] { --rig-body: 1; --rig-rim: 0.86; } /* Working Presence 3.0: a step really runs -- the brightest rim of the calm states */
.orb[data-figure-state='responding'] { --rig-body: 1; --rig-rim: 0.56; }
.orb[data-figure-state='success'] { --rig-body: 1; --rig-rim: 0.8; }
.orb[data-figure-state='warning'] { --rig-body: 0.82; --rig-rim: 0.32; }
.orb[data-figure-state='speaking'] { --rig-body: 1; --rig-rim: 0.5; }
.orb[data-figure-state='attention'] { --rig-body: 0.98; --rig-rim: 0.55; }
.orb[data-figure-state='error'] { --rig-body: 0.6; --rig-rim: 0.26; }
.orb[data-figure-state='unavailable'] { --rig-body: 0.5; }

/* The scene follows the figure where the voice state alone cannot tell:
   attention glows clearly amber (no session runs, so data-voice is idle), and
   error is muted rose instead of alarm-red -- the unavailable principle. */
:root[data-figure-state='attention'] {
  --orb-color: var(--warn);
  --orb-color-2: #ffd98a;
  --halo-speed: 3.2s;
}

/* Working Presence 3.0. The scene's light follows the figure's turn states, so
   the states also read from across the room without a word:
     thinking    indigo -> violet, a slow tempo (the model plans / evaluates)
     working     indigo -> cyan, the brisk tempo (a step really runs)
     responding  cyan -> violet, an even tempo (the answer is being written)
     success     cyan -> near-white, one bright beat (the terminal beat fades it)
     warning     a quiet rose, no alarm
   Approval keeps D29's amber (the attention rule above); error stays reduced. */
:root[data-figure-state='thinking'] {
  --orb-color: var(--light-indigo);
  --orb-color-2: var(--light-violet);
  --ring-speed: 9s;
  --halo-speed: 3.4s;
}

:root[data-figure-state='working'] {
  --orb-color: var(--light-indigo);
  --orb-color-2: var(--light-cyan);
  --ring-speed: 5s;
  --halo-speed: 2s;
}

:root[data-figure-state='responding'] {
  --orb-color: var(--light-cyan);
  --orb-color-2: var(--light-violet);
  --ring-speed: 12s;
  --halo-speed: 2.4s;
}

:root[data-figure-state='success'] {
  --orb-color: var(--light-cyan);
  --orb-color-2: #d8f6ff;
  --ring-speed: 20s;
  --halo-speed: 2.2s;
}

:root[data-figure-state='warning'] {
  --orb-color: color-mix(in srgb, var(--error) 46%, var(--light-violet));
  --orb-color-2: color-mix(in srgb, var(--error) 34%, var(--ink-3));
  --halo-speed: 8s;
  --ring-speed: 90s;
}

:root[data-figure-state='error'] {
  --orb-color: color-mix(in srgb, var(--error) 58%, var(--ink-3));
  --orb-color-2: color-mix(in srgb, var(--error) 42%, var(--ink-3));
  --halo-speed: 11s;
  --ring-speed: 160s;
}

/* The energy flow: very fine particles, drawn by ghostRig.js, behind the figure.
   Absolutely positioned (130 % of the orb, into the halo): out of the grid flow,
   so its size can never feed back into the orb's own size. */
.rig__particles {
  position: absolute;
  top: -15%;
  left: -15%;
  width: 130%;
  height: 130%;
  pointer-events: none;
  mix-blend-mode: screen;
}

.rig {
  position: absolute;
  inset: 0;
  transform-origin: 52% 58%;
  will-change: transform;
}

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

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

.rig__body[data-ready='1'] {
  opacity: var(--rig-body, 0.96);
}

/* The face appears with the body, never floating on its own while the loop loads. */
.rig__face {
  position: absolute;
  top: 0;
  left: 0;
  opacity: 0;
  mix-blend-mode: screen;
  pointer-events: none;
  transition: opacity 400ms var(--ease-out);
  will-change: transform;
}

.rig[data-ready='1'] .rig__face {
  opacity: 1;
}

/* A soft ring of the orb's own light around the figure; it grows with engagement. */
.rig__rim {
  position: absolute;
  inset: 4%;
  border-radius: 50%;
  pointer-events: none;
  background: radial-gradient(closest-side, transparent 60%, color-mix(in srgb, var(--orb-color) 34%, transparent) 78%, transparent 96%);
  mix-blend-mode: screen;
  opacity: var(--rig-rim, 0);
  transition: opacity 700ms var(--ease-out);
}

/* Step-completion "light breath" (D32): window event 'ghost:activity-pulse'
   while thinking briefly lifts the rig's own rim light -- opacity only, ~400 ms,
   then settles back to the state's steady value (ghostRig.js sets/clears
   [data-pulse] on this element; ~400ms). */
.rig__rim[data-pulse='1'] {
  animation: rig-pulse-breath 400ms var(--ease-out);
}

@keyframes rig-pulse-breath {
  0%, 100% { opacity: var(--rig-rim, 0); }
  40% { opacity: calc(var(--rig-rim, 0) + 0.2); }
}

/* An open approval: a warm rim in any state -- it never hides a running
   conversation (there it stays a quiet secondary cue); without a session it is
   the state itself: fully present, with a slow, calm swell. */
.rig__breath::after {
  content: '';
  position: absolute;
  inset: 4%;
  border-radius: 50%;
  pointer-events: none;
  background: radial-gradient(closest-side, transparent 56%, rgba(var(--warn-rgb), 0.58) 75%, transparent 95%);
  mix-blend-mode: screen;
  opacity: 0;
  transition: opacity 700ms var(--ease-out);
}

.orb[data-figure-attention='1'] .rig__breath::after {
  opacity: 0.55;
}

.orb[data-figure-state='attention'] .rig__breath::after {
  opacity: 1;
  animation: rig-presence 3.2s var(--ease-ambient) infinite alternate;
}

@keyframes rig-presence {
  from { opacity: 0.82; }
  to { opacity: 1; }
}

/* Dozing: the body loop is paused, a slow breath keeps the figure alive. */
.orb[data-figure-state='idle'] .rig__breath {
  animation: rig-doze 6.5s var(--ease-ambient) infinite alternate;
}

@keyframes rig-doze {
  from { transform: translateY(0.5%) scale(0.994); }
  to { transform: translateY(-0.5%) scale(1.006); }
}

/* Voice cannot work until the owner acts (no/blocked mic, offline, budget):
   the light goes quiet and grey instead of alarm-red. */
:root[data-voice-failure='unavailable'] {
  --orb-color: var(--ink-3);
  --orb-color-2: var(--ink-3);
  --halo-speed: 12s;
}

.voice-cta[data-failure='unavailable'] {
  --cta-rgb: 132, 151, 184;
}

.voice-cta[data-failure='unavailable'] .voice-cta__ring {
  background: rgba(var(--cta-rgb), 0.6);
  opacity: 0.35;
}

/* Audio cues on/off: a quiet text switch under the CTA (voiceCues.js). */
.voice-cues {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  min-height: 2.75rem;
  padding: 0 var(--s-3);
  border-radius: var(--r-pill);
  color: var(--ink-3);
  font-size: var(--fs-small);
  letter-spacing: 0.02em;
  transition: color var(--dur-2) var(--ease-out);
}

.voice-cues .icon {
  width: 1rem;
  height: 1rem;
}

.voice-cues[data-on='true'] {
  color: var(--ink-2);
}

.voice-cues[data-on='false'] .icon {
  opacity: 0.5;
}

.voice-cta[aria-disabled='true'] {
  cursor: not-allowed;
  opacity: 0.62;
}

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

@media (prefers-reduced-motion: reduce) {
  .orb[data-figure-state='idle'] .rig__breath,
  .orb[data-figure-state='attention'] .rig__breath::after,
  .rig__rim[data-pulse='1'] {
    animation: none;
  }

  :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);
  }

  /* The voice control keeps every state distinguishable without motion: the
     working arc stands still, waves are a static outer ring. */
  .voice-cta__wave,
  .voice-cta[data-state] .voice-cta__wave,
  .voice-cta[data-state='working'] .voice-cta__ring {
    animation: none;
  }

  .voice-cta:is([data-state='connecting'], [data-state='speaking']) .voice-cta__wave {
    opacity: 0.35;
    transform: scale(1.22);
  }
}

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

@media (hover: hover) and (pointer: fine) {
  .voice-cta:hover .voice-cta__disc {
    box-shadow:
      inset 0 0 0 1px rgba(var(--cta-rgb), 0.7),
      inset 0 1px 0 var(--highlight-top),
      0 0 38px rgba(var(--cta-rgb), 0.38),
      var(--shadow-1);
  }

  .voice-cta:hover .voice-cta__ring {
    opacity: 0.9;
  }
}
