"""
UI layout — CUSTOM Gradio Blocks layout for DoodleBook.
Aesthetic: "Construction-paper scrapbook" — a handmade keepsake, not a dashboard.
Signature details:
- Warm construction-paper grain background (inline SVG fractal noise)
- Wobbly hand-drawn borders via an SVG displacement filter (#wobble)
- Washi-tape strips, slightly rotated cards, polaroid-pinned doodle
- Crayon-textured hand-lettered headings (Gaegu / Caveat) + legible body (Nunito)
Off-Brand Badge: zero Gradio defaults. Every styled element uses elem_classes so the
CSS actually targets Gradio 5's DOM (the previous build targeted Gradio 3/4 `.gr-*`
classes that no longer exist, so almost nothing applied).
"""
import gradio as gr
from config import VOICE_CHOICES, DEFAULT_VOICE
THEMES = [
"brave adventure",
"making a new friend",
"overcoming a fear",
"helping someone",
"lost and found",
"learning something new",
]
# ---------------------------------------------------------------------------
# Fonts: injected via
(reliable) instead of CSS @import (often stripped).
# ---------------------------------------------------------------------------
HEAD = """
"""
# ---------------------------------------------------------------------------
# Hidden SVG defs: the hand-drawn "wobble" filter + a crayon underline squiggle.
# Applied to pseudo-element borders so text stays crisp while frames go wavy.
# ---------------------------------------------------------------------------
SVG_DEFS = """
"""
CSS = r"""
/* ============================================================================
DOODLEBOOK — CONSTRUCTION-PAPER SCRAPBOOK
============================================================================ */
:root {
--paper: #f6ecd4; /* warm construction-paper base */
--paper-2: #efe0c2;
--ink: #2e2a26; /* soft black, like a felt pen */
--ink-soft: #6b5d4f;
--crayon-orange: #ef6a3a;
--crayon-teal: #2ba39a;
--crayon-sun: #f4c64a;
--crayon-berry: #d6517a;
--crayon-sky: #4a9fd6;
--crayon-leaf: #74b85a;
--tape: rgba(244, 198, 74, 0.55);
}
/* --- Neutralize the Gradio theme so OUR styles win (was: Soft theme bleed) --- */
.gradio-container,
.gradio-container *:not(svg):not(path) {
--block-background-fill: transparent;
--block-border-width: 0px;
--block-shadow: none;
--panel-background-fill: transparent;
--input-background-fill: #fffdf6;
--body-text-color: var(--ink);
}
.gradio-container {
max-width: 1180px !important;
margin: 0 auto !important;
background: transparent !important;
font-family: 'Nunito', sans-serif !important;
color: var(--ink);
}
/* Paper-grain backdrop: layered fractal noise + soft color blooms */
body, gradio-app {
background-color: var(--paper) !important;
background-image:
radial-gradient(circle at 12% 18%, rgba(239,106,58,0.10), transparent 38%),
radial-gradient(circle at 88% 12%, rgba(43,163,154,0.10), transparent 40%),
radial-gradient(circle at 70% 88%, rgba(214,81,122,0.08), transparent 42%),
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%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.05'/%3E%3C/svg%3E") !important;
background-attachment: fixed !important;
}
/* Reusable hand-drawn frame: a wobbly inked border that doesn't disturb content */
.framed { position: relative; }
.framed::after {
content: "";
position: absolute; inset: 7px;
border: 2.5px solid var(--ink);
border-radius: 16px;
filter: url(#wobble);
pointer-events: none;
opacity: 0.85;
}
/* Washi-tape strip helper */
.taped::before {
content: "";
position: absolute; top: -12px; left: 50%;
width: 110px; height: 26px;
transform: translateX(-50%) rotate(-3deg);
background:
repeating-linear-gradient(45deg, rgba(255,255,255,.35) 0 6px, transparent 6px 12px),
var(--tape);
box-shadow: 0 2px 5px rgba(0,0,0,.12);
z-index: 3;
}
/* ============================== HEADER ============================== */
.app-header {
text-align: center;
padding: 34px 16px 10px;
position: relative;
}
.app-title {
font-family: 'Gaegu', cursive !important;
font-weight: 700 !important;
font-size: clamp(44px, 7vw, 78px) !important;
line-height: 0.95 !important;
color: var(--ink) !important;
margin: 0 !important;
letter-spacing: 1px;
text-shadow:
2px 2px 0 var(--crayon-sun),
4px 4px 0 rgba(239,106,58,.35);
}
.app-title .doodle-o { color: var(--crayon-orange); display: inline-block; transform: rotate(-6deg); }
.app-subtitle {
font-family: 'Caveat', cursive !important;
font-size: clamp(20px, 3vw, 28px) !important;
color: var(--ink-soft) !important;
margin-top: 6px !important;
}
/* hand-drawn crayon underline under the title */
.title-squiggle { display:block; margin: 6px auto 0; width: min(360px, 70%); height: 16px; }
.title-squiggle path {
stroke: var(--crayon-teal); stroke-width: 5; fill: none;
stroke-linecap: round; filter: url(#wobble-strong);
}
/* ============================== CARDS ============================== */
.input-card, .output-card {
position: relative;
background: #fffdf6 !important;
border-radius: 18px;
padding: 30px 26px 26px;
margin: 8px;
box-shadow: 0 10px 26px rgba(46,42,38,.12), 0 2px 0 rgba(0,0,0,.04);
}
.input-card { transform: rotate(-0.7deg); }
.output-card { transform: rotate(0.5deg); }
.input-card:hover, .output-card:hover { transform: rotate(0deg); transition: transform .35s ease; }
/* section labels above fields */
.card-eyebrow {
font-family: 'Gaegu', cursive; font-weight: 700;
font-size: 22px; color: var(--crayon-orange);
margin: 0 0 10px; transform: rotate(-1.5deg);
}
/* ============================== FIELDS ============================== */
.field label span, .doodle-input label span, .tiny-toggle label span {
font-family: 'Gaegu', cursive !important;
font-weight: 700 !important;
font-size: 19px !important;
color: var(--ink) !important;
}
.field textarea,
.field input[type="text"],
.field input:not([type]),
.field .wrap .secondary-wrap input,
.field [data-testid="textbox"],
.field input[role="listbox"],
.field .secondary-wrap,
.field [class*="dropdown"] input {
font-family: 'Nunito', sans-serif !important;
font-size: 17px !important;
color: var(--ink) !important;
background: #fffdf6 !important;
border: 2.5px solid var(--ink) !important;
border-radius: 12px !important;
padding: 11px 14px !important;
box-shadow: 2px 3px 0 rgba(46,42,38,.10) !important;
}
.field textarea:focus,
.field input:focus {
border-color: var(--crayon-orange) !important;
box-shadow: 2px 3px 0 rgba(239,106,58,.30) !important;
outline: none !important;
}
/* doodle image as a pinned polaroid */
.doodle-input { position: relative; }
.doodle-input .image-container,
.doodle-input [data-testid="image"] {
background: #ffffff !important;
border: none !important;
border-radius: 4px !important;
padding: 12px 12px 34px !important; /* polaroid bottom lip */
box-shadow: 0 8px 18px rgba(46,42,38,.18) !important;
transform: rotate(-2deg);
}
.doodle-input img { border-radius: 2px !important; }
.doodle-input .upload-container,
.doodle-input [data-testid="image"] .wrap {
border: 3px dashed var(--crayon-sky) !important;
border-radius: 8px !important;
background: #f3f9ff !important;
color: var(--ink-soft) !important;
}
/* ============================== BUTTONS ============================== */
/* elem_classes land on the