"""CSS and onload JS for the VN UI. Pure presentation strings — no logic.
Installed via launch(css=..., js=...).
"""
import os
# Font for CHARACTER dialogue lines (the narrator keeps --font-dialogue).
# Env-overridable — pass a full CSS font-family stack, e.g.
# ARS_FABULA_DIALOGUE_FONT="'Sazanami Gothic', monospace"
#
# 'MS Gothic' (and 'MS PGothic'/'Osaka-Mono') are Windows/mac-only and ABSENT on
# the Linux HF-Spaces deploy target, where the stack used to fall through to a
# generic 'monospace' — losing the retro-VN look. 'DotGothic16' (loaded from
# Google Fonts in the @import below) is a 16-dot bitmap-gothic webfont built to
# mimic exactly that MS-Gothic aesthetic, so it carries the look everywhere a
# local MS Gothic is unavailable while Windows users still get the real thing.
CHARACTER_FONT = os.getenv(
"ARS_FABULA_DIALOGUE_FONT",
"'MS Gothic', 'MS PGothic', 'DotGothic16', 'Osaka-Mono', monospace",
)
# ═══════════════════════════════════════════════════════════════════════
# CSS — VN themed (incl. scene-turn crossfade + sprite swap animations)
# ═══════════════════════════════════════════════════════════════════════
CSS = """
/* ── Fonts ── */
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600;1,400;1,600&family=Cinzel:wght@400;600&family=Noto+Sans+JP:wght@300;400&family=DotGothic16&display=swap');
/* ── Tokens ── */
:root {
--bg-void: #0d0d1a;
--bg-surface: #13132a;
--bg-raised: #1c1c38;
--bg-screen: #08081a;
--border-subtle: #252545;
--border-mid: #3a3a62;
--border-accent: #e8a87c;
--border-glow: rgba(232,168,124,0.35);
/* One warm text family + gold accent — cool purples are reserved for
surfaces/borders only, so no text sinks into the dark box. */
--text-primary: #ede5da;
--text-secondary:#b9b0a1;
--text-muted: #8f8779;
--text-accent: #e8a87c;
--text-narrator: #c6bdac;
/* One shared panel fill for the dialogue box, choice pills and input bar
so they're all the exact same colour + opacity. */
--dialogue-bg: rgba(15, 15, 25, 0.68);
--font-display: 'Cinzel', 'Georgia', serif;
--font-dialogue: 'Cormorant Garamond', 'Georgia', serif;
--font-ui: 'Noto Sans JP', 'Georgia', serif;
--font-character: __FONT_CHARACTER__; /* injected from ARS_FABULA_DIALOGUE_FONT */
}
/* ── Global Reset ── */
.gradio-container {
background: var(--bg-void) !important;
color: var(--text-primary) !important;
font-family: var(--font-ui) !important;
/* Size the frame to the LARGEST 16:9 that fits the viewport: nearly full
width (~97vw) and full height — the width whose 16:9 height is ~97vh
(97vh × 16/9 ≈ 173vh) — so the stage fills the screen instead of floating
in a band of empty space. (True edge-to-edge would mean dropping the 16:9
aspect on .vn-screen, which re-proportions the sprite framing — kept 16:9
here; see note in chat.) */
width: min(97vw, 2400px, 173vh) !important;
min-width: 720px !important; /* prevent collapse on small viewports */
margin: 0 auto !important;
}
body { background: var(--bg-void) !important; }
*:focus { outline: 2px solid var(--border-accent) !important; outline-offset: 2px; }
/* Cast Setup — a STEP shown AFTER Begin → Generated (between the title and the
character-design phase). Hidden by default; when active it replaces the title
content inside the canvas. JS toggles body.cast-setup-active.
NOTE: The doubled `.vn-title-content.vn-title-content` selector beats Gradio 6's
scoped-prefix specificity issue (same reason #cast-setup#cast-setup was needed). */
/* !important is required: Gradio 6 re-emits every custom rule with a
`.gradio-container-... .contain` scope prefix. The scoped copy of the
base `.vn-title-content { display:flex }` outranks this rule, while the
scoped copy of THIS rule can never match (body is outside .contain) —
so without !important the title stayed visible beside the cast setup. */
body.cast-setup-active .vn-title-content { display: none !important; }
body.cast-setup-active .vn-cast-setup {
display: flex !important;
}
/* But when NOT in cast-setup-active, the cast setup is always hidden. */
.vn-cast-setup { display: none; }
/* ── VN Screen ── */
.vn-screen {
position: relative;
width: 100%;
aspect-ratio: 16 / 9; /* 16:9, height derived purely from width */
min-height: 380px; /* (no vh cap — height tracks the frame, not
the page, so overlays never displace on
zoom/resize) */
background: var(--bg-screen);
border: 1px solid var(--border-mid);
border-radius: 6px;
overflow: hidden;
margin-bottom: 8px;
container-type: inline-size; /* lets dialogue/choice text size in cqw —
i.e. scale with the screen (and thus the
text box), not the page root font */
}
/* Background layers */
.vn-bg {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
object-fit: cover;
}
/* Only crossfade when the background ACTUALLY changed (an .old layer is present
to fade over). Otherwise every beat re-rendered the canvas and the bg faded
up from black again — the "blackish" flash on each advance. */
.vn-bg.old + .vn-bg.new { animation: vnBgFade 1.3s ease both; }
@keyframes vnBgFade { from { opacity: 0; } to { opacity: 1; } }
/* Sprites — anchored to the SCREEN bottom and layered BEHIND the dialogue
box (z 2 < dialogue 4), per VN staging: the box overlaps the sprites'
lower torso, concealing the raw bottom edge of upper-body renders.
The container spans the full screen so percentage heights resolve. */
.vn-chars {
position: absolute;
top: 0; bottom: 0; left: 0;
width: 100%;
display: flex;
justify-content: center;
align-items: flex-end; /* feet on the screen bottom */
gap: 2.4cqw; /* scales with the canvas, not the page */
pointer-events: none;
z-index: 2;
}
.vn-sprite {
/* Sized against the canvas WIDTH (cqw), the stable axis: the sprite is
locked to the canvas's proportions and never changes on browser zoom or
when min-height inflates the box past 16:9. 48cqw ≈ 86% of a true 16:9
canvas's height; max-height caps it if the box is ever taller than 16:9. */
height: 48cqw;
max-height: 86%;
width: auto;
max-width: 37%; /* must exceed the widest bake canvas's display
width — if this cap binds, object-fit:contain
letterboxes the sprite and SHRINKS that
character's face below the equal-face target.
At the 20%-face framing the widest cast member
(marie, ~940×1254 ⇒ ~36% display) just clears
this; three sprites still sum < 100% because the
others are far narrower (~27%), so 3-up doesn't
overlap. Bake canvases widen to fit wide poses
(cast_pipeline fig_w), so keep headroom here. */
object-fit: contain;
object-position: bottom center;
/* No per-render entrance animation: the canvas HTML is rebuilt every beat,
so an entrance keyframe replayed (and "dragged") on each advance. The
active/inactive filter still animates below. */
transition: filter 0.35s ease;
}
/* Active/inactive: the speaker is bright, the rest gently shadowed.
Dimming is FILTER-only — sprites are never made translucent. */
.vn-sprite.active { filter: none; }
.vn-sprite.inactive {
filter: brightness(0.6) saturate(0.85) contrast(0.95);
}
/* Choice state: once the line finishes typing and the choice cluster fades in,
EVERY sprite (active speaker included) takes the inactive dimming, so the
semi-transparent capsules and the free-text bar read cleanly over bright
costumes. Same gate as .vn-choices visibility; the existing 0.35s
filter transition makes it fade in with the capsules. */
.vn-screen[data-choices="1"][data-typing="0"] .vn-sprite,
.vn-screen[data-choices="1"][data-typing="0"] .vn-sprite.active {
filter: brightness(0.6) saturate(0.85) contrast(0.95);
}
@keyframes vnSpriteIn {
from { opacity: 0.2; transform: translateY(10px) scale(0.97); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
/* ── Dialogue Box ── (a wide, centered panel with a LOCKED height — it never
grows or shrinks with the line length — one flat scrim, not a gradient) */
.vn-dialogue {
position: absolute;
bottom: 20px; left: 20%; right: 20%; /* 60% width, centered */
height: clamp(130px, 13.5cqw, 230px); /* scales with the screen width (never
with line length) so it stays in
proportion to the cqw-sized text */
background: var(--dialogue-bg); /* shared panel colour */
border: 1px solid var(--border-subtle);
border-radius: 8px;
padding: 14px 16px 36px;
overflow: hidden; /* clip rather than expand */
}
.vn-dialogue.has-speaker {
border-left: 3px solid var(--border-accent);
}
/* Centered column — the box itself is already narrow (60% of the screen),
so the text uses most of it. */
.vn-dialogue-inner {
max-width: min(94%, 960px);
margin: 0 auto;
}
.vn-name-plate {
display: inline-block;
font-family: var(--font-display);
font-size: clamp(0.7rem, 1.1cqw, 1.2rem);
letter-spacing: 0.08em;
color: var(--text-accent);
background: rgba(232,168,124,0.1);
border: 1px solid var(--border-accent);
border-radius: 3px;
padding: 2px 10px;
margin-bottom: 8px;
}
.vn-name {
font-family: var(--font-display);
font-weight: 600;
color: var(--text-accent);
font-size: clamp(0.75rem, 1.2cqw, 1.3rem);
margin-bottom: 6px;
letter-spacing: 0.06em;
}
/* Character dialogue uses --font-character (MS Gothic by default, env-set). */
.vn-text {
font-family: var(--font-character);
color: var(--text-primary);
/* Scales with the screen/text-box width: ≈ the old 1.15rem on a ~1300px
frame, smaller on regular resolutions, larger on big frames. */
font-size: clamp(0.85rem, 1.4cqw, 1.5rem);
line-height: 1.7;
text-align: left;
padding: 0 16px;
text-indent: 0;
}
/* Narration is set in Georgia serif (distinct from the MS Gothic dialogue). */
.vn-text.narrator {
font-family: Georgia, 'Times New Roman', serif;
font-style: italic;
color: var(--text-narrator);
}
/* Typewriter cursor */
.vn-text.typing::after {
content: '▌';
animation: vnBlink 0.7s step-end infinite;
color: var(--text-accent);
margin-left: 1px;
}
@keyframes vnBlink { 50% { opacity: 0; } }
/* Click-to-advance affordance */
.vn-screen.advancing { cursor: pointer; }
.vn-continue {
position: absolute;
right: 16px; bottom: 10px;
color: var(--text-accent);
font-size: 1rem;
animation: vnPulse 1.2s ease infinite;
pointer-events: none;
}
@keyframes vnPulse { 0%,100% { opacity: 0.4; } 50% { opacity: 1; } }
/* Streaming loader — shown when the reader has caught up to the last beat the
model has produced but the turn is still being written. Same ✦ pulse as the
turn-pending indicator (body.vn-waiting), so "still generating" reads
consistently whether a turn was just submitted or is mid-stream. */
.vn-streaming-wait {
position: absolute;
right: 18px; bottom: 14px;
color: var(--text-accent);
font-size: 1.05rem;
z-index: 30;
animation: vnPulse 1.2s ease infinite;
pointer-events: none;
}
/* ── Choices ── (raised so the free-text bar docks directly beneath them
in the same centered column — one interaction cluster, not two) */
/* Independent vertical column, centered in the open scene area ABOVE the
dialogue box. The container spans the area but passes clicks through
(pointer-events:none) so only the pills are interactive. */
.vn-choices {
position: absolute;
inset: 0; /* span the whole canvas … */
display: flex;
flex-direction: column;
justify-content: center; /* … so the stack sits dead-center vertically */
align-items: center; /* horizontal center; pills size to content */
gap: 0.75rem;
pointer-events: none;
}
.vn-choices .vn-choice-btn { pointer-events: auto; }
/* Choices only appear once the line has finished typing (data-typing flips to
"0"); hidden during 'pending' and '1' so they never precede the text. They
fade in (opacity only — no transform) so the appearance is deliberate. */
.vn-screen:not([data-typing="0"]) .vn-choices { display: none; }
.vn-screen[data-typing="0"] .vn-choices { animation: vnChoicesIn 0.22s ease both; }
@keyframes vnChoicesIn { from { opacity: 0; } to { opacity: 1; } }
/* Choices are standalone rounded capsules — solid (opaque) dark fill so they
pop over the scene without stacking transparent layers, centered text, and
the MS Gothic character font (--font-character). */
.vn-choice-btn {
background: var(--dialogue-bg) !important; /* same fill as the text box */
border: 1px solid rgba(255,255,255,0.2) !important;
border-radius: 30px !important;
color: var(--text-primary) !important;
font-family: var(--font-character) !important; /* MS Gothic */
font-style: normal !important;
font-size: clamp(0.8rem, 1.25cqw, 1.35rem) !important;
padding: 0.75em 2em !important; /* tracks the scaled font */
width: auto !important; /* size to content, not full width */
text-align: center !important;
cursor: pointer !important;
transition: background 0.15s, border-color 0.15s, transform 0.12s !important;
}
.vn-choice-btn:hover {
background: rgba(40, 40, 62, 0.82) !important;
border-color: var(--border-accent) !important;
transform: translateY(-1px);
color: #fff !important;
}
/* Free-text capsule — the last item in the choice column. Same fill/shape as
the pills, with an inline input + send arrow. */
.vn-free-capsule {
display: flex;
align-items: center;
width: min(60%, 460px);
background: var(--dialogue-bg);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 30px;
overflow: hidden;
pointer-events: auto;
transition: border-color 0.15s, background 0.15s;
}
.vn-free-capsule:focus-within {
border-color: var(--border-accent); /* accent the rim only — keep the fill */
}
.vn-free-text,
.vn-free-text:focus {
flex: 1 1 auto;
min-width: 0;
background: transparent !important; /* no Gradio/browser fill on focus */
border: none !important;
outline: none !important;
box-shadow: none !important;
color: var(--text-primary);
font-family: var(--font-character);
font-size: clamp(0.8rem, 1.25cqw, 1.35rem);
padding: 0.7em 0.5em 0.7em 1.5em; /* tracks the scaled font */
}
.vn-free-text::placeholder { color: var(--text-muted); font-style: italic; }
.vn-free-go {
flex: 0 0 auto;
background: transparent;
border: none;
color: var(--text-accent);
font-size: clamp(0.85rem, 1.3cqw, 1.4rem);
cursor: pointer;
padding: 0 1.1em;
}
.vn-free-go:hover { color: #fff; }
/* ── Generation-wait screen ── the app's single waiting vocabulary: every
phase that blocks on generation (sprite bake, VRAM swap, opening turn)
shows this centered ✦ pulse — the same star that marks turn/stream waits. */
.vn-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: 14px;
background: radial-gradient(ellipse at center, #1c1c38 0%, var(--bg-screen) 70%);
}
.vn-loading-star {
color: var(--text-accent);
font-size: clamp(1.6rem, 3.4cqw, 2.6rem);
animation: vnPulse 1.2s ease infinite;
}
.vn-loading-msg {
font-family: var(--font-display);
color: var(--text-accent);
font-size: clamp(0.95rem, 1.8cqw, 1.5rem);
letter-spacing: 0.15em;
}
.vn-loading-sub {
font-family: var(--font-ui);
color: var(--text-secondary);
font-size: clamp(0.7rem, 1.05cqw, 0.95rem);
letter-spacing: 0.08em;
}
/* ── Casting beat ── */
.vn-casting {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: 24px;
background: radial-gradient(ellipse at center, #1c1c38 0%, var(--bg-screen) 70%);
}
.vn-casting .casting-title {
font-family: var(--font-display);
color: var(--text-accent);
font-size: clamp(0.95rem, 1.8cqw, 1.5rem);
letter-spacing: 0.15em;
animation: vnPulse 1.6s ease infinite;
}
.vn-casting .portraits { display: flex; gap: 3cqw; }
.vn-portrait {
width: clamp(120px, 14cqw, 230px); height: clamp(170px, 20cqw, 330px);
border-radius: 8px;
border: 1px solid var(--border-mid);
background: var(--bg-surface);
overflow: hidden;
display: flex; align-items: center; justify-content: center;
}
.vn-portrait.pending { border-style: dashed; border-color: var(--border-subtle); }
/* The card currently being generated: accent border + soft pulse so the eye
tracks which character the bake is on. */
.vn-portrait.active {
border-color: var(--border-accent);
box-shadow: 0 0 0 1px var(--border-accent), 0 0 18px -4px var(--border-accent);
animation: castingCardPulse 1.4s ease-in-out infinite;
}
@keyframes castingCardPulse {
0%, 100% { box-shadow: 0 0 0 1px var(--border-accent), 0 0 12px -6px var(--border-accent); }
50% { box-shadow: 0 0 0 1px var(--border-accent), 0 0 22px -2px var(--border-accent); }
}
/* Coarse generation progress bar (under the casting title). The fill width is
honest roster-level progress; the moving shimmer keeps it alive during the
long blocking ComfyUI bake, when the width can't update mid-call. */
.vn-castbar {
width: clamp(180px, 34cqw, 460px);
height: 6px;
border-radius: 999px;
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
overflow: hidden;
}
.vn-castbar > i {
display: block; height: 100%;
width: 2%;
border-radius: 999px;
background: linear-gradient(90deg, var(--border-mid), var(--text-accent));
transition: width 0.6s ease;
position: relative;
overflow: hidden;
}
.vn-castbar > i::after {
content: ""; position: absolute; inset: 0;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.38), transparent);
animation: castbarShimmer 1.3s linear infinite;
}
@keyframes castbarShimmer {
from { transform: translateX(-100%); }
to { transform: translateX(100%); }
}
/* The ✦ start-symbol shown inside the active card before its sprite exists. */
.casting-spinner {
font-size: clamp(1.4rem, 2.4cqw, 2.2rem);
color: var(--text-accent);
animation: vnPulse 1.2s ease infinite;
}
.vn-portrait img {
width: 100%; height: 100%;
object-fit: cover; object-position: top;
animation: portraitReveal 0.5s ease both;
}
@keyframes portraitReveal {
from { opacity: 0; transform: scale(0.92) translateY(4px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.pname {
font-family: var(--font-ui);
font-size: clamp(0.66rem, 0.95cqw, 0.92rem);
color: var(--text-accent);
margin-top: 4px;
letter-spacing: 0.06em;
}
.pname.pending {
color: var(--text-muted);
}
.portrait-wrap {
text-align: center;
}
/* ── Interactive Casting Review ── */
.vn-casting-review {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
max-height: 100%;
overflow-y: auto;
background: radial-gradient(ellipse at center, #1c1c38 0%, var(--bg-screen) 70%);
padding: 20px;
}
.casting-review-title {
font-family: var(--font-display);
color: var(--text-accent);
font-size: 0.85rem;
letter-spacing: 0.15em;
margin-bottom: 18px;
}
.review-layout {
display: flex;
gap: 28px;
align-items: flex-start;
width: 100%;
max-width: 800px;
max-height: 100%;
justify-content: center;
}
.review-image {
width: clamp(150px, 18cqw, 240px);
height: clamp(250px, 31cqw, 400px);
border: 1px solid var(--border-mid);
border-radius: 6px;
overflow: hidden;
background: var(--bg-surface);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.review-image img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: top;
}
.review-no-image {
color: var(--text-muted);
font-style: italic;
font-size: 0.82rem;
text-align: center;
padding: 16px;
}
.review-panel {
flex: 1;
min-width: 260px;
display: flex;
flex-direction: column;
gap: 10px;
}
.review-name {
font-family: var(--font-display);
color: var(--text-accent);
font-size: clamp(1.15rem, 1.5cqw, 1.6rem);
letter-spacing: 0.06em;
}
/* Designed character description — role badge + personality paragraph */
.review-desc { display: flex; flex-direction: column; gap: 6px; }
.review-role {
align-self: flex-start;
font-family: var(--font-ui);
font-size: clamp(0.66rem, 0.85cqw, 0.92rem);
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--text-accent);
background: rgba(232,168,124,0.1);
border: 1px solid var(--border-accent);
border-radius: 3px;
padding: 2px 10px;
}
.review-personality {
font-family: var(--font-dialogue);
font-style: italic;
font-size: clamp(0.82rem, 1.05cqw, 1.15rem);
line-height: 1.55;
color: var(--text-secondary);
margin: 0;
}
.review-prompt-label {
font-family: var(--font-ui);
font-size: 0.68rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--text-muted);
}
.review-prompt {
width: 100%;
background: var(--bg-surface) !important;
border: 1px solid var(--border-mid) !important;
border-radius: 4px !important;
color: var(--text-primary) !important;
font-family: var(--font-dialogue) !important;
font-size: 0.9rem !important;
line-height: 1.5 !important;
padding: 10px 12px !important;
resize: vertical;
caret-color: var(--text-accent) !important;
}
.review-prompt:focus {
border-color: var(--border-accent) !important;
outline: none !important;
box-shadow: 0 0 0 2px var(--border-glow) !important;
}
/* The editable personality keeps the old read-only paragraph's voice
(italic, secondary) so the screen still reads the same at a glance. */
.review-prompt.review-desc-edit {
font-style: italic;
color: var(--text-secondary) !important;
}
.review-actions {
display: flex;
gap: 12px;
margin-top: 4px;
}
.review-btn {
flex: 1;
padding: 10px 0 !important;
border-radius: 4px !important;
font-family: var(--font-display) !important;
font-size: 0.82rem !important;
letter-spacing: 0.1em !important;
cursor: pointer !important;
transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease !important;
border: 1px solid var(--border-mid) !important;
}
.review-btn.confirm {
background: linear-gradient(135deg, #c07840, #e8a87c) !important;
border-color: var(--border-accent) !important;
color: #0d0d1a !important;
font-weight: 600 !important;
}
.review-btn.confirm:hover {
box-shadow: 0 0 14px var(--border-glow) !important;
}
.review-btn.redo {
background: var(--bg-surface) !important;
color: var(--text-secondary) !important;
}
.review-btn.redo:hover {
border-color: var(--border-accent) !important;
color: var(--text-accent) !important;
}
/* ── Cast Panel extras ── */
.cast-expressions {
font-family: var(--font-ui);
font-size: 0.72rem;
color: var(--text-muted);
margin-top: 4px;
}
.cast-panel-empty {
padding: 8px;
color: var(--text-muted);
font-family: var(--font-dialogue);
font-style: italic;
font-size: clamp(0.88rem, 1.1cqw, 1.2rem);
}
/* ── Cast Cards (sidebar) ── */
.cast-card {
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-left: 3px solid var(--border-accent);
border-radius: 5px;
padding: 9px 12px;
margin-bottom: 6px;
transition: border-color 0.15s;
}
.cast-card:hover { border-color: var(--border-mid); }
.cast-card .name {
font-family: var(--font-display);
font-size: 0.88rem;
color: var(--text-accent);
letter-spacing: 0.06em;
}
.cast-card .role {
font-family: var(--font-ui);
font-size: 0.72rem;
color: var(--text-muted);
margin-top: 2px;
}
.cast-card .personality {
font-family: var(--font-dialogue);
font-style: italic;
font-size: 0.82rem;
color: var(--text-secondary);
margin-top: 5px;
line-height: 1.4;
}
/* ── Header ── */
.vn-header { padding: 14px 0 12px; margin-bottom: 14px; }
.vn-title-main {
font-family: var(--font-display);
font-size: 1.9rem;
font-weight: 600;
color: var(--text-accent);
letter-spacing: 0.3em;
text-shadow: 0 0 40px rgba(232,168,124,0.25);
}
.vn-title-sub {
font-family: var(--font-dialogue);
font-style: italic;
font-size: 0.82rem;
color: var(--text-muted);
letter-spacing: 0.04em;
margin-top: 2px;
}
.vn-header-rule {
height: 1px;
background: linear-gradient(to right, var(--border-accent), transparent);
margin-top: 10px;
opacity: 0.35;
}
/* ── Sidebar ── */
.sidebar-col { border-right: 1px solid var(--border-subtle); padding-right: 14px !important; }
.sidebar-section-label {
font-family: var(--font-display);
font-size: 0.68rem;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--text-accent);
border-bottom: 1px solid var(--border-subtle);
padding-bottom: 4px;
margin: 16px 0 10px;
}
/* ── Scene Info HUD ── */
.scene-info p, .scene-info {
font-family: var(--font-ui) !important;
font-size: 0.72rem !important;
color: var(--text-muted) !important;
letter-spacing: 0.06em !important;
text-transform: uppercase !important;
border-left: 2px solid var(--border-subtle);
padding-left: 8px;
margin: 4px 0 !important;
}
/* ── Buttons ── */
/* Scope button transitions to colour only — `transition: all` animated layout
geometry (transform/size/position) whenever buttons relaid out (e.g. the
choice column appearing), which read as a dark element "dragging" in. */
button { border-radius: 3px !important; transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease !important; }
.gradio-container button:not(.vn-choice-btn) {
font-family: var(--font-display) !important;
font-size: 0.72rem !important;
letter-spacing: 0.1em !important;
text-transform: uppercase !important;
background: var(--bg-surface) !important;
border: 1px solid var(--border-mid) !important;
color: var(--text-secondary) !important;
}
.gradio-container button:not(.vn-choice-btn):hover {
border-color: var(--border-accent) !important;
color: var(--text-accent) !important;
background: var(--bg-raised) !important;
}
button[variant="primary"] {
background: linear-gradient(135deg, #c07840, #e8a87c) !important;
border-color: var(--border-accent) !important;
color: #0d0d1a !important;
font-weight: 600 !important;
}
button[variant="primary"]:hover {
box-shadow: 0 0 14px var(--border-glow) !important;
}
button[variant="stop"] { border-color: #c06060 !important; color: #c06060 !important; }
/* ── Free-text input ── */
#free-input-row { margin-top: 8px; border-top: 1px solid var(--border-subtle); padding-top: 8px; }
#free-input textarea, #free-input input {
background: var(--bg-surface) !important;
border: 1px solid var(--border-mid) !important;
color: var(--text-primary) !important;
font-family: var(--font-dialogue) !important;
font-size: 1rem !important;
font-style: italic !important;
caret-color: var(--text-accent) !important;
border-radius: 3px !important;
}
#free-input textarea:focus, #free-input input:focus {
border-color: var(--border-accent) !important;
box-shadow: 0 0 0 2px var(--border-glow) !important;
}
#free-input label {
font-family: var(--font-ui) !important;
font-size: 0.7rem !important;
letter-spacing: 0.1em !important;
text-transform: uppercase !important;
color: var(--text-muted) !important;
}
#free-submit-btn button {
background: linear-gradient(135deg, #c07840, #e8a87c) !important;
border-color: var(--border-accent) !important;
color: #0d0d1a !important;
font-weight: 600 !important;
}
#free-submit-btn button:hover {
box-shadow: 0 0 14px var(--border-glow) !important;
}
/* ── Narration Log ── */
.gr-textbox textarea {
background: var(--bg-void) !important;
border: 1px solid var(--border-subtle) !important;
color: var(--text-narrator) !important;
font-family: var(--font-dialogue) !important;
font-style: italic !important;
font-size: 0.88rem !important;
line-height: 1.65 !important;
border-radius: 4px !important;
}
label {
font-family: var(--font-ui) !important;
font-size: 0.72rem !important;
letter-spacing: 0.1em !important;
text-transform: uppercase !important;
color: var(--text-muted) !important;
}
/* ── Number input (save slot) ── */
.gr-number input {
background: var(--bg-surface) !important;
border: 1px solid var(--border-mid) !important;
color: var(--text-primary) !important;
font-family: var(--font-ui) !important;
border-radius: 3px !important;
text-align: center !important;
}
/* ── Hide audio widget (audio still plays via autoplay) ── */
.gr-audio { height: 2px !important; overflow: hidden !important; opacity: 0 !important; pointer-events: none !important; margin: 0 !important; }
/* ── Gradio headings ── */
h1, h2, h3 { font-family: var(--font-display) !important; color: var(--text-accent) !important; }
/* ════════════════════════════════════════════════════════════════
REDESIGN — everything lives inside the screen (Ren'Py grammar)
════════════════════════════════════════════════════════════════ */
/* Stacking order: dialogue 4 · choices 5 · tray 10 · panels 20 */
.vn-dialogue { z-index: 4; }
.vn-choices { z-index: 5; }
/* ── Icon tray (top-right of dialogue box) ── */
.vn-icon-tray {
position: absolute;
bottom: 7px; left: 50%;
transform: translateX(-50%);
display: flex;
gap: 8px;
align-items: center;
z-index: 10;
}
.vn-tray-btn {
background: rgba(255,255,255,0.05) !important;
border: 1px solid var(--border-mid) !important;
color: var(--text-secondary) !important;
font-family: var(--font-ui) !important;
font-size: clamp(0.6rem, 0.75cqw, 0.85rem) !important;
letter-spacing: 0.1em !important;
text-transform: uppercase !important;
padding: 2px 8px !important;
cursor: pointer !important;
border-radius: 3px !important;
transition: color 0.15s, border-color 0.15s, background 0.15s !important;
}
.vn-tray-btn:hover, .vn-tray-btn.active {
color: var(--text-accent) !important;
border-color: var(--border-accent) !important;
background: rgba(232,168,124,0.1) !important;
}
.vn-icon-tray .vn-tray-confirm-row { display: none; }
.vn-icon-tray.confirming .vn-tray-normal { display: none; }
.vn-icon-tray.confirming .vn-tray-confirm-row { display: inline !important; }
.vn-reset-confirm { cursor: pointer; color: #c06060; }
.vn-reset-confirm:hover { color: #e07070; }
.vn-reset-cancel { cursor: pointer; color: var(--text-muted); }
.vn-reset-cancel:hover { color: var(--text-secondary); }
/* ── Overlay panels ── */
.vn-panel {
position: absolute;
z-index: 20;
background: rgba(10, 10, 28, 0.97);
font-family: var(--font-ui);
}
.vn-panel-title {
font-family: var(--font-display);
font-size: clamp(0.75rem, 0.95cqw, 1.05rem);
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--text-accent);
border-bottom: 1px solid var(--border-subtle);
padding-bottom: 6px;
margin-bottom: 12px;
}
.vn-panel-close {
position: absolute;
top: 10px; right: 12px;
background: transparent !important;
border: none !important;
color: var(--text-muted) !important;
font-size: clamp(0.9rem, 1.15cqw, 1.3rem) !important;
cursor: pointer !important;
padding: 2px 6px !important;
}
.vn-panel-close:hover { color: var(--text-accent) !important; }
/* Log panel — slides up over the dialogue box.
Closed panels are visibility:hidden, NOT just translated out of the clip:
during Gradio's canvas-HTML swap the browser could paint the off-canvas
panels unclipped for a few frames (seen as dark log/cast sheets "dragging"
across the page each turn). visibility doesn't depend on overflow clipping
or compositor timing, so a closed panel can never paint. The 0s visibility
transition is DELAYED by the slide duration on close (so the slide-out
stays visible) and instant on open.
Transitions are gated behind .vn-anim, which ONLY the user-toggle JS adds
(togglePanel / close / Escape). Render-restoration (restoreUiState after a
canvas swap re-applies .open to the freshly inserted panel) must SNAP to
its state — otherwise every turn render replays the slide-in, dragging the
dark sheet across the scene. */
.vn-panel-log {
bottom: 0; left: 0; right: 0;
height: 55%;
border-top: 1px solid var(--border-mid);
transform: translateY(102%);
visibility: hidden;
transition: none;
overflow-y: auto;
padding: 18px 28px;
}
.vn-panel-log.vn-anim { transition: transform 0.3s ease, visibility 0s linear 0.3s; }
.vn-panel-log.open {
transform: translateY(0);
visibility: visible;
}
.vn-panel-log.open.vn-anim { transition: transform 0.3s ease, visibility 0s; }
.vn-log-text {
font-family: var(--font-dialogue);
font-style: italic;
font-size: clamp(0.85rem, 1.25cqw, 1.35rem);
line-height: 1.7;
color: var(--text-narrator);
white-space: pre-wrap;
}
/* Player answers inside the session register (▸-prefixed lines) */
.vn-log-answer {
font-style: normal;
color: var(--text-accent);
}
/* Cast panel — slides in from the right */
.vn-panel-cast {
top: 0; right: 0; bottom: 0;
width: 40%;
min-width: 260px;
border-left: 1px solid var(--border-mid);
transform: translateX(102%);
visibility: hidden; /* unpaintable when closed — see log panel note */
transition: none; /* animates only on user toggle (.vn-anim) */
overflow-y: auto;
padding: 18px 16px;
}
.vn-panel-cast.vn-anim { transition: transform 0.3s ease, visibility 0s linear 0.3s; }
.vn-panel-cast.open {
transform: translateX(0);
visibility: visible;
}
.vn-panel-cast.open.vn-anim { transition: transform 0.3s ease, visibility 0s; }
.cast-panel-card {
display: flex;
gap: 14px;
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-left: 3px solid var(--border-accent);
border-radius: 5px;
padding: 12px 14px;
margin-bottom: 10px;
}
.cast-panel-thumb {
width: clamp(78px, 8cqw, 130px); height: clamp(112px, 11.5cqw, 188px);
object-fit: cover; object-position: top;
border-radius: 3px;
flex-shrink: 0;
background: var(--bg-raised);
}
.cast-panel-name {
font-family: var(--font-display);
font-size: clamp(1rem, 1.3cqw, 1.45rem);
color: var(--text-accent);
letter-spacing: 0.06em;
}
.cast-panel-role {
font-size: clamp(0.76rem, 1cqw, 1.1rem);
color: var(--text-muted);
margin-top: 2px;
}
.cast-panel-personality {
font-family: var(--font-dialogue);
font-style: italic;
font-size: clamp(0.85rem, 1.1cqw, 1.2rem);
color: var(--text-secondary);
margin-top: 6px;
line-height: 1.45;
}
/* Save panel — centered modal */
.vn-panel-save {
top: 50%; left: 50%;
transform: translate(-50%, -50%) scale(0.95);
opacity: 0;
visibility: hidden; /* unpaintable when closed — see log panel note */
pointer-events: none;
transition: none; /* animates only on user toggle (.vn-anim) */
width: clamp(380px, 34cqw, 560px);
max-height: 86%;
overflow-y: auto;
border: 1px solid var(--border-mid);
border-radius: 6px;
padding: 22px 24px;
box-shadow: 0 8px 60px rgba(0,0,0,0.6);
}
.vn-panel-save.vn-anim { transition: opacity 0.2s, transform 0.2s, visibility 0s linear 0.2s; }
.vn-panel-save.open {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
visibility: visible;
pointer-events: auto;
}
.vn-panel-save.open.vn-anim { transition: opacity 0.2s, transform 0.2s, visibility 0s; }
/* Rich slot rows — one full-width card per slot: name, chapter · turn,
cast names, timestamp. Shared by the save panel and the menu load list. */
.vn-slot-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 14px;
width: 100%;
}
.vn-slot-btn {
display: grid !important;
grid-template-columns: 1fr auto;
row-gap: 2px;
width: 100% !important;
background: var(--bg-surface) !important;
border: 1px solid var(--border-mid) !important;
border-radius: 4px !important;
padding: 8px 12px !important;
cursor: pointer !important;
text-align: left !important;
transition: border-color 0.15s, background 0.15s !important;
}
.vn-slot-btn:hover {
border-color: var(--border-accent) !important;
}
.vn-slot-btn.active {
border-color: var(--border-accent) !important;
background: rgba(232,168,124,0.1) !important;
}
.vn-slot-btn.empty .vn-slot-name { color: var(--text-muted); font-style: italic; }
.vn-slot-btn.disabled { opacity: 0.45 !important; cursor: default !important; }
.vn-slot-name {
font-family: var(--font-display);
font-size: clamp(0.82rem, 1.05cqw, 1.15rem);
color: var(--text-accent);
letter-spacing: 0.05em;
grid-column: 1;
}
.vn-slot-time {
font-family: var(--font-ui);
font-size: clamp(0.66rem, 0.85cqw, 0.92rem);
color: var(--text-muted);
grid-column: 2; grid-row: 1;
align-self: center;
}
.vn-slot-meta {
font-family: var(--font-ui);
font-size: clamp(0.7rem, 0.9cqw, 1rem);
color: var(--text-secondary);
grid-column: 1 / -1;
}
.vn-slot-cast {
font-family: var(--font-dialogue);
font-style: italic;
font-size: clamp(0.7rem, 0.9cqw, 1rem);
color: var(--text-muted);
grid-column: 1 / -1;
}
/* Save-name input — sits between the slots and the Save/Load actions. */
.vn-save-name {
width: 100%;
box-sizing: border-box;
background: rgba(255,255,255,0.04);
border: 1px solid var(--border-mid);
border-radius: 3px;
color: var(--text-primary);
font-family: var(--font-ui);
font-size: clamp(0.78rem, 1cqw, 1.1rem);
padding: 7px 10px;
margin-bottom: 10px;
transition: border-color 0.15s;
}
.vn-save-name:focus {
border-color: var(--border-accent);
outline: none;
box-shadow: 0 0 0 2px var(--border-glow);
}
.vn-save-name::placeholder { color: var(--text-muted); font-style: italic; }
.vn-save-actions { display: flex; gap: 10px; }
.vn-save-action {
flex: 1;
background: var(--bg-surface) !important;
border: 1px solid var(--border-mid) !important;
color: var(--text-secondary) !important;
font-family: var(--font-display) !important;
font-size: clamp(0.72rem, 0.95cqw, 1rem) !important;
letter-spacing: 0.12em !important;
text-transform: uppercase !important;
padding: 9px 0 !important;
border-radius: 3px !important;
cursor: pointer !important;
}
.vn-save-action:hover {
border-color: var(--border-accent) !important;
color: var(--text-accent) !important;
}
.vn-save-status {
margin-top: 12px;
font-size: clamp(0.74rem, 0.95cqw, 1rem);
color: var(--text-muted);
font-style: italic;
min-height: 1.1em;
}
/* ── Title screen ── */
.vn-title-screen {
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(160deg, #0d0d1a 0%, #16162e 60%, #0a0a20 100%);
}
.vn-title-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 22px;
}
.vn-title-screen .vn-title-main { font-size: clamp(1.7rem, 4cqw, 3.1rem); letter-spacing: 0.35em; }
.vn-title-screen .vn-title-sub { font-size: 0.95rem; margin-top: -14px; }
.vn-title-cast-choice {
display: flex;
gap: 0;
border: 1px solid var(--border-mid);
border-radius: 3px;
overflow: hidden;
}
.vn-title-opt {
background: transparent !important;
border: none !important;
border-radius: 0 !important;
color: var(--text-muted) !important;
font-family: var(--font-ui) !important;
font-size: 0.72rem !important;
letter-spacing: 0.12em !important;
text-transform: uppercase !important;
padding: 8px 18px !important;
cursor: pointer !important;
transition: background 0.15s, color 0.15s !important;
}
.vn-title-opt.active {
background: rgba(232,168,124,0.12) !important;
color: var(--text-accent) !important;
}
.vn-title-begin {
font-family: var(--font-display) !important;
font-size: 0.9rem !important;
letter-spacing: 0.22em !important;
text-transform: uppercase !important;
background: linear-gradient(135deg, #c07840, #e8a87c) !important;
border: none !important;
color: #0d0d1a !important;
padding: 12px 44px !important;
border-radius: 3px !important;
cursor: pointer !important;
font-weight: 600 !important;
transition: box-shadow 0.2s !important;
}
.vn-title-begin:hover { box-shadow: 0 0 22px var(--border-glow) !important; }
.vn-title-actions { display: flex; gap: 14px; align-items: center; }
.vn-title-load {
font-family: var(--font-display) !important;
font-size: 0.9rem !important;
letter-spacing: 0.22em !important;
text-transform: uppercase !important;
background: var(--bg-surface) !important;
border: 1px solid var(--border-mid) !important;
color: var(--text-secondary) !important;
padding: 12px 36px !important;
border-radius: 3px !important;
cursor: pointer !important;
transition: color 0.15s, border-color 0.15s !important;
}
.vn-title-load:hover {
color: var(--text-accent) !important;
border-color: var(--border-accent) !important;
}
.vn-title-load.disabled {
opacity: 0.35 !important;
cursor: default !important;
}
/* ── Main-menu Load list — a third title-screen section, like cast setup.
Toggled by body.menu-load-active; !important beats Gradio 6's scoped
re-emission of the base rules (see the cast-setup note above). ── */
.vn-menu-load { display: none; }
body.menu-load-active .vn-title-content { display: none !important; }
body.menu-load-active .vn-cast-setup { display: none !important; }
body.menu-load-active .vn-menu-load {
display: flex !important;
flex-direction: column;
align-items: center;
justify-content: flex-start;
width: 100%;
height: 100%;
padding: 28px 36px;
overflow-y: auto;
gap: 16px;
background: linear-gradient(160deg, #0d0d1a 0%, #16162e 60%, #0a0a20 100%);
}
.vn-ml-title {
font-family: var(--font-display);
font-size: clamp(1.3rem, 2cqw, 2.1rem);
letter-spacing: 0.28em;
color: var(--text-accent);
text-shadow: 0 0 30px rgba(232,168,124,0.2);
}
.vn-ml-list { width: 100%; max-width: min(620px, 80%); }
.vn-ml-back {
font-family: var(--font-display) !important;
font-size: clamp(0.82rem, 1.05cqw, 1.15rem) !important;
letter-spacing: 0.18em !important;
text-transform: uppercase !important;
background: var(--bg-surface) !important;
border: 1px solid var(--border-mid) !important;
color: var(--text-secondary) !important;
padding: 10px 32px !important;
border-radius: 3px !important;
cursor: pointer !important;
}
.vn-ml-back:hover {
color: var(--text-accent) !important;
border-color: var(--border-accent) !important;
}
/* ── Title screen — player name input ── */
.vn-title-name-row { margin-top: 8px; }
.vn-title-name-input {
background: rgba(255,255,255,0.04);
border: 1px solid var(--border-mid);
color: var(--text-primary);
font-family: var(--font-ui);
font-size: 0.85rem;
letter-spacing: 0.06em;
padding: 8px 14px;
border-radius: 3px;
text-align: center;
width: clamp(160px, 20cqw, 280px);
transition: border-color 0.15s;
}
.vn-title-name-input:focus {
border-color: var(--border-accent);
outline: none;
box-shadow: 0 0 0 2px var(--border-glow);
}
.vn-title-name-input::placeholder { color: var(--text-muted); }
/* ── Cast Setup ── */
.vn-cast-setup {
display: none;
flex-direction: column;
align-items: center;
justify-content: flex-start;
width: 100%;
height: 100%;
padding: 28px 36px;
overflow-y: auto;
gap: 14px;
background: linear-gradient(160deg, #0d0d1a 0%, #16162e 60%, #0a0a20 100%);
}
.vn-cs-header { text-align: center; max-width: min(820px, 78%); }
.vn-cs-title {
font-family: var(--font-display);
font-size: clamp(1.3rem, 2cqw, 2.1rem);
letter-spacing: 0.28em;
color: var(--text-accent);
text-shadow: 0 0 30px rgba(232,168,124,0.2);
margin-bottom: 8px;
}
.vn-cs-desc {
font-family: var(--font-dialogue);
font-size: clamp(0.85rem, 1.15cqw, 1.25rem);
line-height: 1.65;
color: var(--text-secondary);
text-align: left;
margin-top: 4px;
}
.vn-cs-desc p { margin: 6px 0; }
/* ZeroGPU "generated cast is a preview here" callout — warm accent box,
shown only on the in-process Space (see screens._zerogpu_mock_notice). */
.vn-cs-notice {
margin: 2px 0 10px;
background: rgba(232,168,124,0.08);
border: 1px solid var(--border-accent);
border-radius: 4px;
padding: 9px 13px;
font-family: var(--font-ui);
font-size: clamp(0.72rem, 0.95cqw, 0.98rem);
line-height: 1.5;
color: var(--text-secondary);
text-align: left;
}
.vn-cs-notice strong { color: var(--text-accent); }
.vn-cs-desc code {
background: rgba(232,168,124,0.1);
color: var(--text-accent);
padding: 0 4px;
border-radius: 2px;
font-size: 0.92em; /* tracks the scaled description text */
}
.vn-cs-cards {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
max-width: min(860px, 80%);
}
.vn-cs-card {
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: 6px;
padding: 12px 14px;
transition: border-color 0.15s;
}
.vn-cs-card:focus-within { border-color: var(--border-accent); }
.vn-cs-card-row {
display: flex;
gap: 10px;
margin-bottom: 8px;
}
.vn-cs-field {
display: flex;
flex-direction: column;
gap: 3px;
flex: 1;
}
.vn-cs-field label {
font-family: var(--font-ui);
font-size: clamp(0.68rem, 0.85cqw, 0.95rem);
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--text-muted);
}
.vn-cs-input {
background: rgba(255,255,255,0.04);
border: 1px solid var(--border-mid);
border-radius: 3px;
color: var(--text-primary);
font-family: var(--font-ui);
font-size: clamp(0.88rem, 1.15cqw, 1.25rem);
padding: 7px 10px;
transition: border-color 0.15s;
width: 100%;
box-sizing: border-box;
}
.vn-cs-input:focus {
border-color: var(--border-accent);
outline: none;
box-shadow: 0 0 0 2px var(--border-glow);
}
.vn-cs-input::placeholder { color: var(--text-muted); font-style: italic; font-size: 0.9em; }
.vn-cs-tags-row { width: 100%; }
.vn-cs-tags-input {
font-family: monospace;
font-size: clamp(0.8rem, 1.05cqw, 1.15rem);
}
.vn-cs-fast {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
max-width: min(860px, 80%);
width: 100%;
padding: 6px 0;
}
.vn-cs-fast input[type="checkbox"] {
accent-color: var(--border-accent);
width: 15px; height: 15px;
cursor: pointer;
flex-shrink: 0;
}
.vn-cs-fast span {
font-family: var(--font-ui);
font-size: clamp(0.78rem, 1cqw, 1.05rem);
color: var(--text-muted);
letter-spacing: 0.04em;
}
.vn-cs-actions {
display: flex;
gap: 14px;
justify-content: center;
width: 100%;
max-width: min(860px, 80%);
padding-top: 6px;
}
.vn-cs-btn {
font-family: var(--font-display) !important;
font-size: clamp(0.82rem, 1.05cqw, 1.15rem) !important;
letter-spacing: 0.18em !important;
text-transform: uppercase !important;
padding: 10px 32px !important;
border-radius: 3px !important;
cursor: pointer !important;
transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease !important;
border: 1px solid var(--border-mid) !important;
}
.vn-cs-btn.vn-cs-back {
background: var(--bg-surface) !important;
color: var(--text-secondary) !important;
}
.vn-cs-btn.vn-cs-back:hover {
border-color: var(--border-accent) !important;
color: var(--text-accent) !important;
}
.vn-cs-btn.vn-cs-go {
background: linear-gradient(135deg, #c07840, #e8a87c) !important;
border-color: var(--border-accent) !important;
color: #0d0d1a !important;
font-weight: 600 !important;
}
.vn-cs-btn.vn-cs-go:hover {
box-shadow: 0 0 14px var(--border-glow) !important;
}
/* Choice pills are styled in the "rounded capsules" block above. */
/* ── Stage wrapper — the relative containing block for the canvas + the
free-text bar. The bar overlays INSIDE the canvas frame, anchored to this
wrapper (an earlier version had no positioned ancestor and the box flew
across the viewport). ── */
#stage { position: relative !important; gap: 0 !important; padding: 0 !important;
margin-bottom: 8px !important; width: 100% !important; }
/* The gr.HTML wrapper insets the canvas by 12px each side; remove it so the
canvas spans the full stage width and the free-text bar (70% of the stage)
lines up with the choices (70% of the canvas). */
#stage .html-container { padding: 0 !important; }
#stage .vn-screen { margin-bottom: 0 !important; }
/* ── Free-text action bar — docked inside the canvas, in the same centered
column as the choices (which sit just above it) and styled to match the
choice buttons, so it reads as part of the interaction cluster rather than
a glued-on pill. Hidden until the END of a turn — revealed only on the last
beat once the text has finished revealing, together with the choices (JS
sets body.vn-playing; see syncStage). ── */
/* The bar is styled to be visually identical to a .vn-choice-btn (see the
"Choice restyle" block above): the same translucent void-scrim, blur(4px),
borderless 2px accent left bar, sharp corners and italic dialogue voice —
so it reads as just one more option in the stack, not a separate widget. */
/* The free-text input now lives INSIDE the canvas as .vn-free-capsule (the
last item in the centered choice column). This Gradio row is kept only as
the hidden value/submit bridge — never shown. */
#free-input-row { display: none !important; }
/* The textbox fills the bar with no inner chrome — every Gradio wrapper
background is forced transparent so only the row's scrim shows (otherwise
the opaque .block fill makes the bar look solid). NOTE: in this Gradio
version the is nested INSIDE the