Files
eranos/src/index.css
T
filemon 1eb126bdf8 Make all stat icons trigger the guided-care flow
Previously only low-status (warning/critical) stat icons were clickable
and the guide auto-cleared when the stat recovered to normal. Now every
stat icon fires onGuide on tap regardless of status, and the guide is
only dismissed by completion events (item used, sleep started, or a
different stat clicked).

Changes:
- StatsCrown: remove the status-gate from onClick and cursor-pointer
- StatsCrown: add z-10 so stat icons layer above the Blobbi visual
- Blobbi animation wrapper: pointer-events-none so it cannot intercept
  taps on overlapping stat icons
- BlobbiDashboard: remove the effect that cleared the guide when the
  stat was normal; drop unused getStatStatus import
- Update comments and prop docs to reflect the new behaviour
2026-04-24 11:16:10 -03:00

790 lines
21 KiB
CSS

@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--radius: 0.75rem;
--success: 142 72% 29%;
--success-foreground: 138 60% 94%;
/* Mobile chrome dimensions — referenced by layout utility classes below */
--top-bar-height: 2.5rem; /* MobileTopBar h-10 = 40px */
--bottom-nav-height: 2.75rem; /* MobileBottomNav h-11 = 44px */
}
.dark {
--success: 142 60% 22%;
--success-foreground: 138 60% 94%;
}
}
@layer base {
* {
@apply border-border;
scrollbar-width: none;
}
*::-webkit-scrollbar {
display: none;
}
body {
@apply bg-background text-foreground;
}
}
@layer utilities {
/* ── Safe-area inset utilities ────────────────────────────────────────────
Use var(--safe-area-inset-*, …) as the outer wrapper so that
Capacitor's SystemBars plugin (which injects --safe-area-inset-* CSS
variables on Android) takes precedence when available. The inner
env(safe-area-inset-*, 0px) is the standard fallback for iOS / web. */
.safe-area-top {
padding-top: var(--safe-area-inset-top, env(safe-area-inset-top, 0px));
}
.safe-area-bottom {
padding-bottom: var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px));
}
.safe-area-inset-top {
top: var(--safe-area-inset-top, env(safe-area-inset-top, 0px));
}
.safe-area-inset-bottom {
bottom: var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px));
}
/* FAB bottom offset: clears bottom nav + safe area inset on mobile */
.bottom-fab {
bottom: calc(1.5rem + var(--bottom-nav-height) + var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px)));
}
/* Position above mobile bottom nav + safe area + arc overhang (28px) */
.bottom-mobile-nav {
bottom: calc(var(--bottom-nav-height) + 28px + var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px)));
}
/* Bottom overscroll padding for the center column:
clears the mobile bottom nav + safe area + generous extra space
so content can be scrolled well past the bottom bar */
.pb-overscroll {
padding-bottom: calc(10vh + var(--bottom-nav-height) + var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px)));
}
@media (min-width: 900px) {
.pb-overscroll {
padding-bottom: 10vh;
}
}
/* Mobile top bar height + safe area inset for sticky elements */
.top-mobile-bar {
top: calc(var(--top-bar-height) + var(--safe-area-inset-top, env(safe-area-inset-top, 0px)));
}
/* New-posts pill: just below the SubHeaderBar on both mobile and desktop */
.new-posts-pill {
top: calc(var(--top-bar-height) + var(--safe-area-inset-top, env(safe-area-inset-top, 0px)) + 3.5rem);
}
@media (min-width: 900px) {
.new-posts-pill {
top: 4rem;
}
}
/* Slide the sub-header bar out of view together with the mobile top bar.
Must clear its own height (100%) + top bar + safe area + arc overhang (20px). */
@media (max-width: 899px) {
.nav-hidden-slide {
transform: translateY(calc(-100% - var(--top-bar-height) - 20px - var(--safe-area-inset-top, env(safe-area-inset-top, 0px))));
}
}
/* Negative margin to pull content area up behind the mobile top bar (only when it's visible) */
@media (max-width: 899px) {
.-mt-mobile-bar {
margin-top: calc(-1 * var(--top-bar-height) - var(--safe-area-inset-top, env(safe-area-inset-top, 0px)));
padding-top: calc(var(--top-bar-height) + var(--safe-area-inset-top, env(safe-area-inset-top, 0px)));
}
}
/* AI chat height on mobile: full viewport minus top bar, extends behind bottom nav.
Padding-bottom keeps input above the nav. */
.ai-chat-height {
height: calc(100dvh - var(--top-bar-height) - var(--safe-area-inset-top, env(safe-area-inset-top, 0px)));
padding-bottom: calc(var(--bottom-nav-height) + var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px)));
}
@media (min-width: 900px) {
.ai-chat-height {
padding-bottom: 0;
}
}
/* Live stream page height on mobile: full viewport minus top bar, bottom nav, and safe-area insets */
.livestream-height {
height: calc(100dvh - var(--top-bar-height) - var(--bottom-nav-height) - var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px)));
max-height: calc(100dvh - var(--top-bar-height) - var(--bottom-nav-height) - var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px)));
}
/* Vine feed slide height: full viewport on mobile (top bar + bottom nav are
both hidden for an immersive experience). Tab bar floats over video. */
.vine-slide-height {
height: 100dvh;
}
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* 3-burst ping: pulses 3 times then stops */
@keyframes ping-3 {
0% { transform: scale(1); opacity: 0.6; }
11% { transform: scale(2); opacity: 0; }
22% { transform: scale(1); opacity: 0; }
33% { transform: scale(1); opacity: 0.6; }
44% { transform: scale(2); opacity: 0; }
55% { transform: scale(1); opacity: 0; }
66% { transform: scale(1); opacity: 0.6; }
77% { transform: scale(2); opacity: 0; }
100% { transform: scale(1); opacity: 0; }
}
.animate-ping-3 {
animation: ping-3 4s ease-out forwards;
}
/* Slow continuous pulse for nagging buttons */
@keyframes pulse-slow {
0%, 100% { transform: scale(1); opacity: 0.5; }
50% { transform: scale(1.8); opacity: 0; }
}
.animate-pulse-slow {
animation: pulse-slow 2.5s ease-in-out infinite;
}
/* Eerie sigil glow pulse */
@keyframes sigil-glow {
0%, 100% { opacity: 0.12; filter: drop-shadow(0 0 4px hsl(var(--primary) / 0.3)); }
50% { opacity: 0.28; filter: drop-shadow(0 0 12px hsl(var(--primary) / 0.6)) drop-shadow(0 0 24px hsl(var(--primary) / 0.25)); }
}
.animate-sigil-glow {
animation: sigil-glow 4s ease-in-out infinite;
}
/* Eyeball blink animation - eyelid sweeps down then back up */
@keyframes eyelid-blink {
0% { transform: translateY(-100%); }
50% { transform: translateY(0%); }
100% { transform: translateY(-100%); }
}
.animate-eyelid-blink {
animation: eyelid-blink 0.5s cubic-bezier(0.4, 0, 0.6, 1) forwards;
}
/* Landing hero staggered fade-in */
@keyframes landing-hero-fade-in {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.landing-hero-fade {
opacity: 0;
animation: landing-hero-fade-in 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes nudge-cancel-fade-in {
from { opacity: 0; }
to { opacity: 0.6; }
}
/* ── NIP-62 Request to Vanish ───────────────────────────────────────────────── */
/* Animated caution stripes — red/dark diagonal bands that scroll */
.vanish-stripes {
background: repeating-linear-gradient(
-45deg,
hsl(0 72% 51% / 0.7),
hsl(0 72% 51% / 0.7) 10px,
hsl(0 72% 51% / 0.25) 10px,
hsl(0 72% 51% / 0.25) 20px
);
background-size: 28.28px 28.28px;
animation: vanish-stripe-scroll 1.5s linear infinite;
}
:is(.dark) .vanish-stripes {
background: repeating-linear-gradient(
-45deg,
hsl(0 72% 51% / 0.6),
hsl(0 72% 51% / 0.6) 10px,
hsl(0 72% 51% / 0.15) 10px,
hsl(0 72% 51% / 0.15) 20px
);
background-size: 28.28px 28.28px;
animation: vanish-stripe-scroll 1.5s linear infinite;
}
@keyframes vanish-stripe-scroll {
from { background-position: 0 0; }
to { background-position: 28.28px 0; }
}
/* Glitch text effect on the /// symbol */
.vanish-glitch-text {
position: relative;
animation: vanish-glitch 3s ease-in-out infinite;
}
@keyframes vanish-glitch {
0%, 90%, 100% { opacity: 1; transform: translate(0); }
92% { opacity: 0.8; transform: translate(-2px, 1px); }
94% { opacity: 0.6; transform: translate(2px, -1px); }
96% { opacity: 0.9; transform: translate(-1px, -1px); }
98% { opacity: 1; transform: translate(1px, 0); }
}
/* Ring pulse animation for the icon */
.vanish-ring-pulse {
animation: vanish-ring 3s ease-out infinite;
}
.vanish-ring-pulse-delayed {
animation: vanish-ring 3s ease-out 1.5s infinite;
}
@keyframes vanish-ring {
0% { transform: scale(1); opacity: 0.4; }
50% { transform: scale(1.15); opacity: 0; }
100% { transform: scale(1); opacity: 0; }
}
/* Slow blink for the status dot */
.vanish-blink {
animation: vanish-blink-kf 2s ease-in-out infinite;
}
@keyframes vanish-blink-kf {
0%, 40%, 100% { opacity: 1; }
50%, 90% { opacity: 0.2; }
}
/* ── Wii Mail envelope grid ─────────────────────────────────────────────────── */
/* Staggered entrance: envelopes pop in one by one.
Keep the animation short and avoid large rotations so the tap target
doesn't shift while the user is already trying to interact. */
@keyframes envelope-entrance {
0% { opacity: 0; transform: scale(0.85) translateY(12px); }
70% { opacity: 1; transform: scale(1.02) translateY(-1px); }
100% { opacity: 1; transform: scale(1) translateY(0); }
}
.envelope-card {
animation: envelope-entrance 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) both;
animation-delay: var(--entrance-delay, 0ms);
transform-origin: center bottom;
cursor: pointer;
}
/* Hover wobble — playful Wii-style oscillation */
@keyframes envelope-wobble {
0% { transform: rotate(0deg); }
15% { transform: rotate(-5deg); }
30% { transform: rotate(4deg); }
45% { transform: rotate(-3deg); }
60% { transform: rotate(2deg); }
75% { transform: rotate(-1deg); }
100% { transform: rotate(0deg); }
}
.envelope-body {
transition: transform 0.15s ease;
}
/* Wobble only on true pointer hover — skip on touch where :hover sticks
and the rotation shifts the tap target, making repeated taps miss. */
@media (hover: hover) and (pointer: fine) {
.envelope-card:hover .envelope-body,
.envelope-card:focus-visible .envelope-body {
animation: envelope-wobble 0.5s ease-in-out;
}
}
.envelope-card:active .envelope-body {
transform: scale(0.95);
animation: none;
}
/* Envelope skeleton shimmer */
@keyframes envelope-skeleton-shimmer {
0% { opacity: 0.4; }
50% { opacity: 0.7; }
100% { opacity: 0.4; }
}
.envelope-skeleton {
animation: envelope-skeleton-shimmer 1.5s ease-in-out infinite;
}
/* Reduced motion: disable envelope animations */
@media (prefers-reduced-motion: reduce) {
.envelope-card {
animation: none;
opacity: 1;
}
.envelope-card:hover .envelope-body,
.envelope-card:focus-visible .envelope-body {
animation: none;
}
}
/* Drawn SVG stickers — ensure inline SVGs fill their container */
[data-sticker] svg,
.sticker-svg-wrap svg {
width: 100%;
height: 100%;
display: block;
}
/* Letter send animation keyframes */
@keyframes letter-send-fade-up {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes letter-send-avatar-in {
0% { transform: scale(0); opacity: 0; }
50% { transform: scale(1.15); opacity: 1; }
70% { transform: scale(0.92); }
85% { transform: scale(1.04); }
100% { transform: scale(1); opacity: 1; }
}
@keyframes letter-send-burst {
0% { transform: translate(0, 0) rotate(0deg) scale(0); opacity: 0; }
15% { opacity: 1; transform: translate(0, 0) rotate(0deg) scale(1); }
60% { opacity: 0.8; transform: translate(var(--burst-tx), var(--burst-ty)) rotate(var(--burst-rot)) scale(1); }
100% { opacity: 0; transform: translate(var(--burst-tx), var(--burst-ty)) rotate(var(--burst-rot)) scale(0.4); }
}
/* Reduced motion: disable all vanish animations */
@media (prefers-reduced-motion: reduce) {
.vanish-stripes {
animation: none;
}
.vanish-glitch-text {
animation: none;
}
.vanish-ring-pulse,
.vanish-ring-pulse-delayed {
animation: none;
opacity: 0;
}
.vanish-blink {
animation: none;
}
}
/* ==========================================
Blobbi Reaction Animations
Shared animations for all Blobbi stages (egg, baby, adult)
========================================== */
@keyframes blobbi-gentle-sway {
0%, 100% {
transform: rotate(-2deg);
}
50% {
transform: rotate(2deg);
}
}
@keyframes blobbi-bounce {
0%, 100% {
transform: translateY(0) rotate(0deg);
}
25% {
transform: translateY(-3px) rotate(1.5deg);
}
50% {
transform: translateY(0) rotate(0deg);
}
75% {
transform: translateY(-3px) rotate(-1.5deg);
}
}
.animate-blobbi-sway {
/* Baby/adult sway is livelier than egg (1.2s vs 1.8s) */
animation: blobbi-gentle-sway 1.2s ease-in-out infinite;
}
.animate-blobbi-bounce {
/* Baby/adult sing bounce - smoother and cuter than before (0.5s) */
animation: blobbi-bounce 0.5s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.animate-blobbi-sway,
.animate-blobbi-bounce {
animation: none !important;
}
}
/* ─── Floating Music Notes ───────────────────────────────────────────────────── */
@keyframes float-up {
0% {
opacity: 0;
transform: translateY(0) scale(0.8);
}
15% {
opacity: 1;
transform: translateY(-10px) scale(1);
}
85% {
opacity: 0.8;
transform: translateY(-60px) scale(1);
}
100% {
opacity: 0;
transform: translateY(-80px) scale(0.6);
}
}
.animate-float-up {
animation: float-up 3s ease-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.animate-float-up {
animation: none !important;
opacity: 0 !important;
}
}
/* ─── Blobbi Eye Animation ───────────────────────────────────────────────────── */
/**
* Eye animation for Blobbi characters.
*
* Features:
* - Mouse tracking: eyes follow cursor instantly
* - Natural blinking: random intervals (2-5s), fast close, slow open
*
* NO CSS transitions - all animation is controlled by JavaScript RAF loop.
* Using transitions causes delayed visual updates.
*
* Transform combines: translate(x y) scale(1 blinkY)
* - translate: mouse tracking position
* - scale: vertical squash for blink effect (1 = open, 0.1 = closed)
*/
.blobbi-eye {
/* SVG transform properties - scale from center of each eye element */
transform-box: fill-box;
transform-origin: center center;
/* NO transition - instant updates only */
}
@media (prefers-reduced-motion: reduce) {
.blobbi-eye {
/* Disable all eye animations for users who prefer reduced motion */
transform: none !important;
}
}
/* ── Milkdown Editor Styles ─────────────────────────────────────────────────── */
.milkdown-editor .milkdown {
@apply outline-none;
}
.milkdown-editor .editor {
@apply outline-none min-h-[400px] p-3;
font-size: 1.125rem;
line-height: 1.75;
}
.milkdown-editor .ProseMirror {
@apply outline-none min-h-[400px];
}
.milkdown-editor .ProseMirror:focus {
@apply outline-none;
}
/* Headings */
.milkdown-editor h1 {
@apply text-3xl font-bold mt-6 mb-4;
}
.milkdown-editor h2 {
@apply text-2xl font-semibold mt-5 mb-3;
}
.milkdown-editor h3 {
@apply text-xl font-semibold mt-4 mb-2;
}
.milkdown-editor h4 {
@apply text-lg font-medium mt-3 mb-2;
}
/* Inline styles */
.milkdown-editor strong {
@apply font-bold;
}
.milkdown-editor em {
@apply italic;
}
.milkdown-editor del {
@apply line-through text-muted-foreground;
}
.milkdown-editor code {
@apply bg-muted px-1.5 py-0.5 rounded text-sm font-mono;
}
/* Block elements */
.milkdown-editor p {
@apply my-1.5;
}
.milkdown-editor blockquote {
@apply border-l-4 border-primary/40 pl-4 my-4 italic text-muted-foreground;
}
.milkdown-editor pre {
@apply bg-muted rounded-lg p-4 my-4 overflow-x-auto;
}
.milkdown-editor pre code {
@apply bg-transparent p-0 font-mono;
font-size: 0.875rem;
}
/* Lists */
.milkdown-editor ul {
@apply list-disc list-inside my-3 space-y-1;
}
.milkdown-editor ol {
@apply list-decimal list-inside my-3 space-y-1;
}
.milkdown-editor li {
@apply pl-1;
}
.milkdown-editor li p {
@apply inline my-0;
}
/* Task lists (GFM) */
.milkdown-editor ul.task-list {
@apply list-none pl-0;
}
.milkdown-editor li.task-list-item {
@apply flex items-start gap-2 pl-0;
}
.milkdown-editor li.task-list-item input[type="checkbox"] {
@apply mt-1.5 h-4 w-4 rounded border-border;
}
/* Links */
.milkdown-editor a {
@apply text-primary underline underline-offset-2 hover:text-primary/80 transition-colors;
}
/* Horizontal rule */
.milkdown-editor hr {
@apply my-6 border-border;
}
/* Images */
.milkdown-editor img {
@apply max-w-full h-auto rounded-lg my-4;
}
/* Tables (GFM) */
.milkdown-editor table {
@apply w-full border-collapse my-4;
}
.milkdown-editor th,
.milkdown-editor td {
@apply border border-border px-3 py-2 text-left;
}
.milkdown-editor th {
@apply bg-muted font-semibold;
}
.milkdown-editor tr:nth-child(even) {
@apply bg-muted/30;
}
/* Upload placeholder */
.milkdown-upload-placeholder {
@apply inline-flex items-center gap-2 px-3 py-1.5 bg-muted/50 rounded-md text-sm text-muted-foreground;
}
.milkdown-upload-placeholder::before {
content: '';
@apply w-4 h-4 border-2 border-muted-foreground/30 border-t-primary rounded-full animate-spin;
}
/* Placeholder — only when unfocused AND content is empty. */
.milkdown-editor .ProseMirror:not(:focus):not(.has-content) > p:first-child::before {
@apply text-muted-foreground pointer-events-none float-left h-0;
content: var(--ph, '');
}
/* Milkdown content area */
.milkdown-editor .milkdown-content .ProseMirror {
@apply min-h-[350px];
}
/* Blobbi idle animations — speed/intensity driven by happiness via inline style */
@keyframes blobbi-bob {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
@keyframes blobbi-sway {
0%, 100% { transform: rotate(0deg); }
25% { transform: rotate(1.5deg); }
75% { transform: rotate(-1.5deg); }
}
/* Hatch ceremony shake animations */
@keyframes blobbi-shake-light {
0%, 100% { transform: rotate(0deg); }
25% { transform: rotate(-3deg); }
75% { transform: rotate(3deg); }
}
@keyframes blobbi-shake-medium {
0%, 100% { transform: rotate(0deg); }
20% { transform: rotate(-6deg); }
40% { transform: rotate(5deg); }
60% { transform: rotate(-4deg); }
80% { transform: rotate(6deg); }
}
@keyframes blobbi-shake-heavy {
0%, 100% { transform: rotate(0deg) scale(1); }
15% { transform: rotate(-8deg) scale(1.02); }
30% { transform: rotate(7deg) scale(0.98); }
45% { transform: rotate(-9deg) scale(1.03); }
60% { transform: rotate(8deg) scale(0.97); }
75% { transform: rotate(-7deg) scale(1.02); }
90% { transform: rotate(9deg) scale(1); }
}
@keyframes blobbi-flash {
0% { opacity: 1; }
100% { opacity: 0; }
}
@keyframes blobbi-fade-to-white {
0% { opacity: 0; }
100% { opacity: 1; }
}
/* ── Shared glow clock for synchronised low-status icons ────────────────
A single animation on the StatsCrown parent drives --stat-glow-intensity
(0→1→0). Children inherit its animated value and compute their own
box-shadow via calc(), giving perfect phase-lock regardless of when
individual icons mount or re-enter a low-status state. */
@property --stat-glow-intensity {
syntax: '<number>';
initial-value: 0;
inherits: true;
}
@keyframes stat-glow-clock {
0%, 8% { --stat-glow-intensity: 0; }
50% { --stat-glow-intensity: 1; }
92%, 100% { --stat-glow-intensity: 0; }
}
/* Legacy per-element keyframes — still used by the reusable StatIndicator
component (src/blobbi/ui/StatIndicator.tsx) outside the crown context. */
@keyframes stat-glow {
0%, 100% { box-shadow: 0 0 0 0 transparent; }
50% { box-shadow: 0 0 6px 2px currentColor; }
}
@keyframes stat-glow-critical {
0%, 100% { box-shadow: 0 0 0 0 transparent; }
50% { box-shadow: 0 0 10px 3px currentColor; }
}
/* Guide-target glow — quick blink + pause for nav arrows & carousel.
Applied at 1.1s linear: rise (0→8%), brief peak (8→18%), fall (18→28%),
idle (28→100% ≈ 0.8 s pause). */
@keyframes guide-glow-slow {
0%, 28%, 100% { box-shadow: 0 0 0 0 transparent; }
8%, 18% { box-shadow: 0 0 4px 1.5px currentColor; }
}
/* Guide-target glow — action-button rhythm, kept punchier (4 s cycle) */
@keyframes guide-glow {
0%, 100% { box-shadow: 0 0 0 0 transparent; }
50% { box-shadow: 0 0 8px 3px currentColor; }
}
/* Room navigation arrow nudge — subtle horizontal pulse */
@keyframes room-arrow-nudge-left {
0%, 100% { transform: translateX(0); }
50% { transform: translateX(-3px); }
}
@keyframes room-arrow-nudge-right {
0%, 100% { transform: translateX(0); }
50% { transform: translateX(3px); }
}
/* ── Overstimulation block overlay ─────────────────────────────────────────── */
@keyframes overstim-vignette-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes overstim-vignette-out {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes overstim-shockwave {
0% { transform: scale(0.1); opacity: 1; }
60% { opacity: 0.6; }
100% { transform: scale(2.2); opacity: 0; }
}