From 7e82eaf3f91b4d102985e8258afc45d0392998aa Mon Sep 17 00:00:00 2001 From: Vlad Doloman Date: Mon, 22 Jun 2026 19:03:33 +0300 Subject: [PATCH] feat: LCD aesthetic CSS with responsive layout and per-state animations --- frontend/style.css | 427 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 427 insertions(+) create mode 100644 frontend/style.css diff --git a/frontend/style.css b/frontend/style.css new file mode 100644 index 0000000..e63bf0b --- /dev/null +++ b/frontend/style.css @@ -0,0 +1,427 @@ +/* ── 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: 380px; + background: #0d0d06; + border: 3px solid var(--amber-dim); + border-radius: 12px; + padding: 1.5rem 1rem 1rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.8rem; + /* 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.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: 160px; + height: 200px; + display: flex; + align-items: center; + justify-content: center; +} + +#llama { + width: 140px; + height: 180px; +} + +/* 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; } + +/* offline */ +body.state-offline #llama { opacity: 0.15; } + +/* ── 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: 7px; + color: var(--amber-dim); +} + +/* ── Stats panel ─────────────────────────────────────────────────────────── */ +#stats-panel { + flex: 0 0 220px; + 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.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-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: 37px 100px; +} +body.state-loading .leg-fr, +body.state-loading .leg-br { + animation: walk-b 0.7s ease-in-out infinite; + transform-origin: 51px 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; } +}