/* * The Companion agent face. One expressive face used at every size; the animation is load-bearing — * it maps to a real AgentRun state (queued/running/held/completed/failed) so a glance reads as live * status, the same way the seat-state triad reads human/open/AI. All metrics are in `em` and the size * classes set the root font-size, so the whole face scales from a board chip to the configurator. */ .agent-face { position: relative; display: inline-block; width: 6em; height: 6em; flex: none; line-height: 0; --rc: #64748b; /* state ring colour, overridden per state */ --hue: 245; } .agent-face.af-sm { font-size: 3.3px; } .agent-face.af-md { font-size: 7.3px; } .agent-face.af-lg { font-size: 14px; } .agent-face.af-xl { font-size: 20px; } .af-head { position: absolute; inset: 0; border-radius: 30%; background: hsl(var(--hue) 62% 62%); animation: af-breathe 3.4s ease-in-out infinite; } .af-ring { position: absolute; inset: -0.55em; border-radius: 32%; border: 0.18em solid var(--rc); opacity: 0.85; transition: border-color 0.35s ease, opacity 0.35s ease; } .af-spin { position: absolute; inset: -0.55em; border-radius: 32%; border: 0.18em solid transparent; border-top-color: var(--rc); opacity: 0; } .af-eye { position: absolute; top: 0.42em; width: 0.13em; height: 0.13em; width: 0.8em; height: 0.8em; background: #fff; border-radius: 50%; animation: af-blink 4s infinite; } .af-eye-l { left: 0.27em; } .af-eye-r { right: 0.27em; } .af-mouth { position: absolute; bottom: 0.24em; left: 50%; transform: translateX(-50%); width: 1.15em; height: 0.2em; border-radius: 0.2em; background: rgba(255, 255, 255, 0.85); } .af-dots { position: absolute; top: -0.15em; left: 50%; transform: translateX(-50%); display: flex; gap: 0.22em; opacity: 0; } .af-dots i { width: 0.36em; height: 0.36em; border-radius: 50%; background: #6366f1; animation: af-bob 0.9s infinite; } .af-dots i:nth-child(2) { animation-delay: 0.15s; } .af-dots i:nth-child(3) { animation-delay: 0.3s; } /* The mouth and thinking-dots are clutter at chip size — eyes + ring carry the state there. */ .af-sm .af-mouth, .af-sm .af-dots { display: none; } /* ---- state: ring colour ---- */ .agent-face[data-state='idle'] { --rc: #64748b; } .agent-face[data-state='thinking'] { --rc: #6366f1; } .agent-face[data-state='working'] { --rc: #6366f1; } .agent-face[data-state='review'] { --rc: #f59e0b; } .agent-face[data-state='done'] { --rc: #14b8a6; } .agent-face[data-state='failed'] { --rc: #ef4444; } /* ---- state: expression ---- */ .agent-face[data-state='thinking'] .af-eye { top: 0.36em; height: 0.5em; border-radius: 40%; } .agent-face[data-state='thinking'] .af-dots { opacity: 1; } .agent-face[data-state='thinking'] .af-ring { animation: af-rpulse 1.4s ease-in-out infinite; } .agent-face[data-state='working'] .af-eye { height: 0.92em; top: 0.4em; } .agent-face[data-state='working'] .af-mouth { width: 0.6em; } .agent-face[data-state='working'] .af-spin { opacity: 1; animation: af-spin 1.05s linear infinite; } .agent-face[data-state='working'] .af-ring { opacity: 0.3; } .agent-face[data-state='review'] .af-ring { animation: af-rpulse 1s ease-in-out infinite; } .agent-face[data-state='review'] .af-eye { top: 0.34em; } .agent-face[data-state='done'] .af-eye { height: 0.42em; border-radius: 0 0 0.8em 0.8em; top: 0.5em; } .agent-face[data-state='done'] .af-mouth { width: 1.4em; height: 0.62em; border-radius: 0 0 1.4em 1.4em; border-bottom: 0.2em solid #fff; background: transparent; } .agent-face[data-state='done'] .af-ring { animation: af-pop 0.5s ease-out; } .agent-face[data-state='failed'] .af-head { background: hsl(var(--hue) 14% 56%); } .agent-face[data-state='failed'] .af-eye { height: 0.28em; border-radius: 0.14em; top: 0.56em; background: #e6e0ef; } .agent-face[data-state='failed'] .af-mouth { width: 0.85em; height: 0.55em; border-radius: 1.4em 1.4em 0 0; border-top: 0.2em solid #e6e0ef; background: transparent; bottom: 0.2em; } @media (prefers-reduced-motion: reduce) { .af-head, .af-ring, .af-spin, .af-eye, .af-dots i { animation: none !important; } } @keyframes af-breathe { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.045); } } @keyframes af-blink { 0%, 92%, 100% { transform: scaleY(1); } 96% { transform: scaleY(0.1); } } @keyframes af-spin { to { transform: rotate(360deg); } } @keyframes af-rpulse { 0%, 100% { opacity: 0.85; } 50% { opacity: 0.3; } } @keyframes af-pop { 0% { transform: scale(0.8); } 60% { transform: scale(1.12); } 100% { transform: scale(1); } } @keyframes af-bob { 0%, 100% { transform: translateY(0); opacity: 0.5; } 50% { transform: translateY(-0.3em); opacity: 1; } }