hollow / styles.css
Pabloler21's picture
fix(game): bottom-anchor the lone opening greeting via .bubble-wrap flex
a086c41
Raw
History Blame Contribute Delete
70 kB
/* Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€
HOLLOW Ò€” Horror CSS for Gradio 6.x
Strategy: dissolve the Gradio "form" chrome into one
continuous void, then layer atmosphere on top.
No JS. @keyframes only (grain, vignette breath,
title flicker, Bond heartbeat).
Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€ */
/* Fonts are loaded via a <link> in <head> (app.py _HEAD_JS): an @import here is
rejected on the Space ("@import rules are not allowed here") because Gradio
injects this stylesheet after its own, so @import isn't the first rule. */
/* Ò”€Ò”€ 1. Theme variable overrides Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€ */
:root {
/* Backgrounds */
--body-background-fill: #0a0810;
--background-fill-primary: #0c0a14;
--background-fill-secondary: transparent; /* Hollow's words float */
/* Accent drives the visitor bubble + focus */
--color-accent: #7a4f8a;
--color-accent-soft: #15101f; /* visitor bubble bg */
/* Borders Ò€” kept near-invisible; the void is continuous */
--border-color-primary: #14101f;
--border-color-accent: #2e2142;
--border-color-accent-subdued: #14101f;
/* Text */
--body-text-color: #c0b8ce;
--body-text-color-subdued: #5a5070;
/* Labels Ò€” almost gone, like worn engraving */
--block-label-text-color: #352f4a;
--block-title-text-color: #352f4a;
--block-label-background-fill: transparent;
--block-info-text-color: #352f4a;
/* Inputs */
--input-background-fill: transparent;
--input-background-fill-focus: transparent;
--input-border-color: #221b34;
--input-border-color-focus: #5a3e72;
--input-placeholder-color: #38305a;
--input-shadow: none;
--input-shadow-focus: none;
/* Buttons */
--button-secondary-background-fill: rgba(30,20,46,0.5);
--button-secondary-background-fill-hover: rgba(44,26,64,0.6);
--button-secondary-text-color: #9a7caa;
--button-shadow: none;
--button-shadow-active: none;
--button-shadow-hover: none;
/* Blocks / panels Ò€” chrome dissolved */
--block-background-fill: transparent;
--block-border-color: transparent;
--block-border-width: 0px;
--block-shadow: none;
--panel-background-fill: transparent;
/* Slider */
--slider-color: #7a4f8a;
/* memory/humanity accent (recall line, recovered items, feedback pulses) β€”
distinct from the dried-blood red reserved for cruelty/bad-ending */
--mem-accent: #e0b283;
--mem-glow: rgba(200,130,70,0.55);
/* Typography */
--font: 'Crimson Text', Georgia, 'Times New Roman', serif;
--font-mono: 'Special Elite', 'Courier New', monospace;
/* Shadows off Ò€” depth comes from atmosphere, not cards */
--shadow-drop: none;
--shadow-drop-lg: none;
--shadow-spread: 0;
}
/* Ò”€Ò”€ 2. The void + breathing fog Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€ */
body {
background-color: #0a0810 !important;
background-image:
radial-gradient(ellipse at 14% -5%, rgba(58,20,84,0.20) 0%, transparent 52%) !important,
radial-gradient(ellipse at 86% 105%, rgba(24,8,44,0.26) 0%, transparent 52%) !important;
background-attachment: fixed !important;
min-height: 100vh;
}
/* Dried-blood stains Ò€” own fixed layer ABOVE the vignette so the corner
darkening can't eat them. Painted over the scene like stains on glass.
Kept off the deep edges; the breathing pulse comes from being in this
layer's neighbourhood, not the vignette. */
.gradio-container::after {
content: '';
position: fixed;
inset: 0;
pointer-events: none;
z-index: 1000;
background:
radial-gradient(ellipse 18% 13% at 70% 30%, rgba(112,70,74,0.26) 0%, transparent 66%),
radial-gradient(ellipse 11% 17% at 18% 55%, rgba(104,64,68,0.22) 0%, transparent 66%),
radial-gradient(ellipse 15% 10% at 52% 78%, rgba(108,66,70,0.20) 0%, transparent 66%),
radial-gradient(ellipse 9% 12% at 38% 34%, rgba(100,62,66,0.18) 0%, transparent 66%),
radial-gradient(ellipse 10% 8% at 84% 64%, rgba(108,68,72,0.20) 0%, transparent 66%);
}
/* Vignette + top fog, breathing slowly like something asleep */
body::before {
content: '';
position: fixed;
inset: 0;
pointer-events: none;
z-index: 1000;
background:
radial-gradient(ellipse at 50% 36%, transparent 38%, rgba(4,3,8,0.58) 100%),
linear-gradient(to bottom, rgba(6,4,12,0.62) 0%, transparent 13%);
animation: dread-breath 11s ease-in-out infinite;
}
@keyframes dread-breath {
0%, 100% { opacity: 0.85; }
50% { opacity: 1.00; }
}
/* Film grain Ò€” fixed, jittering, very faint */
body::after {
content: '';
position: fixed;
inset: -50%;
width: 200%;
height: 200%;
pointer-events: none;
z-index: 1001;
opacity: 0.05;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
animation: grain 1.1s steps(4) infinite;
}
@keyframes grain {
0% { transform: translate(0, 0); }
25% { transform: translate(-2%, 1%); }
50% { transform: translate(1%, -2%); }
75% { transform: translate(-1%, 2%); }
100% { transform: translate(0, 0); }
}
/* Ò”€Ò”€ 3. Container Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€ */
.gradio-container {
background: transparent !important;
font-family: 'Crimson Text', Georgia, serif !important;
font-size: 1.05rem !important;
}
/* Dissolve every panel into the void */
.block,
.form,
.panel,
.gr-box,
.gr-panel {
background: transparent !important;
border: none !important;
box-shadow: none !important;
}
/* Ò”€Ò”€ 4. Title Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€ */
.gradio-container h2 {
font-family: 'Special Elite', 'Courier New', monospace !important;
color: #8a6a9a !important;
letter-spacing: 0.18em !important;
font-size: 1.75rem !important;
font-weight: 400 !important;
text-shadow:
0 0 24px rgba(110,55,145,0.45),
0 0 64px rgba(80,30,115,0.20);
animation: title-flicker 8s ease-in-out infinite;
}
@keyframes title-flicker {
0%, 91%, 96%, 100% { opacity: 1; }
93% { opacity: 0.66; }
94% { opacity: 1; }
95% { opacity: 0.82; }
}
/* Subtitle whisper */
.gradio-container h2 + p,
.gradio-container h2 + p em {
color: #6f5f88 !important;
font-style: italic;
font-size: 0.92rem;
letter-spacing: 0.05em;
}
/* Ò”€Ò”€ 5. Kill Gradio chatbot chrome Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€ */
/* Toolbar (share / copy / fullscreen) breaks the fiction.
Best killed via app.py flags too; this is the backstop. */
.icon-button-wrapper,
.chatbot .icon-button-wrapper,
button[aria-label="Share"],
button[aria-label="Copy"],
button[aria-label="Copy conversation"],
button[aria-label="Fullscreen"],
button[aria-label="Clear"] {
display: none !important;
}
/* The message scroll area Ò€” let it bleed into the void */
.chatbot,
.bubble-wrap,
.message-wrap {
background: transparent !important;
border: none !important;
}
/* Ò”€Ò”€ 6. Hollow's words (bot) Ò€” formless, haunted Ò”€Ò”€ */
.bot {
background: transparent !important;
border: none !important;
border-left: 2px solid #3a2a50 !important;
border-radius: 0 !important;
box-shadow: none !important;
}
.bot .prose,
.bot .message,
.bot p {
font-family: 'Crimson Text', Georgia, serif !important;
font-style: italic !important;
font-size: 1.16rem !important;
color: #b3aac4 !important;
line-height: 1.7 !important;
text-shadow: 0 0 18px rgba(120,70,160,0.18);
}
/* Ò”€Ò”€ 7. Visitor's words (user) Ò€” contained, human Ò”€Ò”€ */
.user {
background: var(--color-accent-soft) !important;
border: 1px solid #211a32 !important;
border-radius: 7px !important;
box-shadow: none !important;
}
.user .prose,
.user .message,
.user p {
font-family: 'Crimson Text', Georgia, serif !important;
font-style: normal !important;
font-size: 1.05rem !important;
color: #cec6dc !important;
line-height: 1.6 !important;
}
/* Ò”€Ò”€ 8. Input Ò€” a line of confession, not a box Ò”€Ò”€ */
textarea,
.gradio-container input[type="text"] {
background: transparent !important;
border: none !important;
border-bottom: 1px solid #2a2340 !important;
border-radius: 0 !important;
font-family: 'Crimson Text', Georgia, serif !important;
font-size: 1.08rem !important;
color: #c8c0d6 !important;
caret-color: #7a4f8a;
letter-spacing: 0.01em;
box-shadow: none !important;
transition: border-color 0.4s ease, box-shadow 0.4s ease !important;
}
textarea:focus,
.gradio-container input[type="text"]:focus {
border-bottom-color: #6a4a82 !important;
box-shadow: 0 8px 18px -14px rgba(120,70,160,0.7) !important;
}
textarea::placeholder {
color: #5a5078 !important;
font-style: italic;
}
#game-inputrow textarea::placeholder {
color: #6a6090 !important;
}
/* Ò”€Ò”€ 9. Buttons (the Ò†’ send) Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€ */
.gradio-container button {
font-family: 'Crimson Text', Georgia, serif !important;
font-size: 1.25rem !important;
letter-spacing: 0.05em;
color: #9a7caa !important;
background: rgba(30,20,46,0.45) !important;
border: 1px solid #2a2040 !important;
border-radius: 6px !important;
box-shadow: none !important;
transition: color 0.35s, border-color 0.35s, box-shadow 0.35s, background 0.35s !important;
}
.gradio-container button:hover {
color: #d3bee2 !important;
border-color: #5a3e72 !important;
background: rgba(44,26,64,0.6) !important;
box-shadow: 0 0 16px rgba(120,60,160,0.38) !important;
}
/* Ò”€Ò”€ 10. Bond meter Ò€” custom HTML, a pulsing thread Ò”€Ò”€ */
/* Rendered by _render_bond() in app.py. Full control:
no Gradio orange, no track, no number box, no reset. */
.bond-meter {
padding: 4px 2px 2px;
user-select: none;
}
.bond-head {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 9px;
}
.bond-name {
font-family: 'Special Elite', monospace;
font-size: 0.7rem;
letter-spacing: 0.32em;
text-transform: uppercase;
color: #4a4063;
}
.bond-tier {
font-family: 'Special Elite', monospace;
font-size: 0.84rem;
letter-spacing: 0.1em;
color: #bcb4c8;
text-shadow: 0 0 12px rgba(150,145,170,0.35);
}
.bond-track {
position: relative;
height: 2px;
border-radius: 2px;
background: #1a1428;
}
.bond-fill {
position: absolute;
left: 0;
top: 0;
height: 100%;
background: linear-gradient(to right, #2b2832, #bcb4c8);
border-radius: 2px;
box-shadow: 0 0 10px rgba(180,174,196,0.35);
transition: width 1.2s cubic-bezier(0.2,0.7,0.2,1);
}
.bond-beat {
position: absolute;
top: -8px;
width: 8px;
height: 18px;
margin-left: -4px;
background: #cdc6d8;
border-radius: 2px;
box-shadow: 0 0 8px rgba(190,184,206,0.6);
animation: heartbeat 2.4s ease-in-out infinite;
transition: left 1.2s cubic-bezier(0.2,0.7,0.2,1);
}
@keyframes heartbeat {
0%, 100% { transform: scaleY(1.0); box-shadow: 0 0 4px rgba(180,174,196,0.40); }
8% { transform: scaleY(1.5); box-shadow: 0 0 14px rgba(205,198,216,0.85); }
18% { transform: scaleY(1.0); box-shadow: 0 0 4px rgba(180,174,196,0.40); }
30% { transform: scaleY(1.25); box-shadow: 0 0 9px rgba(195,188,208,0.62); }
45% { transform: scaleY(1.0); box-shadow: 0 0 4px rgba(180,174,196,0.35); }
}
/* Ò”€Ò”€ 12. Scrollbar Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€ */
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #2a1f40; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #3d2f58; }
/* Ò”€Ò”€ 13. Hide Gradio footer Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€ */
footer { display: none !important; }
/* Ò”€Ò”€ 14. Respect reduced motion Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€ */
@media (prefers-reduced-motion: reduce) {
body::before,
body::after,
.gradio-container h2,
.bond-beat {
animation: none !important;
}
}
/* Ò”€Ò”€ 15. Entity panel Ò€” the child behind a gothic arch Ò”€Ò”€ */
.entity-portal {
position: relative;
aspect-ratio: 5 / 6;
}
/* the scene is masked to a pointed (lancet) arch Ò€” a cathedral window */
.entity-stage {
position: absolute;
inset: 0;
overflow: hidden;
/* a faint ground mist behind the child: the portrait's dark lower half
silhouettes against it instead of sinking into pure black */
background: radial-gradient(ellipse 100% 55% at 50% 105%, #413d52, #0b0a10 78%);
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 120' preserveAspectRatio='none'%3E%3Cpath d='M0,120 L0,52 A58,58 0 0 1 50,4 A58,58 0 0 1 100,52 L100,120 Z' fill='%23fff'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 120' preserveAspectRatio='none'%3E%3Cpath d='M0,120 L0,52 A58,58 0 0 1 50,4 A58,58 0 0 1 100,52 L100,120 Z' fill='%23fff'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
}
/* carved-stone lancet arch: gradient bevel (light top-left, recessed bottom-
right), dark inner groove, faint highlight. No keystone, no corner blocks Ò€”
just worn stone tracing the masked edge so the child sits flush inside it. */
.entity-stage::after {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
z-index: 3;
background-image: var(--stone-grain);
background-size: 130px;
opacity: 0.10;
mix-blend-mode: soft-light;
}
.entity-frame {
position: absolute;
inset: 0;
pointer-events: none;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 120' preserveAspectRatio='none'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0' stop-color='%235a5560'/%3E%3Cstop offset='0.5' stop-color='%232a2632'/%3E%3Cstop offset='1' stop-color='%230c0a10'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cpath d='M2.5,120 L2.5,52 A55,55 0 0 1 50,6 A55,55 0 0 1 97.5,52 L97.5,120' fill='none' stroke='url(%23g)' stroke-width='6'/%3E%3Cpath d='M5,120 L5,53 A53,53 0 0 1 50,9 A53,53 0 0 1 95,53 L95,120' fill='none' stroke='%230c0a10' stroke-width='1.4'/%3E%3Cpath d='M6.2,120 L6.2,53 A52,52 0 0 1 50,10.4 A52,52 0 0 1 93.8,53 L93.8,120' fill='none' stroke='%2346424e' stroke-width='0.6'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
filter: drop-shadow(1px 1px 0 rgba(120,114,130,0.18))
drop-shadow(-1px -1px 1px rgba(0,0,0,0.6));
}
.entity-figure {
position: absolute;
inset: 0;
}
.entity-sway {
animation: entity-sway 9s ease-in-out infinite alternate;
transform-origin: 50% 100%;
}
@keyframes entity-sway {
from { transform: rotate(-0.5deg) translateX(-2px); }
to { transform: rotate(0.5deg) translateX(2px); }
}
/* !important: Gradio's base img reset overrides plain height/width here β€”
without it the square portrait leaves a dead band at the arch bottom */
.entity-img {
position: absolute;
inset: 0;
width: 100% !important;
height: 100% !important;
object-fit: cover !important;
}
/* terror flicker: a fast subliminal convulsion through every face. Hard
steps() cuts, ~0.55s total, too quick to read. The DOM is replaced each
turn so the one-shot animations always restart. */
.entity-flash-wrap {
position: absolute;
inset: 0;
}
.entity-flick {
position: absolute;
inset: 0;
width: 100% !important;
height: 100% !important;
/* match the base silhouette exactly (contain + bottom + same fade mask) so
the flicker/convulse/settle faces never balloon past the standing child */
object-fit: contain !important;
object-position: bottom center !important;
-webkit-mask: linear-gradient(#000 78%, transparent 99%);
mask: linear-gradient(#000 78%, transparent 99%);
opacity: 0;
animation-duration: 0.55s;
animation-timing-function: steps(1, end);
animation-fill-mode: forwards;
}
.entity-flash-wrap.entity-flash-strong .entity-flick { animation-duration: 0.7s; }
/* each face blinks in its own scattered windows Ò€” they overlap into chaos */
.flick-0 { animation-name: flick-terror; } /* terror Ò€” appears most */
.flick-1 { animation-name: flick-almost; }
.flick-2 { animation-name: flick-end; }
.flick-3 { animation-name: flick-base; } /* settles back to the portrait */
@keyframes flick-terror {
0%, 100% { opacity: 0; }
4% { opacity: 1; } 11% { opacity: 0; }
34% { opacity: 1; } 42% { opacity: 0; }
62% { opacity: 1; } 70% { opacity: 0; }
}
@keyframes flick-almost {
0%, 100% { opacity: 0; }
14% { opacity: 1; } 22% { opacity: 0; }
50% { opacity: 1; } 57% { opacity: 0; }
}
@keyframes flick-end {
0%, 100% { opacity: 0; }
24% { opacity: 1; } 31% { opacity: 0; }
76% { opacity: 1; } 82% { opacity: 0; }
}
@keyframes flick-base {
0%, 86% { opacity: 0; }
90%, 100% { opacity: 1; }
}
/* subliminal full-screen echo Ò€” recall turns and the finale only */
.entity-ghost {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 998;
background-size: cover;
background-position: center 25%;
opacity: 0;
filter: grayscale(1) blur(1px);
animation-duration: 0.5s;
animation-timing-function: steps(1, end);
animation-fill-mode: forwards;
}
/* a real screamer: the face stabs the whole screen for a few frames, gone */
.entity-ghost.flash-0 { animation-name: entity-ghost-a; }
.entity-ghost.flash-1 { animation-name: entity-ghost-b; }
@keyframes entity-ghost-a {
0% { opacity: 0; }
8% { opacity: 0.55; }
18% { opacity: 0; }
26% { opacity: 0.4; }
34% { opacity: 0; }
100% { opacity: 0; }
}
@keyframes entity-ghost-b {
0% { opacity: 0; }
8% { opacity: 0.55; }
18% { opacity: 0; }
26% { opacity: 0.4; }
34% { opacity: 0; }
100% { opacity: 0; }
}
/* atmosphere overlays, in front of the portrait */
.entity-fog {
position: absolute;
inset: -15%;
pointer-events: none;
background:
radial-gradient(ellipse 50% 25% at 30% 75%, rgba(150,140,165,0.14) 0%, transparent 70%),
radial-gradient(ellipse 55% 30% at 75% 35%, rgba(120,110,140,0.10) 0%, transparent 70%);
filter: blur(10px);
animation: entity-fog-drift 16s ease-in-out infinite alternate;
}
@keyframes entity-fog-drift {
from { transform: translateX(-3%) translateY(1%); }
to { transform: translateX(3%) translateY(-1.5%); }
}
.entity-grain {
position: absolute;
inset: 0;
pointer-events: none;
opacity: 0.13;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");
}
.entity-vig {
position: absolute;
inset: 0;
pointer-events: none;
background: radial-gradient(ellipse at 50% 42%, transparent 42%, rgba(5,4,9,0.82) 100%);
}
/* Ò”€Ò”€ 16. Treasure Ò€” stone reliquary with inner scroll Ò”€Ò”€ */
/* the frame lives on the panel; the fade mask lives on the inner scroller,
so the mask can't eat the border-image */
.treasure-panel {
height: 500px;
display: flex;
flex-direction: column;
margin-top: -36px;
}
.treasure-scroll {
flex: 1;
overflow-y: auto;
min-height: 0;
margin-top: 6px;
-webkit-mask-image: linear-gradient(
to bottom, transparent 0, #000 14px, #000 calc(100% - 18px), transparent 100%);
mask-image: linear-gradient(
to bottom, transparent 0, #000 14px, #000 calc(100% - 18px), transparent 100%);
}
/* reduced motion: kill real MOTION (drift, sway, glitch translate) but keep
the terror flash and echo Ò€” they are opacity fades, not movement, and they
are the core horror beat. Strip only the glitch from the strong variant. */
@media (prefers-reduced-motion: reduce) {
/* kill ambient drift only Ò€” the flicker and screamer are opacity, the core
horror beat, and must always play */
.entity-sway, .entity-fog { animation: none !important; }
}
/* Ò”€Ò”€ 17. Carved-stone frames Ò€” weathered tablet (Worn Tablet) Ò”€Ò”€ */
/* One reusable 9-slice border-image: directional bevel baked into an SVG
gradient (light top-left, recessed bottom-right), a dark inner groove, a
faint highlight, and rounded chipped corner blocks. Paired with an inset
box-shadow (the recessed well) and a feTurbulence grain overlay. All driven
by CSS variables so intensity retunes in one place. */
:root {
--stone-base: #14121c;
--stone-frame: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0' stop-color='%235a5560'/%3E%3Cstop offset='0.5' stop-color='%232a2632'/%3E%3Cstop offset='1' stop-color='%230c0a10'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect x='3' y='3' width='94' height='94' rx='2' fill='none' stroke='url(%23g)' stroke-width='5'/%3E%3Crect x='8' y='8' width='84' height='84' fill='none' stroke='%230c0a10' stroke-width='1.4'/%3E%3Crect x='10.5' y='10.5' width='79' height='79' fill='none' stroke='%2346424e' stroke-width='0.7' opacity='0.6'/%3E%3Cg fill='%23252230'%3E%3Crect x='1.5' y='1.5' width='15' height='15' rx='4' stroke='url(%23g)' stroke-width='1'/%3E%3Crect x='83.5' y='1.5' width='15' height='15' rx='4' stroke='url(%23g)' stroke-width='1'/%3E%3Crect x='1.5' y='83.5' width='15' height='15' rx='4' stroke='url(%23g)' stroke-width='1'/%3E%3Crect x='83.5' y='83.5' width='15' height='15' rx='4' stroke='url(%23g)' stroke-width='1'/%3E%3C/g%3E%3C/svg%3E");
--stone-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='130' height='130'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
--stone-bevel: inset 1px 1px 0 rgba(120,114,130,0.30),
inset -1px -1px 0 rgba(8,6,12,0.85),
inset 0 0 16px rgba(0,0,0,0.5);
}
/* the reusable carved-stone frame */
.stone-frame,
.treasure-panel,
.user,
.gradio-container button {
border: 11px solid transparent !important;
border-image-source: var(--stone-frame) !important;
border-image-slice: 30 !important;
border-image-width: 11px !important;
border-image-repeat: stretch !important;
border-radius: 0 !important;
box-shadow: var(--stone-bevel) !important;
}
/* Treasure: a stone reliquary, with eroded grain on the inner surface */
.treasure-panel {
position: relative;
background: var(--stone-base) !important;
}
.treasure-panel::before {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
z-index: 0;
background-image: var(--stone-grain);
background-size: 130px;
opacity: 0.10;
mix-blend-mode: soft-light;
}
.treasure-panel > * { position: relative; z-index: 1; }
/* Visitor's words: a carved confession slab */
.user {
background: #120e1c !important;
}
/* Send rune + button: a smaller carved stud */
.gradio-container button {
border-width: 9px !important;
border-image-width: 9px !important;
}
/* Title Ò€” no underline rule */
.gradio-container h2 { border-bottom: none !important; }
/* Ò”€Ò”€ 18. Three endings Ò€” rage portrait & fog dissolve Ò”€Ò”€ */
/* bad ending: a low red breath pulsing over the rage portrait Ò€” the only
color in the entire game, reserved for this moment */
.entity-rage-tint {
/* widen well past the narrow silhouette so the red reads as atmosphere
around the child, not a column-shaped block */
position: absolute;
inset: -40% -170%;
pointer-events: none;
background: radial-gradient(ellipse at 50% 42%, rgba(120, 20, 20, 0.22), transparent 72%);
mix-blend-mode: screen;
animation: rage-pulse 2.6s ease-in-out infinite;
}
@keyframes rage-pulse {
0%, 100% { opacity: 0.55; }
50% { opacity: 1; }
}
/* neutral ending: materialization in reverse Ò€” the child sinks back into
the fog. One-shot on mount; the animation overrides the inline idle style
and `forwards` holds the dissolved frame. Opacity/blur are the beat itself Ò€”
do NOT add these to the prefers-reduced-motion kill lists. */
.entity-dissolve {
animation: entity-dissolve 9s ease-in forwards;
}
@keyframes entity-dissolve {
0% { filter: blur(0px); opacity: 1; }
100% { filter: blur(10px); opacity: 0.18; }
}
/* bad finale frenzy: the capture-flash flicker but sustained Ò€” every face
cycles over the body ~4 times (3.2s), then the animations die and the
rage face beneath stays. Opacity only Ò€” must survive reduced-motion. */
.entity-frenzy-wrap {
position: absolute;
inset: 0;
}
.entity-frenzy-wrap .entity-flick {
animation-duration: 0.55s;
animation-iteration-count: 6;
}
/* frenzy ghost: the terror face stabs the FULL screen three times across the
convulsion (vs the single half-second stab of capture flashes). No flash-0/1
class here Ò€” fresh element on mount, plays once. Opacity only. */
.entity-ghost-frenzy {
animation: ghost-frenzy 3.2s steps(1, end) forwards;
}
@keyframes ghost-frenzy {
0% { opacity: 0; }
6% { opacity: 0.6; }
12% { opacity: 0; }
42% { opacity: 0.65; }
50% { opacity: 0; }
78% { opacity: 0.75; }
86% { opacity: 0; }
100% { opacity: 0; }
}
/* ── 19. Hide Gradio's per-component progress overlay ──
The "processing | 9.0/5.0s" text, orange loader diamond, timer and bar that
Gradio stamps on every output component during generation. The Chatbot's own
"..." pending bubble is a separate element and is NOT affected. */
.progress-text,
.meta-text,
.meta-text-center,
.timer,
.eta-bar,
.progress-bar,
.progress-level,
.status-indicator,
.loader {
display: none !important;
}
/* ── 20. Hollow's typing indicator ──
Three dots that blink in sequence inside the pending chat bubble, so the
wait reads as "thinking" not "frozen". Pure CSS, works in every browser. */
.hollow-typing {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 2px 0;
}
.hollow-typing i {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
opacity: 0.2;
animation: hollow-blink 1.3s infinite both;
}
.hollow-typing i:nth-child(2) { animation-delay: 0.22s; }
.hollow-typing i:nth-child(3) { animation-delay: 0.44s; }
@keyframes hollow-blink {
0%, 75%, 100% { opacity: 0.18; }
35% { opacity: 0.9; transform: translateY(-1px); }
}
/* ── 21. Send button press feedback ──
The button had only :hover β€” a click gave no tactile response. On press it
sinks into the stone (down + scale + inset shadow) and flares; while the
turn processes it dims, reinforcing "sent, working". */
.gradio-container button:active {
transform: translateY(2px) scale(0.96) !important;
color: #ece9f2 !important;
border-color: #6f6a80 !important;
background: rgba(64,38,92,0.8) !important;
box-shadow:
inset 0 3px 9px rgba(0,0,0,0.65),
0 0 24px rgba(150,80,200,0.6) !important;
transition: transform 0.06s ease, box-shadow 0.06s ease,
background 0.06s ease, color 0.06s ease !important;
}
.gradio-container button:disabled,
.gradio-container button[disabled] {
opacity: 0.45 !important;
color: #5a4f72 !important;
box-shadow: none !important;
transform: none !important;
cursor: default !important;
}
/* ── 22. Finale build pulse + soft convulse + restart ── */
/* red suspense pulse during the build of every ending (audio = heartbeat) */
.entity-redpulse {
/* a wide, soft red breath CENTERED on the child that bleeds across the scene
β€” not a hard rectangle confined to the narrow silhouette column */
position: absolute; inset: -40% -170%; pointer-events: none;
background: radial-gradient(ellipse at 50% 45%,
rgba(150, 0, 0, 0.4) 0%, rgba(120, 0, 0, 0.16) 46%, transparent 72%);
mix-blend-mode: screen;
animation: redpulse 1.15s ease-in-out infinite;
}
@keyframes redpulse {
0%, 100% { opacity: 0.15; }
50% { opacity: 0.75; }
}
/* the redemption convulsion: the SAME fast cadence as the bad frenzy, but
through the child's gentler possible faces (almost/base/peace) β€” no red
tint, no full-screen stab. Settles on the smile + sigh. The flick keyframe
names come from the global .flick-0/1/2 rules; we only set the tempo. */
.entity-convulse-soft { position: absolute; inset: 0; pointer-events: none; }
.entity-convulse-soft .entity-flick {
animation-duration: 0.26s; /* faster image-to-image (faces blur past) */
animation-iteration-count: 8; /* longer overall (~2s) before it settles */
}
/* loop convulsion: the same fast flicker as the frenzy but only 3 cycles, so
the `end` face settles quickly (the bad frenzy keeps its longer 6-cycle build) */
.entity-convulse-loop.entity-frenzy-wrap .entity-flick {
animation-duration: 0.55s;
animation-iteration-count: 3;
}
/* the final face eases in over the convulsion and HOLDS at the end, so the
settle render (same image) is seamless β€” the swap is never a visible pop.
Must come AFTER the flick rules above (equal specificity, source order wins). */
.entity-convulse-soft .entity-settle-face,
.entity-convulse-loop.entity-frenzy-wrap .entity-settle-face {
animation-name: convulse-settle;
animation-duration: 1.65s;
animation-iteration-count: 1;
animation-timing-function: ease-out;
animation-fill-mode: forwards;
}
@keyframes convulse-settle {
0%, 60% { opacity: 0; }
100% { opacity: 1; }
}
/* the good convulsion runs longer (~2s) β€” its smile eases in to match, so the
peace_settle render lands seamlessly as the relief sigh plays */
.entity-convulse-soft .entity-settle-face { animation-duration: 1.95s; }
/* restart + mute β€” minimal icon cluster, top-right, low-contrast */
.restart-btn {
background: transparent !important;
border: none !important;
max-width: none; min-width: 0; width: auto !important;
font-size: 1.4rem !important; letter-spacing: normal; text-transform: none;
color: #c2b2dd !important;
opacity: .92; box-shadow: none !important;
text-shadow: 0 1px 6px rgba(0,0,0,0.9);
padding: 4px 6px;
margin-left: 12px;
}
.restart-btn:hover {
color: #e0cdb0 !important;
opacity: 1;
background: transparent !important;
border: none !important;
}
.voice-btn {
background: transparent !important;
border: none !important;
max-width: none; min-width: 0; width: auto !important;
font-size: 1.4rem !important;
color: #c4b8dc !important;
opacity: .92; box-shadow: none !important;
text-shadow: 0 1px 6px rgba(0,0,0,0.9);
padding: 4px 6px;
margin-left: 12px;
}
.voice-btn:hover {
color: #e0cdb0 !important;
opacity: 1;
background: transparent !important;
border: none !important;
}
/* the voice channel: mounted in the DOM (so the <audio> plays) but collapsed
to nothing β€” audio keeps playing in a zero-size element, no visual footprint */
.voice-channel {
height: 0 !important;
min-height: 0 !important;
padding: 0 !important;
margin: 0 !important;
border: none !important;
overflow: hidden !important;
}
/* idle trigger: mounted but invisible */
#idle-trigger, .idle-trigger { display: none !important; }
/* ── 23. Opening menu β€” Direction C ── */
#menu-view {
position: fixed;
inset: 0;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center; /* center title+buttons as one vertical group */
overflow: hidden;
border: none !important;
background: #04040a;
z-index: 50;
}
/* Gradio hides a visible=False column via a class; our id-level display:flex
would otherwise win on specificity and the hidden menu keeps reserving its
min-height (a black gap above the game). Force the collapse. */
#menu-view.hide, #menu-view.hidden,
#game-view.hide, #game-view.hidden { display: none !important; }
.menu-bg {
position: fixed;
inset: -6%; /* overscan so blur edges never reveal a gap */
background-size: cover;
background-position: center 38%;
filter: grayscale(1) blur(46px) brightness(0.34);
transform: scale(1.08);
z-index: 0;
}
/* sharp cinematic frame: the forest, contained like the intro cards */
#menu-frame {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: min(1280px, 95vw);
aspect-ratio: 16 / 10;
max-height: 82vh;
background-size: cover;
background-position: center 38%;
background-repeat: no-repeat;
filter: grayscale(1) brightness(0.92);
border-radius: 0;
box-shadow: none;
-webkit-mask-image: radial-gradient(ellipse 76% 78% at 50% 47%, #000 30%, transparent 86%);
mask-image: radial-gradient(ellipse 76% 78% at 50% 47%, #000 30%, transparent 86%);
z-index: 1;
}
.menu-scrim {
position: fixed;
inset: 0;
background: radial-gradient(ellipse at 50% 30%, transparent 25%, rgba(4,4,10,0.92) 100%);
pointer-events: none;
z-index: 2;
}
.menu-grain {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 2;
opacity: 0.10;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");
}
.menu-vig {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 2;
box-shadow: inset 0 0 120px rgba(0,0,0,0.75);
}
#menu-card {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
z-index: 5;
text-align: center;
font-family: Georgia, serif;
max-width: 560px;
width: 92vw;
}
#menu-card::before {
content: '';
position: absolute;
inset: -56px -90px;
z-index: -1;
pointer-events: none;
background: radial-gradient(ellipse at center,
rgba(4,4,10,0.82) 0%, rgba(4,4,10,0.0) 70%);
}
.menu-title {
font-variant: small-caps;
letter-spacing: 0.18em;
font-weight: 700;
font-size: 78px;
color: #ece6f4;
text-shadow: 0 0 30px #000, 0 4px 12px #000;
}
.menu-eyebrow {
font-family: 'Special Elite', monospace;
font-size: 0.72rem;
letter-spacing: 0.28em;
text-transform: uppercase;
color: #c6bcda;
text-shadow: 0 0 12px rgba(0,0,0,0.95), 0 1px 4px #000;
margin-bottom: 8px;
}
.menu-tag {
font-family: Georgia, serif;
font-size: 1.05rem;
font-style: italic;
color: #dad3e8;
text-shadow: 0 0 14px rgba(0,0,0,0.9), 0 1px 6px #000;
letter-spacing: 0.04em;
margin: 14px auto 0;
max-width: 420px;
}
.menu-opts {
margin-top: 34px;
display: flex;
flex-direction: column;
align-items: center; /* rows shrink to content -> no full-width band */
gap: 6px;
}
.menu-opt {
font-family: Georgia, serif;
font-size: 1.15rem;
letter-spacing: 0.14em;
color: #ded8ec;
text-shadow: 0 1px 6px #000;
padding: 8px 26px;
cursor: pointer;
border-radius: 2px;
transition: color 0.3s, letter-spacing 0.3s, text-shadow 0.3s, background 0.3s;
}
.menu-opt::before {
content: '\203A\00a0\00a0'; /* "β€Ί " */
opacity: 0;
transition: opacity 0.3s;
}
.menu-opt:hover {
color: #f0ecfa;
letter-spacing: 0.2em;
text-shadow: 0 0 16px rgba(180,160,210,0.5);
background: rgba(255,255,255,0.04);
}
.menu-opt:hover::before { opacity: 1; }
.menu-opt-muted {
margin-top: 10px;
font-size: 0.95rem;
color: #b8b0cc;
text-shadow: 0 0 10px #000, 0 1px 3px #000;
letter-spacing: 0.1em;
}
.menu-opt-muted:hover { color: #e0d9ee; }
#menu-proxy { display: none !important; }
.menu-credit {
position: fixed;
bottom: 18px;
left: 0; right: 0;
text-align: center;
z-index: 4;
font-family: 'Special Elite', monospace;
font-size: 0.64rem;
letter-spacing: 0.24em;
text-transform: uppercase;
color: #8c84a4;
text-shadow: 0 1px 6px #000;
}
.menu-btn {
position: relative;
z-index: 4;
background: transparent !important;
border: none !important;
border-image-source: none !important;
box-shadow: none !important;
font-family: Georgia, serif !important;
letter-spacing: 0.16em !important;
color: #c4bede !important;
font-size: 1.05rem !important;
text-transform: none !important;
margin: 8px 0 !important;
padding: 8px 18px !important;
transition: color 0.35s, letter-spacing 0.35s, text-shadow 0.35s, background 0.35s !important;
}
.menu-btn:hover {
color: #f0ecfa !important;
letter-spacing: 0.22em !important;
text-shadow: 0 0 16px rgba(180,160,210,0.5) !important;
background: rgba(255,255,255,0.04) !important;
box-shadow: none !important;
}
.menu-btn::before {
content: 'β–Έ ';
opacity: 0;
margin-right: 0;
transition: opacity 0.35s, margin-right 0.35s;
}
.menu-btn:hover::before {
opacity: 1;
margin-right: 6px;
}
#btn-howto {
position: fixed;
top: 16px;
right: 20px;
z-index: 6;
font-size: 0.82rem !important;
letter-spacing: 0.14em !important;
color: #8a7aa2 !important;
margin: 0 !important;
}
#btn-howto:hover {
color: #d8d0ec !important;
background: rgba(255,255,255,0.04) !important;
}
#btn-howto-close {
display: block !important;
width: fit-content !important;
min-width: 0 !important;
margin: 6px auto 0 !important;
}
#btn-tester, #btn-full {
margin-top: 24px !important;
}
/* ── 24. How to Play overlay ── */
#howto-overlay {
position: fixed;
inset: 0;
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
background: rgba(4, 4, 10, 0.88);
}
#howto-overlay.hide, #howto-overlay.hidden { display: none !important; }
.howto-panel {
max-width: 560px;
width: 92vw;
margin: 0 auto;
border: 11px solid transparent;
border-image: var(--stone-frame) 30;
background: var(--stone-base);
padding: 26px 30px;
font-family: Georgia, serif;
color: #c4bede;
line-height: 1.6;
box-shadow: var(--stone-bevel);
}
.howto-panel h3 {
font-family: 'Special Elite', monospace;
font-size: 1.15rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: #e0d8f0;
margin: 0 0 18px;
text-align: center;
}
.howto-panel p {
margin: 0 0 12px;
}
.howto-controls {
margin-top: 18px !important;
font-size: 0.8rem;
letter-spacing: 0.12em;
color: #6a6088;
text-align: center;
}
/* ── 25. Opening intro sequence (cinematic letterbox) ── */
#intro-view {
position: fixed;
inset: 0;
height: 100vh;
display: flex;
flex-direction: column;
background: #000;
overflow: hidden;
z-index: 50;
}
#intro-view.hide, #intro-view.hidden { display: none !important; }
#intro-stage {
position: relative;
flex: 1;
min-height: 100vh;
cursor: pointer; /* click anywhere to advance */
overflow: hidden;
display: flex;
align-items: center; /* center the frame -> symmetric letterbox bands */
justify-content: center;
background: #000;
}
/* blurred ambient backdrop: a ghost of the same card, fills the bands */
#intro-bg {
position: absolute;
inset: -6%; /* overscan so blur edges never show a gap */
background-position: center;
background-size: cover;
background-repeat: no-repeat;
filter: grayscale(1) blur(42px) brightness(0.32);
transform: scale(1.08);
z-index: 0;
}
#intro-bg::after { /* darken so the framed card pops */
content: '';
position: absolute;
inset: 0;
background: radial-gradient(ellipse at center,
rgba(0,0,0,0.20) 0%, rgba(0,0,0,0.80) 100%);
}
/* centered cinematic window (~16:10) */
#intro-frame {
position: relative;
z-index: 1;
width: min(1280px, 95vw);
aspect-ratio: 16 / 10;
max-height: 82vh;
overflow: hidden;
border-radius: 0;
box-shadow: none;
}
#intro-image {
position: absolute;
inset: 0;
background-position: center; /* JS overrides with the card focal point */
background-size: cover;
background-repeat: no-repeat;
/* fallback when a card image is missing/empty: */
background-color: #04040a;
background-image: radial-gradient(ellipse at 50% 35%, #16131c 0%, #04040a 80%);
filter: grayscale(1) brightness(0.9);
-webkit-mask-image: radial-gradient(ellipse 76% 78% at 50% 47%, #000 30%, transparent 86%);
mask-image: radial-gradient(ellipse 76% 78% at 50% 47%, #000 30%, transparent 86%);
}
#intro-image::after { /* soft bottom darkening so the subtitle reads */
content: '';
position: absolute;
inset: 0;
background: linear-gradient(transparent 60%, rgba(4,4,10,0.55) 98%);
}
/* subtitle panel: anchored to the bottom of the frame */
#intro-panel {
position: absolute;
left: 50%;
bottom: 14px;
transform: translateX(-50%);
width: min(680px, 88%);
z-index: 2;
border: 10px solid transparent;
border-image: var(--stone-frame) 30;
background: rgba(16,14,24,0.62);
backdrop-filter: blur(3px);
-webkit-backdrop-filter: blur(3px);
padding: 13px 20px;
}
#intro-text {
font-family: Georgia, serif;
font-size: 1.05rem;
line-height: 1.6;
color: #d6d0e0;
margin: 0;
min-height: 3.2em; /* reserve height so the panel doesn't grow while typing */
}
/* option rows, revealed inside the stone panel on the final card */
#intro-opts {
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid rgba(120,110,140,0.18);
display: flex;
flex-direction: column;
gap: 2px;
max-height: 0;
overflow: hidden;
opacity: 0;
transition: opacity 0.5s ease, max-height 0.5s ease;
}
#intro-opts.intro-opts-show {
max-height: 220px;
opacity: 1;
}
.intro-opt {
font-family: Georgia, serif;
font-size: 1.02rem;
color: #b9b2cc;
letter-spacing: 0.04em;
padding: 7px 10px;
cursor: pointer;
border-radius: 2px;
transition: color 0.2s, background 0.2s, padding-left 0.2s;
}
.intro-opt::before {
content: '\203A\00a0\00a0'; /* "β€Ί " */
color: #6a5a86;
}
.intro-opt:hover {
color: #f0ecfa;
background: rgba(120,90,140,0.12);
padding-left: 16px;
}
/* the real choice buttons live hidden in the DOM; HTML rows proxy their clicks */
#intro-proxy { display: none !important; }
#intro-skip {
position: absolute;
top: 18px;
right: 22px;
z-index: 4;
width: fit-content !important;
min-width: 0 !important;
font-family: Georgia, serif !important;
letter-spacing: 0.14em !important;
font-size: 0.82rem !important;
color: #cdc7da !important;
background: rgba(8,6,14,0.55) !important;
border: 1px solid rgba(150,142,170,0.35) !important;
box-shadow: none !important;
border-radius: 14px !important;
padding: 5px 16px !important;
text-shadow: 0 1px 4px #000 !important;
}
#intro-skip:hover {
color: #f2eefa !important;
background: rgba(30,24,42,0.82) !important;
border-color: rgba(190,178,210,0.6) !important;
}
/* ── 26. Game scene β€” letterboxed foggy wood (Direction 3b) ── */
#game-view {
position: fixed;
inset: 0;
overflow: hidden;
background: #04040a;
z-index: 50;
/* geometry of the centered 16:9 cinematic frame β€” every in-frame element
(child, subtitle, drawers, fog) positions against these vars, so they sit
INSIDE the frame instead of the full viewport */
--fw: min(1180px, 94vw);
--fh: min(calc(var(--fw) * 0.5625), 84vh);
--band-v: calc((100vh - var(--fh)) / 2); /* top & bottom letterbox band */
--band-h: calc((100vw - var(--fw)) / 2); /* left & right band */
}
#game-view.hide, #game-view.hidden { display: none !important; }
/* the letterboxed stage β€” absolute, centered, real height so children work */
/* passive full-cover container β€” NO transform/filter, so the fixed regions
below resolve against the viewport (a transformed ancestor would trap them) */
#game-stage {
position: absolute !important;
inset: 0 !important;
transform: none !important;
width: auto !important;
height: auto !important;
max-height: none !important;
overflow: visible !important;
background: transparent !important;
border: none !important;
box-shadow: none !important;
padding: 0 !important;
margin: 0 !important;
gap: 0 !important;
}
/* Gradio wrappers inside the stage must not create their own boxes */
#game-stage > .gradio-html,
#game-stage > .gradio-column,
#game-stage > .gradio-row,
#game-stage .gradio-html,
#game-stage .gradio-column,
#game-stage .gradio-row {
background: transparent !important;
border: none !important;
box-shadow: none !important;
padding: 0 !important;
margin: 0 !important;
gap: 0 !important;
}
/* blurred ambient forest backdrop, same treatment as intro/menu */
#game-bg {
position: fixed;
inset: -6%;
background-size: cover;
background-position: center 38%;
filter: grayscale(1) blur(46px) brightness(0.3);
transform: scale(1.1);
z-index: 0;
}
/* sharp forest in a centered cinematic frame, feathered into the blur (depth) */
#game-scene {
position: fixed;
left: 50%; top: 50%;
transform: translate(-50%, -50%);
width: min(1180px, 94vw);
aspect-ratio: 16 / 9;
max-height: 84vh;
background-size: cover;
background-position: center 40%;
background-repeat: no-repeat;
filter: grayscale(1) brightness(0.46);
-webkit-mask: radial-gradient(ellipse 80% 82% at 50% 46%, #000 18%, transparent 88%);
mask: radial-gradient(ellipse 80% 82% at 50% 46%, #000 18%, transparent 88%);
z-index: 1;
pointer-events: none;
}
/* radial vignette β€” the scene breathes through this */
#game-vig {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 3;
background:
radial-gradient(ellipse at 50% 46%, transparent 34%, rgba(6,5,12,0.88) 100%),
linear-gradient(to bottom, rgba(6,4,12,0.5) 0%, transparent 13%);
}
/* a subtle inner frame edge so the scene feels contained like the intro */
#game-frame-edge {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 3;
box-shadow: inset 0 0 210px 90px rgba(5,4,10,0.92);
}
/* rising ground fog β€” dissolves the child's lower half */
#game-groundfog {
position: fixed;
left: var(--band-h); right: var(--band-h); /* confined to the frame width */
bottom: var(--band-v); /* rises from the frame's base */
height: calc(var(--fh) * 0.55);
z-index: 5; /* above the child (z-index 4) so it veils the legs */
pointer-events: none;
background: linear-gradient(transparent,
rgba(150,150,168,0.10) 50%,
rgba(170,170,190,0.18) 80%,
rgba(180,180,200,0.26));
-webkit-mask: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
mask: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}
/* top bar: title + controls β€” sits just above the stage */
#game-topbar {
position: fixed;
top: 14px;
left: 0;
right: 0;
z-index: 10;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 30px;
pointer-events: none;
}
#game-topbar > * { pointer-events: auto; }
#game-title {
font-family: 'Special Elite', monospace;
font-size: 0.9rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: #7d7596;
text-shadow: 0 1px 6px rgba(0,0,0,0.9), 0 0 12px rgba(120,90,160,0.35);
}
#game-title .named {
font-style: italic;
text-transform: none;
letter-spacing: 0.08em;
color: #e2dcf0;
}
/* corner drawers β€” compact chips that expand downward */
.game-drawer {
position: fixed; top: calc(var(--band-v) + 16px); z-index: 8;
width: max-content; max-width: 280px; min-width: 0 !important;
max-height: 34px; /* collapsed = just the chip */
overflow: hidden; cursor: pointer;
background: rgba(10,8,16,0.5);
border: 1px solid rgba(120,110,140,0.22); border-radius: 3px;
padding: 6px 12px;
transition: max-height 0.4s ease, background 0.3s, opacity 0.2s, border-color 0.2s;
opacity: 0.82;
}
.game-drawer:hover { opacity: 1; border-color: rgba(180,160,210,.4); }
.game-drawer.open { max-height: 56vh; width: min(300px, 24vw); background: rgba(12,10,18,0.92); }
#drawer-left { left: calc(var(--band-h) + 16px); }
#drawer-right { right: calc(var(--band-h) + 16px); }
.drawer-count { color: var(--mem-accent, #e0b283); font-weight: 600; }
/* Gradio wraps drawer contents in .block/.html-container/.prose; force them
to fill the drawer so the scroll list and header line up correctly. */
.game-drawer > .block,
.game-drawer > .gradio-html,
.game-drawer .html-container,
.game-drawer .prose,
.game-drawer .treasure-panel,
.game-drawer .recovered-panel {
width: 100% !important;
height: 100% !important;
max-height: 100% !important;
padding: 0 !important;
margin: 0 !important;
border: none !important;
box-shadow: none !important;
background: transparent !important;
overflow: hidden !important;
}
.game-drawer .treasure-panel,
.game-drawer .recovered-panel {
display: flex;
flex-direction: column;
}
.game-drawer .treasure-panel {
margin-top: 0 !important;
height: auto !important;
flex: 1;
}
.game-drawer .treasure-scroll {
flex: 1;
min-height: 0;
overflow-y: auto;
}
.drawer-head {
cursor: pointer;
user-select: none;
padding-bottom: 4px !important;
border-bottom: 1px solid rgba(90,75,115,0.50);
margin-bottom: 6px;
font-family: 'Special Elite', monospace; font-size: 0.66rem; letter-spacing: 0.2em;
text-transform: uppercase; color: #9a93ac; white-space: nowrap;
}
.drawer-head::after {
content: ' β€Ί';
opacity: 0.70;
margin-left: 6px;
display: inline-block;
transition: transform 0.3s ease;
}
.game-drawer.open .drawer-head::after {
transform: rotate(90deg);
}
/* the child's recovered memories read in the warm memory accent */
.game-drawer .recovered-item {
color: var(--mem-accent) !important;
border-left-color: rgba(200,130,70,0.5) !important;
}
/* a memory the child has taken from you, worn as its own */
.game-drawer .recovered-item.stolen {
border-left-style: dashed !important;
opacity: 0.92;
}
.game-drawer .recovered-item.stolen::before {
content: '"'; color: var(--mem-accent); margin-right: 2px;
}
/* the child silhouette zone β€” center, bottom-weighted */
#game-entity {
position: fixed;
/* centered WITHOUT a transform β€” a transformed ancestor would trap the
full-screen .entity-ghost stab into this narrow column */
left: calc(50% - var(--fw) * 0.15);
bottom: var(--band-v); /* feet rest on the frame's bottom edge */
transform: none;
z-index: 4;
width: calc(var(--fw) * 0.30); /* ~30% of the frame, like the mockup */
min-width: 200px;
height: calc(var(--fh) * 0.88);
pointer-events: none;
display: flex;
align-items: flex-end;
justify-content: center;
}
/* Gradio wraps the injected HTML in .html-container and .prose; without
explicit fill they collapse to the prose's line box and the silhouette
(and its image) end up 0Γ—0. Force every wrapper to fill the entity panel. */
#game-entity > .html-container,
#game-entity .html-container,
#game-entity .prose {
width: 100% !important;
height: 100% !important;
padding: 0 !important;
margin: 0 !important;
border: none !important;
box-shadow: none !important;
background: transparent !important;
overflow: visible !important;
}
.entity-scene {
width: 100%;
height: 100%;
position: relative;
display: flex;
align-items: flex-end;
justify-content: center;
}
.entity-silhouette {
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: flex-end;
justify-content: center;
}
.entity-silhouette img {
position: relative;
width: 100% !important;
height: auto !important;
max-height: 100%;
object-fit: contain !important;
object-position: bottom center !important;
flex-shrink: 0;
-webkit-mask: linear-gradient(#000 78%, transparent 99%);
mask: linear-gradient(#000 78%, transparent 99%);
}
/* when the "Almost" face overlays the base, it must remain absolutely
positioned so the two portraits stack instead of sitting side-by-side */
.entity-silhouette .entity-almost {
position: absolute !important;
bottom: 0 !important;
left: 0 !important;
top: auto !important;
right: auto !important;
width: 100% !important;
height: auto !important;
max-height: 100%;
object-fit: contain !important;
object-position: bottom center !important;
}
.entity-glow {
position: absolute;
inset: 5% -10% 0 -10%;
background: radial-gradient(ellipse 55% 70% at 50% 78%,
rgba(150,140,175,0.14) 0%, transparent 64%);
pointer-events: none;
opacity: 0.85;
mix-blend-mode: screen;
}
/* subtitle: the conversation, sat just inside the frame's bottom edge */
#game-dialogue {
position: fixed;
left: 50%;
transform: translateX(-50%);
right: auto;
bottom: max(86px, calc(var(--band-v) - 96px)); /* dropped into the lower letterbox band: clear of the child's body, still above the input line. max() keeps it above the input on short viewports where the band is thin */
width: min(calc(var(--fw) * 0.82), 860px);
z-index: 9;
display: flex; flex-direction: column; align-items: stretch; gap: 4px;
pointer-events: none;
}
/* (no scrim behind the dialogue: it read as a dark rectangle over the scene.
The .bot text-shadow already carries legibility over the fog.) */
/* input bar: a thin line at the very bottom of the viewport, below the frame */
#game-inputbar {
position: fixed;
left: 50%;
transform: translateX(-50%);
bottom: 18px;
width: min(560px, 60vw);
z-index: 9;
display: flex; flex-direction: column; align-items: center; gap: 0;
}
#game-dialogue .chatbot {
width: 100% !important;
max-height: 160px !important;
background: transparent !important;
border: none !important;
border-image: none !important;
box-shadow: none !important;
backdrop-filter: none !important;
padding: 0 !important;
overflow: hidden; /* old lines fade out, no scrollbar */
}
/* kill the inner scroll thumb Gradio shows on the message area (the stray
purple vertical bar to the right of the subtitle) */
#game-dialogue, #game-dialogue * { scrollbar-width: none !important; }
#game-dialogue ::-webkit-scrollbar { width: 0 !important; height: 0 !important; display: none !important; }
/* the opening greeting is the chat's lone first message. Gradio's .message-wrap
(auto-height) sits at the TOP of the fixed-height scroll area (.bubble-wrap),
so a single message floats up onto the child. Make .bubble-wrap a flex column
and push .message-wrap to its bottom β€” but ONLY while there's a single message
(:not(:has(row ~ row))). The moment you reply (2+ rows) the rule drops out, so
the running dialogue (which already sits low via autoscroll) is untouched. */
#game-dialogue .bubble-wrap { display: flex !important; flex-direction: column !important; }
#game-dialogue .message-wrap:not(:has(.message-row ~ .message-row)) { margin-top: auto !important; }
/* transparent rows, centered text β€” kill Gradio's bubble chrome */
#game-dialogue .message-row,
#game-dialogue .message,
#game-dialogue .bubble {
background: transparent !important; border: none !important;
box-shadow: none !important; text-align: center !important;
padding: 2px 0 !important; margin: 0 !important;
}
/* fade-by-recency: newest crisp, older dimmer, 4th-back and beyond hidden.
(Gradio wraps each message in a .message-row β€” verify this selector against
the running app; if rows are .message instead, swap the selector.) */
#game-dialogue .message-row { opacity: 0.38; transition: opacity 0.4s ease; }
#game-dialogue .message-row:nth-last-child(2) { opacity: 0.6; }
#game-dialogue .message-row:nth-last-child(1) { opacity: 1; }
#game-dialogue .message-row:nth-last-child(n+4) { display: none; }
/* the child β€” italic, bright, unlabeled (it owns the scene) */
#game-dialogue .bot, #game-dialogue .bot .prose, #game-dialogue .bot p {
font-style: italic !important; font-size: 1.2rem !important; line-height: 1.55 !important;
color: #ece6f4 !important; text-align: center !important;
text-shadow: 0 2px 16px #000 !important;
}
/* you β€” upright, muted, centered like the bot (distinguished by label + color) */
#game-dialogue .user, #game-dialogue .user .prose, #game-dialogue .user p {
font-style: normal !important; font-size: 1.0rem !important;
color: #aaa2be !important; text-align: center !important; background: transparent !important;
}
/* center the user line so Gradio's narrow right bubble doesn't collapse the
text into a 1-character vertical column at the frame edge. */
#game-dialogue .user {
display: block !important;
max-width: none !important;
margin: 0 auto !important;
padding: 0 !important;
}
#game-dialogue .user .prose::before {
content: 'you ';
font-family: 'Special Elite', monospace;
font-size: 0.6rem; letter-spacing: 0.18em; text-transform: uppercase;
color: #6f6590; margin-right: 7px;
}
/* a recall line (it claims your memory) β€” JS tags the last .bot row .recall-line */
#game-dialogue .bot.recall-line, #game-dialogue .bot.recall-line .prose,
#game-dialogue .bot.recall-line p {
color: var(--mem-accent) !important;
text-shadow: 0 0 20px var(--mem-glow) !important;
}
#game-inputrow {
display: flex; align-items: center; gap: 8px;
width: 100%; margin: 0 auto;
background: rgba(12,9,18,0.82);
border: 7px solid transparent;
border-image: var(--stone-frame) 30;
box-shadow: 0 0 0 1px rgba(0,0,0,0.7), inset 0 0 20px rgba(0,0,0,0.55),
0 8px 24px rgba(0,0,0,0.55), inset 0 1px 0 rgba(150,140,175,0.16);
padding: 2px 11px;
}
#game-inputrow::before {
content: '❯'; color: #6f6590; font-size: 0.92rem; flex: 0 0 auto;
font-family: 'Special Elite', monospace;
}
#game-inputrow textarea, #game-inputrow input[type="text"] {
background: transparent !important; border: none !important; border-radius: 0 !important;
text-align: left !important; font-style: italic !important;
color: #d6d0e0 !important; box-shadow: none !important; padding: 5px 4px !important;
}
#game-inputrow button {
background: transparent !important; border: none !important; box-shadow: none !important;
color: #8a82a0 !important; font-size: 1.1rem !important; min-width: 0 !important;
}
#game-inputrow button:hover { color: #e4dff0 !important; }
#game-dialogue .bond-panel { width: auto !important; margin: 0 auto !important; opacity: 0.55; }
#game-dialogue .bond-panel .bond-track,
#game-dialogue .bond-panel .bond-fill,
#game-dialogue .bond-panel .bond-beat,
#game-dialogue .bond-panel .bond-name { display: none !important; }
#game-dialogue .bond-panel .bond-tier {
font-size: 0.66rem !important; letter-spacing: 0.24em !important; color: #7a7290 !important; }
#game-inputbar .voice-channel,
#game-inputbar .voice-channel * {
height: 0 !important;
min-height: 0 !important;
padding: 0 !important;
margin: 0 !important;
border: none !important;
overflow: hidden !important;
}
/* tone classes β€” the world blooms or rots. Scoped under #game-view: the
tone token is lifted onto #game-view by the head JS (applyTone), so #game-bg
/#game-vig (siblings of #game-entity) actually tint. */
#game-view.tone-warm #game-bg { filter: grayscale(1) blur(38px) brightness(0.62); }
#game-view.tone-warm #game-vig {
background:
radial-gradient(ellipse at 50% 42%, transparent 52%, rgba(4,3,8,0.45) 100%),
linear-gradient(to bottom, rgba(6,4,12,0.28) 0%, transparent 13%);
}
#game-view.tone-warm .entity-img { filter: brightness(1.12) drop-shadow(0 0 18px rgba(190,170,220,0.30)) !important; }
#game-view.tone-neutral #game-bg { filter: grayscale(1) blur(38px) brightness(0.52); }
#game-view.tone-wounded #game-bg { filter: grayscale(1) blur(38px) brightness(0.38); }
#game-view.tone-wounded #game-vig {
background:
radial-gradient(ellipse at 50% 42%, transparent 38%, rgba(4,3,8,0.78) 100%),
linear-gradient(to bottom, rgba(6,4,12,0.65) 0%, transparent 13%);
}
#game-view.tone-wounded .entity-img { opacity: 0.85 !important; }
#game-view.tone-hostile #game-bg {
filter: grayscale(1) blur(38px) brightness(0.30);
}
#game-view.tone-hostile #game-vig {
background:
radial-gradient(ellipse at 50% 42%, transparent 32%, rgba(30,4,8,0.80) 100%),
linear-gradient(to bottom, rgba(40,6,12,0.52) 0%, transparent 20%);
}
#game-view.tone-hostile .entity-img {
filter: sepia(0.22) saturate(1.35) brightness(0.95) drop-shadow(0 0 18px rgba(120,40,40,0.35)) !important;
}
/* recall migration β€” the claimed memory drifts toward the child */
.treasure-item { transition: opacity 0.6s ease; }
.treasure-item.claiming {
animation: claim-migrate 1.4s ease-out forwards;
}
@keyframes claim-migrate {
0% { transform: translateX(0); opacity: 1; }
45% { transform: translateX(60%); opacity: 0.6; }
100% { transform: translateX(120%); opacity: 0.25; }
}
/* keep existing entity animations working inside the new scene */
.entity-flash-wrap,
.entity-frenzy-wrap,
.entity-convulse-soft {
position: absolute;
inset: 0;
pointer-events: none;
}
/* ensure the global body stains intensify under hostile tone */
.tone-hostile ~ .gradio-container::after,
.tone-hostile + .gradio-container::after {
opacity: 1.35;
}
/* feedback: the drawer chip breathes amber when its collection changes */
.game-drawer.pulse {
border-color: rgba(200,150,80,0.6) !important;
box-shadow: 0 0 18px rgba(200,150,80,0.45) !important;
animation: chip-pulse 1.1s ease-in-out 2;
}
.game-drawer.pulse .drawer-head { color: var(--mem-accent) !important; }
@keyframes chip-pulse {
0%, 100% { box-shadow: 0 0 10px rgba(200,150,80,0.25); }
50% { box-shadow: 0 0 22px rgba(200,150,80,0.55); }
}
/* the in-voice whisper that breathes next to the chip, then fades */
.cue-whisper {
position: fixed; z-index: 60; pointer-events: none;
font-family: 'Special Elite', monospace;
font-size: 0.62rem; letter-spacing: 0.14em;
color: var(--mem-accent); text-shadow: 0 0 10px var(--mem-glow);
opacity: 0; transform: translateY(-3px);
transition: opacity 0.5s ease, transform 0.5s ease;
}
.cue-whisper.out { opacity: 0; transform: translateY(-8px); }
.cue-whisper { animation: whisper-in 0.5s ease forwards; }
@keyframes whisper-in { from { opacity: 0; } 40% { opacity: 0.9; } to { opacity: 0.9; } }
@media (max-width: 900px) {
.game-drawer { width: 150px; min-width: 140px; }
#game-entity { width: 42vw; min-width: 160px; left: calc(50% - 21vw); }
#game-dialogue { width: 92vw; }
}
@media (prefers-reduced-motion: reduce) {
.entity-sway { animation: none !important; }
.treasure-item.claiming { animation: none !important; opacity: 0.4; }
.game-drawer.pulse { animation: none !important; }
.cue-whisper { animation: none !important; opacity: 0.9 !important; }
}
/* ── 27. End screen β€” epitaph hero + quiet footer credits over the fog (layout B) ── */
#end-overlay {
position: fixed; inset: 0; z-index: 80;
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 30px;
opacity: 0; pointer-events: none;
transition: opacity 1.6s ease;
}
#end-overlay.shown { opacity: 1; pointer-events: auto; }
/* neutralize Gradio's full-width block wrappers so the inner content truly centers */
#end-overlay > * {
width: auto !important; max-width: 90vw !important; min-width: 0 !important;
background: transparent !important; border: none !important; box-shadow: none !important;
display: flex; flex-direction: column; align-items: center;
}
#end-bg {
position: fixed; inset: -6%; z-index: -2;
background-size: cover; background-position: center 40%; background-repeat: no-repeat;
filter: grayscale(1) blur(30px) brightness(0.26);
}
#end-scrim { position: fixed; inset: 0; z-index: -1;
background: radial-gradient(ellipse at 50% 50%, transparent 30%, rgba(4,3,8,0.92) 100%); }
/* the hero line */
#end-epitaph {
font-family: 'Crimson Text', Georgia, serif; font-style: italic;
font-size: 1.95rem; color: #e6dff2; text-shadow: 0 2px 18px #000;
text-align: center; max-width: min(820px, 88vw); margin: 0;
}
/* the two actions β€” grouped, centered, understated text links */
#end-actions {
flex-direction: row !important; justify-content: center; gap: 34px;
}
#end-actions .end-btn {
background: none !important; border: none !important; box-shadow: none !important;
width: auto !important; min-width: 0 !important; flex: 0 0 auto !important;
font-family: 'Special Elite', monospace; letter-spacing: 0.2em; font-size: 0.92rem;
color: #d7cfe6 !important; opacity: 0.85;
transition: opacity 0.25s ease, color 0.25s ease;
}
#end-actions .end-btn:hover { opacity: 1; color: #f0eaff !important; }
#end-actions .end-btn-dim { color: #7d7498 !important; font-size: 0.82rem; opacity: 0.7; }
/* credits β€” a quiet footer pinned to the bottom (fixed β†’ out of the centered flow) */
#end-credits {
position: fixed; left: 0; right: 0; bottom: 26px;
display: flex; flex-direction: column; align-items: center; gap: 5px; text-align: center;
animation: end-rise 2.2s ease 0.4s both;
}
#end-credits .ec-title {
font-family: 'Special Elite', monospace; letter-spacing: 0.4em; font-size: 0.95rem;
color: #c4bcd8; }
#end-credits .ec-line {
font-family: 'Crimson Text', serif; font-style: italic; color: #9a90b2; font-size: 0.86rem; }
#end-credits .ec-meta {
font-family: 'Special Elite', monospace; font-size: 0.64rem; letter-spacing: 0.18em;
color: #6f6590; }
@keyframes end-rise { from { opacity: 0; transform: translateY(14px); } to { opacity: 0.82; transform: none; } }