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