ACE-Music-Studio / theme.py
techfreakworm's picture
fix(ui): stop layout 'dance' on mobile — cage audio waveform width
de7d60a unverified
raw
history blame
40 kB
"""Brutalist Mono — pure black/white, no color accent.
The aesthetic identity of ACE Music Studio: precision, density, mono-
spaced labels, true neutral grays, and a single white accent rationed
across active states + the primary CTA. Inspired by record-sleeve
liner notes and a code editor's chrome.
Typography (selected per the frontend-design discipline):
- IBM Plex Sans — body, brand, helper text. Mechanically refined,
warmer than Inter, free via Google Fonts.
- IBM Plex Mono — labels, status indicator, brand period, metadata
JSON, all-caps small text. Distinctive character without going
novelty.
The wireframes at ``docs/superpowers/specs/mockups/`` remain the visual
source of truth. The mobile breakpoint at 640 px replaces the sidebar
with a horizontal scroll strip and crushes the outer Gradio padding so
the full 360 px viewport is actually usable.
"""
from __future__ import annotations
import gradio as gr
# --- Palette ----------------------------------------------------------------
BG = "#0A0A0A"
SURFACE = "#141414"
SURFACE_STRONG = "#000000"
SURFACE_RAISED = "#1A1A1A"
BORDER = "#1F1F1F"
BORDER_STRONG = "#2A2A2A"
INK = "#E5E5E5"
INK_MUTED = "#6B6B6B"
INK_FAINT = "#3F3F3F"
PRIMARY = "#FFFFFF"
HOVER_BG = "#1A1A1A"
RADIUS = "4px"
FONT_SANS = '"IBM Plex Sans", -apple-system, BlinkMacSystemFont, system-ui, sans-serif'
FONT_MONO = '"IBM Plex Mono", "JetBrains Mono", ui-monospace, Menlo, monospace'
def build_theme() -> gr.themes.Base:
"""Returns a Gradio theme keyed to Brutalist Mono tokens.
Uses IBM Plex Sans as the body font and IBM Plex Mono as the
monospace partner. Both are pulled from Google Fonts at runtime.
"""
return gr.themes.Base(
primary_hue=gr.themes.colors.gray,
neutral_hue=gr.themes.colors.gray,
font=[
gr.themes.GoogleFont("IBM Plex Sans"),
"system-ui",
"sans-serif",
],
font_mono=[
gr.themes.GoogleFont("IBM Plex Mono"),
"ui-monospace",
"monospace",
],
).set(
body_background_fill=BG,
body_text_color=INK,
background_fill_primary=BG,
background_fill_secondary=SURFACE,
block_background_fill=SURFACE,
block_border_color=BORDER,
block_border_width="1px",
block_radius=RADIUS,
block_label_text_color=INK_MUTED,
block_label_background_fill="transparent",
block_title_background_fill="transparent",
block_title_text_color=INK_MUTED,
input_background_fill=SURFACE_STRONG,
input_border_color=BORDER,
input_border_color_focus=PRIMARY,
input_placeholder_color=INK_FAINT,
button_primary_background_fill=PRIMARY,
button_primary_text_color=BG,
button_primary_background_fill_hover=PRIMARY,
button_secondary_background_fill=SURFACE_STRONG,
button_secondary_text_color=INK,
button_secondary_border_color=BORDER_STRONG,
border_color_primary=BORDER,
border_color_accent=BORDER_STRONG,
color_accent=PRIMARY,
color_accent_soft=SURFACE_RAISED,
)
# Note: all `!important` below is intentional — Gradio's svelte-hashed
# classes load AFTER our CSS in some browsers, and the framework's
# default rules sit at the same specificity as ours without it.
CSS = f"""
/* ============================================================
* Brutalist Mono — global palette + Gradio variable overrides
* Gradio's gr.themes.colors.gray maps to Tailwind slate-*, which has
* a perceptible blue tint on cool-temperature phone displays. Pin
* every neutral + surface variable to true monochrome hex.
* ============================================================ */
:root,
html.dark,
.gradio-container,
.gradio-container.dark,
.gradio-container .dark {{
--neutral-50: #FAFAFA !important;
--neutral-100: #F5F5F5 !important;
--neutral-200: #E5E5E5 !important;
--neutral-300: #D4D4D4 !important;
--neutral-400: #A3A3A3 !important;
--neutral-500: #737373 !important;
--neutral-600: #525252 !important;
--neutral-700: #404040 !important;
--neutral-800: #262626 !important;
--neutral-900: #141414 !important;
--neutral-950: #0A0A0A !important;
--body-background-fill: {BG} !important;
--background-fill-primary: {BG} !important;
--background-fill-secondary: {SURFACE} !important;
--block-background-fill: {SURFACE} !important;
--block-label-background-fill: transparent !important;
--block-title-background-fill: transparent !important;
--input-background-fill: {SURFACE_STRONG} !important;
--border-color-primary: {BORDER} !important;
--border-color-accent: {BORDER_STRONG} !important;
--color-accent: {PRIMARY} !important;
--body-text-color: {INK} !important;
--body-text-color-subdued: {INK_MUTED} !important;
--block-label-text-color: {INK_MUTED} !important;
--block-title-text-color: {INK_MUTED} !important;
--link-text-color: {INK} !important;
font-family: {FONT_SANS};
}}
body, .gradio-container {{
background: {BG} !important;
color: {INK} !important;
font-family: {FONT_SANS} !important;
font-feature-settings: "ss01", "ss03", "cv11";
}}
/* ============================================================
* Crush Gradio's default ``.app`` wrapper padding.
* Default ``.gradio-container > .app`` ships with 16px 32px which
* eats 64 px of the 360 px mobile viewport. Replace with a sane
* scale that respects the breakpoints.
* ============================================================ */
.gradio-container > .app,
.gradio-container .main.fillable {{
padding: 16px 20px !important;
max-width: none !important;
}}
@media (max-width: 640px) {{
.gradio-container > .app,
.gradio-container .main.fillable {{
padding: 8px 10px !important;
}}
}}
main, .contain {{
width: 100% !important;
max-width: none !important;
}}
/* ============================================================
* Header + CTA banner
* ============================================================ */
.ams-header {{
display:flex; justify-content:space-between; align-items:baseline;
padding:10px 2px 6px 2px;
}}
.ams-brand {{
font-family: {FONT_SANS};
font-size:17px; font-weight:600;
letter-spacing:-0.01em; color:{INK};
}}
.ams-brand-period {{ color:{PRIMARY}; font-family: {FONT_MONO}; }}
.ams-status {{
font-family: {FONT_MONO};
font-size:10px; color:{INK_MUTED};
letter-spacing:0.08em; text-transform:uppercase;
}}
.ams-cta {{
font-size:12px; color:{INK_MUTED};
margin:2px 2px 14px 2px; padding-bottom:10px;
border-bottom:1px solid {BORDER};
line-height:1.5;
}}
.ams-cta strong {{ color:{INK}; font-weight:600; }}
.ams-cta-heart {{ color:{PRIMARY}; }}
.ams-cta a {{ color:{INK}; text-decoration:underline; text-decoration-color:{BORDER_STRONG}; }}
/* ============================================================
* Body row (sidebar + content)
* ============================================================ */
.ams-body {{
gap:12px !important;
align-items:stretch !important;
/* Same flex-shrink fix as ``.ams-content``: without ``min-width: 0`` on
the children, a wide audio waveform inside the content column can
blow the row past the viewport on mobile. */
max-width:100% !important;
overflow:hidden !important;
}}
.ams-body > * {{
min-width:0 !important;
}}
/* ============================================================
* Sidebar — desktop ≥ 1024
* ============================================================ */
.ams-sidebar {{
background:{SURFACE_STRONG} !important;
padding:12px 6px !important;
border-radius:{RADIUS} !important;
border:1px solid {BORDER} !important;
min-width:188px;
max-width:210px;
}}
.ams-side-radio {{
background:transparent !important;
border:none !important;
padding:0 !important;
width:100%;
}}
.ams-side-radio > div > .wrap,
.ams-side-radio .wrap {{
display:flex !important;
flex-direction:column !important;
gap:2px !important;
background:transparent !important;
border:none !important;
}}
.ams-side-radio label {{
display:flex !important;
align-items:center !important;
padding:8px 11px !important;
margin:0 !important;
border-radius:3px !important;
border:none !important;
border-left:2px solid transparent !important;
background:transparent !important;
color:{INK_MUTED} !important;
font-family: {FONT_SANS} !important;
font-size:12px !important;
font-weight:500 !important;
letter-spacing:0.005em !important;
cursor:pointer !important;
transition:background 80ms ease, color 80ms ease, border-color 80ms ease;
min-height:0 !important;
width:100%;
box-sizing:border-box;
}}
.ams-side-radio label:hover {{
background:{HOVER_BG} !important;
color:{INK} !important;
}}
.ams-side-radio label input[type="radio"] {{
display:none !important;
}}
.ams-side-radio label.selected,
.ams-side-radio label:has(input[type="radio"]:checked) {{
background:{HOVER_BG} !important;
color:{PRIMARY} !important;
border-left-color:{PRIMARY} !important;
font-weight:600 !important;
}}
.ams-side-radio + div:empty {{ display:none !important; }}
/* History block (below the mode radio) */
.ams-history {{
margin-top:12px;
padding-top:8px;
border-top:1px solid {BORDER};
}}
.ams-history-title {{
font-family: {FONT_MONO};
font-size:9px; color:{INK_MUTED};
letter-spacing:0.12em; text-transform:uppercase;
padding:0 4px 6px 4px;
}}
.ams-history-empty {{
font-family: {FONT_SANS};
font-size:11px; color:{INK_FAINT};
font-style:italic;
padding:4px 4px;
}}
/* ============================================================
* Content pane
* ============================================================ */
.ams-content {{
background:{SURFACE} !important;
border:1px solid {BORDER} !important;
border-radius:{RADIUS} !important;
padding:14px !important;
min-height:540px;
/* Flex children default to ``min-width: auto`` which means they CANNOT
shrink below their content's intrinsic width. The wavesurfer.js
waveform renders pixel-perfect to the audio duration (e.g. a 60 s
clip wants ~600 px), which would push this column wider than the
viewport on mobile and cause the layout to "dance" between
pre-generation and post-generation widths. ``min-width: 0`` lets
the column shrink, and the audio block's own ``overflow: hidden``
clips the inner waveform. */
min-width:0 !important;
/* Match: the row child is also constrained so the audio waveform
can't push it out of bounds on mobile. */
max-width:100% !important;
overflow-x:hidden !important;
}}
.ams-tab-pane {{
background:transparent !important;
border:none !important;
padding:0 !important;
}}
/* Force the inner 2-column row inside each pane to actually stack
on narrow screens — Gradio gr.Row keeps row direction by default. */
@media (max-width: 768px) {{
.ams-tab-pane .row,
.ams-tab-pane [class*="row"][class*="svelte"] {{
flex-direction:column !important;
}}
}}
/* ============================================================
* Form field chrome — labels, helper info, inputs
* Gradio's defaults render labels and helper text at 14-16 px.
* Brutalist Mono wants labels small + uppercase + mono.
* ============================================================ */
/* Scope the small-uppercase-mono treatment to ONLY the label text
spans, NOT the entire <label> wrapper. Cascading text-transform
from a label wrapper would otherwise uppercase the helper text
(.info-text) and the input's own text. */
.ams-content span.has-info,
.ams-content span.svelte-jdcl7l,
.ams-content .block-label > span,
.ams-content [data-testid="block-label"] > span,
.ams-content .label-wrap > span:first-child {{
font-family: {FONT_MONO} !important;
font-size:10px !important;
letter-spacing:0.08em !important;
text-transform:uppercase !important;
color:{INK_MUTED} !important;
background:transparent !important;
border:none !important;
box-shadow:none !important;
padding:0 0 4px 0 !important;
margin:0 !important;
font-weight:500 !important;
display:block !important;
}}
/* Reset the <label> wrapper itself so it doesn't cascade transforms */
.ams-content label.container,
.ams-content label.svelte-1hguek3 {{
text-transform:none !important;
font-family:inherit !important;
font-size:inherit !important;
letter-spacing:normal !important;
display:block !important;
padding:0 !important;
}}
/* Helper text — Gradio 6.14 renders it as <div class="info-text …">.
Force sentence case + sans + small + faint italic. */
.ams-content .info-text,
.ams-content [class*="info-text"],
.ams-content .block .info,
.ams-content [data-testid="info"] {{
font-family: {FONT_SANS} !important;
font-size:10px !important;
color:{INK_FAINT} !important;
letter-spacing:0 !important;
text-transform:none !important;
font-style:italic !important;
padding:0 0 6px 0 !important;
line-height:1.4 !important;
font-weight:400 !important;
}}
.ams-content textarea,
.ams-content input[type="text"],
.ams-content input[type="number"] {{
background:{SURFACE_STRONG} !important;
border:1px solid {BORDER} !important;
border-radius:3px !important;
color:{INK} !important;
font-family: {FONT_SANS} !important;
font-size:13px !important;
padding:10px 12px !important;
line-height:1.5 !important;
}}
.ams-content textarea:focus,
.ams-content input[type="text"]:focus,
.ams-content input[type="number"]:focus {{
outline:none !important;
border-color:{PRIMARY} !important;
box-shadow:none !important;
}}
.ams-content textarea::placeholder,
.ams-content input::placeholder {{
color:{INK_FAINT} !important;
}}
.ams-content input[type="range"] {{
accent-color:{PRIMARY} !important;
}}
/* ============================================================
* Form Radio (Vocal mode) — compact pills, NOT the sidebar tabs
* ============================================================ */
.ams-content .block:has(input[type="radio"]) .wrap {{
display:flex !important;
flex-direction:column !important;
gap:6px !important;
}}
.ams-content .wrap > label {{
font-family: {FONT_SANS} !important;
font-size:12px !important;
text-transform:none !important;
letter-spacing:0 !important;
font-weight:500 !important;
color:{INK} !important;
/* Pure pitch-black background so cool-display tinting can't read
into a neutral grey as bluish. */
background:#000 !important;
border:1px solid {BORDER} !important;
border-radius:3px !important;
padding:7px 12px !important;
display:flex !important;
align-items:center !important;
gap:8px !important;
cursor:pointer !important;
}}
.ams-content .wrap > label:hover {{
border-color:{BORDER_STRONG} !important;
background:#0F0F0F !important;
}}
.ams-content .wrap > label:has(input[type="radio"]:checked) {{
border-color:{PRIMARY} !important;
background:#0F0F0F !important;
}}
/* Custom radio dot */
.ams-content .wrap > label input[type="radio"] {{
appearance:none !important;
-webkit-appearance:none !important;
width:12px !important; height:12px !important;
border:1px solid {BORDER_STRONG} !important;
border-radius:50% !important;
margin:0 !important;
flex-shrink:0 !important;
}}
.ams-content .wrap > label input[type="radio"]:checked {{
border-color:{PRIMARY} !important;
background: radial-gradient({PRIMARY} 0 4px, transparent 5px) !important;
}}
/* ============================================================
* Primary button (▶ Generate)
* ============================================================ */
.ams-content button.primary {{
background:{PRIMARY} !important;
color:{BG} !important;
border:none !important;
border-radius:3px !important;
font-family: {FONT_SANS} !important;
font-size:13px !important;
font-weight:600 !important;
letter-spacing:0.005em !important;
padding:11px 18px !important;
cursor:pointer !important;
transition:transform 80ms ease, opacity 80ms ease;
margin-top:6px !important;
}}
.ams-content button.primary:hover {{
opacity:0.92 !important;
transform:translateY(-1px);
}}
.ams-content button.primary:active {{
transform:translateY(0);
}}
/* ============================================================
* Output panel — gr.Audio (.ams-out-audio) and gr.JSON (.ams-out-meta)
* Targeted via the elem_classes hooks defined in ui.py so we don't
* have to chase svelte-hashed class names.
* ============================================================ */
.ams-content .ams-out {{
background:{SURFACE_STRONG} !important;
border:1px solid {BORDER} !important;
border-radius:3px !important;
padding:12px !important;
margin-top:10px !important;
}}
.ams-content .ams-out-audio {{
min-height:90px !important;
/* Wavesurfer's waveform renders one pixel per audio frame at the
``minPxPerSec`` rate; for a 60 s clip that can exceed the
column's width. Cage it so it can't push the parent wider. */
min-width:0 !important;
max-width:100% !important;
overflow:hidden !important;
}}
.ams-content .ams-out-audio .component-wrapper,
.ams-content .ams-out-audio .waveform-container,
.ams-content .ams-out-audio [data-testid^="waveform"],
.ams-content .ams-out-audio #waveform {{
width:100% !important;
max-width:100% !important;
min-width:0 !important;
overflow:hidden !important;
}}
/* Wavesurfer renders a <canvas> sized in CSS pixels from the audio
duration; force it to the wrapper's width on small screens. */
.ams-content .ams-out-audio canvas,
.ams-content .ams-out-audio wave {{
max-width:100% !important;
}}
.ams-content .ams-out-meta {{
min-height:80px !important;
font-family: {FONT_MONO} !important;
font-size:11px !important;
line-height:1.6 !important;
}}
.ams-content .ams-out-meta span {{
font-family: {FONT_MONO} !important;
font-size:11px !important;
}}
/* The Output/Metadata block labels live as <label class="svelte-19djge9">.
Force the Brutalist label treatment + uppercase regardless of the
svelte hash, since they're inside .ams-out. */
.ams-content .ams-out label,
.ams-content .ams-out label > span {{
font-family: {FONT_MONO} !important;
font-size:10px !important;
letter-spacing:0.08em !important;
text-transform:uppercase !important;
color:{INK_MUTED} !important;
background:transparent !important;
font-weight:500 !important;
padding:0 0 6px 0 !important;
display:block !important;
}}
/* Hide Gradio's tiny inline icon SVG that sits inside the Output /
Metadata label (an 11x11 music-note / braces glyph). The wireframe
doesn't show these; they overlap the label text and create a
"broken" feel. Note that the LARGE empty-state SVG (centered in
the panel body) lives inside ``.empty.svelte-v95lt3`` and is
spared by this selector since it's not a direct label child. */
.ams-content .ams-out label svg,
.ams-content .ams-out > label > span:first-child:has(svg) {{
display:none !important;
}}
/* The large empty-state SVG centered in the panel body — keep, but
make it visually softer (muted faint ink, low opacity). */
.ams-content .ams-out .empty svg,
.ams-content .ams-out [class*="empty"] svg {{
color:{INK_FAINT} !important;
opacity:0.5 !important;
}}
/* ============================================================
* Defeat Gradio's ``div.styler`` wrapper backgrounds.
* Gradio wraps every Row / Form in a <div class="styler svelte-..."
* which has background:var(--border-color-primary) = #1F1F1F by
* default. That produces visible slate-blue "bands" between the
* form rows (most obviously around the Generate button) on cool
* displays. Force it transparent everywhere inside our content.
* ============================================================ */
.ams-content .styler,
.ams-content [class*="styler"],
.ams-content .form > .styler {{
background:transparent !important;
border:none !important;
padding:0 !important;
}}
/* Defeat any residual Gradio block bg that might pull a slate value
despite our --neutral-* override (defensive, harmless if it's
already correct). */
.ams-content > .row,
.ams-content > .row > .column,
.ams-content .form {{
background:transparent !important;
}}
/* Add explicit gap between form column and output column when stacked */
@media (max-width: 768px) {{
.ams-tab-pane > .row,
.ams-tab-pane > [class*="row"] {{
gap:18px !important;
}}
.ams-content .block + .block {{
margin-top:4px !important;
}}
}}
/* ============================================================
* LoRA chip pill — kept for legacy use (chip-style hooks elsewhere)
* ============================================================ */
.ams-chip {{
display:inline-block; padding:5px 10px; border-radius:14px;
font-size:11px; margin:0 5px 5px 0; background:{SURFACE_STRONG};
border:1px solid {BORDER_STRONG}; color:{INK_MUTED}; cursor:pointer;
}}
.ams-chip.on {{ border-color:{PRIMARY}; color:{PRIMARY}; }}
.ams-chip.upload {{ border-style:dashed; color:{PRIMARY}; }}
/* ============================================================
* LoRA accordion (D5)
* The collapsed accordion sits between the duration/vocal-mode row
* and the Generate button. Inside: a note, preset radio, custom
* upload, strength slider, and an "Active: …" Markdown line.
* The outer chrome matches the wireframe's bordered section header.
* ============================================================ */
.ams-content .ams-lora {{
border:1px solid {BORDER} !important;
border-radius:3px !important;
background:{SURFACE_STRONG} !important;
margin-top:10px !important;
padding:0 !important;
}}
/* Accordion summary / label. Gradio 6.14 renders this as either
.label-wrap (older builds) or a native <summary> element. Style
both so the uppercase-mono header is consistent. */
.ams-content .ams-lora > .label-wrap,
.ams-content .ams-lora summary,
.ams-content .ams-lora > button {{
font-family: {FONT_MONO} !important;
font-size:10px !important;
letter-spacing:0.08em !important;
text-transform:uppercase !important;
color:{INK_MUTED} !important;
padding:10px 12px !important;
background:transparent !important;
border:none !important;
}}
.ams-content .ams-lora > .label-wrap span,
.ams-content .ams-lora summary span,
.ams-content .ams-lora > button span {{
color:{INK_MUTED} !important;
font-family: {FONT_MONO} !important;
font-size:10px !important;
letter-spacing:0.08em !important;
text-transform:uppercase !important;
}}
/* Italic note under the header */
.ams-content .ams-lora-note p {{
font-family: {FONT_SANS} !important;
font-size:10px !important;
font-style:italic !important;
color:{INK_FAINT} !important;
line-height:1.4 !important;
margin:0 0 10px 0 !important;
padding:0 12px !important;
}}
/* The expanded body padding — Gradio drops the children inside
.gap (or unnamed wrapper) directly. Use a left/right padding so
the radio + file + slider don't hug the border. */
.ams-content .ams-lora > div:not(.label-wrap):not(summary) {{
padding:0 12px 12px 12px !important;
}}
/* Preset radio: row of compact pills. Gradio renders the radio body
as ``fieldset.ams-lora-preset > div.wrap.svelte-e4x47i > label*``.
The generic Vocal-mode rule
.ams-content .block:has(input[type="radio"]) .wrap
computes specificity (0,4,1) — three classes + the inner attribute
selector via :has. To beat that we chain ``.ams-content .ams-lora
.ams-lora-preset.ams-lora-preset > .wrap`` which is (0,5,0), winning
by one class. */
.ams-content .ams-lora .ams-lora-preset.ams-lora-preset > .wrap {{
display:flex !important;
flex-direction:row !important;
flex-wrap:wrap !important;
gap:6px !important;
background:transparent !important;
border:none !important;
padding:0 !important;
width:100% !important;
}}
.ams-content .ams-lora .ams-lora-preset.ams-lora-preset > .wrap > label {{
flex:0 0 auto !important;
width:auto !important;
max-width:max-content !important;
min-width:0 !important;
background:#000 !important;
border:1px solid {BORDER} !important;
border-radius:14px !important;
padding:5px 12px !important;
font-size:11px !important;
color:{INK_MUTED} !important;
font-weight:500 !important;
display:inline-flex !important;
align-items:center !important;
gap:0 !important;
cursor:pointer !important;
}}
.ams-content .ams-lora .ams-lora-preset.ams-lora-preset > .wrap > label:hover {{
color:{INK} !important;
border-color:{BORDER_STRONG} !important;
}}
.ams-content .ams-lora .ams-lora-preset.ams-lora-preset > .wrap > label:has(input[type="radio"]:checked) {{
border-color:{PRIMARY} !important;
color:{PRIMARY} !important;
background:#0F0F0F !important;
}}
/* Hide the inner radio-dot input; the pill border + color carries
the on/off state on its own. */
.ams-content .ams-lora .ams-lora-preset.ams-lora-preset > .wrap > label input[type="radio"] {{
display:none !important;
width:0 !important; height:0 !important;
margin:0 !important; padding:0 !important;
background:none !important;
border:none !important;
}}
.ams-content .ams-lora .ams-lora-preset.ams-lora-preset > .wrap > label span {{
text-transform:none !important;
letter-spacing:0 !important;
font-family: {FONT_SANS} !important;
font-size:11px !important;
color:inherit !important;
font-weight:500 !important;
}}
/* Custom-upload file widget. Gradio 6.14 renders the drop-zone as
``button.svelte-8prmba`` (NOT ``.upload-container``). The label
above it is ``label.svelte-19djge9.float`` which carries the
uploaded-file metadata once a file is dropped. Style the actual
drop-button and override the legacy ``.upload-container`` rule
from above for forward compatibility. */
.ams-content .ams-lora-file > button {{
min-height:80px !important;
background:#000 !important;
border:1px dashed {BORDER_STRONG} !important;
border-radius:3px !important;
color:{INK_MUTED} !important;
padding:14px 12px !important;
}}
.ams-content .ams-lora-file > button:hover {{
border-color:{PRIMARY} !important;
color:{INK} !important;
}}
.ams-content .ams-lora-file > button .or {{
font-family: {FONT_MONO} !important;
font-size:10px !important;
color:{INK_FAINT} !important;
letter-spacing:0.04em !important;
}}
.ams-content .ams-lora-file > button .icon-wrap svg {{
color:{INK_MUTED} !important;
opacity:0.7 !important;
width:18px !important;
height:18px !important;
}}
/* The floating label that appears once a file is uploaded — give it
the standard Brutalist mono treatment and hide its decorative SVG
so the label text reads cleanly. */
.ams-content .ams-lora-file > label.float {{
font-family: {FONT_MONO} !important;
font-size:10px !important;
letter-spacing:0.08em !important;
text-transform:uppercase !important;
color:{INK_MUTED} !important;
background:transparent !important;
border:none !important;
padding:0 0 6px 0 !important;
}}
.ams-content .ams-lora-file > label.float svg {{
display:none !important;
}}
/* Strength slider — the .info text just below it inherits the
generic helper rule, so no extra work needed. */
.ams-content .ams-lora-strength input[type="range"] {{
accent-color:{PRIMARY} !important;
}}
/* Active LoRA display — high-contrast block, mono font, code in
white so the LoRA name pops. The accordion's own background is
SURFACE_STRONG (true black), so use a slightly raised surface
here to make the bordered box visible. */
.ams-content .ams-lora-active .prose p {{
font-family: {FONT_MONO} !important;
font-size:11px !important;
color:{INK} !important;
background:{SURFACE_RAISED} !important;
border:1px solid {BORDER_STRONG} !important;
border-radius:3px !important;
padding:8px 10px !important;
margin:8px 0 0 0 !important;
line-height:1.5 !important;
}}
.ams-content .ams-lora-active .prose code {{
background:transparent !important;
color:{PRIMARY} !important;
font-family: {FONT_MONO} !important;
font-size:11px !important;
padding:0 !important;
}}
.ams-content .ams-lora-active .prose em {{
color:{INK_FAINT} !important;
font-style:italic !important;
font-family: {FONT_SANS} !important;
}}
@media (max-width: 640px) {{
.ams-content .ams-lora > .label-wrap,
.ams-content .ams-lora summary,
.ams-content .ams-lora > button {{
font-size:9px !important;
padding:8px 10px !important;
}}
.ams-content .ams-lora-note p {{
font-size:9px !important;
padding:0 10px !important;
}}
.ams-content .ams-lora > div:not(.label-wrap):not(summary) {{
padding:0 10px 10px 10px !important;
}}
.ams-content .ams-lora-active .prose p {{
font-size:10px !important;
padding:6px 8px !important;
}}
.ams-content .ams-lora .ams-lora-preset.ams-lora-preset > .wrap > label {{
font-size:10px !important;
padding:5px 9px !important;
}}
.ams-content .ams-lora-file > button {{
min-height:64px !important;
padding:10px 8px !important;
}}
}}
/* ============================================================
* Audio upload widget (Cover / Extend / Edit reference inputs)
* Tagged with ``ams-input-audio`` via elem_classes. Match the dark
* input chrome so it sits next to the textboxes without contrast
* jumps; the gr.Audio drop-button gets the same dashed outline as
* the LoRA upload so users recognise it as a drop-zone.
* ============================================================ */
.ams-content .ams-input-audio {{
background:{SURFACE_STRONG} !important;
border:1px solid {BORDER} !important;
border-radius:3px !important;
padding:8px !important;
margin-bottom:4px !important;
}}
.ams-content .ams-input-audio .empty,
.ams-content .ams-input-audio [class*="empty"] {{
min-height:90px !important;
}}
.ams-content .ams-input-audio button {{
background:#000 !important;
border:1px dashed {BORDER_STRONG} !important;
border-radius:3px !important;
color:{INK_MUTED} !important;
font-family: {FONT_MONO} !important;
font-size:10px !important;
letter-spacing:0.04em !important;
}}
.ams-content .ams-input-audio button:hover {{
border-color:{PRIMARY} !important;
color:{INK} !important;
}}
.ams-content .ams-input-audio svg {{
color:{INK_MUTED} !important;
opacity:0.7 !important;
}}
/* ============================================================
* Experimental accordion (Extend / Edit)
* Reuse the LoRA accordion's visual chrome so the bordered section
* shape is consistent across all accordions, but visually demote
* the summary so users can tell these knobs aren't fully wired.
* ============================================================ */
.ams-content .ams-experimental {{
border:1px solid {BORDER} !important;
border-radius:3px !important;
background:{SURFACE_STRONG} !important;
margin-top:10px !important;
padding:0 !important;
}}
.ams-content .ams-experimental > .label-wrap,
.ams-content .ams-experimental summary,
.ams-content .ams-experimental > button {{
font-family: {FONT_MONO} !important;
font-size:10px !important;
letter-spacing:0.08em !important;
text-transform:uppercase !important;
color:{INK_MUTED} !important;
padding:10px 12px !important;
background:transparent !important;
border:none !important;
opacity:0.7 !important;
}}
.ams-content .ams-experimental > .label-wrap span,
.ams-content .ams-experimental summary span,
.ams-content .ams-experimental > button span {{
color:{INK_MUTED} !important;
font-family: {FONT_MONO} !important;
font-size:10px !important;
letter-spacing:0.08em !important;
text-transform:uppercase !important;
}}
.ams-content .ams-experimental > div:not(.label-wrap):not(summary) {{
padding:0 12px 12px 12px !important;
}}
/* ============================================================
* Lyrics tab (M4)
* Mono draft textbox + secondary "Use in Generate" CTA. The LM-params
* accordion reuses the same chrome as the LoRA + experimental
* accordions so the bordered section header reads consistently.
* ============================================================ */
.ams-content .ams-lyrics-output textarea {{
font-family: {FONT_MONO} !important;
font-size: 12px !important;
line-height: 1.6 !important;
min-height: 280px !important;
background:{SURFACE_STRONG} !important;
border:1px solid {BORDER} !important;
color:{INK} !important;
}}
.ams-content .ams-lyrics-output textarea::placeholder {{
font-style: italic;
}}
.ams-content .ams-lyrics-use-btn {{
margin-top: 6px !important;
}}
.ams-content .ams-lm-accordion {{
border:1px solid {BORDER} !important;
border-radius:3px !important;
background:{SURFACE_STRONG} !important;
margin-top:10px !important;
padding:0 !important;
}}
.ams-content .ams-lm-accordion > .label-wrap,
.ams-content .ams-lm-accordion summary,
.ams-content .ams-lm-accordion > button {{
font-family: {FONT_MONO} !important;
font-size:10px !important;
letter-spacing:0.08em !important;
text-transform:uppercase !important;
color:{INK_MUTED} !important;
padding:10px 12px !important;
background:transparent !important;
border:none !important;
}}
.ams-content .ams-lm-accordion > .label-wrap span,
.ams-content .ams-lm-accordion summary span,
.ams-content .ams-lm-accordion > button span {{
color:{INK_MUTED} !important;
font-family: {FONT_MONO} !important;
font-size:10px !important;
letter-spacing:0.08em !important;
text-transform:uppercase !important;
}}
.ams-content .ams-lm-accordion > div:not(.label-wrap):not(summary) {{
padding:0 12px 12px 12px !important;
}}
/* ============================================================
* Post-process action row (M5/G2) — sits below the Output Audio.
* Three compact mono pills (separate stems / normalise / mp3 export)
* that surface hidden gr.Files / gr.Audio / gr.File widgets once a
* post-process click handler returns. The bordered list chrome on
* stem_files + mp3_file matches the generic .ams-out treatment so
* the populated state reads as a continuation of the Output panel.
* ============================================================ */
.ams-content .ams-post-actions {{
gap: 6px !important;
margin: 8px 0 0 0 !important;
}}
.ams-content .ams-post-btn {{
font-family: {FONT_MONO} !important;
font-size: 11px !important;
letter-spacing: 0.04em !important;
padding: 8px 10px !important;
background: #000 !important;
border: 1px solid {BORDER} !important;
color: {INK} !important;
border-radius: 3px !important;
}}
.ams-content .ams-post-btn:hover {{
border-color: {PRIMARY} !important;
}}
/* Stem files + mp3 file widgets — compact bordered list */
.ams-content .ams-stem-files,
.ams-content .ams-mp3-file {{
background: #000 !important;
border: 1px solid {BORDER} !important;
border-radius: 3px !important;
margin-top: 6px !important;
}}
@media (max-width: 640px) {{
.ams-content .ams-post-btn {{
font-size: 10px !important;
padding: 7px 8px !important;
}}
}}
/* ============================================================
* History rows — clickable-looking compact list (M6/H2)
* Replaces the static "No generations yet" placeholder with a live
* in-memory feed of mode + label tuples. The mode segment renders in
* mono uppercase to mirror the small uppercase labels used throughout
* the sidebar; the label segment uses the sans body face and truncates
* with ellipsis at the sidebar's compact 188-210 px width.
* ============================================================ */
.ams-content .ams-history-wrapper {{
margin-top: 4px;
}}
.ams-history-row {{
display: flex;
gap: 6px;
align-items: baseline;
font-family: {FONT_MONO};
font-size: 10px;
color: {INK_MUTED};
padding: 4px 6px;
border-radius: 3px;
cursor: default;
}}
.ams-history-row:hover {{
background: {HOVER_BG};
color: {INK};
}}
.ams-history-mode {{
color: {PRIMARY};
text-transform: lowercase;
letter-spacing: 0;
flex-shrink: 0;
}}
.ams-history-label {{
color: {INK_MUTED};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: {FONT_SANS};
font-size: 11px;
}}
@media (max-width: 640px) {{
.ams-history,
.ams-history-wrapper {{
display: none !important;
}}
}}
/* Hide Gradio footer + the floating "Use via API" / settings panel */
footer {{ display:none !important; }}
.show-api {{ display:none !important; }}
.built-with {{ display:none !important; }}
/* ============================================================
* Responsive: tablet 640-1024 px
* Keep the full sidebar (with labels) — the icon-rail middle state
* fights Gradio's DOM. Just narrow it slightly.
* ============================================================ */
@media (min-width: 641px) and (max-width: 1024px) {{
.ams-sidebar {{ min-width:160px; max-width:180px; padding:10px 6px !important; }}
.ams-side-radio label {{ font-size:11px !important; padding:7px 9px !important; }}
}}
/* ============================================================
* Responsive: mobile < 640 px
* Sidebar becomes a horizontal scroll pill strip at the top.
* Form + Output stack with proper gap.
* ============================================================ */
@media (max-width: 640px) {{
.ams-body {{
flex-direction:column !important;
gap:8px !important;
}}
.ams-sidebar {{
min-width:100% !important;
max-width:100% !important;
padding:2px 0 !important;
border:none !important;
background:transparent !important;
border-radius:0 !important;
}}
.ams-side-radio .wrap {{
flex-direction:row !important;
flex-wrap:nowrap !important;
overflow-x:auto !important;
overflow-y:hidden !important;
gap:6px !important;
padding:2px 0 !important;
scrollbar-width:none !important;
-ms-overflow-style:none !important;
}}
.ams-side-radio .wrap::-webkit-scrollbar {{ display:none !important; }}
.ams-side-radio label {{
width:auto !important;
min-width:0 !important;
max-width:max-content !important;
flex:0 0 auto !important;
font-family: {FONT_SANS} !important;
font-size:11px !important;
font-weight:600 !important;
white-space:nowrap !important;
padding:7px 12px !important;
background:{SURFACE_STRONG} !important;
border:1px solid {BORDER} !important;
border-radius:3px !important;
justify-content:center !important;
}}
.ams-side-radio label.selected,
.ams-side-radio label:has(input[type="radio"]:checked) {{
border-color:{PRIMARY} !important;
background:{SURFACE_RAISED} !important;
color:{PRIMARY} !important;
}}
.ams-history {{ display:none !important; }}
/* Header + CTA tighter */
.ams-header {{ padding:2px 2px 2px 2px !important; }}
.ams-brand {{ font-size:14px !important; }}
.ams-status {{ font-size:9px !important; }}
.ams-cta {{ font-size:11px !important; margin:0 2px 8px 2px !important; padding-bottom:8px !important; }}
/* Content pane tighter */
.ams-content {{ padding:12px !important; border-radius:3px !important; }}
/* Field labels + info shrink further */
.ams-content label,
.ams-content .block-label,
.ams-content [data-testid="block-label"],
.ams-content span.svelte-jdcl7l,
.ams-content .label-wrap {{
font-size:9px !important;
}}
.ams-content .block .info,
.ams-content [data-testid="info"] {{
font-size:9px !important;
padding-bottom:4px !important;
}}
.ams-content textarea,
.ams-content input[type="text"],
.ams-content input[type="number"] {{
font-size:12px !important;
padding:8px 10px !important;
}}
.ams-content .wrap > label {{
padding:6px 10px !important;
font-size:11px !important;
}}
.ams-content button.primary {{
padding:11px 14px !important;
font-size:12px !important;
}}
.ams-content [data-testid="audio"],
.ams-content .audio-container,
.ams-content .json-holder {{
min-height:64px !important;
}}
}}
"""