"""DiscoverRoute design system — ported from the Claude-design handoff kit. Source of truth: design_handoff_discoverroute (tokens.css / gradio-integration-kit). Low-poly "clay sticker" aesthetic: cream paper, cobalt/grass/coral/sun blocks, Fredoka display type, springy micro-interactions, framed map window. Everything here is presentation only — no behavior. The strings are consumed by app.py (theme/css/head/js) and ui/map.py (in-iframe animation script). """ from __future__ import annotations import gradio as gr # ---------------------------------------------------------------- theme (§1) def build_theme() -> gr.themes.Soft: return gr.themes.Soft( primary_hue=gr.themes.colors.blue, # cobalt — primary actions secondary_hue=gr.themes.colors.green, # grass — discovery route neutral_hue=gr.themes.colors.stone, # warm cream neutrals font=[gr.themes.GoogleFont("DM Sans"), "ui-sans-serif", "system-ui"], font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "monospace"], radius_size=gr.themes.sizes.radius_lg, spacing_size=gr.themes.sizes.spacing_lg, text_size=gr.themes.sizes.text_md, ).set( body_background_fill="#F6ECD9", body_text_color="#2B2620", background_fill_primary="#FFFCF5", border_color_primary="#E7DAC0", button_primary_background_fill="#FF6A52", button_primary_background_fill_hover="#FF5640", button_primary_text_color="#FFFFFF", slider_color="#2F5DF4", input_background_fill="#FFFEFB", input_border_color_focus="#2F5DF4", ) # ---------------------------------------------------------------- css (§2+§5) DR_CSS = """ /* ---- tokens ---- */ :root{ --dr-cream:#F6ECD9; --dr-paper:#FFFCF5; --dr-ink:#2B2620; --dr-soft:#6B6256; --dr-line:#E7DAC0; --dr-cobalt:#2F5DF4; --dr-cobalt-d:#214AD0; --dr-grass:#2FA463; --dr-grass-d:#1F7D49; --dr-coral:#FF6A52; --dr-coral-d:#E14D37; --dr-sun:#FFC247; --dr-r:18px; --dr-spring:cubic-bezier(.34,1.56,.64,1); } .gradio-container{ background: radial-gradient(1100px 520px at 88% -8%,#FBEFD6 0%,transparent 60%), var(--dr-cream) !important; } /* display font on labels + headings */ .gradio-container label span, .gradio-container h1, .gradio-container h2, .gradio-container h3, .dr-label{ font-family:'Fredoka',sans-serif !important; font-weight:600; letter-spacing:-.01em; } /* cards / blocks become tactile stickers */ .gradio-container .block, .gradio-container .form{ border-radius:26px !important; border:1px solid var(--dr-line) !important; box-shadow:0 18px 44px -20px rgba(43,38,32,.32) !important; background:var(--dr-paper) !important; } /* inputs — explicit ink color (theme override otherwise leaves them too pale) */ .gradio-container input[type=text], .gradio-container textarea{ border:2px solid var(--dr-line) !important; border-radius:var(--dr-r) !important; background:#FFFEFB !important; color:var(--dr-ink) !important; transition:border-color .18s, box-shadow .18s !important; } .gradio-container input[type=text]::placeholder, .gradio-container textarea::placeholder{ color:#B9AE99 !important; opacity:1; } .gradio-container input[type=text]:focus, .gradio-container textarea:focus{ border-color:var(--dr-cobalt) !important; box-shadow:0 0 0 4px rgba(47,93,244,.14) !important; } /* primary 'Plan route' — depresses like a real button */ #dr-plan button, .gradio-container button.primary{ font-family:'Fredoka',sans-serif !important; font-weight:600; font-size:18px !important; color:#fff !important; background:var(--dr-coral) !important; border:none !important; border-radius:var(--dr-r) !important; box-shadow:0 6px 0 var(--dr-coral-d), 0 14px 24px -10px rgba(255,106,82,.7) !important; transition:transform .12s, box-shadow .12s !important; } #dr-plan button:hover{ transform:translateY(-2px) !important; } #dr-plan button:active{ transform:translateY(4px) !important; box-shadow:0 2px 0 var(--dr-coral-d) !important; } /* mode toggle as a segmented control */ #dr-mode .wrap{ display:flex; align-items:center; background:#F0E3CC; padding:5px; border-radius:var(--dr-r); gap:6px; } #dr-mode label{ flex:1; justify-content:center; border:none !important; background:transparent !important; color:var(--dr-soft) !important; border-radius:13px !important; transition:all .2s var(--dr-spring); } #dr-mode label span{ color:inherit !important; } #dr-mode label.selected{ background:var(--dr-paper) !important; color:var(--dr-cobalt) !important; box-shadow:0 4px 12px -4px rgba(43,38,32,.25); transform:translateY(-1px); } /* springy sliders — per-slider accents (budget coral · adventurousness sun · green/quiet grass), per components.md §5 */ .gradio-container input[type=range]::-webkit-slider-thumb{ width:26px;height:26px;border-radius:50%;background:#fff;border:4px solid var(--dr-cobalt); box-shadow:0 4px 10px -2px rgba(43,38,32,.4); transition:transform .15s var(--dr-spring); } .gradio-container input[type=range]:active::-webkit-slider-thumb{ transform:scale(1.22); } .gradio-container input[type=range]{ accent-color:var(--dr-cobalt); } #dr-budget input[type=range]{ accent-color:var(--dr-coral); } #dr-budget input[type=range]::-webkit-slider-thumb{ border-color:var(--dr-coral); } #dr-adv input[type=range]{ accent-color:var(--dr-sun); } #dr-adv input[type=range]::-webkit-slider-thumb{ border-color:var(--dr-sun); } #dr-green input[type=range], #dr-quiet input[type=range]{ accent-color:var(--dr-grass); } /* collapsibles -> dashed taste cards (labels at full ink for readability) */ .dr-collapse{ border:1.5px dashed var(--dr-line) !important; border-radius:var(--dr-r) !important; background:#FFFDF8 !important; box-shadow:none !important; } .dr-collapse .label-wrap span, .dr-collapse button span, .dr-collapse > button{ color:var(--dr-ink) !important; opacity:1 !important; } /* route-options radio -> selectable cards (paper bg + ink text, both states) */ #dr-options .wrap{ display:grid; grid-template-columns:repeat(3,1fr); gap:11px; } #dr-options label{ border:2px solid var(--dr-line) !important; border-radius:var(--dr-r) !important; padding:14px !important; background:var(--dr-paper) !important; color:var(--dr-ink) !important; transition:all .2s var(--dr-spring); } #dr-options label span, #dr-options label *{ color:var(--dr-ink) !important; } #dr-options label:hover{ transform:translateY(-3px); } #dr-options label.selected{ border-color:var(--dr-grass) !important; background:#F1FAF4 !important; box-shadow:0 10px 26px -14px rgba(47,164,99,.6) !important; } /* the Leaflet iframe -> a framed 'window' with a titlebar */ #dr-map{ border-radius:26px !important; overflow:hidden; border:1px solid var(--dr-line); box-shadow:0 18px 44px -20px rgba(43,38,32,.34); position:relative; background:var(--dr-paper); } #dr-map::before{ content:'Paris — live map'; display:block; font-family:'Fredoka',sans-serif; font-weight:600; font-size:13.5px; padding:11px 16px 11px 64px; color:var(--dr-ink); border-bottom:1px solid var(--dr-line); background-image:radial-gradient(circle at 20px 50%,#FF6A52 5px,transparent 5px), radial-gradient(circle at 36px 50%,#FFC247 5px,transparent 5px), radial-gradient(circle at 52px 50%,#2FA463 5px,transparent 5px), linear-gradient(180deg,#FFF,#FBF4E6); } #dr-map iframe{ height:520px !important; border:none !important; display:block; width:100%; } /* summary banner */ #dr-summary{ background:linear-gradient(110deg,#2FA463,#37B06E) !important; color:#fff !important; border-radius:var(--dr-r) !important; box-shadow:0 12px 26px -14px rgba(47,164,99,.8) !important; padding:14px 18px !important; } #dr-summary *{ color:#fff !important; } /* interpretation card */ #dr-interp{ background:var(--dr-paper) !important; border-radius:var(--dr-r) !important; padding:13px 18px !important; } /* narrated itinerary -> numbered steps on a dashed timeline rail (components.md §13) */ #dr-itin{ background:var(--dr-paper) !important; border-radius:var(--dr-r) !important; padding:13px 18px !important; } #dr-itin ol{ list-style:none; counter-reset:dr-step; margin:0; padding:0; position:relative; } #dr-itin ol::before{ content:''; position:absolute; left:18px; top:10px; bottom:10px; width:2.5px; background:repeating-linear-gradient(180deg,var(--dr-line) 0 5px,transparent 5px 11px); } #dr-itin ol > li{ counter-increment:dr-step; position:relative; z-index:1; padding:13px 0 13px 52px; border-bottom:1.5px dashed var(--dr-line); transition:transform .2s var(--dr-spring); } #dr-itin ol > li:last-child{ border-bottom:none; } #dr-itin ol > li:hover{ transform:translateX(5px); } #dr-itin ol > li::before{ content:counter(dr-step); position:absolute; left:0; top:10px; width:36px; height:36px; border-radius:12px; display:grid; place-items:center; font-family:'Fredoka',sans-serif; font-weight:700; font-size:16px; color:#fff; background:var(--dr-grass); box-shadow:0 4px 0 var(--dr-grass-d); transition:transform .2s var(--dr-spring); } #dr-itin ol > li:nth-child(3n+2)::before{ background:var(--dr-coral); box-shadow:0 4px 0 var(--dr-coral-d); } #dr-itin ol > li:nth-child(3n)::before{ background:var(--dr-sun); box-shadow:0 4px 0 #E89E1C; } #dr-itin ol > li:hover::before{ transform:translateY(-3px) rotate(-4deg); } /* hero (gr.HTML wrapper sticker) */ #dr-hero{ background:transparent !important; border:none !important; box-shadow:none !important; } /* hide gradio footer */ footer{ visibility:hidden; } /* ---- responsive ---- */ @media (max-width: 860px){ #dr-map iframe{ height: 440px !important; } #dr-options .wrap{ grid-template-columns: 1fr !important; } #dr-results{ margin-top: 14px; } } @media (max-width: 560px){ .gradio-container{ padding: 12px !important; } .gradio-container h1{ font-size: 27px !important; } #dr-map::before{ font-size:12px; background-image:linear-gradient(180deg,#FFF,#FBF4E6); } } /* dark-mode belt-and-suspenders: even if the dark class sticks, keep the design's ink-on-cream readable (the head script also strips the class) */ .dark .gradio-container, .dark .gradio-container .prose, .dark .gradio-container .prose *, .dark .gradio-container label span, .dark .gradio-container p, .dark .gradio-container li{ color:var(--dr-ink) !important; } /* respect reduced motion */ @media (prefers-reduced-motion:reduce){ .gradio-container *{ animation:none !important; transition:none !important; } } /* AA focus rings everywhere */ .gradio-container *:focus-visible{ outline:3px solid var(--dr-cobalt) !important; outline-offset:3px; } """ # ---------------------------------------------------------------- head (§4) # Includes the page enhancer as a real """ # ---------------------------------------------------------------- js (§4/§6) # Outer-page enhancer: bounce results in when they (re)appear. The map's own # route-draw / marker-pop animations run INSIDE the folium iframe (ui/map.py # injects MAP_ANIMATION_JS there — an iframe can't be reached reliably from here). # NOTE: the page enhancer (force-light + entrance animation) lives in DR_HEAD as # a real """ # ---------------------------------------------------------------- hero (§A) # Inline-SVG low-poly isometric placeholder (final clay renders swap in later). _HERO_SVG = """ """ DR_HERO = f"""
""" # ------------------------------------------------------ no-detour state (§C) NO_DETOUR_HTML = """The budget is too tight to carve a worthwhile detour. Loosen it (or raise adventurousness) and I'll thread you past something good.