puck / frontend /src /styles /ui.css
vu1n's picture
Puck — desktop fairy familiar (HF Build Small)
3c124f3
Raw
History Blame Contribute Delete
44.7 kB
/* ============================================================
Puck — sprite, companion window, comments, notifications,
Night Bloom. Loaded after styles.css.
============================================================ */
/* ============================================================
The sprite
============================================================ */
.puck {
position: fixed;
z-index: 60;
left: 0;
top: 0;
width: 64px;
height: 64px;
transform: translate(-100px, -100px);
transition: transform 1.25s cubic-bezier(0.45, 0.05, 0.25, 1);
pointer-events: none;
will-change: transform;
}
.puck.snappy {
transition: transform 0.7s cubic-bezier(0.5, 0, 0.2, 1);
}
.puck-hit {
position: absolute;
inset: -6px;
pointer-events: auto;
cursor: pointer;
}
/* inner wrapper handles bob + facing so the flight transform stays clean */
.puck-bob {
width: 100%;
height: 100%;
animation: bob 3.1s ease-in-out infinite;
position: relative;
}
@keyframes bob {
0%,
100% {
transform: translateY(-3px);
}
50% {
transform: translateY(3px);
}
}
.puck.flying .puck-bob {
animation-duration: 0.9s;
}
.puck-face {
width: 100%;
height: 100%;
position: relative;
transition: transform 0.4s;
}
.puck.face-left .puck-face {
transform: scaleX(-1);
}
/* glow halo */
.puck-glow {
position: absolute;
inset: -55%;
border-radius: 50%;
background: radial-gradient(circle, var(--puck-glow, var(--glow)) 0%, transparent 62%);
opacity: 1;
animation: pulse 3.4s ease-in-out infinite;
}
@keyframes pulse {
0%,
100% {
transform: scale(0.9);
opacity: 0.6;
}
50% {
transform: scale(1.12);
opacity: 1;
}
}
/* body */
.puck-body {
position: absolute;
left: 50%;
top: 50%;
width: 38px;
height: 38px;
transform: translate(-50%, -50%);
border-radius: 50%;
background: radial-gradient(circle at 35% 30%, var(--puck-hi, #cdeccb), var(--puck-body, #8fd6a0) 70%);
box-shadow:
inset -3px -4px 8px rgba(0, 0, 0, 0.25),
0 3px 14px rgba(0, 0, 0, 0.4);
}
.puck-eye {
position: absolute;
top: 40%;
width: 6px;
height: 8px;
border-radius: 50%;
background: #15201a;
}
.puck-eye.l {
left: 31%;
}
.puck-eye.r {
right: 31%;
}
.puck-eye::after {
content: "";
position: absolute;
top: 1px;
left: 1px;
width: 2.5px;
height: 2.5px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.9);
}
.puck-cheek {
position: absolute;
top: 56%;
width: 5px;
height: 3px;
border-radius: 50%;
background: rgba(255, 140, 120, 0.45);
}
.puck-cheek.l {
left: 26%;
}
.puck-cheek.r {
right: 26%;
}
/* wings */
.puck-wing {
position: absolute;
top: 42%;
width: 26px;
height: 30px;
background: radial-gradient(
circle at 50% 30%,
rgba(255, 255, 255, 0.5),
var(--puck-wing, rgba(180, 230, 200, 0.32)) 70%,
transparent
);
border: 0.5px solid rgba(255, 255, 255, 0.4);
}
.puck-wing.l {
right: 56%;
border-radius: 60% 30% 50% 50%;
transform-origin: right center;
animation: flapL 0.34s ease-in-out infinite alternate;
}
.puck-wing.r {
left: 56%;
border-radius: 30% 60% 50% 50%;
transform-origin: left center;
animation: flapR 0.34s ease-in-out infinite alternate;
}
@keyframes flapL {
from {
transform: rotateY(20deg) rotate(8deg);
}
to {
transform: rotateY(60deg) rotate(-12deg);
}
}
@keyframes flapR {
from {
transform: rotateY(-20deg) rotate(-8deg);
}
to {
transform: rotateY(-60deg) rotate(12deg);
}
}
.puck.flying .puck-wing.l,
.puck.flying .puck-wing.r {
animation-duration: 0.18s;
}
.puck.resting .puck-wing.l,
.puck.resting .puck-wing.r {
animation-duration: 0.7s;
}
/* sparkle trail dot */
.puck-trail {
position: absolute;
left: 50%;
top: 50%;
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--accent);
opacity: 0;
}
.puck.flying .puck-trail {
animation: trail 0.9s ease-out infinite;
}
@keyframes trail {
0% {
opacity: 0.8;
transform: translate(-50%, -50%) scale(1);
}
100% {
opacity: 0;
transform: translate(-50%, 14px) scale(0.2);
}
}
/* mood tints applied to :root */
.mood-curious {
--puck-body: #8fd6a0;
--puck-hi: #cdeccb;
--puck-wing: rgba(180, 230, 200, 0.34);
--puck-glow: rgba(150, 220, 170, 0.22);
}
.mood-mischief {
--puck-body: #c9a3ec;
--puck-hi: #ead7fa;
--puck-wing: rgba(210, 180, 240, 0.34);
--puck-glow: rgba(190, 150, 235, 0.24);
}
.mood-sleepy {
--puck-body: #7f93c4;
--puck-hi: #c3cdec;
--puck-wing: rgba(170, 185, 225, 0.3);
--puck-glow: rgba(150, 170, 225, 0.2);
}
.mood-proud {
--puck-body: #edc46a;
--puck-hi: #f7e6b3;
--puck-wing: rgba(240, 210, 150, 0.34);
--puck-glow: rgba(231, 184, 92, 0.26);
}
.mood-grumpy {
--puck-body: #d98a72;
--puck-hi: #f0bca8;
--puck-wing: rgba(225, 170, 150, 0.32);
--puck-glow: rgba(210, 130, 100, 0.22);
}
/* ============================================================
Speech bubble (Puck's comments anchored to the sprite)
============================================================ */
.bubble {
position: fixed;
z-index: 61;
max-width: 290px;
pointer-events: auto;
padding: 12px 15px 13px;
border-radius: 15px;
background: var(--panel-2);
border: 0.5px solid var(--border-strong);
backdrop-filter: blur(26px) saturate(150%);
-webkit-backdrop-filter: blur(26px) saturate(150%);
box-shadow:
var(--shadow),
0 0 28px var(--glow);
transform-origin: var(--bub-org, bottom left);
animation: bubpop 0.4s cubic-bezier(0.2, 1.3, 0.4, 1);
}
@keyframes bubpop {
from {
opacity: 0;
transform: scale(0.7) translateY(8px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.bubble.out {
animation: bubout 0.3s ease forwards;
}
@keyframes bubout {
to {
opacity: 0;
transform: scale(0.85) translateY(6px);
}
}
.bubble .bub-src {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.07em;
text-transform: uppercase;
color: var(--accent);
margin-bottom: 5px;
display: flex;
align-items: center;
gap: 6px;
}
.bubble .bub-src .bub-tier {
color: var(--ink-faint);
font-weight: 600;
}
.bubble .bub-text {
font-family: var(--voice);
font-style: italic;
font-size: 16px;
line-height: 1.4;
color: var(--ink);
}
.bubble.style-plain .bub-text {
font-family: var(--ui);
font-style: normal;
font-size: 14px;
}
.bubble.style-hand .bub-text {
font-family: var(--hand);
font-style: normal;
font-size: 20px;
}
.bubble .bub-tail {
position: absolute;
width: 14px;
height: 14px;
background: inherit;
border-left: 0.5px solid var(--border-strong);
border-bottom: 0.5px solid var(--border-strong);
transform: rotate(45deg);
}
.bubble .bub-acts {
display: flex;
gap: 6px;
margin-top: 10px;
}
.bub-rate {
flex: 1;
appearance: none;
border: 0.5px solid var(--border);
background: var(--panel);
color: var(--ink-soft);
font-family: var(--ui);
font-size: 11px;
padding: 5px 4px;
border-radius: 8px;
cursor: pointer;
transition: all 0.13s;
display: flex;
align-items: center;
justify-content: center;
gap: 3px;
}
.bub-rate:hover {
background: var(--accent);
color: var(--accent-ink);
border-color: transparent;
transform: translateY(-1px);
}
/* ============================================================
Companion window — the "note-taking" feed surface
============================================================ */
.comp {
position: fixed;
z-index: 40;
width: 372px;
display: flex;
flex-direction: column;
max-height: calc(100vh - 120px);
}
.comp-head {
display: flex;
align-items: center;
gap: 10px;
padding: 13px 14px 11px;
cursor: grab;
}
.comp-head:active {
cursor: grabbing;
}
.comp-spr {
width: 34px;
height: 34px;
border-radius: 50%;
flex: none;
position: relative;
background: radial-gradient(circle at 35% 30%, var(--puck-hi, #cdeccb), var(--puck-body, #8fd6a0) 70%);
box-shadow: inset -2px -3px 6px rgba(0, 0, 0, 0.25);
}
.comp-spr .e {
position: absolute;
top: 13px;
width: 4px;
height: 6px;
border-radius: 50%;
background: #15201a;
}
.comp-spr .e.l {
left: 11px;
}
.comp-spr .e.r {
right: 11px;
}
.comp-id {
line-height: 1.15;
}
.comp-id b {
font-size: 14px;
}
.comp-id span {
font-size: 11px;
color: var(--ink-soft);
display: block;
font-family: var(--voice);
font-style: italic;
}
.comp-head .comp-x {
margin-left: auto;
appearance: none;
border: 0;
background: transparent;
color: var(--ink-faint);
font-size: 14px;
cursor: pointer;
width: 24px;
height: 24px;
border-radius: 7px;
}
.comp-head .comp-x:hover {
background: var(--panel);
color: var(--ink);
}
.comp-tabs {
display: flex;
gap: 2px;
padding: 0 12px;
border-bottom: 0.5px solid var(--border);
}
.comp-tab {
appearance: none;
border: 0;
background: transparent;
color: var(--ink-soft);
font-family: var(--ui);
font-size: 12.5px;
font-weight: 600;
padding: 9px 11px;
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -0.5px;
display: flex;
align-items: center;
gap: 6px;
}
.comp-tab:hover {
color: var(--ink);
}
.comp-tab.on {
color: var(--ink);
border-bottom-color: var(--accent);
}
.comp-tab .tcount {
font-size: 10px;
background: var(--panel);
padding: 1px 6px;
border-radius: 9px;
font-weight: 700;
}
.comp-body {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 12px 14px;
}
/* feed items */
.feed-day {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--ink-faint);
margin: 4px 0 10px;
}
.feed-item {
display: flex;
gap: 11px;
padding-bottom: 16px;
position: relative;
}
.feed-item::before {
content: "";
position: absolute;
left: 6px;
top: 16px;
bottom: -2px;
width: 1.5px;
background: var(--border);
}
.feed-item:last-child::before {
display: none;
}
.feed-dot {
width: 13px;
height: 13px;
border-radius: 50%;
flex: none;
margin-top: 2px;
background: var(--panel);
border: 2px solid var(--border-strong);
z-index: 1;
}
.feed-dot.notify {
background: var(--accent);
border-color: var(--accent);
box-shadow: 0 0 10px var(--glow);
}
.feed-dot.interrupt {
background: #ff5b54;
border-color: #ff5b54;
}
.feed-dot.ignore {
background: transparent;
}
.feed-main {
flex: 1;
min-width: 0;
}
.feed-meta {
display: flex;
align-items: baseline;
gap: 8px;
margin-bottom: 2px;
}
.feed-src {
font-size: 10.5px;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
color: var(--ink-soft);
}
.feed-time {
font-size: 10px;
color: var(--ink-faint);
font-family: var(--mono);
margin-left: auto;
}
.feed-say {
font-family: var(--voice);
font-style: italic;
font-size: 14.5px;
line-height: 1.42;
color: var(--ink);
}
.feed-say.plain {
font-family: var(--ui);
font-style: normal;
font-size: 13px;
}
.feed-decision {
font-size: 10.5px;
color: var(--ink-faint);
margin-top: 4px;
font-family: var(--mono);
}
.feed-rated {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 10.5px;
margin-top: 5px;
padding: 2px 8px;
border-radius: 9px;
background: var(--panel);
color: var(--ink-soft);
}
/* chat */
.chat-log {
display: flex;
flex-direction: column;
gap: 11px;
}
.chat-msg {
max-width: 86%;
padding: 9px 13px;
border-radius: 14px;
font-size: 13.5px;
line-height: 1.4;
}
.chat-msg.user {
align-self: flex-end;
background: var(--accent);
color: var(--accent-ink);
border-bottom-right-radius: 5px;
}
.chat-msg.puck {
align-self: flex-start;
background: var(--panel);
border: 0.5px solid var(--border);
font-family: var(--voice);
font-style: italic;
font-size: 14.5px;
border-bottom-left-radius: 5px;
}
.chat-input {
display: flex;
gap: 8px;
padding: 11px 14px;
border-top: 0.5px solid var(--border);
}
.chat-input input {
flex: 1;
background: var(--panel);
border: 0.5px solid var(--border);
border-radius: 10px;
padding: 9px 12px;
color: var(--ink);
font-family: var(--ui);
font-size: 13px;
outline: none;
}
.chat-input input:focus {
border-color: var(--accent);
}
.chat-input button {
appearance: none;
border: 0;
background: var(--accent);
color: var(--accent-ink);
border-radius: 10px;
padding: 0 14px;
font-weight: 600;
cursor: pointer;
}
/* memory garden */
.garden {
display: flex;
flex-direction: column;
gap: 9px;
}
.mem {
display: flex;
gap: 11px;
padding: 11px 12px;
border-radius: 11px;
background: var(--panel);
border: 0.5px solid var(--border);
align-items: flex-start;
transition:
transform 0.15s,
box-shadow 0.15s;
}
.mem.pinned {
border-color: var(--accent);
box-shadow:
0 0 0 0.5px var(--accent),
0 0 18px var(--glow);
}
.mem .mem-ico {
font-size: 19px;
flex: none;
line-height: 1.2;
}
.mem .mem-main {
flex: 1;
min-width: 0;
}
.mem .mem-text {
font-size: 13px;
line-height: 1.4;
}
.mem .mem-foot {
display: flex;
align-items: center;
gap: 8px;
margin-top: 7px;
}
.mem .mem-type {
font-size: 9.5px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--ink-faint);
}
.mem .mem-salience {
flex: 1;
height: 4px;
border-radius: 3px;
background: var(--border);
overflow: hidden;
max-width: 80px;
}
.mem .mem-salience i {
display: block;
height: 100%;
background: var(--accent);
transition: width 0.4s;
}
.mem .mem-acts {
display: flex;
gap: 4px;
}
.mem-btn {
appearance: none;
width: 24px;
height: 24px;
border-radius: 7px;
border: 0.5px solid var(--border);
background: transparent;
cursor: pointer;
font-size: 12px;
opacity: 0.6;
transition: all 0.13s;
}
.mem-btn:hover {
opacity: 1;
background: var(--panel-2);
transform: translateY(-1px);
}
.mem-btn.on {
opacity: 1;
background: var(--accent);
border-color: transparent;
}
.empty {
text-align: center;
color: var(--ink-faint);
font-family: var(--voice);
font-style: italic;
font-size: 14px;
padding: 30px 16px;
line-height: 1.5;
}
/* fairy state strip (companion footer) */
.state-strip {
padding: 11px 14px;
border-top: 0.5px solid var(--border);
display: grid;
grid-template-columns: 1fr 1fr;
gap: 9px 16px;
}
.stat-top {
display: flex;
justify-content: space-between;
font-size: 10.5px;
margin-bottom: 4px;
}
.stat-top .sl {
color: var(--ink-soft);
font-weight: 600;
}
.stat-top .sv {
color: var(--ink-faint);
font-variant-numeric: tabular-nums;
}
.stat-bar {
height: 5px;
border-radius: 3px;
background: var(--border);
overflow: hidden;
}
.stat-bar i {
display: block;
height: 100%;
background: var(--accent);
transition: width 0.6s cubic-bezier(0.3, 0.8, 0.3, 1);
border-radius: 3px;
}
.stat-bar i.delta-up {
animation: glowup 1.2s ease;
}
@keyframes glowup {
0%,
100% {
box-shadow: none;
}
40% {
box-shadow: 0 0 12px var(--accent);
}
}
/* ============================================================
Menu-bar dropdown
============================================================ */
.drop {
position: fixed;
top: 30px;
z-index: 70;
width: 280px;
padding: 8px;
}
.drop-row {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
border-radius: 8px;
font-size: 13px;
cursor: pointer;
}
.drop-row:hover {
background: var(--accent);
color: var(--accent-ink);
}
.drop-row .dr-ico {
width: 18px;
text-align: center;
opacity: 0.8;
}
.drop-row .dr-k {
margin-left: auto;
font-size: 11px;
opacity: 0.5;
font-family: var(--mono);
}
.drop-sep {
height: 0.5px;
background: var(--border);
margin: 6px 4px;
}
.drop-status {
padding: 9px 11px 11px;
}
.drop-status .ds-line {
font-family: var(--voice);
font-style: italic;
font-size: 14px;
line-height: 1.4;
color: var(--ink);
}
.drop-modes {
display: flex;
gap: 5px;
margin-top: 10px;
}
.mode-pill {
flex: 1;
text-align: center;
font-size: 10.5px;
font-weight: 600;
padding: 6px 4px;
border-radius: 8px;
background: var(--panel);
border: 0.5px solid var(--border);
cursor: pointer;
color: var(--ink-soft);
}
.mode-pill.on {
background: var(--accent);
color: var(--accent-ink);
border-color: transparent;
}
/* ============================================================
Notifications (toast / interrupt)
============================================================ */
.toasts {
position: fixed;
top: 34px;
right: 12px;
z-index: 65;
display: flex;
flex-direction: column;
gap: 9px;
width: 330px;
}
.toast {
padding: 13px 14px;
border-radius: 14px;
background: var(--panel-2);
border: 0.5px solid var(--border);
backdrop-filter: blur(26px) saturate(150%);
-webkit-backdrop-filter: blur(26px) saturate(150%);
box-shadow: var(--shadow);
animation: toastin 0.45s cubic-bezier(0.2, 1.1, 0.4, 1);
}
@keyframes toastin {
from {
opacity: 0;
transform: translateX(40px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.toast.out {
animation: toastout 0.3s ease forwards;
}
@keyframes toastout {
to {
opacity: 0;
transform: translateX(40px);
}
}
.toast-top {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.toast-top .tt-spr {
width: 22px;
height: 22px;
border-radius: 50%;
flex: none;
background: radial-gradient(circle at 35% 30%, var(--puck-hi, #cdeccb), var(--puck-body, #8fd6a0) 70%);
}
.toast-top .tt-name {
font-size: 12px;
font-weight: 700;
}
.toast-top .tt-time {
margin-left: auto;
font-size: 10.5px;
color: var(--ink-faint);
font-family: var(--mono);
}
.toast-top .tt-hint {
margin-left: auto;
font-size: 10px;
color: var(--ink-faint);
opacity: 0.7;
white-space: nowrap;
}
/* a toast is grab-and-fling: tap opens the app, throw swats it away */
.toast.throw {
cursor: grab;
user-select: none;
touch-action: none;
}
.toast.throw:active {
cursor: grabbing;
}
.toast-say {
font-family: var(--voice);
font-style: italic;
font-size: 14.5px;
line-height: 1.4;
}
.toast-say.plain {
font-family: var(--ui);
font-style: normal;
font-size: 13px;
}
.toast-acts {
display: flex;
gap: 6px;
margin-top: 10px;
}
/* full interrupt overlay */
.interrupt-wrap {
position: fixed;
inset: 0;
z-index: 80;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(3px);
animation: fadein 0.3s;
}
@keyframes fadein {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.interrupt-card {
width: 380px;
padding: 22px;
text-align: center;
animation: bubpop 0.45s cubic-bezier(0.2, 1.3, 0.4, 1);
}
.interrupt-card .int-spr {
width: 56px;
height: 56px;
margin: 0 auto 14px;
position: relative;
background: radial-gradient(circle at 35% 30%, var(--puck-hi, #cdeccb), var(--puck-body, #8fd6a0) 70%);
border-radius: 50%;
box-shadow: 0 0 30px var(--glow);
}
.interrupt-card .int-say {
font-family: var(--voice);
font-style: italic;
font-size: 19px;
line-height: 1.4;
margin-bottom: 16px;
}
.interrupt-card .int-acts {
display: flex;
gap: 9px;
}
.btn {
appearance: none;
border: 0.5px solid var(--border);
background: var(--panel);
color: var(--ink);
font-family: var(--ui);
font-size: 12.5px;
font-weight: 600;
padding: 8px 12px;
border-radius: 9px;
cursor: pointer;
flex: 1;
transition: all 0.13s;
}
.btn:hover {
background: var(--panel-2);
transform: translateY(-1px);
}
.btn.primary {
background: var(--accent);
color: var(--accent-ink);
border-color: transparent;
}
.btn.primary:hover {
filter: brightness(1.08);
}
/* ============================================================
Night Bloom (sleep / wake)
============================================================ */
.bloom {
position: fixed;
inset: 0;
z-index: 90;
display: flex;
align-items: center;
justify-content: center;
background: radial-gradient(circle at 50% 40%, #14202c, #060a0f 80%);
animation: fadein 0.6s;
overflow: hidden;
}
:root[data-theme="candlelight"] .bloom {
background: radial-gradient(circle at 50% 40%, #2a2114, #140d06 80%);
}
.bloom-stars {
position: absolute;
inset: 0;
background-image: radial-gradient(circle, rgba(255, 255, 255, 0.7) 0.6px, transparent 1px);
background-size: 38px 52px;
opacity: 0.18;
animation: twinkle 5s ease-in-out infinite;
}
@keyframes twinkle {
0%,
100% {
opacity: 0.1;
}
50% {
opacity: 0.25;
}
}
.bloom-card {
width: 460px;
max-width: 90vw;
text-align: center;
position: relative;
z-index: 1;
color: #f0e7d0;
}
.bloom-spr {
width: 70px;
height: 70px;
margin: 0 auto 20px;
position: relative;
border-radius: 50%;
background: radial-gradient(circle at 35% 30%, #c3cdec, #7f93c4 70%);
box-shadow: 0 0 50px rgba(150, 170, 225, 0.5);
animation: bob 3.6s ease-in-out infinite;
}
.bloom-spr .ze {
position: absolute;
top: 28px;
width: 8px;
height: 3px;
border-radius: 3px;
background: #1a2436;
}
.bloom-spr .ze.l {
left: 20px;
}
.bloom-spr .ze.r {
right: 20px;
}
.bloom-stage {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.16em;
text-transform: uppercase;
color: rgba(240, 231, 208, 0.5);
margin-bottom: 14px;
min-height: 14px;
}
.bloom-title {
font-family: var(--voice);
font-style: italic;
font-size: 26px;
margin-bottom: 8px;
}
.bloom-text {
font-size: 14px;
line-height: 1.6;
color: rgba(240, 231, 208, 0.78);
min-height: 70px;
}
.bloom-dream {
font-family: var(--voice);
font-style: italic;
font-size: 17px;
line-height: 1.6;
color: rgba(240, 231, 208, 0.9);
padding: 0 10px;
}
.bloom-deltas {
display: flex;
flex-direction: column;
gap: 8px;
margin: 18px 0;
text-align: left;
}
.bloom-delta {
display: flex;
align-items: center;
gap: 10px;
font-size: 13px;
padding: 9px 13px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.05);
border: 0.5px solid rgba(255, 255, 255, 0.12);
animation: deltain 0.5s ease backwards;
}
@keyframes deltain {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
}
}
.bloom-delta .bd-dir {
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.bloom-delta .bd-dir.up {
color: #7fdc9a;
}
.bloom-delta .bd-dir.down {
color: #e89a8a;
}
.bloom-delta .bd-label {
flex: 1;
}
.bloom-mem {
font-family: var(--voice);
font-style: italic;
}
.bloom-prog {
display: flex;
gap: 5px;
justify-content: center;
margin-top: 20px;
}
.bloom-prog i {
width: 6px;
height: 6px;
border-radius: 50%;
background: rgba(240, 231, 208, 0.25);
transition: all 0.3s;
}
.bloom-prog i.on {
background: rgba(240, 231, 208, 0.85);
transform: scale(1.3);
}
.bloom-btn {
margin-top: 24px;
appearance: none;
border: 0.5px solid rgba(255, 255, 255, 0.25);
background: rgba(255, 255, 255, 0.08);
color: #f0e7d0;
font-family: var(--ui);
font-size: 13.5px;
font-weight: 600;
padding: 11px 26px;
border-radius: 11px;
cursor: pointer;
transition: all 0.15s;
}
.bloom-btn:hover {
background: rgba(255, 255, 255, 0.16);
transform: translateY(-1px);
}
/* sleeping-Z floaties */
.zfly {
position: absolute;
font-family: var(--voice);
font-style: italic;
font-size: 22px;
color: rgba(240, 231, 208, 0.5);
animation: zfly 3.2s ease-out infinite;
}
@keyframes zfly {
0% {
opacity: 0;
transform: translateY(0) scale(0.6);
}
30% {
opacity: 0.7;
}
100% {
opacity: 0;
transform: translateY(-60px) translateX(20px) scale(1.2);
}
}
/* ============================================================
Speaking shimmer — sound rings when Puck talks aloud
============================================================ */
.puck-voicewave {
position: absolute;
left: 50%;
top: 50%;
width: 0;
height: 0;
pointer-events: none;
opacity: 0;
}
.puck.speaking .puck-voicewave {
opacity: 1;
}
.puck.speaking .puck-voicewave i {
position: absolute;
left: 0;
top: 0;
border-radius: 50%;
border: 1.5px solid var(--accent);
transform: translate(-50%, -50%);
animation: voicering 1.5s ease-out infinite;
}
.puck.speaking .puck-voicewave i:nth-child(2) {
animation-delay: 0.5s;
}
.puck.speaking .puck-voicewave i:nth-child(3) {
animation-delay: 1s;
}
@keyframes voicering {
0% {
width: 30px;
height: 30px;
opacity: 0.7;
}
100% {
width: 86px;
height: 86px;
opacity: 0;
}
}
.puck.speaking .puck-glow {
animation-duration: 0.7s;
}
/* ============================================================
Automation offer toast — the "want me to handle it?" card
============================================================ */
.toast.offer {
border-color: var(--accent);
box-shadow:
var(--shadow),
0 0 28px var(--glow);
}
.toast.offer .tt-name {
color: var(--accent);
}
/* feed dots for learned / handled */
.feed-dot.handled {
background: var(--panel);
border-color: var(--accent);
position: relative;
}
.feed-dot.handled::after {
content: "✓";
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 8px;
color: var(--accent);
font-weight: 700;
}
.feed-dot.learned {
background: var(--accent);
border-color: var(--accent);
box-shadow: 0 0 12px var(--glow);
}
/* ============================================================
Learning panel
============================================================ */
.learn-note {
font-family: var(--voice);
font-style: italic;
font-size: 13.5px;
line-height: 1.45;
color: var(--ink-soft);
padding: 2px 0 4px;
}
/* Memory tab — Puck's recent observations (peek log) */
.musings {
display: flex;
flex-direction: column;
margin-top: 6px;
}
.musing {
display: flex;
gap: 9px;
padding: 9px 2px;
border-bottom: 1px solid var(--line, rgba(255, 255, 255, 0.06));
}
.musing-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--accent);
margin-top: 6px;
flex: none;
opacity: 0.65;
}
.musing-main {
flex: 1;
min-width: 0;
}
.musing-text {
font-family: var(--voice);
font-style: italic;
font-size: 13.5px;
line-height: 1.4;
}
.musing-time {
font-size: 10.5px;
color: var(--ink-faint);
font-family: var(--mono);
margin-top: 3px;
}
/* a musing is clickable (→ label this peek) */
button.musing {
width: 100%;
text-align: left;
font: inherit;
color: inherit;
background: none;
border: none;
border-bottom: 1px solid var(--line, rgba(255, 255, 255, 0.06));
cursor: pointer;
}
button.musing:hover {
background: rgba(255, 255, 255, 0.04);
}
/* labeling view: screenshot + reuse-a-label chips + a new label */
.label-shot {
display: block;
width: 100%;
border-radius: 10px;
border: 1px solid var(--line, rgba(255, 255, 255, 0.12));
margin: 8px 0;
}
.label-quip {
font-family: var(--voice);
font-style: italic;
font-size: 13px;
color: var(--ink-soft);
margin-bottom: 10px;
}
.label-pick {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 10px;
}
.label-pick .ltag {
cursor: pointer;
border: none;
}
.label-new {
display: flex;
gap: 6px;
}
.label-new input {
flex: 1;
min-width: 0;
font: inherit;
font-size: 12px;
color: var(--ink);
background: var(--surface-2, rgba(0, 0, 0, 0.08));
border: 1px solid var(--line, rgba(0, 0, 0, 0.12));
border-radius: 8px;
padding: 6px 8px;
}
.label-new button {
font: inherit;
font-size: 12px;
cursor: pointer;
background: var(--accent);
color: var(--accent-ink, #fff7e8);
border: none;
border-radius: 8px;
padding: 6px 12px;
}
.label-back {
font: inherit;
font-size: 12px;
cursor: pointer;
background: none;
color: var(--ink-faint);
border: none;
margin-top: 12px;
padding: 4px 0;
}
.label-saved {
font-size: 13px;
color: var(--accent);
padding: 10px 0;
}
.learn-empty {
font-size: 12.5px;
color: var(--ink-faint);
font-style: italic;
padding: 2px 0;
}
.learn-sources {
display: flex;
flex-direction: column;
gap: 11px;
}
.lsrc {
display: flex;
gap: 11px;
align-items: flex-start;
}
.lsrc-glyph {
font-size: 16px;
width: 22px;
text-align: center;
flex: none;
opacity: 0.85;
margin-top: 1px;
}
.lsrc-main {
flex: 1;
min-width: 0;
}
.lsrc-top {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 8px;
margin-bottom: 5px;
}
.lsrc-label {
font-size: 12.5px;
font-weight: 600;
}
.lsrc-disp {
font-size: 10.5px;
font-weight: 700;
letter-spacing: 0.02em;
padding: 2px 8px;
border-radius: 9px;
background: var(--panel);
white-space: nowrap;
}
.lsrc-disp.hi {
color: var(--accent-ink);
background: var(--accent);
}
.lsrc-disp.mid {
color: var(--accent);
}
.lsrc-disp.lo {
color: var(--ink-soft);
}
.lsrc-disp.off {
color: var(--ink-faint);
}
.lsrc-track {
height: 6px;
border-radius: 4px;
background: var(--border);
overflow: hidden;
position: relative;
}
.lsrc-track::before {
content: "";
position: absolute;
left: 50%;
top: 0;
bottom: 0;
width: 1px;
background: var(--border-strong);
z-index: 1;
}
.lsrc-fill {
display: block;
height: 100%;
background: var(--accent);
transition: width 0.5s cubic-bezier(0.3, 0.8, 0.3, 1);
border-radius: 4px;
}
.lsrc-fill.off,
.lsrc-fill.lo {
background: var(--ink-faint);
}
.lsrc-conf {
font-size: 10px;
color: var(--ink-faint);
font-family: var(--mono);
margin-top: 4px;
}
.learn-tags {
display: flex;
flex-wrap: wrap;
gap: 7px;
}
.ltag {
font-size: 12px;
padding: 5px 11px;
border-radius: 20px;
background: var(--panel);
border: 0.5px solid var(--border);
color: var(--ink);
white-space: nowrap;
}
.ltag.irk {
border-color: rgba(230, 140, 110, 0.4);
color: var(--ink-soft);
}
.learn-autos {
display: flex;
flex-direction: column;
gap: 8px;
}
.lauto {
display: flex;
gap: 11px;
padding: 11px 12px;
border-radius: 11px;
align-items: flex-start;
background: var(--panel);
border: 0.5px solid var(--accent);
box-shadow: 0 0 0 0.5px var(--accent);
}
.lauto-check {
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--accent);
color: var(--accent-ink);
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 800;
flex: none;
margin-top: 1px;
}
.lauto-verb {
font-size: 13px;
font-weight: 600;
}
.lauto-done {
font-size: 11px;
color: var(--ink-soft);
font-family: var(--mono);
margin-top: 3px;
}
/* ============================================================
Settings panel (Tweaks reborn as product UI)
============================================================ */
.settings {
position: fixed;
z-index: 75;
width: 340px;
display: flex;
flex-direction: column;
max-height: calc(100vh - 120px);
}
.set-body {
display: flex;
flex-direction: column;
gap: 10px;
}
.set-body .sect-label {
margin-top: 8px;
}
.set-body .sect-label:first-child {
margin-top: 0;
}
.set-row {
display: flex;
align-items: center;
gap: 10px;
}
.set-label {
font-size: 12px;
color: var(--ink-soft);
width: 88px;
flex: none;
}
.set-ctl {
flex: 1;
min-width: 0;
}
.set-pills {
display: flex;
gap: 5px;
flex-wrap: wrap;
}
.set-pills .mode-pill {
flex: 1 1 auto;
appearance: none;
font-family: var(--ui);
}
.set-swatches {
display: flex;
gap: 7px;
}
.set-swatch {
appearance: none;
width: 22px;
height: 22px;
border-radius: 50%;
cursor: pointer;
border: 2px solid transparent;
transition:
transform 0.13s,
border-color 0.13s;
}
.set-swatch:hover {
transform: scale(1.12);
}
.set-swatch.on {
border-color: var(--ink);
transform: scale(1.12);
}
.set-slider {
display: flex;
align-items: center;
gap: 9px;
}
.set-slider input[type="range"] {
flex: 1;
accent-color: var(--accent);
}
.set-val {
font-size: 11px;
font-family: var(--mono);
color: var(--ink-faint);
width: 24px;
text-align: right;
}
.set-actions {
display: flex;
gap: 8px;
margin-top: 6px;
}
/* menubar puck item is a real <button> now */
.mb-puck {
appearance: none;
border: 0;
background: transparent;
color: inherit;
font: inherit;
}
/* ============================================================
Overlay mode — transparent shell over the real desktop
============================================================ */
html.overlay,
html.overlay body {
background: transparent !important;
}
html.overlay .puck-hit {
cursor: pointer;
}
/* during a real-screen capture, blank Puck so his own sprite/bubbles don't land
in the screenshot he's about to read (the overlay window is otherwise clear) */
html.capturing #root {
opacity: 0;
transition: none;
}
/* Glassier comment surfaces over the real desktop — let it show through, but keep
the blur so text stays legible (fully transparent would kill readability). */
html.overlay .bubble,
html.overlay .toast {
background: color-mix(in srgb, var(--panel-2) 60%, transparent);
backdrop-filter: blur(32px) saturate(160%);
-webkit-backdrop-filter: blur(32px) saturate(160%);
}
/* ============================================================
Attention: alert ring when something's waiting + hover-grow.
On a busy desktop a quiet bob is missable; the ring isn't.
============================================================ */
.puck-alert-ring {
position: absolute;
left: 50%;
top: 50%;
width: 38px;
height: 38px;
margin: -19px 0 0 -19px;
border-radius: 50%;
border: 2px solid var(--accent);
opacity: 0;
pointer-events: none;
}
.puck.alert .puck-alert-ring {
animation: alertring 1.4s ease-out infinite;
}
@keyframes alertring {
0% {
opacity: 0.8;
transform: scale(0.7);
}
70% {
opacity: 0;
transform: scale(2.1);
}
100% {
opacity: 0;
transform: scale(2.1);
}
}
/* alert also quickens the bob and warms the glow so the whole sprite reads "!" */
.puck.alert .puck-bob {
animation-duration: 1.5s;
}
.puck.alert .puck-glow {
animation-duration: 1.4s;
}
/* hover-grow on the pokeable creature — tactile "I'm clickable" feedback */
.puck-bob {
transition: transform 0.18s cubic-bezier(0.3, 0.8, 0.3, 1.4);
}
.puck-hit:hover ~ .puck-bob,
.puck-bob:has(.puck-hit:hover) {
transform: scale(1.18);
}
/* settings inline note (e.g. vision cost explainer) */
.set-note {
font-size: 11px;
line-height: 1.4;
color: var(--ink-faint);
font-style: italic;
padding: 2px 0 2px;
}
/* voice picker — native select + audition button */
.set-voicepick {
display: flex;
gap: 6px;
align-items: center;
}
.set-select {
flex: 1;
min-width: 0;
font: inherit;
font-size: 12px;
color: var(--ink);
background: var(--surface-2, rgba(0, 0, 0, 0.08));
border: 1px solid var(--line, rgba(0, 0, 0, 0.12));
border-radius: 8px;
padding: 5px 7px;
}
/* molt gauge on the Night Bloom wake screen — real trace accumulation */
.bloom-molt {
margin: 14px auto 4px;
max-width: 320px;
text-align: left;
}
.bm-row {
display: flex;
justify-content: space-between;
font-size: 12px;
letter-spacing: 0.3px;
color: rgba(255, 247, 224, 0.82);
margin-bottom: 5px;
}
.bm-bar {
height: 6px;
border-radius: 99px;
background: rgba(255, 255, 255, 0.12);
overflow: hidden;
}
.bm-bar i {
display: block;
height: 100%;
border-radius: 99px;
background: linear-gradient(90deg, var(--accent), #fff7e0);
transition: width 0.8s ease;
}
.bm-note {
margin-top: 7px;
font-size: 11px;
line-height: 1.5;
color: rgba(255, 247, 224, 0.6);
font-style: italic;
}
/* ============================================================
Reactions — one-shot personality gestures. The react-* class
lands on .puck; .puck-bob is keyed in React so the animation
restarts even when the same reaction fires twice in a row.
Each keyframe starts and ends at neutral so the resting bob
resumes seamlessly when the class is removed. Durations mirror
ReactionKind MS in engine/reactions.ts.
============================================================ */
.puck[class*="react-"] .puck-bob {
animation-iteration-count: 1;
}
.puck.react-celebrate .puck-bob {
animation: react-celebrate 1.1s cubic-bezier(0.3, 0.7, 0.3, 1.3);
}
@keyframes react-celebrate {
0% {
transform: translateY(0) scale(1) rotate(0);
}
20% {
transform: translateY(-15px) scale(1.18) rotate(-8deg);
}
45% {
transform: translateY(2px) scale(0.96) rotate(6deg);
}
65% {
transform: translateY(-9px) scale(1.08) rotate(-4deg);
}
100% {
transform: translateY(0) scale(1) rotate(0);
}
}
.puck.react-nani .puck-bob {
animation: react-nani 0.9s cubic-bezier(0.2, 1.5, 0.3, 1);
}
@keyframes react-nani {
0% {
transform: scale(1);
}
14% {
transform: scale(1.03) translateX(-2px);
} /* the freeze beat */
22% {
transform: scale(1.46) translateY(-6px);
} /* SNAP zoom */
34% {
transform: scale(1.4) translateX(4px);
}
46% {
transform: scale(1.43) translateX(-4px);
}
60% {
transform: scale(1.41);
}
100% {
transform: scale(1);
}
}
.puck.react-perk .puck-bob {
animation: react-perk 0.75s ease;
}
@keyframes react-perk {
0% {
transform: scale(1) translateY(0);
}
40% {
transform: scale(1.16) translateY(-7px);
}
100% {
transform: scale(1) translateY(0);
}
}
.puck.react-shrug .puck-bob {
animation: react-shrug 1s ease-in-out;
}
@keyframes react-shrug {
0%,
100% {
transform: translateY(0) rotate(0) scale(1);
}
25% {
transform: translateY(3px) rotate(-7deg) scale(0.97);
}
60% {
transform: translateY(3px) rotate(7deg) scale(0.97);
}
}
.puck.react-sulk .puck-bob {
animation: react-sulk 1.05s ease;
}
@keyframes react-sulk {
0%,
100% {
transform: translateY(0) scale(1);
opacity: 1;
}
35% {
transform: translateY(9px) scale(0.85);
opacity: 0.65;
}
}
.puck.react-dance .puck-bob {
animation: react-dance 1.5s ease-in-out;
}
@keyframes react-dance {
0%,
100% {
transform: translateY(0) rotate(0);
}
15% {
transform: translateY(-6px) rotate(-14deg);
}
35% {
transform: translateY(0) rotate(12deg);
}
55% {
transform: translateY(-6px) rotate(-12deg);
}
75% {
transform: translateY(0) rotate(10deg);
}
}
.puck.react-summon .puck-bob {
animation: react-summon 1.6s cubic-bezier(0.34, 1.56, 0.5, 1);
}
@keyframes react-summon {
0% {
transform: scale(1) translateY(0);
}
18% {
transform: scale(1.42) translateY(-12px) rotate(-6deg);
} /* big size-up */
34% {
transform: scale(1.34) translateY(2px) rotate(5deg);
}
50% {
transform: scale(1.42) translateY(-9px) rotate(-5deg);
} /* insistent second hop */
66% {
transform: scale(1.34) translateY(2px) rotate(4deg);
}
82% {
transform: scale(1.38) translateY(-5px) rotate(0deg);
}
100% {
transform: scale(1) translateY(0) rotate(0);
}
}
.puck.react-pop .puck-bob {
animation: react-pop 0.48s cubic-bezier(0.3, 0.8, 0.3, 1.4);
}
@keyframes react-pop {
0%,
100% {
transform: scale(1);
}
45% {
transform: scale(1.16);
}
}
/* laugh — giddy little bounces with a rocking wobble (found something funny) */
.puck.react-laugh .puck-bob {
animation: react-laugh 1.2s ease-in-out;
}
@keyframes react-laugh {
0%,
100% {
transform: translateY(0) rotate(0) scale(1);
}
15% {
transform: translateY(-8px) rotate(-7deg) scale(1.06);
}
30% {
transform: translateY(1px) rotate(6deg) scale(0.98);
}
45% {
transform: translateY(-7px) rotate(-6deg) scale(1.05);
}
60% {
transform: translateY(1px) rotate(5deg) scale(0.98);
}
75% {
transform: translateY(-5px) rotate(-4deg) scale(1.03);
}
}
/* sad — a slow droop, shrinking and dimming (something poignant or lonely) */
.puck.react-sad .puck-bob {
animation: react-sad 1.3s ease;
}
@keyframes react-sad {
0%,
100% {
transform: translateY(0) scale(1);
filter: none;
}
35%,
70% {
transform: translateY(7px) scale(0.9) rotate(-3deg);
filter: brightness(0.68) saturate(0.6);
}
}
/* fret — a startled recoil then a nervous side-to-side tremble (the human's upset) */
.puck.react-fret .puck-bob {
animation: react-fret 1.1s ease;
}
@keyframes react-fret {
0%,
100% {
transform: translate(0, 0) scale(1);
}
10% {
transform: translate(0, -6px) scale(1.08);
}
25% {
transform: translateX(-4px) scale(1.04);
}
37% {
transform: translateX(4px) scale(1.04);
}
49% {
transform: translateX(-3px) scale(1.02);
}
61% {
transform: translateX(3px) scale(1.02);
}
73% {
transform: translateX(-2px);
}
85% {
transform: translateX(2px);
}
}
/* Emotion COLOR — the aura flushes a feeling-hue for the gesture's duration, then
reverts to the slow mood tint (--puck-glow is overridden on .puck, which beats the
mood-* ancestor and cascades to .puck-glow). Layered on top of the body's mood color
so curiosity stays "his" color and only the strong feelings recolor him. */
.puck.react-nani {
--puck-glow: rgba(244, 244, 255, 0.62); /* white surprise flash */
}
.puck.react-laugh {
--puck-glow: rgba(246, 212, 120, 0.5); /* warm giggle gold */
}
.puck.react-celebrate,
.puck.react-dance {
--puck-glow: rgba(255, 206, 105, 0.62); /* bright delight */
}
.puck.react-celebrate .puck-glow,
.puck.react-dance .puck-glow {
animation-duration: 0.6s; /* giddy fast pulse */
}
.puck.react-fret {
--puck-glow: rgba(240, 92, 74, 0.58); /* alarm red */
}
.puck.react-fret .puck-glow {
animation-duration: 0.45s; /* anxious flicker */
}
.puck.react-sad {
--puck-glow: rgba(108, 148, 230, 0.42); /* cool, lonely blue */
}
.puck.react-sad .puck-glow {
animation-duration: 5s; /* slow, heavy */
}
/* muted — watching but silent: calm the glow, drop a small badge */
.puck.muted .puck-glow {
opacity: 0.35;
animation-duration: 6s;
}
.puck-mute {
position: absolute;
right: -2px;
top: -2px;
font-size: 13px;
line-height: 1;
filter: grayscale(0.3);
pointer-events: none;
z-index: 4;
}
/* camo — after sitting still, Puck cloaks into an active-camo patch (the per-rule
comments below cover the visual; the gist: what's behind reads through sharp, with a
shimmer + rim so he's a cloaked shape, not a hole). Fades in over ~1.2s; dropping the
class is an instant "boo!" reveal. Trigger lives in SimApp's idle loop. */
.puck.camo .puck-body {
/* Active camo, not transparency: the fill goes clear so what's behind reads through
SHARP (overlay: the real desktop through the transparent window; sim: the page).
NO blur — blur is what made it unreadable. */
background: transparent !important;
/* the camo TELL — a non-blurring shimmer (brightness/contrast/hue keep text legible,
unlike blur) so his shape registers as a cloaked patch, not a hole. The backdrop
shimmer only renders in the sim (the overlay's transparent window has nothing to
sample); the refractive rim below carries the tell in both habitats. */
backdrop-filter: brightness(1.08) contrast(1.05) hue-rotate(10deg);
-webkit-backdrop-filter: brightness(1.08) contrast(1.05) hue-rotate(10deg);
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.13),
0 0 8px rgba(170, 215, 255, 0.16);
transition:
background 1.2s ease,
backdrop-filter 1.2s ease,
box-shadow 1.2s ease;
}
.puck.camo .puck-wing,
.puck.camo .puck-cheek,
.puck.camo .puck-trail,
.puck.camo .puck-glow {
opacity: 0.07;
transition: opacity 1.2s ease;
}
.puck.camo .puck-eye {
opacity: 0.34; /* a faint watching glimmer through the cloak */
transition: opacity 1.2s ease;
}
/* the shout — a quick chat-flash that floats up and fades over the sprite */
.puck-shout {
position: absolute;
left: 50%;
bottom: 100%;
white-space: nowrap;
font-weight: 800;
font-size: 14px;
letter-spacing: 0.2px;
color: var(--accent-ink, #fff7e8);
background: var(--accent);
padding: 2px 9px;
border-radius: 11px;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.25);
pointer-events: none;
z-index: 3;
animation: shout-pop 1s ease forwards;
}
@keyframes shout-pop {
0% {
opacity: 0;
transform: translate(-50%, 6px) scale(0.6);
}
18% {
opacity: 1;
transform: translate(-50%, -6px) scale(1.1);
}
35% {
transform: translate(-50%, -8px) scale(1);
}
80% {
opacity: 1;
transform: translate(-50%, -12px) scale(1);
}
100% {
opacity: 0;
transform: translate(-50%, -22px) scale(0.95);
}
}