/* ── 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; } }