Files
Llamagochi/frontend/style.css
Vlad Doloman 71b3c945ee feat: sleep duration timer in stats panel
Track sleeping_since (ISO timestamp) in ServerState; auto-cleared by _set()
whenever state leaves sleeping. Frontend runs a 1s interval showing elapsed
time as "Xs", "Xm Ys", or "Xh Ym" in a new ASLEEP stat row (visible only
during state-sleeping). Timer survives page reload via server-side timestamp.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 13:49:50 +03:00

478 lines
13 KiB
CSS

/* ── Reset & base ─────────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0a0a04;
--amber: #ffb000;
--amber-dim: #a07000;
--amber-dk: #6a4a00;
--green: #00ff88;
--red: #ff4444;
--font: 'Press Start 2P', monospace;
}
html, body {
height: 100%;
background: var(--bg);
color: var(--amber);
font-family: var(--font);
font-size: 10px;
}
/* ── Two-panel layout ────────────────────────────────────────────────────── */
#app {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: center;
min-height: 100vh;
gap: 2rem;
padding: 2rem;
}
/* Portrait / narrow: stack vertically */
@media (orientation: portrait) {
#app {
flex-direction: column;
align-items: center;
}
}
/* ── Screen panel ────────────────────────────────────────────────────────── */
#screen-panel {
flex: 0 0 auto;
}
#screen {
position: relative;
width: 280px;
min-height: 280px;
background: #0d0d06;
border: 3px solid var(--amber-dim);
border-radius: 12px;
padding: 1.2rem 1rem 0.8rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
/* CRT scanline overlay */
background-image: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(0,0,0,0.15) 2px,
rgba(0,0,0,0.15) 4px
);
box-shadow: 0 0 24px rgba(255,176,0,0.15), inset 0 0 40px rgba(0,0,0,0.4);
}
/* ── WebSocket status dot ────────────────────────────────────────────────── */
#ws-dot {
position: absolute;
top: 10px;
right: 10px;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--red);
transition: background 0.3s;
}
#ws-dot.connected { background: var(--green); }
#ws-dot.polling { background: var(--amber-dim); }
#ws-dot.pulse { animation: pulse-dot 0.3s ease-out; }
@keyframes pulse-dot {
0% { transform: scale(1.8); opacity: 0.6; }
100% { transform: scale(1); opacity: 1; }
}
/* ── Llama container ─────────────────────────────────────────────────────── */
#llama-wrap {
position: relative;
width: 150px;
height: 175px;
display: flex;
align-items: center;
justify-content: center;
}
#llama {
width: 135px;
height: 160px;
}
/* SVG part colors */
.part { fill: var(--amber); stroke: none; }
.tail { stroke: var(--amber); fill: none; }
.hoof { fill: var(--amber-dk); }
.nostril { fill: var(--bg); }
.shine { fill: var(--bg); }
.mouth-closed { stroke: var(--bg); fill: none; }
.mouth-open { fill: var(--bg); }
.book { fill: var(--amber-dim); stroke: var(--amber-dk); stroke-width: 0.5; }
.book line { stroke: var(--amber); }
.sleep-eye { stroke: var(--bg); fill: none; }
/* ── State-based element visibility ─────────────────────────────────────── */
/* Default: open eyes, no accessories */
.sleep-eye { display: none; }
.mouth-open { display: none; }
.book { display: none; }
#zzz-wrap { display: none; }
#speech-bubble { display: none; }
#progress-wrap { display: none; }
/* sleeping */
body.state-sleeping .sleep-eye { display: block; }
body.state-sleeping .eye { display: none; }
body.state-sleeping .shine { display: none; }
body.state-sleeping #zzz-wrap { display: flex; }
/* processing_prompt */
body.state-processing_prompt .book { display: block; }
/* generating */
body.state-generating .mouth-open { display: block; }
body.state-generating .mouth-closed { display: none; }
body.state-generating #speech-bubble { display: flex; }
/* loading */
body.state-loading #progress-wrap { display: flex; }
/* processing_prompt */
body.state-processing_prompt #progress-wrap { display: flex; }
/* offline */
body.state-offline #llama { opacity: 0.15; }
/* error: llama flashes red */
body.state-error { --amber: var(--red); --amber-dim: #cc3333; --amber-dk: #882222; }
body.state-error #llama { animation: error-flash 1s ease-in-out infinite; }
@keyframes error-flash {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
/* ── ZZZ ─────────────────────────────────────────────────────────────────── */
#zzz-wrap {
position: absolute;
top: 8px;
right: 4px;
flex-direction: column;
align-items: flex-start;
gap: 2px;
pointer-events: none;
}
.z {
font-family: var(--font);
color: var(--amber);
animation: float-z 2.4s ease-in-out infinite;
opacity: 0;
}
.z1 { font-size: 9px; animation-delay: 0s; }
.z2 { font-size: 11px; animation-delay: 0.6s; }
.z3 { font-size: 13px; animation-delay: 1.2s; }
@keyframes float-z {
0% { opacity: 0; transform: translateY(0); }
20% { opacity: 1; }
80% { opacity: 0.8; }
100% { opacity: 0; transform: translateY(-28px); }
}
/* ── Speech bubble ───────────────────────────────────────────────────────── */
#speech-bubble {
position: absolute;
top: 10px;
right: 0;
background: var(--amber);
color: var(--bg);
border-radius: 8px;
padding: 4px 7px;
font-size: 14px;
gap: 1px;
pointer-events: none;
}
#speech-bubble::after {
content: '';
position: absolute;
bottom: -7px;
left: 10px;
border: 7px solid transparent;
border-top-color: var(--amber);
border-bottom: 0;
}
.dot {
animation: blink-dot 1.2s ease-in-out infinite;
opacity: 0;
}
.d1 { animation-delay: 0s; }
.d2 { animation-delay: 0.3s; }
.d3 { animation-delay: 0.6s; }
@keyframes blink-dot {
0%, 100% { opacity: 0; }
50% { opacity: 1; }
}
/* ── State label ─────────────────────────────────────────────────────────── */
#state-label {
font-size: 8px;
color: var(--amber-dim);
letter-spacing: 2px;
text-transform: uppercase;
}
/* ── Progress bar ────────────────────────────────────────────────────────── */
#progress-wrap {
width: 100%;
flex-direction: column;
align-items: center;
gap: 4px;
}
#progress-bar {
width: 100%;
height: 8px;
border: 1px solid var(--amber-dim);
border-radius: 2px;
overflow: hidden;
}
#progress-fill {
height: 100%;
background: var(--amber);
width: 0%;
transition: width 0.4s ease;
}
#progress-label {
font-size: 8px;
color: var(--amber);
}
/* ── Resize handle ───────────────────────────────────────────────────────── */
#resize-handle {
position: absolute;
right: -6px;
top: 0;
bottom: 0;
width: 12px;
cursor: ew-resize;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.3;
transition: opacity 0.15s;
user-select: none;
z-index: 10;
}
#resize-handle:hover,
#resize-handle.dragging { opacity: 1; }
#resize-handle::after {
content: '';
width: 2px;
height: 48px;
background: var(--amber-dim);
border-radius: 1px;
box-shadow: -4px 0 0 var(--amber-dim), 4px 0 0 var(--amber-dim);
}
@media (orientation: portrait) {
#resize-handle { display: none; }
}
/* ── Stats panel ─────────────────────────────────────────────────────────── */
#stats-panel {
position: relative;
flex: 0 0 300px;
background: #0d0d06;
border: 3px solid var(--amber-dim);
border-radius: 12px;
padding: 1.2rem 1rem;
display: flex;
flex-direction: column;
gap: 0.9rem;
box-shadow: 0 0 24px rgba(255,176,0,0.10);
}
@media (orientation: portrait) {
#stats-panel { width: 280px; flex: 0 0 auto; }
}
.stat-row {
display: flex;
flex-direction: column;
gap: 3px;
}
.sk {
font-size: 7px;
color: var(--amber-dk);
letter-spacing: 1px;
}
.sv {
font-size: 8px;
color: var(--amber);
word-break: break-all;
line-height: 1.5;
}
/* hide irrelevant rows per state */
body.state-offline #row-model,
body.state-offline #row-prompt,
body.state-offline #row-speed,
body.state-offline #row-tokens,
body.state-offline #row-load-stage { display: none; }
body.state-starting #row-prompt,
body.state-starting #row-speed,
body.state-starting #row-tokens,
body.state-starting #row-load-stage { display: none; }
body.state-loading #row-prompt,
body.state-loading #row-speed,
body.state-loading #row-tokens { display: none; }
body.state-idle #row-prompt,
body.state-idle #row-speed,
body.state-idle #row-tokens,
body.state-idle #row-load-stage { display: none; }
body.state-sleeping #row-prompt,
body.state-sleeping #row-speed,
body.state-sleeping #row-tokens,
body.state-sleeping #row-load-stage { display: none; }
body:not(.state-sleeping) #row-sleep-timer { display: none; }
body.state-waiting #row-prompt,
body.state-waiting #row-speed,
body.state-waiting #row-tokens,
body.state-waiting #row-load-stage { display: none; }
body.state-unloading #row-prompt,
body.state-unloading #row-speed,
body.state-unloading #row-tokens,
body.state-unloading #row-load-stage { display: none; }
body.state-error #row-prompt,
body.state-error #row-speed,
body.state-error #row-tokens,
body.state-error #row-load-stage { display: none; }
body.state-processing_prompt #row-speed,
body.state-processing_prompt #row-load-stage { display: none; }
body.state-generating #row-load-stage { display: none; }
/* ── Per-state SVG animations ────────────────────────────────────────────── */
/* sleeping: gentle breathe on body */
body.state-sleeping .body,
body.state-sleeping .neck,
body.state-sleeping .head,
body.state-sleeping .ear {
animation: breathe 3.5s ease-in-out infinite;
transform-origin: 50px 90px;
}
@keyframes breathe {
0%, 100% { transform: scaleY(1); }
50% { transform: scaleY(1.03); }
}
/* idle: occasional ear twitch */
body.state-idle .right-ear {
animation: ear-twitch 5s ease-in-out infinite;
transform-origin: 62px 22px;
}
@keyframes ear-twitch {
0%, 85%, 100% { transform: rotate(15deg); }
90% { transform: rotate(5deg); }
95% { transform: rotate(22deg); }
}
/* waiting: both ears perk forward */
body.state-waiting .left-ear {
transform: rotate(-5deg);
transform-origin: 38px 22px;
transition: transform 0.3s;
}
body.state-waiting .right-ear {
transform: rotate(5deg);
transform-origin: 62px 22px;
transition: transform 0.3s;
}
/* loading: walk cycle on legs */
body.state-loading .leg-fl,
body.state-loading .leg-bl {
animation: walk-a 0.7s ease-in-out infinite;
transform-origin: 34px 100px;
}
body.state-loading .leg-fr,
body.state-loading .leg-br {
animation: walk-b 0.7s ease-in-out infinite;
transform-origin: 45px 100px;
}
@keyframes walk-a {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
@keyframes walk-b {
0%, 100% { transform: translateY(-6px); }
50% { transform: translateY(0); }
}
/* processing_prompt: eyes move side to side (reading) */
body.state-processing_prompt .left-eye,
body.state-processing_prompt .right-eye,
body.state-processing_prompt .shine {
animation: read-eyes 2s ease-in-out infinite;
}
@keyframes read-eyes {
0%, 100% { transform: translateX(0); }
30% { transform: translateX(-1.5px); }
70% { transform: translateX(1.5px); }
}
/* generating: tail wag + mouth animation */
body.state-generating .tail {
animation: wag 0.45s ease-in-out infinite alternate;
transform-origin: 72px 82px;
}
body.state-generating .mouth-open {
animation: talk 0.3s ease-in-out infinite alternate;
transform-origin: 50px 39px;
}
@keyframes wag {
0% { transform: rotate(-18deg); }
100% { transform: rotate(18deg); }
}
@keyframes talk {
0% { transform: scaleY(0.4); }
100% { transform: scaleY(1); }
}
/* warming_up: yawn — jaw drops */
body.state-warming_up .mouth-open { display: block; }
body.state-warming_up .mouth-closed { display: none; }
body.state-warming_up .mouth-open {
animation: yawn 2.5s ease-in-out infinite;
transform-origin: 50px 39px;
}
@keyframes yawn {
0%, 20%, 100% { transform: scaleY(0.3); }
50%, 70% { transform: scaleY(1.4); }
}
/* unloading: whole llama fades and drifts right */
body.state-unloading #llama {
animation: unload 1.5s ease-in-out infinite alternate;
}
@keyframes unload {
0% { transform: translateX(0); opacity: 1; }
100% { transform: translateX(12px); opacity: 0.4; }
}