audio-brief / theme.py
kalamishere's picture
feat(analyse): video-file ingest + YouTube-link fetch
622d4a4
Raw
History Blame Contribute Delete
69.6 kB
"""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>'