Spaces:
Sleeping
Sleeping
| """audio·brief design tokens — lifted from Audio Brief.dc.html (Crate Dock). | |
| Three semantic colour families: | |
| - coral #FF6A3D — generative (prompts, gens, variants, "create") | |
| - mint #5BE0C8 — measured (BPM, key, sections, loudness, "observe") | |
| - amber #FFC24B — anchor (your favourited take we blend from) | |
| Fonts: Space Grotesk for body/heading, JetBrains Mono for numbers + labels. | |
| Background system: near-black (#0F1115) → panel (#121419) → card (#14171D) → | |
| input (#171A20). Borders step from #20242C (faint) to #2A2F38 (chip). | |
| The theme exposes Gradio's tokens; the CSS adds layout primitives the | |
| design needs that Gradio doesn't model (mono labels, metric tile grid, | |
| brand mark, pollen pill). All custom classes prefixed `dc-`. | |
| """ | |
| from __future__ import annotations | |
| import gradio as gr | |
| # --------------------------------------------------------------------------- | |
| # Pollinations App Key — public client identifier for audio-brief, issued at | |
| # auth.pollinations.ai. `pk_` prefix = publishable, safe to commit (Stripe- | |
| # style). Embedded in the OAuth authorize URL so Pollinations knows which | |
| # registered app is asking for consent and scopes the issued user-key to it. | |
| # The redirect URI registered alongside this key MUST match the production | |
| # URL the OAuth flow lands on — currently `https://kalamishere-audio-brief.hf.space/`. | |
| # --------------------------------------------------------------------------- | |
| POLLINATIONS_APP_KEY = "pk_dM3MSkFzNdenZCd5" | |
| # --------------------------------------------------------------------------- | |
| # Colour ramps — c500 anchors the brand colour; c50→c950 fade for hover/text | |
| # --------------------------------------------------------------------------- | |
| CORAL = gr.themes.Color( | |
| c50="#FFE7DD", | |
| c100="#FFD0BC", | |
| c200="#FFB89A", | |
| c300="#FFA279", | |
| c400="#FF8C5A", | |
| c500="#FF6A3D", # brand coral | |
| c600="#E55731", | |
| c700="#CC4424", | |
| c800="#A23217", | |
| c900="#78210B", | |
| c950="#140B07", # dark text on coral button | |
| ) | |
| MINT = gr.themes.Color( | |
| c50="#E1FBF6", | |
| c100="#C2F5EA", | |
| c200="#A2EFDE", | |
| c300="#83E9D3", | |
| c400="#5BE0C8", # brand mint (light) | |
| c500="#5BE0C8", # brand mint | |
| c600="#42C7AE", | |
| c700="#2BB89E", | |
| c800="#1B8C76", | |
| c900="#0F5949", | |
| c950="#06201B", | |
| ) | |
| # Warm-grey neutral — matches design's panel + text greys. | |
| ZINC = gr.themes.Color( | |
| c50="#F2EFE9", # primary text (cream) | |
| c100="#E3E0D9", | |
| c200="#C9CDD4", # secondary text | |
| c300="#99A0AB", # muted text | |
| c400="#7A828D", | |
| c500="#5E6671", # faint label text | |
| c600="#4A5158", | |
| c700="#3A3F47", | |
| c800="#2A2F38", # chip border | |
| c900="#20242C", # divider border | |
| c950="#0F1115", # page bg | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Theme object | |
| # --------------------------------------------------------------------------- | |
| THEME = gr.themes.Base( | |
| primary_hue=CORAL, | |
| secondary_hue=MINT, | |
| neutral_hue=ZINC, | |
| radius_size=gr.themes.sizes.radius_lg, | |
| spacing_size=gr.themes.sizes.spacing_md, | |
| text_size=gr.themes.sizes.text_md, | |
| font=[gr.themes.GoogleFont("Space Grotesk"), "system-ui", "sans-serif"], | |
| font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "ui-monospace", "monospace"], | |
| ).set( | |
| # surfaces | |
| body_background_fill="#0F1115", | |
| body_text_color="#F2EFE9", | |
| body_text_color_subdued="#99A0AB", | |
| background_fill_primary="#0F1115", | |
| background_fill_secondary="#121419", | |
| block_background_fill="#14171D", | |
| block_border_color="#232831", | |
| block_border_width="1px", | |
| block_radius="14px", | |
| block_label_background_fill="transparent", | |
| block_label_text_color="#99A0AB", | |
| block_label_text_size="11px", | |
| block_label_text_weight="600", | |
| block_title_text_color="#F2EFE9", | |
| block_title_text_weight="600", | |
| panel_background_fill="#121419", | |
| panel_border_color="#20242C", | |
| border_color_primary="#232831", | |
| border_color_accent="#FF6A3D", | |
| # buttons | |
| button_primary_background_fill="#FF6A3D", | |
| button_primary_background_fill_hover="#FF8C5A", | |
| button_primary_text_color="#140B07", | |
| button_primary_border_color="#FF6A3D", | |
| button_secondary_background_fill="#171A20", | |
| button_secondary_background_fill_hover="#1B1F26", | |
| button_secondary_text_color="#C9CDD4", | |
| button_secondary_border_color="#2A2F38", | |
| button_cancel_background_fill="#171A20", | |
| button_cancel_text_color="#99A0AB", | |
| button_cancel_border_color="#2A2F38", | |
| button_large_radius="9px", | |
| button_small_radius="7px", | |
| # inputs | |
| input_background_fill="#0F1115", | |
| input_background_fill_focus="#121419", | |
| input_border_color="#232831", | |
| input_border_color_focus="#FF6A3D", | |
| input_placeholder_color="#5E6671", | |
| input_radius="9px", | |
| slider_color="#FF6A3D", | |
| # tables / dataframes | |
| table_border_color="#232831", | |
| table_even_background_fill="#14171D", | |
| table_odd_background_fill="#121419", | |
| table_row_focus="rgba(255,106,61,0.08)", | |
| table_text_color="#C9CDD4", | |
| # code / markdown | |
| code_background_fill="#0F1115", | |
| link_text_color="#5BE0C8", | |
| link_text_color_hover="#FF8C5A", | |
| # accent token used by various states | |
| color_accent="#5BE0C8", | |
| color_accent_soft="rgba(91,224,200,0.14)", | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # CSS — what Gradio's theme tokens don't cover. | |
| # Adds: font import, scrollbar, brand mark, pollen pill, metric tiles, | |
| # inline-coloured number helpers, label mono-uppercase. | |
| # --------------------------------------------------------------------------- | |
| CUSTOM_CSS = """ | |
| /* ---- Fonts (Gradio's font= already injects these, kept as belt-and-braces) */ | |
| @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap'); | |
| body { font-family: 'Space Grotesk', system-ui, sans-serif !important; } | |
| code, kbd, pre, .mono, .dc-mono { font-family: 'JetBrains Mono', ui-monospace, monospace !important; } | |
| /* ---- Palette system (v3 design handoff) ------------------------------- | |
| Three named worlds the user toggles in the top bar. Implemented as a | |
| class on <body> (`pal-studio` / `pal-console` / `pal-dub`) that | |
| defines a token set; every surface styles from var(--token) so a | |
| single class swap re-themes the tree. | |
| The 350ms cross-fade requested in the brief is intentionally NOT used | |
| here — animating background-color through a var() that resolves via a | |
| class swap stalls mid-transition in-engine (renders half-painted). | |
| Instant swap is correct and reliable. See design/README.md §⚠. | |
| Default below `:root` matches Console so var() resolves even before | |
| the JS class-swap runs on first paint. | |
| */ | |
| :root, body.pal-console { | |
| --paper:#13141A; --paper2:#181A22; --paper3:#1C1F28; --input:#1F232D; | |
| --ink:#F2EFE9; --ink2:#C9CDD4; --ink3:#99A0AB; --label:#5E6671; | |
| --line:#23262F; --line2:#1D2027; --chipline:#2E323C; | |
| --coral:#FF6A3D; --coralb:#FF8C5A; --onCoral:#140B07; | |
| --amber:#FFC24B; --metric:#5BE0C8; --mint:#5BE0C8; | |
| --secIntro:#2BB89E; --secBuild:#5BE0C8; --secCore:#FF6A3D; --secOutro:#FF8C5A; | |
| --coralTint: rgba(255,106,61,0.08); | |
| --coralTintHover: rgba(255,106,61,0.14); | |
| --amberTint: rgba(255,194,75,0.10); | |
| --drop: 0 24px 60px rgba(0,0,0,.40); | |
| } | |
| /* Studio (light) was the v3 design's primary new palette but shipped with | |
| multiple legibility regressions on Gradio v6 — browser-default button | |
| backgrounds beating our transparent rule, brand wordmark losing | |
| contrast, cost pip washing out, native button text inheriting the | |
| wrong color. Dropped from the toggle for now; spec preserved below | |
| for a dedicated Studio pass (will need Gradio-native button + input | |
| overrides at .pal-studio scope to beat user-agent styles). | |
| ---- | |
| body.pal-studio { | |
| --paper:#FBF8F1; --paper2:#F3EDE1; --paper3:#EAE2D2; --input:#FCFAF4; | |
| --ink:#0E1A1E; --ink2:#45565A; --ink3:#7A878A; --label:#8A969A; | |
| --line:rgba(14,26,30,0.14); --line2:rgba(14,26,30,0.09); --chipline:rgba(14,26,30,0.20); | |
| --coral:#D9461F; --coralb:#E8633A; --onCoral:#FBF8F1; | |
| --amber:#BC841C; --metric:#157567; --mint:#1C9482; | |
| --secIntro:#157567; --secBuild:#1C9482; --secCore:#D9461F; --secOutro:#E8633A; | |
| --coralTint: rgba(217,70,31,0.07); | |
| --coralTintHover: rgba(217,70,31,0.13); | |
| --amberTint: rgba(188,132,28,0.10); | |
| --drop: 0 1px 2px rgba(14,26,30,.10), 0 14px 40px rgba(14,26,30,.08); | |
| } | |
| */ | |
| body.pal-dub { | |
| --paper:#1B1612; --paper2:#241D17; --paper3:#2E2620; --input:#332A22; | |
| --ink:#E8DECB; --ink2:#C2B7A0; --ink3:#8C8270; --label:#6E6453; | |
| --line:#372E25; --line2:#2A231C; --chipline:#473C30; | |
| --coral:#FF7A4D; --coralb:#FF9468; --onCoral:#1B1612; | |
| --amber:#FFB94B; --metric:#7BE5CC; --mint:#7BE5CC; | |
| --secIntro:#3FB89E; --secBuild:#7BE5CC; --secCore:#FF7A4D; --secOutro:#FF9468; | |
| --coralTint: rgba(255,122,77,0.10); | |
| --coralTintHover: rgba(255,122,77,0.16); | |
| --amberTint: rgba(255,185,75,0.12); | |
| --drop: 0 22px 50px rgba(0,0,0,.55); | |
| } | |
| /* ---- Dub-only character pass — make it actually feel like vinyl, not | |
| just "Console but warmer". Each rule is scoped to body.pal-dub so the | |
| Console look is untouched. Pieces: | |
| 1. Subtle paper-grain noise overlay on body (fractalNoise SVG data | |
| URI, ~3% opacity) so card stock reads as material, not glass. | |
| 2. Brand wordmark dot flips mint → amber — Console's mint dot is a | |
| "measured" cue; Dub leans warmer, amber matches the anchor. | |
| 3. Coral buttons gain an inset bottom shadow so they read as | |
| stamped/inked rather than flat. Hover lifts the stamp. | |
| 4. Card-edge highlight (1px top inner glow + a slightly heavier | |
| border) — gives cards a worn-pressboard feel. | |
| 5. Metric numbers bump to 700 weight — the readout reads as | |
| chunky-typeset rather than the lighter Console face. | |
| 6. Top-bar bottom border thickens to 2px → reads as a turntable | |
| platter rim under the brand. | |
| */ | |
| body.pal-dub::before { | |
| content: ""; | |
| position: fixed; | |
| inset: 0; | |
| pointer-events: none; | |
| z-index: 9999; | |
| opacity: 0.04; | |
| mix-blend-mode: overlay; | |
| background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>"); | |
| background-size: 180px 180px; | |
| } | |
| body.pal-dub .dc-brand .dot { color: var(--amber); } | |
| body.pal-dub .gr-button.gr-button-primary, | |
| body.pal-dub .gr-button-primary, | |
| body.pal-dub button[class*="primary"] { | |
| box-shadow: inset 0 -2px 0 rgba(0,0,0,0.35), 0 1px 0 rgba(255,180,120,0.10) !important; | |
| text-shadow: 0 1px 0 rgba(0,0,0,0.20); | |
| } | |
| body.pal-dub .gr-button.gr-button-primary:hover, | |
| body.pal-dub .gr-button-primary:hover, | |
| body.pal-dub button[class*="primary"]:hover { | |
| box-shadow: inset 0 -1px 0 rgba(0,0,0,0.30), 0 2px 0 rgba(255,180,120,0.14) !important; | |
| } | |
| body.pal-dub .dc-metric, | |
| body.pal-dub .dc-anchor-player, | |
| body.pal-dub .dc-variants-prompt-card, | |
| body.pal-dub .dc-derived-card, | |
| body.pal-dub .dc-variant-title { | |
| box-shadow: inset 0 1px 0 rgba(255,210,170,0.04); | |
| border-width: 1px; | |
| } | |
| body.pal-dub .dc-metric-v { font-weight: 700; letter-spacing: -0.01em; } | |
| body.pal-dub .dc-topbar { border-bottom-width: 2px; border-bottom-color: var(--line); } | |
| body.pal-dub .dc-crate-strip label, | |
| body.pal-dub .dc-crate-strip .wrap label { | |
| box-shadow: inset 0 1px 0 rgba(255,210,170,0.05); | |
| } | |
| /* Anchor card in the variants row reads as the actual record on the | |
| mat — slightly warmer tint than Console's plain amber wash. */ | |
| body.pal-dub .dc-variant.anchor, | |
| body.pal-dub .dc-tile.anchor { | |
| background: linear-gradient(180deg, var(--amberTint) 0%, var(--paper3) 100%) !important; | |
| border-color: color-mix(in srgb, var(--amber) 55%, transparent) !important; | |
| } | |
| /* ---- Scrollbar — slim, neutral */ | |
| ::-webkit-scrollbar { width: 8px; height: 8px; } | |
| ::-webkit-scrollbar-thumb { background: var(--chipline); border-radius: 4px; } | |
| ::-webkit-scrollbar-track { background: transparent; } | |
| /* ---- Page container — palette-driven base, cap content width so the | |
| layout stays focused on wide monitors. Gradio v6 wraps content in | |
| `.wrap > main.contain` (no .gradio-container class). 1200px feels | |
| right for composer + crate; waveform + grids fill this width. | |
| Below 1200px viewport, the container naturally shrinks. | |
| The explicit width:100% on html/body/wrap defends against a | |
| Gradio v6 layout collapse: when a top-level state-changing | |
| element re-renders (e.g. the topbar gr.HTML refreshing after | |
| OAuth completes), the outer flex container can fall back to | |
| min-content width — which then shrinks to the widest single | |
| child (the 360px-capped Generate button). Pinning the chain to | |
| 100% width keeps it filling the viewport across re-renders. */ | |
| html, body, .wrap, gradio-app, gradio-app > div, .gradio-container { | |
| width: 100% !important; | |
| min-width: 100% !important; | |
| max-width: 100% !important; | |
| transform: none !important; | |
| zoom: 1 !important; | |
| } | |
| /* Stop any horizontal overflow so the centered main.contain doesn't | |
| sit off-axis — 100vw includes scrollbar width on Chromium/Brave, | |
| so anything pinned to 100vw drifts the layout right by the scroll | |
| gutter (~15px). Stay with 100% (respects scrollbar). */ | |
| html, body { overflow-x: hidden !important; } | |
| html { zoom: 1 !important; -webkit-text-size-adjust: 100% !important; } | |
| /* Belt-and-braces: if Gradio's .wrap is a flex container, | |
| margin:0 auto on main.contain won't center — assert it explicitly. */ | |
| .wrap { display: block !important; } | |
| main.contain, .wrap > main { | |
| background: transparent !important; | |
| color: var(--ink); | |
| width: 100% !important; | |
| max-width: 1280px !important; | |
| margin: 0 auto !important; | |
| padding: 28px 24px !important; | |
| box-sizing: border-box !important; | |
| } | |
| body { | |
| background: var(--paper) !important; | |
| color: var(--ink); | |
| position: relative; | |
| /* Soft brand-axis gradients on the body bg — coral at top-right | |
| (generative axis), mint at bottom-left (measured axis). Each | |
| sits at ~5% opacity so the surface stays Console-dark but reads | |
| as a composed page rather than a flat black plane. Designer | |
| cleanup pass: gives the eye warmth without competing with the | |
| Generate CTA. */ | |
| background-image: | |
| radial-gradient(60% 50% at 95% 0%, color-mix(in srgb, var(--coral) 14%, transparent), transparent 60%), | |
| radial-gradient(60% 60% at 5% 100%, color-mix(in srgb, var(--mint) 9%, transparent), transparent 65%) !important; | |
| background-attachment: fixed !important; | |
| } | |
| /* ---- Gradio native surface overrides — needed so block/input/button | |
| backgrounds re-paint per palette (the gr.themes.Base values were | |
| baked at construction time, so we re-assert via the class). */ | |
| body .block, body .panel { background: var(--paper2) !important; border-color: var(--line) !important; } | |
| body .form, body .gr-box { background: var(--paper3) !important; border-color: var(--line) !important; } | |
| body input[type="text"], body textarea, body .input-container { background: var(--input) !important; color: var(--ink) !important; border-color: var(--line) !important; } | |
| body .gr-button-primary, body button[class*="primary"] { | |
| background: var(--coral) !important; | |
| color: var(--onCoral) !important; | |
| border-color: var(--coral) !important; | |
| } | |
| body .gr-button-secondary, body button[class*="secondary"] { | |
| background: var(--input) !important; | |
| color: var(--ink2) !important; | |
| border-color: var(--chipline) !important; | |
| } | |
| /* Hide Gradio's footer ("Use via API" etc.) — clutter */ | |
| footer { display: none !important; } | |
| /* ---- Action button width cap — primary buttons should size to content, | |
| not stretch to the row width. ~360px is the design's big-CTA width. | |
| Prevents the "Use for analysis" button from ballooning to 1900px on | |
| a wide monitor when its sibling chip strip is full-width. | |
| Override via `elem_classes=["dc-full-button"]` on buttons that DO | |
| want to span the row (Generate / Regenerate on the prompt card). */ | |
| .gr-button.lg, .gr-button-lg, button.lg, button.gr-button-large { | |
| max-width: 360px !important; | |
| margin-left: 0 !important; | |
| } | |
| .dc-full-button button, button.dc-full-button { | |
| max-width: 100% !important; | |
| width: 100% !important; | |
| } | |
| /* Primary "Use for analysis →" in the crate row — cap its growth */ | |
| .dc-crate-row .gr-button-primary, .dc-crate-row button[class*="primary"] { | |
| max-width: 320px !important; | |
| flex: 0 0 auto !important; | |
| } | |
| /* ---- Headings — Space Grotesk weight/letter-spacing per design */ | |
| h1, h2, h3 { | |
| font-family: 'Space Grotesk', sans-serif !important; | |
| font-weight: 600; | |
| letter-spacing: -0.01em; | |
| color: var(--ink); | |
| } | |
| h1 { font-size: 22px; letter-spacing: -0.02em; } | |
| h2 { font-size: 16px; } | |
| h3 { font-size: 13px; color: var(--ink2); } | |
| /* ---- Brand wordmark — used in top bar HTML block */ | |
| .dc-brand { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 9px; | |
| font-family: 'Space Grotesk', sans-serif; | |
| font-weight: 600; | |
| font-size: 16px; | |
| letter-spacing: -0.01em; | |
| color: var(--ink); | |
| } | |
| .dc-brand .dot { color: var(--mint); margin: 0 1px; } | |
| .dc-brand-bars { | |
| display: inline-flex; | |
| align-items: flex-end; | |
| gap: 2px; | |
| height: 18px; | |
| } | |
| .dc-brand-bars > span { width: 3px; border-radius: 1px; display: inline-block; } | |
| .dc-brand-bars .b1 { height: 7px; background: var(--coral); } | |
| .dc-brand-bars .b2 { height: 16px; background: var(--coral); } | |
| .dc-brand-bars .b3 { height: 10px; background: var(--mint); } | |
| .dc-brand-bars .b4 { height: 13px; background: var(--mint); } | |
| /* ---- Palette toggle — circular swatch buttons. Each circle's interior | |
| paints the palette's paper color, the border ring uses that palette's | |
| accent (coral for Console, amber for Dub). Active = brighter ring + | |
| outer halo + faint scale; inactive dims to 55% so the active swatch | |
| reads loud. Hover restores full opacity. JS in the palette-swap | |
| script (top of app) updates body class + persists to localStorage. */ | |
| .dc-palette-toggle { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| user-select: none; | |
| padding: 0 4px; | |
| } | |
| .dc-palette-toggle button { | |
| appearance: none !important; | |
| cursor: pointer !important; | |
| padding: 0 !important; | |
| width: 26px !important; | |
| height: 26px !important; | |
| flex-shrink: 0 !important; | |
| border-radius: 50% !important; | |
| background: var(--swatch-fill, var(--coral)) !important; | |
| border: 1.5px solid rgba(255,255,255,0.22) !important; | |
| box-shadow: none; | |
| opacity: 0.7; | |
| transition: opacity 120ms ease, transform 120ms ease, box-shadow 160ms ease; | |
| } | |
| .dc-palette-toggle button[data-pal="console"] { | |
| /* Console = coral solid (matches the brand's primary axis) */ | |
| --swatch-fill: #FF6A3D; | |
| } | |
| .dc-palette-toggle button[data-pal="dub"] { | |
| /* Dub = amber solid (the anchor / vinyl colour) */ | |
| --swatch-fill: #FFB94B; | |
| } | |
| .dc-palette-toggle button:hover { opacity: 0.95; transform: scale(1.06); } | |
| .dc-palette-toggle button.active { | |
| opacity: 1; | |
| transform: scale(1.10); | |
| border-color: rgba(255,255,255,0.45) !important; | |
| box-shadow: 0 0 0 2px var(--paper), | |
| 0 0 0 4px color-mix(in srgb, var(--swatch-fill) 70%, transparent), | |
| 0 6px 16px color-mix(in srgb, var(--swatch-fill) 28%, transparent); | |
| } | |
| /* Strip the Gradio .block chrome from gr.HTML elements that hold pure | |
| script/style tags. They were rendering as visible blank bars because | |
| Gradio still paints the block's background/border even when there's | |
| no DOM content. */ | |
| .dc-script-only, | |
| .dc-script-only > .block, | |
| .dc-script-only > div, | |
| .gradio-html.dc-script-only { | |
| display: none !important; | |
| } | |
| /* Off-screen hide for the wallet-bridge textbox. NOT display:none and | |
| NOT visible=False on the gr.Textbox — both pull the element out of | |
| the DOM, so the popup OAuth bridge JS can't find it to write the | |
| sk_... key. Off-screen + 1px + opacity 0 keeps the input mounted, | |
| focusable by JS, and addressable by getElementById('wallet-bridge-key'). */ | |
| .dc-wallet-bridge-hidden { | |
| position: fixed !important; | |
| left: -9999px !important; | |
| top: auto !important; | |
| width: 1px !important; | |
| height: 1px !important; | |
| overflow: hidden !important; | |
| opacity: 0 !important; | |
| pointer-events: none !important; | |
| } | |
| /* ---- Pollen balance pill — doubles as wallet button */ | |
| .dc-pollen-pill { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 7px; | |
| background: var(--input); | |
| border: 1px solid var(--chipline); | |
| border-radius: 20px; | |
| padding: 6px 13px; | |
| cursor: pointer; | |
| transition: border-color 120ms, background 120ms; | |
| user-select: none; | |
| } | |
| .dc-pollen-pill:hover { border-color: var(--coral); } | |
| .dc-pollen-pill .diamond { color: var(--amber); font-size: 13px; line-height: 1; } | |
| .dc-pollen-pill .balance { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-weight: 600; font-size: 13px; color: var(--ink); | |
| } | |
| .dc-pollen-pill .lbl { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-weight: 500; font-size: 10px; color: var(--label); | |
| } | |
| /* Not-connected state — coral CTA tint to invite the click */ | |
| .dc-pollen-pill.not-connected { | |
| background: var(--coralTint); | |
| border-color: var(--coral); | |
| } | |
| .dc-pollen-pill.not-connected:hover { | |
| background: var(--coralTintHover); | |
| border-color: var(--coral); | |
| } | |
| .dc-pollen-pill.not-connected .diamond { color: var(--coralb); } | |
| .dc-pollen-pill.not-connected .balance { color: var(--coralb); } | |
| .dc-pollen-pill.not-connected .lbl { color: var(--coral); opacity: 0.7; } | |
| /* ---- Session spend pill — running total ◆ pollen this visit. Sits to the | |
| left of the wallet pill. Muted by default (it's reference info, not | |
| a CTA); only shown when there's actual spend > 0. */ | |
| .dc-session-pill { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 4px 10px; | |
| background: transparent; | |
| border: 1px solid var(--line2); | |
| border-radius: 14px; | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 11px; | |
| color: var(--label); | |
| user-select: none; | |
| } | |
| .dc-session-pill .amt { font-weight: 600; color: var(--ink3); } | |
| .dc-session-pill .diamond { color: var(--amber); opacity: 0.7; font-size: 11px; } | |
| .dc-session-pill .lbl { font-size: 9px; letter-spacing: 0.08em; text-transform: uppercase; } | |
| /* ---- Inline cost pip — small inline annotation next to the Generate / | |
| Regenerate buttons. Reads "~0.04 ◆ per call · flat". */ | |
| .dc-cost-pip { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 5px; | |
| padding: 4px 10px; | |
| background: var(--paper3); | |
| border: 1px solid var(--line2); | |
| border-radius: 7px; | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 11px; | |
| color: var(--ink3); | |
| margin: 8px 0; | |
| width: fit-content; | |
| } | |
| .dc-cost-pip .amt { color: var(--ink2); font-weight: 600; } | |
| .dc-cost-pip .diamond { color: var(--amber); } | |
| .dc-cost-pip .sep { color: var(--ink3); } | |
| /* ---- Variation HOLD chips — placeholder for the CFG control Pollinations | |
| doesn't expose yet. Entire group disabled at 0.45 opacity so users | |
| can see the shape coming, but can't click. Reactivate when Pollinations | |
| adds a CFG/guidance param to /audio/{text}. */ | |
| .dc-var-hold { | |
| opacity: 0.45; | |
| pointer-events: none; | |
| user-select: none; | |
| margin: 10px 0; | |
| } | |
| .dc-var-hold .head { | |
| display: flex; align-items: center; gap: 8px; | |
| margin-bottom: 6px; | |
| } | |
| .dc-var-hold .head .label { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 11px; | |
| font-weight: 600; | |
| letter-spacing: 0.12em; | |
| text-transform: uppercase; | |
| color: var(--label); | |
| } | |
| .dc-var-hold .head .tag { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 10px; | |
| color: var(--label); | |
| padding: 2px 8px; | |
| border: 1px solid var(--line2); | |
| border-radius: 6px; | |
| background: var(--paper3); | |
| } | |
| .dc-var-hold .chips { display: flex; gap: 6px; } | |
| /* ---- Two-column regen controls — Variant Duration | Variants ------- */ | |
| .dc-regen-control-row { | |
| align-items: stretch !important; | |
| gap: 16px !important; | |
| margin-top: 10px !important; | |
| } | |
| .dc-regen-control-card { | |
| border: 1px solid var(--line) !important; | |
| border-radius: 12px !important; | |
| background: var(--paper2) !important; | |
| padding: 14px 16px !important; | |
| } | |
| .dc-regen-control-card .dc-label { | |
| margin-bottom: 8px; | |
| } | |
| /* Suppress the giant default radio fieldset margin so the choices sit | |
| right under the label inside the card. */ | |
| .dc-regen-control-card fieldset { | |
| margin-top: 0 !important; | |
| } | |
| /* The .info text under each radio (small grey hint) keeps a tight | |
| line-height so the cards stay roughly equal in height. */ | |
| .dc-regen-control-card .info { | |
| margin-top: 8px !important; | |
| line-height: 1.35 !important; | |
| } | |
| /* ---- TRANSFORM tab engine selector — Morpho vs Local SA3 ---------- */ | |
| .dc-transform-engine .wrap, | |
| .dc-transform-engine > div { | |
| display: flex !important; | |
| flex-direction: row !important; | |
| gap: 8px !important; | |
| margin: 0 0 14px !important; | |
| } | |
| .dc-transform-engine label { | |
| flex: 1 1 0 !important; | |
| padding: 10px 14px !important; | |
| border: 1px solid var(--chipline) !important; | |
| border-radius: 10px !important; | |
| background: var(--paper2) !important; | |
| color: var(--ink2) !important; | |
| font-family: 'JetBrains Mono', monospace !important; | |
| font-size: 12px !important; | |
| cursor: pointer !important; | |
| transition: border-color 120ms, color 120ms, background 120ms !important; | |
| } | |
| .dc-transform-engine label:hover { | |
| border-color: var(--ink3) !important; | |
| color: var(--ink) !important; | |
| } | |
| .dc-transform-engine label.selected { | |
| border-color: var(--coral) !important; | |
| color: var(--coral) !important; | |
| background: color-mix(in srgb, var(--coral) 10%, var(--paper2)) !important; | |
| } | |
| .dc-transform-engine input[type="radio"] { | |
| display: none !important; | |
| } | |
| /* ---- Regen history tabs — radio rendered as a horizontal tab strip | |
| above the variants row. Each tab is one saved regen batch | |
| (`MATCH·1`, `LOOSE·1`, `MATCH·2`, etc.). Click → replay that | |
| batch into the variants cards + prompt-used card. */ | |
| .dc-regen-tabs .wrap, | |
| .dc-regen-tabs > div { | |
| display: flex !important; | |
| flex-direction: row !important; | |
| flex-wrap: wrap !important; | |
| gap: 6px !important; | |
| margin: 14px 0 6px !important; | |
| } | |
| .dc-regen-tabs label { | |
| padding: 6px 12px !important; | |
| background: var(--paper3) !important; | |
| border: 1px solid var(--line) !important; | |
| border-radius: 6px !important; | |
| font-family: 'JetBrains Mono', monospace !important; | |
| font-size: 11px !important; | |
| font-weight: 600 !important; | |
| letter-spacing: 0.08em !important; | |
| color: var(--ink3) !important; | |
| cursor: pointer !important; | |
| transition: background 120ms, border-color 120ms, color 120ms; | |
| } | |
| .dc-regen-tabs label:hover { | |
| background: var(--input) !important; | |
| color: var(--ink) !important; | |
| border-color: var(--coral) !important; | |
| } | |
| .dc-regen-tabs label.selected { | |
| background: var(--coralTint) !important; | |
| border-color: color-mix(in srgb, var(--coral) 60%, transparent) !important; | |
| color: var(--coralb) !important; | |
| } | |
| .dc-regen-tabs input[type="radio"] { | |
| display: none !important; | |
| } | |
| /* ---- Variants prompt-used card — sits above the ANCHOR + 5 VARIANTS | |
| title and echoes the derived prompt that produced the batch. Helps | |
| users see at a glance which lens / wording landed which 5 gens. */ | |
| .dc-variants-prompt-card { | |
| background: var(--paper3); | |
| border: 1px solid var(--line); | |
| border-radius: 8px; | |
| padding: 10px 14px; | |
| margin: 14px 0 6px; | |
| } | |
| .dc-variants-prompt-card .head { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 10px; | |
| font-weight: 600; | |
| letter-spacing: 0.12em; | |
| color: var(--label); | |
| margin-bottom: 6px; | |
| } | |
| .dc-variants-prompt-card .body { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 11px; | |
| line-height: 1.55; | |
| color: var(--ink2); | |
| white-space: pre-wrap; | |
| } | |
| /* ---- Variant cards (anchor + 5 variants in one row) — each is a | |
| Column containing a custom HTML title + an Audio component, so | |
| they all line up at the same height regardless of how the audio | |
| widget grows. Title carries the BPM/Harm/Rhyt bar breakdown on | |
| three left-aligned lines + the v-name in the bottom-right. */ | |
| .dc-variant-cell { | |
| min-width: 0; /* let columns shrink in a tight row */ | |
| } | |
| .dc-variant-title { | |
| background: var(--paper3); | |
| border: 1px solid var(--line); | |
| border-radius: 8px 8px 0 0; | |
| border-bottom: none; | |
| padding: 6px 10px; | |
| min-height: 28px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| gap: 8px; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| } | |
| .dc-variant-title .bars { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 10px; | |
| line-height: 1.2; | |
| color: var(--label); /* track behind the meter — muted grey */ | |
| text-align: left; | |
| flex: 1; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| .dc-variant-title .bars .fill { | |
| color: var(--mint); /* mint — measured axis, the metering colour */ | |
| font-weight: 600; | |
| } | |
| .dc-variant-title .bars.placeholder { | |
| color: var(--ink3); /* faded — pre-regen state */ | |
| } | |
| .dc-variant-title .bars.anchor-label { | |
| font-size: 12px; | |
| color: var(--amber); /* amber for the anchor */ | |
| font-weight: 600; | |
| letter-spacing: 0.05em; | |
| } | |
| .dc-variant-title .name { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 11px; | |
| color: var(--label); | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.08em; | |
| } | |
| /* The Audio sits directly under the title — kill its rounded top so | |
| they read as one card. */ | |
| .dc-variant-cell .audio-container, | |
| .dc-variant-cell [class*="audio"] > div:first-child { | |
| border-top-left-radius: 0 !important; | |
| border-top-right-radius: 0 !important; | |
| border-top: none !important; | |
| } | |
| /* ---- Prompt-lens chip row (Analysis tab, above derived prompt textbox). | |
| Three radio options rendered as horizontally-arranged chips that | |
| indicate decreasing fidelity to the measured arc via the leading | |
| freedom-bars glyphs (▮▮▮▮ MATCH / ▮▮▯▯ LOOSE / ▮▯▯▯ FREE). Visual | |
| coherence with the variant card bars is intentional — both use the | |
| same glyph language for "how much constraint the LLM has". */ | |
| .dc-lens-chips .wrap, | |
| .dc-lens-chips > div { | |
| display: flex !important; | |
| flex-direction: row !important; | |
| gap: 8px !important; | |
| margin: 4px 0 12px !important; | |
| } | |
| .dc-lens-chips label { | |
| flex: 1 !important; | |
| padding: 8px 12px !important; | |
| background: var(--paper3) !important; | |
| border: 1px solid var(--line) !important; | |
| border-radius: 8px !important; | |
| cursor: pointer !important; | |
| font-family: 'JetBrains Mono', monospace !important; | |
| font-size: 11px !important; | |
| color: var(--ink2) !important; | |
| text-align: center !important; | |
| line-height: 1.45 !important; | |
| transition: background 120ms, border-color 120ms, color 120ms; | |
| } | |
| .dc-lens-chips label:hover { | |
| background: var(--input) !important; | |
| border-color: var(--coral) !important; | |
| color: var(--ink) !important; | |
| } | |
| .dc-lens-chips label.selected { | |
| background: var(--coralTint) !important; | |
| border-color: color-mix(in srgb, var(--coral) 60%, transparent) !important; | |
| color: var(--coralb) !important; | |
| } | |
| .dc-lens-chips input[type="radio"] { | |
| display: none !important; | |
| } | |
| /* ---- Anchor player (Analysis tab) — raw HTML5 <audio> wrapped in a | |
| compact card directly above the waveform image. We bypass gr.Audio | |
| here because Gradio v6's wavesurfer-based component exposes no | |
| <audio> element for external JS to seek. */ | |
| .dc-anchor-player { | |
| background: var(--paper2); | |
| border: 1px solid var(--line); | |
| border-radius: 12px; | |
| padding: 14px 16px; | |
| margin: 6px 0 12px; | |
| } | |
| .dc-anchor-player .head { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 12px; | |
| margin-bottom: 8px; | |
| } | |
| .dc-anchor-player .badge { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 11px; | |
| font-weight: 600; | |
| letter-spacing: 0.12em; | |
| color: var(--amber); | |
| } | |
| .dc-anchor-player .hint { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 10px; | |
| color: var(--label); | |
| } | |
| /* Dark theme the native <audio> controls — Chrome/Safari render player | |
| chrome differently but a filter inversion is the closest we get to | |
| matching the rest of the panel. Best-effort. */ | |
| .dc-anchor-player audio { | |
| filter: invert(0.85) hue-rotate(180deg) sepia(0.08); | |
| width: 100%; | |
| height: 36px; | |
| } | |
| /* ---- Section seek strip — clickable chips beneath the analysis | |
| waveform. Centred on the page, role-only text (time range is | |
| already in the waveform image above, repeating it as a subtitle | |
| was noise). Click handler seeks `#abv1-anchor` and plays. */ | |
| .dc-section-seek-strip { | |
| display: flex; | |
| gap: 8px; | |
| margin: 10px auto 18px; | |
| justify-content: center; | |
| flex-wrap: wrap; | |
| } | |
| button.dc-section-chip { | |
| cursor: pointer; | |
| background: var(--paper3); | |
| border: 1px solid var(--line2); | |
| border-radius: 8px; | |
| padding: 7px 18px; | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 11px; | |
| font-weight: 600; | |
| letter-spacing: 0.12em; | |
| transition: background 120ms, border-color 120ms; | |
| } | |
| button.dc-section-chip:hover { | |
| background: var(--input); | |
| } | |
| button.dc-section-chip:active { | |
| transform: scale(0.97); | |
| } | |
| /* ---- Validate-vs-Gemini button (Analysis tab) — outline style so it | |
| reads as a secondary CTA next to the brief, not a primary action. | |
| Mint border = measured-grounding promise; flips to coral on hover. */ | |
| .dc-validate-btn button, button.dc-validate-btn { | |
| background: transparent !important; | |
| border: 1px solid color-mix(in srgb, var(--mint) 45%, transparent) !important; | |
| color: var(--mint) !important; | |
| font-family: 'Space Grotesk', sans-serif !important; | |
| font-weight: 600 !important; | |
| font-size: 12px !important; | |
| padding: 6px 14px !important; | |
| border-radius: 8px !important; | |
| height: auto !important; | |
| min-height: 32px !important; | |
| width: fit-content !important; | |
| max-width: 220px !important; | |
| margin: 4px 0 12px !important; | |
| } | |
| .dc-validate-btn button:hover, button.dc-validate-btn:hover { | |
| background: var(--coralTint) !important; | |
| border-color: var(--coralb) !important; | |
| color: var(--coralb) !important; | |
| } | |
| .dc-validate-btn button:disabled, button.dc-validate-btn:disabled { | |
| opacity: 0.35 !important; | |
| cursor: not-allowed !important; | |
| } | |
| .dc-var-hold .chip { | |
| flex: 1; | |
| text-align: center; | |
| padding: 8px 10px; | |
| background: var(--paper3); | |
| border: 1px solid var(--line2); | |
| border-radius: 8px; | |
| font-family: 'Space Grotesk', sans-serif; | |
| font-weight: 600; | |
| font-size: 12px; | |
| color: var(--ink3); | |
| } | |
| /* Hidden trigger button — never visible but JS-clickable from the pill */ | |
| #wallet-disconnect-trigger { display: none !important; } | |
| /* ---- Icon button — single-glyph buttons like ✕ delete, ⟳ refresh */ | |
| .dc-icon-btn button, button.dc-icon-btn { | |
| background: var(--input) !important; | |
| border: 1px solid var(--chipline) !important; | |
| color: var(--ink3) !important; | |
| font-size: 16px !important; | |
| padding: 0 !important; | |
| min-width: 42px !important; | |
| width: 42px !important; | |
| height: 42px !important; | |
| border-radius: 8px !important; | |
| display: inline-flex !important; | |
| align-items: center !important; | |
| justify-content: center !important; | |
| } | |
| .dc-icon-btn button:hover, button.dc-icon-btn:hover { | |
| color: var(--coral) !important; | |
| border-color: var(--coral) !important; | |
| } | |
| /* ---- Crate row — primary + icon stay on one line under the chip strip */ | |
| .dc-crate-row { align-items: center !important; gap: 10px !important; margin-top: 6px !important; } | |
| /* ---- Crate chip strip — Radio rendered as horizontal scrollable tiles. | |
| This is what makes the collection visible at a glance instead of | |
| hiding behind a dropdown. Each tile is a tap-sized chip; the row | |
| scrolls horizontally on overflow (touch + wheel + scrollbar). | |
| Gradio v6 dropped the intermediate `.wrap` div the v5 CSS targeted, | |
| so we also flex the Radio's direct children + cascade label rules | |
| from .dc-crate-strip itself. Belt and braces. */ | |
| .dc-crate-strip > div, | |
| .dc-crate-strip .wrap { | |
| display: flex !important; | |
| flex-direction: row !important; | |
| flex-wrap: nowrap !important; | |
| overflow-x: auto !important; | |
| gap: 8px !important; | |
| padding: 4px 2px 10px !important; | |
| scrollbar-width: thin; | |
| } | |
| .dc-crate-strip label, | |
| .dc-crate-strip .wrap label { | |
| flex: none !important; | |
| text-transform: none !important; | |
| letter-spacing: 0 !important; | |
| font-family: 'JetBrains Mono', monospace !important; | |
| font-weight: 500 !important; | |
| font-size: 11px !important; | |
| padding: 10px 14px !important; | |
| min-width: 200px !important; | |
| max-width: 240px !important; | |
| background: var(--input) !important; | |
| /* Left edge accent — coral for root tiles; amber for variants | |
| (set via the .variant class tagged by the post-render JS below). */ | |
| border: 1px solid var(--line) !important; | |
| border-left: 3px solid var(--coral) !important; | |
| border-radius: 8px !important; | |
| color: var(--ink2) !important; | |
| cursor: pointer !important; | |
| line-height: 1.4 !important; | |
| text-align: left !important; | |
| white-space: normal !important; | |
| overflow: hidden !important; | |
| text-overflow: ellipsis !important; | |
| display: -webkit-box !important; | |
| -webkit-line-clamp: 2 !important; | |
| -webkit-box-orient: vertical !important; | |
| /* Kill any inherited gap/indent. Gradio v6 sets gap:8px on the radio | |
| label (so the hidden input sits next to the span). With the input | |
| display:none the gap remained as a leading dead space before the | |
| text — visible as the ~16px indent before `[39cc30]`. */ | |
| gap: 0 !important; | |
| text-indent: 0 !important; | |
| } | |
| .dc-crate-strip label > span, | |
| .dc-crate-strip .wrap label > span { | |
| /* Defensive: the span carrying the chip text shouldn't bring its own | |
| margin. Some Gradio v6 builds emit `margin-left: 6px` on it. */ | |
| margin: 0 !important; | |
| padding: 0 !important; | |
| } | |
| /* Variant chips — JS adds .variant class on labels containing ↳ */ | |
| .dc-crate-strip label.variant, | |
| .dc-crate-strip .wrap label.variant { | |
| border-left-color: var(--amber) !important; /* amber, matches anchor */ | |
| color: var(--ink2) !important; | |
| } | |
| .dc-crate-strip label.selected, | |
| .dc-crate-strip .wrap label.selected { | |
| background: var(--coralTint) !important; | |
| border-color: color-mix(in srgb, var(--coral) 55%, transparent) !important; | |
| border-left-color: var(--coral) !important; | |
| color: var(--coralb) !important; | |
| } | |
| .dc-crate-strip label.selected.variant, | |
| .dc-crate-strip .wrap label.selected.variant { | |
| background: var(--amberTint) !important; | |
| border-color: color-mix(in srgb, var(--amber) 55%, transparent) !important; | |
| border-left-color: var(--amber) !important; | |
| color: var(--amber) !important; | |
| } | |
| .dc-crate-strip label:hover, | |
| .dc-crate-strip .wrap label:hover { | |
| border-color: var(--coral) !important; | |
| } | |
| .dc-crate-strip label.variant:hover, | |
| .dc-crate-strip .wrap label.variant:hover { | |
| border-color: var(--amber) !important; | |
| } | |
| /* Hide the radio input bullets — Gradio v6 sometimes renders them | |
| visibly even when CSS-styled-as-chips, which adds a stray dot. */ | |
| .dc-crate-strip input[type="radio"] { | |
| display: none !important; | |
| } | |
| /* ---- Avatar circle (gradient coral→mint) */ | |
| .dc-avatar { | |
| width: 30px; height: 30px; border-radius: 50%; | |
| background: linear-gradient(135deg, var(--coral), var(--mint)); | |
| display: inline-block; | |
| } | |
| /* ---- Top bar layout — used by T1b */ | |
| .dc-topbar { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| height: 52px; | |
| padding: 0 20px; | |
| /* Flat: just a faint hairline, no background fill. Designer cleanup — | |
| the heavy paper2 band made the header feel walled off from the | |
| content underneath. */ | |
| border-bottom: 1px solid var(--line2); | |
| background: transparent; | |
| margin: -14px -18px 18px; | |
| border-radius: 0; | |
| } | |
| /* Strip the Gradio .block chrome from the topbar's gr.HTML wrapper — | |
| otherwise the block's own background/border peeks out as a darker | |
| rounded rectangle behind the dc-topbar div. We use the elem_id rather | |
| than walking the DOM tree so this can't drift when Gradio re-renders. */ | |
| #dc-topbar-wrap, #dc-topbar-wrap .block, #dc-topbar-wrap > div { | |
| background: transparent !important; | |
| border: 0 !important; | |
| box-shadow: none !important; | |
| padding: 0 !important; | |
| border-radius: 0 !important; | |
| } | |
| /* ---- Uppercase mono section label */ | |
| .dc-label { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 11px; | |
| font-weight: 600; | |
| letter-spacing: 0.12em; | |
| text-transform: uppercase; | |
| color: var(--label); | |
| } | |
| .dc-label.coral { color: var(--coralb); } | |
| .dc-label.mint { color: var(--mint); } | |
| .dc-label.amber { color: var(--amber); } | |
| /* ---- Metric tile grid — T1c */ | |
| .dc-metric-grid { | |
| display: grid; | |
| grid-template-columns: repeat(6, 1fr); | |
| gap: 10px; | |
| margin: 14px 0; | |
| } | |
| @media (max-width: 900px) { | |
| .dc-metric-grid { grid-template-columns: repeat(3, 1fr); } | |
| } | |
| .dc-metric { | |
| background: var(--paper2); | |
| border: 1px solid var(--line); | |
| border-radius: 10px; | |
| padding: 12px 13px; | |
| } | |
| .dc-metric-k { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 10px; | |
| font-weight: 500; | |
| letter-spacing: 0.1em; | |
| color: var(--label); | |
| text-transform: uppercase; | |
| } | |
| .dc-metric-v { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 21px; | |
| font-weight: 600; | |
| color: var(--ink); | |
| margin-top: 8px; | |
| line-height: 1.1; | |
| } | |
| /* ---- Inline-coloured numbers in brief paragraph — T1 design polish */ | |
| .dc-coral { color: var(--coralb); } | |
| .dc-mint { color: var(--mint); } | |
| .dc-amber { color: var(--amber); } | |
| .dc-cream { color: var(--ink); font-weight: 600; } | |
| /* ---- Coral-bordered derived prompt card — T1d */ | |
| .dc-derived-card { | |
| background: var(--paper3); | |
| border: 1px solid color-mix(in srgb, var(--coral) 40%, transparent); | |
| border-radius: 14px; | |
| padding: 18px; | |
| margin-top: 14px; | |
| } | |
| .dc-derived-card .dc-prompt-box { | |
| background: var(--paper); | |
| border: 1px solid var(--line); | |
| border-radius: 10px; | |
| padding: 13px; | |
| font-size: 14px; | |
| line-height: 1.55; | |
| color: var(--ink); | |
| margin-top: 12px; | |
| } | |
| /* ---- Chip toggles (modifier pills below derived prompt) — T1d */ | |
| .dc-chips { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 7px; | |
| margin-top: 13px; | |
| } | |
| .dc-chip { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 11px; | |
| font-weight: 500; | |
| padding: 5px 11px; | |
| border-radius: 14px; | |
| background: var(--input); | |
| border: 1px solid var(--chipline); | |
| color: var(--ink2); | |
| cursor: pointer; | |
| user-select: none; | |
| } | |
| .dc-chip:hover { border-color: var(--coral); color: var(--coralb); } | |
| .dc-chip.active { background: var(--coralTintHover); color: var(--coralb); border-color: color-mix(in srgb, var(--coral) 55%, transparent); } | |
| /* ---- Variant card (5-grid below composer) — T1e */ | |
| .dc-variant-grid { | |
| display: grid; | |
| grid-template-columns: repeat(5, 1fr); | |
| gap: 12px; | |
| margin-top: 14px; | |
| } | |
| @media (max-width: 1100px) { | |
| .dc-variant-grid { grid-template-columns: repeat(2, 1fr); } | |
| } | |
| .dc-variant { | |
| background: var(--paper3); | |
| border: 1px solid var(--line); | |
| border-radius: 12px; | |
| padding: 13px; | |
| } | |
| .dc-variant.anchor { | |
| background: var(--amberTint); | |
| border-color: color-mix(in srgb, var(--amber) 50%, transparent); | |
| } | |
| .dc-variant .match { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 10px; | |
| font-weight: 600; | |
| } | |
| .dc-variant .match.high { color: var(--mint); } | |
| .dc-variant .match.mid { color: var(--coralb); } | |
| .dc-variant .match.low { color: var(--ink3); } | |
| /* ---- Crate dock — T2a horizontal strip at bottom of work area */ | |
| .dc-crate-dock { | |
| background: var(--paper2); | |
| border-top: 1px solid var(--line2); | |
| padding: 13px 18px; | |
| margin: 14px -18px -14px; /* extend to container edges */ | |
| } | |
| .dc-crate-strip { | |
| display: flex; | |
| gap: 11px; | |
| overflow-x: auto; | |
| padding-bottom: 4px; | |
| } | |
| .dc-tile { | |
| flex: none; | |
| width: 170px; | |
| border-radius: 10px; | |
| padding: 11px; | |
| cursor: pointer; | |
| border: 1px solid var(--line); | |
| background: var(--input); | |
| } | |
| .dc-tile.selected { | |
| border-color: color-mix(in srgb, var(--coral) 55%, transparent); | |
| background: var(--coralTint); | |
| } | |
| .dc-tile.anchor { | |
| border-color: color-mix(in srgb, var(--amber) 50%, transparent); | |
| background: var(--amberTint); | |
| } | |
| /* ---- Tab pill row (Gradio v6 uses .tab-container .tab-wrapper) */ | |
| .tab-wrapper { | |
| border-bottom: none !important; | |
| margin-bottom: 14px !important; | |
| } | |
| .tab-container { | |
| display: inline-flex !important; | |
| background: var(--paper3) !important; | |
| border: 1px solid var(--line2) !important; | |
| border-radius: 10px !important; | |
| padding: 4px !important; | |
| gap: 4px !important; | |
| box-shadow: none !important; | |
| } | |
| .tab-container button { | |
| background: transparent !important; | |
| border: none !important; | |
| border-bottom: none !important; | |
| color: var(--ink3) !important; | |
| padding: 7px 16px !important; | |
| border-radius: 7px !important; | |
| font-family: 'Space Grotesk', sans-serif !important; | |
| font-weight: 500 !important; | |
| font-size: 13px !important; | |
| box-shadow: none !important; | |
| transition: background 120ms, color 120ms; | |
| } | |
| .tab-container button.selected { | |
| background: var(--coralTintHover) !important; | |
| color: var(--coralb) !important; | |
| border-bottom: none !important; | |
| outline: none !important; | |
| } | |
| /* Kill Gradio's mint underline on the selected tab (it's a ::after bar) */ | |
| .tab-container button::after, | |
| .tab-container button.selected::after { | |
| display: none !important; | |
| content: none !important; | |
| background: transparent !important; | |
| } | |
| .tab-container button:hover:not(.selected) { | |
| color: var(--ink2) !important; | |
| } | |
| /* Tab content panel — strip Gradio's default container chrome */ | |
| .tabitem { | |
| border: none !important; | |
| padding: 18px 0 !important; | |
| background: transparent !important; | |
| } | |
| /* Compare-tab C-column caption — sits BELOW the dropdown so the three | |
| dropdowns stay top-aligned. Muted small text styled like field info. */ | |
| .dc-compare-c-caption { | |
| margin-top: 4px !important; | |
| } | |
| .dc-compare-c-caption p, .dc-compare-c-caption em { | |
| font-size: 11px !important; | |
| color: var(--ink3) !important; | |
| line-height: 1.4 !important; | |
| margin: 0 !important; | |
| } | |
| /* ---- Local-server status pill + setup card -------------------------- */ | |
| /* Iframe warning — shown when the Space is loaded via huggingface.co's | |
| wrapper, which silently blocks browser fetches to localhost. The JS | |
| in app.py reveals this block when window.top !== window.self and | |
| fills the link href with the current .hf.space origin. */ | |
| .dc-local-iframe-warn { | |
| padding: 12px 14px; margin: 0 0 12px; | |
| border-radius: 10px; | |
| background: color-mix(in srgb, var(--amber, #f4b860) 12%, var(--paper3)); | |
| border: 1px solid color-mix(in srgb, var(--amber, #f4b860) 45%, transparent); | |
| color: var(--ink); font-size: 12.5px; line-height: 1.5; | |
| font-family: 'JetBrains Mono', monospace; | |
| } | |
| .dc-local-iframe-warn strong { color: var(--amber, #f4b860); font-weight: 600; } | |
| .dc-local-iframe-warn a { | |
| color: var(--ink); text-decoration: underline; | |
| text-underline-offset: 3px; font-weight: 600; | |
| } | |
| .dc-local-iframe-warn a:hover { color: var(--amber, #f4b860); } | |
| .dc-local-status { | |
| display: flex; align-items: center; gap: 10px; | |
| padding: 10px 14px; margin: 0 0 12px; | |
| border-radius: 10px; font-family: 'JetBrains Mono', monospace; | |
| font-size: 12px; color: var(--ink2); | |
| background: var(--paper3); border: 1px solid var(--line); | |
| } | |
| .dc-local-status .dot { font-size: 10px; line-height: 1; } | |
| .dc-local-status .msg { flex: 1; } | |
| .dc-local-status button { | |
| appearance: none; background: transparent; border: 1px solid var(--chipline); | |
| color: var(--ink3); font-family: 'JetBrains Mono', monospace; font-size: 10px; | |
| padding: 4px 10px; border-radius: 6px; cursor: pointer; | |
| letter-spacing: 0.05em; text-transform: uppercase; | |
| } | |
| .dc-local-status button:hover { color: var(--ink); border-color: var(--ink3); } | |
| .dc-local-status.detected { border-color: color-mix(in srgb, var(--mint) 35%, transparent); } | |
| .dc-local-status.detected .dot { color: var(--mint); } | |
| .dc-local-status.detected .msg { color: var(--mint); } | |
| .dc-local-status.not-detected .dot { color: var(--ink3); } | |
| .dc-local-status.not-detected .msg { color: var(--ink3); } | |
| .dc-local-setup { display: flex; flex-direction: column; gap: 8px; padding: 4px 0; } | |
| .dc-local-setup .step { | |
| display: flex; align-items: center; gap: 8px; margin-top: 10px; | |
| font-family: 'JetBrains Mono', monospace; font-size: 11px; | |
| color: var(--label); text-transform: uppercase; letter-spacing: 0.1em; | |
| } | |
| .dc-local-setup .step .num { | |
| display: inline-flex; align-items: center; justify-content: center; | |
| width: 18px; height: 18px; border-radius: 50%; | |
| background: var(--coralTint); color: var(--coral); font-weight: 600; | |
| font-size: 10px; | |
| } | |
| .dc-local-setup .hdr { color: var(--ink2); font-weight: 600; } | |
| .dc-local-setup .code-row { | |
| display: flex; align-items: stretch; gap: 6px; | |
| background: var(--paper3); border: 1px solid var(--line); | |
| border-radius: 8px; padding: 8px 10px; | |
| } | |
| .dc-local-setup .code-row code { | |
| flex: 1; font-family: 'JetBrains Mono', monospace; font-size: 11px; | |
| color: var(--ink); white-space: pre-wrap; word-break: break-all; | |
| background: transparent; padding: 0; | |
| } | |
| .dc-local-setup .code-row .copy { | |
| flex: 0 0 auto; background: var(--paper2); border: 1px solid var(--chipline); | |
| color: var(--ink2); font-family: 'JetBrains Mono', monospace; font-size: 10px; | |
| padding: 0 10px; border-radius: 5px; cursor: pointer; | |
| } | |
| .dc-local-setup .code-row .copy:hover { color: var(--ink); } | |
| .dc-local-setup .hint { | |
| font-family: 'Space Grotesk', sans-serif; font-size: 11px; | |
| color: var(--ink3); line-height: 1.4; | |
| } | |
| .dc-local-setup .hint code { | |
| font-family: 'JetBrains Mono', monospace; font-size: 10px; | |
| color: var(--ink2); background: var(--paper3); padding: 1px 4px; | |
| border-radius: 3px; | |
| } | |
| .dc-local-setup .dl { | |
| background: var(--coral); color: var(--onCoral); border: 0; | |
| padding: 8px 12px; border-radius: 8px; cursor: pointer; | |
| font-family: 'Space Grotesk', sans-serif; font-weight: 600; | |
| font-size: 12px; align-self: flex-start; | |
| } | |
| .dc-local-setup .dl:hover { background: var(--coralb); } | |
| /* ---- Buttons — tighter padding, sharper shadows */ | |
| .gr-button { | |
| font-family: 'Space Grotesk', sans-serif !important; | |
| font-weight: 600 !important; | |
| letter-spacing: 0 !important; | |
| box-shadow: none !important; | |
| transition: all 120ms ease !important; | |
| } | |
| .gr-button.gr-button-primary { | |
| background: var(--coral) !important; | |
| color: var(--onCoral) !important; | |
| border: none !important; | |
| } | |
| .gr-button.gr-button-primary:hover { | |
| background: var(--coralb) !important; | |
| } | |
| .gr-button.gr-button-secondary { | |
| background: var(--input) !important; | |
| border: 1px solid var(--chipline) !important; | |
| color: var(--ink2) !important; | |
| } | |
| /* ---- Field titles — Gradio v6 emits span.has-info inside the block. | |
| Pulled out of grey into a soft coral so the page reads as 'three | |
| sections, primary action' rather than a uniform grey wash. The | |
| coralb shade keeps the contrast on dark surfaces comfortable. */ | |
| span.has-info { | |
| color: var(--coralb) !important; | |
| font-family: 'JetBrains Mono', monospace !important; | |
| font-size: 11px !important; | |
| font-weight: 600 !important; | |
| letter-spacing: 0.10em !important; | |
| text-transform: uppercase !important; | |
| margin-bottom: 6px !important; | |
| display: block !important; | |
| opacity: 0.85; | |
| } | |
| /* Info subtitle below field title — quieter. | |
| Targets both Gradio v6's `.info-text` (Slider/Number) and `.info` (other). */ | |
| .info, .info-text { | |
| color: var(--label) !important; | |
| font-size: 11px !important; | |
| font-family: 'Space Grotesk', sans-serif !important; | |
| text-transform: none !important; | |
| letter-spacing: 0 !important; | |
| font-weight: 400 !important; | |
| line-height: 1.45 !important; | |
| } | |
| /* Radio choice labels */ | |
| .wrap label { | |
| background: var(--input) !important; | |
| border: 1px solid var(--chipline) !important; | |
| border-radius: 7px !important; | |
| color: var(--ink3) !important; | |
| font-family: 'JetBrains Mono', monospace !important; | |
| font-size: 11px !important; | |
| font-weight: 500 !important; | |
| letter-spacing: 0.04em !important; | |
| text-transform: uppercase !important; | |
| padding: 6px 12px !important; | |
| } | |
| .wrap label.selected { | |
| background: var(--coralTintHover) !important; | |
| border-color: color-mix(in srgb, var(--coral) 55%, transparent) !important; | |
| color: var(--coralb) !important; | |
| } | |
| /* ---- Generate-tab prompt textbox — coral left-stripe marks it as the | |
| primary input of the page (DAW-style track identity). All other | |
| inputs (model dropdown, duration radio) stay neutral so the eye | |
| lands on the prompt first. */ | |
| #gen-prompt-box { | |
| border-left: 3px solid var(--coral) !important; | |
| background: linear-gradient(180deg, | |
| color-mix(in srgb, var(--coral) 4%, var(--paper3)), | |
| var(--paper3)) !important; | |
| } | |
| #gen-prompt-box textarea { | |
| background: transparent !important; | |
| } | |
| /* ---- ANALYSE Start band — two equal cards, side-by-side. Not | |
| accordions: bordered Columns + title + subtitle inside, so the | |
| page reads as "pick an input" rather than "accordion management." | |
| Tight padding + compact internals so both cards + settings + | |
| Analyze + Validate/Share row fit the first viewport. | |
| align-items: flex-start so each card sits at its natural height. | |
| Left card has settings + Analyze (tall); right card just has the | |
| drop zone (short). Stretching the right card to match the left's | |
| height made it read as a huge empty panel — Codex's call. */ | |
| .dc-analyse-ingest-row { | |
| align-items: flex-start !important; | |
| } | |
| .dc-ingest-card { | |
| border: 1px solid var(--line) !important; | |
| border-radius: 12px !important; | |
| background: var(--paper2) !important; | |
| padding: 12px 14px !important; | |
| gap: 8px !important; | |
| } | |
| /* Trim internal spacing on grouped Gradio elements inside the card | |
| so the BPM/Genre/Model stack doesn't push Analyze below the fold. */ | |
| .dc-ingest-card .form, | |
| .dc-ingest-card .block { | |
| margin-top: 0 !important; | |
| margin-bottom: 6px !important; | |
| } | |
| .dc-ingest-title { | |
| font-family: 'Space Grotesk', sans-serif; | |
| font-size: 16px; | |
| font-weight: 600; | |
| color: var(--ink); | |
| line-height: 1.2; | |
| margin-bottom: 2px; | |
| } | |
| .dc-ingest-subtitle { | |
| color: var(--ink3); | |
| font-size: 12px; | |
| font-family: 'Space Grotesk', sans-serif; | |
| margin-bottom: 10px; | |
| } | |
| /* Honest-expectations note under the Upload card subtitle — what the | |
| pipeline measures vs what the LLM infers. Kept quieter than the | |
| subtitle: it manages expectations, it shouldn't dominate. */ | |
| .dc-ingest-note { | |
| color: var(--ink3); | |
| font-size: 11px; | |
| font-family: 'JetBrains Mono', monospace; | |
| line-height: 1.5; | |
| margin-bottom: 10px; | |
| padding: 6px 9px; | |
| border-left: 2px solid var(--chipline); | |
| background: color-mix(in srgb, var(--paper3) 60%, transparent); | |
| border-radius: 0 6px 6px 0; | |
| } | |
| /* YouTube-link ingest row under the upload zone: compact single-line | |
| textbox + small Fetch button; status line stays quiet mono. */ | |
| .dc-yt-row { | |
| align-items: center; | |
| gap: 8px; | |
| margin-top: 8px; | |
| } | |
| .dc-yt-row textarea, .dc-yt-row input { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 12px; | |
| } | |
| .dc-yt-status { | |
| color: var(--ink3); | |
| font-size: 11px; | |
| font-family: 'JetBrains Mono', monospace; | |
| margin-top: 2px; | |
| } | |
| .dc-yt-status p { margin: 0; } | |
| /* ---- Journey strip — the one-line loop explainer under the top bar */ | |
| .dc-journey-strip { | |
| display: flex; | |
| align-items: baseline; | |
| flex-wrap: wrap; | |
| gap: 10px; | |
| padding: 9px 14px; | |
| margin: 2px 0 10px; | |
| border: 1px solid var(--line); | |
| border-radius: 10px; | |
| background: var(--paper2); | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 11.5px; | |
| color: var(--ink2); | |
| } | |
| .dc-journey-strip .step { white-space: nowrap; } | |
| .dc-journey-strip .num { | |
| display: inline-block; | |
| color: var(--coral); | |
| font-weight: 700; | |
| margin-right: 2px; | |
| } | |
| .dc-journey-strip .sub { color: var(--ink3); } | |
| .dc-journey-strip .arrow { color: var(--ink3); } | |
| .dc-journey-strip .nudge { | |
| margin-left: auto; | |
| color: var(--ink3); | |
| white-space: nowrap; | |
| } | |
| @media (max-width: 900px) { | |
| .dc-journey-strip .sub { display: none; } | |
| .dc-journey-strip .nudge { margin-left: 0; } | |
| } | |
| /* ---- File-upload zones — Codex move: stop chasing Gradio's native | |
| upload markup (icon class names + text strings keep shifting | |
| between v6 builds, leading to giant icons → blank zones). | |
| Instead: hide everything inside the upload <button>, then | |
| render our own prompt via ::after. The button stays clickable | |
| (pseudo-element gets pointer-events: none). */ | |
| .dc-upload-audio, | |
| .dc-import-file { | |
| min-height: 0 !important; | |
| } | |
| .dc-upload-audio button, | |
| .dc-import-file button { | |
| position: relative !important; | |
| min-height: 0 !important; | |
| display: flex !important; | |
| align-items: center !important; | |
| justify-content: center !important; | |
| } | |
| /* Native upload internals — icon, "Drop File Here", "- or -", | |
| "Click to Upload". All unreliable across Gradio v6 builds, so hide | |
| them inside the button only. */ | |
| .dc-upload-audio button .wrap, | |
| .dc-import-file button .wrap, | |
| .dc-upload-audio button .icon-wrap, | |
| .dc-import-file button .icon-wrap, | |
| .dc-upload-audio button .or, | |
| .dc-import-file button .or { | |
| display: none !important; | |
| } | |
| /* Our deliberate, stable prompt. */ | |
| .dc-upload-audio button::after, | |
| .dc-import-file button::after { | |
| display: flex !important; | |
| align-items: center !important; | |
| justify-content: center !important; | |
| width: 100%; | |
| height: 100%; | |
| color: var(--ink3); | |
| font-family: 'Space Grotesk', sans-serif; | |
| font-size: 18px; | |
| font-weight: 500; | |
| text-align: center; | |
| pointer-events: none; | |
| } | |
| .dc-upload-audio button::after { | |
| content: "Click to Upload"; | |
| } | |
| .dc-import-file button::after { | |
| content: "Drop or choose .abv1"; | |
| } | |
| /* ---- Tab strip — flat, centered, cap-styled section index. Reads as | |
| a navigation header rather than a button row. */ | |
| .tab-nav, .tabs > .tab-nav, [class*="tab-nav"] { | |
| background: transparent !important; | |
| border-bottom: 1px solid var(--line2) !important; | |
| padding: 6px 0 !important; | |
| margin-bottom: 14px !important; | |
| display: flex !important; | |
| justify-content: center !important; | |
| gap: 8px !important; | |
| } | |
| .tab-nav button, .tabs > .tab-nav button, [class*="tab-nav"] button { | |
| font-family: 'JetBrains Mono', monospace !important; | |
| font-size: 14px !important; | |
| font-weight: 600 !important; | |
| letter-spacing: 0.10em !important; | |
| text-transform: uppercase !important; | |
| padding: 10px 24px !important; | |
| color: var(--ink3) !important; | |
| background: transparent !important; | |
| border: 0 !important; | |
| border-bottom: 2px solid transparent !important; | |
| border-radius: 0 !important; | |
| transition: color 120ms ease, border-color 120ms ease !important; | |
| } | |
| .tab-nav button:hover, .tabs > .tab-nav button:hover, [class*="tab-nav"] button:hover { | |
| color: var(--ink2) !important; | |
| } | |
| .tab-nav button.selected, .tabs > .tab-nav button.selected, | |
| .tab-nav button[aria-selected="true"], [class*="tab-nav"] button[aria-selected="true"] { | |
| color: var(--coral) !important; | |
| background: transparent !important; | |
| border-bottom-color: var(--coral) !important; | |
| } | |
| /* ---- Markdown / prose — readable on dark */ | |
| .prose { | |
| max-width: none !important; | |
| color: var(--ink2); | |
| } | |
| .prose strong { color: var(--ink); } | |
| .prose a { color: var(--mint); } | |
| .prose code { | |
| background: var(--paper); | |
| border: 1px solid var(--line); | |
| border-radius: 4px; | |
| padding: 1px 5px; | |
| font-size: 12px; | |
| } | |
| """ | |
| # --------------------------------------------------------------------------- | |
| # Helper HTML snippets — reusable building blocks for the layout. | |
| # Functions return strings; callers wrap in gr.HTML(value=...). | |
| # --------------------------------------------------------------------------- | |
| def brand_html() -> str: | |
| """audio·brief wordmark + 4-bar icon.""" | |
| return ( | |
| '<div class="dc-brand">' | |
| '<span class="dc-brand-bars">' | |
| '<span class="b1"></span><span class="b2"></span>' | |
| '<span class="b3"></span><span class="b4"></span>' | |
| '</span>' | |
| 'audio<span class="dot">·</span>brief' | |
| '</div>' | |
| ) | |
| def palette_toggle_html() -> str: | |
| """Two-state palette segmented control: CONSOLE · DUB. | |
| Studio (light) was the v3 design's headline new palette but shipped | |
| with multiple Gradio v6 legibility regressions (see theme.py top). | |
| Removed from the toggle until we do a dedicated Studio pass with | |
| native button + input overrides. Spec preserved in theme.py. | |
| Click → set body class + persist to localStorage["abv1.palette"]. | |
| On load, a small inline script reads the saved palette and applies | |
| the class so the page paints the chosen world immediately (no | |
| flash of Console before swap). If localStorage still has "studio" | |
| from a previous build, the restore script falls back to console.""" | |
| on_click = ( | |
| "var t=this.closest('.dc-palette-toggle');" | |
| "if(!t)return;" | |
| "var pal=this.dataset.pal;" | |
| "try{localStorage.setItem('abv1.palette',pal);}catch(e){}" | |
| "document.body.classList.remove('pal-studio','pal-console','pal-dub');" | |
| "document.body.classList.add('pal-'+pal);" | |
| "t.querySelectorAll('button').forEach(function(b){" | |
| "b.classList.toggle('active',b.dataset.pal===pal);" | |
| "});" | |
| ) | |
| return ( | |
| '<div class="dc-palette-toggle" role="radiogroup" aria-label="Palette">' | |
| f'<button data-pal="console" class="active" title="Console — dark" ' | |
| f'aria-label="Console palette" onclick="{on_click}"></button>' | |
| f'<button data-pal="dub" title="Dub — warm dark" ' | |
| f'aria-label="Dub palette" onclick="{on_click}"></button>' | |
| '</div>' | |
| ) | |
| def pollen_pill_html(balance: float | None = None, connected: bool = False) -> str: | |
| """Top-right pollen balance pill — doubles as the wallet button. | |
| - Not connected: pill is coral-tinted with text "connect", clicking | |
| triggers the same-tab OAuth redirect to enter.pollinations.ai. | |
| - Connected: pill shows the live balance; clicking it dispatches a | |
| click to the hidden `#wallet-disconnect-trigger` button, which the | |
| Gradio app wires to the disconnect_wallet handler. | |
| `connected` lets a connected user see the pill even when balance is | |
| unknown (Pollinations userinfo doesn't always return a numeric balance). | |
| """ | |
| if not connected: | |
| # Click → open Pollinations OAuth in a NEW TAB. | |
| # | |
| # Why a new tab and not a same-tab nav: when this Space is embedded | |
| # inside HF's wrapper at huggingface.co/spaces/..., the iframe's | |
| # sandbox excludes `allow-top-navigation`, so `window.top.location = | |
| # u` is silently blocked. And a same-iframe navigation runs the | |
| # whole OAuth flow inside the iframe — but GitHub refuses to render | |
| # inside an iframe (X-Frame-Options: DENY), so Continue-with-GitHub | |
| # hits "github.com refused to connect". | |
| # | |
| # `window.open(u, '_blank')` is allowed by HF's sandbox (popups | |
| # are enabled) and opens a fresh top-level browser tab where | |
| # OAuth runs cleanly. The new tab lands on | |
| # kalamishere-audio-brief.hf.space/#api_key=sk_… — same Space, | |
| # top-level — and our demo.load JS captures the fragment on | |
| # that tab. The user then uses the Space from the new tab | |
| # (the wrapper tab can be closed). | |
| # If the popup is blocked (rare), fall back to same-tab navigation — | |
| # the user sees the OAuth flow run in the iframe again, which works | |
| # at least on the direct .hf.space URL even if it fails in the | |
| # huggingface.co wrapper. | |
| # client_id is the public App Key registered at | |
| # auth.pollinations.ai (pk_ prefix = publishable, safe to commit). | |
| # It identifies audio-brief to Pollinations so the issued user | |
| # key is scoped to this app. If Pollinations expects a different | |
| # param name than client_id (e.g. app_key, app_id), this is the | |
| # one line to swap. | |
| on_connect = ( | |
| "var ru=window.location.origin+window.location.pathname;" | |
| f"var u='https://enter.pollinations.ai/authorize?client_id={POLLINATIONS_APP_KEY}" | |
| "&redirect_url='" | |
| "+encodeURIComponent(ru)+'&expiry=30&permissions=usage';" | |
| "var w=window.open(u,'_blank');" | |
| "if(!w){window.location.href=u;}" | |
| ) | |
| return ( | |
| f'<div class="dc-pollen-pill not-connected" onclick="{on_connect}" ' | |
| 'title="Connect your Pollinations wallet">' | |
| '<span class="diamond">◇</span>' | |
| '<span class="balance">connect</span>' | |
| '<span class="lbl">POLLINATIONS</span>' | |
| '</div>' | |
| ) | |
| # Connected — click triggers the hidden disconnect Gradio button. | |
| on_disconnect = ( | |
| "var b=document.querySelector('#wallet-disconnect-trigger button')" | |
| "||document.querySelector('#wallet-disconnect-trigger');" | |
| "if(b)b.click();" | |
| ) | |
| if balance is not None: | |
| # Real balance — show two decimal places, label stays POLLEN. | |
| balance_text = f"{balance:.2f}" | |
| lbl = "POLLEN" | |
| tip = "Pollen remaining on your Pollinations wallet · click to disconnect" | |
| else: | |
| # No balance returned by userinfo — render the pill as a | |
| # "connected" badge so the user knows the OAuth succeeded. | |
| # Honest copy: we can't show the number Pollinations didn't send. | |
| balance_text = "✓" | |
| lbl = "CONNECTED" | |
| tip = "Wallet connected — Pollinations didn't return a balance number on userinfo. Click to disconnect." | |
| return ( | |
| f'<div class="dc-pollen-pill connected" onclick="{on_disconnect}" ' | |
| f'title="{tip}">' | |
| '<span class="diamond">◆</span>' | |
| f'<span class="balance">{balance_text}</span>' | |
| f'<span class="lbl">{lbl}</span>' | |
| '</div>' | |
| ) | |
| def session_spend_pill_html(spend: float, connected: bool) -> str: | |
| """Tiny pill that lives left of the wallet pill: `0.84 ◆ SA3 session`. | |
| Hidden when not connected (no signal to track), or when spend is 0 | |
| (the empty pill is noise — the wallet pill alone is enough). | |
| Label is `SA3 session` (not just `session`) per Codex feedback: the | |
| pill only tracks Pollinations SA3 audio-gen calls (Generate + | |
| Regenerate). Text-LLM calls fired from Analyse / Compare also burn | |
| pollen but aren't ticked here yet — Pollinations doesn't surface a | |
| per-request cost in the LLM response body and any estimate would be | |
| fiction. Renaming is the honest read; we can extend the tracker the | |
| day Pollinations exposes accurate per-call billing.""" | |
| if not connected or not spend or spend <= 0: | |
| return "" | |
| return ( | |
| '<div class="dc-session-pill" ' | |
| 'title="Pollen spent on SA3 audio gens this session — text LLM calls (Analyse / Compare) are NOT counted. Resets on disconnect or reload.">' | |
| f'<span class="amt">{spend:.2f}</span>' | |
| '<span class="diamond">◆</span>' | |
| '<span class="lbl">SA3 session</span>' | |
| '</div>' | |
| ) | |
| def cost_pip_html(cost: float, suffix: str = "per call · flat") -> str: | |
| """Inline cost pip — `~0.04 ◆ per call · flat`. Used next to Generate / | |
| Regenerate buttons so users see the cost before they click.""" | |
| return ( | |
| '<div class="dc-cost-pip">' | |
| f'<span>~</span><span class="amt">{cost:.2f}</span>' | |
| '<span class="diamond">◆</span>' | |
| f'<span class="sep">·</span><span>{suffix}</span>' | |
| '</div>' | |
| ) | |
| def metric_tile_html(label: str, value: str) -> str: | |
| """One cell in the 6-up metric grid.""" | |
| return ( | |
| f'<div class="dc-metric">' | |
| f'<div class="dc-metric-k">{label}</div>' | |
| f'<div class="dc-metric-v">{value}</div>' | |
| f'</div>' | |
| ) | |
| def metric_grid_html(metrics: list[tuple[str, str]]) -> str: | |
| """Render the full 6-metric grid given (label, value) pairs.""" | |
| cells = "".join(metric_tile_html(k, v) for k, v in metrics) | |
| return f'<div class="dc-metric-grid">{cells}</div>' | |