/** * tabby.css - animations + mood expressions for the Tabby companion. * Colors mirror the app's theme tokens (accent #6366f1/#818cf8, surface scale) * with warm pink accents (ears, cheeks, nose) for cuteness. All continuous * motion is disabled under [data-reduced="1"] and prefers-reduced-motion. * @author Nguyễn Ngọc Trí Vĩ */ .tabby-cat { overflow: visible; cursor: pointer; filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.5)); } /* ── palette ── */ .tabby-head, .tabby-body { fill: url(#tabbyFur); stroke: #8b93f9; stroke-width: 2; } .tabby-ear { fill: url(#tabbyFur); stroke: #8b93f9; stroke-width: 2; stroke-linejoin: round; } .tabby-ear-inner { fill: #f7a8c8; } .tabby-paw { fill: #4a4a72; stroke: #8b93f9; stroke-width: 1.5; } .tabby-toe { fill: none; stroke: #2c2c46; stroke-width: 1.2; stroke-linecap: round; opacity: 0.7; } .tabby-stripes path { fill: none; stroke: #8b93f9; stroke-width: 2; stroke-linecap: round; opacity: 0.55; } .tabby-cheeks ellipse { fill: #f7849f; opacity: 0.5; } .tabby-eye { fill: #f3f4ff; } .tabby-pupil { fill: #15131f; } .tabby-glint { fill: #ffffff; opacity: 0.95; } .tabby-glint-sm { opacity: 0.75; } .tabby-nose { fill: #f7849f; } .tabby-tail { fill: url(#tabbyFur); stroke: #8b93f9; stroke-width: 2; stroke-linejoin: round; transform-origin: 80px 68px; } .tabby-whiskers path, .tabby-brows path, .tabby-eyes-happy path, .tabby-eyes-closed path, .tabby-mouth-idle, .tabby-mouth-happy, .tabby-mouth-worried { fill: none; stroke: #dfe1ff; stroke-width: 2.2; stroke-linecap: round; } .tabby-whiskers path { stroke: #9aa0c8; stroke-width: 1.4; opacity: 0.8; } .tabby-brows path { stroke: #8b93f9; } .tabby-halo { fill: url(#tabbyHalo); opacity: 0.18; } .tabby-sparkle path { fill: #fde68a; } .tabby-zzz text, .tabby-bang text { fill: #a5b4fc; font-family: "JetBrains Mono", monospace; font-size: 11px; font-weight: 700; } /* ── default visibility: show open eyes + idle mouth, hide the rest ── */ .tabby-eyes-happy, .tabby-eyes-closed, .tabby-brows, .tabby-mouth-happy, .tabby-mouth-worried, .tabby-zzz, .tabby-bang, .tabby-sparkle { display: none; } /* ── happy ── */ .tabby-cat[data-mood="happy"] .tabby-eyes-open { display: none; } .tabby-cat[data-mood="happy"] .tabby-eyes-happy, .tabby-cat[data-mood="happy"] .tabby-mouth-happy, .tabby-cat[data-mood="happy"] .tabby-sparkle { display: block; } .tabby-cat[data-mood="happy"] .tabby-mouth-idle { display: none; } /* ── watching ── (alert, smiling, tail flick) */ .tabby-cat[data-mood="watching"] .tabby-mouth-idle { display: none; } .tabby-cat[data-mood="watching"] .tabby-mouth-happy { display: block; } /* ── worried ── (brows down, frown) */ .tabby-cat[data-mood="worried"] .tabby-brows, .tabby-cat[data-mood="worried"] .tabby-mouth-worried { display: block; } .tabby-cat[data-mood="worried"] .tabby-mouth-idle { display: none; } .tabby-cat[data-mood="worried"] .tabby-cheeks ellipse { opacity: 0.7; } /* ── stuck ── (alert bang, ears up) */ .tabby-cat[data-mood="stuck"] .tabby-bang { display: block; } /* ── thinking ── keeps open eyes + idle mouth; head tilt handled below */ /* ── sleeping / disconnected ── (closed eyes) */ .tabby-cat[data-mood="sleeping"] .tabby-eyes-open, .tabby-cat[data-mood="disconnected"] .tabby-eyes-open { display: none; } .tabby-cat[data-mood="sleeping"] .tabby-eyes-closed, .tabby-cat[data-mood="disconnected"] .tabby-eyes-closed { display: block; } .tabby-cat[data-mood="sleeping"] .tabby-zzz { display: block; } .tabby-cat[data-mood="disconnected"] { opacity: 0.5; filter: grayscale(0.65) drop-shadow(0 4px 12px rgba(0, 0, 0, 0.5)); } /* ════════ animations ════════ */ @keyframes tabby-breathe { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.035); } } @keyframes tabby-blink { 0%, 92%, 100% { transform: scaleY(1); } 96% { transform: scaleY(0.1); } } @keyframes tabby-shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-2px); } 75% { transform: translateX(2px); } } @keyframes tabby-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-3px); } } @keyframes tabby-tail-flick { 0%, 100% { transform: rotate(0deg); } 50% { transform: rotate(-12deg); } } @keyframes tabby-ears-perk { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-1.5px); } } @keyframes tabby-sparkle-twinkle { 0%, 100% { transform: scale(0.6); opacity: 0.4; } 50% { transform: scale(1); opacity: 1; } } /* idle: gentle breathing + occasional blink */ .tabby-cat[data-mood="idle"] { animation: tabby-breathe 4s ease-in-out infinite; transform-origin: 50px 60px; } /* Blink lives on the inner group so it never overrides the outer group's eye-tracking translate (which would freeze the eyes - see CatAvatar). */ .tabby-cat[data-mood="idle"] .tabby-pupils-blink { animation: tabby-blink 5s ease-in-out infinite; transform-origin: 50px 50px; } /* watching: tail flicks, ears perk */ .tabby-cat[data-mood="watching"] .tabby-tail { animation: tabby-tail-flick 1.8s ease-in-out infinite; } .tabby-cat[data-mood="watching"] .tabby-ears { animation: tabby-ears-perk 1.8s ease-in-out infinite; transform-origin: 50px 24px; } /* happy: head bob + twinkling sparkle */ .tabby-cat[data-mood="happy"] { animation: tabby-bob 0.5s ease-in-out 0s 4; transform-origin: 50px 60px; } .tabby-cat[data-mood="happy"] .tabby-sparkle { animation: tabby-sparkle-twinkle 0.9s ease-in-out infinite; transform-origin: 84px 45px; } /* worried: shake + puff (halo grows) */ .tabby-cat[data-mood="worried"] { animation: tabby-shake 0.35s ease-in-out 0s 3; transform-origin: 50px 60px; } .tabby-cat[data-mood="worried"] .tabby-halo { opacity: 0.32; } /* stuck: ears stay perked, slow breathe */ .tabby-cat[data-mood="stuck"] { animation: tabby-breathe 2.4s ease-in-out infinite; transform-origin: 50px 60px; } .tabby-cat[data-mood="stuck"] .tabby-ears { transform: translateY(-2px); transform-origin: 50px 24px; } /* thinking: subtle head tilt */ .tabby-cat[data-mood="thinking"] { transform: rotate(-6deg); transform-origin: 50px 60px; } /* sleeping: slow breathe, droop */ .tabby-cat[data-mood="sleeping"] { animation: tabby-breathe 5s ease-in-out infinite; transform-origin: 50px 60px; } /* ── reduced motion: kill all continuous animation ── */ .tabby-cat[data-reduced="1"], .tabby-cat[data-reduced="1"] * { animation: none !important; } @media (prefers-reduced-motion: reduce) { .tabby-cat, .tabby-cat * { animation: none !important; } } /* ════════ shell (avatar button, bubble, panel) ════════ */ /* The draggable avatar. Fixed-positioned; left/top are set inline by the drag hook. It docks to an edge (AssistiveTouch-style) and remembers where. */ .tabby-avatar-btn { position: fixed; z-index: 41; /* above the flyout */ background: transparent; border: none; padding: 0; line-height: 0; border-radius: 9999px; cursor: grab; touch-action: none; /* pointer drives the drag instead of scrolling on touch */ /* Glide to the snapped edge after a drag; killed mid-drag for 1:1 tracking. */ transition: left 0.28s cubic-bezier(0.22, 1, 0.36, 1), top 0.28s cubic-bezier(0.22, 1, 0.36, 1), transform 0.15s ease; } .tabby-avatar-btn:hover { transform: scale(1.06); } .tabby-avatar-btn:focus-visible { outline: 2px solid #818cf8; outline-offset: 3px; } .tabby-avatar-btn[data-dragging="1"] { cursor: grabbing; transition: transform 0.15s ease; /* no left/top easing while dragging */ } .tabby-avatar-btn[data-dragging="1"]:hover { transform: scale(1.1); } /* Self-clamping flyout that holds the bubble / panel next to the avatar. Its left/top are computed and set inline by TabbyFlyout so it never crops. */ .tabby-flyout { position: fixed; z-index: 40; display: flex; flex-direction: column; gap: 0.5rem; } .tabby-avatar-btn:focus-visible { outline: 2px solid #818cf8; outline-offset: 3px; } .tabby-error-dot { position: absolute; top: 2px; right: 2px; width: 14px; height: 14px; border-radius: 9999px; background: #ef4444; color: #fff; font-size: 9px; font-weight: 700; display: flex; align-items: center; justify-content: center; border: 2px solid #0c0c14; } .tabby-bubble { max-width: 16rem; background: #1a1a28; border: 1px solid #363650; color: #e8e8f0; font-size: 0.8125rem; line-height: 1.2rem; padding: 0.5rem 0.75rem; border-radius: 0.75rem; border-bottom-right-radius: 0.25rem; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45); } .tabby-bubble-enter { animation: tabby-bubble-in 0.22s ease-out; } @keyframes tabby-bubble-in { from { opacity: 0; transform: translateY(6px) scale(0.96); } to { opacity: 1; transform: translateY(0) scale(1); } } .tabby-cat[data-reduced="1"] ~ * .tabby-bubble-enter, .tabby-bubble.tabby-no-anim { animation: none; }