/* ─────────────────────────────────────────────────
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 in
(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