Spaces:
Running on Zero
Running on Zero
BhargavMN
style(theme): make Celebrate card background transparent for improved visual consistency
5180cb5 | """Liquid-flow visual theme for CityQuest-AI. | |
| A minimalist pale/beige palette with a slowly drifting "water" gradient, | |
| soft pill buttons with a tap ripple, rounded translucent cards, clear sans | |
| typography, and hover tooltips injected onto every ``elem_id``'d control. | |
| Exports ``CSS`` (stylesheet) and ``JS_INIT`` (tooltip + ripple bootstrap), | |
| plus :func:`gradio_theme` for the base Gradio theme object. | |
| """ | |
| import gradio as gr | |
| # ββ Tooltip copy keyed by elem_id (applied via JS) βββββββββββββββββββββββββββ | |
| TOOLTIPS = { | |
| "home-create": "Start a new adventure and become the host", | |
| "home-join": "Enter a room code to join a friend's adventure", | |
| "create-submit": "Create the room and open the lobby", | |
| "create-back": "Back to the start", | |
| "join-submit": "Join this room", | |
| "join-back": "Back to the start", | |
| "lobby-start": "Generate tasks and send every team into play", | |
| "lobby-leave": "Leave this room", | |
| "td-transcribe": "Transcribe your recording with Cohere ASR", | |
| "td-savejournal": "Save this voice/typed note β you can add several per task", | |
| "td-addphoto": "Attach this photo with its tagline (add as many as you like)", | |
| "td-ask": "Ask the in-game guide about this task or place", | |
| "td-complete": "Mark this task done β needs a photo or a journal first", | |
| "td-prev": "Previous task", | |
| "td-next": "Next task", | |
| "td-finish": "Finish and wait for the other teams", | |
| "play-finish": "Finish and wait for the other teams", | |
| "wait-funny-recap": "Make a light, funny recap of your run so far", | |
| "wait-funny-post": "Make a playful poster from your photos", | |
| "win-recap": "Generate the winning episode recap", | |
| "win-endpost": "Generate an ending poster", | |
| "win-grouppost": "Generate a group celebration poster", | |
| "win-new": "Start a brand new adventure", | |
| } | |
| CSS = """ | |
| @import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,600;9..144,700&family=Inter:wght@400;500;600;700&display=swap'); | |
| :root { | |
| /* ββ High-contrast dark theme (WCAG AAA, white-on-dark) ββ | |
| Raw palette tokens (bright accents, used directly by some rules) β¦ */ | |
| --green: #4CAF50; /* bright green β primary buttons / success */ | |
| --green-h: #45A049; /* green hover */ | |
| --green-a: #3D8B40; /* green active/pressed */ | |
| --blue: #2196F3; /* bright blue β interactive */ | |
| --purple: #9C27B0; /* bright purple β CTAs */ | |
| --yellow: #FFC107; /* bright amber β warnings / highlights */ | |
| --red: #F44336; /* bright red β errors */ | |
| /* β¦ and aliases onto the names the rest of the stylesheet + app.py use, | |
| so the whole UI re-skins from this one block. */ | |
| --sand: #1A1A1A; /* page base β darkest */ | |
| --sand-2: #222222; | |
| --paper: #2A2A2A; /* card surface */ | |
| --paper-2: #3A3A3A; /* inputs / soft surfaces / code */ | |
| --line: #4A4A4A; /* borders β visible on dark */ | |
| --clay: #FFB74D; /* warm amber accent */ | |
| --clay-dk: #FFC107; /* bright amber β used as accent text */ | |
| --teal: #4CAF50; /* bright green accent (fills + borders) */ | |
| --teal-dk: #66BB6A; /* lighter green β accent text on dark cards */ | |
| --ink: #FFFFFF; /* primary text β white, no exceptions */ | |
| --muted: #CCCCCC; /* secondary text β light grey */ | |
| --good: #4CAF50; | |
| --radius: 18px; | |
| } | |
| /* Force our palette even if Gradio applies its dark class (we also force | |
| light via the URL param in JS, this is a belt-and-braces override). */ | |
| .dark { --body-text-color: var(--ink); --body-background-fill: var(--sand); } | |
| /* ββ Flowing water background ββ */ | |
| body, .gradio-container, .dark .gradio-container { | |
| background: var(--sand) !important; | |
| color: var(--ink) !important; | |
| font-family: 'Inter', system-ui, sans-serif !important; | |
| } | |
| .gradio-container::before { | |
| content: ''; | |
| position: fixed; inset: -40%; | |
| z-index: -1; | |
| background: | |
| radial-gradient(40% 55% at 20% 30%, rgba(76,175,80,0.12), transparent 60%), /* green glow */ | |
| radial-gradient(45% 50% at 80% 25%, rgba(33,150,243,0.12), transparent 60%), /* blue glow */ | |
| radial-gradient(50% 60% at 60% 85%, rgba(156,39,176,0.10), transparent 60%), /* purple glow */ | |
| radial-gradient(45% 55% at 15% 80%, rgba(255,193,7,0.07), transparent 60%); /* amber glow */ | |
| background-size: 130% 130%; | |
| animation: drift 26s ease-in-out infinite alternate; | |
| filter: blur(8px); | |
| pointer-events: none; | |
| } | |
| @keyframes drift { | |
| 0% { background-position: 30% 35%; } | |
| 50% { background-position: 60% 50%; } | |
| 100% { background-position: 45% 65%; } | |
| } | |
| /* Make every full-screen wrapper transparent so the drifting aura gradient | |
| (.gradio-container::before) shows through. <body> stays the dark base. */ | |
| gradio-app, | |
| .gradio-container, .dark .gradio-container, | |
| .gradio-container > .main, | |
| .gradio-container .wrap, | |
| .gradio-container .contain { | |
| background: transparent !important; | |
| } | |
| footer { display: none !important; } | |
| /* ββ Screen fade/slide transitions ββ */ | |
| .cq-screen { animation: flow-in .55s cubic-bezier(.22,.61,.36,1); } | |
| @keyframes flow-in { | |
| from { opacity: 0; transform: translateY(14px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| /* ββ Cards ββ */ | |
| .cq-card { | |
| background: rgba(42,42,42,0.92) !important; | |
| border: 1px solid var(--line) !important; | |
| border-radius: var(--radius) !important; | |
| padding: 26px !important; | |
| backdrop-filter: blur(6px); | |
| box-shadow: 0 10px 40px -20px rgba(0,0,0,0.6); | |
| } | |
| .cq-soft { background: var(--paper-2) !important; border:1px solid var(--line) !important; | |
| border-radius: var(--radius) !important; padding: 18px !important; } | |
| /* ββ Landing page refinements (scoped to home screen only) ββ */ | |
| /* Remove the grey rectangle/background around the whole home screen */ | |
| .cq-home, .cq-home > .block, .cq-home > div { | |
| background: transparent !important; | |
| border: none !important; | |
| box-shadow: none !important; | |
| } | |
| /* Bigger title "logo" on the landing screen */ | |
| .cq-home .cq-title { | |
| font-size: clamp(2.6rem, 7vw, 4.2rem) !important; | |
| } | |
| /* CHANGE 1: Remove borders / box outlines from the home cards so they float | |
| cleanly against the background β other screens keep their .cq-card chrome. */ | |
| .cq-home-card, | |
| .cq-home-card > .block, | |
| .cq-home-card > div, | |
| .cq-home-card .form, | |
| .cq-home .gr-column, | |
| .cq-home .gr-column > .block { | |
| border: none !important; | |
| box-shadow: none !important; | |
| outline: none !important; | |
| background: transparent !important; | |
| backdrop-filter: none !important; | |
| } | |
| /* CHANGE 3B: Centered, larger description above each button */ | |
| .cq-home .landing-description { | |
| text-align: center !important; | |
| font-size: 1.1rem !important; | |
| color: #CCCCCC !important; | |
| margin-bottom: 16px !important; | |
| line-height: 1.5 !important; | |
| } | |
| /* CHANGE 3A: Bigger, bolder full-width buttons. CHANGE 4: hover/active animation */ | |
| .cq-home .landing-btn { | |
| width: 100% !important; | |
| padding: 20px 32px !important; | |
| font-size: 1.2rem !important; | |
| font-weight: 700 !important; | |
| border-radius: 16px !important; | |
| letter-spacing: 0.5px !important; | |
| min-height: 64px !important; | |
| transition: transform 0.2s ease, box-shadow 0.2s ease, filter 0.2s ease !important; | |
| cursor: pointer !important; | |
| } | |
| /* Make both landing buttons the same colour (green) */ | |
| .cq-home .landing-btn.secondary { | |
| background: var(--green) !important; | |
| border: none !important; | |
| color: #000000 !important; | |
| box-shadow: 0 8px 24px -14px rgba(76,175,80,0.8) !important; | |
| } | |
| .cq-home .landing-btn.secondary:hover { | |
| background: var(--green-h) !important; | |
| border: none !important; | |
| color: #000000 !important; | |
| } | |
| .cq-home .landing-btn:hover { | |
| transform: scale(1.03) !important; | |
| filter: brightness(1.1) !important; | |
| box-shadow: 0 8px 24px rgba(76,175,80,0.4) !important; | |
| } | |
| .cq-home .landing-btn:active { | |
| transform: scale(0.98) !important; | |
| filter: brightness(0.95) !important; | |
| } | |
| /* ββ Create screen refinements (scoped to .cq-create only) ββ */ | |
| /* Remove the outer grey rectangle around the whole screen. Each screen's own | |
| gr.Group (.cq-screen) renders Gradio's default card chrome; strip it so the | |
| inner .cq-card floats against the animated background. Scoped to the group's | |
| own wrapper layers so the inner .cq-card keeps its border/fill. Applies to | |
| every screen (create, join, lobby, play, wait, win). */ | |
| .cq-screen, | |
| .cq-screen > .block, | |
| .cq-screen > div:not(.cq-card), | |
| .cq-screen > .block > div:not(.cq-card) { | |
| background: transparent !important; | |
| border: none !important; | |
| box-shadow: none !important; | |
| backdrop-filter: none !important; | |
| } | |
| /* CHANGE 1: Give the dropdown fields (game type, difficulty, age group, energy) | |
| the same grey opaque input box as the textbox fields. Gradio renders a | |
| dropdown's display element as .wrap (not a typed <input>), so it never picked | |
| up the input fill β this makes them visually match and read as editable. */ | |
| .cq-create .cq-dropdown .wrap { | |
| background: var(--paper-2) !important; | |
| border: 1px solid var(--line) !important; | |
| border-radius: 12px !important; | |
| color: var(--ink) !important; | |
| } | |
| /* CHANGE 4: Center the live game-parameter summary bar */ | |
| .cq-create .game-summary, | |
| .cq-create .game-summary .cq-soft { | |
| text-align: center !important; | |
| width: 100% !important; | |
| letter-spacing: 0.5px !important; | |
| } | |
| .cq-create .game-summary .cq-soft { | |
| border: none !important; | |
| background: transparent !important; | |
| } | |
| /* CHANGE 5: Bigger buttons with hover/active animation (matches landing page) */ | |
| .cq-create .create-btn { | |
| transition: transform 0.2s ease, box-shadow 0.2s ease, filter 0.2s ease !important; | |
| cursor: pointer !important; | |
| padding: 18px 32px !important; | |
| font-size: 1.1rem !important; | |
| font-weight: 700 !important; | |
| border-radius: 12px !important; | |
| min-height: 60px !important; | |
| } | |
| .cq-create .open-lobby-btn:hover { | |
| transform: scale(1.03) !important; | |
| box-shadow: 0 8px 24px rgba(76,175,80,0.4) !important; | |
| filter: brightness(1.1) !important; | |
| } | |
| .cq-create .back-btn:hover { | |
| transform: scale(1.03) !important; | |
| box-shadow: 0 8px 24px rgba(255,255,255,0.1) !important; | |
| filter: brightness(1.15) !important; | |
| } | |
| .cq-create .create-btn:active { | |
| transform: scale(0.98) !important; | |
| filter: brightness(0.95) !important; | |
| } | |
| /* ββ Win screen: black-card reskin + symmetric columns βββββββββββ */ | |
| /* The Celebrate card overrides the default .cq-card grey fill with | |
| pure black and a stronger rounded border, scoped to .cq-win only. */ | |
| .cq-win .cq-win-celebrate.cq-card { | |
| background: #0A0A0A !important; | |
| border: 1px solid rgba(255,255,255,0.08) !important; | |
| border-radius: 20px !important; | |
| padding: 28px !important; | |
| backdrop-filter: none !important; | |
| box-shadow: 0 16px 48px -24px rgba(0,0,0,0.8) !important; | |
| } | |
| /* Strip the default grey block/form fills from inner wrappers inside the | |
| Celebrate card so only the black card shows through. Buttons (<button>) | |
| and the poster drop-zone (.image-container) keep their own styling. */ | |
| .cq-win .cq-win-celebrate > div, | |
| .cq-win .cq-win-celebrate .form, | |
| .cq-win .cq-win-celebrate .block:not(.image-container) { | |
| background: transparent !important; | |
| border: none !important; | |
| box-shadow: none !important; | |
| } | |
| /* Final Standings column gets a matching black card so the two | |
| columns have equal visual weight. */ | |
| .cq-win .cq-win-standings { | |
| background: #0A0A0A !important; | |
| border: 1px solid rgba(255,255,255,0.08) !important; | |
| border-radius: 20px !important; | |
| padding: 28px !important; | |
| box-shadow: 0 16px 48px -24px rgba(0,0,0,0.8) !important; | |
| margin-bottom: 24px !important; | |
| } | |
| /* Strip any inherited chrome from inner blocks inside the standings | |
| card so the markdown sits flush. */ | |
| .cq-win .cq-win-standings > div, | |
| .cq-win .cq-win-standings .block { | |
| background: transparent !important; | |
| border: none !important; | |
| box-shadow: none !important; | |
| padding: 0 !important; | |
| } | |
| /* Win-screen buttons: match landing-page scale+brightness animation. */ | |
| .cq-win .cq-win-btn { | |
| transition: transform 0.2s ease, box-shadow 0.2s ease, | |
| filter 0.2s ease !important; | |
| cursor: pointer !important; | |
| } | |
| .cq-win .cq-win-btn:hover { | |
| transform: scale(1.03) !important; | |
| filter: brightness(1.1) !important; | |
| } | |
| .cq-win .cq-win-btn:active { | |
| transform: scale(0.98) !important; | |
| filter: brightness(0.95) !important; | |
| } | |
| /* Start-a-new-adventure: full-width primary CTA with green accent so | |
| it reads as a deliberate next-step, not an orphaned secondary. */ | |
| .cq-win .cq-win-new { | |
| width: 100% !important; | |
| margin-top: 24px !important; | |
| padding: 18px 32px !important; | |
| font-size: 1.1rem !important; | |
| font-weight: 700 !important; | |
| border-radius: 14px !important; | |
| background: var(--green) !important; | |
| color: #000 !important; | |
| border: none !important; | |
| box-shadow: 0 8px 24px -14px rgba(76,175,80,0.8) !important; | |
| } | |
| /* Recap markdown breathing room: add spacing between header hierarchy | |
| so the long recap doesn't read as a wall. */ | |
| .cq-win .cq-win-celebrate h1, | |
| .cq-win .cq-win-celebrate h2, | |
| .cq-win .cq-win-celebrate h3 { | |
| margin-top: 1.6em !important; | |
| margin-bottom: 0.6em !important; | |
| } | |
| .cq-win .cq-win-celebrate h1:first-child, | |
| .cq-win .cq-win-celebrate h2:first-child, | |
| .cq-win .cq-win-celebrate h3:first-child { | |
| margin-top: 0 !important; | |
| } | |
| /* Poster empty-state: soft dashed drop-zone instead of debug-looking | |
| default placeholder. */ | |
| .cq-poster .image-container:not(:has(img)), | |
| .cq-win .cq-win-celebrate .image-container:not(:has(img)) { | |
| background: rgba(255,255,255,0.02) !important; | |
| border: 1.5px dashed rgba(255,255,255,0.12) !important; | |
| border-radius: 14px !important; | |
| min-height: 240px !important; | |
| position: relative !important; | |
| } | |
| .cq-poster .image-container:not(:has(img))::after, | |
| .cq-win .cq-win-celebrate .image-container:not(:has(img))::after { | |
| content: 'Poster will appear here' !important; | |
| position: absolute !important; | |
| top: 50% !important; left: 50% !important; | |
| transform: translate(-50%, -50%) !important; | |
| color: var(--muted) !important; | |
| font-size: 0.9rem !important; | |
| text-align: center !important; | |
| pointer-events: none !important; | |
| } | |
| /* ββ Headings ββ */ | |
| .cq-title { | |
| font-family: 'Fraunces', serif !important; | |
| font-weight: 600 !important; | |
| color: var(--ink) !important; | |
| letter-spacing: -0.01em !important; | |
| } | |
| .gr-markdown h1, .gr-markdown h2, .gr-markdown h3 { | |
| font-family: 'Fraunces', serif !important; color: var(--ink) !important; | |
| font-weight: 600 !important; | |
| } | |
| .gr-markdown { color: var(--ink) !important; line-height: 1.7 !important; } | |
| .gr-markdown code { | |
| background: var(--paper-2) !important; color: var(--clay-dk) !important; | |
| border-radius: 6px !important; padding: 2px 7px !important; | |
| } | |
| .section-label { | |
| font-size: 11px !important; font-weight: 700 !important; | |
| letter-spacing: 0.14em !important; text-transform: uppercase !important; | |
| color: var(--muted) !important; margin: 6px 0 10px !important; | |
| display: flex; align-items: center; gap: 10px; | |
| } | |
| .section-label::after { content:''; flex:1; height:1px; background: var(--line); } | |
| /* ββ Inputs ββ */ | |
| input[type="text"], input[type="number"], textarea, select, .gr-input { | |
| background: var(--paper-2) !important; /* dark input fields */ | |
| border: 1px solid var(--line) !important; | |
| border-radius: 12px !important; | |
| color: var(--ink) !important; | |
| font-family: 'Inter', sans-serif !important; | |
| } | |
| input:focus, textarea:focus, select:focus { | |
| border-color: var(--teal) !important; outline: none !important; | |
| box-shadow: 0 0 0 4px rgba(76,175,80,0.35) !important; | |
| } | |
| label span, .label-wrap span { | |
| font-size: 11px !important; font-weight: 700 !important; | |
| letter-spacing: 0.1em !important; text-transform: uppercase !important; | |
| color: var(--ink) !important; | |
| } | |
| /* ββ Dropdown menu positioning fix ββ | |
| Cards use backdrop-filter (see .cq-card), which creates a containing block | |
| that clips the dropdown's default position:fixed menu β so the game-type | |
| (and other) dropdown menus rendered off-position / overlapped neighbouring | |
| fields. Render the listbox in-flow (static) directly under its input so it | |
| can't be clipped, sits full-width below the field, and never overlaps. */ | |
| .cq-card, .cq-screen { overflow: visible !important; } | |
| ul.options[role="listbox"] { | |
| position: static !important; | |
| width: 100% !important; | |
| max-height: 260px !important; | |
| overflow-y: auto !important; | |
| margin-top: 4px !important; | |
| z-index: 50 !important; | |
| background: var(--paper) !important; | |
| border: 1px solid var(--line) !important; | |
| border-radius: 12px !important; | |
| box-shadow: 0 12px 28px -16px rgba(62,58,51,0.5) !important; | |
| } | |
| ul.options[role="listbox"] li { | |
| color: var(--ink) !important; | |
| border-radius: 8px !important; | |
| } | |
| /* ββ Buttons: soft water pills ββ */ | |
| .gr-button, button.lg, button.sm { | |
| font-family: 'Inter', sans-serif !important; | |
| font-weight: 600 !important; | |
| border-radius: 999px !important; | |
| transition: transform .18s cubic-bezier(.22,.61,.36,1), box-shadow .2s, background .2s !important; | |
| position: relative; overflow: hidden; | |
| } | |
| .gr-button.primary { | |
| background: var(--green) !important; /* bright green */ | |
| color: #000000 !important; /* black text β 5.2:1 on green */ | |
| border: none !important; | |
| box-shadow: 0 8px 24px -14px rgba(76,175,80,0.8) !important; | |
| } | |
| .gr-button.primary:hover { background: var(--green-h) !important; | |
| color: #000000 !important; transform: translateY(-2px) !important; | |
| box-shadow: 0 14px 30px -14px rgba(76,175,80,0.65) !important; } | |
| .gr-button.primary:active { background: var(--green-a) !important; } | |
| .gr-button.secondary { | |
| background: var(--paper-2) !important; | |
| border: 2px solid var(--green) !important; color: var(--ink) !important; /* white text */ | |
| font-weight: 600 !important; | |
| } | |
| .gr-button.secondary:hover { background: #4A4A4A !important; | |
| border-color: var(--green-h) !important; color: var(--ink) !important; | |
| transform: translateY(-1px) !important; box-shadow: 0 8px 20px -16px rgba(76,175,80,0.6) !important; } | |
| .gr-button[disabled], .gr-button.disabled { opacity: .5 !important; filter: grayscale(.25); | |
| cursor: not-allowed !important; transform: none !important; } | |
| /* Wait/win-screen secondaries: kill Gradio's default purple gradient and | |
| match the themed grey-outline style of .gr-button.secondary. */ | |
| #wait-funny-recap button, #wait-funny-post button, | |
| #win-endpost button, #win-grouppost button { | |
| background: var(--paper-2) !important; | |
| border: 2px solid var(--green) !important; | |
| color: var(--ink) !important; | |
| background-image: none !important; | |
| } | |
| /* ββ Force readable text everywhere (beats Gradio theme defaults) ββ */ | |
| .gradio-container, .gr-markdown, .gr-markdown p, .gr-markdown li, | |
| .prose, .prose p, .prose li, p, span:not(.cq-pill):not([class*='cq-']) { | |
| color: var(--ink); | |
| } | |
| .gr-markdown strong, .gr-markdown b { color: var(--ink) !important; } | |
| .gr-check-radio label, .wrap label { color: var(--ink) !important; } | |
| /* tap ripple */ | |
| .cq-ripple { position:absolute; border-radius:50%; transform: scale(0); | |
| background: rgba(255,255,255,0.5); animation: ripple .6s ease-out; pointer-events:none; } | |
| @keyframes ripple { to { transform: scale(3.2); opacity: 0; } } | |
| /* ββ Big code chip ββ */ | |
| .cq-code { | |
| font-family: 'Fraunces', serif; font-weight: 700; | |
| letter-spacing: 0.32em; font-size: clamp(2rem, 7vw, 3.4rem); | |
| color: var(--clay-dk); background: var(--paper); | |
| border: 1px dashed var(--clay); border-radius: var(--radius); | |
| padding: 14px 26px; display: inline-block; | |
| } | |
| /* ββ Lobby: room-code hint (replaces the redundant "SHAREβ¦" label) ββ */ | |
| .cq-code-hint { | |
| text-align: center; color: var(--muted); | |
| font-size: 0.95rem; margin-bottom: 10px; | |
| } | |
| /* ββ Lobby: copy-to-clipboard button beside the room code ββ */ | |
| .cq-copy-wrap { display: flex; justify-content: center; margin-top: 12px; } | |
| .cq-copy-btn { | |
| background: var(--paper-2); color: var(--ink); | |
| border: 1px solid var(--line); border-radius: 999px; | |
| padding: 8px 20px; font-size: 0.95rem; font-family: 'Inter', sans-serif; | |
| font-weight: 600; cursor: pointer; transition: all 0.2s ease; | |
| } | |
| .cq-copy-btn:hover { | |
| background: #4A4A4A; border-color: var(--green); transform: scale(1.03); | |
| } | |
| .cq-copy-btn.copied { | |
| background: rgba(76,175,80,0.22); border-color: var(--green); color: var(--ink); | |
| transform: scale(1.0); | |
| } | |
| /* ββ Lobby: team-pick buttons β selected state ββ */ | |
| .team-pick.selected button { | |
| background: rgba(76,175,80,0.20) !important; | |
| border: 2px solid var(--green) !important; | |
| color: var(--ink) !important; font-weight: 700 !important; | |
| box-shadow: 0 0 12px rgba(76,175,80,0.3) !important; | |
| } | |
| /* ββ Lobby: "N / M teams ready" progress bar ββ */ | |
| .cq-progress { margin: 2px 0 6px 0; } | |
| .cq-progress-label { | |
| font-size: 0.85rem; color: var(--muted); margin-bottom: 6px; display: block; | |
| } | |
| .cq-progress-track { | |
| height: 6px; background: var(--paper-2); border-radius: 3px; overflow: hidden; | |
| } | |
| .cq-progress-fill { | |
| height: 100%; background: var(--green); border-radius: 3px; | |
| transition: width 0.4s ease; | |
| } | |
| /* ββ Lobby: center the prep status pill (loading / ready) ββ */ | |
| .lobby-prep { text-align: center; } | |
| /* ββ Lobby: host/guest instruction line ββ */ | |
| .lobby-hint, .lobby-hint p { | |
| text-align: center; color: var(--clay-dk) !important; | |
| font-size: 1rem !important; font-weight: 500 !important; | |
| background: rgba(255,193,7,0.08); border-radius: 8px; | |
| padding: 8px 16px; margin: 8px 0 !important; | |
| } | |
| /* ββ Task wall ββ */ | |
| .cq-wall { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px,1fr)); gap: 14px; } | |
| .cq-tcard { | |
| background: var(--paper); border:1px solid var(--line); border-radius: 16px; | |
| padding: 16px; transition: transform .18s, box-shadow .2s; cursor: pointer; | |
| } | |
| .cq-tcard:hover { transform: translateY(-3px); box-shadow: 0 12px 28px -18px rgba(62,58,51,.5); } | |
| .cq-tcard.done { background: linear-gradient(135deg, rgba(110,157,144,.16), var(--paper)); border-color: var(--teal); } | |
| .cq-check { float:right; font-size: 18px; } | |
| .cq-pts { color: var(--clay-dk); font-weight: 700; font-size: 12px; } | |
| /* ββ Status / timer strip ββ */ | |
| .cq-strip { display:flex; gap:10px; flex-wrap:wrap; } | |
| .cq-pill { | |
| background: var(--paper); border:1px solid var(--line); border-radius:999px; | |
| padding: 6px 14px; font-size: 13px; color: var(--ink); | |
| } | |
| /* Pills ease their colour/state changes so polled updates glide instead of | |
| snapping. A CSS transition (not an entry keyframe) is safe to apply to | |
| fragments that re-render every poll tick β it won't re-trigger/flicker. */ | |
| .cq-pill { transition: background .35s ease, border-color .35s ease, color .35s ease; } | |
| .cq-pill.live { border-color: var(--teal); color: var(--teal-dk); } | |
| .cq-pill.wait { border-color: var(--clay); color: var(--clay-dk); | |
| background: rgba(255,193,7,0.15); } | |
| .cq-pill.done { background: var(--teal); color:#0A1F0A; border-color: var(--teal); } | |
| .cq-pill.alert { border-color: var(--red); color: #FF8A80; background: rgba(244,67,54,0.18); } | |
| .cq-timer { font-family:'Fraunces',serif; font-weight:700; font-size: clamp(1.6rem,5vw,2.4rem); | |
| color: var(--teal-dk); transition: color .35s ease; } | |
| /* ββ Reveal animation for on-demand panels (task detail, etc.) ββ | |
| Only applied to elements that toggle on user action β NOT to polled | |
| fragments β so the keyframe never re-fires on a refresh tick. */ | |
| .cq-reveal { animation: cq-reveal .42s cubic-bezier(.22,.61,.36,1); } | |
| @keyframes cq-reveal { | |
| from { opacity: 0; transform: translateY(10px) scale(.99); } | |
| to { opacity: 1; transform: translateY(0) scale(1); } | |
| } | |
| /* Gentle fade for hint/safety blockquotes inside a revealed detail panel. */ | |
| .cq-reveal blockquote { animation: cq-fadein .6s ease both; animation-delay: .1s; } | |
| @keyframes cq-fadein { from { opacity: 0; } to { opacity: 1; } } | |
| /* ββ Loading / buffer states ββ */ | |
| .cq-loading { | |
| display: inline-flex; align-items: center; gap: 10px; | |
| color: var(--teal-dk); font-weight: 600; font-family: 'Inter', sans-serif; | |
| background: rgba(110,157,144,0.12); border: 1px solid #CFE0D9; | |
| border-radius: 999px; padding: 8px 16px; margin: 6px 0; | |
| animation: cq-fade 1.5s ease-in-out infinite; | |
| } | |
| .cq-loading::before { | |
| content: ''; width: 14px; height: 14px; border-radius: 50%; | |
| border: 2.5px solid #CFE0D9; border-top-color: var(--teal-dk); | |
| display: inline-block; animation: cq-spin .8s linear infinite; | |
| } | |
| @keyframes cq-spin { to { transform: rotate(360deg); } } | |
| @keyframes cq-fade { 0%,100% { opacity: .6; } 50% { opacity: 1; } } | |
| .cq-ready { | |
| display: inline-flex; align-items: center; gap: 8px; margin: 6px 0; | |
| color: var(--teal-dk); font-weight: 600; font-family: 'Inter', sans-serif; | |
| background: rgba(110,157,144,0.16); border: 1px solid var(--teal); | |
| border-radius: 999px; padding: 8px 16px; | |
| } | |
| /* Gradio's own progress bar, tinted to the palette */ | |
| .progress-bar, .progress-level-inner { background: var(--teal) !important; color: var(--teal-dk) !important; } | |
| .gallery-item, .image-frame { background: var(--paper-2) !important; } | |
| /* ββ Tooltips (data-tip injected by JS) ββ */ | |
| [data-tip] { position: relative; } | |
| [data-tip]:hover::after { | |
| content: attr(data-tip); | |
| position: absolute; bottom: calc(100% + 9px); left: 50%; transform: translateX(-50%); | |
| background: var(--ink); color: var(--sand); font-family:'Inter',sans-serif; | |
| font-size: 12px; font-weight: 500; line-height: 1.35; letter-spacing: 0; | |
| text-transform: none; padding: 7px 11px; border-radius: 10px; white-space: normal; | |
| width: max-content; max-width: 230px; z-index: 999; pointer-events: none; | |
| box-shadow: 0 8px 24px -10px rgba(0,0,0,.4); animation: flow-in .18s ease; | |
| } | |
| [data-tip]:hover::before { | |
| content: ''; position: absolute; bottom: calc(100% + 3px); left: 50%; | |
| transform: translateX(-50%); border: 6px solid transparent; border-top-color: var(--ink); | |
| z-index: 999; pointer-events: none; | |
| } | |
| /* scrollbars */ | |
| ::-webkit-scrollbar { width: 7px; height: 7px; } | |
| ::-webkit-scrollbar-thumb { background: var(--line); border-radius: 99px; } | |
| /* ββ Horizontal task tab row (in the task detail view) ββ */ | |
| .task-tab-row { | |
| display: flex !important; | |
| flex-direction: row !important; | |
| overflow-x: auto !important; | |
| gap: 8px !important; | |
| padding: 10px 0 !important; | |
| flex-wrap: nowrap !important; | |
| scrollbar-width: thin; | |
| scrollbar-color: var(--green) var(--paper); | |
| } | |
| .task-tab { | |
| background-color: var(--paper) !important; | |
| color: var(--muted) !important; | |
| border: 1.5px solid var(--paper-2) !important; | |
| border-radius: 20px !important; | |
| padding: 8px 16px !important; | |
| min-width: 0 !important; | |
| font-size: 0.85rem !important; | |
| font-weight: 500 !important; | |
| white-space: nowrap !important; | |
| flex-shrink: 0 !important; | |
| cursor: pointer !important; | |
| transition: all 0.2s ease !important; | |
| } | |
| .task-tab:hover { | |
| border-color: var(--green) !important; | |
| color: var(--ink) !important; | |
| transform: scale(1.03) !important; | |
| } | |
| .task-tab-active { | |
| background-color: #1b5e20 !important; | |
| border-color: var(--green) !important; | |
| color: var(--ink) !important; | |
| font-weight: 700 !important; | |
| box-shadow: 0 0 10px rgba(76, 175, 80, 0.3) !important; | |
| } | |
| .task-tab-done { | |
| border-color: var(--green) !important; | |
| color: var(--green) !important; | |
| opacity: 0.8 !important; | |
| } | |
| .task-tab-active.task-tab-done { | |
| background-color: #1b5e20 !important; | |
| color: var(--ink) !important; | |
| opacity: 1 !important; | |
| } | |
| /* ββ Prev / Next task navigation ββ */ | |
| .task-nav-row { | |
| display: flex !important; | |
| justify-content: space-between !important; | |
| align-items: center !important; | |
| margin: 8px 0 !important; | |
| } | |
| .task-nav-btn { | |
| background-color: var(--paper-2) !important; | |
| color: var(--muted) !important; | |
| border: 1px solid var(--line) !important; | |
| border-radius: 8px !important; | |
| padding: 6px 14px !important; | |
| font-size: 0.85rem !important; | |
| transition: all 0.2s ease !important; | |
| } | |
| .task-nav-btn:hover { | |
| border-color: var(--green) !important; | |
| color: var(--ink) !important; | |
| transform: scale(1.03) !important; | |
| } | |
| .task-nav-btn:active { transform: scale(0.97) !important; } | |
| .task-position { | |
| color: var(--muted) !important; | |
| font-size: 0.85rem !important; | |
| text-align: center !important; | |
| } | |
| /* ββ Voice journal accordion ββ */ | |
| .voice-journal-accordion { | |
| border: 1px solid var(--paper-2) !important; | |
| border-radius: 8px !important; | |
| margin-bottom: 8px !important; | |
| } | |
| .voice-journal-accordion .label-wrap { | |
| color: var(--muted) !important; | |
| font-size: 0.9rem !important; | |
| } | |
| /* ββ Hints vs safety flags ββ */ | |
| .hint-item { | |
| border-left: 3px solid var(--yellow) !important; /* amber = discovery */ | |
| padding-left: 10px !important; | |
| margin: 6px 0 !important; | |
| color: var(--muted) !important; | |
| font-size: 0.9rem !important; | |
| } | |
| .safety-item { | |
| border-left: 3px solid var(--red) !important; /* red = warning */ | |
| padding-left: 10px !important; | |
| margin: 6px 0 !important; | |
| color: #FFCDD2 !important; | |
| font-size: 0.9rem !important; | |
| background-color: rgba(244, 67, 54, 0.05) !important; | |
| } | |
| /* ββ Completion unlock instruction ββ */ | |
| .completion-instruction { | |
| text-align: center !important; | |
| font-size: 0.9rem !important; | |
| padding: 6px 12px !important; | |
| border-radius: 8px !important; | |
| margin: 6px 0 !important; | |
| font-weight: 500 !important; | |
| } | |
| /* Locked state reads as an action gate: a dashed callout box, not a flat | |
| dim rectangle, with the lock icon sitting left of the text. */ | |
| .completion-instruction.locked { | |
| display: flex !important; | |
| align-items: center !important; | |
| justify-content: center !important; | |
| gap: 10px !important; | |
| text-align: left !important; | |
| border: 1.5px dashed rgba(255,255,255,0.18) !important; | |
| border-radius: 8px !important; | |
| padding: 8px 14px !important; | |
| color: rgba(255,255,255,0.55) !important; | |
| font-size: 0.82rem !important; | |
| background: rgba(255,255,255,0.03) !important; | |
| } | |
| .completion-instruction.unlocked { | |
| color: var(--green) !important; | |
| background-color: rgba(76, 175, 80, 0.08) !important; | |
| border: 1px solid rgba(76, 175, 80, 0.2) !important; | |
| } | |
| /* ββ Bottom action buttons: Mark completed + Finish are pixel-identical ββ | |
| Both share one rule set targeting the inner <button>; the wrapper blocks | |
| are stripped of padding so neither inherits stray pill-shape/spacing. */ | |
| .complete-btn, .finish-btn, | |
| .complete-btn > div, .finish-btn > div, | |
| .mark-complete-btn { padding: 0 !important; width: 100% !important; } | |
| .complete-btn button, | |
| .finish-btn button { | |
| height: 52px !important; | |
| min-height: 52px !important; | |
| font-size: 1rem !important; | |
| font-weight: 700 !important; | |
| border-radius: 8px !important; | |
| width: 100% !important; | |
| padding: 0 24px !important; | |
| transition: filter 0.2s ease !important; | |
| } | |
| /* Solid green for: Finish (always) + Mark completed when unlocked. */ | |
| .finish-btn button, | |
| .complete-btn button:not(:disabled) { | |
| background: #22c55e !important; | |
| background-color: #22c55e !important; | |
| color: #000 !important; | |
| border: none !important; | |
| cursor: pointer !important; | |
| } | |
| .finish-btn button:hover, | |
| .complete-btn button:not(:disabled):hover { | |
| filter: brightness(1.08) !important; | |
| transform: none !important; | |
| } | |
| /* Mark completed β locked/disabled (outlined ghost). */ | |
| .complete-btn button:disabled, | |
| .complete-btn button[disabled] { | |
| background: transparent !important; | |
| background-color: transparent !important; | |
| border: 1.5px solid rgba(255, 255, 255, 0.15) !important; | |
| color: rgba(255, 255, 255, 0.3) !important; | |
| cursor: not-allowed !important; | |
| } | |
| /* Lobby "Start the adventure" β disabled while waiting for teams. | |
| Ghost-disabled treatment so it reads as locked, not invisible. */ | |
| #lobby-start button:disabled, | |
| #lobby-start button[disabled] { | |
| background: transparent !important; | |
| background-color: transparent !important; | |
| border: 1.5px solid rgba(255, 255, 255, 0.15) !important; | |
| color: rgba(255, 255, 255, 0.3) !important; | |
| cursor: not-allowed !important; | |
| } | |
| /* Host "Start anyway" override β green underlined text link, not a button. | |
| Gradio 6 puts elem_id/elem_classes on the <button> itself, so style the | |
| element directly. Also cover a descendant button + the class, so this holds | |
| whether or not Gradio wraps the control. */ | |
| #lobby-start-anyway, | |
| #lobby-start-anyway button, | |
| .lobby-start-link, | |
| .lobby-start-link button { | |
| background: transparent !important; | |
| background-color: transparent !important; | |
| border: none !important; | |
| box-shadow: none !important; | |
| width: auto !important; | |
| min-width: 0 !important; | |
| padding: 2px 4px !important; | |
| margin: 6px auto 0 !important; | |
| display: block !important; | |
| color: var(--green) !important; | |
| font-weight: 600 !important; | |
| text-decoration: underline !important; | |
| text-underline-offset: 3px !important; | |
| cursor: pointer !important; | |
| } | |
| #lobby-start-anyway:hover, | |
| #lobby-start-anyway button:hover, | |
| .lobby-start-link:hover, | |
| .lobby-start-link button:hover { | |
| filter: brightness(1.15) !important; | |
| transform: none !important; | |
| background: transparent !important; | |
| } | |
| #lobby-start-anyway:disabled, | |
| #lobby-start-anyway[disabled], | |
| #lobby-start-anyway button:disabled, | |
| #lobby-start-anyway button[disabled], | |
| .lobby-start-link:disabled, | |
| .lobby-start-link button:disabled, | |
| .lobby-start-link button[disabled] { | |
| color: rgba(255, 255, 255, 0.3) !important; | |
| text-decoration: underline !important; | |
| cursor: not-allowed !important; | |
| } | |
| /* ββ Audio widget: let the "no microphone detected" message wrap ββ */ | |
| .cq-audio .empty, .cq-audio .source-selection, .cq-audio span, .cq-audio p { | |
| white-space: normal !important; | |
| overflow: visible !important; | |
| text-overflow: clip !important; | |
| } | |
| /* ββ Proof-section button hierarchy ββ */ | |
| /* Primary actions (Save journal, Add photo) β green/filled */ | |
| .btn-primary-action { | |
| background-color: var(--green) !important; | |
| color: #FFFFFF !important; | |
| font-weight: 700 !important; | |
| border: none !important; | |
| transition: all 0.2s ease !important; | |
| } | |
| .btn-primary-action:hover { | |
| background-color: var(--green-h) !important; | |
| filter: brightness(1.1) !important; | |
| transform: scale(1.02) !important; | |
| } | |
| /* Secondary actions (Transcribe) β grey/outlined */ | |
| .btn-secondary-action { | |
| background-color: var(--paper-2) !important; | |
| color: var(--muted) !important; | |
| border: 1px solid var(--line) !important; | |
| transition: all 0.2s ease !important; | |
| } | |
| .btn-secondary-action:hover { | |
| border-color: var(--green) !important; | |
| color: #FFFFFF !important; | |
| transform: scale(1.02) !important; | |
| } | |
| /* ββ Proof columns: voice journal & photos read as matching side-by-side | |
| panels (same surface, padding, and header) ββ */ | |
| .proof-col { | |
| background: var(--paper) !important; | |
| border: 1px solid var(--line) !important; | |
| border-radius: 12px !important; | |
| padding: 18px !important; | |
| } | |
| /* Strip Gradio's grey block + label-header fill behind the textbox labels | |
| (Caption / Your notes). The proof-col already supplies the surface, so the | |
| textarea's own dark fill + rounded border is all we want β no grey wrapper | |
| rectangle and no slate label bar. Scoped to .proof-col so nothing else | |
| changes. CSS-var override neutralizes the fills at the source; the explicit | |
| selectors are the fallback. The textarea/input is never targeted, so its | |
| own rule (dark fill + border) is preserved. */ | |
| .proof-col { | |
| --block-label-background-fill: transparent !important; | |
| --block-background-fill: transparent !important; | |
| --block-title-background-fill: transparent !important; | |
| } | |
| /* 1. Strip the wrapper rectangle β block layers around the textbox. */ | |
| .proof-col .block, | |
| .proof-col .form, | |
| .proof-col [class*="block"]:not(.proof-col) { | |
| background: transparent !important; | |
| border: none !important; | |
| box-shadow: none !important; | |
| padding: 0 !important; | |
| } | |
| /* 2. Kill the slate label bar behind YOUR NOTES / CAPTION (keep the | |
| uppercase label text styling defined for label span / .label-wrap span). */ | |
| .proof-col .label-wrap, | |
| .proof-col label { | |
| background: transparent !important; | |
| background-color: transparent !important; | |
| border: none !important; | |
| box-shadow: none !important; | |
| padding-left: 0 !important; | |
| } | |
| /* Force both panels to equal height, anchored at top. The row's height is set | |
| by the taller panel (Photos); stretch makes the shorter one (Voice) fill it. */ | |
| .panels-row { align-items: stretch !important; gap: 16px !important; } | |
| .panels-row > div { | |
| display: flex !important; | |
| flex-direction: column !important; | |
| height: 100% !important; | |
| } | |
| /* Each panel's inner Gradio Group/Column must also stretch. */ | |
| .panels-row .gr-group, | |
| .panels-row .gr-column, | |
| .panels-row [class*="block"] { | |
| height: 100% !important; | |
| display: flex !important; | |
| flex-direction: column !important; | |
| } | |
| /* Keep the two panel headers on the same baseline. */ | |
| .panel-header { margin: 0 0 10px !important; } | |
| /* ββ In-game guide (hints) ββ */ | |
| /* Header row: "Need a clue?" on the left, points badge on the right. */ | |
| .hints-header { | |
| display: flex !important; | |
| justify-content: space-between !important; | |
| align-items: center !important; | |
| margin: 14px 0 8px !important; | |
| gap: 12px !important; | |
| flex-wrap: nowrap !important; | |
| } | |
| .hints-header > * { flex: 0 0 auto !important; } | |
| .guide-header { | |
| font-size: 0.95rem !important; | |
| font-weight: 600 !important; | |
| color: #FFFFFF !important; /* white β primary prompt */ | |
| margin: 0 !important; | |
| } | |
| .guide-header p { color: #FFFFFF !important; margin: 0 !important; } | |
| /* Larger, weightier amber hint prompt. */ | |
| .hints-label, | |
| .hints-label p { | |
| font-size: 1.05rem !important; | |
| font-weight: 600 !important; | |
| color: #f5a623 !important; | |
| margin-bottom: 10px !important; | |
| } | |
| .points-display, .points-display p { | |
| font-size: 0.85rem !important; | |
| font-weight: 600 !important; | |
| color: var(--yellow) !important; | |
| text-align: right !important; | |
| margin: 0 !important; | |
| } | |
| /* Input + button on one line, equal height, vertically centred. */ | |
| .hints-input-row { | |
| align-items: center !important; | |
| gap: 8px !important; | |
| } | |
| /* Hint input: a clean rounded box (transparent fill, subtle border). */ | |
| .hint-input input, | |
| .hint-input textarea { | |
| background: transparent !important; | |
| background-color: transparent !important; | |
| border: 1.5px solid rgba(255, 255, 255, 0.2) !important; | |
| border-radius: 10px !important; | |
| box-shadow: none !important; | |
| padding: 10px 14px !important; | |
| color: rgba(255, 255, 255, 0.85) !important; | |
| } | |
| .hint-input input:focus, | |
| .hint-input textarea:focus { | |
| border: 1.5px solid #f5a623 !important; | |
| outline: none !important; | |
| box-shadow: none !important; | |
| } | |
| /* Strip every Gradio wrapper layer (block, form, container, wrap) so only the | |
| underline shows β the input had a rounded box AND a rectangle behind it. */ | |
| .hint-input, | |
| .hint-input *:not(input):not(textarea) { | |
| background: transparent !important; | |
| background-color: transparent !important; | |
| border: none !important; | |
| box-shadow: none !important; | |
| } | |
| .hint-input, | |
| .hint-input .block, | |
| .hint-input .form, | |
| .hint-input > div { | |
| padding: 0 !important; | |
| } | |
| .hints-input-row input { | |
| height: 44px !important; | |
| } | |
| .hints-input-row button { | |
| height: 44px !important; | |
| white-space: nowrap !important; | |
| } | |
| .points-display.reduced, .points-display.reduced p { | |
| color: #FF7043 !important; /* orange-red = cost warning */ | |
| } | |
| .btn-hint { | |
| background-color: var(--paper-2) !important; | |
| color: var(--yellow) !important; | |
| border: 1.5px solid var(--yellow) !important; | |
| border-radius: 10px !important; | |
| font-weight: 600 !important; | |
| transition: all 0.2s ease !important; | |
| } | |
| .btn-hint:hover { | |
| background-color: rgba(255, 193, 7, 0.1) !important; | |
| transform: scale(1.02) !important; | |
| } | |
| .btn-hint.spent { | |
| color: #666666 !important; | |
| border-color: var(--line) !important; | |
| cursor: not-allowed !important; | |
| opacity: 0.6 !important; | |
| } | |
| /* ββ Play screen outer card βββββββββββββββββββββββββββββββ */ | |
| .play-card { | |
| background: rgba(42, 42, 42, 0.92) !important; | |
| border: 1px solid var(--line) !important; | |
| border-radius: 16px !important; | |
| box-shadow: | |
| 0 0 0 1px rgba(255, 255, 255, 0.04), | |
| 0 8px 32px rgba(0, 0, 0, 0.45) !important; | |
| padding: 20px !important; | |
| overflow: hidden !important; | |
| } | |
| /* Strip Gradio's own block chrome so we don't stack borders/backgrounds */ | |
| .play-card.gr-group, | |
| .play-card .block:first-child { | |
| background: transparent !important; | |
| border: none !important; | |
| box-shadow: none !important; | |
| padding: 0 !important; | |
| } | |
| /* Clip the task tab bar to the card's rounded top corners */ | |
| .play-card .task-tab-row { | |
| border-radius: 12px 12px 0 0 !important; | |
| } | |
| /* Subtle separators between the major sections */ | |
| .play-card .panels-row { | |
| border-top: 1px solid rgba(255, 255, 255, 0.06) !important; | |
| padding-top: 16px !important; | |
| margin-top: 12px !important; | |
| } | |
| .play-card .hints-header { | |
| border-top: 1px solid rgba(255, 255, 255, 0.06) !important; | |
| padding-top: 14px !important; | |
| margin-top: 12px !important; | |
| } | |
| """ | |
| JS_INIT = """ | |
| function() { | |
| // ββ Force DARK theme so every visitor sees identical colours ββ | |
| // Without this, Gradio follows each user's OS dark/light preference, | |
| // so text/background regions look different per person. | |
| try { | |
| const u = new URL(window.location.href); | |
| if (u.searchParams.get('__theme') !== 'dark') { | |
| u.searchParams.set('__theme', 'dark'); | |
| window.location.replace(u.toString()); | |
| return; | |
| } | |
| } catch (e) {} | |
| document.documentElement.classList.add('dark'); | |
| document.body && document.body.classList.add('dark'); | |
| const TIPS = __TOOLTIPS__; | |
| function applyTips() { | |
| for (const [id, tip] of Object.entries(TIPS)) { | |
| const el = document.getElementById(id); | |
| if (!el) continue; | |
| el.setAttribute('data-tip', tip); | |
| el.setAttribute('title', tip); // native browser tooltip fallback | |
| const btn = el.querySelector('button'); | |
| if (btn) { btn.setAttribute('data-tip', tip); btn.setAttribute('title', tip); } | |
| } | |
| // keep the dark class on in case Gradio strips it | |
| document.documentElement.classList.add('dark'); | |
| } | |
| function ripple(e) { | |
| const btn = e.target.closest('button'); | |
| if (!btn) return; | |
| const r = btn.getBoundingClientRect(); | |
| const c = document.createElement('span'); | |
| c.className = 'cq-ripple'; | |
| const size = Math.max(r.width, r.height); | |
| c.style.width = c.style.height = size + 'px'; | |
| c.style.left = (e.clientX - r.left - size / 2) + 'px'; | |
| c.style.top = (e.clientY - r.top - size / 2) + 'px'; | |
| btn.appendChild(c); | |
| setTimeout(() => c.remove(), 600); | |
| } | |
| document.addEventListener('pointerdown', ripple, true); | |
| applyTips(); | |
| // Gradio re-renders fragments on poll; keep tips fresh. | |
| setInterval(applyTips, 700); | |
| } | |
| """.replace("__TOOLTIPS__", __import__("json").dumps(TOOLTIPS)) | |
| def gradio_theme(): | |
| """Base Gradio theme tuned to the high-contrast dark palette. | |
| Most styling lives in CSS; this only sets the fallback colours Gradio | |
| paints before/around the stylesheet so there is no light flash. | |
| """ | |
| return gr.themes.Base( | |
| primary_hue=gr.themes.colors.green, | |
| secondary_hue=gr.themes.colors.gray, | |
| neutral_hue=gr.themes.colors.gray, | |
| font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"], | |
| ).set( | |
| body_background_fill="#1A1A1A", | |
| body_background_fill_dark="#1A1A1A", | |
| body_text_color="#FFFFFF", | |
| body_text_color_dark="#FFFFFF", | |
| block_background_fill="rgba(42,42,42,0.92)", | |
| block_background_fill_dark="rgba(42,42,42,0.92)", | |
| block_border_color="#4A4A4A", | |
| block_radius="18px", | |
| button_large_radius="999px", | |
| button_small_radius="999px", | |
| ) | |